ddeutil-workflow 0.0.48__tar.gz → 0.0.49__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.48 → ddeutil_workflow-0.0.49}/PKG-INFO +3 -5
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/README.md +2 -4
- ddeutil_workflow-0.0.49/src/ddeutil/workflow/__about__.py +1 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/__init__.py +4 -1
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/api/routes/logs.py +6 -5
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/conf.py +31 -31
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/job.py +10 -5
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/logs.py +144 -80
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/result.py +8 -6
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/reusables.py +3 -3
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/scheduler.py +54 -44
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/stages.py +278 -78
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/utils.py +3 -3
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/workflow.py +107 -87
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil_workflow.egg-info/PKG-INFO +3 -5
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_conf.py +2 -2
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_job.py +10 -11
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_logs_audit.py +2 -2
- ddeutil_workflow-0.0.49/tests/test_logs_trace.py +6 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_reusables_template_filter.py +2 -2
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_stage_handler_exec.py +208 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_workflow_exec.py +1 -1
- ddeutil_workflow-0.0.48/src/ddeutil/workflow/__about__.py +0 -1
- ddeutil_workflow-0.0.48/tests/test_logs_trace.py +0 -6
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/LICENSE +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/pyproject.toml +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/setup.cfg +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/__cron.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/__main__.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/__types.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/api/__init__.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/api/api.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/api/log.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/api/repeat.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/api/routes/__init__.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/api/routes/job.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/api/routes/schedules.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/api/routes/workflows.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/cron.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/exceptions.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil/workflow/params.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil_workflow.egg-info/SOURCES.txt +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil_workflow.egg-info/dependency_links.txt +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil_workflow.egg-info/requires.txt +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/src/ddeutil_workflow.egg-info/top_level.txt +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test__cron.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test__regex.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_cron_on.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_job_exec.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_job_exec_strategy.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_job_strategy.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_params.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_release.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_release_queue.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_result.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_reusables_call_tag.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_reusables_template.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_schedule.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_schedule_pending.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_schedule_tasks.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_schedule_workflow.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_scheduler_control.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_stage.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_utils.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_workflow.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_workflow_exec_job.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_workflow_exec_poke.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_workflow_exec_release.py +0 -0
- {ddeutil_workflow-0.0.48 → ddeutil_workflow-0.0.49}/tests/test_workflow_task.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ddeutil-workflow
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.49
|
4
4
|
Summary: Lightweight workflow orchestration
|
5
5
|
Author-email: ddeutils <korawich.anu@gmail.com>
|
6
6
|
License: MIT
|
@@ -262,14 +262,12 @@ it will use default value and do not raise any error to you.
|
|
262
262
|
|
263
263
|
| Name | Component | Default | Description |
|
264
264
|
|:-----------------------------|:---------:|:--------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------|
|
265
|
-
| **ROOT_PATH** | Core | `.` | Root path or the project path for this workflow engine. |
|
266
265
|
| **REGISTRY_CALLER** | Core | `.` | List of importable string for the call stage. |
|
267
266
|
| **REGISTRY_FILTER** | Core | `ddeutil.workflow.templates` | List of importable string for the filter template. |
|
268
|
-
| **CONF_PATH** | Core |
|
267
|
+
| **CONF_PATH** | Core | `./conf` | The config path that keep all template `.yaml` files. |
|
269
268
|
| **TIMEZONE** | Core | `Asia/Bangkok` | A Timezone string value that will pass to `ZoneInfo` object. |
|
270
|
-
| **STAGE_DEFAULT_ID** | Core | `
|
269
|
+
| **STAGE_DEFAULT_ID** | Core | `false` | A flag that enable default stage ID that use for catch an execution output. |
|
271
270
|
| **STAGE_RAISE_ERROR** | Core | `false` | A flag that all stage raise StageException from stage execution. |
|
272
|
-
| **JOB_DEFAULT_ID** | Core | `false` | A flag that enable default job ID that use for catch an execution output. The ID that use will be sequence number. |
|
273
271
|
| **JOB_RAISE_ERROR** | Core | `true` | A flag that all job raise JobException from job strategy execution. |
|
274
272
|
| **MAX_CRON_PER_WORKFLOW** | Core | `5` | |
|
275
273
|
| **MAX_QUEUE_COMPLETE_HIST** | Core | `16` | |
|
@@ -219,14 +219,12 @@ it will use default value and do not raise any error to you.
|
|
219
219
|
|
220
220
|
| Name | Component | Default | Description |
|
221
221
|
|:-----------------------------|:---------:|:--------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------|
|
222
|
-
| **ROOT_PATH** | Core | `.` | Root path or the project path for this workflow engine. |
|
223
222
|
| **REGISTRY_CALLER** | Core | `.` | List of importable string for the call stage. |
|
224
223
|
| **REGISTRY_FILTER** | Core | `ddeutil.workflow.templates` | List of importable string for the filter template. |
|
225
|
-
| **CONF_PATH** | Core |
|
224
|
+
| **CONF_PATH** | Core | `./conf` | The config path that keep all template `.yaml` files. |
|
226
225
|
| **TIMEZONE** | Core | `Asia/Bangkok` | A Timezone string value that will pass to `ZoneInfo` object. |
|
227
|
-
| **STAGE_DEFAULT_ID** | Core | `
|
226
|
+
| **STAGE_DEFAULT_ID** | Core | `false` | A flag that enable default stage ID that use for catch an execution output. |
|
228
227
|
| **STAGE_RAISE_ERROR** | Core | `false` | A flag that all stage raise StageException from stage execution. |
|
229
|
-
| **JOB_DEFAULT_ID** | Core | `false` | A flag that enable default job ID that use for catch an execution output. The ID that use will be sequence number. |
|
230
228
|
| **JOB_RAISE_ERROR** | Core | `true` | A flag that all job raise JobException from job strategy execution. |
|
231
229
|
| **MAX_CRON_PER_WORKFLOW** | Core | `5` | |
|
232
230
|
| **MAX_QUEUE_COMPLETE_HIST** | Core | `16` | |
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__: str = "0.0.49"
|
@@ -10,7 +10,8 @@ from fastapi import APIRouter, Path, Query
|
|
10
10
|
from fastapi import status as st
|
11
11
|
from fastapi.responses import UJSONResponse
|
12
12
|
|
13
|
-
from ...logs import get_audit
|
13
|
+
from ...logs import get_audit
|
14
|
+
from ...result import Result
|
14
15
|
|
15
16
|
log_route = APIRouter(
|
16
17
|
prefix="/logs",
|
@@ -33,6 +34,7 @@ async def get_traces(
|
|
33
34
|
"""Return all trace logs from the current trace log path that config with
|
34
35
|
`WORKFLOW_LOG_PATH` environment variable name.
|
35
36
|
"""
|
37
|
+
result = Result()
|
36
38
|
return {
|
37
39
|
"message": (
|
38
40
|
f"Getting trace logs with offset: {offset} and limit: {limit}"
|
@@ -44,7 +46,7 @@ async def get_traces(
|
|
44
46
|
exclude_unset=True,
|
45
47
|
exclude_defaults=True,
|
46
48
|
)
|
47
|
-
for trace in
|
49
|
+
for trace in result.trace.find_traces()
|
48
50
|
],
|
49
51
|
}
|
50
52
|
|
@@ -63,12 +65,11 @@ async def get_trace_with_id(run_id: str):
|
|
63
65
|
- **run_id**: A running ID that want to search a trace log from the log
|
64
66
|
path.
|
65
67
|
"""
|
68
|
+
result = Result()
|
66
69
|
return {
|
67
70
|
"message": f"Getting trace log with specific running ID: {run_id}",
|
68
71
|
"trace": (
|
69
|
-
|
70
|
-
.find_log_with_id(run_id)
|
71
|
-
.model_dump(
|
72
|
+
result.trace.find_trace_with_id(run_id).model_dump(
|
72
73
|
by_alias=True,
|
73
74
|
exclude_none=True,
|
74
75
|
exclude_unset=True,
|
@@ -11,7 +11,7 @@ from collections.abc import Iterator
|
|
11
11
|
from datetime import timedelta
|
12
12
|
from functools import cached_property
|
13
13
|
from pathlib import Path
|
14
|
-
from typing import Optional, TypeVar
|
14
|
+
from typing import Final, Optional, TypeVar
|
15
15
|
from zoneinfo import ZoneInfo
|
16
16
|
|
17
17
|
from ddeutil.core import str2bool
|
@@ -21,12 +21,15 @@ from ddeutil.io.paths import glob_files, is_ignored, read_ignore
|
|
21
21
|
from .__types import DictData, TupleStr
|
22
22
|
|
23
23
|
T = TypeVar("T")
|
24
|
-
PREFIX: str = "WORKFLOW"
|
24
|
+
PREFIX: Final[str] = "WORKFLOW"
|
25
25
|
|
26
26
|
|
27
27
|
def env(var: str, default: str | None = None) -> str | None: # pragma: no cov
|
28
28
|
"""Get environment variable with uppercase and adding prefix string.
|
29
29
|
|
30
|
+
:param var: (str) A env variable name.
|
31
|
+
:param default: (str | None) A default value if an env var does not set.
|
32
|
+
|
30
33
|
:rtype: str | None
|
31
34
|
"""
|
32
35
|
return os.getenv(f"{PREFIX}_{var.upper().replace(' ', '_')}", default)
|
@@ -51,22 +54,13 @@ class Config: # pragma: no cov
|
|
51
54
|
"""
|
52
55
|
|
53
56
|
# NOTE: Core
|
54
|
-
@property
|
55
|
-
def root_path(self) -> Path:
|
56
|
-
"""Root path or the project path for this workflow engine that use for
|
57
|
-
combine with `conf_path` value.
|
58
|
-
|
59
|
-
:rtype: Path
|
60
|
-
"""
|
61
|
-
return Path(env("CORE_ROOT_PATH", "."))
|
62
|
-
|
63
57
|
@property
|
64
58
|
def conf_path(self) -> Path:
|
65
59
|
"""Config path that keep all workflow template YAML files.
|
66
60
|
|
67
61
|
:rtype: Path
|
68
62
|
"""
|
69
|
-
return
|
63
|
+
return Path(env("CORE_CONF_PATH", "./conf"))
|
70
64
|
|
71
65
|
@property
|
72
66
|
def tz(self) -> ZoneInfo:
|
@@ -78,12 +72,12 @@ class Config: # pragma: no cov
|
|
78
72
|
return ZoneInfo(env("CORE_TIMEZONE", "UTC"))
|
79
73
|
|
80
74
|
@property
|
81
|
-
def
|
75
|
+
def generate_id_simple_mode(self) -> bool:
|
82
76
|
return str2bool(env("CORE_GENERATE_ID_SIMPLE_MODE", "true"))
|
83
77
|
|
84
78
|
# NOTE: Register
|
85
79
|
@property
|
86
|
-
def
|
80
|
+
def registry_caller(self) -> list[str]:
|
87
81
|
"""Register Caller that is a list of importable string for the call
|
88
82
|
stage model can get.
|
89
83
|
|
@@ -93,7 +87,7 @@ class Config: # pragma: no cov
|
|
93
87
|
return [r.strip() for r in regis_call_str.split(",")]
|
94
88
|
|
95
89
|
@property
|
96
|
-
def
|
90
|
+
def registry_filter(self) -> list[str]:
|
97
91
|
"""Register Filter that is a list of importable string for the filter
|
98
92
|
template.
|
99
93
|
|
@@ -106,7 +100,7 @@ class Config: # pragma: no cov
|
|
106
100
|
|
107
101
|
# NOTE: Log
|
108
102
|
@property
|
109
|
-
def
|
103
|
+
def trace_path(self) -> Path:
|
110
104
|
return Path(env("LOG_TRACE_PATH", "./logs"))
|
111
105
|
|
112
106
|
@property
|
@@ -165,11 +159,7 @@ class Config: # pragma: no cov
|
|
165
159
|
return str2bool(env("CORE_JOB_RAISE_ERROR", "true"))
|
166
160
|
|
167
161
|
@property
|
168
|
-
def
|
169
|
-
return str2bool(env("CORE_JOB_DEFAULT_ID", "false"))
|
170
|
-
|
171
|
-
@property
|
172
|
-
def max_on_per_workflow(self) -> int:
|
162
|
+
def max_cron_per_workflow(self) -> int:
|
173
163
|
"""The maximum on value that store in workflow model.
|
174
164
|
|
175
165
|
:rtype: int
|
@@ -305,23 +295,31 @@ class SimLoad:
|
|
305
295
|
)
|
306
296
|
|
307
297
|
@classmethod
|
308
|
-
def is_ignore(
|
298
|
+
def is_ignore(
|
299
|
+
cls,
|
300
|
+
file: Path,
|
301
|
+
conf_path: Path,
|
302
|
+
*,
|
303
|
+
ignore_filename: Optional[str] = None,
|
304
|
+
) -> bool:
|
309
305
|
"""Check this file was ignored.
|
310
306
|
|
311
307
|
:param file: (Path) A file path that want to check.
|
312
308
|
:param conf_path: (Path) A config path that want to read the config
|
313
309
|
ignore file.
|
310
|
+
:param ignore_filename: (str) An ignore filename.
|
314
311
|
|
315
312
|
:rtype: bool
|
316
313
|
"""
|
317
|
-
|
314
|
+
ignore_filename: str = ignore_filename or ".confignore"
|
315
|
+
return is_ignored(file, read_ignore(conf_path / ignore_filename))
|
318
316
|
|
319
317
|
@classmethod
|
320
318
|
def filter_yaml(cls, file: Path, name: str | None = None) -> DictData:
|
321
319
|
"""Read a YAML file context from an input file path and specific name.
|
322
320
|
|
323
|
-
:param file: (Path)
|
324
|
-
:param name: (str)
|
321
|
+
:param file: (Path) A file path that want to extract YAML context.
|
322
|
+
:param name: (str) A key name that search on a YAML context.
|
325
323
|
|
326
324
|
:rtype: DictData
|
327
325
|
"""
|
@@ -374,8 +372,8 @@ def dynamic(
|
|
374
372
|
class Loader(SimLoad):
|
375
373
|
"""Loader Object that get the config `yaml` file from current path.
|
376
374
|
|
377
|
-
:param name: A name of config data that will read by Yaml Loader object.
|
378
|
-
:param externals: An external parameters
|
375
|
+
:param name: (str) A name of config data that will read by Yaml Loader object.
|
376
|
+
:param externals: (DictData) An external parameters
|
379
377
|
"""
|
380
378
|
|
381
379
|
@classmethod
|
@@ -383,17 +381,19 @@ class Loader(SimLoad):
|
|
383
381
|
cls,
|
384
382
|
obj: object,
|
385
383
|
*,
|
384
|
+
path: Path | None = None,
|
386
385
|
included: list[str] | None = None,
|
387
386
|
excluded: list[str] | None = None,
|
388
|
-
path: Path | None = None,
|
389
387
|
**kwargs,
|
390
388
|
) -> Iterator[tuple[str, DictData]]:
|
391
389
|
"""Override the find class method from the Simple Loader object.
|
392
390
|
|
393
391
|
:param obj: An object that want to validate matching before return.
|
394
|
-
:param
|
395
|
-
:param excluded
|
396
|
-
|
392
|
+
:param path: (Path) A override config path.
|
393
|
+
:param included: An excluded list of data key that want to reject this
|
394
|
+
data if any key exist.
|
395
|
+
:param excluded: An included list of data key that want to filter from
|
396
|
+
data.
|
397
397
|
|
398
398
|
:rtype: Iterator[tuple[str, DictData]]
|
399
399
|
"""
|
@@ -483,7 +483,13 @@ class Job(BaseModel):
|
|
483
483
|
except Exception as err:
|
484
484
|
raise JobException(f"{err.__class__.__name__}: {err}") from err
|
485
485
|
|
486
|
-
def set_outputs(
|
486
|
+
def set_outputs(
|
487
|
+
self,
|
488
|
+
output: DictData,
|
489
|
+
to: DictData,
|
490
|
+
*,
|
491
|
+
job_id: Optional[None] = None,
|
492
|
+
) -> DictData:
|
487
493
|
"""Set an outputs from execution process to the received context. The
|
488
494
|
result from execution will pass to value of `strategies` key.
|
489
495
|
|
@@ -511,22 +517,21 @@ class Job(BaseModel):
|
|
511
517
|
|
512
518
|
:param output: An output context.
|
513
519
|
:param to: A context data that want to add output result.
|
520
|
+
:param job_id: A job ID if the id field does not set.
|
514
521
|
|
515
522
|
:rtype: DictData
|
516
523
|
"""
|
517
524
|
if "jobs" not in to:
|
518
525
|
to["jobs"] = {}
|
519
526
|
|
520
|
-
if self.id is None and
|
521
|
-
"job_default_id", extras=self.extras
|
522
|
-
):
|
527
|
+
if self.id is None and job_id is None:
|
523
528
|
raise JobException(
|
524
529
|
"This job do not set the ID before setting execution output."
|
525
530
|
)
|
526
531
|
|
527
532
|
# NOTE: If the job ID did not set, it will use index of jobs key
|
528
533
|
# instead.
|
529
|
-
_id: str = self.id or
|
534
|
+
_id: str = self.id or job_id
|
530
535
|
|
531
536
|
errors: DictData = (
|
532
537
|
{"errors": output.pop("errors", {})} if "errors" in output else {}
|