django-pfx 1.7.6.dev35__tar.gz → 1.7.6.dev39__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/PKG-INFO +3 -1
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/django_pfx.egg-info/PKG-INFO +3 -1
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/django_pfx.egg-info/SOURCES.txt +2 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/django_pfx.egg-info/requires.txt +3 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/django_pfx.egg-info/scm_file_list.json +2 -0
- django_pfx-1.7.6.dev39/django_pfx.egg-info/scm_version.json +8 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/doc/source/testing.md +36 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/test.py +203 -2
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/requirements.txt +3 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/setup.cfg +1 -0
- django_pfx-1.7.6.dev39/tests/tests/parallel_scenarios.py +89 -0
- django_pfx-1.7.6.dev39/tests/tests/test_table_file_helpers.py +144 -0
- django_pfx-1.7.6.dev39/tests/tests/test_test_runner.py +234 -0
- django_pfx-1.7.6.dev35/django_pfx.egg-info/scm_version.json +0 -8
- django_pfx-1.7.6.dev35/tests/tests/test_test_runner.py +0 -117
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/.gitignore +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/.gitlab-ci.yml +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/.pre-commit-config.yaml +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/LICENSE +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/MANIFEST.in +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/README.md +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/django_pfx.egg-info/dependency_links.txt +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/django_pfx.egg-info/top_level.txt +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/doc/Makefile +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/doc/conf.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/doc/index.rst +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/doc/source/api.views.rst +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/doc/source/authentication.md +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/doc/source/decorator.md +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/doc/source/generate_openapi.md +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/doc/source/getting_started.md +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/doc/source/internationalisation.md +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/doc/source/model.md +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/doc/source/pfx_views.md +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/doc/source/profiling.md +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/doc/source/settings.md +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/doc/source/user_jobs.md +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/img/pfx.png +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/img/pfx.svg +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/make_messages +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/manage.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/apps.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/email_backend/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/email_backend/memory_email_backend.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/email_backend/q_mailjet_email_backend.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/exceptions.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/locale/fr/LC_MESSAGES/django.mo +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/locale/fr/LC_MESSAGES/django.po +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/mailjet/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/mailjet/mailjet.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/mailjet/message_result.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/migrations/0001_initial.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/migrations/0002_alter_email_headers.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/migrations/0003_alter_email_options.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/migrations/0004_alter_email_mail_from.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/migrations/0005_lowercase_verbose_names.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/migrations/0006_alter_email_attachments.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/migrations/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/models/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/models/email.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/providers/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/providers/email_django_q_executor.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/shortcuts.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/tasks.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/templates/emails/base_template.html +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/templates/emails/base_template.txt +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/templates/emails/base_template_subject.txt +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/test.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/urls.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/views/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/emails/views/email_rest_view.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/apidoc/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/apidoc/parameters.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/apidoc/schema.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/apidoc/tags.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/apps.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/decorator/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/decorator/rest.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/default_settings.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/exceptions.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/fields/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/fields/decimal_field.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/fields/media_field.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/fields/minutes_duration_field.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/fields/rich_text_field.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/http/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/http/json_response.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/management/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/management/commands/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/management/commands/makeapidoc.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/management/commands/profile.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/mfa/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/mfa/authenticator.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/mfa/base.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/mfa/email.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/mfa/legacy.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/mfa/sms.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/middleware/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/middleware/authentication.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/middleware/locale.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/middleware/profiling.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/middleware/request_too_big_middleware.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/middleware/ws_cookie_authentication_middleware.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/migrations/0001_initial.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/migrations/0002_pfxpermissionsuser.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/migrations/0003_delete_pfxpermissionsuser.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/migrations/0004_alter_loginban_failed_counter_and_more.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/migrations/0005_mfadevice_mfadevice_mfa_device_unique_backend.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/migrations/0006_alter_pfxuser_managers.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/migrations/0007_alter_pfxuser_options.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/migrations/0008_lowercase_verbose_names.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/migrations/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/migrations/operations/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/migrations/operations/permissions.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/models/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/models/abstract_pfx_base_user.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/models/attachment_mixin.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/models/cache_mixins.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/models/login_ban.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/models/mfa_device.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/models/mfa_user_mixin.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/models/not_null_fields.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/models/ordered_model_mixin.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/models/pfx_models.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/models/pfx_user.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/serializers/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/serializers/json.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/settings.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/shortcuts.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/sms/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/sms/backends/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/sms/backends/base.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/sms/backends/console.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/sms/backends/memory.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/storage/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/storage/exceptions.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/storage/local_storage.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/storage/s3_storage.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/templates/registration/otp_code_email.txt +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/templates/registration/otp_code_subject.txt +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/urls.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/version_check.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/authentication_views.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/fields.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/filters_views.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/locale_views.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/media_rest_view_mixin.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/mfa_authentication_views.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/ordered_rest_view_mixin.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/parameters/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/parameters/date_format.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/parameters/groups.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/parameters/list_count.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/parameters/list_items.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/parameters/list_order.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/parameters/list_search.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/parameters/subset.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/views/rest_views.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/pfxcore/xml_test_runner.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/settings/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/settings/dev.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/userjobs/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/userjobs/apps.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/userjobs/locale/fr/LC_MESSAGES/django.mo +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/userjobs/locale/fr/LC_MESSAGES/django.po +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/userjobs/migrations/0001_initial.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/userjobs/migrations/0002_alter_userjob_options_alter_userjob_auto_queued_and_more.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/userjobs/migrations/0003_clean_scheduled_tasks.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/userjobs/migrations/0004_userjob_summary_url.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/userjobs/migrations/0005_swappable_user_job.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/userjobs/migrations/0006_lowercase_verbose_names.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/userjobs/migrations/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/userjobs/models/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/userjobs/models/user_job.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/userjobs/models/user_job_attachment.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/userjobs/providers/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/userjobs/providers/user_job_django_q_executor.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/userjobs/providers/user_job_websocket_channel_provider.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/userjobs/tasks.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/userjobs/urls.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/userjobs/views/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/userjobs/views/user_job_attachment_view.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/userjobs/views/user_job_view.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pfx/userjobs/views/user_websocket_consumer.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/pyproject.toml +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/serve-doc +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/setup.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/apps.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/channel_group_send_calls.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/mfa_urls.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/migrations/0001_initial.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/migrations/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/models.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/settings/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/settings/ci.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/settings/common.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/settings/dev.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/settings/dev_custom_example.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/settings/dev_default.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/basic_api_errors.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/basic_api_test.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/mfa_test_utils.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_api_doc.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_api_doc_search.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_auth_api.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_body_mixin.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_cache.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_client.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_emails_email_api.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_emails_email_model.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_emails_q_mailjet_email_backend.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_emails_send_email_message.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_emails_test_tools.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_fields_choices.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_fields_date.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_fields_decimal.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_fields_help_text.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_fields_minutes_duration.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_fields_one2many.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_fields_rich_text.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_fields_time.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_filters.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_locale_api.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_mfa_legacy.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_mfa_login_api.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_mfa_setup_api.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_ordered_rest_view_mixin.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_perm_tests.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_permissions.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_perms_api.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_post_migrate_groups_update.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_profiling_middleware.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_settings.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_shortcuts.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_timezone_middleware.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_tools.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_user_job_api.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_user_job_messages.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_user_job_model.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_user_queryset.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_user_websocket.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_verbose_names.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_version_check.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_view_decorators.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_view_fields.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/tests/test_ws_cookie_middleware.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/urls.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests/views.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_base_user/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_base_user/migrations/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_base_user/settings/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_base_user/settings/ci.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_base_user/settings/common.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_base_user/settings/dev.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_base_user/settings/dev_custom_example.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_base_user/settings/dev_default.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_base_user/tests/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_base_user/tests/test_api.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_base_user/tests/test_auth_api.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_base_user/urls.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_base_user/views.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user/migrations/0001_initial.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user/migrations/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user/models.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user/settings/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user/settings/ci.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user/settings/common.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user/settings/dev.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user/settings/dev_custom_example.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user/settings/dev_default.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user/tests/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user/tests/test_api.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user/tests/test_auth_api.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user/urls.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user/views.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user_job/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user_job/migrations/0001_initial.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user_job/migrations/0002_lowercase_verbose_names.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user_job/migrations/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user_job/models.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user_job/providers.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user_job/scope.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user_job/settings/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user_job/settings/ci.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user_job/settings/common.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user_job/settings/dev.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user_job/settings/dev_custom_example.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user_job/settings/dev_default.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user_job/tests/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user_job/tests/test_user_job.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_custom_user_job/urls.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_user_email_login/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_user_email_login/migrations/0001_initial.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_user_email_login/migrations/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_user_email_login/models.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_user_email_login/settings/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_user_email_login/settings/ci.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_user_email_login/settings/common.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_user_email_login/settings/dev.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_user_email_login/settings/dev_custom_example.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_user_email_login/settings/dev_default.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_user_email_login/tests/__init__.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_user_email_login/tests/test_api.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_user_email_login/tests/test_auth_api.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_user_email_login/urls.py +0 -0
- {django_pfx-1.7.6.dev35 → django_pfx-1.7.6.dev39}/tests_user_email_login/views.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-pfx
|
|
3
|
-
Version: 1.7.6.
|
|
3
|
+
Version: 1.7.6.dev39
|
|
4
4
|
Summary: Django PFX is a toolkit designed to streamline the development of RESTful APIs using the Django framework.
|
|
5
5
|
Author: Hervé Martinet
|
|
6
6
|
Author-email: herve.martinet@gmail.com
|
|
@@ -41,6 +41,8 @@ Requires-Dist: pyotp>=2.10.0; extra == "otp"
|
|
|
41
41
|
Provides-Extra: heic-converter
|
|
42
42
|
Requires-Dist: python-magic; extra == "heic-converter"
|
|
43
43
|
Requires-Dist: pillow-heif; extra == "heic-converter"
|
|
44
|
+
Provides-Extra: xlsx
|
|
45
|
+
Requires-Dist: openpyxl; extra == "xlsx"
|
|
44
46
|
Dynamic: license-file
|
|
45
47
|
|
|
46
48
|
# Django PFX
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-pfx
|
|
3
|
-
Version: 1.7.6.
|
|
3
|
+
Version: 1.7.6.dev39
|
|
4
4
|
Summary: Django PFX is a toolkit designed to streamline the development of RESTful APIs using the Django framework.
|
|
5
5
|
Author: Hervé Martinet
|
|
6
6
|
Author-email: herve.martinet@gmail.com
|
|
@@ -41,6 +41,8 @@ Requires-Dist: pyotp>=2.10.0; extra == "otp"
|
|
|
41
41
|
Provides-Extra: heic-converter
|
|
42
42
|
Requires-Dist: python-magic; extra == "heic-converter"
|
|
43
43
|
Requires-Dist: pillow-heif; extra == "heic-converter"
|
|
44
|
+
Provides-Extra: xlsx
|
|
45
|
+
Requires-Dist: openpyxl; extra == "xlsx"
|
|
44
46
|
Dynamic: license-file
|
|
45
47
|
|
|
46
48
|
# Django PFX
|
|
@@ -220,6 +220,7 @@ tests/tests/__init__.py
|
|
|
220
220
|
tests/tests/basic_api_errors.py
|
|
221
221
|
tests/tests/basic_api_test.py
|
|
222
222
|
tests/tests/mfa_test_utils.py
|
|
223
|
+
tests/tests/parallel_scenarios.py
|
|
223
224
|
tests/tests/test_api_doc.py
|
|
224
225
|
tests/tests/test_api_doc_search.py
|
|
225
226
|
tests/tests/test_auth_api.py
|
|
@@ -252,6 +253,7 @@ tests/tests/test_post_migrate_groups_update.py
|
|
|
252
253
|
tests/tests/test_profiling_middleware.py
|
|
253
254
|
tests/tests/test_settings.py
|
|
254
255
|
tests/tests/test_shortcuts.py
|
|
256
|
+
tests/tests/test_table_file_helpers.py
|
|
255
257
|
tests/tests/test_test_runner.py
|
|
256
258
|
tests/tests/test_timezone_middleware.py
|
|
257
259
|
tests/tests/test_tools.py
|
|
@@ -210,6 +210,7 @@
|
|
|
210
210
|
"tests/tests/basic_api_errors.py",
|
|
211
211
|
"tests/tests/basic_api_test.py",
|
|
212
212
|
"tests/tests/mfa_test_utils.py",
|
|
213
|
+
"tests/tests/parallel_scenarios.py",
|
|
213
214
|
"tests/tests/test_api_doc.py",
|
|
214
215
|
"tests/tests/test_api_doc_search.py",
|
|
215
216
|
"tests/tests/test_auth_api.py",
|
|
@@ -242,6 +243,7 @@
|
|
|
242
243
|
"tests/tests/test_profiling_middleware.py",
|
|
243
244
|
"tests/tests/test_settings.py",
|
|
244
245
|
"tests/tests/test_shortcuts.py",
|
|
246
|
+
"tests/tests/test_table_file_helpers.py",
|
|
245
247
|
"tests/tests/test_test_runner.py",
|
|
246
248
|
"tests/tests/test_timezone_middleware.py",
|
|
247
249
|
"tests/tests/test_tools.py",
|
|
@@ -430,3 +430,39 @@ class ATestClass(EmailTestMixin, SMSTestMixin, TestCase):
|
|
|
430
430
|
[(to, message)] = self.sms_outbox()
|
|
431
431
|
self.assertEqual(to, '+41791234567')
|
|
432
432
|
```
|
|
433
|
+
|
|
434
|
+
## CSV and Excel files
|
|
435
|
+
|
|
436
|
+
`CSVFileHelper` and `XLSXFileHelper` read back a table file of a test: an
|
|
437
|
+
export, a file attached to a user job, a file to import.
|
|
438
|
+
|
|
439
|
+
```python
|
|
440
|
+
from pfx.pfxcore.test import CSVFileHelper, XLSXFileHelper
|
|
441
|
+
|
|
442
|
+
f = XLSXFileHelper(response) # or CSVFileHelper(response)
|
|
443
|
+
f.header() # ['Name', 'Birth']
|
|
444
|
+
f.dicts() # [{'Name': 'Alice', 'Birth': datetime(…)}, …]
|
|
445
|
+
f.lines()[0][f.column('Name')] # 'Alice'
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
The file can be given as bytes, a string (its content), a `Path`, an HTTP
|
|
449
|
+
response (streamed or not),
|
|
450
|
+
an iterable of chunks, or the value of a `MediaField` on the local storage
|
|
451
|
+
(`XLSXFileHelper(job.attachments.get().file)`).
|
|
452
|
+
|
|
453
|
+
- `header=2`: the number of the line of the headers, for a file with a title
|
|
454
|
+
above them. The lines after it are the data.
|
|
455
|
+
- `header()`, `lines()`, `dicts()`: the headers, the data as lists, the data
|
|
456
|
+
as dicts by header. `line(n)` and `line_dict(n)` count from 0, the first
|
|
457
|
+
line of the file; `len()` counts every line.
|
|
458
|
+
- `column(title)`: the index of a column.
|
|
459
|
+
- `write_file(path)` and `pretty_print()`: to look at the file while writing a
|
|
460
|
+
test.
|
|
461
|
+
|
|
462
|
+
`CSVFileHelper(data, delimiter=';', encoding='utf-8-sig')` reads the CSV of a
|
|
463
|
+
French Excel; its values are strings.
|
|
464
|
+
|
|
465
|
+
`XLSXFileHelper(data, sheet='Totals')` reads a sheet by name or by index, the
|
|
466
|
+
active one by default. Its values keep their type (numbers, dates), and
|
|
467
|
+
`workbook` and `sheet` give the formats and the styles. It needs
|
|
468
|
+
[openpyxl](https://openpyxl.readthedocs.io/): install `django-pfx[xlsx]`.
|
|
@@ -1,15 +1,25 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import csv
|
|
3
|
+
import io
|
|
1
4
|
import json
|
|
2
5
|
import logging
|
|
3
6
|
import re
|
|
7
|
+
import traceback
|
|
4
8
|
from functools import reduce
|
|
5
9
|
from http.cookies import SimpleCookie
|
|
6
10
|
from json import JSONDecodeError
|
|
11
|
+
from pathlib import Path
|
|
7
12
|
|
|
8
13
|
from django.apps import apps
|
|
9
14
|
from django.db import transaction
|
|
10
15
|
from django.test import override_settings
|
|
11
16
|
from django.test.client import Client as DjangoClient
|
|
12
|
-
from django.test.runner import
|
|
17
|
+
from django.test.runner import (
|
|
18
|
+
DiscoverRunner,
|
|
19
|
+
ParallelTestSuite,
|
|
20
|
+
RemoteTestResult,
|
|
21
|
+
RemoteTestRunner,
|
|
22
|
+
)
|
|
13
23
|
from django.test.runner import _init_worker as django_init_worker
|
|
14
24
|
from django.utils.module_loading import import_string
|
|
15
25
|
|
|
@@ -320,6 +330,130 @@ class TestPermsAssertMixin(TestAssertMixin):
|
|
|
320
330
|
self.assertPerms(**perms)
|
|
321
331
|
|
|
322
332
|
|
|
333
|
+
def file_content(data):
|
|
334
|
+
"""The bytes of a file given to a test.
|
|
335
|
+
|
|
336
|
+
``data`` can be bytes, a string, a ``Path``, an HTTP response (streamed
|
|
337
|
+
or not), an iterable of chunks, or the value of a ``MediaField`` on the
|
|
338
|
+
local storage, such as the attachment of a user job.
|
|
339
|
+
"""
|
|
340
|
+
if isinstance(data, bytes):
|
|
341
|
+
return data
|
|
342
|
+
if isinstance(data, Path):
|
|
343
|
+
return data.read_bytes()
|
|
344
|
+
if isinstance(data, str):
|
|
345
|
+
return data.encode()
|
|
346
|
+
if isinstance(data, dict):
|
|
347
|
+
return Path(settings.STORAGE_LOCAL_ROOT, data['key']).read_bytes()
|
|
348
|
+
if hasattr(data, 'streaming_content'):
|
|
349
|
+
data = data.streaming_content
|
|
350
|
+
elif hasattr(data, 'content'):
|
|
351
|
+
return data.content
|
|
352
|
+
return b"".join(
|
|
353
|
+
chunk.encode() if isinstance(chunk, str) else chunk
|
|
354
|
+
for chunk in data)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class TableFileResponseHelper:
|
|
358
|
+
"""A table file of a test, read back as rows of values.
|
|
359
|
+
|
|
360
|
+
``header`` is the number of the line of the headers, the first by
|
|
361
|
+
default: the lines after it are the data. :meth:`line` counts from 0,
|
|
362
|
+
the first line of the file.
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
def __init__(self, data, header=1):
|
|
366
|
+
self.content = file_content(data)
|
|
367
|
+
self.header_index = header - 1
|
|
368
|
+
self.rows = self.read_rows()
|
|
369
|
+
|
|
370
|
+
def read_rows(self):
|
|
371
|
+
raise NotImplementedError # pragma: no cover
|
|
372
|
+
|
|
373
|
+
def write_file(self, path):
|
|
374
|
+
"""Write the file, to open it while writing a test."""
|
|
375
|
+
Path(path).write_bytes(self.content)
|
|
376
|
+
|
|
377
|
+
def pretty_print(self):
|
|
378
|
+
"""Print the rows as a table, to read them while writing a test."""
|
|
379
|
+
cells = [
|
|
380
|
+
['' if v is None else str(v) for v in row] for row in self.rows]
|
|
381
|
+
widths = [
|
|
382
|
+
max(len(row[i]) for row in cells if i < len(row))
|
|
383
|
+
for i in range(max((len(row) for row in cells), default=0))]
|
|
384
|
+
print()
|
|
385
|
+
for row in cells:
|
|
386
|
+
print(' '.join(v.ljust(w) for v, w in zip(row, widths)).rstrip())
|
|
387
|
+
|
|
388
|
+
def header(self):
|
|
389
|
+
return self.rows[self.header_index]
|
|
390
|
+
|
|
391
|
+
def line(self, n):
|
|
392
|
+
return self.rows[n]
|
|
393
|
+
|
|
394
|
+
def len(self):
|
|
395
|
+
return len(self.rows)
|
|
396
|
+
|
|
397
|
+
def lines(self):
|
|
398
|
+
"""The rows after the headers."""
|
|
399
|
+
return self.rows[self.header_index + 1:]
|
|
400
|
+
|
|
401
|
+
def column(self, title):
|
|
402
|
+
"""The index of the column ``title``."""
|
|
403
|
+
return self.header().index(title)
|
|
404
|
+
|
|
405
|
+
def line_dict(self, n):
|
|
406
|
+
"""The line ``n`` by header."""
|
|
407
|
+
return dict(zip(self.header(), self.line(n)))
|
|
408
|
+
|
|
409
|
+
def dicts(self):
|
|
410
|
+
"""The rows after the headers, by header."""
|
|
411
|
+
return [dict(zip(self.header(), row)) for row in self.lines()]
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class CSVFileHelper(TableFileResponseHelper):
|
|
415
|
+
"""A CSV file of a test; the values are strings."""
|
|
416
|
+
|
|
417
|
+
def __init__(self, data, header=1, delimiter=',', encoding='utf-8-sig'):
|
|
418
|
+
self.delimiter = delimiter
|
|
419
|
+
self.encoding = encoding
|
|
420
|
+
super().__init__(data, header)
|
|
421
|
+
|
|
422
|
+
def read_rows(self):
|
|
423
|
+
return list(csv.reader(
|
|
424
|
+
io.StringIO(self.content.decode(self.encoding)),
|
|
425
|
+
delimiter=self.delimiter))
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
class XLSXFileHelper(TableFileResponseHelper):
|
|
429
|
+
"""An Excel file of a test, read with openpyxl.
|
|
430
|
+
|
|
431
|
+
``sheet`` is the name or the index of the sheet read, the active one
|
|
432
|
+
by default. The values keep their type (numbers, dates); ``workbook``
|
|
433
|
+
and ``sheet`` give the formats and the styles.
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
def __init__(self, data, header=1, sheet=None):
|
|
437
|
+
self.sheet_name = sheet
|
|
438
|
+
super().__init__(data, header)
|
|
439
|
+
|
|
440
|
+
def read_rows(self):
|
|
441
|
+
try:
|
|
442
|
+
import openpyxl
|
|
443
|
+
except ImportError: # pragma: no cover
|
|
444
|
+
raise ImportError(
|
|
445
|
+
"XLSXFileHelper needs openpyxl: install django-pfx[xlsx].")
|
|
446
|
+
|
|
447
|
+
self.workbook = openpyxl.load_workbook(io.BytesIO(self.content))
|
|
448
|
+
if self.sheet_name is None:
|
|
449
|
+
self.sheet = self.workbook.active
|
|
450
|
+
elif isinstance(self.sheet_name, int):
|
|
451
|
+
self.sheet = self.workbook.worksheets[self.sheet_name]
|
|
452
|
+
else:
|
|
453
|
+
self.sheet = self.workbook[self.sheet_name]
|
|
454
|
+
return [list(row) for row in self.sheet.iter_rows(values_only=True)]
|
|
455
|
+
|
|
456
|
+
|
|
323
457
|
class MockBoto3Client:
|
|
324
458
|
def generate_presigned_url(self, *args, **kwargs):
|
|
325
459
|
params = kwargs.get('Params', {})
|
|
@@ -351,8 +485,72 @@ def _init_worker(*args, **kwargs):
|
|
|
351
485
|
override_settings(**get_test_backends()).enable()
|
|
352
486
|
|
|
353
487
|
|
|
488
|
+
class UnpicklableTestError(Exception):
|
|
489
|
+
"""An error a parallel worker could not send back, as text."""
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
class UnpicklableTestFailure(UnpicklableTestError, AssertionError):
|
|
493
|
+
"""A failure a parallel worker could not send back, as text."""
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
class _Repr(str):
|
|
497
|
+
def __repr__(self):
|
|
498
|
+
return str(self)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
class PicklableRemoteTestResult(RemoteTestResult):
|
|
502
|
+
"""Send every outcome of a parallel worker back to the parent.
|
|
503
|
+
|
|
504
|
+
Django aborts the whole run, without the traceback, on an exception it
|
|
505
|
+
cannot pickle (one holding a request, a lock, a local class…). Such an
|
|
506
|
+
exception is sent as its formatted traceback instead, and the run goes
|
|
507
|
+
on."""
|
|
508
|
+
|
|
509
|
+
def _picklable(self, err):
|
|
510
|
+
try:
|
|
511
|
+
self._confirm_picklable(err)
|
|
512
|
+
except Exception:
|
|
513
|
+
cls = (
|
|
514
|
+
UnpicklableTestFailure if issubclass(err[0], AssertionError)
|
|
515
|
+
else UnpicklableTestError)
|
|
516
|
+
return cls, cls(''.join(traceback.format_exception(*err))), None
|
|
517
|
+
return err
|
|
518
|
+
|
|
519
|
+
def _picklable_subtest(self, test, subtest):
|
|
520
|
+
# The subtest holds the running test case, which can become
|
|
521
|
+
# unpicklable after the failure: a fresh one is always sent.
|
|
522
|
+
subtest = copy.copy(subtest)
|
|
523
|
+
subtest.test_case = type(test)(test._testMethodName)
|
|
524
|
+
try:
|
|
525
|
+
self._confirm_picklable(subtest)
|
|
526
|
+
except Exception:
|
|
527
|
+
subtest.params = {
|
|
528
|
+
k: _Repr(repr(v)) for k, v in subtest.params.items()}
|
|
529
|
+
return subtest
|
|
530
|
+
|
|
531
|
+
def addError(self, test, err):
|
|
532
|
+
super().addError(test, self._picklable(err))
|
|
533
|
+
|
|
534
|
+
def addFailure(self, test, err):
|
|
535
|
+
super().addFailure(test, self._picklable(err))
|
|
536
|
+
|
|
537
|
+
def addExpectedFailure(self, test, err):
|
|
538
|
+
super().addExpectedFailure(test, self._picklable(err))
|
|
539
|
+
|
|
540
|
+
def addSubTest(self, test, subtest, err):
|
|
541
|
+
if err is not None:
|
|
542
|
+
err = self._picklable(err)
|
|
543
|
+
subtest = self._picklable_subtest(test, subtest)
|
|
544
|
+
super().addSubTest(test, subtest, err)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
class PicklableRemoteTestRunner(RemoteTestRunner):
|
|
548
|
+
resultclass = PicklableRemoteTestResult
|
|
549
|
+
|
|
550
|
+
|
|
354
551
|
class MemoryBackendsParallelTestSuite(ParallelTestSuite):
|
|
355
552
|
init_worker = _init_worker
|
|
553
|
+
runner_class = PicklableRemoteTestRunner
|
|
356
554
|
|
|
357
555
|
|
|
358
556
|
class MemoryBackendsRunnerMixin:
|
|
@@ -371,7 +569,10 @@ class MemoryBackendsRunnerMixin:
|
|
|
371
569
|
validated, which ``locmem`` never does.
|
|
372
570
|
|
|
373
571
|
A test class decorated with ``override_settings`` keeps its own
|
|
374
|
-
backends.
|
|
572
|
+
backends.
|
|
573
|
+
|
|
574
|
+
With ``--parallel``, the workers also send back the errors Django
|
|
575
|
+
cannot pickle (see :class:`PicklableRemoteTestResult`)."""
|
|
375
576
|
|
|
376
577
|
parallel_test_suite = MemoryBackendsParallelTestSuite
|
|
377
578
|
|
|
@@ -4,6 +4,8 @@ pre-commit
|
|
|
4
4
|
flake8
|
|
5
5
|
isort
|
|
6
6
|
coverage
|
|
7
|
+
# Pickles the tracebacks of the --parallel workers.
|
|
8
|
+
tblib
|
|
7
9
|
psycopg
|
|
8
10
|
Babel
|
|
9
11
|
pyjwt>=2.1
|
|
@@ -31,3 +33,4 @@ munch
|
|
|
31
33
|
# bypasses the error handling of pfx.emails.models.Email.send.
|
|
32
34
|
mailjet_rest<1.8
|
|
33
35
|
responses
|
|
36
|
+
openpyxl
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Test cases that fail in ways a parallel worker cannot pickle.
|
|
2
|
+
|
|
3
|
+
Run by test_test_runner.py, not by the discovery (the name does not start
|
|
4
|
+
with ``test``). They live at module level as real test classes do: the
|
|
5
|
+
parent process rebuilds a test case from its class."""
|
|
6
|
+
import threading
|
|
7
|
+
import unittest
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Unpicklable(Exception):
|
|
11
|
+
def __init__(self):
|
|
12
|
+
super().__init__("holds a lock")
|
|
13
|
+
self.lock = threading.Lock()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class KeywordOnly(Exception):
|
|
17
|
+
"""Pickles, but cannot be rebuilt from its args."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, *, code):
|
|
20
|
+
super().__init__(f"code {code}")
|
|
21
|
+
self.code = code
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UnpicklableAssertion(AssertionError):
|
|
25
|
+
def __init__(self):
|
|
26
|
+
super().__init__("assertion holding a lock")
|
|
27
|
+
self.lock = threading.Lock()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Scenarios(unittest.TestCase):
|
|
31
|
+
|
|
32
|
+
def test_plain_failure(self):
|
|
33
|
+
self.assertEqual(1, 2)
|
|
34
|
+
|
|
35
|
+
def test_lock_in_the_exception(self):
|
|
36
|
+
raise Unpicklable()
|
|
37
|
+
|
|
38
|
+
def test_unpicklable_cause(self):
|
|
39
|
+
try:
|
|
40
|
+
raise Unpicklable()
|
|
41
|
+
except Unpicklable as e:
|
|
42
|
+
raise ValueError("wrapped") from e
|
|
43
|
+
|
|
44
|
+
def test_local_class(self):
|
|
45
|
+
class Local(Exception):
|
|
46
|
+
pass
|
|
47
|
+
raise Local("local class")
|
|
48
|
+
|
|
49
|
+
def test_keyword_only_init(self):
|
|
50
|
+
raise KeywordOnly(code=42)
|
|
51
|
+
|
|
52
|
+
def test_unpicklable_assertion(self):
|
|
53
|
+
raise UnpicklableAssertion()
|
|
54
|
+
|
|
55
|
+
@unittest.expectedFailure
|
|
56
|
+
def test_expected_failure(self):
|
|
57
|
+
raise Unpicklable()
|
|
58
|
+
|
|
59
|
+
def test_subtest_then_a_lock(self):
|
|
60
|
+
with self.subTest(i=1):
|
|
61
|
+
self.assertEqual(1, 5)
|
|
62
|
+
self.lock = threading.Lock()
|
|
63
|
+
|
|
64
|
+
def test_subtest_with_a_lock_param(self):
|
|
65
|
+
with self.subTest(lock=threading.Lock()):
|
|
66
|
+
self.fail("with a lock as param")
|
|
67
|
+
|
|
68
|
+
def test_subtest_unpicklable_error(self):
|
|
69
|
+
with self.subTest(n=1):
|
|
70
|
+
raise Unpicklable()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class TearDownError(unittest.TestCase):
|
|
74
|
+
|
|
75
|
+
def tearDown(self):
|
|
76
|
+
raise Unpicklable()
|
|
77
|
+
|
|
78
|
+
def test_ok(self):
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class SetUpClassError(unittest.TestCase):
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def setUpClass(cls):
|
|
86
|
+
raise Unpicklable()
|
|
87
|
+
|
|
88
|
+
def test_never_runs(self):
|
|
89
|
+
pass
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import os
|
|
3
|
+
import tempfile
|
|
4
|
+
from contextlib import redirect_stdout
|
|
5
|
+
from datetime import date
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from django.http import HttpResponse, StreamingHttpResponse
|
|
9
|
+
from django.test import SimpleTestCase, override_settings
|
|
10
|
+
|
|
11
|
+
from openpyxl import Workbook
|
|
12
|
+
|
|
13
|
+
from pfx.pfxcore.test import CSVFileHelper, XLSXFileHelper
|
|
14
|
+
|
|
15
|
+
CSV = "Name,Age\nAlice,30\nBob,\n"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def xlsx(*sheets):
|
|
19
|
+
"""An Excel file of `sheets`, (title, rows) pairs."""
|
|
20
|
+
workbook = Workbook()
|
|
21
|
+
workbook.remove(workbook.active)
|
|
22
|
+
for title, rows in sheets:
|
|
23
|
+
sheet = workbook.create_sheet(title)
|
|
24
|
+
for row in rows:
|
|
25
|
+
sheet.append(row)
|
|
26
|
+
output = io.BytesIO()
|
|
27
|
+
workbook.save(output)
|
|
28
|
+
return output.getvalue()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
PEOPLE = [['Name', 'Birth'], ['Alice', date(1990, 5, 1)], ['Bob', None]]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TestCSVFileHelper(SimpleTestCase):
|
|
35
|
+
def test_the_sources_of_a_file(self):
|
|
36
|
+
"""Bytes, a string, a response, a streamed one, chunks of text."""
|
|
37
|
+
for data in [
|
|
38
|
+
CSV.encode(), CSV, HttpResponse(CSV),
|
|
39
|
+
StreamingHttpResponse(iter([CSV[:10], CSV[10:]])),
|
|
40
|
+
[CSV[:10].encode(), CSV[10:].encode()]]:
|
|
41
|
+
with self.subTest(data=type(data).__name__):
|
|
42
|
+
f = CSVFileHelper(data)
|
|
43
|
+
self.assertEqual(f.header(), ['Name', 'Age'])
|
|
44
|
+
self.assertEqual(f.len(), 3)
|
|
45
|
+
|
|
46
|
+
def test_lines(self):
|
|
47
|
+
"""`line` counts from the headers, `lines` and `dicts` are the data;
|
|
48
|
+
a CSV value is a string."""
|
|
49
|
+
f = CSVFileHelper(CSV)
|
|
50
|
+
|
|
51
|
+
self.assertEqual(f.line(1), ['Alice', '30'])
|
|
52
|
+
self.assertEqual(f.lines(), [['Alice', '30'], ['Bob', '']])
|
|
53
|
+
self.assertEqual(f.column('Age'), 1)
|
|
54
|
+
self.assertEqual(f.line_dict(2), {'Name': 'Bob', 'Age': ''})
|
|
55
|
+
self.assertEqual(
|
|
56
|
+
f.dicts(),
|
|
57
|
+
[{'Name': 'Alice', 'Age': '30'}, {'Name': 'Bob', 'Age': ''}])
|
|
58
|
+
|
|
59
|
+
def test_a_french_excel_csv(self):
|
|
60
|
+
"""Semicolons, and the byte order mark Excel writes."""
|
|
61
|
+
f = CSVFileHelper(
|
|
62
|
+
"Nom;Âge\nAlice;30\n".encode(), delimiter=';')
|
|
63
|
+
|
|
64
|
+
self.assertEqual(f.dicts(), [{'Nom': 'Alice', 'Âge': '30'}])
|
|
65
|
+
|
|
66
|
+
def test_headers_below_a_title(self):
|
|
67
|
+
f = CSVFileHelper("Report\nName,Age\nAlice,30\n", header=2)
|
|
68
|
+
|
|
69
|
+
self.assertEqual(f.header(), ['Name', 'Age'])
|
|
70
|
+
self.assertEqual(f.dicts(), [{'Name': 'Alice', 'Age': '30'}])
|
|
71
|
+
|
|
72
|
+
def test_unknown_column(self):
|
|
73
|
+
with self.assertRaises(ValueError):
|
|
74
|
+
CSVFileHelper(CSV).column('Size')
|
|
75
|
+
|
|
76
|
+
def test_a_path(self):
|
|
77
|
+
"""A Path is a file to read; a string is the content itself."""
|
|
78
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
79
|
+
path = Path(tmp, 'file.csv')
|
|
80
|
+
path.write_text(CSV)
|
|
81
|
+
|
|
82
|
+
self.assertEqual(CSVFileHelper(path).len(), 3)
|
|
83
|
+
|
|
84
|
+
def test_write_file_and_pretty_print(self):
|
|
85
|
+
f = CSVFileHelper(CSV)
|
|
86
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
87
|
+
path = os.path.join(tmp, 'file.csv')
|
|
88
|
+
f.write_file(path)
|
|
89
|
+
with open(path, 'rb') as written:
|
|
90
|
+
self.assertEqual(written.read(), CSV.encode())
|
|
91
|
+
|
|
92
|
+
out = io.StringIO()
|
|
93
|
+
with redirect_stdout(out):
|
|
94
|
+
f.pretty_print()
|
|
95
|
+
self.assertEqual(
|
|
96
|
+
out.getvalue(), "\nName Age\nAlice 30\nBob\n")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class TestXLSXFileHelper(SimpleTestCase):
|
|
100
|
+
def test_values_keep_their_type(self):
|
|
101
|
+
"""A date is a datetime, an empty cell None."""
|
|
102
|
+
f = XLSXFileHelper(xlsx(('People', PEOPLE)))
|
|
103
|
+
|
|
104
|
+
self.assertEqual(f.header(), ['Name', 'Birth'])
|
|
105
|
+
self.assertEqual(f.line_dict(1)['Birth'].date(), date(1990, 5, 1))
|
|
106
|
+
self.assertEqual(f.dicts()[1], {'Name': 'Bob', 'Birth': None})
|
|
107
|
+
self.assertEqual(f.len(), 3)
|
|
108
|
+
self.assertEqual(f.column('Birth'), 1)
|
|
109
|
+
|
|
110
|
+
def test_the_sheet_read(self):
|
|
111
|
+
"""The active one by default, else by name or by index; the sheet
|
|
112
|
+
is there for the formats and the styles."""
|
|
113
|
+
content = xlsx(('People', PEOPLE), ('Totals', [['Total'], [2]]))
|
|
114
|
+
|
|
115
|
+
self.assertEqual(XLSXFileHelper(content).header(), ['Name', 'Birth'])
|
|
116
|
+
f = XLSXFileHelper(content, sheet='Totals')
|
|
117
|
+
self.assertEqual(f.dicts(), [{'Total': 2}])
|
|
118
|
+
self.assertEqual(f.sheet.title, 'Totals')
|
|
119
|
+
self.assertEqual(
|
|
120
|
+
XLSXFileHelper(content, sheet=1).header(), ['Total'])
|
|
121
|
+
|
|
122
|
+
def test_a_response(self):
|
|
123
|
+
f = XLSXFileHelper(StreamingHttpResponse(
|
|
124
|
+
iter([xlsx(('People', PEOPLE))])))
|
|
125
|
+
|
|
126
|
+
self.assertEqual(f.lines()[0][0], 'Alice')
|
|
127
|
+
|
|
128
|
+
def test_headers_below_a_title(self):
|
|
129
|
+
f = XLSXFileHelper(
|
|
130
|
+
xlsx(('Report', [['Report 2026'], *PEOPLE])), header=2)
|
|
131
|
+
|
|
132
|
+
self.assertEqual(f.header(), ['Name', 'Birth'])
|
|
133
|
+
self.assertEqual(len(f.dicts()), 2)
|
|
134
|
+
|
|
135
|
+
def test_a_file_of_the_local_storage(self):
|
|
136
|
+
"""The value of a MediaField, such as the attachment of a job."""
|
|
137
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
138
|
+
with open(os.path.join(tmp, 'export.xlsx'), 'wb') as f:
|
|
139
|
+
f.write(xlsx(('People', PEOPLE)))
|
|
140
|
+
with override_settings(STORAGE_LOCAL_ROOT=tmp):
|
|
141
|
+
helper = XLSXFileHelper(
|
|
142
|
+
{'key': 'export.xlsx', 'name': 'export.xlsx'})
|
|
143
|
+
|
|
144
|
+
self.assertEqual(helper.header(), ['Name', 'Birth'])
|