arpakitlib 1.5.33__py3-none-any.whl → 1.5.36__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/ar_arpakit_schedule_uust_api_client.py +3 -3
- arpakitlib/{ar_enumeration.py → ar_enumeration_util.py} +1 -1
- arpakitlib/ar_fastapi_util.py +350 -135
- arpakitlib/ar_need_type_util.py +2 -2
- arpakitlib/ar_openai_util.py +1 -1
- arpakitlib/ar_operation_execution_util.py +65 -105
- arpakitlib/ar_sqlalchemy_model_util.py +76 -1
- arpakitlib/{ar_easy_sqlalchemy_util.py → ar_sqlalchemy_util.py} +13 -2
- arpakitlib/ar_type_util.py +4 -8
- arpakitlib/ar_yookassa_api_client.py +2 -2
- {arpakitlib-1.5.33.dist-info → arpakitlib-1.5.36.dist-info}/METADATA +1 -1
- {arpakitlib-1.5.33.dist-info → arpakitlib-1.5.36.dist-info}/RECORD +16 -17
- arpakitlib/ar_story_log_util.py +0 -37
- /arpakitlib/{ar_ssh_runner.py → ar_ssh_util.py} +0 -0
- {arpakitlib-1.5.33.dist-info → arpakitlib-1.5.36.dist-info}/LICENSE +0 -0
- {arpakitlib-1.5.33.dist-info → arpakitlib-1.5.36.dist-info}/NOTICE +0 -0
- {arpakitlib-1.5.33.dist-info → arpakitlib-1.5.36.dist-info}/WHEEL +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
|
arpakitlib/ar_fastapi_util.py
CHANGED
@@ -6,6 +6,8 @@ import asyncio
|
|
6
6
|
import logging
|
7
7
|
import os.path
|
8
8
|
import pathlib
|
9
|
+
import threading
|
10
|
+
import traceback
|
9
11
|
from datetime import datetime
|
10
12
|
from typing import Any, Callable
|
11
13
|
|
@@ -14,23 +16,28 @@ import fastapi.responses
|
|
14
16
|
import starlette.exceptions
|
15
17
|
import starlette.requests
|
16
18
|
import starlette.status
|
17
|
-
from fastapi import FastAPI
|
19
|
+
from fastapi import FastAPI, APIRouter, Query
|
18
20
|
from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html
|
21
|
+
from jaraco.context import suppress
|
19
22
|
from pydantic import BaseModel, ConfigDict
|
20
23
|
from starlette.middleware.cors import CORSMiddleware
|
21
24
|
from starlette.staticfiles import StaticFiles
|
22
25
|
|
23
|
-
from arpakitlib.
|
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
|
24
34
|
|
25
35
|
_ARPAKIT_LIB_MODULE_VERSION = "3.0"
|
26
36
|
|
27
37
|
_logger = logging.getLogger(__name__)
|
28
38
|
|
29
39
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
class BaseAPISchema(BaseModel):
|
40
|
+
class BaseSchema(BaseModel):
|
34
41
|
model_config = ConfigDict(extra="ignore", arbitrary_types_allowed=True, from_attributes=True)
|
35
42
|
|
36
43
|
@classmethod
|
@@ -45,26 +52,22 @@ class BaseAPISchema(BaseModel):
|
|
45
52
|
super().__init_subclass__(**kwargs)
|
46
53
|
|
47
54
|
|
48
|
-
class
|
55
|
+
class BaseSI(BaseSchema):
|
49
56
|
pass
|
50
57
|
|
51
58
|
|
52
|
-
class
|
59
|
+
class BaseSO(BaseSchema):
|
53
60
|
pass
|
54
61
|
|
55
62
|
|
56
|
-
class
|
57
|
-
data: dict[str, Any] = {}
|
58
|
-
|
59
|
-
|
60
|
-
class BaseAPISimpleSO(BaseAPISO):
|
63
|
+
class SimpleSO(BaseSO):
|
61
64
|
id: int
|
62
65
|
long_id: str
|
63
66
|
creation_dt: datetime
|
64
67
|
|
65
68
|
|
66
|
-
class
|
67
|
-
class APIErrorCodes(
|
69
|
+
class ErrorSO(BaseSO):
|
70
|
+
class APIErrorCodes(Enumeration):
|
68
71
|
cannot_authorize = "CANNOT_AUTHORIZE"
|
69
72
|
unknown_error = "UNKNOWN_ERROR"
|
70
73
|
error_in_request = "ERROR_IN_REQUEST"
|
@@ -77,13 +80,33 @@ class APIErrorSO(BaseAPISO):
|
|
77
80
|
error_data: dict[str, Any] = {}
|
78
81
|
|
79
82
|
|
80
|
-
|
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
|
81
102
|
|
82
103
|
|
83
104
|
class APIJSONResponse(fastapi.responses.JSONResponse):
|
84
|
-
def __init__(self, *, content:
|
105
|
+
def __init__(self, *, content: BaseSO, status_code: int = starlette.status.HTTP_200_OK):
|
106
|
+
self.content_ = content
|
107
|
+
self.status_code_ = status_code
|
85
108
|
super().__init__(
|
86
|
-
content=content.model_dump(
|
109
|
+
content=safely_transfer_to_json_str_to_json_obj(content.model_dump()),
|
87
110
|
status_code=status_code
|
88
111
|
)
|
89
112
|
|
@@ -93,7 +116,7 @@ class APIException(fastapi.exceptions.HTTPException):
|
|
93
116
|
self,
|
94
117
|
*,
|
95
118
|
status_code: int = starlette.status.HTTP_400_BAD_REQUEST,
|
96
|
-
error_code: str | None =
|
119
|
+
error_code: str | None = ErrorSO.APIErrorCodes.unknown_error,
|
97
120
|
error_code_specification: str | None = None,
|
98
121
|
error_description: str | None = None,
|
99
122
|
error_data: dict[str, Any] | None = None
|
@@ -106,7 +129,7 @@ class APIException(fastapi.exceptions.HTTPException):
|
|
106
129
|
error_data = {}
|
107
130
|
self.error_data = error_data
|
108
131
|
|
109
|
-
self.
|
132
|
+
self.error_so = ErrorSO(
|
110
133
|
has_error=True,
|
111
134
|
error_code=self.error_code,
|
112
135
|
error_specification=self.error_code_specification,
|
@@ -116,193 +139,385 @@ class APIException(fastapi.exceptions.HTTPException):
|
|
116
139
|
|
117
140
|
super().__init__(
|
118
141
|
status_code=self.status_code,
|
119
|
-
detail=self.
|
142
|
+
detail=self.error_so.model_dump(mode="json")
|
120
143
|
)
|
121
144
|
|
122
145
|
|
123
|
-
def
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
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
|
136
161
|
|
137
|
-
|
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
|
+
)
|
138
172
|
|
139
|
-
|
140
|
-
|
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
|
141
199
|
|
142
|
-
elif isinstance(exception, starlette.exceptions.HTTPException):
|
143
|
-
status_code = exception.status_code
|
144
|
-
if status_code in (starlette.status.HTTP_403_FORBIDDEN, starlette.status.HTTP_401_UNAUTHORIZED):
|
145
|
-
easy_api_error_so.error_code = APIErrorSO.APIErrorCodes.cannot_authorize
|
146
|
-
elif status_code == starlette.status.HTTP_404_NOT_FOUND:
|
147
|
-
easy_api_error_so.error_code = APIErrorSO.APIErrorCodes.not_found
|
148
200
|
else:
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
or isinstance(exception.detail, str)
|
154
|
-
or isinstance(exception.detail, int)
|
155
|
-
or isinstance(exception.detail, float)
|
156
|
-
or isinstance(exception.detail, bool)
|
157
|
-
):
|
158
|
-
easy_api_error_so.error_data["raw"] = exception.detail
|
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
|
159
205
|
|
160
|
-
|
161
|
-
|
162
|
-
easy_api_error_so.error_code = APIErrorSO.APIErrorCodes.error_in_request
|
163
|
-
easy_api_error_so.error_data["raw"] = str(exception.errors()) if exception.errors() else {}
|
206
|
+
if error_so.error_code:
|
207
|
+
error_so.error_code = error_so.error_code.upper().replace(" ", "_").strip()
|
164
208
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
_logger.exception(exception)
|
170
|
-
|
171
|
-
if easy_api_error_so.error_code:
|
172
|
-
easy_api_error_so.error_code = easy_api_error_so.error_code.upper().replace(" ", "_").strip()
|
209
|
+
if error_so.error_code_specification:
|
210
|
+
error_so.error_code_specification = (
|
211
|
+
error_so.error_code_specification.upper().replace(" ", "_").strip()
|
212
|
+
)
|
173
213
|
|
174
|
-
|
175
|
-
|
176
|
-
|
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
|
177
241
|
)
|
178
242
|
|
179
|
-
return
|
180
|
-
content=easy_api_error_so,
|
181
|
-
status_code=status_code
|
182
|
-
)
|
183
|
-
|
243
|
+
return handle_exception
|
184
244
|
|
185
|
-
# ---
|
186
245
|
|
187
|
-
|
188
|
-
|
189
|
-
|
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(
|
190
281
|
exc_class_or_status_code=Exception,
|
191
|
-
handler=
|
282
|
+
handler=handle_exception
|
192
283
|
)
|
193
|
-
|
284
|
+
app.add_exception_handler(
|
194
285
|
exc_class_or_status_code=ValueError,
|
195
|
-
handler=
|
286
|
+
handler=handle_exception
|
196
287
|
)
|
197
|
-
|
288
|
+
app.add_exception_handler(
|
198
289
|
exc_class_or_status_code=fastapi.exceptions.RequestValidationError,
|
199
|
-
handler=
|
290
|
+
handler=handle_exception
|
200
291
|
)
|
201
|
-
|
292
|
+
app.add_exception_handler(
|
202
293
|
exc_class_or_status_code=starlette.exceptions.HTTPException,
|
203
|
-
handler=
|
204
|
-
)
|
205
|
-
return fastapi_app
|
206
|
-
|
207
|
-
|
208
|
-
def add_middleware_cors_to_fastapi_app(*, fastapi_app: FastAPI) -> FastAPI:
|
209
|
-
fastapi_app.add_middleware(
|
210
|
-
CORSMiddleware,
|
211
|
-
allow_origins=["*"],
|
212
|
-
allow_credentials=True,
|
213
|
-
allow_methods=["*"],
|
214
|
-
allow_headers=["*"],
|
215
|
-
)
|
216
|
-
return fastapi_app
|
217
|
-
|
218
|
-
|
219
|
-
def add_ar_fastapi_static_to_fastapi_app(*, fastapi_app: FastAPI):
|
220
|
-
ar_fastapi_static_dirpath = os.path.join(str(pathlib.Path(__file__).parent), "ar_fastapi_static")
|
221
|
-
fastapi_app.mount(
|
222
|
-
"/ar_fastapi_static",
|
223
|
-
StaticFiles(directory=ar_fastapi_static_dirpath),
|
224
|
-
name="ar_fastapi_static"
|
294
|
+
handler=handle_exception
|
225
295
|
)
|
296
|
+
return app
|
226
297
|
|
227
298
|
|
228
|
-
def
|
299
|
+
def add_swagger_to_app(
|
229
300
|
*,
|
230
|
-
|
301
|
+
app: FastAPI,
|
231
302
|
favicon_url: str | None = None
|
232
303
|
):
|
233
|
-
|
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
|
+
)
|
234
309
|
|
235
|
-
@
|
310
|
+
@app.get("/docs", include_in_schema=False)
|
236
311
|
async def custom_swagger_ui_html():
|
237
312
|
return get_swagger_ui_html(
|
238
|
-
openapi_url=
|
239
|
-
title=
|
313
|
+
openapi_url=app.openapi_url,
|
314
|
+
title=app.title,
|
240
315
|
swagger_js_url="/ar_fastapi_static/swagger-ui/swagger-ui-bundle.js",
|
241
316
|
swagger_css_url="/ar_fastapi_static/swagger-ui/swagger-ui.css",
|
242
317
|
swagger_favicon_url=favicon_url
|
243
318
|
)
|
244
319
|
|
245
|
-
@
|
320
|
+
@app.get("/redoc", include_in_schema=False)
|
246
321
|
async def custom_redoc_html():
|
247
322
|
return get_redoc_html(
|
248
|
-
openapi_url=
|
249
|
-
title=
|
323
|
+
openapi_url=app.openapi_url,
|
324
|
+
title=app.title,
|
250
325
|
redoc_js_url="/ar_fastapi_static/redoc/redoc.standalone.js",
|
251
326
|
redoc_favicon_url=favicon_url
|
252
327
|
)
|
253
328
|
|
254
|
-
return
|
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
|
255
373
|
|
256
374
|
|
257
|
-
class
|
375
|
+
class BaseStartupAPIEvent:
|
258
376
|
def __init__(self, *args, **kwargs):
|
259
377
|
self._logger = logging.getLogger(self.__class__.__name__)
|
260
378
|
|
261
|
-
async def
|
379
|
+
async def async_on_startup(self, *args, **kwargs):
|
262
380
|
self._logger.info("on_startup starts")
|
263
381
|
self._logger.info("on_startup ends")
|
264
382
|
|
265
|
-
|
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):
|
266
420
|
self._logger.info("on_shutdown starts")
|
267
421
|
self._logger.info("on_shutdown ends")
|
268
422
|
|
269
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
|
+
|
270
470
|
def create_fastapi_app(
|
271
471
|
*,
|
272
|
-
title: str,
|
273
|
-
description: str | None =
|
274
|
-
|
275
|
-
|
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()
|
276
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
|
+
|
277
489
|
app = FastAPI(
|
278
490
|
title=title,
|
279
491
|
description=description,
|
280
492
|
docs_url=None,
|
281
493
|
redoc_url=None,
|
282
494
|
openapi_url="/openapi",
|
283
|
-
on_startup=[
|
284
|
-
on_shutdown=[
|
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]
|
285
497
|
)
|
286
498
|
|
287
|
-
|
499
|
+
app.state.transmitted_api_data = transmitted_api_data
|
288
500
|
|
289
|
-
|
501
|
+
add_cors_to_app(app=app)
|
290
502
|
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
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_
|
295
509
|
)
|
296
510
|
else:
|
297
|
-
|
298
|
-
|
299
|
-
|
511
|
+
add_exception_handler_to_app(
|
512
|
+
app=app,
|
513
|
+
handle_exception=create_handle_exception()
|
300
514
|
)
|
301
515
|
|
302
|
-
|
516
|
+
add_needed_api_router_to_app(app=app)
|
303
517
|
|
518
|
+
app.include_router(router=api_router)
|
304
519
|
|
305
|
-
|
520
|
+
return app
|
306
521
|
|
307
522
|
|
308
523
|
def __example():
|
arpakitlib/ar_need_type_util.py
CHANGED
@@ -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"
|
arpakitlib/ar_openai_util.py
CHANGED
@@ -4,97 +4,29 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
import logging
|
6
6
|
import traceback
|
7
|
-
from datetime import
|
7
|
+
from datetime import timedelta
|
8
8
|
from typing import Any
|
9
9
|
|
10
|
-
from sqlalchemy import
|
11
|
-
from sqlalchemy.
|
12
|
-
from sqlalchemy.orm import Mapped, mapped_column, Session
|
10
|
+
from sqlalchemy import asc
|
11
|
+
from sqlalchemy.orm import Session
|
13
12
|
|
14
13
|
from arpakitlib.ar_base_worker import BaseWorker
|
15
14
|
from arpakitlib.ar_datetime_util import now_utc_dt
|
16
15
|
from arpakitlib.ar_dict_util import combine_dicts
|
17
|
-
from arpakitlib.
|
18
|
-
from arpakitlib.
|
19
|
-
from arpakitlib.ar_fastapi_util import BaseAPISO, BaseAPISimpleSO
|
20
|
-
from arpakitlib.ar_sqlalchemy_model_util import SimpleDBM
|
16
|
+
from arpakitlib.ar_sqlalchemy_model_util import OperationDBM, StoryLogDBM
|
17
|
+
from arpakitlib.ar_sqlalchemy_util import SQLAlchemyDB
|
21
18
|
|
22
19
|
_ARPAKIT_LIB_MODULE_VERSION = "3.0"
|
23
20
|
|
24
|
-
|
25
|
-
class OperationDBM(SimpleDBM):
|
26
|
-
__tablename__ = "operation"
|
27
|
-
|
28
|
-
class Statuses(EasyEnumeration):
|
29
|
-
waiting_for_execution = "waiting_for_execution"
|
30
|
-
executing = "executing"
|
31
|
-
executed_without_error = "executed_without_error"
|
32
|
-
executed_with_error = "executed_with_error"
|
33
|
-
|
34
|
-
class Types(EasyEnumeration):
|
35
|
-
healthcheck_ = "healthcheck"
|
36
|
-
raise_fake_exception = "raise_fake_exception"
|
37
|
-
|
38
|
-
status: Mapped[str] = mapped_column(
|
39
|
-
TEXT, index=True, insert_default=Statuses.waiting_for_execution,
|
40
|
-
server_default=Statuses.waiting_for_execution, nullable=False
|
41
|
-
)
|
42
|
-
type: Mapped[str] = mapped_column(
|
43
|
-
TEXT, index=True, insert_default=Types.healthcheck_, nullable=False
|
44
|
-
)
|
45
|
-
execution_start_dt: Mapped[datetime | None] = mapped_column(TIMESTAMP(timezone=True), nullable=True)
|
46
|
-
execution_finish_dt: Mapped[datetime | None] = mapped_column(TIMESTAMP(timezone=True), nullable=True)
|
47
|
-
input_data: Mapped[dict[str, Any]] = mapped_column(
|
48
|
-
JSONB,
|
49
|
-
insert_default={},
|
50
|
-
server_default="{}",
|
51
|
-
nullable=False
|
52
|
-
)
|
53
|
-
output_data: Mapped[dict[str, Any]] = mapped_column(JSONB, insert_default={}, server_default="{}", nullable=False)
|
54
|
-
error_data: Mapped[dict[str, Any]] = mapped_column(JSONB, insert_default={}, server_default="{}", nullable=False)
|
55
|
-
|
56
|
-
def raise_if_executed_with_error(self):
|
57
|
-
if self.status == self.Statuses.executed_with_error:
|
58
|
-
raise Exception(
|
59
|
-
f"Operation (id={self.id}, type={self.type}) executed with error, error_data={self.error_data}"
|
60
|
-
)
|
61
|
-
|
62
|
-
def raise_if_error_data(self):
|
63
|
-
if self.status == self.Statuses.executed_with_error:
|
64
|
-
raise Exception(
|
65
|
-
f"Operation (id={self.id}, type={self.type}) has error_data, error_data={self.error_data}"
|
66
|
-
)
|
67
|
-
|
68
|
-
@property
|
69
|
-
def duration(self) -> timedelta | None:
|
70
|
-
if self.execution_start_dt is None or self.execution_finish_dt is None:
|
71
|
-
return None
|
72
|
-
return self.execution_finish_dt - self.execution_start_dt
|
73
|
-
|
74
|
-
@property
|
75
|
-
def duration_total_seconds(self) -> float | None:
|
76
|
-
if self.duration is None:
|
77
|
-
return None
|
78
|
-
return self.duration.total_seconds()
|
79
|
-
|
80
|
-
|
81
|
-
class OperationSO(BaseAPISimpleSO):
|
82
|
-
execution_start_dt: datetime | None
|
83
|
-
execution_finish_dt: datetime | None
|
84
|
-
status: str
|
85
|
-
type: str
|
86
|
-
input_data: dict[str, Any]
|
87
|
-
output_data: dict[str, Any]
|
88
|
-
error_data: dict[str, Any]
|
89
|
-
duration_total_seconds: float | None
|
21
|
+
_logger = logging.getLogger(__name__)
|
90
22
|
|
91
23
|
|
92
24
|
def get_operation_for_execution(
|
93
25
|
*,
|
94
|
-
|
26
|
+
sqlalchemy_db: SQLAlchemyDB,
|
95
27
|
filter_operation_type: str | None = None
|
96
28
|
) -> OperationDBM | None:
|
97
|
-
with
|
29
|
+
with sqlalchemy_db.new_session() as session:
|
98
30
|
query = (
|
99
31
|
session
|
100
32
|
.query(OperationDBM)
|
@@ -111,32 +43,30 @@ def get_operation_by_id(
|
|
111
43
|
*,
|
112
44
|
session: Session,
|
113
45
|
filter_operation_id: int,
|
114
|
-
|
46
|
+
raise_if_not_found: bool = False
|
115
47
|
) -> OperationDBM | None:
|
116
48
|
query = (
|
117
49
|
session
|
118
50
|
.query(OperationDBM)
|
119
51
|
.filter(OperationDBM.id == filter_operation_id)
|
120
52
|
)
|
121
|
-
if
|
53
|
+
if raise_if_not_found:
|
122
54
|
return query.one()
|
123
55
|
else:
|
124
56
|
return query.one_or_none()
|
125
57
|
|
126
58
|
|
127
59
|
class BaseOperationExecutor:
|
128
|
-
def __init__(self, *,
|
60
|
+
def __init__(self, *, sqlalchemy_db: SQLAlchemyDB):
|
129
61
|
self._logger = logging.getLogger(self.__class__.__name__)
|
130
|
-
self.
|
62
|
+
self.sql_alchemy_db = sqlalchemy_db
|
131
63
|
|
132
64
|
async def async_execute_operation(self, operation_dbm: OperationDBM) -> OperationDBM:
|
133
65
|
if operation_dbm.type == OperationDBM.Types.healthcheck_:
|
134
66
|
self._logger.info("healthcheck")
|
135
|
-
elif operation_dbm.type == OperationDBM.Types.
|
67
|
+
elif operation_dbm.type == OperationDBM.Types.raise_fake_exception_:
|
136
68
|
self._logger.info("raise_fake_exception")
|
137
69
|
raise Exception("raise_fake_exception")
|
138
|
-
else:
|
139
|
-
raise ValueError(f"unknown operation.type = {operation_dbm.type}")
|
140
70
|
return operation_dbm
|
141
71
|
|
142
72
|
async def async_safe_execute_operation(self, operation_dbm: OperationDBM) -> OperationDBM:
|
@@ -146,9 +76,9 @@ class BaseOperationExecutor:
|
|
146
76
|
f", operation_dbm.type={operation_dbm.type}"
|
147
77
|
)
|
148
78
|
|
149
|
-
with self.
|
79
|
+
with self.sql_alchemy_db.new_session() as session:
|
150
80
|
operation_dbm: OperationDBM = get_operation_by_id(
|
151
|
-
session=session, filter_operation_id=operation_dbm.id,
|
81
|
+
session=session, filter_operation_id=operation_dbm.id, raise_if_not_found=True
|
152
82
|
)
|
153
83
|
operation_dbm.execution_start_dt = now_utc_dt()
|
154
84
|
operation_dbm.status = OperationDBM.Statuses.executing
|
@@ -165,9 +95,10 @@ class BaseOperationExecutor:
|
|
165
95
|
exception = exception_
|
166
96
|
traceback_str = traceback.format_exc()
|
167
97
|
|
168
|
-
with self.
|
98
|
+
with self.sql_alchemy_db.new_session() as session:
|
99
|
+
|
169
100
|
operation_dbm: OperationDBM = get_operation_by_id(
|
170
|
-
session=session, filter_operation_id=operation_dbm.id,
|
101
|
+
session=session, filter_operation_id=operation_dbm.id, raise_if_not_found=True
|
171
102
|
)
|
172
103
|
operation_dbm.execution_finish_dt = now_utc_dt()
|
173
104
|
if exception:
|
@@ -179,7 +110,21 @@ class BaseOperationExecutor:
|
|
179
110
|
else:
|
180
111
|
operation_dbm.status = OperationDBM.Statuses.executed_without_error
|
181
112
|
session.commit()
|
113
|
+
|
114
|
+
story_log_dbm = StoryLogDBM(
|
115
|
+
level=StoryLogDBM.Levels.error,
|
116
|
+
title="Error in async_execute_operation",
|
117
|
+
data={
|
118
|
+
"operation_id": operation_dbm.id,
|
119
|
+
"exception_str": str(exception),
|
120
|
+
"traceback_str": traceback_str
|
121
|
+
}
|
122
|
+
)
|
123
|
+
session.add(story_log_dbm)
|
124
|
+
session.commit()
|
125
|
+
|
182
126
|
session.refresh(operation_dbm)
|
127
|
+
session.refresh(story_log_dbm)
|
183
128
|
|
184
129
|
self._logger.info(
|
185
130
|
f"finish async_safe_execute_operation"
|
@@ -192,11 +137,9 @@ class BaseOperationExecutor:
|
|
192
137
|
def sync_execute_operation(self, operation_dbm: OperationDBM) -> OperationDBM:
|
193
138
|
if operation_dbm.type == OperationDBM.Types.healthcheck_:
|
194
139
|
self._logger.info("healthcheck")
|
195
|
-
elif operation_dbm.type == OperationDBM.Types.
|
140
|
+
elif operation_dbm.type == OperationDBM.Types.raise_fake_exception_:
|
196
141
|
self._logger.info("raise_fake_exception")
|
197
142
|
raise Exception("raise_fake_exception")
|
198
|
-
else:
|
199
|
-
raise ValueError(f"unknown operation.type = {operation_dbm.type}")
|
200
143
|
return operation_dbm
|
201
144
|
|
202
145
|
def sync_safe_execute_operation(self, operation_dbm: OperationDBM) -> OperationDBM:
|
@@ -206,9 +149,9 @@ class BaseOperationExecutor:
|
|
206
149
|
f", operation_dbm.type={operation_dbm.type}"
|
207
150
|
)
|
208
151
|
|
209
|
-
with self.
|
152
|
+
with self.sql_alchemy_db.new_session() as session:
|
210
153
|
operation_dbm: OperationDBM = get_operation_by_id(
|
211
|
-
session=session, filter_operation_id=operation_dbm.id,
|
154
|
+
session=session, filter_operation_id=operation_dbm.id, raise_if_not_found=True
|
212
155
|
)
|
213
156
|
operation_dbm.execution_start_dt = now_utc_dt()
|
214
157
|
operation_dbm.status = OperationDBM.Statuses.executing
|
@@ -225,9 +168,10 @@ class BaseOperationExecutor:
|
|
225
168
|
exception = exception_
|
226
169
|
traceback_str = traceback.format_exc()
|
227
170
|
|
228
|
-
with self.
|
171
|
+
with self.sql_alchemy_db.new_session() as session:
|
172
|
+
|
229
173
|
operation_dbm: OperationDBM = get_operation_by_id(
|
230
|
-
session=session, filter_operation_id=operation_dbm.id,
|
174
|
+
session=session, filter_operation_id=operation_dbm.id, raise_if_not_found=True
|
231
175
|
)
|
232
176
|
operation_dbm.execution_finish_dt = now_utc_dt()
|
233
177
|
if exception:
|
@@ -239,7 +183,21 @@ class BaseOperationExecutor:
|
|
239
183
|
else:
|
240
184
|
operation_dbm.status = OperationDBM.Statuses.executed_without_error
|
241
185
|
session.commit()
|
186
|
+
|
187
|
+
story_log_dbm = StoryLogDBM(
|
188
|
+
level=StoryLogDBM.Levels.error,
|
189
|
+
title="Error in sync_execute_operation",
|
190
|
+
data={
|
191
|
+
"operation_id": operation_dbm.id,
|
192
|
+
"exception_str": str(exception),
|
193
|
+
"traceback_str": traceback_str
|
194
|
+
}
|
195
|
+
)
|
196
|
+
session.add(story_log_dbm)
|
197
|
+
session.commit()
|
198
|
+
|
242
199
|
session.refresh(operation_dbm)
|
200
|
+
session.refresh(story_log_dbm)
|
243
201
|
|
244
202
|
self._logger.info(
|
245
203
|
f"finish sync_safe_execute_operation"
|
@@ -256,27 +214,29 @@ class ExecuteOperationWorker(BaseWorker):
|
|
256
214
|
def __init__(
|
257
215
|
self,
|
258
216
|
*,
|
259
|
-
|
260
|
-
operation_executor: BaseOperationExecutor,
|
261
|
-
|
217
|
+
sqlalchemy_db: SQLAlchemyDB,
|
218
|
+
operation_executor: BaseOperationExecutor | None = None,
|
219
|
+
filter_operation_type: str | None = None
|
262
220
|
):
|
263
221
|
super().__init__()
|
264
|
-
self.
|
222
|
+
self.sqlalchemy_db = sqlalchemy_db
|
265
223
|
self.timeout_after_run = timedelta(seconds=0.1).total_seconds()
|
266
224
|
self.timeout_after_err_in_run = timedelta(seconds=1).total_seconds()
|
225
|
+
if operation_executor is None:
|
226
|
+
operation_executor = BaseOperationExecutor(sqlalchemy_db=sqlalchemy_db)
|
267
227
|
self.operation_executor = operation_executor
|
268
|
-
self.
|
228
|
+
self.filter_operation_type = filter_operation_type
|
269
229
|
|
270
230
|
async def async_on_startup(self):
|
271
|
-
self.
|
231
|
+
self.sqlalchemy_db.init()
|
272
232
|
|
273
233
|
async def async_execute_operation(self, operation_dbm: OperationDBM) -> OperationDBM:
|
274
234
|
return await self.operation_executor.async_safe_execute_operation(operation_dbm=operation_dbm)
|
275
235
|
|
276
236
|
async def async_run(self):
|
277
237
|
operation_dbm: OperationDBM | None = get_operation_for_execution(
|
278
|
-
|
279
|
-
filter_operation_type=self.
|
238
|
+
sqlalchemy_db=self.sqlalchemy_db,
|
239
|
+
filter_operation_type=self.filter_operation_type
|
280
240
|
)
|
281
241
|
|
282
242
|
if not operation_dbm:
|
@@ -288,15 +248,15 @@ class ExecuteOperationWorker(BaseWorker):
|
|
288
248
|
self._logger.exception(exception)
|
289
249
|
|
290
250
|
def sync_on_startup(self):
|
291
|
-
self.
|
251
|
+
self.sqlalchemy_db.init()
|
292
252
|
|
293
253
|
def sync_execute_operation(self, operation_dbm: OperationDBM) -> OperationDBM:
|
294
254
|
return self.operation_executor.sync_safe_execute_operation(operation_dbm=operation_dbm)
|
295
255
|
|
296
256
|
def sync_run(self):
|
297
257
|
operation_dbm: OperationDBM | None = get_operation_for_execution(
|
298
|
-
|
299
|
-
filter_operation_type=self.
|
258
|
+
sqlalchemy_db=self.sqlalchemy_db,
|
259
|
+
filter_operation_type=self.filter_operation_type
|
300
260
|
)
|
301
261
|
|
302
262
|
if not operation_dbm:
|
@@ -1,13 +1,15 @@
|
|
1
1
|
# arpakit
|
2
2
|
|
3
|
-
from datetime import datetime
|
3
|
+
from datetime import datetime, timedelta
|
4
4
|
from typing import Any
|
5
5
|
from uuid import uuid4
|
6
6
|
|
7
7
|
from sqlalchemy import inspect, INTEGER, TEXT, TIMESTAMP
|
8
|
+
from sqlalchemy.dialects.postgresql import JSONB
|
8
9
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
9
10
|
|
10
11
|
from arpakitlib.ar_datetime_util import now_utc_dt
|
12
|
+
from arpakitlib.ar_enumeration_util import Enumeration
|
11
13
|
from arpakitlib.ar_json_util import safely_transfer_to_json_str
|
12
14
|
|
13
15
|
_ARPAKIT_LIB_MODULE_VERSION = "3.0"
|
@@ -45,3 +47,76 @@ class SimpleDBM(BaseDBM):
|
|
45
47
|
|
46
48
|
def __repr__(self):
|
47
49
|
return f"{self.__class__.__name__.removesuffix('DBM')} (id={self.id})"
|
50
|
+
|
51
|
+
|
52
|
+
class StoryLogDBM(SimpleDBM):
|
53
|
+
__tablename__ = "story_log"
|
54
|
+
|
55
|
+
class Levels(Enumeration):
|
56
|
+
info = "info"
|
57
|
+
warning = "warning"
|
58
|
+
error = "error"
|
59
|
+
|
60
|
+
level: Mapped[str] = mapped_column(
|
61
|
+
TEXT, insert_default=Levels.info, server_default=Levels.info, index=True, nullable=False
|
62
|
+
)
|
63
|
+
title: Mapped[str | None] = mapped_column(TEXT, index=True, default=None, nullable=True)
|
64
|
+
data: Mapped[dict[str, Any]] = mapped_column(
|
65
|
+
JSONB, insert_default={}, server_default="{}", index=True, nullable=False
|
66
|
+
)
|
67
|
+
|
68
|
+
|
69
|
+
class OperationDBM(SimpleDBM):
|
70
|
+
__tablename__ = "operation"
|
71
|
+
|
72
|
+
class Statuses(Enumeration):
|
73
|
+
waiting_for_execution = "waiting_for_execution"
|
74
|
+
executing = "executing"
|
75
|
+
executed_without_error = "executed_without_error"
|
76
|
+
executed_with_error = "executed_with_error"
|
77
|
+
|
78
|
+
class Types(Enumeration):
|
79
|
+
healthcheck_ = "healthcheck"
|
80
|
+
raise_fake_exception_ = "raise_fake_exception"
|
81
|
+
|
82
|
+
status: Mapped[str] = mapped_column(
|
83
|
+
TEXT, index=True, insert_default=Statuses.waiting_for_execution,
|
84
|
+
server_default=Statuses.waiting_for_execution, nullable=False
|
85
|
+
)
|
86
|
+
type: Mapped[str] = mapped_column(
|
87
|
+
TEXT, index=True, insert_default=Types.healthcheck_, nullable=False
|
88
|
+
)
|
89
|
+
execution_start_dt: Mapped[datetime | None] = mapped_column(TIMESTAMP(timezone=True), nullable=True)
|
90
|
+
execution_finish_dt: Mapped[datetime | None] = mapped_column(TIMESTAMP(timezone=True), nullable=True)
|
91
|
+
input_data: Mapped[dict[str, Any]] = mapped_column(
|
92
|
+
JSONB,
|
93
|
+
insert_default={},
|
94
|
+
server_default="{}",
|
95
|
+
nullable=False
|
96
|
+
)
|
97
|
+
output_data: Mapped[dict[str, Any]] = mapped_column(JSONB, insert_default={}, server_default="{}", nullable=False)
|
98
|
+
error_data: Mapped[dict[str, Any]] = mapped_column(JSONB, insert_default={}, server_default="{}", nullable=False)
|
99
|
+
|
100
|
+
def raise_if_executed_with_error(self):
|
101
|
+
if self.status == self.Statuses.executed_with_error:
|
102
|
+
raise Exception(
|
103
|
+
f"Operation (id={self.id}, type={self.type}) executed with error, error_data={self.error_data}"
|
104
|
+
)
|
105
|
+
|
106
|
+
def raise_if_error_data(self):
|
107
|
+
if self.error_data:
|
108
|
+
raise Exception(
|
109
|
+
f"Operation (id={self.id}, type={self.type}) has error_data, error_data={self.error_data}"
|
110
|
+
)
|
111
|
+
|
112
|
+
@property
|
113
|
+
def duration(self) -> timedelta | None:
|
114
|
+
if self.execution_start_dt is None or self.execution_finish_dt is None:
|
115
|
+
return None
|
116
|
+
return self.execution_finish_dt - self.execution_start_dt
|
117
|
+
|
118
|
+
@property
|
119
|
+
def duration_total_seconds(self) -> float | None:
|
120
|
+
if self.duration is None:
|
121
|
+
return None
|
122
|
+
return self.duration.total_seconds()
|
@@ -12,9 +12,20 @@ from sqlalchemy.orm.session import Session
|
|
12
12
|
_ARPAKIT_LIB_MODULE_VERSION = "3.0"
|
13
13
|
|
14
14
|
|
15
|
-
class
|
16
|
-
def __init__(
|
15
|
+
class SQLAlchemyDB:
|
16
|
+
def __init__(
|
17
|
+
self,
|
18
|
+
*,
|
19
|
+
db_url: str = "postgresql://arpakitlib:arpakitlib@localhost:50629/arpakitlib",
|
20
|
+
echo: bool = False,
|
21
|
+
need_include_operation_dbm: bool = False,
|
22
|
+
need_include_story_dbm: bool = False
|
23
|
+
):
|
17
24
|
self._logger = logging.getLogger(self.__class__.__name__)
|
25
|
+
self.need_include_operation_dbm = need_include_operation_dbm
|
26
|
+
self.need_include_story_dbm = need_include_story_dbm
|
27
|
+
if self.need_include_operation_dbm:
|
28
|
+
self.need_include_story_dbm = True
|
18
29
|
self.engine = create_engine(
|
19
30
|
url=db_url,
|
20
31
|
echo=echo,
|
arpakitlib/ar_type_util.py
CHANGED
@@ -1,13 +1,10 @@
|
|
1
1
|
# arpakit
|
2
|
-
|
2
|
+
import inspect
|
3
3
|
from typing import Optional, Any
|
4
4
|
|
5
5
|
_ARPAKIT_LIB_MODULE_VERSION = "3.0"
|
6
6
|
|
7
7
|
|
8
|
-
# ---
|
9
|
-
|
10
|
-
|
11
8
|
class NotSet:
|
12
9
|
pass
|
13
10
|
|
@@ -44,9 +41,6 @@ def make_none_if_not_set(v: Any) -> Any:
|
|
44
41
|
return v
|
45
42
|
|
46
43
|
|
47
|
-
# ---
|
48
|
-
|
49
|
-
|
50
44
|
def raise_for_type(comparable, need_type, comment_for_error: Optional[str] = None):
|
51
45
|
if comparable is need_type:
|
52
46
|
return
|
@@ -73,7 +67,9 @@ def raise_for_types(comparable, need_types, comment_for_error: Optional[str] = N
|
|
73
67
|
raise TypeError(err)
|
74
68
|
|
75
69
|
|
76
|
-
|
70
|
+
def raise_if_not_async_func(func: Any):
|
71
|
+
if not inspect.iscoroutinefunction(func):
|
72
|
+
raise TypeError(f"The provided function '{func.__name__}' is not an async function")
|
77
73
|
|
78
74
|
|
79
75
|
def __example():
|
@@ -12,7 +12,7 @@ import aiohttp
|
|
12
12
|
import requests
|
13
13
|
|
14
14
|
from arpakitlib.ar_dict_util import combine_dicts
|
15
|
-
from arpakitlib.
|
15
|
+
from arpakitlib.ar_enumeration_util import Enumeration
|
16
16
|
from arpakitlib.ar_sleep_util import sync_safe_sleep, async_safe_sleep
|
17
17
|
from arpakitlib.ar_type_util import raise_for_type
|
18
18
|
|
@@ -23,7 +23,7 @@ https://yookassa.ru/developers/api
|
|
23
23
|
"""
|
24
24
|
|
25
25
|
|
26
|
-
class YookassaPaymentStatuses(
|
26
|
+
class YookassaPaymentStatuses(Enumeration):
|
27
27
|
pending = "pending"
|
28
28
|
waiting_for_capture = "waiting_for_capture"
|
29
29
|
succeeded = "succeeded"
|
@@ -6,7 +6,7 @@ arpakitlib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
arpakitlib/ar_additional_model_util.py,sha256=Eq7pvVUgO2L3gYBocm-pP9TrztTb8VNCp7LdRMml-F8,237
|
7
7
|
arpakitlib/ar_aiogram_util.py,sha256=IA48PRMIJrPLMhFA0Eb2vQpLcqm98o9tKfC3pDy8qsI,12022
|
8
8
|
arpakitlib/ar_arpakit_lib_module_util.py,sha256=YzobxRG8-QJ1L5r_8wBdL668CwXoQRIM1Cpec1o2WBc,5447
|
9
|
-
arpakitlib/ar_arpakit_schedule_uust_api_client.py,sha256=
|
9
|
+
arpakitlib/ar_arpakit_schedule_uust_api_client.py,sha256=SYWWQDohPnw0qpBIu2hEvGZRVdaI4NUUQdEjnMnseo4,18237
|
10
10
|
arpakitlib/ar_arpakitlib_info.py,sha256=cvgrLnEznmYkCAg1adbY46ATjD6GJd-Yk8PTgOPjpKM,248
|
11
11
|
arpakitlib/ar_base64_util.py,sha256=aZkg2cZTuAaP2IWeG_LXJ6RO7qhyskVwec-Lks0iM-k,676
|
12
12
|
arpakitlib/ar_base_worker.py,sha256=Y6yRFp1nhhTyv-TeGDao-3q4ICVVJ2zVsKdIlHdiGSI,2897
|
@@ -14,9 +14,8 @@ arpakitlib/ar_cache_file.py,sha256=m73_vU6bMjXsIurSPO9VCLcHsiHk8ITFS0LNjfI_8Uw,3
|
|
14
14
|
arpakitlib/ar_datetime_util.py,sha256=Xe1NiT9oPQzNSG7RVRkhukhbg4i-hhS5ImmV7sPUc8o,971
|
15
15
|
arpakitlib/ar_dict_util.py,sha256=cF5LQJ6tLqyGoEXfDljMDZrikeZoWPw7CgINHIFGvXM,419
|
16
16
|
arpakitlib/ar_dream_ai_api_client.py,sha256=hDPL9wbG4MjIuhn2ed6qepueogANIkt-NddhhiPUv0Y,4029
|
17
|
-
arpakitlib/ar_easy_sqlalchemy_util.py,sha256=HuKRBD4XoxeZ5tpXlDTol5Y6AOzuCBluJQHfyRjlRqs,3224
|
18
17
|
arpakitlib/ar_encrypt_and_decrypt_util.py,sha256=GhWnp7HHkbhwFVVCzO1H07m-5gryr4yjWsXjOaNQm1Y,520
|
19
|
-
arpakitlib/
|
18
|
+
arpakitlib/ar_enumeration_util.py,sha256=0DN46uyI0Gu9JPDgso3XPbnre7hZZefYTZwmmE1iYH4,2250
|
20
19
|
arpakitlib/ar_fastapi_static/redoc/redoc.standalone.js,sha256=WCuodUNv1qVh0oW5fjnJDwb5AwOue73jKHdI9z8iGKU,909365
|
21
20
|
arpakitlib/ar_fastapi_static/swagger-ui/favicon-16x16.png,sha256=ryStYE3Xs7zaj5dauXMHX0ovcKQIeUShL474tjo-B8I,665
|
22
21
|
arpakitlib/ar_fastapi_static/swagger-ui/favicon-32x32.png,sha256=PtYS9B4FDKXnAAytbxy-fn2jn2X8qZwC6Z5lkQVuWDc,628
|
@@ -36,7 +35,7 @@ arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui.css,sha256=jzPZlgJTFwSdSphk9C
|
|
36
35
|
arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui.css.map,sha256=5wq8eXMLU6Zxb45orZPL1zAsBFJReFw6GjYqGpUX3hg,262650
|
37
36
|
arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui.js,sha256=ffrLZHHEQ_g84A-ul3yWa10Kk09waOAxHcQXPuZuavg,339292
|
38
37
|
arpakitlib/ar_fastapi_static/swagger-ui/swagger-ui.js.map,sha256=9UhIW7MqCOZPAz1Sl1IKfZUuhWU0p-LJqrnjjJD9Xhc,1159454
|
39
|
-
arpakitlib/ar_fastapi_util.py,sha256=
|
38
|
+
arpakitlib/ar_fastapi_util.py,sha256=7YUx9WL4hc_TAiNjFDqxdcpAutvv0UtI5IEICbBZdVQ,16847
|
40
39
|
arpakitlib/ar_file_storage_in_dir.py,sha256=D3e3rGuHoI6xqAA5mVvEpVVpOWY1jyjNsjj2UhyHRbE,3674
|
41
40
|
arpakitlib/ar_generate_env_example.py,sha256=WseNlk_So6mTVQ2amMuigWYV4ZVmd940POvXtodoYj0,325
|
42
41
|
arpakitlib/ar_hash_util.py,sha256=Iqy6KBAOLBQMFLWv676boI5sV7atT2B-fb7aCdHOmIQ,340
|
@@ -49,23 +48,23 @@ arpakitlib/ar_list_of_dicts_to_xlsx.py,sha256=MyjEl4Jl4beLVZqLVQMMv0-XDtBD3Xh4Z_
|
|
49
48
|
arpakitlib/ar_list_util.py,sha256=2woOAHAU8oTIiVjZ8GLnx15odEaoQUq3Q0JPxlufFF0,457
|
50
49
|
arpakitlib/ar_logging_util.py,sha256=c5wX2FLqCzb4aLckLVhIJ7go52rJQ4GN9dIkJ6KMc3o,1500
|
51
50
|
arpakitlib/ar_mongodb_util.py,sha256=2ECkTnGAZ92qxioL-fmN6R4yZOSr3bXdXLWTzT1C3vk,4038
|
52
|
-
arpakitlib/ar_need_type_util.py,sha256=
|
53
|
-
arpakitlib/ar_openai_util.py,sha256=
|
54
|
-
arpakitlib/ar_operation_execution_util.py,sha256=
|
51
|
+
arpakitlib/ar_need_type_util.py,sha256=n2kBETxzOSVhSVoy7qUtHtuQzgrrxzgi1_iVQimPb9o,1615
|
52
|
+
arpakitlib/ar_openai_util.py,sha256=dHUbfg1sVVCjsNl_fra3iCMEz1bR-Hk9fE-DdYbu7Wc,1215
|
53
|
+
arpakitlib/ar_operation_execution_util.py,sha256=Vxuu6MoYsmR2GS5UlW_fIDwNf62Gsw0UtjeSnh4Evi8,9928
|
55
54
|
arpakitlib/ar_parse_command.py,sha256=qpr2OwG3Bf7DFiL9S3iWgtbvtE80RSC35E5zFJvjG1I,2714
|
56
55
|
arpakitlib/ar_postgresql_util.py,sha256=SAHEmAyMkZe516uk2gS830v_Wn2kRUZUYNcTNwmgXJk,1160
|
57
56
|
arpakitlib/ar_run_cmd.py,sha256=D_rPavKMmWkQtwvZFz-Io5Ak8eSODHkcFeLPzNVC68g,1072
|
58
57
|
arpakitlib/ar_schedule_uust_api_client.py,sha256=1JGUy6rrjAXdWjeAqiAOQlCAEV3xuc5FUDWfXODKB-A,5770
|
59
58
|
arpakitlib/ar_sleep_util.py,sha256=9ZN4Qo4eZ_q3hjM7vNBQjFRcH-9-sqv3QLSjnxVJE90,1405
|
60
|
-
arpakitlib/ar_sqlalchemy_model_util.py,sha256=
|
61
|
-
arpakitlib/
|
62
|
-
arpakitlib/
|
59
|
+
arpakitlib/ar_sqlalchemy_model_util.py,sha256=tKz6n9zuebo2J9yTr6IQUHoXZ9KF340MAo4UiDXaX_4,4251
|
60
|
+
arpakitlib/ar_sqlalchemy_util.py,sha256=VH23Xld3k5wITkwR0JcaBkO77gmxWn9jlK4eyxKuz_0,3665
|
61
|
+
arpakitlib/ar_ssh_util.py,sha256=jlnss4V4pziBN1rBzoK_lDiWm6nMOqGXfa6NFJSKH-Y,6796
|
63
62
|
arpakitlib/ar_str_util.py,sha256=xSEzmsDvRiZVaxyqFFjcgzpphktCbXg2FHcvsd1DYpA,1885
|
64
|
-
arpakitlib/ar_type_util.py,sha256
|
65
|
-
arpakitlib/ar_yookassa_api_client.py,sha256=
|
63
|
+
arpakitlib/ar_type_util.py,sha256=I6jbTz7_dxR1lkhz1JfUb5ZyLLdXVhG_-hzjdgT6N6s,1932
|
64
|
+
arpakitlib/ar_yookassa_api_client.py,sha256=WZoTd10d2lOmT0lCdlQFTB0LEVARubqLEiFtPNQLvi8,6477
|
66
65
|
arpakitlib/ar_zabbix_util.py,sha256=MTQbmS0QpNCKNOGONNQHf6j7KTZsKGlIbd5rCH0R0WI,6313
|
67
|
-
arpakitlib-1.5.
|
68
|
-
arpakitlib-1.5.
|
69
|
-
arpakitlib-1.5.
|
70
|
-
arpakitlib-1.5.
|
71
|
-
arpakitlib-1.5.
|
66
|
+
arpakitlib-1.5.36.dist-info/LICENSE,sha256=1jqWIkbnMxDfs_i0SXP5qbV6PHjBr1g8506oW7uPjfg,11347
|
67
|
+
arpakitlib-1.5.36.dist-info/METADATA,sha256=wlm2GqyKT6i42_5eIqU_n4UJswvb7F8OoMgcHxX-w5E,2330
|
68
|
+
arpakitlib-1.5.36.dist-info/NOTICE,sha256=wHwmiq3wExfFfgMsE5U5TOBP9_l72ocIG82KurEels0,43
|
69
|
+
arpakitlib-1.5.36.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
70
|
+
arpakitlib-1.5.36.dist-info/RECORD,,
|
arpakitlib/ar_story_log_util.py
DELETED
@@ -1,37 +0,0 @@
|
|
1
|
-
from datetime import datetime
|
2
|
-
from typing import Any
|
3
|
-
|
4
|
-
from sqlalchemy import TEXT
|
5
|
-
from sqlalchemy.dialects.postgresql import JSONB
|
6
|
-
from sqlalchemy.orm import Mapped, mapped_column
|
7
|
-
|
8
|
-
from arpakitlib.ar_enumeration import EasyEnumeration
|
9
|
-
from arpakitlib.ar_fastapi_util import BaseAPISO, BaseAPISimpleSO
|
10
|
-
from arpakitlib.ar_sqlalchemy_model_util import BaseDBM, SimpleDBM
|
11
|
-
|
12
|
-
_ARPAKIT_LIB_MODULE_VERSION = "3.0"
|
13
|
-
|
14
|
-
|
15
|
-
class StoryLogDBM(SimpleDBM):
|
16
|
-
__tablename__ = "story_log"
|
17
|
-
|
18
|
-
class Levels(EasyEnumeration):
|
19
|
-
info = "info"
|
20
|
-
warning = "warning"
|
21
|
-
error = "error"
|
22
|
-
|
23
|
-
level: Mapped[str] = mapped_column(
|
24
|
-
TEXT, insert_default=Levels.info, server_default=Levels.info, index=True, nullable=False
|
25
|
-
)
|
26
|
-
title: Mapped[str | None] = mapped_column(TEXT, index=True, default=None, nullable=True)
|
27
|
-
data: Mapped[dict[str, Any]] = mapped_column(
|
28
|
-
JSONB, insert_default={}, server_default="{}", index=True, nullable=False
|
29
|
-
)
|
30
|
-
|
31
|
-
|
32
|
-
class AdminStoryLogSO(BaseAPISimpleSO):
|
33
|
-
level: str
|
34
|
-
title: str | None
|
35
|
-
data: dict[str, Any]
|
36
|
-
|
37
|
-
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|