seampact-codegen 1.0.0__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.
Files changed (76) hide show
  1. seampact/codegen/__init__.py +2 -0
  2. seampact/codegen/boot_check_hooks.py +38 -0
  3. seampact/codegen/contract_module.py +146 -0
  4. seampact/codegen/credentials_classes.py +99 -0
  5. seampact/codegen/handler_forms.py +132 -0
  6. seampact/codegen/input_schemas.py +130 -0
  7. seampact/codegen/kept_forms.py +522 -0
  8. seampact/codegen/matrix_row.py +23 -0
  9. seampact/codegen/names.py +146 -0
  10. seampact/codegen/native_docs_file.py +169 -0
  11. seampact/codegen/operation_types.py +425 -0
  12. seampact/codegen/placement_lines.py +79 -0
  13. seampact/codegen/printer.py +196 -0
  14. seampact/codegen/py.typed +0 -0
  15. seampact/codegen/pydantic_forms.py +53 -0
  16. seampact/codegen/pydantic_layout.py +178 -0
  17. seampact/codegen/pydantic_models.py +405 -0
  18. seampact/codegen/redacted_members.py +47 -0
  19. seampact/codegen/refusals.py +84 -0
  20. seampact/codegen/request_deadline.py +68 -0
  21. seampact/codegen/response_header_forms.py +73 -0
  22. seampact/codegen/restricted_sort_keys.py +18 -0
  23. seampact/codegen/route_helpers.py +109 -0
  24. seampact/codegen/routes.py +139 -0
  25. seampact/codegen/runtime/__init__.py +7 -0
  26. seampact/codegen/runtime/access_kinds.py +24 -0
  27. seampact/codegen/runtime/access_steps.py +520 -0
  28. seampact/codegen/runtime/body_reader.py +64 -0
  29. seampact/codegen/runtime/cache_headers.py +89 -0
  30. seampact/codegen/runtime/contract.py +232 -0
  31. seampact/codegen/runtime/cors_preflight.py +20 -0
  32. seampact/codegen/runtime/deadline.py +72 -0
  33. seampact/codegen/runtime/digests.py +98 -0
  34. seampact/codegen/runtime/failures.py +33 -0
  35. seampact/codegen/runtime/field_rights.py +193 -0
  36. seampact/codegen/runtime/framework_routes.py +36 -0
  37. seampact/codegen/runtime/framework_versions.py +91 -0
  38. seampact/codegen/runtime/handler_call.py +31 -0
  39. seampact/codegen/runtime/handler_reply.py +148 -0
  40. seampact/codegen/runtime/hinted_values.py +48 -0
  41. seampact/codegen/runtime/hook_check.py +77 -0
  42. seampact/codegen/runtime/idempotency.py +279 -0
  43. seampact/codegen/runtime/input_decoding.py +226 -0
  44. seampact/codegen/runtime/ir_view.py +47 -0
  45. seampact/codegen/runtime/native_docs.py +130 -0
  46. seampact/codegen/runtime/page_facts.py +33 -0
  47. seampact/codegen/runtime/page_reply.py +396 -0
  48. seampact/codegen/runtime/pattern.py +69 -0
  49. seampact/codegen/runtime/principal_members.py +41 -0
  50. seampact/codegen/runtime/public_origin.py +102 -0
  51. seampact/codegen/runtime/pydantic/__init__.py +165 -0
  52. seampact/codegen/runtime/pydantic/checked_input.py +52 -0
  53. seampact/codegen/runtime/pydantic/contract_input.py +183 -0
  54. seampact/codegen/runtime/pydantic/issues.py +142 -0
  55. seampact/codegen/runtime/raw_target.py +103 -0
  56. seampact/codegen/runtime/replies.py +144 -0
  57. seampact/codegen/runtime/reply_verdict.py +215 -0
  58. seampact/codegen/runtime/requirement_params.py +170 -0
  59. seampact/codegen/runtime/response_header_entries.py +226 -0
  60. seampact/codegen/runtime/response_headers.py +114 -0
  61. seampact/codegen/runtime/route_match.py +88 -0
  62. seampact/codegen/runtime/run_settings.py +35 -0
  63. seampact/codegen/runtime/signature_header.py +92 -0
  64. seampact/codegen/runtime/sync_driver.py +35 -0
  65. seampact/codegen/runtime/templates.py +132 -0
  66. seampact/codegen/runtime/uuid_text.py +7 -0
  67. seampact/codegen/runtime/value_rules.py +218 -0
  68. seampact/codegen/runtime/version.py +59 -0
  69. seampact/codegen/runtime/version_token.py +108 -0
  70. seampact/codegen/runtime/wire_codec.py +229 -0
  71. seampact/codegen/runtime/wire_values.py +260 -0
  72. seampact/codegen/source_lines.py +35 -0
  73. seampact_codegen-1.0.0.dist-info/METADATA +18 -0
  74. seampact_codegen-1.0.0.dist-info/RECORD +76 -0
  75. seampact_codegen-1.0.0.dist-info/WHEEL +4 -0
  76. seampact_codegen-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,229 @@
