cndi 0.1.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.
Files changed (46) hide show
  1. cndi/__init__.py +4 -0
  2. cndi/annotations/__init__.py +416 -0
  3. cndi/annotations/component.py +11 -0
  4. cndi/annotations/events.py +79 -0
  5. cndi/annotations/threads.py +35 -0
  6. cndi/autoconfiguration/__init__.py +0 -0
  7. cndi/autoconfiguration/configure.py +11 -0
  8. cndi/binders/__init__.py +0 -0
  9. cndi/binders/consts.py +1 -0
  10. cndi/binders/message/__init__.py +152 -0
  11. cndi/binders/message/mqtt.py +21 -0
  12. cndi/binders/message/rabbitmq.py +163 -0
  13. cndi/binders/message/s3.py +72 -0
  14. cndi/binders/message/utils.py +26 -0
  15. cndi/consts.py +22 -0
  16. cndi/env.py +197 -0
  17. cndi/exception/__init__.py +7 -0
  18. cndi/flask/__init__.py +0 -0
  19. cndi/flask/flask_app.py +106 -0
  20. cndi/healthchecks.py +52 -0
  21. cndi/initializers.py +150 -0
  22. cndi/minio/__init__.py +19 -0
  23. cndi/rasa/__init__.py +0 -0
  24. cndi/rasa/configs.py +68 -0
  25. cndi/resources.py +36 -0
  26. cndi/secrets/__init__.py +0 -0
  27. cndi/secrets/vault.py +46 -0
  28. cndi/tasks/__init__.py +93 -0
  29. cndi/tests.py +21 -0
  30. cndi/utils.py +81 -0
  31. cndi/validation/__init__.py +19 -0
  32. cndi/version.py +1 -0
  33. cndi-0.1.0.dist-info/METADATA +53 -0
  34. cndi-0.1.0.dist-info/RECORD +46 -0
  35. cndi-0.1.0.dist-info/WHEEL +5 -0
  36. cndi-0.1.0.dist-info/licenses/LICENSE +339 -0
  37. cndi-0.1.0.dist-info/top_level.txt +2 -0
  38. tests/__init__.py +0 -0
  39. tests/cndi/__init__.py +0 -0
  40. tests/cndi/annotations/__init__.py +0 -0
  41. tests/cndi/annotations/test_app_initializer.py +14 -0
  42. tests/cndi/annotations/test_components.py +38 -0
  43. tests/cndi/binders/__init__.py +0 -0
  44. tests/cndi/binders/message/__init__.py +74 -0
  45. tests/test_load_envs.py +22 -0
  46. tests/test_resource_finder.py +33 -0
