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
@@ -10,6 +10,7 @@ from typing import Any, Dict, List, Optional
10
10
 
11
11
  import sexpdata
12
12
 
13
+ from ...core.parsing_utils import parse_bool_property
13
14
  from ...core.types import Point
14
15
  from ..base import BaseElementParser
15
16
 
@@ -38,6 +39,7 @@ class SymbolParser(BaseElementParser):
38
39
  "pins": [],
39
40
  "in_bom": True,
40
41
  "on_board": True,
42
+ "instances": [],
41
43
  }
42
44
 
43
45
  for sub_item in item[1:]:
@@ -61,6 +63,11 @@ class SymbolParser(BaseElementParser):
61
63
  prop_data = self._parse_property(sub_item)
62
64
  if prop_data:
63
65
  prop_name = prop_data.get("name")
66
+
67
+ # Store original S-expression for format preservation
68
+ sexp_key = f"__sexp_{prop_name}"
69
+ symbol_data["properties"][sexp_key] = sub_item
70
+
64
71
  if prop_name == "Reference":
65
72
  symbol_data["reference"] = prop_data.get("value")
66
73
  elif prop_name == "Value":
@@ -74,9 +81,20 @@ class SymbolParser(BaseElementParser):
74
81
  prop_value = str(prop_value).replace('\\"', '"')
75
82
  symbol_data["properties"][prop_name] = prop_value
76
83
  elif element_type == "in_bom":
77
- symbol_data["in_bom"] = sub_item[1] == "yes" if len(sub_item) > 1 else True
84
+ symbol_data["in_bom"] = parse_bool_property(
85
+ sub_item[1] if len(sub_item) > 1 else None,
86
+ default=True
87
+ )
78
88
  elif element_type == "on_board":
79
- symbol_data["on_board"] = sub_item[1] == "yes" if len(sub_item) > 1 else True
89
+ symbol_data["on_board"] = parse_bool_property(
90
+ sub_item[1] if len(sub_item) > 1 else None,
91
+ default=True
92
+ )
93
+ elif element_type == "instances":
94
+ # Parse instances section
95
+ instances = self._parse_instances(sub_item)
96
+ if instances:
97
+ symbol_data["instances"] = instances
80
98
 
81
99
  return symbol_data
82
100
 
@@ -95,6 +113,67 @@ class SymbolParser(BaseElementParser):
95
113
  "value": item[2] if len(item) > 2 else None,
96
114
  }
97
115
 
116
+ def _parse_instances(self, item: List[Any]) -> List[Dict[str, Any]]:
117
+ """
118
+ Parse instances section from S-expression.
119
+
120
+ Format:
121
+ (instances
122
+ (project "project_name"
123
+ (path "/root_uuid/sheet_uuid"
124
+ (reference "R1")
125
+ (unit 1))))
126
+ """
127
+ from ...core.types import SymbolInstance
128
+
129
+ instances = []
130
+
131
+ for sub_item in item[1:]:
132
+ if not isinstance(sub_item, list) or len(sub_item) == 0:
133
+ continue
134
+
135
+ element_type = str(sub_item[0]) if isinstance(sub_item[0], sexpdata.Symbol) else None
136
+
137
+ if element_type == "project":
138
+ # Parse project instance
139
+ project = sub_item[1] if len(sub_item) > 1 else None
140
+
141
+ # Find path section within project
142
+ for project_sub in sub_item[2:]:
143
+ if not isinstance(project_sub, list) or len(project_sub) == 0:
144
+ continue
145
+
146
+ path_type = str(project_sub[0]) if isinstance(project_sub[0], sexpdata.Symbol) else None
147
+
148
+ if path_type == "path":
149
+ # Extract path value
150
+ path = project_sub[1] if len(project_sub) > 1 else "/"
151
+ reference = None
152
+ unit = 1
153
+
154
+ # Parse reference and unit from path subsections
155
+ for path_sub in project_sub[2:]:
156
+ if not isinstance(path_sub, list) or len(path_sub) == 0:
157
+ continue
158
+
159
+ path_sub_type = str(path_sub[0]) if isinstance(path_sub[0], sexpdata.Symbol) else None
160
+
161
+ if path_sub_type == "reference":
162
+ reference = path_sub[1] if len(path_sub) > 1 else None
163
+ elif path_sub_type == "unit":
164
+ unit = int(path_sub[1]) if len(path_sub) > 1 else 1
165
+
166
+ # Create instance
167
+ if path and reference:
168
+ instance = SymbolInstance(
169
+ path=path,
170
+ reference=reference,
171
+ unit=unit
172
+ )
173
+ instances.append(instance)
174
+
175
+ return instances
176
+
98
177
 
