arpakitlib 1.5.34__tar.gz → 1.5.37__tar.gz
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-1.5.34 → arpakitlib-1.5.37}/PKG-INFO +1 -1
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_arpakit_schedule_uust_api_client.py +3 -3
- arpakitlib-1.5.34/arpakitlib/ar_enumeration.py → arpakitlib-1.5.37/arpakitlib/ar_enumeration_util.py +1 -1
- arpakitlib-1.5.37/arpakitlib/ar_fastapi_util.py +533 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_need_type_util.py +2 -2
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_openai_util.py +1 -1
- arpakitlib-1.5.34/arpakitlib/ar_operation_util.py → arpakitlib-1.5.37/arpakitlib/ar_operation_execution_util.py +50 -112
- arpakitlib-1.5.37/arpakitlib/ar_sqlalchemy_model_util.py +122 -0
- arpakitlib-1.5.34/arpakitlib/ar_easy_sqlalchemy_util.py → arpakitlib-1.5.37/arpakitlib/ar_sqlalchemy_util.py +2 -16
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_type_util.py +4 -8
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_yookassa_api_client.py +2 -2
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/pyproject.toml +1 -1
- arpakitlib-1.5.34/arpakitlib/ar_fastapi_util.py +0 -328
- arpakitlib-1.5.34/arpakitlib/ar_sqlalchemy_model_util.py +0 -47
- arpakitlib-1.5.34/arpakitlib/ar_story_log_util.py +0 -40
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/LICENSE +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/NOTICE +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/README.md +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/AUTHOR.md +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/LICENSE +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/NOTICE +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/README.md +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/__init__.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_additional_model_util.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_aiogram_util.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_arpakit_lib_module_util.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_arpakitlib_info.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_base64_util.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_base_worker.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_cache_file.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_datetime_util.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_dict_util.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_dream_ai_api_client.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_encrypt_and_decrypt_util.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_fastapi_static/redoc/redoc.standalone.js +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_fastapi_static/swagger-ui/favicon-16x16.png +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_fastapi_static/swagger-ui/favicon-32x32.png +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_fastapi_static/swagger-ui/index.css +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_fastapi_static/swagger-ui/index.html +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_fastapi_static/swagger-ui/oauth2-redirect.html +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_fastapi_static/swagger-ui/swagger-initializer.js +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui-bundle.js +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui-bundle.js.map +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui-es-bundle-core.js +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui-es-bundle-core.js.map +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui-es-bundle.js +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui-es-bundle.js.map +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui-standalone-preset.js +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui-standalone-preset.js.map +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui.css +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui.css.map +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui.js +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui.js.map +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_file_storage_in_dir.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_generate_env_example.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_hash_util.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_http_request_util.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_ip_util.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_json_db.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_json_util.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_jwt_util.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_list_of_dicts_to_xlsx.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_list_util.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_logging_util.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_mongodb_util.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_parse_command.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_postgresql_util.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_run_cmd.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_schedule_uust_api_client.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_sleep_util.py +0 -0
- /arpakitlib-1.5.34/arpakitlib/ar_ssh_runner.py → /arpakitlib-1.5.37/arpakitlib/ar_ssh_util.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_str_util.py +0 -0
- {arpakitlib-1.5.34 → arpakitlib-1.5.37}/arpakitlib/ar_zabbix_util.py +0 -0
@@ -16,7 +16,7 @@ from aiohttp import ClientResponse, ClientTimeout, ClientResponseError
|
|
16
16
|
from pydantic import ConfigDict, BaseModel
|
17
17
|
|
18
18
|
from arpakitlib.ar_dict_util import combine_dicts
|
19
|
-
from arpakitlib.
|
19
|
+
from arpakitlib.ar_enumeration_util import Enumeration
|
20
20
|
from arpakitlib.ar_json_util import safely_transfer_to_json_str
|
21
21
|
from arpakitlib.ar_sleep_util import async_safe_sleep
|
22
22
|
from arpakitlib.ar_type_util import raise_for_type
|
@@ -24,7 +24,7 @@ from arpakitlib.ar_type_util import raise_for_type
|
|
24
24
|
_ARPAKIT_LIB_MODULE_VERSION = "3.0"
|
25
25
|
|
26
26
|
|
27
|
-
class Weekdays(
|
27
|
+
class Weekdays(Enumeration):
|
28
28
|
monday = 1
|
29
29
|
tuesday = 2
|
30
30
|
wednesday = 3
|
@@ -34,7 +34,7 @@ class Weekdays(EasyEnumeration):
|
|
34
34
|
sunday = 7
|
35
35
|
|
36
36
|
|
37
|
-
class Months(
|
37
|
+
class Months(Enumeration):
|
38
38
|
january = 1
|
39
39
|
february = 2
|
40
40
|
march = 3
|
@@ -0,0 +1,533 @@
|
|
1
|
+
# arpakit
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import asyncio
|
6
|
+
import logging
|
7
|
+
import os.path
|
8
|
+
import pathlib
|
9
|
+
import threading
|
10
|
+
import traceback
|
11
|
+
from datetime import datetime
|
12
|
+
from typing import Any, Callable
|
13
|
+
|
14
|
+
import fastapi.exceptions
|
15
|
+
import fastapi.responses
|
16
|
+
import starlette.exceptions
|
17
|
+
import starlette.requests
|
18
|
+
import starlette.status
|
19
|
+
from fastapi import FastAPI, APIRouter, Query
|
20
|
+
from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html
|
21
|
+
from jaraco.context import suppress
|
22
|
+
from pydantic import BaseModel, ConfigDict
|
23
|
+
from starlette.middleware.cors import CORSMiddleware
|
24
|
+
from starlette.staticfiles import StaticFiles
|
25
|
+
|
26
|
+
from arpakitlib.ar_dict_util import combine_dicts
|
27
|
+
from arpakitlib.ar_enumeration_util import Enumeration
|
28
|
+
from arpakitlib.ar_json_util import safely_transfer_to_json_str_to_json_obj
|
29
|
+
from arpakitlib.ar_logging_util import setup_normal_logging
|
30
|
+
from arpakitlib.ar_operation_execution_util import ExecuteOperationWorker
|
31
|
+
from arpakitlib.ar_sqlalchemy_model_util import StoryLogDBM
|
32
|
+
from arpakitlib.ar_sqlalchemy_util import SQLAlchemyDB
|
33
|
+
from arpakitlib.ar_type_util import raise_for_type, raise_if_not_async_func
|
34
|
+
|
35
|
+
_ARPAKIT_LIB_MODULE_VERSION = "3.0"
|
36
|
+
|
37
|
+
_logger = logging.getLogger(__name__)
|
38
|
+
|
39
|
+
|
40
|
+
class BaseSchema(BaseModel):
|
41
|
+
model_config = ConfigDict(extra="ignore", arbitrary_types_allowed=True, from_attributes=True)
|
42
|
+
|
43
|
+
@classmethod
|
44
|
+
def __pydantic_init_subclass__(cls, **kwargs: Any) -> None:
|
45
|
+
if not (
|
46
|
+
cls.__name__.endswith("SO")
|
47
|
+
or cls.__name__.endswith("SI")
|
48
|
+
or cls.__name__.endswith("SchemaIn")
|
49
|
+
or cls.__name__.endswith("SchemaOut")
|
50
|
+
):
|
51
|
+
raise ValueError("APISchema class should ends with SO | SI | SchemaIn | SchemaOut")
|
52
|
+
super().__init_subclass__(**kwargs)
|
53
|
+
|
54
|
+
|
55
|
+
class BaseSI(BaseSchema):
|
56
|
+
pass
|
57
|
+
|
58
|
+
|
59
|
+
class BaseSO(BaseSchema):
|
60
|
+
pass
|
61
|
+
|
62
|
+
|
63
|
+
class SimpleSO(BaseSO):
|
64
|
+
id: int
|
65
|
+
long_id: str
|
66
|
+
creation_dt: datetime
|
67
|
+
|
68
|
+
|
69
|
+
class ErrorSO(BaseSO):
|
70
|
+
class APIErrorCodes(Enumeration):
|
71
|
+
cannot_authorize = "CANNOT_AUTHORIZE"
|
72
|
+
unknown_error = "UNKNOWN_ERROR"
|
73
|
+
error_in_request = "ERROR_IN_REQUEST"
|
74
|
+
not_found = "NOT_FOUND"
|
75
|
+
|
76
|
+
has_error: bool = True
|
77
|
+
error_code: str | None = None
|
78
|
+
error_code_specification: str | None = None
|
79
|
+
error_description: str | None = None
|
80
|
+
error_data: dict[str, Any] = {}
|
81
|
+
|
82
|
+
|
83
|
+
class RawDataSO(BaseSO):
|
84
|
+
data: dict[str, Any] = {}
|
85
|
+
|
86
|
+
|
87
|
+
class StoryLogSO(SimpleSO):
|
88
|
+
level: str
|
89
|
+
title: str | None
|
90
|
+
data: dict[str, Any]
|
91
|
+
|
92
|
+
|
93
|
+
class OperationSO(SimpleSO):
|
94
|
+
execution_start_dt: datetime | None
|
95
|
+
execution_finish_dt: datetime | None
|
96
|
+
status: str
|
97
|
+
type: str
|
98
|
+
input_data: dict[str, Any]
|
99
|
+
output_data: dict[str, Any]
|
100
|
+
error_data: dict[str, Any]
|
101
|
+
duration_total_seconds: float | None
|
102
|
+
|
103
|
+
|
104
|
+
class APIJSONResponse(fastapi.responses.JSONResponse):
|
105
|
+
def __init__(self, *, content: BaseSO, status_code: int = starlette.status.HTTP_200_OK):
|
106
|
+
self.content_ = content
|
107
|
+
self.status_code_ = status_code
|
108
|
+
super().__init__(
|
109
|
+
content=safely_transfer_to_json_str_to_json_obj(content.model_dump()),
|
110
|
+
status_code=status_code
|
111
|
+
)
|
112
|
+
|
113
|
+
|
114
|
+
class APIException(fastapi.exceptions.HTTPException):
|
115
|
+
def __init__(
|
116
|
+
self,
|
117
|
+
*,
|
118
|
+
status_code: int = starlette.status.HTTP_400_BAD_REQUEST,
|
119
|
+
error_code: str | None = ErrorSO.APIErrorCodes.unknown_error,
|
120
|
+
error_code_specification: str | None = None,
|
121
|
+
error_description: str | None = None,
|
122
|
+
error_data: dict[str, Any] | None = None
|
123
|
+
):
|
124
|
+
self.status_code = status_code
|
125
|
+
self.error_code = error_code
|
126
|
+
self.error_code_specification = error_code_specification
|
127
|
+
self.error_description = error_description
|
128
|
+
if error_data is None:
|
129
|
+
error_data = {}
|
130
|
+
self.error_data = error_data
|
131
|
+
|
132
|
+
self.error_so = ErrorSO(
|
133
|
+
has_error=True,
|
134
|
+
error_code=self.error_code,
|
135
|
+
error_specification=self.error_code_specification,
|
136
|
+
error_description=self.error_description,
|
137
|
+
error_data=self.error_data
|
138
|
+
)
|
139
|
+
|
140
|
+
super().__init__(
|
141
|
+
status_code=self.status_code,
|
142
|
+
detail=self.error_so.model_dump(mode="json")
|
143
|
+
)
|
144
|
+
|
145
|
+
|
146
|
+
def create_handle_exception(
|
147
|
+
*,
|
148
|
+
funcs_before_response: list[Callable] | None = None,
|
149
|
+
async_funcs_after_response: list[Callable] | None = None,
|
150
|
+
) -> Any:
|
151
|
+
if funcs_before_response is None:
|
152
|
+
funcs_before_response = []
|
153
|
+
|
154
|
+
if async_funcs_after_response is None:
|
155
|
+
async_funcs_after_response = []
|
156
|
+
|
157
|
+
async def handle_exception(
|
158
|
+
request: starlette.requests.Request, exception: Exception
|
159
|
+
) -> APIJSONResponse:
|
160
|
+
status_code = starlette.status.HTTP_500_INTERNAL_SERVER_ERROR
|
161
|
+
|
162
|
+
error_so = ErrorSO(
|
163
|
+
has_error=True,
|
164
|
+
error_code=ErrorSO.APIErrorCodes.unknown_error,
|
165
|
+
error_data={
|
166
|
+
"exception_type": str(type(exception)),
|
167
|
+
"exception_str": str(exception),
|
168
|
+
"request.method": str(request.method),
|
169
|
+
"request.url": str(request.url),
|
170
|
+
}
|
171
|
+
)
|
172
|
+
|
173
|
+
if isinstance(exception, APIException):
|
174
|
+
old_error_data = error_so.error_data
|
175
|
+
error_so = exception.error_so
|
176
|
+
error_so.error_data = combine_dicts(old_error_data, error_so.error_data)
|
177
|
+
_need_exc_info = False
|
178
|
+
|
179
|
+
elif isinstance(exception, starlette.exceptions.HTTPException):
|
180
|
+
status_code = exception.status_code
|
181
|
+
if status_code in (starlette.status.HTTP_403_FORBIDDEN, starlette.status.HTTP_401_UNAUTHORIZED):
|
182
|
+
error_so.error_code = ErrorSO.APIErrorCodes.cannot_authorize
|
183
|
+
_need_exc_info = False
|
184
|
+
elif status_code == starlette.status.HTTP_404_NOT_FOUND:
|
185
|
+
error_so.error_code = ErrorSO.APIErrorCodes.not_found
|
186
|
+
_need_exc_info = False
|
187
|
+
else:
|
188
|
+
status_code = starlette.status.HTTP_500_INTERNAL_SERVER_ERROR
|
189
|
+
_need_exc_info = True
|
190
|
+
with suppress(Exception):
|
191
|
+
error_so.error_data["exception.detail"] = exception.detail
|
192
|
+
|
193
|
+
elif isinstance(exception, fastapi.exceptions.RequestValidationError):
|
194
|
+
status_code = starlette.status.HTTP_422_UNPROCESSABLE_ENTITY
|
195
|
+
error_so.error_code = ErrorSO.APIErrorCodes.error_in_request
|
196
|
+
with suppress(Exception):
|
197
|
+
error_so.error_data["exception.errors"] = str(exception.errors()) if exception.errors() else {}
|
198
|
+
_need_exc_info = False
|
199
|
+
|
200
|
+
else:
|
201
|
+
status_code = starlette.status.HTTP_500_INTERNAL_SERVER_ERROR
|
202
|
+
error_so.error_code = ErrorSO.APIErrorCodes.unknown_error
|
203
|
+
_logger.exception(exception)
|
204
|
+
_need_exc_info = True
|
205
|
+
|
206
|
+
if error_so.error_code:
|
207
|
+
error_so.error_code = error_so.error_code.upper().replace(" ", "_").strip()
|
208
|
+
|
209
|
+
if error_so.error_code_specification:
|
210
|
+
error_so.error_code_specification = (
|
211
|
+
error_so.error_code_specification.upper().replace(" ", "_").strip()
|
212
|
+
)
|
213
|
+
|
214
|
+
if _need_exc_info:
|
215
|
+
_logger.error(str(exception), exc_info=exception)
|
216
|
+
else:
|
217
|
+
_logger.error(str(exception))
|
218
|
+
|
219
|
+
_kwargs = {}
|
220
|
+
for func in funcs_before_response:
|
221
|
+
data = func(
|
222
|
+
error_so=error_so, status_code=status_code, request=request, exception=exception, **_kwargs
|
223
|
+
)
|
224
|
+
if asyncio.iscoroutine(data):
|
225
|
+
data = await data
|
226
|
+
if data is not None:
|
227
|
+
status_code, error_so, _kwargs = data[0], data[1], data[2]
|
228
|
+
raise_for_type(status_code, int)
|
229
|
+
raise_for_type(error_so, ErrorSO)
|
230
|
+
raise_for_type(_kwargs, dict)
|
231
|
+
|
232
|
+
for async_func_after_response in async_funcs_after_response:
|
233
|
+
raise_if_not_async_func(async_func_after_response)
|
234
|
+
_ = asyncio.create_task(async_func_after_response(
|
235
|
+
error_so=error_so, status_code=status_code, request=request, exception=exception
|
236
|
+
))
|
237
|
+
|
238
|
+
return APIJSONResponse(
|
239
|
+
content=error_so,
|
240
|
+
status_code=status_code
|
241
|
+
)
|
242
|
+
|
243
|
+
return handle_exception
|
244
|
+
|
245
|
+
|
246
|
+
def create_handle_exception_creating_story_log(
|
247
|
+
*,
|
248
|
+
sqlalchemy_db: SQLAlchemyDB
|
249
|
+
) -> Callable:
|
250
|
+
def handle_exception(
|
251
|
+
*,
|
252
|
+
error_so: ErrorSO,
|
253
|
+
status_code: int,
|
254
|
+
request: starlette.requests.Request,
|
255
|
+
exception: Exception,
|
256
|
+
**kwargs
|
257
|
+
) -> (int, ErrorSO, dict[str, Any]):
|
258
|
+
sqlalchemy_db.init()
|
259
|
+
traceback_str = "".join(traceback.format_exception(type(exception), exception, exception.__traceback__))
|
260
|
+
with sqlalchemy_db.new_session() as session:
|
261
|
+
story_log_dbm = StoryLogDBM(
|
262
|
+
level=StoryLogDBM.Levels.error,
|
263
|
+
title=str(exception),
|
264
|
+
data={
|
265
|
+
"error_so": error_so.model_dump(),
|
266
|
+
"traceback_str": traceback_str
|
267
|
+
}
|
268
|
+
)
|
269
|
+
session.add(story_log_dbm)
|
270
|
+
session.commit()
|
271
|
+
session.refresh(story_log_dbm)
|
272
|
+
error_so.error_data.update({"story_log_long_id": story_log_dbm.long_id})
|
273
|
+
kwargs["story_log_id"] = story_log_dbm.id
|
274
|
+
return status_code, error_so, kwargs
|
275
|
+
|
276
|
+
return handle_exception
|
277
|
+
|
278
|
+
|
279
|
+
def add_exception_handler_to_app(*, app: FastAPI, handle_exception: Callable) -> FastAPI:
|
280
|
+
app.add_exception_handler(
|
281
|
+
exc_class_or_status_code=Exception,
|
282
|
+
handler=handle_exception
|
283
|
+
)
|
284
|
+
app.add_exception_handler(
|
285
|
+
exc_class_or_status_code=ValueError,
|
286
|
+
handler=handle_exception
|
287
|
+
)
|
288
|
+
app.add_exception_handler(
|
289
|
+
exc_class_or_status_code=fastapi.exceptions.RequestValidationError,
|
290
|
+
handler=handle_exception
|
291
|
+
)
|
292
|
+
app.add_exception_handler(
|
293
|
+
exc_class_or_status_code=starlette.exceptions.HTTPException,
|
294
|
+
handler=handle_exception
|
295
|
+
)
|
296
|
+
return app
|
297
|
+
|
298
|
+
|
299
|
+
def add_swagger_to_app(
|
300
|
+
*,
|
301
|
+
app: FastAPI,
|
302
|
+
favicon_url: str | None = None
|
303
|
+
):
|
304
|
+
app.mount(
|
305
|
+
"/ar_fastapi_static",
|
306
|
+
StaticFiles(directory=os.path.join(str(pathlib.Path(__file__).parent), "ar_fastapi_static")),
|
307
|
+
name="ar_fastapi_static"
|
308
|
+
)
|
309
|
+
|
310
|
+
@app.get("/docs", include_in_schema=False)
|
311
|
+
async def custom_swagger_ui_html():
|
312
|
+
return get_swagger_ui_html(
|
313
|
+
openapi_url=app.openapi_url,
|
314
|
+
title=app.title,
|
315
|
+
swagger_js_url="/ar_fastapi_static/swagger-ui/swagger-ui-bundle.js",
|
316
|
+
swagger_css_url="/ar_fastapi_static/swagger-ui/swagger-ui.css",
|
317
|
+
swagger_favicon_url=favicon_url
|
318
|
+
)
|
319
|
+
|
320
|
+
@app.get("/redoc", include_in_schema=False)
|
321
|
+
async def custom_redoc_html():
|
322
|
+
return get_redoc_html(
|
323
|
+
openapi_url=app.openapi_url,
|
324
|
+
title=app.title,
|
325
|
+
redoc_js_url="/ar_fastapi_static/redoc/redoc.standalone.js",
|
326
|
+
redoc_favicon_url=favicon_url
|
327
|
+
)
|
328
|
+
|
329
|
+
return app
|
330
|
+
|
331
|
+
|
332
|
+
def add_cors_to_app(*, app: FastAPI):
|
333
|
+
app.add_middleware(
|
334
|
+
CORSMiddleware,
|
335
|
+
allow_origins=["*"],
|
336
|
+
allow_credentials=True,
|
337
|
+
allow_methods=["*"],
|
338
|
+
allow_headers=["*"],
|
339
|
+
)
|
340
|
+
return app
|
341
|
+
|
342
|
+
|
343
|
+
def add_needed_api_router_to_app(*, app: FastAPI):
|
344
|
+
api_router = APIRouter()
|
345
|
+
|
346
|
+
@api_router.get(
|
347
|
+
"/healthcheck",
|
348
|
+
response_model=ErrorSO,
|
349
|
+
status_code=starlette.status.HTTP_200_OK,
|
350
|
+
tags=["Healthcheck"]
|
351
|
+
)
|
352
|
+
async def _():
|
353
|
+
return APIJSONResponse(
|
354
|
+
status_code=starlette.status.HTTP_200_OK,
|
355
|
+
content=RawDataSO(data={"healthcheck": "healthcheck"})
|
356
|
+
)
|
357
|
+
|
358
|
+
@api_router.get(
|
359
|
+
"/arpakitlib",
|
360
|
+
response_model=ErrorSO,
|
361
|
+
status_code=starlette.status.HTTP_200_OK,
|
362
|
+
tags=["arpakitlib"]
|
363
|
+
)
|
364
|
+
async def _():
|
365
|
+
return APIJSONResponse(
|
366
|
+
status_code=starlette.status.HTTP_200_OK,
|
367
|
+
content=RawDataSO(data={"arpakitlib": "arpakitlib"})
|
368
|
+
)
|
369
|
+
|
370
|
+
app.include_router(router=api_router, prefix="")
|
371
|
+
|
372
|
+
return app
|
373
|
+
|
374
|
+
|
375
|
+
class BaseStartupAPIEvent:
|
376
|
+
def __init__(self, *args, **kwargs):
|
377
|
+
self._logger = logging.getLogger(self.__class__.__name__)
|
378
|
+
|
379
|
+
async def async_on_startup(self, *args, **kwargs):
|
380
|
+
self._logger.info("on_startup starts")
|
381
|
+
self._logger.info("on_startup ends")
|
382
|
+
|
383
|
+
|
384
|
+
class InitSqlalchemyDBStartupAPIEvent(BaseStartupAPIEvent):
|
385
|
+
def __init__(self, sqlalchemy_db: SQLAlchemyDB):
|
386
|
+
super().__init__()
|
387
|
+
self.sqlalchemy_db = sqlalchemy_db
|
388
|
+
|
389
|
+
def async_on_startup(self, *args, **kwargs):
|
390
|
+
self.sqlalchemy_db.init()
|
391
|
+
|
392
|
+
|
393
|
+
class SyncSafeRunExecuteOperationWorkerStartupAPIEvent(BaseStartupAPIEvent):
|
394
|
+
def __init__(self, execute_operation_worker: ExecuteOperationWorker):
|
395
|
+
super().__init__()
|
396
|
+
self.execute_operation_worker = execute_operation_worker
|
397
|
+
|
398
|
+
def async_on_startup(self, *args, **kwargs):
|
399
|
+
thread = threading.Thread(
|
400
|
+
target=self.execute_operation_worker.sync_safe_run,
|
401
|
+
daemon=True
|
402
|
+
)
|
403
|
+
thread.start()
|
404
|
+
|
405
|
+
|
406
|
+
class AsyncSafeRunExecuteOperationWorkerStartupAPIEvent(BaseStartupAPIEvent):
|
407
|
+
def __init__(self, execute_operation_worker: ExecuteOperationWorker):
|
408
|
+
super().__init__()
|
409
|
+
self.execute_operation_worker = execute_operation_worker
|
410
|
+
|
411
|
+
def async_on_startup(self, *args, **kwargs):
|
412
|
+
_ = asyncio.create_task(self.execute_operation_worker.async_safe_run())
|
413
|
+
|
414
|
+
|
415
|
+
class BaseShutdownAPIEvent:
|
416
|
+
def __init__(self, *args, **kwargs):
|
417
|
+
self._logger = logging.getLogger(self.__class__.__name__)
|
418
|
+
|
419
|
+
async def async_on_shutdown(self, *args, **kwargs):
|
420
|
+
self._logger.info("on_shutdown starts")
|
421
|
+
self._logger.info("on_shutdown ends")
|
422
|
+
|
423
|
+
|
424
|
+
class BaseTransmittedAPIData(BaseModel):
|
425
|
+
model_config = ConfigDict(extra="ignore", arbitrary_types_allowed=True, from_attributes=True)
|
426
|
+
|
427
|
+
|
428
|
+
def get_transmitted_api_data(request: starlette.requests.Request) -> BaseTransmittedAPIData:
|
429
|
+
return request.app.state.transmitted_api_data
|
430
|
+
|
431
|
+
|
432
|
+
def simple_api_router_for_testing():
|
433
|
+
router = APIRouter(tags=["Testing"])
|
434
|
+
|
435
|
+
@router.get(
|
436
|
+
"/raise_fake_exception_1",
|
437
|
+
response_model=ErrorSO
|
438
|
+
)
|
439
|
+
async def _():
|
440
|
+
raise fastapi.HTTPException(status_code=starlette.status.HTTP_500_INTERNAL_SERVER_ERROR)
|
441
|
+
|
442
|
+
@router.get(
|
443
|
+
"/raise_fake_exception_2",
|
444
|
+
response_model=ErrorSO
|
445
|
+
)
|
446
|
+
async def _():
|
447
|
+
raise APIException(
|
448
|
+
error_code="raise_fake_exception_2",
|
449
|
+
error_code_specification="raise_fake_exception_2",
|
450
|
+
error_description="raise_fake_exception_2"
|
451
|
+
)
|
452
|
+
|
453
|
+
@router.get(
|
454
|
+
"/raise_fake_exception_3",
|
455
|
+
response_model=ErrorSO
|
456
|
+
)
|
457
|
+
async def _():
|
458
|
+
raise Exception("raise_fake_exception_3")
|
459
|
+
|
460
|
+
@router.get(
|
461
|
+
"/check_params",
|
462
|
+
response_model=ErrorSO
|
463
|
+
)
|
464
|
+
async def _(name: int = Query()):
|
465
|
+
return RawDataSO(data={"name": name})
|
466
|
+
|
467
|
+
return router
|
468
|
+
|
469
|
+
|
470
|
+
def create_fastapi_app(
|
471
|
+
*,
|
472
|
+
title: str = "ARPAKITLIB FastAPI",
|
473
|
+
description: str | None = "ARPAKITLIB FastAPI",
|
474
|
+
log_filepath: str | None = "./story.log",
|
475
|
+
handle_exception_: Callable | None = create_handle_exception(),
|
476
|
+
startup_api_events: list[BaseStartupAPIEvent] | None = None,
|
477
|
+
shutdown_api_events: list[BaseStartupAPIEvent] | None = None,
|
478
|
+
transmitted_api_data: BaseTransmittedAPIData = BaseTransmittedAPIData(),
|
479
|
+
api_router: APIRouter = simple_api_router_for_testing()
|
480
|
+
):
|
481
|
+
setup_normal_logging(log_filepath=log_filepath)
|
482
|
+
|
483
|
+
if not startup_api_events:
|
484
|
+
startup_api_events = [BaseStartupAPIEvent()]
|
485
|
+
|
486
|
+
if not shutdown_api_events:
|
487
|
+
shutdown_api_events = [BaseShutdownAPIEvent()]
|
488
|
+
|
489
|
+
app = FastAPI(
|
490
|
+
title=title,
|
491
|
+
description=description,
|
492
|
+
docs_url=None,
|
493
|
+
redoc_url=None,
|
494
|
+
openapi_url="/openapi",
|
495
|
+
on_startup=[api_startup_event.async_on_startup for api_startup_event in startup_api_events],
|
496
|
+
on_shutdown=[api_shutdown_event.async_on_shutdown for api_shutdown_event in shutdown_api_events]
|
497
|
+
)
|
498
|
+
|
499
|
+
app.state.transmitted_api_data = transmitted_api_data
|
500
|
+
|
501
|
+
add_cors_to_app(app=app)
|
502
|
+
|
503
|
+
add_swagger_to_app(app=app)
|
504
|
+
|
505
|
+
if handle_exception_:
|
506
|
+
add_exception_handler_to_app(
|
507
|
+
app=app,
|
508
|
+
handle_exception=handle_exception_
|
509
|
+
)
|
510
|
+
else:
|
511
|
+
add_exception_handler_to_app(
|
512
|
+
app=app,
|
513
|
+
handle_exception=create_handle_exception()
|
514
|
+
)
|
515
|
+
|
516
|
+
add_needed_api_router_to_app(app=app)
|
517
|
+
|
518
|
+
app.include_router(router=api_router)
|
519
|
+
|
520
|
+
return app
|
521
|
+
|
522
|
+
|
523
|
+
def __example():
|
524
|
+
pass
|
525
|
+
|
526
|
+
|
527
|
+
async def __async_example():
|
528
|
+
pass
|
529
|
+
|
530
|
+
|
531
|
+
if __name__ == '__main__':
|
532
|
+
__example()
|
533
|
+
asyncio.run(__async_example())
|
@@ -3,12 +3,12 @@
|
|
3
3
|
import json
|
4
4
|
from typing import Any
|
5
5
|
|
6
|
-
from arpakitlib.
|
6
|
+
from arpakitlib.ar_enumeration_util import Enumeration
|
7
7
|
|
8
8
|
_ARPAKIT_LIB_MODULE_VERSION = "3.0"
|
9
9
|
|
10
10
|
|
11
|
-
class NeedTypes(
|
11
|
+
class NeedTypes(Enumeration):
|
12
12
|
str_ = "str"
|
13
13
|
int_ = "int"
|
14
14
|
bool_ = "bool"
|