ddeutil-workflow 0.0.9__py3-none-any.whl → 0.0.10__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.
@@ -3,33 +3,62 @@
3
3
  # Licensed under the MIT License.
4
4
  # This code refs from: https://github.com/priyanshu-panwar/fastapi-utilities
5
5
  # ------------------------------------------------------------------------------
6
+ from __future__ import annotations
7
+
6
8
  import asyncio
7
- import logging
8
9
  import os
9
10
  from asyncio import ensure_future
10
11
  from datetime import datetime
11
12
  from functools import wraps
12
13
  from zoneinfo import ZoneInfo
13
14
 
14
- from croniter import croniter
15
15
  from starlette.concurrency import run_in_threadpool
16
16
 
17
+ from .cron import CronJob
18
+ from .log import get_logger
19
+
20
+ logger = get_logger("ddeutil.workflow")
17
21
 
18
- def get_cron_delta(cron: str):
22
+
23
+ def get_cronjob_delta(cron: str):
19
24
  """This function returns the time delta between now and the next cron
20
25
  execution time.
21
26
  """
22
27
  now: datetime = datetime.now(
23
28
  tz=ZoneInfo(os.getenv("WORKFLOW_CORE_TIMEZONE", "UTC"))
24
29
  )
25
- cron = croniter(cron, now)
26
- return (cron.get_next(datetime) - now).total_seconds()
30
+ cron = CronJob(cron)
31
+ return (cron.schedule(now).next - now).total_seconds()
32
+
33
+
34
+ def cron_valid(cron: str):
35
+ try:
36
+ CronJob(cron)
37
+ except Exception as err:
38
+ raise ValueError(f"Crontab value does not valid, {cron}") from err
39
+
40
+
41
+ async def run_func(
42
+ is_coroutine,
43
+ func,
44
+ *args,
45
+ raise_exceptions: bool = False,
46
+ **kwargs,
47
+ ):
48
+ try:
49
+ if is_coroutine:
50
+ await func(*args, **kwargs)
51
+ else:
52
+ await run_in_threadpool(func, *args, **kwargs)
53
+ except Exception as e:
54
+ logger.exception(e)
55
+ if raise_exceptions:
56
+ raise e
27
57
 
28
58
 
29
59
  def repeat_at(
30
60
  *,
31
61
  cron: str,
32
- logger: logging.Logger = None,
33
62
  raise_exceptions: bool = False,
34
63
  max_repetitions: int = None,
35
64
  ):
