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.
Files changed (25) hide show
  1. generic_ml_cache_bootstrap/__init__.py +26 -0
  2. generic_ml_cache_bootstrap/application.py +274 -0
  3. generic_ml_cache_bootstrap/diagnostics.py +38 -0
  4. generic_ml_cache_bootstrap/discovery/__init__.py +8 -0
  5. generic_ml_cache_bootstrap/discovery/_entrypoints.py +45 -0
  6. generic_ml_cache_bootstrap/discovery/api_discover.py +61 -0
  7. generic_ml_cache_bootstrap/discovery/client_discover.py +274 -0
  8. generic_ml_cache_bootstrap/discovery/composite_adapter_catalog.py +38 -0
  9. generic_ml_cache_bootstrap/discovery/composite_adapter_resolver.py +51 -0
  10. generic_ml_cache_bootstrap/discovery/composition.py +148 -0
  11. generic_ml_cache_bootstrap/discovery/entrypoint_adapter_catalog.py +159 -0
  12. generic_ml_cache_bootstrap/discovery/entrypoint_adapter_resolver.py +54 -0
  13. generic_ml_cache_bootstrap/discovery/in_memory_adapter_registry.py +97 -0
  14. generic_ml_cache_bootstrap/discovery/static_adapter_catalog.py +36 -0
  15. generic_ml_cache_bootstrap/encryption.py +82 -0
  16. generic_ml_cache_bootstrap/persistence_backend.py +129 -0
  17. generic_ml_cache_bootstrap/progress.py +27 -0
  18. generic_ml_cache_bootstrap/py.typed +0 -0
  19. generic_ml_cache_bootstrap/store_status.py +40 -0
  20. generic_ml_cache_bootstrap/stub_runners.py +34 -0
  21. generic_ml_cache_bootstrap-0.29.0.dist-info/METADATA +74 -0
  22. generic_ml_cache_bootstrap-0.29.0.dist-info/RECORD +25 -0
  23. generic_ml_cache_bootstrap-0.29.0.dist-info/WHEEL +4 -0
  24. generic_ml_cache_bootstrap-0.29.0.dist-info/licenses/LICENSE +201 -0
  25. 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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any