ddeutil-workflow 0.0.6__tar.gz → 0.0.8__tar.gz
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.
- {ddeutil_workflow-0.0.6/src/ddeutil_workflow.egg-info → ddeutil_workflow-0.0.8}/PKG-INFO +48 -76
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/README.md +42 -73
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/pyproject.toml +18 -6
- ddeutil_workflow-0.0.8/src/ddeutil/workflow/__about__.py +1 -0
- ddeutil_workflow-0.0.8/src/ddeutil/workflow/__init__.py +31 -0
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/src/ddeutil/workflow/__types.py +11 -1
- ddeutil_workflow-0.0.8/src/ddeutil/workflow/api.py +120 -0
- ddeutil_workflow-0.0.8/src/ddeutil/workflow/app.py +45 -0
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/src/ddeutil/workflow/exceptions.py +3 -3
- ddeutil_workflow-0.0.8/src/ddeutil/workflow/log.py +79 -0
- ddeutil_workflow-0.0.8/src/ddeutil/workflow/pipeline.py +893 -0
- ddeutil_workflow-0.0.8/src/ddeutil/workflow/repeat.py +134 -0
- ddeutil_workflow-0.0.8/src/ddeutil/workflow/route.py +78 -0
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/src/ddeutil/workflow/stage.py +209 -86
- ddeutil_workflow-0.0.8/src/ddeutil/workflow/utils.py +680 -0
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8/src/ddeutil_workflow.egg-info}/PKG-INFO +48 -76
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/src/ddeutil_workflow.egg-info/SOURCES.txt +10 -0
- ddeutil_workflow-0.0.8/src/ddeutil_workflow.egg-info/requires.txt +11 -0
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/tests/test__regex.py +41 -5
- ddeutil_workflow-0.0.8/tests/test_job.py +7 -0
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/tests/test_pipeline_if.py +3 -3
- ddeutil_workflow-0.0.8/tests/test_pipeline_matrix.py +159 -0
- ddeutil_workflow-0.0.8/tests/test_pipeline_on_ready.py +26 -0
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/tests/test_pipeline_run.py +1 -1
- ddeutil_workflow-0.0.8/tests/test_pipeline_run_raise.py +12 -0
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/tests/test_pipeline_task.py +17 -1
- ddeutil_workflow-0.0.8/tests/test_stage.py +15 -0
- ddeutil_workflow-0.0.8/tests/test_stage_trigger.py +32 -0
- ddeutil_workflow-0.0.8/tests/test_utils.py +41 -0
- ddeutil_workflow-0.0.8/tests/test_utils_param2template.py +71 -0
- ddeutil_workflow-0.0.8/tests/test_utils_result.py +43 -0
- ddeutil_workflow-0.0.6/src/ddeutil/workflow/__about__.py +0 -1
- ddeutil_workflow-0.0.6/src/ddeutil/workflow/__init__.py +0 -9
- ddeutil_workflow-0.0.6/src/ddeutil/workflow/pipeline.py +0 -497
- ddeutil_workflow-0.0.6/src/ddeutil/workflow/utils.py +0 -378
- ddeutil_workflow-0.0.6/src/ddeutil_workflow.egg-info/requires.txt +0 -7
- ddeutil_workflow-0.0.6/tests/test_pipeline_matrix.py +0 -87
- ddeutil_workflow-0.0.6/tests/test_stage_trigger.py +0 -10
- ddeutil_workflow-0.0.6/tests/test_utils.py +0 -8
- ddeutil_workflow-0.0.6/tests/test_utils_result.py +0 -22
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/LICENSE +0 -0
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/setup.cfg +0 -0
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/src/ddeutil/workflow/loader.py +0 -0
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/src/ddeutil/workflow/on.py +0 -0
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/src/ddeutil/workflow/scheduler.py +0 -0
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/src/ddeutil_workflow.egg-info/dependency_links.txt +0 -0
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/src/ddeutil_workflow.egg-info/top_level.txt +0 -0
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/tests/test__conf_exist.py +0 -0
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/tests/test__local_and_global.py +0 -0
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/tests/test_on.py +0 -0
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/tests/test_pipeline.py +0 -0
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/tests/test_pipeline_desc.py +0 -0
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/tests/test_pipeline_on.py +0 -0
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/tests/test_pipeline_params.py +0 -0
- {ddeutil_workflow-0.0.6 → ddeutil_workflow-0.0.8}/tests/test_scheduler.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: ddeutil-workflow
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.8
|
4
4
|
Summary: Data Developer & Engineer Workflow Utility Objects
|
5
5
|
Author-email: ddeutils <korawich.anu@gmail.com>
|
6
6
|
License: MIT
|
@@ -25,8 +25,11 @@ Requires-Dist: fmtutil
|
|
25
25
|
Requires-Dist: ddeutil-io
|
26
26
|
Requires-Dist: python-dotenv==1.0.1
|
27
27
|
Provides-Extra: app
|
28
|
-
Requires-Dist:
|
29
|
-
|
28
|
+
Requires-Dist: schedule<2.0.0,==1.2.2; extra == "app"
|
29
|
+
Provides-Extra: api
|
30
|
+
Requires-Dist: fastapi[standard]==0.112.0; extra == "api"
|
31
|
+
Requires-Dist: apscheduler[sqlalchemy]<4.0.0,==3.10.4; extra == "api"
|
32
|
+
Requires-Dist: croniter==3.0.3; extra == "api"
|
30
33
|
|
31
34
|
# Workflow
|
32
35
|
|
@@ -39,7 +42,6 @@ Requires-Dist: apscheduler[sqlalchemy]==3.10.4; extra == "app"
|
|
39
42
|
|
40
43
|
- [Installation](#installation)
|
41
44
|
- [Getting Started](#getting-started)
|
42
|
-
- [Core Features](#core-features)
|
43
45
|
- [On](#on)
|
44
46
|
- [Pipeline](#pipeline)
|
45
47
|
- [Usage](#usage)
|
@@ -50,12 +52,14 @@ Requires-Dist: apscheduler[sqlalchemy]==3.10.4; extra == "app"
|
|
50
52
|
- [Deployment](#deployment)
|
51
53
|
|
52
54
|
This **Workflow** objects was created for easy to make a simple metadata
|
53
|
-
driven pipeline that able to **ETL, T, EL, or
|
55
|
+
driven for data pipeline orchestration that able to use for **ETL, T, EL, or
|
56
|
+
ELT** by a `.yaml` file template.
|
54
57
|
|
55
|
-
I think
|
56
|
-
write
|
57
|
-
|
58
|
-
|
58
|
+
In my opinion, I think it should not create duplicate pipeline codes if I can
|
59
|
+
write with dynamic input parameters on the one template pipeline that just change
|
60
|
+
the input parameters per use-case instead.
|
61
|
+
This way I can handle a lot of logical pipelines in our orgs with only metadata
|
62
|
+
configuration. It called **Metadata Driven Data Pipeline**.
|
59
63
|
|
60
64
|
Next, we should get some monitoring tools for manage logging that return from
|
61
65
|
pipeline running. Because it not show us what is a use-case that running data
|
@@ -75,9 +79,10 @@ pip install ddeutil-workflow
|
|
75
79
|
This project need `ddeutil-io` extension namespace packages. If you want to install
|
76
80
|
this package with application add-ons, you should add `app` in installation;
|
77
81
|
|
78
|
-
|
79
|
-
|
80
|
-
|
82
|
+
| Usecase | Install Optional |
|
83
|
+
|--------------------|---------------------------|
|
84
|
+
| Scheduler Service | `ddeutil-workflow[app]` |
|
85
|
+
| FastAPI Server | `ddeutil-workflow[api]` |
|
81
86
|
|
82
87
|
## Getting Started
|
83
88
|
|
@@ -160,11 +165,12 @@ use-case.
|
|
160
165
|
> I recommend you to use `task` stage for all actions that you want to do with
|
161
166
|
> pipeline object.
|
162
167
|
|
163
|
-
### Python & Bash
|
164
|
-
|
165
168
|
```yaml
|
166
169
|
run_py_local:
|
167
170
|
type: pipeline.Pipeline
|
171
|
+
on:
|
172
|
+
- cronjob: '* * * * *'
|
173
|
+
timezone: "Asia/Bangkok"
|
168
174
|
params:
|
169
175
|
author-run: str
|
170
176
|
run-date: datetime
|
@@ -174,12 +180,11 @@ run_py_local:
|
|
174
180
|
- name: "Printing Information"
|
175
181
|
id: define-func
|
176
182
|
run: |
|
177
|
-
x = '${{ params.
|
178
|
-
print(f'Hello {x}')
|
183
|
+
x = '${{ params.run-date | fmt("%Y%m%d") }}'
|
184
|
+
print(f'Hello at {x}')
|
179
185
|
|
180
186
|
def echo(name: str):
|
181
187
|
print(f'Hello {name}')
|
182
|
-
|
183
188
|
- name: "Run Sequence and use var from Above"
|
184
189
|
vars:
|
185
190
|
x: ${{ params.author-run }}
|
@@ -187,7 +192,6 @@ run_py_local:
|
|
187
192
|
print(f'Receive x from above with {x}')
|
188
193
|
# Change x value
|
189
194
|
x: int = 1
|
190
|
-
|
191
195
|
- name: "Call Function"
|
192
196
|
vars:
|
193
197
|
echo: ${{ stages.define-func.outputs.echo }}
|
@@ -202,75 +206,34 @@ run_py_local:
|
|
202
206
|
```
|
203
207
|
|
204
208
|
```python
|
209
|
+
from datetime import datetime
|
205
210
|
from ddeutil.workflow.pipeline import Pipeline
|
206
211
|
|
207
|
-
pipe = Pipeline.from_loader(name='run_py_local', externals={})
|
208
|
-
pipe.execute(params={
|
212
|
+
pipe: Pipeline = Pipeline.from_loader(name='run_py_local', externals={})
|
213
|
+
pipe.execute(params={
|
214
|
+
'author-run': 'Local Workflow',
|
215
|
+
'run-date': datetime(2024, 1, 1),
|
216
|
+
})
|
209
217
|
```
|
210
218
|
|
211
219
|
```shell
|
212
|
-
> Hello
|
220
|
+
> Hello at 20240101
|
213
221
|
> Receive x from above with Local Workflow
|
214
222
|
> Hello Caller
|
215
223
|
> Hello World from Shell
|
216
224
|
```
|
217
225
|
|
218
|
-
### Hook (Extract & Load)
|
219
|
-
|
220
|
-
```yaml
|
221
|
-
pipe_el_pg_to_lake:
|
222
|
-
type: pipeline.Pipeline
|
223
|
-
params:
|
224
|
-
run-date: datetime
|
225
|
-
author-email: str
|
226
|
-
jobs:
|
227
|
-
extract-load:
|
228
|
-
stages:
|
229
|
-
- name: "Extract Load from Postgres to Lake"
|
230
|
-
id: extract-load
|
231
|
-
uses: tasks/postgres-to-delta@polars
|
232
|
-
with:
|
233
|
-
source:
|
234
|
-
conn: conn_postgres_url
|
235
|
-
query: |
|
236
|
-
select * from ${{ params.name }}
|
237
|
-
where update_date = '${{ params.datetime }}'
|
238
|
-
sink:
|
239
|
-
conn: conn_az_lake
|
240
|
-
endpoint: "/${{ params.name }}"
|
241
|
-
```
|
242
|
-
|
243
|
-
### Hook (Transform)
|
244
|
-
|
245
|
-
```yaml
|
246
|
-
pipeline_hook_mssql_proc:
|
247
|
-
type: pipeline.Pipeline
|
248
|
-
params:
|
249
|
-
run_date: datetime
|
250
|
-
sp_name: str
|
251
|
-
source_name: str
|
252
|
-
target_name: str
|
253
|
-
jobs:
|
254
|
-
transform:
|
255
|
-
stages:
|
256
|
-
- name: "Transform Data in MS SQL Server"
|
257
|
-
id: transform
|
258
|
-
uses: tasks/mssql-proc@odbc
|
259
|
-
with:
|
260
|
-
exec: ${{ params.sp_name }}
|
261
|
-
params:
|
262
|
-
run_mode: "T"
|
263
|
-
run_date: ${{ params.run_date }}
|
264
|
-
source: ${{ params.source_name }}
|
265
|
-
target: ${{ params.target_name }}
|
266
|
-
```
|
267
|
-
|
268
226
|
## Configuration
|
269
227
|
|
270
228
|
```bash
|
271
229
|
export WORKFLOW_ROOT_PATH=.
|
272
230
|
export WORKFLOW_CORE_REGISTRY=ddeutil.workflow,tests.utils
|
231
|
+
export WORKFLOW_CORE_REGISTRY_FILTER=ddeutil.workflow.utils
|
273
232
|
export WORKFLOW_CORE_PATH_CONF=conf
|
233
|
+
export WORKFLOW_CORE_TIMEZONE=Asia/Bangkok
|
234
|
+
export WORKFLOW_CORE_DEFAULT_STAGE_ID=true
|
235
|
+
export WORKFLOW_CORE_MAX_PIPELINE_POKING=4
|
236
|
+
export WORKFLOW_CORE_MAX_JOB_PARALLEL=2
|
274
237
|
```
|
275
238
|
|
276
239
|
Application config:
|
@@ -283,12 +246,21 @@ export WORKFLOW_APP_INTERVAL=10
|
|
283
246
|
## Deployment
|
284
247
|
|
285
248
|
This package able to run as a application service for receive manual trigger
|
286
|
-
from the master node via RestAPI
|
249
|
+
from the master node via RestAPI or use to be Scheduler background service
|
250
|
+
like crontab job but via Python API.
|
287
251
|
|
288
|
-
|
289
|
-
> This feature do not start yet because I still research and find the best tool
|
290
|
-
> to use it provision an app service, like `starlette`, `fastapi`, `apscheduler`.
|
252
|
+
### Schedule Service
|
291
253
|
|
292
254
|
```shell
|
293
|
-
(venv) $ workflow
|
255
|
+
(venv) $ python src.ddeutil.workflow.app
|
294
256
|
```
|
257
|
+
|
258
|
+
### API Server
|
259
|
+
|
260
|
+
```shell
|
261
|
+
(venv) $ uvicorn src.ddeutil.workflow.api:app --host 0.0.0.0 --port 80 --reload
|
262
|
+
```
|
263
|
+
|
264
|
+
> [!NOTE]
|
265
|
+
> If this package already deploy, it able to use
|
266
|
+
> `uvicorn ddeutil.workflow.api:app --host 0.0.0.0 --port 80`
|
@@ -9,7 +9,6 @@
|
|
9
9
|
|
10
10
|
- [Installation](#installation)
|
11
11
|
- [Getting Started](#getting-started)
|
12
|
-
- [Core Features](#core-features)
|
13
12
|
- [On](#on)
|
14
13
|
- [Pipeline](#pipeline)
|
15
14
|
- [Usage](#usage)
|
@@ -20,12 +19,14 @@
|
|
20
19
|
- [Deployment](#deployment)
|
21
20
|
|
22
21
|
This **Workflow** objects was created for easy to make a simple metadata
|
23
|
-
driven pipeline that able to **ETL, T, EL, or
|
22
|
+
driven for data pipeline orchestration that able to use for **ETL, T, EL, or
|
23
|
+
ELT** by a `.yaml` file template.
|
24
24
|
|
25
|
-
I think
|
26
|
-
write
|
27
|
-
|
28
|
-
|
25
|
+
In my opinion, I think it should not create duplicate pipeline codes if I can
|
26
|
+
write with dynamic input parameters on the one template pipeline that just change
|
27
|
+
the input parameters per use-case instead.
|
28
|
+
This way I can handle a lot of logical pipelines in our orgs with only metadata
|
29
|
+
configuration. It called **Metadata Driven Data Pipeline**.
|
29
30
|
|
30
31
|
Next, we should get some monitoring tools for manage logging that return from
|
31
32
|
pipeline running. Because it not show us what is a use-case that running data
|
@@ -45,9 +46,10 @@ pip install ddeutil-workflow
|
|
45
46
|
This project need `ddeutil-io` extension namespace packages. If you want to install
|
46
47
|
this package with application add-ons, you should add `app` in installation;
|
47
48
|
|
48
|
-
|
49
|
-
|
50
|
-
|
49
|
+
| Usecase | Install Optional |
|
50
|
+
|--------------------|---------------------------|
|
51
|
+
| Scheduler Service | `ddeutil-workflow[app]` |
|
52
|
+
| FastAPI Server | `ddeutil-workflow[api]` |
|
51
53
|
|
52
54
|
## Getting Started
|
53
55
|
|
@@ -130,11 +132,12 @@ use-case.
|
|
130
132
|
> I recommend you to use `task` stage for all actions that you want to do with
|
131
133
|
> pipeline object.
|
132
134
|
|
133
|
-
### Python & Bash
|
134
|
-
|
135
135
|
```yaml
|
136
136
|
run_py_local:
|
137
137
|
type: pipeline.Pipeline
|
138
|
+
on:
|
139
|
+
- cronjob: '* * * * *'
|
140
|
+
timezone: "Asia/Bangkok"
|
138
141
|
params:
|
139
142
|
author-run: str
|
140
143
|
run-date: datetime
|
@@ -144,12 +147,11 @@ run_py_local:
|
|
144
147
|
- name: "Printing Information"
|
145
148
|
id: define-func
|
146
149
|
run: |
|
147
|
-
x = '${{ params.
|
148
|
-
print(f'Hello {x}')
|
150
|
+
x = '${{ params.run-date | fmt("%Y%m%d") }}'
|
151
|
+
print(f'Hello at {x}')
|
149
152
|
|
150
153
|
def echo(name: str):
|
151
154
|
print(f'Hello {name}')
|
152
|
-
|
153
155
|
- name: "Run Sequence and use var from Above"
|
154
156
|
vars:
|
155
157
|
x: ${{ params.author-run }}
|
@@ -157,7 +159,6 @@ run_py_local:
|
|
157
159
|
print(f'Receive x from above with {x}')
|
158
160
|
# Change x value
|
159
161
|
x: int = 1
|
160
|
-
|
161
162
|
- name: "Call Function"
|
162
163
|
vars:
|
163
164
|
echo: ${{ stages.define-func.outputs.echo }}
|
@@ -172,75 +173,34 @@ run_py_local:
|
|
172
173
|
```
|
173
174
|
|
174
175
|
```python
|
176
|
+
from datetime import datetime
|
175
177
|
from ddeutil.workflow.pipeline import Pipeline
|
176
178
|
|
177
|
-
pipe = Pipeline.from_loader(name='run_py_local', externals={})
|
178
|
-
pipe.execute(params={
|
179
|
+
pipe: Pipeline = Pipeline.from_loader(name='run_py_local', externals={})
|
180
|
+
pipe.execute(params={
|
181
|
+
'author-run': 'Local Workflow',
|
182
|
+
'run-date': datetime(2024, 1, 1),
|
183
|
+
})
|
179
184
|
```
|
180
185
|
|
181
186
|
```shell
|
182
|
-
> Hello
|
187
|
+
> Hello at 20240101
|
183
188
|
> Receive x from above with Local Workflow
|
184
189
|
> Hello Caller
|
185
190
|
> Hello World from Shell
|
186
191
|
```
|
187
192
|
|
188
|
-
### Hook (Extract & Load)
|
189
|
-
|
190
|
-
```yaml
|
191
|
-
pipe_el_pg_to_lake:
|
192
|
-
type: pipeline.Pipeline
|
193
|
-
params:
|
194
|
-
run-date: datetime
|
195
|
-
author-email: str
|
196
|
-
jobs:
|
197
|
-
extract-load:
|
198
|
-
stages:
|
199
|
-
- name: "Extract Load from Postgres to Lake"
|
200
|
-
id: extract-load
|
201
|
-
uses: tasks/postgres-to-delta@polars
|
202
|
-
with:
|
203
|
-
source:
|
204
|
-
conn: conn_postgres_url
|
205
|
-
query: |
|
206
|
-
select * from ${{ params.name }}
|
207
|
-
where update_date = '${{ params.datetime }}'
|
208
|
-
sink:
|
209
|
-
conn: conn_az_lake
|
210
|
-
endpoint: "/${{ params.name }}"
|
211
|
-
```
|
212
|
-
|
213
|
-
### Hook (Transform)
|
214
|
-
|
215
|
-
```yaml
|
216
|
-
pipeline_hook_mssql_proc:
|
217
|
-
type: pipeline.Pipeline
|
218
|
-
params:
|
219
|
-
run_date: datetime
|
220
|
-
sp_name: str
|
221
|
-
source_name: str
|
222
|
-
target_name: str
|
223
|
-
jobs:
|
224
|
-
transform:
|
225
|
-
stages:
|
226
|
-
- name: "Transform Data in MS SQL Server"
|
227
|
-
id: transform
|
228
|
-
uses: tasks/mssql-proc@odbc
|
229
|
-
with:
|
230
|
-
exec: ${{ params.sp_name }}
|
231
|
-
params:
|
232
|
-
run_mode: "T"
|
233
|
-
run_date: ${{ params.run_date }}
|
234
|
-
source: ${{ params.source_name }}
|
235
|
-
target: ${{ params.target_name }}
|
236
|
-
```
|
237
|
-
|
238
193
|
## Configuration
|
239
194
|
|
240
195
|
```bash
|
241
196
|
export WORKFLOW_ROOT_PATH=.
|
242
197
|
export WORKFLOW_CORE_REGISTRY=ddeutil.workflow,tests.utils
|
198
|
+
export WORKFLOW_CORE_REGISTRY_FILTER=ddeutil.workflow.utils
|
243
199
|
export WORKFLOW_CORE_PATH_CONF=conf
|
200
|
+
export WORKFLOW_CORE_TIMEZONE=Asia/Bangkok
|
201
|
+
export WORKFLOW_CORE_DEFAULT_STAGE_ID=true
|
202
|
+
export WORKFLOW_CORE_MAX_PIPELINE_POKING=4
|
203
|
+
export WORKFLOW_CORE_MAX_JOB_PARALLEL=2
|
244
204
|
```
|
245
205
|
|
246
206
|
Application config:
|
@@ -253,12 +213,21 @@ export WORKFLOW_APP_INTERVAL=10
|
|
253
213
|
## Deployment
|
254
214
|
|
255
215
|
This package able to run as a application service for receive manual trigger
|
256
|
-
from the master node via RestAPI
|
216
|
+
from the master node via RestAPI or use to be Scheduler background service
|
217
|
+
like crontab job but via Python API.
|
257
218
|
|
258
|
-
|
259
|
-
> This feature do not start yet because I still research and find the best tool
|
260
|
-
> to use it provision an app service, like `starlette`, `fastapi`, `apscheduler`.
|
219
|
+
### Schedule Service
|
261
220
|
|
262
221
|
```shell
|
263
|
-
(venv) $ workflow
|
222
|
+
(venv) $ python src.ddeutil.workflow.app
|
264
223
|
```
|
224
|
+
|
225
|
+
### API Server
|
226
|
+
|
227
|
+
```shell
|
228
|
+
(venv) $ uvicorn src.ddeutil.workflow.api:app --host 0.0.0.0 --port 80 --reload
|
229
|
+
```
|
230
|
+
|
231
|
+
> [!NOTE]
|
232
|
+
> If this package already deploy, it able to use
|
233
|
+
> `uvicorn ddeutil.workflow.api:app --host 0.0.0.0 --port 80`
|
@@ -33,8 +33,12 @@ dynamic = ["version"]
|
|
33
33
|
|
34
34
|
[project.optional-dependencies]
|
35
35
|
app = [
|
36
|
-
"
|
37
|
-
|
36
|
+
"schedule==1.2.2,<2.0.0",
|
37
|
+
]
|
38
|
+
api = [
|
39
|
+
"fastapi[standard]==0.112.0",
|
40
|
+
"apscheduler[sqlalchemy]==3.10.4,<4.0.0",
|
41
|
+
"croniter==3.0.3",
|
38
42
|
]
|
39
43
|
|
40
44
|
[project.urls]
|
@@ -55,8 +59,16 @@ changelog = "CHANGELOG.md"
|
|
55
59
|
branch = true
|
56
60
|
relative_files = true
|
57
61
|
concurrency = ["thread", "multiprocessing"]
|
58
|
-
source = ["ddeutil", "tests"]
|
59
|
-
omit = [
|
62
|
+
source = ["ddeutil.workflow", "tests"]
|
63
|
+
omit = [
|
64
|
+
"scripts/",
|
65
|
+
"src/ddeutil/workflow/api.py",
|
66
|
+
"src/ddeutil/workflow/app.py",
|
67
|
+
"src/ddeutil/workflow/repeat.py",
|
68
|
+
"src/ddeutil/workflow/route.py",
|
69
|
+
"tests/utils.py",
|
70
|
+
"tests/tasks/dummy.py",
|
71
|
+
]
|
60
72
|
|
61
73
|
[tool.coverage.report]
|
62
74
|
exclude_lines = ["raise NotImplementedError"]
|
@@ -71,8 +83,8 @@ addopts = [
|
|
71
83
|
filterwarnings = ["error"]
|
72
84
|
log_cli = true
|
73
85
|
log_cli_level = "DEBUG"
|
74
|
-
log_cli_format = "%(asctime)s [%(levelname)7s] %(message)
|
75
|
-
log_cli_date_format = "%Y
|
86
|
+
log_cli_format = "%(asctime)s [%(levelname)-7s] %(message)-100s (%(filename)s:%(lineno)s)"
|
87
|
+
log_cli_date_format = "%Y%m%d %H:%M:%S"
|
76
88
|
|
77
89
|
[tool.black]
|
78
90
|
line-length = 80
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__: str = "0.0.8"
|
@@ -0,0 +1,31 @@
|
|
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
|
+
# ------------------------------------------------------------------------------
|
6
|
+
from .exceptions import (
|
7
|
+
JobException,
|
8
|
+
ParamValueException,
|
9
|
+
PipelineException,
|
10
|
+
StageException,
|
11
|
+
UtilException,
|
12
|
+
)
|
13
|
+
from .on import AwsOn, On
|
14
|
+
from .pipeline import Job, Pipeline
|
15
|
+
from .stage import (
|
16
|
+
BashStage,
|
17
|
+
EmptyStage,
|
18
|
+
HookStage,
|
19
|
+
PyStage,
|
20
|
+
Stage,
|
21
|
+
TriggerStage,
|
22
|
+
)
|
23
|
+
from .utils import (
|
24
|
+
ChoiceParam,
|
25
|
+
DatetimeParam,
|
26
|
+
IntParam,
|
27
|
+
Param,
|
28
|
+
StrParam,
|
29
|
+
dash2underscore,
|
30
|
+
param2template,
|
31
|
+
)
|
@@ -27,12 +27,21 @@ class Re:
|
|
27
27
|
"""Regular expression config."""
|
28
28
|
|
29
29
|
# NOTE: Search caller
|
30
|
+
# \${{\s*(?P<caller>[a-zA-Z0-9_.\s'\"\[\]\(\)\-\{}]+?)\s*(?P<post_filters>(?:\|\s*(?:[a-zA-Z0-9_]{3,}[a-zA-Z0-9_.,-\\%\s'\"[\]()\{}]+)\s*)*)}}
|
30
31
|
__re_caller: str = r"""
|
31
32
|
\$
|
32
33
|
{{
|
33
|
-
\s*
|
34
|
+
\s*
|
35
|
+
(?P<caller>
|
34
36
|
[a-zA-Z0-9_.\s'\"\[\]\(\)\-\{}]+?
|
35
37
|
)\s*
|
38
|
+
(?P<post_filters>
|
39
|
+
(?:
|
40
|
+
\|\s*
|
41
|
+
(?:[a-zA-Z0-9_]{3,}[a-zA-Z0-9_.,-\\%\s'\"[\]()\{}]*)
|
42
|
+
\s*
|
43
|
+
)*
|
44
|
+
)
|
36
45
|
}}
|
37
46
|
"""
|
38
47
|
RE_CALLER: Pattern = re.compile(
|
@@ -40,6 +49,7 @@ class Re:
|
|
40
49
|
)
|
41
50
|
|
42
51
|
# NOTE: Search task
|
52
|
+
# ^(?P<path>[^/@]+)/(?P<func>[^@]+)@(?P<tag>.+)$
|
43
53
|
__re_task_fmt: str = r"""
|
44
54
|
^
|
45
55
|
(?P<path>[^/@]+)
|
@@ -0,0 +1,120 @@
|
|
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
|
+
# ------------------------------------------------------------------------------
|
6
|
+
from __future__ import annotations
|
7
|
+
|
8
|
+
import asyncio
|
9
|
+
import queue
|
10
|
+
import time
|
11
|
+
import uuid
|
12
|
+
from contextlib import asynccontextmanager
|
13
|
+
from datetime import datetime
|
14
|
+
|
15
|
+
from apscheduler.executors.pool import ProcessPoolExecutor
|
16
|
+
from apscheduler.jobstores.memory import MemoryJobStore
|
17
|
+
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
|
18
|
+
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
19
|
+
from fastapi import BackgroundTasks, FastAPI
|
20
|
+
from fastapi.middleware.gzip import GZipMiddleware
|
21
|
+
from fastapi.responses import UJSONResponse
|
22
|
+
from pydantic import BaseModel
|
23
|
+
|
24
|
+
from .log import get_logger
|
25
|
+
from .repeat import repeat_every
|
26
|
+
from .route import schedule_route, workflow_route
|
27
|
+
|
28
|
+
logger = get_logger(__name__)
|
29
|
+
|
30
|
+
|
31
|
+
def broker_upper_messages():
|
32
|
+
for _ in range(app.queue_limit):
|
33
|
+
try:
|
34
|
+
obj = app.queue.get_nowait()
|
35
|
+
app.output_dict[obj["request_id"]] = obj["text"].upper()
|
36
|
+
logger.info(f"Upper message: {app.output_dict}")
|
37
|
+
except queue.Empty:
|
38
|
+
pass
|
39
|
+
|
40
|
+
|
41
|
+
jobstores = {
|
42
|
+
"default": MemoryJobStore(),
|
43
|
+
"sqlite": SQLAlchemyJobStore(url="sqlite:///jobs-store.sqlite"),
|
44
|
+
}
|
45
|
+
executors = {
|
46
|
+
"default": {"type": "threadpool", "max_workers": 5},
|
47
|
+
"processpool": ProcessPoolExecutor(max_workers=5),
|
48
|
+
}
|
49
|
+
scheduler = AsyncIOScheduler(
|
50
|
+
jobstores=jobstores,
|
51
|
+
executors=executors,
|
52
|
+
timezone="Asia/Bangkok",
|
53
|
+
)
|
54
|
+
|
55
|
+
|
56
|
+
@asynccontextmanager
|
57
|
+
async def lifespan(_: FastAPI):
|
58
|
+
scheduler.start()
|
59
|
+
yield
|
60
|
+
scheduler.shutdown(wait=False)
|
61
|
+
|
62
|
+
|
63
|
+
app = FastAPI(lifespan=lifespan)
|
64
|
+
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
65
|
+
app.include_router(schedule_route)
|
66
|
+
app.include_router(workflow_route)
|
67
|
+
|
68
|
+
app.scheduler = scheduler
|
69
|
+
app.scheduler.add_job(
|
70
|
+
broker_upper_messages,
|
71
|
+
"interval",
|
72
|
+
seconds=10,
|
73
|
+
)
|
74
|
+
app.queue = queue.Queue()
|
75
|
+
app.output_dict = {}
|
76
|
+
app.queue_limit = 2
|
77
|
+
|
78
|
+
|
79
|
+
def write_pipeline(task_id: str, message=""):
|
80
|
+
logger.info(f"{task_id} : {message}")
|
81
|
+
time.sleep(5)
|
82
|
+
logger.info(f"{task_id} : run task successfully!!!")
|
83
|
+
|
84
|
+
|
85
|
+
@app.post("/schedule/{name}", response_class=UJSONResponse)
|
86
|
+
async def send_schedule(name: str, background_tasks: BackgroundTasks):
|
87
|
+
background_tasks.add_task(
|
88
|
+
write_pipeline,
|
89
|
+
name,
|
90
|
+
message=f"some message for {name}",
|
91
|
+
)
|
92
|
+
await fetch_current_time()
|
93
|
+
return {"message": f"Schedule sent {name!r} in the background"}
|
94
|
+
|
95
|
+
|
96
|
+
@repeat_every(seconds=2, max_repetitions=3)
|
97
|
+
async def fetch_current_time():
|
98
|
+
logger.info(f"Fetch: {datetime.now()}")
|
99
|
+
|
100
|
+
|
101
|
+
class Payload(BaseModel):
|
102
|
+
text: str
|
103
|
+
|
104
|
+
|
105
|
+
async def get_result(request_id):
|
106
|
+
while 1:
|
107
|
+
if request_id in app.output_dict:
|
108
|
+
result = app.output_dict[request_id]
|
109
|
+
del app.output_dict[request_id]
|
110
|
+
return {"message": result}
|
111
|
+
await asyncio.sleep(0.001)
|
112
|
+
|
113
|
+
|
114
|
+
@app.post("/upper", response_class=UJSONResponse)
|
115
|
+
async def message_upper(payload: Payload):
|
116
|
+
request_id: str = str(uuid.uuid4())
|
117
|
+
app.queue.put(
|
118
|
+
{"text": payload.text, "request_id": request_id},
|
119
|
+
)
|
120
|
+
return await get_result(request_id)
|
@@ -0,0 +1,45 @@
|
|
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
|
+
# ------------------------------------------------------------------------------
|
6
|
+
from __future__ import annotations
|
7
|
+
|
8
|
+
import functools
|
9
|
+
import time
|
10
|
+
|
11
|
+
import schedule
|
12
|
+
|
13
|
+
|
14
|
+
def catch_exceptions(cancel_on_failure=False):
|
15
|
+
"""Catch exception error from scheduler job."""
|
16
|
+
|
17
|
+
def catch_exceptions_decorator(job_func):
|
18
|
+
@functools.wraps(job_func)
|
19
|
+
def wrapper(*args, **kwargs):
|
20
|
+
try:
|
21
|
+
return job_func(*args, **kwargs)
|
22
|
+
except Exception as err:
|
23
|
+
print(err)
|
24
|
+
|
25
|
+
if cancel_on_failure:
|
26
|
+
return schedule.CancelJob
|
27
|
+
|
28
|
+
return wrapper
|
29
|
+
|
30
|
+
return catch_exceptions_decorator
|
31
|
+
|
32
|
+
|
33
|
+
@catch_exceptions(cancel_on_failure=True)
|
34
|
+
def bad_task():
|
35
|
+
return 1 / 0
|
36
|
+
|
37
|
+
|
38
|
+
schedule.every(5).seconds.do(bad_task)
|
39
|
+
|
40
|
+
if __name__ == "__main__":
|
41
|
+
while True:
|
42
|
+
schedule.run_pending()
|
43
|
+
time.sleep(1)
|
44
|
+
if not schedule.get_jobs():
|
45
|
+
break
|