reykit 1.1.66__py3-none-any.whl → 1.1.68__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.
reykit/rschedule.py CHANGED
@@ -9,14 +9,16 @@
9
9
  """
10
10
 
11
11
 
12
- from typing import Any, Literal
12
+ from typing import Any, Literal, TypedDict, NotRequired
13
+ from functools import wraps as functools_wraps
13
14
  from collections.abc import Callable
14
15
  from apscheduler.executors.pool import ThreadPoolExecutor
15
16
  from apscheduler.schedulers.background import BackgroundScheduler
16
17
  from apscheduler.schedulers.blocking import BlockingScheduler
17
18
  from apscheduler.job import Job
19
+ from reydb.rdb import Database
18
20
 
19
- from .rbase import Base
21
+ from .rbase import Base, throw
20
22
 
21
23
 
22
24
  __all__ = (
@@ -27,6 +29,7 @@ __all__ = (
27
29
  class Schedule(Base):
28
30
  """
29
31
  Schedule type.
32
+ Can create database used `self.build_db` method.
30
33
  """
31
34
 
32
35
 
@@ -35,7 +38,8 @@ class Schedule(Base):
35
38
  max_workers: int = 10,
36
39
  max_instances: int = 1,
37
40
  coalesce: bool = True,
38
- block: bool = False
41
+ block: bool = False,
42
+ database: Database | None = None
39
43
  ) -> None:
40
44
  """
41
45
  Build instance attributes.
@@ -46,17 +50,20 @@ class Schedule(Base):
46
50
  max_instances : Maximum number of synchronized executions of tasks with the same ID.
47
51
  coalesce : Whether to coalesce tasks with the same ID.
48
52
  block : Whether to block.
53
+ database : `Database` instance.
54
+ - `None`: Not use database.
55
+ - `Database`: Automatic record to database.
49
56
  """
50
57
 
51
- # Set parameter.
58
+ # Build.
59
+
60
+ ## Scheduler.
52
61
  executor = ThreadPoolExecutor(max_workers)
53
62
  executors = {'default': executor}
54
63
  job_defaults = {
55
64
  'coalesce': coalesce,
56
65
  'max_instances': max_instances
57
66
  }
58
-
59
- # Instance.
60
67
  if block:
61
68
  scheduler = BlockingScheduler(
62
69
  executors=executors,
@@ -67,10 +74,20 @@ class Schedule(Base):
67
74
  executors=executors,
68
75
  job_defaults=job_defaults
69
76
  )
70
-
71
- # Set attribute.
72
77
  self.scheduler = scheduler
73
78
 
79
+ ## Database.
80
+ self.database = database
81
+
82
+ ### Database path name.
83
+ self.db_names = {
84
+ 'base': 'base',
85
+ 'base.schedule': 'schedule',
86
+ 'base.stats_schedule': 'stats_schedule'
87
+ }
88
+
89
+ self.notes: dict[str, str] = {}
90
+
74
91
 
75
92
  def start(self) -> None:
76
93
  """
@@ -122,13 +139,91 @@ class Schedule(Base):
122
139
  return jobs
123
140
 
124
141
 
142
+ def wrap_record_db(
143
+ self,
144
+ task: Callable,
145
+ note: str | None
146
+ ) -> None:
147
+ """
148
+ Decorator, record to database.
149
+
150
+ Parameters
151
+ ----------
152
+ task : Task.
153
+ note : Task note.
154
+ """
155
+
156
+
157
+ # Define.
158
+ @functools_wraps(task)
159
+ def _task(*args, **kwargs) -> None:
160
+ """
161
+ Decorated function.
162
+
163
+ Parameters
164
+ ----------
165
+ args : Position arguments of function.
166
+ kwargs : Keyword arguments of function.
167
+ """
168
+
169
+ # Handle parameter.
170
+ nonlocal task, note
171
+
172
+ # Status executing.
173
+ data = {
174
+ 'status': 0,
175
+ 'task': task.__name__,
176
+ 'note': note
177
+ }
178
+ conn = self.database.connect()
179
+ conn.execute_insert(
180
+ (self.db_names['base'], self.db_names['base.schedule']),
181
+ data
182
+ )
183
+ id_ = conn.variables['identity']
184
+ conn.commit()
185
+
186
+ # Try execute.
187
+
188
+ ## Record error.
189
+ task = self.database.error.wrap(task, note=note)
190
+
191
+ try:
192
+ task(*args, **kwargs)
193
+
194
+ # Status occurred error.
195
+ except BaseException:
196
+ data = {
197
+ 'id': id_,
198
+ 'status': 2
199
+ }
200
+ self.database.execute_update(
201
+ (self.db_names['base'], self.db_names['base.schedule']),
202
+ data
203
+ )
204
+ raise
205
+
206
+ # Status completed.
207
+ else:
208
+ data = {
209
+ 'id': id_,
210
+ 'status': 1
211
+ }
212
+ self.database.execute_update(
213
+ (self.db_names['base'], self.db_names['base.schedule']),
214
+ data
215
+ )
216
+
217
+ return _task
218
+
219
+
125
220
  def add_task(
126
221
  self,
127
222
  task: Callable,
128
- trigger: Literal['date', 'interval', 'cron'] = 'date',
129
- args: tuple | None = None,
130
- kwargs: dict | None = None,
131
- **trigger_kwargs: Any
223
+ plan: dict[str, Any],
224
+ args: Any | None = None,
225
+ kwargs: Any | None = None,
226
+ note: str | None = None
132
227
  ) -> Job:
