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,604 @@
|
|
|
1
|
+
"""
|
|
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
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
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
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
T = TypeVar("T") # Type variable for collection items
|
|
23
|
+
|
|
24
|
+
|
|
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):
|
|
209
|
+
"""
|
|
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.
|
|
263
|
+
|
|
264
|
+
Provides unified functionality for:
|
|
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
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
def __init__(
|
|
278
|
+
self,
|
|
279
|
+
items: Optional[List[T]] = None,
|
|
280
|
+
validation_level: ValidationLevel = ValidationLevel.NORMAL
|
|
281
|
+
):
|
|
282
|
+
"""
|
|
283
|
+
Initialize base collection.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
items: Initial list of items
|
|
287
|
+
validation_level: Validation level for operations
|
|
288
|
+
"""
|
|
289
|
+
self._items: List[T] = []
|
|
290
|
+
self._validation_level = validation_level
|
|
291
|
+
self._modified = False
|
|
292
|
+
self._batch_mode = False
|
|
293
|
+
|
|
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
|
|
299
|
+
if items:
|
|
300
|
+
for item in items:
|
|
301
|
+
self._add_item_to_collection(item)
|
|
302
|
+
|
|
303
|
+
logger.debug(f"{self.__class__.__name__} initialized with {len(self._items)} items")
|
|
304
|
+
|
|
305
|
+
# Abstract methods for subclasses
|
|
306
|
+
@abstractmethod
|
|
307
|
+
def _get_item_uuid(self, item: T) -> str:
|
|
308
|
+
"""
|
|
309
|
+
Extract UUID from an item.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
item: Item to extract UUID from
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
UUID string
|
|
316
|
+
"""
|
|
317
|
+
pass
|
|
318
|
+
|
|
319
|
+
@abstractmethod
|
|
320
|
+
def _create_item(self, **kwargs) -> T:
|
|
321
|
+
"""
|
|
322
|
+
Create a new item with given parameters.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
**kwargs: Parameters for item creation
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Newly created item
|
|
329
|
+
"""
|
|
330
|
+
pass
|
|
331
|
+
|
|
332
|
+
@abstractmethod
|
|
333
|
+
def _get_index_specs(self) -> List[IndexSpec]:
|
|
334
|
+
"""
|
|
335
|
+
Get index specifications for this collection.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
List of IndexSpec defining indexes for this collection
|
|
339
|
+
"""
|
|
340
|
+
pass
|
|
341
|
+
|
|
342
|
+
# Core collection operations
|
|
343
|
+
def add(self, item: T) -> T:
|
|
344
|
+
"""
|
|
345
|
+
Add an item to the collection.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
item: Item to add
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
The added item
|
|
352
|
+
|
|
353
|
+
Raises:
|
|
354
|
+
ValueError: If item with same UUID already exists
|
|
355
|
+
"""
|
|
356
|
+
# Validation
|
|
357
|
+
if self._validation_level >= ValidationLevel.BASIC:
|
|
358
|
+
if item is None:
|
|
359
|
+
raise ValueError("Cannot add None item to collection")
|
|
360
|
+
|
|
361
|
+
uuid_str = self._get_item_uuid(item)
|
|
362
|
+
|
|
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")
|
|
367
|
+
|
|
368
|
+
return self._add_item_to_collection(item)
|
|
369
|
+
|
|
370
|
+
def remove(self, identifier: Union[str, T]) -> bool:
|
|
371
|
+
"""
|
|
372
|
+
Remove an item from the collection.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
identifier: UUID string or item instance to remove
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
True if item was removed, False if not found
|
|
379
|
+
"""
|
|
380
|
+
self._ensure_indexes_current()
|
|
381
|
+
|
|
382
|
+
if isinstance(identifier, str):
|
|
383
|
+
# Remove by UUID
|
|
384
|
+
index = self._index_registry.get("uuid", identifier)
|
|
385
|
+
if index is None:
|
|
386
|
+
return False
|
|
387
|
+
item = self._items[index]
|
|
388
|
+
else:
|
|
389
|
+
# Remove by item instance
|
|
390
|
+
item = identifier
|
|
391
|
+
uuid_str = self._get_item_uuid(item)
|
|
392
|
+
index = self._index_registry.get("uuid", uuid_str)
|
|
393
|
+
if index is None:
|
|
394
|
+
return False
|
|
395
|
+
|
|
396
|
+
# Remove from main list
|
|
397
|
+
self._items.pop(index)
|
|
398
|
+
self._mark_modified()
|
|
399
|
+
self._index_registry.mark_dirty()
|
|
400
|
+
|
|
401
|
+
logger.debug(f"Removed item with UUID {self._get_item_uuid(item)}")
|
|
402
|
+
return True
|
|
403
|
+
|
|
404
|
+
def get(self, uuid: str) -> Optional[T]:
|
|
405
|
+
"""
|
|
406
|
+
Get an item by UUID.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
uuid: UUID to search for
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
Item if found, None otherwise
|
|
413
|
+
"""
|
|
414
|
+
self._ensure_indexes_current()
|
|
415
|
+
|
|
416
|
+
index = self._index_registry.get("uuid", uuid)
|
|
417
|
+
if index is not None:
|
|
418
|
+
return self._items[index]
|
|
419
|
+
|
|
420
|
+
return None
|
|
421
|
+
|
|
422
|
+
def find(self, predicate: Callable[[T], bool]) -> List[T]:
|
|
423
|
+
"""
|
|
424
|
+
Find all items matching a predicate.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
predicate: Function that returns True for matching items
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
List of matching items
|
|
431
|
+
"""
|
|
432
|
+
return [item for item in self._items if predicate(item)]
|
|
433
|
+
|
|
434
|
+
def filter(self, **criteria) -> List[T]:
|
|
435
|
+
"""
|
|
436
|
+
Filter items by attribute criteria.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
**criteria: Attribute name/value pairs to match
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
List of matching items
|
|
443
|
+
"""
|
|
444
|
+
def matches_criteria(item: T) -> bool:
|
|
445
|
+
for attr, value in criteria.items():
|
|
446
|
+
if not hasattr(item, attr) or getattr(item, attr) != value:
|
|
447
|
+
return False
|
|
448
|
+
return True
|
|
449
|
+
|
|
450
|
+
return self.find(matches_criteria)
|
|
451
|
+
|
|
452
|
+
def clear(self) -> None:
|
|
453
|
+
"""Clear all items from the collection."""
|
|
454
|
+
self._items.clear()
|
|
455
|
+
self._index_registry.mark_dirty()
|
|
456
|
+
self._mark_modified()
|
|
457
|
+
logger.debug(f"Cleared all items from {self.__class__.__name__}")
|
|
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
|
+
|
|
474
|
+
# Collection interface methods
|
|
475
|
+
def __len__(self) -> int:
|
|
476
|
+
"""Number of items in collection."""
|
|
477
|
+
return len(self._items)
|
|
478
|
+
|
|
479
|
+
def __iter__(self) -> Iterator[T]:
|
|
480
|
+
"""Iterate over items in collection."""
|
|
481
|
+
return iter(self._items)
|
|
482
|
+
|
|
483
|
+
def __contains__(self, item: Union[str, T]) -> bool:
|
|
484
|
+
"""Check if item or UUID is in collection."""
|
|
485
|
+
if isinstance(item, str):
|
|
486
|
+
# Check by UUID
|
|
487
|
+
self._ensure_indexes_current()
|
|
488
|
+
return self._index_registry.has_key("uuid", item)
|
|
489
|
+
else:
|
|
490
|
+
# Check by item instance
|
|
491
|
+
uuid_str = self._get_item_uuid(item)
|
|
492
|
+
self._ensure_indexes_current()
|
|
493
|
+
return self._index_registry.has_key("uuid", uuid_str)
|
|
494
|
+
|
|
495
|
+
def __getitem__(self, index: int) -> T:
|
|
496
|
+
"""Get item by index."""
|
|
497
|
+
return self._items[index]
|
|
498
|
+
|
|
499
|
+
# Internal methods
|
|
500
|
+
def _add_item_to_collection(self, item: T) -> T:
|
|
501
|
+
"""
|
|
502
|
+
Internal method to add item to collection.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
item: Item to add
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
The added item
|
|
509
|
+
"""
|
|
510
|
+
self._items.append(item)
|
|
511
|
+
self._mark_modified()
|
|
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()
|
|
516
|
+
|
|
517
|
+
logger.debug(f"Added item with UUID {self._get_item_uuid(item)}")
|
|
518
|
+
return item
|
|
519
|
+
|
|
520
|
+
def _mark_modified(self) -> None:
|
|
521
|
+
"""Mark collection as modified."""
|
|
522
|
+
self._modified = True
|
|
523
|
+
|
|
524
|
+
def _ensure_indexes_current(self) -> None:
|
|
525
|
+
"""Ensure all indexes are current (unless in batch mode)."""
|
|
526
|
+
if not self._batch_mode and self._index_registry.is_dirty():
|
|
527
|
+
self._rebuild_indexes()
|
|
528
|
+
|
|
529
|
+
def _rebuild_indexes(self) -> None:
|
|
530
|
+
"""Rebuild all indexes."""
|
|
531
|
+
self._index_registry.rebuild(self._items)
|
|
532
|
+
logger.debug(f"Rebuilt indexes for {self.__class__.__name__}")
|
|
533
|
+
|
|
534
|
+
# Collection statistics and debugging
|
|
535
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
536
|
+
"""
|
|
537
|
+
Get collection statistics for debugging and monitoring.
|
|
538
|
+
|
|
539
|
+
Returns:
|
|
540
|
+
Dictionary with collection statistics
|
|
541
|
+
"""
|
|
542
|
+
self._ensure_indexes_current()
|
|
543
|
+
return {
|
|
544
|
+
"item_count": len(self._items),
|
|
545
|
+
"index_count": len(self._index_registry.indexes),
|
|
546
|
+
"modified": self._modified,
|
|
547
|
+
"indexes_dirty": self._index_registry.is_dirty(),
|
|
548
|
+
"collection_type": self.__class__.__name__,
|
|
549
|
+
"validation_level": self._validation_level.name,
|
|
550
|
+
"batch_mode": self._batch_mode,
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
@property
|
|
554
|
+
def is_modified(self) -> bool:
|
|
555
|
+
"""Whether collection has been modified."""
|
|
556
|
+
return self._modified
|
|
557
|
+
|
|
558
|
+
def mark_clean(self) -> None:
|
|
559
|
+
"""Mark collection as clean (not modified)."""
|
|
560
|
+
self._modified = False
|
|
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
|