Source code for jitxlib.parts.query_api

"""
Query API for the JITX parts database.

This module provides a Python implementation of the query builder API
for the JITX parts database, translating the Stanza query-api.stanza to Python.
"""

from __future__ import annotations
import os
from dataclasses import dataclass, fields
import logging
from enum import Enum
from collections.abc import Sequence
from typing import (
    Self,
    TypedDict,
    Unpack,
    cast,
)

import jitx
import jitx.container
import jitx.inspect
from jitx.interval import Interval
import jitx.context
import jitx.units
import jitx._structural
from jitx import current


from .commands import PartJSON, dbquery, QueryParamValue
from ._types import ClassName
from ._types.component import (
    Part as PartType,
    Category,
    ComponentCode,
    ResistorEModel,
    InductorEModel,
    CapacitorEModel,
)
from ._types.resistor import Resistor as ResistorType
from ._types.capacitor import Capacitor as CapacitorType
from ._types.inductor import Inductor as InductorType
from ._types.main import to_component, download_model3d_files
from ._build import (
    DBLandpattern,
    create_dbsymbol,
    build_two_pin_mappings,
    build_generic_mappings,
    _set_pin_on_object,
)
from ._sanitize import (
    sanitize_pin,
    python_component_name,
    python_landpattern_name,
)
from ._types.landpattern import PinByNameCode
from .convert import insert_model3d_dir


# Configure logging
logging = logging.getLogger("jitx_parts_database.query_api")


