├── example ├── winners │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_admin.py │ │ ├── test_models.py │ │ ├── test_utils.py │ │ └── test_fields.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── admin.py │ ├── wsgi.py │ ├── urls.py │ └── models.py ├── example-data │ └── Winner-2019-06-27.csv ├── project │ ├── __init__.py │ ├── celery.py │ └── settings.py ├── pyproject.toml ├── manage.py ├── README.rst └── poetry.lock ├── import_export_celery ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0005_exportjob_site_of_origin.py │ ├── 0002_auto_20190923_1132.py │ ├── 0004_exportjob_email_on_completion.py │ ├── 0011_alter_exportjob_email_on_completion.py │ ├── 0008_alter_exportjob_id_alter_importjob_id.py │ ├── 0012_alter_exportjob_id_alter_importjob_id.py │ ├── 0007_auto_20210210_1831.py │ ├── 0010_auto_20231013_0904.py │ ├── 0009_alter_exportjob_options_alter_importjob_options_and_more.py │ ├── 0006_auto_20191125_1236.py │ ├── 0003_exportjob.py │ └── 0001_initial.py ├── locale │ ├── pt │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── eu │ │ └── LC_MESSAGES │ │ └── django.po ├── apps.py ├── models │ ├── __init__.py │ ├── importjob.py │ └── exportjob.py ├── model_config.py ├── templates │ └── email │ │ └── export_job_completion.html ├── fields.py ├── admin_actions.py ├── utils.py ├── admin.py └── tasks.py ├── dev-entrypoint.sh ├── setup-dev-env.sh ├── screenshots ├── admin.png ├── summary.png ├── import_jobs.png ├── new-winner.png ├── view-summary.png ├── new_import_job.png └── perform-import.png ├── CODE_OF_CONDUCT.md ├── requirements_test.txt ├── develop.sh ├── launch_docker ├── Dockerfile ├── .coveragerc ├── MANIFEST.in ├── Makefile ├── .gitignore ├── .editorconfig ├── setup.cfg ├── .pre-commit-config.yaml ├── docker-compose.yaml ├── SHOWCASE_AND_THANKS.md ├── .travis.yml ├── tox.ini ├── .github └── workflows │ ├── publish-to-live-pypi.yml │ ├── publish-to-test-pypi.yml │ └── linters.yml ├── setup.py ├── LICENSE └── README.rst /example/winners/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/winners/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /import_export_celery/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/winners/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /import_export_celery/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dev-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd example 3 | poetry shell 4 | -------------------------------------------------------------------------------- /example/example-data/Winner-2019-06-27.csv: -------------------------------------------------------------------------------- 1 | id,name 2 | 2,bar 3 | -------------------------------------------------------------------------------- /example/project/__init__.py: -------------------------------------------------------------------------------- 1 | from .celery import app as celery_app 2 | 3 | __all__ = ("celery_app",) 4 | -------------------------------------------------------------------------------- /setup-dev-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd example 3 | poetry install 4 | poetry run python3 manage.py migrate 5 | -------------------------------------------------------------------------------- /screenshots/admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-mat/django-import-export-celery/HEAD/screenshots/admin.png -------------------------------------------------------------------------------- /screenshots/summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-mat/django-import-export-celery/HEAD/screenshots/summary.png -------------------------------------------------------------------------------- /screenshots/import_jobs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-mat/django-import-export-celery/HEAD/screenshots/import_jobs.png -------------------------------------------------------------------------------- /screenshots/new-winner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-mat/django-import-export-celery/HEAD/screenshots/new-winner.png -------------------------------------------------------------------------------- /screenshots/view-summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-mat/django-import-export-celery/HEAD/screenshots/view-summary.png -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Please follow the spirit of [The PSF code of conduct](https://www.python.org/psf/conduct/) and try to be nice. 2 | -------------------------------------------------------------------------------- /screenshots/new_import_job.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-mat/django-import-export-celery/HEAD/screenshots/new_import_job.png -------------------------------------------------------------------------------- /screenshots/perform-import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-mat/django-import-export-celery/HEAD/screenshots/perform-import.png -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | Django 2 | django-import-export 3 | django-author 4 | html2text 5 | celery 6 | psycopg2-binary 7 | django-admin-smoke-tests 8 | -------------------------------------------------------------------------------- /import_export_celery/locale/pt/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-mat/django-import-export-celery/HEAD/import_export_celery/locale/pt/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /develop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker-compose down 3 | docker-compose up -d 4 | exec docker exec -u test -it django-import-export-celery_web_1 bash --init-file "/proj/dev-entrypoint.sh" 5 | -------------------------------------------------------------------------------- /launch_docker: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sudo docker run -it --volume=/home/timothy/pu/projects/auto-mat/django-import-export-celery:/proj:rw --entrypoint=/bin/bash --workdir=/proj -p 8001:8080 klub_web 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7 2 | RUN pip3 install poetry celery 3 | RUN apt-get update ; apt-get install -yq python3-psycopg2 gdal-bin 4 | ARG UID 5 | RUN useradd test --uid $UID 6 | RUN chsh test -s /bin/bash 7 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | */virtualenvs/*, 4 | */migrations/*, 5 | */site-packages/* 6 | include = import_export_celery/* 7 | plugins = 8 | django_coverage_plugin 9 | branch = True 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include import_export_celery/static *.js *.css *.map *.png *.ico *.eot *.svg *.ttf *.woff *.woff2 2 | recursive-include import_export_celery/templates *.html 3 | recursive-include import_export_celery/locale *.mo 4 | -------------------------------------------------------------------------------- /import_export_celery/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class ImportExportCeleryConfig(AppConfig): 6 | name = "import_export_celery" 7 | verbose_name = _("Import Export Celery") 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | docker-compose: Dockerfile 2 | mkdir -p pyenv 3 | mkdir -p db 4 | sudo docker-compose build --build-arg UID=$(shell id -u) 5 | sudo docker-compose up -d web postgres 6 | sudo docker exec -it django-import-export-celery_web_1 /proj/setup-dev-env.sh 7 | sudo docker-compose down 8 | 9 | -------------------------------------------------------------------------------- /import_export_celery/models/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 o.s. Auto*Mat 2 | 3 | """Import all models.""" 4 | from import_export_celery.models.exportjob import ExportJob 5 | from import_export_celery.models.importjob import ImportJob 6 | 7 | __all__ = ( 8 | ExportJob, 9 | ImportJob, 10 | ) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.pyc 3 | build/ 4 | dist/ 5 | *.sqlite3 6 | *__pycache__* 7 | example/django-import-export-celery-import-change-summaries/ 8 | example/django-import-export-celery-import-jobs/ 9 | example/django-import-export-celery-export-jobs/ 10 | pyenv/ 11 | django_import_export_celery.egg-info/ 12 | db/ 13 | .idea/ 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.py] 14 | max_line_length = 150 15 | 16 | [*.{scss,js,html}] 17 | max_line_length = 150 18 | 19 | [*.rst] 20 | max_line_length = 150 21 | 22 | [*.yml] 23 | indent_size = 2 24 | -------------------------------------------------------------------------------- /example/winners/admin.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 o.s. Auto*Mat 2 | from django.contrib import admin 3 | 4 | from import_export.admin import ImportExportMixin 5 | from import_export_celery.admin_actions import create_export_job_action 6 | 7 | from . import models 8 | 9 | 10 | @admin.register(models.Winner) 11 | class WinnerAdmin(ImportExportMixin, admin.ModelAdmin): 12 | list_display = ("name",) 13 | 14 | actions = (create_export_job_action,) 15 | -------------------------------------------------------------------------------- /example/winners/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for winners project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "winners.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 150 3 | exclude = 4 | pyenv/*, 5 | env/*, 6 | env-dj19/*, 7 | *migrations/*, 8 | */static/*, 9 | static/*, 10 | src/*, 11 | *.sh, 12 | *.cfg, 13 | *.txt, 14 | *.json, 15 | *.po, 16 | *.gpx, 17 | *.JPG, 18 | *.gpx.gz, 19 | *.html, 20 | *.kml, 21 | *.pdf, 22 | Dockerfile, 23 | .dockerignore, 24 | *.xml 25 | max-complexity = 10 26 | enable-extensions = import-order, blind-except 27 | ignore = C000, C416, C901, E731, W503, W504 28 | -------------------------------------------------------------------------------- /import_export_celery/migrations/0005_exportjob_site_of_origin.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-11-13 13:27 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("import_export_celery", "0004_exportjob_email_on_completion"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="exportjob", 15 | name="site_of_origin", 16 | field=models.TextField(default="", max_length=255), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /example/project/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from celery import Celery 5 | 6 | sys.path.append("../") 7 | 8 | 9 | # set the default Django settings module for the 'celery' program. 10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") 11 | 12 | from django.conf import settings # noqa 13 | 14 | app = Celery("apps.winners") 15 | 16 | # Using a string here means the worker will not have to 17 | # pickle the object when using Windows. 18 | app.config_from_object("django.conf:settings") 19 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) 20 | -------------------------------------------------------------------------------- /import_export_celery/model_config.py: -------------------------------------------------------------------------------- 1 | from django.apps import apps 2 | from import_export.resources import modelresource_factory 3 | from celery.utils.log import get_task_logger 4 | 5 | log = get_task_logger(__name__) 6 | 7 | 8 | class ModelConfig: 9 | def __init__(self, app_label=None, model_name=None, resource=None): 10 | self.model = apps.get_model(app_label=app_label, model_name=model_name) 11 | log.debug(resource) 12 | if resource: 13 | self.resource = resource() 14 | else: 15 | self.resource = modelresource_factory(self.model) 16 | -------------------------------------------------------------------------------- /import_export_celery/templates/email/export_job_completion.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |

Your export job on model {{app_label}}.{{model}} has completed. You can download the file at the following link:

12 | {{link}} 13 | 14 | 15 | -------------------------------------------------------------------------------- /import_export_celery/migrations/0002_auto_20190923_1132.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-23 09:32 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("import_export_celery", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="importjob", 15 | name="job_status", 16 | field=models.CharField( 17 | blank=True, max_length=160, verbose_name="Status of the job" 18 | ), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /example/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "example" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Automat z.s."] 6 | license = "LGPL-3.0-or-later" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.7" 10 | django-import-export = "*" 11 | django = "*" 12 | celery = "*" 13 | django-author = "*" 14 | redis = "*" 15 | psycopg2-binary = "^2.8.4" 16 | html2text = "^2020.1.16" 17 | 18 | [tool.poetry.dev-dependencies] 19 | django-admin-smoke-tests = "^0.3.0" 20 | pudb = "^2019.2" 21 | black = "^22.3.0" 22 | 23 | [build-system] 24 | requires = ["poetry>=0.12"] 25 | build-backend = "poetry.masonry.api" 26 | -------------------------------------------------------------------------------- /example/winners/tests/test_admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib.messages.storage.fallback import FallbackStorage 2 | 3 | from django_admin_smoke_tests import tests 4 | 5 | 6 | class AdminSmokeTest(tests.AdminSiteSmokeTest): 7 | exclude_apps = [] 8 | fixtures = [] 9 | 10 | def post_request(self, post_data={}, params=None): # noqa 11 | request = self.factory.post("/", data=post_data) 12 | request.user = self.superuser 13 | request._dont_enforce_csrf_checks = True 14 | request.session = "session" 15 | request._messages = FallbackStorage(request) 16 | return request 17 | -------------------------------------------------------------------------------- /import_export_celery/migrations/0004_exportjob_email_on_completion.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-11-13 13:12 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("import_export_celery", "0003_exportjob"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="exportjob", 15 | name="email_on_completion", 16 | field=models.BooleanField( 17 | default=True, 18 | verbose_name="Send me an email when this export job is complete", 19 | ), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /import_export_celery/migrations/0011_alter_exportjob_email_on_completion.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.25 on 2024-11-22 12:19 2 | 3 | from django.db import migrations, models 4 | import import_export_celery.utils 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('import_export_celery', '0010_auto_20231013_0904'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='exportjob', 16 | name='email_on_completion', 17 | field=models.BooleanField(default=import_export_celery.utils.get_export_job_email_on_completion, verbose_name='Send me an email when this export job is complete'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /example/winners/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.2 on 2019-06-28 11:47 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="Winner", 15 | fields=[ 16 | ( 17 | "id", 18 | models.AutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ("name", models.CharField(default="", max_length=80)), 26 | ], 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3.7 3 | 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v4.1.0 7 | hooks: 8 | - id: check-added-large-files 9 | - id: check-case-conflict 10 | - id: check-merge-conflict 11 | - id: end-of-file-fixer 12 | - id: trailing-whitespace 13 | 14 | - repo: https://github.com/asottile/pyupgrade 15 | rev: v2.31.1 16 | hooks: 17 | - id: pyupgrade 18 | args: [--py37-plus] 19 | 20 | - repo: https://github.com/psf/black 21 | rev: 22.3.0 22 | hooks: 23 | - id: black 24 | 25 | - repo: https://github.com/PyCQA/flake8 26 | rev: 4.0.1 27 | hooks: 28 | - id: flake8 29 | additional_dependencies: 30 | - flake8-bugbear 31 | - flake8-comprehensions 32 | - flake8-tidy-imports 33 | - flake8-typing-imports 34 | -------------------------------------------------------------------------------- /import_export_celery/migrations/0008_alter_exportjob_id_alter_importjob_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.6 on 2023-05-15 16:34 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('import_export_celery', '0007_auto_20210210_1831'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='exportjob', 15 | name='id', 16 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 17 | ), 18 | migrations.AlterField( 19 | model_name='importjob', 20 | name='id', 21 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /import_export_celery/migrations/0012_alter_exportjob_id_alter_importjob_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2 on 2025-10-17 11:46 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('import_export_celery', '0011_alter_exportjob_email_on_completion'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='exportjob', 15 | name='id', 16 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 17 | ), 18 | migrations.AlterField( 19 | model_name='importjob', 20 | name='id', 21 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /import_export_celery/migrations/0007_auto_20210210_1831.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-02-10 18:31 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("import_export_celery", "0006_auto_20191125_1236"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="exportjob", 15 | name="format", 16 | field=models.CharField( 17 | max_length=255, null=True, verbose_name="Format of file to be exported" 18 | ), 19 | ), 20 | migrations.AlterField( 21 | model_name="importjob", 22 | name="format", 23 | field=models.CharField( 24 | max_length=255, verbose_name="Format of file to be imported" 25 | ), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | web: 4 | build: . 5 | entrypoint: /bin/bash 6 | ports: 7 | - "8001:8000" 8 | links: 9 | - postgres 10 | - redis 11 | tty: true 12 | stdin_open: true 13 | working_dir: /proj/ 14 | user: test 15 | volumes: 16 | - ./:/proj/ 17 | - ./pyenv:/home/test 18 | celery: 19 | build: . 20 | entrypoint: poetry run celery -A project.celery worker -l info 21 | links: 22 | - postgres 23 | - redis 24 | tty: true 25 | stdin_open: true 26 | working_dir: /proj/example 27 | user: test 28 | volumes: 29 | - ./:/proj/ 30 | - ./pyenv:/home/test 31 | redis: 32 | image: redis 33 | postgres: 34 | image: mdillon/postgis:9.6-alpine 35 | volumes: 36 | - ./db:/var/lib/postgresql/data 37 | environment: 38 | POSTGRES_PASSWORD: foobar 39 | POSTGRES_USER: pguser 40 | PGDATA: /var/lib/postgresql/data 41 | -------------------------------------------------------------------------------- /SHOWCASE_AND_THANKS.md: -------------------------------------------------------------------------------- 1 | User showcase and thankyous 2 | ---------------------------- 3 | 4 | Do you use `django-import-export-celery`? Do you want to promote your project or just say thanks? Place a pull request and get included in the showcase and thanks. 5 | 6 | 7 | 8 | [Do Práce na Kole](https://www.dopracenakole.cz/) (Open source on [github](https://github.com/auto-mat/do-prace-na-kole)) 9 | 10 | Do Práce na Kole is a month long bike to work challenge in the Czech Republic. It is run by the non profit [Auto-mat z.s.](https://auto-mat.cz). django-import-export-celery is used to import discount codes from our eshop and export lists of users for data-anaylsis using jupyter. 11 | 12 | Auto-mat z.s. also uses django-import-export-celery for its internal donor database [klub přatel](https://github.com/auto-mat/klub) (also open source). 13 | -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | sys.path.append("../") 6 | 7 | if __name__ == "__main__": 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError: 12 | # The above import may fail for some other reason. Ensure that the 13 | # issue is really that Django is missing to avoid masking other 14 | # exceptions on Python 2. 15 | try: 16 | import django # noqa 17 | except ImportError: 18 | raise ImportError( 19 | "Couldn't import Django. Are you sure it's installed and " 20 | "available on your PYTHONPATH environment variable? Did you " 21 | "forget to activate a virtual environment?" 22 | ) 23 | raise 24 | execute_from_command_line(sys.argv) 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | env: 4 | global: 5 | - DATABASE_NAME="travis_ci" 6 | - DATABASE_USER="postgres" 7 | - DATABASE_PORT=5432 8 | - DATABASE_HOST="" 9 | - DATABASE_PASSWORD="" 10 | - DJANGO_SETTINGS_MODULE="project.settings" 11 | - SECRET_KEY="sadfjasasdfasdfsadfsadfsadfeq" 12 | 13 | matrix: 14 | - DJANGO_VERSION="Django==3.2" 15 | python: 16 | - "3.7" 17 | - "3.8" 18 | - "3.9" 19 | # - "3.10" 20 | install: 21 | - pip install poetry 22 | - cd example 23 | - poetry install 24 | - poetry run pip install -q $DJANGO_VERSION 25 | before_script: 26 | - poetry run black --check . 27 | - poetry run black --check ../import_export_celery 28 | script: 29 | - poetry run python3 manage.py test 30 | after_script: 31 | - coveralls 32 | addons: 33 | postgresql: "9.6" 34 | matrix: 35 | allow_failures: 36 | - env: DJANGO_VERSION='https://github.com/django/django/archive/master.tar.gz' 37 | -------------------------------------------------------------------------------- /example/winners/urls.py: -------------------------------------------------------------------------------- 1 | """winners URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.10/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf import settings 17 | from django.conf.urls.static import static 18 | from django.contrib import admin 19 | from django.urls import path 20 | 21 | urlpatterns = [ 22 | path("admin", admin.site.urls), 23 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 24 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{36,37,38,39,310}-django32 4 | py{38,39,310}-django40 5 | py{38,39,310,311}-django41 6 | py{38,39,310,311,312}-django42 7 | py{310,311,312}-django50 8 | py{310,311,312}-django51 9 | 10 | [testenv] 11 | deps = 12 | -rrequirements_test.txt 13 | coverage 14 | django-coverage-plugin 15 | django32: django>=3.2,<3.3 16 | django40: django>=4.0,<4.1 17 | django41: django>=4.1,<4.2 18 | django42: django>=4.2,<4.3 19 | django50: django>=5.0,<5.1 20 | django51: django>=5.1a1,<5.2 21 | 22 | setenv = 23 | DATABASE_TYPE=sqlite 24 | REDIS_URL=redis://127.0.0.1:6379/0 25 | 26 | allowlist_externals = coverage 27 | 28 | test-executable = 29 | python --version 30 | python -c "import django; print(django.get_version())" 31 | pip install -r requirements_test.txt 32 | {envbindir}/python -Wall {envbindir}/coverage run --append 33 | 34 | commands = 35 | python example/manage.py migrate 36 | {[testenv]test-executable} example/manage.py test winners 37 | coverage report 38 | coverage xml -o coverage.xml 39 | -------------------------------------------------------------------------------- /example/winners/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from import_export.resources import ModelResource 4 | from import_export.fields import Field 5 | 6 | 7 | class Winner(models.Model): 8 | name = models.CharField( 9 | max_length=80, 10 | null=False, 11 | blank=False, 12 | default="", 13 | ) 14 | 15 | @classmethod 16 | def export_resource_classes(cls): 17 | return { 18 | "winners": ("Winners resource", WinnersResource), 19 | "winners_all_caps": ( 20 | "Winners with all caps column resource", 21 | WinnersWithAllCapsResource, 22 | ), 23 | } 24 | 25 | 26 | class WinnersResource(ModelResource): 27 | class Meta: 28 | model = Winner 29 | 30 | def get_export_queryset(self): 31 | """To customise the queryset of the model resource with annotation override""" 32 | return self.Meta.model.objects.all() 33 | 34 | 35 | class WinnersWithAllCapsResource(WinnersResource): 36 | name_all_caps = Field() 37 | 38 | def dehydrate_name_all_caps(self, winner): 39 | return winner.name.upper() 40 | -------------------------------------------------------------------------------- /import_export_celery/fields.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | def lazy_initialize_storage_class(): 5 | from django.conf import settings 6 | try: 7 | from django.core.files.storage import storages 8 | storages_defined = True 9 | except ImportError: 10 | storages_defined = False 11 | 12 | if not hasattr(settings, 'IMPORT_EXPORT_CELERY_STORAGE') and storages_defined: 13 | # Use new style storages if defined 14 | storage_alias = getattr(settings, "IMPORT_EXPORT_CELERY_STORAGE_ALIAS", "default") 15 | storage_class = storages[storage_alias] 16 | else: 17 | # Use old style storages if defined 18 | from django.core.files.storage import get_storage_class 19 | storage_class = get_storage_class(getattr(settings, "IMPORT_EXPORT_CELERY_STORAGE", "django.core.files.storage.FileSystemStorage")) 20 | return storage_class() 21 | 22 | return storage_class 23 | 24 | 25 | class ImportExportFileField(models.FileField): 26 | def __init__(self, *args, **kwargs): 27 | kwargs["storage"] = lazy_initialize_storage_class 28 | super().__init__(*args, **kwargs) 29 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-live-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distributions 📦 to pypi 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | build-n-publish: 10 | name: Build and publish Python 🐍 distributions 📦 to pypi 11 | runs-on: ubuntu-latest 12 | environment: pypi 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | - name: Set up Python 3.9 18 | uses: actions/setup-python@v3 19 | with: 20 | python-version: 3.9 21 | 22 | - name: Install pypa/build 23 | run: >- 24 | python -m 25 | pip install 26 | build 27 | --user 28 | - name: Build a binary wheel and a source tarball 29 | run: >- 30 | python -m 31 | build 32 | --sdist 33 | --wheel 34 | --outdir dist/ 35 | . 36 | 37 | - name: Publish distribution 📦 to PyPI 38 | if: startsWith(github.ref, 'refs/tags') 39 | uses: pypa/gh-action-pypi-publish@release/v1 40 | with: 41 | user: __token__ 42 | password: ${{ secrets.PYPI_API_TOKEN }} 43 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-test-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distributions 📦 to TestPyPI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build-n-publish: 10 | name: Build and publish Python 🐍 distributions 📦 to TestPyPI 11 | environment: pypi-test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | - name: Set up Python 3.9 18 | uses: actions/setup-python@v3 19 | with: 20 | python-version: 3.9 21 | 22 | - name: Install pypa/build 23 | run: >- 24 | python -m 25 | pip install 26 | build 27 | --user 28 | - name: Build a binary wheel and a source tarball 29 | run: >- 30 | python -m 31 | build 32 | --sdist 33 | --wheel 34 | --outdir dist/ 35 | . 36 | 37 | - name: Publish distribution 📦 to Test PyPI 38 | uses: pypa/gh-action-pypi-publish@release/v1 39 | with: 40 | user: __token__ 41 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 42 | repository_url: https://test.pypi.org/legacy/ 43 | skip_existing: true 44 | -------------------------------------------------------------------------------- /example/winners/tests/test_models.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.test import TestCase, override_settings 3 | from django.core.files.base import ContentFile 4 | 5 | from import_export_celery.models.exportjob import ExportJob 6 | from import_export_celery.models.importjob import ImportJob 7 | 8 | 9 | class ImportJobTestCases(TestCase): 10 | 11 | def test_delete_file_on_job_delete(self): 12 | job = ImportJob.objects.create( 13 | file=ContentFile(b"", "file.csv"), 14 | ) 15 | file_path = job.file.path 16 | assert os.path.exists(file_path) 17 | job.delete() 18 | assert not os.path.exists(file_path) 19 | assert not ImportJob.objects.filter(id=job.id).exists() 20 | 21 | 22 | class ExportJobTestCases(TestCase): 23 | def test_create_export_job_default_email_on_completion(self): 24 | job = ExportJob.objects.create( 25 | app_label="winners", model="Winner" 26 | ) 27 | job.refresh_from_db() 28 | self.assertTrue(job.email_on_completion) 29 | 30 | @override_settings(EXPORT_JOB_EMAIL_ON_COMPLETION=False) 31 | def test_create_export_job_false_email_on_completion(self): 32 | job = ExportJob.objects.create( 33 | app_label="winners", model="Winner" 34 | ) 35 | job.refresh_from_db() 36 | self.assertFalse(job.email_on_completion) 37 | -------------------------------------------------------------------------------- /example/README.rst: -------------------------------------------------------------------------------- 1 | Install 2 | ======= 3 | 4 | Launch docker-compose 5 | 6 | .. code-block:: bash 7 | 8 | docker-compose up 9 | 10 | Attach to docker-compose 11 | 12 | .. code-block:: bash 13 | 14 | docker attach djangoimportexportcelery_web 15 | 16 | Install Django dependencies: 17 | 18 | .. code-block:: bash 19 | 20 | cd example 21 | pipenv install 22 | pipenv shell 23 | 24 | Initialize database tables: 25 | 26 | .. code-block:: bash 27 | 28 | python manage.py migrate 29 | 30 | Create a super-user for the admin: 31 | 32 | .. code-block:: bash 33 | 34 | python manage.py createsuperuser 35 | 36 | Restart docker-compose 37 | 38 | .. code-block:: bash 39 | 40 | docker-compose down 41 | 42 | 43 | Run 44 | === 45 | 46 | Launch docker-compose 47 | 48 | .. code-block:: bash 49 | 50 | docker-compose up 51 | 52 | Attach to docker-compose 53 | 54 | .. code-block:: bash 55 | 56 | docker attach djangoimportexportcelery_web 57 | 58 | Enter pipenv shell: 59 | 60 | .. code-block:: bash 61 | 62 | cd example 63 | pipenv shell 64 | 65 | 66 | Actually run the server 67 | 68 | .. code-block:: bash 69 | 70 | python manage.py runserver 0.0.0.0:8000 71 | 72 | The example app will be available from http://127.0.0.1:8001/admin 73 | 74 | Note: parts of this example app were taken from the [djano-leaflet](https://github.com/makinacorpus/django-leaflet/tree/master/example) example app. 75 | -------------------------------------------------------------------------------- /import_export_celery/migrations/0010_auto_20231013_0904.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-10-13 09:04 2 | 3 | from django.db import migrations 4 | import import_export_celery.fields 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('import_export_celery', '0009_alter_exportjob_options_alter_importjob_options_and_more'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='exportjob', 16 | name='file', 17 | field=import_export_celery.fields.ImportExportFileField(max_length=255, storage=import_export_celery.fields.lazy_initialize_storage_class, upload_to='django-import-export-celery-export-jobs', verbose_name='exported file'), 18 | ), 19 | migrations.AlterField( 20 | model_name='importjob', 21 | name='change_summary', 22 | field=import_export_celery.fields.ImportExportFileField(blank=True, null=True, storage=import_export_celery.fields.lazy_initialize_storage_class, upload_to='django-import-export-celery-import-change-summaries', verbose_name='Summary of changes made by this import'), 23 | ), 24 | migrations.AlterField( 25 | model_name='importjob', 26 | name='file', 27 | field=import_export_celery.fields.ImportExportFileField(max_length=255, storage=import_export_celery.fields.lazy_initialize_storage_class, upload_to='django-import-export-celery-import-jobs', verbose_name='File to be imported'), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /import_export_celery/migrations/0009_alter_exportjob_options_alter_importjob_options_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.9 on 2023-05-27 22:35 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('import_export_celery', '0008_alter_exportjob_id_alter_importjob_id'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='exportjob', 15 | options={'verbose_name': 'Export job', 'verbose_name_plural': 'Export jobs'}, 16 | ), 17 | migrations.AlterModelOptions( 18 | name='importjob', 19 | options={'verbose_name': 'Import job', 'verbose_name_plural': 'Import jobs'}, 20 | ), 21 | migrations.AlterField( 22 | model_name='exportjob', 23 | name='id', 24 | field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 25 | ), 26 | migrations.AlterField( 27 | model_name='exportjob', 28 | name='site_of_origin', 29 | field=models.TextField(default='', max_length=255, verbose_name='Site of origin'), 30 | ), 31 | migrations.AlterField( 32 | model_name='importjob', 33 | name='errors', 34 | field=models.TextField(blank=True, default='', verbose_name='Errors'), 35 | ), 36 | migrations.AlterField( 37 | model_name='importjob', 38 | name='id', 39 | field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import os 3 | from setuptools import setup, find_packages 4 | import subprocess 5 | import datetime 6 | 7 | here = os.path.abspath(os.path.dirname(__file__)) 8 | 9 | requires = ["Django", "django-import-export", "django-author", "html2text"] 10 | 11 | try: 12 | version = ( 13 | subprocess.check_output(["git", "describe", "--abbrev=0", "--tags"]) 14 | .decode("utf-8") 15 | .strip() 16 | ) 17 | except subprocess.CalledProcessError: 18 | version = "0.dev" + datetime.datetime.now().strftime("%Y%m%d%H%M%S") 19 | 20 | setup( 21 | name="django-import-export-celery", 22 | version=version, 23 | author="Timothy Hobbs", 24 | author_email="timothy.hobbs@auto-mat.cz", 25 | url="https://github.com/auto-mat/django-import-export-celery", 26 | download_url="http://pypi.python.org/pypi/django-import-export-celery/", 27 | description="Process long running django imports and exports in celery", 28 | long_description=codecs.open( 29 | os.path.join(here, "README.rst"), "r", "utf-8" 30 | ).read(), 31 | long_description_content_type="text/x-rst", 32 | license=( 33 | "License :: OSI Approved :: GNU Lesser General Public License v3.0 or" 34 | " later (LGPLv3.0+)" 35 | ), 36 | install_requires=requires, 37 | packages=find_packages(), 38 | include_package_data=True, 39 | zip_safe=False, 40 | classifiers=[ 41 | "Topic :: Utilities", 42 | "Natural Language :: English", 43 | "Operating System :: OS Independent", 44 | "Intended Audience :: Developers", 45 | "Environment :: Web Environment", 46 | "Framework :: Django", 47 | "Programming Language :: Python :: 3.7", 48 | "Programming Language :: Python :: 3.8", 49 | "Programming Language :: Python :: 3.9", 50 | ], 51 | ) 52 | -------------------------------------------------------------------------------- /import_export_celery/admin_actions.py: -------------------------------------------------------------------------------- 1 | from django.utils import timezone 2 | import json 3 | from uuid import UUID 4 | 5 | from django.utils.translation import gettext_lazy as _ 6 | from django.urls import reverse 7 | from django.shortcuts import redirect 8 | 9 | from .models import ExportJob 10 | 11 | from . import tasks 12 | 13 | 14 | def run_import_job_action(modeladmin, request, queryset): 15 | for instance in queryset: 16 | tasks.logger.info("Importing %s dry-run: False" % (instance.pk)) 17 | tasks.run_import_job.delay(instance.pk, dry_run=False) 18 | 19 | 20 | run_import_job_action.short_description = _("Perform import") 21 | 22 | 23 | def run_import_job_action_dry(modeladmin, request, queryset): 24 | for instance in queryset: 25 | tasks.logger.info("Importing %s dry-run: True" % (instance.pk)) 26 | tasks.run_import_job.delay(instance.pk, dry_run=True) 27 | 28 | 29 | run_import_job_action_dry.short_description = _("Perform dry import") 30 | 31 | 32 | def run_export_job_action(modeladmin, request, queryset): 33 | for instance in queryset: 34 | instance.processing_initiated = timezone.now() 35 | instance.save() 36 | tasks.run_export_job.delay(instance.pk) 37 | 38 | 39 | run_export_job_action.short_description = _("Run export job") 40 | 41 | 42 | def create_export_job_action(modeladmin, request, queryset): 43 | if queryset: 44 | arbitrary_obj = queryset.first() 45 | ej = ExportJob.objects.create( 46 | app_label=arbitrary_obj._meta.app_label, 47 | model=arbitrary_obj._meta.model_name, 48 | queryset=json.dumps( 49 | [ 50 | str(obj.pk) if isinstance(obj.pk, UUID) else obj.pk 51 | for obj in queryset 52 | ] 53 | ), 54 | site_of_origin=request.scheme + "://" + request.get_host(), 55 | ) 56 | rurl = reverse( 57 | "admin:%s_%s_change" 58 | % ( 59 | ej._meta.app_label, 60 | ej._meta.model_name, 61 | ), 62 | args=[ej.pk], 63 | ) 64 | return redirect(rurl) 65 | 66 | 67 | create_export_job_action.short_description = _("Export with celery") 68 | -------------------------------------------------------------------------------- /example/winners/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase, override_settings 2 | 3 | from import_export_celery.utils import ( 4 | get_export_job_mail_subject, 5 | get_export_job_mail_template, 6 | get_export_job_mail_context, 7 | get_export_job_email_on_completion, 8 | DEFAULT_EXPORT_JOB_COMPLETION_MAIL_SUBJECT, 9 | DEFAULT_EXPORT_JOB_COMPLETION_MAIL_TEMPLATE, 10 | DEFAULT_EXPORT_JOB_EMAIL_ON_COMPLETION, 11 | ) 12 | from import_export_celery.models import ExportJob 13 | 14 | 15 | class UtilsTestCases(TestCase): 16 | def test_get_export_job_mail_subject_by_default(self): 17 | self.assertEqual( 18 | DEFAULT_EXPORT_JOB_COMPLETION_MAIL_SUBJECT, get_export_job_mail_subject() 19 | ) 20 | 21 | @override_settings(EXPORT_JOB_COMPLETION_MAIL_SUBJECT="New subject") 22 | def test_get_export_job_mail_subject_overridden(self): 23 | self.assertEqual("New subject", get_export_job_mail_subject()) 24 | 25 | def test_get_export_job_mail_template_default(self): 26 | self.assertEqual( 27 | DEFAULT_EXPORT_JOB_COMPLETION_MAIL_TEMPLATE, get_export_job_mail_template() 28 | ) 29 | 30 | @override_settings(EXPORT_JOB_COMPLETION_MAIL_TEMPLATE="mytemplate.html") 31 | def test_get_export_job_mail_template_overridden(self): 32 | self.assertEqual("mytemplate.html", get_export_job_mail_template()) 33 | 34 | def test_get_export_job_email_on_completion_default(self): 35 | self.assertEqual( 36 | DEFAULT_EXPORT_JOB_EMAIL_ON_COMPLETION, get_export_job_email_on_completion() 37 | ) 38 | 39 | @override_settings(EXPORT_JOB_EMAIL_ON_COMPLETION=False) 40 | def test_get_export_job_email_on_completion_overridden(self): 41 | self.assertEqual(False, get_export_job_email_on_completion()) 42 | 43 | def test_get_export_job_mail_context(self): 44 | export_job = ExportJob.objects.create( 45 | app_label="winners", model="Winner", site_of_origin="http://127.0.0.1:8000" 46 | ) 47 | context = get_export_job_mail_context(export_job) 48 | expected_context = { 49 | "app_label": "winners", 50 | "model": "Winner", 51 | "link": f"http://127.0.0.1:8000/adminimport_export_celery/exportjob/{export_job.id}/change/", 52 | } 53 | self.assertEqual(context, expected_context) 54 | -------------------------------------------------------------------------------- /example/winners/tests/test_fields.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.test import TestCase, override_settings 3 | from django.conf import settings 4 | from django.core.files.storage import FileSystemStorage 5 | import unittest 6 | 7 | from import_export_celery.fields import lazy_initialize_storage_class 8 | 9 | 10 | class FooTestingStorage(FileSystemStorage): 11 | pass 12 | 13 | 14 | class InitializeStorageClassTests(TestCase): 15 | 16 | def test_default(self): 17 | self.assertIsInstance(lazy_initialize_storage_class(), FileSystemStorage) 18 | 19 | @unittest.skipUnless(django.VERSION < (5, 1), "Test only applicable for Django versions < 5.1") 20 | @override_settings( 21 | IMPORT_EXPORT_CELERY_STORAGE="winners.tests.test_fields.FooTestingStorage" 22 | ) 23 | def test_old_style(self): 24 | del settings.IMPORT_EXPORT_CELERY_STORAGE_ALIAS 25 | del settings.STORAGES 26 | self.assertIsInstance(lazy_initialize_storage_class(), FooTestingStorage) 27 | 28 | @unittest.skipUnless((4, 2) <= django.VERSION, "Test only applicable for Django 4.2 and later") 29 | @override_settings( 30 | IMPORT_EXPORT_CELERY_STORAGE_ALIAS="test_import_export_celery", 31 | STORAGES={ 32 | "test_import_export_celery": { 33 | "BACKEND": "winners.tests.test_fields.FooTestingStorage", 34 | }, 35 | "staticfiles": { 36 | "BACKEND": "django.core.files.storage.FileSystemStorage", 37 | }, 38 | "default": { 39 | "BACKEND": "django.core.files.storage.FileSystemStorage", 40 | } 41 | } 42 | 43 | ) 44 | def test_new_style(self): 45 | self.assertIsInstance(lazy_initialize_storage_class(), FooTestingStorage) 46 | 47 | @unittest.skipUnless((4, 2) <= django.VERSION, "Test only applicable for Django 4.2 and later") 48 | @override_settings( 49 | STORAGES={ 50 | "staticfiles": { 51 | "BACKEND": "django.core.files.storage.FileSystemStorage", 52 | }, 53 | "default": { 54 | "BACKEND": "winners.tests.test_fields.FooTestingStorage", 55 | } 56 | } 57 | ) 58 | def test_default_storage(self): 59 | """ Test that "default" storage is used when no alias is provided """ 60 | self.assertIsInstance(lazy_initialize_storage_class(), FooTestingStorage) 61 | -------------------------------------------------------------------------------- /import_export_celery/utils.py: -------------------------------------------------------------------------------- 1 | import html2text 2 | from django.core.mail import send_mail 3 | from django.template.loader import get_template 4 | from django.conf import settings 5 | from django.urls import reverse 6 | from import_export.formats.base_formats import DEFAULT_FORMATS 7 | 8 | DEFAULT_EXPORT_JOB_EMAIL_ON_COMPLETION = True 9 | DEFAULT_EXPORT_JOB_COMPLETION_MAIL_SUBJECT = "Django: Export job completed" 10 | DEFAULT_EXPORT_JOB_COMPLETION_MAIL_TEMPLATE = ( 11 | "email/export_job_completion.html" 12 | ) 13 | IMPORT_EXPORT_CELERY_EXCLUDED_FORMATS = getattr( 14 | settings, 15 | "IMPORT_EXPORT_CELERY_EXCLUDED_FORMATS", 16 | [], 17 | ) 18 | 19 | 20 | def get_formats(): 21 | return [ 22 | format 23 | for format in DEFAULT_FORMATS 24 | if format.TABLIB_MODULE.split(".")[-1].strip("_") 25 | not in IMPORT_EXPORT_CELERY_EXCLUDED_FORMATS 26 | ] 27 | 28 | 29 | def build_html_and_text_message(template_name, context={}): 30 | """ 31 | Render the given template with the context and returns 32 | the data in html and plain text format. 33 | """ 34 | template = get_template(template_name) 35 | html_message = template.render(context) 36 | text_message = html2text.html2text(html_message) 37 | return html_message, text_message 38 | 39 | 40 | def get_export_job_mail_context(export_job): 41 | context = { 42 | "app_label": export_job.app_label, 43 | "model": export_job.model, 44 | "link": export_job.site_of_origin 45 | + reverse( 46 | "admin:%s_%s_change" 47 | % ( 48 | export_job._meta.app_label, 49 | export_job._meta.model_name, 50 | ), 51 | args=[export_job.pk], 52 | ), 53 | } 54 | return context 55 | 56 | 57 | def get_export_job_email_on_completion(): 58 | return getattr( 59 | settings, 60 | "EXPORT_JOB_EMAIL_ON_COMPLETION", 61 | DEFAULT_EXPORT_JOB_EMAIL_ON_COMPLETION, 62 | ) 63 | 64 | 65 | def get_export_job_mail_subject(): 66 | return getattr( 67 | settings, 68 | "EXPORT_JOB_COMPLETION_MAIL_SUBJECT", 69 | DEFAULT_EXPORT_JOB_COMPLETION_MAIL_SUBJECT, 70 | ) 71 | 72 | 73 | def get_export_job_mail_template(): 74 | return getattr( 75 | settings, 76 | "EXPORT_JOB_COMPLETION_MAIL_TEMPLATE", 77 | DEFAULT_EXPORT_JOB_COMPLETION_MAIL_TEMPLATE, 78 | ) 79 | 80 | 81 | def send_export_job_completion_mail(export_job): 82 | """ 83 | Send export job completion mail 84 | """ 85 | subject = get_export_job_mail_subject() 86 | template_name = get_export_job_mail_template() 87 | context = get_export_job_mail_context(export_job) 88 | context.update({"export_job": export_job}) 89 | html_message, text_message = build_html_and_text_message( 90 | template_name, context 91 | ) 92 | send_mail( 93 | subject=subject, 94 | message=text_message, 95 | html_message=html_message, 96 | from_email=settings.SERVER_EMAIL, 97 | recipient_list=[export_job.updated_by.email], 98 | ) 99 | -------------------------------------------------------------------------------- /import_export_celery/migrations/0006_auto_20191125_1236.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.7 on 2019-11-25 12:36 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("import_export_celery", "0005_exportjob_site_of_origin"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="exportjob", 15 | name="job_status", 16 | field=models.CharField( 17 | blank=True, max_length=160, verbose_name="Status of the job" 18 | ), 19 | ), 20 | migrations.AlterField( 21 | model_name="exportjob", 22 | name="format", 23 | field=models.CharField( 24 | choices=[ 25 | ("text/csv", "text/csv"), 26 | ("application/vnd.ms-excel", "application/vnd.ms-excel"), 27 | ( 28 | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 29 | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 30 | ), 31 | ("text/tab-separated-values", "text/tab-separated-values"), 32 | ( 33 | "application/vnd.oasis.opendocument.spreadsheet", 34 | "application/vnd.oasis.opendocument.spreadsheet", 35 | ), 36 | ("application/json", "application/json"), 37 | ("text/yaml", "text/yaml"), 38 | ("text/html", "text/html"), 39 | ], 40 | max_length=255, 41 | null=True, 42 | verbose_name="Format of file to be exported", 43 | ), 44 | ), 45 | migrations.AlterField( 46 | model_name="importjob", 47 | name="format", 48 | field=models.CharField( 49 | choices=[ 50 | ("text/csv", "text/csv"), 51 | ("application/vnd.ms-excel", "application/vnd.ms-excel"), 52 | ( 53 | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 54 | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 55 | ), 56 | ("text/tab-separated-values", "text/tab-separated-values"), 57 | ( 58 | "application/vnd.oasis.opendocument.spreadsheet", 59 | "application/vnd.oasis.opendocument.spreadsheet", 60 | ), 61 | ("application/json", "application/json"), 62 | ("text/yaml", "text/yaml"), 63 | ("text/html", "text/html"), 64 | ], 65 | max_length=255, 66 | verbose_name="Format of file to be imported", 67 | ), 68 | ), 69 | migrations.AlterField( 70 | model_name="importjob", 71 | name="model", 72 | field=models.CharField( 73 | max_length=160, verbose_name="Name of model to import to" 74 | ), 75 | ), 76 | ] 77 | -------------------------------------------------------------------------------- /.github/workflows/linters.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.ref }} 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | services: 14 | postgres: 15 | image: postgres:13 16 | ports: 17 | - 5432:5432 18 | env: 19 | POSTGRES_DB: pguser 20 | POSTGRES_USER: pguser 21 | POSTGRES_PASSWORD: foobar 22 | options: >- 23 | --health-cmd "pg_isready -U pguser" 24 | --health-interval 10s 25 | --health-timeout 5s 26 | --health-retries 5 27 | 28 | redis: 29 | image: redis:latest 30 | ports: 31 | - 6379:6379 32 | options: >- 33 | --health-cmd "redis-cli ping" 34 | --health-interval 10s 35 | --health-timeout 5s 36 | --health-retries 5 37 | 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 42 | django-version: ["3.2", "4.0", "4.1", "4.2", "5.0", "5.1"] 43 | exclude: 44 | - python-version: "3.8" 45 | django-version: "5.0" 46 | - python-version: "3.8" 47 | django-version: "5.1" 48 | 49 | - python-version: "3.9" 50 | django-version: "5.0" 51 | - python-version: "3.9" 52 | django-version: "5.1" 53 | 54 | - python-version: "3.11" 55 | django-version: "3.2" 56 | 57 | - python-version: "3.12" 58 | django-version: "3.2" 59 | - python-version: "3.12" 60 | django-version: "4.0" 61 | 62 | steps: 63 | - uses: actions/checkout@v2 64 | 65 | - name: Set up Python ${{ matrix.python-version }} 66 | uses: actions/setup-python@v2 67 | with: 68 | python-version: ${{ matrix.python-version }} 69 | 70 | - name: Install dependencies 71 | run: | 72 | python -m pip install --upgrade pip 73 | pip install tox tox-gh-actions 74 | 75 | - name: Run Tox 76 | env: 77 | DJANGO_VERSION: ${{ matrix.django-version }} 78 | run: | 79 | PYTHON_VERSION=`echo ${{ matrix.python-version }} | sed 's/\.//'` 80 | DJANGO_VERSION=`echo $DJANGO_VERSION | sed 's/\.//'` 81 | tox -e py${PYTHON_VERSION}-django${DJANGO_VERSION} 82 | 83 | - name: Upload coverage to Codecov 84 | uses: codecov/codecov-action@v4 85 | with: 86 | token: ${{ secrets.CODECOV_TOKEN }} 87 | files: coverage.xml 88 | fail_ci_if_error: true 89 | 90 | flake8: 91 | name: flake8 92 | runs-on: ubuntu-latest 93 | steps: 94 | - name: Checkout 95 | uses: actions/checkout@v2 96 | - name: Set up Python 97 | uses: actions/setup-python@v2 98 | with: 99 | python-version: 3.9 100 | - run: pip install --upgrade flake8 101 | - name: flake8 102 | uses: liskin/gh-problem-matcher-wrap@v1 103 | with: 104 | linters: flake8 105 | run: flake8 106 | -------------------------------------------------------------------------------- /import_export_celery/admin.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 o.s. Auto*Mat 2 | from django import forms 3 | from django.conf import settings 4 | from django.contrib import admin 5 | from django.core.cache import cache 6 | from django.utils.translation import gettext_lazy as _ 7 | 8 | from . import admin_actions, models 9 | 10 | 11 | class JobWithStatusMixin: 12 | @admin.display(description=_("Job status info")) 13 | def job_status_info(self, obj): 14 | job_status = cache.get(self.direction + "_job_status_%s" % obj.pk) 15 | if job_status: 16 | return job_status 17 | else: 18 | return obj.job_status 19 | 20 | 21 | class ImportJobForm(forms.ModelForm): 22 | model = forms.ChoiceField(label=_("Name of model to import to")) 23 | 24 | class Meta: 25 | model = models.ImportJob 26 | fields = "__all__" 27 | 28 | def __init__(self, *args, **kwargs): 29 | super().__init__(*args, **kwargs) 30 | self.fields["model"].choices = [ 31 | (x, x) for x in getattr(settings, "IMPORT_EXPORT_CELERY_MODELS", {}).keys() 32 | ] 33 | self.fields["format"].widget = forms.Select( 34 | choices=self.instance.get_format_choices() 35 | ) 36 | 37 | 38 | @admin.register(models.ImportJob) 39 | class ImportJobAdmin(JobWithStatusMixin, admin.ModelAdmin): 40 | direction = "import" 41 | form = ImportJobForm 42 | list_display = ( 43 | "model", 44 | "job_status_info", 45 | "file", 46 | "change_summary", 47 | "imported", 48 | "author", 49 | "updated_by", 50 | ) 51 | readonly_fields = ( 52 | "job_status_info", 53 | "change_summary", 54 | "imported", 55 | "errors", 56 | "author", 57 | "updated_by", 58 | "processing_initiated", 59 | ) 60 | exclude = ("job_status",) 61 | 62 | list_filter = ("model", "imported") 63 | 64 | actions = ( 65 | admin_actions.run_import_job_action, 66 | admin_actions.run_import_job_action_dry, 67 | ) 68 | 69 | 70 | class ExportJobForm(forms.ModelForm): 71 | class Meta: 72 | model = models.ExportJob 73 | exclude = ("site_of_origin",) 74 | 75 | def __init__(self, *args, **kwargs): 76 | super().__init__(*args, **kwargs) 77 | self.fields["resource"].widget = forms.Select( 78 | choices=self.instance.get_resource_choices() 79 | ) 80 | self.fields["format"].widget = forms.Select( 81 | choices=self.instance.get_format_choices() 82 | ) 83 | 84 | 85 | @admin.register(models.ExportJob) 86 | class ExportJobAdmin(JobWithStatusMixin, admin.ModelAdmin): 87 | direction = "export" 88 | form = ExportJobForm 89 | list_display = ( 90 | "model", 91 | "app_label", 92 | "file", 93 | "job_status_info", 94 | "author", 95 | "updated_by", 96 | ) 97 | readonly_fields = ( 98 | "job_status_info", 99 | "author", 100 | "updated_by", 101 | "app_label", 102 | "model", 103 | "file", 104 | "processing_initiated", 105 | ) 106 | exclude = ("job_status",) 107 | 108 | list_filter = ("model",) 109 | 110 | def has_add_permission(self, request, obj=None): 111 | return False 112 | 113 | actions = (admin_actions.run_export_job_action,) 114 | -------------------------------------------------------------------------------- /import_export_celery/models/importjob.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 o.s. Auto*Mat 2 | 3 | from django.conf import settings 4 | from django.utils import timezone 5 | 6 | from author.decorators import with_author 7 | 8 | from django.db import models, transaction 9 | from django.dispatch import receiver 10 | 11 | from django.db.models.signals import post_save, post_delete 12 | from django.utils.translation import gettext_lazy as _ 13 | 14 | from import_export.formats.base_formats import DEFAULT_FORMATS 15 | 16 | from ..fields import ImportExportFileField 17 | from ..tasks import run_import_job 18 | 19 | import logging 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | 24 | @with_author 25 | class ImportJob(models.Model): 26 | file = ImportExportFileField( 27 | verbose_name=_("File to be imported"), 28 | upload_to="django-import-export-celery-import-jobs", 29 | blank=False, 30 | null=False, 31 | max_length=255, 32 | ) 33 | 34 | processing_initiated = models.DateTimeField( 35 | verbose_name=_("Have we started processing the file? If so when?"), 36 | null=True, 37 | blank=True, 38 | default=None, 39 | ) 40 | 41 | imported = models.DateTimeField( 42 | verbose_name=_("Has the import been completed? If so when?"), 43 | null=True, 44 | blank=True, 45 | default=None, 46 | ) 47 | 48 | format = models.CharField( 49 | verbose_name=_("Format of file to be imported"), 50 | max_length=255, 51 | ) 52 | 53 | change_summary = ImportExportFileField( 54 | verbose_name=_("Summary of changes made by this import"), 55 | upload_to="django-import-export-celery-import-change-summaries", 56 | blank=True, 57 | null=True, 58 | ) 59 | 60 | errors = models.TextField( 61 | verbose_name=_("Errors"), 62 | default="", 63 | blank=True, 64 | ) 65 | 66 | model = models.CharField( 67 | verbose_name=_("Name of model to import to"), 68 | max_length=160, 69 | ) 70 | 71 | job_status = models.CharField( 72 | verbose_name=_("Status of the job"), 73 | max_length=160, 74 | blank=True, 75 | ) 76 | 77 | class Meta: 78 | verbose_name = _("Import job") 79 | verbose_name_plural = _("Import jobs") 80 | 81 | @staticmethod 82 | def get_format_choices(): 83 | """returns choices of available import formats""" 84 | return [ 85 | (f.CONTENT_TYPE, f().get_title()) 86 | for f in DEFAULT_FORMATS 87 | if f().can_import() 88 | ] 89 | 90 | 91 | @receiver(post_save, sender=ImportJob) 92 | def importjob_post_save(sender, instance, **kwargs): 93 | if not instance.processing_initiated: 94 | instance.processing_initiated = timezone.now() 95 | instance.save() 96 | transaction.on_commit( 97 | lambda: run_import_job.delay( 98 | instance.pk, 99 | dry_run=getattr(settings, "IMPORT_DRY_RUN_FIRST_TIME", True), 100 | ) 101 | ) 102 | 103 | 104 | @receiver(post_delete, sender=ImportJob) 105 | def auto_delete_file_on_delete(sender, instance, **kwargs): 106 | """ 107 | Deletes file related to the import job 108 | """ 109 | if instance.file: 110 | try: 111 | instance.file.delete() 112 | except Exception as e: 113 | logger.error( 114 | "Some error occurred while deleting ImportJob file: {0}".format(e) 115 | ) 116 | ImportJob.objects.filter(id=instance.id).delete() 117 | -------------------------------------------------------------------------------- /import_export_celery/models/exportjob.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 o.s. Auto*Mat 2 | from django.utils import timezone 3 | import json 4 | 5 | from author.decorators import with_author 6 | 7 | from django.contrib.contenttypes.models import ContentType 8 | from django.db import models 9 | from django.db import transaction 10 | from django.dispatch import receiver 11 | 12 | from django.db.models.signals import post_save 13 | from django.utils.translation import gettext_lazy as _ 14 | 15 | from ..fields import ImportExportFileField 16 | from ..tasks import run_export_job 17 | from ..utils import get_formats, get_export_job_email_on_completion 18 | 19 | 20 | @with_author 21 | class ExportJob(models.Model): 22 | def __init__(self, *args, **kwargs): 23 | super().__init__(*args, **kwargs) 24 | self._content_type = None 25 | 26 | file = ImportExportFileField( 27 | verbose_name=_("exported file"), 28 | upload_to="django-import-export-celery-export-jobs", 29 | blank=False, 30 | null=False, 31 | max_length=255, 32 | ) 33 | 34 | processing_initiated = models.DateTimeField( 35 | verbose_name=_("Have we started processing the file? If so when?"), 36 | null=True, 37 | blank=True, 38 | default=None, 39 | ) 40 | 41 | job_status = models.CharField( 42 | verbose_name=_("Status of the job"), 43 | max_length=160, 44 | blank=True, 45 | ) 46 | 47 | format = models.CharField( 48 | verbose_name=_("Format of file to be exported"), 49 | max_length=255, 50 | blank=False, 51 | null=True, 52 | ) 53 | 54 | app_label = models.CharField( 55 | verbose_name=_("App label of model to export from"), 56 | max_length=160, 57 | ) 58 | 59 | model = models.CharField( 60 | verbose_name=_("Name of model to export from"), 61 | max_length=160, 62 | ) 63 | 64 | resource = models.CharField( 65 | verbose_name=_("Resource to use when exporting"), 66 | max_length=255, 67 | default="", 68 | ) 69 | 70 | queryset = models.TextField( 71 | verbose_name=_("JSON list of pks to export"), 72 | null=False, 73 | ) 74 | 75 | email_on_completion = models.BooleanField( 76 | verbose_name=_("Send me an email when this export job is complete"), 77 | default=get_export_job_email_on_completion, 78 | ) 79 | 80 | site_of_origin = models.TextField( 81 | verbose_name=_("Site of origin"), 82 | max_length=255, 83 | default="", 84 | ) 85 | 86 | class Meta: 87 | verbose_name = _("Export job") 88 | verbose_name_plural = _("Export jobs") 89 | 90 | def get_resource_class(self): 91 | if self.resource: 92 | return ( 93 | self.get_content_type() 94 | .model_class() 95 | .export_resource_classes()[self.resource][1] 96 | ) 97 | 98 | def get_content_type(self): 99 | if not self._content_type: 100 | self._content_type = ContentType.objects.get( 101 | app_label=self.app_label, 102 | model=self.model, 103 | ) 104 | return self._content_type 105 | 106 | def get_queryset(self): 107 | pks = json.loads(self.queryset) 108 | # If customised queryset for the model exists 109 | # then it'll apply filter on that otherwise it'll 110 | # apply filter directly on the model. 111 | resource_class = self.get_resource_class() 112 | if hasattr(resource_class, "get_export_queryset"): 113 | return resource_class().get_export_queryset().filter(pk__in=pks) 114 | return self.get_content_type().model_class().objects.filter(pk__in=pks) 115 | 116 | def get_resource_choices(self): 117 | return [ 118 | (k, v[0]) 119 | for k, v in self.get_content_type() 120 | .model_class() 121 | .export_resource_classes() 122 | .items() 123 | ] 124 | 125 | @staticmethod 126 | def get_format_choices(): 127 | """returns choices of available export formats""" 128 | return [ 129 | (f.CONTENT_TYPE, f().get_title()) 130 | for f in get_formats() 131 | if f().can_export() 132 | ] 133 | 134 | 135 | @receiver(post_save, sender=ExportJob) 136 | def exportjob_post_save(sender, instance, **kwargs): 137 | if instance.resource and not instance.processing_initiated: 138 | instance.processing_initiated = timezone.now() 139 | instance.save() 140 | transaction.on_commit(lambda: run_export_job.delay(instance.pk)) 141 | -------------------------------------------------------------------------------- /import_export_celery/migrations/0003_exportjob.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-11-13 11:27 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ("import_export_celery", "0002_auto_20190923_1132"), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name="ExportJob", 18 | fields=[ 19 | ( 20 | "id", 21 | models.AutoField( 22 | auto_created=True, 23 | primary_key=True, 24 | serialize=False, 25 | verbose_name="ID", 26 | ), 27 | ), 28 | ( 29 | "file", 30 | models.FileField( 31 | max_length=255, 32 | upload_to="django-import-export-celery-export-jobs", 33 | verbose_name="exported file", 34 | ), 35 | ), 36 | ( 37 | "processing_initiated", 38 | models.DateTimeField( 39 | blank=True, 40 | default=None, 41 | null=True, 42 | verbose_name="Have we started processing the file? If so when?", 43 | ), 44 | ), 45 | ( 46 | "format", 47 | models.CharField( 48 | choices=[ 49 | ("text/csv", "text/csv"), 50 | ("application/vnd.ms-excel", "application/vnd.ms-excel"), 51 | ( 52 | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 53 | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 54 | ), 55 | ("text/tab-separated-values", "text/tab-separated-values"), 56 | ( 57 | "application/vnd.oasis.opendocument.spreadsheet", 58 | "application/vnd.oasis.opendocument.spreadsheet", 59 | ), 60 | ("application/json", "application/json"), 61 | ("text/yaml", "text/yaml"), 62 | ("text/html", "text/html"), 63 | ], 64 | max_length=40, 65 | null=True, 66 | verbose_name="Format of file to be exported", 67 | ), 68 | ), 69 | ( 70 | "app_label", 71 | models.CharField( 72 | max_length=160, verbose_name="App label of model to export from" 73 | ), 74 | ), 75 | ( 76 | "model", 77 | models.CharField( 78 | max_length=160, verbose_name="Name of model to export from" 79 | ), 80 | ), 81 | ( 82 | "resource", 83 | models.CharField( 84 | default="", 85 | max_length=255, 86 | verbose_name="Resource to use when exporting", 87 | ), 88 | ), 89 | ( 90 | "queryset", 91 | models.TextField(verbose_name="JSON list of pks to export"), 92 | ), 93 | ( 94 | "author", 95 | models.ForeignKey( 96 | blank=True, 97 | null=True, 98 | on_delete=django.db.models.deletion.SET_NULL, 99 | related_name="exportjob_create", 100 | to=settings.AUTH_USER_MODEL, 101 | verbose_name="author", 102 | ), 103 | ), 104 | ( 105 | "updated_by", 106 | models.ForeignKey( 107 | blank=True, 108 | null=True, 109 | on_delete=django.db.models.deletion.SET_NULL, 110 | related_name="exportjob_update", 111 | to=settings.AUTH_USER_MODEL, 112 | verbose_name="last updated by", 113 | ), 114 | ), 115 | ], 116 | ), 117 | ] 118 | -------------------------------------------------------------------------------- /import_export_celery/locale/eu/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 2 | # This file is distributed under the same license as the PACKAGE package. 3 | # 4 | # Urtzi Odriozola , 2023. 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: \n" 8 | "Report-Msgid-Bugs-To: \n" 9 | "POT-Creation-Date: 2023-05-17 20:55+0000\n" 10 | "PO-Revision-Date: 2023-06-07 21:56+0100\n" 11 | "Last-Translator: Urtzi Odriozola \n" 12 | "Language-Team: \n" 13 | "Language: eu\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 18 | 19 | #: import_export_celery/admin.py:12 20 | msgid "Job status info" 21 | msgstr "Lanaren egoera" 22 | 23 | #: import_export_celery/admin.py:22 import_export_celery/models/importjob.py:62 24 | msgid "Name of model to import to" 25 | msgstr "Inportatu beharreko modeloaren izena" 26 | 27 | #: import_export_celery/admin_actions.py:20 28 | msgid "Perform import" 29 | msgstr "Hasi inportazioa" 30 | 31 | #: import_export_celery/admin_actions.py:29 32 | msgid "Perform dry import" 33 | msgstr "Hasi probako inportazioa" 34 | 35 | #: import_export_celery/admin_actions.py:39 36 | msgid "Run export job" 37 | msgstr "Hasi esportazio lana" 38 | 39 | #: import_export_celery/admin_actions.py:67 40 | msgid "Export with celery" 41 | msgstr "Exportatu celery bidez" 42 | 43 | #: import_export_celery/apps.py:7 44 | msgid "Import Export Celery" 45 | msgstr "Inportatu - Esportatu Celery bidez" 46 | 47 | #: import_export_celery/models/exportjob.py:27 48 | msgid "exported file" 49 | msgstr "esportatutako fitxategia" 50 | 51 | #: import_export_celery/models/exportjob.py:35 52 | #: import_export_celery/models/importjob.py:30 53 | msgid "Have we started processing the file? If so when?" 54 | msgstr "Fitxategia prozesatzen hasi gara? Hala bada, noiz?" 55 | 56 | #: import_export_celery/models/exportjob.py:42 57 | #: import_export_celery/models/importjob.py:67 58 | msgid "Status of the job" 59 | msgstr "Lanaren egoera" 60 | 61 | #: import_export_celery/models/exportjob.py:48 62 | msgid "Format of file to be exported" 63 | msgstr "Esportatu beharreko fitxategiaren formatua" 64 | 65 | #: import_export_celery/models/exportjob.py:55 66 | msgid "App label of model to export from" 67 | msgstr "Esportatu beharreko modeloaren aplikazio izena" 68 | 69 | #: import_export_celery/models/exportjob.py:60 70 | msgid "Name of model to export from" 71 | msgstr "Esportatu beharreko modeloaren izena" 72 | 73 | #: import_export_celery/models/exportjob.py:65 74 | msgid "Resource to use when exporting" 75 | msgstr "Esportatzeko erabili beharreko baliabidea" 76 | 77 | #: import_export_celery/models/exportjob.py:71 78 | msgid "JSON list of pks to export" 79 | msgstr "Esportatzeko ID-en JSON zerrenda" 80 | 81 | #: import_export_celery/models/exportjob.py:76 82 | msgid "Send me an email when this export job is complete" 83 | msgstr "Bidali eposta bat esportazio lan hau amaitzen denean" 84 | 85 | #: import_export_celery/models/exportjob.py:81 86 | msgid "Site of origin" 87 | msgstr "Jatorrizko gunea" 88 | 89 | #: import_export_celery/models/exportjob.py:87 90 | msgid "Export job" 91 | msgstr "Esportazio lana" 92 | 93 | #: import_export_celery/models/exportjob.py:88 94 | msgid "Export jobs" 95 | msgstr "Esportazio lanak" 96 | 97 | #: import_export_celery/models/importjob.py:22 98 | msgid "File to be imported" 99 | msgstr "Inportatu beharreko fitxategia" 100 | 101 | #: import_export_celery/models/importjob.py:37 102 | msgid "Has the import been completed? If so when?" 103 | msgstr "Inportazioa amaitu da? Hala bada, noiz?" 104 | 105 | #: import_export_celery/models/importjob.py:44 106 | msgid "Format of file to be imported" 107 | msgstr "Inportatu beharreko fitxategiaren formatua" 108 | 109 | #: import_export_celery/models/importjob.py:49 110 | msgid "Summary of changes made by this import" 111 | msgstr "Inportazio honek eginiko aldaketen laburpena" 112 | 113 | #: import_export_celery/models/importjob.py:56 114 | msgid "Errors" 115 | msgstr "Erroreak" 116 | 117 | #: import_export_celery/models/importjob.py:73 118 | msgid "Import job" 119 | msgstr "Inportazio lana" 120 | 121 | #: import_export_celery/models/importjob.py:74 122 | msgid "Import jobs" 123 | msgstr "Inportazio lanak" 124 | 125 | #: import_export_celery/tasks.py:61 126 | #, python-format 127 | msgid "Imported file has a wrong encoding: %s" 128 | msgstr "Inportatutako fitxategiak kodetze okerra du: %s" 129 | 130 | #: import_export_celery/tasks.py:68 131 | #, python-format 132 | msgid "Error reading file: %s" 133 | msgstr "Errorea fitxategia irakurtzean: %s" 134 | 135 | #: import_export_celery/tasks.py:101 136 | #, python-format 137 | msgid "" 138 | "Line: %s - %s\n" 139 | "\t%s\n" 140 | "%s" 141 | msgstr "" 142 | "Lerroa: %s - %s\n" 143 | "\t%s\n" 144 | "%s" 145 | 146 | #: import_export_celery/tasks.py:190 147 | #, python-format 148 | msgid "Import error %s" 149 | msgstr "Inportazio errorea %s" 150 | -------------------------------------------------------------------------------- /import_export_celery/locale/pt/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 2 | # This file is distributed under the same license as the PACKAGE package. 3 | # 4 | # Dan , 2020. 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: \n" 8 | "Report-Msgid-Bugs-To: \n" 9 | "POT-Creation-Date: 2023-05-17 20:55+0000\n" 10 | "PO-Revision-Date: 2023-05-17 21:56+0100\n" 11 | "Last-Translator: Daniel Pluth \n" 12 | "Language-Team: \n" 13 | "Language: pt_BR\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 18 | "X-Generator: Poedit 3.3.1\n" 19 | 20 | #: import_export_celery/admin.py:12 21 | msgid "Job status info" 22 | msgstr "Informação do estado do trabalho" 23 | 24 | #: import_export_celery/admin.py:22 import_export_celery/models/importjob.py:62 25 | msgid "Name of model to import to" 26 | msgstr "Nome do modelo a importar" 27 | 28 | #: import_export_celery/admin_actions.py:20 29 | msgid "Perform import" 30 | msgstr "Confirmar importação" 31 | 32 | #: import_export_celery/admin_actions.py:29 33 | msgid "Perform dry import" 34 | msgstr "Confirmar importação" 35 | 36 | #: import_export_celery/admin_actions.py:39 37 | msgid "Run export job" 38 | msgstr "Correr trabalho de exportação" 39 | 40 | #: import_export_celery/admin_actions.py:67 41 | msgid "Export with celery" 42 | msgstr "Exportar com o celery" 43 | 44 | #: import_export_celery/apps.py:7 45 | msgid "Import Export Celery" 46 | msgstr "Importar ou Exportar com o celery" 47 | 48 | #: import_export_celery/models/exportjob.py:27 49 | msgid "exported file" 50 | msgstr "ficheiro exportado" 51 | 52 | #: import_export_celery/models/exportjob.py:35 53 | #: import_export_celery/models/importjob.py:30 54 | msgid "Have we started processing the file? If so when?" 55 | msgstr "Começou a processar o ficheiro? Se sim, quando?" 56 | 57 | #: import_export_celery/models/exportjob.py:42 58 | #: import_export_celery/models/importjob.py:67 59 | msgid "Status of the job" 60 | msgstr "Estado do trabalho" 61 | 62 | #: import_export_celery/models/exportjob.py:48 63 | msgid "Format of file to be exported" 64 | msgstr "Formato do ficheiro a ser exportado" 65 | 66 | #: import_export_celery/models/exportjob.py:55 67 | msgid "App label of model to export from" 68 | msgstr "Nome da aplicação do modelo para exportar de" 69 | 70 | #: import_export_celery/models/exportjob.py:60 71 | msgid "Name of model to export from" 72 | msgstr "Nome do modelo a exportar de" 73 | 74 | #: import_export_celery/models/exportjob.py:65 75 | msgid "Resource to use when exporting" 76 | msgstr "Recurso a utilizar aquando da exportação" 77 | 78 | #: import_export_celery/models/exportjob.py:71 79 | msgid "JSON list of pks to export" 80 | msgstr "Lista de ids em formato json para exportar" 81 | 82 | #: import_export_celery/models/exportjob.py:76 83 | msgid "Send me an email when this export job is complete" 84 | msgstr "Enviar um email quando o trabalho de exportação estiver completo" 85 | 86 | #: import_export_celery/models/exportjob.py:81 87 | msgid "Site of origin" 88 | msgstr "Site de origem" 89 | 90 | #: import_export_celery/models/exportjob.py:87 91 | msgid "Export job" 92 | msgstr "Trabalho de exportação" 93 | 94 | #: import_export_celery/models/exportjob.py:88 95 | msgid "Export jobs" 96 | msgstr "Trabalhos de exportação" 97 | 98 | #: import_export_celery/models/importjob.py:22 99 | msgid "File to be imported" 100 | msgstr "Arquivo a ser importado" 101 | 102 | #: import_export_celery/models/importjob.py:37 103 | msgid "Has the import been completed? If so when?" 104 | msgstr "A importação foi completa? Se sim, quando?" 105 | 106 | #: import_export_celery/models/importjob.py:44 107 | msgid "Format of file to be imported" 108 | msgstr "Formato do ficheiro a ser importado" 109 | 110 | #: import_export_celery/models/importjob.py:49 111 | msgid "Summary of changes made by this import" 112 | msgstr "Resumo das alterações feitas por esta importação" 113 | 114 | #: import_export_celery/models/importjob.py:56 115 | msgid "Errors" 116 | msgstr "Erros" 117 | 118 | #: import_export_celery/models/importjob.py:73 119 | msgid "Import job" 120 | msgstr "Trabalho de importação" 121 | 122 | #: import_export_celery/models/importjob.py:74 123 | msgid "Import jobs" 124 | msgstr "Trabalhos de importação" 125 | 126 | #: import_export_celery/tasks.py:61 127 | #, python-format 128 | msgid "Imported file has a wrong encoding: %s" 129 | msgstr "O arquivo importado tem uma codificação errada: %s" 130 | 131 | #: import_export_celery/tasks.py:68 132 | #, python-format 133 | msgid "Error reading file: %s" 134 | msgstr "Erro a ler o ficheiro: %s" 135 | 136 | #: import_export_celery/tasks.py:101 137 | #, python-format 138 | msgid "" 139 | "Line: %s - %s\n" 140 | "\t%s\n" 141 | "%s" 142 | msgstr "" 143 | "Linha: %s - %s\n" 144 | "\t%s\n" 145 | "%s" 146 | 147 | #: import_export_celery/tasks.py:190 148 | #, python-format 149 | msgid "Import error %s" 150 | msgstr "Erro de importação %s" 151 | -------------------------------------------------------------------------------- /import_export_celery/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.12 on 2019-06-28 13:55 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name="ImportJob", 19 | fields=[ 20 | ( 21 | "id", 22 | models.AutoField( 23 | auto_created=True, 24 | primary_key=True, 25 | serialize=False, 26 | verbose_name="ID", 27 | ), 28 | ), 29 | ( 30 | "file", 31 | models.FileField( 32 | max_length=255, 33 | upload_to="django-import-export-celery-import-jobs", 34 | verbose_name="File to be imported", 35 | ), 36 | ), 37 | ( 38 | "processing_initiated", 39 | models.DateTimeField( 40 | blank=True, 41 | default=None, 42 | null=True, 43 | verbose_name="Have we started processing the file? If so when?", 44 | ), 45 | ), 46 | ( 47 | "imported", 48 | models.DateTimeField( 49 | blank=True, 50 | default=None, 51 | null=True, 52 | verbose_name="Has the import been completed? If so when?", 53 | ), 54 | ), 55 | ( 56 | "format", 57 | models.CharField( 58 | choices=[ 59 | ("text/csv", "text/csv"), 60 | ("application/vnd.ms-excel", "application/vnd.ms-excel"), 61 | ( 62 | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 63 | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 64 | ), 65 | ("text/tab-separated-values", "text/tab-separated-values"), 66 | ( 67 | "application/vnd.oasis.opendocument.spreadsheet", 68 | "application/vnd.oasis.opendocument.spreadsheet", 69 | ), 70 | ("application/json", "application/json"), 71 | ("text/yaml", "text/yaml"), 72 | ("text/html", "text/html"), 73 | ], 74 | max_length=40, 75 | verbose_name="Format of file to be imported", 76 | ), 77 | ), 78 | ( 79 | "change_summary", 80 | models.FileField( 81 | blank=True, 82 | null=True, 83 | upload_to="django-import-export-celery-import-change-summaries", 84 | verbose_name="Summary of changes made by this import", 85 | ), 86 | ), 87 | ("errors", models.TextField(blank=True, default="")), 88 | ( 89 | "model", 90 | models.CharField( 91 | choices=[("Winner", "Winner")], 92 | max_length=160, 93 | verbose_name="Name of model to import to", 94 | ), 95 | ), 96 | ( 97 | "author", 98 | models.ForeignKey( 99 | blank=True, 100 | null=True, 101 | on_delete=django.db.models.deletion.SET_NULL, 102 | related_name="importjob_create", 103 | to=settings.AUTH_USER_MODEL, 104 | verbose_name="author", 105 | ), 106 | ), 107 | ( 108 | "updated_by", 109 | models.ForeignKey( 110 | blank=True, 111 | null=True, 112 | on_delete=django.db.models.deletion.SET_NULL, 113 | related_name="importjob_update", 114 | to=settings.AUTH_USER_MODEL, 115 | verbose_name="last updated by", 116 | ), 117 | ), 118 | ], 119 | ), 120 | ] 121 | -------------------------------------------------------------------------------- /example/project/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for winners project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.10.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.10/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = "_omc6hxq40u11no0uvi&g__lzj2n^4-dk#l#i+7+vgng!-bb^)" 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | "django.contrib.admin", 35 | "django.contrib.auth", 36 | "django.contrib.contenttypes", 37 | "django.contrib.sessions", 38 | "django.contrib.messages", 39 | "django.contrib.staticfiles", 40 | "winners", 41 | "import_export_celery", 42 | "import_export", 43 | ] 44 | 45 | MIDDLEWARE = [ 46 | "django.middleware.security.SecurityMiddleware", 47 | "django.contrib.sessions.middleware.SessionMiddleware", 48 | "django.middleware.common.CommonMiddleware", 49 | "django.middleware.csrf.CsrfViewMiddleware", 50 | "django.contrib.auth.middleware.AuthenticationMiddleware", 51 | "django.contrib.messages.middleware.MessageMiddleware", 52 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 53 | "author.middlewares.AuthorDefaultBackendMiddleware", 54 | ] 55 | 56 | ROOT_URLCONF = "winners.urls" 57 | 58 | TEMPLATES = [ 59 | { 60 | "BACKEND": "django.template.backends.django.DjangoTemplates", 61 | "DIRS": ["templates"], 62 | "APP_DIRS": True, 63 | "OPTIONS": { 64 | "context_processors": [ 65 | "django.template.context_processors.debug", 66 | "django.template.context_processors.request", 67 | "django.contrib.auth.context_processors.auth", 68 | "django.contrib.messages.context_processors.messages", 69 | ], 70 | "debug": DEBUG, 71 | }, 72 | }, 73 | ] 74 | 75 | WSGI_APPLICATION = "winners.wsgi.application" 76 | 77 | BROKER_URL = os.environ.get("REDIS_URL", "redis://redis") 78 | REDIS_URL = os.environ.get("REDIS_URL", "redis://redis") 79 | # Database 80 | # https://docs.djangoproject.com/en/1.10/ref/settings/#databases 81 | 82 | if os.environ.get("DATABASE_TYPE") == "sqlite": 83 | DATABASES = { 84 | "default": { 85 | "ENGINE": "django.db.backends.sqlite3", 86 | "NAME": os.environ.get("DATABASE_NAME", os.path.join(BASE_DIR, "db.sqlite3")), 87 | } 88 | } 89 | else: 90 | DATABASES = { 91 | "default": { 92 | "ENGINE": "django.db.backends.postgresql_psycopg2", 93 | "NAME": os.environ.get("DATABASE_NAME", "pguser"), 94 | "USER": os.environ.get("DATABASE_USER", "pguser"), 95 | "PASSWORD": os.environ.get("DATABASE_PASSWORD", "foobar"), 96 | "HOST": os.environ.get("DATABASE_HOST", "postgres"), 97 | "PORT": os.environ.get("DATABASE_PORT", ""), 98 | }, 99 | } 100 | 101 | # Password validation 102 | # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators 103 | 104 | AUTH_PASSWORD_VALIDATORS = [ 105 | { 106 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 107 | }, 108 | { 109 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 110 | }, 111 | { 112 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 113 | }, 114 | { 115 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 116 | }, 117 | ] 118 | 119 | 120 | # Internationalization 121 | # https://docs.djangoproject.com/en/1.10/topics/i18n/ 122 | 123 | LANGUAGE_CODE = "en-us" 124 | 125 | TIME_ZONE = "UTC" 126 | 127 | USE_I18N = True 128 | 129 | USE_L10N = True 130 | 131 | USE_TZ = True 132 | 133 | 134 | # Static files (CSS, JavaScript, Images) 135 | # https://docs.djangoproject.com/en/1.10/howto/static-files/ 136 | 137 | STATIC_URL = "/static/" 138 | MEDIA_URL = "/media/" 139 | MEDIA_ROOT = BASE_DIR 140 | 141 | IMPORT_EXPORT_CELERY_MODELS = { 142 | "Winner": {"app_label": "winners", "model_name": "Winner"} 143 | } 144 | 145 | EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" 146 | 147 | # Default import time limits (in seconds) 148 | IMPORT_EXPORT_CELERY_IMPORT_SOFT_TIME_LIMIT = 300 # 5 minutes 149 | IMPORT_EXPORT_CELERY_IMPORT_HARD_TIME_LIMIT = 360 # 6 minutes 150 | 151 | # Default export time limits (in seconds) 152 | IMPORT_EXPORT_CELERY_EXPORT_SOFT_TIME_LIMIT = 300 # 5 minutes 153 | IMPORT_EXPORT_CELERY_EXPORT_HARD_TIME_LIMIT = 360 # 6 minutes 154 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /import_export_celery/tasks.py: -------------------------------------------------------------------------------- 1 | # Author: Timothy Hobbs hobbs.cz> 2 | from django.utils import timezone 3 | import os 4 | 5 | from celery import shared_task 6 | 7 | from django.conf import settings 8 | from django.core.files.base import ContentFile 9 | from django.core.cache import cache 10 | 11 | from django.utils.encoding import force_str 12 | from django.utils.translation import gettext_lazy as _ 13 | 14 | from . import models 15 | from .model_config import ModelConfig 16 | from .utils import send_export_job_completion_mail, get_formats 17 | 18 | from celery.utils.log import get_task_logger 19 | import logging 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | log = get_task_logger(__name__) 24 | 25 | 26 | importables = getattr(settings, "IMPORT_EXPORT_CELERY_MODELS", {}) 27 | 28 | 29 | def change_job_status(job, direction, job_status, dry_run=False): 30 | if dry_run: 31 | job_status = "[Dry run] " + job_status 32 | else: 33 | job_status = job_status 34 | cache.set(direction + "_job_status_%s" % job.pk, job_status) 35 | job.job_status = job_status 36 | job.save() 37 | 38 | 39 | def get_format(job): 40 | for format in get_formats(): 41 | if job.format == format.CONTENT_TYPE: 42 | return format() 43 | break 44 | 45 | 46 | def _run_import_job(import_job, dry_run=True): 47 | change_job_status(import_job, "import", "1/5 Import started", dry_run) 48 | if dry_run: 49 | import_job.errors = "" 50 | model_config = ModelConfig(**importables[import_job.model]) 51 | import_format = get_format(import_job) 52 | # Copied from https://github.com/django-import-export/django-import-export/blob/3c082f98afe7996e79f936418fced3094f141c26/import_export/admin.py#L260 sorry # noqa 53 | try: 54 | data = import_job.file.read() 55 | if not import_format.is_binary(): 56 | data = force_str(data, "utf8") 57 | dataset = import_format.create_dataset(data) 58 | except UnicodeDecodeError as e: 59 | import_job.errors += ( 60 | _("Imported file has a wrong encoding: %s" % e) + "\n" 61 | ) 62 | change_job_status( 63 | import_job, "import", "Imported file has a wrong encoding", dry_run 64 | ) 65 | import_job.save() 66 | return 67 | except Exception as e: 68 | import_job.errors += _("Error reading file: %s") % e + "\n" 69 | change_job_status(import_job, "import", "Error reading file", dry_run) 70 | import_job.save() 71 | return 72 | change_job_status( 73 | import_job, "import", "2/5 Processing import data", dry_run 74 | ) 75 | 76 | class Resource(model_config.resource): 77 | def __init__(self, import_job, *args, **kwargs): 78 | self.import_job = import_job 79 | super().__init__(*args, **kwargs) 80 | 81 | def before_import_row(self, row, **kwargs): 82 | if "row_number" in kwargs: 83 | row_number = kwargs["row_number"] 84 | if row_number % 100 == 0 or row_number == 1: 85 | change_job_status( 86 | import_job, 87 | "import", 88 | f"3/5 Importing row {row_number}/{len(dataset)}", 89 | dry_run, 90 | ) 91 | return super().before_import_row(row, **kwargs) 92 | 93 | resource = Resource(import_job=import_job) 94 | 95 | skip_diff = resource._meta.skip_diff or resource._meta.skip_html_diff 96 | 97 | result = resource.import_data(dataset, dry_run=dry_run) 98 | change_job_status( 99 | import_job, "import", "4/5 Generating import summary", dry_run 100 | ) 101 | for error in result.base_errors: 102 | import_job.errors += f"\n{error.error}\n{error.traceback}\n" 103 | for line, errors in result.row_errors(): 104 | for error in errors: 105 | import_job.errors += _("Line: %s - %s\n\t%s\n%s") % ( 106 | line, 107 | error.error, 108 | ",".join(str(s) for s in error.row.values()), 109 | error.traceback, 110 | ) 111 | 112 | if dry_run: 113 | summary = "" 114 | summary += "" 115 | summary += '' 116 | summary += "" 117 | summary += "" 118 | summary += ( # TODO refactor the existing template so we can use it for this 119 | '' 120 | ) 121 | # https://github.com/django-import-export/django-import-export/blob/6575c3e1d89725701e918696fbc531aeb192a6f7/import_export/templates/admin/import_export/import.html 122 | if not result.invalid_rows and not skip_diff: 123 | cols = lambda row: "" 130 | ) 131 | summary += ( 132 | "" 140 | ) 141 | else: 142 | cols = lambda row: "" 164 | + "" 169 | ) 170 | summary += ( 171 | "" 183 | ) 184 | summary += "
".join([field for field in row.diff]) 124 | summary += ( 125 | "
change_type" 126 | + "".join( 127 | [f.column_name for f in resource.get_user_visible_fields()] 128 | ) 129 | + "
" 133 | + "
".join( 134 | [ 135 | row.import_type + "" + cols(row) 136 | for row in result.valid_rows() 137 | ] 138 | ) 139 | + "
".join( 143 | [str(field) for field in row.values] 144 | ) 145 | 146 | def cols_error(row): 147 | if hasattr(row.error, "message_dict"): 148 | return "".join( 149 | [ 150 | "" 151 | + key 152 | + "" 153 | + "
" 154 | + row.error.message_dict[key][0] 155 | + "
" 156 | for key in row.error.message_dict.keys() 157 | ] 158 | ) 159 | else: 160 | return "".join(message + "
" for message in row.error.messages) 161 | 162 | summary += ( 163 | "
rowerrors" 165 | + "".join( 166 | [f.column_name for f in resource.get_user_visible_fields()] 167 | ) 168 | + "
" 172 | + "
".join( 173 | [ 174 | str(row.number) 175 | + "" 176 | + cols_error(row) 177 | + "" 178 | + cols(row) 179 | for row in result.invalid_rows 180 | ] 181 | ) 182 | + "
" 185 | summary += "" 186 | summary += "" 187 | import_job.change_summary.save( 188 | os.path.split(import_job.file.name)[1] + ".html", 189 | ContentFile(summary.encode("utf-8")), 190 | ) 191 | else: 192 | import_job.imported = timezone.now() 193 | change_job_status(import_job, "import", "5/5 Import job finished", dry_run) 194 | import_job.save() 195 | 196 | 197 | @shared_task( 198 | bind=False, 199 | soft_time_limit=getattr(settings, "IMPORT_EXPORT_CELERY_IMPORT_SOFT_TIME_LIMIT", 0), 200 | time_limit=getattr(settings, "IMPORT_EXPORT_CELERY_IMPORT_HARD_TIME_LIMIT", 0), 201 | ) 202 | def run_import_job(pk, dry_run=True): 203 | log.info(f"Importing {pk} dry-run {dry_run}") 204 | import_job = models.ImportJob.objects.get(pk=pk) 205 | try: 206 | _run_import_job(import_job, dry_run) 207 | except Exception as e: 208 | import_job.errors += _("Import error %s") % e + "\n" 209 | change_job_status(import_job, "import", "Import error", dry_run) 210 | import_job.save() 211 | return 212 | 213 | 214 | @shared_task( 215 | bind=False, 216 | soft_time_limit=getattr(settings, "IMPORT_EXPORT_CELERY_EXPORT_SOFT_TIME_LIMIT", 0), 217 | time_limit=getattr(settings, "IMPORT_EXPORT_CELERY_EXPORT_HARD_TIME_LIMIT", 0), 218 | ) 219 | def run_export_job(pk): 220 | log.info("Exporting %s" % pk) 221 | export_job = models.ExportJob.objects.get(pk=pk) 222 | resource_class = export_job.get_resource_class() 223 | queryset = export_job.get_queryset() 224 | qs_len = len(queryset) 225 | 226 | class Resource(resource_class): 227 | def __init__(self, export_job, *args, **kwargs): 228 | self.row_number = 1 229 | self.export_job = export_job 230 | super().__init__(*args, **kwargs) 231 | 232 | def export_resource(self, *args, **kwargs): 233 | if self.row_number % 20 == 0 or self.row_number == 1: 234 | change_job_status( 235 | export_job, 236 | "export", 237 | f"Exporting row {self.row_number}/{qs_len}", 238 | ) 239 | self.row_number += 1 240 | return super().export_resource(*args, **kwargs) 241 | 242 | resource = Resource(export_job=export_job) 243 | 244 | data = resource.export(queryset) 245 | format = get_format(export_job) 246 | serialized = format.export_data(data) 247 | change_job_status(export_job, "export", "Export complete") 248 | filename = "{app}-{model}-{date}.{extension}".format( 249 | app=export_job.app_label, 250 | model=export_job.model, 251 | date=str(timezone.now()), 252 | extension=format.get_extension(), 253 | ) 254 | if not format.is_binary(): 255 | serialized = serialized.encode("utf8") 256 | export_job.file.save(filename, ContentFile(serialized)) 257 | if export_job.email_on_completion: 258 | send_export_job_completion_mail(export_job) 259 | return 260 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://img.shields.io/pypi/v/django-import-export-celery.svg 2 | :target: https://pypi.org/project/django-import-export-celery/#history 3 | 4 | django-import-export-celery: process slow django imports and exports in celery 5 | ============================================================================== 6 | 7 | django-import-export-celery helps you process long running imports and exports in celery. 8 | 9 | Basic installation 10 | ------------------ 11 | 12 | 1. `Set up celery `__ to work with your project. 13 | 14 | 2. Add ``'import_export_celery'`` to your ``INSTALLED_APPS`` settings variable 15 | 16 | 3. Add ``'author.middlewares.AuthorDefaultBackendMiddleware'`` to your ``MIDDLEWARE_CLASSES`` 17 | 18 | 4. Configure the location of your celery module setup 19 | 20 | :: 21 | 22 | IMPORT_EXPORT_CELERY_INIT_MODULE = "projectname.celery" 23 | 24 | 25 | Setting up imports with celery 26 | ------------------------------ 27 | 28 | A fully configured example project can be found in the example directory of this repository. 29 | 30 | 1. Perform the basic setup procedure described above. 31 | 32 | 2. Configure the IMPORT_EXPORT_CELERY_MODELS variable. 33 | 34 | :: 35 | 36 | def resource(): # Optional 37 | from myapp.models import WinnerResource 38 | return WinnerResource 39 | 40 | 41 | IMPORT_EXPORT_CELERY_MODELS = { 42 | "Winner": { 43 | 'app_label': 'winners', 44 | 'model_name': 'Winner', 45 | 'resource': resource, # Optional 46 | } 47 | } 48 | 49 | The available parameters are `app_label`, `model_name`, and `resource`. 'resource' should be a function which returns a django-import-export `Resource `__. 50 | 51 | 3. Done 52 | 53 | 54 | By default a dry run of the import is initiated when the import object is created. To instead import the file immediately without a dry-run set the `IMPORT_DRY_RUN_FIRST_TIME` to `False` 55 | 56 | :: 57 | 58 | IMPORT_DRY_RUN_FIRST_TIME = False 59 | 60 | 61 | Performing an import 62 | -------------------- 63 | 64 | You will find an example django application that uses django-import-export-celery for importing data. There are instructions for running the example application in the example directory's README file. Once you have it running, you can perform an import with the following steps. 65 | 66 | 1. Navigate to the example applications admin page: 67 | 68 | .. image:: screenshots/admin.png 69 | 70 | 2. Navigate to the ImportJobs table: 71 | 72 | .. image:: screenshots/import_jobs.png 73 | 74 | 3. Create a new import job. There is an example import CSV file in the example/example-data directory. Select that file. Select csv as the file format. We'll be importing to the Winner's model table. 75 | 76 | .. image:: screenshots/new_import_job.png 77 | 78 | 4. Select "Save and continue editing" to save the import job and refresh until you see that a "Summary of changes made by this import" file has been created. 79 | 80 | .. image:: screenshots/summary.png 81 | 82 | 5. You can view the summary if you want. Your import has NOT BEEN PERFORMED YET! 83 | 84 | .. image:: screenshots/view-summary.png 85 | 86 | 6. Return to the import-jobs table, select the import job we just created, and select the "Perform import" action from the actions drop down. 87 | 88 | .. image:: screenshots/perform-import.png 89 | 90 | 7. In a short time, your imported Winner object should show up in your Winners table. 91 | 92 | .. image:: screenshots/new-winner.png 93 | 94 | 95 | Setting up exports 96 | ------------------ 97 | 98 | As with imports, a fully configured example project can be found in the `example` directory. 99 | 100 | 1. Add a `export_resource_classes` classmethod to the model you want to export. 101 | :: 102 | 103 | @classmethod 104 | def export_resource_classes(cls): 105 | return { 106 | 'winners': ('Winners resource', WinnersResource), 107 | 'winners_all_caps': ('Winners with all caps column resource', WinnersWithAllCapsResource), 108 | } 109 | 110 | This should return a dictionary of tuples. The keys should be unique unchanging strings, the tuples should consist of a `resource `__ and a human friendly description of that resource. 111 | 112 | 2. Add the `create_export_job_action` to the model's `ModelAdmin`. 113 | :: 114 | 115 | from django.contrib import admin 116 | from import_export_celery.admin_actions import create_export_job_action 117 | 118 | from . import models 119 | 120 | 121 | @admin.register(models.Winner) 122 | class WinnerAdmin(admin.ModelAdmin): 123 | list_display = ( 124 | 'name', 125 | ) 126 | 127 | actions = ( 128 | create_export_job_action, 129 | ) 130 | 131 | 3. To customise export queryset you need to add `get_export_queryset` to the `ModelResource`. 132 | :: 133 | 134 | class WinnersResource(ModelResource): 135 | class Meta: 136 | model = Winner 137 | 138 | def get_export_queryset(self): 139 | """To customise the queryset of the model resource with annotation override""" 140 | return self.Meta.model.objects.annotate(device_type=Subquery(FCMDevice.objects.filter( 141 | user=OuterRef("pk")).values("type")[:1]) 142 | 4. Done! 143 | 144 | 145 | Performing exports with celery 146 | ------------------------------ 147 | 148 | 1. Perform the basic setup procedure described in the first section. 149 | 150 | 2. Open up the object list for your model in django admin, select the objects you wish to export, and select the `Export with celery` admin action. 151 | 152 | 3. Select the file format and resource you want to use to export the data. 153 | 154 | 4. Save the model 155 | 156 | 5. You will receive an email when the export is done, click on the link in the email 157 | 158 | 6. Click on the link near the bottom of the page titled `Exported file`. 159 | 160 | 161 | Excluding export file formats in the admin site 162 | ----------------------------------------------- 163 | 164 | All available file formats to export are taken from the `Tablib project `__. 165 | 166 | To exclude or disable file formats from the admin site, configure `IMPORT_EXPORT_CELERY_EXCLUDED_FORMATS` django settings variable. This variable is a list of format strings written in lower case. 167 | 168 | :: 169 | 170 | IMPORT_EXPORT_CELERY_EXCLUDED_FORMATS = ["csv", "xls"] 171 | 172 | 173 | Customizing File Storage Backend 174 | -------------------------------- 175 | 176 | **If you are using the new Django 4.2 STORAGES**: 177 | 178 | By default, `import_export_celery` uses Django `default` storage. 179 | To use your own storage, use the the `IMPORT_EXPORT_CELERY_STORAGE_ALIAS` variable in your Django settings and adding the STORAGES definition. 180 | For instance: 181 | 182 | :: 183 | 184 | STORAGES = { 185 | "import_export_celery": { 186 | "BACKEND": "storages.backends.s3boto3.S3Boto3Storage", 187 | }, 188 | } 189 | IMPORT_EXPORT_CELERY_STORAGE_ALIAS = 'import_export_celery' 190 | 191 | **DEPRECATED: If you are using old style storages**: 192 | 193 | Define a custom storage backend by adding the `IMPORT_EXPORT_CELERY_STORAGE` to your Django settings. For instance: 194 | 195 | :: 196 | 197 | IMPORT_EXPORT_CELERY_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" 198 | 199 | 200 | Customizing Task Time Limits 201 | ---------------------------- 202 | 203 | By default, there is no time limit on celery import/export tasks. This can be customized by setting the following variables in your Django settings file. 204 | 205 | :: 206 | 207 | # set import time limits (in seconds) 208 | IMPORT_EXPORT_CELERY_IMPORT_SOFT_TIME_LIMIT = 300 # 5 minutes 209 | IMPORT_EXPORT_CELERY_IMPORT_HARD_TIME_LIMIT = 360 # 6 minutes 210 | 211 | # set export time limits (in seconds) 212 | IMPORT_EXPORT_CELERY_EXPORT_SOFT_TIME_LIMIT = 300 # 5 minutes 213 | IMPORT_EXPORT_CELERY_EXPORT_HARD_TIME_LIMIT = 360 # 6 minutes 214 | 215 | Customizing email settings for export job completion 216 | ---------------------------------------------------------- 217 | 218 | By default the export job completion email uses the following settings 219 | 220 | 221 | :: 222 | 223 | Subject: 'Django: Export job completed' 224 | Email template: 'email/export_job_completion.html' 225 | Email on completion: True 226 | 227 | 228 | The default email template can be found `here `__ 229 | 230 | The default email subject, template and sending behavior can be customized by overriding these values from django settings:- 231 | 232 | 233 | :: 234 | 235 | EXPORT_JOB_COMPLETION_MAIL_SUBJECT="Your custom subject" 236 | EXPORT_JOB_COMPLETION_MAIL_TEMPLATE="path_to_folder/your_custom_template.html" 237 | EXPORT_JOB_EMAIL_ON_COMPLETION = True # Set to False to disable email 238 | 239 | 240 | The email template will get some context variables that you can use to customize your template. 241 | 242 | 243 | :: 244 | 245 | { 246 | export_job: The current instance of ExportJob model 247 | app_label: export_job.app_label 248 | model: export_job.model 249 | link: A link to go to the export_job instance on django admin 250 | } 251 | 252 | 253 | For developers of this library 254 | ------------------------------ 255 | 256 | You can enter a preconfigured dev environment by first running `make` and then launching `./develop.sh` to get into a docker compose environment packed with **redis**, **celery**, **postgres** and everything you need to run and test django-import-export-celery. 257 | 258 | Before submitting a PR please run `flake8` and (in the examples directory) `python3 manange.py test`. 259 | 260 | Please note, that you need to restart celery for changes to propogate to the workers. Do this with `docker-compose down celery`, `docker-compose up celery`. 261 | 262 | Commercial support 263 | ------------------ 264 | 265 | Commercial support is provided by `gradesta s.r.o `_. 266 | 267 | Credits 268 | ------- 269 | 270 | `django-import-export-celery` was developed by the Czech non-profit `auto*mat z.s. `_. 271 | -------------------------------------------------------------------------------- /example/poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "amqp" 5 | version = "5.0.6" 6 | description = "Low-level AMQP client for Python (fork of amqplib)." 7 | optional = false 8 | python-versions = ">=3.6" 9 | files = [ 10 | {file = "amqp-5.0.6-py3-none-any.whl", hash = "sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb"}, 11 | {file = "amqp-5.0.6.tar.gz", hash = "sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2"}, 12 | ] 13 | 14 | [package.dependencies] 15 | vine = "5.0.0" 16 | 17 | [[package]] 18 | name = "asgiref" 19 | version = "3.4.1" 20 | description = "ASGI specs, helper code, and adapters" 21 | optional = false 22 | python-versions = ">=3.6" 23 | files = [ 24 | {file = "asgiref-3.4.1-py3-none-any.whl", hash = "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"}, 25 | {file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"}, 26 | ] 27 | 28 | [package.dependencies] 29 | typing-extensions = {version = "*", markers = "python_version < \"3.8\""} 30 | 31 | [package.extras] 32 | tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] 33 | 34 | [[package]] 35 | name = "billiard" 36 | version = "3.6.4.0" 37 | description = "Python multiprocessing fork with improvements and bugfixes" 38 | optional = false 39 | python-versions = "*" 40 | files = [ 41 | {file = "billiard-3.6.4.0-py3-none-any.whl", hash = "sha256:87103ea78fa6ab4d5c751c4909bcff74617d985de7fa8b672cf8618afd5a875b"}, 42 | {file = "billiard-3.6.4.0.tar.gz", hash = "sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547"}, 43 | ] 44 | 45 | [[package]] 46 | name = "black" 47 | version = "22.3.0" 48 | description = "The uncompromising code formatter." 49 | optional = false 50 | python-versions = ">=3.6.2" 51 | files = [ 52 | {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, 53 | {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, 54 | {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, 55 | {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, 56 | {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, 57 | {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, 58 | {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, 59 | {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, 60 | {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, 61 | {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, 62 | {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, 63 | {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, 64 | {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, 65 | {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, 66 | {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, 67 | {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, 68 | {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, 69 | {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, 70 | {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, 71 | {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, 72 | {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, 73 | {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, 74 | {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, 75 | ] 76 | 77 | [package.dependencies] 78 | click = ">=8.0.0" 79 | mypy-extensions = ">=0.4.3" 80 | pathspec = ">=0.9.0" 81 | platformdirs = ">=2" 82 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 83 | typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} 84 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} 85 | 86 | [package.extras] 87 | colorama = ["colorama (>=0.4.3)"] 88 | d = ["aiohttp (>=3.7.4)"] 89 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 90 | uvloop = ["uvloop (>=0.15.2)"] 91 | 92 | [[package]] 93 | name = "cached-property" 94 | version = "1.5.2" 95 | description = "A decorator for caching properties in classes." 96 | optional = false 97 | python-versions = "*" 98 | files = [ 99 | {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, 100 | {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, 101 | ] 102 | 103 | [[package]] 104 | name = "celery" 105 | version = "5.0.2" 106 | description = "Distributed Task Queue." 107 | optional = false 108 | python-versions = ">=3.6," 109 | files = [ 110 | {file = "celery-5.0.2-py3-none-any.whl", hash = "sha256:930c3acd55349d028c4e7104a7d377729cbcca19d9fce470c17172d9e7f9a8b6"}, 111 | {file = "celery-5.0.2.tar.gz", hash = "sha256:012c814967fe89e3f5d2cf49df2dba3de5f29253a7f4f2270e8fce6b901b4ebf"}, 112 | ] 113 | 114 | [package.dependencies] 115 | billiard = ">=3.6.3.0,<4.0" 116 | click = ">=7.0" 117 | click-didyoumean = ">=0.0.3" 118 | click-repl = ">=0.1.6" 119 | kombu = ">=5.0.0,<6.0" 120 | pytz = ">0.0-dev" 121 | vine = ">=5.0.0,<6.0" 122 | 123 | [package.extras] 124 | arangodb = ["pyArango (>=1.3.2)"] 125 | auth = ["cryptography"] 126 | azureblockblob = ["azure-common (==1.1.5)", "azure-storage (==0.36.0)", "azure-storage-common (==1.1.0)"] 127 | brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] 128 | cassandra = ["cassandra-driver (<3.21.0)"] 129 | consul = ["python-consul"] 130 | cosmosdbsql = ["pydocumentdb (==2.3.2)"] 131 | couchbase = ["couchbase (>=3.0.0)"] 132 | couchdb = ["pycouchdb"] 133 | django = ["Django (>=1.11)"] 134 | dynamodb = ["boto3 (>=1.9.178)"] 135 | elasticsearch = ["elasticsearch"] 136 | eventlet = ["eventlet (>=0.26.1)"] 137 | gevent = ["gevent (>=1.0.0)"] 138 | librabbitmq = ["librabbitmq (>=1.5.0)"] 139 | lzma = ["backports.lzma"] 140 | memcache = ["pylibmc"] 141 | mongodb = ["pymongo[srv] (>=3.3.0)"] 142 | msgpack = ["msgpack"] 143 | pymemcache = ["python-memcached"] 144 | pyro = ["pyro4"] 145 | redis = ["redis (>=3.2.0)"] 146 | s3 = ["boto3 (>=1.9.125)"] 147 | slmq = ["softlayer-messaging (>=1.0.3)"] 148 | solar = ["ephem"] 149 | sqlalchemy = ["sqlalchemy"] 150 | sqs = ["boto3 (>=1.9.125)", "pycurl (==7.43.0.5)"] 151 | tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] 152 | yaml = ["PyYAML (>=3.10)"] 153 | zookeeper = ["kazoo (>=1.3.1)"] 154 | zstd = ["zstandard"] 155 | 156 | [[package]] 157 | name = "click" 158 | version = "8.0.1" 159 | description = "Composable command line interface toolkit" 160 | optional = false 161 | python-versions = ">=3.6" 162 | files = [ 163 | {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, 164 | {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, 165 | ] 166 | 167 | [package.dependencies] 168 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 169 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 170 | 171 | [[package]] 172 | name = "click-didyoumean" 173 | version = "0.0.3" 174 | description = "Enable git-like did-you-mean feature in click." 175 | optional = false 176 | python-versions = "*" 177 | files = [ 178 | {file = "click-didyoumean-0.0.3.tar.gz", hash = "sha256:112229485c9704ff51362fe34b2d4f0b12fc71cc20f6d2b3afabed4b8bfa6aeb"}, 179 | ] 180 | 181 | [package.dependencies] 182 | click = "*" 183 | 184 | [[package]] 185 | name = "click-repl" 186 | version = "0.2.0" 187 | description = "REPL plugin for Click" 188 | optional = false 189 | python-versions = "*" 190 | files = [ 191 | {file = "click-repl-0.2.0.tar.gz", hash = "sha256:cd12f68d745bf6151210790540b4cb064c7b13e571bc64b6957d98d120dacfd8"}, 192 | {file = "click_repl-0.2.0-py3-none-any.whl", hash = "sha256:94b3fbbc9406a236f176e0506524b2937e4b23b6f4c0c0b2a0a83f8a64e9194b"}, 193 | ] 194 | 195 | [package.dependencies] 196 | click = "*" 197 | prompt-toolkit = "*" 198 | six = "*" 199 | 200 | [[package]] 201 | name = "colorama" 202 | version = "0.4.4" 203 | description = "Cross-platform colored terminal text." 204 | optional = false 205 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 206 | files = [ 207 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 208 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 209 | ] 210 | 211 | [[package]] 212 | name = "defusedxml" 213 | version = "0.7.1" 214 | description = "XML bomb protection for Python stdlib modules" 215 | optional = false 216 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 217 | files = [ 218 | {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, 219 | {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, 220 | ] 221 | 222 | [[package]] 223 | name = "diff-match-patch" 224 | version = "20200713" 225 | description = "Repackaging of Google's Diff Match and Patch libraries. Offers robust algorithms to perform the operations required for synchronizing plain text." 226 | optional = false 227 | python-versions = ">=2.7" 228 | files = [ 229 | {file = "diff-match-patch-20200713.tar.gz", hash = "sha256:da6f5a01aa586df23dfc89f3827e1cafbb5420be9d87769eeb079ddfd9477a18"}, 230 | {file = "diff_match_patch-20200713-py3-none-any.whl", hash = "sha256:8bf9d9c4e059d917b5c6312bac0c137971a32815ddbda9c682b949f2986b4d34"}, 231 | ] 232 | 233 | [[package]] 234 | name = "django" 235 | version = "3.2.25" 236 | description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." 237 | optional = false 238 | python-versions = ">=3.6" 239 | files = [ 240 | {file = "Django-3.2.25-py3-none-any.whl", hash = "sha256:a52ea7fcf280b16f7b739cec38fa6d3f8953a5456986944c3ca97e79882b4e38"}, 241 | {file = "Django-3.2.25.tar.gz", hash = "sha256:7ca38a78654aee72378594d63e51636c04b8e28574f5505dff630895b5472777"}, 242 | ] 243 | 244 | [package.dependencies] 245 | asgiref = ">=3.3.2,<4" 246 | pytz = "*" 247 | sqlparse = ">=0.2.2" 248 | 249 | [package.extras] 250 | argon2 = ["argon2-cffi (>=19.1.0)"] 251 | bcrypt = ["bcrypt"] 252 | 253 | [[package]] 254 | name = "django-admin-smoke-tests" 255 | version = "0.3.0" 256 | description = "Runs some quick tests on your admin site objects to make sure there aren't non-existant fields listed, etc." 257 | optional = false 258 | python-versions = "*" 259 | files = [ 260 | {file = "django-admin-smoke-tests-0.3.0.tar.gz", hash = "sha256:5dc35c61610d4e762bc21a6d2ba4599d67491ff5260e576e0454c14b341a2be9"}, 261 | ] 262 | 263 | [package.dependencies] 264 | django = ">=1.6" 265 | six = "*" 266 | 267 | [[package]] 268 | name = "django-author" 269 | version = "1.0.2" 270 | description = "Add special User ForeignKey fields which update automatically" 271 | optional = false 272 | python-versions = "*" 273 | files = [ 274 | {file = "django-author-1.0.2.tar.gz", hash = "sha256:0238b6280f66a8ba6d1c730ab4acc52bc2bf37686940fd4db42e7af458c96635"}, 275 | ] 276 | 277 | [package.dependencies] 278 | setuptools-git = "*" 279 | 280 | [[package]] 281 | name = "django-import-export" 282 | version = "2.5.0" 283 | description = "Django application and library for importing and exporting data with included admin integration." 284 | optional = false 285 | python-versions = ">=3.5" 286 | files = [ 287 | {file = "django-import-export-2.5.0.tar.gz", hash = "sha256:c39c003bfc803fb63ba7742562f1667603a4a8d7426261845d75ce8582d40f48"}, 288 | {file = "django_import_export-2.5.0-py3-none-any.whl", hash = "sha256:cf6f3dabdd4f32dcb26e25c7ddcba7aee3168b55d380b0da79f0349afa17c011"}, 289 | ] 290 | 291 | [package.dependencies] 292 | diff-match-patch = "*" 293 | Django = ">=2.0" 294 | tablib = {version = ">=0.14.0", extras = ["html", "ods", "xls", "xlsx", "yaml"]} 295 | 296 | [[package]] 297 | name = "et-xmlfile" 298 | version = "1.1.0" 299 | description = "An implementation of lxml.xmlfile for the standard library" 300 | optional = false 301 | python-versions = ">=3.6" 302 | files = [ 303 | {file = "et_xmlfile-1.1.0-py3-none-any.whl", hash = "sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada"}, 304 | {file = "et_xmlfile-1.1.0.tar.gz", hash = "sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c"}, 305 | ] 306 | 307 | [[package]] 308 | name = "html2text" 309 | version = "2020.1.16" 310 | description = "Turn HTML into equivalent Markdown-structured text." 311 | optional = false 312 | python-versions = ">=3.5" 313 | files = [ 314 | {file = "html2text-2020.1.16-py3-none-any.whl", hash = "sha256:c7c629882da0cf377d66f073329ccf34a12ed2adf0169b9285ae4e63ef54c82b"}, 315 | {file = "html2text-2020.1.16.tar.gz", hash = "sha256:e296318e16b059ddb97f7a8a1d6a5c1d7af4544049a01e261731d2d5cc277bbb"}, 316 | ] 317 | 318 | [[package]] 319 | name = "importlib-metadata" 320 | version = "4.8.1" 321 | description = "Read metadata from Python packages" 322 | optional = false 323 | python-versions = ">=3.6" 324 | files = [ 325 | {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, 326 | {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, 327 | ] 328 | 329 | [package.dependencies] 330 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 331 | zipp = ">=0.5" 332 | 333 | [package.extras] 334 | docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] 335 | perf = ["ipython"] 336 | testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", "pyfakefs", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy", "pytest-perf (>=0.9.2)"] 337 | 338 | [[package]] 339 | name = "kombu" 340 | version = "5.1.0" 341 | description = "Messaging library for Python." 342 | optional = false 343 | python-versions = ">=3.6" 344 | files = [ 345 | {file = "kombu-5.1.0-py3-none-any.whl", hash = "sha256:e2dedd8a86c9077c350555153825a31e456a0dc20c15d5751f00137ec9c75f0a"}, 346 | {file = "kombu-5.1.0.tar.gz", hash = "sha256:01481d99f4606f6939cdc9b637264ed353ee9e3e4f62cfb582324142c41a572d"}, 347 | ] 348 | 349 | [package.dependencies] 350 | amqp = ">=5.0.6,<6.0.0" 351 | cached-property = {version = "*", markers = "python_version < \"3.8\""} 352 | importlib-metadata = {version = ">=0.18", markers = "python_version < \"3.8\""} 353 | vine = "*" 354 | 355 | [package.extras] 356 | azureservicebus = ["azure-servicebus (>=7.0.0)"] 357 | azurestoragequeues = ["azure-storage-queue"] 358 | consul = ["python-consul (>=0.6.0)"] 359 | librabbitmq = ["librabbitmq (>=1.5.2)"] 360 | mongodb = ["pymongo (>=3.3.0)"] 361 | msgpack = ["msgpack"] 362 | pyro = ["pyro4"] 363 | qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] 364 | redis = ["redis (>=3.3.11)"] 365 | slmq = ["softlayer-messaging (>=1.0.3)"] 366 | sqlalchemy = ["sqlalchemy"] 367 | sqs = ["boto3 (>=1.4.4)", "pycurl (==7.43.0.2)", "urllib3 (<1.26)"] 368 | yaml = ["PyYAML (>=3.10)"] 369 | zookeeper = ["kazoo (>=1.3.1)"] 370 | 371 | [[package]] 372 | name = "markuppy" 373 | version = "1.14" 374 | description = "An HTML/XML generator" 375 | optional = false 376 | python-versions = "*" 377 | files = [ 378 | {file = "MarkupPy-1.14.tar.gz", hash = "sha256:1adee2c0a542af378fe84548ff6f6b0168f3cb7f426b46961038a2bcfaad0d5f"}, 379 | ] 380 | 381 | [[package]] 382 | name = "mypy-extensions" 383 | version = "0.4.3" 384 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 385 | optional = false 386 | python-versions = "*" 387 | files = [ 388 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 389 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 390 | ] 391 | 392 | [[package]] 393 | name = "odfpy" 394 | version = "1.4.1" 395 | description = "Python API and tools to manipulate OpenDocument files" 396 | optional = false 397 | python-versions = "*" 398 | files = [ 399 | {file = "odfpy-1.4.1.tar.gz", hash = "sha256:db766a6e59c5103212f3cc92ec8dd50a0f3a02790233ed0b52148b70d3c438ec"}, 400 | ] 401 | 402 | [package.dependencies] 403 | defusedxml = "*" 404 | 405 | [[package]] 406 | name = "openpyxl" 407 | version = "3.0.7" 408 | description = "A Python library to read/write Excel 2010 xlsx/xlsm files" 409 | optional = false 410 | python-versions = ">=3.6," 411 | files = [ 412 | {file = "openpyxl-3.0.7-py2.py3-none-any.whl", hash = "sha256:46af4eaf201a89b610fcca177eed957635f88770a5462fb6aae4a2a52b0ff516"}, 413 | {file = "openpyxl-3.0.7.tar.gz", hash = "sha256:6456a3b472e1ef0facb1129f3c6ef00713cebf62e736cd7a75bcc3247432f251"}, 414 | ] 415 | 416 | [package.dependencies] 417 | et-xmlfile = "*" 418 | 419 | [[package]] 420 | name = "pathspec" 421 | version = "0.9.0" 422 | description = "Utility library for gitignore style pattern matching of file paths." 423 | optional = false 424 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 425 | files = [ 426 | {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, 427 | {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, 428 | ] 429 | 430 | [[package]] 431 | name = "platformdirs" 432 | version = "2.5.1" 433 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 434 | optional = false 435 | python-versions = ">=3.7" 436 | files = [ 437 | {file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"}, 438 | {file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"}, 439 | ] 440 | 441 | [package.extras] 442 | docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] 443 | test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] 444 | 445 | [[package]] 446 | name = "prompt-toolkit" 447 | version = "3.0.20" 448 | description = "Library for building powerful interactive command lines in Python" 449 | optional = false 450 | python-versions = ">=3.6.2" 451 | files = [ 452 | {file = "prompt_toolkit-3.0.20-py3-none-any.whl", hash = "sha256:6076e46efae19b1e0ca1ec003ed37a933dc94b4d20f486235d436e64771dcd5c"}, 453 | {file = "prompt_toolkit-3.0.20.tar.gz", hash = "sha256:eb71d5a6b72ce6db177af4a7d4d7085b99756bf656d98ffcc4fecd36850eea6c"}, 454 | ] 455 | 456 | [package.dependencies] 457 | wcwidth = "*" 458 | 459 | [[package]] 460 | name = "psycopg2-binary" 461 | version = "2.9.1" 462 | description = "psycopg2 - Python-PostgreSQL Database Adapter" 463 | optional = false 464 | python-versions = ">=3.6" 465 | files = [ 466 | {file = "psycopg2-binary-2.9.1.tar.gz", hash = "sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773"}, 467 | {file = "psycopg2_binary-2.9.1-cp310-cp310-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:24b0b6688b9f31a911f2361fe818492650795c9e5d3a1bc647acbd7440142a4f"}, 468 | {file = "psycopg2_binary-2.9.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:542875f62bc56e91c6eac05a0deadeae20e1730be4c6334d8f04c944fcd99759"}, 469 | {file = "psycopg2_binary-2.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:661509f51531ec125e52357a489ea3806640d0ca37d9dada461ffc69ee1e7b6e"}, 470 | {file = "psycopg2_binary-2.9.1-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:d92272c7c16e105788efe2cfa5d680f07e34e0c29b03c1908f8636f55d5f915a"}, 471 | {file = "psycopg2_binary-2.9.1-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:736b8797b58febabb85494142c627bd182b50d2a7ec65322983e71065ad3034c"}, 472 | {file = "psycopg2_binary-2.9.1-cp310-cp310-win32.whl", hash = "sha256:ebccf1123e7ef66efc615a68295bf6fdba875a75d5bba10a05073202598085fc"}, 473 | {file = "psycopg2_binary-2.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:1f6ca4a9068f5c5c57e744b4baa79f40e83e3746875cac3c45467b16326bab45"}, 474 | {file = "psycopg2_binary-2.9.1-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76"}, 475 | {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aef9aee84ec78af51107181d02fe8773b100b01c5dfde351184ad9223eab3698"}, 476 | {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123c3fb684e9abfc47218d3784c7b4c47c8587951ea4dd5bc38b6636ac57f616"}, 477 | {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:995fc41ebda5a7a663a254a1dcac52638c3e847f48307b5416ee373da15075d7"}, 478 | {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:fbb42a541b1093385a2d8c7eec94d26d30437d0e77c1d25dae1dcc46741a385e"}, 479 | {file = "psycopg2_binary-2.9.1-cp36-cp36m-win32.whl", hash = "sha256:20f1ab44d8c352074e2d7ca67dc00843067788791be373e67a0911998787ce7d"}, 480 | {file = "psycopg2_binary-2.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f6fac64a38f6768e7bc7b035b9e10d8a538a9fadce06b983fb3e6fa55ac5f5ce"}, 481 | {file = "psycopg2_binary-2.9.1-cp37-cp37m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:1e3a362790edc0a365385b1ac4cc0acc429a0c0d662d829a50b6ce743ae61b5a"}, 482 | {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f8559617b1fcf59a9aedba2c9838b5b6aa211ffedecabca412b92a1ff75aac1a"}, 483 | {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a36c7eb6152ba5467fb264d73844877be8b0847874d4822b7cf2d3c0cb8cdcb0"}, 484 | {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:2f62c207d1740b0bde5c4e949f857b044818f734a3d57f1d0d0edc65050532ed"}, 485 | {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:cfc523edecddaef56f6740d7de1ce24a2fdf94fd5e704091856a201872e37f9f"}, 486 | {file = "psycopg2_binary-2.9.1-cp37-cp37m-win32.whl", hash = "sha256:1e85b74cbbb3056e3656f1cc4781294df03383127a8114cbc6531e8b8367bf1e"}, 487 | {file = "psycopg2_binary-2.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1473c0215b0613dd938db54a653f68251a45a78b05f6fc21af4326f40e8360a2"}, 488 | {file = "psycopg2_binary-2.9.1-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:35c4310f8febe41f442d3c65066ca93cccefd75013df3d8c736c5b93ec288140"}, 489 | {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c13d72ed6af7fd2c8acbd95661cf9477f94e381fce0792c04981a8283b52917"}, 490 | {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14db1752acdd2187d99cb2ca0a1a6dfe57fc65c3281e0f20e597aac8d2a5bd90"}, 491 | {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:aed4a9a7e3221b3e252c39d0bf794c438dc5453bc2963e8befe9d4cd324dff72"}, 492 | {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:da113b70f6ec40e7d81b43d1b139b9db6a05727ab8be1ee559f3a69854a69d34"}, 493 | {file = "psycopg2_binary-2.9.1-cp38-cp38-win32.whl", hash = "sha256:4235f9d5ddcab0b8dbd723dca56ea2922b485ea00e1dafacf33b0c7e840b3d32"}, 494 | {file = "psycopg2_binary-2.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:988b47ac70d204aed01589ed342303da7c4d84b56c2f4c4b8b00deda123372bf"}, 495 | {file = "psycopg2_binary-2.9.1-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:7360647ea04db2e7dff1648d1da825c8cf68dc5fbd80b8fb5b3ee9f068dcd21a"}, 496 | {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca86db5b561b894f9e5f115d6a159fff2a2570a652e07889d8a383b5fae66eb4"}, 497 | {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ced67f1e34e1a450cdb48eb53ca73b60aa0af21c46b9b35ac3e581cf9f00e31"}, 498 | {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:0f2e04bd2a2ab54fa44ee67fe2d002bb90cee1c0f1cc0ebc3148af7b02034cbd"}, 499 | {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:3242b9619de955ab44581a03a64bdd7d5e470cc4183e8fcadd85ab9d3756ce7a"}, 500 | {file = "psycopg2_binary-2.9.1-cp39-cp39-win32.whl", hash = "sha256:0b7dae87f0b729922e06f85f667de7bf16455d411971b2043bbd9577af9d1975"}, 501 | {file = "psycopg2_binary-2.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:b4d7679a08fea64573c969f6994a2631908bb2c0e69a7235648642f3d2e39a68"}, 502 | ] 503 | 504 | [[package]] 505 | name = "pudb" 506 | version = "2019.2" 507 | description = "A full-screen, console-based Python debugger" 508 | optional = false 509 | python-versions = "*" 510 | files = [ 511 | {file = "pudb-2019.2.tar.gz", hash = "sha256:e8f0ea01b134d802872184b05bffc82af29a1eb2f9374a277434b932d68f58dc"}, 512 | ] 513 | 514 | [package.dependencies] 515 | pygments = ">=1.0" 516 | urwid = ">=1.1.1" 517 | 518 | [[package]] 519 | name = "pygments" 520 | version = "2.15.0" 521 | description = "Pygments is a syntax highlighting package written in Python." 522 | optional = false 523 | python-versions = ">=3.7" 524 | files = [ 525 | {file = "Pygments-2.15.0-py3-none-any.whl", hash = "sha256:77a3299119af881904cd5ecd1ac6a66214b6e9bed1f2db16993b54adede64094"}, 526 | {file = "Pygments-2.15.0.tar.gz", hash = "sha256:f7e36cffc4c517fbc252861b9a6e4644ca0e5abadf9a113c72d1358ad09b9500"}, 527 | ] 528 | 529 | [package.extras] 530 | plugins = ["importlib-metadata"] 531 | 532 | [[package]] 533 | name = "pytz" 534 | version = "2021.1" 535 | description = "World timezone definitions, modern and historical" 536 | optional = false 537 | python-versions = "*" 538 | files = [ 539 | {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, 540 | {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, 541 | ] 542 | 543 | [[package]] 544 | name = "pyyaml" 545 | version = "5.4.1" 546 | description = "YAML parser and emitter for Python" 547 | optional = false 548 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 549 | files = [ 550 | {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, 551 | {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, 552 | {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, 553 | {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, 554 | {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, 555 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, 556 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, 557 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, 558 | {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, 559 | {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, 560 | {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, 561 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, 562 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, 563 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, 564 | {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, 565 | {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, 566 | {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, 567 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, 568 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, 569 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, 570 | {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, 571 | {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, 572 | {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, 573 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, 574 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, 575 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, 576 | {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, 577 | {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, 578 | {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, 579 | ] 580 | 581 | [[package]] 582 | name = "redis" 583 | version = "3.5.3" 584 | description = "Python client for Redis key-value store" 585 | optional = false 586 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 587 | files = [ 588 | {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"}, 589 | {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, 590 | ] 591 | 592 | [package.extras] 593 | hiredis = ["hiredis (>=0.1.3)"] 594 | 595 | [[package]] 596 | name = "setuptools-git" 597 | version = "1.2" 598 | description = "Setuptools revision control system plugin for Git" 599 | optional = false 600 | python-versions = "*" 601 | files = [ 602 | {file = "setuptools-git-1.2.tar.gz", hash = "sha256:ff64136da01aabba76ae88b050e7197918d8b2139ccbf6144e14d472b9c40445"}, 603 | {file = "setuptools_git-1.2-py2.py3-none-any.whl", hash = "sha256:e7764dccce7d97b4b5a330d7b966aac6f9ac026385743fd6cedad553f2494cfa"}, 604 | ] 605 | 606 | [[package]] 607 | name = "six" 608 | version = "1.16.0" 609 | description = "Python 2 and 3 compatibility utilities" 610 | optional = false 611 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 612 | files = [ 613 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 614 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 615 | ] 616 | 617 | [[package]] 618 | name = "sqlparse" 619 | version = "0.4.1" 620 | description = "A non-validating SQL parser." 621 | optional = false 622 | python-versions = ">=3.5" 623 | files = [ 624 | {file = "sqlparse-0.4.1-py3-none-any.whl", hash = "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0"}, 625 | {file = "sqlparse-0.4.1.tar.gz", hash = "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"}, 626 | ] 627 | 628 | [[package]] 629 | name = "tablib" 630 | version = "3.2.1" 631 | description = "Format agnostic tabular data library (XLS, JSON, YAML, CSV)" 632 | optional = false 633 | python-versions = ">=3.7" 634 | files = [ 635 | {file = "tablib-3.2.1-py3-none-any.whl", hash = "sha256:870d7e688f738531a14937a055e8bba404fbc388e77d4d500b2c904075d1019c"}, 636 | {file = "tablib-3.2.1.tar.gz", hash = "sha256:a57f2770b8c225febec1cb1e65012a69cf30dd28be810e0ff98d024768c7d0f1"}, 637 | ] 638 | 639 | [package.dependencies] 640 | markuppy = {version = "*", optional = true, markers = "extra == \"html\""} 641 | odfpy = {version = "*", optional = true, markers = "extra == \"ods\""} 642 | openpyxl = {version = ">=2.6.0", optional = true, markers = "extra == \"xlsx\""} 643 | pyyaml = {version = "*", optional = true, markers = "extra == \"yaml\""} 644 | xlrd = {version = "*", optional = true, markers = "extra == \"xls\""} 645 | xlwt = {version = "*", optional = true, markers = "extra == \"xls\""} 646 | 647 | [package.extras] 648 | all = ["markuppy", "odfpy", "openpyxl (>=2.6.0)", "pandas", "pyyaml", "tabulate", "xlrd", "xlwt"] 649 | cli = ["tabulate"] 650 | html = ["markuppy"] 651 | ods = ["odfpy"] 652 | pandas = ["pandas"] 653 | xls = ["xlrd", "xlwt"] 654 | xlsx = ["openpyxl (>=2.6.0)"] 655 | yaml = ["pyyaml"] 656 | 657 | [[package]] 658 | name = "tomli" 659 | version = "2.0.1" 660 | description = "A lil' TOML parser" 661 | optional = false 662 | python-versions = ">=3.7" 663 | files = [ 664 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 665 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 666 | ] 667 | 668 | [[package]] 669 | name = "typed-ast" 670 | version = "1.4.3" 671 | description = "a fork of Python 2 and 3 ast modules with type comment support" 672 | optional = false 673 | python-versions = "*" 674 | files = [ 675 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, 676 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, 677 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, 678 | {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, 679 | {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, 680 | {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, 681 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, 682 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, 683 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, 684 | {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, 685 | {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, 686 | {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, 687 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, 688 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, 689 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, 690 | {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, 691 | {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, 692 | {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, 693 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, 694 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, 695 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, 696 | {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, 697 | {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, 698 | {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, 699 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, 700 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, 701 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, 702 | {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, 703 | {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, 704 | {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, 705 | ] 706 | 707 | [[package]] 708 | name = "typing-extensions" 709 | version = "3.10.0.2" 710 | description = "Backported and Experimental Type Hints for Python 3.5+" 711 | optional = false 712 | python-versions = "*" 713 | files = [ 714 | {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, 715 | {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, 716 | {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, 717 | ] 718 | 719 | [[package]] 720 | name = "urwid" 721 | version = "2.1.2" 722 | description = "A full-featured console (xterm et al.) user interface library" 723 | optional = false 724 | python-versions = "*" 725 | files = [ 726 | {file = "urwid-2.1.2.tar.gz", hash = "sha256:588bee9c1cb208d0906a9f73c613d2bd32c3ed3702012f51efe318a3f2127eae"}, 727 | ] 728 | 729 | [[package]] 730 | name = "vine" 731 | version = "5.0.0" 732 | description = "Promises, promises, promises." 733 | optional = false 734 | python-versions = ">=3.6" 735 | files = [ 736 | {file = "vine-5.0.0-py2.py3-none-any.whl", hash = "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30"}, 737 | {file = "vine-5.0.0.tar.gz", hash = "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"}, 738 | ] 739 | 740 | [[package]] 741 | name = "wcwidth" 742 | version = "0.2.5" 743 | description = "Measures the displayed width of unicode strings in a terminal" 744 | optional = false 745 | python-versions = "*" 746 | files = [ 747 | {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, 748 | {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, 749 | ] 750 | 751 | [[package]] 752 | name = "xlrd" 753 | version = "2.0.1" 754 | description = "Library for developers to extract data from Microsoft Excel (tm) .xls spreadsheet files" 755 | optional = false 756 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 757 | files = [ 758 | {file = "xlrd-2.0.1-py2.py3-none-any.whl", hash = "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd"}, 759 | {file = "xlrd-2.0.1.tar.gz", hash = "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88"}, 760 | ] 761 | 762 | [package.extras] 763 | build = ["twine", "wheel"] 764 | docs = ["sphinx"] 765 | test = ["pytest", "pytest-cov"] 766 | 767 | [[package]] 768 | name = "xlwt" 769 | version = "1.3.0" 770 | description = "Library to create spreadsheet files compatible with MS Excel 97/2000/XP/2003 XLS files, on any platform, with Python 2.6, 2.7, 3.3+" 771 | optional = false 772 | python-versions = "*" 773 | files = [ 774 | {file = "xlwt-1.3.0-py2.py3-none-any.whl", hash = "sha256:a082260524678ba48a297d922cc385f58278b8aa68741596a87de01a9c628b2e"}, 775 | {file = "xlwt-1.3.0.tar.gz", hash = "sha256:c59912717a9b28f1a3c2a98fd60741014b06b043936dcecbc113eaaada156c88"}, 776 | ] 777 | 778 | [[package]] 779 | name = "zipp" 780 | version = "3.5.0" 781 | description = "Backport of pathlib-compatible object wrapper for zip files" 782 | optional = false 783 | python-versions = ">=3.6" 784 | files = [ 785 | {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, 786 | {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, 787 | ] 788 | 789 | [package.extras] 790 | docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] 791 | testing = ["func-timeout", "jaraco.itertools", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] 792 | 793 | [metadata] 794 | lock-version = "2.0" 795 | python-versions = "^3.7" 796 | content-hash = "a3648819f33a1e0bf8777c6ea2b3404fd85ab3102dce0a6ccde9547efe96f9ff" 797 | --------------------------------------------------------------------------------