@@ -38,40 +67,37 @@ def repeat_at(
38
67
 
39
68
  :param cron: str
40
69
  Cron-style string for periodic execution, eg. '0 0 * * *' every midnight
41
- :param logger: logging.Logger (default None)
42
- Logger object to log exceptions
43
70
  :param raise_exceptions: bool (default False)
44
71
  Whether to raise exceptions or log them
45
72
  :param max_repetitions: int (default None)
46
73
  Maximum number of times to repeat the function. If None, repeat
47
74
  indefinitely.
48
-
49
75
  """
76
+ if max_repetitions and max_repetitions <= 0:
77
+ raise ValueError(
78
+ "max_repetitions should more than zero if it want to set"
79
+ )
50
80
 
51
81
  def decorator(func):
52
- is_coroutine = asyncio.iscoroutinefunction(func)
82
+ is_coroutine: bool = asyncio.iscoroutinefunction(func)
53
83
 
54
84
  @wraps(func)
55
85
  def wrapper(*_args, **_kwargs):
56
- repititions = 0
57
- if not croniter.is_valid(cron):
58
- raise ValueError("Invalid cron expression")
86
+ repititions: int = 0
87
+ cron_valid(cron)
59
88
 
60
89
  async def loop(*args, **kwargs):
61
90
  nonlocal repititions
62
91
  while max_repetitions is None or repititions < max_repetitions:
63
- try:
64
- sleep_time = get_cron_delta(cron)
65
- await asyncio.sleep(sleep_time)
66
- if is_coroutine:
67
- await func(*args, **kwargs)
68
- else:
69
- await run_in_threadpool(func, *args, **kwargs)
70
- except Exception as e:
71
- if logger:
72
- logger.exception(e)
73
- if raise_exceptions:
74
- raise e
92
+ sleep_time = get_cronjob_delta(cron)
93
+ await asyncio.sleep(sleep_time)
94
+ await run_func(
95
+ is_coroutine,
96
+ func,
97
+ *args,
98
+ raise_exceptions=raise_exceptions,
99
+ **kwargs,
100
+ )
75
101
  repititions += 1
76
102
 
77
103
  ensure_future(loop(*_args, **_kwargs))
@@ -85,7 +111,6 @@ def repeat_every(
85
111
  *,
86
112
  seconds: float,
87
113
  wait_first: bool = False,
88
- logger: logging.Logger = None,
89
114
  raise_exceptions: bool = False,
90
115
  max_repetitions: int = None,
91
116
  ):
@@ -97,17 +122,19 @@ def repeat_every(
97
122
  :param wait_first: bool (default False)
98
123
  Whether to wait `seconds` seconds before executing the function for the
99
124
  first time.
100
- :param logger: logging.Logger (default None)
101
- The logger to use for logging exceptions.
102
125
  :param raise_exceptions: bool (default False)
103
126
  Whether to raise exceptions instead of logging them.
104
127
  :param max_repetitions: int (default None)
105
128
  The maximum number of times to repeat the function. If None, the
106
129
  function will repeat indefinitely.
107
130
  """
131
+ if max_repetitions and max_repetitions <= 0:
132
+ raise ValueError(
133
+ "max_repetitions should more than zero if it want to set"
134
+ )
108
135
 
109
136
  def decorator(func):
110
- is_coroutine = asyncio.iscoroutinefunction(func)
137
+ is_coroutine: bool = asyncio.iscoroutinefunction(func)
111
138
 
112
139
  @wraps(func)
113
140
  async def wrapper(*_args, **_kwargs):
@@ -115,19 +142,19 @@ def repeat_every(
115
142
 
116
143
  async def loop(*args, **kwargs):
117
144
  nonlocal repetitions
145
+
118
146
  if wait_first:
119
147
  await asyncio.sleep(seconds)
148
+
120
149
  while max_repetitions is None or repetitions < max_repetitions:
121
- try:
122
- if is_coroutine:
123
- await func(*args, **kwargs)
124
- else:
125
- await run_in_threadpool(func, *args, **kwargs)
126
- except Exception as e:
127
- if logger is not None:
128
- logger.exception(e)
129
- if raise_exceptions:
130
- raise e
150
+ await run_func(
151
+ is_coroutine,
152
+ func,
153
+ *args,
154
+ raise_exceptions=raise_exceptions,
155
+ **kwargs,
156
+ )
157
+
131
158
  repetitions += 1
132
159
  await asyncio.sleep(seconds)
133
160
 
ddeutil/workflow/route.py CHANGED
@@ -1,22 +1,66 @@
1
+ # ------------------------------------------------------------------------------
2
+ # Copyright (c) 2022 Korawich Anuttra. All rights reserved.
3
+ # Licensed under the MIT License. See LICENSE in the project root for
4
+ # license information.
5
+ # ------------------------------------------------------------------------------
1
6
  from __future__ import annotations
2
7
 
3
- from fastapi import APIRouter, Request
8
+ from fastapi import APIRouter, HTTPException, Request
4
9
  from fastapi import status as st
10
+ from fastapi.responses import UJSONResponse
5
11
 
12
+ from .__types import DictData
6
13
  from .log import get_logger
7
-
8
- logger = get_logger(__name__)
9
- workflow = APIRouter(prefix="/wf", tags=["workflow"])
14
+ from .pipeline import Pipeline
15
+ from .repeat import repeat_every
16
+ from .utils import Loader
17
+
18
+ logger = get_logger("ddeutil.workflow")
19
+ workflow = APIRouter(
20
+ prefix="/workflow",
21
+ tags=["workflow"],
22
+ )
23
+ schedule = APIRouter(
24
+ prefix="/schedule",
25
+ tags=["schedule"],
26
+ )
10
27
 
11
28
 
12
- @workflow.get("/")
29
+ @workflow.get(
30
+ "/",
31
+ response_class=UJSONResponse,
32
+ status_code=st.HTTP_200_OK,
33
+ )
13
34
  async def get_workflows():
14
- return {"message": "getting all pipelines: []"}
35
+ """Return all pipeline workflows that exists in config path."""
36
+ pipelines: DictData = Loader.finds(Pipeline)
37
+ return {
38
+ "message": f"getting all pipelines: {pipelines}",
39
+ }
15
40
 
16
41
 
17
- @workflow.get("/{name}")
18
- async def get_workflow(name: str):
19
- return {"message": f"getting pipeline {name}"}
42
+ @workflow.get(
43
+ "/{name}",
44
+ response_class=UJSONResponse,
45
+ status_code=st.HTTP_200_OK,
46
+ )
47
+ async def get_workflow(name: str) -> DictData:
48
+ """Return model of pipeline that passing an input pipeline name."""
49
+ try:
50
+ pipeline: Pipeline = Pipeline.from_loader(name=name, externals={})
51
+ except ValueError:
52
+ raise HTTPException(
53
+ status_code=st.HTTP_404_NOT_FOUND,
54
+ detail=(
55
+ f"Workflow pipeline name: {name!r} does not found in /conf path"
56
+ ),
57
+ ) from None
58
+ return pipeline.model_dump(
59
+ by_alias=True,
60
+ exclude_none=True,
61
+ exclude_unset=True,
62
+ exclude_defaults=True,
63
+ )
20
64
 
21
65
 
22
66
  @workflow.get("/{name}/logs")
@@ -37,35 +81,12 @@ async def del_workflow_release_log(name: str, release: str):
37
81
  return {"message": f"getting pipeline {name} log in release {release}"}
38
82
 
39
83
 
40
- class JobNotFoundError(Exception):
41
- pass
42
-
84
+ @schedule.on_event("startup")
85
+ @repeat_every(seconds=60)
86
+ def schedule_broker_up():
87
+ logger.info("Start listening schedule from queue ...")
43
88
 
44
- schedule = APIRouter(prefix="/schedule", tags=["schedule"])
45
89
 
46
-
47
- @schedule.post("/", name="scheduler:add_job", status_code=st.HTTP_201_CREATED)
48
- async def add_job(request: Request):
49
- return {"job": f"{request}"}
50
-
51
-
52
- @schedule.get("/", name="scheduler:get_jobs", response_model=list)
90
+ @schedule.get("/", response_class=UJSONResponse)
53
91
  async def get_jobs(request: Request):
54
- jobs = request.app.scheduler.get_jobs()
55
- jobs = [
56
- {k: v for k, v in job.__getstate__().items() if k != "trigger"}
57
- for job in jobs
58
- ]
59
- return jobs
60
-
61
-
62
- @schedule.delete("/{job_id}", name="scheduler:remove_job")
63
- async def remove_job(request: Request, job_id: str):
64
- try:
65
- deleted = request.app.scheduler.remove_job(job_id=job_id)
66
- logger.debug(f"Job {job_id} deleted: {deleted}")
67
- return {"job": f"{job_id}"}
68
- except AttributeError as err:
69
- raise JobNotFoundError(
70
- f"No job by the id of {job_id} was found"
71
- ) from err
92
+ return {}