dmart 0.1.9__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.
- alembic/__init__.py +0 -0
- alembic/env.py +91 -0
- alembic/scripts/__init__.py +0 -0
- alembic/scripts/calculate_checksums.py +77 -0
- alembic/scripts/migration_f7a4949eed19.py +28 -0
- alembic/versions/0f3d2b1a7c21_add_authz_materialized_views.py +87 -0
- alembic/versions/10d2041b94d4_last_checksum_history.py +62 -0
- alembic/versions/1cf4e1ee3cb8_ext_permission_with_filter_fields_values.py +33 -0
- alembic/versions/26bfe19b49d4_rm_failedloginattempts.py +42 -0
- alembic/versions/3c8bca2219cc_add_otp_table.py +38 -0
- alembic/versions/6675fd9dfe42_remove_unique_from_sessions_table.py +36 -0
- alembic/versions/71bc1df82e6a_adding_user_last_login_at.py +43 -0
- alembic/versions/74288ccbd3b5_initial.py +264 -0
- alembic/versions/7520a89a8467_rm_activesession_table.py +39 -0
- alembic/versions/848b623755a4_make_created_nd_updated_at_required.py +138 -0
- alembic/versions/8640dcbebf85_add_notes_to_users.py +32 -0
- alembic/versions/91c94250232a_adding_fk_on_owner_shortname.py +104 -0
- alembic/versions/98ecd6f56f9a_ext_meta_with_owner_group_shortname.py +66 -0
- alembic/versions/9aae9138c4ef_indexing_created_at_updated_at.py +80 -0
- alembic/versions/__init__.py +0 -0
- alembic/versions/b53f916b3f6d_json_to_jsonb.py +492 -0
- alembic/versions/eb5f1ec65156_adding_user_locked_to_device.py +36 -0
- alembic/versions/f7a4949eed19_adding_query_policies_to_meta.py +60 -0
- api/__init__.py +0 -0
- api/info/__init__.py +0 -0
- api/info/router.py +109 -0
- api/managed/__init__.py +0 -0
- api/managed/router.py +1541 -0
- api/managed/utils.py +1850 -0
- api/public/__init__.py +0 -0
- api/public/router.py +758 -0
- api/qr/__init__.py +0 -0
- api/qr/router.py +108 -0
- api/user/__init__.py +0 -0
- api/user/model/__init__.py +0 -0
- api/user/model/errors.py +14 -0
- api/user/model/requests.py +165 -0
- api/user/model/responses.py +11 -0
- api/user/router.py +1401 -0
- api/user/service.py +270 -0
- bundler.py +44 -0
- config/__init__.py +0 -0
- config/channels.json +11 -0
- config/notification.json +17 -0
- data_adapters/__init__.py +0 -0
- data_adapters/adapter.py +16 -0
- data_adapters/base_data_adapter.py +467 -0
- data_adapters/file/__init__.py +0 -0
- data_adapters/file/adapter.py +2043 -0
- data_adapters/file/adapter_helpers.py +1013 -0
- data_adapters/file/archive.py +150 -0
- data_adapters/file/create_index.py +331 -0
- data_adapters/file/create_users_folders.py +52 -0
- data_adapters/file/custom_validations.py +68 -0
- data_adapters/file/drop_index.py +40 -0
- data_adapters/file/health_check.py +560 -0
- data_adapters/file/redis_services.py +1110 -0
- data_adapters/helpers.py +27 -0
- data_adapters/sql/__init__.py +0 -0
- data_adapters/sql/adapter.py +3210 -0
- data_adapters/sql/adapter_helpers.py +491 -0
- data_adapters/sql/create_tables.py +451 -0
- data_adapters/sql/create_users_folders.py +53 -0
- data_adapters/sql/db_to_json_migration.py +482 -0
- data_adapters/sql/health_check_sql.py +232 -0
- data_adapters/sql/json_to_db_migration.py +454 -0
- data_adapters/sql/update_query_policies.py +101 -0
- data_generator.py +81 -0
- dmart-0.1.9.dist-info/METADATA +64 -0
- dmart-0.1.9.dist-info/RECORD +149 -0
- dmart-0.1.9.dist-info/WHEEL +5 -0
- dmart-0.1.9.dist-info/entry_points.txt +2 -0
- dmart-0.1.9.dist-info/top_level.txt +23 -0
- dmart.py +513 -0
- get_settings.py +7 -0
- languages/__init__.py +0 -0
- languages/arabic.json +15 -0
- languages/english.json +16 -0
- languages/kurdish.json +14 -0
- languages/loader.py +13 -0
- main.py +506 -0
- migrate.py +24 -0
- models/__init__.py +0 -0
- models/api.py +203 -0
- models/core.py +597 -0
- models/enums.py +255 -0
- password_gen.py +8 -0
- plugins/__init__.py +0 -0
- plugins/action_log/__init__.py +0 -0
- plugins/action_log/plugin.py +121 -0
- plugins/admin_notification_sender/__init__.py +0 -0
- plugins/admin_notification_sender/plugin.py +124 -0
- plugins/ldap_manager/__init__.py +0 -0
- plugins/ldap_manager/plugin.py +100 -0
- plugins/local_notification/__init__.py +0 -0
- plugins/local_notification/plugin.py +123 -0
- plugins/realtime_updates_notifier/__init__.py +0 -0
- plugins/realtime_updates_notifier/plugin.py +58 -0
- plugins/redis_db_update/__init__.py +0 -0
- plugins/redis_db_update/plugin.py +188 -0
- plugins/resource_folders_creation/__init__.py +0 -0
- plugins/resource_folders_creation/plugin.py +81 -0
- plugins/system_notification_sender/__init__.py +0 -0
- plugins/system_notification_sender/plugin.py +188 -0
- plugins/update_access_controls/__init__.py +0 -0
- plugins/update_access_controls/plugin.py +9 -0
- pytests/__init__.py +0 -0
- pytests/api_user_models_erros_test.py +16 -0
- pytests/api_user_models_requests_test.py +98 -0
- pytests/archive_test.py +72 -0
- pytests/base_test.py +300 -0
- pytests/get_settings_test.py +14 -0
- pytests/json_to_db_migration_test.py +237 -0
- pytests/service_test.py +26 -0
- pytests/test_info.py +55 -0
- pytests/test_status.py +15 -0
- run_notification_campaign.py +98 -0
- scheduled_notification_handler.py +121 -0
- schema_migration.py +208 -0
- schema_modulate.py +192 -0
- set_admin_passwd.py +55 -0
- sync.py +202 -0
- utils/__init__.py +0 -0
- utils/access_control.py +306 -0
- utils/async_request.py +8 -0
- utils/exporter.py +309 -0
- utils/firebase_notifier.py +57 -0
- utils/generate_email.py +38 -0
- utils/helpers.py +352 -0
- utils/hypercorn_config.py +12 -0
- utils/internal_error_code.py +60 -0
- utils/jwt.py +124 -0
- utils/logger.py +167 -0
- utils/middleware.py +99 -0
- utils/notification.py +75 -0
- utils/password_hashing.py +16 -0
- utils/plugin_manager.py +215 -0
- utils/query_policies_helper.py +112 -0
- utils/regex.py +44 -0
- utils/repository.py +529 -0
- utils/router_helper.py +19 -0
- utils/settings.py +165 -0
- utils/sms_notifier.py +21 -0
- utils/social_sso.py +67 -0
- utils/templates/activation.html.j2 +26 -0
- utils/templates/reminder.html.j2 +17 -0
- utils/ticket_sys_utils.py +203 -0
- utils/web_notifier.py +29 -0
- websocket.py +231 -0
api/managed/router.py
ADDED
|
@@ -0,0 +1,1541 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
import traceback
|
|
9
|
+
import zipfile
|
|
10
|
+
import codecs
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from io import StringIO, BytesIO
|
|
13
|
+
from pathlib import Path as FilePath
|
|
14
|
+
from re import sub as res_sub
|
|
15
|
+
# from time import time
|
|
16
|
+
from typing import Any, Callable
|
|
17
|
+
from fastapi import APIRouter, Body, Depends, Form, Path, Query, UploadFile, status
|
|
18
|
+
from fastapi.responses import RedirectResponse, ORJSONResponse
|
|
19
|
+
from starlette.responses import FileResponse, StreamingResponse
|
|
20
|
+
|
|
21
|
+
import models.api as api
|
|
22
|
+
import models.core as core
|
|
23
|
+
import utils.regex as regex
|
|
24
|
+
import utils.repository as repository
|
|
25
|
+
from api.managed.utils import (
|
|
26
|
+
create_or_update_resource_with_payload_handler,
|
|
27
|
+
csv_entries_prepare_docs,
|
|
28
|
+
# data_asset_attachments_handler,
|
|
29
|
+
# data_asset_handler,
|
|
30
|
+
get_mime_type,
|
|
31
|
+
get_resource_content_type_from_payload_content_type,
|
|
32
|
+
handle_update_state,
|
|
33
|
+
import_resources_from_csv_handler,
|
|
34
|
+
serve_request_assign,
|
|
35
|
+
serve_request_create,
|
|
36
|
+
serve_request_delete,
|
|
37
|
+
serve_request_move,
|
|
38
|
+
serve_request_update_acl,
|
|
39
|
+
serve_request_patch,
|
|
40
|
+
serve_request_update,
|
|
41
|
+
update_state_handle_resolution,
|
|
42
|
+
iter_bytesio
|
|
43
|
+
)
|
|
44
|
+
from data_adapters.adapter import data_adapter as db
|
|
45
|
+
from models.enums import (
|
|
46
|
+
ContentType,
|
|
47
|
+
# DataAssetType,
|
|
48
|
+
LockAction,
|
|
49
|
+
RequestType,
|
|
50
|
+
ResourceType,
|
|
51
|
+
TaskType,
|
|
52
|
+
)
|
|
53
|
+
from utils.access_control import access_control
|
|
54
|
+
from utils.helpers import (
|
|
55
|
+
camel_case,
|
|
56
|
+
# csv_file_to_json,
|
|
57
|
+
flatten_dict,
|
|
58
|
+
)
|
|
59
|
+
from utils.internal_error_code import InternalErrorCode
|
|
60
|
+
from utils.jwt import GetJWTToken, JWTBearer
|
|
61
|
+
from utils.plugin_manager import plugin_manager
|
|
62
|
+
from utils.router_helper import is_space_exist
|
|
63
|
+
from utils.settings import settings
|
|
64
|
+
from data_adapters.sql.json_to_db_migration import main as json_to_db_main
|
|
65
|
+
|
|
66
|
+
router = APIRouter(default_response_class=ORJSONResponse)
|
|
67
|
+
|
|
68
|
+
@router.post(
|
|
69
|
+
"/import",
|
|
70
|
+
response_model=api.Response,
|
|
71
|
+
response_model_exclude_none=True
|
|
72
|
+
)
|
|
73
|
+
async def import_data(
|
|
74
|
+
zip_file: UploadFile,
|
|
75
|
+
extra: str|None=None,
|
|
76
|
+
owner_shortname=Depends(JWTBearer()),
|
|
77
|
+
):
|
|
78
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
79
|
+
try:
|
|
80
|
+
content = await zip_file.read()
|
|
81
|
+
|
|
82
|
+
zip_bytes = BytesIO(content)
|
|
83
|
+
|
|
84
|
+
with zipfile.ZipFile(zip_bytes, 'r') as zip_ref:
|
|
85
|
+
zip_ref.extractall(temp_dir)
|
|
86
|
+
|
|
87
|
+
original_spaces_folder = settings.spaces_folder
|
|
88
|
+
settings.spaces_folder = FilePath(temp_dir)
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
await json_to_db_main()
|
|
92
|
+
|
|
93
|
+
return api.Response(
|
|
94
|
+
status=api.Status.success,
|
|
95
|
+
attributes={"message": "Data imported successfully"}
|
|
96
|
+
)
|
|
97
|
+
finally:
|
|
98
|
+
settings.spaces_folder = original_spaces_folder
|
|
99
|
+
|
|
100
|
+
except Exception as e:
|
|
101
|
+
return api.Response(
|
|
102
|
+
status=api.Status.failed,
|
|
103
|
+
attributes={"message": f"Failed to import data: {str(e)}"}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@router.post("/export", response_class=StreamingResponse)
|
|
107
|
+
async def export_data(query: api.Query, user_shortname=Depends(JWTBearer())):
|
|
108
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
109
|
+
try:
|
|
110
|
+
original_spaces_folder = settings.spaces_folder
|
|
111
|
+
temp_spaces_folder = FilePath(temp_dir)
|
|
112
|
+
|
|
113
|
+
zip_buffer = BytesIO()
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
settings.spaces_folder = temp_spaces_folder
|
|
117
|
+
|
|
118
|
+
from data_adapters.sql.db_to_json_migration import export_data_with_query
|
|
119
|
+
await export_data_with_query(query, user_shortname)
|
|
120
|
+
|
|
121
|
+
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
|
122
|
+
for root, _, files in os.walk(temp_dir):
|
|
123
|
+
for file in files:
|
|
124
|
+
file_path = os.path.join(root, file)
|
|
125
|
+
arcname = os.path.relpath(file_path, temp_dir)
|
|
126
|
+
zip_file.write(file_path, arcname)
|
|
127
|
+
|
|
128
|
+
zip_buffer.seek(0)
|
|
129
|
+
|
|
130
|
+
response = StreamingResponse(
|
|
131
|
+
iter([zip_buffer.getvalue()]),
|
|
132
|
+
media_type="application/zip"
|
|
133
|
+
)
|
|
134
|
+
response.headers["Content-Disposition"] = "attachment; filename=export.zip"
|
|
135
|
+
|
|
136
|
+
return response
|
|
137
|
+
finally:
|
|
138
|
+
settings.spaces_folder = original_spaces_folder
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
traceback.print_exc()
|
|
142
|
+
print(f"Export error: {e}")
|
|
143
|
+
return api.Response(
|
|
144
|
+
status=api.Status.failed,
|
|
145
|
+
attributes={"message": f"Failed to export data: {str(e)}"}
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@router.post(
|
|
150
|
+
"/csv/{space_name}",
|
|
151
|
+
response_model=api.Response,
|
|
152
|
+
response_model_exclude_none=True,
|
|
153
|
+
)
|
|
154
|
+
async def generate_csv_from_report_saved_query(
|
|
155
|
+
space_name: str, record: core.Record, user_shortname=Depends(JWTBearer())
|
|
156
|
+
):
|
|
157
|
+
records = (
|
|
158
|
+
await execute(
|
|
159
|
+
space_name=space_name,
|
|
160
|
+
record=record,
|
|
161
|
+
task_type=TaskType.query,
|
|
162
|
+
logged_in_user=user_shortname,
|
|
163
|
+
)
|
|
164
|
+
).records
|
|
165
|
+
if not records:
|
|
166
|
+
raise api.Exception(
|
|
167
|
+
status.HTTP_400_BAD_REQUEST,
|
|
168
|
+
error=api.Error(
|
|
169
|
+
type="media", code=InternalErrorCode.OBJECT_NOT_FOUND, message="Request object is not available"
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
json_data = []
|
|
174
|
+
for r in records:
|
|
175
|
+
if r.attributes is None:
|
|
176
|
+
continue
|
|
177
|
+
json_data.append(flatten_dict(r.attributes))
|
|
178
|
+
|
|
179
|
+
v_path = StringIO()
|
|
180
|
+
|
|
181
|
+
keys: set = set({})
|
|
182
|
+
for row in json_data:
|
|
183
|
+
keys.update(set(row.keys()))
|
|
184
|
+
|
|
185
|
+
writer = csv.DictWriter(v_path, fieldnames=list(keys))
|
|
186
|
+
writer.writeheader()
|
|
187
|
+
writer.writerows(json_data)
|
|
188
|
+
|
|
189
|
+
response = StreamingResponse(
|
|
190
|
+
iter([v_path.getvalue()]), media_type="text/csv")
|
|
191
|
+
response.headers[
|
|
192
|
+
"Content-Disposition"
|
|
193
|
+
] = f"attachment; filename={space_name}_{record.subpath}.csv"
|
|
194
|
+
|
|
195
|
+
await plugin_manager.after_action(
|
|
196
|
+
core.Event(
|
|
197
|
+
space_name=space_name,
|
|
198
|
+
subpath=record.subpath,
|
|
199
|
+
action_type=core.ActionType.query,
|
|
200
|
+
user_shortname=user_shortname,
|
|
201
|
+
attributes={
|
|
202
|
+
"shortname": record.shortname,
|
|
203
|
+
"number_of_records": len(json_data),
|
|
204
|
+
},
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return response
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@router.post("/csv", response_model=api.Response, response_model_exclude_none=True)
|
|
212
|
+
async def csv_entries(query: api.Query, user_shortname=Depends(JWTBearer())):
|
|
213
|
+
await plugin_manager.before_action(
|
|
214
|
+
core.Event(
|
|
215
|
+
space_name=query.space_name,
|
|
216
|
+
subpath=query.subpath,
|
|
217
|
+
action_type=core.ActionType.query,
|
|
218
|
+
user_shortname=user_shortname,
|
|
219
|
+
attributes={"filter_shortnames": query.filter_shortnames},
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
query.retrieve_attachments=True
|
|
224
|
+
folder = await db.load(
|
|
225
|
+
query.space_name,
|
|
226
|
+
'/',
|
|
227
|
+
query.subpath,
|
|
228
|
+
core.Folder,
|
|
229
|
+
user_shortname,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
folder_payload = await db.load_resource_payload(
|
|
233
|
+
query.space_name,
|
|
234
|
+
"/",
|
|
235
|
+
f"{folder.shortname}.json",
|
|
236
|
+
core.Folder,
|
|
237
|
+
)
|
|
238
|
+
folder_views: list = []
|
|
239
|
+
if folder_payload:
|
|
240
|
+
folder_views = folder_payload.get("csv_columns", [])
|
|
241
|
+
if not folder_views:
|
|
242
|
+
folder_views = folder_payload.get("index_attributes", [])
|
|
243
|
+
|
|
244
|
+
keys: list = [i["name"] for i in folder_views]
|
|
245
|
+
keys_existence = dict(zip(keys, [False for _ in range(len(keys))]))
|
|
246
|
+
|
|
247
|
+
# if settings.active_data_db == 'file':
|
|
248
|
+
# _, search_res = await db.query(query, user_shortname)
|
|
249
|
+
# else:
|
|
250
|
+
# _, search_res = await db.query(query, user_shortname)
|
|
251
|
+
# docs_dicts = [search_re.model_dump() for search_re in search_res]
|
|
252
|
+
|
|
253
|
+
_, search_res = await repository.serve_query(
|
|
254
|
+
query, user_shortname,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
docs_dicts = [search_re.model_dump() for search_re in search_res]
|
|
258
|
+
|
|
259
|
+
json_data, deprecated_keys, new_keys = csv_entries_prepare_docs(query, docs_dicts, folder_views, keys_existence)
|
|
260
|
+
|
|
261
|
+
await plugin_manager.after_action(
|
|
262
|
+
core.Event(
|
|
263
|
+
space_name=query.space_name,
|
|
264
|
+
subpath=query.subpath,
|
|
265
|
+
action_type=core.ActionType.query,
|
|
266
|
+
user_shortname=user_shortname,
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
keys = [key for key in keys if keys_existence[key]]
|
|
271
|
+
v_path = StringIO()
|
|
272
|
+
v_path.write(codecs.BOM_UTF8.decode('utf-8'))
|
|
273
|
+
v_path.write(codecs.BOM_UTF8.decode('utf-8'))
|
|
274
|
+
|
|
275
|
+
list_deprecated_keys = list(deprecated_keys)
|
|
276
|
+
keys = list(filter(lambda item: item not in list_deprecated_keys, keys))
|
|
277
|
+
writer = csv.DictWriter(v_path, fieldnames=(keys + list(new_keys)))
|
|
278
|
+
writer.writeheader()
|
|
279
|
+
writer.writerows(json_data)
|
|
280
|
+
|
|
281
|
+
response = StreamingResponse(
|
|
282
|
+
iter([v_path.getvalue()]), media_type="text/csv")
|
|
283
|
+
response.headers[
|
|
284
|
+
"Content-Disposition"
|
|
285
|
+
] = f"attachment; filename={query.space_name}_{query.subpath}.csv"
|
|
286
|
+
|
|
287
|
+
return response
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# @router.post("/space", response_model=api.Response, response_model_exclude_none=True)
|
|
291
|
+
# async def serve_space(
|
|
292
|
+
# request: api.Request, owner_shortname=Depends(JWTBearer())
|
|
293
|
+
# ) -> api.Response:
|
|
294
|
+
# record = request.records[0]
|
|
295
|
+
# history_diff = {}
|
|
296
|
+
# _record = None
|
|
297
|
+
# match request.request_type:
|
|
298
|
+
# case api.RequestType.create:
|
|
299
|
+
# _record = await serve_space_create(request, record, owner_shortname)
|
|
300
|
+
#
|
|
301
|
+
# case api.RequestType.update:
|
|
302
|
+
# history_diff = await serve_space_update(request, record, owner_shortname)
|
|
303
|
+
#
|
|
304
|
+
# case api.RequestType.delete:
|
|
305
|
+
# await serve_space_delete(request, record, owner_shortname)
|
|
306
|
+
#
|
|
307
|
+
# case _:
|
|
308
|
+
# raise api.Exception(
|
|
309
|
+
# status.HTTP_400_BAD_REQUEST,
|
|
310
|
+
# api.Error(
|
|
311
|
+
# type="request",
|
|
312
|
+
# code=InternalErrorCode.UNMATCHED_DATA,
|
|
313
|
+
# message="mismatch with the information provided",
|
|
314
|
+
# ),
|
|
315
|
+
# )
|
|
316
|
+
#
|
|
317
|
+
# await db.initialize_spaces()
|
|
318
|
+
#
|
|
319
|
+
# await access_control.load_permissions_and_roles()
|
|
320
|
+
#
|
|
321
|
+
# await plugin_manager.after_action(
|
|
322
|
+
# core.Event(
|
|
323
|
+
# space_name=record.shortname,
|
|
324
|
+
# subpath=record.subpath,
|
|
325
|
+
# shortname=record.shortname,
|
|
326
|
+
# action_type=core.ActionType(request.request_type),
|
|
327
|
+
# resource_type=ResourceType.space,
|
|
328
|
+
# user_shortname=owner_shortname,
|
|
329
|
+
# attributes={"history_diff": history_diff},
|
|
330
|
+
# )
|
|
331
|
+
# )
|
|
332
|
+
#
|
|
333
|
+
# return api.Response(status=api.Status.success, records=[_record if _record else record])
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@router.post("/query", response_model=api.Response, response_model_exclude_none=True)
|
|
337
|
+
async def query_entries(
|
|
338
|
+
query: api.Query, user_shortname=Depends(JWTBearer())
|
|
339
|
+
) -> api.Response:
|
|
340
|
+
await is_space_exist(query.space_name)
|
|
341
|
+
|
|
342
|
+
await plugin_manager.before_action(
|
|
343
|
+
core.Event(
|
|
344
|
+
space_name=query.space_name,
|
|
345
|
+
subpath=query.subpath,
|
|
346
|
+
action_type=core.ActionType.query,
|
|
347
|
+
user_shortname=user_shortname,
|
|
348
|
+
attributes={"filter_shortnames": query.filter_shortnames},
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
total, records = await repository.serve_query(
|
|
353
|
+
query, user_shortname,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
await plugin_manager.after_action(
|
|
357
|
+
core.Event(
|
|
358
|
+
space_name=query.space_name,
|
|
359
|
+
subpath=query.subpath,
|
|
360
|
+
action_type=core.ActionType.query,
|
|
361
|
+
user_shortname=user_shortname,
|
|
362
|
+
)
|
|
363
|
+
)
|
|
364
|
+
return api.Response(
|
|
365
|
+
status=api.Status.success,
|
|
366
|
+
records=records,
|
|
367
|
+
attributes={"total": total, "returned": len(records)},
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@router.post("/request", response_model=api.Response, response_model_exclude_none=True)
|
|
372
|
+
async def serve_request(
|
|
373
|
+
request: api.Request,
|
|
374
|
+
token=Depends(GetJWTToken()),
|
|
375
|
+
owner_shortname=Depends(JWTBearer()),
|
|
376
|
+
is_internal: bool = False,
|
|
377
|
+
) -> api.Response:
|
|
378
|
+
for r in request.records:
|
|
379
|
+
await is_space_exist(request.space_name, not (request.request_type == RequestType.create and r.resource_type == ResourceType.space))
|
|
380
|
+
|
|
381
|
+
if not request.records:
|
|
382
|
+
raise api.Exception(
|
|
383
|
+
status.HTTP_400_BAD_REQUEST,
|
|
384
|
+
api.Error(
|
|
385
|
+
type="request",
|
|
386
|
+
code=InternalErrorCode.MISSING_DATA,
|
|
387
|
+
message="Request records cannot be empty",
|
|
388
|
+
),
|
|
389
|
+
)
|
|
390
|
+
records = []
|
|
391
|
+
failed_records = []
|
|
392
|
+
match request.request_type:
|
|
393
|
+
case api.RequestType.create:
|
|
394
|
+
records, failed_records = await serve_request_create(request, owner_shortname, token, is_internal)
|
|
395
|
+
|
|
396
|
+
case api.RequestType.update:
|
|
397
|
+
records, failed_records = await serve_request_update(request, owner_shortname)
|
|
398
|
+
|
|
399
|
+
case api.RequestType.assign:
|
|
400
|
+
records, failed_records = await serve_request_assign(request, owner_shortname)
|
|
401
|
+
|
|
402
|
+
case api.RequestType.update_acl:
|
|
403
|
+
records, failed_records = await serve_request_update_acl(request, owner_shortname)
|
|
404
|
+
|
|
405
|
+
case api.RequestType.patch:
|
|
406
|
+
records, failed_records = await serve_request_patch(request, owner_shortname)
|
|
407
|
+
|
|
408
|
+
case api.RequestType.delete:
|
|
409
|
+
records, failed_records = await serve_request_delete(request, owner_shortname)
|
|
410
|
+
|
|
411
|
+
case api.RequestType.move:
|
|
412
|
+
records, failed_records = await serve_request_move(request, owner_shortname)
|
|
413
|
+
|
|
414
|
+
if len(failed_records) == 0:
|
|
415
|
+
return api.Response(status=api.Status.success, records=records)
|
|
416
|
+
else:
|
|
417
|
+
raise api.Exception(
|
|
418
|
+
status_code=400,
|
|
419
|
+
error=api.Error(
|
|
420
|
+
type="request",
|
|
421
|
+
code=InternalErrorCode.SOMETHING_WRONG,
|
|
422
|
+
message="Something went wrong",
|
|
423
|
+
info=[{"successfull": records, "failed": failed_records}],
|
|
424
|
+
),
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
@router.put(
|
|
429
|
+
"/progress-ticket/{space_name}/{subpath:path}/{shortname}/{action}",
|
|
430
|
+
response_model=api.Response,
|
|
431
|
+
response_model_exclude_none=True,
|
|
432
|
+
)
|
|
433
|
+
async def update_state(
|
|
434
|
+
logged_in_user=Depends(JWTBearer()),
|
|
435
|
+
space_name: str = Path(..., pattern=regex.SPACENAME, examples=["data"]),
|
|
436
|
+
subpath: str = Path(..., pattern=regex.SUBPATH, examples=["/content"]),
|
|
437
|
+
shortname: str = Path(..., pattern=regex.SHORTNAME,
|
|
438
|
+
examples=["unique_shortname"]),
|
|
439
|
+
action: str = Path(..., examples=["approve"]),
|
|
440
|
+
resolution: str | None = Body(None, embed=True, examples=[
|
|
441
|
+
"Ticket state resolution"]),
|
|
442
|
+
comment: str | None = Body(None, embed=True, examples=["Nice ticket"]),
|
|
443
|
+
retrieve_lock_status: bool | None = False,
|
|
444
|
+
) -> api.Response:
|
|
445
|
+
await is_space_exist(space_name)
|
|
446
|
+
|
|
447
|
+
_user_roles = await db.get_user_roles(logged_in_user)
|
|
448
|
+
user_roles = _user_roles.keys()
|
|
449
|
+
|
|
450
|
+
await plugin_manager.before_action(
|
|
451
|
+
core.Event(
|
|
452
|
+
space_name=space_name,
|
|
453
|
+
subpath=subpath,
|
|
454
|
+
shortname=shortname,
|
|
455
|
+
action_type=core.ActionType.progress_ticket,
|
|
456
|
+
resource_type=ResourceType.ticket,
|
|
457
|
+
user_shortname=logged_in_user,
|
|
458
|
+
)
|
|
459
|
+
)
|
|
460
|
+
ticket_obj: core.Ticket = await db.load(
|
|
461
|
+
space_name=space_name,
|
|
462
|
+
subpath=subpath,
|
|
463
|
+
shortname=shortname,
|
|
464
|
+
class_type=core.Ticket,
|
|
465
|
+
user_shortname=logged_in_user,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
if not await access_control.check_access(
|
|
469
|
+
user_shortname=logged_in_user,
|
|
470
|
+
space_name=space_name,
|
|
471
|
+
subpath=subpath,
|
|
472
|
+
resource_type=ResourceType.ticket,
|
|
473
|
+
action_type=core.ActionType.progress_ticket,
|
|
474
|
+
resource_is_active=ticket_obj.is_active,
|
|
475
|
+
resource_owner_shortname=ticket_obj.owner_shortname,
|
|
476
|
+
resource_owner_group=ticket_obj.owner_group_shortname,
|
|
477
|
+
record_attributes={"state": "", "resolution_reason": ""},
|
|
478
|
+
entry_shortname=shortname
|
|
479
|
+
):
|
|
480
|
+
raise api.Exception(
|
|
481
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
482
|
+
api.Error(
|
|
483
|
+
type="request",
|
|
484
|
+
code=InternalErrorCode.NOT_ALLOWED,
|
|
485
|
+
message="You don't have permission to this action [38]",
|
|
486
|
+
),
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
workflows_data = await db.load(
|
|
490
|
+
space_name=space_name,
|
|
491
|
+
subpath="workflows",
|
|
492
|
+
shortname=ticket_obj.workflow_shortname,
|
|
493
|
+
class_type=core.Content,
|
|
494
|
+
user_shortname=logged_in_user,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
if (
|
|
498
|
+
workflows_data.payload is not None
|
|
499
|
+
and workflows_data.payload.body is not None
|
|
500
|
+
):
|
|
501
|
+
ticket_obj, workflows_payload, response, old_version_flattend = await handle_update_state(
|
|
502
|
+
space_name, logged_in_user, ticket_obj, action, user_roles
|
|
503
|
+
)
|
|
504
|
+
if resolution:
|
|
505
|
+
ticket_obj = await update_state_handle_resolution(ticket_obj, workflows_payload, response, resolution)
|
|
506
|
+
|
|
507
|
+
new_version_flattend = flatten_dict(ticket_obj.model_dump())
|
|
508
|
+
# new_version_flattend.pop("payload.body", None)
|
|
509
|
+
# new_version_flattend.update(
|
|
510
|
+
# flatten_dict({"payload.body": ticket_obj.model_dump(mode='json')}))
|
|
511
|
+
|
|
512
|
+
if comment:
|
|
513
|
+
time = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
514
|
+
new_version_flattend["comment"] = comment
|
|
515
|
+
await serve_request(
|
|
516
|
+
api.Request(
|
|
517
|
+
space_name=space_name,
|
|
518
|
+
request_type=RequestType.create,
|
|
519
|
+
records=[
|
|
520
|
+
core.Record(
|
|
521
|
+
shortname=f"c_{time}",
|
|
522
|
+
subpath=f"{subpath}/{shortname}",
|
|
523
|
+
resource_type=ResourceType.comment,
|
|
524
|
+
attributes={
|
|
525
|
+
"is_active": True,
|
|
526
|
+
"payload": {
|
|
527
|
+
"content_type": ContentType.comment,
|
|
528
|
+
"body": {
|
|
529
|
+
"body": comment,
|
|
530
|
+
"state": ticket_obj.state,
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
)
|
|
535
|
+
],
|
|
536
|
+
),
|
|
537
|
+
owner_shortname=logged_in_user,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
history_diff = await db.update(
|
|
541
|
+
space_name,
|
|
542
|
+
f"/{subpath}",
|
|
543
|
+
ticket_obj,
|
|
544
|
+
old_version_flattend,
|
|
545
|
+
new_version_flattend,
|
|
546
|
+
["state", "resolution_reason", "comment"],
|
|
547
|
+
logged_in_user,
|
|
548
|
+
retrieve_lock_status=retrieve_lock_status,
|
|
549
|
+
)
|
|
550
|
+
await plugin_manager.after_action(
|
|
551
|
+
core.Event(
|
|
552
|
+
space_name=space_name,
|
|
553
|
+
subpath=subpath,
|
|
554
|
+
shortname=shortname,
|
|
555
|
+
action_type=core.ActionType.progress_ticket,
|
|
556
|
+
resource_type=ResourceType.ticket,
|
|
557
|
+
user_shortname=logged_in_user,
|
|
558
|
+
attributes={
|
|
559
|
+
"history_diff": history_diff,
|
|
560
|
+
"state": ticket_obj.state,
|
|
561
|
+
},
|
|
562
|
+
)
|
|
563
|
+
)
|
|
564
|
+
return api.Response(status=api.Status.success)
|
|
565
|
+
|
|
566
|
+
raise api.Exception(
|
|
567
|
+
status.HTTP_400_BAD_REQUEST,
|
|
568
|
+
error=api.Error(
|
|
569
|
+
type="ticket",
|
|
570
|
+
code=InternalErrorCode.WORKFLOW_BODY_NOT_FOUND,
|
|
571
|
+
message="Workflow body not found"
|
|
572
|
+
),
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
@router.get(
|
|
577
|
+
"/payload/{resource_type}/{space_name}/{subpath:path}/{shortname}.{ext}",
|
|
578
|
+
response_model_exclude_none=True,
|
|
579
|
+
)
|
|
580
|
+
@router.get(
|
|
581
|
+
"/payload/{resource_type}/{space_name}/{subpath:path}/{shortname}.{schema_shortname}.{ext}",
|
|
582
|
+
response_model_exclude_none=True,
|
|
583
|
+
)
|
|
584
|
+
async def retrieve_entry_or_attachment_payload(
|
|
585
|
+
resource_type: ResourceType,
|
|
586
|
+
space_name: str = Path(..., pattern=regex.SPACENAME, examples=["data"]),
|
|
587
|
+
subpath: str = Path(..., pattern=regex.SUBPATH, examples=["/content"]),
|
|
588
|
+
shortname: str = Path(..., pattern=regex.SHORTNAME,
|
|
589
|
+
examples=["unique_shortname"]),
|
|
590
|
+
schema_shortname: str | None = None,
|
|
591
|
+
ext: str = Path(..., pattern=regex.EXT, examples=["png"]),
|
|
592
|
+
logged_in_user=Depends(JWTBearer()),
|
|
593
|
+
) -> Any:
|
|
594
|
+
await plugin_manager.before_action(
|
|
595
|
+
core.Event(
|
|
596
|
+
space_name=space_name,
|
|
597
|
+
subpath=subpath,
|
|
598
|
+
shortname=shortname,
|
|
599
|
+
action_type=core.ActionType.view,
|
|
600
|
+
resource_type=resource_type,
|
|
601
|
+
user_shortname=logged_in_user,
|
|
602
|
+
)
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
cls = getattr(sys.modules["models.core"], camel_case(resource_type))
|
|
606
|
+
meta = await db.load(
|
|
607
|
+
space_name=space_name,
|
|
608
|
+
subpath=subpath,
|
|
609
|
+
shortname=shortname,
|
|
610
|
+
class_type=cls,
|
|
611
|
+
user_shortname=logged_in_user,
|
|
612
|
+
schema_shortname=schema_shortname,
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
if(
|
|
616
|
+
resource_type is not ResourceType.json
|
|
617
|
+
and (meta.payload is None
|
|
618
|
+
or meta.payload.body is None
|
|
619
|
+
or meta.payload.body != f"{shortname}.{ext}")
|
|
620
|
+
):
|
|
621
|
+
raise api.Exception(
|
|
622
|
+
status.HTTP_400_BAD_REQUEST,
|
|
623
|
+
error=api.Error(
|
|
624
|
+
type="media", code=InternalErrorCode.OBJECT_NOT_FOUND, message="Request object is not available"
|
|
625
|
+
),
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
if not await access_control.check_access(
|
|
629
|
+
user_shortname=logged_in_user,
|
|
630
|
+
space_name=space_name,
|
|
631
|
+
subpath=subpath,
|
|
632
|
+
resource_type=resource_type,
|
|
633
|
+
action_type=core.ActionType.view,
|
|
634
|
+
resource_is_active=meta.is_active,
|
|
635
|
+
resource_owner_shortname=meta.owner_shortname,
|
|
636
|
+
resource_owner_group=meta.owner_group_shortname,
|
|
637
|
+
entry_shortname=meta.shortname
|
|
638
|
+
):
|
|
639
|
+
raise api.Exception(
|
|
640
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
641
|
+
api.Error(
|
|
642
|
+
type="request",
|
|
643
|
+
code=InternalErrorCode.NOT_ALLOWED,
|
|
644
|
+
message="You don't have permission to this action [39]",
|
|
645
|
+
),
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
await plugin_manager.after_action(
|
|
649
|
+
core.Event(
|
|
650
|
+
space_name=space_name,
|
|
651
|
+
subpath=subpath,
|
|
652
|
+
shortname=shortname,
|
|
653
|
+
action_type=core.ActionType.view,
|
|
654
|
+
resource_type=resource_type,
|
|
655
|
+
user_shortname=logged_in_user,
|
|
656
|
+
)
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
if settings.active_data_db == "file":
|
|
660
|
+
payload_path = db.payload_path(
|
|
661
|
+
space_name=space_name,
|
|
662
|
+
subpath=subpath,
|
|
663
|
+
class_type=cls,
|
|
664
|
+
schema_shortname=schema_shortname,
|
|
665
|
+
)
|
|
666
|
+
return FileResponse(payload_path / str(meta.payload.body))
|
|
667
|
+
|
|
668
|
+
if meta.payload.content_type == ContentType.json and isinstance(meta.payload.body, dict):
|
|
669
|
+
return api.Response(
|
|
670
|
+
status=api.Status.success,
|
|
671
|
+
attributes=meta.payload.body,
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
data: BytesIO | None = await db.get_media_attachment(space_name, subpath, shortname)
|
|
675
|
+
if data:
|
|
676
|
+
if meta.payload.body.endswith(".svg"):
|
|
677
|
+
mime_type = "image/svg+xml"
|
|
678
|
+
else:
|
|
679
|
+
mime_type = get_mime_type(meta.payload.content_type)
|
|
680
|
+
return StreamingResponse(iter_bytesio(data), media_type=mime_type)
|
|
681
|
+
return api.Response(status=api.Status.failed)
|
|
682
|
+
|
|
683
|
+
@router.post(
|
|
684
|
+
"/resource_with_payload",
|
|
685
|
+
response_model=api.Response,
|
|
686
|
+
response_model_exclude_none=True,
|
|
687
|
+
)
|
|
688
|
+
async def create_or_update_resource_with_payload(
|
|
689
|
+
payload_file: UploadFile,
|
|
690
|
+
request_record: UploadFile,
|
|
691
|
+
space_name: str = Form(..., examples=["data"]),
|
|
692
|
+
sha: str | None = Form(None, examples=["data"]),
|
|
693
|
+
owner_shortname: str = Depends(JWTBearer()),
|
|
694
|
+
):
|
|
695
|
+
# NOTE We currently make no distinction between create and update.
|
|
696
|
+
# in such case update should contain all the data every time.
|
|
697
|
+
await is_space_exist(space_name)
|
|
698
|
+
|
|
699
|
+
record = core.Record.model_validate_json(request_record.file.read())
|
|
700
|
+
|
|
701
|
+
payload_filename = payload_file.filename or ""
|
|
702
|
+
if payload_filename and not re.search(regex.EXT, os.path.splitext(payload_filename)[1][1:]):
|
|
703
|
+
raise api.Exception(
|
|
704
|
+
status.HTTP_400_BAD_REQUEST,
|
|
705
|
+
api.Error(
|
|
706
|
+
type="request",
|
|
707
|
+
code=InternalErrorCode.INVALID_DATA,
|
|
708
|
+
message=f"Invalid payload file extention, it should end with {regex.EXT}",
|
|
709
|
+
),
|
|
710
|
+
)
|
|
711
|
+
resource_content_type = get_resource_content_type_from_payload_content_type(
|
|
712
|
+
payload_file, payload_filename, record
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
await plugin_manager.before_action(
|
|
716
|
+
core.Event(
|
|
717
|
+
space_name=space_name,
|
|
718
|
+
subpath=record.subpath,
|
|
719
|
+
shortname=record.shortname,
|
|
720
|
+
action_type=core.ActionType.create,
|
|
721
|
+
schema_shortname=record.attributes.get("payload", {}).get(
|
|
722
|
+
"schema_shortname", None
|
|
723
|
+
),
|
|
724
|
+
resource_type=record.resource_type,
|
|
725
|
+
user_shortname=owner_shortname,
|
|
726
|
+
)
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
if not await access_control.check_access(
|
|
730
|
+
user_shortname=owner_shortname,
|
|
731
|
+
space_name=space_name,
|
|
732
|
+
subpath=record.subpath,
|
|
733
|
+
resource_type=record.resource_type,
|
|
734
|
+
action_type=core.ActionType.create,
|
|
735
|
+
record_attributes=record.attributes,
|
|
736
|
+
entry_shortname=record.shortname,
|
|
737
|
+
):
|
|
738
|
+
raise api.Exception(
|
|
739
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
740
|
+
api.Error(
|
|
741
|
+
type="request",
|
|
742
|
+
code=InternalErrorCode.NOT_ALLOWED,
|
|
743
|
+
message="You don't have permission to this action [10]",
|
|
744
|
+
),
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
sha1 = hashlib.sha1()
|
|
748
|
+
sha1.update(payload_file.file.read())
|
|
749
|
+
checksum = sha1.hexdigest()
|
|
750
|
+
if isinstance(sha, str) and sha != checksum:
|
|
751
|
+
raise api.Exception(
|
|
752
|
+
status.HTTP_400_BAD_REQUEST,
|
|
753
|
+
api.Error(
|
|
754
|
+
type="request",
|
|
755
|
+
code=InternalErrorCode.INVALID_DATA,
|
|
756
|
+
message="The provided file doesn't match the sha",
|
|
757
|
+
),
|
|
758
|
+
)
|
|
759
|
+
await payload_file.seek(0)
|
|
760
|
+
resource_obj, record = await create_or_update_resource_with_payload_handler(
|
|
761
|
+
record, owner_shortname, space_name, payload_file, payload_filename, checksum, sha, resource_content_type
|
|
762
|
+
)
|
|
763
|
+
try:
|
|
764
|
+
_attachement = await db.load(
|
|
765
|
+
space_name,
|
|
766
|
+
record.subpath,
|
|
767
|
+
record.shortname,
|
|
768
|
+
getattr(sys.modules["models.core"], camel_case(record.resource_type)),
|
|
769
|
+
owner_shortname
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
resource_meta = core.Meta.from_record(
|
|
773
|
+
record=record, owner_shortname=owner_shortname
|
|
774
|
+
)
|
|
775
|
+
resource_meta.payload = resource_obj.payload
|
|
776
|
+
|
|
777
|
+
if resource_obj.payload and isinstance(resource_obj.payload.body, dict):
|
|
778
|
+
await db.update_payload(
|
|
779
|
+
space_name,
|
|
780
|
+
record.subpath,
|
|
781
|
+
resource_meta,
|
|
782
|
+
resource_obj.payload.body,
|
|
783
|
+
owner_shortname
|
|
784
|
+
)
|
|
785
|
+
await db.save_payload(
|
|
786
|
+
space_name, record.subpath, resource_obj, payload_file
|
|
787
|
+
)
|
|
788
|
+
except api.Exception as e:
|
|
789
|
+
if e.error.code == InternalErrorCode.OBJECT_NOT_FOUND:
|
|
790
|
+
await db.save(space_name, record.subpath, resource_obj)
|
|
791
|
+
await db.save_payload(
|
|
792
|
+
space_name, record.subpath, resource_obj, payload_file
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
await plugin_manager.after_action(
|
|
796
|
+
core.Event(
|
|
797
|
+
space_name=space_name,
|
|
798
|
+
subpath=record.subpath,
|
|
799
|
+
shortname=record.shortname,
|
|
800
|
+
action_type=core.ActionType.create,
|
|
801
|
+
schema_shortname=record.attributes.get("payload", {}).get(
|
|
802
|
+
"schema_shortname", None
|
|
803
|
+
),
|
|
804
|
+
resource_type=record.resource_type,
|
|
805
|
+
user_shortname=owner_shortname,
|
|
806
|
+
)
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
return api.Response(
|
|
810
|
+
status=api.Status.success,
|
|
811
|
+
records=[record],
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
@router.post(
|
|
816
|
+
"/resources_from_csv/{resource_type}/{space_name}/{subpath:path}/{schema_shortname}",
|
|
817
|
+
response_model=api.Response,
|
|
818
|
+
response_model_exclude_none=True,
|
|
819
|
+
)
|
|
820
|
+
async def import_resources_from_csv(
|
|
821
|
+
resources_file: UploadFile,
|
|
822
|
+
resource_type: ResourceType,
|
|
823
|
+
space_name: str = Path(..., pattern=regex.SPACENAME, examples=["data"]),
|
|
824
|
+
subpath: str = Path(..., pattern=regex.SUBPATH, examples=["/content"]),
|
|
825
|
+
schema_shortname = None,
|
|
826
|
+
is_update: bool = False,
|
|
827
|
+
owner_shortname=Depends(JWTBearer()),
|
|
828
|
+
):
|
|
829
|
+
contents = await resources_file.read()
|
|
830
|
+
decoded = contents.decode()
|
|
831
|
+
buffer = StringIO(decoded)
|
|
832
|
+
csv_reader = csv.DictReader(buffer)
|
|
833
|
+
|
|
834
|
+
schema_content = None
|
|
835
|
+
if schema_shortname:
|
|
836
|
+
schema_content = await db.get_schema(space_name, schema_shortname, owner_shortname)
|
|
837
|
+
|
|
838
|
+
data_types_mapper: dict[str, Callable] = {
|
|
839
|
+
"integer": int,
|
|
840
|
+
"number": float,
|
|
841
|
+
"string": str,
|
|
842
|
+
"boolean": bool,
|
|
843
|
+
"object": json.loads,
|
|
844
|
+
"array": json.loads,
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
resource_cls = getattr(
|
|
848
|
+
sys.modules["models.core"], camel_case(resource_type)
|
|
849
|
+
)
|
|
850
|
+
meta_class_attributes = resource_cls.model_fields
|
|
851
|
+
failed_shortnames: list = []
|
|
852
|
+
success_count = 0
|
|
853
|
+
for row in csv_reader:
|
|
854
|
+
payload_object, meta_object, shortname = await import_resources_from_csv_handler(
|
|
855
|
+
row,
|
|
856
|
+
meta_class_attributes,
|
|
857
|
+
schema_content,
|
|
858
|
+
data_types_mapper,
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
if "is_active" not in meta_object:
|
|
862
|
+
meta_object["is_active"] = True
|
|
863
|
+
|
|
864
|
+
attributes = meta_object
|
|
865
|
+
|
|
866
|
+
attributes["payload"] = {
|
|
867
|
+
"content_type": ContentType.json,
|
|
868
|
+
"body": payload_object,
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if schema_shortname:
|
|
872
|
+
attributes["payload"]["schema_shortname"] = schema_shortname
|
|
873
|
+
|
|
874
|
+
record = core.Record(
|
|
875
|
+
resource_type=resource_type,
|
|
876
|
+
shortname=shortname,
|
|
877
|
+
subpath=subpath,
|
|
878
|
+
attributes=attributes,
|
|
879
|
+
)
|
|
880
|
+
try:
|
|
881
|
+
await serve_request(
|
|
882
|
+
request=api.Request(
|
|
883
|
+
space_name=space_name,
|
|
884
|
+
request_type=RequestType.update if is_update else RequestType.create,
|
|
885
|
+
records=[record],
|
|
886
|
+
),
|
|
887
|
+
owner_shortname=owner_shortname,
|
|
888
|
+
is_internal=True,
|
|
889
|
+
)
|
|
890
|
+
success_count += 1
|
|
891
|
+
except api.Exception as e:
|
|
892
|
+
err = {shortname: e.__str__()}
|
|
893
|
+
if hasattr(e, "error"):
|
|
894
|
+
err["error"] = str(e.error)
|
|
895
|
+
failed_shortnames.append(err)
|
|
896
|
+
except Exception as e:
|
|
897
|
+
failed_shortnames.append({shortname: e.__str__()})
|
|
898
|
+
|
|
899
|
+
return api.Response(
|
|
900
|
+
status=api.Status.success,
|
|
901
|
+
attributes={
|
|
902
|
+
"success_count": success_count,
|
|
903
|
+
"failed_shortnames": failed_shortnames,
|
|
904
|
+
},
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
@router.get(
|
|
909
|
+
"/entry/{resource_type}/{space_name}/{subpath:path}/{shortname}",
|
|
910
|
+
response_model_exclude_none=True,
|
|
911
|
+
)
|
|
912
|
+
async def retrieve_entry_meta(
|
|
913
|
+
resource_type: ResourceType,
|
|
914
|
+
space_name: str = Path(..., pattern=regex.SPACENAME, examples=["data"]),
|
|
915
|
+
subpath: str = Path(..., pattern=regex.SUBPATH, examples=["/content"]),
|
|
916
|
+
shortname: str = Path(..., pattern=regex.SHORTNAME,
|
|
917
|
+
examples=["unique_shortname"]),
|
|
918
|
+
retrieve_json_payload: bool = False,
|
|
919
|
+
retrieve_attachments: bool = False,
|
|
920
|
+
filter_attachments_types: list = Query(default=[], examples=["media", "comment", "json"]),
|
|
921
|
+
validate_schema: bool = True,
|
|
922
|
+
logged_in_user=Depends(JWTBearer()),
|
|
923
|
+
) -> dict[str, Any]:
|
|
924
|
+
if subpath == settings.root_subpath_mw:
|
|
925
|
+
subpath = "/"
|
|
926
|
+
|
|
927
|
+
await plugin_manager.before_action(
|
|
928
|
+
core.Event(
|
|
929
|
+
space_name=space_name,
|
|
930
|
+
subpath=subpath,
|
|
931
|
+
shortname=shortname,
|
|
932
|
+
action_type=core.ActionType.view,
|
|
933
|
+
resource_type=resource_type,
|
|
934
|
+
user_shortname=logged_in_user,
|
|
935
|
+
)
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
resource_class = getattr(
|
|
939
|
+
sys.modules["models.core"], camel_case(resource_type)
|
|
940
|
+
)
|
|
941
|
+
meta: core.Meta = await db.load(
|
|
942
|
+
space_name=space_name,
|
|
943
|
+
subpath=subpath,
|
|
944
|
+
shortname=shortname,
|
|
945
|
+
class_type=resource_class,
|
|
946
|
+
user_shortname=logged_in_user,
|
|
947
|
+
)
|
|
948
|
+
if meta is None:
|
|
949
|
+
raise api.Exception(
|
|
950
|
+
status.HTTP_400_BAD_REQUEST,
|
|
951
|
+
error=api.Error(
|
|
952
|
+
type="media",
|
|
953
|
+
code=InternalErrorCode.OBJECT_NOT_FOUND,
|
|
954
|
+
message="Request object is not available"
|
|
955
|
+
),
|
|
956
|
+
)
|
|
957
|
+
|
|
958
|
+
if not await access_control.check_access(
|
|
959
|
+
user_shortname=logged_in_user,
|
|
960
|
+
space_name=space_name,
|
|
961
|
+
subpath=subpath,
|
|
962
|
+
resource_type=resource_type,
|
|
963
|
+
action_type=core.ActionType.view,
|
|
964
|
+
resource_is_active=meta.is_active,
|
|
965
|
+
resource_owner_shortname=meta.owner_shortname,
|
|
966
|
+
resource_owner_group=meta.owner_group_shortname,
|
|
967
|
+
entry_shortname=meta.shortname
|
|
968
|
+
):
|
|
969
|
+
raise api.Exception(
|
|
970
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
971
|
+
api.Error(
|
|
972
|
+
type="request",
|
|
973
|
+
code=InternalErrorCode.NOT_ALLOWED,
|
|
974
|
+
message="You don't have permission to this action [41]",
|
|
975
|
+
)
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
attachments = {}
|
|
979
|
+
entry_path = (
|
|
980
|
+
settings.spaces_folder
|
|
981
|
+
/ f"{space_name}/{subpath}/.dm/{shortname}"
|
|
982
|
+
)
|
|
983
|
+
if retrieve_attachments:
|
|
984
|
+
attachments = await db.get_entry_attachments(
|
|
985
|
+
subpath=subpath,
|
|
986
|
+
attachments_path=entry_path,
|
|
987
|
+
retrieve_json_payload=retrieve_json_payload,
|
|
988
|
+
filter_types=filter_attachments_types,
|
|
989
|
+
)
|
|
990
|
+
|
|
991
|
+
if (
|
|
992
|
+
not retrieve_json_payload
|
|
993
|
+
or not meta.payload
|
|
994
|
+
or not meta.payload.body
|
|
995
|
+
or not isinstance(meta.payload.body, str)
|
|
996
|
+
or meta.payload.content_type != ContentType.json
|
|
997
|
+
):
|
|
998
|
+
# TODO
|
|
999
|
+
# include locked before returning the dictionary
|
|
1000
|
+
return {**meta.model_dump(exclude_none=True), "attachments": attachments}
|
|
1001
|
+
|
|
1002
|
+
payload_body = await db.load_resource_payload(
|
|
1003
|
+
space_name=space_name,
|
|
1004
|
+
subpath=subpath,
|
|
1005
|
+
filename=meta.payload.body,
|
|
1006
|
+
class_type=resource_class,
|
|
1007
|
+
)
|
|
1008
|
+
|
|
1009
|
+
if meta.payload and meta.payload.schema_shortname and validate_schema and payload_body:
|
|
1010
|
+
await db.validate_payload_with_schema(
|
|
1011
|
+
payload_data=payload_body,
|
|
1012
|
+
space_name=space_name,
|
|
1013
|
+
schema_shortname=meta.payload.schema_shortname,
|
|
1014
|
+
)
|
|
1015
|
+
|
|
1016
|
+
if payload_body is not None:
|
|
1017
|
+
meta.payload.body = payload_body
|
|
1018
|
+
await plugin_manager.after_action(
|
|
1019
|
+
core.Event(
|
|
1020
|
+
space_name=space_name,
|
|
1021
|
+
subpath=subpath,
|
|
1022
|
+
shortname=shortname,
|
|
1023
|
+
action_type=core.ActionType.view,
|
|
1024
|
+
resource_type=resource_type,
|
|
1025
|
+
user_shortname=logged_in_user,
|
|
1026
|
+
)
|
|
1027
|
+
)
|
|
1028
|
+
# TODO
|
|
1029
|
+
# include locked before returning the dictionary
|
|
1030
|
+
return {**meta.model_dump(exclude_none=True), "attachments": attachments}
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
@router.get("/byuuid/{uuid}", response_model_exclude_none=True)
|
|
1034
|
+
async def get_entry_by_uuid(
|
|
1035
|
+
uuid: str,
|
|
1036
|
+
retrieve_json_payload: bool = False,
|
|
1037
|
+
retrieve_attachments: bool = False,
|
|
1038
|
+
retrieve_lock_status: bool = False,
|
|
1039
|
+
logged_in_user=Depends(JWTBearer()),
|
|
1040
|
+
):
|
|
1041
|
+
return await db.get_entry_by_var(
|
|
1042
|
+
"uuid",
|
|
1043
|
+
uuid,
|
|
1044
|
+
logged_in_user,
|
|
1045
|
+
retrieve_json_payload,
|
|
1046
|
+
retrieve_attachments,
|
|
1047
|
+
retrieve_lock_status,
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
@router.get("/byslug/{slug}", response_model_exclude_none=True)
|
|
1052
|
+
async def get_entry_by_slug(
|
|
1053
|
+
slug: str,
|
|
1054
|
+
retrieve_json_payload: bool = False,
|
|
1055
|
+
retrieve_attachments: bool = False,
|
|
1056
|
+
retrieve_lock_status: bool = False,
|
|
1057
|
+
logged_in_user=Depends(JWTBearer()),
|
|
1058
|
+
):
|
|
1059
|
+
return await db.get_entry_by_var(
|
|
1060
|
+
"slug",
|
|
1061
|
+
slug,
|
|
1062
|
+
logged_in_user,
|
|
1063
|
+
retrieve_json_payload,
|
|
1064
|
+
retrieve_attachments,
|
|
1065
|
+
retrieve_lock_status,
|
|
1066
|
+
)
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
@router.get("/health/{health_type}/{space_name}", response_model_exclude_none=True)
|
|
1070
|
+
async def get_space_report(
|
|
1071
|
+
space_name: str = Path(..., pattern=regex.SPACENAME, examples=["data"]),
|
|
1072
|
+
health_type: str = Path(..., examples=["soft", "hard"]),
|
|
1073
|
+
logged_in_user=Depends(JWTBearer()),
|
|
1074
|
+
):
|
|
1075
|
+
if logged_in_user != "dmart":
|
|
1076
|
+
raise api.Exception(
|
|
1077
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
1078
|
+
error=api.Error(
|
|
1079
|
+
type="access",
|
|
1080
|
+
code=InternalErrorCode.NOT_ALLOWED,
|
|
1081
|
+
message="You don't have permission to this action [23]"
|
|
1082
|
+
),
|
|
1083
|
+
)
|
|
1084
|
+
|
|
1085
|
+
await is_space_exist(space_name)
|
|
1086
|
+
|
|
1087
|
+
if health_type not in ["soft", "hard"]:
|
|
1088
|
+
raise api.Exception(
|
|
1089
|
+
status.HTTP_400_BAD_REQUEST,
|
|
1090
|
+
error=api.Error(
|
|
1091
|
+
type="media",
|
|
1092
|
+
code=InternalErrorCode.INVALID_HEALTH_CHECK,
|
|
1093
|
+
message="Invalid health check type"
|
|
1094
|
+
),
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
os.system(
|
|
1098
|
+
f"./health_check.py -t {health_type} -s {space_name} &")
|
|
1099
|
+
return api.Response(
|
|
1100
|
+
status=api.Status.success,
|
|
1101
|
+
)
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
@router.put("/lock/{resource_type}/{space_name}/{subpath:path}/{shortname}")
|
|
1105
|
+
async def lock_entry(
|
|
1106
|
+
space_name: str = Path(..., pattern=regex.SPACENAME),
|
|
1107
|
+
subpath: str = Path(..., pattern=regex.SUBPATH),
|
|
1108
|
+
shortname: str = Path(..., pattern=regex.SHORTNAME),
|
|
1109
|
+
resource_type: ResourceType | None = ResourceType.ticket,
|
|
1110
|
+
logged_in_user=Depends(JWTBearer()),
|
|
1111
|
+
):
|
|
1112
|
+
await plugin_manager.before_action(
|
|
1113
|
+
core.Event(
|
|
1114
|
+
space_name=space_name,
|
|
1115
|
+
subpath=subpath,
|
|
1116
|
+
shortname=shortname,
|
|
1117
|
+
action_type=core.ActionType.lock,
|
|
1118
|
+
user_shortname=logged_in_user,
|
|
1119
|
+
)
|
|
1120
|
+
)
|
|
1121
|
+
|
|
1122
|
+
if resource_type == ResourceType.ticket:
|
|
1123
|
+
cls = getattr(sys.modules["models.core"], camel_case(resource_type))
|
|
1124
|
+
|
|
1125
|
+
mm = await db.load(
|
|
1126
|
+
space_name=space_name,
|
|
1127
|
+
subpath=subpath,
|
|
1128
|
+
shortname=shortname,
|
|
1129
|
+
class_type=cls,
|
|
1130
|
+
user_shortname=logged_in_user,
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
meta = mm
|
|
1134
|
+
meta.collaborators = meta.collaborators if meta.collaborators else {}
|
|
1135
|
+
if meta.collaborators.get("processed_by") != logged_in_user:
|
|
1136
|
+
meta.collaborators["processed_by"] = logged_in_user
|
|
1137
|
+
request = api.Request(
|
|
1138
|
+
space_name=space_name,
|
|
1139
|
+
request_type=api.RequestType.update,
|
|
1140
|
+
records=[
|
|
1141
|
+
core.Record(
|
|
1142
|
+
resource_type=resource_type,
|
|
1143
|
+
subpath=subpath,
|
|
1144
|
+
shortname=shortname,
|
|
1145
|
+
attributes={
|
|
1146
|
+
"is_active": True,
|
|
1147
|
+
"collaborators": meta.collaborators,
|
|
1148
|
+
},
|
|
1149
|
+
)
|
|
1150
|
+
],
|
|
1151
|
+
)
|
|
1152
|
+
await serve_request(request=request, owner_shortname=logged_in_user)
|
|
1153
|
+
|
|
1154
|
+
# if lock file is doesn't exist
|
|
1155
|
+
# elif lock file exit but lock_period expired
|
|
1156
|
+
# elif lock file exist and lock_period isn't expired but the owner want to extend the lock
|
|
1157
|
+
|
|
1158
|
+
lock_type = await db.lock_handler(
|
|
1159
|
+
space_name,
|
|
1160
|
+
subpath,
|
|
1161
|
+
shortname,
|
|
1162
|
+
logged_in_user,
|
|
1163
|
+
LockAction.lock
|
|
1164
|
+
)
|
|
1165
|
+
|
|
1166
|
+
await db.store_entry_diff(
|
|
1167
|
+
space_name,
|
|
1168
|
+
"/" + subpath,
|
|
1169
|
+
shortname,
|
|
1170
|
+
logged_in_user,
|
|
1171
|
+
{},
|
|
1172
|
+
{"lock_type": lock_type},
|
|
1173
|
+
["lock_type"],
|
|
1174
|
+
core.Content,
|
|
1175
|
+
)
|
|
1176
|
+
|
|
1177
|
+
await plugin_manager.after_action(
|
|
1178
|
+
core.Event(
|
|
1179
|
+
space_name=space_name,
|
|
1180
|
+
subpath=subpath,
|
|
1181
|
+
shortname=shortname,
|
|
1182
|
+
resource_type=ResourceType.ticket,
|
|
1183
|
+
action_type=core.ActionType.lock,
|
|
1184
|
+
user_shortname=logged_in_user,
|
|
1185
|
+
)
|
|
1186
|
+
)
|
|
1187
|
+
|
|
1188
|
+
return api.Response(
|
|
1189
|
+
status=api.Status.success,
|
|
1190
|
+
attributes={
|
|
1191
|
+
"message": f"Successfully locked the entry for {settings.lock_period} seconds",
|
|
1192
|
+
"lock_period": settings.lock_period,
|
|
1193
|
+
},
|
|
1194
|
+
)
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
@router.delete("/lock/{space_name}/{subpath:path}/{shortname}")
|
|
1198
|
+
async def cancel_lock(
|
|
1199
|
+
space_name: str = Path(..., pattern=regex.SPACENAME),
|
|
1200
|
+
subpath: str = Path(..., pattern=regex.SUBPATH),
|
|
1201
|
+
shortname: str = Path(..., pattern=regex.SHORTNAME),
|
|
1202
|
+
logged_in_user=Depends(JWTBearer()),
|
|
1203
|
+
):
|
|
1204
|
+
lock_payload = await db.lock_handler(space_name, subpath, shortname, logged_in_user, LockAction.fetch)
|
|
1205
|
+
|
|
1206
|
+
if not lock_payload or lock_payload["owner_shortname"] != logged_in_user:
|
|
1207
|
+
raise api.Exception(
|
|
1208
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
1209
|
+
error=api.Error(
|
|
1210
|
+
type="lock",
|
|
1211
|
+
code=InternalErrorCode.LOCK_UNAVAILABLE,
|
|
1212
|
+
message="Lock does not exist or you have no access",
|
|
1213
|
+
),
|
|
1214
|
+
)
|
|
1215
|
+
|
|
1216
|
+
await plugin_manager.before_action(
|
|
1217
|
+
core.Event(
|
|
1218
|
+
space_name=space_name,
|
|
1219
|
+
subpath=subpath,
|
|
1220
|
+
shortname=shortname,
|
|
1221
|
+
action_type=core.ActionType.unlock,
|
|
1222
|
+
user_shortname=logged_in_user,
|
|
1223
|
+
)
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1226
|
+
await db.lock_handler(
|
|
1227
|
+
space_name,
|
|
1228
|
+
subpath,
|
|
1229
|
+
shortname,
|
|
1230
|
+
logged_in_user,
|
|
1231
|
+
LockAction.unlock
|
|
1232
|
+
)
|
|
1233
|
+
|
|
1234
|
+
await db.store_entry_diff(
|
|
1235
|
+
space_name,
|
|
1236
|
+
"/" + subpath,
|
|
1237
|
+
shortname,
|
|
1238
|
+
logged_in_user,
|
|
1239
|
+
{},
|
|
1240
|
+
{"lock_type": LockAction.cancel},
|
|
1241
|
+
["lock_type"],
|
|
1242
|
+
core.Content,
|
|
1243
|
+
)
|
|
1244
|
+
|
|
1245
|
+
await plugin_manager.after_action(
|
|
1246
|
+
core.Event(
|
|
1247
|
+
space_name=space_name,
|
|
1248
|
+
subpath=subpath,
|
|
1249
|
+
shortname=shortname,
|
|
1250
|
+
resource_type=ResourceType.ticket,
|
|
1251
|
+
action_type=core.ActionType.unlock,
|
|
1252
|
+
user_shortname=logged_in_user,
|
|
1253
|
+
)
|
|
1254
|
+
)
|
|
1255
|
+
|
|
1256
|
+
return api.Response(
|
|
1257
|
+
status=api.Status.success,
|
|
1258
|
+
attributes={"message": "Entry unlocked successfully"},
|
|
1259
|
+
)
|
|
1260
|
+
|
|
1261
|
+
|
|
1262
|
+
@router.get("/reload-security-data")
|
|
1263
|
+
async def reload_security_data(_=Depends(JWTBearer())):
|
|
1264
|
+
if settings.active_data_db == "file":
|
|
1265
|
+
await access_control.load_permissions_and_roles()
|
|
1266
|
+
|
|
1267
|
+
return api.Response(status=api.Status.success)
|
|
1268
|
+
|
|
1269
|
+
|
|
1270
|
+
@router.post("/excute/{task_type}/{space_name}")
|
|
1271
|
+
async def execute(
|
|
1272
|
+
space_name: str,
|
|
1273
|
+
task_type: TaskType,
|
|
1274
|
+
record: core.Record,
|
|
1275
|
+
logged_in_user=Depends(JWTBearer()),
|
|
1276
|
+
):
|
|
1277
|
+
task_type = task_type
|
|
1278
|
+
meta = await db.load(
|
|
1279
|
+
space_name=space_name,
|
|
1280
|
+
subpath=record.subpath,
|
|
1281
|
+
shortname=record.shortname,
|
|
1282
|
+
class_type=core.Content,
|
|
1283
|
+
user_shortname=logged_in_user,
|
|
1284
|
+
)
|
|
1285
|
+
|
|
1286
|
+
if (
|
|
1287
|
+
meta.payload is None
|
|
1288
|
+
or not isinstance(meta.payload.body, str)
|
|
1289
|
+
or not str(meta.payload.body).endswith(".json")
|
|
1290
|
+
):
|
|
1291
|
+
raise api.Exception(
|
|
1292
|
+
status.HTTP_400_BAD_REQUEST,
|
|
1293
|
+
error=api.Error(
|
|
1294
|
+
type="media", code=InternalErrorCode.OBJECT_NOT_FOUND, message="Request object is not available"
|
|
1295
|
+
),
|
|
1296
|
+
)
|
|
1297
|
+
|
|
1298
|
+
mydict = await db.load_resource_payload(
|
|
1299
|
+
space_name=space_name,
|
|
1300
|
+
subpath=record.subpath,
|
|
1301
|
+
filename=str(meta.payload.body),
|
|
1302
|
+
class_type=core.Content,
|
|
1303
|
+
)
|
|
1304
|
+
|
|
1305
|
+
query_dict = mydict if mydict else {}
|
|
1306
|
+
|
|
1307
|
+
if meta.payload.schema_shortname == "report":
|
|
1308
|
+
query_dict = query_dict["query"]
|
|
1309
|
+
else:
|
|
1310
|
+
query_dict["subpath"] = query_dict["query_subpath"]
|
|
1311
|
+
query_dict.pop("query_subpath")
|
|
1312
|
+
|
|
1313
|
+
for param, value in record.attributes.items():
|
|
1314
|
+
query_dict["search"] = query_dict["search"].replace(
|
|
1315
|
+
f"${param}", str(value))
|
|
1316
|
+
|
|
1317
|
+
query_dict["search"] = res_sub(
|
|
1318
|
+
r"@\w*\:({|\()?\$\w*(}|\))?", "", query_dict["search"]
|
|
1319
|
+
)
|
|
1320
|
+
|
|
1321
|
+
if "offset" in record.attributes:
|
|
1322
|
+
query_dict["offset"] = record.attributes["offset"]
|
|
1323
|
+
|
|
1324
|
+
if "limit" in record.attributes:
|
|
1325
|
+
query_dict["limit"] = record.attributes["limit"]
|
|
1326
|
+
|
|
1327
|
+
if "from_date" in record.attributes:
|
|
1328
|
+
query_dict["from_date"] = record.attributes["from_date"]
|
|
1329
|
+
|
|
1330
|
+
if "to_date" in record.attributes:
|
|
1331
|
+
query_dict["to_date"] = record.attributes["to_date"]
|
|
1332
|
+
|
|
1333
|
+
return await query_entries(
|
|
1334
|
+
query=api.Query(**query_dict), user_shortname=logged_in_user
|
|
1335
|
+
)
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
@router.get(
|
|
1339
|
+
"/s/{token}",
|
|
1340
|
+
response_model_exclude_none=True,
|
|
1341
|
+
)
|
|
1342
|
+
async def shoting_url(
|
|
1343
|
+
token: str,
|
|
1344
|
+
):
|
|
1345
|
+
if url := await db.get_url_shortner(token):
|
|
1346
|
+
return RedirectResponse(url=url)
|
|
1347
|
+
|
|
1348
|
+
return RedirectResponse(url="/web")
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
@router.post(
|
|
1352
|
+
"/apply-alteration/{space_name}/{alteration_name}", response_model_exclude_none=True
|
|
1353
|
+
)
|
|
1354
|
+
async def apply_alteration(
|
|
1355
|
+
space_name: str,
|
|
1356
|
+
alteration_name: str,
|
|
1357
|
+
on_entry: core.Record,
|
|
1358
|
+
logged_in_user=Depends(JWTBearer()),
|
|
1359
|
+
):
|
|
1360
|
+
alteration_meta = await db.load(
|
|
1361
|
+
space_name=space_name,
|
|
1362
|
+
subpath=f"{on_entry.subpath}/{on_entry.shortname}",
|
|
1363
|
+
shortname=alteration_name,
|
|
1364
|
+
class_type=core.Alteration,
|
|
1365
|
+
user_shortname=logged_in_user,
|
|
1366
|
+
)
|
|
1367
|
+
entry_meta: core.Meta = await db.load(
|
|
1368
|
+
space_name=space_name,
|
|
1369
|
+
subpath=f"{on_entry.subpath}",
|
|
1370
|
+
shortname=on_entry.shortname,
|
|
1371
|
+
class_type=getattr(
|
|
1372
|
+
sys.modules["models.core"], camel_case(on_entry.resource_type)
|
|
1373
|
+
),
|
|
1374
|
+
user_shortname=logged_in_user,
|
|
1375
|
+
)
|
|
1376
|
+
|
|
1377
|
+
record: core.Record = entry_meta.to_record(
|
|
1378
|
+
on_entry.subpath, on_entry.shortname, []
|
|
1379
|
+
)
|
|
1380
|
+
record.attributes["payload"] = record.attributes["payload"].__dict__
|
|
1381
|
+
record.attributes["payload"]["body"] = alteration_meta.requested_update
|
|
1382
|
+
|
|
1383
|
+
response = await serve_request(
|
|
1384
|
+
request=api.Request(
|
|
1385
|
+
space_name=space_name, request_type=RequestType.update, records=[
|
|
1386
|
+
record]
|
|
1387
|
+
),
|
|
1388
|
+
owner_shortname=logged_in_user,
|
|
1389
|
+
)
|
|
1390
|
+
|
|
1391
|
+
await db.delete(
|
|
1392
|
+
space_name=space_name,
|
|
1393
|
+
subpath=f"{on_entry.subpath}/{on_entry.shortname}",
|
|
1394
|
+
meta=alteration_meta,
|
|
1395
|
+
user_shortname=logged_in_user,
|
|
1396
|
+
retrieve_lock_status=on_entry.retrieve_lock_status,
|
|
1397
|
+
)
|
|
1398
|
+
return response
|
|
1399
|
+
|
|
1400
|
+
"""
|
|
1401
|
+
@router.post("/data-asset")
|
|
1402
|
+
async def data_asset(
|
|
1403
|
+
query: api.DataAssetQuery,
|
|
1404
|
+
_=Depends(JWTBearer()),
|
|
1405
|
+
):
|
|
1406
|
+
try:
|
|
1407
|
+
duckdb = __import__("duckdb")
|
|
1408
|
+
except ModuleNotFoundError:
|
|
1409
|
+
raise api.Exception(
|
|
1410
|
+
status.HTTP_400_BAD_REQUEST,
|
|
1411
|
+
api.Error(
|
|
1412
|
+
type="request",
|
|
1413
|
+
code=InternalErrorCode.NOT_ALLOWED,
|
|
1414
|
+
message="duckdb is not installed!",
|
|
1415
|
+
),
|
|
1416
|
+
)
|
|
1417
|
+
|
|
1418
|
+
attachments: dict[str, list[core.Record]] = await db.get_entry_attachments(
|
|
1419
|
+
subpath=f"{query.subpath}/{query.shortname}",
|
|
1420
|
+
attachments_path=(
|
|
1421
|
+
settings.spaces_folder
|
|
1422
|
+
/ f"{query.space_name}/{query.subpath}/.dm/{query.shortname}"
|
|
1423
|
+
),
|
|
1424
|
+
filter_types=[query.data_asset_type],
|
|
1425
|
+
filter_shortnames=query.filter_data_assets
|
|
1426
|
+
)
|
|
1427
|
+
files_paths: list[FilePath] = await data_asset_attachments_handler(query, attachments)
|
|
1428
|
+
if not files_paths:
|
|
1429
|
+
raise api.Exception(
|
|
1430
|
+
status.HTTP_400_BAD_REQUEST,
|
|
1431
|
+
api.Error(
|
|
1432
|
+
type="request",
|
|
1433
|
+
code=InternalErrorCode.OBJECT_NOT_FOUND,
|
|
1434
|
+
message="No data asset attachments found for this entry",
|
|
1435
|
+
),
|
|
1436
|
+
)
|
|
1437
|
+
|
|
1438
|
+
if query.data_asset_type in [DataAssetType.sqlite, DataAssetType.duckdb]:
|
|
1439
|
+
conn: duckdb.DuckDBPyConnection = duckdb.connect(str(files_paths[0]))
|
|
1440
|
+
else:
|
|
1441
|
+
conn = duckdb.connect(":default:")
|
|
1442
|
+
await data_asset_handler(conn, query, files_paths, attachments)
|
|
1443
|
+
|
|
1444
|
+
data: duckdb.DuckDBPyRelation = conn.sql(query=query.query_string)
|
|
1445
|
+
|
|
1446
|
+
temp_file = f"temp_file_from_duckdb_{int(round(time() * 1000))}.csv"
|
|
1447
|
+
data.write_csv(file_name=temp_file)
|
|
1448
|
+
data_objects: list[dict[str, Any]] = await csv_file_to_json(FilePath(temp_file))
|
|
1449
|
+
os.remove(temp_file)
|
|
1450
|
+
|
|
1451
|
+
return data_objects
|
|
1452
|
+
|
|
1453
|
+
|
|
1454
|
+
@router.get("/data-asset")
|
|
1455
|
+
async def data_asset_single(
|
|
1456
|
+
resource_type: ResourceType,
|
|
1457
|
+
space_name: str = Path(..., pattern=regex.SPACENAME, examples=["data"]),
|
|
1458
|
+
subpath: str = Path(..., pattern=regex.SUBPATH, examples=["/content"]),
|
|
1459
|
+
shortname: str = Path(..., pattern=regex.SHORTNAME,
|
|
1460
|
+
examples=["unique_shortname"]),
|
|
1461
|
+
schema_shortname: str | None = None,
|
|
1462
|
+
ext: str = Path(..., pattern=regex.EXT, examples=["png"]),
|
|
1463
|
+
logged_in_user=Depends(JWTBearer()),
|
|
1464
|
+
) -> StreamingResponse:
|
|
1465
|
+
await plugin_manager.before_action(
|
|
1466
|
+
core.Event(
|
|
1467
|
+
space_name=space_name,
|
|
1468
|
+
subpath=subpath,
|
|
1469
|
+
shortname=shortname,
|
|
1470
|
+
action_type=core.ActionType.view,
|
|
1471
|
+
resource_type=resource_type,
|
|
1472
|
+
user_shortname=logged_in_user,
|
|
1473
|
+
)
|
|
1474
|
+
)
|
|
1475
|
+
|
|
1476
|
+
cls = getattr(sys.modules["models.core"], camel_case(resource_type))
|
|
1477
|
+
meta: core.Meta = await db.load(
|
|
1478
|
+
space_name=space_name,
|
|
1479
|
+
subpath=subpath,
|
|
1480
|
+
shortname=shortname,
|
|
1481
|
+
class_type=cls,
|
|
1482
|
+
user_shortname=logged_in_user,
|
|
1483
|
+
schema_shortname=schema_shortname,
|
|
1484
|
+
)
|
|
1485
|
+
if (
|
|
1486
|
+
meta.payload is None
|
|
1487
|
+
or meta.payload.body is None
|
|
1488
|
+
or meta.payload.body != f"{shortname}.{ext}"
|
|
1489
|
+
):
|
|
1490
|
+
raise api.Exception(
|
|
1491
|
+
status.HTTP_400_BAD_REQUEST,
|
|
1492
|
+
error=api.Error(
|
|
1493
|
+
type="media", code=InternalErrorCode.OBJECT_NOT_FOUND, message="Request object is not available"
|
|
1494
|
+
),
|
|
1495
|
+
)
|
|
1496
|
+
|
|
1497
|
+
if not await access_control.check_access(
|
|
1498
|
+
user_shortname=logged_in_user,
|
|
1499
|
+
space_name=space_name,
|
|
1500
|
+
subpath=subpath,
|
|
1501
|
+
resource_type=resource_type,
|
|
1502
|
+
action_type=core.ActionType.view,
|
|
1503
|
+
resource_is_active=meta.is_active,
|
|
1504
|
+
resource_owner_shortname=meta.owner_shortname,
|
|
1505
|
+
resource_owner_group=meta.owner_group_shortname,
|
|
1506
|
+
entry_shortname=meta.shortname
|
|
1507
|
+
):
|
|
1508
|
+
raise api.Exception(
|
|
1509
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
1510
|
+
api.Error(
|
|
1511
|
+
type="request",
|
|
1512
|
+
code=InternalErrorCode.NOT_ALLOWED,
|
|
1513
|
+
message="You don't have permission to this action [9]",
|
|
1514
|
+
),
|
|
1515
|
+
)
|
|
1516
|
+
|
|
1517
|
+
payload_path = db.payload_path(
|
|
1518
|
+
space_name=space_name,
|
|
1519
|
+
subpath=subpath,
|
|
1520
|
+
class_type=cls,
|
|
1521
|
+
schema_shortname=schema_shortname,
|
|
1522
|
+
)
|
|
1523
|
+
await plugin_manager.after_action(
|
|
1524
|
+
core.Event(
|
|
1525
|
+
space_name=space_name,
|
|
1526
|
+
subpath=subpath,
|
|
1527
|
+
shortname=shortname,
|
|
1528
|
+
action_type=core.ActionType.view,
|
|
1529
|
+
resource_type=resource_type,
|
|
1530
|
+
user_shortname=logged_in_user,
|
|
1531
|
+
)
|
|
1532
|
+
)
|
|
1533
|
+
|
|
1534
|
+
with open(payload_path / str(meta.payload.body), "r") as csv_file:
|
|
1535
|
+
media = "text/csv" if resource_type == ResourceType.csv else "text/plain"
|
|
1536
|
+
response = StreamingResponse(iter(csv_file.read()), media_type=media)
|
|
1537
|
+
response.headers["Content-Disposition"] = "attachment; filename=data.csv"
|
|
1538
|
+
|
|
1539
|
+
return response
|
|
1540
|
+
|
|
1541
|
+
"""
|