weightsdb 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. {weightsdb-0.2.0 → weightsdb-0.2.1}/CHANGELOG.md +18 -6
  2. {weightsdb-0.2.0 → weightsdb-0.2.1}/PKG-INFO +1 -1
  3. {weightsdb-0.2.0 → weightsdb-0.2.1}/docs/packages/weightsdb/development-plan.md +2 -2
  4. {weightsdb-0.2.0 → weightsdb-0.2.1}/docs/packages/weightsdb/spec.md +4 -4
  5. weightsdb-0.2.1/src/weightsdb/__about__.py +1 -0
  6. {weightsdb-0.2.0 → weightsdb-0.2.1}/src/weightsdb/engine.py +7 -1
  7. {weightsdb-0.2.0 → weightsdb-0.2.1}/tests/integration/test_migrations.py +20 -0
  8. {weightsdb-0.2.0 → weightsdb-0.2.1}/tests/unit/test_engine.py +16 -0
  9. weightsdb-0.2.0/src/weightsdb/__about__.py +0 -1
  10. {weightsdb-0.2.0 → weightsdb-0.2.1}/.editorconfig +0 -0
  11. {weightsdb-0.2.0 → weightsdb-0.2.1}/.github/workflows/ci.yml +0 -0
  12. {weightsdb-0.2.0 → weightsdb-0.2.1}/.github/workflows/release.yml +0 -0
  13. {weightsdb-0.2.0 → weightsdb-0.2.1}/.gitignore +0 -0
  14. {weightsdb-0.2.0 → weightsdb-0.2.1}/.importlinter +0 -0
  15. {weightsdb-0.2.0 → weightsdb-0.2.1}/.pre-commit-config.yaml +0 -0
  16. {weightsdb-0.2.0 → weightsdb-0.2.1}/CONTRIBUTING.md +0 -0
  17. {weightsdb-0.2.0 → weightsdb-0.2.1}/LICENSE +0 -0
  18. {weightsdb-0.2.0 → weightsdb-0.2.1}/README.md +0 -0
  19. {weightsdb-0.2.0 → weightsdb-0.2.1}/SECURITY.md +0 -0
  20. {weightsdb-0.2.0 → weightsdb-0.2.1}/docs/README.md +0 -0
  21. {weightsdb-0.2.0 → weightsdb-0.2.1}/docs/adoption-checklist.md +0 -0
  22. {weightsdb-0.2.0 → weightsdb-0.2.1}/docs/quickstart.md +0 -0
  23. {weightsdb-0.2.0 → weightsdb-0.2.1}/pyproject.toml +0 -0
  24. {weightsdb-0.2.0 → weightsdb-0.2.1}/requirements/README.md +0 -0
  25. {weightsdb-0.2.0 → weightsdb-0.2.1}/requirements/ci.lock +0 -0
  26. {weightsdb-0.2.0 → weightsdb-0.2.1}/requirements/release.in +0 -0
  27. {weightsdb-0.2.0 → weightsdb-0.2.1}/requirements/release.lock +0 -0
  28. {weightsdb-0.2.0 → weightsdb-0.2.1}/src/weightsdb/__init__.py +0 -0
  29. {weightsdb-0.2.0 → weightsdb-0.2.1}/src/weightsdb/backup.py +0 -0
  30. {weightsdb-0.2.0 → weightsdb-0.2.1}/src/weightsdb/errors.py +0 -0
  31. {weightsdb-0.2.0 → weightsdb-0.2.1}/src/weightsdb/health.py +0 -0
  32. {weightsdb-0.2.0 → weightsdb-0.2.1}/src/weightsdb/migrations.py +0 -0
  33. {weightsdb-0.2.0 → weightsdb-0.2.1}/src/weightsdb/py.typed +0 -0
  34. {weightsdb-0.2.0 → weightsdb-0.2.1}/src/weightsdb/redaction.py +0 -0
  35. {weightsdb-0.2.0 → weightsdb-0.2.1}/src/weightsdb/session.py +0 -0
  36. {weightsdb-0.2.0 → weightsdb-0.2.1}/src/weightsdb/testing.py +0 -0
  37. {weightsdb-0.2.0 → weightsdb-0.2.1}/src/weightsdb/types.py +0 -0
  38. {weightsdb-0.2.0 → weightsdb-0.2.1}/tests/conftest.py +0 -0
  39. {weightsdb-0.2.0 → weightsdb-0.2.1}/tests/contract/test_public_api.py +0 -0
  40. {weightsdb-0.2.0 → weightsdb-0.2.1}/tests/integration/_migration_fixture/__init__.py +0 -0
  41. {weightsdb-0.2.0 → weightsdb-0.2.1}/tests/integration/_migration_fixture/env.py +0 -0
  42. {weightsdb-0.2.0 → weightsdb-0.2.1}/tests/integration/_migration_fixture/models.py +0 -0
  43. {weightsdb-0.2.0 → weightsdb-0.2.1}/tests/integration/_migration_fixture/versions/0001_create_widgets.py +0 -0
  44. {weightsdb-0.2.0 → weightsdb-0.2.1}/tests/integration/_migration_fixture/versions/0002_add_note.py +0 -0
  45. {weightsdb-0.2.0 → weightsdb-0.2.1}/tests/integration/test_backup_restore.py +0 -0
  46. {weightsdb-0.2.0 → weightsdb-0.2.1}/tests/integration/test_two_schemas.py +0 -0
  47. {weightsdb-0.2.0 → weightsdb-0.2.1}/tests/performance/test_db_overhead.py +0 -0
  48. {weightsdb-0.2.0 → weightsdb-0.2.1}/tests/unit/test_health.py +0 -0
  49. {weightsdb-0.2.0 → weightsdb-0.2.1}/tests/unit/test_redaction.py +0 -0
  50. {weightsdb-0.2.0 → weightsdb-0.2.1}/tests/unit/test_session.py +0 -0
  51. {weightsdb-0.2.0 → weightsdb-0.2.1}/tests/unit/test_testing.py +0 -0
  52. {weightsdb-0.2.0 → weightsdb-0.2.1}/tests/unit/test_types.py +0 -0
