csv_parts module#

CsvParts#

Resolve passives from a CSV of approved parts. Name it in a chain as a path, or as a resource inside an installed package:

[tool.jitx.parts]
chain = ["csv:parts/approved-parts.csv"]

PartSource([CsvParts("parts/approved-parts.csv")])
PartSource([CsvParts.from_package("acme_parts", "approved.csv")])

The header is the vocabulary. category, mpn and manufacturer are identity, datasheet is reported but not queryable, and every other column becomes a queryable attribute under its own name:

category,mpn,manufacturer,resistance,case,tolerance,price
resistor,RC0402FR-0710KL,Yageo,10000,0402,0.01,0.0032
resistor,ERA-2AEB103X,Panasonic,10 kohm,0402,0.001,0.0900

A dimensioned column takes a bare number or one carrying its own unit. A bare number is given an engineering prefix, so it reaches the schematic as 10 kohm rather than 10000 ohm; a written unit is used as written. Adding a column is all it takes to filter or rank on it, here or in a design.

Being column-driven rather than hardcoded is also what makes this the one to read first when writing another provider: the vocabulary comes from the data, so what is left in the file is the protocol and nothing else.

class CsvPart(identity, payload)[source]#

Bases: object

A resolved part from a CSV row. Implements ResolvedPart.

Parameters:
identity: PartIdentity#
payload: dict[str, JSON]#
materialize(component)[source]#

Attach a landpattern and symbol from the standard generators.

Return type:

None

Parameters:

component (Any)

class CsvParts(path, root=None, package=None)[source]#

Bases: CategoryProvider

Resolve passives from a CSV of approved parts.

category, mpn and manufacturer are consumed as identity and datasheet as payload; every other column becomes an attribute under its own name, so the file’s header is the vocabulary. Dimensioned columns are parsed into quantities.

A row is a candidate when match says so, and candidates are ordered by the query’s rank. Filtering and ordering cover every column the file has, which is the provider invariant: whatever it exposes, it serves.

Parameters:
  • path (str)

  • root (str | None)

  • package (str | None)

path: str#

The file, as written. Relative paths are resolved against root.

root: str | None = None#

Directory path is relative to, when it came from project config.

Deliberately absent from config(), and so from the chain digest: where a project happens to be checked out is not a design decision, and baking an absolute path into the digest would give two developers different chains for the same project and never let the lock match.

package: str | None = None#

Installed package path is a resource of, for a catalogue shipped by a company-wide pip package. Read through importlib.resources, so it works from a wheel, an editable install or a zip import alike.

Unlike root, this one is in config(): which package a catalogue comes from is a design decision, where its files happen to land on disk is not.

name: str#

` and the file’s basename, or the package and basename for a resource.

Two catalogues with the same basename in different directories collide, which is legal; PartSource.names() suffixes duplicates positionally.

Type:

`csv

classmethod from_package(package, resource)[source]#

A catalogue shipped inside an installed package.

The spelling for a company-wide approved-parts list: publish a chain from the package that carries the CSV, and name the file as a resource rather than as a path.

PartSource([CsvParts.from_package(“acme_parts”, “approved.csv”)])

The package name and resource reach the chain digest; the filesystem location does not, so every developer and every CI box computes the same chain for the same installed version.

Parameters:
  • package (str)

  • resource (str)

config()[source]#

What identifies this catalogue, and nothing that does not.

The path or resource name, and the package when there is one: changing either is a decision. root is excluded, and so is wherever a package resolves to on disk, so the digest is the same on every machine.

The package’s version is excluded too, deliberately. A chain published by a package is pinned by dependency version, not by the lockfile; asking the lock to defend against an upgrade as well would pin the same thing twice, in two files, with no way to tell which is authoritative.

Return type:

dict[str, JSON]

property file: Any#

a package resource, or a filesystem path.

A Traversable in the package case rather than a Path, since the file need not be on the filesystem at all.

Type:

Where to read from

resolve_one(query)[source]#

Answer one query. config and rehydrate are still yours to write.

Return type:

Resolved | NoMatch | Unsupported | Deny

Parameters:

query (CanonicalQuery)

distinct(query, key)[source]#

Values of key among matching rows, in sorted order.

Return type:

Sequence[object]

Parameters:
rehydrate(identity, payload)[source]#

Rebuild a part from a lockfile entry.

The payload carries its own version, so this provider can disown a payload it no longer understands rather than the core guessing.

Return type:

ResolvedPart

Parameters: