Encryptors 2.48__tar.gz → 2.49__tar.gz

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 (104) hide show
  1. {encryptors-2.48 → encryptors-2.49}/PKG-INFO +5 -4
  2. {encryptors-2.48 → encryptors-2.49}/setup.py +6 -5
  3. {encryptors-2.48 → encryptors-2.49}/src/Encryptors.egg-info/PKG-INFO +5 -4
  4. {encryptors-2.48 → encryptors-2.49}/src/Encryptors.egg-info/SOURCES.txt +8 -2
  5. {encryptors-2.48 → encryptors-2.49}/src/Encryptors.egg-info/requires.txt +4 -3
  6. encryptors-2.49/src/Osdental/Cache/Redis.py +267 -0
  7. encryptors-2.49/src/Osdental/Cache/__init__.py +53 -0
  8. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Cli/__init__.py +1 -1
  9. encryptors-2.49/src/Osdental/Database/BaseRepository.py +195 -0
  10. encryptors-2.49/src/Osdental/Database/Connection.py +188 -0
  11. encryptors-2.49/src/Osdental/Decorators/Grpc.py +32 -0
  12. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Exception/ControlledException.py +6 -1
  13. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Graphql/Extensions/AuditExtension.py +5 -3
  14. encryptors-2.49/src/Osdental/Graphql/_Helpers/_TenantPolicy.py +46 -0
  15. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Graphql/_Helpers/_TokenService.py +3 -3
  16. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Helpers/AuditDispatcher.py +36 -7
  17. encryptors-2.49/src/Osdental/Helpers/AzureClassifier.py +70 -0
  18. encryptors-2.49/src/Osdental/Helpers/GrpcConnection.py +24 -0
  19. encryptors-2.49/src/Osdental/Helpers/Resilience.py +165 -0
  20. encryptors-2.49/src/Osdental/Helpers/WebsocketClient.py +44 -0
  21. encryptors-2.49/src/Osdental/Helpers/_Ports.py +11 -0
  22. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Http/APIClient.py +2 -2
  23. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Http/_Helpers.py +5 -5
  24. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Messaging/AzureServiceBus.py +25 -6
  25. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Models/AuditConfig.py +3 -1
  26. encryptors-2.49/src/Osdental/Models/Notification.py +10 -0
  27. encryptors-2.49/src/Osdental/Shared/Enums/Profile.py +10 -0
  28. encryptors-2.49/src/Osdental/Storage/AzureBlobStorage.py +131 -0
  29. encryptors-2.49/src/Osdental/Storage/__init__.py +48 -0
  30. encryptors-2.48/src/Osdental/Database/BaseRepository.py +0 -112
  31. encryptors-2.48/src/Osdental/Database/Connection.py +0 -66
  32. encryptors-2.48/src/Osdental/Decorators/Grpc.py +0 -55
  33. encryptors-2.48/src/Osdental/Graphql/_Helpers/_TenantPolicy.py +0 -45
  34. encryptors-2.48/src/Osdental/RedisCache/Redis.py +0 -108
  35. encryptors-2.48/src/Osdental/Shared/Enums/Profile.py +0 -10
  36. encryptors-2.48/src/Osdental/Storage/AzureBlobStorage.py +0 -87
  37. encryptors-2.48/src/Osdental/Storage/__init__.py +0 -20
  38. encryptors-2.48/src/Osdental/__init__.py +0 -0
  39. {encryptors-2.48 → encryptors-2.49}/README.md +0 -0
  40. {encryptors-2.48 → encryptors-2.49}/setup.cfg +0 -0
  41. {encryptors-2.48 → encryptors-2.49}/src/Encryptors.egg-info/dependency_links.txt +0 -0
  42. {encryptors-2.48 → encryptors-2.49}/src/Encryptors.egg-info/entry_points.txt +0 -0
  43. {encryptors-2.48 → encryptors-2.49}/src/Encryptors.egg-info/top_level.txt +0 -0
  44. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Database/__init__.py +0 -0
  45. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Decorators/PublicResolver.py +0 -0
  46. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Decorators/Retry.py +0 -0
  47. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Decorators/SecureResolver.py +0 -0
  48. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Decorators/SqlDataNormalizer.py +0 -0
  49. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Decorators/__init__.py +0 -0
  50. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Encryptor/Aes.py +0 -0
  51. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Encryptor/Argon2.py +0 -0
  52. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Encryptor/Bcrypt.py +0 -0
  53. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Encryptor/Jwt.py +0 -0
  54. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Encryptor/Rsa.py +0 -0
  55. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Encryptor/Sha512.py +0 -0
  56. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Encryptor/__init__.py +0 -0
  57. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Exception/__init__.py +0 -0
  58. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Graphql/Extensions/__init__.py +0 -0
  59. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Graphql/Models/__init__.py +0 -0
  60. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Graphql/_Exceptions/__init__.py +0 -0
  61. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Graphql/_Helpers/_AuditHelper.py +0 -0
  62. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Graphql/_Helpers/_ExtractAuthToken.py +0 -0
  63. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Graphql/_Helpers/__init__.py +0 -0
  64. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Graphql/__init__.py +0 -0
  65. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Helpers/KeyVaultService.py +0 -0
  66. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Helpers/ResponseDecryptor.py +0 -0
  67. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Helpers/__init__.py +0 -0
  68. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Http/__init__.py +0 -0
  69. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Messaging/Kafka.py +0 -0
  70. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Messaging/RabbitMQ.py +0 -0
  71. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Messaging/__init__.py +0 -0
  72. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Models/ApiResponse.py +0 -0
  73. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Models/Response.py +0 -0
  74. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Models/Token.py +0 -0
  75. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Models/_Audit.py +0 -0
  76. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Models/__init__.py +0 -0
  77. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Rest/Context/RequestContext.py +0 -0
  78. {encryptors-2.48/src/Osdental/RedisCache → encryptors-2.49/src/Osdental/Rest/Context}/__init__.py +0 -0
  79. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Rest/Middlewares/RequestContextMiddleware.py +0 -0
  80. {encryptors-2.48/src/Osdental/Rest/Context → encryptors-2.49/src/Osdental/Rest/Middlewares}/__init__.py +0 -0
  81. {encryptors-2.48/src/Osdental/Rest/Middlewares → encryptors-2.49/src/Osdental/Rest}/__init__.py +0 -0
  82. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Shared/Enums/Code.py +0 -0
  83. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Shared/Enums/Constant.py +0 -0
  84. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Shared/Enums/FileType.py +0 -0
  85. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Shared/Enums/GrahpqlOperation.py +0 -0
  86. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Shared/Enums/Message.py +0 -0
  87. {encryptors-2.48/src/Osdental/Rest → encryptors-2.49/src/Osdental/Shared/Enums}/__init__.py +0 -0
  88. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Shared/Logger.py +0 -0
  89. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Shared/Utils/CaseConverter.py +0 -0
  90. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Shared/Utils/CodeGenerator.py +0 -0
  91. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Shared/Utils/DataNormalizer.py +0 -0
  92. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Shared/Utils/DataUtils.py +0 -0
  93. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Shared/Utils/DateUtils.py +0 -0
  94. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Shared/Utils/FileMetaData.py +0 -0
  95. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Shared/Utils/HashValidator.py +0 -0
  96. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Shared/Utils/Mapper.py +0 -0
  97. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Shared/Utils/PasswordGenerator.py +0 -0
  98. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Shared/Utils/QueryGenerator.py +0 -0
  99. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Shared/Utils/RsaUtils.py +0 -0
  100. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Shared/Utils/TextProcessor.py +0 -0
  101. {encryptors-2.48/src/Osdental/Shared/Enums → encryptors-2.49/src/Osdental/Shared/Utils}/__init__.py +0 -0
  102. {encryptors-2.48/src/Osdental/Shared/Utils → encryptors-2.49/src/Osdental/Shared}/__init__.py +0 -0
  103. {encryptors-2.48 → encryptors-2.49}/src/Osdental/Storage/S3Storage.py +0 -0
  104. {encryptors-2.48/src/Osdental/Shared → encryptors-2.49/src/Osdental}/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Encryptors