@@ -7,12 +7,16 @@ packaging and release standards §3.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ### Fixed
11
- - **Coverage fell below the 95 % floor on Python 3.12 (94.89 %).** The uncovered lines were real
12
- gaps, not measurement noise: `upsert`'s documented refusal of a third dialect, the `StorageBusy`
13
- translation on a *read-only* `BEGIN`, `temporary_postgres`'s `WEIGHTSDB_REQUIRE_POSTGRES=1`
14
- failure path, longest-prefix mount selection against an unsorted mount table, and two backups
15
- sharing an mtime. Each now has a test; the floor is met on 3.12, 3.13 and 3.14 with margin.
10
+ ## [0.2.1] — 2026-08-31
11
+
12
+ ### Added
13
+ - **SQLite connections now set `PRAGMA secure_delete=ON`** alongside the existing per-connection
14
+ pragmas. `secure_delete` defaults to whatever the host's SQLite build chose, so whether a
15
+ deleted row's content was actually overwritten on disk varied by machine — and a consumer's
16
+ retention scrub is a promise about the disk, not about the schema (the M4 handoff's
17
+ "WeightsDB 0.2.1" item, raised by LoadCoach's retention verification). The pragma is applied
18
+ by the same `connect` listener as the rest, so a pool reconnect keeps it, and the test observes
19
+ it on a live connection rather than trusting the connect string.
16
20
 
17
21
  ## [0.2.0] — 2026-08-29
18
22
 
@@ -34,6 +38,14 @@ packaging and release standards §3.
34
38
  - SQLite lock contention beyond `busy_timeout` now raises the typed `StorageBusy` instead of a raw
35
39
  `sqlalchemy.exc.OperationalError` — a gap in the code this package was extracted from, invisible
36
40
  with one consumer and no test for it.
41
+ - `database_health()` no longer conflates a database *ahead of head* with a *pending migration*:
42
+ any `current != head` read as "pending migration", which is actively misleading when the
43
+ database was written by a newer build. Phase 3's own test list names the two as distinct cases.
44
+ - **Coverage fell below the 95 % floor on Python 3.12 (94.89 %).** The uncovered lines were real
45
+ gaps, not measurement noise: `upsert`'s documented refusal of a third dialect, the `StorageBusy`
46
+ translation on a *read-only* `BEGIN`, `temporary_postgres`'s `WEIGHTSDB_REQUIRE_POSTGRES=1`
47
+ failure path, longest-prefix mount selection against an unsorted mount table, and two backups
48
+ sharing an mtime. Each now has a test; the floor is met on 3.12, 3.13 and 3.14 with margin.
37
49
  - **`psycopg[binary]` is now part of the `dev` extra.** Seventeen tests assert dialect-specific
38
50
  behaviour by building a `postgresql://` engine — no server involved — which SQLAlchemy cannot do
39
51
  without an importable DBAPI. Every environment that installed `[dev]` alone failed them with
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: weightsdb
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Shared SQLAlchemy + Alembic plumbing: engines, sessions, pragmas, migrations, backup and health. No application table, no shared schema.
5
5
  Project-URL: Homepage, https://github.com/JPKell/WeightsDB
6
6
  Project-URL: Documentation, https://github.com/JPKell/WeightsDB/tree/main/docs
@@ -1,7 +1,7 @@
1
1
  # WeightsDB — Development Plan
2
2
 
3
3
  **Sequence position:** extracted during **LoadCoach Phase 1**, from FreeWeight's in-application
4
- storage layer. Not built speculatively (ADR-0011).
4
+ storage layer. Not built speculatively ([ADR-0011](../../adr/0011-shared-package-boundaries.md)).
5
5
  **Target:** `weightsdb 0.2.0` by the end of Phase 3; FreeWeight adopts it in FreeWeight Phase 12.
6
6
 
7
7
  **Precondition for starting:** FreeWeight is shipping with a working `freeweight.infrastructure.db`
@@ -26,7 +26,7 @@ identical (not yet swapped).
26
26
  * `upsert(session, model, values, *, index_elements)` — the suite's one sanctioned
27
27
  `INSERT … ON CONFLICT DO UPDATE`, because select-then-insert is a race under both dialects and a
28
28
  hand-written clause is how a dialect-specific variant gets in
29
- (ADR-0006).
29
+ ([ADR-0006](../../adr/0006-sqlite-and-postgresql-roles.md)).
30
30
  * Errors: `DatabaseUnavailable`, `StorageBusy`, `SchemaAhead`, `MigrationRequired`,
31
31
  `MigrationFailed`.
32
32
  * Credential redaction helper used by every error and log path.
@@ -2,8 +2,8 @@
2
2
 
3
3
  **Type:** Python package · **Import/distribution name:** `weightsdb` · **Layer:** 3 (capability package)
4
4
  **Status:** Specified, not implemented. **Extraction timing:** LoadCoach Phase 1, from FreeWeight's
5
- `freeweight.infrastructure.db` (ADR-0011).
6
- **Decision records:** ADR-0005, ADR-0006.
5
+ `freeweight.infrastructure.db` ([ADR-0011](../../adr/0011-shared-package-boundaries.md)).
6
+ **Decision records:** [ADR-0005](../../adr/0005-database-strategy.md), [ADR-0006](../../adr/0006-sqlite-and-postgresql-roles.md).
7
7
 
8
8
  ---
9
9
 
@@ -40,7 +40,7 @@ mechanics are shared.
40
40
  | Responsibility | Detail |
41
41
  |---|---|
42
42
  | Engine | `create_engine_for(url, …)` applying dialect-correct settings and pooling |
43
- | SQLite settings | `foreign_keys=ON`, `journal_mode=WAL`, `busy_timeout`, `synchronous=NORMAL`, applied per connection |
43
+ | SQLite settings | `foreign_keys=ON`, `journal_mode=WAL`, `busy_timeout`, `synchronous=NORMAL`, `secure_delete=ON`, applied per connection |
44
44
  | PostgreSQL settings | `statement_timeout`, `lock_timeout`, application name, schema selection |
45
45
  | Sessions | `session_factory`, `session_scope()` context manager with commit/rollback semantics |
46
46
  | Transactions | `transaction(session)` including `BEGIN IMMEDIATE` on SQLite |
@@ -262,4 +262,4 @@ Coverage floor: **95 %**.
262
262
  * Query-plan assertion helpers for tests (`assert_uses_index`), currently duplicated in applications.
263
263
  * Retention/pruning helpers for high-volume tables (samples, telemetry, events).
264
264
  * Optional DuckDB analytics attachment for FreeWeight, if the aggregate budgets are ever missed
265
- (ADR-0006 revisit trigger).
265
+ ([ADR-0006](../../adr/0006-sqlite-and-postgresql-roles.md) revisit trigger).
@@ -0,0 +1 @@
1
+ __version__ = "0.2.1"
@@ -1,7 +1,8 @@
1
1
  """weightsdb.engine — dialect-correct engine construction.
2
2
 
3
3
  Database standards §2: SQLite gets ``foreign_keys=ON``, ``journal_mode=WAL``, ``busy_timeout``,
4
- ``synchronous=NORMAL``, applied per connection so a pool reconnect never silently loses them;
4
+ ``synchronous=NORMAL``, ``secure_delete=ON``, applied per connection so a pool reconnect never
5
+ silently loses them;
5
6
  PostgreSQL gets ``statement_timeout``, ``lock_timeout`` and ``application_name``. Only these two
6
7
  dialects are supported (§2) — a third requires an ADR, not a code change here.
7
8
 
@@ -168,6 +169,11 @@ def _configure_sqlite(engine: Engine, *, busy_timeout_ms: int) -> None:
168
169
  cursor.execute("PRAGMA journal_mode=WAL")
169
170
  cursor.execute(f"PRAGMA busy_timeout={int(busy_timeout_ms)}")
170
171
  cursor.execute("PRAGMA synchronous=NORMAL")
172
+ # secure_delete defaults to whatever the host's SQLite build chose, so whether a
173
+ # deleted row's content is actually overwritten on disk would otherwise vary by
174
+ # machine — and a consumer's retention scrub is a promise about the disk, not about
175
+ # the schema. Set explicitly so the scrub means the same thing everywhere.
176
+ cursor.execute("PRAGMA secure_delete=ON")
171
177
  finally:
172
178
  cursor.close()
173
179
 
@@ -32,6 +32,26 @@ def test_fresh_database_migrates_to_head_sqlite() -> None:
32
32
  assert columns == {"id", "name", "note"}
33
33
 
34
34
 
35
+ def test_the_default_version_table_name_is_a_contract() -> None:
36
+ """The revision lives in ``alembic_version`` unless a consumer says otherwise.
37
+
38
+ Both applications' shipped databases record their revision under the default name
39
+ (FreeWeight P12's named failure mode): a changed default would make every existing consumer
40
+ database look unmigrated — so the default is pinned here, in WeightsDB itself, not only in
41
+ its consumers' suites.
42
+ """
43
+ with temporary_sqlite() as engine:
44
+ MigrationRunner(engine, script_location=_SCRIPT_LOCATION).upgrade(backup=False)
45
+ with engine.connect() as connection:
46
+ tables = {
47
+ row[0]
48
+ for row in connection.execute(
49
+ text("SELECT name FROM sqlite_master WHERE type='table'")
50
+ )
51
+ }
52
+ assert "alembic_version" in tables
53
+
54
+
35
55
  def test_fresh_database_migrates_to_head_postgres() -> None:
36
56
  with temporary_postgres() as engine:
37
57
  runner = MigrationRunner(engine, script_location=_SCRIPT_LOCATION)
@@ -30,6 +30,19 @@ def test_sqlite_pragmas_applied_on_fresh_connection() -> None:
30
30
  assert pragmas["journal_mode"] == "wal"
31
31
 
32
32
 
33
+ def test_sqlite_secure_delete_is_on_for_every_connection() -> None:
34
+ """A retention scrub must mean the same thing on every machine.
35
+
36
+ ``secure_delete`` defaults to whatever the host's SQLite build chose, so whether deleted
37
+ content is actually overwritten on disk would otherwise vary by machine (the M4 handoff's
38
+ WeightsDB 0.2.1 item). Observed on a live connection, not trusted from the connect string.
39
+ """
40
+ with temporary_sqlite() as engine:
41
+ with engine.connect() as connection:
42
+ secure_delete = connection.execute(text("PRAGMA secure_delete")).scalar_one()
43
+ assert secure_delete == 1
44
+
45
+
33
46
  def test_sqlite_pragmas_applied_after_forced_reconnect() -> None:
34
47
  """A pool recycle must not silently drop the pragmas (spec §7, §11.1)."""
35
48
  with temporary_sqlite() as engine:
@@ -44,8 +57,10 @@ def test_sqlite_pragmas_applied_after_forced_reconnect() -> None:
44
57
  connection.rollback()
45
58
  foreign_keys = connection.execute(text("PRAGMA foreign_keys")).scalar_one()
46
59
  journal_mode = connection.execute(text("PRAGMA journal_mode")).scalar_one()
60
+ secure_delete = connection.execute(text("PRAGMA secure_delete")).scalar_one()
47
61
  assert foreign_keys == 1
48
62
  assert journal_mode == "wal"
63
+ assert secure_delete == 1
49
64
 
50
65
 
51
66
  def test_sqlite_busy_timeout_raises_storage_busy() -> None:
@@ -202,6 +217,7 @@ def test_sqlite_connect_listener_hands_transaction_control_to_sqlalchemy() -> No
202
217
  "PRAGMA journal_mode=WAL",
203
218
  "PRAGMA busy_timeout=5000",
204
219
  "PRAGMA synchronous=NORMAL",
220
+ "PRAGMA secure_delete=ON",
205
221
  ]
206
222
 
207
223
 
@@ -1 +0,0 @@
1
- __version__ = "0.2.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes