pax_global_header 0000666 0000000 0000000 00000000064 15007403124 0014506 g ustar 00root root 0000000 0000000 52 comment=c414f744e0f01c2e3d351fd9f4376a72b6c37d68
Flametree-0.2.1/ 0000775 0000000 0000000 00000000000 15007403124 0013412 5 ustar 00root root 0000000 0000000 Flametree-0.2.1/.codeclimate.yml 0000664 0000000 0000000 00000000067 15007403124 0016467 0 ustar 00root root 0000000 0000000 exclude_patterns:
- "ez_setup.py"
- "tests/"
- "docs/"
Flametree-0.2.1/.github/ 0000775 0000000 0000000 00000000000 15007403124 0014752 5 ustar 00root root 0000000 0000000 Flametree-0.2.1/.github/workflows/ 0000775 0000000 0000000 00000000000 15007403124 0017007 5 ustar 00root root 0000000 0000000 Flametree-0.2.1/.github/workflows/build.yml 0000664 0000000 0000000 00000001511 15007403124 0020627 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000000725 15007403124 0021354 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000001736 15007403124 0021207 0 ustar 00root root 0000000 0000000 name: 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/.gitignore 0000664 0000000 0000000 00000003407 15007403124 0015406 0 ustar 00root root 0000000 0000000 # 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/ 0000775 0000000 0000000 00000000000 15007403124 0014753 5 ustar 00root root 0000000 0000000 Flametree-0.2.1/.vscode/settings.json 0000664 0000000 0000000 00000000055 15007403124 0017506 0 ustar 00root root 0000000 0000000 {
"python.formatting.provider": "black"
} Flametree-0.2.1/LICENSE 0000664 0000000 0000000 00000002122 15007403124 0014414 0 ustar 00root root 0000000 0000000 MIT 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.rst 0000664 0000000 0000000 00000025461 15007403124 0015111 0 ustar 00root root 0000000 0000000 .. raw:: html
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/ 0000775 0000000 0000000 00000000000 15007403124 0014342 5 ustar 00root root 0000000 0000000 Flametree-0.2.1/docs/Makefile 0000664 0000000 0000000 00000001172 15007403124 0016003 0 ustar 00root root 0000000 0000000 # 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.py 0000664 0000000 0000000 00000002227 15007403124 0015644 0 ustar 00root root 0000000 0000000 # 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.rst 0000664 0000000 0000000 00000000470 15007403124 0016204 0 ustar 00root root 0000000 0000000 .. 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.bat 0000664 0000000 0000000 00000001440 15007403124 0015746 0 ustar 00root root 0000000 0000000 @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.rst 0000664 0000000 0000000 00000000676 15007403124 0015661 0 ustar 00root root 0000000 0000000 API 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.txt 0000664 0000000 0000000 00000000021 15007403124 0017617 0 ustar 00root root 0000000 0000000 sphinx_rtd_theme
Flametree-0.2.1/flametree/ 0000775 0000000 0000000 00000000000 15007403124 0015356 5 ustar 00root root 0000000 0000000 Flametree-0.2.1/flametree/Directory.py 0000664 0000000 0000000 00000017373 15007403124 0017707 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000005542 15007403124 0020723 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000013662 15007403124 0020575 0 ustar 00root root 0000000 0000000 import 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__.py 0000664 0000000 0000000 00000000353 15007403124 0017470 0 ustar 00root root 0000000 0000000 """ 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.py 0000664 0000000 0000000 00000002640 15007403124 0017072 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000000026 15007403124 0017413 0 ustar 00root root 0000000 0000000 __version__ = "0.2.1"
Flametree-0.2.1/images/ 0000775 0000000 0000000 00000000000 15007403124 0014657 5 ustar 00root root 0000000 0000000 Flametree-0.2.1/images/autocomplete.png 0000664 0000000 0000000 00000022630 15007403124 0020071 0 ustar 00root root 0000000 0000000 ‰PNG
IHDR t ƒ áw sBITÛáOà tEXtSoftware Shutterc‚Ð 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]