nexo-schemas 0.0.16__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 (69) hide show
  1. nexo/schemas/__init__.py +0 -0
  2. nexo/schemas/application.py +292 -0
  3. nexo/schemas/connection.py +134 -0
  4. nexo/schemas/data.py +27 -0
  5. nexo/schemas/document.py +237 -0
  6. nexo/schemas/error/__init__.py +476 -0
  7. nexo/schemas/error/constants.py +50 -0
  8. nexo/schemas/error/descriptor.py +354 -0
  9. nexo/schemas/error/enums.py +40 -0
  10. nexo/schemas/error/metadata.py +15 -0
  11. nexo/schemas/error/spec.py +312 -0
  12. nexo/schemas/exception/__init__.py +0 -0
  13. nexo/schemas/exception/exc.py +911 -0
  14. nexo/schemas/exception/factory.py +1928 -0
  15. nexo/schemas/exception/handlers.py +110 -0
  16. nexo/schemas/google.py +14 -0
  17. nexo/schemas/key/__init__.py +0 -0
  18. nexo/schemas/key/rsa.py +131 -0
  19. nexo/schemas/metadata.py +21 -0
  20. nexo/schemas/mixins/__init__.py +0 -0
  21. nexo/schemas/mixins/filter.py +140 -0
  22. nexo/schemas/mixins/general.py +65 -0
  23. nexo/schemas/mixins/hierarchy.py +19 -0
  24. nexo/schemas/mixins/identity.py +387 -0
  25. nexo/schemas/mixins/parameter.py +50 -0
  26. nexo/schemas/mixins/service.py +40 -0
  27. nexo/schemas/mixins/sort.py +111 -0
  28. nexo/schemas/mixins/timestamp.py +192 -0
  29. nexo/schemas/model.py +240 -0
  30. nexo/schemas/operation/__init__.py +0 -0
  31. nexo/schemas/operation/action/__init__.py +9 -0
  32. nexo/schemas/operation/action/base.py +14 -0
  33. nexo/schemas/operation/action/resource.py +371 -0
  34. nexo/schemas/operation/action/status.py +8 -0
  35. nexo/schemas/operation/action/system.py +6 -0
  36. nexo/schemas/operation/action/websocket.py +6 -0
  37. nexo/schemas/operation/base.py +289 -0
  38. nexo/schemas/operation/constants.py +18 -0
  39. nexo/schemas/operation/context.py +68 -0
  40. nexo/schemas/operation/dependency.py +26 -0
  41. nexo/schemas/operation/enums.py +168 -0
  42. nexo/schemas/operation/extractor.py +36 -0
  43. nexo/schemas/operation/mixins.py +53 -0
  44. nexo/schemas/operation/request.py +1066 -0
  45. nexo/schemas/operation/resource.py +839 -0
  46. nexo/schemas/operation/system.py +55 -0
  47. nexo/schemas/operation/websocket.py +55 -0
  48. nexo/schemas/pagination.py +67 -0
  49. nexo/schemas/parameter.py +60 -0
  50. nexo/schemas/payload.py +116 -0
  51. nexo/schemas/resource.py +64 -0
  52. nexo/schemas/response.py +1041 -0
  53. nexo/schemas/security/__init__.py +0 -0
  54. nexo/schemas/security/api_key.py +63 -0
  55. nexo/schemas/security/authentication.py +848 -0
  56. nexo/schemas/security/authorization.py +922 -0
  57. nexo/schemas/security/enums.py +32 -0
  58. nexo/schemas/security/impersonation.py +179 -0
  59. nexo/schemas/security/token.py +402 -0
  60. nexo/schemas/security/types.py +17 -0
  61. nexo/schemas/success/__init__.py +0 -0
  62. nexo/schemas/success/descriptor.py +100 -0
  63. nexo/schemas/success/enums.py +23 -0
  64. nexo/schemas/user_agent.py +46 -0
  65. nexo_schemas-0.0.16.dist-info/METADATA +87 -0
  66. nexo_schemas-0.0.16.dist-info/RECORD +69 -0
  67. nexo_schemas-0.0.16.dist-info/WHEEL +5 -0
  68. nexo_schemas-0.0.16.dist-info/licenses/LICENSE +21 -0
  69. nexo_schemas-0.0.16.dist-info/top_level.txt +1 -0
