ddeutil-workflow 0.0.48__py3-none-any.whl → 0.0.50__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 +8 -1
- ddeutil/workflow/api/routes/logs.py +6 -5
- ddeutil/workflow/conf.py +40 -40
- ddeutil/workflow/exceptions.py +3 -3
- ddeutil/workflow/job.py +132 -76
- ddeutil/workflow/logs.py +145 -81
- ddeutil/workflow/result.py +20 -10
- ddeutil/workflow/reusables.py +3 -3
- ddeutil/workflow/scheduler.py +54 -44
- ddeutil/workflow/stages.py +514 -114
- ddeutil/workflow/utils.py +44 -40
- ddeutil/workflow/workflow.py +125 -112
- {ddeutil_workflow-0.0.48.dist-info → ddeutil_workflow-0.0.50.dist-info}/METADATA +5 -6
- ddeutil_workflow-0.0.50.dist-info/RECORD +31 -0
- ddeutil_workflow-0.0.48.dist-info/RECORD +0 -31
- {ddeutil_workflow-0.0.48.dist-info → ddeutil_workflow-0.0.50.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.48.dist-info → ddeutil_workflow-0.0.50.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.48.dist-info → ddeutil_workflow-0.0.50.dist-info}/top_level.txt +0 -0
ddeutil/workflow/logs.py
CHANGED
@@ -3,8 +3,9 @@
|
|
3
3
|
# Licensed under the MIT License. See LICENSE in the project root for
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
|
-
# [x] Use
|
7
|
-
|
6
|
+
# [x] Use dynamic config
|
7
|
+
# [x] Use fix config for `get_logger`, and Model initialize step.
|
8
|
+
"""A Logs module contain Trace dataclass and Audit Pydantic model.
|
8
9
|
"""
|
9
10
|
from __future__ import annotations
|
10
11
|
|
@@ -19,14 +20,14 @@ from functools import lru_cache
|
|
19
20
|
from inspect import Traceback, currentframe, getframeinfo
|
20
21
|
from pathlib import Path
|
21
22
|
from threading import get_ident
|
22
|
-
from typing import ClassVar, Literal, Optional, Union
|
23
|
+
from typing import ClassVar, Literal, Optional, TypeVar, Union
|
23
24
|
|
24
|
-
from pydantic import BaseModel, Field
|
25
|
+
from pydantic import BaseModel, Field
|
25
26
|
from pydantic.dataclasses import dataclass
|
26
27
|
from pydantic.functional_validators import model_validator
|
27
28
|
from typing_extensions import Self
|
28
29
|
|
29
|
-
from .__types import DictData, DictStr
|
30
|
+
from .__types import DictData, DictStr
|
30
31
|
from .conf import config, dynamic
|
31
32
|
from .utils import cut_id, get_dt_now
|
32
33
|
|
@@ -60,22 +61,6 @@ def get_logger(name: str):
|
|
60
61
|
|
61
62
|
logger = get_logger("ddeutil.workflow")
|
62
63
|
|
63
|
-
__all__: TupleStr = (
|
64
|
-
"FileTraceLog",
|
65
|
-
"SQLiteTraceLog",
|
66
|
-
"TraceData",
|
67
|
-
"TraceMeta",
|
68
|
-
"TraceLog",
|
69
|
-
"get_dt_tznow",
|
70
|
-
"get_logger",
|
71
|
-
"get_trace",
|
72
|
-
"get_trace_obj",
|
73
|
-
"get_audit",
|
74
|
-
"FileAudit",
|
75
|
-
"SQLiteAudit",
|
76
|
-
"Audit",
|
77
|
-
)
|
78
|
-
|
79
64
|
|
80
65
|
def get_dt_tznow() -> datetime: # pragma: no cov
|
81
66
|
"""Return the current datetime object that passing the config timezone.
|
@@ -170,13 +155,39 @@ class TraceData(BaseModel): # pragma: no cov
|
|
170
155
|
|
171
156
|
|
172
157
|
@dataclass(frozen=True)
|
173
|
-
class
|
174
|
-
"""Base Trace
|
158
|
+
class BaseTrace(ABC): # pragma: no cov
|
159
|
+
"""Base Trace dataclass with abstraction class property."""
|
175
160
|
|
176
161
|
run_id: str
|
177
162
|
parent_run_id: Optional[str] = field(default=None)
|
178
163
|
extras: DictData = field(default_factory=dict, compare=False, repr=False)
|
179
164
|
|
165
|
+
@classmethod
|
166
|
+
@abstractmethod
|
167
|
+
def find_traces(
|
168
|
+
cls,
|
169
|
+
path: Path | None = None,
|
170
|
+
extras: Optional[DictData] = None,
|
171
|
+
) -> Iterator[TraceData]: # pragma: no cov
|
172
|
+
raise NotImplementedError(
|
173
|
+
"Trace dataclass should implement `find_traces` class-method."
|
174
|
+
)
|
175
|
+
|
176
|
+
@classmethod
|
177
|
+
@abstractmethod
|
178
|
+
def find_trace_with_id(
|
179
|
+
cls,
|
180
|
+
run_id: str,
|
181
|
+
force_raise: bool = True,
|
182
|
+
*,
|
183
|
+
path: Path | None = None,
|
184
|
+
extras: Optional[DictData] = None,
|
185
|
+
) -> TraceData:
|
186
|
+
raise NotImplementedError(
|
187
|
+
"Trace dataclass should implement `find_trace_with_id` "
|
188
|
+
"class-method."
|
189
|
+
)
|
190
|
+
|
180
191
|
@abstractmethod
|
181
192
|
def writer(self, message: str, is_err: bool = False) -> None:
|
182
193
|
"""Write a trace message after making to target pointer object. The
|
@@ -318,11 +329,11 @@ class BaseTraceLog(ABC): # pragma: no cov
|
|
318
329
|
logger.exception(msg, stacklevel=2)
|
319
330
|
|
320
331
|
|
321
|
-
class
|
322
|
-
"""Trace
|
332
|
+
class FileTrace(BaseTrace): # pragma: no cov
|
333
|
+
"""File Trace dataclass that write file to the local storage."""
|
323
334
|
|
324
335
|
@classmethod
|
325
|
-
def
|
336
|
+
def find_traces(
|
326
337
|
cls,
|
327
338
|
path: Path | None = None,
|
328
339
|
extras: Optional[DictData] = None,
|
@@ -333,13 +344,13 @@ class FileTraceLog(BaseTraceLog): # pragma: no cov
|
|
333
344
|
:param extras: An extra parameter that want to override core config.
|
334
345
|
"""
|
335
346
|
for file in sorted(
|
336
|
-
(path or dynamic("
|
347
|
+
(path or dynamic("trace_path", extras=extras)).glob("./run_id=*"),
|
337
348
|
key=lambda f: f.lstat().st_mtime,
|
338
349
|
):
|
339
350
|
yield TraceData.from_path(file)
|
340
351
|
|
341
352
|
@classmethod
|
342
|
-
def
|
353
|
+
def find_trace_with_id(
|
343
354
|
cls,
|
344
355
|
run_id: str,
|
345
356
|
force_raise: bool = True,
|
@@ -354,7 +365,7 @@ class FileTraceLog(BaseTraceLog): # pragma: no cov
|
|
354
365
|
:param path:
|
355
366
|
:param extras: An extra parameter that want to override core config.
|
356
367
|
"""
|
357
|
-
base_path: Path = path or dynamic("
|
368
|
+
base_path: Path = path or dynamic("trace_path", extras=extras)
|
358
369
|
file: Path = base_path / f"run_id={run_id}"
|
359
370
|
if file.exists():
|
360
371
|
return TraceData.from_path(file)
|
@@ -368,7 +379,7 @@ class FileTraceLog(BaseTraceLog): # pragma: no cov
|
|
368
379
|
@property
|
369
380
|
def pointer(self) -> Path:
|
370
381
|
log_file: Path = (
|
371
|
-
dynamic("
|
382
|
+
dynamic("trace_path", extras=self.extras)
|
372
383
|
/ f"run_id={self.parent_run_id or self.run_id}"
|
373
384
|
)
|
374
385
|
if not log_file.exists():
|
@@ -383,7 +394,7 @@ class FileTraceLog(BaseTraceLog): # pragma: no cov
|
|
383
394
|
"""
|
384
395
|
cut_run_id: str = cut_id(self.run_id)
|
385
396
|
if not self.parent_run_id:
|
386
|
-
return f"{cut_run_id}
|
397
|
+
return f"{cut_run_id}"
|
387
398
|
|
388
399
|
cut_parent_run_id: str = cut_id(self.parent_run_id)
|
389
400
|
return f"{cut_parent_run_id} -> {cut_run_id}"
|
@@ -453,8 +464,8 @@ class FileTraceLog(BaseTraceLog): # pragma: no cov
|
|
453
464
|
await f.write(trace_meta.model_dump_json() + "\n")
|
454
465
|
|
455
466
|
|
456
|
-
class
|
457
|
-
"""Trace
|
467
|
+
class SQLiteTrace(BaseTrace): # pragma: no cov
|
468
|
+
"""SQLite Trace dataclass that write trace log to the SQLite database file."""
|
458
469
|
|
459
470
|
table_name: ClassVar[str] = "audits"
|
460
471
|
schemas: ClassVar[
|
@@ -468,10 +479,21 @@ class SQLiteTraceLog(BaseTraceLog): # pragma: no cov
|
|
468
479
|
"""
|
469
480
|
|
470
481
|
@classmethod
|
471
|
-
def
|
482
|
+
def find_traces(
|
483
|
+
cls,
|
484
|
+
path: Path | None = None,
|
485
|
+
extras: Optional[DictData] = None,
|
486
|
+
) -> Iterator[TraceData]: ...
|
472
487
|
|
473
488
|
@classmethod
|
474
|
-
def
|
489
|
+
def find_trace_with_id(
|
490
|
+
cls,
|
491
|
+
run_id: str,
|
492
|
+
force_raise: bool = True,
|
493
|
+
*,
|
494
|
+
path: Path | None = None,
|
495
|
+
extras: Optional[DictData] = None,
|
496
|
+
) -> TraceData: ...
|
475
497
|
|
476
498
|
def make_message(self, message: str) -> str: ...
|
477
499
|
|
@@ -480,47 +502,34 @@ class SQLiteTraceLog(BaseTraceLog): # pragma: no cov
|
|
480
502
|
def awriter(self, message: str, is_err: bool = False) -> None: ...
|
481
503
|
|
482
504
|
|
483
|
-
|
484
|
-
|
485
|
-
|
505
|
+
Trace = TypeVar("Trace", bound=BaseTrace)
|
506
|
+
TraceModel = Union[
|
507
|
+
FileTrace,
|
508
|
+
SQLiteTrace,
|
486
509
|
]
|
487
510
|
|
488
511
|
|
489
512
|
def get_trace(
|
490
513
|
run_id: str,
|
491
|
-
parent_run_id: str | None = None,
|
492
514
|
*,
|
515
|
+
parent_run_id: str | None = None,
|
493
516
|
extras: Optional[DictData] = None,
|
494
|
-
) ->
|
495
|
-
"""Get dynamic
|
517
|
+
) -> TraceModel: # pragma: no cov
|
518
|
+
"""Get dynamic Trace instance from the core config (it can override by an
|
519
|
+
extras argument) that passing running ID and parent running ID.
|
496
520
|
|
497
|
-
:param run_id: A running ID.
|
498
|
-
:param parent_run_id: A parent running ID.
|
499
|
-
:param extras: An extra parameter that want to override the core
|
521
|
+
:param run_id: (str) A running ID.
|
522
|
+
:param parent_run_id: (str) A parent running ID.
|
523
|
+
:param extras: (DictData) An extra parameter that want to override the core
|
524
|
+
config values.
|
500
525
|
|
501
526
|
:rtype: TraceLog
|
502
527
|
"""
|
503
|
-
if dynamic("
|
504
|
-
return
|
528
|
+
if dynamic("trace_path", extras=extras).is_file():
|
529
|
+
return SQLiteTrace(
|
505
530
|
run_id, parent_run_id=parent_run_id, extras=(extras or {})
|
506
531
|
)
|
507
|
-
return
|
508
|
-
run_id, parent_run_id=parent_run_id, extras=(extras or {})
|
509
|
-
)
|
510
|
-
|
511
|
-
|
512
|
-
def get_trace_obj(
|
513
|
-
extras: Optional[DictData] = None,
|
514
|
-
) -> type[TraceLog]: # pragma: no cov
|
515
|
-
"""Get trace object.
|
516
|
-
|
517
|
-
:param extras: An extra parameter that want to override the core config.
|
518
|
-
|
519
|
-
:rtype: type[TraceLog]
|
520
|
-
"""
|
521
|
-
if dynamic("log_path", extras=extras).is_file():
|
522
|
-
return SQLiteTraceLog
|
523
|
-
return FileTraceLog
|
532
|
+
return FileTrace(run_id, parent_run_id=parent_run_id, extras=(extras or {}))
|
524
533
|
|
525
534
|
|
526
535
|
class BaseAudit(BaseModel, ABC):
|
@@ -548,15 +557,50 @@ class BaseAudit(BaseModel, ABC):
|
|
548
557
|
execution_time: float = Field(default=0, description="An execution time.")
|
549
558
|
|
550
559
|
@model_validator(mode="after")
|
551
|
-
def __model_action(self
|
560
|
+
def __model_action(self) -> Self:
|
552
561
|
"""Do before the Audit action with WORKFLOW_AUDIT_ENABLE_WRITE env variable.
|
553
562
|
|
554
563
|
:rtype: Self
|
555
564
|
"""
|
556
|
-
if dynamic("enable_write_audit", extras=
|
565
|
+
if dynamic("enable_write_audit", extras=self.extras):
|
557
566
|
self.do_before()
|
558
567
|
return self
|
559
568
|
|
569
|
+
@classmethod
|
570
|
+
@abstractmethod
|
571
|
+
def is_pointed(
|
572
|
+
cls,
|
573
|
+
name: str,
|
574
|
+
release: datetime,
|
575
|
+
*,
|
576
|
+
extras: Optional[DictData] = None,
|
577
|
+
) -> bool:
|
578
|
+
raise NotImplementedError(
|
579
|
+
"Audit should implement `is_pointed` class-method"
|
580
|
+
)
|
581
|
+
|
582
|
+
@classmethod
|
583
|
+
@abstractmethod
|
584
|
+
def find_audits(
|
585
|
+
cls, name: str, *, extras: Optional[DictData] = None
|
586
|
+
) -> Iterator[Self]:
|
587
|
+
raise NotImplementedError(
|
588
|
+
"Audit should implement `find_audits` class-method"
|
589
|
+
)
|
590
|
+
|
591
|
+
@classmethod
|
592
|
+
@abstractmethod
|
593
|
+
def find_audit_with_release(
|
594
|
+
cls,
|
595
|
+
name: str,
|
596
|
+
release: datetime | None = None,
|
597
|
+
*,
|
598
|
+
extras: Optional[DictData] = None,
|
599
|
+
) -> Self:
|
600
|
+
raise NotImplementedError(
|
601
|
+
"Audit should implement `find_audit_with_release` class-method"
|
602
|
+
)
|
603
|
+
|
560
604
|
def do_before(self) -> None: # pragma: no cov
|
561
605
|
"""To something before end up of initial log model."""
|
562
606
|
|
@@ -688,7 +732,11 @@ class FileAudit(BaseAudit):
|
|
688
732
|
|
689
733
|
:rtype: Self
|
690
734
|
"""
|
691
|
-
trace:
|
735
|
+
trace: Trace = get_trace(
|
736
|
+
self.run_id,
|
737
|
+
parent_run_id=self.parent_run_id,
|
738
|
+
extras=self.extras,
|
739
|
+
)
|
692
740
|
|
693
741
|
# NOTE: Check environ variable was set for real writing.
|
694
742
|
if not dynamic("enable_write_audit", extras=self.extras):
|
@@ -726,11 +774,38 @@ class SQLiteAudit(BaseAudit): # pragma: no cov
|
|
726
774
|
primary key ( run_id )
|
727
775
|
"""
|
728
776
|
|
777
|
+
@classmethod
|
778
|
+
def is_pointed(
|
779
|
+
cls,
|
780
|
+
name: str,
|
781
|
+
release: datetime,
|
782
|
+
*,
|
783
|
+
extras: Optional[DictData] = None,
|
784
|
+
) -> bool: ...
|
785
|
+
|
786
|
+
@classmethod
|
787
|
+
def find_audits(
|
788
|
+
cls, name: str, *, extras: Optional[DictData] = None
|
789
|
+
) -> Iterator[Self]: ...
|
790
|
+
|
791
|
+
@classmethod
|
792
|
+
def find_audit_with_release(
|
793
|
+
cls,
|
794
|
+
name: str,
|
795
|
+
release: datetime | None = None,
|
796
|
+
*,
|
797
|
+
extras: Optional[DictData] = None,
|
798
|
+
) -> Self: ...
|
799
|
+
|
729
800
|
def save(self, excluded: list[str] | None) -> SQLiteAudit:
|
730
801
|
"""Save logging data that receive a context data from a workflow
|
731
802
|
execution result.
|
732
803
|
"""
|
733
|
-
trace:
|
804
|
+
trace: Trace = get_trace(
|
805
|
+
self.run_id,
|
806
|
+
parent_run_id=self.parent_run_id,
|
807
|
+
extras=self.extras,
|
808
|
+
)
|
734
809
|
|
735
810
|
# NOTE: Check environ variable was set for real writing.
|
736
811
|
if not dynamic("enable_write_audit", extras=self.extras):
|
@@ -740,19 +815,8 @@ class SQLiteAudit(BaseAudit): # pragma: no cov
|
|
740
815
|
raise NotImplementedError("SQLiteAudit does not implement yet.")
|
741
816
|
|
742
817
|
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
def save(self, excluded: list[str] | None) -> RemoteFileAudit: ...
|
747
|
-
|
748
|
-
|
749
|
-
class RedisAudit(BaseAudit): # pragma: no cov
|
750
|
-
"""Redis Audit Pydantic Model."""
|
751
|
-
|
752
|
-
def save(self, excluded: list[str] | None) -> RedisAudit: ...
|
753
|
-
|
754
|
-
|
755
|
-
Audit = Union[
|
818
|
+
Audit = TypeVar("Audit", bound=BaseAudit)
|
819
|
+
AuditModel = Union[
|
756
820
|
FileAudit,
|
757
821
|
SQLiteAudit,
|
758
822
|
]
|
@@ -760,7 +824,7 @@ Audit = Union[
|
|
760
824
|
|
761
825
|
def get_audit(
|
762
826
|
extras: Optional[DictData] = None,
|
763
|
-
) -> type[
|
827
|
+
) -> type[AuditModel]: # pragma: no cov
|
764
828
|
"""Get an audit class that dynamic base on the config audit path value.
|
765
829
|
|
766
830
|
:param extras: An extra parameter that want to override the core config.
|
ddeutil/workflow/result.py
CHANGED
@@ -21,7 +21,8 @@ from typing_extensions import Self
|
|
21
21
|
|
22
22
|
from .__types import DictData
|
23
23
|
from .conf import dynamic
|
24
|
-
from .
|
24
|
+
from .exceptions import ResultException
|
25
|
+
from .logs import Trace, get_dt_tznow, get_trace
|
25
26
|
from .utils import default_gen_id, gen_id, get_dt_now
|
26
27
|
|
27
28
|
|
@@ -34,12 +35,14 @@ class Status(IntEnum):
|
|
34
35
|
FAILED: int = 1
|
35
36
|
WAIT: int = 2
|
36
37
|
SKIP: int = 3
|
38
|
+
CANCEL: int = 4
|
37
39
|
|
38
40
|
|
39
41
|
SUCCESS = Status.SUCCESS
|
40
42
|
FAILED = Status.FAILED
|
41
43
|
WAIT = Status.WAIT
|
42
44
|
SKIP = Status.SKIP
|
45
|
+
CANCEL = Status.CANCEL
|
43
46
|
|
44
47
|
|
45
48
|
@dataclass(
|
@@ -63,12 +66,11 @@ class Result:
|
|
63
66
|
|
64
67
|
status: Status = field(default=WAIT)
|
65
68
|
context: DictData = field(default_factory=dict)
|
66
|
-
errors: DictData = field(default_factory=dict)
|
67
69
|
run_id: Optional[str] = field(default_factory=default_gen_id)
|
68
70
|
parent_run_id: Optional[str] = field(default=None, compare=False)
|
69
71
|
ts: datetime = field(default_factory=get_dt_tznow, compare=False)
|
70
72
|
|
71
|
-
trace: Optional[
|
73
|
+
trace: Optional[Trace] = field(default=None, compare=False, repr=False)
|
72
74
|
extras: DictData = field(default_factory=dict, compare=False, repr=False)
|
73
75
|
|
74
76
|
@classmethod
|
@@ -113,8 +115,10 @@ class Result:
|
|
113
115
|
:rtype: Self
|
114
116
|
"""
|
115
117
|
if self.trace is None: # pragma: no cov
|
116
|
-
self.trace:
|
117
|
-
self.run_id,
|
118
|
+
self.trace: Trace = get_trace(
|
119
|
+
self.run_id,
|
120
|
+
parent_run_id=self.parent_run_id,
|
121
|
+
extras=self.extras,
|
118
122
|
)
|
119
123
|
return self
|
120
124
|
|
@@ -126,8 +130,8 @@ class Result:
|
|
126
130
|
:rtype: Self
|
127
131
|
"""
|
128
132
|
self.parent_run_id: str = running_id
|
129
|
-
self.trace:
|
130
|
-
self.run_id, running_id, extras=self.extras
|
133
|
+
self.trace: Trace = get_trace(
|
134
|
+
self.run_id, parent_run_id=running_id, extras=self.extras
|
131
135
|
)
|
132
136
|
return self
|
133
137
|
|
@@ -135,7 +139,7 @@ class Result:
|
|
135
139
|
self,
|
136
140
|
status: int | Status,
|
137
141
|
context: DictData | None = None,
|
138
|
-
|
142
|
+
**kwargs,
|
139
143
|
) -> Self:
|
140
144
|
"""Catch the status and context to this Result object. This method will
|
141
145
|
use between a child execution return a result, and it wants to pass
|
@@ -143,7 +147,6 @@ class Result:
|
|
143
147
|
|
144
148
|
:param status: A status enum object.
|
145
149
|
:param context: A context data that will update to the current context.
|
146
|
-
:param error: An error data that will update to the current errors.
|
147
150
|
|
148
151
|
:rtype: Self
|
149
152
|
"""
|
@@ -151,7 +154,14 @@ class Result:
|
|
151
154
|
Status(status) if isinstance(status, int) else status
|
152
155
|
)
|
153
156
|
self.__dict__["context"].update(context or {})
|
154
|
-
|
157
|
+
if kwargs:
|
158
|
+
for k in kwargs:
|
159
|
+
if k in self.__dict__["context"]:
|
160
|
+
self.__dict__["context"][k].update(kwargs[k])
|
161
|
+
else:
|
162
|
+
raise ResultException(
|
163
|
+
f"The key {k!r} does not exists on context data."
|
164
|
+
)
|
155
165
|
return self
|
156
166
|
|
157
167
|
def alive_time(self) -> float: # pragma: no cov
|
ddeutil/workflow/reusables.py
CHANGED
@@ -91,7 +91,7 @@ def make_filter_registry(
|
|
91
91
|
:rtype: dict[str, FilterRegistry]
|
92
92
|
"""
|
93
93
|
rs: dict[str, FilterRegistry] = {}
|
94
|
-
for module in dynamic("
|
94
|
+
for module in dynamic("registry_filter", f=registers):
|
95
95
|
# NOTE: try to sequential import task functions
|
96
96
|
try:
|
97
97
|
importer = import_module(module)
|
@@ -343,7 +343,7 @@ def param2template(
|
|
343
343
|
:returns: An any getter value from the params input.
|
344
344
|
"""
|
345
345
|
registers: Optional[list[str]] = (
|
346
|
-
extras.get("
|
346
|
+
extras.get("registry_filter") if extras else None
|
347
347
|
)
|
348
348
|
filters: dict[str, FilterRegistry] = filters or make_filter_registry(
|
349
349
|
registers=registers
|
@@ -449,7 +449,7 @@ def make_registry(
|
|
449
449
|
"""
|
450
450
|
rs: dict[str, Registry] = {}
|
451
451
|
regis_calls: list[str] = dynamic(
|
452
|
-
"
|
452
|
+
"registry_caller", f=registries
|
453
453
|
) # pragma: no cov
|
454
454
|
regis_calls.extend(["ddeutil.vendors"])
|
455
455
|
|