provider module#
Providers and the chain#
A provider answers part queries. A PartSource is an ordered chain of
them, asked in order, first hit wins.
To write one, subclass Provider, or CategoryProvider to
answer one query at a time rather than a batch, and implement three methods:
config(), which identifies the provider so a chain has a stable digest;
resolve(), which answers a batch; and rehydrate(), which rebuilds a part from
a lockfile entry this provider wrote. distinct() is optional and serves
exploration only. Everything else has a default.
An answer is per query, not per provider. Resolved retires the query,
NoMatch and Unsupported let the chain continue, and
Deny stops it. A provider is asked every query in its chain and
declines what it cannot serve, per query, by returning Unsupported with the keys
it could not process – there is no capability manifest, so nothing can skip a
provider on a declaration, and the aggregated diagnostic can say that nothing in
the chain serves tcr as opposed to nothing matching it.
resolve is handed a batch rather than a query so a provider can answer many at
once, but a batch is not the design: only queries the lock could not answer
reach the chain, so it is the whole set only on a first run, under off, or in
check’s second call – and duplicates are collapsed onto
CanonicalQuery.count. Anything order-wide – order quantities, rebates,
reel consolidation – has to be written knowing that. See
Provider.resolve().
A PartSource is a jitx.context.Context, so with PartSource([…]):
scopes it and a Design can carry one as an attribute. Its members may be
provider instances or registry strings; see jitxlib.parts.policy for
where a chain comes from when the design supplies none.
This module also re-exports the query and record names a provider needs, so a provider has one import site rather than four.
- class Resolved(parts)[source]#
Bases:
objectParts this provider offers, ranked best-first.
Never empty: a provider with nothing to offer returns NoMatch. Selection is parts[0], so an empty tuple would retire a query with nothing to bind.
- Parameters:
parts (tuple[ResolvedPart, ...])
- parts: tuple[ResolvedPart, ...]#
- class NoMatch(reason='')[source]#
Bases:
objectUnderstood the query, has nothing. The chain continues.
- Parameters:
reason (str)
- class Unsupported(keys=(), reason='')[source]#
Bases:
objectCannot honor part of the query. The chain continues.
keys names the query keys this provider cannot process, which is what lets the aggregated diagnostic tell a design author that nothing in the chain serves tcr, as opposed to nothing matching.
- class Deny(reason)[source]#
Bases:
objectPolicy veto. The chain stops and resolution fails with this reason.
- Parameters:
reason (str)
- class ChainOutcome(outcome, trail=())[source]#
Bases:
objectThe chain’s answer for one query, plus what each member said on the way.
One Outcome cannot carry per-provider accumulation, and the diagnostics need all of it.
- Parameters:
- outcome: Resolved | NoMatch | Unsupported | Deny#
- describe(outcome)[source]#
One-line rendering of an outcome, for diagnostics.
- Return type:
- Parameters:
outcome (Resolved | NoMatch | Unsupported | Deny)
- class Provider[source]#
Bases:
ABCWhat a parts provider implements.
Subclass this, or CategoryProvider to answer one query at a time. config, resolve and rehydrate are yours to write; everything else has a default.
- name: str#
How this provider is named in diagnostics and lockfile entries.
Supply it as a dataclass field, a plain class attribute, or an assignment in __init__. A property or a ClassVar is an incompatible override; for a derived name, use field(init=False) and set it in __post_init__.
- discovery_priority: int = 0#
Rank used ONLY when no chain is declared anywhere and one is discovered from entry points; higher is asked first. An explicit PartSource ignores it, because a written chain is already ordered.
Declare it the same way as name.
- abstractmethod config()[source]#
This provider’s configuration as a canonical JSON object.
An object rather than a bare value, so a config is always something with named keys, which is what makes two of them comparable and what keeps a chain digest readable in the file it lands in.
Must cover everything that could change which part gets selected (a file path, a base URL, a series name) and nothing that could not, such as an open connection or a warmed cache.
- abstractmethod resolve(batch)[source]#
Answer a batch. Index-aligned with batch: one outcome per query.
The batch is not the design. It carries the queries that reached the chain on this flush, which is all of them only when all of them are new: a first run against an empty lock, or off mode. Under read and read-write a query the lock can answer never gets here, so against a populated lockfile the batch is the unpinned remainder – often empty. replay never calls this at all. Batching is best effort on top of that, so an early flush splits one elaboration into several batches and no batch can be assumed to be the whole of even the remainder.
check is the exception and always asks twice per flush: once for the ordinary run, over the unpinned remainder, which is what it binds, and once over every query the design asked, which is the clean run it compares the board against. The second is the whole order, so a provider doing order-level work is asked the same question a from-scratch run would ask – and may answer it differently from the first without contradicting itself.
Duplicates are collapsed: one question is one entry however many components asked it, since a hundred identical capacitors must not become a hundred catalogue scans. CanonicalQuery.count carries how many asked.
A provider doing whole-batch optimization – minimum order quantities, bulk rebates, consolidating a reel, allocation across sources – has to be written knowing all of that. Those are properties of the whole order, and the batch is the whole order only on a run where nothing is pinned. The tension is inherent rather than an implementation gap: if a selection depends on the order, adding one component can legitimately move every selection, which is the thing a lock exists to prevent. A provider that makes that trade owns it.
- Return type:
Sequence[Resolved|NoMatch|Unsupported|Deny]- Parameters:
batch (Sequence[CanonicalQuery])
- abstractmethod rehydrate(identity, payload)[source]#
Reconstruct a part from a lockfile entry this provider wrote.
Required of any provider used with a lockfile: only the provider knows what payload means. May raise StalePayload if the payload predates a change it cannot honor.
- Return type:
- Parameters:
identity (PartIdentity)
- distinct(query, key)[source]#
Values of key among parts matching query. Exploration only.
Optional, and None is how a provider says it does not enumerate – which is distinct from (), meaning it enumerated and found nothing. The default says the former, so a provider with no business answering need not mention it.
Not batched, unlike resolve: it is called from a REPL one question at a time, so the batch-is-canonical rule does not apply. There is no fallback and no Deny, because nothing is being selected.
- Return type:
- Parameters:
query (CanonicalQuery)
key (str)
- class CategoryProvider[source]#
Bases:
ProviderConvenience base for a provider that answers per query rather than per batch.
Implements resolve by looping resolve_one, which is what a provider with no batch advantage wants. A provider that can exploit the batch, say one round trip for three hundred queries, should implement resolve directly – and read what Provider.resolve says about what a batch does and does not contain before relying on it for anything order-wide.
- abstractmethod resolve_one(query)[source]#
Answer one query. config and rehydrate are still yours to write.
- Return type:
Resolved|NoMatch|Unsupported|Deny- Parameters:
query (CanonicalQuery)
- resolve(batch)[source]#
Answer a batch. Index-aligned with batch: one outcome per query.
The batch is not the design. It carries the queries that reached the chain on this flush, which is all of them only when all of them are new: a first run against an empty lock, or off mode. Under read and read-write a query the lock can answer never gets here, so against a populated lockfile the batch is the unpinned remainder – often empty. replay never calls this at all. Batching is best effort on top of that, so an early flush splits one elaboration into several batches and no batch can be assumed to be the whole of even the remainder.
check is the exception and always asks twice per flush: once for the ordinary run, over the unpinned remainder, which is what it binds, and once over every query the design asked, which is the clean run it compares the board against. The second is the whole order, so a provider doing order-level work is asked the same question a from-scratch run would ask – and may answer it differently from the first without contradicting itself.
Duplicates are collapsed: one question is one entry however many components asked it, since a hundred identical capacitors must not become a hundred catalogue scans. CanonicalQuery.count carries how many asked.
A provider doing whole-batch optimization – minimum order quantities, bulk rebates, consolidating a reel, allocation across sources – has to be written knowing all of that. Those are properties of the whole order, and the batch is the whole order only on a run where nothing is pinned. The tension is inherent rather than an implementation gap: if a selection depends on the order, adding one component can legitimately move every selection, which is the thing a lock exists to prevent. A provider that makes that trade owns it.
- Return type:
Sequence[Resolved|NoMatch|Unsupported|Deny]- Parameters:
batch (Sequence[CanonicalQuery])
- class Named(name, provider)[source]#
Bases:
objectGive a chain member an explicit name.
A member’s name is what a lockfile entry records and what replay looks up, so two members answering to the same name have to be told apart. They are suffixed positionally by default – acme, acme#2 – which works but says nothing, and moves if the chain is reordered. Name them instead:
PartSource([ Named("approved", AcmeParts(APPROVED_DSN)), Named("fallback", AcmeParts(FALLBACK_DSN)), ])
Not a provider and not a wrapper around one: it is a declaration PartSource reads and discards, so it adds no layer for a part to be resolved through. A member’s name is part of the chain’s identity, so renaming one re-keys the chain and its entries re-resolve once, deliberately, like any other chain edit.
- class PartSource(providers=None, label=None)[source]#
Bases:
ContextAn ordered chain of providers.
Providers are checked in the order given, first hit wins. label names this chain in lockfile entries and diagnostics; it is cosmetic and derived if omitted. Wrap a member in
Namedto give it an explicit name, which is worth doing whenever two members would otherwise collide.Strings in the chain are resolved through the entry-point registry the first time the chain is used, not at construction, so naming a provider package you have not installed fails with a clear error later rather than at import time.
- given_names#
The explicit name of each member, or None where it takes its own.
- config()[source]#
Ordered configs of the chain; the chain digest is built from this.
The member name, not the provider’s own: how a member is addressed is part of the chain’s identity, since that is what lock entries reference. Renaming one is a chain edit and re-keys accordingly.
- names()[source]#
How each member is addressed: what a lock entry records.
An explicit Named wins; otherwise the provider’s own name, positionally suffixed where two collide. Duplicate names in a Python-built chain are legal, so anything keyed by name has to disambiguate rather than silently merge two providers – but a positional suffix says nothing about which member is which and moves when the chain is reordered, so an ambiguous chain is better off naming its members.
Two explicit names that collide are an error, not a thing to suffix: the point of giving one is to be unambiguous.
- outcomes(batch)[source]#
Per-query results, index-aligned. Never raises for a query-level result.
This is what a delegating provider calls: resolve raises on exhaustion, which would make its own NoMatch and Deny branches unreachable.
- Return type:
- Parameters:
batch (Sequence[CanonicalQuery])
- resolve(batch)[source]#
The resolver’s entry point. Wraps outcomes and raises.
PartNotFound on exhaustion, PartDenied on a veto. Note which method the last step belongs to: outcomes reports exhaustion, resolve raises on it. Conflating the two makes delegation impossible.
Use selections instead where the answering member matters, which for anything writing a lockfile it does.
- Return type:
- Parameters:
batch (Sequence[CanonicalQuery])
- selections(batch)[source]#
Each query’s part and which member answered it, index-aligned.
Raises exactly as resolve does; it is the same call with the one fact resolve throws away. The member is its name from names(), which is what a lock entry records and what replay looks up.
That fact cannot be recovered afterwards. part.identity.provider is free text the provider chose: two members may answer to one name, and a delegating provider names its inner source, which is not a member at all. Only the chain knows, and only here.
- Return type:
- Parameters:
batch (Sequence[CanonicalQuery])
- candidates(query, limit)[source]#
Up to limit candidates, keyed by the member that offered them.
Browsing, not choosing, and the two want different things from a chain. resolve takes the first member that answers and stops, which is what precedence means when one part is being selected. A limit asks for up to N, so stopping at a member that offered three of them answers with three while the rest of the chain holds hundreds – which is not what was asked.
Keyed by member, like distinct, and for its reason: which catalogue has a part is usually what the caller is trying to find out. The mapping is in chain order, so precedence is still readable, and nothing is compared across members – ranking two providers’ parts against each other needs an agreed vocabulary for price and the rest that this design deliberately does not impose.
Only members actually asked appear. The walk stops once limit is filled, and a member that was never reached is absent rather than present with an empty list, which would claim it had been asked and had nothing. A member that was asked and had nothing is present with [].
Each member is asked only for the shortfall, and truncated to it, so a provider that ignores limit cannot push the total over it.
A Deny stops the walk and raises, as in resolve: a veto says the query should not be asked at all, and listing a later member’s candidates would defeat it.
- Return type:
dict[str,list[ResolvedPart]]- Parameters:
query (CanonicalQuery)
limit (int)
- distinct(query, key)[source]#
Values of key among matching parts, per provider that implements it.
Keyed by provider name rather than flattened, since which provider knows a value is usually what the caller is trying to find out.
- Return type:
- Parameters:
query (CanonicalQuery)
key (str)