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,404 @@
1
+ """
2
+ Label collection with specialized indexing and label-specific operations.
3
+
4
+ Extends the base IndexedCollection to provide label-specific features like
5
+ text indexing, position-based queries, and label type classification.
6
+ """
7
+
8
+ import logging
9
+ import uuid as uuid_module
10
+ from typing import Any, Dict, List, Optional, Tuple, Union
11
+
12
+ from ..core.types import Point
13
+ from .base import IndexedCollection
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class Label:
19
+ """Label data structure."""
20
+
21
+ def __init__(
22
+ self,
23
+ uuid: str,
24
+ text: str,
25
+ position: Point,
26
+ rotation: float = 0.0,
27
+ label_type: str = "label",
28
+ effects: Optional[Dict[str, Any]] = None,
29
+ ):
30
+ self.uuid = uuid
31
+ self.text = text
32
+ self.position = position
33
+ self.rotation = rotation
34
+ self.label_type = label_type # "label", "global_label", "hierarchical_label"
35
+ self.effects = effects or {}
36
+
37
+ def __repr__(self) -> str:
38
+ return f"Label(text='{self.text}', pos={self.position}, type='{self.label_type}')"
39
+
40
+
41
+ class LabelCollection(IndexedCollection[Label]):
42
+ """
43
+ Collection class for efficient label management.
44
+
45
+ Extends IndexedCollection with label-specific features:
46
+ - Text indexing for finding labels by text content
47
+ - Position indexing for spatial queries
48
+ - Type classification (local, global, hierarchical)
49
+ - Net name management
50
+ - Connectivity analysis support
51
+ """
52
+
53
+ def __init__(self, labels: Optional[List[Label]] = None):
54
+ """
55
+ Initialize label collection.
56
+
57
+ Args:
58
+ labels: Initial list of labels
59
+ """
60
+ self._text_index: Dict[str, List[Label]] = {}
61
+ self._position_index: Dict[Tuple[float, float], List[Label]] = {}
62
+ self._type_index: Dict[str, List[Label]] = {}
63
+
64
+ super().__init__(labels)
65
+
66
+ # Abstract method implementations
67
+ def _get_item_uuid(self, item: Label) -> str:
68
+ """Extract UUID from label."""
69
+ return item.uuid
70
+
71
+ def _create_item(self, **kwargs) -> Label:
72
+ """Create a new label with given parameters."""
73
+ # This will be called by add() methods
74
+ raise NotImplementedError("Use add() method instead")
75
+
76
+ def _build_additional_indexes(self) -> None:
77
+ """Build label-specific indexes."""
78
+ # Clear existing indexes
79
+ self._text_index.clear()
80
+ self._position_index.clear()
81
+ self._type_index.clear()
82
+
83
+ # Rebuild indexes from current items
84
+ for label in self._items:
85
+ # Text index
86
+ text_key = label.text.lower() # Case-insensitive
87
+ if text_key not in self._text_index:
88
+ self._text_index[text_key] = []
89
+ self._text_index[text_key].append(label)
90
+
91
+ # Position index
92
+ pos_key = (label.position.x, label.position.y)
93
+ if pos_key not in self._position_index:
94
+ self._position_index[pos_key] = []
95
+ self._position_index[pos_key].append(label)
96
+
97
+ # Type index
98
+ if label.label_type not in self._type_index:
99
+ self._type_index[label.label_type] = []
100
+ self._type_index[label.label_type].append(label)
101
+
102
+ # Label-specific methods
103
+ def add(
104
+ self,
105
+ text: str,
106
+ position: Union[Point, Tuple[float, float]],
107
+ label_type: str = "label",
108
+ rotation: float = 0.0,
109
+ effects: Optional[Dict[str, Any]] = None,
110
+ label_uuid: Optional[str] = None,
111
+ ) -> Label:
112
+ """
113
+ Add a new label to the collection.
114
+
115
+ Args:
116
+ text: Label text content
117
+ position: Label position
118
+ label_type: Type of label ("label", "global_label", "hierarchical_label")
119
+ rotation: Label rotation in degrees
120
+ effects: Label effects (font, size, etc.)
121
+ label_uuid: Specific UUID for label (auto-generated if None)
122
+
123
+ Returns:
124
+ Newly created Label
125
+
126
+ Raises:
127
+ ValueError: If invalid parameters provided
128
+ """
129
+ # Validate text
130
+ if not text.strip():
131
+ raise ValueError("Label text cannot be empty")
132
+
133
+ # Convert tuple to Point if needed
134
+ if isinstance(position, tuple):
135
+ position = Point(position[0], position[1])
136
+
137
+ # Validate label type
138
+ valid_types = ["label", "global_label", "hierarchical_label"]
139
+ if label_type not in valid_types:
140
+ raise ValueError(f"Invalid label type: {label_type}. Must be one of {valid_types}")
141
+
142
+ # Generate UUID if not provided
143
+ if label_uuid is None:
144
+ label_uuid = str(uuid_module.uuid4())
145
+
146
+ # Create label
147
+ label = Label(
148
+ uuid=label_uuid,
149
+ text=text,
150
+ position=position,
151
+ rotation=rotation,
152
+ label_type=label_type,
153
+ effects=effects or {},
154
+ )
155
+
156
+ # Add to collection using base class method
157
+ return super().add(label)
158
+
159
+ def get_labels_by_text(self, text: str, case_sensitive: bool = False) -> List[Label]:
160
+ """
161
+ Get all labels with specific text.
162
+
163
+ Args:
164
+ text: Text to search for
165
+ case_sensitive: Whether search should be case sensitive
166
+
167
+ Returns:
168
+ List of matching labels
169
+ """
170
+ self._ensure_indexes_current()
171
+
172
+ if case_sensitive:
173
+ return [label for label in self._items if label.text == text]
174
+ else:
175
+ text_key = text.lower()
176
+ return self._text_index.get(text_key, []).copy()
177
+
178
+ def get_labels_at_position(
179
+ self, position: Union[Point, Tuple[float, float]], tolerance: float = 0.0
180
+ ) -> List[Label]:
181
+ """
182
+ Get all labels at a specific position.
183
+
184
+ Args:
185
+ position: Position to search at
186
+ tolerance: Position tolerance for matching
187
+
188
+ Returns:
189
+ List of labels at the position
190
+ """
191
+ self._ensure_indexes_current()
192
+
193
+ if isinstance(position, Point):
194
+ pos_key = (position.x, position.y)
195
+ else:
196
+ pos_key = position
197
+
198
+ if tolerance == 0.0:
199
+ # Exact match
200
+ return self._position_index.get(pos_key, []).copy()
201
+ else:
202
+ # Tolerance-based search
203
+ matching_labels = []
204
+ target_x, target_y = pos_key
205
+
206
+ for label in self._items:
207
+ dx = abs(label.position.x - target_x)
208
+ dy = abs(label.position.y - target_y)
209
+ distance = (dx**2 + dy**2) ** 0.5
210
+
211
+ if distance <= tolerance:
212
+ matching_labels.append(label)
213
+
214
+ return matching_labels
215
+
216
+ def get_labels_by_type(self, label_type: str) -> List[Label]:
217
+ """
218
+ Get all labels of a specific type.
219
+
220
+ Args:
221
+ label_type: Type of labels to find
222
+
223
+ Returns:
224
+ List of labels of the specified type
225
+ """
226
+ self._ensure_indexes_current()
227
+ return self._type_index.get(label_type, []).copy()
228
+
229
+ def get_net_names(self) -> List[str]:
230
+ """
231
+ Get all unique net names from labels.
232
+
233
+ Returns:
234
+ List of unique net names
235
+ """
236
+ return list(set(label.text for label in self._items))
237
+
238
+ def get_labels_for_net(self, net_name: str, case_sensitive: bool = False) -> List[Label]:
239
+ """
240
+ Get all labels for a specific net.
241
+
242
+ Args:
243
+ net_name: Net name to search for
244
+ case_sensitive: Whether search should be case sensitive
245
+
246
+ Returns:
247
+ List of labels for the net
248
+ """
249
+ return self.get_labels_by_text(net_name, case_sensitive)
250
+
251
+ def find_labels_in_region(
252
+ self, min_x: float, min_y: float, max_x: float, max_y: float
253
+ ) -> List[Label]:
254
+ """
255
+ Find all labels within a rectangular region.
256
+
257
+ Args:
258
+ min_x: Minimum X coordinate
259
+ min_y: Minimum Y coordinate
260
+ max_x: Maximum X coordinate
261
+ max_y: Maximum Y coordinate
262
+
263
+ Returns:
264
+ List of labels in the region
265
+ """
266
+ matching_labels = []
267
+
268
+ for label in self._items:
269
+ if min_x <= label.position.x <= max_x and min_y <= label.position.y <= max_y:
270
+ matching_labels.append(label)
271
+
272
+ return matching_labels
273
+
274
+ def update_label_text(self, label_uuid: str, new_text: str) -> bool:
275
+ """
276
+ Update the text of an existing label.
277
+
278
+ Args:
279
+ label_uuid: UUID of label to update
280
+ new_text: New text content
281
+
282
+ Returns:
283
+ True if label was updated, False if not found
284
+
285
+ Raises:
286
+ ValueError: If new text is empty
287
+ """
288
+ if not new_text.strip():
289
+ raise ValueError("Label text cannot be empty")
290
+
291
+ label = self.get(label_uuid)
292
+ if not label:
293
+ return False
294
+
295
+ # Update text
296
+ label.text = new_text
297
+ self._mark_modified()
298
+ self._mark_indexes_dirty()
299
+
300
+ logger.debug(f"Updated label {label_uuid} text to '{new_text}'")
301
+ return True
302
+
303
+ def update_label_position(
304
+ self, label_uuid: str, new_position: Union[Point, Tuple[float, float]]
305
+ ) -> bool:
306
+ """
307
+ Update the position of an existing label.
308
+
309
+ Args:
310
+ label_uuid: UUID of label to update
311
+ new_position: New position
312
+
313
+ Returns:
314
+ True if label was updated, False if not found
315
+ """
316
+ label = self.get(label_uuid)
317
+ if not label:
318
+ return False
319
+
320
+ # Convert tuple to Point if needed
321
+ if isinstance(new_position, tuple):
322
+ new_position = Point(new_position[0], new_position[1])
323
+
324
+ # Update position
325
+ label.position = new_position
326
+ self._mark_modified()
327
+ self._mark_indexes_dirty()
328
+
329
+ logger.debug(f"Updated label {label_uuid} position to {new_position}")
330
+ return True
331
+
332
+ # Bulk operations
333
+ def rename_net(self, old_name: str, new_name: str, case_sensitive: bool = False) -> int:
334
+ """
335
+ Rename all labels for a net.
336
+
337
+ Args:
338
+ old_name: Current net name
339
+ new_name: New net name
340
+ case_sensitive: Whether search should be case sensitive
341
+
342
+ Returns:
343
+ Number of labels renamed
344
+
345
+ Raises:
346
+ ValueError: If new name is empty
347
+ """
348
+ if not new_name.strip():
349
+ raise ValueError("New net name cannot be empty")
350
+
351
+ labels_to_rename = self.get_labels_by_text(old_name, case_sensitive)
352
+
353
+ for label in labels_to_rename:
354
+ label.text = new_name
355
+
356
+ if labels_to_rename:
357
+ self._mark_modified()
358
+ self._mark_indexes_dirty()
359
+
360
+ logger.info(f"Renamed {len(labels_to_rename)} labels from '{old_name}' to '{new_name}'")
361
+ return len(labels_to_rename)
362
+
363
+ def remove_labels_for_net(self, net_name: str, case_sensitive: bool = False) -> int:
364
+ """
365
+ Remove all labels for a specific net.
366
+
367
+ Args:
368
+ net_name: Net name to remove labels for
369
+ case_sensitive: Whether search should be case sensitive
370
+
371
+ Returns:
372
+ Number of labels removed
373
+ """
374
+ labels_to_remove = self.get_labels_by_text(net_name, case_sensitive)
375
+
376
+ for label in labels_to_remove:
377
+ self.remove(label.uuid)
378
+
379
+ logger.info(f"Removed {len(labels_to_remove)} labels for net '{net_name}'")
380
+ return len(labels_to_remove)
381
+
382
+ # Collection statistics
383
+ def get_label_statistics(self) -> Dict[str, Any]:
384
+ """
385
+ Get label statistics for the collection.
386
+
387
+ Returns:
388
+ Dictionary with label statistics
389
+ """
390
+ stats = super().get_statistics()
391
+
392
+ # Add label-specific statistics
393
+ stats.update(
394
+ {
395
+ "unique_texts": len(self._text_index),
396
+ "unique_positions": len(self._position_index),
397
+ "label_types": {
398
+ label_type: len(labels) for label_type, labels in self._type_index.items()
399
+ },
400
+ "net_count": len(self.get_net_names()),
401
+ }
402
+ )
403
+
404
+ return stats