133
228
  """
134
229
  Add task.
@@ -136,35 +231,53 @@ class Schedule(Base):
136
231
  Parameters
137
232
  ----------
138
233
  task : Task function.
139
- trigger : Trigger type.
234
+ plan : Plan trigger keyword arguments.
140
235
  args : Task position arguments.
141
236
  kwargs : Task keyword arguments.
142
- trigger_kwargs : Trigger keyword arguments.
237
+ note : Task note.
143
238
 
144
239
  Returns
145
240
  -------
146
241
  Task instance.
147
242
  """
148
243
 
244
+ # Handle parameter.
245
+ if plan is None:
246
+ plan = {}
247
+ trigger = plan.get('trigger')
248
+ trigger_args = {
249
+ key: value
250
+ for key, value in plan.items()
251
+ if key != 'trigger'
252
+ }
253
+
149
254
  # Add.
255
+
256
+ ## Database.
257
+ if self.database is not None:
258
+ task = self.wrap_record_db(task, note)
259
+
150
260
  job = self.scheduler.add_job(
151
261
  task,
152
262
  trigger,
153
263
  args,
154
264
  kwargs,
155
- **trigger_kwargs
265
+ **trigger_args
156
266
  )
157
267
 
268
+ # Note.
269
+ self.notes[job.id] = note
270
+
158
271
  return job
159
272
 
160
273
 
161
274
  def modify_task(
162
275
  self,
163
276
  task: Job | str,
164
- trigger: Literal['date', 'interval', 'cron'] | None = None,
165
- args: tuple | None = None,
166
- kwargs: dict | None = None,
167
- **trigger_kwargs: Any
277
+ plan: dict[str, Any],
278
+ args: Any | None = None,
279
+ kwargs: Any | None = None,
280
+ note: str | None = None
168
281
  ) -> None:
169
282
  """
170
283
  Modify task.
@@ -172,37 +285,46 @@ class Schedule(Base):
172
285
  Parameters
173
286
  ----------
174
287
  task : Task instance or ID.
175
- trigger : Trigger type.
288
+ plan : Plan trigger keyword arguments.
176
289
  args : Task position arguments.
177
290
  kwargs : Task keyword arguments.
178
- trigger_kwargs : Trigger keyword arguments.
291
+ note : Task note.
179
292
  """
180
293
 
181
- # Arguments.
182
- params_arg = {}
183
- if args is not None:
184
- params_arg['args'] = args
185
- if kwargs is not None:
186
- params_arg['kwargs'] = kwargs
294
+ # Handle parameter.
295
+ if type(task) == Job:
296
+ task = task.id
297
+ if plan is None:
298
+ plan = {}
299
+ trigger = plan.get('trigger')
300
+ trigger_args = {
301
+ key: value
302
+ for key, value in plan.items()
303
+ if key != 'trigger'
304
+ }
187
305
 
