From a286dc5e2a71d4a7e08fb2fefbe22b2966bd8d53 Mon Sep 17 00:00:00 2001 From: gmega Date: Wed, 27 Nov 2024 11:22:07 -0300 Subject: [PATCH] simplify module structure, wrap up config, fix bugs --- benchmarks/core/config.py | 46 ++++++++++++ benchmarks/core/experiments/experiment.py | 17 +++++ .../core/experiments/static_experiment.py | 3 +- .../{deluge/config => core/tests}/__init__.py | 0 .../tests/test_config.py} | 10 ++- benchmarks/core/utils.py | 3 +- .../deluge/{config/deluge.py => config.py} | 37 +++++---- benchmarks/deluge/config/host.py | 21 ------ benchmarks/deluge/config/tests/__init__.py | 0 benchmarks/deluge/config/tests/test_deluge.py | 36 --------- benchmarks/deluge/deluge_node.py | 3 +- benchmarks/deluge/tests/test_config.py | 61 +++++++++++++++ experiments.yaml | 2 + poetry.lock | 75 ++++++++++++++++++- pyproject.toml | 2 + 15 files changed, 238 insertions(+), 78 deletions(-) create mode 100644 benchmarks/core/config.py create mode 100644 benchmarks/core/experiments/experiment.py rename benchmarks/{deluge/config => core/tests}/__init__.py (100%) rename benchmarks/{deluge/config/tests/test_host.py => core/tests/test_config.py} (78%) rename benchmarks/deluge/{config/deluge.py => config.py} (52%) delete mode 100644 benchmarks/deluge/config/host.py delete mode 100644 benchmarks/deluge/config/tests/__init__.py delete mode 100644 benchmarks/deluge/config/tests/test_deluge.py create mode 100644 benchmarks/deluge/tests/test_config.py diff --git a/benchmarks/core/config.py b/benchmarks/core/config.py new file mode 100644 index 0000000..5916ca2 --- /dev/null +++ b/benchmarks/core/config.py @@ -0,0 +1,46 @@ +"""Basic utilities for structuring experiment configurations based on Pydantic schemas.""" + +import re +from abc import abstractmethod +from typing import Annotated + +from pydantic import BaseModel, IPvAnyAddress, AfterValidator +from typing_extensions import Generic + +from benchmarks.core.experiments.experiment import TExperiment + + +def drop_config_suffix(name: str) -> str: + return name[:-6] if name.endswith('Config') else name + + +class ConfigModel(BaseModel): + model_config = { + 'alias_generator': drop_config_suffix + } + + +# This is a simple regex which is not by any means exhaustive but should cover gross syntax errors. +VALID_DOMAIN_NAME = re.compile(r"^localhost$|^(?!-)([A-Za-z0-9-]+\.)+[A-Za-z]{2,6}$") + + +def is_valid_domain_name(domain_name: str): + stripped = domain_name.strip() + matches = VALID_DOMAIN_NAME.match(stripped) + assert matches is not None + return stripped + + +DomainName = Annotated[str, AfterValidator(is_valid_domain_name)] + + +class Host(BaseModel): + address: IPvAnyAddress | DomainName + + +class ExperimentBuilder(Generic[TExperiment], ConfigModel): + """:class:`ExperimentBuilders` can build real :class:`Experiment`s out of :class:`ConfigModel`s. """ + + @abstractmethod + def build(self) -> TExperiment: + pass diff --git a/benchmarks/core/experiments/experiment.py b/benchmarks/core/experiments/experiment.py new file mode 100644 index 0000000..2665350 --- /dev/null +++ b/benchmarks/core/experiments/experiment.py @@ -0,0 +1,17 @@ +"""Basic definitions for structuring experiments.""" + +from abc import ABC, abstractmethod + +from mypy.graph_utils import TypeVar + + +class Experiment(ABC): + """An :class:`Experiment` is an arbitrary piece of code that can be run and measured.""" + + @abstractmethod + def run(self): + """Synchronously runs the experiment, blocking the current thread until it's done.""" + pass + + +TExperiment = TypeVar('TExperiment', bound=Experiment) diff --git a/benchmarks/core/experiments/static_experiment.py b/benchmarks/core/experiments/static_experiment.py index ff5bf0e..e286105 100644 --- a/benchmarks/core/experiments/static_experiment.py +++ b/benchmarks/core/experiments/static_experiment.py @@ -1,10 +1,11 @@ from typing_extensions import Generic, List +from benchmarks.core.experiments.experiment import Experiment from benchmarks.core.network import TInitialMetadata, TNetworkHandle, Node from benchmarks.core.utils import ExperimentData -class StaticDisseminationExperiment(Generic[TNetworkHandle, TInitialMetadata]): +class StaticDisseminationExperiment(Generic[TNetworkHandle, TInitialMetadata], Experiment): def __init__( self, network: List[Node[TNetworkHandle, TInitialMetadata]], diff --git a/benchmarks/deluge/config/__init__.py b/benchmarks/core/tests/__init__.py similarity index 100% rename from benchmarks/deluge/config/__init__.py rename to benchmarks/core/tests/__init__.py diff --git a/benchmarks/deluge/config/tests/test_host.py b/benchmarks/core/tests/test_config.py similarity index 78% rename from benchmarks/deluge/config/tests/test_host.py rename to benchmarks/core/tests/test_config.py index 75cee71..23ccd67 100644 --- a/benchmarks/deluge/config/tests/test_host.py +++ b/benchmarks/core/tests/test_config.py @@ -3,7 +3,7 @@ from ipaddress import IPv4Address, IPv6Address import pytest from pydantic import ValidationError -from benchmarks.deluge.config.host import Host, DomainName +from benchmarks.core.config import Host, DomainName def test_should_parse_ipv4_address(): @@ -26,6 +26,14 @@ def test_should_parse_localhost(): assert h.address == DomainName('localhost') +def test_should_return_correct_string_representation_for_addresses(): + h = Host(address='localhost') + assert str(h.address) == 'localhost' + + h = Host(address='192.168.1.1') + assert str(h.address) == '192.168.1.1' + + def test_should_fail_invalid_names(): invalid_names = [ '-node-1.local.svc', diff --git a/benchmarks/core/utils.py b/benchmarks/core/utils.py index ecc3193..ab12f10 100644 --- a/benchmarks/core/utils.py +++ b/benchmarks/core/utils.py @@ -65,7 +65,8 @@ def sample(n: int) -> Iterator[int]: for i in range(n - 1): j = i + random.randint(0, n - i) tmp = p[j] - p[j], p[j + 1] = p[j + 1], tmp + p[j] = p[i] + p[i] = tmp yield p[i] diff --git a/benchmarks/deluge/config/deluge.py b/benchmarks/deluge/config.py similarity index 52% rename from benchmarks/deluge/config/deluge.py rename to benchmarks/deluge/config.py index 7e769d7..2eb5f7a 100644 --- a/benchmarks/deluge/config/deluge.py +++ b/benchmarks/deluge/config.py @@ -2,58 +2,65 @@ from itertools import islice from pathlib import Path from typing import List -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, Field, model_validator, HttpUrl from torrentool.torrent import Torrent +from urllib3.util import parse_url +from benchmarks.core.config import Host, ExperimentBuilder from benchmarks.core.experiments.static_experiment import StaticDisseminationExperiment -from benchmarks.core.utils import sample -from benchmarks.deluge.config.host import Host +from benchmarks.core.utils import sample, RandomTempData from benchmarks.deluge.deluge_node import DelugeMeta, DelugeNode as RealDelugeNode -class DelugeNode(BaseModel): +class DelugeNodeConfig(BaseModel): address: Host daemon_port: int listen_ports: list[int] = Field(min_length=2, max_length=2) -class DelugeNodeSet(BaseModel): +class DelugeNodeSetConfig(BaseModel): network_size: int = Field(gt=2) address: str daemon_port: int listen_ports: list[int] = Field(min_length=2, max_length=2) - nodes: List[DelugeNode] = [] + nodes: List[DelugeNodeConfig] = [] @model_validator(mode='after') def expand_nodes(self): self.nodes = [ - DelugeNode( + DelugeNodeConfig( address=Host(address=self.address.format(node_index=str(i))), daemon_port=self.daemon_port, listen_ports=self.listen_ports, ) for i in range(1, self.network_size + 1) ] + return self -class DelugeExperiment(BaseModel): +DelugeDisseminationExperiment = StaticDisseminationExperiment[Torrent, DelugeMeta] + + +class DelugeExperimentConfig(ExperimentBuilder[DelugeDisseminationExperiment]): file_size: int = Field(gt=0) - repetitions: int = Field(gt=0) seeders: int = Field(gt=0) shared_volume_path: Path - nodes: List[DelugeNode] | DelugeNodeSet + tracker_announce_url: HttpUrl + nodes: List[DelugeNodeConfig] | DelugeNodeSetConfig - def build(self) -> StaticDisseminationExperiment[Torrent, DelugeMeta]: + def build(self) -> DelugeDisseminationExperiment: + nodes = self.nodes.nodes if isinstance(self.nodes, DelugeNodeSetConfig) else self.nodes return StaticDisseminationExperiment( network=[ RealDelugeNode( name=f'deluge-{i}', volume=self.shared_volume_path / f'deluge-{i}', daemon_port=node.daemon_port, - daemon_address=node.address, + daemon_address=str(node.address.address), ) - for i, node in enumerate(self.nodes) + for i, node in enumerate(nodes) ], - seeders=list(islice(sample(len(self.nodes)), self.seeders)), - data=self.data + seeders=list(islice(sample(len(nodes)), self.seeders)), + data=RandomTempData(size=self.file_size, + meta=DelugeMeta('dataset-1', announce_url=parse_url(str(self.tracker_announce_url)))) ) diff --git a/benchmarks/deluge/config/host.py b/benchmarks/deluge/config/host.py deleted file mode 100644 index 1d0021b..0000000 --- a/benchmarks/deluge/config/host.py +++ /dev/null @@ -1,21 +0,0 @@ -import re -from typing import Annotated - -from pydantic import BaseModel, StringConstraints, IPvAnyAddress, AfterValidator - -# This is a simple regex which is not by any means exhaustive but should cover gross syntax errors. -VALID_DOMAIN_NAME = re.compile(r"^localhost$|^(?!-)([A-Za-z0-9-]+\.)+[A-Za-z]{2,6}$") - - -def is_valid_domain_name(domain_name: str): - stripped = domain_name.strip() - matches = VALID_DOMAIN_NAME.match(stripped) - assert matches is not None - return stripped - - -DomainName = Annotated[str, AfterValidator(is_valid_domain_name)] - - -class Host(BaseModel): - address: IPvAnyAddress | DomainName diff --git a/benchmarks/deluge/config/tests/__init__.py b/benchmarks/deluge/config/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/benchmarks/deluge/config/tests/test_deluge.py b/benchmarks/deluge/config/tests/test_deluge.py deleted file mode 100644 index 9310f9d..0000000 --- a/benchmarks/deluge/config/tests/test_deluge.py +++ /dev/null @@ -1,36 +0,0 @@ -from io import StringIO - -from benchmarks.deluge.config.deluge import DelugeNodeSet, DelugeNode -from benchmarks.deluge.config.host import Host - - -def test_should_expand_node_sets_into_simple_nodes(): - nodeset = DelugeNodeSet( - address='deluge-{node_index}.local.svc', - network_size=4, - daemon_port=6080, - listen_ports=[6081, 6082] - ) - - assert nodeset.nodes == [ - DelugeNode( - address=Host(address='deluge-1.local.svc'), - daemon_port=6080, - listen_ports=[6081, 6082], - ), - DelugeNode( - address=Host(address='deluge-2.local.svc'), - daemon_port=6080, - listen_ports=[6081, 6082], - ), - DelugeNode( - address=Host(address='deluge-3.local.svc'), - daemon_port=6080, - listen_ports=[6081, 6082], - ), - DelugeNode( - address=Host(address='deluge-4.local.svc'), - daemon_port=6080, - listen_ports=[6081, 6082], - ), - ] diff --git a/benchmarks/deluge/deluge_node.py b/benchmarks/deluge/deluge_node.py index 1f65924..e996b9a 100644 --- a/benchmarks/deluge/deluge_node.py +++ b/benchmarks/deluge/deluge_node.py @@ -52,8 +52,6 @@ class DelugeNode(SharedFSNode[Torrent, DelugeMeta]): super().__init__(self.downloads_root) - self._init_folders() - def wipe_all_torrents(self): torrent_ids = list(self.rpc.core.get_torrents_status({}, []).keys()) if torrent_ids: @@ -109,6 +107,7 @@ class DelugeNode(SharedFSNode[Torrent, DelugeMeta]): @property def rpc(self) -> DelugeRPCClient: if self._rpc is None: + self._init_folders() self.connect() return self._rpc diff --git a/benchmarks/deluge/tests/test_config.py b/benchmarks/deluge/tests/test_config.py new file mode 100644 index 0000000..8af288d --- /dev/null +++ b/benchmarks/deluge/tests/test_config.py @@ -0,0 +1,61 @@ +from io import StringIO + +import yaml + +from benchmarks.core.config import Host +from benchmarks.deluge.config import DelugeNodeSetConfig, DelugeNodeConfig, DelugeExperimentConfig + + +def test_should_expand_node_sets_into_simple_nodes(): + nodeset = DelugeNodeSetConfig( + address='deluge-{node_index}.local.svc', + network_size=4, + daemon_port=6080, + listen_ports=[6081, 6082] + ) + + assert nodeset.nodes == [ + DelugeNodeConfig( + address=Host(address='deluge-1.local.svc'), + daemon_port=6080, + listen_ports=[6081, 6082], + ), + DelugeNodeConfig( + address=Host(address='deluge-2.local.svc'), + daemon_port=6080, + listen_ports=[6081, 6082], + ), + DelugeNodeConfig( + address=Host(address='deluge-3.local.svc'), + daemon_port=6080, + listen_ports=[6081, 6082], + ), + DelugeNodeConfig( + address=Host(address='deluge-4.local.svc'), + daemon_port=6080, + listen_ports=[6081, 6082], + ), + ] + + +def test_should_build_experiment_from_config(): + config_file = StringIO(""" + deluge_experiment: + seeders: 3 + tracker_announce_url: http://localhost:2020/announce + file_size: 1024 + shared_volume_path: /var/lib/deluge + + nodes: + network_size: 10 + address: 'node-{node_index}.deluge.codexbenchmarks.svc.cluster.local' + daemon_port: 6890 + listen_ports: [ 6891, 6892 ] + """) + + config = DelugeExperimentConfig.model_validate(yaml.safe_load(config_file)['deluge_experiment']) + experiment = config.build() + + assert len(experiment.nodes) == 10 + + diff --git a/experiments.yaml b/experiments.yaml index 101de67..0e7dfab 100644 --- a/experiments.yaml +++ b/experiments.yaml @@ -1,4 +1,6 @@ deluge_experiment: + seeders: ${SEEDERS} + tracker_announce_url: ${TRACKER_ANNOUNCE_URL} file_size: ${FILE_SIZE} repetitions: ${REPETITIONS} shared_volume_path: ${SHARED_VOLUME_PATH} diff --git a/poetry.lock b/poetry.lock index a1ad4d3..7f362dc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -301,6 +301,68 @@ pluggy = ">=1.5,<2" [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + [[package]] name = "torrentool" version = "1.2.0" @@ -315,6 +377,17 @@ files = [ [package.extras] cli = ["click"] +[[package]] +name = "types-pyyaml" +version = "6.0.12.20240917" +description = "Typing stubs for PyYAML" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-PyYAML-6.0.12.20240917.tar.gz", hash = "sha256:d1405a86f9576682234ef83bcb4e6fff7c9305c8b1fbad5e0bcd4f7dbdc9c587"}, + {file = "types_PyYAML-6.0.12.20240917-py3-none-any.whl", hash = "sha256:392b267f1c0fe6022952462bf5d6523f31e37f6cea49b14cee7ad634b6301570"}, +] + [[package]] name = "typing-extensions" version = "4.12.2" @@ -329,4 +402,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "cc71833eb49981fca809cecda62c182668934f3a100ba6165a14ff895600d954" +content-hash = "c10ab6006a3097ae8fcbac02448e98cf18f61146ab311979e1e9d5e735e2369d" diff --git a/pyproject.toml b/pyproject.toml index 2d8fae3..521d08a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,10 +13,12 @@ deluge-client = "^1.10.2" pathvalidate = "^3.2.1" torrentool = "^1.2.0" pydantic = "^2.10.2" +pyyaml = "^6.0.2" [tool.poetry.group.test.dependencies] pytest = "^8.3.3" mypy = "^1.13.0" +types-pyyaml = "^6.0.12.20240917" [tool.mypy] ignore_missing_imports = true