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.
@@ -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.ar_enumeration import EasyEnumeration
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(EasyEnumeration):
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(EasyEnumeration):
37
+ class Months(Enumeration):
38
38
  january = 1
39
39
  february = 2
40
40
  march = 3
@@ -8,7 +8,7 @@ ValueType = Union[int, str]
8
8
  ValuesForParseType = Union[ValueType, Iterable[ValueType]]
9
9
 
10
10
 
11
- class EasyEnumeration:
11
+ class Enumeration:
12
12
  @classmethod
13
13
  def iter_values(cls) -> Iterator[ValueType]:
14
14
  big_dict = {}
@@ -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.ar_enumeration import EasyEnumeration
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 BaseAPISI(BaseAPISchema):
55
+ class BaseSI(BaseSchema):
49
56
  pass
50
57
 
51
58
 
52
- class BaseAPISO(BaseAPISchema):
59
+ class BaseSO(BaseSchema):
53
60
  pass
54
61
 
55
62
 
56
- class APISimpleDataSO(BaseAPISO):
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 APIErrorSO(BaseAPISO):
67
- class APIErrorCodes(EasyEnumeration):
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: BaseAPISO, status_code: int = starlette.status.HTTP_200_OK):
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(mode="json"),
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 = APIErrorSO.APIErrorCodes.unknown_error,
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.api_error_so = APIErrorSO(
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.api_error_so.model_dump(mode="json")
142
+ detail=self.error_so.model_dump(mode="json")
120
143
  )
121
144
 
122
145
 
