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.
- arras/__init__.py +114 -0
- arras/_catalogs.py +61 -0
- arras/_doctor_types.py +261 -0
- arras/_umf_engine.py +80 -0
- arras/_umf_projection.py +422 -0
- arras/actioncheck.py +309 -0
- arras/actions.py +686 -0
- arras/adapters/__init__.py +4 -0
- arras/adapters/truss/__init__.py +434 -0
- arras/adapters/truss/_conformance.py +42 -0
- arras/adapters/truss/apply.py +769 -0
- arras/adapters/truss/audit.py +502 -0
- arras/adapters/truss/cache.py +124 -0
- arras/adapters/truss/connection.py +631 -0
- arras/adapters/truss/describe.py +116 -0
- arras/adapters/truss/endpoint.py +205 -0
- arras/adapters/truss/export.py +216 -0
- arras/adapters/truss/lifecycle.py +402 -0
- arras/adapters/truss/load.py +354 -0
- arras/adapters/truss/local.py +525 -0
- arras/adapters/truss/outcome.py +315 -0
- arras/adapters/truss/plan.py +658 -0
- arras/adapters/truss/query.py +417 -0
- arras/adapters/truss/reach.py +56 -0
- arras/adapters/truss/registry.py +563 -0
- arras/adapters/truss/values.py +142 -0
- arras/adapters/truss/wire.py +229 -0
- arras/backend.py +184 -0
- arras/boundary.py +251 -0
- arras/branding.py +93 -0
- arras/callscope.py +76 -0
- arras/compat.py +140 -0
- arras/conformance/__init__.py +925 -0
- arras/conformance/__main__.py +63 -0
- arras/conformance/_checks.py +61 -0
- arras/conformance/corpus.json +5460 -0
- arras/conformance/fakes.py +92 -0
- arras/conformance/module.umf.json +516 -0
- arras/conformance/truss.py +239 -0
- arras/conformance/view_cases.json +1298 -0
- arras/conformance/views.py +108 -0
- arras/console/__init__.py +522 -0
- arras/core.py +894 -0
- arras/devcredentials.py +176 -0
- arras/devhost.py +697 -0
- arras/devtruss.py +326 -0
- arras/distribution.py +88 -0
- arras/doctor.py +923 -0
- arras/entityread.py +138 -0
- arras/errors.py +118 -0
- arras/exportcursor.py +67 -0
- arras/fakes.py +845 -0
- arras/fakes_domain.py +120 -0
- arras/fakes_eval.py +232 -0
- arras/fakes_export.py +87 -0
- arras/fakes_import.py +193 -0
- arras/fakes_journal.py +145 -0
- arras/fakes_plan.py +465 -0
- arras/fakes_profile.py +161 -0
- arras/fakes_registry.py +98 -0
- arras/fakes_search.py +37 -0
- arras/fakes_sql.py +625 -0
- arras/fakes_state.py +66 -0
- arras/fakes_weft.py +85 -0
- arras/gqlschema.py +847 -0
- arras/graphfile.py +134 -0
- arras/http.py +101 -0
- arras/inputs.py +149 -0
- arras/mcp/__init__.py +27 -0
- arras/mcp/__main__.py +7 -0
- arras/mcp/backends.py +49 -0
- arras/mcp/config.py +604 -0
- arras/mcp/identity.py +114 -0
- arras/mcp/process.py +168 -0
- arras/mcp/server.py +745 -0
- arras/mcp/tools.py +141 -0
- arras/model.py +145 -0
- arras/names.py +46 -0
- arras/operate/__init__.py +554 -0
- arras/operate/explorer.py +96 -0
- arras/operate/openapi.py +482 -0
- arras/operate/schema_app.py +171 -0
- arras/operate/wire.py +363 -0
- arras/policy.py +553 -0
- arras/public-api.json +1897 -0
- arras/py.typed +0 -0
- arras/registration.py +136 -0
- arras/results.py +281 -0
- arras/routing.py +81 -0
- arras/sandbox.py +260 -0
- arras/schema_admin.py +172 -0
- arras/search.py +256 -0
- arras/searchplan.py +278 -0
- arras/serialize.py +60 -0
- arras/telemetry.py +465 -0
- arras/testing.py +122 -0
- arras/types.py +95 -0
- arras/ui/__init__.py +422 -0
- arras/ui/__main__.py +31 -0
- arras/ui/_brand_page.py +125 -0
- arras/ui/assets.py +304 -0
- arras/ui/npm/documentdrivendx-arras-ui-0.6.3.tgz +0 -0
- arras/ui/static/asset-manifest.json +320 -0
- arras/ui/static/assets/index-BnVFRTvo.js +21 -0
- arras/ui/static/assets/index-DO2QCGiC.css +2 -0
- arras/ui/static/embed.html +14 -0
- arras/ui/static/umf-explorer/chunk-0cyxfbf5.js +1 -0
- arras/ui/static/umf-explorer/chunk-0yrdndnq.js +7 -0
- arras/ui/static/umf-explorer/chunk-189t60eb.js +1 -0
- arras/ui/static/umf-explorer/chunk-19s0r9xs.js +2 -0
- arras/ui/static/umf-explorer/chunk-2zhqqq8v.js +2 -0
- arras/ui/static/umf-explorer/chunk-3b8jxkra.js +2 -0
- arras/ui/static/umf-explorer/chunk-3ckrb4ha.js +128 -0
- arras/ui/static/umf-explorer/chunk-3e927yyq.js +1 -0
- arras/ui/static/umf-explorer/chunk-48my5and.js +1 -0
- arras/ui/static/umf-explorer/chunk-6bbybc4e.js +1 -0
- arras/ui/static/umf-explorer/chunk-74rcqzrq.js +1 -0
- arras/ui/static/umf-explorer/chunk-8avk1t53.js +3805 -0
- arras/ui/static/umf-explorer/chunk-8fwevz7v.js +3 -0
- arras/ui/static/umf-explorer/chunk-9041nsbv.js +1 -0
- arras/ui/static/umf-explorer/chunk-9ht6vzn4.js +1 -0
- arras/ui/static/umf-explorer/chunk-a3r35vwj.js +2 -0
- arras/ui/static/umf-explorer/chunk-a8rwnqq8.js +1 -0
- arras/ui/static/umf-explorer/chunk-age4zjs5.js +1 -0
- arras/ui/static/umf-explorer/chunk-awjyb4jb.js +1 -0
- arras/ui/static/umf-explorer/chunk-d22wwjwm.js +1 -0
- arras/ui/static/umf-explorer/chunk-dnyq4rx2.js +1 -0
- arras/ui/static/umf-explorer/chunk-dyfe9tns.js +1 -0
- arras/ui/static/umf-explorer/chunk-dyhe34jv.js +1 -0
- arras/ui/static/umf-explorer/chunk-fff90kq1.js +1 -0
- arras/ui/static/umf-explorer/chunk-ffy3x09s.js +1 -0
- arras/ui/static/umf-explorer/chunk-h838d7m9.js +1 -0
- arras/ui/static/umf-explorer/chunk-hjqm6009.js +2 -0
- arras/ui/static/umf-explorer/chunk-j1761thh.js +1 -0
- arras/ui/static/umf-explorer/chunk-j6hnavwm.js +1 -0
- arras/ui/static/umf-explorer/chunk-k09yed2v.js +1 -0
- arras/ui/static/umf-explorer/chunk-k2v9vyvz.js +1 -0
- arras/ui/static/umf-explorer/chunk-k9nw71nj.js +1 -0
- arras/ui/static/umf-explorer/chunk-kkbcw88c.js +21 -0
- arras/ui/static/umf-explorer/chunk-kxxpzsd0.js +38 -0
- arras/ui/static/umf-explorer/chunk-mf0yn2d9.js +1 -0
- arras/ui/static/umf-explorer/chunk-mjj9769n.js +64 -0
- arras/ui/static/umf-explorer/chunk-mt4ev39n.js +1 -0
- arras/ui/static/umf-explorer/chunk-my1cq8f9.js +138 -0
- arras/ui/static/umf-explorer/chunk-qqr3ahja.js +2 -0
- arras/ui/static/umf-explorer/chunk-ww4nmdsp.js +193 -0
- arras/ui/static/umf-explorer/chunk-xyn4vvqt.js +73 -0
- arras/ui/static/umf-explorer/chunk-y33s5b58.js +2 -0
- arras/ui/static/umf-explorer/chunk-yc072vtb.js +29 -0
- arras/ui/static/umf-explorer/chunk-yg38jvvb.js +0 -0
- arras/ui/static/umf-explorer/chunk-yp36ghgq.js +1 -0
- arras/ui/static/umf-explorer/chunk-yybmn8nw.js +1 -0
- arras/ui/static/umf-explorer/chunk-z7fkf19q.js +2 -0
- arras/ui/static/umf-explorer/chunk-zc4shbzz.js +17 -0
- arras/ui/static/umf-explorer/chunk-zsn9vjj1.js +1 -0
- arras/ui/static/umf-explorer/chunk-zvzn5tbp.js +1 -0
- arras/ui/static/umf-explorer/components.css +21 -0
- arras/ui/static/umf-explorer/explorer.css +153 -0
- arras/ui/static/umf-explorer/explorer.js +32 -0
- arras/ui/static/umf-explorer/index.html +4 -0
- arras/ui/static/umf-explorer/schema-catalog.json +1 -0
- arras/ui/static/umf-explorer/style.css +7 -0
- arras/umf.py +404 -0
- arras/umf_admission.py +277 -0
- arras/versions.py +16 -0
- arras/view.py +892 -0
- arras/viewcache.py +50 -0
- arras-0.6.3.dist-info/METADATA +358 -0
- arras-0.6.3.dist-info/RECORD +173 -0
- arras-0.6.3.dist-info/WHEEL +4 -0
- arras-0.6.3.dist-info/licenses/LICENSE-APACHE +201 -0
- arras-0.6.3.dist-info/licenses/LICENSE-MIT +21 -0
- 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)
|