django-bulk-hooks 0.2.9__py3-none-any.whl → 0.2.93__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.
@@ -1,565 +1,541 @@
1
- """
2
- Hook factory system for dependency injection.
3
-
4
- This module provides seamless integration with dependency-injector containers,
5
- allowing hooks to be managed as container providers with full DI support.
6
-
7
- Usage Pattern 1 - Container Integration (Recommended):
8
- ```python
9
- from dependency_injector import containers, providers
10
- from django_bulk_hooks import configure_hook_container
11
-
12
- class LoanAccountContainer(containers.DeclarativeContainer):
13
- loan_account_repository = providers.Singleton(LoanAccountRepository)
14
- loan_account_service = providers.Singleton(LoanAccountService)
15
- loan_account_validator = providers.Singleton(LoanAccountValidator)
16
-
17
- # Define hook as a provider
18
- loan_account_hook = providers.Singleton(
19
- LoanAccountHook,
20
- daily_loan_summary_service=Provide["daily_loan_summary_service"],
21
- loan_account_service=loan_account_service,
22
- loan_account_validator=loan_account_validator,
23
- )
24
-
25
- # Configure the hook system to use your container
26
- container = LoanAccountContainer()
27
- configure_hook_container(container)
28
- ```
29
-
30
- Usage Pattern 2 - Explicit Factory Registration:
31
- ```python
32
- from django_bulk_hooks import set_hook_factory
33
-
34
- def create_loan_hook():
35
- return container.loan_account_hook()
36
-
37
- set_hook_factory(LoanAccountHook, create_loan_hook)
38
- ```
39
-
40
- Usage Pattern 3 - Custom Resolver:
41
- ```python
42
- from django_bulk_hooks import configure_hook_container
43
-
44
- def custom_resolver(container, hook_cls, provider_name):
45
- # Custom resolution logic for nested containers
46
- return container.sub_container.get_provider(provider_name)()
47
-
48
- configure_hook_container(container, provider_resolver=custom_resolver)
49
- ```
50
- """
51
-
52
- import logging
53
- import re
54
- import threading
55
- from typing import Any, Callable, Optional, Type
56
-
57
- logger = logging.getLogger(__name__)
58
-
59
-
60
- class HookFactory:
61
- """
62
- Creates hook handler instances with dependency injection.
63
-
64
- Resolution order:
65
- 1. Specific factory for hook class
66
- 2. Container resolver (if configured)
67
- 3. Direct instantiation
68
- """
69
-
70
- def __init__(self):
71
- """Initialize an empty factory."""
72
- self._specific_factories: dict[Type, Callable[[], Any]] = {}
73
- self._container_resolver: Optional[Callable[[Type], Any]] = None
74
- self._lock = threading.RLock()
75
-
76
- def register_factory(self, hook_cls: Type, factory: Callable[[], Any]) -> None:
77
- """
78
- Register a factory function for a specific hook class.
79
-
80
- The factory function should accept no arguments and return an instance
81
- of the hook class with all dependencies injected.
82
-
83
- Args:
84
- hook_cls: The hook class to register a factory for
85
- factory: A callable that returns an instance of hook_cls
86
-
87
- Example:
88
- >>> def create_loan_hook():
89
- ... return container.loan_account_hook()
90
- >>>
91
- >>> factory.register_factory(LoanAccountHook, create_loan_hook)
92
- """
93
- with self._lock:
94
- self._specific_factories[hook_cls] = factory
95
- name = getattr(hook_cls, "__name__", str(hook_cls))
96
- logger.debug(f"Registered factory for {name}")
97
-
98
- def configure_container(
99
- self,
100
- container: Any,
101
- provider_name_resolver: Optional[Callable[[Type], str]] = None,
102
- provider_resolver: Optional[Callable[[Any, Type, str], Any]] = None,
103
- fallback_to_direct: bool = True,
104
- ) -> None:
105
- """
106
- Configure the factory to use a dependency-injector container.
107
-
108
- This is the recommended way to integrate with dependency-injector.
109
- It automatically resolves hooks from container providers.
110
-
111
- Args:
112
- container: The dependency-injector container instance
113
- provider_name_resolver: Optional function to map hook class to provider name.
114
- Default: converts "LoanAccountHook" -> "loan_account_hook"
115
- provider_resolver: Optional function to resolve provider from container.
116
- Signature: (container, hook_cls, provider_name) -> instance
117
- Useful for nested container structures or custom resolution logic.
118
- fallback_to_direct: If True, falls back to direct instantiation when
119
- provider not found. If False, raises error.
120
-
121
- Example (Standard Container):
122
- >>> class AppContainer(containers.DeclarativeContainer):
123
- ... loan_service = providers.Singleton(LoanService)
124
- ... loan_account_hook = providers.Singleton(
125
- ... LoanAccountHook,
126
- ... loan_service=loan_service,
127
- ... )
128
- >>>
129
- >>> container = AppContainer()
130
- >>> factory.configure_container(container)
131
-
132
- Example (Custom Resolver for Nested Containers):
133
- >>> def resolve_nested(container, hook_cls, provider_name):
134
- ... # Navigate nested structure
135
- ... sub_container = container.loan_accounts_container()
136
- ... return getattr(sub_container, provider_name)()
137
- >>>
138
- >>> factory.configure_container(
139
- ... container,
140
- ... provider_resolver=resolve_nested
141
- ... )
142
- """
143
- name_resolver = provider_name_resolver or self._default_name_resolver
144
-
145
- def resolver(hook_cls: Type) -> Any:
146
- """Resolve hook instance from the container."""
147
- provider_name = name_resolver(hook_cls)
148
- name = getattr(hook_cls, "__name__", str(hook_cls))
149
-
150
- # If custom provider resolver is provided, use it
151
- if provider_resolver is not None:
152
- logger.debug(f"Resolving {name} using custom provider resolver")
153
- try:
154
- return provider_resolver(container, hook_cls, provider_name)
155
- except Exception as e:
156
- if fallback_to_direct:
157
- logger.debug(
158
- f"Custom provider resolver failed for {name} ({e}), "
159
- f"falling back to direct instantiation"
160
- )
161
- return hook_cls()
162
- raise
163
-
164
- # Default resolution: look for provider directly on container
165
- if hasattr(container, provider_name):
166
- provider = getattr(container, provider_name)
167
- logger.debug(
168
- f"Resolving {name} from container provider '{provider_name}'"
169
- )
170
- # Call the provider to get the instance
171
- return provider()
172
-
173
- if fallback_to_direct:
174
- logger.debug(
175
- f"Provider '{provider_name}' not found in container for {name}, "
176
- f"falling back to direct instantiation"
177
- )
178
- return hook_cls()
179
-
180
- raise ValueError(
181
- f"Hook {name} not found in container. "
182
- f"Expected provider name: '{provider_name}'. "
183
- f"Available providers: {[p for p in dir(container) if not p.startswith('_')]}"
184
- )
185
-
186
- with self._lock:
187
- self._container_resolver = resolver
188
- container_name = getattr(
189
- container.__class__, "__name__", str(container.__class__)
190
- )
191
- logger.info(
192
- f"Configured hook factory to use container: {container_name}"
193
- )
194
-
195
- def create(self, hook_cls: Type) -> Any:
196
- """
197
- Create a hook instance using the configured resolution strategy.
198
-
199
- Resolution order:
200
- 1. Specific factory registered via register_factory()
201
- 2. Container resolver configured via configure_container()
202
- 3. Direct instantiation hook_cls()
203
-
204
- Args:
205
- hook_cls: The hook class to instantiate
206
-
207
- Returns:
208
- An instance of the hook class
209
-
210
- Raises:
211
- Any exception raised by the factory, container, or constructor
212
- """
213
- with self._lock:
214
- # 1. Check for specific factory
215
- if hook_cls in self._specific_factories:
216
- factory = self._specific_factories[hook_cls]
217
- name = getattr(hook_cls, "__name__", str(hook_cls))
218
- logger.debug(f"Using specific factory for {name}")
219
- return factory()
220
-
221
- # 2. Check for container resolver
222
- if self._container_resolver is not None:
223
- name = getattr(hook_cls, "__name__", str(hook_cls))
224
- logger.debug(f"Using container resolver for {name}")
225
- return self._container_resolver(hook_cls)
226
-
227
- # 3. Fall back to direct instantiation
228
- name = getattr(hook_cls, "__name__", str(hook_cls))
229
- logger.debug(f"Using direct instantiation for {name}")
230
- return hook_cls()
231
-
232
- def clear(self) -> None:
233
- """
234
- Clear all registered factories and container configuration.
235
- Useful for testing.
236
- """
237
- with self._lock:
238
- self._specific_factories.clear()
239
- self._container_resolver = None
240
- logger.debug("Cleared all hook factories and container configuration")
241
-
242
- def is_container_configured(self) -> bool:
243
- """
244
- Check if a container resolver is configured.
245
-
246
- Returns:
247
- True if configure_container() has been called
248
- """
249
- with self._lock:
250
- return self._container_resolver is not None
251
-
252
- def has_factory(self, hook_cls: Type) -> bool:
253
- """
254
- Check if a hook class has a registered factory.
255
-
256
- Args:
257
- hook_cls: The hook class to check
258
-
259
- Returns:
260
- True if a specific factory is registered, False otherwise
261
- """
262
- with self._lock:
263
- return hook_cls in self._specific_factories
264
-
265
- def get_factory(self, hook_cls: Type) -> Optional[Callable[[], Any]]:
266
- """
267
- Get the registered factory for a specific hook class.
268
-
269
- Args:
270
- hook_cls: The hook class to look up
271
-
272
- Returns:
273
- The registered factory function, or None if not registered
274
- """
275
- with self._lock:
276
- return self._specific_factories.get(hook_cls)
277
-
278
- def list_factories(self) -> dict[Type, Callable]:
279
- """
280
- Get a copy of all registered hook factories.
281
-
282
- Returns:
283
- A dictionary mapping hook classes to their factory functions
284
- """
285
- with self._lock:
286
- return self._specific_factories.copy()
287
-
288
- @staticmethod
289
- def _default_name_resolver(hook_cls: Type) -> str:
290
- """
291
- Default naming convention: LoanAccountHook -> loan_account_hook
292
-
293
- Args:
294
- hook_cls: Hook class to convert
295
-
296
- Returns:
297
- Snake-case provider name
298
- """
299
- name = hook_cls.__name__
300
- # Convert CamelCase to snake_case
301
- snake_case = re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()
302
- return snake_case
303
-
304
-
305
- # Global singleton factory
306
- _factory: Optional[HookFactory] = None
307
- _factory_lock = threading.Lock()
308
-
309
-
310
- def get_factory() -> HookFactory:
311
- """
312
- Get the global hook factory instance.
313
-
314
- Creates the factory on first access (singleton pattern).
315
- Thread-safe initialization.
316
-
317
- Returns:
318
- HookFactory singleton instance
319
- """
320
- global _factory
321
-
322
- if _factory is None:
323
- with _factory_lock:
324
- # Double-checked locking
325
- if _factory is None:
326
- _factory = HookFactory()
327
-
328
- return _factory
329
-
330
-
331
- # Backward-compatible module-level functions
332
- def set_hook_factory(hook_cls: Type, factory: Callable[[], Any]) -> None:
333
- """
334
- Register a factory function for a specific hook class.
335
-
336
- The factory function should accept no arguments and return an instance
337
- of the hook class with all dependencies injected.
338
-
339
- Args:
340
- hook_cls: The hook class to register a factory for
341
- factory: A callable that returns an instance of hook_cls
342
-
343
- Example:
344
- >>> def create_loan_hook():
345
- ... return container.loan_account_hook()
346
- >>>
347
- >>> set_hook_factory(LoanAccountHook, create_loan_hook)
348
- """
349
- hook_factory = get_factory()
350
- hook_factory.register_factory(hook_cls, factory)
351
-
352
-
353
- def set_default_hook_factory(factory: Callable[[Type], Any]) -> None:
354
- """
355
- DEPRECATED: Use configure_hook_container with provider_resolver instead.
356
-
357
- This function is kept for backward compatibility but is no longer recommended.
358
- Use configure_hook_container with a custom provider_resolver for similar functionality.
359
-
360
- Args:
361
- factory: A callable that takes a class and returns an instance
362
- """
363
- import warnings
364
-
365
- warnings.warn(
366
- "set_default_hook_factory is deprecated. "
367
- "Use configure_hook_container with provider_resolver instead.",
368
- DeprecationWarning,
369
- stacklevel=2,
370
- )
371
-
372
- # Convert to container-style resolver
373
- def container_resolver(hook_cls):
374
- return factory(hook_cls)
375
-
376
- hook_factory = get_factory()
377
- hook_factory._container_resolver = container_resolver
378
-
379
-
380
- def configure_hook_container(
381
- container: Any,
382
- provider_name_resolver: Optional[Callable[[Type], str]] = None,
383
- provider_resolver: Optional[Callable[[Any, Type, str], Any]] = None,
384
- fallback_to_direct: bool = True,
385
- ) -> None:
386
- """
387
- Configure the hook system to use a dependency-injector container.
388
-
389
- This is the recommended way to integrate with dependency-injector.
390
- It automatically resolves hooks from container providers.
391
-
392
- Args:
393
- container: The dependency-injector container instance
394
- provider_name_resolver: Optional function to map hook class to provider name.
395
- Default: converts "LoanAccountHook" -> "loan_account_hook"
396
- provider_resolver: Optional function to resolve provider from container.
397
- Signature: (container, hook_cls, provider_name) -> instance
398
- Useful for nested container structures.
399
- fallback_to_direct: If True, falls back to direct instantiation when
400
- provider not found. If False, raises error.
401
-
402
- Example:
403
- >>> container = AppContainer()
404
- >>> configure_hook_container(container)
405
- """
406
- hook_factory = get_factory()
407
- hook_factory.configure_container(
408
- container,
409
- provider_name_resolver=provider_name_resolver,
410
- provider_resolver=provider_resolver,
411
- fallback_to_direct=fallback_to_direct,
412
- )
413
-
414
-
415
- def configure_nested_container(
416
- container: Any,
417
- container_path: str,
418
- provider_name_resolver: Optional[Callable[[Type], str]] = None,
419
- fallback_to_direct: bool = True,
420
- ) -> None:
421
- """
422
- DEPRECATED: Use configure_hook_container with provider_resolver instead.
423
-
424
- Configure the hook system for nested/hierarchical container structures.
425
- This is now handled better by passing a custom provider_resolver to
426
- configure_hook_container.
427
-
428
- Args:
429
- container: The root dependency-injector container
430
- container_path: Dot-separated path to sub-container (e.g., "loan_accounts_container")
431
- provider_name_resolver: Optional function to map hook class to provider name
432
- fallback_to_direct: If True, falls back to direct instantiation when provider not found
433
-
434
- Example:
435
- >>> # Instead of this:
436
- >>> configure_nested_container(app_container, "loan_accounts_container")
437
- >>>
438
- >>> # Use this:
439
- >>> def resolve_nested(container, hook_cls, provider_name):
440
- ... sub = container.loan_accounts_container()
441
- ... return getattr(sub, provider_name)()
442
- >>> configure_hook_container(app_container, provider_resolver=resolve_nested)
443
- """
444
- import warnings
445
-
446
- warnings.warn(
447
- "configure_nested_container is deprecated. "
448
- "Use configure_hook_container with provider_resolver instead.",
449
- DeprecationWarning,
450
- stacklevel=2,
451
- )
452
-
453
- def nested_resolver(container_obj, hook_cls, provider_name):
454
- """Navigate to sub-container and get provider."""
455
- # Navigate to sub-container
456
- current = container_obj
457
- for part in container_path.split("."):
458
- if not hasattr(current, part):
459
- raise ValueError(
460
- f"Container path '{container_path}' not found. Missing: {part}"
461
- )
462
- provider = getattr(current, part)
463
- # Call provider to get next level
464
- current = provider()
465
-
466
- # Get the hook provider from sub-container
467
- if not hasattr(current, provider_name):
468
- raise ValueError(
469
- f"Provider '{provider_name}' not found in sub-container. "
470
- f"Available: {[p for p in dir(current) if not p.startswith('_')]}"
471
- )
472
-
473
- hook_provider = getattr(current, provider_name)
474
- logger.debug(
475
- f"Resolved {hook_cls.__name__} from {container_path}.{provider_name}"
476
- )
477
- return hook_provider()
478
-
479
- configure_hook_container(
480
- container,
481
- provider_name_resolver=provider_name_resolver,
482
- provider_resolver=nested_resolver,
483
- fallback_to_direct=fallback_to_direct,
484
- )
485
-
486
-
487
- def clear_hook_factories() -> None:
488
- """
489
- Clear all registered hook factories and container configuration.
490
- Useful for testing.
491
- """
492
- hook_factory = get_factory()
493
- hook_factory.clear()
494
-
495
-
496
- def create_hook_instance(hook_cls: Type) -> Any:
497
- """
498
- Create a hook instance using the configured resolution strategy.
499
-
500
- Resolution order:
501
- 1. Specific factory registered via set_hook_factory()
502
- 2. Container resolver configured via configure_hook_container()
503
- 3. Direct instantiation hook_cls()
504
-
505
- Args:
506
- hook_cls: The hook class to instantiate
507
-
508
- Returns:
509
- An instance of the hook class
510
-
511
- Raises:
512
- Any exception raised by the factory, container, or constructor
513
- """
514
- hook_factory = get_factory()
515
- return hook_factory.create(hook_cls)
516
-
517
-
518
- def get_hook_factory(hook_cls: Type) -> Optional[Callable[[], Any]]:
519
- """
520
- Get the registered factory for a specific hook class.
521
-
522
- Args:
523
- hook_cls: The hook class to look up
524
-
525
- Returns:
526
- The registered factory function, or None if not registered
527
- """
528
- hook_factory = get_factory()
529
- return hook_factory.get_factory(hook_cls)
530
-
531
-
532
- def has_hook_factory(hook_cls: Type) -> bool:
533
- """
534
- Check if a hook class has a registered factory.
535
-
536
- Args:
537
- hook_cls: The hook class to check
538
-
539
- Returns:
540
- True if a specific factory is registered, False otherwise
541
- """
542
- hook_factory = get_factory()
543
- return hook_factory.has_factory(hook_cls)
544
-
545
-
546
- def is_container_configured() -> bool:
547
- """
548
- Check if a container resolver is configured.
549
-
550
- Returns:
551
- True if configure_hook_container() has been called
552
- """
553
- hook_factory = get_factory()
554
- return hook_factory.is_container_configured()
555
-
556
-
557
- def list_registered_factories() -> dict[Type, Callable]:
558
- """
559
- Get a copy of all registered hook factories.
560
-
561
- Returns:
562
- A dictionary mapping hook classes to their factory functions
563
- """
564
- hook_factory = get_factory()
565
- return hook_factory.list_factories()
1
+ """
2
+ Hook factory system for dependency injection.
3
+
4
+ This module provides seamless integration with dependency-injector containers,
5
+ allowing hooks to be managed as container providers with full DI support.
6
+
7
+ Usage Pattern 1 - Container Integration (Recommended):
8
+ ```python
9
+ from dependency_injector import containers, providers
10
+ from django_bulk_hooks import configure_hook_container
11
+
12
+ class LoanAccountContainer(containers.DeclarativeContainer):
13
+ loan_account_repository = providers.Singleton(LoanAccountRepository)
14
+ loan_account_service = providers.Singleton(LoanAccountService)
15
+ loan_account_validator = providers.Singleton(LoanAccountValidator)
16
+
17
+ # Define hook as a provider
18
+ loan_account_hook = providers.Singleton(
19
+ LoanAccountHook,
20
+ daily_loan_summary_service=Provide["daily_loan_summary_service"],
21
+ loan_account_service=loan_account_service,
22
+ loan_account_validator=loan_account_validator,
23
+ )
24
+
25
+ # Configure the hook system to use your container
26
+ container = LoanAccountContainer()
27
+ configure_hook_container(container)
28
+ ```
29
+
30
+ Usage Pattern 2 - Explicit Factory Registration:
31
+ ```python
32
+ from django_bulk_hooks import set_hook_factory
33
+
34
+ def create_loan_hook():
35
+ return container.loan_account_hook()
36
+
37
+ set_hook_factory(LoanAccountHook, create_loan_hook)
38
+ ```
39
+
40
+ Usage Pattern 3 - Custom Resolver:
41
+ ```python
42
+ from django_bulk_hooks import configure_hook_container
43
+
44
+ def custom_resolver(container, hook_cls, provider_name):
45
+ # Custom resolution logic for nested containers
46
+ return container.sub_container.get_provider(provider_name)()
47
+
48
+ configure_hook_container(container, provider_resolver=custom_resolver)
49
+ ```
50
+ """
51
+
52
+ import logging
53
+ import re
54
+ import threading
55
+ from collections.abc import Callable
56
+ from typing import Any
57
+
58
+ logger = logging.getLogger(__name__)
59
+
60
+
61
+ class HookFactory:
62
+ """
63
+ Creates hook handler instances with dependency injection.
64
+
65
+ Resolution order:
66
+ 1. Specific factory for hook class
67
+ 2. Container resolver (if configured)
68
+ 3. Direct instantiation
69
+ """
70
+
71
+ def __init__(self):
72
+ """Initialize an empty factory."""
73
+ self._specific_factories: dict[type, Callable[[], Any]] = {}
74
+ self._container_resolver: Callable[[type], Any] | None = None
75
+ self._lock = threading.RLock()
76
+
77
+ def register_factory(self, hook_cls: type, factory: Callable[[], Any]) -> None:
78
+ """
79
+ Register a factory function for a specific hook class.
80
+
81
+ The factory function should accept no arguments and return an instance
82
+ of the hook class with all dependencies injected.
83
+
84
+ Args:
85
+ hook_cls: The hook class to register a factory for
86
+ factory: A callable that returns an instance of hook_cls
87
+
88
+ Example:
89
+ >>> def create_loan_hook():
90
+ ... return container.loan_account_hook()
91
+ >>>
92
+ >>> factory.register_factory(LoanAccountHook, create_loan_hook)
93
+ """
94
+ with self._lock:
95
+ self._specific_factories[hook_cls] = factory
96
+
97
+ def configure_container(
98
+ self,
99
+ container: Any,
100
+ provider_name_resolver: Callable[[type], str] | None = None,
101
+ provider_resolver: Callable[[Any, type, str], Any] | None = None,
102
+ fallback_to_direct: bool = True,
103
+ ) -> None:
104
+ """
105
+ Configure the factory to use a dependency-injector container.
106
+
107
+ This is the recommended way to integrate with dependency-injector.
108
+ It automatically resolves hooks from container providers.
109
+
110
+ Args:
111
+ container: The dependency-injector container instance
112
+ provider_name_resolver: Optional function to map hook class to provider name.
113
+ Default: converts "LoanAccountHook" -> "loan_account_hook"
114
+ provider_resolver: Optional function to resolve provider from container.
115
+ Signature: (container, hook_cls, provider_name) -> instance
116
+ Useful for nested container structures or custom resolution logic.
117
+ fallback_to_direct: If True, falls back to direct instantiation when
118
+ provider not found. If False, raises error.
119
+
120
+ Example (Standard Container):
121
+ >>> class AppContainer(containers.DeclarativeContainer):
122
+ ... loan_service = providers.Singleton(LoanService)
123
+ ... loan_account_hook = providers.Singleton(
124
+ ... LoanAccountHook,
125
+ ... loan_service=loan_service,
126
+ ... )
127
+ >>>
128
+ >>> container = AppContainer()
129
+ >>> factory.configure_container(container)
130
+
131
+ Example (Custom Resolver for Nested Containers):
132
+ >>> def resolve_nested(container, hook_cls, provider_name):
133
+ ... # Navigate nested structure
134
+ ... sub_container = container.loan_accounts_container()
135
+ ... return getattr(sub_container, provider_name)()
136
+ >>>
137
+ >>> factory.configure_container(
138
+ ... container,
139
+ ... provider_resolver=resolve_nested
140
+ ... )
141
+ """
142
+ name_resolver = provider_name_resolver or self._default_name_resolver
143
+
144
+ def resolver(hook_cls: type) -> Any:
145
+ """Resolve hook instance from the container."""
146
+ provider_name = name_resolver(hook_cls)
147
+ name = getattr(hook_cls, "__name__", str(hook_cls))
148
+
149
+ # If custom provider resolver is provided, use it
150
+ if provider_resolver is not None:
151
+ try:
152
+ return provider_resolver(container, hook_cls, provider_name)
153
+ except Exception as e:
154
+ if fallback_to_direct:
155
+ return hook_cls()
156
+ raise
157
+
158
+ # Default resolution: look for provider directly on container
159
+ if hasattr(container, provider_name):
160
+ provider = getattr(container, provider_name)
161
+ # Call the provider to get the instance
162
+ return provider()
163
+
164
+ if fallback_to_direct:
165
+ return hook_cls()
166
+
167
+ raise ValueError(
168
+ f"Hook {name} not found in container. "
169
+ f"Expected provider name: '{provider_name}'. "
170
+ f"Available providers: {[p for p in dir(container) if not p.startswith('_')]}",
171
+ )
172
+
173
+ with self._lock:
174
+ self._container_resolver = resolver
175
+ container_name = getattr(
176
+ container.__class__,
177
+ "__name__",
178
+ str(container.__class__),
179
+ )
180
+ logger.info(
181
+ f"Configured hook factory to use container: {container_name}",
182
+ )
183
+
184
+ def create(self, hook_cls: type) -> Any:
185
+ """
186
+ Create a hook instance using the configured resolution strategy.
187
+
188
+ Resolution order:
189
+ 1. Specific factory registered via register_factory()
190
+ 2. Container resolver configured via configure_container()
191
+ 3. Direct instantiation hook_cls()
192
+
193
+ Args:
194
+ hook_cls: The hook class to instantiate
195
+
196
+ Returns:
197
+ An instance of the hook class
198
+
199
+ Raises:
200
+ Any exception raised by the factory, container, or constructor
201
+ """
202
+ with self._lock:
203
+ # 1. Check for specific factory
204
+ if hook_cls in self._specific_factories:
205
+ factory = self._specific_factories[hook_cls]
206
+ return factory()
207
+
208
+ # 2. Check for container resolver
209
+ if self._container_resolver is not None:
210
+ return self._container_resolver(hook_cls)
211
+
212
+ # 3. Fall back to direct instantiation
213
+ return hook_cls()
214
+
215
+ def clear(self) -> None:
216
+ """
217
+ Clear all registered factories and container configuration.
218
+ Useful for testing.
219
+ """
220
+ with self._lock:
221
+ self._specific_factories.clear()
222
+ self._container_resolver = None
223
+
224
+ def is_container_configured(self) -> bool:
225
+ """
226
+ Check if a container resolver is configured.
227
+
228
+ Returns:
229
+ True if configure_container() has been called
230
+ """
231
+ with self._lock:
232
+ return self._container_resolver is not None
233
+
234
+ def has_factory(self, hook_cls: type) -> bool:
235
+ """
236
+ Check if a hook class has a registered factory.
237
+
238
+ Args:
239
+ hook_cls: The hook class to check
240
+
241
+ Returns:
242
+ True if a specific factory is registered, False otherwise
243
+ """
244
+ with self._lock:
245
+ return hook_cls in self._specific_factories
246
+
247
+ def get_factory(self, hook_cls: type) -> Callable[[], Any] | None:
248
+ """
249
+ Get the registered factory for a specific hook class.
250
+
251
+ Args:
252
+ hook_cls: The hook class to look up
253
+
254
+ Returns:
255
+ The registered factory function, or None if not registered
256
+ """
257
+ with self._lock:
258
+ return self._specific_factories.get(hook_cls)
259
+
260
+ def list_factories(self) -> dict[type, Callable]:
261
+ """
262
+ Get a copy of all registered hook factories.
263
+
264
+ Returns:
265
+ A dictionary mapping hook classes to their factory functions
266
+ """
267
+ with self._lock:
268
+ return self._specific_factories.copy()
269
+
270
+ @staticmethod
271
+ def _default_name_resolver(hook_cls: type) -> str:
272
+ """
273
+ Default naming convention: LoanAccountHook -> loan_account_hook
274
+
275
+ Args:
276
+ hook_cls: Hook class to convert
277
+
278
+ Returns:
279
+ Snake-case provider name
280
+ """
281
+ name = hook_cls.__name__
282
+ # Convert CamelCase to snake_case
283
+ snake_case = re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()
284
+ return snake_case
285
+
286
+
287
+ # Global singleton factory
288
+ _factory: HookFactory | None = None
289
+ _factory_lock = threading.Lock()
290
+
291
+
292
+ def get_factory() -> HookFactory:
293
+ """
294
+ Get the global hook factory instance.
295
+
296
+ Creates the factory on first access (singleton pattern).
297
+ Thread-safe initialization.
298
+
299
+ Returns:
300
+ HookFactory singleton instance
301
+ """
302
+ global _factory
303
+
304
+ if _factory is None:
305
+ with _factory_lock:
306
+ # Double-checked locking
307
+ if _factory is None:
308
+ _factory = HookFactory()
309
+
310
+ return _factory
311
+
312
+
313
+ # Backward-compatible module-level functions
314
+ def set_hook_factory(hook_cls: type, factory: Callable[[], Any]) -> None:
315
+ """
316
+ Register a factory function for a specific hook class.
317
+
318
+ The factory function should accept no arguments and return an instance
319
+ of the hook class with all dependencies injected.
320
+
321
+ Args:
322
+ hook_cls: The hook class to register a factory for
323
+ factory: A callable that returns an instance of hook_cls
324
+
325
+ Example:
326
+ >>> def create_loan_hook():
327
+ ... return container.loan_account_hook()
328
+ >>>
329
+ >>> set_hook_factory(LoanAccountHook, create_loan_hook)
330
+ """
331
+ hook_factory = get_factory()
332
+ hook_factory.register_factory(hook_cls, factory)
333
+
334
+
335
+ def set_default_hook_factory(factory: Callable[[type], Any]) -> None:
336
+ """
337
+ DEPRECATED: Use configure_hook_container with provider_resolver instead.
338
+
339
+ This function is kept for backward compatibility but is no longer recommended.
340
+ Use configure_hook_container with a custom provider_resolver for similar functionality.
341
+
342
+ Args:
343
+ factory: A callable that takes a class and returns an instance
344
+ """
345
+ import warnings
346
+
347
+ warnings.warn(
348
+ "set_default_hook_factory is deprecated. Use configure_hook_container with provider_resolver instead.",
349
+ DeprecationWarning,
350
+ stacklevel=2,
351
+ )
352
+
353
+ # Convert to container-style resolver
354
+ def container_resolver(hook_cls):
355
+ return factory(hook_cls)
356
+
357
+ hook_factory = get_factory()
358
+ hook_factory._container_resolver = container_resolver
359
+
360
+
361
+ def configure_hook_container(
362
+ container: Any,
363
+ provider_name_resolver: Callable[[type], str] | None = None,
364
+ provider_resolver: Callable[[Any, type, str], Any] | None = None,
365
+ fallback_to_direct: bool = True,
366
+ ) -> None:
367
+ """
368
+ Configure the hook system to use a dependency-injector container.
369
+
370
+ This is the recommended way to integrate with dependency-injector.
371
+ It automatically resolves hooks from container providers.
372
+
373
+ Args:
374
+ container: The dependency-injector container instance
375
+ provider_name_resolver: Optional function to map hook class to provider name.
376
+ Default: converts "LoanAccountHook" -> "loan_account_hook"
377
+ provider_resolver: Optional function to resolve provider from container.
378
+ Signature: (container, hook_cls, provider_name) -> instance
379
+ Useful for nested container structures.
380
+ fallback_to_direct: If True, falls back to direct instantiation when
381
+ provider not found. If False, raises error.
382
+
383
+ Example:
384
+ >>> container = AppContainer()
385
+ >>> configure_hook_container(container)
386
+ """
387
+ hook_factory = get_factory()
388
+ hook_factory.configure_container(
389
+ container,
390
+ provider_name_resolver=provider_name_resolver,
391
+ provider_resolver=provider_resolver,
392
+ fallback_to_direct=fallback_to_direct,
393
+ )
394
+
395
+
396
+ def configure_nested_container(
397
+ container: Any,
398
+ container_path: str,
399
+ provider_name_resolver: Callable[[type], str] | None = None,
400
+ fallback_to_direct: bool = True,
401
+ ) -> None:
402
+ """
403
+ DEPRECATED: Use configure_hook_container with provider_resolver instead.
404
+
405
+ Configure the hook system for nested/hierarchical container structures.
406
+ This is now handled better by passing a custom provider_resolver to
407
+ configure_hook_container.
408
+
409
+ Args:
410
+ container: The root dependency-injector container
411
+ container_path: Dot-separated path to sub-container (e.g., "loan_accounts_container")
412
+ provider_name_resolver: Optional function to map hook class to provider name
413
+ fallback_to_direct: If True, falls back to direct instantiation when provider not found
414
+
415
+ Example:
416
+ >>> # Instead of this:
417
+ >>> configure_nested_container(app_container, "loan_accounts_container")
418
+ >>>
419
+ >>> # Use this:
420
+ >>> def resolve_nested(container, hook_cls, provider_name):
421
+ ... sub = container.loan_accounts_container()
422
+ ... return getattr(sub, provider_name)()
423
+ >>> configure_hook_container(app_container, provider_resolver=resolve_nested)
424
+ """
425
+ import warnings
426
+
427
+ warnings.warn(
428
+ "configure_nested_container is deprecated. Use configure_hook_container with provider_resolver instead.",
429
+ DeprecationWarning,
430
+ stacklevel=2,
431
+ )
432
+
433
+ def nested_resolver(container_obj, hook_cls, provider_name):
434
+ """Navigate to sub-container and get provider."""
435
+ # Navigate to sub-container
436
+ current = container_obj
437
+ for part in container_path.split("."):
438
+ if not hasattr(current, part):
439
+ raise ValueError(
440
+ f"Container path '{container_path}' not found. Missing: {part}",
441
+ )
442
+ provider = getattr(current, part)
443
+ # Call provider to get next level
444
+ current = provider()
445
+
446
+ # Get the hook provider from sub-container
447
+ if not hasattr(current, provider_name):
448
+ raise ValueError(
449
+ f"Provider '{provider_name}' not found in sub-container. Available: {[p for p in dir(current) if not p.startswith('_')]}",
450
+ )
451
+
452
+ hook_provider = getattr(current, provider_name)
453
+ return hook_provider()
454
+
455
+ configure_hook_container(
456
+ container,
457
+ provider_name_resolver=provider_name_resolver,
458
+ provider_resolver=nested_resolver,
459
+ fallback_to_direct=fallback_to_direct,
460
+ )
461
+
462
+
463
+ def clear_hook_factories() -> None:
464
+ """
465
+ Clear all registered hook factories and container configuration.
466
+ Useful for testing.
467
+ """
468
+ hook_factory = get_factory()
469
+ hook_factory.clear()
470
+
471
+
472
+ def create_hook_instance(hook_cls: type) -> Any:
473
+ """
474
+ Create a hook instance using the configured resolution strategy.
475
+
476
+ Resolution order:
477
+ 1. Specific factory registered via set_hook_factory()
478
+ 2. Container resolver configured via configure_hook_container()
479
+ 3. Direct instantiation hook_cls()
480
+
481
+ Args:
482
+ hook_cls: The hook class to instantiate
483
+
484
+ Returns:
485
+ An instance of the hook class
486
+
487
+ Raises:
488
+ Any exception raised by the factory, container, or constructor
489
+ """
490
+ hook_factory = get_factory()
491
+ return hook_factory.create(hook_cls)
492
+
493
+
494
+ def get_hook_factory(hook_cls: type) -> Callable[[], Any] | None:
495
+ """
496
+ Get the registered factory for a specific hook class.
497
+
498
+ Args:
499
+ hook_cls: The hook class to look up
500
+
501
+ Returns:
502
+ The registered factory function, or None if not registered
503
+ """
504
+ hook_factory = get_factory()
505
+ return hook_factory.get_factory(hook_cls)
506
+
507
+
508
+ def has_hook_factory(hook_cls: type) -> bool:
509
+ """
510
+ Check if a hook class has a registered factory.
511
+
512
+ Args:
513
+ hook_cls: The hook class to check
514
+
515
+ Returns:
516
+ True if a specific factory is registered, False otherwise
517
+ """
518
+ hook_factory = get_factory()
519
+ return hook_factory.has_factory(hook_cls)
520
+
521
+
522
+ def is_container_configured() -> bool:
523
+ """
524
+ Check if a container resolver is configured.
525
+
526
+ Returns:
527
+ True if configure_hook_container() has been called
528
+ """
529
+ hook_factory = get_factory()
530
+ return hook_factory.is_container_configured()
531
+
532
+
533
+ def list_registered_factories() -> dict[type, Callable]:
534
+ """
535
+ Get a copy of all registered hook factories.
536
+
537
+ Returns:
538
+ A dictionary mapping hook classes to their factory functions
539
+ """
540
+ hook_factory = get_factory()
541
+ return hook_factory.list_factories()