kicad-sch-api 0.3.0__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 (112) hide show
  1. kicad_sch_api/__init__.py +68 -3
  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/kicad_to_python.py +169 -0
  8. kicad_sch_api/cli/netlist.py +94 -0
  9. kicad_sch_api/cli/types.py +43 -0
  10. kicad_sch_api/collections/__init__.py +36 -0
  11. kicad_sch_api/collections/base.py +604 -0
  12. kicad_sch_api/collections/components.py +1623 -0
  13. kicad_sch_api/collections/junctions.py +206 -0
  14. kicad_sch_api/collections/labels.py +508 -0
  15. kicad_sch_api/collections/wires.py +292 -0
  16. kicad_sch_api/core/__init__.py +37 -2
  17. kicad_sch_api/core/collections/__init__.py +5 -0
  18. kicad_sch_api/core/collections/base.py +248 -0
  19. kicad_sch_api/core/component_bounds.py +34 -7
  20. kicad_sch_api/core/components.py +213 -52
  21. kicad_sch_api/core/config.py +110 -15
  22. kicad_sch_api/core/connectivity.py +692 -0
  23. kicad_sch_api/core/exceptions.py +175 -0
  24. kicad_sch_api/core/factories/__init__.py +5 -0
  25. kicad_sch_api/core/factories/element_factory.py +278 -0
  26. kicad_sch_api/core/formatter.py +60 -9
  27. kicad_sch_api/core/geometry.py +94 -5
  28. kicad_sch_api/core/junctions.py +26 -75
  29. kicad_sch_api/core/labels.py +324 -0
  30. kicad_sch_api/core/managers/__init__.py +30 -0
  31. kicad_sch_api/core/managers/base.py +76 -0
  32. kicad_sch_api/core/managers/file_io.py +246 -0
  33. kicad_sch_api/core/managers/format_sync.py +502 -0
  34. kicad_sch_api/core/managers/graphics.py +580 -0
  35. kicad_sch_api/core/managers/hierarchy.py +661 -0
  36. kicad_sch_api/core/managers/metadata.py +271 -0
  37. kicad_sch_api/core/managers/sheet.py +492 -0
  38. kicad_sch_api/core/managers/text_elements.py +537 -0
  39. kicad_sch_api/core/managers/validation.py +476 -0
  40. kicad_sch_api/core/managers/wire.py +410 -0
  41. kicad_sch_api/core/nets.py +305 -0
  42. kicad_sch_api/core/no_connects.py +252 -0
  43. kicad_sch_api/core/parser.py +194 -970
  44. kicad_sch_api/core/parsing_utils.py +63 -0
  45. kicad_sch_api/core/pin_utils.py +103 -9
  46. kicad_sch_api/core/schematic.py +1328 -1079
  47. kicad_sch_api/core/texts.py +316 -0
  48. kicad_sch_api/core/types.py +159 -23
  49. kicad_sch_api/core/wires.py +27 -75
  50. kicad_sch_api/exporters/__init__.py +10 -0
  51. kicad_sch_api/exporters/python_generator.py +610 -0
  52. kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
  53. kicad_sch_api/geometry/__init__.py +38 -0
  54. kicad_sch_api/geometry/font_metrics.py +22 -0
  55. kicad_sch_api/geometry/routing.py +211 -0
  56. kicad_sch_api/geometry/symbol_bbox.py +608 -0
  57. kicad_sch_api/interfaces/__init__.py +17 -0
  58. kicad_sch_api/interfaces/parser.py +76 -0
  59. kicad_sch_api/interfaces/repository.py +70 -0
  60. kicad_sch_api/interfaces/resolver.py +117 -0
  61. kicad_sch_api/parsers/__init__.py +14 -0
  62. kicad_sch_api/parsers/base.py +145 -0
  63. kicad_sch_api/parsers/elements/__init__.py +22 -0
  64. kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
  65. kicad_sch_api/parsers/elements/label_parser.py +216 -0
  66. kicad_sch_api/parsers/elements/library_parser.py +165 -0
  67. kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
  68. kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
  69. kicad_sch_api/parsers/elements/symbol_parser.py +485 -0
  70. kicad_sch_api/parsers/elements/text_parser.py +250 -0
  71. kicad_sch_api/parsers/elements/wire_parser.py +242 -0
  72. kicad_sch_api/parsers/registry.py +155 -0
  73. kicad_sch_api/parsers/utils.py +80 -0
  74. kicad_sch_api/symbols/__init__.py +18 -0
  75. kicad_sch_api/symbols/cache.py +467 -0
  76. kicad_sch_api/symbols/resolver.py +361 -0
  77. kicad_sch_api/symbols/validators.py +504 -0
  78. kicad_sch_api/utils/logging.py +555 -0
  79. kicad_sch_api/utils/logging_decorators.py +587 -0
  80. kicad_sch_api/utils/validation.py +16 -22
  81. kicad_sch_api/validation/__init__.py +25 -0
  82. kicad_sch_api/validation/erc.py +171 -0
  83. kicad_sch_api/validation/erc_models.py +203 -0
  84. kicad_sch_api/validation/pin_matrix.py +243 -0
  85. kicad_sch_api/validation/validators.py +391 -0
  86. kicad_sch_api/wrappers/__init__.py +14 -0
  87. kicad_sch_api/wrappers/base.py +89 -0
  88. kicad_sch_api/wrappers/wire.py +198 -0
  89. kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
  90. kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
  91. kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
  92. {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
  93. mcp_server/__init__.py +34 -0
  94. mcp_server/example_logging_integration.py +506 -0
  95. mcp_server/models.py +252 -0
  96. mcp_server/server.py +357 -0
  97. mcp_server/tools/__init__.py +32 -0
  98. mcp_server/tools/component_tools.py +516 -0
  99. mcp_server/tools/connectivity_tools.py +532 -0
  100. mcp_server/tools/consolidated_tools.py +1216 -0
  101. mcp_server/tools/pin_discovery.py +333 -0
  102. mcp_server/utils/__init__.py +38 -0
  103. mcp_server/utils/logging.py +127 -0
  104. mcp_server/utils.py +36 -0
  105. kicad_sch_api/core/manhattan_routing.py +0 -430
  106. kicad_sch_api/core/simple_manhattan.py +0 -228
  107. kicad_sch_api/core/wire_routing.py +0 -380
  108. kicad_sch_api-0.3.0.dist-info/METADATA +0 -483
  109. kicad_sch_api-0.3.0.dist-info/RECORD +0 -31
  110. kicad_sch_api-0.3.0.dist-info/entry_points.txt +0 -2
  111. {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
  112. {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
kicad_sch_api/__init__.py CHANGED
@@ -42,20 +42,27 @@ Advanced Usage:
42
42
  print(f"Found {len(issues)} validation issues")
43
43
  """
44
44
 
45
- __version__ = "0.3.0"
45
+ __version__ = "0.5.0"
46
46
  __author__ = "Circuit-Synth"
47
47
  __email__ = "info@circuit-synth.com"
48
48
 
49
49
  from .core.components import Component, ComponentCollection
50
50
  from .core.config import KiCADConfig, config
51
+ from .core.types import PinInfo
51
52
 
52
53
  # Core imports for public API
53
54
  from .core.schematic import Schematic
54
55
  from .library.cache import SymbolLibraryCache, get_symbol_cache
55
56
  from .utils.validation import ValidationError, ValidationIssue
57
+ # Commonly-used exceptions (ValidationError re-exported from utils for backward compat)
58
+ from .core.exceptions import (
59
+ KiCadSchError,
60
+ ElementNotFoundError,
61
+ DuplicateElementError,
62
+ )
56
63
 
57
64
  # Version info
58
- VERSION_INFO = (0, 3, 0)
65
+ VERSION_INFO = (0, 4, 0)
59
66
 
60
67
  # Public API
61
68
  __all__ = [
@@ -63,14 +70,18 @@ __all__ = [
63
70
  "Schematic",
64
71
  "Component",
65
72
  "ComponentCollection",
73
+ "PinInfo",
66
74
  "SymbolLibraryCache",
67
75
  "get_symbol_cache",
68
76
  # Configuration
69
77
  "KiCADConfig",
70
78
  "config",
71
79
  # Exceptions
80
+ "KiCadSchError",
72
81
  "ValidationError",
73
82
  "ValidationIssue",
83
+ "ElementNotFoundError",
84
+ "DuplicateElementError",
74
85
  # Version info
75
86
  "__version__",
76
87
  "VERSION_INFO",
@@ -112,5 +123,59 @@ def create_schematic(name: str = "Untitled") -> "Schematic":
112
123
  return Schematic.create(name)
113
124
 
114
125
 
126
+ def schematic_to_python(
127
+ input_path: str,
128
+ output_path: str,
129
+ template: str = 'default',
130
+ include_hierarchy: bool = True,
131
+ format_code: bool = True,
132
+ add_comments: bool = True
133
+ ):
134
+ """
135
+ Convert KiCad schematic to Python code (one-line convenience function).
136
+
137
+ Loads a KiCad schematic and generates executable Python code that
138
+ recreates it using the kicad-sch-api library.
139
+
140
+ Args:
141
+ input_path: Input .kicad_sch file
142
+ output_path: Output .py file
143
+ template: Code template style ('minimal', 'default', 'verbose', 'documented')
144
+ include_hierarchy: Include hierarchical sheets
145
+ format_code: Format code with Black
146
+ add_comments: Add explanatory comments
147
+
148
+ Returns:
149
+ Path to generated Python file
150
+
151
+ Raises:
152
+ FileNotFoundError: If input file doesn't exist
153
+ CodeGenerationError: If code generation fails
154
+
155
+ Example:
156
+ >>> import kicad_sch_api as ksa
157
+ >>> ksa.schematic_to_python('input.kicad_sch', 'output.py')
158
+ PosixPath('output.py')
159
+
160
+ >>> ksa.schematic_to_python('input.kicad_sch', 'output.py',
161
+ ... template='minimal',
162
+ ... add_comments=False)
163
+ PosixPath('output.py')
164
+ """
165
+ from pathlib import Path
166
+
167
+ # Load schematic
168
+ schematic = Schematic.load(input_path)
169
+
170
+ # Export to Python
171
+ return schematic.export_to_python(
172
+ output_path=output_path,
173
+ template=template,
174
+ include_hierarchy=include_hierarchy,
175
+ format_code=format_code,
176
+ add_comments=add_comments
177
+ )
178
+
179
+
115
180
  # Add convenience functions to __all__
116
- __all__.extend(["load_schematic", "create_schematic"])
181
+ __all__.extend(["load_schematic", "create_schematic", "schematic_to_python"])
@@ -0,0 +1,45 @@
1
+ """
2
+ KiCad CLI wrappers for schematic operations.
3
+
4
+ This module provides Python wrappers around kicad-cli commands with automatic
5
+ fallback to Docker when kicad-cli is not installed locally.
6
+
7
+ Supported operations:
8
+ - ERC (Electrical Rule Check)
9
+ - Netlist export (8 formats)
10
+ - BOM (Bill of Materials) export
11
+ - PDF export
12
+ - SVG export
13
+ - DXF export
14
+
15
+ Example:
16
+ >>> import kicad_sch_api as ksa
17
+ >>> sch = ksa.Schematic('circuit.kicad_sch')
18
+ >>> sch.run_erc()
19
+ >>> sch.export_netlist(format='spice')
20
+ >>> sch.export_bom(exclude_dnp=True)
21
+ """
22
+
23
+ from kicad_sch_api.cli.base import (
24
+ ExecutionMode,
25
+ KiCadExecutor,
26
+ get_executor_info,
27
+ set_execution_mode,
28
+ )
29
+ from kicad_sch_api.cli.types import (
30
+ ErcFormat,
31
+ ErcSeverity,
32
+ NetlistFormat,
33
+ Units,
34
+ )
35
+
36
+ __all__ = [
37
+ "KiCadExecutor",
38
+ "ExecutionMode",
39
+ "get_executor_info",
40
+ "set_execution_mode",
41
+ "NetlistFormat",
42
+ "ErcFormat",
43
+ "ErcSeverity",
44
+ "Units",
45
+ ]
@@ -0,0 +1,302 @@
1
+ """
2
+ Base KiCad CLI executor with Docker fallback support.
3
+
4
+ This module provides the core infrastructure for executing kicad-cli commands
5
+ either locally or via Docker containers.
6
+ """
7
+
8
+ import os
9
+ import shutil
10
+ import subprocess
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ from typing import Any, Dict, List, Optional
14
+
15
+ from kicad_sch_api.cli.types import ExecutionMode
16
+
17
+
18
+ @dataclass
19
+ class ExecutorInfo:
20
+ """Information about available execution modes."""
21
+ local_available: bool
22
+ local_version: Optional[str]
23
+ docker_available: bool
24
+ docker_image: str
25
+ active_mode: ExecutionMode
26
+
27
+
28
+ class KiCadExecutor:
29
+ """
30
+ Core executor for KiCad CLI commands with Docker fallback.
31
+
32
+ Execution strategy:
33
+ 1. AUTO mode (default): Try local kicad-cli, fall back to Docker
34
+ 2. LOCAL mode: Force local kicad-cli (fail if not available)
35
+ 3. DOCKER mode: Force Docker (fail if Docker not available)
36
+
37
+ Environment variables:
38
+ - KICAD_CLI_MODE: Set execution mode (auto|local|docker)
39
+ - KICAD_DOCKER_IMAGE: Override Docker image (default: kicad/kicad:latest)
40
+
41
+ Example:
42
+ >>> executor = KiCadExecutor()
43
+ >>> result = executor.run(['sch', 'export', 'netlist', 'circuit.kicad_sch'])
44
+ """
45
+
46
+ # Class-level cache for detection results
47
+ _local_available: Optional[bool] = None
48
+ _local_version: Optional[str] = None
49
+ _docker_available: Optional[bool] = None
50
+
51
+ def __init__(
52
+ self,
53
+ mode: ExecutionMode = "auto",
54
+ docker_image: str = "kicad/kicad:latest",
55
+ verbose: bool = False,
56
+ ):
57
+ """
58
+ Initialize KiCad executor.
59
+
60
+ Args:
61
+ mode: Execution mode (auto, local, or docker)
62
+ docker_image: Docker image to use
63
+ verbose: Print execution details
64
+ """
65
+ # Check environment variable override
66
+ env_mode = os.getenv("KICAD_CLI_MODE", "").lower()
67
+ if env_mode in ("auto", "local", "docker"):
68
+ mode = env_mode # type: ignore
69
+
70
+ env_image = os.getenv("KICAD_DOCKER_IMAGE", "")
71
+ if env_image:
72
+ docker_image = env_image
73
+
74
+ self.mode = mode
75
+ self.docker_image = docker_image
76
+ self.verbose = verbose
77
+
78
+ # Detect capabilities on first use
79
+ if KiCadExecutor._local_available is None:
80
+ KiCadExecutor._detect_local()
81
+ if KiCadExecutor._docker_available is None:
82
+ KiCadExecutor._detect_docker()
83
+
84
+ @classmethod
85
+ def _detect_local(cls) -> None:
86
+ """Detect if kicad-cli is available locally."""
87
+ try:
88
+ result = subprocess.run(
89
+ ["kicad-cli", "version"],
90
+ capture_output=True,
91
+ text=True,
92
+ timeout=5,
93
+ )
94
+ if result.returncode == 0:
95
+ cls._local_available = True
96
+ cls._local_version = result.stdout.strip()
97
+ else:
98
+ cls._local_available = False
99
+ cls._local_version = None
100
+ except (FileNotFoundError, subprocess.TimeoutExpired):
101
+ cls._local_available = False
102
+ cls._local_version = None
103
+
104
+ @classmethod
105
+ def _detect_docker(cls) -> None:
106
+ """Detect if Docker is available."""
107
+ try:
108
+ result = subprocess.run(
109
+ ["docker", "version"],
110
+ capture_output=True,
111
+ timeout=5,
112
+ )
113
+ cls._docker_available = result.returncode == 0
114
+ except (FileNotFoundError, subprocess.TimeoutExpired):
115
+ cls._docker_available = False
116
+
117
+ def _run_local(self, args: List[str], cwd: Optional[Path] = None) -> subprocess.CompletedProcess:
118
+ """Run kicad-cli locally."""
119
+ if not KiCadExecutor._local_available:
120
+ raise RuntimeError(
121
+ "kicad-cli not found. Install KiCad or use Docker mode.\n"
122
+ "Install: https://www.kicad.org/download/\n"
123
+ "Docker: export KICAD_CLI_MODE=docker"
124
+ )
125
+
126
+ cmd = ["kicad-cli"] + args
127
+ if self.verbose:
128
+ print(f"Running locally: {' '.join(cmd)}")
129
+
130
+ result = subprocess.run(
131
+ cmd,
132
+ capture_output=True,
133
+ text=True,
134
+ cwd=cwd,
135
+ )
136
+
137
+ return result
138
+
139
+ def _run_docker(self, args: List[str], cwd: Optional[Path] = None) -> subprocess.CompletedProcess:
140
+ """Run kicad-cli via Docker."""
141
+ if not KiCadExecutor._docker_available:
142
+ raise RuntimeError(
143
+ "Docker not found. Install Docker or use local mode.\n"
144
+ "Install: https://docs.docker.com/get-docker/\n"
145
+ "Local: Install KiCad from https://www.kicad.org/download/"
146
+ )
147
+
148
+ # Determine working directory
149
+ if cwd:
150
+ work_dir = cwd.resolve()
151
+ else:
152
+ work_dir = Path.cwd()
153
+
154
+ # Build Docker command
155
+ docker_cmd = [
156
+ "docker", "run",
157
+ "--rm", # Remove container after execution
158
+ "-v", f"{work_dir}:/workspace", # Mount working directory
159
+ "-w", "/workspace", # Set working directory
160
+ ]
161
+
162
+ # Add user mapping on Linux/Mac to avoid permission issues
163
+ if os.name != "nt": # Not Windows
164
+ uid = os.getuid()
165
+ gid = os.getgid()
166
+ docker_cmd.extend(["--user", f"{uid}:{gid}"])
167
+
168
+ docker_cmd.extend([
169
+ self.docker_image,
170
+ "kicad-cli",
171
+ ] + args)
172
+
173
+ if self.verbose:
174
+ print(f"Running via Docker: {' '.join(docker_cmd)}")
175
+
176
+ # Check if image exists, pull if needed
177
+ self._ensure_docker_image()
178
+
179
+ result = subprocess.run(
180
+ docker_cmd,
181
+ capture_output=True,
182
+ text=True,
183
+ )
184
+
185
+ return result
186
+
187
+ def _ensure_docker_image(self) -> None:
188
+ """Ensure Docker image is available, pull if needed."""
189
+ # Check if image exists
190
+ result = subprocess.run(
191
+ ["docker", "image", "inspect", self.docker_image],
192
+ capture_output=True,
193
+ )
194
+
195
+ if result.returncode != 0:
196
+ # Image not found, pull it
197
+ if self.verbose:
198
+ print(f"Pulling Docker image: {self.docker_image}")
199
+
200
+ pull_result = subprocess.run(
201
+ ["docker", "pull", self.docker_image],
202
+ capture_output=not self.verbose,
203
+ )
204
+
205
+ if pull_result.returncode != 0:
206
+ raise RuntimeError(f"Failed to pull Docker image: {self.docker_image}")
207
+
208
+ def run(
209
+ self,
210
+ args: List[str],
211
+ cwd: Optional[Path] = None,
212
+ check: bool = True,
213
+ ) -> subprocess.CompletedProcess:
214
+ """
215
+ Execute kicad-cli command.
216
+
217
+ Args:
218
+ args: Command arguments (e.g., ['sch', 'export', 'netlist', 'file.kicad_sch'])
219
+ cwd: Working directory (default: current directory)
220
+ check: Raise exception on non-zero exit code
221
+
222
+ Returns:
223
+ CompletedProcess with stdout, stderr, returncode
224
+
225
+ Raises:
226
+ RuntimeError: If execution fails or kicad-cli unavailable
227
+ """
228
+ if self.mode == "local":
229
+ result = self._run_local(args, cwd)
230
+ elif self.mode == "docker":
231
+ result = self._run_docker(args, cwd)
232
+ else: # auto mode
233
+ if KiCadExecutor._local_available:
234
+ if self.verbose:
235
+ print(f"Using local KiCad CLI ({KiCadExecutor._local_version})")
236
+ result = self._run_local(args, cwd)
237
+ elif KiCadExecutor._docker_available:
238
+ if self.verbose:
239
+ print("Local KiCad CLI not found, using Docker")
240
+ result = self._run_docker(args, cwd)
241
+ else:
242
+ raise RuntimeError(
243
+ "KiCad CLI not available in any mode.\n\n"
244
+ "Install options:\n"
245
+ "1. Install KiCad: https://www.kicad.org/download/\n"
246
+ "2. Install Docker: https://docs.docker.com/get-docker/"
247
+ )
248
+
249
+ if check and result.returncode != 0:
250
+ error_msg = result.stderr if result.stderr else result.stdout
251
+ raise RuntimeError(f"KiCad CLI command failed:\n{error_msg}")
252
+
253
+ return result
254
+
255
+ @classmethod
256
+ def get_info(cls) -> ExecutorInfo:
257
+ """Get information about available execution modes."""
258
+ if cls._local_available is None:
259
+ cls._detect_local()
260
+ if cls._docker_available is None:
261
+ cls._detect_docker()
262
+
263
+ # Determine active mode
264
+ env_mode = os.getenv("KICAD_CLI_MODE", "auto").lower()
265
+ if env_mode not in ("auto", "local", "docker"):
266
+ env_mode = "auto"
267
+
268
+ return ExecutorInfo(
269
+ local_available=cls._local_available or False,
270
+ local_version=cls._local_version,
271
+ docker_available=cls._docker_available or False,
272
+ docker_image=os.getenv("KICAD_DOCKER_IMAGE", "kicad/kicad:latest"),
273
+ active_mode=env_mode, # type: ignore
274
+ )
275
+
276
+
277
+ # Convenience functions
278
+ def get_executor_info() -> ExecutorInfo:
279
+ """
280
+ Get information about KiCad CLI availability.
281
+
282
+ Returns:
283
+ ExecutorInfo with details about local and Docker availability
284
+
285
+ Example:
286
+ >>> info = get_executor_info()
287
+ >>> print(f"Local: {info.local_available}, Docker: {info.docker_available}")
288
+ """
289
+ return KiCadExecutor.get_info()
290
+
291
+
292
+ def set_execution_mode(mode: ExecutionMode) -> None:
293
+ """
294
+ Set the execution mode for all KiCad CLI operations.
295
+
296
+ Args:
297
+ mode: Execution mode (auto, local, or docker)
298
+
299
+ Example:
300
+ >>> set_execution_mode('docker') # Force Docker mode
301
+ """
302
+ os.environ["KICAD_CLI_MODE"] = mode
@@ -0,0 +1,164 @@
1
+ """Bill of Materials (BOM) export functionality using kicad-cli."""
2
+
3
+ from pathlib import Path
4
+ from typing import List, Optional
5
+
6
+ from kicad_sch_api.cli.base import KiCadExecutor
7
+
8
+
9
+ def export_bom(
10
+ schematic_path: Path,
11
+ output_path: Optional[Path] = None,
12
+ preset: Optional[str] = None,
13
+ format_preset: Optional[str] = None,
14
+ fields: Optional[List[str]] = None,
15
+ labels: Optional[List[str]] = None,
16
+ group_by: Optional[List[str]] = None,
17
+ sort_field: str = "Reference",
18
+ sort_asc: bool = True,
19
+ filter: Optional[str] = None,
20
+ exclude_dnp: bool = False,
21
+ include_excluded_from_bom: bool = False,
22
+ field_delimiter: str = ",",
23
+ string_delimiter: str = '"',
24
+ ref_delimiter: str = ",",
25
+ ref_range_delimiter: str = "",
26
+ keep_tabs: bool = False,
27
+ keep_line_breaks: bool = False,
28
+ executor: Optional[KiCadExecutor] = None,
29
+ ) -> Path:
30
+ """
31
+ Export Bill of Materials (BOM) from schematic using kicad-cli.
32
+
33
+ Generates a CSV file with component information, suitable for manufacturing
34
+ and procurement.
35
+
36
+ Args:
37
+ schematic_path: Path to .kicad_sch file
38
+ output_path: Output BOM path (auto-generated if None)
39
+ preset: Named BOM preset from schematic (e.g., "Grouped By Value")
40
+ format_preset: Named BOM format preset (e.g., "CSV")
41
+ fields: List of fields to export (default: Reference, Value, Footprint, Qty, DNP)
42
+ labels: List of labels for exported fields
43
+ group_by: Fields to group references by when values match
44
+ sort_field: Field name to sort by (default: "Reference")
45
+ sort_asc: Sort ascending (True) or descending (False)
46
+ filter: Filter string to remove output lines
47
+ exclude_dnp: Exclude components marked Do-Not-Populate
48
+ include_excluded_from_bom: Include components marked 'Exclude from BOM'
49
+ field_delimiter: Separator between fields/columns (default: ",")
50
+ string_delimiter: Character to surround fields with (default: '"')
51
+ ref_delimiter: Character between individual references (default: ",")
52
+ ref_range_delimiter: Character for reference ranges (default: "", use "-" for ranges like "R1-R5")
53
+ keep_tabs: Keep tab characters from input fields
54
+ keep_line_breaks: Keep line break characters from input fields
55
+ executor: Custom KiCadExecutor instance (creates default if None)
56
+
57
+ Returns:
58
+ Path to generated BOM file
59
+
60
+ Raises:
61
+ RuntimeError: If kicad-cli not found or BOM generation fails
62
+ FileNotFoundError: If schematic file doesn't exist
63
+
64
+ Example:
65
+ >>> from pathlib import Path
66
+ >>> bom = export_bom(
67
+ ... Path('circuit.kicad_sch'),
68
+ ... fields=['Reference', 'Value', 'Footprint', 'MPN'],
69
+ ... group_by=['Value', 'Footprint'],
70
+ ... exclude_dnp=True,
71
+ ... )
72
+ >>> print(f"BOM: {bom}")
73
+
74
+ Common use cases:
75
+ # Simple BOM with default fields
76
+ >>> export_bom(Path('circuit.kicad_sch'))
77
+
78
+ # Manufacturing BOM (exclude DNP, group by value)
79
+ >>> export_bom(
80
+ ... Path('circuit.kicad_sch'),
81
+ ... group_by=['Value', 'Footprint'],
82
+ ... exclude_dnp=True,
83
+ ... )
84
+
85
+ # BOM with manufacturer part numbers
86
+ >>> export_bom(
87
+ ... Path('circuit.kicad_sch'),
88
+ ... fields=['Reference', 'Value', 'Footprint', 'MPN', 'Manufacturer'],
89
+ ... group_by=['MPN'],
90
+ ... )
91
+ """
92
+ schematic_path = Path(schematic_path)
93
+
94
+ if not schematic_path.exists():
95
+ raise FileNotFoundError(f"Schematic not found: {schematic_path}")
96
+
97
+ # Auto-generate output path if not provided
98
+ if output_path is None:
99
+ output_path = schematic_path.with_suffix(".csv")
100
+ else:
101
+ output_path = Path(output_path)
102
+
103
+ # Create executor if not provided
104
+ if executor is None:
105
+ executor = KiCadExecutor()
106
+
107
+ # Build command
108
+ args = [
109
+ "sch", "export", "bom",
110
+ "--output", str(output_path),
111
+ ]
112
+
113
+ # Add optional parameters
114
+ if preset:
115
+ args.extend(["--preset", preset])
116
+
117
+ if format_preset:
118
+ args.extend(["--format-preset", format_preset])
119
+
120
+ if fields:
121
+ args.extend(["--fields", ",".join(fields)])
122
+
123
+ if labels:
124
+ args.extend(["--labels", ",".join(labels)])
125
+
126
+ if group_by:
127
+ args.extend(["--group-by", ",".join(group_by)])
128
+
129
+ if sort_field:
130
+ args.extend(["--sort-field", sort_field])
131
+
132
+ if sort_asc:
133
+ args.append("--sort-asc")
134
+
135
+ if filter:
136
+ args.extend(["--filter", filter])
137
+
138
+ if exclude_dnp:
139
+ args.append("--exclude-dnp")
140
+
141
+ if include_excluded_from_bom:
142
+ args.append("--include-excluded-from-bom")
143
+
144
+ # Delimiter options
145
+ args.extend(["--field-delimiter", field_delimiter])
146
+ args.extend(["--string-delimiter", string_delimiter])
147
+ args.extend(["--ref-delimiter", ref_delimiter])
148
+
149
+ if ref_range_delimiter:
150
+ args.extend(["--ref-range-delimiter", ref_range_delimiter])
151
+
152
+ if keep_tabs:
153
+ args.append("--keep-tabs")
154
+
155
+ if keep_line_breaks:
156
+ args.append("--keep-line-breaks")
157
+
158
+ # Add schematic path
159
+ args.append(str(schematic_path))
160
+
161
+ # Execute command
162
+ executor.run(args, cwd=schematic_path.parent)
163
+
164
+ return output_path