1
+ """Turns a decoded wire body into the tree a generated model validates, and a generated value back into the wire
2
+ tree: object members renamed between their wire names and their Python attributes by the api's member table, unknown
3
+ keys taken out and reported by pointer (the input use's `unknownKeys` policy decides on them), and on the way out
4
+ `date-time` with its offset and the fraction it holds, `date`, `uuid`, decimals and enum members as wire text. Values
5
+ are read by attribute, so a Pydantic model and a frozen dataclass both encode."""
6
+
7
+ from collections.abc import Mapping, Sequence
8
+ from dataclasses import dataclass, field
9
+ from datetime import UTC, date, datetime, timedelta
10
+ from decimal import Decimal
11
+ from enum import Enum
12
+ from typing import Final
13
+ from uuid import UUID
14
+
15
+ from seampact.codegen.runtime.contract import ApiContract
16
+ from seampact.codegen.runtime.failures import BoundaryFailureError
17
+ from seampact.codegen.runtime.ir_view import IrMap, ir_map, ir_maps, ir_string, ir_true
18
+ from seampact.hooks import Absent
19
+
20
+ _LIST_SHAPES: Final = frozenset({"list", "page"})
21
+ _TABLE_SHAPE: Final = "table"
22
+ _OBJECT_SCHEMA: Final = "object"
23
+ _WHOLE_MINUTE: Final = timedelta(minutes=1)
24
+ _FRACTION_DIGITS: Final = 6
25
+
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class PythonTree:
29
+ """The tree a model validates; `unknown_keys` holds the wire pointer of each unknown member taken out."""
30
+
31
+ value: object
32
+ unknown_keys: tuple[str, ...] = field(default=())
33
+
34
+
35
+ class WireCodec:
36
+ def __init__(self, contract: ApiContract) -> None:
37
+ self._contract = contract
38
+
39
+ def to_python(self, type_use: IrMap, tree: object) -> PythonTree:
40
+ """Raises `LookupError` for a schema the api has no member table or object declaration for."""
41
+ unknown: list[str] = []
42
+ value = self._to_python(type_use, tree, "", unknown)
43
+
44
+ return PythonTree(value, tuple(unknown))
45
+
46
+ def to_wire(self, type_use: IrMap, value: object) -> object:
47
+ """Raises `BoundaryFailureError` for a value the contract cannot write: null for a non-nullable opaque
48
+ value, a date-time without an offset, or an object without a declared attribute."""
49
+ if value is None or value is Absent.ABSENT:
50
+ if ir_string(type_use, "kind") == "opaque" and not ir_true(type_use, "nullable"):
51
+ raise BoundaryFailureError(
52
+ "seampact: the handler wrote null for an opaque value the contract does not make nullable; "
53
+ "return a JSON value, or declare it 'nullable: true'."
54
+ )
55
+
56
+ return None
57
+
58
+ kind = ir_string(type_use, "kind")
59
+
60
+ if kind == "ref":
61
+ return self._object_to_wire(ir_string(type_use, "schema") or "", value)
62
+
63
+ if kind == "array":
64
+ return [self.to_wire(ir_map(type_use, "items"), item) for item in _items(value, "an array value")]
65
+
66
+ if kind in ("scalar", "enum", "opaque"):
67
+ return scalar_to_wire(value)
68
+
69
+ raise LookupError(f"seampact: the type kind {kind!r} is not one the boundary encodes.")
70
+
71
+ def body_to_wire(self, response: IrMap, body: object) -> object:
72
+ """A response body as the wire writes it: a list's items, or a table's values keyed as the handler keyed
73
+ them.
74
+
75
+ Raises `BoundaryFailureError` for a list or page body that is not a list, or a table body that is not a
76
+ mapping."""
77
+ body_type = ir_map(response, "body")
78
+ shape = ir_string(response, "shape")
79
+
80
+ if shape in _LIST_SHAPES:
81
+ return [self.to_wire(body_type, item) for item in _items(body, f"a {shape} body")]
82
+
83
+ if shape == _TABLE_SHAPE:
84
+ return {str(key): self.to_wire(body_type, item) for key, item in _entries(body).items()}
85
+
86
+ return self.to_wire(body_type, body)
87
+
88
+ def _to_python(self, type_use: IrMap, tree: object, pointer: str, unknown: list[str]) -> object:
89
+ kind = ir_string(type_use, "kind")
90
+
91
+ if kind == "ref" and isinstance(tree, dict):
92
+ return self._object_to_python(ir_string(type_use, "schema") or "", tree, pointer, unknown)
93
+
94
+ if kind == "array" and isinstance(tree, list):
95
+ items = ir_map(type_use, "items")
96
+ return [self._to_python(items, item, f"{pointer}/{index}", unknown) for index, item in enumerate(tree)]
97
+
98
+ # A scalar, an enum or an opaque value, or a tree of the wrong type, which the model's type rule reports.
99
+ return tree
100
+
101
+ def _object_to_python(
102
+ self, schema_id: str, tree: dict[str, object], pointer: str, unknown: list[str]
103
+ ) -> dict[str, object]:
104
+ attributes, properties = self._object_schema(schema_id)
105
+ renamed: dict[str, object] = {}
106
+
107
+ for wire_name, value in tree.items():
108
+ attribute = attributes.get(wire_name)
109
+ member_pointer = f"{pointer}/{_escaped(wire_name)}"
110
+
111
+ if attribute is None:
112
+ unknown.append(member_pointer)
113
+ continue
114
+
115
+ renamed[attribute] = self._to_python(ir_map(properties[wire_name], "type"), value, member_pointer, unknown)
116
+
117
+ return renamed
118
+
119
+ def _object_to_wire(self, schema_id: str, value: object) -> dict[str, object]:
120
+ attributes, properties = self._object_schema(schema_id)
121
+ encoded: dict[str, object] = {}
122
+
123
+ for wire_name, attribute in attributes.items():
124
+ if not hasattr(value, attribute):
125
+ raise BoundaryFailureError(
126
+ f"seampact: a {type(value).__qualname__} was written for the schema '{schema_id}', which has the "
127
+ f"member '{attribute}'; return the generated type of the schema."
128
+ )
129
+
130
+ held = getattr(value, attribute)
131
+ member = properties[wire_name]
132
+
133
+ # An optional member that is not nullable holds None when absent; a nullable one holds `Field`.
134
+ absent_none = held is None and ir_true(member, "optional") and not ir_true(member, "nullable")
135
+
136
+ if held is Absent.ABSENT or absent_none:
137
+ continue
138
+
139
+ encoded[wire_name] = None if held is None else self.to_wire(ir_map(member, "type"), held)
140
+
141
+ return encoded
142
+
143
+ def _object_schema(self, schema_id: str) -> tuple[Mapping[str, str], dict[str, IrMap]]:
144
+ schema = self._contract.declaration("schemas", schema_id)
145
+ attributes = self._contract.members.get(schema_id)
146
+
147
+ if ir_string(schema, "kind") != _OBJECT_SCHEMA or attributes is None:
148
+ raise LookupError(f"seampact: no generated object type for the schema '{schema_id}'; regenerate the api.")
149
+
150
+ properties = {ir_string(member, "name") or "": member for member in ir_maps(schema, "properties")}
151
+
152
+ return attributes, properties
153
+
154
+
155
+ def scalar_to_wire(value: object) -> object:
156
+ """A scalar, enum or opaque value as the wire writes it.
157
+
158
+ Raises `BoundaryFailureError` for a date-time without an offset and a decimal that is not finite, which the
159
+ contract cannot write."""
160
+ if isinstance(value, Enum):
161
+ return value.value
162
+
163
+ if isinstance(value, datetime):
164
+ if value.utcoffset() is None:
165
+ raise BoundaryFailureError(
166
+ "seampact: the handler wrote a date-time without an offset; give it a time zone, such as UTC."
167
+ )
168
+
169
+ return _date_time_text(value)
170
+
171
+ if isinstance(value, date):
172
+ return value.isoformat()
173
+
174
+ if isinstance(value, Decimal):
175
+ return _decimal_text(value)
176
+
177
+ if isinstance(value, UUID):
178
+ return str(value)
179
+
180
+ return value
181
+
182
+
183
+ def _date_time_text(value: datetime) -> str:
184
+ """RFC 3339 with the fraction the value holds, without trailing zeros (ADR 0003 section 8); an offset with
185
+ seconds, which RFC 3339 cannot write, is written in UTC."""
186
+ offset = value.utcoffset()
187
+
188
+ if offset is not None and offset % _WHOLE_MINUTE:
189
+ value = value.astimezone(UTC)
190
+
191
+ if value.microsecond == 0:
192
+ return value.isoformat(timespec="seconds")
193
+
194
+ seconds, fraction_and_offset = value.isoformat(timespec="microseconds").split(".", 1)
195
+ fraction = fraction_and_offset[:_FRACTION_DIGITS].rstrip("0")
196
+
197
+ return f"{seconds}.{fraction}{fraction_and_offset[_FRACTION_DIGITS:]}"
198
+
199
+
200
+ def _decimal_text(value: Decimal) -> str:
201
+ """ADR 0003's decimal wire form, never an exponent."""
202
+ if not value.is_finite():
203
+ raise BoundaryFailureError(f"seampact: the handler wrote the decimal {value}; write a finite decimal.")
204
+
205
+ return format(value, "f")
206
+
207
+
208
+ def _items(value: object, held_as: str) -> Sequence[object]:
209
+ if not isinstance(value, list | tuple):
210
+ raise BoundaryFailureError(
211
+ f"seampact: the handler wrote {held_as} as a {type(value).__qualname__}, not a list; return a list."
212
+ )
213
+
214
+ return value
215
+
216
+
217
+ def _entries(value: object) -> Mapping[object, object]:
218
+ if not isinstance(value, Mapping):
219
+ raise BoundaryFailureError(
220
+ f"seampact: the handler wrote a table body as a {type(value).__qualname__}, not a mapping; return a "
221
+ "mapping keyed as the contract keys it."
222
+ )
223
+
224
+ return value
225
+
226
+
227
+ def _escaped(key: str) -> str:
228
+ """A JSON Pointer token (RFC 6901)."""
229
+ return key.replace("~", "~0").replace("/", "~1")
@@ -0,0 +1,260 @@
1
+ """The IR's wire form of path, query and header values (ADR 0003): each is text on the wire, read as the integer,
2
+ number or boolean its type names when the text spells one, and kept as text otherwise, so the type rule reports it.
3
+ The query is read from the raw query string with form decoding, never through a framework's parsed query."""
4
+
5
+ import math
6
+ import re
7
+ from collections.abc import Mapping, Sequence
8
+ from decimal import Decimal
9
+ from typing import Final
10
+ from urllib.parse import quote, unquote_plus, unquote_to_bytes
11
+
12
+ from seampact.codegen.runtime.ir_view import IrMap, ir_int, ir_map, ir_string
13
+ from seampact.codegen.runtime.uuid_text import UUID_TEXT
14
+ from seampact.codegen.runtime.wire_codec import scalar_to_wire
15
+
16
+ _INTEGER: Final = re.compile(r"-?(0|[1-9][0-9]*)")
17
+ _NUMBER: Final = re.compile(r"-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?")
18
+ _TRUE: Final = "true"
19
+ _FALSE: Final = "false"
20
+ _PLACEHOLDER: Final = re.compile(r"\{([^{}]+)\}")
21
+ # `encodeURIComponent` keeps the unreserved set and `!*'()`.
22
+ _COMPONENT_SAFE: Final = "-_.!~*'()"
23
+ # RFC 3986's query characters beside the alphanumerics: what a kept query pair may hold unescaped in a `Link` target.
24
+ _QUERY_SAFE: Final = frozenset(b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~!$&'()*+,;=:@/?")
25
+ _PATH_SAFE: Final = frozenset(b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~/")
26
+ _HEX_DIGITS: Final = frozenset(b"0123456789abcdefABCDEF")
27
+ _PERCENT: Final = ord("%")
28
+ # The raw query is text read from its bytes one to one (`RawTarget`), so a pair goes back to its bytes the same way.
29
+ _BYTES_AS_TEXT: Final = "latin-1"
30
+ # `Number#toString` writes plain digits while the decimal point sits at most 21 digits right and 6 zeros left.
31
+ _DECIMAL_DIGITS_MAX: Final = 21
32
+ _DECIMAL_ZEROS_MIN: Final = -6
33
+ _BOOLEAN: Final = (bool,)
34
+ _SCALAR_TYPES: Final = {"integer": (int,), "number": (int, float), "boolean": _BOOLEAN, "string": (str,)}
35
+
36
+
37
+ def scalar_issue(type_use: IrMap, value: object) -> str | None:
38
+ """The rule a scalar breaks among its type, its `uuid` format and its length bounds, the check a scope id, a
39
+ subject id and a rule's proof get before any hook reads them; None for a use of any other kind, which step 6 checks
40
+ in full."""
41
+ expected = _SCALAR_TYPES.get(ir_string(type_use, "type") or "")
42
+
43
+ if expected is None:
44
+ return None
45
+
46
+ if isinstance(value, bool) and expected is not _BOOLEAN:
47
+ return "type"
48
+
49
+ if not isinstance(value, expected):
50
+ return "type"
51
+
52
+ if not isinstance(value, str):
53
+ return None
54
+
55
+ if ir_string(type_use, "format") == "uuid" and not UUID_TEXT.fullmatch(value):
56
+ return "format"
57
+
58
+ refinements = ir_map(type_use, "refinements")
59
+ min_length = ir_int(refinements, "minLength")
60
+ max_length = ir_int(refinements, "maxLength")
61
+
62
+ if min_length is not None and len(value) < min_length:
63
+ return "minLength"
64
+
65
+ if max_length is not None and len(value) > max_length:
66
+ return "maxLength"
67
+
68
+ return None
69
+
70
+
71
+ def decoded_segment(raw: str) -> str | None:
72
+ """A raw path segment percent-decoded, or None when its bytes are not UTF-8."""
73
+ try:
74
+ return unquote_to_bytes(raw).decode("utf-8")
75
+ except UnicodeDecodeError:
76
+ return None
77
+
78
+
79
+ def path_value(raw: str, type_use: object) -> object:
80
+ decoded = decoded_segment(raw)
81
+
82
+ return raw if decoded is None else scalar(decoded, type_use)
83
+
84
+
85
+ def scalar(text: str, type_use: object) -> object:
86
+ """The text of a parameter read as its scalar type names it, else the text itself."""
87
+ scalar_type = ir_string(type_use, "type") if ir_string(type_use, "kind") == "scalar" else None
88
+
89
+ if scalar_type == "integer" and _INTEGER.fullmatch(text):
90
+ return int(text)
91
+
92
+ if scalar_type == "number" and _NUMBER.fullmatch(text):
93
+ return int(text) if _INTEGER.fullmatch(text) else float(text)
94
+
95
+ if scalar_type == "boolean" and text in (_TRUE, _FALSE):
96
+ return text == _TRUE
97
+
98
+ return text
99
+
100
+
101
+ def raw_query(raw: str) -> dict[str, list[str]]:
102
+ """Each query member by its form-decoded name, with all its raw values in order as received."""
103
+ members: dict[str, list[str]] = {}
104
+
105
+ for pair in raw.split("&"):
106
+ if pair == "":
107
+ continue
108
+
109
+ name, _, value = pair.partition("=")
110
+ members.setdefault(_form_decoded(name), []).append(value)
111
+
112
+ return members
113
+
114
+
115
+ def list_items(values: Sequence[str], wire: object) -> list[str]:
116
+ """A parameter's values form-decoded; a list written with `explode: false` is one comma-separated value, split
117
+ on raw commas only, so an encoded `%2C` stays data within one item."""
118
+ unexploded = ir_string(wire, "kind") == "list" and isinstance(wire, Mapping) and wire.get("explode") is False
119
+ items: list[str] = []
120
+
121
+ for value in values:
122
+ items.extend(_form_decoded(item) for item in (value.split(",") if unexploded else [value]))
123
+
124
+ return items
125
+
126
+
127
+ def query_value(values: Sequence[str], type_use: object) -> object:
128
+ """An array parameter takes every value, a scalar one its only value; several values for a scalar stay a list,
129
+ which the type rule refuses."""
130
+ if ir_string(type_use, "kind") == "array":
131
+ items = ir_map(type_use, "items")
132
+ return [scalar(value, items) for value in values]
133
+
134
+ return scalar(values[0], type_use) if len(values) == 1 else list(values)
135
+
136
+
137
+ def _form_decoded(text: str) -> str:
138
+ """`+` is a space; bytes that are not UTF-8 keep the text as received, so the type rule reports it."""
139
+ try:
140
+ return unquote_plus(text, encoding="utf-8", errors="strict")
141
+ except UnicodeDecodeError:
142
+ return text
143
+
144
+
145
+ def contract_path(template: str, values: Mapping[str, object]) -> str:
146
+ """The contract path `template` with each `{wire name}` replaced by its value encoded as one segment, as the
147
+ generated route helpers build a path.
148
+
149
+ Raises `ValueError` for a value that cannot be one segment."""
150
+ return _PLACEHOLDER.sub(
151
+ lambda placeholder: path_segment(placeholder.group(1), values[placeholder.group(1)]), template
152
+ )
153
+
154
+
155
+ def path_segment(name: str, value: object) -> str:
156
+ """A path value as one segment, as the generated clients send it: its wire text, encoded as JavaScript's
157
+ `encodeURIComponent` writes it.
158
+
159
+ Raises `ValueError` for an empty, `.` or `..` text, which a URL parser reads as another route, or a value with no
160
+ wire text."""
161
+ text = _wire_text(name, value)
162
+
163
+ if text in ("", ".", ".."):
164
+ raise ValueError(
165
+ f"The path parameter '{name}' must be one non-empty segment, not '{text}': it would reach another route."
166
+ )
167
+
168
+ return component_encoded(text)
169
+
170
+
171
+ def component_encoded(text: str) -> str:
172
+ """`text` as JavaScript's `encodeURIComponent` writes it: every UTF-8 byte outside the unreserved set and `!*'()`
173
+ percent-encoded."""
174
+ return quote(text, safe=_COMPONENT_SAFE)
175
+
176
+
177
+ def kept_query_pairs(raw: str, names: Sequence[str]) -> list[str]:
178
+ """The raw query's pairs whose form-decoded name is not one of `names`, in order, each made link safe."""
179
+ return [
180
+ link_safe(pair) for pair in raw.split("&") if pair != "" and _form_decoded(pair.partition("=")[0]) not in names
181
+ ]
182
+
183
+
184
+ def link_safe(pair: str) -> str:
185
+ """Bytes outside the RFC 3986 query set, and a `%` that starts no escape, percent-encoded, so a raw `>` or `"` the
186
+ server accepted cannot break a `Link` target built from the pair."""
187
+ return _escaped_outside(pair, _QUERY_SAFE)
188
+
189
+
190
+ def path_link_safe(request_path: str) -> str:
191
+ """The request path with every byte but the unreserved ones and `/` percent-encoded, valid escapes kept, so a
192
+ segment the server accepted adds no relation to a `Link` header or an absolute link built from it."""
193
+ return _escaped_outside(request_path, _PATH_SAFE)
194
+
195
+
196
+ def _escaped_outside(text: str, safe_bytes: frozenset[int]) -> str:
197
+ data = text.encode(_BYTES_AS_TEXT, errors="strict")
198
+ safe: list[str] = []
199
+
200
+ for index, byte in enumerate(data):
201
+ starts_escape = byte == _PERCENT and _is_escape(data, index)
202
+ kept = starts_escape or (byte != _PERCENT and byte in safe_bytes)
203
+ safe.append(chr(byte) if kept else f"%{byte:02X}")
204
+
205
+ return "".join(safe)
206
+
207
+
208
+ def _is_escape(data: bytes, index: int) -> bool:
209
+ return index + 2 < len(data) and data[index + 1] in _HEX_DIGITS and data[index + 2] in _HEX_DIGITS
210
+
211
+
212
+ def scalar_text(value: object) -> str | None:
213
+ """A scalar's wire text as JavaScript's `String(value)` writes it, so every target writes the same text; None for
214
+ an object, an array or a number that is not finite."""
215
+ if isinstance(value, bool):
216
+ return _TRUE if value else _FALSE
217
+
218
+ if isinstance(value, float):
219
+ return _javascript_number(value) if math.isfinite(value) else None
220
+
221
+ wire = scalar_to_wire(value)
222
+
223
+ return str(wire) if isinstance(wire, str | int) else None
224
+
225
+
226
+ def _wire_text(name: str, value: object) -> str:
227
+ if isinstance(value, float) and not math.isfinite(value):
228
+ raise ValueError(f"The path parameter '{name}' must be a finite number, not {value}.")
229
+
230
+ text = scalar_text(value)
231
+
232
+ if text is None:
233
+ raise ValueError(f"The path parameter '{name}' has no wire text: {type(value).__qualname__}.")
234
+
235
+ return text
236
+
237
+
238
+ def _javascript_number(value: float) -> str:
239
+ """The text JavaScript's `String(value)` writes of a finite number: plain digits from 1e-7 up to 1e21, an exponent
240
+ outside."""
241
+ if value == 0:
242
+ return "0"
243
+
244
+ _sign, digit_tuple, exponent = Decimal(repr(abs(value))).normalize().as_tuple()
245
+ digits = "".join(str(digit) for digit in digit_tuple)
246
+ point = len(digits) + int(exponent)
247
+ prefix = "-" if value < 0 else ""
248
+
249
+ if len(digits) <= point <= _DECIMAL_DIGITS_MAX:
250
+ return prefix + digits + "0" * (point - len(digits))
251
+
252
+ if 0 < point <= _DECIMAL_DIGITS_MAX:
253
+ return prefix + digits[:point] + "." + digits[point:]
254
+
255
+ if _DECIMAL_ZEROS_MIN < point <= 0:
256
+ return prefix + "0." + "0" * -point + digits
257
+
258
+ mantissa = digits[0] + ("." + digits[1:] if len(digits) > 1 else "")
259
+
260
+ return f"{prefix}{mantissa}e{'+' if point > 0 else '-'}{abs(point - 1)}"
@@ -0,0 +1,35 @@
1
+ """Lines of generated Python that ruff formats as written: the package-relative imports, which ruff sorts after the
2
+ seampact ones, one blank line before the first statement and two before a class or function."""
3
+
4
+ from collections.abc import Sequence
5
+ from typing import Final
6
+
7
+ from seampact.codegen.printer import INDENT, fits
8
+
9
+ _DEFINITIONS: Final = ("class ", "def ", "async def ", "@")
10
+ OPERATIONS_MODULE: Final = "operations"
11
+
12
+
13
+ def with_local_imports(modules: Sequence[str], names: Sequence[str], first_block: Sequence[str]) -> list[str]:
14
+ """The first block of a module, preceded by `from . import <module> as _<module>` for each sibling module and
15
+ `from .operations import <names>` for names of the package's operations module, sorted as ruff sorts them."""
16
+ imports = [f"from . import {module} as _{module}" for module in sorted(modules)]
17
+ ordered = sorted(set(names), key=_member_order)
18
+ single = f"from .{OPERATIONS_MODULE} import {', '.join(ordered)}"
19
+
20
+ if ordered and fits(single):
21
+ imports.append(single)
22
+ elif ordered:
23
+ imports += [f"from .{OPERATIONS_MODULE} import (", *(f"{INDENT}{name}," for name in ordered), ")"]
24
+
25
+ separator = ["", ""] if first_block and first_block[0].startswith(_DEFINITIONS) else [""]
26
+
27
+ return [*imports, *separator, *first_block]
28
+
29
+
30
+ def _member_order(name: str) -> tuple[int, str]:
31
+ """ruff's `order-by-type`: constants, then classes, then the rest; modules are lower case."""
32
+ if name.isupper():
33
+ return (0, name)
34
+
35
+ return (1, name) if name[:1].isupper() else (2, name)
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.5
2
+ Name: seampact-codegen
3
+ Version: 1.0.0
4
+ Summary: The neutral code generation of the seampact Python targets and the framework-free runtime of generated code.
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Typing :: Typed
10
+ Requires-Python: >=3.12
11
+ Requires-Dist: seampact-hooks==1.0.0
12
+ Provides-Extra: generate
13
+ Requires-Dist: seampact-ir==1.0.0; extra == 'generate'
14
+ Provides-Extra: pydantic
15
+ Requires-Dist: pydantic<3,>=2.9; extra == 'pydantic'
16
+ Description-Content-Type: text/markdown
17
+
18
+ The neutral code generation plan and Pydantic emitter of the Python targets, and the framework-free runtime their generated code imports.
@@ -0,0 +1,76 @@
1
+ seampact/codegen/__init__.py,sha256=2mu3bKeEzNhyx8_K77J87SITftXRq7E1rq5GUgODwaY,228
2
+ seampact/codegen/boot_check_hooks.py,sha256=AAu6AGEHhAzj5f-7May3hILcjahU95ygpLbXc8mICac,1262
3
+ seampact/codegen/contract_module.py,sha256=L795efHj-JfH3f2YR2m7IBZwLC1-En77B_VaVyd8wLo,6077
4
+ seampact/codegen/credentials_classes.py,sha256=eF_yJ7Pw5Wz3JS9cFXu2UgAncB4SLjKhNAmVmrtv6VI,4213
5
+ seampact/codegen/handler_forms.py,sha256=w3NsBSepz8-qw2S23d5oMPGSC6kJ9xELF8AH1WSaPTQ,4723
6
+ seampact/codegen/input_schemas.py,sha256=JlOrYFp-7pQKZ2FFuVGoM4TVi3RNSJLEuGRd7xNKz2s,4621
7
+ seampact/codegen/kept_forms.py,sha256=ssg8qy0QqGCBTvdhU15fE0w9-oOtNxgk4L9JkC75w5E,22186
8
+ seampact/codegen/matrix_row.py,sha256=T0S8f3JyeCaJvyoUoHTDdkHw6AMBkZbmnaAnQ9-rg9g,991
9
+ seampact/codegen/names.py,sha256=Oc7FWQLK9W5YQRGAX-Ddl38RP-n3EwZ13DY1SOOXqXo,6084
10
+ seampact/codegen/native_docs_file.py,sha256=llDo1G5qs9lHkykaC2RPbAn7rMPnAErjKfrlMsUW_b8,6900
11
+ seampact/codegen/operation_types.py,sha256=uE2mtYpwNwd8qwuld2Wk0qM9flu-vyZ2Gn4nrUTiLOc,17149
12
+ seampact/codegen/placement_lines.py,sha256=ZHDrcOdqX5HVTKl5TC5Xe32xQHBW0jakMykppSvDwmA,3130
13
+ seampact/codegen/printer.py,sha256=Q-agn08z_dHz1vFHYqp27WWJQRBJ4KfUc4k4H8itNx4,7116
14
+ seampact/codegen/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ seampact/codegen/pydantic_forms.py,sha256=nbDQEDfTFTfI2sk9R2WHHwGxDr8kBxzaTiuzbEa-eTE,2377
16
+ seampact/codegen/pydantic_layout.py,sha256=iV_JBHD4RfU0CDBhQ8KqzwhMZfkSMc35NgzwljZpGuI,6904
17
+ seampact/codegen/pydantic_models.py,sha256=AnPgzoNm2ZuxFD7tmtGA94P7Kf2Hs6_aNPWzly8iWvU,16331
18
+ seampact/codegen/redacted_members.py,sha256=LlnJ-B1vRmhWf6t-tmMsdNX_evDFXokjfIgmtvtUYWg,1873
19
+ seampact/codegen/refusals.py,sha256=Wc9n3u4YAxETZfrOIfPt8N2DV2hBp1KZBOwVMB2M_9w,3450
20
+ seampact/codegen/request_deadline.py,sha256=pGQtpKoeIyaJfa7nCZ84dFVAOnE6rZbbn02LztWJCbM,2677
21
+ seampact/codegen/response_header_forms.py,sha256=ZFtgaK1M3df_SZyxyilwvY2ZI3MzmDkZ7LIhU-WbTNs,3213
22
+ seampact/codegen/restricted_sort_keys.py,sha256=MJUV055aE442MxiQ6nsTzNIz4qrmEWZe29nUQYgAJMM,746
23
+ seampact/codegen/route_helpers.py,sha256=aExfo5Ig3JlXZ6r3mKNzuYXlJ5j1_P8OIrV5LjTQ3vo,4675
24
+ seampact/codegen/routes.py,sha256=8LQomkDOp_enNvFivzX2w62Unk4-4BU3r2EkO-2-VJ4,5006
25
+ seampact/codegen/source_lines.py,sha256=xLrhARNjoVaPoLyIbX0MTSfLtfmEjxA3qvkp0KsxXTY,1565
26
+ seampact/codegen/runtime/__init__.py,sha256=A6lh9ZgdhKXBi-K4cwURJ6bIcciOJNcHFRtE2llcUr8,437
27
+ seampact/codegen/runtime/access_kinds.py,sha256=RV4LKI_Bfj3nQL-FrzVcSDQtzIKJWH1sm_OczP4PPbI,1101
28
+ seampact/codegen/runtime/access_steps.py,sha256=YkXtdSPNJDBIEvnDx6YSCxJF37sUYKklncqX8u5qr_4,20754
29
+ seampact/codegen/runtime/body_reader.py,sha256=hKdFyUZQ9HQREORHvQeJIkPtSfkrvcS84Hv1gi-qjus,2442
30
+ seampact/codegen/runtime/cache_headers.py,sha256=L26z4tpBIHVNxFoWYdhGEugbRkcgcYji7HAXOg3EDcA,3257
31
+ seampact/codegen/runtime/contract.py,sha256=MR4dYzOWsVfc2IpDoqFC7OKMNkHi5ux8B52QhBmkCOk,11104
32
+ seampact/codegen/runtime/cors_preflight.py,sha256=tUQIGr0yKJIx5_uRp6SNB0fKiVvW9TTzJPeH11AgxrM,809
33
+ seampact/codegen/runtime/deadline.py,sha256=sB6zzouRGFCiS3BQKKO4aWhfnNCv7DeZHxeDgSXH4D4,3002
34
+ seampact/codegen/runtime/digests.py,sha256=lw6wcesv5Oj595ZAbCN0wO9kl08zG8X_hdD3sDxntj0,3694
35
+ seampact/codegen/runtime/failures.py,sha256=_00KuIjxhXg-yanuMYG4hcRmuUfhDRv99G9Vev4yovk,1479
36
+ seampact/codegen/runtime/field_rights.py,sha256=pmFG19ZvJq8jH0E5DrKv5SXywG_FKaU-4gdk7KDIrTI,7570
37
+ seampact/codegen/runtime/framework_routes.py,sha256=1_ZFqoKvZWXRr5TPXwGBp6P1dngUj4uJ-GoXoxlgHHg,1393
38
+ seampact/codegen/runtime/framework_versions.py,sha256=5Szre-JtK-rI5IcL5CF5ACQFnj0QtrtLoN74HSazTpo,3504
39
+ seampact/codegen/runtime/handler_call.py,sha256=Sv1STll1lGUJXq6D3pXw4Efpj1id6gWtskw_IyjW0H4,1408
40
+ seampact/codegen/runtime/handler_reply.py,sha256=RL0J9kTYpb_0kvP4TJQmcgW4oAAZdjG9THhj3uk7jHM,6842
41
+ seampact/codegen/runtime/hinted_values.py,sha256=UqkVIudACNMp9ehG0vY40Z13WAAIGIP6GI7a1J950Hs,1875
42
+ seampact/codegen/runtime/hook_check.py,sha256=OdLycQIMNeAop_bYUuIXX3MIw7EazRS6EoHvLygVplc,2754
43
+ seampact/codegen/runtime/idempotency.py,sha256=f0prdZC4RyCL4dHWS5nk-cTlCuqh4cwf1fz4eZwD73Q,11651
44
+ seampact/codegen/runtime/input_decoding.py,sha256=COXqqLvV9xyAGQRICB-Q5PWjB79h613U544fbg-TyMw,8236
45
+ seampact/codegen/runtime/ir_view.py,sha256=uf1NXSkBssUiVyXvIotp-yEU67CfVxXgisMHzpitm5Q,1666
46
+ seampact/codegen/runtime/native_docs.py,sha256=tce0NSXrpXpjTgeMWhe7Xu2SIqkRnlwY-VLmVSrl96I,4889
47
+ seampact/codegen/runtime/page_facts.py,sha256=EUdFL7SwY7F1pDP_e2ZXcG5VxR2ybyjJ9NMMIBEBZPQ,1444
48
+ seampact/codegen/runtime/page_reply.py,sha256=96sCe8PGUhHsSlQk5ax28dfcGDVclWHsesXc2fVCVH0,15250
49
+ seampact/codegen/runtime/pattern.py,sha256=k8SAv6fgzK9vpfcgxEr3HGJmY5gWCkviYQkH6BalLDo,2279
50
+ seampact/codegen/runtime/principal_members.py,sha256=rp1YOJOm87xyMRLMs9vQfOrEYStt8YN8S1I-rUKtU5M,1947
51
+ seampact/codegen/runtime/public_origin.py,sha256=tyPV6uEHN9xnhaXG7pUEFzKFBegQsCXFcXt0i60Kmyo,3925
52
+ seampact/codegen/runtime/raw_target.py,sha256=Ph6-ksb14qA9EgBTqCv260CLjhoBm4AtEbpbaZW3310,3699
53
+ seampact/codegen/runtime/replies.py,sha256=czlcIvR9l3hep6KeUm6E81EDomXpo4v74oiTxM1GTU4,5743
54
+ seampact/codegen/runtime/reply_verdict.py,sha256=H6vMAeydO1f4W75_9Ore1LZUHcpCHJfruaeqk6IrYgw,7428
55
+ seampact/codegen/runtime/requirement_params.py,sha256=VthIFeHGgSYmfBkBfcbmAQdiDG_BUHU62qdkF1NHwF8,7514
56
+ seampact/codegen/runtime/response_header_entries.py,sha256=9LZu5RhnTrHnioq9dzpdXK-ZuumueNfbYbihsXwQ9N0,7803
57
+ seampact/codegen/runtime/response_headers.py,sha256=C3cE3QAgO6_vnppaegp1kOQyyJHfz_w6qpvIiTo2DbA,5465
58
+ seampact/codegen/runtime/route_match.py,sha256=-T8ul7EW0H_YZuSnyp5A5AKwugfd-MWxJvJ_ipr8UCM,3128
59
+ seampact/codegen/runtime/run_settings.py,sha256=EMQgl0Ru9idKO0zCYlAufhrTRHukMC-XSqRgof95EAQ,1263
60
+ seampact/codegen/runtime/signature_header.py,sha256=38Zh-vZJbiefA7FrGq23l_HmudcyhEhXSTfJgDK0lsg,2980
61
+ seampact/codegen/runtime/sync_driver.py,sha256=oPVMo7vUAuPTGmr50hMrhWRxFdRs3MP65ExfiuB2YWk,1366
62
+ seampact/codegen/runtime/templates.py,sha256=5PbFqSZhGK2LzoiJeZUSEBbJjuZSc1AX8hjnqQOIj5o,4062
63
+ seampact/codegen/runtime/uuid_text.py,sha256=uC7kmx6B_LRA35xQ241vvjKvfvY7x8GYFAEXJfqkPEE,327
64
+ seampact/codegen/runtime/value_rules.py,sha256=MDY3umSM9ZYfgRvIbYoO3KEwVr1bzp04N8HwSkU1f1E,8292
65
+ seampact/codegen/runtime/version.py,sha256=E4sGeqn4loulZ_8BpjcCCXTmC9TQU_HoE4pXjxFPjOU,2491
66
+ seampact/codegen/runtime/version_token.py,sha256=R1_Apddd4EB6IO8fwbYktMTyWY2Lgna53WIrnxIL9n4,5050
67
+ seampact/codegen/runtime/wire_codec.py,sha256=ImIpQ60MFL5LUcLdYYr-qn27orO2aFE-Mp4iqHqP2pM,9215
68
+ seampact/codegen/runtime/wire_values.py,sha256=OSSei111cbLTVCdz5a1zV0RNaeQ1YBrROuBbsQ8PeaQ,10177
69
+ seampact/codegen/runtime/pydantic/__init__.py,sha256=JPg2u9lS-GibWiKyfO78PKszSLZxPq8CzbN23mV5aHM,6039
70
+ seampact/codegen/runtime/pydantic/checked_input.py,sha256=qHlkV7VD6EcZH2IrDT6RWCev6kx4TraPhvJMZYpJecQ,2377
71
+ seampact/codegen/runtime/pydantic/contract_input.py,sha256=GPOkU5sQCUw_-vFUZFoX7w-83Fz1Bihi0H2MZH5jkjc,8431
72
+ seampact/codegen/runtime/pydantic/issues.py,sha256=rWCnYrwtAHQAwVnItDm_FNXrayhQTquifsOpvJ2MKu0,5295
73
+ seampact_codegen-1.0.0.dist-info/METADATA,sha256=P0MWFggtZ72Jebo7xlDG6M6n6PvE3-riesdsZ49ycgo,754
74
+ seampact_codegen-1.0.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
75
+ seampact_codegen-1.0.0.dist-info/licenses/LICENSE,sha256=5ZUBhQlzdoQ7CzKtydHCW5CGsxG5ucoOu1lKo7k3RrY,1070
76
+ seampact_codegen-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Julien Metral
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.