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.
- django_bulk_hooks/__init__.py +20 -27
- django_bulk_hooks/changeset.py +214 -230
- django_bulk_hooks/conditions.py +12 -12
- django_bulk_hooks/decorators.py +68 -26
- django_bulk_hooks/dispatcher.py +369 -58
- django_bulk_hooks/factory.py +541 -565
- django_bulk_hooks/handler.py +106 -115
- django_bulk_hooks/helpers.py +258 -99
- django_bulk_hooks/manager.py +134 -130
- django_bulk_hooks/models.py +89 -76
- django_bulk_hooks/operations/__init__.py +5 -5
- django_bulk_hooks/operations/analyzer.py +299 -172
- django_bulk_hooks/operations/bulk_executor.py +742 -437
- django_bulk_hooks/operations/coordinator.py +928 -472
- django_bulk_hooks/operations/field_utils.py +335 -0
- django_bulk_hooks/operations/mti_handler.py +696 -473
- django_bulk_hooks/operations/mti_plans.py +103 -87
- django_bulk_hooks/operations/record_classifier.py +196 -0
- django_bulk_hooks/queryset.py +233 -189
- django_bulk_hooks/registry.py +276 -288
- {django_bulk_hooks-0.2.9.dist-info → django_bulk_hooks-0.2.93.dist-info}/METADATA +55 -4
- django_bulk_hooks-0.2.93.dist-info/RECORD +27 -0
- django_bulk_hooks/debug_utils.py +0 -145
- django_bulk_hooks-0.2.9.dist-info/RECORD +0 -26
- {django_bulk_hooks-0.2.9.dist-info → django_bulk_hooks-0.2.93.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.2.9.dist-info → django_bulk_hooks-0.2.93.dist-info}/WHEEL +0 -0
django_bulk_hooks/factory.py
CHANGED
|
@@ -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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
self.
|
|
74
|
-
self.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
>>>
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
...
|
|
124
|
-
...
|
|
125
|
-
...
|
|
126
|
-
...
|
|
127
|
-
|
|
128
|
-
>>>
|
|
129
|
-
>>> container
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
...
|
|
135
|
-
... sub_container
|
|
136
|
-
|
|
137
|
-
>>>
|
|
138
|
-
|
|
139
|
-
...
|
|
140
|
-
...
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
f"
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
"""
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
"""
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
"""
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
"""
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
)
|
|
386
|
-
"""
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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()
|