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,412 @@
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,
180
+ position: Union[Point, Tuple[float, float]],
181
+ tolerance: float = 0.0
182
+ ) -> List[Label]:
183
+ """
184
+ Get all labels at a specific position.
185
+
186
+ Args:
187
+ position: Position to search at
188
+ tolerance: Position tolerance for matching
189
+
190
+ Returns:
191
+ List of labels at the position
192
+ """
193
+ self._ensure_indexes_current()
194
+
195
+ if isinstance(position, Point):
196
+ pos_key = (position.x, position.y)
197
+ else:
198
+ pos_key = position
199
+
200
+ if tolerance == 0.0:
201
+ # Exact match
202
+ return self._position_index.get(pos_key, []).copy()
203
+ else:
204
+ # Tolerance-based search
205
+ matching_labels = []
206
+ target_x, target_y = pos_key
207
+
208
+ for label in self._items:
209
+ dx = abs(label.position.x - target_x)
210
+ dy = abs(label.position.y - target_y)
211
+ distance = (dx ** 2 + dy ** 2) ** 0.5
212
+
213
+ if distance <= tolerance:
214
+ matching_labels.append(label)
215
+
216
+ return matching_labels
217
+
218
+ def get_labels_by_type(self, label_type: str) -> List[Label]:
219
+ """
220
+ Get all labels of a specific type.
221
+
222
+ Args:
223
+ label_type: Type of labels to find
224
+
225
+ Returns:
226
+ List of labels of the specified type
227
+ """
228
+ self._ensure_indexes_current()
229
+ return self._type_index.get(label_type, []).copy()
230
+
231
+ def get_net_names(self) -> List[str]:
232
+ """
233
+ Get all unique net names from labels.
234
+
235
+ Returns:
236
+ List of unique net names
237
+ """
238
+ return list(set(label.text for label in self._items))
239
+
240
+ def get_labels_for_net(self, net_name: str, case_sensitive: bool = False) -> List[Label]:
241
+ """
242
+ Get all labels for a specific net.
243
+
244
+ Args:
245
+ net_name: Net name to search for
246
+ case_sensitive: Whether search should be case sensitive
247
+
248
+ Returns:
249
+ List of labels for the net
250
+ """
251
+ return self.get_labels_by_text(net_name, case_sensitive)
252
+
253
+ def find_labels_in_region(
254
+ self,
255
+ min_x: float,
256
+ min_y: float,
257
+ max_x: float,
258
+ max_y: float
259
+ ) -> List[Label]:
260
+ """
261
+ Find all labels within a rectangular region.
262
+
263
+ Args:
264
+ min_x: Minimum X coordinate
265
+ min_y: Minimum Y coordinate
266
+ max_x: Maximum X coordinate
267
+ max_y: Maximum Y coordinate
268
+
269
+ Returns:
270
+ List of labels in the region
271
+ """
272
+ matching_labels = []
273
+
274
+ for label in self._items:
275
+ if (min_x <= label.position.x <= max_x and
276
+ min_y <= label.position.y <= max_y):
277
+ matching_labels.append(label)
278
+
279
+ return matching_labels
280
+
281
+ def update_label_text(self, label_uuid: str, new_text: str) -> bool:
282
+ """
283
+ Update the text of an existing label.
284
+
285
+ Args:
286
+ label_uuid: UUID of label to update
287
+ new_text: New text content
288
+
289
+ Returns:
290
+ True if label was updated, False if not found
291
+
292
+ Raises:
293
+ ValueError: If new text is empty
294
+ """
295
+ if not new_text.strip():
296
+ raise ValueError("Label text cannot be empty")
297
+
298
+ label = self.get(label_uuid)
299
+ if not label:
300
+ return False
301
+
302
+ # Update text
303
+ label.text = new_text
304
+ self._mark_modified()
305
+ self._mark_indexes_dirty()
306
+
307
+ logger.debug(f"Updated label {label_uuid} text to '{new_text}'")
308
+ return True
309
+
310
+ def update_label_position(
311
+ self,
312
+ label_uuid: str,
313
+ new_position: Union[Point, Tuple[float, float]]
314
+ ) -> bool:
315
+ """
316
+ Update the position of an existing label.
317
+
318
+ Args:
319
+ label_uuid: UUID of label to update
320
+ new_position: New position
321
+
322
+ Returns:
323
+ True if label was updated, False if not found
324
+ """
325
+ label = self.get(label_uuid)
326
+ if not label:
327
+ return False
328
+
329
+ # Convert tuple to Point if needed
330
+ if isinstance(new_position, tuple):
331
+ new_position = Point(new_position[0], new_position[1])
332
+
333
+ # Update position
334
+ label.position = new_position
335
+ self._mark_modified()
336
+ self._mark_indexes_dirty()
337
+
338
+ logger.debug(f"Updated label {label_uuid} position to {new_position}")
339
+ return True
340
+
341
+ # Bulk operations
342
+ def rename_net(self, old_name: str, new_name: str, case_sensitive: bool = False) -> int:
343
+ """
344
+ Rename all labels for a net.
345
+
346
+ Args:
347
+ old_name: Current net name
348
+ new_name: New net name
349
+ case_sensitive: Whether search should be case sensitive
350
+
351
+ Returns:
352
+ Number of labels renamed
353
+
354
+ Raises:
355
+ ValueError: If new name is empty
356
+ """
357
+ if not new_name.strip():
358
+ raise ValueError("New net name cannot be empty")
359
+
360
+ labels_to_rename = self.get_labels_by_text(old_name, case_sensitive)
361
+
362
+ for label in labels_to_rename:
363
+ label.text = new_name
364
+
365
+ if labels_to_rename:
366
+ self._mark_modified()
367
+ self._mark_indexes_dirty()
368
+
369
+ logger.info(f"Renamed {len(labels_to_rename)} labels from '{old_name}' to '{new_name}'")
370
+ return len(labels_to_rename)
371
+
372
+ def remove_labels_for_net(self, net_name: str, case_sensitive: bool = False) -> int:
373
+ """
374
+ Remove all labels for a specific net.
375
+
376
+ Args:
377
+ net_name: Net name to remove labels for
378
+ case_sensitive: Whether search should be case sensitive
379
+
380
+ Returns:
381
+ Number of labels removed
382
+ """
383
+ labels_to_remove = self.get_labels_by_text(net_name, case_sensitive)
384
+
385
+ for label in labels_to_remove:
386
+ self.remove(label.uuid)
387
+
388
+ logger.info(f"Removed {len(labels_to_remove)} labels for net '{net_name}'")
389
+ return len(labels_to_remove)
390
+
391
+ # Collection statistics
392
+ def get_label_statistics(self) -> Dict[str, Any]:
393
+ """
394
+ Get label statistics for the collection.
395
+
396
+ Returns:
397
+ Dictionary with label statistics
398
+ """
399
+ stats = super().get_statistics()
400
+
401
+ # Add label-specific statistics
402
+ stats.update({
403
+ "unique_texts": len(self._text_index),
404
+ "unique_positions": len(self._position_index),
405
+ "label_types": {
406
+ label_type: len(labels)
407
+ for label_type, labels in self._type_index.items()
408
+ },
409
+ "net_count": len(self.get_net_names())
410
+ })
411
+
412
+ return stats