pax_global_header00006660000000000000000000000064150074031240014506gustar00rootroot0000000000000052 comment=c414f744e0f01c2e3d351fd9f4376a72b6c37d68 Flametree-0.2.1/000077500000000000000000000000001500740312400134125ustar00rootroot00000000000000Flametree-0.2.1/.codeclimate.yml000066400000000000000000000000671500740312400164670ustar00rootroot00000000000000exclude_patterns: - "ez_setup.py" - "tests/" - "docs/" Flametree-0.2.1/.github/000077500000000000000000000000001500740312400147525ustar00rootroot00000000000000Flametree-0.2.1/.github/workflows/000077500000000000000000000000001500740312400170075ustar00rootroot00000000000000Flametree-0.2.1/.github/workflows/build.yml000066400000000000000000000015111500740312400206270ustar00rootroot00000000000000name: build on: [push, workflow_dispatch] jobs: build: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install dependencies run: | python -m pip install --upgrade pip pip install pytest pytest-cov coveralls pip install matplotlib pandas weasyprint - name: Install run: | pip install -e . - name: Test with pytest run: | python -m pytest --cov flametree --cov-report term-missing - name: Coveralls uses: coverallsapp/github-action@v2 continue-on-error: true with: github-token: ${{ secrets.GITHUB_TOKEN }} env: COVERALLS_SERVICE_NAME: github Flametree-0.2.1/.github/workflows/document.yml000066400000000000000000000007251500740312400213540ustar00rootroot00000000000000name: documentation on: workflow_dispatch: release: types: [created] jobs: document: runs-on: ubuntu-24.04 environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} permissions: pages: write id-token: write steps: - id: deployment uses: sphinx-notes/pages@v3 with: python_version: 3.12 documentation_path: ./docs requirements_path: ./docs/requirements.txt Flametree-0.2.1/.github/workflows/publish.yml000066400000000000000000000017361500740312400212070ustar00rootroot00000000000000name: Publish to PyPI on: release: types: [created] jobs: test: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install run: | pip install pytest pip install matplotlib pandas weasyprint pip install -e . - name: Test run: | python -m pytest deploy: runs-on: ubuntu-24.04 needs: [test] permissions: id-token: write steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.12' cache: pip cache-dependency-path: '**/pyproject.toml' - name: Install dependencies run: | pip install setuptools wheel build - name: Build run: | python -m build - name: Publish uses: pypa/gh-action-pypi-publish@release/v1 Flametree-0.2.1/.gitignore000066400000000000000000000034071500740312400154060ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ Flametree-0.2.1/.vscode/000077500000000000000000000000001500740312400147535ustar00rootroot00000000000000Flametree-0.2.1/.vscode/settings.json000066400000000000000000000000551500740312400175060ustar00rootroot00000000000000{ "python.formatting.provider": "black" }Flametree-0.2.1/LICENSE000066400000000000000000000021221500740312400144140ustar00rootroot00000000000000MIT License Copyright (c) 2017 Edinburgh Genome Foundry, University of Edinburgh Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Flametree-0.2.1/README.rst000066400000000000000000000254611500740312400151110ustar00rootroot00000000000000.. raw:: html

Flametree Logo

Python file operations made easy

