Encryptors 2.58__tar.gz → 2.60__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 (103) hide show
  1. {encryptors-2.58 → encryptors-2.60}/PKG-INFO +1 -1
  2. {encryptors-2.58 → encryptors-2.60}/setup.py +1 -1
  3. {encryptors-2.58 → encryptors-2.60}/src/Encryptors.egg-info/PKG-INFO +1 -1
  4. {encryptors-2.58 → encryptors-2.60}/src/Encryptors.egg-info/SOURCES.txt +4 -17
  5. encryptors-2.60/src/Osdental/Context/__init__.py +5 -0
  6. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Decorators/SecureResolver.py +4 -3
  7. encryptors-2.60/src/Osdental/Enums/ResultType.py +5 -0
  8. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Helpers/AuditHelper.py +58 -10
  9. encryptors-2.60/src/Osdental/Helpers/AuditQueue.py +45 -0
  10. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Http/APIClient.py +37 -12
  11. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Services/ServiceBusAuditEmitter.py +6 -2
  12. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Services/__init__.py +1 -1
  13. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Storage/__init__.py +2 -2
  14. encryptors-2.58/src/Osdental/Graphql/Extensions/AuditExtension.py +0 -196
  15. encryptors-2.58/src/Osdental/Graphql/_Exceptions/__init__.py +0 -8
  16. encryptors-2.58/src/Osdental/Graphql/_Helpers/_AuditHelper.py +0 -67
  17. encryptors-2.58/src/Osdental/Graphql/_Helpers/_ExtractAuthToken.py +0 -11
  18. encryptors-2.58/src/Osdental/Graphql/_Helpers/_TenantPolicy.py +0 -46
  19. encryptors-2.58/src/Osdental/Graphql/_Helpers/_TokenService.py +0 -44
  20. encryptors-2.58/src/Osdental/Helpers/AuditDispatcher.py +0 -169
  21. encryptors-2.58/src/Osdental/Http/_Helpers.py +0 -125
  22. encryptors-2.58/src/Osdental/Models/__init__.py +0 -0
  23. encryptors-2.58/src/Osdental/Rest/Context/RequestContext.py +0 -19
  24. encryptors-2.58/src/Osdental/Rest/Context/__init__.py +0 -0
  25. encryptors-2.58/src/Osdental/Rest/Middlewares/RequestContextMiddleware.py +0 -22
  26. encryptors-2.58/src/Osdental/Rest/Middlewares/__init__.py +0 -0
  27. encryptors-2.58/src/Osdental/Rest/__init__.py +0 -0
  28. encryptors-2.58/src/Osdental/Utils/__init__.py +0 -0
  29. encryptors-2.58/src/Osdental/__init__.py +0 -0
  30. {encryptors-2.58 → encryptors-2.60}/README.md +0 -0
  31. {encryptors-2.58 → encryptors-2.60}/setup.cfg +0 -0
  32. {encryptors-2.58 → encryptors-2.60}/src/Encryptors.egg-info/dependency_links.txt +0 -0
  33. {encryptors-2.58 → encryptors-2.60}/src/Encryptors.egg-info/entry_points.txt +0 -0
  34. {encryptors-2.58 → encryptors-2.60}/src/Encryptors.egg-info/requires.txt +0 -0
  35. {encryptors-2.58 → encryptors-2.60}/src/Encryptors.egg-info/top_level.txt +0 -0
  36. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Cache/Redis.py +0 -0
  37. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Cache/__init__.py +0 -0
  38. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Cli/__init__.py +0 -0
  39. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Constants/Constant.py +0 -0
  40. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Constants/Message.py +0 -0
  41. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Constants/__init__.py +0 -0
  42. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Database/BaseRepository.py +0 -0
  43. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Database/Connection.py +0 -0
  44. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Database/__init__.py +0 -0
  45. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Decorators/Grpc.py +0 -0
  46. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Decorators/Retry.py +0 -0
  47. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Decorators/__init__.py +0 -0
  48. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Encryptor/Aes.py +0 -0
  49. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Encryptor/Argon2.py +0 -0
  50. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Encryptor/Bcrypt.py +0 -0
  51. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Encryptor/Jwt.py +0 -0
  52. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Encryptor/Rsa.py +0 -0
  53. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Encryptor/Sha512.py +0 -0
  54. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Encryptor/__init__.py +0 -0
  55. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Enums/AuditType.py +0 -0
  56. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Enums/ErrorSource.py +0 -0
  57. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Enums/FileType.py +0 -0
  58. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Enums/GrahpqlOperation.py +0 -0
  59. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Enums/Profile.py +0 -0
  60. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Enums/StatusCode.py +0 -0
  61. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Enums/__init__.py +0 -0
  62. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Exception/ControlledException.py +0 -0
  63. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Exception/__init__.py +0 -0
  64. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Helpers/AzureClassifier.py +0 -0
  65. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Helpers/GrpcConnection.py +0 -0
  66. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Helpers/JwtTokenHelper.py +0 -0
  67. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Helpers/Resilience.py +0 -0
  68. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Helpers/ResponseDecryptor.py +0 -0
  69. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Helpers/_AuthTokenProcessor.py +0 -0
  70. {encryptors-2.58/src/Osdental/Graphql/Extensions → encryptors-2.60/src/Osdental/Helpers}/__init__.py +0 -0
  71. {encryptors-2.58/src/Osdental/Graphql/_Helpers → encryptors-2.60/src/Osdental/Http}/__init__.py +0 -0
  72. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Messaging/AzureServiceBus.py +0 -0
  73. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Messaging/Kafka.py +0 -0
  74. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Messaging/RabbitMQ.py +0 -0
  75. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Messaging/__init__.py +0 -0
  76. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Models/ApiResponse.py +0 -0
  77. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Models/AuditContext.py +0 -0
  78. /encryptors-2.58/src/Osdental/Graphql/Models/__init__.py → /encryptors-2.60/src/Osdental/Models/Graphql.py +0 -0
  79. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Models/Notification.py +0 -0
  80. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Models/Response.py +0 -0
  81. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Models/Token.py +0 -0
  82. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Models/TokenClaims.py +0 -0
  83. {encryptors-2.58/src/Osdental/Graphql → encryptors-2.60/src/Osdental/Models}/__init__.py +0 -0
  84. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Secrets/AzureKeyVaultProvider.py +0 -0
  85. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Secrets/__init__.py +0 -0
  86. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Services/JwtAuthTokenService.py +0 -0
  87. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Services/WebsocketClient.py +0 -0
  88. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Storage/AzureBlobStorage.py +0 -0
  89. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Storage/S3Storage.py +0 -0
  90. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Utils/CaseConverter.py +0 -0
  91. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Utils/CodeGenerator.py +0 -0
  92. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Utils/DataNormalizer.py +0 -0
  93. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Utils/DataUtils.py +0 -0
  94. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Utils/DateUtils.py +0 -0
  95. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Utils/FileMetaData.py +0 -0
  96. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Utils/HashValidator.py +0 -0
  97. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Utils/Mapper.py +0 -0
  98. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Utils/PasswordGenerator.py +0 -0
  99. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Utils/QueryGenerator.py +0 -0
  100. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Utils/RsaUtils.py +0 -0
  101. {encryptors-2.58 → encryptors-2.60}/src/Osdental/Utils/TextProcessor.py +0 -0
  102. {encryptors-2.58/src/Osdental/Helpers → encryptors-2.60/src/Osdental/Utils}/__init__.py +0 -0
  103. {encryptors-2.58/src/Osdental/Http → encryptors-2.60/src/Osdental}/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Encryptors
