server-decorator-entity 2.0.19__py3-none-any.whl

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.
@@ -0,0 +1,43 @@
1
+ from server_decorator_entity.action import action
2
+ from server_decorator_entity.canonical import canonical_json
3
+ from server_decorator_entity.markers import (
4
+ Bool,
5
+ Datetime,
6
+ Email,
7
+ EnumOf,
8
+ FieldMarker,
9
+ Float,
10
+ Id,
11
+ Int,
12
+ ListOf,
13
+ Obj,
14
+ Ref,
15
+ Str,
16
+ Uri,
17
+ Uuid,
18
+ )
19
+ from server_decorator_entity.restful import entity_spec, entity_spec_canonical, restful
20
+ from server_decorator_entity.validate import RESERVED_WIRE_NAMES
21
+
22
+ __all__ = [
23
+ "RESERVED_WIRE_NAMES",
24
+ "Bool",
25
+ "Datetime",
26
+ "Email",
27
+ "EnumOf",
28
+ "FieldMarker",
29
+ "Float",
30
+ "Id",
31
+ "Int",
32
+ "ListOf",
33
+ "Obj",
34
+ "Ref",
35
+ "Str",
36
+ "Uri",
37
+ "Uuid",
38
+ "action",
39
+ "canonical_json",
40
+ "entity_spec",
41
+ "entity_spec_canonical",
42
+ "restful",
43
+ ]
@@ -0,0 +1,45 @@
1
+ from collections.abc import Callable
2
+ from typing import Any, TypeVar
3
+
4
+ from server_decorator_entity.markers import FieldMarker
5
+
6
+ F = TypeVar("F", bound=Callable[..., Any])
7
+
8
+ _ATTR = "__entity_action__"
9
+
10
+
11
+ def action(
12
+ name: str,
13
+ *,
14
+ description: str | None = None,
15
+ state_field: str | None = None,
16
+ from_state: str | None = None,
17
+ to_state: str | None = None,
18
+ input: list[tuple[str, FieldMarker]] | None = None,
19
+ transactional: bool = True,
20
+ ) -> Callable[[F], F]:
21
+ def mark(fn: F) -> F:
22
+ if isinstance(fn, (staticmethod, classmethod)):
23
+ kind = "static" if isinstance(fn, staticmethod) else "class"
24
+ underlying = fn.__func__
25
+ raise TypeError(
26
+ f"@action must decorate an instance method (got {kind} method "
27
+ f"'{underlying.__name__}')"
28
+ )
29
+ spec: dict[str, Any] = {"name": name}
30
+ if description is not None:
31
+ spec["description"] = description
32
+ if from_state is not None or to_state is not None:
33
+ spec["stateField"] = state_field or "status"
34
+ if from_state is not None:
35
+ spec["from"] = from_state
36
+ if to_state is not None:
37
+ spec["to"] = to_state
38
+ if input:
39
+ spec["input"] = [m.to_ir(n) for n, m in input]
40
+ if not transactional:
41
+ spec["transactional"] = False
42
+ setattr(fn, _ATTR, spec)
43
+ return fn
44
+
45
+ return mark
@@ -0,0 +1,8 @@
1
+ """Canonical EntitySpec JSON — contracts/rules/entity-spec-canonical.md."""
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+
7
+ def canonical_json(value: Any) -> str:
8
+ return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
@@ -0,0 +1,119 @@
1
+ """Field markers used inside Annotated[...] on @restful classes."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from server_decorator_entity.validate import (
7
+ assert_enum_nonempty,
8
+ assert_format_pattern_exclusive,
9
+ assert_integer_or_none,
10
+ assert_portable_pattern,
11
+ assert_wire_name_not_reserved,
12
+ )
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class FieldMarker:
17
+ type: str
18
+ description: str | None = None
19
+ role: str | None = None
20
+ format: str | None = None
21
+ enum: tuple[str, ...] | None = None
22
+ min: int | None = None
23
+ max: int | None = None
24
+ pattern: str | None = None
25
+ optional: bool = False
26
+ nullable: bool = False
27
+ readonly: bool = False
28
+ writeonly: bool = False
29
+ filterable: bool = False
30
+ item: "FieldMarker | None" = None
31
+ ref: str | None = None
32
+ fields: tuple[tuple[str, "FieldMarker"], ...] | None = None # (name, marker) pairs for obj
33
+
34
+ def to_ir(self, name: str) -> dict[str, Any]:
35
+ label = f'field "{name}"'
36
+ assert_wire_name_not_reserved(name, label)
37
+ assert_format_pattern_exclusive(self.format, self.pattern, label)
38
+ assert_portable_pattern(self.pattern, label)
39
+ assert_enum_nonempty(self.enum, label)
40
+ assert_integer_or_none(self.min, f"{label}.min")
41
+ assert_integer_or_none(self.max, f"{label}.max")
42
+
43
+ ir: dict[str, Any] = {"name": name, "type": self.type}
44
+ if self.role:
45
+ ir["role"] = self.role
46
+ if self.format:
47
+ ir["format"] = self.format
48
+ if self.enum:
49
+ ir["enum"] = list(self.enum)
50
+ if self.min is not None:
51
+ ir["min"] = self.min
52
+ if self.max is not None:
53
+ ir["max"] = self.max
54
+ if self.pattern is not None:
55
+ ir["pattern"] = self.pattern
56
+ if self.description is not None:
57
+ ir["description"] = self.description
58
+ for flag in ("optional", "nullable", "readonly", "writeonly", "filterable"):
59
+ if getattr(self, flag):
60
+ ir[flag] = True
61
+ if self.item:
62
+ ir["item"] = self.item.to_ir(name)
63
+ if self.ref is not None:
64
+ ir["ref"] = self.ref
65
+ if self.fields is not None:
66
+ ir["fields"] = [m.to_ir(n) for n, m in self.fields]
67
+ return ir
68
+
69
+
70
+ def Id() -> FieldMarker: # noqa: N802
71
+ return FieldMarker(type="str", role="id", readonly=True)
72
+
73
+
74
+ def Str(**kw: Any) -> FieldMarker: # noqa: N802
75
+ return FieldMarker(type="str", **kw)
76
+
77
+
78
+ def Email(**kw: Any) -> FieldMarker: # noqa: N802
79
+ return FieldMarker(type="str", format="email", **kw)
80
+
81
+
82
+ def Uuid(**kw: Any) -> FieldMarker: # noqa: N802
83
+ return FieldMarker(type="str", format="uuid", **kw)
84
+
85
+
86
+ def Uri(**kw: Any) -> FieldMarker: # noqa: N802
87
+ return FieldMarker(type="str", format="uri", **kw)
88
+
89
+
90
+ def Int(**kw: Any) -> FieldMarker: # noqa: N802
91
+ return FieldMarker(type="int", **kw)
92
+
93
+
94
+ def Float(**kw: Any) -> FieldMarker: # noqa: N802
95
+ return FieldMarker(type="float", **kw)
96
+
97
+
98
+ def Bool(**kw: Any) -> FieldMarker: # noqa: N802
99
+ return FieldMarker(type="bool", **kw)
100
+
101
+
102
+ def Datetime(**kw: Any) -> FieldMarker: # noqa: N802
103
+ return FieldMarker(type="datetime", **kw)
104
+
105
+
106
+ def EnumOf(*values: str, **kw: Any) -> FieldMarker: # noqa: N802
107
+ return FieldMarker(type="enum", enum=values, **kw)
108
+
109
+
110
+ def Ref(entity: str, **kw: Any) -> FieldMarker: # noqa: N802
111
+ return FieldMarker(type="ref", ref=entity, **kw)
112
+
113
+
114
+ def ListOf(item: FieldMarker, **kw: Any) -> FieldMarker: # noqa: N802
115
+ return FieldMarker(type="list", item=item, **kw)
116
+
117
+
118
+ def Obj(fields: list[tuple[str, FieldMarker]], **kw: Any) -> FieldMarker: # noqa: N802
119
+ return FieldMarker(type="obj", fields=tuple(fields), **kw)
File without changes
@@ -0,0 +1,128 @@
1
+ import copy
2
+ import inspect
3
+ import re
4
+ import typing
5
+ from collections.abc import Callable
6
+ from typing import Any, TypeVar
7
+
8
+ from server_decorator_entity.action import _ATTR
9
+ from server_decorator_entity.canonical import canonical_json
10
+ from server_decorator_entity.markers import FieldMarker
11
+ from server_decorator_entity.validate import assert_valid_security
12
+
13
+ C = TypeVar("C", bound=type)
14
+
15
+ _SPEC_ATTR = "__entity_spec__"
16
+ _DEFAULT_OPS = ["list", "get", "create", "update", "delete"]
17
+ _WIRE_NAME_RE = re.compile(r"^[a-z][A-Za-z0-9]*$")
18
+
19
+
20
+ def _camel(name: str) -> str:
21
+ head, *rest = name.split("_")
22
+ return head + "".join(s.capitalize() for s in rest)
23
+
24
+
25
+ def restful(
26
+ *,
27
+ path: str,
28
+ transports: list[str] | None = None,
29
+ ops: list[str] | None = None,
30
+ pagination: str = "offset",
31
+ description: str | None = None,
32
+ tags: list[str] | None = None,
33
+ security: dict[str, Any] | None = None,
34
+ ) -> Callable[[C], C]:
35
+ def wrap(cls: C) -> C:
36
+ # Own-class declarations only (N2: no MRO walk) — a @restful subclass must not
37
+ # re-collect its base's fields. get_type_hints() still resolves the (possibly
38
+ # string/forward-ref) annotation values; own_annotations only supplies which
39
+ # names — and their declaration order — belong to this class.
40
+ own_annotations: dict[str, Any] = inspect.get_annotations(cls)
41
+ hints = typing.get_type_hints(cls, include_extras=True)
42
+ fields: list[dict[str, Any]] = []
43
+ seen_wire_names: set[str] = set()
44
+ for attr_name in own_annotations:
45
+ hint = hints.get(attr_name)
46
+ marker = next(
47
+ (m for m in getattr(hint, "__metadata__", ()) if isinstance(m, FieldMarker)), None
48
+ )
49
+ if marker is None:
50
+ continue
51
+ wire_name = _camel(attr_name)
52
+ if not _WIRE_NAME_RE.match(wire_name):
53
+ raise ValueError(
54
+ f"{cls.__name__}.{attr_name}: computed wire name {wire_name!r} does not "
55
+ f"match ^[a-z][A-Za-z0-9]*$"
56
+ )
57
+ if wire_name in seen_wire_names:
58
+ raise ValueError(
59
+ f"{cls.__name__}.{attr_name}: wire name {wire_name!r} collides with "
60
+ f"another field"
61
+ )
62
+ seen_wire_names.add(wire_name)
63
+ fields.append(marker.to_ir(wire_name))
64
+
65
+ # `or` is truthiness-coalescing, not None-coalescing — an explicit `transports=[]`/
66
+ # `ops=[]` must fail fast here (not be silently replaced by the default), matching TS's
67
+ # `??`-then-validate approach (C4). Only an omitted (`None`) argument gets the default.
68
+ resolved_transports = ["rest"] if transports is None else transports
69
+ if not resolved_transports:
70
+ raise ValueError(f"{cls.__name__}: transports must not be empty")
71
+ resolved_ops = list(_DEFAULT_OPS) if ops is None else ops
72
+ if not resolved_ops:
73
+ raise ValueError(f"{cls.__name__}: ops must not be empty")
74
+
75
+ actions: list[dict[str, Any]] = []
76
+ for member in vars(cls).values():
77
+ if isinstance(member, (staticmethod, classmethod)):
78
+ underlying = member.__func__
79
+ if hasattr(underlying, _ATTR):
80
+ kind = "static" if isinstance(member, staticmethod) else "class"
81
+ raise TypeError(
82
+ f"@action must decorate an instance method (got {kind} method "
83
+ f"'{underlying.__name__}')"
84
+ )
85
+ continue
86
+ if callable(member) and hasattr(member, _ATTR):
87
+ actions.append(getattr(member, _ATTR))
88
+ spec: dict[str, Any] = {
89
+ "specVersion": "1.2.0",
90
+ "entity": cls.__name__,
91
+ "path": path,
92
+ "transports": resolved_transports,
93
+ "ops": resolved_ops,
94
+ "pagination": pagination,
95
+ "fields": fields,
96
+ }
97
+ if actions:
98
+ spec["actions"] = actions
99
+ assert_valid_security(security, resolved_ops, [a["name"] for a in actions], cls.__name__)
100
+ # Keep absent keys absent so canonical JSON is byte-unchanged for 1.1-era declarations.
101
+ if description is not None:
102
+ spec["description"] = description
103
+ if tags is not None:
104
+ spec["tags"] = tags
105
+ if security is not None:
106
+ spec["security"] = security
107
+ setattr(cls, _SPEC_ATTR, spec)
108
+ return cls
109
+
110
+ return wrap
111
+
112
+
113
+ def _stored_spec(cls: type) -> dict[str, Any]:
114
+ # Own-attribute only (cls.__dict__, never getattr's MRO walk) — an undecorated
115
+ # subclass of a @restful base must not silently inherit the base's spec.
116
+ spec = cls.__dict__.get(_SPEC_ATTR)
117
+ if spec is None:
118
+ raise TypeError(f"{cls.__name__} is not decorated with @restful")
119
+ return spec # type: ignore[no-any-return]
120
+
121
+
122
+ def entity_spec(cls: type) -> dict[str, Any]:
123
+ # Deep copy so callers can't poison the memoized spec (parity with Node's deep-freeze).
124
+ return copy.deepcopy(_stored_spec(cls))
125
+
126
+
127
+ def entity_spec_canonical(cls: type) -> str:
128
+ return canonical_json(_stored_spec(cls))
File without changes
@@ -0,0 +1,63 @@
1
+ from typing import Annotated, Any
2
+
3
+ from server_decorator_entity import (
4
+ Bool,
5
+ Datetime,
6
+ Email,
7
+ EnumOf,
8
+ Float,
9
+ Id,
10
+ Int,
11
+ ListOf,
12
+ Obj,
13
+ Str,
14
+ action,
15
+ restful,
16
+ )
17
+
18
+
19
+ @restful(
20
+ path="users",
21
+ transports=["rest", "rpc", "sse", "ws"],
22
+ description="A registered user account",
23
+ tags=["identity"],
24
+ security={
25
+ "schemes": [
26
+ {"name": "bearerAuth", "type": "http", "scheme": "bearer", "bearerFormat": "JWT"}
27
+ ],
28
+ "ops": {"create": ["user:write"], "update": ["user:write"], "delete": ["user:admin"]},
29
+ "actions": {"activate": ["user:admin"]},
30
+ },
31
+ )
32
+ class User:
33
+ id: Annotated[str, Id()]
34
+ email: Annotated[str, Email(filterable=True, description="Primary contact address")]
35
+ name: Annotated[str, Str(min=2, max=80)]
36
+ status: Annotated[str, EnumOf("inactive", "active", "banned", filterable=True)]
37
+ login_count: Annotated[int, Int(min=0, readonly=True)]
38
+ tags: Annotated[list[str], ListOf(Str())]
39
+ profile: Annotated[
40
+ dict[str, Any] | None, Obj([("bio", Str(max=500, optional=True))], optional=True)
41
+ ]
42
+ birthday: Annotated[str | None, Datetime(optional=True, nullable=True)]
43
+ # I-d (P1 gate-review): writable, non-readonly fields to exercise DTO derivation rules the
44
+ # original 16-case contract never touched (JSON-type strictness on float/bool, writeonly
45
+ # exclusion from read).
46
+ score: Annotated[float | None, Float(min=0, optional=True)]
47
+ active: Annotated[bool | None, Bool(optional=True)]
48
+ secret_note: Annotated[str | None, Str(writeonly=True, max=100, optional=True)]
49
+
50
+ @action(
51
+ "activate",
52
+ description="Move a dormant account back to active",
53
+ from_state="inactive",
54
+ to_state="active",
55
+ input=[("reason", Str(max=200, optional=True))],
56
+ )
57
+ def activate(self) -> None: ...
58
+
59
+ @action("ping")
60
+ def ping(self) -> None: ...
61
+
62
+
63
+ UserFixture = User
@@ -0,0 +1,155 @@
1
+ """Decoration-time IR validation shared across field markers and @restful.
2
+
3
+ Mirrors node/packages/entity/src/validate.ts — see contracts/rules/entity-spec-validation.md
4
+ for the shared invariants (reserved wire names, format+pattern exclusivity, integer min/max,
5
+ non-empty enum/ops/transports) both language packages enforce identically.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ # Public pydantic.BaseModel attribute names (pydantic v2). A wire name colliding with one of
11
+ # these makes gen-pydantic's create_model() raise "shadows an attribute in parent BaseModel" —
12
+ # reject at decoration time in BOTH languages instead of letting it crash gen-pydantic's model
13
+ # build. See contracts/rules/entity-spec-validation.md "Reserved wire names".
14
+ RESERVED_WIRE_NAMES: frozenset[str] = frozenset(
15
+ {
16
+ "construct",
17
+ "copy",
18
+ "dict",
19
+ "from_orm",
20
+ "json",
21
+ "model_computed_fields",
22
+ "model_config",
23
+ "model_construct",
24
+ "model_copy",
25
+ "model_dump",
26
+ "model_dump_json",
27
+ "model_extra",
28
+ "model_fields",
29
+ "model_fields_set",
30
+ "model_json_schema",
31
+ "model_parametrized_name",
32
+ "model_post_init",
33
+ "model_rebuild",
34
+ "model_validate",
35
+ "model_validate_json",
36
+ "model_validate_strings",
37
+ "parse_file",
38
+ "parse_obj",
39
+ "parse_raw",
40
+ "schema",
41
+ "schema_json",
42
+ "update_forward_refs",
43
+ "validate",
44
+ }
45
+ )
46
+
47
+
48
+ def assert_integer_or_none(value: Any, label: str) -> None:
49
+ if value is None:
50
+ return
51
+ # bool is a subclass of int in Python — isinstance(True, int) is True — so it must be
52
+ # excluded explicitly, not just checked for isinstance(value, int).
53
+ if not isinstance(value, int) or isinstance(value, bool):
54
+ raise ValueError(f"{label}: min/max must be integers (got {value!r})")
55
+
56
+
57
+ def assert_wire_name_not_reserved(name: str, label: str) -> None:
58
+ if name in RESERVED_WIRE_NAMES:
59
+ raise ValueError(
60
+ f"{label}: wire name {name!r} is reserved (shadows a pydantic BaseModel attribute)"
61
+ )
62
+
63
+
64
+ def assert_format_pattern_exclusive(fmt: Any, pattern: Any, label: str) -> None:
65
+ if fmt is not None and pattern is not None:
66
+ raise ValueError(f"{label}: format and pattern are mutually exclusive")
67
+
68
+
69
+ def assert_enum_nonempty(enum: Any, label: str) -> None:
70
+ if enum is not None and len(enum) == 0:
71
+ raise ValueError(f"{label}: enum must have at least one value")
72
+
73
+
74
+ _LOOKAROUND_MARKERS = ("(?=", "(?!", "(?<=", "(?<!")
75
+
76
+
77
+ def _has_backreference(pattern: str) -> bool:
78
+ """A `\\` run of odd length immediately before a digit 1-9 means that digit is escaped — a
79
+ backreference (`\\1`). An even-length run means the backslashes are themselves escaped
80
+ (`\\\\1` is a literal backslash followed by the literal digit `1`), so it's NOT a
81
+ backreference. `\\0` is never a backreference (it's either a literal `0` or, in some engines,
82
+ a null escape — not a capture-group reference), so only digits 1-9 are scanned. This is a
83
+ conservative lexical scan, not a regex parser — see
84
+ contracts/rules/entity-spec-validation.md for what it does and does not catch.
85
+ """
86
+ for i, c in enumerate(pattern):
87
+ if c in "123456789":
88
+ backslashes = 0
89
+ j = i - 1
90
+ while j >= 0 and pattern[j] == "\\":
91
+ backslashes += 1
92
+ j -= 1
93
+ if backslashes % 2 == 1:
94
+ return True
95
+ return False
96
+
97
+
98
+ def assert_portable_pattern(pattern: Any, label: str) -> None:
99
+ """pydantic v2's Rust-backed regex engine (used by gen-pydantic) does not support lookaround
100
+ or backreferences — both are valid Python `re` / JS RegExp syntax, so a pattern using either
101
+ builds fine in gen-zod but crashes gen-pydantic's entire model build. Reject non-portable
102
+ patterns at DECORATION time (before either generator ever sees the IR), in both languages —
103
+ see contracts/rules/entity-spec-validation.md.
104
+ """
105
+ if pattern is None:
106
+ return
107
+ for marker in _LOOKAROUND_MARKERS:
108
+ if marker in pattern:
109
+ raise ValueError(
110
+ f"{label}: pattern {pattern!r} is not RE2/rust-regex compatible (contains "
111
+ f"lookaround {marker!r}) — see contracts/rules/entity-spec-validation.md"
112
+ )
113
+ if _has_backreference(pattern):
114
+ raise ValueError(
115
+ f"{label}: pattern {pattern!r} is not RE2/rust-regex compatible (contains a "
116
+ f"backreference) — see contracts/rules/entity-spec-validation.md"
117
+ )
118
+
119
+
120
+ __all__ = [
121
+ "RESERVED_WIRE_NAMES",
122
+ "assert_enum_nonempty",
123
+ "assert_format_pattern_exclusive",
124
+ "assert_integer_or_none",
125
+ "assert_portable_pattern",
126
+ "assert_wire_name_not_reserved",
127
+ ]
128
+
129
+
130
+ def assert_valid_security(
131
+ security: dict[str, Any] | None,
132
+ ops: list[str],
133
+ action_names: list[str],
134
+ entity: str,
135
+ ) -> None:
136
+ """Mirror of Node's validateSecurity() in packages/entity/src/validate.ts."""
137
+ if security is None:
138
+ return
139
+ schemes = security.get("schemes", [])
140
+ if not schemes:
141
+ raise ValueError(f"{entity}: security.schemes must not be empty")
142
+ for s in schemes:
143
+ name = s.get("name")
144
+ if s.get("type") not in ("http", "apiKey"):
145
+ raise ValueError(f'{entity}: scheme "{name}": type must be "http" or "apiKey"')
146
+ if s["type"] == "http" and "scheme" not in s:
147
+ raise ValueError(f'{entity}: scheme "{name}": http requires "scheme"')
148
+ if s["type"] == "apiKey" and ("in" not in s or "paramName" not in s):
149
+ raise ValueError(f'{entity}: scheme "{name}": apiKey requires "in" and "paramName"')
150
+ for op in security.get("ops", {}):
151
+ if op not in ops:
152
+ raise ValueError(f"{entity}: security.ops.{op} is not declared in ops")
153
+ for action_name in security.get("actions", {}):
154
+ if action_name not in action_names:
155
+ raise ValueError(f"{entity}: security.actions.{action_name} is not a declared action")
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.5
2
+ Name: server-decorator-entity
3
+ Version: 2.0.19
4
+ Summary: Declarative entity decorators emitting the language-neutral EntitySpec IR. Polyglot sibling of @montionugera/entity-spec.
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Provides-Extra: dev
8
+ Requires-Dist: jsonschema>=4.21; extra == 'dev'
9
+ Requires-Dist: mypy>=1.10; extra == 'dev'
10
+ Requires-Dist: pytest-cov>=5; extra == 'dev'
11
+ Requires-Dist: pytest>=8; extra == 'dev'
12
+ Requires-Dist: ruff>=0.5; extra == 'dev'
13
+ Description-Content-Type: text/markdown
14
+
15
+ # server-decorator-entity (Python)
16
+
17
+ Declarative entity decorators (`@restful`, `@action`, field markers) emitting the language-neutral EntitySpec IR — polyglot sibling of `@montionugera/entity-spec`, sharing the wire-format contracts in [`../../../contracts`](../../../contracts).
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install server-decorator-entity
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```python
28
+ from typing import Annotated
29
+ from server_decorator_entity import restful, Id, Email, entity_spec
30
+
31
+ @restful(path="users")
32
+ class User:
33
+ id: Annotated[str, Id()]
34
+ email: Annotated[str, Email()]
35
+
36
+ spec = entity_spec(User) # -> EntitySpec IR, byte-identical to the Node emitter
37
+ ```
38
+
39
+ `entity_spec_canonical(User)` returns the IR as a canonical JSON string (codepoint-sorted keys,
40
+ `None`-dropping) — the exact bytes both languages' generators and the cross-language parity
41
+ check compare against.
@@ -0,0 +1,12 @@
1
+ server_decorator_entity/__init__.py,sha256=tqlDxK1waungYVvAcXEHl54Idra4dNcIR1NLIlxlzmE,781
2
+ server_decorator_entity/action.py,sha256=_ixpHHM4J8KGPx8rIpyRtUcIN5yKqd2dNCu7YFfqMCw,1462
3
+ server_decorator_entity/canonical.py,sha256=lFX82wVG8-1qpeuYA57dwOif4kxQyK1MT2omiY6CjB4,243
4
+ server_decorator_entity/markers.py,sha256=acEC9Z7oomjP_D384_j4wr5XhGiOvlc6X-66qWOrSHM,3705
5
+ server_decorator_entity/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ server_decorator_entity/restful.py,sha256=WQvmEevGQKOj4e1N9aPcsLGaoNebzUiNRrbj3nLYC0s,5207
7
+ server_decorator_entity/validate.py,sha256=FUB7bdRrXsetE7ZOp3eSa8FadoDJR_XGblNsMqZ128M,6076
8
+ server_decorator_entity/testing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ server_decorator_entity/testing/user_fixture.py,sha256=XW1P4XxMguShaUumOnXFLGUCxZeNvUyc1lq1Vv3iEao,1948
10
+ server_decorator_entity-2.0.19.dist-info/METADATA,sha256=Elvck7S19qcJhLaok4hBYBKGCfW6Ji-NqHK6hAFO1uk,1376
11
+ server_decorator_entity-2.0.19.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
12
+ server_decorator_entity-2.0.19.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any