kicad-sch-api 0.2.0__py3-none-any.whl → 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kicad-sch-api might be problematic. Click here for more details.

@@ -36,10 +36,10 @@ class WireCollection:
36
36
  self._wires: List[Wire] = wires or []
37
37
  self._uuid_index: Dict[str, int] = {}
38
38
  self._modified = False
39
-
39
+
40
40
  # Build UUID index
41
41
  self._rebuild_index()
42
-
42
+
43
43
  logger.debug(f"WireCollection initialized with {len(self._wires)} wires")
44
44
 
45
45
  def _rebuild_index(self):
@@ -67,7 +67,7 @@ class WireCollection:
67
67
  points: Optional[List[Union[Point, Tuple[float, float]]]] = None,
68
68
  wire_type: WireType = WireType.WIRE,
69
69
  stroke_width: float = 0.0,
70
- uuid: Optional[str] = None
70
+ uuid: Optional[str] = None,
71
71
  ) -> str:
72
72
  """
73
73
  Add a wire to the collection.
@@ -112,12 +112,7 @@ class WireCollection:
112
112
  raise ValueError("Must provide either start/end points or points list")
113
113
 
114
114
  # Create wire
115
- wire = Wire(
116
- uuid=uuid,
117
- points=wire_points,
118
- wire_type=wire_type,
119
- stroke_width=stroke_width
120
- )
115
+ wire = Wire(uuid=uuid, points=wire_points, wire_type=wire_type, stroke_width=stroke_width)
121
116
 
122
117
  # Add to collection
123
118
  self._wires.append(wire)
@@ -148,17 +143,23 @@ class WireCollection:
148
143
  logger.debug(f"Removed wire: {uuid}")
149
144
  return True
150
145
 
151
- def get_by_point(self, point: Union[Point, Tuple[float, float]], tolerance: float = 0.1) -> List[Wire]:
146
+ def get_by_point(
147
+ self, point: Union[Point, Tuple[float, float]], tolerance: float = None
148
+ ) -> List[Wire]:
152
149
  """
153
150
  Find wires that pass through or near a point.
154
151
 
155
152
  Args:
156
153
  point: Point to search near
157
- tolerance: Distance tolerance
154
+ tolerance: Distance tolerance (uses config default if None)
158
155
 
159
156
  Returns:
160
157
  List of wires near the point
