ddeutil-workflow 0.0.10__py3-none-any.whl → 0.0.11__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.
@@ -1 +1 @@
1
- __version__: str = "0.0.10"
1
+ __version__: str = "0.0.11"
@@ -6,12 +6,13 @@
6
6
  from .exceptions import (
7
7
  JobException,
8
8
  ParamValueException,
9
- PipelineException,
10
9
  StageException,
11
10
  UtilException,
11
+ WorkflowException,
12
12
  )
13
+ from .job import Job, Strategy
13
14
  from .on import On, interval2crontab
14
- from .pipeline import Job, Pipeline, Strategy
15
+ from .scheduler import Workflow
15
16
  from .stage import Stage, handler_result
16
17
  from .utils import (
17
18
  Param,
ddeutil/workflow/api.py CHANGED
@@ -6,9 +6,14 @@
6
6
  from __future__ import annotations
7
7
 
8
8
  import asyncio
9
+ import contextlib
9
10
  import os
10
11
  import uuid
12
+ from collections.abc import AsyncIterator
13
+ from datetime import datetime, timedelta
11
14
  from queue import Empty, Queue
15
+ from threading import Thread
16
+ from typing import TypedDict
12
17
 
13
18
  from ddeutil.core import str2bool
14
19
  from dotenv import load_dotenv
@@ -19,12 +24,55 @@ from pydantic import BaseModel
19
24
 
20
25
  from .__about__ import __version__
21
26
  from .log import get_logger
22
- from .repeat import repeat_every
27
+ from .repeat import repeat_at, repeat_every
28
+ from .scheduler import WorkflowTask
23
29
 
24
30
  load_dotenv()
25
31
  logger = get_logger("ddeutil.workflow")
26
32
 
27
33
 
34
+ class State(TypedDict):
35
+ upper_queue: Queue
36
+ upper_result: dict[str, str]
37
+ scheduler: list[str]
38
+ workflow_threads: dict[str, Thread]
39
+ workflow_tasks: list[WorkflowTask]
40
+ workflow_queue: dict[str, list[datetime]]
41
+ workflow_running: dict[str, list[datetime]]
42
+
43
+
44
+ @contextlib.asynccontextmanager
45
+ async def lifespan(a: FastAPI) -> AsyncIterator[State]:
46
+ a.state.upper_queue = Queue()
47
+ a.state.upper_result = {}
48
+ a.state.scheduler = []
49
+ a.state.workflow_threads = {}
50
+ a.state.workflow_tasks = []
51
+ a.state.workflow_queue = {}
52
+ a.state.workflow_running = {}
53
+
54
+ await asyncio.create_task(broker_upper_messages())
55
+
56
+ yield {
57
+ "upper_queue": a.state.upper_queue,
58
+ "upper_result": a.state.upper_result,
59
+ # NOTE: Scheduler value should be contain a key of workflow workflow and
60
+ # list of datetime of queue and running.
61
+ #
62
+ # ... {
63
+ # ... '<workflow-name>': (
64
+ # ... [<running-datetime>, ...], [<queue-datetime>, ...]
65
+ # ... )
66
+ # ... }
67
+ #
68
+ "scheduler": a.state.scheduler,
69
+ "workflow_queue": a.state.workflow_queue,
70
+ "workflow_running": a.state.workflow_running,
71
+ "workflow_threads": a.state.workflow_threads,
72
+ "workflow_tasks": a.state.workflow_tasks,
73
+ }
74
+
75
+
28
76
  app = FastAPI(
29
77
  titile="Workflow API",
30
78
  description=(
@@ -32,47 +80,52 @@ app = FastAPI(
32
80
  "execute or schedule workflow via RestAPI."
33
81
  ),
34
82
  version=__version__,
83
+ lifespan=lifespan,
84
+ default_response_class=UJSONResponse,
35
85
  )
36
86
  app.add_middleware(GZipMiddleware, minimum_size=1000)
37
- app.queue = Queue()
38
- app.output_dict = {}
39
- app.queue_limit = 5
40
87
 
41
88
 
42
- @app.on_event("startup")
43
89
  @repeat_every(seconds=10)
44
- def broker_upper_messages():
90
+ async def broker_upper_messages():
45
91
  """Broker for receive message from the `/upper` path and change it to upper
46
92
  case. This broker use interval running in background every 10 seconds.
47
93
  """
48
- for _ in range(app.queue_limit):
94
+ for _ in range(10):
49
95
  try:
50
- obj = app.queue.get_nowait()
51
- app.output_dict[obj["request_id"]] = obj["text"].upper()
52
- logger.info(f"Upper message: {app.output_dict}")
96
+ obj = app.state.upper_queue.get_nowait()
97
+ app.state.upper_result[obj["request_id"]] = obj["text"].upper()
98
+ logger.info(f"Upper message: {app.state.upper_result}")
53
99
  except Empty:
54
100
  pass
101
+ await asyncio.sleep(0.0001)
55
102
 
56
103
 
57
104
  class Payload(BaseModel):
58
105
  text: str
59
106
 
60
107
 
61
- async def get_result(request_id):
108
+ async def get_result(request_id: str) -> dict[str, str]:
62
109
  """Get data from output dict that global."""
63
110
  while True:
64
- if request_id in app.output_dict:
65
- result = app.output_dict[request_id]
66
- del app.output_dict[request_id]
111
+ if request_id in app.state.upper_result:
112
+ result: str = app.state.upper_result[request_id]
113
+ del app.state.upper_result[request_id]
67
114
  return {"message": result}
68
115
  await asyncio.sleep(0.0025)
69
116
 
70
117
 
71
- @app.post("/upper", response_class=UJSONResponse)
118
+ @app.get("/")
119
+ @app.get("/api")
120
+ async def health():
121
+ return {"message": "Workflow API already start up"}
122
+
123
+
124
+ @app.post("/api")
72
125
  async def message_upper(payload: Payload):
73
126
  """Convert message from any case to the upper case."""
74
127
  request_id: str = str(uuid.uuid4())
75
- app.queue.put(
128
+ app.state.upper_queue.put(
76
129
  {"text": payload.text, "request_id": request_id},
77
130
  )
78
131
  return await get_result(request_id)
@@ -85,5 +138,20 @@ if str2bool(os.getenv("WORKFLOW_API_ENABLE_ROUTE_WORKFLOW", "true")):
85
138
 
86
139
  if str2bool(os.getenv("WORKFLOW_API_ENABLE_ROUTE_SCHEDULE", "true")):
87
140
  from .route import schedule
141
+ from .scheduler import workflow_task
88
142
 
89
143
  app.include_router(schedule)
144
+
145
+ @schedule.on_event("startup")
146
+ @repeat_at(cron="* * * * *", delay=2)
147
+ def schedule_broker_up():
148
+ logger.debug(
149
+ f"[SCHEDULER]: Start listening schedule from queue "
150
+ f"{app.state.scheduler}"
151
+ )
152
+ if app.state.workflow_tasks:
153
+ workflow_task(
154
+ app.state.workflow_tasks,
155
+ stop=datetime.now() + timedelta(minutes=1),
156
+ threads=app.state.workflow_threads,
157
+ )
ddeutil/workflow/cli.py CHANGED
@@ -29,21 +29,21 @@ cli.add_typer(
29
29
 
30
30
  @cli.command()
31
31
  def run(
32
- pipeline: Annotated[
32
+ workflow: Annotated[
33
33
  str,
34
- Argument(help="A pipeline name that want to run manually"),
34
+ Argument(help="A workflow name that want to run manually"),
35
35
  ],
36
36
  params: Annotated[
37
37
  str,
38
38
  Argument(
39
- help="A json string for parameters of this pipeline execution."
39
+ help="A json string for parameters of this workflow execution."
40
40
  ),
41
41
  ],
42
42
  ):
43
- """Run pipeline workflow manually with an input custom parameters that able
44
- to receive with pipeline params config.
43
+ """Run workflow workflow manually with an input custom parameters that able
44
+ to receive with workflow params config.
45
45
  """
46
- logger.info(f"Running pipeline name: {pipeline}")
46
+ logger.info(f"Running workflow name: {workflow}")
47
47
  logger.info(f"... with Parameters: {json.dumps(json.loads(params))}")
48
48
 
49
49
 
@@ -63,7 +63,7 @@ def schedule(
63
63
  externals: Annotated[
64
64
  Optional[str],
65
65
  Argument(
66
- help="A json string for parameters of this pipeline execution."
66
+ help="A json string for parameters of this workflow execution."
67
67
  ),
68
68
  ] = None,
69
69
  ):
@@ -77,20 +77,20 @@ def schedule(
77
77
  tz=ZoneInfo(os.getenv("WORKFLOW_CORE_TIMEZONE", "UTC"))
78
78
  )
79
79
 
80
- from .scheduler import workflow
80
+ from .scheduler import workflow_runner
81
81
 
82
82
  # NOTE: Start running workflow scheduler application.
83
- workflow_rs: list[str] = workflow(
83
+ workflow_rs: list[str] = workflow_runner(
84
84
  stop=stop, excluded=excluded, externals=json.loads(externals)
85
85
  )
86
86
  logger.info(f"Application run success: {workflow_rs}")
87
87
 
88
88
 
89
- @cli_log.command("pipeline-get")
90
- def pipeline_log_get(
89
+ @cli_log.command("workflow-get")
90
+ def workflow_log_get(
91
91
  name: Annotated[
92
92
  str,
93
- Argument(help="A pipeline name that want to getting log"),
93
+ Argument(help="A workflow name that want to getting log"),
94
94
  ],
95
95
  limit: Annotated[
96
96
  int,
@@ -113,8 +113,8 @@ class LogMode(str, Enum):
113
113
  delete = "delete"
114
114
 
115
115
 
116
- @cli_log.command("pipeline-delete")
117
- def pipeline_log_delete(
116
+ @cli_log.command("workflow-delete")
117
+ def workflow_log_delete(
118
118
  mode: Annotated[
119
119
  LogMode,
120
120
  Argument(case_sensitive=True),
@@ -6,22 +6,22 @@
6
6
  from __future__ import annotations
7
7
 
8
8
 
9
- class WorkflowException(Exception): ...
9
+ class BaseWorkflowException(Exception): ...
10
10
 
11
11
 
12
- class UtilException(WorkflowException): ...
12
+ class UtilException(BaseWorkflowException): ...
13
13
 
14
14
 
15
- class StageException(WorkflowException): ...
15
+ class StageException(BaseWorkflowException): ...
16
16
 
17
17
 
18
- class JobException(WorkflowException): ...
18
+ class JobException(BaseWorkflowException): ...
19
19
 
20
20
 
21
- class PipelineException(WorkflowException): ...
21
+ class WorkflowException(BaseWorkflowException): ...
22
22
 
23
23
 
24
- class PipelineFailException(WorkflowException): ...
24
+ class WorkflowFailException(WorkflowException): ...
25
25
 
26
26
 
27
27
  class ParamValueException(WorkflowException): ...