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/public/router.py ADDED
@@ -0,0 +1,758 @@
1
+ import hashlib
2
+ from re import sub as res_sub
3
+ from uuid import uuid4
4
+ from fastapi import APIRouter, Body, Depends, Form, Path, Query, UploadFile, status
5
+ from models.enums import AttachmentType, ContentType, ResourceType, TaskType, PublicSubmitResourceType
6
+ from data_adapters.adapter import data_adapter as db
7
+ import models.api as api
8
+ from utils.helpers import camel_case
9
+ from utils.internal_error_code import InternalErrorCode
10
+ import utils.regex as regex
11
+ import models.core as core
12
+ from api.managed.utils import get_mime_type, get_resource_content_type_from_payload_content_type, \
13
+ create_or_update_resource_with_payload_handler, iter_bytesio
14
+ from typing import Any, Union, Optional
15
+ import sys
16
+ import re
17
+ import os
18
+ from utils.access_control import access_control
19
+ import utils.repository as repository
20
+ from utils.plugin_manager import plugin_manager
21
+ from utils.router_helper import is_space_exist
22
+ from utils.settings import settings
23
+ from starlette.responses import FileResponse, StreamingResponse
24
+
25
+ from utils.ticket_sys_utils import set_init_state_for_record
26
+ from fastapi.responses import ORJSONResponse
27
+
28
+ router = APIRouter(default_response_class=ORJSONResponse)
29
+
30
+ # Retrieve publically-available content
31
+
32
+
33
+ @router.post("/query", response_model=api.Response, response_model_exclude_none=True)
34
+ async def query_entries(query: api.Query) -> api.Response:
35
+ await plugin_manager.before_action(
36
+ core.Event(
37
+ space_name=query.space_name,
38
+ subpath=query.subpath,
39
+ action_type=core.ActionType.query,
40
+ user_shortname="anonymous",
41
+ attributes={"filter_shortnames": query.filter_shortnames},
42
+ )
43
+ )
44
+
45
+ total, records = await repository.serve_query(
46
+ query, "anonymous"
47
+ )
48
+
49
+ await plugin_manager.after_action(
50
+ core.Event(
51
+ space_name=query.space_name,
52
+ subpath=query.subpath,
53
+ action_type=core.ActionType.query,
54
+ user_shortname="anonymous",
55
+ attributes={"filter_shortnames": query.filter_shortnames},
56
+ )
57
+ )
58
+
59
+ return api.Response(
60
+ status=api.Status.success,
61
+ records=records,
62
+ attributes={"total": total, "returned": len(records)},
63
+ )
64
+
65
+
66
+ @router.get(
67
+ "/entry/{resource_type}/{space_name}/{subpath:path}/{shortname}",
68
+ response_model_exclude_none=True,
69
+ )
70
+ async def retrieve_entry_meta(
71
+ resource_type: ResourceType,
72
+ space_name: str = Path(..., pattern=regex.SPACENAME),
73
+ subpath: str = Path(..., pattern=regex.SUBPATH),
74
+ shortname: str = Path(..., pattern=regex.SHORTNAME),
75
+ retrieve_json_payload: bool = False,
76
+ retrieve_attachments: bool = False,
77
+ filter_attachments_types: list = Query(default=[], examples=["media", "comment", "json"]),
78
+ ) -> dict[str, Any]:
79
+ if subpath == settings.root_subpath_mw:
80
+ subpath = "/"
81
+
82
+ await plugin_manager.before_action(
83
+ core.Event(
84
+ space_name=space_name,
85
+ subpath=subpath,
86
+ shortname=shortname,
87
+ action_type=core.ActionType.view,
88
+ resource_type=resource_type,
89
+ user_shortname="anonymous",
90
+ )
91
+ )
92
+
93
+ resource_class = getattr(
94
+ sys.modules["models.core"], camel_case(resource_type))
95
+ meta = await db.load(
96
+ space_name=space_name,
97
+ subpath=subpath,
98
+ shortname=shortname,
99
+ class_type=resource_class,
100
+ user_shortname="anonymous",
101
+ )
102
+ if meta is None:
103
+ raise api.Exception(
104
+ status.HTTP_400_BAD_REQUEST,
105
+ error=api.Error(
106
+ type="media", code=InternalErrorCode.OBJECT_NOT_FOUND, message="Request object is not found"
107
+ ),
108
+ )
109
+
110
+ if not await access_control.check_access(
111
+ user_shortname="anonymous",
112
+ space_name=space_name,
113
+ subpath=subpath,
114
+ resource_type=resource_type,
115
+ action_type=core.ActionType.view,
116
+ resource_is_active=meta.is_active,
117
+ resource_owner_shortname=meta.owner_shortname,
118
+ resource_owner_group=meta.owner_group_shortname,
119
+ entry_shortname=meta.shortname,
120
+ ):
121
+ raise api.Exception(
122
+ status.HTTP_401_UNAUTHORIZED,
123
+ api.Error(
124
+ type="request",
125
+ code=InternalErrorCode.NOT_ALLOWED,
126
+ message="You don't have permission to this action [14]",
127
+ ),
128
+ )
129
+
130
+ attachments = {}
131
+ entry_path = (
132
+ settings.spaces_folder
133
+ / f"{space_name}/{subpath}/.dm/{shortname}"
134
+ )
135
+ if retrieve_attachments:
136
+ attachments = await db.get_entry_attachments(
137
+ subpath=subpath,
138
+ attachments_path=entry_path,
139
+ retrieve_json_payload=retrieve_json_payload,
140
+ filter_types=filter_attachments_types
141
+ )
142
+
143
+ if (not retrieve_json_payload or
144
+ not meta.payload or
145
+ not meta.payload.body or
146
+ not isinstance(meta.payload.body, str) or
147
+ meta.payload.content_type != ContentType.json
148
+ ):
149
+ # TODO
150
+ # include locked before returning the dictionary
151
+ return {
152
+ **meta.dict(exclude_none=True),
153
+ "attachments": attachments
154
+ }
155
+
156
+ payload_body = await db.load_resource_payload(
157
+ space_name=space_name,
158
+ subpath=subpath,
159
+ filename=meta.payload.body,
160
+ class_type=resource_class,
161
+ )
162
+
163
+ if meta.payload and meta.payload.schema_shortname and payload_body:
164
+ await db.validate_payload_with_schema(
165
+ payload_data=payload_body,
166
+ space_name=space_name,
167
+ schema_shortname=meta.payload.schema_shortname,
168
+ )
169
+
170
+ meta.payload.body = payload_body
171
+ await plugin_manager.after_action(
172
+ core.Event(
173
+ space_name=space_name,
174
+ subpath=subpath,
175
+ shortname=shortname,
176
+ action_type=core.ActionType.view,
177
+ resource_type=resource_type,
178
+ user_shortname="anonymous",
179
+ )
180
+ )
181
+
182
+ return {
183
+ **meta.dict(exclude_none=True),
184
+ "attachments": attachments
185
+ }
186
+
187
+
188
+ # Public payload retrieval; can be used in "src=" in html pages
189
+ @router.get(
190
+ "/payload/{resource_type}/{space_name}/{subpath:path}/{shortname}.{ext}",
191
+ response_model=None
192
+ )
193
+ async def retrieve_entry_or_attachment_payload(
194
+ resource_type: ResourceType,
195
+ space_name: str = Path(..., pattern=regex.SPACENAME),
196
+ subpath: str = Path(..., pattern=regex.SUBPATH),
197
+ shortname: str = Path(..., pattern=regex.SHORTNAME),
198
+ ext: str = Path(..., pattern=regex.EXT),
199
+ ) -> FileResponse | api.Response | StreamingResponse:
200
+ await plugin_manager.before_action(
201
+ core.Event(
202
+ space_name=space_name,
203
+ subpath=subpath,
204
+ shortname=shortname,
205
+ action_type=core.ActionType.view,
206
+ resource_type=resource_type,
207
+ user_shortname="anonymous",
208
+ )
209
+ )
210
+ cls = getattr(sys.modules["models.core"], camel_case(resource_type))
211
+ meta = await db.load(
212
+ space_name=space_name,
213
+ subpath=subpath,
214
+ shortname=shortname,
215
+ class_type=cls,
216
+ user_shortname="anonymous",
217
+ )
218
+ if (
219
+ meta.payload is None
220
+ or meta.payload.body is None
221
+ or meta.payload.body != f"{shortname}.{ext}"
222
+ ):
223
+ raise api.Exception(
224
+ status.HTTP_400_BAD_REQUEST,
225
+ error=api.Error(
226
+ type="media", code=InternalErrorCode.OBJECT_NOT_FOUND, message="Request object is not available"
227
+ ),
228
+ )
229
+
230
+ if not await access_control.check_access(
231
+ user_shortname="anonymous",
232
+ space_name=space_name,
233
+ subpath=subpath,
234
+ resource_type=resource_type,
235
+ action_type=core.ActionType.view,
236
+ resource_is_active=meta.is_active,
237
+ resource_owner_shortname=meta.owner_shortname,
238
+ resource_owner_group=meta.owner_group_shortname,
239
+ entry_shortname=meta.shortname,
240
+ ):
241
+ raise api.Exception(
242
+ status.HTTP_401_UNAUTHORIZED,
243
+ api.Error(
244
+ type="request",
245
+ code=InternalErrorCode.NOT_ALLOWED,
246
+ message="You don't have permission to this action [15]",
247
+ ),
248
+ )
249
+
250
+ await plugin_manager.after_action(
251
+ core.Event(
252
+ space_name=space_name,
253
+ subpath=subpath,
254
+ shortname=shortname,
255
+ action_type=core.ActionType.view,
256
+ resource_type=resource_type,
257
+ user_shortname="anonymous",
258
+ )
259
+ )
260
+
261
+ if settings.active_data_db == "file":
262
+ payload_path = db.payload_path(
263
+ space_name=space_name,
264
+ subpath=subpath,
265
+ class_type=cls,
266
+ )
267
+ return FileResponse(payload_path / str(meta.payload.body))
268
+
269
+ if meta.payload.content_type == ContentType.json and isinstance(meta.payload.body, dict):
270
+ return api.Response(
271
+ status=api.Status.success,
272
+ attributes=meta.payload.body
273
+ )
274
+
275
+ data = await db.get_media_attachment(space_name, subpath, shortname)
276
+ if data:
277
+ return StreamingResponse(iter_bytesio(data), media_type=get_mime_type(meta.payload.content_type))
278
+
279
+ return api.Response(status=api.Status.failed)
280
+
281
+
282
+ """
283
+ @router.post("/submit", response_model_exclude_none=True)
284
+ async def submit() -> api.Response:
285
+ return api.Response(status=api.Status.success)
286
+ """
287
+
288
+
289
+ @router.get(
290
+ "/query/{type}/{space_name}/{subpath:path}",
291
+ response_model=api.Response,
292
+ response_model_exclude_none=True,
293
+ )
294
+ async def query_via_urlparams(
295
+ query: api.Query = Depends(api.Query),
296
+ ) -> api.Response:
297
+ await plugin_manager.before_action(
298
+ core.Event(
299
+ space_name=query.space_name,
300
+ subpath=query.subpath,
301
+ action_type=core.ActionType.query,
302
+ user_shortname="anonymous",
303
+ attributes={"filter_shortnames": query.filter_shortnames},
304
+ )
305
+ )
306
+
307
+ total, records = await repository.serve_query(
308
+ query, "anonymous"
309
+ )
310
+
311
+ await plugin_manager.after_action(
312
+ core.Event(
313
+ space_name=query.space_name,
314
+ subpath=query.subpath,
315
+ action_type=core.ActionType.query,
316
+ user_shortname="anonymous",
317
+ attributes={"filter_shortnames": query.filter_shortnames},
318
+ )
319
+ )
320
+
321
+ return api.Response(
322
+ status=api.Status.success,
323
+ records=records,
324
+ attributes={"total": total, "returned": len(records)},
325
+ )
326
+
327
+
328
+ @router.post(
329
+ "/resource_with_payload",
330
+ response_model=api.Response,
331
+ response_model_exclude_none=True,
332
+ )
333
+ async def create_or_update_resource_with_payload(
334
+ payload_file: UploadFile,
335
+ request_record: UploadFile,
336
+ space_name: str = Form(..., examples=["data"]),
337
+ sha: str | None = Form(None, examples=["data"]),
338
+ ):
339
+ # NOTE We currently make no distinction between create and update.
340
+ # in such case update should contain all the data every time.
341
+ await is_space_exist(space_name)
342
+
343
+ record = core.Record.model_validate_json(request_record.file.read())
344
+
345
+ payload_filename = payload_file.filename or ""
346
+ if payload_filename and not re.search(regex.EXT, os.path.splitext(payload_filename)[1][1:]):
347
+ raise api.Exception(
348
+ status.HTTP_400_BAD_REQUEST,
349
+ api.Error(
350
+ type="request",
351
+ code=InternalErrorCode.INVALID_DATA,
352
+ message=f"Invalid payload file extention, it should end with {regex.EXT}",
353
+ ),
354
+ )
355
+ resource_content_type = get_resource_content_type_from_payload_content_type(
356
+ payload_file, payload_filename, record
357
+ )
358
+
359
+ await plugin_manager.before_action(
360
+ core.Event(
361
+ space_name=space_name,
362
+ subpath=record.subpath,
363
+ shortname=record.shortname,
364
+ action_type=core.ActionType.create,
365
+ schema_shortname=record.attributes.get("payload", {}).get(
366
+ "schema_shortname", None
367
+ ),
368
+ resource_type=record.resource_type,
369
+ user_shortname="anonymous",
370
+ )
371
+ )
372
+
373
+ if not await access_control.check_access(
374
+ user_shortname="anonymous",
375
+ space_name=space_name,
376
+ subpath=record.subpath,
377
+ resource_type=record.resource_type,
378
+ action_type=core.ActionType.create,
379
+ record_attributes=record.attributes,
380
+ entry_shortname=record.shortname,
381
+ ):
382
+ raise api.Exception(
383
+ status.HTTP_401_UNAUTHORIZED,
384
+ api.Error(
385
+ type="request",
386
+ code=InternalErrorCode.NOT_ALLOWED,
387
+ message="You don't have permission to this action [50]",
388
+ ),
389
+ )
390
+
391
+ sha1 = hashlib.sha1()
392
+ sha1.update(payload_file.file.read())
393
+ checksum = sha1.hexdigest()
394
+ if isinstance(sha, str) and sha != checksum:
395
+ raise api.Exception(
396
+ status.HTTP_400_BAD_REQUEST,
397
+ api.Error(
398
+ type="request",
399
+ code=InternalErrorCode.INVALID_DATA,
400
+ message="The provided file doesn't match the sha",
401
+ ),
402
+ )
403
+ await payload_file.seek(0)
404
+ resource_obj, record = await create_or_update_resource_with_payload_handler(
405
+ record, "anonymous", space_name, payload_file, payload_filename, checksum, sha, resource_content_type
406
+ )
407
+
408
+ await db.save(space_name, record.subpath, resource_obj)
409
+ await db.save_payload(
410
+ space_name, record.subpath, resource_obj, payload_file
411
+ )
412
+
413
+ await plugin_manager.after_action(
414
+ core.Event(
415
+ space_name=space_name,
416
+ subpath=record.subpath,
417
+ shortname=record.shortname,
418
+ action_type=core.ActionType.create,
419
+ schema_shortname=record.attributes.get("payload", {}).get(
420
+ "schema_shortname", None
421
+ ),
422
+ resource_type=record.resource_type,
423
+ user_shortname="anonymous",
424
+ )
425
+ )
426
+
427
+ return api.Response(
428
+ status=api.Status.success,
429
+ records=[record],
430
+ )
431
+
432
+
433
+ @router.post("/submit/{space_name}/{schema_shortname}/{subpath:path}")
434
+ @router.post("/submit/{space_name}/{resource_type}/{schema_shortname}/{subpath:path}")
435
+ @router.post("/submit/{space_name}/{resource_type}/{workflow_shortname}/{schema_shortname}/{subpath:path}")
436
+ async def create_entry(
437
+ space_name: str = Path(...),
438
+ schema_shortname: str = Path(...),
439
+ subpath: str = Path(..., pattern=regex.SUBPATH),
440
+ resource_type: PublicSubmitResourceType | None = None,
441
+ workflow_shortname: str | None = None,
442
+ body_dict: dict[str, Any] = Body(...),
443
+ ):
444
+ allowed_models = settings.allowed_submit_models
445
+ entry_resource_type: ResourceType = ResourceType(resource_type.name) if resource_type else ResourceType.content
446
+ if (
447
+ space_name not in allowed_models
448
+ or schema_shortname not in allowed_models[space_name]
449
+ ):
450
+ raise api.Exception(
451
+ status.HTTP_400_BAD_REQUEST,
452
+ api.Error(
453
+ type="request",
454
+ code=InternalErrorCode.NOT_ALLOWED_LOCATION,
455
+ message="Selected location is not allowed",
456
+ ),
457
+ )
458
+
459
+ if not await access_control.check_access(
460
+ user_shortname="anonymous",
461
+ space_name=space_name,
462
+ subpath=subpath,
463
+ resource_type=entry_resource_type,
464
+ action_type=core.ActionType.create,
465
+ record_attributes=body_dict,
466
+ ):
467
+ raise api.Exception(
468
+ status.HTTP_401_UNAUTHORIZED,
469
+ api.Error(
470
+ type="request",
471
+ code=InternalErrorCode.NOT_ALLOWED,
472
+ message="You don't have permission to this action [13]",
473
+ ),
474
+ )
475
+
476
+ uuid = uuid4()
477
+ shortname = str(uuid)[:8]
478
+ await plugin_manager.before_action(
479
+ core.Event(
480
+ space_name=space_name,
481
+ subpath=subpath,
482
+ shortname=shortname,
483
+ action_type=core.ActionType.create,
484
+ schema_shortname=schema_shortname,
485
+ resource_type=entry_resource_type,
486
+ user_shortname="anonymous",
487
+ )
488
+ )
489
+
490
+ content_obj: Optional[Union[core.Content, core.Ticket]] = None
491
+ if entry_resource_type == ResourceType.ticket:
492
+ if workflow_shortname is None:
493
+ raise api.Exception(
494
+ status.HTTP_400_BAD_REQUEST,
495
+ api.Error(
496
+ type="request",
497
+ code=InternalErrorCode.INVALID_DATA,
498
+ message="Workflow shortname is required for ticket creation",
499
+ ),
500
+ )
501
+
502
+ record = core.Record(
503
+ subpath=subpath,
504
+ shortname=shortname,
505
+ resource_type=entry_resource_type,
506
+ attributes={
507
+ "workflow_shortname": workflow_shortname,
508
+ **body_dict
509
+ }
510
+ )
511
+ record = await set_init_state_for_record(record, space_name, "anonymous")
512
+ if not record or not record.attributes.get("state"):
513
+ raise api.Exception(
514
+ status.HTTP_400_BAD_REQUEST,
515
+ api.Error(
516
+ type="request",
517
+ code=InternalErrorCode.INVALID_DATA,
518
+ message="Failed to set initial state",
519
+ ),
520
+ )
521
+ content_obj = core.Ticket(
522
+ uuid=uuid,
523
+ shortname=shortname,
524
+ is_active=True,
525
+ owner_shortname="anonymous",
526
+ payload=core.Payload(
527
+ content_type=ContentType.json,
528
+ schema_shortname=schema_shortname,
529
+ body=f"{shortname}.json",
530
+ ),
531
+ state=record.attributes["state"],
532
+ workflow_shortname=workflow_shortname,
533
+ is_open=record.attributes["is_open"]
534
+ )
535
+ elif entry_resource_type == ResourceType.content:
536
+ content_obj = core.Content(
537
+ uuid=uuid,
538
+ shortname=shortname,
539
+ is_active=True,
540
+ owner_shortname="anonymous",
541
+ payload=core.Payload(
542
+ content_type=ContentType.json,
543
+ schema_shortname=schema_shortname,
544
+ body=f"{shortname}.json",
545
+ ),
546
+ )
547
+
548
+ if content_obj is None:
549
+ raise api.Exception(
550
+ status.HTTP_400_BAD_REQUEST,
551
+ api.Error(
552
+ type="request",
553
+ code=InternalErrorCode.INVALID_DATA,
554
+ message="Invalid resource type for entry creation",
555
+ ),
556
+ )
557
+
558
+ if content_obj.payload and content_obj.payload.schema_shortname:
559
+ await db.validate_payload_with_schema(
560
+ payload_data=body_dict,
561
+ space_name=space_name,
562
+ schema_shortname=content_obj.payload.schema_shortname,
563
+ )
564
+
565
+ response_data = await db.save(space_name, subpath, content_obj)
566
+ if response_data is None:
567
+ raise api.Exception(
568
+ status.HTTP_400_BAD_REQUEST,
569
+ api.Error(
570
+ type="request",
571
+ code=InternalErrorCode.SOMETHING_WRONG,
572
+ message="Something went wrong while saving the entry",
573
+ ),
574
+ )
575
+ await db.save_payload_from_json(
576
+ space_name, subpath, content_obj, body_dict
577
+ )
578
+
579
+ await plugin_manager.after_action(
580
+ core.Event(
581
+ space_name=space_name,
582
+ subpath=subpath,
583
+ shortname=shortname,
584
+ action_type=core.ActionType.create,
585
+ schema_shortname=schema_shortname,
586
+ resource_type=entry_resource_type,
587
+ user_shortname="anonymous",
588
+ attributes={}
589
+ )
590
+ )
591
+ response_data = response_data.model_dump(exclude_none=True, by_alias=True)
592
+ del response_data["query_policies"]
593
+ return api.Response(
594
+ status=api.Status.success,
595
+ records=[response_data],
596
+ )
597
+
598
+
599
+ @router.post("/attach/{space_name}")
600
+ async def create_attachment(
601
+ space_name: str,
602
+ record: core.Record
603
+ ):
604
+ if record.resource_type not in AttachmentType.__members__:
605
+ raise api.Exception(
606
+ status.HTTP_401_UNAUTHORIZED,
607
+ api.Error(
608
+ type="request",
609
+ code=InternalErrorCode.NOT_ALLOWED,
610
+ message="You don't have permission to this action [54]",
611
+ ),
612
+ )
613
+
614
+ if not await access_control.check_access(
615
+ user_shortname="anonymous",
616
+ space_name=space_name,
617
+ subpath=record.subpath,
618
+ resource_type=record.resource_type,
619
+ action_type=core.ActionType.create,
620
+ record_attributes=record.attributes,
621
+ ):
622
+ raise api.Exception(
623
+ status.HTTP_401_UNAUTHORIZED,
624
+ api.Error(
625
+ type="request",
626
+ code=InternalErrorCode.NOT_ALLOWED,
627
+ message="You don't have permission to this action [55]",
628
+ ),
629
+ )
630
+
631
+ await plugin_manager.before_action(
632
+ core.Event(
633
+ space_name=space_name,
634
+ subpath=record.subpath,
635
+ action_type=core.ActionType.create,
636
+ resource_type=record.resource_type,
637
+ user_shortname="anonymous",
638
+ )
639
+ )
640
+
641
+ record.shortname = 'auto'
642
+ attachment_obj = core.Meta.from_record(
643
+ record=record, owner_shortname="anonymous"
644
+ )
645
+
646
+ await db.save(space_name, record.subpath, attachment_obj)
647
+
648
+ await plugin_manager.after_action(
649
+ core.Event(
650
+ space_name=space_name,
651
+ subpath=record.subpath,
652
+ shortname=attachment_obj.shortname,
653
+ action_type=core.ActionType.create,
654
+ resource_type=record.resource_type,
655
+ user_shortname="anonymous",
656
+ attributes={}
657
+ )
658
+ )
659
+
660
+ return api.Response(status=api.Status.success)
661
+
662
+
663
+ @router.post("/excute/{task_type}/{space_name}")
664
+ async def excute(space_name: str, task_type: TaskType, record: core.Record):
665
+ task_type = task_type
666
+ meta = await db.load(
667
+ space_name=space_name,
668
+ subpath=record.subpath,
669
+ shortname=record.shortname,
670
+ class_type=core.Content,
671
+ user_shortname="anonymous"
672
+ )
673
+
674
+ if (
675
+ meta.payload is None
676
+ or not isinstance(meta.payload.body, str)
677
+ or not str(meta.payload.body).endswith(".json")
678
+ ):
679
+ raise api.Exception(
680
+ status.HTTP_400_BAD_REQUEST,
681
+ error=api.Error(
682
+ type="media", code=InternalErrorCode.OBJECT_NOT_FOUND, message="Request object is not available"
683
+ ),
684
+ )
685
+
686
+ mydict = await db.load_resource_payload(
687
+ space_name=space_name,
688
+ subpath=record.subpath,
689
+ filename=str(meta.payload.body),
690
+ class_type=core.Content,
691
+ )
692
+ query_dict = mydict if mydict else {}
693
+
694
+ if meta.payload.schema_shortname == "report":
695
+ query_dict = query_dict["query"]
696
+ else:
697
+ query_dict["subpath"] = query_dict["query_subpath"]
698
+ query_dict.pop("query_subpath")
699
+
700
+ for param, value in record.attributes.items():
701
+ query_dict["search"] = query_dict["search"].replace(
702
+ f"${param}", str(value))
703
+
704
+ query_dict["search"] = res_sub(
705
+ r"@\w*\:({|\()?\$\w*(}|\))?", "", query_dict["search"]
706
+ )
707
+
708
+ if "offset" in record.attributes:
709
+ query_dict["offset"] = record.attributes["offset"]
710
+
711
+ if "limit" in record.attributes:
712
+ query_dict["limit"] = record.attributes["limit"]
713
+
714
+ if "from_date" in record.attributes:
715
+ query_dict["from_date"] = record.attributes["from_date"]
716
+
717
+ if "to_date" in record.attributes:
718
+ query_dict["to_date"] = record.attributes["to_date"]
719
+
720
+ filter_shortnames = record.attributes.get("filter_shortnames", [])
721
+ query_dict["filter_shortnames"] = filter_shortnames if isinstance(
722
+ filter_shortnames, list) else []
723
+
724
+ return await query_entries(api.Query(**query_dict))
725
+
726
+
727
+ @router.get("/byuuid/{uuid}", response_model_exclude_none=True)
728
+ async def get_entry_by_uuid(
729
+ uuid: str,
730
+ retrieve_json_payload: bool = False,
731
+ retrieve_attachments: bool = False,
732
+ retrieve_lock_status: bool = False
733
+ ):
734
+ return await db.get_entry_by_var(
735
+ "uuid",
736
+ uuid,
737
+ "anonymous",
738
+ retrieve_json_payload,
739
+ retrieve_attachments,
740
+ retrieve_lock_status,
741
+ )
742
+
743
+
744
+ @router.get("/byslug/{slug}", response_model_exclude_none=True)
745
+ async def get_entry_by_slug(
746
+ slug: str,
747
+ retrieve_json_payload: bool = False,
748
+ retrieve_attachments: bool = False,
749
+ retrieve_lock_status: bool = False,
750
+ ):
751
+ return await db.get_entry_by_var(
752
+ "slug",
753
+ slug,
754
+ "anonymous",
755
+ retrieve_json_payload,
756
+ retrieve_attachments,
757
+ retrieve_lock_status,
758
+ )