3
- Version: 2.58
3
+ Version: 2.60
4
4
  Summary: End-to-end algorithm library
5
5
  Author: OSDental LLC
6
6
  Author-email: support@osdental.ai
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
  # ANDERSON REVISAR EL CACHE LOCAL DEL KEYVAULT PARA VALIDAR SI FUNCIONA
3
3
  setup(
4
4
  name="Encryptors",
5
- version="2.58",
5
+ version="2.60",
6
6
  author="OSDental LLC",
7
7
  author_email="support@osdental.ai",
8
8
  description="End-to-end algorithm library",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Encryptors
3
- Version: 2.58
3
+ Version: 2.60
4
4
  Summary: End-to-end algorithm library
5
5
  Author: OSDental LLC
6
6
  Author-email: support@osdental.ai
@@ -13,6 +13,7 @@ src/Osdental/Cli/__init__.py
13
13
  src/Osdental/Constants/Constant.py
14
14
  src/Osdental/Constants/Message.py
15
15
  src/Osdental/Constants/__init__.py
16
+ src/Osdental/Context/__init__.py
16
17
  src/Osdental/Database/BaseRepository.py
17
18
  src/Osdental/Database/Connection.py
18
19
  src/Osdental/Database/__init__.py
@@ -32,22 +33,13 @@ src/Osdental/Enums/ErrorSource.py
32
33
  src/Osdental/Enums/FileType.py
33
34
  src/Osdental/Enums/GrahpqlOperation.py
34
35
  src/Osdental/Enums/Profile.py
36
+ src/Osdental/Enums/ResultType.py
35
37
  src/Osdental/Enums/StatusCode.py
36
38
  src/Osdental/Enums/__init__.py
37
39
  src/Osdental/Exception/ControlledException.py
38
40
  src/Osdental/Exception/__init__.py
39
- src/Osdental/Graphql/__init__.py
40
- src/Osdental/Graphql/Extensions/AuditExtension.py
41
- src/Osdental/Graphql/Extensions/__init__.py
42
- src/Osdental/Graphql/Models/__init__.py
43
- src/Osdental/Graphql/_Exceptions/__init__.py
44
- src/Osdental/Graphql/_Helpers/_AuditHelper.py
45
- src/Osdental/Graphql/_Helpers/_ExtractAuthToken.py
46
- src/Osdental/Graphql/_Helpers/_TenantPolicy.py
47
- src/Osdental/Graphql/_Helpers/_TokenService.py
48
- src/Osdental/Graphql/_Helpers/__init__.py
49
- src/Osdental/Helpers/AuditDispatcher.py
50
41
  src/Osdental/Helpers/AuditHelper.py
42
+ src/Osdental/Helpers/AuditQueue.py
51
43
  src/Osdental/Helpers/AzureClassifier.py
52
44
  src/Osdental/Helpers/GrpcConnection.py
53
45
  src/Osdental/Helpers/JwtTokenHelper.py
@@ -56,7 +48,6 @@ src/Osdental/Helpers/ResponseDecryptor.py
56
48
  src/Osdental/Helpers/_AuthTokenProcessor.py
57
49
  src/Osdental/Helpers/__init__.py
58
50
  src/Osdental/Http/APIClient.py
59
- src/Osdental/Http/_Helpers.py
60
51
  src/Osdental/Http/__init__.py
61
52
  src/Osdental/Messaging/AzureServiceBus.py
62
53
  src/Osdental/Messaging/Kafka.py
@@ -64,16 +55,12 @@ src/Osdental/Messaging/RabbitMQ.py
64
55
  src/Osdental/Messaging/__init__.py
65
56
  src/Osdental/Models/ApiResponse.py
66
57
  src/Osdental/Models/AuditContext.py
58
+ src/Osdental/Models/Graphql.py
67
59
  src/Osdental/Models/Notification.py
68
60
  src/Osdental/Models/Response.py
69
61
  src/Osdental/Models/Token.py
70
62
  src/Osdental/Models/TokenClaims.py
71
63
  src/Osdental/Models/__init__.py
72
- src/Osdental/Rest/__init__.py
73
- src/Osdental/Rest/Context/RequestContext.py
74
- src/Osdental/Rest/Context/__init__.py
75
- src/Osdental/Rest/Middlewares/RequestContextMiddleware.py
76
- src/Osdental/Rest/Middlewares/__init__.py
77
64
  src/Osdental/Secrets/AzureKeyVaultProvider.py
78
65
  src/Osdental/Secrets/__init__.py
79
66
  src/Osdental/Services/JwtAuthTokenService.py
@@ -0,0 +1,5 @@
1
+ from contextvars import ContextVar
2
+ from typing import Optional
3
+ from Osdental.Models.AuditContext import AuditContext
4
+
5
+ _ctx: ContextVar[Optional[AuditContext]] = ContextVar('ctx', default=None)
@@ -3,7 +3,7 @@ from functools import wraps
3
3
  from typing import Callable
4
4
  from graphql import GraphQLResolveInfo
5
5
  from Osdental.Models.Response import Response
6
- from Osdental.Graphql.Models import BaseGraphQLContext
6
+ from Osdental.Models.Graphql import BaseGraphQLContext
7
7
  from Osdental.Models.TokenClaims import UserTokenClaims
8
8
  from Osdental.Models.Token import AuthToken
9
9
  from Osdental.Enums.Profile import Profile
@@ -19,12 +19,11 @@ from Osdental.Models.AuditContext import AuditContext
19
19
  from Osdental.Helpers.AuditHelper import (
20
20
  _build_success_payload, _build_error_payload, _build_unexpected_payload
21
21
  )
22
-
22
+ from Osdental.Context import _ctx
23
23
 
24
24
  logger = logging.getLogger(__name__)
25
25
 
26
26
 
27
-
28
27
  def resolver(public: bool = False, action=None):
29
28
 
30
29
  def decorator(func: Callable):
@@ -95,6 +94,8 @@ def resolver(public: bool = False, action=None):
95
94
  decrypted_payload=decrypted_payload,
96
95
  )
