arras 0.6.3__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 (173) hide show
  1. arras/__init__.py +114 -0
  2. arras/_catalogs.py +61 -0
  3. arras/_doctor_types.py +261 -0
  4. arras/_umf_engine.py +80 -0
  5. arras/_umf_projection.py +422 -0
  6. arras/actioncheck.py +309 -0
  7. arras/actions.py +686 -0
  8. arras/adapters/__init__.py +4 -0
  9. arras/adapters/truss/__init__.py +434 -0
  10. arras/adapters/truss/_conformance.py +42 -0
  11. arras/adapters/truss/apply.py +769 -0
  12. arras/adapters/truss/audit.py +502 -0
  13. arras/adapters/truss/cache.py +124 -0
  14. arras/adapters/truss/connection.py +631 -0
  15. arras/adapters/truss/describe.py +116 -0
  16. arras/adapters/truss/endpoint.py +205 -0
  17. arras/adapters/truss/export.py +216 -0
  18. arras/adapters/truss/lifecycle.py +402 -0
  19. arras/adapters/truss/load.py +354 -0
  20. arras/adapters/truss/local.py +525 -0
  21. arras/adapters/truss/outcome.py +315 -0
  22. arras/adapters/truss/plan.py +658 -0
  23. arras/adapters/truss/query.py +417 -0
  24. arras/adapters/truss/reach.py +56 -0
  25. arras/adapters/truss/registry.py +563 -0
  26. arras/adapters/truss/values.py +142 -0
  27. arras/adapters/truss/wire.py +229 -0
  28. arras/backend.py +184 -0
  29. arras/boundary.py +251 -0
  30. arras/branding.py +93 -0
  31. arras/callscope.py +76 -0
  32. arras/compat.py +140 -0
  33. arras/conformance/__init__.py +925 -0
  34. arras/conformance/__main__.py +63 -0
  35. arras/conformance/_checks.py +61 -0
  36. arras/conformance/corpus.json +5460 -0
  37. arras/conformance/fakes.py +92 -0
  38. arras/conformance/module.umf.json +516 -0
  39. arras/conformance/truss.py +239 -0
  40. arras/conformance/view_cases.json +1298 -0
  41. arras/conformance/views.py +108 -0
  42. arras/console/__init__.py +522 -0
  43. arras/core.py +894 -0
  44. arras/devcredentials.py +176 -0
  45. arras/devhost.py +697 -0
  46. arras/devtruss.py +326 -0
  47. arras/distribution.py +88 -0
  48. arras/doctor.py +923 -0
  49. arras/entityread.py +138 -0
  50. arras/errors.py +118 -0
  51. arras/exportcursor.py +67 -0
  52. arras/fakes.py +845 -0
  53. arras/fakes_domain.py +120 -0
  54. arras/fakes_eval.py +232 -0
  55. arras/fakes_export.py +87 -0
  56. arras/fakes_import.py +193 -0
  57. arras/fakes_journal.py +145 -0
  58. arras/fakes_plan.py +465 -0
  59. arras/fakes_profile.py +161 -0
  60. arras/fakes_registry.py +98 -0
  61. arras/fakes_search.py +37 -0
  62. arras/fakes_sql.py +625 -0
  63. arras/fakes_state.py +66 -0
  64. arras/fakes_weft.py +85 -0
  65. arras/gqlschema.py +847 -0
  66. arras/graphfile.py +134 -0
  67. arras/http.py +101 -0
  68. arras/inputs.py +149 -0
  69. arras/mcp/__init__.py +27 -0
  70. arras/mcp/__main__.py +7 -0
  71. arras/mcp/backends.py +49 -0
  72. arras/mcp/config.py +604 -0
  73. arras/mcp/identity.py +114 -0
  74. arras/mcp/process.py +168 -0
  75. arras/mcp/server.py +745 -0
  76. arras/mcp/tools.py +141 -0
  77. arras/model.py +145 -0
  78. arras/names.py +46 -0
  79. arras/operate/__init__.py +554 -0
  80. arras/operate/explorer.py +96 -0
  81. arras/operate/openapi.py +482 -0
  82. arras/operate/schema_app.py +171 -0
  83. arras/operate/wire.py +363 -0
  84. arras/policy.py +553 -0
  85. arras/public-api.json +1897 -0
  86. arras/py.typed +0 -0
  87. arras/registration.py +136 -0
  88. arras/results.py +281 -0
  89. arras/routing.py +81 -0
  90. arras/sandbox.py +260 -0
  91. arras/schema_admin.py +172 -0
  92. arras/search.py +256 -0
  93. arras/searchplan.py +278 -0
  94. arras/serialize.py +60 -0
  95. arras/telemetry.py +465 -0
  96. arras/testing.py +122 -0
  97. arras/types.py +95 -0
  98. arras/ui/__init__.py +422 -0
  99. arras/ui/__main__.py +31 -0
  100. arras/ui/_brand_page.py +125 -0
  101. arras/ui/assets.py +304 -0
  102. arras/ui/npm/documentdrivendx-arras-ui-0.6.3.tgz +0 -0
  103. arras/ui/static/asset-manifest.json +320 -0
  104. arras/ui/static/assets/index-BnVFRTvo.js +21 -0
  105. arras/ui/static/assets/index-DO2QCGiC.css +2 -0
  106. arras/ui/static/embed.html +14 -0
  107. arras/ui/static/umf-explorer/chunk-0cyxfbf5.js +1 -0
  108. arras/ui/static/umf-explorer/chunk-0yrdndnq.js +7 -0
  109. arras/ui/static/umf-explorer/chunk-189t60eb.js +1 -0
  110. arras/ui/static/umf-explorer/chunk-19s0r9xs.js +2 -0
  111. arras/ui/static/umf-explorer/chunk-2zhqqq8v.js +2 -0
  112. arras/ui/static/umf-explorer/chunk-3b8jxkra.js +2 -0
  113. arras/ui/static/umf-explorer/chunk-3ckrb4ha.js +128 -0
  114. arras/ui/static/umf-explorer/chunk-3e927yyq.js +1 -0
  115. arras/ui/static/umf-explorer/chunk-48my5and.js +1 -0
  116. arras/ui/static/umf-explorer/chunk-6bbybc4e.js +1 -0
  117. arras/ui/static/umf-explorer/chunk-74rcqzrq.js +1 -0
  118. arras/ui/static/umf-explorer/chunk-8avk1t53.js +3805 -0
  119. arras/ui/static/umf-explorer/chunk-8fwevz7v.js +3 -0
  120. arras/ui/static/umf-explorer/chunk-9041nsbv.js +1 -0
  121. arras/ui/static/umf-explorer/chunk-9ht6vzn4.js +1 -0
  122. arras/ui/static/umf-explorer/chunk-a3r35vwj.js +2 -0
  123. arras/ui/static/umf-explorer/chunk-a8rwnqq8.js +1 -0
  124. arras/ui/static/umf-explorer/chunk-age4zjs5.js +1 -0
  125. arras/ui/static/umf-explorer/chunk-awjyb4jb.js +1 -0
  126. arras/ui/static/umf-explorer/chunk-d22wwjwm.js +1 -0
  127. arras/ui/static/umf-explorer/chunk-dnyq4rx2.js +1 -0
  128. arras/ui/static/umf-explorer/chunk-dyfe9tns.js +1 -0
  129. arras/ui/static/umf-explorer/chunk-dyhe34jv.js +1 -0
  130. arras/ui/static/umf-explorer/chunk-fff90kq1.js +1 -0
  131. arras/ui/static/umf-explorer/chunk-ffy3x09s.js +1 -0
  132. arras/ui/static/umf-explorer/chunk-h838d7m9.js +1 -0
  133. arras/ui/static/umf-explorer/chunk-hjqm6009.js +2 -0
  134. arras/ui/static/umf-explorer/chunk-j1761thh.js +1 -0
  135. arras/ui/static/umf-explorer/chunk-j6hnavwm.js +1 -0
  136. arras/ui/static/umf-explorer/chunk-k09yed2v.js +1 -0
  137. arras/ui/static/umf-explorer/chunk-k2v9vyvz.js +1 -0
  138. arras/ui/static/umf-explorer/chunk-k9nw71nj.js +1 -0
  139. arras/ui/static/umf-explorer/chunk-kkbcw88c.js +21 -0
  140. arras/ui/static/umf-explorer/chunk-kxxpzsd0.js +38 -0
  141. arras/ui/static/umf-explorer/chunk-mf0yn2d9.js +1 -0
  142. arras/ui/static/umf-explorer/chunk-mjj9769n.js +64 -0
  143. arras/ui/static/umf-explorer/chunk-mt4ev39n.js +1 -0
  144. arras/ui/static/umf-explorer/chunk-my1cq8f9.js +138 -0
  145. arras/ui/static/umf-explorer/chunk-qqr3ahja.js +2 -0
  146. arras/ui/static/umf-explorer/chunk-ww4nmdsp.js +193 -0
  147. arras/ui/static/umf-explorer/chunk-xyn4vvqt.js +73 -0
  148. arras/ui/static/umf-explorer/chunk-y33s5b58.js +2 -0
  149. arras/ui/static/umf-explorer/chunk-yc072vtb.js +29 -0
  150. arras/ui/static/umf-explorer/chunk-yg38jvvb.js +0 -0
  151. arras/ui/static/umf-explorer/chunk-yp36ghgq.js +1 -0
  152. arras/ui/static/umf-explorer/chunk-yybmn8nw.js +1 -0
  153. arras/ui/static/umf-explorer/chunk-z7fkf19q.js +2 -0
  154. arras/ui/static/umf-explorer/chunk-zc4shbzz.js +17 -0
  155. arras/ui/static/umf-explorer/chunk-zsn9vjj1.js +1 -0
  156. arras/ui/static/umf-explorer/chunk-zvzn5tbp.js +1 -0
  157. arras/ui/static/umf-explorer/components.css +21 -0
  158. arras/ui/static/umf-explorer/explorer.css +153 -0
  159. arras/ui/static/umf-explorer/explorer.js +32 -0
  160. arras/ui/static/umf-explorer/index.html +4 -0
  161. arras/ui/static/umf-explorer/schema-catalog.json +1 -0
  162. arras/ui/static/umf-explorer/style.css +7 -0
  163. arras/umf.py +404 -0
  164. arras/umf_admission.py +277 -0
  165. arras/versions.py +16 -0
  166. arras/view.py +892 -0
  167. arras/viewcache.py +50 -0
  168. arras-0.6.3.dist-info/METADATA +358 -0
  169. arras-0.6.3.dist-info/RECORD +173 -0
  170. arras-0.6.3.dist-info/WHEEL +4 -0
  171. arras-0.6.3.dist-info/licenses/LICENSE-APACHE +201 -0
  172. arras-0.6.3.dist-info/licenses/LICENSE-MIT +21 -0
  173. arras-0.6.3.dist-info/licenses/LICENSE.md +239 -0
