├── 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: "| ".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 | + " |
"
130 | )
131 | summary += (
132 | "| "
133 | + " |
| ".join(
134 | [
135 | row.import_type + " | " + cols(row)
136 | for row in result.valid_rows()
137 | ]
138 | )
139 | + " |
"
140 | )
141 | else:
142 | cols = lambda row: "".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 | " | | row | "
164 | + "errors | "
165 | + " | ".join(
166 | [f.column_name for f in resource.get_user_visible_fields()]
167 | )
168 | + " |
"
169 | )
170 | summary += (
171 | "| "
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 | + " |
"
183 | )
184 | summary += "
"
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 |
--------------------------------------------------------------------------------