kicad-sch-api 0.3.5__py3-none-any.whl → 0.4.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 (81) 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/collections/__init__.py +2 -2
  10. kicad_sch_api/collections/base.py +5 -7
  11. kicad_sch_api/collections/components.py +24 -12
  12. kicad_sch_api/collections/junctions.py +31 -43
  13. kicad_sch_api/collections/labels.py +19 -27
  14. kicad_sch_api/collections/wires.py +17 -18
  15. kicad_sch_api/core/collections/__init__.py +5 -0
  16. kicad_sch_api/core/collections/base.py +248 -0
  17. kicad_sch_api/core/component_bounds.py +5 -0
  18. kicad_sch_api/core/components.py +67 -45
  19. kicad_sch_api/core/config.py +85 -3
  20. kicad_sch_api/core/factories/__init__.py +5 -0
  21. kicad_sch_api/core/factories/element_factory.py +276 -0
  22. kicad_sch_api/core/formatter.py +3 -1
  23. kicad_sch_api/core/junctions.py +26 -75
  24. kicad_sch_api/core/labels.py +29 -53
  25. kicad_sch_api/core/managers/__init__.py +26 -0
  26. kicad_sch_api/core/managers/file_io.py +244 -0
  27. kicad_sch_api/core/managers/format_sync.py +501 -0
  28. kicad_sch_api/core/managers/graphics.py +579 -0
  29. kicad_sch_api/core/managers/metadata.py +269 -0
  30. kicad_sch_api/core/managers/sheet.py +454 -0
  31. kicad_sch_api/core/managers/text_elements.py +536 -0
  32. kicad_sch_api/core/managers/validation.py +475 -0
  33. kicad_sch_api/core/managers/wire.py +352 -0
  34. kicad_sch_api/core/nets.py +38 -43
  35. kicad_sch_api/core/no_connects.py +33 -55
  36. kicad_sch_api/core/parser.py +75 -1731
  37. kicad_sch_api/core/schematic.py +951 -1192
  38. kicad_sch_api/core/texts.py +28 -55
  39. kicad_sch_api/core/types.py +60 -22
  40. kicad_sch_api/core/wires.py +27 -75
  41. kicad_sch_api/geometry/font_metrics.py +3 -1
  42. kicad_sch_api/geometry/symbol_bbox.py +40 -21
  43. kicad_sch_api/interfaces/__init__.py +1 -1
  44. kicad_sch_api/interfaces/parser.py +1 -1
  45. kicad_sch_api/interfaces/repository.py +1 -1
  46. kicad_sch_api/interfaces/resolver.py +1 -1
  47. kicad_sch_api/parsers/__init__.py +2 -2
  48. kicad_sch_api/parsers/base.py +7 -10
  49. kicad_sch_api/parsers/elements/__init__.py +22 -0
  50. kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
  51. kicad_sch_api/parsers/elements/label_parser.py +194 -0
  52. kicad_sch_api/parsers/elements/library_parser.py +165 -0
  53. kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
  54. kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
  55. kicad_sch_api/parsers/elements/symbol_parser.py +313 -0
  56. kicad_sch_api/parsers/elements/text_parser.py +250 -0
  57. kicad_sch_api/parsers/elements/wire_parser.py +242 -0
  58. kicad_sch_api/parsers/registry.py +4 -2
  59. kicad_sch_api/parsers/utils.py +80 -0
  60. kicad_sch_api/symbols/__init__.py +1 -1
  61. kicad_sch_api/symbols/cache.py +9 -12
  62. kicad_sch_api/symbols/resolver.py +20 -26
  63. kicad_sch_api/symbols/validators.py +188 -137
  64. kicad_sch_api/validation/__init__.py +25 -0
  65. kicad_sch_api/validation/erc.py +171 -0
  66. kicad_sch_api/validation/erc_models.py +203 -0
  67. kicad_sch_api/validation/pin_matrix.py +243 -0
  68. kicad_sch_api/validation/validators.py +391 -0
  69. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/METADATA +17 -9
  70. kicad_sch_api-0.4.1.dist-info/RECORD +87 -0
  71. kicad_sch_api/core/manhattan_routing.py +0 -430
  72. kicad_sch_api/core/simple_manhattan.py +0 -228
  73. kicad_sch_api/core/wire_routing.py +0 -380
  74. kicad_sch_api/parsers/label_parser.py +0 -254
  75. kicad_sch_api/parsers/symbol_parser.py +0 -227
  76. kicad_sch_api/parsers/wire_parser.py +0 -99
  77. kicad_sch_api-0.3.5.dist-info/RECORD +0 -58
  78. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/WHEEL +0 -0
  79. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/entry_points.txt +0 -0
  80. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/licenses/LICENSE +0 -0
  81. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/top_level.txt +0 -0
