qena-shared-lib 0.1.17__py3-none-any.whl → 0.1.19__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. qena_shared_lib/__init__.py +20 -2
  2. qena_shared_lib/alias.py +27 -0
  3. qena_shared_lib/application.py +4 -4
  4. qena_shared_lib/background.py +9 -7
  5. qena_shared_lib/cache.py +61 -0
  6. qena_shared_lib/enums.py +8 -0
  7. qena_shared_lib/eventbus.py +373 -0
  8. qena_shared_lib/exception_handling.py +409 -0
  9. qena_shared_lib/exceptions.py +167 -57
  10. qena_shared_lib/http/__init__.py +110 -0
  11. qena_shared_lib/{http.py → http/_base.py} +36 -36
  12. qena_shared_lib/http/_exception_handlers.py +202 -0
  13. qena_shared_lib/http/_request.py +24 -0
  14. qena_shared_lib/http/_response.py +24 -0
  15. qena_shared_lib/kafka/__init__.py +21 -0
  16. qena_shared_lib/kafka/_base.py +233 -0
  17. qena_shared_lib/kafka/_consumer.py +597 -0
  18. qena_shared_lib/kafka/_exception_handlers.py +124 -0
  19. qena_shared_lib/kafka/_producer.py +133 -0
  20. qena_shared_lib/logging.py +17 -13
  21. qena_shared_lib/mongodb.py +575 -0
  22. qena_shared_lib/rabbitmq/__init__.py +6 -6
  23. qena_shared_lib/rabbitmq/_base.py +68 -132
  24. qena_shared_lib/rabbitmq/_channel.py +2 -4
  25. qena_shared_lib/rabbitmq/_exception_handlers.py +69 -142
  26. qena_shared_lib/rabbitmq/_listener.py +245 -180
  27. qena_shared_lib/rabbitmq/_publisher.py +5 -5
  28. qena_shared_lib/rabbitmq/_rpc_client.py +21 -22
  29. qena_shared_lib/rabbitmq/message/__init__.py +19 -0
  30. qena_shared_lib/rabbitmq/message/_inbound.py +13 -0
  31. qena_shared_lib/rabbitmq/message/_outbound.py +13 -0
  32. qena_shared_lib/redis.py +47 -0
  33. qena_shared_lib/remotelogging/_base.py +34 -28
  34. qena_shared_lib/remotelogging/logstash/_base.py +3 -2
  35. qena_shared_lib/remotelogging/logstash/_http_sender.py +2 -4
  36. qena_shared_lib/remotelogging/logstash/_tcp_sender.py +2 -2
  37. qena_shared_lib/scheduler.py +24 -15
  38. qena_shared_lib/security.py +39 -32
  39. qena_shared_lib/sync.py +91 -0
  40. qena_shared_lib/utils.py +13 -11
  41. {qena_shared_lib-0.1.17.dist-info → qena_shared_lib-0.1.19.dist-info}/METADATA +395 -32
  42. qena_shared_lib-0.1.19.dist-info/RECORD +50 -0
  43. qena_shared_lib-0.1.19.dist-info/WHEEL +4 -0
  44. qena_shared_lib/exception_handlers.py +0 -235
  45. qena_shared_lib-0.1.17.dist-info/RECORD +0 -31
  46. qena_shared_lib-0.1.17.dist-info/WHEEL +0 -4
@@ -173,44 +173,51 @@ class EndpointAclValidator:
173
173
  permission_match_strategy or PermissionMatch.SOME
174
174
  )
175
175
 
176
+ if self._permissions is not None:
177
+ self._permissions = sorted(self._permissions)
178
+
176
179
  def __call__(
177
180
  self, user_info: Annotated[UserInfo, Depends(extract_user_info)]
178
181
  ) -> UserInfo:
