umf-codegen 0.10.8__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.
- umf_codegen/__init__.py +198 -0
- umf_codegen/__main__.py +47 -0
- umf_codegen/_embedded.py +2 -0
- umf_codegen/_native.py +445 -0
- umf_codegen/_release.py +8 -0
- umf_codegen/_resources/actions.package.json +1 -0
- umf_codegen/_resources/binding.package.json +384 -0
- umf_codegen/_resources/core.constraints.json +6134 -0
- umf_codegen/_resources/core.domain-policy.json +610 -0
- umf_codegen/_resources/core.package-schema.json +1907 -0
- umf_codegen/_resources/core.reference-schema.json +1907 -0
- umf_codegen/_resources/core.schema.json +1908 -0
- umf_codegen/_resources/manifest.json +1 -0
- umf_codegen/_resources/saga.package.json +616 -0
- umf_codegen/_resources/stable-binding.package.json +385 -0
- umf_codegen/_resources/tables.package.json +1031 -0
- umf_codegen/_writer.py +194 -0
- umf_codegen/py.typed +0 -0
- umf_codegen-0.10.8.dist-info/METADATA +105 -0
- umf_codegen-0.10.8.dist-info/RECORD +25 -0
- umf_codegen-0.10.8.dist-info/WHEEL +4 -0
- umf_codegen-0.10.8.dist-info/entry_points.txt +2 -0
- umf_codegen-0.10.8.dist-info/licenses/LICENSE-APACHE +201 -0
- umf_codegen-0.10.8.dist-info/licenses/LICENSE-MIT +21 -0
- umf_codegen-0.10.8.dist-info/licenses/LICENSE.md +239 -0
umf_codegen/_native.py
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
2
|
+
"""V8-free bridge to the pinned native UMF codegen CLI."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import math
|
|
8
|
+
import os
|
|
9
|
+
import selectors
|
|
10
|
+
import subprocess
|
|
11
|
+
import time
|
|
12
|
+
from collections.abc import Iterator
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
from importlib import resources
|
|
15
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from ._release import (
|
|
20
|
+
CODEGEN_CLI_PROTOCOL_VERSION,
|
|
21
|
+
CODEGEN_PROTOCOL_VERSION,
|
|
22
|
+
RESOURCE_MANIFEST_SHA256,
|
|
23
|
+
UMF_CORE_ENGINE_VERSION,
|
|
24
|
+
UMF_CORE_VERSION,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class GenerationError(ValueError):
|
|
29
|
+
def __init__(self, code: str, message: str, *, path: str | None = None):
|
|
30
|
+
self.code, self.path = code, path
|
|
31
|
+
super().__init__(message)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_MAX_REQUEST = 32 * 1024 * 1024
|
|
35
|
+
_MAX_RESPONSE = 64 * 1024 * 1024
|
|
36
|
+
_MAX_SOURCE = 16 * 1024 * 1024
|
|
37
|
+
_MAX_CAPABILITIES = 1024 * 1024
|
|
38
|
+
_MAX_JSON_RESPONSE_VALUES = 1_000_000
|
|
39
|
+
_MAX_SAFE_INTEGER = 9_007_199_254_740_991
|
|
40
|
+
_RESOURCE_FILES = {
|
|
41
|
+
"tablesPackage": "tables.package.json",
|
|
42
|
+
"actionsPackage": "actions.package.json",
|
|
43
|
+
"bindingPackage": "binding.package.json",
|
|
44
|
+
"stableBindingPackage": "stable-binding.package.json",
|
|
45
|
+
}
|
|
46
|
+
_CORE_RESOURCE_FILES = {
|
|
47
|
+
"coreSchema": ("core.schema.json", "spec/core/schema.json"),
|
|
48
|
+
"referenceSchema": ("core.reference-schema.json", "spec/core/semantic-type-reference.schema.json"),
|
|
49
|
+
"packageSchema": ("core.package-schema.json", "spec/core/extension-package.schema.json"),
|
|
50
|
+
"constraints": ("core.constraints.json", "spec/metamodel/generated/core-constraints.json"),
|
|
51
|
+
"domainPolicy": ("core.domain-policy.json", "spec/metamodel/generated/domain-policy.json"),
|
|
52
|
+
}
|
|
53
|
+
_RESOURCE_NAMES = set(_RESOURCE_FILES.values()) | {"saga.package.json"} | {
|
|
54
|
+
filename for filename, _ in _CORE_RESOURCE_FILES.values()
|
|
55
|
+
}
|
|
56
|
+
_CLI_OPERATIONS = {
|
|
57
|
+
"generate": "generate",
|
|
58
|
+
"pack_tools": "pack-tools",
|
|
59
|
+
"deployment": "deployment",
|
|
60
|
+
"verify_deployment": "verify-deployment",
|
|
61
|
+
"migration": "migration",
|
|
62
|
+
"verify_migration": "verify-migration",
|
|
63
|
+
"discover_changes": "discover-changes",
|
|
64
|
+
"row_scope": "row-scope",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@contextmanager
|
|
69
|
+
def _cli_binary() -> Iterator[Path]:
|
|
70
|
+
"""Resolve only the binary shipped inside the pinned umf-core wheel."""
|
|
71
|
+
if os.name != "posix":
|
|
72
|
+
raise GenerationError("ENGINE_UNAVAILABLE", "The isolated native codegen CLI is unavailable")
|
|
73
|
+
try:
|
|
74
|
+
item = resources.files("umf").joinpath("_bin", "umf")
|
|
75
|
+
with resources.as_file(item) as resolved:
|
|
76
|
+
path = Path(resolved)
|
|
77
|
+
if not path.is_file() or not os.access(path, os.X_OK):
|
|
78
|
+
raise GenerationError("ENGINE_UNAVAILABLE", "Pinned native codegen CLI is missing")
|
|
79
|
+
yield path
|
|
80
|
+
except GenerationError:
|
|
81
|
+
raise
|
|
82
|
+
except Exception as error:
|
|
83
|
+
raise GenerationError("ENGINE_UNAVAILABLE", "Pinned native codegen CLI is unavailable") from error
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _verify_native() -> tuple[Any, type[ValueError]]:
|
|
87
|
+
try:
|
|
88
|
+
installed = version("umf-core")
|
|
89
|
+
if installed != UMF_CORE_VERSION:
|
|
90
|
+
raise ImportError(f"umf-core {UMF_CORE_VERSION} is required")
|
|
91
|
+
from umf._rust import _rust_core
|
|
92
|
+
from umf.rust_codegen import UmfCoreError, encode_codegen_cli_request_json
|
|
93
|
+
|
|
94
|
+
if not callable(getattr(_rust_core, "execute_codegen_wire", None)):
|
|
95
|
+
raise ImportError("Native codegen wire API is missing") # noqa: TRY004 -- absent installed capability is an import/availability failure, not caller type error
|
|
96
|
+
if _rust_core.engine_version() != UMF_CORE_ENGINE_VERSION:
|
|
97
|
+
raise ImportError("Native core ABI identity differs")
|
|
98
|
+
if _rust_core.codegen_protocol_version() != CODEGEN_PROTOCOL_VERSION:
|
|
99
|
+
raise ImportError("Native codegen protocol identity differs")
|
|
100
|
+
if not callable(encode_codegen_cli_request_json):
|
|
101
|
+
raise ImportError("Native codegen CLI request helper is missing") # noqa: TRY004 -- absent installed capability is an import/availability failure, not caller type error
|
|
102
|
+
return encode_codegen_cli_request_json, UmfCoreError
|
|
103
|
+
except (ImportError, PackageNotFoundError, AttributeError, ValueError, RuntimeError) as error:
|
|
104
|
+
raise GenerationError(
|
|
105
|
+
"ENGINE_UNAVAILABLE", f"Pinned umf-core {UMF_CORE_VERSION} native codegen engine is required"
|
|
106
|
+
) from error
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _load_resource_manifest() -> dict[str, Any]:
|
|
110
|
+
root = resources.files("umf_codegen").joinpath("_resources")
|
|
111
|
+
try:
|
|
112
|
+
raw = root.joinpath("manifest.json").read_bytes()
|
|
113
|
+
if hashlib.sha256(raw).hexdigest() != RESOURCE_MANIFEST_SHA256:
|
|
114
|
+
raise ValueError("Codegen resource manifest pin differs")
|
|
115
|
+
manifest = json.loads(raw)
|
|
116
|
+
if (type(manifest) is not dict or manifest.get("profile") != "umf.codegen-resources/1"
|
|
117
|
+
or set(manifest) != {"profile", "files"} or type(manifest.get("files")) is not dict
|
|
118
|
+
or set(manifest["files"]) != _RESOURCE_NAMES):
|
|
119
|
+
raise ValueError("Codegen resource manifest differs")
|
|
120
|
+
packaged_names = {path.name for path in root.iterdir() if path.name != "manifest.json"}
|
|
121
|
+
if packaged_names != _RESOURCE_NAMES:
|
|
122
|
+
raise ValueError("Codegen resource inventory differs")
|
|
123
|
+
for name, digest in manifest["files"].items():
|
|
124
|
+
if type(digest) is not str or hashlib.sha256(root.joinpath(name).read_bytes()).hexdigest() != digest:
|
|
125
|
+
raise ValueError("Codegen resource differs")
|
|
126
|
+
|
|
127
|
+
core_root = resources.files("umf")
|
|
128
|
+
for filename, core_path in _CORE_RESOURCE_FILES.values():
|
|
129
|
+
if root.joinpath(filename).read_bytes() != core_root.joinpath(*core_path.split("/")).read_bytes():
|
|
130
|
+
raise ValueError("Native UMF core resource cohort differs")
|
|
131
|
+
return manifest
|
|
132
|
+
except Exception as error:
|
|
133
|
+
raise GenerationError("ENGINE_UNAVAILABLE", "Pinned native codegen resources are unavailable") from error
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _terminate(process: subprocess.Popen[bytes]) -> None:
|
|
137
|
+
if process.poll() is None:
|
|
138
|
+
try:
|
|
139
|
+
process.kill()
|
|
140
|
+
except OSError:
|
|
141
|
+
pass
|
|
142
|
+
try:
|
|
143
|
+
process.wait()
|
|
144
|
+
except OSError:
|
|
145
|
+
pass
|
|
146
|
+
for pipe in (process.stdin, process.stdout):
|
|
147
|
+
if pipe is not None:
|
|
148
|
+
try:
|
|
149
|
+
pipe.close()
|
|
150
|
+
except OSError:
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _run_cli(binary: Path, arguments: list[str], payload: bytes, deadline: float,
|
|
155
|
+
output_limit: int) -> tuple[int, bytes]:
|
|
156
|
+
"""Run one CLI call with bounded pipes and a shared monotonic deadline."""
|
|
157
|
+
if time.monotonic() >= deadline:
|
|
158
|
+
raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
|
|
159
|
+
process: subprocess.Popen[bytes] | None = None
|
|
160
|
+
selector: selectors.BaseSelector | None = None
|
|
161
|
+
output = bytearray()
|
|
162
|
+
try:
|
|
163
|
+
try:
|
|
164
|
+
process = subprocess.Popen(
|
|
165
|
+
[os.fspath(binary), *arguments],
|
|
166
|
+
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
|
|
167
|
+
close_fds=True, cwd=os.fspath(binary.parent),
|
|
168
|
+
)
|
|
169
|
+
except OSError as error:
|
|
170
|
+
raise GenerationError("ENGINE_UNAVAILABLE", "Pinned native codegen CLI could not start") from error
|
|
171
|
+
if process.stdin is None or process.stdout is None:
|
|
172
|
+
raise OSError("Native codegen CLI pipes could not be created")
|
|
173
|
+
if time.monotonic() >= deadline:
|
|
174
|
+
raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
|
|
175
|
+
|
|
176
|
+
selector = selectors.DefaultSelector()
|
|
177
|
+
input_fd = process.stdin.fileno()
|
|
178
|
+
output_fd = process.stdout.fileno()
|
|
179
|
+
os.set_blocking(input_fd, False)
|
|
180
|
+
os.set_blocking(output_fd, False)
|
|
181
|
+
selector.register(output_fd, selectors.EVENT_READ, "stdout")
|
|
182
|
+
offset = 0
|
|
183
|
+
if payload:
|
|
184
|
+
selector.register(input_fd, selectors.EVENT_WRITE, "stdin")
|
|
185
|
+
else:
|
|
186
|
+
process.stdin.close()
|
|
187
|
+
|
|
188
|
+
while selector.get_map():
|
|
189
|
+
remaining = deadline - time.monotonic()
|
|
190
|
+
if remaining <= 0:
|
|
191
|
+
raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
|
|
192
|
+
events = selector.select(remaining)
|
|
193
|
+
if not events:
|
|
194
|
+
raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
|
|
195
|
+
for key, _ in events:
|
|
196
|
+
if key.data == "stdin":
|
|
197
|
+
try:
|
|
198
|
+
offset += os.write(input_fd, payload[offset:offset + 65536])
|
|
199
|
+
except BlockingIOError:
|
|
200
|
+
continue
|
|
201
|
+
except BrokenPipeError:
|
|
202
|
+
offset = len(payload)
|
|
203
|
+
if offset >= len(payload):
|
|
204
|
+
selector.unregister(input_fd)
|
|
205
|
+
process.stdin.close()
|
|
206
|
+
else:
|
|
207
|
+
remaining_capacity = output_limit + 1 - len(output)
|
|
208
|
+
chunk = os.read(output_fd, min(65536, remaining_capacity))
|
|
209
|
+
if not chunk:
|
|
210
|
+
selector.unregister(output_fd)
|
|
211
|
+
process.stdout.close()
|
|
212
|
+
continue
|
|
213
|
+
output.extend(chunk)
|
|
214
|
+
if len(output) > output_limit:
|
|
215
|
+
raise GenerationError("GENERATION_CAPACITY", "Native codegen CLI exceeded its output limit")
|
|
216
|
+
|
|
217
|
+
remaining = deadline - time.monotonic()
|
|
218
|
+
if remaining <= 0:
|
|
219
|
+
raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
|
|
220
|
+
try:
|
|
221
|
+
returncode = process.wait(timeout=remaining)
|
|
222
|
+
except subprocess.TimeoutExpired as error:
|
|
223
|
+
raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired") from error
|
|
224
|
+
if time.monotonic() >= deadline:
|
|
225
|
+
raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
|
|
226
|
+
return returncode, bytes(output)
|
|
227
|
+
except GenerationError:
|
|
228
|
+
raise
|
|
229
|
+
except (OSError, ValueError, subprocess.SubprocessError) as error:
|
|
230
|
+
raise GenerationError("GENERATION_FAILED", "Native codegen CLI failed") from error
|
|
231
|
+
finally:
|
|
232
|
+
if selector is not None:
|
|
233
|
+
selector.close()
|
|
234
|
+
if process is not None:
|
|
235
|
+
_terminate(process)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _check_cli_capabilities(raw: bytes, manifest: dict[str, Any]) -> None:
|
|
239
|
+
try:
|
|
240
|
+
capabilities = json.loads(raw)
|
|
241
|
+
except (UnicodeDecodeError, ValueError) as error:
|
|
242
|
+
raise GenerationError("ENGINE_UNAVAILABLE", "Native codegen CLI capabilities are invalid") from error
|
|
243
|
+
expected_keys = {
|
|
244
|
+
"protocol", "version", "profile", "resourceManifest", "resourceManifestSha256",
|
|
245
|
+
"inputFormat", "outputWireProtocol",
|
|
246
|
+
}
|
|
247
|
+
if (type(capabilities) is not dict
|
|
248
|
+
or set(capabilities) != expected_keys
|
|
249
|
+
or capabilities.get("protocol") != CODEGEN_CLI_PROTOCOL_VERSION
|
|
250
|
+
or type(capabilities.get("version")) is not str
|
|
251
|
+
or not capabilities["version"]
|
|
252
|
+
or capabilities.get("profile") != "umf.codegen-resources/1"
|
|
253
|
+
or capabilities.get("resourceManifest") != manifest
|
|
254
|
+
or capabilities.get("resourceManifestSha256") != RESOURCE_MANIFEST_SHA256
|
|
255
|
+
or capabilities.get("inputFormat") != "umf.codegen-json-core-admission/1"
|
|
256
|
+
or capabilities.get("outputWireProtocol") != "umf.core-wire/1"):
|
|
257
|
+
raise GenerationError("ENGINE_UNAVAILABLE", "Native codegen CLI protocol or resources differ")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _decode_cli_error(raw: bytes) -> dict[str, Any]:
|
|
261
|
+
try:
|
|
262
|
+
envelope = json.loads(raw)
|
|
263
|
+
except (UnicodeDecodeError, ValueError) as error:
|
|
264
|
+
raise GenerationError("GENERATION_FAILED", "Native codegen CLI returned an invalid error") from error
|
|
265
|
+
if type(envelope) is not dict or set(envelope) != {"ok", "error"} or envelope.get("ok") is not False:
|
|
266
|
+
raise GenerationError("GENERATION_FAILED", "Native codegen CLI returned an invalid error")
|
|
267
|
+
error = envelope.get("error")
|
|
268
|
+
if (type(error) is not dict or not {"code", "message"} <= set(error)
|
|
269
|
+
or set(error) - {"code", "message", "path"}
|
|
270
|
+
or type(error.get("code")) is not str or not error["code"]
|
|
271
|
+
or type(error.get("message")) is not str
|
|
272
|
+
or ("path" in error and type(error["path"]) is not str)):
|
|
273
|
+
raise GenerationError("GENERATION_FAILED", "Native codegen CLI returned an invalid error")
|
|
274
|
+
return error
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _decode_cli_json_response(raw: bytes, deadline: float) -> dict[str, Any]:
|
|
278
|
+
"""Decode bounded CLI JSON while retaining ECMAScript numeric tokens.
|
|
279
|
+
|
|
280
|
+
The core-wire decoder intentionally materializes every binary64 value as a
|
|
281
|
+
Python float. Codegen's historical JSON result contract materializes
|
|
282
|
+
ECMAScript integer tokens as Python ints, so this CLI-only path consumes
|
|
283
|
+
the CLI's already-supported compact JSON output. The strict parser rejects
|
|
284
|
+
response shapes the core serializer cannot produce.
|
|
285
|
+
"""
|
|
286
|
+
if len(raw) > _MAX_RESPONSE:
|
|
287
|
+
raise GenerationError("GENERATION_CAPACITY", "Native codegen CLI exceeded its output limit")
|
|
288
|
+
|
|
289
|
+
def reject_constant(value: str) -> Any:
|
|
290
|
+
raise ValueError(f"non-finite JSON constant: {value}")
|
|
291
|
+
|
|
292
|
+
def parse_integer(token: str) -> int:
|
|
293
|
+
value = int(token)
|
|
294
|
+
if abs(value) > _MAX_SAFE_INTEGER or (value == 0 and token.startswith("-")):
|
|
295
|
+
raise ValueError("integer outside the core interoperable numeric profile")
|
|
296
|
+
return value
|
|
297
|
+
|
|
298
|
+
def parse_float(token: str) -> float:
|
|
299
|
+
value = float(token)
|
|
300
|
+
if (not math.isfinite(value)
|
|
301
|
+
or (value == 0.0 and math.copysign(1.0, value) < 0.0)
|
|
302
|
+
or (value.is_integer() and abs(value) > _MAX_SAFE_INTEGER)):
|
|
303
|
+
raise ValueError("number outside the core interoperable numeric profile")
|
|
304
|
+
return value
|
|
305
|
+
|
|
306
|
+
def object_pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
307
|
+
value: dict[str, Any] = {}
|
|
308
|
+
for key, item in pairs:
|
|
309
|
+
if key in value:
|
|
310
|
+
raise ValueError("duplicate JSON object key")
|
|
311
|
+
value[key] = item
|
|
312
|
+
return value
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
text = raw.decode("utf-8")
|
|
316
|
+
envelope = json.loads(
|
|
317
|
+
text,
|
|
318
|
+
parse_constant=reject_constant,
|
|
319
|
+
parse_int=parse_integer,
|
|
320
|
+
parse_float=parse_float,
|
|
321
|
+
object_pairs_hook=object_pairs,
|
|
322
|
+
)
|
|
323
|
+
except (UnicodeDecodeError, ValueError, RecursionError) as error:
|
|
324
|
+
raise GenerationError("GENERATION_FAILED", "Native codegen CLI returned an invalid response") from error
|
|
325
|
+
if time.monotonic() >= deadline:
|
|
326
|
+
raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
|
|
327
|
+
|
|
328
|
+
remaining = _MAX_JSON_RESPONSE_VALUES
|
|
329
|
+
|
|
330
|
+
def check_tree(value: Any, depth: int = 0) -> None:
|
|
331
|
+
nonlocal remaining
|
|
332
|
+
remaining -= 1
|
|
333
|
+
if remaining < 0 or depth > 256:
|
|
334
|
+
raise ValueError("Native codegen CLI JSON response exceeds structural limits")
|
|
335
|
+
if time.monotonic() >= deadline:
|
|
336
|
+
raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
|
|
337
|
+
if value is None or type(value) in (bool, int, float):
|
|
338
|
+
return
|
|
339
|
+
if type(value) is str:
|
|
340
|
+
for offset in range(0, len(value), 4096):
|
|
341
|
+
if time.monotonic() >= deadline:
|
|
342
|
+
raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
|
|
343
|
+
value[offset:offset + 4096].encode("utf-8", "surrogatepass")
|
|
344
|
+
return
|
|
345
|
+
if type(value) is list:
|
|
346
|
+
for item in value:
|
|
347
|
+
check_tree(item, depth + 1)
|
|
348
|
+
return
|
|
349
|
+
if type(value) is dict:
|
|
350
|
+
for key, item in value.items():
|
|
351
|
+
check_tree(key, depth + 1)
|
|
352
|
+
check_tree(item, depth + 1)
|
|
353
|
+
return
|
|
354
|
+
raise ValueError("Native codegen CLI JSON response contains an unsupported value")
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
check_tree(envelope)
|
|
358
|
+
except GenerationError:
|
|
359
|
+
raise
|
|
360
|
+
except ValueError as error:
|
|
361
|
+
raise GenerationError("GENERATION_FAILED", "Native codegen CLI returned an invalid response") from error
|
|
362
|
+
return envelope
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def execute(request: dict[str, object], timeout_seconds: float, *, operation: str = "generate") -> dict[str, object]:
|
|
366
|
+
if type(timeout_seconds) not in (int, float) or not 0 < timeout_seconds <= 60:
|
|
367
|
+
raise ValueError("timeout_seconds must be greater than zero and at most 60")
|
|
368
|
+
started = time.monotonic()
|
|
369
|
+
deadline = started + timeout_seconds
|
|
370
|
+
if operation not in _CLI_OPERATIONS:
|
|
371
|
+
raise ValueError("Unknown native operation")
|
|
372
|
+
source = request.get("text")
|
|
373
|
+
if operation in ("generate", "deployment"):
|
|
374
|
+
if type(source) is not str:
|
|
375
|
+
raise ValueError("source text must be at most 16 MiB of UTF-8")
|
|
376
|
+
source_bytes = 0
|
|
377
|
+
for offset in range(0, len(source), 64 * 1024):
|
|
378
|
+
if time.monotonic() >= deadline:
|
|
379
|
+
raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
|
|
380
|
+
source_bytes += len(source[offset:offset + 64 * 1024].encode("utf-8"))
|
|
381
|
+
if source_bytes > _MAX_SOURCE:
|
|
382
|
+
raise ValueError("source text must be at most 16 MiB of UTF-8")
|
|
383
|
+
|
|
384
|
+
encode_request, core_error = _verify_native()
|
|
385
|
+
try:
|
|
386
|
+
encoded = encode_request(request, deadline=deadline, max_bytes=_MAX_REQUEST)
|
|
387
|
+
except core_error as error:
|
|
388
|
+
if getattr(error, "code", None) == "DEADLINE":
|
|
389
|
+
raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired") from error
|
|
390
|
+
raise
|
|
391
|
+
request_bytes = encoded.encode("ascii")
|
|
392
|
+
if len(request_bytes) > _MAX_REQUEST:
|
|
393
|
+
raise ValueError("native request must be at most 32 MiB of JSON")
|
|
394
|
+
if time.monotonic() >= deadline:
|
|
395
|
+
raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
|
|
396
|
+
|
|
397
|
+
manifest = _load_resource_manifest()
|
|
398
|
+
if time.monotonic() >= deadline:
|
|
399
|
+
raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
|
|
400
|
+
with _cli_binary() as binary:
|
|
401
|
+
capabilities_status, capabilities_raw = _run_cli(
|
|
402
|
+
binary, ["codegen", "--capabilities"], b"", deadline, _MAX_CAPABILITIES
|
|
403
|
+
)
|
|
404
|
+
if capabilities_status == 73:
|
|
405
|
+
raise GenerationError("GENERATION_CAPACITY", "Native codegen allocator limit exceeded")
|
|
406
|
+
if capabilities_status != 0:
|
|
407
|
+
raise GenerationError("ENGINE_UNAVAILABLE", "Native codegen CLI capability check failed")
|
|
408
|
+
_check_cli_capabilities(capabilities_raw, manifest)
|
|
409
|
+
|
|
410
|
+
cli_operation = _CLI_OPERATIONS[operation]
|
|
411
|
+
status, response = _run_cli(
|
|
412
|
+
binary,
|
|
413
|
+
["codegen", cli_operation, "--request", "-", "--input-format", "json",
|
|
414
|
+
"--output-format", "json"],
|
|
415
|
+
request_bytes, deadline, _MAX_RESPONSE,
|
|
416
|
+
)
|
|
417
|
+
if status == 73:
|
|
418
|
+
raise GenerationError("GENERATION_CAPACITY", "Native codegen allocator limit exceeded")
|
|
419
|
+
if status in (1, 2):
|
|
420
|
+
error = _decode_cli_error(response)
|
|
421
|
+
if error["code"] == "DEADLINE":
|
|
422
|
+
raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
|
|
423
|
+
raise GenerationError(error["code"], error["message"], path=error.get("path"))
|
|
424
|
+
if status != 0:
|
|
425
|
+
raise GenerationError("GENERATION_FAILED", "Native codegen CLI exited unexpectedly")
|
|
426
|
+
envelope = _decode_cli_json_response(response, deadline)
|
|
427
|
+
if type(envelope) is not dict or type(envelope.get("ok")) is not bool:
|
|
428
|
+
raise GenerationError("GENERATION_FAILED", "Native codegen CLI returned an invalid response")
|
|
429
|
+
if envelope["ok"] is not True:
|
|
430
|
+
failure = envelope.get("error")
|
|
431
|
+
if (set(envelope) != {"ok", "error"} or type(failure) is not dict
|
|
432
|
+
or not {"code", "message"} <= set(failure)
|
|
433
|
+
or set(failure) - {"code", "message", "path"}
|
|
434
|
+
or type(failure.get("code")) is not str or not failure["code"]
|
|
435
|
+
or type(failure.get("message")) is not str
|
|
436
|
+
or ("path" in failure and type(failure["path"]) is not str)):
|
|
437
|
+
raise GenerationError("GENERATION_FAILED", "Native codegen CLI returned an invalid response")
|
|
438
|
+
if failure["code"] == "DEADLINE":
|
|
439
|
+
raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
|
|
440
|
+
raise GenerationError(failure["code"], failure["message"], path=failure.get("path"))
|
|
441
|
+
if set(envelope) != {"ok", "result"} or type(envelope.get("result")) is not dict:
|
|
442
|
+
raise GenerationError("GENERATION_FAILED", "Native codegen CLI returned an invalid response")
|
|
443
|
+
if time.monotonic() >= deadline:
|
|
444
|
+
raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
|
|
445
|
+
return envelope["result"]
|
umf_codegen/_release.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
2
|
+
# Generated by the R4 resource freeze.
|
|
3
|
+
RESOURCE_MANIFEST_SHA256 = "5ebd51f10967ba1a92ed82784cef017ece06633ab158155c26247732f641837e"
|
|
4
|
+
UMF_CODEGEN_VERSION = "0.10.8"
|
|
5
|
+
UMF_CORE_VERSION = "0.10.8"
|
|
6
|
+
UMF_CORE_ENGINE_VERSION = "umf-core-r2"
|
|
7
|
+
CODEGEN_PROTOCOL_VERSION = "umf-codegen-r3-seven-ops-nine-resources/1"
|
|
8
|
+
CODEGEN_CLI_PROTOCOL_VERSION = "umf.codegen-cli/1"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"id":"umf.actions","version":"0.1.0","coreVersion":"0.10.0","description":"Declarative action metadata over UMF core 0.10.0; executor qualification remains separate.","schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"urn:umf:actions:0.1.0","type":"object","properties":{"actions":{"type":"array","items":{"$ref":"#/$defs/action"}}},"required":["actions"],"$defs":{"ref":{"type":"object","properties":{"module":{"type":"string","minLength":1},"element":{"type":"string","minLength":1}},"required":["module","element"]},"key":{"type":"object","properties":{"module":{"type":"string","minLength":1},"element":{"type":"string","minLength":1},"key":{"type":"string","minLength":1}},"required":["module","element","key"]},"relationship":{"type":"object","properties":{"module":{"type":"string","minLength":1},"relationship":{"type":"string","minLength":1}},"required":["module","relationship"]},"profile":{"type":"object","properties":{"id":{"type":"string","minLength":1},"version":{"type":"string","minLength":1}},"required":["id","version"]},"parameter":{"oneOf":[{"type":"object","properties":{"kind":{"const":"value"},"id":{"type":"string","minLength":1},"field":{"$ref":"#/$defs/ref"},"required":{"type":"boolean"}},"required":["kind","id","field","required"]},{"type":"object","properties":{"kind":{"const":"entity"},"id":{"type":"string","minLength":1},"target":{"$ref":"#/$defs/key"},"required":{"const":true}},"required":["kind","id","target","required"]}]},"reference":{"oneOf":[{"type":"object","properties":{"parameter":{"type":"string","minLength":1}},"required":["parameter"],"not":{"anyOf":[{"required":["output"],"properties":{"output":{}}},{"required":["record"],"properties":{"record":{}}},{"required":["relationship"],"properties":{"relationship":{}}}]}},{"type":"object","properties":{"output":{"type":"string","minLength":1}},"required":["output"],"not":{"anyOf":[{"required":["parameter"],"properties":{"parameter":{}}},{"required":["record"],"properties":{"record":{}}},{"required":["relationship"],"properties":{"relationship":{}}}]}},{"type":"object","properties":{"record":{"$ref":"#/$defs/ref"}},"required":["record"],"not":{"anyOf":[{"required":["parameter"],"properties":{"parameter":{}}},{"required":["output"],"properties":{"output":{}}},{"required":["relationship"],"properties":{"relationship":{}}}]}},{"type":"object","properties":{"relationship":{"$ref":"#/$defs/relationship"}},"required":["relationship"],"not":{"anyOf":[{"required":["parameter"],"properties":{"parameter":{}}},{"required":["output"],"properties":{"output":{}}},{"required":["record"],"properties":{"record":{}}}]}}]},"rule":{"type":"object","properties":{"language":{"type":"string","minLength":1},"version":{"type":"string","minLength":1},"expression":{"type":"string","minLength":1,"maxLength":65536},"references":{"type":"array","items":{"$ref":"#/$defs/reference"},"maxItems":128,"minItems":0}},"required":["language","version","expression","references"]},"condition":{"type":"object","properties":{"id":{"type":"string","minLength":1},"rule":{"$ref":"#/$defs/rule"},"failure":{"type":"object","properties":{"code":{"type":"string","minLength":1},"message":{"type":"string","minLength":1}},"required":["code","message"]}},"required":["id","rule","failure"]},"frame":{"type":"object","properties":{"id":{"type":"string","minLength":1},"record":{"$ref":"#/$defs/ref"},"selector":{"$ref":"#/$defs/rule"},"maxEntities":{"type":"integer","minimum":1,"maximum":256},"fields":{"type":"array","items":{"$ref":"#/$defs/ref"},"maxItems":256,"minItems":0},"relationships":{"type":"array","items":{"$ref":"#/$defs/relationship"},"maxItems":128,"minItems":0},"create":{"type":"boolean"},"delete":{"type":"boolean"}},"required":["id","record","selector","maxEntities","fields","relationships","create","delete"]},"entity":{"oneOf":[{"type":"object","properties":{"parameter":{"type":"string","minLength":1}},"required":["parameter"],"not":{"required":["created"],"properties":{"created":{}}}},{"type":"object","properties":{"created":{"type":"string","minLength":1}},"required":["created"],"not":{"required":["parameter"],"properties":{"parameter":{}}}}]},"literal":{"oneOf":[{"type":"null"},{"type":"object","required":["boolean"],"properties":{"boolean":{"type":"boolean"}},"additionalProperties":false},{"type":"object","required":["integerToken"],"properties":{"integerToken":{"type":"string","pattern":"^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$(?![\\s\\S])"}},"additionalProperties":false},{"type":"object","required":["decimalToken"],"properties":{"decimalToken":{"type":"string","pattern":"^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$(?![\\s\\S])"}},"additionalProperties":false},{"type":"object","required":["string"],"properties":{"string":{"type":"string"}},"additionalProperties":false},{"type":"object","required":["binaryHex"],"properties":{"binaryHex":{"type":"string","pattern":"^([0-9a-fA-F]{2})*$(?![\\s\\S])"}},"additionalProperties":false},{"type":"object","required":["floatToken"],"properties":{"floatToken":{"type":"string","pattern":"^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$(?![\\s\\S])"}},"additionalProperties":false},{"type":"object","required":["date"],"properties":{"date":{"type":"string"}},"additionalProperties":false},{"type":"object","required":["time"],"properties":{"time":{"type":"string"}},"additionalProperties":false},{"type":"object","required":["timestamp"],"properties":{"timestamp":{"type":"string"}},"additionalProperties":false},{"type":"object","required":["array"],"properties":{"array":{"type":"array","items":{"$ref":"#/$defs/literal"}}},"additionalProperties":false},{"type":"object","required":["map"],"properties":{"map":{"type":"object","additionalProperties":{"$ref":"#/$defs/literal"}}},"additionalProperties":false}]},"value":{"oneOf":[{"type":"object","properties":{"parameter":{"type":"string","minLength":1}},"required":["parameter"],"not":{"required":["literal"],"properties":{"literal":{}}}},{"type":"object","properties":{"literal":{"$ref":"#/$defs/literal"}},"required":["literal"],"not":{"required":["parameter"],"properties":{"parameter":{}}}}]},"assignment":{"type":"object","properties":{"field":{"$ref":"#/$defs/ref"},"value":{"$ref":"#/$defs/value"}},"required":["field","value"]},"effect":{"oneOf":[{"type":"object","properties":{"id":{"type":"string","minLength":1},"kind":{"const":"create"},"record":{"$ref":"#/$defs/ref"},"key":{"type":"string","minLength":1},"values":{"type":"array","items":{"$ref":"#/$defs/assignment"},"maxItems":256,"minItems":0}},"required":["id","kind","record","key","values"]},{"type":"object","properties":{"id":{"type":"string","minLength":1},"kind":{"const":"set"},"entity":{"$ref":"#/$defs/entity"},"values":{"type":"array","items":{"$ref":"#/$defs/assignment"},"maxItems":256,"minItems":1}},"required":["id","kind","entity","values"]},{"type":"object","properties":{"id":{"type":"string","minLength":1},"kind":{"const":"delete"},"entity":{"$ref":"#/$defs/entity"}},"required":["id","kind","entity"]},{"type":"object","properties":{"id":{"type":"string","minLength":1},"kind":{"const":"link"},"relationship":{"$ref":"#/$defs/relationship"},"source":{"$ref":"#/$defs/entity"},"target":{"$ref":"#/$defs/entity"},"association":{"$ref":"#/$defs/entity"}},"required":["id","kind","relationship","source","target"]},{"type":"object","properties":{"id":{"type":"string","minLength":1},"kind":{"const":"unlink"},"relationship":{"$ref":"#/$defs/relationship"},"source":{"$ref":"#/$defs/entity"},"target":{"$ref":"#/$defs/entity"},"association":{"$ref":"#/$defs/entity"}},"required":["id","kind","relationship","source","target"]},{"type":"object","properties":{"id":{"type":"string","minLength":1},"kind":{"type":"string","minLength":1,"not":{"enum":["create","set","delete","link","unlink"]}}},"required":["id","kind"]}]},"binding":{"oneOf":[{"type":"object","properties":{"kind":{"const":"recipe"},"profile":{"type":"object","properties":{"id":{"const":"graph-write"},"version":{"enum":["1","2"]}},"required":["id","version"]},"effects":{"type":"array","items":{"$ref":"#/$defs/effect"},"maxItems":256,"minItems":1}},"required":["kind","profile","effects"]},{"type":"object","properties":{"kind":{"const":"handler"},"profile":{"$ref":"#/$defs/profile"},"handler":{"$ref":"#/$defs/profile"}},"required":["kind","profile","handler"]}]},"authorization":{"oneOf":[{"type":"object","properties":{"kind":{"const":"roles"},"profile":{"$ref":"#/$defs/profile"},"roles":{"type":"array","items":{"type":"string","minLength":1},"maxItems":64,"minItems":1,"uniqueItems":true}},"required":["kind","profile","roles"]},{"type":"object","properties":{"kind":{"const":"policy"},"profile":{"$ref":"#/$defs/profile"},"resources":{"type":"array","items":{"type":"string","minLength":1},"maxItems":128,"minItems":0,"uniqueItems":true}},"required":["kind","profile","resources"]}]},"attribution":{"oneOf":[{"type":"object","properties":{"subject":{"const":"person-required"}},"required":["subject"],"not":{"required":["profile"],"properties":{"profile":{}}}},{"type":"object","properties":{"subject":{"const":"actor-profile"},"profile":{"$ref":"#/$defs/profile"}},"required":["subject","profile"]}]},"action":{"type":"object","properties":{"id":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"description":{"type":"string","minLength":1},"parameters":{"type":"array","items":{"$ref":"#/$defs/parameter"},"maxItems":128,"minItems":0},"outputs":{"type":"array","items":{"$ref":"#/$defs/parameter"},"maxItems":128,"minItems":0},"preconditions":{"type":"array","items":{"$ref":"#/$defs/condition"},"maxItems":128,"minItems":0},"postconditions":{"type":"array","items":{"$ref":"#/$defs/condition"},"maxItems":128,"minItems":0},"failures":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string","minLength":1},"message":{"type":"string","minLength":1},"retryable":{"type":"boolean"}},"required":["code","message","retryable"]},"maxItems":128,"minItems":0},"reads":{"type":"array","items":{"$ref":"#/$defs/frame"},"maxItems":128,"minItems":0},"writes":{"type":"array","items":{"$ref":"#/$defs/frame"},"maxItems":128,"minItems":0},"binding":{"$ref":"#/$defs/binding"},"authorization":{"$ref":"#/$defs/authorization"},"attribution":{"$ref":"#/$defs/attribution"},"atomicity":{"const":"single-store"},"idempotency":{"const":"caller-key-optional"},"result":{"type":"object","properties":{"identities":{"const":"created-and-changed"},"version":{"const":"store-opaque"},"receipt":{"const":"read-at-least"}},"required":["identities","version","receipt"]},"dddOperation":{"type":"object","properties":{"module":{"type":"string","minLength":1},"element":{"type":"string","minLength":1},"operation":{"type":"string","minLength":1}},"required":["module","element","operation"]}},"required":["id","name","description","parameters","outputs","preconditions","postconditions","failures","reads","writes","binding","authorization","attribution","atomicity","idempotency","result"]}}},"semantics":"CONTRACT-900. Metadata validation and preservation only; registration never authorizes or invokes execution.","scopes":["module"],"capabilities":{"validation":"semantic","directions":[],"evidence":["tests/actions/structure.test.ts","tests/actions/api.test.ts","tests/actions/contracts.test.ts","tests/actions/assessment.test.ts","tests/actions/preservation.test.ts","tests/actions/reports.test.ts","tests/actions/corpus.test.ts","fixtures/actions/browser.json"]}}
|