arpakitlib 1.7.66__py3-none-any.whl → 1.7.124__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 (81) hide show
  1. arpakitlib/_arpakit_project_template/example.env +4 -0
  2. arpakitlib/_arpakit_project_template/manage/hello_world.py +2 -2
  3. arpakitlib/_arpakit_project_template/manage/json_beutify.py +4 -4
  4. arpakitlib/_arpakit_project_template/manage/poetry_config.sh +2 -0
  5. arpakitlib/_arpakit_project_template/manage/poetry_self_add_plugin_export.sh +2 -0
  6. arpakitlib/_arpakit_project_template/manage/sandbox/sandbox_1.py +4 -4
  7. arpakitlib/_arpakit_project_template/manage/sandbox/sandbox_2.py +4 -4
  8. arpakitlib/_arpakit_project_template/manage/sandbox/sandbox_3.py +4 -4
  9. arpakitlib/_arpakit_project_template/manage/sandbox/sandbox_4.py +4 -4
  10. arpakitlib/_arpakit_project_template/manage/sandbox/sandbox_5.py +4 -4
  11. arpakitlib/_arpakit_project_template/manage/sandbox/sandbox_6.py +4 -4
  12. arpakitlib/_arpakit_project_template/manage/sandbox/sandbox_7.py +4 -4
  13. arpakitlib/_arpakit_project_template/resource/static/healthcheck +1 -0
  14. arpakitlib/_arpakit_project_template/src/admin1/__init__.py +0 -0
  15. arpakitlib/_arpakit_project_template/src/admin1/add_admin_in_app.py +25 -0
  16. arpakitlib/_arpakit_project_template/src/admin1/admin_auth.py +29 -0
  17. arpakitlib/_arpakit_project_template/src/admin1/model_view.py +19 -0
  18. arpakitlib/_arpakit_project_template/src/api/auth.py +1 -6
  19. arpakitlib/_arpakit_project_template/src/api/create_api_app.py +13 -52
  20. arpakitlib/_arpakit_project_template/src/api/event.py +51 -2
  21. arpakitlib/_arpakit_project_template/src/api/router/main_router.py +3 -0
  22. arpakitlib/_arpakit_project_template/src/api/router/v1/get_api_error_info.py +6 -4
  23. arpakitlib/_arpakit_project_template/src/api/start_api_for_dev_with_reload.py +12 -4
  24. arpakitlib/_arpakit_project_template/src/api/{start_api_for_dev.py → start_api_for_dev_without_reload.py} +3 -3
  25. arpakitlib/_arpakit_project_template/src/api/transmitted_api_data.py +4 -0
  26. arpakitlib/_arpakit_project_template/src/business_service/hello_world.py +12 -0
  27. arpakitlib/_arpakit_project_template/{manage/logging_check.py → src/core/check_logging.py} +3 -3
  28. arpakitlib/_arpakit_project_template/src/core/check_settings.py +12 -0
  29. arpakitlib/_arpakit_project_template/src/core/generate_settings_env_example.py +16 -0
  30. arpakitlib/_arpakit_project_template/src/core/settings.py +13 -5
  31. arpakitlib/_arpakit_project_template/src/core/util.py +0 -18
  32. arpakitlib/_arpakit_project_template/{manage/sqlalchemy_db_check_conn.py → src/db/check_conn_sqlalchemy_db.py} +2 -1
  33. arpakitlib/_arpakit_project_template/src/db/const.py +0 -0
  34. arpakitlib/_arpakit_project_template/src/db/init_sqlalchemy_db.py +11 -0
  35. arpakitlib/_arpakit_project_template/src/db/reinit_sqlalchemy_db.py +13 -0
  36. arpakitlib/_arpakit_project_template/src/db/util.py +21 -0
  37. arpakitlib/_arpakit_project_template/src/operation_execution/operation_executor.py +6 -4
  38. arpakitlib/_arpakit_project_template/src/operation_execution/scheduled_operations.py +3 -3
  39. arpakitlib/_arpakit_project_template/src/operation_execution/{start_operation_executor_worker_for_dev.py → start_operation_executor_worker.py} +7 -5
  40. arpakitlib/_arpakit_project_template/src/operation_execution/start_scheduled_operation_creator_worker.py +19 -0
  41. arpakitlib/_arpakit_project_template/src/operation_execution/util.py +0 -0
  42. arpakitlib/_arpakit_project_template/src/tg_bot/__init__.py +0 -0
  43. arpakitlib/_arpakit_project_template/src/tg_bot/router/__init__.py +0 -0
  44. arpakitlib/_arpakit_project_template/src/tg_bot/start_tg_bot.py +0 -0
  45. arpakitlib/api_key_util.py +12 -0
  46. arpakitlib/ar_additional_model_util.py +18 -1
  47. arpakitlib/ar_arpakit_lib_module_util.py +13 -1
  48. arpakitlib/ar_arpakit_schedule_uust_api_client_util.py +24 -3
  49. arpakitlib/ar_arpakitlib_cli_util.py +2 -0
  50. arpakitlib/ar_base_worker_util.py +67 -18
  51. arpakitlib/ar_exception_util.py +13 -0
  52. arpakitlib/ar_fastapi_util.py +129 -98
  53. arpakitlib/ar_file_util.py +2 -0
  54. arpakitlib/ar_func_util.py +55 -0
  55. arpakitlib/ar_json_util.py +11 -9
  56. arpakitlib/ar_need_type_util.py +8 -1
  57. arpakitlib/ar_openai_api_client_util.py +16 -2
  58. arpakitlib/ar_operation_execution_util.py +143 -141
  59. arpakitlib/ar_schedule_uust_api_client_util.py +13 -6
  60. arpakitlib/ar_settings_util.py +24 -5
  61. arpakitlib/ar_sqlalchemy_model_util.py +37 -7
  62. arpakitlib/ar_ssh_runner_util.py +2 -2
  63. arpakitlib/ar_str_util.py +30 -1
  64. arpakitlib/ar_type_util.py +52 -7
  65. {arpakitlib-1.7.66.dist-info → arpakitlib-1.7.124.dist-info}/METADATA +27 -20
  66. {arpakitlib-1.7.66.dist-info → arpakitlib-1.7.124.dist-info}/RECORD +70 -62
  67. {arpakitlib-1.7.66.dist-info → arpakitlib-1.7.124.dist-info}/WHEEL +1 -1
  68. arpakitlib/_arpakit_project_template/AUTHOR.md +0 -4
  69. arpakitlib/_arpakit_project_template/manage/example_nginx_proxy.nginx +0 -14
  70. arpakitlib/_arpakit_project_template/manage/example_poetry_arpakitlib.sh +0 -1
  71. arpakitlib/_arpakit_project_template/manage/example_pyproject.toml +0 -18
  72. arpakitlib/_arpakit_project_template/manage/example_systemd.service +0 -12
  73. arpakitlib/_arpakit_project_template/manage/requirements.txt +0 -209
  74. arpakitlib/_arpakit_project_template/manage/settings_check.py +0 -10
  75. arpakitlib/_arpakit_project_template/manage/settings_generate_env_example.py +0 -13
  76. arpakitlib/_arpakit_project_template/manage/sqlalchemy_db_init.py +0 -10
  77. arpakitlib/_arpakit_project_template/manage/sqlalchemy_db_reinit.py +0 -10
  78. arpakitlib/_arpakit_project_template/src/operation_execution/start_scheduled_operation_creator_worker_for_dev.py +0 -16
  79. {arpakitlib-1.7.66.dist-info → arpakitlib-1.7.124.dist-info}/LICENSE +0 -0
  80. {arpakitlib-1.7.66.dist-info → arpakitlib-1.7.124.dist-info}/NOTICE +0 -0
  81. {arpakitlib-1.7.66.dist-info → arpakitlib-1.7.124.dist-info}/entry_points.txt +0 -0