188
- ## Modify.
189
- if params_arg != {}:
190
- self.scheduler.modify_job(
191
- task.id,
192
- **params_arg
306
+ # Modify plan.
307
+ if plan != {}:
308
+ self.scheduler.reschedule_job(
309
+ task,
310
+ trigger=trigger,
311
+ **trigger_args
193
312
  )
194
313
 
195
- # Trigger.
314
+ # Modify arguments.
196
315
  if (
197
- trigger is not None
198
- or trigger_kwargs != {}
316
+ args != ()
317
+ or kwargs != {}
199
318
  ):
200
- self.scheduler.reschedule_job(
201
- task.id,
202
- trigger=trigger,
203
- **trigger_kwargs
319
+ self.scheduler.modify_job(
320
+ task,
321
+ args=args,
322
+ kwargs=kwargs
204
323
  )
205
324
 
325
+ # Modify note.
326
+ self.notes[task] = note
327
+
206
328
 
207
329
  def remove_task(
208
330
  self,
@@ -270,4 +392,162 @@ class Schedule(Base):
270
392
  self.scheduler.resume_job(id_)
271
393
 
272
394
 
395
+ def build_db(self) -> None:
396
+ """
397
+ Check and build all standard databases and tables, by `self.db_names`.
398
+ """
399
+
400
+ # Check.
401
+ if self.database is None:
402
+ throw(ValueError, self.database)
403
+
404
+ # Set parameter.
405
+
406
+ ## Database.
407
+ databases = [
408
+ {
409
+ 'name': self.db_names['base']
410
+ }
411
+ ]
412
+
413
+ ## Table.
414
+ tables = [
415
+
416
+ ### 'schedule'.
417
+ {
418
+ 'path': (self.db_names['base'], self.db_names['base.schedule']),
419
+ 'fields': [
420
+ {
421
+ 'name': 'create_time',
422
+ 'type': 'datetime',
423
+ 'constraint': 'NOT NULL DEFAULT CURRENT_TIMESTAMP',
424
+ 'comment': 'Record create time.'
425
+ },
426
+ {
427
+ 'name': 'update_time',
428
+ 'type': 'datetime',
429
+ 'constraint': 'DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP',
430
+ 'comment': 'Record update time.'
431
+ },
432
+ {
433
+ 'name': 'id',
434
+ 'type': 'int unsigned',
435
+ 'constraint': 'NOT NULL AUTO_INCREMENT',
436
+ 'comment': 'ID.'
437
+ },
438
+ {
439
+ 'name': 'status',
440
+ 'type': 'tinyint',
441
+ 'constraint': 'NOT NULL',
442
+ 'comment': 'Schedule status, 0 is executing, 1 is completed, 2 is occurred error.'
443
+ },
444
+ {
445
+ 'name': 'task',
446
+ 'type': 'varchar(100)',
447
+ 'constraint': 'NOT NULL',
448
+ 'comment': 'Schedule task function name.'
449
+ },
450
+ {
451
+ 'name': 'note',
452
+ 'type': 'varchar(500)',
453
+ 'comment': 'Schedule note.'
454
+ }
455
+ ],
456
+ 'primary': 'id',
457
+ 'indexes': [
458
+ {
459
+ 'name': 'n_create_time',
460
+ 'fields': 'create_time',
461
+ 'type': 'noraml',
462
+ 'comment': 'Record create time normal index.'
463
+ },
464
+ {
465
+ 'name': 'n_update_time',
466
+ 'fields': 'update_time',
467
+ 'type': 'noraml',
468
+ 'comment': 'Record update time normal index.'
469
+ },
470
+ {
471
+ 'name': 'n_task',
472
+ 'fields': 'task',
473
+ 'type': 'noraml',
474
+ 'comment': 'Schedule task function name normal index.'
475
+ }
476
+ ],
477
+ 'comment': 'Schedule execute record table.'
478
+ }
479
+
480
+ ]
481
+
482
+ ## View stats.
483
+ views_stats = [
484
+
485
+ ### 'stats_schedule'.
486
+ {
487
+ 'path': (self.db_names['base'], self.db_names['base.stats_schedule']),
488
+ 'items': [
489
+ {
490
+ 'name': 'count',
491
+ 'select': (
492
+ 'SELECT COUNT(1)\n'
493
+ f'FROM `{self.db_names['base']}`.`{self.db_names['base.schedule']}`'
494
+ ),
495
+ 'comment': 'Schedule count.'
496
+ },
497
+ {
498
+ 'name': 'past_day_count',
499
+ 'select': (
500
+ 'SELECT COUNT(1)\n'
501
+ f'FROM `{self.db_names['base']}`.`{self.db_names['base.schedule']}`\n'
502
+ 'WHERE TIMESTAMPDIFF(DAY, `create_time`, NOW()) = 0'
503
+ ),
504
+ 'comment': 'Schedule count in the past day.'
505
+ },
506
+ {
507
+ 'name': 'past_week_count',
508
+ 'select': (
509
+ 'SELECT COUNT(1)\n'
510
+ f'FROM `{self.db_names['base']}`.`{self.db_names['base.schedule']}`\n'
511
+ 'WHERE TIMESTAMPDIFF(DAY, `create_time`, NOW()) <= 6'
512
+ ),
513
+ 'comment': 'Schedule count in the past week.'
514
+ },
515
+ {
516
+ 'name': 'past_month_count',
517
+ 'select': (
518
+ 'SELECT COUNT(1)\n'
519
+ f'FROM `{self.db_names['base']}`.`{self.db_names['base.schedule']}`\n'
520
+ 'WHERE TIMESTAMPDIFF(DAY, `create_time`, NOW()) <= 29'
521
+ ),
522
+ 'comment': 'Schedule count in the past month.'
523
+ },
524
+ {
525
+ 'name': 'task_count',
526
+ 'select': (
527
+ 'SELECT COUNT(DISTINCT `task`)\n'
528
+ f'FROM `{self.db_names['base']}`.`{self.db_names['base.schedule']}`'
529
+ ),
530
+ 'comment': 'Task count.'
531
+ },
532
+ {
533
+ 'name': 'last_time',
534
+ 'select': (
535
+ 'SELECT IFNULL(MAX(`update_time`), MAX(`create_time`))\n'
536
+ f'FROM `{self.db_names['base']}`.`{self.db_names['base.schedule']}`'
537
+ ),
538
+ 'comment': 'Schedule last record time.'
539
+ }
540
+ ]
541
+
542
+ }
543
+
544
+ ]
545
+
546
+ # Build.
547
+ self.database.build.build(databases, tables, views_stats=views_stats)
548
+
549
+ ## Error.
550
+ self.database.error.build_db()
551
+
552
+
273
553
  __iter__ = tasks
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reykit
3
- Version: 1.1.66
3
+ Version: 1.1.68
4
4
  Summary: Kit method set.
5
5
  Project-URL: homepage, https://github.com/reyxbo/reykit/
6
6
  Author-email: Rey <reyxbo@163.com>
@@ -11,7 +11,7 @@ reykit/rnum.py,sha256=PhG4V_BkVfCJUsbpMDN1umGZly1Hsus80TW8bpyBtyY,3653
11
11
  reykit/ros.py,sha256=Yi_mfYzVHwjjUZke0BNX7PKFLZpoT5NRyj5aDOSNQRU,46911
12
12
  reykit/rrand.py,sha256=4VwooITgox54_GonELcJfcIpStDi-UJchpnyWKnyeIA,8606
13
13
  reykit/rre.py,sha256=1qva7xatKVE9qC2j7IujjXSM59qxHWwTYpiizFFQ8Xo,6024
14
- reykit/rschedule.py,sha256=QakEAtOcMg8uL2iOLre9uSsH-DsW6uAvzdXFiPJw_1o,5767
14
+ reykit/rschedule.py,sha256=uUSZyWAjurkWPlvDO57OgRLAreeE7WHOV0J_MG6S-zY,14985
15
15
  reykit/rstdout.py,sha256=yesWo7wIGablpyAu-2J2Gw11Qp3GdQjGICTyIcvLyt4,8200
16
16
  reykit/rsys.py,sha256=AP62KyN40flCeQJBclfJq8shachSAFT0LkVjiKsXkrw,24946
17
17
  reykit/rtable.py,sha256=UQ-JlwjssMR3gY1iY-VGQEKQ5_BZabpJy6TL7Fx19c4,12200
@@ -22,7 +22,7 @@ reykit/rwrap.py,sha256=FEmeK_fboJ-OyXeJf8bilc7U2ph8xIbZGNHb6fLCy2c,15063
22
22
  reykit/rzip.py,sha256=BGEONswuBZxQ-zcgd_xp2fcvYesC9AmKaaXWvnT3bTI,3456
23
23
  reykit/rdll/__init__.py,sha256=nLSb8onBm2ilyoxzpDzUeGfSCKwkLEesIhzK3LiJ8mk,701
24
24
  reykit/rdll/rdll_core.py,sha256=o6-rKcTQgxZQe0kD3GnwyNb3KL9IogzgCQNOmYLMm7A,5086
25
- reykit-1.1.66.dist-info/METADATA,sha256=TXCGGPt494VS6fF95ogjAuJovsm4kqc-TCMinRBG-X8,1872
26
- reykit-1.1.66.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
27
- reykit-1.1.66.dist-info/licenses/LICENSE,sha256=UYLPqp7BvPiH8yEZduJqmmyEl6hlM3lKrFIefiD4rvk,1059
28
- reykit-1.1.66.dist-info/RECORD,,
25
+ reykit-1.1.68.dist-info/METADATA,sha256=VkV4-QQHi1iAAq-8m7WMj0KO9qyqIsRkNfes0n2l6RM,1872
26
+ reykit-1.1.68.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
27
+ reykit-1.1.68.dist-info/licenses/LICENSE,sha256=UYLPqp7BvPiH8yEZduJqmmyEl6hlM3lKrFIefiD4rvk,1059
28
+ reykit-1.1.68.dist-info/RECORD,,