diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..b4e7ee8 --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +ignore = E203, E266, E501, W503, F403, F401, F405 +max-line-length = 120 +select = B,C,E,F,W,T4,B9 +exclude = __pycache__ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..dd4e905 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,38 @@ +repos: + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + args: ["--profile", "black", "--filter-files"] + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-merge-conflict + - id: end-of-file-fixer + - id: requirements-txt-fixer + - id: trailing-whitespace + args: ["--markdown-linebreak-ext=md"] + + - repo: https://github.com/asottile/pyupgrade + rev: v3.3.1 + hooks: + - id: pyupgrade + args: ["--py36-plus"] + + - repo: https://github.com/asottile/add-trailing-comma + rev: v2.4.0 + hooks: + - id: add-trailing-comma + args: ["--py36-plus"] + + - repo: https://github.com/psf/black + rev: 23.3.0 + hooks: + - id: black + language_version: python3 + + - repo: https://github.com/PyCQA/flake8 + rev: 6.0.0 + hooks: + - id: flake8 diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 0000000..7daf7ac --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,9 @@ +# Credits + +## Development Lead + +- [Alican Toprak ](mailto:alican@querhin.com) + +## Contributors + +- [Michael Pƶlzl ](mailto:git@michaelpoelzl.at) diff --git a/AUTHORS.rst b/AUTHORS.rst deleted file mode 100644 index ca78bb3..0000000 --- a/AUTHORS.rst +++ /dev/null @@ -1,13 +0,0 @@ -======= -Credits -======= - -Development Lead ----------------- - -* Alican Toprak - -Contributors ------------- - -None yet. Why not be the first? diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..bc8094e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,119 @@ +# Contributing + +Contributions are welcome, and they are greatly appreciated! Every +little bit helps, and credit will always be given. + +You can contribute in many ways: + +## Types of Contributions + +### Report Bugs + +Report bugs at . + +If you are reporting a bug, please include: + +- Your operating system name and version. +- Any details about your local setup that might be helpful in troubleshooting. +- Detailed steps to reproduce the bug. + +### Fix Bugs + +Look through the GitHub issues for bugs. Anything tagged with "bug" is +open to whoever wants to implement it. + +### Implement Features + +Look through the GitHub issues for features. Anything tagged with +"feature" is open to whoever wants to implement it. + +### Write Documentation + +`django-tus` could always use more documentation, whether as part of the +official `django-tus` docs, in docstrings, or even on the web in blog +posts, articles, and such. + +### Submit Feedback + +The best way to send feedback is to file an issue at +. + +If you are proposing a feature: + +- Explain in detail how it would work. +- Keep the scope as narrow as possible, to make it easier to implement. +- Remember that this is a volunteer-driven project, and that contributions + are welcome :) + +## Get Started! + +Ready to contribute? Here's how to set up `django-tus` for +local development. + +1. Fork the `django-tus` repo on GitHub. + +2. Clone your fork locally: + + ```sh + git clone git@github.com:your_name_here/django-tus.git + ``` + +3. Install your local copy into a virtualenv. Assuming you have + virtualenvwrapper installed, this is how you set up your fork for + local development: + + ```sh + mkvirtualenv django-tus + cd django-tus/ + python setup.py develop + ``` + +4. Create a branch for local development: + + ```sh + git checkout -b name-of-your-bugfix-or-feature + ``` + + Now you can make your changes locally. + +5. When you're done making changes, check that your changes pass + flake8 and the tests, including testing other Python versions with + tox: + + ```sh + flake8 django_tus tests + python setup.py test + tox + ``` + + To get flake8 and tox, just pip install them into your virtualenv. + +6. Commit your changes and push your branch to GitHub: + + ```sh + git add . + git commit -m "Your detailed description of your changes." + git push origin name-of-your-bugfix-or-feature + ``` + +7. Submit a pull request through the GitHub website. + +## Pull Request Guidelines + +Before you submit a pull request, check that it meets these guidelines: + +1. The pull request should include tests. +2. If the pull request adds functionality, the docs should be updated. + Put your new functionality into a function with a docstring, and add + the feature to the list in README.md. +3. The pull request should work for Python 3.9+, and for + PyPy. Check + and make sure that the tests pass for all supported Python versions. + +## Tips + +To run a subset of tests: + + ```sh + python -m unittest tests.test_django_tus + ``` diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst deleted file mode 100644 index 6bd194a..0000000 --- a/CONTRIBUTING.rst +++ /dev/null @@ -1,112 +0,0 @@ -============ -Contributing -============ - -Contributions are welcome, and they are greatly appreciated! Every -little bit helps, and credit will always be given. - -You can contribute in many ways: - -Types of Contributions ----------------------- - -Report Bugs -~~~~~~~~~~~ - -Report bugs at https://github.com/alican/django-tus/issues. - -If you are reporting a bug, please include: - -* Your operating system name and version. -* Any details about your local setup that might be helpful in troubleshooting. -* Detailed steps to reproduce the bug. - -Fix Bugs -~~~~~~~~ - -Look through the GitHub issues for bugs. Anything tagged with "bug" -is open to whoever wants to implement it. - -Implement Features -~~~~~~~~~~~~~~~~~~ - -Look through the GitHub issues for features. Anything tagged with "feature" -is open to whoever wants to implement it. - -Write Documentation -~~~~~~~~~~~~~~~~~~~ - -django-tus could always use more documentation, whether as part of the -official django-tus docs, in docstrings, or even on the web in blog posts, -articles, and such. - -Submit Feedback -~~~~~~~~~~~~~~~ - -The best way to send feedback is to file an issue at https://github.com/alican/django-tus/issues. - -If you are proposing a feature: - -* Explain in detail how it would work. -* Keep the scope as narrow as possible, to make it easier to implement. -* Remember that this is a volunteer-driven project, and that contributions - are welcome :) - -Get Started! ------------- - -Ready to contribute? Here's how to set up `django-tus` for local development. - -1. Fork the `django-tus` repo on GitHub. -2. Clone your fork locally:: - - $ git clone git@github.com:your_name_here/django-tus.git - -3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: - - $ mkvirtualenv django-tus - $ cd django-tus/ - $ python setup.py develop - -4. Create a branch for local development:: - - $ git checkout -b name-of-your-bugfix-or-feature - - Now you can make your changes locally. - -5. When you're done making changes, check that your changes pass flake8 and the - tests, including testing other Python versions with tox:: - - $ flake8 django_tus tests - $ python setup.py test - $ tox - - To get flake8 and tox, just pip install them into your virtualenv. - -6. Commit your changes and push your branch to GitHub:: - - $ git add . - $ git commit -m "Your detailed description of your changes." - $ git push origin name-of-your-bugfix-or-feature - -7. Submit a pull request through the GitHub website. - -Pull Request Guidelines ------------------------ - -Before you submit a pull request, check that it meets these guidelines: - -1. The pull request should include tests. -2. If the pull request adds functionality, the docs should be updated. Put - your new functionality into a function with a docstring, and add the - feature to the list in README.rst. -3. The pull request should work for Python 2.6, 2.7, and 3.3, and for PyPy. Check - https://travis-ci.org/alican/django-tus/pull_requests - and make sure that the tests pass for all supported Python versions. - -Tips ----- - -To run a subset of tests:: - - $ python -m unittest tests.test_django_tus diff --git a/HISTORY.md b/HISTORY.md new file mode 100644 index 0000000..143e1b8 --- /dev/null +++ b/HISTORY.md @@ -0,0 +1,9 @@ +# History + +## 0.5.0 (2020-10-01) + +- Second release on PyPI. + +## 0.1.0 (2016-08-06) + +- First release on PyPI. diff --git a/HISTORY.rst b/HISTORY.rst deleted file mode 100644 index b82df72..0000000 --- a/HISTORY.rst +++ /dev/null @@ -1,9 +0,0 @@ -.. :changelog: - -History -------- - -0.1.0 (2016-08-06) -++++++++++++++++++ - -* First release on PyPI. diff --git a/LICENSE b/LICENSE index eea98c3..724713b 100644 --- a/LICENSE +++ b/LICENSE @@ -8,7 +8,3 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of 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. - - - - diff --git a/MANIFEST.in b/MANIFEST.in index 06ebb91..0c45cb3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ -include AUTHORS.rst -include CONTRIBUTING.rst -include HISTORY.rst +include AUTHORS.md +include CONTRIBUTING.md +include HISTORY.md include LICENSE -include README.rst +include README.md recursive-include django_tus *.html *.png *.gif *js *.css *jpg *jpeg *svg *py diff --git a/README.md b/README.md new file mode 100644 index 0000000..9c82e94 --- /dev/null +++ b/README.md @@ -0,0 +1,142 @@ +# django-tus + +[![image](https://badge.fury.io/py/django-tus.png)](https://badge.fury.io/py/django-tus) + +[![image](https://travis-ci.org/alican/django-tus.png?branch=master)](https://travis-ci.org/alican/django-tus) + +Django app implementing server side of tus protocol to powering +resumable file uploads for Django projects. + +## Supported Django/Python Versions + +- Django 3.2 LTS and Django 4.0, 4.1, 4.2 LTS +- Python 3.9+ + +## Documentation + +The full documentation is at . + +## Example project + +This example Django project includes a javascript TUS demo client and +implements `django-tus` as tus server: +https://github.com/alican/django-tus-example/ + +## Quickstart + +Install `django-tus`: + +```sh +pip install django-tus +``` + +Add 'django_tus' to your INSTALLED_APPS setting.: + +```py +INSTALLED_APPS = [ + ... + "django_tus", +] +``` + +Add following urls to your urls.py: + +```py +from django.urls import path +from django_tus.views import TusUpload + +... + +path("upload/", TusUpload.as_view(), name="tus_upload"), +path("upload//", TusUpload.as_view(), name="tus_upload_chunks"), +``` + +Configure and add these settings in your settings.py: + +```py +TUS_UPLOAD_DIR = os.path.join(BASE_DIR, "tus_upload") +TUS_DESTINATION_DIR = os.path.join(BASE_DIR, "media", "uploads") +TUS_FILE_NAME_FORMAT = "increment" # Other options are: "random-suffix", "random", "keep" +TUS_EXISTING_FILE = "error" # Other options are: "overwrite", "error", "rename" +``` + +Django has a setting for maximal memory size for uploaded files. This +setting needs to be higher than the chunk size of the tus client: + +```py +DATA_UPLOAD_MAX_MEMORY_SIZE = 5242880 +``` + +Since `django-tus` uses the Django cache, if you are running multiple +instances (e.g. via uwsgi) you need to change the default cache to +either database-backed or file-backed, for example like this: + +```py +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', + 'LOCATION': '/var/tmp/django_cache', + } +} +``` + +## Todo + +- Concatenation Tus extension is not implemented +- More Tus-Extensions + +## Running Tests + +Activate your virtual env, then install the testing requirements with: + +```sh +pip install -r requirements_test.txt +``` + +Run the tests with: + +```sh +pytest +``` + +You can even generate a coverage report with: + +```sh +pytest --cov=django_tus --cov-report=html +``` + +You can run + +```sh +tox +``` + +to test against multiple Python and Django versions. + +## Credits + +- http://tus.io/protocols/resumable-upload.html +- https://github.com/matthoskins1980/Flask-Tus + +## MIT License + +Copyright (c) 2020, Alican Toprak + +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. diff --git a/README.rst b/README.rst deleted file mode 100644 index b85e82d..0000000 --- a/README.rst +++ /dev/null @@ -1,98 +0,0 @@ -============================= -django-tus -============================= - -.. image:: https://badge.fury.io/py/django-tus.png - :target: https://badge.fury.io/py/django-tus - -.. image:: https://travis-ci.org/alican/django-tus.png?branch=master - :target: https://travis-ci.org/alican/django-tus - -Django app implementing server side of tus protocol to powering resumable file uploads for django projects. - -Supported Django/Python Versions ---------------------------------- - - Django 2.2.x LTS and - Django 3.0.x, 3.1.x, 3.2.x - - Python > 3.5 - -Documentation -------------- - -The full documentation is at https://django-tus.readthedocs.org. - -Example project ---------------- - -This example django project includes a javascript TUS demo client and implements django-tus as tus server:: https://github.com/alican/django-tus-example/ - -Quickstart -------------- - -Install django-tus:: - - pip install django-tus - - -Add 'django_tus' to your INSTALLED_APPS setting.:: - - INSTALLED_APPS = ( - ... - 'django_tus', - ) - -Add following urls to your urls.py.:: - - path('upload/', TusUpload.as_view(), name='tus_upload'), - path('upload/', TusUpload.as_view(), name='tus_upload_chunks'), - - -Configure and add this settings in your settings.py:: - - TUS_UPLOAD_DIR = os.path.join(BASE_DIR, 'tus_upload') - TUS_DESTINATION_DIR = os.path.join(BASE_DIR, 'media', 'uploads') - TUS_FILE_NAME_FORMAT = 'increment' # Other options are: 'random-suffix', 'random', 'keep' - TUS_EXISTING_FILE = 'error' # Other options are: 'overwrite', 'error', 'rename' - - -Django has a setting for maximal memory size for uploaded files. This setting needs to be higher than the chunksize of -the tus client:: - - DATA_UPLOAD_MAX_MEMORY_SIZE = 5242880 - - -Todo --------- - -* More Tus-Extensions - -Running Tests --------------- - -Activate your virtual env, then install the testing requirements with `pip install -r requirements_test.txt`. - -Run the tests with `pytest`. - -You can even generate a coverage report with `pytest --cov=django_tus --cov-report=html`. - -You can run `tox` to test against multiple Python and Django versions. - -Credits ---------- - - * http://tus.io/protocols/resumable-upload.html - * https://github.com/matthoskins1980/Flask-Tus - - -MIT License -------------- - -Copyright (c) 2020, Alican Toprak - -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. diff --git a/conftest.py b/conftest.py index 868a08a..082f20f 100644 --- a/conftest.py +++ b/conftest.py @@ -2,9 +2,9 @@ def pytest_logger_config(logger_config): - logger_config.add_loggers(['testing',], stdout_level='debug') - logger_config.set_log_option_default('testing') + logger_config.add_loggers(["testing"], stdout_level="debug") + logger_config.set_log_option_default("testing") def pytest_logger_logdirlink(config): - return os.path.join(os.path.dirname(__file__), 'tests', 'logs') + return os.path.join(os.path.dirname(__file__), "tests", "logs") diff --git a/django_tus/__init__.py b/django_tus/__init__.py index 57466d8..fd73ccf 100644 --- a/django_tus/__init__.py +++ b/django_tus/__init__.py @@ -1,7 +1,7 @@ -__version__ = '0.1.0' +__version__ = "0.5.0" -default_app_config = 'django_tus.apps.DjangoTusConfig' +default_app_config = "django_tus.apps.DjangoTusConfig" -tus_api_version = '1.0.0' -tus_api_version_supported = ['1.0.0', ] -tus_api_extensions = ['creation', 'termination', 'file-check'] +tus_api_version = "1.0.0" +tus_api_version_supported = ["1.0.0"] +tus_api_extensions = ["creation", "termination", "file-check"] diff --git a/django_tus/apps.py b/django_tus/apps.py index 2e3fc08..aa0b9ae 100644 --- a/django_tus/apps.py +++ b/django_tus/apps.py @@ -3,8 +3,7 @@ from django.apps import AppConfig from django_tus.conf import settings -from django_tus.errors import BAD_CONFIG_ERROR_TUS_DESTINATION_DIR -from django_tus.errors import BAD_CONFIG_ERROR_TUS_UPLOAD_DIR +from django_tus.errors import BAD_CONFIG_ERROR_TUS_DESTINATION_DIR, BAD_CONFIG_ERROR_TUS_UPLOAD_DIR def django_tus_config_check(app_configs, **kwargs): @@ -15,23 +14,22 @@ def django_tus_config_check(app_configs, **kwargs): """ errors = [] - if not getattr(settings, 'TUS_UPLOAD_DIR', ''): + if not getattr(settings, "TUS_UPLOAD_DIR", ""): errors.append(BAD_CONFIG_ERROR_TUS_UPLOAD_DIR) - if not getattr(settings, 'TUS_DESTINATION_DIR', ''): + if not getattr(settings, "TUS_DESTINATION_DIR", ""): errors.append(BAD_CONFIG_ERROR_TUS_DESTINATION_DIR) return errors class DjangoTusConfig(AppConfig): - - name = 'django_tus' - verbose_name = 'Django TUS' + name = "django_tus" + verbose_name = "Django TUS" def ready(self): - from django.core.checks import register, Tags + from django.core.checks import Tags, register + register(django_tus_config_check, Tags.compatibility, deploy=False) Path(settings.TUS_DESTINATION_DIR).mkdir(parents=True, exist_ok=True) Path(settings.TUS_UPLOAD_DIR).mkdir(parents=True, exist_ok=True) - diff --git a/django_tus/conf.py b/django_tus/conf.py index b21eb82..b055ea2 100644 --- a/django_tus/conf.py +++ b/django_tus/conf.py @@ -1,48 +1,47 @@ import os -from appconf import AppConf + from django.conf import settings +from appconf import AppConf + class DjangoTusAppConf(AppConf): """ The settings of `django-tus` powedered by the excellent `django-appconf` (not to confound with the Django app config). """ + class Meta: - prefix = 'tus' + prefix = "tus" - UPLOAD_URL = '/media' + UPLOAD_URL = "/media" MAX_FILE_SIZE = 4294967296 # in bytes, default is 4 GB TIMEOUT = 3600 # in seconds - UPLOAD_DIR = '' - FILE_NAME_FORMAT = 'increment' - EXISTING_FILE = 'error' - DESTINATION_DIR = '' + UPLOAD_DIR = "" + FILE_NAME_FORMAT = "increment" + EXISTING_FILE = "error" + DESTINATION_DIR = "" def configure_upload_dir(self, value): - # The setting has been configured, return it. if value: return value # Build a default setting based on BASE_DIR, if available. - if hasattr(settings, 'BASE_DIR'): - return os.path.join(settings.BASE_DIR, 'tmp', 'uploads') + if hasattr(settings, "BASE_DIR"): + return os.path.join(settings.BASE_DIR, "tmp", "uploads") # Setting is not configured. - return '' - + return "" def configure_destination_dir(self, value): - # The setting has been configured, return it. if value: return value # Build a default setting based on MEDIA_ROOT, if available. - if hasattr(settings, 'MEDIA_ROOT'): - return os.path.join(settings.MEDIA_ROOT, 'uploads') + if hasattr(settings, "MEDIA_ROOT"): + return os.path.join(settings.MEDIA_ROOT, "uploads") # Setting is not configured. - return '' - + return "" diff --git a/django_tus/errors.py b/django_tus/errors.py index 75a5b57..a6858a8 100644 --- a/django_tus/errors.py +++ b/django_tus/errors.py @@ -1,17 +1,16 @@ from django.core.checks import Error - BAD_CONFIG_ERROR_TUS_UPLOAD_DIR = Error( - 'Error while checking the configuration for "django-tus', - hint='Is TUS_UPLOAD_DIR set correctly?', - obj='django.conf.settings.TUS_UPLOAD_DIR', - id='django-tus.E001', + 'Error while checking the configuration for "django-tus"', + hint="Is TUS_UPLOAD_DIR set correctly?", + obj="django.conf.settings.TUS_UPLOAD_DIR", + id="django-tus.E001", ) BAD_CONFIG_ERROR_TUS_DESTINATION_DIR = Error( - 'Error while checking the configuration for "django-tus', - hint='Is TUS_DESTINATION_DIR set correctly?', - obj='django.conf.settings.TUS_DESTINATION_DIR', - id='django-tus.E002', + 'Error while checking the configuration for "django-tus"', + hint="Is TUS_DESTINATION_DIR set correctly?", + obj="django.conf.settings.TUS_DESTINATION_DIR", + id="django-tus.E002", ) diff --git a/django_tus/response.py b/django_tus/response.py index 2ccefa0..1348d61 100644 --- a/django_tus/response.py +++ b/django_tus/response.py @@ -5,17 +5,16 @@ class TusResponse(HttpResponse): - _base_tus_headers = { - 'Tus-Resumable': tus_api_version, - 'Tus-Version': ",".join(tus_api_version_supported), - 'Tus-Extension': ",".join(tus_api_extensions), - 'Tus-Max-Size': settings.TUS_MAX_FILE_SIZE, - 'Access-Control-Allow-Origin': "*", - 'Access-Control-Allow-Methods': "PATCH,HEAD,GET,POST,OPTIONS", - 'Access-Control-Expose-Headers': "Tus-Resumable,upload-length,upload-metadata,Location,Upload-Offset", - 'Access-Control-Allow-Headers': "Tus-Resumable,upload-length,upload-metadata,Location,Upload-Offset,content-type", - 'Cache-Control': 'no-store' + "Tus-Resumable": tus_api_version, + "Tus-Version": ",".join(tus_api_version_supported), + "Tus-Extension": ",".join(tus_api_extensions), + "Tus-Max-Size": settings.TUS_MAX_FILE_SIZE, + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "PATCH,HEAD,GET,POST,OPTIONS", + "Access-Control-Expose-Headers": "Tus-Resumable,upload-length,upload-metadata,Location,Upload-Offset", + "Access-Control-Allow-Headers": "Tus-Resumable,upload-length,upload-metadata,Location,Upload-Offset,content-type", + "Cache-Control": "no-store", } def add_headers(self, headers: dict): @@ -29,5 +28,6 @@ def __init__(self, extra_headers=None, *args, **kwargs): if extra_headers: self.add_headers(extra_headers) + class Tus404(TusResponse, Http404): pass diff --git a/django_tus/signals.py b/django_tus/signals.py index 06a1bc2..6369a86 100644 --- a/django_tus/signals.py +++ b/django_tus/signals.py @@ -1,13 +1,15 @@ import django.dispatch - tus_upload_finished_signal = django.dispatch.Signal() """ This signal provides the following keyword arguments: + sender metadata + resource_id filename upload_file_path file_size upload_url destination_folder + request """ diff --git a/django_tus/tus_server.py b/django_tus/tus_server.py index d42ecef..53b35c9 100644 --- a/django_tus/tus_server.py +++ b/django_tus/tus_server.py @@ -1,5 +1,3 @@ - - class TusServer: def handshake(self): """ @@ -7,17 +5,20 @@ def handshake(self): :return: """ + def start(self, request): """ :param request: :return: """ + def upload(self): """ :return: """ + def finish(self): """ diff --git a/django_tus/tusfile.py b/django_tus/tusfile.py index dee3837..e026108 100644 --- a/django_tus/tusfile.py +++ b/django_tus/tusfile.py @@ -36,13 +36,13 @@ def create_random_suffix_name(self) -> str: @classmethod def random_string(cls, length: int = 11) -> str: letters_and_digits = string.ascii_letters + string.digits - return''.join((random.choice(letters_and_digits) for i in range(length))) + return "".join(random.choice(letters_and_digits) for i in range(length)) def create_incremented_name(self) -> str: index = 1 name, extension = self.get_name_and_extension() while True: - filename = '{}.{:04d}{}'.format(name, index, extension) + filename = f"{name}.{index:04d}{extension}" index += 1 if not os.path.lexists(os.path.join(settings.TUS_DESTINATION_DIR, filename)): break @@ -50,16 +50,15 @@ def create_incremented_name(self) -> str: class TusFile: - def get_storage(self): return FileSystemStorage() def __init__(self, resource_id: str): self.resource_id = resource_id - self.filename = cache.get("tus-uploads/{}/filename".format(resource_id)) - self.file_size = int(cache.get("tus-uploads/{}/file_size".format(resource_id))) - self.metadata = cache.get("tus-uploads/{}/metadata".format(resource_id)) - self.offset = cache.get("tus-uploads/{}/offset".format(resource_id)) + self.filename = cache.get(f"tus-uploads/{resource_id}/filename") + self.file_size = int(cache.get(f"tus-uploads/{resource_id}/file_size")) + self.metadata = cache.get(f"tus-uploads/{resource_id}/metadata") + self.offset = cache.get(f"tus-uploads/{resource_id}/offset") @staticmethod def get_tusfile_or_404(resource_id): @@ -70,15 +69,15 @@ def get_tusfile_or_404(resource_id): @staticmethod def resource_exists(resource_id: str): - return cache.get("tus-uploads/{}/filename".format(resource_id), None) is not None + return cache.get(f"tus-uploads/{resource_id}/filename", None) is not None @staticmethod def create_initial_file(metadata, file_size: int): resource_id = str(uuid.uuid4()) - cache.add("tus-uploads/{}/filename".format(resource_id), "{}".format(metadata.get("filename")), settings.TUS_TIMEOUT) - cache.add("tus-uploads/{}/file_size".format(resource_id), file_size, settings.TUS_TIMEOUT) - cache.add("tus-uploads/{}/offset".format(resource_id), 0, settings.TUS_TIMEOUT) - cache.add("tus-uploads/{}/metadata".format(resource_id), metadata, settings.TUS_TIMEOUT) + cache.add(f"tus-uploads/{resource_id}/filename", "{}".format(metadata.get("filename")), settings.TUS_TIMEOUT) + cache.add(f"tus-uploads/{resource_id}/file_size", file_size, settings.TUS_TIMEOUT) + cache.add(f"tus-uploads/{resource_id}/offset", 0, settings.TUS_TIMEOUT) + cache.add(f"tus-uploads/{resource_id}/metadata", metadata, settings.TUS_TIMEOUT) tus_file = TusFile(resource_id) tus_file.write_init_file() @@ -91,17 +90,16 @@ def get_path(self): return os.path.join(settings.TUS_UPLOAD_DIR, self.resource_id) def rename(self): - setting = settings.TUS_FILE_NAME_FORMAT - if setting == 'keep': + if setting == "keep": if self.check_existing_file(self.filename): return TusResponse(status=409, reason="File with same name already exists") - elif setting == 'random': + elif setting == "random": self.filename = FilenameGenerator(self.filename).create_random_name() - elif setting == 'random-suffix': + elif setting == "random-suffix": self.filename = FilenameGenerator(self.filename).create_random_suffix_name() - elif setting == 'increment': + elif setting == "increment": self.filename = FilenameGenerator(self.filename).create_incremented_name() else: return ValueError() @@ -109,12 +107,14 @@ def rename(self): shutil.move(self.get_path(), os.path.join(settings.TUS_DESTINATION_DIR, self.filename)) def clean(self): - cache.delete_many([ - "tus-uploads/{}/file_size".format(self.resource_id), - "tus-uploads/{}/filename".format(self.resource_id), - "tus-uploads/{}/offset".format(self.resource_id), - "tus-uploads/{}/metadata".format(self.resource_id), - ]) + cache.delete_many( + [ + f"tus-uploads/{self.resource_id}/file_size", + f"tus-uploads/{self.resource_id}/filename", + f"tus-uploads/{self.resource_id}/offset", + f"tus-uploads/{self.resource_id}/metadata", + ], + ) @staticmethod def check_existing_file(filename: str): @@ -122,41 +122,47 @@ def check_existing_file(filename: str): def write_init_file(self): try: - with open(self.get_path(), 'wb') as f: - f.seek(self.file_size - 1) - f.write(b'\0') - except IOError as e: - error_message = "Unable to create file: {}".format(e) + with open(self.get_path(), "wb") as f: + if self.file_size != 0: + f.seek(self.file_size - 1) + f.write(b"\0") + except OSError as e: + error_message = f"Unable to create file: {e}" logger.error(error_message, exc_info=True) return TusResponse(status=500, reason=error_message) def write_chunk(self, chunk): try: - with open(self.get_path(), 'r+b') as f: + with open(self.get_path(), "r+b") as f: f.seek(chunk.offset) f.write(chunk.content) - self.offset = cache.incr("tus-uploads/{}/offset".format(self.resource_id), chunk.chunk_size) - - except IOError: - logger.error("patch", extra={'request': chunk.META, 'tus': { - "resource_id": self.resource_id, - "filename": self.filename, - "file_size": self.file_size, - "metadata": self.metadata, - "offset": self.offset, - "upload_file_path": self.get_path(), - }}) + self.offset = cache.incr(f"tus-uploads/{self.resource_id}/offset", chunk.chunk_size) + + except OSError: + logger.error( + "patch", + extra={ + "request": chunk.META, + "tus": { + "resource_id": self.resource_id, + "filename": self.filename, + "file_size": self.file_size, + "metadata": self.metadata, + "offset": self.offset, + "upload_file_path": self.get_path(), + }, + }, + ) return TusResponse(status=500) def is_complete(self): return self.offset == self.file_size def __str__(self): - return "{} ({})".format(self.filename, self.resource_id) + return f"{self.filename} ({self.resource_id})" class TusInitFile: - def __init__(self, offset, chunk_size, content): self.offset = offset self.chunk_size = chunk_size diff --git a/django_tus/views.py b/django_tus/views.py index 57c3d95..ea41bbc 100644 --- a/django_tus/views.py +++ b/django_tus/views.py @@ -5,11 +5,12 @@ from django.views.decorators.csrf import csrf_exempt from django.views.generic import View +from pathvalidate import is_valid_filename + from django_tus.conf import settings from django_tus.response import TusResponse from django_tus.signals import tus_upload_finished_signal -from django_tus.tusfile import TusFile, TusChunk, FilenameGenerator -from pathvalidate import is_valid_filename +from django_tus.tusfile import FilenameGenerator, TusChunk, TusFile logger = logging.getLogger(__name__) @@ -21,15 +22,14 @@ class TusUpload(View): @method_decorator(csrf_exempt) def dispatch(self, *args, **kwargs): - if not self.request.META.get("HTTP_TUS_RESUMABLE"): return TusResponse(status=405, content="Method Not Allowed") - override_method = self.request.META.get('HTTP_X_HTTP_METHOD_OVERRIDE') + override_method = self.request.META.get("HTTP_X_HTTP_METHOD_OVERRIDE") if override_method: self.request.method = override_method - return super(TusUpload, self).dispatch(*args, **kwargs) + return super().dispatch(*args, **kwargs) def finished(self): if self.on_finish is not None: @@ -39,22 +39,21 @@ def get_metadata(self, request): metadata = {} if request.META.get("HTTP_UPLOAD_METADATA"): for kv in request.META.get("HTTP_UPLOAD_METADATA").split(","): - splited_metadata = kv.split(" ") - if len(splited_metadata) == 2: - key, value = splited_metadata + split_metadata = kv.split(" ") + if len(split_metadata) == 2: + key, value = split_metadata value = base64.b64decode(value) if isinstance(value, bytes): value = value.decode() metadata[key] = value else: - metadata[splited_metadata[0]] = "" + metadata[split_metadata[0]] = "" return metadata def options(self, request, *args, **kwargs): return TusResponse(status=204) def post(self, request, *args, **kwargs): - metadata = self.get_metadata(request) metadata["filename"] = self.validate_filename(metadata) @@ -63,7 +62,11 @@ def post(self, request, *args, **kwargs): if message_id: metadata["message_id"] = base64.b64decode(message_id) - if settings.TUS_EXISTING_FILE == 'error' and settings.TUS_FILE_NAME_FORMAT == 'keep' and TusFile.check_existing_file(metadata.get("filename")): + if ( + settings.TUS_EXISTING_FILE == "error" + and settings.TUS_FILE_NAME_FORMAT == "keep" + and TusFile.check_existing_file(metadata.get("filename")) + ): return TusResponse(status=409, reason="File with same name already exists") file_size = int(request.META.get("HTTP_UPLOAD_LENGTH", "0")) # TODO: check min max upload size @@ -72,19 +75,21 @@ def post(self, request, *args, **kwargs): return TusResponse( status=201, - extra_headers={'Location': '{}{}'.format(request.build_absolute_uri(), tus_file.resource_id)}) + extra_headers={"Location": f"{request.build_absolute_uri()}{tus_file.resource_id}"}, + ) def head(self, request, resource_id): - tus_file = TusFile.get_tusfile_or_404(str(resource_id)) - return TusResponse(status=200, - extra_headers={ - 'Upload-Offset': tus_file.offset, - 'Upload-Length': tus_file.file_size}) + return TusResponse( + status=200, + extra_headers={ + "Upload-Offset": tus_file.offset, + "Upload-Length": tus_file.file_size, + }, + ) def patch(self, request, resource_id, *args, **kwargs): - tus_file = TusFile.get_tusfile_or_404(str(resource_id)) chunk = TusChunk(request) @@ -104,25 +109,26 @@ def patch(self, request, resource_id, *args, **kwargs): tus_file.rename() tus_file.clean() - self.send_signal(tus_file) + self.send_signal(tus_file, request) self.finished() - return TusResponse(status=204, extra_headers={'Upload-Offset': tus_file.offset}) + return TusResponse(status=204, extra_headers={"Upload-Offset": tus_file.offset}) - def send_signal(self, tus_file): + def send_signal(self, tus_file, request): tus_upload_finished_signal.send( sender=self.__class__, metadata=tus_file.metadata, + resource_id=tus_file.resource_id, filename=tus_file.filename, upload_file_path=tus_file.get_path(), file_size=tus_file.file_size, upload_url=settings.TUS_UPLOAD_URL, - destination_folder=settings.TUS_DESTINATION_DIR) + destination_folder=settings.TUS_DESTINATION_DIR, + request=request, + ) def validate_filename(self, metadata): filename = metadata.get("filename", "") if not is_valid_filename(filename): filename = FilenameGenerator.random_string(16) return filename - - diff --git a/docs/authors.rst b/docs/authors.rst index e122f91..bcfd9cb 100644 --- a/docs/authors.rst +++ b/docs/authors.rst @@ -1 +1 @@ -.. include:: ../AUTHORS.rst +.. include:: ../AUTHORS.md diff --git a/docs/conf.py b/docs/conf.py index 40760da..6813e85 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # complexity documentation build configuration file, created by # sphinx-quickstart on Tue Jul 9 22:26:36 2013. # @@ -11,43 +9,44 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import os +import sys + +import django_tus # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) cwd = os.getcwd() parent = os.path.dirname(cwd) sys.path.append(parent) -import django_tus - # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'django-tus' -copyright = u'2016, Alican Toprak' +project = "django-tus" +copyright = "2016, Alican Toprak" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -60,161 +59,164 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = "default" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'django-tusdoc' +htmlhelp_basename = "django-tusdoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'django-tus.tex', u'django-tus Documentation', - u'Alican Toprak', 'manual'), + ( + "index", + "django-tus.tex", + "django-tus Documentation", + "Alican Toprak", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- @@ -222,12 +224,17 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'django-tus', u'django-tus Documentation', - [u'Alican Toprak'], 1) + ( + "index", + "django-tus", + "django-tus Documentation", + ["Alican Toprak"], + 1, + ), ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -236,19 +243,25 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'django-tus', u'django-tus Documentation', - u'Alican Toprak', 'django-tus', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "django-tus", + "django-tus Documentation", + "Alican Toprak", + "django-tus", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False diff --git a/docs/contributing.rst b/docs/contributing.rst index e582053..58977a8 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -1 +1 @@ -.. include:: ../CONTRIBUTING.rst +.. include:: ../CONTRIBUTING.md diff --git a/docs/history.rst b/docs/history.rst index 2506499..fcd2eb2 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1 +1 @@ -.. include:: ../HISTORY.rst +.. include:: ../HISTORY.md diff --git a/docs/readme.rst b/docs/readme.rst index 72a3355..bdff72a 100644 --- a/docs/readme.rst +++ b/docs/readme.rst @@ -1 +1 @@ -.. include:: ../README.rst +.. include:: ../README.md diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c7fbd0a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[tool.isort] +known_first_party = ["django-tus"] +known_django = ["django"] +known_djangorestframework = ["rest_framework"] +sections = ["FUTURE", "STDLIB", "DJANGO", "DJANGORESTFRAMEWORK", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] +profile = "black" +multi_line_output = 3 +line_length = 120 +skip_gitignore = true + +[tool.black] +line_length = 120 +target-version = ['py311'] +include = '\.pyi?$' diff --git a/requirements.txt b/requirements.txt index 4ab1bc5..5407cfa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -django>=3.0 -django-appconf -pathvalidate==2.3.0 +Django>=4.2 +django-appconf==1.0.* +pathvalidate==3.0.* diff --git a/requirements_dev.txt b/requirements_dev.txt index 62f1ead..51b3a7f 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,4 +1,4 @@ -r requirements.txt -bumpversion==0.5.3 -wheel==0.38.1 -tuspy +bumpversion==0.6.* +tuspy==1.0.* +wheel==0.40.* diff --git a/requirements_test.txt b/requirements_test.txt index e9b8653..403cf39 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,11 +1,11 @@ -django-appconf -pathvalidate==2.3.0 -coverage==5.3 -flake8>=3.8.3 -mock>=3.0.5 -tox>=3.20.0 -pytest-pythonpath -pytest-cov -pytest-django -pytest-logger -tuspy +coverage==7.2.* +django-appconf==1.0.* +flake8==6.0.* +mock==5.0.* +pathvalidate==3.0.* +pytest-cov==4.1.* +pytest-django==4.5.* +pytest-logger==0.5.* +pytest-pythonpath==0.7.* +tox==4.6.* +tuspy==1.0.* diff --git a/setup.cfg b/setup.cfg index bb59891..af5d28b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,13 @@ +[pep8] +ignore = E402,E722 +max-line-length = 120 +exclude = *migrations*, *settings* + +[pycodestyle] +ignore = E402,E722 +max-line-length = 120 +exclude = *migrations*, *settings* + [bumpversion] current_version = 0.5.0 commit = True diff --git a/setup.py b/setup.py index 9695d06..fe93561 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- import os import sys @@ -8,68 +7,69 @@ except ImportError: from distutils.core import setup -version = '0.5.0' +version = "0.5.0" -if sys.argv[-1] == 'publish': +if sys.argv[-1] == "publish": try: import wheel + print("Wheel version: ", wheel.__version__) except ImportError: print('Wheel library missing. Please run "pip install wheel"') sys.exit() - os.system('python setup.py sdist upload') - os.system('python setup.py bdist_wheel upload') + os.system("python setup.py sdist upload") + os.system("python setup.py bdist_wheel upload") sys.exit() -if sys.argv[-1] == 'tag': +if sys.argv[-1] == "tag": print("Tagging the version on github:") - os.system("git tag -a %s -m 'version %s'" % (version, version)) + os.system(f"git tag -a {version} -m 'version {version}'") os.system("git push --tags") sys.exit() -history = open('HISTORY.rst').read().replace('.. :changelog:', '') +history = open("HISTORY.md").read().replace(".. :changelog:", "") + def read(f): - return open(f, 'r', encoding='utf-8').read() + return open(f, encoding="utf-8").read() + setup( - name='django-tus', + name="django-tus", version=version, - description="Django app implementing server side of tus v1.0.0 powering resumable file uploads for django projects", - long_description=read('README.rst'), - author='Alican Toprak', - author_email='alican@querhin.com', - url='https://github.com/alican/django-tus', + description="Django app implementing server side of tus v1.0.0 powering resumable file uploads for Django projects", + long_description=read("README.md"), + author="Alican Toprak", + author_email="alican@querhin.com", + url="https://github.com/alican/django-tus", packages=[ - 'django_tus', + "django_tus", ], include_package_data=True, install_requires=[ - 'django>=2.2', - 'django-appconf', - 'pathvalidate==2.3.0' + "django>=4.2", + "django-appconf", + "pathvalidate==3.0", ], license="MIT", - long_description_content_type='text/x-rst', + long_description_content_type="text/x-rst", zip_safe=False, - keywords='django-tus', - python_requires=">=3.5", + keywords="django-tus", + python_requires=">=3.9", classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Framework :: Django', - 'Framework :: Django :: 2.2', - 'Framework :: Django :: 3.0', - 'Framework :: Django :: 3.1', - 'Framework :: Django :: 3.2', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Natural Language :: English', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Topic :: Internet :: WWW/HTTP', + "Development Status :: 5 - Production/Stable", + "Framework :: Django", + "Framework :: Django :: 3.2", + "Framework :: Django :: 4.0", + "Framework :: Django :: 4.1", + "Framework :: Django :: 4.2", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Internet :: WWW/HTTP", ], ) diff --git a/tests/settings.py b/tests/settings.py index 4ba6ac8..b159205 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -5,68 +5,66 @@ DEBUG = False TEMPLATE_DEBUG = DEBUG -TIME_ZONE = 'UTC' -LANGUAGE_CODE = 'de' +TIME_ZONE = "UTC" +LANGUAGE_CODE = "de" SITE_ID = 1 USE_L10N = True USE_TZ = True -SECRET_KEY = 'local' +SECRET_KEY = "local" -ROOT_URLCONF = 'tests.urls' +ROOT_URLCONF = "tests.urls" DATABASES = { - 'default': { + "default": { "ENGINE": "django.db.backends.sqlite3", - } + }, } MIDDLEWARE_CLASSES = [ - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.common.CommonMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", ) -STATIC_URL = 'static/' +STATIC_URL = "static/" -MEDIA_ROOT = os.path.dirname(os.path.abspath(__file__)) + '/upload' +MEDIA_ROOT = os.path.dirname(os.path.abspath(__file__)) + "/upload" -MEDIA_URL = 'media/' +MEDIA_URL = "media/" -INSTALLED_APPS = ( +INSTALLED_APPS = [ "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sites", "django_tus", -) +] -PASSWORD_HASHERS = ( - 'django.contrib.auth.hashers.MD5PasswordHasher', -) +PASSWORD_HASHERS = ("django.contrib.auth.hashers.MD5PasswordHasher",) TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.template.context_processors.i18n', - 'django.template.context_processors.media', - 'django.template.context_processors.static', - 'django.template.context_processors.tz', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.template.context_processors.tz", + "django.contrib.messages.context_processors.messages", ], }, }, diff --git a/tests/test_config.py b/tests/test_config.py index 0a1fab9..b5b5333 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -3,56 +3,54 @@ import pytest from django_tus.apps import django_tus_config_check -from django_tus.errors import BAD_CONFIG_ERROR_TUS_DESTINATION_DIR -from django_tus.errors import BAD_CONFIG_ERROR_TUS_UPLOAD_DIR +from django_tus.errors import BAD_CONFIG_ERROR_TUS_DESTINATION_DIR, BAD_CONFIG_ERROR_TUS_UPLOAD_DIR -class TestDefaultSettings(object): - +class TestDefaultSettings: def test_settings(self): - errors = django_tus_config_check(['django_tus']) + errors = django_tus_config_check(["django_tus"]) assert errors == [] from django.conf import settings - assert settings.TUS_UPLOAD_URL == '/media' + assert settings.TUS_UPLOAD_URL == "/media" assert isinstance(settings.TUS_MAX_FILE_SIZE, int) assert isinstance(settings.TUS_TIMEOUT, int) - #assert settings.TUS_UPLOAD_DIR == os.path.dirname(os.path.abspath(__file__)) + '/tmp/uploads' - #assert settings.TUS_DESTINATION_DIR == os.path.dirname(os.path.abspath(__file__)) + '/upload' - + # assert settings.TUS_UPLOAD_DIR == os.path.dirname(os.path.abspath(__file__)) + '/tmp/uploads' + # assert settings.TUS_DESTINATION_DIR == os.path.dirname(os.path.abspath(__file__)) + '/upload' -class TestManuallyConfiguredSettings(object): +class TestManuallyConfiguredSettings: def test_configured_upload_dir(self, settings): - settings.TUS_UPLOAD_DIR = '/tmp/django-tus/upload/dir' - errors = django_tus_config_check(['django_tus']) + settings.TUS_UPLOAD_DIR = "/tmp/django-tus/upload/dir" + errors = django_tus_config_check(["django_tus"]) assert errors == [] from django.conf import settings - assert settings.TUS_UPLOAD_DIR == '/tmp/django-tus/upload/dir' + + assert settings.TUS_UPLOAD_DIR == "/tmp/django-tus/upload/dir" def test_configured_destination_dir(self, settings): - settings.TUS_DESTINATION_DIR = '/tmp/django-tus/destination/dir' - errors = django_tus_config_check(['django_tus']) + settings.TUS_DESTINATION_DIR = "/tmp/django-tus/destination/dir" + errors = django_tus_config_check(["django_tus"]) assert errors == [] from django.conf import settings - assert settings.TUS_DESTINATION_DIR == '/tmp/django-tus/destination/dir' + assert settings.TUS_DESTINATION_DIR == "/tmp/django-tus/destination/dir" -class TestConfigErrors(object): +class TestConfigErrors: @pytest.fixture() def settings_without_base_dir(self, settings): # According to https://github.com/pytest-dev/pytest-django/issues/33#issuecomment-18058652 # this is the way how to fiddle with settings. del settings.BASE_DIR - settings.TUS_UPLOAD_DIR = '' + settings.TUS_UPLOAD_DIR = "" yield settings def test_unconfigured_upload_dir(self, settings_without_base_dir): - errors = django_tus_config_check(['django_tus']) + errors = django_tus_config_check(["django_tus"]) assert errors == [BAD_CONFIG_ERROR_TUS_UPLOAD_DIR] @pytest.fixture() @@ -60,9 +58,9 @@ def settings_without_media_root(self, settings): # According to https://github.com/pytest-dev/pytest-django/issues/33#issuecomment-18058652 # this is the way how to fiddle with settings. del settings.MEDIA_ROOT - settings.TUS_DESTINATION_DIR = '' + settings.TUS_DESTINATION_DIR = "" yield settings def test_unconfigured_destination_dir(self, settings_without_media_root): - errors = django_tus_config_check(['django_tus']) + errors = django_tus_config_check(["django_tus"]) assert errors == [BAD_CONFIG_ERROR_TUS_DESTINATION_DIR] diff --git a/tests/test_views.py b/tests/test_views.py index 62f3a34..c42eec4 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,17 +1,17 @@ from django.urls import reverse -from tusclient.client import TusClient +from tusclient.client import TusClient -class TestUploadView(object): +class TestUploadView: def test_get_is_not_allowed(self, client): - response = client.get(reverse('tus_upload')) + response = client.get(reverse("tus_upload")) assert response.status_code == 405 def test_upload_file(self, live_server): tus_client = TusClient( - live_server.url + reverse('tus_upload') + live_server.url + reverse("tus_upload"), ) - uploader = tus_client.uploader('tests/files/hello_world.txt', chunk_size=200) + uploader = tus_client.uploader("tests/files/hello_world.txt", chunk_size=200) uploader.upload() assert uploader.request.status_code == 204 diff --git a/tests/urls.py b/tests/urls.py index d3a1091..fe561a7 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1,12 +1,12 @@ from django.conf import settings -from django.conf.urls import url from django.conf.urls.static import static +from django.urls import path from django_tus.views import TusUpload urlpatterns = [ - url(r'^upload/$', TusUpload.as_view(), name='tus_upload'), - url(r'^upload/(?P[0-9a-z-]+)$', TusUpload.as_view(), name='tus_upload_chunks'), + path(r"^upload/$", TusUpload.as_view(), name="tus_upload"), + path(r"^upload/(?P[0-9a-z-]+)$/", TusUpload.as_view(), name="tus_upload_chunks"), ] urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/tox.ini b/tox.ini index 73a677b..18db40a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,9 @@ [tox] envlist = - {py35,py36,py37,py38,py39}-django22 - {py36,py37,py38,py39}-django30 - {py36,py37,py38,py39}-django31 - {py36,py37,py38,py39}-django32 + {py39,py310,py311}-django32 + {py39,py310,py311}-django40 + {py39,py310,py311}-django41 + {py39,py310,py311}-django42 [testenv] @@ -12,10 +12,10 @@ setenv = DJANGO_SETTINGS_MODULE=tests.settings deps = - django22: Django>=2.2.8,<2.3 - django30: Django>=3.0,<3.1 - django31: Django>=3.1,<3.2 - django32: Django>=3.2,<3.3 + django32: Django>=3.2,<4.0 + django40: Django>=4.0,<4.1 + django41: Django>=4.1,<4.2 + django42: Django>=4.2,<5.0 -r{toxinidir}/requirements_test.txt # Prevent "test command found but not installed in testenv" pytest