@@ -9,55 +9,34 @@ import logging
9
9
  import uuid as uuid_module
10
10
  from typing import Any, Dict, List, Optional, Tuple, Union
11
11
 
12
+ from .collections import BaseCollection
12
13
  from .types import Junction, Point
13
14
 
14
15
  logger = logging.getLogger(__name__)
15
16
 
16
17
 
17
- class JunctionCollection:
18
+ class JunctionCollection(BaseCollection[Junction]):
18
19
  """
19
20
  Professional junction collection with enhanced management features.
20
21
 
22
+ Inherits from BaseCollection for standard operations and adds junction-specific
23
+ functionality.
24
+
21
25
  Features:
22
- - Fast UUID-based lookup and indexing
26
+ - Fast UUID-based lookup and indexing (inherited)
23
27
  - Position-based junction queries
24
- - Bulk operations for performance
28
+ - Bulk operations for performance (inherited)
25
29
  - Validation and conflict detection
26
30
  """
27
31
 
28
- def __init__(self, junctions: Optional[List[Junction]] = None):
32
+ def __init__(self, junctions: Optional[List[Junction]] = None) -> None:
29
33
  """
30
34
  Initialize junction collection.
31
35
 
32
36
  Args:
33
37
  junctions: Initial list of junctions
34
38
  """
35
- self._junctions: List[Junction] = junctions or []
36
- self._uuid_index: Dict[str, int] = {}
37
- self._modified = False
38
-
39
- # Build UUID index
40
- self._rebuild_index()
41
-
42
- logger.debug(f"JunctionCollection initialized with {len(self._junctions)} junctions")
43
-
44
- def _rebuild_index(self):
45
- """Rebuild UUID index for fast lookups."""
46
- self._uuid_index = {junction.uuid: i for i, junction in enumerate(self._junctions)}
47
-
48
- def __len__(self) -> int:
49
- """Number of junctions in collection."""
50
- return len(self._junctions)
51
-
52
- def __iter__(self):
53
- """Iterate over junctions."""
54
- return iter(self._junctions)
55
-
56
- def __getitem__(self, uuid: str) -> Junction:
57
- """Get junction by UUID."""
58
- if uuid not in self._uuid_index:
59
- raise KeyError(f"Junction with UUID '{uuid}' not found")
60
- return self._junctions[self._uuid_index[uuid]]
39
+ super().__init__(junctions, collection_name="junctions")
61
40
 