3
- Version: 2.48
3
+ Version: 2.49
4
4
  Summary: End-to-end algorithm library
5
5
  Author: OSDental LLC
6
6
  Author-email: support@osdental.ai
@@ -16,7 +16,7 @@ Requires-Dist: ariadne==0.26.2
16
16
  Requires-Dist: azure-core==1.32.0
17
17
  Requires-Dist: azure-servicebus==7.13.0
18
18
  Requires-Dist: azure-storage-blob==12.24.0
19
- Requires-Dist: azure-identity==1.25.1
19
+ Requires-Dist: azure-identity<2,>=1.24
20
20
  Requires-Dist: azure-keyvault-secrets==4.10.0
21
21
  Requires-Dist: certifi==2024.12.14
22
22
  Requires-Dist: cffi==1.17.1
@@ -32,7 +32,7 @@ Requires-Dist: isodate==0.7.2
32
32
  Requires-Dist: pycparser==2.22
33
33
  Requires-Dist: pydantic==2.12.2
34
34
  Requires-Dist: pydantic_core==2.41.4
35
- Requires-Dist: PyJWT==2.10.1
35
+ Requires-Dist: PyJWT~=2.12.0
36
36
  Requires-Dist: aioodbc==0.5.0
37
37
  Requires-Dist: python-dotenv==1.0.1
