gpdatacached 0.2.0__tar.gz → 0.3.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 (58) hide show
  1. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/.gitignore +3 -0
  2. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/CHANGELOG.md +88 -0
  3. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/PKG-INFO +6 -6
  4. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/README.md +3 -3
  5. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/README.zh-CN.md +3 -3
  6. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/docs/en/api.md +64 -17
  7. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/docs/en/extensibility.md +3 -2
  8. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/docs/en/lifecycle.md +2 -2
  9. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/docs/en/overview.md +2 -2
  10. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/docs/en/pandas-support.md +3 -3
  11. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/docs/en/performance-guide.md +2 -2
  12. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/docs/en/pydantic-support.md +1 -1
  13. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/docs/zh/api.md +39 -10
  14. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/docs/zh/extensibility.md +3 -2
  15. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/docs/zh/lifecycle.md +3 -3
  16. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/docs/zh/overview.md +2 -2
  17. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/docs/zh/pandas-support.md +5 -5
  18. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/docs/zh/performance-guide.md +2 -2
  19. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/docs/zh/pydantic-support.md +1 -1
  20. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/pyproject.toml +1 -1
  21. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/__init__.py +15 -2
  22. gpdatacached-0.3.0/src/gpdatacached/builtins/decimal_object.py +39 -0
  23. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/builtins/registry.py +11 -0
  24. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/data_cache.py +114 -28
  25. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/domain.py +16 -0
  26. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/extensions/pandas_support.py +1 -1
  27. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/keys.py +30 -1
  28. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/LICENSE +0 -0
  29. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/docs/en/index.md +0 -0
  30. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/docs/en/locking.md +0 -0
  31. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/docs/zh/index.md +0 -0
  32. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/docs/zh/locking.md +0 -0
  33. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/builtins/__init__.py +0 -0
  34. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/builtins/bool_object.py +0 -0
  35. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/builtins/datetime_object.py +0 -0
  36. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/builtins/dict_object.py +0 -0
  37. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/builtins/float_object.py +0 -0
  38. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/builtins/int_object.py +0 -0
  39. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/builtins/list_object.py +0 -0
  40. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/builtins/none_object.py +0 -0
  41. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/builtins/set_object.py +0 -0
  42. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/builtins/str_object.py +0 -0
  43. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/builtins/tuple_object.py +0 -0
  44. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/codec.py +0 -0
  45. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/config.py +0 -0
  46. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/container.py +0 -0
  47. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/exceptions.py +0 -0
  48. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/extensions/__init__.py +0 -0
  49. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/extensions/pydantic_support.py +0 -0
  50. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/gc.py +0 -0
  51. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/lifecycle.py +0 -0
  52. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/locking.py +0 -0
  53. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/meta.py +0 -0
  54. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/namespace.py +0 -0
  55. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/object.py +0 -0
  56. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/refset.py +0 -0
  57. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/scalar.py +0 -0
  58. {gpdatacached-0.2.0 → gpdatacached-0.3.0}/src/gpdatacached/types.py +0 -0
@@ -192,6 +192,9 @@ __marimo__/
192
192
  draft/
193
193
  dev_docs/
194
194
 
195
+ # Agent demo scripts (runnable demos for review/verification workflows)
196
+ demos/
197
+
195
198
  # user progress
196
199
  PROGRESS.md
197
200
  progress.md
@@ -8,6 +8,94 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  GitHub Releases (with download artifacts) are published at
9
9
  <https://github.com/LinnetCodes/gpdatacached/releases>.
10
10
 
