kicad-sch-api 0.4.1__py3-none-any.whl → 0.5.1__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.
Files changed (66) hide show
  1. kicad_sch_api/__init__.py +67 -2
  2. kicad_sch_api/cli/kicad_to_python.py +169 -0
  3. kicad_sch_api/collections/__init__.py +23 -8
  4. kicad_sch_api/collections/base.py +369 -59
  5. kicad_sch_api/collections/components.py +1376 -187
  6. kicad_sch_api/collections/junctions.py +129 -289
  7. kicad_sch_api/collections/labels.py +391 -287
  8. kicad_sch_api/collections/wires.py +202 -316
  9. kicad_sch_api/core/__init__.py +37 -2
  10. kicad_sch_api/core/component_bounds.py +34 -12
  11. kicad_sch_api/core/components.py +146 -7
  12. kicad_sch_api/core/config.py +25 -12
  13. kicad_sch_api/core/connectivity.py +692 -0
  14. kicad_sch_api/core/exceptions.py +175 -0
  15. kicad_sch_api/core/factories/element_factory.py +3 -1
  16. kicad_sch_api/core/formatter.py +24 -7
  17. kicad_sch_api/core/geometry.py +94 -5
  18. kicad_sch_api/core/managers/__init__.py +4 -0
  19. kicad_sch_api/core/managers/base.py +76 -0
  20. kicad_sch_api/core/managers/file_io.py +3 -1
  21. kicad_sch_api/core/managers/format_sync.py +3 -2
  22. kicad_sch_api/core/managers/graphics.py +3 -2
  23. kicad_sch_api/core/managers/hierarchy.py +661 -0
  24. kicad_sch_api/core/managers/metadata.py +4 -2
  25. kicad_sch_api/core/managers/sheet.py +52 -14
  26. kicad_sch_api/core/managers/text_elements.py +3 -2
  27. kicad_sch_api/core/managers/validation.py +3 -2
  28. kicad_sch_api/core/managers/wire.py +112 -54
  29. kicad_sch_api/core/parsing_utils.py +63 -0
  30. kicad_sch_api/core/pin_utils.py +103 -9
  31. kicad_sch_api/core/schematic.py +343 -29
  32. kicad_sch_api/core/types.py +79 -7
  33. kicad_sch_api/exporters/__init__.py +10 -0
  34. kicad_sch_api/exporters/python_generator.py +610 -0
  35. kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
  36. kicad_sch_api/geometry/__init__.py +15 -3
  37. kicad_sch_api/geometry/routing.py +211 -0
  38. kicad_sch_api/parsers/elements/label_parser.py +30 -8
  39. kicad_sch_api/parsers/elements/symbol_parser.py +255 -83
  40. kicad_sch_api/utils/logging.py +555 -0
  41. kicad_sch_api/utils/logging_decorators.py +587 -0
  42. kicad_sch_api/utils/validation.py +16 -22
  43. kicad_sch_api/wrappers/__init__.py +14 -0
  44. kicad_sch_api/wrappers/base.py +89 -0
  45. kicad_sch_api/wrappers/wire.py +198 -0
  46. kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
  47. kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
  48. kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
  49. {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
  50. mcp_server/__init__.py +34 -0
  51. mcp_server/example_logging_integration.py +506 -0
  52. mcp_server/models.py +252 -0
  53. mcp_server/server.py +357 -0
  54. mcp_server/tools/__init__.py +32 -0
  55. mcp_server/tools/component_tools.py +516 -0
  56. mcp_server/tools/connectivity_tools.py +532 -0
  57. mcp_server/tools/consolidated_tools.py +1216 -0
  58. mcp_server/tools/pin_discovery.py +333 -0
  59. mcp_server/utils/__init__.py +38 -0
  60. mcp_server/utils/logging.py +127 -0
  61. mcp_server/utils.py +36 -0
  62. kicad_sch_api-0.4.1.dist-info/METADATA +0 -491
  63. kicad_sch_api-0.4.1.dist-info/RECORD +0 -87
  64. kicad_sch_api-0.4.1.dist-info/entry_points.txt +0 -2
  65. {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
  66. {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,8 +1,8 @@
1
1
  """
2
- Wire collection with specialized indexing and wire-specific operations.
2
+ Enhanced wire management with IndexRegistry integration.
3
3
 
4
- Extends the base IndexedCollection to provide wire-specific features like
5
- endpoint indexing, multi-point wire support, and connectivity management.
4
+ Provides WireCollection using BaseCollection infrastructure with
5
+ endpoint indexing and wire geometry queries.
6
6
  """
7
7
 
8
8
  import logging
@@ -10,397 +10,283 @@ import uuid as uuid_module
10
10
  from typing import Any, Dict, List, Optional, Tuple, Union
11
11
 
12
12
  from ..core.types import Point, Wire, WireType
13
- from .base import IndexedCollection
13
+ from .base import BaseCollection, IndexSpec, ValidationLevel
14
14
 
15
15
  logger = logging.getLogger(__name__)
16
16
 
17
17
 
18
- class WireCollection(IndexedCollection[Wire]):
18
+ class WireCollection(BaseCollection[Wire]):
19
19
  """
20
- Professional wire collection with enhanced management features.
20
+ Wire collection with endpoint indexing and geometry queries.
21
21
 
22
- Extends IndexedCollection with wire-specific features:
23
- - Endpoint indexing for connectivity analysis
22
+ Inherits from BaseCollection for UUID indexing and adds wire-specific
23
+ functionality for endpoint queries and wire type filtering.
24
+
25
+ Features:
26
+ - Fast UUID lookup via IndexRegistry
24
27
  - Multi-point wire support
25
- - Wire type classification (normal, bus, etc.)
26
- - Bulk operations for performance
27
- - Junction management integration
28
+ - Endpoint-based queries
29
+ - Horizontal/vertical wire detection
30
+ - Lazy index rebuilding
31
+ - Batch mode support
28
32
  """
29
33
 
30
- def __init__(self, wires: Optional[List[Wire]] = None):
34
+ def __init__(
35
+ self,
36
+ wires: Optional[List[Wire]] = None,
37
+ validation_level: ValidationLevel = ValidationLevel.NORMAL,
38
+ ):
31
39
  """
32
40
  Initialize wire collection.
33
41
 
34
42
  Args:
35
43
  wires: Initial list of wires
44
+ validation_level: Validation level for operations
36
45
  """
37
- self._endpoint_index: Dict[Tuple[float, float], List[Wire]] = {}
38
- self._type_index: Dict[WireType, List[Wire]] = {}
46
+ super().__init__(validation_level=validation_level)
47
+
48
+ # Add initial wires
49
+ if wires:
50
+ with self.batch_mode():
51
+ for wire in wires:
52
+ super().add(wire)
39
53
 
40
- super().__init__(wires)
54
+ logger.debug(f"WireCollection initialized with {len(self)} wires")
41
55
 
42
- # Abstract method implementations
56
+ # BaseCollection abstract method implementations
43
57
  def _get_item_uuid(self, item: Wire) -> str:
44
58
  """Extract UUID from wire."""
45
59
  return item.uuid
46
60
 
47
61
  def _create_item(self, **kwargs) -> Wire:
48
- """Create a new wire with given parameters."""
49
- # This will be called by add() methods
50
- raise NotImplementedError("Use add() method instead")
51
-
52
- def _build_additional_indexes(self) -> None:
53
- """Build wire-specific indexes."""
54
- # Clear existing indexes
55
- self._endpoint_index.clear()
56
- self._type_index.clear()
57
-
58
- # Rebuild indexes from current items
59
- for wire in self._items:
60
- # Endpoint index - index by all endpoints
61
- for point in wire.points:
62
- endpoint = (point.x, point.y)
63
- if endpoint not in self._endpoint_index:
64
- self._endpoint_index[endpoint] = []
65
- self._endpoint_index[endpoint].append(wire)
66
-
67
- # Type index
68
- wire_type = getattr(wire, "wire_type", WireType.WIRE)
69
- if wire_type not in self._type_index:
70
- self._type_index[wire_type] = []
71
- self._type_index[wire_type].append(wire)
72
-
73
- # Wire-specific methods
62
+ """Create a new wire (not typically used directly)."""
63
+ raise NotImplementedError("Use add() method to create wires")
64
+
65
+ def _get_index_specs(self) -> List[IndexSpec]:
66
+ """Get index specifications for wire collection."""
67
+ return [
68
+ IndexSpec(
69
+ name="uuid",
70
+ key_func=lambda w: w.uuid,
71
+ unique=True,
72
+ description="UUID index for fast lookups",
73
+ ),
74
+ ]
75
+
76
+ # Wire-specific add method
74
77
  def add(
75
78
  self,
76
- start: Union[Point, Tuple[float, float]],
77
- end: Union[Point, Tuple[float, float]],
78
- wire_type: WireType = WireType.WIRE,
79
- stroke_width: float = 0.0,
80
- stroke_type: str = "default",
81
- wire_uuid: Optional[str] = None,
82
- ) -> Wire:
83
- """
84
- Add a new wire to the collection.
85
-
86
- Args:
87
- start: Starting point of the wire
88
- end: Ending point of the wire
89
- wire_type: Type of wire (normal, bus, etc.)
90
- stroke_width: Wire stroke width
91
- stroke_type: Wire stroke type
92
- wire_uuid: Specific UUID for wire (auto-generated if None)
93
-
94
- Returns:
95
- Newly created Wire
96
- """
97
- # Convert tuples to Points if needed
98
- if isinstance(start, tuple):
99
- start = Point(start[0], start[1])
100
- if isinstance(end, tuple):
101
- end = Point(end[0], end[1])
102
-
103
- # Validate wire points
104
- if start == end:
105
- raise ValueError("Wire start and end points cannot be the same")
106
-
107
- # Generate UUID if not provided
108
- if wire_uuid is None:
109
- wire_uuid = str(uuid_module.uuid4())
110
-
111
- # Create wire
112
- wire = Wire(
113
- uuid=wire_uuid,
114
- points=[start, end],
115
- wire_type=wire_type,
116
- stroke_width=stroke_width,
117
- stroke_type=stroke_type,
118
- )
119
-
120
- # Add to collection using base class method
121
- return super().add(wire)
122
-
123
- def add_multi_point(
124
- self,
125
- points: List[Union[Point, Tuple[float, float]]],
79
+ start: Optional[Union[Point, Tuple[float, float]]] = None,
80
+ end: Optional[Union[Point, Tuple[float, float]]] = None,
81
+ points: Optional[List[Union[Point, Tuple[float, float]]]] = None,
126
82
  wire_type: WireType = WireType.WIRE,
127
83
  stroke_width: float = 0.0,
128
- stroke_type: str = "default",
129
- wire_uuid: Optional[str] = None,
130
- ) -> Wire:
84
+ uuid: Optional[str] = None,
85
+ ) -> str:
131
86
  """
132
- Add a multi-point wire to the collection.
87
+ Add a wire to the collection.
133
88
 
134
89
  Args:
135
- points: List of points defining the wire path
136
- wire_type: Type of wire (normal, bus, etc.)
137
- stroke_width: Wire stroke width
138
- stroke_type: Wire stroke type
139
- wire_uuid: Specific UUID for wire (auto-generated if None)
90
+ start: Start point (for simple wires)
91
+ end: End point (for simple wires)
92
+ points: List of points (for multi-point wires)
93
+ wire_type: Wire type (wire or bus)
94
+ stroke_width: Line width
95
+ uuid: Optional UUID (auto-generated if not provided)
140
96
 
141
97
  Returns:
142
- Newly created Wire
98
+ UUID of the created wire
143
99
 
144
100
  Raises:
145
- ValueError: If less than 2 points provided
101
+ ValueError: If neither start/end nor points are provided
102
+ ValueError: If UUID already exists
146
103
  """
147
- if len(points) < 2:
148
- raise ValueError("Wire must have at least 2 points")
149
-
150
- # Convert tuples to Points if needed
151
- converted_points = []
152
- for point in points:
153
- if isinstance(point, tuple):
154
- converted_points.append(Point(point[0], point[1]))
155
- else:
156
- converted_points.append(point)
157
-
158
104
  # Generate UUID if not provided
159
- if wire_uuid is None:
160
- wire_uuid = str(uuid_module.uuid4())
161
-
162
- # Create wire
163
- wire = Wire(
164
- uuid=wire_uuid,
165
- points=converted_points,
166
- wire_type=wire_type,
167
- stroke_width=stroke_width,
168
- stroke_type=stroke_type,
169
- )
170
-
171
- # Add to collection using base class method
172
- return super().add(wire)
173
-
174
- def get_wires_at_point(self, point: Union[Point, Tuple[float, float]]) -> List[Wire]:
175
- """
176
- Get all wires that pass through a specific point.
177
-
178
- Args:
179
- point: Point to search at
180
-
181
- Returns:
182
- List of wires passing through the point
183
- """
184
- self._ensure_indexes_current()
185
-
186
- if isinstance(point, Point):
187
- endpoint = (point.x, point.y)
105
+ if uuid is None:
106
+ uuid = str(uuid_module.uuid4())
188
107
  else:
189
- endpoint = point
108
+ # Check for duplicate
109
+ self._ensure_indexes_current()
110
+ if self._index_registry.has_key("uuid", uuid):
111
+ raise ValueError(f"Wire with UUID '{uuid}' already exists")
112
+
113
+ # Convert points
114
+ wire_points = []
115
+ if points:
116
+ # Multi-point wire
117
+ for point in points:
118
+ if isinstance(point, tuple):
119
+ wire_points.append(Point(point[0], point[1]))
120
+ else:
121
+ wire_points.append(point)
122
+ elif start is not None and end is not None:
123
+ # Simple 2-point wire
124
+ if isinstance(start, tuple):
125
+ start = Point(start[0], start[1])
126
+ if isinstance(end, tuple):
127
+ end = Point(end[0], end[1])
128
+ wire_points = [start, end]
129
+ else:
130
+ raise ValueError("Must provide either start/end points or points list")
190
131
 
191
- return self._endpoint_index.get(endpoint, []).copy()
132
+ # Validate wire has at least 2 points
133
+ if len(wire_points) < 2:
134
+ raise ValueError("Wire must have at least 2 points")
192
135
 
193
- def get_wires_by_type(self, wire_type: WireType) -> List[Wire]:
194
- """
195
- Get all wires of a specific type.
136
+ # Create wire
137
+ wire = Wire(uuid=uuid, points=wire_points, wire_type=wire_type, stroke_width=stroke_width)
196
138
 
197
- Args:
198
- wire_type: Type of wires to find
139
+ # Add to collection
140
+ super().add(wire)
199
141
 
200
- Returns:
201
- List of wires of the specified type
202
- """
203
- self._ensure_indexes_current()
204
- return self._type_index.get(wire_type, []).copy()
142
+ logger.debug(f"Added wire: {len(wire_points)} points, UUID={uuid}")
143
+ return uuid
205
144
 
206
- def get_connected_wires(self, wire: Wire) -> List[Wire]:
145
+ # Endpoint-based queries
146
+ def get_by_endpoint(
147
+ self, point: Union[Point, Tuple[float, float]], tolerance: float = 0.01
148
+ ) -> List[Wire]:
207
149
  """
208
- Get all wires connected to the given wire.
150
+ Find all wires with an endpoint near a given point.
209
151
 
210
152
  Args:
211
- wire: Wire to find connections for
153
+ point: Point to search for
154
+ tolerance: Distance tolerance for matching
212
155
 
213
156
  Returns:
214
- List of connected wires (excluding the input wire)
157
+ List of wires with endpoint near the point
215
158
  """
216
- connected = set()
159
+ if isinstance(point, tuple):
160
+ point = Point(point[0], point[1])
217
161
 
218
- # Check connections at all endpoints
219
- for point in wire.points:
220
- endpoint_wires = self.get_wires_at_point(point)
221
- for endpoint_wire in endpoint_wires:
222
- if endpoint_wire.uuid != wire.uuid:
223
- connected.add(endpoint_wire)
162
+ matching_wires = []
163
+ for wire in self._items:
164
+ # Check first and last point (endpoints)
165
+ if (wire.points[0].distance_to(point) <= tolerance or
166
+ wire.points[-1].distance_to(point) <= tolerance):
167
+ matching_wires.append(wire)
224
168
 
225
- return list(connected)
169
+ return matching_wires
226
170
 
227
- def find_wire_networks(self) -> List[List[Wire]]:
171
+ def get_at_point(
172
+ self, point: Union[Point, Tuple[float, float]], tolerance: float = 0.01
173
+ ) -> List[Wire]:
228
174
  """
229
- Find all connected wire networks in the collection.
175
+ Find all wires that pass through or near a point.
176
+
177
+ Args:
178
+ point: Point to search for
179
+ tolerance: Distance tolerance for matching
230
180
 
231
181
  Returns:
232
- List of wire networks, each network is a list of connected wires
182
+ List of wires passing through the point
233
183
  """
234
- visited = set()
235
- networks = []
184
+ if isinstance(point, tuple):
185
+ point = Point(point[0], point[1])
236
186
 
187
+ matching_wires = []
237
188
  for wire in self._items:
238
- if wire.uuid in visited:
239
- continue
240
-
241
- # Start a new network with this wire
242
- network = []
243
- to_visit = [wire]
244
-
245
- while to_visit:
246
- current_wire = to_visit.pop()
247
- if current_wire.uuid in visited:
248
- continue
249
-
250
- visited.add(current_wire.uuid)
251
- network.append(current_wire)
252
-
253
- # Add all connected wires to visit list
254
- connected = self.get_connected_wires(current_wire)
255
- for connected_wire in connected:
256
- if connected_wire.uuid not in visited:
257
- to_visit.append(connected_wire)
258
-
259
- if network:
260
- networks.append(network)
189
+ # Check if any point in wire is near the search point
190
+ for wire_point in wire.points:
191
+ if wire_point.distance_to(point) <= tolerance:
192
+ matching_wires.append(wire)
193
+ break # Found match, move to next wire
261
194
 
262
- return networks
195
+ return matching_wires
263
196
 
264
- def modify_wire_path(
265
- self, wire_uuid: str, new_points: List[Union[Point, Tuple[float, float]]]
266
- ) -> bool:
197
+ # Wire geometry queries
198
+ def get_horizontal(self) -> List[Wire]:
267
199
  """
268
- Modify the path of an existing wire.
269
-
270
- Args:
271
- wire_uuid: UUID of wire to modify
272
- new_points: New points for the wire path
200
+ Get all horizontal wires (Y coordinates equal).
273
201
 
274
202
  Returns:
275
- True if wire was modified, False if not found
276
-
277
- Raises:
278
- ValueError: If less than 2 points provided
203
+ List of horizontal wires
279
204
  """
280
- if len(new_points) < 2:
281
- raise ValueError("Wire must have at least 2 points")
282
-
283
- wire = self.get(wire_uuid)
284
- if not wire:
285
- return False
286
-
287
- # Convert tuples to Points if needed
288
- converted_points = []
289
- for point in new_points:
290
- if isinstance(point, tuple):
291
- converted_points.append(Point(point[0], point[1]))
292
- else:
293
- converted_points.append(point)
294
-
295
- # Update wire points
296
- wire.points = converted_points
297
- self._mark_modified()
298
- self._mark_indexes_dirty()
205
+ horizontal = []
206
+ for wire in self._items:
207
+ if len(wire.points) == 2:
208
+ # Simple 2-point wire
209
+ if abs(wire.points[0].y - wire.points[1].y) < 0.01:
210
+ horizontal.append(wire)
299
211
 
300
- logger.debug(f"Modified wire {wire_uuid} path with {len(new_points)} points")
301
- return True
212
+ return horizontal
302
213
 
303
- # Bulk operations for performance
304
- def remove_wires_at_point(self, point: Union[Point, Tuple[float, float]]) -> int:
214
+ def get_vertical(self) -> List[Wire]:
305
215
  """
306
- Remove all wires passing through a specific point.
307
-
308
- Args:
309
- point: Point where wires should be removed
216
+ Get all vertical wires (X coordinates equal).
310
217
 
311
218
  Returns:
312
- Number of wires removed
219
+ List of vertical wires
313
220
  """
314
- wires_to_remove = self.get_wires_at_point(point)
315
-
316
- for wire in wires_to_remove:
317
- self.remove(wire.uuid)
221
+ vertical = []
222
+ for wire in self._items:
223
+ if len(wire.points) == 2:
224
+ # Simple 2-point wire
225
+ if abs(wire.points[0].x - wire.points[1].x) < 0.01:
226
+ vertical.append(wire)
318
227
 
319
- logger.info(f"Removed {len(wires_to_remove)} wires at point {point}")
320
- return len(wires_to_remove)
228
+ return vertical
321
229
 
322
- def bulk_update_stroke(
323
- self,
324
- wire_type: Optional[WireType] = None,
325
- stroke_width: Optional[float] = None,
326
- stroke_type: Optional[str] = None,
327
- ) -> int:
230
+ def get_by_type(self, wire_type: WireType) -> List[Wire]:
328
231
  """
329
- Bulk update stroke properties for wires.
232
+ Get all wires of a specific type.
330
233
 
331
234
  Args:
332
- wire_type: Filter by wire type (None for all)
333
- stroke_width: New stroke width (None to keep existing)
334
- stroke_type: New stroke type (None to keep existing)
235
+ wire_type: Wire type to filter by
335
236
 
336
237
  Returns:
337
- Number of wires updated
238
+ List of wires matching the type
338
239
  """
339
- # Get wires to update
340
- if wire_type is not None:
341
- wires_to_update = self.get_wires_by_type(wire_type)
342
- else:
343
- wires_to_update = list(self._items)
344
-
345
- # Apply updates
346
- for wire in wires_to_update:
347
- if stroke_width is not None:
348
- wire.stroke_width = stroke_width
349
- if stroke_type is not None:
350
- wire.stroke_type = stroke_type
351
-
352
- if wires_to_update:
353
- self._mark_modified()
240
+ return [w for w in self._items if w.wire_type == wire_type]
354
241
 
355
- logger.info(f"Bulk updated stroke properties for {len(wires_to_update)} wires")
356
- return len(wires_to_update)
357
-
358
- # Collection statistics
359
- def get_connection_statistics(self) -> Dict[str, Any]:
242
+ # Statistics
243
+ def get_statistics(self) -> Dict[str, Any]:
360
244
  """
361
- Get connection statistics for the wire collection.
245
+ Get wire collection statistics.
362
246
 
363
247
  Returns:
364
- Dictionary with connection statistics
365
- """
366
- stats = super().get_statistics()
367
-
368
- # Add wire-specific statistics
369
- stats.update(
370
- {
371
- "endpoint_count": len(self._endpoint_index),
372
- "wire_types": {
373
- wire_type.value: len(wires) for wire_type, wires in self._type_index.items()
374
- },
375
- "networks": len(self.find_wire_networks()),
376
- "total_length": sum(self._calculate_wire_length(wire) for wire in self._items),
377
- }
378
- )
379
-
380
- return stats
381
-
382
- def _calculate_wire_length(self, wire: Wire) -> float:
248
+ Dictionary with wire statistics
383
249
  """
384
- Calculate the total length of a wire.
385
-
386
- Args:
387
- wire: Wire to calculate length for
388
-
389
- Returns:
390
- Total wire length
391
- """
392
- if len(wire.points) < 2:
393
- return 0.0
394
-
395
- total_length = 0.0
396
- for i in range(len(wire.points) - 1):
397
- start_point = wire.points[i]
398
- end_point = wire.points[i + 1]
399
-
400
- dx = end_point.x - start_point.x
401
- dy = end_point.y - start_point.y
402
- segment_length = (dx**2 + dy**2) ** 0.5
403
-
404
- total_length += segment_length
405
-
406
- return total_length
250
+ if not self._items:
251
+ base_stats = super().get_statistics()
252
+ base_stats.update({
253
+ "total_wires": 0,
254
+ "total_segments": 0,
255
+ "wire_count": 0,
256
+ "bus_count": 0,
257
+ "horizontal_count": 0,
258
+ "vertical_count": 0,
259
+ "avg_points_per_wire": 0,
260
+ })
261
+ return base_stats
262
+
263
+ wire_count = sum(1 for w in self._items if w.wire_type == WireType.WIRE)
264
+ bus_count = sum(1 for w in self._items if w.wire_type == WireType.BUS)
265
+ total_segments = sum(len(w.points) - 1 for w in self._items)
266
+ avg_points = sum(len(w.points) for w in self._items) / len(self._items)
267
+
268
+ horizontal = len(self.get_horizontal())
269
+ vertical = len(self.get_vertical())
270
+
271
+ base_stats = super().get_statistics()
272
+ base_stats.update({
273
+ "total_wires": len(self._items),
274
+ "total_segments": total_segments,
275
+ "wire_count": wire_count,
276
+ "bus_count": bus_count,
277
+ "horizontal_count": horizontal,
278
+ "vertical_count": vertical,
279
+ "avg_points_per_wire": avg_points,
280
+ })
281
+
282
+ return base_stats
283
+
284
+ # Compatibility methods
285
+ @property
286
+ def modified(self) -> bool:
287
+ """Check if collection has been modified (compatibility)."""
288
+ return self.is_modified
289
+
290
+ def mark_saved(self) -> None:
291
+ """Mark collection as saved (reset modified flag)."""
292
+ self.mark_clean()
@@ -1,10 +1,28 @@
1
1
  """Core kicad-sch-api functionality."""
2
2
 
3
- from .components import Component, ComponentCollection
3
+ from ..collections import Component, ComponentCollection
4
4
  from .formatter import ExactFormatter
5
5
  from .parser import SExpressionParser
6
6
  from .schematic import Schematic, create_schematic, load_schematic
7
- from .types import Junction, Label, Net, Point, SchematicSymbol, Wire
7
+ from .types import Junction, Label, Net, PinInfo, Point, SchematicSymbol, Wire
8
+ # Exception hierarchy
9
+ from .exceptions import (
10
+ KiCadSchError,
11
+ ValidationError,
12
+ ReferenceError,
13
+ LibraryError,
14
+ GeometryError,
15
+ NetError,
16
+ ParseError,
17
+ FormatError,
18
+ CollectionError,
19
+ ElementNotFoundError,
20
+ DuplicateElementError,
21
+ CollectionOperationError,
22
+ FileOperationError,
23
+ CLIError,
24
+ SchematicStateError,
25
+ )
8
26
 
9
27
  __all__ = [
10
28
  "Schematic",
@@ -16,8 +34,25 @@ __all__ = [
16
34
  "Junction",
17
35
  "Label",
18
36
  "Net",
37
+ "PinInfo",
19
38
  "SExpressionParser",
20
39
  "ExactFormatter",
21
40
  "load_schematic",
22
41
  "create_schematic",
42
+ # Exceptions
43
+ "KiCadSchError",
44
+ "ValidationError",
45
+ "ReferenceError",
46
+ "LibraryError",
47
+ "GeometryError",
48
+ "NetError",
49
+ "ParseError",
50
+ "FormatError",
51
+ "CollectionError",
52
+ "ElementNotFoundError",
53
+ "DuplicateElementError",
54
+ "CollectionOperationError",
55
+ "FileOperationError",
56
+ "CLIError",
57
+ "SchematicStateError",
23
58
  ]