feat(my-deepagent): v0.2 PR #1 — Postgres migration (ahead of M8-Py FastAPI)
Switches the production backing store from SQLite to PostgreSQL 16, per DR-2.
The migration trigger is two concurrent writers on the my-deepagent ORM
tables — which first appears with FastAPI (M8-Py). Doing the cut now keeps
the surface area small while M8-Py is still planning.
Production deps: `asyncpg`, `psycopg[binary]`, `langgraph-checkpoint-postgres`.
Test deps: `aiosqlite` (the bulk of unit + integration tests stay on sqlite
tmp_path for speed; the E2E suite and the new checkpointer tests exercise
the live Postgres path).
Highlights
- `persistence/db.py`: dialect-aware connect listener. SQLite still gets
WAL + busy_timeout=5000 + foreign_keys=ON; Postgres gets `SET TIME ZONE 'UTC'`.
Added `Database.dialect_name` + `drop_schema` (test-only).
- `persistence/checkpointer.py`: SqliteSaver → AsyncPostgresSaver. API is
now async (`async with`) and takes a connection string. SQLAlchemy URL
prefixes (`+asyncpg`, `+psycopg`) are auto-stripped to a plain libpq DSN
(`_to_psycopg_dsn` helper, 4 unit tests).
- `persistence/upsert.py` (new): `insert_for(session)` — dialect-aware UPSERT
helper. Picks `postgresql.insert` or `sqlite.insert` based on the bound
engine. Replaces 5 hardcoded `sqlite_insert` call sites in `budget.py`,
`recovery.py`, `cli/doctor.py`.
- `persistence/models.py`: `RunRow` partial unique index declares both
`postgresql_where=` and `sqlite_where=` for cross-dialect correctness.
- `config.py`: default `database_url` now
`postgresql+asyncpg://devflow:devflow@localhost:55432/mydeepagent`. v3
`devflow` DB preserved untouched; v4 lives in a fresh `mydeepagent` DB.
- `cli/doctor.py` check 8: dialect-aware DB liveness probe. Postgres path
runs `SELECT 1` (pg_isready equivalent); SQLite keeps `PRAGMA integrity_check`.
- `alembic/env.py`: env-aware URL resolution (`MYDEEPAGENT_DATABASE_URL` >
`DATABASE_URL` > default). Async driver prefixes are mapped to the sync
equivalents alembic needs.
- `alembic/versions/9f2a6c79667e_v0_2_baseline_schema_postgres.py` (new):
fresh baseline autogenerated against live Postgres. Old SQLite migrations
(`79945fdc2649`, `839f2233e346`) deleted — v0.2 starts a clean history.
- `tests/conftest.py` (new): `pg_db_url` async fixture creates a fresh DB
per test against docker-compose `devflow-postgres` and drops it on
teardown after terminating lingering backends.
- `tests/integration/test_checkpointer.py`: rewritten for AsyncPostgresSaver
(4 pure DSN-converter unit tests + 3 async context-manager integration tests).
- `tests/integration/test_e2e_workflow.py`: switched to `pg_db_url`. Real
OpenRouter E2E now exercises the production Postgres path end-to-end.
Recovery
- Previous SQLite database at the platformdirs data_dir is NOT auto-migrated;
v0.1.0 was the only release that wrote to it. Set
`MYDEEPAGENT_DATABASE_URL=sqlite+aiosqlite:///<path>` to read it.
- The v3 `devflow` Postgres DB is preserved untouched (separate database
name); to inspect: `psql -h localhost -p 55432 -U devflow -d devflow`.
Gates
- ruff check + ruff format --check + mypy --strict: PASS (102 source files)
- pytest non-E2E: 576 PASS (5.46 s)
- pytest E2E real OpenRouter on Postgres: 1 PASS (122.93 s, ~$0.05/run)
--no-verify: lefthook still TS-only (deleted in 0e61b2d but still queryable
in git history).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,74 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **v0.2 PR #1 — Postgres migration**: production backing store switched from
|
||||||
|
SQLite to PostgreSQL 16 ahead of M8-Py (FastAPI) per DR-2.
|
||||||
|
- `pyproject.toml`: `asyncpg>=0.30` + `psycopg[binary]>=3.2` +
|
||||||
|
`langgraph-checkpoint-postgres>=2.0.0` added to runtime; `aiosqlite>=0.20`
|
||||||
|
moved to `[dependency-groups].dev` (test-only); `langgraph-checkpoint-sqlite`
|
||||||
|
removed.
|
||||||
|
- `src/my_deepagent/persistence/db.py`: dialect-aware connect listener —
|
||||||
|
SQLite still gets `WAL` + `busy_timeout=5000` + `foreign_keys=ON`, Postgres
|
||||||
|
gets `SET TIME ZONE 'UTC'`. New `Database.dialect_name` property + `drop_schema`
|
||||||
|
method for tests.
|
||||||
|
- `src/my_deepagent/persistence/checkpointer.py`: `SqliteSaver` →
|
||||||
|
`AsyncPostgresSaver`. API is now async (`async with`) and takes a
|
||||||
|
connection string; SQLAlchemy URL prefixes (`postgresql+asyncpg://`,
|
||||||
|
`postgresql+psycopg://`) are auto-stripped to a plain libpq DSN. New
|
||||||
|
`_to_psycopg_dsn` helper covered by 4 unit tests.
|
||||||
|
- `src/my_deepagent/persistence/upsert.py` (new): `insert_for(session)` —
|
||||||
|
dialect-aware UPSERT helper. Picks `postgresql.insert` or `sqlite.insert`
|
||||||
|
based on the bound engine's dialect. Replaces 5 hardcoded `sqlite_insert`
|
||||||
|
call sites in `budget.py`, `recovery.py`, and `cli/doctor.py`.
|
||||||
|
- `src/my_deepagent/config.py`: `database_url` default switched from
|
||||||
|
`sqlite+aiosqlite:///<data_dir>/database.sqlite3` to
|
||||||
|
`postgresql+asyncpg://devflow:devflow@localhost:55432/mydeepagent`. The v3
|
||||||
|
`devflow` DB is preserved untouched; v4 lives in a fresh `mydeepagent` DB.
|
||||||
|
- `src/my_deepagent/persistence/models.py`: `RunRow.__table_args__` partial
|
||||||
|
unique index now declares **both** `postgresql_where=` and `sqlite_where=`
|
||||||
|
so the index is partial on both dialects.
|
||||||
|
- `src/my_deepagent/cli/doctor.py`: check 8 (`disk+db`) is now dialect-aware
|
||||||
|
— Postgres path runs `SELECT 1` (pg_isready equivalent: proves
|
||||||
|
reachability + auth + DB exists); SQLite path keeps
|
||||||
|
`PRAGMA integrity_check`. Doctor docstring updated.
|
||||||
|
- `alembic/env.py`: env-aware URL resolution — `MYDEEPAGENT_DATABASE_URL` >
|
||||||
|
`DATABASE_URL` > Postgres default. Async driver prefixes
|
||||||
|
(`+asyncpg`, `+aiosqlite`) are mapped to the sync equivalents alembic
|
||||||
|
needs (`+psycopg`, plain `sqlite`).
|
||||||
|
- `alembic/versions/9f2a6c79667e_v0_2_baseline_schema_postgres.py` (new):
|
||||||
|
fresh baseline autogenerated against live Postgres. Old SQLite baseline
|
||||||
|
`79945fdc2649` + partial-index migration `839f2233e346` deleted.
|
||||||
|
- `tests/conftest.py` (new): `pg_db_url` async fixture. Creates a fresh
|
||||||
|
Postgres database per test (against docker-compose `devflow-postgres`)
|
||||||
|
via the maintenance `postgres` DB; drops on teardown after terminating
|
||||||
|
any lingering backends. Used by the E2E suite and the new checkpointer
|
||||||
|
tests.
|
||||||
|
- `tests/integration/test_checkpointer.py`: rewritten for AsyncPostgresSaver
|
||||||
|
(4 pure DSN-converter tests + 3 async context-manager tests).
|
||||||
|
- `tests/integration/test_e2e_workflow.py`: switched from `sqlite+aiosqlite`
|
||||||
|
tmp_path to `pg_db_url`. Real OpenRouter E2E now exercises the production
|
||||||
|
Postgres path end-to-end (~122 s, ~$0.05/run).
|
||||||
|
|
||||||
|
### Migration trigger (per DR-2)
|
||||||
|
- The bound is *two concurrent writers* on `runs` / `run_phases` / `llm_calls`.
|
||||||
|
Today the CLI is the only writer — but M8-Py (FastAPI) introduces a second
|
||||||
|
one, and SQLite WAL allows only a single concurrent writer. Doing the move
|
||||||
|
*before* M8-Py lands gives the test surface time to harden.
|
||||||
|
- Recovery: previous SQLite database at
|
||||||
|
`~/Library/Application Support/my-deepagent/database.sqlite3` (macOS) /
|
||||||
|
`$XDG_DATA_HOME/my-deepagent/database.sqlite3` is **not migrated** —
|
||||||
|
v0.1.0 was the only release that wrote to it and v0.2 starts a fresh
|
||||||
|
history. Set `MYDEEPAGENT_DATABASE_URL=sqlite+aiosqlite:///<path>` to
|
||||||
|
read the legacy file if needed.
|
||||||
|
|
||||||
|
### Gates
|
||||||
|
- ruff check + ruff format --check + mypy --strict: PASS (102 source files)
|
||||||
|
- pytest non-E2E: 576 PASS (5.46 s) — bulk on sqlite tmp_path, new
|
||||||
|
checkpointer suite on Postgres `pg_db_url`
|
||||||
|
- pytest E2E real OpenRouter: 1 PASS (122.93 s) on Postgres backend
|
||||||
|
|
||||||
|
|
||||||
## [0.1.0] - 2026-05-16
|
## [0.1.0] - 2026-05-16
|
||||||
|
|
||||||
First tagged milestone of the Python rewrite. The pre-Python-rewrite TypeScript
|
First tagged milestone of the Python rewrite. The pre-Python-rewrite TypeScript
|
||||||
|
|||||||
@@ -9,11 +9,27 @@ from alembic import context
|
|||||||
# access to the values within the .ini file in use.
|
# access to the values within the .ini file in use.
|
||||||
config = context.config
|
config = context.config
|
||||||
|
|
||||||
# Load DATABASE_URL from environment, falling back to a local SQLite file.
|
# Resolve DATABASE_URL from environment (MYDEEPAGENT_DATABASE_URL takes
|
||||||
# Alembic uses synchronous SQLAlchemy, so strip the async driver prefix when
|
# precedence, then DATABASE_URL, then the v0.2 Postgres default).
|
||||||
# present (sqlite+aiosqlite:// → sqlite://).
|
# Alembic uses synchronous SQLAlchemy, so async driver prefixes are stripped:
|
||||||
_raw_url: str = os.environ.get("DATABASE_URL", "sqlite:///./database.sqlite3")
|
# postgresql+asyncpg:// → postgresql://
|
||||||
_sync_url: str = _raw_url.replace("sqlite+aiosqlite://", "sqlite://")
|
# postgresql+psycopg:// → postgresql:// (kept for psycopg sync default)
|
||||||
|
# sqlite+aiosqlite:// → sqlite:// (legacy / test override)
|
||||||
|
_raw_url: str = (
|
||||||
|
os.environ.get("MYDEEPAGENT_DATABASE_URL")
|
||||||
|
or os.environ.get("DATABASE_URL")
|
||||||
|
or "postgresql://devflow:devflow@localhost:55432/mydeepagent"
|
||||||
|
)
|
||||||
|
# Alembic always runs synchronously. Convert async drivers to a sync equivalent
|
||||||
|
# that we know is installed: psycopg (v3) for Postgres, the stdlib sqlite3 for
|
||||||
|
# SQLite. Do NOT fall through to a bare `postgresql://` URL because SQLAlchemy
|
||||||
|
# would then try to import psycopg2 (which is not in our deps).
|
||||||
|
_sync_url: str = _raw_url.replace("postgresql+asyncpg://", "postgresql+psycopg://").replace(
|
||||||
|
"sqlite+aiosqlite://", "sqlite://"
|
||||||
|
)
|
||||||
|
# Allow a bare `postgresql://` URL too — it has to be promoted to psycopg.
|
||||||
|
if _sync_url.startswith("postgresql://"):
|
||||||
|
_sync_url = "postgresql+psycopg://" + _sync_url[len("postgresql://") :]
|
||||||
config.set_main_option("sqlalchemy.url", _sync_url)
|
config.set_main_option("sqlalchemy.url", _sync_url)
|
||||||
|
|
||||||
# Interpret the config file for Python logging.
|
# Interpret the config file for Python logging.
|
||||||
|
|||||||
@@ -1,638 +0,0 @@
|
|||||||
"""add active-run partial unique index and FK constraints
|
|
||||||
|
|
||||||
Revision ID: 839f2233e346
|
|
||||||
Revises: 79945fdc2649
|
|
||||||
Create Date: 2026-05-15 18:51:14.343577
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- P0-1: Adds partial unique index ux_active_run_repo_base on runs(repo_path, base_branch)
|
|
||||||
WHERE state NOT IN ('completed', 'failed', 'aborted'). SQLAlchemy autogenerate
|
|
||||||
cannot detect sqlite_where clauses, so this index is managed manually.
|
|
||||||
- P0-3: Adds FK constraints that were missing in the baseline migration:
|
|
||||||
* runs.template_id -> workflow_templates.id RESTRICT
|
|
||||||
* run_bindings.persona_id -> agent_personas.id RESTRICT
|
|
||||||
* interactive_sessions.persona_id -> agent_personas.id RESTRICT
|
|
||||||
* run_events.phase_id -> run_phases.id CASCADE
|
|
||||||
* approval_requests.phase_id -> run_phases.id CASCADE
|
|
||||||
* artifacts.phase_id -> run_phases.id CASCADE
|
|
||||||
* tool_calls.run_id -> runs.id CASCADE
|
|
||||||
* tool_calls.phase_id -> run_phases.id CASCADE
|
|
||||||
* tool_calls.interactive_session_id -> interactive_sessions.id CASCADE
|
|
||||||
* llm_calls.run_id -> runs.id CASCADE
|
|
||||||
* llm_calls.phase_id -> run_phases.id CASCADE
|
|
||||||
* llm_calls.interactive_session_id -> interactive_sessions.id CASCADE
|
|
||||||
* phase_feedback.run_id -> runs.id CASCADE
|
|
||||||
* phase_feedback.phase_id -> run_phases.id CASCADE
|
|
||||||
- runs.current_phase_id intentionally has NO FK: it forms a circular reference with
|
|
||||||
run_phases.run_id. SQLite does not support deferrable FK constraints in the same
|
|
||||||
way as PostgreSQL, so referential integrity for this column is enforced by
|
|
||||||
application code rather than the database.
|
|
||||||
- SQLite does not support ADD CONSTRAINT via ALTER TABLE. All FK additions are done
|
|
||||||
by recreating the affected tables (copy-data-drop-rename pattern).
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from collections.abc import Sequence
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "839f2233e346"
|
|
||||||
down_revision: str | Sequence[str] | None = "79945fdc2649"
|
|
||||||
branch_labels: str | Sequence[str] | None = None
|
|
||||||
depends_on: str | Sequence[str] | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema.
|
|
||||||
|
|
||||||
SQLite does not support ALTER TABLE ... ADD CONSTRAINT, so each table that needs
|
|
||||||
a new FK is rebuilt using the standard SQLite table-rename pattern:
|
|
||||||
1. Disable FK enforcement during rebuild (PRAGMA foreign_keys=OFF).
|
|
||||||
2. Create new table with correct FK constraints.
|
|
||||||
3. Copy data from old table.
|
|
||||||
4. Drop old table.
|
|
||||||
5. Rename new table to original name.
|
|
||||||
6. Re-enable FK enforcement (PRAGMA foreign_keys=ON).
|
|
||||||
|
|
||||||
Indexes and unique constraints referencing the old table are also recreated.
|
|
||||||
"""
|
|
||||||
# Disable FK enforcement during table rebuild to avoid constraint violations
|
|
||||||
# while the old tables (with no FK columns) are temporarily inconsistent.
|
|
||||||
op.execute("PRAGMA foreign_keys=OFF")
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# runs: add template_id FK (RESTRICT) + P0-1 partial unique index.
|
|
||||||
# Rebuild because SQLite cannot ADD CONSTRAINT.
|
|
||||||
# The partial unique index is created after the rebuild (not before)
|
|
||||||
# because DROP TABLE would destroy any pre-existing index on the old table.
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE runs_new (
|
|
||||||
id TEXT NOT NULL,
|
|
||||||
template_id TEXT NOT NULL
|
|
||||||
REFERENCES workflow_templates (id) ON DELETE RESTRICT,
|
|
||||||
template_hash TEXT NOT NULL,
|
|
||||||
state TEXT NOT NULL,
|
|
||||||
repo_path TEXT NOT NULL,
|
|
||||||
base_branch TEXT NOT NULL,
|
|
||||||
worktree_root TEXT NOT NULL,
|
|
||||||
current_phase_id TEXT,
|
|
||||||
started_at TEXT,
|
|
||||||
ended_at TEXT,
|
|
||||||
final_report_path TEXT,
|
|
||||||
paused_from_state TEXT,
|
|
||||||
created_at TEXT NOT NULL,
|
|
||||||
updated_at TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (id)
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"INSERT INTO runs_new SELECT id, template_id, template_hash, state, "
|
|
||||||
"repo_path, base_branch, worktree_root, current_phase_id, "
|
|
||||||
"started_at, ended_at, final_report_path, paused_from_state, "
|
|
||||||
"created_at, updated_at FROM runs"
|
|
||||||
)
|
|
||||||
op.execute("DROP TABLE runs")
|
|
||||||
op.execute("ALTER TABLE runs_new RENAME TO runs")
|
|
||||||
# P0-1: partial unique index — created after the rebuild.
|
|
||||||
op.execute(
|
|
||||||
"CREATE UNIQUE INDEX ux_active_run_repo_base "
|
|
||||||
"ON runs (repo_path, base_branch) "
|
|
||||||
"WHERE state NOT IN ('completed', 'failed', 'aborted')"
|
|
||||||
)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# run_bindings: add persona_id FK (RESTRICT)
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE run_bindings_new (
|
|
||||||
id TEXT NOT NULL,
|
|
||||||
run_id TEXT NOT NULL
|
|
||||||
REFERENCES runs (id) ON DELETE CASCADE,
|
|
||||||
role_id TEXT NOT NULL,
|
|
||||||
persona_id TEXT NOT NULL
|
|
||||||
REFERENCES agent_personas (id) ON DELETE RESTRICT,
|
|
||||||
persona_hash TEXT NOT NULL,
|
|
||||||
backend TEXT NOT NULL,
|
|
||||||
binding_hash TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
UNIQUE (run_id, role_id)
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"INSERT INTO run_bindings_new SELECT id, run_id, role_id, persona_id, "
|
|
||||||
"persona_hash, backend, binding_hash FROM run_bindings"
|
|
||||||
)
|
|
||||||
op.execute("DROP TABLE run_bindings")
|
|
||||||
op.execute("ALTER TABLE run_bindings_new RENAME TO run_bindings")
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# interactive_sessions: add persona_id FK (RESTRICT)
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE interactive_sessions_new (
|
|
||||||
id TEXT NOT NULL,
|
|
||||||
persona_id TEXT NOT NULL
|
|
||||||
REFERENCES agent_personas (id) ON DELETE RESTRICT,
|
|
||||||
persona_hash TEXT NOT NULL,
|
|
||||||
started_at TEXT,
|
|
||||||
ended_at TEXT,
|
|
||||||
last_message_at TEXT,
|
|
||||||
state TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (id)
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"INSERT INTO interactive_sessions_new SELECT id, persona_id, persona_hash, "
|
|
||||||
"started_at, ended_at, last_message_at, state FROM interactive_sessions"
|
|
||||||
)
|
|
||||||
op.execute("DROP TABLE interactive_sessions")
|
|
||||||
op.execute("ALTER TABLE interactive_sessions_new RENAME TO interactive_sessions")
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# run_events: add phase_id FK (CASCADE)
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE run_events_new (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
run_id TEXT NOT NULL
|
|
||||||
REFERENCES runs (id) ON DELETE CASCADE,
|
|
||||||
phase_id TEXT
|
|
||||||
REFERENCES run_phases (id) ON DELETE CASCADE,
|
|
||||||
seq INTEGER NOT NULL,
|
|
||||||
type TEXT NOT NULL,
|
|
||||||
payload JSON NOT NULL,
|
|
||||||
idempotency_key TEXT NOT NULL,
|
|
||||||
ts TEXT NOT NULL,
|
|
||||||
UNIQUE (run_id, seq),
|
|
||||||
UNIQUE (run_id, idempotency_key)
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"INSERT INTO run_events_new SELECT id, run_id, phase_id, seq, type, "
|
|
||||||
"payload, idempotency_key, ts FROM run_events"
|
|
||||||
)
|
|
||||||
op.execute("DROP INDEX IF EXISTS run_events_run_id_ts_idx")
|
|
||||||
op.execute("DROP TABLE run_events")
|
|
||||||
op.execute("ALTER TABLE run_events_new RENAME TO run_events")
|
|
||||||
op.execute("CREATE INDEX run_events_run_id_ts_idx ON run_events (run_id, ts)")
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# approval_requests: add phase_id FK (CASCADE)
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE approval_requests_new (
|
|
||||||
id TEXT NOT NULL,
|
|
||||||
run_id TEXT NOT NULL
|
|
||||||
REFERENCES runs (id) ON DELETE CASCADE,
|
|
||||||
phase_id TEXT
|
|
||||||
REFERENCES run_phases (id) ON DELETE CASCADE,
|
|
||||||
gate_key TEXT NOT NULL,
|
|
||||||
state TEXT NOT NULL,
|
|
||||||
idempotency_key TEXT NOT NULL,
|
|
||||||
payload JSON NOT NULL,
|
|
||||||
created_at TEXT NOT NULL,
|
|
||||||
resolved_at TEXT,
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
UNIQUE (idempotency_key)
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"INSERT INTO approval_requests_new SELECT id, run_id, phase_id, gate_key, "
|
|
||||||
"state, idempotency_key, payload, created_at, resolved_at FROM approval_requests"
|
|
||||||
)
|
|
||||||
op.execute("DROP TABLE approval_requests")
|
|
||||||
op.execute("ALTER TABLE approval_requests_new RENAME TO approval_requests")
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# artifacts: add phase_id FK (CASCADE)
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE artifacts_new (
|
|
||||||
id TEXT NOT NULL,
|
|
||||||
run_id TEXT NOT NULL
|
|
||||||
REFERENCES runs (id) ON DELETE CASCADE,
|
|
||||||
phase_id TEXT
|
|
||||||
REFERENCES run_phases (id) ON DELETE CASCADE,
|
|
||||||
path TEXT NOT NULL,
|
|
||||||
schema_id TEXT NOT NULL,
|
|
||||||
hash TEXT NOT NULL,
|
|
||||||
valid INTEGER NOT NULL,
|
|
||||||
validation_error JSON,
|
|
||||||
created_at TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
UNIQUE (run_id, path, hash)
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"INSERT INTO artifacts_new SELECT id, run_id, phase_id, path, schema_id, "
|
|
||||||
"hash, valid, validation_error, created_at FROM artifacts"
|
|
||||||
)
|
|
||||||
op.execute("DROP TABLE artifacts")
|
|
||||||
op.execute("ALTER TABLE artifacts_new RENAME TO artifacts")
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# tool_calls: add run_id / phase_id / interactive_session_id FKs (CASCADE)
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE tool_calls_new (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
run_id TEXT
|
|
||||||
REFERENCES runs (id) ON DELETE CASCADE,
|
|
||||||
phase_id TEXT
|
|
||||||
REFERENCES run_phases (id) ON DELETE CASCADE,
|
|
||||||
interactive_session_id TEXT
|
|
||||||
REFERENCES interactive_sessions (id) ON DELETE CASCADE,
|
|
||||||
tool_name TEXT NOT NULL,
|
|
||||||
args JSON NOT NULL,
|
|
||||||
result JSON,
|
|
||||||
error TEXT,
|
|
||||||
duration_ms INTEGER NOT NULL,
|
|
||||||
ts TEXT NOT NULL
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"INSERT INTO tool_calls_new SELECT id, run_id, phase_id, interactive_session_id, "
|
|
||||||
"tool_name, args, result, error, duration_ms, ts FROM tool_calls"
|
|
||||||
)
|
|
||||||
op.execute("DROP INDEX IF EXISTS tool_calls_run_id_ts_idx")
|
|
||||||
op.execute("DROP TABLE tool_calls")
|
|
||||||
op.execute("ALTER TABLE tool_calls_new RENAME TO tool_calls")
|
|
||||||
op.execute("CREATE INDEX tool_calls_run_id_ts_idx ON tool_calls (run_id, ts)")
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# llm_calls: add run_id / phase_id / interactive_session_id FKs (CASCADE)
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE llm_calls_new (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
run_id TEXT
|
|
||||||
REFERENCES runs (id) ON DELETE CASCADE,
|
|
||||||
phase_id TEXT
|
|
||||||
REFERENCES run_phases (id) ON DELETE CASCADE,
|
|
||||||
interactive_session_id TEXT
|
|
||||||
REFERENCES interactive_sessions (id) ON DELETE CASCADE,
|
|
||||||
thread_id TEXT NOT NULL,
|
|
||||||
persona_name TEXT NOT NULL,
|
|
||||||
persona_version INTEGER NOT NULL,
|
|
||||||
model TEXT NOT NULL,
|
|
||||||
role TEXT NOT NULL,
|
|
||||||
turn_index INTEGER NOT NULL,
|
|
||||||
input_tokens INTEGER NOT NULL,
|
|
||||||
output_tokens INTEGER NOT NULL,
|
|
||||||
cached_tokens INTEGER NOT NULL,
|
|
||||||
reasoning_tokens INTEGER NOT NULL,
|
|
||||||
cost_usd_input REAL NOT NULL,
|
|
||||||
cost_usd_output REAL NOT NULL,
|
|
||||||
cost_usd_total REAL NOT NULL,
|
|
||||||
latency_ms INTEGER NOT NULL,
|
|
||||||
status TEXT NOT NULL,
|
|
||||||
error_code TEXT,
|
|
||||||
request_id TEXT,
|
|
||||||
ts TEXT NOT NULL
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"INSERT INTO llm_calls_new SELECT id, run_id, phase_id, interactive_session_id, "
|
|
||||||
"thread_id, persona_name, persona_version, model, role, turn_index, "
|
|
||||||
"input_tokens, output_tokens, cached_tokens, reasoning_tokens, "
|
|
||||||
"cost_usd_input, cost_usd_output, cost_usd_total, latency_ms, status, "
|
|
||||||
"error_code, request_id, ts FROM llm_calls"
|
|
||||||
)
|
|
||||||
op.execute("DROP INDEX IF EXISTS llm_calls_run_id_ts_idx")
|
|
||||||
op.execute("DROP INDEX IF EXISTS llm_calls_interactive_session_id_ts_idx")
|
|
||||||
op.execute("DROP INDEX IF EXISTS llm_calls_model_ts_idx")
|
|
||||||
op.execute("DROP TABLE llm_calls")
|
|
||||||
op.execute("ALTER TABLE llm_calls_new RENAME TO llm_calls")
|
|
||||||
op.execute("CREATE INDEX llm_calls_run_id_ts_idx ON llm_calls (run_id, ts)")
|
|
||||||
op.execute(
|
|
||||||
"CREATE INDEX llm_calls_interactive_session_id_ts_idx "
|
|
||||||
"ON llm_calls (interactive_session_id, ts)"
|
|
||||||
)
|
|
||||||
op.execute("CREATE INDEX llm_calls_model_ts_idx ON llm_calls (model, ts)")
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# phase_feedback: add run_id / phase_id FKs (CASCADE)
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE phase_feedback_new (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
run_id TEXT NOT NULL
|
|
||||||
REFERENCES runs (id) ON DELETE CASCADE,
|
|
||||||
phase_id TEXT NOT NULL
|
|
||||||
REFERENCES run_phases (id) ON DELETE CASCADE,
|
|
||||||
reaction TEXT,
|
|
||||||
comment TEXT,
|
|
||||||
created_at TEXT NOT NULL
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"INSERT INTO phase_feedback_new SELECT id, run_id, phase_id, "
|
|
||||||
"reaction, comment, created_at FROM phase_feedback"
|
|
||||||
)
|
|
||||||
op.execute("DROP TABLE phase_feedback")
|
|
||||||
op.execute("ALTER TABLE phase_feedback_new RENAME TO phase_feedback")
|
|
||||||
|
|
||||||
# Re-enable FK enforcement now that all tables have been rebuilt.
|
|
||||||
op.execute("PRAGMA foreign_keys=ON")
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema.
|
|
||||||
|
|
||||||
Reverses all FK additions and drops the partial unique index.
|
|
||||||
Tables that were rebuilt are reverted to their pre-upgrade structure
|
|
||||||
(no FK constraints on the affected columns).
|
|
||||||
"""
|
|
||||||
op.execute("PRAGMA foreign_keys=OFF")
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Revert phase_feedback
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE phase_feedback_old (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
run_id TEXT NOT NULL,
|
|
||||||
phase_id TEXT NOT NULL,
|
|
||||||
reaction TEXT,
|
|
||||||
comment TEXT,
|
|
||||||
created_at TEXT NOT NULL
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"INSERT INTO phase_feedback_old SELECT id, run_id, phase_id, "
|
|
||||||
"reaction, comment, created_at FROM phase_feedback"
|
|
||||||
)
|
|
||||||
op.execute("DROP TABLE phase_feedback")
|
|
||||||
op.execute("ALTER TABLE phase_feedback_old RENAME TO phase_feedback")
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Revert llm_calls
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE llm_calls_old (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
run_id TEXT,
|
|
||||||
phase_id TEXT,
|
|
||||||
interactive_session_id TEXT,
|
|
||||||
thread_id TEXT NOT NULL,
|
|
||||||
persona_name TEXT NOT NULL,
|
|
||||||
persona_version INTEGER NOT NULL,
|
|
||||||
model TEXT NOT NULL,
|
|
||||||
role TEXT NOT NULL,
|
|
||||||
turn_index INTEGER NOT NULL,
|
|
||||||
input_tokens INTEGER NOT NULL,
|
|
||||||
output_tokens INTEGER NOT NULL,
|
|
||||||
cached_tokens INTEGER NOT NULL,
|
|
||||||
reasoning_tokens INTEGER NOT NULL,
|
|
||||||
cost_usd_input REAL NOT NULL,
|
|
||||||
cost_usd_output REAL NOT NULL,
|
|
||||||
cost_usd_total REAL NOT NULL,
|
|
||||||
latency_ms INTEGER NOT NULL,
|
|
||||||
status TEXT NOT NULL,
|
|
||||||
error_code TEXT,
|
|
||||||
request_id TEXT,
|
|
||||||
ts TEXT NOT NULL
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"INSERT INTO llm_calls_old SELECT id, run_id, phase_id, interactive_session_id, "
|
|
||||||
"thread_id, persona_name, persona_version, model, role, turn_index, "
|
|
||||||
"input_tokens, output_tokens, cached_tokens, reasoning_tokens, "
|
|
||||||
"cost_usd_input, cost_usd_output, cost_usd_total, latency_ms, status, "
|
|
||||||
"error_code, request_id, ts FROM llm_calls"
|
|
||||||
)
|
|
||||||
op.execute("DROP INDEX IF EXISTS llm_calls_run_id_ts_idx")
|
|
||||||
op.execute("DROP INDEX IF EXISTS llm_calls_interactive_session_id_ts_idx")
|
|
||||||
op.execute("DROP INDEX IF EXISTS llm_calls_model_ts_idx")
|
|
||||||
op.execute("DROP TABLE llm_calls")
|
|
||||||
op.execute("ALTER TABLE llm_calls_old RENAME TO llm_calls")
|
|
||||||
op.execute("CREATE INDEX llm_calls_run_id_ts_idx ON llm_calls (run_id, ts)")
|
|
||||||
op.execute(
|
|
||||||
"CREATE INDEX llm_calls_interactive_session_id_ts_idx "
|
|
||||||
"ON llm_calls (interactive_session_id, ts)"
|
|
||||||
)
|
|
||||||
op.execute("CREATE INDEX llm_calls_model_ts_idx ON llm_calls (model, ts)")
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Revert tool_calls
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE tool_calls_old (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
run_id TEXT,
|
|
||||||
phase_id TEXT,
|
|
||||||
interactive_session_id TEXT,
|
|
||||||
tool_name TEXT NOT NULL,
|
|
||||||
args JSON NOT NULL,
|
|
||||||
result JSON,
|
|
||||||
error TEXT,
|
|
||||||
duration_ms INTEGER NOT NULL,
|
|
||||||
ts TEXT NOT NULL
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"INSERT INTO tool_calls_old SELECT id, run_id, phase_id, interactive_session_id, "
|
|
||||||
"tool_name, args, result, error, duration_ms, ts FROM tool_calls"
|
|
||||||
)
|
|
||||||
op.execute("DROP INDEX IF EXISTS tool_calls_run_id_ts_idx")
|
|
||||||
op.execute("DROP TABLE tool_calls")
|
|
||||||
op.execute("ALTER TABLE tool_calls_old RENAME TO tool_calls")
|
|
||||||
op.execute("CREATE INDEX tool_calls_run_id_ts_idx ON tool_calls (run_id, ts)")
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Revert artifacts
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE artifacts_old (
|
|
||||||
id TEXT NOT NULL,
|
|
||||||
run_id TEXT NOT NULL
|
|
||||||
REFERENCES runs (id) ON DELETE CASCADE,
|
|
||||||
phase_id TEXT,
|
|
||||||
path TEXT NOT NULL,
|
|
||||||
schema_id TEXT NOT NULL,
|
|
||||||
hash TEXT NOT NULL,
|
|
||||||
valid INTEGER NOT NULL,
|
|
||||||
validation_error JSON,
|
|
||||||
created_at TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
UNIQUE (run_id, path, hash)
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"INSERT INTO artifacts_old SELECT id, run_id, phase_id, path, schema_id, "
|
|
||||||
"hash, valid, validation_error, created_at FROM artifacts"
|
|
||||||
)
|
|
||||||
op.execute("DROP TABLE artifacts")
|
|
||||||
op.execute("ALTER TABLE artifacts_old RENAME TO artifacts")
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Revert approval_requests
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE approval_requests_old (
|
|
||||||
id TEXT NOT NULL,
|
|
||||||
run_id TEXT NOT NULL
|
|
||||||
REFERENCES runs (id) ON DELETE CASCADE,
|
|
||||||
phase_id TEXT,
|
|
||||||
gate_key TEXT NOT NULL,
|
|
||||||
state TEXT NOT NULL,
|
|
||||||
idempotency_key TEXT NOT NULL,
|
|
||||||
payload JSON NOT NULL,
|
|
||||||
created_at TEXT NOT NULL,
|
|
||||||
resolved_at TEXT,
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
UNIQUE (idempotency_key)
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"INSERT INTO approval_requests_old SELECT id, run_id, phase_id, gate_key, "
|
|
||||||
"state, idempotency_key, payload, created_at, resolved_at FROM approval_requests"
|
|
||||||
)
|
|
||||||
op.execute("DROP TABLE approval_requests")
|
|
||||||
op.execute("ALTER TABLE approval_requests_old RENAME TO approval_requests")
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Revert run_events
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE run_events_old (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
run_id TEXT NOT NULL
|
|
||||||
REFERENCES runs (id) ON DELETE CASCADE,
|
|
||||||
phase_id TEXT,
|
|
||||||
seq INTEGER NOT NULL,
|
|
||||||
type TEXT NOT NULL,
|
|
||||||
payload JSON NOT NULL,
|
|
||||||
idempotency_key TEXT NOT NULL,
|
|
||||||
ts TEXT NOT NULL,
|
|
||||||
UNIQUE (run_id, seq),
|
|
||||||
UNIQUE (run_id, idempotency_key)
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"INSERT INTO run_events_old SELECT id, run_id, phase_id, seq, type, "
|
|
||||||
"payload, idempotency_key, ts FROM run_events"
|
|
||||||
)
|
|
||||||
op.execute("DROP INDEX IF EXISTS run_events_run_id_ts_idx")
|
|
||||||
op.execute("DROP TABLE run_events")
|
|
||||||
op.execute("ALTER TABLE run_events_old RENAME TO run_events")
|
|
||||||
op.execute("CREATE INDEX run_events_run_id_ts_idx ON run_events (run_id, ts)")
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Revert interactive_sessions
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE interactive_sessions_old (
|
|
||||||
id TEXT NOT NULL,
|
|
||||||
persona_id TEXT NOT NULL,
|
|
||||||
persona_hash TEXT NOT NULL,
|
|
||||||
started_at TEXT,
|
|
||||||
ended_at TEXT,
|
|
||||||
last_message_at TEXT,
|
|
||||||
state TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (id)
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"INSERT INTO interactive_sessions_old SELECT id, persona_id, persona_hash, "
|
|
||||||
"started_at, ended_at, last_message_at, state FROM interactive_sessions"
|
|
||||||
)
|
|
||||||
op.execute("DROP TABLE interactive_sessions")
|
|
||||||
op.execute("ALTER TABLE interactive_sessions_old RENAME TO interactive_sessions")
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Revert run_bindings
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE run_bindings_old (
|
|
||||||
id TEXT NOT NULL,
|
|
||||||
run_id TEXT NOT NULL
|
|
||||||
REFERENCES runs (id) ON DELETE CASCADE,
|
|
||||||
role_id TEXT NOT NULL,
|
|
||||||
persona_id TEXT NOT NULL,
|
|
||||||
persona_hash TEXT NOT NULL,
|
|
||||||
backend TEXT NOT NULL,
|
|
||||||
binding_hash TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
UNIQUE (run_id, role_id)
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"INSERT INTO run_bindings_old SELECT id, run_id, role_id, persona_id, "
|
|
||||||
"persona_hash, backend, binding_hash FROM run_bindings"
|
|
||||||
)
|
|
||||||
op.execute("DROP TABLE run_bindings")
|
|
||||||
op.execute("ALTER TABLE run_bindings_old RENAME TO run_bindings")
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Revert runs (remove template_id FK)
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
op.execute("DROP INDEX IF EXISTS ux_active_run_repo_base")
|
|
||||||
op.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE runs_old (
|
|
||||||
id TEXT NOT NULL,
|
|
||||||
template_id TEXT NOT NULL,
|
|
||||||
template_hash TEXT NOT NULL,
|
|
||||||
state TEXT NOT NULL,
|
|
||||||
repo_path TEXT NOT NULL,
|
|
||||||
base_branch TEXT NOT NULL,
|
|
||||||
worktree_root TEXT NOT NULL,
|
|
||||||
current_phase_id TEXT,
|
|
||||||
started_at TEXT,
|
|
||||||
ended_at TEXT,
|
|
||||||
final_report_path TEXT,
|
|
||||||
paused_from_state TEXT,
|
|
||||||
created_at TEXT NOT NULL,
|
|
||||||
updated_at TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (id)
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
op.execute(
|
|
||||||
"INSERT INTO runs_old SELECT id, template_id, template_hash, state, "
|
|
||||||
"repo_path, base_branch, worktree_root, current_phase_id, "
|
|
||||||
"started_at, ended_at, final_report_path, paused_from_state, "
|
|
||||||
"created_at, updated_at FROM runs"
|
|
||||||
)
|
|
||||||
op.execute("DROP TABLE runs")
|
|
||||||
op.execute("ALTER TABLE runs_old RENAME TO runs")
|
|
||||||
|
|
||||||
op.execute("PRAGMA foreign_keys=ON")
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
"""baseline schema for v0.1.0
|
"""v0.2 baseline schema (Postgres)
|
||||||
|
|
||||||
Revision ID: 79945fdc2649
|
Revision ID: 9f2a6c79667e
|
||||||
Revises:
|
Revises:
|
||||||
Create Date: 2026-05-15 17:19:09.577439
|
Create Date: 2026-05-16 17:58:43.967026
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ import sqlalchemy as sa
|
|||||||
from alembic import op
|
from alembic import op
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = "79945fdc2649"
|
revision: str = "9f2a6c79667e"
|
||||||
down_revision: str | Sequence[str] | None = None
|
down_revision: str | Sequence[str] | None = None
|
||||||
branch_labels: str | Sequence[str] | None = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: str | Sequence[str] | None = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
@@ -41,51 +41,6 @@ def upgrade() -> None:
|
|||||||
sa.Column("last_updated", sa.Text(), nullable=False),
|
sa.Column("last_updated", sa.Text(), nullable=False),
|
||||||
sa.PrimaryKeyConstraint("scope"),
|
sa.PrimaryKeyConstraint("scope"),
|
||||||
)
|
)
|
||||||
op.create_table(
|
|
||||||
"interactive_sessions",
|
|
||||||
sa.Column("id", sa.String(length=36), nullable=False),
|
|
||||||
sa.Column("persona_id", sa.String(length=36), nullable=False),
|
|
||||||
sa.Column("persona_hash", sa.Text(), nullable=False),
|
|
||||||
sa.Column("started_at", sa.Text(), nullable=True),
|
|
||||||
sa.Column("ended_at", sa.Text(), nullable=True),
|
|
||||||
sa.Column("last_message_at", sa.Text(), nullable=True),
|
|
||||||
sa.Column("state", sa.Text(), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
op.create_table(
|
|
||||||
"llm_calls",
|
|
||||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
|
||||||
sa.Column("run_id", sa.String(length=36), nullable=True),
|
|
||||||
sa.Column("phase_id", sa.String(length=36), nullable=True),
|
|
||||||
sa.Column("interactive_session_id", sa.String(length=36), nullable=True),
|
|
||||||
sa.Column("thread_id", sa.Text(), nullable=False),
|
|
||||||
sa.Column("persona_name", sa.Text(), nullable=False),
|
|
||||||
sa.Column("persona_version", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("model", sa.Text(), nullable=False),
|
|
||||||
sa.Column("role", sa.Text(), nullable=False),
|
|
||||||
sa.Column("turn_index", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("input_tokens", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("output_tokens", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("cached_tokens", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("reasoning_tokens", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("cost_usd_input", sa.Float(), nullable=False),
|
|
||||||
sa.Column("cost_usd_output", sa.Float(), nullable=False),
|
|
||||||
sa.Column("cost_usd_total", sa.Float(), nullable=False),
|
|
||||||
sa.Column("latency_ms", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("status", sa.Text(), nullable=False),
|
|
||||||
sa.Column("error_code", sa.Text(), nullable=True),
|
|
||||||
sa.Column("request_id", sa.Text(), nullable=True),
|
|
||||||
sa.Column("ts", sa.Text(), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
"llm_calls_interactive_session_id_ts_idx",
|
|
||||||
"llm_calls",
|
|
||||||
["interactive_session_id", "ts"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
op.create_index("llm_calls_model_ts_idx", "llm_calls", ["model", "ts"], unique=False)
|
|
||||||
op.create_index("llm_calls_run_id_ts_idx", "llm_calls", ["run_id", "ts"], unique=False)
|
|
||||||
op.create_table(
|
op.create_table(
|
||||||
"model_pricing",
|
"model_pricing",
|
||||||
sa.Column("model", sa.Text(), nullable=False),
|
sa.Column("model", sa.Text(), nullable=False),
|
||||||
@@ -106,14 +61,27 @@ def upgrade() -> None:
|
|||||||
sa.PrimaryKeyConstraint("persona_hash"),
|
sa.PrimaryKeyConstraint("persona_hash"),
|
||||||
)
|
)
|
||||||
op.create_table(
|
op.create_table(
|
||||||
"phase_feedback",
|
"workflow_templates",
|
||||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
sa.Column("id", sa.String(length=36), nullable=False),
|
||||||
sa.Column("run_id", sa.String(length=36), nullable=False),
|
sa.Column("name", sa.Text(), nullable=False),
|
||||||
sa.Column("phase_id", sa.String(length=36), nullable=False),
|
sa.Column("version", sa.Integer(), nullable=False),
|
||||||
sa.Column("reaction", sa.Text(), nullable=True),
|
sa.Column("hash", sa.Text(), nullable=False),
|
||||||
sa.Column("comment", sa.Text(), nullable=True),
|
sa.Column("definition", sa.JSON(), nullable=False),
|
||||||
sa.Column("created_at", sa.Text(), nullable=False),
|
sa.Column("created_at", sa.Text(), nullable=False),
|
||||||
sa.PrimaryKeyConstraint("id"),
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("hash"),
|
||||||
|
)
|
||||||
|
op.create_table(
|
||||||
|
"interactive_sessions",
|
||||||
|
sa.Column("id", sa.String(length=36), nullable=False),
|
||||||
|
sa.Column("persona_id", sa.String(length=36), nullable=False),
|
||||||
|
sa.Column("persona_hash", sa.Text(), nullable=False),
|
||||||
|
sa.Column("started_at", sa.Text(), nullable=True),
|
||||||
|
sa.Column("ended_at", sa.Text(), nullable=True),
|
||||||
|
sa.Column("last_message_at", sa.Text(), nullable=True),
|
||||||
|
sa.Column("state", sa.Text(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(["persona_id"], ["agent_personas.id"], ondelete="RESTRICT"),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
op.create_table(
|
op.create_table(
|
||||||
"runs",
|
"runs",
|
||||||
@@ -131,63 +99,16 @@ def upgrade() -> None:
|
|||||||
sa.Column("paused_from_state", sa.Text(), nullable=True),
|
sa.Column("paused_from_state", sa.Text(), nullable=True),
|
||||||
sa.Column("created_at", sa.Text(), nullable=False),
|
sa.Column("created_at", sa.Text(), nullable=False),
|
||||||
sa.Column("updated_at", sa.Text(), nullable=False),
|
sa.Column("updated_at", sa.Text(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(["template_id"], ["workflow_templates.id"], ondelete="RESTRICT"),
|
||||||
sa.PrimaryKeyConstraint("id"),
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
op.create_table(
|
op.create_index(
|
||||||
"tool_calls",
|
"ux_active_run_repo_base",
|
||||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
"runs",
|
||||||
sa.Column("run_id", sa.String(length=36), nullable=True),
|
["repo_path", "base_branch"],
|
||||||
sa.Column("phase_id", sa.String(length=36), nullable=True),
|
unique=True,
|
||||||
sa.Column("interactive_session_id", sa.String(length=36), nullable=True),
|
postgresql_where=sa.text("state NOT IN ('completed', 'failed', 'aborted')"),
|
||||||
sa.Column("tool_name", sa.Text(), nullable=False),
|
sqlite_where=sa.text("state NOT IN ('completed', 'failed', 'aborted')"),
|
||||||
sa.Column("args", sa.JSON(), nullable=False),
|
|
||||||
sa.Column("result", sa.JSON(), nullable=True),
|
|
||||||
sa.Column("error", sa.Text(), nullable=True),
|
|
||||||
sa.Column("duration_ms", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("ts", sa.Text(), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
op.create_index("tool_calls_run_id_ts_idx", "tool_calls", ["run_id", "ts"], unique=False)
|
|
||||||
op.create_table(
|
|
||||||
"workflow_templates",
|
|
||||||
sa.Column("id", sa.String(length=36), nullable=False),
|
|
||||||
sa.Column("name", sa.Text(), nullable=False),
|
|
||||||
sa.Column("version", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("hash", sa.Text(), nullable=False),
|
|
||||||
sa.Column("definition", sa.JSON(), nullable=False),
|
|
||||||
sa.Column("created_at", sa.Text(), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
sa.UniqueConstraint("hash"),
|
|
||||||
)
|
|
||||||
op.create_table(
|
|
||||||
"approval_requests",
|
|
||||||
sa.Column("id", sa.String(length=36), nullable=False),
|
|
||||||
sa.Column("run_id", sa.String(length=36), nullable=False),
|
|
||||||
sa.Column("phase_id", sa.String(length=36), nullable=True),
|
|
||||||
sa.Column("gate_key", sa.Text(), nullable=False),
|
|
||||||
sa.Column("state", sa.Text(), nullable=False),
|
|
||||||
sa.Column("idempotency_key", sa.Text(), nullable=False),
|
|
||||||
sa.Column("payload", sa.JSON(), nullable=False),
|
|
||||||
sa.Column("created_at", sa.Text(), nullable=False),
|
|
||||||
sa.Column("resolved_at", sa.Text(), nullable=True),
|
|
||||||
sa.ForeignKeyConstraint(["run_id"], ["runs.id"], ondelete="CASCADE"),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
sa.UniqueConstraint("idempotency_key"),
|
|
||||||
)
|
|
||||||
op.create_table(
|
|
||||||
"artifacts",
|
|
||||||
sa.Column("id", sa.String(length=36), nullable=False),
|
|
||||||
sa.Column("run_id", sa.String(length=36), nullable=False),
|
|
||||||
sa.Column("phase_id", sa.String(length=36), nullable=True),
|
|
||||||
sa.Column("path", sa.Text(), nullable=False),
|
|
||||||
sa.Column("schema_id", sa.Text(), nullable=False),
|
|
||||||
sa.Column("hash", sa.Text(), nullable=False),
|
|
||||||
sa.Column("valid", sa.Boolean(), nullable=False),
|
|
||||||
sa.Column("validation_error", sa.JSON(), nullable=True),
|
|
||||||
sa.Column("created_at", sa.Text(), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(["run_id"], ["runs.id"], ondelete="CASCADE"),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
sa.UniqueConstraint("run_id", "path", "hash", name="uq_artifacts_run_path_hash"),
|
|
||||||
)
|
)
|
||||||
op.create_table(
|
op.create_table(
|
||||||
"run_bindings",
|
"run_bindings",
|
||||||
@@ -198,6 +119,7 @@ def upgrade() -> None:
|
|||||||
sa.Column("persona_hash", sa.Text(), nullable=False),
|
sa.Column("persona_hash", sa.Text(), nullable=False),
|
||||||
sa.Column("backend", sa.Text(), nullable=False),
|
sa.Column("backend", sa.Text(), nullable=False),
|
||||||
sa.Column("binding_hash", sa.Text(), nullable=False),
|
sa.Column("binding_hash", sa.Text(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(["persona_id"], ["agent_personas.id"], ondelete="RESTRICT"),
|
||||||
sa.ForeignKeyConstraint(["run_id"], ["runs.id"], ondelete="CASCADE"),
|
sa.ForeignKeyConstraint(["run_id"], ["runs.id"], ondelete="CASCADE"),
|
||||||
sa.PrimaryKeyConstraint("id"),
|
sa.PrimaryKeyConstraint("id"),
|
||||||
sa.UniqueConstraint("run_id", "role_id", name="uq_run_bindings_run_role"),
|
sa.UniqueConstraint("run_id", "role_id", name="uq_run_bindings_run_role"),
|
||||||
@@ -215,22 +137,6 @@ def upgrade() -> None:
|
|||||||
sa.PrimaryKeyConstraint("id"),
|
sa.PrimaryKeyConstraint("id"),
|
||||||
sa.UniqueConstraint("idempotency_key"),
|
sa.UniqueConstraint("idempotency_key"),
|
||||||
)
|
)
|
||||||
op.create_table(
|
|
||||||
"run_events",
|
|
||||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
|
||||||
sa.Column("run_id", sa.String(length=36), nullable=False),
|
|
||||||
sa.Column("phase_id", sa.String(length=36), nullable=True),
|
|
||||||
sa.Column("seq", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("type", sa.Text(), nullable=False),
|
|
||||||
sa.Column("payload", sa.JSON(), nullable=False),
|
|
||||||
sa.Column("idempotency_key", sa.Text(), nullable=False),
|
|
||||||
sa.Column("ts", sa.Text(), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(["run_id"], ["runs.id"], ondelete="CASCADE"),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
sa.UniqueConstraint("run_id", "idempotency_key", name="uq_run_events_run_idempotency"),
|
|
||||||
sa.UniqueConstraint("run_id", "seq", name="uq_run_events_run_seq"),
|
|
||||||
)
|
|
||||||
op.create_index("run_events_run_id_ts_idx", "run_events", ["run_id", "ts"], unique=False)
|
|
||||||
op.create_table(
|
op.create_table(
|
||||||
"run_inputs",
|
"run_inputs",
|
||||||
sa.Column("id", sa.String(length=36), nullable=False),
|
sa.Column("id", sa.String(length=36), nullable=False),
|
||||||
@@ -257,6 +163,126 @@ def upgrade() -> None:
|
|||||||
sa.PrimaryKeyConstraint("id"),
|
sa.PrimaryKeyConstraint("id"),
|
||||||
sa.UniqueConstraint("run_id", "phase_key", name="uq_run_phases_run_phase"),
|
sa.UniqueConstraint("run_id", "phase_key", name="uq_run_phases_run_phase"),
|
||||||
)
|
)
|
||||||
|
op.create_table(
|
||||||
|
"approval_requests",
|
||||||
|
sa.Column("id", sa.String(length=36), nullable=False),
|
||||||
|
sa.Column("run_id", sa.String(length=36), nullable=False),
|
||||||
|
sa.Column("phase_id", sa.String(length=36), nullable=True),
|
||||||
|
sa.Column("gate_key", sa.Text(), nullable=False),
|
||||||
|
sa.Column("state", sa.Text(), nullable=False),
|
||||||
|
sa.Column("idempotency_key", sa.Text(), nullable=False),
|
||||||
|
sa.Column("payload", sa.JSON(), nullable=False),
|
||||||
|
sa.Column("created_at", sa.Text(), nullable=False),
|
||||||
|
sa.Column("resolved_at", sa.Text(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(["phase_id"], ["run_phases.id"], ondelete="CASCADE"),
|
||||||
|
sa.ForeignKeyConstraint(["run_id"], ["runs.id"], ondelete="CASCADE"),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("idempotency_key"),
|
||||||
|
)
|
||||||
|
op.create_table(
|
||||||
|
"artifacts",
|
||||||
|
sa.Column("id", sa.String(length=36), nullable=False),
|
||||||
|
sa.Column("run_id", sa.String(length=36), nullable=False),
|
||||||
|
sa.Column("phase_id", sa.String(length=36), nullable=True),
|
||||||
|
sa.Column("path", sa.Text(), nullable=False),
|
||||||
|
sa.Column("schema_id", sa.Text(), nullable=False),
|
||||||
|
sa.Column("hash", sa.Text(), nullable=False),
|
||||||
|
sa.Column("valid", sa.Boolean(), nullable=False),
|
||||||
|
sa.Column("validation_error", sa.JSON(), nullable=True),
|
||||||
|
sa.Column("created_at", sa.Text(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(["phase_id"], ["run_phases.id"], ondelete="CASCADE"),
|
||||||
|
sa.ForeignKeyConstraint(["run_id"], ["runs.id"], ondelete="CASCADE"),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("run_id", "path", "hash", name="uq_artifacts_run_path_hash"),
|
||||||
|
)
|
||||||
|
op.create_table(
|
||||||
|
"llm_calls",
|
||||||
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column("run_id", sa.String(length=36), nullable=True),
|
||||||
|
sa.Column("phase_id", sa.String(length=36), nullable=True),
|
||||||
|
sa.Column("interactive_session_id", sa.String(length=36), nullable=True),
|
||||||
|
sa.Column("thread_id", sa.Text(), nullable=False),
|
||||||
|
sa.Column("persona_name", sa.Text(), nullable=False),
|
||||||
|
sa.Column("persona_version", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("model", sa.Text(), nullable=False),
|
||||||
|
sa.Column("role", sa.Text(), nullable=False),
|
||||||
|
sa.Column("turn_index", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("input_tokens", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("output_tokens", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("cached_tokens", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("reasoning_tokens", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("cost_usd_input", sa.Float(), nullable=False),
|
||||||
|
sa.Column("cost_usd_output", sa.Float(), nullable=False),
|
||||||
|
sa.Column("cost_usd_total", sa.Float(), nullable=False),
|
||||||
|
sa.Column("latency_ms", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("status", sa.Text(), nullable=False),
|
||||||
|
sa.Column("error_code", sa.Text(), nullable=True),
|
||||||
|
sa.Column("request_id", sa.Text(), nullable=True),
|
||||||
|
sa.Column("ts", sa.Text(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["interactive_session_id"], ["interactive_sessions.id"], ondelete="CASCADE"
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(["phase_id"], ["run_phases.id"], ondelete="CASCADE"),
|
||||||
|
sa.ForeignKeyConstraint(["run_id"], ["runs.id"], ondelete="CASCADE"),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"llm_calls_interactive_session_id_ts_idx",
|
||||||
|
"llm_calls",
|
||||||
|
["interactive_session_id", "ts"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index("llm_calls_model_ts_idx", "llm_calls", ["model", "ts"], unique=False)
|
||||||
|
op.create_index("llm_calls_run_id_ts_idx", "llm_calls", ["run_id", "ts"], unique=False)
|
||||||
|
op.create_table(
|
||||||
|
"phase_feedback",
|
||||||
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column("run_id", sa.String(length=36), nullable=False),
|
||||||
|
sa.Column("phase_id", sa.String(length=36), nullable=False),
|
||||||
|
sa.Column("reaction", sa.Text(), nullable=True),
|
||||||
|
sa.Column("comment", sa.Text(), nullable=True),
|
||||||
|
sa.Column("created_at", sa.Text(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(["phase_id"], ["run_phases.id"], ondelete="CASCADE"),
|
||||||
|
sa.ForeignKeyConstraint(["run_id"], ["runs.id"], ondelete="CASCADE"),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
op.create_table(
|
||||||
|
"run_events",
|
||||||
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column("run_id", sa.String(length=36), nullable=False),
|
||||||
|
sa.Column("phase_id", sa.String(length=36), nullable=True),
|
||||||
|
sa.Column("seq", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("type", sa.Text(), nullable=False),
|
||||||
|
sa.Column("payload", sa.JSON(), nullable=False),
|
||||||
|
sa.Column("idempotency_key", sa.Text(), nullable=False),
|
||||||
|
sa.Column("ts", sa.Text(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(["phase_id"], ["run_phases.id"], ondelete="CASCADE"),
|
||||||
|
sa.ForeignKeyConstraint(["run_id"], ["runs.id"], ondelete="CASCADE"),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("run_id", "idempotency_key", name="uq_run_events_run_idempotency"),
|
||||||
|
sa.UniqueConstraint("run_id", "seq", name="uq_run_events_run_seq"),
|
||||||
|
)
|
||||||
|
op.create_index("run_events_run_id_ts_idx", "run_events", ["run_id", "ts"], unique=False)
|
||||||
|
op.create_table(
|
||||||
|
"tool_calls",
|
||||||
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column("run_id", sa.String(length=36), nullable=True),
|
||||||
|
sa.Column("phase_id", sa.String(length=36), nullable=True),
|
||||||
|
sa.Column("interactive_session_id", sa.String(length=36), nullable=True),
|
||||||
|
sa.Column("tool_name", sa.Text(), nullable=False),
|
||||||
|
sa.Column("args", sa.JSON(), nullable=False),
|
||||||
|
sa.Column("result", sa.JSON(), nullable=True),
|
||||||
|
sa.Column("error", sa.Text(), nullable=True),
|
||||||
|
sa.Column("duration_ms", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("ts", sa.Text(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["interactive_session_id"], ["interactive_sessions.id"], ondelete="CASCADE"
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(["phase_id"], ["run_phases.id"], ondelete="CASCADE"),
|
||||||
|
sa.ForeignKeyConstraint(["run_id"], ["runs.id"], ondelete="CASCADE"),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
op.create_index("tool_calls_run_id_ts_idx", "tool_calls", ["run_id", "ts"], unique=False)
|
||||||
op.create_table(
|
op.create_table(
|
||||||
"approval_decisions",
|
"approval_decisions",
|
||||||
sa.Column("id", sa.String(length=36), nullable=False),
|
sa.Column("id", sa.String(length=36), nullable=False),
|
||||||
@@ -278,26 +304,32 @@ def downgrade() -> None:
|
|||||||
"""Downgrade schema."""
|
"""Downgrade schema."""
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.drop_table("approval_decisions")
|
op.drop_table("approval_decisions")
|
||||||
op.drop_table("run_phases")
|
|
||||||
op.drop_table("run_inputs")
|
|
||||||
op.drop_index("run_events_run_id_ts_idx", table_name="run_events")
|
|
||||||
op.drop_table("run_events")
|
|
||||||
op.drop_table("run_commands")
|
|
||||||
op.drop_table("run_bindings")
|
|
||||||
op.drop_table("artifacts")
|
|
||||||
op.drop_table("approval_requests")
|
|
||||||
op.drop_table("workflow_templates")
|
|
||||||
op.drop_index("tool_calls_run_id_ts_idx", table_name="tool_calls")
|
op.drop_index("tool_calls_run_id_ts_idx", table_name="tool_calls")
|
||||||
op.drop_table("tool_calls")
|
op.drop_table("tool_calls")
|
||||||
op.drop_table("runs")
|
op.drop_index("run_events_run_id_ts_idx", table_name="run_events")
|
||||||
|
op.drop_table("run_events")
|
||||||
op.drop_table("phase_feedback")
|
op.drop_table("phase_feedback")
|
||||||
op.drop_table("persona_consents")
|
|
||||||
op.drop_table("model_pricing")
|
|
||||||
op.drop_index("llm_calls_run_id_ts_idx", table_name="llm_calls")
|
op.drop_index("llm_calls_run_id_ts_idx", table_name="llm_calls")
|
||||||
op.drop_index("llm_calls_model_ts_idx", table_name="llm_calls")
|
op.drop_index("llm_calls_model_ts_idx", table_name="llm_calls")
|
||||||
op.drop_index("llm_calls_interactive_session_id_ts_idx", table_name="llm_calls")
|
op.drop_index("llm_calls_interactive_session_id_ts_idx", table_name="llm_calls")
|
||||||
op.drop_table("llm_calls")
|
op.drop_table("llm_calls")
|
||||||
|
op.drop_table("artifacts")
|
||||||
|
op.drop_table("approval_requests")
|
||||||
|
op.drop_table("run_phases")
|
||||||
|
op.drop_table("run_inputs")
|
||||||
|
op.drop_table("run_commands")
|
||||||
|
op.drop_table("run_bindings")
|
||||||
|
op.drop_index(
|
||||||
|
"ux_active_run_repo_base",
|
||||||
|
table_name="runs",
|
||||||
|
postgresql_where=sa.text("state NOT IN ('completed', 'failed', 'aborted')"),
|
||||||
|
sqlite_where=sa.text("state NOT IN ('completed', 'failed', 'aborted')"),
|
||||||
|
)
|
||||||
|
op.drop_table("runs")
|
||||||
op.drop_table("interactive_sessions")
|
op.drop_table("interactive_sessions")
|
||||||
|
op.drop_table("workflow_templates")
|
||||||
|
op.drop_table("persona_consents")
|
||||||
|
op.drop_table("model_pricing")
|
||||||
op.drop_table("budget_ledger")
|
op.drop_table("budget_ledger")
|
||||||
op.drop_table("agent_personas")
|
op.drop_table("agent_personas")
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
@@ -4,7 +4,8 @@ version = "0.1.0"
|
|||||||
description = "Add your description here"
|
description = "Add your description here"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aiosqlite>=0.20",
|
"asyncpg>=0.30",
|
||||||
|
"psycopg[binary]>=3.2",
|
||||||
"alembic>=1.14",
|
"alembic>=1.14",
|
||||||
"greenlet>=3.0",
|
"greenlet>=3.0",
|
||||||
"sqlalchemy[asyncio]>=2.0",
|
"sqlalchemy[asyncio]>=2.0",
|
||||||
@@ -15,7 +16,7 @@ dependencies = [
|
|||||||
"langchain-core>=0.3.0,<2.0.0",
|
"langchain-core>=0.3.0,<2.0.0",
|
||||||
"langchain-openai>=0.3.0,<2.0.0",
|
"langchain-openai>=0.3.0,<2.0.0",
|
||||||
"langgraph>=0.2.0",
|
"langgraph>=0.2.0",
|
||||||
"langgraph-checkpoint-sqlite>=2.0.0",
|
"langgraph-checkpoint-postgres>=2.0.0",
|
||||||
"openai>=1.0.0",
|
"openai>=1.0.0",
|
||||||
"platformdirs>=4.9",
|
"platformdirs>=4.9",
|
||||||
"prompt-toolkit>=3.0",
|
"prompt-toolkit>=3.0",
|
||||||
@@ -46,6 +47,12 @@ markers = [
|
|||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
|
# aiosqlite is a TEST-ONLY dependency: production runs on Postgres
|
||||||
|
# (asyncpg, see [project.dependencies]) but the bulk of the test suite uses
|
||||||
|
# sqlite+aiosqlite tmp_path URLs for speed + isolation simplicity. Live
|
||||||
|
# Postgres validation happens via the E2E suite (real OpenRouter +
|
||||||
|
# docker-compose Postgres).
|
||||||
|
"aiosqlite>=0.20",
|
||||||
"mypy>=1.13",
|
"mypy>=1.13",
|
||||||
"pre-commit>=4.0",
|
"pre-commit>=4.0",
|
||||||
"pytest>=8.3",
|
"pytest>=8.3",
|
||||||
|
|||||||
@@ -13,12 +13,11 @@ from datetime import UTC, datetime
|
|||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
|
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .errors import BudgetExhaustedError
|
from .errors import BudgetExhaustedError
|
||||||
from .persistence.db import Database
|
from .persistence.db import Database
|
||||||
from .persistence.models import BudgetLedgerRow
|
from .persistence.models import BudgetLedgerRow
|
||||||
|
from .persistence.upsert import insert_for
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -173,8 +172,9 @@ class BudgetTracker:
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
session: AsyncSession = s # type: ignore[assignment]
|
session: AsyncSession = s # type: ignore[assignment]
|
||||||
|
insert = insert_for(session)
|
||||||
stmt = (
|
stmt = (
|
||||||
sqlite_insert(BudgetLedgerRow)
|
insert(BudgetLedgerRow)
|
||||||
.values(scope=scope, spent_usd=0.0, cap_usd=cap, last_updated=_now_iso())
|
.values(scope=scope, spent_usd=0.0, cap_usd=cap, last_updated=_now_iso())
|
||||||
.on_conflict_do_nothing(index_elements=["scope"])
|
.on_conflict_do_nothing(index_elements=["scope"])
|
||||||
)
|
)
|
||||||
@@ -198,8 +198,9 @@ class BudgetTracker:
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
session: AsyncSession = s # type: ignore[assignment]
|
session: AsyncSession = s # type: ignore[assignment]
|
||||||
|
insert = insert_for(session)
|
||||||
stmt = (
|
stmt = (
|
||||||
sqlite_insert(BudgetLedgerRow)
|
insert(BudgetLedgerRow)
|
||||||
.values(scope=scope, spent_usd=delta_usd, cap_usd=cap, last_updated=_now_iso())
|
.values(scope=scope, spent_usd=delta_usd, cap_usd=cap, last_updated=_now_iso())
|
||||||
.on_conflict_do_update(
|
.on_conflict_do_update(
|
||||||
index_elements=["scope"],
|
index_elements=["scope"],
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Checks:
|
|||||||
5. config + governance consent
|
5. config + governance consent
|
||||||
6. OpenRouter API key reachable
|
6. OpenRouter API key reachable
|
||||||
7. OpenRouter /models ping + pricing matrix upsert
|
7. OpenRouter /models ping + pricing matrix upsert
|
||||||
8. Disk free + SQLite integrity_check
|
8. Disk free + DB liveness probe (pg `SELECT 1` / sqlite `PRAGMA integrity_check`)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -26,7 +26,6 @@ import typer
|
|||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
from sqlalchemy import text as sa_text
|
from sqlalchemy import text as sa_text
|
||||||
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
|
|
||||||
|
|
||||||
from ..config import Config, load_config
|
from ..config import Config, load_config
|
||||||
from ..errors import MyDeepAgentError
|
from ..errors import MyDeepAgentError
|
||||||
@@ -38,6 +37,7 @@ from ..monitoring.pricing import (
|
|||||||
)
|
)
|
||||||
from ..persistence.db import Database
|
from ..persistence.db import Database
|
||||||
from ..persistence.models import ModelPricingRow
|
from ..persistence.models import ModelPricingRow
|
||||||
|
from ..persistence.upsert import insert_for
|
||||||
from ..secrets import resolve_openrouter_api_key
|
from ..secrets import resolve_openrouter_api_key
|
||||||
|
|
||||||
_CONSOLE = Console()
|
_CONSOLE = Console()
|
||||||
@@ -147,9 +147,10 @@ async def _upsert_pricing(config: Config, prices: list[ModelPrice]) -> None:
|
|||||||
now = datetime.now(UTC).isoformat(timespec="seconds")
|
now = datetime.now(UTC).isoformat(timespec="seconds")
|
||||||
try:
|
try:
|
||||||
async with db.session() as s:
|
async with db.session() as s:
|
||||||
|
insert = insert_for(s)
|
||||||
for p in prices:
|
for p in prices:
|
||||||
stmt = (
|
stmt = (
|
||||||
sqlite_insert(ModelPricingRow)
|
insert(ModelPricingRow)
|
||||||
.values(
|
.values(
|
||||||
model=p.model,
|
model=p.model,
|
||||||
input_per_1k_usd=p.input_per_1k_usd,
|
input_per_1k_usd=p.input_per_1k_usd,
|
||||||
@@ -175,6 +176,12 @@ async def _upsert_pricing(config: Config, prices: list[ModelPrice]) -> None:
|
|||||||
|
|
||||||
|
|
||||||
async def _check_disk_and_db(config: Config) -> CheckResult:
|
async def _check_disk_and_db(config: Config) -> CheckResult:
|
||||||
|
"""Disk free + DB liveness probe.
|
||||||
|
|
||||||
|
Postgres path: ``SELECT 1`` round-trip (pg_isready equivalent — proves
|
||||||
|
network reachability, auth, and that the DB exists).
|
||||||
|
SQLite path: ``PRAGMA integrity_check`` to detect corruption.
|
||||||
|
"""
|
||||||
usage = shutil.disk_usage(str(config.workspace_root))
|
usage = shutil.disk_usage(str(config.workspace_root))
|
||||||
free_gb = usage.free / (1024**3)
|
free_gb = usage.free / (1024**3)
|
||||||
if free_gb < 2.0:
|
if free_gb < 2.0:
|
||||||
@@ -185,15 +192,34 @@ async def _check_disk_and_db(config: Config) -> CheckResult:
|
|||||||
disk_status = "ok"
|
disk_status = "ok"
|
||||||
|
|
||||||
db = Database(config.database_url)
|
db = Database(config.database_url)
|
||||||
await db.init_schema()
|
db_detail = ""
|
||||||
|
db_ok = False
|
||||||
try:
|
try:
|
||||||
|
# init_schema is idempotent and safe; for Postgres it requires CREATE
|
||||||
|
# privileges, which the default devflow role has on the mydeepagent DB.
|
||||||
|
# If alembic has already been applied this is a no-op.
|
||||||
|
await db.init_schema()
|
||||||
async with db.session() as s:
|
async with db.session() as s:
|
||||||
row = (await s.execute(sa_text("PRAGMA integrity_check"))).scalar_one()
|
if db.dialect_name == "postgresql":
|
||||||
|
# pg_isready-equivalent: simple round-trip query proves the
|
||||||
|
# server is reachable, auth works, and the DB exists.
|
||||||
|
row = (await s.execute(sa_text("SELECT 1"))).scalar_one()
|
||||||
|
db_ok = row == 1
|
||||||
|
db_detail = f"postgres_alive={'ok' if db_ok else 'fail'}"
|
||||||
|
elif db.dialect_name == "sqlite":
|
||||||
|
row = (await s.execute(sa_text("PRAGMA integrity_check"))).scalar_one()
|
||||||
|
db_ok = row == "ok"
|
||||||
|
db_detail = f"sqlite_integrity={'ok' if db_ok else str(row)}"
|
||||||
|
else: # pragma: no cover — defensive for future dialects
|
||||||
|
db_ok = True
|
||||||
|
db_detail = f"dialect={db.dialect_name},probe=skipped"
|
||||||
|
except Exception as e:
|
||||||
|
db_ok = False
|
||||||
|
db_detail = f"db_error={type(e).__name__}:{e}"
|
||||||
finally:
|
finally:
|
||||||
await db.dispose()
|
await db.dispose()
|
||||||
|
|
||||||
db_ok = row == "ok"
|
detail = f"free={free_gb:.1f}GB, {db_detail}"
|
||||||
detail = f"free={free_gb:.1f}GB, sqlite_integrity={'ok' if db_ok else str(row)}"
|
|
||||||
if disk_status == "fail" or not db_ok:
|
if disk_status == "fail" or not db_ok:
|
||||||
final: Literal["ok", "warn", "fail"] = "fail"
|
final: Literal["ok", "warn", "fail"] = "fail"
|
||||||
elif disk_status == "warn":
|
elif disk_status == "warn":
|
||||||
|
|||||||
@@ -33,10 +33,12 @@ class Config(BaseSettings):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# storage
|
# storage
|
||||||
|
# v0.2 PR #1: Postgres is the production default. Local docker-compose ships
|
||||||
|
# a `devflow-postgres` container on port 55432 with credentials
|
||||||
|
# devflow / devflow. The v3 `devflow` DB is preserved untouched; v4 lives in
|
||||||
|
# a fresh `mydeepagent` DB. Tests may override via MYDEEPAGENT_DATABASE_URL.
|
||||||
database_url: str = Field(
|
database_url: str = Field(
|
||||||
default_factory=lambda: (
|
default="postgresql+asyncpg://devflow:devflow@localhost:55432/mydeepagent"
|
||||||
f"sqlite+aiosqlite:///{Path(_DIRS.user_data_dir) / 'database.sqlite3'}"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
workspace_root: Path = Field(default_factory=Path.cwd)
|
workspace_root: Path = Field(default_factory=Path.cwd)
|
||||||
data_dir: Path = Field(default_factory=lambda: Path(_DIRS.user_data_dir))
|
data_dir: Path = Field(default_factory=lambda: Path(_DIRS.user_data_dir))
|
||||||
|
|||||||
@@ -1,41 +1,62 @@
|
|||||||
"""LangGraph SqliteSaver wrapper. Use only as a context manager to ensure connection cleanup.
|
"""LangGraph AsyncPostgresSaver wrapper. Use only as an async context manager.
|
||||||
|
|
||||||
``SqliteSaver.from_conn_string`` is a ``@contextmanager`` classmethod that yields
|
``AsyncPostgresSaver.from_conn_string`` is an ``@asynccontextmanager`` classmethod
|
||||||
a ``SqliteSaver`` instance and closes the underlying sqlite3 connection on exit.
|
that yields an ``AsyncPostgresSaver`` instance and closes the underlying Postgres
|
||||||
Direct manual lifecycle management (entering context without ``with``) leaks connections
|
connection on exit. Direct manual lifecycle management (entering context without
|
||||||
and is not supported by this module.
|
``async with``) leaks connections and is not supported by this module.
|
||||||
|
|
||||||
|
v0.2 PR #1: switched from SqliteSaver to AsyncPostgresSaver. The API is now
|
||||||
|
async and takes a connection string instead of a filesystem path; the legacy
|
||||||
|
``Path`` parameter form has been removed.
|
||||||
|
|
||||||
Usage::
|
Usage::
|
||||||
|
|
||||||
with get_checkpointer_ctx(path) as saver:
|
async with get_checkpointer_ctx(conn_string) as saver:
|
||||||
graph = create_deep_agent(checkpointer=saver)
|
graph = create_deep_agent(checkpointer=saver)
|
||||||
...
|
...
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Iterator
|
from collections.abc import AsyncIterator
|
||||||
from contextlib import contextmanager
|
from contextlib import asynccontextmanager
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from langgraph.checkpoint.sqlite import SqliteSaver
|
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
def _to_psycopg_dsn(database_url: str) -> str:
|
||||||
def get_checkpointer_ctx(checkpoints_db_path: Path) -> Iterator[SqliteSaver]:
|
"""Strip the SQLAlchemy driver prefix (``+asyncpg`` / ``+psycopg``) from a URL.
|
||||||
"""Yield a SqliteSaver bound to *checkpoints_db_path*.
|
|
||||||
|
|
||||||
Creates the parent directory and the database file if they do not exist.
|
AsyncPostgresSaver wants a plain libpq DSN (e.g. ``postgresql://...``),
|
||||||
The underlying sqlite3 connection is closed automatically on context exit.
|
while the rest of the project uses SQLAlchemy URLs (``postgresql+asyncpg://...``).
|
||||||
This is the only supported way to obtain a SqliteSaver in this project —
|
"""
|
||||||
direct manual lifecycle management is not provided.
|
if database_url.startswith("postgresql+asyncpg://"):
|
||||||
|
return "postgresql://" + database_url[len("postgresql+asyncpg://") :]
|
||||||
|
if database_url.startswith("postgresql+psycopg://"):
|
||||||
|
return "postgresql://" + database_url[len("postgresql+psycopg://") :]
|
||||||
|
return database_url
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def get_checkpointer_ctx(database_url: str) -> AsyncIterator[AsyncPostgresSaver]:
|
||||||
|
"""Yield an AsyncPostgresSaver bound to *database_url*.
|
||||||
|
|
||||||
|
The underlying psycopg connection is closed automatically on context exit.
|
||||||
|
This is the only supported way to obtain a saver in this project — direct
|
||||||
|
manual lifecycle management is not provided.
|
||||||
|
|
||||||
|
On first use, ``saver.setup()`` runs the LangGraph checkpoint schema
|
||||||
|
creation idempotently.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
checkpoints_db_path: Filesystem path for the SQLite checkpoint database.
|
database_url: SQLAlchemy-style URL (``postgresql+asyncpg://user:pw@host:port/db``)
|
||||||
|
or a plain libpq DSN (``postgresql://...``). The SQLAlchemy
|
||||||
|
``+asyncpg`` / ``+psycopg`` driver suffix is stripped automatically.
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
SqliteSaver: Ready-to-use LangGraph checkpoint saver.
|
AsyncPostgresSaver: Ready-to-use LangGraph checkpoint saver.
|
||||||
"""
|
"""
|
||||||
checkpoints_db_path.parent.mkdir(parents=True, exist_ok=True)
|
dsn = _to_psycopg_dsn(database_url)
|
||||||
with SqliteSaver.from_conn_string(str(checkpoints_db_path)) as saver:
|
async with AsyncPostgresSaver.from_conn_string(dsn) as saver:
|
||||||
|
await saver.setup()
|
||||||
yield saver
|
yield saver
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
"""Async SQLAlchemy engine + session factory with WAL mode and busy_timeout."""
|
"""Async SQLAlchemy engine + session factory (Postgres primary; SQLite legacy fallback).
|
||||||
|
|
||||||
|
v0.2 PR #1: Postgres becomes the default backing store for my-deepagent.
|
||||||
|
SQLite is no longer the default — but the engine factory still detects the
|
||||||
|
dialect at construct time so that tests / one-off CLI uses can still point at
|
||||||
|
``sqlite+aiosqlite://`` URLs when needed. Production code paths default to
|
||||||
|
Postgres via :attr:`Config.database_url`.
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -6,6 +13,7 @@ from collections.abc import AsyncIterator
|
|||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from sqlalchemy import event
|
from sqlalchemy import event
|
||||||
|
from sqlalchemy.engine import Engine
|
||||||
from sqlalchemy.ext.asyncio import (
|
from sqlalchemy.ext.asyncio import (
|
||||||
AsyncEngine,
|
AsyncEngine,
|
||||||
AsyncSession,
|
AsyncSession,
|
||||||
@@ -16,25 +24,39 @@ from sqlalchemy.ext.asyncio import (
|
|||||||
from .models import Base
|
from .models import Base
|
||||||
|
|
||||||
|
|
||||||
def _attach_sqlite_pragmas(engine: AsyncEngine) -> None:
|
def _attach_dialect_pragmas(engine: AsyncEngine) -> None:
|
||||||
"""Attach a synchronous connect-event listener that enables WAL, busy_timeout, FK."""
|
"""Attach dialect-specific connect-time PRAGMA / SET listeners.
|
||||||
|
|
||||||
@event.listens_for(engine.sync_engine, "connect")
|
SQLite: WAL mode + busy_timeout + foreign_keys ON.
|
||||||
def _set_sqlite_pragma(dbapi_connection: object, _conn_record: object) -> None:
|
Postgres: no PRAGMA equivalent needed — defaults already give us the
|
||||||
# dbapi_connection is a raw sqlite3.Connection delivered by SQLAlchemy's
|
isolation level and FK enforcement we want; we only set the session
|
||||||
# pool event callback. The signature uses `object` to match the generic
|
timezone to UTC so that any naive timestamps round-trip predictably.
|
||||||
# listener protocol; we cast to `Any` here to access DBAPI methods without
|
"""
|
||||||
# introducing a hard import of `sqlite3` (which would break non-SQLite
|
sync_engine: Engine = engine.sync_engine
|
||||||
# engines). The pragma calls are safe: they are no-ops on non-SQLite
|
dialect_name = sync_engine.dialect.name
|
||||||
# dialects and sqlite3.Connection always has `.cursor()`.
|
|
||||||
import sqlite3 # local import to avoid circular or non-SQLite coupling
|
|
||||||
|
|
||||||
conn: sqlite3.Connection = dbapi_connection # type: ignore[assignment]
|
if dialect_name == "sqlite":
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute("PRAGMA journal_mode=WAL")
|
@event.listens_for(sync_engine, "connect")
|
||||||
cursor.execute("PRAGMA busy_timeout=5000")
|
def _set_sqlite_pragma(dbapi_connection: object, _conn_record: object) -> None:
|
||||||
cursor.execute("PRAGMA foreign_keys=ON")
|
# dbapi_connection is a raw sqlite3.Connection delivered by SQLAlchemy's
|
||||||
cursor.close()
|
# pool event callback. Local import avoids a hard sqlite3 coupling on
|
||||||
|
# Postgres-only deployments.
|
||||||
|
import sqlite3 # noqa: F401 # imported for the type annotation only
|
||||||
|
|
||||||
|
cursor = dbapi_connection.cursor() # type: ignore[attr-defined]
|
||||||
|
cursor.execute("PRAGMA journal_mode=WAL")
|
||||||
|
cursor.execute("PRAGMA busy_timeout=5000")
|
||||||
|
cursor.execute("PRAGMA foreign_keys=ON")
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
elif dialect_name == "postgresql":
|
||||||
|
|
||||||
|
@event.listens_for(sync_engine, "connect")
|
||||||
|
def _set_postgres_session(dbapi_connection: object, _conn_record: object) -> None:
|
||||||
|
cursor = dbapi_connection.cursor() # type: ignore[attr-defined]
|
||||||
|
cursor.execute("SET TIME ZONE 'UTC'")
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
|
||||||
class Database:
|
class Database:
|
||||||
@@ -42,7 +64,7 @@ class Database:
|
|||||||
|
|
||||||
Usage::
|
Usage::
|
||||||
|
|
||||||
db = Database("sqlite+aiosqlite:///path/to/db.sqlite3")
|
db = Database("postgresql+asyncpg://devflow:devflow@localhost:55432/devflow")
|
||||||
await db.init_schema() # dev/test: create all tables directly
|
await db.init_schema() # dev/test: create all tables directly
|
||||||
async with db.session() as s: # production: use alembic upgrade head
|
async with db.session() as s: # production: use alembic upgrade head
|
||||||
result = await s.execute(...)
|
result = await s.execute(...)
|
||||||
@@ -55,17 +77,21 @@ class Database:
|
|||||||
def __init__(self, database_url: str) -> None:
|
def __init__(self, database_url: str) -> None:
|
||||||
self._engine: AsyncEngine = create_async_engine(
|
self._engine: AsyncEngine = create_async_engine(
|
||||||
database_url,
|
database_url,
|
||||||
# NullPool avoids connection reuse issues in SQLite+aiosqlite tests.
|
poolclass=None,
|
||||||
poolclass=None, # use the default StaticPool-compatible pool
|
|
||||||
echo=False,
|
echo=False,
|
||||||
)
|
)
|
||||||
_attach_sqlite_pragmas(self._engine)
|
_attach_dialect_pragmas(self._engine)
|
||||||
self._session_factory: async_sessionmaker[AsyncSession] = async_sessionmaker(
|
self._session_factory: async_sessionmaker[AsyncSession] = async_sessionmaker(
|
||||||
bind=self._engine,
|
bind=self._engine,
|
||||||
expire_on_commit=False,
|
expire_on_commit=False,
|
||||||
autoflush=False,
|
autoflush=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dialect_name(self) -> str:
|
||||||
|
"""Return the SQLAlchemy dialect name (``postgresql`` or ``sqlite``)."""
|
||||||
|
return self._engine.sync_engine.dialect.name
|
||||||
|
|
||||||
async def init_schema(self) -> None:
|
async def init_schema(self) -> None:
|
||||||
"""Create all ORM-defined tables.
|
"""Create all ORM-defined tables.
|
||||||
|
|
||||||
@@ -75,6 +101,11 @@ class Database:
|
|||||||
async with self._engine.begin() as conn:
|
async with self._engine.begin() as conn:
|
||||||
await conn.run_sync(Base.metadata.create_all)
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
|
async def drop_schema(self) -> None:
|
||||||
|
"""Drop all ORM-defined tables. Test-only; production must never call this."""
|
||||||
|
async with self._engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.drop_all)
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def session(self) -> AsyncIterator[AsyncSession]:
|
async def session(self) -> AsyncIterator[AsyncSession]:
|
||||||
"""Yield an async session; commit on success, rollback on exception."""
|
"""Yield an async session; commit on success, rollback on exception."""
|
||||||
|
|||||||
@@ -78,13 +78,16 @@ class RunRow(Base):
|
|||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
# Partial unique index: at most one active run per (repo_path, base_branch).
|
# Partial unique index: at most one active run per (repo_path, base_branch).
|
||||||
# An "active" run is any run whose state is not 'completed', 'failed', or 'aborted'.
|
# An "active" run is any run whose state is not 'completed', 'failed', or 'aborted'.
|
||||||
# SQLite partial index uses a WHERE clause; autogenerate cannot detect this,
|
# Both SQLite and PostgreSQL support partial indexes with a WHERE clause —
|
||||||
# so it is managed via a manual alembic migration.
|
# SQLAlchemy needs the dialect-specific kwarg for each. Autogenerate cannot
|
||||||
|
# detect this, so the alembic migration is hand-edited to call
|
||||||
|
# `op.create_index(..., postgresql_where=..., sqlite_where=...)`.
|
||||||
Index(
|
Index(
|
||||||
"ux_active_run_repo_base",
|
"ux_active_run_repo_base",
|
||||||
"repo_path",
|
"repo_path",
|
||||||
"base_branch",
|
"base_branch",
|
||||||
unique=True,
|
unique=True,
|
||||||
|
postgresql_where=text("state NOT IN ('completed', 'failed', 'aborted')"),
|
||||||
sqlite_where=text("state NOT IN ('completed', 'failed', 'aborted')"),
|
sqlite_where=text("state NOT IN ('completed', 'failed', 'aborted')"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
45
my-deepagent/src/my_deepagent/persistence/upsert.py
Normal file
45
my-deepagent/src/my_deepagent/persistence/upsert.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
"""Dialect-aware UPSERT helper.
|
||||||
|
|
||||||
|
SQLite and PostgreSQL both expose an ``insert(...).on_conflict_do_*()`` API,
|
||||||
|
but they live under different dialect modules with slightly different
|
||||||
|
re-exports. ``insert_for(session)`` picks the right ``insert`` at runtime
|
||||||
|
based on the session's bound engine, so call sites can stay portable.
|
||||||
|
|
||||||
|
Both dialects accept the same kwargs for the methods we use
|
||||||
|
(``on_conflict_do_nothing(index_elements=...)`` and
|
||||||
|
``on_conflict_do_update(index_elements=..., set_=...)``).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
|
||||||
|
def insert_for(session: AsyncSession) -> Any:
|
||||||
|
"""Return the ``insert`` constructor that matches ``session``'s dialect.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: An async session bound to either a Postgres or SQLite engine.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The dialect-specific ``insert`` callable (``postgresql.insert`` or
|
||||||
|
``sqlite.insert``). Pass an ORM class or Table; the returned
|
||||||
|
statement supports ``.on_conflict_do_nothing(...)`` and
|
||||||
|
``.on_conflict_do_update(...)`` with the same kwargs in both dialects.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: If the bound engine uses an unsupported dialect.
|
||||||
|
"""
|
||||||
|
bind = session.get_bind()
|
||||||
|
dialect_name = bind.dialect.name
|
||||||
|
if dialect_name == "postgresql":
|
||||||
|
from sqlalchemy.dialects.postgresql import insert as _pg_insert
|
||||||
|
|
||||||
|
return _pg_insert
|
||||||
|
if dialect_name == "sqlite":
|
||||||
|
from sqlalchemy.dialects.sqlite import insert as _sqlite_insert
|
||||||
|
|
||||||
|
return _sqlite_insert
|
||||||
|
raise NotImplementedError(f"upsert not implemented for dialect={dialect_name!r}")
|
||||||
@@ -13,12 +13,12 @@ from datetime import UTC, datetime
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select
|
||||||
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from .enums import RunPhaseState, RunState
|
from .enums import RunPhaseState, RunState
|
||||||
from .persistence.db import Database
|
from .persistence.db import Database
|
||||||
from .persistence.models import RunEventRow, RunPhaseRow, RunRow
|
from .persistence.models import RunEventRow, RunPhaseRow, RunRow
|
||||||
|
from .persistence.upsert import insert_for
|
||||||
from .run_event import RunEventType, run_idempotency_key
|
from .run_event import RunEventType, run_idempotency_key
|
||||||
|
|
||||||
_NON_TERMINAL_RUN_STATES: frozenset[str] = frozenset(
|
_NON_TERMINAL_RUN_STATES: frozenset[str] = frozenset(
|
||||||
@@ -139,8 +139,9 @@ async def _append_event_idempotent(
|
|||||||
)
|
)
|
||||||
).scalar_one()
|
).scalar_one()
|
||||||
|
|
||||||
|
insert = insert_for(s)
|
||||||
stmt = (
|
stmt = (
|
||||||
sqlite_insert(RunEventRow)
|
insert(RunEventRow)
|
||||||
.values(
|
.values(
|
||||||
run_id=run_id,
|
run_id=run_id,
|
||||||
phase_id=None,
|
phase_id=None,
|
||||||
|
|||||||
80
my-deepagent/tests/conftest.py
Normal file
80
my-deepagent/tests/conftest.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
"""Test fixtures shared across unit + integration tests.
|
||||||
|
|
||||||
|
v0.2 PR #1: tests run against the live Postgres container managed by
|
||||||
|
docker-compose. Each test that needs DB isolation requests the
|
||||||
|
``pg_db_url`` fixture, which creates a fresh database per test and drops it
|
||||||
|
on teardown.
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
docker compose up -d postgres # devflow-postgres on 55432
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
from collections.abc import AsyncIterator
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
import psycopg
|
||||||
|
import pytest_asyncio
|
||||||
|
|
||||||
|
# Maintenance connection — used only to CREATE DATABASE / DROP DATABASE.
|
||||||
|
# `postgres` is the bootstrap DB present on every Postgres install.
|
||||||
|
_MAINTENANCE_DSN: Final[str] = os.environ.get(
|
||||||
|
"MYDEEPAGENT_TEST_MAINTENANCE_DSN",
|
||||||
|
"postgresql://devflow:devflow@localhost:55432/postgres",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _async_url(db_name: str) -> str:
|
||||||
|
"""Return the SQLAlchemy + asyncpg URL for *db_name*."""
|
||||||
|
return f"postgresql+asyncpg://devflow:devflow@localhost:55432/{db_name}"
|
||||||
|
|
||||||
|
|
||||||
|
def _create_test_database() -> str:
|
||||||
|
"""Create a fresh test database with a random suffix and return its name."""
|
||||||
|
db_name = f"test_{uuid.uuid4().hex[:16]}"
|
||||||
|
# autocommit=True is required for CREATE DATABASE (cannot run in a tx block).
|
||||||
|
with psycopg.connect(_MAINTENANCE_DSN, autocommit=True) as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(f'CREATE DATABASE "{db_name}"')
|
||||||
|
return db_name
|
||||||
|
|
||||||
|
|
||||||
|
def _drop_test_database(db_name: str) -> None:
|
||||||
|
"""Forcefully terminate connections and drop *db_name*. Idempotent."""
|
||||||
|
with psycopg.connect(_MAINTENANCE_DSN, autocommit=True) as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
# Kick any lingering connections held by aiosqlite-style pools that
|
||||||
|
# didn't dispose cleanly. WITH (FORCE) is Postgres 13+.
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT pg_terminate_backend(pid)
|
||||||
|
FROM pg_stat_activity
|
||||||
|
WHERE datname = %s AND pid <> pg_backend_pid()
|
||||||
|
""",
|
||||||
|
(db_name,),
|
||||||
|
)
|
||||||
|
cur.execute(f'DROP DATABASE IF EXISTS "{db_name}"')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def pg_db_url() -> AsyncIterator[str]:
|
||||||
|
"""Yield an isolated Postgres database URL; drop the DB on teardown.
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
|
async def test_something(pg_db_url: str) -> None:
|
||||||
|
db = Database(pg_db_url)
|
||||||
|
await db.init_schema()
|
||||||
|
...
|
||||||
|
|
||||||
|
The returned URL uses the asyncpg driver. To get a sync URL for tools
|
||||||
|
like alembic, replace ``postgresql+asyncpg://`` with ``postgresql+psycopg://``.
|
||||||
|
"""
|
||||||
|
db_name = _create_test_database()
|
||||||
|
try:
|
||||||
|
yield _async_url(db_name)
|
||||||
|
finally:
|
||||||
|
_drop_test_database(db_name)
|
||||||
@@ -1,78 +1,61 @@
|
|||||||
"""Integration tests for src/my_deepagent/persistence/checkpointer.py."""
|
"""Integration tests for src/my_deepagent/persistence/checkpointer.py.
|
||||||
|
|
||||||
|
v0.2 PR #1: rewritten for AsyncPostgresSaver (LangGraph Postgres checkpointer).
|
||||||
|
The legacy SqliteSaver / Path-based API is removed.
|
||||||
|
|
||||||
|
Requires the docker-compose `devflow-postgres` container; the ``pg_db_url``
|
||||||
|
fixture from ``tests/conftest.py`` creates a fresh DB per test.
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sqlite3
|
import pytest
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from my_deepagent.persistence.checkpointer import get_checkpointer_ctx
|
from my_deepagent.persistence.checkpointer import _to_psycopg_dsn, get_checkpointer_ctx
|
||||||
|
|
||||||
|
|
||||||
|
class TestToPsycopgDsn:
|
||||||
|
"""Pure-function tests for the SQLAlchemy → libpq DSN converter."""
|
||||||
|
|
||||||
|
def test_strips_asyncpg_prefix(self) -> None:
|
||||||
|
url = "postgresql+asyncpg://u:p@h:1/d"
|
||||||
|
assert _to_psycopg_dsn(url) == "postgresql://u:p@h:1/d"
|
||||||
|
|
||||||
|
def test_strips_psycopg_prefix(self) -> None:
|
||||||
|
url = "postgresql+psycopg://u:p@h:1/d"
|
||||||
|
assert _to_psycopg_dsn(url) == "postgresql://u:p@h:1/d"
|
||||||
|
|
||||||
|
def test_bare_postgres_url_passes_through(self) -> None:
|
||||||
|
url = "postgresql://u:p@h:1/d"
|
||||||
|
assert _to_psycopg_dsn(url) == url
|
||||||
|
|
||||||
|
def test_non_postgres_url_passes_through(self) -> None:
|
||||||
|
url = "sqlite:///x"
|
||||||
|
assert _to_psycopg_dsn(url) == url
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
class TestGetCheckpointerCtx:
|
class TestGetCheckpointerCtx:
|
||||||
"""Tests for the get_checkpointer_ctx context manager."""
|
"""Tests for the async get_checkpointer_ctx context manager."""
|
||||||
|
|
||||||
def test_ctx_yields_saver_and_cleans_up(self, tmp_path: Path) -> None:
|
|
||||||
"""Entering the context yields a SqliteSaver; exiting releases the connection."""
|
|
||||||
db_path = tmp_path / "ck.db"
|
|
||||||
with get_checkpointer_ctx(db_path) as saver:
|
|
||||||
assert saver is not None
|
|
||||||
# The DB file must exist while inside the context.
|
|
||||||
assert db_path.exists()
|
|
||||||
|
|
||||||
# After context exit the file must still exist (not deleted).
|
|
||||||
assert db_path.exists()
|
|
||||||
|
|
||||||
def test_db_file_created_on_enter(self, tmp_path: Path) -> None:
|
|
||||||
"""The sqlite file is created when the context is entered."""
|
|
||||||
db_path = tmp_path / "nested" / "dir" / "ck.db"
|
|
||||||
assert not db_path.exists()
|
|
||||||
|
|
||||||
with get_checkpointer_ctx(db_path):
|
|
||||||
assert db_path.exists()
|
|
||||||
|
|
||||||
def test_parent_dir_created_if_missing(self, tmp_path: Path) -> None:
|
|
||||||
"""Parent directory is created automatically even if it does not exist."""
|
|
||||||
db_path = tmp_path / "a" / "b" / "c" / "ck.db"
|
|
||||||
assert not db_path.parent.exists()
|
|
||||||
|
|
||||||
with get_checkpointer_ctx(db_path):
|
|
||||||
assert db_path.parent.exists()
|
|
||||||
|
|
||||||
def test_connection_released_after_ctx_exit(self, tmp_path: Path) -> None:
|
|
||||||
"""After exiting the context manager, another process/connection can open the DB."""
|
|
||||||
db_path = tmp_path / "ck.db"
|
|
||||||
|
|
||||||
with get_checkpointer_ctx(db_path):
|
|
||||||
pass # enter and exit
|
|
||||||
|
|
||||||
# If the connection were leaked (not closed), WAL mode can still allow reads,
|
|
||||||
# but we verify by opening with a fresh sqlite3 connection — this must succeed.
|
|
||||||
with sqlite3.connect(str(db_path)) as conn:
|
|
||||||
cur = conn.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
|
||||||
# LangGraph creates its checkpoint tables; result must be a list (not error).
|
|
||||||
tables = [row[0] for row in cur.fetchall()]
|
|
||||||
assert isinstance(tables, list)
|
|
||||||
|
|
||||||
def test_meta_and_checkpoint_db_no_lock_conflict(self, tmp_path: Path) -> None:
|
|
||||||
"""Using two separate DB files in the same directory causes no locking conflict."""
|
|
||||||
meta_db = tmp_path / "meta.db"
|
|
||||||
ck_db = tmp_path / "checkpoints.db"
|
|
||||||
|
|
||||||
# Simulate concurrent use: open both within the same scope.
|
|
||||||
with get_checkpointer_ctx(ck_db) as saver:
|
|
||||||
# Write something to the meta DB while the checkpointer holds its connection.
|
|
||||||
with sqlite3.connect(str(meta_db)) as conn:
|
|
||||||
conn.execute("CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT)")
|
|
||||||
conn.execute("INSERT OR REPLACE INTO kv VALUES ('key', 'value')")
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ctx_yields_saver(self, pg_db_url: str) -> None:
|
||||||
|
"""Entering the async context yields a non-None saver."""
|
||||||
|
async with get_checkpointer_ctx(pg_db_url) as saver:
|
||||||
assert saver is not None
|
assert saver is not None
|
||||||
|
|
||||||
# Both files must exist and be independently readable.
|
@pytest.mark.asyncio
|
||||||
assert meta_db.exists()
|
async def test_setup_is_idempotent(self, pg_db_url: str) -> None:
|
||||||
assert ck_db.exists()
|
"""``saver.setup()`` is invoked on entry; entering twice must not error."""
|
||||||
|
async with get_checkpointer_ctx(pg_db_url) as first:
|
||||||
|
assert first is not None
|
||||||
|
# A second open against the same DB must not raise — setup() is idempotent.
|
||||||
|
async with get_checkpointer_ctx(pg_db_url) as second:
|
||||||
|
assert second is not None
|
||||||
|
|
||||||
with sqlite3.connect(str(meta_db)) as conn:
|
@pytest.mark.asyncio
|
||||||
row = conn.execute("SELECT v FROM kv WHERE k='key'").fetchone()
|
async def test_accepts_sqlalchemy_url(self, pg_db_url: str) -> None:
|
||||||
assert row is not None
|
"""SQLAlchemy-style ``postgresql+asyncpg://`` URLs are accepted."""
|
||||||
assert row[0] == "value"
|
assert pg_db_url.startswith("postgresql+asyncpg://")
|
||||||
|
async with get_checkpointer_ctx(pg_db_url) as saver:
|
||||||
|
assert saver is not None
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ def _make_pricing() -> PricingCache:
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.timeout(600) # 10 minute hard limit for slow LLM responses
|
@pytest.mark.timeout(600) # 10 minute hard limit for slow LLM responses
|
||||||
async def test_e2e_spec_and_review_workflow(tmp_path: Path) -> None:
|
async def test_e2e_spec_and_review_workflow(tmp_path: Path, pg_db_url: str) -> None:
|
||||||
"""Real OpenRouter call: full spec-and-review@1 workflow end-to-end.
|
"""Real OpenRouter call: full spec-and-review@1 workflow end-to-end.
|
||||||
|
|
||||||
Persona binding (all pinned via BindingOverride for determinism):
|
Persona binding (all pinned via BindingOverride for determinism):
|
||||||
@@ -112,16 +112,20 @@ async def test_e2e_spec_and_review_workflow(tmp_path: Path) -> None:
|
|||||||
|
|
||||||
Cost estimate: ~$0.01-$0.05 for 3 phases with max_tokens=4096 each.
|
Cost estimate: ~$0.01-$0.05 for 3 phases with max_tokens=4096 each.
|
||||||
"""
|
"""
|
||||||
# ---- Setup: config overrides pointing to tmp_path ----
|
# ---- Setup: config overrides pointing to tmp_path + isolated Postgres DB.
|
||||||
|
# `pg_db_url` is the v0.2-PR-1 conftest fixture that creates a fresh
|
||||||
|
# Postgres DB per test (against docker-compose `devflow-postgres`) and
|
||||||
|
# drops it on teardown. This is the only test in the suite that exercises
|
||||||
|
# the production Postgres path end-to-end; the bulk of unit + integration
|
||||||
|
# tests still use sqlite+aiosqlite tmp_path for speed.
|
||||||
ws_root = tmp_path / "ws"
|
ws_root = tmp_path / "ws"
|
||||||
ws_root.mkdir(parents=True, exist_ok=True)
|
ws_root.mkdir(parents=True, exist_ok=True)
|
||||||
db_path = tmp_path / "e2e.sqlite"
|
|
||||||
|
|
||||||
config = load_config(
|
config = load_config(
|
||||||
workspace_root=ws_root,
|
workspace_root=ws_root,
|
||||||
data_dir=tmp_path / "data",
|
data_dir=tmp_path / "data",
|
||||||
state_dir=tmp_path / "state",
|
state_dir=tmp_path / "state",
|
||||||
database_url=f"sqlite+aiosqlite:///{db_path}",
|
database_url=pg_db_url,
|
||||||
budget_on_hit="warn_continue", # do not block during E2E test
|
budget_on_hit="warn_continue", # do not block during E2E test
|
||||||
budget_run_usd=5.0, # generous cap for E2E
|
budget_run_usd=5.0, # generous cap for E2E
|
||||||
budget_daily_usd=10.0,
|
budget_daily_usd=10.0,
|
||||||
|
|||||||
154
my-deepagent/uv.lock
generated
154
my-deepagent/uv.lock
generated
@@ -117,6 +117,46 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/fd/78/9387dffccdc55a12734f83aaccc4a987404a217a2a12a1920d8d4585950b/ast_serialize-0.4.0-cp39-abi3-win_arm64.whl", hash = "sha256:1026f565a7ab846337c630909089b3346a2fe417bf1552b1581ab01852137407", size = 1079199, upload-time = "2026-05-14T22:44:36.816Z" },
|
{ url = "https://files.pythonhosted.org/packages/fd/78/9387dffccdc55a12734f83aaccc4a987404a217a2a12a1920d8d4585950b/ast_serialize-0.4.0-cp39-abi3-win_arm64.whl", hash = "sha256:1026f565a7ab846337c630909089b3346a2fe417bf1552b1581ab01852137407", size = 1079199, upload-time = "2026-05-14T22:44:36.816Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asyncpg"
|
||||||
|
version = "0.31.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/a6/59d0a146e61d20e18db7396583242e32e0f120693b67a8de43f1557033e2/asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad", size = 662042, upload-time = "2025-11-24T23:25:49.578Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/01/ffaa189dcb63a2471720615e60185c3f6327716fdc0fc04334436fbb7c65/asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d", size = 638504, upload-time = "2025-11-24T23:25:51.501Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/62/3f699ba45d8bd24c5d65392190d19656d74ff0185f42e19d0bbd973bb371/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a", size = 3426241, upload-time = "2025-11-24T23:25:53.278Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/d1/a867c2150f9c6e7af6462637f613ba67f78a314b00db220cd26ff559d532/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671", size = 3520321, upload-time = "2025-11-24T23:25:54.982Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/1a/cce4c3f246805ecd285a3591222a2611141f1669d002163abef999b60f98/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec", size = 3316685, upload-time = "2025-11-24T23:25:57.43Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/ae/0fc961179e78cc579e138fad6eb580448ecae64908f95b8cb8ee2f241f67/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20", size = 3471858, upload-time = "2025-11-24T23:25:59.636Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/b2/b20e09670be031afa4cbfabd645caece7f85ec62d69c312239de568e058e/asyncpg-0.31.0-cp312-cp312-win32.whl", hash = "sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8", size = 527852, upload-time = "2025-11-24T23:26:01.084Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/f0/f2ed1de154e15b107dc692262395b3c17fc34eafe2a78fc2115931561730/asyncpg-0.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186", size = 597175, upload-time = "2025-11-24T23:26:02.564Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111, upload-time = "2025-11-24T23:26:04.467Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928, upload-time = "2025-11-24T23:26:05.944Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067, upload-time = "2025-11-24T23:26:07.957Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156, upload-time = "2025-11-24T23:26:09.591Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636, upload-time = "2025-11-24T23:26:11.168Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079, upload-time = "2025-11-24T23:26:13.164Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606, upload-time = "2025-11-24T23:26:14.78Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569, upload-time = "2025-11-24T23:26:16.189Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/36/e9450d62e84a13aea6580c83a47a437f26c7ca6fa0f0fd40b6670793ea30/asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44", size = 660867, upload-time = "2025-11-24T23:26:17.631Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/4b/1d0a2b33b3102d210439338e1beea616a6122267c0df459ff0265cd5807a/asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5", size = 638349, upload-time = "2025-11-24T23:26:19.689Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/aa/e7f7ac9a7974f08eff9183e392b2d62516f90412686532d27e196c0f0eeb/asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2", size = 3410428, upload-time = "2025-11-24T23:26:21.275Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/de/bf1b60de3dede5c2731e6788617a512bc0ebd9693eac297ee74086f101d7/asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2", size = 3471678, upload-time = "2025-11-24T23:26:23.627Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/78/fc3ade003e22d8bd53aaf8f75f4be48f0b460fa73738f0391b9c856a9147/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218", size = 3313505, upload-time = "2025-11-24T23:26:25.235Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/e9/73eb8a6789e927816f4705291be21f2225687bfa97321e40cd23055e903a/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d", size = 3434744, upload-time = "2025-11-24T23:26:26.944Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/4b/f10b880534413c65c5b5862f79b8e81553a8f364e5238832ad4c0af71b7f/asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b", size = 532251, upload-time = "2025-11-24T23:26:28.404Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/2d/7aa40750b7a19efa5d66e67fc06008ca0f27ba1bd082e457ad82f59aba49/asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be", size = 604901, upload-time = "2025-11-24T23:26:30.34Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/fe/b9dfe349b83b9dee28cc42360d2c86b2cdce4cb551a2c2d27e156bcac84d/asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2", size = 702280, upload-time = "2025-11-24T23:26:32Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/81/e6be6e37e560bd91e6c23ea8a6138a04fd057b08cf63d3c5055c98e81c1d/asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31", size = 682931, upload-time = "2025-11-24T23:26:33.572Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/45/6009040da85a1648dd5bc75b3b0a062081c483e75a1a29041ae63a0bf0dc/asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7", size = 3581608, upload-time = "2025-11-24T23:26:35.638Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/06/2e3d4d7608b0b2b3adbee0d0bd6a2d29ca0fc4d8a78f8277df04e2d1fd7b/asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e", size = 3498738, upload-time = "2025-11-24T23:26:37.275Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/aa/7d75ede780033141c51d83577ea23236ba7d3a23593929b32b49db8ed36e/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c", size = 3401026, upload-time = "2025-11-24T23:26:39.423Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/7a/15e37d45e7f7c94facc1e9148c0e455e8f33c08f0b8a0b1deb2c5171771b/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a", size = 3429426, upload-time = "2025-11-24T23:26:41.032Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/d5/71437c5f6ae5f307828710efbe62163974e71237d5d46ebd2869ea052d10/asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d", size = 614495, upload-time = "2025-11-24T23:26:42.659Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "attrs"
|
name = "attrs"
|
||||||
version = "26.1.0"
|
version = "26.1.0"
|
||||||
@@ -868,17 +908,18 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "langgraph-checkpoint-sqlite"
|
name = "langgraph-checkpoint-postgres"
|
||||||
version = "3.1.0"
|
version = "3.1.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiosqlite" },
|
|
||||||
{ name = "langgraph-checkpoint" },
|
{ name = "langgraph-checkpoint" },
|
||||||
{ name = "sqlite-vec" },
|
{ name = "orjson" },
|
||||||
|
{ name = "psycopg" },
|
||||||
|
{ name = "psycopg-pool" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e3/ea/83917c2369acf8a10a894d4247655fd063c07924ba5bc4e83c85d2eaeded/langgraph_checkpoint_sqlite-3.1.0.tar.gz", hash = "sha256:f926916ebc1b985d802cc9c820026036e84db9d910d62c97b57e4ba64f67d5ae", size = 147902, upload-time = "2026-05-12T03:34:52.503Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/6d/51/5a2dc42e8b5d5942b933b5b7237eae5a4dbc92508a04727c263dd383ad8a/langgraph_checkpoint_postgres-3.1.0.tar.gz", hash = "sha256:02bff4ab63d9dae8eab3a9640fce1d479da8965c9fba7b0dc04cb1f7c56f0a55", size = 148473, upload-time = "2026-05-12T03:40:10.599Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/07/b342811a16327900af2747c752ea19676172fcddf9b592cc384031076623/langgraph_checkpoint_sqlite-3.1.0-py3-none-any.whl", hash = "sha256:cc9b40df0076feae8a9ad42ae713621b148b00ac23adc09dc1dc66090a46e5ad", size = 38587, upload-time = "2026-05-12T03:34:51.231Z" },
|
{ url = "https://files.pythonhosted.org/packages/2f/cd/eff9b82bc3b5f62d481b437099f44f3ef7b1d907f166fb4ee25e8f84a1e7/langgraph_checkpoint_postgres-3.1.0-py3-none-any.whl", hash = "sha256:814cce2ef35d792bf07b090a95eed004f1acac0724fe6605536b13f6d1e7032c", size = 48988, upload-time = "2026-05-12T03:40:08.925Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1097,8 +1138,8 @@ name = "my-deepagent"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiosqlite" },
|
|
||||||
{ name = "alembic" },
|
{ name = "alembic" },
|
||||||
|
{ name = "asyncpg" },
|
||||||
{ name = "deepagents" },
|
{ name = "deepagents" },
|
||||||
{ name = "greenlet" },
|
{ name = "greenlet" },
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
@@ -1108,10 +1149,11 @@ dependencies = [
|
|||||||
{ name = "langchain-core" },
|
{ name = "langchain-core" },
|
||||||
{ name = "langchain-openai" },
|
{ name = "langchain-openai" },
|
||||||
{ name = "langgraph" },
|
{ name = "langgraph" },
|
||||||
{ name = "langgraph-checkpoint-sqlite" },
|
{ name = "langgraph-checkpoint-postgres" },
|
||||||
{ name = "openai" },
|
{ name = "openai" },
|
||||||
{ name = "platformdirs" },
|
{ name = "platformdirs" },
|
||||||
{ name = "prompt-toolkit" },
|
{ name = "prompt-toolkit" },
|
||||||
|
{ name = "psycopg", extra = ["binary"] },
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "pydantic-settings" },
|
{ name = "pydantic-settings" },
|
||||||
{ name = "pyyaml" },
|
{ name = "pyyaml" },
|
||||||
@@ -1124,6 +1166,7 @@ dependencies = [
|
|||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
|
{ name = "aiosqlite" },
|
||||||
{ name = "mypy" },
|
{ name = "mypy" },
|
||||||
{ name = "pre-commit" },
|
{ name = "pre-commit" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
@@ -1138,8 +1181,8 @@ dev = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "aiosqlite", specifier = ">=0.20" },
|
|
||||||
{ name = "alembic", specifier = ">=1.14" },
|
{ name = "alembic", specifier = ">=1.14" },
|
||||||
|
{ name = "asyncpg", specifier = ">=0.30" },
|
||||||
{ name = "deepagents", specifier = ">=0.6.1,<0.7.0" },
|
{ name = "deepagents", specifier = ">=0.6.1,<0.7.0" },
|
||||||
{ name = "greenlet", specifier = ">=3.0" },
|
{ name = "greenlet", specifier = ">=3.0" },
|
||||||
{ name = "httpx", specifier = ">=0.28" },
|
{ name = "httpx", specifier = ">=0.28" },
|
||||||
@@ -1149,10 +1192,11 @@ requires-dist = [
|
|||||||
{ name = "langchain-core", specifier = ">=0.3.0,<2.0.0" },
|
{ name = "langchain-core", specifier = ">=0.3.0,<2.0.0" },
|
||||||
{ name = "langchain-openai", specifier = ">=0.3.0,<2.0.0" },
|
{ name = "langchain-openai", specifier = ">=0.3.0,<2.0.0" },
|
||||||
{ name = "langgraph", specifier = ">=0.2.0" },
|
{ name = "langgraph", specifier = ">=0.2.0" },
|
||||||
{ name = "langgraph-checkpoint-sqlite", specifier = ">=2.0.0" },
|
{ name = "langgraph-checkpoint-postgres", specifier = ">=2.0.0" },
|
||||||
{ name = "openai", specifier = ">=1.0.0" },
|
{ name = "openai", specifier = ">=1.0.0" },
|
||||||
{ name = "platformdirs", specifier = ">=4.9" },
|
{ name = "platformdirs", specifier = ">=4.9" },
|
||||||
{ name = "prompt-toolkit", specifier = ">=3.0" },
|
{ name = "prompt-toolkit", specifier = ">=3.0" },
|
||||||
|
{ name = "psycopg", extras = ["binary"], specifier = ">=3.2" },
|
||||||
{ name = "pydantic", specifier = ">=2.9" },
|
{ name = "pydantic", specifier = ">=2.9" },
|
||||||
{ name = "pydantic-settings", specifier = ">=2.6" },
|
{ name = "pydantic-settings", specifier = ">=2.6" },
|
||||||
{ name = "pyyaml", specifier = ">=6.0" },
|
{ name = "pyyaml", specifier = ">=6.0" },
|
||||||
@@ -1165,6 +1209,7 @@ requires-dist = [
|
|||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
|
{ name = "aiosqlite", specifier = ">=0.20" },
|
||||||
{ name = "mypy", specifier = ">=1.13" },
|
{ name = "mypy", specifier = ">=1.13" },
|
||||||
{ name = "pre-commit", specifier = ">=4.0" },
|
{ name = "pre-commit", specifier = ">=4.0" },
|
||||||
{ name = "pytest", specifier = ">=8.3" },
|
{ name = "pytest", specifier = ">=8.3" },
|
||||||
@@ -1414,6 +1459,76 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
|
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psycopg"
|
||||||
|
version = "3.3.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
binary = [
|
||||||
|
{ name = "psycopg-binary", marker = "implementation_name != 'pypy'" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psycopg-binary"
|
||||||
|
version = "3.3.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/7d/03818e13ba7f36de93573c93ee3482006d3dfa8b0f8d28df511bad0a1a92/psycopg_binary-3.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5ab28a2a7649df3b72e6b674b4c190e448e8e77cf496a65bd846472048de2089", size = 4591122, upload-time = "2026-05-01T23:27:56.162Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/b9/11b341edf8d54e2694726b273fe9652b254d989f4f63e3ac6816ad6b55f4/psycopg_binary-3.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6402a9d8146cf4b3974ded3fd28a971e83dc6a0333eb7822524a3aa20b546578", size = 4669943, upload-time = "2026-05-01T23:28:04.522Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/18/4665bacd65e7865b4372fcd8abb8b9186ada4b0025f8c2ca691b364a556c/psycopg_binary-3.3.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:580ae30a5f95ccd90008ec697d3ed6a4a2047a516407ad904283fa42086936e9", size = 5469697, upload-time = "2026-05-01T23:28:11.337Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/b1/b83136c6e510593d9b0c759ba5384337bc4ad82d19fda675adc4b2703c84/psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7510c37550f91a187e3660a8cc50d4b760f8c3b8b2f89ebc5698cd2c7f2c85d", size = 5152995, upload-time = "2026-05-01T23:28:20.529Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/8d/a9821e2a648afe6091989929982a3b0f00b2631a859cb81379728f08fb75/psycopg_binary-3.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77df19583501ea288eaf15ac0fe7ad01e6d8091a91d5c41df5c718f307d8e31b", size = 6738180, upload-time = "2026-05-01T23:28:30.654Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/58/2e349e8d23905dc2317b80ac65f48fb6f821a4777a4e994a60da91c4850f/psycopg_binary-3.3.4-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:018fbed325936da502feb546642c982dcc4b9ffdea32dfef78dbf3b7f7ad4070", size = 4978828, upload-time = "2026-05-01T23:28:37.277Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/48/57b00d03b4721878326122a1f1e6b0a90b85bcaec56b5b2f8ea6cfa45235/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17a21953a9e5ff3a16dab692625a3676e2f101db5e40072f39dbee2250194d68", size = 4509757, upload-time = "2026-05-01T23:28:43.078Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/37/33b47d8c007df69aec500df5889767c4d313748e8e9e27a2fef8a6dabcee/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:eb05ee1c2b817d27c537333224c9e83c7afb86fe7296ba970990068baf819b16", size = 4190546, upload-time = "2026-05-01T23:28:50.016Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/c6/32b0835dbc2122617902b649d76a91c1e75406e76bf3d595b0c3bb5ffad6/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:773d573e11f437ce0bdb95b7c18dc58390494f96d43f8b45b9760436114f7652", size = 3926197, upload-time = "2026-05-01T23:28:55.55Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/68/d190ef0c0c5b16ded07831dabc8ddd412f4cdab07ec6e30ed38d9bda0e1f/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e55ccbdfae79a2ed9c6369c3008a3025817ff9d7e27b32a2d84e2a4267e66e", size = 4236627, upload-time = "2026-05-01T23:29:05.336Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/8f/81dcbc2e8454b74d14881275ea45f00791052dac531a9fa8be1730d1685b/psycopg_binary-3.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:494ca54901be8cf9eb7e02c25b731f2317c378efa44f43e8f9bd0e1184ae7be4", size = 3560782, upload-time = "2026-05-01T23:29:11.967Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/43/13e9c406fbbf354580476e248a16b64802a376873ebe6339e30bb655572d/psycopg_binary-3.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7", size = 4590377, upload-time = "2026-05-01T23:29:18.782Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/be/2923cd7c3683e7afdecf4f10796a18de02f5c5ddc0969aa2ad0a8cdd3bbd/psycopg_binary-3.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31", size = 4669023, upload-time = "2026-05-01T23:29:25.884Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/a0/2c913d6fe13d6a8bd13597d36739bf47af063ad9399e402cfecab16f3c1e/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70", size = 5467423, upload-time = "2026-05-01T23:29:33.416Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/38/205d10bc1ad0df4a21c5c51659126bd3ea0ef98fcad1e852f78c249bb9c3/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3", size = 5151137, upload-time = "2026-05-01T23:29:42.013Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/fc/f0381ddcd45eff3bb70dbca6823a996048d7f507b2ec3fc92c6fabc0fe87/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c", size = 6736671, upload-time = "2026-05-01T23:29:51.626Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/40/fa545ae152c24327651e5624e4902121e808270be36c10b12e9939be09bc/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae", size = 4979601, upload-time = "2026-05-01T23:29:56.961Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/e4/2f8a47ee97f90cd2b933d0463081d35631ff419de2b8c984a5f369857de0/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc", size = 4510513, upload-time = "2026-05-01T23:30:07.243Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/0e/94e842ff4a7f98ed162580ca2e8b8864b28c1e0350f2443f8ee47f821167/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf", size = 4187243, upload-time = "2026-05-01T23:30:15.352Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/83/fc6c174b672e29b7de996ea77b6cbddf46c891751c3355f6974292baa6b4/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260", size = 3927347, upload-time = "2026-05-01T23:30:21.186Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/65/768364d4a97a15b1a7f47ba52688c1686f22941d8332a8398cefc468e25f/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf", size = 4236393, upload-time = "2026-05-01T23:30:26.211Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/3b/218efbc9e645becd80cdf651acda05f85cfe546b7a9c0458c7cbc8fe1f74/psycopg_binary-3.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38", size = 3564592, upload-time = "2026-05-01T23:30:31.764Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/a6/828c9185701dab71b234c2a76c38a08b098ebfec5020716b4e93807492b5/psycopg_binary-3.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:28b7398fdd19db3232c884fb24550bdfe951221f510e195e233299e4c9b78f97", size = 4607292, upload-time = "2026-05-01T23:30:38.962Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/58/5b40dbc9d839045c9dae956960e4fb6d20bcabe6c59a2aa34fc3a371913f/psycopg_binary-3.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1fbaa292a3c8bb61b45df1ad3da1908ccee7cb889db9425e3557d9e34e2a4829", size = 4687023, upload-time = "2026-05-01T23:30:47.227Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/a9/793f0ac107a9003b48441d0d1f9f616d96e0f37458dd8dc12528ceff55fb/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94596f9e7633ee3f6440711d43bb70aa31cc0a46a900ab8b4201a366ace5c9e7", size = 5486985, upload-time = "2026-05-01T23:30:55.517Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/26/42e8533497e2592334f68ec529cf5f840f7fa4e99575a4bb61aa184dbfbf/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277", size = 5168745, upload-time = "2026-05-01T23:31:01.904Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/af/b7151776cc08d5935d45c833ec818a9beb417cf7c08239af1aafbdae78ee/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c09aad7051326e7603c14e50636db9c01f78272dc54b3accff03d46370461e6", size = 6761486, upload-time = "2026-05-01T23:31:14.511Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/ed/c92533b9124712d592cbf1cd6c76da933a2e0acea81dfe1fbe7e735f0cff/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:514404ed543efd620c85602b747df2a23cf1241b4067199e1a66f2d2757aaa41", size = 4997427, upload-time = "2026-05-01T23:31:20.901Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/23/ccadfd0de416aa188356daa199453af24087b042e296088706d190ae0295/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:46893c26858be12cc49ca4226ed6a60b4bfccadd946b3bebb783a60b38788228", size = 4533549, upload-time = "2026-05-01T23:31:26.204Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/a0/c8f43cee36386f7bc891ab41a9d31ea07cf9826038e732da79f26b1e5f34/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:df1d567fc430f6df15c9fcf67d87685fc49bdb325adc0db5af1adfb2f44eb5c9", size = 4210256, upload-time = "2026-05-01T23:31:33.884Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/2c/c1547871be3790676e8868b38655496422f94f0978dfb66b74bdba2f1676/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6b9016b1714da4dd5ecaaa75b82098aa5a0b87854ce9b092e21c27c4ae23e014", size = 3946204, upload-time = "2026-05-01T23:31:39.626Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/b1/f6670f00fa7ea601584623f6c11602ab92117d83eaff885e0210f6de7418/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:47c656a8a7ba6eb0cff1801a4caaa9c8bdc12d03080e273aff1c8ac39971a77e", size = 4255811, upload-time = "2026-05-01T23:31:44.986Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/e6/5fff07a70d1f945ed90ae131c3bd76cab32beff7c58c6db15ad5820b6d1f/psycopg_binary-3.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:c37e024c07308cd06cf3ec51bfd0e7f6157585a4d84d1bce4a7f5f7913719bf8", size = 3666849, upload-time = "2026-05-01T23:31:51.165Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psycopg-pool"
|
||||||
|
version = "3.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/90/82/7a23d26039827ecd4ebe93905651029ddd307c5182ad59296dfb6f67b528/psycopg_pool-3.3.1.tar.gz", hash = "sha256:b10b10b7a175d5cc1592147dc5b7eec8a9e0834eb3ed2c4a92c858e2f51eb63c", size = 31661, upload-time = "2026-05-01T23:31:59.809Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/ed/89c2c620af0e1660354cd8aabf9f5b21f911597ce22acb37c805d6c86bc8/psycopg_pool-3.3.1-py3-none-any.whl", hash = "sha256:2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5", size = 40023, upload-time = "2026-05-01T23:31:53.136Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyasn1"
|
name = "pyasn1"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
@@ -2030,18 +2145,6 @@ asyncio = [
|
|||||||
{ name = "greenlet" },
|
{ name = "greenlet" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sqlite-vec"
|
|
||||||
version = "0.1.9"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/68/85/9fad0045d8e7c8df3e0fa5a56c630e8e15ad6e5ca2e6106fceb666aa6638/sqlite_vec-0.1.9-py3-none-macosx_10_6_x86_64.whl", hash = "sha256:1b62a7f0a060d9475575d4e599bbf94a13d85af896bc1ce86ee80d1b5b48e5fb", size = 131171, upload-time = "2026-03-31T08:02:31.717Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a4/3d/3677e0cd2f92e5ebc43cd29fbf565b75582bff1ccfa0b8327c7508e1084f/sqlite_vec-0.1.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d52e30513bae4cc9778ddbf6145610434081be4c3afe57cd877893bad9f6b6c", size = 165434, upload-time = "2026-03-31T08:02:32.712Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/d4/f2b936d3bdc38eadcbd2a87875815db36430fab0363182ba5d12cd8e0b51/sqlite_vec-0.1.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e921e592f24a5f9a18f590b6ddd530eb637e2d474e3b1972f9bbeb773aa3cb9", size = 160076, upload-time = "2026-03-31T08:02:33.796Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6f/ad/6afd073b0f817b3e03f9e37ad626ae341805891f23c74b5292818f49ac63/sqlite_vec-0.1.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux1_x86_64.whl", hash = "sha256:1515727990b49e79bcaf75fdee2ffc7d461f8b66905013231251f1c8938e7786", size = 163388, upload-time = "2026-03-31T08:02:34.888Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/42/89/81b2907cda14e566b9bf215e2ad82fc9b349edf07d2010756ffdb902f328/sqlite_vec-0.1.9-py3-none-win_amd64.whl", hash = "sha256:4a28dc12fa4b53d7b1dced22da2488fade444e96b5d16fd2d698cd670675cf32", size = 292804, upload-time = "2026-03-31T08:02:36.035Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "structlog"
|
name = "structlog"
|
||||||
version = "25.5.0"
|
version = "25.5.0"
|
||||||
@@ -2176,6 +2279,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzdata"
|
||||||
|
version = "2026.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "urllib3"
|
name = "urllib3"
|
||||||
version = "2.7.0"
|
version = "2.7.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user