179
- if (
180
- self._user_type is not None
181
- and (
182
- user_info.user_type is None
183
- or user_info.user_type != self._user_type
184
- )
185
- ) or (
186
- self._permissions is not None
187
- and (
188
- user_info.user_permissions is None
189
- or not self._permissions_match(user_info.user_permissions)
190
- )
182
+ if self._user_type_match(user_info) and self._permissions_match(
183
+ user_info
191
184
  ):
192
- raise Unauthorized(
193
- message=MESSAGE,
194
- response_code=RESPONSE_CODE,
195
- tags=[user_info.user_id],
196
- extra={
197
- "userId": user_info.user_id,
198
- "userType": user_info.user_type,
199
- "userPermissions": str(user_info.user_permissions or []),
200
- "requiredUserType": self._user_type or "None",
201
- "requiredPermissions": str(self._permissions or []),
202
- "permissionMatchStrategy": self._permission_match_strategy.name,
203
- },
204
- )
205
-
206
- return user_info
207
-
208
- def _permissions_match(self, user_permissions: list[str]) -> bool:
209
- assert self._permissions is not None
185
+ return user_info
186
+
187
+ raise Unauthorized(
188
+ message=MESSAGE,
189
+ response_code=RESPONSE_CODE,
190
+ tags=[user_info.user_id],
191
+ extra={
192
+ "userId": user_info.user_id,
193
+ "userType": user_info.user_type,
194
+ "userPermissions": str(user_info.user_permissions or []),
195
+ "requiredUserType": self._user_type or "None",
196
+ "requiredPermissions": str(self._permissions or []),
197
+ "permissionMatchStrategy": self._permission_match_strategy.name,
198
+ },
199
+ )
200
+
201
+ def _user_type_match(self, user_info: UserInfo) -> bool:
202
+ if self._user_type is None:
203
+ return True
204
+
205
+ if user_info.user_type is None:
206
+ return False
207
+
208
+ return user_info.user_type == self._user_type
209
+
210
+ def _permissions_match(self, user_info: UserInfo) -> bool:
211
+ if self._permissions is None:
212
+ return True
213
+
214
+ if user_info.user_permissions is None:
215
+ return False
210
216
 
211
217
  if self._permission_match_strategy == PermissionMatch.ALL:
212
- return sorted(self._permissions) == sorted(user_permissions)
218
+ return self._permissions == sorted(user_info.user_permissions)
213
219
 
214
220
  return any(
215
- permission in self._permissions for permission in user_permissions
221
+ permission in self._permissions
222
+ for permission in user_info.user_permissions
216
223
  )
@@ -0,0 +1,91 @@
1
+ from inspect import Traceback
2
+ from typing import cast
3
+
4
+ from redis.asyncio import Redis
5
+ from redis.asyncio.lock import Lock
6
+ from typing_extensions import Self
7
+
8
+ from .redis import RedisDependent
9
+
10
+ __all__ = [
11
+ "DistributedLockGuard",
12
+ "DistributedLockManager",
13
+ ]
14
+
15
+
16
+ class DistributedLockGuard:
17
+ def __init__(
18
+ self,
19
+ distributed_lock_manager: "DistributedLockManager",
20
+ key: str,
21
+ blocking: bool = True,
22
+ ):
23
+ self._distributed_lock_manager = distributed_lock_manager
24
+ self._key = key
25
+ self._blocking = blocking
26
+ self._is_acquired = False
27
+
28
+ @property
29
+ def is_aquired(self) -> bool:
30
+ return self._is_acquired
31
+
32
+ async def __aenter__(self) -> Self:
33
+ if self._blocking:
34
+ await self._distributed_lock_manager.acquire(self._key)
35
+ else:
36
+ self._is_acquired = (
37
+ await self._distributed_lock_manager.try_acquire(self._key)
38
+ )
39
+
40
+ return self
41
+
42
+ async def __aexit__(
43
+ self,
44
+ exception_type: type[Exception],
45
+ exception: Exception,
46
+ traceback: Traceback,
47
+ ) -> None:
48
+ del exception_type, exception, traceback
49
+
50
+ await self._distributed_lock_manager.release(self._key)
51
+
52
+ self._is_acquired = False
53
+
54
+
55
+ class DistributedLockManager(RedisDependent):
56
+ def __init__(self, lock_timeout: int = 30):
57
+ self._lock_timeout = lock_timeout
58
+ self._lockes: dict[str, Lock] = {}
59
+
60
+ def attach(self, redis_client: Redis) -> None:
61
+ self._redis_client = redis_client
62
+
63
+ async def acquire(self, key: str) -> None:
64
+ lock = self._register_lock(key)
65
+
66
+ await lock.acquire()
67
+
68
+ async def try_acquire(self, key: str) -> bool:
69
+ lock = self._register_lock(key=key, blocking=False)
70
+ is_acquired = await lock.acquire()
71
+
72
+ return cast(bool, is_acquired)
73
+
74
+ def _register_lock(self, key: str, blocking: bool = True) -> Lock:
75
+ if key not in self._lockes:
76
+ self._lockes[key] = self._redis_client.lock(
77
+ name=key, blocking=blocking, timeout=self._lock_timeout
78
+ )
79
+
80
+ return self._lockes[key]
81
+
82
+ async def release(self, key: str) -> None:
83
+ if key not in self._lockes or not await self._lockes[key].owned():
84
+ return
85
+
86
+ await self._lockes[key].release()
87
+
88
+ def __call__(self, key: str, blocking: bool = True) -> DistributedLockGuard:
89
+ return DistributedLockGuard(
90
+ distributed_lock_manager=self, key=key, blocking=blocking
91
+ )
qena_shared_lib/utils.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from asyncio import AbstractEventLoop, get_running_loop
2
+ from types import UnionType
2
3
  from typing import Generator
3
4
 
4
5
  from pydantic import TypeAdapter
@@ -11,28 +12,29 @@ class AsyncEventLoopMixin:
11
12
 
12
13
  @property
13
14
  def loop(self) -> AbstractEventLoop:
14
- if self._LOOP is None:
15
- self._LOOP = get_running_loop()
15
+ if AsyncEventLoopMixin._LOOP is None:
16
+ AsyncEventLoopMixin._LOOP = get_running_loop()
16
17
 
17
- return self._LOOP
18
+ return AsyncEventLoopMixin._LOOP
18
19
 
19
- def init(self) -> None:
20
- self._LOOP = get_running_loop()
20
+ @staticmethod
21
+ def reset_running_loop() -> None:
22
+ AsyncEventLoopMixin._LOOP = get_running_loop()
21
23
 
22
24
 
23
25
  class TypeAdapterCache:
24
- _cache: dict[type, TypeAdapter] = {}
26
+ _CACHE: dict[type | UnionType, TypeAdapter] = {}
25
27
 
26
28
  @classmethod
27
- def cache_annotation(cls, annotation: type) -> None:
28
- if annotation not in cls._cache:
29
- cls._cache[annotation] = TypeAdapter(annotation)
29
+ def cache_annotation(cls, annotation: type | UnionType) -> None:
30
+ if annotation not in cls._CACHE:
31
+ cls._CACHE[annotation] = TypeAdapter(annotation)
30
32
 
31
33
  @classmethod
32
- def get_type_adapter(cls, annotation: type) -> TypeAdapter:
34
+ def get_type_adapter(cls, annotation: type | UnionType) -> TypeAdapter:
33
35
  cls.cache_annotation(annotation)
34
36
 
35
- return cls._cache[annotation]
37
+ return cls._CACHE[annotation]
36
38
 
37
39
 
38
40
  class YieldOnce: