kicad-sch-api 0.4.0__py3-none-any.whl → 0.4.2__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 (57) hide show
  1. kicad_sch_api/__init__.py +2 -2
  2. kicad_sch_api/cli/__init__.py +45 -0
  3. kicad_sch_api/cli/base.py +302 -0
  4. kicad_sch_api/cli/bom.py +164 -0
  5. kicad_sch_api/cli/erc.py +229 -0
  6. kicad_sch_api/cli/export_docs.py +289 -0
  7. kicad_sch_api/cli/netlist.py +94 -0
  8. kicad_sch_api/cli/types.py +43 -0
  9. kicad_sch_api/core/collections/__init__.py +5 -0
  10. kicad_sch_api/core/collections/base.py +248 -0
  11. kicad_sch_api/core/component_bounds.py +5 -0
  12. kicad_sch_api/core/components.py +142 -47
  13. kicad_sch_api/core/config.py +85 -3
  14. kicad_sch_api/core/factories/__init__.py +5 -0
  15. kicad_sch_api/core/factories/element_factory.py +276 -0
  16. kicad_sch_api/core/formatter.py +22 -5
  17. kicad_sch_api/core/junctions.py +26 -75
  18. kicad_sch_api/core/labels.py +28 -52
  19. kicad_sch_api/core/managers/file_io.py +3 -2
  20. kicad_sch_api/core/managers/metadata.py +6 -5
  21. kicad_sch_api/core/managers/validation.py +3 -2
  22. kicad_sch_api/core/managers/wire.py +7 -1
  23. kicad_sch_api/core/nets.py +38 -43
  24. kicad_sch_api/core/no_connects.py +29 -53
  25. kicad_sch_api/core/parser.py +75 -1765
  26. kicad_sch_api/core/schematic.py +211 -148
  27. kicad_sch_api/core/texts.py +28 -55
  28. kicad_sch_api/core/types.py +59 -18
  29. kicad_sch_api/core/wires.py +27 -75
  30. kicad_sch_api/parsers/elements/__init__.py +22 -0
  31. kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
  32. kicad_sch_api/parsers/elements/label_parser.py +194 -0
  33. kicad_sch_api/parsers/elements/library_parser.py +165 -0
  34. kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
  35. kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
  36. kicad_sch_api/parsers/elements/symbol_parser.py +313 -0
  37. kicad_sch_api/parsers/elements/text_parser.py +250 -0
  38. kicad_sch_api/parsers/elements/wire_parser.py +242 -0
  39. kicad_sch_api/parsers/utils.py +80 -0
  40. kicad_sch_api/validation/__init__.py +25 -0
  41. kicad_sch_api/validation/erc.py +171 -0
  42. kicad_sch_api/validation/erc_models.py +203 -0
  43. kicad_sch_api/validation/pin_matrix.py +243 -0
  44. kicad_sch_api/validation/validators.py +391 -0
  45. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/METADATA +17 -9
  46. kicad_sch_api-0.4.2.dist-info/RECORD +87 -0
  47. kicad_sch_api/core/manhattan_routing.py +0 -430
  48. kicad_sch_api/core/simple_manhattan.py +0 -228
  49. kicad_sch_api/core/wire_routing.py +0 -380
  50. kicad_sch_api/parsers/label_parser.py +0 -254
  51. kicad_sch_api/parsers/symbol_parser.py +0 -222
  52. kicad_sch_api/parsers/wire_parser.py +0 -99
  53. kicad_sch_api-0.4.0.dist-info/RECORD +0 -67
  54. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/WHEEL +0 -0
  55. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/entry_points.txt +0 -0
  56. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/licenses/LICENSE +0 -0
  57. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/top_level.txt +0 -0
@@ -18,14 +18,14 @@ class Point:
18
18
  x: float
19
19
  y: float
20
20
 
21
- def __post_init__(self):
21
+ def __post_init__(self) -> None:
22
22
  # Ensure coordinates are float
23
23
  object.__setattr__(self, "x", float(self.x))
24
24
  object.__setattr__(self, "y", float(self.y))
25
25
 
26
26
  def distance_to(self, other: "Point") -> float:
27
27
  """Calculate distance to another point."""
28
- return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5
28
+ return float(((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5)
29
29
 
30
30
  def offset(self, dx: float, dy: float) -> "Point":
31
31
  """Create new point offset by dx, dy."""
@@ -35,6 +35,43 @@ class Point:
35
35
  return f"({self.x:.3f}, {self.y:.3f})"
36
36
 
37
37
 
38
+ def point_from_dict_or_tuple(
39
+ position: Union[Point, Dict[str, float], Tuple[float, float], List[float], Any]
40
+ ) -> Point:
41
+ """
42
+ Convert various position formats to a Point object.
43
+
44
+ Supports multiple input formats for maximum flexibility:
45
+ - Point: Returns as-is
46
+ - Dict with 'x' and 'y' keys: Extracts and creates Point
47
+ - Tuple/List with 2 elements: Creates Point from coordinates
48
+ - Other: Returns as-is (assumes it's already a Point-like object)
49
+
50
+ Args:
51
+ position: Position in any supported format
52
+
53
+ Returns:
54
+ Point object
55
+
56
+ Example:
57
+ >>> point_from_dict_or_tuple({"x": 10, "y": 20})
58
+ Point(x=10.0, y=20.0)
59
+ >>> point_from_dict_or_tuple((10, 20))
60
+ Point(x=10.0, y=20.0)
61
+ >>> point_from_dict_or_tuple(Point(10, 20))
62
+ Point(x=10.0, y=20.0)
63
+ """
64
+ if isinstance(position, Point):
65
+ return position
66
+ elif isinstance(position, dict):
67
+ return Point(position.get("x", 0), position.get("y", 0))
68
+ elif isinstance(position, (list, tuple)) and len(position) >= 2:
69
+ return Point(position[0], position[1])
70
+ else:
71
+ # Assume it's already a Point-like object or will be handled by caller
72
+ return position
73
+
74
+
38
75
  @dataclass(frozen=True)
39
76
  class Rectangle:
40
77
  """Rectangle defined by two corner points."""
@@ -110,7 +147,7 @@ class SchematicPin:
110
147
  length: float = 2.54 # Standard pin length in mm
111
148
  rotation: float = 0.0 # Rotation in degrees
112
149
 
113
- def __post_init__(self):
150
+ def __post_init__(self) -> None:
114
151
  # Ensure types are correct
115
152
  self.pin_type = PinType(self.pin_type) if isinstance(self.pin_type, str) else self.pin_type
116
153
  self.pin_shape = (
@@ -135,7 +172,7 @@ class SchematicSymbol:
135
172
  on_board: bool = True
136
173
  unit: int = 1
137
174
 
138
- def __post_init__(self):
175
+ def __post_init__(self) -> None:
139
176
  # Generate UUID if not provided
140
177
  if not self.uuid:
141
178
  self.uuid = str(uuid4())
@@ -163,6 +200,10 @@ class SchematicSymbol:
163
200
  if not pin:
164
201
  return None
165
202
  # TODO: Apply rotation and symbol position transformation
203
+ # NOTE: Currently assumes 0° rotation. For rotated components, pin positions
204
+ # would need to be transformed using rotation matrix before adding to component position.
205
+ # This affects pin-to-pin wiring accuracy for rotated components.
206
+ # Priority: MEDIUM - Would improve wiring accuracy for rotated components
166
207
  return Point(self.position.x + pin.position.x, self.position.y + pin.position.y)
167
208
 
168
209
 
@@ -183,7 +224,7 @@ class Wire:
183
224
  stroke_width: float = 0.0
184
225
  stroke_type: str = "default"
185
226
 
186
- def __post_init__(self):
227
+ def __post_init__(self) -> None:
187
228
  if not self.uuid:
188
229
  self.uuid = str(uuid4())
189
230
 
@@ -196,7 +237,7 @@ class Wire:
196
237
  raise ValueError("Wire must have at least 2 points")
197
238
 
198
239
  @classmethod
199
- def from_start_end(cls, uuid: str, start: Point, end: Point, **kwargs) -> "Wire":
240
+ def from_start_end(cls, uuid: str, start: Point, end: Point, **kwargs: Any) -> "Wire":
200
241
  """Create wire from start and end points (convenience method)."""
201
242
  return cls(uuid=uuid, points=[start, end], **kwargs)
202
243
 
@@ -244,7 +285,7 @@ class Junction:
244
285
  diameter: float = 0 # KiCAD default diameter
245
286
  color: Tuple[int, int, int, int] = (0, 0, 0, 0) # RGBA color
246
287
 
247
- def __post_init__(self):
288
+ def __post_init__(self) -> None:
248
289
  if not self.uuid:
249
290
  self.uuid = str(uuid4())
250
291
 
@@ -280,7 +321,7 @@ class Label:
280
321
  size: float = 1.27
281
322
  shape: Optional[HierarchicalLabelShape] = None # Only for hierarchical labels
282
323
 
283
- def __post_init__(self):
324
+ def __post_init__(self) -> None:
284
325
  if not self.uuid:
285
326
  self.uuid = str(uuid4())
286
327
 
@@ -305,7 +346,7 @@ class Text:
305
346
  size: float = 1.27
306
347
  exclude_from_sim: bool = False
307
348
 
308
- def __post_init__(self):
349
+ def __post_init__(self) -> None:
309
350
  if not self.uuid:
310
351
  self.uuid = str(uuid4())
311
352
 
@@ -333,7 +374,7 @@ class TextBox:
333
374
  justify_vertical: str = "top"
334
375
  exclude_from_sim: bool = False
335
376
 
336
- def __post_init__(self):
377
+ def __post_init__(self) -> None:
337
378
  if not self.uuid:
338
379
  self.uuid = str(uuid4())
339
380
 
@@ -349,7 +390,7 @@ class SchematicRectangle:
349
390
  stroke_type: str = "default"
350
391
  fill_type: str = "none"
351
392
 
352
- def __post_init__(self):
393
+ def __post_init__(self) -> None:
353
394
  if not self.uuid:
354
395
  self.uuid = str(uuid4())
355
396
 
@@ -378,7 +419,7 @@ class Image:
378
419
  data: str # Base64-encoded image data
379
420
  scale: float = 1.0
380
421
 
381
- def __post_init__(self):
422
+ def __post_init__(self) -> None:
382
423
  if not self.uuid:
383
424
  self.uuid = str(uuid4())
384
425
 
@@ -390,7 +431,7 @@ class NoConnect:
390
431
  uuid: str
391
432
  position: Point
392
433
 
393
- def __post_init__(self):
434
+ def __post_init__(self) -> None:
394
435
  if not self.uuid:
395
436
  self.uuid = str(uuid4())
396
437
 
@@ -404,13 +445,13 @@ class Net:
404
445
  wires: List[str] = field(default_factory=list) # Wire UUIDs
405
446
  labels: List[str] = field(default_factory=list) # Label UUIDs
406
447
 
407
- def add_connection(self, reference: str, pin: str):
448
+ def add_connection(self, reference: str, pin: str) -> None:
408
449
  """Add component pin to net."""
409
450
  connection = (reference, pin)
410
451
  if connection not in self.components:
411
452
  self.components.append(connection)
412
453
 
413
- def remove_connection(self, reference: str, pin: str):
454
+ def remove_connection(self, reference: str, pin: str) -> None:
414
455
  """Remove component pin from net."""
415
456
  connection = (reference, pin)
416
457
  if connection in self.components:
@@ -436,7 +477,7 @@ class Sheet:
436
477
  stroke_type: str = "solid"
437
478
  fill_color: Tuple[float, float, float, float] = (0, 0, 0, 0.0)
438
479
 
439
- def __post_init__(self):
480
+ def __post_init__(self) -> None:
440
481
  if not self.uuid:
441
482
  self.uuid = str(uuid4())
442
483
 
@@ -451,7 +492,7 @@ class SheetPin:
451
492
  pin_type: PinType = PinType.BIDIRECTIONAL
452
493
  size: float = 1.27
453
494
 
454
- def __post_init__(self):
495
+ def __post_init__(self) -> None:
455
496
  if not self.uuid:
456
497
  self.uuid = str(uuid4())
457
498
 
@@ -494,7 +535,7 @@ class Schematic:
494
535
  rectangles: List[SchematicRectangle] = field(default_factory=list)
495
536
  lib_symbols: Dict[str, Any] = field(default_factory=dict)
496
537
 
497
- def __post_init__(self):
538
+ def __post_init__(self) -> None:
498
539
  if not self.uuid:
499
540
  self.uuid = str(uuid4())
500
541
 
@@ -9,56 +9,36 @@ import logging
9
9
  import uuid as uuid_module
10
10
  from typing import Any, Dict, List, Optional, Tuple, Union
11
11
 
12
+ from .collections import BaseCollection
12
13
  from .types import Point, Wire, WireType
13
14
 
14
15
  logger = logging.getLogger(__name__)
15
16
 
16
17
 
17
- class WireCollection:
18
+ class WireCollection(BaseCollection[Wire]):
18
19
  """
19
20
  Professional wire collection with enhanced management features.
20
21
 
22
+ Inherits from BaseCollection for standard operations and adds wire-specific
23
+ functionality.
24
+
21
25
  Features:
22
- - Fast UUID-based lookup and indexing
23
- - Bulk operations for performance
26
+ - Fast UUID-based lookup and indexing (inherited)
27
+ - Bulk operations for performance (inherited)
24
28
  - Multi-point wire support
25
29
  - Validation and conflict detection
26
30
  - Junction management integration
31
+ - Wire geometry queries (horizontal, vertical, by-point)
27
32
  """
28
33
 
29
- def __init__(self, wires: Optional[List[Wire]] = None):
34
+ def __init__(self, wires: Optional[List[Wire]] = None) -> None:
30
35
  """
31
36
  Initialize wire collection.
32
37
 
33
38
  Args:
34
39
  wires: Initial list of wires
35
40
  """
36
- self._wires: List[Wire] = wires or []
37
- self._uuid_index: Dict[str, int] = {}
38
- self._modified = False
39
-
40
- # Build UUID index
41
- self._rebuild_index()
42
-
43
- logger.debug(f"WireCollection initialized with {len(self._wires)} wires")
44
-
45
- def _rebuild_index(self):
46
- """Rebuild UUID index for fast lookups."""
47
- self._uuid_index = {wire.uuid: i for i, wire in enumerate(self._wires)}
48
-
49
- def __len__(self) -> int:
50
- """Number of wires in collection."""
51
- return len(self._wires)
52
-
53
- def __iter__(self):
54
- """Iterate over wires."""
55
- return iter(self._wires)
56
-
57
- def __getitem__(self, uuid: str) -> Wire:
58
- """Get wire by UUID."""
59
- if uuid not in self._uuid_index:
60
- raise KeyError(f"Wire with UUID '{uuid}' not found")
61
- return self._wires[self._uuid_index[uuid]]
41
+ super().__init__(wires, collection_name="wires")
62
42
 
63
43
  def add(
64
44
  self,
@@ -114,37 +94,14 @@ class WireCollection:
114
94
  # Create wire
115
95
  wire = Wire(uuid=uuid, points=wire_points, wire_type=wire_type, stroke_width=stroke_width)
116
96
 
117
- # Add to collection
118
- self._wires.append(wire)
119
- self._uuid_index[uuid] = len(self._wires) - 1
120
- self._modified = True
97
+ # Add to collection using base class method
98
+ self._add_item(wire)
121
99
 
122
100
  logger.debug(f"Added wire: {len(wire_points)} points, UUID={uuid}")
123
101
  return uuid
124
102
 
125
- def remove(self, uuid: str) -> bool:
126
- """
127
- Remove wire by UUID.
128
-
129
- Args:
130
- uuid: Wire UUID to remove
131
-
132
- Returns:
133
- True if wire was removed, False if not found
134
- """
135
- if uuid not in self._uuid_index:
136
- return False
137
-
138
- index = self._uuid_index[uuid]
139
- del self._wires[index]
140
- self._rebuild_index()
141
- self._modified = True
142
-
143
- logger.debug(f"Removed wire: {uuid}")
144
- return True
145
-
146
103
  def get_by_point(
147
- self, point: Union[Point, Tuple[float, float]], tolerance: float = None
104
+ self, point: Union[Point, Tuple[float, float]], tolerance: Optional[float] = None
148
105
  ) -> List[Wire]:
149
106
  """
150
107
  Find wires that pass through or near a point.
@@ -164,7 +121,7 @@ class WireCollection:
164
121
  point = Point(point[0], point[1])
165
122
 
166
123
  matching_wires = []
167
- for wire in self._wires:
124
+ for wire in self._items:
168
125
  # Check if any wire point is close
169
126
  for wire_point in wire.points:
170
127
  if wire_point.distance_to(point) <= tolerance:
@@ -213,40 +170,35 @@ class WireCollection:
213
170
 
214
171
  def get_horizontal_wires(self) -> List[Wire]:
215
172
  """Get all horizontal wires."""
216
- return [wire for wire in self._wires if wire.is_horizontal()]
173
+ return [wire for wire in self._items if wire.is_horizontal()]
217
174
 
218
175
  def get_vertical_wires(self) -> List[Wire]:
219
176
  """Get all vertical wires."""
220
- return [wire for wire in self._wires if wire.is_vertical()]
177
+ return [wire for wire in self._items if wire.is_vertical()]
221
178
 
222
179
  def get_statistics(self) -> Dict[str, Any]:
223
- """Get wire collection statistics."""
224
- total_length = sum(wire.length for wire in self._wires)
225
- simple_wires = sum(1 for wire in self._wires if wire.is_simple())
226
- multi_point_wires = len(self._wires) - simple_wires
180
+ """Get wire collection statistics (extends base statistics)."""
181
+ base_stats = super().get_statistics()
182
+ total_length = sum(wire.length for wire in self._items)
183
+ simple_wires = sum(1 for wire in self._items if wire.is_simple())
184
+ multi_point_wires = len(self._items) - simple_wires
227
185
 
228
186
  return {
229
- "total_wires": len(self._wires),
187
+ **base_stats,
188
+ "total_wires": len(self._items),
230
189
  "simple_wires": simple_wires,
231
190
  "multi_point_wires": multi_point_wires,
232
191
  "total_length": total_length,
233
- "avg_length": total_length / len(self._wires) if self._wires else 0,
192
+ "avg_length": total_length / len(self._items) if self._items else 0,
234
193
  "horizontal_wires": len(self.get_horizontal_wires()),
235
194
  "vertical_wires": len(self.get_vertical_wires()),
236
195
  }
237
196
 
238
- def clear(self):
239
- """Remove all wires from collection."""
240
- self._wires.clear()
241
- self._uuid_index.clear()
242
- self._modified = True
243
- logger.debug("Cleared all wires")
244
-
245
197
  @property
246
198
  def modified(self) -> bool:
247
199
  """Check if collection has been modified."""
248
- return self._modified
200
+ return self.is_modified()
249
201
 
250
- def mark_saved(self):
202
+ def mark_saved(self) -> None:
251
203
  """Mark collection as saved (reset modified flag)."""
252
- self._modified = False
204
+ self.reset_modified_flag()
@@ -0,0 +1,22 @@
1
+ """
2
+ Element-specific parsers for KiCAD schematic elements.
3
+
4
+ This module contains specialized parsers for different schematic element types,
5
+ extracted from the monolithic parser.py for better maintainability.
6
+ """
7
+
8
+ from .graphics_parser import GraphicsParser
9
+
10
+ # Additional element parsers will be imported here as they are created
11
+ # from .wire_parser import WireParser
12
+ # from .label_parser import LabelParser
13
+ # from .text_parser import TextParser
14
+ # from .sheet_parser import SheetParser
15
+ # from .library_parser import LibraryParser
16
+ # from .symbol_parser import SymbolParser
17
+ # from .metadata_parser import MetadataParser
18
+
19
+ __all__ = [
20
+ "GraphicsParser",
21
+ # Will be populated as parsers are added
22
+ ]