99
178
  def _symbol_to_sexp(self, symbol_data: Dict[str, Any], schematic_uuid: str = None) -> List[Any]:
100
179
  """Convert symbol to S-expression."""
@@ -132,40 +211,85 @@ class SymbolParser(BaseElementParser):
132
211
  # Add properties with proper positioning and effects
133
212
  lib_id = symbol_data.get("lib_id", "")
134
213
  is_power_symbol = "power:" in lib_id
214
+ rotation = symbol_data.get("rotation", 0)
135
215
 
136
216
  if symbol_data.get("reference"):
137
- # Power symbol references should be hidden by default
138
- ref_hide = is_power_symbol
139
- ref_prop = self._create_property_with_positioning(
140
- "Reference", symbol_data["reference"], pos, 0, "left", hide=ref_hide
141
- )
142
- sexp.append(ref_prop)
217
+ # Check for preserved S-expression
218
+ preserved_ref = symbol_data.get("properties", {}).get("__sexp_Reference")
219
+ if preserved_ref:
220
+ # Use preserved format but update the value
221
+ ref_prop = list(preserved_ref)
222
+ if len(ref_prop) >= 3:
223
+ ref_prop[2] = symbol_data["reference"]
224
+ sexp.append(ref_prop)
225
+ else:
226
+ # No preserved format - create new (for newly added components)
227
+ ref_hide = is_power_symbol
228
+ ref_prop = self._create_property_with_positioning(
229
+ "Reference", symbol_data["reference"], pos, 0, "left", hide=ref_hide, rotation=rotation
230
+ )
231
+ sexp.append(ref_prop)
143
232
 
144
233
  if symbol_data.get("value"):
145
- # Power symbol values need different positioning
146
- if is_power_symbol:
147
- val_prop = self._create_power_symbol_value_property(
148
- symbol_data["value"], pos, lib_id
149
- )
234
+ # Check for preserved S-expression
235
+ preserved_val = symbol_data.get("properties", {}).get("__sexp_Value")
236
+ if preserved_val:
237
+ # Use preserved format but update the value
238
+ val_prop = list(preserved_val)
239
+ if len(val_prop) >= 3:
240
+ val_prop[2] = symbol_data["value"]
241
+ sexp.append(val_prop)
150
242
  else:
151
- val_prop = self._create_property_with_positioning(
152
- "Value", symbol_data["value"], pos, 1, "left"
153
- )
154
- sexp.append(val_prop)
243
+ # No preserved format - create new (for newly added components)
244
+ if is_power_symbol:
245
+ val_prop = self._create_power_symbol_value_property(
246
+ symbol_data["value"], pos, lib_id, rotation
247
+ )
248
+ else:
249
+ val_prop = self._create_property_with_positioning(
250
+ "Value", symbol_data["value"], pos, 1, "left", rotation=rotation
251
+ )
252
+ sexp.append(val_prop)
155
253
 
156
254
  footprint = symbol_data.get("footprint")
157
255
  if footprint is not None: # Include empty strings but not None
