Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor LCSC API into seperate class #509

Merged
merged 4 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions lcsc_api.py
Original file line number Diff line number Diff line change
@@ -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!"}
79 changes: 27 additions & 52 deletions partdetails.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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

# ---------------------------------------------------------------------
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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."""
Expand All @@ -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",
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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."""
Expand Down