97
96
 
97
+ _ctx.set(audit_ctx)
98
+
98
99
  # ── 6. EJECUTAR RESOLVER ──────────────────
99
100
  result = await func(obj, info, **kwargs)
100
101
 
@@ -0,0 +1,5 @@
1
+ from enum import StrEnum
2
+
3
+ class ResultType(StrEnum):
4
+ RESPONSE = "Response"
5
+ ERROR = "Error"
@@ -1,11 +1,14 @@
1
1
  import json
2
+ from typing import Optional, List, Dict, Any
2
3
  from fastapi import Request
3
- from typing import Optional, Dict, Any, Literal
4
+ from httpx import HTTPStatusError, RequestError
4
5
  from Osdental.Models.AuditContext import AuditContext
5
6
  from Osdental.Models.Response import Response
7
+ from Osdental.Models.ApiResponse import ApiResponse
6
8
  from Osdental.Exception.ControlledException import OSDException
7
9
  from Osdental.Enums.StatusCode import StatusCode
8
10
  from Osdental.Enums.AuditType import AuditType
11
+ from Osdental.Enums.ResultType import ResultType
9
12
 
10
13
  def build_request_payload(
11
14
  request: Request,
@@ -53,8 +56,8 @@ def build_request_payload(
53
56
  }
54
57
 
55
58
  def _build_final_payload(
56
- type_: Literal["RESPONSE", "ERROR"],
57
- status_code: int,
59
+ status_code: int | str,
60
+ type_: ResultType = ResultType.ERROR,
58
61
  result: Optional[Any] = None,
59
62
  error: Optional[Any] = None
60
63
  ) -> Dict[str, Any]:
@@ -73,25 +76,23 @@ def _build_final_payload(
73
76
  }
74
77
 
75
78
  def _build_success_payload(ctx: Optional[AuditContext], result: Response) -> Dict[str, Any]:
76
- base = _base_payload(ctx, AuditType.INTERNAL)
79
+ base = _base_payload(ctx)
77
80
  return base | _build_final_payload(
78
- type_="RESPONSE",
81
+ type_=ResultType.RESPONSE,
79
82
  status_code=result.status,
80
83
  result=result.data
81
84
  )
82
85
 
83
86
  def _build_error_payload(ctx: Optional[AuditContext], exc: OSDException) -> Dict[str, Any]:
84
- base = _base_payload(ctx, AuditType.INTERNAL)
87
+ base = _base_payload(ctx)
85
88
  return base | _build_final_payload(
86
- type_="ERROR",
87
89
  status_code=exc.audit_status_code,
88
90
  error=exc.error or exc.message
89
91
  )
90
92
 
91
93
  def _build_unexpected_payload(ctx: Optional[AuditContext], exc: Exception) -> Dict[str, Any]:
92
- base = _base_payload(ctx, AuditType.INTERNAL)
94
+ base = _base_payload(ctx)
93
95
  return base | _build_final_payload(
94
- type_="ERROR",
95
96
  status_code=StatusCode.INTERNAL_SERVER_ERROR,
96
97
  error=str(exc)
97
98
  )
@@ -115,4 +116,51 @@ def _base_payload(ctx: Optional[AuditContext], audit_type: AuditType = AuditType
115
116
  "httpMethod": ctx.http_method,
116
117
  "messageIn": json.dumps(ctx.variables) if ctx.variables else "*",
117
118
  "auditLog": audit_type,
118
- }
119
+ }
120
+
121
+
122
+ # ------------ FOR EXTERNAL APIS --------------------
123
+ def _build_success_http_payload(ctx: Optional[AuditContext], result: ApiResponse) -> Dict[str, Any]:
124
+ base = _base_payload(ctx, AuditType.EXTERNAL)
125
+ return base | _build_final_payload(
126
+ type_=ResultType.RESPONSE,
127
+ status_code=result.status,
128
+ result=result.data
129
+ )
130
+
131
+ def _build_error_http_payload(ctx: Optional[AuditContext], exc: HTTPStatusError) -> Dict[str, Any]:
132
+ base = _base_payload(ctx, AuditType.EXTERNAL)
133
+ return base | _build_final_payload(
134
+ status_code=exc.response.status_code,
135
+ error=exc.response.text
136
+ )
137
+
138
+ def _build_error_http_request_payload(ctx: Optional[AuditContext], exc: RequestError) -> Dict[str, Any]:
139
+ base = _base_payload(ctx, AuditType.EXTERNAL)
140
+ return base | _build_final_payload(
141
+ status_code=503,
142
+ error=str(exc)
143
+ )
144
+
145
+ def _build_unexpected_http_payload(ctx: Optional[AuditContext], exc: Exception) -> Dict[str, Any]:
146
+ base = _base_payload(ctx, AuditType.EXTERNAL)
147
+ return base | _build_final_payload(
148
+ status_code=StatusCode.INTERNAL_SERVER_ERROR,
149
+ error=str(exc)
150
+ )
151
+
152
+ def _build_success_graphql_payload(ctx: Optional[AuditContext], result: Dict[str, Any]) -> Dict[str, Any]:
153
+ base = _base_payload(ctx, AuditType.EXTERNAL)
154
+ return base | _build_final_payload(
155
+ type_=ResultType.RESPONSE,
156
+ status_code=200,
157
+ result=result
158
+ )
159
+
160
+
161
+ def _build_error_graphql_payload(ctx: Optional[AuditContext], errors: List[Dict[str, Any]]) -> Dict[str, Any]:
162
+ base = _base_payload(ctx, AuditType.EXTERNAL)
163
+ return base | _build_final_payload(
164
+ status_code=StatusCode.INTERNAL_SERVER_ERROR,
165
+ error=json.dumps(errors)
166
+ )
@@ -0,0 +1,45 @@
1
+ import asyncio
2
+ import logging
3
+
4
+ logger = logging.getLogger(__name__)
5
+
6
+
7
+ class AuditQueue:
8
+
9
+ def __init__(
10
+ self,
11
+ worker,
12
+ maxsize: int = 1000,
13
+ workers: int = 5
14
+ ):
15
+ self._worker = worker
16
+ self._queue = asyncio.Queue(maxsize=maxsize)
17
+ self._workers = workers
18
+ self._tasks: list[asyncio.Task] = []
19
+
20
+ async def emit(self, payload):
21
+ await self._queue.put(payload)
22
+
23
+ def start(self):
24
+ for _ in range(self._workers):
25
+ task = asyncio.create_task(self._consume())
26
+ self._tasks.append(task)
27
+
28
+ async def _consume(self):
29
+ while True:
30
+ payload = await self._queue.get()
31
+
32
+ try:
33
+ await self._worker(payload)
34
+
35
+ except Exception:
36
+ logger.exception("Error processing audit")
37
+
38
+ finally:
39
+ self._queue.task_done()
40
+
41
+ async def stop(self):
42
+ await self._queue.join()
43
+
44
+ for task in self._tasks:
45
+ task.cancel()
@@ -3,12 +3,16 @@ from typing import Optional, Dict, Any
3
3
  import httpx
