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.
- nexo/schemas/__init__.py +0 -0
- nexo/schemas/application.py +292 -0
- nexo/schemas/connection.py +134 -0
- nexo/schemas/data.py +27 -0
- nexo/schemas/document.py +237 -0
- nexo/schemas/error/__init__.py +476 -0
- nexo/schemas/error/constants.py +50 -0
- nexo/schemas/error/descriptor.py +354 -0
- nexo/schemas/error/enums.py +40 -0
- nexo/schemas/error/metadata.py +15 -0
- nexo/schemas/error/spec.py +312 -0
- nexo/schemas/exception/__init__.py +0 -0
- nexo/schemas/exception/exc.py +911 -0
- nexo/schemas/exception/factory.py +1928 -0
- nexo/schemas/exception/handlers.py +110 -0
- nexo/schemas/google.py +14 -0
- nexo/schemas/key/__init__.py +0 -0
- nexo/schemas/key/rsa.py +131 -0
- nexo/schemas/metadata.py +21 -0
- nexo/schemas/mixins/__init__.py +0 -0
- nexo/schemas/mixins/filter.py +140 -0
- nexo/schemas/mixins/general.py +65 -0
- nexo/schemas/mixins/hierarchy.py +19 -0
- nexo/schemas/mixins/identity.py +387 -0
- nexo/schemas/mixins/parameter.py +50 -0
- nexo/schemas/mixins/service.py +40 -0
- nexo/schemas/mixins/sort.py +111 -0
- nexo/schemas/mixins/timestamp.py +192 -0
- nexo/schemas/model.py +240 -0
- nexo/schemas/operation/__init__.py +0 -0
- nexo/schemas/operation/action/__init__.py +9 -0
- nexo/schemas/operation/action/base.py +14 -0
- nexo/schemas/operation/action/resource.py +371 -0
- nexo/schemas/operation/action/status.py +8 -0
- nexo/schemas/operation/action/system.py +6 -0
- nexo/schemas/operation/action/websocket.py +6 -0
- nexo/schemas/operation/base.py +289 -0
- nexo/schemas/operation/constants.py +18 -0
- nexo/schemas/operation/context.py +68 -0
- nexo/schemas/operation/dependency.py +26 -0
- nexo/schemas/operation/enums.py +168 -0
- nexo/schemas/operation/extractor.py +36 -0
- nexo/schemas/operation/mixins.py +53 -0
- nexo/schemas/operation/request.py +1066 -0
- nexo/schemas/operation/resource.py +839 -0
- nexo/schemas/operation/system.py +55 -0
- nexo/schemas/operation/websocket.py +55 -0
- nexo/schemas/pagination.py +67 -0
- nexo/schemas/parameter.py +60 -0
- nexo/schemas/payload.py +116 -0
- nexo/schemas/resource.py +64 -0
- nexo/schemas/response.py +1041 -0
- nexo/schemas/security/__init__.py +0 -0
- nexo/schemas/security/api_key.py +63 -0
- nexo/schemas/security/authentication.py +848 -0
- nexo/schemas/security/authorization.py +922 -0
- nexo/schemas/security/enums.py +32 -0
- nexo/schemas/security/impersonation.py +179 -0
- nexo/schemas/security/token.py +402 -0
- nexo/schemas/security/types.py +17 -0
- nexo/schemas/success/__init__.py +0 -0
- nexo/schemas/success/descriptor.py +100 -0
- nexo/schemas/success/enums.py +23 -0
- nexo/schemas/user_agent.py +46 -0
- nexo_schemas-0.0.16.dist-info/METADATA +87 -0
- nexo_schemas-0.0.16.dist-info/RECORD +69 -0
- nexo_schemas-0.0.16.dist-info/WHEEL +5 -0
- nexo_schemas-0.0.16.dist-info/licenses/LICENSE +21 -0
- 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,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
|
+
}
|