katalyst-core 0.1.1a1__tar.gz
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-0.1.1a1/.gitignore +123 -0
- katalyst_core-0.1.1a1/PKG-INFO +45 -0
- katalyst_core-0.1.1a1/README.md +28 -0
- katalyst_core-0.1.1a1/pyproject.toml +37 -0
- katalyst_core-0.1.1a1/src/katalyst_core/__init__.py +178 -0
- katalyst_core-0.1.1a1/src/katalyst_core/cache.py +153 -0
- katalyst_core-0.1.1a1/src/katalyst_core/compiled.py +281 -0
- katalyst_core-0.1.1a1/src/katalyst_core/contract.py +150 -0
- katalyst_core-0.1.1a1/src/katalyst_core/graph.py +336 -0
- katalyst_core-0.1.1a1/src/katalyst_core/local.py +107 -0
- katalyst_core-0.1.1a1/src/katalyst_core/mappings.py +115 -0
- katalyst_core-0.1.1a1/src/katalyst_core/migrate.py +84 -0
- katalyst_core-0.1.1a1/src/katalyst_core/py.typed +0 -0
- katalyst_core-0.1.1a1/src/katalyst_core/query.py +252 -0
- katalyst_core-0.1.1a1/src/katalyst_core/schema/katalyst_core.yaml +219 -0
- katalyst_core-0.1.1a1/src/katalyst_core/schema/released/0.1.0/SHA256SUMS +1 -0
- katalyst_core-0.1.1a1/src/katalyst_core/schema/released/0.1.0/katalyst_core.yaml +219 -0
- katalyst_core-0.1.1a1/src/katalyst_core/shapes.py +351 -0
- katalyst_core-0.1.1a1/tests/test_compiled.py +157 -0
- katalyst_core-0.1.1a1/tests/test_core.py +358 -0
- katalyst_core-0.1.1a1/tests/test_variation.py +154 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
# Not a Python build directory: Bard's SPA keeps shared helpers (utils.ts, download.ts) in
|
|
19
|
+
# src/lib, and ignoring it left a fresh clone unable to build or test the SPA.
|
|
20
|
+
!plugins/bard/web/src/lib/
|
|
21
|
+
lib64/
|
|
22
|
+
parts/
|
|
23
|
+
sdist/
|
|
24
|
+
var/
|
|
25
|
+
wheels/
|
|
26
|
+
*.egg-info/
|
|
27
|
+
.installed.cfg
|
|
28
|
+
*.egg
|
|
29
|
+
|
|
30
|
+
# PyInstaller
|
|
31
|
+
*.manifest
|
|
32
|
+
*.spec
|
|
33
|
+
|
|
34
|
+
# Installer logs
|
|
35
|
+
pip-log.txt
|
|
36
|
+
pip-delete-this-directory.txt
|
|
37
|
+
|
|
38
|
+
# Unit test / coverage reports
|
|
39
|
+
htmlcov/
|
|
40
|
+
.tox/
|
|
41
|
+
.nox/
|
|
42
|
+
.coverage
|
|
43
|
+
.coverage.*
|
|
44
|
+
.cache
|
|
45
|
+
nosetests.xml
|
|
46
|
+
coverage.xml
|
|
47
|
+
*.cover
|
|
48
|
+
*.py,cover
|
|
49
|
+
.hypothesis/
|
|
50
|
+
.pytest_cache/
|
|
51
|
+
|
|
52
|
+
# Translations
|
|
53
|
+
*.mo
|
|
54
|
+
*.pot
|
|
55
|
+
|
|
56
|
+
# Environments
|
|
57
|
+
.env
|
|
58
|
+
.venv
|
|
59
|
+
env/
|
|
60
|
+
venv/
|
|
61
|
+
ENV/
|
|
62
|
+
env.bak/
|
|
63
|
+
venv.bak/
|
|
64
|
+
|
|
65
|
+
# Spyder project settings
|
|
66
|
+
.spyderproject
|
|
67
|
+
.spyproject
|
|
68
|
+
|
|
69
|
+
# Rope project settings
|
|
70
|
+
.ropeproject
|
|
71
|
+
|
|
72
|
+
# mkdocs documentation
|
|
73
|
+
/site
|
|
74
|
+
|
|
75
|
+
# mypy / pyright
|
|
76
|
+
.mypy_cache/
|
|
77
|
+
.dmypy.json
|
|
78
|
+
dmypy.json
|
|
79
|
+
.pyright/
|
|
80
|
+
|
|
81
|
+
# Ruff
|
|
82
|
+
.ruff_cache/
|
|
83
|
+
|
|
84
|
+
# IDE
|
|
85
|
+
.idea/
|
|
86
|
+
.vscode/
|
|
87
|
+
*.swp
|
|
88
|
+
*.swo
|
|
89
|
+
*~
|
|
90
|
+
|
|
91
|
+
# OS
|
|
92
|
+
.DS_Store
|
|
93
|
+
Thumbs.db
|
|
94
|
+
|
|
95
|
+
# Local configuration
|
|
96
|
+
*.local
|
|
97
|
+
|
|
98
|
+
# Browser/Playwright state files
|
|
99
|
+
browser-state-*.json
|
|
100
|
+
|
|
101
|
+
# Agent harness scratch (run exhaust, never the design bundle)
|
|
102
|
+
.opencode/sessions/
|
|
103
|
+
.opencode/analysis/
|
|
104
|
+
.opencode/package-lock.json
|
|
105
|
+
|
|
106
|
+
# Build artifacts
|
|
107
|
+
site/
|
|
108
|
+
stack-tests/cucumber-report/
|
|
109
|
+
stack-tests/test-results/
|
|
110
|
+
|
|
111
|
+
# Generated artifacts (rebuild with `just generate-schemas`)
|
|
112
|
+
taxonomy/src/katalyst_taxonomy/engine/generated/
|
|
113
|
+
.worktrees/
|
|
114
|
+
.plan-74-75-76-77.md
|
|
115
|
+
.HANDOFF-scaffolding-wiring.md
|
|
116
|
+
.HANDOFF-2-cicd-migration.md
|
|
117
|
+
.HANDOFF-action-execution-fixes.md
|
|
118
|
+
.HANDOFF-bdd-coverage.md
|
|
119
|
+
.HANDOFF-*.md
|
|
120
|
+
docs/superpowers/
|
|
121
|
+
|
|
122
|
+
# Node dependencies — installed, never committed
|
|
123
|
+
node_modules/
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: katalyst-core
|
|
3
|
+
Version: 0.1.1a1
|
|
4
|
+
Summary: katalyst-core — the LinkML module every Katalyst schema module imports
|
|
5
|
+
Author: eSimplicity
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Requires-Dist: linkml-map>=0.5.4
|
|
9
|
+
Requires-Dist: linkml-runtime<2,>=1.12
|
|
10
|
+
Requires-Dist: linkml<2,>=1.12
|
|
11
|
+
Requires-Dist: pint<0.26,>=0.25
|
|
12
|
+
Requires-Dist: pyoxigraph>=0.5.12
|
|
13
|
+
Requires-Dist: pyshacl>=0.30
|
|
14
|
+
Requires-Dist: pyyaml>=6
|
|
15
|
+
Requires-Dist: rdflib>=7
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# katalyst-core
|
|
19
|
+
|
|
20
|
+
The LinkML module every Katalyst schema module imports
|
|
21
|
+
(`src/katalyst_core/schema/katalyst_core.yaml`):
|
|
22
|
+
|
|
23
|
+
- `Node`: the base node. Identity (`id`, a URI under a declared prefix; core itself is `https://w3id.org/katalyst/core/`, prefix `kc:`), `kind`, `name`, `description`.
|
|
24
|
+
- Lifecycle: `lifecycle_state` with a `retired` state, which is the default delete.
|
|
25
|
+
- Provenance: `source`, `activity`, `derived_from`.
|
|
26
|
+
- Two times: valid time (`valid_from`/`valid_to`) and recorded time (`recorded_from`/`recorded_to`).
|
|
27
|
+
- `Relation`: a reified relationship (`subject`, `predicate`, `object`) with a `confidence`
|
|
28
|
+
and its own provenance and times.
|
|
29
|
+
- The extension bag (`extensions`): data no loaded module defines, kept and never deleted.
|
|
30
|
+
- `DeletePolicy` (block, cascade, detach, redirect): every relationship slot carries one
|
|
31
|
+
as its `delete_policy` annotation.
|
|
32
|
+
|
|
33
|
+
A module imports it as `katalyst_core`:
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
imports:
|
|
37
|
+
- linkml:types
|
|
38
|
+
- katalyst_core
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The Python package defines no schema. It opens modules (`open_module`), holds them to the
|
|
42
|
+
core contract (`contract.check_contract`, `kata core check`), and moves records between
|
|
43
|
+
YAML and RDF (`graph.to_graph` / `graph.from_graph`, `kata core save` / `kata core load`).
|
|
44
|
+
|
|
45
|
+
Lint the schema with the LinkML linter: `just core-lint`. The full `linkml` toolchain is a runtime dependency, so validation and linting are available wherever core is.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# katalyst-core
|
|
2
|
+
|
|
3
|
+
The LinkML module every Katalyst schema module imports
|
|
4
|
+
(`src/katalyst_core/schema/katalyst_core.yaml`):
|
|
5
|
+
|
|
6
|
+
- `Node`: the base node. Identity (`id`, a URI under a declared prefix; core itself is `https://w3id.org/katalyst/core/`, prefix `kc:`), `kind`, `name`, `description`.
|
|
7
|
+
- Lifecycle: `lifecycle_state` with a `retired` state, which is the default delete.
|
|
8
|
+
- Provenance: `source`, `activity`, `derived_from`.
|
|
9
|
+
- Two times: valid time (`valid_from`/`valid_to`) and recorded time (`recorded_from`/`recorded_to`).
|
|
10
|
+
- `Relation`: a reified relationship (`subject`, `predicate`, `object`) with a `confidence`
|
|
11
|
+
and its own provenance and times.
|
|
12
|
+
- The extension bag (`extensions`): data no loaded module defines, kept and never deleted.
|
|
13
|
+
- `DeletePolicy` (block, cascade, detach, redirect): every relationship slot carries one
|
|
14
|
+
as its `delete_policy` annotation.
|
|
15
|
+
|
|
16
|
+
A module imports it as `katalyst_core`:
|
|
17
|
+
|
|
18
|
+
```yaml
|
|
19
|
+
imports:
|
|
20
|
+
- linkml:types
|
|
21
|
+
- katalyst_core
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The Python package defines no schema. It opens modules (`open_module`), holds them to the
|
|
25
|
+
core contract (`contract.check_contract`, `kata core check`), and moves records between
|
|
26
|
+
YAML and RDF (`graph.to_graph` / `graph.from_graph`, `kata core save` / `kata core load`).
|
|
27
|
+
|
|
28
|
+
Lint the schema with the LinkML linter: `just core-lint`. The full `linkml` toolchain is a runtime dependency, so validation and linting are available wherever core is.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "katalyst-core"
|
|
3
|
+
version = "0.1.1a1"
|
|
4
|
+
description = "katalyst-core — the LinkML module every Katalyst schema module imports"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "Proprietary" }
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "eSimplicity" }
|
|
10
|
+
]
|
|
11
|
+
dependencies = [
|
|
12
|
+
# The full toolchain at runtime: validation for CRUD and the linter. Licences of
|
|
13
|
+
# linkml and its dependencies (CC0, GPLv3+ rfc3987, W3C) are tracked by the owner.
|
|
14
|
+
"linkml>=1.12,<2",
|
|
15
|
+
"linkml-runtime>=1.12,<2",
|
|
16
|
+
# One validator for every check: SHACL shapes generated from the modules.
|
|
17
|
+
"pyshacl>=0.30",
|
|
18
|
+
"rdflib>=7",
|
|
19
|
+
"pyyaml>=6",
|
|
20
|
+
# Migrations between schema versions (LinkML-Map, Apache 2.0).
|
|
21
|
+
"linkml-map>=0.5.4",
|
|
22
|
+
# linkml-map's unit support (ucumvert 0.3.2) fails to load its UCUM definitions under
|
|
23
|
+
# pint 0.26 ("the alias i.U. is not a valid unit alias"); 0.25 is the newest that works.
|
|
24
|
+
"pint>=0.25,<0.26",
|
|
25
|
+
# The derived graph index below the query interface (MIT or Apache 2.0).
|
|
26
|
+
"pyoxigraph>=0.5.12",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["hatchling"]
|
|
31
|
+
build-backend = "hatchling.build"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/katalyst_core"]
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel.sources]
|
|
37
|
+
"src" = ""
|
|
@@ -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
|
|
@@ -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
|