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,18 @@
1
+ """A sort key restricted by a right, refused by name for every Python target: none checks the right on a sort."""
2
+
3
+ from seampact.codegen.refusals import Delivery, IrPlace, Refusals
4
+ from seampact.ir.model import Operation
5
+
6
+
7
+ def refuse_restricted_sort_keys(operation: Operation, at: IrPlace, refusals: Refusals) -> None:
8
+ if operation.paging is None:
9
+ return
10
+
11
+ for position, sort_key in enumerate(operation.paging.sort_keys):
12
+ if sort_key.right is not None:
13
+ refusals.waiting(
14
+ "paging.sortKey.right",
15
+ f"The sort key '{sort_key.key}' restricted by the right '{sort_key.right}'",
16
+ at.below("paging", "sortKeys", position),
17
+ Delivery.CONSUMER,
18
+ )
@@ -0,0 +1,109 @@
1
+ """The route helpers of one api (ADR 0023 and ADR 0026 "Routes"): `<Api>RouteNames`, one constant per operation
2
+ holding the route name the placement registers, and `<Api>Urls`, one static method per operation building the
3
+ contract's path from one typed argument per path parameter, in path order, each value encoded as one segment by the
4
+ runtime. The artefact switch `routeHelpers: false` leaves both out."""
5
+
6
+ from collections.abc import Sequence
7
+ from typing import Final
8
+
9
+ from seampact.codegen.printer import INDENT, PythonModule, fits, string_literal
10
+ from seampact.codegen.refusals import Delivery, IrPlace, Refusals
11
+ from seampact.codegen.routes import RouteEntry, placeholders
12
+ from seampact.ir.model import ContractIR, EnumTypeUse, Operation, ScalarType, ScalarTypeUse, TypeUse
13
+ from seampact.ir.naming import IrName, NamingRole, python_identifier
14
+
15
+ _SCALAR_ANNOTATIONS: Final = {
16
+ ScalarType.STRING: "str",
17
+ ScalarType.INTEGER: "int",
18
+ ScalarType.NUMBER: "float",
19
+ ScalarType.BOOLEAN: "bool",
20
+ }
21
+ _FORMAT_ANNOTATIONS: Final = {
22
+ "uuid": ("uuid", "_uuid", "_uuid.UUID"),
23
+ "date-time": ("datetime", "_datetime", "_datetime.datetime"),
24
+ }
25
+
26
+
27
+ _NAMES_REGISTERED: Final = "The route name of each operation, as the target registers it."
28
+
29
+
30
+ def route_helpers_module(
31
+ ir: ContractIR, routes: Sequence[RouteEntry], refusals: Refusals, names_registered: str = _NAMES_REGISTERED
32
+ ) -> str:
33
+ """`names_registered` is the route names' docstring: a target that registers them under a namespace says so."""
34
+ api_class = python_identifier(IrName(tuple(ir.api.name.segments), NamingRole.SCHEMA))
35
+ module = PythonModule(f"The route names and contract paths of api '{ir.api.id}'.")
36
+ typing = module.imported("typing", "_typing")
37
+ wire = module.imported("seampact.codegen.runtime.wire_values", "_wire_values")
38
+ operations = {operation.id: (index, operation) for index, operation in enumerate(ir.operations)}
39
+ names = [
40
+ f"class {api_class}RouteNames:",
41
+ f'{INDENT}"""{names_registered}"""',
42
+ "",
43
+ ]
44
+ urls = [f"class {api_class}Urls:", f'{INDENT}"""The path of each operation, each value encoded as one segment."""']
45
+
46
+ for entry in sorted(routes, key=lambda route: operations[route.operation_id][0]):
47
+ index, operation = operations[entry.operation_id]
48
+ constant = python_identifier(IrName(tuple(operation.name.segments), NamingRole.ENUM_MEMBER))
49
+ names += [f"{INDENT}{constant}: {typing}.Final = {string_literal(entry.name)}"]
50
+ urls += [
51
+ "",
52
+ *_url_method(module, wire, entry, operation, IrPlace(f"/operations/{index}", operation.id), refusals),
53
+ ]
54
+
55
+ module.add_block(names)
56
+ module.add_block(urls)
57
+
58
+ return module.text()
59
+
60
+
61
+ def _url_method(
62
+ module: PythonModule, wire: str, entry: RouteEntry, operation: Operation, at: IrPlace, refusals: Refusals
63
+ ) -> list[str]:
64
+ method = python_identifier(IrName(tuple(operation.name.segments), NamingRole.OPERATION))
65
+ types = {param.name: param.type for param in operation.params.path}
66
+ parameters = [
67
+ f"{segment.attribute}: {_annotation(module, types[segment.text], at, refusals)}"
68
+ for segment in placeholders(entry)
69
+ ]
70
+ doc = f'{INDENT * 2}"""The path of operation \'{operation.id}\', `{entry.contract_path}`."""'
71
+ head = [f"{INDENT}@staticmethod", f"{INDENT}def {method}({', '.join(parameters)}) -> str:", doc]
72
+ path = string_literal(entry.contract_path)
73
+
74
+ if not parameters:
75
+ return [*head, f"{INDENT * 2}return {path}"]
76
+
77
+ values = "{" + ", ".join(f"{string_literal(s.text)}: {s.attribute}" for s in placeholders(entry)) + "}"
78
+ single = f"{INDENT * 2}return {wire}.contract_path({path}, {values})"
79
+
80
+ if fits(single):
81
+ return [*head, single]
82
+
83
+ return [
84
+ *head,
85
+ f"{INDENT * 2}return {wire}.contract_path(",
86
+ f"{INDENT * 3}{path},",
87
+ f"{INDENT * 3}{values},",
88
+ f"{INDENT * 2})",
89
+ ]
90
+
91
+
92
+ def _annotation(module: PythonModule, type_use: TypeUse, at: IrPlace, refusals: Refusals) -> str:
93
+ # An enum member is a `StrEnum` member, so a `str` takes it.
94
+ if isinstance(type_use, EnumTypeUse):
95
+ return "str"
96
+
97
+ if isinstance(type_use, ScalarTypeUse) and isinstance(type_use.type, ScalarType):
98
+ formatted = _FORMAT_ANNOTATIONS.get(type_use.format or "")
99
+
100
+ if formatted is not None:
101
+ module.imported(formatted[0], formatted[1])
102
+ return formatted[2]
103
+
104
+ return _SCALAR_ANNOTATIONS.get(type_use.type, "str")
105
+
106
+ refusals.waiting(
107
+ "path.param.type", f"A path parameter of kind '{type_use.kind}'", at.below("params", "path"), Delivery.CONSUMER
108
+ )
109
+ return "str"
@@ -0,0 +1,139 @@
1
+ """The route table of one api, as every Python placement declares it (ADR 0026 "Routes"): one route per operation,
2
+ named `seampact.<api>.<operationId>`, its path in the framework's placeholder syntax with each placeholder named after
3
+ the parameter's attribute, `GET` also serving `HEAD`, and literal segments before placeholders at each depth. Static
4
+ segments holding `{`, `}` or `?`, segments a client sends percent-encoded, and segments that mix a placeholder with
5
+ text are refused by name."""
6
+
7
+ import re
8
+ from collections.abc import Sequence
9
+ from dataclasses import dataclass
10
+ from functools import cmp_to_key
11
+ from typing import Final
12
+
13
+ from seampact.codegen.names import member_identifier
14
+ from seampact.codegen.refusals import Delivery, IrPlace, Refusals
15
+ from seampact.ir.model import ContractIR, HttpMethod, Operation
16
+
17
+ _PLACEHOLDER: Final = re.compile(r"\{([^{}]+)\}")
18
+ # What a client sends unencoded in a path segment (RFC 3986 pchar without `%`).
19
+ _SENT_AS_IS: Final = re.compile(r"[A-Za-z0-9\-._~!$&'()*+,;=:@]*")
20
+ _LITERAL_SYNTAX: Final = frozenset("{}?")
21
+
22
+
23
+ @dataclass(frozen=True, slots=True)
24
+ class RouteSegment:
25
+ """A literal segment's text, or a placeholder's wire name and attribute."""
26
+
27
+ text: str
28
+ attribute: str | None = None
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class RouteEntry:
33
+ operation_id: str
34
+ name: str
35
+ methods: tuple[str, ...]
36
+ contract_path: str
37
+ segments: tuple[RouteSegment, ...]
38
+
39
+ def path(self) -> str:
40
+ """The path with `{attribute}` placeholders, as Starlette, Django and Ninja name them."""
41
+ return "".join(
42
+ "/" + (segment.text if segment.attribute is None else f"{{{segment.attribute}}}")
43
+ for segment in self.segments
44
+ )
45
+
46
+
47
+ def route_name(api_id: str, operation_id: str) -> str:
48
+ return f"seampact.{api_id}.{operation_id}"
49
+
50
+
51
+ def route_table(ir: ContractIR, mount: str, refusals: Refusals) -> list[RouteEntry]:
52
+ entries = [
53
+ entry
54
+ for index, operation in enumerate(ir.operations)
55
+ if (entry := _entry(ir.api.id, operation, mount, IrPlace(f"/operations/{index}", operation.id), refusals))
56
+ ]
57
+
58
+ return sorted(entries, key=cmp_to_key(_literals_first))
59
+
60
+
61
+ def _entry(api_id: str, operation: Operation, mount: str, at: IrPlace, refusals: Refusals) -> RouteEntry | None:
62
+ if not isinstance(operation.method, HttpMethod):
63
+ refusals.unknown_value(operation.method.raw, at.below("method"))
64
+ return None
65
+
66
+ method = operation.method.value.upper()
67
+ contract_path = mount + operation.path
68
+ segments = [_segment(text, operation, at.below("path"), refusals) for text in contract_path.split("/")[1:]]
69
+
70
+ if any(segment is None for segment in segments):
71
+ return None
72
+
73
+ methods = (method, "HEAD") if method == "GET" else (method,)
74
+
75
+ return RouteEntry(
76
+ operation.id,
77
+ route_name(api_id, operation.id),
78
+ methods,
79
+ contract_path,
80
+ tuple(segment for segment in segments if segment is not None),
81
+ )
82
+
83
+
84
+ def _segment(text: str, operation: Operation, at: IrPlace, refusals: Refusals) -> RouteSegment | None:
85
+ placeholder = _PLACEHOLDER.fullmatch(text)
86
+
87
+ if placeholder is not None:
88
+ return _placeholder(placeholder.group(1), operation, at, refusals)
89
+
90
+ if "{" in text and "}" in text:
91
+ refusals.waiting(
92
+ "path.segment.mixed", f"The path segment '{text}', a placeholder mixed with text", at, Delivery.CONSUMER
93
+ )
94
+ return None
95
+
96
+ if _LITERAL_SYNTAX.intersection(text):
97
+ refusals.waiting(
98
+ "path.segment.literal",
99
+ f"The static path segment '{text}', holding '{{', '}}' or '?'",
100
+ at,
101
+ Delivery.CONSUMER,
102
+ )
103
+ return None
104
+
105
+ if _SENT_AS_IS.fullmatch(text) is None:
106
+ refusals.waiting(
107
+ "path.segment.encoded",
108
+ f"The static path segment '{text}', which a client percent-encodes",
109
+ at,
110
+ Delivery.CONSUMER,
111
+ )
112
+ return None
113
+
114
+ return RouteSegment(text)
115
+
116
+
117
+ def _placeholder(wire_name: str, operation: Operation, at: IrPlace, refusals: Refusals) -> RouteSegment | None:
118
+ param = next((param for param in operation.params.path if param.name == wire_name), None)
119
+ attribute = None if param is None else member_identifier(param.name)
120
+
121
+ if attribute is None:
122
+ refusals.inconsistent(
123
+ "path.param.missing", f"The path placeholder '{{{wire_name}}}' names no path parameter", at
124
+ )
125
+ return None
126
+
127
+ return RouteSegment(wire_name, attribute)
128
+
129
+
130
+ def _literals_first(first: RouteEntry, second: RouteEntry) -> int:
131
+ for mine, theirs in zip(first.segments, second.segments, strict=False):
132
+ if (mine.attribute is None) != (theirs.attribute is None):
133
+ return -1 if mine.attribute is None else 1
134
+
135
+ return len(first.segments) - len(second.segments)
136
+
137
+
138
+ def placeholders(entry: RouteEntry) -> Sequence[RouteSegment]:
139
+ return [segment for segment in entry.segments if segment.attribute is not None]
@@ -0,0 +1,7 @@
1
+ """The framework-free runtime of generated Python code: step 1's raw-target match and decoding, the wire codec,
2
+ replies, the outgoing verdict, the pattern check, the runtime version (G3) and the sync driver of the steps, which
3
+ are written once as coroutines. A placement (FastAPI, Django) reads the request and writes the reply around them."""
4
+
5
+ from seampact.codegen.runtime.version import RUNTIME_VERSION
6
+
7
+ __all__ = ["RUNTIME_VERSION"]
@@ -0,0 +1,24 @@
1
+ """The access kind of an operation, which a framework's own security layer is configured by (ADR 0027 "DRF" and
2
+ "Ninja"): `public`, `optional` or `identity` from its access, or `signed` when a signature verifies its identity."""
3
+
4
+ from typing import Final
5
+
6
+ from seampact.codegen.runtime.contract import ApiContract
7
+ from seampact.codegen.runtime.ir_view import ir_map, ir_string
8
+
9
+ SIGNED_KIND: Final = "signed"
10
+ # The kinds of the PHP targets' native security layers: a signed identity never shares the classes of a signed-in
11
+ # caller's routes.
12
+ ACCESS_KINDS: Final = ("public", "optional", "identity", SIGNED_KIND)
13
+
14
+
15
+ def access_kind(contract: ApiContract, operation_id: str) -> str:
16
+ """`public`, `optional` or `identity` from the operation's access, or `signed` when a signature verifies its
17
+ identity."""
18
+ access = ir_map(contract.operation(operation_id), "access")
19
+ identity = ir_string(access, "identity")
20
+
21
+ if identity is not None and ir_string(contract.declaration("identities", identity), "signed") is not None:
22
+ return SIGNED_KIND
23
+
24
+ return ir_string(access, "kind") or ""