kicad-sch-api 0.4.0__py3-none-any.whl → 0.4.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kicad-sch-api might be problematic. Click here for more details.

Files changed (57) hide show
  1. kicad_sch_api/__init__.py +2 -2
  2. kicad_sch_api/cli/__init__.py +45 -0
  3. kicad_sch_api/cli/base.py +302 -0
  4. kicad_sch_api/cli/bom.py +164 -0
  5. kicad_sch_api/cli/erc.py +229 -0
  6. kicad_sch_api/cli/export_docs.py +289 -0
  7. kicad_sch_api/cli/netlist.py +94 -0
  8. kicad_sch_api/cli/types.py +43 -0
  9. kicad_sch_api/core/collections/__init__.py +5 -0
  10. kicad_sch_api/core/collections/base.py +248 -0
  11. kicad_sch_api/core/component_bounds.py +5 -0
  12. kicad_sch_api/core/components.py +142 -47
  13. kicad_sch_api/core/config.py +85 -3
  14. kicad_sch_api/core/factories/__init__.py +5 -0
  15. kicad_sch_api/core/factories/element_factory.py +276 -0
  16. kicad_sch_api/core/formatter.py +22 -5
  17. kicad_sch_api/core/junctions.py +26 -75
  18. kicad_sch_api/core/labels.py +28 -52
  19. kicad_sch_api/core/managers/file_io.py +3 -2
  20. kicad_sch_api/core/managers/metadata.py +6 -5
  21. kicad_sch_api/core/managers/validation.py +3 -2
  22. kicad_sch_api/core/managers/wire.py +7 -1
  23. kicad_sch_api/core/nets.py +38 -43
  24. kicad_sch_api/core/no_connects.py +29 -53
  25. kicad_sch_api/core/parser.py +75 -1765
  26. kicad_sch_api/core/schematic.py +211 -148
  27. kicad_sch_api/core/texts.py +28 -55
  28. kicad_sch_api/core/types.py +59 -18
  29. kicad_sch_api/core/wires.py +27 -75
  30. kicad_sch_api/parsers/elements/__init__.py +22 -0
  31. kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
  32. kicad_sch_api/parsers/elements/label_parser.py +194 -0
  33. kicad_sch_api/parsers/elements/library_parser.py +165 -0
  34. kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
  35. kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
  36. kicad_sch_api/parsers/elements/symbol_parser.py +313 -0
  37. kicad_sch_api/parsers/elements/text_parser.py +250 -0
  38. kicad_sch_api/parsers/elements/wire_parser.py +242 -0
  39. kicad_sch_api/parsers/utils.py +80 -0
  40. kicad_sch_api/validation/__init__.py +25 -0
  41. kicad_sch_api/validation/erc.py +171 -0
  42. kicad_sch_api/validation/erc_models.py +203 -0
  43. kicad_sch_api/validation/pin_matrix.py +243 -0
  44. kicad_sch_api/validation/validators.py +391 -0
  45. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/METADATA +17 -9
  46. kicad_sch_api-0.4.2.dist-info/RECORD +87 -0
  47. kicad_sch_api/core/manhattan_routing.py +0 -430
  48. kicad_sch_api/core/simple_manhattan.py +0 -228
  49. kicad_sch_api/core/wire_routing.py +0 -380
  50. kicad_sch_api/parsers/label_parser.py +0 -254
  51. kicad_sch_api/parsers/symbol_parser.py +0 -222
  52. kicad_sch_api/parsers/wire_parser.py +0 -99
  53. kicad_sch_api-0.4.0.dist-info/RECORD +0 -67
  54. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/WHEEL +0 -0
  55. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/entry_points.txt +0 -0
  56. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/licenses/LICENSE +0 -0
  57. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/top_level.txt +0 -0
kicad_sch_api/__init__.py CHANGED
@@ -42,7 +42,7 @@ Advanced Usage:
42
42
  print(f"Found {len(issues)} validation issues")
43
43
  """
44
44
 
45
- __version__ = "0.3.3"
45
+ __version__ = "0.4.2"
46
46
  __author__ = "Circuit-Synth"
47
47
  __email__ = "info@circuit-synth.com"
48
48
 
@@ -55,7 +55,7 @@ from .library.cache import SymbolLibraryCache, get_symbol_cache
55
55
  from .utils.validation import ValidationError, ValidationIssue
56
56
 
57
57
  # Version info
58
- VERSION_INFO = (0, 3, 3)
58
+ VERSION_INFO = (0, 4, 0)
59
59
 
60
60
  # Public API
61
61
  __all__ = [
@@ -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