arpakitlib 1.6.47__py3-none-any.whl → 1.7.90__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.
- arpakitlib/_arpakit_project_template/.gitignore +51 -0
- arpakitlib/_arpakit_project_template/.python-version +1 -0
- arpakitlib/_arpakit_project_template/ARPAKITLIB +1 -0
- arpakitlib/_arpakit_project_template/AUTHOR.md +4 -0
- arpakitlib/_arpakit_project_template/NOTICE +16 -0
- arpakitlib/_arpakit_project_template/README.md +6 -0
- arpakitlib/_arpakit_project_template/example.env +22 -0
- arpakitlib/_arpakit_project_template/manage/__init__.py +0 -0
- arpakitlib/_arpakit_project_template/manage/docker_ps.sh +2 -0
- arpakitlib/_arpakit_project_template/manage/docker_ps_a.sh +2 -0
- arpakitlib/_arpakit_project_template/manage/docker_run_postgres_for_dev.sh +4 -0
- arpakitlib/_arpakit_project_template/manage/docker_start_postgres_for_dev.sh +2 -0
- arpakitlib/_arpakit_project_template/manage/docker_stop_postgres_for_dev.sh +2 -0
- arpakitlib/_arpakit_project_template/manage/example_nginx_proxy.nginx +14 -0
- arpakitlib/_arpakit_project_template/manage/example_poetry_arpakitlib.sh +1 -0
- arpakitlib/_arpakit_project_template/manage/example_pyproject.toml +18 -0
- arpakitlib/_arpakit_project_template/manage/example_systemd.service +12 -0
- arpakitlib/_arpakit_project_template/manage/git_branch.sh +2 -0
- arpakitlib/_arpakit_project_template/manage/git_commit.sh +3 -0
- arpakitlib/_arpakit_project_template/manage/git_push_arpakit_company_github_1.sh +4 -0
- arpakitlib/_arpakit_project_template/manage/git_push_arpakit_company_gitlab_1.sh +4 -0
- arpakitlib/_arpakit_project_template/manage/git_push_arpakit_github_1.sh +4 -0
- arpakitlib/_arpakit_project_template/manage/git_push_arpakit_gitlab_1.sh +4 -0
- arpakitlib/_arpakit_project_template/manage/git_remote_v.sh +2 -0
- arpakitlib/_arpakit_project_template/manage/git_set_arpakit_company_origin.sh +7 -0
- arpakitlib/_arpakit_project_template/manage/git_set_arpakit_origin.sh +7 -0
- arpakitlib/_arpakit_project_template/manage/git_status.sh +2 -0
- arpakitlib/_arpakit_project_template/manage/hello_world.py +6 -0
- arpakitlib/_arpakit_project_template/manage/json_beutify.py +10 -0
- arpakitlib/_arpakit_project_template/manage/logging_check.py +14 -0
- arpakitlib/_arpakit_project_template/manage/note/__init__.py +0 -0
- arpakitlib/_arpakit_project_template/manage/note/note_1.txt +0 -0
- arpakitlib/_arpakit_project_template/manage/note/note_2.txt +0 -0
- arpakitlib/_arpakit_project_template/manage/note/note_3.txt +0 -0
- arpakitlib/_arpakit_project_template/manage/note/note_4.txt +0 -0
- arpakitlib/_arpakit_project_template/manage/note/note_5.txt +0 -0
- arpakitlib/_arpakit_project_template/manage/poetry_add_plugin_export.sh +2 -0
- arpakitlib/_arpakit_project_template/manage/poetry_check.sh +2 -0
- arpakitlib/_arpakit_project_template/manage/poetry_clear_cache.sh +4 -0
- arpakitlib/_arpakit_project_template/manage/poetry_config_virtualenvs_in_project_true.sh +2 -0
- arpakitlib/_arpakit_project_template/manage/poetry_generate_requirements.txt.sh +1 -0
- arpakitlib/_arpakit_project_template/manage/poetry_install.sh +5 -0
- arpakitlib/_arpakit_project_template/manage/poetry_lock.sh +2 -0
- arpakitlib/_arpakit_project_template/manage/poetry_remove_and_add_arpakitlib.sh +7 -0
- arpakitlib/_arpakit_project_template/manage/poetry_show.sh +2 -0
- arpakitlib/_arpakit_project_template/manage/poetry_show_arpakitlib.sh +2 -0
- arpakitlib/_arpakit_project_template/manage/poetry_update.sh +6 -0
- arpakitlib/_arpakit_project_template/manage/poetry_update_arpakitlib.sh +5 -0
- arpakitlib/_arpakit_project_template/manage/requirements.txt +209 -0
- arpakitlib/_arpakit_project_template/manage/sandbox/__init__.py +0 -0
- arpakitlib/_arpakit_project_template/manage/sandbox/sandbox_1.py +14 -0
- arpakitlib/_arpakit_project_template/manage/sandbox/sandbox_2.py +14 -0
- arpakitlib/_arpakit_project_template/manage/sandbox/sandbox_3.py +14 -0
- arpakitlib/_arpakit_project_template/manage/sandbox/sandbox_4.py +14 -0
- arpakitlib/_arpakit_project_template/manage/sandbox/sandbox_5.py +14 -0
- arpakitlib/_arpakit_project_template/manage/sandbox/sandbox_6.py +14 -0
- arpakitlib/_arpakit_project_template/manage/sandbox/sandbox_7.py +14 -0
- arpakitlib/_arpakit_project_template/manage/sandbox/sandbox_8.sh +0 -0
- arpakitlib/_arpakit_project_template/manage/sandbox/sandbox_9.sh +0 -0
- arpakitlib/_arpakit_project_template/manage/settings_check.py +10 -0
- arpakitlib/_arpakit_project_template/manage/settings_generate_env_example.py +13 -0
- arpakitlib/_arpakit_project_template/manage/sqlalchemy_db_check_conn.py +11 -0
- arpakitlib/_arpakit_project_template/manage/sqlalchemy_db_init.py +11 -0
- arpakitlib/_arpakit_project_template/manage/sqlalchemy_db_reinit.py +11 -0
- arpakitlib/_arpakit_project_template/resource/__init__.py +0 -0
- arpakitlib/_arpakit_project_template/resource/static/__init__.py +0 -0
- arpakitlib/_arpakit_project_template/resource/static/healthcheck +1 -0
- arpakitlib/_arpakit_project_template/resource/static/helloworld +1 -0
- arpakitlib/_arpakit_project_template/src/__init__.py +0 -0
- arpakitlib/_arpakit_project_template/src/additional_model/__init__.py +0 -0
- arpakitlib/_arpakit_project_template/src/additional_model/additional_model.py +6 -0
- arpakitlib/_arpakit_project_template/src/api/__init__.py +0 -0
- arpakitlib/_arpakit_project_template/src/api/asgi.py +3 -0
- arpakitlib/_arpakit_project_template/src/api/auth.py +1 -0
- arpakitlib/_arpakit_project_template/src/api/const.py +13 -0
- arpakitlib/_arpakit_project_template/src/api/create_api_app.py +117 -0
- arpakitlib/_arpakit_project_template/src/api/event.py +20 -0
- arpakitlib/_arpakit_project_template/src/api/router/__init__.py +0 -0
- arpakitlib/_arpakit_project_template/src/api/router/main_router.py +9 -0
- arpakitlib/_arpakit_project_template/src/api/router/v1/__init__.py +0 -0
- arpakitlib/_arpakit_project_template/src/api/router/v1/get_api_error_info.py +27 -0
- arpakitlib/_arpakit_project_template/src/api/router/v1/main_router.py +11 -0
- arpakitlib/_arpakit_project_template/src/api/schema/__init__.py +0 -0
- arpakitlib/_arpakit_project_template/src/api/schema/v1/__init__.py +0 -0
- arpakitlib/_arpakit_project_template/src/api/schema/v1/in_.py +0 -0
- arpakitlib/_arpakit_project_template/src/api/schema/v1/out.py +6 -0
- arpakitlib/_arpakit_project_template/src/api/start_api_for_dev.py +17 -0
- arpakitlib/_arpakit_project_template/src/api/start_api_for_dev_with_reload.py +9 -0
- arpakitlib/_arpakit_project_template/src/api/transmitted_api_data.py +9 -0
- arpakitlib/_arpakit_project_template/src/api/util.py +0 -0
- arpakitlib/_arpakit_project_template/src/business_service/__init__.py +0 -0
- arpakitlib/_arpakit_project_template/src/core/__init__.py +0 -0
- arpakitlib/_arpakit_project_template/src/core/const.py +48 -0
- arpakitlib/_arpakit_project_template/src/core/settings.py +86 -0
- arpakitlib/_arpakit_project_template/src/core/util.py +58 -0
- arpakitlib/_arpakit_project_template/src/db/__init__.py +0 -0
- arpakitlib/_arpakit_project_template/src/db/sqlalchemy_model.py +8 -0
- arpakitlib/_arpakit_project_template/src/db/util.py +21 -0
- arpakitlib/_arpakit_project_template/src/operation_execution/__init__.py +0 -0
- arpakitlib/_arpakit_project_template/src/operation_execution/const.py +9 -0
- arpakitlib/_arpakit_project_template/src/operation_execution/operation_executor.py +14 -0
- arpakitlib/_arpakit_project_template/src/operation_execution/scheduled_operations.py +25 -0
- arpakitlib/_arpakit_project_template/src/operation_execution/start_operation_executor_worker_for_dev.py +18 -0
- arpakitlib/_arpakit_project_template/src/operation_execution/start_scheduled_operation_creator_worker_for_dev.py +17 -0
- arpakitlib/_arpakit_project_template/src/operation_execution/util.py +21 -0
- arpakitlib/_arpakit_project_template/src/test_data/__init__.py +0 -0
- arpakitlib/_arpakit_project_template/src/test_data/make_test_data_1.py +6 -0
- arpakitlib/_arpakit_project_template/src/test_data/make_test_data_2.py +6 -0
- arpakitlib/_arpakit_project_template/src/test_data/make_test_data_3.py +6 -0
- arpakitlib/_arpakit_project_template/src/test_data/make_test_data_4.py +6 -0
- arpakitlib/_arpakit_project_template/src/test_data/make_test_data_5.py +6 -0
- arpakitlib/_arpakit_project_template/src/util/__init__.py +0 -0
- arpakitlib/api_key_util.py +21 -0
- arpakitlib/ar_additional_model_util.py +25 -2
- arpakitlib/ar_aiogram_util.py +10 -18
- arpakitlib/ar_arpakit_lib_module_util.py +6 -1
- arpakitlib/ar_arpakit_project_template_util.py +96 -0
- arpakitlib/ar_arpakit_schedule_uust_api_client_util.py +24 -3
- arpakitlib/ar_arpakitlib_cli_util.py +79 -0
- arpakitlib/ar_base_worker_util.py +95 -48
- arpakitlib/ar_dream_ai_api_client_util.py +26 -52
- arpakitlib/ar_enumeration_util.py +11 -0
- arpakitlib/ar_exception_util.py +18 -0
- arpakitlib/ar_fastapi_static/healthcheck +1 -0
- arpakitlib/ar_fastapi_util.py +270 -137
- arpakitlib/ar_file_util.py +22 -0
- arpakitlib/ar_func_util.py +55 -0
- arpakitlib/ar_http_request_util.py +35 -6
- arpakitlib/ar_json_util.py +13 -7
- arpakitlib/ar_logging_util.py +5 -2
- arpakitlib/ar_need_type_util.py +12 -2
- arpakitlib/{ar_openai_util.py → ar_openai_api_client_util.py} +16 -2
- arpakitlib/ar_operation_execution_util.py +250 -105
- arpakitlib/ar_parse_command.py +25 -7
- arpakitlib/ar_schedule_uust_api_client_util.py +37 -23
- arpakitlib/ar_settings_util.py +37 -11
- arpakitlib/ar_sleep_util.py +0 -13
- arpakitlib/ar_sqlalchemy_model_util.py +47 -10
- arpakitlib/ar_sqlalchemy_util.py +4 -3
- arpakitlib/{ar_ssh_util.py → ar_ssh_runner_util.py} +2 -2
- arpakitlib/ar_str_util.py +43 -2
- arpakitlib/ar_type_util.py +68 -4
- arpakitlib/ar_yookassa_api_client_util.py +26 -44
- {arpakitlib-1.6.47.dist-info → arpakitlib-1.7.90.dist-info}/METADATA +17 -8
- arpakitlib-1.7.90.dist-info/NOTICE +16 -0
- arpakitlib-1.7.90.dist-info/RECORD +186 -0
- arpakitlib-1.7.90.dist-info/entry_points.txt +3 -0
- arpakitlib/AUTHOR.md +0 -7
- arpakitlib/NOTICE +0 -2
- arpakitlib/ar_arpakitlib_info.py +0 -13
- arpakitlib/ar_generate_env_example.py +0 -22
- arpakitlib-1.6.47.dist-info/NOTICE +0 -2
- arpakitlib-1.6.47.dist-info/RECORD +0 -70
- /arpakitlib/{LICENSE → _arpakit_project_template/LICENSE} +0 -0
- /arpakitlib/{ar_zabbix_util.py → ar_zabbix_api_client_util.py} +0 -0
- {arpakitlib-1.6.47.dist-info → arpakitlib-1.7.90.dist-info}/LICENSE +0 -0
- {arpakitlib-1.6.47.dist-info → arpakitlib-1.7.90.dist-info}/WHEEL +0 -0
arpakitlib/ar_fastapi_util.py
CHANGED
@@ -6,8 +6,8 @@ import asyncio
|
|
6
6
|
import logging
|
7
7
|
import os.path
|
8
8
|
import pathlib
|
9
|
-
import threading
|
10
9
|
import traceback
|
10
|
+
from contextlib import suppress
|
11
11
|
from datetime import datetime
|
12
12
|
from typing import Any, Callable
|
13
13
|
|
@@ -17,24 +17,24 @@ import fastapi.security
|
|
17
17
|
import starlette.exceptions
|
18
18
|
import starlette.requests
|
19
19
|
import starlette.status
|
20
|
-
from fastapi import FastAPI, APIRouter, Query, Security
|
20
|
+
from fastapi import FastAPI, APIRouter, Query, Security, Depends
|
21
21
|
from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html
|
22
22
|
from fastapi.security import APIKeyHeader
|
23
|
-
from jaraco.context import suppress
|
24
23
|
from pydantic import BaseModel, ConfigDict
|
24
|
+
from starlette import status
|
25
25
|
from starlette.middleware.cors import CORSMiddleware
|
26
26
|
from starlette.staticfiles import StaticFiles
|
27
27
|
|
28
|
-
from arpakitlib.ar_base_worker_util import BaseWorker
|
28
|
+
from arpakitlib.ar_base_worker_util import BaseWorker, safe_run_worker_in_background, SafeRunInBackgroundModes
|
29
29
|
from arpakitlib.ar_dict_util import combine_dicts
|
30
30
|
from arpakitlib.ar_enumeration_util import Enumeration
|
31
|
-
from arpakitlib.
|
31
|
+
from arpakitlib.ar_file_storage_in_dir_util import FileStorageInDir
|
32
|
+
from arpakitlib.ar_func_util import raise_if_not_async_func, is_async_function, is_async_object
|
33
|
+
from arpakitlib.ar_json_util import safely_transfer_obj_to_json_str_to_json_obj
|
32
34
|
from arpakitlib.ar_logging_util import setup_normal_logging
|
33
|
-
from arpakitlib.
|
34
|
-
from arpakitlib.ar_settings_util import SimpleSettings
|
35
|
-
from arpakitlib.ar_sqlalchemy_model_util import StoryLogDBM
|
35
|
+
from arpakitlib.ar_sqlalchemy_model_util import StoryLogDBM, OperationDBM
|
36
36
|
from arpakitlib.ar_sqlalchemy_util import SQLAlchemyDB
|
37
|
-
from arpakitlib.ar_type_util import raise_for_type,
|
37
|
+
from arpakitlib.ar_type_util import raise_for_type, raise_if_none
|
38
38
|
|
39
39
|
_ARPAKIT_LIB_MODULE_VERSION = "3.0"
|
40
40
|
|
@@ -70,16 +70,21 @@ class SimpleSO(BaseSO):
|
|
70
70
|
creation_dt: datetime
|
71
71
|
|
72
72
|
|
73
|
-
class
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
73
|
+
class BaseAPIErrorCodes(Enumeration):
|
74
|
+
cannot_authorize = "CANNOT_AUTHORIZE"
|
75
|
+
unknown_error = "UNKNOWN_ERROR"
|
76
|
+
error_in_request = "ERROR_IN_REQUEST"
|
77
|
+
not_found = "NOT_FOUND"
|
78
|
+
|
79
|
+
|
80
|
+
class BaseAPIErrorSpecificationCodes(Enumeration):
|
81
|
+
pass
|
79
82
|
|
83
|
+
|
84
|
+
class ErrorSO(BaseSO):
|
80
85
|
has_error: bool = True
|
81
86
|
error_code: str | None = None
|
82
|
-
|
87
|
+
error_specification_code: str | None = None
|
83
88
|
error_description: str | None = None
|
84
89
|
error_data: dict[str, Any] = {}
|
85
90
|
|
@@ -93,6 +98,10 @@ class StoryLogSO(SimpleSO):
|
|
93
98
|
title: str | None
|
94
99
|
data: dict[str, Any]
|
95
100
|
|
101
|
+
@classmethod
|
102
|
+
def from_story_log_dbm(cls, *, story_log_dbm: StoryLogDBM) -> StoryLogSO:
|
103
|
+
return cls.model_validate(story_log_dbm.simple_dict(include_sd_properties=True))
|
104
|
+
|
96
105
|
|
97
106
|
class OperationSO(SimpleSO):
|
98
107
|
execution_start_dt: datetime | None
|
@@ -104,13 +113,29 @@ class OperationSO(SimpleSO):
|
|
104
113
|
error_data: dict[str, Any]
|
105
114
|
duration_total_seconds: float | None
|
106
115
|
|
116
|
+
@classmethod
|
117
|
+
def from_operation_dbm(cls, *, operation_dbm: OperationDBM) -> OperationSO:
|
118
|
+
return cls.model_validate(operation_dbm.simple_dict(include_sd_properties=True))
|
119
|
+
|
107
120
|
|
108
121
|
class APIJSONResponse(fastapi.responses.JSONResponse):
|
109
|
-
def __init__(self, *, content: BaseSO, status_code: int = starlette.status.HTTP_200_OK):
|
122
|
+
def __init__(self, *, content: dict | list | BaseSO | None, status_code: int = starlette.status.HTTP_200_OK):
|
123
|
+
if isinstance(content, dict):
|
124
|
+
content = safely_transfer_obj_to_json_str_to_json_obj(content)
|
125
|
+
elif isinstance(content, list):
|
126
|
+
content = safely_transfer_obj_to_json_str_to_json_obj(content)
|
127
|
+
elif isinstance(content, BaseSO):
|
128
|
+
content = safely_transfer_obj_to_json_str_to_json_obj(content.model_dump())
|
129
|
+
elif content is None:
|
130
|
+
content = None
|
131
|
+
else:
|
132
|
+
raise ValueError(f"unknown content type, type(content)={type(content)}")
|
133
|
+
|
110
134
|
self.content_ = content
|
111
135
|
self.status_code_ = status_code
|
136
|
+
|
112
137
|
super().__init__(
|
113
|
-
content=
|
138
|
+
content=content,
|
114
139
|
status_code=status_code
|
115
140
|
)
|
116
141
|
|
@@ -120,14 +145,14 @@ class APIException(fastapi.exceptions.HTTPException):
|
|
120
145
|
self,
|
121
146
|
*,
|
122
147
|
status_code: int = starlette.status.HTTP_400_BAD_REQUEST,
|
123
|
-
error_code: str | None =
|
124
|
-
|
148
|
+
error_code: str | None = BaseAPIErrorCodes.unknown_error,
|
149
|
+
error_specification_code: str | None = None,
|
125
150
|
error_description: str | None = None,
|
126
151
|
error_data: dict[str, Any] | None = None
|
127
152
|
):
|
128
153
|
self.status_code = status_code
|
129
154
|
self.error_code = error_code
|
130
|
-
self.
|
155
|
+
self.error_specification_code = error_specification_code
|
131
156
|
self.error_description = error_description
|
132
157
|
if error_data is None:
|
133
158
|
error_data = {}
|
@@ -136,7 +161,7 @@ class APIException(fastapi.exceptions.HTTPException):
|
|
136
161
|
self.error_so = ErrorSO(
|
137
162
|
has_error=True,
|
138
163
|
error_code=self.error_code,
|
139
|
-
|
164
|
+
error_specification_code=self.error_specification_code,
|
140
165
|
error_description=self.error_description,
|
141
166
|
error_data=self.error_data
|
142
167
|
)
|
@@ -149,14 +174,16 @@ class APIException(fastapi.exceptions.HTTPException):
|
|
149
174
|
|
150
175
|
def create_handle_exception(
|
151
176
|
*,
|
152
|
-
funcs_before_response: list[Callable] | None = None,
|
153
|
-
async_funcs_after_response: list[Callable] | None = None,
|
154
|
-
) ->
|
177
|
+
funcs_before_response: list[Callable | None] | None = None,
|
178
|
+
async_funcs_after_response: list[Callable | None] | None = None,
|
179
|
+
) -> Callable:
|
155
180
|
if funcs_before_response is None:
|
156
181
|
funcs_before_response = []
|
182
|
+
funcs_before_response = [v for v in funcs_before_response if v is not None]
|
157
183
|
|
158
184
|
if async_funcs_after_response is None:
|
159
185
|
async_funcs_after_response = []
|
186
|
+
async_funcs_after_response = [v for v in async_funcs_after_response if v is not None]
|
160
187
|
|
161
188
|
async def handle_exception(
|
162
189
|
request: starlette.requests.Request, exception: Exception
|
@@ -165,12 +192,13 @@ def create_handle_exception(
|
|
165
192
|
|
166
193
|
error_so = ErrorSO(
|
167
194
|
has_error=True,
|
168
|
-
error_code=
|
195
|
+
error_code=BaseAPIErrorCodes.unknown_error,
|
169
196
|
error_data={
|
170
197
|
"exception_type": str(type(exception)),
|
171
198
|
"exception_str": str(exception),
|
172
199
|
"request.method": str(request.method),
|
173
200
|
"request.url": str(request.url),
|
201
|
+
"request.headers": str(request.headers),
|
174
202
|
}
|
175
203
|
)
|
176
204
|
|
@@ -183,10 +211,10 @@ def create_handle_exception(
|
|
183
211
|
elif isinstance(exception, starlette.exceptions.HTTPException):
|
184
212
|
status_code = exception.status_code
|
185
213
|
if status_code in (starlette.status.HTTP_403_FORBIDDEN, starlette.status.HTTP_401_UNAUTHORIZED):
|
186
|
-
error_so.error_code =
|
214
|
+
error_so.error_code = BaseAPIErrorCodes.cannot_authorize
|
187
215
|
_need_exc_info = False
|
188
216
|
elif status_code == starlette.status.HTTP_404_NOT_FOUND:
|
189
|
-
error_so.error_code =
|
217
|
+
error_so.error_code = BaseAPIErrorCodes.not_found
|
190
218
|
_need_exc_info = False
|
191
219
|
else:
|
192
220
|
status_code = starlette.status.HTTP_500_INTERNAL_SERVER_ERROR
|
@@ -196,25 +224,31 @@ def create_handle_exception(
|
|
196
224
|
|
197
225
|
elif isinstance(exception, fastapi.exceptions.RequestValidationError):
|
198
226
|
status_code = starlette.status.HTTP_422_UNPROCESSABLE_ENTITY
|
199
|
-
error_so.error_code =
|
227
|
+
error_so.error_code = BaseAPIErrorCodes.error_in_request
|
200
228
|
with suppress(Exception):
|
201
229
|
error_so.error_data["exception.errors"] = str(exception.errors()) if exception.errors() else {}
|
202
230
|
_need_exc_info = False
|
203
231
|
|
204
232
|
else:
|
205
233
|
status_code = starlette.status.HTTP_500_INTERNAL_SERVER_ERROR
|
206
|
-
error_so.error_code =
|
234
|
+
error_so.error_code = BaseAPIErrorCodes.unknown_error
|
207
235
|
_logger.exception(exception)
|
208
236
|
_need_exc_info = True
|
209
237
|
|
210
238
|
if error_so.error_code:
|
211
239
|
error_so.error_code = error_so.error_code.upper().replace(" ", "_").strip()
|
212
240
|
|
213
|
-
if error_so.
|
214
|
-
error_so.
|
215
|
-
error_so.
|
241
|
+
if error_so.error_specification_code:
|
242
|
+
error_so.error_specification_code = (
|
243
|
+
error_so.error_specification_code.upper().replace(" ", "_").strip()
|
216
244
|
)
|
217
245
|
|
246
|
+
if error_so.error_code == BaseAPIErrorCodes.not_found:
|
247
|
+
status_code = status.HTTP_404_NOT_FOUND
|
248
|
+
|
249
|
+
if error_so.error_code == BaseAPIErrorCodes.cannot_authorize:
|
250
|
+
status_code = status.HTTP_401_UNAUTHORIZED
|
251
|
+
|
218
252
|
if _need_exc_info:
|
219
253
|
_logger.error(str(exception), exc_info=exception)
|
220
254
|
else:
|
@@ -225,7 +259,7 @@ def create_handle_exception(
|
|
225
259
|
_func_data = func(
|
226
260
|
status_code=status_code, error_so=error_so, request=request, exception=exception, **_kwargs
|
227
261
|
)
|
228
|
-
if
|
262
|
+
if is_async_function(_func_data):
|
229
263
|
_func_data = await _func_data
|
230
264
|
if _func_data is not None:
|
231
265
|
status_code, error_so, _kwargs = _func_data[0], _func_data[1], _func_data[2]
|
@@ -247,11 +281,12 @@ def create_handle_exception(
|
|
247
281
|
return handle_exception
|
248
282
|
|
249
283
|
|
250
|
-
def
|
284
|
+
def create_story_log_before_response_in_handle_exception(
|
251
285
|
*,
|
252
|
-
sqlalchemy_db: SQLAlchemyDB
|
286
|
+
sqlalchemy_db: SQLAlchemyDB,
|
287
|
+
ignore_api_error_code_not_found: bool = True
|
253
288
|
) -> Callable:
|
254
|
-
def
|
289
|
+
def func(
|
255
290
|
*,
|
256
291
|
status_code: int,
|
257
292
|
error_so: ErrorSO,
|
@@ -259,6 +294,8 @@ def create_handle_exception_creating_story_log(
|
|
259
294
|
exception: Exception,
|
260
295
|
**kwargs
|
261
296
|
) -> (int, ErrorSO, dict[str, Any]):
|
297
|
+
if ignore_api_error_code_not_found and error_so.error_code == BaseAPIErrorCodes.not_found:
|
298
|
+
return status_code, error_so, kwargs
|
262
299
|
sqlalchemy_db.init()
|
263
300
|
traceback_str = "".join(traceback.format_exception(type(exception), exception, exception.__traceback__))
|
264
301
|
with sqlalchemy_db.new_session() as session:
|
@@ -277,7 +314,7 @@ def create_handle_exception_creating_story_log(
|
|
277
314
|
kwargs["story_log_id"] = story_log_dbm.id
|
278
315
|
return status_code, error_so, kwargs
|
279
316
|
|
280
|
-
return
|
317
|
+
return func
|
281
318
|
|
282
319
|
|
283
320
|
def add_exception_handler_to_app(*, app: FastAPI, handle_exception: Callable) -> FastAPI:
|
@@ -349,7 +386,7 @@ def add_needed_api_router_to_app(*, app: FastAPI):
|
|
349
386
|
|
350
387
|
@api_router.get(
|
351
388
|
"/healthcheck",
|
352
|
-
response_model=ErrorSO,
|
389
|
+
response_model=RawDataSO | ErrorSO,
|
353
390
|
status_code=starlette.status.HTTP_200_OK,
|
354
391
|
tags=["Healthcheck"]
|
355
392
|
)
|
@@ -361,14 +398,14 @@ def add_needed_api_router_to_app(*, app: FastAPI):
|
|
361
398
|
|
362
399
|
@api_router.get(
|
363
400
|
"/arpakitlib",
|
364
|
-
response_model=ErrorSO,
|
401
|
+
response_model=RawDataSO | ErrorSO,
|
365
402
|
status_code=starlette.status.HTTP_200_OK,
|
366
403
|
tags=["arpakitlib"]
|
367
404
|
)
|
368
405
|
async def _():
|
369
406
|
return APIJSONResponse(
|
370
407
|
status_code=starlette.status.HTTP_200_OK,
|
371
|
-
content=RawDataSO(data={"arpakitlib":
|
408
|
+
content=RawDataSO(data={"arpakitlib": True})
|
372
409
|
)
|
373
410
|
|
374
411
|
app.include_router(router=api_router, prefix="")
|
@@ -399,140 +436,221 @@ class InitSqlalchemyDBStartupAPIEvent(BaseStartupAPIEvent):
|
|
399
436
|
super().__init__()
|
400
437
|
self.sqlalchemy_db = sqlalchemy_db
|
401
438
|
|
402
|
-
def async_on_startup(self, *args, **kwargs):
|
439
|
+
async def async_on_startup(self, *args, **kwargs):
|
403
440
|
self.sqlalchemy_db.init()
|
404
441
|
|
405
442
|
|
406
|
-
class
|
407
|
-
def __init__(self,
|
443
|
+
class SafeRunWorkerStartupAPIEvent(BaseStartupAPIEvent):
|
444
|
+
def __init__(self, workers: list[BaseWorker], safe_run_in_background_mode: str):
|
408
445
|
super().__init__()
|
409
|
-
self.
|
446
|
+
self.workers = workers
|
447
|
+
self.safe_run_in_background_mode = safe_run_in_background_mode
|
410
448
|
|
411
|
-
def async_on_startup(self, *args, **kwargs):
|
412
|
-
|
413
|
-
|
414
|
-
daemon=True
|
415
|
-
)
|
416
|
-
thread.start()
|
449
|
+
async def async_on_startup(self, *args, **kwargs):
|
450
|
+
for worker in self.workers:
|
451
|
+
_ = safe_run_worker_in_background(worker=worker, mode=SafeRunInBackgroundModes.async_task)
|
417
452
|
|
418
453
|
|
419
|
-
class
|
420
|
-
def __init__(self,
|
454
|
+
class InitFileStoragesInDir(BaseStartupAPIEvent):
|
455
|
+
def __init__(self, file_storages_in_dir: list[FileStorageInDir | None]):
|
421
456
|
super().__init__()
|
422
|
-
|
457
|
+
file_storages_in_dir = [v for v in file_storages_in_dir if v is not None]
|
458
|
+
self.file_storages_in_dir = file_storages_in_dir
|
423
459
|
|
424
|
-
def async_on_startup(self, *args, **kwargs):
|
425
|
-
|
460
|
+
async def async_on_startup(self, *args, **kwargs):
|
461
|
+
for file_storage_in_dir in self.file_storages_in_dir:
|
462
|
+
file_storage_in_dir.init()
|
426
463
|
|
427
464
|
|
428
465
|
class BaseTransmittedAPIData(BaseModel):
|
429
466
|
model_config = ConfigDict(extra="ignore", arbitrary_types_allowed=True, from_attributes=True)
|
430
467
|
|
431
|
-
settings: SimpleSettings | None = None
|
432
|
-
|
433
|
-
|
434
|
-
class SimpleTransmittedAPIData(BaseTransmittedAPIData):
|
435
|
-
sqlalchemy_db: SQLAlchemyDB | None = None
|
436
|
-
|
437
468
|
|
438
469
|
def get_transmitted_api_data(request: starlette.requests.Request) -> BaseTransmittedAPIData:
|
439
470
|
return request.app.state.transmitted_api_data
|
440
471
|
|
441
472
|
|
442
|
-
class
|
473
|
+
class BaseAPIAuthData(BaseModel):
|
443
474
|
model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True, from_attributes=True)
|
444
475
|
|
476
|
+
require_api_key_string: bool = False
|
477
|
+
require_token_string: bool = False
|
478
|
+
|
479
|
+
require_correct_api_key: bool = False
|
480
|
+
require_correct_token: bool = False
|
481
|
+
|
445
482
|
token_string: str | None = None
|
446
483
|
api_key_string: str | None = None
|
447
484
|
|
485
|
+
is_token_string_correct: bool | None = None
|
486
|
+
is_api_key_string_correct: bool | None = None
|
487
|
+
|
448
488
|
|
449
|
-
def
|
489
|
+
def base_api_auth(
|
450
490
|
*,
|
451
491
|
require_api_key_string: bool = False,
|
452
492
|
require_token_string: bool = False,
|
493
|
+
validate_api_key_func: Callable | None = None,
|
494
|
+
validate_token_func: Callable | None = None,
|
495
|
+
correct_api_keys: str | list[str] | None = None,
|
496
|
+
correct_tokens: str | list[str] | None = None,
|
497
|
+
require_correct_api_key: bool = False,
|
498
|
+
require_correct_token: bool = False,
|
499
|
+
**kwargs
|
453
500
|
) -> Callable:
|
501
|
+
if isinstance(correct_api_keys, str):
|
502
|
+
correct_api_keys = [correct_api_keys]
|
503
|
+
if correct_api_keys is not None:
|
504
|
+
raise_for_type(correct_api_keys, list)
|
505
|
+
validate_api_key_func = lambda *args, **kwargs_: kwargs_["api_key_string"] in correct_api_keys
|
506
|
+
|
507
|
+
if isinstance(correct_tokens, str):
|
508
|
+
correct_tokens = [correct_tokens]
|
509
|
+
if correct_tokens is not None:
|
510
|
+
raise_for_type(correct_tokens, list)
|
511
|
+
validate_token_func = lambda *args, **kwargs_: kwargs_["token_string"] in correct_tokens
|
512
|
+
|
513
|
+
if require_correct_api_key:
|
514
|
+
raise_if_none(validate_api_key_func)
|
515
|
+
require_api_key_string = True
|
516
|
+
|
517
|
+
if require_correct_token:
|
518
|
+
raise_if_none(validate_token_func)
|
519
|
+
require_token_string = True
|
520
|
+
|
454
521
|
async def func(
|
455
522
|
*,
|
456
523
|
ac: fastapi.security.HTTPAuthorizationCredentials | None = fastapi.Security(
|
457
524
|
fastapi.security.HTTPBearer(auto_error=False)
|
458
525
|
),
|
459
526
|
api_key_string: str | None = Security(APIKeyHeader(name="apikey", auto_error=False)),
|
460
|
-
request:
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
527
|
+
request: starlette.requests.Request,
|
528
|
+
transmitted_api_data: BaseTransmittedAPIData = Depends(get_transmitted_api_data)
|
529
|
+
) -> BaseAPIAuthData:
|
530
|
+
|
531
|
+
api_auth_data = BaseAPIAuthData(
|
532
|
+
require_api_key_string=require_api_key_string,
|
533
|
+
require_token_string=require_token_string,
|
534
|
+
require_correct_api_key=require_correct_api_key,
|
535
|
+
require_correct_token=require_correct_token
|
536
|
+
)
|
469
537
|
|
470
538
|
# api_key
|
471
539
|
|
472
|
-
|
540
|
+
api_auth_data.api_key_string = api_key_string
|
473
541
|
|
474
|
-
if not
|
475
|
-
|
476
|
-
if not
|
477
|
-
|
478
|
-
if not
|
479
|
-
|
542
|
+
if not api_auth_data.api_key_string and "api_key" in request.headers.keys():
|
543
|
+
api_auth_data.api_key_string = request.headers["api_key"]
|
544
|
+
if not api_auth_data.api_key_string and "api-key" in request.headers.keys():
|
545
|
+
api_auth_data.api_key_string = request.headers["api-key"]
|
546
|
+
if not api_auth_data.api_key_string and "apikey" in request.headers.keys():
|
547
|
+
api_auth_data.api_key_string = request.headers["apikey"]
|
480
548
|
|
481
|
-
if not
|
482
|
-
|
483
|
-
if not
|
484
|
-
|
485
|
-
if not
|
486
|
-
|
487
|
-
|
488
|
-
_error_data["res.api_key_string"] = res.api_key_string
|
549
|
+
if not api_auth_data.api_key_string and "api_key" in request.query_params.keys():
|
550
|
+
api_auth_data.api_key_string = request.query_params["api_key"]
|
551
|
+
if not api_auth_data.api_key_string and "api-key" in request.query_params.keys():
|
552
|
+
api_auth_data.api_key_string = request.query_params["api-key"]
|
553
|
+
if not api_auth_data.api_key_string and "apikey" in request.query_params.keys():
|
554
|
+
api_auth_data.api_key_string = request.query_params["apikey"]
|
489
555
|
|
490
556
|
# token
|
491
557
|
|
492
|
-
|
558
|
+
api_auth_data.token_string = ac.credentials if ac and ac.credentials and ac.credentials.strip() else None
|
493
559
|
|
494
|
-
if not
|
495
|
-
|
560
|
+
if not api_auth_data.token_string and "token" in request.headers.keys():
|
561
|
+
api_auth_data.token_string = request.headers["token"]
|
496
562
|
|
497
|
-
if not
|
498
|
-
|
499
|
-
if not
|
500
|
-
|
501
|
-
if not
|
502
|
-
|
563
|
+
if not api_auth_data.token_string and "user_token" in request.headers.keys():
|
564
|
+
api_auth_data.token_string = request.headers["user_token"]
|
565
|
+
if not api_auth_data.token_string and "user-token" in request.headers.keys():
|
566
|
+
api_auth_data.token_string = request.headers["user-token"]
|
567
|
+
if not api_auth_data.token_string and "usertoken" in request.headers.keys():
|
568
|
+
api_auth_data.token_string = request.headers["usertoken"]
|
503
569
|
|
504
|
-
if not
|
505
|
-
|
570
|
+
if not api_auth_data.token_string and "token" in request.query_params.keys():
|
571
|
+
api_auth_data.token_string = request.query_params["token"]
|
506
572
|
|
507
|
-
if not
|
508
|
-
|
509
|
-
if not
|
510
|
-
|
511
|
-
if not
|
512
|
-
|
573
|
+
if not api_auth_data.token_string and "user_token" in request.query_params.keys():
|
574
|
+
api_auth_data.token_string = request.query_params["user_token"]
|
575
|
+
if not api_auth_data.token_string and "user-token" in request.query_params.keys():
|
576
|
+
api_auth_data.token_string = request.query_params["user-token"]
|
577
|
+
if not api_auth_data.token_string and "usertoken" in request.query_params.keys():
|
578
|
+
api_auth_data.token_string = request.query_params["usertoken"]
|
513
579
|
|
514
|
-
if
|
515
|
-
|
516
|
-
if not
|
517
|
-
|
580
|
+
if api_auth_data.token_string:
|
581
|
+
api_auth_data.token_string = api_auth_data.token_string.strip()
|
582
|
+
if not api_auth_data.token_string:
|
583
|
+
api_auth_data.token_string = None
|
518
584
|
|
519
|
-
|
585
|
+
# api_key
|
520
586
|
|
521
|
-
if require_api_key_string and not
|
587
|
+
if require_api_key_string and not api_auth_data.api_key_string:
|
522
588
|
raise APIException(
|
523
589
|
status_code=starlette.status.HTTP_401_UNAUTHORIZED,
|
524
|
-
error_code=
|
525
|
-
error_data=
|
590
|
+
error_code=BaseAPIErrorCodes.cannot_authorize,
|
591
|
+
error_data=safely_transfer_obj_to_json_str_to_json_obj(api_auth_data.model_dump())
|
526
592
|
)
|
527
593
|
|
528
|
-
|
594
|
+
# token
|
595
|
+
|
596
|
+
if require_token_string and not api_auth_data.token_string:
|
529
597
|
raise APIException(
|
530
598
|
status_code=starlette.status.HTTP_401_UNAUTHORIZED,
|
531
|
-
error_code=
|
532
|
-
error_data=
|
599
|
+
error_code=BaseAPIErrorCodes.cannot_authorize,
|
600
|
+
error_data=safely_transfer_obj_to_json_str_to_json_obj(api_auth_data.model_dump())
|
533
601
|
)
|
534
602
|
|
535
|
-
|
603
|
+
# api_key
|
604
|
+
|
605
|
+
if validate_api_key_func is not None:
|
606
|
+
validate_api_key_func_data = validate_api_key_func(
|
607
|
+
api_key_string=api_auth_data.api_key_string,
|
608
|
+
token_string=api_auth_data.token_string,
|
609
|
+
base_api_auth_data=api_auth_data,
|
610
|
+
transmitted_api_data=transmitted_api_data,
|
611
|
+
request=request,
|
612
|
+
**kwargs
|
613
|
+
)
|
614
|
+
if is_async_object(validate_api_key_func_data):
|
615
|
+
validate_api_key_func_data = await validate_api_key_func_data
|
616
|
+
api_auth_data.is_api_key_string_correct = validate_api_key_func_data
|
617
|
+
|
618
|
+
# token
|
619
|
+
|
620
|
+
if validate_token_func is not None:
|
621
|
+
validate_token_func_data = validate_token_func(
|
622
|
+
api_key_string=api_auth_data.api_key_string,
|
623
|
+
token_string=api_auth_data.token_string,
|
624
|
+
base_api_auth_data=api_auth_data,
|
625
|
+
transmitted_api_data=transmitted_api_data,
|
626
|
+
request=request,
|
627
|
+
**kwargs
|
628
|
+
)
|
629
|
+
if is_async_object(validate_token_func_data):
|
630
|
+
validate_token_func_data_data = await validate_token_func_data
|
631
|
+
api_auth_data.is_token_string_correct = validate_token_func_data_data
|
632
|
+
|
633
|
+
# api_key
|
634
|
+
|
635
|
+
if require_correct_api_key:
|
636
|
+
if not api_auth_data.is_api_key_string_correct:
|
637
|
+
raise APIException(
|
638
|
+
status_code=starlette.status.HTTP_401_UNAUTHORIZED,
|
639
|
+
error_code=BaseAPIErrorCodes.cannot_authorize,
|
640
|
+
error_data=safely_transfer_obj_to_json_str_to_json_obj(api_auth_data.model_dump())
|
641
|
+
)
|
642
|
+
|
643
|
+
# token
|
644
|
+
|
645
|
+
if require_correct_token:
|
646
|
+
if not api_auth_data.is_token_string_correct:
|
647
|
+
raise APIException(
|
648
|
+
status_code=starlette.status.HTTP_401_UNAUTHORIZED,
|
649
|
+
error_code=BaseAPIErrorCodes.cannot_authorize,
|
650
|
+
error_data=safely_transfer_obj_to_json_str_to_json_obj(api_auth_data.model_dump())
|
651
|
+
)
|
652
|
+
|
653
|
+
return api_auth_data
|
536
654
|
|
537
655
|
return func
|
538
656
|
|
@@ -554,7 +672,7 @@ def simple_api_router_for_testing():
|
|
554
672
|
async def _():
|
555
673
|
raise APIException(
|
556
674
|
error_code="raise_fake_exception_2",
|
557
|
-
|
675
|
+
error_specification_code="raise_fake_exception_2",
|
558
676
|
error_description="raise_fake_exception_2"
|
559
677
|
)
|
560
678
|
|
@@ -566,7 +684,7 @@ def simple_api_router_for_testing():
|
|
566
684
|
raise Exception("raise_fake_exception_3")
|
567
685
|
|
568
686
|
@router.get(
|
569
|
-
"/
|
687
|
+
"/check_params_1",
|
570
688
|
response_model=ErrorSO
|
571
689
|
)
|
572
690
|
async def _(name: int = Query()):
|
@@ -575,8 +693,8 @@ def simple_api_router_for_testing():
|
|
575
693
|
return router
|
576
694
|
|
577
695
|
|
578
|
-
|
579
|
-
"name": "
|
696
|
+
DEFAULT_CONTACT = {
|
697
|
+
"name": "ARPAKIT Company",
|
580
698
|
"email": "support@arpakit.com"
|
581
699
|
}
|
582
700
|
|
@@ -586,23 +704,32 @@ def create_fastapi_app(
|
|
586
704
|
title: str = "arpakitlib FastAPI",
|
587
705
|
description: str | None = "arpakitlib FastAPI",
|
588
706
|
log_filepath: str | None = "./story.log",
|
589
|
-
handle_exception_: Callable | None =
|
590
|
-
startup_api_events: list[BaseStartupAPIEvent] | None = None,
|
591
|
-
shutdown_api_events: list[BaseShutdownAPIEvent] | None = None,
|
707
|
+
handle_exception_: Callable | None = None,
|
708
|
+
startup_api_events: list[BaseStartupAPIEvent | None] | None = None,
|
709
|
+
shutdown_api_events: list[BaseShutdownAPIEvent | None] | None = None,
|
592
710
|
transmitted_api_data: BaseTransmittedAPIData = BaseTransmittedAPIData(),
|
593
711
|
main_api_router: APIRouter = simple_api_router_for_testing(),
|
594
|
-
contact: dict[str, Any] | None = None
|
712
|
+
contact: dict[str, Any] | None = None,
|
713
|
+
media_dirpath: str | None = None,
|
714
|
+
static_dirpath: str | None = None
|
595
715
|
):
|
596
|
-
|
597
|
-
contact = _DEFAULT_CONTACT
|
716
|
+
_logger.info("start create_fastapi_app")
|
598
717
|
|
599
718
|
setup_normal_logging(log_filepath=log_filepath)
|
600
719
|
|
720
|
+
if handle_exception_ is None:
|
721
|
+
handle_exception_ = create_handle_exception()
|
722
|
+
|
723
|
+
if contact is None:
|
724
|
+
contact = DEFAULT_CONTACT
|
725
|
+
|
601
726
|
if not startup_api_events:
|
602
727
|
startup_api_events = [BaseStartupAPIEvent()]
|
728
|
+
startup_api_events = [v for v in startup_api_events if v is not None]
|
603
729
|
|
604
730
|
if not shutdown_api_events:
|
605
731
|
shutdown_api_events = [BaseShutdownAPIEvent()]
|
732
|
+
shutdown_api_events = [v for v in shutdown_api_events if v is not None]
|
606
733
|
|
607
734
|
app = FastAPI(
|
608
735
|
title=title,
|
@@ -615,27 +742,33 @@ def create_fastapi_app(
|
|
615
742
|
contact=contact
|
616
743
|
)
|
617
744
|
|
745
|
+
if media_dirpath is not None:
|
746
|
+
if not os.path.exists(media_dirpath):
|
747
|
+
os.makedirs(media_dirpath, exist_ok=True)
|
748
|
+
app.mount("/media", StaticFiles(directory=media_dirpath), name="media")
|
749
|
+
|
750
|
+
if static_dirpath is not None:
|
751
|
+
if not os.path.exists(static_dirpath):
|
752
|
+
os.makedirs(static_dirpath, exist_ok=True)
|
753
|
+
app.mount("/static", StaticFiles(directory=static_dirpath), name="static")
|
754
|
+
|
618
755
|
app.state.transmitted_api_data = transmitted_api_data
|
619
756
|
|
620
757
|
add_cors_to_app(app=app)
|
621
758
|
|
622
759
|
add_swagger_to_app(app=app)
|
623
760
|
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
)
|
629
|
-
else:
|
630
|
-
add_exception_handler_to_app(
|
631
|
-
app=app,
|
632
|
-
handle_exception=create_handle_exception()
|
633
|
-
)
|
761
|
+
add_exception_handler_to_app(
|
762
|
+
app=app,
|
763
|
+
handle_exception=handle_exception_
|
764
|
+
)
|
634
765
|
|
635
766
|
add_needed_api_router_to_app(app=app)
|
636
767
|
|
637
768
|
app.include_router(router=main_api_router)
|
638
769
|
|
770
|
+
_logger.info("finish create_fastapi_app")
|
771
|
+
|
639
772
|
return app
|
640
773
|
|
641
774
|
|