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.
- kicad_sch_api/__init__.py +68 -3
- kicad_sch_api/cli/__init__.py +45 -0
- kicad_sch_api/cli/base.py +302 -0
- kicad_sch_api/cli/bom.py +164 -0
- kicad_sch_api/cli/erc.py +229 -0
- kicad_sch_api/cli/export_docs.py +289 -0
- kicad_sch_api/cli/kicad_to_python.py +169 -0
- kicad_sch_api/cli/netlist.py +94 -0
- kicad_sch_api/cli/types.py +43 -0
- kicad_sch_api/collections/__init__.py +36 -0
- kicad_sch_api/collections/base.py +604 -0
- kicad_sch_api/collections/components.py +1623 -0
- kicad_sch_api/collections/junctions.py +206 -0
- kicad_sch_api/collections/labels.py +508 -0
- kicad_sch_api/collections/wires.py +292 -0
- kicad_sch_api/core/__init__.py +37 -2
- kicad_sch_api/core/collections/__init__.py +5 -0
- kicad_sch_api/core/collections/base.py +248 -0
- kicad_sch_api/core/component_bounds.py +34 -7
- kicad_sch_api/core/components.py +213 -52
- kicad_sch_api/core/config.py +110 -15
- kicad_sch_api/core/connectivity.py +692 -0
- kicad_sch_api/core/exceptions.py +175 -0
- kicad_sch_api/core/factories/__init__.py +5 -0
- kicad_sch_api/core/factories/element_factory.py +278 -0
- kicad_sch_api/core/formatter.py +60 -9
- kicad_sch_api/core/geometry.py +94 -5
- kicad_sch_api/core/junctions.py +26 -75
- kicad_sch_api/core/labels.py +324 -0
- kicad_sch_api/core/managers/__init__.py +30 -0
- kicad_sch_api/core/managers/base.py +76 -0
- kicad_sch_api/core/managers/file_io.py +246 -0
- kicad_sch_api/core/managers/format_sync.py +502 -0
- kicad_sch_api/core/managers/graphics.py +580 -0
- kicad_sch_api/core/managers/hierarchy.py +661 -0
- kicad_sch_api/core/managers/metadata.py +271 -0
- kicad_sch_api/core/managers/sheet.py +492 -0
- kicad_sch_api/core/managers/text_elements.py +537 -0
- kicad_sch_api/core/managers/validation.py +476 -0
- kicad_sch_api/core/managers/wire.py +410 -0
- kicad_sch_api/core/nets.py +305 -0
- kicad_sch_api/core/no_connects.py +252 -0
- kicad_sch_api/core/parser.py +194 -970
- kicad_sch_api/core/parsing_utils.py +63 -0
- kicad_sch_api/core/pin_utils.py +103 -9
- kicad_sch_api/core/schematic.py +1328 -1079
- kicad_sch_api/core/texts.py +316 -0
- kicad_sch_api/core/types.py +159 -23
- kicad_sch_api/core/wires.py +27 -75
- kicad_sch_api/exporters/__init__.py +10 -0
- kicad_sch_api/exporters/python_generator.py +610 -0
- kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
- kicad_sch_api/geometry/__init__.py +38 -0
- kicad_sch_api/geometry/font_metrics.py +22 -0
- kicad_sch_api/geometry/routing.py +211 -0
- kicad_sch_api/geometry/symbol_bbox.py +608 -0
- kicad_sch_api/interfaces/__init__.py +17 -0
- kicad_sch_api/interfaces/parser.py +76 -0
- kicad_sch_api/interfaces/repository.py +70 -0
- kicad_sch_api/interfaces/resolver.py +117 -0
- kicad_sch_api/parsers/__init__.py +14 -0
- kicad_sch_api/parsers/base.py +145 -0
- kicad_sch_api/parsers/elements/__init__.py +22 -0
- kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
- kicad_sch_api/parsers/elements/label_parser.py +216 -0
- kicad_sch_api/parsers/elements/library_parser.py +165 -0
- kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
- kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
- kicad_sch_api/parsers/elements/symbol_parser.py +485 -0
- kicad_sch_api/parsers/elements/text_parser.py +250 -0
- kicad_sch_api/parsers/elements/wire_parser.py +242 -0
- kicad_sch_api/parsers/registry.py +155 -0
- kicad_sch_api/parsers/utils.py +80 -0
- kicad_sch_api/symbols/__init__.py +18 -0
- kicad_sch_api/symbols/cache.py +467 -0
- kicad_sch_api/symbols/resolver.py +361 -0
- kicad_sch_api/symbols/validators.py +504 -0
- kicad_sch_api/utils/logging.py +555 -0
- kicad_sch_api/utils/logging_decorators.py +587 -0
- kicad_sch_api/utils/validation.py +16 -22
- kicad_sch_api/validation/__init__.py +25 -0
- kicad_sch_api/validation/erc.py +171 -0
- kicad_sch_api/validation/erc_models.py +203 -0
- kicad_sch_api/validation/pin_matrix.py +243 -0
- kicad_sch_api/validation/validators.py +391 -0
- kicad_sch_api/wrappers/__init__.py +14 -0
- kicad_sch_api/wrappers/base.py +89 -0
- kicad_sch_api/wrappers/wire.py +198 -0
- kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
- kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
- kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
- mcp_server/__init__.py +34 -0
- mcp_server/example_logging_integration.py +506 -0
- mcp_server/models.py +252 -0
- mcp_server/server.py +357 -0
- mcp_server/tools/__init__.py +32 -0
- mcp_server/tools/component_tools.py +516 -0
- mcp_server/tools/connectivity_tools.py +532 -0
- mcp_server/tools/consolidated_tools.py +1216 -0
- mcp_server/tools/pin_discovery.py +333 -0
- mcp_server/utils/__init__.py +38 -0
- mcp_server/utils/logging.py +127 -0
- mcp_server/utils.py +36 -0
- kicad_sch_api/core/manhattan_routing.py +0 -430
- kicad_sch_api/core/simple_manhattan.py +0 -228
- kicad_sch_api/core/wire_routing.py +0 -380
- kicad_sch_api-0.3.0.dist-info/METADATA +0 -483
- kicad_sch_api-0.3.0.dist-info/RECORD +0 -31
- kicad_sch_api-0.3.0.dist-info/entry_points.txt +0 -2
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exception hierarchy for kicad-sch-api.
|
|
3
|
+
|
|
4
|
+
Provides a structured exception hierarchy for better error handling and debugging.
|
|
5
|
+
All exceptions inherit from the base KiCadSchError class.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, List, Optional, TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
# Import validation types for type hints
|
|
11
|
+
# ValidationLevel is imported at runtime in methods that need it
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..utils.validation import ValidationIssue
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class KiCadSchError(Exception):
|
|
17
|
+
"""Base exception for all kicad-sch-api errors."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ValidationError(KiCadSchError):
|
|
23
|
+
"""
|
|
24
|
+
Raised when validation fails.
|
|
25
|
+
|
|
26
|
+
Supports rich error context with field/value information and can collect
|
|
27
|
+
multiple validation issues.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
message: str,
|
|
33
|
+
issues: Optional[List["ValidationIssue"]] = None,
|
|
34
|
+
field: str = "",
|
|
35
|
+
value: Any = None,
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Initialize validation error with context.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
message: Error message describing the validation failure
|
|
42
|
+
issues: List of validation issues (for collecting multiple errors)
|
|
43
|
+
field: The field name that failed validation
|
|
44
|
+
value: The invalid value that was provided
|
|
45
|
+
"""
|
|
46
|
+
self.issues = issues or []
|
|
47
|
+
self.field = field
|
|
48
|
+
self.value = value
|
|
49
|
+
super().__init__(message)
|
|
50
|
+
|
|
51
|
+
def add_issue(self, issue: "ValidationIssue") -> None:
|
|
52
|
+
"""Add a validation issue to this error."""
|
|
53
|
+
self.issues.append(issue)
|
|
54
|
+
|
|
55
|
+
def get_errors(self) -> List["ValidationIssue"]:
|
|
56
|
+
"""Get only error-level issues."""
|
|
57
|
+
# Import here to avoid circular dependency
|
|
58
|
+
from ..utils.validation import ValidationLevel
|
|
59
|
+
|
|
60
|
+
return [
|
|
61
|
+
issue
|
|
62
|
+
for issue in self.issues
|
|
63
|
+
if hasattr(issue, 'level') and issue.level in (ValidationLevel.ERROR, ValidationLevel.CRITICAL)
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
def get_warnings(self) -> List["ValidationIssue"]:
|
|
67
|
+
"""Get only warning-level issues."""
|
|
68
|
+
# Import here to avoid circular dependency
|
|
69
|
+
from ..utils.validation import ValidationLevel
|
|
70
|
+
|
|
71
|
+
return [issue for issue in self.issues if hasattr(issue, 'level') and issue.level == ValidationLevel.WARNING]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ReferenceError(ValidationError):
|
|
75
|
+
"""Raised when a component reference is invalid."""
|
|
76
|
+
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class LibraryError(ValidationError):
|
|
81
|
+
"""Raised when a library or symbol reference is invalid."""
|
|
82
|
+
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class GeometryError(ValidationError):
|
|
87
|
+
"""Raised when geometry validation fails (positions, shapes, dimensions)."""
|
|
88
|
+
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class NetError(ValidationError):
|
|
93
|
+
"""Raised when a net specification or operation is invalid."""
|
|
94
|
+
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ParseError(KiCadSchError):
|
|
99
|
+
"""Raised when parsing a schematic file fails."""
|
|
100
|
+
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class FormatError(KiCadSchError):
|
|
105
|
+
"""Raised when formatting a schematic file fails."""
|
|
106
|
+
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class CollectionError(KiCadSchError):
|
|
111
|
+
"""Raised when a collection operation fails."""
|
|
112
|
+
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ElementNotFoundError(CollectionError):
|
|
117
|
+
"""Raised when an element is not found in a collection."""
|
|
118
|
+
|
|
119
|
+
def __init__(self, message: str, element_type: str = "", identifier: str = ""):
|
|
120
|
+
"""
|
|
121
|
+
Initialize element not found error.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
message: Error message
|
|
125
|
+
element_type: Type of element (e.g., 'component', 'wire', 'junction')
|
|
126
|
+
identifier: The identifier used to search (e.g., 'R1', UUID)
|
|
127
|
+
"""
|
|
128
|
+
self.element_type = element_type
|
|
129
|
+
self.identifier = identifier
|
|
130
|
+
super().__init__(message)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class DuplicateElementError(CollectionError):
|
|
134
|
+
"""Raised when attempting to add a duplicate element."""
|
|
135
|
+
|
|
136
|
+
def __init__(self, message: str, element_type: str = "", identifier: str = ""):
|
|
137
|
+
"""
|
|
138
|
+
Initialize duplicate element error.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
message: Error message
|
|
142
|
+
element_type: Type of element (e.g., 'component', 'wire', 'junction')
|
|
143
|
+
identifier: The duplicate identifier (e.g., 'R1', UUID)
|
|
144
|
+
"""
|
|
145
|
+
self.element_type = element_type
|
|
146
|
+
self.identifier = identifier
|
|
147
|
+
super().__init__(message)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class CollectionOperationError(CollectionError):
|
|
151
|
+
"""Raised when a collection operation fails for reasons other than not found/duplicate."""
|
|
152
|
+
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class FileOperationError(KiCadSchError):
|
|
157
|
+
"""Raised when a file I/O operation fails."""
|
|
158
|
+
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class CLIError(KiCadSchError):
|
|
163
|
+
"""Raised when KiCad CLI execution fails."""
|
|
164
|
+
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class SchematicStateError(KiCadSchError):
|
|
169
|
+
"""
|
|
170
|
+
Raised when an operation requires specific schematic state.
|
|
171
|
+
|
|
172
|
+
Examples: schematic must be saved before export, etc.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
pass
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Element Factory for creating schematic elements from dictionaries.
|
|
3
|
+
|
|
4
|
+
Centralizes object creation logic that was previously duplicated in Schematic.__init__.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import uuid
|
|
8
|
+
from typing import Any, Dict, List
|
|
9
|
+
|
|
10
|
+
from ..types import (
|
|
11
|
+
HierarchicalLabelShape,
|
|
12
|
+
Junction,
|
|
13
|
+
Label,
|
|
14
|
+
LabelType,
|
|
15
|
+
Net,
|
|
16
|
+
NoConnect,
|
|
17
|
+
Point,
|
|
18
|
+
Text,
|
|
19
|
+
Wire,
|
|
20
|
+
WireType,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def point_from_dict_or_tuple(position: Any) -> Point:
|
|
25
|
+
"""Convert position data (dict or tuple) to Point object."""
|
|
26
|
+
if isinstance(position, dict):
|
|
27
|
+
return Point(position.get("x", 0), position.get("y", 0))
|
|
28
|
+
elif isinstance(position, (list, tuple)):
|
|
29
|
+
return Point(position[0], position[1])
|
|
30
|
+
elif isinstance(position, Point):
|
|
31
|
+
return position
|
|
32
|
+
else:
|
|
33
|
+
return Point(0, 0)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ElementFactory:
|
|
37
|
+
"""Factory for creating schematic elements from dictionary data."""
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def create_wire(wire_dict: Dict[str, Any]) -> Wire:
|
|
41
|
+
"""
|
|
42
|
+
Create Wire object from dictionary.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
wire_dict: Dictionary containing wire data
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Wire object
|
|
49
|
+
"""
|
|
50
|
+
points = []
|
|
51
|
+
for point_data in wire_dict.get("points", []):
|
|
52
|
+
if isinstance(point_data, dict):
|
|
53
|
+
points.append(Point(point_data["x"], point_data["y"]))
|
|
54
|
+
elif isinstance(point_data, (list, tuple)):
|
|
55
|
+
points.append(Point(point_data[0], point_data[1]))
|
|
56
|
+
else:
|
|
57
|
+
points.append(point_data)
|
|
58
|
+
|
|
59
|
+
return Wire(
|
|
60
|
+
uuid=wire_dict.get("uuid", str(uuid.uuid4())),
|
|
61
|
+
points=points,
|
|
62
|
+
wire_type=WireType(wire_dict.get("wire_type", "wire")),
|
|
63
|
+
stroke_width=wire_dict.get("stroke_width", 0.0),
|
|
64
|
+
stroke_type=wire_dict.get("stroke_type", "default"),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def create_junction(junction_dict: Dict[str, Any]) -> Junction:
|
|
69
|
+
"""
|
|
70
|
+
Create Junction object from dictionary.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
junction_dict: Dictionary containing junction data
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Junction object
|
|
77
|
+
"""
|
|
78
|
+
position = junction_dict.get("position", {"x": 0, "y": 0})
|
|
79
|
+
pos = point_from_dict_or_tuple(position)
|
|
80
|
+
|
|
81
|
+
return Junction(
|
|
82
|
+
uuid=junction_dict.get("uuid", str(uuid.uuid4())),
|
|
83
|
+
position=pos,
|
|
84
|
+
diameter=junction_dict.get("diameter", 0),
|
|
85
|
+
color=junction_dict.get("color", (0, 0, 0, 0)),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def create_text(text_dict: Dict[str, Any]) -> Text:
|
|
90
|
+
"""
|
|
91
|
+
Create Text object from dictionary.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
text_dict: Dictionary containing text data
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Text object
|
|
98
|
+
"""
|
|
99
|
+
position = text_dict.get("position", {"x": 0, "y": 0})
|
|
100
|
+
pos = point_from_dict_or_tuple(position)
|
|
101
|
+
|
|
102
|
+
return Text(
|
|
103
|
+
uuid=text_dict.get("uuid", str(uuid.uuid4())),
|
|
104
|
+
position=pos,
|
|
105
|
+
text=text_dict.get("text", ""),
|
|
106
|
+
rotation=text_dict.get("rotation", 0.0),
|
|
107
|
+
size=text_dict.get("size", 1.27),
|
|
108
|
+
exclude_from_sim=text_dict.get("exclude_from_sim", False),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def create_label(label_dict: Dict[str, Any]) -> Label:
|
|
113
|
+
"""
|
|
114
|
+
Create Label object from dictionary.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
label_dict: Dictionary containing label data
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Label object
|
|
121
|
+
"""
|
|
122
|
+
position = label_dict.get("position", {"x": 0, "y": 0})
|
|
123
|
+
pos = point_from_dict_or_tuple(position)
|
|
124
|
+
|
|
125
|
+
return Label(
|
|
126
|
+
uuid=label_dict.get("uuid", str(uuid.uuid4())),
|
|
127
|
+
position=pos,
|
|
128
|
+
text=label_dict.get("text", ""),
|
|
129
|
+
label_type=LabelType(label_dict.get("label_type", "label")),
|
|
130
|
+
rotation=label_dict.get("rotation", 0.0),
|
|
131
|
+
size=label_dict.get("size", 1.27),
|
|
132
|
+
shape=(
|
|
133
|
+
HierarchicalLabelShape(label_dict.get("shape"))
|
|
134
|
+
if label_dict.get("shape")
|
|
135
|
+
else None
|
|
136
|
+
),
|
|
137
|
+
justify_h=label_dict.get("justify_h", "left"),
|
|
138
|
+
justify_v=label_dict.get("justify_v", "bottom"),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def create_no_connect(no_connect_dict: Dict[str, Any]) -> NoConnect:
|
|
143
|
+
"""
|
|
144
|
+
Create NoConnect object from dictionary.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
no_connect_dict: Dictionary containing no-connect data
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
NoConnect object
|
|
151
|
+
"""
|
|
152
|
+
position = no_connect_dict.get("position", {"x": 0, "y": 0})
|
|
153
|
+
pos = point_from_dict_or_tuple(position)
|
|
154
|
+
|
|
155
|
+
return NoConnect(
|
|
156
|
+
uuid=no_connect_dict.get("uuid", str(uuid.uuid4())),
|
|
157
|
+
position=pos,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def create_net(net_dict: Dict[str, Any]) -> Net:
|
|
162
|
+
"""
|
|
163
|
+
Create Net object from dictionary.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
net_dict: Dictionary containing net data
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Net object
|
|
170
|
+
"""
|
|
171
|
+
return Net(
|
|
172
|
+
name=net_dict.get("name", ""),
|
|
173
|
+
components=net_dict.get("components", []),
|
|
174
|
+
wires=net_dict.get("wires", []),
|
|
175
|
+
labels=net_dict.get("labels", []),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def create_wires_from_list(wire_data: List[Any]) -> List[Wire]:
|
|
180
|
+
"""
|
|
181
|
+
Create list of Wire objects from list of dictionaries.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
wire_data: List of wire dictionaries
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
List of Wire objects
|
|
188
|
+
"""
|
|
189
|
+
wires = []
|
|
190
|
+
for wire_dict in wire_data:
|
|
191
|
+
if isinstance(wire_dict, dict):
|
|
192
|
+
wires.append(ElementFactory.create_wire(wire_dict))
|
|
193
|
+
return wires
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def create_junctions_from_list(junction_data: List[Any]) -> List[Junction]:
|
|
197
|
+
"""
|
|
198
|
+
Create list of Junction objects from list of dictionaries.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
junction_data: List of junction dictionaries
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
List of Junction objects
|
|
205
|
+
"""
|
|
206
|
+
junctions = []
|
|
207
|
+
for junction_dict in junction_data:
|
|
208
|
+
if isinstance(junction_dict, dict):
|
|
209
|
+
junctions.append(ElementFactory.create_junction(junction_dict))
|
|
210
|
+
return junctions
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def create_texts_from_list(text_data: List[Any]) -> List[Text]:
|
|
214
|
+
"""
|
|
215
|
+
Create list of Text objects from list of dictionaries.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
text_data: List of text dictionaries
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
List of Text objects
|
|
222
|
+
"""
|
|
223
|
+
texts = []
|
|
224
|
+
for text_dict in text_data:
|
|
225
|
+
if isinstance(text_dict, dict):
|
|
226
|
+
texts.append(ElementFactory.create_text(text_dict))
|
|
227
|
+
return texts
|
|
228
|
+
|
|
229
|
+
@staticmethod
|
|
230
|
+
def create_labels_from_list(label_data: List[Any]) -> List[Label]:
|
|
231
|
+
"""
|
|
232
|
+
Create list of Label objects from list of dictionaries.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
label_data: List of label dictionaries
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
List of Label objects
|
|
239
|
+
"""
|
|
240
|
+
labels = []
|
|
241
|
+
for label_dict in label_data:
|
|
242
|
+
if isinstance(label_dict, dict):
|
|
243
|
+
labels.append(ElementFactory.create_label(label_dict))
|
|
244
|
+
return labels
|
|
245
|
+
|
|
246
|
+
@staticmethod
|
|
247
|
+
def create_no_connects_from_list(no_connect_data: List[Any]) -> List[NoConnect]:
|
|
248
|
+
"""
|
|
249
|
+
Create list of NoConnect objects from list of dictionaries.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
no_connect_data: List of no-connect dictionaries
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
List of NoConnect objects
|
|
256
|
+
"""
|
|
257
|
+
no_connects = []
|
|
258
|
+
for no_connect_dict in no_connect_data:
|
|
259
|
+
if isinstance(no_connect_dict, dict):
|
|
260
|
+
no_connects.append(ElementFactory.create_no_connect(no_connect_dict))
|
|
261
|
+
return no_connects
|
|
262
|
+
|
|
263
|
+
@staticmethod
|
|
264
|
+
def create_nets_from_list(net_data: List[Any]) -> List[Net]:
|
|
265
|
+
"""
|
|
266
|
+
Create list of Net objects from list of dictionaries.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
net_data: List of net dictionaries
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
List of Net objects
|
|
273
|
+
"""
|
|
274
|
+
nets = []
|
|
275
|
+
for net_dict in net_data:
|
|
276
|
+
if isinstance(net_dict, dict):
|
|
277
|
+
nets.append(ElementFactory.create_net(net_dict))
|
|
278
|
+
return nets
|
kicad_sch_api/core/formatter.py
CHANGED
|
@@ -55,7 +55,9 @@ class ExactFormatter:
|
|
|
55
55
|
self.rules["generator"] = FormatRule(inline=True, quote_indices={1})
|
|
56
56
|
self.rules["generator_version"] = FormatRule(inline=True, quote_indices={1})
|
|
57
57
|
self.rules["uuid"] = FormatRule(inline=True, quote_indices={1})
|
|
58
|
-
self.rules["paper"] = FormatRule(
|
|
58
|
+
self.rules["paper"] = FormatRule(
|
|
59
|
+
inline=True, quote_indices={1}
|
|
60
|
+
) # Paper size should be quoted per KiCad format
|
|
59
61
|
|
|
60
62
|
# Title block
|
|
61
63
|
self.rules["title_block"] = FormatRule(inline=False)
|
|
@@ -123,6 +125,10 @@ class ExactFormatter:
|
|
|
123
125
|
self.rules["global_label"] = FormatRule(inline=False, quote_indices={1})
|
|
124
126
|
self.rules["hierarchical_label"] = FormatRule(inline=False, quote_indices={1})
|
|
125
127
|
|
|
128
|
+
# Text elements
|
|
129
|
+
self.rules["text"] = FormatRule(inline=False, quote_indices={1})
|
|
130
|
+
self.rules["text_box"] = FormatRule(inline=False, quote_indices={1})
|
|
131
|
+
|
|
126
132
|
# Effects and text formatting
|
|
127
133
|
self.rules["effects"] = FormatRule(inline=False)
|
|
128
134
|
self.rules["font"] = FormatRule(inline=False)
|
|
@@ -146,6 +152,9 @@ class ExactFormatter:
|
|
|
146
152
|
self.rules["embedded_fonts"] = FormatRule(inline=True)
|
|
147
153
|
self.rules["page"] = FormatRule(inline=True, quote_indices={1})
|
|
148
154
|
|
|
155
|
+
# Image element
|
|
156
|
+
self.rules["image"] = FormatRule(inline=False, custom_handler=self._format_image)
|
|
157
|
+
|
|
149
158
|
def format(self, data: Any) -> str:
|
|
150
159
|
"""
|
|
151
160
|
Format S-expression data with exact KiCAD formatting.
|
|
@@ -189,7 +198,8 @@ class ExactFormatter:
|
|
|
189
198
|
elif isinstance(element, str):
|
|
190
199
|
# Quote strings that need quoting
|
|
191
200
|
if self._needs_quoting(element):
|
|
192
|
-
|
|
201
|
+
escaped = self._escape_string(element)
|
|
202
|
+
return f'"{escaped}"'
|
|
193
203
|
return element
|
|
194
204
|
elif isinstance(element, float):
|
|
195
205
|
# Custom float formatting for KiCAD compatibility
|
|
@@ -273,6 +283,8 @@ class ExactFormatter:
|
|
|
273
283
|
"junction",
|
|
274
284
|
"label",
|
|
275
285
|
"hierarchical_label",
|
|
286
|
+
"text",
|
|
287
|
+
"text_box",
|
|
276
288
|
"polyline",
|
|
277
289
|
"rectangle",
|
|
278
290
|
):
|
|
@@ -378,7 +390,8 @@ class ExactFormatter:
|
|
|
378
390
|
result += f"\n{next_indent}{self._format_element(element, indent_level + 1)}"
|
|
379
391
|
else:
|
|
380
392
|
if i in rule.quote_indices and isinstance(element, str):
|
|
381
|
-
|
|
393
|
+
escaped_element = self._escape_string(element)
|
|
394
|
+
result += f' "{escaped_element}"'
|
|
382
395
|
else:
|
|
383
396
|
result += f" {self._format_element(element, 0)}"
|
|
384
397
|
|
|
@@ -390,7 +403,8 @@ class ExactFormatter:
|
|
|
390
403
|
indent = "\t" * indent_level
|
|
391
404
|
next_indent = "\t" * (indent_level + 1)
|
|
392
405
|
|
|
393
|
-
|
|
406
|
+
tag = str(lst[0])
|
|
407
|
+
result = f"({tag}"
|
|
394
408
|
|
|
395
409
|
for i, element in enumerate(lst[1:], 1):
|
|
396
410
|
if isinstance(element, list):
|
|
@@ -419,9 +433,18 @@ class ExactFormatter:
|
|
|
419
433
|
return True
|
|
420
434
|
|
|
421
435
|
def _escape_string(self, text: str) -> str:
|
|
422
|
-
"""Escape
|
|
423
|
-
#
|
|
424
|
-
|
|
436
|
+
"""Escape special characters in string for S-expression formatting."""
|
|
437
|
+
# Escape backslashes first (must be done before other replacements)
|
|
438
|
+
text = text.replace('\\', '\\\\')
|
|
439
|
+
# Escape double quotes
|
|
440
|
+
text = text.replace('"', '\\"')
|
|
441
|
+
# Escape newlines (convert actual newlines to escaped representation)
|
|
442
|
+
text = text.replace('\n', '\\n')
|
|
443
|
+
# Escape carriage returns
|
|
444
|
+
text = text.replace('\r', '\\r')
|
|
445
|
+
# Escape tabs
|
|
446
|
+
text = text.replace('\t', '\\t')
|
|
447
|
+
return text
|
|
425
448
|
|
|
426
449
|
def _needs_quoting(self, text: str) -> bool:
|
|
427
450
|
"""Check if string needs to be quoted."""
|
|
@@ -460,8 +483,8 @@ class ExactFormatter:
|
|
|
460
483
|
for item in lst[1:]:
|
|
461
484
|
if isinstance(item, list) and len(item) >= 1:
|
|
462
485
|
tag = str(item[0])
|
|
463
|
-
if tag in ["version", "generator", "generator_version"] and len(item) >= 2:
|
|
464
|
-
if tag in ["generator", "generator_version"]:
|
|
486
|
+
if tag in ["version", "generator", "generator_version", "uuid"] and len(item) >= 2:
|
|
487
|
+
if tag in ["generator", "generator_version", "uuid"]:
|
|
465
488
|
header_parts.append(f'({tag} "{item[1]}")')
|
|
466
489
|
else:
|
|
467
490
|
header_parts.append(f"({tag} {item[1]})")
|
|
@@ -510,6 +533,34 @@ class ExactFormatter:
|
|
|
510
533
|
result += f"\n{indent})"
|
|
511
534
|
return result
|
|
512
535
|
|
|
536
|
+
def _format_image(self, lst: List[Any], indent_level: int) -> str:
|
|
537
|
+
"""Format image elements with base64 data split across lines."""
|
|
538
|
+
indent = "\t" * indent_level
|
|
539
|
+
next_indent = "\t" * (indent_level + 1)
|
|
540
|
+
|
|
541
|
+
result = f"({lst[0]}"
|
|
542
|
+
|
|
543
|
+
# Process each element
|
|
544
|
+
for element in lst[1:]:
|
|
545
|
+
if isinstance(element, list):
|
|
546
|
+
tag = str(element[0]) if element else ""
|
|
547
|
+
if tag == "data":
|
|
548
|
+
# Special handling for data element
|
|
549
|
+
# First chunk on same line as (data, rest on subsequent lines
|
|
550
|
+
if len(element) > 1:
|
|
551
|
+
result += f'\n{next_indent}({element[0]} "{element[1]}"'
|
|
552
|
+
for chunk in element[2:]:
|
|
553
|
+
result += f'\n{next_indent}\t"{chunk}"'
|
|
554
|
+
result += f"\n{next_indent})"
|
|
555
|
+
else:
|
|
556
|
+
result += f"\n{next_indent}({element[0]})"
|
|
557
|
+
else:
|
|
558
|
+
# Regular element formatting
|
|
559
|
+
result += f"\n{next_indent}{self._format_element(element, indent_level + 1)}"
|
|
560
|
+
|
|
561
|
+
result += f"\n{indent})"
|
|
562
|
+
return result
|
|
563
|
+
|
|
513
564
|
|
|
514
565
|
class CompactFormatter(ExactFormatter):
|
|
515
566
|
"""Compact formatter for minimal output size."""
|