diff --git a/.copier/package.yml b/.copier/package.yml index 9c67383..c9332a5 100644 --- a/.copier/package.yml +++ b/.copier/package.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier; NEVER EDIT MANUALLY -_commit: v2024.18 +_commit: v2024.24 _src_path: gh:westerveltco/django-twc-package author_email: josh@joshthomas.dev author_name: Josh Thomas @@ -19,5 +19,6 @@ python_versions: - '3.10' - '3.11' - '3.12' +- '3.13' test_django_main: true versioning_scheme: SemVer diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml deleted file mode 100644 index d3c6246..0000000 --- a/.github/workflows/labels.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: labels - -on: - schedule: - # https://crontab.guru/#30_2_*_*_* - - cron: "30 2 * * *" - workflow_dispatch: - -permissions: - issues: write - -jobs: - labels: - runs-on: ubuntu-latest - - steps: - - uses: EndBug/label-sync@v2 - with: - config-file: https://raw.githubusercontent.com/westerveltco/.github/main/.github/labels.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d361a22..4b98be6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,53 +4,32 @@ on: release: types: [released] -jobs: - check: - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - steps: - - uses: actions/checkout@v4 +permissions: + contents: write - - name: Check most recent test run on `main` - id: latest-test-result - run: | - echo "result=$(gh run list \ - --branch=main \ - --workflow=test.yml \ - --json headBranch,workflowName,conclusion \ - --jq '.[] | select(.headBranch=="main" and .conclusion=="success") | .conclusion' \ - | head -n 1)" >> $GITHUB_OUTPUT - - - name: OK - if: ${{ (contains(steps.latest-test-result.outputs.result, 'success')) }} - run: exit 0 - - - name: Fail - if: ${{ !contains(steps.latest-test-result.outputs.result, 'success') }} - run: exit 1 +jobs: + test: + uses: ./.github/workflows/test.yml pypi: if: ${{ github.event_name == 'release' }} runs-on: ubuntu-latest - needs: check + needs: test environment: release permissions: contents: read id-token: write steps: - uses: actions/checkout@v4 - with: - persist-credentials: false - - uses: westerveltco/setup-ci-action@v0 + - uses: actions/setup-python@v5 with: python-version: 3.12 - extra-python-dependencies: hatch - use-uv: true + + - name: Install dependencies + run: | + python -m pip install -U pip uv + python -m uv pip install --system hatch - name: Build package run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aca2c12..6fea849 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,9 +1,10 @@ name: test on: + pull_request: push: branches: [main] - pull_request: + workflow_call: concurrency: group: test-${{ github.head_ref }} @@ -20,14 +21,15 @@ jobs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - uses: actions/checkout@v4 - with: - persist-credentials: false - - uses: westerveltco/setup-ci-action@v0 + - uses: actions/setup-python@v5 with: python-version: 3.8 - extra-python-dependencies: nox - use-uv: true + + - name: Install dependencies + run: | + python -m pip install -U pip uv + python -m uv pip install --system nox - id: set-matrix run: | @@ -42,14 +44,16 @@ jobs: matrix: ${{ fromJSON(needs.generate-matrix.outputs.matrix) }} steps: - uses: actions/checkout@v4 - with: - persist-credentials: false - - uses: westerveltco/setup-ci-action@v0 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - extra-python-dependencies: nox - use-uv: true + allow-prereleases: true + + - name: Install dependencies + run: | + python -m pip install -U pip uv + python -m uv pip install --system nox - name: Run tests run: | @@ -71,14 +75,15 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - persist-credentials: false - - uses: westerveltco/setup-ci-action@v0 + - uses: actions/setup-python@v5 with: python-version: 3.8 - extra-python-dependencies: nox - use-uv: true + + - name: Install dependencies + run: | + python -m pip install -U pip uv + python -m uv pip install --system nox - name: Run mypy run: | @@ -88,14 +93,15 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - persist-credentials: false - - uses: westerveltco/setup-ci-action@v0 + - uses: actions/setup-python@v5 with: python-version: 3.8 - extra-python-dependencies: nox - use-uv: true + + - name: Install dependencies + run: | + python -m pip install -U pip uv + python -m uv pip install --system nox - name: Run coverage run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 6770ab8..832485c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,58 +18,67 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/ ## [Unreleased] +### Added + +- Support for Python 3.13. + +### Changed + +- Bumped `django-twc-package` template version to 2024.24. +- Refactored how app settings are accessed within library to use a frozen `dataclass`. + ## [0.3.2] ### Added -- Added a `py.typed` file for static type checkers. +- Added a `py.typed` file for static type checkers. ## [0.3.1] ### Fixed -- Correctly JSON serialize `Task` kwargs when going from the in-memory representation contained in the task registry to actual model instances in the database. First reported by [@joshuadavidthomas](https://github.com/joshuadavidthomas) in [#30](https://github.com/westerveltco/django-q-registry/issues/30). +- Correctly JSON serialize `Task` kwargs when going from the in-memory representation contained in the task registry to actual model instances in the database. First reported by [@joshuadavidthomas](https://github.com/joshuadavidthomas) in [#30](https://github.com/westerveltco/django-q-registry/issues/30). ## [0.3.0] ### Changed -- Now using v2024.18 of `django-twc-package`. +- Now using v2024.18 of `django-twc-package`. ### Removed -- Dropped support for Django 3.2. +- Dropped support for Django 3.2. ## [0.2.1] ### Added -- Added a `TaskRegistry.created_tasks` attribute to store the `Task` instances created by the `TaskRegistry`. +- Added a `TaskRegistry.created_tasks` attribute to store the `Task` instances created by the `TaskRegistry`. ### Changed -- Now using v2024.12 of `django-twc-package`. +- Now using v2024.12 of `django-twc-package`. ### Fixed -- Fixed a bug in the `setup_periodic_tasks` management command where newly created tasks via `Task.objects.create_from_registry` were immediately deleted via `Task.objects.delete_dangling_objects`. Newly created tasks are now added to the `TaskRegistry.created_tasks` attribute and are only deleted if they are not in the `TaskRegistry.created_tasks` attribute. +- Fixed a bug in the `setup_periodic_tasks` management command where newly created tasks via `Task.objects.create_from_registry` were immediately deleted via `Task.objects.delete_dangling_objects`. Newly created tasks are now added to the `TaskRegistry.created_tasks` attribute and are only deleted if they are not in the `TaskRegistry.created_tasks` attribute. ## [0.2.0] ### Added -- Refactored the `django_q_registry.registry.Task` dataclass into a `django_q_registry.models.Task` Django model. This should make it more flexible and robust for registering tasks and the associated `django_q.models.Schedule` instances. +- Refactored the `django_q_registry.registry.Task` dataclass into a `django_q_registry.models.Task` Django model. This should make it more flexible and robust for registering tasks and the associated `django_q.models.Schedule` instances. ### Changed -- Now using [`django-twc-package`](https://github.com/westerveltco/django-twc-package) template for repository and package structure. -- The default for the `Q_REGISTRY["PERIOIDIC_TASK_SUFFIX"]` app setting has been changed from `"- CRON"` to `"- QREGISTRY"`. -- All database logic has been moved from the `TaskRegistry` to the `setup_periodic_tasks` management command. -- GitHub Actions `test` workflow now uses the output of `nox -l --json` to dynamically generate the test matrix. +- Now using [`django-twc-package`](https://github.com/westerveltco/django-twc-package) template for repository and package structure. +- The default for the `Q_REGISTRY["PERIOIDIC_TASK_SUFFIX"]` app setting has been changed from `"- CRON"` to `"- QREGISTRY"`. +- All database logic has been moved from the `TaskRegistry` to the `setup_periodic_tasks` management command. +- GitHub Actions `test` workflow now uses the output of `nox -l --json` to dynamically generate the test matrix. ### Fixed -- Fixed a bug in the hashing of a `Task` where the `hash` function was passed unhashable values (e.g. a `dict`). Thanks to [@Tobi-De](https://github.com/Tobi-De) for the bug report ([#6](https://github.com/westerveltco/django-q-registry/issues/6)). +- Fixed a bug in the hashing of a `Task` where the `hash` function was passed unhashable values (e.g. a `dict`). Thanks to [@Tobi-De](https://github.com/Tobi-De) for the bug report ([#6](https://github.com/westerveltco/django-q-registry/issues/6)). ## [0.1.0] @@ -77,18 +86,18 @@ Initial release! ### Added -- Initial documentation. -- Initial tests. -- Initial CI/CD (GitHub Actions). -- A registry for Django Q2 periodic tasks. - - `registry.register` function for registering periodic tasks with a convenience decorator `register_task`. - - A `TASKS` setting for registering periodic tasks from Django settings. -- Autodiscovery of periodic tasks from a Django project's `tasks.py` files. -- A `setup_periodic_tasks` management command for setting up periodic tasks in the Django Q2 broker. +- Initial documentation. +- Initial tests. +- Initial CI/CD (GitHub Actions). +- A registry for Django Q2 periodic tasks. + - `registry.register` function for registering periodic tasks with a convenience decorator `register_task`. + - A `TASKS` setting for registering periodic tasks from Django settings. +- Autodiscovery of periodic tasks from a Django project's `tasks.py` files. +- A `setup_periodic_tasks` management command for setting up periodic tasks in the Django Q2 broker. ### New Contributors -- Josh Thomas (maintainer) +- Josh Thomas (maintainer) [unreleased]: https://github.com/westerveltco/django-q-registry/compare/v0.3.2...HEAD [0.1.0]: https://github.com/westerveltco/django-q-registry/releases/tag/v0.1.0 diff --git a/README.md b/README.md index 1201c8d..264b282 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,12 @@ A Django app to register periodic Django Q tasks. ## Requirements -- Python 3.8, 3.9, 3.10, 3.11, 3.12 +- Python 3.8, 3.9, 3.10, 3.11, 3.12, 3.13 - Django 4.2, 5.0 - Django Q2 1.4.3+ - This package has only been tested with the Django ORM broker. -## Getting Started +## Installation 1. Install the package from PyPI: @@ -34,7 +34,7 @@ INSTALLED_APPS = [ ] ``` -## Usage +## Getting Started ### Registering Periodic Tasks diff --git a/noxfile.py b/noxfile.py index a03ff8a..e04cfab 100644 --- a/noxfile.py +++ b/noxfile.py @@ -13,7 +13,8 @@ PY310 = "3.10" PY311 = "3.11" PY312 = "3.12" -PY_VERSIONS = [PY38, PY39, PY310, PY311, PY312] +PY313 = "3.13" +PY_VERSIONS = [PY38, PY39, PY310, PY311, PY312, PY313] PY_DEFAULT = PY_VERSIONS[0] PY_LATEST = PY_VERSIONS[-1] diff --git a/pyproject.toml b/pyproject.toml index 9bc83af..797eed6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython" ] dependencies = ["django>=4.2", "django_q2>=1.4.3"] @@ -48,7 +49,8 @@ dev = [ "pytest-django", "pytest-randomly", "pytest-reverse", - "pytest-xdist" + "pytest-xdist", + "ruff" ] docs = [ "cogapp", @@ -102,7 +104,12 @@ django_settings_module = "tests.settings" strict_settings = false [tool.djlint] +blank_line_after_tag = "endblock,endpartialdef,extends,load" +blank_line_before_tag = "block,partialdef" +custom_blocks = "partialdef" +ignore = "H031" # Don't require `meta` tag keywords indent = 2 +profile = "django" [tool.hatch.build] exclude = [".*", "Justfile"] @@ -115,7 +122,13 @@ path = "src/django_q_registry/__init__.py" [tool.mypy] check_untyped_defs = true -exclude = "docs/.*\\.py$" +exclude = [ + "docs", + "tests", + "migrations", + "venv", + ".venv" +] mypy_path = "src/" no_implicit_optional = true plugins = ["mypy_django_plugin.main"] @@ -126,7 +139,12 @@ warn_unused_ignores = true [[tool.mypy.overrides]] ignore_errors = true ignore_missing_imports = true -module = ["django_q.*", "django_q_registry.*.migrations.*", "tests.*"] +module = [ + "*.migrations.*", + "django_q.*", + "docs.*", + "tests.*" +] [tool.mypy_django_plugin] ignore_missing_model_attributes = true diff --git a/src/django_q_registry/_typing.py b/src/django_q_registry/_typing.py new file mode 100644 index 0000000..73adda6 --- /dev/null +++ b/src/django_q_registry/_typing.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +import sys + +if sys.version_info >= (3, 12): + from typing import override as typing_override +else: # pragma: no cover + from typing_extensions import ( + override as typing_override, # pyright: ignore[reportUnreachable] + ) + +override = typing_override diff --git a/src/django_q_registry/conf.py b/src/django_q_registry/conf.py index 88a979c..f8c2552 100644 --- a/src/django_q_registry/conf.py +++ b/src/django_q_registry/conf.py @@ -1,23 +1,26 @@ +# pyright: reportAny=false from __future__ import annotations +from dataclasses import dataclass +from dataclasses import field from typing import Any -from typing import ClassVar from django.conf import settings +from ._typing import override + +DJANGO_Q_REGISTRY_SETTINGS_NAME = "Q_REGISTRY" -class AppSettings: - DEFAULT_SETTINGS: ClassVar[dict[str, Any]] = { - "PERIODIC_TASK_SUFFIX": " - QREGISTRY", - "TASKS": [], - } - def __getattr__(self, key): - return self.get(key) +@dataclass(frozen=True) +class AppSettings: + PERIODIC_TASK_SUFFIX: str = " - QREGISTRY" + TASKS: list[dict[str, Any]] = field(default_factory=list) - def get(self, key): - user_settings = getattr(settings, "Q_REGISTRY", {}) - return user_settings.get(key, self.DEFAULT_SETTINGS.get(key)) + @override + def __getattribute__(self, __name: str) -> object: + user_settings = getattr(settings, DJANGO_Q_REGISTRY_SETTINGS_NAME, {}) + return user_settings.get(__name, super().__getattribute__(__name)) app_settings = AppSettings()