dmart 0.1.0__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 (106) hide show
  1. alembic/__init__.py +0 -0
  2. alembic/env.py +91 -0
  3. api/__init__.py +0 -0
  4. api/info/__init__.py +0 -0
  5. api/info/router.py +109 -0
  6. api/managed/__init__.py +0 -0
  7. api/managed/router.py +1541 -0
  8. api/managed/utils.py +1850 -0
  9. api/public/__init__.py +0 -0
  10. api/public/router.py +758 -0
  11. api/qr/__init__.py +0 -0
  12. api/qr/router.py +108 -0
  13. api/user/__init__.py +0 -0
  14. api/user/router.py +1401 -0
  15. api/user/service.py +270 -0
  16. bundler.py +44 -0
  17. config/__init__.py +0 -0
  18. config/channels.json +11 -0
  19. config/notification.json +17 -0
  20. data_adapters/__init__.py +0 -0
  21. data_adapters/adapter.py +16 -0
  22. data_adapters/base_data_adapter.py +467 -0
  23. data_adapters/file/__init__.py +0 -0
  24. data_adapters/file/adapter.py +2043 -0
  25. data_adapters/file/adapter_helpers.py +1013 -0
  26. data_adapters/file/archive.py +150 -0
  27. data_adapters/file/create_index.py +331 -0
  28. data_adapters/file/create_users_folders.py +52 -0
  29. data_adapters/file/custom_validations.py +68 -0
  30. data_adapters/file/drop_index.py +40 -0
  31. data_adapters/file/health_check.py +560 -0
  32. data_adapters/file/redis_services.py +1110 -0
  33. data_adapters/helpers.py +27 -0
  34. data_adapters/sql/__init__.py +0 -0
  35. data_adapters/sql/adapter.py +3210 -0
  36. data_adapters/sql/adapter_helpers.py +491 -0
  37. data_adapters/sql/create_tables.py +451 -0
  38. data_adapters/sql/create_users_folders.py +53 -0
  39. data_adapters/sql/db_to_json_migration.py +482 -0
  40. data_adapters/sql/health_check_sql.py +232 -0
  41. data_adapters/sql/json_to_db_migration.py +454 -0
  42. data_adapters/sql/update_query_policies.py +101 -0
  43. data_generator.py +81 -0
  44. dmart-0.1.0.dist-info/METADATA +27 -0
  45. dmart-0.1.0.dist-info/RECORD +106 -0
  46. dmart-0.1.0.dist-info/WHEEL +5 -0
  47. dmart-0.1.0.dist-info/entry_points.txt +2 -0
  48. dmart-0.1.0.dist-info/top_level.txt +23 -0
  49. dmart.py +513 -0
  50. get_settings.py +7 -0
  51. languages/__init__.py +0 -0
  52. languages/arabic.json +15 -0
  53. languages/english.json +16 -0
  54. languages/kurdish.json +14 -0
  55. languages/loader.py +13 -0
  56. main.py +506 -0
  57. migrate.py +24 -0
  58. models/__init__.py +0 -0
  59. models/api.py +203 -0
  60. models/core.py +597 -0
  61. models/enums.py +255 -0
  62. password_gen.py +8 -0
  63. plugins/__init__.py +0 -0
  64. pytests/__init__.py +0 -0
  65. pytests/api_user_models_erros_test.py +16 -0
  66. pytests/api_user_models_requests_test.py +98 -0
  67. pytests/archive_test.py +72 -0
  68. pytests/base_test.py +300 -0
  69. pytests/get_settings_test.py +14 -0
  70. pytests/json_to_db_migration_test.py +237 -0
  71. pytests/service_test.py +26 -0
  72. pytests/test_info.py +55 -0
  73. pytests/test_status.py +15 -0
  74. run_notification_campaign.py +98 -0
  75. scheduled_notification_handler.py +121 -0
  76. schema_migration.py +208 -0
  77. schema_modulate.py +192 -0
  78. set_admin_passwd.py +55 -0
  79. sync.py +202 -0
  80. utils/__init__.py +0 -0
  81. utils/access_control.py +306 -0
  82. utils/async_request.py +8 -0
  83. utils/exporter.py +309 -0
  84. utils/firebase_notifier.py +57 -0
  85. utils/generate_email.py +38 -0
  86. utils/helpers.py +352 -0
  87. utils/hypercorn_config.py +12 -0
  88. utils/internal_error_code.py +60 -0
  89. utils/jwt.py +124 -0
  90. utils/logger.py +167 -0
  91. utils/middleware.py +99 -0
  92. utils/notification.py +75 -0
  93. utils/password_hashing.py +16 -0
  94. utils/plugin_manager.py +215 -0
  95. utils/query_policies_helper.py +112 -0
  96. utils/regex.py +44 -0
  97. utils/repository.py +529 -0
  98. utils/router_helper.py +19 -0
  99. utils/settings.py +165 -0
  100. utils/sms_notifier.py +21 -0
  101. utils/social_sso.py +67 -0
  102. utils/templates/activation.html.j2 +26 -0
  103. utils/templates/reminder.html.j2 +17 -0
  104. utils/ticket_sys_utils.py +203 -0
  105. utils/web_notifier.py +29 -0
  106. websocket.py +231 -0