161
158
  """
159
+ if tolerance is None:
160
+ from .config import config
161
+
162
+ tolerance = config.tolerance.position_tolerance
162
163
  if isinstance(point, tuple):
163
164
  point = Point(point[0], point[1])
164
165
 
@@ -178,33 +179,36 @@ class WireCollection:
178
179
 
179
180
  return matching_wires
180
181
 
181
- def _point_on_segment(self, point: Point, seg_start: Point, seg_end: Point, tolerance: float) -> bool:
182
+ def _point_on_segment(
183
+ self, point: Point, seg_start: Point, seg_end: Point, tolerance: float
184
+ ) -> bool:
182
185
  """Check if point lies on line segment within tolerance."""
183
186
  # Vector from seg_start to seg_end
184
187
  seg_vec = Point(seg_end.x - seg_start.x, seg_end.y - seg_start.y)
185
188
  seg_length = seg_start.distance_to(seg_end)
186
-
187
- if seg_length < 0.001: # Very short segment
189
+
190
+ from .config import config
191
+
192
+ if seg_length < config.tolerance.wire_segment_min: # Very short segment
188
193
  return seg_start.distance_to(point) <= tolerance
189
-
194
+
190
195
  # Vector from seg_start to point
191
196
  point_vec = Point(point.x - seg_start.x, point.y - seg_start.y)
192
-
197
+
193
198
  # Project point onto segment
194
- dot_product = (point_vec.x * seg_vec.x + point_vec.y * seg_vec.y)
199
+ dot_product = point_vec.x * seg_vec.x + point_vec.y * seg_vec.y
195
200
  projection = dot_product / (seg_length * seg_length)
196
-
201
+
197
202
  # Check if projection is within segment bounds
198
203
  if projection < 0 or projection > 1:
199
204
  return False
200
-
205
+
201
206
  # Calculate distance from point to line
202
207
  proj_point = Point(
203
- seg_start.x + projection * seg_vec.x,
204
- seg_start.y + projection * seg_vec.y
208
+ seg_start.x + projection * seg_vec.x, seg_start.y + projection * seg_vec.y
205
209
  )
206
210
  distance = point.distance_to(proj_point)
207
-
211
+
208
212
  return distance <= tolerance
209
213
 
210
214
  def get_horizontal_wires(self) -> List[Wire]:
@@ -220,7 +224,7 @@ class WireCollection:
220
224
  total_length = sum(wire.length for wire in self._wires)
221
225
  simple_wires = sum(1 for wire in self._wires if wire.is_simple())
222
226
  multi_point_wires = len(self._wires) - simple_wires
223
-
227
+
224
228
  return {
225
229
  "total_wires": len(self._wires),
226
230
  "simple_wires": simple_wires,
@@ -228,7 +232,7 @@ class WireCollection:
228
232
  "total_length": total_length,
229
233
  "avg_length": total_length / len(self._wires) if self._wires else 0,
230
234
  "horizontal_wires": len(self.get_horizontal_wires()),
231
- "vertical_wires": len(self.get_vertical_wires())
235
+ "vertical_wires": len(self.get_vertical_wires()),
232
236
  }
233
237
 
234
238
  def clear(self):
@@ -245,4 +249,4 @@ class WireCollection:
245
249
 
246
250
  def mark_saved(self):
247
251
  """Mark collection as saved (reset modified flag)."""
248
- self._modified = False
252
+ self._modified = False
@@ -7,4 +7,4 @@ using a SQLite search index built from the existing symbol cache.
7
7
 
8
8
  from .search_index import ComponentSearchIndex, get_search_index
9
9
 
10
- __all__ = ["ComponentSearchIndex", "get_search_index"]
10
+ __all__ = ["ComponentSearchIndex", "get_search_index"]
@@ -18,19 +18,20 @@ logger = logging.getLogger(__name__)
18
18
 
19
19
  class ComponentSearchIndex:
20
20
  """Fast SQLite-based search index for KiCAD components."""
21
-
21
+
22
22
  def __init__(self, cache_dir: Optional[Path] = None):
23
23
  """Initialize the search index."""
24
24
  self.cache_dir = cache_dir or Path.home() / ".cache" / "kicad-sch-api"
25
25
  self.cache_dir.mkdir(parents=True, exist_ok=True)
26
-
26
+
27
27
  self.db_path = self.cache_dir / "search_index.db"
28
28
  self._init_database()
29
-
29
+
30
30
  def _init_database(self):
31
31
  """Initialize the SQLite database schema."""
32
32
  with sqlite3.connect(str(self.db_path)) as conn:
33
- conn.execute("""
33
+ conn.execute(
34
+ """
34
35
  CREATE TABLE IF NOT EXISTS components (
35
36
  lib_id TEXT PRIMARY KEY,
36
37
  name TEXT NOT NULL,
@@ -42,110 +43,136 @@ class ComponentSearchIndex:
42
43
  category TEXT DEFAULT '',
43
44
  last_updated REAL DEFAULT 0
44
45
  )
45
- """)
46
-
46
+ """
47
+ )
48
+
47
49
  # Create search indexes for fast queries
48
- conn.execute("""
50
+ conn.execute(
51
+ """
49
52
  CREATE INDEX IF NOT EXISTS idx_name
50
53
  ON components(name COLLATE NOCASE)
51
- """)
52
-
53
- conn.execute("""
54
+ """
55
+ )
56
+
57
+ conn.execute(
58
+ """
54
59
  CREATE INDEX IF NOT EXISTS idx_description
55
60
  ON components(description COLLATE NOCASE)
56
- """)
57
-
58
- conn.execute("""
61
+ """
62
+ )
63
+
64
+ conn.execute(
65
+ """
59
66
  CREATE INDEX IF NOT EXISTS idx_library
60
67
  ON components(library)
61
- """)
62
-
63
- conn.execute("""
68
+ """
69
+ )
70
+
71
+ conn.execute(
72
+ """
64
73
  CREATE INDEX IF NOT EXISTS idx_category
65
74
  ON components(category)
66
- """)
67
-
75
+ """
76
+ )
77
+
68
78
  # Full-text search virtual table for advanced queries
69
- conn.execute("""
79
+ conn.execute(
80
+ """
70
81
  CREATE VIRTUAL TABLE IF NOT EXISTS components_fts
71
82
  USING fts5(lib_id, name, description, keywords, content=components)
72
- """)
73
-
83
+ """
84
+ )
85
+
74
86
  conn.commit()
75
87
  logger.debug("Initialized search index database")
76
-
88
+
77
89
  def rebuild_index(self, progress_callback: Optional[callable] = None) -> int:
78
90
  """Rebuild the search index from the symbol cache."""
79
91
  start_time = time.time()
80
92
  symbol_cache = get_symbol_cache()
81
-
93
+
82
94
  # Get all cached symbols
83
95
  symbols = []
84
96
  for lib_name in symbol_cache._library_index.keys():
85
97
  try:
86
98
  lib_symbols = symbol_cache.get_library_symbols(lib_name)
87
99
  symbols.extend(lib_symbols)
88
-
100
+
89
101
  if progress_callback:
90
102
  progress_callback(f"Indexing {lib_name}: {len(lib_symbols)} symbols")
91
-
103
+
92
104
  except Exception as e:
93
105
  logger.warning(f"Failed to load library {lib_name}: {e}")
94
-
106
+
95
107
  # Clear and rebuild index
96
108
  with sqlite3.connect(str(self.db_path)) as conn:
97
109
  conn.execute("DELETE FROM components")
98
110
  conn.execute("DELETE FROM components_fts")
99
-
111
+
100
112
  # Insert symbols in batches for better performance
101
113
  batch_size = 100
102
114
  for i in range(0, len(symbols), batch_size):
103
- batch = symbols[i:i + batch_size]
104
-
115
+ batch = symbols[i : i + batch_size]
116
+
105
117
  # Prepare batch data
106
118
  batch_data = []
107
119
  for symbol in batch:
108
- batch_data.append((
109
- symbol.lib_id,
110
- symbol.name,
111
- symbol.library,
112
- symbol.description,
113
- symbol.keywords,
114
- symbol.reference_prefix,
115
- len(symbol.pins),
116
- self._categorize_component(symbol),
117
- time.time()
118
- ))
119
-
120
+ batch_data.append(
121
+ (
122
+ symbol.lib_id,
123
+ symbol.name,
124
+ symbol.library,
125
+ symbol.description,
126
+ symbol.keywords,
127
+ symbol.reference_prefix,
128
+ len(symbol.pins),
129
+ self._categorize_component(symbol),
130
+ time.time(),
131
+ )
132
+ )
133
+
120
134
  # Insert batch
121
- conn.executemany("""
135
+ conn.executemany(
136
+ """
122
137
  INSERT OR REPLACE INTO components
123
138
  (lib_id, name, library, description, keywords, reference_prefix,
124
139
  pin_count, category, last_updated)
125
140
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
126
- """, batch_data)
127
-
141
+ """,
142
+ batch_data,
143
+ )
144
+
128
145
  # Update FTS table
129
- conn.executemany("""
146
+ conn.executemany(
147
+ """
130
148
  INSERT OR REPLACE INTO components_fts
131
149
  (lib_id, name, description, keywords)
132
150
  VALUES (?, ?, ?, ?)
133
- """, [(data[0], data[1], data[3], data[4]) for data in batch_data])
134
-
151
+ """,
152
+ [(data[0], data[1], data[3], data[4]) for data in batch_data],
153
+ )
154
+
135
155
  if progress_callback:
136
- progress_callback(f"Indexed {min(i + batch_size, len(symbols))}/{len(symbols)} components")
137
-
156
+ progress_callback(
157
+ f"Indexed {min(i + batch_size, len(symbols))}/{len(symbols)} components"
158
+ )
159
+
138
160
  conn.commit()
139
-
161
+
140
162
  elapsed = time.time() - start_time
141
163
  logger.info(f"Rebuilt search index with {len(symbols)} components in {elapsed:.2f}s")
142
164
  return len(symbols)
143
-
144
- def search(self, query: str, library: Optional[str] = None,
145
- category: Optional[str] = None, limit: int = 20) -> List[Dict[str, Any]]:
165
+
166
+ def search(
167
+ self,
168
+ query: str,
169
+ library: Optional[str] = None,
170
+ category: Optional[str] = None,
171
+ limit: int = 20,
172
+ ) -> List[Dict[str, Any]]:
146
173
  """Search components using multiple strategies."""
147
174
  results = []
148
-
175
+
149
176
  # Try different search strategies
150
177
  strategies = [
151
178
  self._search_exact_match,
@@ -153,39 +180,40 @@ class ComponentSearchIndex:
153
180
  self._search_contains,
154
181
  self._search_fts,
155
182
  ]
156
-
183
+
157
184
  for strategy in strategies:
158
185
  try:
159
186
  strategy_results = strategy(query, library, category, limit - len(results))
160
-
187
+
161
188
  # Avoid duplicates
162
189
  existing_ids = {r["lib_id"] for r in results}
163
190
  new_results = [r for r in strategy_results if r["lib_id"] not in existing_ids]
164
-
191
+
165
192
  results.extend(new_results)
166
-
193
+
167
194
  if len(results) >= limit:
168
195
  break
169
-
196
+
170
197
  except Exception as e:
171
198
  logger.debug(f"Search strategy failed: {e}")
172
-
199
+
173
200
  return results[:limit]
174
-
175
- def _search_exact_match(self, query: str, library: Optional[str],
176
- category: Optional[str], limit: int) -> List[Dict[str, Any]]:
201
+
202
+ def _search_exact_match(
203
+ self, query: str, library: Optional[str], category: Optional[str], limit: int
204
+ ) -> List[Dict[str, Any]]:
177
205
  """Search for exact name matches."""
178
206
  conditions = ["name = ? COLLATE NOCASE"]
179
207
  params = [query]
180
-
208
+
181
209
  if library:
182
210
  conditions.append("library = ?")
183
211
  params.append(library)
184
-
212
+
185
213
  if category:
186
214
  conditions.append("category = ?")
187
215
  params.append(category)
188
-
216
+
189
217
  sql = f"""
