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.
- katalyst_core/__init__.py +178 -0
- katalyst_core/cache.py +153 -0
- katalyst_core/compiled.py +281 -0
- katalyst_core/contract.py +150 -0
- katalyst_core/graph.py +336 -0
- katalyst_core/local.py +107 -0
- katalyst_core/mappings.py +115 -0
- katalyst_core/migrate.py +84 -0
- katalyst_core/py.typed +0 -0
- katalyst_core/query.py +252 -0
- katalyst_core/schema/katalyst_core.yaml +219 -0
- katalyst_core/schema/released/0.1.0/SHA256SUMS +1 -0
- katalyst_core/schema/released/0.1.0/katalyst_core.yaml +219 -0
- katalyst_core/shapes.py +351 -0
- katalyst_core-0.1.1a1.dist-info/METADATA +45 -0
- katalyst_core-0.1.1a1.dist-info/RECORD +17 -0
- katalyst_core-0.1.1a1.dist-info/WHEEL +4 -0
|
@@ -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
|
+
)
|