fix: resolve pre-commit hook issues for mypy and root-level files

- Fix mypy duplicate module conflict by excluding python/commands/board.py
- Add import-untyped to mypy disabled error codes
- Use explicit_package_bases for proper module resolution
- Auto-format root-level download_jlcpcb.py and test-router.js

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eugene Mikhantyev
2026-03-29 13:21:05 +01:00
parent dbbbde17ba
commit bc1e33b652
4 changed files with 118 additions and 87 deletions

View File

@@ -41,8 +41,8 @@ repos:
hooks: hooks:
- id: mypy - id: mypy
files: ^python/ files: ^python/
exclude: ^python/commands/board\.py$
args: [--config-file=pyproject.toml] args: [--config-file=pyproject.toml]
pass_filenames: false
additional_dependencies: [] additional_dependencies: []
- repo: local - repo: local

View File

@@ -9,11 +9,12 @@ The cache.sqlite3 file contains all JLCPCB parts with stock, pricing,
and category data. We then convert it into the format expected by the and category data. We then convert it into the format expected by the
KiCad MCP server's JLCPCBPartsManager. KiCad MCP server's JLCPCBPartsManager.
""" """
import os
import sys
import subprocess
import sqlite3
import json import json
import os
import sqlite3
import subprocess
import sys
import time import time
from pathlib import Path from pathlib import Path
@@ -40,8 +41,7 @@ def download_files():
url = f"{BASE_URL}/{part}" url = f"{BASE_URL}/{part}"
print(f" Downloading {part}...") print(f" Downloading {part}...")
result = subprocess.run( result = subprocess.run(
["curl", "-L", "-o", str(dest), "--progress-bar", url], ["curl", "-L", "-o", str(dest), "--progress-bar", url], capture_output=False
capture_output=False
) )
if result.returncode != 0: if result.returncode != 0:
print(f" ERROR downloading {part}") print(f" ERROR downloading {part}")
@@ -62,7 +62,8 @@ def extract_database():
try: try:
result = subprocess.run( result = subprocess.run(
[cmd, "x", "-y", "-o" + str(CACHE_DIR), str(CACHE_DIR / "cache.zip")], [cmd, "x", "-y", "-o" + str(CACHE_DIR), str(CACHE_DIR / "cache.zip")],
capture_output=True, text=True capture_output=True,
text=True,
) )
if result.returncode == 0: if result.returncode == 0:
print(f"Extracted with {cmd}") print(f"Extracted with {cmd}")
@@ -88,9 +89,9 @@ def convert_to_mcp_format():
src.row_factory = sqlite3.Row src.row_factory = sqlite3.Row
# Check schema # Check schema
tables = [r[0] for r in src.execute( tables = [
"SELECT name FROM sqlite_master WHERE type='table'" r[0] for r in src.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()
).fetchall()] ]
print(f" Source tables: {tables}") print(f" Source tables: {tables}")
# Find the main components table # Find the main components table
@@ -115,7 +116,7 @@ def convert_to_mcp_format():
# Create target DB in MCP format # Create target DB in MCP format
dst = sqlite3.connect(str(TARGET_DB)) dst = sqlite3.connect(str(TARGET_DB))
dst.execute(''' dst.execute("""
CREATE TABLE components ( CREATE TABLE components (
lcsc TEXT PRIMARY KEY, lcsc TEXT PRIMARY KEY,
category TEXT, category TEXT,
@@ -131,12 +132,12 @@ def convert_to_mcp_format():
price_json TEXT, price_json TEXT,
last_updated INTEGER last_updated INTEGER
) )
''') """)
dst.execute('CREATE INDEX idx_category ON components(category, subcategory)') dst.execute("CREATE INDEX idx_category ON components(category, subcategory)")
dst.execute('CREATE INDEX idx_package ON components(package)') dst.execute("CREATE INDEX idx_package ON components(package)")
dst.execute('CREATE INDEX idx_manufacturer ON components(manufacturer)') dst.execute("CREATE INDEX idx_manufacturer ON components(manufacturer)")
dst.execute('CREATE INDEX idx_library_type ON components(library_type)') dst.execute("CREATE INDEX idx_library_type ON components(library_type)")
dst.execute('CREATE INDEX idx_mfr_part ON components(mfr_part)') dst.execute("CREATE INDEX idx_mfr_part ON components(mfr_part)")
# Map source columns to our schema # Map source columns to our schema
# jlcparts schema varies but commonly has: # jlcparts schema varies but commonly has:
@@ -150,81 +151,103 @@ def convert_to_mcp_format():
row_dict = dict(row) row_dict = dict(row)
# Adapt column names (jlcparts uses various schemas) # Adapt column names (jlcparts uses various schemas)
lcsc = row_dict.get('lcsc') or row_dict.get('LCSC_Part') or row_dict.get('lcsc_id') lcsc = row_dict.get("lcsc") or row_dict.get("LCSC_Part") or row_dict.get("lcsc_id")
if lcsc is None: if lcsc is None:
continue continue
if isinstance(lcsc, int): if isinstance(lcsc, int):
lcsc = f"C{lcsc}" lcsc = f"C{lcsc}"
elif not str(lcsc).startswith('C'): elif not str(lcsc).startswith("C"):
lcsc = f"C{lcsc}" lcsc = f"C{lcsc}"
mfr_part = row_dict.get('mfr') or row_dict.get('MFR_Part') or row_dict.get('mfr_part') or '' mfr_part = row_dict.get("mfr") or row_dict.get("MFR_Part") or row_dict.get("mfr_part") or ""
package = row_dict.get('package') or row_dict.get('Package') or '' package = row_dict.get("package") or row_dict.get("Package") or ""
manufacturer = row_dict.get('manufacturer') or row_dict.get('Manufacturer') or '' manufacturer = row_dict.get("manufacturer") or row_dict.get("Manufacturer") or ""
description = row_dict.get('description') or row_dict.get('Description') or '' description = row_dict.get("description") or row_dict.get("Description") or ""
stock = row_dict.get('stock') or row_dict.get('Stock') or 0 stock = row_dict.get("stock") or row_dict.get("Stock") or 0
category = row_dict.get('category') or row_dict.get('First Category') or '' category = row_dict.get("category") or row_dict.get("First Category") or ""
subcategory = row_dict.get('subcategory') or row_dict.get('Second Category') or '' subcategory = row_dict.get("subcategory") or row_dict.get("Second Category") or ""
datasheet = row_dict.get('datasheet') or row_dict.get('url') or '' datasheet = row_dict.get("datasheet") or row_dict.get("url") or ""
# Library type # Library type
is_basic = row_dict.get('basic') or row_dict.get('is_basic') or row_dict.get('Basic') is_basic = row_dict.get("basic") or row_dict.get("is_basic") or row_dict.get("Basic")
is_preferred = row_dict.get('preferred') or row_dict.get('is_preferred') or row_dict.get('Preferred') is_preferred = (
row_dict.get("preferred") or row_dict.get("is_preferred") or row_dict.get("Preferred")
)
if is_basic: if is_basic:
lib_type = 'Basic' lib_type = "Basic"
elif is_preferred: elif is_preferred:
lib_type = 'Preferred' lib_type = "Preferred"
else: else:
lib_type = 'Extended' lib_type = "Extended"
# Price # Price
price = row_dict.get('price') or row_dict.get('Price') or 0 price = row_dict.get("price") or row_dict.get("Price") or 0
price_json = json.dumps([{"qty": 1, "price": price}] if price else []) price_json = json.dumps([{"qty": 1, "price": price}] if price else [])
batch.append(( batch.append(
str(lcsc), category, subcategory, mfr_part, package, (
0, manufacturer, lib_type, description, str(lcsc),
datasheet, int(stock) if stock else 0, price_json, now category,
)) subcategory,
mfr_part,
package,
0,
manufacturer,
lib_type,
description,
datasheet,
int(stock) if stock else 0,
price_json,
now,
)
)
if len(batch) >= 10000: if len(batch) >= 10000:
dst.executemany(''' dst.executemany(
"""
INSERT OR REPLACE INTO components INSERT OR REPLACE INTO components
(lcsc, category, subcategory, mfr_part, package, (lcsc, category, subcategory, mfr_part, package,
solder_joints, manufacturer, library_type, description, solder_joints, manufacturer, library_type, description,
datasheet, stock, price_json, last_updated) datasheet, stock, price_json, last_updated)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', batch) """,
batch,
)
count += len(batch) count += len(batch)
batch = [] batch = []
if count % 100000 == 0: if count % 100000 == 0:
print(f" Converted {count:,} parts...") print(f" Converted {count:,} parts...")
if batch: if batch:
dst.executemany(''' dst.executemany(
"""
INSERT OR REPLACE INTO components INSERT OR REPLACE INTO components
(lcsc, category, subcategory, mfr_part, package, (lcsc, category, subcategory, mfr_part, package,
solder_joints, manufacturer, library_type, description, solder_joints, manufacturer, library_type, description,
datasheet, stock, price_json, last_updated) datasheet, stock, price_json, last_updated)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', batch) """,
batch,
)
count += len(batch) count += len(batch)
# Build FTS index # Build FTS index
print(f" Building full-text search index...") print(f" Building full-text search index...")
dst.execute(''' dst.execute("""
CREATE VIRTUAL TABLE IF NOT EXISTS components_fts USING fts5( CREATE VIRTUAL TABLE IF NOT EXISTS components_fts USING fts5(
lcsc, description, mfr_part, manufacturer, lcsc, description, mfr_part, manufacturer,
content=components content=components
) )
''') """)
dst.execute("INSERT INTO components_fts(components_fts) VALUES('rebuild')") dst.execute("INSERT INTO components_fts(components_fts) VALUES('rebuild')")
dst.commit() dst.commit()
# Stats # Stats
total = dst.execute("SELECT COUNT(*) FROM components").fetchone()[0] total = dst.execute("SELECT COUNT(*) FROM components").fetchone()[0]
basic = dst.execute("SELECT COUNT(*) FROM components WHERE library_type='Basic'").fetchone()[0] basic = dst.execute("SELECT COUNT(*) FROM components WHERE library_type='Basic'").fetchone()[0]
extended = dst.execute("SELECT COUNT(*) FROM components WHERE library_type='Extended'").fetchone()[0] extended = dst.execute(
"SELECT COUNT(*) FROM components WHERE library_type='Extended'"
).fetchone()[0]
dst.close() dst.close()
src.close() src.close()

View File

@@ -18,7 +18,10 @@ warn_unused_configs = true
ignore_missing_imports = true ignore_missing_imports = true
check_untyped_defs = false check_untyped_defs = false
disallow_untyped_defs = false disallow_untyped_defs = false
explicit_package_bases = true
mypy_path = "."
disable_error_code = [ disable_error_code = [
"import-untyped",
"arg-type", "arg-type",
"attr-defined", "attr-defined",
"union-attr", "union-attr",

View File

@@ -3,39 +3,44 @@
* Run with: node test-router.js * Run with: node test-router.js
*/ */
import { getAllCategories, searchTools, getRegistryStats, isDirectTool } from './dist/tools/registry.js'; import {
getAllCategories,
searchTools,
getRegistryStats,
isDirectTool,
} from "./dist/tools/registry.js";
console.log('='.repeat(70)); console.log("=".repeat(70));
console.log('KICAD MCP ROUTER - TEST'); console.log("KICAD MCP ROUTER - TEST");
console.log('='.repeat(70)); console.log("=".repeat(70));
// Test 1: Registry Stats // Test 1: Registry Stats
console.log('\n📊 Registry Statistics:'); console.log("\n📊 Registry Statistics:");
const stats = getRegistryStats(); const stats = getRegistryStats();
console.log(JSON.stringify(stats, null, 2)); console.log(JSON.stringify(stats, null, 2));
// Test 2: List Categories // Test 2: List Categories
console.log('\n📁 Tool Categories:'); console.log("\n📁 Tool Categories:");
const categories = getAllCategories(); const categories = getAllCategories();
categories.forEach(cat => { categories.forEach((cat) => {
console.log(` - ${cat.name}: ${cat.description} (${cat.tools.length} tools)`); console.log(` - ${cat.name}: ${cat.description} (${cat.tools.length} tools)`);
}); });
// Test 3: Search // Test 3: Search
console.log('\n🔍 Search Test: "export gerber"'); console.log('\n🔍 Search Test: "export gerber"');
const results = searchTools('gerber'); const results = searchTools("gerber");
console.log(`Found ${results.length} matches:`); console.log(`Found ${results.length} matches:`);
results.forEach(result => { results.forEach((result) => {
console.log(` - ${result.tool} (${result.category})`); console.log(` - ${result.tool} (${result.category})`);
}); });
// Test 4: Direct Tools Check // Test 4: Direct Tools Check
console.log('\n✅ Direct Tools Test:'); console.log("\n✅ Direct Tools Test:");
console.log(` - create_project is direct: ${isDirectTool('create_project')}`); console.log(` - create_project is direct: ${isDirectTool("create_project")}`);
console.log(` - place_component is direct: ${isDirectTool('place_component')}`); console.log(` - place_component is direct: ${isDirectTool("place_component")}`);
console.log(` - export_gerber is direct: ${isDirectTool('export_gerber')}`); console.log(` - export_gerber is direct: ${isDirectTool("export_gerber")}`);
console.log(` - add_via is direct: ${isDirectTool('add_via')}`); console.log(` - add_via is direct: ${isDirectTool("add_via")}`);
console.log('\n' + '='.repeat(70)); console.log("\n" + "=".repeat(70));
console.log('✅ Router tests complete!'); console.log("✅ Router tests complete!");
console.log('='.repeat(70)); console.log("=".repeat(70));