First commit

This commit is contained in:
2026-07-25 10:32:28 +01:00
commit f614bcd4b6
11 changed files with 356 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
+1
View File
@@ -0,0 +1 @@
3.14
+6
View File
@@ -0,0 +1,6 @@
.PHONY: run_ut
run_ut:
python -m unittest discover -s . -p 'test_*.py'
build:
pyinstaller -F taskrunner.py
View File
+38
View File
@@ -0,0 +1,38 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='main',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
+183
View File
@@ -0,0 +1,183 @@
from __future__ import annotations
import json
import datetime
from os import error, name
from typing import Callable, Dict, List, Any
from dataclasses import dataclass, field
from abc import ABC, abstractmethod
from enum import Enum, StrEnum
class Status(StrEnum):
GOOD = "GOOD"
BAD = "BAD"
@dataclass
class StepLog(object):
step_name: str
status: Status
duration_sec: int
msg: str
error: List[str] = field(default_factory=list)
substeps: List[StepLog] = field(default_factory=list)
@property
def failed(self) -> bool:
""" Checks if the step failed """
return len(self.error) > 0 or self.status == Status.BAD
# # NOTE: maybe shoulnd't have used an enum
# class StepLogEncoder(json.JSONEncoder):
# """ JSON Encoder for Status Enums. """
# def default(self, o):
# if isinstance(o, Status):
# return o.name
# return super().default(o)
class StepIF(ABC):
""" Interface for plays. """
@abstractmethod
def pre(self) -> StepLog:
""" Run at the start to validate the configurations, inputs, etc. """
pass
@abstractmethod
def play(self) -> StepLog:
""" Steps to actually run the backup. """
@abstractmethod
def post(self) -> StepLog:
""" Run at the end to validate the actions made. """
def run(self, name: str) -> StepLog:
""" Run the step. """
start_time: float = datetime.datetime.now().timestamp()
substeps: List[StepLog] = []
status: Status = Status.GOOD
for op in [self.pre, self.play, self.post]:
log: StepLog = op()
status = Status.BAD if log.failed else Status.GOOD
substeps.append(log)
if status == Status.BAD:
break
end_time: float = datetime.datetime.now().timestamp()
delta: int = int(end_time - start_time)
errors = []
msg: str = ""
if status == Status.BAD:
errors = ["substep failed"]
else:
msg = "success"
return StepLog(
step_name=name, status=status, duration_sec=delta,
msg=msg, error=errors, substeps=substeps,
)
class CustomStep(StepIF):
""" Pre made play to backup directories. """
def __init__(
self,
name: str,
pre_fn: Callable[[Dict[str, Any]], StepLog],
play_fn: Callable[[Dict[str, Any]], StepLog],
post_fn: Callable[[Dict[str, Any]], StepLog],
):
self.name: str = name
self.pre_fn = pre_fn
self.play_fn = play_fn
self.post_fn = post_fn
self.__context: Dict[str, Any] = {}
def pre(self) -> StepLog:
return self.pre_fn(self.__context)
def play(self) -> StepLog:
return self.play_fn(self.__context)
def post(self):
return self.post_fn(self.__context)
@dataclass
class StepEntry(object):
name: str
step: StepIF
class Play(object):
def __init__(self, name : str):
self.name: str = name
self.__steps: List[StepEntry] = []
def add_step(self, name: str, stepRunner : StepIF):
self.__steps.append(StepEntry(name=name, step=stepRunner))
def view_playbook(self) -> str:
steps: List[str] = []
for s in self.__steps:
steps.append(s.name)
data = {
"name": self.name,
"number_of_steps": len(steps),
"steps": steps,
}
return json.dumps(data)
def play(self) -> StepLog:
start_time: float = datetime.datetime.now().timestamp()
playLog: StepLog = StepLog(
step_name=self.name,
status=Status.GOOD,
duration_sec=0,
msg="",
error=[],
substeps=[],
)
for s in self.__steps:
log: StepLog = s.step.run(s.name)
playLog.substeps.append(log)
if log.failed:
playLog.error.append(f"failed in step: {s.name}")
playLog.status = Status.BAD
break
end_time: float = datetime.datetime.now().timestamp()
delta: int = int(end_time - start_time)
playLog.duration_sec = delta
if playLog.status == Status.GOOD:
playLog.msg = "success"
return playLog
class Service(object):
def __init__(self, name: str):
self.name = name
self.__playbook: List[StepIF] = []
@classmethod
def run(cls):
pass
+40
View File
@@ -0,0 +1,40 @@
from Service import StepIF, StepLog, Status
class DirectoryStep(StepIF):
""" Pre made play to backup directories. """
def __init__(self, source : str, destiny : str):
self.name: str = f"backup {source} -> {destiny}"
self.source: str = source
self.destiny: str = destiny
def pre(self) -> StepLog:
return StepLog(
step_name = "pre " + self.name,
status = Status.GOOD,
duration_sec=0,
msg="",
error= [],
substeps=[]
)
def play(self) -> StepLog:
return StepLog(
step_name = "run " + self.name,
status = Status.GOOD,
duration_sec=0,
msg="",
error=[],
substeps=[]
)
def post(self):
return StepLog(
step_name = "post " + self.name,
status = Status.GOOD,
duration_sec=0,
msg="",
error=[],
substeps=[]
)
View File
+14
View File
@@ -0,0 +1,14 @@
import os
import unittest
from Service import Service
CDIR = os.path.dirname(__file__)
class TestService(unittest.TestCase):
def test_service_init(self):
name: str = "jellyfin"
newService: Service = Service(name)
self.assertEqual(newService.name, name)
+11
View File
@@ -0,0 +1,11 @@
[project]
name = "taskrunner"
version = "0.1.0"
description = "Utility to run tasks such as backups"
readme = "README.md"
requires-python = ">=3.14"
dependencies = []
[tool.pyright]
venvPath = "."
venv = ".venv"
+53
View File
@@ -0,0 +1,53 @@
import json
from playbook import StepLog, CustomStep, Play, Status
from dataclasses import asdict
def pre(ctx) -> StepLog:
ctx["a"] = 5
return StepLog(
"pre something",
Status.GOOD,
0,
msg="set 5",
)
def play(ctx):
ctx["a"] = ctx["a"] + 10
return StepLog(
"play something",
Status.BAD,
0,
msg="added 10",
)
def post(ctx):
ctx["a"] = ctx["a"] / 2
return StepLog(
"post something",
Status.GOOD,
0,
msg=f"halved {ctx["a"]}",
)
def main():
newPlay: Play = Play("backup dirs")
newPlay.add_step(
"backup dirs",
CustomStep(
"custom step",
pre, play, post,
)
)
print(newPlay.view_playbook())
print(json.dumps(asdict(newPlay.play()), indent=2))
if __name__ == "__main__":
main()