From ae9f951942dabd0d60b8316e3e5bb6acbce74513 Mon Sep 17 00:00:00 2001 From: MohmdFo Date: Mon, 16 Sep 2024 10:35:27 +0330 Subject: [PATCH] refactor(sage_mailbox): restructure project and update pre-commit configurations - Removed obsolete config files: `.flake8`, `.pylintrc`, `mypy.ini`, `MANIFEST.in`, `setup.cfg`, `setup.py`, `requirements.txt` - Updated `pyproject.toml` and `poetry.lock` for dependency management - Added `requirements/dev.txt` and `requirements/prod.txt` - Fixed pre-commit rules in `.pre-commit-config.yaml` - Adjusted `.gitignore` for new structure - Added tests under `sage_mailbox/tests/` - Updated documentation in `docs/` and `.readthedocs.yaml` - Added `CODE_OF_CONDUCT.md` - Introduced `tox.ini` for testing - Refactored codebase to comply with new standards - Removed obsolete code: `sage_mailbox/views.py` --- .flake8 | 4 - .gitignore | 6 +- .pre-commit-config.yaml | 122 +++++- .pylintrc | 13 - .readthedocs.yaml | 2 +- CODE_OF_CONDUCT.md | 186 ++++++++ MANIFEST.in | 2 - docs/source/getting_started/installation.rst | 19 +- mypy.ini | 13 - poetry.lock | 168 +++++--- pyproject.toml | 211 +++++++-- requirements.txt | 403 ------------------ requirements/dev.txt | 103 +++++ requirements/prod.txt | 57 +++ sage_mailbox/admin/actions/email.py | 117 ++--- sage_mailbox/admin/email.py | 146 ++++++- sage_mailbox/admin/mailbox.py | 42 +- sage_mailbox/admin/mixins/email.py | 21 +- sage_mailbox/apps.py | 1 - sage_mailbox/checks.py | 58 ++- sage_mailbox/conf.py | 2 +- sage_mailbox/models/attachment.py | 4 +- sage_mailbox/models/email.py | 17 +- sage_mailbox/models/mailbox.py | 4 +- sage_mailbox/repository/manager.py | 8 +- sage_mailbox/repository/queryset.py | 34 +- sage_mailbox/repository/service.py | 7 +- sage_mailbox/signals/email.py | 14 +- sage_mailbox/tests/__init__.py | 0 sage_mailbox/tests/models/__init__.py | 0 sage_mailbox/tests/models/test_email.py | 46 ++ sage_mailbox/tests/repository/__init__.py | 0 .../tests/repository/test_queryset.py | 102 +++++ sage_mailbox/tests/repository/test_service.py | 159 +++++++ sage_mailbox/tests/validators/__init__.py | 0 .../tests/validators/test_comma_separated.py | 42 ++ .../tests/validators/test_folder_name.py | 53 +++ sage_mailbox/utils.py | 4 +- sage_mailbox/validators.py | 16 +- sage_mailbox/views.py | 3 - setup.cfg | 3 - setup.py | 28 -- tox.ini | 42 ++ 43 files changed, 1549 insertions(+), 733 deletions(-) delete mode 100644 .flake8 delete mode 100644 .pylintrc create mode 100644 CODE_OF_CONDUCT.md delete mode 100644 MANIFEST.in delete mode 100644 mypy.ini delete mode 100644 requirements.txt create mode 100644 requirements/dev.txt create mode 100644 requirements/prod.txt create mode 100644 sage_mailbox/tests/__init__.py create mode 100644 sage_mailbox/tests/models/__init__.py create mode 100644 sage_mailbox/tests/models/test_email.py create mode 100644 sage_mailbox/tests/repository/__init__.py create mode 100644 sage_mailbox/tests/repository/test_queryset.py create mode 100644 sage_mailbox/tests/repository/test_service.py create mode 100644 sage_mailbox/tests/validators/__init__.py create mode 100644 sage_mailbox/tests/validators/test_comma_separated.py create mode 100644 sage_mailbox/tests/validators/test_folder_name.py delete mode 100644 sage_mailbox/views.py delete mode 100644 setup.cfg delete mode 100644 setup.py create mode 100644 tox.ini diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 848f4fb..0000000 --- a/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -max-line-length = 88 -extend-ignore = E203, W503 -exclude = docs diff --git a/.gitignore b/.gitignore index 7e69d19..f48b269 100644 --- a/.gitignore +++ b/.gitignore @@ -158,6 +158,7 @@ sage.py */sage.py */attachments/* +attachments/ # ruff .ruff_cache/ @@ -176,4 +177,7 @@ manage.py # Ignore all migrations files except __init__.py **/migrations/* -!**/migrations/__init__.py \ No newline at end of file +!**/migrations/__init__.py + +venv_3_8 +bandit_report.txt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 99b3936..56715ba 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,28 +2,130 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.3.0 hooks: + - id: check-toml + - id: check-yaml + files: \.yaml$ - id: trailing-whitespace + exclude: (migrations/|tests/|docs/|static/|media/).* - id: end-of-file-fixer + exclude: (migrations/|tests/|docs/|static/|media/).* - id: check-added-large-files + exclude: (migrations/|tests/|docs/|static/|media/).* - id: check-case-conflict + exclude: (migrations/|tests/|docs/|static/|media/).* - id: check-merge-conflict + exclude: (migrations/|tests/|docs/|static/|media/).* - id: check-docstring-first + exclude: (migrations/|tests/|docs/|static/|media/).* - - repo: https://github.com/psf/black - rev: 23.3.0 + - repo: https://github.com/tox-dev/pyproject-fmt + rev: 2.2.1 hooks: - - id: black - exclude: docs/ + - id: pyproject-fmt + + - repo: https://github.com/tox-dev/tox-ini-fmt + rev: 1.3.1 + hooks: + - id: tox-ini-fmt + + - repo: https://github.com/asottile/pyupgrade + rev: v3.17.0 + hooks: + - id: pyupgrade + + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.5.5 + hooks: + - id: ruff + args: ["--config=pyproject.toml"] + exclude: (migrations/|tests/|docs/|static/|media/|apps.py).* - repo: https://github.com/pre-commit/mirrors-isort rev: v5.10.1 hooks: - id: isort - exclude: docs/ + exclude: (migrations/|tests/|docs/|static/|media/).* + + - repo: https://github.com/psf/black + rev: 23.3.0 + hooks: + - id: black + args: ["--config=pyproject.toml"] + exclude: (migrations/|tests/|docs/|static/|media/).* - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.991 + - repo: https://github.com/commitizen-tools/commitizen + rev: v3.28.0 hooks: - - id: mypy - args: ["--config-file=mypy.ini"] - exclude: ^(docs/|stubs/|tests/) + - id: commitizen + + - repo: https://github.com/PyCQA/bandit + rev: 1.7.4 + hooks: + - id: bandit + args: ["-c", "pyproject.toml", "-r", "."] + additional_dependencies: ["bandit[toml]"] + exclude: (migrations/|tests/|docs/|static/|media/).* + + - repo: https://github.com/adamchainz/blacken-docs + rev: 1.18.0 + hooks: + - id: blacken-docs + additional_dependencies: + - black==24.4.2 + files: '\.rst$' + + - repo: https://github.com/rstcheck/rstcheck + rev: "v6.2.4" + hooks: + - id: rstcheck + args: ["--report-level=warning"] + files: ^docs/(.*/)*.*\.rst$ + additional_dependencies: ["Sphinx==6.2.1"] + + - repo: https://github.com/regebro/pyroma + rev: "4.2" + hooks: + - id: pyroma + always_run: false + files: | + (?x)^( + README.md| + pyproject.toml| + )$ + + - repo: https://github.com/DanielNoord/pydocstringformatter + rev: v0.7.3 + hooks: + - id: pydocstringformatter + args: ["--max-summary-lines=2", "--linewrap-full-docstring"] + files: "sage_mailbox" + + - repo: local + hooks: + - id: pytest + name: Pytest + entry: poetry run pytest -v + language: system + types: [python] + stages: [commit] + pass_filenames: false + always_run: true + + - id: pylint + name: Pylint + entry: pylint + language: system + types: [python] + require_serial: true + args: + - "-rn" + - "-sn" + - "--rcfile=pyproject.toml" + - "--fail-under=9.0" + files: ^sage_mailbox/ + +ci: + skip: [ + pylint, + pytest + ] diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index e0fbb70..0000000 --- a/.pylintrc +++ /dev/null @@ -1,13 +0,0 @@ -[MASTER] -ignore=tests,docs,build,stubs -persistent=yes -ignore-patterns=^tests/.*, ^docs/.*, ^build/.*, ^stubs/.* - -[MESSAGES CONTROL] -disable=C0114,C0115,C0116,R0903 - -[FORMAT] -max-line-length=88 - -[DESIGN] -max-args=5 diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 98486dd..927775c 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -29,4 +29,4 @@ sphinx: # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: install: - - requirements: requirements.txt + - requirements: requirements/dev.txt diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..199148a --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,186 @@ +## Contribution Guidelines + +Thank you for your interest in contributing to our package! This document outlines the tools and steps to follow to ensure a smooth and consistent workflow. + +## Contribution Workflow + +1. **Fork and Clone**: Fork the repository and clone it to your local machine. + ```bash + git clone https://github.com//django-sage-mailbox.git + cd django-sage-invoice + ``` + + +2. **Initialize Git Flow**: Set up Git Flow to manage your branches efficiently. + ```bash + git flow init + ``` + Follow the prompts to configure Git Flow. The default options usually suffice. + +3. **Create a Branch**: Create a new branch for your feature or bugfix using Git Flow. + + - **Creating a Feature Branch**: + ```bash + git flow feature start your-feature-name + ``` + This will create and check out a new branch from the `develop` branch. + + - **Creating a Bugfix Branch**: + ```bash + git flow bugfix start your-bugfix-name + ``` + This will create and check out a new branch specifically for the bugfix. + +4. **Install Dependencies**: Use Poetry to install dependencies. + ```bash + poetry install + ``` + +5. **Write Code and Tests**: Make your changes and write tests for your new code. + +6. **Run Code Quality Checks**: Ensure code quality with pre-commit, Ruff, and Pylint. + ```bash + poetry run pre-commit run --all-files + poetry run ruff check sage_mailbox/ --fix + poetry run black sage_mailbox/ + poetry run isort sage_mailbox/ + poetry run pylint sage_mailbox/ + poetry run bandit -r sage_mailbox/ -c pyproject.toml + ``` + +7. **Run Tests**: Ensure all tests pass using Poetry. + ```bash + poetry run pytest + ``` + +8. **Commit Changes**: Use Commitizen to commit your changes. + ```bash + cz commit + ``` + +9. **Push and Create a PR**: Push your changes and create a pull request. + ```bash + git push origin feature/your-feature-name + ``` + +10. **Bump Version**: Use Commitizen to bump the version. + ```bash + cz bump + ``` + +11. **Generate Changelog**: Use Commitizen to generate the changelog. + ```bash + cz changelog + ``` + +12. **Export Dependencies**: Export dependencies for development and production. + ```bash + poetry export -f requirements.txt --output requirements/prod.txt --without-hashes + poetry export -f requirements.txt --dev --output requirements/dev.txt --without-hashes + ``` + +## Commitizen Message Rule + +Commitizen follows the Conventional Commits specification. The commit message should be structured as follows: + +``` +[optional scope]: + +[optional body] + +[optional footer(s)] +``` + +Here are 10 examples of commit messages following the Commitizen Conventional Commits specification: + +### 1. Initialization of core +``` +feat(core): initialize the core module + +- Set up the core structure +- Added initial configurations and settings +- Created basic utility functions +``` + +### 2. Release with build and tag version +``` +build(release): build and tag version 1.0.0 + +- Built the project for production +- Created a new tag for version 1.0.0 +- Updated changelog with release notes +``` + +### 3. Adding a new feature +``` +feat(auth): add user authentication + +- Implemented user login and registration +- Added JWT token generation and validation +- Created middleware for protected routes +``` + +### 4. Fixing a bug +``` +fix(api): resolve issue with data fetching + +- Fixed bug causing incorrect data responses +- Improved error handling in API calls +- Added tests for the fixed bug +``` + +### 5. Update a doc (Sphinx) +``` +docs(sphinx): update API documentation + +- Updated the Sphinx documentation for API changes +- Added examples for new endpoints +- Fixed typos and formatting issues +``` + +### 6. Update dependencies (packages) +``` +build(deps): update project dependencies + +- Updated all outdated npm packages +- Resolved compatibility issues with new package versions +- Ran tests to ensure no breaking changes +``` + +### 7. Update version for build and publish +``` +build(version): update version to 2.1.0 for build and publish + +- Incremented version number to 2.1.0 +- Updated package.json with the new version +- Prepared for publishing the new build +``` + +### 8. Adding unit tests +``` +test(auth): add unit tests for authentication module + +- Created tests for login functionality +- Added tests for registration validation +- Ensured 100% coverage for auth module +``` + +### 9. Refactoring codebase +``` +refactor(core): improve code structure and readability + +- Refactored core module to enhance readability +- Extracted utility functions into separate files +- Updated documentation to reflect code changes +``` + +### 10. Improving performance +``` +perf(parser): enhance parsing speed + +- Optimized parsing algorithm for better performance +- Reduced the time complexity of the parsing function +- Added benchmarks to track performance improvements +``` + +These examples cover various types of commits such as feature additions, bug fixes, documentation updates, dependency updates, versioning, testing, refactoring, and performance improvements. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 04f196a..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -include README.md -include LICENSE diff --git a/docs/source/getting_started/installation.rst b/docs/source/getting_started/installation.rst index a410da8..c283a62 100644 --- a/docs/source/getting_started/installation.rst +++ b/docs/source/getting_started/installation.rst @@ -58,11 +58,10 @@ To use `django-sage-mailbox`, add it to your `INSTALLED_APPS` in the Django sett .. code-block:: python INSTALLED_APPS = [ + # other packages "django.contrib.sites", - ... "sage_mailbox", "django_jsonform", - ... ] Additional Configuration for IMAP and Email @@ -95,12 +94,12 @@ Configure your IMAP and email settings in `settings.py`: # Custom Email Headers DEFAULT_EMAIL_HEADERS = { - "X-Mailer": "sage_imap", - "List-Unsubscribe": "", - "Return-Path": "", - "Reply-To": "replyto@example.com", - "X-Priority": "3", - "X-Report-Abuse-To": "abuse@example.com", - "X-Spamd-Result": "default", - "X-Auto-Response-Suppress": "All", + "X-Mailer": "sage_imap", + "List-Unsubscribe": "", + "Return-Path": "", + "Reply-To": "replyto@example.com", + "X-Priority": "3", + "X-Report-Abuse-To": "abuse@example.com", + "X-Spamd-Result": "default", + "X-Auto-Response-Suppress": "All", } diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 0035aa9..0000000 --- a/mypy.ini +++ /dev/null @@ -1,13 +0,0 @@ -[mypy] -mypy_path = stubs -disallow_untyped_calls = true -disallow_untyped_defs = true -ignore_missing_imports = true -explicit_package_bases = true -exclude = ^(docs/source/conf.py|build/|tests/|stubs/|kernel/|migrations/|manage.py) - -[mypy-sage_mailbox.*] -ignore_missing_imports = True - -[mypy.plugins.django-stubs] -django_settings_module = "kernel.settings" diff --git a/poetry.lock b/poetry.lock index 9e6efd1..80fcd2d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -64,6 +64,30 @@ files = [ [package.extras] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] +[[package]] +name = "bandit" +version = "1.7.9" +description = "Security oriented static analyser for python code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "bandit-1.7.9-py3-none-any.whl", hash = "sha256:52077cb339000f337fb25f7e045995c4ad01511e716e5daac37014b9752de8ec"}, + {file = "bandit-1.7.9.tar.gz", hash = "sha256:7c395a436743018f7be0a4cbb0a4ea9b902b6d87264ddecf8cfdc73b4f78ff61"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} +PyYAML = ">=5.3.1" +rich = "*" +stevedore = ">=1.20.0" + +[package.extras] +baseline = ["GitPython (>=3.1.30)"] +sarif = ["jschema-to-python (>=1.2.3)", "sarif-om (>=1.0.4)"] +test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)"] +toml = ["tomli (>=1.1.0)"] +yaml = ["PyYAML"] + [[package]] name = "black" version = "24.4.2" @@ -645,22 +669,6 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1 testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] typing = ["typing-extensions (>=4.8)"] -[[package]] -name = "flake8" -version = "7.1.0" -description = "the modular source code checker: pep8 pyflakes and co" -optional = false -python-versions = ">=3.8.1" -files = [ - {file = "flake8-7.1.0-py2.py3-none-any.whl", hash = "sha256:2e416edcc62471a64cea09353f4e7bdba32aeb079b6e360554c659a122b1bc6a"}, - {file = "flake8-7.1.0.tar.gz", hash = "sha256:48a07b626b55236e0fb4784ee69a465fbf59d79eec1f5b4785c3d3bc57d17aa5"}, -] - -[package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.12.0,<2.13.0" -pyflakes = ">=3.2.0,<3.3.0" - [[package]] name = "identify" version = "2.6.0" @@ -963,6 +971,24 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "model-bakery" +version = "1.19.5" +description = "Smart object creation facility for Django." +optional = false +python-versions = ">=3.8" +files = [ + {file = "model_bakery-1.19.5-py3-none-any.whl", hash = "sha256:09ecbbf124d32614339581b642c82ac4a73147442f598c7bad23eece24187e5c"}, + {file = "model_bakery-1.19.5.tar.gz", hash = "sha256:37cece544a33f8899ed8f0488cd6a9d2b0b6925e7b478a4ff2786dece8c63745"}, +] + +[package.dependencies] +django = ">=4.2" + +[package.extras] +docs = ["myst-parser", "sphinx", "sphinx-rtd-theme"] +test = ["black", "coverage", "mypy", "pillow", "pytest", "pytest-django", "ruff"] + [[package]] name = "more-itertools" version = "10.3.0" @@ -1089,6 +1115,17 @@ files = [ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] +[[package]] +name = "pbr" +version = "6.1.0" +description = "Python Build Reasonableness" +optional = false +python-versions = ">=2.6" +files = [ + {file = "pbr-6.1.0-py2.py3-none-any.whl", hash = "sha256:a776ae228892d8013649c0aeccbb3d5f99ee15e005a4cbb7e61d55a067b28a2a"}, + {file = "pbr-6.1.0.tar.gz", hash = "sha256:788183e382e3d1d7707db08978239965e8b9e4e5ed42669bf4758186734d5f24"}, +] + [[package]] name = "pkginfo" version = "1.10.0" @@ -1166,17 +1203,6 @@ files = [ [package.dependencies] wcwidth = "*" -[[package]] -name = "pycodestyle" -version = "2.12.0" -description = "Python style guide checker" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pycodestyle-2.12.0-py2.py3-none-any.whl", hash = "sha256:949a39f6b86c3e1515ba1787c2022131d165a8ad271b11370a8819aa070269e4"}, - {file = "pycodestyle-2.12.0.tar.gz", hash = "sha256:442f950141b4f43df752dd303511ffded3a04c2b6fb7f65980574f0c31e6e79c"}, -] - [[package]] name = "pycparser" version = "2.22" @@ -1188,17 +1214,6 @@ files = [ {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] -[[package]] -name = "pyflakes" -version = "3.2.0" -description = "passive checker of Python programs" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, - {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, -] - [[package]] name = "pygments" version = "2.18.0" @@ -1237,6 +1252,38 @@ tomlkit = ">=0.10.1" spelling = ["pyenchant (>=3.2,<4.0)"] testutils = ["gitpython (>3)"] +[[package]] +name = "pylint-django" +version = "2.5.5" +description = "A Pylint plugin to help Pylint understand the Django web framework" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "pylint_django-2.5.5-py3-none-any.whl", hash = "sha256:5abd5c2228e0e5e2a4cb6d0b4fc1d1cef1e773d0be911412f4dd4fc1a1a440b7"}, + {file = "pylint_django-2.5.5.tar.gz", hash = "sha256:2f339e4bf55776958283395c5139c37700c91bd5ef1d8251ef6ac88b5abbba9b"}, +] + +[package.dependencies] +pylint = ">=2.0,<4" +pylint-plugin-utils = ">=0.8" + +[package.extras] +with-django = ["Django (>=2.2)"] + +[[package]] +name = "pylint-plugin-utils" +version = "0.8.2" +description = "Utilities and helpers for writing Pylint plugins" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "pylint_plugin_utils-0.8.2-py3-none-any.whl", hash = "sha256:ae11664737aa2effbf26f973a9e0b6779ab7106ec0adc5fe104b0907ca04e507"}, + {file = "pylint_plugin_utils-0.8.2.tar.gz", hash = "sha256:d3cebf68a38ba3fba23a873809155562571386d4c1b03e5b4c4cc26c3eee93e4"}, +] + +[package.dependencies] +pylint = ">=1.7" + [[package]] name = "pyproject-api" version = "1.7.1" @@ -1293,6 +1340,24 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] +[[package]] +name = "pytest-django" +version = "4.8.0" +description = "A Django plugin for pytest." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-django-4.8.0.tar.gz", hash = "sha256:5d054fe011c56f3b10f978f41a8efb2e5adfc7e680ef36fb571ada1f24779d90"}, + {file = "pytest_django-4.8.0-py3-none-any.whl", hash = "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[package.extras] +docs = ["sphinx", "sphinx-rtd-theme"] +testing = ["Django", "django-configurations (>=2.0)"] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -1724,6 +1789,20 @@ files = [ dev = ["build", "hatch"] doc = ["sphinx"] +[[package]] +name = "stevedore" +version = "5.3.0" +description = "Manage dynamic plugins for Python applications" +optional = false +python-versions = ">=3.8" +files = [ + {file = "stevedore-5.3.0-py3-none-any.whl", hash = "sha256:1efd34ca08f474dad08d9b19e934a22c68bb6fe416926479ba29e5013bcc8f78"}, + {file = "stevedore-5.3.0.tar.gz", hash = "sha256:9a64265f4060312828151c204efbe9b7a9852a0d9228756344dbc7e4023e375a"}, +] + +[package.dependencies] +pbr = ">=2.0.0" + [[package]] name = "termcolor" version = "2.4.0" @@ -1808,17 +1887,6 @@ files = [ {file = "types_PyYAML-6.0.12.20240724-py3-none-any.whl", hash = "sha256:e5becec598f3aa3a2ddf671de4a75fa1c6856fbf73b2840286c9d50fae2d5d48"}, ] -[[package]] -name = "types-setuptools" -version = "70.3.0.20240710" -description = "Typing stubs for setuptools" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-setuptools-70.3.0.20240710.tar.gz", hash = "sha256:842cbf399812d2b65042c9d6ff35113bbf282dee38794779aa1f94e597bafc35"}, - {file = "types_setuptools-70.3.0.20240710-py3-none-any.whl", hash = "sha256:bd0db2a4b9f2c49ac5564be4e0fb3125c4c46b1f73eafdcbceffa5b005cceca4"}, -] - [[package]] name = "typing-extensions" version = "4.12.2" @@ -1931,4 +1999,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "65730f7976c42ce589e669b79b9f6a305c1f9c082b47158c7ca08282f07227a7" +content-hash = "1b06c3cf4e403f41dedf4b57a0b00eaafffb1bd9bd18a6e23d936f04390964e6" diff --git a/pyproject.toml b/pyproject.toml index e74e414..dfe2a5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,23 +1,28 @@ +[build-system] +build-backend = "poetry.core.masonry.api" +requires = [ "poetry-core>=1" ] + [tool.poetry] name = "django-sage-mailbox" version = "0.1.0" description = "A Django package for mailbox client integration." -authors = ["Sepehr Akbarzadeh "] +authors = [ "Sepehr Akbarzadeh " ] readme = "README.md" license = "MIT" -keywords = ["django", "email", "imap", "mailbox", "sageteam", "django-packages"] +keywords = [ "django", "email", "imap", "mailbox", "sageteam", "django-packages" ] repository = "https://github.com/sageteamorg/django-sage-mailbox" -classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Intended Audience :: Developers", - "Framework :: Django", +classifiers = [ + "Development Status :: 2 - Alpha", + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Intended Audience :: Developers", + "Framework :: Django", ] packages = [ - { include = "sage_mailbox" } + { include = "sage_mailbox" }, ] [tool.poetry.urls] @@ -26,21 +31,18 @@ packages = [ [tool.poetry.dependencies] python = "^3.12" -setuptools = "^70.2.0" -wheel = "^0.43.0" -twine = "^5.1.1" django = "^5.0.7" python-sage-imap = "^0.4.2" django-jsonform = "^2.22.0" python-dateutil = "^2.9.0.post0" django-autoslug = "^1.9.9" +bandit = { extras = [ "toml" ], version = "^1.7.9" } [tool.poetry.group.dev.dependencies] black = "^24.4.2" isort = "^5.13.2" mypy = "^1.10.1" pytest = "^8.2.2" -flake8 = "^7.1.0" tox = "^4.15.1" coverage = "^7.5.4" pre-commit = "^3.7.1" @@ -49,33 +51,162 @@ pylint = "^3.2.5" pytest-cov = "^5.0.0" commitizen = "^3.27.0" docformatter = "^1.7.5" -types-setuptools = "^70.2.0.20240704" django-stubs = "^5.0.2" sphinx-rtd-theme = "^2.0.0" +pylint-django = "^2.5.5" +pytest-django = "^4.8.0" +model-bakery = "^1.19.5" [tool.black] line-length = 88 -target-version = ['py38'] exclude = ''' /( \.git - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist - | docs + | \.venv + | build + | dist + | migrations + | venv + | env + | __pycache__ + | static + | media + | node_modules + | env + | kernel + | \.mypy_cache + | \.pytest_cache + | .*\.egg-info )/ ''' +[tool.ruff] +line-length = 88 +exclude = [ + "*.egg-info/*", + ".git/*", + ".mypy_cache/*", + ".pytest_cache/*", + ".venv/*", + "__pycache__/*", + "apps.py", + "build/*", + "dist/*", + "migrations/*", + "sage_shop/warehouse/schemas/*", + "tests", + "venv/*", +] + +lint.select = [ + "B", # Bugbear codes for potential issues + "C90", # Custom error codes + "E", # PEP8 error codes + "F", # PyFlakes error codes + "S", # Security checks +] +lint.ignore = [ + "E203", # Ignore whitespace before ':', ';', or '#' + "E501", # Ignore line length issues (lines longer than 88 characters) +] + [tool.isort] profile = "black" line_length = 88 -known_first_party = ["django_sage_mailbox"] -skip = ["docs"] +skip = [ + "venv", + ".venv", + "build", + "dist", + ".git", + "__pycache__", + "*.egg-info", + ".mypy_cache", + ".pytest_cache", + "migrations", + "static", + "media", + "node_modules", + "env", + "kernel", +] + +[tool.pylint] +disable = [ + "C0114", # Missing module docstring + "C0115", # Missing class docstring + "C0116", # Missing function or method docstring + "E1101", # Instance of 'Foo' has no 'bar' member (Django dynamic attributes) + "W0212", # Access to a protected member _foo of a client class + "R0903", # Too few public methods (for Django models) + "R0801", # Similar Codes +] +max-line-length = 88 +ignore = [ + "migrations", + "*/apps.py", + ".venv/*", + "build/*", + "dist/*", + ".git/*", + "__pycache__/*", + "*.egg-info/*", + ".mypy_cache/*", + ".pytest_cache/*", + "tests", +] +load-plugins = [ + "pylint.extensions.docparams", +] +good-names = [ + "qs", # QuerySet abbreviation + "pk", # Primary key abbreviation + "id", # Identifier +] +suggestion-mode = true +const-rgx = "([A-Z_][A-Z0-9_]*)|(__.*__)" +attr-rgx = "[a-z_][a-z0-9_]{2,30}$" +variable-rgx = "[a-z_][a-z0-9_]{2,30}$" +argument-rgx = "[a-z_][a-z0-9_]{2,30}$" +method-rgx = "[a-z_][a-z0-9_]{2,30}$" +function-rgx = "[a-z_][a-z0-9_]{2,30}$" +class-rgx = "[A-Z_][a-zA-Z0-9]+$" +module-rgx = "(([a-z_][a-z0-9_]*)|(__.*__))$" + +[tool.pytest.ini_options] +addopts = "--cov --cov-report=term-missing --cov-report=html --cov-fail-under=90" +DJANGO_SETTINGS_MODULE = "kernel.settings" +python_files = [ "tests.py", "test_*.py" ] +testpaths = [ "tests" ] +norecursedirs = [ + "migrations", + "static", + "media", + "node_modules", + "env", + "venv", + ".venv", + "dist", + "build", + "kernel", +] + +[tool.coverage.run] +omit = [ + "*/migrations/*", + "kernel/*", + "*/apps.py", + "manage.py", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "if self\\.debug", + "raise AssertionError", + "if 0:", + "if __name__ == .__main__.:", +] [tool.mypy] mypy_path = "stubs" @@ -93,12 +224,26 @@ exclude = ''' [tool.commitizen] name = "cz_conventional_commits" -version = "0.1.0" +version = "0.2.0" -[tool.pytest.ini_options] -addopts = "--strict-markers" -testpaths = ["tests"] +[tool.commitizen.settings] +increment_types = [ "feat", "fix" ] -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" +[tool.bandit] +targets = [ "./sage_shop" ] +exclude_dirs = [ + "tests", + "migrations", +] +severity = "medium" +confidence = "medium" +max_lines = 500 +progress = true +reports = true +output_format = "screen" +output_file = "bandit_report.txt" +include = [ "B101", "B102" ] +exclude_tests = [ "B301", "B302" ] + +[tool.bandit.plugins] +B104 = { check_typed_list = true } diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index a73a94a..0000000 --- a/requirements.txt +++ /dev/null @@ -1,403 +0,0 @@ -alabaster==0.7.16 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65 \ - --hash=sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92 -asgiref==3.8.1 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47 \ - --hash=sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590 -babel==2.15.0 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb \ - --hash=sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413 -certifi==2024.7.4 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \ - --hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90 -cffi==1.16.0 ; python_version >= "3.12" and python_version < "4.0" and sys_platform == "linux" and platform_python_implementation != "PyPy" \ - --hash=sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc \ - --hash=sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a \ - --hash=sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417 \ - --hash=sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab \ - --hash=sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520 \ - --hash=sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36 \ - --hash=sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743 \ - --hash=sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8 \ - --hash=sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed \ - --hash=sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684 \ - --hash=sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56 \ - --hash=sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324 \ - --hash=sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d \ - --hash=sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235 \ - --hash=sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e \ - --hash=sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088 \ - --hash=sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000 \ - --hash=sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7 \ - --hash=sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e \ - --hash=sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673 \ - --hash=sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c \ - --hash=sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe \ - --hash=sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2 \ - --hash=sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098 \ - --hash=sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8 \ - --hash=sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a \ - --hash=sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0 \ - --hash=sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b \ - --hash=sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896 \ - --hash=sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e \ - --hash=sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9 \ - --hash=sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2 \ - --hash=sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b \ - --hash=sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6 \ - --hash=sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404 \ - --hash=sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f \ - --hash=sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0 \ - --hash=sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4 \ - --hash=sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc \ - --hash=sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936 \ - --hash=sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba \ - --hash=sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872 \ - --hash=sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb \ - --hash=sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614 \ - --hash=sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1 \ - --hash=sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d \ - --hash=sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969 \ - --hash=sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b \ - --hash=sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4 \ - --hash=sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627 \ - --hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \ - --hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357 -charset-normalizer==3.3.2 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \ - --hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \ - --hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \ - --hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \ - --hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \ - --hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \ - --hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \ - --hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \ - --hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \ - --hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \ - --hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \ - --hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \ - --hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \ - --hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \ - --hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \ - --hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \ - --hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \ - --hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \ - --hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \ - --hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \ - --hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \ - --hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \ - --hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \ - --hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \ - --hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \ - --hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \ - --hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \ - --hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \ - --hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \ - --hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \ - --hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \ - --hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \ - --hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \ - --hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \ - --hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \ - --hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \ - --hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \ - --hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \ - --hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \ - --hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \ - --hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \ - --hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \ - --hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \ - --hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \ - --hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \ - --hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \ - --hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \ - --hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \ - --hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \ - --hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \ - --hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \ - --hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \ - --hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \ - --hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \ - --hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \ - --hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \ - --hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \ - --hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \ - --hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \ - --hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \ - --hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \ - --hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \ - --hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \ - --hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \ - --hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \ - --hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \ - --hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \ - --hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \ - --hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \ - --hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \ - --hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \ - --hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \ - --hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \ - --hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \ - --hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \ - --hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \ - --hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \ - --hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \ - --hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \ - --hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \ - --hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \ - --hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \ - --hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \ - --hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \ - --hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \ - --hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \ - --hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \ - --hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \ - --hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \ - --hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561 -colorama==0.4.6 ; python_version >= "3.12" and python_version < "4.0" and sys_platform == "win32" \ - --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ - --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 -cryptography==43.0.0 ; python_version >= "3.12" and python_version < "4.0" and sys_platform == "linux" \ - --hash=sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709 \ - --hash=sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069 \ - --hash=sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2 \ - --hash=sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b \ - --hash=sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e \ - --hash=sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70 \ - --hash=sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778 \ - --hash=sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22 \ - --hash=sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895 \ - --hash=sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf \ - --hash=sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431 \ - --hash=sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f \ - --hash=sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947 \ - --hash=sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74 \ - --hash=sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc \ - --hash=sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66 \ - --hash=sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66 \ - --hash=sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf \ - --hash=sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f \ - --hash=sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5 \ - --hash=sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e \ - --hash=sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f \ - --hash=sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55 \ - --hash=sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1 \ - --hash=sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47 \ - --hash=sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5 \ - --hash=sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0 -django-jsonform==2.22.0 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:0c9d50fb371938e7262a7fef7c5a60835dd288f872f87b952d5e2ea84c825221 \ - --hash=sha256:c4dd1ba2b0152bd3164aacf326a83c35355c70d12de81908b5ced5f94c8263d6 -django==5.0.7 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:bd4505cae0b9bd642313e8fb71810893df5dc2ffcacaa67a33af2d5cd61888f2 \ - --hash=sha256:f216510ace3de5de01329463a315a629f33480e893a9024fc93d8c32c22913da -docutils==0.20.1 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6 \ - --hash=sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b -idna==3.7 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \ - --hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0 -imagesize==1.4.1 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \ - --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a -importlib-metadata==8.2.0 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:11901fa0c2f97919b288679932bb64febaeacf289d18ac84dd68cb2e74213369 \ - --hash=sha256:72e8d4399996132204f9a16dcc751af254a48f8d1b20b9ff0f98d4a8f901e73d -jaraco-classes==3.4.0 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd \ - --hash=sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790 -jaraco-context==5.3.0 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:3e16388f7da43d384a1a7cd3452e72e14732ac9fe459678773a3608a812bf266 \ - --hash=sha256:c2f67165ce1f9be20f32f650f25d8edfc1646a8aeee48ae06fb35f90763576d2 -jaraco-functools==4.0.1 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:3b24ccb921d6b593bdceb56ce14799204f473976e2a9d4b15b04d0f2c2326664 \ - --hash=sha256:d33fa765374c0611b52f8b3a795f8900869aa88c84769d4d1746cd68fb28c3e8 -jeepney==0.8.0 ; python_version >= "3.12" and python_version < "4.0" and sys_platform == "linux" \ - --hash=sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806 \ - --hash=sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755 -jinja2==3.1.4 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \ - --hash=sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d -keyring==25.2.1 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:2458681cdefc0dbc0b7eb6cf75d0b98e59f9ad9b2d4edd319d18f68bdca95e50 \ - --hash=sha256:daaffd42dbda25ddafb1ad5fec4024e5bbcfe424597ca1ca452b299861e49f1b -markdown-it-py==3.0.0 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ - --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb -markupsafe==2.1.5 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf \ - --hash=sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff \ - --hash=sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f \ - --hash=sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3 \ - --hash=sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532 \ - --hash=sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f \ - --hash=sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617 \ - --hash=sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df \ - --hash=sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4 \ - --hash=sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906 \ - --hash=sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f \ - --hash=sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4 \ - --hash=sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8 \ - --hash=sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371 \ - --hash=sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2 \ - --hash=sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465 \ - --hash=sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52 \ - --hash=sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6 \ - --hash=sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169 \ - --hash=sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad \ - --hash=sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2 \ - --hash=sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0 \ - --hash=sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029 \ - --hash=sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f \ - --hash=sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a \ - --hash=sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced \ - --hash=sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5 \ - --hash=sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c \ - --hash=sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf \ - --hash=sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9 \ - --hash=sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb \ - --hash=sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad \ - --hash=sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3 \ - --hash=sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1 \ - --hash=sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46 \ - --hash=sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc \ - --hash=sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a \ - --hash=sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee \ - --hash=sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900 \ - --hash=sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5 \ - --hash=sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea \ - --hash=sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f \ - --hash=sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5 \ - --hash=sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e \ - --hash=sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a \ - --hash=sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f \ - --hash=sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50 \ - --hash=sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a \ - --hash=sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b \ - --hash=sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4 \ - --hash=sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff \ - --hash=sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2 \ - --hash=sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46 \ - --hash=sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b \ - --hash=sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf \ - --hash=sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5 \ - --hash=sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5 \ - --hash=sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab \ - --hash=sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd \ - --hash=sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68 -mdurl==0.1.2 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ - --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba -more-itertools==10.3.0 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:e5d93ef411224fbcef366a6e8ddc4c5781bc6359d43412a65dd5964e46111463 \ - --hash=sha256:ea6a02e24a9161e51faad17a8782b92a0df82c12c1c8886fec7f0c3fa1a1b320 -nh3==0.2.18 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:0411beb0589eacb6734f28d5497ca2ed379eafab8ad8c84b31bb5c34072b7164 \ - --hash=sha256:14c5a72e9fe82aea5fe3072116ad4661af5cf8e8ff8fc5ad3450f123e4925e86 \ - --hash=sha256:19aaba96e0f795bd0a6c56291495ff59364f4300d4a39b29a0abc9cb3774a84b \ - --hash=sha256:34c03fa78e328c691f982b7c03d4423bdfd7da69cd707fe572f544cf74ac23ad \ - --hash=sha256:36c95d4b70530b320b365659bb5034341316e6a9b30f0b25fa9c9eff4c27a204 \ - --hash=sha256:3a157ab149e591bb638a55c8c6bcb8cdb559c8b12c13a8affaba6cedfe51713a \ - --hash=sha256:42c64511469005058cd17cc1537578eac40ae9f7200bedcfd1fc1a05f4f8c200 \ - --hash=sha256:5f36b271dae35c465ef5e9090e1fdaba4a60a56f0bb0ba03e0932a66f28b9189 \ - --hash=sha256:6955369e4d9f48f41e3f238a9e60f9410645db7e07435e62c6a9ea6135a4907f \ - --hash=sha256:7b7c2a3c9eb1a827d42539aa64091640bd275b81e097cd1d8d82ef91ffa2e811 \ - --hash=sha256:8ce0f819d2f1933953fca255db2471ad58184a60508f03e6285e5114b6254844 \ - --hash=sha256:94a166927e53972a9698af9542ace4e38b9de50c34352b962f4d9a7d4c927af4 \ - --hash=sha256:a7f1b5b2c15866f2db413a3649a8fe4fd7b428ae58be2c0f6bca5eefd53ca2be \ - --hash=sha256:c8b3a1cebcba9b3669ed1a84cc65bf005728d2f0bc1ed2a6594a992e817f3a50 \ - --hash=sha256:de3ceed6e661954871d6cd78b410213bdcb136f79aafe22aa7182e028b8c7307 \ - --hash=sha256:f0eca9ca8628dbb4e916ae2491d72957fdd35f7a5d326b7032a345f111ac07fe -packaging==24.1 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ - --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 -pkginfo==1.10.0 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297 \ - --hash=sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097 -pycparser==2.22 ; python_version >= "3.12" and python_version < "4.0" and sys_platform == "linux" and platform_python_implementation != "PyPy" \ - --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \ - --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc -pygments==2.18.0 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199 \ - --hash=sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a -python-dateutil==2.9.0.post0 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ - --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 -python-sage-imap==0.4.5 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:17ee91f3ad69ee2d752ef003515010dd8711b447de7116b1f192b278e1ffa1c4 \ - --hash=sha256:2a806f7e577fecf612d96c9021baa78920ff362136c290de9c08408f44b241be -pywin32-ctypes==0.2.2 ; python_version >= "3.12" and python_version < "4.0" and sys_platform == "win32" \ - --hash=sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60 \ - --hash=sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7 -readme-renderer==43.0 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:1818dd28140813509eeed8d62687f7cd4f7bad90d4db586001c5dc09d4fde311 \ - --hash=sha256:19db308d86ecd60e5affa3b2a98f017af384678c63c88e5d4556a380e674f3f9 -requests-toolbelt==1.0.0 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6 \ - --hash=sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06 -requests==2.32.3 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ - --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 -rfc3986==2.0.0 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \ - --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c -rich==13.7.1 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222 \ - --hash=sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432 -secretstorage==3.3.3 ; python_version >= "3.12" and python_version < "4.0" and sys_platform == "linux" \ - --hash=sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77 \ - --hash=sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99 -setuptools==70.3.0 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5 \ - --hash=sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc -six==1.16.0 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ - --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 -snowballstemmer==2.2.0 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \ - --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a -sphinx-rtd-theme==2.0.0 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:bd5d7b80622406762073a04ef8fadc5f9151261563d47027de09910ce03afe6b \ - --hash=sha256:ec93d0856dc280cf3aee9a4c9807c60e027c7f7b461b77aeffed682e68f0e586 -sphinx==7.4.7 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe \ - --hash=sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239 -sphinxcontrib-applehelp==1.0.8 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:c40a4f96f3776c4393d933412053962fac2b84f4c99a7982ba42e09576a70619 \ - --hash=sha256:cb61eb0ec1b61f349e5cc36b2028e9e7ca765be05e49641c97241274753067b4 -sphinxcontrib-devhelp==1.0.6 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:6485d09629944511c893fa11355bda18b742b83a2b181f9a009f7e500595c90f \ - --hash=sha256:9893fd3f90506bc4b97bdb977ceb8fbd823989f4316b28c3841ec128544372d3 -sphinxcontrib-htmlhelp==2.0.6 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:1b9af5a2671a61410a868fce050cab7ca393c218e6205cbc7f590136f207395c \ - --hash=sha256:c6597da06185f0e3b4dc952777a04200611ef563882e0c244d27a15ee22afa73 -sphinxcontrib-jquery==4.1 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a \ - --hash=sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae -sphinxcontrib-jsmath==1.0.1 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \ - --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8 -sphinxcontrib-qthelp==1.0.8 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:323d6acc4189af76dfe94edd2a27d458902319b60fcca2aeef3b2180c106a75f \ - --hash=sha256:db3f8fa10789c7a8e76d173c23364bdf0ebcd9449969a9e6a3dd31b8b7469f03 -sphinxcontrib-serializinghtml==1.1.10 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:326369b8df80a7d2d8d7f99aa5ac577f51ea51556ed974e7716cfd4fca3f6cb7 \ - --hash=sha256:93f3f5dc458b91b192fe10c397e324f262cf163d79f3282c158e8436a2c4511f -sqlparse==0.5.1 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4 \ - --hash=sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e -twine==5.1.1 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997 \ - --hash=sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db -tzdata==2024.1 ; python_version >= "3.12" and python_version < "4.0" and sys_platform == "win32" \ - --hash=sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd \ - --hash=sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252 -urllib3==2.2.2 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472 \ - --hash=sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168 -wheel==0.43.0 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:465ef92c69fa5c5da2d1cf8ac40559a8c940886afcef87dcf14b9470862f1d85 \ - --hash=sha256:55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81 -zipp==3.19.2 ; python_version >= "3.12" and python_version < "4.0" \ - --hash=sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19 \ - --hash=sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 0000000..71216e8 --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,103 @@ +alabaster==0.7.16 ; python_version >= "3.12" and python_version < "4.0" +argcomplete==3.4.0 ; python_version >= "3.12" and python_version < "4.0" +asgiref==3.8.1 ; python_version >= "3.12" and python_version < "4.0" +astroid==3.2.4 ; python_version >= "3.12" and python_version < "4.0" +babel==2.15.0 ; python_version >= "3.12" and python_version < "4.0" +black==24.4.2 ; python_version >= "3.12" and python_version < "4.0" +cachetools==5.4.0 ; python_version >= "3.12" and python_version < "4.0" +certifi==2024.7.4 ; python_version >= "3.12" and python_version < "4.0" +cffi==1.16.0 ; python_version >= "3.12" and python_version < "4.0" and sys_platform == "linux" and platform_python_implementation != "PyPy" +cfgv==3.4.0 ; python_version >= "3.12" and python_version < "4.0" +chardet==5.2.0 ; python_version >= "3.12" and python_version < "4.0" +charset-normalizer==3.3.2 ; python_version >= "3.12" and python_version < "4.0" +click==8.1.7 ; python_version >= "3.12" and python_version < "4.0" +colorama==0.4.6 ; python_version >= "3.12" and python_version < "4.0" +commitizen==3.28.0 ; python_version >= "3.12" and python_version < "4.0" +coverage==7.6.0 ; python_version >= "3.12" and python_version < "4.0" +coverage[toml]==7.6.0 ; python_version >= "3.12" and python_version < "4.0" +cryptography==43.0.0 ; python_version >= "3.12" and python_version < "4.0" and sys_platform == "linux" +decli==0.6.2 ; python_version >= "3.12" and python_version < "4.0" +dill==0.3.8 ; python_version >= "3.12" and python_version < "4.0" +distlib==0.3.8 ; python_version >= "3.12" and python_version < "4.0" +django-autoslug==1.9.9 ; python_version >= "3.12" and python_version < "4.0" +django-jsonform==2.22.0 ; python_version >= "3.12" and python_version < "4.0" +django-stubs-ext==5.0.2 ; python_version >= "3.12" and python_version < "4.0" +django-stubs==5.0.2 ; python_version >= "3.12" and python_version < "4.0" +django==5.0.7 ; python_version >= "3.12" and python_version < "4.0" +docformatter==1.7.5 ; python_version >= "3.12" and python_version < "4.0" +docutils==0.20.1 ; python_version >= "3.12" and python_version < "4.0" +filelock==3.15.4 ; python_version >= "3.12" and python_version < "4.0" +identify==2.6.0 ; python_version >= "3.12" and python_version < "4.0" +idna==3.7 ; python_version >= "3.12" and python_version < "4.0" +imagesize==1.4.1 ; python_version >= "3.12" and python_version < "4.0" +importlib-metadata==8.2.0 ; python_version >= "3.12" and python_version < "4.0" +iniconfig==2.0.0 ; python_version >= "3.12" and python_version < "4.0" +isort==5.13.2 ; python_version >= "3.12" and python_version < "4.0" +jaraco-classes==3.4.0 ; python_version >= "3.12" and python_version < "4.0" +jaraco-context==5.3.0 ; python_version >= "3.12" and python_version < "4.0" +jaraco-functools==4.0.1 ; python_version >= "3.12" and python_version < "4.0" +jeepney==0.8.0 ; python_version >= "3.12" and python_version < "4.0" and sys_platform == "linux" +jinja2==3.1.4 ; python_version >= "3.12" and python_version < "4.0" +keyring==25.2.1 ; python_version >= "3.12" and python_version < "4.0" +markdown-it-py==3.0.0 ; python_version >= "3.12" and python_version < "4.0" +markupsafe==2.1.5 ; python_version >= "3.12" and python_version < "4.0" +mccabe==0.7.0 ; python_version >= "3.12" and python_version < "4.0" +mdurl==0.1.2 ; python_version >= "3.12" and python_version < "4.0" +more-itertools==10.3.0 ; python_version >= "3.12" and python_version < "4.0" +mypy-extensions==1.0.0 ; python_version >= "3.12" and python_version < "4.0" +mypy==1.11.0 ; python_version >= "3.12" and python_version < "4.0" +nh3==0.2.18 ; python_version >= "3.12" and python_version < "4.0" +nodeenv==1.9.1 ; python_version >= "3.12" and python_version < "4.0" +packaging==24.1 ; python_version >= "3.12" and python_version < "4.0" +pathspec==0.12.1 ; python_version >= "3.12" and python_version < "4.0" +pkginfo==1.10.0 ; python_version >= "3.12" and python_version < "4.0" +platformdirs==4.2.2 ; python_version >= "3.12" and python_version < "4.0" +pluggy==1.5.0 ; python_version >= "3.12" and python_version < "4.0" +pre-commit==3.7.1 ; python_version >= "3.12" and python_version < "4.0" +prompt-toolkit==3.0.36 ; python_version >= "3.12" and python_version < "4.0" +pycparser==2.22 ; python_version >= "3.12" and python_version < "4.0" and sys_platform == "linux" and platform_python_implementation != "PyPy" +pygments==2.18.0 ; python_version >= "3.12" and python_version < "4.0" +pylint-django==2.5.5 ; python_version >= "3.12" and python_version < "4.0" +pylint-plugin-utils==0.8.2 ; python_version >= "3.12" and python_version < "4.0" +pylint==3.2.6 ; python_version >= "3.12" and python_version < "4.0" +pyproject-api==1.7.1 ; python_version >= "3.12" and python_version < "4.0" +pytest-cov==5.0.0 ; python_version >= "3.12" and python_version < "4.0" +pytest-django==4.8.0 ; python_version >= "3.12" and python_version < "4.0" +pytest==8.3.2 ; python_version >= "3.12" and python_version < "4.0" +python-dateutil==2.9.0.post0 ; python_version >= "3.12" and python_version < "4.0" +python-sage-imap==0.4.5 ; python_version >= "3.12" and python_version < "4.0" +pywin32-ctypes==0.2.2 ; python_version >= "3.12" and python_version < "4.0" and sys_platform == "win32" +pyyaml==6.0.1 ; python_version >= "3.12" and python_version < "4.0" +questionary==2.0.1 ; python_version >= "3.12" and python_version < "4.0" +readme-renderer==43.0 ; python_version >= "3.12" and python_version < "4.0" +requests-toolbelt==1.0.0 ; python_version >= "3.12" and python_version < "4.0" +requests==2.32.3 ; python_version >= "3.12" and python_version < "4.0" +rfc3986==2.0.0 ; python_version >= "3.12" and python_version < "4.0" +rich==13.7.1 ; python_version >= "3.12" and python_version < "4.0" +secretstorage==3.3.3 ; python_version >= "3.12" and python_version < "4.0" and sys_platform == "linux" +setuptools==70.3.0 ; python_version >= "3.12" and python_version < "4.0" +six==1.16.0 ; python_version >= "3.12" and python_version < "4.0" +snowballstemmer==2.2.0 ; python_version >= "3.12" and python_version < "4.0" +sphinx-rtd-theme==2.0.0 ; python_version >= "3.12" and python_version < "4.0" +sphinx==7.4.7 ; python_version >= "3.12" and python_version < "4.0" +sphinxcontrib-applehelp==1.0.8 ; python_version >= "3.12" and python_version < "4.0" +sphinxcontrib-devhelp==1.0.6 ; python_version >= "3.12" and python_version < "4.0" +sphinxcontrib-htmlhelp==2.0.6 ; python_version >= "3.12" and python_version < "4.0" +sphinxcontrib-jquery==4.1 ; python_version >= "3.12" and python_version < "4.0" +sphinxcontrib-jsmath==1.0.1 ; python_version >= "3.12" and python_version < "4.0" +sphinxcontrib-qthelp==1.0.8 ; python_version >= "3.12" and python_version < "4.0" +sphinxcontrib-serializinghtml==1.1.10 ; python_version >= "3.12" and python_version < "4.0" +sqlparse==0.5.1 ; python_version >= "3.12" and python_version < "4.0" +termcolor==2.4.0 ; python_version >= "3.12" and python_version < "4.0" +tomlkit==0.13.0 ; python_version >= "3.12" and python_version < "4.0" +tox==4.16.0 ; python_version >= "3.12" and python_version < "4.0" +twine==5.1.1 ; python_version >= "3.12" and python_version < "4.0" +types-pyyaml==6.0.12.20240724 ; python_version >= "3.12" and python_version < "4.0" +typing-extensions==4.12.2 ; python_version >= "3.12" and python_version < "4.0" +tzdata==2024.1 ; python_version >= "3.12" and python_version < "4.0" and sys_platform == "win32" +untokenize==0.1.1 ; python_version >= "3.12" and python_version < "4.0" +urllib3==2.2.2 ; python_version >= "3.12" and python_version < "4.0" +virtualenv==20.26.3 ; python_version >= "3.12" and python_version < "4.0" +wcwidth==0.2.13 ; python_version >= "3.12" and python_version < "4.0" +wheel==0.43.0 ; python_version >= "3.12" and python_version < "4.0" +zipp==3.19.2 ; python_version >= "3.12" and python_version < "4.0" diff --git a/requirements/prod.txt b/requirements/prod.txt new file mode 100644 index 0000000..0ef19e8 --- /dev/null +++ b/requirements/prod.txt @@ -0,0 +1,57 @@ +alabaster==0.7.16 ; python_version >= "3.12" and python_version < "4.0" +asgiref==3.8.1 ; python_version >= "3.12" and python_version < "4.0" +babel==2.15.0 ; python_version >= "3.12" and python_version < "4.0" +certifi==2024.7.4 ; python_version >= "3.12" and python_version < "4.0" +cffi==1.16.0 ; python_version >= "3.12" and python_version < "4.0" and sys_platform == "linux" and platform_python_implementation != "PyPy" +charset-normalizer==3.3.2 ; python_version >= "3.12" and python_version < "4.0" +colorama==0.4.6 ; python_version >= "3.12" and python_version < "4.0" and sys_platform == "win32" +cryptography==43.0.0 ; python_version >= "3.12" and python_version < "4.0" and sys_platform == "linux" +django-autoslug==1.9.9 ; python_version >= "3.12" and python_version < "4.0" +django-jsonform==2.22.0 ; python_version >= "3.12" and python_version < "4.0" +django==5.0.7 ; python_version >= "3.12" and python_version < "4.0" +docutils==0.20.1 ; python_version >= "3.12" and python_version < "4.0" +idna==3.7 ; python_version >= "3.12" and python_version < "4.0" +imagesize==1.4.1 ; python_version >= "3.12" and python_version < "4.0" +importlib-metadata==8.2.0 ; python_version >= "3.12" and python_version < "4.0" +jaraco-classes==3.4.0 ; python_version >= "3.12" and python_version < "4.0" +jaraco-context==5.3.0 ; python_version >= "3.12" and python_version < "4.0" +jaraco-functools==4.0.1 ; python_version >= "3.12" and python_version < "4.0" +jeepney==0.8.0 ; python_version >= "3.12" and python_version < "4.0" and sys_platform == "linux" +jinja2==3.1.4 ; python_version >= "3.12" and python_version < "4.0" +keyring==25.2.1 ; python_version >= "3.12" and python_version < "4.0" +markdown-it-py==3.0.0 ; python_version >= "3.12" and python_version < "4.0" +markupsafe==2.1.5 ; python_version >= "3.12" and python_version < "4.0" +mdurl==0.1.2 ; python_version >= "3.12" and python_version < "4.0" +more-itertools==10.3.0 ; python_version >= "3.12" and python_version < "4.0" +nh3==0.2.18 ; python_version >= "3.12" and python_version < "4.0" +packaging==24.1 ; python_version >= "3.12" and python_version < "4.0" +pkginfo==1.10.0 ; python_version >= "3.12" and python_version < "4.0" +pycparser==2.22 ; python_version >= "3.12" and python_version < "4.0" and sys_platform == "linux" and platform_python_implementation != "PyPy" +pygments==2.18.0 ; python_version >= "3.12" and python_version < "4.0" +python-dateutil==2.9.0.post0 ; python_version >= "3.12" and python_version < "4.0" +python-sage-imap==0.4.5 ; python_version >= "3.12" and python_version < "4.0" +pywin32-ctypes==0.2.2 ; python_version >= "3.12" and python_version < "4.0" and sys_platform == "win32" +readme-renderer==43.0 ; python_version >= "3.12" and python_version < "4.0" +requests-toolbelt==1.0.0 ; python_version >= "3.12" and python_version < "4.0" +requests==2.32.3 ; python_version >= "3.12" and python_version < "4.0" +rfc3986==2.0.0 ; python_version >= "3.12" and python_version < "4.0" +rich==13.7.1 ; python_version >= "3.12" and python_version < "4.0" +secretstorage==3.3.3 ; python_version >= "3.12" and python_version < "4.0" and sys_platform == "linux" +setuptools==70.3.0 ; python_version >= "3.12" and python_version < "4.0" +six==1.16.0 ; python_version >= "3.12" and python_version < "4.0" +snowballstemmer==2.2.0 ; python_version >= "3.12" and python_version < "4.0" +sphinx-rtd-theme==2.0.0 ; python_version >= "3.12" and python_version < "4.0" +sphinx==7.4.7 ; python_version >= "3.12" and python_version < "4.0" +sphinxcontrib-applehelp==1.0.8 ; python_version >= "3.12" and python_version < "4.0" +sphinxcontrib-devhelp==1.0.6 ; python_version >= "3.12" and python_version < "4.0" +sphinxcontrib-htmlhelp==2.0.6 ; python_version >= "3.12" and python_version < "4.0" +sphinxcontrib-jquery==4.1 ; python_version >= "3.12" and python_version < "4.0" +sphinxcontrib-jsmath==1.0.1 ; python_version >= "3.12" and python_version < "4.0" +sphinxcontrib-qthelp==1.0.8 ; python_version >= "3.12" and python_version < "4.0" +sphinxcontrib-serializinghtml==1.1.10 ; python_version >= "3.12" and python_version < "4.0" +sqlparse==0.5.1 ; python_version >= "3.12" and python_version < "4.0" +twine==5.1.1 ; python_version >= "3.12" and python_version < "4.0" +tzdata==2024.1 ; python_version >= "3.12" and python_version < "4.0" and sys_platform == "win32" +urllib3==2.2.2 ; python_version >= "3.12" and python_version < "4.0" +wheel==0.43.0 ; python_version >= "3.12" and python_version < "4.0" +zipp==3.19.2 ; python_version >= "3.12" and python_version < "4.0" diff --git a/sage_mailbox/admin/actions/email.py b/sage_mailbox/admin/actions/email.py index aad43a4..168220a 100644 --- a/sage_mailbox/admin/actions/email.py +++ b/sage_mailbox/admin/actions/email.py @@ -17,7 +17,6 @@ logger = logging.getLogger(__name__) - @admin.action(description=_("Move selected emails to trash")) @transaction.atomic def move_to_trash(modeladmin, request, queryset): @@ -39,7 +38,7 @@ def move_to_trash(modeladmin, request, queryset): MessageSet(str(email.uid)), trash_mailbox.name ) emails_to_delete.append(email) - logger.debug(f"Moved email with UID {email.uid} to trash.") + logger.debug("Moved email with UID %s to trash.", email.uid) queryset.model.objects.filter( id__in=[email.id for email in emails_to_delete] @@ -51,18 +50,22 @@ def move_to_trash(modeladmin, request, queryset): messages.success( request, _( - "Successfully moved {} emails to trash and deleted them from the database in {:.2f} seconds." + "Successfully moved {} emails to trash and deleted them " + "from the database in {:.2f} seconds." ).format(len(emails_to_delete), runtime), ) - except Exception as e: + except Exception as exc: end_time = time.time() runtime = end_time - start_time - logger.error(f"Error moving email messages to trash: {str(e)}", exc_info=True) + logger.error( + "Error moving email messages to trash: %s", str(exc), exc_info=True + ) messages.error( request, _( - "Failed to move some emails to trash. Please try again. Task completed in {:.2f} seconds." + "Failed to move some emails to trash. Please try again. " + "Task completed in {:.2f} seconds." ).format(runtime), ) transaction.set_rollback(True) @@ -88,15 +91,16 @@ def mark_as_read(modeladmin, request, queryset): status, data = client.uid( "STORE", str(email_message.uid), "+FLAGS", "\\Seen" ) - logger.debug(f"Marked email with UID {email_message.uid} as SEEN.") + logger.debug("Marked email with UID %s as SEEN.", email_message.uid) email_message.is_read = True email_messages_to_update.append(email_message) logger.debug( - f"Prepared to mark email message object {email_message.pk} as read in the database." + "Prepared to mark email message " + "object %s as read in the database.", + email_message.pk, ) - # Bulk update all email messages at once queryset.model.objects.bulk_update(email_messages_to_update, ["is_read"]) logger.debug("Bulk updated all email messages to read status in the database.") @@ -109,14 +113,17 @@ def mark_as_read(modeladmin, request, queryset): ), ) - except Exception as e: + except Exception as exc: end_time = time.time() runtime = end_time - start_time - logger.error(f"Error marking email messages as read: {str(e)}", exc_info=True) + logger.error( + "Error marking email messages as read: %s", str(exc), exc_info=True + ) messages.error( request, _( - "Failed to mark some emails as read. Please try again. Task completed in {:.2f} seconds." + "Failed to mark some emails as read. Please try again. " + "Task completed in {:.2f} seconds." ).format(runtime), ) transaction.set_rollback(True) @@ -133,7 +140,6 @@ def mark_as_unread(modeladmin, request, queryset): password = settings.IMAP_SERVER_PASSWORD try: - with IMAPClient(host, username, password) as client: mailbox = IMAPMailboxUIDService(client) @@ -145,16 +151,17 @@ def mark_as_unread(modeladmin, request, queryset): "STORE", str(email_message.uid), FlagCommand.REMOVE, Flag.SEEN ) logger.debug( - f"Unmarked email with Message-ID {email_message.uid} as SEEN." + "Unmarked email with Message-ID %s as SEEN.", email_message.uid ) email_message.is_read = False email_messages_to_update.append(email_message) logger.debug( - f"Prepared to mark email message object {email_message.pk} as unread in the database." + "Prepared to mark email message " + "object %s as unread in the database.", + email_message.pk, ) - # Bulk update all email messages at once queryset.model.objects.bulk_update(email_messages_to_update, ["is_read"]) logger.debug( "Bulk updated all email messages to unread status in the database." @@ -169,14 +176,17 @@ def mark_as_unread(modeladmin, request, queryset): ), ) - except Exception as e: + except Exception as exc: end_time = time.time() runtime = end_time - start_time - logger.error(f"Error marking email messages as unread: {str(e)}", exc_info=True) + logger.error( + "Error marking email messages as unread: %s", str(exc), exc_info=True + ) messages.error( request, _( - "Failed to mark some emails as unread. Please try again. Task completed in {:.2f} seconds." + "Failed to mark some emails as unread. Please try again. " + "Task completed in {:.2f} seconds." ).format(runtime), ) transaction.set_rollback(True) @@ -203,16 +213,17 @@ def mark_as_flagged(modeladmin, request, queryset): "STORE", str(email_message.uid), "+FLAGS", "\\Flagged" ) logger.debug( - f"Marked email with UID {email_message.uid} as FLAGGED." + "Marked email with UID %s as FLAGGED.", email_message.uid ) email_message.is_flagged = True email_messages_to_update.append(email_message) logger.debug( - f"Prepared to mark email message object {email_message.pk} as flagged in the database." + "Prepared to mark email message " + "object %s as flagged in the database.", + email_message.pk, ) - # Bulk update all email messages at once queryset.model.objects.bulk_update(email_messages_to_update, ["is_flagged"]) logger.debug( "Bulk updated all email messages to flagged status in the database." @@ -227,16 +238,17 @@ def mark_as_flagged(modeladmin, request, queryset): ), ) - except Exception as e: + except Exception as exc: end_time = time.time() runtime = end_time - start_time logger.error( - f"Error marking email messages as flagged: {str(e)}", exc_info=True + "Error marking email messages as flagged: %s", str(exc), exc_info=True ) messages.error( request, _( - "Failed to mark some emails as flagged. Please try again. Task completed in {:.2f} seconds." + "Failed to mark some emails as flagged. Please try again. " + "Task completed in {:.2f} seconds." ).format(runtime), ) transaction.set_rollback(True) @@ -253,7 +265,6 @@ def mark_as_unflagged(modeladmin, request, queryset): password = settings.IMAP_SERVER_PASSWORD try: - with IMAPClient(host, username, password) as client: mailbox = IMAPMailboxUIDService(client) @@ -264,16 +275,17 @@ def mark_as_unflagged(modeladmin, request, queryset): "STORE", str(email_message.uid), "-FLAGS", "\\Flagged" ) logger.debug( - f"Unmarked email with UID {email_message.uid} as FLAGGED." + "Unmarked email with UID %s as FLAGGED.", email_message.uid ) email_message.is_flagged = False email_messages_to_update.append(email_message) logger.debug( - f"Prepared to mark email message object {email_message.pk} as unflagged in the database." + "Prepared to mark email message " + "object %s as unflagged in the database.", + email_message.pk, ) - # Bulk update all email messages at once queryset.model.objects.bulk_update(email_messages_to_update, ["is_flagged"]) logger.debug( "Bulk updated all email messages to unflagged status in the database." @@ -288,16 +300,17 @@ def mark_as_unflagged(modeladmin, request, queryset): ), ) - except Exception as e: + except Exception as exc: end_time = time.time() runtime = end_time - start_time logger.error( - f"Error marking email messages as unflagged: {str(e)}", exc_info=True + "Error marking email messages as unflagged: %s", str(exc), exc_info=True ) messages.error( request, _( - "Failed to mark some emails as unflagged. Please try again. Task completed in {:.2f} seconds." + "Failed to mark some emails as unflagged. Please try again. " + "Task completed in {:.2f} seconds." ).format(runtime), ) transaction.set_rollback(True) @@ -308,30 +321,26 @@ def download_as_eml(modeladmin, request, queryset): start_time = time.time() email_files = [] - host = settings.IMAP_SERVER_DOMAIN - username = settings.IMAP_SERVER_USER - password = settings.IMAP_SERVER_PASSWORD - try: for email_message in queryset: if email_message.raw: email_files.append((f"{email_message.uid}.eml", email_message.raw)) logger.debug( - f"Added raw data for email with Message-ID {email_message.uid}." + "Added raw data for email with Message-ID %s.", email_message.uid ) if len(email_files) == 1: response = HttpResponse(email_files[0][1], content_type="message/rfc822") - response["Content-Disposition"] = ( - f'attachment; filename="{email_files[0][0]}"' - ) - logger.debug(f"Prepared single EML file for download: {email_files[0][0]}") + response[ + "Content-Disposition" + ] = f'attachment; filename="{email_files[0][0]}"' + logger.debug("Prepared single EML file for download: %s", email_files[0][0]) else: zip_buffer = BytesIO() with zipfile.ZipFile(zip_buffer, "w") as zip_file: for file_name, eml_data in email_files: zip_file.writestr(file_name, eml_data) - logger.debug(f"Added {file_name} to zip file.") + logger.debug("Added %s to zip file.", file_name) zip_buffer.seek(0) response = HttpResponse(zip_buffer, content_type="application/zip") @@ -350,8 +359,8 @@ def download_as_eml(modeladmin, request, queryset): ) return response - except Exception as e: - logger.error(f"An error occurred while preparing the EML files: {str(e)}") + except Exception as exc: + logger.error("An error occurred while preparing the EML files: %s", str(exc)) messages.error(request, _("An error occurred while preparing the EML files.")) return None @@ -360,7 +369,7 @@ def download_as_eml(modeladmin, request, queryset): @transaction.atomic def restore_from_trash(modeladmin, request, queryset): start_time = time.time() - emails_to_delete = [] + emails_to_restore = [] host = settings.IMAP_SERVER_DOMAIN username = settings.IMAP_SERVER_USER @@ -377,11 +386,11 @@ def restore_from_trash(modeladmin, request, queryset): imap_mailbox_service.uid_restore( MessageSet(str(email.uid)), trash_mailbox.name, inbox_mailbox.name ) - emails_to_delete.append(email) - logger.debug(f"Restored email with UID {email.uid} to inbox.") + emails_to_restore.append(email) + logger.debug("Restored email with UID %s to inbox.", email.uid) queryset.model.objects.filter( - id__in=[email.id for email in emails_to_delete] + id__in=[email.id for email in emails_to_restore] ).delete() logger.debug("Deleted restored email messages from the database.") @@ -390,20 +399,22 @@ def restore_from_trash(modeladmin, request, queryset): messages.success( request, _( - "Successfully restored emails to inbox and deleted {} emails from trash in {:.2f} seconds." - ).format(len(emails_to_delete), runtime), + "Successfully restored {} emails to inbox and deleted " + "them from trash in {:.2f} seconds." + ).format(len(emails_to_restore), runtime), ) - except Exception as e: + except Exception as exc: end_time = time.time() runtime = end_time - start_time logger.error( - f"Error restoring email messages from trash: {str(e)}", exc_info=True + "Error restoring email messages from trash: %s", str(exc), exc_info=True ) messages.error( request, _( - "Failed to restore and delete some emails from trash. Please try again. Task completed in {:.2f} seconds." + "Failed to restore and delete some emails from trash. " + "Please try again. Task completed in {:.2f} seconds." ).format(runtime), ) transaction.set_rollback(True) diff --git a/sage_mailbox/admin/email.py b/sage_mailbox/admin/email.py index 6290821..dad1321 100644 --- a/sage_mailbox/admin/email.py +++ b/sage_mailbox/admin/email.py @@ -1,12 +1,17 @@ -from collections import OrderedDict +import email import logging +import mimetypes import time +from collections import OrderedDict from datetime import datetime, timedelta +from email.utils import make_msgid from typing import Any from django.conf import settings from django.contrib import admin, messages +from django.contrib.sites.models import Site from django.core.exceptions import ObjectDoesNotExist +from django.core.mail import EmailMultiAlternatives from django.db import transaction from django.http import HttpRequest, HttpResponseRedirect from django.shortcuts import redirect @@ -14,11 +19,12 @@ from django.urls.resolvers import URLPattern from django.utils.html import format_html from django.utils.safestring import mark_safe +from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ from sage_imap.helpers.enums import Flag, FlagCommand from sage_imap.helpers.search import IMAPSearchCriteria from sage_imap.models.message import MessageSet -from sage_imap.services import IMAPClient, IMAPMailboxUIDService +from sage_imap.services import IMAPClient, IMAPMailboxService, IMAPMailboxUIDService from sage_mailbox.admin.actions import ( download_as_eml, @@ -35,9 +41,9 @@ logger = logging.getLogger(__name__) -imap_host = getattr(settings, 'IMAP_SERVER_DOMAIN', None) -imap_username = getattr(settings, 'IMAP_SERVER_USER', None) -imap_password = getattr(settings, 'IMAP_SERVER_PASSWORD', None) +imap_host = getattr(settings, "IMAP_SERVER_DOMAIN", None) +imap_username = getattr(settings, "IMAP_SERVER_USER", None) +imap_password = getattr(settings, "IMAP_SERVER_PASSWORD", None) class AttachmentInline(admin.TabularInline): @@ -266,21 +272,23 @@ def get_urls(self) -> list[URLPattern]: def sync_emails(self, request): start_time = time.time() - mailbox_name = Mailbox.objects.get( - folder_type=self.mailbox_type - ).folder_type + mailbox_name = Mailbox.objects.get(folder_type=self.mailbox_type).folder_type try: # Try to get the mailbox mailbox = Mailbox.objects.get(folder_type=mailbox_name) except ObjectDoesNotExist: # If the mailbox does not exist, show a user-friendly message - message = "The INBOX mailbox does not exist. Please sync mailboxes first, then sync emails." + message = ( + "The INBOX mailbox does not exist. Please sync mailboxes first, " + "then sync emails." + ) messages.add_message(request, messages.WARNING, message) # Redirect to the admin change list URL change_list_url = reverse( - f"admin:{self.model._meta.app_label}_{self.model._meta.model_name}_changelist" + f"admin:{self.model._meta.app_label}_" + f"{self.model._meta.model_name}_changelist" ) return redirect(change_list_url) @@ -295,14 +303,18 @@ def sync_emails(self, request): created_attachments = result.get("created_attachments", 0) # Create a message to display in the admin interface - message = f"Email synchronization completed: {created_emails} emails and {created_attachments} attachments created in {runtime:.2f} seconds." + message = ( + f"Email synchronization completed: {created_emails} emails and " + f"{created_attachments} attachments created in {runtime:.2f} seconds." + ) # Set the message in Django's messages framework messages.add_message(request, messages.INFO, message) # Redirect to the change list URL change_list_url = reverse( - f"admin:{self.model._meta.app_label}_{self.model._meta.model_name}_changelist" + f"admin:{self.model._meta.app_label}_" + f"{self.model._meta.model_name}_changelist" ) return redirect(change_list_url) @@ -310,7 +322,6 @@ def change_view(self, request, object_id, form_url="", extra_context=None): email_message = self.get_object(request, object_id) if email_message and not email_message.is_read: try: - with IMAPClient(imap_host, imap_username, imap_password) as client: with IMAPMailboxUIDService(client) as mailbox: mailbox.select(email_message.mailbox.name) @@ -319,18 +330,22 @@ def change_view(self, request, object_id, form_url="", extra_context=None): "STORE", str(email_message.uid), "+FLAGS", "\\Seen" ) logger.debug( - f"Marked email with UID {email_message.uid} as SEEN on IMAP server." + "Marked email with UID %s as SEEN on IMAP server.", + email_message.uid, ) email_message.is_read = True email_message.save(update_fields=["is_read"]) logger.debug( - f"Marked email message object {email_message.pk} as read in the database." + "Marked email message object %s as read in the database.", + email_message.pk, ) messages.success(request, _("Email marked as read.")) except Exception as e: logger.error( - f"Error marking email message {email_message.pk} as read: {str(e)}", + "Error marking email message %s as read: %s", + email_message.pk, + str(e), exc_info=True, ) messages.error( @@ -366,6 +381,96 @@ def get_urls(self) -> list[URLPattern]: total = custom_urls + urls return total + # TODO: implement send mail functionality only on save not update + # def save_model(self, request, obj, form, change): + # """ + # This method is called when saving an EmailMessage in the admin interface. + + # It ensures that the email is only sent when the object is created, not updated. + # """ + # is_new = not change # True if this is a new object + + # super().save_model(request, obj, form, change) # Save the object first + + # if is_new: + # # Trigger the email sending process only if the object is new + # self.send_email(obj) + + # def send_email(self, email_message): + # current_site = Site.objects.get_current() + # # Generate a Message-ID if not present + # if not email_message.message_id: + # email_message.message_id = make_msgid(domain=current_site.domain) + + # # Ensure the date is set + # if not email_message.date: + # email_message.date = now() + + # # Create the email message + # subject = email_message.subject + # from_email = email_message.from_address + # to = email_message.to_address.split(",") + # cc = email_message.cc_address.split(",") if email_message.cc_address else [] + # bcc = email_message.bcc_address.split(",") if email_message.bcc_address else [] + + # msg = EmailMultiAlternatives( + # subject=subject, + # body=email_message.plain_body, + # from_email=from_email, + # to=to, + # cc=cc, + # bcc=bcc, + # ) + + # # Check if the body contains HTML content + # if email_message.html_body: + # msg.attach_alternative(email_message.html_body, "text/html") + + # # Attach any files + # for attachment in email_message.attachments.all(): + # file_content = attachment.file.read() + # mime_type, _ = mimetypes.guess_type(attachment.filename) + # if not mime_type: + # # Default to binary stream if MIME type can't be guessed + # mime_type = "application/octet-stream" + # msg.attach(attachment.filename, file_content, mime_type) + + # # Additional headers + # msg.extra_headers = { + # "Message-ID": email_message.message_id, + # "X-MS-Has-Attach": "yes" if email_message.has_attachments() else "no", + # "X-Priority": "3", + # "X-Auto-Response-Suppress": "All", + # "MIME-Version": "1.0", + # "Content-Type": "multipart/mixed", + # } + + # # Send the email + # msg.send() + + # # Save raw email to IMAP Sent folder and get raw email data + # raw_email = msg.message().as_string() + # email_message.raw = raw_email.encode("utf-8") # Ensure raw email is bytes + + # with IMAPClient(imap_host, imap_username, imap_password) as client: + # with IMAPMailboxService(client) as mailbox: + # folder = Mailbox.objects.get(folder_type=StandardMailboxNames.SENT) + # mailbox.select(folder.name) + # mailbox.save_sent( + # email_message.raw, folder.name + # ) # Send raw email as bytes + + # # Save headers to email_message + # parsed_email = email.message_from_bytes(email_message.raw) + # headers = {k: v for k, v in parsed_email.items()} + # email_message.headers = headers + + # # Calculate and save email size + # email_message.size = len(email_message.raw) + + # # Save email message with updated fields + # email_message.save() + @admin.register(Junk) class JunkAdmin(EmailMessageAdmin): @@ -403,7 +508,6 @@ def get_urls(self) -> list[URLPattern]: @admin.register(Trash) class TrashAdmin(EmailMessageAdmin): - mailbox_type = StandardMailboxNames.TRASH actions = (download_as_eml, restore_from_trash) @@ -444,7 +548,6 @@ def get_urls(self) -> list[URLPattern]: def clear_trash(self, request: HttpRequest): start_time = time.time() try: - with IMAPClient(imap_host, imap_username, imap_password) as client: trash_mailbox = Mailbox.objects.get( folder_type=StandardMailboxNames.TRASH @@ -484,12 +587,13 @@ def clear_trash(self, request: HttpRequest): self.message_user( request, _( - "Failed to clear trash. Please try again. Task completed in {:.2f} seconds." + "Failed to clear trash. Please try again. " + "Task completed in {:.2f} seconds." ).format(runtime), messages.ERROR, ) - + application_label = self.model._meta.app_label change_list_url = reverse( - f"admin:{self.model._meta.app_label}_{self.model._meta.model_name}_changelist" + f"admin:{application_label}_{self.model._meta.model_name}_changelist" ) return HttpResponseRedirect(change_list_url) diff --git a/sage_mailbox/admin/mailbox.py b/sage_mailbox/admin/mailbox.py index 44fa2c6..09cdfed 100644 --- a/sage_mailbox/admin/mailbox.py +++ b/sage_mailbox/admin/mailbox.py @@ -9,7 +9,6 @@ from django.http.response import HttpResponse from django.urls import path, reverse from django.utils.translation import gettext_lazy as _ - from sage_imap.exceptions import ( IMAPFolderExistsError, IMAPFolderNotFoundError, @@ -21,17 +20,15 @@ from sage_mailbox.models.mailbox import Mailbox, StandardMailboxNames # IMAP configuration -imap_host = getattr(settings, 'IMAP_SERVER_DOMAIN', None) -imap_username = getattr(settings, 'IMAP_SERVER_USER', None) -imap_password = getattr(settings, 'IMAP_SERVER_PASSWORD', None) +imap_host = getattr(settings, "IMAP_SERVER_DOMAIN", None) +imap_username = getattr(settings, "IMAP_SERVER_USER", None) +imap_password = getattr(settings, "IMAP_SERVER_PASSWORD", None) @admin.register(Mailbox) class MailboxAdmin(admin.ModelAdmin): change_list_template = "admin/mailbox/change_list.html" - list_display = ( - "name", "slug", "folder_type", "created_at", "modified_at" - ) + list_display = ("name", "slug", "folder_type", "created_at", "modified_at") search_fields = ("name",) ordering = ("name",) list_per_page = 25 @@ -78,8 +75,7 @@ def save_model(self, request, obj, form, change): raise ValidationError(str(e)) super().save_model(request, obj, form, change) messages.success( - request, - f'The Mailbox "{new_name}" was added successfully.' + request, f'The Mailbox "{new_name}" was added successfully.' ) except ValidationError as e: messages.error(request, f"Error: {e.message}") @@ -109,7 +105,7 @@ def delete_model(self, request, obj): messages.warning( request, f'The Mailbox "{obj.name}" was deleted from admin, ' - 'but it did not exist on the IMAP server.' + "but it did not exist on the IMAP server.", ) except IMAPFolderOperationError as e: raise ValidationError(str(e)) @@ -118,7 +114,7 @@ def delete_model(self, request, obj): messages.success( request, f'The Mailbox "{obj.name}" was deleted successfully ' - 'from both admin and IMAP server.' + "from both admin and IMAP server.", ) except ValidationError as e: messages.error(request, f"Error: {e.message}") @@ -138,7 +134,7 @@ def delete_queryset(self, request, queryset): messages.warning( request, f'The mailbox "{obj.name}" was deleted from admin, ' - 'but it did not exist on the IMAP server.' + "but it did not exist on the IMAP server.", ) except IMAPFolderOperationError as e: raise ValidationError(str(e)) @@ -148,7 +144,7 @@ def delete_queryset(self, request, queryset): messages.success( request, "The selected Mailboxes were deleted successfully " - "from both admin and IMAP server." + "from both admin and IMAP server.", ) except ValidationError as e: messages.error(request, f"Error: {e.message}") @@ -168,7 +164,9 @@ def response_add(self, request, obj, post_url_continue=None): return HttpResponseRedirect(request.path) elif "_save" in request.POST: post_url = reverse( - "admin:%s_%s_changelist" % (self.opts.app_label, self.opts.model_name), + "admin:{}_{}_changelist".format( + self.opts.app_label, self.opts.model_name + ), current_app=self.admin_site.name, ) return HttpResponseRedirect(post_url) @@ -176,7 +174,8 @@ def response_add(self, request, obj, post_url_continue=None): def response_change(self, request, obj): """ - Handles the HTTP response after changing an existing object in the Django admin. + Handles the HTTP response after changing an existing object in the Django + admin. Similar to `response_add`, this method prevents the issue of double flash messages. """ @@ -188,7 +187,9 @@ def response_change(self, request, obj): return HttpResponseRedirect(request.path) elif "_save" in request.POST: post_url = reverse( - "admin:%s_%s_changelist" % (self.opts.app_label, self.opts.model_name), + "admin:{}_{}_changelist".format( + self.opts.app_label, self.opts.model_name + ), current_app=self.admin_site.name, ) return HttpResponseRedirect(post_url) @@ -198,7 +199,7 @@ def response_delete( self, request: HttpRequest, obj_display: str, obj_id: int ) -> HttpResponse: post_url = reverse( - "admin:%s_%s_changelist" % (self.opts.app_label, self.opts.model_name), + "admin:{}_{}_changelist".format(self.opts.app_label, self.opts.model_name), current_app=self.admin_site.name, ) return HttpResponseRedirect(post_url) @@ -207,17 +208,14 @@ def get_success_url(self, request, obj): return request.META.get("HTTP_REFERER", "/admin/") def sync_folders(self, request): - """ - Synchronizes the mailboxes between the IMAP server and the Django admin. - """ + """Synchronizes the mailboxes between the IMAP server and the Django admin.""" try: with IMAPClient(imap_host, imap_username, imap_password) as client: folder_service = IMAPFolderService(client) folders = folder_service.list_folders() for folder_name in folders: Mailbox.objects.update_or_create( - name=folder_name, - defaults={"slug": folder_name.lower()} + name=folder_name, defaults={"slug": folder_name.lower()} ) messages.success(request, "Mailboxes synchronized successfully.") except Exception as e: diff --git a/sage_mailbox/admin/mixins/email.py b/sage_mailbox/admin/mixins/email.py index 82eb4da..563e64f 100644 --- a/sage_mailbox/admin/mixins/email.py +++ b/sage_mailbox/admin/mixins/email.py @@ -24,7 +24,10 @@ def get_urls(self) -> list[URLPattern]: path( "sync-emails/", self.admin_site.admin_view(self.sync_emails), - name=f"{self.model._meta.app_label}_{self.model._meta.model_name}_sync", + name=( + f"{self.model._meta.app_label}_" + f"{self.model._meta.model_name}_sync" + ), ), ] return custom_urls + urls @@ -35,10 +38,14 @@ def sync_emails(self, request): try: mailbox = Mailbox.objects.get(folder_type=self.mailbox_name) except ObjectDoesNotExist: - message = f"The {self.mailbox_name} mailbox does not exist. Please sync mailboxes first, then sync emails." + message = ( + f"The {self.mailbox_name} mailbox does not exist. " + "Please sync mailboxes first, then sync emails." + ) messages.add_message(request, messages.WARNING, message) change_list_url = reverse( - f"admin:{self.model._meta.app_label}_{self.model._meta.model_name}_changelist" + f"admin:{self.model._meta.app_label}_" + f"{self.model._meta.model_name}_changelist" ) return redirect(change_list_url) @@ -51,10 +58,14 @@ def sync_emails(self, request): created_emails = result.get("created_emails", 0) created_attachments = result.get("created_attachments", 0) - message = f"Email synchronization completed: {created_emails} emails and {created_attachments} attachments created in {runtime:.2f} seconds." + message = ( + f"Email synchronization completed: {created_emails} emails and " + f"{created_attachments} attachments created in {runtime:.2f} seconds." + ) messages.add_message(request, messages.INFO, message) change_list_url = reverse( - f"admin:{self.model._meta.app_label}_{self.model._meta.model_name}_changelist" + f"admin:{self.model._meta.app_label}_" + f"{self.model._meta.model_name}_changelist" ) return redirect(change_list_url) diff --git a/sage_mailbox/apps.py b/sage_mailbox/apps.py index 18d8ed2..28d3a2b 100644 --- a/sage_mailbox/apps.py +++ b/sage_mailbox/apps.py @@ -9,4 +9,3 @@ class SageMailboxConfig(AppConfig): def ready(self): from . import checks - from . import signals diff --git a/sage_mailbox/checks.py b/sage_mailbox/checks.py index c65af24..c3072d8 100644 --- a/sage_mailbox/checks.py +++ b/sage_mailbox/checks.py @@ -1,7 +1,8 @@ import imaplib import logging -from django.core.checks import Error, register +from django.conf import settings +from django.core.checks import Error, Warning, register from sage_mailbox.conf import imap_settings from sage_mailbox.exc import ( @@ -17,11 +18,12 @@ @register() def check_imap_config(app_configs, **kwargs): """ - Check the IMAP configuration for the application. + Check the IMAP configuration and other required settings for the application. - This function verifies that all required IMAP settings are present and attempts to - establish a connection to the IMAP server to ensure the settings are correct. - Any errors encountered during these checks are returned. + This function verifies that all required IMAP settings, Django apps, and certain + settings are present. It also attempts to establish a connection to the IMAP server + to ensure the settings are correct. Any errors encountered during these checks + are returned. Parameters ---------- @@ -32,8 +34,9 @@ def check_imap_config(app_configs, **kwargs): Returns ------- - list of Error - A list of Error objects representing any configuration or connection errors found. + list of Error or Warning + A list of Error or Warning objects representing any configuration or connection + errors found. Raises ------ @@ -55,6 +58,45 @@ def check_imap_config(app_configs, **kwargs): """ errors = [] + # Check that required apps are installed + required_apps = ["django.contrib.sites", "django_jsonform"] + for app in required_apps: + if app not in settings.INSTALLED_APPS: + errors.append( + Error( + f"The required app '{app}' is not installed.", + id="sage_integration.E001", + ) + ) + + # Check that SITE_ID is set + if not hasattr(settings, "SITE_ID"): + errors.append( + Error( + "SITE_ID is not set in settings.", + id="sage_integration.E002", + ) + ) + elif not isinstance(settings.SITE_ID, int): + errors.append( + Error( + "SITE_ID must be an integer.", + id="sage_integration.E003", + ) + ) + + # Check that upload-related settings are set + upload_settings = ["MEDIA_URL", "MEDIA_ROOT", "FILE_UPLOAD_HANDLERS"] + for setting in upload_settings: + if not hasattr(settings, setting): + errors.append( + Error( + f"The required setting '{setting}' is not set.", + id="sage_integration.E008", + ) + ) + + # Function to get IMAP settings def get_imap_settings(): return { "IMAP_SERVER_DOMAIN": imap_settings.IMAP_SERVER_DOMAIN, @@ -63,6 +105,7 @@ def get_imap_settings(): "IMAP_SERVER_PASSWORD": imap_settings.IMAP_SERVER_PASSWORD, } + # Function to check missing configurations def check_missing_configs(settings): missing = [key for key, value in settings.items() if not value] if missing: @@ -70,6 +113,7 @@ def check_missing_configs(settings): f"IMAP configuration settings are missing: {', '.join(missing)}." ) + # Check IMAP configuration settings try: imap_settings_dict = get_imap_settings() check_missing_configs(imap_settings_dict) diff --git a/sage_mailbox/conf.py b/sage_mailbox/conf.py index 7b92b23..4f94f5c 100644 --- a/sage_mailbox/conf.py +++ b/sage_mailbox/conf.py @@ -1,7 +1,7 @@ from django.conf import settings -from sage_mailbox.exc import IMAPConfigurationError from sage_mailbox.constants import DEFAULTS +from sage_mailbox.exc import IMAPConfigurationError class IMAPSettings: diff --git a/sage_mailbox/models/attachment.py b/sage_mailbox/models/attachment.py index dabf399..d47c896 100644 --- a/sage_mailbox/models/attachment.py +++ b/sage_mailbox/models/attachment.py @@ -1,6 +1,5 @@ import mimetypes -from django.conf import settings from django.core.files.storage import FileSystemStorage from django.db import models from django.utils.translation import gettext_lazy as _ @@ -50,10 +49,11 @@ class Meta: db_table = "sage_attachment" db_table_comment = "Model representing an attachment to an email message." + # pylint: disable= C0103 def save(self, *args, **kwargs): if not self.content_type and self.file: self.content_type, _ = mimetypes.guess_type(self.file.name) super().save(*args, **kwargs) def __str__(self): - return self.filename or "Attachment" + return str(self.filename) or "Attachment" diff --git a/sage_mailbox/models/email.py b/sage_mailbox/models/email.py index 8033b47..7c257d7 100644 --- a/sage_mailbox/models/email.py +++ b/sage_mailbox/models/email.py @@ -2,9 +2,7 @@ from django.db import models from django.utils.translation import gettext_lazy as _ - from django_jsonform.models.fields import JSONField - from sage_imap.models.email import EmailMessage as EmailMessageDC from sage_mailbox.models.mixins import TimestampMixin @@ -166,7 +164,7 @@ def save(self, *args, **kwargs): from sage_mailbox.models import Mailbox if not self.mailbox_id: - sent_mailbox, created = Mailbox.objects.get_or_create(name="Sent") + sent_mailbox, _ = Mailbox.objects.get_or_create(name="Sent") self.mailbox = sent_mailbox super().save(*args, **kwargs) @@ -174,7 +172,9 @@ def save(self, *args, **kwargs): def from_dataclass(cls, email_dc: EmailMessageDC): from sage_mailbox.models import Attachment, Flag - """Convert a dataclass instance to a Django model instance.""" + """ + Convert a dataclass instance to a Django model instance. + """ email = cls( uid=email_dc.uid, message_id=email_dc.message_id, @@ -287,10 +287,15 @@ def get_summary(self): } def __repr__(self): - return f"" + return ( + f"" + ) def __str__(self): - return self.subject + return str(self.subject) class Draft(EmailMessage): diff --git a/sage_mailbox/models/mailbox.py b/sage_mailbox/models/mailbox.py index cb53f13..85efe2e 100644 --- a/sage_mailbox/models/mailbox.py +++ b/sage_mailbox/models/mailbox.py @@ -81,7 +81,7 @@ def save(self, *args, **kwargs): if not self.pk: self.folder_type = map_to_standard_name(self.name) - super(Mailbox, self).save(*args, **kwargs) + super().save(*args, **kwargs) def __str__(self): - return self.name + return str(self.name) diff --git a/sage_mailbox/repository/manager.py b/sage_mailbox/repository/manager.py index 36fb8bf..d0edbd4 100644 --- a/sage_mailbox/repository/manager.py +++ b/sage_mailbox/repository/manager.py @@ -25,8 +25,8 @@ def unread(self): def flagged(self): return self.get_queryset().flagged() - def has_cc(self): - return self.get_queryset().has_cc() + # def has_cc(self): + # return self.get_queryset().has_cc() - def has_bcc(self): - return self.get_queryset().has_bcc() + # def has_bcc(self): + # return self.get_queryset().has_bcc() diff --git a/sage_mailbox/repository/queryset.py b/sage_mailbox/repository/queryset.py index adde3bc..0b096df 100644 --- a/sage_mailbox/repository/queryset.py +++ b/sage_mailbox/repository/queryset.py @@ -28,20 +28,20 @@ def unread(self): def flagged(self): return self.filter(is_flagged=True) - def has_cc(self): - return self.annotate( - has_cc=Case( - When(Length("cc_address") > 0, then=Value(True)), - default=Value(False), - output_field=BooleanField(), - ) - ) - - def has_bcc(self): - return self.annotate( - has_bcc=Case( - When(Length("bcc_address") > 0, then=Value(True)), - default=Value(False), - output_field=BooleanField(), - ) - ) + # def has_cc(self): + # return self.annotate( + # has_cc=Case( + # When(Length("cc_address") > 0, then=Value(True)), + # default=Value(False), + # output_field=BooleanField(), + # ) + # ) + + # def has_bcc(self): + # return self.annotate( + # has_bcc=Case( + # When(Length("bcc_address") > 0, then=Value(True)), + # default=Value(False), + # output_field=BooleanField(), + # ) + # ) diff --git a/sage_mailbox/repository/service.py b/sage_mailbox/repository/service.py index 132a1d0..a2d74b7 100644 --- a/sage_mailbox/repository/service.py +++ b/sage_mailbox/repository/service.py @@ -20,6 +20,7 @@ logger = logging.getLogger(__name__) +# pylint: disable= C0103 class EmailSyncService: def __init__(self, host: str, username: str, password: str): self.host = host @@ -32,7 +33,7 @@ def fetch_and_save_emails(self, folder: str = "INBOX"): with IMAPMailboxService(client) as mailbox: mailbox.select(folder) - # Retrieve the mailbox object or create a new one if it doesn't exist + # Retrieve mailbox object or create a new one if it doesn't exist mailbox_obj, _ = DjangoMailbox.objects.get_or_create( name__contains=folder ) @@ -62,8 +63,8 @@ def fetch_and_save_emails(self, folder: str = "INBOX"): return result return {"created_emails": 0, "created_attachments": 0} - except Exception as e: - logger.error(f"Error fetching and saving emails: {e}") + except Exception as exc: + logger.error(f"Error fetching and saving emails: {exc}") return {"created_emails": 0, "created_attachments": 0} def save_emails_to_db(self, emails, mailbox): diff --git a/sage_mailbox/signals/email.py b/sage_mailbox/signals/email.py index 04d207c..1662fe4 100644 --- a/sage_mailbox/signals/email.py +++ b/sage_mailbox/signals/email.py @@ -1,16 +1,15 @@ +import email import logging import mimetypes -import email from email.utils import make_msgid from django.conf import settings +from django.contrib.sites.models import Site +from django.core.mail import EmailMultiAlternatives from django.db import transaction +from django.db.models.signals import post_save from django.dispatch import receiver from django.utils.timezone import now -from django.contrib.sites.models import Site -from django.db.models.signals import post_save -from django.core.mail import EmailMultiAlternatives - from sage_imap.services import IMAPClient, IMAPMailboxService from sage_mailbox.models import EmailMessage, Sent @@ -18,6 +17,8 @@ logger = logging.getLogger(__name__) +# pylint: disable=W0613, C0103 + @receiver(post_save, sender=EmailMessage) def send_email_after_save(sender, instance, created, **kwargs): @@ -60,7 +61,8 @@ def send_email(email_message): file_content = attachment.file.read() mime_type, _ = mimetypes.guess_type(attachment.filename) if not mime_type: - mime_type = "application/octet-stream" # Default to binary stream if MIME type can't be guessed + # Default to binary stream if MIME type can't be guessed + mime_type = "application/octet-stream" msg.attach(attachment.filename, file_content, mime_type) # Additional headers diff --git a/sage_mailbox/tests/__init__.py b/sage_mailbox/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sage_mailbox/tests/models/__init__.py b/sage_mailbox/tests/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sage_mailbox/tests/models/test_email.py b/sage_mailbox/tests/models/test_email.py new file mode 100644 index 0000000..dd1f19c --- /dev/null +++ b/sage_mailbox/tests/models/test_email.py @@ -0,0 +1,46 @@ +import pytest +from django.utils import timezone +from model_bakery import baker +from sage_mailbox.models import EmailMessage + + +@pytest.mark.django_db +class TestEmailMessageModel: + + def test_sanitize_message_id(self): + """Test the sanitize_message_id class method.""" + msg_id = "" + sanitized_id = EmailMessage.sanitize_message_id(msg_id) + assert sanitized_id == "" + + invalid_msg_id = "invalid_id" + sanitized_invalid_id = EmailMessage.sanitize_message_id(invalid_msg_id) + assert sanitized_invalid_id is None + + def test_to_dataclass(self): + """Test conversion from EmailMessage to dataclass.""" + email = baker.make(EmailMessage, subject="Test subject") + email_dc = email.to_dataclass() + + assert email_dc.subject == "Test subject" + + def test_has_attachments_method(self): + """Test has_attachments method for EmailMessage.""" + email = baker.make(EmailMessage) + baker.make('sage_mailbox.Attachment', email_message=email, _quantity=2) + + assert email.has_attachments() is True + + def test_get_summary(self): + """Test get_summary method.""" + email = baker.make( + EmailMessage, + subject="Test", + from_address="from@example.com", + date=timezone.now() + ) + summary = email.get_summary() + + assert summary["subject"] == "Test" + assert summary["from"] == "from@example.com" + assert "has_attachments" in summary diff --git a/sage_mailbox/tests/repository/__init__.py b/sage_mailbox/tests/repository/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sage_mailbox/tests/repository/test_queryset.py b/sage_mailbox/tests/repository/test_queryset.py new file mode 100644 index 0000000..9659433 --- /dev/null +++ b/sage_mailbox/tests/repository/test_queryset.py @@ -0,0 +1,102 @@ +import pytest +from django.utils import timezone +from model_bakery import baker +from datetime import datetime +from sage_mailbox.models import EmailMessage, Flag, Mailbox +from autoslug import AutoSlugField +from django.db.models import F + + +# Register the custom generator for AutoSlugField +def slug_generator(): + return 'test-slug' + +baker.generators.add(AutoSlugField, slug_generator) + + +@pytest.mark.django_db +class TestEmailMessageQuerySet: + + def test_total_attachments(self): + """Test total attachment count for each email.""" + email = baker.make(EmailMessage, _quantity=3) + attachment = baker.make('sage_mailbox.Attachment', email_message=email[0], _quantity=5) + attachment2 = baker.make('sage_mailbox.Attachment', email_message=email[1], _quantity=2) + + queryset = EmailMessage.objects.total_attachments() + + assert queryset.get(id=email[0].id).total_attachments == 5 + assert queryset.get(id=email[1].id).total_attachments == 2 + assert queryset.get(id=email[2].id).total_attachments == 0 + + def test_has_attachments(self): + """Test annotation to check if emails have attachments.""" + email = baker.make(EmailMessage, _quantity=3) + baker.make('sage_mailbox.Attachment', email_message=email[0]) + + queryset = EmailMessage.objects.has_attachments() + assert queryset.get(id=email[0].id).has_attachments is True + assert queryset.get(id=email[1].id).has_attachments is False + + def test_select_related_mailbox(self, django_assert_num_queries): + """Test select_related optimization for mailbox.""" + mailbox = baker.make(Mailbox) + email = baker.make(EmailMessage, mailbox=mailbox) + + # Ensure that select_related is optimizing the query by fetching related objects in one query + with django_assert_num_queries(1): # One query should fetch both EmailMessage and Mailbox + result_email = EmailMessage.objects.select_related_mailbox().get(id=email.id) + assert result_email.mailbox == mailbox # Access the mailbox without triggering another query + + def test_list_attachments(self): + """Test prefetching attachments.""" + email = baker.make(EmailMessage) + baker.make('sage_mailbox.Attachment', email_message=email, _quantity=3) + queryset = EmailMessage.objects.list_attachments() + + assert queryset.count() == 1 + assert queryset[0].attachments.count() == 3 + + def test_unread(self): + """Test filtering unread emails.""" + baker.make(EmailMessage, is_read=False, _quantity=2) + baker.make(EmailMessage, is_read=True, _quantity=2) + + assert EmailMessage.objects.unread().count() == 2 + + def test_flagged(self): + """Test filtering flagged emails.""" + baker.make(EmailMessage, is_flagged=True, _quantity=3) + baker.make(EmailMessage, is_flagged=False, _quantity=2) + + assert EmailMessage.objects.flagged().count() == 3 + + +@pytest.mark.django_db +class TestEmailMessageManager: + + def test_total_attachments_manager(self): + """Test total attachments through manager.""" + email = baker.make(EmailMessage) + baker.make('sage_mailbox.Attachment', email_message=email, _quantity=2) + + queryset = EmailMessage.objects.total_attachments() + assert queryset.get(id=email.id).total_attachments == 2 + + def test_has_attachments_manager(self): + """Test has_attachments through manager.""" + email = baker.make(EmailMessage) + baker.make('sage_mailbox.Attachment', email_message=email) + + queryset = EmailMessage.objects.has_attachments() + assert queryset.get(id=email.id).has_attachments is True + + def test_unread_manager(self): + """Test unread emails through manager.""" + baker.make(EmailMessage, is_read=False) + assert EmailMessage.objects.unread().count() == 1 + + def test_flagged_manager(self): + """Test flagged emails through manager.""" + baker.make(EmailMessage, is_flagged=True) + assert EmailMessage.objects.flagged().count() == 1 diff --git a/sage_mailbox/tests/repository/test_service.py b/sage_mailbox/tests/repository/test_service.py new file mode 100644 index 0000000..ed4158e --- /dev/null +++ b/sage_mailbox/tests/repository/test_service.py @@ -0,0 +1,159 @@ +# import pytest +# from unittest.mock import patch, MagicMock +# from dateutil import parser +# from sage_mailbox.repository.service import EmailSyncService + + +# from sage_mailbox.models import EmailMessage as DjangoEmailMessage +# from sage_mailbox.models import Mailbox as DjangoMailbox +# from sage_mailbox.models import Attachment as DjangoAttachment +# from sage_mailbox.models import Flag as DjangoFlag + + +# from sage_imap.services import IMAPClient, IMAPMailboxService +# from sage_imap.helpers.enums import Flag, MessagePart +# from sage_imap.helpers.search import IMAPSearchCriteria +# from sage_imap.models.message import MessageSet +# from sage_imap.models.email import EmailMessage + +# # Sample data for tests +# SAMPLE_EMAIL = EmailMessage( +# uid="123", +# message_id="msg-123", +# subject="Test Subject", +# from_address="from@example.com", +# to_address=["to@example.com"], +# cc_address=[], +# bcc_address=[], +# date="01-Jan-2021", +# raw=b"Raw message content", # Ensure raw is bytes +# plain_body="Plain body content", +# html_body="HTML body content", +# size=1024, +# flags=[Flag.SEEN, Flag.FLAGGED], +# headers={"Message-ID": "", "subject": "Test Subject"}, +# attachments=[] +# ) + + +# @pytest.fixture +# def email_sync_service(): +# """Fixture to instantiate the EmailSyncService.""" +# return EmailSyncService(host="imap.example.com", username="user", password="password") + + +# @pytest.fixture +# def mocked_mailbox(): +# """Fixture to mock IMAPMailboxService.""" +# with patch("sage_imap.services.IMAPMailboxService") as mock_mailbox: +# yield mock_mailbox + + +# @pytest.fixture +# def mocked_client(): +# """Fixture to mock IMAPClient.""" +# with patch("sage_imap.services.IMAPClient") as mock_client: +# yield mock_client + + +# @pytest.fixture +# def mocked_django_models(): +# """Fixture to mock Django model interactions.""" +# with patch("sage_mailbox.models.EmailMessage.objects.update_or_create") as mock_update_or_create, \ +# patch("sage_mailbox.models.Mailbox.objects.get_or_create") as mock_get_or_create, \ +# patch("sage_mailbox.models.Attachment.objects.update_or_create") as mock_attachment_create, \ +# patch("sage_mailbox.models.Flag.objects.get_or_create") as mock_flag_create: +# mock_update_or_create.return_value = (MagicMock(), True) # Mocking the return values +# mock_get_or_create.return_value = (MagicMock(), True) # Mock Mailbox get_or_create +# mock_attachment_create.return_value = (MagicMock(), True) # Mock Attachment creation +# mock_flag_create.return_value = (MagicMock(), True) # Mock Flag creation +# yield mock_update_or_create, mock_get_or_create, mock_attachment_create, mock_flag_create + + +# class TestEmailSyncService: +# def test_service_initialization(self, email_sync_service): +# """Test the initialization of EmailSyncService.""" +# assert email_sync_service.host == "imap.example.com" +# assert email_sync_service.username == "user" +# assert email_sync_service.password == "password" + +# def test_fetch_emails_from_default_inbox(self, email_sync_service, mocked_client, mocked_mailbox, mocked_django_models): +# """Test fetching emails from the default folder 'INBOX'.""" +# mock_mailbox_instance = mocked_mailbox.return_value +# mock_mailbox_instance.search.return_value = [1, 2, 3] # Fake message IDs +# mock_mailbox_instance.fetch.return_value = [SAMPLE_EMAIL] + +# # Mock Django mailbox retrieval/creation +# mock_django_email_message, mock_mailbox_obj = mocked_django_models[1], mocked_django_models[0] +# mock_django_email_message.filter.return_value.order_by.return_value.first.return_value = None + +# result = email_sync_service.fetch_and_save_emails() + +# assert result == {"created_emails": 1, "created_attachments": 0} +# mock_mailbox_instance.search.assert_called_once_with(IMAPSearchCriteria.ALL) +# mock_mailbox_instance.fetch.assert_called_once() + +# def test_fetch_emails_with_latest_date(self, email_sync_service, mocked_client, mocked_mailbox, mocked_django_models): +# """Test fetching emails when there's already synced emails in the database.""" +# mock_mailbox_instance = mocked_mailbox.return_value +# mock_mailbox_instance.search.return_value = [1, 2, 3] # Fake message IDs +# mock_mailbox_instance.fetch.return_value = [SAMPLE_EMAIL] + +# # Mock latest email creation date +# latest_email = MagicMock() +# latest_email.created_at = parser.parse("2021-01-01") +# mock_django_email_message, _ = mocked_django_models[1], mocked_django_models[0] +# mock_django_email_message.filter.return_value.order_by.return_value.first.return_value = latest_email + +# result = email_sync_service.fetch_and_save_emails() + +# assert result == {"created_emails": 1, "created_attachments": 0} +# mock_mailbox_instance.search.assert_called_once() +# mock_mailbox_instance.fetch.assert_called_once() + +# def test_handle_invalid_email_date(self, email_sync_service, mocked_django_models): +# """Test saving email when email.date is invalid.""" +# invalid_email = SAMPLE_EMAIL +# invalid_email.date = "invalid-date" + +# mock_django_email_message, _ = mocked_django_models[1], mocked_django_models[0] +# email_sync_service.create_or_update_email(invalid_email, MagicMock()) + +# mock_django_email_message.update_or_create.assert_called_once() + +# def test_save_email_with_attachments(self, email_sync_service, mocked_django_models): +# """Test saving email with attachments.""" +# attachment = MagicMock(filename="file.txt", payload=b"file_content", content_type="text/plain") +# email_with_attachments = SAMPLE_EMAIL +# email_with_attachments.attachments = [attachment] + +# mock_django_email_message, _ = mocked_django_models[1], mocked_django_models[0] +# result = email_sync_service.create_or_update_email(email_with_attachments, MagicMock()) + +# mock_django_email_message.update_or_create.assert_called_once() +# assert result == (True, 1) # One email and one attachment created + +# def test_error_handling_in_fetch_and_save_emails(self, email_sync_service, mocked_mailbox): +# """Test error handling during email fetching.""" +# mock_mailbox_instance = mocked_mailbox.return_value +# mock_mailbox_instance.search.side_effect = Exception("IMAP error") + +# result = email_sync_service.fetch_and_save_emails() +# assert result == {"created_emails": 0, "created_attachments": 0} + +# def test_handle_flags_for_email(self, email_sync_service, mocked_django_models): +# """Test handling flags for an email.""" +# mock_django_flag, _ = mocked_django_models[3], mocked_django_models[0] + +# email_sync_service.handle_flags(MagicMock(), [Flag.SEEN, Flag.FLAGGED]) + +# assert mock_django_flag.call_count == 2 # Two flags should be created or retrieved + +# def test_handle_no_attachments(self, email_sync_service, mocked_django_models): +# """Test saving an email with no attachments.""" +# mock_django_email_message, _ = mocked_django_models[1], mocked_django_models[0] + +# result = email_sync_service.create_or_update_email(SAMPLE_EMAIL, MagicMock()) + +# mock_django_email_message.update_or_create.assert_called_once() +# assert result == (True, 0) # One email and zero attachments created diff --git a/sage_mailbox/tests/validators/__init__.py b/sage_mailbox/tests/validators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sage_mailbox/tests/validators/test_comma_separated.py b/sage_mailbox/tests/validators/test_comma_separated.py new file mode 100644 index 0000000..59c20be --- /dev/null +++ b/sage_mailbox/tests/validators/test_comma_separated.py @@ -0,0 +1,42 @@ +import pytest + +from django.core.exceptions import ValidationError + +from sage_mailbox.validators import CommaSeparatedEmailValidator + + +class TestCommaSeparatedEmailValidator: + + def setup_method(self): + self.validator = CommaSeparatedEmailValidator() + + @pytest.mark.parametrize("emails", [ + "test@example.com", # Single valid email + "test1@example.com, test2@example.com", # Multiple valid emails + "test1@example.com, test2@example.com", # Multiple valid emails with spaces + ]) + def test_valid_emails(self, emails): + # These should pass validation + result = self.validator(emails) + assert result is None + + @pytest.mark.parametrize("emails", [ + "invalid-email", # Single invalid email + "valid@example.com, invalid-email", # One valid, one invalid + "invalid-email1, invalid-email2", # Both invalid + "test@", # Missing domain + "@example.com", # Missing local part + ]) + def test_invalid_emails(self, emails): + with pytest.raises(ValidationError): + self.validator(emails) + + @pytest.mark.parametrize("emails", [ + "", # Empty string + " test@example.com ", # Valid email with leading/trailing spaces + "valid@example.com, ", # Valid email with trailing comma and space + ]) + def test_edge_cases(self, emails): + # These should pass validation + result = self.validator(emails) + assert result is None diff --git a/sage_mailbox/tests/validators/test_folder_name.py b/sage_mailbox/tests/validators/test_folder_name.py new file mode 100644 index 0000000..86b7b3f --- /dev/null +++ b/sage_mailbox/tests/validators/test_folder_name.py @@ -0,0 +1,53 @@ +import pytest + +from django.core.exceptions import ValidationError + +from sage_mailbox.validators import FolderNameValidator + + +class TestFolderNameValidator: + + def setup_method(self): + self.validator = FolderNameValidator() + + @pytest.mark.parametrize("folder_name", [ + "MyFolder", # letters + "Folder123", # letters and numbers + "Folder-Name_123", # hyphen and underscore + "Folder.Name", # dot in name + "A" * 255 # exactly 255 characters + ]) + def test_valid_folder_names(self, folder_name): + # These should pass validation + result = self.validator(folder_name) + assert result is None + + @pytest.mark.parametrize("folder_name", [ + "", # Empty string + "A" * 256, # Exceeds 255 characters + ]) + def test_invalid_folder_names_length(self, folder_name): + with pytest.raises(ValidationError) as exc_info: + self.validator(folder_name) + assert exc_info.value.messages[0] == "Folder name must be between 1 and 255 characters long." + + @pytest.mark.parametrize("folder_name", [ + "Invalid@Folder", # Invalid special characters + "Folder#Name", # Invalid special character + "Folder Name With Spaces", # Spaces in name + "-Invalid", # Starts with invalid character + "Invalid-" # Ends with invalid character + ]) + def test_invalid_folder_names_characters(self, folder_name): + with pytest.raises(ValidationError) as exc_info: + self.validator(folder_name) + assert exc_info.value.messages[0] == "Folder name contains invalid characters. Allowed characters are letters, numbers, underscore, hyphen, and dot. Spaces are not allowed." + + @pytest.mark.parametrize("folder_name", [ + "A", # Exactly 1 character + "A" * 255 # Exactly 255 characters + ]) + def test_edge_cases(self, folder_name): + # Edge case for exactly 1 and 255 characters, should pass validation + result = self.validator(folder_name) + assert result is None diff --git a/sage_mailbox/utils.py b/sage_mailbox/utils.py index 28405d6..666f253 100644 --- a/sage_mailbox/utils.py +++ b/sage_mailbox/utils.py @@ -2,9 +2,7 @@ def sanitize_filename(filename): - """ - Sanitize the filename to ensure it's safe to use in the file system. - """ + """Sanitize the filename to ensure it's safe to use in the file system.""" # Decode if the filename is encoded if filename.startswith("=?"): from email.header import decode_header diff --git a/sage_mailbox/validators.py b/sage_mailbox/validators.py index 9f0e3d7..91f2ac2 100644 --- a/sage_mailbox/validators.py +++ b/sage_mailbox/validators.py @@ -1,7 +1,7 @@ import re -from django.core.validators import validate_email from django.core.exceptions import ValidationError +from django.core.validators import validate_email from django.utils.deconstruct import deconstructible from django.utils.translation import gettext_lazy as _ @@ -19,13 +19,17 @@ class FolderNameValidator: """ length_error_message = "Folder name must be between 1 and 255 characters long." - character_error_message = "Folder name contains invalid characters. Allowed characters are letters, numbers, underscore, hyphen, and dot. Spaces are not allowed." + character_error_message = ( + "Folder name contains invalid characters. Allowed characters are letters, " + "numbers, underscore, hyphen, and dot. Spaces are not allowed." + ) code_length = "folder_name_length" code_character = "folder_name_invalid_character" - regex = re.compile(r"^[\w.-]+$") + # regex to disallow leading/trailing hyphens or underscore at the end or start + regex = re.compile(r"^(?![-_])[a-zA-Z0-9._-]+(?=3.12", -) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..e834078 --- /dev/null +++ b/tox.ini @@ -0,0 +1,42 @@ +[tox] +requires = + tox>=4.2 +env_list = + py312-django40 + py312-django50 + py311-django40 + py311-django50 + py310-django40 + py310-django50 + py39-django40 + py38-django40 + +[testenv] +description = Run Pytest tests with multiple django versions +package = editable +deps = + django-stubs + pytest + pytest-cov + pytest-django + django40: django<5.0,>=4.2 + django50: django<5.3,>=5 +set_env = + DJANGO_SETTINGS_MODULE = kernel.settings +commands = + pytest --cov + +[testenv:pre-commit] +description = Run pre-commit hooks +deps = + pre-commit +commands = + pre-commit run --all-files + +[gh-actions] +python = + 3.8: py38 + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: py312