arpakitlib 1.6.47__py3-none-any.whl → 1.7.90__py3-none-any.whl

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