cndi/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ import logging
2
+
3
+ logging.basicConfig(format=f'%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO)
4
+ BASE_NAME = "RCN"
@@ -0,0 +1,416 @@
1
+ """
2
+ This module provides functionality for managing components and beans in the application.
3
+
4
+ It includes functionality for importing modules, normalizing module and class names, and managing various stores for beans, components, profiles, conditional rendering, and overrides.
5
+
6
+ Variables:
7
+ validatedBeans: A list of validated beans.
8
+ beans: A list of beans.
9
+ autowires: A list of autowires.
10
+ components: A list of components.
11
+ beanStore: A dictionary storing beans.
12
+ componentStore: A dictionary storing components.
13
+ profilesStores: A dictionary storing profiles.
14
+ conditionalRender: A dictionary storing conditional rendering settings.
15
+ overrideStore: A dictionary storing overrides.
16
+
17
+ Functions:
18
+ importModuleName: Imports a module given its full name.
19
+ normaliseModuleAndClassName: Normalizes a module and class name.
20
+ """
21
+
22
+ import logging
23
+ import os
24
+ import types
25
+ from typing import Callable, Any
26
+
27
+ from cndi.annotations.component import ComponentClass
28
+ from cndi.env import RCN_ACTIVE_PROFILE
29
+ from cndi.exception import BeanNotFoundException
30
+
31
+ logger = logging.getLogger("cndi.annotations")
32
+
33
+ class SingletonContext:
34
+ """Singleton context manager for storing beans, components, and related data."""
35
+ _instance = None
36
+ _frozen = False
37
+
38
+ def __new__(cls):
39
+ if cls._instance is None:
40
+ cls._instance = super(SingletonContext, cls).__new__(cls)
41
+ cls._instance.validatedBeans = []
42
+ cls._instance.beans = []
43
+ cls._instance.autowires = []
44
+ cls._instance.components = []
45
+ cls._instance.beanStore = {}
46
+ cls._instance.componentStore = {}
47
+ cls._instance.profilesStores = {}
48
+ cls._instance.conditionalRender = {}
49
+ cls._instance.overrideStore = {}
50
+ return cls._instance
51
+
52
+ def freeze(self):
53
+ """Freeze the context to make all stored values immutable."""
54
+ self.validatedBeans = tuple(self.validatedBeans)
55
+ self.beans = tuple(self.beans)
56
+ self.autowires = tuple(self.autowires)
57
+ self.components = tuple(self.components)
58
+ self.beanStore = types.MappingProxyType(self.beanStore)
59
+ self.componentStore = types.MappingProxyType(self.componentStore)
60
+ self.profilesStores = types.MappingProxyType(self.profilesStores)
61
+ self.conditionalRender = types.MappingProxyType(self.conditionalRender)
62
+ self.overrideStore = types.MappingProxyType(self.overrideStore)
63
+ SingletonContext._frozen = True
64
+
65
+ def __setattr__(self, name: str, value: Any) -> None:
66
+ if SingletonContext._frozen:
67
+ raise AttributeError(f"Cannot modify frozen context attribute: {name}")
68
+ super().__setattr__(name, value)
69
+
70
+ context = SingletonContext()
71
+
72
+ from functools import wraps
73
+ import importlib
74
+ import copy
75
+
76
+
77
+ def importModuleName(fullname):
78
+ modules = fullname.split('.')
79
+ module = importlib.import_module(modules[-1], package='.'.join(modules[:-1]))
80
+ return module
81
+
82
+
83
+ def normaliseModuleAndClassName(name):
84
+ nameList: list = name.split(".")
85
+ if "__init__" in nameList:
86
+ nameList.remove("__init__")
87
+ return '.'.join(nameList)
88
+
89
+
90
+ def getBeanObject(objectType):
91
+ """
92
+ Retrieves a bean object from the bean storage.
93
+
94
+ This function queries the bean storage for a bean of the specified type. If the bean is found, it returns a new instance of the bean if the 'newInstance' attribute of the bean is True, otherwise it returns the existing instance.
95
+
96
+ Args:
97
+ objectType: The type of the bean to retrieve.
98
+
99
+ Returns:
100
+ An instance of the bean of the specified type.
101
+ """
102
+ if isinstance(objectType, type):
103
+ objectType = normaliseModuleAndClassName('.'.join([objectType.__module__, objectType.__name__]))
104
+ bean = queryBeanStorage(objectType)
105
+ objectInstance = bean['objectInstance']
106
+ return copy.deepcopy(objectInstance) if bean['newInstance'] else objectInstance
107
+
108
+
109
+ def queryBeanStorage(fullname):
110
+ objectType = normaliseModuleAndClassName(fullname)
111
+ bean = context.beanStore[objectType]
112
+ return bean
113
+
114
+
115
+ class AutowiredClass:
116
+ def __init__(self, required, func, kwargs: dict):
117
+ self.fullname = '.'.join([func.__qualname__])
118
+ self.className = normaliseModuleAndClassName('.'.join(func.__qualname__.split(".")[:-1]))
119
+ self.func = func
120
+ self.kwargs = kwargs
121
+ self.required = required
122
+ self.context = SingletonContext()
123
+
124
+ def dependencyInject(self):
125
+ dependencies = self.calculateDependencies()
126
+ dependencyNotFound = list()
127
+ for dependency in dependencies:
128
+ if dependency not in self.context.beanStore:
129
+ dependencyNotFound.append(dependency)
130
+
131
+ if len(dependencyNotFound) > 0:
132
+ logger.warning(f"Skipping {self.fullname}")
133
+ assert not self.required, "Could not initialize " + self.fullname + " with beans " + str(
134
+ dependencyNotFound)
135
+
136
+ kwargs = self.kwargs
137
+ args = dict()
138
+ for (key, value) in kwargs.items():
139
+ fullName = normaliseModuleAndClassName('.'.join([value.__module__, value.__name__]))
140
+ if fullName in self.context.beanStore:
141
+ args[key] = getBeanObject(fullName)
142
+
143
+ if self.className in self.context.beanStore:
144
+ self.func(self.context.beanStore[self.className], **args)
145
+ else:
146
+ logger.debug(f"{self.className} {self.fullname}")
147
+ self.func(**args)
148
+
149
+ def calculateDependencies(self):
150
+ return list(
151
+ map(lambda dependency: normaliseModuleAndClassName('.'.join([dependency.__module__, dependency.__name__])),
152
+ self.kwargs.values()))
153
+
154
+
155
+ def OverrideBeanType(type: object):
156
+ """
157
+ OverrideBeanType is used to override the current annotated @Component class to be injected as some other object
158
+
159
+ :param type: Class type to override while performing Dependency Injection
160
+ :return: function wrapper
161
+ """
162
+
163
+ def inner_function(func):
164
+ @wraps(func)
165
+ def wrapper(*args, **kwargs):
166
+ return func(*args, **kwargs)
167
+
168
+ fullname = '.'.join([wrapper.__module__, wrapper.__name__])
169
+
170
+ context.overrideStore[fullname] = {
171
+ "func": wrapper,
172
+ "overrideType": type
173
+ }
174
+ return wrapper
175
+
176
+ return inner_function
177
+
178
+
179
+ def queryOverideBeanStore(fullname):
180
+ if fullname in context.overrideStore:
181
+ return context.overrideStore[fullname]
182
+ else:
183
+ return None
184
+
185
+
186
+ def Component(func: object):
187
+ """
188
+ A decorator that registers a class as a component.
189
+
190
+ When a class is decorated with @Component, the AppInitializer tries to automatically initialize the class. The class is registered with its full name, which is constructed from the module name and the class name.
191
+
192
+ Args:
193
+ func: The class to be registered as a component.
194
+
195
+ Returns:
196
+ The decorated class.
197
+ """
198
+ @wraps(func)
199
+ def wrapper(*args, **kwargs):
200
+ return func(*args, **kwargs)
201
+
202
+ moduleName = wrapper.__module__[:-9] if wrapper.__module__.endswith(".__init__") else wrapper.__module__
203
+ componentFullName = '.'.join([moduleName, wrapper.__qualname__])
204
+ logger.debug(f"Registering Function name " + componentFullName)
205
+ duplicateComponents = list(filter(lambda component: component.fullname == componentFullName, context.components))
206
+ if duplicateComponents.__len__() > 0:
207
+ logger.debug(f"Duplicate Component found for: {duplicateComponents}")
208
+ else:
209
+ context.components.append(ComponentClass(**{
210
+ 'fullname': componentFullName,
211
+ 'func': wrapper,
212
+ 'annotations': wrapper.__init__.__annotations__ if "__annotations__" in dir(wrapper.__init__) else {}
213
+ }))
214
+ return wrapper
215
+
216
+
217
+ def validateBean(fullname):
218
+ """
219
+ Validates Beans before performing the dependency injection
220
+ :param fullname: bean class full classpath name
221
+ :return: Boolean type
222
+ """
223
+
224
+ profile = queryProfileData(fullname)
225
+ condition = queryContitionalRenderingStore(fullname)
226
+ if profile is None and condition is None:
227
+ return True
228
+ flag = True
229
+
230
+ if profile is not None:
231
+ profileNames = set(profile['profiles'])
232
+ environmentProfiles = set(map(lambda x: x.strip(), os.environ[RCN_ACTIVE_PROFILE].split(",")))
233
+ intersectionProfiles = profileNames.intersection(environmentProfiles)
234
+
235
+ flag &= intersectionProfiles.__len__() >= 1
236
+
237
+ if condition is not None:
238
+ callback = condition['callback']
239
+ callbackValue = callback(condition['func'])
240
+ flag &= bool(callbackValue)
241
+
242
+ if flag is False:
243
+ logger.debug("Validation Failed for Bean " + fullname)
244
+
245
+ return flag
246
+
247
+
248
+ def Bean(newInstance=False):
249
+ """
250
+
251
+ :param newInstance:
252
+ :return:
253
+ """
254
+
255
+ def inner_function(func):
256
+ annotations = func.__annotations__
257
+ returnType = annotations['return']
258
+ del annotations['return']
259
+
260
+ @wraps(func)
261
+ def wrapper(*args, **kwargs):
262
+ return func(*args, **kwargs)
263
+
264
+ fullname = '.'.join([returnType.__module__, returnType.__name__])
265
+ annotations = dict(
266
+ map(lambda key: (key, '.'.join([annotations[key].__module__, annotations[key].__qualname__])), annotations))
267
+
268
+ if validateBean(fullname):
269
+ context.beans.append({
270
+ 'name': fullname,
271
+ 'newInstance': newInstance,
272
+ 'object': wrapper,
273
+ 'fullname': wrapper.__qualname__,
274
+ 'kwargs': annotations,
275
+ 'index': len(context.beans)
276
+ })
277
+
278
+ return wrapper
279
+ else:
280
+ return None
281
+
282
+ return inner_function
283
+
284
+
285
+ def ConditionalRendering(callback=lambda method: True, overrideFullName = None):
286
+ """
287
+ A decorator that conditionally renders a class based on a callback function.
288
+
289
+ This decorator checks the return value of the provided callback function. If the callback returns True, the class is rendered. If the callback returns False, the class is not rendered.
290
+
291
+ Args:
292
+ callback: A function that determines whether the class should be rendered. This function should take no arguments and return a boolean.
293
+
294
+ Returns:
295
+ The decorated class, if the callback returns True. None, if the callback returns False.
296
+ """
297
+
298
+ def inner_function(func: object):
299
+ @wraps(func)
300
+ def wrapper(*args, **kwargs):
301
+ return func(*args, **kwargs)
302
+
303
+ fullname = ".".join([wrapper.__module__, wrapper.__qualname__]) if overrideFullName is None else overrideFullName
304
+
305
+ context.conditionalRender[fullname] = {
306
+ "func": wrapper,
307
+ "callback": callback
308
+ }
309
+
310
+ return wrapper
311
+
312
+ return inner_function
313
+
314
+
315
+ def queryContitionalRenderingStore(fullname):
316
+ if fullname in context.conditionalRender:
317
+ return context.conditionalRender[fullname]
318
+ else:
319
+ return None
320
+
321
+ def constructKeyWordArguments(annotations, required=True):
322
+ kwargs = dict()
323
+ for key, classObject in annotations.items():
324
+ beanName = f"{classObject.__module__}.{classObject.__name__}"
325
+ if beanName in context.beanStore:
326
+ kwargs[key] = getBeanObject(beanName)
327
+ elif required:
328
+ raise BeanNotFoundException(f"Following Bean failed to load in SingletonContext: "+beanName)
329
+ else:
330
+ logger.warn(f"Bean not found {beanName} and required is set to false")
331
+ return kwargs
332
+
333
+ def Profile(profiles=["default"]):
334
+ """
335
+
336
+ :param profiles:
337
+ :return:
338
+ """
339
+
340
+ def inner_function(func: Callable):
341
+ @wraps(func)
342
+ def wrapper(*args, **kwargs):
343
+ return func(*args, **kwargs)
344
+
345
+ fullname = ".".join([wrapper.__module__, wrapper.__qualname__])
346
+ context.profilesStores[fullname] = {
347
+ "func": wrapper,
348
+ "profiles": profiles
349
+ }
350
+
351
+ return wrapper
352
+
353
+ return inner_function
354
+
355
+
356
+ def queryProfileData(fullname):
357
+ if fullname in context.profilesStores:
358
+ return context.profilesStores.get(fullname)
359
+ else:
360
+ return None
361
+
362
+
363
+ def Autowired(required=True):
364
+ """
365
+
366
+ :param required:
367
+ :return:
368
+ """
369
+
370
+ def inner_function(func: object):
371
+ annotations = func.__annotations__
372
+
373
+ @wraps(func)
374
+ def wrapper(*args, **kwargs):
375
+ return func(*args, **kwargs)
376
+
377
+ context.autowires.append(AutowiredClass(required=required, **{
378
+ 'kwargs': annotations,
379
+ 'func': wrapper
380
+ }))
381
+
382
+ return wrapper
383
+
384
+ return inner_function
385
+
386
+ def getBean(beans, name):
387
+ return list(filter(lambda x: x['name'] == name, beans))[0]
388
+
389
+
390
+ def workOrder(beans):
391
+ allBeanNames = list(map(lambda bean: bean['name'], beans))
392
+ beanQueue = list(filter(lambda bean: len(bean['kwargs']) == 0, beans))
393
+ beanIndexes = list(map(lambda bean: bean['index'], beanQueue))
394
+
395
+ beanDependents = list(filter(lambda bean: bean['index'] not in beanIndexes, beans))
396
+ beanQueueNames = list(map(lambda bean: bean['name'], beanQueue))
397
+
398
+ for i in range(len(beanQueue)):
399
+ beanQueue[i]['index'] = i
400
+
401
+ for dependents in beanDependents:
402
+ args = list(dependents['kwargs'].values())
403
+ flag = True
404
+ for argClassName in args:
405
+ if (argClassName not in beanQueueNames and argClassName in allBeanNames) or argClassName in beanQueueNames:
406
+ flag = flag and True
407
+ dependents['index'] = getBean(beans, argClassName)['index'] + max(beanIndexes)
408
+ else:
409
+ flag = False
410
+
411
+ if flag:
412
+ beanQueue.append(dependents)
413
+ beanQueueNames.append(dependents['name'])
414
+
415
+ assert len(beanQueue) == len(beans), "Somebeans were not initialized properly"
416
+ return list(sorted(beanQueue, key=lambda x: x['index']))
@@ -0,0 +1,11 @@
1
+ class ComponentClass:
2
+ def __init__(self, fullname, func, annotations):
3
+ self.fullname = fullname
4
+ self.func = func
5
+ self.annotations = annotations
6
+
7
+ def getInnerAutowiredClasses(self, autowires):
8
+ return list(filter(
9
+ lambda autowire: autowire.className == self.fullname,
10
+ autowires
11
+ ))
@@ -0,0 +1,79 @@
1
+ import asyncio
2
+ import dataclasses
3
+ import logging
4
+ from concurrent.futures.thread import ThreadPoolExecutor
5
+ from functools import wraps
6
+ from typing import Dict, Callable
7
+
8
+ from cndi.annotations import Component, constructKeyWordArguments, ConditionalRendering
9
+ from cndi.consts import RCN_EVENTS_ENABLE, RCN_EVENTS_NUM_THREADS
10
+ from cndi.env import getContextEnvironment
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ class BuiltInEventsTypes:
15
+ ON_ENV_LOAD="on_env_load"
16
+ ON_CONTEXT_LOAD="on_context_load"
17
+
18
+ @dataclasses.dataclass
19
+ class Event(object):
20
+ def __init__(self, eventType,
21
+ eventCallback, kwargs=None):
22
+ if kwargs is None:
23
+ kwargs = {}
24
+ self.eventType = eventType
25
+ self.eventCallback: Callable = eventCallback
26
+ self.kwargs = kwargs
27
+
28
+ REGISTERED_EVENTS: Dict[str, dict[str, Event]] = dict()
29
+ def register_event(event: Event, func_name: str):
30
+ if event.eventType not in REGISTERED_EVENTS:
31
+ REGISTERED_EVENTS[event.eventType] = dict()
32
+
33
+ REGISTERED_EVENTS[event.eventType][func_name] = event
34
+
35
+ def OnEvent(event):
36
+ def inner_function(func):
37
+ annotations = func.__annotations__
38
+
39
+ func_name = '.'.join([func.__module__, func.__qualname__])
40
+ @wraps(func)
41
+ def wrapper(*args, **kwargs):
42
+ if asyncio.iscoroutinefunction(func):
43
+ return asyncio.run(func(*args, **kwargs))
44
+ return func(*args, **kwargs)
45
+
46
+ register_event(Event(eventType=event, eventCallback=wrapper, kwargs=annotations), func_name)
47
+
48
+ return wrapper
49
+ return inner_function
50
+
51
+ class EventNotFound(Exception):
52
+ def __init__(self, *args):
53
+ super().__init__( *args)
54
+
55
+ @Component
56
+ @ConditionalRendering(callback=lambda x: getContextEnvironment(RCN_EVENTS_ENABLE, defaultValue=False, castFunc=bool)
57
+ and getContextEnvironment(RCN_EVENTS_NUM_THREADS, defaultValue=1, castFunc=int) > 0)
58
+ class EventBus:
59
+ def __init__(self):
60
+ self._executor = ThreadPoolExecutor(max_workers=getContextEnvironment(RCN_EVENTS_NUM_THREADS, defaultValue=1, castFunc=int))
61
+
62
+ def publish(self, event_name: str, data=None) -> None:
63
+ if data is None:
64
+ data = {}
65
+ if event_name in REGISTERED_EVENTS:
66
+ for func_name, event in REGISTERED_EVENTS[event_name].items():
67
+ kwargs = {
68
+ **constructKeyWordArguments(event.kwargs, required=False),
69
+ **data
70
+ }
71
+ kwargs = dict(map(lambda x: [x, kwargs[x]], set(event.kwargs.keys()).intersection(kwargs.keys())))
72
+ self._executor.submit(event.eventCallback, **kwargs)
73
+ else:
74
+ logger.warning(f"{event_name} event not found, please check the decorators")
75
+
76
+ def subscribe(self, event_name: str, event: Event) -> None:
77
+ if event_name not in REGISTERED_EVENTS:
78
+ REGISTERED_EVENTS[event_name] = dict()
79
+ REGISTERED_EVENTS[event_name][event.eventType] = event
@@ -0,0 +1,35 @@
1
+ import logging
2
+ from threading import Thread
3
+ from typing import List
4
+
5
+ from cndi.annotations import Component, ConditionalRendering
6
+ from cndi.consts import RCN_ENABLE_CONTEXT_THREADS
7
+ from cndi.env import getContextEnvironment
8
+
9
+ log = logging.getLogger(__name__)
10
+
11
+ def isContextThreadEnable(dependent):
12
+ enabled = getContextEnvironment(RCN_ENABLE_CONTEXT_THREADS, defaultValue=False, castFunc=bool)
13
+ if not enabled:
14
+ log.warning(f"{dependent} Component depends on {__name__}.{ContextThreads.__name__}")
15
+ log.warning(f"SingletonContext Threads is disable in property please enable it by setting {RCN_ENABLE_CONTEXT_THREADS} to true")
16
+
17
+ return enabled
18
+
19
+ @Component
20
+ @ConditionalRendering(callback=lambda x: getContextEnvironment(RCN_ENABLE_CONTEXT_THREADS, defaultValue=False, castFunc=bool))
21
+ class ContextThreads:
22
+ def __init__(self):
23
+ self.threads: List[Thread] = list()
24
+
25
+ def add_thread(self, thread):
26
+ self.threads.append(thread)
27
+
28
+ def clean_up(self):
29
+ exitedThread = []
30
+ for thread in self.threads:
31
+ if not thread.is_alive():
32
+ exitedThread.append(thread)
33
+
34
+ for exitThread in exitedThread:
35
+ self.threads.remove(exitThread)
File without changes
@@ -0,0 +1,11 @@
1
+ from cndi.annotations import Component, ConditionalRendering
2
+ from cndi.consts import RCN_AUTO_CONFIGURE_SECRETS_ENABLE
3
+ from cndi.env import getContextEnvironment
4
+
5
+
6
+ @Component
7
+ @ConditionalRendering(callback=lambda x: getContextEnvironment(RCN_AUTO_CONFIGURE_SECRETS_ENABLE, defaultValue=False, castFunc=bool))
8
+ class AutoConfigurationProviders:
9
+ _PROVIDERS = dict()
10
+ def __init__(self):
11
+ pass
File without changes
cndi/binders/consts.py ADDED
@@ -0,0 +1 @@
1
+ RCN_MESSAGE_BINDERS="rcn.binders.message"