gpdatacached 0.1.0__tar.gz → 0.2.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.
Files changed (57) hide show
  1. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/.gitignore +4 -0
  2. gpdatacached-0.2.0/CHANGELOG.md +116 -0
  3. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/PKG-INFO +12 -11
  4. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/README.md +11 -10
  5. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/README.zh-CN.md +11 -10
  6. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/docs/en/api.md +189 -12
  7. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/docs/en/extensibility.md +17 -10
  8. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/docs/en/index.md +2 -1
  9. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/docs/en/lifecycle.md +34 -23
  10. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/docs/en/locking.md +11 -11
  11. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/docs/en/overview.md +22 -4
  12. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/docs/en/pandas-support.md +16 -4
  13. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/docs/en/performance-guide.md +20 -16
  14. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/docs/en/pydantic-support.md +15 -3
  15. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/docs/zh/api.md +147 -12
  16. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/docs/zh/extensibility.md +17 -10
  17. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/docs/zh/index.md +2 -1
  18. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/docs/zh/lifecycle.md +35 -24
  19. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/docs/zh/locking.md +11 -11
  20. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/docs/zh/overview.md +21 -3
  21. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/docs/zh/pandas-support.md +16 -4
  22. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/docs/zh/performance-guide.md +20 -16
  23. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/docs/zh/pydantic-support.md +15 -3
  24. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/pyproject.toml +1 -0
  25. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/__init__.py +27 -11
  26. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/builtins/dict_object.py +23 -19
  27. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/builtins/list_object.py +18 -13
  28. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/builtins/set_object.py +9 -0
  29. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/config.py +25 -0
  30. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/container.py +29 -2
  31. gpdatacached-0.2.0/src/gpdatacached/data_cache.py +214 -0
  32. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/domain.py +40 -4
  33. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/exceptions.py +18 -0
  34. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/extensions/pandas_support.py +60 -25
  35. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/extensions/pydantic_support.py +27 -4
  36. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/namespace.py +16 -7
  37. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/object.py +37 -10
  38. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/types.py +40 -5
  39. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/LICENSE +0 -0
  40. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/builtins/__init__.py +0 -0
  41. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/builtins/bool_object.py +0 -0
  42. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/builtins/datetime_object.py +0 -0
  43. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/builtins/float_object.py +0 -0
  44. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/builtins/int_object.py +0 -0
  45. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/builtins/none_object.py +0 -0
  46. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/builtins/registry.py +0 -0
  47. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/builtins/str_object.py +0 -0
  48. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/builtins/tuple_object.py +0 -0
  49. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/codec.py +0 -0
  50. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/extensions/__init__.py +0 -0
  51. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/gc.py +0 -0
  52. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/keys.py +0 -0
  53. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/lifecycle.py +0 -0
  54. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/locking.py +0 -0
  55. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/meta.py +0 -0
  56. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/refset.py +0 -0
  57. {gpdatacached-0.1.0 → gpdatacached-0.2.0}/src/gpdatacached/scalar.py +0 -0
@@ -177,6 +177,9 @@ CLAUDE.md
177
177
  AGENTS.md
178
178
  .opencode/
179
179
 
180
+ # ZCode
181
+ .zcode/
182
+
180
183
  # Github Copilot
181
184
  .github/copilot-instructions.md
182
185
 
@@ -195,3 +198,4 @@ progress.md
195
198
 
196
199
  # user ignore
197
200
  tests/mocks/log/
