kicad-sch-api 0.3.2__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 (39) hide show
  1. kicad_sch_api/__init__.py +2 -2
  2. kicad_sch_api/collections/__init__.py +21 -0
  3. kicad_sch_api/collections/base.py +296 -0
  4. kicad_sch_api/collections/components.py +422 -0
  5. kicad_sch_api/collections/junctions.py +378 -0
  6. kicad_sch_api/collections/labels.py +412 -0
  7. kicad_sch_api/collections/wires.py +407 -0
  8. kicad_sch_api/core/formatter.py +31 -0
  9. kicad_sch_api/core/labels.py +348 -0
  10. kicad_sch_api/core/nets.py +310 -0
  11. kicad_sch_api/core/no_connects.py +274 -0
  12. kicad_sch_api/core/parser.py +72 -0
  13. kicad_sch_api/core/schematic.py +185 -9
  14. kicad_sch_api/core/texts.py +343 -0
  15. kicad_sch_api/core/types.py +26 -0
  16. kicad_sch_api/geometry/__init__.py +26 -0
  17. kicad_sch_api/geometry/font_metrics.py +20 -0
  18. kicad_sch_api/geometry/symbol_bbox.py +589 -0
  19. kicad_sch_api/interfaces/__init__.py +17 -0
  20. kicad_sch_api/interfaces/parser.py +76 -0
  21. kicad_sch_api/interfaces/repository.py +70 -0
  22. kicad_sch_api/interfaces/resolver.py +117 -0
  23. kicad_sch_api/parsers/__init__.py +14 -0
  24. kicad_sch_api/parsers/base.py +148 -0
  25. kicad_sch_api/parsers/label_parser.py +254 -0
  26. kicad_sch_api/parsers/registry.py +153 -0
  27. kicad_sch_api/parsers/symbol_parser.py +227 -0
  28. kicad_sch_api/parsers/wire_parser.py +99 -0
  29. kicad_sch_api/symbols/__init__.py +18 -0
  30. kicad_sch_api/symbols/cache.py +470 -0
  31. kicad_sch_api/symbols/resolver.py +367 -0
  32. kicad_sch_api/symbols/validators.py +453 -0
  33. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/METADATA +1 -1
  34. kicad_sch_api-0.3.5.dist-info/RECORD +58 -0
  35. kicad_sch_api-0.3.2.dist-info/RECORD +0 -31
  36. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/WHEEL +0 -0
  37. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/entry_points.txt +0 -0
  38. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/licenses/LICENSE +0 -0
  39. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,470 @@