@@ -0,0 +1,371 @@
1
+ import re
2
+ from fastapi import status, HTTPException, Request
3
+ from pydantic import Field
4
+ from typing import (
5
+ Annotated,
6
+ Callable,
7
+ Generic,
8
+ Literal,
9
+ Mapping,
10
+ Type,
11
+ TypeVar,
12
+ overload,
13
+ )
14
+ from ..enums import (
15
+ ResourceOperationType,
16
+ ResourceOperationTypeT,
17
+ OptResourceOperationType,
18
+ ResourceOperationUpdateType,
19
+ OptResourceOperationUpdateType,
20
+ ResourceOperationDataUpdateType,
21
+ OptResourceOperationDataUpdateType,
22
+ ResourceOperationStatusUpdateType,
23
+ OptResourceOperationStatusUpdateType,
24
+ )
25
+ from .base import BaseOperationAction
26
+
27
+
28
+ class ResourceOperationAction(
29
+ BaseOperationAction[ResourceOperationTypeT], Generic[ResourceOperationTypeT]
30
+ ):
31
+ pass
32
+
33
+
34
+ class CreateResourceOperationAction(
35
+ ResourceOperationAction[Literal[ResourceOperationType.CREATE]]
36
+ ):
37
+ type: Literal[ResourceOperationType.CREATE] = ResourceOperationType.CREATE
38
+
39
+
40
+ CREATE_RESOURCE_OPERATION_ACTION = CreateResourceOperationAction()
41
+
42
+
43
+ class ReadResourceOperationAction(
44
+ ResourceOperationAction[Literal[ResourceOperationType.READ]]
45
+ ):
46
+ type: Literal[ResourceOperationType.READ] = ResourceOperationType.READ
47
+
48
+
49
+ class UpdateResourceOperationAction(
50
+ ResourceOperationAction[Literal[ResourceOperationType.UPDATE]]
51
+ ):
52
+ type: Literal[ResourceOperationType.UPDATE] = ResourceOperationType.UPDATE
53
+ update_type: Annotated[
54
+ OptResourceOperationUpdateType,
55
+ Field(None, description="Update type (optional)"),
56
+ ] = None
57
+ data_update_type: Annotated[
58
+ OptResourceOperationDataUpdateType,
59
+ Field(None, description="Data update type (optional)"),
60
+ ] = None
61
+ status_update_type: Annotated[
62
+ OptResourceOperationStatusUpdateType,
63
+ Field(None, description="Status update type (optional)"),
64
+ ] = None
65
+
66
+
67
+ class DeleteResourceOperationAction(
68
+ ResourceOperationAction[Literal[ResourceOperationType.DELETE]]
69
+ ):
70
+ type: Literal[ResourceOperationType.DELETE] = ResourceOperationType.DELETE
71
+
72
+
73
+ ResourceOperationActions = (
74
+ CreateResourceOperationAction,
75
+ ReadResourceOperationAction,
76
+ UpdateResourceOperationAction,
77
+ DeleteResourceOperationAction,
78
+ )
79
+
80
+
81
+ AnyResourceOperationAction = (
82
+ CreateResourceOperationAction
83
+ | ReadResourceOperationAction
84
+ | UpdateResourceOperationAction
85
+ | DeleteResourceOperationAction
86
+ )
87
+
88
+
89
+ AnyResourceOperationActionT = TypeVar(
90
+ "AnyResourceOperationActionT", bound=AnyResourceOperationAction
91
+ )
92
+
93
+
94
+ OptAnyResourceOperationAction = AnyResourceOperationAction | None
95
+
96
+
97
+ TYPE_ACTION_MODEL_MAP: Mapping[ResourceOperationType, Type] = {
98
+ ResourceOperationType.CREATE: CreateResourceOperationAction,
99
+ ResourceOperationType.READ: ReadResourceOperationAction,
100
+ ResourceOperationType.UPDATE: UpdateResourceOperationAction,
101
+ ResourceOperationType.DELETE: DeleteResourceOperationAction,
102
+ }
103
+
104
+
105
+ class ResourceOperationActionFactory:
106
+ @overload
107
+ @staticmethod
108
+ def generate(
109
+ type_: Literal[ResourceOperationType.CREATE],
110
+ /,
111
+ ) -> CreateResourceOperationAction: ...
112
+ @overload
113
+ @staticmethod
114
+ def generate(
115
+ type_: Literal[ResourceOperationType.READ],
116
+ /,
117
+ ) -> ReadResourceOperationAction: ...
118
+ @overload
119
+ @staticmethod
120
+ def generate(
121
+ type_: Literal[ResourceOperationType.UPDATE],
122
+ *,
123
+ update_type: OptResourceOperationUpdateType = ...,
124
+ data_update_type: OptResourceOperationDataUpdateType = ...,
125
+ status_update_type: OptResourceOperationStatusUpdateType = ...,
126
+ ) -> UpdateResourceOperationAction: ...
127
+ @overload
128
+ @staticmethod
129
+ def generate(
130
+ type_: Literal[ResourceOperationType.DELETE],
131
+ /,
132
+ ) -> DeleteResourceOperationAction: ...
133
+ @overload
134
+ @staticmethod
135
+ def generate(
136
+ type_: ResourceOperationType,
137
+ *,
138
+ update_type: OptResourceOperationUpdateType = None,
139
+ data_update_type: OptResourceOperationDataUpdateType = None,
140
+ status_update_type: OptResourceOperationStatusUpdateType = None,
141
+ ) -> AnyResourceOperationAction: ...
142
+ @staticmethod
143
+ def generate(
144
+ type_: ResourceOperationType,
145
+ *,
146
+ update_type: OptResourceOperationUpdateType = None,
147
+ data_update_type: OptResourceOperationDataUpdateType = None,
148
+ status_update_type: OptResourceOperationStatusUpdateType = None,
149
+ ) -> AnyResourceOperationAction:
150
+ if type_ is ResourceOperationType.CREATE:
151
+ return CreateResourceOperationAction()
152
+
153
+ elif type_ is ResourceOperationType.READ:
154
+ return ReadResourceOperationAction()
155
+
156
+ elif type_ is ResourceOperationType.UPDATE:
157
+ return UpdateResourceOperationAction(
158
+ update_type=update_type,
159
+ data_update_type=data_update_type,
160
+ status_update_type=status_update_type,
161
+ )
162
+
163
+ elif type_ is ResourceOperationType.DELETE:
164
+ return DeleteResourceOperationAction()
165
+
166
+ @overload
167
+ @staticmethod
168
+ def extract(
169
+ *,
170
+ request: Request,
171
+ from_state: bool = True,
172
+ strict: Literal[False],
173
+ ) -> AnyResourceOperationAction: ...
174
+ @overload
175
+ @staticmethod
176
+ def extract(
177
+ type_: Literal[ResourceOperationType.CREATE],
178
+ *,
179
+ request: Request,
180
+ from_state: bool = True,
181
+ strict: Literal[True],
182
+ ) -> CreateResourceOperationAction: ...
183
+ @overload
184
+ @staticmethod
185
+ def extract(
186
+ type_: Literal[ResourceOperationType.READ],
187
+ *,
188
+ request: Request,
189
+ from_state: bool = True,
190
+ strict: Literal[True],
191
+ ) -> ReadResourceOperationAction: ...
192
+ @overload
193
+ @staticmethod
194
+ def extract(
195
+ type_: Literal[ResourceOperationType.UPDATE],
196
+ *,
197
+ request: Request,
198
+ from_state: bool = True,
199
+ strict: Literal[True],
200
+ ) -> UpdateResourceOperationAction: ...
201
+ @overload
202
+ @staticmethod
203
+ def extract(
204
+ type_: Literal[ResourceOperationType.DELETE],
205
+ *,
206
+ request: Request,
207
+ from_state: bool = True,
208
+ strict: Literal[True],
209
+ ) -> DeleteResourceOperationAction: ...
210
+ @overload
211
+ @staticmethod
212
+ def extract(
213
+ type_: OptResourceOperationType = None,
214
+ *,
215
+ request: Request,
216
+ from_state: bool = True,
217
+ strict: bool = False,
218
+ ) -> AnyResourceOperationAction: ...
219
+ @staticmethod
220
+ def extract(
221
+ type_: OptResourceOperationType = None,
222
+ *,
223
+ request: Request,
224
+ from_state: bool = True,
225
+ strict: bool = False,
226
+ ) -> AnyResourceOperationAction:
227
+ if from_state:
228
+ action = request.state.operation_action
229
+ if not strict:
230
+ if not isinstance(action, ResourceOperationActions):
231
+ raise HTTPException(
232
+ status_code=status.HTTP_400_BAD_REQUEST,
233
+ detail=f"Invalid resource_operation_action in request's state: {action}",
234
+ )
235
+ return action
236
+
237
+ else:
238
+ if type_ is None:
239
+ raise HTTPException(
240
+ status_code=status.HTTP_400_BAD_REQUEST,
241
+ detail="Argument 'type_' must be given for strict extraction",
242
+ )
243
+ model = TYPE_ACTION_MODEL_MAP[type_]
244
+ if not isinstance(action, model):
245
+ raise HTTPException(
246
+ status_code=status.HTTP_400_BAD_REQUEST,
247
+ detail=f"Mismatched 'resource_operation_action' type, expected '{model.__name__}' but received '{type(action).__name__}'",
248
+ )
249
+ return action
250
+
251
+ else:
252
+ action = None
253
+ update_type = None
254
+ data_update_type = None
255
+ status_update_type = None
256
+
257
+ if request.method == "POST":
258
+ action = CreateResourceOperationAction()
259
+ elif request.method == "GET":
260
+ action = ReadResourceOperationAction()
261
+ elif request.method in ["PATCH", "PUT"]:
262
+ if request.method == "PUT":
263
+ update_type = ResourceOperationUpdateType.DATA
264
+ data_update_type = ResourceOperationDataUpdateType.FULL
265
+ elif request.method == "PATCH":
266
+ status_pattern = re.search(
267
+ r"/status/(delete|restore|deactivate|activate)(?:/.*)?$",
268
+ request.url.path,
269
+ )
270
+ if status_pattern:
271
+ update_type = ResourceOperationUpdateType.STATUS
272
+ action = status_pattern.group(1)
273
+ try:
274
+ status_update_type = ResourceOperationStatusUpdateType(
275
+ action
276
+ )
277
+ except ValueError:
278
+ # This shouldn't happen since regex already validates, but keep for safety
279
+ pass
280
+ else:
281
+ update_type = ResourceOperationUpdateType.DATA
282
+ data_update_type = ResourceOperationDataUpdateType.PARTIAL
283
+ action = UpdateResourceOperationAction(
284
+ update_type=update_type,
285
+ data_update_type=data_update_type,
286
+ status_update_type=status_update_type,
287
+ )
288
+ elif request.method == "DELETE":
289
+ action = DeleteResourceOperationAction()
290
+
291
+ if action is None:
292
+ raise HTTPException(
293
+ status_code=status.HTTP_400_BAD_REQUEST,
294
+ detail="Unable to determine resource operation action",
295
+ )
296
+
297
+ if not strict:
298
+ if not isinstance(action, ResourceOperationActions):
299
+ raise HTTPException(
300
+ status_code=status.HTTP_400_BAD_REQUEST,
301
+ detail=f"Invalid resource_operation_action in request's state: {action}",
302
+ )
303
+ return action
304
+
305
+ else:
306
+ if type_ is None:
307
+ raise HTTPException(
308
+ status_code=status.HTTP_400_BAD_REQUEST,
309
+ detail="Argument 'type_' must be given for strict extraction",
310
+ )
311
+ model = TYPE_ACTION_MODEL_MAP[type_]
312
+ if not isinstance(action, model):
313
+ raise HTTPException(
314
+ status_code=status.HTTP_400_BAD_REQUEST,
315
+ detail=f"Mismatched 'resource_operation_action' type, expected '{model.__name__}' but received '{type(action).__name__}'",
316
+ )
317
+ return action
318
+
319
+ @overload
320
+ @staticmethod
321
+ def as_dependency(
322
+ *,
323
+ from_state: bool = True,
324
+ strict: Literal[False],
325
+ ) -> Callable[..., AnyResourceOperationAction]: ...
326
+ @overload
327
+ @staticmethod
328
+ def as_dependency(
329
+ type_: Literal[ResourceOperationType.CREATE],
330
+ *,
331
+ from_state: bool = True,
332
+ strict: Literal[True],
333
+ ) -> Callable[..., CreateResourceOperationAction]: ...
334
+ @overload
335
+ @staticmethod
336
+ def as_dependency(
337
+ type_: Literal[ResourceOperationType.READ],
338
+ *,
339
+ from_state: bool = True,
340
+ strict: Literal[True],
341
+ ) -> Callable[..., ReadResourceOperationAction]: ...
342
+ @overload
343
+ @staticmethod
344
+ def as_dependency(
345
+ type_: Literal[ResourceOperationType.UPDATE],
346
+ *,
347
+ from_state: bool = True,
348
+ strict: Literal[True],
349
+ ) -> Callable[..., UpdateResourceOperationAction]: ...
350
+ @overload
351
+ @staticmethod
352
+ def as_dependency(
353
+ type_: Literal[ResourceOperationType.DELETE],
354
+ *,
355
+ from_state: bool = True,
356
+ strict: Literal[True],
357
+ ) -> Callable[..., DeleteResourceOperationAction]: ...
358
+ @staticmethod
359
+ def as_dependency(
360
+ type_: OptResourceOperationType = None,
361
+ *,
362
+ from_state: bool = True,
363
+ strict: bool = False,
364
+ ) -> Callable[..., AnyResourceOperationAction]:
365
+
366
+ def dependency(request: Request) -> AnyResourceOperationAction:
367
+ return ResourceOperationActionFactory.extract(
368
+ type_, request=request, from_state=from_state, strict=strict
369
+ )
370
+
371
+ return dependency
@@ -0,0 +1,8 @@
1
+ from ..enums import ResourceOperationStatusUpdateType
2
+ from .base import BaseOperationAction
3
+
4
+
5
+ class StatusUpdateOperationAction(
6
+ BaseOperationAction[ResourceOperationStatusUpdateType]
7
+ ):
8
+ pass
@@ -0,0 +1,6 @@
1
+ from ..enums import SystemOperationType
2
+ from .base import SimpleOperationAction
3
+
4
+
5
+ class SystemOperationAction(SimpleOperationAction[SystemOperationType]):
6
+ pass
@@ -0,0 +1,6 @@
1
+ from ..enums import WebSocketOperationType
2
+ from .base import SimpleOperationAction
3
+
4
+
5
+ class WebSocketOperationAction(SimpleOperationAction[WebSocketOperationType]):
6
+ pass
@@ -0,0 +1,289 @@
1
+ import os
2
+ from functools import cached_property
3
+ from google.cloud.pubsub_v1.publisher.futures import Future
4
+ from logging import Logger
5
+ from typing import Generic
6
+ from nexo.logging.enums import LogLevel
7
+ from nexo.types.boolean import BoolT, OptBool
8
+ from nexo.types.dict import (
9
+ OptStrToStrDict,
10
+ StrToAnyDict,
11
+ StrToStrDict,
12
+ )
13
+ from nexo.utils.exception import extract_details
14
+ from nexo.utils.merger import merge_dicts
15
+ from ..application import ApplicationContextMixin
16
+ from ..connection import OptConnectionContextT, ConnectionContextMixin
17
+ from ..google import ListOfPublisherHandlers
18
+ from ..error import OptAnyErrorT, ErrorMixin
19
+ from ..mixins.general import Success
20
+ from ..resource import OptResourceT, ResourceMixin
21
+ from ..response import (
22
+ OptResponseContextT,
23
+ ResponseContextMixin,
24
+ OptResponseT,
25
+ ResponseMixin,
26
+ )
27
+ from ..security.authentication import (
28
+ OptAnyAuthentication,
29
+ AuthenticationMixin,
30
+ )
31
+ from ..security.authorization import OptAnyAuthorization, AuthorizationMixin
32
+ from ..security.impersonation import OptImpersonation, ImpersonationMixin
33
+ from .action import (
34
+ ActionMixin,
35
+ ActionT,
36
+ )
37
+ from .context import ContextMixin
38
+ from .enums import OperationType as OperationTypeEnum, ListOfOperationTypes
39
+ from .mixins import Id, OperationType, Summary, TimestampMixin
40
+
41
+
42
+ class BaseOperation(
43
+ ResponseContextMixin[OptResponseContextT],
44
+ ResponseMixin[OptResponseT],
45
+ ImpersonationMixin[OptImpersonation],
46
+ AuthorizationMixin[OptAnyAuthorization],
47
+ AuthenticationMixin[OptAnyAuthentication],
48
+ ConnectionContextMixin[OptConnectionContextT],
49
+ ErrorMixin[OptAnyErrorT],
50
+ Success[BoolT],
51
+ Summary,
52
+ TimestampMixin,
53
+ ResourceMixin[OptResourceT],
54
+ ActionMixin[ActionT],
55
+ ContextMixin,
56
+ OperationType,
57
+ Id,
58
+ ApplicationContextMixin,
59
+ Generic[
60
+ ActionT,
61
+ OptResourceT,
62
+ BoolT,
63
+ OptAnyErrorT,
64
+ OptConnectionContextT,
65
+ OptResponseT,
66
+ OptResponseContextT,
67
+ ],
68
+ ):
69
+ @property
70
+ def log_message(self) -> str:
71
+ message = f"Operation {self.id} - {self.type} - "
72
+
73
+ success_information = "success" if self.success else "failed"
74
+
75
+ if self.response_context is not None:
76
+ success_information += f" {self.response_context.status_code}"
77
+
78
+ message += f"{success_information} - "
79
+
80
+ duration: float = self.timestamp.duration
81
+ if duration < 1:
82
+ duration_str = f"{int(duration * 1000)}ms"
83
+ else:
84
+ duration_str = f"{duration:.2f}s"
85
+
86
+ message += f"{duration_str} - "
87
+
88
+ if self.connection_context is not None:
89
+ message += (
90
+ f"{self.connection_context.method} {self.connection_context.url} - "
91
+ f"IP: {self.connection_context.ip_address} - "
92
+ )
93
+
94
+ if self.authentication is None:
95
+ authentication = "No Authentication"
96
+ else:
97
+ if not self.authentication.user.is_authenticated:
98
+ authentication = "Unauthenticated"
99
+ else:
100
+ authentication = (
101
+ "Authenticated | "
102
+ f"Organization: {self.authentication.user.organization} | "
103
+ f"Username: {self.authentication.user.display_name} | "
104
+ f"Email: {self.authentication.user.identity}"
105
+ )
106
+
107
+ message += f"{authentication} - "
108
+ message += self.summary
109
+
110
+ return message
111
+
112
+ @property
113
+ def labels(self) -> StrToStrDict:
114
+ labels = {
115
+ "environment": self.application_context.environment,
116
+ "service_key": self.application_context.service_key,
117
+ "instance_id": self.application_context.instance_id,
118
+ "operation_id": str(self.id),
119
+ "operation_type": self.type,
120
+ "success": str(self.success),
121
+ }
122
+
123
+ if self.connection_context is not None:
124
+ if self.connection_context.method is not None:
125
+ labels["method"] = self.connection_context.method
126
+ labels["url"] = self.connection_context.url
127
+ if self.response_context is not None:
128
+ labels["status_code"] = str(self.response_context.status_code)
129
+
130
+ return labels
131
+
132
+ def log_labels(
133
+ self,
134
+ *,
135
+ additional_labels: OptStrToStrDict = None,
136
+ override_labels: OptStrToStrDict = None,
137
+ ) -> StrToStrDict:
138
+ if override_labels is not None:
139
+ return override_labels
140
+
141
+ labels = self.labels
142
+ if additional_labels is not None:
143
+ for k, v in additional_labels.items():
144
+ if k in labels.keys():
145
+ raise ValueError(
146
+ f"Key '{k}' already exist in labels, override the labels if necessary"
147
+ )
148
+ labels[k] = v
149
+ labels = merge_dicts(labels, additional_labels)
150
+ return labels
151
+
152
+ def log_extra(
153
+ self,
154
+ *,
155
+ additional_extra: OptStrToStrDict = None,
156
+ override_extra: OptStrToStrDict = None,
157
+ additional_labels: OptStrToStrDict = None,
158
+ override_labels: OptStrToStrDict = None,
159
+ ) -> StrToAnyDict:
160
+ labels = self.log_labels(
161
+ additional_labels=additional_labels, override_labels=override_labels
162
+ )
163
+
164
+ if override_extra is not None:
165
+ extra = override_extra
166
+ else:
167
+ extra = {
168
+ "json_fields": {"operation": self.model_dump(mode="json")},
169
+ "labels": labels,
170
+ }
171
+ if additional_extra is not None:
172
+ extra = merge_dicts(extra, additional_extra)
173
+
174
+ return extra
175
+
176
+ def log(
177
+ self,
178
+ logger: Logger,
179
+ level: LogLevel,
180
+ *,
181
+ exc_info: OptBool = None,
182
+ additional_extra: OptStrToStrDict = None,
183
+ override_extra: OptStrToStrDict = None,
184
+ additional_labels: OptStrToStrDict = None,
185
+ override_labels: OptStrToStrDict = None,
186
+ ):
187
+ try:
188
+ message = self.log_message
189
+ extra = self.log_extra(
190
+ additional_extra=additional_extra,
191
+ override_extra=override_extra,
192
+ additional_labels=additional_labels,
193
+ override_labels=override_labels,
194
+ )
195
+ logger.log(
196
+ level,
197
+ message,
198
+ exc_info=exc_info,
199
+ extra=extra,
200
+ )
201
+ except Exception as e:
202
+ labels = self.log_labels(
203
+ additional_labels=additional_labels, override_labels=override_labels
204
+ )
205
+ logger.error(
206
+ f"Failed logging {self.type} Operation {self.id}",
207
+ exc_info=True,
208
+ extra={
209
+ "json_fields": {"exc_details": extract_details(e)},
210
+ "labels": labels,
211
+ },
212
+ )
213
+
214
+ @cached_property
215
+ def allowed_to_publish(self) -> bool:
216
+ # 1. Read from env (comma-separated string)
217
+ raw_value = (
218
+ os.getenv("PUBLISHABLE_OPERATIONS", "")
219
+ .strip()
220
+ .removeprefix("[")
221
+ .removesuffix("]")
222
+ .replace(" ", "")
223
+ )
224
+
225
+ # 2. Parse into list of OperationType
226
+ publishable_ops: ListOfOperationTypes = []
227
+ if raw_value:
228
+ for v in raw_value.split(","):
229
+ v = v.strip().replace('"', "").lower()
230
+ try:
231
+ publishable_ops.append(OperationTypeEnum(v))
232
+ except ValueError:
233
+ continue # ignore invalid values
234
+
235
+ # 3. Check if self.type is allowed
236
+ return self.type in publishable_ops
237
+
238
+ def publish(
239
+ self,
240
+ logger: Logger,
241
+ publishers: ListOfPublisherHandlers = [],
242
+ *,
243
+ additional_labels: OptStrToStrDict = None,
244
+ override_labels: OptStrToStrDict = None,
245
+ ) -> list[Future]:
246
+ if not self.allowed_to_publish:
247
+ return []
248
+
249
+ labels = self.log_labels(
250
+ additional_labels=additional_labels, override_labels=override_labels
251
+ )
252
+ futures: list[Future] = []
253
+ for publisher in publishers:
254
+ topic_path = publisher.client.topic_path(
255
+ publisher.project_id, publisher.topic_id
256
+ )
257
+ try:
258
+ future: Future = publisher.client.publish(
259
+ topic_path,
260
+ data=self.model_dump_json().encode(),
261
+ **self.application_context.model_dump(mode="json"),
262
+ )
263
+ message_id: str = future.result()
264
+
265
+ logger.debug(
266
+ f"Successfully published {self.type} Operation {self.id} message {message_id} to {topic_path}",
267
+ extra={
268
+ "json_fields": {
269
+ "operation": self.model_dump(mode="json"),
270
+ "message_id": message_id,
271
+ "topic_path": topic_path,
272
+ },
273
+ "labels": labels,
274
+ },
275
+ )
276
+
277
+ futures.append(future)
278
+ except Exception as e:
279
+ logger.error(
280
+ f"Failed publishing {self.type} Operation {self.id}",
281
+ exc_info=True,
282
+ extra={
283
+ "json_fields": {"exc_details": extract_details(e)},
284
+ "labels": labels,
285
+ },
286
+ )
287
+ raise
288
+
289
+ return futures
@@ -0,0 +1,18 @@
1
+ from typing import Mapping
2
+ from nexo.enums.status import DataStatus, SeqOfDataStatuses
3
+ from .enums import ResourceOperationStatusUpdateType
4
+
5
+
6
+ STATUS_UPDATE_RULES: Mapping[ResourceOperationStatusUpdateType, SeqOfDataStatuses] = {
7
+ ResourceOperationStatusUpdateType.DELETE: (DataStatus.INACTIVE, DataStatus.ACTIVE),
8
+ ResourceOperationStatusUpdateType.RESTORE: (DataStatus.DELETED,),
9
+ ResourceOperationStatusUpdateType.DEACTIVATE: (DataStatus.ACTIVE,),
10
+ ResourceOperationStatusUpdateType.ACTIVATE: (DataStatus.INACTIVE,),
11
+ }
12
+
13
+ STATUS_UPDATE_RESULT: Mapping[ResourceOperationStatusUpdateType, DataStatus] = {
14
+ ResourceOperationStatusUpdateType.DELETE: DataStatus.DELETED,
15
+ ResourceOperationStatusUpdateType.RESTORE: DataStatus.ACTIVE,
16
+ ResourceOperationStatusUpdateType.DEACTIVATE: DataStatus.INACTIVE,
17
+ ResourceOperationStatusUpdateType.ACTIVATE: DataStatus.ACTIVE,
18
+ }