ddeutil-workflow 0.0.62__py3-none-any.whl → 0.0.64__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.
- ddeutil/workflow/__about__.py +1 -1
- ddeutil/workflow/__init__.py +2 -31
- ddeutil/workflow/api/__init__.py +5 -84
- ddeutil/workflow/api/routes/__init__.py +0 -1
- ddeutil/workflow/api/routes/job.py +0 -1
- ddeutil/workflow/api/routes/logs.py +0 -2
- ddeutil/workflow/api/routes/workflows.py +0 -3
- ddeutil/workflow/conf.py +40 -36
- ddeutil/workflow/event.py +2 -1
- ddeutil/workflow/exceptions.py +0 -3
- ddeutil/workflow/reusables.py +104 -14
- ddeutil/workflow/stages.py +60 -52
- ddeutil/workflow/utils.py +4 -2
- {ddeutil_workflow-0.0.62.dist-info → ddeutil_workflow-0.0.64.dist-info}/METADATA +11 -57
- ddeutil_workflow-0.0.64.dist-info/RECORD +28 -0
- ddeutil/workflow/api/routes/schedules.py +0 -141
- ddeutil/workflow/api/utils.py +0 -174
- ddeutil/workflow/scheduler.py +0 -813
- ddeutil_workflow-0.0.62.dist-info/RECORD +0 -31
- {ddeutil_workflow-0.0.62.dist-info → ddeutil_workflow-0.0.64.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.62.dist-info → ddeutil_workflow-0.0.64.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.62.dist-info → ddeutil_workflow-0.0.64.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.62.dist-info → ddeutil_workflow-0.0.64.dist-info}/top_level.txt +0 -0
ddeutil/workflow/__about__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__: str = "0.0.
|
1
|
+
__version__: str = "0.0.64"
|
ddeutil/workflow/__init__.py
CHANGED
@@ -5,12 +5,7 @@
|
|
5
5
|
# ------------------------------------------------------------------------------
|
6
6
|
from .__cron import CronJob, CronRunner
|
7
7
|
from .__types import DictData, DictStr, Matrix, Re, TupleStr
|
8
|
-
from .conf import
|
9
|
-
Config,
|
10
|
-
FileLoad,
|
11
|
-
config,
|
12
|
-
env,
|
13
|
-
)
|
8
|
+
from .conf import *
|
14
9
|
from .event import *
|
15
10
|
from .exceptions import *
|
16
11
|
from .job import *
|
@@ -37,31 +32,7 @@ from .result import (
|
|
37
32
|
Result,
|
38
33
|
Status,
|
39
34
|
)
|
40
|
-
from .reusables import
|
41
|
-
FILTERS,
|
42
|
-
FilterFunc,
|
43
|
-
FilterRegistry,
|
44
|
-
ReturnTagFunc,
|
45
|
-
TagFunc,
|
46
|
-
custom_filter,
|
47
|
-
extract_call,
|
48
|
-
get_args_const,
|
49
|
-
has_template,
|
50
|
-
make_filter_registry,
|
51
|
-
make_registry,
|
52
|
-
map_post_filter,
|
53
|
-
not_in_template,
|
54
|
-
param2template,
|
55
|
-
str2template,
|
56
|
-
tag,
|
57
|
-
)
|
58
|
-
from .scheduler import (
|
59
|
-
Schedule,
|
60
|
-
ScheduleWorkflow,
|
61
|
-
schedule_control,
|
62
|
-
schedule_runner,
|
63
|
-
schedule_task,
|
64
|
-
)
|
35
|
+
from .reusables import *
|
65
36
|
from .stages import *
|
66
37
|
from .utils import *
|
67
38
|
from .workflow import *
|
ddeutil/workflow/api/__init__.py
CHANGED
@@ -7,8 +7,6 @@ from __future__ import annotations
|
|
7
7
|
|
8
8
|
import contextlib
|
9
9
|
from collections.abc import AsyncIterator
|
10
|
-
from datetime import datetime, timedelta
|
11
|
-
from typing import TypedDict
|
12
10
|
|
13
11
|
from dotenv import load_dotenv
|
14
12
|
from fastapi import FastAPI, Request
|
@@ -20,49 +18,18 @@ from fastapi.middleware.gzip import GZipMiddleware
|
|
20
18
|
from fastapi.responses import UJSONResponse
|
21
19
|
|
22
20
|
from ..__about__ import __version__
|
23
|
-
from ..conf import api_config
|
21
|
+
from ..conf import api_config
|
24
22
|
from ..logs import get_logger
|
25
|
-
from
|
26
|
-
from ..workflow import ReleaseQueue, WorkflowTask
|
27
|
-
from .routes import job, log
|
28
|
-
from .utils import repeat_at
|
23
|
+
from .routes import job, log, workflow
|
29
24
|
|
30
25
|
load_dotenv()
|
31
26
|
logger = get_logger("uvicorn.error")
|
32
27
|
|
33
28
|
|
34
|
-
class State(TypedDict):
|
35
|
-
"""TypeDict for State of FastAPI application."""
|
36
|
-
|
37
|
-
scheduler: list[str]
|
38
|
-
workflow_threads: ReleaseThreads
|
39
|
-
workflow_tasks: list[WorkflowTask]
|
40
|
-
workflow_queue: dict[str, ReleaseQueue]
|
41
|
-
|
42
|
-
|
43
29
|
@contextlib.asynccontextmanager
|
44
|
-
async def lifespan(
|
30
|
+
async def lifespan(_: FastAPI) -> AsyncIterator[dict[str, list]]:
|
45
31
|
"""Lifespan function for the FastAPI application."""
|
46
|
-
|
47
|
-
a.state.workflow_threads = {}
|
48
|
-
a.state.workflow_tasks = []
|
49
|
-
a.state.workflow_queue = {}
|
50
|
-
|
51
|
-
yield {
|
52
|
-
# NOTE: Scheduler value should be contained a key of workflow and
|
53
|
-
# list of datetime of queue and running.
|
54
|
-
#
|
55
|
-
# ... {
|
56
|
-
# ... '<workflow-name>': (
|
57
|
-
# ... [<running-datetime>, ...], [<queue-datetime>, ...]
|
58
|
-
# ... )
|
59
|
-
# ... }
|
60
|
-
#
|
61
|
-
"scheduler": a.state.scheduler,
|
62
|
-
"workflow_queue": a.state.workflow_queue,
|
63
|
-
"workflow_threads": a.state.workflow_threads,
|
64
|
-
"workflow_tasks": a.state.workflow_tasks,
|
65
|
-
}
|
32
|
+
yield {}
|
66
33
|
|
67
34
|
|
68
35
|
app = FastAPI(
|
@@ -99,53 +66,7 @@ async def health():
|
|
99
66
|
# NOTE Add the jobs and logs routes by default.
|
100
67
|
app.include_router(job, prefix=api_config.prefix_path)
|
101
68
|
app.include_router(log, prefix=api_config.prefix_path)
|
102
|
-
|
103
|
-
|
104
|
-
# NOTE: Enable the workflows route.
|
105
|
-
if api_config.enable_route_workflow:
|
106
|
-
from .routes import workflow
|
107
|
-
|
108
|
-
app.include_router(workflow, prefix=api_config.prefix_path)
|
109
|
-
|
110
|
-
|
111
|
-
# NOTE: Enable the schedules route.
|
112
|
-
if api_config.enable_route_schedule:
|
113
|
-
from ..logs import get_audit
|
114
|
-
from ..scheduler import schedule_task
|
115
|
-
from .routes import schedule
|
116
|
-
|
117
|
-
app.include_router(schedule, prefix=api_config.prefix_path)
|
118
|
-
|
119
|
-
@schedule.on_event("startup")
|
120
|
-
@repeat_at(cron="* * * * *", delay=2)
|
121
|
-
def scheduler_listener():
|
122
|
-
"""Schedule broker every minute at 02 second."""
|
123
|
-
logger.debug(
|
124
|
-
f"[SCHEDULER]: Start listening schedule from queue "
|
125
|
-
f"{app.state.scheduler}"
|
126
|
-
)
|
127
|
-
if app.state.workflow_tasks:
|
128
|
-
schedule_task(
|
129
|
-
app.state.workflow_tasks,
|
130
|
-
stop=datetime.now(config.tz) + timedelta(minutes=1),
|
131
|
-
queue=app.state.workflow_queue,
|
132
|
-
threads=app.state.workflow_threads,
|
133
|
-
audit=get_audit(),
|
134
|
-
)
|
135
|
-
|
136
|
-
@schedule.on_event("startup")
|
137
|
-
@repeat_at(cron="*/5 * * * *", delay=10)
|
138
|
-
def monitoring():
|
139
|
-
"""Monitoring workflow thread that running in the background."""
|
140
|
-
logger.debug("[MONITOR]: Start monitoring threading.")
|
141
|
-
snapshot_threads: list[str] = list(app.state.workflow_threads.keys())
|
142
|
-
for t_name in snapshot_threads:
|
143
|
-
|
144
|
-
thread_release: ReleaseThread = app.state.workflow_threads[t_name]
|
145
|
-
|
146
|
-
# NOTE: remove the thread that running success.
|
147
|
-
if not thread_release["thread"].is_alive():
|
148
|
-
app.state.workflow_threads.pop(t_name)
|
69
|
+
app.include_router(workflow, prefix=api_config.prefix_path)
|
149
70
|
|
150
71
|
|
151
72
|
@app.exception_handler(RequestValidationError)
|
@@ -44,7 +44,6 @@ async def get_traces(
|
|
44
44
|
by_alias=True,
|
45
45
|
exclude_none=True,
|
46
46
|
exclude_unset=True,
|
47
|
-
exclude_defaults=True,
|
48
47
|
)
|
49
48
|
for trace in result.trace.find_traces()
|
50
49
|
],
|
@@ -73,7 +72,6 @@ async def get_trace_with_id(run_id: str):
|
|
73
72
|
by_alias=True,
|
74
73
|
exclude_none=True,
|
75
74
|
exclude_unset=True,
|
76
|
-
exclude_defaults=True,
|
77
75
|
)
|
78
76
|
),
|
79
77
|
}
|
@@ -56,7 +56,6 @@ async def get_workflow_by_name(name: str) -> DictData:
|
|
56
56
|
by_alias=True,
|
57
57
|
exclude_none=False,
|
58
58
|
exclude_unset=True,
|
59
|
-
exclude_defaults=True,
|
60
59
|
)
|
61
60
|
|
62
61
|
|
@@ -99,7 +98,6 @@ async def get_workflow_audits(name: str):
|
|
99
98
|
by_alias=True,
|
100
99
|
exclude_none=False,
|
101
100
|
exclude_unset=True,
|
102
|
-
exclude_defaults=True,
|
103
101
|
)
|
104
102
|
for audit in get_audit().find_audits(name=name)
|
105
103
|
],
|
@@ -133,6 +131,5 @@ async def get_workflow_release_audit(name: str, release: str):
|
|
133
131
|
by_alias=True,
|
134
132
|
exclude_none=False,
|
135
133
|
exclude_unset=True,
|
136
|
-
exclude_defaults=True,
|
137
134
|
),
|
138
135
|
}
|
ddeutil/workflow/conf.py
CHANGED
@@ -6,11 +6,9 @@
|
|
6
6
|
from __future__ import annotations
|
7
7
|
|
8
8
|
import copy
|
9
|
-
import json
|
10
9
|
import os
|
11
10
|
from abc import ABC, abstractmethod
|
12
11
|
from collections.abc import Iterator
|
13
|
-
from datetime import timedelta
|
14
12
|
from functools import cached_property
|
15
13
|
from inspect import isclass
|
16
14
|
from pathlib import Path
|
@@ -18,8 +16,9 @@ from typing import Final, Optional, Protocol, TypeVar, Union
|
|
18
16
|
from zoneinfo import ZoneInfo
|
19
17
|
|
20
18
|
from ddeutil.core import str2bool
|
21
|
-
from ddeutil.io import YamlFlResolve
|
19
|
+
from ddeutil.io import YamlFlResolve, search_env_replace
|
22
20
|
from ddeutil.io.paths import glob_files, is_ignored, read_ignore
|
21
|
+
from pydantic import SecretStr, TypeAdapter
|
23
22
|
|
24
23
|
from .__types import DictData
|
25
24
|
|
@@ -162,28 +161,6 @@ class Config: # pragma: no cov
|
|
162
161
|
def max_queue_complete_hist(self) -> int:
|
163
162
|
return int(env("CORE_MAX_QUEUE_COMPLETE_HIST", "16"))
|
164
163
|
|
165
|
-
# NOTE: App
|
166
|
-
@property
|
167
|
-
def max_schedule_process(self) -> int:
|
168
|
-
return int(env("APP_MAX_PROCESS", "2"))
|
169
|
-
|
170
|
-
@property
|
171
|
-
def max_schedule_per_process(self) -> int:
|
172
|
-
return int(env("APP_MAX_SCHEDULE_PER_PROCESS", "100"))
|
173
|
-
|
174
|
-
@property
|
175
|
-
def stop_boundary_delta(self) -> timedelta:
|
176
|
-
stop_boundary_delta_str: str = env(
|
177
|
-
"APP_STOP_BOUNDARY_DELTA", '{"minutes": 5, "seconds": 20}'
|
178
|
-
)
|
179
|
-
try:
|
180
|
-
return timedelta(**json.loads(stop_boundary_delta_str))
|
181
|
-
except Exception as err:
|
182
|
-
raise ValueError(
|
183
|
-
"Config `WORKFLOW_APP_STOP_BOUNDARY_DELTA` can not parsing to"
|
184
|
-
f"timedelta with {stop_boundary_delta_str}."
|
185
|
-
) from err
|
186
|
-
|
187
164
|
|
188
165
|
class APIConfig:
|
189
166
|
"""API Config object."""
|
@@ -192,14 +169,6 @@ class APIConfig:
|
|
192
169
|
def prefix_path(self) -> str:
|
193
170
|
return env("API_PREFIX_PATH", "/api/v1")
|
194
171
|
|
195
|
-
@property
|
196
|
-
def enable_route_workflow(self) -> bool:
|
197
|
-
return str2bool(env("API_ENABLE_ROUTE_WORKFLOW", "true"))
|
198
|
-
|
199
|
-
@property
|
200
|
-
def enable_route_schedule(self) -> bool:
|
201
|
-
return str2bool(env("API_ENABLE_ROUTE_SCHEDULE", "true"))
|
202
|
-
|
203
172
|
|
204
173
|
class BaseLoad(ABC): # pragma: no cov
|
205
174
|
"""Base Load object is the abstraction object for any Load object that
|
@@ -321,15 +290,16 @@ class FileLoad(BaseLoad):
|
|
321
290
|
*,
|
322
291
|
path: Optional[Path] = None,
|
323
292
|
paths: Optional[list[Path]] = None,
|
324
|
-
excluded: list[str]
|
293
|
+
excluded: Optional[list[str]] = None,
|
325
294
|
extras: Optional[DictData] = None,
|
326
295
|
) -> Iterator[tuple[str, DictData]]:
|
327
296
|
"""Find all data that match with object type in config path. This class
|
328
297
|
method can use include and exclude list of identity name for filter and
|
329
298
|
adds-on.
|
330
299
|
|
331
|
-
:param obj: An object that want to validate matching before
|
332
|
-
|
300
|
+
:param obj: (object) An object that want to validate matching before
|
301
|
+
return.
|
302
|
+
:param path: (Path) A config path object.
|
333
303
|
:param paths: (list[Path]) A list of config path object.
|
334
304
|
:param excluded: An included list of data key that want to filter from
|
335
305
|
data.
|
@@ -474,3 +444,37 @@ class Loader(Protocol): # pragma: no cov
|
|
474
444
|
def finds(
|
475
445
|
cls, obj: object, *args, **kwargs
|
476
446
|
) -> Iterator[tuple[str, DictData]]: ...
|
447
|
+
|
448
|
+
|
449
|
+
def pass_env(value: T) -> T: # pragma: no cov
|
450
|
+
"""Passing environment variable to an input value.
|
451
|
+
|
452
|
+
:param value: (Any) A value that want to pass env var searching.
|
453
|
+
|
454
|
+
:rtype: Any
|
455
|
+
"""
|
456
|
+
if isinstance(value, dict):
|
457
|
+
return {k: pass_env(value[k]) for k in value}
|
458
|
+
elif isinstance(value, (list, tuple, set)):
|
459
|
+
return type(value)([pass_env(i) for i in value])
|
460
|
+
if not isinstance(value, str):
|
461
|
+
return value
|
462
|
+
|
463
|
+
rs: str = search_env_replace(value)
|
464
|
+
return None if rs == "null" else rs
|
465
|
+
|
466
|
+
|
467
|
+
class CallerSecret(SecretStr): # pragma: no cov
|
468
|
+
"""Workflow Secret String model."""
|
469
|
+
|
470
|
+
def get_secret_value(self) -> str:
|
471
|
+
"""Override get_secret_value by adding pass_env before return the
|
472
|
+
real-value.
|
473
|
+
|
474
|
+
:rtype: str
|
475
|
+
"""
|
476
|
+
return pass_env(super().get_secret_value())
|
477
|
+
|
478
|
+
|
479
|
+
# NOTE: Define the caller secret type for use it directly in the caller func.
|
480
|
+
CallerSecretType = TypeAdapter(CallerSecret)
|
ddeutil/workflow/event.py
CHANGED
@@ -17,6 +17,7 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
17
17
|
from pydantic import BaseModel, ConfigDict, Field, ValidationInfo
|
18
18
|
from pydantic.functional_serializers import field_serializer
|
19
19
|
from pydantic.functional_validators import field_validator, model_validator
|
20
|
+
from pydantic_extra_types.timezone_name import TimeZoneName
|
20
21
|
from typing_extensions import Self
|
21
22
|
|
22
23
|
from .__cron import WEEKDAYS, CronJob, CronJobYear, CronRunner, Options
|
@@ -92,7 +93,7 @@ class Crontab(BaseModel):
|
|
92
93
|
),
|
93
94
|
]
|
94
95
|
tz: Annotated[
|
95
|
-
|
96
|
+
TimeZoneName,
|
96
97
|
Field(
|
97
98
|
description="A timezone string value",
|
98
99
|
alias="timezone",
|
ddeutil/workflow/exceptions.py
CHANGED
ddeutil/workflow/reusables.py
CHANGED
@@ -14,7 +14,17 @@ from ast import Call, Constant, Expr, Module, Name, parse
|
|
14
14
|
from datetime import datetime
|
15
15
|
from functools import wraps
|
16
16
|
from importlib import import_module
|
17
|
-
from typing import
|
17
|
+
from typing import (
|
18
|
+
Annotated,
|
19
|
+
Any,
|
20
|
+
Callable,
|
21
|
+
Literal,
|
22
|
+
Optional,
|
23
|
+
Protocol,
|
24
|
+
TypeVar,
|
25
|
+
Union,
|
26
|
+
get_type_hints,
|
27
|
+
)
|
18
28
|
|
19
29
|
try:
|
20
30
|
from typing import ParamSpec
|
@@ -23,6 +33,8 @@ except ImportError:
|
|
23
33
|
|
24
34
|
from ddeutil.core import getdot, import_string, lazy
|
25
35
|
from ddeutil.io import search_env_replace
|
36
|
+
from pydantic import BaseModel, ConfigDict, Field, create_model
|
37
|
+
from pydantic.alias_generators import to_pascal
|
26
38
|
from pydantic.dataclasses import dataclass
|
27
39
|
|
28
40
|
from .__types import DictData, Re
|
@@ -111,7 +123,7 @@ def make_filter_registry(
|
|
111
123
|
if not (
|
112
124
|
hasattr(func, "filter")
|
113
125
|
and str(getattr(func, "mark", "NOT SET")) == "filter"
|
114
|
-
):
|
126
|
+
): # pragma: no cov
|
115
127
|
continue
|
116
128
|
|
117
129
|
func: FilterFunc
|
@@ -231,6 +243,7 @@ def not_in_template(value: Any, *, not_in: str = "matrix.") -> bool:
|
|
231
243
|
|
232
244
|
:param value: A value that want to find parameter template prefix.
|
233
245
|
:param not_in: The not-in string that use in the `.startswith` function.
|
246
|
+
(Default is `matrix.`)
|
234
247
|
|
235
248
|
:rtype: bool
|
236
249
|
"""
|
@@ -279,7 +292,7 @@ def str2template(
|
|
279
292
|
:param value: (str) A string value that want to map with params.
|
280
293
|
:param params: (DictData) A parameter value that getting with matched
|
281
294
|
regular expression.
|
282
|
-
:param filters: A mapping of filter registry.
|
295
|
+
:param filters: (dict[str, FilterRegistry]) A mapping of filter registry.
|
283
296
|
:param registers: (Optional[list[str]]) Override list of register.
|
284
297
|
|
285
298
|
:rtype: str
|
@@ -304,12 +317,14 @@ def str2template(
|
|
304
317
|
]
|
305
318
|
|
306
319
|
# NOTE: from validate step, it guarantees that caller exists in params.
|
320
|
+
# I recommend to avoid logging params context on this case because it
|
321
|
+
# can include secret value.
|
307
322
|
try:
|
308
323
|
getter: Any = getdot(caller, params)
|
309
|
-
except ValueError
|
324
|
+
except ValueError:
|
310
325
|
raise UtilException(
|
311
326
|
f"Parameters does not get dot with caller: {caller!r}."
|
312
|
-
) from
|
327
|
+
) from None
|
313
328
|
|
314
329
|
# NOTE:
|
315
330
|
# If type of getter caller is not string type, and it does not use to
|
@@ -341,10 +356,11 @@ def param2template(
|
|
341
356
|
"""Pass param to template string that can search by ``RE_CALLER`` regular
|
342
357
|
expression.
|
343
358
|
|
344
|
-
:param value: A value that want to map with params
|
345
|
-
:param params: A parameter value that getting with matched
|
346
|
-
expression.
|
347
|
-
:param filters: A filter mapping for mapping
|
359
|
+
:param value: (Any) A value that want to map with params.
|
360
|
+
:param params: (DictData) A parameter value that getting with matched
|
361
|
+
regular expression.
|
362
|
+
:param filters: (dict[str, FilterRegistry]) A filter mapping for mapping
|
363
|
+
with `map_post_filter` func.
|
348
364
|
:param extras: (Optional[list[str]]) An Override extras.
|
349
365
|
|
350
366
|
:rtype: Any
|
@@ -376,8 +392,8 @@ def datetime_format(value: datetime, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
|
|
376
392
|
|
377
393
|
Examples:
|
378
394
|
|
379
|
-
|
380
|
-
|
395
|
+
>>> "${{ start-date | fmt('%Y%m%d') }}"
|
396
|
+
>>> "${{ start-date | fmt }}"
|
381
397
|
|
382
398
|
:param value: (datetime) A datetime value that want to format to string
|
383
399
|
value.
|
@@ -399,7 +415,7 @@ def coalesce(value: Optional[T], default: Any) -> T:
|
|
399
415
|
|
400
416
|
Examples:
|
401
417
|
|
402
|
-
|
418
|
+
>>> "${{ value | coalesce('foo') }}"
|
403
419
|
|
404
420
|
:param value: A value that want to check nullable.
|
405
421
|
:param default: A default value that use to returned value if an input
|
@@ -412,7 +428,14 @@ def coalesce(value: Optional[T], default: Any) -> T:
|
|
412
428
|
def get_item(
|
413
429
|
value: DictData, key: Union[str, int], default: Optional[Any] = None
|
414
430
|
) -> Any:
|
415
|
-
"""Get a value with an input specific key.
|
431
|
+
"""Get a value with an input specific key.
|
432
|
+
|
433
|
+
Examples:
|
434
|
+
|
435
|
+
>>> "${{ value | getitem('key') }}"
|
436
|
+
>>> "${{ value | getitem('key', 'default') }}"
|
437
|
+
|
438
|
+
"""
|
416
439
|
if not isinstance(value, dict):
|
417
440
|
raise UtilException(
|
418
441
|
f"The value that pass to `getitem` filter should be `dict` not "
|
@@ -422,7 +445,14 @@ def get_item(
|
|
422
445
|
|
423
446
|
|
424
447
|
@custom_filter("getindex") # pragma: no cov
|
425
|
-
def get_index(value: list[Any], index: int):
|
448
|
+
def get_index(value: list[Any], index: int) -> Any:
|
449
|
+
"""Get a value with an input specific index.
|
450
|
+
|
451
|
+
Examples:
|
452
|
+
|
453
|
+
>>> "${{ value | getindex(1) }}"
|
454
|
+
|
455
|
+
"""
|
426
456
|
if not isinstance(value, list):
|
427
457
|
raise UtilException(
|
428
458
|
f"The value that pass to `getindex` filter should be `list` not "
|
@@ -605,3 +635,63 @@ def extract_call(
|
|
605
635
|
f"`REGISTER.{call.path}.registries.{call.func}`"
|
606
636
|
)
|
607
637
|
return rgt[call.func][call.tag]
|
638
|
+
|
639
|
+
|
640
|
+
class BaseCallerArgs(BaseModel): # pragma: no cov
|
641
|
+
"""Base Caller Args model."""
|
642
|
+
|
643
|
+
model_config = ConfigDict(
|
644
|
+
arbitrary_types_allowed=True,
|
645
|
+
use_enum_values=True,
|
646
|
+
)
|
647
|
+
|
648
|
+
|
649
|
+
def create_model_from_caller(func: Callable) -> BaseModel: # pragma: no cov
|
650
|
+
"""Create model from the caller function. This function will use for
|
651
|
+
validate the caller function argument typed-hint that valid with the args
|
652
|
+
field.
|
653
|
+
|
654
|
+
Reference:
|
655
|
+
- https://github.com/lmmx/pydantic-function-models
|
656
|
+
- https://docs.pydantic.dev/1.10/usage/models/#dynamic-model-creation
|
657
|
+
|
658
|
+
:param func: (Callable) A caller function.
|
659
|
+
|
660
|
+
:rtype: BaseModel
|
661
|
+
"""
|
662
|
+
sig: inspect.Signature = inspect.signature(func)
|
663
|
+
type_hints: dict[str, Any] = get_type_hints(func)
|
664
|
+
fields: dict[str, Any] = {}
|
665
|
+
for name in sig.parameters:
|
666
|
+
param: inspect.Parameter = sig.parameters[name]
|
667
|
+
|
668
|
+
# NOTE: Skip all `*args` and `**kwargs` parameters.
|
669
|
+
if param.kind in (
|
670
|
+
inspect.Parameter.VAR_KEYWORD,
|
671
|
+
inspect.Parameter.VAR_POSITIONAL,
|
672
|
+
):
|
673
|
+
continue
|
674
|
+
|
675
|
+
if name.startswith("_"):
|
676
|
+
kwargs = {"serialization_alias": name}
|
677
|
+
rename: str = name.removeprefix("_")
|
678
|
+
else:
|
679
|
+
kwargs = {}
|
680
|
+
rename: str = name
|
681
|
+
|
682
|
+
if param.default != inspect.Parameter.empty:
|
683
|
+
fields[rename] = Annotated[
|
684
|
+
type_hints[name],
|
685
|
+
Field(default=param.default, **kwargs),
|
686
|
+
]
|
687
|
+
else:
|
688
|
+
fields[rename] = Annotated[
|
689
|
+
type_hints[name],
|
690
|
+
Field(..., **kwargs),
|
691
|
+
]
|
692
|
+
|
693
|
+
return create_model(
|
694
|
+
to_pascal(func.__name__),
|
695
|
+
__base__=BaseCallerArgs,
|
696
|
+
**fields,
|
697
|
+
)
|