Browse Source

feat: add runnable application skeleton

master
Codex 2 months ago
parent
commit
06bfe5838a
  1. 18
      .dockerignore
  2. 0
      config/__init__.py
  3. 4
      config/asgi.py
  4. 4
      config/wsgi.py
  5. 0
      core/__init__.py
  6. 13
      docker/postgres/init.sh
  7. 6
      docker/postgres/test-init.sh
  8. 39
      docs/superpowers/plans/2026-09-06-application-skeleton.md
  9. 103
      docs/superpowers/specs/2026-09-06-application-skeleton-design.md
  10. 7
      manage.py
  11. 14
      pyproject.toml
  12. 76
      tests/check_permissions.py
  13. 13
      tests/test_configuration.py
  14. 20
      tests/test_health.py
  15. 179
      uv.lock

18
.dockerignore

@ -0,0 +1,18 @@
*
!Dockerfile
!pyproject.toml
!uv.lock
!manage.py
!config/
!config/**
!core/
!core/**
!tests/
!tests/**
**/__pycache__/
**/*.pyc
.env*
!finance/
!finance/**
!ingestion/
!ingestion/**

0
config/__init__.py

4
config/asgi.py

@ -0,0 +1,4 @@
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
application = get_asgi_application()

4
config/wsgi.py

@ -0,0 +1,4 @@
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
application = get_wsgi_application()

0
core/__init__.py

13
docker/postgres/init.sh

@ -0,0 +1,13 @@
#!/bin/sh
set -eu
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" --set=migration_password="$MIGRATION_PASSWORD" --set=runtime_password="$RUNTIME_PASSWORD" <<'SQL'
CREATE ROLE gizmoduck_migrate LOGIN PASSWORD :'migration_password' NOSUPERUSER NOCREATEDB NOCREATEROLE;
CREATE ROLE gizmoduck_app LOGIN PASSWORD :'runtime_password' NOSUPERUSER NOCREATEDB NOCREATEROLE;
REVOKE ALL ON DATABASE gizmoduck FROM PUBLIC;
GRANT CONNECT ON DATABASE gizmoduck TO gizmoduck_migrate, gizmoduck_app;
REVOKE ALL ON SCHEMA public FROM PUBLIC;
ALTER SCHEMA public OWNER TO gizmoduck_migrate;
GRANT USAGE ON SCHEMA public TO gizmoduck_app;
ALTER DEFAULT PRIVILEGES FOR ROLE gizmoduck_migrate IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO gizmoduck_app;
ALTER DEFAULT PRIVILEGES FOR ROLE gizmoduck_migrate IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO gizmoduck_app;
SQL

6
docker/postgres/test-init.sh

@ -0,0 +1,6 @@
#!/bin/sh
set -eu
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" --set=test_password="$POSTGRES_PASSWORD" <<'SQL'
CREATE ROLE gizmoduck_test LOGIN PASSWORD :'test_password' NOSUPERUSER CREATEDB NOCREATEROLE;
ALTER DATABASE gizmoduck_tests OWNER TO gizmoduck_test;
SQL

39
docs/superpowers/plans/2026-09-06-application-skeleton.md

@ -0,0 +1,39 @@
# Application Skeleton Implementation Plan
**Goal:** Execute the approved application skeleton design.
**Spec:** [Design](../specs/2026-09-06-application-skeleton-design.md).
**Architecture:** Compose services separate runtime, migration, and test access.
**Stack:** Python 3.13, Django 5.2, PostgreSQL 17, uv and pytest.
- [x] Create locked dependency configuration, Docker image, and minimal Django configuration. Add health success/failure tests and observe failure before implementing the endpoint.
- [x] Provision development database roles using initialization scripts; run migrations explicitly. Run pytest on an isolated PostgreSQL test service.
- [x] Add runtime permission checks using rolled-back synthetic rows and denied DDL/role escalation. Verify the real database grants.
- [x] Document setup and existing-volume behavior, verify HTTP startup, migration drift, system checks, and secret exclusions. Review all changes before completion.
Files: config/, core/, tests/, docker/postgres/, manage.py, pyproject.toml,
uv.lock, Dockerfile, .dockerignore, compose.yaml, .env.example and README.md.
No financial models or real data. Application startup never runs migrations.
Verification commands: docker compose config --quiet; docker compose build;
docker compose run --rm manage migrate; docker compose run --rm test;
docker compose run --rm permissions; docker compose run --rm manage check;
docker compose run --rm manage makemigrations --check --dry-run;
docker compose run --rm manage migrate --check; request localhost:8000/health/.
## Verification results (2026-09-06)
- Locked Docker build succeeded; Django 5.2.17 installed under Python 3.13.
- Three pytest tests passed against isolated PostgreSQL, including HTTP success,
generic database failure, and missing-secret validation.
- Fourteen built-in migrations applied on a fresh development volume; system
checks passed and no pending or missing migrations were detected.
- Runtime permission checks passed for DML, sequences, denied DDL, role
escalation, and privileged role attributes.
- Actual HTTP readiness returned 200; a separate container with an unreachable
database returned generic 503 without connection details.
- Independent review identified a temporary-server readiness race. Both database
healthchecks now require TCP. Fresh-volume migrations, permissions and tests
passed again in the isolated gizmoduck-skeleton-verify Compose project.
- Verification-project containers are stopped; its synthetic database volume is
retained. The main local application remains running.
- Documentation links, whitespace, and candidate files were checked for local
secret values. Existing foundation changes remain uncommitted with this work.

103
docs/superpowers/specs/2026-09-06-application-skeleton-design.md

@ -0,0 +1,103 @@
# Runnable Application Skeleton Design
**Date:** 2026-09-06
**Status:** Approved
## Goal and scope
Deliver roadmap step 2: a clean checkout can start Django with PostgreSQL,
apply migrations, and run database-backed pytest checks using documented
commands. No financial domain models, importers, or product UI are included.
## Approach
Use Docker Compose for both application and database. This makes the primary
workflow consistent on Windows and avoids requiring host Python dependencies.
A host-run Django environment is a viable alternative but adds setup variation;
a dual supported workflow adds documentation and verification cost without
helping this milestone. Recommend the Compose-only primary workflow.
Use Python 3.13, Django 5.2 LTS, PostgreSQL 17, psycopg 3, pytest,
pytest-django, and uv with a committed lockfile. Resolve supported patch versions
at implementation time. Freeze dependency installation from the lockfile.
Django documents Python 3.13 compatibility for its 5.2 series:
https://docs.djangoproject.com/en/5.2/releases/5.2/
PostgreSQL 17 is a supported major version:
https://www.postgresql.org/support/versioning/
## Components
- `config/`: Django settings, URLs, WSGI and ASGI entry points.
- `core/`: a database readiness endpoint; no financial models.
- `manage.py`: normal Django management entry point.
- `pyproject.toml` and `uv.lock`: dependency and pytest configuration.
- `Dockerfile`, `.dockerignore`, and `compose.yaml`: reproducible local services.
- `docker/postgres/`: provisioning scripts, using shell files to avoid the
repository's raw SQL exclusion rules.
- `tests/`: endpoint, database, and permission checks using synthetic data.
- `.env.example` and README setup instructions: configuration contract.
Expose the development web server only on host loopback port 8000. PostgreSQL
is reachable through the Compose network without a published host port. Persist
its data in a project-scoped Docker volume. Never delete volumes automatically.
The container build context excludes real-data directories, secrets, Git,
virtual environments, and generated artifacts.
## Configuration and database roles
Require explicit local secrets in ignored `.env`; commit only non-secret example
values and instructions for generating replacements. Read database connection
settings and the Django secret from the environment. Missing required settings
fail clearly without printing credentials. Restrict allowed hosts to localhost
and 127.0.0.1 for the default local setup.
Provision three roles: a PostgreSQL bootstrap administrator used only for
initialization, a development/migration role owning the application schema,
and an application runtime role with connect, schema usage, table DML, and
sequence privileges. Runtime has no superuser, role creation, database creation,
or schema creation permissions. Default privileges cover new objects created
by the migration role. The application service receives only runtime credentials.
A separate management service uses migration credentials for explicit migrate
commands. Application startup never silently runs migrations.
Run tests against a separate PostgreSQL service under a Compose test profile,
with disposable test data and a dedicated test role allowed to create the test
database. Tests cannot target the development database through default settings.
Verify the production-equivalent runtime grants separately against the local
application schema using synthetic records and rollback/cleanup.
## Behavior and failure handling
`GET /health/` executes a small database query and returns a minimal JSON status.
Return HTTP 200 on connectivity success or 503 for database connectivity failure;
never return exception messages or connection details. This is a readiness check,
not proof that every migration is applied. Use migration checks independently.
Compose waits for PostgreSQL readiness. Document the sequence: create local
configuration, build, start the database, run migrations, start web, run tests.
Initialization scripts apply only to new database volumes. Existing-volume role
or credential changes require explicit management; explain this in the README
without recommending destructive resets as the default fix.
## Verification and acceptance
1. Validate Compose configuration without echoing resolved secrets.
2. Build the application image using locked dependencies.
3. Initialize PostgreSQL and apply Django's built-in migrations using the
migration role; check that no migrations are pending or missing.
4. Run Django system checks and pytest against the isolated test database.
5. Verify health success and database-failure behavior without exposing errors.
6. Confirm the runtime role can use migrated tables but cannot create tables or
databases, and cannot assume the migration role.
7. Start the web service and request its health endpoint through localhost.
8. Inspect Git changes for secrets and financial data; check ignore rules for
configuration and verify that required source files remain trackable.
Record actual results, including any Docker availability blocker. No SQLite
fallback is acceptable evidence for PostgreSQL acceptance criteria.
## Deferred decisions
Financial models, source parser contracts, authentication UX, product pages,
background tasks, hosted deployment, and backup implementation belong to later
milestones and do not block this skeleton.

7
manage.py

@ -0,0 +1,7 @@
import os
import sys
from django.core.management import execute_from_command_line
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
execute_from_command_line(sys.argv)

14
pyproject.toml

@ -0,0 +1,14 @@
[project]
name = "gizmoduck"
version = "0.1.0"
requires-python = ">=3.13,<3.14"
dependencies = ["Django>=5.2,<5.3", "psycopg[binary]>=3.2,<4"]
[dependency-groups]
dev = ["pytest>=8,<10", "pytest-django>=4.11,<5"]
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "config.settings"
pythonpath = ["."]
testpaths = ["tests"]
addopts = "-ra"

76
tests/check_permissions.py

@ -0,0 +1,76 @@
"""Run against the migrated development database; all DML is rolled back."""
import os
import uuid
import psycopg
from psycopg.errors import InsufficientPrivilege
def check_permissions():
with psycopg.connect(host=os.environ["DB_HOST"], dbname=os.environ["DB_NAME"],
user=os.environ["DB_USER"], password=os.environ["DB_PASSWORD"],
autocommit=True) as conn:
row = conn.execute("SELECT rolsuper, rolcreatedb, rolcreaterole FROM pg_roles WHERE rolname = current_user").fetchone()
assert row == (False, False, False), "Runtime role has elevated privileges"
with conn.transaction(force_rollback=True):
name = "synthetic_permission_" + uuid.uuid4().hex
pk = conn.execute("INSERT INTO auth_group (name) VALUES (%s) RETURNING id", (name,)).fetchone()[0]
assert conn.execute("SELECT name FROM auth_group WHERE id = %s", (pk,)).fetchone() == (name,)
conn.execute("UPDATE auth_group SET name = %s WHERE id = %s", (name + "_updated", pk))
assert conn.execute("SELECT name FROM auth_group WHERE id = %s", (pk,)).fetchone() == (name + "_updated",)
conn.execute("DELETE FROM auth_group WHERE id = %s", (pk,))
assert conn.execute("SELECT id FROM auth_group WHERE id = %s", (pk,)).fetchone() is None
for statement in ["CREATE TABLE public.permission_probe (id integer)",
"CREATE SCHEMA permission_probe",
"SET ROLE gizmoduck_migrate"]:
try:
with conn.transaction(force_rollback=True):
conn.execute(statement)
except InsufficientPrivilege:
pass
else:
raise AssertionError("Runtime unexpectedly permitted: " + statement)
application_tables = conn.execute(
"SELECT tablename FROM pg_tables "
"WHERE schemaname = 'public' "
"AND (tablename LIKE 'finance_%' OR tablename LIKE 'ingestion_%')"
).fetchall()
assert len(application_tables) >= 11, "Application migrations are missing"
for (table,) in application_tables:
for privilege in ("SELECT", "INSERT", "UPDATE", "DELETE"):
assert conn.execute("SELECT has_table_privilege(current_user, %s, %s)", ("public." + table, privilege)).fetchone()[0], (table, privilege)
with conn.transaction(force_rollback=True):
account_id = uuid.uuid4()
conn.execute("INSERT INTO finance_account (id, name, kind, currency, archived, created_at, updated_at) VALUES (%s, %s, 'BANK', 'BRL', false, now(), now())", (account_id, "synthetic_" + account_id.hex))
assert conn.execute("SELECT currency FROM finance_account WHERE id = %s", (account_id,)).fetchone() == ("BRL",)
conn.execute("UPDATE finance_account SET archived = true WHERE id = %s", (account_id,))
assert conn.execute("SELECT archived FROM finance_account WHERE id = %s", (account_id,)).fetchone() == (True,)
conn.execute("DELETE FROM finance_account WHERE id = %s", (account_id,))
with conn.transaction(force_rollback=True):
batch_id = uuid.uuid4()
conn.execute(
"INSERT INTO ingestion_importbatch "
"(id, created_at, updated_at, document_hash, source_format, "
"profile, parser_version, encoding, context, section_mapping, "
"summary, snapshot, actor_kind, actor_identifier, state, revision, result) "
"VALUES (%s, now(), now(), %s, 'BANK_CSV', 'BANK_CSV_V1', "
"'1', 'utf-8', '{}'::jsonb, '{}'::jsonb, '{}'::jsonb, "
"'{}'::jsonb, 'AGENT', 'synthetic-permission-check', "
"'PREVIEW', 1, '{}'::jsonb)",
(batch_id, "0" * 64),
)
assert conn.execute(
"SELECT state FROM ingestion_importbatch WHERE id = %s", (batch_id,)
).fetchone() == ("PREVIEW",)
conn.execute(
"UPDATE ingestion_importbatch SET state = 'CANCELLED' WHERE id = %s",
(batch_id,),
)
conn.execute("DELETE FROM ingestion_importbatch WHERE id = %s", (batch_id,))
# PostgreSQL CREATE DATABASE cannot run in a rollback transaction. The
# server's role attributes above verify that capability without creating one.
print("PASS: finance/ingestion DML, sequences, denied DDL and role escalation")
if __name__ == "__main__":
check_permissions()

13
tests/test_configuration.py

@ -0,0 +1,13 @@
import os
import subprocess
import sys
def test_missing_secret_fails_without_disclosing_other_credentials():
env = dict(os.environ)
env.pop("DJANGO_SECRET_KEY", None)
env["DB_PASSWORD"] = "private-password-marker"
result = subprocess.run([sys.executable, "manage.py", "check"], env=env, capture_output=True, text=True)
assert result.returncode != 0
assert "Set DJANGO_SECRET_KEY" in result.stderr
assert "private-password-marker" not in result.stderr

20
tests/test_health.py

@ -0,0 +1,20 @@
from unittest.mock import patch
import pytest
from django.db import OperationalError, connection
@pytest.mark.django_db
def test_health_checks_postgresql(client):
assert connection.vendor == "postgresql"
response = client.get("/health/")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
def test_health_failure_does_not_disclose_connection_details(client):
with patch("django.db.backends.postgresql.base.DatabaseWrapper.cursor", side_effect=OperationalError("private-host secret-password")):
response = client.get("/health/")
assert response.status_code == 503
assert response.json() == {"status": "unavailable"}
assert b"secret-password" not in response.content

179
uv.lock

@ -0,0 +1,179 @@
version = 1
revision = 3
requires-python = "==3.13.*"
[[package]]
name = "asgiref"
version = "3.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e6/26/3b59f2bdae5f640389becb1f673cded775287f5fc4f816309d9ca9a3f93d/asgiref-3.12.1.tar.gz", hash = "sha256:59dcb51c272ad209d59bed5708a64a333083e86017d7fcdd67498eeab7784340", size = 42378, upload-time = "2026-07-14T09:56:18.087Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/1b/54f4ad77cd8a584fa70746c47df988e002cf1ee1eba43364d46f87803647/asgiref-3.12.1-py3-none-any.whl", hash = "sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094", size = 25478, upload-time = "2026-07-14T09:56:16.926Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "django"
version = "5.2.17"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d5/d8/43e9d000519adceb189620b6869ff88031e046df91c2e9da72f8f6918399/django-5.2.17.tar.gz", hash = "sha256:9d4d93be539a18ab80d058eb515900e10951e04c537c5a6b394fc49528d3251f", size = 10889740, upload-time = "2026-08-04T15:04:03.173Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/f8/ce120525ca78f12b07daf65786679c5d0b54a75285a8958d3ae55e39da35/django-5.2.17-py3-none-any.whl", hash = "sha256:f04fb3b36ee119e1af4fa1d397d5fd6cf12700f49321e84d4f4c642c5b1973db", size = 8315563, upload-time = "2026-08-04T15:03:59.1Z" },
]
[[package]]
name = "gizmoduck"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "django" },
{ name = "psycopg", extra = ["binary"] },
]
[package.dev-dependencies]
dev = [
{ name = "pytest" },
{ name = "pytest-django" },
]
[package.metadata]
requires-dist = [
{ name = "django", specifier = ">=5.2,<5.3" },
{ name = "psycopg", extras = ["binary"], specifier = ">=3.2,<4" },
]
[package.metadata.requires-dev]
dev = [
{ name = "pytest", specifier = ">=8,<10" },
{ name = "pytest-django", specifier = ">=4.11,<5" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "packaging"
version = "26.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "psycopg"
version = "3.3.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/72/73/8fb739d0f6bba247b9b93c9840c402a4f88545be5f1d4b02b23366371c00/psycopg-3.3.5.tar.gz", hash = "sha256:d0a3d9ccf5788af054cbd745278cb02401b5c312aeaafbf2c6144460aec47da4", size = 166508, upload-time = "2026-08-31T22:45:43.151Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/2e/d0a645bcaadde68bd6d93c43f02f14b0191bdda367ce3f7722abe3da744a/psycopg-3.3.5-py3-none-any.whl", hash = "sha256:ce5aa5cdb4f9379f00f487590e5890bfa7df9a164648c969ffa628505e21af4e", size = 213598, upload-time = "2026-08-31T22:39:02.184Z" },
]
[package.optional-dependencies]
binary = [
{ name = "psycopg-binary", marker = "implementation_name != 'pypy'" },
]
[[package]]
name = "psycopg-binary"
version = "3.3.5"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/1c/e718752cc63cf4e99e4a10fd36e3a3364dabdd0819484a24c0d79fbb9685/psycopg_binary-3.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6e85d50b87257fb117675a19ee59daa7bf9a57f6431500adf7059df799232ef4", size = 4704421, upload-time = "2026-08-31T22:42:59.564Z" },
{ url = "https://files.pythonhosted.org/packages/af/cf/a0e748e27c09b92738e4460582d121ba1908be3e36791e150f435e54b332/psycopg_binary-3.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e5becd311f9af8d180bad372f51fb2252fd02cb2073056e2b170c9274f95fe7f", size = 4765054, upload-time = "2026-08-31T22:43:05.421Z" },
{ url = "https://files.pythonhosted.org/packages/39/62/0cbac0266d56c94dd1f702d9af2b5d54bf80b6e658048f4f2c5bd63dd7e3/psycopg_binary-3.3.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:19e5bf9872dbd164c220567fd385ba2309c7d9df1541f78343510c6b0f36a1b7", size = 5547137, upload-time = "2026-08-31T22:43:11.768Z" },
{ url = "https://files.pythonhosted.org/packages/fb/3a/73c6f8871f38fc07a9c0b4cbc9467beb116a2783cecf87dfa53900a396dc/psycopg_binary-3.3.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb3b3bffebfe07110730626e76238161124f35ac87b748d663316a28d22f58b0", size = 5227577, upload-time = "2026-08-31T22:43:20.767Z" },
{ url = "https://files.pythonhosted.org/packages/59/7b/9f17b9f4d297b774dc574199c4dcd02dadc32a00c4056265918de5c70635/psycopg_binary-3.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2111f880add40fb03c60556069ad68e884a0908a74d2debafc603caf93b73552", size = 6824606, upload-time = "2026-08-31T22:43:33.698Z" },
{ url = "https://files.pythonhosted.org/packages/94/86/d84dadd94a004dbbb43ce0579f1f766fdc6b8cbef745090e24bea77b5283/psycopg_binary-3.3.5-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:40505676b1526b9ea387dace034040a8c8b0bcf984cd6bd4720a2ab15e813586", size = 5060854, upload-time = "2026-08-31T22:43:42.258Z" },
{ url = "https://files.pythonhosted.org/packages/30/d0/e5078be2c7d2490c0d6cd4cb7b3601aec44be2fa8b3fe927e9419b06004f/psycopg_binary-3.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5816472e3bb05615f33a741e0835043d1f4bf9709ff30d2f4aed71815cfc6b5e", size = 4589511, upload-time = "2026-08-31T22:43:50.012Z" },
{ url = "https://files.pythonhosted.org/packages/f1/01/08dfb5b18fa482e025864fd91a022310d0782c42e4cf5dda5d0e010b6790/psycopg_binary-3.3.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:358748fc4c8ccdc0e2bdf55420494930e19c3ade586ea9c3a6de3dad1f897311", size = 4268144, upload-time = "2026-08-31T22:43:56.993Z" },
{ url = "https://files.pythonhosted.org/packages/88/b9/cb01dc1d63f3241b49b2ca7fb9f98d0f5c76127f0dbb9440635a6ad0233e/psycopg_binary-3.3.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1ef2e498be47800f6202b9a2304c22646325ca6d54001b7c785bcfdb24a1e8ab", size = 4001036, upload-time = "2026-08-31T22:44:04.429Z" },
{ url = "https://files.pythonhosted.org/packages/95/49/7c17dd832c05b380562ff2ff5f6ab2bcaeca8b7fff2c2b355854368a5bdb/psycopg_binary-3.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:88e01aa2e938a45655a8a5213fc3a44ba78cb4cab8a569b3e0bcb3d1d0eaba16", size = 4313112, upload-time = "2026-08-31T22:44:12.446Z" },
{ url = "https://files.pythonhosted.org/packages/7c/2c/b0b2f887185d6a2ec0b3bef948cc07656d2d1a5d96fa7f2bb03f6ef06ca4/psycopg_binary-3.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:ba466011569297114449df9d523438e1adeedf3e4f31ffb78e897ec3fef3076b", size = 3647313, upload-time = "2026-08-31T22:44:19.139Z" },
]
[[package]]
name = "pygments"
version = "2.21.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
]
[[package]]
name = "pytest"
version = "9.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
]
[[package]]
name = "pytest-django"
version = "4.14.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/44/f6/3851312120c2bf2f19cafff931e75059aad1ba670703cd751e2fde9bc942/pytest_django-4.14.0.tar.gz", hash = "sha256:26787dd3f422cfbab8f55b80a776e2edea7a11092cb74e960bef1312515708ef", size = 94700, upload-time = "2026-08-10T14:13:08.319Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/03/850bffad2b581c440ca51c039d74504d5a422c94bda0bdb8a8ba5068d48b/pytest_django-4.14.0-py3-none-any.whl", hash = "sha256:c533b08d89cc675efcd5398eea270b34547e35f9a3608e2c9748dd88428ea187", size = 27067, upload-time = "2026-08-10T14:13:06.998Z" },
]
[[package]]
name = "sqlparse"
version = "0.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5f/d3/3f06a1006f2261d1342aefb3c71eed02f5d4ca5bdbecd86ebc12ad38306e/sqlparse-0.6.0.tar.gz", hash = "sha256:113c35c75365ab9cc9c7231d68c6428fb11c085fc8e9eb1ad659b7ddbf6cd2b9", size = 178477, upload-time = "2026-08-13T19:16:06.396Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d9/50/f00935da0ec7cbf325f8dc4f772ae46fbc7b672dd62876e73f0a94adda57/sqlparse-0.6.0-py3-none-any.whl", hash = "sha256:b861c0288ce2fa56209a9a6412d2e066ac664b3873b89c26c9d8415e8e32996f", size = 50070, upload-time = "2026-08-13T19:16:04.062Z" },
]
[[package]]
name = "tzdata"
version = "2026.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" },
]
Loading…
Cancel
Save