190
218
  SELECT lib_id, name, library, description, keywords, reference_prefix,
191
219
  pin_count, category, 1.0 as match_score
@@ -195,25 +223,26 @@ class ComponentSearchIndex:
195
223
  LIMIT ?
196
224
  """
197
225
  params.append(limit)
198
-
226
+
199
227
  with sqlite3.connect(str(self.db_path)) as conn:
200
228
  conn.row_factory = sqlite3.Row
201
229
  return [dict(row) for row in conn.execute(sql, params)]
202
-
203
- def _search_prefix_match(self, query: str, library: Optional[str],
204
- category: Optional[str], limit: int) -> List[Dict[str, Any]]:
230
+
231
+ def _search_prefix_match(
232
+ self, query: str, library: Optional[str], category: Optional[str], limit: int
233
+ ) -> List[Dict[str, Any]]:
205
234
  """Search for components starting with query."""
206
235
  conditions = ["name LIKE ? COLLATE NOCASE"]
207
236
  params = [f"{query}%"]
208
-
237
+
209
238
  if library:
210
239
  conditions.append("library = ?")
211
240
  params.append(library)
212
-
241
+
213
242
  if category:
214
243
  conditions.append("category = ?")
215
244
  params.append(category)
216
-
245
+
217
246
  sql = f"""
