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,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
|
-
|
|
47
|
-
|
|
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
|
+
]
|