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