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
@@ -0,0 +1,610 @@
1
+ """
2
+ Python Code Generator for KiCad Schematics.
3
+
4
+ This module converts loaded KiCad schematic objects into executable Python code
5
+ that uses the kicad-sch-api library to recreate the schematic.
6
+ """
7
+
8
+ import re
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import Any, Dict, List, Optional
12
+
13
+ try:
14
+ from jinja2 import Environment, PackageLoader, select_autoescape
15
+ JINJA2_AVAILABLE = True
16
+ except ImportError:
17
+ JINJA2_AVAILABLE = False
18
+
19
+
20
+ class CodeGenerationError(Exception):
21
+ """Error during Python code generation."""
22
+ pass
23
+
24
+
25
+ class TemplateNotFoundError(CodeGenerationError):
26
+ """Template file not found."""
27
+ pass
28
+
29
+
30
+ class PythonCodeGenerator:
31
+ """
32
+ Generate executable Python code from KiCad schematics.
33
+
34
+ This class converts loaded Schematic objects into executable Python
35
+ code that uses the kicad-sch-api to recreate the schematic.
36
+
37
+ Attributes:
38
+ template: Template style ('minimal', 'default', 'verbose', 'documented')
39
+ format_code: Whether to format code with Black
40
+ add_comments: Whether to add explanatory comments
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ template: str = 'default',
46
+ format_code: bool = True,
47
+ add_comments: bool = True
48
+ ):
49
+ """
50
+ Initialize code generator.
51
+
52
+ Args:
53
+ template: Template style to use ('minimal', 'default', 'verbose', 'documented')
54
+ format_code: Format output with Black (if available)
55
+ add_comments: Add explanatory comments
56
+ """
57
+ self.template = template
58
+ self.format_code = format_code
59
+ self.add_comments = add_comments
60
+
61
+ # Initialize Jinja2 environment if available
62
+ if JINJA2_AVAILABLE:
63
+ self.jinja_env = Environment(
64
+ loader=PackageLoader('kicad_sch_api', 'exporters/templates'),
65
+ trim_blocks=True,
66
+ lstrip_blocks=True,
67
+ autoescape=select_autoescape(['html', 'xml'])
68
+ )
69
+ # Register custom filters
70
+ self.jinja_env.filters['sanitize'] = self._sanitize_variable_name
71
+ else:
72
+ self.jinja_env = None
73
+
74
+ def generate(
75
+ self,
76
+ schematic, # Type: Schematic (avoid circular import)
77
+ include_hierarchy: bool = True,
78
+ output_path: Optional[Path] = None
79
+ ) -> str:
80
+ """
81
+ Generate Python code from schematic.
82
+
83
+ Args:
84
+ schematic: Loaded Schematic object
85
+ include_hierarchy: Include hierarchical sheets
86
+ output_path: Optional output file path
87
+
88
+ Returns:
89
+ Generated Python code as string
90
+
91
+ Raises:
92
+ CodeGenerationError: If code generation fails
93
+ TemplateNotFoundError: If template doesn't exist
94
+ """
95
+ # Extract all schematic data
96
+ data = self._extract_schematic_data(schematic, include_hierarchy)
97
+
98
+ # Generate code using template or fallback
99
+ if self.jinja_env and self.template != 'minimal':
100
+ code = self._generate_with_template(data)
101
+ else:
102
+ # Use simple string-based generation for minimal or if Jinja2 unavailable
103
+ code = self._generate_minimal(data)
104
+
105
+ # Format code
106
+ if self.format_code:
107
+ code = self._format_with_black(code)
108
+
109
+ # Validate syntax
110
+ self._validate_syntax(code)
111
+
112
+ # Write to file if path provided
113
+ if output_path:
114
+ output_path = Path(output_path)
115
+ output_path.write_text(code, encoding='utf-8')
116
+ # Make executable on Unix-like systems
117
+ try:
118
+ output_path.chmod(0o755)
119
+ except Exception:
120
+ pass # Windows doesn't support chmod
121
+
122
+ return code
123
+
124
+ def _extract_schematic_data(
125
+ self,
126
+ schematic,
127
+ include_hierarchy: bool
128
+ ) -> Dict[str, Any]:
129
+ """
130
+ Extract all data from schematic for code generation.
131
+
132
+ Args:
133
+ schematic: Schematic to extract from
134
+ include_hierarchy: Include hierarchical sheets
135
+
136
+ Returns:
137
+ Dictionary with all template data
138
+ """
139
+ return {
140
+ 'metadata': self._extract_metadata(schematic),
141
+ 'components': self._extract_components(schematic),
142
+ 'wires': self._extract_wires(schematic),
143
+ 'labels': self._extract_labels(schematic),
144
+ 'sheets': self._extract_sheets(schematic) if include_hierarchy else [],
145
+ 'options': {
146
+ 'add_comments': self.add_comments,
147
+ 'include_hierarchy': include_hierarchy
148
+ }
149
+ }
150
+
151
+ def _extract_metadata(self, schematic) -> Dict[str, Any]:
152
+ """Extract schematic metadata."""
153
+ import kicad_sch_api
154
+
155
+ # Get project name from schematic
156
+ name = getattr(schematic, 'name', None) or 'untitled'
157
+
158
+ # Get title from title block if available
159
+ title = ''
160
+ if hasattr(schematic, 'title_block') and schematic.title_block:
161
+ title = getattr(schematic.title_block, 'title', '')
162
+
163
+ if not title:
164
+ title = name
165
+
166
+ return {
167
+ 'name': name,
168
+ 'title': title,
169
+ 'version': kicad_sch_api.__version__,
170
+ 'date': datetime.now().isoformat(),
171
+ 'source_file': str(schematic.filepath) if hasattr(schematic, 'filepath') and schematic.filepath else 'unknown'
172
+ }
173
+
174
+ def _extract_components(self, schematic) -> List[Dict[str, Any]]:
175
+ """
176
+ Extract component data.
177
+
178
+ Returns:
179
+ List of component dictionaries
180
+ """
181
+ components = []
182
+
183
+ # Access components collection
184
+ comp_collection = schematic.components if hasattr(schematic, 'components') else []
185
+
186
+ for comp in comp_collection:
187
+ # Extract component properties
188
+ ref = getattr(comp, 'reference', '') or getattr(comp, 'ref', '')
189
+ lib_id = getattr(comp, 'lib_id', '') or getattr(comp, 'symbol', '')
190
+ value = getattr(comp, 'value', '')
191
+ footprint = getattr(comp, 'footprint', '')
192
+
193
+ # Get position
194
+ pos = getattr(comp, 'position', None)
195
+ if pos:
196
+ x = getattr(pos, 'x', 0.0)
197
+ y = getattr(pos, 'y', 0.0)
198
+ else:
199
+ x, y = 0.0, 0.0
200
+
201
+ # Get rotation
202
+ rotation = getattr(comp, 'rotation', 0)
203
+
204
+ comp_data = {
205
+ 'ref': ref,
206
+ 'variable': self._sanitize_variable_name(ref),
207
+ 'lib_id': lib_id,
208
+ 'value': value,
209
+ 'footprint': footprint,
210
+ 'x': x,
211
+ 'y': y,
212
+ 'rotation': rotation,
213
+ 'properties': self._extract_custom_properties(comp)
214
+ }
215
+ components.append(comp_data)
216
+
217
+ return components
218
+
219
+ def _extract_wires(self, schematic) -> List[Dict[str, Any]]:
220
+ """
221
+ Extract wire data.
222
+
223
+ Returns:
224
+ List of wire dictionaries
225
+ """
226
+ wires = []
227
+
228
+ # Access wires collection
229
+ wire_collection = schematic.wires if hasattr(schematic, 'wires') else []
230
+
231
+ for wire in wire_collection:
232
+ # Get start and end points
233
+ start = getattr(wire, 'start', None)
234
+ end = getattr(wire, 'end', None)
235
+
236
+ if start and end:
237
+ wire_data = {
238
+ 'start_x': getattr(start, 'x', 0.0),
239
+ 'start_y': getattr(start, 'y', 0.0),
240
+ 'end_x': getattr(end, 'x', 0.0),
241
+ 'end_y': getattr(end, 'y', 0.0),
242
+ 'style': getattr(wire, 'style', 'solid')
243
+ }
244
+ wires.append(wire_data)
245
+
246
+ return wires
247
+
248
+ def _extract_labels(self, schematic) -> List[Dict[str, Any]]:
249
+ """
250
+ Extract label data.
251
+
252
+ Returns:
253
+ List of label dictionaries
254
+ """
255
+ labels = []
256
+
257
+ # Access labels collection
258
+ label_collection = schematic.labels if hasattr(schematic, 'labels') else []
259
+
260
+ for label in label_collection:
261
+ # Get label properties
262
+ text = getattr(label, 'text', '')
263
+ pos = getattr(label, 'position', None)
264
+
265
+ if pos:
266
+ x = getattr(pos, 'x', 0.0)
267
+ y = getattr(pos, 'y', 0.0)
268
+ else:
269
+ x, y = 0.0, 0.0
270
+
271
+ label_type = getattr(label, 'label_type', 'local')
272
+ rotation = getattr(label, 'rotation', 0)
273
+
274
+ label_data = {
275
+ 'text': text,
276
+ 'x': x,
277
+ 'y': y,
278
+ 'type': label_type,
279
+ 'rotation': rotation
280
+ }
281
+ labels.append(label_data)
282
+
283
+ return labels
284
+
285
+ def _extract_sheets(self, schematic) -> List[Dict[str, Any]]:
286
+ """
287
+ Extract hierarchical sheet data.
288
+
289
+ Returns:
290
+ List of sheet dictionaries
291
+ """
292
+ sheets = []
293
+
294
+ # Access sheets collection if available
295
+ if not hasattr(schematic, 'sheets'):
296
+ return sheets
297
+
298
+ # SheetManager doesn't support direct iteration
299
+ # For now, return empty list - hierarchical support is Phase 3
300
+ # TODO: Implement when SheetManager has proper iteration support
301
+ return sheets
302
+
303
+ # The code below is for future when SheetManager is iterable:
304
+ for sheet in getattr(schematic.sheets, 'data', []):
305
+ # Get sheet properties
306
+ name = getattr(sheet, 'name', '')
307
+ filename = getattr(sheet, 'filename', '')
308
+
309
+ pos = getattr(sheet, 'position', None)
310
+ size = getattr(sheet, 'size', None)
311
+
312
+ if pos:
313
+ x = getattr(pos, 'x', 0.0)
314
+ y = getattr(pos, 'y', 0.0)
315
+ else:
316
+ x, y = 0.0, 0.0
317
+
318
+ if size:
319
+ width = getattr(size, 'width', 100.0)
320
+ height = getattr(size, 'height', 100.0)
321
+ else:
322
+ width, height = 100.0, 100.0
323
+
324
+ # Extract pins
325
+ pins = []
326
+ if hasattr(sheet, 'pins'):
327
+ for pin in sheet.pins:
328
+ pin_pos = getattr(pin, 'position', None)
329
+ pins.append({
330
+ 'name': getattr(pin, 'name', ''),
331
+ 'type': getattr(pin, 'pin_type', 'input'),
332
+ 'x': getattr(pin_pos, 'x', 0.0) if pin_pos else 0.0,
333
+ 'y': getattr(pin_pos, 'y', 0.0) if pin_pos else 0.0
334
+ })
335
+
336
+ sheet_data = {
337
+ 'name': name,
338
+ 'filename': filename,
339
+ 'x': x,
340
+ 'y': y,
341
+ 'width': width,
342
+ 'height': height,
343
+ 'pins': pins
344
+ }
345
+ sheets.append(sheet_data)
346
+
347
+ return sheets
348
+
349
+ def _extract_custom_properties(self, component) -> List[Dict[str, str]]:
350
+ """
351
+ Extract custom component properties.
352
+
353
+ Returns:
354
+ List of property dictionaries
355
+ """
356
+ # Standard properties to exclude
357
+ standard_props = {
358
+ 'Reference', 'Value', 'Footprint', 'Datasheet',
359
+ 'ki_keywords', 'ki_description', 'ki_fp_filters',
360
+ 'Description' # Also exclude Description as it's often auto-generated
361
+ }
362
+
363
+ properties = []
364
+
365
+ # Get properties if available
366
+ if hasattr(component, 'properties'):
367
+ comp_props = component.properties
368
+ if isinstance(comp_props, dict):
369
+ for prop_name, prop_value in comp_props.items():
370
+ # Skip internal properties (start with __)
371
+ if prop_name.startswith('__'):
372
+ continue
373
+ # Skip standard properties
374
+ if prop_name in standard_props:
375
+ continue
376
+ # Skip if value contains non-serializable content
377
+ str_value = str(prop_value)
378
+ if 'Symbol(' in str_value or '[Symbol(' in str_value:
379
+ continue
380
+
381
+ properties.append({
382
+ 'name': prop_name,
383
+ 'value': str_value
384
+ })
385
+
386
+ return properties
387
+
388
+ @staticmethod
389
+ def _sanitize_variable_name(name: str) -> str:
390
+ """
391
+ Convert reference/name to valid Python variable name.
392
+
393
+ Args:
394
+ name: Original name (R1, 3V3, etc.)
395
+
396
+ Returns:
397
+ Sanitized variable name (r1, _3v3, etc.)
398
+
399
+ Examples:
400
+ >>> PythonCodeGenerator._sanitize_variable_name('R1')
401
+ 'r1'
402
+ >>> PythonCodeGenerator._sanitize_variable_name('3V3')
403
+ '_3v3'
404
+ >>> PythonCodeGenerator._sanitize_variable_name('U$1')
405
+ 'u_1'
406
+ """
407
+ import keyword
408
+
409
+ # Handle special power net cases
410
+ power_nets = {
411
+ '3V3': '_3v3',
412
+ '3.3V': '_3v3',
413
+ '+3V3': '_3v3',
414
+ '+3.3V': '_3v3',
415
+ '5V': '_5v',
416
+ '+5V': '_5v',
417
+ '12V': '_12v',
418
+ '+12V': '_12v',
419
+ 'VCC': 'vcc',
420
+ 'VDD': 'vdd',
421
+ 'GND': 'gnd',
422
+ 'VSS': 'vss'
423
+ }
424
+
425
+ if name in power_nets:
426
+ return power_nets[name]
427
+
428
+ # Convert to lowercase
429
+ var_name = name.lower()
430
+
431
+ # Replace invalid characters
432
+ var_name = var_name.replace('$', '_')
433
+ var_name = var_name.replace('+', 'p')
434
+ var_name = var_name.replace('-', 'n')
435
+ var_name = var_name.replace('.', '_')
436
+ var_name = re.sub(r'[^a-z0-9_]', '_', var_name)
437
+
438
+ # Remove consecutive underscores
439
+ var_name = re.sub(r'_+', '_', var_name)
440
+
441
+ # Strip leading/trailing underscores
442
+ var_name = var_name.strip('_')
443
+
444
+ # Prefix if starts with digit or is empty
445
+ if not var_name or var_name[0].isdigit():
446
+ var_name = '_' + var_name
447
+
448
+ # Ensure not a Python keyword
449
+ if keyword.iskeyword(var_name):
450
+ var_name = var_name + '_'
451
+
452
+ return var_name
453
+
454
+ def _generate_with_template(self, data: Dict[str, Any]) -> str:
455
+ """
456
+ Generate code using Jinja2 template.
457
+
458
+ Args:
459
+ data: Template data
460
+
461
+ Returns:
462
+ Generated code
463
+
464
+ Raises:
465
+ TemplateNotFoundError: If template not found
466
+ """
467
+ try:
468
+ template = self.jinja_env.get_template(f'{self.template}.py.jinja2')
469
+ code = template.render(**data)
470
+ return code
471
+ except Exception as e:
472
+ raise TemplateNotFoundError(
473
+ f"Template '{self.template}' not found or invalid: {e}"
474
+ ) from e
475
+
476
+ def _generate_minimal(self, data: Dict[str, Any]) -> str:
477
+ """
478
+ Generate minimal Python code without templates.
479
+
480
+ This is a fallback when Jinja2 is not available or for minimal template.
481
+
482
+ Args:
483
+ data: Extracted schematic data
484
+
485
+ Returns:
486
+ Generated Python code
487
+ """
488
+ lines = []
489
+
490
+ # Header
491
+ lines.append("#!/usr/bin/env python3")
492
+ lines.append('"""')
493
+ lines.append(f"{data['metadata']['title']}")
494
+ lines.append("")
495
+ lines.append(f"Generated from: {data['metadata']['source_file']}")
496
+ lines.append(f"Generated by: kicad-sch-api v{data['metadata']['version']}")
497
+ lines.append('"""')
498
+ lines.append("")
499
+ lines.append("import kicad_sch_api as ksa")
500
+ lines.append("")
501
+
502
+ # Function definition
503
+ func_name = self._sanitize_variable_name(data['metadata']['name'])
504
+ lines.append(f"def create_{func_name}():")
505
+ lines.append(f' """Create {data["metadata"]["title"]} schematic."""')
506
+ lines.append("")
507
+
508
+ # Create schematic
509
+ lines.append(" # Create schematic")
510
+ lines.append(f" sch = ksa.create_schematic('{data['metadata']['name']}')")
511
+ lines.append("")
512
+
513
+ # Add components
514
+ if data['components']:
515
+ lines.append(" # Add components")
516
+ for comp in data['components']:
517
+ lines.append(f" {comp['variable']} = sch.components.add(")
518
+ lines.append(f" '{comp['lib_id']}',")
519
+ lines.append(f" reference='{comp['ref']}',")
520
+ lines.append(f" value='{comp['value']}',")
521
+ lines.append(f" position=({comp['x']}, {comp['y']})")
522
+ if comp['rotation'] != 0:
523
+ lines.append(f" rotation={comp['rotation']}")
524
+ lines.append(" )")
525
+ if comp['footprint']:
526
+ lines.append(f" {comp['variable']}.footprint = '{comp['footprint']}'")
527
+ for prop in comp['properties']:
528
+ lines.append(f" {comp['variable']}.set_property('{prop['name']}', '{prop['value']}')")
529
+ lines.append("")
530
+
531
+ # Add wires
532
+ if data['wires']:
533
+ lines.append(" # Add wires")
534
+ for wire in data['wires']:
535
+ lines.append(f" sch.add_wire(")
536
+ lines.append(f" start=({wire['start_x']}, {wire['start_y']}),")
537
+ lines.append(f" end=({wire['end_x']}, {wire['end_y']})")
538
+ lines.append(" )")
539
+ lines.append("")
540
+
541
+ # Add labels
542
+ if data['labels']:
543
+ lines.append(" # Add labels")
544
+ for label in data['labels']:
545
+ lines.append(f" sch.add_label(")
546
+ lines.append(f" '{label['text']}',")
547
+ lines.append(f" position=({label['x']}, {label['y']})")
548
+ lines.append(" )")
549
+ lines.append("")
550
+
551
+ # Return
552
+ lines.append(" return sch")
553
+ lines.append("")
554
+ lines.append("")
555
+
556
+ # Main block
557
+ lines.append("if __name__ == '__main__':")
558
+ lines.append(f" schematic = create_{func_name}()")
559
+ lines.append(f" schematic.save('{data['metadata']['name']}.kicad_sch')")
560
+ lines.append(f" print('✅ Schematic generated: {data['metadata']['name']}.kicad_sch')")
561
+ lines.append("")
562
+
563
+ return '\n'.join(lines)
564
+
565
+ def _format_with_black(self, code: str) -> str:
566
+ """
567
+ Format code using Black formatter.
568
+
569
+ Args:
570
+ code: Unformatted Python code
571
+
572
+ Returns:
573
+ Formatted code (or original if Black unavailable)
574
+ """
575
+ try:
576
+ import black
577
+
578
+ mode = black.Mode(
579
+ target_versions={black.TargetVersion.PY38},
580
+ line_length=88,
581
+ string_normalization=True
582
+ )
583
+
584
+ formatted = black.format_str(code, mode=mode)
585
+ return formatted
586
+
587
+ except ImportError:
588
+ # Black not available, return unformatted
589
+ return code
590
+
591
+ except Exception:
592
+ # Black failed, return unformatted
593
+ return code
594
+
595
+ def _validate_syntax(self, code: str) -> None:
596
+ """
597
+ Validate generated code syntax.
598
+
599
+ Args:
600
+ code: Generated Python code
601
+
602
+ Raises:
603
+ CodeGenerationError: If code has syntax errors
604
+ """
605
+ try:
606
+ compile(code, '<generated>', 'exec')
607
+ except SyntaxError as e:
608
+ raise CodeGenerationError(
609
+ f"Generated code has syntax error at line {e.lineno}: {e.msg}"
610
+ ) from e
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ {{ metadata.title }}
4
+
5
+ Generated from: {{ metadata.source_file }}
6
+ Generated by: kicad-sch-api v{{ metadata.version }}
7
+ Date: {{ metadata.date }}
8
+ """
9
+
10
+ import kicad_sch_api as ksa
11
+
12
+
13
+ def create_{{ metadata.name|sanitize }}():
14
+ """Create {{ metadata.title }} schematic."""
15
+
16
+ # Create schematic
17
+ sch = ksa.create_schematic('{{ metadata.name }}')
18
+
19
+ {% if components %}
20
+ # Add components
21
+ {% for comp in components %}
22
+ {{ comp.variable }} = sch.components.add(
23
+ '{{ comp.lib_id }}',
24
+ reference='{{ comp.ref }}',
25
+ value='{{ comp.value }}',
26
+ position=({{ comp.x }}, {{ comp.y }}){% if comp.rotation != 0 %},
27
+ rotation={{ comp.rotation }}{% endif %}
28
+
29
+ )
30
+ {%- if comp.footprint %}
31
+ {{ comp.variable }}.footprint = '{{ comp.footprint }}'
32
+ {%- endif %}
33
+ {%- for prop in comp.properties %}
34
+ {{ comp.variable }}.set_property('{{ prop.name }}', '{{ prop.value }}')
35
+ {%- endfor %}
36
+
37
+ {% endfor %}
38
+ {% endif %}
39
+ {%- if wires %}
40
+ # Add wires
41
+ {% for wire in wires %}
42
+ sch.add_wire(
43
+ start=({{ wire.start_x }}, {{ wire.start_y }}),
44
+ end=({{ wire.end_x }}, {{ wire.end_y }})
45
+ )
46
+ {% endfor %}
47
+
48
+ {% endif %}
49
+ {%- if labels %}
50
+ # Add labels
51
+ {% for label in labels %}
52
+ sch.add_label(
53
+ '{{ label.text }}',
54
+ position=({{ label.x }}, {{ label.y }})
55
+ )
56
+ {% endfor %}
57
+
58
+ {% endif %}
59
+ return sch
60
+
61
+
62
+ if __name__ == '__main__':
63
+ schematic = create_{{ metadata.name|sanitize }}()
64
+ schematic.save('{{ metadata.name }}.kicad_sch')
65
+ print('✅ Schematic generated: {{ metadata.name }}.kicad_sch')