@@ -11,6 +11,7 @@ from typing import Any, Callable
11
11
  from pydantic import ConfigDict
12
12
  from pydantic.v1 import BaseModel
13
13
  from sqlalchemy import asc
14
+ from sqlalchemy.exc import NoResultFound
14
15
  from sqlalchemy.orm import Session
15
16
 
16
17
  from arpakitlib.ar_base_worker_util import BaseWorker
@@ -30,7 +31,8 @@ def get_operation_for_execution(
30
31
  *,
31
32
  session: Session | None = None,
32
33
  sqlalchemy_db: SQLAlchemyDB | None = None,
33
- filter_operation_types: list[str] | str | None = None
34
+ filter_operation_types: list[str] | str | None = None,
35
+ lock: bool = False
34
36
  ) -> OperationDBM | None:
35
37
  if isinstance(filter_operation_types, str):
36
38
  filter_operation_types = [filter_operation_types]
@@ -43,6 +45,10 @@ def get_operation_for_execution(
43
45
  )
44
46
  if filter_operation_types:
45
47
  query = query.filter(OperationDBM.type.in_(filter_operation_types))
48
+
49
+ if lock:
50
+ query = query.with_for_update()
51
+
46
52
  query = query.order_by(asc(OperationDBM.creation_dt))
47
53
  operation_dbm: OperationDBM | None = query.first()
48
54
  return operation_dbm
