This repository has been archived by the owner on Dec 10, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add api pickers * add LED test case * add file license headers * fix imports * add missing find by lcsc_id/mfr methods to api picker lib * fix for Component.PARAMs removal * fix for extra diode filters * more Component.PARAMs * update api tests to match jlcpcb test changes * fix for recent changes * fix capacitor e-series * add api picker perf test * fix manufacturer_name sometimes available * dedup picker tests * run gen_F.py * add lru cache on api queries * surface in examples/buildutil * fix typing issues * run gen_F.py * refactor */pickers.py * switch to fastapi backend * embed default API url * Minor improvements - Fix types - Improve ConfigFlags - Change Exceptions * api client init simplification * revert assertIsInstance * api picker test conditions cleanup * singleton api client * fix TestPickerPerformanceJlcpcb casing * bump default api timeout 10s -> 30s * custom domain * 404 -> PickError --------- Co-authored-by: iopapamanoglou <[email protected]>
- Loading branch information
1 parent
930f60b
commit 8d12b33
Showing
12 changed files
with
1,164 additions
and
378 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,249 @@ | ||
# This file is part of the faebryk project | ||
# SPDX-License-Identifier: MIT | ||
|
||
import functools | ||
import logging | ||
import textwrap | ||
from dataclasses import dataclass | ||
|
||
import requests | ||
from dataclasses_json import dataclass_json | ||
from pint import DimensionalityError | ||
|
||
from faebryk.core.module import Module | ||
|
||
# TODO: replace with API-specific data model | ||
from faebryk.libs.picker.jlcpcb.jlcpcb import Component, MappingParameterDB | ||
from faebryk.libs.picker.lcsc import LCSC_NoDataException, LCSC_PinmapException | ||
from faebryk.libs.picker.picker import PickError | ||
from faebryk.libs.util import ConfigFlagString, try_or | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
DEFAULT_API_URL = "https://components.atopileapi.com" | ||
DEFAULT_API_TIMEOUT_SECONDS = 30 | ||
API_URL = ConfigFlagString("PICKER_API_URL", DEFAULT_API_URL, "API URL") | ||
API_KEY = ConfigFlagString("PICKER_API_KEY", "", "API key") | ||
|
||
|
||
class ApiError(Exception): ... | ||
|
||
|
||
class ApiNotConfiguredError(ApiError): ... | ||
|
||
|
||
class ApiHTTPError(ApiError): | ||
def __init__(self, error: requests.exceptions.HTTPError): | ||
super().__init__() | ||
self.response = error.response | ||
|
||
def __str__(self) -> str: | ||
status_code = self.response.status_code | ||
detail = self.response.json()["detail"] | ||
return f"{super().__str__()}: {status_code} {detail}" | ||
|
||
|
||
def check_compatible_parameters( | ||
module: Module, component: Component, mapping: list[MappingParameterDB] | ||
) -> bool: | ||
""" | ||
Check if the parameters of a component are compatible with the module | ||
""" | ||
# TODO: serialize the module and check compatibility in the backend | ||
|
||
params = component.get_params(mapping) | ||
param_matches = [ | ||
try_or( | ||
lambda: p.is_subset_of(getattr(module, m.param_name)), | ||
default=False, | ||
catch=DimensionalityError, | ||
) | ||
for p, m in zip(params, mapping) | ||
] | ||
|
||
if not (is_compatible := all(param_matches)): | ||
logger.debug( | ||
f"Component {component.lcsc} doesn't match: " | ||
f"{[p for p, v in zip(params, param_matches) if not v]}" | ||
) | ||
|
||
return is_compatible | ||
|
||
|
||
def try_attach( | ||
cmp: Module, parts: list[Component], mapping: list[MappingParameterDB], qty: int | ||
) -> bool: | ||
failures = [] | ||
for part in parts: | ||
if not check_compatible_parameters(cmp, part, mapping): | ||
continue | ||
|
||
try: | ||
part.attach(cmp, mapping, qty, allow_TBD=False) | ||
return True | ||
except (ValueError, Component.ParseError) as e: | ||
failures.append((part, e)) | ||
except LCSC_NoDataException as e: | ||
failures.append((part, e)) | ||
except LCSC_PinmapException as e: | ||
failures.append((part, e)) | ||
|
||
if failures: | ||
fail_str = textwrap.indent( | ||
"\n" + f"{'\n'.join(f'{c}: {e}' for c, e in failures)}", " " * 4 | ||
) | ||
|
||
raise PickError( | ||
f"Failed to attach any components to module {cmp}: {len(failures)}" | ||
f" {fail_str}", | ||
cmp, | ||
) | ||
|
||
return False | ||
|
||
|
||
type SIvalue = str | ||
|
||
|
||
@dataclass_json | ||
@dataclass(frozen=True) | ||
class FootprintCandidate: | ||
footprint: str | ||
pin_count: int | ||
|
||
|
||
@dataclass_json | ||
@dataclass(frozen=True) | ||
class BaseParams: | ||
footprint_candidates: list[FootprintCandidate] | ||
qty: int | ||
|
||
def convert_to_dict(self) -> dict: | ||
return self.to_dict() # type: ignore | ||
|
||
|
||
@dataclass(frozen=True) | ||
class ResistorParams(BaseParams): | ||
resistances: list[SIvalue] | ||
|
||
|
||
@dataclass(frozen=True) | ||
class CapacitorParams(BaseParams): | ||
capacitances: list[SIvalue] | ||
|
||
|
||
@dataclass(frozen=True) | ||
class InductorParams(BaseParams): | ||
inductances: list[SIvalue] | ||
|
||
|
||
@dataclass(frozen=True) | ||
class TVSParams(BaseParams): ... | ||
|
||
|
||
@dataclass(frozen=True) | ||
class DiodeParams(BaseParams): | ||
max_currents: list[SIvalue] | ||
reverse_working_voltages: list[SIvalue] | ||
|
||
|
||
@dataclass(frozen=True) | ||
class LEDParams(BaseParams): ... | ||
|
||
|
||
@dataclass(frozen=True) | ||
class MOSFETParams(BaseParams): ... | ||
|
||
|
||
@dataclass(frozen=True) | ||
class LDOParams(BaseParams): ... | ||
|
||
|
||
@dataclass(frozen=True) | ||
class LCSCParams: | ||
lcsc: int | ||
|
||
|
||
@dataclass(frozen=True) | ||
class ManufacturerPartParams: | ||
manufacturer_name: str | ||
mfr: str | ||
qty: int | ||
|
||
|
||
class ApiClient: | ||
@dataclass | ||
class Config: | ||
api_url: str = API_URL.get() | ||
api_key: str = API_KEY.get() | ||
|
||
config = Config() | ||
|
||
def __init__(self): | ||
self._client = requests.Session() | ||
self._client.headers["Authorization"] = f"Bearer {self.config.api_key}" | ||
|
||
def _get(self, url: str, timeout: float = 10) -> requests.Response: | ||
try: | ||
response = self._client.get(f"{self.config.api_url}{url}", timeout=timeout) | ||
response.raise_for_status() | ||
except requests.exceptions.HTTPError as e: | ||
raise ApiHTTPError(e) from e | ||
|
||
return response | ||
|
||
def _post( | ||
self, url: str, data: dict, timeout: float = DEFAULT_API_TIMEOUT_SECONDS | ||
) -> requests.Response: | ||
try: | ||
response = self._client.post( | ||
f"{self.config.api_url}{url}", json=data, timeout=timeout | ||
) | ||
response.raise_for_status() | ||
except requests.exceptions.HTTPError as e: | ||
raise ApiHTTPError(e) from e | ||
|
||
return response | ||
|
||
@functools.lru_cache(maxsize=None) | ||
def fetch_part_by_lcsc(self, lcsc: int) -> list[Component]: | ||
response = self._get(f"/v0/component/lcsc/{lcsc}") | ||
return [Component(**part) for part in response.json()["components"]] | ||
|
||
@functools.lru_cache(maxsize=None) | ||
def fetch_part_by_mfr(self, mfr: str, mfr_pn: str) -> list[Component]: | ||
response = self._get(f"/v0/component/mfr/{mfr}/{mfr_pn}") | ||
return [Component(**part) for part in response.json()["components"]] | ||
|
||
def query_parts(self, method: str, params: BaseParams) -> list[Component]: | ||
response = self._post(f"/v0/query/{method}", params.convert_to_dict()) | ||
return [Component(**part) for part in response.json()["components"]] | ||
|
||
def fetch_resistors(self, params: ResistorParams) -> list[Component]: | ||
return self.query_parts("resistors", params) | ||
|
||
def fetch_capacitors(self, params: CapacitorParams) -> list[Component]: | ||
return self.query_parts("capacitors", params) | ||
|
||
def fetch_inductors(self, params: InductorParams) -> list[Component]: | ||
return self.query_parts("inductors", params) | ||
|
||
def fetch_tvs(self, params: TVSParams) -> list[Component]: | ||
return self.query_parts("tvs", params) | ||
|
||
def fetch_diodes(self, params: DiodeParams) -> list[Component]: | ||
return self.query_parts("diodes", params) | ||
|
||
def fetch_leds(self, params: LEDParams) -> list[Component]: | ||
return self.query_parts("leds", params) | ||
|
||
def fetch_mosfets(self, params: MOSFETParams) -> list[Component]: | ||
return self.query_parts("mosfets", params) | ||
|
||
def fetch_ldos(self, params: LDOParams) -> list[Component]: | ||
return self.query_parts("ldos", params) | ||
|
||
|
||
@functools.lru_cache | ||
def get_api_client() -> ApiClient: | ||
return ApiClient() |
Oops, something went wrong.