django-bulk-hooks 0.1.280__py3-none-any.whl → 0.2.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.

Potentially problematic release.


This version of django-bulk-hooks might be problematic. Click here for more details.

@@ -0,0 +1,565 @@
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()