api/managed/utils.py ADDED
@@ -0,0 +1,1850 @@
1
+ from datetime import datetime
2
+ from io import BytesIO
3
+ from typing import Any
4
+
5
+ from fastapi import status
6
+ from utils.generate_email import generate_email_from_template, generate_subject
7
+ from data_adapters.file.custom_validations import validate_csv_with_schema, validate_jsonl_with_schema
8
+ from utils.internal_error_code import InternalErrorCode
9
+ from utils.router_helper import is_space_exist
10
+ from utils.ticket_sys_utils import (
11
+ set_init_state_from_record,
12
+ set_init_state_for_record,
13
+ transite,
14
+ post_transite,
15
+ check_open_state,
16
+ )
17
+ import models.api as api
18
+ import models.core as core
19
+ from models.enums import (
20
+ ContentType,
21
+ RequestType,
22
+ ResourceType,
23
+ DataAssetType,
24
+ )
25
+ import sys
26
+ import json
27
+ from utils.access_control import access_control
28
+ import utils.repository as repository
29
+ from utils.helpers import (
30
+ camel_case,
31
+ flatten_dict,
32
+ )
33
+ from utils.settings import settings
34
+ from utils.plugin_manager import plugin_manager
35
+ from api.user.service import (
36
+ send_email,
37
+ send_sms,
38
+ )
39
+ from languages.loader import languages
40
+ from data_adapters.adapter import data_adapter as db
41
+ from pathlib import Path as FilePath
42
+ import asyncio
43
+
44
+
45
+ async def iter_bytesio(data: BytesIO, chunk_size: int = 8192):
46
+ data.seek(0)
47
+ while True:
48
+ chunk = data.read(chunk_size)
49
+ if not chunk:
50
+ break
51
+ try:
52
+ yield chunk
53
+ except (BrokenPipeError, ConnectionResetError):
54
+ return
55
+
56
+ def csv_entries_prepare_docs(query, docs_dicts, folder_views, keys_existence):
57
+ json_data = []
58
+ timestamp_fields = ["created_at", "updated_at"]
59
+ new_keys: set[str] = set()
60
+ deprecated_keys: set[str] = set()
61
+
62
+ for redis_document in docs_dicts:
63
+ rows: list[dict] = [{}]
64
+ flattened_doc = flatten_dict(redis_document)
65
+ for folder_view in folder_views:
66
+ column_key = folder_view.get("key")
67
+ column_title = folder_view.get("name")
68
+ attribute_val = flattened_doc.get(column_key)
69
+
70
+ if column_key.startswith('attachments.') and attribute_val is None:
71
+ parts = column_key.split('.')
72
+ if len(parts) >= 3:
73
+ attachment_type = parts[1]
74
+ property_name = '.'.join(parts[2:])
75
+
76
+ attachment_key = f"attachments.{attachment_type}"
77
+ attachments_array = flattened_doc.get(attachment_key)
78
+
79
+ if isinstance(attachments_array, list):
80
+ flattened_attachments = [
81
+ flatten_dict(attachment) if isinstance(attachment, dict) else attachment
82
+ for attachment in attachments_array
83
+ ]
84
+ attribute_val = [
85
+ flattened_attachment.get(property_name)
86
+ for flattened_attachment in flattened_attachments
87
+ if isinstance(flattened_attachment, dict) and flattened_attachment.get(
88
+ property_name) is not None
89
+ ]
90
+ attribute_val = [val for val in attribute_val if val is not None]
91
+
92
+ if attribute_val:
93
+ keys_existence[column_title] = True
94
+ """
95
+ Extract array items in a separate row per item
96
+ - list_new_rows = []
97
+ - for row in rows:
98
+ - for item in new_list[1:]:
99
+ - new_row = row
100
+ - add item attributes to the new_row
101
+ - list_new_rows.append(new_row)
102
+ - add new_list[0] attributes to row
103
+ -
104
+ - rows += list_new_rows
105
+ """
106
+ if isinstance(attribute_val, list) and len(attribute_val) > 0:
107
+ if isinstance(attribute_val[0], dict):
108
+ joined_values = []
109
+ for item in attribute_val:
110
+ if isinstance(item, dict):
111
+ item_values = [str(v) for v in item.values()]
112
+ joined_values.extend(item_values)
113
+ else:
114
+ joined_values.append(str(item))
115
+ new_col = "|".join(joined_values)
116
+ else:
117
+ new_col = "|".join(str(item) for item in attribute_val)
118
+
119
+ for row in rows:
120
+ row[column_title] = new_col
121
+
122
+ elif attribute_val and not isinstance(attribute_val, list):
123
+ new_col = attribute_val if column_key not in timestamp_fields else \
124
+ datetime.fromtimestamp(attribute_val).strftime(
125
+ '%Y-%m-%d %H:%M:%S')
126
+ for row in rows:
127
+ row[column_title] = new_col
128
+ json_data += rows
129
+
130
+ # Sort all entries from all schemas
131
+ if query.sort_by in core.Meta.model_fields and len(query.filter_schema_names) > 1:
132
+ json_data = sorted(
133
+ json_data,
134
+ key=lambda d: d[query.sort_by] if query.sort_by in d else "",
135
+ reverse=(query.sort_type == api.SortType.descending),
136
+ )
137
+
138
+ return json_data, deprecated_keys, new_keys
139
+
140
+
141
+ async def serve_request_create_check_access(request, record, owner_shortname):
142
+ if not await access_control.check_access(
143
+ user_shortname=owner_shortname,
144
+ space_name=request.space_name,
145
+ subpath=record.subpath,
146
+ resource_type=record.resource_type,
147
+ action_type=core.ActionType.create,
148
+ record_attributes=record.attributes,
149
+ ):
150
+ raise api.Exception(
151
+ status.HTTP_401_UNAUTHORIZED,
152
+ api.Error(
153
+ type="request",
154
+ code=InternalErrorCode.NOT_ALLOWED,
155
+ message="You don't have permission to this action [4]",
156
+ ),
157
+ )
158
+
159
+
160
+ async def send_sms_email_invitation(resource_obj, record):
161
+ # SMS Invitation
162
+ if not resource_obj.is_msisdn_verified and resource_obj.msisdn:
163
+ inv_link = await repository.store_user_invitation_token(
164
+ resource_obj, "SMS"
165
+ )
166
+ if inv_link:
167
+ await send_sms(
168
+ msisdn=record.attributes.get("msisdn", ""),
169
+ message=languages[
170
+ resource_obj.language
171
+ ]["invitation_message"].replace(
172
+ "{link}",
173
+ await repository.url_shortner(inv_link)
174
+ ),
175
+ )
176
+ # EMAIL Invitation
177
+ if not resource_obj.is_email_verified and resource_obj.email:
178
+ inv_link = await repository.store_user_invitation_token(
179
+ resource_obj, "EMAIL"
180
+ )
181
+ if inv_link:
182
+ await send_email(
183
+ from_address=settings.email_sender,
184
+ to_address=resource_obj.email,
185
+ message=generate_email_from_template(
186
+ "activation",
187
+ {
188
+ "link": await repository.url_shortner(
189
+ inv_link
190
+ ),
191
+ "name": record.attributes.get(
192
+ "displayname", {}
193
+ ).get("en", ""),
194
+ "shortname": resource_obj.shortname,
195
+ "msisdn": resource_obj.msisdn,
196
+ },
197
+ ),
198
+ subject=generate_subject("activation"),
199
+ )
200
+
201
+
202
+ def set_resource_object(record, resource_obj, is_internal):
203
+ if not is_internal or "created_at" not in record.attributes:
204
+ resource_obj.created_at = datetime.now()
205
+ resource_obj.updated_at = datetime.now()
206
+ body_shortname = record.shortname
207
+
208
+ separate_payload_data = None
209
+ if (
210
+ resource_obj.payload
211
+ and resource_obj.payload.content_type == ContentType.json
212
+ and resource_obj.payload.body is not None
213
+ ):
214
+ separate_payload_data = resource_obj.payload.body
215
+ if settings.active_data_db == 'file':
216
+ resource_obj.payload.body = body_shortname + (
217
+ ".json" if record.resource_type != ResourceType.log else ".jsonl"
218
+ )
219
+ return separate_payload_data, resource_obj
220
+
221
+
222
+ async def serve_request_create(request: api.Request, owner_shortname: str, token: str, is_internal: bool = False):
223
+ failed_records = []
224
+ records = []
225
+
226
+ async def process_record(record):
227
+ if record.subpath[0] != "/":
228
+ record.subpath = f"/{record.subpath}"
229
+ try:
230
+ if record.resource_type == ResourceType.space:
231
+ created = await serve_space_create(request, record, owner_shortname)
232
+ if created:
233
+ await db.initialize_spaces()
234
+ await access_control.load_permissions_and_roles()
235
+
236
+ await plugin_manager.after_action(
237
+ core.Event(
238
+ space_name=record.shortname,
239
+ subpath=record.subpath,
240
+ shortname=record.shortname,
241
+ action_type=core.ActionType.create,
242
+ resource_type=ResourceType.space,
243
+ user_shortname=owner_shortname,
244
+ )
245
+ )
246
+ return created.to_record(record.subpath, created.shortname, []), None
247
+
248
+ schema_shortname: str | None = None
249
+ if (
250
+ "payload" in record.attributes
251
+ and isinstance(record.attributes.get("payload", None), dict)
252
+ and "schema_shortname" in record.attributes["payload"]
253
+ ):
254
+ schema_shortname = record.attributes["payload"]["schema_shortname"]
255
+ await plugin_manager.before_action(
256
+ core.Event(
257
+ space_name=request.space_name,
258
+ subpath=record.subpath,
259
+ shortname=record.shortname,
260
+ action_type=core.ActionType.create,
261
+ schema_shortname=schema_shortname,
262
+ resource_type=record.resource_type,
263
+ user_shortname=owner_shortname,
264
+ )
265
+ )
266
+
267
+ await serve_request_create_check_access(request, record, owner_shortname)
268
+
269
+ if record.resource_type == ResourceType.ticket:
270
+ record = await set_init_state_for_record(record, request.space_name, owner_shortname)
271
+
272
+ shortname_exists = await db.is_entry_exist(
273
+ space_name=request.space_name,
274
+ subpath=record.subpath,
275
+ shortname=record.shortname,
276
+ resource_type=record.resource_type,
277
+ schema_shortname=record.attributes.get("schema_shortname", None),
278
+ )
279
+
280
+ if record.shortname != settings.auto_uuid_rule and shortname_exists:
281
+ raise api.Exception(
282
+ status.HTTP_400_BAD_REQUEST,
283
+ api.Error(
284
+ type="request",
285
+ code=InternalErrorCode.SHORTNAME_ALREADY_EXIST,
286
+ message=f"This shortname {record.shortname} already exists",
287
+ ),
288
+ )
289
+
290
+ await db.validate_uniqueness(
291
+ request.space_name, record, RequestType.create, owner_shortname
292
+ )
293
+
294
+ resource_obj = core.Meta.from_record(
295
+ record=record, owner_shortname=owner_shortname
296
+ )
297
+
298
+ separate_payload_data, resource_obj = set_resource_object(
299
+ record, resource_obj, is_internal
300
+ )
301
+
302
+ if (
303
+ resource_obj.payload
304
+ and resource_obj.payload.content_type == ContentType.json
305
+ and resource_obj.payload.schema_shortname
306
+ and isinstance(separate_payload_data, dict)
307
+ ):
308
+ await db.validate_payload_with_schema(
309
+ payload_data=separate_payload_data,
310
+ space_name=request.space_name,
311
+ schema_shortname=resource_obj.payload.schema_shortname,
312
+ )
313
+
314
+ await db.save(
315
+ request.space_name,
316
+ record.subpath,
317
+ resource_obj,
318
+ )
319
+
320
+ if isinstance(resource_obj, core.User):
321
+ await send_sms_email_invitation(resource_obj, record)
322
+
323
+ if separate_payload_data is not None and isinstance(
324
+ separate_payload_data, dict
325
+ ):
326
+ await db.update_payload(
327
+ request.space_name,
328
+ record.subpath,
329
+ resource_obj,
330
+ separate_payload_data,
331
+ owner_shortname,
332
+ )
333
+
334
+ rec = resource_obj.to_record(
335
+ record.subpath,
336
+ resource_obj.shortname,
337
+ [],
338
+ )
339
+ record.attributes["logged_in_user_token"] = token
340
+ await plugin_manager.after_action(
341
+ core.Event(
342
+ space_name=request.space_name,
343
+ subpath=record.subpath,
344
+ shortname=resource_obj.shortname,
345
+ action_type=core.ActionType.create,
346
+ schema_shortname=(
347
+ record.attributes["payload"].get("schema_shortname", None)
348
+ if record.attributes.get("payload")
349
+ else None
350
+ ),
351
+ resource_type=record.resource_type,
352
+ user_shortname=owner_shortname,
353
+ attributes=record.attributes,
354
+ )
355
+ )
356
+ return rec, None
357
+ except api.Exception as e:
358
+ return None, {
359
+ "record": record,
360
+ "error": e.error.message,
361
+ "error_code": e.error.code,
362
+ }
363
+
364
+ results = await asyncio.gather(*(process_record(r) for r in request.records))
365
+ for rec, failed in results:
366
+ if rec is not None:
367
+ records.append(rec)
368
+ if failed is not None:
369
+ failed_records.append(failed)
370
+
371
+ return records, failed_records
372
+
373
+
374
+ async def serve_request_update_fetch_payload(
375
+ old_resource_obj, record, request, resource_cls, schema_shortname
376
+ ):
377
+ old_resource_payload_body : dict[str, Any] = {}
378
+ old_version_flattend = flatten_dict(
379
+ old_resource_obj.model_dump()
380
+ )
381
+ if (
382
+ record.resource_type != ResourceType.log
383
+ and old_resource_obj.payload
384
+ and old_resource_obj.payload.content_type == ContentType.json
385
+ ):
386
+ try:
387
+ if isinstance(old_resource_obj.payload.body, str):
388
+ mybody = await db.load_resource_payload(
389
+ space_name=request.space_name,
390
+ subpath=record.subpath,
391
+ filename=old_resource_obj.payload.body,
392
+ class_type=resource_cls,
393
+ schema_shortname=schema_shortname,
394
+ )
395
+ else:
396
+ mybody = old_resource_obj.payload.body
397
+ old_resource_payload_body = mybody if mybody else {}
398
+ except api.Exception as e:
399
+ if request.request_type == api.RequestType.update:
400
+ raise e
401
+
402
+ old_version_flattend.pop("payload.body", None)
403
+ old_version_flattend.update(
404
+ flatten_dict(
405
+ {"payload.body": old_resource_payload_body}
406
+ )
407
+ )
408
+
409
+ return old_version_flattend, old_resource_payload_body
410
+
411
+
412
+ async def serve_request_update(request, owner_shortname: str):
413
+ records: list[core.Record] = []
414
+ failed_records: list[dict] = []
415
+
416
+ async def process_record(record):
417
+ try:
418
+ if record.subpath[0] != "/":
419
+ record.subpath = f"/{record.subpath}"
420
+
421
+ if record.resource_type == ResourceType.space:
422
+ history_diff = await serve_space_update(
423
+ request,
424
+ record,
425
+ owner_shortname,
426
+ )
427
+
428
+ await db.initialize_spaces()
429
+ await access_control.load_permissions_and_roles()
430
+ await plugin_manager.after_action(
431
+ core.Event(
432
+ space_name=record.shortname,
433
+ subpath=record.subpath,
434
+ shortname=record.shortname,
435
+ action_type=core.ActionType.update,
436
+ resource_type=ResourceType.space,
437
+ user_shortname=owner_shortname,
438
+ attributes={"history_diff": history_diff},
439
+ )
440
+ )
441
+ return record, None
442
+
443
+ record_schema_shortname = record.attributes.get("payload", {}).get(
444
+ "schema_shortname", None
445
+ ) if record.attributes.get("payload", {}) is not None else None
446
+ await plugin_manager.before_action(
447
+ core.Event(
448
+ space_name=request.space_name,
449
+ subpath=record.subpath,
450
+ shortname=record.shortname,
451
+ schema_shortname=record_schema_shortname,
452
+ action_type=core.ActionType.update,
453
+ resource_type=record.resource_type,
454
+ user_shortname=owner_shortname,
455
+ )
456
+ )
457
+
458
+ resource_cls = getattr(
459
+ sys.modules["models.core"], camel_case(
460
+ record.resource_type
461
+ )
462
+ )
463
+ old_resource_obj = await db.load(
464
+ space_name=request.space_name,
465
+ subpath=record.subpath,
466
+ shortname=record.shortname,
467
+ class_type=resource_cls,
468
+ user_shortname=owner_shortname,
469
+ schema_shortname=record_schema_shortname,
470
+ )
471
+
472
+ requested_checksum = record.attributes.get("last_checksum_history")
473
+ if requested_checksum:
474
+ latest_history = await db.get_latest_history(
475
+ space_name=request.space_name,
476
+ subpath=record.subpath,
477
+ shortname=record.shortname,
478
+ )
479
+ if latest_history and latest_history.last_checksum_history != requested_checksum:
480
+ raise api.Exception(
481
+ status.HTTP_409_CONFLICT,
482
+ api.Error(
483
+ type="request",
484
+ code=InternalErrorCode.CONFLICT,
485
+ message="Resource has been updated by another request!",
486
+ ),
487
+ )
488
+
489
+ # CHECK PERMISSION
490
+ if not await access_control.check_access(
491
+ user_shortname=owner_shortname,
492
+ space_name=request.space_name,
493
+ subpath=record.subpath,
494
+ resource_type=record.resource_type,
495
+ action_type=core.ActionType.update,
496
+ resource_is_active=old_resource_obj.is_active,
497
+ resource_owner_shortname=old_resource_obj.owner_shortname,
498
+ resource_owner_group=old_resource_obj.owner_group_shortname,
499
+ record_attributes=record.attributes,
500
+ entry_shortname=record.shortname
501
+ ):
502
+ raise api.Exception(
503
+ status.HTTP_401_UNAUTHORIZED,
504
+ api.Error(
505
+ type="request",
506
+ code=InternalErrorCode.NOT_ALLOWED,
507
+ message="You don't have permission to this action [5]",
508
+ ),
509
+ )
510
+
511
+ # GET PAYLOAD DATA
512
+ old_version_flattend, old_resource_payload_body = await serve_request_update_fetch_payload(
513
+ old_resource_obj, record, request, resource_cls, record_schema_shortname
514
+ )
515
+
516
+ # GENERATE NEW RESOURCE OBJECT
517
+ resource_obj = old_resource_obj
518
+ resource_obj.updated_at = datetime.now()
519
+
520
+ new_version_flattend = {}
521
+
522
+ if record.resource_type == ResourceType.log:
523
+ if payload := record.attributes.get("payload", {}):
524
+ new_resource_payload_data = payload.get("body", {})
525
+ else:
526
+ new_resource_payload_data = None
527
+ else:
528
+ new_resource_payload_data = resource_obj.update_from_record(
529
+ record=record,
530
+ old_body=old_resource_payload_body,
531
+ )
532
+
533
+ new_version_flattend = resource_obj.model_dump()
534
+ if new_resource_payload_data:
535
+ new_version_flattend["payload"] = {
536
+ **new_version_flattend["payload"],
537
+ "body": new_resource_payload_data
538
+ }
539
+ new_version_flattend = flatten_dict(new_version_flattend)
540
+
541
+ await db.validate_uniqueness(
542
+ request.space_name, record, RequestType.update, owner_shortname
543
+ )
544
+ # VALIDATE SEPARATE PAYLOAD BODY
545
+ if (
546
+ resource_obj.payload
547
+ and resource_obj.payload.content_type == ContentType.json
548
+ and resource_obj.payload.schema_shortname
549
+ and new_resource_payload_data is not None
550
+ ):
551
+ await db.validate_payload_with_schema(
552
+ payload_data=new_resource_payload_data,
553
+ space_name=request.space_name,
554
+ schema_shortname=resource_obj.payload.schema_shortname,
555
+ )
556
+
557
+ if record.resource_type == ResourceType.log:
558
+ history_diff = await db.update(
559
+ space_name=request.space_name,
560
+ subpath=record.subpath,
561
+ meta=resource_obj,
562
+ old_version_flattend={},
563
+ new_version_flattend={},
564
+ updated_attributes_flattend=[],
565
+ user_shortname=owner_shortname,
566
+ schema_shortname=record_schema_shortname,
567
+ retrieve_lock_status=record.retrieve_lock_status,
568
+ )
569
+ else:
570
+ updated_attributes_flattend = list(
571
+ flatten_dict(record.attributes).keys()
572
+ )
573
+
574
+ if (settings.active_data_db == 'sql'
575
+ and new_resource_payload_data is not None
576
+ and resource_obj.payload
577
+ and resource_obj.payload.content_type == ContentType.json):
578
+ resource_obj.payload.body = new_resource_payload_data
579
+
580
+ history_diff = await db.update(
581
+ space_name=request.space_name,
582
+ subpath=record.subpath,
583
+ meta=resource_obj,
584
+ old_version_flattend=old_version_flattend,
585
+ new_version_flattend=new_version_flattend,
586
+ updated_attributes_flattend=updated_attributes_flattend,
587
+ user_shortname=owner_shortname,
588
+ schema_shortname=record_schema_shortname,
589
+ retrieve_lock_status=record.retrieve_lock_status,
590
+ )
591
+
592
+ if new_resource_payload_data is not None:
593
+ await db.save_payload_from_json(
594
+ request.space_name,
595
+ record.subpath,
596
+ resource_obj,
597
+ new_resource_payload_data,
598
+ )
599
+
600
+ if (
601
+ isinstance(resource_obj, core.User) and
602
+ record.attributes.get("is_active", None) is not None
603
+ ):
604
+ if not record.attributes.get("is_active"):
605
+ await db.remove_user_session(record.shortname)
606
+
607
+ rec = resource_obj.to_record(
608
+ record.subpath, resource_obj.shortname, []
609
+ )
610
+
611
+ await plugin_manager.after_action(
612
+ core.Event(
613
+ space_name=request.space_name,
614
+ subpath=record.subpath,
615
+ shortname=record.shortname,
616
+ schema_shortname=record_schema_shortname,
617
+ action_type=core.ActionType.update,
618
+ resource_type=record.resource_type,
619
+ user_shortname=owner_shortname,
620
+ attributes={"history_diff": history_diff},
621
+ )
622
+ )
623
+ return rec, None
624
+ except api.Exception as e:
625
+ return None, {
626
+ "record": record,
627
+ "error": e.error.message,
628
+ "error_code": e.error.code,
629
+ }
630
+
631
+ results = await asyncio.gather(*(process_record(r) for r in request.records))
632
+ for rec, failed in results:
633
+ if rec is not None:
634
+ records.append(rec)
635
+ if failed is not None:
636
+ failed_records.append(failed)
637
+ return records, failed_records
638
+
639
+
640
+ async def serve_request_patch(request, owner_shortname: str):
641
+ records: list[core.Record] = []
642
+ failed_records: list[dict] = []
643
+
644
+ async def process_record(record):
645
+ try:
646
+ if record.subpath[0] != "/":
647
+ record.subpath = f"/{record.subpath}"
648
+
649
+ await plugin_manager.before_action(
650
+ core.Event(
651
+ space_name=request.space_name,
652
+ subpath=record.subpath,
653
+ shortname=record.shortname,
654
+ schema_shortname=record.attributes.get("payload", {}).get(
655
+ "schema_shortname", None
656
+ ),
657
+ action_type=core.ActionType.update,
658
+ resource_type=record.resource_type,
659
+ user_shortname=owner_shortname,
660
+ )
661
+ )
662
+
663
+ resource_cls = getattr(
664
+ sys.modules["models.core"], camel_case(
665
+ record.resource_type
666
+ )
667
+ )
668
+ schema_shortname = record.attributes.get("payload", {}).get(
669
+ "schema_shortname"
670
+ )
671
+ old_resource_obj = await db.load(
672
+ space_name=request.space_name,
673
+ subpath=record.subpath,
674
+ shortname=record.shortname,
675
+ class_type=resource_cls,
676
+ user_shortname=owner_shortname,
677
+ schema_shortname=schema_shortname,
678
+ )
679
+
680
+ # CHECK PERMISSION
681
+ if not await access_control.check_access(
682
+ user_shortname=owner_shortname,
683
+ space_name=request.space_name,
684
+ subpath=record.subpath,
685
+ resource_type=record.resource_type,
686
+ action_type=core.ActionType.update,
687
+ resource_is_active=old_resource_obj.is_active,
688
+ resource_owner_shortname=old_resource_obj.owner_shortname,
689
+ resource_owner_group=old_resource_obj.owner_group_shortname,
690
+ record_attributes=record.attributes,
691
+ entry_shortname=record.shortname
692
+ ):
693
+ raise api.Exception(
694
+ status.HTTP_401_UNAUTHORIZED,
695
+ api.Error(
696
+ type="request",
697
+ code=InternalErrorCode.NOT_ALLOWED,
698
+ message="You don't have permission to this action [8]",
699
+ ),
700
+ )
701
+
702
+ # GET PAYLOAD DATA
703
+ old_version_flattend, old_resource_payload_body = await serve_request_update_fetch_payload(
704
+ old_resource_obj, record, request, resource_cls, schema_shortname
705
+ )
706
+
707
+ # GENERATE NEW RESOURCE OBJECT
708
+ resource_obj = old_resource_obj
709
+ resource_obj.updated_at = datetime.now()
710
+
711
+ new_version_flattend = {}
712
+
713
+ if record.resource_type == ResourceType.log:
714
+ new_resource_payload_data = record.attributes.get("payload", {}).get(
715
+ "body", {}
716
+ )
717
+ else:
718
+ new_resource_payload_data = (
719
+ resource_obj.update_from_record(
720
+ record=record,
721
+ old_body=old_resource_payload_body,
722
+ )
723
+ )
724
+ new_version_flattend = resource_obj.model_dump()
725
+ if new_resource_payload_data:
726
+ new_version_flattend["payload"] = {
727
+ **new_version_flattend["payload"],
728
+ "body": new_resource_payload_data
729
+ }
730
+ new_version_flattend = flatten_dict(new_version_flattend)
731
+
732
+ await db.validate_uniqueness(
733
+ request.space_name, record, RequestType.update, owner_shortname
734
+ )
735
+
736
+ if record.resource_type == ResourceType.log:
737
+ history_diff = await db.update(
738
+ space_name=request.space_name,
739
+ subpath=record.subpath,
740
+ meta=resource_obj,
741
+ old_version_flattend={},
742
+ new_version_flattend={},
743
+ updated_attributes_flattend=[],
744
+ user_shortname=owner_shortname,
745
+ schema_shortname=schema_shortname,
746
+ retrieve_lock_status=record.retrieve_lock_status,
747
+ )
748
+ else:
749
+ updated_attributes_flattend = list(
750
+ flatten_dict(record.attributes).keys()
751
+ )
752
+
753
+ # VALIDATE SEPARATE PAYLOAD BODY
754
+ if (
755
+ resource_obj.payload
756
+ and resource_obj.payload.content_type == ContentType.json
757
+ and resource_obj.payload.schema_shortname
758
+ and new_resource_payload_data is not None
759
+ ):
760
+ await db.validate_payload_with_schema(
761
+ payload_data=new_resource_payload_data,
762
+ space_name=request.space_name,
763
+ schema_shortname=resource_obj.payload.schema_shortname,
764
+ )
765
+
766
+ history_diff = await db.update(
767
+ space_name=request.space_name,
768
+ subpath=record.subpath,
769
+ meta=resource_obj,
770
+ old_version_flattend=old_version_flattend,
771
+ new_version_flattend=new_version_flattend,
772
+ updated_attributes_flattend=updated_attributes_flattend,
773
+ user_shortname=owner_shortname,
774
+ schema_shortname=schema_shortname,
775
+ retrieve_lock_status=record.retrieve_lock_status,
776
+ )
777
+
778
+ if new_resource_payload_data is not None:
779
+ await db.save_payload_from_json(
780
+ request.space_name,
781
+ record.subpath,
782
+ resource_obj,
783
+ new_resource_payload_data,
784
+ )
785
+
786
+ if (
787
+ isinstance(resource_obj, core.User) and
788
+ record.attributes.get("is_active") is False
789
+ ):
790
+ await db.remove_user_session(record.shortname)
791
+ if resource_obj.payload and new_resource_payload_data:
792
+ resource_obj.payload.body = new_resource_payload_data
793
+ rec = resource_obj.to_record(
794
+ record.subpath, resource_obj.shortname, []
795
+ )
796
+
797
+ await plugin_manager.after_action(
798
+ core.Event(
799
+ space_name=request.space_name,
800
+ subpath=record.subpath,
801
+ shortname=record.shortname,
802
+ schema_shortname=record.attributes.get("payload", {}).get(
803
+ "schema_shortname", None
804
+ ),
805
+ action_type=core.ActionType.update,
806
+ resource_type=record.resource_type,
807
+ user_shortname=owner_shortname,
808
+ attributes={"history_diff": history_diff},
809
+ )
810
+ )
811
+ return rec, None
812
+ except api.Exception as e:
813
+ return None, {
814
+ "record": record,
815
+ "error": e.error.message,
816
+ "error_code": e.error.code,
817
+ }
818
+
819
+ results = await asyncio.gather(*(process_record(r) for r in request.records))
820
+ for rec, failed in results:
821
+ if rec is not None:
822
+ records.append(rec)
823
+ if failed is not None:
824
+ failed_records.append(failed)
825
+ return records, failed_records
826
+
827
+
828
+ async def serve_request_assign(request, owner_shortname: str):
829
+ records: list[core.Record] = []
830
+ failed_records: list[dict] = []
831
+
832
+ async def process_record(record):
833
+ try:
834
+ if not record.attributes.get("owner_shortname"):
835
+ raise api.Exception(
836
+ status.HTTP_400_BAD_REQUEST,
837
+ api.Error(
838
+ type="request",
839
+ code=InternalErrorCode.MISSING_DATA,
840
+ message="The owner_shortname is required",
841
+ ),
842
+ )
843
+ _target_user = await db.load(
844
+ space_name=settings.management_space,
845
+ subpath=settings.users_subpath,
846
+ shortname=record.attributes["owner_shortname"],
847
+ class_type=core.User,
848
+ )
849
+
850
+ if record.subpath[0] != "/":
851
+ record.subpath = f"/{record.subpath}"
852
+ await plugin_manager.before_action(
853
+ core.Event(
854
+ space_name=request.space_name,
855
+ subpath=record.subpath,
856
+ shortname=record.shortname,
857
+ schema_shortname=record.attributes.get("payload", {}).get(
858
+ "schema_shortname", None
859
+ ),
860
+ action_type=core.ActionType.update,
861
+ resource_type=record.resource_type,
862
+ user_shortname=owner_shortname,
863
+ )
864
+ )
865
+
866
+ resource_cls = getattr(
867
+ sys.modules["models.core"], camel_case(
868
+ record.resource_type)
869
+ )
870
+ schema_shortname = record.attributes.get("payload", {}).get(
871
+ "schema_shortname"
872
+ )
873
+ resource_obj = await db.load(
874
+ space_name=request.space_name,
875
+ subpath=record.subpath,
876
+ shortname=record.shortname,
877
+ class_type=resource_cls,
878
+ user_shortname=owner_shortname,
879
+ schema_shortname=schema_shortname,
880
+ )
881
+
882
+ # CHECK PERMISSION
883
+ if not await access_control.check_access(
884
+ user_shortname=owner_shortname,
885
+ space_name=request.space_name,
886
+ subpath=record.subpath,
887
+ resource_type=record.resource_type,
888
+ action_type=core.ActionType.assign,
889
+ resource_is_active=resource_obj.is_active,
890
+ resource_owner_shortname=resource_obj.owner_shortname,
891
+ resource_owner_group=resource_obj.owner_group_shortname,
892
+ record_attributes=record.attributes,
893
+ entry_shortname=record.shortname
894
+ ):
895
+ raise api.Exception(
896
+ status.HTTP_401_UNAUTHORIZED,
897
+ api.Error(
898
+ type="request",
899
+ code=InternalErrorCode.NOT_ALLOWED,
900
+ message="You don't have permission to this action [25]",
901
+ ),
902
+ )
903
+
904
+ old_version_flattend = flatten_dict(resource_obj.model_dump())
905
+
906
+ resource_obj.updated_at = datetime.now()
907
+ resource_obj.owner_shortname = record.attributes["owner_shortname"]
908
+
909
+ history_diff = await db.update(
910
+ space_name=request.space_name,
911
+ subpath=record.subpath,
912
+ meta=resource_obj,
913
+ old_version_flattend=old_version_flattend,
914
+ new_version_flattend=flatten_dict(resource_obj.model_dump()),
915
+ updated_attributes_flattend=["owner_shortname"],
916
+ user_shortname=owner_shortname,
917
+ schema_shortname=schema_shortname,
918
+ retrieve_lock_status=record.retrieve_lock_status,
919
+ )
920
+
921
+ rec = resource_obj.to_record(
922
+ record.subpath, resource_obj.shortname, []
923
+ )
924
+
925
+ await plugin_manager.after_action(
926
+ core.Event(
927
+ space_name=request.space_name,
928
+ subpath=record.subpath,
929
+ shortname=record.shortname,
930
+ schema_shortname=record.attributes.get("payload", {}).get(
931
+ "schema_shortname", None
932
+ ),
933
+ action_type=core.ActionType.update,
934
+ resource_type=record.resource_type,
935
+ user_shortname=owner_shortname,
936
+ attributes={"history_diff": history_diff},
937
+ )
938
+ )
939
+ return rec, None
940
+ except api.Exception as e:
941
+ return None, {
942
+ "record": record,
943
+ "error": e.error.message,
944
+ "error_code": e.error.code,
945
+ }
946
+
947
+ results = await asyncio.gather(*(process_record(r) for r in request.records))
948
+ for rec, failed in results:
949
+ if rec is not None:
950
+ records.append(rec)
951
+ if failed is not None:
952
+ failed_records.append(failed)
953
+
954
+ return records, failed_records
955
+
956
+
957
+ async def serve_request_update_acl(request, owner_shortname: str):
958
+ records: list[core.Record] = []
959
+ failed_records: list[dict] = []
960
+
961
+ async def process_record(record):
962
+ try:
963
+ if record.attributes.get("acl", None) is None:
964
+ raise api.Exception(
965
+ status.HTTP_400_BAD_REQUEST,
966
+ api.Error(
967
+ type="request",
968
+ code=InternalErrorCode.MISSING_DATA,
969
+ message="The acl is required",
970
+ ),
971
+ )
972
+
973
+ if record.subpath[0] != "/":
974
+ record.subpath = f"/{record.subpath}"
975
+ await plugin_manager.before_action(
976
+ core.Event(
977
+ space_name=request.space_name,
978
+ subpath=record.subpath,
979
+ shortname=record.shortname,
980
+ schema_shortname=record.attributes.get("payload", {}).get(
981
+ "schema_shortname", None
982
+ ),
983
+ action_type=core.ActionType.update,
984
+ resource_type=record.resource_type,
985
+ user_shortname=owner_shortname,
986
+ )
987
+ )
988
+
989
+ resource_cls = getattr(
990
+ sys.modules["models.core"], camel_case(
991
+ record.resource_type)
992
+ )
993
+ schema_shortname = record.attributes.get("payload", {}).get(
994
+ "schema_shortname"
995
+ )
996
+ resource_obj = await db.load(
997
+ space_name=request.space_name,
998
+ subpath=record.subpath,
999
+ shortname=record.shortname,
1000
+ class_type=resource_cls,
1001
+ user_shortname=owner_shortname,
1002
+ schema_shortname=schema_shortname,
1003
+ )
1004
+
1005
+ # CHECK PERMISSION
1006
+ if not await access_control.check_access(
1007
+ user_shortname=owner_shortname,
1008
+ space_name=request.space_name,
1009
+ subpath=record.subpath,
1010
+ resource_type=record.resource_type,
1011
+ action_type=core.ActionType.update,
1012
+ resource_is_active=resource_obj.is_active,
1013
+ resource_owner_shortname=resource_obj.owner_shortname,
1014
+ resource_owner_group=resource_obj.owner_group_shortname,
1015
+ record_attributes=record.attributes,
1016
+ entry_shortname=record.shortname
1017
+ ):
1018
+ raise api.Exception(
1019
+ status.HTTP_401_UNAUTHORIZED,
1020
+ api.Error(
1021
+ type="request",
1022
+ code=InternalErrorCode.NOT_ALLOWED,
1023
+ message="You don't have permission to this action [26]",
1024
+ ),
1025
+ )
1026
+
1027
+ old_version_flattend = flatten_dict(resource_obj.model_dump())
1028
+
1029
+ resource_obj.updated_at = datetime.now()
1030
+ resource_obj.acl = record.attributes["acl"]
1031
+
1032
+ history_diff = await db.update(
1033
+ space_name=request.space_name,
1034
+ subpath=record.subpath,
1035
+ meta=resource_obj,
1036
+ old_version_flattend=old_version_flattend,
1037
+ new_version_flattend=flatten_dict(resource_obj.model_dump()),
1038
+ updated_attributes_flattend=["acl"],
1039
+ user_shortname=owner_shortname,
1040
+ schema_shortname=schema_shortname,
1041
+ retrieve_lock_status=record.retrieve_lock_status,
1042
+ )
1043
+
1044
+ rec = resource_obj.to_record(
1045
+ record.subpath, resource_obj.shortname, []
1046
+ )
1047
+
1048
+ await plugin_manager.after_action(
1049
+ core.Event(
1050
+ space_name=request.space_name,
1051
+ subpath=record.subpath,
1052
+ shortname=record.shortname,
1053
+ schema_shortname=record.attributes.get("payload", {}).get(
1054
+ "schema_shortname", None
1055
+ ),
1056
+ action_type=core.ActionType.update,
1057
+ resource_type=record.resource_type,
1058
+ user_shortname=owner_shortname,
1059
+ attributes={"history_diff": history_diff},
1060
+ )
1061
+ )
1062
+ return rec, None
1063
+ except api.Exception as e:
1064
+ return None, {
1065
+ "record": record,
1066
+ "error": e.error.message,
1067
+ "error_code": e.error.code,
1068
+ }
1069
+
1070
+ results = await asyncio.gather(*(process_record(r) for r in request.records))
1071
+ for rec, failed in results:
1072
+ if rec is not None:
1073
+ records.append(rec)
1074
+ if failed is not None:
1075
+ failed_records.append(failed)
1076
+ return records, failed_records
1077
+
1078
+
1079
+ async def serve_request_delete(request, owner_shortname: str):
1080
+ records: list[core.Record] = []
1081
+ failed_records: list[dict] = []
1082
+
1083
+ async def process_record(record):
1084
+ try:
1085
+ if record.subpath[0] != "/":
1086
+ record.subpath = f"/{record.subpath}"
1087
+
1088
+ if record.resource_type == ResourceType.space:
1089
+ await serve_space_delete(request, record, owner_shortname)
1090
+ await db.initialize_spaces()
1091
+ await access_control.load_permissions_and_roles()
1092
+ await plugin_manager.after_action(
1093
+ core.Event(
1094
+ space_name=record.shortname,
1095
+ subpath=record.subpath,
1096
+ shortname=record.shortname,
1097
+ action_type=core.ActionType.delete,
1098
+ resource_type=ResourceType.space,
1099
+ user_shortname=owner_shortname,
1100
+ )
1101
+ )
1102
+ return record, None
1103
+
1104
+ await plugin_manager.before_action(
1105
+ core.Event(
1106
+ space_name=request.space_name,
1107
+ subpath=record.subpath,
1108
+ shortname=record.shortname,
1109
+ action_type=core.ActionType.delete,
1110
+ resource_type=record.resource_type,
1111
+ user_shortname=owner_shortname,
1112
+ )
1113
+ )
1114
+
1115
+ resource_cls = getattr(
1116
+ sys.modules["models.core"], camel_case(
1117
+ record.resource_type)
1118
+ )
1119
+ schema_shortname = record.attributes.get("payload", {}).get(
1120
+ "schema_shortname"
1121
+ )
1122
+ resource_obj = await db.load(
1123
+ space_name=request.space_name,
1124
+ subpath=record.subpath,
1125
+ shortname=record.shortname,
1126
+ class_type=resource_cls,
1127
+ user_shortname=owner_shortname,
1128
+ schema_shortname=schema_shortname,
1129
+ )
1130
+ if not await access_control.check_access(
1131
+ user_shortname=owner_shortname,
1132
+ space_name=request.space_name,
1133
+ subpath=record.subpath,
1134
+ resource_type=record.resource_type,
1135
+ action_type=core.ActionType.delete,
1136
+ resource_is_active=resource_obj.is_active,
1137
+ resource_owner_shortname=resource_obj.owner_shortname,
1138
+ resource_owner_group=resource_obj.owner_group_shortname,
1139
+ entry_shortname=record.shortname
1140
+ ):
1141
+ raise api.Exception(
1142
+ status.HTTP_401_UNAUTHORIZED,
1143
+ api.Error(
1144
+ type="request",
1145
+ code=InternalErrorCode.NOT_ALLOWED,
1146
+ message="You don't have permission to this action [6]",
1147
+ ),
1148
+ )
1149
+ try:
1150
+ await db.delete(
1151
+ space_name=request.space_name,
1152
+ subpath=record.subpath,
1153
+ meta=resource_obj,
1154
+ user_shortname=owner_shortname,
1155
+ schema_shortname=schema_shortname,
1156
+ retrieve_lock_status=record.retrieve_lock_status,
1157
+ )
1158
+ except api.Exception as e:
1159
+ return None, {
1160
+ "record": record,
1161
+ "error": e.error.message,
1162
+ "error_code": e.error.code,
1163
+ }
1164
+
1165
+ await plugin_manager.after_action(
1166
+ core.Event(
1167
+ space_name=request.space_name,
1168
+ subpath=record.subpath,
1169
+ shortname=record.shortname,
1170
+ action_type=core.ActionType.delete,
1171
+ resource_type=record.resource_type,
1172
+ user_shortname=owner_shortname,
1173
+ attributes={"entry": resource_obj},
1174
+ )
1175
+ )
1176
+
1177
+ return record, None
1178
+ except api.Exception as e:
1179
+ return None, {
1180
+ "record": record,
1181
+ "error": e.error.message,
1182
+ "error_code": e.error.code,
1183
+ }
1184
+
1185
+ results = await asyncio.gather(*(process_record(r) for r in request.records))
1186
+ for rec, failed in results:
1187
+ if rec is not None:
1188
+ records.append(rec)
1189
+ if failed is not None:
1190
+ failed_records.append(failed)
1191
+
1192
+ return records, failed_records
1193
+
1194
+
1195
+ async def serve_request_move(request, owner_shortname: str):
1196
+ records: list[core.Record] = []
1197
+ failed_records: list[dict] = []
1198
+
1199
+ async def process_record(record):
1200
+ try:
1201
+ if record.subpath[0] != "/":
1202
+ record.subpath = f"/{record.subpath}"
1203
+
1204
+ if (
1205
+ not record.attributes.get("src_space_name")
1206
+ or not record.attributes.get("src_subpath")
1207
+ or not record.attributes.get("src_shortname")
1208
+ or not record.attributes.get("dest_space_name")
1209
+ or not record.attributes.get("dest_subpath")
1210
+ or not record.attributes.get("dest_shortname")
1211
+ ):
1212
+ raise api.Exception(
1213
+ status.HTTP_400_BAD_REQUEST,
1214
+ api.Error(
1215
+ type="move",
1216
+ code=InternalErrorCode.PROVID_SOURCE_PATH,
1217
+ message="Please provide a source and destination path and a src shortname",
1218
+ ),
1219
+ )
1220
+
1221
+ await plugin_manager.before_action(
1222
+ core.Event(
1223
+ space_name=request.space_name,
1224
+ subpath=record.attributes["src_subpath"],
1225
+ shortname=record.attributes["src_shortname"],
1226
+ action_type=core.ActionType.move,
1227
+ resource_type=record.resource_type,
1228
+ user_shortname=owner_shortname,
1229
+ attributes={
1230
+ "dest_space_name": record.attributes["dest_space_name"],
1231
+ "dest_subpath": record.attributes["dest_subpath"]
1232
+ },
1233
+ )
1234
+ )
1235
+
1236
+ resource_cls = getattr(
1237
+ sys.modules["models.core"], camel_case(
1238
+ record.resource_type)
1239
+ )
1240
+ resource_obj = await db.load(
1241
+ space_name=request.space_name,
1242
+ subpath=record.attributes["src_subpath"],
1243
+ shortname=record.attributes["src_shortname"],
1244
+ class_type=resource_cls,
1245
+ user_shortname=owner_shortname,
1246
+ )
1247
+ check_src_subpath = await access_control.check_access(
1248
+ user_shortname=owner_shortname,
1249
+ space_name=request.space_name,
1250
+ subpath=record.attributes["src_subpath"],
1251
+ resource_type=record.resource_type,
1252
+ action_type=core.ActionType.delete,
1253
+ resource_is_active=resource_obj.is_active,
1254
+ resource_owner_shortname=resource_obj.owner_shortname,
1255
+ resource_owner_group=resource_obj.owner_group_shortname,
1256
+ entry_shortname=record.shortname
1257
+ )
1258
+ check_dest_subpath = await access_control.check_access(
1259
+ user_shortname=owner_shortname,
1260
+ space_name=request.space_name,
1261
+ subpath=record.attributes["dest_subpath"],
1262
+ resource_type=record.resource_type,
1263
+ action_type=core.ActionType.create,
1264
+ )
1265
+ if not check_src_subpath or not check_dest_subpath:
1266
+ raise api.Exception(
1267
+ status.HTTP_401_UNAUTHORIZED,
1268
+ api.Error(
1269
+ type="request",
1270
+ code=InternalErrorCode.NOT_ALLOWED,
1271
+ message="You don't have permission to this action [7]",
1272
+ ),
1273
+ )
1274
+
1275
+ try:
1276
+ await db.move(
1277
+ record.attributes["src_space_name"],
1278
+ record.attributes["src_subpath"],
1279
+ record.attributes["src_shortname"],
1280
+ record.attributes["dest_space_name"],
1281
+ record.attributes["dest_subpath"],
1282
+ record.attributes["dest_shortname"],
1283
+ resource_obj,
1284
+ )
1285
+ except api.Exception as e:
1286
+ return None, {
1287
+ "record": record,
1288
+ "error": e.error.message,
1289
+ "error_code": e.error.code,
1290
+ }
1291
+
1292
+ await plugin_manager.after_action(
1293
+ core.Event(
1294
+ space_name=request.space_name,
1295
+ subpath=record.attributes["dest_subpath"],
1296
+ shortname=record.attributes["dest_shortname"],
1297
+ action_type=core.ActionType.move,
1298
+ resource_type=record.resource_type,
1299
+ user_shortname=owner_shortname,
1300
+ attributes={
1301
+ "src_subpath": record.attributes["src_subpath"],
1302
+ "src_shortname": record.attributes["src_shortname"],
1303
+ },
1304
+ )
1305
+ )
1306
+
1307
+ return record, None
1308
+ except api.Exception as e:
1309
+ return None, {
1310
+ "record": record,
1311
+ "error": e.error.message,
1312
+ "error_code": e.error.code,
1313
+ }
1314
+
1315
+ results = await asyncio.gather(*(process_record(r) for r in request.records))
1316
+ for rec, failed in results:
1317
+ if rec is not None:
1318
+ records.append(rec)
1319
+ if failed is not None:
1320
+ failed_records.append(failed)
1321
+
1322
+ return records, failed_records
1323
+
1324
+
1325
+ def get_resource_content_type_from_payload_content_type(payload_file, payload_filename, record):
1326
+ if payload_filename.endswith(".json"):
1327
+ return ContentType.json
1328
+ elif payload_file.content_type == "application/pdf":
1329
+ return ContentType.pdf
1330
+ elif payload_file.content_type == "application/vnd.android.package-archive":
1331
+ return ContentType.apk
1332
+ elif payload_file.content_type == "text/csv":
1333
+ return ContentType.csv
1334
+ elif payload_file.content_type == "application/octet-stream":
1335
+ if record.attributes.get("content_type") == "jsonl":
1336
+ return ContentType.jsonl
1337
+ elif record.attributes.get("content_type") == "sqlite":
1338
+ return ContentType.sqlite
1339
+ elif record.attributes.get("content_type") == "parquet":
1340
+ return ContentType.parquet
1341
+ else:
1342
+ return ContentType.text
1343
+ elif payload_file.content_type == "text/markdown":
1344
+ return ContentType.markdown
1345
+ elif payload_file.content_type and "image/" in payload_file.content_type:
1346
+ return ContentType.image
1347
+ elif payload_file.content_type and "audio/" in payload_file.content_type:
1348
+ return ContentType.audio
1349
+ elif payload_file.content_type and "video/" in payload_file.content_type:
1350
+ return ContentType.video
1351
+ else:
1352
+ raise api.Exception(
1353
+ status.HTTP_406_NOT_ACCEPTABLE,
1354
+ api.Error(
1355
+ type="attachment",
1356
+ code=InternalErrorCode.NOT_SUPPORTED_TYPE,
1357
+ message="The file type is not supported",
1358
+ ),
1359
+ )
1360
+
1361
+
1362
+ async def handle_update_state(space_name, logged_in_user, ticket_obj, action, user_roles):
1363
+ workflows_data = await db.load(
1364
+ space_name=space_name,
1365
+ subpath="workflows",
1366
+ shortname=ticket_obj.workflow_shortname,
1367
+ class_type=core.Content,
1368
+ user_shortname=logged_in_user,
1369
+ )
1370
+
1371
+ workflows_payload: Any = {}
1372
+ if workflows_data.payload is not None and workflows_data.payload.body is not None:
1373
+ if settings.active_data_db == 'file':
1374
+ workflows_payload = await db.load_resource_payload(
1375
+ space_name=space_name,
1376
+ subpath="workflows",
1377
+ filename=str(workflows_data.payload.body),
1378
+ class_type=core.Content,
1379
+ )
1380
+ else:
1381
+ workflows_payload = workflows_data.payload.body
1382
+ else:
1383
+ raise api.Exception(
1384
+ status_code=status.HTTP_400_BAD_REQUEST,
1385
+ error=api.Error(
1386
+ type="transition",
1387
+ code=InternalErrorCode.MISSING_DATA,
1388
+ message="Invalid workflow",
1389
+ ),
1390
+ )
1391
+
1392
+ if not ticket_obj.is_open:
1393
+ raise api.Exception(
1394
+ status_code=status.HTTP_400_BAD_REQUEST,
1395
+ error=api.Error(
1396
+ type="transition",
1397
+ code=InternalErrorCode.TICKET_ALREADY_CLOSED,
1398
+ message="Ticket is already in closed",
1399
+ ),
1400
+ )
1401
+ response = transite(
1402
+ workflows_payload.get("states", []), ticket_obj.state, action, user_roles
1403
+ )
1404
+
1405
+ if not response.get("status", False):
1406
+ raise api.Exception(
1407
+ status_code=status.HTTP_400_BAD_REQUEST,
1408
+ error=api.Error(
1409
+ type="transition",
1410
+ code=InternalErrorCode.INVALID_TICKET_STATUS,
1411
+ message=response.get("message", "")
1412
+ ),
1413
+ )
1414
+
1415
+ old_version_flattend = flatten_dict(ticket_obj.model_dump())
1416
+
1417
+ ticket_obj.state = response["message"]
1418
+ ticket_obj.is_open = check_open_state(
1419
+ workflows_payload.get("states", []), response["message"]
1420
+ )
1421
+
1422
+ return ticket_obj, workflows_payload, response, old_version_flattend
1423
+
1424
+
1425
+ async def update_state_handle_resolution(ticket_obj, workflows_payload, response, resolution):
1426
+ post_response = post_transite(
1427
+ workflows_payload["states"], response["message"], resolution
1428
+ )
1429
+ if not post_response["status"]:
1430
+ raise api.Exception(
1431
+ status_code=status.HTTP_400_BAD_REQUEST,
1432
+ error=api.Error(
1433
+ type="transition",
1434
+ code=InternalErrorCode.INVALID_TICKET_STATUS,
1435
+ message=post_response["message"],
1436
+ ),
1437
+ )
1438
+ ticket_obj.resolution_reason = resolution
1439
+ return ticket_obj
1440
+
1441
+
1442
+ async def serve_space_create(request, record, owner_shortname: str):
1443
+ await is_space_exist(request.space_name, should_exist=False)
1444
+
1445
+ if not await access_control.check_access(
1446
+ user_shortname=owner_shortname,
1447
+ space_name=settings.all_spaces_mw,
1448
+ subpath="/",
1449
+ resource_type=ResourceType.space,
1450
+ action_type=core.ActionType.create,
1451
+ record_attributes=record.attributes,
1452
+ ):
1453
+ raise api.Exception(
1454
+ status.HTTP_401_UNAUTHORIZED,
1455
+ api.Error(
1456
+ type="request",
1457
+ code=InternalErrorCode.NOT_ALLOWED,
1458
+ message="You don't have permission to this action [1]",
1459
+ ),
1460
+ )
1461
+
1462
+ resource_obj = core.Meta.from_record(
1463
+ record=record, owner_shortname=owner_shortname
1464
+ )
1465
+ resource_obj.is_active = True
1466
+ resource_obj.shortname = request.space_name
1467
+ if isinstance(resource_obj, core.Space):
1468
+ resource_obj.indexing_enabled = True
1469
+ resource_obj.active_plugins = [
1470
+ "action_log",
1471
+ "redis_db_update",
1472
+ "resource_folders_creation",
1473
+ ]
1474
+
1475
+ return await db.save(
1476
+ request.space_name,
1477
+ record.subpath,
1478
+ resource_obj,
1479
+ )
1480
+
1481
+
1482
+ async def serve_space_update(request, record, owner_shortname: str, is_replace: bool = False):
1483
+ try:
1484
+ space = core.Space.from_record(record, owner_shortname)
1485
+ await is_space_exist(request.space_name)
1486
+
1487
+ if (
1488
+ request.space_name != record.shortname
1489
+ ):
1490
+ raise Exception
1491
+ except Exception:
1492
+ raise api.Exception(
1493
+ status.HTTP_400_BAD_REQUEST,
1494
+ api.Error(
1495
+ type="request",
1496
+ code=InternalErrorCode.INVALID_SPACE_NAME,
1497
+ message=f"Space name {request.space_name} provided is empty or invalid [6]",
1498
+ ),
1499
+ )
1500
+ if not await access_control.check_access(
1501
+ user_shortname=owner_shortname,
1502
+ space_name=settings.all_spaces_mw,
1503
+ subpath="/",
1504
+ resource_type=ResourceType.space,
1505
+ action_type=core.ActionType.update,
1506
+ record_attributes=record.attributes,
1507
+ entry_shortname=record.shortname
1508
+ ):
1509
+ raise api.Exception(
1510
+ status.HTTP_401_UNAUTHORIZED,
1511
+ api.Error(
1512
+ type="request",
1513
+ code=InternalErrorCode.NOT_ALLOWED,
1514
+ message="You don't have permission to this action [2]",
1515
+ ),
1516
+ )
1517
+
1518
+ await plugin_manager.before_action(
1519
+ core.Event(
1520
+ space_name=space.shortname,
1521
+ subpath=record.subpath,
1522
+ shortname=space.shortname,
1523
+ action_type=core.ActionType.update,
1524
+ resource_type=record.resource_type,
1525
+ user_shortname=owner_shortname,
1526
+ )
1527
+ )
1528
+
1529
+ old_space = await db.load(
1530
+ space_name=space.shortname,
1531
+ subpath=record.subpath,
1532
+ shortname=space.shortname,
1533
+ class_type=core.Space,
1534
+ user_shortname=owner_shortname,
1535
+ )
1536
+
1537
+ requested_checksum = record.attributes.get("last_checksum_history")
1538
+ if requested_checksum:
1539
+ latest_history = await db.get_latest_history(
1540
+ space_name=space.shortname,
1541
+ subpath=record.subpath,
1542
+ shortname=space.shortname,
1543
+ )
1544
+ if latest_history and latest_history.last_checksum_history != requested_checksum:
1545
+ raise api.Exception(
1546
+ status.HTTP_409_CONFLICT,
1547
+ api.Error(
1548
+ type="request",
1549
+ code=InternalErrorCode.CONFLICT,
1550
+ message="Resource has been updated by another request. Please refresh and try again.",
1551
+ ),
1552
+ )
1553
+
1554
+ old_flat = flatten_dict(old_space.model_dump())
1555
+ new_flat = flatten_dict(space.model_dump())
1556
+ updated_attributes_flattend = list(
1557
+ flatten_dict(record.attributes).keys()
1558
+ )
1559
+ if is_replace:
1560
+ updated_attributes_flattend = list(old_flat.keys()) + list(new_flat.keys())
1561
+ history_diff = await db.update(
1562
+ space_name=space.shortname,
1563
+ subpath=record.subpath,
1564
+ meta=space,
1565
+ old_version_flattend=old_flat,
1566
+ new_version_flattend=new_flat,
1567
+ updated_attributes_flattend=updated_attributes_flattend,
1568
+ user_shortname=owner_shortname,
1569
+ retrieve_lock_status=record.retrieve_lock_status,
1570
+ )
1571
+ return history_diff
1572
+
1573
+
1574
+ async def serve_space_delete(request, record, owner_shortname: str):
1575
+ if request.space_name == "management":
1576
+ raise api.Exception(
1577
+ status.HTTP_400_BAD_REQUEST,
1578
+ api.Error(
1579
+ type="request",
1580
+ code=InternalErrorCode.CANNT_DELETE,
1581
+ message="Cannot delete management space",
1582
+ ),
1583
+ )
1584
+
1585
+ await is_space_exist(request.space_name)
1586
+
1587
+ if not await access_control.check_access(
1588
+ user_shortname=owner_shortname,
1589
+ space_name=settings.all_spaces_mw,
1590
+ subpath="/",
1591
+ resource_type=ResourceType.space,
1592
+ action_type=core.ActionType.delete,
1593
+ entry_shortname=record.shortname
1594
+ ):
1595
+ raise api.Exception(
1596
+ status.HTTP_401_UNAUTHORIZED,
1597
+ api.Error(
1598
+ type="request",
1599
+ code=InternalErrorCode.NOT_ALLOWED,
1600
+ message="You don't have permission to this action [3]",
1601
+ ),
1602
+ )
1603
+ await repository.delete_space(request.space_name, record, owner_shortname)
1604
+ await db.drop_index(request.space_name)
1605
+
1606
+
1607
+
1608
+ async def data_asset_attachments_handler(query, attachments):
1609
+ files_paths = []
1610
+ for attachment in attachments.get(query.data_asset_type, []):
1611
+ file_path = db.payload_path(
1612
+ space_name=query.space_name,
1613
+ subpath=f"{query.subpath}/{query.shortname}",
1614
+ class_type=getattr(sys.modules["models.core"], camel_case(query.data_asset_type)),
1615
+ )
1616
+ if (
1617
+ not isinstance(attachment.attributes.get("payload"), core.Payload)
1618
+ or not isinstance(attachment.attributes["payload"].body, str)
1619
+ or not (file_path / attachment.attributes["payload"].body).is_file()
1620
+ ):
1621
+ raise api.Exception(
1622
+ status_code=status.HTTP_404_NOT_FOUND,
1623
+ error=api.Error(
1624
+ type="db",
1625
+ code=InternalErrorCode.INVALID_DATA,
1626
+ message=f"Invalid data asset file found at {attachment.subpath}/{attachment.shortname}",
1627
+ ),
1628
+ )
1629
+
1630
+ file_path /= attachment.attributes["payload"].body
1631
+ if (
1632
+ attachment.attributes["payload"].schema_shortname
1633
+ and attachment.resource_type == DataAssetType.csv
1634
+ ):
1635
+ await validate_csv_with_schema(
1636
+ file_path=file_path,
1637
+ space_name=query.space_name,
1638
+ schema_shortname=attachment.attributes["payload"].schema_shortname
1639
+ )
1640
+ if (
1641
+ attachment.attributes["payload"].schema_shortname
1642
+ and attachment.resource_type == DataAssetType.jsonl
1643
+ ):
1644
+ await validate_jsonl_with_schema(
1645
+ file_path=file_path,
1646
+ space_name=query.space_name,
1647
+ schema_shortname=attachment.attributes["payload"].schema_shortname
1648
+ )
1649
+ files_paths.append(file_path)
1650
+ return files_paths
1651
+
1652
+
1653
+ async def data_asset_handler(conn, query, files_paths, attachments):
1654
+ for idx, file_path in enumerate(files_paths):
1655
+ # Load the file into the in-memory DB
1656
+ match query.data_asset_type:
1657
+ case DataAssetType.csv:
1658
+ globals().setdefault(
1659
+ attachments[query.data_asset_type][idx].shortname,
1660
+ conn.read_csv(str(file_path))
1661
+ )
1662
+ case DataAssetType.jsonl:
1663
+ globals().setdefault(
1664
+ attachments[query.data_asset_type][idx].shortname,
1665
+ conn.read_json(
1666
+ str(file_path),
1667
+ format='auto'
1668
+ )
1669
+ )
1670
+ case DataAssetType.parquet:
1671
+ globals().setdefault(
1672
+ attachments[query.data_asset_type][idx].shortname,
1673
+ conn.read_parquet(str(file_path))
1674
+ )
1675
+
1676
+
1677
+ async def import_resources_from_csv_handler(
1678
+ row, meta_class_attributes, schema_content, data_types_mapper,
1679
+ ):
1680
+ shortname = ""
1681
+ meta_object = {}
1682
+ payload_object = {}
1683
+ for key, value in row.items():
1684
+ if not key or not value:
1685
+ continue
1686
+
1687
+ if key == "shortname":
1688
+ shortname = value
1689
+ continue
1690
+
1691
+ keys_list = [i.strip() for i in key.split(".")]
1692
+ if keys_list[0] in meta_class_attributes:
1693
+ match len(keys_list):
1694
+ case 1:
1695
+ if str(meta_class_attributes[keys_list[0]].annotation).startswith('list'):
1696
+ meta_object[keys_list[0].strip()] = [
1697
+ e.strip().strip("'").strip('"') for e in value.strip("[]").split(",")
1698
+ ]
1699
+ else:
1700
+ meta_object[keys_list[0].strip()] = value
1701
+ case 2:
1702
+ if keys_list[0].strip() not in meta_object:
1703
+ meta_object[keys_list[0].strip()] = []
1704
+ meta_object[keys_list[0].strip(
1705
+ )][keys_list[1].strip()] = value
1706
+ continue
1707
+
1708
+ if schema_content is not None:
1709
+ current_schema_property = schema_content
1710
+ for item in keys_list:
1711
+ if "oneOf" in current_schema_property:
1712
+ for oneOf_item in current_schema_property["oneOf"]:
1713
+ if (
1714
+ "properties" in oneOf_item
1715
+ and item.strip() in oneOf_item["properties"]
1716
+ ):
1717
+ current_schema_property = oneOf_item["properties"][
1718
+ item.strip()
1719
+ ]
1720
+ break
1721
+ else:
1722
+ if (
1723
+ "properties" in current_schema_property
1724
+ and item.strip() in current_schema_property["properties"]
1725
+ ):
1726
+ current_schema_property = current_schema_property["properties"][
1727
+ item.strip()
1728
+ ]
1729
+
1730
+ if current_schema_property["type"] in ["number", "integer"]:
1731
+ value = value.replace(",", "")
1732
+ try:
1733
+ value = data_types_mapper[current_schema_property["type"]](value)
1734
+ if current_schema_property["type"] == "array":
1735
+ value = [
1736
+ str(item) if type(item) in [int, float] else item for item in value
1737
+ ]
1738
+ except ValueError as e:
1739
+ raise api.Exception(
1740
+ status.HTTP_400_BAD_REQUEST,
1741
+ api.Error(
1742
+ type="request",
1743
+ code=InternalErrorCode.INVALID_DATA,
1744
+ message=f"Invalid value for {key}: {value}",
1745
+ info=[{"message": str(e)}],
1746
+ ),
1747
+ )
1748
+
1749
+ match len(keys_list):
1750
+ case 1:
1751
+ payload_object[keys_list[0].strip()] = value
1752
+ case 2:
1753
+ if keys_list[0].strip() not in payload_object:
1754
+ payload_object[keys_list[0].strip()] = {}
1755
+ payload_object[keys_list[0].strip(
1756
+ )][keys_list[1].strip()] = value
1757
+ case 3:
1758
+ if keys_list[0].strip() not in payload_object:
1759
+ payload_object[keys_list[0].strip()] = {}
1760
+ if keys_list[1].strip() not in payload_object[keys_list[0].strip()]:
1761
+ payload_object[keys_list[0].strip(
1762
+ )][keys_list[1].strip()] = {}
1763
+ payload_object[keys_list[0].strip()][keys_list[1].strip()][
1764
+ keys_list[2].strip()
1765
+ ] = value
1766
+ case _:
1767
+ continue
1768
+ if shortname == "":
1769
+ shortname = settings.auto_uuid_rule
1770
+ return payload_object, meta_object, shortname
1771
+
1772
+
1773
+ async def create_or_update_resource_with_payload_handler(
1774
+ record, owner_shortname, space_name, payload_file, payload_filename, checksum, sha, resource_content_type
1775
+ ):
1776
+ if record.resource_type == ResourceType.ticket:
1777
+ record = await set_init_state_from_record(
1778
+ record, owner_shortname, space_name
1779
+ )
1780
+ resource_obj = core.Meta.from_record(
1781
+ record=record, owner_shortname=owner_shortname)
1782
+ if record.resource_type == ResourceType.ticket:
1783
+ record = await set_init_state_from_record(
1784
+ record, owner_shortname, space_name
1785
+ )
1786
+
1787
+ file_extension = FilePath(payload_filename).suffix
1788
+ if file_extension.startswith('.'):
1789
+ file_extension = file_extension[1:]
1790
+
1791
+ resource_obj.payload = core.Payload(
1792
+ content_type=resource_content_type,
1793
+ checksum=checksum,
1794
+ client_checksum=sha if isinstance(sha, str) else None,
1795
+ schema_shortname="meta_schema"
1796
+ if record.resource_type == ResourceType.schema
1797
+ else record.attributes.get("payload", {}).get("schema_shortname", None),
1798
+ body=f"{record.shortname}.{file_extension}",
1799
+ )
1800
+ if (
1801
+ not isinstance(resource_obj, core.Attachment)
1802
+ and not isinstance(resource_obj, core.Content)
1803
+ and not isinstance(resource_obj, core.Ticket)
1804
+ and not isinstance(resource_obj, core.Schema)
1805
+ ):
1806
+ raise api.Exception(
1807
+ status.HTTP_400_BAD_REQUEST,
1808
+ api.Error(
1809
+ type="attachment",
1810
+ code=InternalErrorCode.SOME_SUPPORTED_TYPE,
1811
+ message="Only resources of type 'attachment' or 'content' are allowed",
1812
+ ),
1813
+ )
1814
+ if settings.active_data_db == "file":
1815
+ resource_obj.payload.body = f"{resource_obj.shortname}.{file_extension}"
1816
+ elif not isinstance(resource_obj, core.Attachment):
1817
+ resource_obj.payload.body = json.load(payload_file.file)
1818
+ payload_file.file.seek(0)
1819
+
1820
+ if (
1821
+ resource_content_type == ContentType.json
1822
+ and resource_obj.payload.schema_shortname
1823
+ ):
1824
+ await db.validate_payload_with_schema(
1825
+ payload_data=payload_file,
1826
+ space_name=space_name,
1827
+ schema_shortname=resource_obj.payload.schema_shortname,
1828
+ )
1829
+
1830
+ return resource_obj, record
1831
+
1832
+
1833
+ def get_mime_type(content_type: ContentType) -> str:
1834
+ mime_types = {
1835
+ ContentType.text: "text/plain",
1836
+ ContentType.markdown: "text/markdown",
1837
+ ContentType.html: "text/html",
1838
+ ContentType.json: "application/json",
1839
+ ContentType.image: "image/jpeg",
1840
+ ContentType.python: "text/x-python",
1841
+ ContentType.pdf: "application/pdf",
1842
+ ContentType.audio: "audio/mpeg",
1843
+ ContentType.video: "video/mp4",
1844
+ ContentType.csv: "text/csv",
1845
+ ContentType.parquet: "application/octet-stream",
1846
+ ContentType.jsonl: "application/jsonlines",
1847
+ ContentType.duckdb: "application/octet-stream",
1848
+ ContentType.sqlite: "application/vnd.sqlite3"
1849
+ }
1850
+ return mime_types.get(content_type, "application/octet-stream")