kicad-sch-api 0.3.4__py3-none-any.whl → 0.4.0__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.

Files changed (47) hide show
  1. kicad_sch_api/collections/__init__.py +21 -0
  2. kicad_sch_api/collections/base.py +294 -0
  3. kicad_sch_api/collections/components.py +434 -0
  4. kicad_sch_api/collections/junctions.py +366 -0
  5. kicad_sch_api/collections/labels.py +404 -0
  6. kicad_sch_api/collections/wires.py +406 -0
  7. kicad_sch_api/core/components.py +5 -0
  8. kicad_sch_api/core/formatter.py +3 -1
  9. kicad_sch_api/core/labels.py +348 -0
  10. kicad_sch_api/core/managers/__init__.py +26 -0
  11. kicad_sch_api/core/managers/file_io.py +243 -0
  12. kicad_sch_api/core/managers/format_sync.py +501 -0
  13. kicad_sch_api/core/managers/graphics.py +579 -0
  14. kicad_sch_api/core/managers/metadata.py +268 -0
  15. kicad_sch_api/core/managers/sheet.py +454 -0
  16. kicad_sch_api/core/managers/text_elements.py +536 -0
  17. kicad_sch_api/core/managers/validation.py +474 -0
  18. kicad_sch_api/core/managers/wire.py +346 -0
  19. kicad_sch_api/core/nets.py +310 -0
  20. kicad_sch_api/core/no_connects.py +276 -0
  21. kicad_sch_api/core/parser.py +75 -41
  22. kicad_sch_api/core/schematic.py +904 -1074
  23. kicad_sch_api/core/texts.py +343 -0
  24. kicad_sch_api/core/types.py +13 -4
  25. kicad_sch_api/geometry/font_metrics.py +3 -1
  26. kicad_sch_api/geometry/symbol_bbox.py +56 -43
  27. kicad_sch_api/interfaces/__init__.py +17 -0
  28. kicad_sch_api/interfaces/parser.py +76 -0
  29. kicad_sch_api/interfaces/repository.py +70 -0
  30. kicad_sch_api/interfaces/resolver.py +117 -0
  31. kicad_sch_api/parsers/__init__.py +14 -0
  32. kicad_sch_api/parsers/base.py +145 -0
  33. kicad_sch_api/parsers/label_parser.py +254 -0
  34. kicad_sch_api/parsers/registry.py +155 -0
  35. kicad_sch_api/parsers/symbol_parser.py +222 -0
  36. kicad_sch_api/parsers/wire_parser.py +99 -0
  37. kicad_sch_api/symbols/__init__.py +18 -0
  38. kicad_sch_api/symbols/cache.py +467 -0
  39. kicad_sch_api/symbols/resolver.py +361 -0
  40. kicad_sch_api/symbols/validators.py +504 -0
  41. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/METADATA +1 -1
  42. kicad_sch_api-0.4.0.dist-info/RECORD +67 -0
  43. kicad_sch_api-0.3.4.dist-info/RECORD +0 -34
  44. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/WHEEL +0 -0
  45. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/entry_points.txt +0 -0
  46. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/licenses/LICENSE +0 -0
  47. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,434 @@