11
+ ## [0.3.0] — 2026-10-16
12
+
13
+ ### Added
14
+
15
+ - **Dotted domain names via the `@` filename escape (`GPDataCache`)** —
16
+ domain names may now contain `.` end-to-end through the config-file path.
17
+ gpconfig addresses configs by dot-separated paths, so a `.` in a config
18
+ filename is consumed as a path separator and the file becomes
19
+ unaddressable; the new convention writes each `.` in a domain name as `@`
20
+ in the filename (chosen for its internet-address connotation — the natural
21
+ spelling when a domain mirrors an internet domain):
22
+ `domain_name@com.yaml` configures the domain `"domain_name.com"`, and
23
+ `cache.get_domain("domain_name.com")`, `name in cache`, and
24
+ `gpdc://domain_name.com/{ns}/{obj}[...]` URIs resolve it transparently.
25
+ The mapping is injective because `@` is not a legal character in GPDC
26
+ names. `GPDataCache.get_domain` now validates the requested name against
27
+ the GPDC domain-name rules (`ValueError` otherwise; URI resolution reports
28
+ the same case as `ObjectNotFoundByURIError`, and `has_uri` stays aligned).
29
+ `GPDCDomain`'s name resolution restores `@` → `.` for names derived from
30
+ a config-file stem (before the name-mismatch check), so gpconfig's
31
+ object factory path also constructs correctly-named domains:
32
+ `folder.get_object("domain_name@com")` yields the domain
33
+ `"domain_name.com"`, identical to `GPDataCache.get_domain(
34
+ "domain_name.com")`. An explicit `name=` argument containing `@` is
35
+ still rejected. The escape helpers (`DOMAIN_DOT_ESCAPE`,
36
+ `escape_domain_name`, `unescape_domain_name`) are exported from the
37
+ package root. **No gpconfig-side change is
38
+ needed or wanted** (owner ruling 2026-10-16): gpconfig keeps
39
+ `config.name` as the raw stem, preserving the
40
+ `config.name == addressable path component` invariant; a gpconfig-side
41
+ request (fail-fast validation of config filenames containing `.` at manager
42
+ init, plus a docs note) is recorded in
43
+ `dev_docs/issue-gpconfig-at-escape-in-config-names.md`); that request
44
+ has since landed in gpconfig 0.4.0.
45
+ - **Startup warning for unaddressable config filenames** — `GPDataCache`
46
+ construction logs one warning per config file whose name contains a real
47
+ `.` (listed by `list_configs()` but never loadable by gpconfig's
48
+ dot-notation), suggesting the `@` spelling.
49
+
50
+ ### Fixed
51
+
52
+ - `GPDataCache.__contains__` and `get_domain` no longer disagree for
53
+ config files whose stem contains `.` (previously `in` returned `True`
54
+ while `get_domain` raised `ConfigNotFoundError`; such files are now
55
+ warned about at construction and looked up only through valid names).
56
+
57
+ ## [0.2.1] — 2026-10-12
58
+
59
+ ### Added
60
+
61
+ - **`Decimal` builtin scalar type** — `decimal.Decimal` is auto-registered on
62
+ `import gpdatacached` as `scalar:decimal`; `DecimalObject` and `DecimalCodec`
63
+ are exported from the package root. Encoded losslessly as
64
+ `decimal:{str(value)}`: coefficient/exponent (significance) are preserved
65
+ exactly, extreme exponents use scientific notation, and non-finite values
66
+ (`NaN`, `sNaN`, `±Infinity`) plus `-0` roundtrip as-is — the same policy as
67
+ `float`'s `nan`/`inf`. Works everywhere a registered scalar does: standalone
68
+ objects, container elements, dict keys, set members, tuple elements, and
69
+ pandas object-dtype columns/index labels. Malformed payloads raise
70
+ `ValueError` on decode. Note the documented cross-type-equality caveat:
71
+ natively-equal values of different numeric types (`1` vs `Decimal(1)`) are
72
+ distinct GPDC dict keys/set members, as `1` vs `1.0` already were.
73
+
74
+ ### Changed
75
+
76
+ - Require `gpconfig>=0.4.0` (was `>=0.3.0`, a version never published).
77
+ gpconfig 0.4.0 ships fail-fast validation of config filenames containing
78
+ `.` plus the `@`-convention documentation; gpdatacached's own
79
+ construction-time warning remains as defense for it.
80
+
81
+ ### Documentation
82
+
83
+ - Full docs-vs-code synchronization pass across `docs/` (English + zh-CN
84
+ mirrors) and both READMEs: README now lists `Decimal` among the supported
85
+ scalar types and its Quick Start lifecycle example is runnable as written
86
+ (`update_cache_mode_and_ttl` instead of mode-restricted `expire_in`);
87
+ `api.md` documents `remaining_ttl()` and the exact `ObjectGoneError`
88
+ surface; the extensibility codec diagram and the overview layer-dependency
89
+ edge were corrected to match the code (`GPDCContainerCodec` is a sibling
90
+ root of `GPDCCodec`, and the extensions depend on the type-system layer,
91
+ not the builtins); performance-guide RTT attributions were updated; the
92
+ zh-CN mirrors gained the translated §7 ownership heading (with a durable
93
+ explicit anchor) and several translation fixes. `keys.py`'s `make_ref_key`
94
+ docstring and the pandas `UnsupportedElementTypeError` guidance message
95
+ were corrected to match reality (hash, not set; `decimal` included). The
96
+ README status badge now reads Beta, aligned with the packaging
97
+ classifier `Development Status :: 4 - Beta`.
98
+
11
99
  ## [0.2.0] — 2026-07-26
12
100
 
13
101
  ### Added
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: gpdatacached
3
- Version: 0.2.0
3
+ Version: 0.3.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
@@ -20,7 +20,7 @@ Classifier: Programming Language :: Python :: 3.12
20
20
  Classifier: Programming Language :: Python :: 3.13
21
21
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
22
  Requires-Python: >=3.10
23
- Requires-Dist: gpconfig>=0.3.0
23
+ Requires-Dist: gpconfig>=0.4.0
24
24
  Requires-Dist: pydantic>=2.0
25
25
  Requires-Dist: redis>=5.0
26
26
  Provides-Extra: dev
@@ -41,7 +41,7 @@ Description-Content-Type: text/markdown
41
41
 
42
42
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
43
43
  [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/)
