Encryptors 2.47__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.
- {encryptors-2.47 → encryptors-2.49}/PKG-INFO +5 -4
- {encryptors-2.47 → encryptors-2.49}/setup.py +6 -5
- {encryptors-2.47 → encryptors-2.49}/src/Encryptors.egg-info/PKG-INFO +5 -4
- {encryptors-2.47 → encryptors-2.49}/src/Encryptors.egg-info/SOURCES.txt +10 -3
- {encryptors-2.47 → encryptors-2.49}/src/Encryptors.egg-info/requires.txt +4 -3
- encryptors-2.49/src/Osdental/Cache/Redis.py +267 -0
- encryptors-2.49/src/Osdental/Cache/__init__.py +53 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Cli/__init__.py +1 -1
- encryptors-2.49/src/Osdental/Database/BaseRepository.py +195 -0
- encryptors-2.49/src/Osdental/Database/Connection.py +188 -0
- encryptors-2.49/src/Osdental/Decorators/Grpc.py +32 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Exception/ControlledException.py +14 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Graphql/Extensions/AuditExtension.py +5 -3
- encryptors-2.49/src/Osdental/Graphql/_Helpers/_TenantPolicy.py +46 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Graphql/_Helpers/_TokenService.py +3 -3
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Helpers/AuditDispatcher.py +50 -20
- encryptors-2.49/src/Osdental/Helpers/AzureClassifier.py +70 -0
- encryptors-2.49/src/Osdental/Helpers/GrpcConnection.py +24 -0
- encryptors-2.49/src/Osdental/Helpers/Resilience.py +165 -0
- encryptors-2.49/src/Osdental/Helpers/WebsocketClient.py +44 -0
- encryptors-2.49/src/Osdental/Helpers/_Ports.py +11 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Http/APIClient.py +18 -28
- encryptors-2.49/src/Osdental/Http/_Helpers.py +122 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Messaging/AzureServiceBus.py +25 -6
- encryptors-2.49/src/Osdental/Models/ApiResponse.py +17 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Models/AuditConfig.py +3 -1
- encryptors-2.49/src/Osdental/Models/Notification.py +10 -0
- encryptors-2.49/src/Osdental/Shared/Enums/Message.py +40 -0
- encryptors-2.49/src/Osdental/Shared/Enums/Profile.py +10 -0
- encryptors-2.49/src/Osdental/Storage/AzureBlobStorage.py +131 -0
- encryptors-2.49/src/Osdental/Storage/__init__.py +48 -0
- encryptors-2.47/src/Osdental/Database/BaseRepository.py +0 -112
- encryptors-2.47/src/Osdental/Database/Connection.py +0 -66
- encryptors-2.47/src/Osdental/Decorators/Grpc.py +0 -55
- encryptors-2.47/src/Osdental/Graphql/_Helpers/_TenantPolicy.py +0 -45
- encryptors-2.47/src/Osdental/Http/_Exceptions.py +0 -12
- encryptors-2.47/src/Osdental/RedisCache/Redis.py +0 -108
- encryptors-2.47/src/Osdental/Shared/Enums/Message.py +0 -39
- encryptors-2.47/src/Osdental/Shared/Enums/Profile.py +0 -10
- encryptors-2.47/src/Osdental/Storage/AzureBlobStorage.py +0 -87
- encryptors-2.47/src/Osdental/Storage/__init__.py +0 -20
- encryptors-2.47/src/Osdental/__init__.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/README.md +0 -0
- {encryptors-2.47 → encryptors-2.49}/setup.cfg +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Encryptors.egg-info/dependency_links.txt +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Encryptors.egg-info/entry_points.txt +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Encryptors.egg-info/top_level.txt +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Database/__init__.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Decorators/PublicResolver.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Decorators/Retry.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Decorators/SecureResolver.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Decorators/SqlDataNormalizer.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Decorators/__init__.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Encryptor/Aes.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Encryptor/Argon2.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Encryptor/Bcrypt.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Encryptor/Jwt.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Encryptor/Rsa.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Encryptor/Sha512.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Encryptor/__init__.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Exception/__init__.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Graphql/Extensions/__init__.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Graphql/Models/__init__.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Graphql/_Exceptions/__init__.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Graphql/_Helpers/_AuditHelper.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Graphql/_Helpers/_ExtractAuthToken.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Graphql/_Helpers/__init__.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Graphql/__init__.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Helpers/KeyVaultService.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Helpers/ResponseDecryptor.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Helpers/__init__.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Http/__init__.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Messaging/Kafka.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Messaging/RabbitMQ.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Messaging/__init__.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Models/Response.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Models/Token.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Models/_Audit.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Models/__init__.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Rest/Context/RequestContext.py +0 -0
- {encryptors-2.47/src/Osdental/RedisCache → encryptors-2.49/src/Osdental/Rest/Context}/__init__.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Rest/Middlewares/RequestContextMiddleware.py +0 -0
- {encryptors-2.47/src/Osdental/Rest/Context → encryptors-2.49/src/Osdental/Rest/Middlewares}/__init__.py +0 -0
- {encryptors-2.47/src/Osdental/Rest/Middlewares → encryptors-2.49/src/Osdental/Rest}/__init__.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Shared/Enums/Code.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Shared/Enums/Constant.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Shared/Enums/FileType.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Shared/Enums/GrahpqlOperation.py +0 -0
- {encryptors-2.47/src/Osdental/Rest → encryptors-2.49/src/Osdental/Shared/Enums}/__init__.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Shared/Logger.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Shared/Utils/CaseConverter.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Shared/Utils/CodeGenerator.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Shared/Utils/DataNormalizer.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Shared/Utils/DataUtils.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Shared/Utils/DateUtils.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Shared/Utils/FileMetaData.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Shared/Utils/HashValidator.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Shared/Utils/Mapper.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Shared/Utils/PasswordGenerator.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Shared/Utils/QueryGenerator.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Shared/Utils/RsaUtils.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Shared/Utils/TextProcessor.py +0 -0
- {encryptors-2.47/src/Osdental/Shared/Enums → encryptors-2.49/src/Osdental/Shared/Utils}/__init__.py +0 -0
- {encryptors-2.47/src/Osdental/Shared/Utils → encryptors-2.49/src/Osdental/Shared}/__init__.py +0 -0
- {encryptors-2.47 → encryptors-2.49}/src/Osdental/Storage/S3Storage.py +0 -0
- {encryptors-2.47/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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2
|
+
# ANDERSON REVISAR EL CACHE LOCAL DEL KEYVAULT PARA VALIDAR SI FUNCIONA
|
|
3
3
|
setup(
|
|
4
4
|
name="Encryptors",
|
|
5
|
-
version="2.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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,23 +39,28 @@ 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
|
-
src/Osdental/Http/
|
|
51
|
+
src/Osdental/Http/_Helpers.py
|
|
45
52
|
src/Osdental/Http/__init__.py
|
|
46
53
|
src/Osdental/Messaging/AzureServiceBus.py
|
|
47
54
|
src/Osdental/Messaging/Kafka.py
|
|
48
55
|
src/Osdental/Messaging/RabbitMQ.py
|
|
49
56
|
src/Osdental/Messaging/__init__.py
|
|
57
|
+
src/Osdental/Models/ApiResponse.py
|
|
50
58
|
src/Osdental/Models/AuditConfig.py
|
|
59
|
+
src/Osdental/Models/Notification.py
|
|
51
60
|
src/Osdental/Models/Response.py
|
|
52
61
|
src/Osdental/Models/Token.py
|
|
53
62
|
src/Osdental/Models/_Audit.py
|
|
54
63
|
src/Osdental/Models/__init__.py
|
|
55
|
-
src/Osdental/RedisCache/Redis.py
|
|
56
|
-
src/Osdental/RedisCache/__init__.py
|
|
57
64
|
src/Osdental/Rest/__init__.py
|
|
58
65
|
src/Osdental/Rest/Context/RequestContext.py
|
|
59
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
|
|
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
|
|
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
|
|
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.
|
|
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()
|