generic-ml-cache-bootstrap 0.29.0__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.
- generic_ml_cache_bootstrap/__init__.py +26 -0
- generic_ml_cache_bootstrap/application.py +274 -0
- generic_ml_cache_bootstrap/diagnostics.py +38 -0
- generic_ml_cache_bootstrap/discovery/__init__.py +8 -0
- generic_ml_cache_bootstrap/discovery/_entrypoints.py +45 -0
- generic_ml_cache_bootstrap/discovery/api_discover.py +61 -0
- generic_ml_cache_bootstrap/discovery/client_discover.py +274 -0
- generic_ml_cache_bootstrap/discovery/composite_adapter_catalog.py +38 -0
- generic_ml_cache_bootstrap/discovery/composite_adapter_resolver.py +51 -0
- generic_ml_cache_bootstrap/discovery/composition.py +148 -0
- generic_ml_cache_bootstrap/discovery/entrypoint_adapter_catalog.py +159 -0
- generic_ml_cache_bootstrap/discovery/entrypoint_adapter_resolver.py +54 -0
- generic_ml_cache_bootstrap/discovery/in_memory_adapter_registry.py +97 -0
- generic_ml_cache_bootstrap/discovery/static_adapter_catalog.py +36 -0
- generic_ml_cache_bootstrap/encryption.py +82 -0
- generic_ml_cache_bootstrap/persistence_backend.py +129 -0
- generic_ml_cache_bootstrap/progress.py +27 -0
- generic_ml_cache_bootstrap/py.typed +0 -0
- generic_ml_cache_bootstrap/store_status.py +40 -0
- generic_ml_cache_bootstrap/stub_runners.py +34 -0
- generic_ml_cache_bootstrap-0.29.0.dist-info/METADATA +74 -0
- generic_ml_cache_bootstrap-0.29.0.dist-info/RECORD +25 -0
- generic_ml_cache_bootstrap-0.29.0.dist-info/WHEEL +4 -0
- generic_ml_cache_bootstrap-0.29.0.dist-info/licenses/LICENSE +201 -0
- generic_ml_cache_bootstrap-0.29.0.dist-info/licenses/NOTICE +8 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Daniel Slobozian
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""EntryPointAdapterResolver — construct a concrete adapter from its id.
|
|
4
|
+
|
|
5
|
+
The resolver's counterpart to EntryPointAdapterCatalog: where the catalog reads
|
|
6
|
+
``descriptor()`` to answer "what exists?", the resolver constructs the vetted class
|
|
7
|
+
to answer "give me the implementation." It resolves ONLY from the catalog's
|
|
8
|
+
already-gated classes (X15) — it never enumerates or ``load()``s entry points itself,
|
|
9
|
+
so resolving one adapter can no longer run the import-time code of every installed
|
|
10
|
+
plugin (a non-whitelisted plugin the catalog gated out is never imported). Local
|
|
11
|
+
clients are built with the per-run config; API runners take none.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import cast
|
|
17
|
+
|
|
18
|
+
from generic_ml_cache_core.application.port.outbound.adapter_resolver_port import (
|
|
19
|
+
AdapterResolverPort,
|
|
20
|
+
)
|
|
21
|
+
from generic_ml_cache_core.application.port.outbound.local_client_port import LocalClientPort
|
|
22
|
+
from generic_ml_cache_core.application.port.outbound.ml_runner_port import MlRunnerPort
|
|
23
|
+
from generic_ml_cache_core.common.errors import UnknownClient
|
|
24
|
+
|
|
25
|
+
from generic_ml_cache_bootstrap.discovery.entrypoint_adapter_catalog import EntryPointAdapterCatalog
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class EntryPointAdapterResolver(AdapterResolverPort):
|
|
29
|
+
"""Resolve an ``adapter_id`` to a constructed adapter from the catalog's vetted classes."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, catalog: EntryPointAdapterCatalog) -> None:
|
|
32
|
+
self._catalog = catalog
|
|
33
|
+
|
|
34
|
+
def _class_for(self, adapter_id: str) -> type:
|
|
35
|
+
cls = self._catalog.resolve_class(adapter_id)
|
|
36
|
+
if cls is None:
|
|
37
|
+
raise UnknownClient(f"no adapter for id {adapter_id!r}")
|
|
38
|
+
return cls
|
|
39
|
+
|
|
40
|
+
def resolve_local_client(
|
|
41
|
+
self,
|
|
42
|
+
adapter_id: str,
|
|
43
|
+
executable_override: str | None = None,
|
|
44
|
+
timeout: float | None = None,
|
|
45
|
+
stream_path: str | None = None,
|
|
46
|
+
) -> LocalClientPort:
|
|
47
|
+
cls = self._class_for(adapter_id)
|
|
48
|
+
return cast(
|
|
49
|
+
LocalClientPort,
|
|
50
|
+
cls(executable_override=executable_override, timeout=timeout, stream_path=stream_path),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def resolve_runner(self, adapter_id: str) -> MlRunnerPort:
|
|
54
|
+
return cast(MlRunnerPort, self._class_for(adapter_id)())
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Daniel Slobozian
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""InMemoryAdapterRegistry — programmatic adapter registration (tests, embedding).
|
|
4
|
+
|
|
5
|
+
The relocated, infrastructure-layer home of the old ``register()`` seam. Tests
|
|
6
|
+
and embedding apps register adapter *classes* here; a composition root then
|
|
7
|
+
exposes them as a StaticAdapterCatalog and resolves them. It carries no
|
|
8
|
+
packaging-discovery — it is just an in-memory map — so core never needs it.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Sequence
|
|
14
|
+
from typing import cast
|
|
15
|
+
|
|
16
|
+
from generic_ml_cache_core.application.domain.model.catalog.adapter_descriptor import (
|
|
17
|
+
AdapterDescriptor,
|
|
18
|
+
)
|
|
19
|
+
from generic_ml_cache_core.application.domain.model.execution.execution_kind import ExecutionKind
|
|
20
|
+
from generic_ml_cache_core.application.port.outbound.adapter_catalog_port import AdapterCatalogPort
|
|
21
|
+
from generic_ml_cache_core.application.port.outbound.adapter_resolver_port import (
|
|
22
|
+
AdapterResolverPort,
|
|
23
|
+
)
|
|
24
|
+
from generic_ml_cache_core.application.port.outbound.local_client_port import LocalClientPort
|
|
25
|
+
from generic_ml_cache_core.application.port.outbound.ml_runner_port import MlRunnerPort
|
|
26
|
+
from generic_ml_cache_core.common.errors import UnknownClient
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class InMemoryAdapterRegistry(AdapterCatalogPort, AdapterResolverPort):
|
|
30
|
+
"""A map of adapter classes registered in-process, keyed by ``adapter_id``.
|
|
31
|
+
|
|
32
|
+
It is a *live* catalog and resolver in one: composition merges it with the
|
|
33
|
+
entry-point catalog/resolver, and registrations made after that merge are
|
|
34
|
+
still seen (the merge holds this object, not a snapshot)."""
|
|
35
|
+
|
|
36
|
+
def __init__(self) -> None:
|
|
37
|
+
self._classes: dict[str, type] = {}
|
|
38
|
+
|
|
39
|
+
def register(self, cls: type) -> None:
|
|
40
|
+
"""Register an adapter class (must expose a ``descriptor()`` classmethod)."""
|
|
41
|
+
descriptor = cast(AdapterDescriptor, cls.descriptor()) # type: ignore[attr-defined]
|
|
42
|
+
self._classes[descriptor.adapter_id] = cls
|
|
43
|
+
|
|
44
|
+
def clear(self) -> None:
|
|
45
|
+
self._classes.clear()
|
|
46
|
+
|
|
47
|
+
# ------------------------------------------------------------------
|
|
48
|
+
# AdapterCatalogPort (live view of the registered classes)
|
|
49
|
+
# ------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
def list_adapters(self) -> Sequence[AdapterDescriptor]:
|
|
52
|
+
return [cast(AdapterDescriptor, cls.descriptor()) for cls in self._classes.values()] # type: ignore[attr-defined]
|
|
53
|
+
|
|
54
|
+
def find_by_client_name(self, client_name: str) -> Sequence[AdapterDescriptor]:
|
|
55
|
+
return [d for d in self.list_adapters() if d.client_name == client_name]
|
|
56
|
+
|
|
57
|
+
def supports(self, client_name: str, mode: ExecutionKind) -> bool:
|
|
58
|
+
return any(
|
|
59
|
+
d.client_name == client_name and d.supports_mode(mode) for d in self.list_adapters()
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def _class_for(self, adapter_id: str) -> type:
|
|
63
|
+
cls = self._classes.get(adapter_id)
|
|
64
|
+
if cls is None:
|
|
65
|
+
raise UnknownClient(f"no adapter for id {adapter_id!r}")
|
|
66
|
+
return cls
|
|
67
|
+
|
|
68
|
+
def resolve_local_client(
|
|
69
|
+
self,
|
|
70
|
+
adapter_id: str,
|
|
71
|
+
executable_override: str | None = None,
|
|
72
|
+
timeout: float | None = None,
|
|
73
|
+
stream_path: str | None = None,
|
|
74
|
+
) -> LocalClientPort:
|
|
75
|
+
cls = self._class_for(adapter_id)
|
|
76
|
+
return cast(
|
|
77
|
+
LocalClientPort,
|
|
78
|
+
cls(executable_override=executable_override, timeout=timeout, stream_path=stream_path),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def resolve_runner(self, adapter_id: str) -> MlRunnerPort:
|
|
82
|
+
return cast(MlRunnerPort, self._class_for(adapter_id)())
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
#: Process-wide default registry — the relocation of the old module-level
|
|
86
|
+
#: ``register()`` global. Composition roots merge it with the entry-point catalog.
|
|
87
|
+
_DEFAULT_REGISTRY = InMemoryAdapterRegistry()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def register(cls: type) -> None:
|
|
91
|
+
"""Register an adapter class into the process-wide default registry."""
|
|
92
|
+
_DEFAULT_REGISTRY.register(cls)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def default_registry() -> InMemoryAdapterRegistry:
|
|
96
|
+
"""Return the process-wide default in-memory registry."""
|
|
97
|
+
return _DEFAULT_REGISTRY
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Daniel Slobozian
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""StaticAdapterCatalog — a catalog over a fixed list of descriptors.
|
|
4
|
+
|
|
5
|
+
Useful for tests, minimal deployments, and as the in-process half of a composite
|
|
6
|
+
(e.g. test fakes registered alongside entry-point adapters). It performs no
|
|
7
|
+
discovery; it simply answers questions over the descriptors it was given.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Iterable, Sequence
|
|
13
|
+
|
|
14
|
+
from generic_ml_cache_core.application.domain.model.catalog.adapter_descriptor import (
|
|
15
|
+
AdapterDescriptor,
|
|
16
|
+
)
|
|
17
|
+
from generic_ml_cache_core.application.domain.model.execution.execution_kind import ExecutionKind
|
|
18
|
+
from generic_ml_cache_core.application.port.outbound.adapter_catalog_port import AdapterCatalogPort
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class StaticAdapterCatalog(AdapterCatalogPort):
|
|
22
|
+
"""An AdapterCatalogPort backed by a predefined list of descriptors."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, descriptors: Iterable[AdapterDescriptor] = ()) -> None:
|
|
25
|
+
self._descriptors: list[AdapterDescriptor] = list(descriptors)
|
|
26
|
+
|
|
27
|
+
def list_adapters(self) -> Sequence[AdapterDescriptor]:
|
|
28
|
+
return list(self._descriptors)
|
|
29
|
+
|
|
30
|
+
def find_by_client_name(self, client_name: str) -> Sequence[AdapterDescriptor]:
|
|
31
|
+
return [d for d in self._descriptors if d.client_name == client_name]
|
|
32
|
+
|
|
33
|
+
def supports(self, client_name: str, mode: ExecutionKind) -> bool:
|
|
34
|
+
return any(
|
|
35
|
+
d.client_name == client_name and d.supports_mode(mode) for d in self._descriptors
|
|
36
|
+
)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Daniel Slobozian
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Driver-facing facade for the store-encryption commands (W28).
|
|
4
|
+
|
|
5
|
+
The CLI's ``gmlcache encrypt / decrypt / rotate / invalidate / status`` commands
|
|
6
|
+
are driver *feature* logic, but running them means constructing concrete crypto
|
|
7
|
+
adapters (the cipher, the manifest store, the store lock, the store encryptor) —
|
|
8
|
+
the ``cli._compose -> adapters.…crypto/persistence`` edges W28 removes. This facade
|
|
9
|
+
owns that wiring in the composition root; the CLI controller keeps only its own
|
|
10
|
+
user-facing concerns (prompts, exit codes, the pip-install hint).
|
|
11
|
+
|
|
12
|
+
The operations raise the same core errors they always did
|
|
13
|
+
(``EncryptionStateError`` / ``WrongEncryptionToken`` / ``StoreLocked``) for the
|
|
14
|
+
driver to translate, plus a bare ``ImportError`` when the optional ``[encryption]``
|
|
15
|
+
extra is not installed (only the token-minting operations need the cipher).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from generic_ml_cache_adapters.adapter.outbound.crypto.filesystem_encryption_manifest_store import (
|
|
23
|
+
FilesystemEncryptionManifestStore,
|
|
24
|
+
)
|
|
25
|
+
from generic_ml_cache_adapters.adapter.outbound.crypto.store_encryptor import StoreEncryptor
|
|
26
|
+
from generic_ml_cache_adapters.adapter.outbound.persistence.filesystem_store_lock import (
|
|
27
|
+
FilesystemStoreLock,
|
|
28
|
+
)
|
|
29
|
+
from generic_ml_cache_core.application.domain.model.encryption.encryption_state import (
|
|
30
|
+
EncryptionState,
|
|
31
|
+
)
|
|
32
|
+
from generic_ml_cache_core.application.port.outbound.cipher_port import CipherPort
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _load_cipher() -> CipherPort:
|
|
36
|
+
"""Construct the AES-GCM cipher. Raises ``ImportError`` if the optional
|
|
37
|
+
``[encryption]`` extra (the ``cryptography`` dependency) is not installed."""
|
|
38
|
+
from generic_ml_cache_adapters.adapter.outbound.crypto.aesgcm_cipher import AesGcmCipher
|
|
39
|
+
|
|
40
|
+
return AesGcmCipher()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class StoreEncryptionOps:
|
|
44
|
+
"""Composition-root facade for the store-encryption operations, so a driver can
|
|
45
|
+
run the encrypt commands without importing any concrete crypto adapter."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, store_root: Path) -> None:
|
|
48
|
+
self._store_root = Path(store_root)
|
|
49
|
+
|
|
50
|
+
def status(self) -> EncryptionState:
|
|
51
|
+
"""The store's current encryption state (no cipher needed)."""
|
|
52
|
+
return FilesystemEncryptionManifestStore(self._store_root).state()
|
|
53
|
+
|
|
54
|
+
def enable(self) -> str:
|
|
55
|
+
"""Enable encryption and return the freshly minted token (shown once)."""
|
|
56
|
+
cipher = _load_cipher()
|
|
57
|
+
token = cipher.generate_token()
|
|
58
|
+
self._encryptor(cipher).enable(token)
|
|
59
|
+
return token
|
|
60
|
+
|
|
61
|
+
def disable(self, token: str) -> None:
|
|
62
|
+
"""Decrypt the store back to plaintext with the current token."""
|
|
63
|
+
self._encryptor(_load_cipher()).disable(token)
|
|
64
|
+
|
|
65
|
+
def rotate(self, old_token: str) -> str:
|
|
66
|
+
"""Rotate to a freshly minted token and return it (shown once)."""
|
|
67
|
+
cipher = _load_cipher()
|
|
68
|
+
new_token = cipher.generate_token()
|
|
69
|
+
self._encryptor(cipher).rotate(old_token, new_token)
|
|
70
|
+
return new_token
|
|
71
|
+
|
|
72
|
+
def invalidate(self) -> None:
|
|
73
|
+
"""Crypto-shred: wipe the cache and return the store to empty + public."""
|
|
74
|
+
self._encryptor().invalidate()
|
|
75
|
+
|
|
76
|
+
def _encryptor(self, cipher: CipherPort | None = None) -> StoreEncryptor:
|
|
77
|
+
return StoreEncryptor(
|
|
78
|
+
self._store_root,
|
|
79
|
+
FilesystemEncryptionManifestStore(self._store_root),
|
|
80
|
+
FilesystemStoreLock(self._store_root),
|
|
81
|
+
cipher,
|
|
82
|
+
)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Daniel Slobozian
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""PersistenceBackend — the DB-backed outbound adapters, bundled as one unit (V33).
|
|
4
|
+
|
|
5
|
+
The repository, the call journal, and the migration adapter all share ONE database
|
|
6
|
+
through ONE connection FACTORY (each operation opens its own connection from it), so
|
|
7
|
+
they are an atomic unit — you cannot have a Postgres repository and a SQLite journal
|
|
8
|
+
"for the same store". Grouping them into a single injected bundle kills the bare
|
|
9
|
+
``conn_factory`` seam that C-1 exposed: once the dialect layer is gone, a free
|
|
10
|
+
connection factory is a *crossable* seam (a Postgres factory handed to SQLite-dialect
|
|
11
|
+
adapters explodes at runtime). Here the connection factory is owned by / constructed
|
|
12
|
+
with the dialect adapters, never handed across.
|
|
13
|
+
|
|
14
|
+
The bundle is composition plumbing, NOT a hexagonal port: it is a frozen dataclass
|
|
15
|
+
of the individual core outbound ports (the same shape as ``ApplicationApi`` on the
|
|
16
|
+
inbound side). Core still owns and injects each port one by one — the core never
|
|
17
|
+
sees a "backend"; the bundle exists only at composition time. Both the bundle type
|
|
18
|
+
and the shipped ``sqlite_persistence_backend`` factory live here in bootstrap
|
|
19
|
+
(never in adapters — a factory in adapters returning a bootstrap type would invert
|
|
20
|
+
``adapters ──▶ bootstrap``); adapters ship only the leaf classes.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import cast
|
|
29
|
+
|
|
30
|
+
from generic_ml_cache_adapters.adapter.outbound.clock.system_clock import SystemClock
|
|
31
|
+
from generic_ml_cache_adapters.adapter.outbound.metrics.access_registry import AccessRegistry
|
|
32
|
+
from generic_ml_cache_adapters.adapter.outbound.metrics.journal_metrics import JournalMetrics
|
|
33
|
+
from generic_ml_cache_adapters.adapter.outbound.persistence.sqlite.execution_repository import (
|
|
34
|
+
SqliteExecutionRepository,
|
|
35
|
+
)
|
|
36
|
+
from generic_ml_cache_adapters.datasource import sqlite_connection_factory
|
|
37
|
+
from generic_ml_cache_adapters.db import DbConnection
|
|
38
|
+
from generic_ml_cache_adapters.migration_runner import SqliteStoreMigration
|
|
39
|
+
from generic_ml_cache_core.application.port.outbound.call_journal_ports import (
|
|
40
|
+
CallStatsPort,
|
|
41
|
+
PurgeJournalPort,
|
|
42
|
+
RecordCallEventPort,
|
|
43
|
+
SessionQueryPort,
|
|
44
|
+
SessionReportSourcePort,
|
|
45
|
+
SessionSpecPort,
|
|
46
|
+
SessionTagsPort,
|
|
47
|
+
)
|
|
48
|
+
from generic_ml_cache_core.application.port.outbound.diagnostics_port import DiagnosticsPort
|
|
49
|
+
from generic_ml_cache_core.application.port.outbound.ml_run_ports import (
|
|
50
|
+
AnnotateMlRunPort,
|
|
51
|
+
InspectMlRunsPort,
|
|
52
|
+
PurgeMlRunsPort,
|
|
53
|
+
ReadMlRunPort,
|
|
54
|
+
SaveMlRunPort,
|
|
55
|
+
)
|
|
56
|
+
from generic_ml_cache_core.application.port.outbound.repair_ml_runs_port import RepairMlRunsPort
|
|
57
|
+
from generic_ml_cache_core.application.port.outbound.store_migration_port import StoreMigrationPort
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class PersistenceBackend:
|
|
62
|
+
"""The DB-backed outbound ports, grouped so they share one datasource.
|
|
63
|
+
|
|
64
|
+
One field per outbound port the use-case graph consumes (mirrors ApplicationApi
|
|
65
|
+
on the inbound side). The shipped ``sqlite_persistence_backend`` binds one
|
|
66
|
+
``SqliteExecutionRepository`` instance to every ``*_ml_run(s)`` field and one
|
|
67
|
+
``JournalMetrics`` instance to every journal field — one impl, many role ports
|
|
68
|
+
(V32/B-1). An embedder replacing the DB implements these ports over their own
|
|
69
|
+
store and packs their own backend.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
# the execution repository's role ports
|
|
73
|
+
save_ml_run: SaveMlRunPort
|
|
74
|
+
read_ml_run: ReadMlRunPort
|
|
75
|
+
annotate_ml_run: AnnotateMlRunPort
|
|
76
|
+
inspect_ml_runs: InspectMlRunsPort
|
|
77
|
+
purge_ml_runs: PurgeMlRunsPort
|
|
78
|
+
repair_ml_runs: RepairMlRunsPort
|
|
79
|
+
# the call journal's role ports
|
|
80
|
+
record_call_event: RecordCallEventPort
|
|
81
|
+
call_stats: CallStatsPort
|
|
82
|
+
session_report_source: SessionReportSourcePort
|
|
83
|
+
session_query: SessionQueryPort
|
|
84
|
+
purge_journal: PurgeJournalPort
|
|
85
|
+
session_tags: SessionTagsPort
|
|
86
|
+
session_spec: SessionSpecPort
|
|
87
|
+
# the whole-store migration contract
|
|
88
|
+
migration: StoreMigrationPort
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def sqlite_persistence_backend(
|
|
92
|
+
db_path: Path,
|
|
93
|
+
diag: DiagnosticsPort | None = None,
|
|
94
|
+
*,
|
|
95
|
+
check_same_thread: bool = True,
|
|
96
|
+
) -> PersistenceBackend:
|
|
97
|
+
"""The shipped SQLite persistence backend: repository + journal + migration over
|
|
98
|
+
one SQLite connection factory (owned here, never exposed).
|
|
99
|
+
|
|
100
|
+
``check_same_thread=False`` for a multi-threaded driver (the daemon's async pool);
|
|
101
|
+
the default single-threaded ``True`` suits the CLI. The clock is internal — only
|
|
102
|
+
the repository uses it (to stamp supersession / ``persisted_at``)."""
|
|
103
|
+
conn_factory = cast(
|
|
104
|
+
"Callable[[], DbConnection]",
|
|
105
|
+
sqlite_connection_factory(db_path, check_same_thread=check_same_thread),
|
|
106
|
+
)
|
|
107
|
+
clock = SystemClock()
|
|
108
|
+
repository = SqliteExecutionRepository(conn_factory, clock)
|
|
109
|
+
journal = JournalMetrics(AccessRegistry(conn_factory, diag=diag))
|
|
110
|
+
# store_root = the DB's directory, holding ``store.lock`` — lets the migration run
|
|
111
|
+
# under the blocking-exclusive store lock so a concurrent first-init is serialized
|
|
112
|
+
# (Y1) rather than racing to double-seed schema_version or collide on the DDL.
|
|
113
|
+
migration = SqliteStoreMigration(conn_factory, diag, store_root=db_path.parent)
|
|
114
|
+
return PersistenceBackend(
|
|
115
|
+
save_ml_run=repository,
|
|
116
|
+
read_ml_run=repository,
|
|
117
|
+
annotate_ml_run=repository,
|
|
118
|
+
inspect_ml_runs=repository,
|
|
119
|
+
purge_ml_runs=repository,
|
|
120
|
+
repair_ml_runs=repository,
|
|
121
|
+
record_call_event=journal,
|
|
122
|
+
call_stats=journal,
|
|
123
|
+
session_report_source=journal,
|
|
124
|
+
session_query=journal,
|
|
125
|
+
purge_journal=journal,
|
|
126
|
+
session_tags=journal,
|
|
127
|
+
session_spec=journal,
|
|
128
|
+
migration=migration,
|
|
129
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Daniel Slobozian
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Driver-facing hook for the live-progress stream (W28).
|
|
4
|
+
|
|
5
|
+
The NDJSON progress writer is an adapter concern — it opens files, so it lives in
|
|
6
|
+
the ``adapters`` package (and core's raw-I/O guard keeps it out of the pure core).
|
|
7
|
+
A *driver* that also needs to write the same event log (the CLI's async-jobs
|
|
8
|
+
runner appends job events in the run-stream format so ``watch`` reads one log) must
|
|
9
|
+
not import that adapter directly — that is the exact ``cli -> adapters`` edge W28
|
|
10
|
+
removes. Instead it reaches the writer through this composition-root hook: the
|
|
11
|
+
driver depends on ``bootstrap``, ``bootstrap`` owns the wiring to ``adapters``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from generic_ml_cache_adapters.stream import StreamWriter
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def open_progress_stream(path: Path) -> StreamWriter:
|
|
22
|
+
"""Open a best-effort NDJSON progress writer at ``path``.
|
|
23
|
+
|
|
24
|
+
Safe to call even if ``path`` is unwritable: streaming is then silently
|
|
25
|
+
disabled and the caller's writes are dropped, never raised.
|
|
26
|
+
"""
|
|
27
|
+
return StreamWriter(path)
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Daniel Slobozian
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Driver-facing read-only store-status probe (W28, folds W26).
|
|
4
|
+
|
|
5
|
+
The CLI's ``gmlcache doctor`` reports the store's applied migrations. It used to do
|
|
6
|
+
so by handing the read-write ``sqlite_connection_factory`` to a mutating status
|
|
7
|
+
helper — which created the store directory, the database file, and the
|
|
8
|
+
``schema_version`` table as a side effect. A *status probe* must never initialize the
|
|
9
|
+
store it inspects (W26): a mistyped store path would silently spawn an empty store.
|
|
10
|
+
|
|
11
|
+
This hook does it read-only: an absent database file reports "unmigrated" without
|
|
12
|
+
touching the filesystem; a present one is opened ``mode=ro`` and read, never
|
|
13
|
+
created. It also keeps ``doctor`` free of any direct ``adapters`` import (W28) — the
|
|
14
|
+
driver depends on ``bootstrap``, ``bootstrap`` owns the wiring to ``adapters``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import cast
|
|
22
|
+
|
|
23
|
+
from generic_ml_cache_adapters.datasource import sqlite_connection_factory
|
|
24
|
+
from generic_ml_cache_adapters.db import DbConnection
|
|
25
|
+
from generic_ml_cache_adapters.migration_runner import applied_schema_version
|
|
26
|
+
|
|
27
|
+
_DB_NAME = "executions.sqlite3"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def applied_migrations(store_root: Path) -> list[dict[str, str]]:
|
|
31
|
+
"""Report the store's applied migrations without initializing it.
|
|
32
|
+
|
|
33
|
+
Absent database file → ``[]`` (unmigrated), no filesystem touch; present → open
|
|
34
|
+
the database read-only and read the applied migrations, never ``CREATE``.
|
|
35
|
+
"""
|
|
36
|
+
db_path = Path(store_root) / _DB_NAME
|
|
37
|
+
if not db_path.exists():
|
|
38
|
+
return []
|
|
39
|
+
factory = cast("Callable[[], DbConnection]", sqlite_connection_factory(db_path, read_only=True))
|
|
40
|
+
return applied_schema_version(factory)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Daniel Slobozian
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Driver-facing hook for the CLI's no-credential stub mode (W28).
|
|
4
|
+
|
|
5
|
+
When the CLI is invoked without selecting a client, every API client name is
|
|
6
|
+
served by the in-process stub adapter — it records/replays a canned response with
|
|
7
|
+
no live call, so demos and cache tests exercise the full pipeline without real
|
|
8
|
+
credentials. Constructing that concrete stub adapter is the last direct
|
|
9
|
+
``cli._compose -> adapters.adapter.outbound`` edge W28 removes; the driver asks the
|
|
10
|
+
composition root for the stub-runner map instead.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import cast
|
|
16
|
+
|
|
17
|
+
from generic_ml_cache_adapters.adapter.outbound.api.stub_api_client_adapter import (
|
|
18
|
+
StubApiClientAdapter,
|
|
19
|
+
)
|
|
20
|
+
from generic_ml_cache_core.application.domain.model.catalog.adapter_boundary import AdapterBoundary
|
|
21
|
+
from generic_ml_cache_core.application.port.outbound.adapter_catalog_port import AdapterCatalogPort
|
|
22
|
+
from generic_ml_cache_core.application.port.outbound.registered_adapter_port import (
|
|
23
|
+
RegisteredAdapterPort,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def stub_api_runners(catalog: AdapterCatalogPort) -> dict[str, RegisteredAdapterPort]:
|
|
28
|
+
"""Map every API client name in the catalog to one in-process stub adapter."""
|
|
29
|
+
stub = cast("RegisteredAdapterPort", StubApiClientAdapter())
|
|
30
|
+
return {
|
|
31
|
+
descriptor.client_name: stub
|
|
32
|
+
for descriptor in catalog.list_adapters()
|
|
33
|
+
if descriptor.boundary is AdapterBoundary.API
|
|
34
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: generic-ml-cache-bootstrap
|
|
3
|
+
Version: 0.29.0
|
|
4
|
+
Summary: Composition root and plugin discovery for generic-ml-cache: discovers gmlcache.adapters plugins (with load-time whitelisting and provenance trust) and wires the application.
|
|
5
|
+
Project-URL: Homepage, https://github.com/danielslobozian/generic-ml-cache
|
|
6
|
+
Project-URL: Repository, https://github.com/danielslobozian/generic-ml-cache
|
|
7
|
+
Project-URL: Issues, https://github.com/danielslobozian/generic-ml-cache/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/danielslobozian/generic-ml-cache/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Daniel Slobozian
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
License-File: NOTICE
|
|
13
|
+
Keywords: ai,cache,composition-root,discovery,hexagonal,llm,plugins
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: generic-ml-cache-adapters==0.29.*
|
|
26
|
+
Requires-Dist: generic-ml-cache-core==0.29.*
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: coverage>=7; extra == 'dev'
|
|
29
|
+
Requires-Dist: import-linter>=2; extra == 'dev'
|
|
30
|
+
Requires-Dist: pyright>=1.1; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-timeout; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff>=0.15; extra == 'dev'
|
|
35
|
+
Provides-Extra: encryption
|
|
36
|
+
Requires-Dist: generic-ml-cache-adapters[encryption]==0.29.*; extra == 'encryption'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# generic-ml-cache-bootstrap
|
|
40
|
+
|
|
41
|
+
#### The composition root and plugin discovery for gmlcache
|
|
42
|
+
|
|
43
|
+
The startup layer that assembles the application. It discovers the
|
|
44
|
+
`gmlcache.adapters` plugins (whitelisting them at load time and judging which
|
|
45
|
+
distributions are trusted), then wires the [core](https://github.com/danielslobozian/generic-ml-cache/tree/main/packages/core)
|
|
46
|
+
use cases to the concrete [adapters](https://github.com/danielslobozian/generic-ml-cache/tree/main/packages/adapters)
|
|
47
|
+
and hands the drivers a ready application API.
|
|
48
|
+
|
|
49
|
+
It is the one place that freely wires both `core` and `adapters` — the
|
|
50
|
+
composition root in hexagonal terms. The drivers
|
|
51
|
+
([cli](https://github.com/danielslobozian/generic-ml-cache/tree/main/packages/cli),
|
|
52
|
+
daemon) get their wired application from `bootstrap` and reach the domain only
|
|
53
|
+
through the inbound ports of that `ApplicationApi` bundle; the DB-backed adapters
|
|
54
|
+
are grouped behind an injectable `PersistenceBackend`, so no driver ever touches a
|
|
55
|
+
raw connection. A driver keeps only a few deliberate *direct* edges to `adapters`,
|
|
56
|
+
each a driver-policy choice rather than domain wiring: the CLI's own
|
|
57
|
+
`encrypt`/`decrypt`/`rotate` command helpers (the crypto adapters) and its demo
|
|
58
|
+
stub adapter, and each driver selecting its own diagnostics (logging) adapter.
|
|
59
|
+
An adapter never imports `bootstrap` (that would be a leaf calling the composition
|
|
60
|
+
root). These edges are frozen by import-linter contracts.
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
You normally get it transitively (`pip install generic-ml-cache-cli` pulls it
|
|
65
|
+
in). Installed on its own:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install generic-ml-cache-bootstrap
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
Apache-2.0 — see [`LICENSE`](https://github.com/danielslobozian/generic-ml-cache/blob/main/LICENSE)
|
|
74
|
+
and [`NOTICE`](https://github.com/danielslobozian/generic-ml-cache/blob/main/NOTICE).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
generic_ml_cache_bootstrap/__init__.py,sha256=o_WpASTjKfguUGrMS1AQ53YR6zynJwo9K0HqO9_lxkM,1124
|
|
2
|
+
generic_ml_cache_bootstrap/application.py,sha256=qBt_4qOIThe_wwiw5X4J_miMXSjUwv3HYHcSToBPcCU,12988
|
|
3
|
+
generic_ml_cache_bootstrap/diagnostics.py,sha256=FeQaAT7lPtqA8QbErzgHvOPdxPPmAPzl5XoHyy52Lzg,1750
|
|
4
|
+
generic_ml_cache_bootstrap/encryption.py,sha256=tTIA3HIp-T4YXG4IOW_AuESAQQme_k3FUmPd5s8jBXk,3480
|
|
5
|
+
generic_ml_cache_bootstrap/persistence_backend.py,sha256=TUKivdVIbsUfr6anaip3MsPr8A5ZvXKe6bjkZBaKnYU,5786
|
|
6
|
+
generic_ml_cache_bootstrap/progress.py,sha256=x1xXBa_af9_pbwPkBfojMvXQUydWDIbS7PCMoDMX568,1138
|
|
7
|
+
generic_ml_cache_bootstrap/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
generic_ml_cache_bootstrap/store_status.py,sha256=P8NyaF8srEkMr3AFsutdTx3UR8y1R4xGFed9RDkoV-s,1799
|
|
9
|
+
generic_ml_cache_bootstrap/stub_runners.py,sha256=rPMCOQgnr8kvoomvEx_D1Vm7fHcs4Uyfw02BZk0TOpk,1491
|
|
10
|
+
generic_ml_cache_bootstrap/discovery/__init__.py,sha256=r8hplqEchFcwDwXgPBKLT6FmNtLbgQQBdofFzwZmZhc,403
|
|
11
|
+
generic_ml_cache_bootstrap/discovery/_entrypoints.py,sha256=R2eBCT7oAH16NqC0jkAe20VnXeTuhMyb-53uaRKXOd4,1509
|
|
12
|
+
generic_ml_cache_bootstrap/discovery/api_discover.py,sha256=yq8fsHLUugF7blqYNOBnJ-WIHO9YN6lYriv8wux9QjU,2275
|
|
13
|
+
generic_ml_cache_bootstrap/discovery/client_discover.py,sha256=ENsg0rETyiGyjEJDAtapDIH0X8_6wqcWSZ9nqSfEPoo,9875
|
|
14
|
+
generic_ml_cache_bootstrap/discovery/composite_adapter_catalog.py,sha256=Sv8y4e8F61NNwciC8k3zxXmnm4O-newM8J-yeE0T1i8,1627
|
|
15
|
+
generic_ml_cache_bootstrap/discovery/composite_adapter_resolver.py,sha256=PTTin0z3aX0c1GGxZWGbxEQCF2a9jwqm2rjm_9p_lCs,1978
|
|
16
|
+
generic_ml_cache_bootstrap/discovery/composition.py,sha256=u7TV6J8iQMEflfCZfDEEioqZuJq3bjGoqioGzajaDog,6585
|
|
17
|
+
generic_ml_cache_bootstrap/discovery/entrypoint_adapter_catalog.py,sha256=jshTpep829PbiAyXzfnnDvLpWhf7V2ApSRayPxKtnmE,7206
|
|
18
|
+
generic_ml_cache_bootstrap/discovery/entrypoint_adapter_resolver.py,sha256=lAye9rB09OA1L69qwXCbCOJymqKDAVJIjw0QkFOvW6w,2266
|
|
19
|
+
generic_ml_cache_bootstrap/discovery/in_memory_adapter_registry.py,sha256=Ba1irjFWHGrdtd3U6PtF5OpfrHAu-uYI-Z7CXj0qaTI,4054
|
|
20
|
+
generic_ml_cache_bootstrap/discovery/static_adapter_catalog.py,sha256=wpjE7IJVeQonJDwEWtrHSaBxms-N14PM1NaBEaQgIqI,1526
|
|
21
|
+
generic_ml_cache_bootstrap-0.29.0.dist-info/METADATA,sha256=7-0tEMK83Bv2sVCIpyQodpQopePAEzkXWOpKuMDm50w,3654
|
|
22
|
+
generic_ml_cache_bootstrap-0.29.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
23
|
+
generic_ml_cache_bootstrap-0.29.0.dist-info/licenses/LICENSE,sha256=de-gfE0q-xTYImzwC3dj3S7BxVhanf6RmIGjo_7y3aw,11357
|
|
24
|
+
generic_ml_cache_bootstrap-0.29.0.dist-info/licenses/NOTICE,sha256=hQoAdw5YSwg3GmeY8hmy9nyOh20La-Qc6mre-TKpn5M,329
|
|
25
|
+
generic_ml_cache_bootstrap-0.29.0.dist-info/RECORD,,
|