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,26 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Daniel Slobozian
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""generic-ml-cache-bootstrap: the composition root and plugin discovery layer.
|
|
4
|
+
|
|
5
|
+
This is where the application is assembled at startup. It owns two jobs that are
|
|
6
|
+
neither pure domain (core) nor a driven adapter (adapters):
|
|
7
|
+
|
|
8
|
+
* **Discovery** — find the ``gmlcache.adapters`` plugins, whitelist them at load
|
|
9
|
+
time, and decide which distributions are trusted (provenance).
|
|
10
|
+
* **Composition** — wire the core use cases to the concrete adapters and hand the
|
|
11
|
+
drivers (CLI, daemon) a ready application API.
|
|
12
|
+
|
|
13
|
+
It depends on both ``core`` and ``adapters`` (it must ``new`` the infra), and is
|
|
14
|
+
imported only by the drivers — never by an adapter (that would be a leaf calling
|
|
15
|
+
the composition root). See ``docs/design/bootstrap-split-and-v4-plan.md``.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from importlib.metadata import PackageNotFoundError
|
|
21
|
+
from importlib.metadata import version as _pkg_version
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
__version__ = _pkg_version("generic-ml-cache-bootstrap")
|
|
25
|
+
except PackageNotFoundError: # pragma: no cover
|
|
26
|
+
__version__ = "0+unknown"
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Daniel Slobozian
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""build_application_api — the shared composition root.
|
|
4
|
+
|
|
5
|
+
The ~15-object assembly that turns a store location + a connection factory into
|
|
6
|
+
a wired :class:`ApplicationApi` was duplicated, almost line for line, between the
|
|
7
|
+
CLI's ``_compose.py`` and the daemon's ``app.py``. It lives here now, in the one
|
|
8
|
+
package allowed to import both ``core`` and the concrete ``adapters``.
|
|
9
|
+
|
|
10
|
+
Only one thing genuinely differs between the drivers: **which client adapters to
|
|
11
|
+
wire**. The CLI runs one selected client per invocation; the daemon wires every
|
|
12
|
+
whitelisted client. That single variation is injected as the ``build_runners``
|
|
13
|
+
strategy; everything else (store recovery, blob store + encryption, migrations,
|
|
14
|
+
repository, metrics, the use-case graph) is identical and lives here.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from generic_ml_cache_adapters.adapter.outbound.api.anthropic_subscription_relay_adapter import (
|
|
23
|
+
AnthropicSubscriptionRelayAdapter,
|
|
24
|
+
)
|
|
25
|
+
from generic_ml_cache_adapters.adapter.outbound.crypto.encrypting_blob_store import (
|
|
26
|
+
EncryptingBlobStore,
|
|
27
|
+
TokenRequiredBlobStore,
|
|
28
|
+
)
|
|
29
|
+
from generic_ml_cache_adapters.adapter.outbound.crypto.filesystem_encryption_manifest_store import (
|
|
30
|
+
FilesystemEncryptionManifestStore,
|
|
31
|
+
)
|
|
32
|
+
from generic_ml_cache_adapters.adapter.outbound.crypto.store_encryptor import StoreEncryptor
|
|
33
|
+
from generic_ml_cache_adapters.adapter.outbound.diagnostics.null_diagnostics_adapter import (
|
|
34
|
+
NullDiagnosticsAdapter,
|
|
35
|
+
)
|
|
36
|
+
from generic_ml_cache_adapters.adapter.outbound.fingerprint.filesystem_file_fingerprint import (
|
|
37
|
+
FilesystemFileFingerprint,
|
|
38
|
+
)
|
|
39
|
+
from generic_ml_cache_adapters.adapter.outbound.persistence.filesystem_execution_key_lock import (
|
|
40
|
+
FilesystemExecutionKeyLock,
|
|
41
|
+
lock_wait_for_client_timeout,
|
|
42
|
+
)
|
|
43
|
+
from generic_ml_cache_adapters.adapter.outbound.persistence.filesystem_store_lock import (
|
|
44
|
+
FilesystemStoreLock,
|
|
45
|
+
)
|
|
46
|
+
from generic_ml_cache_adapters.adapter.outbound.storage.filesystem_blob_store import (
|
|
47
|
+
FilesystemBlobStore,
|
|
48
|
+
)
|
|
49
|
+
from generic_ml_cache_adapters.adapter.outbound.workspace.filesystem_workspace import (
|
|
50
|
+
FilesystemWorkspace,
|
|
51
|
+
)
|
|
52
|
+
from generic_ml_cache_core.application.port.outbound.adapter_catalog_port import AdapterCatalogPort
|
|
53
|
+
from generic_ml_cache_core.application.port.outbound.adapter_resolver_port import (
|
|
54
|
+
AdapterResolverPort,
|
|
55
|
+
)
|
|
56
|
+
from generic_ml_cache_core.application.port.outbound.blob_store_port import BlobStorePort
|
|
57
|
+
from generic_ml_cache_core.application.port.outbound.diagnostics_port import DiagnosticsPort
|
|
58
|
+
from generic_ml_cache_core.application.port.outbound.file_fingerprint_port import (
|
|
59
|
+
FileFingerprintPort,
|
|
60
|
+
)
|
|
61
|
+
from generic_ml_cache_core.application.port.outbound.registered_adapter_port import (
|
|
62
|
+
RegisteredAdapterPort,
|
|
63
|
+
)
|
|
64
|
+
from generic_ml_cache_core.application.port.outbound.store_migration_port import (
|
|
65
|
+
CURRENT_MODEL_VERSION,
|
|
66
|
+
StoreMigrationPort,
|
|
67
|
+
)
|
|
68
|
+
from generic_ml_cache_core.application.usecase.artifact_content_service import (
|
|
69
|
+
ArtifactContentService,
|
|
70
|
+
)
|
|
71
|
+
from generic_ml_cache_core.application.usecase.execution_query_service import ExecutionQueryService
|
|
72
|
+
from generic_ml_cache_core.application.usecase.probe_service import ProbeService
|
|
73
|
+
from generic_ml_cache_core.application.usecase.purge_service import PurgeService
|
|
74
|
+
from generic_ml_cache_core.application.usecase.repair_store_service import RepairStoreService
|
|
75
|
+
from generic_ml_cache_core.application.usecase.run_ml_execution_service import (
|
|
76
|
+
RunMlExecutionService,
|
|
77
|
+
)
|
|
78
|
+
from generic_ml_cache_core.application.usecase.session_admin_service import SessionAdminService
|
|
79
|
+
from generic_ml_cache_core.application.usecase.session_report_service import SessionReportService
|
|
80
|
+
from generic_ml_cache_core.application.usecase.session_tags_service import SessionTagsService
|
|
81
|
+
from generic_ml_cache_core.application.usecase.store_stats_service import StoreStatsService
|
|
82
|
+
from generic_ml_cache_core.application.wiring.application_api import ApplicationApi
|
|
83
|
+
from generic_ml_cache_core.common.errors import PersistenceContractOutdated
|
|
84
|
+
|
|
85
|
+
from generic_ml_cache_bootstrap.discovery.composition import catalog_and_resolver_for
|
|
86
|
+
from generic_ml_cache_bootstrap.persistence_backend import (
|
|
87
|
+
PersistenceBackend,
|
|
88
|
+
sqlite_persistence_backend,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
_BLOBS_DIRNAME = "blobs"
|
|
92
|
+
#: The shipped SQLite store's database filename under the store root — used to build
|
|
93
|
+
#: the default PersistenceBackend when an embedder injects none.
|
|
94
|
+
_DB_NAME = "executions.sqlite3"
|
|
95
|
+
|
|
96
|
+
# The driver-specific variation: given the resolved catalog + resolver, return the
|
|
97
|
+
# runner adapters to wire, keyed by client NAME. (CLI: one selected client; daemon:
|
|
98
|
+
# all whitelisted.)
|
|
99
|
+
BuildRunners = Callable[[AdapterCatalogPort, AdapterResolverPort], dict[str, RegisteredAdapterPort]]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def recover_store(store_root: Path) -> None:
|
|
103
|
+
"""Roll back any half-finished encryption migration before the store is read."""
|
|
104
|
+
StoreEncryptor(
|
|
105
|
+
store_root,
|
|
106
|
+
FilesystemEncryptionManifestStore(store_root),
|
|
107
|
+
FilesystemStoreLock(store_root),
|
|
108
|
+
).recover()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def resolve_blob_store(store_root: Path, encryption_token: str | None) -> BlobStorePort:
|
|
112
|
+
"""The blob store, wrapped for encryption when the store has a manifest."""
|
|
113
|
+
blob_store: BlobStorePort = FilesystemBlobStore(store_root / _BLOBS_DIRNAME)
|
|
114
|
+
manifest = FilesystemEncryptionManifestStore(store_root).load()
|
|
115
|
+
if manifest is None:
|
|
116
|
+
return blob_store
|
|
117
|
+
if encryption_token is None:
|
|
118
|
+
return TokenRequiredBlobStore(blob_store)
|
|
119
|
+
from generic_ml_cache_adapters.adapter.outbound.crypto.aesgcm_cipher import AesGcmCipher
|
|
120
|
+
|
|
121
|
+
cipher = AesGcmCipher()
|
|
122
|
+
data_key = cipher.open_envelope(encryption_token, manifest)
|
|
123
|
+
return EncryptingBlobStore(blob_store, cipher, data_key)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def provision_store(migration: StoreMigrationPort) -> None:
|
|
127
|
+
"""Run the persistence-contract handshake (C-2), then migrate.
|
|
128
|
+
|
|
129
|
+
Fail-fast at boot if the injected adapter implements an older model contract
|
|
130
|
+
than this build requires, rather than letting a stale mapping silently drop a
|
|
131
|
+
field. The shipped SQLite adapter is always current; the check guards a
|
|
132
|
+
third-party adapter that might lag ``CURRENT_MODEL_VERSION``.
|
|
133
|
+
"""
|
|
134
|
+
implemented = migration.implemented_version()
|
|
135
|
+
if implemented < CURRENT_MODEL_VERSION:
|
|
136
|
+
raise PersistenceContractOutdated(
|
|
137
|
+
f"persistence adapter implements model version {implemented}, but this build "
|
|
138
|
+
f"requires version {CURRENT_MODEL_VERSION}; upgrade the adapter's mapping/migrations"
|
|
139
|
+
)
|
|
140
|
+
migration.migrate_to_current()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def build_application_api(
|
|
144
|
+
store_root: Path,
|
|
145
|
+
build_runners: BuildRunners,
|
|
146
|
+
*,
|
|
147
|
+
persistence: PersistenceBackend | None = None,
|
|
148
|
+
blob_store: BlobStorePort | None = None,
|
|
149
|
+
file_fingerprint: FileFingerprintPort | None = None,
|
|
150
|
+
encryption_token: str | None = None,
|
|
151
|
+
max_size: int | None = None,
|
|
152
|
+
whitelist: frozenset[str] | None = None,
|
|
153
|
+
client_timeout: float | None = None,
|
|
154
|
+
diag: DiagnosticsPort | None = None,
|
|
155
|
+
) -> ApplicationApi:
|
|
156
|
+
"""Wire the application: the use-case graph over its outbound adapters.
|
|
157
|
+
|
|
158
|
+
``build_runners`` is the only required driver-specific input — it chooses which
|
|
159
|
+
client adapters to wire. The infrastructure adapters are OVERRIDABLE with shipped
|
|
160
|
+
defaults (V33, ``@ConditionalOnMissingBean``): the DB-backed adapters as one
|
|
161
|
+
``PersistenceBackend`` bundle (default = SQLite under ``store_root``), and the
|
|
162
|
+
blob store / file fingerprint individually. A standalone driver injects nothing and
|
|
163
|
+
gets the batteries-included SQLite + filesystem stack; an embedder supplies a
|
|
164
|
+
Postgres/S3 backend and the C-2 boot handshake validates it.
|
|
165
|
+
"""
|
|
166
|
+
store_root = Path(store_root)
|
|
167
|
+
_diag: DiagnosticsPort = diag if diag is not None else NullDiagnosticsAdapter()
|
|
168
|
+
# Roll back a half-finished encryption migration ONLY on the default filesystem
|
|
169
|
+
# stack (X20). recover_store hardcodes FilesystemEncryptionManifestStore +
|
|
170
|
+
# FilesystemStoreLock, so if an embedder injects its own blob store (Postgres/S3)
|
|
171
|
+
# it owns its own recovery and bootstrap must not read/write/lock local FS
|
|
172
|
+
# encryption state under a nominal store_root. Checked before blob_store is
|
|
173
|
+
# resolved to its default below.
|
|
174
|
+
if blob_store is None:
|
|
175
|
+
recover_store(store_root)
|
|
176
|
+
persistence = persistence or sqlite_persistence_backend(store_root / _DB_NAME, _diag)
|
|
177
|
+
blob_store = (
|
|
178
|
+
blob_store if blob_store is not None else resolve_blob_store(store_root, encryption_token)
|
|
179
|
+
)
|
|
180
|
+
file_fingerprint = (
|
|
181
|
+
file_fingerprint if file_fingerprint is not None else FilesystemFileFingerprint()
|
|
182
|
+
)
|
|
183
|
+
# One shared per-key lock instance (X7): the record path and eviction/purge both
|
|
184
|
+
# coordinate on it, so an eviction of a key waits for a concurrent write of that
|
|
185
|
+
# same key (and vice versa) — its in-process stripe only serializes if it is the
|
|
186
|
+
# same object. Size its cross-process wait to the client call it protects (Y2), so a
|
|
187
|
+
# contender waits out an ordinary slow call (coalescing into one client run) instead
|
|
188
|
+
# of tripping the old fixed 5s and running the expensive call a second time.
|
|
189
|
+
execution_key_lock = FilesystemExecutionKeyLock(
|
|
190
|
+
store_root, _diag, timeout_seconds=lock_wait_for_client_timeout(client_timeout)
|
|
191
|
+
)
|
|
192
|
+
provision_store(persistence.migration)
|
|
193
|
+
catalog, resolver = catalog_and_resolver_for(whitelist)
|
|
194
|
+
runners = build_runners(catalog, resolver)
|
|
195
|
+
# The caching HTTP gateway (the daemon's /gateway/claude route) dispatches an
|
|
196
|
+
# API_PASSTHROUGH command to this verbatim relay through the shared run use case —
|
|
197
|
+
# it is not a user-selectable client, so it is wired here rather than discovered.
|
|
198
|
+
runners.setdefault(AnthropicSubscriptionRelayAdapter.name, AnthropicSubscriptionRelayAdapter())
|
|
199
|
+
# One application service per capability (grouped by shared machinery); each is
|
|
200
|
+
# exposed through the segregated per-operation inbound-port fields of ApplicationApi
|
|
201
|
+
# (B-1). On the outbound side (V32/V33) each service depends only on the role ports
|
|
202
|
+
# it needs, drawn from the PersistenceBackend bundle's per-role fields (one impl
|
|
203
|
+
# instance backs several of them).
|
|
204
|
+
purge = PurgeService(
|
|
205
|
+
persistence.purge_ml_runs,
|
|
206
|
+
blob_store,
|
|
207
|
+
journal=persistence.purge_journal,
|
|
208
|
+
sessions=persistence.session_query,
|
|
209
|
+
execution_key_lock=execution_key_lock,
|
|
210
|
+
diag=_diag,
|
|
211
|
+
)
|
|
212
|
+
session_tags = SessionTagsService(persistence.session_tags)
|
|
213
|
+
session_admin = SessionAdminService(
|
|
214
|
+
specs=persistence.session_spec, sessions=persistence.session_query
|
|
215
|
+
)
|
|
216
|
+
session_report = SessionReportService(
|
|
217
|
+
report_source=persistence.session_report_source,
|
|
218
|
+
sessions=persistence.session_query,
|
|
219
|
+
repository=persistence.read_ml_run,
|
|
220
|
+
repair_source=persistence.repair_ml_runs,
|
|
221
|
+
)
|
|
222
|
+
execution_query = ExecutionQueryService(persistence.inspect_ml_runs)
|
|
223
|
+
store_stats = StoreStatsService(persistence.call_stats)
|
|
224
|
+
artifact_content = ArtifactContentService(blob_store)
|
|
225
|
+
repair_store = RepairStoreService(
|
|
226
|
+
repair_source=persistence.repair_ml_runs,
|
|
227
|
+
artifact_persistence=persistence.save_ml_run,
|
|
228
|
+
blob_store=blob_store,
|
|
229
|
+
)
|
|
230
|
+
return ApplicationApi(
|
|
231
|
+
run_ml=RunMlExecutionService(
|
|
232
|
+
file_fingerprint,
|
|
233
|
+
runners,
|
|
234
|
+
blob_store,
|
|
235
|
+
save=persistence.save_ml_run,
|
|
236
|
+
read=persistence.read_ml_run,
|
|
237
|
+
annotate=persistence.annotate_ml_run,
|
|
238
|
+
record=persistence.record_call_event,
|
|
239
|
+
execution_key_lock=execution_key_lock,
|
|
240
|
+
store_lock=FilesystemStoreLock(store_root),
|
|
241
|
+
purge_service=purge,
|
|
242
|
+
max_size=max_size,
|
|
243
|
+
workspace=FilesystemWorkspace(),
|
|
244
|
+
diag=_diag,
|
|
245
|
+
),
|
|
246
|
+
probe=ProbeService(file_fingerprint, persistence.read_ml_run),
|
|
247
|
+
purge_by_key=purge,
|
|
248
|
+
purge_by_tag=purge,
|
|
249
|
+
purge_by_session=purge,
|
|
250
|
+
purge_by_session_tag=purge,
|
|
251
|
+
purge_all=purge,
|
|
252
|
+
evict_stale=purge,
|
|
253
|
+
evict_to_quota=purge,
|
|
254
|
+
tag_session=session_tags,
|
|
255
|
+
untag_session=session_tags,
|
|
256
|
+
list_session_tags=session_tags,
|
|
257
|
+
set_session_spec=session_admin,
|
|
258
|
+
clear_session_spec=session_admin,
|
|
259
|
+
get_session_spec=session_admin,
|
|
260
|
+
list_session_ids=session_admin,
|
|
261
|
+
sessions_for_tag=session_admin,
|
|
262
|
+
execution_keys_for_session=session_admin,
|
|
263
|
+
report_for_session=session_report,
|
|
264
|
+
report_for_tag=session_report,
|
|
265
|
+
list_execution_summaries=execution_query,
|
|
266
|
+
total_stored_bytes=execution_query,
|
|
267
|
+
tags_for_execution=execution_query,
|
|
268
|
+
find_current_execution=execution_query,
|
|
269
|
+
find_executions_by_key_prefix=execution_query,
|
|
270
|
+
event_counts=store_stats,
|
|
271
|
+
hit_counts_by_key=store_stats,
|
|
272
|
+
read_artifact_blob=artifact_content,
|
|
273
|
+
repair_store=repair_store,
|
|
274
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Daniel Slobozian
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Driver-facing hook for the diagnostics (logging) port (W28).
|
|
4
|
+
|
|
5
|
+
Choosing whether to log, at what level, and to which file is *driver policy* — the
|
|
6
|
+
CLI resolves it from flags + env + config, the daemon from env. But constructing
|
|
7
|
+
the concrete diagnostics adapter (null vs structlog) is infrastructure, and a
|
|
8
|
+
driver must not import a concrete ``adapters.…diagnostics`` class to do it — that
|
|
9
|
+
is the ``cli``/``daemon`` -> ``adapters`` edge W28 removes. The driver resolves the
|
|
10
|
+
settings and hands them here; the composition root builds the port.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from generic_ml_cache_adapters.adapter.outbound.diagnostics.null_diagnostics_adapter import (
|
|
18
|
+
NullDiagnosticsAdapter,
|
|
19
|
+
)
|
|
20
|
+
from generic_ml_cache_adapters.adapter.outbound.diagnostics.structlog_diagnostics_adapter import (
|
|
21
|
+
StructlogDiagnosticsAdapter,
|
|
22
|
+
)
|
|
23
|
+
from generic_ml_cache_core.application.port.outbound.diagnostics_port import DiagnosticsPort
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build_diagnostics(log_level: str | None, log_file: Path | None = None) -> DiagnosticsPort:
|
|
27
|
+
"""Build the diagnostics port from a driver's resolved logging settings.
|
|
28
|
+
|
|
29
|
+
An empty/absent ``log_level`` selects the quiet no-op adapter (the store still
|
|
30
|
+
works; nothing is logged). Otherwise structured logging is written to
|
|
31
|
+
``log_file`` (its parent directory is created), which must be provided.
|
|
32
|
+
"""
|
|
33
|
+
if not log_level:
|
|
34
|
+
return NullDiagnosticsAdapter()
|
|
35
|
+
if log_file is None:
|
|
36
|
+
raise ValueError("a log file is required when a log level is set")
|
|
37
|
+
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
return StructlogDiagnosticsAdapter(log_file, level=log_level)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Daniel Slobozian
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Adapter discovery: catalogs, resolvers, and descriptor helpers.
|
|
4
|
+
|
|
5
|
+
This is infrastructure — it may scan installed entry points and construct
|
|
6
|
+
concrete adapter classes. Core only sees the AdapterCatalogPort / AdapterResolverPort
|
|
7
|
+
contracts; the implementations live here and are injected by the composition root.
|
|
8
|
+
"""
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Daniel Slobozian
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Low-level entry-point access for the discovery layer.
|
|
4
|
+
|
|
5
|
+
This is the one place that touches ``importlib.metadata`` — the packaging system.
|
|
6
|
+
It belongs in the adapters/infrastructure package, never in core.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import importlib.metadata
|
|
12
|
+
|
|
13
|
+
ADAPTER_ENTRYPOINT_GROUP = "gmlcache.adapters"
|
|
14
|
+
ADAPTER_CONTRACT_VERSION = "1"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def iter_entry_points(
|
|
18
|
+
group: str = ADAPTER_ENTRYPOINT_GROUP,
|
|
19
|
+
) -> list[importlib.metadata.EntryPoint]:
|
|
20
|
+
"""Return the entry points in ``group``."""
|
|
21
|
+
return list(importlib.metadata.entry_points(group=group))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def distribution_name(entry_point: importlib.metadata.EntryPoint) -> str:
|
|
25
|
+
"""The name of the distribution providing ``entry_point`` (``""`` if unknown).
|
|
26
|
+
|
|
27
|
+
Used to decide trust at load time: an entry point whose distribution is in
|
|
28
|
+
the trusted set is loaded; others are third-party, off unless whitelisted.
|
|
29
|
+
"""
|
|
30
|
+
dist = entry_point.dist
|
|
31
|
+
if dist is None or not dist.metadata:
|
|
32
|
+
return ""
|
|
33
|
+
return dist.name or ""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def describe_source(entry_point: importlib.metadata.EntryPoint) -> str:
|
|
37
|
+
"""Human-readable "<package> <version>" for the distribution providing ``entry_point``."""
|
|
38
|
+
dist = entry_point.dist
|
|
39
|
+
if dist is None:
|
|
40
|
+
return entry_point.value
|
|
41
|
+
name = dist.name or ""
|
|
42
|
+
version = dist.version or ""
|
|
43
|
+
if name and version:
|
|
44
|
+
return f"{name} {version}"
|
|
45
|
+
return name or entry_point.value
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Daniel Slobozian
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""API provider discovery: list models for a registered API provider.
|
|
4
|
+
|
|
5
|
+
Advisory and read-only — mirrors the client discover module but for direct
|
|
6
|
+
REST adapters (Gemini, Anthropic, OpenAI …) rather than local CLI clients.
|
|
7
|
+
The adapter reads its own API key from the environment when ``api_key`` is not
|
|
8
|
+
passed explicitly.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from generic_ml_cache_core.application.domain.model.model_listing import ModelListing
|
|
14
|
+
from generic_ml_cache_core.application.port.outbound.model_listing_port import ModelListingPort
|
|
15
|
+
from generic_ml_cache_core.common.errors import UnknownClient
|
|
16
|
+
|
|
17
|
+
from generic_ml_cache_bootstrap.discovery.composition import get_adapter
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def list_api_models(
|
|
21
|
+
provider: str,
|
|
22
|
+
api_key: str | None = None,
|
|
23
|
+
whitelist: frozenset[str] | None = None,
|
|
24
|
+
) -> ModelListing:
|
|
25
|
+
"""List models for a registered API provider.
|
|
26
|
+
|
|
27
|
+
Returns a :class:`ModelListing` in all cases — never raises for an absent or
|
|
28
|
+
unsupported provider. Three honest outcomes:
|
|
29
|
+
|
|
30
|
+
* unknown provider → ``present=False``
|
|
31
|
+
* provider registered but adapter does not implement ModelListingPort →
|
|
32
|
+
``supported=False``
|
|
33
|
+
* provider listed → ``supported=True`` and ``models`` populated
|
|
34
|
+
|
|
35
|
+
``api_key`` is accepted for backward compatibility; adapters resolve their
|
|
36
|
+
key from the environment when it is not passed.
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
adapter = get_adapter(provider, whitelist=whitelist)
|
|
40
|
+
except UnknownClient as exc:
|
|
41
|
+
return ModelListing(name=provider, present=False, supported=False, reason=str(exc))
|
|
42
|
+
|
|
43
|
+
if not isinstance(adapter, ModelListingPort):
|
|
44
|
+
return ModelListing(
|
|
45
|
+
name=provider,
|
|
46
|
+
present=True,
|
|
47
|
+
supported=False,
|
|
48
|
+
reason="this provider has no model-listing endpoint",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
models = adapter.list_models()
|
|
53
|
+
except Exception as exc: # noqa: BLE001 — any transport failure → honest error
|
|
54
|
+
return ModelListing(
|
|
55
|
+
name=provider,
|
|
56
|
+
present=True,
|
|
57
|
+
supported=True,
|
|
58
|
+
reason=f"model listing failed: {exc}",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return ModelListing(name=provider, present=True, supported=True, models=models)
|