@@ -61,7 +67,8 @@ def get_operation_by_id(
61
67
  session: Session | None = None,
62
68
  sqlalchemy_db: SQLAlchemyDB | None = None,
63
69
  filter_operation_id: int,
64
- raise_if_not_found: bool = False
70
+ raise_if_not_found: bool = False,
71
+ lock: bool = False
65
72
  ) -> OperationDBM | None:
66
73
  def func(session_: Session):
67
74
  query = (
@@ -69,8 +76,16 @@ def get_operation_by_id(
69
76
  .query(OperationDBM)
70
77
  .filter(OperationDBM.id == filter_operation_id)
71
78
  )
79
+
80
+ if lock:
81
+ query = query.with_for_update()
82
+
72
83
  if raise_if_not_found:
73
- return query.one()
84
+ try:
85
+ return query.one()
86
+ except NoResultFound:
87
+ if raise_if_not_found:
88
+ raise ValueError("Operation not found")
74
89
  else:
75
90
  return query.one_or_none()
76
91
 
@@ -130,21 +145,17 @@ class BaseOperationExecutor:
130
145
  self._logger = logging.getLogger(self.__class__.__name__)
131
146
  self.sql_alchemy_db = sqlalchemy_db
132
147
 
133
- def sync_execute_operation(self, operation_dbm: OperationDBM) -> OperationDBM:
148
+ def sync_execute_operation(self, operation_dbm: OperationDBM, session: Session) -> OperationDBM:
134
149
  if operation_dbm.type == BaseOperationTypes.healthcheck_:
135
150
  self._logger.info("healthcheck")
136
151
  elif operation_dbm.type == BaseOperationTypes.raise_fake_exception_:
137
152
  self._logger.info("raise_fake_exception")
138
153
  raise Exception("raise_fake_exception")
139
- else:
140
- raise Exception(
141
- f"unknown operation_dbm.type,"
142
- f" operation_dbm.id={operation_dbm.id},"
143
- f" operation_dbm.type={operation_dbm.type}"
144
- )
145
154
  return operation_dbm
146
155
 
147
- def sync_safe_execute_operation(self, operation_dbm: OperationDBM) -> OperationDBM:
156
+ def sync_safe_execute_operation(
157
+ self, operation_dbm: OperationDBM, worker: OperationExecutorWorker, session: Session
158
+ ) -> OperationDBM:
148
159
  self._logger.info(
149
160
  f"start "
150
161
  f"operation_dbm.id={operation_dbm.id}, "
@@ -152,20 +163,21 @@ class BaseOperationExecutor:
152
163
  f"operation_dbm.status={operation_dbm.status}"
153
164
  )
154
165
 
155
- with self.sql_alchemy_db.new_session() as session:
156
- operation_dbm: OperationDBM = get_operation_by_id(
157
- session=session, filter_operation_id=operation_dbm.id, raise_if_not_found=True
158
- )
159
- operation_dbm.execution_start_dt = now_utc_dt()
160
- operation_dbm.status = OperationDBM.Statuses.executing
161
- session.commit()
162
- session.refresh(operation_dbm)
166
+ operation_dbm.execution_start_dt = now_utc_dt()
167
+ operation_dbm.status = OperationDBM.Statuses.executing
168
+ operation_dbm.output_data = combine_dicts(
169
+ operation_dbm.output_data,
170
+ {
171
+ worker.worker_fullname: True
172
+ }
173
+ )
174
+ session.commit()
163
175
 
164
176
  exception: BaseException | None = None
165
177
  traceback_str: str | None = None
166
178
 
167
179
  try:
168
- self.sync_execute_operation(operation_dbm=operation_dbm)
180
+ self.sync_execute_operation(operation_dbm=operation_dbm, session=session)
169
181
  except BaseException as exception_:
170
182
  self._logger.error(
171
183
  f"error in sync_execute_operation (id={operation_dbm.id}, type={operation_dbm.type})",
@@ -174,40 +186,34 @@ class BaseOperationExecutor:
174
186
  exception = exception_
175
187
  traceback_str = traceback.format_exc()
176
188
 
177
- with self.sql_alchemy_db.new_session() as session:
178
-
179
- operation_dbm: OperationDBM = get_operation_by_id(
180
- session=session, filter_operation_id=operation_dbm.id, raise_if_not_found=True
189
+ operation_dbm.execution_finish_dt = now_utc_dt()
190
+ if exception:
191
+ operation_dbm.status = OperationDBM.Statuses.executed_with_error
192
+ operation_dbm.error_data = combine_dicts(
193
+ {
194
+ "exception_str": str(exception),
195
+ "traceback_str": traceback_str
196
+ },
197
+ operation_dbm.error_data
181
198
  )
182
- operation_dbm.execution_finish_dt = now_utc_dt()
183
- if exception:
184
- operation_dbm.status = OperationDBM.Statuses.executed_with_error
185
- operation_dbm.error_data = combine_dicts(
186
- {
187
- "exception_str": str(exception),
188
- "traceback_str": traceback_str
189
- },
190
- operation_dbm.error_data
191
- )
192
- else:
193
- operation_dbm.status = OperationDBM.Statuses.executed_without_error
199
+ else:
200
+ operation_dbm.status = OperationDBM.Statuses.executed_without_error
201
+ session.commit()
202
+
203
+ if exception:
204
+ story_log_dbm = StoryLogDBM(
205
+ level=StoryLogDBM.Levels.error,
206
+ title=f"error in sync_execute_operation (id={operation_dbm.id}, type={operation_dbm.type})",
207
+ data={
208
+ "operation_id": operation_dbm.id,
209
+ "exception_str": str(exception),
210
+ "traceback_str": traceback_str
211
+ }
212
+ )
213
+ session.add(story_log_dbm)
194
214
  session.commit()
195
215
 
196
- if exception:
197
- story_log_dbm = StoryLogDBM(
198
- level=StoryLogDBM.Levels.error,
199
- title=f"error in sync_execute_operation (id={operation_dbm.id}, type={operation_dbm.type})",
200
- data={
201
- "operation_id": operation_dbm.id,
202
- "exception_str": str(exception),
203
- "traceback_str": traceback_str
204
- }
205
- )
206
- session.add(story_log_dbm)
207
- session.commit()
208
- session.refresh(story_log_dbm)
209
-
210
- session.refresh(operation_dbm)
216
+ session.refresh(operation_dbm)
211
217
 
212
218
  self._logger.info(
213
219
  f"finish sync_safe_execute_operation, "
@@ -219,21 +225,17 @@ class BaseOperationExecutor:
219
225
 
220
226
  return operation_dbm
221
227
 
222
- async def async_execute_operation(self, operation_dbm: OperationDBM) -> OperationDBM:
228
+ async def async_execute_operation(self, operation_dbm: OperationDBM, session: Session) -> OperationDBM:
223
229
  if operation_dbm.type == BaseOperationTypes.healthcheck_:
224
230
  self._logger.info("healthcheck")
225
231
  elif operation_dbm.type == BaseOperationTypes.raise_fake_exception_:
226
232
  self._logger.info("raise_fake_exception")
227
233
  raise Exception("raise_fake_exception")
228
- else:
229
- raise Exception(
230
- f"unknown operation_dbm.type,"
231
- f" operation_dbm.id={operation_dbm.id},"
232
- f" operation_dbm.type={operation_dbm.type}"
233
- )
234
234
  return operation_dbm
235
235
 
236
- async def async_safe_execute_operation(self, operation_dbm: OperationDBM) -> OperationDBM:
236
+ async def async_safe_execute_operation(
237
+ self, operation_dbm: OperationDBM, worker: OperationExecutorWorker, session: Session
238
+ ) -> OperationDBM:
237
239
  self._logger.info(
238
240
  f"start "
239
241
  f"operation_dbm.id={operation_dbm.id}, "
@@ -241,20 +243,21 @@ class BaseOperationExecutor:
241
243
  f"operation_dbm.status={operation_dbm.status}"
242
244
  )
243
245
 
244
- with self.sql_alchemy_db.new_session() as session:
245
- operation_dbm: OperationDBM = get_operation_by_id(
246
- session=session, filter_operation_id=operation_dbm.id, raise_if_not_found=True
247
- )
248
- operation_dbm.execution_start_dt = now_utc_dt()
249
- operation_dbm.status = OperationDBM.Statuses.executing
250
- session.commit()
251
- session.refresh(operation_dbm)
246
+ operation_dbm.execution_start_dt = now_utc_dt()
247
+ operation_dbm.status = OperationDBM.Statuses.executing
248
+ operation_dbm.output_data = combine_dicts(
249
+ operation_dbm.output_data,
250
+ {
251
+ worker.worker_fullname: True
252
+ }
253
+ )
254
+ session.commit()
252
255
 
253
256
  exception: BaseException | None = None
254
257
  traceback_str: str | None = None
255
258
 
256
259
  try:
257
- await self.async_execute_operation(operation_dbm=operation_dbm)
260
+ await self.async_execute_operation(operation_dbm=operation_dbm, session=session)
258
261
  except BaseException as exception_:
259
262
  self._logger.error(
260
263
  f"error in async_execute_operation (id={operation_dbm.id}, type={operation_dbm.type})",
@@ -263,40 +266,34 @@ class BaseOperationExecutor:
263
266
  exception = exception_
264
267
  traceback_str = traceback.format_exc()
265
268
 
266
- with self.sql_alchemy_db.new_session() as session:
267
-
268
- operation_dbm: OperationDBM = get_operation_by_id(
269
- session=session, filter_operation_id=operation_dbm.id, raise_if_not_found=True
269
+ operation_dbm.execution_finish_dt = now_utc_dt()
270
+ if exception:
271
+ operation_dbm.status = OperationDBM.Statuses.executed_with_error
272
+ operation_dbm.error_data = combine_dicts(
273
+ {
274
+ "exception_str": str(exception),
275
+ "traceback_str": traceback_str
276
+ },
277
+ operation_dbm.error_data
270
278
  )
271
- operation_dbm.execution_finish_dt = now_utc_dt()
272
- if exception:
273
- operation_dbm.status = OperationDBM.Statuses.executed_with_error
274
- operation_dbm.error_data = combine_dicts(
275
- {
276
- "exception_str": str(exception),
277
- "traceback_str": traceback_str
278
- },
279
- operation_dbm.error_data
280
- )
281
- else:
282
- operation_dbm.status = OperationDBM.Statuses.executed_without_error
279
+ else:
280
+ operation_dbm.status = OperationDBM.Statuses.executed_without_error
281
+ session.commit()
282
+
283
+ if exception:
284
+ story_log_dbm = StoryLogDBM(
285
+ level=StoryLogDBM.Levels.error,
286
+ title=f"error in async_execute_operation (id={operation_dbm.id}, type={operation_dbm.type})",
287
+ data={
288
+ "operation_id": operation_dbm.id,
289
+ "exception_str": str(exception),
290
+ "traceback_str": traceback_str
291
+ }
292
+ )
293
+ session.add(story_log_dbm)
283
294
  session.commit()
284
295
 
285
- if exception:
286
- story_log_dbm = StoryLogDBM(
287
- level=StoryLogDBM.Levels.error,
288
- title=f"error in async_execute_operation (id={operation_dbm.id}, type={operation_dbm.type})",
289
- data={
290
- "operation_id": operation_dbm.id,
291
- "exception_str": str(exception),
292
- "traceback_str": traceback_str
293
- }
294
- )
295
- session.add(story_log_dbm)
296
- session.commit()
297
- session.refresh(story_log_dbm)
298
-
299
- session.refresh(operation_dbm)
296
+ session.refresh(operation_dbm)
300
297
 
301
298
  self._logger.info(
302
299
  f"finish async_safe_execute_operation, "
@@ -317,53 +314,56 @@ class OperationExecutorWorker(BaseWorker):
317
314
  sqlalchemy_db: SQLAlchemyDB,
318
315
  operation_executor: BaseOperationExecutor | None = None,
319
316
  filter_operation_types: str | list[str] | None = None,
320
- timeout_after_run=timedelta(seconds=0.1).total_seconds(),
321
- timeout_after_err_in_run=timedelta(seconds=1).total_seconds()
317
+ startup_funcs: list[Any] | None = None
322
318
  ):
323
- super().__init__()
319
+ super().__init__(startup_funcs=startup_funcs)
324
320
  self.sqlalchemy_db = sqlalchemy_db
325
- self.timeout_after_run = timeout_after_run
326
- self.timeout_after_err_in_run = timeout_after_err_in_run
327
321
  if operation_executor is None:
328
322
  operation_executor = BaseOperationExecutor(sqlalchemy_db=sqlalchemy_db)
329
323
  self.operation_executor = operation_executor
324
+ if isinstance(filter_operation_types, str):
325
+ filter_operation_types = [filter_operation_types]
330
326
  self.filter_operation_types = filter_operation_types
331
327
 
332
328
  def sync_on_startup(self):
333
329
  self.sqlalchemy_db.init()
330
+ self.sync_run_startup_funcs()
334
331
 
335
- def sync_execute_operation(self, operation_dbm: OperationDBM) -> OperationDBM:
336
- return self.operation_executor.sync_safe_execute_operation(operation_dbm=operation_dbm)
337
-
338
- def sync_run(self):
339
- operation_dbm: OperationDBM | None = get_operation_for_execution(
340
- sqlalchemy_db=self.sqlalchemy_db,
341
- filter_operation_types=self.filter_operation_types
332
+ def sync_execute_operation(self, operation_dbm: OperationDBM, session: Session) -> OperationDBM:
333
+ return self.operation_executor.sync_safe_execute_operation(
334
+ operation_dbm=operation_dbm, worker=self, session=session
342
335
  )
343
- if not operation_dbm:
344
- return
345
- self.sync_execute_operation(operation_dbm=operation_dbm)
346
336
 
347
- def sync_run_on_error(self, exception: BaseException, **kwargs):
348
- pass
337
+ def sync_run(self):
338
+ with self.sqlalchemy_db.new_session() as session:
339
+ operation_dbm: OperationDBM | None = get_operation_for_execution(
340
+ session=session,
341
+ filter_operation_types=self.filter_operation_types,
342
+ lock=True
343
+ )
344
+ if not operation_dbm:
345
+ return
346
+ self.sync_execute_operation(operation_dbm=operation_dbm, session=session)
349
347
 
350
348
  async def async_on_startup(self):
351
349
  self.sqlalchemy_db.init()
350
+ await self.async_run_startup_funcs()
352
351
 
353
- async def async_execute_operation(self, operation_dbm: OperationDBM) -> OperationDBM:
354
- return await self.operation_executor.async_safe_execute_operation(operation_dbm=operation_dbm)
355
-
356
- async def async_run(self):
357
- operation_dbm: OperationDBM | None = get_operation_for_execution(
358
- sqlalchemy_db=self.sqlalchemy_db,
359
- filter_operation_types=self.filter_operation_types
352
+ async def async_execute_operation(self, operation_dbm: OperationDBM, session: Session) -> OperationDBM:
353
+ return await self.operation_executor.async_safe_execute_operation(
354
+ operation_dbm=operation_dbm, worker=self, session=session
360
355
  )
361
- if not operation_dbm:
362
- return
363
- await self.async_execute_operation(operation_dbm=operation_dbm)
364
356
 
365
- async def async_run_on_error(self, exception: BaseException, **kwargs):
366
- pass
357
+ async def async_run(self):
358
+ with self.sqlalchemy_db.new_session() as session:
359
+ operation_dbm: OperationDBM | None = get_operation_for_execution(
360
+ sqlalchemy_db=self.sqlalchemy_db,
361
+ filter_operation_types=self.filter_operation_types,
362
+ lock=True
363
+ )
364
+ if not operation_dbm:
365
+ return
366
+ await self.async_execute_operation(operation_dbm=operation_dbm, session=session)
367
367
 
368
368
 
369
369
  class ScheduledOperation(BaseModel):
@@ -380,20 +380,20 @@ class ScheduledOperationCreatorWorker(BaseWorker):
380
380
  self,
381
381
  *,
382
382
  sqlalchemy_db: SQLAlchemyDB,
383
- scheduled_operations: list[ScheduledOperation] | None = None,
384
- timeout_after_run=timedelta(seconds=0.1).total_seconds(),
385
- timeout_after_err_in_run=timedelta(seconds=1).total_seconds()
383
+ scheduled_operations: ScheduledOperation | list[ScheduledOperation] | None = None,
384
+ startup_funcs: list[Any] | None = None
386
385
  ):
387
- super().__init__()
386
+ super().__init__(startup_funcs=startup_funcs)
388
387
  self.sqlalchemy_db = sqlalchemy_db
389
- self.timeout_after_run = timeout_after_run
390
- self.timeout_after_err_in_run = timeout_after_err_in_run
391
388
  if scheduled_operations is None:
392
389
  scheduled_operations = []
390
+ if isinstance(scheduled_operations, ScheduledOperation):
391
+ scheduled_operations = [scheduled_operations]
393
392
  self.scheduled_operations = scheduled_operations
394
393
 
395
394
  def sync_on_startup(self):
396
395
  self.sqlalchemy_db.init()
396
+ self.sync_run_startup_funcs()
397
397
 
398
398
  def sync_run(self):
399
399
  timeout = None
@@ -424,6 +424,7 @@ class ScheduledOperationCreatorWorker(BaseWorker):
424
424
 
425
425
  async def async_on_startup(self):
426
426
  self.sqlalchemy_db.init()
427
+ await self.async_run_startup_funcs()
427
428
 
428
429
  async def async_run(self):
429
430
  timeout: timedelta | None = None
@@ -453,24 +454,25 @@ class ScheduledOperationCreatorWorker(BaseWorker):
453
454
  await async_safe_sleep(n=timeout)
454
455
 
455
456
 
456
- def every_timedelta_is_time_func(*, td: timedelta) -> Callable:
457
+ def every_timedelta_is_time_func(*, td: timedelta, now_dt_func: Callable = now_utc_dt) -> Callable:
457
458
  last_now_utc_dt = now_utc_dt()
458
459
 
459
460
  def func() -> bool:
460
461
  nonlocal last_now_utc_dt
461
- now_utc_dt_ = now_utc_dt()
462
- if (now_utc_dt_ - last_now_utc_dt) >= td:
463
- last_now_utc_dt = now_utc_dt_
462
+ now_dt_func_ = now_dt_func()
463
+ if (now_dt_func_ - last_now_utc_dt) >= td:
464
+ last_now_utc_dt = now_dt_func_
464
465
  return True
465
466
  return False
466
467
 
467
468
  return func
468
469
 
469
470
 
470
- def between_different_times_is_time_func(*, from_time: time, to_time: time) -> Callable:
471
+ def between_different_times_is_time_func(
472
+ *, from_time: time, to_time: time, now_dt_func: Callable = now_utc_dt
473
+ ) -> Callable:
471
474
  def func() -> bool:
472
- now_utc_dt_ = now_utc_dt()
473
- if from_time <= now_utc_dt_.time() <= to_time:
475
+ if from_time <= now_dt_func().time() <= to_time:
474
476
  return True
475
477
  return False
476
478
 
@@ -189,18 +189,25 @@ class ScheduleUUSTAPIClient:
189
189
  return False
190
190
  return True
191
191
 
192
- async def check_all(self):
192
+ async def check_all(self) -> dict[str, Any]:
193
+ current_semester = await self.get_current_semester()
194
+ self._logger.info(f"current_semester: {current_semester}")
195
+
196
+ current_week = await self.get_current_week()
197
+ self._logger.info(f"current_week: {current_week}")
198
+
193
199
  groups = await self.get_groups()
194
200
  self._logger.info(f"groups len: {len(groups)}")
195
201
 
196
202
  teachers = await self.get_teachers()
197
203
  self._logger.info(f"teachers len: {len(teachers)}")
198
204
 
199
- current_semester = await self.get_current_semester()
200
- self._logger.info(f"current_semester: {current_semester}")
201
-
202
- current_week = await self.get_current_week()
203
- self._logger.info(f"current_week: {current_week}")
205
+ return {
206
+ "current_semester": current_semester,
207
+ "current_week": current_week,
208
+ "len(groups)": len(groups),
209
+ "len(teachers)": len(teachers)
210
+ }
204
211
 
205
212
 
206
213
  def __example():
@@ -8,6 +8,8 @@ from pydantic_settings import BaseSettings
8
8
 
9
9
  from arpakitlib.ar_enumeration_util import Enumeration
10
10
 
11
+ _ARPAKIT_LIB_MODULE_VERSION = "3.0"
12
+
11
13
 
12
14
  def generate_env_example(settings_class: Union[BaseSettings, type[BaseSettings]]):
13
15
  res = ""
@@ -19,29 +21,38 @@ def generate_env_example(settings_class: Union[BaseSettings, type[BaseSettings]]
19
21
  return res
20
22
 
21
23
 
24
+ class ModeTypes(Enumeration):
25
+ not_prod: str = "not_prod"
26
+ prod: str = "prod"
27
+
28
+
22
29
  class SimpleSettings(BaseSettings):
23
30
  model_config = ConfigDict(extra="ignore")
24
31
 
25
- class ModeTypes(Enumeration):
26
- not_prod: str = "not_prod"
27
- prod: str = "prod"
28
-
29
32
  mode_type: str = ModeTypes.not_prod
30
33
 
31
34
  @field_validator("mode_type")
32
35
  @classmethod
33
36
  def validate_mode_type(cls, v: str):
34
- cls.ModeTypes.parse_and_validate_values(v)
37
+ ModeTypes.parse_and_validate_values(v.lower().strip())
35
38
  return v
36
39
 
37
40
  @property
38
41
  def is_mode_type_not_prod(self) -> bool:
39
42
  return self.mode_type == self.ModeTypes.not_prod
40
43
 
44
+ def raise_if_mode_type_not_prod(self):
45
+ if self.is_mode_type_not_prod:
46
+ raise ValueError(f"mode type = {self.mode_type}")
47
+
41
48
  @property
42
49
  def is_mode_type_prod(self) -> bool:
43
50
  return self.mode_type == self.ModeTypes.prod
44
51
 
52
+ def raise_if_mode_type_prod(self):
53
+ if self.is_mode_type_prod:
54
+ raise ValueError(f"mode type = {self.mode_type}")
55
+
45
56
  @classmethod
46
57
  def generate_env_example(cls) -> str:
47
58
  return generate_env_example(settings_class=cls)
@@ -52,3 +63,11 @@ class SimpleSettings(BaseSettings):
52
63
  with open(filepath, mode="w") as f:
53
64
  f.write(env_example)
54
65
  return env_example
66
+
67
+
68
+ def __example():
69
+ pass
70
+
71
+
72
+ if __name__ == '__main__':
73
+ __example()
@@ -4,13 +4,13 @@ from datetime import datetime, timedelta
4
4
  from typing import Any
5
5
  from uuid import uuid4
6
6
 
7
- from sqlalchemy import inspect, INTEGER, TEXT, TIMESTAMP
7
+ from sqlalchemy import inspect, INTEGER, TEXT, TIMESTAMP, func
8
8
  from sqlalchemy.dialects.postgresql import JSONB
9
9
  from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
10
10
 
11
11
  from arpakitlib.ar_datetime_util import now_utc_dt
12
12
  from arpakitlib.ar_enumeration_util import Enumeration
13
- from arpakitlib.ar_json_util import safely_transfer_to_json_str
13
+ from arpakitlib.ar_json_util import safely_transfer_obj_to_json_str
14
14
 
15
15
  _ARPAKIT_LIB_MODULE_VERSION = "3.0"
16
16
 
@@ -34,17 +34,41 @@ class BaseDBM(DeclarativeBase):
34
34
  return self._bus_data
35
35
 
36
36
  def simple_dict(self, *, include_sd_properties: bool = True) -> dict[str, Any]:
37
- res = {c.key: getattr(self, c.key) for c in inspect(self).mapper.column_attrs}
37
+ res = {}
38
+
39
+ for c in inspect(self).mapper.column_attrs:
40
+ value = getattr(self, c.key)
41
+ if isinstance(value, BaseDBM):
42
+ res[c.key] = value.simple_dict(include_sd_properties=include_sd_properties)
43
+ elif isinstance(value, list):
44
+ res[c.key] = [
45
+ item.simple_dict(include_sd_properties=include_sd_properties)
46
+ if isinstance(item, BaseDBM) else item
47
+ for item in value
48
+ ]
49
+ else:
50
+ res[c.key] = value
38
51
 
39
52
  if include_sd_properties:
40
53
  for attr_name in dir(self):
41
54
  if attr_name.startswith("sdp_") and isinstance(getattr(type(self), attr_name, None), property):
42
- res[attr_name.removesuffix("sdp_")] = getattr(self, attr_name)
55
+ prop_name = attr_name.removeprefix("sdp_")
56
+ value = getattr(self, attr_name)
57
+ if isinstance(value, BaseDBM):
58
+ res[prop_name] = value.simple_dict(include_sd_properties=include_sd_properties)
59
+ elif isinstance(value, list):
60
+ res[prop_name] = [
61
+ item.simple_dict(include_sd_properties=include_sd_properties)
62
+ if isinstance(item, BaseDBM) else item
63
+ for item in value
64
+ ]
65
+ else:
66
+ res[prop_name] = value
43
67
 
44
68
  return res
45
69
 
46
70
  def simple_json(self, *, include_sd_properties: bool = True) -> str:
47
- return safely_transfer_to_json_str(self.simple_dict(include_sd_properties=include_sd_properties))
71
+ return safely_transfer_obj_to_json_str(self.simple_dict(include_sd_properties=include_sd_properties))
48
72
 
49
73
 
50
74
  class SimpleDBM(BaseDBM):
@@ -54,10 +78,12 @@ class SimpleDBM(BaseDBM):
54
78
  INTEGER, primary_key=True, autoincrement=True, sort_order=-3, nullable=False
55
79
  )
56
80
  long_id: Mapped[str] = mapped_column(
57
- TEXT, insert_default=generate_default_long_id, unique=True, sort_order=-2, nullable=False
81
+ TEXT, insert_default=generate_default_long_id, server_default=func.gen_random_uuid(),
82
+ unique=True, sort_order=-2, nullable=False
58
83
  )
59
84
  creation_dt: Mapped[datetime] = mapped_column(
60
- TIMESTAMP(timezone=True), insert_default=now_utc_dt, index=True, sort_order=-1, nullable=False
85
+ TIMESTAMP(timezone=True), insert_default=now_utc_dt, server_default=func.now(),
86
+ index=True, sort_order=-1, nullable=False
61
87
  )
62
88
 
63
89
  def __repr__(self):
@@ -137,6 +163,10 @@ class OperationDBM(SimpleDBM):
137
163
  return None
138
164
  return self.duration.total_seconds()
139
165
 
166
+ @property
167
+ def sdp_duration_total_seconds(self) -> float | None:
168
+ return self.duration_total_seconds
169
+
140
170
 
141
171
  def __example():
142
172
  pass
@@ -11,7 +11,7 @@ import asyncssh
11
11
  import paramiko
12
12
  from pydantic import BaseModel
13
13
 
14
- from arpakitlib.ar_json_util import safely_transfer_to_json_str
14
+ from arpakitlib.ar_json_util import safely_transfer_obj_to_json_str
15
15
 
16
16
  _ARPAKIT_LIB_MODULE_VERSION = "3.0"
17
17
 
@@ -61,7 +61,7 @@ class SSHRunRes(BaseModel):
61
61
  }
62
62
 
63
63
  def simple_json(self) -> str:
64
- return safely_transfer_to_json_str(self.simple_dict())
64
+ return safely_transfer_obj_to_json_str(self.simple_dict())
65
65
 
66
66
  def __repr__(self) -> str:
67
67
  return self.simple_json()