context checker decorator
main / Explore-Gitea-Actions (push) Successful in 17s

This commit is contained in:
2026-07-28 22:52:53 +01:00
parent 9304585ae5
commit c74cc51103
5 changed files with 60 additions and 34 deletions
+10 -4
View File
@@ -17,12 +17,18 @@ steps:
context: context:
serviceName: "gitea-db" serviceName: "gitea-db"
tmpPathPrefix: "/tmp/db_dump.sql" tmpPathPrefix: "/tmp/db_dump.sql"
- name: "backup db to restic repo" - name: "test context"
actions: actions:
dump_database: "backup_data_to_restic_repo" dump_database: "dump_pg_database"
context: context:
sourcePath: "/tmp/db_dump.sql" serviceName: "gitea-db"
serviceTag: "gitea-db" tmpPathPrefix: "/tmp/db_dump.sql"
# - name: "backup db to restic repo"
# actions:
# dump_database: "backup_data_to_restic_repo"
# context:
# sourcePath: "/tmp/db_dump.sql"
# serviceTag: "gitea-db"
# - name: "backup rusty-packages" # - name: "backup rusty-packages"
# actions: # actions:
# create_repo_dir_if_not_exists: "create_dir_if_not_exists" # create_repo_dir_if_not_exists: "create_dir_if_not_exists"
+19 -21
View File
@@ -7,7 +7,7 @@ import json
import importlib.util import importlib.util
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Dict, List, Any, Optional, Tuple from typing import Dict, List, Any, Optional, Tuple
from dataclasses import asdict, dataclass from dataclasses import MISSING, asdict, dataclass
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from playbook.models import Status, StepLog, timed_run from playbook.models import Status, StepLog, timed_run
@@ -30,7 +30,7 @@ class StepIF(ABC):
class CustomStep(StepIF): class CustomStep(StepIF):
""" Setup custom steps. """ """ Setup custom steps. """
def __init__( self, name: str, actions: List[ActionFn], context: Dict[str, Any]): def __init__(self, name: str, actions: List[ActionFn], context: Dict[str, Any]):
self.name: str = name self.name: str = name
self.__actions = actions self.__actions = actions
self.__context = context self.__context = context
@@ -43,9 +43,17 @@ class CustomStep(StepIF):
""" Run the step. """ """ Run the step. """
substeps: List[StepLog] = [] substeps: List[StepLog] = []
status: Status = Status.GOOD status: Status = Status.GOOD
# NOTE:
# Use this to move context from the previous step to the next step
# This is wrong, we are moving away from having a list of steps here to a single
# step for each operation
prev_step_ctx: Dict[str, Any] = {}
for op in self.__actions: for op in self.__actions:
log: StepLog = timed_run(op, ctx | self.__context, op.__name__) log: StepLog = timed_run(op, ctx | prev_step_ctx | self.__context, op.__name__)
status = Status.BAD if log.failed else Status.GOOD status = Status.BAD if log.failed else Status.GOOD
prev_step_ctx = log.pipe_ctx
substeps.append(log) substeps.append(log)
@@ -55,13 +63,15 @@ class CustomStep(StepIF):
errors = [] errors = []
msg: str = "" msg: str = ""
if status == Status.BAD: if status == Status.BAD:
errors = ["substep failed"] # Note, use this to set the standar error output/formatting
# errors = [{"status": "failed", "output": ""}]
errors = []
else: else:
msg = "success" msg = "success"
return StepLog( return StepLog(
step_name=self.name, status=status, step_name=self.name, status=status,
msg=msg, error=errors, substeps=substeps, msg=msg, error=errors, substeps=substeps, pipe_ctx=prev_step_ctx
) )
@@ -70,20 +80,6 @@ class PlaybookError(Exception):
pass pass
class ContextWrapper:
"""Safely extract values with error messages."""
def __init__(self, ctx: Dict[str, Any]):
self._ctx = ctx
def get(self, key: str, default: Any = None) -> Any:
return self._ctx.get(key, default)
def require(self, *keys: str) -> Tuple[List[str], Dict[str, Any]]:
missing = [k for k in keys if not self._ctx.get(k)]
values = {k: self._ctx.get(k) for k in keys if k not in missing}
return missing, values
@dataclass @dataclass
class StepEntry(object): class StepEntry(object):
name: str name: str
@@ -184,8 +180,10 @@ class Play(object):
substeps=[], substeps=[],
) )
prev_ctx: Dict[str, Any] = {}
for s in self._steps: for s in self._steps:
log: StepLog = timed_run(s.step.run, self._context) log: StepLog = timed_run(s.step.run, self._context | prev_ctx)
prev_ctx = log.pipe_ctx
playLog.substeps.append(log) playLog.substeps.append(log)
@@ -217,7 +215,7 @@ class Play(object):
except KeyError as e: except KeyError as e:
raise PlaybookError(f"Step '{name}' is missing required action field: {e}") from e raise PlaybookError(f"Step '{name}' is missing required action field: {e}") from e
return CustomStep(name, functionList, self._context | ctx) return CustomStep(name, functionList, ctx)
# NOTE: Move to factory # NOTE: Move to factory
@classmethod @classmethod
+1 -4
View File
@@ -1,11 +1,8 @@
from dataclasses import dataclass from dataclasses import dataclass
from playbook.models import StepLog from playbook.models import StepLog, ActionFn
from typing import Callable, Dict, Any from typing import Callable, Dict, Any
ActionFn = Callable[[Dict[str, Any], str], StepLog]
@dataclass @dataclass
class Action(object): class Action(object):
name: str name: str
+26 -2
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import time import time
from enum import StrEnum from enum import StrEnum
from typing import List, Dict, Any, Callable from typing import List, Dict, Any, Callable
@@ -33,14 +35,33 @@ class StepLog(object):
return len(self.error) > 0 or self.status == Status.BAD return len(self.error) > 0 or self.status == Status.BAD
@classmethod @classmethod
def ok(cls, name: str, msg: str = "success") -> StepLog: def ok(cls, name: str, msg: str = "success", pipe_ctx: Dict[str, Any] = {}) -> StepLog:
return cls(step_name=name, status=Status.GOOD, msg=msg) return cls(step_name=name, status=Status.GOOD, msg=msg, pipe_ctx=pipe_ctx)
@classmethod @classmethod
def fail(cls, name: str, errors: List[Any]) -> StepLog: def fail(cls, name: str, errors: List[Any]) -> StepLog:
return cls(step_name=name, status=Status.BAD, error=errors) return cls(step_name=name, status=Status.BAD, error=errors)
class ContextChecker:
"""Safely extract values with error messages."""
def __init__(self, ctx: Dict[str, Any]):
self._ctx = ctx
@classmethod
def requires(cls, *args):
def decorator(func: ActionFn):
def wrapper(ctx: Dict[str, Any], name: str) -> StepLog:
if all(key in ctx for key in args):
return func(ctx, name)
else:
missing_keys = [key for key in args if key not in ctx]
missing_keys_msg = f"missing context keys: {missing_keys}"
return StepLog.fail(name, [{"status": "failed", "output": missing_keys_msg}])
return wrapper
return decorator
def human_readable_date(time: int) -> str: def human_readable_date(time: int) -> str:
seconds = time // 1_000_000_000 seconds = time // 1_000_000_000
nanos = time % 1_000_000_000 nanos = time % 1_000_000_000
@@ -68,3 +89,6 @@ def timed_run(op: Callable[..., StepLog], *args: Any, **kwargs: Any) -> StepLog:
log.end_date = human_readable_date(end_date) log.end_date = human_readable_date(end_date)
return log return log
ActionFn = Callable[[Dict[str, Any], str], StepLog]
+4 -3
View File
@@ -1,7 +1,7 @@
# restic actions registry # restic actions registry
from playbook.action_registry import ActionRegistry from playbook.action_registry import ActionRegistry
from playbook.models import StepLog from playbook.models import StepLog
from playbook import ContextWrapper from playbook.models import ContextChecker
from registry.docker_actions import dockeractions from registry.docker_actions import dockeractions
@@ -16,6 +16,7 @@ dbpgactions = ActionRegistry("dbpg_actions_reg")
@dbpgactions.register(name="dump_pg_database", version="") @dbpgactions.register(name="dump_pg_database", version="")
@ContextChecker.requires("database")
def dump_pg_database(ctx, name) -> StepLog: def dump_pg_database(ctx, name) -> StepLog:
c = ContextWrapper(ctx) print(ctx, file=sys.stderr)
return StepLog.ok(name, "") return StepLog.ok(name, "", pipe_ctx={"new_data": "coolData"})