.. image:: https://github.com/Edinburgh-Genome-Foundry/Flametree/actions/workflows/build.yml/badge.svg :target: https://github.com/Edinburgh-Genome-Foundry/Flametree/actions/workflows/build.yml :alt: GitHub CI build status .. image:: https://coveralls.io/repos/github/Edinburgh-Genome-Foundry/Flametree/badge.svg :target: https://coveralls.io/github/Edinburgh-Genome-Foundry/Flametree Flametree is a Python library which provides a simple syntax for handling files and folders (no ``os.path.join``, ``os.listdir`` etc.), and works the same way for different file systems. Write a Flametree program to read/write files in disk folders, and your code will also be able to read/write in zip archives and virtual (in-memory) archives - which is particularly useful on web servers. As an illustration, here is how to use Flametree to read a file ``texts/poems/the_raven.txt``, replace all occurences of the word "raven" by "seagull" in the text, and write the result to a new file ``the_seagull.txt`` in the same folder: .. code:: python from flametree import file_tree with file_tree("texts") as root: poem_text = root.poems.the_raven_txt.read() new_text = poem_text.replace("raven", "seagull") root.poems._file("the_seagull.txt").write(new_text) Even in this very simple use case, the syntax is clearer than the ``os`` way, which would write as follows: .. code:: python import os with open(os.path.join("poems", "the_raven.txt"), "r") as f: poem_text = f.read() new_text = poem_text.replace("raven", "seagull") with open(os.path.join("poems", "the_seagull.txt"), "w") as f: content = f.write(new_text) Moreover, the same Flametree code also works for files inside a zip archive: .. code:: python with file_tree("my_archive.zip") as root: poem_text = root.poems.the_raven_txt.read() new_text = poem_text.replace("raven", "seagull") root.poems._file("the_seagull.txt").write(new_text) Now in hard mode: suppose that your server receives binary zip data of an archive containing ``poems/the_raven.txt``, and must return back a new zip containing a file ``poems/the_seagull.txt``. Here again, the syntax of the core operations is the same: .. code:: python destination_zip = file_tree("@memory") # Create a new virtual zip with file_tree(the_raven_zip_data) as root: poem_text = root.poems.the_raven_txt.read() new_text = poem_text.replace("raven", "seagull") destination_zip._dir("poems")._file("the_seagull.txt").write(new_text) destination_zip_data = destination_zip._close() # Now send the data to the client See section *Usage* below for more examples and features. Installation ------------ Flametree should work on Windows/Max/Linux, with Python 2 and 3, and has no external dependency. It can be installed by unzipping the source code in one directory and using this command: :: python setup.py install You can also install it directly from the Python Package Index with this command: :: pip install flametree Contribute ---------- Flametree is an open-source software originally written by Zulko_ and released on Github_ under the MIT licence (Copyright 2017 Edinburgh Genome Foundry, University of Edinburgh). Everyone is welcome to contribute! In particular if you have ideas of new kinds of file systems to add to Flametree. Usage ----- Opening a file tree ~~~~~~~~~~~~~~~~~~~ Here is how you open different kinds of file systems: .. code:: python from flametree import file_tree # Open a directory from the disk's file system: root = file_tree("my_folder/") # Open a zip archive on the disk: root = file_tree("my_archive.zip") # Connect to a file-like object (file handle, StringIO...) of a zip: root = file_tree(file_like_object) # Create a virtual 'in-memory' zip file: root = file_tree("@memory") # Open some data string representing a zip to read root = file_tree(some_big_zip_data_string) In the two first examples, if ``my_folder`` or ``my_archive.zip`` do not exist, they will be automatically created. If they do exist, it is possible to completely overwrite them with the option ``replace=True``. Exploring a file tree: ~~~~~~~~~~~~~~~~~~~~~~ Once you have created the ``root`` element with one of the methods above, you can display the whole file tree with ``root._tree_view()`` : .. code:: >>> print (root._tree_view()) texts/ poems/ dover_beach.txt the_raven.txt the_tyger.txt todo_list.txt figures/ figure1.png figure2.png Readme.md The attributes of a directory like ``root`` are its files and subdirectories. For instance to print the content of ``dover_beach.txt`` you would write: .. code:: python print( root.texts.poems.dover_beach_txt.read() ) or even simpler: .. code:: python root.texts.poems.dover_beach_txt.print_content() Notice that the ``.`` before ``txt`` was replaced by ``_`` so as to form a valid attribute name. This syntactic sugar is particularly useful to explore a file tree in IPython Notebooks or other editors offering auto-completion: .. image:: https://raw.githubusercontent.com/Edinburgh-Genome-Foundry/Flametree/master/images/autocomplete.png :alt: [illustration] :align: center Alternatively, you can access files and directories using dictionary calls: .. code:: python root["texts"]["poems"]["dover_beach.txt"] To iterate through the subdirectories of a directory, use the ``_dirs`` attribute: .. code:: python for subdirectory in root._dirs: print (subdirectory._name) # Will print 'texts' and 'figures' To iterate through the files of a directory, use the ``_files`` attribute: .. code:: python for f in root.figures._files: print (f._name) # Will print 'figure1.png' and 'figure2.png' Finally, use ``_all_files`` to iterate through all files nested in a directory. The snippet below prints the content of all ``.txt`` files in the file tree: .. code:: python for f in root._all_files: if f._name.endswith(".txt"): f.print_content() Creating files and folders ~~~~~~~~~~~~~~~~~~~~~~~~~~ To create a new subdirectory use ``_dir``: .. code:: python root._dir("data") # create a 'data' folder at the root root.data._dir("reports") # create a 'reports' folder under `root/data` To create a new file use ``_file``: .. code:: python root._file("joke.txt") # create a 'joke.txt' file at the root. root.texts._file("hello.txt") # create 'hello.txt' in `root/texts`. To write content in a file, use ``.write``: .. code:: python root.joke_txt.write("A plateau is the highest form of flattery.") Writing to a file will use mode ``a`` (append) by default. To overwrite the file set the write mode to ``"w"``. Let's erase and rewrite that ``joke.txt``: .. code:: python root.joke_txt.write("'DNA' stands for National Dyslexic Association.", "w") File and directory creation commands can be chained. Let us create some new folders ``data/`` and ``data/test_1/``, and write to file ``data/test_1/values.csv``, all in a single line: .. code:: python root._dir("data")._dir("test_1")._file("values.csv").write("1,15,25") Beware that ``._dir`` and ``._file`` **overwrite their target by default**, which means that if you write: .. code:: python root._dir("data")._file("values_1.csv").write("1,4,7") root._dir("data")._file("values_2.csv").write("2,9,7") The directory ``data`` will only contain ``values_2.csv``, because the second line's ``_dir("data")`` erases the ``data`` directory and starts a new one. To avoid this, either use ``root.data`` in the second line: .. code:: python root._dir("data")._file("values_1.csv").write("1,4,7") root.data._file("values_2.csv").write("2,9,7") Or use ``replace=False`` in ``_dir``: .. code:: python root._dir("data")._file("values_1.csv").write("1,4,7") root._dir("data", replace=False)._file("values_2.csv").write("2,9,7") Other operations ~~~~~~~~~~~~~~~~ You can move, copy, and delete a file with ``.move(folder)``, ``.copy(folder)``, ``.delete()``, and a directory with ``._move(folder)``, ``._copy(folder)``, ``._delete()``. .. code:: python root.data.values1_csv.delete() # delete file 'values1.csv' root.data._delete() # delete directory 'data' # Move folder `plots` from `root/figures` to `other_root/figures` root.figures.plots._move(other_root.figures) # Move file `fig.png` from `root/figures` to `other_root/figures` root.figures.fig_png.move(other_root.figures) Special rules for ZIP archives ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ It is not currently possible to modify/delete a file that is already zipped into an archive (because zips are not really made for that, it would be doable but would certainly be a hack). When creating files and folders in a zip with Flametree, the changes in the actual zip will only be performed by closing the ``root`` with ``root._close()`` (after which the ``root`` can't be used any more). If it is an in-memory zip, ``root._close()`` returns the value of the zip content as a string (Python 2) or bytes (Python 3). Here are a few examples: .. code:: python root = file_tree("archive.zip") root._file("hello.txt").write("Hi there !") root._close() # Equivalent to the previous, using `with`: with file_tree("archive.zip") as root: root._file("hello.txt").write("Hi there !") # Getting binary data of an in-memory zip file: root = file_tree("@memory") root._file("hello.txt").write("Hi there !") binary_data = root._close() Using file writers from other libraries ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Some libraries have file-generating methods which expect a file name or a file object to write too. You can also feed Flametree files to these functions. for instance here is how to use Weasyprint to create a PDF ``pdfs/report.pdf`` .. code:: python import weasyprint from flametree import file_tree root = file_tree(".") # or 'archive.zip' to write in an archive. html = weasyprint.HTML(string="Hello world!", base_url='.') html.write_pdf(root._dir("pdfs")._file("test.pdf")) And here is how you would save a Matplotlib figure in a zip archive: .. code:: python import matplotlib.pyplot as plt from flametree import file_tree fig, ax = plt.subplots(1) ax.plot([1, 2, 3], [3, 1, 2]) with file_tree("archive.zip") as root: fig.savefig(root._dir("plots")._file("figure.png"), format="png") That's all folks ! .. _Zulko: https://github.com/Zulko/ .. _Github: https://github.com/Edinburgh-Genome-Foundry/flametree Flametree-0.2.1/docs/000077500000000000000000000000001500740312400143425ustar00rootroot00000000000000Flametree-0.2.1/docs/Makefile000066400000000000000000000011721500740312400160030ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) Flametree-0.2.1/docs/conf.py000066400000000000000000000022271500740312400156440ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html import os import sys sys.path.insert(0, os.path.abspath("../flametree/")) # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = "flametree" copyright = "2017 Edinburgh Genome Foundry, University of Edinburgh" author = "Zulko" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ "sphinx.ext.autodoc", "sphinx.ext.napoleon", # "sphinx_autodoc_typehints", ] templates_path = ["_templates"] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] napoleon_numpy_docstring = True # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = "sphinx_rtd_theme" html_static_path = ["_static"] Flametree-0.2.1/docs/index.rst000066400000000000000000000004701500740312400162040ustar00rootroot00000000000000.. flametree documentation master file, created by sphinx-quickstart on Fri Mar 28 14:35:51 2025. Flametree ========= `General documentation `_ :doc:`API reference ` ---- .. toctree:: :hidden: :maxdepth: 1 :caption: Flametree ref Flametree-0.2.1/docs/make.bat000066400000000000000000000014401500740312400157460ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.https://www.sphinx-doc.org/ exit /b 1 ) if "%1" == "" goto help %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd Flametree-0.2.1/docs/ref.rst000066400000000000000000000006761500740312400156610ustar00rootroot00000000000000API reference ============= .. autoclass:: flametree.Directory :members: :inherited-members: .. autoclass:: flametree.File :members: :inherited-members: .. autoclass:: flametree.DiskFileManager :members: :inherited-members: .. autoclass:: flametree.ZipFileManager :members: :inherited-members: .. autofunction:: flametree.file_tree .. .. automodule:: flametree .. :members: .. :undoc-members: .. :show-inheritance: Flametree-0.2.1/docs/requirements.txt000066400000000000000000000000211500740312400176170ustar00rootroot00000000000000sphinx_rtd_theme Flametree-0.2.1/flametree/000077500000000000000000000000001500740312400153565ustar00rootroot00000000000000Flametree-0.2.1/flametree/Directory.py000066400000000000000000000173731500740312400177070ustar00rootroot00000000000000import os import re non_alphanum_regexpr = re.compile(r"[^a-zA-Z\d]") def sanitize_name(name): """Return the name with all non-alphanumerics replaced by '_'.""" if name[0] in "0123456789": name = "_" + name return re.sub(non_alphanum_regexpr, "_", name) class FileTreeElement: """Base class for Directories and Files.""" def __init__(self, location=".", name=None, file_manager=None): # Initialize the properties and the files manager self._name = name self._location = location self._file_manager = file_manager if name is None: self._path = self._location else: self._path = os.path.join(self._location._path, self._name) # Automatically explore the folder and subfolders to build a tree if self._is_dir: self._dict = {} self._files = [] self._dirs = [] for filename in self._file_manager.list_files(self): self._file(filename, replace=False) for dirname in self._file_manager.list_dirs(self): self._dir(dirname, replace=False) def _close(self): """Close the file manager.""" return self._file_manager.close() def __exit__(self, *a): """Exit and close the file manager.""" self._close() def __enter__(self): """Enter the Matrix.""" return self def __repr__(self): """Pretty representation of the element, as its path""" return "<%s%s>" % (self._path, "/" if self._is_dir else "") class Directory(FileTreeElement): """Directories contains a list of ``_files`` and subdirectories ``_dirs``. You can also access all files in the directory's subtree with ``._all_files`` """ _is_dir = True def _dir(self, name, replace=True): """Create and return a new subdirectory in the current directory. If replace is True and the subdirectory exists, it is overwritten. """ if name in self._dict: subdir = self[name] if replace: self[name]._delete() else: return self[name] subdir = Directory(location=self, name=name, file_manager=self._file_manager) # From here we create self._file_manager.create(subdir, replace=replace) self._dirs.append(subdir) self._dict[name] = subdir self.__dict__[sanitize_name(subdir._name)] = subdir return subdir def _file(self, name, replace=True): """Create a new file or overwrite an existing one.""" if name in self._dict: f = self[name] if replace: f.delete() else: return f else: f = File(location=self, name=name, file_manager=self._file_manager) # From here we create self._file_manager.create(f, replace=replace) self._files.append(f) self._dict[f._name] = f self.__dict__[sanitize_name(f._name)] = f return f @property def _filenames(self): """Return the list of names of all files in the dir (not nested)""" return [f._name for f in self._files] @property def _dirnames(self): """Return the list of names of all subdirectories (not nested)""" return [subdir._name for subdir in self._dirs] @property def _all_files(self): """Return a list of all file objects in that tree.""" return self._files + sum([d._all_files for d in self._dirs], []) def _tree_view(self, indent_size=2, indent_level=0, as_lines=False): """Return a string representation of the tree for pretty printing.""" lines = [] space = indent_size * indent_level * " " for subdir in sorted(self._dirs, key=lambda subdir: subdir._name): lines.append("%s%s/" % (space, subdir._name)) lines += subdir._tree_view( indent_size=indent_size, indent_level=indent_level + 1, as_lines=True ) for f in sorted(self._files, key=lambda f: f._name): lines.append("%s%s" % (space, f._name)) return lines if as_lines else "\n".join(lines) def _move(self, directory, replace_dirs=True, replace_files=True): """Move this directory into the specified directory. If ``replace_dirs`` is True, existing directories will be erased or overwritten. If ``replace_files`` is True, existing files will be overwritten. """ # TODO: That's not the most memory-efficient way: better file-by-file self._copy(directory, replace_dirs=replace_dirs, replace_files=replace_files) self._delete() def _copy(self, directory, replace_dirs=True, replace_files=True): """Copy this directory into the specified directory. If ``replace_dirs`` is True, existing directories will be erased or overwritten. If ``replace_files`` is True, existing files will be overwritten. """ target = directory._dir(self._name, replace=replace_dirs) for f in self._files: f.copy(target, replace=replace_files) for subdir in self._dirs: # print ("subdir", subdir) # new_target = target._dir(subdir._name, replace=replace_dirs) subdir._copy(target, replace_dirs=replace_dirs, replace_files=replace_files) def _delete(self): """Delete this folder or file""" if isinstance(self._location, str): raise IOError("You can't delete the root dir with Flametree.") self._file_manager.delete(self) location = self._location location._dict.pop(self._name) location.__dict__.pop(self._name, None) location._dirs = [ subdir for subdir in location._dirs if subdir._name != self._name ] def __getitem__(self, it): return self._dict[it] class File(FileTreeElement): _is_dir = False def read(self, mode="r", **kw): """Return the file's content as a string (mode 'r') or bytes ('rb').""" return self._file_manager.read(self, mode=mode) def write(self, content, mode="a"): """Write data in the file.""" if hasattr(content, "decode") and not mode.endswith("b"): mode += "b" # You'll thank me for this. Unless it breaks something. self._file_manager.write(self, content, mode=mode) def print_content(self): """Print the file's content.""" print(self.read()) def move(self, target, replace=True): """Move this file to the specified target (a directory or a file)""" self.copy(target, replace=replace) self.delete() def copy(self, target, replace=True): if target._is_dir: if replace or (self._name not in target._dict): target._file(self._name).write(self.read(mode="rb"), mode="wb") else: target.write(self.read(mode="rb"), mode="wb") def delete(self): """Delete this file""" self._file_manager.delete(self) location = self._location location._dict.pop(self._name) location.__dict__.pop(self._name, None) location._files = [f for f in location._files if f._name != self._name] def open(self, mode="a"): return self._file_manager.open(self, mode=mode) @property def _name_no_extension(self): """File name without the extension""" return self._name.split(".")[0] @property def _path_no_extension(self): """File path without the extension""" return self._path.split(".")[0] @property def _extension(self): """File path without the extension""" return "" if "." not in self._name else self._name.split(".")[-1] def __lt__(self, other): return self._path < other._path Flametree-0.2.1/flametree/DiskFileManager.py000066400000000000000000000055421500740312400207230ustar00rootroot00000000000000import os import shutil class DiskFileManager: """Reader and Writer for disk files. Parameters ---------- target : str A directory on the disk. If it doesn't exist, it will be created. replace : bool If the `target` directory exists, should it be completely replaced or simply appended to? """ def __init__(self, target, replace=False): self.target = target if replace and os.path.exists(target): shutil.rmtree(target) if not os.path.exists(target): os.makedirs(target) @staticmethod def list_directory_content(directory, element_type="file"): """Return the list of all file or dir objects in the directory.""" filtr = os.path.isfile if (element_type == "file") else os.path.isdir path = directory._path return ( [] if not os.path.exists(path) else [name for name in os.listdir(path) if filtr(os.path.join(path, name))] ) @classmethod def list_files(cls, directory): """Return the list of all file objects in the directory.""" return cls.list_directory_content(directory, element_type="file") @classmethod def list_dirs(cls, directory): """Return the list of all directory objects in the directory.""" return cls.list_directory_content(directory, element_type="dirs") @staticmethod def read(fileobject, mode="r"): """Return the entire content of a file. The mode can be 'r' or 'rb'.""" with open(fileobject._path, mode=mode) as f: result = f.read() return result @staticmethod def write(fileobject, content, mode="a"): """Write the content (str, bytes) to the given file object.""" with open(fileobject._path, mode=mode) as f: f.write(content) @staticmethod def delete(target): """Delete the file on disk.""" if target._is_dir: shutil.rmtree(target._path) else: os.remove(target._path) def create(self, target, replace=False): """Create a new, empty file or directory on disk.""" path = target._path if replace and os.path.exists(path): self.delete(target) if replace or (not os.path.exists(path)): if target._is_dir: os.mkdir(path) else: with open(path, "w") as f: pass @staticmethod def join_paths(*paths): """Join paths in a system/independent way -- actually os.path.join.""" return os.path.join(*paths) @staticmethod def close(): """This method does nothing for DiskFileManagers.""" pass def open(self, fileobject, mode="a"): """Open a file on disk at the location given by the file object.""" return open(fileobject._path, mode=mode) Flametree-0.2.1/flametree/ZipFileManager.py000066400000000000000000000136621500740312400205750ustar00rootroot00000000000000import os import sys import zipfile import re from collections import defaultdict PYTHON3 = sys.version_info[0] == 3 if PYTHON3: from io import StringIO, BytesIO StringBytesIO = BytesIO else: from StringIO import StringIO BytesIO = StringIO StringBytesIO = StringIO EMPTY_ZIP_BYTES = b"PK\x05\x06" + 18 * b"\x00" class ZipFileManager: """Reader and Writer of ZIP files. Parameters ---------- path : str Path to a zip archive to be read or written to. source : str or bytes or file-like object Either a string/bytes representing zipped data, or a file-like object connected to zipped data. replace : bool In case the provided `path` is pointing to an already-existing file, should it be erased or appended to? """ # The Zipfile manager manages at the same time files already in the zip # archive when it was created, and files left uncompressed in memory. # The uncompressed files in memory are flushed into the archive upon # closing of the manager, with the ``.close`` method. def __init__(self, path=None, source=None, replace=False): self.path = "." if path is None else path if path == "@memory": # VIRTUAL ZIP FROM SCRATCH self.source = StringBytesIO() self.writer = zipfile.ZipFile( self.source, "a", compression=zipfile.ZIP_DEFLATED ) self.reader = zipfile.ZipFile(StringBytesIO(EMPTY_ZIP_BYTES), "r") elif path is not None: # ON DISK ZIP self.source = path if replace or not os.path.exists(path): with open(self.source, "wb") as f: f.write(EMPTY_ZIP_BYTES) self.writer = zipfile.ZipFile( self.source, "a", compression=zipfile.ZIP_DEFLATED ) self.reader = zipfile.ZipFile(self.source, "r") else: # VIRTUAL ZIP FROM EXISTING DATA self.source = source if isinstance(self.source, (str, bytes)): self.source = StringBytesIO(source) self.writer = zipfile.ZipFile( self.source, "a", compression=zipfile.ZIP_DEFLATED ) self.reader = zipfile.ZipFile(self.source, "r") self.files_data = defaultdict(lambda *a: StringBytesIO()) def relative_path(self, target): path = target._path[len(self.path) + 1 :] if target._is_dir and path != "": path += "/" return path def list_directory_components(self, directory, regexpr): path = self.relative_path(directory) matches = [ re.match(regexpr % re.escape(path), name) for name in self.reader.namelist() ] return sorted( set( [ match.groups()[0] for match in matches if match is not None and (match.groups()[0] != "") ] ) ) def list_files(self, directory): return self.list_directory_components(directory, regexpr=r"%s([^/]*)$") def list_dirs(self, directory): return self.list_directory_components(directory, regexpr=r"%s([^/]*)/") def read(self, fileobject, mode="r"): path = self.relative_path(fileobject).strip("/") if path in self.files_data: result = self.files_data[path].getvalue() else: result = self.reader.read(path) if (mode == "r") and hasattr(result, "decode"): result = result.decode("utf8") return result def write(self, fileobject, content, mode="w"): path = self.relative_path(fileobject) if self.path_exists_in_file(fileobject): raise NotImplementedError( "Rewriting a file already zipped is not currently supported. " "It may actually not even be possible, or in an inelegant way." ) if mode in ("w", "wb"): # i.e. not append self.files_data.pop(path, None) # overwrite if exists! if not isinstance(content, bytes): content = content.encode("utf-8") self.files_data[path].write(content) def delete(self, directory): raise NotImplementedError( "Deleting/modifying/overwriting an already-zipped file " "is not currently supported. " "It may actually not even be possible, or in a very inelegant way." ) def create(self, directory, replace=False): if self.path_exists_in_file(directory) and replace: self.delete(directory) # TODO: I don't know how to create an empty dir in a zip but right # now it doesn't really matter because the directories are created # the moment we create a file whose address in this directory. def path_exists_in_file(self, directory): return self.relative_path(directory) in self.reader.namelist() @staticmethod def join_paths(*paths): return "/".join(*paths) def close(self): for path, data in self.files_data.items(): self.writer.writestr(path, data.getvalue()) self.writer.close() if hasattr(self.source, "getvalue"): return self.source.getvalue() def open(self, fileobject, mode="a"): path = self.relative_path(fileobject) if mode in ("r", "rb"): container = {"r": StringIO, "rb": BytesIO}[mode] if path in self.files_data: content = self.files_data[path].getvalue() if mode == "r": content = content.decode() return container(content) else: return container(self.read(fileobject, mode=mode)) else: if mode == "w" and path not in self.files_data: self.files_data[path] = StringIO() elif mode == "wb" and path not in self.files_data: self.files_data[path] = BytesIO() return self.files_data[path] Flametree-0.2.1/flametree/__init__.py000066400000000000000000000003531500740312400174700ustar00rootroot00000000000000""" flametree/__init__.py """ # __all__ = [] from .version import __version__ from .Directory import Directory, File from .DiskFileManager import DiskFileManager from .ZipFileManager import ZipFileManager from .utils import file_tree Flametree-0.2.1/flametree/utils.py000066400000000000000000000026401500740312400170720ustar00rootroot00000000000000import os import shutil from .ZipFileManager import ZipFileManager from .DiskFileManager import DiskFileManager from .Directory import Directory import string printable = set(string.printable) - set("\x0b\x0c") def is_hex(s): return any(c not in printable for c in s) def file_tree(target, replace=False): """Open a connection to a file tree which can be either a disk folder, a zip archive, or an in-memory zip archive. Parameters ---------- target : str or flametree directory Either the path to a target folder, or a zip file, or '@memory' to write a zip file in memory (at which case a string of the zip file is returned). If the target is already a flametree directory, it is returned as-is. replace : bool If True, will remove the target if it already exists. If False, new files will be written inside the target and some files may be overwritten. """ if isinstance(target, Directory): return target if (not isinstance(target, str)) or is_hex(target): return Directory(file_manager=ZipFileManager(source=target)) elif target == "@memory": return Directory("@memory", file_manager=ZipFileManager("@memory")) elif target.lower().endswith(".zip"): return Directory(target, file_manager=ZipFileManager(target, replace=replace)) else: return Directory(target, file_manager=DiskFileManager(target)) Flametree-0.2.1/flametree/version.py000066400000000000000000000000261500740312400174130ustar00rootroot00000000000000__version__ = "0.2.1" Flametree-0.2.1/images/000077500000000000000000000000001500740312400146575ustar00rootroot00000000000000Flametree-0.2.1/images/autocomplete.png000066400000000000000000000226301500740312400200710ustar00rootroot00000000000000‰PNG  IHDRtƒáwsBITÛáOàtEXtSoftwareShutterc‚Ð IDATxÚí�{PSçú Y5iÕ�T÷6éÍDœM¨S!:G¡ýUÀž£Ð9VqNQzêϯ ýÙné´îq¶=#Ý[�T�µö@±3bõ7¢þ¦ öº% U U®JBË%õBR�¬�ÛùcAXäºBˆú|¦ÓÁäÍû®çYÏú®÷}Þw­—p¹\€ Â5ÞçRWW§V«ö\Ôi�9oWÖÿ#Þ#Œ{N$=½îÒœ·uõÿ #œªO£=Ô±¤¤ýüër6†/…÷ÔÚC¢ÝuÍïªÆ˜ñ_CÏѤ§7]Šù{]ÕÖa1h>ûÕúφ,¢š&Äþ¥yÙ᦯–^X©ÙtIÅ,ü M'žZW»ìt÷™¤°¯Cê|Ò©—–œ�v îIªf£2ñKÉîºú ¦�’÷5Äl?wRö~àÒ ‡El‰yw{¼× ÒÔp´`ÎÆ î{'©Ù°q@íÑ«@Ï…C—–|ðº¨aI¯Ïóųî?©Úzúð2¸°)!9iÓ%XvøôVUHi‰DÐp¶¶‡+Ø{Òpñ½„Ä} s·WV1”…�ÆÛ ÅÅǵ37ÆÇ½Ìl0Z1 Æg²€ÕHåƒ(c­.­yú ±;[Ìã ä%%+Dµ¢%%òÐZ‘.ýÇî%"ã‘Ô§Ÿ�ªâ“VoÝ{‚Û)kÖžì8²íH j[Œ¡úaÜ­@0çâ )}¸§/$«õö\‘‡QòQÙ Sí‰Z+XkÏÖšV†80 U[Ï÷¬¬9þì…‹µÏÙyöÈGG7� U¥ÂG¶âðé·V'Ú´ú`œçÐ.ˆ&�Èã#.~.Y… Hrg!Œ ‘ŒNy-0‚(ì£ñ])‹“Á‘³$&>>�^Ž®[}Ò³½dÙÙuûV¯[êóŠ |�¤"~寸•¨æÏVÆn»°÷KCÆ»Š‰qõ0sÞÞ�¡QQ_í>¯Ýù—Õ-­ùˆV¶~W+± ÄdÄt|vÄ=%KÕù¬ .ƒE-àîÍ!ÌÍ&ÿc%ªþ`ÆÊ•+36S¾»Vþj�/{;àÂν“Û&ÃÈx‚jÞ»zÓkÜ�¯>zý£3â¬6¥z5ð ©QŸ’Š…€ ÍŠ0\íq¬šwÏü=Zö¥¾7<ÃÎÊ,­ fW®Ä' �ñê¹€|Å�ísµû%$˜>Ü'1×ùè˘»ý+ä, ÷å—.•Á‘/7m�ùp…JB©XºÔ#£j¼pò‘FØêkFÉ òŒ’G5ÛöiÕµÞ^#sKÃÅ‹g/_×5Ô�TÍΕ;DËJŽoT€bãñ’³êuY¹3nôüX€ƒ4MÖ설×__'#)ãÅ/÷~i†˜^÷:Ò V„ájO[KŸÕl:²z벺£+å,üÀÞ âÂIþp…nò(ãòâÚµk–@4ý}.€lƒ®×Ï÷ݕވ›#"‚ DsâÞ8PÙb‹Åb±ôꬊ‘Ñ¥»½ÞÓË‚ D+Îù;�€5´ŸûûKæHèï$²˜eoì.Ñu[,K÷ñU‚�¬:Í<ªîÓ>> ÐD¯îÀ†e1s$C_Iæ.ÙpXçÃÌàVX,½§—±ätoˆ®î­|CBsv×1¬>¾BB¢e‡Ûƒû!+¬ËA8Wœ³ È0cXç‚L0¦3I³Òÿù_ÊîáãEÈã›sAÆãå‚�©eè äaϹЃ#tͦçÒÉo PÆKÿwÿ¯„ìýϹ"<-È8BĸˆK€°Åˆ~0c¡Ë»Þú×+0-æü¿]1¨-ȸª 3¾8�GÄÒÿôù!2‘Hþë…Þ^< È8*‹wH¹5…þ*L‰ñÑsa‹0¢ä‘Ä[>˜rC¦Ä|êˆwÿUA½‘Ëåòַʸ%flúB8º¢ÁÁÁ«W¯vww£ëä±E&“ÅÄÄZPܲ2}!ìv;-.555ó~o\­@ÿ"ȣǴŽÑØlƒ=]�½÷îZ) D$ùÄ“Óå³þ( Àh4¶´´¼ð ôÂÉpô…ç^˜ÛÝÝ�Ê‚ �6w~3^»¬ë»?jªôÉ?<5]6kšôÉþþþk—«ïÜ6Ò=£Ñè±jß=† ©-�ÇïyT¹{û7}{Û3dN8].Ý!x‘S¦EM™ª¿ÕF¼é3f2u$œü Åe’!ä-/˜öÁ�ˆé!þޝe›–ý�?‰�›•5õƒcQ gàižxl6Û­¶–iÒ駯‹�àÉžúÓ­¶f»Í£9[·e¨çât:°¸DýKTÎÞõ¿Ý/k|`5L†&�‡èd=àxòæ¾"ŒÖfÏãK#nS…ÛïúAwç/S§Iì'�çû¡Ÿ/‘>ÙÝõ+´¶¶H¸°ì¼heq:��“äãü!¯¿œø:I$ÁKëD3ì&'D°yDðÞÝ;däTØ�GM¹wÛÑÑÑAðx<‹Ä…V—I„Ùà0£� Qb+ÿ›ÝÜæ4 ™ŸGÎþ+e’Q�u�ÏP–p:�<Ï{�sa™y *.‘‚ÌÏ#gßèß{¼E.^È'úZË÷Q-fþ ÁÒu¢ ù$€Ýdo>c-/wX™Ç À,Ñ¿î¹ß›6ÿ¯Óæýéø:«ÿ›+Œe B^t:¹$A0# `ÐÙY=x¾d°k€6�Ÿv j¾mðèvªc`ø®°42k‹ ïëþ£»&ø3øKב1þ”€Aç�6ûÕ“Ôå:ÌÄ {§¾êŽ_½Ü ®ö`NFTÆ«ü;_÷.ÀÜXÁªA/fùÒ"Á”—éÆàÙÖs(fˆÉ¨°#*¨lãaìVÜ”’oçEªúó F„,Ü;5i¦ýÄ–�–›«£Ñ di½¢-°(áp8hq!‚)1! ˆBE –ç‰æG:;oØ)!1ûy�B -f‰àµ¼HU”ëNýà�ÛÄô…ó× fÏê?òù°0Ù.8I€(�èåżž¯-—‡ö´pØ?›"ùËþ7Ûu§~ðj'ðgóç'�oFóŠr©®€Gù¾AÅ_#^_g;Tè°À áŠ-A§õd©ÃÁº‰å»£4RWÏåÁ·]¢™üÙÑ‹^mt:Øž WG‰å¬@È[°N4ÛW78ˆ«=T7=*ãUþ�oÊØœXÁª^ôÖ¨àh®(„ªhQÆ6Gþn{¨fúŒÉÛaGTP+ØÄC˜VnG,~1b¶ÐÒasŸQá‚Ù`¿l Á�±Ck­,c^¡ËrX¤={ÙRxÐv—65’GÏeÌZ+VEA瑾âo]%Ö—öMYü ¹ ¼¿º‹E�gËE'D #^^ìº{ÅÖjnŒE ³ÖŠãf;¯þ­¯|ø«ï_gm�Xž`ý¼ÜÖFêÔ7‚ŒW"—ÿp¿¬�xa›XŽïþ�ÕhcÛ_1_ wŽõ}~fDä%p„bÊÝÛ]òf¯ò!.A]=ªðʨ7Sù¦oú‹ÑÔá[Á®B>`9ôW›`pÑÞ)/GG<+±7˜C4ÓgL†QA­`raYΆo‹× ➃Žáʧ¿1\×öU¹ƒî¹xÌ…:Ís¸œ½üóaû`Àyw@ÈÓh´}wÑåNFÖž´ðc4Ý R`"’êü¸¥<Ð[k ŠúÏ®·u‚|ßýòþŽKímbþ6ñÂtñ«ÏCO‰ÅûŠ Ø !ÛèA1‡‰“ë3y¢¤T€VËÉòÐòÛàaP X7å­yƒ Wì�7]w¸•H¶žœ²HüfÀyë›á�k?„o»\�Œ©;.žHš™~c’ Âõ'VܱUëÉ‘"r eø aÌLèûzÐh›qñ–…1 Žß~û-«ÑåKàù0àb~å°9í‚(vƾ�'°ß N�vØ=nÎ>æ�uÙnÝ͘ =Uö�ƒÖl?ñW˲u¢ù EI E·mgó, ]\‰ kOFñúnØ©hÁ‚íäuFŠš•·‚U .û`Øfú‹In&bÂó7V¸nž±Ã6AœZªaÆRápþð­Ó1A×MeeePõ ª5‚ÄÄD‡Ãa·Û«««Ž‹¼?²¹IŒÜÿÝAÜÏ®Àø3tRmCC�Áªì¿‹ãf5ò âê®…xÓ¶lÿ±Ûö’çÑËE//®ø7Gç»�×5±†½'oXŠwÛË£²ÖE¼¶ÎVX8d5K?„oEX5„0öq ›É`EýàÍAÁ³ËQW\ óà6ÅÙ½*8K–,áóù�€Ïçóx<úÿÌ•/lfŽÂYþosM3Æd©àM0µ9l ŒTæJþj¸í0<'”œ±ã?'Z•Ê·×yŸêÁ«Û}­¾gw�æ6{õÁ�³z€Ù‚é‘�kÖžìërôÜ-·œ×Ôñ²y¡ù�C+ÆRû€ssïÕ¨yžs'øIÛEÒAÛÙ»¹k°¬ÄJrÕJχt$_9ªc<= `ÐEÙB³bì®öJÈ]>HõO³�|&’­·‚m œ™é¿£4¢ÜiØYá?ä8³ÂPn§@�”A‚£ö‡‰|BÇçSÑÌoYå\Âzp±ë˜åæ¢Èg7LÙ¼Ðvë6!Ó3¡¯ŠºÚŶ€»Ú ‚Ù"ÓfÚ:o»ìàê¸h¿;ê\)"¡ß~¬¿ºJ,W££¼•£±_ot8„¼éÑÅL¢§À~­Íϼ¹@êjÞGÝ0зý¥S’ÖŠ—Ö÷o`ÕMnØÂ3µÚ ­N+ÓF<;L_[;=/ª V„ãj¯ –•³ÖE¼ö–íÐA‡•…·‚u Ü™ !ÇCPXZ(ä8²ÂÑ6ØÜ/ÔD´¶x&‰è·Èh @O3#–ÿŸ ­ååá§f<µÈ>³+€°^ai¶ŸØ>°tƒh�&"Ànr\/¡.0§âƒ*f;±�·b]ÄüTr>€ãë+ö»!¥UÔ0à(ßÞgH-Z,м"põu:®Ÿ¶ÕÖ» êEñk‹ êò@ù×Èm5ÿ ¹x»¨y»µË¼ ªÍúÃÏD æ?O¨Ûöú#Ö Ü¦ßXz’Á�J.Ž\^u¿¬!ˆ8±‚?„nfÈñÀ•šàÊ ›ãê—&�è,÷J×yŠEBÕH†˜§XȈ,/ç¤çâýG¨FDOO�Íf³ÛíW®\Ijþw@0Bþ+ˆÒ˜¨CïâF�=DÌߦ®xÞ~â­�–pWñü¼dã´'ÿÀãñÜ©YŸ´·6vývO«Õ …BwN—NèºK2b ’s|ÿö$BŸpÇA¡+{øŠˆ%ÏOØ’ÿ±%VÆoX„p%)‹"(²…3Àuýë‰Yå�LâxxޝHJÁYûï œh‚€Ãº�±3ýrq4ô;šK,å 茀~Gý��ï ê(Æ6=".(+“‚[»ÿßè…ñeªÆ]ün‚äa@!‚â‚ Š 2F¨º,9A(óšBþ]Žœ äYu“x›ÒeJ B�¯ÇÓŒâ2QôÆ™^I=À&CÈCt²l<˜šÊò³RÔr’ ‚ •Úô¼ŠžIšŒÃI©Î­¨Î¥]�øí6奼ö±@¦ŠOP’T“®æøûËË* š*³&Oàà°hò©‹2V«�•“è ÄoŒ�±i{Ž]ëuõ4é*+*uúžï³`­Êɪ˜Dû]S©– ˆÄ SOE^šZJAȵYe=ê/ËM‹¥?'åÚôüJëhÊUAOm©°I<üÌBla»ãgYUWš“¢vDf¾Î}¦Št)AÈ3™Ç­/N$ B™££X7Aé+rÓb‡{©rub¦—'£/T3Úð•s êj�3W™£&B�Ã(ÀÜXÁªÊTWœ©•“A�ÊÄÜÊžÍ “áGTP+ØÆÃØ­ ÓnÒLÝèaŽ>_MdJ™ ”éù¹é±î N@š˜»K`­+ÓO¢±QWWWGGÇÍ›7Oœ8áòAï±xÐd¯‘ˆñ ÉÉñ* Ȳ¯Y\.—«ûXªDªäõ›7¯I�(6ßëþyà½×NìY#MvAÑåí+ØÔÐ[�­>†ìÍë“U"Pl®>ÊÞï×Ë$ëˇ>h/HÕÚDvM|¿^Mêæìììõ© * È6_ci„Ëår¹,�åEEEEE;âEŠ=�ßsµåZ¶ Ü�öVïP€Šy.‚ù�+‚Ô`©^/�hT"‘*yÍúõ©@±nÖf‰Éð#*¨lšÓ Wã€dÍ÷ÌcnÜ£¥–÷ú>èî" (v4ú³ê?«¾ýé§Ÿ®_¿ÞØØØÔÔÔÒÒÒÖÖvóæÍöövƒÁÐÑÑñ믿vuuýgÕ·'Nœèèèèêêúí·ßîÝ»÷ûï¿÷õõY,«ÕJ?äìp8<^ßíøñÇ/_¾¬ÓéŠ €$µ¨qäZjlìuÇ Ä´»|‡TÃWFÐÃŽ)ЈFû24ü×`©Þ,óT¼S©Íþö‘hÚ,�¬)ïu¹Ú â@³§Ñº —åûT€jO;ÓqíÝc2Çrm³Ì[\‚{’!.–ê=Plfbp?„oE°†¬`˜Ñ¸GñEÝ¡L ˜ä ¢Xú!PÈ…mÅ�’0*oÜã­7£î= o!‰ËåË—üñǺºº†††ŸþÙý«æææ––––––ÖÖÖ¶`ðX>=�PX˜©v§¤jµ€j*®0ƒ(5/s8‡DÆæìŠh.®ÐÓi§ &"õ¥Ë/6‚&7'LÃ�±™ZÔ—ºÇÒÄü²l…ùxzf~nú–Ðì/ÍU‡’ô (+Œú…TÉaÚ$Ošêò߯W¬?¥+L‘†ä‡ð­`Uƒ(uWú°Ê”€^GOt„0¾b’o‡ç.¬P§g©ÀZÁŒ@’–£õy¦²œ¬*¥îŠå&è˜/Ê3<æwý£I×Ê} LëŒÊ%Ãil¬ @¯ÓSl Lz�Î P¿eÞ ž~íœÀÔ32 &µye;TæÓï|\ñe9êÐòk±™ "h~'66-'¿¸B§ç:«ÆÚ“ÆÒôEïÔXA–’•"Ñá[Á®¹Vé–Rª”P&*43ýÆ$'ÙÒ0ýÀ‰Êô\͈ºPu…Å�eæøÒª)?-ý¸Y±¹¢8�+�x¿çe è{‡®Téã®@‡„T*•ÆV’V:X‚˜€Ž ¥§D >º"¤T-5œž¢ø¸ÙšÌ”�góäée:*'gWééOÞ9ý €bMQEq¦š£Î {Oš�²„diÕ¹OÓrÒ› ¥¡ø!|+XÕ@’dØfú‹INÓÜX!OÉM€µçò+LiédSq©;²|hKOiºö�*2¹¨räls0Ñ3Üç e‚ù ³G¤–»ék>Œ“Éä}1KIV&bÎNIXIubŠ'‰Zæ­…ªÛ•þ‰$"¨ß’^ú M›Y\©§\½íÕ§ö¬Q�áø›iùMÜ™ÁÖ“ EºÊвý0~šÎ˜'bé‡ð­«†fübh2X!OÉIAU~EUWXjEV¦WÚT‘¥]{Ú¿¿²,Sɱ¼¥$äaKòéDu¬ @_Áìê™êêŒJ:^ƒíà°:3þjPjcEºÒÀ«â)]nÚÇÍ¢äcuu{4P³Åg$±:H©R›–[ZY¤h.kâjxÄÚ“2u¬@�Sº_Æ/òÂÎÜY1¦Ø̘ã�C+ü5Á•ÒÄœÔä—––A•“î¡-&]Žvù§͎ꊜX’señД1H Ëœ‹ŸPÈL‘€õtn±Þ}ûÏßU X-à®I.°êÄ U—£& BšXì{¹‚ßHmnº Ì_dåŽZÒÑSY\èþÀT™“ö‰A”Z\œ®Tç–î×@ý;iyMË&(½®nÔ}ª§É ’{Þ¨‚Y1vW{2,/i9C«ªXø!|+ØÖÀ™™þk Q�r&¬¬ðr\Y!MÌI“@Í;™_˜A““®}òR}Ò¬Ø\^™§å~|ÈÜÿlì9Vc"gP›W˜\ºöܵº,=EIÕU¯2€l}a–še�‘^h|]Ï{çý´])uyŒ;B€ƒ4Uf½ðf“">%Q«”‚©©ì‹sPdïòJí±" Wû”—âyï|‘ž•®/M‘÷V°®�;3!ÔxK+…GV�Úœ4Éñ/Ìñ9£rµ”.'åý™¼§03­�©Gy…9dú|vU¼g‘‚Ôrýúõ†††k×®\ç’PîoúÜÒ~jG²J"™fÍþï»C,0¼æ";Y!qçÐ º½V«H|~Á®Kã±ìT�L4tª„5;Šª»GÖzHRO�ª·}<(²«-¬š°4Û±&A#“Ð Hñë ª},vba…Ÿu.Á=9zÝÐÚˆý^¿ÄœX¬zˆj;ÓÓ*Ɇ£ƒL°˜dQ�–3²óCÀ&8²bxÉ̱nKq|‘ì·¾�Ö¹444\¿~½©©©µµõÖ­[Ì2¿þúkgggWWWw0ˆ7nÐ{E·µµ­Zµ � ¥Ë”/úB¾¿½)G‰ÞxÜé)Ö>õfM©ÞÊ´pÇ>?\üŽýÖ"jµÚ½£ˆ{_ºŒG®7–å� '�Î  UKÑx£©ËßU¢ÔÜĉŽã ￉ sˆ…<0ô¥y…u& LueŸ6ƒdMŽÅåñއüB�®²ø¸Ù»P0øœ ¹ç‚góߢš ßÿ¸Šâ«R÷—2î#�e<¼óq€D³þXY^ìÄ¿~#À3‹2È”JÜâeü/Y]Vbf WH�ÊÌÒ²\5‰ñÀ�¸`È!��„k uMè† ßD‡ ȸ€=A‚‘ÀÏÌö\¦Än-‚<"âI‘ÐÅ­E�‡îd=ØxЗ榧Ä*éWï¤<6%§´‰»C �Áeÿà"‹&¸µS]þÇÇk$ U|‚VJšôºšsŸ¬�WQW]—§�4ûF ¸L>uQÆjQY�€HK»-ÌWoêKSÔkÏ}œS–£K—O’ƒÄ­EpkàÐ ÜZ„+Xl-âõR_%ýh{OÝ$Ú·ZZZúé'ÜZ·Á­EÚ­Eèýï<âSÑ���---7oÞÔëõ¿üòKgggww·Ñhüí·ßnß¾}çÎ�»wïÞ KqÁ­EpkÜZdÒm-Ò[½gýš5kR“52�Ägû—–!.l§¢qk‘À­àÖ"ìkÀ­E‚[Ánkª®ô‹ãÇ�Ÿ>WoI|fNVâ¤z$�¥¸àÖ"�s°¸ð_Ìk(IDATµH5àÖ",¬`µµˆ<«Îår¹,Ý×NíP×}²v^bþdz¼�åln-ÜZ$„pk6V°ÝZHylZ^E…^ùÒñÜÜʬ²ÄI2Íz….n-D~qk‘ðkÀ­EF©Kð­EFrºÀZ§ÓÃd!œåÿ¸µˆÏ>n-2æpk‘Q�ÙZăž&SÀnáÃ&.¸µÈð(· ¥ÎÌô_Ó£½µˆ©®´L7j!WSaήfYJ¢|Òˆ ´¶¶²˜Šö÷ŽòîcÉ" çó³7¯IPÈÖ�Zç¤ÀP±¢xÕšû÷œòœúÞü5ô~¿Y0²À#5A!ÍÐügoù €ˆñþú¥ùî….Á›è.Òˆñ©ë³³³³×'+Ù^+DÂyû0Oz¿ýßóÝÿAüÀ�Áj`ñö6Ãæ½ùA#*ÐOÙù!@\Y1§Óé†ÑÅðT!ÈC$+úÂü¿{6šÖæÀ'œ·÷ ÜJFËý·[Yè> 0<ØyA�‡·ÛBÿáÝyqë såK˜ú22,bÊ øÚ[À£Û‚ƒ Q·…Ùyñ'.Ý÷õ>6}Ѓ ÷ÿ}ŽÐèÚ™j‚Ê‚ ã°È-.ã#:Ûâ1\ò¹{‘w…~ÅÅ�páóù‡Ã­[<ÏápКâVeA‰A�‡(Ûâ=(¡ó,Ì¡sÖÙ[S ”œ®€NÖÒÿgê‹[8<&‰PYäÑÐæÔ²‡²xô\ÆÖ®€Ù ­/n­Ál ‚<&™¦ÊøÌæŽAe††E´ ÐÓCn5a¦c°Ã‚ �R†))æ°ÈCPBZí22,r¹\îž‹;ƒë†®%A�‘‘{ˆ‰Ç\·ÐŒ­Ûîu.]^Ï‚Ùy´3/ `@ê"]�w·Çc±œwžÅA^qñ� oíðP�s¡+böSÜÿÄ ‚<Ú]÷åïó‘"}y˨ž ó1"æ8ˆþ's… ª ‚<=÷Õí³·Â4…Óy0ùzÈóC¦ àË¥ä]Ÿ–1\õÿ]ÞÔLÑA°,IEND®B`‚Flametree-0.2.1/images/logo.png000066400000000000000000000402131500740312400163250ustar00rootroot00000000000000‰PNG  IHDRVWÆf “sBIT|dˆ pHYs„„Ãí‘êtEXtSoftwarewww.inkscape.org›î< IDATxœì½yœ\Wuïû]{ïsjèQ­É²e��™dŒ 2$J.ð€„)/&ÉË!á2Ü ÜäÞ —!7ÉËø2Þ7#!ÉMb¹˜ Ä N\ 0x€ØØ²-[–5´ÔCMçì½îûTwuu˲¤îVKÞßϧ>]½«êœ]ÕC­Zë·~KXTÞŽ°aë^alVøä ÃX) wÚš2]WŽ)»oRnGy+Š€€®Ç^‰D"‘H$γÖ'X ª®¦÷Ú,ɱ9#¦“u Î¡²ÁCÝ:»ŠœÝweŒ^áøæ=0(¢oÃ(ȩΗH$‰D"q®XÓ@EAx[T]y»åîilj¦#¯9\Ça�£ô1¸óF!ó4Š’²WRŒ”pÂÄgëÖÀá›”ëPÞ¼õäÙ+‘”ÙJ$‰D"qnX“ÀJ¡Þ \¥…–Ñz†79mjXj(98‹Q¡,Ε”Z€ö ôØz„ù?V²m"p÷¾ðh‚«eO2[‰D"‘H$Ö�U ¬TW<–p†ý»»FÓ¾†– Ä5)}ëê@N,"¡ÀÑEÊ6ÞtpÒÁK—zY0Ù)80áÙz[`/átƒ«�%½V"‘H$‰5ᬫ“S‘¾Xýò=†Þ!Ç!W£Sk2jFè1†ú‹èšCôëPF?B.璉YLhaì<.Ì£e‹\:Ì…�N�mz¶ÝØCàzàº%AÒéL)ÈJ$‰D"±Š¸Ó}À@™ïÔÜŽ0zÂИ°¸Â‘›Œ¶Ö‘0BÇü,w—×r}7ÇßÕx1;i’…?3GÐYJ“#d”Ö1R¶iæÂÔlÁçvyØ×¸®†½GÞ©VχT.L$‰D"qö<ê®@Q}TAUÔUíFøz Q 3¥`ƒPS:2;†ãkø³NÎt€#þ¼�ãå¨lGÃv‚n£Ô­x»‰–Ž£y“uŽ52.n[fwÅŽÁëªr#×/ ¬dàòÈÏO«ç—H$‰D"q†<ªÀê”ÕÛOÀü;B­2/Þ0gè„P¥7pÐyCƇ²�Ù†È6¬nŰk6Qè8âFèjƒf-‡yÇþ]–ë1ì­Žq,ìãdAÖÛ9xRE’­C"‘H$‰3á«SfqúÕ[Ö®¯Ö¶"ìv¸=Õ�ŒR+™NSÊC|Cm±wðÿÊá.Ï^Èï×_ÇasÈ6¼ÙŠeŠ�oBkc´ÇšÌ�Ôqe|vÄöUY«~Pt² ë­ë'}â§Ð�%‰D"‘H¬ÀIƒ‡SToZ‹Z§Eþ†ç#4w˜Ë˜™¬£Y“BÇ Lôj:òV::Š éð5倠N›oí½—'…¯`åø£är„Ò'+fÉZÌ›.#¡`×|É®‘ueß>Øóˆ‚ö•ro'ÏÇ%�{"‘H$‰GɲàéQˆÓ—>f8 êß¾¡q¥Ðœ7´F} uz2JËl"“M”\DW¡a ÄSÊ8Ÿ]Í>÷TÁQðŠî¸V?�×#XG©q Ã,›ºmÚÚåÉ›{t�J§\1·²oñ™Üì}„€+�&‰D"‘8K–”1¨zûPù¬_ò[ ªâ÷ûØ#ìÙyWp¥PZ¥Ó ˜ÌãµÀ˜Ú? r'ÖÜ�ÐÅQðÊâË|cñ%¥$ã†üåÜ!»1lÆÈV'(u_Œ2+5r›óàtÆÌ¸¥Þ1ÜtÔBG¸½÷Áž¸§½ÕoZx'+®H* &‰D"‘8 �Õ)2UÃ¥¿G¨èt„Õ�åc^xª ä�€–%µnèb¤ƒÕ6Fº %@{‹yuï¬ ÉyoþjË¥6d £df„®¡'9Æä›É˜™°Œõ Ìšófa�+—X°¨ÃZ*tOÁU"‘H$‰3ÂÀ)»þ–g©€*óSÝ6PÑ�—½ÀN`ïeØb£& ÖãB‰j Á£!£c^�7ß�——ÓÕçò ßæ…å�´¥Á_ç/LJ©*¸ÚDÏN`eµ#œ Æ”Íñ³�³j[v•Â�ºBÞ]`­,t_|Î'éLÁU"‘H$‰“aNa-°4ØÌRíöU¥¶Û‡*Já@Ç@) ÂW¼ár€9˜è²B©— %d¢´y9wû]ü×9á§ç„›Ëqºz%/(§¹*à ¹„¿¯}=¢›™"„)`œv1FæGxXëh–q÷\Îüˆãp×r¬0Ð1¸2îk7@§ ÷,–˃}Þ )¸J$‰D"q:˜G•©:Y–ªQTù ÕÎ * ®0<8„Ž7Øš¡ž Z·˜Ü !¹†÷¶-…žÂû»‚eŒ  þïÎlsÜl¯å~» Ïf”)¼L¡µI:Ù8¹¡å›X—s¢ƒ«‹½áö®¥Ö‹{Ú?oâ>4XýòàiW‰D"‘H$ÜÌÇjå j/,h©nïT J NuéT‡ Ãö lUaÚ:uËEMÁ{Kð–Ò;²"j ‚8åB_Ÿf8r^UÜ�A)±ü]öB›�-x¶Â=³‰ž› Ó…¬IÏÖ˜iç<\ϸÈÇàŽžYÜç@ypÏžø÷.yÞ‹º«“W)k•H$‰Db˜•«Gª´TƒY*W {fI@e½azÌr|ÔbG,Ya9^8ÔdXÉÉðÔÔ@îä•5�«vô²x: HÎNUžUà ÙÎín<[A·!º ã·Ðe3¥Ÿ¤+cÈX“îh�Ù^ÆÃ�ŒÙÒr¸0d…Y])ÜUW •!ê®Rp•H$‰Dâ4vV_éúbé€N H\)삨¿Þå ›U� a®4Ø`0 ƒ--G¤ÆqmPÔÆ(Ã&:f;¥ì.cŽoAe+¥Âœ¶é~�€j†¡']~½~-…i~´óÏG�iŒLJÇhP·Ÿ¢ÑûDO¶#4(�ÏÛ¬�ðúÞgy¢¿eŽy}GµÉCAxªUrý+. ¿C-Ì1eZŒi�£Ý’ú¼§°�I‚«�Náì‚«dšH$‰Dú¥ÀÁ2×õgTm7Ì�XÚ=G»æ˜içÌvsZ¦Æšt²&j›tÌÚ�0‚‘&Fê(uŽIÎo›x©Ñ¶9Yáh•–ãã†cÞpÄ›…òå#WýnÁë!xJùªD"‘H$ÄÀj1`ô©ÚÇ€ég•©:0T„cÞ0=n˜* ¦´tk޲pˆ©1›×ª�$0Ál1vo&Q?‰2†„$Ôù¢Ý†JÜË¥¾@>Ë`�ËJ@Ë�v+V=™ÅÐÆ l³¤…†<5ê&gÖ9lÍ2_X̸ÁLÊ©ƒ«…nAVœ‡8@ÊZ%‰D"‘p×—–½úAU_¨îVªd²ÒSC7wd…eÖÔ(òêtÌEÙähþÍÌÉu`G0|™Mü#r‚N¢2‚‘ŸÍ.`LKž -Tk|K>Á»‚p"À‹jPx§1º×Œò$9ÁKòQ Â�ÏÉaΔRš°mæCA^”ôB +•9QFBà†-}ƒ‡rñÕØ]SØûö {€˜—úù©·#K†6÷o]š@mÍŽþè˜^…clú>'Vá¸ç u 1ð}˜?G{Y‰ â_Æ0wöòhÈ�‘¡µ.ÐZåóŒTç:—gã寇Ÿ7˜9‹Ç7ˆÏq5i�U>æé²ÞON0`¬tŒÿo�K–ýmº%7êªn¯,Ü�¦ªTMM 'ªLÕ˜7øÒRŽfr\hЕ1Zå³ÙK9*ßÇ{Ú93 /ίáªl'SrÈ0Ê—ä"î—qžå�aÔ£r‚ÍÖñ“#£dÀC¢ÔB‚aGÜo ˜øÏ.7ðÂ|œ9©ó%Wã™ãEųxqë£t} #]òF�Þl‰÷�) ÊñqÃÔÌòàj?ÄŽG`Ï…}Ъb¹T‡f'®5¿ üȺžq95b p¦dÀ±¡µ{Y|¥ ¼ø¦�ïïžÌêüƒY ^ ü¯¡µàIÀ¡õßÎ)ùŸÀ· ­½ xã*Ÿç��׬ò1O—­À‘s¼‡aþ3ð¶s½‰GàfàYgñø·?µ:[YƽÀˆoïf×è<+ñÛÀ÷¯ãùVâràžU8Î{€W­ÂqΆ †ø¾�U̵ìeQWµ¸‚ø¶×ª¶dªN”ã -o˜öŽ"stz9†³a„Ž#d›˜6¯âo:9wz8à/:–R·Ð3»L€4¹©öxjxžW-1Z’é4Ì;übÞ›+"ŠÛ5¾‹`õ4ä~&ÙÇ'\—¸Í< ãšXê©Ñ+rB3ÃÕ,íÒr°´Láø¸Á„ŲàÁÉý•ÏÕR1ûÒ’à�·UòµJ<——A®^tör:Œ¿p®7±ßÀò *‘8_x<ðJbÐ~?ðólìì_â40Kº�%ºª¾îh`‚€ÊBùÏxƒxƒ ;aq¥ÃZGYÔq¶nÕ DG81ð�<-¯S(cì³;8Pe«¾¶<ÈDzÄDIC=…@W„€‚ƪÀª¨²H –Z°<1ÄOÖ˜§pB')M«uÚÔe†É-&Ä,Û¬7H�¨¹ 1g|ÌÎí"¾íí®ö½8¸™…¯ë›µJœß¼‘• yp½7r|/p͹ÞÄøÍs½‰Db•˜þ+ðEàÏñ^«€YÒ1Cs×€X� . ¢Âô¸aS¤ ²P¡[³H0”ÎÑ+r<5zÚ¤�1J&ȸŸ½¹.¼¥<ÉÁ8‚¡d„»Ç0ª=¾¡÷ &-) hU»TÁ#xæ*±zM¢%¨GDA,W•G(%ç3ù•Ó¤K#5TrNH�bÜÑñ)-¦´Hˆã1,ãƒÊ»–Œ¾œ)I³§fxÃIn{9ðÄuÜË™`�ßgã4h¼¸ú\o"‘Xe®>B”~$Îcú«¾»:ìÐÕ¬ÕE…-A8„™Ò !f«:uËæÂ0ë-me-§(êmàÃ*c7ûÙì®ä¿8h©r±(uDµÎ§ÝÇL~cq?uz¨F!àÆST{ÉQ %Šr�‰¶ Û|´RTº,Ø]Î"µ€b8d.E´A0M¼ïÒÕ’¬ ´405�êi³A™ôÈŒ°… («W¨¦ÜÞÚÀÞ*Ð;‰ÖJY'Ãз_‡óô)O}—ÄIx 0y’Û„˜ú™õÛÎñ\béâïÎñ>&€·œã=ü8pë:žo#6y¼øÄ*ç¿u|ƒ|Ë5™§ËÙ×OÆ/ƒŸ3e Qðà%¬ÜñŸ‰MëYùYà3ëx¾‡ÖðØ? |e �?̲f™ñú�`}WU<biŒIa²f« J†naqÁRxG–e>C$'˜b(#¼·ö,0ð¸ \×}˜:DO�˶°%´y~ù¢AP Õu儱Œ« nµ9G$îû)þ8VZ($€*N„1m3##LË6�Ô· ¦GQ–„¬ê ,7eÚS&DñAØ2)9nð,°� U—`²Û/œ ¾|ø\o"ñ¨.÷µYª§ø>b Ü]·�¿üg×Äp¶üW`Û9<Ÿ›Yß6‘»«ËÙ²RôoÀÁU8öjs«÷w øIàÍ,ïl{ ð ±c=øÎûÉgYß qfIi«/Xw¥,ÉVõÅê}M’¨ ÞÐTƒ¨Á[‡5.U¶†P­ó±l7Ì6¸8L³‰.häÜjÇ8.ñ—ioù F££aQͤÊÃUçߤ–Üm„÷eŒk�gûƒm#Úņ^õXa“¶˜3S”Œ�ŒáeŒÂŒbC“Žk0Gƒ–äØÜbsõb¥‰:«ê²80 d‡¥Z«ë‡4WÉÓ*1Às€k‡Ö¾�¥�€Û€o]·�9Wÿñžÿ‰œûîØDbµ8ü4ðLà¾nÿMàªuÝQbU0Kìîªë;aÁZÁTšª¾®JBÌVIˆ—ÞHƒ‡àPŸaC¡F)Mnr±ÕuT ^Ø›2”Œ€ã‹.úÏŒhɵå4F# Õ9˜³ÄŒÕAqüqm=1”WwïbDg±Ì“I•#Thh4íQ'0Ni&èé� fd‚y?F7kЕ^rºÁЮ[Z¥å@Ï"“1xdHÈ>¨µ‚��µJl|~hèû}D›€‡ÖÏ;ÀÏqî2F¿Á¹÷“J$V›[�¯c¹ý@øsVnzIl``ýlÕ�*[µº³T4ãWQC/³”Áâ­Ã&£”br>�=“Í_\_„ÃCY«A†Eìƒl4¿Ä¹b+Ëý�ÞQ}}çÐúóØX�w}†=¶&ˆ­áëÍ‹ˆBÿA6ŠÿW"q¶ÜKü¨>,x:ÉVä¼Ã°o¨p0[ul [ƒ©*¸ †ÞRWƒñ…äàsÔÔhÉ(ŸÊž À–ÐâYåZ�Ï…#ÆUV pEh‹E5‹�QÃýfQ‡²9tx^ùo™û$Ï+öã˜Çj‹,´qt±”ˆ*F4Vlâ¸|÷ó>Ík8ê~ŽCò:nŒ2ŒÐËkt»5ln9Z·UVN²V‡||MvA´ u³ò æDbÉoÐáxš˜­ø p`èþ1kõw,N?ë[¢p¬l¯ð§ë¸‡Db­Ùü·ÖŽôþr^aØÃR3Ð~¶êÈ@¶j>˜p4 �`誡ž ï�`±ÆQö2 —#eÎòkiI,õ½¨x¤ŸS@™“Å_’MêQ�œØû—!XÛWƒÏ-ñ–ùÏñ]�;ØÄ rmQÓD %Aƒ"Ú&j·r -Oãîrœ_šËøÓ–ãç2ºöÅ̘+陿µA×dxëp¥¥å s}«ê;Ó/vâtÈX®ú#;HJ– S¿ ؼÆû:]fyw’%fÞÖëoâ‡�+‡Öþ‰sß¡˜H¬6¿Z»šå:ÍÄfiíöÀ@'à ¶j¬*„F_g¥†¢®“¡Eé]Óà3ùóØfy†ŸÅ(¨ºýt±XæÕ!ÄÇ“£äÿâ6Çù{ª¼°x€\ÚXß&-r߯….yè‘S`4`5Tû„ãÕ|§ÍÚ£­ÛøTiÊs³ wy¥+WRšJm€äÑ<´ OÇO’µZ±ï6 ]FraÌóRàâ�ïøÃ¡ûü Km,Äàj£ñàΡµ¯eyin-˜"~bÄ[Ò‰ �yàÝ+¬¿l½7’8s ·_¹T´~¨ß ¨‹Ú*Thf«jB,ÆYÄZzÁa%CC�¿¯]Í<}'õ M>%š{"ž‹µ·v|ÕŽÈñZC©¡’1KÎÍ.ê«vûi..gÈB‡º¶i†5Ó¥N�LJ„€A1Œ^3U·á”–8 ¶Ëb 'À6#Xí GÞ 9ë˜+¦fÊMDבíÄLÞ.Dì¯âõ)ˆJ,cX´þ–{«<Ü0´ö&6^Ú¿ü§Ö�µ“¿•åY¼?$Ú�$"Ãó:!Vçf¡ 1x0AØÈÄ@'  Í�lÕL°Ô‚A¬%ø +^r„œ}î94C‡çGÐ Qû¤%†8pT . Ñ ç3nŒ‚1h†ÇûjÛ)Å ÀË{ûÉB�\{4è‘Q�û’\=V•8-�"’Z•7‡‚º>ÌÈ•½9<ÁÁw6 A›¤I F—®Ì°ÎÒ­Z!êÉŽW:³AûJô»“ {"ò$–Ï|ÇJwd¹ˆýÉÀXõ�=`¹ÏÍ“X@®&O%šƒ'zY%*ÿ›å¦“ê‰ Ìb)°_Ü ›ŒB?kE3W]5PêÁP¨Å‡WK2 ©ñwíå\ãà ŠÅ#”ˆX)0x¾®ˆÓ¼OˆåSù‡b¹5áKn €«ýžPÎ�›XeRà¼'—€ ‚SÁ H5Œ5Ü]u"\^¶°thÈ—yIm†W6À×áŸkQhOƒŽià}NÏ:L°tC,Î=B9€FܤÙ�‰È�°4ë´Ÿ“ë�ndy&k£z5ý,ÌBèóvà¢5:ßï±Ü8ñ­,× $žh:È$Ð<{Iœ1°Ú?P<¢Ú*XÈXõg.諚,!w¨d›ó‘úžþHdž[D0ˆ("£%–´DÔs�ŸçâÿIÈ�s\2¦�ãoj[P �çÿéÜCM ²²$S�ó�šQ@°Š�A•Úhå€pŸ‰¢ùQ-ØL7– ÖáƒÜi.K�NrD2¬q´œÅ„ؽ8öåÀ»º±“rßžÁîÀDâ\Àa�ÔŸprk%ŠÚypé*ïk5ø2ðCkc¬Mée,Hûï,Ïð%"GWXÛh�-‰“`¸«+Kº·› ¬.ŠÖûVO =5˜¾•�e@BÎ]æi\¤GÙ©Ýê±€ CmÄc ¼¬8�]þ²6É;ë[ia1 ¯o•meGI.§� °œ7ˆX‡xGª¢`?°Ú¡-„ÅÇsKàòÛÆçd”ŒaÈ !ÃK†‡z‡ 3» ÇaÅràÄNÊA�U"ù–Îìrò2`Ÿ?`iêß±¼¶Qø9àÈÐÚYÝ¿†ð»+¬ÿçvœN"±^Œ­°6»î»HœfA_±ðØdÔËÊ€õšPSAÔP‹XC'd˜�ñ€�âˆìà)å!Å` ÁcÔ/Œ­å©e‡kËø†²ßä«f~}ñ0Ï(§±Ä,U¦�Lã~£ªÊ4j²JÉÁ8D,š&G+‹†'—DZ¡ÀR Dáü¥aná¹~Õl#HFIŽGèÙ˜�Ë ÝJc5[e簾b‰ô‘HöD䆾ÿ§.]Þ?´ö½lL—ñià‡Ö ðK«xŽ7Õr#Î,³DâT ¿ãlÌA܉ˆ¥À¾¾ªÏ$,vV—~)iÆr`0UÐåü[þÔÑøÕþ(ÁÄÒœ¢&¦®€€QETQ…s•CäkŠi¾£ó™z2 £Që%Q‹�È&É� Õ ¯Ž/V�„\ã�‚”-0Z ¸´œ[¸-¨8œ±`¥qøÊB µð|a:Äé2��j�‰EžOôœä÷åc‡K\±q—ÿ;Ëua/¾i޽…8€v�øñU8v"q>ðx–V÷’fzœ7Ä ¢¯¯ÚJÌR�è뫨är*ÑJ…"Š`è‡ ŽÒdxrî5ñf]»<Þw0b1"ØêwAB<›ˆ,è°ZÞ]Ÿ"ŽN†'„¯í<€ÅcB´Q°A,¨C5ŠäK©á©¨¡&GM†ˆåŽ*°ÚæÙâ[˜P`(°RžQéÑÔ82à¨Ú…K¦†P·”}3TÈÚUlxåúC™û¤¹�‰år·ÿò(ûqâ̰A6¢;ÄOÏo^aý×Y.6?]~ž(Fä]ÀmgyÜDâ|áVXû·ußEâŒ1K²/Gtñú ¾ªtU˜UCN,fË€>d”!ç!¹€mᇆ8P«ÌOÌfÅq3"Bá�j[9Z•ÿ^Þ;Ê›çïÃjÀ©b„8šF\Ü,9ÞÄ€ª´uJê1È 5‚f´$ç¾Êíý)~K‰¡@´D(±xT¡Qi4:¦†UƒªÁ‹¥«BÖ÷ïR�†,URYPÀ¢€àöN¼}éÜÀT|l²R†éœÞ§Ìá¬Õ ظÓíÿžè~>ÈS8»`ð*àÿZ›&õÛ&[¼v…µ]÷]$ΘE»…~6¦/\`Ú*t¨ôU…‹úª^¨Dã.£ Îq�°#Ìý Ê@? ×QÃ�Ù$÷Ú8Gík{3¼¨7�>!Þ_킪05:Ò �…4éѤ�&žÁÔ’s‹›\,EGJŒøª#1 ¹z *† ¬ j)UȉAdŸÙêz_À>̾ٓ»¯§ ë±ÄXª‰šÞsšÇøóêqƒlÔ¬ĬU9´öμ{éÿ'ŽËäí¬Ü!•H\ˆ<ƒåú’+›†&6(n¡#pQÝp}-ÚÄBϹãÕÀ×èØwï[£c_ˆ8–‹Öÿ‚Ó›ž i~ÃÀÚwÇ·ÌœñîÖŽÛ‰Y¹Aß­)b†é?žæ±^ ¼phí˘oÂ#8U¾«w T¤*Ë9PÁ`)%TQ�ø5UQï±x,kø²�ctžì§Éªñ9ª!Êç5nA ‰Ÿˆs ñ|JµÔ‰�Ô(1¨j²PMÇVxMvŸâå�Ž[kÅZΔû;R`u:¼¸dhíT 'ã�, ¬F‰¿w†Ç[kÞ|'1 êó&âó¿ãQ#'곆y3Ë I7 O^ããÿÃ?±ñøOĘA VþÛXm®8õ]ΊõÔˆ=e��¿ãTw8YµH»Ÿ­ª´G&Ø*cå0ÆQ�qB=7&ð±ƒOuUª‹b¹ß6¸ÕE-Ô³ËÛ¼ª’\x•ä”’Ó35 [§0u Ó +MzÒ  A�¥Ôñ’ó‰lŠ®Äçòìâhˆb@¦"ˆ‰{™'–›ÚEL,OŠ1ÁÔPªa6˜Ø Yq"S,öiœl´Mâ±Ê°hý“À-gx¬ÏŸZÛˆóû#Wƒ8NïÓé�²üû?’‚‹Äc‡~y…õßæ\×F§�[quœåIîB™Ç¬Ž1–R=›Ó’Å12� úe=Á bPµcù”C§Ê‹ŠyT,Á¨à�€FK8!†�fã|Å49â2JFð\ê»\]¶¹ªlÓÂð‘,~ZÞ®m®ô'Å¢x@Ô€1xµÌI�vU²Ü:Qób)ÑKÏ8°–ºèlÔ7²ÄÆb7Ë5g[¾z'ðìÎñ±³<îZñ¢Iè`÷¥ÀKX.pf;Ñtt�“u&›ˆå¿•*·5†‰óŒ•«Y•…ÏÇšU¶ª†"³´¼#8G×ä v »Sâ¢P¼?G oìÙ Ž[lœwôßcLãXªraPƒj,"Þlüe}3Ȫ t0u_p£lÖ’fÌU‚×WtRIå j¢1¨ÆÎÂ{ÝøB�Ö¥å¾þ|ôÁ"X¬F�.©ËÚ–iÏš�{ÖèØ_X£ã^ˆü K³I‡�÷žå1ÿš˜ñ´øA6n`U?Áò ê7ˆ¦žÃ÷A~�øQn�Ó)#ž+ÞBÔ€­)K±1a¹Èé2<ŽhÒóÝ'9ÞA¢/ÜZêøùÿ€Ï¯áñï\Ãcó3À]kxüa[œe¬X�‰2GÔÕ�®Š\0jéGYdô$–ëJS«Šk‘®qàËÜ�ÅÎÀ;MƒV(í ½˜­RÅ^+þ·kò—õÍ AÐ|›Ë}‡ å�ɹÓ6˜ËQq­šˆž]žàiaT¬˜ÊæÁâÉQ2þÝÄÌ–A¹LÛhß ‹œ`s´Œ£mÊ`è9kGžµå�$Ô¹fŒ¨ä�ˆclΆðgÄYŸW³|8ëFáCD †A“ÐÝÄLÖÉôa׿9Êùñ)ýcDï±Äc‹w²öMw¯î[ãó òq.œÒûGX.§XWV¬fˆê«V 5Ø\èxG©ŽÂå�å54ÄAÆ#I¥®8¿`ú‰mŒá.ÇÍdª<ѼZ¤¨ñ(0-†ëk›P`T=¯käéeL‰Ý€=,ŸÌÆx_m _e ºLeóà(E«Œ•rTjÜac`µÓÏÑÀᵎ‘:ÁÔéh�QãbicÉËSëÏ�u¾‡¥3½ÑÌr5x±³®Ÿ ˈOo[¥ã¯o&:°~$y±Cr¥®¦ßb¹½ÂÛX¹U$‘x,ð~àûXû.ÀÄòÈÁƒ¨@Š`éXGÏä”>§¤NG”ÒÄKƒÉs„F¥¯‚@%`¯:þîµQßt‰lßãJcç ¢�÷Õ'è‰Á(¼©}/{ÊcdÒÁi£]¬vÉéòüâ8?ÔzˆºÆsß�má¨Í Ä¡Ð�œ@� uJ©s‡ÙÄQskO3ˆÄà° ©´.d„åSÿì_¥ãßÁr×ö7pöÎækÉWˆãnÙÌò5ß |ýÐÚm$‹�ÄcO”v<Ÿh2œ‚ªóœ˜±Úa”h$¥¸UêBѳ`ãL@ïjtiÒ5#Œd„’&[Pôh“ó€kbüÌÂØ`ÁráHå²¾C}¥©,Z•ü”yàÖJƒõüòO/¦A1F£7–dÇKGñCoÀ tð5fqæøÞ³ú9$Î?ö²ÜpãàæU<ÇÅ+|ÿ*àúU<ÇjóóD!î–�µ"Lýù‚uàWWxìOðÈz¬DâBã-DMá‘s½‘ÄêáØ #ZŽ[Œ2¨QÚ@é,'4Ãg5BÑ$˜z~œBÆQ;Ž2‚Jƒm¾Å½6ç ŒÄ ”öeä1kUˆe®ÒWmòKô˜Š=„�/å5B�ííÂhKœóGªq΢ÁD;…'y¸²œçV7Ê>7ÁË‹ÃÔ0(!U,w›yÀÄ2äóË2­3ÏNœÔ)¤Å˜Þˆ5‡P51AÈ«ýÍåŠ}],ÖR—8¶X€8usçŸ÷ÙØ�Õ4ñÍb°32#zñ¼¼úþ'€Ë†÷AàŸ×|w‰ÄÙñËD Ï™ð«DgõAžü·³ÚQbñ¨±:DT;LÆhšÜ2HÍàŒ£(ktMƒÀ8-÷D¼\„!@hMv„÷ZxH´ÕQ�²õhü‰0#fAŒ>®1N2ôM4PWû©ã¹"Ìb¤À„jÎ#BPK�€«Œ?�žíg¸Õ�Òýf”+Â<Š€ûM�Ì¢Õ$%/ðm:r ïën+áñv„×6¾§÷Ðä ¾J# ÃIÆ$gªÊêxõvׯ”›V~q%M#¿PÙEÌ� öO¾xŽÎÿhxQ'²g`í›�—;j†íº,ê'•Ûˆ�®gÂk‰5ŽÁrþK€×zV»Jl( »Üâ›ÿY¼>&J§&´­¥�Œ¢¬S˜I´?Åùöë+˜á[èØË(e„«´@)² ¢é§�JoÕÐsÕ´¯ÉŠêª~pÕ©ÆßÔB,ÿEËôxD=h zˆÆËàyBÙ^(<ÞgxÍPuÜ#Mþ¨~ ¥Ä’ãk:Gp¡ÆWJáÓÌ)ÜVÂM=KKžM(œ¢(ÚSÔTEJ£ ‰ÚF—©hö.  R0uáóz–‹®×“áAÅ�ü—ÖøE 1´þûÀÝk½©Dâó%VYó«œù|ÍÄ$;€íV9L "&Œ2 ô‚¡,³&C²ŒcæUÜ.á-sŽ_™ÏxGÛÒÕ§àe„]E`R£>â7Q9­›*o%„�x#ÃBàªìÒD5 yF2æ�# ”…E5*¬Œ–J�˜ÉjàÉ«`m^,…1üS¶‰?h\B‹Ayuïav…"ÊüPìÓR‰B÷0Ъ£�êõèshà1WÔ–d#g«IÎÒ‘3}žOë²Ú—A£Ð>ßÍÒnÄ�ȇ� ­=�åFˆ‡YÛe‰ÄFâ­,÷ÜÊúÍÑK¬1°Úé––¸ŽZ^ y0ôÈhóL>ÚËä¥w{x8Œƒd<µl°ß48a2P jj–x]u$jª�ÑÌÊe!úÿ\„»íÕ|%ûZîÊžAGF艡4B)Ltg§É Š«ö{¯ÉùÕúÅ|´šGXÓÀwô°§<†Ñfx†ƒ­UmL`o^0"ŸÇ†’ÜæùáÞ<½<ŒÑ"] 3Ô¸Ÿ77•Ÿ…ŸQÆu5¿k J[2æK‚‰™+5ʱc‹V 'ãºT|Œ°’hýL.?ZV‘3ìø¾¹øÍG¸ýfàOÖi/‰ÄFá_Xù÷þ÷ˆC×ç9†+jQ3´Ã(‡ˆ��ÓÄX�¨7²çîM˜�ñªzÁ“Lxeâ±ZIJC=Wù˜µºÙ�qÐÖQ*†ºBCcðqD¨ôQQ/e¤-ïù¦^—›ºð…øPWx0XŽËE”8J1ñ‚A%WûM¾$¬¹X[|[÷.~¬s ;Êi -D:(]Dºär€¿­åêðwµ{™àÄ÷pÚ% %¥÷d½X–œ4Ê”Q¶VÂõ�N¹‹èaµoߺü�†k�¯Z{˜µ/€X°䩜&¿Kòá}”˜Ñ +Ü–H\èü˧(<ŽT¿ ˆõ°]NtV[ª,�¥&Š5��¬xòò6ÉGùö¦òÓ#𤼠.âÅ D‡õo,Zñ:ðQ7g†¨³šª4Tc�£]„.V;í`¤Çˆ*³CÉŸã ^2J ¥X¼X‚1x ªpk£|Aùñömüdûó<§8€ÑyD[mCè‚vz(=–÷@�¢t0t(µ€Pà Ϥó¨Q¦‰Áæ!…ëWÔ”Ýue'íL\�¼i…µ?zk|ÞøƒÖ‡ J7"sÀÏ®°þ·Äòi"ñXä1¸æG�ç®ó^«Ìb§^_guÊ�ÞJãÉz%!xœ)ÈÂ>nÈ÷ó xOM�R ÇE`‹®®²V·Ú_q B%dߢ:ë›�]Œv°ÚÂÑÂÒbT�ðuY ^U9¶x²jLlÔìÕB•±*>gãËLJ. G1aÇ,†ùTÑELCá˜DñïN½C+‚ë!®Ä¸r¡ ¨Ç]I_Ue«;χ g‰3eø¶¡5þp�ÎÿÇ,7Ï|Ëõ^‘?nø¾GÔV%�eþšåÞm†Ø9x.»Žg‰ávÍ.w%TÖÓÓàm á=¥-QSPÒCèñ„r?s’ó€8T¢ˆ¼ßù÷’¢MS ¼ß�1oˆá ¡ %†ûŒÅÒC´ƒÐÂ2�e–Q›#¼m$ð“#%?Ù Lp'Ž9BPJ©L<±„Í63+ÑäYåØ0�0ÚÆh¡ D—Ç#|ÉLq„ÎsË[Qm#ÚÅ…‚¼S�YO°aÅ2àÊÄõ·&�ÕÌëXj ±óm½¦¶`y—]|ÿ:�ÿlðÀw?P]^E²ØM$ )ŸZ»–äëv^cØ}›Fë€�r J¼d=Oi<�Pठ¾‹H›gwFsPàãn¡X ÇáÉÊ+Šèk5k “�âÕpE(ä¶·Ù&*%†.BæÉtŽœì,?ÁÎð1®Ô}\R~”‰ò«xõCåÑÅôG%ãÆ<Žý˜ -žW܇‘6BC‘â�€”|>‹îØ›9À”£Z8é0"Ä”½X­T¼  ®ì#5>v0À¯°þ{ë¼�ß]aíÙØóûÜü�êò�çx/‰ÄFáàgVXÿàŠuÞKb•X:„¹_ÜZe®j6`|‰ÒÃÓÅI´ÅfsŒ'…¯p›ÝÄýbcâuÂU¾àÙeÔžÜeÿ�7S¸<ĵϹ ‚D $ôiWz¨yŒÌÑô1î!—ã(¢>,ˆÁcñ"¼§~9F•ëz·âBè"„Êã*ªBõÜâ&yH¢Û5å§‘²E)Œë"ÝëKj6à[aÅ2à5…}°2]‚HÊ^]@¼xÂÐÚ½À߯ó>n":—²ƒsçŸH$Ξß>9´Ö ê*7zçobL” Õc§Û~»·ˆâÛ“•h(Èéb$>ªs¼¸ó)ž Âÿªm�Á%Aµ €à›Š� Q°þ)—scÖäê*Øš‘Œ[ÜDe™àQ�Ù+#Œï µQhÆãMT‰Åc¹1»ˆ{Í8WûyŠ?L0±»0TUÀ"bÐjFa[ 7dO`\�ò’Þ§±fñ±kW`³’ºóŒº@¨LŒ.”oz“1èc�•,þ�XâZoÞµÂÚù bO$+ˆ%òá&˜½œ~u‰! {ªÌÊ5�Âl§ƒ‰ºóŒ%õ¬ —&´1fŽšÌ°SàÚò ”&“o‰A>ŠÙU…ïîuØbÙðc®Æaq4+±ûDz‹ b•mB)JP�O4à� †R]tsWËÍùf>”?€ÉÐáºî¿ÇR$oˆù"qxªQ߀÷Ô®bFbá7önÀ1CZÔ³6¶èR =jÝ€oôÄÒlÕ~€ª��nÀEÿª”¡º0y"ðÒ¡µ‚õ­ón`fhm/ÑÕ<‘HœŸÜ üÚ ë¿l[ç½$Î’Å!Ì·¥°Øe”C^› 쯅Ê8³‹J¯ófQ™á�Op_s'ší|ÉnÂåðêò8A1u5|G¯ËçufŒðoY�ËBÁ=b8`Fø¬ÛÎsŠ6b,¥±ˆÔZJŠ£0�Œ2Ôø²›âúì2hhÉ÷vo£FY�qŽCšÍI5C%z_ýeíJ¾j£¶êiå'y~ïŒÌÑÐ9œo“içJj”ÔÅsD”+EëCÂõG(¦äí£f;pã:Ÿó}œž¡çëYþ½xhÕvtzÌÃrÑú÷}¡ëÃo²¾ŽñŸfùðêÄ…Å/ßÎRÙÁq–àëÖøÜ¿üøŸc�/±²ÝÄjð»,ÿð¹–|xÛà‚ã&`ï>e÷ž¥w Añ6PsÊ’š)eusô|›50¦Æ÷u>È;ßÂaÙÄçÝ&f%ãºbš-A h`3Â÷w»üq½Æqî1N•R„d—óÔòcxT/C 0cÍ X4dx[çÓùvÞŸíÂ#d^×ýw.Ö6Áˆ‰*SéÆÔ€q“:Y»šûÌvï¿Ìk»7P“�Ãæ-lÑECJš¥g¼(l«øJ´^WÔ•}û ÿR]\·V?«Çuà…ë|Îa�Ò#Ñ$¦è‡YoÑú0¿ËòÀêõÀ[Xß(�e®]çóuO}—ÄyN‹øw|K?̽–èûvÞûš5<öJÔN}—3f¥ùªkÉìð‚Y’yé[/ì'j­¶Ø@}Þ“w<]jÒ!ódv–¼8ŽÓcŒêü©óvèî²£üNmûlF @M>3‰ç �;ª²`)ñgNrþ**^štcÆ\Í!wÓæUœ°/cÖ¾˜–}&Ÿ³»øÛ*¨rª|[÷~vù.Q€oQqÍÑ�¨Ñ£ÆMîqüVý낪Ëý¼±óWdzË ªs¸N‹&¤,(�ç˜ ©´UýlÕ‚hªÒi¼~’2 ¤²à…·›†Ön>vö2È��­�- ‰ÄùË¿´Âzws1и/ꇵV_ è9�=^Rj,Z�źãˆNGã oœ¿�§ù{`^ïÏ7óŽÚ$·Pí!Z2¡%oè´xJågÕç6·™g—Óq×Ò“§q·¯ñWxwnì óLryØÁ¥AhòÚ;»»®2ŒÿÞw­}ö™='™Ì¤É4mhƒÆV,hK¤ñ½òŠ ‚½Ñ‚—Ò¿@o/Do¡‚A‘ÒB¨ÕÚ�´ÛRiÚÆ:ùNæ«3çcïµÖëÅ>ó=‰1mK×6gŸÍ™³×|pæáyßõ¼‘ïŒæ¸/öÇ;=IØåÜ0¿~ÁöÈ8@;¿ðFø;GÛ�±ÿ'v[ï_nÂ}žâÃí§Ú�ÜâûßL^Úv­+²�çØù÷õa/ßegŒJIR¼=PôåinýŒÎ* ø nüóîƒâ•íÄŒvŽß ¤ézL`(0¹Zñ�r¦ñÌVžó{: C…úŠ››"È ‘ýDÙO²i�áIÿY^ðGéKgãnÀljøTqo²"ÊŠ)FâøñÞ ð«ÁîK¿ÓÁ#ˆÍ£6ÏP§Ôó–«x[÷pY·¦bà 6/q¼~el�ŽÍci‘IY@Ó Þ­¢£G'j.Dî*\b¶Hm³º7(­ =¹µ ¸ó±ýs~U&“Éd2[6„Õø9'Žc‹¸¢VþY;f{Žwlª Æ ún’ÀödlššœN‘Ò#ÙÇóþ“¼ì�pV÷í�Šq0ÖLãøFíùå^Û>m?œ„Ò~Û‰Ìk¹ãý:8bs|>¼ÁçÂÛ+x[–𶈋‹LÈR+ªtçôbCq¹¡ç"Ñ%B‘ˆjßõZÄÂÉ�¢ªu«²°Êd2™L&l/ž`-F@ÖãŽzc.û\"¬À]½šw—ÀM�‹`Þ�ñDj×бšVIŸ2­p<,r<¾ÁÙÏ‹z7ïøœÕ)"JB8ïJ¦Ç9‹‹ÿE“,$8è=Wt#&¢gîNç¹'Îñ@8MÉ*jí¼@ec™B–�¸LW–‘´‚×v„Íž&0h¢‰èŠÄÜ&QutìVµ¢ªeóNÀ,ª2™L&“Élbͱ‚Í®°^<5|®ÔÊá(¸¨ô+Ï⨠îó âF—‘NB쥇±s{ˆiQz“ˆL¬Bµ¤/or�Ó~?gt†®MñP]\—cå\Ã3ÅYîJ—¸'œãPZ c3Dlãtø‚÷@V±øî=ÄVдJ—!=7 hEÓP¯F>á# Æ}U×SÌnU&“Éd2™mlVp­’àémâÊ÷—‡KÓžŠ«©K?VD«P­h´"ÅI[QÕX…£‹I—DI뇘y’tÊ×y-×ì±úQ{ž‚—1kPêñ(�vF Kƒv×¢ôñ¶JJ}º¶B-¦´�¯‡XYs{lhº�úì�‰ª�ç 9f!“Éd2™�;bíîºVPýÙxymqµ•²ç(GÝñ\Ö*†tq”46AtÔiO—‘NPPÒX¤ƒÑA(Ú˜ñ4ú �/�wn]å^�ïUÆAÑãgWk?àL�cqDdH!œ ð2  C"# 1m#‚�ÔÃÈíýÀŒK\PÛѬþ:¬÷U� -�¶%À«Ž¯ÉnU&“Éd2™VX=ЬG¬‰«µ]‚'�I›4~qUìU†µ£,=ËÞC]PK´ÀQ2‡uJб j¤ƒ¤O’’#H‡�ÿ6ÉîçT4^l„‘ÁŽF)� û9e|±ˆJ ¨6¨Õë–øÚŒðÖŠ­Nª±²fohŠÈÁ‹�¤‰—vuª6‹ª–ìVe2™L&“¹.Ö+Ø,Øžo‰`€-âÊ¡hMmiðLÏQG]:šŽ£l<- é`΃4êI¡ƒ3G-¦5ÉaêXò_¡±¯!25^‹¡¼L•~MÕü ‘H’„·€H XÝÐI|C'5HlP8˜j–]¢;ŒL®¶;ÿLl}÷ßá÷)ªÈnU&“Éd2™–Í ®W\­5´ÎÕÊ¡(\Ž­È:ÓsÌDeØu,GQ8Rð$ç©L¨§Žžä¦5E�ÒO5ÅMrŒüDJ¼�§+K”’h‚¡šH’(c$Å„j$„È„‹„&Rù€+¡ŽÌøHUFÒ™´îR­•þæ€0Wó>DUv«2™L&“ɬ±]XÁµÄÕæ²à©‘p sA˜o”CI8ÌGE÷*+“ÊpäpQ&¥)>)uR¼wÄä(�~tDS¤Ĥíkj  ‰T&í1©‰àšHÒDûHQ·nTé•KTeÄþmíkÔ¸(Æt‘ÖÕOÃÖHØ!ª6÷Uí² ²[•Éd2™LfƒÝ„\S\�Ú_ Ÿ�õÒàh“Ú“°Ð(MÐ$̘ðnRÜ!e1aÛGM‚+•¡ • #Û¸¯ŒÏ×U)† �4~ÞU£tÇ©r‰ØOô|–Ú×\Ò6kÖ¨1륿S°.ª6vÿÁnNUU™L&“Éd®ƒ« +¸fYv”7»W‡�sIÐ(Ì—RëDÝÌï$ ÓÀböK©}¯)`9]}ˆ¬i+d¦ÔXö©±L«1?I�ÛĸD{>ëŒs°µ— ¶”þÖDÕ ÖÂQ!‹ªL&“Éd27ÀåÛªÐ/¡¯ IEND®B`‚Flametree-0.2.1/pypi-readme.rst000066400000000000000000000050541500740312400163640ustar00rootroot00000000000000Flametree ========= Flametree is a Python library which provides a simple syntax for handling files and folders (no ``os.path.join``, ``os.listdir`` etc.), and works the same way for different file systems. Write a Flametree program to read/write files in disk folders, and your code will also be able to read/write in zip archives and virtual (in-memory) archives - which is particularly useful on web servers. As an illustration, here is how to use Flametree to read a file ``texts/poems/the_raven.txt``, replace all occurences of the word "raven" by "seagull" in the text, and write the result to a new file ``the_seagull.txt`` in the same folder: .. code:: python from flametree import file_tree with file_tree("texts") as root: poem_text = root.poems.the_raven_txt.read() new_text = poem_text.replace("raven", "seagull") root.poems._file("the_seagull.txt").write(new_text) Even in this very simple use case, the syntax is clearer than the ``os`` way, which would write as follows: .. code:: python import os with open(os.path.join("poems", "the_raven.txt"), "r") as f: poem_text = f.read() new_text = poem_text.replace("raven", "seagull") with open(os.path.join("poems", "the_seagull.txt"), "w") as f: content = f.write(new_text) Moreover, the same Flametree code also works for files inside a zip archive: .. code:: python with file_tree("my_archive.zip") as root: poem_text = root.poems.the_raven_txt.read() new_text = poem_text.replace("raven", "seagull") root.poems._file("the_seagull.txt").write(new_text) Now in hard mode: suppose that your server receives binary zip data of an archive containing ``poems/the_raven.txt``, and must return back a new zip containing a file ``poems/the_seagull.txt``. Here again, the syntax of the core operations is the same: .. code:: python destination_zip = file_tree("@memory") # Create a new virtual zip with file_tree(the_raven_zip_data) as root: poem_text = root.poems.the_raven_txt.read() new_text = poem_text.replace("raven", "seagull") destination_zip._dir("poems")._file("the_seagull.txt").write(new_text) destination_zip_data = destination_zip._close() # Now send the data to the client See section *Usage* below for more examples and features. Infos ----- **PIP installation:** .. code:: bash pip install flametree **Github Page:** ``_ **License:** MIT Copyright 2017 Edinburgh Genome Foundry, University of Edinburgh Flametree-0.2.1/pyproject.toml000066400000000000000000000007301500740312400163260ustar00rootroot00000000000000[project] name = "flametree" version = "0.2.1" license = "MIT" authors = [{ name = "Zulko" }] description = "Python file and zip operations made easy" readme = "pypi-readme.rst" keywords = ["file system", "zip", "archive", "file", "directory"] [project.urls] Homepage = "https://github.com/Edinburgh-Genome-Foundry/Flametree" [build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] exclude = ["docs*", "tests*"] Flametree-0.2.1/tests/000077500000000000000000000000001500740312400145545ustar00rootroot00000000000000Flametree-0.2.1/tests/test_basics.py000066400000000000000000000141641500740312400174370ustar00rootroot00000000000000import os import sys from flametree import file_tree, DiskFileManager, ZipFileManager import pytest PYTHON3 = sys.version_info[0] == 3 zip_path = "test.zip" test_dir = "testdir" ALL_FILES = set(["bla.txt", "bli.txt", "blu.txt", "Readme.md"]) def test_directory(tmpdir): # CREATE AND POPULATE A DIRECTORY FROM SCRATCH dir_path = os.path.join(str(tmpdir), "test_dir") root = file_tree(dir_path) assert root._file_manager.__class__ == DiskFileManager root._file("Readme.md").write("This is a test zip") root._dir("texts")._dir("shorts")._file("bla.txt").write("bla bla bla") root.texts.shorts._file("bli.txt").write("bli bli bli") root.texts.shorts._file("blu.txt").write("blu blu blu") # READ AN EXISTING FILE (two ways) assert root.texts.shorts.bla_txt.read() == "bla bla bla" with root.texts.shorts.bla_txt.open("r") as f: assert f.read() == "bla bla bla" # TEST REPLACE BEHAVIOR (replace=False) root._dir("trash")._file("bla.txt").write("bla bla bla") root._dir("trash")._file("blu.txt").write("blu blu blu") assert root.trash._filenames == ["blu.txt"] root.trash._delete() root._dir("trash")._file("bla.txt").write("bla bla bla") root._dir("trash", replace=False)._file("blu.txt").write("blu blu blu") assert set(root.trash._filenames) == set(["bla.txt", "blu.txt"]) # READ AN EXISTING DIRECTORY root = file_tree(dir_path) assert set([f._name for f in root._all_files]) == ALL_FILES # APPEND TO AN EXISTING DIRECTORY root._dir("newdir")._file("new_file.txt").write("I am a new file") root = file_tree(dir_path) assert set([f._name for f in root._all_files]) == ALL_FILES.union( set(["new_file.txt"]) ) # TEST DELETION path = root.newdir.new_file_txt._path assert os.path.exists(path) root.newdir.new_file_txt.delete() assert not os.path.exists(path) assert not any([f._path == path for f in root.newdir._files]) # TEST DIRECTORY COPYING root._dir("new_folder") root.texts._copy(root.new_folder) assert [d._name for d in root.new_folder._dirs] == ["texts"] # TEST DIRECTORY MOVING root._dir("newer_folder") root.new_folder.texts._move(root.newer_folder) assert [d._name for d in root.new_folder._dirs] == [] assert [d._name for d in root.newer_folder._dirs] == ["texts"] # TEST FILE COPYING root._dir("newest_folder") root.newer_folder.texts.shorts.bla_txt.copy(root.newest_folder) assert [f._name for f in root.newest_folder._files] == ["bla.txt"] # TEST FILE MOVING root._dir("newester_folder") root.newer_folder.texts.shorts.bla_txt.move(root.newester_folder) assert [f._name for f in root.newester_folder._files] == ["bla.txt"] remaining_files = set([d._name for d in root.newer_folder._all_files]) assert remaining_files == set(["bli.txt", "blu.txt"]) def test_zip(tmpdir): # CREATE AND POPULATE A ZIP FROM SCRATCH, IN MEMORY root = file_tree("@memory") assert root._file_manager.__class__ == ZipFileManager root._file("Readme.md").write("This is a test zip") root._dir("texts")._dir("shorts")._file("bla.txt").write("bla bla bla") root.texts.shorts._file("bli.txt").write("bli bli bli") root.texts.shorts._file("blu.txt").write("blu blu blu") data = root._close() # READ A ZIP FROM DATA IN MEMORY root = file_tree(data) assert set([f._name for f in root._all_files]) == ALL_FILES # APPEND TO A CREATED ZIP IN MEMORY root._dir("newdir")._file("new_file.txt").write("I am a new file") new_data = root._close() # VERIFY THAT THE DATA RECEIVED CAN BE WRITTEN TO A VALID ZIP zip_path = os.path.join(str(tmpdir), "test.zip") with open(zip_path, ("wb" if PYTHON3 else "w")) as f: f.write(new_data) root = file_tree(zip_path) # READ ZIP FROM DISK, APPEND TO IT assert set([f._name for f in root._all_files]) == ALL_FILES.union( set(["new_file.txt"]) ) root.newdir._file("new_file2.txt").write("I am another new file") root._close() # REOPEN A ZIP root = file_tree(zip_path) assert set([f._name for f in root._all_files]) == ALL_FILES.union( set(["new_file.txt", "new_file2.txt"]) ) print(root._tree_view()) root.newdir._file("new_file_that_doesnt_exist.txt") with pytest.raises(NotImplementedError): root.newdir._file("new_file.txt") # overwrite is not supported. # Open/read file in zip with root.texts.shorts.bla_txt.open("r") as f: assert f.read() == "bla bla bla" def test_file_tree(tmpdir): """Assert that the directory function dispatches as expected.""" root = file_tree(os.path.join(str(tmpdir), "my_folder/")) assert root._file_manager.__class__ == DiskFileManager root = file_tree(os.path.join(str(tmpdir), "my_archive.zip")) assert root._file_manager.__class__ == ZipFileManager root = file_tree("@memory") assert root._file_manager.__class__ == ZipFileManager root._file("test.txt").write("bla bla bla bla bla bla bla bla") data = root._close() root = file_tree(data) assert root._file_manager.__class__ == ZipFileManager assert [f._name for f in root._all_files] == ["test.txt"] def test_matplotlib_writer(tmpdir): zip_path = os.path.join(str(tmpdir), "archive.zip") folder_path = os.path.join(str(tmpdir), "test_folder") try: import matplotlib.pyplot as plt except ImportError: print("Not running test_matplotlib_writer: Matplotlib not available.") return True fig, ax = plt.subplots(1) with file_tree(zip_path) as zip_root: fig_dir = zip_root._dir("figures", replace=False) fig.savefig(fig_dir._file("fig.png").open("wb"), format="png") fig.savefig(fig_dir._file("fig.pdf").open("wb"), format="pdf") with file_tree(folder_path) as root: fig_dir = root._dir("figures", replace=False) fig.savefig(fig_dir._file("fig.png").open("wb"), format="png") # fig.savefig(fig_dir._file("fig.pdf").open("wb"), format="pdf") assert set([f._name for f in root._all_files]) == set(["fig.png", "fig.pdf"]) assert os.path.exists(os.path.join(folder_path, "figures", "fig.pdf")) Flametree-0.2.1/tests/test_third_parties.py000066400000000000000000000027611500740312400210340ustar00rootroot00000000000000import os import sys from flametree import file_tree, DiskFileManager, ZipFileManager import pytest # THIRD PARTY LIBS BEING TESTED import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt from matplotlib.backends.backend_pdf import PdfPages import weasyprint import pandas PYTHON3 = sys.version_info[0] == 3 def test_weasyprint(tmpdir): path = str(tmpdir) root = file_tree(str(tmpdir)) # or 'archive.zip' to write in an archive. html = weasyprint.HTML(string="Hello world!", base_url=".") html.write_pdf(root._dir("pdfs")._file("test.pdf")) with open(os.path.join(path, "pdfs", "test.pdf"), "rb") as f: filesize = len(f.read()) assert 4000 < filesize def test_matplotlib(tmpdir): path = str(tmpdir) root = file_tree(path) fig, ax = plt.subplots(1) fig.savefig(root._file("fig1.png").open("wb"), format="png") with PdfPages(root._file("plots.pdf").open("wb")) as pdf_io: for i in range(3): fig, ax = plt.subplots(1) pdf_io.savefig(fig) plt.close(fig) with open(os.path.join(path, "fig1.png"), "rb") as f: filesize = len(f.read()) assert filesize > 7000 with open(os.path.join(path, "plots.pdf"), "rb") as f: filesize = len(f.read()) assert filesize > 6000 def test_pandas(tmpdir): path = str(tmpdir) root = file_tree("@memory") root._file("test.csv").write("A,B,C\n1,2,3\n4,5,6") dataframe = pandas.read_csv(root.test_csv.open("r"))