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,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