kicad-sch-api 0.3.4__py3-none-any.whl → 0.3.5__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 (34) hide show
  1. kicad_sch_api/collections/__init__.py +21 -0
  2. kicad_sch_api/collections/base.py +296 -0
  3. kicad_sch_api/collections/components.py +422 -0
  4. kicad_sch_api/collections/junctions.py +378 -0
  5. kicad_sch_api/collections/labels.py +412 -0
  6. kicad_sch_api/collections/wires.py +407 -0
  7. kicad_sch_api/core/labels.py +348 -0
  8. kicad_sch_api/core/nets.py +310 -0
  9. kicad_sch_api/core/no_connects.py +274 -0
  10. kicad_sch_api/core/schematic.py +136 -2
  11. kicad_sch_api/core/texts.py +343 -0
  12. kicad_sch_api/core/types.py +12 -0
  13. kicad_sch_api/geometry/symbol_bbox.py +26 -32
  14. kicad_sch_api/interfaces/__init__.py +17 -0
  15. kicad_sch_api/interfaces/parser.py +76 -0
  16. kicad_sch_api/interfaces/repository.py +70 -0
  17. kicad_sch_api/interfaces/resolver.py +117 -0
  18. kicad_sch_api/parsers/__init__.py +14 -0
  19. kicad_sch_api/parsers/base.py +148 -0
  20. kicad_sch_api/parsers/label_parser.py +254 -0
  21. kicad_sch_api/parsers/registry.py +153 -0
  22. kicad_sch_api/parsers/symbol_parser.py +227 -0
  23. kicad_sch_api/parsers/wire_parser.py +99 -0
  24. kicad_sch_api/symbols/__init__.py +18 -0
  25. kicad_sch_api/symbols/cache.py +470 -0
  26. kicad_sch_api/symbols/resolver.py +367 -0
  27. kicad_sch_api/symbols/validators.py +453 -0
  28. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.3.5.dist-info}/METADATA +1 -1
  29. kicad_sch_api-0.3.5.dist-info/RECORD +58 -0
  30. kicad_sch_api-0.3.4.dist-info/RECORD +0 -34
  31. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.3.5.dist-info}/WHEEL +0 -0
  32. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.3.5.dist-info}/entry_points.txt +0 -0
  33. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.3.5.dist-info}/licenses/LICENSE +0 -0
  34. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.3.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,70 @@
