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.
- seampact/codegen/__init__.py +2 -0
- seampact/codegen/boot_check_hooks.py +38 -0
- seampact/codegen/contract_module.py +146 -0
- seampact/codegen/credentials_classes.py +99 -0
- seampact/codegen/handler_forms.py +132 -0
- seampact/codegen/input_schemas.py +130 -0
- seampact/codegen/kept_forms.py +522 -0
- seampact/codegen/matrix_row.py +23 -0
- seampact/codegen/names.py +146 -0
- seampact/codegen/native_docs_file.py +169 -0
- seampact/codegen/operation_types.py +425 -0
- seampact/codegen/placement_lines.py +79 -0
- seampact/codegen/printer.py +196 -0
- seampact/codegen/py.typed +0 -0
- seampact/codegen/pydantic_forms.py +53 -0
- seampact/codegen/pydantic_layout.py +178 -0
- seampact/codegen/pydantic_models.py +405 -0
- seampact/codegen/redacted_members.py +47 -0
- seampact/codegen/refusals.py +84 -0
- seampact/codegen/request_deadline.py +68 -0
- seampact/codegen/response_header_forms.py +73 -0
- seampact/codegen/restricted_sort_keys.py +18 -0
- seampact/codegen/route_helpers.py +109 -0
- seampact/codegen/routes.py +139 -0
- seampact/codegen/runtime/__init__.py +7 -0
- seampact/codegen/runtime/access_kinds.py +24 -0
- seampact/codegen/runtime/access_steps.py +520 -0
- seampact/codegen/runtime/body_reader.py +64 -0
- seampact/codegen/runtime/cache_headers.py +89 -0
- seampact/codegen/runtime/contract.py +232 -0
- seampact/codegen/runtime/cors_preflight.py +20 -0
- seampact/codegen/runtime/deadline.py +72 -0
- seampact/codegen/runtime/digests.py +98 -0
- seampact/codegen/runtime/failures.py +33 -0
- seampact/codegen/runtime/field_rights.py +193 -0
- seampact/codegen/runtime/framework_routes.py +36 -0
- seampact/codegen/runtime/framework_versions.py +91 -0
- seampact/codegen/runtime/handler_call.py +31 -0
- seampact/codegen/runtime/handler_reply.py +148 -0
- seampact/codegen/runtime/hinted_values.py +48 -0
- seampact/codegen/runtime/hook_check.py +77 -0
- seampact/codegen/runtime/idempotency.py +279 -0
- seampact/codegen/runtime/input_decoding.py +226 -0
- seampact/codegen/runtime/ir_view.py +47 -0
- seampact/codegen/runtime/native_docs.py +130 -0
- seampact/codegen/runtime/page_facts.py +33 -0
- seampact/codegen/runtime/page_reply.py +396 -0
- seampact/codegen/runtime/pattern.py +69 -0
- seampact/codegen/runtime/principal_members.py +41 -0
- seampact/codegen/runtime/public_origin.py +102 -0
- seampact/codegen/runtime/pydantic/__init__.py +165 -0
- seampact/codegen/runtime/pydantic/checked_input.py +52 -0
- seampact/codegen/runtime/pydantic/contract_input.py +183 -0
- seampact/codegen/runtime/pydantic/issues.py +142 -0
- seampact/codegen/runtime/raw_target.py +103 -0
- seampact/codegen/runtime/replies.py +144 -0
- seampact/codegen/runtime/reply_verdict.py +215 -0
- seampact/codegen/runtime/requirement_params.py +170 -0
- seampact/codegen/runtime/response_header_entries.py +226 -0
- seampact/codegen/runtime/response_headers.py +114 -0
- seampact/codegen/runtime/route_match.py +88 -0
- seampact/codegen/runtime/run_settings.py +35 -0
- seampact/codegen/runtime/signature_header.py +92 -0
- seampact/codegen/runtime/sync_driver.py +35 -0
- seampact/codegen/runtime/templates.py +132 -0
- seampact/codegen/runtime/uuid_text.py +7 -0
- seampact/codegen/runtime/value_rules.py +218 -0
- seampact/codegen/runtime/version.py +59 -0
- seampact/codegen/runtime/version_token.py +108 -0
- seampact/codegen/runtime/wire_codec.py +229 -0
- seampact/codegen/runtime/wire_values.py +260 -0
- seampact/codegen/source_lines.py +35 -0
- seampact_codegen-1.0.0.dist-info/METADATA +18 -0
- seampact_codegen-1.0.0.dist-info/RECORD +76 -0
- seampact_codegen-1.0.0.dist-info/WHEEL +4 -0
- 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 ""
|