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
main.py
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
#!/usr/bin/env -S BACKEND_ENV=config.env python3
|
|
2
|
+
""" Main module """
|
|
3
|
+
import socket
|
|
4
|
+
from starlette.datastructures import UploadFile
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
from os import getpid
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
import traceback
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Any, cast
|
|
14
|
+
from urllib.parse import urlparse, quote
|
|
15
|
+
from jsonschema.exceptions import ValidationError as SchemaValidationError
|
|
16
|
+
from pydantic import ValidationError
|
|
17
|
+
from languages.loader import load_langs
|
|
18
|
+
from utils.middleware import CustomRequestMiddleware, ChannelMiddleware
|
|
19
|
+
from utils.jwt import JWTBearer
|
|
20
|
+
from utils.plugin_manager import plugin_manager
|
|
21
|
+
from fastapi import Depends, FastAPI, Request, Response, status
|
|
22
|
+
from utils.logger import logging_schema
|
|
23
|
+
from fastapi.logger import logger
|
|
24
|
+
from fastapi.encoders import jsonable_encoder
|
|
25
|
+
from fastapi.exceptions import RequestValidationError
|
|
26
|
+
from utils.access_control import access_control
|
|
27
|
+
from fastapi.responses import ORJSONResponse
|
|
28
|
+
from hypercorn.asyncio import serve
|
|
29
|
+
from hypercorn.config import Config
|
|
30
|
+
from starlette.concurrency import iterate_in_threadpool
|
|
31
|
+
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
32
|
+
import models.api as api
|
|
33
|
+
from utils.settings import settings
|
|
34
|
+
from asgi_correlation_id import CorrelationIdMiddleware
|
|
35
|
+
from data_adapters.adapter import data_adapter as db
|
|
36
|
+
from api.managed.router import router as managed
|
|
37
|
+
from api.qr.router import router as qr
|
|
38
|
+
from api.public.router import router as public
|
|
39
|
+
from api.user.router import router as user
|
|
40
|
+
from api.info.router import router as info, git_info
|
|
41
|
+
from utils.internal_error_code import InternalErrorCode
|
|
42
|
+
|
|
43
|
+
@asynccontextmanager
|
|
44
|
+
async def lifespan(app: FastAPI):
|
|
45
|
+
logger.info("Starting up")
|
|
46
|
+
print('{"stage":"starting up"}')
|
|
47
|
+
|
|
48
|
+
openapi_schema = app.openapi()
|
|
49
|
+
paths = openapi_schema["paths"]
|
|
50
|
+
for path in paths:
|
|
51
|
+
for method in paths[path]:
|
|
52
|
+
responses = paths[path][method]["responses"]
|
|
53
|
+
if responses.get("422"):
|
|
54
|
+
responses.pop("422")
|
|
55
|
+
app.openapi_schema = openapi_schema
|
|
56
|
+
|
|
57
|
+
await db.initialize_spaces()
|
|
58
|
+
await access_control.load_permissions_and_roles()
|
|
59
|
+
# await plugin_manager.load_plugins(app, capture_body)
|
|
60
|
+
yield
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
logger.info("Application shutting down")
|
|
64
|
+
print('{"stage":"shutting down"}')
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
app = FastAPI(
|
|
68
|
+
lifespan=lifespan,
|
|
69
|
+
title="Datamart API",
|
|
70
|
+
description="Structured Content Management System",
|
|
71
|
+
version=str(git_info["tag"]),
|
|
72
|
+
redoc_url=None,
|
|
73
|
+
docs_url=f"{settings.base_path}/docs",
|
|
74
|
+
openapi_url=f"{settings.base_path}/openapi.json",
|
|
75
|
+
servers=[{"url": f"{settings.base_path}/"}],
|
|
76
|
+
contact={
|
|
77
|
+
"name": "Kefah T. Issa",
|
|
78
|
+
"url": "https://dmart.cc",
|
|
79
|
+
"email": "kefah.issa@gmail.com",
|
|
80
|
+
},
|
|
81
|
+
license_info={
|
|
82
|
+
"name": "GNU Affero General Public License v3+",
|
|
83
|
+
"url": "https://www.gnu.org/licenses/agpl-3.0.en.html",
|
|
84
|
+
},
|
|
85
|
+
swagger_ui_parameters={"defaultModelsExpandDepth": -1},
|
|
86
|
+
openapi_tags=[
|
|
87
|
+
{"name": "user", "description": "User registration, login, profile and delete"},
|
|
88
|
+
{
|
|
89
|
+
"name": "managed",
|
|
90
|
+
"description": "Login-only content management api and media upload",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"name": "public",
|
|
94
|
+
"description": "Public api for query and GET access to media",
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
default_response_class=ORJSONResponse,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def capture_body(request: Request):
|
|
102
|
+
request.state.request_body = {}
|
|
103
|
+
|
|
104
|
+
if (
|
|
105
|
+
request.method == "POST"
|
|
106
|
+
and "application/json" in request.headers.get("content-type", "")
|
|
107
|
+
):
|
|
108
|
+
request.state.request_body = await request.json()
|
|
109
|
+
|
|
110
|
+
if (
|
|
111
|
+
request.method == "POST"
|
|
112
|
+
and request.headers.get("content-type")
|
|
113
|
+
and "multipart/form-data" in request.headers.get("content-type", [])
|
|
114
|
+
):
|
|
115
|
+
form = await request.form()
|
|
116
|
+
for field in form:
|
|
117
|
+
one = form[field]
|
|
118
|
+
if isinstance(one, str):
|
|
119
|
+
request.state.request_body[field] = form.get(field)
|
|
120
|
+
elif isinstance(one, UploadFile):
|
|
121
|
+
# TODO try to find a way to capture .json file content without await exeption
|
|
122
|
+
# inner_json= form.get(field).file
|
|
123
|
+
# form_to_dict[field]["file_name"]=form.get(field).filename
|
|
124
|
+
# form_to_dict[field]["content_type"]=form.get(field).content_type
|
|
125
|
+
request.state.request_body[field] = {
|
|
126
|
+
"name": one.filename,
|
|
127
|
+
"content_type": one.content_type,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@app.exception_handler(StarletteHTTPException)
|
|
132
|
+
async def my_exception_handler(_, exception):
|
|
133
|
+
return ORJSONResponse(content=exception.detail, status_code=exception.status_code)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@app.exception_handler(RequestValidationError)
|
|
137
|
+
async def validation_exception_handler(_: Request, exc: RequestValidationError):
|
|
138
|
+
err = jsonable_encoder({"detail": exc.errors()})["detail"]
|
|
139
|
+
raise api.Exception(
|
|
140
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
141
|
+
error=api.Error(
|
|
142
|
+
code=InternalErrorCode.UNPROCESSABLE_ENTITY, type="validation", message="Validation error [1]", info=err
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
app.add_middleware(CustomRequestMiddleware)
|
|
148
|
+
app.add_middleware(ChannelMiddleware)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def set_middleware_extra(request, response, start_time, user_shortname, exception_data, response_body):
|
|
152
|
+
extra = {
|
|
153
|
+
"props": {
|
|
154
|
+
"timestamp": start_time,
|
|
155
|
+
"duration": 1000 * (time.time() - start_time),
|
|
156
|
+
"server": socket.gethostname(),
|
|
157
|
+
"process_id": getpid(),
|
|
158
|
+
"user_shortname": user_shortname,
|
|
159
|
+
"request": {
|
|
160
|
+
"url": request.url._url,
|
|
161
|
+
"verb": request.method,
|
|
162
|
+
"path": quote(str(request.url.path)),
|
|
163
|
+
"query_params": dict(request.query_params.items()),
|
|
164
|
+
"headers": dict(request.headers.items()),
|
|
165
|
+
},
|
|
166
|
+
"response": {
|
|
167
|
+
"headers": dict(response.headers.items()),
|
|
168
|
+
"http_status": response.status_code,
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if exception_data is not None:
|
|
174
|
+
extra["props"]["exception"] = exception_data
|
|
175
|
+
|
|
176
|
+
if hasattr(request.state, "request_body"):
|
|
177
|
+
extra["props"]["request"]["body"] = request.state.request_body
|
|
178
|
+
|
|
179
|
+
if response_body and isinstance(response_body, dict):
|
|
180
|
+
extra["props"]["response"]["body"] = response_body
|
|
181
|
+
|
|
182
|
+
return extra
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def set_middleware_response_headers(request, response):
|
|
187
|
+
referer = request.headers.get(
|
|
188
|
+
"referer",
|
|
189
|
+
request.headers.get("origin",
|
|
190
|
+
request.headers.get("x-forwarded-proto", "http")
|
|
191
|
+
+ "://"
|
|
192
|
+
+ request.headers.get(
|
|
193
|
+
"x-forwarded-host", f"{settings.listening_host}:{settings.listening_port}"
|
|
194
|
+
)),
|
|
195
|
+
)
|
|
196
|
+
origin = urlparse(referer)
|
|
197
|
+
response.headers[
|
|
198
|
+
"Access-Control-Allow-Origin"
|
|
199
|
+
] = f"{origin.scheme}://{origin.netloc}"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
response.headers["Access-Control-Allow-Credentials"] = "true"
|
|
203
|
+
response.headers["Access-Control-Allow-Headers"] = "content-type, charset, authorization, accept-language, content-length"
|
|
204
|
+
response.headers["Access-Control-Max-Age"] = "600"
|
|
205
|
+
response.headers[
|
|
206
|
+
"Access-Control-Allow-Methods"
|
|
207
|
+
] = "OPTIONS, DELETE, POST, GET, PATCH, PUT"
|
|
208
|
+
|
|
209
|
+
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
|
210
|
+
response.headers["Pragma"] = "no-cache"
|
|
211
|
+
response.headers["Expires"] = "0"
|
|
212
|
+
response.headers["x-server-time"] = datetime.now().isoformat()
|
|
213
|
+
response.headers["Access-Control-Expose-Headers"] = "x-server-time"
|
|
214
|
+
return response
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def mask_sensitive_data(data):
|
|
218
|
+
if isinstance(data, dict):
|
|
219
|
+
return {k: mask_sensitive_data(v) if k not in ['password', 'access_token', 'refresh_token', 'auth_token', 'jwt', 'otp', 'code', 'token'] else '******' for k, v in data.items()}
|
|
220
|
+
elif isinstance(data, list):
|
|
221
|
+
return [mask_sensitive_data(item) for item in data]
|
|
222
|
+
elif isinstance(data, str) and 'auth_token' in data:
|
|
223
|
+
return '******'
|
|
224
|
+
return data
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def set_logging(response, extra, request, exception_data):
|
|
228
|
+
_extra = mask_sensitive_data(extra)
|
|
229
|
+
if isinstance(_extra, dict):
|
|
230
|
+
if 400 <= response.status_code < 500:
|
|
231
|
+
logger.warning("Served request", extra=_extra)
|
|
232
|
+
elif response.status_code >= 500 or exception_data is not None:
|
|
233
|
+
logger.error("Served request", extra=_extra)
|
|
234
|
+
elif request.method != "OPTIONS": # Do not log OPTIONS request, to reduce excessive logging
|
|
235
|
+
logger.info("Served request", extra=_extra)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def set_stack(e):
|
|
239
|
+
return [
|
|
240
|
+
{
|
|
241
|
+
"file": frame.f_code.co_filename,
|
|
242
|
+
"function": frame.f_code.co_name,
|
|
243
|
+
"line": lineno,
|
|
244
|
+
}
|
|
245
|
+
for frame, lineno in traceback.walk_tb(e.__traceback__)
|
|
246
|
+
if "site-packages" not in frame.f_code.co_filename
|
|
247
|
+
]
|
|
248
|
+
|
|
249
|
+
@app.middleware("http")
|
|
250
|
+
async def middle(request: Request, call_next):
|
|
251
|
+
"""Wrapper function to manage errors and logging"""
|
|
252
|
+
if (
|
|
253
|
+
request.url._url.endswith("/docs")
|
|
254
|
+
or request.url._url.endswith("openapi.json")
|
|
255
|
+
or request.url._url.endswith("/favicon.ico")
|
|
256
|
+
):
|
|
257
|
+
return await call_next(request)
|
|
258
|
+
|
|
259
|
+
start_time = time.time()
|
|
260
|
+
response_body: str | dict = {}
|
|
261
|
+
exception_data: dict[str, Any] | None = None
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
response = await asyncio.wait_for(call_next(request), timeout=settings.request_timeout)
|
|
266
|
+
raw_response = [section async for section in response.body_iterator]
|
|
267
|
+
response.body_iterator = iterate_in_threadpool(iter(raw_response))
|
|
268
|
+
raw_data = b"".join(raw_response)
|
|
269
|
+
if raw_data:
|
|
270
|
+
try:
|
|
271
|
+
response_body = json.loads(raw_data)
|
|
272
|
+
except Exception:
|
|
273
|
+
response_body = {}
|
|
274
|
+
except asyncio.TimeoutError:
|
|
275
|
+
response = ORJSONResponse(content={'status':'failed',
|
|
276
|
+
'error': {"code":504, "message": 'Request processing time excedeed limit'}},
|
|
277
|
+
status_code=status.HTTP_504_GATEWAY_TIMEOUT)
|
|
278
|
+
response_body = json.loads(str(response.body, 'utf8'))
|
|
279
|
+
except api.Exception as e:
|
|
280
|
+
if settings.active_data_db == 'sql':
|
|
281
|
+
if e.error.message.startswith('(sqlalchemy.dialects.postgresql'):
|
|
282
|
+
response = ORJSONResponse(
|
|
283
|
+
status_code=500,
|
|
284
|
+
content={
|
|
285
|
+
"status": "failed",
|
|
286
|
+
"error": 'Something went wrong',
|
|
287
|
+
},
|
|
288
|
+
)
|
|
289
|
+
else:
|
|
290
|
+
response = ORJSONResponse(
|
|
291
|
+
status_code=e.status_code,
|
|
292
|
+
content=jsonable_encoder(
|
|
293
|
+
api.Response(status=api.Status.failed, error=e.error)
|
|
294
|
+
),
|
|
295
|
+
)
|
|
296
|
+
else:
|
|
297
|
+
response = ORJSONResponse(
|
|
298
|
+
status_code=e.status_code,
|
|
299
|
+
content=jsonable_encoder(
|
|
300
|
+
api.Response(status=api.Status.failed, error=e.error)
|
|
301
|
+
),
|
|
302
|
+
)
|
|
303
|
+
stack = set_stack(e)
|
|
304
|
+
exception_data = {"props": {"exception": str(e), "stack": stack}}
|
|
305
|
+
response_body = json.loads(str(response.body, 'utf8'))
|
|
306
|
+
except ValidationError as e:
|
|
307
|
+
stack = set_stack(e)
|
|
308
|
+
exception_data = {"props": {"exception": str(e), "stack": stack}}
|
|
309
|
+
response = ORJSONResponse(
|
|
310
|
+
status_code=422,
|
|
311
|
+
content={
|
|
312
|
+
"status": "failed",
|
|
313
|
+
"error": {
|
|
314
|
+
"type": "validation",
|
|
315
|
+
"code": 422,
|
|
316
|
+
"message": "Validation error [2]",
|
|
317
|
+
"info": jsonable_encoder(e.errors()),
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
)
|
|
321
|
+
response_body = json.loads(str(response.body, 'utf8'))
|
|
322
|
+
except SchemaValidationError as e:
|
|
323
|
+
stack = set_stack(e)
|
|
324
|
+
exception_data = {"props": {"exception": str(e), "stack": stack}}
|
|
325
|
+
response = ORJSONResponse(
|
|
326
|
+
status_code=400,
|
|
327
|
+
content={
|
|
328
|
+
"status": "failed",
|
|
329
|
+
"error": {
|
|
330
|
+
"type": "validation",
|
|
331
|
+
"code": 422,
|
|
332
|
+
"message": "Validation error [3]",
|
|
333
|
+
"info": [{
|
|
334
|
+
"loc": list(e.path),
|
|
335
|
+
"msg": e.message
|
|
336
|
+
}],
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
)
|
|
340
|
+
response_body = json.loads(str(response.body, 'utf8'))
|
|
341
|
+
except Exception:
|
|
342
|
+
exception_message = ""
|
|
343
|
+
stack = None
|
|
344
|
+
if ee := sys.exc_info()[1]:
|
|
345
|
+
stack = set_stack(ee)
|
|
346
|
+
exception_message = str(ee)
|
|
347
|
+
exception_data = {"props": {"exception": str(ee), "stack": stack}}
|
|
348
|
+
|
|
349
|
+
error_log = {"type": "general", "code": 99, "message": exception_message}
|
|
350
|
+
if settings.debug_enabled:
|
|
351
|
+
error_log["stack"] = stack
|
|
352
|
+
response = ORJSONResponse(
|
|
353
|
+
status_code=500,
|
|
354
|
+
content={
|
|
355
|
+
"status": "failed",
|
|
356
|
+
"error": error_log,
|
|
357
|
+
},
|
|
358
|
+
)
|
|
359
|
+
response_body = json.loads(str(response.body, 'utf8'))
|
|
360
|
+
|
|
361
|
+
response = set_middleware_response_headers(request, response)
|
|
362
|
+
|
|
363
|
+
user_shortname = "guest"
|
|
364
|
+
if request.url.path == "/user/login":
|
|
365
|
+
try:
|
|
366
|
+
body = getattr(request.state, "request_body", {}) or {}
|
|
367
|
+
if isinstance(body, dict):
|
|
368
|
+
shortname_value = body.get("shortname")
|
|
369
|
+
if isinstance(shortname_value, str) and shortname_value.strip():
|
|
370
|
+
user_shortname = shortname_value
|
|
371
|
+
except Exception:
|
|
372
|
+
pass
|
|
373
|
+
else:
|
|
374
|
+
try:
|
|
375
|
+
user_shortname = str(await JWTBearer().__call__(request))
|
|
376
|
+
except Exception:
|
|
377
|
+
user_shortname = "guest"
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
extra = set_middleware_extra(request, response, start_time, user_shortname, exception_data, response_body)
|
|
381
|
+
|
|
382
|
+
set_logging(response, extra, request, exception_data)
|
|
383
|
+
|
|
384
|
+
#TODO: CHECK THIS
|
|
385
|
+
# if settings.hide_stack_trace:
|
|
386
|
+
# if (
|
|
387
|
+
# response_body and isinstance(response_body, dict)
|
|
388
|
+
# and "error" in response_body
|
|
389
|
+
# and "stack" in response_body["error"]
|
|
390
|
+
# ):
|
|
391
|
+
# response_body["error"].pop("stack", None)
|
|
392
|
+
#
|
|
393
|
+
# response.body_iterator = iterate_in_threadpool(iter([json.dumps(response_body).encode("utf-8")]))
|
|
394
|
+
|
|
395
|
+
return response
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
app.add_middleware(
|
|
399
|
+
CorrelationIdMiddleware,
|
|
400
|
+
header_name='X-Correlation-ID',
|
|
401
|
+
update_request_header=False,
|
|
402
|
+
validator=None,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@app.get("/", include_in_schema=False)
|
|
407
|
+
async def root():
|
|
408
|
+
"""Dummy api end point"""
|
|
409
|
+
return {"status": "success", "message": "DMART API"}
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
# @app.get("/s", include_in_schema=False)
|
|
413
|
+
# async def secrets(key):
|
|
414
|
+
# if key == "alpha":
|
|
415
|
+
# return settings.dict()
|
|
416
|
+
|
|
417
|
+
"""
|
|
418
|
+
@app.get("/spaces-backup", include_in_schema=False)
|
|
419
|
+
async def space_backup(key: str):
|
|
420
|
+
if not key or key != "ABC":
|
|
421
|
+
return api.Response(
|
|
422
|
+
status=api.Status.failed,
|
|
423
|
+
error=api.Error(type="git", code=InternalErrorCode.INVALID_APP_KEY, message="Api key is invalid"),
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
import subprocess
|
|
427
|
+
|
|
428
|
+
cmd = "/usr/bin/bash -c 'cd .. && ./spaces-backup.sh'"
|
|
429
|
+
# cmd = "../git-update.sh"
|
|
430
|
+
|
|
431
|
+
result_stdout, result_stderr = subprocess.Popen(
|
|
432
|
+
cmd.split(" "), stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
433
|
+
).communicate()
|
|
434
|
+
attributes = {
|
|
435
|
+
"stdout": result_stdout.decode().split("\n"),
|
|
436
|
+
"stderr": result_stderr.decode().split("\n"),
|
|
437
|
+
}
|
|
438
|
+
return api.Response(status=api.Status.success, attributes=attributes)
|
|
439
|
+
"""
|
|
440
|
+
|
|
441
|
+
app.include_router(
|
|
442
|
+
user, prefix="/user", tags=["user"], dependencies=[Depends(capture_body)]
|
|
443
|
+
)
|
|
444
|
+
app.include_router(
|
|
445
|
+
managed, prefix="/managed", tags=["managed"], dependencies=[Depends(capture_body)]
|
|
446
|
+
)
|
|
447
|
+
app.include_router(
|
|
448
|
+
qr,
|
|
449
|
+
prefix="/qr",
|
|
450
|
+
tags=["QR"],
|
|
451
|
+
dependencies=[Depends(capture_body)],
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
app.include_router(
|
|
455
|
+
public, prefix="/public", tags=["public"], dependencies=[Depends(capture_body)]
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
app.include_router(
|
|
459
|
+
info, prefix="/info", tags=["info"], dependencies=[Depends(capture_body)]
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
# load plugins
|
|
463
|
+
asyncio.run(plugin_manager.load_plugins(app, capture_body))
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
@app.options("/{x:path}", include_in_schema=False)
|
|
467
|
+
async def myoptions():
|
|
468
|
+
return Response(status_code=status.HTTP_200_OK)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
@app.get("/{x:path}", include_in_schema=False)
|
|
472
|
+
@app.post("/{x:path}", include_in_schema=False)
|
|
473
|
+
@app.put("/{x:path}", include_in_schema=False)
|
|
474
|
+
@app.patch("/{x:path}", include_in_schema=False)
|
|
475
|
+
@app.delete("/{x:path}", include_in_schema=False)
|
|
476
|
+
async def catchall() -> None:
|
|
477
|
+
raise api.Exception(
|
|
478
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
479
|
+
error=api.Error(
|
|
480
|
+
type="catchall", code=InternalErrorCode.INVALID_ROUTE, message="Requested method or path is invalid"
|
|
481
|
+
),
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
load_langs()
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
async def main():
|
|
488
|
+
config = Config()
|
|
489
|
+
config.bind = [f"{settings.listening_host}:{settings.listening_port}"]
|
|
490
|
+
config.backlog = 200
|
|
491
|
+
|
|
492
|
+
config.logconfig_dict = logging_schema
|
|
493
|
+
config.errorlog = logger
|
|
494
|
+
|
|
495
|
+
try:
|
|
496
|
+
await serve(cast(Any, app), config)
|
|
497
|
+
except OSError as e:
|
|
498
|
+
print("[!1server]", e)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
if __name__ == "__main__":
|
|
502
|
+
try:
|
|
503
|
+
asyncio.run(main())
|
|
504
|
+
except Exception as e:
|
|
505
|
+
print("[!1server]", e)
|
|
506
|
+
|
migrate.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env -S BACKEND_ENV=config.env python3
|
|
2
|
+
import asyncio
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
if len(sys.argv) < 2:
|
|
8
|
+
print("Usage: migrate.py <json_to_db|db_to_json>")
|
|
9
|
+
sys.exit(1)
|
|
10
|
+
|
|
11
|
+
command = sys.argv[1]
|
|
12
|
+
|
|
13
|
+
if command == "json_to_db":
|
|
14
|
+
import data_adapters.sql.json_to_db_migration as json_to_db_migration
|
|
15
|
+
asyncio.run(json_to_db_migration.main())
|
|
16
|
+
elif command == "db_to_json":
|
|
17
|
+
import data_adapters.sql.db_to_json_migration as db_to_json_migration
|
|
18
|
+
db_to_json_migration.main()
|
|
19
|
+
else:
|
|
20
|
+
print("Invalid command. Use 'json_to_db' or 'db_to_json'.")
|
|
21
|
+
sys.exit(1)
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
main()
|
models/__init__.py
ADDED
|
File without changes
|