kicad-sch-api 0.3.2__py3-none-any.whl → 0.3.5__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 (39) hide show
  1. kicad_sch_api/__init__.py +2 -2
  2. kicad_sch_api/collections/__init__.py +21 -0
  3. kicad_sch_api/collections/base.py +296 -0
  4. kicad_sch_api/collections/components.py +422 -0
  5. kicad_sch_api/collections/junctions.py +378 -0
  6. kicad_sch_api/collections/labels.py +412 -0
  7. kicad_sch_api/collections/wires.py +407 -0
  8. kicad_sch_api/core/formatter.py +31 -0
  9. kicad_sch_api/core/labels.py +348 -0
  10. kicad_sch_api/core/nets.py +310 -0
  11. kicad_sch_api/core/no_connects.py +274 -0
  12. kicad_sch_api/core/parser.py +72 -0
  13. kicad_sch_api/core/schematic.py +185 -9
  14. kicad_sch_api/core/texts.py +343 -0
  15. kicad_sch_api/core/types.py +26 -0
  16. kicad_sch_api/geometry/__init__.py +26 -0
  17. kicad_sch_api/geometry/font_metrics.py +20 -0
  18. kicad_sch_api/geometry/symbol_bbox.py +589 -0
  19. kicad_sch_api/interfaces/__init__.py +17 -0
  20. kicad_sch_api/interfaces/parser.py +76 -0
  21. kicad_sch_api/interfaces/repository.py +70 -0
  22. kicad_sch_api/interfaces/resolver.py +117 -0
  23. kicad_sch_api/parsers/__init__.py +14 -0
  24. kicad_sch_api/parsers/base.py +148 -0
  25. kicad_sch_api/parsers/label_parser.py +254 -0
  26. kicad_sch_api/parsers/registry.py +153 -0
  27. kicad_sch_api/parsers/symbol_parser.py +227 -0
  28. kicad_sch_api/parsers/wire_parser.py +99 -0
  29. kicad_sch_api/symbols/__init__.py +18 -0
  30. kicad_sch_api/symbols/cache.py +470 -0
  31. kicad_sch_api/symbols/resolver.py +367 -0
  32. kicad_sch_api/symbols/validators.py +453 -0
  33. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/METADATA +1 -1
  34. kicad_sch_api-0.3.5.dist-info/RECORD +58 -0
  35. kicad_sch_api-0.3.2.dist-info/RECORD +0 -31
  36. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/WHEEL +0 -0
  37. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/entry_points.txt +0 -0
  38. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/licenses/LICENSE +0 -0
  39. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,422 @@
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
+ snapped_pos = snap_to_grid((position.x, position.y), grid_size=1.27)
254
+ position = Point(snapped_pos[0], snapped_pos[1])
255
+
256
+ # Generate UUID if not provided
257
+ if component_uuid is None:
258
+ component_uuid = str(uuid.uuid4())
259
+
260
+ # Create SchematicSymbol data
261
+ symbol_data = SchematicSymbol(
262
+ uuid=component_uuid,
263
+ lib_id=lib_id,
264
+ reference=reference,
265
+ value=value,
266
+ position=position,
267
+ rotation=0.0,
268
+ unit=unit,
269
+ in_bom=True,
270
+ on_board=True,
271
+ footprint=footprint,
272
+ properties=properties.copy()
273
+ )
274
+
275
+ # Create component wrapper
276
+ component = Component(symbol_data, self)
277
+
278
+ # Add to collection using base class method
279
+ return super().add(component)
280
+
281
+ def get_by_reference(self, reference: str) -> Optional[Component]:
282
+ """
283
+ Get component by reference.
284
+
285
+ Args:
286
+ reference: Component reference to find
287
+
288
+ Returns:
289
+ Component if found, None otherwise
290
+ """
291
+ self._ensure_indexes_current()
292
+ return self._reference_index.get(reference)
293
+
294
+ def get_by_lib_id(self, lib_id: str) -> List[Component]:
295
+ """
296
+ Get all components with a specific lib_id.
297
+
298
+ Args:
299
+ lib_id: Library ID to search for
300
+
301
+ Returns:
302
+ List of matching components
303
+ """
304
+ self._ensure_indexes_current()
305
+ return self._lib_id_index.get(lib_id, []).copy()
306
+
307
+ def get_by_value(self, value: str) -> List[Component]:
308
+ """
309
+ Get all components with a specific value.
310
+
311
+ Args:
312
+ value: Component value to search for
313
+
314
+ Returns:
315
+ List of matching components
316
+ """
317
+ self._ensure_indexes_current()
318
+ return self._value_index.get(value, []).copy()
319
+
320
+ def _generate_reference(self, lib_id: str) -> str:
321
+ """
322
+ Generate a unique reference for a component.
323
+
324
+ Args:
325
+ lib_id: Library ID to generate reference for
326
+
327
+ Returns:
328
+ Unique reference string
329
+ """
330
+ # Extract base reference from lib_id
331
+ if ":" in lib_id:
332
+ base_ref = lib_id.split(":")[-1]
333
+ else:
334
+ base_ref = lib_id
335
+
336
+ # Map common component types to standard prefixes
337
+ ref_prefixes = {
338
+ "R": "R", "Resistor": "R",
339
+ "C": "C", "Capacitor": "C",
340
+ "L": "L", "Inductor": "L",
341
+ "D": "D", "Diode": "D",
342
+ "Q": "Q", "Transistor": "Q",
343
+ "U": "U", "IC": "U", "Amplifier": "U",
344
+ "J": "J", "Connector": "J",
345
+ "SW": "SW", "Switch": "SW",
346
+ "F": "F", "Fuse": "F",
347
+ "TP": "TP", "TestPoint": "TP",
348
+ }
349
+
350
+ prefix = ref_prefixes.get(base_ref, "U")
351
+
352
+ # Ensure indexes are current before checking
353
+ self._ensure_indexes_current()
354
+
355
+ # Find next available number
356
+ counter = 1
357
+ while f"{prefix}{counter}" in self._reference_index:
358
+ counter += 1
359
+
360
+ return f"{prefix}{counter}"
361
+
362
+ def _find_available_position(self) -> Point:
363
+ """
364
+ Find an available position for a new component.
365
+
366
+ Returns:
367
+ Available position point
368
+ """
369
+ # Start at a reasonable position and check for conflicts
370
+ base_x, base_y = 100.0, 100.0
371
+ spacing = 25.4 # 1 inch spacing
372
+
373
+ # Check existing positions to avoid overlap
374
+ used_positions = {(comp.position.x, comp.position.y) for comp in self._items}
375
+
376
+ # Find first available position in a grid pattern
377
+ for row in range(10): # Check up to 10 rows
378
+ for col in range(10): # Check up to 10 columns
379
+ x = base_x + col * spacing
380
+ y = base_y + row * spacing
381
+ if (x, y) not in used_positions:
382
+ return Point(x, y)
383
+
384
+ # Fallback to random position if grid is full
385
+ return Point(base_x + len(self._items) * spacing, base_y)
386
+
387
+ def _update_reference_index(self, old_reference: str, new_reference: str) -> None:
388
+ """
389
+ Update reference index when component reference changes.
390
+
391
+ Args:
392
+ old_reference: Previous reference
393
+ new_reference: New reference
394
+ """
395
+ if old_reference in self._reference_index:
396
+ component = self._reference_index.pop(old_reference)
397
+ self._reference_index[new_reference] = component
398
+
399
+ # Bulk operations for performance
400
+ def bulk_update(self, criteria: Dict[str, Any], updates: Dict[str, Any]) -> int:
401
+ """
402
+ Perform bulk update on components matching criteria.
403
+
404
+ Args:
405
+ criteria: Criteria for selecting components
406
+ updates: Updates to apply
407
+
408
+ Returns:
409
+ Number of components updated
410
+ """
411
+ matching_components = self.filter(**criteria)
412
+
413
+ for component in matching_components:
414
+ for attr, value in updates.items():
415
+ if hasattr(component, attr):
416
+ setattr(component, attr, value)
417
+
418
+ self._mark_modified()
419
+ self._mark_indexes_dirty()
420
+
421
+ logger.info(f"Bulk updated {len(matching_components)} components")
422
+ return len(matching_components)