induscode 0.1.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.
- induscode/__init__.py +56 -0
- induscode/addons/__init__.py +176 -0
- induscode/addons/contract.py +923 -0
- induscode/addons/dispatch/__init__.py +43 -0
- induscode/addons/dispatch/event_dispatcher.py +348 -0
- induscode/addons/dispatch/tool_interceptor.py +349 -0
- induscode/addons/host.py +469 -0
- induscode/addons/loader.py +314 -0
- induscode/addons/manifest.py +232 -0
- induscode/addons/surface.py +199 -0
- induscode/boot/__init__.py +108 -0
- induscode/boot/auth_vault.py +323 -0
- induscode/boot/boot.py +210 -0
- induscode/boot/contract.py +223 -0
- induscode/boot/invocation.py +117 -0
- induscode/boot/runners/__init__.py +42 -0
- induscode/boot/runners/link_runner.py +82 -0
- induscode/boot/runners/oneshot_runner.py +85 -0
- induscode/boot/runners/registry.py +46 -0
- induscode/boot/runners/repl_runner.py +340 -0
- induscode/boot/runners/session.py +549 -0
- induscode/boot/stages.py +198 -0
- induscode/boot/upgrade/__init__.py +36 -0
- induscode/boot/upgrade/apply.py +125 -0
- induscode/boot/upgrade/upgrades.py +136 -0
- induscode/briefing/__init__.py +115 -0
- induscode/briefing/compose.py +414 -0
- induscode/briefing/contract.py +528 -0
- induscode/briefing/macros.py +721 -0
- induscode/briefing/skills.py +417 -0
- induscode/capability_deck/__init__.py +233 -0
- induscode/capability_deck/bridge_ledger/__init__.py +66 -0
- induscode/capability_deck/bridge_ledger/key.py +181 -0
- induscode/capability_deck/bridge_ledger/ledger.py +276 -0
- induscode/capability_deck/bridge_ledger/network.py +336 -0
- induscode/capability_deck/builtin_bridge.py +358 -0
- induscode/capability_deck/cards/__init__.py +116 -0
- induscode/capability_deck/cards/bg_process.py +482 -0
- induscode/capability_deck/cards/memory.py +226 -0
- induscode/capability_deck/cards/saas.py +280 -0
- induscode/capability_deck/cards/task.py +256 -0
- induscode/capability_deck/cards/todo.py +312 -0
- induscode/capability_deck/contract.py +450 -0
- induscode/capability_deck/manifest.py +126 -0
- induscode/capability_deck/provision.py +217 -0
- induscode/channels/__init__.py +146 -0
- induscode/channels/contract.py +585 -0
- induscode/channels/framer.py +132 -0
- induscode/channels/link/__init__.py +50 -0
- induscode/channels/link/dialog.py +246 -0
- induscode/channels/link/driver.py +308 -0
- induscode/channels/link/server.py +217 -0
- induscode/channels/oneshot.py +178 -0
- induscode/channels/ops.py +140 -0
- induscode/channels/session_ops.py +172 -0
- induscode/conductor/__init__.py +240 -0
- induscode/conductor/catalog.py +309 -0
- induscode/conductor/conductor.py +1084 -0
- induscode/conductor/contract.py +1035 -0
- induscode/conductor/matcher.py +291 -0
- induscode/conductor/serialize.py +575 -0
- induscode/conductor/signal_hub.py +382 -0
- induscode/conductor/skill_parse.py +294 -0
- induscode/conductor/transcript_store.py +449 -0
- induscode/console/__init__.py +236 -0
- induscode/console/app.py +1677 -0
- induscode/console/components/__init__.py +62 -0
- induscode/console/components/banner.py +499 -0
- induscode/console/components/banner_sweep.py +188 -0
- induscode/console/components/emblem.py +181 -0
- induscode/console/components/status_bar.py +102 -0
- induscode/console/contract.py +836 -0
- induscode/console/input/__init__.py +107 -0
- induscode/console/input/chord.py +197 -0
- induscode/console/input/dir_reader.py +113 -0
- induscode/console/input/intents.py +258 -0
- induscode/console/input/providers.py +469 -0
- induscode/console/mount.py +137 -0
- induscode/console/overlays/__init__.py +94 -0
- induscode/console/overlays/auth.py +503 -0
- induscode/console/overlays/pickers.py +526 -0
- induscode/console/overlays/router.py +129 -0
- induscode/console/overlays/sessions.py +232 -0
- induscode/console/reducer.py +145 -0
- induscode/console/resume_picker.py +156 -0
- induscode/console/slash_commands/__init__.py +78 -0
- induscode/console/slash_commands/builtins.py +254 -0
- induscode/console/slash_commands/dynamic.py +217 -0
- induscode/console/slash_commands/integrations.py +949 -0
- induscode/console/slash_commands/transcript.py +404 -0
- induscode/console/slash_commands/workbench.py +430 -0
- induscode/console/startup.py +434 -0
- induscode/console/theme/__init__.py +44 -0
- induscode/console/theme/adapter.py +168 -0
- induscode/console/theme/palette.py +128 -0
- induscode/console/theme/resolve.py +123 -0
- induscode/console/theme/tokens.py +185 -0
- induscode/console_slash/__init__.py +111 -0
- induscode/console_slash/contract.py +185 -0
- induscode/console_slash/registry.py +140 -0
- induscode/console_slash/resolve.py +194 -0
- induscode/console_slash/shared.py +172 -0
- induscode/entry.py +108 -0
- induscode/insight/__init__.py +153 -0
- induscode/insight/collector.py +73 -0
- induscode/insight/replay.py +305 -0
- induscode/insight/wrapper.py +1115 -0
- induscode/kit/__init__.py +82 -0
- induscode/kit/clipboard_image.py +215 -0
- induscode/kit/external_editor.py +120 -0
- induscode/kit/image.py +188 -0
- induscode/kit/shell.py +89 -0
- induscode/kit/tool_fetch.py +288 -0
- induscode/launch/__init__.py +224 -0
- induscode/launch/catalog.py +310 -0
- induscode/launch/contract.py +569 -0
- induscode/launch/credentials.py +852 -0
- induscode/launch/invocation/__init__.py +39 -0
- induscode/launch/invocation/attachments.py +281 -0
- induscode/launch/invocation/flags.py +210 -0
- induscode/launch/invocation/read.py +369 -0
- induscode/launch/invocation/usage.py +110 -0
- induscode/launch/oauth.py +808 -0
- induscode/launch/packages.py +299 -0
- induscode/launch/pickers.py +291 -0
- induscode/py.typed +0 -0
- induscode/runtime_bridge/__init__.py +166 -0
- induscode/runtime_bridge/bridges/__init__.py +66 -0
- induscode/runtime_bridge/bridges/_drive.py +268 -0
- induscode/runtime_bridge/bridges/builtins.py +177 -0
- induscode/runtime_bridge/bridges/claude_cli.py +198 -0
- induscode/runtime_bridge/bridges/codex_cli.py +203 -0
- induscode/runtime_bridge/bridges/indusagi_cli.py +217 -0
- induscode/runtime_bridge/broker.py +397 -0
- induscode/runtime_bridge/contract.py +734 -0
- induscode/runtime_bridge/sink.py +351 -0
- induscode/sessions/__init__.py +25 -0
- induscode/sessions/contract.py +119 -0
- induscode/sessions/library.py +350 -0
- induscode/settings/__init__.py +47 -0
- induscode/settings/contract.py +313 -0
- induscode/settings/manager.py +268 -0
- induscode/transcript_export/__init__.py +109 -0
- induscode/transcript_export/contract.py +522 -0
- induscode/transcript_export/publish.py +455 -0
- induscode/transcript_export/sgr.py +566 -0
- induscode/transcript_export/template.py +319 -0
- induscode/transcript_export/theme_bridge.py +325 -0
- induscode/window_budget/__init__.py +76 -0
- induscode/window_budget/budget/__init__.py +26 -0
- induscode/window_budget/budget/estimate.py +273 -0
- induscode/window_budget/budget/gate.py +60 -0
- induscode/window_budget/budget/slice.py +145 -0
- induscode/window_budget/condenser.py +170 -0
- induscode/window_budget/contract.py +329 -0
- induscode/window_budget/summarize/__init__.py +33 -0
- induscode/window_budget/summarize/condense.py +212 -0
- induscode/window_budget/summarize/prompt.py +241 -0
- induscode/workspace/__init__.py +30 -0
- induscode/workspace/brand.py +96 -0
- induscode/workspace/locator.py +269 -0
- induscode-0.1.0.dist-info/METADATA +97 -0
- induscode-0.1.0.dist-info/RECORD +167 -0
- induscode-0.1.0.dist-info/WHEEL +4 -0
- induscode-0.1.0.dist-info/entry_points.txt +3 -0
- induscode-0.1.0.dist-info/licenses/CREDITS.md +22 -0
- induscode-0.1.0.dist-info/licenses/NOTICE +7 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""Model catalog — :class:`ModelCatalog` (port of TS ``src/conductor/catalog/catalog.ts``).
|
|
2
|
+
|
|
3
|
+
A transform pipeline over the framework model list. The framework publishes
|
|
4
|
+
its catalog per provider via :func:`indusagi.ai.get_models`; each entry is a
|
|
5
|
+
rich :class:`indusagi.ai.Model` record. This module **re-derives** that data
|
|
6
|
+
into the conductor's own normalized shape — a :class:`CatalogCard` — through a
|
|
7
|
+
validate → normalize → key pipeline, then exposes a small lookup surface
|
|
8
|
+
(:meth:`ModelCatalog.all`, :meth:`ModelCatalog.by_provider`,
|
|
9
|
+
:meth:`ModelCatalog.get`) keyed by a canonical ``"provider/modelId"`` id.
|
|
10
|
+
|
|
11
|
+
The full framework :class:`~indusagi.ai.Model` is retained on each card so the
|
|
12
|
+
conductor can hand a complete model object to the agent without a second
|
|
13
|
+
registry round-trip.
|
|
14
|
+
|
|
15
|
+
Port notes
|
|
16
|
+
----------
|
|
17
|
+
- The TS zod gate (``rawModelGate.safeParse``) becomes a **manual tolerant
|
|
18
|
+
probe** (:func:`_clears_gate`): a malformed provider record is silently
|
|
19
|
+
DROPPED, never raised on. pydantic is deliberately not pulled in for six
|
|
20
|
+
field checks. # parity: the gate drops invalid cards — it must never throw.
|
|
21
|
+
- Raw records may be the framework's frozen :class:`~indusagi.ai.Model`
|
|
22
|
+
dataclasses (the live source) *or* plain mappings (test sources); every
|
|
23
|
+
field read goes through the tolerant :func:`_field_of` accessor, mirroring
|
|
24
|
+
how zod validated plain objects structurally.
|
|
25
|
+
- TS ``z.<type>().optional()`` admits *undefined* but rejects *null*; the
|
|
26
|
+
Python probe mirrors that by distinguishing an absent field (passes) from a
|
|
27
|
+
present ``None`` (rejects).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
33
|
+
from dataclasses import dataclass
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
from indusagi.ai import get_models, get_providers
|
|
37
|
+
|
|
38
|
+
from induscode.conductor.contract import ModelCardRef
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"CatalogCard",
|
|
42
|
+
"CatalogSource",
|
|
43
|
+
"ModelCatalog",
|
|
44
|
+
"canonical_id",
|
|
45
|
+
"to_card_ref",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Card shape
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True, slots=True)
|
|
55
|
+
class CatalogCard:
|
|
56
|
+
"""One normalized entry in the catalog (TS ``CatalogCard``).
|
|
57
|
+
|
|
58
|
+
Extends the lightweight :class:`~induscode.conductor.contract.ModelCardRef`
|
|
59
|
+
identity projection with the extra capability facets the matcher filters
|
|
60
|
+
on, plus the original framework ``Model``. ``ref`` is the slice a UI
|
|
61
|
+
lists/labels with; ``model`` is the full record the conductor binds to the
|
|
62
|
+
agent. Field names keep the TS spelling.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
# Canonical "provider/modelId" identifier — the catalog key.
|
|
66
|
+
id: str
|
|
67
|
+
# Owning provider slug.
|
|
68
|
+
provider: str
|
|
69
|
+
# Provider-scoped model id (e.g. "claude-sonnet-4").
|
|
70
|
+
modelId: str
|
|
71
|
+
# Human-readable display name.
|
|
72
|
+
name: str
|
|
73
|
+
# Whether the model exposes a reasoning/thinking budget.
|
|
74
|
+
reasoning: bool
|
|
75
|
+
# Whether the model accepts image input.
|
|
76
|
+
acceptsImages: bool
|
|
77
|
+
# Context window size in tokens (0 when the framework omits it).
|
|
78
|
+
contextTokens: int
|
|
79
|
+
# The framework model record, retained verbatim for binding.
|
|
80
|
+
model: Any
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def to_card_ref(card: CatalogCard) -> ModelCardRef:
|
|
84
|
+
"""Project a :class:`CatalogCard` down to its public ``ModelCardRef`` slice."""
|
|
85
|
+
return ModelCardRef(
|
|
86
|
+
id=card.id,
|
|
87
|
+
provider=card.provider,
|
|
88
|
+
modelId=card.modelId,
|
|
89
|
+
name=card.name,
|
|
90
|
+
reasoning=card.reasoning,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# Validation gate (the zod gate, hand-probed)
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
#: Sentinel marking "field not present at all" (TS ``undefined``).
|
|
99
|
+
_ABSENT: Any = object()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _field_of(raw: Any, name: str) -> Any:
|
|
103
|
+
"""Read ``name`` off a raw record — mapping key or dataclass attribute —
|
|
104
|
+
returning :data:`_ABSENT` when the field does not exist."""
|
|
105
|
+
if isinstance(raw, Mapping):
|
|
106
|
+
return raw.get(name, _ABSENT)
|
|
107
|
+
return getattr(raw, name, _ABSENT)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _is_nonempty_str(value: Any) -> bool:
|
|
111
|
+
return isinstance(value, str) and len(value) >= 1
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _clears_gate(raw: Any) -> bool:
|
|
115
|
+
"""The minimal slice of a framework ``Model`` the catalog depends on.
|
|
116
|
+
|
|
117
|
+
Run every raw record through this gate so a malformed provider entry is
|
|
118
|
+
dropped rather than poisoning lookups downstream. Unknown extra fields are
|
|
119
|
+
tolerated (only named keys are read). Mirrors the TS zod gate exactly:
|
|
120
|
+
|
|
121
|
+
- ``id`` / ``name`` / ``provider``: required non-empty strings
|
|
122
|
+
(``provider`` admits any non-empty string, matching ``z.string()`` over
|
|
123
|
+
the ``KnownProvider | string`` union);
|
|
124
|
+
- ``reasoning``: optional bool;
|
|
125
|
+
- ``input``: optional array of strings;
|
|
126
|
+
- ``contextWindow``: optional number.
|
|
127
|
+
"""
|
|
128
|
+
if raw is None or isinstance(raw, (str, int, float, bool)):
|
|
129
|
+
return False
|
|
130
|
+
if not _is_nonempty_str(_field_of(raw, "id")):
|
|
131
|
+
return False
|
|
132
|
+
if not _is_nonempty_str(_field_of(raw, "name")):
|
|
133
|
+
return False
|
|
134
|
+
if not _is_nonempty_str(_field_of(raw, "provider")):
|
|
135
|
+
return False
|
|
136
|
+
reasoning = _field_of(raw, "reasoning")
|
|
137
|
+
if reasoning is not _ABSENT and not isinstance(reasoning, bool):
|
|
138
|
+
return False
|
|
139
|
+
inputs = _field_of(raw, "input")
|
|
140
|
+
if inputs is not _ABSENT:
|
|
141
|
+
if not isinstance(inputs, Sequence) or isinstance(inputs, (str, bytes)):
|
|
142
|
+
return False
|
|
143
|
+
if not all(isinstance(item, str) for item in inputs):
|
|
144
|
+
return False
|
|
145
|
+
context_window = _field_of(raw, "contextWindow")
|
|
146
|
+
if context_window is not _ABSENT and (
|
|
147
|
+
isinstance(context_window, bool) or not isinstance(context_window, (int, float))
|
|
148
|
+
):
|
|
149
|
+
return False
|
|
150
|
+
return True
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
# Pipeline helpers
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def canonical_id(provider: str, model_id: str) -> str:
|
|
159
|
+
"""Compose the canonical catalog key from a provider + model id."""
|
|
160
|
+
return f"{provider}/{model_id}"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _normalize_card(raw: Any) -> CatalogCard:
|
|
164
|
+
"""Normalize one validated raw record into a :class:`CatalogCard`.
|
|
165
|
+
|
|
166
|
+
Pure: the same input always yields the same card. Capability flags are
|
|
167
|
+
derived defensively from the (optional) framework fields.
|
|
168
|
+
"""
|
|
169
|
+
provider = str(_field_of(raw, "provider"))
|
|
170
|
+
model_id = _field_of(raw, "id")
|
|
171
|
+
raw_inputs = _field_of(raw, "input")
|
|
172
|
+
inputs = (
|
|
173
|
+
list(raw_inputs)
|
|
174
|
+
if isinstance(raw_inputs, Sequence) and not isinstance(raw_inputs, (str, bytes))
|
|
175
|
+
else []
|
|
176
|
+
)
|
|
177
|
+
context_window = _field_of(raw, "contextWindow")
|
|
178
|
+
return CatalogCard(
|
|
179
|
+
id=canonical_id(provider, model_id),
|
|
180
|
+
provider=provider,
|
|
181
|
+
modelId=model_id,
|
|
182
|
+
name=_field_of(raw, "name"),
|
|
183
|
+
reasoning=_field_of(raw, "reasoning") is True,
|
|
184
|
+
acceptsImages="image" in inputs,
|
|
185
|
+
contextTokens=(
|
|
186
|
+
int(context_window)
|
|
187
|
+
if isinstance(context_window, (int, float)) and not isinstance(context_window, bool)
|
|
188
|
+
else 0
|
|
189
|
+
),
|
|
190
|
+
model=raw,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _build_index(
|
|
195
|
+
providers: Sequence[str],
|
|
196
|
+
pull: Callable[[str], Sequence[Any]],
|
|
197
|
+
) -> dict[str, CatalogCard]:
|
|
198
|
+
"""The transform pipeline: pull every provider's raw model list, validate,
|
|
199
|
+
drop rejects, normalize, and de-duplicate by canonical id (first wins).
|
|
200
|
+
The result is a stable, insertion-ordered map."""
|
|
201
|
+
index: dict[str, CatalogCard] = {}
|
|
202
|
+
for provider in providers:
|
|
203
|
+
try:
|
|
204
|
+
raws = pull(str(provider))
|
|
205
|
+
except Exception:
|
|
206
|
+
continue # a provider that can't be enumerated is simply absent
|
|
207
|
+
for raw in raws:
|
|
208
|
+
if not _clears_gate(raw):
|
|
209
|
+
continue
|
|
210
|
+
card = _normalize_card(raw)
|
|
211
|
+
if card.id not in index:
|
|
212
|
+
index[card.id] = card
|
|
213
|
+
return index
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
# ModelCatalog
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@dataclass(frozen=True, slots=True)
|
|
222
|
+
class CatalogSource:
|
|
223
|
+
"""Injection seam for tests: how the catalog sources its raw data
|
|
224
|
+
(TS ``CatalogSource``)."""
|
|
225
|
+
|
|
226
|
+
# Enumerate the providers to scan.
|
|
227
|
+
providers: Callable[[], Sequence[str]]
|
|
228
|
+
# Pull the raw model list for one provider.
|
|
229
|
+
models: Callable[[str], Sequence[Any]]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
#: Providers excluded from the live catalog.
|
|
233
|
+
#:
|
|
234
|
+
#: ``mock`` is the framework's echo provider (its one model, ``mock-default``,
|
|
235
|
+
#: replies "Mock response to: …"). It is invaluable in tests but must never
|
|
236
|
+
#: surface to a real session — neither as the auto-selected default nor as a
|
|
237
|
+
#: row in the model picker — so it is filtered out of the live source. Tests
|
|
238
|
+
#: that want it inject their own :class:`CatalogSource`.
|
|
239
|
+
_EXCLUDED_PROVIDERS: frozenset[str] = frozenset({"mock"})
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _framework_source() -> CatalogSource:
|
|
243
|
+
"""The live framework source — ``get_providers`` + ``get_models`` from
|
|
244
|
+
:mod:`indusagi.ai`. Built lazily per call site: no import-time registry I/O."""
|
|
245
|
+
return CatalogSource(
|
|
246
|
+
providers=lambda: [
|
|
247
|
+
p for p in get_providers() if str(p) not in _EXCLUDED_PROVIDERS
|
|
248
|
+
],
|
|
249
|
+
models=lambda provider: get_models(provider),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class ModelCatalog:
|
|
254
|
+
"""A normalized, validated view of the framework model list.
|
|
255
|
+
|
|
256
|
+
Construct once (the build pipeline runs eagerly in the constructor) and
|
|
257
|
+
query many times. The instance is immutable after construction; call
|
|
258
|
+
:meth:`refreshed` to rebuild against the current framework state.
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
__slots__ = ("_index", "_by_provider")
|
|
262
|
+
|
|
263
|
+
def __init__(self, source: CatalogSource | None = None) -> None:
|
|
264
|
+
"""``source`` is the raw-data seam; defaults to the live
|
|
265
|
+
:mod:`indusagi.ai` registry."""
|
|
266
|
+
src = source if source is not None else _framework_source()
|
|
267
|
+
self._index: dict[str, CatalogCard] = _build_index(src.providers(), src.models)
|
|
268
|
+
grouped: dict[str, list[CatalogCard]] = {}
|
|
269
|
+
for card in self._index.values():
|
|
270
|
+
grouped.setdefault(card.provider, []).append(card)
|
|
271
|
+
self._by_provider: dict[str, list[CatalogCard]] = grouped
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def size(self) -> int:
|
|
275
|
+
"""Total number of cards in the catalog."""
|
|
276
|
+
return len(self._index)
|
|
277
|
+
|
|
278
|
+
def all(self) -> list[CatalogCard]:
|
|
279
|
+
"""Every card, in insertion order (provider scan order)."""
|
|
280
|
+
return list(self._index.values())
|
|
281
|
+
|
|
282
|
+
def by_provider(self, provider: str) -> list[CatalogCard]:
|
|
283
|
+
"""Cards owned by one provider (empty list if the provider has none)."""
|
|
284
|
+
return list(self._by_provider.get(str(provider), []))
|
|
285
|
+
|
|
286
|
+
def providers(self) -> list[str]:
|
|
287
|
+
"""The providers that contributed at least one card, in scan order."""
|
|
288
|
+
return list(self._by_provider.keys())
|
|
289
|
+
|
|
290
|
+
def get(self, id: str) -> CatalogCard | None:
|
|
291
|
+
"""Look up one card by its canonical ``"provider/modelId"`` id. Also
|
|
292
|
+
accepts a bare provider-scoped model id when it is unambiguous across
|
|
293
|
+
providers."""
|
|
294
|
+
direct = self._index.get(id)
|
|
295
|
+
if direct is not None:
|
|
296
|
+
return direct
|
|
297
|
+
if "/" not in id:
|
|
298
|
+
matches = [c for c in self._index.values() if c.modelId == id]
|
|
299
|
+
if len(matches) == 1:
|
|
300
|
+
return matches[0]
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
def has(self, id: str) -> bool:
|
|
304
|
+
"""True when the canonical id resolves to a card."""
|
|
305
|
+
return self.get(id) is not None
|
|
306
|
+
|
|
307
|
+
def refreshed(self, source: CatalogSource | None = None) -> "ModelCatalog":
|
|
308
|
+
"""Rebuild against the current framework state, returning a fresh catalog."""
|
|
309
|
+
return ModelCatalog(source if source is not None else _framework_source())
|