txstore 0.1.0__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.
- txstore-0.1.0/.gitignore +13 -0
- txstore-0.1.0/AGENTS.md +80 -0
- txstore-0.1.0/CLAUDE.md +1 -0
- txstore-0.1.0/CONFIG.md +90 -0
- txstore-0.1.0/EXCEPTIONS.md +46 -0
- txstore-0.1.0/LICENSE +21 -0
- txstore-0.1.0/PKG-INFO +151 -0
- txstore-0.1.0/README.md +121 -0
- txstore-0.1.0/ROADMAP.md +66 -0
- txstore-0.1.0/SETUP.md +116 -0
- txstore-0.1.0/STORAGE.md +434 -0
- txstore-0.1.0/STRUCTURE.md +58 -0
- txstore-0.1.0/STYLE.md +43 -0
- txstore-0.1.0/TESTING.md +92 -0
- txstore-0.1.0/VALIDATION.md +151 -0
- txstore-0.1.0/docs/development/README.md +31 -0
- txstore-0.1.0/docs/development/architecture.md +52 -0
- txstore-0.1.0/docs/development/class.md +51 -0
- txstore-0.1.0/docs/development/diagrams/architecture-modules.puml +53 -0
- txstore-0.1.0/docs/development/diagrams/architecture-modules.svg +2 -0
- txstore-0.1.0/docs/development/diagrams/class-storage.puml +71 -0
- txstore-0.1.0/docs/development/diagrams/class-storage.svg +2 -0
- txstore-0.1.0/docs/development/diagrams/footmark-token-path.puml +10 -0
- txstore-0.1.0/docs/development/diagrams/footmark-token-path.svg +2 -0
- txstore-0.1.0/docs/development/diagrams/s3-metadata-rewrite.puml +18 -0
- txstore-0.1.0/docs/development/diagrams/s3-metadata-rewrite.svg +2 -0
- txstore-0.1.0/docs/development/diagrams/s3-writer-lock.puml +27 -0
- txstore-0.1.0/docs/development/diagrams/s3-writer-lock.svg +2 -0
- txstore-0.1.0/docs/development/diagrams/transactions-commit.puml +25 -0
- txstore-0.1.0/docs/development/diagrams/transactions-commit.svg +2 -0
- txstore-0.1.0/docs/development/diagrams/transactions-discard.puml +13 -0
- txstore-0.1.0/docs/development/diagrams/transactions-discard.svg +2 -0
- txstore-0.1.0/docs/development/diagrams/transactions-revert.puml +13 -0
- txstore-0.1.0/docs/development/diagrams/transactions-revert.svg +2 -0
- txstore-0.1.0/docs/development/footmark.md +56 -0
- txstore-0.1.0/docs/development/s3.md +76 -0
- txstore-0.1.0/docs/development/transactions.md +97 -0
- txstore-0.1.0/docs/guides/README.md +37 -0
- txstore-0.1.0/docs/guides/backends.md +91 -0
- txstore-0.1.0/docs/guides/history.md +81 -0
- txstore-0.1.0/docs/guides/introduction.md +52 -0
- txstore-0.1.0/docs/guides/transactions.md +63 -0
- txstore-0.1.0/docs/reference/README.md +10 -0
- txstore-0.1.0/docs/reference/footmark.proto +15 -0
- txstore-0.1.0/pyproject.toml +138 -0
- txstore-0.1.0/scripts/check_docs.py +501 -0
- txstore-0.1.0/scripts/render_diagrams.py +173 -0
- txstore-0.1.0/src/txstore/__init__.py +108 -0
- txstore-0.1.0/src/txstore/_capabilities.py +185 -0
- txstore-0.1.0/src/txstore/_env.py +70 -0
- txstore-0.1.0/src/txstore/_footmark_wire.py +182 -0
- txstore-0.1.0/src/txstore/_fs_paths.py +279 -0
- txstore-0.1.0/src/txstore/_reclaim.py +217 -0
- txstore-0.1.0/src/txstore/_reserved.py +47 -0
- txstore-0.1.0/src/txstore/_s3_acl.py +57 -0
- txstore-0.1.0/src/txstore/_s3_client.py +462 -0
- txstore-0.1.0/src/txstore/_s3_errors.py +119 -0
- txstore-0.1.0/src/txstore/_s3_lock.py +366 -0
- txstore-0.1.0/src/txstore/_timestamp.py +128 -0
- txstore-0.1.0/src/txstore/base.py +208 -0
- txstore-0.1.0/src/txstore/factory.py +157 -0
- txstore-0.1.0/src/txstore/footmark.py +195 -0
- txstore-0.1.0/src/txstore/hexdigest.py +162 -0
- txstore-0.1.0/src/txstore/history.py +633 -0
- txstore-0.1.0/src/txstore/metrics.py +113 -0
- txstore-0.1.0/src/txstore/py.typed +0 -0
- txstore-0.1.0/src/txstore/storage_fs.py +686 -0
- txstore-0.1.0/src/txstore/storage_http.py +258 -0
- txstore-0.1.0/src/txstore/storage_null.py +114 -0
- txstore-0.1.0/src/txstore/storage_s3.py +727 -0
- txstore-0.1.0/src/txstore/transaction.py +1086 -0
- txstore-0.1.0/src/txstore/url.py +426 -0
- txstore-0.1.0/tests/conftest.py +93 -0
- txstore-0.1.0/tests/helpers/__init__.py +0 -0
- txstore-0.1.0/tests/helpers/backends.py +88 -0
- txstore-0.1.0/tests/helpers/s3.py +89 -0
- txstore-0.1.0/tests/helpers/storage.py +384 -0
- txstore-0.1.0/tests/test_base.py +197 -0
- txstore-0.1.0/tests/test_capabilities.py +119 -0
- txstore-0.1.0/tests/test_check_docs.py +277 -0
- txstore-0.1.0/tests/test_env.py +75 -0
- txstore-0.1.0/tests/test_factory.py +369 -0
- txstore-0.1.0/tests/test_footmark.py +185 -0
- txstore-0.1.0/tests/test_footmark_wire.py +86 -0
- txstore-0.1.0/tests/test_fs_paths.py +456 -0
- txstore-0.1.0/tests/test_fs_transactions.py +617 -0
- txstore-0.1.0/tests/test_harness.py +62 -0
- txstore-0.1.0/tests/test_hexdigest.py +128 -0
- txstore-0.1.0/tests/test_history.py +1363 -0
- txstore-0.1.0/tests/test_metrics.py +93 -0
- txstore-0.1.0/tests/test_package.py +408 -0
- txstore-0.1.0/tests/test_render_diagrams.py +255 -0
- txstore-0.1.0/tests/test_reserved.py +264 -0
- txstore-0.1.0/tests/test_s3_acl.py +112 -0
- txstore-0.1.0/tests/test_s3_client.py +424 -0
- txstore-0.1.0/tests/test_s3_errors.py +183 -0
- txstore-0.1.0/tests/test_s3_lock.py +657 -0
- txstore-0.1.0/tests/test_s3_transactions.py +686 -0
- txstore-0.1.0/tests/test_storage_fs.py +918 -0
- txstore-0.1.0/tests/test_storage_http.py +580 -0
- txstore-0.1.0/tests/test_storage_null.py +135 -0
- txstore-0.1.0/tests/test_storage_s3.py +954 -0
- txstore-0.1.0/tests/test_timestamp.py +122 -0
- txstore-0.1.0/tests/test_transaction.py +145 -0
- txstore-0.1.0/tests/test_transactions.py +2133 -0
- txstore-0.1.0/tests/test_url.py +584 -0
- txstore-0.1.0/uv.lock +1824 -0
txstore-0.1.0/.gitignore
ADDED
txstore-0.1.0/AGENTS.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# AI coding agent guide
|
|
2
|
+
|
|
3
|
+
Root contracts decide behavior; the implementation shows current state. Keep one canonical owner per
|
|
4
|
+
fact; link instead of copying.
|
|
5
|
+
|
|
6
|
+
## Source priority
|
|
7
|
+
|
|
8
|
+
1. The root contract that owns the domain below; `STYLE.md` prevails on style.
|
|
9
|
+
2. Implementation under `src/txstore/` when checking current behavior.
|
|
10
|
+
3. `docs/guides/` for user how-tos, `docs/development/` for contributor explanations and diagrams,
|
|
11
|
+
`docs/reference/` for reference artifacts, and `ROADMAP.md` for planned work and known
|
|
12
|
+
deviations. All are non-canonical.
|
|
13
|
+
|
|
14
|
+
Resolve drift by updating the owning contract and implementation together, not by adding a second
|
|
15
|
+
definition.
|
|
16
|
+
|
|
17
|
+
## Canonical sources
|
|
18
|
+
|
|
19
|
+
| File | Owns |
|
|
20
|
+
| -- | -- |
|
|
21
|
+
| `STYLE.md` | Coding conventions, logging rules, performance rules; prevails on style |
|
|
22
|
+
| `STRUCTURE.md` | Layout, module ownership, public façade, internal import directions |
|
|
23
|
+
| `STORAGE.md` | Storage ABI, backends, construction, transactions, footmarks and their wire format, timestamps, history, S3 ACL and metadata rules, digests |
|
|
24
|
+
| `VALIDATION.md` | Input constraints at the URL, path, configuration, and retention boundaries |
|
|
25
|
+
| `EXCEPTIONS.md` | Exception classes, raising conditions, chaining rules |
|
|
26
|
+
| `CONFIG.md` | `make_storage` and retention settings, `TX_STORE_*` variables, defaults, secrets |
|
|
27
|
+
| `TESTING.md` | Required coverage, harness rules, gates |
|
|
28
|
+
| `SETUP.md` | Install, test, quality gates, diagrams, dependency policy, release, upgrade notes |
|
|
29
|
+
|
|
30
|
+
## Task routing
|
|
31
|
+
|
|
32
|
+
Read only the relevant sections; locate code from [STRUCTURE.md](STRUCTURE.md#layout).
|
|
33
|
+
|
|
34
|
+
| Task | Primary | Also check |
|
|
35
|
+
| -- | -- | -- |
|
|
36
|
+
| Backend / primitive | `STORAGE.md` backends and primitives | `STRUCTURE.md` layout, `VALIDATION.md` URLs, `TESTING.md` |
|
|
37
|
+
| Transaction / history / lock | `STORAGE.md` transactions and history | `EXCEPTIONS.md`, `TESTING.md` |
|
|
38
|
+
| Footmark wire / timestamp | `STORAGE.md` footmarks and timestamps | `docs/reference/footmark.proto`, `TESTING.md` golden vectors |
|
|
39
|
+
| S3 ACL / metadata | `STORAGE.md` S3 sections | `VALIDATION.md` canned ACLs, `CONFIG.md` |
|
|
40
|
+
| URL / path rule | `VALIDATION.md` | `EXCEPTIONS.md`, `TESTING.md` |
|
|
41
|
+
| Exception | `EXCEPTIONS.md` | Owning contract of the condition |
|
|
42
|
+
| Argument / `TX_STORE_*` variable | `CONFIG.md` | `VALIDATION.md` environment values, `src/txstore/_env.py` |
|
|
43
|
+
| Digest | `STORAGE.md` digests | `src/txstore/hexdigest.py` |
|
|
44
|
+
| Import boundary | `STRUCTURE.md` dependencies | `tests/test_package.py`, `STYLE.md` |
|
|
45
|
+
| Dependency change | `SETUP.md` dependency policy | `STRUCTURE.md` dependencies, `tests/test_package.py`, `.github/debian-test.sh` |
|
|
46
|
+
| Bug fix | Owning contract + `EXCEPTIONS.md` | `TESTING.md` |
|
|
47
|
+
| Tests | `TESTING.md` | Contract for the behavior under test |
|
|
48
|
+
| Contract / doc edit | Writing rules below → owning contract | `scripts/check_docs.py`, `SETUP.md` quality gates |
|
|
49
|
+
| Release | `SETUP.md` release | `.github/workflows/release.yml`, `ROADMAP.md` phases |
|
|
50
|
+
| Quality gate / CI change | `SETUP.md` quality gates | `.github/workflows/ci.yml` |
|
|
51
|
+
| Developer explanation / diagram | `docs/development/` | Owning root contract, `SETUP.md` diagrams |
|
|
52
|
+
| User how-to / future behavior | `docs/guides/` / `ROADMAP.md` | Owning root contract before implementing |
|
|
53
|
+
|
|
54
|
+
## Agent workflow
|
|
55
|
+
|
|
56
|
+
1. Read only the owning contract sections from the routing table; open the module named in
|
|
57
|
+
[STRUCTURE.md](STRUCTURE.md#layout).
|
|
58
|
+
2. Implement within the import directions of [STRUCTURE.md](STRUCTURE.md#dependencies). Write errors
|
|
59
|
+
as `condition → ExceptionClass` and link `EXCEPTIONS.md` instead of copying its table. A new
|
|
60
|
+
setting → [CONFIG.md](CONFIG.md#maintenance); a new public name →
|
|
61
|
+
[STRUCTURE.md](STRUCTURE.md#maintenance).
|
|
62
|
+
3. Add the narrowest test that falsifies the change per [TESTING.md](TESTING.md#maintenance).
|
|
63
|
+
4. Run the [SETUP.md quality gates](SETUP.md#quality-gates); contract and `docs/` edits also run
|
|
64
|
+
`uv run python scripts/check_docs.py`. CI runs the gates again.
|
|
65
|
+
|
|
66
|
+
## Contract writing rules
|
|
67
|
+
|
|
68
|
+
- One owner per fact. Link it as `→ FILE.md#anchor`; never copy its table or values.
|
|
69
|
+
- Preamble opens with `Owns: <domain>. Routing → AGENTS.md.`, optionally followed by one short
|
|
70
|
+
orienting paragraph. No cross-document pointer chains.
|
|
71
|
+
- Errors: `condition → ExceptionClass`; the class must be in `EXCEPTIONS.md` or be `ValueError` or
|
|
72
|
+
`NotImplementedError`.
|
|
73
|
+
- Prefer tables; one fact per row. Narrative cells stay under ~300 characters: split rows rather
|
|
74
|
+
than pad. Enumerations (method lists, value lists) are exempt.
|
|
75
|
+
- Tables use `| -- |` separator rows and unpadded cells; do not run a table aligner.
|
|
76
|
+
- Bare imperative rules; no RFC 2119 keywords. Headings are sentence case and unique per file.
|
|
77
|
+
- No TODOs or "current exception" notes in contracts; record them in
|
|
78
|
+
[ROADMAP.md known deviations](ROADMAP.md#known-deviations) and link.
|
|
79
|
+
- Wrap prose at 100 columns; table rows are exempt. Prefer `→` shortcuts to sentences.
|
|
80
|
+
- After any contract or `docs/` edit run `uv run python scripts/check_docs.py`.
|
txstore-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
txstore-0.1.0/CONFIG.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Configuration contract
|
|
2
|
+
|
|
3
|
+
Owns: `make_storage` and `HistoryRetention.from_env` settings, their `TX_STORE_*` environment
|
|
4
|
+
variables, resolution order, defaults, and secrets. Routing →
|
|
5
|
+
[AGENTS.md](AGENTS.md#canonical-sources).
|
|
6
|
+
|
|
7
|
+
txstore's own configuration is keyword arguments and `TX_STORE_*` variables only; an application
|
|
8
|
+
that exposes its own flags maps them to the keyword arguments below. The HTTP and S3 clients also
|
|
9
|
+
honour their libraries' standard environment → [Secrets](#secrets).
|
|
10
|
+
|
|
11
|
+
## Resolution
|
|
12
|
+
|
|
13
|
+
| Rule | Behavior |
|
|
14
|
+
| -- | -- |
|
|
15
|
+
| Order | Explicit keyword argument, then the `TX_STORE_*` variable, then the default. A `None` argument counts as not given. |
|
|
16
|
+
| Lazy read | A variable is read only when its argument is `None`, so an invalid value fails only a call that falls back to it. |
|
|
17
|
+
| Readers | Only `make_storage` and `HistoryRetention.from_env` read variables; backend constructors and `HistoryRetention(...)` take explicit values only. |
|
|
18
|
+
| Timing | Variables are read on each call, never at import. |
|
|
19
|
+
| Values | Grammar and failures → [VALIDATION.md](VALIDATION.md#environment-values). |
|
|
20
|
+
|
|
21
|
+
## Storage construction
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
make_storage(url, *, create_path=False, verify_ssl=None, s3_access=None, s3_secret=None,
|
|
25
|
+
s3_default_acl=None, s3_bucket_acl=None)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
→ [STORAGE.md](STORAGE.md#construction).
|
|
29
|
+
|
|
30
|
+
| Argument | Variable | Default | Notes |
|
|
31
|
+
| -- | -- | -- | -- |
|
|
32
|
+
| `create_path` | none | `False` | Create the S3 bucket or FS root when missing. |
|
|
33
|
+
| `verify_ssl` | `TX_STORE_VERIFY_SSL` | `True` | TLS verification for HTTP and S3; resolving to `False` logs a warning. |
|
|
34
|
+
| `s3_access` | `TX_STORE_S3_ACCESS` | none | Secret; required for `s3://`. |
|
|
35
|
+
| `s3_secret` | `TX_STORE_S3_SECRET` | none | Secret; required for `s3://`. |
|
|
36
|
+
| `s3_default_acl` | `TX_STORE_S3_DEFAULT_ACL` | `none` (`NO_ACL`) | Object ACL for writes that pass none → [STORAGE.md](STORAGE.md#s3-acl-rules); values → [VALIDATION.md](VALIDATION.md#s3-canned-acls). |
|
|
37
|
+
| `s3_bucket_acl` | `TX_STORE_S3_BUCKET_ACL` | `None` | Bucket ACL at creation → [STORAGE.md](STORAGE.md#construction). |
|
|
38
|
+
|
|
39
|
+
## History retention
|
|
40
|
+
|
|
41
|
+
`HistoryRetention.from_env(*, keep=None, max_age_days=None)` → [STORAGE.md](STORAGE.md#retention);
|
|
42
|
+
values → [VALIDATION.md](VALIDATION.md#history-retention).
|
|
43
|
+
|
|
44
|
+
| Argument | Variable | Default | Notes |
|
|
45
|
+
| -- | -- | -- | -- |
|
|
46
|
+
| `keep` | `TX_STORE_HISTORY_KEEP` | `20` (`HISTORY_KEEP_DEFAULT`) | Floor of protected ids → [STORAGE.md](STORAGE.md#retention). |
|
|
47
|
+
| `max_age_days` | `TX_STORE_HISTORY_MAX_AGE_DAYS` | `90` (`HISTORY_MAX_AGE_DAYS_DEFAULT`) | Expiry age → [STORAGE.md](STORAGE.md#retention). |
|
|
48
|
+
|
|
49
|
+
## Environment variables
|
|
50
|
+
|
|
51
|
+
| Variable | Read by | Type |
|
|
52
|
+
| -- | -- | -- |
|
|
53
|
+
| `TX_STORE_VERIFY_SSL` | `make_storage` | Boolean |
|
|
54
|
+
| `TX_STORE_S3_ACCESS` | `make_storage` | String, secret |
|
|
55
|
+
| `TX_STORE_S3_SECRET` | `make_storage` | String, secret |
|
|
56
|
+
| `TX_STORE_S3_DEFAULT_ACL` | `make_storage` | Object canned ACL or `none` |
|
|
57
|
+
| `TX_STORE_S3_BUCKET_ACL` | `make_storage` | Bucket canned ACL |
|
|
58
|
+
| `TX_STORE_HISTORY_KEEP` | `HistoryRetention.from_env` | Integer |
|
|
59
|
+
| `TX_STORE_HISTORY_MAX_AGE_DAYS` | `HistoryRetention.from_env` | Integer |
|
|
60
|
+
|
|
61
|
+
## Secrets
|
|
62
|
+
|
|
63
|
+
- `TX_STORE_S3_ACCESS` and `TX_STORE_S3_SECRET`, and the matching arguments, are secrets: never
|
|
64
|
+
logged, never part of `repr`, never written to storage.
|
|
65
|
+
- Credentials in a URL's userinfo (`user:password@host`) and its query string are replaced by `***`
|
|
66
|
+
in logs, `repr`, and exception messages; which URLs are accepted →
|
|
67
|
+
[VALIDATION.md](VALIDATION.md#storage-urls).
|
|
68
|
+
- Percent-encode non-ASCII characters and `/`, `?`, `#`, and `@` in URL credentials; how an
|
|
69
|
+
unencoded one parses → [VALIDATION.md](VALIDATION.md#storage-urls). A URL whose authority does not
|
|
70
|
+
parse appears as `<unparseable URL>`; a password that parses as a host, port, and path is kept out
|
|
71
|
+
of logs only by the encoding.
|
|
72
|
+
- Object keys are names, not secrets: backends name them in logs and exception messages, `;params`
|
|
73
|
+
included; HTTP join refusals that name only the base →
|
|
74
|
+
[VALIDATION.md](VALIDATION.md#http-url-join-safety). Carry credentials in URL userinfo, netrc, or
|
|
75
|
+
the S3 settings, never in a key.
|
|
76
|
+
- Prefer the variables, or a caller's secret store, to literals in code.
|
|
77
|
+
- The HTTP backend sends URL userinfo as Basic auth on URLs stripped of it, so no `requests` error
|
|
78
|
+
or chained cause quotes the credentials.
|
|
79
|
+
- The HTTP backend sends `~/.netrc` (or `$NETRC`) credentials for the URL's host, overriding URL
|
|
80
|
+
userinfo, and routes through `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`; `REQUESTS_CA_BUNDLE`
|
|
81
|
+
replaces the CA store but never turns verification off. boto3 reads its own variables for S3.
|
|
82
|
+
- Over plain `http://`, URL userinfo and netrc credentials travel as cleartext Basic auth; the HTTP
|
|
83
|
+
backend warns at construction when such a URL carries userinfo, and once per instance when it
|
|
84
|
+
sends netrc credentials. Use `https://` for an authenticated server.
|
|
85
|
+
|
|
86
|
+
## Maintenance
|
|
87
|
+
|
|
88
|
+
A new setting adds its keyword argument, its `_env` read with a `TX_STORE_` literal, and its row
|
|
89
|
+
here in the same change; `scripts/check_docs.py` fails when a read variable has no
|
|
90
|
+
[Environment variables](#environment-variables) row or a row names no read variable.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Exception contract
|
|
2
|
+
|
|
3
|
+
Owns: exception classes, the conditions that raise them, and raising and chaining rules. Routing →
|
|
4
|
+
[AGENTS.md](AGENTS.md#canonical-sources).
|
|
5
|
+
|
|
6
|
+
## Rules
|
|
7
|
+
|
|
8
|
+
- Contracts write a failure as `condition → ExceptionClass`.
|
|
9
|
+
- Backend and storage-format failures raise `StorageException` or its subclass; a caller handling
|
|
10
|
+
`StorageException` handles every storage failure.
|
|
11
|
+
- Wrap a library error (`ClientError`, `BotoCoreError`, `OSError`, `requests` errors) in
|
|
12
|
+
`StorageException` with `raise ... from exc`; never let it escape a public `Storage` method.
|
|
13
|
+
Errors from a `put_stream` source stream → [STORAGE.md](STORAGE.md#storage-abi).
|
|
14
|
+
- Invalid arguments and configuration raise `ValueError`, except a URL that parses but cannot name a
|
|
15
|
+
storage (empty, HTTP or S3 without host, `s3://` without resolved keys): `make_storage` or the
|
|
16
|
+
backend constructor raises `StorageException` → [STORAGE.md](STORAGE.md#construction); a malformed
|
|
17
|
+
`s3://` path stays `ValueError` → [Other errors](#other-errors).
|
|
18
|
+
- Unsupported backend operations → [Other errors](#other-errors).
|
|
19
|
+
- Messages state the condition and the object; secrets never appear →
|
|
20
|
+
[CONFIG.md](CONFIG.md#secrets).
|
|
21
|
+
|
|
22
|
+
## Storage exceptions
|
|
23
|
+
|
|
24
|
+
| Exception | Module | Raised when |
|
|
25
|
+
| -- | -- | -- |
|
|
26
|
+
| `StorageException` | `txstore/base.py` | A backend operation, transaction, history, or lock step fails → [STORAGE.md](STORAGE.md#failure-semantics). |
|
|
27
|
+
| `StorageNotFoundException` | `txstore/base.py` | The named object is missing → [STORAGE.md](STORAGE.md#storage-abi); subclass of `StorageException`. |
|
|
28
|
+
|
|
29
|
+
## Other errors
|
|
30
|
+
|
|
31
|
+
| Error | Raised when |
|
|
32
|
+
| -- | -- |
|
|
33
|
+
| `ValueError` | Unsupported URL scheme, an authority that does not parse, an `s3://` URL holding `@`, an `fs://` URL with an authority or a path that is not absolute, or a storage URL holding `?` or `#` → [VALIDATION.md](VALIDATION.md#storage-urls). |
|
|
34
|
+
| `ValueError` | Empty S3 bucket, or an S3 prefix with an empty segment → [VALIDATION.md](VALIDATION.md#s3-url-path-split). |
|
|
35
|
+
| `ValueError` | Unsafe HTTP join passed to `join_url`; `StorageHTTP` raises `StorageException` instead → [VALIDATION.md](VALIDATION.md#http-url-join-safety). |
|
|
36
|
+
| `ValueError` | Invalid canned ACL → [VALIDATION.md](VALIDATION.md#s3-canned-acls). |
|
|
37
|
+
| `ValueError` | Invalid environment value → [VALIDATION.md](VALIDATION.md#environment-values). |
|
|
38
|
+
| `ValueError` | Out-of-range retention → [VALIDATION.md](VALIDATION.md#history-retention). |
|
|
39
|
+
| `ValueError` | Out-of-range timestamp passed to `Timestamp`; one in a history record raises `StorageException` instead → [STORAGE.md](STORAGE.md#timestamps). |
|
|
40
|
+
| `ValueError` | Out-of-range transaction id → [STORAGE.md](STORAGE.md#lifecycle). |
|
|
41
|
+
| `NotImplementedError` | A read-only, write-only, or non-transactional backend is asked for an operation it lacks → [STORAGE.md](STORAGE.md#backends); not a `StorageException` → [ROADMAP.md](ROADMAP.md#known-deviations). |
|
|
42
|
+
|
|
43
|
+
## Maintenance
|
|
44
|
+
|
|
45
|
+
A new exception class adds a row to [Storage exceptions](#storage-exceptions) in the same change;
|
|
46
|
+
`scripts/check_docs.py` fails when the table and the `class *Exception` definitions differ.
|
txstore-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Václav Vinklát
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
txstore-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: txstore
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unified object storage for Python: one interface over filesystem, S3, and HTTP, with transactions.
|
|
5
|
+
Project-URL: Homepage, https://github.com/vinklat/txstore
|
|
6
|
+
Project-URL: Source, https://github.com/vinklat/txstore
|
|
7
|
+
Project-URL: Issues, https://github.com/vinklat/txstore/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/vinklat/txstore/blob/main/docs/guides/README.md
|
|
9
|
+
Project-URL: Changelog, https://github.com/vinklat/txstore/blob/main/SETUP.md#upgrade-notes
|
|
10
|
+
Author: Václav Vinklát
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: atomic,digest,filesystem,object-storage,s3,storage,transaction
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: System :: Archiving
|
|
22
|
+
Classifier: Topic :: System :: Filesystems
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.13
|
|
25
|
+
Requires-Dist: boto3<2,>=1.40
|
|
26
|
+
Requires-Dist: botocore<2,>=1.40
|
|
27
|
+
Requires-Dist: pyxattr<0.9,>=0.8.1
|
|
28
|
+
Requires-Dist: requests<3,>=2.32
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# txstore
|
|
32
|
+
|
|
33
|
+
Unified object storage for Python: one `Storage` interface over a local filesystem, S3-compatible
|
|
34
|
+
buckets, and HTTP servers, selected by URL. Read, write, list, and stream objects between backends
|
|
35
|
+
with the same calls, and on the filesystem and S3, optionally group writes into a transaction you
|
|
36
|
+
commit, discard, or revert as one unit.
|
|
37
|
+
|
|
38
|
+
Current version: **0.1.0**. Planned work → [ROADMAP.md](https://github.com/vinklat/txstore/blob/v0.1.0/ROADMAP.md).
|
|
39
|
+
|
|
40
|
+
## Capabilities
|
|
41
|
+
|
|
42
|
+
- One `Storage` interface over the filesystem (`fs://`), S3-compatible object stores (`s3://`),
|
|
43
|
+
read-only HTTP (`http://`, `https://`), and a write-only digest sink; `make_storage(url)` picks
|
|
44
|
+
the backend, so code switches storage by changing a URL.
|
|
45
|
+
- Get, put, list, copy, move, and metadata calls that behave the same on every backend that supports
|
|
46
|
+
them.
|
|
47
|
+
- Transactions on filesystem and S3: writes are staged as hidden footmark objects and committed,
|
|
48
|
+
discarded, or reverted together.
|
|
49
|
+
- A transaction history at the storage root with age and count retention, revert of the latest
|
|
50
|
+
transaction, and cleanup of orphaned footmarks.
|
|
51
|
+
- A cross-process writer lock: `flock` on the filesystem, conditional writes on S3.
|
|
52
|
+
- Streamed transfers between storages that return MD5, SHA-1, SHA-256, and SHA-512 digests in one
|
|
53
|
+
pass.
|
|
54
|
+
- S3 canned-ACL preservation across transactional copies and metadata rewrites.
|
|
55
|
+
- Configuration by keyword arguments or `TX_STORE_*` environment variables; no CLI coupling.
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
### Install
|
|
60
|
+
|
|
61
|
+
Released versions are published to [PyPI](https://pypi.org/project/txstore/):
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install txstore
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Settings → [CONFIG.md](https://github.com/vinklat/txstore/blob/v0.1.0/CONFIG.md); behavior → [STORAGE.md](https://github.com/vinklat/txstore/blob/v0.1.0/STORAGE.md).
|
|
68
|
+
|
|
69
|
+
### Stream between backends
|
|
70
|
+
|
|
71
|
+
Every backend is a `Storage` built from a URL, so moving data between technologies is one call.
|
|
72
|
+
Download a web page and save it to disk, streamed in chunks rather than loaded into memory:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from txstore import make_storage
|
|
76
|
+
|
|
77
|
+
web = make_storage("https://docs.python.org/3")
|
|
78
|
+
disk = make_storage("fs:///srv/data", create_path=True)
|
|
79
|
+
|
|
80
|
+
digests = web.transfer("library/io.html", disk, "pages/io.html")
|
|
81
|
+
print(digests)
|
|
82
|
+
# {'MD5Sum': '...', 'SHA1': '...', 'SHA256': '...', 'SHA512': '...', 'Size': '180750'}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Swap either URL for `s3://...` and the same call uploads to or downloads from a bucket. HTTP storage
|
|
86
|
+
is read-only and does not follow redirects; point the base URL at the final location.
|
|
87
|
+
|
|
88
|
+
### Group writes into a transaction
|
|
89
|
+
|
|
90
|
+
Writes to the filesystem or S3 can be grouped into a transaction:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from txstore import HistoryRetention, TransactionHistory, make_storage, maybe_transaction
|
|
94
|
+
|
|
95
|
+
storage = make_storage("fs:///srv/data", create_path=True)
|
|
96
|
+
retention = HistoryRetention.from_env()
|
|
97
|
+
|
|
98
|
+
# Write a data file and the manifest that points to it: both are applied together on commit.
|
|
99
|
+
with maybe_transaction(storage, retention):
|
|
100
|
+
storage.put("reports/october.csv", b"day,orders\n1,42\n2,57\n")
|
|
101
|
+
storage.put("manifest.json", b'{"latest": "reports/october.csv"}')
|
|
102
|
+
|
|
103
|
+
# A failure inside the block discards every staged write.
|
|
104
|
+
try:
|
|
105
|
+
with maybe_transaction(storage, retention):
|
|
106
|
+
storage.put("reports/november.csv", b"day,orders\n1,???\n")
|
|
107
|
+
raise ValueError("validation failed")
|
|
108
|
+
except ValueError:
|
|
109
|
+
assert not storage.exists("reports/november.csv")
|
|
110
|
+
|
|
111
|
+
# Changed your mind later? Revert the last transaction as one unit.
|
|
112
|
+
history = TransactionHistory(storage)
|
|
113
|
+
print(
|
|
114
|
+
[(t.id, t.timestamp.to_json()) for t in history.load()]
|
|
115
|
+
) # [(1, '2026-10-23T19:08:36.024119Z')]
|
|
116
|
+
history.revert_last()
|
|
117
|
+
assert not storage.exists("manifest.json")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Connect to S3
|
|
121
|
+
|
|
122
|
+
On S3, pass the endpoint and bucket in the URL and the credentials as arguments or variables:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
export TX_STORE_S3_ACCESS=... TX_STORE_S3_SECRET=...
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
storage = make_storage("s3://s3.example.com/bucket/prefix", s3_default_acl="private")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Documentation
|
|
133
|
+
|
|
134
|
+
| Location | Audience | Authority |
|
|
135
|
+
| -- | -- | -- |
|
|
136
|
+
| `/` root contract documents | Contributors and AI agents | Canonical |
|
|
137
|
+
| [docs/guides/](https://github.com/vinklat/txstore/blob/v0.1.0/docs/guides/README.md) | Library users | Non-canonical guides and examples |
|
|
138
|
+
| [docs/development/](https://github.com/vinklat/txstore/blob/v0.1.0/docs/development/README.md) | Contributors | Non-canonical code maps, flow diagrams, explanations |
|
|
139
|
+
| [docs/reference/](https://github.com/vinklat/txstore/blob/v0.1.0/docs/reference/README.md) | Contributors and tooling | Non-canonical reference artifacts |
|
|
140
|
+
|
|
141
|
+
## Canonical ownership
|
|
142
|
+
|
|
143
|
+
Domain owners and agent navigation → [AGENTS.md](https://github.com/vinklat/txstore/blob/v0.1.0/AGENTS.md#canonical-sources).
|
|
144
|
+
|
|
145
|
+
## Technology
|
|
146
|
+
|
|
147
|
+
Python 3.13+, boto3, requests, pyxattr; pytest, moto, Ruff, mypy, and uv for development.
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
MIT → [LICENSE](https://github.com/vinklat/txstore/blob/v0.1.0/LICENSE).
|
txstore-0.1.0/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# txstore
|
|
2
|
+
|
|
3
|
+
Unified object storage for Python: one `Storage` interface over a local filesystem, S3-compatible
|
|
4
|
+
buckets, and HTTP servers, selected by URL. Read, write, list, and stream objects between backends
|
|
5
|
+
with the same calls, and on the filesystem and S3, optionally group writes into a transaction you
|
|
6
|
+
commit, discard, or revert as one unit.
|
|
7
|
+
|
|
8
|
+
Current version: **0.1.0**. Planned work → [ROADMAP.md](https://github.com/vinklat/txstore/blob/v0.1.0/ROADMAP.md).
|
|
9
|
+
|
|
10
|
+
## Capabilities
|
|
11
|
+
|
|
12
|
+
- One `Storage` interface over the filesystem (`fs://`), S3-compatible object stores (`s3://`),
|
|
13
|
+
read-only HTTP (`http://`, `https://`), and a write-only digest sink; `make_storage(url)` picks
|
|
14
|
+
the backend, so code switches storage by changing a URL.
|
|
15
|
+
- Get, put, list, copy, move, and metadata calls that behave the same on every backend that supports
|
|
16
|
+
them.
|
|
17
|
+
- Transactions on filesystem and S3: writes are staged as hidden footmark objects and committed,
|
|
18
|
+
discarded, or reverted together.
|
|
19
|
+
- A transaction history at the storage root with age and count retention, revert of the latest
|
|
20
|
+
transaction, and cleanup of orphaned footmarks.
|
|
21
|
+
- A cross-process writer lock: `flock` on the filesystem, conditional writes on S3.
|
|
22
|
+
- Streamed transfers between storages that return MD5, SHA-1, SHA-256, and SHA-512 digests in one
|
|
23
|
+
pass.
|
|
24
|
+
- S3 canned-ACL preservation across transactional copies and metadata rewrites.
|
|
25
|
+
- Configuration by keyword arguments or `TX_STORE_*` environment variables; no CLI coupling.
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
### Install
|
|
30
|
+
|
|
31
|
+
Released versions are published to [PyPI](https://pypi.org/project/txstore/):
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install txstore
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Settings → [CONFIG.md](https://github.com/vinklat/txstore/blob/v0.1.0/CONFIG.md); behavior → [STORAGE.md](https://github.com/vinklat/txstore/blob/v0.1.0/STORAGE.md).
|
|
38
|
+
|
|
39
|
+
### Stream between backends
|
|
40
|
+
|
|
41
|
+
Every backend is a `Storage` built from a URL, so moving data between technologies is one call.
|
|
42
|
+
Download a web page and save it to disk, streamed in chunks rather than loaded into memory:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from txstore import make_storage
|
|
46
|
+
|
|
47
|
+
web = make_storage("https://docs.python.org/3")
|
|
48
|
+
disk = make_storage("fs:///srv/data", create_path=True)
|
|
49
|
+
|
|
50
|
+
digests = web.transfer("library/io.html", disk, "pages/io.html")
|
|
51
|
+
print(digests)
|
|
52
|
+
# {'MD5Sum': '...', 'SHA1': '...', 'SHA256': '...', 'SHA512': '...', 'Size': '180750'}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Swap either URL for `s3://...` and the same call uploads to or downloads from a bucket. HTTP storage
|
|
56
|
+
is read-only and does not follow redirects; point the base URL at the final location.
|
|
57
|
+
|
|
58
|
+
### Group writes into a transaction
|
|
59
|
+
|
|
60
|
+
Writes to the filesystem or S3 can be grouped into a transaction:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from txstore import HistoryRetention, TransactionHistory, make_storage, maybe_transaction
|
|
64
|
+
|
|
65
|
+
storage = make_storage("fs:///srv/data", create_path=True)
|
|
66
|
+
retention = HistoryRetention.from_env()
|
|
67
|
+
|
|
68
|
+
# Write a data file and the manifest that points to it: both are applied together on commit.
|
|
69
|
+
with maybe_transaction(storage, retention):
|
|
70
|
+
storage.put("reports/october.csv", b"day,orders\n1,42\n2,57\n")
|
|
71
|
+
storage.put("manifest.json", b'{"latest": "reports/october.csv"}')
|
|
72
|
+
|
|
73
|
+
# A failure inside the block discards every staged write.
|
|
74
|
+
try:
|
|
75
|
+
with maybe_transaction(storage, retention):
|
|
76
|
+
storage.put("reports/november.csv", b"day,orders\n1,???\n")
|
|
77
|
+
raise ValueError("validation failed")
|
|
78
|
+
except ValueError:
|
|
79
|
+
assert not storage.exists("reports/november.csv")
|
|
80
|
+
|
|
81
|
+
# Changed your mind later? Revert the last transaction as one unit.
|
|
82
|
+
history = TransactionHistory(storage)
|
|
83
|
+
print(
|
|
84
|
+
[(t.id, t.timestamp.to_json()) for t in history.load()]
|
|
85
|
+
) # [(1, '2026-10-23T19:08:36.024119Z')]
|
|
86
|
+
history.revert_last()
|
|
87
|
+
assert not storage.exists("manifest.json")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Connect to S3
|
|
91
|
+
|
|
92
|
+
On S3, pass the endpoint and bucket in the URL and the credentials as arguments or variables:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
export TX_STORE_S3_ACCESS=... TX_STORE_S3_SECRET=...
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
storage = make_storage("s3://s3.example.com/bucket/prefix", s3_default_acl="private")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Documentation
|
|
103
|
+
|
|
104
|
+
| Location | Audience | Authority |
|
|
105
|
+
| -- | -- | -- |
|
|
106
|
+
| `/` root contract documents | Contributors and AI agents | Canonical |
|
|
107
|
+
| [docs/guides/](https://github.com/vinklat/txstore/blob/v0.1.0/docs/guides/README.md) | Library users | Non-canonical guides and examples |
|
|
108
|
+
| [docs/development/](https://github.com/vinklat/txstore/blob/v0.1.0/docs/development/README.md) | Contributors | Non-canonical code maps, flow diagrams, explanations |
|
|
109
|
+
| [docs/reference/](https://github.com/vinklat/txstore/blob/v0.1.0/docs/reference/README.md) | Contributors and tooling | Non-canonical reference artifacts |
|
|
110
|
+
|
|
111
|
+
## Canonical ownership
|
|
112
|
+
|
|
113
|
+
Domain owners and agent navigation → [AGENTS.md](https://github.com/vinklat/txstore/blob/v0.1.0/AGENTS.md#canonical-sources).
|
|
114
|
+
|
|
115
|
+
## Technology
|
|
116
|
+
|
|
117
|
+
Python 3.13+, boto3, requests, pyxattr; pytest, moto, Ruff, mypy, and uv for development.
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT → [LICENSE](https://github.com/vinklat/txstore/blob/v0.1.0/LICENSE).
|
txstore-0.1.0/ROADMAP.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# txstore Roadmap
|
|
2
|
+
|
|
3
|
+
## Phases
|
|
4
|
+
|
|
5
|
+
| Phase | Status | Feature | Notes |
|
|
6
|
+
| -- | -- | -- | -- |
|
|
7
|
+
| 0.1.0 | ✅ | Initial public release | fs, S3, HTTP, and null backends; transactions, history, and retention; protobuf-free footmark codec; configuration by arguments or `TX_STORE_*` variables; Debian testing floors |
|
|
8
|
+
| 0.2.0 | ⏳ | Debian package for forky | `python3-txstore` built with `dh-python` and `pybuild-plugin-pyproject`, autopkgtest running pytest, DEP-14 `debian/latest` branch, `debian/watch` on GitHub `vX.Y.Z` tags, `uversionmangle` mapping a PEP 440 suffix to `~` (`0.2.0rc1` → `0.2.0~rc1`) so a pre-release sorts before its final; ITP, sponsor or Debian Python Team, upload to unstable, migrate to testing before the forky freeze |
|
|
9
|
+
| 1.0.0 | ⏳ | Stable API | Freeze the façade before the forky freeze so forky ships an API maintained for the whole release |
|
|
10
|
+
|
|
11
|
+
## Ideas
|
|
12
|
+
|
|
13
|
+
Unscheduled improvements, each waiting on a trigger. Promote a row to a phase when its trigger fires
|
|
14
|
+
or the work is planned; drop it when it no longer applies.
|
|
15
|
+
|
|
16
|
+
| Idea | Revisit when | Area |
|
|
17
|
+
| -- | -- | -- |
|
|
18
|
+
| Generated API reference in `docs/reference/` | The façade stabilizes (1.0.0) | [STRUCTURE.md](STRUCTURE.md#layout) |
|
|
19
|
+
| GCS and Azure Blob backends | A user needs transactions on another object store | [STORAGE.md](STORAGE.md#backends) |
|
|
20
|
+
| Async API; the per-thread writer-lock claim in `history_lock` then becomes per task (a `ContextVar` of a `frozenset`) | A caller needs concurrent transfers without threads | [STORAGE.md](STORAGE.md#storage-abi) |
|
|
21
|
+
| Size cap or total deadline on `get` | A caller reads objects from an untrusted mirror: `get` holds the whole decoded body in memory on every backend, so a hostile HTTP origin can send an endless or gzip-bombed body; large reads use `get_stream` or `transfer` | [STORAGE.md](STORAGE.md#storage-abi) |
|
|
22
|
+
| Renewable S3 lock | Critical sections approach the S3 lock TTL | [STORAGE.md](STORAGE.md#locking) |
|
|
23
|
+
| Transactional writes as one manifest object | Partial commits on unreachable backends cost real data | [STORAGE.md](STORAGE.md#failure-semantics) |
|
|
24
|
+
| Split `Storage` into readable, writable, and transactional interfaces | Before freezing the façade (1.0.0): the read-only and write-only mixins exist mainly to stub half of a 17-method ABC, and `TransactionNotSupportedMixin` only stubs transaction names on non-transactional backends; the split also adds the public transactional type that `is_transactional` narrows to, leaving `FootmarkMixin` an implementation mixin | [STORAGE.md](STORAGE.md#storage-abi) |
|
|
25
|
+
| Inject `S3Client` into `StorageS3`; no network I/O in the constructor | A caller needs a fake client or lazy bucket creation; `create_bucket` runs in `__init__` today | [STORAGE.md](STORAGE.md#construction) |
|
|
26
|
+
| Bind the bucket inside `S3Client` | The `S3Client` surface is next refactored: every call passes the same bucket, and `_LockObject` and `is_public` carry it too; `_LockObject` and its eight helpers then become one `_S3Lock` class | [STRUCTURE.md](STRUCTURE.md#layout) |
|
|
27
|
+
| Move the canned-ACL choice sets, parsers, and public-ACL set beside the `S3Client` literals, behind an `AclPolicy` holding the default, bucket, reserved, and verify-public settings | The row above lands: ACL rules span `_s3_client`, `storage_s3`, `_s3_acl`, and `factory`, the four settings always travel together, and `acl: str \| None` means an octal mode on `fs` and a canned ACL on `s3` | [STORAGE.md](STORAGE.md#s3-acl-rules) |
|
|
28
|
+
| Public commit hook: `maybe_transaction` calls `transaction_commit()`, which keeps its `bool` result, and `_commit_listed` goes away | A subclass needs a commit hook, or before freezing the façade (1.0.0): today the hook is the private `_commit_listed`. Lands with or after the row below, which retires the `hidden` listing `_commit_listed` hands the prune, so that listing never becomes public; a result tuple would also be truthy whatever `changed` is | [STORAGE.md](STORAGE.md#backend-primitives) |
|
|
29
|
+
| Commit and discard from the transaction's own footmarks, not a full listing; auto-prune lists only when a record expires | Roots reach ~100k keys: every recorded write lists every key under the root while holding `history_lock`; this also retires the commit listing the prune reuses | [STORAGE.md](STORAGE.md#backend-primitives) |
|
|
30
|
+
| Footmarks under a dedicated namespace prefix, so commit, revert, prune, and sweeps list only footmarks | The row above is not enough: a footmark layout change, cheapest before 1.0.0 | [STORAGE.md](STORAGE.md#footmarks) |
|
|
31
|
+
| Mixin-owned public read, write, delete, and metadata pipeline; backends supply primitives only | With the interface split (1.0.0): backends still sequence path validation, overlay call, ACL sidecar, and metrics by hand; `StorageFS` calls `_validate_path` in every verb, `_obj_full` does not confine, three resolve-under-root checks differ (`validate_path`, `_obj_check_reserved`, `_obj_checked_footmark`), and `url.join_path` keeps an unused `no_root=True` branch | [STORAGE.md](STORAGE.md#backend-primitives) |
|
|
32
|
+
| Reclaim as pure classifiers plus one path delete, with a separate prune-plan type | With the interface split (1.0.0): each `find_*` / forget pair re-classifies the paths it is handed, and `PruneResult` is both the plan and the outcome; `_prune_locked` / `_apply_prune` then move from `TransactionHistory` onto the plan type | [STORAGE.md](STORAGE.md#lifecycle) |
|
|
33
|
+
| `TransactionHistory` takes a typed transactional port instead of any `Storage` | With the interface split (1.0.0): every history entry point (`history_lock`, `maybe_transaction`, and `TransactionHistory.load` / `save` / `revert_last` / `pop_last` / `prune`) re-checks `is_transactional`, `append` / `save` rely on the caller holding `history_lock`, and history reaches the private `_commit_listed`, `_transaction_abandon`, `_obj_lock`, `_obj_get_reserved`, and `_obj_put_reserved`; a raw `transaction_begin` auto-increments the instance id, which ignores history, while `maybe_transaction` takes `next_id` | [STORAGE.md](STORAGE.md#operations) |
|
|
34
|
+
| Per-transaction object attributes owned by `FootmarkMixin` | The pipeline above lands: `StorageS3` keeps the ACL sidecar and three `in_transaction()` branches itself | [STORAGE.md](STORAGE.md#s3-acl-rules) |
|
|
35
|
+
| Transaction overlay as an object `transaction_begin` installs and commit, discard, or revert remove, over a typed primitives port | The rows above land (1.0.0): seven overlay helpers branch on `in_transaction()`, `transaction_begin` resets the staged-footmark ledger by hand, and `FootmarkMixin` holds the lifecycle, overlay, and primitives in one class | [STORAGE.md](STORAGE.md#backend-primitives) |
|
|
36
|
+
| The overlay records which of its own footmarks exist, so a staged write probes only the original | S3 syncs of thousands of puts: a staged put of a new key costs three HEAD requests (BACKUP, original, DELETE marker) under `history_lock` | [STORAGE.md](STORAGE.md#footmarks) |
|
|
37
|
+
| A parsed storage-URL value type that renders redacted in `str`, `repr`, and messages, and owns the per-scheme host and userinfo rules | Before freezing the façade (1.0.0), a URL secret leaking at a new site, a third URL backend, or a new authority check outside `url.py`: `parse_storage_url` returns a public tuple, so redaction is applied per call site, `make_storage` and both URL backends re-parse the authority from strings, and each backend constructor repeats the query, host, and userinfo-stripping steps | [VALIDATION.md](VALIDATION.md#storage-urls) |
|
|
38
|
+
| A `Footmarks` object on `TransactionModel` with `path_for(operation, path)` and `operation_of(token)`, still a `Sequence[str]` | With the overlay object above: two sites index or enumerate the token tuple by `ObjectOperation` value | [STORAGE.md](STORAGE.md#footmarks) |
|
|
39
|
+
| A pooled `requests.Session` per `StorageHTTP` instance, closed through a backend `close()` lifecycle | A caller syncs many small objects from an https mirror: every request opens a new connection and TLS handshake, since `requests.request` builds a throwaway session; netrc stays read per request, so an edit takes effect at once | [STORAGE.md](STORAGE.md#backends) |
|
|
40
|
+
| Lazy probe that the S3 endpoint enforces conditional writes | A user reports lost writes on a non-AWS endpoint; one probe on first lock, never in the constructor | [STORAGE.md](STORAGE.md#locking) |
|
|
41
|
+
| `StorageFS` without symlink resolution on mutation or read | A shared root with untrusted writers: a directory swapped for a link between `validate_path` and a write, read, or unlink is followed; closing it needs one root object that opens, unlinks, renames, and stats by relative path through `openat`-style descriptors, replacing per-call-site checks | [VALIDATION.md](VALIDATION.md#filename-safety) |
|
|
42
|
+
| `StorageFS` refuses a hard-linked object, footmark, or `.transactions.lock` | A shared root with untrusted writers where `fs.protected_hardlinks` is off or absent (macOS, the BSDs): a hard link to a file outside the root is read as an object and copied by commit and revert, and a linked `.transactions.lock` puts the history `flock` on the outside file, a lock denial of service but no write; refusing `st_nlink > 1` would also refuse roots backed up with hard-link snapshots | [VALIDATION.md](VALIDATION.md#filename-safety) |
|
|
43
|
+
| `StorageFS` commit pre-flight that refuses a staged file whose path became a directory, and staged `mkdir` / `rmdir` | A caller stages a file at `d` and then `d/child` or `mkdir("d")` in one transaction: commit fails part-way in set order, and `mkdir` / `rmdir` act immediately | [STORAGE.md](STORAGE.md#failure-semantics) |
|
|
44
|
+
| Reclaim `StorageFS` write temp files left by a crash | Leftovers accumulate; they are hidden from `list()` but never deleted | [STORAGE.md](STORAGE.md#backend-primitives) |
|
|
45
|
+
| Fixed `0600` mode for `StorageFS` history and lock files | A root is shared with users who must not read the audit log; today the umask applies as for objects | [VALIDATION.md](VALIDATION.md#filename-safety) |
|
|
46
|
+
| Maintenance CLI for transactional roots: retention prune, orphan check and sweep, temp-file cleanup, history inspection | Operators run these from ad-hoc scripts; the CLI needs its own entry point and a layout decision | [STORAGE.md](STORAGE.md#retention) |
|
|
47
|
+
|
|
48
|
+
## Known deviations
|
|
49
|
+
|
|
50
|
+
Implementation that does not yet meet a root contract. Contracts link here instead of carrying
|
|
51
|
+
TODOs; remove a row when the code meets the rule.
|
|
52
|
+
|
|
53
|
+
| Contract | Deviation | Source |
|
|
54
|
+
| -- | -- | -- |
|
|
55
|
+
| [STORAGE.md](STORAGE.md#recording-rules) | History append after commit, `save` after revert, and a prune's forget-then-rewrite are not atomic with the footmark lifecycle; a crash between them leaves committed objects without a record, or a record whose footmarks are gone. | `txstore/history.py` `maybe_transaction`, `revert_last`, `prune` |
|
|
56
|
+
| [STORAGE.md](STORAGE.md#lifecycle) | Revert infers a created object from a PUT footmark without its BACKUP, so a BACKUP removed outside txstore (by hand, an S3 lifecycle rule, a peer's orphan sweep during a raw transaction) makes revert delete the original; an explicit created marker in the footmark set would close it. | `txstore/transaction.py` `transaction_revert` |
|
|
57
|
+
| [STORAGE.md](STORAGE.md#lifecycle) | Off `fs`, commit checks a staged BACKUP footmark only against its listing, so one removed outside txstore after the listing goes unnoticed and commit overwrites the only other copy of its original. | `txstore/transaction.py` `_commit_listed`, `_obj_checked_footmark` |
|
|
58
|
+
| [STORAGE.md](STORAGE.md#lifecycle) | Commit takes a staged footmark the listing lacks for an alias once `_obj_exists` finds it, so on `fs` one replaced outside txstore by a directory of its name is skipped instead of refused. | `txstore/transaction.py` `_commit_listed` |
|
|
59
|
+
| [STORAGE.md](STORAGE.md#failure-semantics) | Transaction atomicity is best-effort when the backend fails mid-commit or mid-discard; remote state may be left partially updated. | `txstore/transaction.py` `FootmarkMixin` |
|
|
60
|
+
| [STORAGE.md](STORAGE.md#failure-semantics) | `maybe_transaction` reads the history before beginning, so a malformed `.transactions.json` blocks every transactional write; nothing bypasses history recording. | `txstore/history.py` `maybe_transaction` |
|
|
61
|
+
| [STORAGE.md](STORAGE.md#s3-acl-rules) | A copy without a sidecar entry carries only a group canned ACL; `private`, `bucket-owner-*`, or `aws-exec-read` on a key not written in the transaction is reset by a revert, which always copies outside a transaction, or by any other copy outside a transaction. | `txstore/storage_s3.py` `_obj_copy` |
|
|
62
|
+
| [STORAGE.md](STORAGE.md#locking) | `history_lock` spans the whole `maybe_transaction` body, including bulk transfers, and the whole `prune` run; a peer writer that waits past the S3 acquire timeout fails instead of queueing. | `txstore/history.py` `maybe_transaction`, `prune` |
|
|
63
|
+
| [STORAGE.md](STORAGE.md#locking) | Expiry is judged by the breaker's clock against the holder's stamp, so a clock skew approaching the 12 h TTL breaks a live lock early or keeps an expired one. | `txstore/_s3_lock.py` `_body_is_breakable` |
|
|
64
|
+
| [STORAGE.md](STORAGE.md#locking) | The S3 lock stamps `expires_at` once at acquire and never renews it, so a critical section outliving the TTL can be taken over while its holder still runs; a crashed holder must be cleared by deleting `.transactions.lock`. | `txstore/_s3_lock.py` `s3_lock` |
|
|
65
|
+
| [STORAGE.md](STORAGE.md#locking) | A lock write that commits while every botocore attempt fails in transport aborts the acquire with the body left inside its TTL; peers wait for it to expire. | `txstore/_s3_lock.py` `_try_create`, `_try_take_over` |
|
|
66
|
+
| [EXCEPTIONS.md](EXCEPTIONS.md#rules) | `NotImplementedError` from a capability stub is not a `StorageException`, so a caller handling only `StorageException` does not catch it. | `txstore/_capabilities.py` |
|