1
+ """
2
+ Component collection with specialized indexing and component-specific operations.
3
+
4
+ Extends the base IndexedCollection to provide component-specific features like
5
+ reference indexing, lib_id grouping, and automatic reference generation.
6
+ """
7
+
8
+ import logging
9
+ import uuid
10
+ from typing import Any, Dict, List, Optional, Tuple, Union
11
+
12
+ from ..core.types import Point, SchematicSymbol
13
+ from ..library.cache import get_symbol_cache
14
+ from ..utils.validation import SchematicValidator, ValidationError
15
+ from .base import IndexedCollection
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class Component:
21
+ """
22
+ Enhanced wrapper for schematic components with modern API.
23
+
24
+ Provides intuitive access to component properties, pins, and operations
25
+ while maintaining exact format preservation for professional use.
26
+ """
27
+
28
+ def __init__(self, symbol_data: SchematicSymbol, parent_collection: "ComponentCollection"):
29
+ """
30
+ Initialize component wrapper.
31
+
32
+ Args:
33
+ symbol_data: Underlying symbol data
34
+ parent_collection: Parent collection for updates
35
+ """
36
+ self._data = symbol_data
37
+ self._collection = parent_collection
38
+ self._validator = SchematicValidator()
39
+
40
+ @property
41
+ def uuid(self) -> str:
42
+ """Component UUID."""
43
+ return self._data.uuid
44
+
45
+ @property
46
+ def reference(self) -> str:
47
+ """Component reference (e.g., 'R1')."""
48
+ return self._data.reference
49
+
50
+ @reference.setter
51
+ def reference(self, value: str):
52
+ """Set component reference with validation."""
53
+ if not self._validator.validate_reference(value):
54
+ raise ValidationError(f"Invalid reference format: {value}")
55
+
56
+ # Check for duplicates in parent collection
57
+ if self._collection.get_by_reference(value) is not None:
58
+ raise ValidationError(f"Reference {value} already exists")
59
+
60
+ old_ref = self._data.reference
61
+ self._data.reference = value
62
+ self._collection._update_reference_index(old_ref, value)
63
+ self._collection._mark_modified()
64
+ logger.debug(f"Updated reference: {old_ref} -> {value}")
65
+
66
+ @property
67
+ def value(self) -> str:
68
+ """Component value (e.g., '10k')."""
69
+ return self._data.value
70
+
71
+ @value.setter
72
+ def value(self, value: str):
73
+ """Set component value."""
74
+ self._data.value = value
75
+ self._collection._mark_modified()
76
+
77
+ @property
78
+ def lib_id(self) -> str:
79
+ """Component library ID (e.g., 'Device:R')."""
80
+ return self._data.lib_id
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 = value
104
+ self._collection._mark_modified()
105
+
106
+ @property
107
+ def footprint(self) -> Optional[str]:
108
+ """Component footprint."""
109
+ return self._data.footprint
110
+
111
+ @footprint.setter
112
+ def footprint(self, value: Optional[str]):
113
+ """Set component footprint."""
114
+ self._data.footprint = value
115
+ self._collection._mark_modified()
116
+
117
+ def get_property(self, name: str) -> Optional[str]:
118
+ """Get component property value."""
119
+ return self._data.properties.get(name)
120
+
121
+ def set_property(self, name: str, value: str) -> None:
122
+ """Set component property value."""
123
+ self._data.properties[name] = value
124
+ self._collection._mark_modified()
125
+
126
+ def __repr__(self) -> str:
127
+ """Detailed representation."""
128
+ return (
129
+ f"Component(ref='{self.reference}', lib_id='{self.lib_id}', "
130
+ f"value='{self.value}', pos={self.position}, rotation={self.rotation})"
131
+ )
132
+
133
+
134
+ class ComponentCollection(IndexedCollection[Component]):
135
+ """
136
+ Collection class for efficient component management.
137
+
138
+ Extends IndexedCollection with component-specific features:
139
+ - Reference-based indexing for fast component lookup
140
+ - Lib_id grouping for filtering by component type
141
+ - Value indexing for filtering by component value
142
+ - Automatic reference generation
143
+ - Component validation and conflict detection
144
+ """
145
+
146
+ def __init__(self, components: Optional[List[SchematicSymbol]] = None):
147
+ """
148
+ Initialize component collection.
149
+
150
+ Args:
151
+ components: Initial list of component data
152
+ """
153
+ self._reference_index: Dict[str, Component] = {}
154
+ self._lib_id_index: Dict[str, List[Component]] = {}
155
+ self._value_index: Dict[str, List[Component]] = {}
156
+
157
+ # Initialize base collection
158
+ wrapped_components = []
159
+ if components:
160
+ wrapped_components = [Component(comp_data, self) for comp_data in components]
161
+
162
+ super().__init__(wrapped_components)
163
+
164
+ # Abstract method implementations
165
+ def _get_item_uuid(self, item: Component) -> str:
166
+ """Extract UUID from component."""
167
+ return item.uuid
168
+
169
+ def _create_item(self, **kwargs) -> Component:
170
+ """Create a new component with given parameters."""
171
+ # This will be called by add() methods in subclasses
172
+ raise NotImplementedError("Use add() method instead")
173
+
174
+ def _build_additional_indexes(self) -> None:
175
+ """Build component-specific indexes."""
176
+ # Clear existing indexes
177
+ self._reference_index.clear()
178
+ self._lib_id_index.clear()
179
+ self._value_index.clear()
180
+
181
+ # Rebuild indexes from current items
182
+ for component in self._items:
183
+ # Reference index
184
+ self._reference_index[component.reference] = component
185
+
186
+ # Lib_id index
187
+ if component.lib_id not in self._lib_id_index:
188
+ self._lib_id_index[component.lib_id] = []
189
+ self._lib_id_index[component.lib_id].append(component)
190
+
191
+ # Value index
192
+ if component.value not in self._value_index:
193
+ self._value_index[component.value] = []
194
+ self._value_index[component.value].append(component)
195
+
196
+ # Component-specific methods
197
+ def add(
198
+ self,
199
+ lib_id: str,
200
+ reference: Optional[str] = None,
201
+ value: str = "",
202
+ position: Optional[Union[Point, Tuple[float, float]]] = None,
203
+ footprint: Optional[str] = None,
204
+ unit: int = 1,
205
+ component_uuid: Optional[str] = None,
206
+ **properties,
207
+ ) -> Component:
208
+ """
209
+ Add a new component to the schematic.
210
+
211
+ Args:
212
+ lib_id: Library identifier (e.g., "Device:R")
213
+ reference: Component reference (auto-generated if None)
214
+ value: Component value
215
+ position: Component position (auto-placed if None)
216
+ footprint: Component footprint
217
+ unit: Unit number for multi-unit components (1-based)
218
+ component_uuid: Specific UUID for component (auto-generated if None)
219
+ **properties: Additional component properties
220
+
221
+ Returns:
222
+ Newly created Component
223
+
224
+ Raises:
225
+ ValidationError: If component data is invalid
226
+ """
227
+ # Validate lib_id
228
+ validator = SchematicValidator()
229
+ if not validator.validate_lib_id(lib_id):
230
+ raise ValidationError(f"Invalid lib_id format: {lib_id}")
231
+
232
+ # Generate reference if not provided
233
+ if not reference:
234
+ reference = self._generate_reference(lib_id)
235
+
236
+ # Validate reference
237
+ if not validator.validate_reference(reference):
238
+ raise ValidationError(f"Invalid reference format: {reference}")
239
+
240
+ # Check for duplicate reference
241
+ self._ensure_indexes_current()
242
+ if reference in self._reference_index:
243
+ raise ValidationError(f"Reference {reference} already exists")
244
+
245
+ # Set default position if not provided
246
+ if position is None:
247
+ position = self._find_available_position()
248
+ elif isinstance(position, tuple):
249
+ position = Point(position[0], position[1])
250
+
251
+ # Always snap component position to KiCAD grid (1.27mm = 50mil)
252
+ from ..core.geometry import snap_to_grid
253
+
254
+ snapped_pos = snap_to_grid((position.x, position.y), grid_size=1.27)
255
+ position = Point(snapped_pos[0], snapped_pos[1])
256
+
257
+ # Generate UUID if not provided
258
+ if component_uuid is None:
259
+ component_uuid = str(uuid.uuid4())
260
+
261
+ # Create SchematicSymbol data
262
+ symbol_data = SchematicSymbol(
263
+ uuid=component_uuid,
264
+ lib_id=lib_id,
265
+ reference=reference,
266
+ value=value,
267
+ position=position,
268
+ rotation=0.0,
269
+ unit=unit,
270
+ in_bom=True,
271
+ on_board=True,
272
+ footprint=footprint,
273
+ properties=properties.copy(),
274
+ )
275
+
276
+ # Create component wrapper
277
+ component = Component(symbol_data, self)
278
+
279
+ # Add to collection using base class method
280
+ return super().add(component)
281
+
282
+ def get_by_reference(self, reference: str) -> Optional[Component]:
283
+ """
284
+ Get component by reference.
285
+
286
+ Args:
287
+ reference: Component reference to find
288
+
289
+ Returns:
290
+ Component if found, None otherwise
291
+ """
292
+ self._ensure_indexes_current()
293
+ return self._reference_index.get(reference)
294
+
295
+ def get_by_lib_id(self, lib_id: str) -> List[Component]:
296
+ """
297
+ Get all components with a specific lib_id.
298
+
299
+ Args:
300
+ lib_id: Library ID to search for
301
+
302
+ Returns:
303
+ List of matching components
304
+ """
305
+ self._ensure_indexes_current()
306
+ return self._lib_id_index.get(lib_id, []).copy()
307
+
308
+ def get_by_value(self, value: str) -> List[Component]:
309
+ """
310
+ Get all components with a specific value.
311
+
312
+ Args:
313
+ value: Component value to search for
314
+
315
+ Returns:
316
+ List of matching components
317
+ """
318
+ self._ensure_indexes_current()
319
+ return self._value_index.get(value, []).copy()
320
+
321
+ def _generate_reference(self, lib_id: str) -> str:
322
+ """
323
+ Generate a unique reference for a component.
324
+
325
+ Args:
326
+ lib_id: Library ID to generate reference for
327
+
328
+ Returns:
329
+ Unique reference string
330
+ """
331
+ # Extract base reference from lib_id
332
+ if ":" in lib_id:
333
+ base_ref = lib_id.split(":")[-1]
334
+ else:
335
+ base_ref = lib_id
336
+
337
+ # Map common component types to standard prefixes
338
+ ref_prefixes = {
339
+ "R": "R",
340
+ "Resistor": "R",
341
+ "C": "C",
342
+ "Capacitor": "C",
343
+ "L": "L",
344
+ "Inductor": "L",
345
+ "D": "D",
346
+ "Diode": "D",
347
+ "Q": "Q",
348
+ "Transistor": "Q",
349
+ "U": "U",
350
+ "IC": "U",
351
+ "Amplifier": "U",
352
+ "J": "J",
353
+ "Connector": "J",
354
+ "SW": "SW",
355
+ "Switch": "SW",
356
+ "F": "F",
357
+ "Fuse": "F",
358
+ "TP": "TP",
359
+ "TestPoint": "TP",
360
+ }
361
+
362
+ prefix = ref_prefixes.get(base_ref, "U")
363
+
364
+ # Ensure indexes are current before checking
365
+ self._ensure_indexes_current()
366
+
367
+ # Find next available number
368
+ counter = 1
369
+ while f"{prefix}{counter}" in self._reference_index:
370
+ counter += 1
371
+
372
+ return f"{prefix}{counter}"
373
+
374
+ def _find_available_position(self) -> Point:
375
+ """
376
+ Find an available position for a new component.
377
+
378
+ Returns:
379
+ Available position point
380
+ """
381
+ # Start at a reasonable position and check for conflicts
382
+ base_x, base_y = 100.0, 100.0
383
+ spacing = 25.4 # 1 inch spacing
384
+
385
+ # Check existing positions to avoid overlap
386
+ used_positions = {(comp.position.x, comp.position.y) for comp in self._items}
387
+
388
+ # Find first available position in a grid pattern
389
+ for row in range(10): # Check up to 10 rows
390
+ for col in range(10): # Check up to 10 columns
391
+ x = base_x + col * spacing
392
+ y = base_y + row * spacing
393
+ if (x, y) not in used_positions:
394
+ return Point(x, y)
395
+
396
+ # Fallback to random position if grid is full
397
+ return Point(base_x + len(self._items) * spacing, base_y)
398
+
399
+ def _update_reference_index(self, old_reference: str, new_reference: str) -> None:
400
+ """
401
+ Update reference index when component reference changes.
402
+
403
+ Args:
404
+ old_reference: Previous reference
405
+ new_reference: New reference
406
+ """
407
+ if old_reference in self._reference_index:
408
+ component = self._reference_index.pop(old_reference)
409
+ self._reference_index[new_reference] = component
410
+
411
+ # Bulk operations for performance
412
+ def bulk_update(self, criteria: Dict[str, Any], updates: Dict[str, Any]) -> int:
413
+ """
414
+ Perform bulk update on components matching criteria.
415
+
416
+ Args:
417
+ criteria: Criteria for selecting components
418
+ updates: Updates to apply
419
+
420
+ Returns:
421
+ Number of components updated
422
+ """
423
+ matching_components = self.filter(**criteria)
424
+
425
+ for component in matching_components:
426
+ for attr, value in updates.items():
427
+ if hasattr(component, attr):
428
+ setattr(component, attr, value)
429
+
430
+ self._mark_modified()
431
+ self._mark_indexes_dirty()
432
+
433
+ logger.info(f"Bulk updated {len(matching_components)} components")
434
+ return len(matching_components)