diff --git a/.circleci/config.yml b/.circleci/config.yml
index 8e1eab5..78937fa 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -140,6 +140,19 @@ jobs:
- run-tests:
pyversion: 313
+ test-python314:
+ executor:
+ name: python
+ pyversion: "3.14"
+ pgversion: "18.0"
+ debiandist: "trixie"
+ steps:
+ - checkout
+ - install-dependencies:
+ extra: dev, test
+ - run-tests:
+ pyversion: 314
+
analysis:
executor:
name: python
@@ -222,6 +235,12 @@ workflows:
only: /.*/
branches:
only: /.*/
+ - test-python314:
+ filters:
+ tags:
+ only: /.*/
+ branches:
+ only: /.*/
- analysis:
filters:
tags:
diff --git a/README.md b/README.md
index 7d32022..4a2e98d 100644
--- a/README.md
+++ b/README.md
@@ -8,8 +8,8 @@
| :memo: | **License** | [](http://doge.mit-license.org) |
| :package: | **PyPi** | [](https://pypi.python.org/pypi/django-postgres-extra) |
| :four_leaf_clover: | **Code coverage** | [](https://coveralls.io/github/SectorLabs/django-postgres-extra?branch=master) |
-|
| **Django Versions** | 2.0, 2.1, 2.2, 3.0, 3.1, 3.2, 4.0, 4.1, 4.2, 5.0, 5.1, 5.2 |
-|
| **Python Versions** | 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13 |
+|
| **Django Versions** | 2.0, 2.1, 2.2, 3.0, 3.1, 3.2, 4.0, 4.1, 4.2, 5.0, 5.1, 5.2, 6.0 |
+|
| **Python Versions** | 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, 3.14 |
|
| **Psycopg Versions** | 2, 3 |
| :book: | **Documentation** | [Read The Docs](https://django-postgres-extra.readthedocs.io/en/master/) |
| :warning: | **Upgrade** | [Upgrade from v1.x](https://django-postgres-extra.readthedocs.io/en/master/major_releases.html#new-features)
@@ -18,7 +18,7 @@
| :droplet: | **Future enhancements** | [Potential features](https://github.com/SectorLabs/django-postgres-extra/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement) |
`django-postgres-extra` aims to make all of PostgreSQL's awesome features available through the Django ORM. We do this by taking care of all the hassle. As opposed to the many small packages that are available to try to bring a single feature to Django with minimal effort. ``django-postgres-extra`` goes the extra mile, with well tested implementations, seamless migrations and much more.
-
+
With seamless we mean that any features we add will work truly seamlessly. You should not have to manually modify your migrations to work with fields and objects provided by this package.
---
@@ -66,6 +66,7 @@ For Django 2.2 and older:
* **HStore unique and required constraints on specific HStore keys**
## Working with the code
+
### Prerequisites
* PostgreSQL 14 or newer.
diff --git a/psqlextra/backend/migrations/operations/create_materialized_view_model.py b/psqlextra/backend/migrations/operations/create_materialized_view_model.py
index 9ca2320..421d9c3 100644
--- a/psqlextra/backend/migrations/operations/create_materialized_view_model.py
+++ b/psqlextra/backend/migrations/operations/create_materialized_view_model.py
@@ -1,3 +1,5 @@
+import django
+
from django.db.migrations.operations.models import CreateModel
from psqlextra.backend.migrations.state import (
@@ -32,12 +34,16 @@ def __init__(
self.with_data = with_data
def state_forwards(self, app_label, state):
+ options = dict(self.options)
+ options.setdefault("indexes", [])
+ if django.VERSION >= (2, 2):
+ options.setdefault("constraints", [])
state.add_model(
PostgresMaterializedViewModelState(
app_label=app_label,
name=self.name,
fields=list(self.fields),
- options=dict(self.options),
+ options=options,
bases=tuple(self.bases),
managers=list(self.managers),
view_options=dict(self.view_options),
diff --git a/psqlextra/backend/migrations/operations/create_partitioned_model.py b/psqlextra/backend/migrations/operations/create_partitioned_model.py
index e447965..94622df 100644
--- a/psqlextra/backend/migrations/operations/create_partitioned_model.py
+++ b/psqlextra/backend/migrations/operations/create_partitioned_model.py
@@ -1,3 +1,5 @@
+import django
+
from django.db.migrations.operations.models import CreateModel
from psqlextra.backend.migrations.state import PostgresPartitionedModelState
@@ -27,12 +29,16 @@ def __init__(
self.partitioning_options = partitioning_options or {}
def state_forwards(self, app_label, state):
+ options = dict(self.options)
+ options.setdefault("indexes", [])
+ if django.VERSION >= (2, 2):
+ options.setdefault("constraints", [])
state.add_model(
PostgresPartitionedModelState(
app_label=app_label,
name=self.name,
fields=list(self.fields),
- options=dict(self.options),
+ options=options,
bases=tuple(self.bases),
managers=list(self.managers),
partitioning_options=dict(self.partitioning_options),
@@ -76,12 +82,14 @@ def reduce(self, *args, **kwargs):
# replace CreateModel operation with PostgresCreatePartitionedModel
if isinstance(result, list) and result:
for i, op in enumerate(result):
- if isinstance(op, CreateModel):
- _, args, kwargs = op.deconstruct()
+ if isinstance(op, CreateModel) and not isinstance(
+ op, PostgresCreatePartitionedModel
+ ):
+ _, op_args, op_kwargs = op.deconstruct()
result[i] = PostgresCreatePartitionedModel(
- *args,
- **kwargs,
- partitioning_options=self.partitioning_options
+ *op_args,
+ **op_kwargs,
+ partitioning_options=self.partitioning_options,
)
return result
diff --git a/psqlextra/backend/migrations/operations/create_view_model.py b/psqlextra/backend/migrations/operations/create_view_model.py
index 005a234..ac695f4 100644
--- a/psqlextra/backend/migrations/operations/create_view_model.py
+++ b/psqlextra/backend/migrations/operations/create_view_model.py
@@ -1,3 +1,5 @@
+import django
+
from django.db.migrations.operations.models import CreateModel
from psqlextra.backend.migrations.state import PostgresViewModelState
@@ -27,12 +29,16 @@ def __init__(
self.view_options = view_options or {}
def state_forwards(self, app_label, state):
+ options = dict(self.options)
+ options.setdefault("indexes", [])
+ if django.VERSION >= (2, 2):
+ options.setdefault("constraints", [])
state.add_model(
PostgresViewModelState(
app_label=app_label,
name=self.name,
fields=list(self.fields),
- options=dict(self.options),
+ options=options,
bases=tuple(self.bases),
managers=list(self.managers),
view_options=dict(self.view_options),
diff --git a/psqlextra/expressions.py b/psqlextra/expressions.py
index 20486df..66a0cd6 100644
--- a/psqlextra/expressions.py
+++ b/psqlextra/expressions.py
@@ -60,7 +60,7 @@ def as_sql(self, compiler, connection):
sql.append("hstore(%s, NULL)")
params.append(key)
- return " || ".join(sql), params
+ return " || ".join(sql), tuple(params)
class HStoreColumn(expressions.Col):
@@ -105,7 +105,7 @@ def as_sql(self, compiler, connection):
return (
"%s.%s->'%s'"
% (qn(self.alias), qn(self.target.column), self.hstore_key),
- [],
+ (),
)
def relabeled_clone(self, relabels):
diff --git a/psqlextra/lookups.py b/psqlextra/lookups.py
index 4010310..a998f71 100644
--- a/psqlextra/lookups.py
+++ b/psqlextra/lookups.py
@@ -21,7 +21,7 @@ def as_sql(self, compiler, connection):
_, rhs_params = self.process_rhs(compiler, connection)
rhs = ",".join([f"(%s)" for _ in rhs_params]) # noqa: F541
- return f"{lhs} IN (VALUES {rhs})", lhs_params + list(rhs_params)
+ return f"{lhs} IN (VALUES {rhs})", (*lhs_params, *rhs_params)
@Field.register_lookup
diff --git a/pyproject.toml b/pyproject.toml
index b1baa93..333a8fb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -30,12 +30,15 @@ classifiers = [
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
]
requires-python = ">=3.7"
dependencies = [
- "Django>=2.0,<6.0",
+ "Django>=2.0,<7.0",
"python-dateutil>=2.8.0,<=3.0.0",
]
dynamic = ["version"]
@@ -72,14 +75,17 @@ test = [
"syrupy==4.9.1; python_version >= '3.9'",
"syrupy==2.3.1; python_version <= '3.8'",
]
-test-report = ["coveralls==4.0.1"]
+test-report = [
+ "coveralls==4.0.1; python_version < '3.13'",
+ "coveralls==4.1.0; python_version >= '3.13'",
+]
analysis = [
"black==22.3.0",
"flake8==7.2.0",
"autoflake==2.3.1",
"autopep8==2.3.2",
"isort==6.0.1",
- "docformatter==1.7.7",
+ "docformatter==1.7.7; python_version < '3.14'",
"mypy==1.16.0",
"django-stubs==4.2.7",
"typing-extensions==4.14.0",
@@ -123,6 +129,17 @@ exclude-newer = "3 days"
# solve.
environments = ["python_version >= '3.11'"]
+# `untokenize` 0.1.1 (transitive via `docformatter==1.7.7`) ships only an
+# sdist whose setup.py uses `ast.Constant.s`, removed in Python 3.14. The
+# `python_version < '3.14'` marker on docformatter keeps it out of the 3.14
+# install set, but uv still needs metadata for resolution and tries to
+# build the sdist with the active interpreter. Vouch for the metadata
+# directly (no runtime deps) so uv skips the build.
+[[tool.uv.dependency-metadata]]
+name = "untokenize"
+version = "0.1.1"
+requires-dist = []
+
[tool.black]
line-length = 80
exclude = '''
diff --git a/tests/migrations.py b/tests/migrations.py
index d82af3c..f965a61 100644
--- a/tests/migrations.py
+++ b/tests/migrations.py
@@ -1,3 +1,5 @@
+import copy
+
from contextlib import contextmanager
from typing import List
from unittest import mock
@@ -87,9 +89,11 @@ def make_migration(app_label="tests", from_state=None, to_state=None):
specified_apps=app_labels, dry_run=False
)
+ # Deep copy states because the autodetector mutates them (e.g.
+ # options.pop("indexes")) which breaks callers that reuse states.
autodetector = MigrationAutodetector(
- from_state or loader.project_state(),
- to_state or ProjectState.from_apps(apps),
+ copy.deepcopy(from_state) if from_state else loader.project_state(),
+ copy.deepcopy(to_state) if to_state else ProjectState.from_apps(apps),
questioner,
)
diff --git a/tests/test_schema_editor_clone_model_to_schema.py b/tests/test_schema_editor_clone_model_to_schema.py
index c84e74c..b6f540c 100644
--- a/tests/test_schema_editor_clone_model_to_schema.py
+++ b/tests/test_schema_editor_clone_model_to_schema.py
@@ -34,6 +34,26 @@ def _create_schema() -> str:
return name
+def _is_not_null_constraint(name, info):
+ """PG17+ stores NOT NULL as named constraints in pg_constraint."""
+ return (
+ name.endswith("_not_null")
+ and not info["primary_key"]
+ and not info["unique"]
+ and not info["check"]
+ and not info["index"]
+ and info["foreign_key"] is None
+ )
+
+
+def _filter_not_null_constraints(constraints):
+ return {
+ k: v
+ for k, v in constraints.items()
+ if not _is_not_null_constraint(k, v)
+ }
+
+
@transaction.atomic
def _assert_cloned_table_is_same(
source_table_fqn: Tuple[str, str],
@@ -62,11 +82,15 @@ def _assert_cloned_table_is_same(
else:
assert source_relations == target_relations
- source_constraints = db_introspection.get_constraints(
- source_table_name, schema_name=source_schema_name
+ source_constraints = _filter_not_null_constraints(
+ db_introspection.get_constraints(
+ source_table_name, schema_name=source_schema_name
+ )
)
- target_constraints = db_introspection.get_constraints(
- target_table_name, schema_name=target_schema_name
+ target_constraints = _filter_not_null_constraints(
+ db_introspection.get_constraints(
+ target_table_name, schema_name=target_schema_name
+ )
)
if excluding_constraints_and_indexes:
assert target_constraints == {}
@@ -168,7 +192,12 @@ def fake_model(fake_model_fk_target_1, fake_model_fk_target_2):
name="first_last_name_uniq",
),
models.CheckConstraint(
- check=Q(age__gt=0, height__gt=0), name="age_height_check"
+ **{
+ "condition"
+ if django.VERSION >= (6, 0)
+ else "check": Q(age__gt=0, height__gt=0)
+ },
+ name="age_height_check",
),
],
"unique_together": (
diff --git a/tox.ini b/tox.ini
index a7638c5..d8de0f3 100644
--- a/tox.ini
+++ b/tox.ini
@@ -7,7 +7,8 @@ envlist =
py37-dj{20,21,22,30,31,32}-psycopg{28,29}
{py38,py39,py310}-dj{21,22,30,31,32,40}-psycopg{28,29}
{py38,py39,py310,py311}-dj{41}-psycopg{28,29}
- {py310,py311,py312,py313}-dj{42,50,51,52}-psycopg{28,29,31,32}
+ {py310,py311,py312,py313,py314}-dj{42,50,51,52}-psycopg{28,29,31,32,33}
+ {py312,py313,py314}-dj{60}-psycopg{28,29,31,32,33}
[testenv]
deps =
@@ -23,10 +24,12 @@ deps =
dj50: Django~=5.0.1
dj51: Django~=5.1.0
dj52: Django~=5.2.0
+ dj60: Django~=6.0.0
psycopg28: psycopg2[binary]~=2.8
psycopg29: psycopg2[binary]~=2.9
psycopg31: psycopg[binary]~=3.1
psycopg32: psycopg[binary]~=3.2
+ psycopg33: psycopg[binary]~=3.3
.[dev]
.[test]
setenv =
diff --git a/uv.lock b/uv.lock
index 969d0e3..257760b 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2,16 +2,23 @@ version = 1
revision = 3
requires-python = ">=3.7"
resolution-markers = [
- "python_full_version >= '3.11'",
+ "python_full_version >= '3.13'",
+ "python_full_version >= '3.11' and python_full_version < '3.13'",
]
supported-markers = [
"python_full_version >= '3.11'",
]
[options]
-exclude-newer = "2026-04-22T11:01:24.585394269Z"
+exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
exclude-newer-span = "P3D"
+[manifest]
+
+[[manifest.dependency-metadata]]
+name = "untokenize"
+version = "0.1.1"
+
[[package]]
name = "alabaster"
version = "1.0.0"
@@ -21,6 +28,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" },
]
+[[package]]
+name = "annotated-doc"
+version = "0.0.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
+]
+
[[package]]
name = "asgiref"
version = "3.11.1"
@@ -482,16 +498,36 @@ toml = [
name = "coveralls"
version = "4.0.1"
source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.11' and python_full_version < '3.13'",
+]
dependencies = [
- { name = "coverage", extra = ["toml"], marker = "python_full_version >= '3.11'" },
- { name = "docopt", marker = "python_full_version >= '3.11'" },
- { name = "requests", marker = "python_full_version >= '3.11'" },
+ { name = "coverage", extra = ["toml"], marker = "python_full_version >= '3.11' and python_full_version < '3.13'" },
+ { name = "docopt", marker = "python_full_version >= '3.11' and python_full_version < '3.13'" },
+ { name = "requests", marker = "python_full_version >= '3.11' and python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/61/75/a454fb443eb6a053833f61603a432ffbd7dd6ae53a11159bacfadb9d6219/coveralls-4.0.1.tar.gz", hash = "sha256:7b2a0a2bcef94f295e3cf28dcc55ca40b71c77d1c2446b538e85f0f7bc21aa69", size = 12419, upload-time = "2024-05-15T12:56:14.297Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/63/e5/6708c75e2a4cfca929302d4d9b53b862c6dc65bd75e6933ea3d20016d41d/coveralls-4.0.1-py3-none-any.whl", hash = "sha256:7a6b1fa9848332c7b2221afb20f3df90272ac0167060f41b5fe90429b30b1809", size = 13599, upload-time = "2024-05-15T12:56:12.342Z" },
]
+[[package]]
+name = "coveralls"
+version = "4.1.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.13'",
+]
+dependencies = [
+ { name = "coverage", extra = ["toml"], marker = "python_full_version >= '3.13'" },
+ { name = "requests", marker = "python_full_version >= '3.13'" },
+ { name = "typer", marker = "python_full_version >= '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/97/16/aa263cb5450de470234f5176ce42664c5e0535aab1a2094b739447065797/coveralls-4.1.0.tar.gz", hash = "sha256:dab364025ba80cbb95ce56c6fc62cd9172d7fd637060ea235dde99d9b46a4494", size = 12736, upload-time = "2026-02-28T15:16:47.591Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/60/f1/5a884d2e8e3e7e7d854d3032b777bd22aad1598161cf9aa77ac37d684908/coveralls-4.1.0-py3-none-any.whl", hash = "sha256:bfacfda2d443c24fc90d67035027cec15015fff2dbd036427e8bf8f4953dda2e", size = 14168, upload-time = "2026-02-28T15:16:46.181Z" },
+]
+
[[package]]
name = "cryptography"
version = "46.0.7"
@@ -577,7 +613,7 @@ analysis = [
{ name = "autopep8", marker = "python_full_version >= '3.11'" },
{ name = "black", marker = "python_full_version >= '3.11'" },
{ name = "django-stubs", marker = "python_full_version >= '3.11'" },
- { name = "docformatter", marker = "python_full_version >= '3.11'" },
+ { name = "docformatter", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
{ name = "flake8", marker = "python_full_version >= '3.11'" },
{ name = "isort", marker = "python_full_version >= '3.11'" },
{ name = "mypy", marker = "python_full_version >= '3.11'" },
@@ -611,7 +647,8 @@ test = [
{ name = "types-psycopg2", marker = "python_full_version >= '3.11'" },
]
test-report = [
- { name = "coveralls", marker = "python_full_version >= '3.11'" },
+ { name = "coveralls", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" },
+ { name = "coveralls", version = "4.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" },
]
[package.metadata]
@@ -623,10 +660,11 @@ requires-dist = [
{ name = "coverage", marker = "python_full_version == '3.8.*' and extra == 'test'", specifier = "==7.6.1" },
{ name = "coverage", marker = "python_full_version < '3.8' and extra == 'test'", specifier = "==6.2" },
{ name = "coverage", marker = "python_full_version >= '3.9' and extra == 'test'", specifier = "==7.8.2" },
- { name = "coveralls", marker = "extra == 'test-report'", specifier = "==4.0.1" },
- { name = "django", specifier = ">=2.0,<6.0" },
+ { name = "coveralls", marker = "python_full_version >= '3.13' and extra == 'test-report'", specifier = "==4.1.0" },
+ { name = "coveralls", marker = "python_full_version < '3.13' and extra == 'test-report'", specifier = "==4.0.1" },
+ { name = "django", specifier = ">=2.0,<7.0" },
{ name = "django-stubs", marker = "extra == 'analysis'", specifier = "==4.2.7" },
- { name = "docformatter", marker = "extra == 'analysis'", specifier = "==1.7.7" },
+ { name = "docformatter", marker = "python_full_version < '3.14' and extra == 'analysis'", specifier = "==1.7.7" },
{ name = "docutils", marker = "extra == 'docs'", specifier = "==0.21.2" },
{ name = "flake8", marker = "extra == 'analysis'", specifier = "==7.2.0" },
{ name = "freezegun", marker = "python_full_version >= '3.8' and extra == 'test'", specifier = "==1.5.2" },
@@ -878,6 +916,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" },
]
+[[package]]
+name = "markdown-it-py"
+version = "4.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mdurl", marker = "python_full_version >= '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
+]
+
[[package]]
name = "markupsafe"
version = "3.0.3"
@@ -983,6 +1033,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" },
]
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
+]
+
[[package]]
name = "more-itertools"
version = "11.0.2"
@@ -1447,6 +1506,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" },
]
+[[package]]
+name = "rich"
+version = "15.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py", marker = "python_full_version >= '3.13'" },
+ { name = "pygments", marker = "python_full_version >= '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
+]
+
[[package]]
name = "roman-numerals"
version = "4.1.0"
@@ -1481,6 +1553,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" },
]
+[[package]]
+name = "shellingham"
+version = "1.5.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
+]
+
[[package]]
name = "six"
version = "1.17.0"
@@ -1734,6 +1815,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/24/aa/636b8eb9637944d2d94b766997d0420d1911abfd6392a6e3e2a75347949a/twine-3.7.1-py3-none-any.whl", hash = "sha256:8c120845fc05270f9ee3e9d7ebbed29ea840e41f48cd059e04733f7e1d401345", size = 35982, upload-time = "2021-12-07T11:46:01.03Z" },
]
+[[package]]
+name = "typer"
+version = "0.25.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-doc", marker = "python_full_version >= '3.13'" },
+ { name = "click", marker = "python_full_version >= '3.13'" },
+ { name = "rich", marker = "python_full_version >= '3.13'" },
+ { name = "shellingham", marker = "python_full_version >= '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7b/27/ede8cec7596e0041ba7e7b80b47d132562f56ff454313a16f6084e555c9f/typer-0.25.0.tar.gz", hash = "sha256:123eaf9f19bb40fd268310e12a542c0c6b4fab9c98d9d23342a01ff95e3ce930", size = 120150, upload-time = "2026-04-26T08:46:14.767Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9a/72/193d4e586ec5a4db834a36bbeb47641a62f951f114ffd0fe5b1b46e8d56f/typer-0.25.0-py3-none-any.whl", hash = "sha256:ac01b48823d3db9a83c9e164338057eadbb1c9957a2a6b4eeb486669c560b5dc", size = 55993, upload-time = "2026-04-26T08:46:15.889Z" },
+]
+
[[package]]
name = "types-dj-database-url"
version = "1.3.0.4"