policy module#
Registration and chain discovery#
How a chain is found when the design did not hand one over, and how a string in a chain becomes a provider instance.
Where a chain comes from, in precedence order:
a
with PartSource([...])blocka
partsattribute on theDesign[tool.jitx.parts] chainin the current project’s pyproject.tomla chain published by an installed package, through the
jitxlib-parts-policyentry point groupauto-discovery from the
jitxlib-parts-providergroup, opt-in only, behinddiscover = true
Levels 1 and 2 come from the context stack, so PartSource.get() handles
them. This module is levels 3 to 5, and a level is consulted only when every
earlier one declined. Configure none of them and NoChainConfigured says so,
listing the options, rather than a guess being made.
A provider package registers itself in the jitxlib-parts-provider group, which
is what lets a chain name it as a string. "csv:approved-parts.csv" splits on
the first colon: the name before it selects the entry point, the rest is the
provider’s single constructor argument, and a path is taken relative to the
project. "ideal" takes no argument. A package publishing a whole chain instead
of a single provider registers in jitxlib-parts-policy, which is level 4 and
the ordinary way an organization ships its own procurement policy.
Remapped lives here rather than with the providers because it is chain
configuration rather than a provider of its own: it wraps one whose attribute
names differ from the ones a design uses, and it is written into the chain like
any other member.
- resolve_provider_name(spec, root=None)[source]#
Turn a chain string into a provider instance.
“csv:approved-parts.csv” splits on the first colon: csv names the entry point, and the remainder is its single constructor argument. A bare “ideal” constructs with no arguments.
root is passed on to providers that accept it, so a path written in project config is relative to the project. A provider that takes no root is constructed without one, which is why this inspects the signature rather than requiring every provider to accept an argument it has no use for.
Ambiguity is an error naming the candidates, since the project’s own config is the documented way to disambiguate.
- current_project(start=None)[source]#
The project start sits in, or None if it sits in none.
start defaults to the working directory. The returned project has already read its pyproject.toml, so pass it on rather than looking it up again: both discovered_source and discovered_lock take the project this returns.
None rather than a raise, because having no project is an ordinary state here and not a failure: it is what a bare script or a notebook looks like, and it turns the lockfile off and leaves only a published chain to find. Every caller has a branch for it.
Cached per starting directory, so the file is read once rather than once per part in the design. Reset with
clear_project_cache().- Return type:
- Parameters:
start (Path | None)
- clear_project_cache()[source]#
Forget which project each directory belongs to, and its config with it.
- Return type:
- discovered_source(project)[source]#
The chain implied by the installed environment, computed once per project.
Takes the project from
current_project(), or None where there is no project, such as a bare script or a notebook: only the published chain is consulted then. Reset withclear_discovered_sources().- Return type:
- Parameters:
project (JitxProject | None)
- clear_discovered_sources()[source]#
Drop every cached chain. For tests, and for a fresh run in one process.
- Return type:
- class Remapped(inner, map=None)[source]#
Bases:
ProviderWrap a provider whose attribute names differ from the ones a design uses.
The map is canonical-to-provider: {“case”: “package_size”} sends a query’s case filter as package_size, and rewrites the attribute back on the way out so the design never sees the provider’s spelling.
Part of the chain digest, since it is chain configuration living in the project. So adding a remap re-resolves, exactly like changing a CSV path.
- 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__.
- 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.
- 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])
- rehydrate(identity, payload)[source]#
Re-apply the outbound map, or replay reports provider spellings.
- Return type:
- Parameters:
identity (PartIdentity)