django-bulk-hooks 0.1.83__py3-none-any.whl → 0.2.100__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,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 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()