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.
@@ -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