4
4
  from http import HTTPMethod
5
5
  from Osdental.Decorators.Retry import rest_retry
6
- from Osdental.Http._Helpers import (
7
- audit_success, audit_http_error, audit_exception_error,
8
- audit_unknown_error, audit_graphql_error
9
- )
6
+ from Osdental.Models.ApiResponse import ApiResponse
7
+ from Osdental.Services import IAuditEmitter
10
8
  from Osdental.Exception.ControlledException import HttpClientException
11
9
  from Osdental.Enums.StatusCode import StatusCode
10
+ from Osdental.Context import _ctx
11
+ from Osdental.Helpers.AuditHelper import (
12
+ _build_success_http_payload, _build_error_http_payload,
13
+ _build_error_http_request_payload, _build_unexpected_http_payload,
14
+ _build_success_graphql_payload, _build_error_graphql_payload
15
+ )
12
16
 
13
17
  logger = logging.getLogger(__name__)
14
18
 
@@ -16,9 +20,12 @@ class APIClient:
16
20
 
17
21
  def __init__(
18
22
  self,
23
+ audit_emit: Optional[IAuditEmitter] = None,
19
24
  timeout: Optional[httpx.Timeout] = None,
20
25
  limits: Optional[httpx.Limits] = None
21
26
  ):
27
+ self._audit_emit = audit_emit
28
+
22
29
  self._client = httpx.AsyncClient(
23
30
  follow_redirects=True,
24
31
  timeout=timeout or httpx.Timeout(10.0, read=20.0),
@@ -34,6 +41,9 @@ class APIClient:
34
41
  @rest_retry
35
42
  async def _request(self, method: HTTPMethod, url: str, **kwargs) -> httpx.Response:
36
43
 
44
+ audit_ctx = _ctx.get()
45
+ audit_ctx.headers = kwargs.get("headers") or {}
46
+
37
47
  try:
38
48
 
39
49
  response = await self._client.request(
@@ -44,22 +54,31 @@ class APIClient:
44
54
 
45
55
  response.raise_for_status()
46
56
 
47
- audit_success(response, method, url, kwargs)
57
+ api_res = ApiResponse(
58
+ status=response.status_code,
59
+ data=response.text
60
+ )
61
+
62
+ await self._audit_emit.emit(
63
+ _build_success_http_payload(audit_ctx, api_res)
64
+ )
48
65
 
49
66
  return response
50
67
 
51
68
  except httpx.HTTPStatusError as exc:
52
69
 
53
- audit_http_error(exc.response, method, url, kwargs)
70
+ await self._audit_emit.emit(
71
+ _build_error_http_payload(audit_ctx, exc)
72
+ )
73
+
54
74
  raise HttpClientException(
55
75
  status_code=StatusCode.BAD_GATEWAY,
56
76
  error=exc.response.text,
57
- raw_status_code=exc.response.status_code,
58
77
  ) from exc
59
78
 
60
79
  except httpx.RequestError as exc:
61
80
 
62
- audit_exception_error(exc, method, url, kwargs)
81
+ _build_error_http_request_payload(audit_ctx, exc)
63
82
  raise HttpClientException(
64
83
  status_code=StatusCode.GATEWAY_TIMEOUT,
65
84
  error=str(exc),
@@ -67,7 +86,7 @@ class APIClient:
67
86
 
68
87
  except Exception as exc:
69
88
 
70
- audit_unknown_error(exc, method, url, kwargs)
89
+ _build_unexpected_http_payload(audit_ctx, exc)
71
90
 
72
91
  raise
73
92
 
@@ -105,6 +124,9 @@ class APIClient:
105
124
  headers: Optional[Dict[str, str]] = None
106
125
  ) -> Dict[str, Any]:
107
126
 
127
+ audit_ctx = _ctx.get()
128
+ audit_ctx.headers = headers or {}
129
+
108
130
  payload = {
109
131
  "query": query,
110
132
  "variables": variables
@@ -119,13 +141,16 @@ class APIClient:
119
141
 
120
142
  data = response.json()
121
143
 
122
- if "errors" in data:
144
+ _build_success_graphql_payload(audit_ctx, data)
123
145
 
146
+ if "errors" in data:
147
+
148
+ errors = data['errors']
124
149
  logger.error(
125
- f"GraphQL error: {data['errors']}"
150
+ f"GraphQL error: {errors}"
126
151
  )
127
152
 
128
- audit_graphql_error(data["errors"], url, payload)
153
+ _build_error_graphql_payload(audit_ctx, errors)
129
154
  raise HttpClientException(
130
155
  message="GraphQL execution failed",
131
156
  error=str(data["errors"])
@@ -8,10 +8,14 @@ class ServiceBusAuditEmitter:
8
8
  def __init__(self, messaging: IMessageQueue, storage: IStorageService):
9
9
  self._messaging = messaging
10
10
  self._storage = storage
11
+
11
12
 
12
- async def emit(self, payload: Dict[str, Any]) -> None:
13
+ async def process(self, payload: Dict[str, Any]) -> None:
13
14
  message_log_id = payload.get("idMessageLog")
14
15
  data = json.dumps(payload)
15
16
  blob_name = f"audits/{message_log_id}.text"
16
- await self._storage.upload(blob_name, data)
17
+ url = await self._storage.upload(blob_name, data)
18
+ if not url:
19
+ return
20
+
17
21
  await self._messaging.send_message(blob_name)
@@ -49,5 +49,5 @@ class IAuthTokenService(ABC):
49
49
 
50
50
 
51
51
  class IAuditEmitter(Protocol):
52
- async def emit(self, payload: Dict[str, Any]) -> None:
52
+ async def process(self, payload: Dict[str, Any]) -> None:
53
53
  ...
@@ -6,13 +6,13 @@ from azure.storage.blob import BlobSasPermissions
6
6
  class IStorageService(ABC):
7
7
 
8
8
  @abstractmethod
9
- async def upload(self, blob_name: str, data: bytes | str) -> str | bool:
9
+ async def upload(self, blob_name: str, data: bytes | str) -> str:
10
10
  """ Upload a file to blob storage """
11
11
  pass
12
12
 
13
13
 
14
14
  @abstractmethod
15
- async def download(self, blob_name: str) -> bytes | None:
15
+ async def download(self, blob_name: str) -> bytes:
16
16
  """ Download a file from blob storage """
17
17
  pass
18
18
 
@@ -1,196 +0,0 @@
1
- import inspect
2
- import logging
3
- import json
4
- from graphql.pyutils import is_awaitable
5
- from ariadne.types import Extension
6
- from Osdental.Encryptor.Aes import AES
7
- from Osdental.Graphql._Helpers._TenantPolicy import TenantPolicy
8
- from Osdental.Rest.Context.RequestContext import (
9
- current_context,
10
- RequestContext
11
- )
12
- from Osdental.Models.Response import Response
13
- from Osdental.Graphql.Models import BaseGraphQLContext
14
- from Osdental.Constants.Constant import Constant
15
-
16
- logger = logging.getLogger(__name__)
17
-
18
- class AuditExtension(Extension):
19
-
20
- def request_started(self, context):
21
- request = context.request
22
- self._ctx_token = current_context.set(
23
- RequestContext(
24
- request=request,
25
- request_id=request.headers.get("x-request-id"),
26
- user=None
27
- )
28
- )
29
-
30
- self.errors = None
31
- self.request_payload = None
32
- self.result = None
33
-
34
- def _should_skip(self, body) -> bool:
35
- query = body.get("query", "") or ""
36
- return "__schema" in query or "__type(" in query
37
-
38
- async def resolve(self, next_, root, info, **kwargs):
39
-
40
- context: BaseGraphQLContext = info.context
41
- request = context.request
42
-
43
- # cache body
44
- context._cached_body = (
45
- getattr(context, "_cached_body", None) or await request.json()
46
- )
47
- body = context._cached_body
48
-
49
- # skip introspection
50
- if self._should_skip(body):
51
- result = next_(root, info, **kwargs)
52
- if is_awaitable(result):
53
- result = await result
54
- return result
55
-
56
- is_root = root is None
57
-
58
- # SOLO ROOT
59
- if is_root:
60
-
61
- # identificar resolver público
62
- resolver_fn = inspect.unwrap(next_)
63
- is_public = getattr(resolver_fn, "_is_public", False)
64
-
65
- # inicializar auth UNA SOLA VEZ
66
- if not hasattr(context, "_auth_initialized"):
67
- context._auth_initialized = True
68
-
69
- headers = request.headers
70
- container = context.container
71
-
72
- original_token = None
73
- context.aes_auth = None
74
- if not is_public and headers.get("authorization"):
75
- token_service = container.token_service
76
- aes_auth = request.app.state.aes_auth
77
- aes_user = request.app.state.aes_user
78
-
79
- try:
80
- original_token = await token_service.authenticate(headers, aes_user)
81
- except Exception:
82
- original_token = None
83
-
84
- context.aes_auth = aes_auth
85
-
86
- # VALIDACIÓN SOLO ROOT
87
- if not is_public and not original_token:
88
- raise ValueError("Authorization required")
89
-
90
- # payload
91
- variables = body.get("variables") or {}
92
-
93
- encrypted_payload = (
94
- kwargs.get("data")
95
- or variables.get("data")
96
- )
97
-
98
- decrypted_payload = None
99
-
100
- if not is_public and encrypted_payload is not None:
101
-
102
- if not isinstance(encrypted_payload, str):
103
- raise ValueError("Encrypted payload must be a string")
104
-
105
- if not context.aes_auth:
106
- raise ValueError("Missing AES configuration")
107
-
108
- try:
109
- decrypted = AES.decrypt(context.aes_auth, encrypted_payload)
110
- except Exception:
111
- raise ValueError("Invalid encrypted payload")
112
-
113
- if isinstance(decrypted, dict):
114
- decrypted_payload = decrypted
115
-
116
- elif isinstance(decrypted, str):
117
- try:
118
- decrypted_payload = json.loads(decrypted)
119
- except Exception:
120
- raise ValueError("Decrypted payload is not valid JSON")
121
-
122
- else:
123
- raise ValueError("Unsupported decrypted payload type")
124
-
125
- if decrypted_payload is not None:
126
- kwargs["data"] = decrypted_payload
127
-
128
- if not is_public:
129
- token = TenantPolicy.resolve(
130
- token=original_token,
131
- headers=request.headers,
132
- decrypted_payload=decrypted_payload,
133
- operation_type=info.operation.operation.value
134
- )
135
- context.token = token
136
-
137
- # auditoría request payload
138
- if not self.request_payload:
139
- token = context.token
140
-
141
- self.request = request
142
- self.request_payload = {
143
- "operation_type": info.operation.operation.value,
144
- "operation_name": body.get("operationName", "UnknownOperation"),
145
- "query": body.get("query"),
146
- "variables": decrypted_payload,
147
- "user": token.user_full_name if token else "Public"
148
- }
149
-
150
- # SIEMPRE ejecutar resolver (root + fields)
151
- result = next_(root, info, **kwargs)
152
-
153
- if is_awaitable(result):
154
- result = await result
155
-
156
- # SOLO ROOT captura resultado final
157
- if is_root and self.result is None:
158
- self.result = result
159
-
160
- return result
161
-
162
- def has_errors(self, errors, context):
163
- self.errors = errors
164
-
165
- def request_finished(self, context):
166
-
167
- if not self.request_payload:
168
- return
169
-
170
- query = self.request_payload.get("query", "")
171
-
172
- if self._should_skip({"query": query}):
173
- return
174
-
175
- if self.result is None:
176
- return
177
-
178
- decrypted_key = context.aes_auth
179
-
180
- if isinstance(self.result, Response):
181
- decrypted_key = self.result.key or decrypted_key
182
-
183
- dispatcher = context.request.app.state.audit_dispatcher
184
-
185
- try:
186
- dispatcher.dispatch(
187
- request=self.request,
188
- request_payload=self.request_payload,
189
- result=self.result,
190
- metadata={
191
- "decrypted_key": decrypted_key
192
- },
193
- audit_type=Constant.MESSAGE_LOG_INTERNAL
194
- )
195
- except Exception as e:
196
- logger.warning(f"[AUDIT ERROR]: {str(e)}")
@@ -1,8 +0,0 @@
1
- from Osdental.Exception.ControlledException import OSDException
2
-
3
- class AESKeyNotFound(OSDException):
4
- def __init__(
5
- self,
6
- error: str = None
7
- ):
8
- super().__init__(error=error)