gpdatacached 0.2.1__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.
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/.gitignore +3 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/CHANGELOG.md +53 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/PKG-INFO +2 -2
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/docs/en/api.md +47 -12
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/docs/zh/api.md +23 -6
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/pyproject.toml +1 -1
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/__init__.py +12 -2
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/data_cache.py +114 -28
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/domain.py +16 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/keys.py +29 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/LICENSE +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/README.md +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/README.zh-CN.md +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/docs/en/extensibility.md +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/docs/en/index.md +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/docs/en/lifecycle.md +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/docs/en/locking.md +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/docs/en/overview.md +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/docs/en/pandas-support.md +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/docs/en/performance-guide.md +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/docs/en/pydantic-support.md +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/docs/zh/extensibility.md +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/docs/zh/index.md +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/docs/zh/lifecycle.md +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/docs/zh/locking.md +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/docs/zh/overview.md +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/docs/zh/pandas-support.md +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/docs/zh/performance-guide.md +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/docs/zh/pydantic-support.md +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/builtins/__init__.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/builtins/bool_object.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/builtins/datetime_object.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/builtins/decimal_object.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/builtins/dict_object.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/builtins/float_object.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/builtins/int_object.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/builtins/list_object.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/builtins/none_object.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/builtins/registry.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/builtins/set_object.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/builtins/str_object.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/builtins/tuple_object.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/codec.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/config.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/container.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/exceptions.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/extensions/__init__.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/extensions/pandas_support.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/extensions/pydantic_support.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/gc.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/lifecycle.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/locking.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/meta.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/namespace.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/object.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/refset.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/scalar.py +0 -0
- {gpdatacached-0.2.1 → gpdatacached-0.3.0}/src/gpdatacached/types.py +0 -0
|
@@ -8,6 +8,52 @@ 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
|
+
|
|
11
57
|
## [0.2.1] — 2026-10-12
|
|
12
58
|
|
|
13
59
|
### Added
|
|
@@ -25,6 +71,13 @@ GitHub Releases (with download artifacts) are published at
|
|
|
25
71
|
natively-equal values of different numeric types (`1` vs `Decimal(1)`) are
|
|
26
72
|
distinct GPDC dict keys/set members, as `1` vs `1.0` already were.
|
|
27
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
|
+
|
|
28
81
|
### Documentation
|
|
29
82
|
|
|
30
83
|
- Full docs-vs-code synchronization pass across `docs/` (English + zh-CN
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: gpdatacached
|
|
3
|
-
Version: 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.
|
|
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
|
|
@@ -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")
|
|
@@ -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
|
|
300
|
-
|
|
301
|
-
[`get_domain`](#gpdatacache) access, and cached in
|
|
302
|
-
is a snapshot: config files edited after
|
|
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
|
|
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.
|
|
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),
|
|
374
|
-
|
|
375
|
-
|
|
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
|
|
@@ -761,6 +782,20 @@ def validate_object_name(name: str) -> None
|
|
|
761
782
|
|
|
762
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.)
|
|
763
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
|
+
|
|
764
799
|
---
|
|
765
800
|
|
|
766
801
|
## Exceptions
|
|
@@ -107,6 +107,7 @@ folder = mgr.get_config("gpdc") # -> GPConfigFolder;示例布局见 tes
|
|
|
107
107
|
with GPDataCache(folder) as cache:
|
|
108
108
|
"core_domain" in cache # 存在对应配置时为 True(不触碰 Redis)
|
|
109
109
|
domain = cache.get_domain("core_domain") # 惰性构造 + 缓存
|
|
110
|
+
site = cache.get_domain("domain_name.com") # 带点名字 -> domain_name@com.yaml
|
|
110
111
|
|
|
111
112
|
# 3. 通过 GPDC URI 解析对象 —— 包括匿名嵌套对象。
|
|
112
113
|
ns = domain.get_namespace("core_app")
|
|
@@ -209,7 +210,7 @@ class GPDCNamespace(name, domain, *, config=None) # MutableMapping
|
|
|
209
210
|
|
|
210
211
|
`GPDCDomain` 内部的逻辑隔离空间,完整实现了由 Redis 支撑的 `MutableMapping` 协议。
|
|
211
212
|
|
|
212
|
-
**命名规则:** 名称只能使用字母、数字、`.` 和 `_`;形如 `__xxx__` 的名称被保留。匿名对象(内部名称以 `~` 开头)不会出现在公开的映射 API 中。常见的**被拒绝**字符(不在允许清单内;且 `:` 还会与 Redis key 分隔符冲突):连字符 `-`、空格、`:` —— 请改用 `_` 或
|
|
213
|
+
**命名规则:** 名称只能使用字母、数字、`.` 和 `_`;形如 `__xxx__` 的名称被保留。匿名对象(内部名称以 `~` 开头)不会出现在公开的映射 API 中。常见的**被拒绝**字符(不在允许清单内;且 `:` 还会与 Redis key 分隔符冲突):连字符 `-`、空格、`:` —— 请改用 `_` 或 `.`。该规则适用于**对象名与 namespace 名**,也适用于 **domain 名**(domain 名中的 `.` 在 GPDC 内部处处合法 —— 但当 domain 由配置文件经 `GPDataCache` 驱动时,文件名中的 `.` 必须写作 `@`,见 [`GPDataCache` 的 `@` 转义](#gpdatacache))。
|
|
213
214
|
|
|
214
215
|
**属性**
|
|
215
216
|
|
|
@@ -286,15 +287,17 @@ class GPDataCache(folder: gpconfig.GPConfigFolder) # 上下文管理器
|
|
|
286
287
|
|
|
287
288
|
一个门面(facade),负责管理从 [`gpconfig`](https://github.com/linnetcodes/gpconfig) 的 `GPConfigFolder` 中发现的 `GPDCDomain` 实例,并将 GPDC URI 解析为 [`GPDCDataObject`](#gpdcdataobject) 实例。当你的 domain 由配置文件驱动、而非在代码中临时构造时,它是推荐的入口。
|
|
288
289
|
|
|
289
|
-
**构造。** 基于 `GPConfigFolder` 构造。构造时只会保存该文件夹的引用 —— **不会创建任何 domain
|
|
290
|
+
**构造。** 基于 `GPConfigFolder` 构造。构造时只会保存该文件夹的引用 —— **不会创建任何 domain**(但会扫描一次文件夹,对无法寻址的配置文件名发出告警,见下)。Domain 在首次通过 [`get_domain`](#gpdatacache) 访问时惰性构造,并缓存在一个内部 dict 中。该缓存是一个快照:构造之后被编辑的配置文件**不会**被重新读取;要应用变更需重建 `GPDataCache`。
|
|
290
291
|
|
|
291
292
|
`GPDataCache` **不会**创建缺少配置文件的 domain —— 临时 domain 请直接使用 [`GPDCDomain`](#gpdcdomain)。
|
|
292
293
|
|
|
294
|
+
> **Domain 名中的点号 —— `@` 转义。** gpconfig 以点分路径寻址配置,因此配置文件名中的 `.` 会被当作路径分隔符消费,该文件将无法被寻址。要使用带点的 domain 名 —— 当一个 domain 对应一个互联网域名时的自然形态,例如 `domain_name.com` —— 请在配置文件名中将每个 `.` 写作 `@`:`domain_name@com.yaml` 配置 domain `"domain_name.com"`,`cache.get_domain("domain_name.com")` / `cache.get_object_by_uri("gpdc://domain_name.com/ns/obj")` 会透明解析。该映射是无歧义的,因为 `@` 不是 GPDC 名称的合法字符。`GPDCDomain` 自身会对来自配置文件 stem 的名字做 `@` → `.` 还原,因此 gpconfig 的对象工厂路径同样可用:`folder.get_object("domain_name@com")` 构造出名为 `domain_name.com` 的 domain,与 `cache.get_domain("domain_name.com")` 完全一致。显式 `name=` 参数含 `@` 仍会被拒绝 —— 转义只服务于文件名,代码里直接写 `.` 即可。名字中含真实 `.` 的配置文件会被文件夹列出但永远无法加载;`GPDataCache` 在构造时会对这类文件记录一次告警。
|
|
295
|
+
|
|
293
296
|
> **注册副作用。** `import gpdatacached` 会将 `GPDCDomain` 和 `GPDCDomainConfig` 注册到 gpconfig 的对象工厂(幂等),这正是 `folder.get_object(name)` 能够构造 `GPDCDomain` 的原因。无需额外设置。
|
|
294
297
|
|
|
295
298
|
**支持的协议方法**
|
|
296
299
|
|
|
297
|
-
- `name in cache` →
|
|
300
|
+
- `name in cache` → 文件夹中存在 `name` 的配置时为 `True`(带点的名字会查找其 `@` 转义后的文件名)。这是纯配置快照检查:**不**触碰 Redis,也不构造(或要求存在)`GPDCDomain` 实例 —— 答案只取决于配置文件是否存在,与是否调用过 `get_domain(name)` 无关。domain 实例在首次 `get_domain(name)` 访问时惰性创建。
|
|
298
301
|
- `with cache as c` / `__exit__` → 退出时关闭所有已缓存的 domain;`close()` 是幂等的。
|
|
299
302
|
|
|
300
303
|
> **`close()` 后的行为 —— 成员检查的例外。** `cache.close()` 之后,触碰资源的方法(`get_domain`、`get_object_by_uri`、`has_uri`)会抛 `RuntimeError("GPDataCache is closed")`。`name in cache` 成员检查是**故意的例外**:因为它只读配置快照、不触碰任何资源,所以在 `close()` 后仍然可用。这是有意为之 —— "这个 domain 是否有配置?" 这个问题与 cache 是否打开无关。
|
|
@@ -303,7 +306,7 @@ class GPDataCache(folder: gpconfig.GPConfigFolder) # 上下文管理器
|
|
|
303
306
|
|
|
304
307
|
| 方法 | 说明 |
|
|
305
308
|
|---|---|
|
|
306
|
-
| `get_domain(name)` | 返回 `name` 对应的已缓存 `GPDCDomain
|
|
309
|
+
| `get_domain(name)` | 返回 `name` 对应的已缓存 `GPDCDomain`,首次访问时惰性构造。带点的名字会解析到其 `@` 转义后的配置文件(见上)。`name` 不是合法 domain 名时抛 `ValueError`;无对应配置时抛出 gpconfig 的 `ConfigNotFoundError`。`close()` 后抛 `RuntimeError`。 |
|
|
307
310
|
| [`get_object_by_uri(uri)`](#get_object_by_uri) | 将 GPDC URI 解析为 `GPDCDataObject`。`close()` 后抛 `RuntimeError`。 |
|
|
308
311
|
| `has_uri(uri)` | 当 `get_object_by_uri(uri)` 会成功时返回 `True`。`close()` 后抛 `RuntimeError`。 |
|
|
309
312
|
| `close()` | 关闭所有已缓存的 domain。幂等。 |
|
|
@@ -337,7 +340,7 @@ gpdc://{domain}/{namespace}/{name}[/{path_segment}...]
|
|
|
337
340
|
|
|
338
341
|
**标量终结点会被拒绝。** 嵌套在容器内部的标量以内联值形式存储(而非作为被引用对象),因此深入到一个标量元素得到的是原生 Python 值。由于 `get_object_by_uri` 只返回对象,终止于标量的路径会抛出 `ObjectNotFoundByURIError`。例如,给定 `ns["cfg"] = {"port": 8080}`,`gpdc://d/ns/cfg/port` 会抛错(值 `8080` 是标量,而非数据对象)。
|
|
339
342
|
|
|
340
|
-
**错误。** 每一种失败情况都会抛出 [`ObjectNotFoundByURIError`](#objectnotfoundbyurierror)(携带 `.uri` 和 `.reason`):URI 格式错误(缺少 `gpdc
|
|
343
|
+
**错误。** 每一种失败情况都会抛出 [`ObjectNotFoundByURIError`](#objectnotfoundbyurierror)(携带 `.uri` 和 `.reason`):URI 格式错误(缺少 `gpdc://`、分段少于三段、存在空分段)、非法的 domain/namespace/object 名(三个定位分段都会预先校验)、未知的 domain、缺少 namespace、缺少根对象、某个路径分段失败(`KeyError`/`IndexError`/`TypeError`),以及标量终结点。
|
|
341
344
|
|
|
342
345
|
**示例。** 给定 `ns["cfg"] = {"server": {"host": "x"}, "tags": ["a","b"]}`:
|
|
343
346
|
|
|
@@ -357,7 +360,7 @@ gpdc://{domain}/{namespace}/{name}[/{path_segment}...]
|
|
|
357
360
|
class ObjectNotFoundByURIError(GPDCError)
|
|
358
361
|
```
|
|
359
362
|
|
|
360
|
-
当 GPDC URI 无法解析为 `GPDCDataObject` 时,由 [`GPDataCache.get_object_by_uri`](#get_object_by_uri) 抛出。涵盖所有失败情况:URI
|
|
363
|
+
当 GPDC URI 无法解析为 `GPDCDataObject` 时,由 [`GPDataCache.get_object_by_uri`](#get_object_by_uri) 抛出。涵盖所有失败情况:URI 语法格式错误、非法的 domain/namespace/object 名、未知的 domain(无配置)、缺少 namespace、缺少根对象、某个路径分段失败,以及标量终结点。携带 `.uri`(URI 字符串)和 `.reason`(简短说明)。
|
|
361
364
|
|
|
362
365
|
`has_uri` 只会吞掉这个异常;基础设施错误(如 Redis 连接失败)会继续向上传播。
|
|
363
366
|
|
|
@@ -719,6 +722,20 @@ def validate_object_name(name: str) -> None
|
|
|
719
722
|
|
|
720
723
|
校验 `name` 仅包含字母、数字、`.` 或 `_`,且不是被保留的 `__xxx__` 名称。否则抛出 `ValueError`。(内部还存在等价的 domain 名称与 namespace 名称校验器。)
|
|
721
724
|
|
|
725
|
+
### Domain 名 `@` 转义辅助函数
|
|
726
|
+
|
|
727
|
+
```python
|
|
728
|
+
DOMAIN_DOT_ESCAPE: str # "@"
|
|
729
|
+
|
|
730
|
+
def escape_domain_name(name: str) -> str
|
|
731
|
+
# "domain_name.com" -> "domain_name@com" ('.' -> '@')
|
|
732
|
+
|
|
733
|
+
def unescape_domain_name(stem: str) -> str
|
|
734
|
+
# "domain_name@com" -> "domain_name.com" ('@' -> '.')
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
[`GPDataCache` `@` 转义约定](#gpdatacache)的辅助函数:gpconfig 以点分路径寻址配置,因此 domain 配置文件名中的 `.` 无法被寻址 —— domain 名中的每个 `.` 在文件名中写作 `@`。由于 `@` 不是 GPDC 名称的合法字符,该映射是单射的;同理 `unescape_domain_name` 可安全地作用于任何 stem 来源的名字。通常无需直接使用 —— `GPDataCache`(寻址时转义)与 `GPDCDomain`(加载时还原)会自动应用。
|
|
738
|
+
|
|
722
739
|
---
|
|
723
740
|
|
|
724
741
|
## 异常
|
|
@@ -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.
|
|
5
|
+
__version__ = "0.3.0"
|
|
6
6
|
|
|
7
7
|
from .codec import GPDCCodec, GPDCContainerCodec, GPDCScalarCodec
|
|
8
8
|
from .config import GPDCDomainConfig, GPDCNamespaceConfig
|
|
@@ -17,7 +17,14 @@ from .exceptions import (
|
|
|
17
17
|
ObjectNotFoundByURIError,
|
|
18
18
|
)
|
|
19
19
|
from .domain import GPDCDomain, reset_domain_config
|
|
20
|
-
from .keys import
|
|
20
|
+
from .keys import (
|
|
21
|
+
DOMAIN_DOT_ESCAPE,
|
|
22
|
+
escape_domain_name,
|
|
23
|
+
make_uri,
|
|
24
|
+
unescape_domain_name,
|
|
25
|
+
uri_to_redis_key,
|
|
26
|
+
validate_object_name,
|
|
27
|
+
)
|
|
21
28
|
from .namespace import GPDCNamespace
|
|
22
29
|
from .types import GPDCTypeRegistry
|
|
23
30
|
from .object import GPDCDataObject
|
|
@@ -91,6 +98,9 @@ __all__ = [
|
|
|
91
98
|
"GPDCContainerCodec",
|
|
92
99
|
"GPDCScalarCodec",
|
|
93
100
|
"GPDCTypeRegistry",
|
|
101
|
+
"DOMAIN_DOT_ESCAPE",
|
|
102
|
+
"escape_domain_name",
|
|
103
|
+
"unescape_domain_name",
|
|
94
104
|
"validate_object_name",
|
|
95
105
|
"make_uri",
|
|
96
106
|
"uri_to_redis_key",
|
|
@@ -8,10 +8,18 @@ from typing import TYPE_CHECKING
|
|
|
8
8
|
from gpconfig import ConfigNotFoundError
|
|
9
9
|
|
|
10
10
|
from .exceptions import ObjectNotFoundByURIError
|
|
11
|
+
from .keys import (
|
|
12
|
+
DOMAIN_DOT_ESCAPE,
|
|
13
|
+
escape_domain_name,
|
|
14
|
+
validate_domain_name,
|
|
15
|
+
validate_namespace_name,
|
|
16
|
+
validate_object_name,
|
|
17
|
+
)
|
|
11
18
|
|
|
12
19
|
if TYPE_CHECKING:
|
|
13
20
|
from gpconfig import GPConfigFolder
|
|
14
21
|
|
|
22
|
+
from .config import GPDCDomainConfig
|
|
15
23
|
from .domain import GPDCDomain
|
|
16
24
|
from .object import GPDCDataObject
|
|
17
25
|
|
|
@@ -21,9 +29,11 @@ logger = logging.getLogger(__name__)
|
|
|
21
29
|
class GPDataCache:
|
|
22
30
|
"""Manage GPDCDomain instances from a GPConfigFolder and resolve GPDC URIs.
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
Domain configs are loaded lazily on first ``get_domain`` access and
|
|
33
|
+
cached; ``__init__`` does not create any domain. Domain names may contain
|
|
34
|
+
'.' — gpconfig cannot address dotted filenames, so a '.' in a domain name
|
|
35
|
+
is written as '@' in its config file (``domain_name@com.yaml`` configures
|
|
36
|
+
domain ``"domain_name.com"``; see DOMAIN_DOT_ESCAPE).
|
|
27
37
|
|
|
28
38
|
The cache is a snapshot: config files edited after construction are NOT
|
|
29
39
|
re-read. Rebuild the GPDataCache to pick up changes.
|
|
@@ -38,29 +48,97 @@ class GPDataCache:
|
|
|
38
48
|
self._folder = folder
|
|
39
49
|
self._domains: dict[str, GPDCDomain] = {}
|
|
40
50
|
self._closed = False
|
|
51
|
+
self._warn_dotted_config_stems()
|
|
52
|
+
|
|
53
|
+
def _warn_dotted_config_stems(self) -> None:
|
|
54
|
+
"""Warn once about config stems containing '.' — they are unaddressable.
|
|
55
|
+
|
|
56
|
+
gpconfig's dot-notation consumes '.' as a path separator, so such a
|
|
57
|
+
file is listed by ``list_configs()`` but can never be loaded by name.
|
|
58
|
+
The '@' escape (DOMAIN_DOT_ESCAPE) is the supported way to spell a
|
|
59
|
+
dotted domain name in a filename.
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
names = self._folder.list_configs()
|
|
63
|
+
except Exception:
|
|
64
|
+
# __init__ must stay side-effect-free relative to the folder; a
|
|
65
|
+
# broken listing will surface on first real use instead.
|
|
66
|
+
return
|
|
67
|
+
for name in names:
|
|
68
|
+
if "." in name:
|
|
69
|
+
logger.warning(
|
|
70
|
+
"Config %r in config folder %r contains '.' and cannot be addressed "
|
|
71
|
+
"by gpconfig's dot-notation; rename it to %r ('.' in a domain name "
|
|
72
|
+
"is written as '%s' in the config filename).",
|
|
73
|
+
name,
|
|
74
|
+
self._folder.path,
|
|
75
|
+
escape_domain_name(name),
|
|
76
|
+
DOMAIN_DOT_ESCAPE,
|
|
77
|
+
)
|
|
41
78
|
|
|
42
79
|
def __contains__(self, domain_name: object) -> bool:
|
|
43
80
|
"""Return True iff ``domain_name`` has a config in this folder.
|
|
44
81
|
|
|
45
82
|
Pure config-snapshot check via ``folder.list_configs()``: does not
|
|
46
|
-
touch Redis and does not construct a GPDCDomain.
|
|
83
|
+
touch Redis and does not construct a GPDCDomain. The name is validated
|
|
84
|
+
and '@'-escaped first, so ``"domain_name.com" in cache`` matches the
|
|
85
|
+
config file ``domain_name@com.yaml``; names that are not legal GPDC
|
|
86
|
+
domain names (including any containing '@') return False.
|
|
47
87
|
"""
|
|
48
88
|
if not isinstance(domain_name, str):
|
|
49
89
|
return False
|
|
50
|
-
|
|
90
|
+
try:
|
|
91
|
+
validate_domain_name(domain_name)
|
|
92
|
+
except ValueError:
|
|
93
|
+
return False
|
|
94
|
+
return escape_domain_name(domain_name) in self._folder.list_configs()
|
|
51
95
|
|
|
52
96
|
def get_domain(self, domain_name: str) -> "GPDCDomain":
|
|
53
97
|
"""Return the cached GPDCDomain for ``domain_name``, creating it lazily.
|
|
54
98
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
99
|
+
``domain_name`` is validated against the GPDC name rules and
|
|
100
|
+
'@'-escaped for the config lookup (see DOMAIN_DOT_ESCAPE), so a
|
|
101
|
+
dotted domain name resolves to its '@'-escaped config file. The
|
|
102
|
+
config is loaded as GPDCDomainConfig and constructed directly —
|
|
103
|
+
GPDCDomain's own name resolution restores the '@'-escaped stem
|
|
104
|
+
(``domain_name@com``) to the real dotted name
|
|
105
|
+
(``domain_name.com``), so the same factory-built path via
|
|
106
|
+
``folder.get_object("domain_name@com")`` yields an identically
|
|
107
|
+
named domain.
|
|
108
|
+
|
|
109
|
+
Raises ValueError for names that are not legal GPDC domain names,
|
|
110
|
+
and gpconfig's ``ConfigNotFoundError`` if the domain has no config
|
|
111
|
+
file in this folder. GPDataCache does not create domains without a
|
|
112
|
+
config (use GPDCDomain directly for ad-hoc domains).
|
|
58
113
|
"""
|
|
59
114
|
self._check_open()
|
|
115
|
+
validate_domain_name(domain_name)
|
|
60
116
|
if domain_name not in self._domains:
|
|
61
|
-
|
|
117
|
+
config = self._load_domain_config(domain_name)
|
|
118
|
+
# Imported lazily to keep this module decoupled from the
|
|
119
|
+
# package's import order (same rationale as _traverse).
|
|
120
|
+
from .domain import GPDCDomain
|
|
121
|
+
|
|
122
|
+
self._domains[domain_name] = GPDCDomain(config)
|
|
62
123
|
return self._domains[domain_name]
|
|
63
124
|
|
|
125
|
+
def _load_domain_config(self, domain_name: str) -> "GPDCDomainConfig":
|
|
126
|
+
"""Load the domain config for ``domain_name`` (via its '@'-escaped stem).
|
|
127
|
+
|
|
128
|
+
The config keeps its raw stem in ``config.name``; GPDCDomain's name
|
|
129
|
+
resolution performs the '@' -> '.' restore. Raises
|
|
130
|
+
ConfigNotFoundError when no config answers to the escaped name, or
|
|
131
|
+
when something other than a domain config does (e.g. a subfolder
|
|
132
|
+
shadows the config file).
|
|
133
|
+
"""
|
|
134
|
+
from .config import GPDCDomainConfig
|
|
135
|
+
|
|
136
|
+
escaped = escape_domain_name(domain_name)
|
|
137
|
+
config = self._folder.get_config(escaped, config_cls=GPDCDomainConfig)
|
|
138
|
+
if not isinstance(config, GPDCDomainConfig):
|
|
139
|
+
raise ConfigNotFoundError(escaped, f"resolves to {type(config).__name__}, not a GPDCDomainConfig")
|
|
140
|
+
return config
|
|
141
|
+
|
|
64
142
|
def get_object_by_uri(self, uri: str) -> "GPDCDataObject":
|
|
65
143
|
"""Resolve a GPDC URI to a GPDCDataObject.
|
|
66
144
|
|
|
@@ -72,8 +150,9 @@ class GPDataCache:
|
|
|
72
150
|
terminates on a scalar value raises ObjectNotFoundByURIError.
|
|
73
151
|
|
|
74
152
|
Raises ObjectNotFoundByURIError for every failure mode: malformed
|
|
75
|
-
URI,
|
|
76
|
-
broken path, scalar
|
|
153
|
+
URI, invalid domain/namespace/object name, missing domain config,
|
|
154
|
+
missing namespace, missing root object, broken path, scalar
|
|
155
|
+
terminal.
|
|
77
156
|
"""
|
|
78
157
|
self._check_open()
|
|
79
158
|
domain_name, ns_name, obj_name, path_parts = self._parse_uri(uri)
|
|
@@ -88,17 +167,13 @@ class GPDataCache:
|
|
|
88
167
|
try:
|
|
89
168
|
ns = domain.get_namespace(ns_name)
|
|
90
169
|
except KeyError as e:
|
|
91
|
-
raise ObjectNotFoundByURIError(
|
|
92
|
-
uri, f"namespace {ns_name!r} not found in domain {domain_name!r}"
|
|
93
|
-
) from e
|
|
170
|
+
raise ObjectNotFoundByURIError(uri, f"namespace {ns_name!r} not found in domain {domain_name!r}") from e
|
|
94
171
|
|
|
95
172
|
# Root object: get_object returns the object itself (not a value).
|
|
96
173
|
try:
|
|
97
174
|
root = ns.get_object(obj_name)
|
|
98
175
|
except KeyError as e:
|
|
99
|
-
raise ObjectNotFoundByURIError(
|
|
100
|
-
uri, f"object {obj_name!r} not found in namespace {ns_name!r}"
|
|
101
|
-
) from e
|
|
176
|
+
raise ObjectNotFoundByURIError(uri, f"object {obj_name!r} not found in namespace {ns_name!r}") from e
|
|
102
177
|
|
|
103
178
|
# _traverse descends path_parts via __getitem__ (int-coercing ListObject
|
|
104
179
|
# segments) and enforces the object-only terminal contract; with no
|
|
@@ -125,15 +200,30 @@ class GPDataCache:
|
|
|
125
200
|
"""Split a GPDC URI into (domain, namespace, name, path_parts)."""
|
|
126
201
|
if not uri.startswith("gpdc://"):
|
|
127
202
|
raise ObjectNotFoundByURIError(uri, "URI must start with 'gpdc://'")
|
|
128
|
-
rest = uri[len("gpdc://"):]
|
|
203
|
+
rest = uri[len("gpdc://") :]
|
|
129
204
|
parts = rest.split("/")
|
|
130
205
|
if len(parts) < 3:
|
|
131
|
-
raise ObjectNotFoundByURIError(
|
|
132
|
-
uri, "URI must have at least domain/namespace/name segments"
|
|
133
|
-
)
|
|
206
|
+
raise ObjectNotFoundByURIError(uri, "URI must have at least domain/namespace/name segments")
|
|
134
207
|
if any(p == "" for p in parts):
|
|
135
208
|
raise ObjectNotFoundByURIError(uri, "URI contains an empty segment")
|
|
136
209
|
domain, ns_name, obj_name = parts[0], parts[1], parts[2]
|
|
210
|
+
# Validate all three locating segments up front so every URI failure
|
|
211
|
+
# mode surfaces as ObjectNotFoundByURIError: a ValueError from any
|
|
212
|
+
# downstream validator (e.g. namespace.get_object validates object
|
|
213
|
+
# names before its existence check) must not leak through
|
|
214
|
+
# get_object_by_uri, and has_uri relies on that contract. Path
|
|
215
|
+
# segments are NOT validated here — they are dict keys / list
|
|
216
|
+
# indices, and invalid ones fail traversal with KeyError/IndexError,
|
|
217
|
+
# which _traverse already wraps.
|
|
218
|
+
for label, segment, validator in (
|
|
219
|
+
("domain", domain, validate_domain_name),
|
|
220
|
+
("namespace", ns_name, validate_namespace_name),
|
|
221
|
+
("object", obj_name, validate_object_name),
|
|
222
|
+
):
|
|
223
|
+
try:
|
|
224
|
+
validator(segment)
|
|
225
|
+
except ValueError as e:
|
|
226
|
+
raise ObjectNotFoundByURIError(uri, f"invalid {label} name {segment!r}: {e}") from e
|
|
137
227
|
path_parts = parts[3:]
|
|
138
228
|
return domain, ns_name, obj_name, path_parts
|
|
139
229
|
|
|
@@ -164,16 +254,14 @@ class GPDataCache:
|
|
|
164
254
|
if not hasattr(current, "__getitem__"):
|
|
165
255
|
raise ObjectNotFoundByURIError(
|
|
166
256
|
uri,
|
|
167
|
-
f"path segment {part!r} cannot descend: "
|
|
168
|
-
f"{current.canonical_uri} does not support subscript access",
|
|
257
|
+
f"path segment {part!r} cannot descend: {current.canonical_uri} does not support subscript access",
|
|
169
258
|
)
|
|
170
259
|
# Type-based key coercion (decision D9).
|
|
171
260
|
if isinstance(current, ListObject):
|
|
172
261
|
if not part.isdigit():
|
|
173
262
|
raise ObjectNotFoundByURIError(
|
|
174
263
|
uri,
|
|
175
|
-
f"path segment {part!r} invalid for ListObject: "
|
|
176
|
-
f"must be a non-negative integer",
|
|
264
|
+
f"path segment {part!r} invalid for ListObject: must be a non-negative integer",
|
|
177
265
|
)
|
|
178
266
|
key = int(part)
|
|
179
267
|
else:
|
|
@@ -186,9 +274,7 @@ class GPDataCache:
|
|
|
186
274
|
) from e
|
|
187
275
|
# Terminal check: must be a GPDCDataObject, not a scalar value.
|
|
188
276
|
if not isinstance(current, GPDCDataObject):
|
|
189
|
-
raise ObjectNotFoundByURIError(
|
|
190
|
-
uri, "terminal is a scalar value; get_object_by_uri returns objects only"
|
|
191
|
-
)
|
|
277
|
+
raise ObjectNotFoundByURIError(uri, "terminal is a scalar value; get_object_by_uri returns objects only")
|
|
192
278
|
return current
|
|
193
279
|
|
|
194
280
|
def close(self) -> None:
|
|
@@ -15,6 +15,7 @@ from .keys import (
|
|
|
15
15
|
make_domain_meta_key,
|
|
16
16
|
make_domain_namespaces_key,
|
|
17
17
|
make_namespace_meta_key,
|
|
18
|
+
unescape_domain_name,
|
|
18
19
|
validate_domain_name,
|
|
19
20
|
)
|
|
20
21
|
|
|
@@ -52,7 +53,22 @@ class GPDCDomain(GPConfigurable):
|
|
|
52
53
|
# loaded from a file (== filename stem); it is "" for a programmatically
|
|
53
54
|
# constructed config. Prefer config.name, fall back to the explicit name
|
|
54
55
|
# argument, and reject a conflict between the two.
|
|
56
|
+
#
|
|
57
|
+
# A '@' in config.name is the filename escape for '.' (see
|
|
58
|
+
# keys.DOMAIN_DOT_ESCAPE): gpconfig's dot-notation cannot address
|
|
59
|
+
# dotted filenames, so domain_name@com.yaml spells the domain
|
|
60
|
+
# "domain_name.com". Restore it here — before the mismatch check and
|
|
61
|
+
# validation — so both gpconfig's object factory
|
|
62
|
+
# (folder.get_object("domain_name@com")) and GPDataCache construct the
|
|
63
|
+
# identically-named domain. The restore applies to any non-empty
|
|
64
|
+
# config.name (which for a file-loaded config IS the stem; a
|
|
65
|
+
# programmatically assigned name containing '@' is restored the same
|
|
66
|
+
# way — '@' cannot occur in a legal domain name, so this never alters
|
|
67
|
+
# an already-valid name). An explicit name= argument containing '@' is
|
|
68
|
+
# NOT restored and is rejected loudly by validate_domain_name below.
|
|
55
69
|
config_name = config.name
|
|
70
|
+
if config_name:
|
|
71
|
+
config_name = unescape_domain_name(config_name)
|
|
56
72
|
if name is not None and config_name:
|
|
57
73
|
if name != config_name:
|
|
58
74
|
raise ValueError(
|
|
@@ -112,6 +112,35 @@ def validate_namespace_name(name: str) -> None:
|
|
|
112
112
|
_validate_name(name, "namespace name")
|
|
113
113
|
|
|
114
114
|
|
|
115
|
+
DOMAIN_DOT_ESCAPE = "@"
|
|
116
|
+
"""Escape for '.' in domain config filenames (internet-domain adaptation).
|
|
117
|
+
|
|
118
|
+
gpconfig addresses configs by dot-separated paths, so a '.' in a config file
|
|
119
|
+
stem is consumed as a path separator and the file becomes unaddressable
|
|
120
|
+
(listed by ``list_configs`` but never found). A '@' in the stem instead
|
|
121
|
+
stands for a literal '.' in the domain name: ``domain_name@com.yaml``
|
|
122
|
+
configures the domain ``"domain_name.com"``. The '@' was chosen for its
|
|
123
|
+
internet-address connotation (user@host); the mapping is injective because
|
|
124
|
+
'@' is not a legal character in GPDC names, so two distinct domain names can
|
|
125
|
+
never escape to the same filename.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def escape_domain_name(name: str) -> str:
|
|
130
|
+
"""Return the config-file form of a domain name ('.' -> '@')."""
|
|
131
|
+
return name.replace(".", DOMAIN_DOT_ESCAPE)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def unescape_domain_name(stem: str) -> str:
|
|
135
|
+
"""Return the domain name for a config-file stem ('@' -> '.').
|
|
136
|
+
|
|
137
|
+
Inverse of :func:`escape_domain_name`. Safe to apply unconditionally to
|
|
138
|
+
a stem-derived name: '@' cannot occur in a legal GPDC domain name, so
|
|
139
|
+
this never alters an already-valid name.
|
|
140
|
+
"""
|
|
141
|
+
return stem.replace(DOMAIN_DOT_ESCAPE, ".")
|
|
142
|
+
|
|
143
|
+
|
|
115
144
|
def collect_ref_names_from_raw(r, raw_key: str, raw_type: str) -> list[str]:
|
|
116
145
|
"""Scan a container's raw Redis data and return the names of all referenced objects.
|
|
117
146
|
|
|
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
|
|
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
|