Added the built int functions registry
Still lots to do tho
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
playbook_name: "test_playbook"
|
||||
steps:
|
||||
- name: "backup dir1"
|
||||
actions:
|
||||
pre: "directory_exists"
|
||||
play: "copy_directory"
|
||||
post: "verify_copy"
|
||||
context:
|
||||
source_dir: "./dir1"
|
||||
destiny_dir: "./dir2"
|
||||
- name: "backup dir2"
|
||||
actions:
|
||||
pre: "directory_exists"
|
||||
play: "copy_directory"
|
||||
post: "verify_copy"
|
||||
context:
|
||||
source_dir: "./dir1"
|
||||
destiny_dir: "./dir2"
|
||||
+104
-49
@@ -1,41 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import yaml
|
||||
import json
|
||||
import time
|
||||
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)
|
||||
from playbook.models import Status, StepLog
|
||||
from playbook.action_registry import ActionFn, ActionRegistry
|
||||
from playbook.premade_steps_registry import registry
|
||||
|
||||
|
||||
class StepIF(ABC):
|
||||
@@ -54,6 +30,19 @@ class StepIF(ABC):
|
||||
def post(self) -> StepLog:
|
||||
""" Run at the end to validate the actions made. """
|
||||
|
||||
@abstractmethod
|
||||
def get_action_names(self) -> Dict[str, str]:
|
||||
""" Return a mapping of action roles (pre, play, post) to function names. """
|
||||
pass
|
||||
|
||||
def __timed_run(self, op) -> StepLog:
|
||||
"""Function that sets the duration_sec field on the log."""
|
||||
start_time: int = time.perf_counter_ns()
|
||||
log: StepLog = op()
|
||||
log.duration_sec = time.perf_counter_ns() - start_time
|
||||
|
||||
return log
|
||||
|
||||
def run(self, name: str) -> StepLog:
|
||||
""" Run the step. """
|
||||
start_time: float = datetime.datetime.now().timestamp()
|
||||
@@ -61,7 +50,7 @@ class StepIF(ABC):
|
||||
substeps: List[StepLog] = []
|
||||
status: Status = Status.GOOD
|
||||
for op in [self.pre, self.play, self.post]:
|
||||
log: StepLog = op()
|
||||
log: StepLog = self.__timed_run(op)
|
||||
status = Status.BAD if log.failed else Status.GOOD
|
||||
|
||||
substeps.append(log)
|
||||
@@ -86,14 +75,15 @@ class StepIF(ABC):
|
||||
|
||||
|
||||
class CustomStep(StepIF):
|
||||
""" Pre made play to backup directories. """
|
||||
""" Setup custom steps. """
|
||||
|
||||
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],
|
||||
pre_fn: ActionFn,
|
||||
play_fn: ActionFn,
|
||||
post_fn: ActionFn,
|
||||
context: Dict[str, Any]
|
||||
):
|
||||
self.name: str = name
|
||||
|
||||
@@ -101,16 +91,28 @@ class CustomStep(StepIF):
|
||||
self.play_fn = play_fn
|
||||
self.post_fn = post_fn
|
||||
|
||||
self.__context: Dict[str, Any] = {}
|
||||
self.__context = context
|
||||
|
||||
def pre(self) -> StepLog:
|
||||
return self.pre_fn(self.__context)
|
||||
return self.pre_fn(self.__context, self.name)
|
||||
|
||||
def play(self) -> StepLog:
|
||||
return self.play_fn(self.__context)
|
||||
return self.play_fn(self.__context, self.name)
|
||||
|
||||
def post(self):
|
||||
return self.post_fn(self.__context)
|
||||
def post(self) -> StepLog:
|
||||
return self.post_fn(self.__context, self.name)
|
||||
|
||||
def get_action_names(self) -> Dict[str, str]:
|
||||
return {
|
||||
"pre": self.pre_fn.__name__,
|
||||
"play": self.pre_fn.__name__,
|
||||
"post": self.pre_fn.__name__,
|
||||
}
|
||||
|
||||
|
||||
class PlaybookError(Exception):
|
||||
""" Base exception for Playbook parsing and execution failures. """
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -123,19 +125,23 @@ class Play(object):
|
||||
def __init__(self, name : str):
|
||||
self.name: str = name
|
||||
self.__steps: List[StepEntry] = []
|
||||
self.__registries: List[ActionRegistry] = [registry]
|
||||
|
||||
def add_step(self, name: str, stepRunner : StepIF):
|
||||
self.__steps.append(StepEntry(name=name, step=stepRunner))
|
||||
|
||||
def view_playbook(self) -> str:
|
||||
steps: List[str] = []
|
||||
steps_info = []
|
||||
for s in self.__steps:
|
||||
steps.append(s.name)
|
||||
steps_info.append({
|
||||
"name": s.name,
|
||||
"actions": s.step.get_action_names()
|
||||
})
|
||||
|
||||
data = {
|
||||
"name": self.name,
|
||||
"number_of_steps": len(steps),
|
||||
"steps": steps,
|
||||
"number_of_steps": len(steps_info),
|
||||
"steps": steps_info,
|
||||
}
|
||||
|
||||
return json.dumps(data)
|
||||
@@ -170,14 +176,63 @@ class Play(object):
|
||||
|
||||
return playLog
|
||||
|
||||
def __get_function_from_registry(self, fn_name) -> ActionFn:
|
||||
for reg in self.__registries:
|
||||
try:
|
||||
return reg.get(fn_name, "*")
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
class Service(object):
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
self.__playbook: List[StepIF] = []
|
||||
raise ValueError(f"no function with name '{fn_name}' was found")
|
||||
|
||||
def __build_custom_step(self, name, actions, ctx) -> CustomStep:
|
||||
try:
|
||||
pre_fn = self.__get_function_from_registry(actions["pre"])
|
||||
play_fn = self.__get_function_from_registry(actions["play"])
|
||||
post_fn = self.__get_function_from_registry(actions["post"])
|
||||
except KeyError as e:
|
||||
raise PlaybookError(f"Step '{name}' is missing required action field: {e}") from e
|
||||
|
||||
return CustomStep(name=name, pre_fn=pre_fn, play_fn=play_fn, post_fn=post_fn, context=ctx)
|
||||
|
||||
@classmethod
|
||||
def run(cls):
|
||||
pass
|
||||
def from_yaml(cls, fp: str) -> Play:
|
||||
""" Loads the playbook from a given YAML file. """
|
||||
try:
|
||||
with open(fp, "r") as f:
|
||||
file_contents = f.read()
|
||||
except FileNotFoundError as e:
|
||||
raise PlaybookError(f"Playbook file not found: {fp}") from e
|
||||
except Exception as e:
|
||||
raise PlaybookError(f"Failed to read file {fp}: {e}") from e
|
||||
|
||||
return cls.__from_yaml_str(file_contents)
|
||||
|
||||
@classmethod
|
||||
def __from_yaml_str(cls, yaml_str: str) -> Play:
|
||||
data = None
|
||||
try:
|
||||
data = yaml.safe_load(yaml_str)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
playbook = cls(data["playbook_name"])
|
||||
|
||||
for idx, step_cfg in enumerate(data.get("steps", [])):
|
||||
step_name = step_cfg.get("name", f"step_{idx}")
|
||||
actions = step_cfg.get("actions", {})
|
||||
context = step_cfg.get("actions", {})
|
||||
|
||||
if not isinstance(actions, dict) or len(actions) != 3:
|
||||
raise PlaybookError(
|
||||
f"Step '{step_name}' (index {idx}) must define an 'actions' map "
|
||||
"containing 'pre', 'play' and 'post'"
|
||||
)
|
||||
|
||||
try:
|
||||
customStep: CustomStep = playbook.__build_custom_step(step_name, actions, context)
|
||||
playbook.add_step(step_cfg["name"], customStep)
|
||||
except Exception as e:
|
||||
raise PlaybookError(f"Error configuring step '{step_name}': {e}") from e
|
||||
|
||||
return playbook
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
from dataclasses import dataclass
|
||||
from playbook.models import StepLog
|
||||
from typing import Callable, Dict, Any
|
||||
|
||||
|
||||
ActionFn = Callable[[Dict[str, Any], str], StepLog]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Action(object):
|
||||
name: str
|
||||
fn: ActionFn
|
||||
ver: str
|
||||
|
||||
|
||||
class ActionRegistry:
|
||||
def __init__(self, name: str):
|
||||
self.name: str = name
|
||||
self.__actions = {}
|
||||
|
||||
def register(self, name: str, version: str):
|
||||
"""Decorator to register python functions with a name and version."""
|
||||
def decorator(function: ActionFn):
|
||||
if name not in self.__actions:
|
||||
self.__actions[name] = {}
|
||||
|
||||
self.__actions[name][version] = function
|
||||
return function
|
||||
|
||||
return decorator
|
||||
|
||||
def get(self, name: str, ver: str) -> ActionFn:
|
||||
if name not in self.__actions:
|
||||
raise ValueError(f"Action '{name}' is not registered in registry '{self.name}'")
|
||||
|
||||
versions_dict = self.__actions[name]
|
||||
if not versions_dict:
|
||||
raise ValueError(f"No registered versions found for function '{name}'")
|
||||
|
||||
if ver == "*":
|
||||
return next(iter(versions_dict.values()))
|
||||
|
||||
if ver not in self.__actions[name]:
|
||||
raise ValueError(f"Version '{ver}' is not registered for function '{name}'")
|
||||
|
||||
return versions_dict[ver]
|
||||
@@ -0,0 +1,13 @@
|
||||
import os
|
||||
import unittest
|
||||
from a import ActionRegistry
|
||||
|
||||
|
||||
CDIR = os.path.dirname(__file__)
|
||||
|
||||
|
||||
class TestActionRegistry(unittest.TestCase):
|
||||
def test_creating_action_registry(self):
|
||||
reg: ActionRegistry = ActionRegistry("test_registry")
|
||||
|
||||
self.assertEqual(reg.name, "test_registry")
|
||||
@@ -0,0 +1,26 @@
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum, StrEnum
|
||||
from typing import Callable, Dict, List, Any
|
||||
|
||||
|
||||
class Status(StrEnum):
|
||||
GOOD = "GOOD"
|
||||
BAD = "BAD"
|
||||
|
||||
|
||||
@dataclass
|
||||
class StepLog(object):
|
||||
step_name: str
|
||||
status: Status = Status.BAD # By default set it to bad so the it fails by default
|
||||
# The user needs to be explicit
|
||||
duration_sec: int = 0
|
||||
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
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
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=[]
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
# In this file we will only put premade steps
|
||||
|
||||
from playbook.models import StepLog, Status
|
||||
from playbook.action_registry import ActionRegistry
|
||||
|
||||
|
||||
# Module wide registry with built in functions
|
||||
registry: ActionRegistry = ActionRegistry("testrunner_registry")
|
||||
|
||||
|
||||
@registry.register(name="directory_exists", version="stat1.0")
|
||||
def directory_exists(ctx, name) -> StepLog:
|
||||
log: StepLog = StepLog(
|
||||
step_name=name,
|
||||
status=Status.GOOD,
|
||||
msg="source directory exists",
|
||||
)
|
||||
return log
|
||||
|
||||
|
||||
@registry.register(name="copy_directory", version="cp1.0")
|
||||
def copy_directory(ctx, name) -> StepLog:
|
||||
log: StepLog = StepLog(
|
||||
step_name=name,
|
||||
status=Status.GOOD,
|
||||
msg="directory was copied/movied/borged/rsynced",
|
||||
)
|
||||
return log
|
||||
|
||||
|
||||
@registry.register(name="verify_copy", version="verify1.0")
|
||||
def verify_copy(ctx, name) -> StepLog:
|
||||
log: StepLog = StepLog(
|
||||
step_name=name,
|
||||
status=Status.GOOD,
|
||||
msg="copied/movied/borged/rsynced exists and is valid",
|
||||
)
|
||||
return log
|
||||
@@ -1,14 +1,5 @@
|
||||
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)
|
||||
|
||||
+3
-1
@@ -4,7 +4,9 @@ version = "0.1.0"
|
||||
description = "Utility to run tasks such as backups"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = []
|
||||
dependencies = [
|
||||
"pyaml>=26.7.0",
|
||||
]
|
||||
|
||||
[tool.pyright]
|
||||
venvPath = "."
|
||||
|
||||
+7
-10
@@ -34,19 +34,16 @@ def post(ctx):
|
||||
|
||||
|
||||
def main():
|
||||
newPlay: Play = Play("backup dirs")
|
||||
|
||||
newPlay.add_step(
|
||||
"backup dirs",
|
||||
CustomStep(
|
||||
"custom step",
|
||||
pre, play, post,
|
||||
)
|
||||
)
|
||||
newPlay: Play
|
||||
try:
|
||||
newPlay = Play.from_yaml("./playbook.yaml")
|
||||
except Exception as e:
|
||||
print(f"Failed to load playbook: {e}")
|
||||
exit(1)
|
||||
|
||||
print(newPlay.view_playbook())
|
||||
|
||||
print(json.dumps(asdict(newPlay.play()), indent=2))
|
||||
# print(json.dumps(asdict(newPlay.play()), indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.14"
|
||||
|
||||
[[package]]
|
||||
name = "pyaml"
|
||||
version = "26.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/15/6a/acfdf17de0d6947b419da8696e02b781b18de2cf49e0472298b50e1f0711/pyaml-26.7.0.tar.gz", hash = "sha256:11cda3a796efc6dbce0d56836be56cfd26289dad07bcd78e9904086729929c93", size = 30669, upload-time = "2026-07-04T00:34:38.532Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/26/3f05f9e3692853dff663913d5d62fb8df3f5640c8d70b06588c0b51ed953/pyaml-26.7.0-py3-none-any.whl", hash = "sha256:cfa382780c43ae660669b87d394d550a41856ef175f5749f51f753e12d7077ac", size = 27226, upload-time = "2026-07-04T00:34:37.198Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "taskrunner"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "pyaml" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "pyaml", specifier = ">=26.7.0" }]
|
||||
Reference in New Issue
Block a user