1
+ """
2
+ Symbol cache interface and implementation for KiCAD symbol libraries.
3
+
4
+ Provides high-performance caching with clear separation between cache
5
+ management and symbol resolution concerns.
6
+ """
7
+
8
+ import hashlib
9
+ import json
10
+ import logging
11
+ import os
12
+ import time
13
+ from abc import ABC, abstractmethod
14
+ from dataclasses import dataclass, field
15
+ from pathlib import Path
16
+ from typing import Any, Dict, List, Optional, Set, Tuple, Union
17
+
18
+ import sexpdata
19
+
20
+ from ..library.cache import SymbolDefinition, LibraryStats
21
+ from ..utils.validation import ValidationError
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class ISymbolCache(ABC):
27
+ """
28
+ Interface for symbol caching implementations.
29
+
30
+ Defines the contract for symbol caching without coupling to specific
31
+ inheritance resolution or validation logic.
32
+ """
33
+
34
+ @abstractmethod
35
+ def get_symbol(self, lib_id: str) -> Optional[SymbolDefinition]:
36
+ """
37
+ Get a symbol by library ID.
38
+
39
+ Args:
40
+ lib_id: Library identifier (e.g., "Device:R")
41
+
42
+ Returns:
43
+ Symbol definition if found, None otherwise
44
+ """
45
+ pass
46
+
47
+ @abstractmethod
48
+ def has_symbol(self, lib_id: str) -> bool:
49
+ """
50
+ Check if symbol exists in cache.
51
+
52
+ Args:
53
+ lib_id: Library identifier to check
54
+
55
+ Returns:
56
+ True if symbol exists in cache
57
+ """
58
+ pass
59
+
60
+ @abstractmethod
61
+ def add_library_path(self, library_path: Union[str, Path]) -> bool:
62
+ """
63
+ Add a library path to the cache.
64
+
65
+ Args:
66
+ library_path: Path to .kicad_sym file
67
+
68
+ Returns:
69
+ True if library was added successfully
70
+ """
71
+ pass
72
+
73
+ @abstractmethod
74
+ def get_library_symbols(self, library_name: str) -> List[str]:
75
+ """
76
+ Get all symbol IDs from a specific library.
77
+
78
+ Args:
79
+ library_name: Name of library
80
+
81
+ Returns:
82
+ List of symbol lib_ids from the library
83
+ """
84
+ pass
85
+
86
+ @abstractmethod
87
+ def clear_cache(self) -> None:
88
+ """Clear all cached symbols."""
89
+ pass
90
+
91
+ @abstractmethod
92
+ def get_cache_statistics(self) -> Dict[str, Any]:
93
+ """
94
+ Get cache performance statistics.
95
+
96
+ Returns:
97
+ Dictionary with cache statistics
98
+ """
99
+ pass
100
+
101
+
102
+ class SymbolCache(ISymbolCache):
103
+ """
104
+ High-performance symbol cache implementation.
105
+
106
+ Focuses purely on caching functionality without inheritance resolution,
107
+ which is handled by the SymbolResolver.
108
+ """
109
+
110
+ def __init__(self, cache_dir: Optional[Path] = None, enable_persistence: bool = True):
111
+ """
112
+ Initialize the symbol cache.
113
+
114
+ Args:
115
+ cache_dir: Directory to store cached symbol data
116
+ enable_persistence: Whether to persist cache to disk
117
+ """
118
+ self._symbols: Dict[str, SymbolDefinition] = {}
119
+ self._library_paths: Set[Path] = set()
120
+
121
+ # Cache configuration
122
+ self._cache_dir = cache_dir or Path.home() / ".cache" / "kicad-sch-api" / "symbols"
123
+ self._enable_persistence = enable_persistence
124
+
125
+ if enable_persistence:
126
+ self._cache_dir.mkdir(parents=True, exist_ok=True)
127
+
128
+ # Indexes for fast lookup
129
+ self._symbol_index: Dict[str, str] = {} # symbol_name -> lib_id
130
+ self._library_index: Dict[str, Path] = {} # library_name -> path
131
+ self._lib_stats: Dict[str, LibraryStats] = {}
132
+
133
+ # Performance tracking
134
+ self._cache_hits = 0
135
+ self._cache_misses = 0
136
+ self._total_load_time = 0.0
137
+
138
+ # Load persistent cache if available
139
+ self._index_file = self._cache_dir / "symbol_index.json" if enable_persistence else None
140
+ if enable_persistence:
141
+ self._load_persistent_index()
142
+
143
+ logger.info(f"Symbol cache initialized (persistence: {enable_persistence})")
144
+
145
+ def get_symbol(self, lib_id: str) -> Optional[SymbolDefinition]:
146
+ """
147
+ Get a symbol by library ID.
148
+
149
+ Note: This returns the raw symbol without inheritance resolution.
150
+ Use SymbolResolver for fully resolved symbols.
151
+ """
152
+ if lib_id in self._symbols:
153
+ self._cache_hits += 1
154
+ symbol = self._symbols[lib_id]
155
+ symbol.access_count += 1
156
+ symbol.last_accessed = time.time()
157
+ return symbol
158
+
159
+ self._cache_misses += 1
160
+
161
+ # Try to load from library
162
+ symbol = self._load_symbol_from_library(lib_id)
163
+ if symbol:
164
+ self._symbols[lib_id] = symbol
165
+
166
+ return symbol
167
+
168
+ def has_symbol(self, lib_id: str) -> bool:
169
+ """Check if symbol exists in cache."""
170
+ if lib_id in self._symbols:
171
+ return True
172
+
173
+ # Check if we can load it
174
+ return self._can_load_symbol(lib_id)
175
+
176
+ def add_library_path(self, library_path: Union[str, Path]) -> bool:
177
+ """Add a library path to the cache."""
178
+ library_path = Path(library_path)
179
+
180
+ if not library_path.exists():
181
+ logger.warning(f"Library file not found: {library_path}")
182
+ return False
183
+
184
+ if not library_path.suffix == ".kicad_sym":
185
+ logger.warning(f"Not a KiCAD symbol library: {library_path}")
186
+ return False
187
+
188
+ if library_path in self._library_paths:
189
+ logger.debug(f"Library already in cache: {library_path}")
190
+ return True
191
+
192
+ self._library_paths.add(library_path)
193
+ library_name = library_path.stem
194
+ self._library_index[library_name] = library_path
195
+
196
+ # Initialize library statistics
197
+ stat = library_path.stat()
198
+ self._lib_stats[library_name] = LibraryStats(
199
+ library_path=library_path,
200
+ file_size=stat.st_size,
201
+ last_modified=stat.st_mtime,
202
+ symbol_count=0 # Will be updated when library is loaded
203
+ )
204
+
205
+ logger.info(f"Added library: {library_name} ({library_path})")
206
+ return True
207
+
208
+ def get_library_symbols(self, library_name: str) -> List[str]:
209
+ """Get all symbol IDs from a specific library."""
210
+ if library_name not in self._library_index:
211
+ return []
212
+
213
+ # Load library if not already loaded
214
+ library_path = self._library_index[library_name]
215
+ symbols = []
216
+
217
+ try:
218
+ with open(library_path, "r", encoding="utf-8") as f:
219
+ content = f.read()
220
+
221
+ parsed = sexpdata.loads(content, true=None, false=None, nil=None)
222
+
223
+ # Extract symbol names from parsed data
224
+ for item in parsed[1:]: # Skip first item which is 'kicad_symbol_lib'
225
+ if isinstance(item, list) and len(item) > 1:
226
+ if item[0] == sexpdata.Symbol("symbol"):
227
+ symbol_name = str(item[1]).strip('"')
228
+ lib_id = f"{library_name}:{symbol_name}"
229
+ symbols.append(lib_id)
230
+
231
+ except Exception as e:
232
+ logger.error(f"Error loading symbols from {library_name}: {e}")
233
+
234
+ return symbols
235
+
236
+ def clear_cache(self) -> None:
237
+ """Clear all cached symbols."""
238
+ self._symbols.clear()
239
+ self._symbol_index.clear()
240
+ self._cache_hits = 0
241
+ self._cache_misses = 0
242
+ logger.info("Symbol cache cleared")
243
+
244
+ def get_cache_statistics(self) -> Dict[str, Any]:
245
+ """Get cache performance statistics."""
246
+ total_requests = self._cache_hits + self._cache_misses
247
+ hit_rate = (self._cache_hits / total_requests * 100) if total_requests > 0 else 0
248
+
249
+ return {
250
+ "symbols_cached": len(self._symbols),
251
+ "libraries_loaded": len(self._library_paths),
252
+ "cache_hits": self._cache_hits,
253
+ "cache_misses": self._cache_misses,
254
+ "hit_rate_percent": hit_rate,
255
+ "total_load_time": self._total_load_time,
256
+ "library_stats": {
257
+ name: {
258
+ "file_size": stats.file_size,
259
+ "symbols_count": stats.symbols_count,
260
+ "last_loaded": stats.last_loaded
261
+ }
262
+ for name, stats in self._lib_stats.items()
263
+ }
264
+ }
265
+
266
+ # Private methods for implementation details
267
+ def _load_symbol_from_library(self, lib_id: str) -> Optional[SymbolDefinition]:
268
+ """Load symbol from library file."""
269
+ if ":" not in lib_id:
270
+ logger.warning(f"Invalid lib_id format: {lib_id}")
271
+ return None
272
+
273
+ library_name, symbol_name = lib_id.split(":", 1)
274
+
275
+ if library_name not in self._library_index:
276
+ logger.debug(f"Library {library_name} not in cache")
277
+ return None
278
+
279
+ library_path = self._library_index[library_name]
280
+
281
+ try:
282
+ start_time = time.time()
283
+
284
+ with open(library_path, "r", encoding="utf-8") as f:
285
+ content = f.read()
286
+
287
+ parsed = sexpdata.loads(content, true=None, false=None, nil=None)
288
+ symbol_data = self._find_symbol_in_parsed_data(parsed, symbol_name)
289
+
290
+ if not symbol_data:
291
+ logger.debug(f"Symbol {symbol_name} not found in {library_name}")
292
+ return None
293
+
294
+ # Create symbol definition without inheritance resolution
295
+ symbol = self._create_symbol_definition(symbol_data, lib_id, library_name)
296
+
297
+ load_time = time.time() - start_time
298
+ self._total_load_time += load_time
299
+ symbol.load_time = load_time
300
+
301
+ logger.debug(f"Loaded symbol {lib_id} in {load_time:.3f}s")
302
+ return symbol
303
+
304
+ except Exception as e:
305
+ logger.error(f"Error loading symbol {lib_id}: {e}")
306
+ return None
307
+
308
+ def _find_symbol_in_parsed_data(self, parsed_data: List, symbol_name: str) -> Optional[List]:
309
+ """Find symbol data in parsed library content."""
310
+ for item in parsed_data[1:]: # Skip first item which is 'kicad_symbol_lib'
311
+ if isinstance(item, list) and len(item) > 1:
312
+ if item[0] == sexpdata.Symbol("symbol"):
313
+ name = str(item[1]).strip('"')
314
+ if name == symbol_name:
315
+ return item
316
+ return None
317
+
318
+ def _create_symbol_definition(
319
+ self,
320
+ symbol_data: List,
321
+ lib_id: str,
322
+ library_name: str
323
+ ) -> SymbolDefinition:
324
+ """Create SymbolDefinition from parsed symbol data."""
325
+ symbol_name = str(symbol_data[1]).strip('"')
326
+
327
+ # Extract basic symbol properties
328
+ properties = self._extract_symbol_properties(symbol_data)
329
+ pins = self._extract_symbol_pins(symbol_data)
330
+ graphic_elements = self._extract_graphic_elements(symbol_data)
331
+
332
+ # Check for extends directive
333
+ extends = self._check_extends_directive(symbol_data)
334
+
335
+ return SymbolDefinition(
336
+ lib_id=lib_id,
337
+ name=symbol_name,
338
+ library=library_name,
339
+ reference_prefix=properties.get("reference_prefix", "U"),
340
+ description=properties.get("description", ""),
341
+ keywords=properties.get("keywords", ""),
342
+ datasheet=properties.get("datasheet", ""),
343
+ pins=pins,
344
+ units=properties.get("units", 1),
345
+ power_symbol=properties.get("power_symbol", False),
346
+ graphic_elements=graphic_elements,
347
+ raw_kicad_data=symbol_data,
348
+ extends=extends # Store extends information for resolver
349
+ )
350
+
351
+ def _extract_symbol_properties(self, symbol_data: List) -> Dict[str, Any]:
352
+ """Extract symbol properties from symbol data."""
353
+ properties = {
354
+ "reference_prefix": "U",
355
+ "description": "",
356
+ "keywords": "",
357
+ "datasheet": "",
358
+ "units": 1,
359
+ "power_symbol": False
360
+ }
361
+
362
+ for item in symbol_data[1:]:
363
+ if isinstance(item, list) and len(item) >= 2:
364
+ key = str(item[0])
365
+ if key == "property":
366
+ prop_name = str(item[1]).strip('"')
367
+ prop_value = str(item[2]).strip('"') if len(item) > 2 else ""
368
+
369
+ if prop_name == "Reference":
370
+ # Extract prefix from reference like "R" from "R?"
371
+ ref = prop_value.rstrip("?")
372
+ if ref:
373
+ properties["reference_prefix"] = ref
374
+ elif prop_name == "ki_description":
375
+ properties["description"] = prop_value
376
+ elif prop_name == "ki_keywords":
377
+ properties["keywords"] = prop_value
378
+ elif prop_name == "ki_fp_filters":
379
+ properties["datasheet"] = prop_value
380
+
381
+ return properties
382
+
383
+ def _extract_symbol_pins(self, symbol_data: List) -> List:
384
+ """Extract pins from symbol data."""
385
+ # For now, return empty list - pin extraction would be implemented here
386
+ # This would parse pin definitions from the symbol units
387
+ return []
388
+
389
+ def _extract_graphic_elements(self, symbol_data: List) -> List[Dict[str, Any]]:
390
+ """Extract graphic elements from symbol data."""
391
+ # For now, return empty list - graphic element extraction would be implemented here
392
+ # This would parse rectangles, circles, arcs, etc. from symbol units
393
+ return []
394
+
395
+ def _check_extends_directive(self, symbol_data: List) -> Optional[str]:
396
+ """Check if symbol has extends directive and return parent symbol name."""
397
+ if not isinstance(symbol_data, list):
398
+ return None
399
+
400
+ for item in symbol_data[1:]:
401
+ if isinstance(item, list) and len(item) >= 2:
402
+ if str(item[0]) == "extends":
403
+ parent_name = str(item[1]).strip('"')
404
+ logger.debug(f"Found extends directive: {parent_name}")
405
+ return parent_name
406
+ return None
407
+
408
+ def _can_load_symbol(self, lib_id: str) -> bool:
409
+ """Check if symbol can be loaded without actually loading it."""
410
+ if ":" not in lib_id:
411
+ return False
412
+
413
+ library_name, _ = lib_id.split(":", 1)
414
+ return library_name in self._library_index
415
+
416
+ def _load_persistent_index(self) -> None:
417
+ """Load persistent index from disk."""
418
+ if not self._index_file or not self._index_file.exists():
419
+ return
420
+
421
+ try:
422
+ with open(self._index_file, "r") as f:
423
+ index_data = json.load(f)
424
+
425
+ # Restore symbol index
426
+ self._symbol_index = index_data.get("symbol_index", {})
427
+
428
+ # Restore library paths that still exist
429
+ for lib_path_str in index_data.get("library_paths", []):
430
+ lib_path = Path(lib_path_str)
431
+ if lib_path.exists():
432
+ self._library_paths.add(lib_path)
433
+ self._library_index[lib_path.stem] = lib_path
434
+
435
+ logger.debug(f"Loaded persistent index with {len(self._symbol_index)} symbols")
436
+
437
+ except Exception as e:
438
+ logger.warning(f"Failed to load persistent index: {e}")
439
+
440
+ def save_persistent_index(self) -> None:
441
+ """Save current index to disk."""
442
+ if not self._enable_persistence or not self._index_file:
443
+ return
444
+
445
+ try:
446
+ index_data = {
447
+ "symbol_index": self._symbol_index,
448
+ "library_paths": [str(path) for path in self._library_paths],
449
+ "created": time.time()
450
+ }
451
+
452
+ with open(self._index_file, "w") as f:
453
+ json.dump(index_data, f, indent=2)
454
+
455
+ logger.debug("Saved persistent index")
456
+
457
+ except Exception as e:
458
+ logger.warning(f"Failed to save persistent index: {e}")
459
+
460
+ def _find_symbol_in_parsed_data(self, parsed_data: List, symbol_name: str) -> Optional[List]:
461
+ """Find symbol data in parsed library data."""
462
+ if not isinstance(parsed_data, list):
463
+ return None
464
+
465
+ for item in parsed_data:
466
+ if isinstance(item, list) and len(item) >= 2:
467
+ if str(item[0]) == "symbol" and str(item[1]) == symbol_name:
468
+ return item
469
+
470
+ return None