Coverage for src/dataknobs_common/registry.py: 24%

1"""Generic registry pattern for managing named items.

2

3 This module provides reusable registry implementations that packages can extend

4 to manage collections of named items (tools, bots, resources, etc.).

5

6 The registry patterns support:

7- Thread-safe item management

8- Optional caching with TTL

9- Optional metrics collection

10- Generic typing for type safety

11- Both sync and async variants

12

13 Example:

14 ```python

15 from dataknobs_common.registry import Registry

16

17 # Create a simple registry

18 class ToolRegistry(Registry[Tool]):

19 def __init__(self):

20 super().__init__("tools")

21

22 def register_tool(self, tool: Tool) -> None:

23 self.register(tool.name, tool, metadata={"type": "tool"})

24

25 registry = ToolRegistry()

26 registry.register_tool(my_tool)

27 tool = registry.get("my_tool")

28 ```

29

30 With Caching:

31 ```python

32 from dataknobs_common.registry import CachedRegistry

33

34 class BotRegistry(CachedRegistry[Bot]):

35 def __init__(self):

36 super().__init__("bots", cache_ttl=300)

37

38 def get_or_create_bot(self, client_id: str) -> Bot:

39 return self.get_cached(

40 client_id,

41 factory=lambda: self._create_bot(client_id)

42 )

43 ```

44"""

45

46 import asyncio

47 import threading

48 import time

49 from typing import (

50 Any,

51 Callable,

52 Dict,

53 Generic,

54 List,

55 TypeVar,

56)

57

58 from dataknobs_common.exceptions import NotFoundError, OperationError

59

60 T = TypeVar("T")

61

62

63 class Registry(Generic[T]):

64 """Base registry for managing named items with optional metrics.

65

66 This is a thread-safe registry that manages a collection of items by

67 unique keys. It provides core operations for registration, lookup,

68 and enumeration.

69

70 The registry is generic, so you can specify the type of items it

71 manages for better type safety.

72

73 Attributes:

74 name: Name of the registry (for logging/debugging)

75

76 Args:

77 name: Name for this registry instance

78 enable_metrics: Whether to track registration metrics

79

80 Example:

81 ```python

82 registry = Registry[str]("my_registry")

83 registry.register("key1", "value1")

84 registry.get("key1")

85 # 'value1'

86 registry.count()

87 # 1

88 ```

89 """

90

91 def __init__(self, name: str, enable_metrics: bool = False):

92 """Initialize the registry.

93

94 Args:

95 name: Registry name for identification

96 enable_metrics: Enable metrics tracking

97 """

98 self._name = name

99 self._items: Dict[str, T] = {}

100 self._lock = threading.RLock()

101 self._metrics: Dict[str, Dict[str, Any]] | None = {} if enable_metrics else None

102

103 @property

104 def name(self) -> str:

105 """Get registry name."""

106 return self._name

107

108 def register(

109 self,

110 key: str,

111 item: T,

112 metadata: Dict[str, Any] | None = None,

113 allow_overwrite: bool = False,

114 ) -> None:

115 """Register an item by key.

116

117 Args:

118 key: Unique identifier for the item

119 item: Item to register

120 metadata: Optional metadata about the item

121 allow_overwrite: Whether to allow overwriting existing items

122

123 Raises:

124 OperationError: If item already exists and allow_overwrite is False

125

126 Example:

127 ```python

128 registry.register("tool1", my_tool, metadata={"version": "1.0"})

129 ```

130 """

131 with self._lock:

132 if not allow_overwrite and key in self._items:

133 raise OperationError(

134 f"Item '{key}' already registered in {self._name}",

135 context={"key": key, "registry": self._name},

136 )

137

138 self._items[key] = item

139

140 if self._metrics is not None:

141 self._metrics[key] = {

142 "registered_at": time.time(),

143 "metadata": metadata or {},

144 }

145

146 def unregister(self, key: str) -> T:

147 """Unregister and return an item by key.

148

149 Args:

150 key: Key of item to unregister

151

152 Returns:

153 The unregistered item

154

155 Raises:

156 NotFoundError: If item not found

157

158 Example:

159 ```python

160 item = registry.unregister("tool1")

161 ```

162 """

163 with self._lock:

164 if key not in self._items:

165 raise NotFoundError(

166 f"Item not found: {key}",

167 context={"key": key, "registry": self._name},

168 )

169

170 item = self._items.pop(key)

171

172 if self._metrics is not None and key in self._metrics:

173 del self._metrics[key]

174

175 return item

176

177 def get(self, key: str) -> T:

178 """Get an item by key.

179

180 Args:

181 key: Key of item to retrieve

182

183 Returns:

184 The registered item

185

186 Raises:

187 NotFoundError: If item not found

188

189 Example:

190 ```python

191 item = registry.get("tool1")

192 ```

193 """

194 with self._lock:

195 if key not in self._items:

196 raise NotFoundError(

197 f"Item not found: {key}",

198 context={"key": key, "registry": self._name, "available_keys": list(self._items.keys())},

199 )

200 return self._items[key]

201

202 def get_optional(self, key: str) -> T | None:

203 """Get an item by key, returning None if not found.

204

205 Args:

206 key: Key of item to retrieve

207

208 Returns:

209 The registered item or None

210

211 Example:

212 ```python

213 item = registry.get_optional("tool1")

214 if item is None:

215 print("Not found")

216 ```

217 """

218 with self._lock:

219 return self._items.get(key)

220

221 def has(self, key: str) -> bool:

222 """Check if item exists.

223

224 Args:

225 key: Key to check

226

227 Returns:

228 True if item exists

229

230 Example:

231 ```python

232 if registry.has("tool1"):

233 print("Found")

234 ```

235 """

236 with self._lock:

237 return key in self._items

238

239 def list_keys(self) -> List[str]:

240 """List all registered keys.

241

242 Returns:

243 List of registered keys

244

245 Example:

246 ```python

247 keys = registry.list_keys()

248 print(keys)

249 # ['tool1', 'tool2']

250 ```

251 """

252 with self._lock:

253 return list(self._items.keys())

254

255 def list_items(self) -> List[T]:

256 """List all registered items.

257

258 Returns:

259 List of registered items

260

261 Example:

262 ```python

263 items = registry.list_items()

264 for item in items:

265 print(item)

266 ```

267 """

268 with self._lock:

269 return list(self._items.values())

270

271 def items(self) -> List[tuple[str, T]]:

272 """Get all key-item pairs.

273

274 Returns:

275 List of (key, item) tuples

276

277 Example:

278 ```python

279 for key, item in registry.items():

280 print(f"{key}: {item}")

281 ```

282 """

283 with self._lock:

284 return list(self._items.items())

285

286 def count(self) -> int:

287 """Get count of registered items.

288

289 Returns:

290 Number of items in registry

291

292 Example:

293 ```python

294 count = registry.count()

295 print(f"Registry has {count} items")

296 ```

297 """

298 with self._lock:

299 return len(self._items)

300

301 def clear(self) -> None:

302 """Clear all items from registry.

303

304 Example:

305 ```python

306 registry.clear()

307 registry.count()

308 # 0

309 ```

310 """

311 with self._lock:

312 self._items.clear()

313 if self._metrics is not None:

314 self._metrics.clear()

315

316 def get_metrics(self, key: str | None = None) -> Dict[str, Any]:

317 """Get registration metrics.

318

319 Args:

320 key: Optional specific key to get metrics for

321

322 Returns:

323 Metrics dictionary

324

325 Example:

326 ```python

327 metrics = registry.get_metrics()

328 print(metrics)

329 # {'tool1': {'registered_at': 1699456789.0, 'metadata': {}}}

330 ```

331 """

332 with self._lock:

333 if self._metrics is None:

334 return {}

335

336 if key:

337 return self._metrics.get(key, {})

338

339 return dict(self._metrics)

340

341 def __len__(self) -> int:

342 """Get number of registered items using len()."""

343 return self.count()

344

345 def __contains__(self, key: str) -> bool:

346 """Check if item exists using 'in' operator."""

347 return self.has(key)

348

349 def __iter__(self):

350 """Iterate over registered items."""

351 return iter(self.list_items())

352

353

354 class CachedRegistry(Registry[T]):

