Source code for doot.workflow.actions.control_flow.control_flow

  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