123
- def simple_api_handle_exception(request: starlette.requests.Request, exception: Exception) -> APIJSONResponse:
124
- return from_exception_to_api_json_response(request=request, exception=exception)
125
-
126
-
127
- def from_exception_to_api_json_response(
128
- request: starlette.requests.Request, exception: Exception
129
- ) -> APIJSONResponse:
130
- _logger.exception(exception)
131
-
132
- easy_api_error_so = APIErrorSO(
133
- has_error=True,
134
- error_code=APIErrorSO.APIErrorCodes.unknown_error
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
- status_code = starlette.status.HTTP_500_INTERNAL_SERVER_ERROR
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
- if isinstance(exception, APIException):
140
- easy_api_error_so = exception.api_error_so
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
- easy_api_error_so.error_code = APIErrorSO.APIErrorCodes.unknown_error
150
- if (
151
- isinstance(exception.detail, dict)
152
- or isinstance(exception.detail, list)
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
- elif isinstance(exception, fastapi.exceptions.RequestValidationError):
161
- status_code = starlette.status.HTTP_422_UNPROCESSABLE_ENTITY
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
- else:
166
- status_code = starlette.status.HTTP_500_INTERNAL_SERVER_ERROR
167
- easy_api_error_so.error_code = APIErrorSO.APIErrorCodes.unknown_error
168
- easy_api_error_so.error_data["raw"] = str(exception)
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
- if easy_api_error_so.error_code_specification:
175
- easy_api_error_so.error_code_specification = (
176
- easy_api_error_so.error_code_specification.upper().replace(" ", "_").strip()
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 APIJSONResponse(
180
- content=easy_api_error_so,
181
- status_code=status_code
182
- )
183
-
243
+ return handle_exception
184
244
 
185
- # ---
186
245
 
187
-
188
- def add_exception_handler_to_fastapi_app(*, fastapi_app: FastAPI, api_handle_exception_: Callable) -> FastAPI:
189
- fastapi_app.add_exception_handler(
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=api_handle_exception_
282
+ handler=handle_exception
192
283
  )
193
- fastapi_app.add_exception_handler(
284
+ app.add_exception_handler(
194
285
  exc_class_or_status_code=ValueError,
195
- handler=api_handle_exception_
286
+ handler=handle_exception
196
287
  )
197
- fastapi_app.add_exception_handler(
288
+ app.add_exception_handler(
198
289
  exc_class_or_status_code=fastapi.exceptions.RequestValidationError,
199
- handler=api_handle_exception_
290
+ handler=handle_exception
200
291
  )
201
- fastapi_app.add_exception_handler(
292
+ app.add_exception_handler(
202
293
  exc_class_or_status_code=starlette.exceptions.HTTPException,
203
- handler=api_handle_exception_
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 add_ar_fastapi_static_docs_and_redoc_handlers_to_fastapi_app(
299
+ def add_swagger_to_app(
229
300
  *,
230
- fastapi_app: FastAPI,
301
+ app: FastAPI,
231
302
  favicon_url: str | None = None
232
303
  ):
233
- add_ar_fastapi_static_to_fastapi_app(fastapi_app=fastapi_app)
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
- @fastapi_app.get("/docs", include_in_schema=False)
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=fastapi_app.openapi_url,
239
- title=fastapi_app.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
- @fastapi_app.get("/redoc", include_in_schema=False)
320
+ @app.get("/redoc", include_in_schema=False)
246
321
  async def custom_redoc_html():
247
322
  return get_redoc_html(
248
- openapi_url=fastapi_app.openapi_url,
249
- title=fastapi_app.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 fastapi_app
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 BaseAPIEvent:
375
+ class BaseStartupAPIEvent:
258
376
  def __init__(self, *args, **kwargs):
259
377
  self._logger = logging.getLogger(self.__class__.__name__)
260
378
 
261
- async def on_startup(self, *args, **kwargs):
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
- async def on_shutdown(self, *args, **kwargs):
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 = None,
274
- api_event: BaseAPIEvent | None = BaseAPIEvent(),
275
- api_handle_exception_: Callable | None = simple_api_handle_exception
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=[api_event.on_startup] if api_event else [],
284
- on_shutdown=[api_event.on_shutdown] if api_event else []
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
- add_middleware_cors_to_fastapi_app(fastapi_app=app)
499
+ app.state.transmitted_api_data = transmitted_api_data
288
500
 
289
- add_ar_fastapi_static_docs_and_redoc_handlers_to_fastapi_app(fastapi_app=app)
501
+ add_cors_to_app(app=app)
290
502
 
291
- if api_handle_exception_:
292
- add_exception_handler_to_fastapi_app(
293
- fastapi_app=app,
294
- api_handle_exception_=api_handle_exception_
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
- add_exception_handler_to_fastapi_app(
298
- fastapi_app=app,
299
- api_handle_exception_=simple_api_handle_exception
511
+ add_exception_handler_to_app(
512
+ app=app,
513
+ handle_exception=create_handle_exception()
300
514
  )
301
515
 
302
- return app
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():
@@ -3,12 +3,12 @@
3
3
  import json
4
4
  from typing import Any
5
5
 
6
- from arpakitlib.ar_enumeration import EasyEnumeration
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(EasyEnumeration):
11
+ class NeedTypes(Enumeration):
12
12
  str_ = "str"
13
13
  int_ = "int"
14
14
  bool_ = "bool"
@@ -13,7 +13,7 @@ https://platform.openai.com/docs/
13
13
  """
14
14
 
15
15
 
16
- class EasyOpenAI:
16
+ class OpenAIAPIClient:
17
17
  def __init__(
18
18
  self,
19
19
  *,
@@ -4,97 +4,29 @@ from __future__ import annotations
4
4
 
5
5
  import logging
6
6
  import traceback
7
- from datetime import datetime, timedelta
7
+ from datetime import timedelta
8
8
  from typing import Any
9
9
 
10
- from sqlalchemy import TIMESTAMP, TEXT, asc
11
- from sqlalchemy.dialects.postgresql import JSONB
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.ar_easy_sqlalchemy_util import EasySQLAlchemyDB
18
- from arpakitlib.ar_enumeration import EasyEnumeration
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
- easy_sql_alchemy_db: EasySQLAlchemyDB,
26
+ sqlalchemy_db: SQLAlchemyDB,
95
27
  filter_operation_type: str | None = None
96
28
  ) -> OperationDBM | None:
97
- with easy_sql_alchemy_db.new_session() as session:
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
- strict: bool = False
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 strict:
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, *, easy_sql_alchemy_db: EasySQLAlchemyDB):
60
+ def __init__(self, *, sqlalchemy_db: SQLAlchemyDB):
129
61
  self._logger = logging.getLogger(self.__class__.__name__)
130
- self.easy_sql_alchemy_db = easy_sql_alchemy_db
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.raise_fake_exception:
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.easy_sql_alchemy_db.new_session() as session:
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, strict=True
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.easy_sql_alchemy_db.new_session() as session:
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, strict=True
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.raise_fake_exception:
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.easy_sql_alchemy_db.new_session() as session:
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, strict=True
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.easy_sql_alchemy_db.new_session() as session:
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, strict=True
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
- easy_sql_alchemy_db: EasySQLAlchemyDB,
260
- operation_executor: BaseOperationExecutor,
261
- need_operation_type: str | None = None
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.easy_sql_alchemy_db = easy_sql_alchemy_db
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.need_operation_type = need_operation_type
228
+ self.filter_operation_type = filter_operation_type
269
229
 
270
230
  async def async_on_startup(self):
271
- self.easy_sql_alchemy_db.init()
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
- easy_sql_alchemy_db=self.easy_sql_alchemy_db,
279
- filter_operation_type=self.need_operation_type
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.easy_sql_alchemy_db.init()
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
- easy_sql_alchemy_db=self.easy_sql_alchemy_db,
299
- filter_operation_type=self.need_operation_type
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 EasySQLAlchemyDB:
16
- def __init__(self, *, db_url: str, echo: bool = False):
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,
@@ -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.ar_enumeration import EasyEnumeration
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(EasyEnumeration):
26
+ class YookassaPaymentStatuses(Enumeration):
27
27
  pending = "pending"
28
28
  waiting_for_capture = "waiting_for_capture"
29
29
  succeeded = "succeeded"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: arpakitlib
3
- Version: 1.5.33
3
+ Version: 1.5.36
4
4
  Summary: arpakitlib
5
5
  Home-page: https://github.com/ARPAKIT-Company/arpakitlib
6
6
  License: Apache-2.0
@@ -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=V8n5XxkrsZo7ASNAepD0mqgRtI7qIleCDgk7WAmdLW8,18244
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/ar_enumeration.py,sha256=6KUJYOabHDPLfdigBVN0ZI4ZOUJh8TkL0g4o92Hke2I,2254
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=lt5ziw_nbueLH71d4QHi8VE2hof6YYqPEwLF2w5vjkE,9864
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=qCRSWlSgx-3yU0NRHZDQ5lCOmuZKcz2Na3py9nr6hJM,1618
53
- arpakitlib/ar_openai_util.py,sha256=d5Aj1O2yo_zYLZCLeOLvuveYYxA2jGOqhMs1oUbuVk8,1210
54
- arpakitlib/ar_operation_execution_util.py,sha256=PGyyvJoYAv01MWQl1TNWGamchfIrDkSovLrNMiyKqEg,11770
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=3zscvaloi9XY1NR70rJ4-jJlFUIqhmTbQ9wdvK-Yjf8,1379
61
- arpakitlib/ar_ssh_runner.py,sha256=jlnss4V4pziBN1rBzoK_lDiWm6nMOqGXfa6NFJSKH-Y,6796
62
- arpakitlib/ar_story_log_util.py,sha256=NKzC1nZkjofRTePHhncatO7B25hAFPvIwG45XWjI0tQ,1060
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=-h-SCsVl11eVo1u4hy2Asn0IfD5TIxmX3Ndug4AvnPE,1761
65
- arpakitlib/ar_yookassa_api_client.py,sha256=BwsTygaXf35AACVBl_09uYlSD_t-U1OOzbj58OOFT4Q,6480
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.33.dist-info/LICENSE,sha256=1jqWIkbnMxDfs_i0SXP5qbV6PHjBr1g8506oW7uPjfg,11347
68
- arpakitlib-1.5.33.dist-info/METADATA,sha256=xScH-mZtscIaV19pqqlcgX6IfeEKych_R7jlqzO-FJg,2330
69
- arpakitlib-1.5.33.dist-info/NOTICE,sha256=wHwmiq3wExfFfgMsE5U5TOBP9_l72ocIG82KurEels0,43
70
- arpakitlib-1.5.33.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
71
- arpakitlib-1.5.33.dist-info/RECORD,,
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,,
@@ -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