38
38
  Requires-Dist: pydantic_settings==2.13.1
@@ -45,7 +45,8 @@ Requires-Dist: tenacity==9.1.2
45
45
  Requires-Dist: typing_extensions==4.15.0
46
46
  Requires-Dist: tzlocal==5.2
47
47
  Requires-Dist: urllib3==2.3.0
48
- Requires-Dist: redis==5.2.1
48
+ Requires-Dist: redis<8,>=5
49
+ Requires-Dist: redis-entraid==1.1.2
49
50
  Requires-Dist: colorlog==6.9.0
50
51
  Requires-Dist: click==8.2.0
51
52
  Requires-Dist: grpcio==1.75.0
@@ -1,8 +1,8 @@
1
1
  from setuptools import setup, find_packages
2
- # ANDERSON ESTO YA SE SUBIO Y ESTA ESTABLE, AUN TE QUEDA PENDIENTE LA AUDITORIA CON RSA Y AES DE ACCESSTOKEN
2
+ # ANDERSON REVISAR EL CACHE LOCAL DEL KEYVAULT PARA VALIDAR SI FUNCIONA
3
3
  setup(
4
4
  name="Encryptors",
5
- version="2.48",
5
+ version="2.49",
6
6
  author="OSDental LLC",
7
7
  author_email="support@osdental.ai",
8
8
  description="End-to-end algorithm library",
@@ -24,7 +24,7 @@ setup(
24
24
  "azure-core==1.32.0",
25
25
  "azure-servicebus==7.13.0",
26
26
  "azure-storage-blob==12.24.0",
27
- "azure-identity==1.25.1",
27
+ "azure-identity>=1.24,<2",
28
28
  "azure-keyvault-secrets==4.10.0",
29
29
  "certifi==2024.12.14",
30
30
  "cffi==1.17.1",
@@ -40,7 +40,7 @@ setup(
40
40
  "pycparser==2.22",
41
41
  "pydantic==2.12.2",
42
42
  "pydantic_core==2.41.4",
43
- "PyJWT==2.10.1",
43
+ "PyJWT~=2.12.0",
44
44
  "aioodbc==0.5.0",
45
45
  "python-dotenv==1.0.1",
46
46
  "pydantic_settings==2.13.1",
@@ -53,7 +53,8 @@ setup(
53
53
  "typing_extensions==4.15.0",
54
54
  "tzlocal==5.2",
55
55
  "urllib3==2.3.0",
56
- "redis==5.2.1",
56
+ "redis>=5,<8",
57
+ "redis-entraid==1.1.2",
57
58
  "colorlog==6.9.0",
58
59
  "click==8.2.0",
59
60
  "grpcio==1.75.0",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Encryptors
3
- Version: 2.48
3
+ Version: 2.49
4
4
  Summary: End-to-end algorithm library
5
5
  Author: OSDental LLC
6
6
  Author-email: support@osdental.ai
@@ -16,7 +16,7 @@ Requires-Dist: ariadne==0.26.2
16
16
  Requires-Dist: azure-core==1.32.0
17
17
  Requires-Dist: azure-servicebus==7.13.0
18
18
  Requires-Dist: azure-storage-blob==12.24.0
19
- Requires-Dist: azure-identity==1.25.1
19
+ Requires-Dist: azure-identity<2,>=1.24
20
20
  Requires-Dist: azure-keyvault-secrets==4.10.0
21
21
  Requires-Dist: certifi==2024.12.14
22
22
  Requires-Dist: cffi==1.17.1
@@ -32,7 +32,7 @@ Requires-Dist: isodate==0.7.2
32
32
  Requires-Dist: pycparser==2.22
33
33
  Requires-Dist: pydantic==2.12.2
34
34
  Requires-Dist: pydantic_core==2.41.4
35
- Requires-Dist: PyJWT==2.10.1
35
+ Requires-Dist: PyJWT~=2.12.0
36
36
  Requires-Dist: aioodbc==0.5.0
37
37
  Requires-Dist: python-dotenv==1.0.1
38
38
  Requires-Dist: pydantic_settings==2.13.1
@@ -45,7 +45,8 @@ Requires-Dist: tenacity==9.1.2
45
45
  Requires-Dist: typing_extensions==4.15.0
46
46
  Requires-Dist: tzlocal==5.2
47
47
  Requires-Dist: urllib3==2.3.0
48
- Requires-Dist: redis==5.2.1
48
+ Requires-Dist: redis<8,>=5
49
+ Requires-Dist: redis-entraid==1.1.2
49
50
  Requires-Dist: colorlog==6.9.0
50
51
  Requires-Dist: click==8.2.0
51
52
  Requires-Dist: grpcio==1.75.0
@@ -7,6 +7,8 @@ src/Encryptors.egg-info/entry_points.txt
7
7
  src/Encryptors.egg-info/requires.txt
8
8
  src/Encryptors.egg-info/top_level.txt
9
9
  src/Osdental/__init__.py
10
+ src/Osdental/Cache/Redis.py
11
+ src/Osdental/Cache/__init__.py
10
12
  src/Osdental/Cli/__init__.py
11
13
  src/Osdental/Database/BaseRepository.py
12
14
  src/Osdental/Database/Connection.py
@@ -37,8 +39,13 @@ src/Osdental/Graphql/_Helpers/_TenantPolicy.py
37
39
  src/Osdental/Graphql/_Helpers/_TokenService.py
38
40
  src/Osdental/Graphql/_Helpers/__init__.py
39
41
  src/Osdental/Helpers/AuditDispatcher.py
42
+ src/Osdental/Helpers/AzureClassifier.py
43
+ src/Osdental/Helpers/GrpcConnection.py
40
44
  src/Osdental/Helpers/KeyVaultService.py
45
+ src/Osdental/Helpers/Resilience.py
41
46
  src/Osdental/Helpers/ResponseDecryptor.py
47
+ src/Osdental/Helpers/WebsocketClient.py
48
+ src/Osdental/Helpers/_Ports.py
42
49
  src/Osdental/Helpers/__init__.py
43
50
  src/Osdental/Http/APIClient.py
44
51
  src/Osdental/Http/_Helpers.py
@@ -49,12 +56,11 @@ src/Osdental/Messaging/RabbitMQ.py
49
56
  src/Osdental/Messaging/__init__.py
50
57
  src/Osdental/Models/ApiResponse.py
51
58
  src/Osdental/Models/AuditConfig.py
59
+ src/Osdental/Models/Notification.py
52
60
  src/Osdental/Models/Response.py
53
61
  src/Osdental/Models/Token.py
54
62
  src/Osdental/Models/_Audit.py
55
63
  src/Osdental/Models/__init__.py
56
- src/Osdental/RedisCache/Redis.py
57
- src/Osdental/RedisCache/__init__.py
58
64
  src/Osdental/Rest/__init__.py
59
65
  src/Osdental/Rest/Context/RequestContext.py
60
66
  src/Osdental/Rest/Context/__init__.py
@@ -5,7 +5,7 @@ ariadne==0.26.2
5
5
  azure-core==1.32.0
6
6
  azure-servicebus==7.13.0
7
7
  azure-storage-blob==12.24.0
8
- azure-identity==1.25.1
8
+ azure-identity<2,>=1.24
9
9
  azure-keyvault-secrets==4.10.0
10
10
  certifi==2024.12.14
11
11
  cffi==1.17.1
@@ -21,7 +21,7 @@ isodate==0.7.2
21
21
  pycparser==2.22
22
22
  pydantic==2.12.2
23
23
  pydantic_core==2.41.4
24
- PyJWT==2.10.1
24
+ PyJWT~=2.12.0
25
25
  aioodbc==0.5.0
26
26
  python-dotenv==1.0.1
27
27
  pydantic_settings==2.13.1
@@ -34,7 +34,8 @@ tenacity==9.1.2
34
34
  typing_extensions==4.15.0
35
35
  tzlocal==5.2
36
36
  urllib3==2.3.0
37
- redis==5.2.1
37
+ redis<8,>=5
38
+ redis-entraid==1.1.2
38
39
  colorlog==6.9.0
39
40
  click==8.2.0
40
41
  grpcio==1.75.0
@@ -0,0 +1,267 @@
1
+ import asyncio
2
+ import json
3
+ from typing import Dict, List, Optional, Any
4
+ import redis.asyncio as redis
5
+ from redis_entraid.cred_provider import (
6
+ create_from_default_azure_credential
7
+ )
8
+ from Osdental.Cache import ICacheService
9
+ from Osdental.Exception.ControlledException import RedisException
10
+ from Osdental.Helpers.Resilience import AzureResiliencePolicy, AzureTransientError
11
+ from Osdental.Helpers.AzureClassifier import classify_redis
12
+ from Osdental.Shared.Logger import logger
13
+ from Osdental.Shared.Enums.Message import Message
14
+
15
+
16
+ class RedisCacheAsync(ICacheService):
17
+
18
+ _instances: Dict[str, "RedisCacheAsync"] = {}
19
+ _lock = asyncio.Lock()
20
+
21
+ _policy = AzureResiliencePolicy(
22
+ "redis-cache",
23
+ max_attempts=3,
24
+ base_delay=0.5,
25
+ failure_threshold=5,
26
+ )
27
+
28
+ def __new__(cls, redis_host: str, redis_port: int = 6380):
29
+
30
+ key = f"{redis_host}:{redis_port}"
31
+
32
+ if key not in cls._instances:
33
+ cls._instances[key] = super().__new__(cls)
34
+
35
+ return cls._instances[key]
36
+
37
+ def __init__(
38
+ self,
39
+ redis_host: str,
40
+ redis_port: int = 6380
41
+ ):
42
+
43
+ if hasattr(self, "_initialized"):
44
+ return
45
+
46
+ self.redis_host = redis_host
47
+ self.redis_port = redis_port
48
+
49
+ self.client = None
50
+
51
+ self._initialized = True
52
+
53
+
54
+ async def _call(self, func, *args, **kwargs):
55
+ """
56
+ Ejecuta una operación de Redis a través de la policy.
57
+ Clasifica el error y deja que la policy decida si reintenta.
58
+ Si se agota o el circuito está abierto, convierte a RedisException.
59
+ """
60
+ await self._ensure_connection()
61
+
62
+ async def _inner():
63
+ try:
64
+ return await func(*args, **kwargs)
65
+ except Exception as exc:
66
+ raise classify_redis(exc)
67
+
68
+ try:
69
+ return await self._policy.execute(_inner)
70
+ except AzureTransientError as exc:
71
+ logger.error("redis.exhausted error=%s", exc)
72
+ raise RedisException(message=Message.UNEXPECTED_ERROR_MSG, error=str(exc))
73
+ except Exception as exc:
74
+ raise RedisException(message=Message.UNEXPECTED_ERROR_MSG, error=str(exc))
75
+
76
+
77
+ async def connect(self):
78
+ if self.client:
79
+ return
80
+
81
+ async with self._lock:
82
+
83
+ if self.client:
84
+ return
85
+
86
+ try:
87
+ credential_provider = (
88
+ create_from_default_azure_credential(
89
+ ("https://redis.azure.com/.default",)
90
+ )
91
+ )
92
+
93
+ self.client = redis.Redis(
94
+ host=self.redis_host,
95
+ port=self.redis_port,
96
+ ssl=True,
97
+ credential_provider=credential_provider,
98
+ decode_responses=True,
99
+ health_check_interval=30,
100
+ retry_on_timeout=True,
101
+ socket_connect_timeout=5,
102
+ socket_timeout=5,
103
+ max_connections=50,
104
+ socket_keepalive=True
105
+ )
106
+
107
+ await self.client.ping()
108
+
109
+ logger.info(
110
+ "Redis connected with Managed Identity"
111
+ )
112
+
113
+ except Exception as e:
114
+ self.client = None
115
+
116
+ logger.error(
117
+ f"Redis connection error: {str(e)}"
118
+ )
119
+
120
+ raise RedisException(
121
+ message=Message.UNEXPECTED_ERROR_MSG,
122
+ error=str(e)
123
+ )
124
+
125
+ async def _ensure_connection(self):
126
+
127
+ if not self.client:
128
+ await self.connect()
129
+
130
+ async def set_dict(
131
+ self,
132
+ key: str,
133
+ value: Dict[str, Any],
134
+ ttl: Optional[int] = None
135
+ ):
136
+
137
+ await self._call(
138
+ self.client.set(
139
+ key,
140
+ json.dumps(value),
141
+ ex=ttl
142
+ )
143
+ )
144
+
145
+
146
+ async def set_str(
147
+ self,
148
+ key: str,
149
+ value: str,
150
+ ttl: Optional[int] = None
151
+ ):
152
+
153
+ await self._call(
154
+ self.client.set(
155
+ key,
156
+ value,
157
+ ex=ttl
158
+ )
159
+ )
160
+
161
+ async def get_dict(
162
+ self,
163
+ key: str
164
+ ) -> Optional[Dict[str, Any]]:
165
+
166
+ value = await self._call(
167
+ self.client.get(key)
168
+ )
169
+
170
+ return json.loads(value) if value else None
171
+
172
+
173
+ async def get_str(
174
+ self,
175
+ key: str
176
+ ) -> Optional[str]:
177
+
178
+ return await self._call(
179
+ self.client.get(key)
180
+ )
181
+
182
+
183
+ async def delete(self, key: str) -> bool:
184
+
185
+ result = await self._call(
186
+ self.client.delete(key)
187
+ )
188
+
189
+ return result > 0
190
+
191
+
192
+ async def exists(self, key: str) -> bool:
193
+
194
+ result = await self._call(
195
+ self.client.exists(key)
196
+ )
197
+
198
+ return result > 0
199
+
200
+
201
+ async def flush(self):
202
+
203
+ await self._call(
204
+ self.client.flushdb()
205
+ )
206
+
207
+
208
+ async def flush_all(self):
209
+
210
+ await self._call(
211
+ self.client.flushall()
212
+ )
213
+
214
+
215
+ async def mget(
216
+ self,
217
+ keys: List[str]
218
+ ) -> List[Optional[Any]]:
219
+
220
+ values = await self._call(
221
+ self.client.mget(keys)
222
+ )
223
+
224
+ return [
225
+ json.loads(value)
226
+ if value else None
227
+ for value in values
228
+ ]
229
+
230
+
231
+ async def clear_cache(self, prefix: str):
232
+
233
+ await self._ensure_connection()
234
+
235
+ async for key in self._scan_keys(
236
+ match=f"{prefix}*"
237
+ ):
238
+
239
+ await self._call(self.client.delete(key))
240
+
241
+
242
+ async def _scan_keys(self, match: str = "*"):
243
+
244
+ async for key in self.client.scan_iter(
245
+ match=match
246
+ ):
247
+ yield key
248
+
249
+ async def close(self):
250
+
251
+ try:
252
+
253
+ if self.client:
254
+
255
+ await self.client.aclose()
256
+
257
+ self.client = None
258
+
259
+ logger.info(
260
+ "Redis connection closed"
261
+ )
262
+
263
+ except Exception as e:
264
+
265
+ logger.error(
266
+ f"Redis close error: {str(e)}"
267
+ )
@@ -0,0 +1,53 @@
1
+ from typing import Protocol, Optional, Dict, List, Any
2
+
3
+
4
+ class ICacheService(Protocol):
5
+
6
+ async def set_dict(self, key: str, value: Dict[str, Any], ttl: Optional[int] = None):
7
+ """Set a JSON value in the cache."""
8
+ pass
9
+
10
+ async def set_str(self, key: str, value: str, ttl: Optional[int] = None):
11
+ """Set a string value in the cache."""
12
+ pass
13
+
14
+ async def get_dict(self, key: str) -> Optional[Dict[str, Any]]:
15
+ """Get a JSON value from the cache and convert it back to a Python object."""
16
+ pass
17
+
18
+
19
+ async def get_str(self, key: str) -> Optional[str]:
20
+ """Get a string value from the cache."""
21
+ pass
22
+
23
+ async def delete(self, key: str) -> bool:
24
+ """Delete a value from the cache."""
25
+ pass
26
+
27
+ async def exists(self, key: str) -> bool:
28
+ """Check if a key exists in the cache."""
29
+ pass
30
+
31
+ async def flush(self):
32
+ """Flush all keys in the cache."""
33
+ pass
34
+
35
+ async def flush_all(self):
36
+ """Flush all keys in all Redis databases."""
37
+ pass
38
+
39
+ async def mget(self, keys:List[str]) -> List[Optional[str]]:
40
+ """Get multiple values from the cache."""
41
+ pass
42
+
43
+ async def clear_cache(self, prefix: str):
44
+ """Delete all keys matching the given prefix."""
45
+ pass
46
+
47
+ async def _scan_keys(self, match: str = "*"):
48
+ """Asynchronous generator to scan keys matching a pattern."""
49
+ pass
50
+
51
+ async def close(self):
52
+ """Close the connection pool and Redis client."""
53
+ pass
@@ -44,7 +44,7 @@ def serve(port: int):
44
44
  @click.argument('redis_env')
45
45
  async def clean_redis(redis_env: str):
46
46
  try:
47
- from Osdental.RedisCache.Redis import RedisCacheAsync
47
+ from Osdental.Cache.Redis import RedisCacheAsync
48
48
  redis_url = os.getenv(redis_env)
49
49
  if not redis_url:
50
50
  logger.warning(f'Environment variable not found: {redis_env}')
@@ -0,0 +1,195 @@
1
+ from collections.abc import Iterable
2
+ from typing import Any
3
+ from sqlalchemy import text
4
+ from sqlalchemy.exc import ResourceClosedError, DBAPIError
5
+ from sqlalchemy.ext.asyncio import AsyncSession
6
+ from Osdental.Exception.ControlledException import DatabaseException
7
+ from Osdental.Helpers.Resilience import AzureResiliencePolicy, AzureTransientError
8
+ from Osdental.Shared.Utils.DataNormalizer import normalize
9
+ from Osdental.Shared.Enums.Message import Message
10
+
11
+ _TRANSIENT_SQL_ERRORS = {
12
+ "08S01", "40197", "40501", "40613", "49918",
13
+ "4060", "10928", "10929", "10053", "10054", "10060"
14
+ }
15
+
16
+ def _is_transient(exc: Exception) -> bool:
17
+ msg = str(exc).upper()
18
+ return any(code in msg for code in _TRANSIENT_SQL_ERRORS)
19
+
20
+ class BaseRepository:
21
+
22
+ _policy = AzureResiliencePolicy(
23
+ "sql-database",
24
+ max_attempts=3,
25
+ base_delay=1.0,
26
+ failure_threshold=5,
27
+ )
28
+
29
+ def __init__(self, async_session: AsyncSession):
30
+ self.async_session = async_session
31
+
32
+
33
+ def _get_status_value(self, rows: dict, *possible_keys: str):
34
+ for key in possible_keys:
35
+ if key in rows:
36
+ return rows[key]
37
+ return None
38
+
39
+
40
+ async def _safe_execute(self, query: str, params: dict | None = None):
41
+ async def _inner():
42
+ try:
43
+ return await self.async_session.execute(text(query), params or {})
44
+ except DBAPIError as exc:
45
+ if _is_transient(exc):
46
+ raise AzureTransientError(str(exc)) from exc
47
+ raise
48
+ return await self._policy.execute(_inner)
49
+
50
+
51
+ async def _execute_query(
52
+ self,
53
+ query: str,
54
+ params: dict | None = None,
55
+ *,
56
+ validate: bool = False,
57
+ success_codes: str | int | Iterable[str | int] | None = None,
58
+ many: bool = False,
59
+ as_dict: bool = False
60
+ ) -> Any:
61
+
62
+ result = await self._safe_execute(query, params)
63
+
64
+ row = (
65
+ result.mappings().all()
66
+ if many
67
+ else result.mappings().first()
68
+ )
69
+
70
+ if validate and row is not None:
71
+ validation_row = None
72
+
73
+ if many:
74
+
75
+ if (
76
+ isinstance(row, list)
77
+ and len(row) == 1
78
+ ):
79
+ validation_row = row[0]
80
+
81
+ else:
82
+
83
+ validation_row = row
84
+
85
+ if validation_row is not None:
86
+
87
+ status_code = self._get_status_value(
88
+ validation_row,
89
+ "statusCode",
90
+ "STATUS_CODE",
91
+ "status_code"
92
+ )
93
+
94
+ status_message = self._get_status_value(
95
+ validation_row,
96
+ "statusMessage",
97
+ "STATUS_MESSAGE",
98
+ "status_message"
99
+ )
100
+
101
+ if status_code is not None and success_codes is not None:
102
+
103
+ if not isinstance(success_codes, tuple):
104
+
105
+ if isinstance(success_codes, Iterable) and not isinstance(success_codes, (str, bytes)):
106
+ success_codes = tuple(success_codes)
107
+ else:
108
+ success_codes = (success_codes,)
109
+
110
+ if status_code not in success_codes:
111
+ raise DatabaseException(
112
+ message=Message.UNEXPECTED_ERROR_MSG,
113
+ error=status_message,
114
+ status_code=status_code
115
+ )
116
+
117
+ if as_dict and row is not None:
118
+ return normalize(row)
119
+
120
+ return row
121
+
122
+
123
+ async def _execute_command(
124
+ self,
125
+ query: str,
126
+ params: dict | None = None,
127
+ *,
128
+ validate: bool = True,
129
+ success_codes: str | int | Iterable[str | int] | None = None,
130
+ as_dict: bool = False,
131
+ commit_on_failure: bool = False
132
+ ) -> Any:
133
+
134
+ result = await self._safe_execute(query, params)
135
+
136
+ try:
137
+ row = result.mappings().first()
138
+ except ResourceClosedError:
139
+ return None
140
+
141
+ if not validate or not row:
142
+ return normalize(row) if as_dict else row
143
+
144
+
145
+ status_code = self._get_status_value(
146
+ row,
147
+ "statusCode",
148
+ "STATUS_CODE",
149
+ "status_code"
150
+ )
151
+
152
+ status_message = self._get_status_value(
153
+ row,
154
+ "statusMessage",
155
+ "STATUS_MESSAGE",
156
+ "status_message"
157
+ )
158
+
159
+ # Si no se define success_codes, solo valida que no sea None
160
+ if success_codes is None:
161
+ if status_code is None:
162
+ raise DatabaseException(
163
+ message="Status code missing",
164
+ error="Status code missing",
165
+ status_code="DB_ERROR_MISSING"
166
+ )
167
+
168
+ else:
169
+
170
+ if not isinstance(success_codes, tuple):
171
+ if isinstance(success_codes, Iterable) and not isinstance(success_codes, (str, bytes)):
172
+ success_codes = tuple(success_codes)
173
+ else:
174
+ success_codes = (success_codes,)
175
+
176
+ if status_code not in success_codes:
177
+ if commit_on_failure:
178
+ await self.async_session.commit()
179
+
180
+ raise DatabaseException(
181
+ message=status_message,
182
+ error=status_message,
183
+ status_code=status_code
184
+ )
185
+
186
+ return normalize(row) if as_dict else row
187
+
188
+
189
+ async def _execute_scalar(
190
+ self,
191
+ query: str,
192
+ params: dict | None = None
193
+ ) -> Any | None:
194
+ result = await self._safe_execute(query, params)
195
+ return result.scalar_one_or_none()