diff --git a/lcsc_api.py b/lcsc_api.py new file mode 100644 index 0000000..0cf0135 --- /dev/null +++ b/lcsc_api.py @@ -0,0 +1,47 @@ +"""Unofficial LCSC API.""" +import io +from pathlib import Path + +import requests # pylint: disable=import-error + + +class LCSC_API: + """Unofficial LCSC API.""" + + def __init__(self): + self.headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36" + } # pretend we are browser, otherwise their cloud service blocks the request + + def get_part_data(self, lcsc_number: str) -> dict: + """Get data for a given LCSC number from the API.""" + r = requests.get( + f"https://cart.jlcpcb.com/shoppingCart/smtGood/getComponentDetail?componentCode={lcsc_number}", + headers=self.headers, + timeout=10, + ) + if r.status_code != requests.codes.ok: # pylint: disable=no-member + return {"success": False, "msg": "non-OK HTTP response status"} + data = r.json() + if not data.get("data"): + return { + "success": False, + "msg": "returned JSON data does not have expected 'data' attribute", + } + return {"success": True, "data": data} + + def download_bitmap(self, url: str) -> io.BytesIO | None: + """Download a picture of the part from the API.""" + content = requests.get(url, headers=self.headers, timeout=10).content + return io.BytesIO(content) + + def download_datasheet(self, url: str, path: Path): + """Download and save a datasheet from the API.""" + r = requests.get(url, stream=True, headers=self.headers, timeout=10) + if r.status_code != requests.codes.ok: # pylint: disable=no-member + return {"success": False, "msg": "non-OK HTTP response status"} + if not r: + return {"success": False, "msg": "Failed to download datasheet!"} + with open(path, "wb") as f: + f.write(r.content) + return {"success": True, "msg": "Successfully downloaded datasheet!"} diff --git a/partdetails.py b/partdetails.py index e27140b..6be55f2 100644 --- a/partdetails.py +++ b/partdetails.py @@ -1,16 +1,15 @@ """Contains the part details modal dialog.""" -import io import logging from pathlib import Path import webbrowser -import requests # pylint: disable=import-error import wx # pylint: disable=import-error import wx.dataview # pylint: disable=import-error from .events import MessageEvent from .helpers import HighResWxSize, loadBitmapScaled +from .lcsc_api import LCSC_API class PartDetailsDialog(wx.Dialog): @@ -31,10 +30,8 @@ def __init__(self, parent, part): self.parent = parent self.part = part self.datasheet_path = Path(self.parent.project_path) / "datasheets" - self.headers = { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36" - } # pretend we are browser, otherwise their cloud service blocks the request - self.pdfurl = None + self.lcsc_api = LCSC_API() + self.pdfurl = "" self.picture = None # --------------------------------------------------------------------- @@ -111,7 +108,7 @@ def __init__(self, parent, part): self.parent.scale_factor, ) ) - self.openpdf_button.SetBitmapMargins((2, 0)) + self.savepdf_button.SetBitmapMargins((2, 0)) self.openpdf_button.SetBitmap( loadBitmapScaled( @@ -150,29 +147,17 @@ def savepdf(self, *_): filename = self.pdfurl.rsplit("/", maxsplit=1)[1] self.logger.info("Save datasheet %s to %s", filename, self.datasheet_path) self.datasheet_path.mkdir(parents=True, exist_ok=True) - r = requests.get( - str(self.pdfurl), stream=True, headers=self.headers, timeout=10 + result = self.lcsc_api.download_datasheet(self.pdfurl, self.datasheet_path / filename) + title = "Success" if result["success"] else "Error" + style = "info" if result["success"] else "error" + wx.PostEvent( + self.parent, + MessageEvent( + title=title, + text=result["msg"], + style=style, + ), ) - if r: - with open(self.datasheet_path / filename, "wb") as f: - f.write(r.content) - wx.PostEvent( - self.parent, - MessageEvent( - title="Success", - text="Successfully downloaded datasheet!", - style="info", - ), - ) - else: - wx.PostEvent( - self.parent, - MessageEvent( - title="Error", - text="Failed to download datasheet!", - style="error", - ), - ) def openpdf(self, *_): """Open the linked datasheet PDF on button click.""" @@ -181,28 +166,18 @@ def openpdf(self, *_): def get_scaled_bitmap(self, url, width, height): """Download a picture from a URL and convert it into a wx Bitmap.""" - content = requests.get(url, headers=self.headers, timeout=10).content - io_bytes = io.BytesIO(content) + io_bytes = self.lcsc_api.download_bitmap(url) image = wx.Image(io_bytes) image = image.Scale(width, height, wx.IMAGE_QUALITY_HIGH) result = wx.Bitmap(image) return result def get_part_data(self): - """Fetch part data from JLCPCB API and parse it into the table, set picture and PDF link.""" - r = requests.get( - f"https://cart.jlcpcb.com/shoppingCart/smtGood/getComponentDetail?componentCode={self.part}", - headers=self.headers, - timeout=10, - ) - if r.status_code != requests.codes.ok: # pylint: disable=no-member - self.report_part_data_fetch_error("non-OK HTTP response status") - - data = r.json() - if not data.get("data"): - self.report_part_data_fetch_error( - "returned JSON data does not have expected 'data' attribute" - ) + """Get part data from JLCPCB API and parse it into the table, set picture and PDF link.""" + result = self.lcsc_api.get_part_data(self.part) + if not result["success"]: + self.report_part_data_fetch_error(result["msg"]) + return parameters = { "componentCode": "Component Code", @@ -220,16 +195,16 @@ def get_part_data(self): "leastNumber": "Minimal Quantity", "leastNumberPrice": "Minimum price", } - parttype = data.get("data", {}).get("componentLibraryType") + parttype = result["data"].get("data", {}).get("componentLibraryType") if parttype and parttype == "base": self.data_list.AppendItem(["Type", "Basic"]) elif parttype and parttype == "expand": self.data_list.AppendItem(["Type", "Extended"]) for k, v in parameters.items(): - val = data.get("data", {}).get(k) + val = result["data"].get("data", {}).get(k) if val: self.data_list.AppendItem([v, str(val)]) - prices = data.get("data", {}).get("jlcPrices", []) + prices = result["data"].get("data", {}).get("jlcPrices", []) if prices: for price in prices: start = price.get("startNumber") @@ -248,7 +223,7 @@ def get_part_data(self): str(price.get("productPrice")), ] ) - prices = data.get("data", {}).get("prices", []) + prices = result["data"].get("data", {}).get("prices", []) if prices: for price in prices: start = price.get("startNumber") @@ -267,14 +242,14 @@ def get_part_data(self): str(price.get("productPrice")), ] ) - for attribute in data.get("data", {}).get("attributes", []): + for attribute in result["data"].get("data", {}).get("attributes", []): self.data_list.AppendItem( [ attribute.get("attribute_name_en"), str(attribute.get("attribute_value_name")), ] ) - picture = data.get("data", {}).get("minImage") + picture = result["data"].get("data", {}).get("minImage") if picture: # get the full resolution image instead of the thumbnail picture = picture.replace("96x96", "900x900") @@ -285,7 +260,7 @@ def get_part_data(self): int(200 * self.parent.scale_factor), ) ) - self.pdfurl = data.get("data", {}).get("dataManualUrl") + self.pdfurl = result["data"].get("data", {}).get("dataManualUrl") def report_part_data_fetch_error(self, reason): """Spawn a message box with an erro message if the fetch fails."""