katalyst-core 0.1.1a1__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.
@@ -0,0 +1,178 @@
1
+ """katalyst-core — the LinkML module every Katalyst schema module imports.
2
+
3
+ The schema itself is ``schema/katalyst_core.yaml``; nothing here defines a class or a
4
+ slot. This package locates the module, opens LinkML modules that import it, holds them
5
+ to the core contract (:mod:`katalyst_core.contract`) and moves records between their
6
+ YAML form and their graph form (:mod:`katalyst_core.graph`).
7
+ """
8
+
9
+ # linkml_runtime ships no type information.
10
+ # pyright: reportMissingTypeStubs=false, reportUnknownMemberType=false
11
+ # pyright: reportUnknownVariableType=false, reportUnknownArgumentType=false
12
+
13
+ from __future__ import annotations
14
+
15
+ from collections.abc import Iterable, Mapping
16
+ from functools import cache
17
+ from importlib.metadata import entry_points
18
+ from pathlib import Path
19
+ from typing import TYPE_CHECKING, Any, cast
20
+
21
+ import yaml
22
+
23
+ if TYPE_CHECKING:
24
+ # LinkML is imported where a view is opened, not with the package: a command that
25
+ # only reads definitions (from the schema cache) never pays for it.
26
+ from linkml_runtime.utils.schemaview import SchemaView
27
+
28
+ __all__ = [
29
+ "CORE_IMPORT",
30
+ "MODULES_GROUP",
31
+ "SCHEMA_PATH",
32
+ "enum_values",
33
+ "installed_modules",
34
+ "kinds",
35
+ "local_modules",
36
+ "namespaces",
37
+ "open_installed",
38
+ "open_module",
39
+ "register_modules",
40
+ ]
41
+
42
+ SCHEMA_PATH = Path(__file__).parent / "schema" / "katalyst_core.yaml"
43
+ """The katalyst-core LinkML module."""
44
+
45
+ CORE_IMPORT = "katalyst_core"
46
+ """The name a module lists under ``imports:`` to import katalyst-core."""
47
+
48
+
49
+ MODULES_GROUP = "katalyst_core.modules"
50
+ """Entry-point group a package registers its LinkML modules under.
51
+
52
+ Each entry point loads to a mapping of module name (what an ``imports:`` line says) to
53
+ the module's YAML file. A plugin ships its schema this way; core knows no plugin.
54
+ """
55
+
56
+
57
+ _REGISTERED: dict[str, Path] = {}
58
+
59
+
60
+ def register_modules(modules: Mapping[str, Path]) -> None:
61
+ """Add modules for this process: a LinkML schema bundle installed from a source."""
62
+ _REGISTERED.update({str(k): Path(v) for k, v in modules.items()})
63
+
64
+
65
+ def installed_modules() -> dict[str, Path]:
66
+ """Every installed LinkML module by name, katalyst-core first.
67
+
68
+ Packages' modules (``katalyst_core.modules``), then any a schema bundle registered.
69
+ """
70
+ modules: dict[str, Path] = {CORE_IMPORT: SCHEMA_PATH, **_packaged_modules()}
71
+ modules.update(_REGISTERED)
72
+ return modules
73
+
74
+
75
+ @cache
76
+ def _packaged_modules() -> dict[str, Path]:
77
+ """The modules installed packages register (the entry points do not change in a process)."""
78
+ modules: dict[str, Path] = {}
79
+ for ep in sorted(entry_points(group=MODULES_GROUP), key=lambda e: e.name):
80
+ provided: Mapping[str, Any] = ep.load()
81
+ for name, path in provided.items():
82
+ modules[str(name)] = Path(path)
83
+ return modules
84
+
85
+
86
+ def _importmap(modules: Mapping[str, Path]) -> dict[str, Any]:
87
+ return {name: str(path.with_suffix("")) for name, path in modules.items()}
88
+
89
+
90
+ def local_modules(paths: Iterable[Path]) -> dict[str, Path]:
91
+ """Local module files (not installed) by name: the ``name`` each one declares.
92
+
93
+ Each is held to the rules for local modules first (:mod:`katalyst_core.local`): it
94
+ extends the installed baseline only by subtyping, against the version it pins.
95
+
96
+ Raises:
97
+ katalyst_core.local.LocalModuleError: A module breaks those rules.
98
+ """
99
+ from katalyst_core.local import check_local_module
100
+
101
+ found: dict[str, Path] = {}
102
+ baseline = installed_modules()
103
+ for path in paths:
104
+ raw: Any = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
105
+ name = str(raw.get("name") or path.stem) if isinstance(raw, dict) else path.stem
106
+ check_local_module(path, baseline)
107
+ found[name] = path.resolve()
108
+ return found
109
+
110
+
111
+ def open_module(path: Path | None = None, extra: Mapping[str, Path] | None = None) -> SchemaView:
112
+ """Open a LinkML module (katalyst-core itself by default).
113
+
114
+ It may import katalyst-core (``katalyst_core``), any installed module and any module
115
+ in ``extra`` by name, wherever they live.
116
+ """
117
+ from linkml_runtime.utils.schemaview import SchemaView
118
+
119
+ importmap = _importmap({**installed_modules(), **(extra or {})})
120
+ return SchemaView(str(path or SCHEMA_PATH), importmap=importmap)
121
+
122
+
123
+ def open_installed(extra: Mapping[str, Path] | None = None) -> SchemaView:
124
+ """Every installed module at once, and ``extra`` local ones: a view importing all."""
125
+ from linkml_runtime.linkml_model.meta import SchemaDefinition
126
+ from linkml_runtime.utils.schemaview import SchemaView
127
+
128
+ modules = {**installed_modules(), **(extra or {})}
129
+ root = SchemaDefinition(
130
+ id="https://w3id.org/katalyst/installed",
131
+ name="installed",
132
+ imports=["linkml:types", *modules],
133
+ prefixes=cast(Any, {"linkml": "https://w3id.org/linkml/"}),
134
+ default_prefix="linkml",
135
+ )
136
+ return SchemaView(root, importmap=_importmap(modules))
137
+
138
+
139
+ def namespaces(sv: SchemaView) -> dict[str, str]:
140
+ """Every prefix declared by the module open in ``sv`` or by any module it imports.
141
+
142
+ ``SchemaView.namespaces()`` gives only the root module's. A compiled schema
143
+ (:mod:`katalyst_core.compiled`) holds them already.
144
+ """
145
+ compiled = getattr(sv, "compiled_namespaces", None)
146
+ if compiled is not None:
147
+ return dict(compiled())
148
+ merged: dict[str, str] = {}
149
+ for name in reversed(sv.imports_closure()):
150
+ schema: Any = (sv.schema_map or {}).get(name)
151
+ prefixes: dict[str, Any] = (schema.prefixes or {}) if schema else {}
152
+ for prefix in prefixes.values():
153
+ merged[str(prefix.prefix_prefix)] = str(prefix.prefix_reference)
154
+ merged.update({str(k): str(v) for k, v in sv.namespaces().items()})
155
+ return merged
156
+
157
+
158
+ def enum_values(sv: SchemaView, name: str) -> list[str]:
159
+ """The permissible values of enum ``name``; empty when no loaded module defines it."""
160
+ enum: Any = sv.get_enum(name)
161
+ values: dict[str, Any] = (enum.permissible_values or {}) if enum else {}
162
+ return [str(v) for v in values]
163
+
164
+
165
+ def kinds(sv: SchemaView) -> dict[str, str]:
166
+ """Node kind to class, for every class that declares a ``kind`` annotation."""
167
+ found: dict[str, str] = {}
168
+ for name in sv.all_classes():
169
+ cls: Any = sv.get_class(name)
170
+ annotations: Any = cls.annotations or {}
171
+ kind: Any = (
172
+ annotations.get("kind")
173
+ if isinstance(annotations, dict)
174
+ else getattr(annotations, "kind", None)
175
+ )
176
+ if kind is not None:
177
+ found[str(getattr(kind, "value", kind))] = str(name)
178
+ return found
katalyst_core/cache.py ADDED
@@ -0,0 +1,153 @@
1
+ """What is built from the installed LinkML modules, kept on disk between processes.
2
+
3
+ Opening the modules (LinkML's ``SchemaView``) and building from them takes about a second,
4
+ and every command needs what they define. So what is built from them is kept as JSON in
5
+ the active home's cache, keyed by a digest of everything it was built from:
6
+
7
+ - the contents of every installed module (and every module a schema bundle registered),
8
+ - the source of the code that built it (each builder names its own files), and
9
+ - the builder's own key parts (the modules asked for, a format number), and
10
+ - the versions of the libraries the build reads them with (LinkML, rdflib, katalyst-core).
11
+
12
+ Any change to one of them gives a new key, so a stale entry is never read; writing the
13
+ new entry removes the old ones of the same name and parts. An entry is written to a temporary file and
14
+ renamed into place, so a reader never sees half of one; an entry that cannot be read is
15
+ built again and replaced. Without a cache directory (no home, or one that cannot be
16
+ written) everything is built in the process, as before.
17
+
18
+ The runtime that owns the home says where the cache lives
19
+ (:func:`set_cache_dir_provider`); katalyst-core knows no home.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import hashlib
25
+ import json
26
+ import os
27
+ import tempfile
28
+ from collections.abc import Callable, Iterable
29
+ from functools import cache
30
+ from pathlib import Path
31
+ from typing import Any, TypeVar
32
+
33
+ __all__ = ["cache_dir", "cached_json", "modules_digest", "set_cache_dir_provider"]
34
+
35
+ _provider: Callable[[], Path | None] | None = None
36
+
37
+ T = TypeVar("T")
38
+
39
+
40
+ def _same(data: Any) -> Any:
41
+ return data
42
+
43
+
44
+ def set_cache_dir_provider(provider: Callable[[], Path | None] | None) -> None:
45
+ """Say where the schema cache lives: a callable giving the directory, or ``None``."""
46
+ global _provider
47
+ _provider = provider
48
+
49
+
50
+ def cache_dir() -> Path | None:
51
+ """The schema cache directory, or ``None`` when there is none to use."""
52
+ if _provider is None:
53
+ return None
54
+ try:
55
+ return _provider()
56
+ except Exception: # no home resolves: build in the process instead
57
+ return None
58
+
59
+
60
+ def modules_digest() -> str:
61
+ """A digest of every installed module's name and contents."""
62
+ from katalyst_core import installed_modules
63
+
64
+ modules = sorted(installed_modules().items())
65
+ stamp = tuple((name, str(path), *_stat(path)) for name, path in modules)
66
+ return _digest(stamp)
67
+
68
+
69
+ def _stat(path: Path) -> tuple[int, int]:
70
+ info = path.stat()
71
+ return info.st_mtime_ns, info.st_size
72
+
73
+
74
+ @cache
75
+ def _digest(stamp: tuple[tuple[Any, ...], ...]) -> str:
76
+ """The contents' digest, read once per process for the same files (path, mtime, size)."""
77
+ digest = hashlib.sha256()
78
+ for name, path, *_ in stamp:
79
+ digest.update(str(name).encode())
80
+ digest.update(b"\0")
81
+ digest.update(Path(path).read_bytes())
82
+ digest.update(b"\0")
83
+ return digest.hexdigest()
84
+
85
+
86
+ #: Libraries whose version changes what is built from the same modules.
87
+ _LIBRARIES = ("linkml-runtime", "linkml", "rdflib", "katalyst-core")
88
+
89
+
90
+ @cache
91
+ def _library_versions() -> str:
92
+ from importlib.metadata import PackageNotFoundError, version
93
+
94
+ found: list[str] = []
95
+ for name in _LIBRARIES:
96
+ try:
97
+ found.append(f"{name}={version(name)}")
98
+ except PackageNotFoundError:
99
+ found.append(f"{name}=")
100
+ return ";".join(found)
101
+
102
+
103
+ def _key(kind: str, sources: Iterable[Path], parts: Iterable[str]) -> str:
104
+ digest = hashlib.sha256(kind.encode())
105
+ digest.update(_library_versions().encode())
106
+ digest.update(modules_digest().encode())
107
+ for source in sources:
108
+ digest.update(Path(source).read_bytes())
109
+ for part in parts:
110
+ digest.update(b"\0" + part.encode())
111
+ return digest.hexdigest()[:32]
112
+
113
+
114
+ def cached_json(
115
+ kind: str,
116
+ build: Callable[[], Any],
117
+ *,
118
+ load: Callable[[Any], T] = _same,
119
+ sources: Iterable[Path] = (),
120
+ parts: Iterable[str] = (),
121
+ directory: Path | None = None,
122
+ ) -> T:
123
+ """``load(build())``, with what *build* returns (JSON data) kept in the cache.
124
+
125
+ *kind* names the entry (``definitions``, ``compiled``); *sources* are the files of
126
+ the code that builds it, *parts* any further key strings. An entry that cannot be
127
+ read or that *load* refuses is built again and replaced. *directory* overrides
128
+ :func:`cache_dir`.
129
+ """
130
+ where = directory if directory is not None else cache_dir()
131
+ if where is None:
132
+ return load(build())
133
+ parts = tuple(parts)
134
+ family = f"{kind}-{hashlib.sha256(chr(0).join(parts).encode()).hexdigest()[:8]}"
135
+ path = where / f"{family}-{_key(kind, sources, parts)}.json"
136
+ try:
137
+ return load(json.loads(path.read_text(encoding="utf-8")))
138
+ except Exception: # absent, unreadable or refused: build it again
139
+ pass
140
+ data = build()
141
+ result = load(data)
142
+ try:
143
+ where.mkdir(parents=True, exist_ok=True)
144
+ fd, partial = tempfile.mkstemp(dir=where, prefix=f".{kind}-", suffix=".tmp")
145
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
146
+ json.dump(data, handle, separators=(",", ":"))
147
+ os.replace(partial, path)
148
+ for stale in where.glob(f"{family}-*.json"):
149
+ if stale != path:
150
+ stale.unlink(missing_ok=True)
151
+ except OSError: # a cache that cannot be written is only slower
152
+ pass
153
+ return result
@@ -0,0 +1,281 @@
1
+ """The installed modules compiled for the write path: what a ``SchemaView`` answers there.
2
+
3
+ Putting a node through the local shapes needs, for each class, its URI, its induced
4
+ slots (range, cardinality, inlining, annotations), its ancestors, and the modules'
5
+ namespaces, enums and types. LinkML's ``SchemaView`` computes them, but opening one costs
6
+ more than the rest of a write. :func:`compile_view` asks a view once for everything the
7
+ write path reads, :class:`CompiledSchema` answers the same questions from that data, and
8
+ :func:`installed_compiled` keeps it in the schema cache (:mod:`katalyst_core.cache`),
9
+ keyed by the modules' contents and this file's source.
10
+
11
+ :class:`CompiledSchema` implements only the part of the ``SchemaView`` interface that
12
+ :mod:`katalyst_core.graph` (writing records to the graph), :mod:`katalyst_core.shapes`
13
+ (running shapes and wording their findings) and the taxonomy's write path use; anything
14
+ else needs a real view.
15
+ """
16
+
17
+ # linkml_runtime ships no type information.
18
+ # pyright: reportMissingTypeStubs=false, reportUnknownMemberType=false
19
+ # pyright: reportUnknownVariableType=false, reportUnknownArgumentType=false
20
+
21
+ from __future__ import annotations
22
+
23
+ from dataclasses import dataclass, field
24
+ from pathlib import Path
25
+ from typing import TYPE_CHECKING, Any, cast
26
+
27
+ if TYPE_CHECKING:
28
+ from linkml_runtime.utils.schemaview import SchemaView
29
+
30
+ __all__ = ["CompiledSchema", "compile_view", "installed_compiled"]
31
+
32
+ #: The JSON form below; bump on a change to it.
33
+ _FORMAT = "1"
34
+
35
+ #: Slots :mod:`katalyst_core.graph` looks up by name (the extension bag).
36
+ _NAMED_SLOTS = ("extensions", "key", "value")
37
+
38
+
39
+ def _plain(value: Any) -> Any:
40
+ value = getattr(value, "value", value)
41
+ if hasattr(value, "_as_dict"):
42
+ value = value._as_dict
43
+ if isinstance(value, dict):
44
+ raw = cast("dict[Any, Any]", value)
45
+ keys = {str(k) for k in raw}
46
+ if {"tag", "value"} <= keys <= {"tag", "value", "extensions", "annotations"}:
47
+ # An annotation, in the form an induced slot holds it.
48
+ return _plain(raw["value"])
49
+ return {str(k): _plain(v) for k, v in raw.items()}
50
+ if isinstance(value, (list, tuple)):
51
+ return [_plain(v) for v in cast("list[Any]", value)]
52
+ return value
53
+
54
+
55
+ def _annotations(element: Any) -> dict[str, Any]:
56
+ raw: Any = getattr(element, "annotations", None) or {}
57
+ if not isinstance(raw, dict):
58
+ raw = getattr(raw, "_as_dict", {}) or {}
59
+ return {str(k): _plain(v) for k, v in cast("dict[Any, Any]", raw).items()}
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class _Range:
64
+ range: str | None
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class CompiledSlot:
69
+ """A slot as a class induces it: what the write path reads of a ``SlotDefinition``."""
70
+
71
+ name: str
72
+ uri: str
73
+ range: str | None = None
74
+ multivalued: bool = False
75
+ required: bool = False
76
+ inlined: bool = False
77
+ inlined_as_list: bool = False
78
+ identifier: bool = False
79
+ designates_type: bool = False
80
+ any_of: tuple[_Range, ...] = ()
81
+ annotations: dict[str, Any] = field(default_factory=lambda: {})
82
+
83
+ def to_json(self) -> dict[str, Any]:
84
+ return {
85
+ "name": self.name,
86
+ "uri": self.uri,
87
+ "range": self.range,
88
+ "multivalued": self.multivalued,
89
+ "required": self.required,
90
+ "inlined": self.inlined,
91
+ "inlined_as_list": self.inlined_as_list,
92
+ "identifier": self.identifier,
93
+ "designates_type": self.designates_type,
94
+ "any_of": [r.range for r in self.any_of],
95
+ "annotations": self.annotations,
96
+ }
97
+
98
+ @staticmethod
99
+ def of(data: dict[str, Any]) -> CompiledSlot:
100
+ return CompiledSlot(
101
+ name=str(data["name"]),
102
+ uri=str(data["uri"]),
103
+ range=data["range"],
104
+ multivalued=bool(data["multivalued"]),
105
+ required=bool(data["required"]),
106
+ inlined=bool(data["inlined"]),
107
+ inlined_as_list=bool(data["inlined_as_list"]),
108
+ identifier=bool(data["identifier"]),
109
+ designates_type=bool(data["designates_type"]),
110
+ any_of=tuple(_Range(r) for r in data["any_of"]),
111
+ annotations=dict(data["annotations"]),
112
+ )
113
+
114
+
115
+ @dataclass(frozen=True)
116
+ class CompiledClass:
117
+ """A class: what the write path reads of a ``ClassDefinition``."""
118
+
119
+ name: str
120
+ uri: str
121
+ class_uri: str | None
122
+ annotations: dict[str, Any]
123
+ slots: tuple[CompiledSlot, ...]
124
+ identifier: str | None
125
+ ancestors: tuple[str, ...]
126
+ parents: tuple[str, ...]
127
+
128
+
129
+ @dataclass(frozen=True)
130
+ class _Enum:
131
+ permissible_values: dict[str, None]
132
+
133
+
134
+ @dataclass(frozen=True)
135
+ class _Type:
136
+ uri: str | None
137
+
138
+
139
+ @dataclass(frozen=True)
140
+ class _Schema:
141
+ default_range: str | None
142
+
143
+
144
+ class CompiledSchema:
145
+ """The installed modules as the write path reads them (a ``SchemaView`` subset)."""
146
+
147
+ def __init__(self, data: dict[str, Any]) -> None:
148
+ if data.get("format") != _FORMAT:
149
+ raise ValueError("not a compiled schema of this format")
150
+ self.schema = _Schema(default_range=data["default_range"])
151
+ self._namespaces: dict[str, str] = dict(data["namespaces"])
152
+ self._classes: dict[str, CompiledClass] = {}
153
+ for name, raw in data["classes"].items():
154
+ self._classes[name] = CompiledClass(
155
+ name=name,
156
+ uri=raw["uri"],
157
+ class_uri=raw["class_uri"],
158
+ annotations=raw["annotations"],
159
+ slots=tuple(CompiledSlot.of(s) for s in raw["slots"]),
160
+ identifier=raw["identifier"],
161
+ ancestors=tuple(raw["ancestors"]),
162
+ parents=tuple(raw["parents"]),
163
+ )
164
+ self._enums = {n: _Enum(dict.fromkeys(v)) for n, v in data["enums"].items()}
165
+ self._types = {n: _Type(u) for n, u in data["types"].items()}
166
+ self._named = {n: CompiledSlot.of(s) for n, s in data["named_slots"].items()}
167
+ self.kinds: dict[str, str] = dict(data["kinds"])
168
+
169
+ # --- the SchemaView subset ------------------------------------------------------
170
+ def all_classes(self) -> dict[str, CompiledClass]:
171
+ return self._classes
172
+
173
+ def get_class(self, name: str) -> CompiledClass | None:
174
+ return self._classes.get(name)
175
+
176
+ def class_induced_slots(self, name: str) -> list[CompiledSlot]:
177
+ return list(self._classes[name].slots)
178
+
179
+ def get_identifier_slot(self, name: str) -> CompiledSlot | None:
180
+ cls = self._classes.get(name)
181
+ if cls is None or cls.identifier is None:
182
+ return None
183
+ return next(s for s in cls.slots if s.name == cls.identifier)
184
+
185
+ def class_ancestors(self, name: str) -> list[str]:
186
+ return list(self._classes[name].ancestors)
187
+
188
+ def class_parents(self, name: str, mixins: bool = True) -> list[str]:
189
+ del mixins # compiled with mixins, as the write path asks
190
+ return list(self._classes[name].parents)
191
+
192
+ def get_uri(self, element: CompiledClass | CompiledSlot, expand: bool = True) -> str:
193
+ del expand # compiled expanded
194
+ return element.uri
195
+
196
+ def get_slot(self, name: str) -> CompiledSlot | None:
197
+ return self._named.get(name)
198
+
199
+ def all_enums(self) -> dict[str, _Enum]:
200
+ return self._enums
201
+
202
+ def get_enum(self, name: str) -> _Enum | None:
203
+ return self._enums.get(name)
204
+
205
+ def get_type(self, name: str) -> _Type | None:
206
+ return self._types.get(name)
207
+
208
+ def compiled_namespaces(self) -> dict[str, str]:
209
+ return dict(self._namespaces)
210
+
211
+
212
+ def compile_view(sv: SchemaView) -> dict[str, Any]:
213
+ """Everything the write path reads of *sv*, as JSON data."""
214
+ from katalyst_core import kinds, namespaces
215
+
216
+ def slot_json(slot: Any) -> dict[str, Any]:
217
+ any_of: list[Any] = list(slot.any_of or [])
218
+ return CompiledSlot(
219
+ name=str(slot.name),
220
+ uri=str(sv.get_uri(slot, expand=True)),
221
+ range=str(slot.range) if slot.range else None,
222
+ multivalued=bool(slot.multivalued),
223
+ required=bool(slot.required),
224
+ inlined=bool(slot.inlined),
225
+ inlined_as_list=bool(slot.inlined_as_list),
226
+ identifier=bool(slot.identifier),
227
+ designates_type=bool(slot.designates_type),
228
+ any_of=tuple(_Range(str(r.range) if r.range else None) for r in any_of),
229
+ annotations=_annotations(slot),
230
+ ).to_json()
231
+
232
+ classes: dict[str, Any] = {}
233
+ for name in sv.all_classes():
234
+ cls: Any = sv.get_class(name)
235
+ identifier = sv.get_identifier_slot(name)
236
+ classes[str(name)] = {
237
+ "uri": str(sv.get_uri(cls, expand=True)),
238
+ "class_uri": str(cls.class_uri) if cls.class_uri else None,
239
+ "annotations": _annotations(cls),
240
+ "slots": [slot_json(s) for s in sv.class_induced_slots(name)],
241
+ "identifier": str(identifier.name) if identifier is not None else None,
242
+ "ancestors": [str(a) for a in sv.class_ancestors(name)],
243
+ "parents": [str(p) for p in sv.class_parents(name, mixins=True)],
244
+ }
245
+ enums: dict[str, list[str]] = {}
246
+ for name in sv.all_enums():
247
+ enum: Any = sv.get_enum(name)
248
+ enums[str(name)] = [str(v) for v in (enum.permissible_values or {})]
249
+ types: dict[str, str | None] = {}
250
+ for name in sv.all_types():
251
+ type_def: Any = sv.get_type(name)
252
+ types[str(name)] = str(type_def.uri) if type_def is not None and type_def.uri else None
253
+ named: dict[str, Any] = {}
254
+ for name in _NAMED_SLOTS:
255
+ slot = sv.get_slot(name)
256
+ if slot is not None:
257
+ named[name] = slot_json(slot)
258
+ return {
259
+ "format": _FORMAT,
260
+ "default_range": getattr(sv.schema, "default_range", None),
261
+ "namespaces": namespaces(sv),
262
+ "classes": classes,
263
+ "enums": enums,
264
+ "types": types,
265
+ "named_slots": named,
266
+ "kinds": kinds(sv),
267
+ }
268
+
269
+
270
+ def installed_compiled() -> CompiledSchema:
271
+ """The installed modules compiled (:func:`compile_view`), from the schema cache."""
272
+ from katalyst_core import open_installed
273
+ from katalyst_core.cache import cached_json
274
+
275
+ return cached_json(
276
+ "compiled",
277
+ lambda: compile_view(open_installed()),
278
+ load=CompiledSchema,
279
+ sources=(Path(__file__),),
280
+ parts=(_FORMAT,),
281
+ )