# Special marker class to denote using distinct on a field
[docs] class FindDistinctType: """Marker class for FIND_DISTINCT functionality. Use the FIND_DISTINCT constant instead of creating instances directly. """ def __repr__(self) -> str: return "FIND_DISTINCT" def __str__(self) -> str: return "FIND_DISTINCT"
# Special marker instance to denote using distinct on a field FIND_DISTINCT = FindDistinctType()
[docs] class AuthorizedVendor(Enum): """Authorized vendors for electronic components.""" JLCPCB = "JLCPCB" LCSC = "LCSC" DigiKey = "DigiKey" Future = "Future" Mouser = "Mouser" Arrow = "Arrow" Avnet = "Avnet" Newark = "Newark"
[docs] class SortDir(Enum): """Sort direction options.""" INCREASING = "increasing" DECREASING = "decreasing"
[docs] @dataclass(frozen=True) class SortKey: """Sort key for database queries.""" key: str direction: SortDir
[docs] @dataclass(frozen=True) class DistinctKey: """Distinct key for database queries.""" key: str
[docs] @dataclass(frozen=True) class ExistKeys: """Existence keys for database queries.""" keys: Sequence[str]
[docs] class FindOptimum(Enum): """Find optimum options.""" FIND_MAXIMUM = "find_maximum" FIND_MINIMUM = "find_minimum"
[docs] class PartQueryDict(TypedDict, total=False): trust: str | Sequence[str] | FindDistinctType | None category: str | Sequence[str] | FindDistinctType | None mpn: str | Sequence[str] | FindDistinctType | None mounting: str | Sequence[str] | FindDistinctType | None manufacturer: str | Sequence[str] | FindDistinctType | None description: str | Sequence[str] | FindDistinctType | None case: str | Sequence[str] | FindDistinctType | None min_stock: int | FindDistinctType | None quantity_needed: int | FindDistinctType | None price: float | Interval | FindOptimum | Sequence[float] | FindDistinctType | None x: float | Interval | Sequence[float] | FindDistinctType | None y: float | Interval | Sequence[float] | FindDistinctType | None z: float | Interval | Sequence[float] | FindDistinctType | None area: float | Interval | FindOptimum | Sequence[float] | FindDistinctType | None rated_temperature_min: float | Interval | Sequence[float] | FindDistinctType | None rated_temperature_max: float | Interval | Sequence[float] | FindDistinctType | None operating_temperature: Interval | FindDistinctType | None stock: int | FindDistinctType | None sellers: Sequence[str | AuthorizedVendor] | FindDistinctType | None sort: SortKey | Sequence[SortKey] | FindDistinctType | None exist: ExistKeys | FindDistinctType | None distinct: DistinctKey | FindDistinctType | None ignore_stock: bool | FindDistinctType | None
[docs] class PassiveQueryDict(PartQueryDict, total=False): type: str | Sequence[str] | FindDistinctType | None tolerance: float | jitx.units.PlainQuantity | FindDistinctType | None precision: float | jitx.units.PlainQuantity | FindDistinctType | None tolerance_min: float | Interval | FindDistinctType | None tolerance_max: float | Interval | FindDistinctType | None component_datasheet: str | Sequence[str] | FindDistinctType | None metadata_image: str | Sequence[str] | FindDistinctType | None metadata_digi_key_part_number: str | Sequence[str] | FindDistinctType | None metadata_description: str | Sequence[str] | FindDistinctType | None metadata_packaging: str | Sequence[str] | FindDistinctType | None
[docs] class ResistorQueryDict(PassiveQueryDict, total=False): resistance: ( float | jitx.units.PlainQuantity | Interval | Sequence[float] | FindDistinctType | None ) rated_power: float | Interval | Sequence[float] | FindDistinctType | None composition: str | FindDistinctType | None tcr_pos: float | Interval | FindDistinctType | None tcr_neg: float | Interval | FindDistinctType | None metadata_series: str | FindDistinctType | None metadata_features: str | FindDistinctType | None metadata_supplier_device_package: str | FindDistinctType | None metadata_number_of_terminations: int | FindDistinctType | None
[docs] class CapacitorQueryDict(PassiveQueryDict, total=False): capacitance: float | jitx.units.PlainQuantity | Interval | FindDistinctType | None anode: str | FindDistinctType | None electrolyte: str | FindDistinctType | None esr: float | Interval | FindDistinctType | None esr_frequency: float | Interval | FindDistinctType | None rated_voltage: float | Interval | FindDistinctType | None rated_voltage_ac: float | Interval | FindDistinctType | None rated_current_pk: float | Interval | FindDistinctType | None rated_current_rms: float | Interval | FindDistinctType | None temperature_coefficient_code: str | FindDistinctType | None temperature_coefficient_raw_data: str | FindDistinctType | None temperature_coefficient_tolerance: float | FindDistinctType | None temperature_coefficient_lower_temperature: float | FindDistinctType | None temperature_coefficient_upper_temperature: float | FindDistinctType | None temperature_coefficient_change: float | FindDistinctType | None metadata_lifetime_temp: float | Interval | FindDistinctType | None metadata_applications: str | FindDistinctType | None metadata_ripple_current_low_frequency: float | Interval | FindDistinctType | None metadata_ripple_current_high_frequency: float | Interval | FindDistinctType | None metadata_lead_spacing: float | Interval | FindDistinctType | None
[docs] class InductorQueryDict(PassiveQueryDict, total=False): inductance: float | jitx.units.PlainQuantity | Interval | FindDistinctType | None material_core: str | FindDistinctType | None shielding: str | FindDistinctType | None current_rating: float | Interval | FindDistinctType | None saturation_current: float | Interval | FindDistinctType | None dc_resistance: float | Interval | FindDistinctType | None quality_factor: float | Interval | FindDistinctType | None quality_factor_frequency: float | Interval | FindDistinctType | None self_resonant_frequency: float | Interval | FindDistinctType | None
[docs] @dataclass(frozen=True, kw_only=True) class PartQuery(jitx.context.Context): """Context for part queries.""" trust: str | Sequence[str] | FindDistinctType | None = None category: str | Sequence[str] | FindDistinctType | None = None mpn: str | Sequence[str] | FindDistinctType | None = None mounting: str | Sequence[str] | FindDistinctType | None = None manufacturer: str | Sequence[str] | FindDistinctType | None = None description: str | Sequence[str] | FindDistinctType | None = None case: str | Sequence[str] | FindDistinctType | None = None min_stock: int | FindDistinctType | None = None quantity_needed: int | FindDistinctType | None = None price: ( float | Interval | FindOptimum | Sequence[float] | FindDistinctType | None ) = None x: float | Interval | Sequence[float] | FindDistinctType | None = None y: float | Interval | Sequence[float] | FindDistinctType | None = None z: float | Interval | Sequence[float] | FindDistinctType | None = None area: float | Interval | FindOptimum | Sequence[float] | FindDistinctType | None = ( None ) rated_temperature_min: ( float | Interval | Sequence[float] | FindDistinctType | None ) = None rated_temperature_max: ( float | Interval | Sequence[float] | FindDistinctType | None ) = None # more-intuitive shortcut for specifying the above two operating_temperature: Interval | FindDistinctType | None = None stock: int | FindDistinctType | None = None sellers: Sequence[str | AuthorizedVendor] | FindDistinctType | None = None sort: SortKey | Sequence[SortKey] | FindDistinctType | None = None exist: ExistKeys | FindDistinctType | None = None distinct: DistinctKey | FindDistinctType | None = None ignore_stock: bool | FindDistinctType | None = None
[docs] @classmethod def from_query(cls, query: PartQuery) -> PartQuery: """Create a PartQuery from a PartQuery.""" # Filter params to only include fields defined in this class valid_fields = {field.name for field in fields(cls)} all_params = query.params() filtered_params = {k: v for k, v in all_params.items() if k in valid_fields} # Log warning for dropped parameters dropped_params = set(all_params.keys()) - valid_fields if dropped_params: logging.warning( f"Dropped parameters in PartQuery.from_query: {sorted(dropped_params)}" ) return cls(**cast(PartQueryDict, filtered_params))
[docs] def update(self, **kwargs: Unpack[PartQueryDict]) -> PartQuery: """Update the context with new values. Args: **kwargs: Keyword arguments corresponding to dataclass fields Returns: New PartQuery with updated values """ # Validate that all kwargs are valid field names valid_fields = {field.name for field in fields(self)} invalid_fields = set(kwargs.keys()) - valid_fields if invalid_fields: raise ValueError(f"Invalid fields for PartQuery: {invalid_fields}") # type(self) is the constructor for the class (`cls` in classmethods). It may be a subclass of PartQuery. return type(self)(**cast(PartQueryDict, {**self.params(), **kwargs}))
[docs] def params(self) -> PartQueryDict: """Get the parameters for the query.""" return cast( PartQueryDict, {k: v for k, v in self.__dict__.items() if v is not None} )
# Context are immutable ("frozen") because when circuits are instantiated, there is memoization, # the memoization key has all Contexts accessed by the first run of the circuit constructor.
[docs] @dataclass(frozen=True, kw_only=True) class PassiveQuery(PartQuery): """Context for passive queries.""" type: str | Sequence[str] | FindDistinctType | None = None tolerance: float | jitx.units.PlainQuantity | FindDistinctType | None = None precision: float | jitx.units.PlainQuantity | FindDistinctType | None = ( None # stanza version was typed as Percentage (FIXME: review) ) tolerance_min: float | Interval | FindDistinctType | None = None tolerance_max: float | Interval | FindDistinctType | None = None component_datasheet: str | Sequence[str] | FindDistinctType | None = ( None # stanza version was typed as ? (FIXME: review) ) metadata_image: str | Sequence[str] | FindDistinctType | None = ( None # stanza version was typed as ? (FIXME: review) ) metadata_digi_key_part_number: str | Sequence[str] | FindDistinctType | None = None metadata_description: str | Sequence[str] | FindDistinctType | None = None metadata_packaging: str | Sequence[str] | FindDistinctType | None = ( None # stanza version was typed as ? (FIXME: review) )
[docs] @classmethod def from_query(cls, query: PartQuery) -> PassiveQuery: """Create a PassiveQuery from a PartQuery.""" # Filter params to only include fields defined in this class valid_fields = {field.name for field in fields(cls)} all_params = query.params() filtered_params = {k: v for k, v in all_params.items() if k in valid_fields} # Log warning for dropped parameters dropped_params = set(all_params.keys()) - valid_fields if dropped_params: logging.warning( f"Dropped parameters in PassiveQuery.from_query: {sorted(dropped_params)}" ) return cls(**cast(PassiveQueryDict, filtered_params))
[docs] def update(self, **kwargs: Unpack[PassiveQueryDict]) -> PassiveQuery: """Update the context with new values. Args: **kwargs: Keyword arguments corresponding to dataclass fields Returns: New PassiveQuery with updated values """ # Validate that all kwargs are valid field names valid_fields = {field.name for field in fields(self)} invalid_fields = set(kwargs.keys()) - valid_fields if invalid_fields: raise ValueError(f"Invalid fields for PassiveQuery: {invalid_fields}") return type(self)(**cast(PassiveQueryDict, {**self.params(), **kwargs}))
[docs] def params(self) -> PassiveQueryDict: """Get the parameters for the query.""" return cast( PassiveQueryDict, {k: v for k, v in self.__dict__.items() if v is not None} )
[docs] @dataclass(frozen=True, kw_only=True) class ResistorQuery(PassiveQuery): """Context for resistor queries.""" category: str | Sequence[str] | FindDistinctType | None = "resistor" resistance: ( float | jitx.units.PlainQuantity | Interval | Sequence[float] | FindDistinctType | None ) = None rated_power: float | Interval | Sequence[float] | FindDistinctType | None = None composition: str | FindDistinctType | None = None tcr_pos: float | Interval | FindDistinctType | None = None tcr_neg: float | Interval | FindDistinctType | None = None metadata_series: str | FindDistinctType | None = None metadata_features: str | FindDistinctType | None = ( None # stanza version was typed as ? (FIXME: review) ) metadata_supplier_device_package: str | FindDistinctType | None = None metadata_number_of_terminations: int | FindDistinctType | None = None def __post_init__(self): """Validate that category is set to 'resistor'.""" if not (isinstance(self.category, str) and self.category == "resistor"): raise ValueError( f'ResistorQuery category must be "resistor", got {self.category!r}' )
[docs] @classmethod def from_query(cls, query: PartQuery) -> ResistorQuery: """Create a ResistorQuery from a PartQuery.""" # Filter params to only include fields defined in this class valid_fields = {field.name for field in fields(cls)} all_params = query.params() filtered_params = { k: v for k, v in all_params.items() if k in valid_fields and not (k == "category" and v != "resistor") } # Log warning for dropped parameters dropped_params = set(all_params.keys()) - set(filtered_params.keys()) if dropped_params: logging.warning( f"Dropped parameters in ResistorQuery.from_query: {sorted(dropped_params)}" ) return cls(**cast(ResistorQueryDict, filtered_params))
[docs] def update(self, **kwargs: Unpack[ResistorQueryDict]) -> ResistorQuery: """Update the context with new values. Args: **kwargs: Keyword arguments corresponding to dataclass fields Returns: New ResistorQuery with updated values """ # Validate that all kwargs are valid field names valid_fields = {field.name for field in fields(self)} invalid_fields = set(kwargs.keys()) - valid_fields if invalid_fields: raise ValueError(f"Invalid fields for ResistorQuery: {invalid_fields}") return type(self)(**cast(ResistorQueryDict, {**self.params(), **kwargs}))
[docs] @classmethod def refine(cls, **kwargs: Unpack[ResistorQueryDict]) -> ResistorQuery: """Refine an existing context with additional values.""" return cls.require().update(**kwargs)
[docs] def params(self) -> ResistorQueryDict: """Get the parameters for the query.""" return cast( ResistorQueryDict, {k: v for k, v in self.__dict__.items() if v is not None} )
[docs] @dataclass(frozen=True, kw_only=True) class CapacitorQuery(PassiveQuery): """Context for capacitor queries.""" category: str | Sequence[str] | FindDistinctType | None = "capacitor" capacitance: ( float | jitx.units.PlainQuantity | Interval | FindDistinctType | None ) = None anode: str | FindDistinctType | None = None electrolyte: str | FindDistinctType | None = None esr: float | Interval | FindDistinctType | None = None esr_frequency: float | Interval | FindDistinctType | None = None rated_voltage: float | Interval | FindDistinctType | None = None rated_voltage_ac: float | Interval | FindDistinctType | None = None rated_current_pk: float | Interval | FindDistinctType | None = None rated_current_rms: float | Interval | FindDistinctType | None = None temperature_coefficient_code: str | FindDistinctType | None = ( None # stanza version was 'temperature-coefficient_code' and typed as ? (FIXME: review) ) temperature_coefficient_raw_data: str | FindDistinctType | None = ( None # stanza version was 'temperature-coefficient_raw-data' and typed as ? (FIXME: review). This is user-facing for 'temperature-coefficient.raw_data'. ) temperature_coefficient_tolerance: float | FindDistinctType | None = ( None # stanza version was 'temperature-coefficient_tolerance' and typed as ? (FIXME: review) ) temperature_coefficient_lower_temperature: float | FindDistinctType | None = ( None # stanza version was 'temperature-coefficient_lower-temperature' and typed as ? (FIXME: review) ) temperature_coefficient_upper_temperature: float | FindDistinctType | None = ( None # stanza version was 'temperature-coefficient_upper-temperature' and typed as ? (FIXME: review) ) temperature_coefficient_change: float | FindDistinctType | None = ( None # stanza version was 'temperature-coefficient_change' and typed as ? (FIXME: review) ) metadata_lifetime_temp: float | Interval | FindDistinctType | None = None metadata_applications: str | FindDistinctType | None = ( None # stanza version was typed as ? (FIXME: review) ) metadata_ripple_current_low_frequency: ( float | Interval | FindDistinctType | None ) = None metadata_ripple_current_high_frequency: ( float | Interval | FindDistinctType | None ) = None metadata_lead_spacing: float | Interval | FindDistinctType | None = None def __post_init__(self): """Validate that category is set to 'capacitor'.""" if not (isinstance(self.category, str) and self.category == "capacitor"): raise ValueError( f'CapacitorQuery category must be "capacitor", got {self.category!r}' )
[docs] @classmethod def from_query(cls, query: PartQuery) -> CapacitorQuery: """Create a CapacitorQuery from a PartQuery.""" # Filter params to only include fields defined in this class valid_fields = {field.name for field in fields(cls)} all_params = query.params() filtered_params = { k: v for k, v in all_params.items() if k in valid_fields and not (k == "category" and v != "capacitor") } # Log warning for dropped parameters dropped_params = set(all_params.keys()) - set(filtered_params.keys()) if dropped_params: logging.warning( f"Dropped parameters in CapacitorQuery.from_query: {sorted(dropped_params)}" ) return cls(**cast(CapacitorQueryDict, filtered_params))
[docs] def update(self, **kwargs: Unpack[CapacitorQueryDict]) -> CapacitorQuery: """Update the context with new values. Args: **kwargs: Keyword arguments corresponding to dataclass fields Returns: New CapacitorQuery with updated values """ # Validate that all kwargs are valid field names valid_fields = {field.name for field in fields(self)} invalid_fields = set(kwargs.keys()) - valid_fields if invalid_fields: raise ValueError(f"Invalid fields for CapacitorQuery: {invalid_fields}") return type(self)(**cast(CapacitorQueryDict, {**self.params(), **kwargs}))
[docs] @classmethod def refine(cls, **kwargs: Unpack[CapacitorQueryDict]) -> CapacitorQuery: """Refine an existing context with additional values.""" return cls.require().update(**kwargs)
[docs] def params(self) -> CapacitorQueryDict: """Get the parameters for the query.""" return cast( CapacitorQueryDict, {k: v for k, v in self.__dict__.items() if v is not None}, )
[docs] @dataclass(frozen=True, kw_only=True) class InductorQuery(PassiveQuery): """Context for inductor queries.""" category: str | Sequence[str] | FindDistinctType | None = "inductor" inductance: ( float | jitx.units.PlainQuantity | Interval | FindDistinctType | None ) = None material_core: str | FindDistinctType | None = None shielding: str | FindDistinctType | None = None current_rating: float | Interval | FindDistinctType | None = None saturation_current: float | Interval | FindDistinctType | None = None dc_resistance: float | Interval | FindDistinctType | None = None quality_factor: float | Interval | FindDistinctType | None = None quality_factor_frequency: float | Interval | FindDistinctType | None = None self_resonant_frequency: float | Interval | FindDistinctType | None = None def __post_init__(self): """Validate that category is set to 'inductor'.""" if not (isinstance(self.category, str) and self.category == "inductor"): raise ValueError( f'InductorQuery category must be "inductor", got {self.category!r}' )
[docs] @classmethod def from_query(cls, query: PartQuery) -> InductorQuery: """Create an InductorQuery from a PartQuery.""" # Filter params to only include fields defined in this class valid_fields = {field.name for field in fields(cls)} all_params = query.params() filtered_params = { k: v for k, v in all_params.items() if k in valid_fields and not (k == "category" and v != "inductor") } # Log warning for dropped parameters dropped_params = set(all_params.keys()) - set(filtered_params.keys()) if dropped_params: logging.warning( f"Dropped parameters in InductorQuery.from_query: {sorted(dropped_params)}" ) return cls(**cast(InductorQueryDict, filtered_params))
[docs] def update(self, **kwargs: Unpack[InductorQueryDict]) -> InductorQuery: """Update the context with new values. Args: **kwargs: Keyword arguments corresponding to dataclass fields Returns: New InductorQuery with updated values """ # Validate that all kwargs are valid field names valid_fields = {field.name for field in fields(self)} invalid_fields = set(kwargs.keys()) - valid_fields if invalid_fields: raise ValueError(f"Invalid fields for InductorQuery: {invalid_fields}") return type(self)(**cast(InductorQueryDict, {**self.params(), **kwargs}))
[docs] @classmethod def refine(cls, **kwargs: Unpack[InductorQueryDict]) -> InductorQuery: """Refine an existing context with additional values.""" return cls.require().update(**kwargs)
[docs] def params(self) -> InductorQueryDict: """Get the parameters for the query.""" return cast( InductorQueryDict, {k: v for k, v in self.__dict__.items() if v is not None} )
[docs] class TwoPinShortTrace(Enum): """Short trace options for two-pin components.""" SHORT_TRACE_BOTH = "short_trace_both" SHORT_TRACE_ANODE = "short_trace_anode" SHORT_TRACE_CATHODE = "short_trace_cathode" SHORT_TRACE_NEITHER = "short_trace_neither"
# Key name overrides _key_overrides = { "esr-frequency": "esr_frequency", "temperature-coefficient_raw-data": "temperature-coefficient.raw_data", "quantity-needed": "max-minimum_quantity", "stock!": "_stock", # Those were handled as overrides in stanza but are now handled in extract. # "sellers!": "_sellers", # "sort!": "_sort", # "exist!": "_exist", # "distinct!": "_distinct", }
[docs] def to_db_key(key: str) -> str: """Convert a Python key to the correct database key, applying underscore-to-dot mapping and overrides.""" if key in _key_overrides: return _key_overrides[key] return key.replace("_", ".")
# Map python keyword argument names to stanza keywords _key_mapping = { "trust": "trust", "category": "category", "mpn": "mpn", "mounting": "mounting", "manufacturer": "manufacturer", "description": "description", "case": "case", "min_stock": "min-stock", "quantity_needed": "quantity-needed", "price": "price", "x": "x", "y": "y", "z": "z", "area": "area", "rated_temperature_min": "rated-temperature_min", "rated_temperature_max": "rated-temperature_max", "operating_temperature": "operating-temperature", "stock": "stock!", "sellers": "sellers!", "sort": "sort!", "exist": "exist!", "distinct": "distinct!", "ignore_stock": "ignore-stock", # Passive keys "type": "type", "tolerance": "tolerance", "precision": "precision", "tolerance_min": "tolerance_min", "tolerance_max": "tolerance_max", "component_datasheet": "component_datasheet", "metadata_image": "metadata_image", "metadata_digi_key_part_number": "metadata_digi-key-part-number", "metadata_description": "metadata_description", "metadata_packaging": "metadata_packaging", # Resistor keys "resistance": "resistance", "rated_power": "rated-power", "composition": "composition", "tcr_pos": "tcr_pos", "tcr_neg": "tcr_neg", "metadata_series": "metadata_series", "metadata_features": "metadata_features", "metadata_supplier_device_package": "metadata_supplier-device-package", "metadata_number_of_terminations": "metadata_number-of-terminations", # Capacitor keys "capacitance": "capacitance", "anode": "anode", "electrolyte": "electrolyte", "esr": "esr", "esr_frequency": "esr-frequency", "rated_voltage": "rated-voltage", "rated_voltage_ac": "rated-voltage-ac", "rated_current_pk": "rated-current-pk", "rated_current_rms": "rated-current-rms", "temperature_coefficient_code": "temperature-coefficient_code", "temperature_coefficient_raw_data": "temperature-coefficient_raw-data", "temperature_coefficient_tolerance": "temperature-coefficient_tolerance", "temperature_coefficient_lower_temperature": "temperature-coefficient_lower-temperature", "temperature_coefficient_upper_temperature": "temperature-coefficient_upper-temperature", "temperature_coefficient_change": "temperature-coefficient_change", "metadata_lifetime_temp": "metadata_lifetime-temp", "metadata_applications": "metadata_applications", "metadata_ripple_current_low_frequency": "metadata_ripple-current-low-frequency", "metadata_ripple_current_high_frequency": "metadata_ripple-current-high-frequency", "metadata_lead_spacing": "metadata_lead-spacing", # Inductor keys "inductance": "inductance", "material_core": "material-core", "shielding": "shielding", "current_rating": "current-rating", "saturation_current": "saturation-current", "dc_resistance": "dc-resistance", "quality_factor": "quality-factor", "quality_factor_frequency": "quality-factor-frequency", "self_resonant_frequency": "self-resonant-frequency", } ProcessedQueryParamValue = ( DistinctKey | ExistKeys | Interval | SortKey | Sequence[SortKey] | bool | float | int | str )
[docs] def preprocess_keyword_args( kwargs: PartQueryDict, ) -> dict[str, ProcessedQueryParamValue]: """Preprocess keyword arguments for queries. Args: kwargs: Keyword arguments to preprocess Returns: Preprocessed arguments """ result = {} # There can only be one FIND_DISTINCT parameter, otherwise raise an error. distinct_params = [ key for key, value in kwargs.items() if isinstance(value, FindDistinctType) ] if len(distinct_params) > 1: raise ValueError( "There can only be one FIND_DISTINCT parameter. Got: {distinct_params}" ) # There can only be one FIND_OPTIMUM parameter, otherwise raise an error. find_optimum_params = [ key for key, value in kwargs.items() if isinstance(value, FindOptimum) ] if len(find_optimum_params) > 1: raise ValueError( "There can only be one FIND_OPTIMUM parameter. Got: {find_optimum_params}" ) # There cannot be both tolerance and precision, otherwise raise an error. if kwargs.get("tolerance") is not None and kwargs.get("precision") is not None: raise ValueError( "Cannot specify both 'tolerance' and 'precision' in Parts DB parameters." ) # Special handling for FindOptimum and FindDistinct for original_key, value in kwargs.items(): if original_key not in _key_mapping: raise ValueError(f"Unknown Parts DB query parameter: {original_key}") # Special handling for FIND_DISTINCT if isinstance(value, FindDistinctType): result["distinct!"] = DistinctKey(original_key) # Special handling for FindOptimum elif isinstance(value, FindOptimum): direction = ( SortDir.DECREASING if value == FindOptimum.FIND_MAXIMUM else SortDir.INCREASING ) result["sort!"] = SortKey(original_key, direction) # Special handling for precision (converts to tolerance) elif original_key == "precision": result["tolerance"] = value # Normal case else: key = _key_mapping[original_key] result[key] = value return result
[docs] def extract(qb: PartQuery) -> dict[str, QueryParamValue]: """Extract query parameters for sending to the database. Args: qb: Query context to extract parameters from Returns: Extracted parameters """ params = {} # Preprocess keyword arguments all_params = preprocess_keyword_args(qb.params()) # Handle special keys and transformations for key, value in all_params.items(): db_key = to_db_key(key) if db_key == "operating-temperature": # Cannot specify both operating-temperature and rated-temperature if ( "rated-temperature_min" in all_params or "rated-temperature_max" in all_params ): raise ValueError( "Cannot specify both 'rated-temperature' and 'operating-temperature' keys" ) # Extract min and max from interval if not isinstance(value, Interval): raise ValueError( f"Invalid non-Interval value for 'operating-temperature': {value}" ) if value.min_value is not None: params["min-rated-temperature_min"] = value.min_value if value.max_value is not None: params["max-rated-temperature_max"] = value.max_value elif db_key == "sellers!": if isinstance(value, Sequence): params["_sellers"] = [ e.value if isinstance(e, AuthorizedVendor) else e for e in value ] elif db_key == "distinct!": if isinstance(value, DistinctKey): params["_distinct"] = to_db_key(_key_mapping[value.key]) elif db_key == "exist!": if isinstance(value, ExistKeys): params["_exist"] = [to_db_key(_key_mapping[k]) for k in value.keys] elif db_key == "sort!": sort_keys: Sequence[SortKey] if isinstance(value, SortKey): sort_keys = (value,) # NOTE: "not isinstance(value, str)" is needed so type checkers do not think 'value' could be 'str' # and then fails at 'sk.key' later. elif ( isinstance(value, Sequence) and not isinstance(value, str) and all(isinstance(v, SortKey) for v in value) ): sort_keys = value else: raise ValueError(f"Invalid sort value: {value}") # Format sort keys sort_strings = [] for sk in sort_keys: db_sort_key = to_db_key(_key_mapping[sk.key]) prefix = "" if sk.direction == SortDir.INCREASING else "-" sort_strings.append(f"{prefix}{db_sort_key}") params["_sort"] = sort_strings # Special handling for price sorting if any(sk.key == "price" for sk in sort_keys) and not any( k in ["stock!", "_stock"] for k in all_params.keys() ): params["_stock"] = 1 # Handle general case else: # Handle intervals if isinstance(value, Interval): if value.min_value is not None: params[f"min-{db_key}"] = value.min_value if value.max_value is not None: params[f"max-{db_key}"] = value.max_value else: params[db_key] = value # Special handling for ignore-stock if "ignore-stock" in all_params and all_params["ignore-stock"] is True: if "min-stock" in params: del params["min-stock"] if "_stock" in params: del params["_stock"] return params
SMD_PKGS = ( "009005", "0301m", "01005", "0402m", "0201", "0603m", "0202", "0606m", "0204", "0510m", "Wide 0402", "0306", "0816m", "Wide 0603", "0402", "1005m", "0505", "1414m", "0508", "1220m", "Wide 0805", "0603", "1608m", "0612", "1632m", "Wide 1206", "0805", "2012m", "1111", "2828m", "1206", "3216m", "1210", "3225m", "1218", "3246m", "Wide 1812", "1225", "3263m", "Wide 2512", "1530", "3876m", "Wide 3015", "1808", "4520m", "1812", "4532m", "1825", "4564m", "1835", "4589", "Wide 3518", "5020m", "2010", "5025m", "2043", "Wide 4320", "2220", "5750m", "2225", "5763m", "2312", "6032m", "2512", "6331m", "2725", "7142m", "2728", "7142m", "Wide 2827", "2816", "2817", "7142m", "2953", "Wide 5929", "3920", "1052m", )
[docs] def is_valid_smd_pkg(pkg: str) -> bool: """Check if a package is a valid SMD package.""" return pkg in SMD_PKGS
# Helper functions for valid packages
[docs] def valid_smd_pkgs(min_pkg: str = "0402") -> Sequence[str]: """Get a list of valid SMD packages. Args: min_pkg: Minimum package size Returns: List of valid SMD packages """ if not is_valid_smd_pkg(min_pkg): raise ValueError( f"Unknown SMD package requested as minimum: {min_pkg}. The known packages are:\n{SMD_PKGS}" ) start_idx = SMD_PKGS.index(min_pkg) return SMD_PKGS[start_idx:]
# Top-level query builder functions
[docs] def make_resistor_query( qb: PartQuery | None = None, **kwargs: Unpack[ResistorQueryDict] ) -> ResistorQuery: """Make a ResistorQuery from a PartQuery or ResistorQuery. Args: qb: Optional base query context **kwargs: Additional query parameters Returns: ResistorQuery with the specified parameters """ base_query = qb or ResistorQuery.get() or PartQuery.get() if isinstance(base_query, ResistorQuery): return base_query.update(**kwargs) elif isinstance(base_query, PartQuery): return ResistorQuery.from_query(base_query).update(**kwargs) else: return ResistorQuery(**kwargs)
[docs] def make_capacitor_query( qb: PartQuery | None = None, **kwargs: Unpack[CapacitorQueryDict] ) -> CapacitorQuery: """Make a CapacitorQuery from a PartQuery or CapacitorQuery. Args: qb: Optional base query context **kwargs: Additional query parameters Returns: CapacitorQuery with the specified parameters """ base_query = qb or CapacitorQuery.get() or PartQuery.get() if isinstance(base_query, CapacitorQuery): return base_query.update(**kwargs) elif isinstance(base_query, PartQuery): return CapacitorQuery.from_query(base_query).update(**kwargs) else: return CapacitorQuery(**kwargs)
[docs] def make_inductor_query( qb: PartQuery | None = None, **kwargs: Unpack[InductorQueryDict] ) -> InductorQuery: """Make an InductorQuery from a PartQuery or InductorQuery. Args: qb: Optional base query context **kwargs: Additional query parameters Returns: InductorQuery with the specified parameters """ base_query = qb or InductorQuery.get() or PartQuery.get() if isinstance(base_query, InductorQuery): return base_query.update(**kwargs) elif isinstance(base_query, PartQuery): return InductorQuery.from_query(base_query).update(**kwargs) else: return InductorQuery(**kwargs)
[docs] def make_part_query( qb: PartQuery | None = None, **kwargs: Unpack[PartQueryDict] ) -> PartQuery: """Make a PartQuery from a PartQuery. Args: qb: Optional base query context **kwargs: Additional query parameters Returns: PartQuery with the specified parameters """ base_query = qb or PartQuery.get() if isinstance(base_query, PartQuery): return base_query.update(**kwargs) else: return PartQuery(**kwargs)
# Internal functions for creating components def _internal_create_components(qb: PartQuery, limit: int) -> Sequence[PartType]: """Internal function to create components from a query. Args: qb: Query context limit: Maximum number of components to create Returns: List of components Raises: ValueError: If no components meet the requirements """ params = extract(qb) results: Sequence[PartJSON] = dbquery(params, limit) if not results: raise ValueError(f"No components meeting requirements: {params}") return [to_component(result) for result in results] def _internal_create_component(qb: PartQuery) -> PartType: """Internal function to create a single component from a query. Args: qb: Query context Returns: PartType Raises: ValueError: If no components meet the requirements """ comps = _internal_create_components(qb, 1) if not comps: raise ValueError("Component list is empty") return comps[0] # Public API for creating components
[docs] class InsertContainer(jitx.container.Container): nets: list[jitx.Net] a_short_trace: jitx.net.ShortTrace | None c_short_trace: jitx.net.ShortTrace | None
def _set_metadata(comp: jitx.Component, cc: ComponentCode) -> None: """Set common metadata fields on a component from ComponentCode.""" comp.mpn = cc.mpn comp.manufacturer = cc.manufacturer comp.reference_designator_prefix = cc.reference_prefix comp.datasheet = cc.datasheet def _build_landpattern(cc: ComponentCode) -> tuple[ComponentCode, DBLandpattern]: """Build landpattern with model3d handling from ComponentCode. Returns the (possibly mutated) ComponentCode and the DBLandpattern. Must be absolute — Model3D resolves relative paths from its own source file, not cwd. Creates a dynamic subclass so the instance's class __name__ matches the landpattern name. """ model3d_dir = os.path.join(os.getcwd(), "3d-models") cc = insert_model3d_dir(cc, model3d_dir) failed_3d = download_model3d_files(cc) assert cc.landpattern is not None lp_class_name = python_landpattern_name(cc.landpattern.name) lp = DBLandpattern(cc, missing_3d_models=failed_3d) ClassName(lp_class_name).assign(lp) return cc, lp # Note from Philippe : yeah... this is neat and all but delete if it is not robust and it breaks. def _set_component_class_name(comp: jitx.Component, cc: ComponentCode) -> None: """Set the component's class to a dynamic subclass so __name__ matches the part. Uses passivate() to create the class (prevents __init_subclass__ from triggering magic_context on Instantiable descriptors), then registers a postprocessor to change __class__ after the prepost type(self)-is-cls check has already passed — a direct assignment inside __init__ would break Component's @early/@late handlers. """ comp_class_name = python_component_name(cc.mpn, cc.name) ClassName(comp_class_name).assign(comp) def _validate_percent(value: object, name: str) -> None: """Assert that a quantity is a dimensionless percentage.""" if isinstance(value, jitx.units.PlainQuantity): assert (value.dimensionality, value.units) == ( jitx.units.percent.dimensionality, jitx.units.percent, ), ( f"{name} Quantity requested by the user from the Parts DB " f"only support a dimensionless percentage, got: '{value.units}'." )
[docs] class Resistor(jitx.Component): """Resistor component. Args: query: query to optionally override the current ResistorQuery or PartQuery from the DesignContext. comp_name: name of the component to be created. **kwargs: Additional query parameters Raises: Exception: If no components meet the requirements """ mpn: str manufacturer: str datasheet: str reference_designator_prefix: str value: jitx.units.PlainQuantity p1: jitx.Port p2: jitx.Port landpattern: jitx.Landpattern symbol: jitx.Symbol cmappings: list[jitx.PadMapping | jitx.SymbolMapping] data: ResistorType def __init__( self, query: PartQuery | None = None, *, comp_name: str | None = None, **kwargs: Unpack[ResistorQueryDict], ): rq = make_resistor_query(query, **kwargs) if isinstance(rq.resistance, jitx.units.PlainQuantity): assert rq.resistance.dimensionality == jitx.units.ohm.dimensionality, ( f"Resistance Quantity requested by the user from the Parts DB is not in a compatible unit: '{rq.resistance.units}'." ) _validate_percent(rq.tolerance, "Tolerance") _validate_percent(rq.precision, "Precision") try: part = _internal_create_component(rq) if not isinstance(part, ResistorType): raise TypeError("Component returned is not a Resistor") self.data = part cc = part.component _set_metadata(self, cc) _set_component_class_name(self, cc) if isinstance(rq.resistance, jitx.units.PlainQuantity): self.value = rq.resistance else: emodel = cc.emodel.value if cc.emodel else None assert ( isinstance(emodel, ResistorEModel) and emodel.resistance is not None ), "Missing resistance value from Parts DB" self.value = jitx.units.PlainQuantity( emodel.resistance, jitx.units.ohm ).to_compact() from jitxlib.symbols.resistor import ResistorSymbol self.symbol = ResistorSymbol() cc, self.landpattern = _build_landpattern(cc) self.p1 = jitx.Port() self.p2 = jitx.Port() p1, p2, *_ = list(cc.pin_properties.pins) if len(_) != 0: raise ValueError( f"Expected exactly two ports for resistors from the Parts DB, got extras: {_}" ) self.cmappings = build_two_pin_mappings( self, self.landpattern, self.symbol, p1, p2 ) except Exception as e: arg_list = "\n- ".join(f"{k}: {v}" for k, v in extract(rq).items()) logging.error(f"Failed to create resistor for query:\n- {arg_list}\n{e}") raise
[docs] def insert( self, pin_a: jitx.Port | jitx.Net, pin_b: jitx.Port | jitx.Net, *, short_trace: TwoPinShortTrace | bool = TwoPinShortTrace.SHORT_TRACE_NEITHER, ) -> Self: """Insert this resistor between two pins of a circuit. Args: self: Component pin_a: First pin pin_b: Second pin short_trace: Short trace option """ c = InsertContainer() c.nets = [ pin_a + self.p1, pin_b + self.p2, ] st = to_short_trace_enum(short_trace) if ( st == TwoPinShortTrace.SHORT_TRACE_BOTH or st == TwoPinShortTrace.SHORT_TRACE_ANODE ): if not isinstance(pin_a, jitx.Port): raise ValueError( "Cannot make a shortrace with a net. Give a port to Resistor.insert's pin_a." ) c.a_short_trace = jitx.net.ShortTrace(pin_a, self.p1) if ( st == TwoPinShortTrace.SHORT_TRACE_BOTH or st == TwoPinShortTrace.SHORT_TRACE_CATHODE ): if not isinstance(pin_b, jitx.Port): raise ValueError( "Cannot make a shortrace with a net. Give a port to Resistor.insert's pin_b." ) c.c_short_trace = jitx.net.ShortTrace(pin_b, self.p2) circuit = current.circuit circuit += c return self
[docs] class Capacitor(jitx.Component): """Capacitor component. Args: query: query to optionally override the current CapacitorQuery or PartQuery from the DesignContext. comp_name: name of the component to be created. polarized: whether the capacitor is polarized. **kwargs: Additional query parameters Raises: Exception: If no components meet the requirements """ mpn: str manufacturer: str datasheet: str reference_designator_prefix: str value: jitx.units.PlainQuantity p1: jitx.Port p2: jitx.Port landpattern: jitx.Landpattern symbol: jitx.Symbol cmappings: list[jitx.PadMapping | jitx.SymbolMapping] # True if p1 is the anode and p2 is the cathode (pin names a and c in parts db) polarized: bool data: CapacitorType def __init__( self, query: PartQuery | None = None, *, comp_name: str | None = None, polarized: bool = False, **kwargs: Unpack[CapacitorQueryDict], ): rq = make_capacitor_query(query, **kwargs) if isinstance(rq.capacitance, jitx.units.PlainQuantity): assert rq.capacitance.dimensionality == jitx.units.F.dimensionality, ( f"Capacitance Quantity requested by the user from the Parts DB is not in a compatible unit: '{rq.capacitance.units}'." ) _validate_percent(rq.tolerance, "Tolerance") _validate_percent(rq.precision, "Precision") try: part = _internal_create_component(rq) if not isinstance(part, CapacitorType): raise TypeError("Component returned is not a Capacitor") self.data = part cc = part.component _set_metadata(self, cc) _set_component_class_name(self, cc) if isinstance(rq.capacitance, jitx.units.PlainQuantity): self.value = rq.capacitance else: emodel = cc.emodel.value if cc.emodel else None assert ( isinstance(emodel, CapacitorEModel) and emodel.capacitance is not None ), "Missing capacitance value from Parts DB" self.value = jitx.units.PlainQuantity( emodel.capacitance, jitx.units.F ).to_compact() # Detect polarization from pin names (a = anode, c = cathode) pin_name_set = { sanitize_pin(pin.pin).value.pin_name for pin in cc.pin_properties.pins if isinstance(sanitize_pin(pin.pin).value, PinByNameCode) } self.polarized = pin_name_set == {"a", "c"} assert not (polarized and not self.polarized), ( f"The Parts DB returned the non-polarized capacitor {cc.mpn} to a PolarizedCapacitor instantiation. " "PolarizedCapacitor is just a type extension to expose the anode pin as `a` and the cathode pin as `c`. " "It is the responsibility of the user to ensure the returned component is polarized. " "Change or restrict the query to match a polarized component." ) if self.polarized: from jitxlib.symbols.capacitor import PolarizedCapacitorSymbol self.symbol = PolarizedCapacitorSymbol() else: from jitxlib.symbols.capacitor import CapacitorSymbol self.symbol = CapacitorSymbol() cc, self.landpattern = _build_landpattern(cc) self.p1 = jitx.Port() self.p2 = jitx.Port() p1, p2, *_ = list(cc.pin_properties.pins) if len(_) != 0: raise ValueError( f"Expected exactly two ports for capacitors from the Parts DB, got extras: {_}" ) if self.polarized: assert isinstance(p1.pin.value, PinByNameCode) and isinstance( p2.pin.value, PinByNameCode ) if (p1.pin.value.pin_name, p2.pin.value.pin_name) != ("a", "c"): p1, p2 = p2, p1 self.cmappings = build_two_pin_mappings( self, self.landpattern, self.symbol, p1, p2 ) except Exception as e: arg_list = "\n- ".join(f"{k}: {v}" for k, v in extract(rq).items()) logging.error(f"Failed to create capacitor for query:\n- {arg_list}\n{e}") raise
[docs] def insert( self, pin_a: jitx.Port | jitx.Net, pin_b: jitx.Port | jitx.Net, *, short_trace: TwoPinShortTrace | bool = TwoPinShortTrace.SHORT_TRACE_NEITHER, ) -> Self: """Insert this capacitor between two pins of a circuit. Args: self: Component pin_a: First pin pin_b: Second pin short_trace: Short trace option """ c = InsertContainer() c.nets = [ pin_a + self.p1, pin_b + self.p2, ] st = to_short_trace_enum(short_trace) if ( st == TwoPinShortTrace.SHORT_TRACE_BOTH or st == TwoPinShortTrace.SHORT_TRACE_ANODE ): if not isinstance(pin_a, jitx.Port): raise ValueError( "Cannot make a shortrace with a net. Give a port to Capacitor.insert's pin_a." ) c.a_short_trace = jitx.net.ShortTrace(pin_a, self.p1) if ( st == TwoPinShortTrace.SHORT_TRACE_BOTH or st == TwoPinShortTrace.SHORT_TRACE_CATHODE ): if not isinstance(pin_b, jitx.Port): raise ValueError( "Cannot make a shortrace with a net. Give a port to Capacitor.insert's pin_b." ) c.c_short_trace = jitx.net.ShortTrace(pin_b, self.p2) circuit = current.circuit circuit += c return self
[docs] class PolarizedCapacitor(Capacitor): """Polarized capacitor component.""" a: jitx.Port c: jitx.Port def __init__(self, *args, **kwargs): super().__init__(*args, polarized=True, **kwargs) assert self.polarized, ( "PolarizedCapacitor must be polarized, it is not a polarized capacitor in the parts DB." ) # Port aliases self.a = jitx._structural.Proxy.create(self.p1, ref=True) self.c = jitx._structural.Proxy.create(self.p2, ref=True)
[docs] class Inductor(jitx.Component): """Inductor component. Args: query: query to optionally override the current InductorQuery or PartQuery from the DesignContext. comp_name: name of the component to be created. **kwargs: Additional query parameters Raises: Exception: If no components meet the requirements """ mpn: str manufacturer: str datasheet: str reference_designator_prefix: str value: jitx.units.PlainQuantity p1: jitx.Port p2: jitx.Port landpattern: jitx.Landpattern symbol: jitx.Symbol cmappings: list[jitx.PadMapping | jitx.SymbolMapping] data: InductorType def __init__( self, query: PartQuery | None = None, *, comp_name: str | None = None, **kwargs: Unpack[InductorQueryDict], ): rq = make_inductor_query(query, **kwargs) if isinstance(rq.inductance, jitx.units.PlainQuantity): assert rq.inductance.dimensionality == jitx.units.H.dimensionality, ( f"Inductance Quantity requested by the user from the Parts DB is not in a compatible unit: '{rq.inductance.units}'." ) _validate_percent(rq.tolerance, "Tolerance") _validate_percent(rq.precision, "Precision") try: part = _internal_create_component(rq) if not isinstance(part, InductorType): raise TypeError("Component returned is not an Inductor") self.data = part cc = part.component _set_metadata(self, cc) _set_component_class_name(self, cc) if isinstance(rq.inductance, jitx.units.PlainQuantity): self.value = rq.inductance else: emodel = cc.emodel.value if cc.emodel else None assert ( isinstance(emodel, InductorEModel) and emodel.inductance is not None ), "Missing inductance value from Parts DB" self.value = jitx.units.PlainQuantity( emodel.inductance, jitx.units.H ).to_compact() from jitxlib.symbols.inductor import InductorSymbol self.symbol = InductorSymbol() cc, self.landpattern = _build_landpattern(cc) self.p1 = jitx.Port() self.p2 = jitx.Port() p1, p2, *_ = list(cc.pin_properties.pins) if len(_) != 0: raise ValueError( f"Expected exactly two ports for inductors from the Parts DB, got extras: {_}" ) self.cmappings = build_two_pin_mappings( self, self.landpattern, self.symbol, p1, p2 ) except Exception as e: arg_list = "\n- ".join(f"{k}: {v}" for k, v in extract(rq).items()) logging.error(f"Failed to create inductor for query:\n- {arg_list}\n{e}") raise
[docs] def insert( self, pin_a: jitx.Port | jitx.Net, pin_b: jitx.Port | jitx.Net, *, short_trace: TwoPinShortTrace | bool = TwoPinShortTrace.SHORT_TRACE_NEITHER, ) -> Self: """Insert this inductor between two pins of a circuit. Args: self: Component pin_a: First pin pin_b: Second pin short_trace: Short trace option """ c = InsertContainer() c.nets = [ pin_a + self.p1, pin_b + self.p2, ] st = to_short_trace_enum(short_trace) if ( st == TwoPinShortTrace.SHORT_TRACE_BOTH or st == TwoPinShortTrace.SHORT_TRACE_ANODE ): if not isinstance(pin_a, jitx.Port): raise ValueError( "Cannot make a shortrace with a net. Give a port to Inductor.insert's pin_a." ) c.a_short_trace = jitx.net.ShortTrace(pin_a, self.p1) if ( st == TwoPinShortTrace.SHORT_TRACE_BOTH or st == TwoPinShortTrace.SHORT_TRACE_CATHODE ): if not isinstance(pin_b, jitx.Port): raise ValueError( "Cannot make a shortrace with a net. Give a port to Inductor.insert's pin_b." ) c.c_short_trace = jitx.net.ShortTrace(pin_b, self.p2) circuit = current.circuit circuit += c return self
[docs] class Part(jitx.Component): """Part component. Args: query: query to optionally override the current PartQuery from the DesignContext. comp_name: name of the component to be created. **kwargs: Additional query parameters Raises: Exception: If no components meet the requirements """ mpn: str manufacturer: str datasheet: str reference_designator_prefix: str landpattern: jitx.Landpattern symbol: jitx.Symbol | dict[int | str, jitx.Symbol] cmappings: list[jitx.PadMapping | jitx.SymbolMapping] data: PartType def __init__( self, query: PartQuery | None = None, *, comp_name: str | None = None, **kwargs: Unpack[PartQueryDict], ): rq = make_part_query(query, **kwargs) try: part = _internal_create_component(rq) self.data = part cc = part.component _set_metadata(self, cc) _set_component_class_name(self, cc) for pin_prop in cc.pin_properties.pins: _set_pin_on_object(self, pin_prop.pin, jitx.Port()) cc, self.landpattern = _build_landpattern(cc) # Build symbol: use stdlib symbol for known passives, otherwise from data if part.category == Category.RESISTOR: from jitxlib.symbols.resistor import ResistorSymbol self.symbol = ResistorSymbol() elif part.category == Category.INDUCTOR: from jitxlib.symbols.inductor import InductorSymbol self.symbol = InductorSymbol() elif part.category == Category.CAPACITOR: from jitxlib.symbols.capacitor import CapacitorSymbol self.symbol = CapacitorSymbol() elif cc.symbols: if len(cc.symbols) == 1: self.symbol = create_dbsymbol(cc.symbols[0]) else: self.symbol = {sc.bank: create_dbsymbol(sc) for sc in cc.symbols} else: raise ValueError("Component has no symbols") self.cmappings = build_generic_mappings( self, self.landpattern, self.symbol, cc ) except Exception as e: arg_list = "\n- ".join(f"{k}: {v}" for k, v in extract(rq).items()) logging.error(f"Failed to create part for query:\n- {arg_list}\n{e}") raise
[docs] def insert( self, pin_a: jitx.Port | jitx.Net, pin_b: jitx.Port | jitx.Net, *, short_trace: TwoPinShortTrace | bool = TwoPinShortTrace.SHORT_TRACE_NEITHER, ) -> Self: """Insert this part between two pins of a circuit. Args: self: Component pin_a: First pin pin_b: Second pin short_trace: Short trace option Raises: AssertionError: If the part does not have exactly 2 pins. """ c = InsertContainer() ports = tuple(jitx.inspect.decompose(self, jitx.Port)) assert len(ports) == 2, "Part must have exactly 2 pins to be inserted." p1, p2 = ports c.nets = [ pin_a + p1, pin_b + p2, ] st = to_short_trace_enum(short_trace) if ( st == TwoPinShortTrace.SHORT_TRACE_BOTH or st == TwoPinShortTrace.SHORT_TRACE_ANODE ): if not isinstance(pin_a, jitx.Port): raise ValueError( "Cannot make a shortrace with a net. Give a port to Part.insert's pin_a." ) c.a_short_trace = jitx.net.ShortTrace(pin_a, p1) if ( st == TwoPinShortTrace.SHORT_TRACE_BOTH or st == TwoPinShortTrace.SHORT_TRACE_CATHODE ): if not isinstance(pin_b, jitx.Port): raise ValueError( "Cannot make a shortrace with a net. Give a port to Part.insert's pin_b." ) c.c_short_trace = jitx.net.ShortTrace(pin_b, p2) circuit = current.circuit circuit += c return self
# Functions for searching components
[docs] def search_resistors( qb: PartQuery | None = None, *, limit: int = 1000, **kwargs: Unpack[ResistorQueryDict], ) -> Sequence[PartJSON]: """Search for resistors. Args: qb: Query context limit: Maximum number of results **kwargs: Additional query parameters Returns: List of resistors """ rq = make_resistor_query(qb, **kwargs) params = extract(rq) results = dbquery(params, limit) return results
[docs] def search_capacitors( qb: PartQuery | None = None, *, limit: int = 1000, **kwargs: Unpack[CapacitorQueryDict], ) -> Sequence[PartJSON]: """Search for capacitors. Args: qb: Query context limit: Maximum number of results **kwargs: Additional query parameters Returns: List of capacitors """ cq = make_capacitor_query(qb, **kwargs) params = extract(cq) results = dbquery(params, limit) return results
[docs] def search_inductors( qb: PartQuery | None = None, *, limit: int = 1000, **kwargs: Unpack[InductorQueryDict], ) -> Sequence[PartJSON]: """Search for inductors. Args: qb: Query context limit: Maximum number of results **kwargs: Additional query parameters Returns: List of inductors """ iq = make_inductor_query(qb, **kwargs) params = extract(iq) results = dbquery(params, limit) return results
[docs] def search_parts( qb: PartQuery | None = None, *, limit: int = 1000, **kwargs: Unpack[PartQueryDict], ) -> Sequence[PartJSON]: """Search for parts. Args: qb: Query context limit: Maximum number of results **kwargs: Additional query parameters Returns: List of parts """ pq = make_part_query(qb, **kwargs) params = extract(pq) results = dbquery(params, limit) return results
# Insert utility functions for two-pin components
[docs] def to_short_trace_enum(short_trace: TwoPinShortTrace | bool) -> TwoPinShortTrace: """Convert a short trace parameter to a TwoPinShortTrace enum.""" # compatibility conversions for old interface if isinstance(short_trace, bool): return ( TwoPinShortTrace.SHORT_TRACE_ANODE if short_trace else TwoPinShortTrace.SHORT_TRACE_NEITHER ) return short_trace
[docs] def get_element_ports(inst: jitx.Component) -> tuple[jitx.Port, jitx.Port]: """Get the ports of an element.""" mAC = anode_cathode(inst) if mAC: return mAC else: a, b, *_ = jitx.inspect.decompose(inst, jitx.Port) assert len(_) == 0, ( f"Expected passive components from the Parts DB to have two ports, got {a}, {b} and extra ports: {_}" ) return a, b
[docs] def anode_cathode(inst: jitx.Component) -> tuple[jitx.Port, jitx.Port] | None: """Get the anode and cathode ports of an element.""" a = getattr(inst, "a", None) c = getattr(inst, "c", None) if a and c: return a, c