355 """Registry with time-based caching support.

356

357 Extends the base registry with caching capabilities. Items can be

358 retrieved from cache with automatic expiration and refresh based on TTL.

359 Implements LRU eviction when cache size exceeds limits.

360

361 Args:

362 name: Registry name

363 cache_ttl: Cache time-to-live in seconds (default: 300)

364 max_cache_size: Maximum number of cached items (default: 1000)

365

366 Example:

367 ```python

368 registry = CachedRegistry[Bot]("bots", cache_ttl=300)

369 bot = registry.get_cached(

370 "client1",

371 factory=lambda: create_bot("client1")

372 )

373 ```

374 """

375

376 def __init__(

377 self,

378 name: str,

379 cache_ttl: int = 300,

380 max_cache_size: int = 1000,

381 ):

382 """Initialize cached registry.

383

384 Args:

385 name: Registry name

386 cache_ttl: Time-to-live for cached items in seconds

387 max_cache_size: Maximum cache size before eviction

388 """

389 super().__init__(name, enable_metrics=True)

390 self._cache: Dict[str, tuple[T, float]] = {}

391 self._cache_ttl = cache_ttl

392 self._max_cache_size = max_cache_size

393 self._cache_hits = 0

394 self._cache_misses = 0

395

396 def get_cached(

397 self,

398 key: str,

399 factory: Callable[[], T],

400 force_refresh: bool = False,

401 ) -> T:

402 """Get item from cache with automatic refresh.

403

404 If item exists in cache and is not expired, returns cached version.

405 Otherwise, calls factory to create new item and caches it.

406

407 Args:

408 key: Cache key

409 factory: Callable that creates the item if not cached

410 force_refresh: Force refresh even if cached

411

412 Returns:

413 Cached or newly created item

414

415 Example:

416 ```python

417 def create_bot():

418 return Bot("my-bot")

419 bot = registry.get_cached("bot1", create_bot)

420 ```

421 """

422 with self._lock:

423 # Check cache

424 if not force_refresh and key in self._cache:

425 item, cached_at = self._cache[key]

426 if time.time() - cached_at < self._cache_ttl:

427 self._cache_hits += 1

428 return item

429

430 # Cache miss - create new item

431 self._cache_misses += 1

432 item = factory()

433 self._cache[key] = (item, time.time())

434

435 # Evict if cache too large

436 if len(self._cache) > self._max_cache_size:

437 self._evict_oldest()

438

439 return item

440

441 def invalidate_cache(self, key: str | None = None) -> None:

442 """Invalidate cache for a key or all keys.

443

444 Args:

445 key: Specific key to invalidate, or None to invalidate all

446

447 Example:

448 ```python

449 registry.invalidate_cache("bot1") # Invalidate one

450 registry.invalidate_cache() # Invalidate all

451 ```

452 """

453 with self._lock:

454 if key:

455 if key in self._cache:

456 del self._cache[key]

457 else:

458 self._cache.clear()

459

460 def get_cache_stats(self) -> Dict[str, Any]:

461 """Get cache statistics.

462

463 Returns:

464 Dictionary with cache statistics

465

466 Example:

467 ```python

468 stats = registry.get_cache_stats()

469 print(f"Hit rate: {stats['hit_rate']:.2%}")

470 ```

471 """

472 with self._lock:

473 total = self._cache_hits + self._cache_misses

474 hit_rate = self._cache_hits / total if total > 0 else 0.0

475

476 return {

477 "size": len(self._cache),

478 "max_size": self._max_cache_size,

479 "ttl_seconds": self._cache_ttl,

480 "hits": self._cache_hits,

481 "misses": self._cache_misses,

482 "total_requests": total,

483 "hit_rate": hit_rate,

484 }

485

486 def _evict_oldest(self) -> None:

487 """Evict oldest cache entries (LRU).

488

489 Removes oldest 10% of cache entries when max size is exceeded.

490 """

491 sorted_items = sorted(self._cache.items(), key=lambda x: x[1][1])