218
247
  SELECT lib_id, name, library, description, keywords, reference_prefix,
219
248
  pin_count, category, 0.8 as match_score
@@ -223,25 +252,26 @@ class ComponentSearchIndex:
223
252
  LIMIT ?
224
253
  """
225
254
  params.append(limit)
226
-
255
+
227
256
  with sqlite3.connect(str(self.db_path)) as conn:
228
257
  conn.row_factory = sqlite3.Row
229
258
  return [dict(row) for row in conn.execute(sql, params)]
230
-
231
- def _search_contains(self, query: str, library: Optional[str],
232
- category: Optional[str], limit: int) -> List[Dict[str, Any]]:
259
+
260
+ def _search_contains(
261
+ self, query: str, library: Optional[str], category: Optional[str], limit: int
262
+ ) -> List[Dict[str, Any]]:
233
263
  """Search for components containing query in name or description."""
234
264
  conditions = ["(name LIKE ? COLLATE NOCASE OR description LIKE ? COLLATE NOCASE)"]
235
265
  params = [f"%{query}%", f"%{query}%"]
236
-
266
+
237
267
  if library:
238
268
  conditions.append("library = ?")
239
269
  params.append(library)
240
-
270
+
241
271
  if category:
242
272
  conditions.append("category = ?")
243
273
  params.append(category)
244
-
274
+
245
275
  sql = f"""
