chore: normalize all tracked files to LF line endings

Mechanical application of the `.gitattributes` rules from the prior commit.
All 50 files differ only in line endings — verified by
`git diff --cached --ignore-all-space` being empty.

Before: main had 42 CRLF + 27 LF Python files plus mixed-ending in YAML,
templates, and shell scripts. After: every text file is LF (except the
Windows-native *.ps1, *.bat scripts which remain CRLF per gitattributes).

This eliminates the noisy-diff failure mode seen in PR #102, where a
small logic change produced a 918-line diff due to whole-file CRLF→LF
conversion.
This commit is contained in:
Eugene Mikhantyev
2026-04-18 15:23:00 +01:00
parent 2c35ff40a7
commit bfc25639c2
50 changed files with 18167 additions and 18167 deletions

View File

@@ -1,240 +1,240 @@
"""
JLCSearch API client (public, no authentication required)
Alternative to official JLCPCB API using the community-maintained
jlcsearch service at https://jlcsearch.tscircuit.com/
"""
import logging
import time
from typing import Any, Callable, Dict, List, Optional, Union
import requests
logger = logging.getLogger("kicad_interface")
class JLCSearchClient:
"""
Client for JLCSearch public API (tscircuit)
Provides access to JLCPCB parts database without authentication
via the community-maintained jlcsearch service.
"""
BASE_URL = "https://jlcsearch.tscircuit.com"
def __init__(self) -> None:
"""Initialize JLCSearch API client"""
pass
def search_components(
self, category: str = "components", limit: int = 100, offset: int = 0, **filters: Dict
) -> List[Dict]:
"""
Search components in JLCSearch database
Args:
category: Component category (e.g., "resistors", "capacitors", "components")
limit: Maximum number of results
offset: Offset for pagination
**filters: Additional filters (e.g., package="0603", resistance=1000)
Returns:
List of component dicts
"""
url = f"{self.BASE_URL}/{category}/list.json"
params = {"limit": limit, "offset": offset, **filters}
try:
response = requests.get(url, params=params, timeout=30)
response.raise_for_status()
data = response.json()
# The response has the category name as key
# e.g., {"resistors": [...]} or {"components": [...]}
for key, value in data.items():
if isinstance(value, list):
return value
return []
except requests.exceptions.RequestException as e:
logger.error(f"Failed to search JLCSearch: {e}")
raise Exception(f"JLCSearch API request failed: {e}")
def search_resistors(
self, resistance: Optional[int] = None, package: Optional[str] = None, limit: int = 100
) -> List[Dict]:
"""
Search for resistors
Args:
resistance: Resistance value in ohms
package: Package type (e.g., "0603", "0805")
limit: Maximum results
Returns:
List of resistor dicts with fields:
- lcsc: LCSC number (integer)
- mfr: Manufacturer part number
- package: Package size
- is_basic: True if basic library part
- resistance: Resistance in ohms
- tolerance_fraction: Tolerance (0.01 = 1%)
- power_watts: Power rating in mW
- stock: Available stock
- price1: Price per unit
"""
filters: Dict[str, Any] = {}
if resistance is not None:
filters["resistance"] = resistance
if package:
filters["package"] = package
return self.search_components("resistors", limit=limit, **filters)
def search_capacitors(
self, capacitance: Optional[float] = None, package: Optional[str] = None, limit: int = 100
) -> List[Dict]:
"""
Search for capacitors
Args:
capacitance: Capacitance value in farads
package: Package type
limit: Maximum results
Returns:
List of capacitor dicts
"""
filters: Dict[str, Any] = {}
if capacitance is not None:
filters["capacitance"] = capacitance
if package:
filters["package"] = package
return self.search_components("capacitors", limit=limit, **filters)
def get_part_by_lcsc(self, lcsc_number: int) -> Optional[Dict]:
"""
Get part details by LCSC number
Args:
lcsc_number: LCSC number (integer, without 'C' prefix)
Returns:
Part dict or None if not found
"""
# Search across all components filtering by LCSC
# Note: jlcsearch doesn't have a dedicated single-part endpoint
# so we search and filter
try:
results = self.search_components("components", limit=1, lcsc=lcsc_number)
return results[0] if results else None
except Exception as e:
logger.error(f"Failed to get part C{lcsc_number}: {e}")
return None
def download_all_components(
self, callback: Optional[Callable[[int, str], None]] = None, batch_size: int = 100
) -> List[Dict]:
"""
Download all components from jlcsearch database
Note: tscircuit API has a hard-coded 100 result limit per request.
Full catalog download requires ~25,000 paginated requests (~40-60 minutes).
Args:
callback: Optional progress callback function(parts_count, status_msg)
batch_size: Number of parts per batch (max 100 due to API limit)
Returns:
List of all parts
"""
all_parts = []
offset = 0
logger.info("Starting full jlcsearch parts database download...")
while True:
try:
batch = self.search_components("components", limit=batch_size, offset=offset)
# Stop if no results returned (end of catalog)
if not batch or len(batch) == 0:
break
all_parts.extend(batch)
offset += len(batch)
if callback:
callback(len(all_parts), f"Downloaded {len(all_parts)} parts...")
else:
logger.info(f"Downloaded {len(all_parts)} parts so far...")
# Continue pagination - API returns exactly 100 results per page until exhausted
# Only stop when we get 0 results (handled above)
# Rate limiting - be nice to the API
time.sleep(0.1)
except Exception as e:
logger.error(f"Error downloading parts at offset {offset}: {e}")
if len(all_parts) > 0:
logger.warning(f"Partial download available: {len(all_parts)} parts")
return all_parts
else:
raise
logger.info(f"Download complete: {len(all_parts)} parts retrieved")
return all_parts
def test_jlcsearch_connection() -> bool:
"""
Test JLCSearch API connection
Returns:
True if connection successful, False otherwise
"""
try:
client = JLCSearchClient()
# Test by searching for 1k resistors
results = client.search_resistors(resistance=1000, limit=5)
logger.info(f"JLCSearch API connection test successful - found {len(results)} resistors")
return True
except Exception as e:
logger.error(f"JLCSearch API connection test failed: {e}")
return False
if __name__ == "__main__":
# Test the JLCSearch client
logging.basicConfig(level=logging.INFO)
print("Testing JLCSearch API connection...")
if test_jlcsearch_connection():
print("✓ Connection successful!")
client = JLCSearchClient()
print("\nSearching for 1k 0603 resistors...")
resistors = client.search_resistors(resistance=1000, package="0603", limit=5)
print(f"✓ Found {len(resistors)} resistors")
if resistors:
print(f"\nExample resistor:")
r = resistors[0]
print(f" LCSC: C{r.get('lcsc')}")
print(f" MFR: {r.get('mfr')}")
print(f" Package: {r.get('package')}")
print(f" Resistance: {r.get('resistance')}Ω")
print(f" Tolerance: {r.get('tolerance_fraction', 0) * 100}%")
print(f" Power: {r.get('power_watts')}mW")
print(f" Stock: {r.get('stock')}")
print(f" Price: ${r.get('price1')}")
print(f" Basic Library: {'Yes' if r.get('is_basic') else 'No'}")
else:
print("✗ Connection failed")
"""
JLCSearch API client (public, no authentication required)
Alternative to official JLCPCB API using the community-maintained
jlcsearch service at https://jlcsearch.tscircuit.com/
"""
import logging
import time
from typing import Any, Callable, Dict, List, Optional, Union
import requests
logger = logging.getLogger("kicad_interface")
class JLCSearchClient:
"""
Client for JLCSearch public API (tscircuit)
Provides access to JLCPCB parts database without authentication
via the community-maintained jlcsearch service.
"""
BASE_URL = "https://jlcsearch.tscircuit.com"
def __init__(self) -> None:
"""Initialize JLCSearch API client"""
pass
def search_components(
self, category: str = "components", limit: int = 100, offset: int = 0, **filters: Dict
) -> List[Dict]:
"""
Search components in JLCSearch database
Args:
category: Component category (e.g., "resistors", "capacitors", "components")
limit: Maximum number of results
offset: Offset for pagination
**filters: Additional filters (e.g., package="0603", resistance=1000)
Returns:
List of component dicts
"""
url = f"{self.BASE_URL}/{category}/list.json"
params = {"limit": limit, "offset": offset, **filters}
try:
response = requests.get(url, params=params, timeout=30)
response.raise_for_status()
data = response.json()
# The response has the category name as key
# e.g., {"resistors": [...]} or {"components": [...]}
for key, value in data.items():
if isinstance(value, list):
return value
return []
except requests.exceptions.RequestException as e:
logger.error(f"Failed to search JLCSearch: {e}")
raise Exception(f"JLCSearch API request failed: {e}")
def search_resistors(
self, resistance: Optional[int] = None, package: Optional[str] = None, limit: int = 100
) -> List[Dict]:
"""
Search for resistors
Args:
resistance: Resistance value in ohms
package: Package type (e.g., "0603", "0805")
limit: Maximum results
Returns:
List of resistor dicts with fields:
- lcsc: LCSC number (integer)
- mfr: Manufacturer part number
- package: Package size
- is_basic: True if basic library part
- resistance: Resistance in ohms
- tolerance_fraction: Tolerance (0.01 = 1%)
- power_watts: Power rating in mW
- stock: Available stock
- price1: Price per unit
"""
filters: Dict[str, Any] = {}
if resistance is not None:
filters["resistance"] = resistance
if package:
filters["package"] = package
return self.search_components("resistors", limit=limit, **filters)
def search_capacitors(
self, capacitance: Optional[float] = None, package: Optional[str] = None, limit: int = 100
) -> List[Dict]:
"""
Search for capacitors
Args:
capacitance: Capacitance value in farads
package: Package type
limit: Maximum results
Returns:
List of capacitor dicts
"""
filters: Dict[str, Any] = {}
if capacitance is not None:
filters["capacitance"] = capacitance
if package:
filters["package"] = package
return self.search_components("capacitors", limit=limit, **filters)
def get_part_by_lcsc(self, lcsc_number: int) -> Optional[Dict]:
"""
Get part details by LCSC number
Args:
lcsc_number: LCSC number (integer, without 'C' prefix)
Returns:
Part dict or None if not found
"""
# Search across all components filtering by LCSC
# Note: jlcsearch doesn't have a dedicated single-part endpoint
# so we search and filter
try:
results = self.search_components("components", limit=1, lcsc=lcsc_number)
return results[0] if results else None
except Exception as e:
logger.error(f"Failed to get part C{lcsc_number}: {e}")
return None
def download_all_components(
self, callback: Optional[Callable[[int, str], None]] = None, batch_size: int = 100
) -> List[Dict]:
"""
Download all components from jlcsearch database
Note: tscircuit API has a hard-coded 100 result limit per request.
Full catalog download requires ~25,000 paginated requests (~40-60 minutes).
Args:
callback: Optional progress callback function(parts_count, status_msg)
batch_size: Number of parts per batch (max 100 due to API limit)
Returns:
List of all parts
"""
all_parts = []
offset = 0
logger.info("Starting full jlcsearch parts database download...")
while True:
try:
batch = self.search_components("components", limit=batch_size, offset=offset)
# Stop if no results returned (end of catalog)
if not batch or len(batch) == 0:
break
all_parts.extend(batch)
offset += len(batch)
if callback:
callback(len(all_parts), f"Downloaded {len(all_parts)} parts...")
else:
logger.info(f"Downloaded {len(all_parts)} parts so far...")
# Continue pagination - API returns exactly 100 results per page until exhausted
# Only stop when we get 0 results (handled above)
# Rate limiting - be nice to the API
time.sleep(0.1)
except Exception as e:
logger.error(f"Error downloading parts at offset {offset}: {e}")
if len(all_parts) > 0:
logger.warning(f"Partial download available: {len(all_parts)} parts")
return all_parts
else:
raise
logger.info(f"Download complete: {len(all_parts)} parts retrieved")
return all_parts
def test_jlcsearch_connection() -> bool:
"""
Test JLCSearch API connection
Returns:
True if connection successful, False otherwise
"""
try:
client = JLCSearchClient()
# Test by searching for 1k resistors
results = client.search_resistors(resistance=1000, limit=5)
logger.info(f"JLCSearch API connection test successful - found {len(results)} resistors")
return True
except Exception as e:
logger.error(f"JLCSearch API connection test failed: {e}")
return False
if __name__ == "__main__":
# Test the JLCSearch client
logging.basicConfig(level=logging.INFO)
print("Testing JLCSearch API connection...")
if test_jlcsearch_connection():
print("✓ Connection successful!")
client = JLCSearchClient()
print("\nSearching for 1k 0603 resistors...")
resistors = client.search_resistors(resistance=1000, package="0603", limit=5)
print(f"✓ Found {len(resistors)} resistors")
if resistors:
print(f"\nExample resistor:")
r = resistors[0]
print(f" LCSC: C{r.get('lcsc')}")
print(f" MFR: {r.get('mfr')}")
print(f" Package: {r.get('package')}")
print(f" Resistance: {r.get('resistance')}Ω")
print(f" Tolerance: {r.get('tolerance_fraction', 0) * 100}%")
print(f" Power: {r.get('power_watts')}mW")
print(f" Stock: {r.get('stock')}")
print(f" Price: ${r.get('price1')}")
print(f" Basic Library: {'Yes' if r.get('is_basic') else 'No'}")
else:
print("✗ Connection failed")