kicad-sch-api 0.0.1__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.

@@ -0,0 +1,652 @@
1
+ """
2
+ Enhanced component management for KiCAD schematics.
3
+
4
+ This module provides a modern, intuitive API for working with schematic components,
5
+ featuring fast lookup, bulk operations, and advanced filtering capabilities.
6
+ """
7
+
8
+ import logging
9
+ import uuid
10
+ from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
11
+
12
+ from ..library.cache import SymbolDefinition, get_symbol_cache
13
+ from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
14
+ from .types import Point, SchematicPin, SchematicSymbol
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class Component:
20
+ """
21
+ Enhanced wrapper for schematic components with modern API.
22
+
23
+ Provides intuitive access to component properties, pins, and operations
24
+ while maintaining exact format preservation for professional use.
25
+ """
26
+
27
+ def __init__(self, symbol_data: SchematicSymbol, parent_collection: "ComponentCollection"):
28
+ """
29
+ Initialize component wrapper.
30
+
31
+ Args:
32
+ symbol_data: Underlying symbol data
33
+ parent_collection: Parent collection for updates
34
+ """
35
+ self._data = symbol_data
36
+ self._collection = parent_collection
37
+ self._validator = SchematicValidator()
38
+
39
+ # Core properties with validation
40
+ @property
41
+ def reference(self) -> str:
42
+ """Component reference (e.g., 'R1')."""
43
+ return self._data.reference
44
+
45
+ @reference.setter
46
+ def reference(self, value: str):
47
+ """Set component reference with validation."""
48
+ if not self._validator.validate_reference(value):
49
+ raise ValidationError(f"Invalid reference format: {value}")
50
+
51
+ # Check for duplicates in parent collection
52
+ if self._collection.get(value) is not None:
53
+ raise ValidationError(f"Reference {value} already exists")
54
+
55
+ old_ref = self._data.reference
56
+ self._data.reference = value
57
+ self._collection._update_reference_index(old_ref, value)
58
+ logger.debug(f"Updated reference: {old_ref} -> {value}")
59
+
60
+ @property
61
+ def value(self) -> str:
62
+ """Component value (e.g., '10k')."""
63
+ return self._data.value
64
+
65
+ @value.setter
66
+ def value(self, value: str):
67
+ """Set component value."""
68
+ self._data.value = value
69
+ self._collection._mark_modified()
70
+
71
+ @property
72
+ def footprint(self) -> Optional[str]:
73
+ """Component footprint."""
74
+ return self._data.footprint
75
+
76
+ @footprint.setter
77
+ def footprint(self, value: Optional[str]):
78
+ """Set component footprint."""
79
+ self._data.footprint = value
80
+ self._collection._mark_modified()
81
+
82
+ @property
83
+ def position(self) -> Point:
84
+ """Component position."""
85
+ return self._data.position
86
+
87
+ @position.setter
88
+ def position(self, value: Union[Point, Tuple[float, float]]):
89
+ """Set component position."""
90
+ if isinstance(value, tuple):
91
+ value = Point(value[0], value[1])
92
+ self._data.position = value
93
+ self._collection._mark_modified()
94
+
95
+ @property
96
+ def rotation(self) -> float:
97
+ """Component rotation in degrees."""
98
+ return self._data.rotation
99
+
100
+ @rotation.setter
101
+ def rotation(self, value: float):
102
+ """Set component rotation."""
103
+ self._data.rotation = float(value) % 360
104
+ self._collection._mark_modified()
105
+
106
+ @property
107
+ def lib_id(self) -> str:
108
+ """Library ID (e.g., 'Device:R')."""
109
+ return self._data.lib_id
110
+
111
+ @property
112
+ def library(self) -> str:
113
+ """Library name."""
114
+ return self._data.library
115
+
116
+ @property
117
+ def symbol_name(self) -> str:
118
+ """Symbol name within library."""
119
+ return self._data.symbol_name
120
+
121
+ # Properties dictionary
122
+ @property
123
+ def properties(self) -> Dict[str, str]:
124
+ """Dictionary of all component properties."""
125
+ return self._data.properties
126
+
127
+ def get_property(self, name: str, default: Optional[str] = None) -> Optional[str]:
128
+ """Get property value by name."""
129
+ return self._data.properties.get(name, default)
130
+
131
+ def set_property(self, name: str, value: str):
132
+ """Set property value with validation."""
133
+ if not isinstance(name, str) or not isinstance(value, str):
134
+ raise ValidationError("Property name and value must be strings")
135
+
136
+ self._data.properties[name] = value
137
+ self._collection._mark_modified()
138
+ logger.debug(f"Set property {self.reference}.{name} = {value}")
139
+
140
+ def remove_property(self, name: str) -> bool:
141
+ """Remove property by name."""
142
+ if name in self._data.properties:
143
+ del self._data.properties[name]
144
+ self._collection._mark_modified()
145
+ return True
146
+ return False
147
+
148
+ # Pin access
149
+ @property
150
+ def pins(self) -> List[SchematicPin]:
151
+ """List of component pins."""
152
+ return self._data.pins
153
+
154
+ def get_pin(self, pin_number: str) -> Optional[SchematicPin]:
155
+ """Get pin by number."""
156
+ return self._data.get_pin(pin_number)
157
+
158
+ def get_pin_position(self, pin_number: str) -> Optional[Point]:
159
+ """Get absolute position of pin."""
160
+ return self._data.get_pin_position(pin_number)
161
+
162
+ # Component state
163
+ @property
164
+ def in_bom(self) -> bool:
165
+ """Whether component appears in bill of materials."""
166
+ return self._data.in_bom
167
+
168
+ @in_bom.setter
169
+ def in_bom(self, value: bool):
170
+ """Set BOM inclusion."""
171
+ self._data.in_bom = bool(value)
172
+ self._collection._mark_modified()
173
+
174
+ @property
175
+ def on_board(self) -> bool:
176
+ """Whether component appears on PCB."""
177
+ return self._data.on_board
178
+
179
+ @on_board.setter
180
+ def on_board(self, value: bool):
181
+ """Set board inclusion."""
182
+ self._data.on_board = bool(value)
183
+ self._collection._mark_modified()
184
+
185
+ # Utility methods
186
+ def move(self, x: float, y: float):
187
+ """Move component to new position."""
188
+ self.position = Point(x, y)
189
+
190
+ def translate(self, dx: float, dy: float):
191
+ """Translate component by offset."""
192
+ current = self.position
193
+ self.position = Point(current.x + dx, current.y + dy)
194
+
195
+ def rotate(self, angle: float):
196
+ """Rotate component by angle (degrees)."""
197
+ self.rotation = (self.rotation + angle) % 360
198
+
199
+ def copy_properties_from(self, other: "Component"):
200
+ """Copy all properties from another component."""
201
+ for name, value in other.properties.items():
202
+ self.set_property(name, value)
203
+
204
+ def get_symbol_definition(self) -> Optional[SymbolDefinition]:
205
+ """Get the symbol definition from library cache."""
206
+ cache = get_symbol_cache()
207
+ return cache.get_symbol(self.lib_id)
208
+
209
+ def update_from_library(self) -> bool:
210
+ """Update component pins and metadata from library definition."""
211
+ symbol_def = self.get_symbol_definition()
212
+ if not symbol_def:
213
+ return False
214
+
215
+ # Update pins
216
+ self._data.pins = symbol_def.pins.copy()
217
+
218
+ # Update reference prefix if needed
219
+ if not self.reference.startswith(symbol_def.reference_prefix):
220
+ logger.warning(
221
+ f"Reference {self.reference} doesn't match expected prefix {symbol_def.reference_prefix}"
222
+ )
223
+
224
+ self._collection._mark_modified()
225
+ return True
226
+
227
+ def validate(self) -> List[ValidationIssue]:
228
+ """Validate this component."""
229
+ return self._validator.validate_component(self._data.__dict__)
230
+
231
+ def to_dict(self) -> Dict[str, Any]:
232
+ """Convert component to dictionary representation."""
233
+ return {
234
+ "reference": self.reference,
235
+ "lib_id": self.lib_id,
236
+ "value": self.value,
237
+ "footprint": self.footprint,
238
+ "position": {"x": self.position.x, "y": self.position.y},
239
+ "rotation": self.rotation,
240
+ "properties": self.properties.copy(),
241
+ "in_bom": self.in_bom,
242
+ "on_board": self.on_board,
243
+ "pin_count": len(self.pins),
244
+ }
245
+
246
+ def __str__(self) -> str:
247
+ """String representation."""
248
+ return f"<Component {self.reference}: {self.lib_id} = '{self.value}' @ {self.position}>"
249
+
250
+ def __repr__(self) -> str:
251
+ """Detailed representation."""
252
+ return (
253
+ f"Component(ref='{self.reference}', lib_id='{self.lib_id}', "
254
+ f"value='{self.value}', pos={self.position}, rotation={self.rotation})"
255
+ )
256
+
257
+
258
+ class ComponentCollection:
259
+ """
260
+ Collection class for efficient component management.
261
+
262
+ Provides fast lookup, filtering, and bulk operations for schematic components.
263
+ Optimized for schematics with hundreds or thousands of components.
264
+ """
265
+
266
+ def __init__(self, components: List[SchematicSymbol] = None):
267
+ """
268
+ Initialize component collection.
269
+
270
+ Args:
271
+ components: Initial list of component data
272
+ """
273
+ self._components: List[Component] = []
274
+ self._reference_index: Dict[str, Component] = {}
275
+ self._lib_id_index: Dict[str, List[Component]] = {}
276
+ self._value_index: Dict[str, List[Component]] = {}
277
+ self._modified = False
278
+
279
+ # Add initial components
280
+ if components:
281
+ for comp_data in components:
282
+ self._add_to_indexes(Component(comp_data, self))
283
+
284
+ logger.debug(f"ComponentCollection initialized with {len(self._components)} components")
285
+
286
+ def add(
287
+ self,
288
+ lib_id: str,
289
+ reference: Optional[str] = None,
290
+ value: str = "",
291
+ position: Optional[Union[Point, Tuple[float, float]]] = None,
292
+ footprint: Optional[str] = None,
293
+ **properties,
294
+ ) -> Component:
295
+ """
296
+ Add a new component to the schematic.
297
+
298
+ Args:
299
+ lib_id: Library identifier (e.g., "Device:R")
300
+ reference: Component reference (auto-generated if None)
301
+ value: Component value
302
+ position: Component position (auto-placed if None)
303
+ footprint: Component footprint
304
+ **properties: Additional component properties
305
+
306
+ Returns:
307
+ Newly created Component
308
+
309
+ Raises:
310
+ ValidationError: If component data is invalid
311
+ """
312
+ # Validate lib_id
313
+ validator = SchematicValidator()
314
+ if not validator.validate_lib_id(lib_id):
315
+ raise ValidationError(f"Invalid lib_id format: {lib_id}")
316
+
317
+ # Generate reference if not provided
318
+ if not reference:
319
+ reference = self._generate_reference(lib_id)
320
+
321
+ # Validate reference
322
+ if not validator.validate_reference(reference):
323
+ raise ValidationError(f"Invalid reference format: {reference}")
324
+
325
+ # Check for duplicate reference
326
+ if reference in self._reference_index:
327
+ raise ValidationError(f"Reference {reference} already exists")
328
+
329
+ # Set default position if not provided
330
+ if position is None:
331
+ position = self._find_available_position()
332
+ elif isinstance(position, tuple):
333
+ position = Point(position[0], position[1])
334
+
335
+ # Create component data
336
+ component_data = SchematicSymbol(
337
+ uuid=str(uuid.uuid4()),
338
+ lib_id=lib_id,
339
+ position=position,
340
+ reference=reference,
341
+ value=value,
342
+ footprint=footprint,
343
+ properties=properties,
344
+ )
345
+
346
+ # Get symbol definition and update pins
347
+ symbol_cache = get_symbol_cache()
348
+ symbol_def = symbol_cache.get_symbol(lib_id)
349
+ if symbol_def:
350
+ component_data.pins = symbol_def.pins.copy()
351
+
352
+ # Create component wrapper
353
+ component = Component(component_data, self)
354
+
355
+ # Add to collection
356
+ self._add_to_indexes(component)
357
+ self._modified = True
358
+
359
+ logger.info(f"Added component: {reference} ({lib_id})")
360
+ return component
361
+
362
+ def remove(self, reference: str) -> bool:
363
+ """
364
+ Remove component by reference.
365
+
366
+ Args:
367
+ reference: Component reference to remove
368
+
369
+ Returns:
370
+ True if component was removed
371
+ """
372
+ component = self._reference_index.get(reference)
373
+ if not component:
374
+ return False
375
+
376
+ # Remove from all indexes
377
+ self._remove_from_indexes(component)
378
+ self._modified = True
379
+
380
+ logger.info(f"Removed component: {reference}")
381
+ return True
382
+
383
+ def get(self, reference: str) -> Optional[Component]:
384
+ """Get component by reference."""
385
+ return self._reference_index.get(reference)
386
+
387
+ def filter(self, **criteria) -> List[Component]:
388
+ """
389
+ Filter components by various criteria.
390
+
391
+ Args:
392
+ lib_id: Filter by library ID
393
+ value: Filter by value (exact match)
394
+ value_pattern: Filter by value pattern (contains)
395
+ reference_pattern: Filter by reference pattern
396
+ footprint: Filter by footprint
397
+ in_area: Filter by area (tuple of (x1, y1, x2, y2))
398
+
399
+ Returns:
400
+ List of matching components
401
+ """
402
+ results = list(self._components)
403
+
404
+ # Apply filters
405
+ if "lib_id" in criteria:
406
+ lib_id = criteria["lib_id"]
407
+ results = [c for c in results if c.lib_id == lib_id]
408
+
409
+ if "value" in criteria:
410
+ value = criteria["value"]
411
+ results = [c for c in results if c.value == value]
412
+
413
+ if "value_pattern" in criteria:
414
+ pattern = criteria["value_pattern"].lower()
415
+ results = [c for c in results if pattern in c.value.lower()]
416
+
417
+ if "reference_pattern" in criteria:
418
+ import re
419
+
420
+ pattern = re.compile(criteria["reference_pattern"])
421
+ results = [c for c in results if pattern.match(c.reference)]
422
+
423
+ if "footprint" in criteria:
424
+ footprint = criteria["footprint"]
425
+ results = [c for c in results if c.footprint == footprint]
426
+
427
+ if "in_area" in criteria:
428
+ x1, y1, x2, y2 = criteria["in_area"]
429
+ results = [c for c in results if x1 <= c.position.x <= x2 and y1 <= c.position.y <= y2]
430
+
431
+ if "has_property" in criteria:
432
+ prop_name = criteria["has_property"]
433
+ results = [c for c in results if prop_name in c.properties]
434
+
435
+ return results
436
+
437
+ def filter_by_type(self, component_type: str) -> List[Component]:
438
+ """Filter components by type (e.g., 'R' for resistors)."""
439
+ return [
440
+ c for c in self._components if c.symbol_name.upper().startswith(component_type.upper())
441
+ ]
442
+
443
+ def in_area(self, x1: float, y1: float, x2: float, y2: float) -> List[Component]:
444
+ """Get components within rectangular area."""
445
+ return self.filter(in_area=(x1, y1, x2, y2))
446
+
447
+ def near_point(
448
+ self, point: Union[Point, Tuple[float, float]], radius: float
449
+ ) -> List[Component]:
450
+ """Get components within radius of a point."""
451
+ if isinstance(point, tuple):
452
+ point = Point(point[0], point[1])
453
+
454
+ results = []
455
+ for component in self._components:
456
+ if component.position.distance_to(point) <= radius:
457
+ results.append(component)
458
+ return results
459
+
460
+ def bulk_update(self, criteria: Dict[str, Any], updates: Dict[str, Any]) -> int:
461
+ """
462
+ Update multiple components matching criteria.
463
+
464
+ Args:
465
+ criteria: Filter criteria (same as filter method)
466
+ updates: Dictionary of property updates
467
+
468
+ Returns:
469
+ Number of components updated
470
+ """
471
+ matching = self.filter(**criteria)
472
+
473
+ for component in matching:
474
+ # Update basic properties
475
+ for key, value in updates.items():
476
+ if hasattr(component, key):
477
+ setattr(component, key, value)
478
+ else:
479
+ # Add as custom property
480
+ component.set_property(key, str(value))
481
+
482
+ if matching:
483
+ self._modified = True
484
+
485
+ logger.info(f"Bulk updated {len(matching)} components")
486
+ return len(matching)
487
+
488
+ def sort_by_reference(self):
489
+ """Sort components by reference designator."""
490
+ self._components.sort(key=lambda c: c.reference)
491
+
492
+ def sort_by_position(self, by_x: bool = True):
493
+ """Sort components by position."""
494
+ if by_x:
495
+ self._components.sort(key=lambda c: (c.position.x, c.position.y))
496
+ else:
497
+ self._components.sort(key=lambda c: (c.position.y, c.position.x))
498
+
499
+ def validate_all(self) -> List[ValidationIssue]:
500
+ """Validate all components in collection."""
501
+ all_issues = []
502
+ validator = SchematicValidator()
503
+
504
+ # Validate individual components
505
+ for component in self._components:
506
+ issues = component.validate()
507
+ all_issues.extend(issues)
508
+
509
+ # Validate collection-level rules
510
+ references = [c.reference for c in self._components]
511
+ if len(references) != len(set(references)):
512
+ # Find duplicates
513
+ seen = set()
514
+ duplicates = set()
515
+ for ref in references:
516
+ if ref in seen:
517
+ duplicates.add(ref)
518
+ seen.add(ref)
519
+
520
+ for ref in duplicates:
521
+ all_issues.append(
522
+ ValidationIssue(
523
+ category="reference", message=f"Duplicate reference: {ref}", level="error"
524
+ )
525
+ )
526
+
527
+ return all_issues
528
+
529
+ def get_statistics(self) -> Dict[str, Any]:
530
+ """Get collection statistics."""
531
+ lib_counts = {}
532
+ value_counts = {}
533
+
534
+ for component in self._components:
535
+ # Count by library
536
+ lib = component.library
537
+ lib_counts[lib] = lib_counts.get(lib, 0) + 1
538
+
539
+ # Count by value
540
+ value = component.value
541
+ if value:
542
+ value_counts[value] = value_counts.get(value, 0) + 1
543
+
544
+ return {
545
+ "total_components": len(self._components),
546
+ "unique_references": len(self._reference_index),
547
+ "libraries_used": len(lib_counts),
548
+ "library_breakdown": lib_counts,
549
+ "most_common_values": sorted(value_counts.items(), key=lambda x: x[1], reverse=True)[
550
+ :10
551
+ ],
552
+ "modified": self._modified,
553
+ }
554
+
555
+ # Collection interface
556
+ def __len__(self) -> int:
557
+ """Number of components."""
558
+ return len(self._components)
559
+
560
+ def __iter__(self) -> Iterator[Component]:
561
+ """Iterate over components."""
562
+ return iter(self._components)
563
+
564
+ def __getitem__(self, key: Union[int, str]) -> Component:
565
+ """Get component by index or reference."""
566
+ if isinstance(key, int):
567
+ return self._components[key]
568
+ elif isinstance(key, str):
569
+ component = self._reference_index.get(key)
570
+ if component is None:
571
+ raise KeyError(f"Component not found: {key}")
572
+ return component
573
+ else:
574
+ raise TypeError(f"Invalid key type: {type(key)}")
575
+
576
+ def __contains__(self, reference: str) -> bool:
577
+ """Check if reference exists."""
578
+ return reference in self._reference_index
579
+
580
+ # Internal methods
581
+ def _add_to_indexes(self, component: Component):
582
+ """Add component to all indexes."""
583
+ self._components.append(component)
584
+ self._reference_index[component.reference] = component
585
+
586
+ # Add to lib_id index
587
+ lib_id = component.lib_id
588
+ if lib_id not in self._lib_id_index:
589
+ self._lib_id_index[lib_id] = []
590
+ self._lib_id_index[lib_id].append(component)
591
+
592
+ # Add to value index
593
+ value = component.value
594
+ if value:
595
+ if value not in self._value_index:
596
+ self._value_index[value] = []
597
+ self._value_index[value].append(component)
598
+
599
+ def _remove_from_indexes(self, component: Component):
600
+ """Remove component from all indexes."""
601
+ self._components.remove(component)
602
+ del self._reference_index[component.reference]
603
+
604
+ # Remove from lib_id index
605
+ lib_id = component.lib_id
606
+ if lib_id in self._lib_id_index:
607
+ self._lib_id_index[lib_id].remove(component)
608
+ if not self._lib_id_index[lib_id]:
609
+ del self._lib_id_index[lib_id]
610
+
611
+ # Remove from value index
612
+ value = component.value
613
+ if value and value in self._value_index:
614
+ self._value_index[value].remove(component)
615
+ if not self._value_index[value]:
616
+ del self._value_index[value]
617
+
618
+ def _update_reference_index(self, old_ref: str, new_ref: str):
619
+ """Update reference index when component reference changes."""
620
+ if old_ref in self._reference_index:
621
+ component = self._reference_index[old_ref]
622
+ del self._reference_index[old_ref]
623
+ self._reference_index[new_ref] = component
624
+
625
+ def _mark_modified(self):
626
+ """Mark collection as modified."""
627
+ self._modified = True
628
+
629
+ def _generate_reference(self, lib_id: str) -> str:
630
+ """Generate unique reference for component."""
631
+ # Get reference prefix from symbol definition
632
+ symbol_cache = get_symbol_cache()
633
+ symbol_def = symbol_cache.get_symbol(lib_id)
634
+ prefix = symbol_def.reference_prefix if symbol_def else "U"
635
+
636
+ # Find next available number
637
+ counter = 1
638
+ while f"{prefix}{counter}" in self._reference_index:
639
+ counter += 1
640
+
641
+ return f"{prefix}{counter}"
642
+
643
+ def _find_available_position(self) -> Point:
644
+ """Find an available position for automatic placement."""
645
+ # Simple grid placement - could be enhanced with collision detection
646
+ grid_size = 10.0 # 10mm grid
647
+ max_per_row = 10
648
+
649
+ row = len(self._components) // max_per_row
650
+ col = len(self._components) % max_per_row
651
+
652
+ return Point(col * grid_size, row * grid_size)