246
276
  SELECT lib_id, name, library, description, keywords, reference_prefix,
247
277
  pin_count, category, 0.6 as match_score
@@ -253,17 +283,18 @@ class ComponentSearchIndex:
253
283
  LIMIT ?
254
284
  """
255
285
  params.extend([f"%{query}%", limit])
256
-
286
+
257
287
  with sqlite3.connect(str(self.db_path)) as conn:
258
288
  conn.row_factory = sqlite3.Row
259
289
  return [dict(row) for row in conn.execute(sql, params)]
260
-
261
- def _search_fts(self, query: str, library: Optional[str],
262
- category: Optional[str], limit: int) -> List[Dict[str, Any]]:
290
+
291
+ def _search_fts(
292
+ self, query: str, library: Optional[str], category: Optional[str], limit: int
293
+ ) -> List[Dict[str, Any]]:
263
294
  """Full-text search using FTS5."""
264
295
  # Build FTS query
265
- fts_query = ' '.join(f'"{term}"*' for term in query.split())
266
-
296
+ fts_query = " ".join(f'"{term}"*' for term in query.split())
297
+
267
298
  sql = """
268
299
  SELECT c.lib_id, c.name, c.library, c.description, c.keywords,
269
300
  c.reference_prefix, c.pin_count, c.category,
@@ -273,18 +304,18 @@ class ComponentSearchIndex:
273
304
  WHERE fts MATCH ?
274
305
  """
275
306
  params = [fts_query]
276
-
307
+
277
308
  if library:
278
309
  sql += " AND c.library = ?"
279
310
  params.append(library)
280
-
311
+
281
312
  if category:
282
313
  sql += " AND c.category = ?"
283
314
  params.append(category)
284
-
315
+
285
316
  sql += " ORDER BY fts.rank LIMIT ?"
286
317
  params.append(limit)
287
-
318
+
288
319
  try:
289
320
  with sqlite3.connect(str(self.db_path)) as conn:
290
321
  conn.row_factory = sqlite3.Row
@@ -292,7 +323,7 @@ class ComponentSearchIndex:
292
323
  except sqlite3.OperationalError:
293
324
  # FTS query failed, return empty results
294
325
  return []
295
-
326
+
296
327
  def get_libraries(self) -> List[Dict[str, Any]]:
297
328
  """Get all available libraries with component counts."""
298
329
  sql = """
@@ -301,11 +332,11 @@ class ComponentSearchIndex:
301
332
  GROUP BY library
302
333
  ORDER BY library
