1## base_action.py -*- mode: python -*-
2"""
3Actions for task control flow.
4ie: Early exit from a task if a file exists
5"""
6# Imports:
7from __future__ import annotations
8
9# ##-- stdlib imports
10# import abc
11import datetime
12# import enum
13import functools as ftz
14import itertools as itz
15import logging as logmod
16import pathlib as pl
17import re
18import shutil
19import time
20import types
21from time import sleep
22from os import environ
23
24# ##-- end stdlib imports
25
26# ##-- 3rd party imports
27import sh
28from jgdv import Proto, Mixin
29from jgdv.structs.strang import CodeReference
30
31# ##-- end 3rd party imports
32
33# ##-- 1st party imports
34import doot
35from doot.workflow.actions import DootBaseAction
36from doot.errors import TaskError, TaskFailed
37from doot.mixins.path_manip import PathManip_m
38from doot.util.dkey import DKey, DKeyed
39from doot.workflow.actions.util.decorators import ControlFlow
40
41# ##-- end 1st party imports
42
43# ##-- types
44# isort: off
45import abc
46import collections.abc
47from typing import TYPE_CHECKING, cast, assert_type, assert_never
48from typing import Generic, NewType
49# Protocols:
50from typing import Protocol, runtime_checkable
51# Typing Decorators:
52from typing import no_type_check, final, override, overload
53
54if TYPE_CHECKING:
55 from jgdv import Maybe
56 from typing import Final
57 from typing import ClassVar, Any, LiteralString
58 from typing import Never, Self, Literal
59 from typing import TypeGuard
60 from collections.abc import Iterable, Iterator, Callable, Generator
61 from collections.abc import Sequence, Mapping, MutableMapping, Hashable
62
63##--|
64
65# isort: on
66# ##-- end types
67
68##-- logging
69logging = logmod.getLogger(__name__)
70##-- end logging
71
[docs]
72class PredicateCheck(DootBaseAction):
73 """
74 Get a predicate using the kwarg `pred`,
75 call it with the action spec and task state.
76 return its result for the task runner to handle
77
78 """
79
80 @DKeyed.references("pred")
81 def __call__(self, spec, state, _pred) -> dict|bool|None:
82 predicate = _pred()
83 return predicate(spec,state)
84
[docs]
85class FileExistsCheck(DootBaseAction):
86 """ Continue only if a file exists. invertable with `not`.
87 converts to a failure instead of skip with fail=true
88 """
89
90 @DKeyed.args
91 @DKeyed.types("not", check=bool, fallback=False)
92 @DKeyed.types("fail", check=bool, fallback=False)
93 def __call__(self, spec, state, args, _invert, _fail) -> dict|bool|None:
94 fail = self.ActRE.FAIL if _fail else self.ActRE.SKIP
95
96 for arg in args:
97 path = DKey[pl.Path](arg).expand(spec, state, on_fail=None)
98 exists = bool(path and path.exists())
99 if _invert:
100 exists = not exists
101 match exists:
102 case True:
103 continue
104 case False:
105 return fail
106
107 return None
108
[docs]
109class SuffixCheck(DootBaseAction):
110 """ Continue only if args ext is in supplied extensions
111 invertable, failable
112 """
113
114 @DKeyed.args
115 @DKeyed.types("exts", check=list)
116 @DKeyed.types("not", check=bool, fallback=False)
117 @DKeyed.types("fail", check=bool, fallback=False)
118 def __call__(self, spec, state, args, exts, _invert, _fail):
119 result = self.ActRE.SKIP
120 if _fail:
121 result = self.ActRE.FAIL
122
123 for arg in args:
124 path = DKey[pl.Path](arg).expand(spec, state, on_fail=None)
125 match path.suffix in exts, _invert:
126 case False, True:
127 continue
128 case False, False:
129 return result
130 case True, True:
131 return result
132 case True, False:
133 continue
134
[docs]
135@Mixin(PathManip_m, allow_inheritance=True)
136class RelativeCheck(DootBaseAction):
137 """ continue only if paths are relative to a base.
138 invertable. Skips by default, can fail
139 """
140
141 @DKeyed.args
142 @DKeyed.types("bases", check=list)
143 @DKeyed.types("not", check=bool, fallback=False)
144 @DKeyed.types("fail", check=bool, fallback=False)
145 def __call__(self, spec, state, args, _bases, _invert, _fail):
146 result = self.ActRE.SKIP
147 if _fail:
148 result = self.ActRE.SKIP
149
150 roots = self._build_roots(spec, state, _bases)
151 try:
152 for arg in args:
153 path = DKey[pl.Path](arg).expand(spec, state, on_fail=None)
154 match self._get_relative(path, roots), _invert:
155 case None, True:
156 return
157 case None, False:
158 return result
159 case _, True:
160 return result
161 case _, False:
162 return
163 except ValueError:
164 return result
165
[docs]
166class LogAction(DootBaseAction):
167 """ A Basic log/print action """
168
169 @DKeyed.types("level", check=str|int, fallback="user")
170 @DKeyed.formats("msg")
171 @DKeyed.formats("target", fallback="task")
172 @DKeyed.formats("prefix", fallback=None)
173 def __call__(self, spec, state, level, msg, target, prefix):
174 assert(msg is not None), "msg"
175 match level:
176 case int():
177 pass
178 case str():
179 level = logmod._nameToLevel.get(level, 0)
180 doot.report.wf.act(info=prefix, msg=msg, level=level)
181
[docs]
182class StalenessCheck(DootBaseAction):
183 """ Skip the rest of the task if old hasn't been modified since new was modifed """
184
185 @DKeyed.paths("old", "new")
186 def __call__(self, spec, state, old, new) -> dict|bool|None:
187 if new.exists() and (old.stat().st_mtime_ns <= new.stat().st_mtime_ns):
188 return self.ActRE.SKIP
189
[docs]
190class AssertInstalled(DootBaseAction):
191 """
192 Easily check a program can be found and used
193 """
194
195 @DKeyed.args
196 @DKeyed.types("env", fallback=None, check=sh.Command|None)
197 def __call__(self, spec, state, args, env) -> dict|bool|None:
198 env = env or sh
199 failures = []
200 for prog in args:
201 try:
202 getattr(env, prog)
203 except sh.CommandNotFound:
204 failures.append(prog)
205
206 if not bool(failures):
207 return
208
209 logging.exception("Required Programs were not found: %s", ", ".join(failures))
210 return self.ActRE.FAIL
211
[docs]
212class WaitAction(DootBaseAction):
213 """ An action that waits for some amount of time """
214
215 @DKeyed.types("count")
216 def __call__(self, spec, state, count):
217 sleep(count)
218
[docs]
219class TriggerActionGroup(DootBaseAction):
220 """ Trigger a non-standard action group """
221
222 def __call__(self, spec, state):
223 raise NotImplementedError()
224
[docs]
225class EnvVarCheck(DootBaseAction):
226 """ Check the shell environment for the presence of certain args """
227
228 @DKeyed.args
229 @DKeyed.types("not", check=bool, fallback=False)
230 @DKeyed.types("fail", check=bool, fallback=False)
231 def __call__(self, spec, state, _args, _invert, _fail) -> bool:
232 fail = self.ActRE.FAIL if _fail else self.ActRE.SKIP
233 for arg in _args:
234 exists = arg in environ
235 if _invert:
236 exists = not exists
237 match exists:
238 case True:
239 continue
240 case False:
241 doot.report.wf.fail(info="EnvCheck", msg=f"Failed On: {arg}")
242 return fail