arras/__init__.py ADDED
@@ -0,0 +1,114 @@
1
+ # SPDX-License-Identifier: MIT OR Apache-2.0
2
+ """Arras: the interface to an ontology.
3
+
4
+ `__all__` is the surface a host imports from; everything else is internal and may change.
5
+ """
6
+
7
+ from arras.actions import ActionInfo
8
+ from arras.core import Arras, Context, MigrationResult, Ontology, Registry, Status
9
+ from arras.errors import (
10
+ ArrasError,
11
+ Conflict,
12
+ Failure,
13
+ Invalid,
14
+ NotYetCurrent,
15
+ PreconditionFailed,
16
+ Refused,
17
+ Unavailable,
18
+ Unsupported,
19
+ Violation,
20
+ )
21
+ from arras.model import (
22
+ Capabilities,
23
+ EntityTypeInfo,
24
+ ModuleInfo,
25
+ Placement,
26
+ PropertyInfo,
27
+ RelationshipInfo,
28
+ )
29
+ from arras.results import (
30
+ BackendStatus,
31
+ BatchReceipt,
32
+ Change,
33
+ ChangesPage,
34
+ ImportResult,
35
+ ItemResult,
36
+ Load,
37
+ LoadsPage,
38
+ Receipt,
39
+ RecordsPage,
40
+ RegisteredDocument,
41
+ RegistrationCheck,
42
+ RegistryResult,
43
+ Rejection,
44
+ SchemaChange,
45
+ SchemaPlan,
46
+ Validation,
47
+ WeftSupport,
48
+ )
49
+ from arras.schema_admin import Schema
50
+ from arras.types import (
51
+ Column,
52
+ Consistency,
53
+ Freshness,
54
+ Position,
55
+ Principal,
56
+ Revision,
57
+ Rows,
58
+ Statement,
59
+ )
60
+ from arras.versions import CONTRACT_VERSIONS
61
+
62
+ __all__ = [
63
+ "CONTRACT_VERSIONS",
64
+ "ActionInfo",
65
+ "Arras",
66
+ "ArrasError",
67
+ "BackendStatus",
68
+ "BatchReceipt",
69
+ "Capabilities",
70
+ "Change",
71
+ "ChangesPage",
72
+ "Column",
73
+ "Conflict",
74
+ "Consistency",
75
+ "Context",
76
+ "EntityTypeInfo",
77
+ "Failure",
78
+ "Freshness",
79
+ "ImportResult",
80
+ "Invalid",
81
+ "ItemResult",
82
+ "Load",
83
+ "LoadsPage",
84
+ "MigrationResult",
85
+ "ModuleInfo",
86
+ "NotYetCurrent",
87
+ "Ontology",
88
+ "Placement",
89
+ "Position",
90
+ "PreconditionFailed",
91
+ "Principal",
92
+ "PropertyInfo",
93
+ "Receipt",
94
+ "RecordsPage",
95
+ "Refused",
96
+ "RegisteredDocument",
97
+ "RegistrationCheck",
98
+ "Registry",
99
+ "RegistryResult",
100
+ "Rejection",
101
+ "RelationshipInfo",
102
+ "Revision",
103
+ "Rows",
104
+ "Schema",
105
+ "SchemaChange",
106
+ "SchemaPlan",
107
+ "Statement",
108
+ "Status",
109
+ "Unavailable",
110
+ "Unsupported",
111
+ "Validation",
112
+ "Violation",
113
+ "WeftSupport",
114
+ ]
arras/_catalogs.py ADDED
@@ -0,0 +1,61 @@
1
+ # SPDX-License-Identifier: MIT OR Apache-2.0
2
+ """Private per-call catalog collection shared by the Arras interface (CONTRACT-002)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from collections.abc import Callable, Mapping
7
+ from dataclasses import dataclass, replace
8
+
9
+ from arras.backend import Backend
10
+ from arras.errors import ArrasError, Refused, Unavailable
11
+ from arras.model import ModuleInfo, Placement
12
+ from arras.types import Principal
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class Catalogs:
17
+ """What each backend said about the modules the principal may read, and who failed."""
18
+
19
+ by_backend: dict[str, Mapping[str, ModuleInfo]]
20
+ failures: dict[str, ArrasError]
21
+
22
+ def failure(self) -> ArrasError | None:
23
+ """The error to report: an expired token is `Refused`, whatever else is down."""
24
+ refused = [e for e in self.failures.values() if isinstance(e, Refused)]
25
+ return (refused or list(self.failures.values()) or [None])[0]
26
+
27
+ def module(self, name: str) -> ModuleInfo | None:
28
+ """The record store's copy where it answered, else another backend's."""
29
+ holders = [(b, c[name]) for b, c in self.by_backend.items() if name in c]
30
+ for b, module in holders:
31
+ if module.placement.record == b:
32
+ return module
33
+ return holders[0][1] if holders else None
34
+
35
+ def of(self, backend: str, name: str) -> ModuleInfo | None:
36
+ return self.by_backend.get(backend, {}).get(name)
37
+
38
+ def names(self) -> list[str]:
39
+ return sorted({name for catalog in self.by_backend.values() for name in catalog})
40
+
41
+
42
+ def collect_catalogs(
43
+ backends: Mapping[str, Backend],
44
+ principal: Principal,
45
+ placement_of: Callable[[str], Placement | None],
46
+ ) -> Catalogs:
47
+ """Collect fresh catalogs as this principal, filtering to host-placed readable modules."""
48
+ by_backend: dict[str, Mapping[str, ModuleInfo]] = {}
49
+ failures: dict[str, ArrasError] = {}
50
+ for name, backend in backends.items():
51
+ try:
52
+ held = backend.catalog(principal)
53
+ except (Unavailable, Refused) as error:
54
+ failures[name] = error
55
+ continue
56
+ by_backend[name] = {
57
+ module: replace(info, placement=placement)
58
+ for module, info in held.items()
59
+ if (placement := placement_of(module)) is not None and name in placement.readers
60
+ }
61
+ return Catalogs(by_backend, failures)
arras/_doctor_types.py ADDED
@@ -0,0 +1,261 @@
1
+ # SPDX-License-Identifier: MIT OR Apache-2.0
2
+ """Immutable request and report values for the offline compatibility doctor."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ from collections.abc import Mapping
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+ from types import MappingProxyType
11
+ from typing import Any, Literal, cast
12
+
13
+ from arras.distribution import DistributionConfig, TelemetryNames
14
+
15
+ _MAX_ITEMS = 32
16
+
17
+
18
+ class DistributionInvalid(ValueError):
19
+ """A structurally valid distribution request violates its typed value constraints."""
20
+
21
+
22
+ def _telemetry_fields(value: object) -> dict[str, Any]: # pyright: ignore[reportUnusedFunction]
23
+ """The closed JSON shape for typed telemetry names; semantic validation stays shared."""
24
+ if type(value) is not dict:
25
+ raise ValueError("telemetry must be an object")
26
+ fields = cast(dict[str, Any], value)
27
+ if set(fields) - set(TelemetryNames.__dataclass_fields__) or not all(
28
+ isinstance(item, str) or (key == "service_name" and item is None)
29
+ for key, item in fields.items()
30
+ ):
31
+ raise ValueError("telemetry fields have invalid shape")
32
+ return fields
33
+
34
+
35
+ def pairs(pairs: list[tuple[str, object]]) -> dict[str, object]:
36
+ result: dict[str, object] = {}
37
+ for key, value in pairs:
38
+ if key in result:
39
+ raise ValueError("duplicate key")
40
+ result[key] = value
41
+ return result
42
+
43
+
44
+ def load_json_object(raw: bytes) -> dict[str, Any]:
45
+ value: object = json.loads(raw.decode("utf-8"), object_pairs_hook=pairs)
46
+ if not isinstance(value, dict):
47
+ raise ValueError("JSON object required")
48
+ return cast(dict[str, Any], value)
49
+
50
+
51
+ def _is_instance(value: object, kind: type[object]) -> bool:
52
+ return isinstance(value, kind)
53
+
54
+
55
+ def _validate_request(request: DoctorRequest) -> None:
56
+ raw_maps: tuple[tuple[str, object], ...] = (
57
+ ("expected_packages", request.expected_packages),
58
+ ("expected_contracts", request.expected_contracts),
59
+ ("required_python_exports", request.required_python_exports),
60
+ ("required_npm_exports", request.required_npm_exports),
61
+ )
62
+ for name, raw in raw_maps:
63
+ if not _is_instance(raw, Mapping):
64
+ raise ValueError(f"{name}: mapping required")
65
+ raw_tuples: tuple[tuple[str, object], ...] = (
66
+ ("required_role_tokens", request.required_role_tokens),
67
+ ("env_names", request.env_names),
68
+ ("config_files", request.config_files),
69
+ ("module_documents", request.module_documents),
70
+ )
71
+ for name, raw in raw_tuples:
72
+ if not _is_instance(raw, tuple):
73
+ raise ValueError(f"{name}: tuple required")
74
+ _validate_request_maps(request)
75
+ if any(
76
+ not isinstance(item, str) for item in cast(tuple[object, ...], request.required_role_tokens)
77
+ ):
78
+ raise ValueError("required_role_tokens: string values required")
79
+ if any(not isinstance(item, str) for item in cast(tuple[object, ...], request.env_names)):
80
+ raise ValueError("env_names: string values required")
81
+ if any(not isinstance(item, Path) for item in cast(tuple[object, ...], request.config_files)):
82
+ raise ValueError("config_files: Path values required")
83
+ if any(
84
+ not isinstance(item, Path) for item in cast(tuple[object, ...], request.module_documents)
85
+ ):
86
+ raise ValueError("module_documents: Path values required")
87
+ object.__setattr__(
88
+ request, "expected_packages", MappingProxyType(dict(request.expected_packages))
89
+ )
90
+ object.__setattr__(
91
+ request, "expected_contracts", MappingProxyType(dict(request.expected_contracts))
92
+ )
93
+ object.__setattr__(
94
+ request,
95
+ "required_python_exports",
96
+ MappingProxyType({k: tuple(v) for k, v in request.required_python_exports.items()}),
97
+ )
98
+ object.__setattr__(
99
+ request,
100
+ "required_npm_exports",
101
+ MappingProxyType({k: tuple(v) for k, v in request.required_npm_exports.items()}),
102
+ )
103
+ object.__setattr__(request, "required_role_tokens", tuple(request.required_role_tokens))
104
+ object.__setattr__(request, "env_names", tuple(request.env_names))
105
+ object.__setattr__(request, "config_files", tuple(request.config_files))
106
+ object.__setattr__(request, "module_documents", tuple(request.module_documents))
107
+ if request.module_profile is not None and not isinstance(
108
+ cast(object, request.module_profile), str
109
+ ):
110
+ raise ValueError("module_profile must be a string")
111
+ if request.distribution is not None and not isinstance(
112
+ cast(object, request.distribution), DistributionConfig
113
+ ):
114
+ raise ValueError("distribution must be a DistributionConfig")
115
+ if request.asset_manifest is not None and not isinstance(
116
+ cast(object, request.asset_manifest), Path
117
+ ):
118
+ raise ValueError("asset_manifest must be a Path")
119
+ if request.theme_manifest is not None and not isinstance(
120
+ cast(object, request.theme_manifest), Path
121
+ ):
122
+ raise ValueError("theme_manifest must be a Path")
123
+ paths = (*request.config_files, *request.module_documents)
124
+ if len(paths) > _MAX_ITEMS:
125
+ raise ValueError("config_files and module_documents: at most 32 paths")
126
+
127
+
128
+ def _validate_request_maps(request: DoctorRequest) -> None:
129
+ for package, version in request.expected_packages.items():
130
+ if (
131
+ not _is_instance(package, str)
132
+ or not _is_instance(version, str)
133
+ or not package.strip()
134
+ or not version.strip()
135
+ ):
136
+ raise ValueError("expected_packages: nonempty package names and versions required")
137
+ for key, version in cast(Mapping[object, object], request.expected_contracts).items():
138
+ if not isinstance(key, str) or type(version) is not int or version < 1:
139
+ raise ValueError("expected_contracts: names and positive integer versions required")
140
+ for name in ("required_python_exports", "required_npm_exports"):
141
+ mapping = getattr(request, name)
142
+ for key, names in cast(Mapping[object, object], mapping).items():
143
+ if (
144
+ not isinstance(key, str)
145
+ or not isinstance(names, tuple)
146
+ or any(not isinstance(item, str) for item in cast(tuple[object, ...], names))
147
+ ):
148
+ raise ValueError(f"{name}: string keys and tuple of names required")
149
+
150
+
151
+ @dataclass(frozen=True, slots=True)
152
+ class DoctorRequest:
153
+ expected_packages: Mapping[str, str] = field(default_factory=lambda: MappingProxyType({}))
154
+ expected_contracts: Mapping[str, int] = field(default_factory=lambda: MappingProxyType({}))
155
+ required_python_exports: Mapping[str, tuple[str, ...]] = field(
156
+ default_factory=lambda: MappingProxyType({})
157
+ )
158
+ required_npm_exports: Mapping[str, tuple[str, ...]] = field(
159
+ default_factory=lambda: MappingProxyType({})
160
+ )
161
+ required_role_tokens: tuple[str, ...] = ()
162
+ distribution: DistributionConfig | None = None
163
+ env_names: tuple[str, ...] = ()
164
+ config_files: tuple[Path, ...] = ()
165
+ asset_manifest: Path | None = None
166
+ theme_manifest: Path | None = None
167
+ module_documents: tuple[Path, ...] = ()
168
+ module_profile: str | None = None
169
+
170
+ def __post_init__(self) -> None:
171
+ _validate_request(self)
172
+
173
+
174
+ @dataclass(frozen=True, slots=True)
175
+ class Diagnostic:
176
+ code: str
177
+ severity: Literal["error", "unsupported", "warning", "info"]
178
+ surface: str
179
+ path: str
180
+ message: str
181
+
182
+
183
+ @dataclass(frozen=True, slots=True)
184
+ class DoctorReport:
185
+ ok: bool
186
+ complete: bool
187
+ observed: Mapping[str, object]
188
+ diagnostics: tuple[Diagnostic, ...]
189
+ schema_version: int = 1
190
+
191
+ def __post_init__(self) -> None:
192
+ object.__setattr__(self, "observed", _freeze(self.observed))
193
+ object.__setattr__(self, "diagnostics", tuple(self.diagnostics))
194
+
195
+ def to_dict(self) -> dict[str, object]:
196
+ return {
197
+ "schema_version": self.schema_version,
198
+ "ok": self.ok,
199
+ "complete": self.complete,
200
+ "observed": _json_value(self.observed),
201
+ "diagnostics": [
202
+ {
203
+ "code": d.code,
204
+ "severity": d.severity,
205
+ "surface": d.surface,
206
+ "path": d.path,
207
+ "message": d.message,
208
+ }
209
+ for d in self.diagnostics
210
+ ],
211
+ }
212
+
213
+
214
+ def _freeze(value: object) -> object:
215
+ if isinstance(value, Mapping):
216
+ entries = cast(Mapping[object, object], value)
217
+ return MappingProxyType({str(k): _freeze(v) for k, v in entries.items()})
218
+ if isinstance(value, tuple | list):
219
+ items = cast(tuple[object, ...] | list[object], value)
220
+ return tuple(_freeze(v) for v in items)
221
+ return value
222
+
223
+
224
+ def _json_value(value: object) -> object:
225
+ if isinstance(value, Mapping):
226
+ entries = cast(Mapping[object, object], value)
227
+ return {str(k): _json_value(v) for k, v in entries.items()}
228
+ if isinstance(value, tuple | list):
229
+ items = cast(tuple[object, ...] | list[object], value)
230
+ return [_json_value(v) for v in items]
231
+ return value
232
+
233
+
234
+ def request_error_report() -> DoctorReport:
235
+ return DoctorReport(
236
+ False,
237
+ True,
238
+ MappingProxyType({"provenance": "request rejected"}),
239
+ (
240
+ Diagnostic(
241
+ "request_invalid", "error", "request", "request", "request is invalid or unsafe"
242
+ ),
243
+ ),
244
+ )
245
+
246
+
247
+ def distribution_error_report() -> DoctorReport:
248
+ return DoctorReport(
249
+ False,
250
+ True,
251
+ MappingProxyType({"provenance": "request rejected"}),
252
+ (
253
+ Diagnostic(
254
+ "distribution_invalid",
255
+ "error",
256
+ "distribution",
257
+ "distribution",
258
+ "distribution settings violate Brand or launcher constraints",
259
+ ),
260
+ ),
261
+ )
arras/_umf_engine.py ADDED
@@ -0,0 +1,80 @@
1
+ # SPDX-License-Identifier: MIT OR Apache-2.0
2
+ """Private, inert bridge to the selected official UMF Python engine."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass
8
+ from importlib import import_module
9
+ from importlib.metadata import version
10
+ from typing import TYPE_CHECKING, Any, cast
11
+
12
+ if TYPE_CHECKING:
13
+ from umf import Registry, Validation
14
+
15
+ OfficialRegistryFactory = Callable[[], Registry]
16
+ OfficialValidator = Callable[[object, Registry | None], Validation]
17
+ else:
18
+ OfficialRegistryFactory = Callable[..., Any]
19
+ OfficialValidator = Callable[..., Any]
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class EngineDiagnostic:
24
+ code: str
25
+ path: str
26
+ message: str
27
+ severity: str
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class EngineResult:
32
+ distribution: str
33
+ version: str
34
+ valid: bool
35
+ complete: bool
36
+ diagnostics: tuple[EngineDiagnostic, ...]
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class ReaderRefusal:
41
+ distribution: str
42
+ version: str
43
+
44
+
45
+ def read_and_validate(text: str) -> EngineResult | ReaderRefusal:
46
+ """Use the official reader and validator with a new empty registry."""
47
+ sdk = cast(Any, import_module("umf"))
48
+
49
+ installed_version = version("umf-core")
50
+ try:
51
+ reader = cast(Callable[[str, str], Any], sdk.read_json_value)
52
+ raw: object = reader(text, "json")
53
+ except ValueError:
54
+ return ReaderRefusal("umf-core", installed_version)
55
+ registry_factory = cast(OfficialRegistryFactory, sdk.Registry)
56
+ validator = cast(OfficialValidator, sdk.validate_document)
57
+ result = validator(raw, registry_factory())
58
+ diagnostics = tuple(
59
+ EngineDiagnostic(
60
+ item.code,
61
+ item.path,
62
+ item.message,
63
+ item.severity,
64
+ )
65
+ for item in result.diagnostics
66
+ )
67
+ return EngineResult("umf-core", installed_version, result.valid, result.complete, diagnostics)
68
+
69
+
70
+ def read_typed_snapshot(text: str) -> object:
71
+ """Read a post-validation document through the official typed reader.
72
+
73
+ The caller is responsible for admitting this exact text first. Returning only an
74
+ `exclude_unset` model dump keeps the mutable official model inside this bridge while
75
+ preserving which optional fields were explicitly supplied.
76
+ """
77
+ sdk = cast(Any, import_module("umf"))
78
+ reader = cast(Callable[[str, str], Any], sdk.read_document)
79
+ document = reader(text, "json")
80
+ return document.model_dump(mode="python", exclude_unset=True)