201
+ /.superpowers/
@@ -0,0 +1,116 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ GitHub Releases (with download artifacts) are published at
9
+ <https://github.com/LinnetCodes/gpdatacached/releases>.
10
+
11
+ ## [0.2.0] — 2026-07-26
12
+
13
+ ### Added
14
+
15
+ - **`GPDataCache` facade** — a config-driven entry point that discovers
16
+ `GPDCDomain` instances from a `gpconfig` `GPConfigFolder`, lazily constructs
17
+ them on first access, and resolves GPDC URIs to live `GPDCDataObject`
18
+ instances. Includes `__contains__` (config-snapshot membership, usable after
19
+ `close()`), `close()` (closes all cached domains), and the URI accessors below.
20
+ - **GPDC URI access** — `GPDataCache.get_object_by_uri(uri)` and
21
+ `has_uri(uri)`, with multi-segment path traversal into nested containers
22
+ (e.g. `gpdc://domain/ns/order_obj/items/0`). Terminals on a scalar field
23
+ raise `ObjectNotFoundByURIError`. The utilities `make_uri` /
24
+ `uri_to_redis_key` / `validate_object_name` are part of the public surface.
25
+ - **`expire_now()`** on `GPDCDataObject` — an explicit, mode-independent
26
+ "delete now" entry point (full teardown incl. anonymous subtrees; raises
27
+ `CanonicalObjectReferencedError` if still referenced). Use this instead of
28
+ `expire_in(0)` / `expire_at(past)` when you want unambiguous immediate
29
+ deletion.
30
+ - **Public exception bases** — `GPDCError` (root of the whole hierarchy) and
31
+ `GPDCLockError` (base for lock-acquisition/ownership errors) are now
32
+ exported, so callers can catch by category: `except GPDCError` catches every
33
+ library error; `except GPDCLockError` catches every lock failure.
34
+ - **`ObjectNotFoundByURIError`** — raised by URI resolution on any
35
+ unresolvable URI (missing domain/namespace/object, broken traversal,
36
+ scalar terminal, malformed syntax).
37
+ - **Pydantic container-mode field validation** —
38
+ `BaseModelContainerObject.__setitem__` now validates the value against the
39
+ field's declared type *before* any Redis write (single-field via pydantic's
40
+ `TypeAdapter`); a type mismatch raises `pydantic.ValidationError` at the
41
+ call site and leaves the object untouched. Cross-field validators still
42
+ require the scalar codec.
43
+ - gpconfig integration: `GPDCDomain` / `GPDCDomainConfig` are registered with
44
+ `gpconfig`'s object factory at import time, so a domain can be constructed
45
+ directly from a config file via `folder.get_object(name)`.
46
+
47
+ ### Changed
48
+
49
+ - **BREAKING:** `GPDCDomain.__init__` signature changed from
50
+ `(name, config)` to `(config, name=None)`, conforming to gpconfig's
51
+ `cls(config)` configurable convention. The old call order raises
52
+ `TypeError` with migration guidance before any Redis connection is made.
53
+ `config.name` (the filename stem when loaded from a file) is preferred as
54
+ the domain name; the explicit `name` argument is the fallback.
55
+ - `expire_in(seconds)` now requires `seconds > 0` (raises `ValueError`
56
+ otherwise). Use `expire_now()` for immediate deletion.
57
+ - `GC interval` validation: `gc_interval_seconds` must now be `>= 30`
58
+ (default `300` unchanged). Smaller values busy-looped the background GC
59
+ thread.
60
+
61
+ ### Fixed
62
+
63
+ - **Pandas non-unit `RangeIndex`** — `DataFrameObject.__len__` and
64
+ `SeriesObject`/`DataFrameObject` `extend` now honour the stored
65
+ `range_step` instead of assuming `step == 1`. Extending a non-unit-step
66
+ DataFrame now stays in the compact range representation (previously
67
+ promoted to labelled levels).
68
+ - **`SeriesObject.name` identity** — `.name` now returns the namespace
69
+ binding (inherited from `GPDCDataObject`, matching `.canonical_name` /
70
+ `.uri`); the pandas label is exposed as `.series_name`. Restores the
71
+ base-class identity contract that `SeriesObject` had been overriding.
72
+ - **Failed multi-element encodes** — `encode_session` now also reverses
73
+ ref-count increments it made on pre-existing containers, and a failed
74
+ first-time store cleans up the just-written binding `obj:`/`raw:`/`ref:`
75
+ keys, so a failed encode no longer leaves phantom referrers or orphan
76
+ metadata.
77
+ - **Stale-handle mutations** — every container mutator now runs a
78
+ `_check_alive()` liveness guard *before* its first Redis write, so a
79
+ handle whose object has been deleted/expired raises `ObjectGoneError`
80
+ without first corrupting the `raw:` key.
81
+ - **Sliding-TTL performance characterization** — sliding refresh batches the
82
+ TTL re-apply into one pipeline per tree level (the load-bearing cost is the
83
+ per-child `HGETALL`), corrected in the Performance Guide.
84
+
85
+ ### Documentation
86
+
87
+ - New MkDocs site (Material theme) with full EN + Simplified-Chinese (ZH)
88
+ documentation: overview, API reference, lifecycle, locking, performance
89
+ guide, extensibility, pydantic/pandas support.
90
+ - Durable explicit-anchor scheme (`{ #id }` attributes) + a `mkdocs build
91
+ --strict` validation gate, so editing a heading never silently breaks an
92
+ in-page cross-reference.
93
+
94
+ [0.2.0]: https://github.com/LinnetCodes/gpdatacached/releases/tag/version-0.2.0
95
+
96
+ ## [0.1.0] — 2026-07-04
97
+
98
+ Initial public release.
99
+
100
+ Core typed cross-process data sharing and caching over Redis:
101
+
102
+ - `GPDCDomain` / `GPDCNamespace` / `GPDCDataObject` three-tier model.
103
+ - Typed scalars (int/float/str/bool/datetime/date/time/none/tuple) and live
104
+ containers (list/dict/set) with reference semantics — mutate in place, no
105
+ read-modify-write.
106
+ - Per-object lifecycle: `permanent` / `ttl` / `sliding-window` cache modes
107
+ with cascade propagation.
108
+ - Anonymous nested objects with reference counting and cascade delete.
109
+ - Opt-in distributed locking (`obj.lock()` / `try_lock()`) via
110
+ `SET NX EX` + `BLPOP` with Lua-atomic release.
111
+ - Background GC for orphaned/expired keys.
112
+ - Extensibility: `GPDCTypeRegistry`, codec/object ABCs, pandas
113
+ (`Series`/`DataFrame`) and pydantic support extensions.
114
+ - Strict config validation; `reset_domain_config` utility.
115
+
116
+ [0.1.0]: https://github.com/LinnetCodes/gpdatacached/releases/tag/version-0.1.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gpdatacached
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Redis-based cross-process data sharing and caching library
5
5
  Project-URL: Homepage, https://github.com/linnetcodes/gpdatacached
6
6
  Project-URL: Documentation, https://linnetcodes.github.io/gpdatacached
@@ -59,6 +59,7 @@ Sharing mutable state across processes is painful. `multiprocessing.Manager` is
59
59
  - **Lifecycle control** — per-object `permanent` / `ttl` / `sliding-window` cache modes, with cascade propagation to owned subtrees.
60
60
  - **Background GC** — reclaims expired and orphaned anonymous objects.
61
61
  - **Opt-in distributed locking** — serialize writes across processes with `obj.lock()`.
