15 changed files with 496 additions and 0 deletions
@ -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,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() |
||||
@ -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,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 |
||||
@ -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 |
||||
@ -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. |
||||
@ -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. |
||||
@ -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) |
||||
@ -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" |
||||
@ -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() |
||||
@ -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 |
||||
@ -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 |
||||
@ -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…
Reference in new issue