gpdatacached 0.2.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/CHANGELOG.md +35 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/PKG-INFO +5 -5
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/README.md +3 -3
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/README.zh-CN.md +3 -3
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/docs/en/api.md +17 -5
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/docs/en/extensibility.md +3 -2
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/docs/en/lifecycle.md +2 -2
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/docs/en/overview.md +2 -2
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/docs/en/pandas-support.md +3 -3
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/docs/en/performance-guide.md +2 -2
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/docs/en/pydantic-support.md +1 -1
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/docs/zh/api.md +16 -4
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/docs/zh/extensibility.md +3 -2
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/docs/zh/lifecycle.md +3 -3
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/docs/zh/overview.md +2 -2
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/docs/zh/pandas-support.md +5 -5
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/docs/zh/performance-guide.md +2 -2
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/docs/zh/pydantic-support.md +1 -1
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/__init__.py +4 -1
- gpdatacached-0.2.1/src/gpdatacached/builtins/decimal_object.py +39 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/builtins/registry.py +11 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/extensions/pandas_support.py +1 -1
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/keys.py +1 -1
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/.gitignore +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/LICENSE +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/docs/en/index.md +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/docs/en/locking.md +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/docs/zh/index.md +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/docs/zh/locking.md +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/pyproject.toml +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/builtins/__init__.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/builtins/bool_object.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/builtins/datetime_object.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/builtins/dict_object.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/builtins/float_object.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/builtins/int_object.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/builtins/list_object.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/builtins/none_object.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/builtins/set_object.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/builtins/str_object.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/builtins/tuple_object.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/codec.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/config.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/container.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/data_cache.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/domain.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/exceptions.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/extensions/__init__.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/extensions/pydantic_support.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/gc.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/lifecycle.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/locking.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/meta.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/namespace.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/object.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/refset.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/scalar.py +0 -0
- {gpdatacached-0.2.0 → gpdatacached-0.2.1}/src/gpdatacached/types.py +0 -0
|
@@ -8,6 +8,41 @@ 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.2.1] — 2026-10-12
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`Decimal` builtin scalar type** — `decimal.Decimal` is auto-registered on
|
|
16
|
+
`import gpdatacached` as `scalar:decimal`; `DecimalObject` and `DecimalCodec`
|
|
17
|
+
are exported from the package root. Encoded losslessly as
|
|
18
|
+
`decimal:{str(value)}`: coefficient/exponent (significance) are preserved
|
|
19
|
+
exactly, extreme exponents use scientific notation, and non-finite values
|
|
20
|
+
(`NaN`, `sNaN`, `±Infinity`) plus `-0` roundtrip as-is — the same policy as
|
|
21
|
+
`float`'s `nan`/`inf`. Works everywhere a registered scalar does: standalone
|
|
22
|
+
objects, container elements, dict keys, set members, tuple elements, and
|
|
23
|
+
pandas object-dtype columns/index labels. Malformed payloads raise
|
|
24
|
+
`ValueError` on decode. Note the documented cross-type-equality caveat:
|
|
25
|
+
natively-equal values of different numeric types (`1` vs `Decimal(1)`) are
|
|
26
|
+
distinct GPDC dict keys/set members, as `1` vs `1.0` already were.
|
|
27
|
+
|
|
28
|
+
### Documentation
|
|
29
|
+
|
|
30
|
+
- Full docs-vs-code synchronization pass across `docs/` (English + zh-CN
|
|
31
|
+
mirrors) and both READMEs: README now lists `Decimal` among the supported
|
|
32
|
+
scalar types and its Quick Start lifecycle example is runnable as written
|
|
33
|
+
(`update_cache_mode_and_ttl` instead of mode-restricted `expire_in`);
|
|
34
|
+
`api.md` documents `remaining_ttl()` and the exact `ObjectGoneError`
|
|
35
|
+
surface; the extensibility codec diagram and the overview layer-dependency
|
|
36
|
+
edge were corrected to match the code (`GPDCContainerCodec` is a sibling
|
|
37
|
+
root of `GPDCCodec`, and the extensions depend on the type-system layer,
|
|
38
|
+
not the builtins); performance-guide RTT attributions were updated; the
|
|
39
|
+
zh-CN mirrors gained the translated §7 ownership heading (with a durable
|
|
40
|
+
explicit anchor) and several translation fixes. `keys.py`'s `make_ref_key`
|
|
41
|
+
docstring and the pandas `UnsupportedElementTypeError` guidance message
|
|
42
|
+
were corrected to match reality (hash, not set; `decimal` included). The
|
|
43
|
+
README status badge now reads Beta, aligned with the packaging
|
|
44
|
+
classifier `Development Status :: 4 - Beta`.
|
|
45
|
+
|
|
11
46
|
## [0.2.0] — 2026-07-26
|
|
12
47
|
|
|
13
48
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: gpdatacached
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
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
|
|
@@ -41,7 +41,7 @@ Description-Content-Type: text/markdown
|
|
|
41
41
|
|
|
42
42
|
[](LICENSE)
|
|
43
43
|
[](https://www.python.org/)
|
|
44
|
-
[](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").
|
|
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)
|
|
6
6
|
[](https://www.python.org/)
|
|
7
|
-
[](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").
|
|
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)
|
|
6
6
|
[](https://www.python.org/)
|
|
7
|
-
[](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").
|
|
71
|
+
ns.get_object("temp").update_cache_mode_and_ttl("ttl", 60) # permanent → ttl,60 秒后过期
|
|
72
72
|
|
|
73
73
|
domain.close()
|
|
74
74
|
```
|
|
@@ -202,7 +202,7 @@ Configuration for a `GPDCDomain`. All fields are validated; lock fields must be
|
|
|
202
202
|
| `gc_lock_ttl_seconds` | `int` | `30` | TTL for locks acquired by GC (must be `≥ 1`). |
|
|
203
203
|
| `gc_lock_wait_seconds` | `int` | `5` | GC per-object try-lock timeout (must be `≥ 0`). |
|
|
204
204
|
|
|
205
|
-
👉 These four fields
|
|
205
|
+
👉 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
206
|
|
|
207
207
|
---
|
|
208
208
|
|
|
@@ -431,12 +431,14 @@ Abstract base class for every GPDC data object. Subclasses are split into [`GPDC
|
|
|
431
431
|
|---|---|---|
|
|
432
432
|
| `type` | `str` | GPDC type string, e.g. `"scalar:int"`, `"collection:list"`. |
|
|
433
433
|
| `meta` | `ObjectMeta \| None` | Full metadata record; `None` if the object has vanished. |
|
|
434
|
-
| `cache_mode` | `str` | Effective cache mode
|
|
434
|
+
| `cache_mode` | `str` | Effective cache mode. |
|
|
435
435
|
| `ttl` | `int` | Declared TTL in seconds (`0` = permanent). |
|
|
436
436
|
| `created_at` | `datetime \| None` | Creation timestamp. |
|
|
437
437
|
| `updated_at` | `datetime \| None` | Last update timestamp. |
|
|
438
438
|
| `value` | `object` | The Python value (abstract; subclasses implement). |
|
|
439
439
|
|
|
440
|
+
> The meta-backed properties (`cache_mode`, `ttl`, `created_at`, `updated_at`) each raise `ObjectGoneError` if the object's metadata no longer exists.
|
|
441
|
+
|
|
440
442
|
**Lifecycle methods**
|
|
441
443
|
|
|
442
444
|
> 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 +451,7 @@ Abstract base class for every GPDC data object. Subclasses are split into [`GPDC
|
|
|
449
451
|
| `persist()` | Switch to `cache_mode="permanent"` with `ttl=0`. Requires ownership. |
|
|
450
452
|
| `update_cache_mode_and_ttl(cache_mode, ttl)` | Replace the full cache policy. Requires ownership. |
|
|
451
453
|
| `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. |
|
|
454
|
+
| `remaining_ttl()` | Remaining TTL in seconds on the canonical key, as reported by the Redis `TTL` command (`-1` = no expiry, `-2` = key missing). |
|
|
452
455
|
| `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
456
|
|
|
454
457
|
**Locking methods** (opt-in, never called automatically by mutations)
|
|
@@ -480,7 +483,7 @@ Base class for every scalar object. A scalar stores a single encoded value in it
|
|
|
480
483
|
- `value` getter decodes the stored value via the registered codec.
|
|
481
484
|
- `value` setter validates the Python type against the registered codec, updates metadata, and refreshes TTL when `cache_mode="sliding"`.
|
|
482
485
|
|
|
483
|
-
Built-in subclasses: [`IntObject`](#intobject), [`FloatObject`](#floatobject), [`StrObject`](#strobject), [`BoolObject`](#boolobject), [`DateTimeObject`](#datetimeobject), [`DateObject`](#dateobject), [`TimeObject`](#timeobject), [`NoneObject`](#noneobject), [`TupleObject`](#tupleobject).
|
|
486
|
+
Built-in subclasses: [`IntObject`](#intobject), [`FloatObject`](#floatobject), [`StrObject`](#strobject), [`BoolObject`](#boolobject), [`DateTimeObject`](#datetimeobject), [`DateObject`](#dateobject), [`TimeObject`](#timeobject), [`DecimalObject`](#decimalobject), [`NoneObject`](#noneobject), [`TupleObject`](#tupleobject).
|
|
484
487
|
|
|
485
488
|
---
|
|
486
489
|
|
|
@@ -526,6 +529,7 @@ All scalar classes inherit from [`GPDCScalarObject`](#gpdcscalarobject). Reading
|
|
|
526
529
|
| [`DateTimeObject`](#datetimeobject) | `scalar:datetime` | `datetime.datetime` | `datetime:{isoformat}` |
|
|
527
530
|
| [`DateObject`](#dateobject) | `scalar:date` | `datetime.date` (excludes `datetime`) | `date:{isoformat}` |
|
|
528
531
|
| [`TimeObject`](#timeobject) | `scalar:time` | `datetime.time` | `time:{isoformat}` |
|
|
532
|
+
| [`DecimalObject`](#decimalobject) | `scalar:decimal` | `decimal.Decimal` | `decimal:{str(d)}` |
|
|
529
533
|
| [`NoneObject`](#noneobject) | `scalar:none` | `NoneType` | `none:None` |
|
|
530
534
|
| [`TupleObject`](#tupleobject) | `scalar:tuple` | `tuple` (scalar elements only) | `tuple:{json}` |
|
|
531
535
|
|
|
@@ -557,6 +561,14 @@ Wraps `datetime.date`. Note: a `datetime.datetime` is rejected by `DateCodec` ev
|
|
|
557
561
|
|
|
558
562
|
Wraps `datetime.time`.
|
|
559
563
|
|
|
564
|
+
### `DecimalObject`
|
|
565
|
+
|
|
566
|
+
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.
|
|
567
|
+
|
|
568
|
+
- **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`.
|
|
569
|
+
- **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.
|
|
570
|
+
- **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.
|
|
571
|
+
|
|
560
572
|
### `NoneObject`
|
|
561
573
|
|
|
562
574
|
Wraps `None`. Used to represent the presence of a `None` value distinctly from a missing key.
|
|
@@ -654,7 +666,7 @@ Abstract base for scalar codecs. Provides the shared helper:
|
|
|
654
666
|
|---|---|
|
|
655
667
|
| `_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
668
|
|
|
657
|
-
Built-in subclasses: `BoolCodec`, `DateTimeCodec`, `DateCodec`, `DictCodec` *(see note)*, `FloatCodec`, `IntCodec`, `ListCodec` *(see note)*, `NoneCodec`, `SetCodec` *(see note)*, `StrCodec`, `TimeCodec`, `TupleCodec`.
|
|
669
|
+
Built-in subclasses: `BoolCodec`, `DateTimeCodec`, `DateCodec`, `DecimalCodec`, `DictCodec` *(see note)*, `FloatCodec`, `IntCodec`, `ListCodec` *(see note)*, `NoneCodec`, `SetCodec` *(see note)*, `StrCodec`, `TimeCodec`, `TupleCodec`.
|
|
658
670
|
|
|
659
671
|
> Scalar codec classes for containers (`ListCodec`, `DictCodec`, `SetCodec`) are re-exported only for completeness — they actually inherit from [`GPDCContainerCodec`](#gpdccontainercodec).
|
|
660
672
|
|
|
@@ -761,7 +773,7 @@ All public exceptions descend from the public `GPDCError(Exception)` root. Use `
|
|
|
761
773
|
class GPDCError(Exception)
|
|
762
774
|
```
|
|
763
775
|
|
|
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.
|
|
776
|
+
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
777
|
|
|
766
778
|
### `GPDCLockError`
|
|
767
779
|
|
|
@@ -38,7 +38,7 @@ Two value families exist, with different contracts:
|
|
|
38
38
|
|
|
39
39
|
---
|
|
40
40
|
|
|
41
|
-
## 2. The Registration
|
|
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 (
|
|
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 -->
|
|
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
|
|
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`) + `
|
|
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`, ~
|
|
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
|
|
|
@@ -391,12 +391,14 @@ class GPDCDataObject(name, namespace, *, canonical_name=None) # ABC
|
|
|
391
391
|
|---|---|---|
|
|
392
392
|
| `type` | `str` | GPDC 类型字符串,如 `"scalar:int"`、`"collection:list"`。 |
|
|
393
393
|
| `meta` | `ObjectMeta \| None` | 完整元信息记录;对象已消失时为 `None`。 |
|
|
394
|
-
| `cache_mode` | `str` |
|
|
394
|
+
| `cache_mode` | `str` | 生效的缓存模式。 |
|
|
395
395
|
| `ttl` | `int` | 声明的 TTL(秒),`0` 表示永久。 |
|
|
396
396
|
| `created_at` | `datetime \| None` | 创建时间戳。 |
|
|
397
397
|
| `updated_at` | `datetime \| None` | 最近更新时间戳。 |
|
|
398
398
|
| `value` | `object` | Python 值(抽象;由子类实现)。 |
|
|
399
399
|
|
|
400
|
+
> 元信息支撑的属性(`cache_mode`、`ttl`、`created_at`、`updated_at`)在对象元信息已不存在时都会抛出 `ObjectGoneError`。
|
|
401
|
+
|
|
400
402
|
**生命周期方法**
|
|
401
403
|
|
|
402
404
|
> 本节是 API 摘要。完整的模型 —— 策略来源、滑动刷新、所有权、匿名/引用对象的生命周期与陷阱 —— 见[生命周期管理](./lifecycle.md)。
|
|
@@ -409,6 +411,7 @@ class GPDCDataObject(name, namespace, *, canonical_name=None) # ABC
|
|
|
409
411
|
| `persist()` | 切换为 `cache_mode="permanent"` 且 `ttl=0`。需要 ownership。 |
|
|
410
412
|
| `update_cache_mode_and_ttl(cache_mode, ttl)` | 替换整个缓存策略。需要 ownership。 |
|
|
411
413
|
| `refresh_ttl()` | 将对象自身的 TTL 重新应用到其 key,并级联刷新其拥有的子树(用于滑动窗口刷新路径)。永久对象为空操作。 |
|
|
414
|
+
| `remaining_ttl()` | canonical key 上的剩余 TTL(秒),直接来自 Redis `TTL` 命令(`-1` = 无过期,`-2` = key 不存在)。 |
|
|
412
415
|
| `take_ownership()` | 把**被引用对象**提升为自持(主要用例:指向匿名嵌套对象的命名引用)。对命名(已自持)对象的引用是正确的 no-op。调用本身必须经由命名引用句柄发起。见[生命周期 §7](./lifecycle.md#7-the-ownership-model)。 |
|
|
413
416
|
|
|
414
417
|
**加锁方法**(可选使用,变更操作永远不会自动调用)
|
|
@@ -440,7 +443,7 @@ class GPDCScalarObject(name, namespace, *, canonical_name=None) # GPDCDataObje
|
|
|
440
443
|
- `value` 的 getter 通过已注册的 codec 解码存储的值。
|
|
441
444
|
- `value` 的 setter 会校验 Python 类型是否与已注册 codec 匹配,更新元信息,并在 `cache_mode="sliding"` 时刷新 TTL。
|
|
442
445
|
|
|
443
|
-
内建子类:[`IntObject`](#intobject)、[`FloatObject`](#floatobject)、[`StrObject`](#strobject)、[`BoolObject`](#boolobject)、[`DateTimeObject`](#datetimeobject)、[`DateObject`](#dateobject)、[`TimeObject`](#timeobject)、[`NoneObject`](#noneobject)、[`TupleObject`](#tupleobject)。
|
|
446
|
+
内建子类:[`IntObject`](#intobject)、[`FloatObject`](#floatobject)、[`StrObject`](#strobject)、[`BoolObject`](#boolobject)、[`DateTimeObject`](#datetimeobject)、[`DateObject`](#dateobject)、[`TimeObject`](#timeobject)、[`DecimalObject`](#decimalobject)、[`NoneObject`](#noneobject)、[`TupleObject`](#tupleobject)。
|
|
444
447
|
|
|
445
448
|
---
|
|
446
449
|
|
|
@@ -486,6 +489,7 @@ class GPDCContainerObject(name, namespace, *, canonical_name=None) # GPDCDataO
|
|
|
486
489
|
| [`DateTimeObject`](#datetimeobject) | `scalar:datetime` | `datetime.datetime` | `datetime:{isoformat}` |
|
|
487
490
|
| [`DateObject`](#dateobject) | `scalar:date` | `datetime.date`(不含 `datetime`) | `date:{isoformat}` |
|
|
488
491
|
| [`TimeObject`](#timeobject) | `scalar:time` | `datetime.time` | `time:{isoformat}` |
|
|
492
|
+
| [`DecimalObject`](#decimalobject) | `scalar:decimal` | `decimal.Decimal` | `decimal:{str(d)}` |
|
|
489
493
|
| [`NoneObject`](#noneobject) | `scalar:none` | `NoneType` | `none:None` |
|
|
490
494
|
| [`TupleObject`](#tupleobject) | `scalar:tuple` | `tuple`(仅标量元素) | `tuple:{json}` |
|
|
491
495
|
|
|
@@ -517,6 +521,14 @@ class GPDCContainerObject(name, namespace, *, canonical_name=None) # GPDCDataO
|
|
|
517
521
|
|
|
518
522
|
封装 `datetime.time`。
|
|
519
523
|
|
|
524
|
+
### `DecimalObject`
|
|
525
|
+
|
|
526
|
+
封装 `decimal.Decimal`(0.2.1 新增)。编码为 `decimal:{str(value)}`:payload 为 `str(Decimal)`,精确保留系数与指数 —— `Decimal("1.10")` 往返后仍是 `1.10` 而非 `1.1`,极大/极小指数使用科学计数法(`1E-27`)。解码遇到非法 payload 抛出 `ValueError`。
|
|
527
|
+
|
|
528
|
+
- **非有限值**(`NaN`、`sNaN`、`±Infinity`)与 `-0` 原样存取,与 `float` codec 对 `nan`/`inf` 的处理一致。比较语义遵循标准库:`NaN != NaN`,对 `sNaN` 做 `==` 比较会抛出 `decimal.InvalidOperation`。
|
|
529
|
+
- **精确性**:`str()` 与上下文无关 —— 存取过程不做任何舍入。若想表达十进制字面量,请用字符串构造(`Decimal("0.1")`);`Decimal(0.1)` 会忠实存储完整的二进制展开。
|
|
530
|
+
- **跨类型相等性**:GPDC 按 wire 编码形式匹配字典键与集合成员,因此原生命中相等的不同数值类型在存储后是不同条目 —— 先 `d[1] = "a"` 再 `d[Decimal(1)] = "b"` 会得到两个条目(`1` 与 `1.0` 之间早就是如此)。而原生字面量 `{1: "a", Decimal(1): "b"}` 在 GPDC 接触之前就已坍缩为一个键。
|
|
531
|
+
|
|
520
532
|
### `NoneObject`
|
|
521
533
|
|
|
522
534
|
封装 `None`。用于将“值为 `None`”与“键不存在”区分开。
|
|
@@ -614,7 +626,7 @@ class GPDCScalarCodec(GPDCCodec)
|
|
|
614
626
|
|---|---|
|
|
615
627
|
| `_parse_encoded(encoded) -> tuple[str, str]` | 将 `name:payload` 形式的编码值拆分为 `(type_name, raw)`。输入为空或不含冒号时抛 `ValueError`。 |
|
|
616
628
|
|
|
617
|
-
内建子类:`BoolCodec`、`DateTimeCodec`、`DateCodec`、`DictCodec` *(见下注)*、`FloatCodec`、`IntCodec`、`ListCodec` *(见下注)*、`NoneCodec`、`SetCodec` *(见下注)*、`StrCodec`、`TimeCodec`、`TupleCodec`。
|
|
629
|
+
内建子类:`BoolCodec`、`DateTimeCodec`、`DateCodec`、`DecimalCodec`、`DictCodec` *(见下注)*、`FloatCodec`、`IntCodec`、`ListCodec` *(见下注)*、`NoneCodec`、`SetCodec` *(见下注)*、`StrCodec`、`TimeCodec`、`TupleCodec`。
|
|
618
630
|
|
|
619
631
|
> 容器的标量 codec 类(`ListCodec`、`DictCodec`、`SetCodec`)被再次导出仅为完整性 —— 它们实际上继承自 [`GPDCContainerCodec`](#gpdccontainercodec)。
|
|
620
632
|
|
|
@@ -719,7 +731,7 @@ def validate_object_name(name: str) -> None
|
|
|
719
731
|
class GPDCError(Exception)
|
|
720
732
|
```
|
|
721
733
|
|
|
722
|
-
gpdatacached
|
|
734
|
+
gpdatacached 异常层次的根基类。本库抛出的每一个自定义异常都是 `GPDCError` 的子类,因此 `except GPDCError` 是最宽泛的全捕获。(普通的标准库异常 —— 如类型不支持或参数顺序错误抛出的 `TypeError`、非法 payload 抛出的 `ValueError` —— 按原样抛出。)当你需要区分不同失败模式时,优先捕获具体的子类。
|
|
723
735
|
|
|
724
736
|
### `GPDCLockError`
|
|
725
737
|
|
|
@@ -38,7 +38,7 @@ graph LR
|
|
|
38
38
|
|
|
39
39
|
---
|
|
40
40
|
|
|
41
|
-
## 2.
|
|
41
|
+
## 2. 注册条目
|
|
42
42
|
|
|
43
43
|
注册一个类型,永远是通过一次调用把四样东西配对:
|
|
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
|
+
两个**相互独立**的根:`GPDCCodec`(标量)与 `GPDCContainerCodec`(容器)是兄弟关系 —— 容器 codec 永远不实现 `encode_value`/`decode_value`。
|
|
100
|
+
|
|
100
101
|
### `GPDCCodec` —— 根 ABC
|
|
101
102
|
|
|
102
103
|
```python
|
|
@@ -194,7 +194,7 @@ graph TD
|
|
|
194
194
|
|
|
195
195
|
---
|
|
196
196
|
|
|
197
|
-
## 7.
|
|
197
|
+
## 7. 所有权模型 { #7-the-ownership-model }
|
|
198
198
|
|
|
199
199
|
每个对象在元信息中有一个 `owner` 字段。所有权决定谁能控制对象的生命周期,也是匿名对象运作的核心。
|
|
200
200
|
|
|
@@ -307,7 +307,7 @@ ns["alias"] = ns["original"] # 两个名字现在都解析到同一份 canonic
|
|
|
307
307
|
| 别名(`del ns["alias"]`) | 移除 `obj:alias` 字符串 + canonical owner 引用计数减一。若 canonical owner 是匿名的且这是其最后一个引用者,则级联删除该匿名对象。 |
|
|
308
308
|
| 在存在别名时删除 canonical owner(`del ns["original"]`) | **抛出 `CanonicalObjectReferencedError`。** 你必须先删除别名。 |
|
|
309
309
|
|
|
310
|
-
|
|
310
|
+
这种保护是有意为之:静默删除其他名字仍指向的数据会是个陷阱。该异常的 `.referrers` 字段会告诉你需要清理哪些绑定。
|
|
311
311
|
|
|
312
312
|
### 引用绑定自身的生命周期
|
|
313
313
|
|
|
@@ -362,7 +362,7 @@ domain.run_gc("analytics") # 单个 namespace
|
|
|
362
362
|
| `take_ownership()` | 把**被引用对象**提升为自持(主要用例:指向匿名嵌套对象的命名引用;对已自持的命名对象的引用是正确的 no-op)。 |
|
|
363
363
|
| `ttl`(属性) | 声明的 TTL(秒)。 |
|
|
364
364
|
| `cache_mode`(属性) | 当前缓存模式。 |
|
|
365
|
-
| `remaining_ttl()` | Redis 报告的实际剩余 TTL
|
|
365
|
+
| `remaining_ttl()` | Redis 报告的实际剩余 TTL(0;-1 = 无过期;-2 = key 已不存在)。 |
|
|
366
366
|
| `created_at` / `updated_at`(属性) | 元信息中的时间戳。 |
|
|
367
367
|
|
|
368
368
|
外加 domain 级的 `domain.run_gc(namespace=None)`,用于手动触发 GC。
|
|
@@ -71,7 +71,7 @@ graph TD
|
|
|
71
71
|
end
|
|
72
72
|
subgraph L4["第 4 层 —— 内建类型"]
|
|
73
73
|
BREG["builtins.registry<br/>(导入时自动注册)"]
|
|
74
|
-
SCALARS["标量类型<br/>int·float·str·bool·datetime·date·time·none·tuple"]
|
|
74
|
+
SCALARS["标量类型<br/>int·float·decimal·str·bool·datetime·date·time·none·tuple"]
|
|
75
75
|
CONTAINERS["容器类型<br/>list·dict·set"]
|
|
76
76
|
end
|
|
77
77
|
subgraph L3["第 3 层 —— 类型系统"]
|
|
@@ -92,7 +92,7 @@ graph TD
|
|
|
92
92
|
EXC["exceptions"]
|
|
93
93
|
end
|
|
94
94
|
REDIS[("Redis")]
|
|
95
|
-
L5 -->
|
|
95
|
+
L5 --> L3
|
|
96
96
|
L4 --> L3
|
|
97
97
|
L3 --> L2
|
|
98
98
|
L3 --> L1
|
|
@@ -38,7 +38,7 @@ pip install "gpdatacached[pandas]"
|
|
|
38
38
|
|---|---|
|
|
39
39
|
| **行索引** | `RangeIndex`(默认整数索引,紧凑存储)**以及**任意带标签索引,包括 `pd.MultiIndex`(按每个 level 存一个 `collection:list` 子对象)。`.index_names` 始终返回元组。 |
|
|
40
40
|
| **DataFrame 列** | 单层(标量名称)**或** `pd.MultiIndex`(元组名称)。行轴与列轴相互独立,可组合(DataFrame 可同时拥有行 MultiIndex 与列 MultiIndex)。 |
|
|
41
|
-
| **值 dtype** | `int64`/`Int64`、`float64`/`Float64`、`bool`/`boolean
|
|
41
|
+
| **值 dtype** | 任意宽度的整数/浮点/布尔 dtype(如 `int64`/`Int64`、`float64`/`Float64`、`bool`/`boolean`)、`str`/`string`、`object`(任何可标量编码的元素 —— 含 0.2.1 起内建的 `decimal.Decimal`)、`datetime64`(naive 与 tz-aware,任意单位)、`category`。 |
|
|
42
42
|
| **变更** | 行 `extend`(Series 与 DataFrame);DataFrame 的列 `__setitem__` / `__delitem__`(结构性的新增/替换/删除)。 |
|
|
43
43
|
|
|
44
44
|
说明:
|
|
@@ -114,7 +114,7 @@ domain.close()
|
|
|
114
114
|
| `.loc[key]` | O(N),选择性 | 深度 k 的前缀标签扫描;仅解码匹配的行,但会读取所有索引 level —— **不要放进热循环** |
|
|
115
115
|
| `df[col]` / `df[tuple]` | O(N),单列 | (DataFrame)单列,返回 `pd.Series` |
|
|
116
116
|
| `df[level0]` / `df[list]` | O(N_cols) + O(匹配数·N) | 列轴选择(成本低;列数少) |
|
|
117
|
-
| `.value` | **完整还原**(读取所有列 + 所有索引 level,重建整个 frame —— 尽管同属 `O(N)` 量级,但远比选择性 `.loc`
|
|
117
|
+
| `.value` | **完整还原**(读取所有列 + 所有索引 level,重建整个 frame —— 尽管同属 `O(N)` 量级,但远比选择性 `.loc` 重得多) | 整个 `pd.Series` / `pd.DataFrame`;最终退路 |
|
|
118
118
|
|
|
119
119
|
**决策规则:**
|
|
120
120
|
|
|
@@ -185,7 +185,7 @@ GPDC 类型 `collection:pandas_dataframe`。底层是一个 Redis hash:列值
|
|
|
185
185
|
|
|
186
186
|
| 方法 | 说明 |
|
|
187
187
|
|---|---|
|
|
188
|
-
| `extend(other)` | 原地追加行。接受 `pd.DataFrame`、list 的 dict,或 dict 行的列表。对于真正的 MultiIndex 行 DataFrame(`L ≥ 2`),只接受带匹配 MultiIndex 的 `DataFrame`(dict/list 输入会合成 MultiIndex 无法提供的位置标签 → `TypeError`;请使用 `.value
|
|
188
|
+
| `extend(other)` | 原地追加行。接受 `pd.DataFrame`、list 的 dict,或 dict 行的列表。对于真正的 MultiIndex 行 DataFrame(`L ≥ 2`),只接受带匹配 MultiIndex 的 `DataFrame`(dict/list 输入会合成 MultiIndex 无法提供的位置标签 → `TypeError`;请使用 `.value`)。category 列会拒绝不在既有分类中的值(`ValueError`,发生在任何 Redis 写入之前)。修改 Redis;返回 `None`。 |
|
|
189
189
|
| `deepcopy()` | 返回完全独立的 `pd.DataFrame`(`.value.copy(deep=True)`)。 |
|
|
190
190
|
|
|
191
191
|
**列轴访问(单层列)**
|
|
@@ -195,7 +195,7 @@ GPDC 类型 `collection:pandas_dataframe`。底层是一个 Redis hash:列值
|
|
|
195
195
|
| `df[col]` | 选取单列,返回 `pd.Series`。 |
|
|
196
196
|
| `df[list_of_keys]` | 由所选列组成的子 `pd.DataFrame`(即便只有一个 key 也始终返回子 DataFrame)。 |
|
|
197
197
|
| `df[col] = values` | 新增或替换列。`len(values)` 必须等于当前行数。对空 DataFrame 通过新增列来引入行会被拒绝。 |
|
|
198
|
-
| `del df[col]` |
|
|
198
|
+
| `del df[col]` | 删除列。共享的索引会被保留(即便删到最后一列)。 |
|
|
199
199
|
| `col in df` | 列存在时为 `True`。 |
|
|
200
200
|
| `iter(df)` | 遍历列名。 |
|
|
201
201
|
|
|
@@ -248,4 +248,4 @@ class UnsupportedDataFrameError(TypeError)
|
|
|
248
248
|
class UnsupportedElementTypeError(TypeError)
|
|
249
249
|
```
|
|
250
250
|
|
|
251
|
-
当某个 Series/DataFrame 元素无法被 `GPDCScalarCodec` 子类编码时抛出 —— 即它是容器类型(`list`/`dict`/`set`/容器注册的 pydantic 模型)或不支持的类型。Series/DataFrame 元素必须是能被 `GPDCScalarCodec`
|
|
251
|
+
当某个 Series/DataFrame 元素无法被 `GPDCScalarCodec` 子类编码时抛出 —— 即它是容器类型(`list`/`dict`/`set`/容器注册的 pydantic 模型)或不支持的类型。Series/DataFrame 元素必须是能被 `GPDCScalarCodec` 子类编码的标量类型。错误信息会列举允许的标量类型。允许的内建标量(0.2.1+):int/float/bool/str/datetime/date/time/decimal/tuple/None,外加任意标量注册的自定义类型;object 列与索引标签可持有其中任何一种 —— 包括 `decimal.Decimal`。
|
|
@@ -36,7 +36,7 @@ graph TD
|
|
|
36
36
|
| `ns[k]`(标量读) | O(1),约 3 RTT | `_resolve_binding`(`TYPE` + `HGETALL`)**再加**标量 `.value` getter 重新加载 meta(`HGETALL`)。 |
|
|
37
37
|
| `ns[k] = [a, b, c]`(扁平、元素为标量的容器) | O(N) 本地编码,数据写入 **O(1)** RTT | 一次 `encode_session` + 一次批量 `create_raw`(单条 `RPUSH`/`HSET`/`SADD` 携带全部元素)+ `HGET` ×2(namespace 默认策略)+ 元信息 `HSET` + 对 obj/raw/ref key 的 TTL 应用(共约 8 RTT,与 N 无关,对标量元素成立)。 |
|
|
38
38
|
| `ns[k] = [[...], ...]`(嵌套容器) | O(**嵌套容器数量**) RTT + O(N) 本地 | 每个嵌套容器成为一个匿名对象(meta `HSET` + 自身 `create_raw` + `HINCRBY` 引用 + TTL 应用,每个约 5–6 RTT)。**深度不乘** —— 决定规模的是总节点数。 |
|
|
39
|
-
| `container.append(x)` / `container[k] = v`(标量元素、非 sliding) | O(1),约 5–6 RTT | `HGETALL
|
|
39
|
+
| `container.append(x)` / `container[k] = v`(标量元素、非 sliding) | O(1),约 5–6 RTT | `HGETALL` ×2(存活/策略检查 + data-owner 解析)+ 数据写入(`RPUSH`/`HSET`)+ `LLEN`/`HLEN` + 元信息 `HSET`。**若 `cache_mode="sliding"` 再加子树遍历**(§3);**若元素本身是容器,再加约 5–6 RTT**(创建匿名对象)。 |
|
|
40
40
|
| `iter(ns)` / `len(ns)` | O(N),批量 SCAN | 每 ~100 个 key 一页若干 RTT(`len` 委托给 `iter`)。不要放进热循环。 |
|
|
41
41
|
| `k in ns` | O(1),1–4 RTT | 直接对该具名 key 做 `TYPE` + `HGETALL`(owner 绑定 1–2 RTT);引用会再加一次 `GET` + 另一次 `TYPE`(别名最多 4 RTT)。**不是** SCAN。不随 namespace 大小而增长。 |
|
|
42
42
|
| `del ns[k]`(容器 owner) | O(所属子树大小) | `_teardown_owner_container` 释放每个嵌套引用(`HDEL`)并级联删除成为孤儿的匿名子对象;每节点约 1+ RTT。若仍存在活跃的别名引用者则抛 `CanonicalObjectReferencedError` —— 先删别名。 |
|
|
@@ -48,7 +48,7 @@ graph TD
|
|
|
48
48
|
| `.value`(完整容器读取) | 扁平:1 RTT + O(N) 本地;嵌套:+O(嵌套数) RTT | 一次批量读(`LRANGE`/`HGETALL`/`SMEMBERS`);每个 `ref:` 元素再加一次 `HGETALL` + 解码其匿名对象。 |
|
|
49
49
|
| `name in cache`(`GPDataCache`) | **本地,0 RTT** | 纯配置快照检查,通过 `folder.list_configs()` 实现。不触碰 Redis,也不会构造 domain。是本库中最便宜的成员测试。 |
|
|
50
50
|
| `cache.get_domain(name)` | 首次调用:domain 构造(读/校验 domain meta);后续:**0 RTT** | 首次访问时经由 gpconfig 的工厂构造并缓存 `GPDCDomain`;之后的调用返回已缓存实例。不会重新读取配置文件。 |
|
|
51
|
-
| `cache.get_object_by_uri(uri)` | 根:约 4–5 RTT;**每条路径分段 +1 RTT** | `get_namespace`(`SISMEMBER` + namespace-meta `HGETALL`,约
|
|
51
|
+
| `cache.get_object_by_uri(uri)` | 根:约 4–5 RTT;**每条路径分段 +1–2 RTT** | `get_namespace`(`SISMEMBER` + namespace-meta `HGETALL` + 幂等 `SADD`,约 3 RTT)+ 根对象解析(`TYPE` + `HGETALL`,约 2–3 RTT)。每条额外路径分段是一次 `__getitem__` 读取(ListObject:`LLEN` + `LINDEX` 共 2 RTT;DictObject:`HGET` 1 RTT)。`cache.has_uri(uri)` 成本相同(它用 try/except 调用 `get_object_by_uri`)。domain 仅在首次访问时构造;同一 domain 之后的 URI 会复用已缓存的 domain。 |
|
|
52
52
|
|
|
53
53
|
---
|
|
54
54
|
|
|
@@ -134,7 +134,7 @@ domain.close()
|
|
|
134
134
|
|
|
135
135
|
整个对象随后以 `{type_prefix}:{json}` 的形式存储,其中 `type_prefix` 是 GPDC 类型中 `scalar:` 之后的部分。
|
|
136
136
|
|
|
137
|
-
**字段类型限制。** 每个字段都必须能被某个 `GPDCScalarCodec` 子类编码(int / float / bool / str / datetime / date / time / tuple / None / 任意标量注册的自定义类型)。容器字段类型(`list`、`dict`、`set`、容器注册的 pydantic 模型)在编码时会抛出 `TypeError` —— 请改用容器模式。
|
|
137
|
+
**字段类型限制。** 每个字段都必须能被某个 `GPDCScalarCodec` 子类编码(int / float / bool / str / datetime / date / time / decimal / tuple / None / 任意标量注册的自定义类型)。容器字段类型(`list`、`dict`、`set`、容器注册的 pydantic 模型)在编码时会抛出 `TypeError` —— 请改用容器模式。
|
|
138
138
|
|
|
139
139
|
### `PydanticModelScalarCodec`
|
|
140
140
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Software library version (SemVer), tracks feature releases and bugfixes.
|
|
4
4
|
# Independent of GPDC_PROTOCOL_VERSION in domain.py.
|
|
5
|
-
__version__ = "0.2.
|
|
5
|
+
__version__ = "0.2.1"
|
|
6
6
|
|
|
7
7
|
from .codec import GPDCCodec, GPDCContainerCodec, GPDCScalarCodec
|
|
8
8
|
from .config import GPDCDomainConfig, GPDCNamespaceConfig
|
|
@@ -26,6 +26,7 @@ from .container import GPDCContainerObject
|
|
|
26
26
|
from .data_cache import GPDataCache
|
|
27
27
|
from .builtins.bool_object import BoolObject
|
|
28
28
|
from .builtins.datetime_object import DateObject, DateTimeObject, TimeObject
|
|
29
|
+
from .builtins.decimal_object import DecimalObject
|
|
29
30
|
from .builtins.dict_object import DictObject
|
|
30
31
|
from .builtins.float_object import FloatObject
|
|
31
32
|
from .builtins.int_object import IntObject
|
|
@@ -45,6 +46,7 @@ _GPConfigManager.register_config_class(GPDCDomainConfig)
|
|
|
45
46
|
|
|
46
47
|
from .builtins.bool_object import BoolCodec as BoolCodec # noqa: F401,E402
|
|
47
48
|
from .builtins.datetime_object import DateCodec as DateCodec, DateTimeCodec as DateTimeCodec, TimeCodec as TimeCodec # noqa: F401,E402
|
|
49
|
+
from .builtins.decimal_object import DecimalCodec as DecimalCodec # noqa: F401,E402
|
|
48
50
|
from .builtins.dict_object import DictCodec as DictCodec # noqa: F401,E402
|
|
49
51
|
from .builtins.float_object import FloatCodec as FloatCodec # noqa: F401,E402
|
|
50
52
|
from .builtins.int_object import IntCodec as IntCodec # noqa: F401,E402
|
|
@@ -78,6 +80,7 @@ __all__ = [
|
|
|
78
80
|
"DateTimeObject",
|
|
79
81
|
"DateObject",
|
|
80
82
|
"TimeObject",
|
|
83
|
+
"DecimalObject",
|
|
81
84
|
"NoneObject",
|
|
82
85
|
"ListObject",
|
|
83
86
|
"DictObject",
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal, InvalidOperation
|
|
4
|
+
|
|
5
|
+
from ..codec import GPDCScalarCodec
|
|
6
|
+
from ..scalar import GPDCScalarObject
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DecimalCodec(GPDCScalarCodec):
|
|
10
|
+
"""Lossless decimal.Decimal codec: payload is ``str(value)``.
|
|
11
|
+
|
|
12
|
+
``str(Decimal)`` preserves coefficient/exponent exactly (``Decimal("1.10")``
|
|
13
|
+
stays ``1.10``, extreme exponents use scientific notation), and
|
|
14
|
+
``Decimal(raw)`` restores it byte-for-byte. Non-finite values (NaN, sNaN,
|
|
15
|
+
±Infinity) and ``-0`` roundtrip too — same policy as float's nan/inf.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def encode_value(cls, value: object) -> str:
|
|
20
|
+
if not isinstance(value, Decimal):
|
|
21
|
+
raise TypeError(f"DecimalCodec cannot encode {type(value).__name__!r}")
|
|
22
|
+
return f"decimal:{value}"
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def decode_value(cls, encoded: str) -> object:
|
|
26
|
+
type_name, raw = cls._parse_encoded(encoded)
|
|
27
|
+
if type_name != "decimal":
|
|
28
|
+
raise ValueError(f"DecimalCodec cannot decode type {type_name!r}")
|
|
29
|
+
try:
|
|
30
|
+
return Decimal(raw)
|
|
31
|
+
except InvalidOperation:
|
|
32
|
+
# Decimal("garbage") raises decimal.InvalidOperation (an
|
|
33
|
+
# ArithmeticError), not ValueError like int()/float() — normalise
|
|
34
|
+
# to the error type every other scalar codec raises.
|
|
35
|
+
raise ValueError(f"Invalid scalar value for type 'decimal': {raw!r}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DecimalObject(GPDCScalarObject):
|
|
39
|
+
TYPE = "scalar:decimal"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import datetime
|
|
4
|
+
from decimal import Decimal
|
|
4
5
|
|
|
5
6
|
from ..types import GPDCTypeRegistry
|
|
6
7
|
from .bool_object import BoolCodec, BoolObject
|
|
@@ -12,6 +13,7 @@ from .datetime_object import (
|
|
|
12
13
|
TimeCodec,
|
|
13
14
|
TimeObject,
|
|
14
15
|
)
|
|
16
|
+
from .decimal_object import DecimalCodec, DecimalObject
|
|
15
17
|
from .dict_object import DictCodec, DictObject
|
|
16
18
|
from .float_object import FloatCodec, FloatObject
|
|
17
19
|
from .int_object import IntCodec, IntObject
|
|
@@ -51,6 +53,15 @@ def _register_builtin_types() -> None:
|
|
|
51
53
|
codec_class=FloatCodec,
|
|
52
54
|
priority=0,
|
|
53
55
|
)
|
|
56
|
+
# Decimal is not an int/float/str subclass, so the default priority
|
|
57
|
+
# cannot be shadowed by (or shadow) another builtin entry.
|
|
58
|
+
GPDCTypeRegistry.register(
|
|
59
|
+
python_type=Decimal,
|
|
60
|
+
gpdc_type="scalar:decimal",
|
|
61
|
+
object_class=DecimalObject,
|
|
62
|
+
codec_class=DecimalCodec,
|
|
63
|
+
priority=0,
|
|
64
|
+
)
|
|
54
65
|
GPDCTypeRegistry.register(
|
|
55
66
|
python_type=str,
|
|
56
67
|
gpdc_type="scalar:str",
|
|
@@ -106,7 +106,7 @@ class UnsupportedElementTypeError(TypeError):
|
|
|
106
106
|
|
|
107
107
|
_GUIDANCE = (
|
|
108
108
|
"\nSeries/DataFrame elements must be scalar types encodable by a "
|
|
109
|
-
"GPDCScalarCodec subclass (int/float/bool/str/datetime/date/time/tuple/None, or a "
|
|
109
|
+
"GPDCScalarCodec subclass (int/float/bool/str/datetime/date/time/decimal/tuple/None, or a "
|
|
110
110
|
"scalar-registered custom type). Container types are not allowed as elements."
|
|
111
111
|
)
|
|
112
112
|
|
|
@@ -37,7 +37,7 @@ def make_raw_key(domain: str, namespace: str, name: str) -> str:
|
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
def make_ref_key(domain: str, namespace: str, name: str) -> str:
|
|
40
|
-
"""Return the Redis key for reference tracking (Redis
|
|
40
|
+
"""Return the Redis key for reference tracking (Redis hash)."""
|
|
41
41
|
return f"gpdc:{domain}:{namespace}:ref:{name}"
|
|
42
42
|
|
|
43
43
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|