kicad-sch-api 0.4.1__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 +67 -2
- kicad_sch_api/cli/kicad_to_python.py +169 -0
- kicad_sch_api/collections/__init__.py +23 -8
- kicad_sch_api/collections/base.py +369 -59
- kicad_sch_api/collections/components.py +1376 -187
- kicad_sch_api/collections/junctions.py +129 -289
- kicad_sch_api/collections/labels.py +391 -287
- kicad_sch_api/collections/wires.py +202 -316
- kicad_sch_api/core/__init__.py +37 -2
- kicad_sch_api/core/component_bounds.py +34 -12
- kicad_sch_api/core/components.py +146 -7
- kicad_sch_api/core/config.py +25 -12
- kicad_sch_api/core/connectivity.py +692 -0
- kicad_sch_api/core/exceptions.py +175 -0
- kicad_sch_api/core/factories/element_factory.py +3 -1
- kicad_sch_api/core/formatter.py +24 -7
- kicad_sch_api/core/geometry.py +94 -5
- kicad_sch_api/core/managers/__init__.py +4 -0
- kicad_sch_api/core/managers/base.py +76 -0
- kicad_sch_api/core/managers/file_io.py +3 -1
- kicad_sch_api/core/managers/format_sync.py +3 -2
- kicad_sch_api/core/managers/graphics.py +3 -2
- kicad_sch_api/core/managers/hierarchy.py +661 -0
- kicad_sch_api/core/managers/metadata.py +4 -2
- kicad_sch_api/core/managers/sheet.py +52 -14
- kicad_sch_api/core/managers/text_elements.py +3 -2
- kicad_sch_api/core/managers/validation.py +3 -2
- kicad_sch_api/core/managers/wire.py +112 -54
- kicad_sch_api/core/parsing_utils.py +63 -0
- kicad_sch_api/core/pin_utils.py +103 -9
- kicad_sch_api/core/schematic.py +343 -29
- kicad_sch_api/core/types.py +79 -7
- 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 +15 -3
- kicad_sch_api/geometry/routing.py +211 -0
- kicad_sch_api/parsers/elements/label_parser.py +30 -8
- kicad_sch_api/parsers/elements/symbol_parser.py +255 -83
- 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/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.4.1.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-0.4.1.dist-info/METADATA +0 -491
- kicad_sch_api-0.4.1.dist-info/RECORD +0 -87
- kicad_sch_api-0.4.1.dist-info/entry_points.txt +0 -2
- {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,51 +1,308 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Base collection
|
|
3
|
-
|
|
4
|
-
Provides
|
|
5
|
-
|
|
2
|
+
Base collection infrastructure with centralized index management.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- IndexSpec: Index specification and declaration
|
|
6
|
+
- IndexRegistry: Centralized index management with lazy rebuilding
|
|
7
|
+
- PropertyDict: Auto-tracking dictionary for modification detection
|
|
8
|
+
- ValidationLevel: Configurable validation levels
|
|
9
|
+
- BaseCollection: Abstract base class for all collections
|
|
6
10
|
"""
|
|
7
11
|
|
|
8
12
|
import logging
|
|
9
13
|
from abc import ABC, abstractmethod
|
|
10
|
-
from
|
|
14
|
+
from collections.abc import MutableMapping
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from functools import total_ordering
|
|
18
|
+
from typing import Any, Callable, Dict, Generic, Iterator, List, Optional, Set, TypeVar, Union
|
|
11
19
|
|
|
12
20
|
logger = logging.getLogger(__name__)
|
|
13
21
|
|
|
14
22
|
T = TypeVar("T") # Type variable for collection items
|
|
15
23
|
|
|
16
24
|
|
|
17
|
-
|
|
25
|
+
@total_ordering
|
|
26
|
+
class ValidationLevel(Enum):
|
|
27
|
+
"""
|
|
28
|
+
Validation level for collection operations.
|
|
29
|
+
|
|
30
|
+
Controls the amount of validation performed during collection operations.
|
|
31
|
+
Higher levels provide more safety but lower performance.
|
|
32
|
+
"""
|
|
33
|
+
NONE = 0 # No validation (maximum performance)
|
|
34
|
+
BASIC = 1 # Basic checks (duplicates, nulls)
|
|
35
|
+
NORMAL = 2 # Standard validation (default)
|
|
36
|
+
STRICT = 3 # Strict validation (referential integrity)
|
|
37
|
+
PARANOID = 4 # Maximum validation (everything, very slow)
|
|
38
|
+
|
|
39
|
+
def __lt__(self, other):
|
|
40
|
+
"""Compare validation levels by value."""
|
|
41
|
+
if isinstance(other, ValidationLevel):
|
|
42
|
+
return self.value < other.value
|
|
43
|
+
return NotImplemented
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class IndexSpec:
|
|
48
|
+
"""
|
|
49
|
+
Specification for a collection index.
|
|
50
|
+
|
|
51
|
+
Defines how to build and maintain an index for fast lookups.
|
|
52
|
+
"""
|
|
53
|
+
name: str
|
|
54
|
+
key_func: Callable[[Any], Any]
|
|
55
|
+
unique: bool = True
|
|
56
|
+
description: str = ""
|
|
57
|
+
|
|
58
|
+
def __post_init__(self):
|
|
59
|
+
"""Validate index specification."""
|
|
60
|
+
if not self.name:
|
|
61
|
+
raise ValueError("Index name cannot be empty")
|
|
62
|
+
if not callable(self.key_func):
|
|
63
|
+
raise ValueError("Index key_func must be callable")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class IndexRegistry:
|
|
67
|
+
"""
|
|
68
|
+
Centralized registry for managing collection indexes.
|
|
69
|
+
|
|
70
|
+
Provides:
|
|
71
|
+
- Lazy index rebuilding (only when needed)
|
|
72
|
+
- Multiple index support (uuid, reference, lib_id, etc.)
|
|
73
|
+
- Duplicate detection for unique indexes
|
|
74
|
+
- Unified index management API
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(self, specs: List[IndexSpec]):
|
|
78
|
+
"""
|
|
79
|
+
Initialize index registry.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
specs: List of index specifications to manage
|
|
83
|
+
"""
|
|
84
|
+
self.specs = {spec.name: spec for spec in specs}
|
|
85
|
+
self.indexes: Dict[str, Dict[Any, Any]] = {spec.name: {} for spec in specs}
|
|
86
|
+
self._dirty = False
|
|
87
|
+
|
|
88
|
+
logger.debug(f"IndexRegistry initialized with {len(specs)} indexes: {list(self.specs.keys())}")
|
|
89
|
+
|
|
90
|
+
def mark_dirty(self) -> None:
|
|
91
|
+
"""Mark all indexes as needing rebuild."""
|
|
92
|
+
self._dirty = True
|
|
93
|
+
logger.debug("Indexes marked dirty")
|
|
94
|
+
|
|
95
|
+
def is_dirty(self) -> bool:
|
|
96
|
+
"""Check if indexes need rebuilding."""
|
|
97
|
+
return self._dirty
|
|
98
|
+
|
|
99
|
+
def rebuild(self, items: List[Any]) -> None:
|
|
100
|
+
"""
|
|
101
|
+
Rebuild all indexes from items.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
items: List of items to index
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
ValueError: If unique index has duplicates
|
|
108
|
+
"""
|
|
109
|
+
logger.debug(f"Rebuilding {len(self.indexes)} indexes for {len(items)} items")
|
|
110
|
+
|
|
111
|
+
# Clear all indexes
|
|
112
|
+
for index_name in self.indexes:
|
|
113
|
+
self.indexes[index_name].clear()
|
|
114
|
+
|
|
115
|
+
# Rebuild each index
|
|
116
|
+
for spec in self.specs.values():
|
|
117
|
+
self._rebuild_index(spec, items)
|
|
118
|
+
|
|
119
|
+
self._dirty = False
|
|
120
|
+
logger.debug("Index rebuild complete")
|
|
121
|
+
|
|
122
|
+
def _rebuild_index(self, spec: IndexSpec, items: List[Any]) -> None:
|
|
123
|
+
"""
|
|
124
|
+
Rebuild a single index.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
spec: Index specification
|
|
128
|
+
items: Items to index
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
ValueError: If unique index has duplicates
|
|
132
|
+
"""
|
|
133
|
+
index = self.indexes[spec.name]
|
|
134
|
+
|
|
135
|
+
for i, item in enumerate(items):
|
|
136
|
+
try:
|
|
137
|
+
key = spec.key_func(item)
|
|
138
|
+
|
|
139
|
+
if spec.unique:
|
|
140
|
+
if key in index:
|
|
141
|
+
raise ValueError(
|
|
142
|
+
f"Duplicate key '{key}' in unique index '{spec.name}'"
|
|
143
|
+
)
|
|
144
|
+
index[key] = i
|
|
145
|
+
else:
|
|
146
|
+
# Non-unique index: multiple items per key
|
|
147
|
+
if key not in index:
|
|
148
|
+
index[key] = []
|
|
149
|
+
index[key].append(i)
|
|
150
|
+
|
|
151
|
+
except ValueError:
|
|
152
|
+
# Re-raise ValueError (e.g., duplicate key)
|
|
153
|
+
raise
|
|
154
|
+
except Exception as e:
|
|
155
|
+
# Log and skip other errors (e.g., key_func failure)
|
|
156
|
+
logger.warning(f"Failed to index item {i} in '{spec.name}': {e}")
|
|
157
|
+
# Continue indexing other items
|
|
158
|
+
|
|
159
|
+
def get(self, index_name: str, key: Any) -> Optional[Any]:
|
|
160
|
+
"""
|
|
161
|
+
Get value from an index.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
index_name: Name of the index
|
|
165
|
+
key: Key to look up
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Index value if found, None otherwise
|
|
169
|
+
"""
|
|
170
|
+
if index_name not in self.indexes:
|
|
171
|
+
raise KeyError(f"Unknown index: {index_name}")
|
|
172
|
+
|
|
173
|
+
return self.indexes[index_name].get(key)
|
|
174
|
+
|
|
175
|
+
def has_key(self, index_name: str, key: Any) -> bool:
|
|
176
|
+
"""
|
|
177
|
+
Check if key exists in index.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
index_name: Name of the index
|
|
181
|
+
key: Key to check
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
True if key exists, False otherwise
|
|
185
|
+
"""
|
|
186
|
+
if index_name not in self.indexes:
|
|
187
|
+
raise KeyError(f"Unknown index: {index_name}")
|
|
188
|
+
|
|
189
|
+
return key in self.indexes[index_name]
|
|
190
|
+
|
|
191
|
+
def add_spec(self, spec: IndexSpec) -> None:
|
|
192
|
+
"""
|
|
193
|
+
Add a new index specification.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
spec: Index specification to add
|
|
197
|
+
"""
|
|
198
|
+
if spec.name in self.specs:
|
|
199
|
+
raise ValueError(f"Index '{spec.name}' already exists")
|
|
200
|
+
|
|
201
|
+
self.specs[spec.name] = spec
|
|
202
|
+
self.indexes[spec.name] = {}
|
|
203
|
+
self.mark_dirty()
|
|
204
|
+
|
|
205
|
+
logger.debug(f"Added index spec: {spec.name}")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class PropertyDict(MutableMapping):
|
|
18
209
|
"""
|
|
19
|
-
|
|
210
|
+
Dictionary that automatically tracks modifications.
|
|
211
|
+
|
|
212
|
+
Wraps a dictionary and notifies a callback when any changes occur.
|
|
213
|
+
Implements the full MutableMapping interface.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
def __init__(self, data: Optional[Dict[str, Any]] = None, on_modify: Optional[Callable[[], None]] = None):
|
|
217
|
+
"""
|
|
218
|
+
Initialize property dictionary.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
data: Initial dictionary data
|
|
222
|
+
on_modify: Callback to invoke when dict is modified
|
|
223
|
+
"""
|
|
224
|
+
self._data = data or {}
|
|
225
|
+
self._on_modify = on_modify
|
|
226
|
+
|
|
227
|
+
def __getitem__(self, key: str) -> Any:
|
|
228
|
+
"""Get item by key."""
|
|
229
|
+
return self._data[key]
|
|
230
|
+
|
|
231
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
232
|
+
"""Set item and trigger modification callback."""
|
|
233
|
+
self._data[key] = value
|
|
234
|
+
if self._on_modify:
|
|
235
|
+
self._on_modify()
|
|
236
|
+
|
|
237
|
+
def __delitem__(self, key: str) -> None:
|
|
238
|
+
"""Delete item and trigger modification callback."""
|
|
239
|
+
del self._data[key]
|
|
240
|
+
if self._on_modify:
|
|
241
|
+
self._on_modify()
|
|
242
|
+
|
|
243
|
+
def __iter__(self) -> Iterator[str]:
|
|
244
|
+
"""Iterate over keys."""
|
|
245
|
+
return iter(self._data)
|
|
246
|
+
|
|
247
|
+
def __len__(self) -> int:
|
|
248
|
+
"""Number of items."""
|
|
249
|
+
return len(self._data)
|
|
250
|
+
|
|
251
|
+
def __repr__(self) -> str:
|
|
252
|
+
"""String representation."""
|
|
253
|
+
return f"PropertyDict({self._data!r})"
|
|
254
|
+
|
|
255
|
+
def set_callback(self, on_modify: Callable[[], None]) -> None:
|
|
256
|
+
"""Set or update the modification callback."""
|
|
257
|
+
self._on_modify = on_modify
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class BaseCollection(Generic[T], ABC):
|
|
261
|
+
"""
|
|
262
|
+
Abstract base class for all schematic element collections.
|
|
20
263
|
|
|
21
264
|
Provides unified functionality for:
|
|
22
|
-
-
|
|
23
|
-
- Automatic
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
265
|
+
- Lazy index rebuilding via IndexRegistry
|
|
266
|
+
- Automatic modification tracking
|
|
267
|
+
- Configurable validation levels
|
|
268
|
+
- Batch mode for performance
|
|
269
|
+
- Consistent collection operations (add, remove, get, filter)
|
|
270
|
+
|
|
271
|
+
Subclasses must implement:
|
|
272
|
+
- _get_item_uuid(item): Extract UUID from item
|
|
273
|
+
- _create_item(**kwargs): Create new item instance
|
|
274
|
+
- _get_index_specs(): Return list of IndexSpec for this collection
|
|
27
275
|
"""
|
|
28
276
|
|
|
29
|
-
def __init__(
|
|
277
|
+
def __init__(
|
|
278
|
+
self,
|
|
279
|
+
items: Optional[List[T]] = None,
|
|
280
|
+
validation_level: ValidationLevel = ValidationLevel.NORMAL
|
|
281
|
+
):
|
|
30
282
|
"""
|
|
31
|
-
Initialize
|
|
283
|
+
Initialize base collection.
|
|
32
284
|
|
|
33
285
|
Args:
|
|
34
|
-
items: Initial list of items
|
|
286
|
+
items: Initial list of items
|
|
287
|
+
validation_level: Validation level for operations
|
|
35
288
|
"""
|
|
36
289
|
self._items: List[T] = []
|
|
37
|
-
self.
|
|
290
|
+
self._validation_level = validation_level
|
|
38
291
|
self._modified = False
|
|
39
|
-
self.
|
|
292
|
+
self._batch_mode = False
|
|
40
293
|
|
|
41
|
-
#
|
|
294
|
+
# Set up index registry with subclass-specific indexes
|
|
295
|
+
index_specs = self._get_index_specs()
|
|
296
|
+
self._index_registry = IndexRegistry(index_specs)
|
|
297
|
+
|
|
298
|
+
# Add initial items
|
|
42
299
|
if items:
|
|
43
300
|
for item in items:
|
|
44
301
|
self._add_item_to_collection(item)
|
|
45
302
|
|
|
46
303
|
logger.debug(f"{self.__class__.__name__} initialized with {len(self._items)} items")
|
|
47
304
|
|
|
48
|
-
# Abstract methods for subclasses
|
|
305
|
+
# Abstract methods for subclasses
|
|
49
306
|
@abstractmethod
|
|
50
307
|
def _get_item_uuid(self, item: T) -> str:
|
|
51
308
|
"""
|
|
@@ -55,7 +312,7 @@ class IndexedCollection(Generic[T], ABC):
|
|
|
55
312
|
item: Item to extract UUID from
|
|
56
313
|
|
|
57
314
|
Returns:
|
|
58
|
-
UUID string
|
|
315
|
+
UUID string
|
|
59
316
|
"""
|
|
60
317
|
pass
|
|
61
318
|
|
|
@@ -73,12 +330,12 @@ class IndexedCollection(Generic[T], ABC):
|
|
|
73
330
|
pass
|
|
74
331
|
|
|
75
332
|
@abstractmethod
|
|
76
|
-
def
|
|
333
|
+
def _get_index_specs(self) -> List[IndexSpec]:
|
|
77
334
|
"""
|
|
78
|
-
|
|
335
|
+
Get index specifications for this collection.
|
|
79
336
|
|
|
80
|
-
|
|
81
|
-
|
|
337
|
+
Returns:
|
|
338
|
+
List of IndexSpec defining indexes for this collection
|
|
82
339
|
"""
|
|
83
340
|
pass
|
|
84
341
|
|
|
@@ -96,14 +353,17 @@ class IndexedCollection(Generic[T], ABC):
|
|
|
96
353
|
Raises:
|
|
97
354
|
ValueError: If item with same UUID already exists
|
|
98
355
|
"""
|
|
99
|
-
|
|
356
|
+
# Validation
|
|
357
|
+
if self._validation_level >= ValidationLevel.BASIC:
|
|
358
|
+
if item is None:
|
|
359
|
+
raise ValueError("Cannot add None item to collection")
|
|
100
360
|
|
|
101
|
-
|
|
102
|
-
self._ensure_indexes_current()
|
|
361
|
+
uuid_str = self._get_item_uuid(item)
|
|
103
362
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
363
|
+
# Check for duplicate UUID
|
|
364
|
+
self._ensure_indexes_current()
|
|
365
|
+
if self._index_registry.has_key("uuid", uuid_str):
|
|
366
|
+
raise ValueError(f"Item with UUID {uuid_str} already exists")
|
|
107
367
|
|
|
108
368
|
return self._add_item_to_collection(item)
|
|
109
369
|
|
|
@@ -121,24 +381,22 @@ class IndexedCollection(Generic[T], ABC):
|
|
|
121
381
|
|
|
122
382
|
if isinstance(identifier, str):
|
|
123
383
|
# Remove by UUID
|
|
124
|
-
|
|
384
|
+
index = self._index_registry.get("uuid", identifier)
|
|
385
|
+
if index is None:
|
|
125
386
|
return False
|
|
126
|
-
|
|
127
|
-
index = self._uuid_index[identifier]
|
|
128
387
|
item = self._items[index]
|
|
129
388
|
else:
|
|
130
389
|
# Remove by item instance
|
|
131
390
|
item = identifier
|
|
132
391
|
uuid_str = self._get_item_uuid(item)
|
|
133
|
-
|
|
392
|
+
index = self._index_registry.get("uuid", uuid_str)
|
|
393
|
+
if index is None:
|
|
134
394
|
return False
|
|
135
395
|
|
|
136
|
-
index = self._uuid_index[uuid_str]
|
|
137
|
-
|
|
138
396
|
# Remove from main list
|
|
139
397
|
self._items.pop(index)
|
|
140
398
|
self._mark_modified()
|
|
141
|
-
self.
|
|
399
|
+
self._index_registry.mark_dirty()
|
|
142
400
|
|
|
143
401
|
logger.debug(f"Removed item with UUID {self._get_item_uuid(item)}")
|
|
144
402
|
return True
|
|
@@ -155,8 +413,8 @@ class IndexedCollection(Generic[T], ABC):
|
|
|
155
413
|
"""
|
|
156
414
|
self._ensure_indexes_current()
|
|
157
415
|
|
|
158
|
-
|
|
159
|
-
|
|
416
|
+
index = self._index_registry.get("uuid", uuid)
|
|
417
|
+
if index is not None:
|
|
160
418
|
return self._items[index]
|
|
161
419
|
|
|
162
420
|
return None
|
|
@@ -183,7 +441,6 @@ class IndexedCollection(Generic[T], ABC):
|
|
|
183
441
|
Returns:
|
|
184
442
|
List of matching items
|
|
185
443
|
"""
|
|
186
|
-
|
|
187
444
|
def matches_criteria(item: T) -> bool:
|
|
188
445
|
for attr, value in criteria.items():
|
|
189
446
|
if not hasattr(item, attr) or getattr(item, attr) != value:
|
|
@@ -195,10 +452,25 @@ class IndexedCollection(Generic[T], ABC):
|
|
|
195
452
|
def clear(self) -> None:
|
|
196
453
|
"""Clear all items from the collection."""
|
|
197
454
|
self._items.clear()
|
|
198
|
-
self.
|
|
455
|
+
self._index_registry.mark_dirty()
|
|
199
456
|
self._mark_modified()
|
|
200
457
|
logger.debug(f"Cleared all items from {self.__class__.__name__}")
|
|
201
458
|
|
|
459
|
+
# Batch mode operations
|
|
460
|
+
def batch_mode(self):
|
|
461
|
+
"""
|
|
462
|
+
Context manager for batch operations.
|
|
463
|
+
|
|
464
|
+
Defers index rebuilding until the batch is complete.
|
|
465
|
+
|
|
466
|
+
Example:
|
|
467
|
+
with collection.batch_mode():
|
|
468
|
+
for i in range(1000):
|
|
469
|
+
collection.add(create_item(i))
|
|
470
|
+
# Indexes rebuilt only once here
|
|
471
|
+
"""
|
|
472
|
+
return BatchContext(self)
|
|
473
|
+
|
|
202
474
|
# Collection interface methods
|
|
203
475
|
def __len__(self) -> int:
|
|
204
476
|
"""Number of items in collection."""
|
|
@@ -213,12 +485,12 @@ class IndexedCollection(Generic[T], ABC):
|
|
|
213
485
|
if isinstance(item, str):
|
|
214
486
|
# Check by UUID
|
|
215
487
|
self._ensure_indexes_current()
|
|
216
|
-
return
|
|
488
|
+
return self._index_registry.has_key("uuid", item)
|
|
217
489
|
else:
|
|
218
490
|
# Check by item instance
|
|
219
491
|
uuid_str = self._get_item_uuid(item)
|
|
220
492
|
self._ensure_indexes_current()
|
|
221
|
-
return
|
|
493
|
+
return self._index_registry.has_key("uuid", uuid_str)
|
|
222
494
|
|
|
223
495
|
def __getitem__(self, index: int) -> T:
|
|
224
496
|
"""Get item by index."""
|
|
@@ -237,7 +509,10 @@ class IndexedCollection(Generic[T], ABC):
|
|
|
237
509
|
"""
|
|
238
510
|
self._items.append(item)
|
|
239
511
|
self._mark_modified()
|
|
240
|
-
|
|
512
|
+
|
|
513
|
+
# Always mark indexes as dirty when items change
|
|
514
|
+
# Batch mode just defers the rebuild, not the dirty flag
|
|
515
|
+
self._index_registry.mark_dirty()
|
|
241
516
|
|
|
242
517
|
logger.debug(f"Added item with UUID {self._get_item_uuid(item)}")
|
|
243
518
|
return item
|
|
@@ -246,24 +521,14 @@ class IndexedCollection(Generic[T], ABC):
|
|
|
246
521
|
"""Mark collection as modified."""
|
|
247
522
|
self._modified = True
|
|
248
523
|
|
|
249
|
-
def _mark_indexes_dirty(self) -> None:
|
|
250
|
-
"""Mark indexes as needing rebuild."""
|
|
251
|
-
self._dirty_indexes = True
|
|
252
|
-
|
|
253
524
|
def _ensure_indexes_current(self) -> None:
|
|
254
|
-
"""Ensure all indexes are current."""
|
|
255
|
-
if self.
|
|
525
|
+
"""Ensure all indexes are current (unless in batch mode)."""
|
|
526
|
+
if not self._batch_mode and self._index_registry.is_dirty():
|
|
256
527
|
self._rebuild_indexes()
|
|
257
528
|
|
|
258
529
|
def _rebuild_indexes(self) -> None:
|
|
259
530
|
"""Rebuild all indexes."""
|
|
260
|
-
|
|
261
|
-
self._uuid_index = {self._get_item_uuid(item): i for i, item in enumerate(self._items)}
|
|
262
|
-
|
|
263
|
-
# Let subclasses rebuild their additional indexes
|
|
264
|
-
self._build_additional_indexes()
|
|
265
|
-
|
|
266
|
-
self._dirty_indexes = False
|
|
531
|
+
self._index_registry.rebuild(self._items)
|
|
267
532
|
logger.debug(f"Rebuilt indexes for {self.__class__.__name__}")
|
|
268
533
|
|
|
269
534
|
# Collection statistics and debugging
|
|
@@ -277,10 +542,12 @@ class IndexedCollection(Generic[T], ABC):
|
|
|
277
542
|
self._ensure_indexes_current()
|
|
278
543
|
return {
|
|
279
544
|
"item_count": len(self._items),
|
|
280
|
-
"
|
|
545
|
+
"index_count": len(self._index_registry.indexes),
|
|
281
546
|
"modified": self._modified,
|
|
282
|
-
"indexes_dirty": self.
|
|
547
|
+
"indexes_dirty": self._index_registry.is_dirty(),
|
|
283
548
|
"collection_type": self.__class__.__name__,
|
|
549
|
+
"validation_level": self._validation_level.name,
|
|
550
|
+
"batch_mode": self._batch_mode,
|
|
284
551
|
}
|
|
285
552
|
|
|
286
553
|
@property
|
|
@@ -292,3 +559,46 @@ class IndexedCollection(Generic[T], ABC):
|
|
|
292
559
|
"""Mark collection as clean (not modified)."""
|
|
293
560
|
self._modified = False
|
|
294
561
|
logger.debug(f"Marked {self.__class__.__name__} as clean")
|
|
562
|
+
|
|
563
|
+
@property
|
|
564
|
+
def validation_level(self) -> ValidationLevel:
|
|
565
|
+
"""Current validation level."""
|
|
566
|
+
return self._validation_level
|
|
567
|
+
|
|
568
|
+
def set_validation_level(self, level: ValidationLevel) -> None:
|
|
569
|
+
"""
|
|
570
|
+
Set validation level.
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
level: New validation level
|
|
574
|
+
"""
|
|
575
|
+
self._validation_level = level
|
|
576
|
+
logger.debug(f"Set validation level to {level.name}")
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
class BatchContext:
|
|
580
|
+
"""Context manager for batch operations."""
|
|
581
|
+
|
|
582
|
+
def __init__(self, collection: BaseCollection):
|
|
583
|
+
"""
|
|
584
|
+
Initialize batch context.
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
collection: Collection to batch operations on
|
|
588
|
+
"""
|
|
589
|
+
self.collection = collection
|
|
590
|
+
|
|
591
|
+
def __enter__(self):
|
|
592
|
+
"""Enter batch mode - defers index rebuilds."""
|
|
593
|
+
self.collection._batch_mode = True
|
|
594
|
+
logger.debug(f"Entered batch mode for {self.collection.__class__.__name__}")
|
|
595
|
+
return self.collection
|
|
596
|
+
|
|
597
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
598
|
+
"""Exit batch mode and rebuild indexes if needed."""
|
|
599
|
+
self.collection._batch_mode = False
|
|
600
|
+
# Indexes are already marked dirty by add operations
|
|
601
|
+
# Just ensure they're rebuilt now
|
|
602
|
+
self.collection._ensure_indexes_current()
|
|
603
|
+
logger.debug(f"Exited batch mode for {self.collection.__class__.__name__}")
|
|
604
|
+
return False
|