initial scaffolding
This commit is contained in:
commit
3299cdb246
|
@ -0,0 +1,2 @@
|
||||||
|
*.pyc
|
||||||
|
.idea
|
|
@ -0,0 +1,40 @@
|
||||||
|
from abc import abstractmethod, ABC
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from typing_extensions import Generic, TypeVar, List, Optional
|
||||||
|
|
||||||
|
TNode = TypeVar('TNode', bound='Node')
|
||||||
|
TFileHandle = TypeVar('TFileHandle')
|
||||||
|
|
||||||
|
|
||||||
|
class Node(ABC, Generic[TFileHandle]):
|
||||||
|
"""A :class:`Node` represents a peer within a :class:`FileSharingNetwork`."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def seed(
|
||||||
|
self,
|
||||||
|
file: Path,
|
||||||
|
handle: Optional[TFileHandle]
|
||||||
|
) -> TFileHandle:
|
||||||
|
"""
|
||||||
|
Makes the current :class:`Node` a seeder for the specified file.
|
||||||
|
|
||||||
|
:param file: path to the file to seed.
|
||||||
|
:param handle: an existing network handle to this file. If none is provided, a new one
|
||||||
|
will be generated.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def leech(self, handle: TFileHandle):
|
||||||
|
"""Makes the current node a leecher for the provided handle."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FileSharingNetwork(Generic[TNode], ABC):
|
||||||
|
"""A :class:`FileSharingNetwork` is a set of :class:`Node`s that share
|
||||||
|
an interest in a given file."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def nodes(self) -> List[TNode]:
|
||||||
|
pass
|
|
@ -0,0 +1,19 @@
|
||||||
|
import random
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Callable, Iterator
|
||||||
|
|
||||||
|
# A Sampler samples without replacement from [0, ..., n].
|
||||||
|
Sampler = Callable[[int], Iterator[int]]
|
||||||
|
|
||||||
|
# A DataGenerator generates files for experiments.
|
||||||
|
DataGenerator = Callable[[], Path]
|
||||||
|
|
||||||
|
|
||||||
|
def sample(n: int) -> Iterator[int]:
|
||||||
|
"""Samples without replacement using a Fisher-Yates shuffle."""
|
||||||
|
p = list(range(0, n))
|
||||||
|
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
|
||||||
|
yield p[i]
|
|
@ -0,0 +1,35 @@
|
||||||
|
from typing_extensions import Generic
|
||||||
|
|
||||||
|
from benchmarks.core.network import FileSharingNetwork, TNode
|
||||||
|
from benchmarks.core.utils import Sampler, DataGenerator
|
||||||
|
|
||||||
|
|
||||||
|
class StaticDisseminationExperiment(Generic[TNode]):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
network: FileSharingNetwork[TNode],
|
||||||
|
seeders: int,
|
||||||
|
sampler: Sampler,
|
||||||
|
generator: DataGenerator
|
||||||
|
):
|
||||||
|
self.network = network
|
||||||
|
self.sampler = sampler
|
||||||
|
self.generate_data = generator
|
||||||
|
self.seeders = seeders
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
sample = self.sampler(len(self.network.nodes))
|
||||||
|
seeder_idx = [next(sample) for _ in range(0, self.seeders)]
|
||||||
|
seeders, leechers = (
|
||||||
|
[self.network.nodes[i] for i in seeder_idx],
|
||||||
|
[self.network.nodes[i] for i in range(0, len(self.network.nodes)) if i not in seeder_idx]
|
||||||
|
)
|
||||||
|
|
||||||
|
data = self.generate_data()
|
||||||
|
handle = None
|
||||||
|
|
||||||
|
for node in seeders:
|
||||||
|
handle = node.seed(data, handle)
|
||||||
|
|
||||||
|
for node in leechers:
|
||||||
|
node.leech(handle)
|
|
@ -0,0 +1,87 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
from benchmarks.core.network import FileSharingNetwork, TFileHandle, TNode, Node
|
||||||
|
from benchmarks.core.utils import Sampler
|
||||||
|
from benchmarks.experiments.static_experiment import StaticDisseminationExperiment
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MockHandle:
|
||||||
|
path: Path
|
||||||
|
|
||||||
|
|
||||||
|
def mock_sampler(elements: List[int]) -> Sampler:
|
||||||
|
return lambda _: iter(elements)
|
||||||
|
|
||||||
|
|
||||||
|
class MockNode(Node[MockHandle]):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.seeding: Optional[MockNode] = None
|
||||||
|
self.leeching: Optional[MockHandle] = None
|
||||||
|
|
||||||
|
def seed(self, path: Path, handle: Optional[MockHandle] = None) -> MockHandle:
|
||||||
|
self.seeding = path
|
||||||
|
return MockHandle(path)
|
||||||
|
|
||||||
|
def leech(self, handle: MockHandle):
|
||||||
|
self.leeching = handle
|
||||||
|
|
||||||
|
|
||||||
|
class MockFileSharingNetwork(FileSharingNetwork[MockNode]):
|
||||||
|
|
||||||
|
def __init__(self, n: int):
|
||||||
|
self._nodes = [MockNode() for _ in range(n)]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nodes(self) -> List[TNode]:
|
||||||
|
return self._nodes
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_place_seeders():
|
||||||
|
network = MockFileSharingNetwork(n=13)
|
||||||
|
file = Path('/path/to/data')
|
||||||
|
seeder_indexes = [9, 6, 3]
|
||||||
|
|
||||||
|
experiment = StaticDisseminationExperiment(
|
||||||
|
seeders=3,
|
||||||
|
sampler=mock_sampler(seeder_indexes),
|
||||||
|
network=network,
|
||||||
|
generator=lambda: Path('/path/to/data'),
|
||||||
|
)
|
||||||
|
|
||||||
|
experiment.run()
|
||||||
|
|
||||||
|
actual_seeders = set()
|
||||||
|
for index, node in enumerate(network.nodes):
|
||||||
|
if node.seeding is not None:
|
||||||
|
actual_seeders.add(index)
|
||||||
|
assert node.seeding == file
|
||||||
|
|
||||||
|
assert actual_seeders == set(seeder_indexes)
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_place_leechers():
|
||||||
|
network = MockFileSharingNetwork(n=13)
|
||||||
|
file = Path('/path/to/data')
|
||||||
|
seeder_indexes = [9, 6, 3]
|
||||||
|
|
||||||
|
experiment = StaticDisseminationExperiment(
|
||||||
|
seeders=3,
|
||||||
|
sampler=mock_sampler(seeder_indexes),
|
||||||
|
network=network,
|
||||||
|
generator=lambda: Path('/path/to/data'),
|
||||||
|
)
|
||||||
|
|
||||||
|
experiment.run()
|
||||||
|
|
||||||
|
actual_leechers = set()
|
||||||
|
for index, node in enumerate(network.nodes):
|
||||||
|
if node.leeching is not None:
|
||||||
|
assert node.leeching.path == file
|
||||||
|
assert node.seeding is None
|
||||||
|
actual_leechers.add(index)
|
||||||
|
|
||||||
|
assert actual_leechers == set(range(13)) - set(seeder_indexes)
|
|
@ -0,0 +1,159 @@
|
||||||
|
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
description = "Cross-platform colored terminal text."
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
|
files = [
|
||||||
|
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||||
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deluge-client"
|
||||||
|
version = "1.10.2"
|
||||||
|
description = "Simple Deluge Client"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "deluge-client-1.10.2.tar.gz", hash = "sha256:3881aee3c4e0ca9dd8a56b710047b837e1d087a83e421636a074771f92a9f1b5"},
|
||||||
|
{file = "deluge_client-1.10.2-py3-none-any.whl", hash = "sha256:ea052663c0ed3a2247517d316c3784d70c83279dd9e088623fd06b164768f4f1"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.0.0"
|
||||||
|
description = "brain-dead simple config-ini parsing"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||||
|
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy"
|
||||||
|
version = "1.13.0"
|
||||||
|
description = "Optional static typing for Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"},
|
||||||
|
{file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"},
|
||||||
|
{file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"},
|
||||||
|
{file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"},
|
||||||
|
{file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"},
|
||||||
|
{file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"},
|
||||||
|
{file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"},
|
||||||
|
{file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"},
|
||||||
|
{file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"},
|
||||||
|
{file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"},
|
||||||
|
{file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"},
|
||||||
|
{file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"},
|
||||||
|
{file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"},
|
||||||
|
{file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"},
|
||||||
|
{file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"},
|
||||||
|
{file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"},
|
||||||
|
{file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"},
|
||||||
|
{file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"},
|
||||||
|
{file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"},
|
||||||
|
{file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"},
|
||||||
|
{file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"},
|
||||||
|
{file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"},
|
||||||
|
{file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"},
|
||||||
|
{file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"},
|
||||||
|
{file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"},
|
||||||
|
{file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"},
|
||||||
|
{file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"},
|
||||||
|
{file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"},
|
||||||
|
{file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"},
|
||||||
|
{file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"},
|
||||||
|
{file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"},
|
||||||
|
{file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
mypy-extensions = ">=1.0.0"
|
||||||
|
typing-extensions = ">=4.6.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dmypy = ["psutil (>=4.0)"]
|
||||||
|
faster-cache = ["orjson"]
|
||||||
|
install-types = ["pip"]
|
||||||
|
mypyc = ["setuptools (>=50)"]
|
||||||
|
reports = ["lxml"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy-extensions"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Type system extensions for programs checked with the mypy type checker."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
files = [
|
||||||
|
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
||||||
|
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "24.1"
|
||||||
|
description = "Core utilities for Python packages"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
|
||||||
|
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.5.0"
|
||||||
|
description = "plugin and hook calling mechanisms for python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
|
||||||
|
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["pre-commit", "tox"]
|
||||||
|
testing = ["pytest", "pytest-benchmark"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "8.3.3"
|
||||||
|
description = "pytest: simple powerful testing with Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"},
|
||||||
|
{file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||||
|
iniconfig = "*"
|
||||||
|
packaging = "*"
|
||||||
|
pluggy = ">=1.5,<2"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.12.2"
|
||||||
|
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
|
||||||
|
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
lock-version = "2.0"
|
||||||
|
python-versions = "^3.12"
|
||||||
|
content-hash = "4c657d5b89f926722ec65f35124c9ac0e8138a4264be1860490ab5eaa8a1eb44"
|
|
@ -0,0 +1,20 @@
|
||||||
|
[tool.poetry]
|
||||||
|
name = "bittorrent-benchmarks"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Harness for benchmarking Codex against BitTorrent."
|
||||||
|
authors = ["Your Name <you@example.com>"]
|
||||||
|
license = "MIT"
|
||||||
|
readme = "README.md"
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.12"
|
||||||
|
deluge-client = "^1.10.2"
|
||||||
|
|
||||||
|
|
||||||
|
[tool.poetry.group.test.dependencies]
|
||||||
|
pytest = "^8.3.3"
|
||||||
|
mypy = "^1.13.0"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
Loading…
Reference in New Issue