44
- [![Development Status](https://img.shields.io/badge/status-alpha-orange.svg)](https://pypi.org/project/gpdatacached/)
44
+ [![Development Status](https://img.shields.io/badge/status-beta-blue.svg)](https://pypi.org/project/gpdatacached/)
45
45
 
46
46
  A Redis-based **cross-process data sharing and caching** library for Python. Any process that can reach the same Redis instance can share live, mutable, typed data structures — scalars, lists, dicts, sets — with reference semantics, lifecycle control, and optional distributed locking.
47
47
 
@@ -53,7 +53,7 @@ A Redis-based **cross-process data sharing and caching** library for Python. Any
53
53
 
54
54
  Sharing mutable state across processes is painful. `multiprocessing.Manager` is single-process-bottlenecked; raw Redis forces you to (de)serialize everything by hand and gives you no type safety, no reference tracking, no TTL cascades. `gpdatacached` gives you a dict-like API on top of Redis with:
55
55
 
56
- - **Typed objects** — store `int`, `str`, `datetime`, `list`, `dict`, `set`, … and get the same type back.
56
+ - **Typed objects** — store `int`, `decimal.Decimal`, `str`, `datetime`, `list`, `dict`, `set`, … and get the same type back.
57
57
  - **Live containers** — `ns["tags"].append("x")` mutates Redis directly; no read-modify-write races on the whole structure.
58
58
  - **Reference sharing** — bind the same container under multiple names inside a namespace; the data is stored once.
59
59
  - **Lifecycle control** — per-object `permanent` / `ttl` / `sliding-window` cache modes, with cascade propagation to owned subtrees.
@@ -105,7 +105,7 @@ assert ns["tags"].value == ["b", "c", "d"]
105
105
 
106
106
  # Lifecycle control.
107
107
  ns["temp"] = "x"
108
- ns.get_object("temp").expire_in(60) # requires cache_mode="ttl"
108
+ ns.get_object("temp").update_cache_mode_and_ttl("ttl", 60) # permanent → ttl, expires in 60s
109
109
 
110
110
  domain.close()
111
111
  ```
@@ -4,7 +4,7 @@
4
4
 
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
6
  [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/)
7
- [![Development Status](https://img.shields.io/badge/status-alpha-orange.svg)](https://pypi.org/project/gpdatacached/)
7
+ [![Development Status](https://img.shields.io/badge/status-beta-blue.svg)](https://pypi.org/project/gpdatacached/)
8
8
 
9
9
  A Redis-based **cross-process data sharing and caching** library for Python. Any process that can reach the same Redis instance can share live, mutable, typed data structures — scalars, lists, dicts, sets — with reference semantics, lifecycle control, and optional distributed locking.
10
10
 
@@ -16,7 +16,7 @@ A Redis-based **cross-process data sharing and caching** library for Python. Any
16
16
 
17
17
  Sharing mutable state across processes is painful. `multiprocessing.Manager` is single-process-bottlenecked; raw Redis forces you to (de)serialize everything by hand and gives you no type safety, no reference tracking, no TTL cascades. `gpdatacached` gives you a dict-like API on top of Redis with:
18
18
 
19
- - **Typed objects** — store `int`, `str`, `datetime`, `list`, `dict`, `set`, … and get the same type back.
19
+ - **Typed objects** — store `int`, `decimal.Decimal`, `str`, `datetime`, `list`, `dict`, `set`, … and get the same type back.
20
20
  - **Live containers** — `ns["tags"].append("x")` mutates Redis directly; no read-modify-write races on the whole structure.
21
21
  - **Reference sharing** — bind the same container under multiple names inside a namespace; the data is stored once.
22
22
  - **Lifecycle control** — per-object `permanent` / `ttl` / `sliding-window` cache modes, with cascade propagation to owned subtrees.
@@ -68,7 +68,7 @@ assert ns["tags"].value == ["b", "c", "d"]
68
68
 
69
69
  # Lifecycle control.
70
70
  ns["temp"] = "x"
71
- ns.get_object("temp").expire_in(60) # requires cache_mode="ttl"
71
+ ns.get_object("temp").update_cache_mode_and_ttl("ttl", 60) # permanent → ttl, expires in 60s
72
72
 
73
73
  domain.close()
74
74
  ```
@@ -4,7 +4,7 @@
4
4
 
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
6
  [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/)
7
- [![Development Status](https://img.shields.io/badge/status-alpha-orange.svg)](https://pypi.org/project/gpdatacached/)
7
+ [![Development Status](https://img.shields.io/badge/status-beta-blue.svg)](https://pypi.org/project/gpdatacached/)
8
8
 
9
9
  一个基于 Redis 的**跨进程数据共享与缓存** Python 库。任何能够访问同一 Redis 实例的进程,都可以共享活跃的、可变的、带类型的数据结构 —— 标量、list、dict、set —— 并具备引用语义、生命周期控制以及可选的分布式锁。
10
10
 
@@ -16,7 +16,7 @@
16
16
 
17
17
  跨进程共享可变状态一直很麻烦:`multiprocessing.Manager` 会形成单进程瓶颈;直接使用 Redis 又需要你手动序列化所有数据,且没有类型安全、没有引用追踪、没有 TTL 级联。`gpdatacached` 在 Redis 之上提供了一套类 dict 的 API,特性包括:
18
18
 
19
- - **带类型的对象** —— 存储 `int`、`str`、`datetime`、`list`、`dict`、`set` …,读取时得到相同类型。
19
+ - **带类型的对象** —— 存储 `int`、`decimal.Decimal`、`str`、`datetime`、`list`、`dict`、`set` …,读取时得到相同类型。
20
20
  - **活跃容器** —— `ns["tags"].append("x")` 直接修改 Redis,避免对整个结构的“读-改-写”竞态。
21
21
  - **引用共享** —— 在同一 namespace 内,同一个容器可绑定到多个名称;数据仅存储一份。
22
22
  - **生命周期控制** —— 每个对象可设置 `permanent` / `ttl` / `sliding`(滑动窗口)缓存模式,并将策略级联传播到所拥有的子树。
@@ -68,7 +68,7 @@ assert ns["tags"].value == ["b", "c", "d"]
68
68
 
69
69
  # 生命周期控制。
70
70
  ns["temp"] = "x"
71
- ns.get_object("temp").expire_in(60) # 要求 cache_mode="ttl"
71
+ ns.get_object("temp").update_cache_mode_and_ttl("ttl", 60) # permanent → ttl,60 秒后过期
72
72
 
73
73
  domain.close()
74
74
  ```
@@ -110,6 +110,7 @@ folder = mgr.get_config("gpdc") # -> GPConfigFolder; sample layouts in te
110
110
  with GPDataCache(folder) as cache:
111
111
  "core_domain" in cache # True iff a config exists (no Redis)
112
112
  domain = cache.get_domain("core_domain") # lazy construct + cache
113
+ site = cache.get_domain("domain_name.com") # dotted name -> domain_name@com.yaml
113
114
 
114
115
  # 3. Resolve objects by GPDC URI — including anonymous nested objects.
115
116
  ns = domain.get_namespace("core_app")
@@ -202,7 +203,7 @@ Configuration for a `GPDCDomain`. All fields are validated; lock fields must be
202
203
  | `gc_lock_ttl_seconds` | `int` | `30` | TTL for locks acquired by GC (must be `≥ 1`). |
203
204
  | `gc_lock_wait_seconds` | `int` | `5` | GC per-object try-lock timeout (must be `≥ 0`). |
204
205
 
205
- 👉 These four fields are consistent across every process that opens the same domain. See [Multi-Process Locking](./locking.md) for what each controls, the cost locks impose, and the GC contract.
206
+ 👉 These four fields must be kept consistent across every process that opens the same domain (mismatch raises `GPDCLockConfigInconsistentError` at domain construction). See [Multi-Process Locking](./locking.md) for what each controls, the cost locks impose, and the GC contract.
206
207
 
207
208
  ---
208
209
 
@@ -214,7 +215,7 @@ class GPDCNamespace(name, domain, *, config=None) # MutableMapping
214
215
 
215
216
  A logical isolation space inside a `GPDCDomain`. Implements the full `MutableMapping` protocol backed by Redis.
216
217
 
217
- **Naming rule:** names may use letters, digits, `.` and `_`; names wrapped in `__xxx__` are reserved. Anonymous objects (internal names starting with `~`) are hidden from the public mapping API. Common characters that are **rejected** because they fall outside the allow-list (and, for `:`, collide with the Redis key separator): hyphen `-`, spaces, and `:` — use `_` or `.` instead.
218
+ **Naming rule:** names may use letters, digits, `.` and `_`; names wrapped in `__xxx__` are reserved. Anonymous objects (internal names starting with `~`) are hidden from the public mapping API. Common characters that are **rejected** because they fall outside the allow-list (and, for `:`, collide with the Redis key separator): hyphen `-`, spaces, and `:` — use `_` or `.` instead. This rule applies to **object and namespace names**, and to **domain names** (a `.` in a domain name is legal everywhere in GPDC — but when the domain is driven by a config file through `GPDataCache`, the `.` must be spelled `@` in the filename; see the [`GPDataCache` `@` escape](#gpdatacache)).
218
219
 
219
220
  **Properties**
220
221
 
@@ -296,15 +297,34 @@ recommended entry point when your domains are driven by config files rather
296
297
  than constructed ad-hoc in code.
297
298
 
298
299
  **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.
300
+ folder reference only — **no domain is created** (it does scan the folder once
301
+ to warn about unaddressable config filenames; see below). Domains are
302
+ constructed lazily on first [`get_domain`](#gpdatacache) access, and cached in
303
+ an internal dict. The cache is a snapshot: config files edited after
304
+ construction are **not** re-read; rebuild the `GPDataCache` to pick up changes.
304
305
 
305
306
  `GPDataCache` does **not** create domains lacking a config file — for ad-hoc
306
307
  domains use [`GPDCDomain`](#gpdcdomain) directly.
307
308
 
309
+ > **Dots in domain names — the `@` escape.** gpconfig addresses configs by
310
+ > dot-separated paths, so a `.` in a config filename is consumed as a path
311
+ > separator and the file becomes unaddressable. To use a dotted domain name —
312
+ > the natural shape when a domain mirrors an internet domain, e.g.
313
+ > `domain_name.com` — write each `.` as `@` in the config filename:
314
+ > `domain_name@com.yaml` configures the domain `"domain_name.com"`, and
315
+ > `cache.get_domain("domain_name.com")` /
316
+ > `cache.get_object_by_uri("gpdc://domain_name.com/ns/obj")` resolve it
317
+ > transparently. The mapping is unambiguous because `@` is not a legal
318
+ > character in GPDC names. `GPDCDomain` itself performs the `@` → `.`
319
+ > restore for names derived from a config-file stem, so gpconfig's object
320
+ > factory path also works: `folder.get_object("domain_name@com")`
321
+ > constructs a domain named `domain_name.com`, identical to
322
+ > `cache.get_domain("domain_name.com")`. An explicit `name=` argument
323
+ > containing `@` is still rejected — the escape exists for filenames;
324
+ > in code, write the `.` directly. Config files whose names contain a real `.` are
325
+ > listed by the folder but can never be loaded; `GPDataCache` logs a warning
326
+ > about them once at construction.
327
+
308
328
  > **Registration side effect.** `import gpdatacached` registers `GPDCDomain`
309
329
  > and `GPDCDomainConfig` with gpconfig's object factory (idempotent), which is
310
330
  > what makes `folder.get_object(name)` able to construct a `GPDCDomain`. No
@@ -312,7 +332,7 @@ domains use [`GPDCDomain`](#gpdcdomain) directly.
312
332
 
313
333
  **Dunder protocols**
314
334
 
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.
335
+ - `name in cache` → `True` iff a config for `name` exists in the folder (dotted names look up their `@`-escaped filename). 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. Names that are not legal GPDC domain names (e.g. containing `@`) return `False`. The domain instance is created lazily on first `get_domain(name)` access.
316
336
  - `with cache as c` / `__exit__` → closes every cached domain on exit; `close()` is idempotent.
317
337
 
318
338
  > **`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.
@@ -321,7 +341,7 @@ domains use [`GPDCDomain`](#gpdcdomain) directly.
321
341
 
322
342
  | Method | Description |
323
343
  |---|---|
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()`. |
344
+ | `get_domain(name)` | Return the cached `GPDCDomain` for `name`, constructing it lazily on first access. Dotted names resolve to their `@`-escaped config file (see above). Raises `ValueError` if `name` is not a legal domain name; propagates gpconfig's `ConfigNotFoundError` if no config exists for `name`. Raises `RuntimeError` after `close()`. |
325
345
  | [`get_object_by_uri(uri)`](#get_object_by_uri) | Resolve a GPDC URI to a `GPDCDataObject`. Raises `RuntimeError` after `close()`. |
326
346
  | `has_uri(uri)` | Return `True` iff `get_object_by_uri(uri)` would succeed. Raises `RuntimeError` after `close()`. |
327
347
  | `close()` | Close every cached domain. Idempotent. |
@@ -370,9 +390,10 @@ For example, given `ns["cfg"] = {"port": 8080}`, `gpdc://d/ns/cfg/port` raises
370
390
 
371
391
  **Errors.** Every failure mode raises [`ObjectNotFoundByURIError`](#objectnotfoundbyurierror)
372
392
  (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.
393
+ three segments, empty segment), an invalid domain/namespace/object name (each
394
+ locating segment is validated up front), unknown domain, missing namespace,
395
+ missing root object, a path segment that fails
396
+ (`KeyError`/`IndexError`/`TypeError`), and a scalar terminal.
376
397
 
377
398
  **Example.** Given `ns["cfg"] = {"server": {"host": "x"}, "tags": ["a","b"]}`:
378
399
 
@@ -395,7 +416,7 @@ class ObjectNotFoundByURIError(GPDCError)
395
416
  Raised by [`GPDataCache.get_object_by_uri`](#get_object_by_uri) when a GPDC URI
396
417
  cannot be resolved to a `GPDCDataObject`. Covers every failure mode: malformed
397
418
  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
419
+ an invalid domain/namespace/object name, a path segment that fails, and a scalar terminal. Carries `.uri` (the URI
399
420
  string) and `.reason` (a short explanation).
400
421
 
401
422
  `has_uri` swallows only this exception; infrastructure errors (e.g. Redis
@@ -431,12 +452,14 @@ Abstract base class for every GPDC data object. Subclasses are split into [`GPDC
431
452
  |---|---|---|
432
453
  | `type` | `str` | GPDC type string, e.g. `"scalar:int"`, `"collection:list"`. |
433
454
  | `meta` | `ObjectMeta \| None` | Full metadata record; `None` if the object has vanished. |
434
- | `cache_mode` | `str` | Effective cache mode; raises `ObjectGoneError` if missing. |
455
+ | `cache_mode` | `str` | Effective cache mode. |
435
456
  | `ttl` | `int` | Declared TTL in seconds (`0` = permanent). |
436
457
  | `created_at` | `datetime \| None` | Creation timestamp. |
437
458
  | `updated_at` | `datetime \| None` | Last update timestamp. |
438
459
  | `value` | `object` | The Python value (abstract; subclasses implement). |
439
460
 
461
+ > The meta-backed properties (`cache_mode`, `ttl`, `created_at`, `updated_at`) each raise `ObjectGoneError` if the object's metadata no longer exists.
462
+
440
463
  **Lifecycle methods**
441
464
 
442
465
  > This is the API summary. For the full model — policy resolution, sliding refresh, ownership, anonymous/reference lifecycle, and pitfalls — see [Lifecycle Management](./lifecycle.md).
@@ -449,6 +472,7 @@ Abstract base class for every GPDC data object. Subclasses are split into [`GPDC
449
472
  | `persist()` | Switch to `cache_mode="permanent"` with `ttl=0`. Requires ownership. |
450
473
  | `update_cache_mode_and_ttl(cache_mode, ttl)` | Replace the full cache policy. Requires ownership. |
451
474
  | `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. |
475
+ | `remaining_ttl()` | Remaining TTL in seconds on the canonical key, as reported by the Redis `TTL` command (`-1` = no expiry, `-2` = key missing). |
452
476
  | `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). |
453
477
 
454
478
  **Locking methods** (opt-in, never called automatically by mutations)
@@ -480,7 +504,7 @@ Base class for every scalar object. A scalar stores a single encoded value in it
480
504
  - `value` getter decodes the stored value via the registered codec.
481
505
  - `value` setter validates the Python type against the registered codec, updates metadata, and refreshes TTL when `cache_mode="sliding"`.
482
506
 
483
- Built-in subclasses: [`IntObject`](#intobject), [`FloatObject`](#floatobject), [`StrObject`](#strobject), [`BoolObject`](#boolobject), [`DateTimeObject`](#datetimeobject), [`DateObject`](#dateobject), [`TimeObject`](#timeobject), [`NoneObject`](#noneobject), [`TupleObject`](#tupleobject).
507
+ Built-in subclasses: [`IntObject`](#intobject), [`FloatObject`](#floatobject), [`StrObject`](#strobject), [`BoolObject`](#boolobject), [`DateTimeObject`](#datetimeobject), [`DateObject`](#dateobject), [`TimeObject`](#timeobject), [`DecimalObject`](#decimalobject), [`NoneObject`](#noneobject), [`TupleObject`](#tupleobject).
484
508
 
485
509
  ---
486
510
 
@@ -526,6 +550,7 @@ All scalar classes inherit from [`GPDCScalarObject`](#gpdcscalarobject). Reading
526
550
  | [`DateTimeObject`](#datetimeobject) | `scalar:datetime` | `datetime.datetime` | `datetime:{isoformat}` |
527
551
  | [`DateObject`](#dateobject) | `scalar:date` | `datetime.date` (excludes `datetime`) | `date:{isoformat}` |
528
552
  | [`TimeObject`](#timeobject) | `scalar:time` | `datetime.time` | `time:{isoformat}` |
553
+ | [`DecimalObject`](#decimalobject) | `scalar:decimal` | `decimal.Decimal` | `decimal:{str(d)}` |
529
554
  | [`NoneObject`](#noneobject) | `scalar:none` | `NoneType` | `none:None` |
530
555
  | [`TupleObject`](#tupleobject) | `scalar:tuple` | `tuple` (scalar elements only) | `tuple:{json}` |
531
556
 
@@ -557,6 +582,14 @@ Wraps `datetime.date`. Note: a `datetime.datetime` is rejected by `DateCodec` ev
557
582
 
558
583
  Wraps `datetime.time`.
559
584
 
585
+ ### `DecimalObject`
586
+
587
+ Wraps `decimal.Decimal` (added in 0.2.1). Encoded as `decimal:{str(value)}`: the payload is `str(Decimal)`, which preserves the exact coefficient and exponent — `Decimal("1.10")` roundtrips as `1.10`, not `1.1`, and extreme exponents use scientific notation (`1E-27`). A malformed payload raises `ValueError` on decode.
588
+
589
+ - **Non-finite values** (`NaN`, `sNaN`, `±Infinity`) and `-0` are stored and restored as-is, matching the `float` codec's treatment of `nan`/`inf`. Comparison semantics follow the standard library: `NaN != NaN`, and comparing an `sNaN` with `==` raises `decimal.InvalidOperation`.
590
+ - **Exactness**: `str()` is context-independent — no rounding is applied on store or load. Construct from strings (`Decimal("0.1")`), not floats, if you mean the decimal literal; `Decimal(0.1)` faithfully stores the full binary expansion.
591
+ - **Cross-type equality**: GPDC matches dict keys and set members by their encoded wire form, so natively-equal values of different numeric types become distinct entries once stored — `d[1] = "a"` followed by `d[Decimal(1)] = "b"` yields two entries (the same already holds for `1` vs `1.0`). A native literal like `{1: "a", Decimal(1): "b"}` collapses to one key before GPDC ever sees it.
592
+
560
593
  ### `NoneObject`
561
594
 
562
595
  Wraps `None`. Used to represent the presence of a `None` value distinctly from a missing key.
@@ -654,7 +687,7 @@ Abstract base for scalar codecs. Provides the shared helper:
654
687
  |---|---|
655
688
  | `_parse_encoded(encoded) -> tuple[str, str]` | Split a `name:payload` encoded value into `(type_name, raw)`. Raises `ValueError` on empty or colon-less input. |
656
689
 
657
- Built-in subclasses: `BoolCodec`, `DateTimeCodec`, `DateCodec`, `DictCodec` *(see note)*, `FloatCodec`, `IntCodec`, `ListCodec` *(see note)*, `NoneCodec`, `SetCodec` *(see note)*, `StrCodec`, `TimeCodec`, `TupleCodec`.
690
+ Built-in subclasses: `BoolCodec`, `DateTimeCodec`, `DateCodec`, `DecimalCodec`, `DictCodec` *(see note)*, `FloatCodec`, `IntCodec`, `ListCodec` *(see note)*, `NoneCodec`, `SetCodec` *(see note)*, `StrCodec`, `TimeCodec`, `TupleCodec`.
658
691
 
659
692
  > Scalar codec classes for containers (`ListCodec`, `DictCodec`, `SetCodec`) are re-exported only for completeness — they actually inherit from [`GPDCContainerCodec`](#gpdccontainercodec).
660
693
 
@@ -749,6 +782,20 @@ def validate_object_name(name: str) -> None
749
782
 
750
783
  Validate that `name` contains only letters, digits, `.` or `_` and is not a reserved `__xxx__` name. Raises `ValueError` otherwise. (Equivalent validators exist internally for domain and namespace names.)
751
784
 
785
+ ### Domain-name `@` escape helpers
786
+
787
+ ```python
788
+ DOMAIN_DOT_ESCAPE: str # "@"
789
+
790
+ def escape_domain_name(name: str) -> str
791
+ # "domain_name.com" -> "domain_name@com" ('.' -> '@')
792
+
793
+ def unescape_domain_name(stem: str) -> str
794
+ # "domain_name@com" -> "domain_name.com" ('@' -> '.')
795
+ ```
796
+
797
+ Helpers for the [`GPDataCache` `@` escape convention](#gpdatacache): gpconfig addresses configs by dot-separated paths, so a `.` in a domain config filename is unaddressable — each `.` in a domain name is written as `@` in the filename instead. The mapping is injective because `@` is not a legal character in GPDC names. `unescape_domain_name` is safe to apply to any stem-derived name for the same reason. You rarely need these directly — `GPDataCache` (lookup escaping) and `GPDCDomain` (load-time restore) apply them automatically.
798
+
752
799
  ---
753
800
 
754
801
  ## Exceptions
@@ -761,7 +808,7 @@ All public exceptions descend from the public `GPDCError(Exception)` root. Use `
761
808
  class GPDCError(Exception)
762
809
  ```
763
810
 
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.
811
+ The root of the gpdatacached exception hierarchy. Every gpdatacached-specific exception raised by this library is a subclass of `GPDCError`, so `except GPDCError` is the broad catch-all. (Plain stdlib errors — e.g. `TypeError` for unsupported types or an invalid argument order, `ValueError` for invalid payloads — are raised as-is.) Prefer catching the specific subclass when you need to distinguish failure modes.
765
812
 
766
813
  ### `GPDCLockError`
767
814
 
@@ -38,7 +38,7 @@ Two value families exist, with different contracts:
38
38
 
39
39
  ---
40
40
 
41
- ## 2. The Registration Triple
41
+ ## 2. The Registration Entry
42
42
 
43
43
  Registering a type always means pairing four things via one call:
44
44
 
@@ -94,9 +94,10 @@ graph TD
94
94
  C1["GPDCScalarCodec"]
95
95
  C2["GPDCContainerCodec (ABC)"]
96
96
  C0 -->|encode_value / decode_value| C1
97
- C0 -->|create_raw / read_raw / delete_raw| C2
98
97
  ```
99
98
 
99
+ Two **independent** roots: `GPDCCodec` (scalars) and `GPDCContainerCodec` (containers) are siblings — a container codec never implements `encode_value`/`decode_value`.
100
+
100
101
  ### `GPDCCodec` — the root ABC
101
102
 
102
103
  ```python
@@ -194,7 +194,7 @@ graph TD
194
194
 
195
195
  ---
196
196
 
197
- ## 7. The Ownership Model
197
+ ## 7. The Ownership Model { #7-the-ownership-model }
198
198
 
199
199
  Every object has an `owner` field in its metadata. Ownership determines who controls the object's lifecycle and is central to how anonymous objects work.
200
200
 
@@ -362,7 +362,7 @@ For reference, here is the complete set of user-facing lifecycle methods on `GPD
362
362
  | `take_ownership()` | Promote **the referenced object** to own itself (primary use case: a named reference to an anonymous nested object; a correct no-op on a reference to an already self-owned named object). |
363
363
  | `ttl` (property) | The declared TTL in seconds. |
364
364
  | `cache_mode` (property) | The current cache mode. |
365
- | `remaining_ttl()` | The actual remaining TTL reported by Redis (can be 0 or -1). |
365
+ | `remaining_ttl()` | The actual remaining TTL reported by Redis (0; -1 = no expiry; -2 = key already gone). |
366
366
  | `created_at` / `updated_at` (properties) | Timestamps from metadata. |
367
367
 
368
368
  Plus the domain-level `domain.run_gc(namespace=None)` for manual GC.
@@ -71,7 +71,7 @@ graph TD
71
71
  end
72
72
  subgraph L4["Layer 4 — Built-in types"]
73
73
  BREG["builtins.registry<br/>(auto-registered on import)"]
74
- SCALARS["scalar types<br/>int·float·str·bool·datetime·date·time·none·tuple"]
74
+ SCALARS["scalar types<br/>int·float·decimal·str·bool·datetime·date·time·none·tuple"]
75
75
  CONTAINERS["container types<br/>list·dict·set"]
76
76
  end
77
77
  subgraph L3["Layer 3 — Type system"]
@@ -92,7 +92,7 @@ graph TD
92
92
  EXC["exceptions"]
93
93
  end
94
94
  REDIS[("Redis")]
95
- L5 --> L4
95
+ L5 --> L3
96
96
  L4 --> L3
97
97
  L3 --> L2
98
98
  L3 --> L1
@@ -38,7 +38,7 @@ Two corollaries:
38
38
  |---|---|
39
39
  | **Row indexes** | `RangeIndex` (default int index, stored compactly) **and** any labelled index including `pd.MultiIndex` (stored one `collection:list` child per level). `.index_names` always returns a tuple. |
40
40
  | **DataFrame columns** | Single-level (scalar names) **or** `pd.MultiIndex` (tuple names). The row and column axes are independent and compose (a DataFrame may have both a row and a column MultiIndex). |
41
- | **Value dtypes** | `int64`/`Int64`, `float64`/`Float64`, `bool`/`boolean`, `str`/`string`, `object`, `datetime64` (naive & tz-aware), `category`. |
41
+ | **Value dtypes** | Integer / float / boolean dtypes of any width (e.g. `int64`/`Int64`, `float64`/`Float64`, `bool`/`boolean`), `str`/`string`, `object` (any scalar-encodable element — including `decimal.Decimal`, builtin since 0.2.1), `datetime64` (naive & tz-aware, any unit), `category`. |
42
42
  | **Mutation** | Row `extend` (Series & DataFrame); DataFrame column `__setitem__` / `__delitem__` (structural add/replace/drop). |
43
43
 
44
44
  Notes:
@@ -185,7 +185,7 @@ GPDC type `collection:pandas_dataframe`. Backed by a Redis hash with column valu
185
185
 
186
186
  | Method | Description |
187
187
  |---|---|
188
- | `extend(other)` | Append rows in place. Accepts a `pd.DataFrame`, a `dict` of lists, or a list of `dict` rows. For a genuine MultiIndex-row DataFrame (`L ≥ 2`), only a `DataFrame` with a matching MultiIndex is accepted (dict/list inputs synthesise positional labels a MultiIndex cannot supply → `TypeError`; use `.value`). Mutates Redis; returns `None`. |
188
+ | `extend(other)` | Append rows in place. Accepts a `pd.DataFrame`, a `dict` of lists, or a list of `dict` rows. For a genuine MultiIndex-row DataFrame (`L ≥ 2`), only a `DataFrame` with a matching MultiIndex is accepted (dict/list inputs synthesise positional labels a MultiIndex cannot supply → `TypeError`; use `.value`). Category columns reject values outside the stored categories (`ValueError`, raised before any Redis write). Mutates Redis; returns `None`. |
189
189
  | `deepcopy()` | Return a fully independent `pd.DataFrame` (`.value.copy(deep=True)`). |
190
190
 
191
191
  **Column-axis access (single-level columns)**
@@ -248,4 +248,4 @@ Raised when a DataFrame cannot be stored element-wise by `DataFrameObject`. The
248
248
  class UnsupportedElementTypeError(TypeError)
249
249
  ```
250
250
 
251
- Raised when a Series/DataFrame element is not encodable by a `GPDCScalarCodec` subclass — i.e. it is a container type (`list`/`dict`/`set`/container-registered pydantic model) or an unsupported type. Series/DataFrame elements must be scalar types encodable by a `GPDCScalarCodec` subclass. The message enumerates the allowed scalar types.
251
+ Raised when a Series/DataFrame element is not encodable by a `GPDCScalarCodec` subclass — i.e. it is a container type (`list`/`dict`/`set`/container-registered pydantic model) or an unsupported type. Series/DataFrame elements must be scalar types encodable by a `GPDCScalarCodec` subclass. The message enumerates the allowed scalar types. Allowed builtin scalars (0.2.1+): int/float/bool/str/datetime/date/time/decimal/tuple/None, plus any scalar-registered custom type; object-dtype columns and index labels may hold any of them — including `decimal.Decimal`.
@@ -36,7 +36,7 @@ A quick reference. "RTT" = Redis round-trip. N = number of elements; counts are
36
36
  | `ns[k]` (scalar read) | O(1), ~3 RTT | `_resolve_binding` (`TYPE` + `HGETALL`) **plus** the scalar `.value` getter re-loads meta (`HGETALL`). |
37
37
  | `ns[k] = [a, b, c]` (flat, scalar-element container) | O(N) local encode, **O(1)** data-write RTT | One `encode_session` + one bulk `create_raw` (single `RPUSH`/`HSET`/`SADD` carrying all elements) + `HGET` ×2 (namespace-default policy) + metadata `HSET` + TTL applies on the obj/raw/ref keys (≈ 8 RTT total, independent of N for scalar elements). |
38
38
  | `ns[k] = [[...], ...]` (nested container) | O(**nested-container count**) RTT + O(N) local | Each nested container becomes one anonymous object (meta `HSET` + its own `create_raw` + `HINCRBY` ref + TTL applies ≈ 5–6 RTT each). Depth does **not** multiply — it is the total node count that scales. |
39
- | `container.append(x)` / `container[k] = v` (scalar element, non-sliding) | O(1), ~5–6 RTT | `HGETALL` (policy) + data write (`RPUSH`/`HSET`) + `_touch` (`LLEN`/`HLEN` + `HGETALL` + `HSET`). **Add the sliding-subtree walk if `cache_mode="sliding"`** (§3); **add ~5–6 RTT if the element is itself a container** (anonymous-object creation). |
39
+ | `container.append(x)` / `container[k] = v` (scalar element, non-sliding) | O(1), ~5–6 RTT | `HGETALL` ×2 (liveness/policy + data-owner resolve) + data write (`RPUSH`/`HSET`) + `LLEN`/`HLEN` + metadata `HSET`. **Add the sliding-subtree walk if `cache_mode="sliding"`** (§3); **add ~5–6 RTT if the element is itself a container** (anonymous-object creation). |
40
40
  | `iter(ns)` / `len(ns)` | O(N), batched SCAN | A few RTT per ~100-key SCAN page (`len` delegates to `iter`). Don't put in a hot loop. |
41
41
  | `k in ns` | O(1), 1–4 RTT | Direct `TYPE` + `HGETALL` on the named key (1–2 RTT for owner bindings); references add a `GET` + another `TYPE` (up to 4 RTT for aliases). **Not** a SCAN. Does not scale with namespace size. |
42
42
  | `del ns[k]` (container owner) | O(owned subtree size) | `_teardown_owner_container` releases each nested ref (`HDEL`) and cascade-deletes orphaned anonymous children; ~1+ RTT per node. Raises `CanonicalObjectReferencedError` if live alias referrers exist — delete those first. |
@@ -48,7 +48,7 @@ A quick reference. "RTT" = Redis round-trip. N = number of elements; counts are
48
48
  | `.value` (full container read) | flat: 1 RTT + O(N) local; nested: +O(nested count) RTT | One bulk read (`LRANGE`/`HGETALL`/`SMEMBERS`); each `ref:` element adds one `HGETALL` + decode for its anonymous object. |
49
49
  | `name in cache` (`GPDataCache`) | **local, 0 RTT** | Pure config-snapshot check via `folder.list_configs()`. Does not touch Redis and does not construct a domain. The cheapest membership test in the library. |
50
50
  | `cache.get_domain(name)` | 1st call: domain construction (domain-meta read/verify); subsequent: **0 RTT** | First access constructs and caches the `GPDCDomain` via gpconfig's factory; later calls return the cached instance. Does not re-read config files. |
51
- | `cache.get_object_by_uri(uri)` | root: ~4–5 RTT; **+1 RTT per path segment** | `get_namespace` (`SISMEMBER` + namespace-meta `HGETALL`, ~2 RTT) + root resolve (`TYPE` + `HGETALL`, ~2–3 RTT). Each extra path segment is one `__getitem__` read (`LINDEX` for `ListObject`, `HGET` for `DictObject`). `cache.has_uri(uri)` has the same cost (it calls `get_object_by_uri` under a try/except). The domain is only constructed on the first access; later URIs in the same domain reuse the cached domain. |
51
+ | `cache.get_object_by_uri(uri)` | root: ~4–5 RTT; **+1–2 RTT per path segment** | `get_namespace` (`SISMEMBER` + namespace-meta `HGETALL` + idempotent `SADD`, ~3 RTT) + root resolve (`TYPE` + `HGETALL`, ~2–3 RTT). Each extra path segment is one `__getitem__` read (ListObject: `LLEN` + `LINDEX` — 2 RTT; DictObject: `HGET` — 1 RTT). `cache.has_uri(uri)` has the same cost (it calls `get_object_by_uri` under a try/except). The domain is only constructed on the first access; later URIs in the same domain reuse the cached domain. |
52
52
 
53
53
  ---
54
54
 
@@ -134,7 +134,7 @@ Stores the entire model as a single JSON-encoded Redis string. Reads return a fr
134
134
 
135
135
  The whole object is then stored as `{type_prefix}:{json}`, where `type_prefix` is the part of the GPDC type after `scalar:`.
136
136
 
137
- **Field type restriction.** Every field must be encodable by a `GPDCScalarCodec` subclass (int / float / bool / str / datetime / date / time / tuple / None / any scalar-registered custom type). Container field types (`list`, `dict`, `set`, container-registered pydantic models) raise `TypeError` at encode time — use container mode instead.
137
+ **Field type restriction.** Every field must be encodable by a `GPDCScalarCodec` subclass (int / float / bool / str / datetime / date / time / decimal / tuple / None / any scalar-registered custom type). Container field types (`list`, `dict`, `set`, container-registered pydantic models) raise `TypeError` at encode time — use container mode instead.
138
138
 
139
139
  ### `PydanticModelScalarCodec`
140
140