492 num_to_remove = max(1, len(sorted_items) // 10)

493

494 for key, _ in sorted_items[:num_to_remove]:

495 del self._cache[key]

496

497

498 class AsyncRegistry(Generic[T]):

499 """Async-safe registry for managing named items.

500

501 Similar to Registry but uses asyncio locks for async-safe operations.

502 Use this when working in async contexts.

503

504 Args:

505 name: Registry name

506 enable_metrics: Enable metrics tracking

507

508 Example:

509 >>> registry = AsyncRegistry[Tool]("tools")

510 >>> await registry.register("tool1", my_tool)

511 >>> tool = await registry.get("tool1")

512 """

513

514 def __init__(self, name: str, enable_metrics: bool = False):

515 """Initialize async registry.

516

517 Args:

518 name: Registry name

519 enable_metrics: Enable metrics tracking

520 """

521 self._name = name

522 self._items: Dict[str, T] = {}

523 self._lock = asyncio.Lock()

524 self._metrics: Dict[str, Dict[str, Any]] | None = {} if enable_metrics else None

525

526 @property

527 def name(self) -> str:

528 """Get registry name."""

529 return self._name

530

531 async def register(

532 self,

533 key: str,

534 item: T,

535 metadata: Dict[str, Any] | None = None,

536 allow_overwrite: bool = False,

537 ) -> None:

538 """Register an item by key.

539

540 Args:

541 key: Unique identifier

542 item: Item to register

543 metadata: Optional metadata

544 allow_overwrite: Allow overwriting existing items

545

546 Raises:

547 OperationError: If item exists and allow_overwrite is False

548 """

549 async with self._lock:

550 if not allow_overwrite and key in self._items:

551 raise OperationError(

552 f"Item '{key}' already registered in {self._name}",

553 context={"key": key, "registry": self._name},

554 )

555

556 self._items[key] = item

557

558 if self._metrics is not None:

559 self._metrics[key] = {

560 "registered_at": time.time(),

561 "metadata": metadata or {},

562 }

563

564 async def unregister(self, key: str) -> T:

565 """Unregister and return an item.

566

567 Args:

568 key: Key to unregister

569

570 Returns:

571 The unregistered item

572

573 Raises:

574 NotFoundError: If item not found

575 """

576 async with self._lock:

577 if key not in self._items:

578 raise NotFoundError(

579 f"Item not found: {key}",

580 context={"key": key, "registry": self._name},

581 )

582

583 item = self._items.pop(key)

584

585 if self._metrics is not None and key in self._metrics:

586 del self._metrics[key]

587

588 return item

589

590 async def get(self, key: str) -> T:

591 """Get an item by key.

592

593 Args:

594 key: Key to retrieve

595

596 Returns:

597 The registered item

598

599 Raises:

600 NotFoundError: If item not found

601 """

602 async with self._lock:

603 if key not in self._items:

604 raise NotFoundError(

605 f"Item not found: {key}",

606 context={"key": key, "registry": self._name, "available_keys": list(self._items.keys())},

607 )

608 return self._items[key]

609

610 async def get_optional(self, key: str) -> T | None:

611 """Get an item, returning None if not found.

612

613 Args:

614 key: Key to retrieve

615

616 Returns:

617 The item or None

618 """

619 async with self._lock:

620 return self._items.get(key)

621

622 async def has(self, key: str) -> bool:

623 """Check if item exists.

624

625 Args:

626 key: Key to check

627

628 Returns:

629 True if exists

630 """

631 async with self._lock:

632 return key in self._items

633

634 async def list_keys(self) -> List[str]:

635 """List all registered keys.

636

637 Returns:

638 List of keys

639 """

640 async with self._lock:

641 return list(self._items.keys())

642

643 async def list_items(self) -> List[T]:

644 """List all registered items.

645

646 Returns:

647 List of items

648 """

649 async with self._lock:

650 return list(self._items.values())

651

652 async def items(self) -> List[tuple[str, T]]:

653 """Get all key-item pairs.

654

655 Returns:

656 List of (key, item) tuples

657 """

658 async with self._lock:

659 return list(self._items.items())

660

661 async def count(self) -> int:

662 """Get count of registered items.

663

664 Returns:

665 Number of items

666 """

667 async with self._lock:

668 return len(self._items)

669

670 async def clear(self) -> None:

671 """Clear all items."""

672 async with self._lock:

673 self._items.clear()

674 if self._metrics is not None:

675 self._metrics.clear()

676

677 async def get_metrics(self, key: str | None = None) -> Dict[str, Any]:

678 """Get registration metrics.

679

680 Args:

681 key: Optional specific key

682

683 Returns:

684 Metrics dictionary

685 """

686 async with self._lock:

687 if self._metrics is None:

688 return {}

689

690 if key:

691 return self._metrics.get(key, {})

692

693 return dict(self._metrics)

694

695 def __len__(self) -> int:

696 """Get number of registered items using len()."""

697 # Note: This is synchronous but safe since it just reads the dict

698 return len(self._items)

699

700 def __contains__(self, key: str) -> bool:

701 """Check if item exists using 'in' operator."""

702 # Note: This is synchronous but safe since it just reads the dict

703 return key in self._items

704

705 def __iter__(self):

706 """Iterate over registered items."""

707 # Note: Returns iterator over current snapshot

708 return iter(list(self._items.values()))

709

710

711 class PluginRegistry(Generic[T]):

712 """Registry for plugins with factory support and defaults.

713

714 A specialized registry pattern for managing plugins (adapters, handlers,

715 providers, etc.) that supports:

716 - Class or factory function registration

717 - Lazy instantiation with configuration

718 - Default fallback when plugin not found

719 - Instance caching

720 - Type validation

721

722 This pattern is useful when you need to:

723 - Register different implementations of an interface

724 - Create instances on-demand with configuration

725 - Provide graceful fallbacks for unregistered keys

726

727 Args:

728 name: Registry name

729 default_factory: Default factory to use when key not found

730

731 Example:

732 ```python

733 from dataknobs_common.registry import PluginRegistry

734

735 # Define base class

736 class Handler:

737 def __init__(self, name: str, config: dict):

738 self.name = name

739 self.config = config

740

741 class DefaultHandler(Handler):

742 pass

743

744 class CustomHandler(Handler):

745 pass

746

747 # Create registry with default

748 registry = PluginRegistry[Handler]("handlers", default_factory=DefaultHandler)

749

750 # Register plugins

751 registry.register("custom", CustomHandler)

752

753 # Get instances

754 handler = registry.get("custom", config={"timeout": 30})

755 default = registry.get("unknown", config={}) # Uses default

756 ```

757

758 With async factories:

759 ```python

760 async def create_async_handler(name, config):

761 handler = AsyncHandler(name, config)

762 await handler.initialize()

763 return handler

764

765 registry.register("async", create_async_handler)

766 handler = await registry.get_async("async", config={"url": "..."})

767 ```

768 """

769

770 def __init__(

771 self,

772 name: str,

773 default_factory: type[T] | Callable[..., T] | None = None,

774 validate_type: type | None = None,

775 ):

776 """Initialize plugin registry.

777

778 Args:

779 name: Registry name for identification

780 default_factory: Default class or factory to use when key not found

781 validate_type: Optional base type to validate registrations against

782 """

783 self._name = name

784 self._factories: Dict[str, type[T] | Callable[..., T]] = {}

785 self._instances: Dict[str, T] = {}

786 self._lock = threading.RLock()

787 self._default_factory = default_factory

788 self._validate_type = validate_type

789

790 @property

791 def name(self) -> str:

792 """Get registry name."""

793 return self._name

794

795 def register(

796 self,

797 key: str,

798 factory: type[T] | Callable[..., T],

799 override: bool = False,

800 ) -> None:

801 """Register a plugin class or factory.

802

803 Args:

804 key: Unique identifier for the plugin

805 factory: Plugin class or factory function that creates instances

806 override: If True, allow overriding existing registration

807

808 Raises:

809 OperationError: If key already registered and override=False

810 TypeError: If factory doesn't match validate_type

811

812 Example:

813 ```python

814 # Register a class

815 registry.register("handler1", MyHandler)

816

817 # Register a factory function

818 registry.register("handler2", lambda name, config: create_handler(name, config))

819 ```

820 """

821 with self._lock:

822 # Check for existing registration

823 if not override and key in self._factories:

824 raise OperationError(

825 f"Plugin '{key}' already registered in {self._name}. "

826 f"Use override=True to replace.",

827 context={"key": key, "registry": self._name},

828 )

829

830 # Validate type if specified

831 if self._validate_type and isinstance(factory, type):

832 if not issubclass(factory, self._validate_type):

833 raise TypeError(

834 f"Factory class must be a subclass of {self._validate_type.__name__}, "

835 f"got {factory.__name__}"

836 )

837 elif not callable(factory):

838 raise TypeError(

839 f"Factory must be a class or callable, got {type(factory).__name__}"

840 )

841

842 # Register

843 self._factories[key] = factory

844

845 # Clear cached instance if overriding

846 if key in self._instances:

847 del self._instances[key]

848

849 def unregister(self, key: str) -> None:

850 """Unregister a plugin.

851

852 Args:

853 key: Key to unregister

854

855 Raises:

856 NotFoundError: If key not registered

857 """

858 with self._lock:

859 if key not in self._factories:

860 raise NotFoundError(

861 f"Plugin not found: {key}",

862 context={"key": key, "registry": self._name},

863 )

864

865 del self._factories[key]

866

867 # Clear cached instance

868 if key in self._instances:

869 del self._instances[key]

870

871 def is_registered(self, key: str) -> bool:

872 """Check if a plugin is registered.

873

874 Args:

875 key: Key to check

876

877 Returns:

878 True if registered

879 """

880 with self._lock:

881 return key in self._factories

882

883 def get(

884 self,

885 key: str,

886 config: Dict[str, Any] | None = None,

887 use_cache: bool = True,

888 use_default: bool = True,

889 ) -> T:

890 """Get a plugin instance.

891

892 Creates instance if not cached, using the registered factory.

893

894 Args:

895 key: Plugin identifier

896 config: Configuration dictionary passed to factory

897 use_cache: Return cached instance if available

898 use_default: Use default factory if key not registered

899

900 Returns:

901 Plugin instance

902

903 Raises:

904 NotFoundError: If key not registered and use_default=False

905

906 Example:

907 ```python

908 handler = registry.get("custom", config={"timeout": 30})

909 ```

910 """

911 with self._lock:

912 # Check cache

913 if use_cache and key in self._instances:

914 return self._instances[key]

915

916 # Get factory

917 if key in self._factories:

918 factory = self._factories[key]

919 elif use_default and self._default_factory:

920 factory = self._default_factory

921 else:

922 raise NotFoundError(

923 f"Plugin '{key}' not registered and no default available",

924 context={

925 "key": key,

926 "registry": self._name,

927 "available": list(self._factories.keys()),

928 },

929 )

930

931 # Create instance

932 try:

933 if isinstance(factory, type):

934 instance = factory(key, config or {})

935 else:

936 instance = factory(key, config or {})

937

938 # Validate instance type if specified

939 if self._validate_type and not isinstance(instance, self._validate_type):

940 raise TypeError(

941 f"Factory must return a {self._validate_type.__name__} instance, "

942 f"got {type(instance).__name__}"

943 )

944

945 except Exception as e:

946 raise OperationError(

947 f"Failed to create plugin '{key}': {e}",

948 context={"key": key, "registry": self._name},

949 ) from e

950

951 # Cache instance

952 if use_cache:

953 self._instances[key] = instance

954

955 return instance

956

957 async def get_async(

958 self,

959 key: str,

960 config: Dict[str, Any] | None = None,

961 use_cache: bool = True,

962 use_default: bool = True,

963 ) -> T:

964 """Get a plugin instance, supporting async factories.

965

966 Like get() but awaits the factory if it's a coroutine function.

967

968 Args:

969 key: Plugin identifier

970 config: Configuration dictionary

971 use_cache: Return cached instance if available

972 use_default: Use default factory if key not registered

973

974 Returns:

975 Plugin instance

976

977 Example:

978 ```python

979 handler = await registry.get_async("async-handler", config={"url": "..."})

980 ```

981 """

982 with self._lock:

983 # Check cache

984 if use_cache and key in self._instances:

985 return self._instances[key]

986

987 # Get factory

988 if key in self._factories:

989 factory = self._factories[key]

990 elif use_default and self._default_factory:

991 factory = self._default_factory

992 else:

993 raise NotFoundError(

994 f"Plugin '{key}' not registered and no default available",

995 context={

996 "key": key,

997 "registry": self._name,

998 "available": list(self._factories.keys()),

999 },

1000 )

1001

1002 # Create instance (outside lock for async)

1003 try:

1004 if isinstance(factory, type):

1005 instance = factory(key, config or {})

1006 else:

1007 result = factory(key, config or {})

1008 # Await if coroutine

1009 if asyncio.iscoroutine(result):

1010 instance = await result

1011 else:

1012 instance = result

1013

1014 # Validate instance type

1015 if self._validate_type and not isinstance(instance, self._validate_type):

1016 raise TypeError(

1017 f"Factory must return a {self._validate_type.__name__} instance, "

1018 f"got {type(instance).__name__}"

1019 )

1020

1021 except Exception as e:

1022 raise OperationError(

1023 f"Failed to create plugin '{key}': {e}",

1024 context={"key": key, "registry": self._name},

1025 ) from e

1026

1027 # Cache instance

1028 with self._lock:

1029 if use_cache:

1030 self._instances[key] = instance

1031

1032 return instance

1033

1034 def list_keys(self) -> List[str]:

1035 """List all registered plugin keys.

1036

1037 Returns:

1038 List of registered keys

1039 """

1040 with self._lock:

1041 return list(self._factories.keys())

1042

1043 def clear_cache(self, key: str | None = None) -> None:

1044 """Clear cached instances.

1045

1046 Args:

1047 key: Specific key to clear, or None for all

1048 """

1049 with self._lock:

1050 if key:

1051 if key in self._instances:

1052 del self._instances[key]

1053 else:

1054 self._instances.clear()

1055

1056 def get_factory(self, key: str) -> type[T] | Callable[..., T] | None:

1057 """Get the registered factory for a key.

1058

1059 Args:

1060 key: Plugin identifier

1061

1062 Returns:

1063 Factory class or function, or None if not registered

1064 """

1065 with self._lock:

1066 return self._factories.get(key)

1067

1068 @property

1069 def cached_instances(self) -> Dict[str, T]:

1070 """Get the dictionary of cached instances.

1071

1072 Returns:

1073 Dictionary mapping keys to cached instances

1074

1075 Note:

1076 This returns the internal cache dictionary. Modifications

1077 will affect the cache directly.

1078 """

1079 return self._instances

1080

1081 def set_default_factory(self, factory: type[T] | Callable[..., T]) -> None:

1082 """Set the default factory.

1083

1084 Args:

1085 factory: New default factory

1086

1087 Raises:

1088 TypeError: If factory doesn't match validate_type

1089 """

1090 if self._validate_type and isinstance(factory, type):

1091 if not issubclass(factory, self._validate_type):

1092 raise TypeError(

1093 f"Default factory must be a subclass of {self._validate_type.__name__}"

1094 )

1095

1096 self._default_factory = factory

1097

1098 def bulk_register(

1099 self,

1100 factories: Dict[str, type[T] | Callable[..., T]],

1101 override: bool = False,

1102 ) -> None:

1103 """Register multiple plugins at once.

1104

1105 Args:

1106 factories: Dictionary mapping keys to factories

1107 override: Allow overriding existing registrations

1108

1109 Example:

1110 ```python

1111 registry.bulk_register({

1112 "handler1": Handler1,

1113 "handler2": Handler2,

1114 })

1115 ```

1116 """

1117 for key, factory in factories.items():

1118 self.register(key, factory, override=override)

1119

1120 def copy(self) -> Dict[str, type[T] | Callable[..., T]]:

1121 """Get a copy of all registered factories.

1122

1123 Returns:

1124 Dictionary of key to factory mappings

1125 """

1126 with self._lock:

1127 return dict(self._factories)

1128

1129 def __len__(self) -> int:

1130 """Get number of registered plugins."""

1131 return len(self._factories)

1132

1133 def __contains__(self, key: str) -> bool:

1134 """Check if plugin is registered using 'in' operator."""

1135 return self.is_registered(key)

1136

1137 def __repr__(self) -> str:

1138 """Get string representation."""

1139 return (

1140 f"PluginRegistry("

1141 f"name='{self._name}', "

1142 f"plugins={len(self._factories)}, "

1143 f"cached={len(self._instances)}"

1144 f")"

1145 )

1146

1147

1148 __all__ = [

1149 "Registry",

1150 "CachedRegistry",

1151 "AsyncRegistry",

1152 "PluginRegistry",

1153]