158
- fp_prop = self._create_property_with_positioning(
159
- "Footprint", footprint, pos, 2, "left", hide=True
160
- )
161
- sexp.append(fp_prop)
256
+ # Check for preserved S-expression
257
+ preserved_fp = symbol_data.get("properties", {}).get("__sexp_Footprint")
258
+ if preserved_fp:
259
+ # Use preserved format but update the value
260
+ fp_prop = list(preserved_fp)
261
+ if len(fp_prop) >= 3:
262
+ fp_prop[2] = footprint
263
+ sexp.append(fp_prop)
264
+ else:
265
+ # No preserved format - create new (for newly added components)
266
+ fp_prop = self._create_property_with_positioning(
267
+ "Footprint", footprint, pos, 2, "left", hide=True
268
+ )
269
+ sexp.append(fp_prop)
162
270
 
163
271
  for prop_name, prop_value in symbol_data.get("properties", {}).items():
164
- escaped_value = str(prop_value).replace('"', '\\"')
165
- prop = self._create_property_with_positioning(
166
- prop_name, escaped_value, pos, 3, "left", hide=True
167
- )
168
- sexp.append(prop)
272
+ # Skip internal preservation keys
273
+ if prop_name.startswith("__sexp_"):
274
+ continue
275
+
276
+ # Check if we have a preserved S-expression for this custom property
277
+ preserved_prop = symbol_data.get("properties", {}).get(f"__sexp_{prop_name}")
278
+ if preserved_prop:
279
+ # Use preserved format but update the value
280
+ prop = list(preserved_prop)
281
+ if len(prop) >= 3:
282
+ # Re-escape quotes when saving
283
+ escaped_value = str(prop_value).replace('"', '\\"')
284
+ prop[2] = escaped_value
285
+ sexp.append(prop)
286
+ else:
287
+ # No preserved format - create new (for newly added properties)
288
+ escaped_value = str(prop_value).replace('"', '\\"')
289
+ prop = self._create_property_with_positioning(
290
+ prop_name, escaped_value, pos, 3, "left", hide=True
291
+ )
292
+ sexp.append(prop)
169
293
 
170
294
  # Add pin UUID assignments (required by KiCAD)
171
295
  for pin in symbol_data.get("pins", []):
@@ -177,53 +301,83 @@ class SymbolParser(BaseElementParser):
177
301
  # Add instances section (required by KiCAD)
178
302
  from ...core.config import config
179
303
 
180
- # Get project name from config or properties
181
- project_name = symbol_data.get("properties", {}).get("project_name")
182
- if not project_name:
183
- project_name = getattr(self, "project_name", config.defaults.project_name)
184
-
185
- # CRITICAL FIX: Use the FULL hierarchy_path from properties if available
186
- # For hierarchical schematics, this contains the complete path: /root_uuid/sheet_symbol_uuid/...
187
- # This ensures KiCad can properly annotate components in sub-sheets
188
- hierarchy_path = symbol_data.get("properties", {}).get("hierarchy_path")
189
- if hierarchy_path:
190
- # Use the full hierarchical path (includes root + all sheet symbols)
191
- instance_path = hierarchy_path
192
- logger.debug(
193
- f"🔧 Using FULL hierarchy_path: {instance_path} for component {symbol_data.get('reference', 'unknown')}"
194
- )
304
+ # HIERARCHICAL FIX: Check if user explicitly set instances
305
+ # If so, preserve them exactly as-is (don't generate!)
306
+ user_instances = symbol_data.get("instances")
307
+ if user_instances:
308
+ logger.debug(f"🔍 HIERARCHICAL FIX: Component {symbol_data.get('reference')} has {len(user_instances)} user-set instance(s)")
309
+ # Build instances sexp from user data
310
+ instances_sexp = [sexpdata.Symbol("instances")]
311
+ for inst in user_instances:
312
+ project = inst.get('project', getattr(self, 'project_name', 'circuit'))
313
+ path = inst.get('path', '/')
314
+ reference = inst.get('reference', symbol_data.get('reference', 'U?'))
315
+ unit = inst.get('unit', 1)
316
+
317
+ logger.debug(f" Instance: project={project}, path={path}, ref={reference}, unit={unit}")
318
+
319
+ instances_sexp.append([
320
+ sexpdata.Symbol("project"),
321
+ project,
322
+ [
323
+ sexpdata.Symbol("path"),
324
+ path, # PRESERVE user-set hierarchical path!
325
+ [sexpdata.Symbol("reference"), reference],
326
+ [sexpdata.Symbol("unit"), unit],
327
+ ],
328
+ ])
329
+ sexp.append(instances_sexp)
195
330
  else:
196
- # Fallback: use root_uuid or schematic_uuid for flat designs
197
- root_uuid = (
198
- symbol_data.get("properties", {}).get("root_uuid")
199
- or schematic_uuid
200
- or str(uuid.uuid4())
201
- )
202
- instance_path = f"/{root_uuid}"
331
+ # No user-set instances - generate default (backward compatibility)
332
+ logger.debug(f"🔍 HIERARCHICAL FIX: Component {symbol_data.get('reference')} has NO user instances, generating default")
333
+
334
+ # Get project name from config or properties
335
+ project_name = symbol_data.get("properties", {}).get("project_name")
336
+ if not project_name:
337
+ project_name = getattr(self, "project_name", config.defaults.project_name)
338
+
339
+ # CRITICAL FIX: Use the FULL hierarchy_path from properties if available
340
+ # For hierarchical schematics, this contains the complete path: /root_uuid/sheet_symbol_uuid/...
341
+ # This ensures KiCad can properly annotate components in sub-sheets
342
+ hierarchy_path = symbol_data.get("properties", {}).get("hierarchy_path")
343
+ if hierarchy_path:
344
+ # Use the full hierarchical path (includes root + all sheet symbols)
345
+ instance_path = hierarchy_path
346
+ logger.debug(
347
+ f"🔧 Using FULL hierarchy_path: {instance_path} for component {symbol_data.get('reference', 'unknown')}"
348
+ )
349
+ else:
350
+ # Fallback: use root_uuid or schematic_uuid for flat designs
351
+ root_uuid = (
352
+ symbol_data.get("properties", {}).get("root_uuid")
353
+ or schematic_uuid
354
+ or str(uuid.uuid4())
355
+ )
356
+ instance_path = f"/{root_uuid}"
357
+ logger.debug(
358
+ f"🔧 Using root UUID path: {instance_path} for component {symbol_data.get('reference', 'unknown')}"
359
+ )
360
+
203
361
  logger.debug(
204
- f"🔧 Using root UUID path: {instance_path} for component {symbol_data.get('reference', 'unknown')}"
362
+ f"🔧 Component properties keys: {list(symbol_data.get('properties', {}).keys())}"
205
363
  )
364
+ logger.debug(f"🔧 Using project name: '{project_name}'")
206
365
 
207
- logger.debug(
208
- f"🔧 Component properties keys: {list(symbol_data.get('properties', {}).keys())}"
209
- )
210
- logger.debug(f"🔧 Using project name: '{project_name}'")
211
-
212
- sexp.append(
213
- [
214
- sexpdata.Symbol("instances"),
366
+ sexp.append(
215
367
  [
216
- sexpdata.Symbol("project"),
217
- project_name,
368
+ sexpdata.Symbol("instances"),
218
369
  [
219
- sexpdata.Symbol("path"),
220
- instance_path,
221
- [sexpdata.Symbol("reference"), symbol_data.get("reference", "U?")],
222
- [sexpdata.Symbol("unit"), symbol_data.get("unit", 1)],
370
+ sexpdata.Symbol("project"),
371
+ project_name,
372
+ [
373
+ sexpdata.Symbol("path"),
374
+ instance_path,
375
+ [sexpdata.Symbol("reference"), symbol_data.get("reference", "U?")],
376
+ [sexpdata.Symbol("unit"), symbol_data.get("unit", 1)],
377
+ ],
223
378
  ],
224
- ],
225
- ]
226
- )
379
+ ]
380
+ )
227
381
 
228
382
  return sexp
229
383
 
@@ -236,13 +390,14 @@ class SymbolParser(BaseElementParser):
236
390
  offset_index: int,