62
41
  def add(
63
42
  self,
@@ -94,35 +73,12 @@ class JunctionCollection:
94
73
  # Create junction
95
74
  junction = Junction(uuid=uuid, position=position, diameter=diameter, color=color)
96
75
 
97
- # Add to collection
98
- self._junctions.append(junction)
99
- self._uuid_index[uuid] = len(self._junctions) - 1
100
- self._modified = True
76
+ # Add to collection using base class method
77
+ self._add_item(junction)
101
78
 
102
79
  logger.debug(f"Added junction at {position}, UUID={uuid}")
103
80
  return uuid
104
81
 
105
- def remove(self, uuid: str) -> bool:
106
- """
107
- Remove junction by UUID.
108
-
109
- Args:
110
- uuid: Junction UUID to remove
111
-
112
- Returns:
113
- True if junction was removed, False if not found
114
- """
115
- if uuid not in self._uuid_index:
116
- return False
117
-
118
- index = self._uuid_index[uuid]
119
- del self._junctions[index]
120
- self._rebuild_index()
121
- self._modified = True
122
-
123
- logger.debug(f"Removed junction: {uuid}")
124
- return True
125
-
126
82
  def get_at_position(
127
83
  self, position: Union[Point, Tuple[float, float]], tolerance: float = 0.01
128
84
  ) -> Optional[Junction]:
@@ -139,7 +95,7 @@ class JunctionCollection:
139
95
  if isinstance(position, tuple):
140
96
  position = Point(position[0], position[1])
141
97
 
142
- for junction in self._junctions:
98
+ for junction in self._items:
143
99
  if junction.position.distance_to(position) <= tolerance:
144
100
  return junction
145
101
 
@@ -162,40 +118,35 @@ class JunctionCollection:
162
118
  point = Point(point[0], point[1])
163
119
 
164
120
  matching_junctions = []
165
- for junction in self._junctions:
121
+ for junction in self._items:
166
122
  if junction.position.distance_to(point) <= tolerance:
167
123
  matching_junctions.append(junction)
168
124
 
169
125
  return matching_junctions
170
126
 
171
127
  def get_statistics(self) -> Dict[str, Any]:
172
- """Get junction collection statistics."""
173
- if not self._junctions:
174
- return {"total_junctions": 0, "avg_diameter": 0, "positions": []}
128
+ """Get junction collection statistics (extends base statistics)."""
129
+ base_stats = super().get_statistics()
130
+ if not self._items:
131
+ return {**base_stats, "total_junctions": 0, "avg_diameter": 0, "positions": []}
175
132
 
176
- avg_diameter = sum(j.diameter for j in self._junctions) / len(self._junctions)
177
- positions = [(j.position.x, j.position.y) for j in self._junctions]
133
+ avg_diameter = sum(j.diameter for j in self._items) / len(self._items)
134
+ positions = [(j.position.x, j.position.y) for j in self._items]
178
135
 
179
136
  return {
180
- "total_junctions": len(self._junctions),
137
+ **base_stats,
138
+ "total_junctions": len(self._items),
181
139
  "avg_diameter": avg_diameter,
182
140
  "positions": positions,
183
- "unique_diameters": len(set(j.diameter for j in self._junctions)),
184
- "unique_colors": len(set(j.color for j in self._junctions)),
141
+ "unique_diameters": len(set(j.diameter for j in self._items)),
142
+ "unique_colors": len(set(j.color for j in self._items)),
185
143
  }
186
144
 
187
- def clear(self):
188
- """Remove all junctions from collection."""
189
- self._junctions.clear()
190
- self._uuid_index.clear()
191
- self._modified = True
192
- logger.debug("Cleared all junctions")
193
-
194
145
  @property
195
146
  def modified(self) -> bool:
196
147
  """Check if collection has been modified."""
197
- return self._modified
148
+ return self.is_modified()
198
149
 
199
- def mark_saved(self):
150
+ def mark_saved(self) -> None:
200
151
  """Mark collection as saved (reset modified flag)."""
201
- self._modified = False
152
+ self.reset_modified_flag()
@@ -10,7 +10,8 @@ import uuid
10
10
  from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
11
11
 
12
12
  from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
13
- from .types import Point, Label
13
+ from .collections import BaseCollection
14
+ from .types import Label, Point
14
15
 
15
16
  logger = logging.getLogger(__name__)
16
17
 
@@ -114,10 +115,13 @@ class LabelElement:
114
115
  return f"<Label '{self.text}' @ {self.position}>"
115
116
 
116
117
 
117
- class LabelCollection:
118
+ class LabelCollection(BaseCollection[LabelElement]):
118
119
  """
119
120
  Collection class for efficient label element management.
120
121
 
122
+ Inherits from BaseCollection for standard operations and adds label-specific
123
+ functionality including text-based indexing.
124
+
121
125
  Provides fast lookup, filtering, and bulk operations for schematic label elements.
122
126
  """
123
127
 
@@ -128,18 +132,17 @@ class LabelCollection:
128
132
  Args:
129
133
  labels: Initial list of label data
130
134
  """
131
- self._labels: List[LabelElement] = []
132
- self._uuid_index: Dict[str, LabelElement] = {}
135
+ # Initialize base collection
136
+ super().__init__([], collection_name="labels")
137
+
138
+ # Additional label-specific index
133
139
  self._text_index: Dict[str, List[LabelElement]] = {}
134
- self._modified = False
135
140
 
136
141
  # Add initial labels
137
142
  if labels:
138
143
  for label_data in labels:
139
144
  self._add_to_indexes(LabelElement(label_data, self))
140
145
 
141
- logger.debug(f"LabelCollection initialized with {len(self._labels)} labels")
142
-
143
146
  def add(
144
147
  self,
145
148
  text: str,
@@ -201,9 +204,7 @@ class LabelCollection:
201
204
  logger.debug(f"Added label: {label_element}")
202
205
  return label_element
203
206
 
204
- def get(self, label_uuid: str) -> Optional[LabelElement]:
205
- """Get label by UUID."""
206
- return self._uuid_index.get(label_uuid)
207
+ # get() method inherited from BaseCollection
207
208
 
208
209
  def get_by_text(self, text: str) -> List[LabelElement]:
209
210
  """Get all labels with the given text."""
@@ -219,13 +220,19 @@ class LabelCollection:
219
220
  Returns:
220
221
  True if label was removed, False if not found
221
222
  """
222
- label_element = self._uuid_index.get(label_uuid)
223
+ label_element = self.get(label_uuid)
223
224
  if not label_element:
224
225
  return False
225
226
 
226
- # Remove from indexes
227
- self._remove_from_indexes(label_element)
228
- self._mark_modified()
227
+ # Remove from text index
228
+ text = label_element.text
229
+ if text in self._text_index:
230
+ self._text_index[text].remove(label_element)
231
+ if not self._text_index[text]:
232
+ del self._text_index[text]
233
+
234
+ # Remove using base class method
235
+ super().remove(label_uuid)
229
236
 
230
237
  logger.debug(f"Removed label: {label_element}")
231
238
  return True
@@ -245,7 +252,7 @@ class LabelCollection:
245
252
  return self._text_index.get(text, []).copy()
246
253
  else:
247
254
  matches = []
248
- for label_element in self._labels:
255
+ for label_element in self._items:
249
256
  if text.lower() in label_element.text.lower():
250
257
  matches.append(label_element)
251
258
  return matches
@@ -260,7 +267,7 @@ class LabelCollection:
260
267
  Returns:
261
268
  List of labels matching predicate
262
269
  """
263
- return [label for label in self._labels if predicate(label)]
270
+ return [label for label in self._items if predicate(label)]
264
271
 
265
272
  def bulk_update(self, criteria: Callable[[LabelElement], bool], updates: Dict[str, Any]):
266
273
  """
@@ -271,7 +278,7 @@ class LabelCollection:
271
278
  updates: Dictionary of property updates
272
279
  """
273
280
  updated_count = 0
274
- for label_element in self._labels:
281
+ for label_element in self._items:
275
282
  if criteria(label_element):
276
283
  for prop, value in updates.items():
277
284
  if hasattr(label_element, prop):
@@ -284,15 +291,12 @@ class LabelCollection:
284
291
 
285
292
  def clear(self):
286
293
  """Remove all labels from collection."""
287
- self._labels.clear()
288
- self._uuid_index.clear()
289
294
  self._text_index.clear()
290
- self._mark_modified()
295
+ super().clear()
291
296
 
292
297
  def _add_to_indexes(self, label_element: LabelElement):
293
- """Add label to internal indexes."""
294
- self._labels.append(label_element)
295
- self._uuid_index[label_element.uuid] = label_element
298
+ """Add label to internal indexes (base + text index)."""
299
+ self._add_item(label_element)
296
300
 
297
301
  # Add to text index
298
302
  text = label_element.text
@@ -300,18 +304,6 @@ class LabelCollection:
300
304
  self._text_index[text] = []
301
305
  self._text_index[text].append(label_element)
302
306
 
303
- def _remove_from_indexes(self, label_element: LabelElement):
304
- """Remove label from internal indexes."""
305
- self._labels.remove(label_element)
306
- del self._uuid_index[label_element.uuid]
307
-
308
- # Remove from text index
309
- text = label_element.text
310
- if text in self._text_index:
311
- self._text_index[text].remove(label_element)
312
- if not self._text_index[text]:
313
- del self._text_index[text]
314
-
315
307
  def _update_text_index(self, old_text: str, label_element: LabelElement):
316
308
  """Update text index when label text changes."""
317
309
  # Remove from old text index
@@ -326,23 +318,7 @@ class LabelCollection:
326
318
  self._text_index[new_text] = []
327
319
  self._text_index[new_text].append(label_element)
328
320
 
329
- def _mark_modified(self):
330
- """Mark collection as modified."""
331
- self._modified = True
332
-
333
- # Collection interface methods
334
- def __len__(self) -> int:
335
- """Return number of labels."""
336
- return len(self._labels)
337
-
338
- def __iter__(self) -> Iterator[LabelElement]:
339
- """Iterate over labels."""
340
- return iter(self._labels)
341
-
342
- def __getitem__(self, index: int) -> LabelElement:
343
- """Get label by index."""
344
- return self._labels[index]
345
-
321
+ # Collection interface methods - __len__, __iter__, __getitem__ inherited from BaseCollection
346
322
  def __bool__(self) -> bool:
347
323
  """Return True if collection has labels."""
348
- return len(self._labels) > 0
324
+ return len(self._items) > 0
@@ -0,0 +1,26 @@
1
+ """
2
+ Schematic management modules for separating responsibilities.
3
+
4
+ This package contains specialized managers for different aspects of schematic
5
+ manipulation, enabling clean separation of concerns and better maintainability.
6
+ """
7
+
8
+ from .file_io import FileIOManager
9
+ from .format_sync import FormatSyncManager
10
+ from .graphics import GraphicsManager
11
+ from .metadata import MetadataManager
12
+ from .sheet import SheetManager
13
+ from .text_elements import TextElementManager
14
+ from .validation import ValidationManager
15
+ from .wire import WireManager
16
+
17
+ __all__ = [
18
+ "FileIOManager",
19
+ "FormatSyncManager",
20
+ "GraphicsManager",
21
+ "MetadataManager",
22
+ "SheetManager",
23
+ "TextElementManager",
24
+ "ValidationManager",
25
+ "WireManager",
26
+ ]
@@ -0,0 +1,244 @@
1
+ """
2
+ File I/O Manager for KiCAD schematic operations.
3
+
4
+ Handles all file system interactions including loading, saving, and backup operations
5
+ while maintaining exact format preservation.
6
+ """
7
+
8
+ import logging
9
+ import time
10
+ from pathlib import Path
11
+ from typing import Any, Dict, Optional, Union
12
+
13
+ from ...utils.validation import ValidationError
14
+ from ..config import config
15
+ from ..formatter import ExactFormatter
16
+ from ..parser import SExpressionParser
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class FileIOManager:
22
+ """
23
+ Manages file I/O operations for KiCAD schematics.
24
+
25
+ Responsible for:
26
+ - Loading schematic files with validation
27
+ - Saving with format preservation
28
+ - Creating backup files
29
+ - Managing file paths and metadata
30
+ """
31
+
32
+ def __init__(self):
33
+ """Initialize the FileIOManager."""
34
+ self._parser = SExpressionParser(preserve_format=True)
35
+ self._formatter = ExactFormatter()
36
+
37
+ def load_schematic(self, file_path: Union[str, Path]) -> Dict[str, Any]:
38
+ """
39
+ Load a KiCAD schematic file.
40
+
41
+ Args:
42
+ file_path: Path to .kicad_sch file
43
+
44
+ Returns:
45
+ Parsed schematic data
46
+
47
+ Raises:
48
+ FileNotFoundError: If file doesn't exist
49
+ ValidationError: If file is invalid or corrupted
50
+ """
51
+ start_time = time.time()
52
+ file_path = Path(file_path)
53
+
54
+ if not file_path.exists():
55
+ raise FileNotFoundError(f"Schematic file not found: {file_path}")
56
+
57
+ if not file_path.suffix == ".kicad_sch":
58
+ raise ValidationError(f"Not a KiCAD schematic file: {file_path}")
59
+
60
+ logger.info(f"Loading schematic: {file_path}")
61
+
62
+ try:
63
+ schematic_data = self._parser.parse_file(file_path)
64
+ load_time = time.time() - start_time
65
+ logger.info(f"Loaded schematic in {load_time:.3f}s")
66
+
67
+ return schematic_data
68
+
69
+ except Exception as e:
70
+ logger.error(f"Failed to load schematic {file_path}: {e}")
71
+ raise ValidationError(f"Invalid schematic file: {e}") from e
72
+
73
+ def save_schematic(
74
+ self,
75
+ schematic_data: Dict[str, Any],
76
+ file_path: Union[str, Path],
77
+ preserve_format: bool = True,
78
+ ) -> None:
79
+ """
80
+ Save schematic data to file.
81
+
82
+ Args:
83
+ schematic_data: Schematic data to save
84
+ file_path: Target file path
85
+ preserve_format: Whether to preserve exact formatting
86
+
87
+ Raises:
88
+ PermissionError: If file cannot be written
89
+ ValidationError: If data is invalid
90
+ """
91
+ start_time = time.time()
92
+ file_path = Path(file_path)
93
+
94
+ logger.info(f"Saving schematic: {file_path}")
95
+
96
+ try:
97
+ # Ensure parent directory exists
98
+ file_path.parent.mkdir(parents=True, exist_ok=True)
99
+
100
+ # Convert to S-expression format and save
101
+ sexp_data = self._parser._schematic_data_to_sexp(schematic_data)
102
+ formatted_content = self._formatter.format(sexp_data)
103
+
104
+ with open(file_path, "w", encoding="utf-8") as f:
105
+ f.write(formatted_content)
106
+
107
+ save_time = time.time() - start_time
108
+ logger.info(f"Saved schematic in {save_time:.3f}s")
109
+
110
+ except PermissionError as e:
111
+ logger.error(f"Permission denied saving to {file_path}: {e}")
112
+ raise
113
+ except Exception as e:
114
+ logger.error(f"Failed to save schematic to {file_path}: {e}")
115
+ raise ValidationError(f"Save failed: {e}") from e
116
+
117
+ def create_backup(self, file_path: Union[str, Path], suffix: str = ".backup") -> Path:
118
+ """
119
+ Create a backup copy of the schematic file.
120
+
121
+ Args:
122
+ file_path: Source file to backup
123
+ suffix: Backup file suffix
124
+
125
+ Returns:
126
+ Path to backup file
127
+
128
+ Raises:
129
+ FileNotFoundError: If source file doesn't exist
130
+ PermissionError: If backup cannot be created
131
+ """
132
+ file_path = Path(file_path)
133
+
134
+ if not file_path.exists():
135
+ raise FileNotFoundError(f"Cannot backup non-existent file: {file_path}")
136
+
137
+ # Create backup with timestamp if suffix doesn't include one
138
+ if suffix == ".backup":
139
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
140
+ backup_path = file_path.with_suffix(f".{timestamp}.backup")
141
+ else:
142
+ backup_path = file_path.with_suffix(f"{file_path.suffix}{suffix}")
143
+
144
+ try:
145
+ # Copy file content
146
+ backup_path.write_bytes(file_path.read_bytes())
147
+ logger.info(f"Created backup: {backup_path}")
148
+ return backup_path
149
+
150
+ except Exception as e:
151
+ logger.error(f"Failed to create backup {backup_path}: {e}")
152
+ raise PermissionError(f"Backup failed: {e}") from e
153
+
154
+ def validate_file_path(self, file_path: Union[str, Path]) -> Path:
155
+ """
156
+ Validate and normalize a file path for schematic operations.
157
+
158
+ Args:
159
+ file_path: Path to validate
160
+
161
+ Returns:
162
+ Normalized Path object
163
+
164
+ Raises:
165
+ ValidationError: If path is invalid
166
+ """
167
+ file_path = Path(file_path)
168
+
169
+ # Ensure .kicad_sch extension
170
+ if not file_path.suffix:
171
+ file_path = file_path.with_suffix(".kicad_sch")
172
+ elif file_path.suffix != ".kicad_sch":
173
+ raise ValidationError(f"Invalid schematic file extension: {file_path.suffix}")
174
+
175
+ # Validate path characters
176
+ try:
177
+ file_path.resolve()
178
+ except (OSError, ValueError) as e:
179
+ raise ValidationError(f"Invalid file path: {e}") from e
180
+
181
+ return file_path
182
+
183
+ def get_file_info(self, file_path: Union[str, Path]) -> Dict[str, Any]:
184
+ """
185
+ Get file system information about a schematic file.
186
+
187
+ Args:
188
+ file_path: Path to analyze
189
+
190
+ Returns:
191
+ Dictionary with file information
192
+
193
+ Raises:
194
+ FileNotFoundError: If file doesn't exist
195
+ """
196
+ file_path = Path(file_path)
197
+
198
+ if not file_path.exists():
199
+ raise FileNotFoundError(f"File not found: {file_path}")
200
+
201
+ stat = file_path.stat()
202
+
203
+ return {
204
+ "path": str(file_path.resolve()),
205
+ "size": stat.st_size,
206
+ "modified": stat.st_mtime,
207
+ "created": getattr(stat, "st_birthtime", stat.st_ctime),
208
+ "readable": file_path.is_file() and file_path.exists(),
209
+ "writable": file_path.parent.exists() and file_path.parent.is_dir(),
210
+ "extension": file_path.suffix,
211
+ }
212
+
213
+ def create_empty_schematic_data(self) -> Dict[str, Any]:
214
+ """
215
+ Create empty schematic data structure.
216
+
217
+ Returns:
218
+ Empty schematic data dictionary
219
+ """
220
+ return {
221
+ "kicad_sch": {
222
+ "version": 20230819,
223
+ "generator": config.file_format.generator_default,
224
+ "uuid": None, # Will be set by calling code
225
+ "paper": config.paper.default,
226
+ "lib_symbols": {},
227
+ "symbol": [],
228
+ "wire": [],
229
+ "junction": [],
230
+ "label": [],
231
+ "hierarchical_label": [],
232
+ "global_label": [],
233
+ "text": [],
234
+ "text_box": [],
235
+ "polyline": [],
236
+ "rectangle": [],
237
+ "circle": [],
238
+ "arc": [],
239
+ "image": [],
240
+ "sheet": [],
241
+ "sheet_instances": [],
242
+ "symbol_instances": [],
243
+ }
244
+ }