62
+ - **URI-based access** — manage multiple domains from config and resolve `gpdc://{domain}/{namespace}/{name}` URIs to data objects via `GPDataCache`, including traversal into anonymous nested containers.
62
63
  - **Optional extensions** — cache [pydantic](https://docs.pydantic.dev/) `BaseModel` records and [pandas](https://pandas.pydata.org/) `Series` / `DataFrame` (see [Extensions](#extensions)).
63
64
 
64
65
  ---
@@ -89,7 +90,7 @@ config = GPDCDomainConfig(
89
90
  redis_port=6379,
90
91
  redis_password="secret",
91
92
  )
92
- domain = GPDCDomain("mydomain", config)
93
+ domain = GPDCDomain(config, name="mydomain")
93
94
  ns = domain.ensure_namespace("analytics")
94
95
 
95
96
  # Scalars read back as native Python values.
@@ -129,21 +130,21 @@ GPDCDomain ── GPDCNamespace ── GPDCDataObject (scalar | container)
129
130
 
130
131
  ## Documentation
131
132
 
132
- 📖 **[Full documentation index](./docs/index.md)** — the entry point to all guides and references below.
133
+ 📖 **[Full documentation index](./docs/en/index.md)** — the entry point to all guides and references below.
133
134
 
134
- - **[Overview](./docs/overview.md)** — design purpose, philosophy, and end-to-end architecture (start here to understand the library as a whole).
135
- - **[API Reference](./docs/api.md)** — complete public API reference.
136
- - **[Lifecycle Management](./docs/lifecycle.md)** — how object creation, expiry, refresh, and deletion work, including anonymous and reference objects (read this to set correct expectations).
137
- - **[Extensibility](./docs/extensibility.md)** — the type system explained, with worked examples for adding your own scalar and container types (codecs, object classes, registration).
138
- - **[Performance Guide](./docs/performance-guide.md)** — usage patterns that degrade performance (sliding TTL overuse, deep nesting, unnecessary GC/locking) and their recommended alternatives. Read before putting GPDC on a hot path.
139
- - **[Multi-Process Locking](./docs/locking.md)** — when to lock, performance impact, recommended usage, deadlock avoidance, and GC interaction.
135
+ - **[Overview](./docs/en/overview.md)** — design purpose, philosophy, and end-to-end architecture (start here to understand the library as a whole).
136
+ - **[API Reference](./docs/en/api.md)** — complete public API reference.
137
+ - **[Lifecycle Management](./docs/en/lifecycle.md)** — how object creation, expiry, refresh, and deletion work, including anonymous and reference objects (read this to set correct expectations).
138
+ - **[Extensibility](./docs/en/extensibility.md)** — the type system explained, with worked examples for adding your own scalar and container types (codecs, object classes, registration).
139
+ - **[Performance Guide](./docs/en/performance-guide.md)** — usage patterns that degrade performance (sliding TTL overuse, deep nesting, unnecessary GC/locking) and their recommended alternatives. Read before putting GPDC on a hot path.
140
+ - **[Multi-Process Locking](./docs/en/locking.md)** — when to lock, performance impact, recommended usage, deadlock avoidance, and GC interaction.
140
141
 
141
142
  ## Extensions
142
143
 
143
144
  Optional built-in extensions under `gpdatacached.extensions`:
144
145
 
145
- - **[Pydantic Support](./docs/pydantic-support.md)** — cache pydantic `BaseModel` records. Two storage modes: scalar (atomic JSON) for scalar-only models, container (Redis hash) for models needing field-level access or nested `list`/`dict`/`set` fields. Pydantic is a core dependency; no extra install needed.
146
- - **[Pandas Support](./docs/pandas-support.md)** — cache pandas `Series` / `DataFrame` as live objects. Online selective accessors (`.iloc`, `.loc`, `df[col]`) for one-shot subset reads; `.value` for full restore; `extend` for appends. Requires `pip install "gpdatacached[pandas]"`.
146
+ - **[Pydantic Support](./docs/en/pydantic-support.md)** — cache pydantic `BaseModel` records. Two storage modes: scalar (atomic JSON) for scalar-only models, container (Redis hash) for models needing field-level access or nested `list`/`dict`/`set` fields. Pydantic is a core dependency; no extra install needed.
147
+ - **[Pandas Support](./docs/en/pandas-support.md)** — cache pandas `Series` / `DataFrame` as live objects. Online selective accessors (`.iloc`, `.loc`, `df[col]`) for one-shot subset reads; `.value` for full restore; `extend` for appends. Requires `pip install "gpdatacached[pandas]"`.
147
148
 
148
149
  ---
149
150
 
@@ -22,6 +22,7 @@ Sharing mutable state across processes is painful. `multiprocessing.Manager` is
22
22
  - **Lifecycle control** — per-object `permanent` / `ttl` / `sliding-window` cache modes, with cascade propagation to owned subtrees.
23
23
  - **Background GC** — reclaims expired and orphaned anonymous objects.
24
24
  - **Opt-in distributed locking** — serialize writes across processes with `obj.lock()`.
25
+ - **URI-based access** — manage multiple domains from config and resolve `gpdc://{domain}/{namespace}/{name}` URIs to data objects via `GPDataCache`, including traversal into anonymous nested containers.
25
26
  - **Optional extensions** — cache [pydantic](https://docs.pydantic.dev/) `BaseModel` records and [pandas](https://pandas.pydata.org/) `Series` / `DataFrame` (see [Extensions](#extensions)).
26
27
 
27
28
  ---
@@ -52,7 +53,7 @@ config = GPDCDomainConfig(
52
53
  redis_port=6379,
53
54
  redis_password="secret",
54
55
  )
55
- domain = GPDCDomain("mydomain", config)
56
+ domain = GPDCDomain(config, name="mydomain")
56
57
  ns = domain.ensure_namespace("analytics")
57
58
 
58
59
  # Scalars read back as native Python values.
@@ -92,21 +93,21 @@ GPDCDomain ── GPDCNamespace ── GPDCDataObject (scalar | container)
92
93
 
93
94
  ## Documentation
94
95
 
95
- 📖 **[Full documentation index](./docs/index.md)** — the entry point to all guides and references below.
96
+ 📖 **[Full documentation index](./docs/en/index.md)** — the entry point to all guides and references below.
96
97
 
97
- - **[Overview](./docs/overview.md)** — design purpose, philosophy, and end-to-end architecture (start here to understand the library as a whole).
98
- - **[API Reference](./docs/api.md)** — complete public API reference.
99
- - **[Lifecycle Management](./docs/lifecycle.md)** — how object creation, expiry, refresh, and deletion work, including anonymous and reference objects (read this to set correct expectations).
100
- - **[Extensibility](./docs/extensibility.md)** — the type system explained, with worked examples for adding your own scalar and container types (codecs, object classes, registration).
101
- - **[Performance Guide](./docs/performance-guide.md)** — usage patterns that degrade performance (sliding TTL overuse, deep nesting, unnecessary GC/locking) and their recommended alternatives. Read before putting GPDC on a hot path.
102
- - **[Multi-Process Locking](./docs/locking.md)** — when to lock, performance impact, recommended usage, deadlock avoidance, and GC interaction.
98
+ - **[Overview](./docs/en/overview.md)** — design purpose, philosophy, and end-to-end architecture (start here to understand the library as a whole).
99
+ - **[API Reference](./docs/en/api.md)** — complete public API reference.
100
+ - **[Lifecycle Management](./docs/en/lifecycle.md)** — how object creation, expiry, refresh, and deletion work, including anonymous and reference objects (read this to set correct expectations).
101
+ - **[Extensibility](./docs/en/extensibility.md)** — the type system explained, with worked examples for adding your own scalar and container types (codecs, object classes, registration).
102
+ - **[Performance Guide](./docs/en/performance-guide.md)** — usage patterns that degrade performance (sliding TTL overuse, deep nesting, unnecessary GC/locking) and their recommended alternatives. Read before putting GPDC on a hot path.
103
+ - **[Multi-Process Locking](./docs/en/locking.md)** — when to lock, performance impact, recommended usage, deadlock avoidance, and GC interaction.
103
104
 
104
105
  ## Extensions
105
106
 
106
107
  Optional built-in extensions under `gpdatacached.extensions`:
107
108
 
108
- - **[Pydantic Support](./docs/pydantic-support.md)** — cache pydantic `BaseModel` records. Two storage modes: scalar (atomic JSON) for scalar-only models, container (Redis hash) for models needing field-level access or nested `list`/`dict`/`set` fields. Pydantic is a core dependency; no extra install needed.
109
- - **[Pandas Support](./docs/pandas-support.md)** — cache pandas `Series` / `DataFrame` as live objects. Online selective accessors (`.iloc`, `.loc`, `df[col]`) for one-shot subset reads; `.value` for full restore; `extend` for appends. Requires `pip install "gpdatacached[pandas]"`.
109
+ - **[Pydantic Support](./docs/en/pydantic-support.md)** — cache pydantic `BaseModel` records. Two storage modes: scalar (atomic JSON) for scalar-only models, container (Redis hash) for models needing field-level access or nested `list`/`dict`/`set` fields. Pydantic is a core dependency; no extra install needed.
110
+ - **[Pandas Support](./docs/en/pandas-support.md)** — cache pandas `Series` / `DataFrame` as live objects. Online selective accessors (`.iloc`, `.loc`, `df[col]`) for one-shot subset reads; `.value` for full restore; `extend` for appends. Requires `pip install "gpdatacached[pandas]"`.
110
111
 
111
112
  ---
112
113
 
@@ -22,6 +22,7 @@
22
22
  - **生命周期控制** —— 每个对象可设置 `permanent` / `ttl` / `sliding`(滑动窗口)缓存模式,并将策略级联传播到所拥有的子树。
23
23
  - **后台 GC** —— 自动回收过期或成为孤儿的匿名对象。
24
24
  - **可选的分布式锁** —— 通过 `obj.lock()` 在多进程间串行化写入。
25
+ - **基于 URI 的访问** —— 通过 `GPDataCache` 从配置管理多个 domain,并将 `gpdc://{domain}/{namespace}/{name}` 形式的 URI 解析为数据对象,包括对匿名嵌套容器的遍历。
25
26
  - **可选扩展** —— 可缓存 [pydantic](https://docs.pydantic.dev/) `BaseModel` 记录与 [pandas](https://pandas.pydata.org/) `Series` / `DataFrame`(见[扩展](#扩展))。
26
27
 
27
28
  ---
@@ -52,7 +53,7 @@ config = GPDCDomainConfig(
52
53
  redis_port=6379,
53
54
  redis_password="secret",
54
55
  )
55
- domain = GPDCDomain("mydomain", config)
56
+ domain = GPDCDomain(config, name="mydomain")
56
57
  ns = domain.ensure_namespace("analytics")
57
58
 
58
59
  # 标量读取出来是原生 Python 值。
@@ -92,21 +93,21 @@ GPDCDomain ── GPDCNamespace ── GPDCDataObject (标量 | 容器)
92
93
 
93
94
  ## 文档
94
95
 
95
- 📖 **[完整文档索引](./docs/zh-CN/index.md)** —— 以下所有指南与参考的入口。
96
+ 📖 **[完整文档索引](./docs/zh/index.md)** —— 以下所有指南与参考的入口。
96
97
 
97
- - **[总体概览](./docs/zh-CN/overview.md)** —— 设计目的、设计理念与端到端整体架构(从这里开始,从整体上理解本库)。
98
- - **[API 参考](./docs/zh-CN/api.md)** —— 完整的公共 API 参考。
99
- - **[生命周期管理](./docs/zh-CN/lifecycle.md)** —— 对象的创建、过期、刷新与删除如何运作,含匿名对象与引用对象(阅读本文以建立合理预期)。
100
- - **[可扩展性](./docs/zh-CN/extensibility.md)** —— 类型系统详解,含添加自定义标量与容器类型的完整示例(codec、对象类、注册)。
101
- - **[性能指南](./docs/zh-CN/performance-guide.md)** —— 会拖累性能的用法(滥用 sliding TTL、过深嵌套、不必要地启用 GC/加锁)及其推荐替代。把 GPDC 放上热路径前必读。
102
- - **[多进程加锁](./docs/zh-CN/locking.md)** —— 何时需要加锁、性能影响、推荐用法、避免死锁的顺序以及与 GC 的交互。
98
+ - **[总体概览](./docs/zh/overview.md)** —— 设计目的、设计理念与端到端整体架构(从这里开始,从整体上理解本库)。
99
+ - **[API 参考](./docs/zh/api.md)** —— 完整的公共 API 参考。
100
+ - **[生命周期管理](./docs/zh/lifecycle.md)** —— 对象的创建、过期、刷新与删除如何运作,含匿名对象与引用对象(阅读本文以建立合理预期)。
101
+ - **[可扩展性](./docs/zh/extensibility.md)** —— 类型系统详解,含添加自定义标量与容器类型的完整示例(codec、对象类、注册)。
102
+ - **[性能指南](./docs/zh/performance-guide.md)** —— 会拖累性能的用法(滥用 sliding TTL、过深嵌套、不必要地启用 GC/加锁)及其推荐替代。把 GPDC 放上热路径前必读。
103
+ - **[多进程加锁](./docs/zh/locking.md)** —— 何时需要加锁、性能影响、推荐用法、避免死锁的顺序以及与 GC 的交互。
103
104
 
104
105
  ## 扩展
105
106
 
106
107
  `gpdatacached.extensions` 下的可选内建扩展:
107
108
 
108
- - **[Pydantic 支持](./docs/zh-CN/pydantic-support.md)** —— 缓存 pydantic `BaseModel` 记录。两种存储模式:标量模式(原子 JSON,适用于纯标量模型)和容器模式(Redis hash,适用于需要字段级访问或嵌套 `list`/`dict`/`set` 字段的模型)。pydantic 是核心依赖,无需额外安装。
109
- - **[Pandas 支持](./docs/zh-CN/pandas-support.md)** —— 将 pandas `Series` / `DataFrame` 缓存为活跃对象。在线选择性访问器(`.iloc`、`.loc`、`df[col]`)用于一次性子集读取;`.value` 用于完整还原;`extend` 用于追加。需要 `pip install "gpdatacached[pandas]"`。
109
+ - **[Pydantic 支持](./docs/zh/pydantic-support.md)** —— 缓存 pydantic `BaseModel` 记录。两种存储模式:标量模式(原子 JSON,适用于纯标量模型)和容器模式(Redis hash,适用于需要字段级访问或嵌套 `list`/`dict`/`set` 字段的模型)。pydantic 是核心依赖,无需额外安装。
110
+ - **[Pandas 支持](./docs/zh/pandas-support.md)** —— 将 pandas `Series` / `DataFrame` 缓存为活跃对象。在线选择性访问器(`.iloc`、`.loc`、`df[col]`)用于一次性子集读取;`.value` 用于完整还原;`extend` 用于追加。需要 `pip install "gpdatacached[pandas]"`。
110
111
 
111
112
  ---
112
113
 
@@ -44,7 +44,7 @@ GPDCDomain ──┬── GPDCNamespace ──┬── GPDCDataObject (scalar
44
44
 
45
45
  ---
46
46
 
47
- ## Quick Start
47
+ ## Quick Start { #api-quick-start }
48
48
 
49
49
  ```python
50
50
  import gpdatacached
@@ -57,7 +57,7 @@ config = GPDCDomainConfig(
57
57
  redis_db=0,
58
58
  redis_password="secret",
59
59
  )
60
- domain = GPDCDomain("mydomain", config)
60
+ domain = GPDCDomain(config, name="mydomain")
61
61
 
62
62
  # 2. Get or create a namespace.
63
63
  ns = domain.ensure_namespace("analytics")
@@ -90,12 +90,49 @@ domain.close()
90
90
 
91
91
  ---
92
92
 
93
+ ## Managing Domains via Configuration: `GPDataCache`
94
+
95
+ When your application drives many domains from configuration files managed by
96
+ [`gpconfig`](https://github.com/linnetcodes/gpconfig), `GPDataCache` is the
97
+ higher-level entry point. It holds a `GPConfigFolder`, lazily constructs and
98
+ caches `GPDCDomain` instances from it, and resolves GPDC URIs to data objects.
99
+
100
+ ```python
101
+ from gpconfig import GPConfigManager
102
+ import gpdatacached # registers GPDCDomain with gpconfig
103
+ from gpdatacached import GPDataCache
104
+
105
+ # 1. Build a GPConfigFolder for the "gpdc" subfolder of your config folder.
106
+ mgr = GPConfigManager("myproject", cfg_folder="/path/to/configs")
107
+ folder = mgr.get_config("gpdc") # -> GPConfigFolder; sample layouts in tests/configs/gpdc
108
+
109
+ # 2. GPDataCache manages the domains found in that folder.
110
+ with GPDataCache(folder) as cache:
111
+ "core_domain" in cache # True iff a config exists (no Redis)
112
+ domain = cache.get_domain("core_domain") # lazy construct + cache
113
+
114
+ # 3. Resolve objects by GPDC URI — including anonymous nested objects.
115
+ ns = domain.get_namespace("core_app")
116
+ ns["config"] = {"server": {"host": "localhost", "port": 8080}, "tags": ["a", "b"]}
117
+
118
+ cfg = cache.get_object_by_uri("gpdc://core_domain/core_app/config")
119
+ server = cache.get_object_by_uri("gpdc://core_domain/core_app/config/server")
120
+ cache.has_uri("gpdc://core_domain/core_app/missing") # False
121
+ # close() on exit closes every cached domain (idempotent).
122
+ ```
123
+
124
+ See [`GPDataCache`](#gpdatacache) below for the full API and the [URI resolution
125
+ rules](#get_object_by_uri) (path traversal, scalar terminals, `ListObject`
126
+ index coercion).
127
+
128
+ ---
129
+
93
130
  ## Core Types
94
131
 
95
132
  ### `GPDCDomain`
96
133
 
97
134
  ```python
98
- class GPDCDomain(name: str, config: GPDCDomainConfig)
135
+ class GPDCDomain(config: GPDCDomainConfig, name: str | None = None)
99
136
  ```
100
137
 
101
138
  The configuration root and Redis connection pool. On construction it:
@@ -158,7 +195,7 @@ Configuration for a `GPDCDomain`. All fields are validated; lock fields must be
158
195
  | `default_cache_mode` | `str` | `"permanent"` | One of `"permanent"`, `"ttl"`, `"sliding"`. |
159
196
  | `default_ttl_seconds` | `int` | `0` | Default TTL; must be `> 0` when mode is `"ttl"`/`"sliding"`. |
160
197
  | `gc_enabled` | `bool` | `False` | Start the background GC thread. |
161
- | `gc_interval_seconds` | `int` | `300` | Background GC cycle interval. |
198
+ | `gc_interval_seconds` | `int` | `300` | Background GC cycle interval (must be `≥ 30`; smaller values busy-loop the GC thread and flood Redis). |
162
199
  | `namespaces` | `dict[str, GPDCNamespaceConfig]` | `{}` | Predefined namespaces to create eagerly. |
163
200
  | `default_lock_ttl_seconds` | `int` | `30` | TTL on lock keys (must be `≥ 1`). |
164
201
  | `default_lock_acquire_timeout_seconds` | `int` | `30` | Blocking timeout for `obj.lock()` (must be `≥ 0`). |
@@ -239,13 +276,133 @@ Delete the four lock-config fields (`default_lock_ttl_seconds`, `default_lock_ac
239
276
 
240
277
  ```python
241
278
  reset_domain_config("mydomain", old_config) # must be called BEFORE construction
242
- domain = GPDCDomain("mydomain", new_config) # writes the new lock config
279
+ domain = GPDCDomain(new_config, name="mydomain") # writes the new lock config
243
280
  ```
244
281
 
245
282
  Only the Redis connection fields on `config` are read; lock fields on the passed config are ignored.
246
283
 
247
284
  ---
248
285
 
286
+ ### `GPDataCache`
287
+
288
+ ```python
289
+ class GPDataCache(folder: gpconfig.GPConfigFolder) # context manager
290
+ ```
291
+
292
+ A facade that manages `GPDCDomain` instances discovered in a
293
+ [`gpconfig`](https://github.com/linnetcodes/gpconfig) `GPConfigFolder`, and
294
+ resolves GPDC URIs to [`GPDCDataObject`](#gpdcdataobject) instances. It is the
295
+ recommended entry point when your domains are driven by config files rather
296
+ than constructed ad-hoc in code.
297
+
298
+ **Construction.** Built from a `GPConfigFolder`. On construction it stores the
299
+ folder reference only — **no domain is created**. Domains are constructed
300
+ lazily (via gpconfig's object factory `folder.get_object(name)`) on first
301
+ [`get_domain`](#gpdatacache) access, and cached in an internal dict. The cache
302
+ is a snapshot: config files edited after construction are **not** re-read;
303
+ rebuild the `GPDataCache` to pick up changes.
304
+
305
+ `GPDataCache` does **not** create domains lacking a config file — for ad-hoc
306
+ domains use [`GPDCDomain`](#gpdcdomain) directly.
307
+
308
+ > **Registration side effect.** `import gpdatacached` registers `GPDCDomain`
309
+ > and `GPDCDomainConfig` with gpconfig's object factory (idempotent), which is
310
+ > what makes `folder.get_object(name)` able to construct a `GPDCDomain`. No
311
+ > extra setup is needed.
312
+
313
+ **Dunder protocols**
314
+
315
+ - `name in cache` → `True` iff a config named `name` exists in the folder. This is a pure config-snapshot check: it does **not** touch Redis and does not construct (or require) a `GPDCDomain` instance — the answer depends only on whether the config file is present, not on whether `get_domain(name)` has been called. The domain instance is created lazily on first `get_domain(name)` access.
316
+ - `with cache as c` / `__exit__` → closes every cached domain on exit; `close()` is idempotent.
317
+
318
+ > **`close()` behaviour — the membership exception.** After `cache.close()`, the resource-touching methods (`get_domain`, `get_object_by_uri`, `has_uri`) raise `RuntimeError("GPDataCache is closed")`. The `name in cache` membership check is the **deliberate exception**: because it only reads the config snapshot and touches no resources, it keeps working after `close()`. This is intentional — "does a config for this domain exist?" is meaningful regardless of whether the cache is open.
319
+
320
+ **Methods**
321
+
322
+ | Method | Description |
323
+ |---|---|
324
+ | `get_domain(name)` | Return the cached `GPDCDomain` for `name`, constructing it lazily on first access. Propagates gpconfig's `ConfigNotFoundError` if no config exists for `name`. Raises `RuntimeError` after `close()`. |
325
+ | [`get_object_by_uri(uri)`](#get_object_by_uri) | Resolve a GPDC URI to a `GPDCDataObject`. Raises `RuntimeError` after `close()`. |
326
+ | `has_uri(uri)` | Return `True` iff `get_object_by_uri(uri)` would succeed. Raises `RuntimeError` after `close()`. |
327
+ | `close()` | Close every cached domain. Idempotent. |
328
+
329
+ ---
330
+
331
+ #### `get_object_by_uri`
332
+
333
+ ```python
334
+ def get_object_by_uri(self, uri: str) -> GPDCDataObject
335
+ ```
336
+
337
+ Resolve a GPDC URI to a [`GPDCDataObject`](#gpdcdataobject). The URI must begin
338
+ with `gpdc://` and has the form:
339
+
340
+ ```
341
+ gpdc://{domain}/{namespace}/{name}[/{path_segment}...]
342
+ ```
343
+
344
+ - The first three segments (`domain` / `namespace` / `name`) locate the **root
345
+ object**, fetched via `namespace.get_object(name)` — so the root is always
346
+ returned as a `GPDCDataObject`, **even when it is a scalar** (unlike
347
+ `ns[name]`, which unwraps scalars to native values).
348
+ - Any further segments form a **path** that descends into the object via
349
+ `__getitem__`, one segment at a time. This is how anonymous nested containers
350
+ become reachable: `gpdc://d/ns/obj/key/0` walks `obj["key"][0]`.
351
+
352
+ **Returns objects, never values.** `get_object_by_uri` always returns a
353
+ `GPDCDataObject`. It cannot be used to read a scalar value or an element value
354
+ directly.
355
+
356
+ **Path-segment rules**
357
+
358
+ | Current object | Segment handling |
359
+ |---|---|
360
+ | `ListObject` | The segment must be a **non-negative integer** (a pure-digit string); it is converted to `int` before indexing. Negative indices, decimals, and slices are rejected. |
361
+ | `DictObject` / pydantic container-mode model / other subscriptable container | The segment is used as-is (a string key). |
362
+ | `SetObject`, scalar objects | Have no `__getitem__` — any further path segment raises `ObjectNotFoundByURIError`. |
363
+
364
+ **Scalar terminals are rejected.** Scalars nested inside a container are stored
365
+ as inlined values (not as referenced objects), so descending into a scalar
366
+ element yields a native Python value. Because `get_object_by_uri` returns
367
+ objects only, a path that terminates on a scalar raises `ObjectNotFoundByURIError`.
368
+ For example, given `ns["cfg"] = {"port": 8080}`, `gpdc://d/ns/cfg/port` raises
369
+ (the value `8080` is a scalar, not a data object).
370
+
371
+ **Errors.** Every failure mode raises [`ObjectNotFoundByURIError`](#objectnotfoundbyurierror)
372
+ (carries `.uri` and `.reason`): malformed URI (missing `gpdc://`, fewer than
373
+ three segments, empty segment), unknown domain, missing namespace, missing root
374
+ object, a path segment that fails (`KeyError`/`IndexError`/`TypeError`), and a
375
+ scalar terminal.
376
+
377
+ **Example.** Given `ns["cfg"] = {"server": {"host": "x"}, "tags": ["a","b"]}`:
378
+
379
+ | URI | Result |
380
+ |---|---|
381
+ | `gpdc://d/ns/cfg` | the `DictObject` for `cfg` |
382
+ | `gpdc://d/ns/cfg/server` | the nested `DictObject` for `server` |
383
+ | `gpdc://d/ns/cfg/server/host` | raises — `host` is the scalar `"x"` |
384
+ | `gpdc://d/ns/cfg/tags/0` | raises — `tags[0]` is the scalar `"a"` |
385
+ | `gpdc://d/ns/cfg/missing` | raises — `KeyError` on the path segment |
386
+
387
+ ---
388
+
389
+ ### `ObjectNotFoundByURIError`
390
+
391
+ ```python
392
+ class ObjectNotFoundByURIError(GPDCError)
393
+ ```
394
+
395
+ Raised by [`GPDataCache.get_object_by_uri`](#get_object_by_uri) when a GPDC URI
396
+ cannot be resolved to a `GPDCDataObject`. Covers every failure mode: malformed
397
+ URI syntax, unknown domain (no config), missing namespace, missing root object,
398
+ a path segment that fails, and a scalar terminal. Carries `.uri` (the URI
399
+ string) and `.reason` (a short explanation).
400
+
401
+ `has_uri` swallows only this exception; infrastructure errors (e.g. Redis
402
+ connection failures) propagate.
403
+
404
+ ---
405
+
249
406
  ## Data Objects
250
407
 
251
408
  ### `GPDCDataObject`
@@ -286,12 +443,13 @@ Abstract base class for every GPDC data object. Subclasses are split into [`GPDC
286
443
 
287
444
  | Method | Description |
288
445
  |---|---|
289
- | `expire_in(seconds)` | Set a new TTL. Object must be in `cache_mode="ttl"`. Requires ownership. |
446
+ | `expire_in(seconds)` | Set a new TTL. Object must be in `cache_mode="ttl"`. `seconds` must be `> 0` — to delete now, use `expire_now()`. Requires ownership. |
290
447
  | `expire_at(when)` | Set absolute expiry time. Object must be in `cache_mode="ttl"`. Requires ownership. Deletes the object immediately if `when` is in the past (equivalent to `del ns[name]`: cleans `obj:`/`raw:`/`ref:` and cascade-deletes anonymous subtrees; raises `CanonicalObjectReferencedError` if the owner is still referenced — check `has_referrers()` first). |
448
+ | `expire_now()` | Delete this object immediately. Equivalent to `del ns[canonical_name]`: full teardown (obj:/raw:/ref: + cascade-delete of anonymous subtrees). Works in any `cache_mode` (unlike `expire_in`/`expire_at`, which require `cache_mode="ttl"`). Requires ownership; raises `CanonicalObjectReferencedError` if the owner is still referenced by aliases — check `has_referrers()` first. |
291
449
  | `persist()` | Switch to `cache_mode="permanent"` with `ttl=0`. Requires ownership. |
292
450
  | `update_cache_mode_and_ttl(cache_mode, ttl)` | Replace the full cache policy. Requires ownership. |
293
451
  | `refresh_ttl()` | Re-apply this object's own TTL to its keys and cascade-refresh its owned subtree (used by the sliding-window path). No-op for permanent objects. |
294
- | `take_ownership()` | Promote a reference object to be the canonical owner of its own data. Only valid for named references. |
452
+ | `take_ownership()` | Promote **the referenced object** to own itself (primary use case: a named reference to an anonymous nested object). On a reference to a named (already self-owned) object it is a correct no-op. The call itself must be made via a named reference handle. See [Lifecycle §7](./lifecycle.md#7-the-ownership-model). |
295
453
 
296
454
  **Locking methods** (opt-in, never called automatically by mutations)
297
455
 
@@ -467,7 +625,7 @@ Members may be any hashable scalar (no containers, no empty tuple). Implements t
467
625
 
468
626
  ## Codecs
469
627
 
470
- Codecs convert between Python values and their Redis storage representation. Every type registered with [`GPDCTypeRegistry`](#type-registry) must supply a codec.
628
+ Codecs convert between Python values and their Redis storage representation. Every type registered with [`GPDCTypeRegistry`](#api-type-registry) must supply a codec.
471
629
 
472
630
  > This section is the API summary. For a full walkthrough of writing codecs — scalar and container, with worked examples (`Color`, `Counter`) and the mutation hooks every container must call — see the dedicated [Extensibility](./extensibility.md) document.
473
631
 
@@ -516,7 +674,7 @@ Abstract base for container codecs. Containers store their elements in a dedicat
516
674
 
517
675
  ---
518
676
 
519
- ## Type Registry
677
+ ## Type Registry { #api-type-registry }
520
678
 
521
679
  ### `GPDCTypeRegistry`
522
680
 
@@ -562,7 +720,10 @@ GPDCTypeRegistry.register(
562
720
 
563
721
  ## Key & URI Utilities
564
722
 
565
- Low-level helpers for working with GPDC's key/URI scheme. Most applications never need these directly.
723
+ Low-level helpers for working with GPDC's key/URI scheme. For actually
724
+ *resolving* a URI to a data object, use
725
+ [`GPDataCache.get_object_by_uri`](#get_object_by_uri) — these helpers only build
726
+ or convert URI/key strings and are rarely needed directly.
566
727
 
567
728
  ### `make_uri`
568
729
 
@@ -592,7 +753,23 @@ Validate that `name` contains only letters, digits, `.` or `_` and is not a rese
592
753
 
593
754
  ## Exceptions
594
755
 
595
- All public exceptions descend from a private `GPDCError(Exception)` base. Catch them individually for precise error handling.
756
+ All public exceptions descend from the public `GPDCError(Exception)` root. Use `except GPDCError` to catch every gpdatacached error, or catch them individually for precise handling. The lock-acquisition errors additionally share a public `GPDCLockError` base — `except GPDCLockError` catches every lock-acquisition/ownership failure (currently `LockAcquisitionTimeout`).
757
+
758
+ ### `GPDCError`
759
+
760
+ ```python
761
+ class GPDCError(Exception)
762
+ ```
763
+
764
+ The root of the gpdatacached exception hierarchy. Every exception raised by this library is a subclass of `GPDCError`, so `except GPDCError` is the broad catch-all. Prefer catching the specific subclass when you need to distinguish failure modes.
765
+
766
+ ### `GPDCLockError`
767
+
768
+ ```python
769
+ class GPDCLockError(GPDCError)
770
+ ```
771
+
772
+ Base class for lock-acquisition/ownership errors. `except GPDCLockError` catches every lock-related failure (currently `LockAcquisitionTimeout`; `LockOwnershipError` also descends from it but is swallowed internally by `HeldLock.release()`).
596
773
 
597
774
  ### `ObjectGoneError`
598
775
 
@@ -624,7 +801,7 @@ Raised during encoding when a container references itself (directly or transitiv
624
801
  class LockAcquisitionTimeout(GPDCLockError)
625
802
  ```
626
803
 
627
- Raised when `obj.lock()` cannot acquire the lock within the configured `timeout`. Carries `.lock_key` and `.timeout`. See [Multi-Process Locking](./locking.md).
804
+ Raised when `obj.lock()` cannot acquire the lock within the configured `timeout`. Carries `.lock_key` and `.timeout`. It extends the public `GPDCLockError` base, so `except GPDCLockError` catches it. See [Multi-Process Locking](./locking.md).
628
805
 
629
806
  ### `GPDCLockConfigInconsistentError`
630
807