303
334
  """
304
-
335
+
305
336
  with sqlite3.connect(str(self.db_path)) as conn:
306
337
  conn.row_factory = sqlite3.Row
307
338
  return [dict(row) for row in conn.execute(sql)]
308
-
339
+
309
340
  def get_categories(self) -> List[Dict[str, Any]]:
310
341
  """Get all component categories with counts."""
311
342
  sql = """
@@ -315,11 +346,11 @@ class ComponentSearchIndex:
315
346
  GROUP BY category
316
347
  ORDER BY component_count DESC
317
348
  """
318
-
349
+
319
350
  with sqlite3.connect(str(self.db_path)) as conn:
320
351
  conn.row_factory = sqlite3.Row
321
352
  return [dict(row) for row in conn.execute(sql)]
322
-
353
+
323
354
  def validate_component(self, lib_id: str) -> Optional[Dict[str, Any]]:
324
355
  """Check if a component exists in the index."""
325
356
  sql = """
@@ -328,41 +359,45 @@ class ComponentSearchIndex:
328
359
  FROM components
329
360
  WHERE lib_id = ?
330
361
  """
331
-
362
+
332
363
  with sqlite3.connect(str(self.db_path)) as conn:
333
364
  conn.row_factory = sqlite3.Row
334
365
  result = conn.execute(sql, [lib_id]).fetchone()
335
366
  return dict(result) if result else None
336
-
367
+
337
368
  def get_stats(self) -> Dict[str, Any]:
338
369
  """Get search index statistics."""
339
370
  with sqlite3.connect(str(self.db_path)) as conn:
340
371
  total_components = conn.execute("SELECT COUNT(*) FROM components").fetchone()[0]
341
- total_libraries = conn.execute("SELECT COUNT(DISTINCT library) FROM components").fetchone()[0]
342
-
372
+ total_libraries = conn.execute(
373
+ "SELECT COUNT(DISTINCT library) FROM components"
374
+ ).fetchone()[0]
375
+
343
376
  # Get library breakdown
344
- library_stats = conn.execute("""
377
+ library_stats = conn.execute(
378
+ """
345
379
  SELECT library, COUNT(*) as count
346
380
  FROM components
347
381
  GROUP BY library
348
382
  ORDER BY count DESC
349
383
  LIMIT 10
350
- """).fetchall()
351
-
384
+ """
385
+ ).fetchall()
386
+
352
387
  return {
353
388
  "total_components": total_components,
354
389
  "total_libraries": total_libraries,
355
390
  "top_libraries": [{"library": lib, "count": count} for lib, count in library_stats],
356
391
  "database_path": str(self.db_path),
357
- "database_size_mb": round(self.db_path.stat().st_size / (1024 * 1024), 2)
392
+ "database_size_mb": round(self.db_path.stat().st_size / (1024 * 1024), 2),
358
393
  }
359
-
394
+
360
395
  def _categorize_component(self, symbol: SymbolDefinition) -> str:
361
396
  """Categorize a component based on its properties."""
362
397
  prefix = symbol.reference_prefix.upper()
363
398
  name_lower = symbol.name.lower()
364
399
  desc_lower = symbol.description.lower()
365
-
400
+
366
401
  # Category mapping based on reference prefix and description
367
402
  if prefix == "R":
368
403
  return "resistor"
@@ -410,7 +445,7 @@ def get_search_index() -> ComponentSearchIndex:
410
445
  def ensure_index_built(rebuild: bool = False) -> int:
411
446
  """Ensure the search index is built and up-to-date."""
412
447
  index = get_search_index()
413
-
448
+
414
449
  if rebuild or not index.db_path.exists():
415
450
  logger.info("Building component search index...")
416
451
  return index.rebuild_index()
@@ -418,4 +453,4 @@ def ensure_index_built(rebuild: bool = False) -> int:
418
453
  # Check if index needs updating based on symbol cache
419
454
  stats = index.get_stats()
420
455
  logger.info(f"Search index ready: {stats['total_components']} components")
421
- return stats["total_components"]
456
+ return stats["total_components"]