1
+ """
2
+ Repository interfaces for schematic data persistence.
3
+
4
+ These interfaces define the contract for loading and saving schematic data,
5
+ abstracting away the specific file format and storage mechanisms.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from pathlib import Path
10
+ from typing import Any, Dict, Protocol, Union
11
+
12
+
13
+ class ISchematicRepository(Protocol):
14
+ """Interface for schematic data persistence operations."""
15
+
16
+ def load(self, filepath: Union[str, Path]) -> Dict[str, Any]:
17
+ """
18
+ Load schematic data from a file.
19
+
20
+ Args:
21
+ filepath: Path to the schematic file
22
+
23
+ Returns:
24
+ Complete schematic data structure
25
+
26
+ Raises:
27
+ FileNotFoundError: If file doesn't exist
28
+ RepositoryError: If file cannot be loaded
29
+ """
30
+ ...
31
+
32
+ def save(self, data: Dict[str, Any], filepath: Union[str, Path]) -> None:
33
+ """
34
+ Save schematic data to a file.
35
+
36
+ Args:
37
+ data: Complete schematic data structure
38
+ filepath: Path where to save the file
39
+
40
+ Raises:
41
+ RepositoryError: If file cannot be saved
42
+ """
43
+ ...
44
+
45
+ def exists(self, filepath: Union[str, Path]) -> bool:
46
+ """
47
+ Check if a schematic file exists.
48
+
49
+ Args:
50
+ filepath: Path to check
51
+
52
+ Returns:
53
+ True if file exists and is accessible
54
+ """
55
+ ...
56
+
57
+ def validate_format(self, filepath: Union[str, Path]) -> bool:
58
+ """
59
+ Validate that a file is in the correct format.
60
+
61
+ Args:
62
+ filepath: Path to the file to validate
63
+
64
+ Returns:
65
+ True if file format is valid
66
+
67
+ Raises:
68
+ FileNotFoundError: If file doesn't exist
69
+ """
70
+ ...
@@ -0,0 +1,117 @@
1
+ """
2
+ Symbol resolver interfaces for symbol library operations.
3
+
4
+ These interfaces define the contract for resolving symbols from libraries,
5
+ handling inheritance chains, and managing symbol caching.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from typing import Any, Dict, List, Optional, Protocol
10
+
11
+
12
+ class ISymbolResolver(Protocol):
13
+ """Interface for symbol resolution and inheritance operations."""
14
+
15
+ def resolve_symbol(self, lib_id: str) -> Optional[Dict[str, Any]]:
16
+ """
17
+ Resolve a symbol with full inheritance chain applied.
18
+
19
+ Args:
20
+ lib_id: Library identifier (e.g., "Device:R")
21
+
22
+ Returns:
23
+ Fully resolved symbol data with inheritance applied,
24
+ or None if symbol not found
25
+
26
+ Raises:
27
+ SymbolError: If symbol resolution fails
28
+ """
29
+ ...
30
+
31
+ def get_symbol_raw(self, lib_id: str) -> Optional[Dict[str, Any]]:
32
+ """
33
+ Get raw symbol data without inheritance resolution.
34
+
35
+ Args:
36
+ lib_id: Library identifier
37
+
38
+ Returns:
39
+ Raw symbol data as stored in library,
40
+ or None if symbol not found
41
+ """
42
+ ...
43
+
44
+ def resolve_inheritance_chain(self, lib_id: str) -> List[str]:
45
+ """
46
+ Get the complete inheritance chain for a symbol.
47
+
48
+ Args:
49
+ lib_id: Library identifier
50
+
51
+ Returns:
52
+ List of lib_ids in inheritance order (base to derived)
53
+
54
+ Raises:
55
+ SymbolError: If circular inheritance detected
56
+ """
57
+ ...
58
+
59
+ def is_symbol_available(self, lib_id: str) -> bool:
60
+ """
61
+ Check if a symbol is available in the libraries.
62
+
63
+ Args:
64
+ lib_id: Library identifier
65
+
66
+ Returns:
67
+ True if symbol exists and can be resolved
68
+ """
69
+ ...
70
+
71
+ def clear_cache(self) -> None:
72
+ """Clear any cached symbol data."""
73
+ ...
74
+
75
+
76
+ class ISymbolCache(Protocol):
77
+ """Interface for symbol library caching operations."""
78
+
79
+ def get_symbol(self, lib_id: str) -> Optional[Dict[str, Any]]:
80
+ """
81
+ Get a symbol from cache or load from library.
82
+
83
+ Args:
84
+ lib_id: Library identifier
85
+
86
+ Returns:
87
+ Symbol data or None if not found
88
+ """
89
+ ...
90
+
91
+ def cache_symbol(self, lib_id: str, symbol_data: Dict[str, Any]) -> None:
92
+ """
93
+ Cache symbol data.
94
+
95
+ Args:
96
+ lib_id: Library identifier
97
+ symbol_data: Symbol data to cache
98
+ """
99
+ ...
100
+
101
+ def invalidate(self, lib_id: Optional[str] = None) -> None:
102
+ """
103
+ Invalidate cache entries.
104
+
105
+ Args:
106
+ lib_id: Specific symbol to invalidate, or None for all
107
+ """
108
+ ...
109
+
110
+ def get_cache_stats(self) -> Dict[str, Any]:
111
+ """
112
+ Get cache statistics.
113
+
114
+ Returns:
115
+ Dictionary with cache statistics (hits, misses, size, etc.)
116
+ """
117
+ ...
@@ -0,0 +1,14 @@
1
+ """
2
+ Modular S-expression parsers for KiCAD elements.
3
+
4
+ This package provides specialized parsers for different types of KiCAD
5
+ S-expression elements, organized by responsibility and testable in isolation.
6
+ """
7
+
8
+ from .registry import ElementParserRegistry
9
+ from .base import BaseElementParser
10
+
11
+ __all__ = [
12
+ "ElementParserRegistry",
13
+ "BaseElementParser",
14
+ ]
@@ -0,0 +1,148 @@
1
+ """
2
+ Base parser implementation for S-expression elements.
3
+
4
+ Provides common functionality and utilities for all element parsers.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ from ..interfaces.parser import IElementParser
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class BaseElementParser(IElementParser):
16
+ """Base implementation for S-expression element parsers."""
17
+
18
+ def __init__(self, element_type: str):
19
+ """
20
+ Initialize base parser.
21
+
22
+ Args:
23
+ element_type: The S-expression element type this parser handles
24
+ """
25
+ self.element_type = element_type
26
+ self._logger = logger.getChild(self.__class__.__name__)
27
+
28
+ def can_parse(self, element: List[Any]) -> bool:
29
+ """Check if this parser can handle the given element type."""
30
+ if not element or not isinstance(element, list):
31
+ return False
32
+
33
+ element_type = element[0] if element else None
34
+ # Convert sexpdata.Symbol to string for comparison
35
+ element_type_str = str(element_type) if element_type else None
36
+ return element_type_str == self.element_type
37
+
38
+ def parse(self, element: List[Any]) -> Optional[Dict[str, Any]]:
39
+ """
40
+ Parse an S-expression element with error handling.
41
+
42
+ This method provides common error handling and validation,
43
+ then delegates to the specific parse_element implementation.
44
+ """
45
+ if not self.can_parse(element):
46
+ return None
47
+
48
+ try:
49
+ result = self.parse_element(element)
50
+ if result is not None:
51
+ self._logger.debug(f"Successfully parsed {self.element_type} element")
52
+ return result
53
+ except Exception as e:
54
+ self._logger.error(f"Failed to parse {self.element_type} element: {e}")
55
+ return None
56
+
57
+ def parse_element(self, element: List[Any]) -> Optional[Dict[str, Any]]:
58
+ """
59
+ Parse the specific element type.
60
+
61
+ This method should be implemented by subclasses to handle
62
+ the specific parsing logic for their element type.
63
+
64
+ Args:
65
+ element: S-expression element to parse
66
+
67
+ Returns:
68
+ Parsed element data or None if parsing failed
69
+
70
+ Raises:
71
+ NotImplementedError: If not implemented by subclass
72
+ """
73
+ raise NotImplementedError(f"parse_element not implemented for {self.element_type}")
74
+
75
+ def _extract_position(self, element: List[Any]) -> Optional[Dict[str, float]]:
76
+ """
77
+ Extract position information from common S-expression formats.
78
+
79
+ Many KiCAD elements have position info in formats like:
80
+ (at x y [angle])
81
+
82
+ Args:
83
+ element: S-expression sub-element containing position
84
+
85
+ Returns:
86
+ Dictionary with x, y, and optionally angle, or None if not found
87
+ """
88
+ if not isinstance(element, list) or len(element) < 3:
89
+ return None
90
+
91
+ if element[0] != "at":
92
+ return None
93
+
94
+ try:
95
+ result = {
96
+ "x": float(element[1]),
97
+ "y": float(element[2])
98
+ }
99
+
100
+ # Optional angle parameter
101
+ if len(element) > 3:
102
+ result["angle"] = float(element[3])
103
+
104
+ return result
105
+ except (ValueError, IndexError):
106
+ return None
107
+
108
+ def _extract_property_list(self, elements: List[Any], property_name: str) -> List[Dict[str, Any]]:
109
+ """
110
+ Extract all instances of a property from a list of elements.
111
+
112
+ Args:
113
+ elements: List of S-expression elements
114
+ property_name: Name of property to extract
115
+
116
+ Returns:
117
+ List of parsed property dictionaries
118
+ """
119
+ properties = []
120
+ for element in elements:
121
+ if (isinstance(element, list) and
122
+ len(element) > 0 and
123
+ element[0] == property_name):
124
+ prop = self._parse_property_element(element)
125
+ if prop:
126
+ properties.append(prop)
127
+ return properties
128
+
129
+ def _parse_property_element(self, element: List[Any]) -> Optional[Dict[str, Any]]:
130
+ """
131
+ Parse a generic property element.
132
+
133
+ Override this method in subclasses for property-specific parsing.
134
+
135
+ Args:
136
+ element: S-expression property element
137
+
138
+ Returns:
139
+ Parsed property data or None
140
+ """
141
+ if len(element) < 2:
142
+ return None
143
+
144
+ return {
145
+ "type": element[0],
146
+ "value": element[1] if len(element) > 1 else None,
147
+ "raw": element
148
+ }
@@ -0,0 +1,254 @@
1
+ """
2
+ Label element parser for S-expression label definitions.
3
+
4
+ Handles parsing of text labels, hierarchical labels, and other text elements.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ import sexpdata
11
+
12
+ from .base import BaseElementParser
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class LabelParser(BaseElementParser):
18
+ """Parser for label S-expression elements."""
19
+
20
+ def __init__(self):
21
+ """Initialize label parser."""
22
+ super().__init__("label")
23
+
24
+ def parse_element(self, element: List[Any]) -> Optional[Dict[str, Any]]:
25
+ """
26
+ Parse a label S-expression element.
27
+
28
+ Expected format:
29
+ (label "text" (at x y angle) (effects (font (size s s))))
30
+
31
+ Args:
32
+ element: Label S-expression element
33
+
34
+ Returns:
35
+ Parsed label data with text, position, and formatting
36
+ """
37
+ if len(element) < 2:
38
+ return None
39
+
40
+ label_data = {
41
+ "text": str(element[1]),
42
+ "position": {"x": 0, "y": 0, "angle": 0},
43
+ "effects": {
44
+ "font_size": 1.27,
45
+ "font_thickness": 0.15,
46
+ "bold": False,
47
+ "italic": False,
48
+ "hide": False,
49
+ "justify": []
50
+ },
51
+ "uuid": None
52
+ }
53
+
54
+ for elem in element[2:]:
55
+ if not isinstance(elem, list):
56
+ continue
57
+
58
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
59
+
60
+ if elem_type == "at":
61
+ self._parse_position(elem, label_data)
62
+ elif elem_type == "effects":
63
+ label_data["effects"] = self._parse_effects(elem)
64
+ elif elem_type == "uuid":
65
+ label_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
66
+
67
+ return label_data
68
+
69
+ def _parse_position(self, at_element: List[Any], label_data: Dict[str, Any]) -> None:
70
+ """
71
+ Parse position from at element.
72
+
73
+ Args:
74
+ at_element: (at x y [angle])
75
+ label_data: Label data dictionary to update
76
+ """
77
+ try:
78
+ if len(at_element) >= 3:
79
+ label_data["position"]["x"] = float(at_element[1])
80
+ label_data["position"]["y"] = float(at_element[2])
81
+ if len(at_element) >= 4:
82
+ label_data["position"]["angle"] = float(at_element[3])
83
+ except (ValueError, IndexError) as e:
84
+ self._logger.warning(f"Invalid position coordinates: {at_element}, error: {e}")
85
+
86
+ def _parse_effects(self, effects_element: List[Any]) -> Dict[str, Any]:
87
+ """
88
+ Parse effects element for text formatting.
89
+
90
+ Args:
91
+ effects_element: (effects (font ...) (justify ...) ...)
92
+
93
+ Returns:
94
+ Parsed effects data
95
+ """
96
+ effects = {
97
+ "font_size": 1.27,
98
+ "font_thickness": 0.15,
99
+ "bold": False,
100
+ "italic": False,
101
+ "hide": False,
102
+ "justify": []
103
+ }
104
+
105
+ for elem in effects_element[1:]:
106
+ if isinstance(elem, list) and len(elem) > 0:
107
+ elem_type = str(elem[0])
108
+ if elem_type == "font":
109
+ self._parse_font(elem, effects)
110
+ elif elem_type == "justify":
111
+ effects["justify"] = [str(j) for j in elem[1:]]
112
+ elif elem_type == "hide":
113
+ effects["hide"] = True
114
+
115
+ return effects
116
+
117
+ def _parse_font(self, font_element: List[Any], effects: Dict[str, Any]) -> None:
118
+ """
119
+ Parse font element within effects.
120
+
121
+ Args:
122
+ font_element: (font (size w h) (thickness t) ...)
123
+ effects: Effects dictionary to update
124
+ """
125
+ for elem in font_element[1:]:
126
+ if isinstance(elem, list) and len(elem) > 0:
127
+ elem_type = str(elem[0])
128
+ if elem_type == "size":
129
+ try:
130
+ if len(elem) >= 3:
131
+ # Usually (size width height), use width for font_size
132
+ effects["font_size"] = float(elem[1])
133
+ except (ValueError, IndexError):
134
+ pass
135
+ elif elem_type == "thickness":
136
+ try:
137
+ effects["font_thickness"] = float(elem[1])
138
+ except (ValueError, IndexError):
139
+ pass
140
+ elif elem_type == "bold":
141
+ effects["bold"] = True
142
+ elif elem_type == "italic":
143
+ effects["italic"] = True
144
+
145
+
146
+ class HierarchicalLabelParser(BaseElementParser):
147
+ """Parser for hierarchical label S-expression elements."""
148
+
149
+ def __init__(self):
150
+ """Initialize hierarchical label parser."""
151
+ super().__init__("hierarchical_label")
152
+
153
+ def parse_element(self, element: List[Any]) -> Optional[Dict[str, Any]]:
154
+ """
155
+ Parse a hierarchical label S-expression element.
156
+
157
+ Expected format:
158
+ (hierarchical_label "text" (shape input) (at x y angle) ...)
159
+
160
+ Args:
161
+ element: Hierarchical label S-expression element
162
+
163
+ Returns:
164
+ Parsed hierarchical label data
165
+ """
166
+ if len(element) < 2:
167
+ return None
168
+
169
+ label_data = {
170
+ "text": str(element[1]),
171
+ "shape": "input", # Default shape
172
+ "position": {"x": 0, "y": 0, "angle": 0},
173
+ "effects": {
174
+ "font_size": 1.27,
175
+ "font_thickness": 0.15,
176
+ "bold": False,
177
+ "italic": False,
178
+ "hide": False,
179
+ "justify": []
180
+ },
181
+ "uuid": None
182
+ }
183
+
184
+ for elem in element[2:]:
185
+ if not isinstance(elem, list):
186
+ continue
187
+
188
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
189
+
190
+ if elem_type == "shape":
191
+ label_data["shape"] = str(elem[1]) if len(elem) > 1 else "input"
192
+ elif elem_type == "at":
193
+ self._parse_position(elem, label_data)
194
+ elif elem_type == "effects":
195
+ label_data["effects"] = self._parse_effects(elem)
196
+ elif elem_type == "uuid":
197
+ label_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
198
+
199
+ return label_data
200
+
201
+ def _parse_position(self, at_element: List[Any], label_data: Dict[str, Any]) -> None:
202
+ """Parse position from at element."""
203
+ try:
204
+ if len(at_element) >= 3:
205
+ label_data["position"]["x"] = float(at_element[1])
206
+ label_data["position"]["y"] = float(at_element[2])
207
+ if len(at_element) >= 4:
208
+ label_data["position"]["angle"] = float(at_element[3])
209
+ except (ValueError, IndexError) as e:
210
+ self._logger.warning(f"Invalid position coordinates: {at_element}, error: {e}")
211
+
212
+ def _parse_effects(self, effects_element: List[Any]) -> Dict[str, Any]:
213
+ """Parse effects element for text formatting."""
214
+ effects = {
215
+ "font_size": 1.27,
216
+ "font_thickness": 0.15,
217
+ "bold": False,
218
+ "italic": False,
219
+ "hide": False,
220
+ "justify": []
221
+ }
222
+
223
+ for elem in effects_element[1:]:
224
+ if isinstance(elem, list) and len(elem) > 0:
225
+ elem_type = str(elem[0])
226
+ if elem_type == "font":
227
+ self._parse_font(elem, effects)
228
+ elif elem_type == "justify":
229
+ effects["justify"] = [str(j) for j in elem[1:]]
230
+ elif elem_type == "hide":
231
+ effects["hide"] = True
232
+
233
+ return effects
234
+
235
+ def _parse_font(self, font_element: List[Any], effects: Dict[str, Any]) -> None:
236
+ """Parse font element within effects."""
237
+ for elem in font_element[1:]:
238
+ if isinstance(elem, list) and len(elem) > 0:
239
+ elem_type = str(elem[0])
240
+ if elem_type == "size":
241
+ try:
242
+ if len(elem) >= 3:
243
+ effects["font_size"] = float(elem[1])
244
+ except (ValueError, IndexError):
245
+ pass
246
+ elif elem_type == "thickness":
247
+ try:
248
+ effects["font_thickness"] = float(elem[1])
249
+ except (ValueError, IndexError):
250
+ pass
251
+ elif elem_type == "bold":
252
+ effects["bold"] = True
253
+ elif elem_type == "italic":
254
+ effects["italic"] = True