umf-core 0.10.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.
umf/__init__.py ADDED
@@ -0,0 +1,54 @@
1
+ # SPDX-License-Identifier: MIT OR Apache-2.0
2
+ """Official Python UMF support; consumer extensions remain consumer-owned."""
3
+
4
+ from .models import (
5
+ CoreItemTypeReference,
6
+ CoreSemanticTypeReference,
7
+ Document,
8
+ Element,
9
+ Module,
10
+ Reference,
11
+ Vocabulary,
12
+ )
13
+ from .registry import Diagnostic, Extension, Registry, Validation
14
+ from .schemas import (
15
+ core_schema,
16
+ current_core_version,
17
+ dataset_source_schema,
18
+ domain_pack_schema,
19
+ schema_validator,
20
+ semantic_type_reference_schema,
21
+ )
22
+ from .serialization import (
23
+ read_document,
24
+ read_json_value,
25
+ write_document,
26
+ write_json_value,
27
+ )
28
+ from .validation import validate_document
29
+
30
+ __version__ = "0.10.0"
31
+ __all__ = [
32
+ "CoreItemTypeReference",
33
+ "CoreSemanticTypeReference",
34
+ "Diagnostic",
35
+ "Document",
36
+ "Element",
37
+ "Extension",
38
+ "Module",
39
+ "Reference",
40
+ "Registry",
41
+ "Validation",
42
+ "Vocabulary",
43
+ "core_schema",
44
+ "current_core_version",
45
+ "dataset_source_schema",
46
+ "domain_pack_schema",
47
+ "read_document",
48
+ "read_json_value",
49
+ "schema_validator",
50
+ "semantic_type_reference_schema",
51
+ "validate_document",
52
+ "write_document",
53
+ "write_json_value",
54
+ ]
@@ -0,0 +1,467 @@
1
+ # SPDX-License-Identifier: MIT OR Apache-2.0
2
+ """Interpreter for the inert, model-generated UMF constraint program.
3
+
4
+ The kernel contains generic data operations only. Core decisions, diagnostic
5
+ texts and order are supplied by ``spec/metamodel/generated/core-constraints.json``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import math
12
+ from collections.abc import Mapping
13
+ from dataclasses import dataclass
14
+ from typing import Any
15
+
16
+ from .registry import Diagnostic, Registry
17
+
18
+
19
+ class _Missing:
20
+ pass
21
+
22
+
23
+ MISSING = _Missing()
24
+
25
+
26
+ @dataclass
27
+ class _CanonicalMap:
28
+ values: dict[str, Any]
29
+
30
+
31
+ def _canonical(value: Any) -> str:
32
+ """Canonical, prototype-independent JSON key used for equality and maps."""
33
+
34
+ def normalize(item: Any):
35
+ if item is None or type(item) in (str, bool, int):
36
+ return item
37
+ if type(item) is float:
38
+ if not math.isfinite(item):
39
+ raise ValueError("Non-finite value in constraint program")
40
+ # JavaScript JSON serialization renders integral Numbers without a
41
+ # decimal suffix (for example, 1.0 as "1").
42
+ if item.is_integer():
43
+ return int(item)
44
+ return item
45
+ if type(item) is list:
46
+ return [normalize(child) for child in item]
47
+ if isinstance(item, Mapping):
48
+ if any(type(key) is not str for key in item):
49
+ raise ValueError("Non-text object key in constraint program")
50
+ return {key: normalize(item[key]) for key in sorted(item)}
51
+ raise ValueError("Non-JSON value in constraint program")
52
+
53
+ return json.dumps(normalize(value), ensure_ascii=False, separators=(",", ":"))
54
+
55
+
56
+ def _equal(left: Any, right: Any) -> bool:
57
+ if left is MISSING or right is MISSING:
58
+ return left is right
59
+ return _canonical(left) == _canonical(right)
60
+
61
+
62
+ def _js_string(value: Any) -> str:
63
+ if value is MISSING:
64
+ return "undefined"
65
+ if value is None:
66
+ return "null"
67
+ if value is True:
68
+ return "true"
69
+ if value is False:
70
+ return "false"
71
+ if isinstance(value, (dict, list)):
72
+ return "[object Object]" if isinstance(value, dict) else ",".join(_js_string(item) if item is not None else "" for item in value)
73
+ return str(value)
74
+
75
+
76
+ def _get(value: Any, path: Any) -> Any:
77
+ if not isinstance(path, list):
78
+ raise ValueError("get path must be an array") # noqa: TRY004 -- invalid constraint data uses the public ValueError contract
79
+ for part in path:
80
+ if isinstance(value, Mapping) and type(part) is str:
81
+ if part not in value:
82
+ return MISSING
83
+ value = value[part]
84
+ elif isinstance(value, Diagnostic) and type(part) is str and part in {"code", "path", "message", "severity"}:
85
+ value = getattr(value, part)
86
+ elif isinstance(value, list) and type(part) is int and 0 <= part < len(value):
87
+ value = value[part]
88
+ else:
89
+ return MISSING
90
+ return value
91
+
92
+
93
+ def _truthy(value: Any) -> bool:
94
+ if value is MISSING or value is None or value is False:
95
+ return False
96
+ if type(value) in (int, float) and value == 0:
97
+ return False
98
+ if isinstance(value, str) and not value: # noqa: SIM103 -- explicit ordered truthiness cases mirror the portable kernel
99
+ return False
100
+ return True
101
+
102
+
103
+ def _eval(expr: Any, env: dict[str, Any], registry: Registry | None, charge=lambda: None):
104
+ charge()
105
+ if isinstance(expr, list):
106
+ return [_eval(item, env, registry, charge) for item in expr]
107
+ if isinstance(expr, Mapping):
108
+ if set(expr) == {"var"}:
109
+ name = expr["var"]
110
+ if name not in env:
111
+ raise ValueError(f"Unbound constraint variable: {name}")
112
+ return env[name]
113
+ if "op" in expr:
114
+ if set(expr) - {"op", "args", "as", "index", "value"}:
115
+ raise ValueError("Unknown constraint expression member")
116
+ op = expr["op"]
117
+ args = expr.get("args", [])
118
+ if not isinstance(args, list):
119
+ raise ValueError("Constraint op args must be an array")
120
+ if op == "and":
121
+ for arg in args:
122
+ if not _truthy(_eval(arg, env, registry, charge)):
123
+ return False
124
+ return True
125
+ if op == "or":
126
+ for arg in args:
127
+ if _truthy(_eval(arg, env, registry, charge)):
128
+ return True
129
+ return False
130
+ if op in {"filter", "map", "find", "some", "every", "index"}:
131
+ return _collection(expr, env, registry, charge)
132
+ values = [_eval(arg, env, registry, charge) for arg in args]
133
+ return _apply(op, values, registry)
134
+ return {key: _eval(value, env, registry, charge) for key, value in expr.items()}
135
+ return expr
136
+
137
+
138
+ def _collection(expr: Mapping[str, Any], env: dict[str, Any], registry: Registry | None, charge):
139
+ op = expr["op"]
140
+ args = expr.get("args", [])
141
+ if len(args) != 1:
142
+ raise ValueError(f"{op} expects one collection argument")
143
+ source = _eval(args[0], env, registry, charge)
144
+ if source is MISSING or source is None:
145
+ source = []
146
+ if not isinstance(source, (list, tuple)):
147
+ raise ValueError(f"{op} input must be a sequence") # noqa: TRY004 -- invalid constraint data uses the public ValueError contract
148
+ name = expr.get("as")
149
+ index_name = expr.get("index")
150
+ if type(name) is not str or not name:
151
+ raise ValueError(f"{op} requires a local binding")
152
+ projected = []
153
+ if op in {"find", "some", "every"}:
154
+ for i, item in enumerate(source):
155
+ local = dict(env)
156
+ local[name] = item
157
+ if index_name is not None:
158
+ if type(index_name) is not str or not index_name:
159
+ raise ValueError(f"{op} index binding must be a name")
160
+ local[index_name] = i
161
+ matches = _truthy(_eval(expr.get("value", {"var": name}), local, registry, charge))
162
+ if op == "find" and matches:
163
+ return item
164
+ if op == "some" and matches:
165
+ return True
166
+ if op == "every" and not matches:
167
+ return False
168
+ return op == "every" if op != "find" else MISSING
169
+ for i, item in enumerate(source):
170
+ local = dict(env)
171
+ local[name] = item
172
+ if index_name is not None:
173
+ if type(index_name) is not str or not index_name:
174
+ raise ValueError(f"{op} index binding must be a name")
175
+ local[index_name] = i
176
+ value = _eval(expr.get("value", {"var": name}), local, registry, charge)
177
+ projected.append((item, value))
178
+ if op == "filter":
179
+ return [item for item, predicate in projected if _truthy(predicate)]
180
+ if op == "map":
181
+ return [value for _, value in projected]
182
+ result: dict[str, Any] = {}
183
+ for item, projected_key in projected:
184
+ token = _canonical(projected_key)
185
+ result[token] = item
186
+ return _CanonicalMap(result)
187
+
188
+
189
+ def _apply(op: str, a: list[Any], registry: Registry | None):
190
+ if op == "get":
191
+ if len(a) != 2:
192
+ raise ValueError("get expects [value,path]")
193
+ return _get(a[0], a[1])
194
+ if op == "has":
195
+ if len(a) != 2 or not isinstance(a[1], list):
196
+ raise ValueError("has expects [value,path]")
197
+ return _get(a[0], a[1]) is not MISSING
198
+ if op == "keys":
199
+ if len(a) != 1 or not isinstance(a[0], Mapping):
200
+ raise ValueError("keys expects an object")
201
+ return list(a[0])
202
+ if op == "values":
203
+ if len(a) != 1 or not isinstance(a[0], Mapping):
204
+ raise ValueError("values expects an object")
205
+ return list(a[0].values())
206
+ if op == "entries":
207
+ if len(a) != 1 or not isinstance(a[0], Mapping):
208
+ raise ValueError("entries expects an object")
209
+ return [[key, value] for key, value in a[0].items()]
210
+ if op == "length":
211
+ if len(a) != 1:
212
+ raise ValueError("length expects one value")
213
+ if isinstance(a[0], _CanonicalMap):
214
+ return len(a[0].values)
215
+ if a[0] is MISSING or a[0] is None:
216
+ return 0
217
+ if isinstance(a[0], (str, list, tuple)):
218
+ return len(a[0])
219
+ return getattr(a[0], "length", 0)
220
+ if op == "without":
221
+ if len(a) != 2 or not isinstance(a[0], Mapping) or not isinstance(a[1], list):
222
+ raise ValueError("without expects [object,keys]")
223
+ return {key: value for key, value in a[0].items() if key not in a[1]}
224
+ if op in ("eq", "ne"):
225
+ if len(a) != 2:
226
+ raise ValueError(f"{op} expects two values")
227
+ equal = _equal(a[0], a[1])
228
+ return equal if op == "eq" else not equal
229
+ if op in ("lt", "lte", "gt", "gte"):
230
+ if len(a) != 2 or type(a[0]) not in (int, float) or type(a[1]) not in (int, float):
231
+ raise ValueError(f"{op} expects numeric operands")
232
+ return {"lt": a[0] < a[1], "lte": a[0] <= a[1], "gt": a[0] > a[1], "gte": a[0] >= a[1]}[op]
233
+ if op == "in":
234
+ if len(a) != 2:
235
+ raise ValueError("in expects [value,collection]")
236
+ if isinstance(a[1], _CanonicalMap):
237
+ return _canonical(a[0]) in a[1].values
238
+ if isinstance(a[1], (list, tuple)):
239
+ return any(_equal(a[0], value) for value in a[1])
240
+ raise ValueError("in collection must be a sequence or canonical Map")
241
+ if op == "not":
242
+ if len(a) != 1:
243
+ raise ValueError("not expects one value")
244
+ return not _truthy(a[0])
245
+ if op in ("and", "or"):
246
+ return all(_truthy(value) for value in a) if op == "and" else any(_truthy(value) for value in a)
247
+ if op == "concat":
248
+ return "".join("" if value is None or value is MISSING else _js_string(value) for value in a)
249
+ if op == "pointer":
250
+ if len(a) != 1:
251
+ raise ValueError("pointer expects one value")
252
+ value = _js_string(a[0])
253
+ return value.replace("~", "~0").replace("/", "~1")
254
+ if op == "tuple":
255
+ return a
256
+ if op == "flatten":
257
+ if len(a) != 1 or not isinstance(a[0], list) or any(not isinstance(item, list) for item in a[0]):
258
+ raise ValueError("flatten expects an array of arrays")
259
+ return [child for group in a[0] for child in group]
260
+ if op == "canonical":
261
+ if len(a) != 1:
262
+ raise ValueError("canonical expects one value")
263
+ return _canonical(a[0])
264
+ if op == "lookup":
265
+ if len(a) != 2 or not isinstance(a[0], _CanonicalMap):
266
+ raise ValueError("lookup expects [index,key]")
267
+ return a[0].values.get(_canonical(a[1]), MISSING)
268
+ if op == "registered":
269
+ if len(a) != 2:
270
+ raise ValueError("registered expects [extensionId,version]")
271
+ return bool(registry and registry.get(a[0], a[1]))
272
+ if op == "sort":
273
+ if len(a) != 1 or not isinstance(a[0], list):
274
+ raise ValueError("sort expects a sequence")
275
+ return sorted(a[0], key=_js_string)
276
+ if op == "set":
277
+ return _CanonicalMap({})
278
+ if op == "literal-errors":
279
+ from .literal_kernel import literal_errors
280
+
281
+ return literal_errors(*a)
282
+ if op == "literal-identity":
283
+ from .literal_kernel import literal_identity
284
+
285
+ value = literal_identity(*a)
286
+ return MISSING if value is None else value
287
+ if op == "numeric-interval":
288
+ from .literal_kernel import numeric_interval
289
+
290
+ return numeric_interval(*a)
291
+ if op == "extension-errors":
292
+ return _extension_errors(a, registry)
293
+ raise ValueError(f"Unknown constraint operation: {op}")
294
+
295
+
296
+ def _extension_errors(args: list[Any], registry: Registry | None):
297
+ if len(args) != 5:
298
+ raise ValueError("extension-errors expects five arguments")
299
+ _document, payloads, scope, base_path, policy = args
300
+ if registry is None:
301
+ registry = Registry()
302
+ result = []
303
+ if not isinstance(payloads, Mapping):
304
+ return []
305
+ # Policy is data supplied by the generated model; the registered callback is
306
+ # the sole executable boundary and receives copied source context.
307
+ def add(key: str, at: str, detail: str | None = None):
308
+ rule = policy[key]
309
+ result.append(Diagnostic(rule["code"], at, detail if detail is not None else rule["message"], rule["severity"]))
310
+
311
+ for extension_id, content in payloads.items():
312
+ declaration = _get(_document, ["vocabularies", extension_id])
313
+ at = str(base_path) + "/extensions/" + str(extension_id).replace("~", "~0").replace("/", "~1")
314
+ if declaration is MISSING:
315
+ add("undeclared", at)
316
+ continue
317
+ entry = registry.get(extension_id, declaration.get("version"))
318
+ if entry is None:
319
+ continue
320
+ definition, validator = entry
321
+ if scope not in definition.scopes:
322
+ add("scope", at)
323
+ continue
324
+ errors = list(validator.iter_errors(content))
325
+ for error in errors:
326
+ suffix = "".join("/" + str(part).replace("~", "~0").replace("/", "~1") for part in error.path)
327
+ add("structure", at + suffix, error.message)
328
+ if not errors and definition.semantics is not None:
329
+ from .json import copy_json
330
+
331
+ document = copy_json(_document)
332
+ try:
333
+ returned = definition.semantics(copy_json(content), {"document": document, "scope": scope, "path": at})
334
+ if not isinstance(returned, (list, tuple)) or any(not isinstance(d, Diagnostic) or d.severity not in ("error", "warning") for d in returned):
335
+ raise ValueError("Extension callback must return Diagnostic objects")
336
+ result.extend(returned)
337
+ except Exception as error: # noqa: BLE001 -- consumer callback failures become scoped diagnostics
338
+ add("failure", at, str(error))
339
+ elif not errors:
340
+ add("unchecked", at)
341
+ return result
342
+
343
+
344
+ def _run_steps(steps: list[Any], env: dict[str, Any], registry: Registry | None, out: list[Diagnostic], charge):
345
+ for step in steps:
346
+ charge()
347
+ if not isinstance(step, Mapping):
348
+ raise ValueError("Constraint step must be an object") # noqa: TRY004 -- invalid constraint data uses the public ValueError contract
349
+ op = step.get("op")
350
+ if op == "let":
351
+ name = step.get("name")
352
+ if type(name) is not str or not name:
353
+ raise ValueError("let requires a name")
354
+ env[name] = _eval(step.get("value"), env, registry, charge)
355
+ elif op == "for":
356
+ values = _eval(step.get("each"), env, registry, charge)
357
+ if values is MISSING or values is None:
358
+ values = []
359
+ if not isinstance(values, (list, tuple)):
360
+ raise ValueError("for each must evaluate to a sequence")
361
+ name = step.get("as")
362
+ index_name = step.get("index")
363
+ if type(name) is not str or not name:
364
+ raise ValueError("for requires a local binding")
365
+ for index, value in enumerate(values):
366
+ local = dict(env)
367
+ local[name] = value
368
+ if index_name is not None:
369
+ if type(index_name) is not str or not index_name:
370
+ raise ValueError("for index must be a local name")
371
+ local[index_name] = index
372
+ _run_steps(step.get("body", []), local, registry, out, charge)
373
+ elif op == "if":
374
+ condition = _eval(step.get("test"), env, registry, charge)
375
+ _run_steps(step.get("then", []) if _truthy(condition) else step.get("else", []), dict(env), registry, out, charge)
376
+ elif op == "emit":
377
+ if "when" in step and not _truthy(_eval(step["when"], env, registry, charge)):
378
+ continue
379
+ diagnostic = _eval(step.get("diagnostic"), env, registry, charge)
380
+ if isinstance(diagnostic, Diagnostic):
381
+ if diagnostic.severity not in ("error", "warning"):
382
+ raise ValueError("Malformed diagnostic")
383
+ out.append(diagnostic)
384
+ continue
385
+ if not isinstance(diagnostic, Mapping):
386
+ raise ValueError("emit requires a diagnostic object")
387
+ values = {key: diagnostic.get(key) for key in ("code", "path", "message", "severity")}
388
+ if any(type(value) is not str for value in values.values()):
389
+ raise ValueError("Diagnostic fields must evaluate to text")
390
+ out.append(Diagnostic(**values))
391
+ elif op == "remember":
392
+ seen = _eval(step.get("set"), env, registry, charge)
393
+ if not isinstance(seen, _CanonicalMap):
394
+ raise ValueError("remember set must be a generated canonical Map")
395
+ key = _canonical(_eval(step.get("key"), env, registry, charge))
396
+ if key in seen.values:
397
+ if "diagnostic" in step:
398
+ diagnostic = _eval(step["diagnostic"], env, registry, charge)
399
+ if isinstance(diagnostic, Diagnostic):
400
+ if diagnostic.severity not in ("error", "warning"):
401
+ raise ValueError("Malformed diagnostic")
402
+ out.append(diagnostic)
403
+ continue
404
+ if not isinstance(diagnostic, Mapping):
405
+ raise ValueError("remember diagnostic must be an object")
406
+ values = {field: diagnostic.get(field) for field in ("code", "path", "message", "severity")}
407
+ if any(type(value) is not str for value in values.values()):
408
+ raise ValueError("Diagnostic fields must evaluate to text")
409
+ out.append(Diagnostic(**values))
410
+ else:
411
+ seen.values[key] = _eval(step["value"], env, registry, charge) if "value" in step else True
412
+ else:
413
+ raise ValueError(f"Unknown constraint step: {op}")
414
+
415
+
416
+ def _check_program(program: Mapping[str, Any]) -> None:
417
+ operations = {"get", "has", "keys", "values", "entries", "without", "length", "eq", "ne", "lt", "lte", "gt", "gte", "in", "not", "and", "or", "concat", "pointer", "tuple", "flatten", "canonical", "filter", "map", "find", "some", "every", "sort", "index", "lookup", "registered", "set", "literal-errors", "literal-identity", "numeric-interval", "extension-errors"}
418
+ steps = {"let", "for", "if", "emit", "remember"}
419
+
420
+ def check_expr(expr: Any):
421
+ if isinstance(expr, list):
422
+ for item in expr:
423
+ check_expr(item)
424
+ elif isinstance(expr, Mapping):
425
+ if "op" in expr and expr["op"] not in operations:
426
+ raise ValueError(f"Unknown constraint operation: {expr['op']}")
427
+ for value in expr.values():
428
+ check_expr(value)
429
+
430
+ def check_steps(sequence: Any):
431
+ if not isinstance(sequence, list):
432
+ raise ValueError("Constraint step body must be an array") # noqa: TRY004 -- invalid constraint data uses the public ValueError contract
433
+ for step in sequence:
434
+ if not isinstance(step, Mapping) or step.get("op") not in steps:
435
+ raise ValueError(f"Unknown constraint step: {step.get('op') if isinstance(step, Mapping) else None}")
436
+ for key, value in step.items():
437
+ if key in {"body", "then", "else"}:
438
+ check_steps(value)
439
+ elif key != "op":
440
+ check_expr(value)
441
+
442
+ check_steps(program.get("steps"))
443
+
444
+
445
+ def run_core_constraints(document: dict, registry: Registry | None = None, *, program=None, domain_policy=None):
446
+ """Execute model-authored current-core constraints in their declared order."""
447
+ if program is None or domain_policy is None:
448
+ from .generated_resources import generated_resource
449
+
450
+ program = generated_resource("constraints") if program is None else program
451
+ domain_policy = generated_resource("domain_policy") if domain_policy is None else domain_policy
452
+ if not isinstance(program, Mapping) or program.get("profile") != "umf.constraints/1":
453
+ raise ValueError("Unsupported UMF constraint program")
454
+ _check_program(program)
455
+ registry = registry or Registry()
456
+ diagnostics: list[Diagnostic] = []
457
+ visits = 0
458
+
459
+ def charge():
460
+ nonlocal visits
461
+ visits += 1
462
+ if visits > 5_000_000:
463
+ raise ValueError("Constraint operation limit exceeded")
464
+
465
+ _run_steps(program["steps"], {"doc": document, "policy": domain_policy}, registry, diagnostics, charge)
466
+ return tuple(diagnostics)
467
+
@@ -0,0 +1,47 @@
1
+ # SPDX-License-Identifier: MIT OR Apache-2.0
2
+ """Read-only access to compiler-owned UMF resources in source and wheel builds."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ from functools import cache
8
+ from importlib.resources import files
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ _RESOURCE_PATHS = {
13
+ "schema": "spec/core/schema.json",
14
+ "semantic_reference": "spec/core/semantic-type-reference.schema.json",
15
+ "constraints": "spec/metamodel/generated/core-constraints.json",
16
+ "domain_policy": "spec/metamodel/generated/domain-policy.json",
17
+ "python_shapes": "spec/metamodel/generated/python-shapes.json",
18
+ }
19
+
20
+
21
+ def _load_json(relative: str) -> Any:
22
+ """Resolve an installed resource first, then the repository checkout."""
23
+ resource = files("umf").joinpath(*relative.split("/"))
24
+ if resource.is_file():
25
+ return json.loads(resource.read_text(encoding="utf-8"))
26
+ package_root = Path(__file__).resolve().parents[2]
27
+ source = package_root / relative
28
+ if not source.is_file():
29
+ source = package_root.parent / relative
30
+ if not source.is_file():
31
+ raise FileNotFoundError(f"Generated UMF resource missing: {relative}")
32
+ return json.loads(source.read_text(encoding="utf-8"))
33
+
34
+
35
+ @cache
36
+ def _cached(name: str) -> Any:
37
+ try:
38
+ relative = _RESOURCE_PATHS[name]
39
+ except KeyError as error:
40
+ raise ValueError(f"Unknown generated UMF resource: {name}") from error
41
+ return _load_json(relative)
42
+
43
+
44
+ def generated_resource(name: str) -> Any:
45
+ """Return an isolated copy of one compiler-owned generated resource."""
46
+ return json.loads(json.dumps(_cached(name), ensure_ascii=False))
47
+
umf/json.py ADDED
@@ -0,0 +1,51 @@
1
+ # SPDX-License-Identifier: MIT OR Apache-2.0
2
+ """Portable JSON admission matching UMF's interoperable numeric boundary."""
3
+
4
+ import math
5
+
6
+ MAX_DEPTH = 128
7
+ MAX_VALUES = 100_000
8
+ MAX_TEXT_LENGTH = 4_000_000
9
+ MAX_SAFE_INTEGER = 2**53 - 1
10
+
11
+
12
+ def copy_json(value):
13
+ active = set()
14
+ count = 0
15
+
16
+ def visit(item, depth):
17
+ nonlocal count
18
+ count += 1
19
+ if count > MAX_VALUES or depth > MAX_DEPTH:
20
+ raise ValueError("LIMIT: JSON structural limit exceeded")
21
+ if item is None or type(item) in (str, bool):
22
+ return item
23
+ if type(item) is int:
24
+ if abs(item) > MAX_SAFE_INTEGER:
25
+ raise ValueError("NUMBER: use an exact integerToken carrier")
26
+ return item
27
+ if type(item) is float:
28
+ if (
29
+ not math.isfinite(item)
30
+ or item == 0
31
+ and math.copysign(1, item) < 0
32
+ or item.is_integer()
33
+ and abs(item) > MAX_SAFE_INTEGER
34
+ ):
35
+ raise ValueError("NUMBER: outside the interoperable numeric profile")
36
+ return item
37
+ if type(item) not in (list, dict):
38
+ raise ValueError("NON_JSON: plain JSON values required")
39
+ if id(item) in active:
40
+ raise ValueError("CYCLE: cyclic JSON value")
41
+ active.add(id(item))
42
+ try:
43
+ if type(item) is list:
44
+ return [visit(child, depth + 1) for child in item]
45
+ if any(type(key) is not str for key in item):
46
+ raise ValueError("KEY: JSON object keys must be strings")
47
+ return {key: visit(child, depth + 1) for key, child in item.items()}
48
+ finally:
49
+ active.remove(id(item))
50
+
51
+ return visit(value, 0)