From f614bcd4b65a03a5af39389b7a2ea7e8bf9846ad Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Sat, 25 Jul 2026 10:32:28 +0100 Subject: [PATCH] First commit --- .gitignore | 10 ++ .python-version | 1 + Makefile | 6 + README.md | 0 main.spec | 38 ++++++ playbook/__init__.py | 183 +++++++++++++++++++++++++++++ playbook/premade_steps.py | 40 +++++++ playbook/unittests/__init__.py | 0 playbook/unittests/test_service.py | 14 +++ pyproject.toml | 11 ++ taskrunner.py | 53 +++++++++ 11 files changed, 356 insertions(+) create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 Makefile create mode 100644 README.md create mode 100644 main.spec create mode 100644 playbook/__init__.py create mode 100644 playbook/premade_steps.py create mode 100644 playbook/unittests/__init__.py create mode 100644 playbook/unittests/test_service.py create mode 100644 pyproject.toml create mode 100644 taskrunner.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..505a3b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5c965eb --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +.PHONY: run_ut +run_ut: + python -m unittest discover -s . -p 'test_*.py' + +build: + pyinstaller -F taskrunner.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/main.spec b/main.spec new file mode 100644 index 0000000..2ba8dd9 --- /dev/null +++ b/main.spec @@ -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, +) diff --git a/playbook/__init__.py b/playbook/__init__.py new file mode 100644 index 0000000..ea01d17 --- /dev/null +++ b/playbook/__init__.py @@ -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 + + diff --git a/playbook/premade_steps.py b/playbook/premade_steps.py new file mode 100644 index 0000000..6c858f7 --- /dev/null +++ b/playbook/premade_steps.py @@ -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=[] + ) diff --git a/playbook/unittests/__init__.py b/playbook/unittests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/playbook/unittests/test_service.py b/playbook/unittests/test_service.py new file mode 100644 index 0000000..15d610d --- /dev/null +++ b/playbook/unittests/test_service.py @@ -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) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..146e1fc --- /dev/null +++ b/pyproject.toml @@ -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" diff --git a/taskrunner.py b/taskrunner.py new file mode 100644 index 0000000..2df4518 --- /dev/null +++ b/taskrunner.py @@ -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()