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.
Files changed (112) hide show
  1. kicad_sch_api/__init__.py +68 -3
  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/kicad_to_python.py +169 -0
  8. kicad_sch_api/cli/netlist.py +94 -0
  9. kicad_sch_api/cli/types.py +43 -0
  10. kicad_sch_api/collections/__init__.py +36 -0
  11. kicad_sch_api/collections/base.py +604 -0
  12. kicad_sch_api/collections/components.py +1623 -0
  13. kicad_sch_api/collections/junctions.py +206 -0
  14. kicad_sch_api/collections/labels.py +508 -0
  15. kicad_sch_api/collections/wires.py +292 -0
  16. kicad_sch_api/core/__init__.py +37 -2
  17. kicad_sch_api/core/collections/__init__.py +5 -0
  18. kicad_sch_api/core/collections/base.py +248 -0
  19. kicad_sch_api/core/component_bounds.py +34 -7
  20. kicad_sch_api/core/components.py +213 -52
  21. kicad_sch_api/core/config.py +110 -15
  22. kicad_sch_api/core/connectivity.py +692 -0
  23. kicad_sch_api/core/exceptions.py +175 -0
  24. kicad_sch_api/core/factories/__init__.py +5 -0
  25. kicad_sch_api/core/factories/element_factory.py +278 -0
  26. kicad_sch_api/core/formatter.py +60 -9
  27. kicad_sch_api/core/geometry.py +94 -5
  28. kicad_sch_api/core/junctions.py +26 -75
  29. kicad_sch_api/core/labels.py +324 -0
  30. kicad_sch_api/core/managers/__init__.py +30 -0
  31. kicad_sch_api/core/managers/base.py +76 -0
  32. kicad_sch_api/core/managers/file_io.py +246 -0
  33. kicad_sch_api/core/managers/format_sync.py +502 -0
  34. kicad_sch_api/core/managers/graphics.py +580 -0
  35. kicad_sch_api/core/managers/hierarchy.py +661 -0
  36. kicad_sch_api/core/managers/metadata.py +271 -0
  37. kicad_sch_api/core/managers/sheet.py +492 -0
  38. kicad_sch_api/core/managers/text_elements.py +537 -0
  39. kicad_sch_api/core/managers/validation.py +476 -0
  40. kicad_sch_api/core/managers/wire.py +410 -0
  41. kicad_sch_api/core/nets.py +305 -0
  42. kicad_sch_api/core/no_connects.py +252 -0
  43. kicad_sch_api/core/parser.py +194 -970
  44. kicad_sch_api/core/parsing_utils.py +63 -0
  45. kicad_sch_api/core/pin_utils.py +103 -9
  46. kicad_sch_api/core/schematic.py +1328 -1079
  47. kicad_sch_api/core/texts.py +316 -0
  48. kicad_sch_api/core/types.py +159 -23
  49. kicad_sch_api/core/wires.py +27 -75
  50. kicad_sch_api/exporters/__init__.py +10 -0
  51. kicad_sch_api/exporters/python_generator.py +610 -0
  52. kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
  53. kicad_sch_api/geometry/__init__.py +38 -0
  54. kicad_sch_api/geometry/font_metrics.py +22 -0
  55. kicad_sch_api/geometry/routing.py +211 -0
  56. kicad_sch_api/geometry/symbol_bbox.py +608 -0
  57. kicad_sch_api/interfaces/__init__.py +17 -0
  58. kicad_sch_api/interfaces/parser.py +76 -0
  59. kicad_sch_api/interfaces/repository.py +70 -0
  60. kicad_sch_api/interfaces/resolver.py +117 -0
  61. kicad_sch_api/parsers/__init__.py +14 -0
  62. kicad_sch_api/parsers/base.py +145 -0
  63. kicad_sch_api/parsers/elements/__init__.py +22 -0
  64. kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
  65. kicad_sch_api/parsers/elements/label_parser.py +216 -0
  66. kicad_sch_api/parsers/elements/library_parser.py +165 -0
  67. kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
  68. kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
  69. kicad_sch_api/parsers/elements/symbol_parser.py +485 -0
  70. kicad_sch_api/parsers/elements/text_parser.py +250 -0
  71. kicad_sch_api/parsers/elements/wire_parser.py +242 -0
  72. kicad_sch_api/parsers/registry.py +155 -0
  73. kicad_sch_api/parsers/utils.py +80 -0
  74. kicad_sch_api/symbols/__init__.py +18 -0
  75. kicad_sch_api/symbols/cache.py +467 -0
  76. kicad_sch_api/symbols/resolver.py +361 -0
  77. kicad_sch_api/symbols/validators.py +504 -0
  78. kicad_sch_api/utils/logging.py +555 -0
  79. kicad_sch_api/utils/logging_decorators.py +587 -0
  80. kicad_sch_api/utils/validation.py +16 -22
  81. kicad_sch_api/validation/__init__.py +25 -0
  82. kicad_sch_api/validation/erc.py +171 -0
  83. kicad_sch_api/validation/erc_models.py +203 -0
  84. kicad_sch_api/validation/pin_matrix.py +243 -0
  85. kicad_sch_api/validation/validators.py +391 -0
  86. kicad_sch_api/wrappers/__init__.py +14 -0
  87. kicad_sch_api/wrappers/base.py +89 -0
  88. kicad_sch_api/wrappers/wire.py +198 -0
  89. kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
  90. kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
  91. kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
  92. {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
  93. mcp_server/__init__.py +34 -0
  94. mcp_server/example_logging_integration.py +506 -0
  95. mcp_server/models.py +252 -0
  96. mcp_server/server.py +357 -0
  97. mcp_server/tools/__init__.py +32 -0
  98. mcp_server/tools/component_tools.py +516 -0
  99. mcp_server/tools/connectivity_tools.py +532 -0
  100. mcp_server/tools/consolidated_tools.py +1216 -0
  101. mcp_server/tools/pin_discovery.py +333 -0
  102. mcp_server/utils/__init__.py +38 -0
  103. mcp_server/utils/logging.py +127 -0
  104. mcp_server/utils.py +36 -0
  105. kicad_sch_api/core/manhattan_routing.py +0 -430
  106. kicad_sch_api/core/simple_manhattan.py +0 -228
  107. kicad_sch_api/core/wire_routing.py +0 -380
  108. kicad_sch_api-0.3.0.dist-info/METADATA +0 -483
  109. kicad_sch_api-0.3.0.dist-info/RECORD +0 -31
  110. kicad_sch_api-0.3.0.dist-info/entry_points.txt +0 -2
  111. {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
  112. {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,587 @@
1
+ """Advanced decorators for operation logging and performance monitoring.
2
+
3
+ This module provides decorators for:
4
+ - Operation entry/exit logging with context
5
+ - Function timing and performance tracking
6
+ - Exception logging with context
7
+ - Context managers for multi-step operations
8
+ - Retry logic with logging
9
+ """
10
+
11
+ import logging
12
+ import functools
13
+ import time
14
+ import traceback
15
+ from contextlib import contextmanager
16
+ from typing import Any, Callable, Optional, TypeVar, List, Dict
17
+ from pathlib import Path
18
+
19
+ # Type definitions
20
+ T = TypeVar("T")
21
+
22
+
23
+ def log_operation(
24
+ operation_name: Optional[str] = None,
25
+ level: int = logging.INFO,
26
+ include_args: bool = False,
27
+ include_result: bool = False,
28
+ ) -> Callable:
29
+ """Decorator for logging function entry and exit.
30
+
31
+ Logs when a function starts and completes, with optional argument/result logging.
32
+
33
+ Args:
34
+ operation_name: Name for logs (default: function name)
35
+ level: Log level (default: INFO)
36
+ include_args: Include function arguments in logs (default: False)
37
+ include_result: Include return value in logs (default: False)
38
+
39
+ Example:
40
+ @log_operation(operation_name="create_component")
41
+ def add_resistor(schematic, value):
42
+ # Logs: "START: create_component"
43
+ # ... code ...
44
+ # Logs: "COMPLETE: create_component"
45
+ return resistor
46
+
47
+ @log_operation(include_args=True, include_result=True)
48
+ def calculate_position(x, y):
49
+ # Logs: "START: calculate_position args=x, y=10"
50
+ # ... code ...
51
+ # Logs: "COMPLETE: calculate_position result=(100, 200)"
52
+ return (100, 200)
53
+ """
54
+
55
+ def decorator(func: Callable[..., T]) -> Callable[..., T]:
56
+ op_name = operation_name or func.__name__
57
+ logger = logging.getLogger(func.__module__)
58
+
59
+ @functools.wraps(func)
60
+ def wrapper(*args: Any, **kwargs: Any) -> T:
61
+ # Log entry
62
+ entry_msg = f"START: {op_name}"
63
+ if include_args and (args or kwargs):
64
+ arg_strs = [repr(arg) for arg in args[:3]] # Limit to 3 args
65
+ kwarg_strs = [f"{k}={v!r}" for k, v in list(kwargs.items())[:3]]
66
+ all_args = ", ".join(arg_strs + kwarg_strs)
67
+ entry_msg += f" ({all_args})"
68
+
69
+ logger.log(level, entry_msg)
70
+
71
+ # Execute function
72
+ start = time.time()
73
+ try:
74
+ result = func(*args, **kwargs)
75
+ elapsed = (time.time() - start) * 1000
76
+
77
+ # Log exit
78
+ exit_msg = f"COMPLETE: {op_name} ({elapsed:.2f}ms)"
79
+ if include_result:
80
+ exit_msg += f" result={result!r}"
81
+
82
+ logger.log(level, exit_msg)
83
+ return result
84
+
85
+ except Exception as e:
86
+ elapsed = (time.time() - start) * 1000
87
+ logger.error(
88
+ f"FAILED: {op_name} ({elapsed:.2f}ms): {e.__class__.__name__}: {e}",
89
+ exc_info=True,
90
+ )
91
+ raise
92
+
93
+ return wrapper
94
+
95
+ return decorator
96
+
97
+
98
+ def log_timing(
99
+ level: int = logging.DEBUG,
100
+ threshold_ms: Optional[float] = None,
101
+ ) -> Callable:
102
+ """Decorator for performance tracking with optional slow operation alerts.
103
+
104
+ Logs execution time. If threshold is set, logs WARNING for slow operations.
105
+
106
+ Args:
107
+ level: Log level for normal operations (default: DEBUG)
108
+ threshold_ms: Alert if slower than this many milliseconds (optional)
109
+
110
+ Example:
111
+ @log_timing()
112
+ def get_pin_position(component, pin):
113
+ # Logs: "get_pin_position: 5.23ms"
114
+ return position
115
+
116
+ @log_timing(threshold_ms=50)
117
+ def expensive_operation():
118
+ # Logs: "expensive_operation: 150.45ms" at WARNING level
119
+ return result
120
+ """
121
+
122
+ def decorator(func: Callable[..., T]) -> Callable[..., T]:
123
+ logger = logging.getLogger(func.__module__)
124
+
125
+ @functools.wraps(func)
126
+ def wrapper(*args: Any, **kwargs: Any) -> T:
127
+ start = time.time()
128
+ try:
129
+ result = func(*args, **kwargs)
130
+ elapsed = (time.time() - start) * 1000
131
+
132
+ # Determine log level based on threshold
133
+ log_level = level
134
+ message = f"{func.__name__}: {elapsed:.2f}ms"
135
+
136
+ if threshold_ms and elapsed > threshold_ms:
137
+ log_level = logging.WARNING
138
+ message += f" (SLOW, threshold: {threshold_ms}ms)"
139
+
140
+ logger.log(log_level, message)
141
+ return result
142
+
143
+ except Exception as e:
144
+ elapsed = (time.time() - start) * 1000
145
+ logger.error(
146
+ f"{func.__name__} failed after {elapsed:.2f}ms",
147
+ exc_info=True,
148
+ )
149
+ raise
150
+
151
+ return wrapper
152
+
153
+ return decorator
154
+
155
+
156
+ def log_errors(
157
+ operation_name: Optional[str] = None,
158
+ reraise: bool = True,
159
+ ) -> Callable:
160
+ """Decorator for comprehensive exception logging and optional suppression.
161
+
162
+ Logs exceptions with full context. Can optionally suppress exceptions.
163
+
164
+ Args:
165
+ operation_name: Name for error logs (default: function name)
166
+ reraise: Re-raise exception after logging (default: True)
167
+
168
+ Example:
169
+ @log_errors(operation_name="validate_circuit")
170
+ def validate(circuit):
171
+ # Logs exception with full context
172
+ # Raises exception
173
+ raise ValueError("Invalid circuit")
174
+
175
+ @log_errors(reraise=False)
176
+ def safe_operation():
177
+ # Logs exception but doesn't raise
178
+ # Returns None
179
+ raise RuntimeError("Something went wrong")
180
+ return result # Not reached
181
+ """
182
+
183
+ def decorator(func: Callable[..., T]) -> Callable[..., Optional[T]]:
184
+ op_name = operation_name or func.__name__
185
+ logger = logging.getLogger(func.__module__)
186
+
187
+ @functools.wraps(func)
188
+ def wrapper(*args: Any, **kwargs: Any) -> Optional[T]:
189
+ try:
190
+ return func(*args, **kwargs)
191
+
192
+ except Exception as e:
193
+ # Build comprehensive error message
194
+ error_msg = f"Error in {op_name}: {e.__class__.__name__}: {e}"
195
+
196
+ # Try to extract component context if available
197
+ component_info = ""
198
+ for arg in args:
199
+ if hasattr(arg, "reference"):
200
+ component_info = f" [component: {arg.reference}]"
201
+ break
202
+
203
+ if component_info:
204
+ error_msg += component_info
205
+
206
+ # Log with full context
207
+ logger.error(error_msg, exc_info=True)
208
+
209
+ # Either re-raise or suppress
210
+ if reraise:
211
+ raise
212
+ else:
213
+ return None
214
+
215
+ return wrapper
216
+
217
+ return decorator
218
+
219
+
220
+ def log_retry(
221
+ max_attempts: int = 3,
222
+ delay_ms: float = 100,
223
+ backoff: float = 2.0,
224
+ exceptions: tuple = (Exception,),
225
+ ) -> Callable:
226
+ """Decorator for logging retry logic.
227
+
228
+ Automatically retries failed operations with exponential backoff.
229
+
230
+ Args:
231
+ max_attempts: Maximum number of attempts (default: 3)
232
+ delay_ms: Delay between retries in milliseconds (default: 100)
233
+ backoff: Multiply delay by this after each retry (default: 2.0)
234
+ exceptions: Exceptions to catch for retry (default: all)
235
+
236
+ Example:
237
+ @log_retry(max_attempts=3, delay_ms=100)
238
+ def load_symbol(symbol_name):
239
+ # Retries up to 3 times with exponential backoff
240
+ return load_from_cache(symbol_name)
241
+
242
+ @log_retry(exceptions=(IOError, TimeoutError))
243
+ def fetch_data(url):
244
+ # Only retries on IOError or TimeoutError
245
+ return requests.get(url)
246
+ """
247
+
248
+ def decorator(func: Callable[..., T]) -> Callable[..., T]:
249
+ logger = logging.getLogger(func.__module__)
250
+
251
+ @functools.wraps(func)
252
+ def wrapper(*args: Any, **kwargs: Any) -> T:
253
+ last_exception = None
254
+ current_delay = delay_ms
255
+
256
+ for attempt in range(1, max_attempts + 1):
257
+ try:
258
+ if attempt > 1:
259
+ logger.info(
260
+ f"Retry {attempt}/{max_attempts} for {func.__name__}"
261
+ )
262
+
263
+ return func(*args, **kwargs)
264
+
265
+ except exceptions as e:
266
+ last_exception = e
267
+ logger.warning(
268
+ f"Attempt {attempt}/{max_attempts} failed in "
269
+ f"{func.__name__}: {e.__class__.__name__}: {e}"
270
+ )
271
+
272
+ if attempt < max_attempts:
273
+ time.sleep(current_delay / 1000)
274
+ current_delay *= backoff
275
+ else:
276
+ logger.error(
277
+ f"All {max_attempts} attempts failed for {func.__name__}"
278
+ )
279
+
280
+ # All attempts exhausted
281
+ raise last_exception
282
+
283
+ return wrapper
284
+
285
+ return decorator
286
+
287
+
288
+ @contextmanager
289
+ def log_context(
290
+ context_name: str,
291
+ component: Optional[str] = None,
292
+ log_level: int = logging.DEBUG,
293
+ **context_info: Any,
294
+ ):
295
+ """Context manager for logging a block of code.
296
+
297
+ Logs entry and exit with timing. Logs exceptions.
298
+
299
+ Args:
300
+ context_name: Name of the context block
301
+ component: Optional component reference
302
+ log_level: Log level (default: DEBUG)
303
+ **context_info: Additional context information
304
+
305
+ Example:
306
+ with log_context("load_symbols", log_level=logging.INFO):
307
+ # Logs: "ENTER: load_symbols"
308
+ # ... code ...
309
+ # Logs: "EXIT: load_symbols (42.3ms)"
310
+
311
+ with log_context("configure_resistor", component="R1", value="10k"):
312
+ # Logs: "ENTER: configure_resistor [R1] value=10k"
313
+ # ... code ...
314
+ """
315
+ logger = logging.getLogger(__name__)
316
+ start = time.time()
317
+
318
+ # Build entry message
319
+ entry_msg = f"ENTER: {context_name}"
320
+ if component:
321
+ entry_msg += f" [{component}]"
322
+ if context_info:
323
+ info_parts = [f"{k}={v}" for k, v in context_info.items()]
324
+ entry_msg += f" ({', '.join(info_parts)})"
325
+
326
+ logger.log(log_level, entry_msg)
327
+
328
+ try:
329
+ yield
330
+ # Log successful exit
331
+ elapsed = (time.time() - start) * 1000
332
+ logger.log(log_level, f"EXIT: {context_name} ({elapsed:.2f}ms)")
333
+
334
+ except Exception as e:
335
+ elapsed = (time.time() - start) * 1000
336
+ logger.error(
337
+ f"EXCEPTION in {context_name} ({elapsed:.2f}ms): "
338
+ f"{e.__class__.__name__}: {e}",
339
+ exc_info=True,
340
+ )
341
+ raise
342
+
343
+
344
+ @contextmanager
345
+ def log_step(step_name: str, total_steps: Optional[int] = None):
346
+ """Context manager for logging multi-step processes.
347
+
348
+ Useful for tracking progress through complex operations.
349
+
350
+ Args:
351
+ step_name: Name of this step
352
+ total_steps: Total steps in process (optional)
353
+
354
+ Example:
355
+ total = 3
356
+ with log_step("loading symbols", total_steps=total):
357
+ # Logs: "[Step 1/?] loading symbols"
358
+ pass
359
+ with log_step("creating components", total_steps=total):
360
+ # Logs: "[Step 2/?] creating components"
361
+ pass
362
+ with log_step("connecting wires", total_steps=total):
363
+ # Logs: "[Step 3/?] connecting wires"
364
+ pass
365
+ """
366
+ logger = logging.getLogger(__name__)
367
+
368
+ # Track step number (would need to be in a wrapper)
369
+ msg = f"Step: {step_name}"
370
+ if total_steps:
371
+ msg += f" (of {total_steps})"
372
+
373
+ logger.info(msg)
374
+
375
+ try:
376
+ yield
377
+ except Exception as e:
378
+ logger.error(f"Failed at step: {step_name}", exc_info=True)
379
+ raise
380
+
381
+
382
+ class ComponentLogger:
383
+ """Logger for tracking component operations across multiple steps.
384
+
385
+ Groups related component operations with automatic tagging.
386
+
387
+ Example:
388
+ with ComponentLogger("R1") as logger:
389
+ logger.debug("Setting value")
390
+ logger.info("Configured successfully")
391
+ # All logs automatically tagged with [R1]
392
+ """
393
+
394
+ def __init__(self, component_ref: str):
395
+ """Initialize component logger.
396
+
397
+ Args:
398
+ component_ref: Component reference (e.g., "R1", "C2")
399
+ """
400
+ self.component_ref = component_ref
401
+ self.logger = logging.getLogger(__name__)
402
+ self.operations: List[Dict[str, Any]] = []
403
+
404
+ def debug(self, message: str) -> None:
405
+ """Log debug message."""
406
+ msg = f"[{self.component_ref}] {message}"
407
+ self.logger.debug(msg)
408
+ self._record_operation("DEBUG", message)
409
+
410
+ def info(self, message: str) -> None:
411
+ """Log info message."""
412
+ msg = f"[{self.component_ref}] {message}"
413
+ self.logger.info(msg)
414
+ self._record_operation("INFO", message)
415
+
416
+ def warning(self, message: str) -> None:
417
+ """Log warning message."""
418
+ msg = f"[{self.component_ref}] {message}"
419
+ self.logger.warning(msg)
420
+ self._record_operation("WARNING", message)
421
+
422
+ def error(self, message: str, exc_info: bool = False) -> None:
423
+ """Log error message."""
424
+ msg = f"[{self.component_ref}] {message}"
425
+ self.logger.error(msg, exc_info=exc_info)
426
+ self._record_operation("ERROR", message)
427
+
428
+ def _record_operation(self, level: str, message: str) -> None:
429
+ """Record operation in internal history."""
430
+ self.operations.append(
431
+ {
432
+ "timestamp": time.time(),
433
+ "level": level,
434
+ "message": message,
435
+ }
436
+ )
437
+
438
+ def get_history(self) -> List[Dict[str, Any]]:
439
+ """Get operation history."""
440
+ return self.operations.copy()
441
+
442
+ def summary(self) -> str:
443
+ """Get summary of operations."""
444
+ if not self.operations:
445
+ return f"{self.component_ref}: No operations logged"
446
+
447
+ levels = {}
448
+ for op in self.operations:
449
+ level = op["level"]
450
+ levels[level] = levels.get(level, 0) + 1
451
+
452
+ summary_parts = [f"{self.component_ref}:"]
453
+ for level, count in sorted(levels.items()):
454
+ summary_parts.append(f"{level}={count}")
455
+
456
+ return " ".join(summary_parts)
457
+
458
+ def __enter__(self):
459
+ """Context manager entry."""
460
+ return self
461
+
462
+ def __exit__(self, exc_type, exc_val, exc_tb):
463
+ """Context manager exit."""
464
+ if exc_type:
465
+ self.error(f"Exception: {exc_type.__name__}: {exc_val}")
466
+ return False
467
+
468
+
469
+ class OperationTimer:
470
+ """Context manager for measuring operation time with logging.
471
+
472
+ Automatically logs operation timing with optional alerts for slow operations.
473
+
474
+ Example:
475
+ with OperationTimer("calculate_positions", threshold_ms=100):
476
+ # Logs: "TIMER: calculate_positions started"
477
+ # ... calculation ...
478
+ # Logs: "TIMER: calculate_positions completed in 50.23ms"
479
+
480
+ with OperationTimer("load_library", threshold_ms=500):
481
+ # If slower than 500ms, logs at WARNING level
482
+ pass
483
+ """
484
+
485
+ def __init__(
486
+ self,
487
+ operation_name: str,
488
+ threshold_ms: Optional[float] = None,
489
+ log_level: int = logging.INFO,
490
+ ):
491
+ """Initialize timer.
492
+
493
+ Args:
494
+ operation_name: Name of operation
495
+ threshold_ms: Alert if slower than this (optional)
496
+ log_level: Log level for normal operations (default: INFO)
497
+ """
498
+ self.operation_name = operation_name
499
+ self.threshold_ms = threshold_ms
500
+ self.log_level = log_level
501
+ self.logger = logging.getLogger(__name__)
502
+ self.start_time = None
503
+
504
+ def __enter__(self):
505
+ """Start timer."""
506
+ self.start_time = time.time()
507
+ self.logger.log(self.log_level, f"TIMER: {self.operation_name} started")
508
+ return self
509
+
510
+ def __exit__(self, exc_type, exc_val, exc_tb):
511
+ """Stop timer and log result."""
512
+ elapsed = (time.time() - self.start_time) * 1000
513
+
514
+ if exc_type:
515
+ self.logger.error(
516
+ f"TIMER: {self.operation_name} failed after {elapsed:.2f}ms: "
517
+ f"{exc_type.__name__}: {exc_val}",
518
+ exc_info=True,
519
+ )
520
+ return False
521
+
522
+ # Determine log level based on threshold
523
+ level = self.log_level
524
+ message = f"TIMER: {self.operation_name} completed in {elapsed:.2f}ms"
525
+
526
+ if self.threshold_ms and elapsed > self.threshold_ms:
527
+ level = logging.WARNING
528
+ message += f" (SLOW, threshold: {self.threshold_ms}ms)"
529
+
530
+ self.logger.log(level, message)
531
+ return False
532
+
533
+ def elapsed(self) -> float:
534
+ """Get elapsed time in milliseconds."""
535
+ if self.start_time is None:
536
+ return 0
537
+ return (time.time() - self.start_time) * 1000
538
+
539
+
540
+ def trace_calls(
541
+ log_level: int = logging.DEBUG,
542
+ ) -> Callable:
543
+ """Decorator for detailed call tracing.
544
+
545
+ Logs all calls to the function with full arguments and return values.
546
+
547
+ Args:
548
+ log_level: Log level for traces (default: DEBUG)
549
+
550
+ Example:
551
+ @trace_calls()
552
+ def calculate(x, y, operation="add"):
553
+ # Logs: "TRACE: calculate called with x=5, y=3, operation='add'"
554
+ # ... calculation ...
555
+ # Logs: "TRACE: calculate returned 8"
556
+ return result
557
+ """
558
+
559
+ def decorator(func: Callable[..., T]) -> Callable[..., T]:
560
+ logger = logging.getLogger(func.__module__)
561
+
562
+ @functools.wraps(func)
563
+ def wrapper(*args: Any, **kwargs: Any) -> T:
564
+ # Log call
565
+ arg_strs = [repr(arg) for arg in args]
566
+ kwarg_strs = [f"{k}={v!r}" for k, v in kwargs.items()]
567
+ all_args = ", ".join(arg_strs + kwarg_strs)
568
+
569
+ logger.log(
570
+ log_level, f"TRACE: {func.__name__} called with {all_args}"
571
+ )
572
+
573
+ # Execute and log return
574
+ result = func(*args, **kwargs)
575
+ logger.log(
576
+ log_level, f"TRACE: {func.__name__} returned {result!r}"
577
+ )
578
+
579
+ return result
580
+
581
+ return wrapper
582
+
583
+ return decorator
584
+
585
+
586
+ # Module-level logger
587
+ logger = logging.getLogger(__name__)
@@ -11,8 +11,21 @@ from dataclasses import dataclass
11
11
  from enum import Enum
12
12
  from typing import Any, Dict, List, Optional, Set, Union
13
13
 
14
+ # Import ValidationError from new exception hierarchy for backward compatibility
15
+ from ..core.exceptions import ValidationError
16
+
14
17
  logger = logging.getLogger(__name__)
15
18
 
19
+ # Export list for public API
20
+ __all__ = [
21
+ 'ValidationError',
22
+ 'ValidationIssue',
23
+ 'ValidationLevel',
24
+ 'SchematicValidator',
25
+ 'validate_schematic_file',
26
+ 'collect_validation_errors',
27
+ ]
28
+
16
29
 
17
30
  class ValidationLevel(Enum):
18
31
  """Validation issue severity levels."""
@@ -43,28 +56,9 @@ class ValidationIssue:
43
56
  return f"{self.level.value.upper()}: {self.category}: {self.message}{context_str}{suggestion_str}"
44
57
 
45
58
 
46
- class ValidationError(Exception):
47
- """Exception raised when critical validation errors are found."""
48
-
49
- def __init__(self, message: str, issues: List[ValidationIssue] = None):
50
- super().__init__(message)
51
- self.issues = issues or []
52
-
53
- def add_issue(self, issue: ValidationIssue):
54
- """Add a validation issue to this error."""
55
- self.issues.append(issue)
56
-
57
- def get_errors(self) -> List[ValidationIssue]:
58
- """Get only error-level issues."""
59
- return [
60
- issue
61
- for issue in self.issues
62
- if issue.level in (ValidationLevel.ERROR, ValidationLevel.CRITICAL)
63
- ]
64
-
65
- def get_warnings(self) -> List[ValidationIssue]:
66
- """Get only warning-level issues."""
67
- return [issue for issue in self.issues if issue.level == ValidationLevel.WARNING]
59
+ # ValidationError is now imported from core.exceptions (see imports at top)
60
+ # This provides backward compatibility for existing code while using the new
61
+ # exception hierarchy
68
62
 
69
63
 
70
64
  class SchematicValidator:
@@ -0,0 +1,25 @@
1
+ """
2
+ Electrical Rules Check (ERC) validation module.
3
+
4
+ Provides comprehensive electrical validation for KiCAD schematics.
5
+ """
6
+
7
+ from kicad_sch_api.validation.erc import ElectricalRulesChecker
8
+ from kicad_sch_api.validation.erc_models import (
9
+ ERCConfig,
10
+ ERCResult,
11
+ ERCViolation,
12
+ )
13
+ from kicad_sch_api.validation.pin_matrix import (
14
+ PinConflictMatrix,
15
+ PinSeverity,
16
+ )
17
+
18
+ __all__ = [
19
+ "ERCViolation",
20
+ "ERCResult",
21
+ "ERCConfig",
22
+ "PinConflictMatrix",
23
+ "PinSeverity",
24
+ "ElectricalRulesChecker",
25
+ ]