orionis 0.321.0__py3-none-any.whl → 0.323.0__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,543 +0,0 @@
1
- import inspect
2
- from threading import Lock
3
- from typing import Callable, Any, Dict, Deque, Optional, Type, get_origin, get_args
4
- from collections import deque
5
- from orionis._container.container_integrity import ContainerIntegrity
6
- from orionis._container.lifetimes import Lifetime
7
- from orionis._container.exception import OrionisContainerException, OrionisContainerValueError, OrionisContainerTypeError
8
- from orionis._contracts.container.container import IContainer
9
-
10
- class Container(IContainer):
11
- """
12
- Service container and dependency injection manager.
13
-
14
- This class follows the singleton pattern to manage service bindings, instances,
15
- and different lifecycle types such as transient, singleton, and scoped.
16
- """
17
-
18
- _instance = None
19
- _lock = Lock()
20
-
21
- @classmethod
22
- def destroy(cls):
23
- """
24
- Destroys the container instance.
25
- """
26
- cls._instance = None
27
-
28
- def __new__(cls):
29
- """
30
- Create a new instance of the container.
31
- """
32
- if cls._instance is None:
33
- with cls._lock:
34
- if cls._instance is None:
35
- cls._instance = super().__new__(cls)
36
- cls._instance._scoped_instances = {}
37
- cls._instance._singleton_instances = {}
38
- cls._instance._instances_services = {}
39
- cls._instance._transient_services = {}
40
- cls._instance._scoped_services = {}
41
- cls._instance._singleton_services = {}
42
- cls._instance._aliases_services = {}
43
- cls._instance.instance(IContainer, cls._instance)
44
- return cls._instance
45
-
46
- def bind(self, abstract: Callable[..., Any], concrete: Callable[..., Any], lifetime: str = Lifetime.TRANSIENT.value) -> None:
47
- """
48
- Binds an abstract type to a concrete implementation with a specified lifetime.
49
-
50
- Parameters
51
- ----------
52
- abstract : Callable[..., Any]
53
- The abstract base type or alias to be bound.
54
- concrete : Callable[..., Any]
55
- The concrete implementation to associate with the abstract type.
56
- lifetime : str
57
- The lifecycle of the binding. Must be one of 'transient', 'scoped', or 'singleton'.
58
-
59
- Raises
60
- ------
61
- OrionisContainerValueError
62
- If an invalid lifetime is provided or the concrete implementation is None.
63
-
64
- Examples
65
- --------
66
- >>> container.bind(MyService, MyServiceImplementation, "singleton")
67
- """
68
- if lifetime not in [member.value for member in Lifetime]:
69
- raise OrionisContainerValueError(f"Invalid lifetime type '{lifetime}'.")
70
-
71
- if concrete is None:
72
- raise OrionisContainerValueError("Concrete implementation cannot be None when binding a service.")
73
-
74
- abstract = abstract or concrete
75
- ContainerIntegrity.ensureIsCallable(concrete)
76
- ContainerIntegrity.ensureNotMain(concrete)
77
-
78
- service_entry = {
79
- "concrete": concrete,
80
- "async": inspect.iscoroutinefunction(concrete)
81
- }
82
-
83
- service_registry = {
84
- Lifetime.TRANSIENT.value: self._transient_services,
85
- Lifetime.SCOPED.value: self._scoped_services,
86
- Lifetime.SINGLETON.value: self._singleton_services
87
- }
88
-
89
- if ContainerIntegrity.isAbstract(abstract):
90
- ContainerIntegrity.ensureImplementation(abstract, concrete)
91
- service_registry[lifetime][abstract] = service_entry
92
- return
93
-
94
- if ContainerIntegrity.isAlias(abstract):
95
- service_registry[lifetime][abstract] = service_entry
96
- return
97
-
98
- raise OrionisContainerValueError(f"Invalid abstract type '{abstract}'. It must be a valid alias or an abstract class.")
99
-
100
- def transient(self, abstract: Callable[..., Any], concrete: Callable[..., Any]) -> None:
101
- """
102
- Registers a service with a transient lifetime.
103
-
104
- Parameters
105
- ----------
106
- abstract : Callable[..., Any]
107
- The abstract base type or alias to be bound.
108
- concrete : Callable[..., Any]
109
- The concrete implementation to associate with the abstract type.
110
-
111
- Examples
112
- --------
113
- >>> container.transient(MyService, MyServiceImplementation)
114
- """
115
-
116
- self.bind(abstract, concrete, Lifetime.TRANSIENT.value)
117
-
118
- def scoped(self, abstract: Callable[..., Any], concrete: Callable[..., Any]) -> None:
119
- """
120
- Registers a service with a scoped lifetime.
121
-
122
- Parameters
123
- ----------
124
- abstract : Callable[..., Any]
125
- The abstract base type or alias to be bound.
126
- concrete : Callable[..., Any]
127
- The concrete implementation to associate with the abstract type.
128
-
129
- Examples
130
- --------
131
- >>> container.scoped(MyService, MyServiceImplementation)
132
- """
133
-
134
- self.bind(abstract, concrete, Lifetime.SCOPED.value)
135
-
136
- def singleton(self, abstract: Callable[..., Any], concrete: Callable[..., Any]) -> None:
137
- """
138
- Registers a service with a singleton lifetime.
139
-
140
- Parameters
141
- ----------
142
- abstract : Callable[..., Any]
143
- The abstract base type or alias to be bound.
144
- concrete : Callable[..., Any]
145
- The concrete implementation to associate with the abstract type.
146
-
147
- Examples
148
- --------
149
- >>> container.singleton(MyService, MyServiceImplementation)
150
- """
151
-
152
- self.bind(abstract, concrete, Lifetime.SINGLETON.value)
153
-
154
- def instance(self, abstract: Callable[..., Any], instance: Any) -> None:
155
- """
156
- Registers an already instantiated object in the container.
157
-
158
- Parameters
159
- ----------
160
- abstract : Callable[..., Any]
161
- The abstract base type or alias to be bound.
162
- instance : Any
163
- The instance to be stored.
164
-
165
- Raises
166
- ------
167
- OrionisContainerValueError
168
- If the instance is None.
169
-
170
- Examples
171
- --------
172
- >>> container.instance(MyService, my_service_instance)
173
- """
174
-
175
- if instance is None:
176
- raise OrionisContainerValueError("The provided instance cannot be None.")
177
-
178
- ContainerIntegrity.ensureIsInstance(instance)
179
-
180
- if ContainerIntegrity.isAbstract(abstract):
181
- ContainerIntegrity.ensureImplementation(abstract, instance.__class__)
182
- self._instances_services[abstract] = instance
183
- return
184
-
185
- if ContainerIntegrity.isAlias(abstract):
186
- self._instances_services[abstract] = instance
187
- return
188
-
189
- raise OrionisContainerValueError(f"Invalid abstract type '{abstract}'. It must be a valid alias or an abstract class.")
190
-
191
- def bound(self, abstract_or_alias: Callable[..., Any]) -> bool:
192
- """
193
- Checks if a service or alias is bound in the container.
194
-
195
- Parameters
196
- ----------
197
- abstract_or_alias : Callable[..., Any]
198
- The abstract type or alias to check.
199
-
200
- Returns
201
- -------
202
- bool
203
- True if the service is bound, False otherwise.
204
-
205
- Examples
206
- --------
207
- >>> container.bound(MyService)
208
- True
209
- """
210
-
211
- service_dicts = [
212
- self._instances_services,
213
- self._transient_services,
214
- self._scoped_services,
215
- self._singleton_services,
216
- self._aliases_services
217
- ]
218
- return any(abstract_or_alias in service_dict for service_dict in service_dicts)
219
-
220
- def has(self, abstract_or_alias: Callable[..., Any]) -> bool:
221
- """
222
- Alias for `bound()` method.
223
-
224
- Parameters
225
- ----------
226
- abstract_or_alias : Callable[..., Any]
227
- The abstract type or alias to check.
228
-
229
- Returns
230
- -------
231
- bool
232
- True if the service is bound, False otherwise.
233
-
234
- Examples
235
- --------
236
- >>> container.has(MyService)
237
- True
238
- """
239
-
240
- return self.bound(abstract_or_alias)
241
-
242
- def alias(self, alias: Callable[..., Any], abstract: Callable[..., Any]) -> None:
243
- """
244
- Creates an alias for an existing abstract binding.
245
-
246
- Parameters
247
- ----------
248
- alias : Callable[..., Any]
249
- The alias name.
250
- abstract : Callable[..., Any]
251
- The existing abstract type to alias.
252
-
253
- Raises
254
- ------
255
- OrionisContainerValueError
256
- If the abstract type is not registered or the alias is already in use.
257
-
258
- Examples
259
- --------
260
- >>> container.alias("DatabaseService", MyDatabaseService)
261
- """
262
-
263
- if not self.has(abstract):
264
- raise OrionisContainerValueError(f"Abstract '{abstract}' is not registered in the container.")
265
-
266
- if alias in self._aliases_services:
267
- raise OrionisContainerValueError(f"Alias '{alias}' is already in use.")
268
-
269
- if not ContainerIntegrity.isAlias(abstract):
270
- raise OrionisContainerValueError(f"Invalid target abstract type: {abstract}. It must be an alias.")
271
-
272
- self._aliases_services[alias] = abstract
273
-
274
- def isAlias(self, name: str) -> bool:
275
- """
276
- Checks if a given name is an alias.
277
-
278
- Parameters
279
- ----------
280
- name : str
281
- The name to check.
282
-
283
- Returns
284
- -------
285
- bool
286
- True if the name is an alias, False otherwise.
287
-
288
- Raises
289
- ------
290
- OrionisContainerTypeError
291
- If the name is not a string.
292
-
293
- Examples
294
- --------
295
- >>> container.isAlias("DatabaseService")
296
- True
297
- """
298
-
299
- if not isinstance(name, str):
300
- raise OrionisContainerTypeError("The name must be a valid string.")
301
- return name in self._aliases_services
302
-
303
- def getBindings(self) -> Dict[str, Any]:
304
- """
305
- Retrieves all registered service bindings.
306
-
307
- Returns
308
- -------
309
- dict
310
- A dictionary containing all instances, transient, scoped, singleton, and alias services.
311
-
312
- Examples
313
- --------
314
- >>> container.getBindings()
315
- """
316
-
317
- return {
318
- "instances": self._instances_services,
319
- "transient": self._transient_services,
320
- "scoped": self._scoped_services,
321
- "singleton": self._singleton_services,
322
- "aliases": self._aliases_services
323
- }
324
-
325
- def getAlias(self, name: str) -> Callable[..., Any]:
326
- """
327
- Retrieves the abstract type associated with an alias.
328
-
329
- Parameters
330
- ----------
331
- name : str
332
- The alias name.
333
-
334
- Returns
335
- -------
336
- Callable[..., Any]
337
- The abstract type associated with the alias.
338
-
339
- Raises
340
- ------
341
- OrionisContainerValueError
342
- If the alias is not registered.
343
-
344
- Examples
345
- --------
346
- >>> container.getAlias("DatabaseService")
347
- <class 'MyDatabaseService'>
348
- """
349
-
350
- if not isinstance(name, str):
351
- raise OrionisContainerValueError("The name must be a valid string.")
352
-
353
- if name not in self._aliases_services:
354
- raise OrionisContainerValueError(f"Alias '{name}' is not registered in the container.")
355
-
356
- return self._aliases_services[name]
357
-
358
- def forgetScopedInstances(self) -> None:
359
- """
360
- Clears all scoped instances.
361
-
362
- Examples
363
- --------
364
- >>> container.forgetScopedInstances()
365
- """
366
-
367
- self._scoped_instances = {}
368
-
369
- def newRequest(self) -> None:
370
- """
371
- Resets scoped instances to handle a new request.
372
-
373
- Examples
374
- --------
375
- >>> container.newRequest()
376
- """
377
-
378
- self.forgetScopedInstances()
379
-
380
- async def make(self, abstract_or_alias: Callable[..., Any]) -> Any:
381
- """
382
- Resolves and instantiates a service from the container.
383
-
384
- Parameters
385
- ----------
386
- abstract_or_alias : Callable[..., Any]
387
- The abstract type or alias to resolve.
388
-
389
- Returns
390
- -------
391
- Any
392
- The instantiated service.
393
-
394
- Raises
395
- ------
396
- OrionisContainerException
397
- If the service is not found.
398
-
399
- Examples
400
- --------
401
- >>> service = await container.make(MyService)
402
- """
403
- if abstract_or_alias in self._aliases_services:
404
- abstract_or_alias = self._aliases_services[abstract_or_alias]
405
-
406
- if abstract_or_alias in self._instances_services:
407
- return self._instances_services[abstract_or_alias]
408
-
409
- if abstract_or_alias in self._singleton_services:
410
- if abstract_or_alias not in self._singleton_instances:
411
- service = self._singleton_services[abstract_or_alias]
412
- self._singleton_instances[abstract_or_alias] = await self._resolve(service['concrete'])
413
- return self._singleton_instances[abstract_or_alias]
414
-
415
- if abstract_or_alias in self._scoped_services:
416
- if abstract_or_alias not in self._scoped_instances:
417
- service = self._scoped_services[abstract_or_alias]
418
- self._scoped_instances[abstract_or_alias] = await self._resolve(service['concrete'])
419
- return self._scoped_instances[abstract_or_alias]
420
-
421
- if abstract_or_alias in self._transient_services:
422
- service = self._transient_services[abstract_or_alias]
423
- return await self._resolve(service['concrete'])
424
-
425
- raise OrionisContainerException(f"No binding found for '{abstract_or_alias}' in the container.")
426
-
427
- async def _resolve(self, concrete: Callable[..., Any], resolving: Optional[Deque[Type]] = None) -> Any:
428
- """
429
- Asynchronous method to resolve dependencies recursively and instantiate a class.
430
-
431
- Parameters
432
- ----------
433
- concrete : Callable[..., Any]
434
- The concrete implementation to instantiate.
435
- resolving : Optional[Deque[Type]], optional
436
- A queue to track resolving dependencies and prevent circular dependencies.
437
-
438
- Returns
439
- -------
440
- Any
441
- The instantiated object.
442
-
443
- Raises
444
- ------
445
- OrionisContainerException
446
- If circular dependencies are detected or instantiation fails.
447
-
448
- Examples
449
- --------
450
- >>> instance = await container._resolve(MyClass)
451
- """
452
- if resolving is None:
453
- resolving = deque()
454
-
455
- if concrete in resolving:
456
- raise OrionisContainerException(f"Circular dependency detected for {concrete}.")
457
-
458
- resolving.append(concrete)
459
-
460
- try:
461
- signature = inspect.signature(concrete)
462
- except ValueError as e:
463
- raise OrionisContainerException(f"Unable to inspect signature of {concrete}: {str(e)}")
464
-
465
- resolved_dependencies: Dict[str, Any] = {}
466
- unresolved_dependencies = deque()
467
-
468
- for param_name, param in signature.parameters.items():
469
- if param_name == 'self':
470
- continue
471
-
472
- if param.kind in (param.VAR_POSITIONAL, param.VAR_KEYWORD):
473
- continue
474
-
475
- if param.annotation is param.empty and param.default is param.empty:
476
- unresolved_dependencies.append(param_name)
477
- continue
478
-
479
- if param.default is not param.empty:
480
- resolved_dependencies[param_name] = param.default
481
- continue
482
-
483
- if param.annotation is not param.empty:
484
- param_type = param.annotation
485
-
486
- if get_origin(param_type) is not None:
487
- param_type = get_args(param_type)[0]
488
-
489
- if isinstance(param_type, type) and not issubclass(param_type, (int, str, bool, float)):
490
- if self.has(param_type):
491
- resolved_dependencies[param_name] = await self.make(param_type)
492
- else:
493
- resolved_dependencies[param_name] = await self._resolve_dependency(param_type, resolving)
494
- else:
495
- resolved_dependencies[param_name] = param_type
496
-
497
- while unresolved_dependencies:
498
- dep_name = unresolved_dependencies.popleft()
499
- if dep_name not in resolved_dependencies:
500
- resolved_dependencies[dep_name] = await self._resolve_dependency(dep_name, resolving)
501
-
502
- try:
503
- instance = concrete(**resolved_dependencies)
504
- resolving.pop()
505
- return instance
506
- except Exception as e:
507
- raise OrionisContainerException(f"Failed to instantiate {concrete}: {str(e)}")
508
-
509
- async def _resolve_dependency(self, dep_type: Any, resolving: Optional[Deque[Type]] = None) -> Any:
510
- """
511
- Asynchronously resolves a dependency by instantiating or retrieving it from the container.
512
-
513
- Parameters
514
- ----------
515
- dep_type : Any
516
- The dependency type to resolve.
517
- resolving : Optional[Deque[Type]], optional
518
- A queue to track resolving dependencies.
519
-
520
- Returns
521
- -------
522
- Any
523
- The resolved dependency.
524
-
525
- Raises
526
- ------
527
- OrionisContainerException
528
- If the dependency cannot be resolved.
529
-
530
- Examples
531
- --------
532
- >>> dependency = await container._resolve_dependency(MyDependency)
533
- """
534
- if resolving is None:
535
- resolving = deque()
536
-
537
- if isinstance(dep_type, type):
538
- if self.has(dep_type):
539
- return await self.make(dep_type)
540
- else:
541
- return await self._resolve(dep_type, resolving)
542
-
543
- raise OrionisContainerException(f"Cannot resolve dependency of type {dep_type}")