237
391
  justify: str = "left",
238
392
  hide: bool = False,
393
+ rotation: float = 0,
239
394
  ) -> List[Any]:
240
395
  """Create a property with proper positioning and effects like KiCAD."""
241
396
  from ...core.config import config
242
397
 
243
398
  # Calculate property position using configuration
244
- prop_x, prop_y, rotation = config.get_property_position(
245
- prop_name, (component_pos.x, component_pos.y), offset_index
399
+ prop_x, prop_y, text_rotation = config.get_property_position(
400
+ prop_name, (component_pos.x, component_pos.y), offset_index, rotation
246
401
  )
247
402
 
248
403
  # Build effects section based on hide status
@@ -266,7 +421,7 @@ class SymbolParser(BaseElementParser):
266
421
  sexpdata.Symbol("at"),
267
422
  round(prop_x, 4) if prop_x != int(prop_x) else int(prop_x),
268
423
  round(prop_y, 4) if prop_y != int(prop_y) else int(prop_y),
269
- rotation,
424
+ text_rotation,
270
425
  ],
271
426
  effects,
272
427
  ]
@@ -275,22 +430,39 @@ class SymbolParser(BaseElementParser):
275
430
 
276
431
 
277
432
  def _create_power_symbol_value_property(
278
- self, value: str, component_pos: Point, lib_id: str
433
+ self, value: str, component_pos: Point, lib_id: str, rotation: float = 0
279
434
  ) -> List[Any]:
280
- """Create Value property for power symbols with correct positioning."""
281
- # Power symbols have different value positioning based on type
282
- if "GND" in lib_id:
283
- # GND value goes below the symbol
284
- prop_x = component_pos.x
285
- prop_y = component_pos.y + 5.08 # Below GND symbol
286
- elif "+3.3V" in lib_id or "VDD" in lib_id:
287
- # Positive voltage values go below the symbol
288
- prop_x = component_pos.x
289
- prop_y = component_pos.y - 5.08 # Above symbol (negative offset)
435
+ """Create Value property for power symbols with correct positioning.
436
+
437
+ Matches circuit-synth power_symbol_positioning.py logic exactly.
438
+ """
439
+ offset = 5.08 # KiCad standard offset
440
+ is_gnd_type = "GND" in lib_id.upper() or "VSS" in lib_id.upper()
441
+
442
+ # Rotation-aware positioning (matching circuit-synth logic)
443
+ if rotation == 0:
444
+ if is_gnd_type:
445
+ prop_x, prop_y = component_pos.x, component_pos.y + offset # GND points down, text below
446
+ else:
447
+ prop_x, prop_y = component_pos.x, component_pos.y - offset # VCC points up, text above
448
+ elif rotation == 90:
449
+ if is_gnd_type:
450
+ prop_x, prop_y = component_pos.x - offset, component_pos.y # GND left, text left
451
+ else:
452
+ prop_x, prop_y = component_pos.x + offset, component_pos.y # VCC right, text right
453
+ elif rotation == 180:
454
+ if is_gnd_type:
455
+ prop_x, prop_y = component_pos.x, component_pos.y - offset # GND inverted up, text above
456
+ else:
457
+ prop_x, prop_y = component_pos.x, component_pos.y + offset # VCC inverted down, text below
458
+ elif rotation == 270:
459
+ if is_gnd_type:
460
+ prop_x, prop_y = component_pos.x + offset, component_pos.y # GND right, text right
461
+ else:
462
+ prop_x, prop_y = component_pos.x - offset, component_pos.y # VCC left, text left
290
463
  else:
291
- # Default power symbol positioning
292
- prop_x = component_pos.x
293
- prop_y = component_pos.y + 3.556
464
+ # Fallback for non-standard rotations
465
+ prop_x, prop_y = component_pos.x, component_pos.y - offset if not is_gnd_type else component_pos.y + offset
294
466
 
295
467
  prop_sexp = [
296
468
  sexpdata.Symbol("property"),