From 66c813f36b5912f5933d6d2f1ec3d4849dfffc5e Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Mon, 17 Feb 2025 14:21:04 -0800 Subject: [PATCH 01/71] collect runs_on --- software/hil/pytest_plugin.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/software/hil/pytest_plugin.py b/software/hil/pytest_plugin.py index 93679732..85ebc832 100644 --- a/software/hil/pytest_plugin.py +++ b/software/hil/pytest_plugin.py @@ -14,6 +14,7 @@ - The 'record' class is from hil.framework in this same package. """ +from dataclasses import dataclass import logging import socket from datetime import datetime @@ -293,3 +294,25 @@ def machine_config(request: _Request) -> Generator[ConfigDict, None, None]: yield config_obj finally: save_config(config_obj, Path(request.config.rootdir) / configs_path, pet_name) + + +@dataclass +class RunsOn: + hostname: str | None + + def __init__(self, *args, hostname: str | None = None): + self.hostname = hostname + + +runs_on_key = pytest.StashKey[dict[str, list[RunsOn]]]() + + +@pytest.hookimpl(hookwrapper=True) +def pytest_collection_finish(session: pytest.Session): + session.config.stash[runs_on_key] = { + item.nodeid: [ + RunsOn(*m.args, **m.kwargs) for m in item.own_markers if m.name == "runs_on" + ] + for item in session.items + } + yield From 51a9c79145ad951283c9379b8246f0fc40a3f456 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Mon, 17 Feb 2025 14:46:31 -0800 Subject: [PATCH 02/71] HeterogenousLoadScheduling wip --- pyproject.toml | 1 + software/hil/pytest_plugin.py | 19 ++++----- software/hil/test_scheduler.py | 74 ++++++++++++++++++++++++++++++++++ uv.lock | 24 +++++++++++ 4 files changed, 109 insertions(+), 9 deletions(-) create mode 100644 software/hil/test_scheduler.py diff --git a/pyproject.toml b/pyproject.toml index 869e6c2a..03d05af6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ dependencies = [ "pytest-benchmark>=5.1.0", "pytest-html>=4.1.1", "pytest-timeout>=2.3.1", + "pytest-xdist>=3.6.1", "rich>=13.9.4", "smbus2>=0.5.0", ] diff --git a/software/hil/pytest_plugin.py b/software/hil/pytest_plugin.py index 85ebc832..b71a8b83 100644 --- a/software/hil/pytest_plugin.py +++ b/software/hil/pytest_plugin.py @@ -14,7 +14,6 @@ - The 'record' class is from hil.framework in this same package. """ -from dataclasses import dataclass import logging import socket from datetime import datetime @@ -23,12 +22,17 @@ import altair as alt from hil.utils.config import ConfigDict, load_config, save_config +from hil.test_scheduler import HeterogenousLoadScheduling, RunsOn import pathvalidate import polars as pl import pytest from pytest_html import extras as html_extras from .framework import Trace, record as hil_record +from xdist.remote import Producer +from xdist.scheduler.protocol import Scheduling + +from .framework import record as hil_record logger = logging.getLogger(__name__) @@ -296,14 +300,6 @@ def machine_config(request: _Request) -> Generator[ConfigDict, None, None]: save_config(config_obj, Path(request.config.rootdir) / configs_path, pet_name) -@dataclass -class RunsOn: - hostname: str | None - - def __init__(self, *args, hostname: str | None = None): - self.hostname = hostname - - runs_on_key = pytest.StashKey[dict[str, list[RunsOn]]]() @@ -316,3 +312,8 @@ def pytest_collection_finish(session: pytest.Session): for item in session.items } yield + + +@pytest.hookimpl() +def pytest_xdist_make_scheduler(config: pytest.Config, log: Producer) -> Scheduling: + return HeterogenousLoadScheduling(config, log, config.stash[runs_on_key]) diff --git a/software/hil/test_scheduler.py b/software/hil/test_scheduler.py new file mode 100644 index 00000000..1145cc5d --- /dev/null +++ b/software/hil/test_scheduler.py @@ -0,0 +1,74 @@ +from collections.abc import Sequence +from dataclasses import dataclass +import pytest +from xdist.scheduler.load import LoadScheduling +from xdist.workermanage import WorkerController +from xdist.remote import Producer + + +@dataclass +class RunsOn: + hostname: str | None + + def __init__(self, *args, hostname: str | None = None): + self.hostname = hostname + + def check(self, node: WorkerController) -> bool: ... + + +NodeId = str + + +class HeterogenousLoadScheduling(LoadScheduling): + """ + Implement load scheduling across heterogeneous nodes. + + Similar to `LoadScheduling`, except tests are only allocated to compatible worker nodes, as determined by the `runs_on` marker. + """ + + def __init__( + self, + config: pytest.Config, + log: Producer, + runs_on_by_nodeid: dict[NodeId, list[RunsOn]], + ): + self.runs_on_by_nodeid = runs_on_by_nodeid + + @property + def nodes(self) -> list[WorkerController]: ... + + @property + def collection_is_completed(self) -> bool: ... + + @property + def tests_finished(self) -> bool: ... + + @property + def has_pending(self) -> bool: ... + + def add_node(self, node: WorkerController) -> None: ... + + def add_node_collection( + self, + node: WorkerController, + collection: Sequence[str], + ) -> None: ... + + def mark_test_complete( + self, + node: WorkerController, + item_index: int, + duration: float = 0, + ) -> None: ... + + def mark_test_pending(self, item: str) -> None: ... + + def remove_pending_tests_from_node( + self, + node: WorkerController, + indices: Sequence[int], + ) -> None: ... + + def remove_node(self, node: WorkerController) -> str | None: ... + + def schedule(self) -> None: ... diff --git a/uv.lock b/uv.lock index ec780fb3..d151f215 100644 --- a/uv.lock +++ b/uv.lock @@ -53,6 +53,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, ] +[[package]] +name = "execnet" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/ff/b4c0dc78fbe20c3e59c0c7334de0c27eb4001a2b2017999af398bf730817/execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3", size = 166524 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612 }, +] + [[package]] name = "filelock" version = "3.17.0" @@ -76,6 +85,7 @@ dependencies = [ { name = "pytest-benchmark" }, { name = "pytest-html" }, { name = "pytest-timeout" }, + { name = "pytest-xdist" }, { name = "rich" }, { name = "smbus2" }, ] @@ -98,6 +108,7 @@ requires-dist = [ { name = "pytest-benchmark", specifier = ">=5.1.0" }, { name = "pytest-html", specifier = ">=4.1.1" }, { name = "pytest-timeout", specifier = ">=2.3.1" }, + { name = "pytest-xdist", specifier = ">=3.6.1" }, { name = "rich", specifier = ">=13.9.4" }, { name = "smbus2", specifier = ">=0.5.0" }, ] @@ -441,6 +452,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/27/14af9ef8321f5edc7527e47def2a21d8118c6f329a9342cc61387a0c0599/pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e", size = 14148 }, ] +[[package]] +name = "pytest-xdist" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/c4/3c310a19bc1f1e9ef50075582652673ef2bfc8cd62afef9585683821902f/pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d", size = 84060 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/82/1d96bf03ee4c0fdc3c0cbe61470070e659ca78dc0086fb88b66c185e2449/pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7", size = 46108 }, +] + [[package]] name = "pyyaml" version = "6.0.2" From 4de348c176e201586274b6705da7a1890468a611 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Mon, 17 Feb 2025 16:46:49 -0800 Subject: [PATCH 03/71] asyncio_default_fixture_loop_scope=function --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 03d05af6..20b47957 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ packages = ["software/hil"] [tool.pytest.ini_options] asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" testpaths = ["tests"] python_files = ["test_*.py"] addopts = [ From 4271dd67fcda1b799fc27d75a25de6ea83ce779a Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Mon, 17 Feb 2025 16:47:57 -0800 Subject: [PATCH 04/71] collect tests on controller to determine affinity --- software/hil/pytest_plugin.py | 22 ++++++++++++++++------ software/hil/test_scheduler.py | 3 ++- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/software/hil/pytest_plugin.py b/software/hil/pytest_plugin.py index b71a8b83..d550ee60 100644 --- a/software/hil/pytest_plugin.py +++ b/software/hil/pytest_plugin.py @@ -303,17 +303,27 @@ def machine_config(request: _Request) -> Generator[ConfigDict, None, None]: runs_on_key = pytest.StashKey[dict[str, list[RunsOn]]]() -@pytest.hookimpl(hookwrapper=True) -def pytest_collection_finish(session: pytest.Session): +@pytest.hookimpl(tryfirst=True) +def pytest_collection(session: pytest.Session): session.config.stash[runs_on_key] = { item.nodeid: [ RunsOn(*m.args, **m.kwargs) for m in item.own_markers if m.name == "runs_on" ] - for item in session.items + for item in session.perform_collect() } - yield + session._notfound = [] + session._initial_parts = [] + session._collection_cache = {} + session.items = [] + session.testscollected = 0 + return True -@pytest.hookimpl() +@pytest.hookimpl def pytest_xdist_make_scheduler(config: pytest.Config, log: Producer) -> Scheduling: - return HeterogenousLoadScheduling(config, log, config.stash[runs_on_key]) + try: + runs_on_by_nodeid = config.stash[runs_on_key] + except KeyError: + raise RuntimeError("runs_on_key not found in stash") + + return HeterogenousLoadScheduling(config, log, runs_on_by_nodeid) diff --git a/software/hil/test_scheduler.py b/software/hil/test_scheduler.py index 1145cc5d..096fbb45 100644 --- a/software/hil/test_scheduler.py +++ b/software/hil/test_scheduler.py @@ -23,7 +23,8 @@ class HeterogenousLoadScheduling(LoadScheduling): """ Implement load scheduling across heterogeneous nodes. - Similar to `LoadScheduling`, except tests are only allocated to compatible worker nodes, as determined by the `runs_on` marker. + Similar to `LoadScheduling`, except tests are only allocated to compatible worker nodes. + Affinity is determined by the `runs_on` marker. """ def __init__( From 01f6735a4a59bf551b9da0d429b8b7650083a13c Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Mon, 17 Feb 2025 16:54:10 -0800 Subject: [PATCH 05/71] group nodeids --- software/hil/test_scheduler.py | 57 ++++++++++++---------------------- 1 file changed, 20 insertions(+), 37 deletions(-) diff --git a/software/hil/test_scheduler.py b/software/hil/test_scheduler.py index 096fbb45..28e80398 100644 --- a/software/hil/test_scheduler.py +++ b/software/hil/test_scheduler.py @@ -13,7 +13,9 @@ class RunsOn: def __init__(self, *args, hostname: str | None = None): self.hostname = hostname - def check(self, node: WorkerController) -> bool: ... + def check(self, node: WorkerController) -> bool: + # FIXME + return self.hostname == node.gateway.remoteaddress NodeId = str @@ -25,6 +27,10 @@ class HeterogenousLoadScheduling(LoadScheduling): Similar to `LoadScheduling`, except tests are only allocated to compatible worker nodes. Affinity is determined by the `runs_on` marker. + + Attributes: + runs_on_by_nodeid: Mapping of test node ids to RunsOn records. + nodeids_by_worker: Mapping of workers to the node ids that they are compatible with. """ def __init__( @@ -34,42 +40,19 @@ def __init__( runs_on_by_nodeid: dict[NodeId, list[RunsOn]], ): self.runs_on_by_nodeid = runs_on_by_nodeid + self.nodeids_by_worker: dict[WorkerController, list[NodeId]] = {} + super().__init__(config, log) - @property - def nodes(self) -> list[WorkerController]: ... - - @property - def collection_is_completed(self) -> bool: ... - - @property - def tests_finished(self) -> bool: ... - - @property - def has_pending(self) -> bool: ... - - def add_node(self, node: WorkerController) -> None: ... + def add_node(self, node: WorkerController) -> None: + super().add_node(node) + self.nodeids_by_worker[node] = [] def add_node_collection( - self, - node: WorkerController, - collection: Sequence[str], - ) -> None: ... - - def mark_test_complete( - self, - node: WorkerController, - item_index: int, - duration: float = 0, - ) -> None: ... - - def mark_test_pending(self, item: str) -> None: ... - - def remove_pending_tests_from_node( - self, - node: WorkerController, - indices: Sequence[int], - ) -> None: ... - - def remove_node(self, node: WorkerController) -> str | None: ... - - def schedule(self) -> None: ... + self, node: WorkerController, collection: Sequence[str] + ) -> None: + super().add_node_collection(node, collection) + self.nodeids_by_worker[node] = [ + nodeid + for nodeid in collection + if any(run_on.check(node) for run_on in self.runs_on_by_nodeid[nodeid]) + ] From 2f908c18d8f49008ad1c780551c46656a417eab3 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Mon, 17 Feb 2025 17:21:45 -0800 Subject: [PATCH 06/71] schedling wip --- software/hil/test_scheduler.py | 103 ++++++++++++++++++++++++++++----- 1 file changed, 89 insertions(+), 14 deletions(-) diff --git a/software/hil/test_scheduler.py b/software/hil/test_scheduler.py index 28e80398..9675befb 100644 --- a/software/hil/test_scheduler.py +++ b/software/hil/test_scheduler.py @@ -15,10 +15,7 @@ def __init__(self, *args, hostname: str | None = None): def check(self, node: WorkerController) -> bool: # FIXME - return self.hostname == node.gateway.remoteaddress - - -NodeId = str + return self.hostname is None or self.hostname == node.gateway.id class HeterogenousLoadScheduling(LoadScheduling): @@ -29,30 +26,108 @@ class HeterogenousLoadScheduling(LoadScheduling): Affinity is determined by the `runs_on` marker. Attributes: - runs_on_by_nodeid: Mapping of test node ids to RunsOn records. - nodeids_by_worker: Mapping of workers to the node ids that they are compatible with. + runs_on_by_test_name: Mapping of test names to RunsOn records. + test_names_by_worker: Mapping of workers to the test names that they are compatible with. + test_indices_by_worker: Mapping of workers to the indices of the tests that they are compatible with. """ def __init__( self, config: pytest.Config, log: Producer, - runs_on_by_nodeid: dict[NodeId, list[RunsOn]], + runs_on_by_test_name: dict[str, list[RunsOn]], ): - self.runs_on_by_nodeid = runs_on_by_nodeid - self.nodeids_by_worker: dict[WorkerController, list[NodeId]] = {} + self.runs_on_by_test_name = runs_on_by_test_name + self.test_names_by_worker: dict[WorkerController, list[str]] = {} + self.test_indices_by_worker: dict[WorkerController, list[int]] = {} super().__init__(config, log) def add_node(self, node: WorkerController) -> None: super().add_node(node) - self.nodeids_by_worker[node] = [] + self.test_names_by_worker[node] = [] def add_node_collection( self, node: WorkerController, collection: Sequence[str] ) -> None: super().add_node_collection(node, collection) - self.nodeids_by_worker[node] = [ - nodeid - for nodeid in collection - if any(run_on.check(node) for run_on in self.runs_on_by_nodeid[nodeid]) + self.test_names_by_worker[node] = [ + test_name + for test_name in collection + if any( + run_on.check(node) for run_on in self.runs_on_by_test_name[test_name] + ) + ] + self.test_indices_by_worker[node] = [ + collection.index(test_name) for test_name in self.test_names_by_worker[node] ] + + def _check_collection(self): + """ + Check if every test has a compatible worker. + """ + + for test_name, runs_on_list in self.runs_on_by_test_name.items(): + if not runs_on_list: + continue + + for runs_on in runs_on_list: + if any(runs_on.check(node) for node in self.nodes): + break + else: + raise ValueError( + f"Test {test_name} has no compatible worker: requires one of {runs_on_list}" + ) + + def schedule(self) -> None: + # FIXME: close review + + assert self.collection_is_completed + + # If already scheduled, just check schedules + if self.collection is not None: + for node in self.nodes: + self.check_schedule(node) + return + + # Verify collections are identical + if not self._check_nodes_have_same_collection(): + self.log("**Different tests collected, aborting run**") + return + + self._check_collection() + + # Initialize collection and pending tests + self.collection = next(iter(self.node2collection.values())) + self.pending[:] = range(len(self.collection)) + if not self.collection: + return + + if self.maxschedchunk is None: + self.maxschedchunk = len(self.collection) + + # For each node, calculate how many tests it should initially receive + for node in self.nodes: + compatible_tests = len(self.test_indices_by_worker[node]) + if compatible_tests == 0: + continue + + # Calculate initial chunk size for this node + items_per_node = compatible_tests // len(self.nodes) + node_chunksize = min(max(items_per_node // 4, 2), self.maxschedchunk) + + # Send initial batch of tests + self._send_tests(node, node_chunksize) + + # If no more pending tests, start shutting down nodes + if not self.pending: + for node in self.nodes: + node.shutdown() + + def _send_tests(self, node: WorkerController, num: int) -> None: + compatible_indices = set(self.test_indices_by_worker[node]) + compatible_pending = [i for i in self.pending if i in compatible_indices] + tests_for_node = compatible_pending[:num] + if tests_for_node: + self.pending = [i for i in self.pending if i not in tests_for_node] + self.node2pending[node].extend(tests_for_node) + node.send_runtest_some(tests_for_node) From b1b22cfe20a5481953decfefd48d782e8b2c91e2 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Mon, 17 Feb 2025 19:42:26 -0800 Subject: [PATCH 07/71] configure node --- software/hil/pytest_plugin.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/software/hil/pytest_plugin.py b/software/hil/pytest_plugin.py index d550ee60..255ed049 100644 --- a/software/hil/pytest_plugin.py +++ b/software/hil/pytest_plugin.py @@ -30,6 +30,7 @@ from .framework import Trace, record as hil_record from xdist.remote import Producer +from xdist.workermanage import WorkerController from xdist.scheduler.protocol import Scheduling from .framework import record as hil_record @@ -327,3 +328,17 @@ def pytest_xdist_make_scheduler(config: pytest.Config, log: Producer) -> Schedul raise RuntimeError("runs_on_key not found in stash") return HeterogenousLoadScheduling(config, log, runs_on_by_nodeid) + + +@pytest.hookimpl +def pytest_configure_node(node: WorkerController): + channel = node.gateway.remote_exec( + """ + import subprocess + result = subprocess.run(['uv', 'sync', '--frozen'], capture_output=True, text=True) + channel.send((result.returncode, result.stdout, result.stderr)) + """ + ) + return_code, _, stderr = channel.receive() + assert return_code == 0 + print(stderr) From ffb5ebac678bd5c794f1f60dbcd77c1df671efd4 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Mon, 17 Feb 2025 19:42:41 -0800 Subject: [PATCH 08/71] fix distribution to local --- software/hil/test_scheduler.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/software/hil/test_scheduler.py b/software/hil/test_scheduler.py index 9675befb..22ef8648 100644 --- a/software/hil/test_scheduler.py +++ b/software/hil/test_scheduler.py @@ -50,13 +50,24 @@ def add_node_collection( self, node: WorkerController, collection: Sequence[str] ) -> None: super().add_node_collection(node, collection) - self.test_names_by_worker[node] = [ - test_name - for test_name in collection - if any( - run_on.check(node) for run_on in self.runs_on_by_test_name[test_name] - ) - ] + + # TODO: ensure there is a local worker called "local" + if node.gateway.id == "local": + self.test_names_by_worker[node] = [ + test_name + for test_name in collection + if not self.runs_on_by_test_name[test_name] + ] + else: + self.test_names_by_worker[node] = [ + test_name + for test_name in collection + if any( + runs_on.check(node) + for runs_on in self.runs_on_by_test_name[test_name] + ) + ] + self.test_indices_by_worker[node] = [ collection.index(test_name) for test_name in self.test_names_by_worker[node] ] @@ -96,6 +107,9 @@ def schedule(self) -> None: self._check_collection() + print(self.test_names_by_worker) + print(self.test_indices_by_worker) + # Initialize collection and pending tests self.collection = next(iter(self.node2collection.values())) self.pending[:] = range(len(self.collection)) @@ -127,6 +141,7 @@ def _send_tests(self, node: WorkerController, num: int) -> None: compatible_indices = set(self.test_indices_by_worker[node]) compatible_pending = [i for i in self.pending if i in compatible_indices] tests_for_node = compatible_pending[:num] + print(f"sending {tests_for_node} to {node}") if tests_for_node: self.pending = [i for i in self.pending if i not in tests_for_node] self.node2pending[node].extend(tests_for_node) From 9d9f020c32f7efc4dfd085e6e0837998067d2763 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 18 Feb 2025 12:12:44 -0800 Subject: [PATCH 09/71] collect tests on workers --- software/hil/pytest_plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/software/hil/pytest_plugin.py b/software/hil/pytest_plugin.py index 255ed049..1a2c7ead 100644 --- a/software/hil/pytest_plugin.py +++ b/software/hil/pytest_plugin.py @@ -317,7 +317,6 @@ def pytest_collection(session: pytest.Session): session._collection_cache = {} session.items = [] session.testscollected = 0 - return True @pytest.hookimpl From d36128e12bb46085e09da0acaae1a062062c3c82 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 18 Feb 2025 12:13:00 -0800 Subject: [PATCH 10/71] less noise --- software/hil/pytest_plugin.py | 7 +++---- software/hil/test_scheduler.py | 4 ---- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/software/hil/pytest_plugin.py b/software/hil/pytest_plugin.py index 1a2c7ead..a75123b1 100644 --- a/software/hil/pytest_plugin.py +++ b/software/hil/pytest_plugin.py @@ -334,10 +334,9 @@ def pytest_configure_node(node: WorkerController): channel = node.gateway.remote_exec( """ import subprocess - result = subprocess.run(['uv', 'sync', '--frozen'], capture_output=True, text=True) - channel.send((result.returncode, result.stdout, result.stderr)) + result = subprocess.run(['uv', 'sync', '--frozen'], capture_output=True) + channel.send(result.returncode) """ ) - return_code, _, stderr = channel.receive() + return_code = channel.receive() assert return_code == 0 - print(stderr) diff --git a/software/hil/test_scheduler.py b/software/hil/test_scheduler.py index 22ef8648..44192e7b 100644 --- a/software/hil/test_scheduler.py +++ b/software/hil/test_scheduler.py @@ -107,9 +107,6 @@ def schedule(self) -> None: self._check_collection() - print(self.test_names_by_worker) - print(self.test_indices_by_worker) - # Initialize collection and pending tests self.collection = next(iter(self.node2collection.values())) self.pending[:] = range(len(self.collection)) @@ -141,7 +138,6 @@ def _send_tests(self, node: WorkerController, num: int) -> None: compatible_indices = set(self.test_indices_by_worker[node]) compatible_pending = [i for i in self.pending if i in compatible_indices] tests_for_node = compatible_pending[:num] - print(f"sending {tests_for_node} to {node}") if tests_for_node: self.pending = [i for i in self.pending if i not in tests_for_node] self.node2pending[node].extend(tests_for_node) From 91d79118aab4cb322938b911bc6d6bed08d0aaf8 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 18 Feb 2025 12:50:47 -0800 Subject: [PATCH 11/71] fix double collection --- software/hil/pytest_plugin.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/software/hil/pytest_plugin.py b/software/hil/pytest_plugin.py index a75123b1..5c6f34a4 100644 --- a/software/hil/pytest_plugin.py +++ b/software/hil/pytest_plugin.py @@ -304,19 +304,14 @@ def machine_config(request: _Request) -> Generator[ConfigDict, None, None]: runs_on_key = pytest.StashKey[dict[str, list[RunsOn]]]() -@pytest.hookimpl(tryfirst=True) +@pytest.hookimpl(trylast=True) def pytest_collection(session: pytest.Session): session.config.stash[runs_on_key] = { item.nodeid: [ RunsOn(*m.args, **m.kwargs) for m in item.own_markers if m.name == "runs_on" ] - for item in session.perform_collect() + for item in session.items } - session._notfound = [] - session._initial_parts = [] - session._collection_cache = {} - session.items = [] - session.testscollected = 0 @pytest.hookimpl From c394ea7147dac4a19eb149709f78fea2b9ecf750 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 18 Feb 2025 12:55:28 -0800 Subject: [PATCH 12/71] rename --- software/hil/pytest_plugin.py | 3 +-- software/hil/{test_scheduler.py => scheduling.py} | 0 2 files changed, 1 insertion(+), 2 deletions(-) rename software/hil/{test_scheduler.py => scheduling.py} (100%) diff --git a/software/hil/pytest_plugin.py b/software/hil/pytest_plugin.py index 5c6f34a4..1b035c7d 100644 --- a/software/hil/pytest_plugin.py +++ b/software/hil/pytest_plugin.py @@ -22,7 +22,7 @@ import altair as alt from hil.utils.config import ConfigDict, load_config, save_config -from hil.test_scheduler import HeterogenousLoadScheduling, RunsOn +from hil.scheduling import HeterogenousLoadScheduling, RunsOn import pathvalidate import polars as pl import pytest @@ -33,7 +33,6 @@ from xdist.workermanage import WorkerController from xdist.scheduler.protocol import Scheduling -from .framework import record as hil_record logger = logging.getLogger(__name__) diff --git a/software/hil/test_scheduler.py b/software/hil/scheduling.py similarity index 100% rename from software/hil/test_scheduler.py rename to software/hil/scheduling.py From dbaea862127b77637deb1ba15c0dc23d9d99a638 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 18 Feb 2025 13:47:58 -0800 Subject: [PATCH 13/71] check_schedule --- software/hil/scheduling.py | 44 +++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/software/hil/scheduling.py b/software/hil/scheduling.py index 44192e7b..98ddfc35 100644 --- a/software/hil/scheduling.py +++ b/software/hil/scheduling.py @@ -89,25 +89,54 @@ def _check_collection(self): f"Test {test_name} has no compatible worker: requires one of {runs_on_list}" ) + def check_schedule(self, node: WorkerController, duration: float = 0) -> None: + """Ref: `LoadScheduling.check_schedule`""" + if node.shutting_down: + return + + if self.pending: + compatible_indices = set(self.test_indices_by_worker[node]) + compatible_pending = [i for i in self.pending if i in compatible_indices] + + if not compatible_pending: + node.shutdown() + return + + num_nodes = sum(1 for n in self.nodes if self.test_indices_by_worker[n]) + items_per_node_min = max(2, len(compatible_pending) // num_nodes // 4) + items_per_node_max = max(2, len(compatible_pending) // num_nodes // 2) + + node_pending = self.node2pending[node] + if len(node_pending) < items_per_node_min: + if duration >= 0.1 and len(node_pending) >= 2: + # Node is busy + return + + num_send = items_per_node_max - len(node_pending) + # Keep at least 2 tests pending even if maxschedchunk=1 + maxschedchunk = max(2 - len(node_pending), self.maxschedchunk) + self._send_tests(node, min(num_send, maxschedchunk)) + else: + node.shutdown() + + self.log("num items waiting for node:", len(self.pending)) + def schedule(self) -> None: - # FIXME: close review + """Ref: `LoadScheduling.schedule`""" assert self.collection_is_completed - # If already scheduled, just check schedules if self.collection is not None: for node in self.nodes: self.check_schedule(node) return - # Verify collections are identical if not self._check_nodes_have_same_collection(): self.log("**Different tests collected, aborting run**") return self._check_collection() - # Initialize collection and pending tests self.collection = next(iter(self.node2collection.values())) self.pending[:] = range(len(self.collection)) if not self.collection: @@ -116,20 +145,15 @@ def schedule(self) -> None: if self.maxschedchunk is None: self.maxschedchunk = len(self.collection) - # For each node, calculate how many tests it should initially receive for node in self.nodes: - compatible_tests = len(self.test_indices_by_worker[node]) - if compatible_tests == 0: + if (compatible_tests := len(self.test_indices_by_worker[node])) == 0: continue - # Calculate initial chunk size for this node items_per_node = compatible_tests // len(self.nodes) node_chunksize = min(max(items_per_node // 4, 2), self.maxschedchunk) - # Send initial batch of tests self._send_tests(node, node_chunksize) - # If no more pending tests, start shutting down nodes if not self.pending: for node in self.nodes: node.shutdown() From 19d32361ba459d4c07927569b5b72530c25ef2d2 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 18 Feb 2025 16:40:22 -0800 Subject: [PATCH 14/71] fix test collection hook sequencing --- software/hil/pytest_plugin.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/software/hil/pytest_plugin.py b/software/hil/pytest_plugin.py index 1b035c7d..568c34b2 100644 --- a/software/hil/pytest_plugin.py +++ b/software/hil/pytest_plugin.py @@ -303,8 +303,9 @@ def machine_config(request: _Request) -> Generator[ConfigDict, None, None]: runs_on_key = pytest.StashKey[dict[str, list[RunsOn]]]() -@pytest.hookimpl(trylast=True) +@pytest.hookimpl(tryfirst=True) def pytest_collection(session: pytest.Session): + session.perform_collect() session.config.stash[runs_on_key] = { item.nodeid: [ RunsOn(*m.args, **m.kwargs) for m in item.own_markers if m.name == "runs_on" @@ -312,6 +313,9 @@ def pytest_collection(session: pytest.Session): for item in session.items } + # block second collection + return True + @pytest.hookimpl def pytest_xdist_make_scheduler(config: pytest.Config, log: Producer) -> Scheduling: From 8fd18db42d32fe7787017be865626da111609716 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 18 Feb 2025 16:44:49 -0800 Subject: [PATCH 15/71] iter_markers --- software/hil/pytest_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/software/hil/pytest_plugin.py b/software/hil/pytest_plugin.py index 568c34b2..390b33bb 100644 --- a/software/hil/pytest_plugin.py +++ b/software/hil/pytest_plugin.py @@ -308,7 +308,7 @@ def pytest_collection(session: pytest.Session): session.perform_collect() session.config.stash[runs_on_key] = { item.nodeid: [ - RunsOn(*m.args, **m.kwargs) for m in item.own_markers if m.name == "runs_on" + RunsOn(*m.args, **m.kwargs) for m in item.iter_markers(name="runs_on") ] for item in session.items } From 0146a6a462315be7ec614d0825dd330023cb7b7b Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Mon, 24 Feb 2025 13:08:13 -0800 Subject: [PATCH 16/71] ray (wip) --- pyproject.toml | 8 +- software/hil/dist_plugin.py | 155 +++++++ software/hil/pytest_plugin.py | 45 +- uv.lock | 820 +++++++++++++++++++++++++++++++++- 4 files changed, 960 insertions(+), 68 deletions(-) create mode 100644 software/hil/dist_plugin.py diff --git a/pyproject.toml b/pyproject.toml index 20b47957..0e726be9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,13 +18,14 @@ dependencies = [ "pytest-benchmark>=5.1.0", "pytest-html>=4.1.1", "pytest-timeout>=2.3.1", - "pytest-xdist>=3.6.1", + "ray[client,default]", "rich>=13.9.4", "smbus2>=0.5.0", ] [project.entry-points."pytest11"] hil-plugin = "hil.pytest_plugin" +dist-plugin = "hil.dist_plugin" [dependency-groups] dev = [ @@ -33,6 +34,11 @@ dev = [ "ruff>=0.9.6", ] +[tool.uv.sources] +# TODO: use released version (pending 3.13 support) +ray = { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-macosx_11_0_arm64.whl" } +# ray = { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-manylinux2014_aarch64.whl" } + [tool.hatch.build] sources = ["software"] diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py new file mode 100644 index 00000000..a730e03b --- /dev/null +++ b/software/hil/dist_plugin.py @@ -0,0 +1,155 @@ +from dataclasses import dataclass +from pathlib import Path + +import pytest + +import ray +import ray.util.queue +import socket + + +@dataclass +class RunsOn: + hostname: str | None + + def __init__(self, *args, hostname: str | None = None): + self.hostname = hostname + + def check(self, node) -> bool: + # FIXME + return self.hostname is None or self.hostname == node.gateway.id + + +# TODO: actor pool +@ray.remote(resources={"remote": 1}) +class Remote: + """ + Runs on remote node. + + Sets up pytest test loop. Receives test nodes to run. + """ + + def __init__( + self, + test_queue: ray.util.queue.Queue, + message_queue: ray.util.queue.Queue, + args: list[str], + ): + self.test_queue = test_queue + self.message_queue = message_queue + self.hostname = socket.gethostname() + + from _pytest.config import get_config + + # TODO: is this too early? do we need to call pytest_cmdline_parse? + self.config = get_config(args, None) + + pluginmanager = self.config.pluginmanager + print("pluginmanager:", pluginmanager) + + self.config.pluginmanager.hook.pytest_cmdline_parse( + pluginmanager=pluginmanager, args=args + ) + + self.config.option.usepdb = False + self.config.option.dist = "no" + self.config.option.distload = False + self.config.option.numprocesses = None + self.config.option.maxprocesses = None + + print("config:", self.config) + + # self.config.hook.pytest_cmdline_main(config=self.config) + + print("finished pytest configuration") + + # ls + print(list(Path.cwd().glob("*"))) + + async def process_tests(self): + # TODO: shutdown signal + + try: + while True: + test = await self.test_queue.get_async(block=True) + print("running test:", test) + + self.message_queue.put(f"{self.hostname}: processing test: {test}") + except ray.util.queue.Empty: + pass + + def process_test(self): ... + + @pytest.hookimpl + def pytest_runtestloop(self, session: pytest.Session): + print("pytest_runtestloop on remote") + + +class DSession: + runs_on_key = pytest.StashKey[dict[str, list[RunsOn]]]() + + def __init__(self, config: pytest.Config): + if not ray.is_initialized(): + # TODO: from config + ray.init( + address="ray://192.168.1.199:10001", + # namespace="hil", + runtime_env={ + # TODO: smarter working dir (relative to __file__, git root, etc) + "working_dir": str(Path.cwd()), + # TODO: doesn't work — use this instead: + # `RAY_RUNTIME_ENV_HOOK=ray._private.runtime_env.uv_runtime_env_hook.hook` + # "py_executable": "uv run --isolated", + # TODO: exclusions from file? + "excludes": [ + "**/*.step", + "**/*.wrl", + "**/*.kicad_pcb", + "**/*.kicad_pro", + "**/*.kicad_sch", + "**/fp-lib-table", + ], + }, + ) + + # TODO: queue per resource type + self.test_queue = ray.util.queue.Queue() + self.message_queue = ray.util.queue.Queue() + + # TODO: remote per worker for each queue + self.remote1 = Remote.remote(self.test_queue, self.message_queue, config.args) + self.remote1.process_tests.remote() # type: ignore + + @pytest.hookimpl(tryfirst=True) + def pytest_collection(self, session: pytest.Session): + session.perform_collect() + session.config.stash[self.runs_on_key] = { + item.nodeid: [ + RunsOn(*m.args, **m.kwargs) for m in item.iter_markers(name="runs_on") + ] + for item in session.items + } + + return True + + @pytest.hookimpl(tryfirst=True) + def pytest_runtestloop(self, session: pytest.Session): + # TODO: shutdown handling + + for item in session.items: + print("enqueuing:", item.nodeid) + self.test_queue.put(item.nodeid) + + while True: + try: + print("received message:", self.message_queue.get(timeout=1)) + except ray.util.queue.Empty: + break + + return True + + +@pytest.hookimpl(trylast=True) +def pytest_configure(config): + session = DSession(config) + config.pluginmanager.register(session, "dist") diff --git a/software/hil/pytest_plugin.py b/software/hil/pytest_plugin.py index 390b33bb..c206dacb 100644 --- a/software/hil/pytest_plugin.py +++ b/software/hil/pytest_plugin.py @@ -22,16 +22,13 @@ import altair as alt from hil.utils.config import ConfigDict, load_config, save_config -from hil.scheduling import HeterogenousLoadScheduling, RunsOn + import pathvalidate import polars as pl import pytest from pytest_html import extras as html_extras from .framework import Trace, record as hil_record -from xdist.remote import Producer -from xdist.workermanage import WorkerController -from xdist.scheduler.protocol import Scheduling logger = logging.getLogger(__name__) @@ -298,43 +295,3 @@ def machine_config(request: _Request) -> Generator[ConfigDict, None, None]: yield config_obj finally: save_config(config_obj, Path(request.config.rootdir) / configs_path, pet_name) - - -runs_on_key = pytest.StashKey[dict[str, list[RunsOn]]]() - - -@pytest.hookimpl(tryfirst=True) -def pytest_collection(session: pytest.Session): - session.perform_collect() - session.config.stash[runs_on_key] = { - item.nodeid: [ - RunsOn(*m.args, **m.kwargs) for m in item.iter_markers(name="runs_on") - ] - for item in session.items - } - - # block second collection - return True - - -@pytest.hookimpl -def pytest_xdist_make_scheduler(config: pytest.Config, log: Producer) -> Scheduling: - try: - runs_on_by_nodeid = config.stash[runs_on_key] - except KeyError: - raise RuntimeError("runs_on_key not found in stash") - - return HeterogenousLoadScheduling(config, log, runs_on_by_nodeid) - - -@pytest.hookimpl -def pytest_configure_node(node: WorkerController): - channel = node.gateway.remote_exec( - """ - import subprocess - result = subprocess.run(['uv', 'sync', '--frozen'], capture_output=True) - channel.send(result.returncode) - """ - ) - return_code = channel.receive() - assert return_code == 0 diff --git a/uv.lock b/uv.lock index d151f215..0d5f20d0 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,75 @@ version = 1 requires-python = ">=3.13" +resolution-markers = [ + "sys_platform == 'darwin'", + "sys_platform != 'darwin'", +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/07/508f9ebba367fc3370162e53a3cfd12f5652ad79f0e0bfdf9f9847c6f159/aiohappyeyeballs-2.4.6.tar.gz", hash = "sha256:9b05052f9042985d32ecbe4b59a77ae19c006a78f1344d7fdad69d28ded3d0b0", size = 21726 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/4c/03fb05f56551828ec67ceb3665e5dc51638042d204983a03b0a1541475b6/aiohappyeyeballs-2.4.6-py3-none-any.whl", hash = "sha256:147ec992cf873d74f5062644332c539fcd42956dc69453fe5204195e560517e1", size = 14543 }, +] + +[[package]] +name = "aiohttp" +version = "3.11.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/4b/952d49c73084fb790cb5c6ead50848c8e96b4980ad806cf4d2ad341eaa03/aiohttp-3.11.12.tar.gz", hash = "sha256:7603ca26d75b1b86160ce1bbe2787a0b706e592af5b2504e12caa88a217767b0", size = 7673175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/9b/cea185d4b543ae08ee478373e16653722c19fcda10d2d0646f300ce10791/aiohttp-3.11.12-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:413ad794dccb19453e2b97c2375f2ca3cdf34dc50d18cc2693bd5aed7d16f4b9", size = 698148 }, + { url = "https://files.pythonhosted.org/packages/91/5c/80d47fe7749fde584d1404a68ade29bcd7e58db8fa11fa38e8d90d77e447/aiohttp-3.11.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a93d28ed4b4b39e6f46fd240896c29b686b75e39cc6992692e3922ff6982b4c", size = 460831 }, + { url = "https://files.pythonhosted.org/packages/8e/f9/de568f8a8ca6b061d157c50272620c53168d6e3eeddae78dbb0f7db981eb/aiohttp-3.11.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d589264dbba3b16e8951b6f145d1e6b883094075283dafcab4cdd564a9e353a0", size = 453122 }, + { url = "https://files.pythonhosted.org/packages/8b/fd/b775970a047543bbc1d0f66725ba72acef788028fce215dc959fd15a8200/aiohttp-3.11.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5148ca8955affdfeb864aca158ecae11030e952b25b3ae15d4e2b5ba299bad2", size = 1665336 }, + { url = "https://files.pythonhosted.org/packages/82/9b/aff01d4f9716245a1b2965f02044e4474fadd2bcfe63cf249ca788541886/aiohttp-3.11.12-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:525410e0790aab036492eeea913858989c4cb070ff373ec3bc322d700bdf47c1", size = 1718111 }, + { url = "https://files.pythonhosted.org/packages/e0/a9/166fd2d8b2cc64f08104aa614fad30eee506b563154081bf88ce729bc665/aiohttp-3.11.12-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bd8695be2c80b665ae3f05cb584093a1e59c35ecb7d794d1edd96e8cc9201d7", size = 1775293 }, + { url = "https://files.pythonhosted.org/packages/13/c5/0d3c89bd9e36288f10dc246f42518ce8e1c333f27636ac78df091c86bb4a/aiohttp-3.11.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0203433121484b32646a5f5ea93ae86f3d9559d7243f07e8c0eab5ff8e3f70e", size = 1677338 }, + { url = "https://files.pythonhosted.org/packages/72/b2/017db2833ef537be284f64ead78725984db8a39276c1a9a07c5c7526e238/aiohttp-3.11.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40cd36749a1035c34ba8d8aaf221b91ca3d111532e5ccb5fa8c3703ab1b967ed", size = 1603365 }, + { url = "https://files.pythonhosted.org/packages/fc/72/b66c96a106ec7e791e29988c222141dd1219d7793ffb01e72245399e08d2/aiohttp-3.11.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a7442662afebbf7b4c6d28cb7aab9e9ce3a5df055fc4116cc7228192ad6cb484", size = 1618464 }, + { url = "https://files.pythonhosted.org/packages/3f/50/e68a40f267b46a603bab569d48d57f23508801614e05b3369898c5b2910a/aiohttp-3.11.12-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8a2fb742ef378284a50766e985804bd6adb5adb5aa781100b09befdbfa757b65", size = 1657827 }, + { url = "https://files.pythonhosted.org/packages/c5/1d/aafbcdb1773d0ba7c20793ebeedfaba1f3f7462f6fc251f24983ed738aa7/aiohttp-3.11.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cee3b117a8d13ab98b38d5b6bdcd040cfb4181068d05ce0c474ec9db5f3c5bb", size = 1616700 }, + { url = "https://files.pythonhosted.org/packages/b0/5e/6cd9724a2932f36e2a6b742436a36d64784322cfb3406ca773f903bb9a70/aiohttp-3.11.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f6a19bcab7fbd8f8649d6595624856635159a6527861b9cdc3447af288a00c00", size = 1685643 }, + { url = "https://files.pythonhosted.org/packages/8b/38/ea6c91d5c767fd45a18151675a07c710ca018b30aa876a9f35b32fa59761/aiohttp-3.11.12-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e4cecdb52aaa9994fbed6b81d4568427b6002f0a91c322697a4bfcc2b2363f5a", size = 1715487 }, + { url = "https://files.pythonhosted.org/packages/8e/24/e9edbcb7d1d93c02e055490348df6f955d675e85a028c33babdcaeda0853/aiohttp-3.11.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:30f546358dfa0953db92ba620101fefc81574f87b2346556b90b5f3ef16e55ce", size = 1672948 }, + { url = "https://files.pythonhosted.org/packages/25/be/0b1fb737268e003198f25c3a68c2135e76e4754bf399a879b27bd508a003/aiohttp-3.11.12-cp313-cp313-win32.whl", hash = "sha256:ce1bb21fc7d753b5f8a5d5a4bae99566386b15e716ebdb410154c16c91494d7f", size = 410396 }, + { url = "https://files.pythonhosted.org/packages/68/fd/677def96a75057b0a26446b62f8fbb084435b20a7d270c99539c26573bfd/aiohttp-3.11.12-cp313-cp313-win_amd64.whl", hash = "sha256:f7914ab70d2ee8ab91c13e5402122edbc77821c66d2758abb53aabe87f013287", size = 436234 }, +] + +[[package]] +name = "aiohttp-cors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/9e/6cdce7c3f346d8fd487adf68761728ad8cd5fbc296a7b07b92518350d31f/aiohttp-cors-0.7.0.tar.gz", hash = "sha256:4d39c6d7100fd9764ed1caf8cebf0eb01bf5e3f24e2e073fda6234bc48b19f5d", size = 35966 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/e7/e436a0c0eb5127d8b491a9b83ecd2391c6ff7dcd5548dfaec2080a2340fd/aiohttp_cors-0.7.0-py3-none-any.whl", hash = "sha256:0451ba59fdf6909d0e2cd21e4c0a43752bc0703d33fc78ae94d9d9321710193e", size = 27564 }, +] + +[[package]] +name = "aiosignal" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597 }, +] [[package]] name = "altair" @@ -17,6 +87,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/f3/0b6ced594e51cc95d8c1fc1640d3623770d01e4969d29c0bd09945fafefa/altair-5.5.0-py3-none-any.whl", hash = "sha256:91a310b926508d560fe0148d02a194f38b824122641ef528113d029fcd129f8c", size = 731200 }, ] +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + [[package]] name = "attrs" version = "25.1.0" @@ -26,6 +105,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/30/d4986a882011f9df997a55e6becd864812ccfcd821d64aac8570ee39f719/attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a", size = 63152 }, ] +[[package]] +name = "cachetools" +version = "5.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080 }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + [[package]] name = "cfgv" version = "3.4.0" @@ -35,6 +132,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, ] +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -45,21 +176,24 @@ wheels = [ ] [[package]] -name = "distlib" -version = "0.3.9" +name = "colorful" +version = "0.5.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/5f/38e40c3bc4107c39e4062d943026b8ee25154cb4b185b882f274a1ab65da/colorful-0.5.6.tar.gz", hash = "sha256:b56d5c01db1dac4898308ea889edcb113fbee3e6ec5df4bacffd61d5241b5b8d", size = 209280 } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, + { url = "https://files.pythonhosted.org/packages/b3/61/39e7db0cb326c9c8f6a49fad4fc9c2f1241f05a4e10f0643fc31ce26a7e0/colorful-0.5.6-py2.py3-none-any.whl", hash = "sha256:eab8c1c809f5025ad2b5238a50bd691e26850da8cac8f90d660ede6ea1af9f1e", size = 201369 }, ] [[package]] -name = "execnet" -version = "2.1.1" +name = "distlib" +version = "0.3.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/ff/b4c0dc78fbe20c3e59c0c7334de0c27eb4001a2b2017999af398bf730817/execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3", size = 166524 } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612 }, + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, ] [[package]] @@ -71,6 +205,89 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164 }, ] +[[package]] +name = "frozenlist" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/ed/0f4cec13a93c02c47ec32d81d11c0c1efbadf4a471e3f3ce7cad366cbbd3/frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", size = 39930 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/3b/915f0bca8a7ea04483622e84a9bd90033bab54bdf485479556c74fd5eaf5/frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953", size = 91538 }, + { url = "https://files.pythonhosted.org/packages/c7/d1/a7c98aad7e44afe5306a2b068434a5830f1470675f0e715abb86eb15f15b/frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0", size = 52849 }, + { url = "https://files.pythonhosted.org/packages/3a/c8/76f23bf9ab15d5f760eb48701909645f686f9c64fbb8982674c241fbef14/frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2", size = 50583 }, + { url = "https://files.pythonhosted.org/packages/1f/22/462a3dd093d11df623179d7754a3b3269de3b42de2808cddef50ee0f4f48/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f", size = 265636 }, + { url = "https://files.pythonhosted.org/packages/80/cf/e075e407fc2ae7328155a1cd7e22f932773c8073c1fc78016607d19cc3e5/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608", size = 270214 }, + { url = "https://files.pythonhosted.org/packages/a1/58/0642d061d5de779f39c50cbb00df49682832923f3d2ebfb0fedf02d05f7f/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b", size = 273905 }, + { url = "https://files.pythonhosted.org/packages/ab/66/3fe0f5f8f2add5b4ab7aa4e199f767fd3b55da26e3ca4ce2cc36698e50c4/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840", size = 250542 }, + { url = "https://files.pythonhosted.org/packages/f6/b8/260791bde9198c87a465224e0e2bb62c4e716f5d198fc3a1dacc4895dbd1/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439", size = 267026 }, + { url = "https://files.pythonhosted.org/packages/2e/a4/3d24f88c527f08f8d44ade24eaee83b2627793fa62fa07cbb7ff7a2f7d42/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de", size = 257690 }, + { url = "https://files.pythonhosted.org/packages/de/9a/d311d660420b2beeff3459b6626f2ab4fb236d07afbdac034a4371fe696e/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641", size = 253893 }, + { url = "https://files.pythonhosted.org/packages/c6/23/e491aadc25b56eabd0f18c53bb19f3cdc6de30b2129ee0bc39cd387cd560/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e", size = 267006 }, + { url = "https://files.pythonhosted.org/packages/08/c4/ab918ce636a35fb974d13d666dcbe03969592aeca6c3ab3835acff01f79c/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9", size = 276157 }, + { url = "https://files.pythonhosted.org/packages/c0/29/3b7a0bbbbe5a34833ba26f686aabfe982924adbdcafdc294a7a129c31688/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03", size = 264642 }, + { url = "https://files.pythonhosted.org/packages/ab/42/0595b3dbffc2e82d7fe658c12d5a5bafcd7516c6bf2d1d1feb5387caa9c1/frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c", size = 44914 }, + { url = "https://files.pythonhosted.org/packages/17/c4/b7db1206a3fea44bf3b838ca61deb6f74424a8a5db1dd53ecb21da669be6/frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28", size = 51167 }, + { url = "https://files.pythonhosted.org/packages/c6/c8/a5be5b7550c10858fcf9b0ea054baccab474da77d37f1e828ce043a3a5d4/frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", size = 11901 }, +] + +[[package]] +name = "google-api-core" +version = "2.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/b7/481c83223d7b4f02c7651713fceca648fa3336e1571b9804713f66bca2d8/google_api_core-2.24.1.tar.gz", hash = "sha256:f8b36f5456ab0dd99a1b693a40a31d1e7757beea380ad1b38faaf8941eae9d8a", size = 163508 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/a6/8e30ddfd3d39ee6d2c76d3d4f64a83f77ac86a4cab67b286ae35ce9e4369/google_api_core-2.24.1-py3-none-any.whl", hash = "sha256:bc78d608f5a5bf853b80bd70a795f703294de656c096c0968320830a4bc280f1", size = 160059 }, +] + +[[package]] +name = "google-auth" +version = "2.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/eb/d504ba1daf190af6b204a9d4714d457462b486043744901a6eeea711f913/google_auth-2.38.0.tar.gz", hash = "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4", size = 270866 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/47/603554949a37bca5b7f894d51896a9c534b9eab808e2520a748e081669d0/google_auth-2.38.0-py2.py3-none-any.whl", hash = "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a", size = 210770 }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.68.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/d2/c08f0d9f94b45faca68e355771329cba2411c777c8713924dd1baee0e09c/googleapis_common_protos-1.68.0.tar.gz", hash = "sha256:95d38161f4f9af0d9423eed8fb7b64ffd2568c3464eb542ff02c5bfa1953ab3c", size = 57367 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/85/c99a157ee99d67cc6c9ad123abb8b1bfb476fab32d2f3511c59314548e4f/googleapis_common_protos-1.68.0-py2.py3-none-any.whl", hash = "sha256:aaf179b2f81df26dfadac95def3b16a95064c76a5f45f07e4c68a21bb371c4ac", size = 164985 }, +] + +[[package]] +name = "grpcio" +version = "1.70.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/e1/4b21b5017c33f3600dcc32b802bb48fe44a4d36d6c066f52650c7c2690fa/grpcio-1.70.0.tar.gz", hash = "sha256:8d1584a68d5922330025881e63a6c1b54cc8117291d382e4fa69339b6d914c56", size = 12788932 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/38/66d0f32f88feaf7d83f8559cd87d899c970f91b1b8a8819b58226de0a496/grpcio-1.70.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:aa573896aeb7d7ce10b1fa425ba263e8dddd83d71530d1322fd3a16f31257b4a", size = 5199218 }, + { url = "https://files.pythonhosted.org/packages/c1/96/947df763a0b18efb5cc6c2ae348e56d97ca520dc5300c01617b234410173/grpcio-1.70.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:d405b005018fd516c9ac529f4b4122342f60ec1cee181788249372524e6db429", size = 11445983 }, + { url = "https://files.pythonhosted.org/packages/fd/5b/f3d4b063e51b2454bedb828e41f3485800889a3609c49e60f2296cc8b8e5/grpcio-1.70.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f32090238b720eb585248654db8e3afc87b48d26ac423c8dde8334a232ff53c9", size = 5663954 }, + { url = "https://files.pythonhosted.org/packages/bd/0b/dab54365fcedf63e9f358c1431885478e77d6f190d65668936b12dd38057/grpcio-1.70.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfa089a734f24ee5f6880c83d043e4f46bf812fcea5181dcb3a572db1e79e01c", size = 6304323 }, + { url = "https://files.pythonhosted.org/packages/76/a8/8f965a7171ddd336ce32946e22954aa1bbc6f23f095e15dadaa70604ba20/grpcio-1.70.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f19375f0300b96c0117aca118d400e76fede6db6e91f3c34b7b035822e06c35f", size = 5910939 }, + { url = "https://files.pythonhosted.org/packages/1b/05/0bbf68be8b17d1ed6f178435a3c0c12e665a1e6054470a64ce3cb7896596/grpcio-1.70.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:7c73c42102e4a5ec76608d9b60227d917cea46dff4d11d372f64cbeb56d259d0", size = 6631405 }, + { url = "https://files.pythonhosted.org/packages/79/6a/5df64b6df405a1ed1482cb6c10044b06ec47fd28e87c2232dbcf435ecb33/grpcio-1.70.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:0a5c78d5198a1f0aa60006cd6eb1c912b4a1520b6a3968e677dbcba215fabb40", size = 6190982 }, + { url = "https://files.pythonhosted.org/packages/42/aa/aeaac87737e6d25d1048c53b8ec408c056d3ed0c922e7c5efad65384250c/grpcio-1.70.0-cp313-cp313-win32.whl", hash = "sha256:fe9dbd916df3b60e865258a8c72ac98f3ac9e2a9542dcb72b7a34d236242a5ce", size = 3598359 }, + { url = "https://files.pythonhosted.org/packages/1f/79/8edd2442d2de1431b4a3de84ef91c37002f12de0f9b577fb07b452989dbc/grpcio-1.70.0-cp313-cp313-win_amd64.whl", hash = "sha256:4119fed8abb7ff6c32e3d2255301e59c316c22d31ab812b3fbcbaf3d0d87cc68", size = 4293938 }, +] + [[package]] name = "hil" version = "0.1.0" @@ -85,7 +302,7 @@ dependencies = [ { name = "pytest-benchmark" }, { name = "pytest-html" }, { name = "pytest-timeout" }, - { name = "pytest-xdist" }, + { name = "ray", extra = ["client", "default"] }, { name = "rich" }, { name = "smbus2" }, ] @@ -108,7 +325,7 @@ requires-dist = [ { name = "pytest-benchmark", specifier = ">=5.1.0" }, { name = "pytest-html", specifier = ">=4.1.1" }, { name = "pytest-timeout", specifier = ">=2.3.1" }, - { name = "pytest-xdist", specifier = ">=3.6.1" }, + { name = "ray", extras = ["client", "default"], url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-macosx_11_0_arm64.whl" }, { name = "rich", specifier = ">=13.9.4" }, { name = "smbus2", specifier = ">=0.5.0" }, ] @@ -129,6 +346,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/00/1fd4a117c6c93f2dcc5b7edaeaf53ea45332ef966429be566ca16c2beb94/identify-2.6.7-py2.py3-none-any.whl", hash = "sha256:155931cb617a401807b09ecec6635d6c692d180090a1cedca8ef7d58ba5b6aa0", size = 99097 }, ] +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + [[package]] name = "iniconfig" version = "2.0.0" @@ -226,6 +452,49 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, ] +[[package]] +name = "msgpack" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/d0/7555686ae7ff5731205df1012ede15dd9d927f6227ea151e901c7406af4f/msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e", size = 167260 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/b0/380f5f639543a4ac413e969109978feb1f3c66e931068f91ab6ab0f8be00/msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf", size = 151142 }, + { url = "https://files.pythonhosted.org/packages/c8/ee/be57e9702400a6cb2606883d55b05784fada898dfc7fd12608ab1fdb054e/msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330", size = 84523 }, + { url = "https://files.pythonhosted.org/packages/7e/3a/2919f63acca3c119565449681ad08a2f84b2171ddfcff1dba6959db2cceb/msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734", size = 81556 }, + { url = "https://files.pythonhosted.org/packages/7c/43/a11113d9e5c1498c145a8925768ea2d5fce7cbab15c99cda655aa09947ed/msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e", size = 392105 }, + { url = "https://files.pythonhosted.org/packages/2d/7b/2c1d74ca6c94f70a1add74a8393a0138172207dc5de6fc6269483519d048/msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca", size = 399979 }, + { url = "https://files.pythonhosted.org/packages/82/8c/cf64ae518c7b8efc763ca1f1348a96f0e37150061e777a8ea5430b413a74/msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915", size = 383816 }, + { url = "https://files.pythonhosted.org/packages/69/86/a847ef7a0f5ef3fa94ae20f52a4cacf596a4e4a010197fbcc27744eb9a83/msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d", size = 380973 }, + { url = "https://files.pythonhosted.org/packages/aa/90/c74cf6e1126faa93185d3b830ee97246ecc4fe12cf9d2d31318ee4246994/msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434", size = 387435 }, + { url = "https://files.pythonhosted.org/packages/7a/40/631c238f1f338eb09f4acb0f34ab5862c4e9d7eda11c1b685471a4c5ea37/msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c", size = 399082 }, + { url = "https://files.pythonhosted.org/packages/e9/1b/fa8a952be252a1555ed39f97c06778e3aeb9123aa4cccc0fd2acd0b4e315/msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc", size = 69037 }, + { url = "https://files.pythonhosted.org/packages/b6/bc/8bd826dd03e022153bfa1766dcdec4976d6c818865ed54223d71f07862b3/msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f", size = 75140 }, +] + +[[package]] +name = "multidict" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 }, + { url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 }, + { url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 }, + { url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 }, + { url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 }, + { url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 }, + { url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 }, + { url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 }, + { url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 }, + { url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 }, + { url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 }, + { url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 }, + { url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 }, + { url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 }, + { url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 }, + { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, +] + [[package]] name = "narwhals" version = "1.26.0" @@ -272,6 +541,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/94/cd9e9b04012c015cb6320ab3bf43bc615e248dddfeb163728e800a5d96f0/numpy-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:97b974d3ba0fb4612b77ed35d7627490e8e3dff56ab41454d9e8b23448940576", size = 12696208 }, ] +[[package]] +name = "opencensus" +version = "0.11.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "opencensus-context" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/a7/a46dcffa1b63084f9f17fe3c8cb20724c4c8f91009fd0b2cfdb27d5d2b35/opencensus-0.11.4.tar.gz", hash = "sha256:cbef87d8b8773064ab60e5c2a1ced58bbaa38a6d052c41aec224958ce544eff2", size = 64966 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/ed/9fbdeb23a09e430d87b7d72d430484b88184633dc50f6bfb792354b6f661/opencensus-0.11.4-py2.py3-none-any.whl", hash = "sha256:a18487ce68bc19900336e0ff4655c5a116daf10c1b3685ece8d971bddad6a864", size = 128225 }, +] + +[[package]] +name = "opencensus-context" +version = "0.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/96/3b6f638f6275a8abbd45e582448723bffa29c1fb426721dedb5c72f7d056/opencensus-context-0.1.3.tar.gz", hash = "sha256:a03108c3c10d8c80bb5ddf5c8a1f033161fa61972a9917f9b9b3a18517f0088c", size = 4066 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/68/162c97ea78c957d68ecf78a5c5041d2e25bd5562bdf5d89a6cbf7f8429bf/opencensus_context-0.1.3-py2.py3-none-any.whl", hash = "sha256:073bb0590007af276853009fac7e4bab1d523c3f03baf4cb4511ca38967c6039", size = 5060 }, +] + [[package]] name = "packaging" version = "24.2" @@ -338,6 +630,82 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/b3/df14c580d82b9627d173ceea305ba898dca135feb360b6d84019d0803d3b/pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b", size = 220560 }, ] +[[package]] +name = "prometheus-client" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/62/14/7d0f567991f3a9af8d1cd4f619040c93b68f09a02b6d0b6ab1b2d1ded5fe/prometheus_client-0.21.1.tar.gz", hash = "sha256:252505a722ac04b0456be05c05f75f45d760c2911ffc45f2a06bcaed9f3ae3fb", size = 78551 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/c2/ab7d37426c179ceb9aeb109a85cda8948bb269b7561a0be870cc656eefe4/prometheus_client-0.21.1-py3-none-any.whl", hash = "sha256:594b45c410d6f4f8888940fe80b5cc2521b305a1fafe1c58609ef715a001f301", size = 54682 }, +] + +[[package]] +name = "propcache" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/76/f941e63d55c0293ff7829dd21e7cf1147e90a526756869a9070f287a68c9/propcache-0.3.0.tar.gz", hash = "sha256:a8fd93de4e1d278046345f49e2238cdb298589325849b2645d4a94c53faeffc5", size = 42722 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/0f/a79dd23a0efd6ee01ab0dc9750d8479b343bfd0c73560d59d271eb6a99d4/propcache-0.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a2b9bf8c79b660d0ca1ad95e587818c30ccdb11f787657458d6f26a1ea18c568", size = 77287 }, + { url = "https://files.pythonhosted.org/packages/b8/51/76675703c90de38ac75adb8deceb3f3ad99b67ff02a0fa5d067757971ab8/propcache-0.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0c1a133d42c6fc1f5fbcf5c91331657a1ff822e87989bf4a6e2e39b818d0ee9", size = 44923 }, + { url = "https://files.pythonhosted.org/packages/01/9b/fd5ddbee66cf7686e73c516227c2fd9bf471dbfed0f48329d095ea1228d3/propcache-0.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bb2f144c6d98bb5cbc94adeb0447cfd4c0f991341baa68eee3f3b0c9c0e83767", size = 44325 }, + { url = "https://files.pythonhosted.org/packages/13/1c/6961f11eb215a683b34b903b82bde486c606516c1466bf1fa67f26906d51/propcache-0.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1323cd04d6e92150bcc79d0174ce347ed4b349d748b9358fd2e497b121e03c8", size = 225116 }, + { url = "https://files.pythonhosted.org/packages/ef/ea/f8410c40abcb2e40dffe9adeed017898c930974650a63e5c79b886aa9f73/propcache-0.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b812b3cb6caacd072276ac0492d249f210006c57726b6484a1e1805b3cfeea0", size = 229905 }, + { url = "https://files.pythonhosted.org/packages/ef/5a/a9bf90894001468bf8e6ea293bb00626cc9ef10f8eb7996e9ec29345c7ed/propcache-0.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:742840d1d0438eb7ea4280f3347598f507a199a35a08294afdcc560c3739989d", size = 233221 }, + { url = "https://files.pythonhosted.org/packages/dd/ce/fffdddd9725b690b01d345c1156b4c2cc6dca09ab5c23a6d07b8f37d6e2f/propcache-0.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c6e7e4f9167fddc438cd653d826f2222222564daed4116a02a184b464d3ef05", size = 227627 }, + { url = "https://files.pythonhosted.org/packages/58/ae/45c89a5994a334735a3032b48e8e4a98c05d9536ddee0719913dc27da548/propcache-0.3.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a94ffc66738da99232ddffcf7910e0f69e2bbe3a0802e54426dbf0714e1c2ffe", size = 214217 }, + { url = "https://files.pythonhosted.org/packages/01/84/bc60188c3290ff8f5f4a92b9ca2d93a62e449c8daf6fd11ad517ad136926/propcache-0.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c6ec957025bf32b15cbc6b67afe233c65b30005e4c55fe5768e4bb518d712f1", size = 212921 }, + { url = "https://files.pythonhosted.org/packages/14/b3/39d60224048feef7a96edabb8217dc3f75415457e5ebbef6814f8b2a27b5/propcache-0.3.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:549722908de62aa0b47a78b90531c022fa6e139f9166be634f667ff45632cc92", size = 208200 }, + { url = "https://files.pythonhosted.org/packages/9d/b3/0a6720b86791251273fff8a01bc8e628bc70903513bd456f86cde1e1ef84/propcache-0.3.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5d62c4f6706bff5d8a52fd51fec6069bef69e7202ed481486c0bc3874912c787", size = 208400 }, + { url = "https://files.pythonhosted.org/packages/e9/4f/bb470f3e687790547e2e78105fb411f54e0cdde0d74106ccadd2521c6572/propcache-0.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:24c04f8fbf60094c531667b8207acbae54146661657a1b1be6d3ca7773b7a545", size = 218116 }, + { url = "https://files.pythonhosted.org/packages/34/71/277f7f9add469698ac9724c199bfe06f85b199542121a71f65a80423d62a/propcache-0.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7c5f5290799a3f6539cc5e6f474c3e5c5fbeba74a5e1e5be75587746a940d51e", size = 222911 }, + { url = "https://files.pythonhosted.org/packages/92/e3/a7b9782aef5a2fc765b1d97da9ec7aed2f25a4e985703608e73232205e3f/propcache-0.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4fa0e7c9c3cf7c276d4f6ab9af8adddc127d04e0fcabede315904d2ff76db626", size = 216563 }, + { url = "https://files.pythonhosted.org/packages/ab/76/0583ca2c551aa08ffcff87b2c6849c8f01c1f6fb815a5226f0c5c202173e/propcache-0.3.0-cp313-cp313-win32.whl", hash = "sha256:ee0bd3a7b2e184e88d25c9baa6a9dc609ba25b76daae942edfb14499ac7ec374", size = 39763 }, + { url = "https://files.pythonhosted.org/packages/80/ec/c6a84f9a36f608379b95f0e786c111d5465926f8c62f12be8cdadb02b15c/propcache-0.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1c8f7d896a16da9455f882870a507567d4f58c53504dc2d4b1e1d386dfe4588a", size = 43650 }, + { url = "https://files.pythonhosted.org/packages/ee/95/7d32e3560f5bf83fc2f2a4c1b0c181d327d53d5f85ebd045ab89d4d97763/propcache-0.3.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e560fd75aaf3e5693b91bcaddd8b314f4d57e99aef8a6c6dc692f935cc1e6bbf", size = 82140 }, + { url = "https://files.pythonhosted.org/packages/86/89/752388f12e6027a5e63f5d075f15291ded48e2d8311314fff039da5a9b11/propcache-0.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65a37714b8ad9aba5780325228598a5b16c47ba0f8aeb3dc0514701e4413d7c0", size = 47296 }, + { url = "https://files.pythonhosted.org/packages/1b/4c/b55c98d586c69180d3048984a57a5ea238bdeeccf82dbfcd598e935e10bb/propcache-0.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:07700939b2cbd67bfb3b76a12e1412405d71019df00ca5697ce75e5ef789d829", size = 46724 }, + { url = "https://files.pythonhosted.org/packages/0f/b6/67451a437aed90c4e951e320b5b3d7eb584ade1d5592f6e5e8f678030989/propcache-0.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c0fdbdf6983526e269e5a8d53b7ae3622dd6998468821d660d0daf72779aefa", size = 291499 }, + { url = "https://files.pythonhosted.org/packages/ee/ff/e4179facd21515b24737e1e26e02615dfb5ed29416eed4cf5bc6ac5ce5fb/propcache-0.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:794c3dd744fad478b6232289c866c25406ecdfc47e294618bdf1697e69bd64a6", size = 293911 }, + { url = "https://files.pythonhosted.org/packages/76/8d/94a8585992a064a23bd54f56c5e58c3b8bf0c0a06ae10e56f2353ae16c3d/propcache-0.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4544699674faf66fb6b4473a1518ae4999c1b614f0b8297b1cef96bac25381db", size = 293301 }, + { url = "https://files.pythonhosted.org/packages/b0/b8/2c860c92b4134f68c7716c6f30a0d723973f881c32a6d7a24c4ddca05fdf/propcache-0.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fddb8870bdb83456a489ab67c6b3040a8d5a55069aa6f72f9d872235fbc52f54", size = 281947 }, + { url = "https://files.pythonhosted.org/packages/cd/72/b564be7411b525d11757b713c757c21cd4dc13b6569c3b2b8f6d3c96fd5e/propcache-0.3.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f857034dc68d5ceb30fb60afb6ff2103087aea10a01b613985610e007053a121", size = 268072 }, + { url = "https://files.pythonhosted.org/packages/37/68/d94649e399e8d7fc051e5a4f2334efc567993525af083db145a70690a121/propcache-0.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:02df07041e0820cacc8f739510078f2aadcfd3fc57eaeeb16d5ded85c872c89e", size = 275190 }, + { url = "https://files.pythonhosted.org/packages/d8/3c/446e125f5bbbc1922964dd67cb541c01cdb678d811297b79a4ff6accc843/propcache-0.3.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f47d52fd9b2ac418c4890aad2f6d21a6b96183c98021f0a48497a904199f006e", size = 254145 }, + { url = "https://files.pythonhosted.org/packages/f4/80/fd3f741483dc8e59f7ba7e05eaa0f4e11677d7db2077522b92ff80117a2a/propcache-0.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9ff4e9ecb6e4b363430edf2c6e50173a63e0820e549918adef70515f87ced19a", size = 257163 }, + { url = "https://files.pythonhosted.org/packages/dc/cf/6292b5ce6ed0017e6a89024a827292122cc41b6259b30ada0c6732288513/propcache-0.3.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ecc2920630283e0783c22e2ac94427f8cca29a04cfdf331467d4f661f4072dac", size = 280249 }, + { url = "https://files.pythonhosted.org/packages/e8/f0/fd9b8247b449fe02a4f96538b979997e229af516d7462b006392badc59a1/propcache-0.3.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:c441c841e82c5ba7a85ad25986014be8d7849c3cfbdb6004541873505929a74e", size = 288741 }, + { url = "https://files.pythonhosted.org/packages/64/71/cf831fdc2617f86cfd7f414cfc487d018e722dac8acc098366ce9bba0941/propcache-0.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c929916cbdb540d3407c66f19f73387f43e7c12fa318a66f64ac99da601bcdf", size = 277061 }, + { url = "https://files.pythonhosted.org/packages/42/78/9432542a35d944abeca9e02927a0de38cd7a298466d8ffa171536e2381c3/propcache-0.3.0-cp313-cp313t-win32.whl", hash = "sha256:0c3e893c4464ebd751b44ae76c12c5f5c1e4f6cbd6fbf67e3783cd93ad221863", size = 42252 }, + { url = "https://files.pythonhosted.org/packages/6f/45/960365f4f8978f48ebb56b1127adf33a49f2e69ecd46ac1f46d6cf78a79d/propcache-0.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:75e872573220d1ee2305b35c9813626e620768248425f58798413e9c39741f46", size = 46425 }, + { url = "https://files.pythonhosted.org/packages/b5/35/6c4c6fc8774a9e3629cd750dc24a7a4fb090a25ccd5c3246d127b70f9e22/propcache-0.3.0-py3-none-any.whl", hash = "sha256:67dda3c7325691c2081510e92c561f465ba61b975f481735aefdfc845d2cd043", size = 12101 }, +] + +[[package]] +name = "proto-plus" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/79/a5c6cbb42268cfd3ddc652dc526889044a8798c688a03ff58e5e92b743c8/proto_plus-1.26.0.tar.gz", hash = "sha256:6e93d5f5ca267b54300880fff156b6a3386b3fa3f43b1da62e680fc0c586ef22", size = 56136 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/c3/59308ccc07b34980f9d532f7afc718a9f32b40e52cde7a740df8d55632fb/proto_plus-1.26.0-py3-none-any.whl", hash = "sha256:bf2dfaa3da281fc3187d12d224c707cb57214fb2c22ba854eb0c105a3fb2d4d7", size = 50166 }, +] + +[[package]] +name = "protobuf" +version = "5.29.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/d1/e0a911544ca9993e0f17ce6d3cc0932752356c1b0a834397f28e63479344/protobuf-5.29.3.tar.gz", hash = "sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620", size = 424945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/7a/1e38f3cafa022f477ca0f57a1f49962f21ad25850c3ca0acd3b9d0091518/protobuf-5.29.3-cp310-abi3-win32.whl", hash = "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888", size = 422708 }, + { url = "https://files.pythonhosted.org/packages/61/fa/aae8e10512b83de633f2646506a6d835b151edf4b30d18d73afd01447253/protobuf-5.29.3-cp310-abi3-win_amd64.whl", hash = "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a", size = 434508 }, + { url = "https://files.pythonhosted.org/packages/dd/04/3eaedc2ba17a088961d0e3bd396eac764450f431621b58a04ce898acd126/protobuf-5.29.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e", size = 417825 }, + { url = "https://files.pythonhosted.org/packages/4f/06/7c467744d23c3979ce250397e26d8ad8eeb2bea7b18ca12ad58313c1b8d5/protobuf-5.29.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84", size = 319573 }, + { url = "https://files.pythonhosted.org/packages/a8/45/2ebbde52ad2be18d3675b6bee50e68cd73c9e0654de77d595540b5129df8/protobuf-5.29.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f", size = 319672 }, + { url = "https://files.pythonhosted.org/packages/fd/b2/ab07b09e0f6d143dfb839693aa05765257bceaa13d03bf1a696b78323e7a/protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f", size = 172550 }, +] + [[package]] name = "py-cpuinfo" version = "9.0.0" @@ -347,6 +715,81 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335 }, ] +[[package]] +name = "py-spy" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/cd/9dacc04604dc4398ce5bed77ed59918ad0940f15165954d4aaa651cc640c/py_spy-0.4.0.tar.gz", hash = "sha256:806602ce7972782cc9c1e383f339bfc27bfb822d42485e6a3e0530ae5040e1f0", size = 253236 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/7e/02ca3ee68507db47afce769504060d71b4dc1455f0f9faa8d32fc7762221/py_spy-0.4.0-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f2cf3f7130e7d780471faa5957441d3b4e0ec39a79b2c00f4c33d494f7728428", size = 3617847 }, + { url = "https://files.pythonhosted.org/packages/65/7c/d9e26cc4c8e91f96a3a65de04d2e2e4131fbcaf6830d10917d4fab9d6788/py_spy-0.4.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:47cdda4c34d9b6cb01f3aaeceb2e88faf57da880207fe72ff6ff97e9bb6cc8a9", size = 1761955 }, + { url = "https://files.pythonhosted.org/packages/d2/e4/8fbfd219b7f282b80e6b2e74c9197850d2c51db8555705567bb65507b060/py_spy-0.4.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eee3d0bde85ca5cf4f01f012d461180ca76c24835a96f7b5c4ded64eb6a008ab", size = 2059471 }, + { url = "https://files.pythonhosted.org/packages/a7/1d/79a94a5ace810c13b730ce96765ca465c171b4952034f1be7402d8accbc1/py_spy-0.4.0-py2.py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c5f06ffce4c9c98b7fc9f5e67e5e7db591173f1351837633f3f23d9378b1d18a", size = 2067486 }, + { url = "https://files.pythonhosted.org/packages/6d/90/fbbb038f826a83ed15ebc4ae606815d6cad6c5c6399c86c7ab96f6c60817/py_spy-0.4.0-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:87573e64dbfdfc89ba2e0f5e2f525aa84e0299c7eb6454b47ea335fde583a7a0", size = 2141433 }, + { url = "https://files.pythonhosted.org/packages/c9/c1/5e012669ebb687e546dc99fcfc4861ebfcf3a337b7a41af945df23140bb5/py_spy-0.4.0-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8bf2f3702cef367a489faa45177b41a6c31b2a3e5bd78c978d44e29340152f5a", size = 2732951 }, + { url = "https://files.pythonhosted.org/packages/74/8b/dd8490660019a6b0be28d9ffd2bf1db967604b19f3f2719c0e283a16ac7f/py_spy-0.4.0-py2.py3-none-win_amd64.whl", hash = "sha256:77d8f637ade38367d944874776f45b703b7ac5938b1f7be8891f3a5876ddbb96", size = 1810770 }, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/67/6afbf0d507f73c32d21084a79946bfcfca5fbc62a72057e9c23797a737c9/pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c", size = 310028 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/89/bc88a6711935ba795a679ea6ebee07e128050d6382eaa35a0a47c8032bdc/pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd", size = 181537 }, +] + +[[package]] +name = "pydantic" +version = "2.10.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, + { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, + { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, + { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, + { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, + { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, + { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, + { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, + { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, + { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, + { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, + { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, + { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, +] + [[package]] name = "pygments" version = "2.19.1" @@ -452,19 +895,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/27/14af9ef8321f5edc7527e47def2a21d8118c6f329a9342cc61387a0c0599/pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e", size = 14148 }, ] -[[package]] -name = "pytest-xdist" -version = "3.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "execnet" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/41/c4/3c310a19bc1f1e9ef50075582652673ef2bfc8cd62afef9585683821902f/pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d", size = 84060 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/82/1d96bf03ee4c0fdc3c0cbe61470070e659ca78dc0086fb88b66c185e2449/pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7", size = 46108 }, -] - [[package]] name = "pyyaml" version = "6.0.2" @@ -482,6 +912,232 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, ] +[[package]] +name = "ray" +version = "3.0.0.dev0" +source = { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-macosx_11_0_arm64.whl" } +dependencies = [ + { name = "aiosignal" }, + { name = "click" }, + { name = "filelock" }, + { name = "frozenlist" }, + { name = "jsonschema" }, + { name = "msgpack" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "pyyaml" }, + { name = "requests" }, +] +wheels = [ + { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:03192ae8af46a96c7e8e165de8b8b6acd86d08a52d06c4b66e763078e05370e4" }, +] + +[package.optional-dependencies] +client = [ + { name = "grpcio" }, +] +default = [ + { name = "aiohttp" }, + { name = "aiohttp-cors" }, + { name = "colorful" }, + { name = "grpcio" }, + { name = "opencensus" }, + { name = "prometheus-client" }, + { name = "py-spy" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "smart-open" }, + { name = "virtualenv" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiohttp", marker = "extra == 'air'", specifier = ">=3.7" }, + { name = "aiohttp", marker = "extra == 'all'", specifier = ">=3.7" }, + { name = "aiohttp", marker = "extra == 'default'", specifier = ">=3.7" }, + { name = "aiohttp", marker = "extra == 'llm'", specifier = ">=3.7" }, + { name = "aiohttp", marker = "extra == 'serve'", specifier = ">=3.7" }, + { name = "aiohttp", marker = "extra == 'serve-grpc'", specifier = ">=3.7" }, + { name = "aiohttp-cors", marker = "extra == 'air'" }, + { name = "aiohttp-cors", marker = "extra == 'all'" }, + { name = "aiohttp-cors", marker = "extra == 'default'" }, + { name = "aiohttp-cors", marker = "extra == 'llm'" }, + { name = "aiohttp-cors", marker = "extra == 'serve'" }, + { name = "aiohttp-cors", marker = "extra == 'serve-grpc'" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "extra == 'llm'" }, + { name = "boto3", marker = "extra == 'llm'" }, + { name = "click", specifier = ">=7.0" }, + { name = "colorful", marker = "extra == 'air'" }, + { name = "colorful", marker = "extra == 'all'" }, + { name = "colorful", marker = "extra == 'default'" }, + { name = "colorful", marker = "extra == 'llm'" }, + { name = "colorful", marker = "extra == 'serve'" }, + { name = "colorful", marker = "extra == 'serve-grpc'" }, + { name = "cupy-cuda12x", marker = "sys_platform != 'darwin' and extra == 'adag'" }, + { name = "cupy-cuda12x", marker = "sys_platform != 'darwin' and extra == 'all'" }, + { name = "cupy-cuda12x", marker = "sys_platform != 'darwin' and extra == 'cgraph'" }, + { name = "dm-tree", marker = "extra == 'all'" }, + { name = "dm-tree", marker = "extra == 'rllib'" }, + { name = "fastapi", marker = "extra == 'air'" }, + { name = "fastapi", marker = "extra == 'all'" }, + { name = "fastapi", marker = "extra == 'llm'" }, + { name = "fastapi", marker = "extra == 'serve'" }, + { name = "fastapi", marker = "extra == 'serve-grpc'" }, + { name = "filelock" }, + { name = "frozenlist" }, + { name = "fsspec", marker = "extra == 'air'" }, + { name = "fsspec", marker = "extra == 'all'" }, + { name = "fsspec", marker = "extra == 'data'" }, + { name = "fsspec", marker = "extra == 'llm'" }, + { name = "fsspec", marker = "extra == 'rllib'" }, + { name = "fsspec", marker = "extra == 'train'" }, + { name = "fsspec", marker = "extra == 'tune'" }, + { name = "grpcio", marker = "python_full_version >= '3.10' and extra == 'air'", specifier = ">=1.42.0" }, + { name = "grpcio", marker = "python_full_version >= '3.10' and extra == 'all'", specifier = ">=1.42.0" }, + { name = "grpcio", marker = "python_full_version >= '3.10' and extra == 'default'", specifier = ">=1.42.0" }, + { name = "grpcio", marker = "python_full_version >= '3.10' and extra == 'llm'", specifier = ">=1.42.0" }, + { name = "grpcio", marker = "python_full_version >= '3.10' and extra == 'serve'", specifier = ">=1.42.0" }, + { name = "grpcio", marker = "python_full_version >= '3.10' and extra == 'serve-grpc'", specifier = ">=1.42.0" }, + { name = "grpcio", marker = "python_full_version < '3.10' and extra == 'air'", specifier = ">=1.32.0" }, + { name = "grpcio", marker = "python_full_version < '3.10' and extra == 'all'", specifier = ">=1.32.0" }, + { name = "grpcio", marker = "python_full_version < '3.10' and extra == 'default'", specifier = ">=1.32.0" }, + { name = "grpcio", marker = "python_full_version < '3.10' and extra == 'llm'", specifier = ">=1.32.0" }, + { name = "grpcio", marker = "python_full_version < '3.10' and extra == 'serve'", specifier = ">=1.32.0" }, + { name = "grpcio", marker = "python_full_version < '3.10' and extra == 'serve-grpc'", specifier = ">=1.32.0" }, + { name = "grpcio", marker = "sys_platform == 'darwin' and extra == 'all'", specifier = "!=1.56.0" }, + { name = "grpcio", marker = "sys_platform == 'darwin' and extra == 'client'", specifier = "!=1.56.0" }, + { name = "grpcio", marker = "extra == 'all'" }, + { name = "grpcio", marker = "extra == 'client'" }, + { name = "gymnasium", marker = "extra == 'all'", specifier = "==1.0.0" }, + { name = "gymnasium", marker = "extra == 'rllib'", specifier = "==1.0.0" }, + { name = "jsonref", marker = "extra == 'llm'", specifier = ">=1.1.0" }, + { name = "jsonschema" }, + { name = "lz4", marker = "extra == 'all'" }, + { name = "lz4", marker = "extra == 'rllib'" }, + { name = "memray", marker = "sys_platform != 'win32' and extra == 'all'" }, + { name = "memray", marker = "sys_platform != 'win32' and extra == 'observability'" }, + { name = "msgpack", specifier = ">=1.0.0,<2.0.0" }, + { name = "numpy", marker = "extra == 'air'", specifier = ">=1.20" }, + { name = "numpy", marker = "extra == 'all'", specifier = ">=1.20" }, + { name = "numpy", marker = "extra == 'data'", specifier = ">=1.20" }, + { name = "numpy", marker = "extra == 'llm'", specifier = ">=1.20" }, + { name = "opencensus", marker = "extra == 'air'" }, + { name = "opencensus", marker = "extra == 'all'" }, + { name = "opencensus", marker = "extra == 'default'" }, + { name = "opencensus", marker = "extra == 'llm'" }, + { name = "opencensus", marker = "extra == 'serve'" }, + { name = "opencensus", marker = "extra == 'serve-grpc'" }, + { name = "opentelemetry-api", marker = "extra == 'all'" }, + { name = "opentelemetry-api", marker = "extra == 'observability'" }, + { name = "opentelemetry-exporter-otlp", marker = "extra == 'all'" }, + { name = "opentelemetry-exporter-otlp", marker = "extra == 'observability'" }, + { name = "opentelemetry-sdk", marker = "extra == 'all'" }, + { name = "opentelemetry-sdk", marker = "extra == 'observability'" }, + { name = "ormsgpack", marker = "extra == 'all'", specifier = "==1.7.0" }, + { name = "ormsgpack", marker = "extra == 'rllib'", specifier = "==1.7.0" }, + { name = "packaging" }, + { name = "pandas", marker = "extra == 'air'" }, + { name = "pandas", marker = "extra == 'air'", specifier = ">=1.3" }, + { name = "pandas", marker = "extra == 'all'" }, + { name = "pandas", marker = "extra == 'all'", specifier = ">=1.3" }, + { name = "pandas", marker = "extra == 'data'", specifier = ">=1.3" }, + { name = "pandas", marker = "extra == 'llm'", specifier = ">=1.3" }, + { name = "pandas", marker = "extra == 'rllib'" }, + { name = "pandas", marker = "extra == 'train'" }, + { name = "pandas", marker = "extra == 'tune'" }, + { name = "prometheus-client", marker = "extra == 'air'", specifier = ">=0.7.1" }, + { name = "prometheus-client", marker = "extra == 'all'", specifier = ">=0.7.1" }, + { name = "prometheus-client", marker = "extra == 'default'", specifier = ">=0.7.1" }, + { name = "prometheus-client", marker = "extra == 'llm'", specifier = ">=0.7.1" }, + { name = "prometheus-client", marker = "extra == 'serve'", specifier = ">=0.7.1" }, + { name = "prometheus-client", marker = "extra == 'serve-grpc'", specifier = ">=0.7.1" }, + { name = "protobuf", specifier = ">=3.15.3,!=3.19.5" }, + { name = "py-spy", marker = "python_full_version >= '3.12' and extra == 'air'", specifier = ">=0.4.0" }, + { name = "py-spy", marker = "python_full_version >= '3.12' and extra == 'all'", specifier = ">=0.4.0" }, + { name = "py-spy", marker = "python_full_version >= '3.12' and extra == 'default'", specifier = ">=0.4.0" }, + { name = "py-spy", marker = "python_full_version >= '3.12' and extra == 'llm'", specifier = ">=0.4.0" }, + { name = "py-spy", marker = "python_full_version >= '3.12' and extra == 'serve'", specifier = ">=0.4.0" }, + { name = "py-spy", marker = "python_full_version >= '3.12' and extra == 'serve-grpc'", specifier = ">=0.4.0" }, + { name = "py-spy", marker = "python_full_version < '3.12' and extra == 'air'", specifier = ">=0.2.0" }, + { name = "py-spy", marker = "python_full_version < '3.12' and extra == 'all'", specifier = ">=0.2.0" }, + { name = "py-spy", marker = "python_full_version < '3.12' and extra == 'default'", specifier = ">=0.2.0" }, + { name = "py-spy", marker = "python_full_version < '3.12' and extra == 'llm'", specifier = ">=0.2.0" }, + { name = "py-spy", marker = "python_full_version < '3.12' and extra == 'serve'", specifier = ">=0.2.0" }, + { name = "py-spy", marker = "python_full_version < '3.12' and extra == 'serve-grpc'", specifier = ">=0.2.0" }, + { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'air'", specifier = "<18" }, + { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'all'", specifier = "<18" }, + { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'data'", specifier = "<18" }, + { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'llm'", specifier = "<18" }, + { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'rllib'", specifier = "<18" }, + { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'train'", specifier = "<18" }, + { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'tune'", specifier = "<18" }, + { name = "pyarrow", marker = "extra == 'air'", specifier = ">=9.0.0" }, + { name = "pyarrow", marker = "extra == 'all'", specifier = ">=9.0.0" }, + { name = "pyarrow", marker = "extra == 'data'", specifier = ">=9.0.0" }, + { name = "pyarrow", marker = "extra == 'llm'", specifier = ">=9.0.0" }, + { name = "pyarrow", marker = "extra == 'rllib'", specifier = ">=9.0.0" }, + { name = "pyarrow", marker = "extra == 'train'", specifier = ">=9.0.0" }, + { name = "pyarrow", marker = "extra == 'tune'", specifier = ">=9.0.0" }, + { name = "pydantic", marker = "extra == 'air'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, + { name = "pydantic", marker = "extra == 'all'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, + { name = "pydantic", marker = "extra == 'default'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, + { name = "pydantic", marker = "extra == 'llm'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, + { name = "pydantic", marker = "extra == 'serve'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, + { name = "pydantic", marker = "extra == 'serve-grpc'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, + { name = "pydantic", marker = "extra == 'train'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, + { name = "pyopenssl", marker = "extra == 'all'" }, + { name = "pyopenssl", marker = "extra == 'serve-grpc'" }, + { name = "pyyaml" }, + { name = "pyyaml", marker = "extra == 'all'" }, + { name = "pyyaml", marker = "extra == 'rllib'" }, + { name = "requests" }, + { name = "requests", marker = "extra == 'air'" }, + { name = "requests", marker = "extra == 'all'" }, + { name = "requests", marker = "extra == 'default'" }, + { name = "requests", marker = "extra == 'llm'" }, + { name = "requests", marker = "extra == 'rllib'" }, + { name = "requests", marker = "extra == 'serve'" }, + { name = "requests", marker = "extra == 'serve-grpc'" }, + { name = "requests", marker = "extra == 'train'" }, + { name = "requests", marker = "extra == 'tune'" }, + { name = "scipy", marker = "extra == 'all'" }, + { name = "scipy", marker = "extra == 'rllib'" }, + { name = "smart-open", marker = "extra == 'air'" }, + { name = "smart-open", marker = "extra == 'all'" }, + { name = "smart-open", marker = "extra == 'default'" }, + { name = "smart-open", marker = "extra == 'llm'" }, + { name = "smart-open", marker = "extra == 'serve'" }, + { name = "smart-open", marker = "extra == 'serve-grpc'" }, + { name = "starlette", marker = "extra == 'air'" }, + { name = "starlette", marker = "extra == 'all'" }, + { name = "starlette", marker = "extra == 'llm'" }, + { name = "starlette", marker = "extra == 'serve'" }, + { name = "starlette", marker = "extra == 'serve-grpc'" }, + { name = "tensorboardx", marker = "extra == 'air'", specifier = ">=1.9" }, + { name = "tensorboardx", marker = "extra == 'all'", specifier = ">=1.9" }, + { name = "tensorboardx", marker = "extra == 'rllib'", specifier = ">=1.9" }, + { name = "tensorboardx", marker = "extra == 'train'", specifier = ">=1.9" }, + { name = "tensorboardx", marker = "extra == 'tune'", specifier = ">=1.9" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'air'" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'all'" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'llm'" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'serve'" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'serve-grpc'" }, + { name = "virtualenv", marker = "extra == 'air'", specifier = ">=20.0.24,!=20.21.1" }, + { name = "virtualenv", marker = "extra == 'all'", specifier = ">=20.0.24,!=20.21.1" }, + { name = "virtualenv", marker = "extra == 'default'", specifier = ">=20.0.24,!=20.21.1" }, + { name = "virtualenv", marker = "extra == 'llm'", specifier = ">=20.0.24,!=20.21.1" }, + { name = "virtualenv", marker = "extra == 'serve'", specifier = ">=20.0.24,!=20.21.1" }, + { name = "virtualenv", marker = "extra == 'serve-grpc'", specifier = ">=20.0.24,!=20.21.1" }, + { name = "vllm", marker = "extra == 'llm'", specifier = ">=0.7.2" }, + { name = "watchfiles", marker = "extra == 'air'" }, + { name = "watchfiles", marker = "extra == 'all'" }, + { name = "watchfiles", marker = "extra == 'llm'" }, + { name = "watchfiles", marker = "extra == 'serve'" }, + { name = "watchfiles", marker = "extra == 'serve-grpc'" }, +] + [[package]] name = "referencing" version = "0.36.2" @@ -495,6 +1151,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 }, ] +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + [[package]] name = "rich" version = "13.9.4" @@ -542,6 +1213,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f8/30/7ac943f69855c2db77407ae363484b915d861702dbba1aa82d68d57f42be/rpds_py-0.22.3-cp313-cp313t-win_amd64.whl", hash = "sha256:f5cf2a0c2bdadf3791b5c205d55a37a54025c6e18a71c71f82bb536cf9a454bf", size = 233794 }, ] +[[package]] +name = "rsa" +version = "4.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/65/7d973b89c4d2351d7fb232c2e452547ddfa243e93131e7cfa766da627b52/rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21", size = 29711 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/97/fa78e3d2f65c02c8e1268b9aba606569fe97f6c8f7c2d74394553347c145/rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", size = 34315 }, +] + [[package]] name = "ruff" version = "0.9.6" @@ -567,6 +1250,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e8/a8/d71f44b93e3aa86ae232af1f2126ca7b95c0f515ec135462b3e1f351441c/ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a", size = 10177499 }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "smart-open" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/30/1f41c3d3b8cec82024b4b277bfd4e5b18b765ae7279eb9871fa25c503778/smart_open-7.1.0.tar.gz", hash = "sha256:a4f09f84f0f6d3637c6543aca7b5487438877a21360e7368ccf1f704789752ba", size = 72044 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/18/9a8d9f01957aa1f8bbc5676d54c2e33102d247e146c1a3679d3bd5cc2e3a/smart_open-7.1.0-py3-none-any.whl", hash = "sha256:4b8489bb6058196258bafe901730c7db0dcf4f083f316e97269c66f45502055b", size = 61746 }, +] + [[package]] name = "smbus2" version = "0.5.0" @@ -585,6 +1289,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] +[[package]] +name = "urllib3" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, +] + [[package]] name = "virtualenv" version = "20.29.2" @@ -598,3 +1311,64 @@ sdist = { url = "https://files.pythonhosted.org/packages/f1/88/dacc875dd54a8acad wheels = [ { url = "https://files.pythonhosted.org/packages/93/fa/849483d56773ae29740ae70043ad88e068f98a6401aa819b5d6bee604683/virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a", size = 4301478 }, ] + +[[package]] +name = "wrapt" +version = "1.17.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800 }, + { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824 }, + { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920 }, + { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690 }, + { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861 }, + { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174 }, + { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721 }, + { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763 }, + { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585 }, + { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676 }, + { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871 }, + { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312 }, + { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062 }, + { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155 }, + { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471 }, + { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208 }, + { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339 }, + { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232 }, + { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476 }, + { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377 }, + { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986 }, + { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750 }, + { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 }, +] + +[[package]] +name = "yarl" +version = "1.18.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/4b94a8e6d2b51b599516a5cb88e5bc99b4d8d4583e468057eaa29d5f0918/yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", size = 181062 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/c7/c790513d5328a8390be8f47be5d52e141f78b66c6c48f48d241ca6bd5265/yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb", size = 140789 }, + { url = "https://files.pythonhosted.org/packages/30/aa/a2f84e93554a578463e2edaaf2300faa61c8701f0898725842c704ba5444/yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa", size = 94144 }, + { url = "https://files.pythonhosted.org/packages/c6/fc/d68d8f83714b221a85ce7866832cba36d7c04a68fa6a960b908c2c84f325/yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782", size = 91974 }, + { url = "https://files.pythonhosted.org/packages/56/4e/d2563d8323a7e9a414b5b25341b3942af5902a2263d36d20fb17c40411e2/yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0", size = 333587 }, + { url = "https://files.pythonhosted.org/packages/25/c9/cfec0bc0cac8d054be223e9f2c7909d3e8442a856af9dbce7e3442a8ec8d/yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482", size = 344386 }, + { url = "https://files.pythonhosted.org/packages/ab/5d/4c532190113b25f1364d25f4c319322e86232d69175b91f27e3ebc2caf9a/yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186", size = 345421 }, + { url = "https://files.pythonhosted.org/packages/23/d1/6cdd1632da013aa6ba18cee4d750d953104a5e7aac44e249d9410a972bf5/yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58", size = 339384 }, + { url = "https://files.pythonhosted.org/packages/9a/c4/6b3c39bec352e441bd30f432cda6ba51681ab19bb8abe023f0d19777aad1/yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53", size = 326689 }, + { url = "https://files.pythonhosted.org/packages/23/30/07fb088f2eefdc0aa4fc1af4e3ca4eb1a3aadd1ce7d866d74c0f124e6a85/yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2", size = 345453 }, + { url = "https://files.pythonhosted.org/packages/63/09/d54befb48f9cd8eec43797f624ec37783a0266855f4930a91e3d5c7717f8/yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8", size = 341872 }, + { url = "https://files.pythonhosted.org/packages/91/26/fd0ef9bf29dd906a84b59f0cd1281e65b0c3e08c6aa94b57f7d11f593518/yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1", size = 347497 }, + { url = "https://files.pythonhosted.org/packages/d9/b5/14ac7a256d0511b2ac168d50d4b7d744aea1c1aa20c79f620d1059aab8b2/yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a", size = 359981 }, + { url = "https://files.pythonhosted.org/packages/ca/b3/d493221ad5cbd18bc07e642894030437e405e1413c4236dd5db6e46bcec9/yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10", size = 366229 }, + { url = "https://files.pythonhosted.org/packages/04/56/6a3e2a5d9152c56c346df9b8fb8edd2c8888b1e03f96324d457e5cf06d34/yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8", size = 360383 }, + { url = "https://files.pythonhosted.org/packages/fd/b7/4b3c7c7913a278d445cc6284e59b2e62fa25e72758f888b7a7a39eb8423f/yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d", size = 310152 }, + { url = "https://files.pythonhosted.org/packages/f5/d5/688db678e987c3e0fb17867970700b92603cadf36c56e5fb08f23e822a0c/yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c", size = 315723 }, + { url = "https://files.pythonhosted.org/packages/f5/4b/a06e0ec3d155924f77835ed2d167ebd3b211a7b0853da1cf8d8414d784ef/yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", size = 45109 }, +] From 284c894f7778a4b1c51b409688052a63d3877332 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Mon, 24 Feb 2025 17:17:30 -0800 Subject: [PATCH 17/71] remote test exec --- software/hil/dist_plugin.py | 92 ++++++++++++++++++++++++------------- 1 file changed, 60 insertions(+), 32 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index a730e03b..0e475953 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -1,5 +1,7 @@ from dataclasses import dataclass from pathlib import Path +import time +from typing import TypedDict import pytest @@ -8,6 +10,9 @@ import socket +from _pytest.config import _prepareconfig + + @dataclass class RunsOn: hostname: str | None @@ -20,6 +25,13 @@ def check(self, node) -> bool: return self.hostname is None or self.hostname == node.gateway.id +class Event(TypedDict): + event: str + nodeid: str + hostname: str | None + item: object | None + + # TODO: actor pool @ray.remote(resources={"remote": 1}) class Remote: @@ -38,51 +50,64 @@ def __init__( self.test_queue = test_queue self.message_queue = message_queue self.hostname = socket.gethostname() + self.config = _prepareconfig(args, [self]) - from _pytest.config import get_config - - # TODO: is this too early? do we need to call pytest_cmdline_parse? - self.config = get_config(args, None) - - pluginmanager = self.config.pluginmanager - print("pluginmanager:", pluginmanager) - - self.config.pluginmanager.hook.pytest_cmdline_parse( - pluginmanager=pluginmanager, args=args - ) - + # TODO: review + # self.config.option.loadgroup = self.config.getvalue("dist") == "loadgroup" + self.config.option.looponfail = False self.config.option.usepdb = False self.config.option.dist = "no" self.config.option.distload = False self.config.option.numprocesses = None self.config.option.maxprocesses = None + self.config.option.basetemp = Path.cwd() / "dist_tmp" - print("config:", self.config) + # TODO: suppress terminal output + self.config.hook.pytest_cmdline_main(config=self.config) - # self.config.hook.pytest_cmdline_main(config=self.config) + def get_item(self, nodeid: str): + # TODO: build index - print("finished pytest configuration") + for item in self.session.items: + if item.nodeid == nodeid: + return item - # ls - print(list(Path.cwd().glob("*"))) + raise ValueError(f"Item with nodeid {nodeid} not found") + + def process_test(self, nodeid: str): + item = self.get_item(nodeid) + + # TODO: nextitem + self.config.hook.pytest_runtest_protocol(item=item, nextitem=None) + + @pytest.hookimpl(tryfirst=True) + def pytest_runtestloop(self, session: pytest.Session): + self.session = session - async def process_tests(self): # TODO: shutdown signal try: while True: - test = await self.test_queue.get_async(block=True) + test = self.test_queue.get(block=True) print("running test:", test) - self.message_queue.put(f"{self.hostname}: processing test: {test}") + self.process_test(test) except ray.util.queue.Empty: pass - def process_test(self): ... + return True @pytest.hookimpl - def pytest_runtestloop(self, session: pytest.Session): - print("pytest_runtestloop on remote") + def pytest_runtest_logreport(self, report: pytest.TestReport) -> None: + # TODO: typed message + self.message_queue.put( + { + "event": "report", + "nodeid": report.nodeid, + "hostname": self.hostname, + "report": report, + } + ) class DSession: @@ -92,14 +117,11 @@ def __init__(self, config: pytest.Config): if not ray.is_initialized(): # TODO: from config ray.init( + log_to_driver=False, # hide worker output address="ray://192.168.1.199:10001", # namespace="hil", runtime_env={ - # TODO: smarter working dir (relative to __file__, git root, etc) - "working_dir": str(Path.cwd()), - # TODO: doesn't work — use this instead: - # `RAY_RUNTIME_ENV_HOOK=ray._private.runtime_env.uv_runtime_env_hook.hook` - # "py_executable": "uv run --isolated", + # `export RAY_RUNTIME_ENV_HOOK=ray._private.runtime_env.uv_runtime_env_hook.hook` # TODO: exclusions from file? "excludes": [ "**/*.step", @@ -116,9 +138,14 @@ def __init__(self, config: pytest.Config): self.test_queue = ray.util.queue.Queue() self.message_queue = ray.util.queue.Queue() + args = [str(x) for x in config.invocation_params.args or ()] + # TODO: remote per worker for each queue - self.remote1 = Remote.remote(self.test_queue, self.message_queue, config.args) - self.remote1.process_tests.remote() # type: ignore + self.remote1 = Remote.remote(self.test_queue, self.message_queue, args) + # self.remote1.process_tests.remote() # type: ignore + + # TODO: better + time.sleep(10) @pytest.hookimpl(tryfirst=True) def pytest_collection(self, session: pytest.Session): @@ -137,12 +164,13 @@ def pytest_runtestloop(self, session: pytest.Session): # TODO: shutdown handling for item in session.items: - print("enqueuing:", item.nodeid) self.test_queue.put(item.nodeid) while True: try: - print("received message:", self.message_queue.get(timeout=1)) + message = self.message_queue.get(timeout=1, block=True) + report = message["report"] + session.config.hook.pytest_runtest_logreport(report=report) except ray.util.queue.Empty: break From d88fab0eeba55a9bcee98697c0bd56de4e06ff23 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Mon, 24 Feb 2025 18:58:50 -0800 Subject: [PATCH 18/71] process all tests --- software/hil/dist_plugin.py | 55 ++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index 0e475953..7105f428 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -1,6 +1,5 @@ from dataclasses import dataclass from pathlib import Path -import time from typing import TypedDict import pytest @@ -74,11 +73,12 @@ def get_item(self, nodeid: str): raise ValueError(f"Item with nodeid {nodeid} not found") - def process_test(self, nodeid: str): - item = self.get_item(nodeid) + def process_test(self, nodeid_now: str, nodeid_next: str | None): + print("processing: ", [nodeid_now, nodeid_next]) + item_now = self.get_item(nodeid_now) + item_next = self.get_item(nodeid_next) if nodeid_next else None - # TODO: nextitem - self.config.hook.pytest_runtest_protocol(item=item, nextitem=None) + self.config.hook.pytest_runtest_protocol(item=item_now, nextitem=item_next) @pytest.hookimpl(tryfirst=True) def pytest_runtestloop(self, session: pytest.Session): @@ -87,13 +87,24 @@ def pytest_runtestloop(self, session: pytest.Session): # TODO: shutdown signal try: + test_now = self.test_queue.get(block=True, timeout=1) + while True: - test = self.test_queue.get(block=True) - print("running test:", test) + try: + test_next = self.test_queue.get(block=True, timeout=1) + except ray.util.queue.Empty: + # TODO: 'finished' event + test_next = None + + self.process_test(test_now, test_next) - self.process_test(test) + if test_next is None: + break + + test_now = test_next except ray.util.queue.Empty: - pass + # TODO: 'finished' event + return True return True @@ -142,10 +153,6 @@ def __init__(self, config: pytest.Config): # TODO: remote per worker for each queue self.remote1 = Remote.remote(self.test_queue, self.message_queue, args) - # self.remote1.process_tests.remote() # type: ignore - - # TODO: better - time.sleep(10) @pytest.hookimpl(tryfirst=True) def pytest_collection(self, session: pytest.Session): @@ -163,16 +170,26 @@ def pytest_collection(self, session: pytest.Session): def pytest_runtestloop(self, session: pytest.Session): # TODO: shutdown handling - for item in session.items: - self.test_queue.put(item.nodeid) + reports_by_nodeid = {item.nodeid: None for item in session.items} + # TODO: manage queue size + self.test_queue.put_nowait_batch([item.nodeid for item in session.items]) + + # TODO: start/finish events while True: try: - message = self.message_queue.get(timeout=1, block=True) - report = message["report"] - session.config.hook.pytest_runtest_logreport(report=report) + message = self.message_queue.get(block=True, timeout=0.1) + reports_by_nodeid[message["nodeid"]] = message["report"] + session.config.hook.pytest_runtest_logreport(report=message["report"]) except ray.util.queue.Empty: - break + # TODO: what if results aren't coming? + if all(reports_by_nodeid.values()): + break + + # TODO: better error + assert all(reports_by_nodeid.values()), "No test report for: " + ", ".join( + [nodeid for nodeid, report in reports_by_nodeid.items() if report is None] + ) return True From e43a0fe7215517466db026d2142738f48e5fadb2 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Mon, 24 Feb 2025 18:59:29 -0800 Subject: [PATCH 19/71] fix runtime env --- software/hil/dist_plugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index 7105f428..af889c5e 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -61,7 +61,6 @@ def __init__( self.config.option.maxprocesses = None self.config.option.basetemp = Path.cwd() / "dist_tmp" - # TODO: suppress terminal output self.config.hook.pytest_cmdline_main(config=self.config) def get_item(self, nodeid: str): @@ -130,9 +129,11 @@ def __init__(self, config: pytest.Config): ray.init( log_to_driver=False, # hide worker output address="ray://192.168.1.199:10001", - # namespace="hil", runtime_env={ # `export RAY_RUNTIME_ENV_HOOK=ray._private.runtime_env.uv_runtime_env_hook.hook` + # TODO: relative to something? project root? + "working_dir": Path.cwd(), + "py_modules": ["software/hil"], # TODO: auto via uv # TODO: exclusions from file? "excludes": [ "**/*.step", From 83b455874e0f7f6582f6cca38c7a8adbe22d0f73 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Mon, 24 Feb 2025 19:00:14 -0800 Subject: [PATCH 20/71] conditional source for ray --- pyproject.toml | 6 +- uv.lock | 273 +++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 259 insertions(+), 20 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0e726be9..7a7a0370 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,8 +36,10 @@ dev = [ [tool.uv.sources] # TODO: use released version (pending 3.13 support) -ray = { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-macosx_11_0_arm64.whl" } -# ray = { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-manylinux2014_aarch64.whl" } +ray = [ + { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-macosx_11_0_arm64.whl", marker = "sys_platform == 'darwin'" }, + { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-manylinux2014_aarch64.whl", marker = "sys_platform != 'darwin'" }, +] [tool.hatch.build] sources = ["software"] diff --git a/uv.lock b/uv.lock index 0d5f20d0..e6323081 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.13" resolution-markers = [ "sys_platform == 'darwin'", @@ -302,7 +303,8 @@ dependencies = [ { name = "pytest-benchmark" }, { name = "pytest-html" }, { name = "pytest-timeout" }, - { name = "ray", extra = ["client", "default"] }, + { name = "ray", version = "3.0.0.dev0", source = { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-macosx_11_0_arm64.whl" }, extra = ["client", "default"], marker = "sys_platform == 'darwin'" }, + { name = "ray", version = "3.0.0.dev0", source = { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-manylinux2014_aarch64.whl" }, extra = ["client", "default"], marker = "sys_platform != 'darwin'" }, { name = "rich" }, { name = "smbus2" }, ] @@ -325,7 +327,8 @@ requires-dist = [ { name = "pytest-benchmark", specifier = ">=5.1.0" }, { name = "pytest-html", specifier = ">=4.1.1" }, { name = "pytest-timeout", specifier = ">=2.3.1" }, - { name = "ray", extras = ["client", "default"], url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-macosx_11_0_arm64.whl" }, + { name = "ray", extras = ["client", "default"], marker = "sys_platform != 'darwin'", url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-manylinux2014_aarch64.whl" }, + { name = "ray", extras = ["client", "default"], marker = "sys_platform == 'darwin'", url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-macosx_11_0_arm64.whl" }, { name = "rich", specifier = ">=13.9.4" }, { name = "smbus2", specifier = ">=0.5.0" }, ] @@ -916,38 +919,271 @@ wheels = [ name = "ray" version = "3.0.0.dev0" source = { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-macosx_11_0_arm64.whl" } +resolution-markers = [ + "sys_platform == 'darwin'", +] dependencies = [ + { name = "aiosignal", marker = "sys_platform == 'darwin'" }, + { name = "click", marker = "sys_platform == 'darwin'" }, + { name = "filelock", marker = "sys_platform == 'darwin'" }, + { name = "frozenlist", marker = "sys_platform == 'darwin'" }, + { name = "jsonschema", marker = "sys_platform == 'darwin'" }, + { name = "msgpack", marker = "sys_platform == 'darwin'" }, + { name = "packaging", marker = "sys_platform == 'darwin'" }, + { name = "protobuf", marker = "sys_platform == 'darwin'" }, + { name = "pyyaml", marker = "sys_platform == 'darwin'" }, + { name = "requests", marker = "sys_platform == 'darwin'" }, +] +wheels = [ + { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:33853918609d08a7fb8ad04afb1a6e1e616ea054ac87f6a15ccc40319165f918" }, +] + +[package.optional-dependencies] +client = [ + { name = "grpcio", marker = "sys_platform == 'darwin'" }, +] +default = [ + { name = "aiohttp", marker = "sys_platform == 'darwin'" }, + { name = "aiohttp-cors", marker = "sys_platform == 'darwin'" }, + { name = "colorful", marker = "sys_platform == 'darwin'" }, + { name = "grpcio", marker = "sys_platform == 'darwin'" }, + { name = "opencensus", marker = "sys_platform == 'darwin'" }, + { name = "prometheus-client", marker = "sys_platform == 'darwin'" }, + { name = "py-spy", marker = "sys_platform == 'darwin'" }, + { name = "pydantic", marker = "sys_platform == 'darwin'" }, + { name = "requests", marker = "sys_platform == 'darwin'" }, + { name = "smart-open", marker = "sys_platform == 'darwin'" }, + { name = "virtualenv", marker = "sys_platform == 'darwin'" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiohttp", marker = "extra == 'air'", specifier = ">=3.7" }, + { name = "aiohttp", marker = "extra == 'all'", specifier = ">=3.7" }, + { name = "aiohttp", marker = "extra == 'default'", specifier = ">=3.7" }, + { name = "aiohttp", marker = "extra == 'llm'", specifier = ">=3.7" }, + { name = "aiohttp", marker = "extra == 'serve'", specifier = ">=3.7" }, + { name = "aiohttp", marker = "extra == 'serve-grpc'", specifier = ">=3.7" }, + { name = "aiohttp-cors", marker = "extra == 'air'" }, + { name = "aiohttp-cors", marker = "extra == 'all'" }, + { name = "aiohttp-cors", marker = "extra == 'default'" }, + { name = "aiohttp-cors", marker = "extra == 'llm'" }, + { name = "aiohttp-cors", marker = "extra == 'serve'" }, + { name = "aiohttp-cors", marker = "extra == 'serve-grpc'" }, { name = "aiosignal" }, - { name = "click" }, + { name = "async-timeout", marker = "extra == 'llm'" }, + { name = "boto3", marker = "extra == 'llm'" }, + { name = "click", specifier = ">=7.0" }, + { name = "colorful", marker = "extra == 'air'" }, + { name = "colorful", marker = "extra == 'all'" }, + { name = "colorful", marker = "extra == 'default'" }, + { name = "colorful", marker = "extra == 'llm'" }, + { name = "colorful", marker = "extra == 'serve'" }, + { name = "colorful", marker = "extra == 'serve-grpc'" }, + { name = "cupy-cuda12x", marker = "sys_platform != 'darwin' and extra == 'adag'" }, + { name = "cupy-cuda12x", marker = "sys_platform != 'darwin' and extra == 'all'" }, + { name = "cupy-cuda12x", marker = "sys_platform != 'darwin' and extra == 'cgraph'" }, + { name = "dm-tree", marker = "extra == 'all'" }, + { name = "dm-tree", marker = "extra == 'rllib'" }, + { name = "fastapi", marker = "extra == 'air'" }, + { name = "fastapi", marker = "extra == 'all'" }, + { name = "fastapi", marker = "extra == 'llm'" }, + { name = "fastapi", marker = "extra == 'serve'" }, + { name = "fastapi", marker = "extra == 'serve-grpc'" }, { name = "filelock" }, { name = "frozenlist" }, + { name = "fsspec", marker = "extra == 'air'" }, + { name = "fsspec", marker = "extra == 'all'" }, + { name = "fsspec", marker = "extra == 'data'" }, + { name = "fsspec", marker = "extra == 'llm'" }, + { name = "fsspec", marker = "extra == 'rllib'" }, + { name = "fsspec", marker = "extra == 'train'" }, + { name = "fsspec", marker = "extra == 'tune'" }, + { name = "grpcio", marker = "python_full_version >= '3.10' and extra == 'air'", specifier = ">=1.42.0" }, + { name = "grpcio", marker = "python_full_version >= '3.10' and extra == 'all'", specifier = ">=1.42.0" }, + { name = "grpcio", marker = "python_full_version >= '3.10' and extra == 'default'", specifier = ">=1.42.0" }, + { name = "grpcio", marker = "python_full_version >= '3.10' and extra == 'llm'", specifier = ">=1.42.0" }, + { name = "grpcio", marker = "python_full_version >= '3.10' and extra == 'serve'", specifier = ">=1.42.0" }, + { name = "grpcio", marker = "python_full_version >= '3.10' and extra == 'serve-grpc'", specifier = ">=1.42.0" }, + { name = "grpcio", marker = "python_full_version < '3.10' and extra == 'air'", specifier = ">=1.32.0" }, + { name = "grpcio", marker = "python_full_version < '3.10' and extra == 'all'", specifier = ">=1.32.0" }, + { name = "grpcio", marker = "python_full_version < '3.10' and extra == 'default'", specifier = ">=1.32.0" }, + { name = "grpcio", marker = "python_full_version < '3.10' and extra == 'llm'", specifier = ">=1.32.0" }, + { name = "grpcio", marker = "python_full_version < '3.10' and extra == 'serve'", specifier = ">=1.32.0" }, + { name = "grpcio", marker = "python_full_version < '3.10' and extra == 'serve-grpc'", specifier = ">=1.32.0" }, + { name = "grpcio", marker = "sys_platform == 'darwin' and extra == 'all'", specifier = "!=1.56.0" }, + { name = "grpcio", marker = "sys_platform == 'darwin' and extra == 'client'", specifier = "!=1.56.0" }, + { name = "grpcio", marker = "extra == 'all'" }, + { name = "grpcio", marker = "extra == 'client'" }, + { name = "gymnasium", marker = "extra == 'all'", specifier = "==1.0.0" }, + { name = "gymnasium", marker = "extra == 'rllib'", specifier = "==1.0.0" }, + { name = "jsonref", marker = "extra == 'llm'", specifier = ">=1.1.0" }, { name = "jsonschema" }, - { name = "msgpack" }, + { name = "lz4", marker = "extra == 'all'" }, + { name = "lz4", marker = "extra == 'rllib'" }, + { name = "memray", marker = "sys_platform != 'win32' and extra == 'all'" }, + { name = "memray", marker = "sys_platform != 'win32' and extra == 'observability'" }, + { name = "msgpack", specifier = ">=1.0.0,<2.0.0" }, + { name = "numpy", marker = "extra == 'air'", specifier = ">=1.20" }, + { name = "numpy", marker = "extra == 'all'", specifier = ">=1.20" }, + { name = "numpy", marker = "extra == 'data'", specifier = ">=1.20" }, + { name = "numpy", marker = "extra == 'llm'", specifier = ">=1.20" }, + { name = "opencensus", marker = "extra == 'air'" }, + { name = "opencensus", marker = "extra == 'all'" }, + { name = "opencensus", marker = "extra == 'default'" }, + { name = "opencensus", marker = "extra == 'llm'" }, + { name = "opencensus", marker = "extra == 'serve'" }, + { name = "opencensus", marker = "extra == 'serve-grpc'" }, + { name = "opentelemetry-api", marker = "extra == 'all'" }, + { name = "opentelemetry-api", marker = "extra == 'observability'" }, + { name = "opentelemetry-exporter-otlp", marker = "extra == 'all'" }, + { name = "opentelemetry-exporter-otlp", marker = "extra == 'observability'" }, + { name = "opentelemetry-sdk", marker = "extra == 'all'" }, + { name = "opentelemetry-sdk", marker = "extra == 'observability'" }, + { name = "ormsgpack", marker = "extra == 'all'", specifier = "==1.7.0" }, + { name = "ormsgpack", marker = "extra == 'rllib'", specifier = "==1.7.0" }, { name = "packaging" }, - { name = "protobuf" }, + { name = "pandas", marker = "extra == 'air'" }, + { name = "pandas", marker = "extra == 'air'", specifier = ">=1.3" }, + { name = "pandas", marker = "extra == 'all'" }, + { name = "pandas", marker = "extra == 'all'", specifier = ">=1.3" }, + { name = "pandas", marker = "extra == 'data'", specifier = ">=1.3" }, + { name = "pandas", marker = "extra == 'llm'", specifier = ">=1.3" }, + { name = "pandas", marker = "extra == 'rllib'" }, + { name = "pandas", marker = "extra == 'train'" }, + { name = "pandas", marker = "extra == 'tune'" }, + { name = "prometheus-client", marker = "extra == 'air'", specifier = ">=0.7.1" }, + { name = "prometheus-client", marker = "extra == 'all'", specifier = ">=0.7.1" }, + { name = "prometheus-client", marker = "extra == 'default'", specifier = ">=0.7.1" }, + { name = "prometheus-client", marker = "extra == 'llm'", specifier = ">=0.7.1" }, + { name = "prometheus-client", marker = "extra == 'serve'", specifier = ">=0.7.1" }, + { name = "prometheus-client", marker = "extra == 'serve-grpc'", specifier = ">=0.7.1" }, + { name = "protobuf", specifier = ">=3.15.3,!=3.19.5" }, + { name = "py-spy", marker = "python_full_version >= '3.12' and extra == 'air'", specifier = ">=0.4.0" }, + { name = "py-spy", marker = "python_full_version >= '3.12' and extra == 'all'", specifier = ">=0.4.0" }, + { name = "py-spy", marker = "python_full_version >= '3.12' and extra == 'default'", specifier = ">=0.4.0" }, + { name = "py-spy", marker = "python_full_version >= '3.12' and extra == 'llm'", specifier = ">=0.4.0" }, + { name = "py-spy", marker = "python_full_version >= '3.12' and extra == 'serve'", specifier = ">=0.4.0" }, + { name = "py-spy", marker = "python_full_version >= '3.12' and extra == 'serve-grpc'", specifier = ">=0.4.0" }, + { name = "py-spy", marker = "python_full_version < '3.12' and extra == 'air'", specifier = ">=0.2.0" }, + { name = "py-spy", marker = "python_full_version < '3.12' and extra == 'all'", specifier = ">=0.2.0" }, + { name = "py-spy", marker = "python_full_version < '3.12' and extra == 'default'", specifier = ">=0.2.0" }, + { name = "py-spy", marker = "python_full_version < '3.12' and extra == 'llm'", specifier = ">=0.2.0" }, + { name = "py-spy", marker = "python_full_version < '3.12' and extra == 'serve'", specifier = ">=0.2.0" }, + { name = "py-spy", marker = "python_full_version < '3.12' and extra == 'serve-grpc'", specifier = ">=0.2.0" }, + { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'air'", specifier = "<18" }, + { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'all'", specifier = "<18" }, + { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'data'", specifier = "<18" }, + { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'llm'", specifier = "<18" }, + { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'rllib'", specifier = "<18" }, + { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'train'", specifier = "<18" }, + { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'tune'", specifier = "<18" }, + { name = "pyarrow", marker = "extra == 'air'", specifier = ">=9.0.0" }, + { name = "pyarrow", marker = "extra == 'all'", specifier = ">=9.0.0" }, + { name = "pyarrow", marker = "extra == 'data'", specifier = ">=9.0.0" }, + { name = "pyarrow", marker = "extra == 'llm'", specifier = ">=9.0.0" }, + { name = "pyarrow", marker = "extra == 'rllib'", specifier = ">=9.0.0" }, + { name = "pyarrow", marker = "extra == 'train'", specifier = ">=9.0.0" }, + { name = "pyarrow", marker = "extra == 'tune'", specifier = ">=9.0.0" }, + { name = "pydantic", marker = "extra == 'air'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, + { name = "pydantic", marker = "extra == 'all'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, + { name = "pydantic", marker = "extra == 'default'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, + { name = "pydantic", marker = "extra == 'llm'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, + { name = "pydantic", marker = "extra == 'serve'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, + { name = "pydantic", marker = "extra == 'serve-grpc'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, + { name = "pydantic", marker = "extra == 'train'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, + { name = "pyopenssl", marker = "extra == 'all'" }, + { name = "pyopenssl", marker = "extra == 'serve-grpc'" }, { name = "pyyaml" }, + { name = "pyyaml", marker = "extra == 'all'" }, + { name = "pyyaml", marker = "extra == 'rllib'" }, { name = "requests" }, + { name = "requests", marker = "extra == 'air'" }, + { name = "requests", marker = "extra == 'all'" }, + { name = "requests", marker = "extra == 'default'" }, + { name = "requests", marker = "extra == 'llm'" }, + { name = "requests", marker = "extra == 'rllib'" }, + { name = "requests", marker = "extra == 'serve'" }, + { name = "requests", marker = "extra == 'serve-grpc'" }, + { name = "requests", marker = "extra == 'train'" }, + { name = "requests", marker = "extra == 'tune'" }, + { name = "scipy", marker = "extra == 'all'" }, + { name = "scipy", marker = "extra == 'rllib'" }, + { name = "smart-open", marker = "extra == 'air'" }, + { name = "smart-open", marker = "extra == 'all'" }, + { name = "smart-open", marker = "extra == 'default'" }, + { name = "smart-open", marker = "extra == 'llm'" }, + { name = "smart-open", marker = "extra == 'serve'" }, + { name = "smart-open", marker = "extra == 'serve-grpc'" }, + { name = "starlette", marker = "extra == 'air'" }, + { name = "starlette", marker = "extra == 'all'" }, + { name = "starlette", marker = "extra == 'llm'" }, + { name = "starlette", marker = "extra == 'serve'" }, + { name = "starlette", marker = "extra == 'serve-grpc'" }, + { name = "tensorboardx", marker = "extra == 'air'", specifier = ">=1.9" }, + { name = "tensorboardx", marker = "extra == 'all'", specifier = ">=1.9" }, + { name = "tensorboardx", marker = "extra == 'rllib'", specifier = ">=1.9" }, + { name = "tensorboardx", marker = "extra == 'train'", specifier = ">=1.9" }, + { name = "tensorboardx", marker = "extra == 'tune'", specifier = ">=1.9" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'air'" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'all'" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'llm'" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'serve'" }, + { name = "uvicorn", extras = ["standard"], marker = "extra == 'serve-grpc'" }, + { name = "virtualenv", marker = "extra == 'air'", specifier = ">=20.0.24,!=20.21.1" }, + { name = "virtualenv", marker = "extra == 'all'", specifier = ">=20.0.24,!=20.21.1" }, + { name = "virtualenv", marker = "extra == 'default'", specifier = ">=20.0.24,!=20.21.1" }, + { name = "virtualenv", marker = "extra == 'llm'", specifier = ">=20.0.24,!=20.21.1" }, + { name = "virtualenv", marker = "extra == 'serve'", specifier = ">=20.0.24,!=20.21.1" }, + { name = "virtualenv", marker = "extra == 'serve-grpc'", specifier = ">=20.0.24,!=20.21.1" }, + { name = "vllm", marker = "extra == 'llm'", specifier = ">=0.7.2" }, + { name = "watchfiles", marker = "extra == 'air'" }, + { name = "watchfiles", marker = "extra == 'all'" }, + { name = "watchfiles", marker = "extra == 'llm'" }, + { name = "watchfiles", marker = "extra == 'serve'" }, + { name = "watchfiles", marker = "extra == 'serve-grpc'" }, +] +provides-extras = ["adag", "air", "all", "cgraph", "client", "data", "default", "llm", "observability", "rllib", "serve", "serve-grpc", "train", "tune"] + +[[package]] +name = "ray" +version = "3.0.0.dev0" +source = { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-manylinux2014_aarch64.whl" } +resolution-markers = [ + "sys_platform != 'darwin'", +] +dependencies = [ + { name = "aiosignal", marker = "sys_platform != 'darwin'" }, + { name = "click", marker = "sys_platform != 'darwin'" }, + { name = "filelock", marker = "sys_platform != 'darwin'" }, + { name = "frozenlist", marker = "sys_platform != 'darwin'" }, + { name = "jsonschema", marker = "sys_platform != 'darwin'" }, + { name = "msgpack", marker = "sys_platform != 'darwin'" }, + { name = "packaging", marker = "sys_platform != 'darwin'" }, + { name = "protobuf", marker = "sys_platform != 'darwin'" }, + { name = "pyyaml", marker = "sys_platform != 'darwin'" }, + { name = "requests", marker = "sys_platform != 'darwin'" }, ] wheels = [ - { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:03192ae8af46a96c7e8e165de8b8b6acd86d08a52d06c4b66e763078e05370e4" }, + { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:671205d4302dc18cf06aa1c633287346e3327c000b699a882e9b756998b99341" }, ] [package.optional-dependencies] client = [ - { name = "grpcio" }, + { name = "grpcio", marker = "sys_platform != 'darwin'" }, ] default = [ - { name = "aiohttp" }, - { name = "aiohttp-cors" }, - { name = "colorful" }, - { name = "grpcio" }, - { name = "opencensus" }, - { name = "prometheus-client" }, - { name = "py-spy" }, - { name = "pydantic" }, - { name = "requests" }, - { name = "smart-open" }, - { name = "virtualenv" }, + { name = "aiohttp", marker = "sys_platform != 'darwin'" }, + { name = "aiohttp-cors", marker = "sys_platform != 'darwin'" }, + { name = "colorful", marker = "sys_platform != 'darwin'" }, + { name = "grpcio", marker = "sys_platform != 'darwin'" }, + { name = "opencensus", marker = "sys_platform != 'darwin'" }, + { name = "prometheus-client", marker = "sys_platform != 'darwin'" }, + { name = "py-spy", marker = "sys_platform != 'darwin'" }, + { name = "pydantic", marker = "sys_platform != 'darwin'" }, + { name = "requests", marker = "sys_platform != 'darwin'" }, + { name = "smart-open", marker = "sys_platform != 'darwin'" }, + { name = "virtualenv", marker = "sys_platform != 'darwin'" }, ] [package.metadata] @@ -1137,6 +1373,7 @@ requires-dist = [ { name = "watchfiles", marker = "extra == 'serve'" }, { name = "watchfiles", marker = "extra == 'serve-grpc'" }, ] +provides-extras = ["cgraph", "client", "data", "default", "observability", "serve", "tune", "adag", "serve-grpc", "rllib", "train", "air", "all", "llm"] [[package]] name = "referencing" From 3f1a1ae2671e7b9893c7e41296a6a4f265e94cc1 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Mon, 24 Feb 2025 20:59:40 -0800 Subject: [PATCH 21/71] structured events --- software/hil/dist_plugin.py | 96 +++++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 40 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index af889c5e..19966b25 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -1,6 +1,5 @@ from dataclasses import dataclass from pathlib import Path -from typing import TypedDict import pytest @@ -24,11 +23,23 @@ def check(self, node) -> bool: return self.hostname is None or self.hostname == node.gateway.id -class Event(TypedDict): - event: str - nodeid: str - hostname: str | None - item: object | None +NodeId = str + + +class Events: + @dataclass + class Start: + hostname: str | None + + @dataclass + class Finish: + hostname: str | None + + @dataclass + class Report: + hostname: str | None + nodeid: NodeId + report: pytest.TestReport # TODO: actor pool @@ -73,7 +84,6 @@ def get_item(self, nodeid: str): raise ValueError(f"Item with nodeid {nodeid} not found") def process_test(self, nodeid_now: str, nodeid_next: str | None): - print("processing: ", [nodeid_now, nodeid_next]) item_now = self.get_item(nodeid_now) item_next = self.get_item(nodeid_next) if nodeid_next else None @@ -83,40 +93,32 @@ def process_test(self, nodeid_now: str, nodeid_next: str | None): def pytest_runtestloop(self, session: pytest.Session): self.session = session - # TODO: shutdown signal + self.message_queue.put(Events.Start(hostname=self.hostname)) - try: - test_now = self.test_queue.get(block=True, timeout=1) + # should be long enough to populate the queue + test_now = self.test_queue.get(block=True, timeout=30) - while True: - try: - test_next = self.test_queue.get(block=True, timeout=1) - except ray.util.queue.Empty: - # TODO: 'finished' event - test_next = None + while True: + try: + test_next = self.test_queue.get_nowait() + # TODO: shutdown signal + except ray.util.queue.Empty: + test_next = None - self.process_test(test_now, test_next) + self.process_test(test_now, test_next) - if test_next is None: - break + if test_next is None: + break - test_now = test_next - except ray.util.queue.Empty: - # TODO: 'finished' event - return True + test_now = test_next + self.message_queue.put(Events.Finish(hostname=self.hostname)) return True @pytest.hookimpl def pytest_runtest_logreport(self, report: pytest.TestReport) -> None: - # TODO: typed message self.message_queue.put( - { - "event": "report", - "nodeid": report.nodeid, - "hostname": self.hostname, - "report": report, - } + Events.Report(hostname=self.hostname, nodeid=report.nodeid, report=report) ) @@ -171,26 +173,40 @@ def pytest_collection(self, session: pytest.Session): def pytest_runtestloop(self, session: pytest.Session): # TODO: shutdown handling - reports_by_nodeid = {item.nodeid: None for item in session.items} + reports_by_nodeid: dict[NodeId, pytest.TestReport | None] = { + item.nodeid: None for item in session.items + } # TODO: manage queue size self.test_queue.put_nowait_batch([item.nodeid for item in session.items]) - # TODO: start/finish events + started = False + finished = False + while True: try: - message = self.message_queue.get(block=True, timeout=0.1) - reports_by_nodeid[message["nodeid"]] = message["report"] - session.config.hook.pytest_runtest_logreport(report=message["report"]) + message = self.message_queue.get_nowait() + match message: + case Events.Report(_, nodeid, report): + reports_by_nodeid[nodeid] = report + session.config.hook.pytest_runtest_logreport(report=report) + case Events.Start(_): + started = True + case Events.Finish(_): + assert started + finished = True + case _: + print(f"unknown message: {message}") except ray.util.queue.Empty: - # TODO: what if results aren't coming? - if all(reports_by_nodeid.values()): + message = None + if finished: break # TODO: better error - assert all(reports_by_nodeid.values()), "No test report for: " + ", ".join( - [nodeid for nodeid, report in reports_by_nodeid.items() if report is None] - ) + missing_reports = [ + nodeid for nodeid, report in reports_by_nodeid.items() if report is None + ] + assert not missing_reports, f"No test report for: {', '.join(missing_reports)}" return True From e72d9bdfbcc7ad001e37b208570ed5c958496cc7 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Mon, 24 Feb 2025 21:05:38 -0800 Subject: [PATCH 22/71] better item lookup --- software/hil/dist_plugin.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index 19966b25..cfd675ad 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -74,24 +74,15 @@ def __init__( self.config.hook.pytest_cmdline_main(config=self.config) - def get_item(self, nodeid: str): - # TODO: build index - - for item in self.session.items: - if item.nodeid == nodeid: - return item - - raise ValueError(f"Item with nodeid {nodeid} not found") - def process_test(self, nodeid_now: str, nodeid_next: str | None): - item_now = self.get_item(nodeid_now) - item_next = self.get_item(nodeid_next) if nodeid_next else None - + item_now = self._items_by_nodeid[nodeid_now] + item_next = self._items_by_nodeid[nodeid_next] if nodeid_next else None self.config.hook.pytest_runtest_protocol(item=item_now, nextitem=item_next) @pytest.hookimpl(tryfirst=True) def pytest_runtestloop(self, session: pytest.Session): self.session = session + self._items_by_nodeid = {item.nodeid: item for item in session.items} self.message_queue.put(Events.Start(hostname=self.hostname)) From d2ef0e9db2f94fa6f90ef7677e4baa268ab0f77a Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Tue, 25 Feb 2025 12:13:28 -0800 Subject: [PATCH 23/71] Add basic server deps --- pyproject.toml | 4 ++ uv.lock | 171 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 20b47957..d08c3356 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,9 +10,11 @@ readme = "README.md" requires-python = ">=3.13" dependencies = [ "altair>=5.5.0", + "fastapi>=0.115.8", "numpy>=2.2.2", "pathvalidate>=3.2.3", "polars>=1.22.0", + "pydantic>=2.10.6", "pytest>=8.3.4", "pytest-asyncio>=0.25.3", "pytest-benchmark>=5.1.0", @@ -21,6 +23,8 @@ dependencies = [ "pytest-xdist>=3.6.1", "rich>=13.9.4", "smbus2>=0.5.0", + "typer>=0.15.1", + "uvicorn>=0.34.0", ] [project.entry-points."pytest11"] diff --git a/uv.lock b/uv.lock index d151f215..fe78366c 100644 --- a/uv.lock +++ b/uv.lock @@ -17,6 +17,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/f3/0b6ced594e51cc95d8c1fc1640d3623770d01e4969d29c0bd09945fafefa/altair-5.5.0-py3-none-any.whl", hash = "sha256:91a310b926508d560fe0148d02a194f38b824122641ef528113d029fcd129f8c", size = 731200 }, ] +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, +] + [[package]] name = "attrs" version = "25.1.0" @@ -35,6 +57,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, ] +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -62,6 +96,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612 }, ] +[[package]] +name = "fastapi" +version = "0.115.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/b2/5a5dc4affdb6661dea100324e19a7721d5dc524b464fe8e366c093fd7d87/fastapi-0.115.8.tar.gz", hash = "sha256:0ce9111231720190473e222cdf0f07f7206ad7e53ea02beb1d2dc36e2f0741e9", size = 295403 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/7d/2d6ce181d7a5f51dedb8c06206cbf0ec026a99bf145edd309f9e17c3282f/fastapi-0.115.8-py3-none-any.whl", hash = "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf", size = 94814 }, +] + [[package]] name = "filelock" version = "3.17.0" @@ -71,15 +119,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164 }, ] +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + [[package]] name = "hil" version = "0.1.0" source = { editable = "." } dependencies = [ { name = "altair" }, + { name = "fastapi" }, { name = "numpy" }, { name = "pathvalidate" }, { name = "polars" }, + { name = "pydantic" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-benchmark" }, @@ -88,6 +147,8 @@ dependencies = [ { name = "pytest-xdist" }, { name = "rich" }, { name = "smbus2" }, + { name = "typer" }, + { name = "uvicorn" }, ] [package.dev-dependencies] @@ -100,9 +161,11 @@ dev = [ [package.metadata] requires-dist = [ { name = "altair", specifier = ">=5.5.0" }, + { name = "fastapi", specifier = ">=0.115.8" }, { name = "numpy", specifier = ">=2.2.2" }, { name = "pathvalidate", specifier = ">=3.2.3" }, { name = "polars", specifier = ">=1.22.0" }, + { name = "pydantic", specifier = ">=2.10.6" }, { name = "pytest", specifier = ">=8.3.4" }, { name = "pytest-asyncio", specifier = ">=0.25.3" }, { name = "pytest-benchmark", specifier = ">=5.1.0" }, @@ -111,6 +174,8 @@ requires-dist = [ { name = "pytest-xdist", specifier = ">=3.6.1" }, { name = "rich", specifier = ">=13.9.4" }, { name = "smbus2", specifier = ">=0.5.0" }, + { name = "typer", specifier = ">=0.15.1" }, + { name = "uvicorn", specifier = ">=0.34.0" }, ] [package.metadata.requires-dev] @@ -129,6 +194,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/00/1fd4a117c6c93f2dcc5b7edaeaf53ea45332ef966429be566ca16c2beb94/identify-2.6.7-py2.py3-none-any.whl", hash = "sha256:155931cb617a401807b09ecec6635d6c692d180090a1cedca8ef7d58ba5b6aa0", size = 99097 }, ] +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + [[package]] name = "iniconfig" version = "2.0.0" @@ -347,6 +421,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335 }, ] +[[package]] +name = "pydantic" +version = "2.10.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, + { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, + { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, + { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, + { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, + { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, + { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, + { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, + { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, + { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, + { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, + { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, + { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, +] + [[package]] name = "pygments" version = "2.19.1" @@ -567,6 +680,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e8/a8/d71f44b93e3aa86ae232af1f2126ca7b95c0f515ec135462b3e1f351441c/ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a", size = 10177499 }, ] +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, +] + [[package]] name = "smbus2" version = "0.5.0" @@ -576,6 +698,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/85/9f/2235ba9001e3c29fc342eeb222104420bcb7bac51555f0c034376a744075/smbus2-0.5.0-py2.py3-none-any.whl", hash = "sha256:1a15c3b9fa69357beb038cc0b5d37939702f8bfde1ddc89ca9f17d8461dbe949", size = 11527 }, ] +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "starlette" +version = "0.45.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/fb/2984a686808b89a6781526129a4b51266f678b2d2b97ab2d325e56116df8/starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f", size = 2574076 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/61/f2b52e107b1fc8944b33ef56bf6ac4ebbe16d91b94d2b87ce013bf63fb84/starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d", size = 71507 }, +] + +[[package]] +name = "typer" +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/dca7b219718afd37a0068f4f2530a727c2b74a8b6e8e0c0080a4c0de4fcd/typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a", size = 99789 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/cc/0a838ba5ca64dc832aa43f727bd586309846b0ffb2ce52422543e6075e8a/typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", size = 44908 }, +] + [[package]] name = "typing-extensions" version = "4.12.2" @@ -585,6 +743,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] +[[package]] +name = "uvicorn" +version = "0.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, +] + [[package]] name = "virtualenv" version = "20.29.2" From 9fad65a22060fcae4516b4d47b9c08c781b357c0 Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Tue, 25 Feb 2025 12:14:44 -0800 Subject: [PATCH 24/71] Add cspell --- cspell.json | 123 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 cspell.json diff --git a/cspell.json b/cspell.json new file mode 100644 index 00000000..124c3c1e --- /dev/null +++ b/cspell.json @@ -0,0 +1,123 @@ +{ + "version": "0.2", + "language": "en", + "words": [ + "activ", + "addinivalue", + "aiosmbus", + "aiter", + "altair", + "anext", + "apwr", + "asyncio", + "atopile", + "axolotl", + "backplane", + "baudrate", + "caplog", + "capybara", + "cellsim", + "changzhou", + "coms", + "configdict", + "conftest", + "coro", + "datasheet", + "dbvr", + "derpy", + "disch", + "dtparam", + "dtypes", + "eeprom", + "elec", + "esda", + "faebryk", + "funcs", + "gethostname", + "getnode", + "hanrun", + "hctl", + "hilignore", + "hirose", + "hookimpl", + "hookwrapper", + "hotplug", + "ichg", + "idrlr", + "ilim", + "infcd", + "initialconftest", + "khertz", + "kicad", + "kohm", + "lcsc", + "levelname", + "lmin", + "lookback", + "lowpass", + "makereport", + "moduleinterface", + "mosfet", + "mosi", + "napowderly", + "narwhal", + "nodeid", + "opamp", + "ovuv", + "partno", + "pdrvr", + "pinmap", + "polars", + "preinit", + "pullup", + "pytest", + "quokka", + "rdwr", + "rohs", + "rootdir", + "rter", + "runtest", + "sclk", + "setp", + "setpoint", + "shottkey", + "shou", + "smbus", + "snooty", + "squiggly", + "srss", + "stemma", + "stusb", + "subclassing", + "swpa", + "timedelta", + "toplevel", + "truecolor", + "uart", + "usbpd", + "usbs", + "vbus", + "vdiv", + "venv", + "vout", + "vreg", + "vsink", + "vsys", + "wacky", + "wiggly", + "xlvddcr", + "youtai", + "zesty", + "zhongshan", + "zippy" + ], + "ignorePaths": [ + "node_modules/**", + "venv/**", + ".git/**", + "*.pyc", + "__pycache__/**", + ".venv/**", + "build/**" + ] +} From a6ffd06fd20871d754ed1a17a822bda9a7918807 Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Tue, 25 Feb 2025 14:23:43 -0800 Subject: [PATCH 25/71] Init server --- software/httpdist_server/README.md | 13 ++ software/httpdist_server/__init__.py | 0 software/httpdist_server/server.py | 290 +++++++++++++++++++++++++++ 3 files changed, 303 insertions(+) create mode 100644 software/httpdist_server/README.md create mode 100644 software/httpdist_server/__init__.py create mode 100644 software/httpdist_server/server.py diff --git a/software/httpdist_server/README.md b/software/httpdist_server/README.md new file mode 100644 index 00000000..970142cc --- /dev/null +++ b/software/httpdist_server/README.md @@ -0,0 +1,13 @@ +# pytest-httpdist + +This plugin is used to distribute tests over multiple workers, via an HTTP server. + +## Parts + +1. One `pytest` plugin running as a "requester" +2. Greater than one `pytest` plugin running as "worker" nodes +3. A server that proxies requested tests from the "requester" node to appropriate "worker" nodes +4. A bootstrap application whose job is to spool up worker nodes. It's responsible for: + - Polling the server for sessions + - Starting a worker for a session with a command passed to it by the server + - Killing a worker if a session is aborted diff --git a/software/httpdist_server/__init__.py b/software/httpdist_server/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/software/httpdist_server/server.py b/software/httpdist_server/server.py new file mode 100644 index 00000000..23e49a74 --- /dev/null +++ b/software/httpdist_server/server.py @@ -0,0 +1,290 @@ +from dataclasses import dataclass +from datetime import datetime, timedelta +from typing import Literal +import uuid +from attrs import field +import fastapi +import pytest +import uvicorn +from pydantic import BaseModel +from fastapi import UploadFile +import cloudpickle + +app = fastapi.FastAPI() + + +@dataclass +class Worker: + worker_id: str # Typically the worker's mac address + pet_name: str + tags: set[str] + + last_heartbeat: datetime | None = None + + @property + def is_alive(self) -> bool: + return ( + self.last_heartbeat is not None + and self.last_heartbeat > datetime.now() - timedelta(seconds=30) + ) + + def heartbeat(self): + self.last_heartbeat = datetime.now() + + +@dataclass +class Session: + @dataclass + class Test: + worker_requirements: set[str] + node_id: str + + status: Literal["pending", "running", "finished"] = "pending" + assigned_worker: Worker | None = None + report: bytes | None = None + + session_id: str + + state: Literal["setup", "running", "stopped"] = "setup" + tests: dict[str, Test] = field(factory=dict) + env: UploadFile | None = None + + async def stop(self): + self.state = "stopped" + if self.env is not None: + await self.env.close() + + +# TODO: stick this in a database or something +sessions: dict[str, Session] = {} +workers: list[Worker] = [] + + +@app.get("/") +async def root(): + return {"message": "Hello World!"} + + +class GetSessionResponse(BaseModel): + """Response to a request to start a new session""" + + session_id: str + + +@app.get("/get-session") +async def start_session(): + session_id = str(uuid.uuid4()) + sessions[session_id] = Session(session_id=session_id) + return GetSessionResponse(session_id=session_id) + + +@app.post("/session/{session_id}/stop") +async def stop_session(session_id: str): + if session_id not in sessions: + raise fastapi.HTTPException(status_code=404, detail="Session not found") + + await sessions[session_id].stop() + + return {"message": "Stop signal sent"} + + +@app.post("/session/{session_id}/env") +async def upload_session_env(session_id: str, env: UploadFile): + """Upload environment file for a test session""" + if session_id not in sessions: + raise fastapi.HTTPException(status_code=404, detail="Session not found") + + # TODO: Store this in a proper storage backend + sessions[session_id].env = env + + return {"message": "Environment file uploaded successfully"} + + +class PostSessionsTestsRequest(BaseModel): + class Test(BaseModel): + worker_requirements: set[str] + node_id: str + + tests: list[Test] + + +@app.post("/session/{session_id}/tests") +async def add_tests(session_id: str, request: PostSessionsTestsRequest): + """Add a test to a session""" + if session_id not in sessions: + raise fastapi.HTTPException(status_code=404, detail="Session not found") + + unprocessable_tags = set() + worker_tags = {tag for worker in workers for tag in worker.tags} + for test in request.tests: + for tag in test.worker_requirements: + if tag not in worker_tags: + unprocessable_tags.add(tag) + + sessions[session_id].tests[test.node_id] = Session.Test( + test.worker_requirements, test.node_id + ) + + if unprocessable_tags: + raise fastapi.HTTPException( + status_code=422, + detail=f"Tests with unprocessable tags: {unprocessable_tags}", + ) + + return {"message": "Tests added successfully"} + + +class GetSessionTestsResponse(BaseModel): + test_status: dict[str, Literal["pending", "running", "finished"]] + + +@app.get("/session/{session_id}/finished-tests") +async def get_finished_tests(session_id: str) -> GetSessionTestsResponse: + """Get the tests for a session""" + if session_id not in sessions: + raise fastapi.HTTPException(status_code=404, detail="Session not found") + + return GetSessionTestsResponse( + test_status={ + test.node_id: test.status + for test in sessions[session_id].tests.values() + if test.status == "finished" + } + ) + + +class TestReport(bytes): + @staticmethod + def from_report(report: pytest.TestReport) -> "TestReport": + return TestReport(cloudpickle.dumps(report)) + + def as_report(self) -> pytest.TestReport: + return cloudpickle.loads(self) + + +class PostWorkerRegisterRequest(BaseModel): + worker_id: str + pet_name: str + tags: list[str] + + +@app.post("/worker/{worker_id}/heartbeat") +async def heartbeat(worker_id: str): + """Heartbeat for a worker""" + for worker in workers: + if worker.worker_id == worker_id: + worker.heartbeat() + return {"message": "Heartbeat received"} + + raise fastapi.HTTPException(status_code=404, detail="Worker not found") + + +class PostWorkerSessionTestReportRequest(BaseModel): + report: TestReport + + +@app.post("/worker/session/{session_id}/test/{test_id}/report") +async def post_test_report( + session_id: str, test_id: str, request: PostWorkerSessionTestReportRequest +): + """Upload the result for a test""" + if session_id not in sessions: + raise fastapi.HTTPException(status_code=404, detail="Session not found") + + if test_id not in sessions[session_id].tests: + raise fastapi.HTTPException(status_code=404, detail="Test not found") + + test = sessions[session_id].tests[test_id] + test.report = request.report + test.status = "finished" + + return {"message": "Test result uploaded successfully"} + + +@app.get("/session/{session_id}/test/{test_id}/report") +async def get_test_report(session_id: str, test_id: str) -> TestReport: + """Get the report for a test""" + if session_id not in sessions: + raise fastapi.HTTPException(status_code=404, detail="Session not found") + + if test_id not in sessions[session_id].tests: + raise fastapi.HTTPException(status_code=404, detail="Test not found") + + test = sessions[session_id].tests[test_id] + if test.report is None: + raise fastapi.HTTPException(status_code=404, detail="Test report not found") + + return TestReport(test.report) + + +@app.get("/worker/{worker_id}/get-session") +async def get_session(worker_id: str) -> str | None: + """Get the session for a worker""" + for worker in workers: + if worker.worker_id == worker_id: + for session in sessions.values(): + if session.state == "running": + if any( + test.worker_requirements.issubset(worker.tags) + for test in session.tests.values() + ): + return session.session_id + + return None + + raise fastapi.HTTPException(status_code=404, detail="Worker not found") + + +@app.get("/worker/session/{session_id}/env") +async def get_session_env(session_id: str): + """Get the environment for a session""" + if session_id not in sessions: + raise fastapi.HTTPException(status_code=404, detail="Session not found") + + return sessions[session_id].env + + +class GetWorkerSessionTestsResponse(BaseModel): + action: Literal["run", "stop"] + test_now: str | None + test_next: str | None + + +@app.get("/worker/{worker_id}/session/{session_id}/tests") +async def get_session_tests(worker_id: str, session_id: str): + """Get the tests for a session""" + if session_id not in sessions: + raise fastapi.HTTPException(status_code=404, detail="Session not found") + + if sessions[session_id].state == "setup": + sessions[session_id].state = "running" + elif sessions[session_id].state == "stopped": + return GetWorkerSessionTestsResponse( + action="stop", + test_now=None, + test_next=None, + ) + + for worker in workers: + if worker.worker_id == worker_id: + break + else: + raise fastapi.HTTPException(status_code=404, detail="Worker not found") + + worker_testable: list[str] = [] + for test in sessions[session_id].tests.values(): + if test.status == "pending" and test.worker_requirements.issubset(worker.tags): + worker_testable.append(test.node_id) + + if len(worker_testable) >= 2: + break + + return GetWorkerSessionTestsResponse( + action="run" if len(worker_testable) > 0 else "stop", + test_now=worker_testable[0] if len(worker_testable) > 0 else None, + test_next=worker_testable[1] if len(worker_testable) > 1 else None, + ) + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) From 828855141b52e0c3841c312ab3542cc0b25f9848 Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Tue, 25 Feb 2025 14:29:15 -0800 Subject: [PATCH 26/71] Add cloudpickle and multipart files --- pyproject.toml | 2 ++ uv.lock | 29 ++++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 00ecd4d5..0b5433de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ readme = "README.md" requires-python = ">=3.13" dependencies = [ "altair>=5.5.0", + "cloudpickle>=3.1.1", "fastapi>=0.115.8", "numpy>=2.2.2", "pathvalidate>=3.2.3", @@ -21,6 +22,7 @@ dependencies = [ "pytest-html>=4.1.1", "pytest-timeout>=2.3.1", "ray[client,default]", + "python-multipart>=0.0.20", "rich>=13.9.4", "smbus2>=0.5.0", "typer>=0.15.1", diff --git a/uv.lock b/uv.lock index 6921530c..aace1c5a 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,4 @@ version = 1 -revision = 1 requires-python = ">=3.13" resolution-markers = [ "sys_platform == 'darwin'", @@ -180,6 +179,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, ] +[[package]] +name = "cloudpickle" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/39/069100b84d7418bc358d81669d5748efb14b9cceacd2f9c75f550424132f/cloudpickle-3.1.1.tar.gz", hash = "sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64", size = 22113 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/e8/64c37fadfc2816a7701fa8a6ed8d87327c7d54eacfbfb6edab14a2f2be75/cloudpickle-3.1.1-py3-none-any.whl", hash = "sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e", size = 20992 }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -331,6 +339,7 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "altair" }, + { name = "cloudpickle" }, { name = "fastapi" }, { name = "numpy" }, { name = "pathvalidate" }, @@ -341,6 +350,7 @@ dependencies = [ { name = "pytest-benchmark" }, { name = "pytest-html" }, { name = "pytest-timeout" }, + { name = "python-multipart" }, { name = "ray", version = "3.0.0.dev0", source = { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-macosx_11_0_arm64.whl" }, extra = ["client", "default"], marker = "sys_platform == 'darwin'" }, { name = "ray", version = "3.0.0.dev0", source = { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-manylinux2014_aarch64.whl" }, extra = ["client", "default"], marker = "sys_platform != 'darwin'" }, { name = "rich" }, @@ -359,6 +369,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "altair", specifier = ">=5.5.0" }, + { name = "cloudpickle", specifier = ">=3.1.1" }, { name = "fastapi", specifier = ">=0.115.8" }, { name = "numpy", specifier = ">=2.2.2" }, { name = "pathvalidate", specifier = ">=3.2.3" }, @@ -369,6 +380,7 @@ requires-dist = [ { name = "pytest-benchmark", specifier = ">=5.1.0" }, { name = "pytest-html", specifier = ">=4.1.1" }, { name = "pytest-timeout", specifier = ">=2.3.1" }, + { name = "python-multipart", specifier = ">=0.0.20" }, { name = "ray", extras = ["client", "default"], marker = "sys_platform != 'darwin'", url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-manylinux2014_aarch64.whl" }, { name = "ray", extras = ["client", "default"], marker = "sys_platform == 'darwin'", url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-macosx_11_0_arm64.whl" }, { name = "rich", specifier = ">=13.9.4" }, @@ -942,6 +954,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/27/14af9ef8321f5edc7527e47def2a21d8118c6f329a9342cc61387a0c0599/pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e", size = 14148 }, ] +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -979,7 +1000,7 @@ dependencies = [ { name = "requests", marker = "sys_platform == 'darwin'" }, ] wheels = [ - { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6db7a884f524d8bddecfed2af1eb3baa9ff9e97e7f01e603a29ab5d7e4b5a1f4" }, + { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4830e139d6855b51de6be677c3a5d8f4bb9bc789069a053b6bba1b4c74117da6" }, ] [package.optional-dependencies] @@ -1187,7 +1208,6 @@ requires-dist = [ { name = "watchfiles", marker = "extra == 'serve'" }, { name = "watchfiles", marker = "extra == 'serve-grpc'" }, ] -provides-extras = ["adag", "air", "all", "cgraph", "client", "data", "default", "llm", "observability", "rllib", "serve", "serve-grpc", "train", "tune"] [[package]] name = "ray" @@ -1209,7 +1229,7 @@ dependencies = [ { name = "requests", marker = "sys_platform != 'darwin'" }, ] wheels = [ - { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:43f3993ef55e7cac733eb8b318dc18310fed7470748fecaf0e8f979bad3743fa" }, + { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:344b7d5bcf505799b6c7a40484dc2883b9f6a1b3db44dbc42606be07860db214" }, ] [package.optional-dependencies] @@ -1417,7 +1437,6 @@ requires-dist = [ { name = "watchfiles", marker = "extra == 'serve'" }, { name = "watchfiles", marker = "extra == 'serve-grpc'" }, ] -provides-extras = ["cgraph", "client", "data", "default", "observability", "serve", "tune", "adag", "serve-grpc", "rllib", "train", "air", "all", "llm"] [[package]] name = "referencing" From ae6e2afdd2dd21a05149b1af1c2ec00de9453ca4 Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Tue, 25 Feb 2025 14:32:22 -0800 Subject: [PATCH 27/71] Split models and application server --- software/httpdist_server/models.py | 48 ++++++++++++++++++++++++++ software/httpdist_server/server.py | 54 +++++++----------------------- 2 files changed, 60 insertions(+), 42 deletions(-) create mode 100644 software/httpdist_server/models.py diff --git a/software/httpdist_server/models.py b/software/httpdist_server/models.py new file mode 100644 index 00000000..b5d14958 --- /dev/null +++ b/software/httpdist_server/models.py @@ -0,0 +1,48 @@ +import cloudpickle +from pydantic import BaseModel +from typing import Literal + +import pytest + + +class GetSessionResponse(BaseModel): + """Response to a request to start a new session""" + + session_id: str + + +class PostSessionsTestsRequest(BaseModel): + class Test(BaseModel): + worker_requirements: set[str] + node_id: str + + tests: list[Test] + + +class GetSessionTestsResponse(BaseModel): + test_status: dict[str, Literal["pending", "running", "finished"]] + + +class TestReport(bytes): + @staticmethod + def from_report(report: pytest.TestReport) -> "TestReport": + return TestReport(cloudpickle.dumps(report)) + + def as_report(self) -> pytest.TestReport: + return cloudpickle.loads(self) + + +class PostWorkerRegisterRequest(BaseModel): + worker_id: str + pet_name: str + tags: list[str] + + +class PostWorkerSessionTestReportRequest(BaseModel): + report: TestReport + + +class GetWorkerSessionTestsResponse(BaseModel): + action: Literal["run", "stop"] + test_now: str | None + test_next: str | None diff --git a/software/httpdist_server/server.py b/software/httpdist_server/server.py index 23e49a74..ae1ea7db 100644 --- a/software/httpdist_server/server.py +++ b/software/httpdist_server/server.py @@ -1,14 +1,21 @@ +import uuid from dataclasses import dataclass from datetime import datetime, timedelta from typing import Literal -import uuid -from attrs import field + import fastapi -import pytest import uvicorn -from pydantic import BaseModel +from attrs import field from fastapi import UploadFile -import cloudpickle +from pydantic import BaseModel + +from httpdist_server.models import ( + GetSessionTestsResponse, + GetWorkerSessionTestsResponse, + PostSessionsTestsRequest, + PostWorkerSessionTestReportRequest, + TestReport, +) app = fastapi.FastAPI() @@ -100,14 +107,6 @@ async def upload_session_env(session_id: str, env: UploadFile): return {"message": "Environment file uploaded successfully"} -class PostSessionsTestsRequest(BaseModel): - class Test(BaseModel): - worker_requirements: set[str] - node_id: str - - tests: list[Test] - - @app.post("/session/{session_id}/tests") async def add_tests(session_id: str, request: PostSessionsTestsRequest): """Add a test to a session""" @@ -134,10 +133,6 @@ async def add_tests(session_id: str, request: PostSessionsTestsRequest): return {"message": "Tests added successfully"} -class GetSessionTestsResponse(BaseModel): - test_status: dict[str, Literal["pending", "running", "finished"]] - - @app.get("/session/{session_id}/finished-tests") async def get_finished_tests(session_id: str) -> GetSessionTestsResponse: """Get the tests for a session""" @@ -153,21 +148,6 @@ async def get_finished_tests(session_id: str) -> GetSessionTestsResponse: ) -class TestReport(bytes): - @staticmethod - def from_report(report: pytest.TestReport) -> "TestReport": - return TestReport(cloudpickle.dumps(report)) - - def as_report(self) -> pytest.TestReport: - return cloudpickle.loads(self) - - -class PostWorkerRegisterRequest(BaseModel): - worker_id: str - pet_name: str - tags: list[str] - - @app.post("/worker/{worker_id}/heartbeat") async def heartbeat(worker_id: str): """Heartbeat for a worker""" @@ -179,10 +159,6 @@ async def heartbeat(worker_id: str): raise fastapi.HTTPException(status_code=404, detail="Worker not found") -class PostWorkerSessionTestReportRequest(BaseModel): - report: TestReport - - @app.post("/worker/session/{session_id}/test/{test_id}/report") async def post_test_report( session_id: str, test_id: str, request: PostWorkerSessionTestReportRequest @@ -244,12 +220,6 @@ async def get_session_env(session_id: str): return sessions[session_id].env -class GetWorkerSessionTestsResponse(BaseModel): - action: Literal["run", "stop"] - test_now: str | None - test_next: str | None - - @app.get("/worker/{worker_id}/session/{session_id}/tests") async def get_session_tests(worker_id: str, session_id: str): """Get the tests for a session""" From b8d5fa80a6f1130c93f2739cc45688714277f763 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 25 Feb 2025 14:34:11 -0800 Subject: [PATCH 28/71] ray -> http (wip) --- software/hil/dist_plugin.py | 231 ++++++++++++++++++++---------------- 1 file changed, 128 insertions(+), 103 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index cfd675ad..6a63dcb6 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -1,14 +1,12 @@ from dataclasses import dataclass +from enum import StrEnum, auto from pathlib import Path +import time import pytest -import ray -import ray.util.queue -import socket - -from _pytest.config import _prepareconfig +PLUGIN_NAME = "dist" @dataclass @@ -26,6 +24,14 @@ def check(self, node) -> bool: NodeId = str +class TestStatus(StrEnum): + # FIXME + Passed = auto() + Failed = auto() + Skipped = auto() + Error = auto() + + class Events: @dataclass class Start: @@ -42,25 +48,22 @@ class Report: report: pytest.TestReport -# TODO: actor pool -@ray.remote(resources={"remote": 1}) -class Remote: +class EndOfSession(Exception): + pass + + +class Worker: """ - Runs on remote node. + Runs on worker node once test session is started. - Sets up pytest test loop. Receives test nodes to run. + - retrieves active test session from server + - polls for allocated tests + - executes tests and reports results + - uploads artifacts at end of session """ - def __init__( - self, - test_queue: ray.util.queue.Queue, - message_queue: ray.util.queue.Queue, - args: list[str], - ): - self.test_queue = test_queue - self.message_queue = message_queue - self.hostname = socket.gethostname() - self.config = _prepareconfig(args, [self]) + def __init__(self, config: pytest.Config): + self.config = config # TODO: review # self.config.option.loadgroup = self.config.getvalue("dist") == "loadgroup" @@ -72,81 +75,118 @@ def __init__( self.config.option.maxprocesses = None self.config.option.basetemp = Path.cwd() / "dist_tmp" - self.config.hook.pytest_cmdline_main(config=self.config) - def process_test(self, nodeid_now: str, nodeid_next: str | None): item_now = self._items_by_nodeid[nodeid_now] item_next = self._items_by_nodeid[nodeid_next] if nodeid_next else None self.config.hook.pytest_runtest_protocol(item=item_now, nextitem=item_next) + def signal_ready(self): ... + + def signal_done(self): ... + + def fetch_work(self) -> tuple[NodeId, NodeId | None]: ... + + def report_result(self, nodeid: NodeId, report: pytest.TestReport): ... + + def upload_artifacts(self): ... + @pytest.hookimpl(tryfirst=True) def pytest_runtestloop(self, session: pytest.Session): self.session = session self._items_by_nodeid = {item.nodeid: item for item in session.items} - self.message_queue.put(Events.Start(hostname=self.hostname)) - - # should be long enough to populate the queue - test_now = self.test_queue.get(block=True, timeout=30) + self.signal_ready() while True: try: - test_next = self.test_queue.get_nowait() - # TODO: shutdown signal - except ray.util.queue.Empty: - test_next = None - - self.process_test(test_now, test_next) - - if test_next is None: + nodeid_now, nodeid_next = self.fetch_work() + self.process_test(nodeid_now, nodeid_next) + except EndOfSession: break - test_now = test_next + self.signal_done() - self.message_queue.put(Events.Finish(hostname=self.hostname)) return True @pytest.hookimpl def pytest_runtest_logreport(self, report: pytest.TestReport) -> None: - self.message_queue.put( - Events.Report(hostname=self.hostname, nodeid=report.nodeid, report=report) - ) + self.report_result(report.nodeid, report) + + @pytest.hookimpl + def pytest_sessionfinish(self, session: pytest.Session, exitstatus: int): + self.upload_artifacts() + + +class TestResults: + nodeids: set[NodeId] + reports: dict[NodeId, pytest.TestReport] + + def __init__(self, nodeids: set[NodeId]): + self.nodeids = nodeids + self.reports = {} + + @property + def all_done(self) -> bool: + return len(self.reports) == len(self.nodeids) + + def add(self, nodeid: NodeId, report: pytest.TestReport): + if nodeid not in self.nodeids: + raise ValueError(f"Unknown nodeid: {nodeid}") + + if self.reports[nodeid] is not None: + raise ValueError(f"Test result already set for {nodeid}") + + self.reports[nodeid] = report -class DSession: +class Client: + """ + Runs on test client. + + - gets test session from server + - uploads runtime env + - send collected tests to server + - receives test results from server + - downloads artifacts + """ + runs_on_key = pytest.StashKey[dict[str, list[RunsOn]]]() + results: TestResults + statuses: dict[NodeId, TestStatus] def __init__(self, config: pytest.Config): - if not ray.is_initialized(): - # TODO: from config - ray.init( - log_to_driver=False, # hide worker output - address="ray://192.168.1.199:10001", - runtime_env={ - # `export RAY_RUNTIME_ENV_HOOK=ray._private.runtime_env.uv_runtime_env_hook.hook` - # TODO: relative to something? project root? - "working_dir": Path.cwd(), - "py_modules": ["software/hil"], # TODO: auto via uv - # TODO: exclusions from file? - "excludes": [ - "**/*.step", - "**/*.wrl", - "**/*.kicad_pcb", - "**/*.kicad_pro", - "**/*.kicad_sch", - "**/fp-lib-table", - ], - }, - ) - - # TODO: queue per resource type - self.test_queue = ray.util.queue.Queue() - self.message_queue = ray.util.queue.Queue() - - args = [str(x) for x in config.invocation_params.args or ()] - - # TODO: remote per worker for each queue - self.remote1 = Remote.remote(self.test_queue, self.message_queue, args) + self.config = config + + def submit_env(self): ... + + def submit_tests(self, session: pytest.Session): ... + + def fetch_results(self) -> list[pytest.TestReport]: + # TOOD: fetch new statuses + new_statuses: dict[NodeId, TestStatus] = {} + new_reports: list[pytest.TestReport] = [] + + for nodeid in new_statuses.keys() - self.statuses.keys(): + new_report = self.fetch_report(nodeid) + self.results.add(nodeid, new_report) + new_reports.append(new_report) + + self.statuses = new_statuses + + return new_reports + + def fetch_report(self, nodeid: NodeId) -> pytest.TestReport: ... + + def download_artifacts(self): ... + + @pytest.hookimpl + def pytest_sessionstart(self, session: pytest.Session): + # TODO: get session + self.submit_env() + + @pytest.hookimpl + def pytest_sessionfinish(self, session: pytest.Session, exitstatus: int): + self.download_artifacts() @pytest.hookimpl(tryfirst=True) def pytest_collection(self, session: pytest.Session): @@ -164,45 +204,30 @@ def pytest_collection(self, session: pytest.Session): def pytest_runtestloop(self, session: pytest.Session): # TODO: shutdown handling - reports_by_nodeid: dict[NodeId, pytest.TestReport | None] = { - item.nodeid: None for item in session.items - } - - # TODO: manage queue size - self.test_queue.put_nowait_batch([item.nodeid for item in session.items]) + self.submit_tests(session) - started = False - finished = False + self.results = TestResults(set(item.nodeid for item in session.items)) while True: - try: - message = self.message_queue.get_nowait() - match message: - case Events.Report(_, nodeid, report): - reports_by_nodeid[nodeid] = report - session.config.hook.pytest_runtest_logreport(report=report) - case Events.Start(_): - started = True - case Events.Finish(_): - assert started - finished = True - case _: - print(f"unknown message: {message}") - except ray.util.queue.Empty: - message = None - if finished: - break - - # TODO: better error - missing_reports = [ - nodeid for nodeid, report in reports_by_nodeid.items() if report is None - ] - assert not missing_reports, f"No test report for: {', '.join(missing_reports)}" + new_reports = self.fetch_results() + for report in new_reports: + session.config.hook.pytest_runtest_logreport(report=report) + + if self.results.all_done: + break + + time.sleep(1) # FIXME return True @pytest.hookimpl(trylast=True) def pytest_configure(config): - session = DSession(config) - config.pluginmanager.register(session, "dist") + is_worker = config.getoption("worker") + session = Worker(config) if is_worker else Client(config) + config.pluginmanager.register(session, PLUGIN_NAME) + + +@pytest.hookimpl +def pytest_addoption(parser, pluginmanager): + parser.addoption("--worker", help="Run as a worker node", default=False) From 1901fb9a6560b4caef64fc1ba8e5e7e88c977b39 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 25 Feb 2025 14:58:29 -0800 Subject: [PATCH 29/71] init api client --- pyproject.toml | 3 ++- software/hil/dist_plugin.py | 12 ++++++++++++ uv.lock | 35 ++++++++++++++++++++++++++++++++++- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0b5433de..85e13f2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,6 @@ requires-python = ">=3.13" dependencies = [ "altair>=5.5.0", "cloudpickle>=3.1.1", - "fastapi>=0.115.8", "numpy>=2.2.2", "pathvalidate>=3.2.3", "polars>=1.22.0", @@ -27,6 +26,8 @@ dependencies = [ "smbus2>=0.5.0", "typer>=0.15.1", "uvicorn>=0.34.0", + "httpx>=0.28.1", + "fastapi>=0.115.8", ] [project.entry-points."pytest11"] diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index 6a63dcb6..081be6f2 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -3,6 +3,7 @@ from pathlib import Path import time +import httpx import pytest @@ -52,6 +53,15 @@ class EndOfSession(Exception): pass +class ApiClient: + # FIXME + API_URL = "http://localhost:8000" + + def __init__(self, config: pytest.Config): + self.config = config + self._client = httpx.AsyncClient() + + class Worker: """ Runs on worker node once test session is started. @@ -64,6 +74,7 @@ class Worker: def __init__(self, config: pytest.Config): self.config = config + self.api_client = ApiClient(config) # TODO: review # self.config.option.loadgroup = self.config.getvalue("dist") == "loadgroup" @@ -156,6 +167,7 @@ class Client: def __init__(self, config: pytest.Config): self.config = config + self.api_client = ApiClient(config) def submit_env(self): ... diff --git a/uv.lock b/uv.lock index aace1c5a..f3534c47 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.13" resolution-markers = [ "sys_platform == 'darwin'", @@ -341,6 +342,7 @@ dependencies = [ { name = "altair" }, { name = "cloudpickle" }, { name = "fastapi" }, + { name = "httpx" }, { name = "numpy" }, { name = "pathvalidate" }, { name = "polars" }, @@ -371,6 +373,7 @@ requires-dist = [ { name = "altair", specifier = ">=5.5.0" }, { name = "cloudpickle", specifier = ">=3.1.1" }, { name = "fastapi", specifier = ">=0.115.8" }, + { name = "httpx", specifier = ">=0.28.1" }, { name = "numpy", specifier = ">=2.2.2" }, { name = "pathvalidate", specifier = ">=3.2.3" }, { name = "polars", specifier = ">=1.22.0" }, @@ -396,6 +399,34 @@ dev = [ { name = "ruff", specifier = ">=0.9.6" }, ] +[[package]] +name = "httpcore" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + [[package]] name = "identify" version = "2.6.8" @@ -1000,7 +1031,7 @@ dependencies = [ { name = "requests", marker = "sys_platform == 'darwin'" }, ] wheels = [ - { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4830e139d6855b51de6be677c3a5d8f4bb9bc789069a053b6bba1b4c74117da6" }, + { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0aa165e6bf9736c98284296d7f088aea0a71456ed9fd18a57ff872f6aefed7f2" }, ] [package.optional-dependencies] @@ -1208,6 +1239,7 @@ requires-dist = [ { name = "watchfiles", marker = "extra == 'serve'" }, { name = "watchfiles", marker = "extra == 'serve-grpc'" }, ] +provides-extras = ["adag", "air", "all", "cgraph", "client", "data", "default", "llm", "observability", "rllib", "serve", "serve-grpc", "train", "tune"] [[package]] name = "ray" @@ -1437,6 +1469,7 @@ requires-dist = [ { name = "watchfiles", marker = "extra == 'serve'" }, { name = "watchfiles", marker = "extra == 'serve-grpc'" }, ] +provides-extras = ["cgraph", "client", "data", "default", "observability", "serve", "tune", "adag", "serve-grpc", "rllib", "train", "air", "all", "llm"] [[package]] name = "referencing" From 851178613fd14919320f5990e3916a96d9ea6761 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 25 Feb 2025 15:04:40 -0800 Subject: [PATCH 30/71] fastapi[standard] --- pyproject.toml | 2 +- uv.lock | 161 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 160 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 85e13f2b..5c9b0629 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ "typer>=0.15.1", "uvicorn>=0.34.0", "httpx>=0.28.1", - "fastapi>=0.115.8", + "fastapi[standard]>=0.115.8", ] [project.entry-points."pytest11"] diff --git a/uv.lock b/uv.lock index f3534c47..90c2480d 100644 --- a/uv.lock +++ b/uv.lock @@ -219,6 +219,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, ] +[[package]] +name = "dnspython" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632 }, +] + +[[package]] +name = "email-validator" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521 }, +] + [[package]] name = "fastapi" version = "0.115.8" @@ -233,6 +255,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/7d/2d6ce181d7a5f51dedb8c06206cbf0ec026a99bf145edd309f9e17c3282f/fastapi-0.115.8-py3-none-any.whl", hash = "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf", size = 94814 }, ] +[package.optional-dependencies] +standard = [ + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard"] }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "python-multipart" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich-toolkit" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/73/82a5831fbbf8ed75905bacf5b2d9d3dfd6f04d6968b29fe6f72a5ae9ceb1/fastapi_cli-0.0.7.tar.gz", hash = "sha256:02b3b65956f526412515907a0793c9094abd4bfb5457b389f645b0ea6ba3605e", size = 16753 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/e6/5daefc851b514ce2287d8f5d358ae4341089185f78f3217a69d0ce3a390c/fastapi_cli-0.0.7-py3-none-any.whl", hash = "sha256:d549368ff584b2804336c61f192d86ddea080c11255f375959627911944804f4", size = 10705 }, +] + +[package.optional-dependencies] +standard = [ + { name = "uvicorn", extra = ["standard"] }, +] + [[package]] name = "filelock" version = "3.17.0" @@ -341,7 +392,7 @@ source = { editable = "." } dependencies = [ { name = "altair" }, { name = "cloudpickle" }, - { name = "fastapi" }, + { name = "fastapi", extra = ["standard"] }, { name = "httpx" }, { name = "numpy" }, { name = "pathvalidate" }, @@ -372,7 +423,7 @@ dev = [ requires-dist = [ { name = "altair", specifier = ">=5.5.0" }, { name = "cloudpickle", specifier = ">=3.1.1" }, - { name = "fastapi", specifier = ">=0.115.8" }, + { name = "fastapi", extras = ["standard"], specifier = ">=0.115.8" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "numpy", specifier = ">=2.2.2" }, { name = "pathvalidate", specifier = ">=3.2.3" }, @@ -412,6 +463,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, ] +[[package]] +name = "httptools" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214 }, + { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431 }, + { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121 }, + { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805 }, + { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858 }, + { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042 }, + { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682 }, +] + [[package]] name = "httpx" version = "0.28.1" @@ -985,6 +1051,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/27/14af9ef8321f5edc7527e47def2a21d8118c6f329a9342cc61387a0c0599/pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e", size = 14148 }, ] +[[package]] +name = "python-dotenv" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, +] + [[package]] name = "python-multipart" version = "0.0.20" @@ -1512,6 +1587,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, ] +[[package]] +name = "rich-toolkit" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/8a/71cfbf6bf6257ea785d1f030c22468f763eea1b3e5417620f2ba9abd6dca/rich_toolkit-0.13.2.tar.gz", hash = "sha256:fea92557530de7c28f121cbed572ad93d9e0ddc60c3ca643f1b831f2f56b95d3", size = 72288 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/1b/1c2f43af46456050b27810a7a013af8a7e12bc545a0cdc00eb0df55eb769/rich_toolkit-0.13.2-py3-none-any.whl", hash = "sha256:f3f6c583e5283298a2f7dbd3c65aca18b7f818ad96174113ab5bec0b0e35ed61", size = 13566 }, +] + [[package]] name = "rpds-py" version = "0.23.1" @@ -1689,6 +1778,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, ] +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123 }, + { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325 }, + { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806 }, + { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068 }, + { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428 }, + { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 }, +] + [[package]] name = "virtualenv" version = "20.29.2" @@ -1703,6 +1817,49 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/93/fa/849483d56773ae29740ae70043ad88e068f98a6401aa819b5d6bee604683/virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a", size = 4301478 }, ] +[[package]] +name = "watchfiles" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/26/c705fc77d0a9ecdb9b66f1e2976d95b81df3cae518967431e7dbf9b5e219/watchfiles-1.0.4.tar.gz", hash = "sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205", size = 94625 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/98/f03efabec64b5b1fa58c0daab25c68ef815b0f320e54adcacd0d6847c339/watchfiles-1.0.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8012bd820c380c3d3db8435e8cf7592260257b378b649154a7948a663b5f84e9", size = 390954 }, + { url = "https://files.pythonhosted.org/packages/16/09/4dd49ba0a32a45813debe5fb3897955541351ee8142f586303b271a02b40/watchfiles-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa216f87594f951c17511efe5912808dfcc4befa464ab17c98d387830ce07b60", size = 381133 }, + { url = "https://files.pythonhosted.org/packages/76/59/5aa6fc93553cd8d8ee75c6247763d77c02631aed21551a97d94998bf1dae/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c9953cf85529c05b24705639ffa390f78c26449e15ec34d5339e8108c7c407", size = 449516 }, + { url = "https://files.pythonhosted.org/packages/4c/aa/df4b6fe14b6317290b91335b23c96b488d365d65549587434817e06895ea/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf684aa9bba4cd95ecb62c822a56de54e3ae0598c1a7f2065d51e24637a3c5d", size = 454820 }, + { url = "https://files.pythonhosted.org/packages/5e/71/185f8672f1094ce48af33252c73e39b48be93b761273872d9312087245f6/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f44a39aee3cbb9b825285ff979ab887a25c5d336e5ec3574f1506a4671556a8d", size = 481550 }, + { url = "https://files.pythonhosted.org/packages/85/d7/50ebba2c426ef1a5cb17f02158222911a2e005d401caf5d911bfca58f4c4/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38320582736922be8c865d46520c043bff350956dfc9fbaee3b2df4e1740a4b", size = 518647 }, + { url = "https://files.pythonhosted.org/packages/f0/7a/4c009342e393c545d68987e8010b937f72f47937731225b2b29b7231428f/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39f4914548b818540ef21fd22447a63e7be6e24b43a70f7642d21f1e73371590", size = 497547 }, + { url = "https://files.pythonhosted.org/packages/0f/7c/1cf50b35412d5c72d63b2bf9a4fffee2e1549a245924960dd087eb6a6de4/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f12969a3765909cf5dc1e50b2436eb2c0e676a3c75773ab8cc3aa6175c16e902", size = 452179 }, + { url = "https://files.pythonhosted.org/packages/d6/a9/3db1410e1c1413735a9a472380e4f431ad9a9e81711cda2aaf02b7f62693/watchfiles-1.0.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0986902677a1a5e6212d0c49b319aad9cc48da4bd967f86a11bde96ad9676ca1", size = 614125 }, + { url = "https://files.pythonhosted.org/packages/f2/e1/0025d365cf6248c4d1ee4c3d2e3d373bdd3f6aff78ba4298f97b4fad2740/watchfiles-1.0.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:308ac265c56f936636e3b0e3f59e059a40003c655228c131e1ad439957592303", size = 611911 }, + { url = "https://files.pythonhosted.org/packages/55/55/035838277d8c98fc8c917ac9beeb0cd6c59d675dc2421df5f9fcf44a0070/watchfiles-1.0.4-cp313-cp313-win32.whl", hash = "sha256:aee397456a29b492c20fda2d8961e1ffb266223625346ace14e4b6d861ba9c80", size = 271152 }, + { url = "https://files.pythonhosted.org/packages/f0/e5/96b8e55271685ddbadc50ce8bc53aa2dff278fb7ac4c2e473df890def2dc/watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc", size = 285216 }, +] + +[[package]] +name = "websockets" +version = "15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/7a/8bc4d15af7ff30f7ba34f9a172063bfcee9f5001d7cef04bee800a658f33/websockets-15.0.tar.gz", hash = "sha256:ca36151289a15b39d8d683fd8b7abbe26fc50be311066c5f8dcf3cb8cee107ab", size = 175574 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/23/be28dc1023707ac51768f848d28a946443041a348ee3a54abdf9f6283372/websockets-15.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d2244d8ab24374bed366f9ff206e2619345f9cd7fe79aad5225f53faac28b6b1", size = 174714 }, + { url = "https://files.pythonhosted.org/packages/8f/ff/02b5e9fbb078e7666bf3d25c18c69b499747a12f3e7f2776063ef3fb7061/websockets-15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3a302241fbe825a3e4fe07666a2ab513edfdc6d43ce24b79691b45115273b5e7", size = 172374 }, + { url = "https://files.pythonhosted.org/packages/8e/61/901c8d4698e0477eff4c3c664d53f898b601fa83af4ce81946650ec2a4cb/websockets-15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:10552fed076757a70ba2c18edcbc601c7637b30cdfe8c24b65171e824c7d6081", size = 172605 }, + { url = "https://files.pythonhosted.org/packages/d2/4b/dc47601a80dff317aecf8da7b4ab278d11d3494b2c373b493e4887561f90/websockets-15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c53f97032b87a406044a1c33d1e9290cc38b117a8062e8a8b285175d7e2f99c9", size = 182380 }, + { url = "https://files.pythonhosted.org/packages/83/f7/b155d2b38f05ed47a0b8de1c9ea245fcd7fc625d89f35a37eccba34b42de/websockets-15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1caf951110ca757b8ad9c4974f5cac7b8413004d2f29707e4d03a65d54cedf2b", size = 181325 }, + { url = "https://files.pythonhosted.org/packages/d3/ff/040a20c01c294695cac0e361caf86f33347acc38f164f6d2be1d3e007d9f/websockets-15.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bf1ab71f9f23b0a1d52ec1682a3907e0c208c12fef9c3e99d2b80166b17905f", size = 181763 }, + { url = "https://files.pythonhosted.org/packages/cb/6a/af23e93678fda8341ac8775e85123425e45c608389d3514863c702896ea5/websockets-15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bfcd3acc1a81f106abac6afd42327d2cf1e77ec905ae11dc1d9142a006a496b6", size = 182097 }, + { url = "https://files.pythonhosted.org/packages/7e/3e/1069e159c30129dc03c01513b5830237e576f47cedb888777dd885cae583/websockets-15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c8c5c8e1bac05ef3c23722e591ef4f688f528235e2480f157a9cfe0a19081375", size = 181485 }, + { url = "https://files.pythonhosted.org/packages/9a/a7/c91c47103f1cd941b576bbc452601e9e01f67d5c9be3e0a9abe726491ab5/websockets-15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:86bfb52a9cfbcc09aba2b71388b0a20ea5c52b6517c0b2e316222435a8cdab72", size = 181466 }, + { url = "https://files.pythonhosted.org/packages/16/32/a4ca6e3d56c24aac46b0cf5c03b841379f6409d07fc2044b244f90f54105/websockets-15.0-cp313-cp313-win32.whl", hash = "sha256:26ba70fed190708551c19a360f9d7eca8e8c0f615d19a574292b7229e0ae324c", size = 175673 }, + { url = "https://files.pythonhosted.org/packages/c0/31/25a417a23e985b61ffa5544f9facfe4a118cb64d664c886f1244a8baeca5/websockets-15.0-cp313-cp313-win_amd64.whl", hash = "sha256:ae721bcc8e69846af00b7a77a220614d9b2ec57d25017a6bbde3a99473e41ce8", size = 176115 }, + { url = "https://files.pythonhosted.org/packages/e8/b2/31eec524b53f01cd8343f10a8e429730c52c1849941d1f530f8253b6d934/websockets-15.0-py3-none-any.whl", hash = "sha256:51ffd53c53c4442415b613497a34ba0aa7b99ac07f1e4a62db5dcd640ae6c3c3", size = 169023 }, +] + [[package]] name = "wrapt" version = "1.17.2" From 521c0e44221953491aa72711e4e8eefecf96cf32 Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Tue, 25 Feb 2025 14:45:53 -0800 Subject: [PATCH 31/71] Remove ray dependency --- pyproject.toml | 8 - uv.lock | 953 ------------------------------------------------- 2 files changed, 961 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5c9b0629..95e6c252 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,6 @@ dependencies = [ "pytest-benchmark>=5.1.0", "pytest-html>=4.1.1", "pytest-timeout>=2.3.1", - "ray[client,default]", "python-multipart>=0.0.20", "rich>=13.9.4", "smbus2>=0.5.0", @@ -41,13 +40,6 @@ dev = [ "ruff>=0.9.6", ] -[tool.uv.sources] -# TODO: use released version (pending 3.13 support) -ray = [ - { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-macosx_11_0_arm64.whl", marker = "sys_platform == 'darwin'" }, - { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-manylinux2014_aarch64.whl", marker = "sys_platform != 'darwin'" }, -] - [tool.hatch.build] sources = ["software"] diff --git a/uv.lock b/uv.lock index 90c2480d..4bc6abfd 100644 --- a/uv.lock +++ b/uv.lock @@ -1,77 +1,10 @@ version = 1 -revision = 1 requires-python = ">=3.13" resolution-markers = [ "sys_platform == 'darwin'", "sys_platform != 'darwin'", ] -[[package]] -name = "aiohappyeyeballs" -version = "2.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/08/07/508f9ebba367fc3370162e53a3cfd12f5652ad79f0e0bfdf9f9847c6f159/aiohappyeyeballs-2.4.6.tar.gz", hash = "sha256:9b05052f9042985d32ecbe4b59a77ae19c006a78f1344d7fdad69d28ded3d0b0", size = 21726 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/4c/03fb05f56551828ec67ceb3665e5dc51638042d204983a03b0a1541475b6/aiohappyeyeballs-2.4.6-py3-none-any.whl", hash = "sha256:147ec992cf873d74f5062644332c539fcd42956dc69453fe5204195e560517e1", size = 14543 }, -] - -[[package]] -name = "aiohttp" -version = "3.11.13" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohappyeyeballs" }, - { name = "aiosignal" }, - { name = "attrs" }, - { name = "frozenlist" }, - { name = "multidict" }, - { name = "propcache" }, - { name = "yarl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/3f/c4a667d184c69667b8f16e0704127efc5f1e60577df429382b4d95fd381e/aiohttp-3.11.13.tar.gz", hash = "sha256:8ce789231404ca8fff7f693cdce398abf6d90fd5dae2b1847477196c243b1fbb", size = 7674284 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/dc/7d58d33cec693f1ddf407d4ab975445f5cb507af95600f137b81683a18d8/aiohttp-3.11.13-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9862d077b9ffa015dbe3ce6c081bdf35135948cb89116e26667dd183550833d1", size = 698372 }, - { url = "https://files.pythonhosted.org/packages/84/e7/5d88514c9e24fbc8dd6117350a8ec4a9314f4adae6e89fe32e3e639b0c37/aiohttp-3.11.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbfef0666ae9e07abfa2c54c212ac18a1f63e13e0760a769f70b5717742f3ece", size = 461057 }, - { url = "https://files.pythonhosted.org/packages/96/1a/8143c48a929fa00c6324f85660cb0f47a55ed9385f0c1b72d4b8043acf8e/aiohttp-3.11.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a1f7d857c4fcf7cabb1178058182c789b30d85de379e04f64c15b7e88d66fb", size = 453340 }, - { url = "https://files.pythonhosted.org/packages/2f/1c/b8010e4d65c5860d62681088e5376f3c0a940c5e3ca8989cae36ce8c3ea8/aiohttp-3.11.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba40b7ae0f81c7029583a338853f6607b6d83a341a3dcde8bed1ea58a3af1df9", size = 1665561 }, - { url = "https://files.pythonhosted.org/packages/19/ed/a68c3ab2f92fdc17dfc2096117d1cfaa7f7bdded2a57bacbf767b104165b/aiohttp-3.11.13-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5b95787335c483cd5f29577f42bbe027a412c5431f2f80a749c80d040f7ca9f", size = 1718335 }, - { url = "https://files.pythonhosted.org/packages/27/4f/3a0b6160ce663b8ebdb65d1eedff60900cd7108838c914d25952fe2b909f/aiohttp-3.11.13-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7d474c5c1f0b9405c1565fafdc4429fa7d986ccbec7ce55bc6a330f36409cad", size = 1775522 }, - { url = "https://files.pythonhosted.org/packages/0b/58/9da09291e19696c452e7224c1ce8c6d23a291fe8cd5c6b247b51bcda07db/aiohttp-3.11.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e83fb1991e9d8982b3b36aea1e7ad27ea0ce18c14d054c7a404d68b0319eebb", size = 1677566 }, - { url = "https://files.pythonhosted.org/packages/3d/18/6184f2bf8bbe397acbbbaa449937d61c20a6b85765f48e5eddc6d84957fe/aiohttp-3.11.13-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4586a68730bd2f2b04a83e83f79d271d8ed13763f64b75920f18a3a677b9a7f0", size = 1603590 }, - { url = "https://files.pythonhosted.org/packages/04/94/91e0d1ca0793012ccd927e835540aa38cca98bdce2389256ab813ebd64a3/aiohttp-3.11.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fe4eb0e7f50cdb99b26250d9328faef30b1175a5dbcfd6d0578d18456bac567", size = 1618688 }, - { url = "https://files.pythonhosted.org/packages/71/85/d13c3ea2e48a10b43668305d4903838834c3d4112e5229177fbcc23a56cd/aiohttp-3.11.13-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2a8a6bc19818ac3e5596310ace5aa50d918e1ebdcc204dc96e2f4d505d51740c", size = 1658053 }, - { url = "https://files.pythonhosted.org/packages/12/6a/3242a35100de23c1e8d9e05e8605e10f34268dee91b00d9d1e278c58eb80/aiohttp-3.11.13-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f27eec42f6c3c1df09cfc1f6786308f8b525b8efaaf6d6bd76c1f52c6511f6a", size = 1616917 }, - { url = "https://files.pythonhosted.org/packages/f5/b3/3f99b6f0a9a79590a7ba5655dbde8408c685aa462247378c977603464d0a/aiohttp-3.11.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2a4a13dfbb23977a51853b419141cd0a9b9573ab8d3a1455c6e63561387b52ff", size = 1685872 }, - { url = "https://files.pythonhosted.org/packages/8a/2e/99672181751f280a85e24fcb9a2c2469e8b1a0de1746b7b5c45d1eb9a999/aiohttp-3.11.13-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:02876bf2f69b062584965507b07bc06903c2dc93c57a554b64e012d636952654", size = 1715719 }, - { url = "https://files.pythonhosted.org/packages/7a/cd/68030356eb9a7d57b3e2823c8a852709d437abb0fbff41a61ebc351b7625/aiohttp-3.11.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b992778d95b60a21c4d8d4a5f15aaab2bd3c3e16466a72d7f9bfd86e8cea0d4b", size = 1673166 }, - { url = "https://files.pythonhosted.org/packages/03/61/425397a9a2839c609d09fdb53d940472f316a2dbeaa77a35b2628dae6284/aiohttp-3.11.13-cp313-cp313-win32.whl", hash = "sha256:507ab05d90586dacb4f26a001c3abf912eb719d05635cbfad930bdbeb469b36c", size = 410615 }, - { url = "https://files.pythonhosted.org/packages/9c/54/ebb815bc0fe057d8e7a11c086c479e972e827082f39aeebc6019dd4f0862/aiohttp-3.11.13-cp313-cp313-win_amd64.whl", hash = "sha256:5ceb81a4db2decdfa087381b5fc5847aa448244f973e5da232610304e199e7b2", size = 436452 }, -] - -[[package]] -name = "aiohttp-cors" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/44/9e/6cdce7c3f346d8fd487adf68761728ad8cd5fbc296a7b07b92518350d31f/aiohttp-cors-0.7.0.tar.gz", hash = "sha256:4d39c6d7100fd9764ed1caf8cebf0eb01bf5e3f24e2e073fda6234bc48b19f5d", size = 35966 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/e7/e436a0c0eb5127d8b491a9b83ecd2391c6ff7dcd5548dfaec2080a2340fd/aiohttp_cors-0.7.0-py3-none-any.whl", hash = "sha256:0451ba59fdf6909d0e2cd21e4c0a43752bc0703d33fc78ae94d9d9321710193e", size = 27564 }, -] - -[[package]] -name = "aiosignal" -version = "1.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "frozenlist" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597 }, -] - [[package]] name = "altair" version = "5.5.0" @@ -119,15 +52,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/30/d4986a882011f9df997a55e6becd864812ccfcd821d64aac8570ee39f719/attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a", size = 63152 }, ] -[[package]] -name = "cachetools" -version = "5.5.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080 }, -] - [[package]] name = "certifi" version = "2025.1.31" @@ -146,28 +70,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, ] -[[package]] -name = "charset-normalizer" -version = "3.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, - { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, - { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, - { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, - { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, - { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, - { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, - { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, - { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, - { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, - { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, - { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, - { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, - { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, -] - [[package]] name = "click" version = "8.1.8" @@ -198,18 +100,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] -[[package]] -name = "colorful" -version = "0.5.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fa/5f/38e40c3bc4107c39e4062d943026b8ee25154cb4b185b882f274a1ab65da/colorful-0.5.6.tar.gz", hash = "sha256:b56d5c01db1dac4898308ea889edcb113fbee3e6ec5df4bacffd61d5241b5b8d", size = 209280 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/61/39e7db0cb326c9c8f6a49fad4fc9c2f1241f05a4e10f0643fc31ce26a7e0/colorful-0.5.6-py2.py3-none-any.whl", hash = "sha256:eab8c1c809f5025ad2b5238a50bd691e26850da8cac8f90d660ede6ea1af9f1e", size = 201369 }, -] - [[package]] name = "distlib" version = "0.3.9" @@ -293,89 +183,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164 }, ] -[[package]] -name = "frozenlist" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8f/ed/0f4cec13a93c02c47ec32d81d11c0c1efbadf4a471e3f3ce7cad366cbbd3/frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", size = 39930 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/3b/915f0bca8a7ea04483622e84a9bd90033bab54bdf485479556c74fd5eaf5/frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953", size = 91538 }, - { url = "https://files.pythonhosted.org/packages/c7/d1/a7c98aad7e44afe5306a2b068434a5830f1470675f0e715abb86eb15f15b/frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0", size = 52849 }, - { url = "https://files.pythonhosted.org/packages/3a/c8/76f23bf9ab15d5f760eb48701909645f686f9c64fbb8982674c241fbef14/frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2", size = 50583 }, - { url = "https://files.pythonhosted.org/packages/1f/22/462a3dd093d11df623179d7754a3b3269de3b42de2808cddef50ee0f4f48/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f", size = 265636 }, - { url = "https://files.pythonhosted.org/packages/80/cf/e075e407fc2ae7328155a1cd7e22f932773c8073c1fc78016607d19cc3e5/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608", size = 270214 }, - { url = "https://files.pythonhosted.org/packages/a1/58/0642d061d5de779f39c50cbb00df49682832923f3d2ebfb0fedf02d05f7f/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b", size = 273905 }, - { url = "https://files.pythonhosted.org/packages/ab/66/3fe0f5f8f2add5b4ab7aa4e199f767fd3b55da26e3ca4ce2cc36698e50c4/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840", size = 250542 }, - { url = "https://files.pythonhosted.org/packages/f6/b8/260791bde9198c87a465224e0e2bb62c4e716f5d198fc3a1dacc4895dbd1/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439", size = 267026 }, - { url = "https://files.pythonhosted.org/packages/2e/a4/3d24f88c527f08f8d44ade24eaee83b2627793fa62fa07cbb7ff7a2f7d42/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de", size = 257690 }, - { url = "https://files.pythonhosted.org/packages/de/9a/d311d660420b2beeff3459b6626f2ab4fb236d07afbdac034a4371fe696e/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641", size = 253893 }, - { url = "https://files.pythonhosted.org/packages/c6/23/e491aadc25b56eabd0f18c53bb19f3cdc6de30b2129ee0bc39cd387cd560/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e", size = 267006 }, - { url = "https://files.pythonhosted.org/packages/08/c4/ab918ce636a35fb974d13d666dcbe03969592aeca6c3ab3835acff01f79c/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9", size = 276157 }, - { url = "https://files.pythonhosted.org/packages/c0/29/3b7a0bbbbe5a34833ba26f686aabfe982924adbdcafdc294a7a129c31688/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03", size = 264642 }, - { url = "https://files.pythonhosted.org/packages/ab/42/0595b3dbffc2e82d7fe658c12d5a5bafcd7516c6bf2d1d1feb5387caa9c1/frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c", size = 44914 }, - { url = "https://files.pythonhosted.org/packages/17/c4/b7db1206a3fea44bf3b838ca61deb6f74424a8a5db1dd53ecb21da669be6/frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28", size = 51167 }, - { url = "https://files.pythonhosted.org/packages/c6/c8/a5be5b7550c10858fcf9b0ea054baccab474da77d37f1e828ce043a3a5d4/frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", size = 11901 }, -] - -[[package]] -name = "google-api-core" -version = "2.24.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-auth" }, - { name = "googleapis-common-protos" }, - { name = "proto-plus" }, - { name = "protobuf" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b8/b7/481c83223d7b4f02c7651713fceca648fa3336e1571b9804713f66bca2d8/google_api_core-2.24.1.tar.gz", hash = "sha256:f8b36f5456ab0dd99a1b693a40a31d1e7757beea380ad1b38faaf8941eae9d8a", size = 163508 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/a6/8e30ddfd3d39ee6d2c76d3d4f64a83f77ac86a4cab67b286ae35ce9e4369/google_api_core-2.24.1-py3-none-any.whl", hash = "sha256:bc78d608f5a5bf853b80bd70a795f703294de656c096c0968320830a4bc280f1", size = 160059 }, -] - -[[package]] -name = "google-auth" -version = "2.38.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cachetools" }, - { name = "pyasn1-modules" }, - { name = "rsa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c6/eb/d504ba1daf190af6b204a9d4714d457462b486043744901a6eeea711f913/google_auth-2.38.0.tar.gz", hash = "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4", size = 270866 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/47/603554949a37bca5b7f894d51896a9c534b9eab808e2520a748e081669d0/google_auth-2.38.0-py2.py3-none-any.whl", hash = "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a", size = 210770 }, -] - -[[package]] -name = "googleapis-common-protos" -version = "1.68.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/54/d2/c08f0d9f94b45faca68e355771329cba2411c777c8713924dd1baee0e09c/googleapis_common_protos-1.68.0.tar.gz", hash = "sha256:95d38161f4f9af0d9423eed8fb7b64ffd2568c3464eb542ff02c5bfa1953ab3c", size = 57367 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/85/c99a157ee99d67cc6c9ad123abb8b1bfb476fab32d2f3511c59314548e4f/googleapis_common_protos-1.68.0-py2.py3-none-any.whl", hash = "sha256:aaf179b2f81df26dfadac95def3b16a95064c76a5f45f07e4c68a21bb371c4ac", size = 164985 }, -] - -[[package]] -name = "grpcio" -version = "1.70.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/e1/4b21b5017c33f3600dcc32b802bb48fe44a4d36d6c066f52650c7c2690fa/grpcio-1.70.0.tar.gz", hash = "sha256:8d1584a68d5922330025881e63a6c1b54cc8117291d382e4fa69339b6d914c56", size = 12788932 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/38/66d0f32f88feaf7d83f8559cd87d899c970f91b1b8a8819b58226de0a496/grpcio-1.70.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:aa573896aeb7d7ce10b1fa425ba263e8dddd83d71530d1322fd3a16f31257b4a", size = 5199218 }, - { url = "https://files.pythonhosted.org/packages/c1/96/947df763a0b18efb5cc6c2ae348e56d97ca520dc5300c01617b234410173/grpcio-1.70.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:d405b005018fd516c9ac529f4b4122342f60ec1cee181788249372524e6db429", size = 11445983 }, - { url = "https://files.pythonhosted.org/packages/fd/5b/f3d4b063e51b2454bedb828e41f3485800889a3609c49e60f2296cc8b8e5/grpcio-1.70.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f32090238b720eb585248654db8e3afc87b48d26ac423c8dde8334a232ff53c9", size = 5663954 }, - { url = "https://files.pythonhosted.org/packages/bd/0b/dab54365fcedf63e9f358c1431885478e77d6f190d65668936b12dd38057/grpcio-1.70.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfa089a734f24ee5f6880c83d043e4f46bf812fcea5181dcb3a572db1e79e01c", size = 6304323 }, - { url = "https://files.pythonhosted.org/packages/76/a8/8f965a7171ddd336ce32946e22954aa1bbc6f23f095e15dadaa70604ba20/grpcio-1.70.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f19375f0300b96c0117aca118d400e76fede6db6e91f3c34b7b035822e06c35f", size = 5910939 }, - { url = "https://files.pythonhosted.org/packages/1b/05/0bbf68be8b17d1ed6f178435a3c0c12e665a1e6054470a64ce3cb7896596/grpcio-1.70.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:7c73c42102e4a5ec76608d9b60227d917cea46dff4d11d372f64cbeb56d259d0", size = 6631405 }, - { url = "https://files.pythonhosted.org/packages/79/6a/5df64b6df405a1ed1482cb6c10044b06ec47fd28e87c2232dbcf435ecb33/grpcio-1.70.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:0a5c78d5198a1f0aa60006cd6eb1c912b4a1520b6a3968e677dbcba215fabb40", size = 6190982 }, - { url = "https://files.pythonhosted.org/packages/42/aa/aeaac87737e6d25d1048c53b8ec408c056d3ed0c922e7c5efad65384250c/grpcio-1.70.0-cp313-cp313-win32.whl", hash = "sha256:fe9dbd916df3b60e865258a8c72ac98f3ac9e2a9542dcb72b7a34d236242a5ce", size = 3598359 }, - { url = "https://files.pythonhosted.org/packages/1f/79/8edd2442d2de1431b4a3de84ef91c37002f12de0f9b577fb07b452989dbc/grpcio-1.70.0-cp313-cp313-win_amd64.whl", hash = "sha256:4119fed8abb7ff6c32e3d2255301e59c316c22d31ab812b3fbcbaf3d0d87cc68", size = 4293938 }, -] - [[package]] name = "h11" version = "0.14.0" @@ -404,8 +211,6 @@ dependencies = [ { name = "pytest-html" }, { name = "pytest-timeout" }, { name = "python-multipart" }, - { name = "ray", version = "3.0.0.dev0", source = { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-macosx_11_0_arm64.whl" }, extra = ["client", "default"], marker = "sys_platform == 'darwin'" }, - { name = "ray", version = "3.0.0.dev0", source = { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-manylinux2014_aarch64.whl" }, extra = ["client", "default"], marker = "sys_platform != 'darwin'" }, { name = "rich" }, { name = "smbus2" }, { name = "typer" }, @@ -435,8 +240,6 @@ requires-dist = [ { name = "pytest-html", specifier = ">=4.1.1" }, { name = "pytest-timeout", specifier = ">=2.3.1" }, { name = "python-multipart", specifier = ">=0.0.20" }, - { name = "ray", extras = ["client", "default"], marker = "sys_platform != 'darwin'", url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-manylinux2014_aarch64.whl" }, - { name = "ray", extras = ["client", "default"], marker = "sys_platform == 'darwin'", url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-macosx_11_0_arm64.whl" }, { name = "rich", specifier = ">=13.9.4" }, { name = "smbus2", specifier = ">=0.5.0" }, { name = "typer", specifier = ">=0.15.1" }, @@ -608,49 +411,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, ] -[[package]] -name = "msgpack" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cb/d0/7555686ae7ff5731205df1012ede15dd9d927f6227ea151e901c7406af4f/msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e", size = 167260 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/b0/380f5f639543a4ac413e969109978feb1f3c66e931068f91ab6ab0f8be00/msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf", size = 151142 }, - { url = "https://files.pythonhosted.org/packages/c8/ee/be57e9702400a6cb2606883d55b05784fada898dfc7fd12608ab1fdb054e/msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330", size = 84523 }, - { url = "https://files.pythonhosted.org/packages/7e/3a/2919f63acca3c119565449681ad08a2f84b2171ddfcff1dba6959db2cceb/msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734", size = 81556 }, - { url = "https://files.pythonhosted.org/packages/7c/43/a11113d9e5c1498c145a8925768ea2d5fce7cbab15c99cda655aa09947ed/msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e", size = 392105 }, - { url = "https://files.pythonhosted.org/packages/2d/7b/2c1d74ca6c94f70a1add74a8393a0138172207dc5de6fc6269483519d048/msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca", size = 399979 }, - { url = "https://files.pythonhosted.org/packages/82/8c/cf64ae518c7b8efc763ca1f1348a96f0e37150061e777a8ea5430b413a74/msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915", size = 383816 }, - { url = "https://files.pythonhosted.org/packages/69/86/a847ef7a0f5ef3fa94ae20f52a4cacf596a4e4a010197fbcc27744eb9a83/msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d", size = 380973 }, - { url = "https://files.pythonhosted.org/packages/aa/90/c74cf6e1126faa93185d3b830ee97246ecc4fe12cf9d2d31318ee4246994/msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434", size = 387435 }, - { url = "https://files.pythonhosted.org/packages/7a/40/631c238f1f338eb09f4acb0f34ab5862c4e9d7eda11c1b685471a4c5ea37/msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c", size = 399082 }, - { url = "https://files.pythonhosted.org/packages/e9/1b/fa8a952be252a1555ed39f97c06778e3aeb9123aa4cccc0fd2acd0b4e315/msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc", size = 69037 }, - { url = "https://files.pythonhosted.org/packages/b6/bc/8bd826dd03e022153bfa1766dcdec4976d6c818865ed54223d71f07862b3/msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f", size = 75140 }, -] - -[[package]] -name = "multidict" -version = "6.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 }, - { url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 }, - { url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 }, - { url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 }, - { url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 }, - { url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 }, - { url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 }, - { url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 }, - { url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 }, - { url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 }, - { url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 }, - { url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 }, - { url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 }, - { url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 }, - { url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 }, - { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, -] - [[package]] name = "narwhals" version = "1.28.0" @@ -697,29 +457,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/97/9b/484f7d04b537d0a1202a5ba81c6f53f1846ae6c63c2127f8df869ed31342/numpy-2.2.3-cp313-cp313t-win_amd64.whl", hash = "sha256:aee2512827ceb6d7f517c8b85aa5d3923afe8fc7a57d028cffcd522f1c6fd082", size = 12706784 }, ] -[[package]] -name = "opencensus" -version = "0.11.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core" }, - { name = "opencensus-context" }, - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/15/a7/a46dcffa1b63084f9f17fe3c8cb20724c4c8f91009fd0b2cfdb27d5d2b35/opencensus-0.11.4.tar.gz", hash = "sha256:cbef87d8b8773064ab60e5c2a1ced58bbaa38a6d052c41aec224958ce544eff2", size = 64966 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/ed/9fbdeb23a09e430d87b7d72d430484b88184633dc50f6bfb792354b6f661/opencensus-0.11.4-py2.py3-none-any.whl", hash = "sha256:a18487ce68bc19900336e0ff4655c5a116daf10c1b3685ece8d971bddad6a864", size = 128225 }, -] - -[[package]] -name = "opencensus-context" -version = "0.1.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/96/3b6f638f6275a8abbd45e582448723bffa29c1fb426721dedb5c72f7d056/opencensus-context-0.1.3.tar.gz", hash = "sha256:a03108c3c10d8c80bb5ddf5c8a1f033161fa61972a9917f9b9b3a18517f0088c", size = 4066 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/68/162c97ea78c957d68ecf78a5c5041d2e25bd5562bdf5d89a6cbf7f8429bf/opencensus_context-0.1.3-py2.py3-none-any.whl", hash = "sha256:073bb0590007af276853009fac7e4bab1d523c3f03baf4cb4511ca38967c6039", size = 5060 }, -] - [[package]] name = "packaging" version = "24.2" @@ -786,82 +523,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/b3/df14c580d82b9627d173ceea305ba898dca135feb360b6d84019d0803d3b/pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b", size = 220560 }, ] -[[package]] -name = "prometheus-client" -version = "0.21.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/62/14/7d0f567991f3a9af8d1cd4f619040c93b68f09a02b6d0b6ab1b2d1ded5fe/prometheus_client-0.21.1.tar.gz", hash = "sha256:252505a722ac04b0456be05c05f75f45d760c2911ffc45f2a06bcaed9f3ae3fb", size = 78551 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/c2/ab7d37426c179ceb9aeb109a85cda8948bb269b7561a0be870cc656eefe4/prometheus_client-0.21.1-py3-none-any.whl", hash = "sha256:594b45c410d6f4f8888940fe80b5cc2521b305a1fafe1c58609ef715a001f301", size = 54682 }, -] - -[[package]] -name = "propcache" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/92/76/f941e63d55c0293ff7829dd21e7cf1147e90a526756869a9070f287a68c9/propcache-0.3.0.tar.gz", hash = "sha256:a8fd93de4e1d278046345f49e2238cdb298589325849b2645d4a94c53faeffc5", size = 42722 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/0f/a79dd23a0efd6ee01ab0dc9750d8479b343bfd0c73560d59d271eb6a99d4/propcache-0.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a2b9bf8c79b660d0ca1ad95e587818c30ccdb11f787657458d6f26a1ea18c568", size = 77287 }, - { url = "https://files.pythonhosted.org/packages/b8/51/76675703c90de38ac75adb8deceb3f3ad99b67ff02a0fa5d067757971ab8/propcache-0.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0c1a133d42c6fc1f5fbcf5c91331657a1ff822e87989bf4a6e2e39b818d0ee9", size = 44923 }, - { url = "https://files.pythonhosted.org/packages/01/9b/fd5ddbee66cf7686e73c516227c2fd9bf471dbfed0f48329d095ea1228d3/propcache-0.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bb2f144c6d98bb5cbc94adeb0447cfd4c0f991341baa68eee3f3b0c9c0e83767", size = 44325 }, - { url = "https://files.pythonhosted.org/packages/13/1c/6961f11eb215a683b34b903b82bde486c606516c1466bf1fa67f26906d51/propcache-0.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1323cd04d6e92150bcc79d0174ce347ed4b349d748b9358fd2e497b121e03c8", size = 225116 }, - { url = "https://files.pythonhosted.org/packages/ef/ea/f8410c40abcb2e40dffe9adeed017898c930974650a63e5c79b886aa9f73/propcache-0.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b812b3cb6caacd072276ac0492d249f210006c57726b6484a1e1805b3cfeea0", size = 229905 }, - { url = "https://files.pythonhosted.org/packages/ef/5a/a9bf90894001468bf8e6ea293bb00626cc9ef10f8eb7996e9ec29345c7ed/propcache-0.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:742840d1d0438eb7ea4280f3347598f507a199a35a08294afdcc560c3739989d", size = 233221 }, - { url = "https://files.pythonhosted.org/packages/dd/ce/fffdddd9725b690b01d345c1156b4c2cc6dca09ab5c23a6d07b8f37d6e2f/propcache-0.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c6e7e4f9167fddc438cd653d826f2222222564daed4116a02a184b464d3ef05", size = 227627 }, - { url = "https://files.pythonhosted.org/packages/58/ae/45c89a5994a334735a3032b48e8e4a98c05d9536ddee0719913dc27da548/propcache-0.3.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a94ffc66738da99232ddffcf7910e0f69e2bbe3a0802e54426dbf0714e1c2ffe", size = 214217 }, - { url = "https://files.pythonhosted.org/packages/01/84/bc60188c3290ff8f5f4a92b9ca2d93a62e449c8daf6fd11ad517ad136926/propcache-0.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c6ec957025bf32b15cbc6b67afe233c65b30005e4c55fe5768e4bb518d712f1", size = 212921 }, - { url = "https://files.pythonhosted.org/packages/14/b3/39d60224048feef7a96edabb8217dc3f75415457e5ebbef6814f8b2a27b5/propcache-0.3.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:549722908de62aa0b47a78b90531c022fa6e139f9166be634f667ff45632cc92", size = 208200 }, - { url = "https://files.pythonhosted.org/packages/9d/b3/0a6720b86791251273fff8a01bc8e628bc70903513bd456f86cde1e1ef84/propcache-0.3.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5d62c4f6706bff5d8a52fd51fec6069bef69e7202ed481486c0bc3874912c787", size = 208400 }, - { url = "https://files.pythonhosted.org/packages/e9/4f/bb470f3e687790547e2e78105fb411f54e0cdde0d74106ccadd2521c6572/propcache-0.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:24c04f8fbf60094c531667b8207acbae54146661657a1b1be6d3ca7773b7a545", size = 218116 }, - { url = "https://files.pythonhosted.org/packages/34/71/277f7f9add469698ac9724c199bfe06f85b199542121a71f65a80423d62a/propcache-0.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7c5f5290799a3f6539cc5e6f474c3e5c5fbeba74a5e1e5be75587746a940d51e", size = 222911 }, - { url = "https://files.pythonhosted.org/packages/92/e3/a7b9782aef5a2fc765b1d97da9ec7aed2f25a4e985703608e73232205e3f/propcache-0.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4fa0e7c9c3cf7c276d4f6ab9af8adddc127d04e0fcabede315904d2ff76db626", size = 216563 }, - { url = "https://files.pythonhosted.org/packages/ab/76/0583ca2c551aa08ffcff87b2c6849c8f01c1f6fb815a5226f0c5c202173e/propcache-0.3.0-cp313-cp313-win32.whl", hash = "sha256:ee0bd3a7b2e184e88d25c9baa6a9dc609ba25b76daae942edfb14499ac7ec374", size = 39763 }, - { url = "https://files.pythonhosted.org/packages/80/ec/c6a84f9a36f608379b95f0e786c111d5465926f8c62f12be8cdadb02b15c/propcache-0.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1c8f7d896a16da9455f882870a507567d4f58c53504dc2d4b1e1d386dfe4588a", size = 43650 }, - { url = "https://files.pythonhosted.org/packages/ee/95/7d32e3560f5bf83fc2f2a4c1b0c181d327d53d5f85ebd045ab89d4d97763/propcache-0.3.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e560fd75aaf3e5693b91bcaddd8b314f4d57e99aef8a6c6dc692f935cc1e6bbf", size = 82140 }, - { url = "https://files.pythonhosted.org/packages/86/89/752388f12e6027a5e63f5d075f15291ded48e2d8311314fff039da5a9b11/propcache-0.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65a37714b8ad9aba5780325228598a5b16c47ba0f8aeb3dc0514701e4413d7c0", size = 47296 }, - { url = "https://files.pythonhosted.org/packages/1b/4c/b55c98d586c69180d3048984a57a5ea238bdeeccf82dbfcd598e935e10bb/propcache-0.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:07700939b2cbd67bfb3b76a12e1412405d71019df00ca5697ce75e5ef789d829", size = 46724 }, - { url = "https://files.pythonhosted.org/packages/0f/b6/67451a437aed90c4e951e320b5b3d7eb584ade1d5592f6e5e8f678030989/propcache-0.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c0fdbdf6983526e269e5a8d53b7ae3622dd6998468821d660d0daf72779aefa", size = 291499 }, - { url = "https://files.pythonhosted.org/packages/ee/ff/e4179facd21515b24737e1e26e02615dfb5ed29416eed4cf5bc6ac5ce5fb/propcache-0.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:794c3dd744fad478b6232289c866c25406ecdfc47e294618bdf1697e69bd64a6", size = 293911 }, - { url = "https://files.pythonhosted.org/packages/76/8d/94a8585992a064a23bd54f56c5e58c3b8bf0c0a06ae10e56f2353ae16c3d/propcache-0.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4544699674faf66fb6b4473a1518ae4999c1b614f0b8297b1cef96bac25381db", size = 293301 }, - { url = "https://files.pythonhosted.org/packages/b0/b8/2c860c92b4134f68c7716c6f30a0d723973f881c32a6d7a24c4ddca05fdf/propcache-0.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fddb8870bdb83456a489ab67c6b3040a8d5a55069aa6f72f9d872235fbc52f54", size = 281947 }, - { url = "https://files.pythonhosted.org/packages/cd/72/b564be7411b525d11757b713c757c21cd4dc13b6569c3b2b8f6d3c96fd5e/propcache-0.3.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f857034dc68d5ceb30fb60afb6ff2103087aea10a01b613985610e007053a121", size = 268072 }, - { url = "https://files.pythonhosted.org/packages/37/68/d94649e399e8d7fc051e5a4f2334efc567993525af083db145a70690a121/propcache-0.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:02df07041e0820cacc8f739510078f2aadcfd3fc57eaeeb16d5ded85c872c89e", size = 275190 }, - { url = "https://files.pythonhosted.org/packages/d8/3c/446e125f5bbbc1922964dd67cb541c01cdb678d811297b79a4ff6accc843/propcache-0.3.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f47d52fd9b2ac418c4890aad2f6d21a6b96183c98021f0a48497a904199f006e", size = 254145 }, - { url = "https://files.pythonhosted.org/packages/f4/80/fd3f741483dc8e59f7ba7e05eaa0f4e11677d7db2077522b92ff80117a2a/propcache-0.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9ff4e9ecb6e4b363430edf2c6e50173a63e0820e549918adef70515f87ced19a", size = 257163 }, - { url = "https://files.pythonhosted.org/packages/dc/cf/6292b5ce6ed0017e6a89024a827292122cc41b6259b30ada0c6732288513/propcache-0.3.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ecc2920630283e0783c22e2ac94427f8cca29a04cfdf331467d4f661f4072dac", size = 280249 }, - { url = "https://files.pythonhosted.org/packages/e8/f0/fd9b8247b449fe02a4f96538b979997e229af516d7462b006392badc59a1/propcache-0.3.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:c441c841e82c5ba7a85ad25986014be8d7849c3cfbdb6004541873505929a74e", size = 288741 }, - { url = "https://files.pythonhosted.org/packages/64/71/cf831fdc2617f86cfd7f414cfc487d018e722dac8acc098366ce9bba0941/propcache-0.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c929916cbdb540d3407c66f19f73387f43e7c12fa318a66f64ac99da601bcdf", size = 277061 }, - { url = "https://files.pythonhosted.org/packages/42/78/9432542a35d944abeca9e02927a0de38cd7a298466d8ffa171536e2381c3/propcache-0.3.0-cp313-cp313t-win32.whl", hash = "sha256:0c3e893c4464ebd751b44ae76c12c5f5c1e4f6cbd6fbf67e3783cd93ad221863", size = 42252 }, - { url = "https://files.pythonhosted.org/packages/6f/45/960365f4f8978f48ebb56b1127adf33a49f2e69ecd46ac1f46d6cf78a79d/propcache-0.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:75e872573220d1ee2305b35c9813626e620768248425f58798413e9c39741f46", size = 46425 }, - { url = "https://files.pythonhosted.org/packages/b5/35/6c4c6fc8774a9e3629cd750dc24a7a4fb090a25ccd5c3246d127b70f9e22/propcache-0.3.0-py3-none-any.whl", hash = "sha256:67dda3c7325691c2081510e92c561f465ba61b975f481735aefdfc845d2cd043", size = 12101 }, -] - -[[package]] -name = "proto-plus" -version = "1.26.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/26/79/a5c6cbb42268cfd3ddc652dc526889044a8798c688a03ff58e5e92b743c8/proto_plus-1.26.0.tar.gz", hash = "sha256:6e93d5f5ca267b54300880fff156b6a3386b3fa3f43b1da62e680fc0c586ef22", size = 56136 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/c3/59308ccc07b34980f9d532f7afc718a9f32b40e52cde7a740df8d55632fb/proto_plus-1.26.0-py3-none-any.whl", hash = "sha256:bf2dfaa3da281fc3187d12d224c707cb57214fb2c22ba854eb0c105a3fb2d4d7", size = 50166 }, -] - -[[package]] -name = "protobuf" -version = "5.29.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f7/d1/e0a911544ca9993e0f17ce6d3cc0932752356c1b0a834397f28e63479344/protobuf-5.29.3.tar.gz", hash = "sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620", size = 424945 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/7a/1e38f3cafa022f477ca0f57a1f49962f21ad25850c3ca0acd3b9d0091518/protobuf-5.29.3-cp310-abi3-win32.whl", hash = "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888", size = 422708 }, - { url = "https://files.pythonhosted.org/packages/61/fa/aae8e10512b83de633f2646506a6d835b151edf4b30d18d73afd01447253/protobuf-5.29.3-cp310-abi3-win_amd64.whl", hash = "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a", size = 434508 }, - { url = "https://files.pythonhosted.org/packages/dd/04/3eaedc2ba17a088961d0e3bd396eac764450f431621b58a04ce898acd126/protobuf-5.29.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e", size = 417825 }, - { url = "https://files.pythonhosted.org/packages/4f/06/7c467744d23c3979ce250397e26d8ad8eeb2bea7b18ca12ad58313c1b8d5/protobuf-5.29.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84", size = 319573 }, - { url = "https://files.pythonhosted.org/packages/a8/45/2ebbde52ad2be18d3675b6bee50e68cd73c9e0654de77d595540b5129df8/protobuf-5.29.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f", size = 319672 }, - { url = "https://files.pythonhosted.org/packages/fd/b2/ab07b09e0f6d143dfb839693aa05765257bceaa13d03bf1a696b78323e7a/protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f", size = 172550 }, -] - [[package]] name = "py-cpuinfo" version = "9.0.0" @@ -871,42 +532,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335 }, ] -[[package]] -name = "py-spy" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/cd/9dacc04604dc4398ce5bed77ed59918ad0940f15165954d4aaa651cc640c/py_spy-0.4.0.tar.gz", hash = "sha256:806602ce7972782cc9c1e383f339bfc27bfb822d42485e6a3e0530ae5040e1f0", size = 253236 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/7e/02ca3ee68507db47afce769504060d71b4dc1455f0f9faa8d32fc7762221/py_spy-0.4.0-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f2cf3f7130e7d780471faa5957441d3b4e0ec39a79b2c00f4c33d494f7728428", size = 3617847 }, - { url = "https://files.pythonhosted.org/packages/65/7c/d9e26cc4c8e91f96a3a65de04d2e2e4131fbcaf6830d10917d4fab9d6788/py_spy-0.4.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:47cdda4c34d9b6cb01f3aaeceb2e88faf57da880207fe72ff6ff97e9bb6cc8a9", size = 1761955 }, - { url = "https://files.pythonhosted.org/packages/d2/e4/8fbfd219b7f282b80e6b2e74c9197850d2c51db8555705567bb65507b060/py_spy-0.4.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eee3d0bde85ca5cf4f01f012d461180ca76c24835a96f7b5c4ded64eb6a008ab", size = 2059471 }, - { url = "https://files.pythonhosted.org/packages/a7/1d/79a94a5ace810c13b730ce96765ca465c171b4952034f1be7402d8accbc1/py_spy-0.4.0-py2.py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c5f06ffce4c9c98b7fc9f5e67e5e7db591173f1351837633f3f23d9378b1d18a", size = 2067486 }, - { url = "https://files.pythonhosted.org/packages/6d/90/fbbb038f826a83ed15ebc4ae606815d6cad6c5c6399c86c7ab96f6c60817/py_spy-0.4.0-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:87573e64dbfdfc89ba2e0f5e2f525aa84e0299c7eb6454b47ea335fde583a7a0", size = 2141433 }, - { url = "https://files.pythonhosted.org/packages/c9/c1/5e012669ebb687e546dc99fcfc4861ebfcf3a337b7a41af945df23140bb5/py_spy-0.4.0-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8bf2f3702cef367a489faa45177b41a6c31b2a3e5bd78c978d44e29340152f5a", size = 2732951 }, - { url = "https://files.pythonhosted.org/packages/74/8b/dd8490660019a6b0be28d9ffd2bf1db967604b19f3f2719c0e283a16ac7f/py_spy-0.4.0-py2.py3-none-win_amd64.whl", hash = "sha256:77d8f637ade38367d944874776f45b703b7ac5938b1f7be8891f3a5876ddbb96", size = 1810770 }, -] - -[[package]] -name = "pyasn1" -version = "0.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, -] - -[[package]] -name = "pyasn1-modules" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1d/67/6afbf0d507f73c32d21084a79946bfcfca5fbc62a72057e9c23797a737c9/pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c", size = 310028 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/89/bc88a6711935ba795a679ea6ebee07e128050d6382eaa35a0a47c8032bdc/pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd", size = 181537 }, -] - [[package]] name = "pydantic" version = "2.10.6" @@ -1086,466 +711,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, ] -[[package]] -name = "ray" -version = "3.0.0.dev0" -source = { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-macosx_11_0_arm64.whl" } -resolution-markers = [ - "sys_platform == 'darwin'", -] -dependencies = [ - { name = "aiosignal", marker = "sys_platform == 'darwin'" }, - { name = "click", marker = "sys_platform == 'darwin'" }, - { name = "filelock", marker = "sys_platform == 'darwin'" }, - { name = "frozenlist", marker = "sys_platform == 'darwin'" }, - { name = "jsonschema", marker = "sys_platform == 'darwin'" }, - { name = "msgpack", marker = "sys_platform == 'darwin'" }, - { name = "packaging", marker = "sys_platform == 'darwin'" }, - { name = "protobuf", marker = "sys_platform == 'darwin'" }, - { name = "pyyaml", marker = "sys_platform == 'darwin'" }, - { name = "requests", marker = "sys_platform == 'darwin'" }, -] -wheels = [ - { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0aa165e6bf9736c98284296d7f088aea0a71456ed9fd18a57ff872f6aefed7f2" }, -] - -[package.optional-dependencies] -client = [ - { name = "grpcio", marker = "sys_platform == 'darwin'" }, -] -default = [ - { name = "aiohttp", marker = "sys_platform == 'darwin'" }, - { name = "aiohttp-cors", marker = "sys_platform == 'darwin'" }, - { name = "colorful", marker = "sys_platform == 'darwin'" }, - { name = "grpcio", marker = "sys_platform == 'darwin'" }, - { name = "opencensus", marker = "sys_platform == 'darwin'" }, - { name = "prometheus-client", marker = "sys_platform == 'darwin'" }, - { name = "py-spy", marker = "sys_platform == 'darwin'" }, - { name = "pydantic", marker = "sys_platform == 'darwin'" }, - { name = "requests", marker = "sys_platform == 'darwin'" }, - { name = "smart-open", marker = "sys_platform == 'darwin'" }, - { name = "virtualenv", marker = "sys_platform == 'darwin'" }, -] - -[package.metadata] -requires-dist = [ - { name = "aiohttp", marker = "extra == 'air'", specifier = ">=3.7" }, - { name = "aiohttp", marker = "extra == 'all'", specifier = ">=3.7" }, - { name = "aiohttp", marker = "extra == 'default'", specifier = ">=3.7" }, - { name = "aiohttp", marker = "extra == 'llm'", specifier = ">=3.7" }, - { name = "aiohttp", marker = "extra == 'serve'", specifier = ">=3.7" }, - { name = "aiohttp", marker = "extra == 'serve-grpc'", specifier = ">=3.7" }, - { name = "aiohttp-cors", marker = "extra == 'air'" }, - { name = "aiohttp-cors", marker = "extra == 'all'" }, - { name = "aiohttp-cors", marker = "extra == 'default'" }, - { name = "aiohttp-cors", marker = "extra == 'llm'" }, - { name = "aiohttp-cors", marker = "extra == 'serve'" }, - { name = "aiohttp-cors", marker = "extra == 'serve-grpc'" }, - { name = "aiosignal" }, - { name = "async-timeout", marker = "extra == 'llm'" }, - { name = "boto3", marker = "extra == 'llm'" }, - { name = "click", specifier = ">=7.0" }, - { name = "colorful", marker = "extra == 'air'" }, - { name = "colorful", marker = "extra == 'all'" }, - { name = "colorful", marker = "extra == 'default'" }, - { name = "colorful", marker = "extra == 'llm'" }, - { name = "colorful", marker = "extra == 'serve'" }, - { name = "colorful", marker = "extra == 'serve-grpc'" }, - { name = "cupy-cuda12x", marker = "sys_platform != 'darwin' and extra == 'adag'" }, - { name = "cupy-cuda12x", marker = "sys_platform != 'darwin' and extra == 'all'" }, - { name = "cupy-cuda12x", marker = "sys_platform != 'darwin' and extra == 'cgraph'" }, - { name = "dm-tree", marker = "extra == 'all'" }, - { name = "dm-tree", marker = "extra == 'rllib'" }, - { name = "fastapi", marker = "extra == 'air'" }, - { name = "fastapi", marker = "extra == 'all'" }, - { name = "fastapi", marker = "extra == 'llm'" }, - { name = "fastapi", marker = "extra == 'serve'" }, - { name = "fastapi", marker = "extra == 'serve-grpc'" }, - { name = "filelock" }, - { name = "frozenlist" }, - { name = "fsspec", marker = "extra == 'air'" }, - { name = "fsspec", marker = "extra == 'all'" }, - { name = "fsspec", marker = "extra == 'data'" }, - { name = "fsspec", marker = "extra == 'llm'" }, - { name = "fsspec", marker = "extra == 'rllib'" }, - { name = "fsspec", marker = "extra == 'train'" }, - { name = "fsspec", marker = "extra == 'tune'" }, - { name = "grpcio", marker = "python_full_version >= '3.10' and extra == 'air'", specifier = ">=1.42.0" }, - { name = "grpcio", marker = "python_full_version >= '3.10' and extra == 'all'", specifier = ">=1.42.0" }, - { name = "grpcio", marker = "python_full_version >= '3.10' and extra == 'default'", specifier = ">=1.42.0" }, - { name = "grpcio", marker = "python_full_version >= '3.10' and extra == 'llm'", specifier = ">=1.42.0" }, - { name = "grpcio", marker = "python_full_version >= '3.10' and extra == 'serve'", specifier = ">=1.42.0" }, - { name = "grpcio", marker = "python_full_version >= '3.10' and extra == 'serve-grpc'", specifier = ">=1.42.0" }, - { name = "grpcio", marker = "python_full_version < '3.10' and extra == 'air'", specifier = ">=1.32.0" }, - { name = "grpcio", marker = "python_full_version < '3.10' and extra == 'all'", specifier = ">=1.32.0" }, - { name = "grpcio", marker = "python_full_version < '3.10' and extra == 'default'", specifier = ">=1.32.0" }, - { name = "grpcio", marker = "python_full_version < '3.10' and extra == 'llm'", specifier = ">=1.32.0" }, - { name = "grpcio", marker = "python_full_version < '3.10' and extra == 'serve'", specifier = ">=1.32.0" }, - { name = "grpcio", marker = "python_full_version < '3.10' and extra == 'serve-grpc'", specifier = ">=1.32.0" }, - { name = "grpcio", marker = "sys_platform == 'darwin' and extra == 'all'", specifier = "!=1.56.0" }, - { name = "grpcio", marker = "sys_platform == 'darwin' and extra == 'client'", specifier = "!=1.56.0" }, - { name = "grpcio", marker = "extra == 'all'" }, - { name = "grpcio", marker = "extra == 'client'" }, - { name = "gymnasium", marker = "extra == 'all'", specifier = "==1.0.0" }, - { name = "gymnasium", marker = "extra == 'rllib'", specifier = "==1.0.0" }, - { name = "jsonref", marker = "extra == 'llm'", specifier = ">=1.1.0" }, - { name = "jsonschema" }, - { name = "lz4", marker = "extra == 'all'" }, - { name = "lz4", marker = "extra == 'rllib'" }, - { name = "memray", marker = "sys_platform != 'win32' and extra == 'all'" }, - { name = "memray", marker = "sys_platform != 'win32' and extra == 'observability'" }, - { name = "msgpack", specifier = ">=1.0.0,<2.0.0" }, - { name = "numpy", marker = "extra == 'air'", specifier = ">=1.20" }, - { name = "numpy", marker = "extra == 'all'", specifier = ">=1.20" }, - { name = "numpy", marker = "extra == 'data'", specifier = ">=1.20" }, - { name = "numpy", marker = "extra == 'llm'", specifier = ">=1.20" }, - { name = "opencensus", marker = "extra == 'air'" }, - { name = "opencensus", marker = "extra == 'all'" }, - { name = "opencensus", marker = "extra == 'default'" }, - { name = "opencensus", marker = "extra == 'llm'" }, - { name = "opencensus", marker = "extra == 'serve'" }, - { name = "opencensus", marker = "extra == 'serve-grpc'" }, - { name = "opentelemetry-api", marker = "extra == 'all'" }, - { name = "opentelemetry-api", marker = "extra == 'observability'" }, - { name = "opentelemetry-exporter-otlp", marker = "extra == 'all'" }, - { name = "opentelemetry-exporter-otlp", marker = "extra == 'observability'" }, - { name = "opentelemetry-sdk", marker = "extra == 'all'" }, - { name = "opentelemetry-sdk", marker = "extra == 'observability'" }, - { name = "ormsgpack", marker = "extra == 'all'", specifier = "==1.7.0" }, - { name = "ormsgpack", marker = "extra == 'rllib'", specifier = "==1.7.0" }, - { name = "packaging" }, - { name = "pandas", marker = "extra == 'air'" }, - { name = "pandas", marker = "extra == 'air'", specifier = ">=1.3" }, - { name = "pandas", marker = "extra == 'all'" }, - { name = "pandas", marker = "extra == 'all'", specifier = ">=1.3" }, - { name = "pandas", marker = "extra == 'data'", specifier = ">=1.3" }, - { name = "pandas", marker = "extra == 'llm'", specifier = ">=1.3" }, - { name = "pandas", marker = "extra == 'rllib'" }, - { name = "pandas", marker = "extra == 'train'" }, - { name = "pandas", marker = "extra == 'tune'" }, - { name = "prometheus-client", marker = "extra == 'air'", specifier = ">=0.7.1" }, - { name = "prometheus-client", marker = "extra == 'all'", specifier = ">=0.7.1" }, - { name = "prometheus-client", marker = "extra == 'default'", specifier = ">=0.7.1" }, - { name = "prometheus-client", marker = "extra == 'llm'", specifier = ">=0.7.1" }, - { name = "prometheus-client", marker = "extra == 'serve'", specifier = ">=0.7.1" }, - { name = "prometheus-client", marker = "extra == 'serve-grpc'", specifier = ">=0.7.1" }, - { name = "protobuf", specifier = ">=3.15.3,!=3.19.5" }, - { name = "py-spy", marker = "python_full_version >= '3.12' and extra == 'air'", specifier = ">=0.4.0" }, - { name = "py-spy", marker = "python_full_version >= '3.12' and extra == 'all'", specifier = ">=0.4.0" }, - { name = "py-spy", marker = "python_full_version >= '3.12' and extra == 'default'", specifier = ">=0.4.0" }, - { name = "py-spy", marker = "python_full_version >= '3.12' and extra == 'llm'", specifier = ">=0.4.0" }, - { name = "py-spy", marker = "python_full_version >= '3.12' and extra == 'serve'", specifier = ">=0.4.0" }, - { name = "py-spy", marker = "python_full_version >= '3.12' and extra == 'serve-grpc'", specifier = ">=0.4.0" }, - { name = "py-spy", marker = "python_full_version < '3.12' and extra == 'air'", specifier = ">=0.2.0" }, - { name = "py-spy", marker = "python_full_version < '3.12' and extra == 'all'", specifier = ">=0.2.0" }, - { name = "py-spy", marker = "python_full_version < '3.12' and extra == 'default'", specifier = ">=0.2.0" }, - { name = "py-spy", marker = "python_full_version < '3.12' and extra == 'llm'", specifier = ">=0.2.0" }, - { name = "py-spy", marker = "python_full_version < '3.12' and extra == 'serve'", specifier = ">=0.2.0" }, - { name = "py-spy", marker = "python_full_version < '3.12' and extra == 'serve-grpc'", specifier = ">=0.2.0" }, - { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'air'", specifier = "<18" }, - { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'all'", specifier = "<18" }, - { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'data'", specifier = "<18" }, - { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'llm'", specifier = "<18" }, - { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'rllib'", specifier = "<18" }, - { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'train'", specifier = "<18" }, - { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'tune'", specifier = "<18" }, - { name = "pyarrow", marker = "extra == 'air'", specifier = ">=9.0.0" }, - { name = "pyarrow", marker = "extra == 'all'", specifier = ">=9.0.0" }, - { name = "pyarrow", marker = "extra == 'data'", specifier = ">=9.0.0" }, - { name = "pyarrow", marker = "extra == 'llm'", specifier = ">=9.0.0" }, - { name = "pyarrow", marker = "extra == 'rllib'", specifier = ">=9.0.0" }, - { name = "pyarrow", marker = "extra == 'train'", specifier = ">=9.0.0" }, - { name = "pyarrow", marker = "extra == 'tune'", specifier = ">=9.0.0" }, - { name = "pydantic", marker = "extra == 'air'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, - { name = "pydantic", marker = "extra == 'all'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, - { name = "pydantic", marker = "extra == 'default'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, - { name = "pydantic", marker = "extra == 'llm'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, - { name = "pydantic", marker = "extra == 'serve'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, - { name = "pydantic", marker = "extra == 'serve-grpc'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, - { name = "pydantic", marker = "extra == 'train'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, - { name = "pyopenssl", marker = "extra == 'all'" }, - { name = "pyopenssl", marker = "extra == 'serve-grpc'" }, - { name = "pyyaml" }, - { name = "pyyaml", marker = "extra == 'all'" }, - { name = "pyyaml", marker = "extra == 'rllib'" }, - { name = "requests" }, - { name = "requests", marker = "extra == 'air'" }, - { name = "requests", marker = "extra == 'all'" }, - { name = "requests", marker = "extra == 'default'" }, - { name = "requests", marker = "extra == 'llm'" }, - { name = "requests", marker = "extra == 'rllib'" }, - { name = "requests", marker = "extra == 'serve'" }, - { name = "requests", marker = "extra == 'serve-grpc'" }, - { name = "requests", marker = "extra == 'train'" }, - { name = "requests", marker = "extra == 'tune'" }, - { name = "scipy", marker = "extra == 'all'" }, - { name = "scipy", marker = "extra == 'rllib'" }, - { name = "smart-open", marker = "extra == 'air'" }, - { name = "smart-open", marker = "extra == 'all'" }, - { name = "smart-open", marker = "extra == 'default'" }, - { name = "smart-open", marker = "extra == 'llm'" }, - { name = "smart-open", marker = "extra == 'serve'" }, - { name = "smart-open", marker = "extra == 'serve-grpc'" }, - { name = "starlette", marker = "extra == 'air'" }, - { name = "starlette", marker = "extra == 'all'" }, - { name = "starlette", marker = "extra == 'llm'" }, - { name = "starlette", marker = "extra == 'serve'" }, - { name = "starlette", marker = "extra == 'serve-grpc'" }, - { name = "tensorboardx", marker = "extra == 'air'", specifier = ">=1.9" }, - { name = "tensorboardx", marker = "extra == 'all'", specifier = ">=1.9" }, - { name = "tensorboardx", marker = "extra == 'rllib'", specifier = ">=1.9" }, - { name = "tensorboardx", marker = "extra == 'train'", specifier = ">=1.9" }, - { name = "tensorboardx", marker = "extra == 'tune'", specifier = ">=1.9" }, - { name = "uvicorn", extras = ["standard"], marker = "extra == 'air'" }, - { name = "uvicorn", extras = ["standard"], marker = "extra == 'all'" }, - { name = "uvicorn", extras = ["standard"], marker = "extra == 'llm'" }, - { name = "uvicorn", extras = ["standard"], marker = "extra == 'serve'" }, - { name = "uvicorn", extras = ["standard"], marker = "extra == 'serve-grpc'" }, - { name = "virtualenv", marker = "extra == 'air'", specifier = ">=20.0.24,!=20.21.1" }, - { name = "virtualenv", marker = "extra == 'all'", specifier = ">=20.0.24,!=20.21.1" }, - { name = "virtualenv", marker = "extra == 'default'", specifier = ">=20.0.24,!=20.21.1" }, - { name = "virtualenv", marker = "extra == 'llm'", specifier = ">=20.0.24,!=20.21.1" }, - { name = "virtualenv", marker = "extra == 'serve'", specifier = ">=20.0.24,!=20.21.1" }, - { name = "virtualenv", marker = "extra == 'serve-grpc'", specifier = ">=20.0.24,!=20.21.1" }, - { name = "vllm", marker = "extra == 'llm'", specifier = ">=0.7.2" }, - { name = "watchfiles", marker = "extra == 'air'" }, - { name = "watchfiles", marker = "extra == 'all'" }, - { name = "watchfiles", marker = "extra == 'llm'" }, - { name = "watchfiles", marker = "extra == 'serve'" }, - { name = "watchfiles", marker = "extra == 'serve-grpc'" }, -] -provides-extras = ["adag", "air", "all", "cgraph", "client", "data", "default", "llm", "observability", "rllib", "serve", "serve-grpc", "train", "tune"] - -[[package]] -name = "ray" -version = "3.0.0.dev0" -source = { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-manylinux2014_aarch64.whl" } -resolution-markers = [ - "sys_platform != 'darwin'", -] -dependencies = [ - { name = "aiosignal", marker = "sys_platform != 'darwin'" }, - { name = "click", marker = "sys_platform != 'darwin'" }, - { name = "filelock", marker = "sys_platform != 'darwin'" }, - { name = "frozenlist", marker = "sys_platform != 'darwin'" }, - { name = "jsonschema", marker = "sys_platform != 'darwin'" }, - { name = "msgpack", marker = "sys_platform != 'darwin'" }, - { name = "packaging", marker = "sys_platform != 'darwin'" }, - { name = "protobuf", marker = "sys_platform != 'darwin'" }, - { name = "pyyaml", marker = "sys_platform != 'darwin'" }, - { name = "requests", marker = "sys_platform != 'darwin'" }, -] -wheels = [ - { url = "https://s3-us-west-2.amazonaws.com/ray-wheels/latest/ray-3.0.0.dev0-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:344b7d5bcf505799b6c7a40484dc2883b9f6a1b3db44dbc42606be07860db214" }, -] - -[package.optional-dependencies] -client = [ - { name = "grpcio", marker = "sys_platform != 'darwin'" }, -] -default = [ - { name = "aiohttp", marker = "sys_platform != 'darwin'" }, - { name = "aiohttp-cors", marker = "sys_platform != 'darwin'" }, - { name = "colorful", marker = "sys_platform != 'darwin'" }, - { name = "grpcio", marker = "sys_platform != 'darwin'" }, - { name = "opencensus", marker = "sys_platform != 'darwin'" }, - { name = "prometheus-client", marker = "sys_platform != 'darwin'" }, - { name = "py-spy", marker = "sys_platform != 'darwin'" }, - { name = "pydantic", marker = "sys_platform != 'darwin'" }, - { name = "requests", marker = "sys_platform != 'darwin'" }, - { name = "smart-open", marker = "sys_platform != 'darwin'" }, - { name = "virtualenv", marker = "sys_platform != 'darwin'" }, -] - -[package.metadata] -requires-dist = [ - { name = "aiohttp", marker = "extra == 'air'", specifier = ">=3.7" }, - { name = "aiohttp", marker = "extra == 'all'", specifier = ">=3.7" }, - { name = "aiohttp", marker = "extra == 'default'", specifier = ">=3.7" }, - { name = "aiohttp", marker = "extra == 'llm'", specifier = ">=3.7" }, - { name = "aiohttp", marker = "extra == 'serve'", specifier = ">=3.7" }, - { name = "aiohttp", marker = "extra == 'serve-grpc'", specifier = ">=3.7" }, - { name = "aiohttp-cors", marker = "extra == 'air'" }, - { name = "aiohttp-cors", marker = "extra == 'all'" }, - { name = "aiohttp-cors", marker = "extra == 'default'" }, - { name = "aiohttp-cors", marker = "extra == 'llm'" }, - { name = "aiohttp-cors", marker = "extra == 'serve'" }, - { name = "aiohttp-cors", marker = "extra == 'serve-grpc'" }, - { name = "aiosignal" }, - { name = "async-timeout", marker = "extra == 'llm'" }, - { name = "boto3", marker = "extra == 'llm'" }, - { name = "click", specifier = ">=7.0" }, - { name = "colorful", marker = "extra == 'air'" }, - { name = "colorful", marker = "extra == 'all'" }, - { name = "colorful", marker = "extra == 'default'" }, - { name = "colorful", marker = "extra == 'llm'" }, - { name = "colorful", marker = "extra == 'serve'" }, - { name = "colorful", marker = "extra == 'serve-grpc'" }, - { name = "cupy-cuda12x", marker = "sys_platform != 'darwin' and extra == 'adag'" }, - { name = "cupy-cuda12x", marker = "sys_platform != 'darwin' and extra == 'all'" }, - { name = "cupy-cuda12x", marker = "sys_platform != 'darwin' and extra == 'cgraph'" }, - { name = "dm-tree", marker = "extra == 'all'" }, - { name = "dm-tree", marker = "extra == 'rllib'" }, - { name = "fastapi", marker = "extra == 'air'" }, - { name = "fastapi", marker = "extra == 'all'" }, - { name = "fastapi", marker = "extra == 'llm'" }, - { name = "fastapi", marker = "extra == 'serve'" }, - { name = "fastapi", marker = "extra == 'serve-grpc'" }, - { name = "filelock" }, - { name = "frozenlist" }, - { name = "fsspec", marker = "extra == 'air'" }, - { name = "fsspec", marker = "extra == 'all'" }, - { name = "fsspec", marker = "extra == 'data'" }, - { name = "fsspec", marker = "extra == 'llm'" }, - { name = "fsspec", marker = "extra == 'rllib'" }, - { name = "fsspec", marker = "extra == 'train'" }, - { name = "fsspec", marker = "extra == 'tune'" }, - { name = "grpcio", marker = "python_full_version >= '3.10' and extra == 'air'", specifier = ">=1.42.0" }, - { name = "grpcio", marker = "python_full_version >= '3.10' and extra == 'all'", specifier = ">=1.42.0" }, - { name = "grpcio", marker = "python_full_version >= '3.10' and extra == 'default'", specifier = ">=1.42.0" }, - { name = "grpcio", marker = "python_full_version >= '3.10' and extra == 'llm'", specifier = ">=1.42.0" }, - { name = "grpcio", marker = "python_full_version >= '3.10' and extra == 'serve'", specifier = ">=1.42.0" }, - { name = "grpcio", marker = "python_full_version >= '3.10' and extra == 'serve-grpc'", specifier = ">=1.42.0" }, - { name = "grpcio", marker = "python_full_version < '3.10' and extra == 'air'", specifier = ">=1.32.0" }, - { name = "grpcio", marker = "python_full_version < '3.10' and extra == 'all'", specifier = ">=1.32.0" }, - { name = "grpcio", marker = "python_full_version < '3.10' and extra == 'default'", specifier = ">=1.32.0" }, - { name = "grpcio", marker = "python_full_version < '3.10' and extra == 'llm'", specifier = ">=1.32.0" }, - { name = "grpcio", marker = "python_full_version < '3.10' and extra == 'serve'", specifier = ">=1.32.0" }, - { name = "grpcio", marker = "python_full_version < '3.10' and extra == 'serve-grpc'", specifier = ">=1.32.0" }, - { name = "grpcio", marker = "sys_platform == 'darwin' and extra == 'all'", specifier = "!=1.56.0" }, - { name = "grpcio", marker = "sys_platform == 'darwin' and extra == 'client'", specifier = "!=1.56.0" }, - { name = "grpcio", marker = "extra == 'all'" }, - { name = "grpcio", marker = "extra == 'client'" }, - { name = "gymnasium", marker = "extra == 'all'", specifier = "==1.0.0" }, - { name = "gymnasium", marker = "extra == 'rllib'", specifier = "==1.0.0" }, - { name = "jsonref", marker = "extra == 'llm'", specifier = ">=1.1.0" }, - { name = "jsonschema" }, - { name = "lz4", marker = "extra == 'all'" }, - { name = "lz4", marker = "extra == 'rllib'" }, - { name = "memray", marker = "sys_platform != 'win32' and extra == 'all'" }, - { name = "memray", marker = "sys_platform != 'win32' and extra == 'observability'" }, - { name = "msgpack", specifier = ">=1.0.0,<2.0.0" }, - { name = "numpy", marker = "extra == 'air'", specifier = ">=1.20" }, - { name = "numpy", marker = "extra == 'all'", specifier = ">=1.20" }, - { name = "numpy", marker = "extra == 'data'", specifier = ">=1.20" }, - { name = "numpy", marker = "extra == 'llm'", specifier = ">=1.20" }, - { name = "opencensus", marker = "extra == 'air'" }, - { name = "opencensus", marker = "extra == 'all'" }, - { name = "opencensus", marker = "extra == 'default'" }, - { name = "opencensus", marker = "extra == 'llm'" }, - { name = "opencensus", marker = "extra == 'serve'" }, - { name = "opencensus", marker = "extra == 'serve-grpc'" }, - { name = "opentelemetry-api", marker = "extra == 'all'" }, - { name = "opentelemetry-api", marker = "extra == 'observability'" }, - { name = "opentelemetry-exporter-otlp", marker = "extra == 'all'" }, - { name = "opentelemetry-exporter-otlp", marker = "extra == 'observability'" }, - { name = "opentelemetry-sdk", marker = "extra == 'all'" }, - { name = "opentelemetry-sdk", marker = "extra == 'observability'" }, - { name = "ormsgpack", marker = "extra == 'all'", specifier = "==1.7.0" }, - { name = "ormsgpack", marker = "extra == 'rllib'", specifier = "==1.7.0" }, - { name = "packaging" }, - { name = "pandas", marker = "extra == 'air'" }, - { name = "pandas", marker = "extra == 'air'", specifier = ">=1.3" }, - { name = "pandas", marker = "extra == 'all'" }, - { name = "pandas", marker = "extra == 'all'", specifier = ">=1.3" }, - { name = "pandas", marker = "extra == 'data'", specifier = ">=1.3" }, - { name = "pandas", marker = "extra == 'llm'", specifier = ">=1.3" }, - { name = "pandas", marker = "extra == 'rllib'" }, - { name = "pandas", marker = "extra == 'train'" }, - { name = "pandas", marker = "extra == 'tune'" }, - { name = "prometheus-client", marker = "extra == 'air'", specifier = ">=0.7.1" }, - { name = "prometheus-client", marker = "extra == 'all'", specifier = ">=0.7.1" }, - { name = "prometheus-client", marker = "extra == 'default'", specifier = ">=0.7.1" }, - { name = "prometheus-client", marker = "extra == 'llm'", specifier = ">=0.7.1" }, - { name = "prometheus-client", marker = "extra == 'serve'", specifier = ">=0.7.1" }, - { name = "prometheus-client", marker = "extra == 'serve-grpc'", specifier = ">=0.7.1" }, - { name = "protobuf", specifier = ">=3.15.3,!=3.19.5" }, - { name = "py-spy", marker = "python_full_version >= '3.12' and extra == 'air'", specifier = ">=0.4.0" }, - { name = "py-spy", marker = "python_full_version >= '3.12' and extra == 'all'", specifier = ">=0.4.0" }, - { name = "py-spy", marker = "python_full_version >= '3.12' and extra == 'default'", specifier = ">=0.4.0" }, - { name = "py-spy", marker = "python_full_version >= '3.12' and extra == 'llm'", specifier = ">=0.4.0" }, - { name = "py-spy", marker = "python_full_version >= '3.12' and extra == 'serve'", specifier = ">=0.4.0" }, - { name = "py-spy", marker = "python_full_version >= '3.12' and extra == 'serve-grpc'", specifier = ">=0.4.0" }, - { name = "py-spy", marker = "python_full_version < '3.12' and extra == 'air'", specifier = ">=0.2.0" }, - { name = "py-spy", marker = "python_full_version < '3.12' and extra == 'all'", specifier = ">=0.2.0" }, - { name = "py-spy", marker = "python_full_version < '3.12' and extra == 'default'", specifier = ">=0.2.0" }, - { name = "py-spy", marker = "python_full_version < '3.12' and extra == 'llm'", specifier = ">=0.2.0" }, - { name = "py-spy", marker = "python_full_version < '3.12' and extra == 'serve'", specifier = ">=0.2.0" }, - { name = "py-spy", marker = "python_full_version < '3.12' and extra == 'serve-grpc'", specifier = ">=0.2.0" }, - { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'air'", specifier = "<18" }, - { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'all'", specifier = "<18" }, - { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'data'", specifier = "<18" }, - { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'llm'", specifier = "<18" }, - { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'rllib'", specifier = "<18" }, - { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'train'", specifier = "<18" }, - { name = "pyarrow", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin' and extra == 'tune'", specifier = "<18" }, - { name = "pyarrow", marker = "extra == 'air'", specifier = ">=9.0.0" }, - { name = "pyarrow", marker = "extra == 'all'", specifier = ">=9.0.0" }, - { name = "pyarrow", marker = "extra == 'data'", specifier = ">=9.0.0" }, - { name = "pyarrow", marker = "extra == 'llm'", specifier = ">=9.0.0" }, - { name = "pyarrow", marker = "extra == 'rllib'", specifier = ">=9.0.0" }, - { name = "pyarrow", marker = "extra == 'train'", specifier = ">=9.0.0" }, - { name = "pyarrow", marker = "extra == 'tune'", specifier = ">=9.0.0" }, - { name = "pydantic", marker = "extra == 'air'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, - { name = "pydantic", marker = "extra == 'all'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, - { name = "pydantic", marker = "extra == 'default'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, - { name = "pydantic", marker = "extra == 'llm'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, - { name = "pydantic", marker = "extra == 'serve'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, - { name = "pydantic", marker = "extra == 'serve-grpc'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, - { name = "pydantic", marker = "extra == 'train'", specifier = "!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3" }, - { name = "pyopenssl", marker = "extra == 'all'" }, - { name = "pyopenssl", marker = "extra == 'serve-grpc'" }, - { name = "pyyaml" }, - { name = "pyyaml", marker = "extra == 'all'" }, - { name = "pyyaml", marker = "extra == 'rllib'" }, - { name = "requests" }, - { name = "requests", marker = "extra == 'air'" }, - { name = "requests", marker = "extra == 'all'" }, - { name = "requests", marker = "extra == 'default'" }, - { name = "requests", marker = "extra == 'llm'" }, - { name = "requests", marker = "extra == 'rllib'" }, - { name = "requests", marker = "extra == 'serve'" }, - { name = "requests", marker = "extra == 'serve-grpc'" }, - { name = "requests", marker = "extra == 'train'" }, - { name = "requests", marker = "extra == 'tune'" }, - { name = "scipy", marker = "extra == 'all'" }, - { name = "scipy", marker = "extra == 'rllib'" }, - { name = "smart-open", marker = "extra == 'air'" }, - { name = "smart-open", marker = "extra == 'all'" }, - { name = "smart-open", marker = "extra == 'default'" }, - { name = "smart-open", marker = "extra == 'llm'" }, - { name = "smart-open", marker = "extra == 'serve'" }, - { name = "smart-open", marker = "extra == 'serve-grpc'" }, - { name = "starlette", marker = "extra == 'air'" }, - { name = "starlette", marker = "extra == 'all'" }, - { name = "starlette", marker = "extra == 'llm'" }, - { name = "starlette", marker = "extra == 'serve'" }, - { name = "starlette", marker = "extra == 'serve-grpc'" }, - { name = "tensorboardx", marker = "extra == 'air'", specifier = ">=1.9" }, - { name = "tensorboardx", marker = "extra == 'all'", specifier = ">=1.9" }, - { name = "tensorboardx", marker = "extra == 'rllib'", specifier = ">=1.9" }, - { name = "tensorboardx", marker = "extra == 'train'", specifier = ">=1.9" }, - { name = "tensorboardx", marker = "extra == 'tune'", specifier = ">=1.9" }, - { name = "uvicorn", extras = ["standard"], marker = "extra == 'air'" }, - { name = "uvicorn", extras = ["standard"], marker = "extra == 'all'" }, - { name = "uvicorn", extras = ["standard"], marker = "extra == 'llm'" }, - { name = "uvicorn", extras = ["standard"], marker = "extra == 'serve'" }, - { name = "uvicorn", extras = ["standard"], marker = "extra == 'serve-grpc'" }, - { name = "virtualenv", marker = "extra == 'air'", specifier = ">=20.0.24,!=20.21.1" }, - { name = "virtualenv", marker = "extra == 'all'", specifier = ">=20.0.24,!=20.21.1" }, - { name = "virtualenv", marker = "extra == 'default'", specifier = ">=20.0.24,!=20.21.1" }, - { name = "virtualenv", marker = "extra == 'llm'", specifier = ">=20.0.24,!=20.21.1" }, - { name = "virtualenv", marker = "extra == 'serve'", specifier = ">=20.0.24,!=20.21.1" }, - { name = "virtualenv", marker = "extra == 'serve-grpc'", specifier = ">=20.0.24,!=20.21.1" }, - { name = "vllm", marker = "extra == 'llm'", specifier = ">=0.7.2" }, - { name = "watchfiles", marker = "extra == 'air'" }, - { name = "watchfiles", marker = "extra == 'all'" }, - { name = "watchfiles", marker = "extra == 'llm'" }, - { name = "watchfiles", marker = "extra == 'serve'" }, - { name = "watchfiles", marker = "extra == 'serve-grpc'" }, -] -provides-extras = ["cgraph", "client", "data", "default", "observability", "serve", "tune", "adag", "serve-grpc", "rllib", "train", "air", "all", "llm"] - [[package]] name = "referencing" version = "0.36.2" @@ -1559,21 +724,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 }, ] -[[package]] -name = "requests" -version = "2.32.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, -] - [[package]] name = "rich" version = "13.9.4" @@ -1635,18 +785,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/15/6d22d07e063ce5e9bfbd96db9ec2fbb4693591b4503e3a76996639474d02/rpds_py-0.23.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d6f6512a90bd5cd9030a6237f5346f046c6f0e40af98657568fa45695d4de59d", size = 235415 }, ] -[[package]] -name = "rsa" -version = "4.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/65/7d973b89c4d2351d7fb232c2e452547ddfa243e93131e7cfa766da627b52/rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21", size = 29711 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/97/fa78e3d2f65c02c8e1268b9aba606569fe97f6c8f7c2d74394553347c145/rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", size = 34315 }, -] - [[package]] name = "ruff" version = "0.9.7" @@ -1681,27 +819,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, ] -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, -] - -[[package]] -name = "smart-open" -version = "7.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/21/30/1f41c3d3b8cec82024b4b277bfd4e5b18b765ae7279eb9871fa25c503778/smart_open-7.1.0.tar.gz", hash = "sha256:a4f09f84f0f6d3637c6543aca7b5487438877a21360e7368ccf1f704789752ba", size = 72044 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/18/9a8d9f01957aa1f8bbc5676d54c2e33102d247e146c1a3679d3bd5cc2e3a/smart_open-7.1.0-py3-none-any.whl", hash = "sha256:4b8489bb6058196258bafe901730c7db0dcf4f083f316e97269c66f45502055b", size = 61746 }, -] - [[package]] name = "smbus2" version = "0.5.0" @@ -1756,15 +873,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] -[[package]] -name = "urllib3" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, -] - [[package]] name = "uvicorn" version = "0.34.0" @@ -1859,64 +967,3 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/31/25a417a23e985b61ffa5544f9facfe4a118cb64d664c886f1244a8baeca5/websockets-15.0-cp313-cp313-win_amd64.whl", hash = "sha256:ae721bcc8e69846af00b7a77a220614d9b2ec57d25017a6bbde3a99473e41ce8", size = 176115 }, { url = "https://files.pythonhosted.org/packages/e8/b2/31eec524b53f01cd8343f10a8e429730c52c1849941d1f530f8253b6d934/websockets-15.0-py3-none-any.whl", hash = "sha256:51ffd53c53c4442415b613497a34ba0aa7b99ac07f1e4a62db5dcd640ae6c3c3", size = 169023 }, ] - -[[package]] -name = "wrapt" -version = "1.17.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800 }, - { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824 }, - { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920 }, - { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690 }, - { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861 }, - { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174 }, - { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721 }, - { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763 }, - { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585 }, - { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676 }, - { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871 }, - { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312 }, - { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062 }, - { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155 }, - { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471 }, - { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208 }, - { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339 }, - { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232 }, - { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476 }, - { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377 }, - { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986 }, - { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750 }, - { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 }, -] - -[[package]] -name = "yarl" -version = "1.18.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "multidict" }, - { name = "propcache" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/4b94a8e6d2b51b599516a5cb88e5bc99b4d8d4583e468057eaa29d5f0918/yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", size = 181062 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/30/c7/c790513d5328a8390be8f47be5d52e141f78b66c6c48f48d241ca6bd5265/yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb", size = 140789 }, - { url = "https://files.pythonhosted.org/packages/30/aa/a2f84e93554a578463e2edaaf2300faa61c8701f0898725842c704ba5444/yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa", size = 94144 }, - { url = "https://files.pythonhosted.org/packages/c6/fc/d68d8f83714b221a85ce7866832cba36d7c04a68fa6a960b908c2c84f325/yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782", size = 91974 }, - { url = "https://files.pythonhosted.org/packages/56/4e/d2563d8323a7e9a414b5b25341b3942af5902a2263d36d20fb17c40411e2/yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0", size = 333587 }, - { url = "https://files.pythonhosted.org/packages/25/c9/cfec0bc0cac8d054be223e9f2c7909d3e8442a856af9dbce7e3442a8ec8d/yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482", size = 344386 }, - { url = "https://files.pythonhosted.org/packages/ab/5d/4c532190113b25f1364d25f4c319322e86232d69175b91f27e3ebc2caf9a/yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186", size = 345421 }, - { url = "https://files.pythonhosted.org/packages/23/d1/6cdd1632da013aa6ba18cee4d750d953104a5e7aac44e249d9410a972bf5/yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58", size = 339384 }, - { url = "https://files.pythonhosted.org/packages/9a/c4/6b3c39bec352e441bd30f432cda6ba51681ab19bb8abe023f0d19777aad1/yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53", size = 326689 }, - { url = "https://files.pythonhosted.org/packages/23/30/07fb088f2eefdc0aa4fc1af4e3ca4eb1a3aadd1ce7d866d74c0f124e6a85/yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2", size = 345453 }, - { url = "https://files.pythonhosted.org/packages/63/09/d54befb48f9cd8eec43797f624ec37783a0266855f4930a91e3d5c7717f8/yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8", size = 341872 }, - { url = "https://files.pythonhosted.org/packages/91/26/fd0ef9bf29dd906a84b59f0cd1281e65b0c3e08c6aa94b57f7d11f593518/yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1", size = 347497 }, - { url = "https://files.pythonhosted.org/packages/d9/b5/14ac7a256d0511b2ac168d50d4b7d744aea1c1aa20c79f620d1059aab8b2/yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a", size = 359981 }, - { url = "https://files.pythonhosted.org/packages/ca/b3/d493221ad5cbd18bc07e642894030437e405e1413c4236dd5db6e46bcec9/yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10", size = 366229 }, - { url = "https://files.pythonhosted.org/packages/04/56/6a3e2a5d9152c56c346df9b8fb8edd2c8888b1e03f96324d457e5cf06d34/yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8", size = 360383 }, - { url = "https://files.pythonhosted.org/packages/fd/b7/4b3c7c7913a278d445cc6284e59b2e62fa25e72758f888b7a7a39eb8423f/yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d", size = 310152 }, - { url = "https://files.pythonhosted.org/packages/f5/d5/688db678e987c3e0fb17867970700b92603cadf36c56e5fb08f23e822a0c/yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c", size = 315723 }, - { url = "https://files.pythonhosted.org/packages/f5/4b/a06e0ec3d155924f77835ed2d167ebd3b211a7b0853da1cf8d8414d784ef/yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", size = 45109 }, -] From 75103bbaed64d7f5531cafaf9398510d91349a26 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 25 Feb 2025 15:17:10 -0800 Subject: [PATCH 32/71] binary data handling --- software/httpdist_server/models.py | 2 +- software/httpdist_server/server.py | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/software/httpdist_server/models.py b/software/httpdist_server/models.py index b5d14958..1e0824ff 100644 --- a/software/httpdist_server/models.py +++ b/software/httpdist_server/models.py @@ -39,7 +39,7 @@ class PostWorkerRegisterRequest(BaseModel): class PostWorkerSessionTestReportRequest(BaseModel): - report: TestReport + report: bytes class GetWorkerSessionTestsResponse(BaseModel): diff --git a/software/httpdist_server/server.py b/software/httpdist_server/server.py index ae1ea7db..636db7bb 100644 --- a/software/httpdist_server/server.py +++ b/software/httpdist_server/server.py @@ -1,3 +1,5 @@ +import base64 +from collections.abc import AsyncGenerator import uuid from dataclasses import dataclass from datetime import datetime, timedelta @@ -7,6 +9,7 @@ import uvicorn from attrs import field from fastapi import UploadFile +from fastapi.responses import StreamingResponse from pydantic import BaseModel from httpdist_server.models import ( @@ -171,14 +174,14 @@ async def post_test_report( raise fastapi.HTTPException(status_code=404, detail="Test not found") test = sessions[session_id].tests[test_id] - test.report = request.report + test.report = base64.b64decode(request.report) test.status = "finished" return {"message": "Test result uploaded successfully"} @app.get("/session/{session_id}/test/{test_id}/report") -async def get_test_report(session_id: str, test_id: str) -> TestReport: +async def get_test_report(session_id: str, test_id: str) -> StreamingResponse: """Get the report for a test""" if session_id not in sessions: raise fastapi.HTTPException(status_code=404, detail="Session not found") @@ -190,7 +193,13 @@ async def get_test_report(session_id: str, test_id: str) -> TestReport: if test.report is None: raise fastapi.HTTPException(status_code=404, detail="Test report not found") - return TestReport(test.report) + async def report_generator(report: bytes) -> AsyncGenerator[bytes, None]: + report_data = TestReport(report) + yield report_data + + return StreamingResponse( + report_generator(test.report), media_type="application/octet-stream" + ) @app.get("/worker/{worker_id}/get-session") From 39d80738117a8720d474d4fa65f3dc305c7734e9 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 25 Feb 2025 15:26:11 -0800 Subject: [PATCH 33/71] attrs -> dataclasses --- software/httpdist_server/server.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/software/httpdist_server/server.py b/software/httpdist_server/server.py index 636db7bb..dc3b9202 100644 --- a/software/httpdist_server/server.py +++ b/software/httpdist_server/server.py @@ -1,13 +1,12 @@ import base64 from collections.abc import AsyncGenerator import uuid -from dataclasses import dataclass +from dataclasses import dataclass, field from datetime import datetime, timedelta from typing import Literal import fastapi import uvicorn -from attrs import field from fastapi import UploadFile from fastapi.responses import StreamingResponse from pydantic import BaseModel @@ -56,7 +55,7 @@ class Test: session_id: str state: Literal["setup", "running", "stopped"] = "setup" - tests: dict[str, Test] = field(factory=dict) + tests: dict[str, Test] = field(default_factory=dict) env: UploadFile | None = None async def stop(self): From 7c45ba8d361f0bf620354ca3d98bad330cd5ed38 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 25 Feb 2025 16:13:15 -0800 Subject: [PATCH 34/71] init client --- software/hil/dist_plugin.py | 96 ++++++++++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 23 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index 081be6f2..c1688c10 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -1,7 +1,7 @@ +import asyncio from dataclasses import dataclass from enum import StrEnum, auto from pathlib import Path -import time import httpx import pytest @@ -23,6 +23,7 @@ def check(self, node) -> bool: NodeId = str +SessionId = str class TestStatus(StrEnum): @@ -53,14 +54,58 @@ class EndOfSession(Exception): pass +class ApiUsageError(Exception): + pass + + +class SessionNotStartedError(ApiUsageError): + pass + + class ApiClient: # FIXME API_URL = "http://localhost:8000" + session_id: SessionId | None = None def __init__(self, config: pytest.Config): self.config = config self._client = httpx.AsyncClient() + async def _post(self, path: str, data: dict): + response = await self._client.post(f"{self.API_URL}/{path}", json=data) + response.raise_for_status() + return response.json() + + async def _get(self, path: str, params: dict | None = None): + response = await self._client.get(f"{self.API_URL}/{path}", params=params) + response.raise_for_status() + return response.json() + + async def get_session(self) -> SessionId: + session = await self._get("get-session") + session_id = session["session_id"] + print(session_id) + self.session_id = session_id + return session_id + + async def submit_tests(self, nodeids: set[NodeId]): + if self.session_id is None: + raise SessionNotStartedError("Must have an active session") + + await self._post(f"session/{self.session_id}/tests", {"tests": list(nodeids)}) + + async def fetch_statuses(self) -> dict[NodeId, TestStatus]: + if self.session_id is None: + raise SessionNotStartedError("Must have an active session") + + return await self._get(f"session/{self.session_id}/finished-tests") + + async def fetch_report(self, nodeid: NodeId) -> pytest.TestReport: + if self.session_id is None: + raise SessionNotStartedError("Must have an active session") + + return await self._get(f"session/{self.session_id}/test/{nodeid}/report") + class Worker: """ @@ -171,15 +216,16 @@ def __init__(self, config: pytest.Config): def submit_env(self): ... - def submit_tests(self, session: pytest.Session): ... + async def submit_tests(self, session: pytest.Session): + nodeids = {item.nodeid for item in session.items} + await self.api_client.submit_tests(nodeids) - def fetch_results(self) -> list[pytest.TestReport]: - # TOOD: fetch new statuses - new_statuses: dict[NodeId, TestStatus] = {} - new_reports: list[pytest.TestReport] = [] + async def fetch_results(self) -> list[pytest.TestReport]: + new_statuses: dict[NodeId, TestStatus] = await self.api_client.fetch_statuses() + new_reports: list[pytest.TestReport] = [] for nodeid in new_statuses.keys() - self.statuses.keys(): - new_report = self.fetch_report(nodeid) + new_report = await self.api_client.fetch_report(nodeid) self.results.add(nodeid, new_report) new_reports.append(new_report) @@ -187,15 +233,8 @@ def fetch_results(self) -> list[pytest.TestReport]: return new_reports - def fetch_report(self, nodeid: NodeId) -> pytest.TestReport: ... - def download_artifacts(self): ... - @pytest.hookimpl - def pytest_sessionstart(self, session: pytest.Session): - # TODO: get session - self.submit_env() - @pytest.hookimpl def pytest_sessionfinish(self, session: pytest.Session, exitstatus: int): self.download_artifacts() @@ -216,24 +255,35 @@ def pytest_collection(self, session: pytest.Session): def pytest_runtestloop(self, session: pytest.Session): # TODO: shutdown handling - self.submit_tests(session) + async def run(): + # self.submit_env() # TODO + await self.api_client.get_session() + await self.submit_tests(session) + + asyncio.run(run()) + + return True self.results = TestResults(set(item.nodeid for item in session.items)) - while True: - new_reports = self.fetch_results() - for report in new_reports: - session.config.hook.pytest_runtest_logreport(report=report) + async def run(): + while True: + new_reports = await self.fetch_results() - if self.results.all_done: - break + for report in new_reports: + session.config.hook.pytest_runtest_logreport(report=report) + + if self.results.all_done: + break + + await asyncio.sleep(1) # FIXME - time.sleep(1) # FIXME + asyncio.run(run()) return True -@pytest.hookimpl(trylast=True) +@pytest.hookimpl(tryfirst=True) def pytest_configure(config): is_worker = config.getoption("worker") session = Worker(config) if is_worker else Client(config) From 15a88082ab423d3260d92358a539f4ea54037fda Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 25 Feb 2025 16:24:37 -0800 Subject: [PATCH 35/71] more client --- software/hil/dist_plugin.py | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index c1688c10..d1d2bf43 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from enum import StrEnum, auto from pathlib import Path +from typing import TypedDict import httpx import pytest @@ -26,6 +27,11 @@ def check(self, node) -> bool: SessionId = str +class TestSpec(TypedDict): + node_id: NodeId + worker_requirements: list[RunsOn] | None + + class TestStatus(StrEnum): # FIXME Passed = auto() @@ -84,15 +90,19 @@ async def _get(self, path: str, params: dict | None = None): async def get_session(self) -> SessionId: session = await self._get("get-session") session_id = session["session_id"] - print(session_id) self.session_id = session_id return session_id - async def submit_tests(self, nodeids: set[NodeId]): + async def submit_tests(self, tests: list[TestSpec]): if self.session_id is None: raise SessionNotStartedError("Must have an active session") - await self._post(f"session/{self.session_id}/tests", {"tests": list(nodeids)}) + try: + await self._post(f"session/{self.session_id}/tests", {"tests": tests}) + except httpx.HTTPStatusError as e: + if e.response.status_code == 422: + raise ApiUsageError(e.response.text) + raise async def fetch_statuses(self) -> dict[NodeId, TestStatus]: if self.session_id is None: @@ -213,12 +223,19 @@ class Client: def __init__(self, config: pytest.Config): self.config = config self.api_client = ApiClient(config) + self.statuses = {} def submit_env(self): ... async def submit_tests(self, session: pytest.Session): nodeids = {item.nodeid for item in session.items} - await self.api_client.submit_tests(nodeids) + runs_on = session.config.stash[self.runs_on_key] + await self.api_client.submit_tests( + [ + TestSpec(node_id=nodeid, worker_requirements=runs_on.get(nodeid)) + for nodeid in nodeids + ] + ) async def fetch_results(self) -> list[pytest.TestReport]: new_statuses: dict[NodeId, TestStatus] = await self.api_client.fetch_statuses() @@ -255,21 +272,15 @@ def pytest_collection(self, session: pytest.Session): def pytest_runtestloop(self, session: pytest.Session): # TODO: shutdown handling + self.results = TestResults(set(item.nodeid for item in session.items)) + async def run(): # self.submit_env() # TODO await self.api_client.get_session() await self.submit_tests(session) - asyncio.run(run()) - - return True - - self.results = TestResults(set(item.nodeid for item in session.items)) - - async def run(): while True: new_reports = await self.fetch_results() - for report in new_reports: session.config.hook.pytest_runtest_logreport(report=report) From 8308e2d8e851117c8905a3f3a88f7b638e869e54 Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Tue, 25 Feb 2025 15:53:18 -0800 Subject: [PATCH 36/71] Add empty tests for testing testing --- tests/test_nothing.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 tests/test_nothing.py diff --git a/tests/test_nothing.py b/tests/test_nothing.py new file mode 100644 index 00000000..3373f795 --- /dev/null +++ b/tests/test_nothing.py @@ -0,0 +1,6 @@ +def test_nothing(): + pass + + +def test_fail(): + assert False From 2d1f02948b20786f9566e48f5892b5e20351b4b9 Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Tue, 25 Feb 2025 16:04:16 -0800 Subject: [PATCH 37/71] WIP: mostly working worker --- software/hil/dist_plugin.py | 102 +++++++++++++++++++++++------ software/httpdist_server/server.py | 51 +++++++-------- 2 files changed, 107 insertions(+), 46 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index d1d2bf43..87865d5b 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -4,11 +4,14 @@ from pathlib import Path from typing import TypedDict +import cloudpickle import httpx import pytest +import logging +logger = logging.getLogger(__name__) -PLUGIN_NAME = "dist" +PLUGIN_NAME = "httpdist" @dataclass @@ -116,6 +119,10 @@ async def fetch_report(self, nodeid: NodeId) -> pytest.TestReport: return await self._get(f"session/{self.session_id}/test/{nodeid}/report") + @property + def client(self) -> httpx.AsyncClient: + return self._client + class Worker: """ @@ -141,46 +148,86 @@ def __init__(self, config: pytest.Config): self.config.option.maxprocesses = None self.config.option.basetemp = Path.cwd() / "dist_tmp" + @property + def worker_id(self) -> str: + worker_id = self.config.getoption("httpdist_worker_id") + assert isinstance(worker_id, str) + return worker_id + + @property + def session_id(self) -> str: + session_id = self.config.getoption("httpdist_session_id") + assert isinstance(session_id, str) + return session_id + def process_test(self, nodeid_now: str, nodeid_next: str | None): item_now = self._items_by_nodeid[nodeid_now] item_next = self._items_by_nodeid[nodeid_next] if nodeid_next else None self.config.hook.pytest_runtest_protocol(item=item_now, nextitem=item_next) - def signal_ready(self): ... + async def signal_ready(self): ... + + async def signal_done(self): ... + + async def fetch_work(self) -> tuple[NodeId, NodeId | None]: + response = await self.api_client.client.get( + f"{self.api_client.API_URL}/worker/{self.worker_id}/session/{self.session_id}/tests" + ) + try: + response.raise_for_status() + except Exception as e: + logger.exception(f"Failed to fetch work: {e}") + raise - def signal_done(self): ... + data = response.json() + if data["action"] == "stop": + raise EndOfSession() - def fetch_work(self) -> tuple[NodeId, NodeId | None]: ... + return data["test_now"], data["test_next"] - def report_result(self, nodeid: NodeId, report: pytest.TestReport): ... + async def report_result(self, nodeid: NodeId, report: pytest.TestReport): + response = await self.api_client.client.post( + f"{self.api_client.API_URL}/worker/session/{self.session_id}/test/{nodeid}/report", + json={"report": cloudpickle.dumps(report)}, + ) + try: + response.raise_for_status() + except Exception as e: + logger.exception(f"Failed to report result for {nodeid}: {e}") + raise + else: + logger.info(f"Reported result for {nodeid}") - def upload_artifacts(self): ... + async def upload_artifacts(self): ... @pytest.hookimpl(tryfirst=True) def pytest_runtestloop(self, session: pytest.Session): self.session = session self._items_by_nodeid = {item.nodeid: item for item in session.items} - self.signal_ready() + async def _run_tests(): + await self.signal_ready() + + while True: + try: + nodeid_now, nodeid_next = await self.fetch_work() + self.process_test(nodeid_now, nodeid_next) + except EndOfSession: + break - while True: - try: - nodeid_now, nodeid_next = self.fetch_work() - self.process_test(nodeid_now, nodeid_next) - except EndOfSession: - break + await self.signal_done() - self.signal_done() + asyncio.run(_run_tests()) return True @pytest.hookimpl def pytest_runtest_logreport(self, report: pytest.TestReport) -> None: - self.report_result(report.nodeid, report) + asyncio.run(self.report_result(report.nodeid, report)) @pytest.hookimpl def pytest_sessionfinish(self, session: pytest.Session, exitstatus: int): - self.upload_artifacts() + asyncio.run(self.upload_artifacts()) class TestResults: @@ -295,12 +342,27 @@ async def run(): @pytest.hookimpl(tryfirst=True) -def pytest_configure(config): - is_worker = config.getoption("worker") +def pytest_configure(config: pytest.Config): + httpdist_worker_id = config.getoption("httpdist_worker_id") + httpdist_session_id = config.getoption("httpdist_session_id") + + is_worker = httpdist_worker_id or httpdist_session_id + + if is_worker and not httpdist_worker_id: + raise pytest.UsageError( + "httpdist-worker-id is required when running as a worker" + ) + + if is_worker and not httpdist_session_id: + raise pytest.UsageError( + "httpdist-session-id is required when running as a worker" + ) + session = Worker(config) if is_worker else Client(config) config.pluginmanager.register(session, PLUGIN_NAME) @pytest.hookimpl -def pytest_addoption(parser, pluginmanager): - parser.addoption("--worker", help="Run as a worker node", default=False) +def pytest_addoption(parser: pytest.Parser, pluginmanager): + parser.addoption("--httpdist-worker-id", help="Worker ID", default=None) + parser.addoption("--httpdist-session-id", help="Session ID", default=None) diff --git a/software/httpdist_server/server.py b/software/httpdist_server/server.py index dc3b9202..6a1a6a0e 100644 --- a/software/httpdist_server/server.py +++ b/software/httpdist_server/server.py @@ -1,8 +1,8 @@ import base64 from collections.abc import AsyncGenerator +import logging import uuid from dataclasses import dataclass, field -from datetime import datetime, timedelta from typing import Literal import fastapi @@ -19,6 +19,8 @@ TestReport, ) +logger = logging.getLogger(__name__) + app = fastapi.FastAPI() @@ -28,18 +30,6 @@ class Worker: pet_name: str tags: set[str] - last_heartbeat: datetime | None = None - - @property - def is_alive(self) -> bool: - return ( - self.last_heartbeat is not None - and self.last_heartbeat > datetime.now() - timedelta(seconds=30) - ) - - def heartbeat(self): - self.last_heartbeat = datetime.now() - @dataclass class Session: @@ -65,8 +55,28 @@ async def stop(self): # TODO: stick this in a database or something -sessions: dict[str, Session] = {} -workers: list[Worker] = [] +sessions: dict[str, Session] = { + "test-session": Session( + session_id="test-session", + tests={ + "tests/test_nothing.py::test_nothing": Session.Test( + node_id="tests/test_nothing.py::test_nothing", + worker_requirements={"other"}, + ), + "tests/test_nothing.py::test_fail": Session.Test( + node_id="tests/test_nothing.py::test_fail", + worker_requirements={"cellsim"}, + ), + }, + ), +} +workers: list[Worker] = [ + Worker( + worker_id="2ccf6728745b", + pet_name="chunky-otter", + tags={"cellsim"}, + ) +] @app.get("/") @@ -150,17 +160,6 @@ async def get_finished_tests(session_id: str) -> GetSessionTestsResponse: ) -@app.post("/worker/{worker_id}/heartbeat") -async def heartbeat(worker_id: str): - """Heartbeat for a worker""" - for worker in workers: - if worker.worker_id == worker_id: - worker.heartbeat() - return {"message": "Heartbeat received"} - - raise fastapi.HTTPException(status_code=404, detail="Worker not found") - - @app.post("/worker/session/{session_id}/test/{test_id}/report") async def post_test_report( session_id: str, test_id: str, request: PostWorkerSessionTestReportRequest From 712716c2c7eb75dde1402c4d7a2d5d8668ebed61 Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Tue, 25 Feb 2025 16:25:38 -0800 Subject: [PATCH 38/71] WIP: dist plugin disting --- software/hil/dist_plugin.py | 9 ++++++++- software/httpdist_server/server.py | 10 ++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index 87865d5b..05a76a20 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -137,6 +137,7 @@ class Worker: def __init__(self, config: pytest.Config): self.config = config self.api_client = ApiClient(config) + self._reporting_tasks: list[asyncio.Task] = [] # TODO: review # self.config.option.loadgroup = self.config.getvalue("dist") == "loadgroup" @@ -217,13 +218,19 @@ async def _run_tests(): await self.signal_done() + await asyncio.gather(*self._reporting_tasks) + asyncio.run(_run_tests()) return True @pytest.hookimpl def pytest_runtest_logreport(self, report: pytest.TestReport) -> None: - asyncio.run(self.report_result(report.nodeid, report)) + # This hook is called from within the runtestloop, so we need to run + # it in a task instead of calling it directly + self._reporting_tasks.append( + asyncio.create_task(self.report_result(report.nodeid, report)) + ) @pytest.hookimpl def pytest_sessionfinish(self, session: pytest.Session, exitstatus: int): diff --git a/software/httpdist_server/server.py b/software/httpdist_server/server.py index 6a1a6a0e..e389b438 100644 --- a/software/httpdist_server/server.py +++ b/software/httpdist_server/server.py @@ -61,7 +61,7 @@ async def stop(self): tests={ "tests/test_nothing.py::test_nothing": Session.Test( node_id="tests/test_nothing.py::test_nothing", - worker_requirements={"other"}, + worker_requirements={"cellsim"}, ), "tests/test_nothing.py::test_fail": Session.Test( node_id="tests/test_nothing.py::test_fail", @@ -250,8 +250,14 @@ async def get_session_tests(worker_id: str, session_id: str): worker_testable: list[str] = [] for test in sessions[session_id].tests.values(): - if test.status == "pending" and test.worker_requirements.issubset(worker.tags): + if ( + test.status == "pending" + and test.assigned_worker is None + and test.worker_requirements.issubset(worker.tags) + ): worker_testable.append(test.node_id) + test.assigned_worker = worker + test.status = "running" if len(worker_testable) >= 2: break From 9443ddd9c571fbe6b07b2638786c10d7494e0689 Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Tue, 25 Feb 2025 16:30:20 -0800 Subject: [PATCH 39/71] Cloudpickle test report --- software/hil/dist_plugin.py | 3 ++- software/httpdist_server/models.py | 12 ++---------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index 05a76a20..5345c903 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -1,4 +1,5 @@ import asyncio +import base64 from dataclasses import dataclass from enum import StrEnum, auto from pathlib import Path @@ -189,7 +190,7 @@ async def fetch_work(self) -> tuple[NodeId, NodeId | None]: async def report_result(self, nodeid: NodeId, report: pytest.TestReport): response = await self.api_client.client.post( f"{self.api_client.API_URL}/worker/session/{self.session_id}/test/{nodeid}/report", - json={"report": cloudpickle.dumps(report)}, + json={"report": base64.b64encode(cloudpickle.dumps(report)).decode()}, ) try: response.raise_for_status() diff --git a/software/httpdist_server/models.py b/software/httpdist_server/models.py index 1e0824ff..35e826e0 100644 --- a/software/httpdist_server/models.py +++ b/software/httpdist_server/models.py @@ -1,9 +1,6 @@ -import cloudpickle from pydantic import BaseModel from typing import Literal -import pytest - class GetSessionResponse(BaseModel): """Response to a request to start a new session""" @@ -23,13 +20,8 @@ class GetSessionTestsResponse(BaseModel): test_status: dict[str, Literal["pending", "running", "finished"]] -class TestReport(bytes): - @staticmethod - def from_report(report: pytest.TestReport) -> "TestReport": - return TestReport(cloudpickle.dumps(report)) - - def as_report(self) -> pytest.TestReport: - return cloudpickle.loads(self) +class TestReport(str): + """Represents a pytest TestReport as a cloudpickled and base64 encoded object""" class PostWorkerRegisterRequest(BaseModel): From 1b9705cd191c76f655c41f5b5f78bc196eef08c2 Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Tue, 25 Feb 2025 16:50:53 -0800 Subject: [PATCH 40/71] WIP; dist worker appears to be working --- software/hil/dist_plugin.py | 7 +++++-- software/httpdist_server/models.py | 7 ++----- software/httpdist_server/server.py | 26 ++++++++------------------ 3 files changed, 15 insertions(+), 25 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index 5345c903..773e368d 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -189,8 +189,11 @@ async def fetch_work(self) -> tuple[NodeId, NodeId | None]: async def report_result(self, nodeid: NodeId, report: pytest.TestReport): response = await self.api_client.client.post( - f"{self.api_client.API_URL}/worker/session/{self.session_id}/test/{nodeid}/report", - json={"report": base64.b64encode(cloudpickle.dumps(report)).decode()}, + f"{self.api_client.API_URL}/worker/session/{self.session_id}/test/report", + json={ + "node_id": nodeid, + "report": base64.b64encode(cloudpickle.dumps(report)).decode(), + }, ) try: response.raise_for_status() diff --git a/software/httpdist_server/models.py b/software/httpdist_server/models.py index 35e826e0..a2c71e07 100644 --- a/software/httpdist_server/models.py +++ b/software/httpdist_server/models.py @@ -20,10 +20,6 @@ class GetSessionTestsResponse(BaseModel): test_status: dict[str, Literal["pending", "running", "finished"]] -class TestReport(str): - """Represents a pytest TestReport as a cloudpickled and base64 encoded object""" - - class PostWorkerRegisterRequest(BaseModel): worker_id: str pet_name: str @@ -31,7 +27,8 @@ class PostWorkerRegisterRequest(BaseModel): class PostWorkerSessionTestReportRequest(BaseModel): - report: bytes + node_id: str + report: str class GetWorkerSessionTestsResponse(BaseModel): diff --git a/software/httpdist_server/server.py b/software/httpdist_server/server.py index e389b438..b61f9ac0 100644 --- a/software/httpdist_server/server.py +++ b/software/httpdist_server/server.py @@ -1,5 +1,3 @@ -import base64 -from collections.abc import AsyncGenerator import logging import uuid from dataclasses import dataclass, field @@ -8,7 +6,6 @@ import fastapi import uvicorn from fastapi import UploadFile -from fastapi.responses import StreamingResponse from pydantic import BaseModel from httpdist_server.models import ( @@ -16,7 +13,6 @@ GetWorkerSessionTestsResponse, PostSessionsTestsRequest, PostWorkerSessionTestReportRequest, - TestReport, ) logger = logging.getLogger(__name__) @@ -40,7 +36,7 @@ class Test: status: Literal["pending", "running", "finished"] = "pending" assigned_worker: Worker | None = None - report: bytes | None = None + report: str | None = None session_id: str @@ -160,26 +156,26 @@ async def get_finished_tests(session_id: str) -> GetSessionTestsResponse: ) -@app.post("/worker/session/{session_id}/test/{test_id}/report") +@app.post("/worker/session/{session_id}/test/report") async def post_test_report( - session_id: str, test_id: str, request: PostWorkerSessionTestReportRequest + session_id: str, request: PostWorkerSessionTestReportRequest ): """Upload the result for a test""" if session_id not in sessions: raise fastapi.HTTPException(status_code=404, detail="Session not found") - if test_id not in sessions[session_id].tests: + if request.node_id not in sessions[session_id].tests: raise fastapi.HTTPException(status_code=404, detail="Test not found") - test = sessions[session_id].tests[test_id] - test.report = base64.b64decode(request.report) + test = sessions[session_id].tests[request.node_id] + test.report = request.report test.status = "finished" return {"message": "Test result uploaded successfully"} @app.get("/session/{session_id}/test/{test_id}/report") -async def get_test_report(session_id: str, test_id: str) -> StreamingResponse: +async def get_test_report(session_id: str, test_id: str) -> str: """Get the report for a test""" if session_id not in sessions: raise fastapi.HTTPException(status_code=404, detail="Session not found") @@ -191,13 +187,7 @@ async def get_test_report(session_id: str, test_id: str) -> StreamingResponse: if test.report is None: raise fastapi.HTTPException(status_code=404, detail="Test report not found") - async def report_generator(report: bytes) -> AsyncGenerator[bytes, None]: - report_data = TestReport(report) - yield report_data - - return StreamingResponse( - report_generator(test.report), media_type="application/octet-stream" - ) + return test.report @app.get("/worker/{worker_id}/get-session") From 70fb5131e5c91fb5271ac8ef9cd5dcba39705b23 Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Tue, 25 Feb 2025 17:03:39 -0800 Subject: [PATCH 41/71] No node_ids in URL --- software/httpdist_server/models.py | 4 ++++ software/httpdist_server/server.py | 11 ++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/software/httpdist_server/models.py b/software/httpdist_server/models.py index a2c71e07..8e897a0d 100644 --- a/software/httpdist_server/models.py +++ b/software/httpdist_server/models.py @@ -26,6 +26,10 @@ class PostWorkerRegisterRequest(BaseModel): tags: list[str] +class GetSessionTestReportRequest(BaseModel): + node_id: str + + class PostWorkerSessionTestReportRequest(BaseModel): node_id: str report: str diff --git a/software/httpdist_server/server.py b/software/httpdist_server/server.py index b61f9ac0..eca27777 100644 --- a/software/httpdist_server/server.py +++ b/software/httpdist_server/server.py @@ -9,6 +9,7 @@ from pydantic import BaseModel from httpdist_server.models import ( + GetSessionTestReportRequest, GetSessionTestsResponse, GetWorkerSessionTestsResponse, PostSessionsTestsRequest, @@ -157,7 +158,7 @@ async def get_finished_tests(session_id: str) -> GetSessionTestsResponse: @app.post("/worker/session/{session_id}/test/report") -async def post_test_report( +async def submit_test_report( session_id: str, request: PostWorkerSessionTestReportRequest ): """Upload the result for a test""" @@ -174,16 +175,16 @@ async def post_test_report( return {"message": "Test result uploaded successfully"} -@app.get("/session/{session_id}/test/{test_id}/report") -async def get_test_report(session_id: str, test_id: str) -> str: +@app.post("/session/{session_id}/test/report") +async def query_test_report(session_id: str, request: GetSessionTestReportRequest): """Get the report for a test""" if session_id not in sessions: raise fastapi.HTTPException(status_code=404, detail="Session not found") - if test_id not in sessions[session_id].tests: + if request.node_id not in sessions[session_id].tests: raise fastapi.HTTPException(status_code=404, detail="Test not found") - test = sessions[session_id].tests[test_id] + test = sessions[session_id].tests[request.node_id] if test.report is None: raise fastapi.HTTPException(status_code=404, detail="Test report not found") From c30ee94fa94197afb91807407662053021fc386c Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Tue, 25 Feb 2025 17:05:19 -0800 Subject: [PATCH 42/71] Split API classes --- software/hil/dist_plugin.py | 90 ++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 51 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index 773e368d..21925e13 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -29,6 +29,7 @@ def check(self, node) -> bool: NodeId = str SessionId = str +WorkerId = str class TestSpec(TypedDict): @@ -72,7 +73,7 @@ class SessionNotStartedError(ApiUsageError): pass -class ApiClient: +class ApiBase: # FIXME API_URL = "http://localhost:8000" session_id: SessionId | None = None @@ -91,7 +92,9 @@ async def _get(self, path: str, params: dict | None = None): response.raise_for_status() return response.json() - async def get_session(self) -> SessionId: + +class ClientApi(ApiBase): + async def get_client_session(self) -> SessionId: session = await self._get("get-session") session_id = session["session_id"] self.session_id = session_id @@ -120,9 +123,30 @@ async def fetch_report(self, nodeid: NodeId) -> pytest.TestReport: return await self._get(f"session/{self.session_id}/test/{nodeid}/report") - @property - def client(self) -> httpx.AsyncClient: - return self._client + +class WorkerApi(ApiBase): + async def signal_ready(self): ... + + async def signal_done(self): ... + + async def fetch_work(self, worker_id: WorkerId) -> tuple[NodeId, NodeId | None]: + data = await self._get(f"worker/{worker_id}/session/{self.session_id}/tests") + + if data["action"] == "stop": + raise EndOfSession() + + return data["test_now"], data["test_next"] + + async def report_result(self, nodeid: NodeId, report: pytest.TestReport): + await self._post( + f"worker/session/{self.session_id}/test/report", + { + "node_id": nodeid, + "report": base64.b64encode(cloudpickle.dumps(report)).decode(), + }, + ) + + async def upload_artifacts(self): ... class Worker: @@ -137,7 +161,7 @@ class Worker: def __init__(self, config: pytest.Config): self.config = config - self.api_client = ApiClient(config) + self.api_client = WorkerApi(config) self._reporting_tasks: list[asyncio.Task] = [] # TODO: review @@ -167,60 +191,24 @@ def process_test(self, nodeid_now: str, nodeid_next: str | None): item_next = self._items_by_nodeid[nodeid_next] if nodeid_next else None self.config.hook.pytest_runtest_protocol(item=item_now, nextitem=item_next) - async def signal_ready(self): ... - - async def signal_done(self): ... - - async def fetch_work(self) -> tuple[NodeId, NodeId | None]: - response = await self.api_client.client.get( - f"{self.api_client.API_URL}/worker/{self.worker_id}/session/{self.session_id}/tests" - ) - try: - response.raise_for_status() - except Exception as e: - logger.exception(f"Failed to fetch work: {e}") - raise - - data = response.json() - if data["action"] == "stop": - raise EndOfSession() - - return data["test_now"], data["test_next"] - - async def report_result(self, nodeid: NodeId, report: pytest.TestReport): - response = await self.api_client.client.post( - f"{self.api_client.API_URL}/worker/session/{self.session_id}/test/report", - json={ - "node_id": nodeid, - "report": base64.b64encode(cloudpickle.dumps(report)).decode(), - }, - ) - try: - response.raise_for_status() - except Exception as e: - logger.exception(f"Failed to report result for {nodeid}: {e}") - raise - else: - logger.info(f"Reported result for {nodeid}") - - async def upload_artifacts(self): ... - @pytest.hookimpl(tryfirst=True) def pytest_runtestloop(self, session: pytest.Session): self.session = session self._items_by_nodeid = {item.nodeid: item for item in session.items} async def _run_tests(): - await self.signal_ready() + await self.api_client.signal_ready() while True: try: - nodeid_now, nodeid_next = await self.fetch_work() + nodeid_now, nodeid_next = await self.api_client.fetch_work( + self.worker_id + ) self.process_test(nodeid_now, nodeid_next) except EndOfSession: break - await self.signal_done() + await self.api_client.signal_done() await asyncio.gather(*self._reporting_tasks) @@ -233,12 +221,12 @@ def pytest_runtest_logreport(self, report: pytest.TestReport) -> None: # This hook is called from within the runtestloop, so we need to run # it in a task instead of calling it directly self._reporting_tasks.append( - asyncio.create_task(self.report_result(report.nodeid, report)) + asyncio.create_task(self.api_client.report_result(report.nodeid, report)) ) @pytest.hookimpl def pytest_sessionfinish(self, session: pytest.Session, exitstatus: int): - asyncio.run(self.upload_artifacts()) + asyncio.run(self.api_client.upload_artifacts()) class TestResults: @@ -280,7 +268,7 @@ class Client: def __init__(self, config: pytest.Config): self.config = config - self.api_client = ApiClient(config) + self.api_client = ClientApi(config) self.statuses = {} def submit_env(self): ... @@ -334,7 +322,7 @@ def pytest_runtestloop(self, session: pytest.Session): async def run(): # self.submit_env() # TODO - await self.api_client.get_session() + await self.api_client.get_client_session() await self.submit_tests(session) while True: From bef144ff6c2b78e8dc8b14b670947ecfe4969747 Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Tue, 25 Feb 2025 17:07:08 -0800 Subject: [PATCH 43/71] Fix session_id for worker API --- software/hil/dist_plugin.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index 21925e13..da53dad0 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -125,6 +125,12 @@ async def fetch_report(self, nodeid: NodeId) -> pytest.TestReport: class WorkerApi(ApiBase): + def __init__(self, config: pytest.Config): + super().__init__(config) + session_id = config.getoption("httpdist_session_id") + assert isinstance(session_id, str) + self.session_id = session_id + async def signal_ready(self): ... async def signal_done(self): ... @@ -180,12 +186,6 @@ def worker_id(self) -> str: assert isinstance(worker_id, str) return worker_id - @property - def session_id(self) -> str: - session_id = self.config.getoption("httpdist_session_id") - assert isinstance(session_id, str) - return session_id - def process_test(self, nodeid_now: str, nodeid_next: str | None): item_now = self._items_by_nodeid[nodeid_now] item_next = self._items_by_nodeid[nodeid_next] if nodeid_next else None From 76c61c312beddba6df5a58447239d3fd70b6afe0 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 25 Feb 2025 17:08:01 -0800 Subject: [PATCH 44/71] fix api usage --- software/hil/dist_plugin.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index da53dad0..2bc1f06f 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -114,14 +114,16 @@ async def submit_tests(self, tests: list[TestSpec]): async def fetch_statuses(self) -> dict[NodeId, TestStatus]: if self.session_id is None: raise SessionNotStartedError("Must have an active session") - - return await self._get(f"session/{self.session_id}/finished-tests") + response = await self._get(f"session/{self.session_id}/finished-tests") + return response["test_status"] async def fetch_report(self, nodeid: NodeId) -> pytest.TestReport: if self.session_id is None: raise SessionNotStartedError("Must have an active session") - return await self._get(f"session/{self.session_id}/test/{nodeid}/report") + return await self._post( + f"session/{self.session_id}/test/report", {"node_id": nodeid} + ) class WorkerApi(ApiBase): From a316b7fd8154042b8c1f6439ddacac3727948a3f Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 25 Feb 2025 17:10:26 -0800 Subject: [PATCH 45/71] fix adding new test report --- software/hil/dist_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index 2bc1f06f..48f9e902 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -247,7 +247,7 @@ def add(self, nodeid: NodeId, report: pytest.TestReport): if nodeid not in self.nodeids: raise ValueError(f"Unknown nodeid: {nodeid}") - if self.reports[nodeid] is not None: + if self.reports.get(nodeid) is not None: raise ValueError(f"Test result already set for {nodeid}") self.reports[nodeid] = report From 8477c3db971b69a787856a5844ecc0ed7e8cf34e Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 25 Feb 2025 17:15:43 -0800 Subject: [PATCH 46/71] decode testreport --- software/hil/dist_plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index 48f9e902..b111786b 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -121,9 +121,10 @@ async def fetch_report(self, nodeid: NodeId) -> pytest.TestReport: if self.session_id is None: raise SessionNotStartedError("Must have an active session") - return await self._post( + report = await self._post( f"session/{self.session_id}/test/report", {"node_id": nodeid} ) + return cloudpickle.loads(base64.b64decode(report)) class WorkerApi(ApiBase): From ea5da7ae4645df83227166e34ab3ed3a31bb54df Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Tue, 25 Feb 2025 17:16:05 -0800 Subject: [PATCH 47/71] Overly log --- software/hil/dist_plugin.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index b111786b..7d3cf454 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -141,18 +141,21 @@ async def signal_done(self): ... async def fetch_work(self, worker_id: WorkerId) -> tuple[NodeId, NodeId | None]: data = await self._get(f"worker/{worker_id}/session/{self.session_id}/tests") + logger.info(f"Received work: {data}") + if data["action"] == "stop": raise EndOfSession() return data["test_now"], data["test_next"] async def report_result(self, nodeid: NodeId, report: pytest.TestReport): + response = { + "node_id": nodeid, + "report": base64.b64encode(cloudpickle.dumps(report)).decode(), + } await self._post( f"worker/session/{self.session_id}/test/report", - { - "node_id": nodeid, - "report": base64.b64encode(cloudpickle.dumps(report)).decode(), - }, + response, ) async def upload_artifacts(self): ... @@ -207,8 +210,10 @@ async def _run_tests(): nodeid_now, nodeid_next = await self.api_client.fetch_work( self.worker_id ) + logger.info(f"Received work: {nodeid_now}, {nodeid_next}") self.process_test(nodeid_now, nodeid_next) except EndOfSession: + logger.info("Received end of session signal") break await self.api_client.signal_done() From 2f8b59aef866711639857e04930fa646cdc489c2 Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Tue, 25 Feb 2025 17:17:58 -0800 Subject: [PATCH 48/71] Ensure we run both tests --- software/httpdist_server/server.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/software/httpdist_server/server.py b/software/httpdist_server/server.py index eca27777..15d52f8f 100644 --- a/software/httpdist_server/server.py +++ b/software/httpdist_server/server.py @@ -247,11 +247,11 @@ async def get_session_tests(worker_id: str, session_id: str): and test.worker_requirements.issubset(worker.tags) ): worker_testable.append(test.node_id) - test.assigned_worker = worker - test.status = "running" - - if len(worker_testable) >= 2: - break + if len(worker_testable) == 1: + test.assigned_worker = worker + test.status = "running" + elif len(worker_testable) >= 2: + break return GetWorkerSessionTestsResponse( action="run" if len(worker_testable) > 0 else "stop", From 0291c00d16b72317a130344072c4110944c2e0c3 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 25 Feb 2025 18:07:23 -0800 Subject: [PATCH 49/71] per-phase test reports --- software/hil/dist_plugin.py | 65 ++++++++++++++++++++---------- software/httpdist_server/models.py | 3 +- software/httpdist_server/server.py | 35 +++++++++------- 3 files changed, 66 insertions(+), 37 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index 7d3cf454..764c83ed 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from enum import StrEnum, auto from pathlib import Path -from typing import TypedDict +from typing import Literal, TypedDict, cast import cloudpickle import httpx @@ -111,18 +111,22 @@ async def submit_tests(self, tests: list[TestSpec]): raise ApiUsageError(e.response.text) raise - async def fetch_statuses(self) -> dict[NodeId, TestStatus]: + async def fetch_statuses( + self, + ) -> dict[NodeId, list[Literal["setup", "call", "teardown"]]]: if self.session_id is None: raise SessionNotStartedError("Must have an active session") response = await self._get(f"session/{self.session_id}/finished-tests") return response["test_status"] - async def fetch_report(self, nodeid: NodeId) -> pytest.TestReport: + async def fetch_report( + self, nodeid: NodeId, phase: Literal["setup", "call", "teardown"] + ) -> pytest.TestReport: if self.session_id is None: raise SessionNotStartedError("Must have an active session") report = await self._post( - f"session/{self.session_id}/test/report", {"node_id": nodeid} + f"session/{self.session_id}/test/report/{phase}", {"node_id": nodeid} ) return cloudpickle.loads(base64.b64decode(report)) @@ -148,15 +152,18 @@ async def fetch_work(self, worker_id: WorkerId) -> tuple[NodeId, NodeId | None]: return data["test_now"], data["test_next"] - async def report_result(self, nodeid: NodeId, report: pytest.TestReport): - response = { + async def report_result( + self, + nodeid: NodeId, + report: pytest.TestReport, + phase: Literal["setup", "call", "teardown"], + ): + data = { "node_id": nodeid, "report": base64.b64encode(cloudpickle.dumps(report)).decode(), + "phase": phase, } - await self._post( - f"worker/session/{self.session_id}/test/report", - response, - ) + await self._post(f"worker/session/{self.session_id}/test/report", data) async def upload_artifacts(self): ... @@ -229,7 +236,9 @@ def pytest_runtest_logreport(self, report: pytest.TestReport) -> None: # This hook is called from within the runtestloop, so we need to run # it in a task instead of calling it directly self._reporting_tasks.append( - asyncio.create_task(self.api_client.report_result(report.nodeid, report)) + asyncio.create_task( + self.api_client.report_result(report.nodeid, report, phase=report.when) + ) ) @pytest.hookimpl @@ -239,7 +248,7 @@ def pytest_sessionfinish(self, session: pytest.Session, exitstatus: int): class TestResults: nodeids: set[NodeId] - reports: dict[NodeId, pytest.TestReport] + reports: dict[NodeId, dict[Literal["setup", "call", "teardown"], pytest.TestReport]] def __init__(self, nodeids: set[NodeId]): self.nodeids = nodeids @@ -247,16 +256,24 @@ def __init__(self, nodeids: set[NodeId]): @property def all_done(self) -> bool: - return len(self.reports) == len(self.nodeids) + return len(self.reports) == len(self.nodeids) and all( + "teardown" in phases for phases in self.reports.values() + ) def add(self, nodeid: NodeId, report: pytest.TestReport): if nodeid not in self.nodeids: raise ValueError(f"Unknown nodeid: {nodeid}") - if self.reports.get(nodeid) is not None: - raise ValueError(f"Test result already set for {nodeid}") + if report.when not in ["setup", "call", "teardown"]: + raise ValueError(f"Unknown phase: {report.when}") + + phase = cast(Literal["setup", "call", "teardown"], report.when) + + if (reports := self.reports.get(nodeid)) is not None: + if phase in reports: + raise ValueError(f"Test result already set for {nodeid}, phase {phase}") - self.reports[nodeid] = report + self.reports[nodeid] = self.reports.get(nodeid, {}) | {phase: report} # type: ignore # TODO class Client: @@ -272,7 +289,7 @@ class Client: runs_on_key = pytest.StashKey[dict[str, list[RunsOn]]]() results: TestResults - statuses: dict[NodeId, TestStatus] + statuses: dict[NodeId, list[Literal["setup", "call", "teardown"]]] def __init__(self, config: pytest.Config): self.config = config @@ -292,13 +309,17 @@ async def submit_tests(self, session: pytest.Session): ) async def fetch_results(self) -> list[pytest.TestReport]: - new_statuses: dict[NodeId, TestStatus] = await self.api_client.fetch_statuses() + new_statuses: dict[ + NodeId, list[Literal["setup", "call", "teardown"]] + ] = await self.api_client.fetch_statuses() new_reports: list[pytest.TestReport] = [] - for nodeid in new_statuses.keys() - self.statuses.keys(): - new_report = await self.api_client.fetch_report(nodeid) - self.results.add(nodeid, new_report) - new_reports.append(new_report) + for nodeid in new_statuses.keys(): + for phase in new_statuses[nodeid]: + if phase not in self.statuses[nodeid]: + new_report = await self.api_client.fetch_report(nodeid, phase) + self.results.add(nodeid, new_report) + new_reports.append(new_report) self.statuses = new_statuses diff --git a/software/httpdist_server/models.py b/software/httpdist_server/models.py index 8e897a0d..b2dfa9ca 100644 --- a/software/httpdist_server/models.py +++ b/software/httpdist_server/models.py @@ -17,7 +17,7 @@ class Test(BaseModel): class GetSessionTestsResponse(BaseModel): - test_status: dict[str, Literal["pending", "running", "finished"]] + test_status: dict[str, list[Literal["setup", "call", "teardown"]]] class PostWorkerRegisterRequest(BaseModel): @@ -32,6 +32,7 @@ class GetSessionTestReportRequest(BaseModel): class PostWorkerSessionTestReportRequest(BaseModel): node_id: str + phase: Literal["setup", "call", "teardown"] report: str diff --git a/software/httpdist_server/server.py b/software/httpdist_server/server.py index 15d52f8f..d877a26c 100644 --- a/software/httpdist_server/server.py +++ b/software/httpdist_server/server.py @@ -37,7 +37,9 @@ class Test: status: Literal["pending", "running", "finished"] = "pending" assigned_worker: Worker | None = None - report: str | None = None + reports: dict[Literal["setup", "call", "teardown"], str | None] = field( + default_factory=lambda: {"setup": None, "call": None, "teardown": None} + ) session_id: str @@ -148,13 +150,13 @@ async def get_finished_tests(session_id: str) -> GetSessionTestsResponse: if session_id not in sessions: raise fastapi.HTTPException(status_code=404, detail="Session not found") - return GetSessionTestsResponse( - test_status={ - test.node_id: test.status - for test in sessions[session_id].tests.values() - if test.status == "finished" - } - ) + completed_phases: dict[str, list[Literal["setup", "call", "teardown"]]] = { + test.node_id: [ + phase for phase in test.reports.keys() if test.reports[phase] is not None + ] + for test in sessions[session_id].tests.values() # type: ignore + } + return GetSessionTestsResponse(test_status=completed_phases) @app.post("/worker/session/{session_id}/test/report") @@ -169,14 +171,19 @@ async def submit_test_report( raise fastapi.HTTPException(status_code=404, detail="Test not found") test = sessions[session_id].tests[request.node_id] - test.report = request.report - test.status = "finished" + test.reports[request.phase] = request.report + if request.phase == "teardown": # TODO + test.status = "finished" return {"message": "Test result uploaded successfully"} -@app.post("/session/{session_id}/test/report") -async def query_test_report(session_id: str, request: GetSessionTestReportRequest): +@app.post("/session/{session_id}/test/report/{phase}") +async def query_test_report( + session_id: str, + phase: Literal["setup", "call", "teardown"], + request: GetSessionTestReportRequest, +): """Get the report for a test""" if session_id not in sessions: raise fastapi.HTTPException(status_code=404, detail="Session not found") @@ -185,10 +192,10 @@ async def query_test_report(session_id: str, request: GetSessionTestReportReques raise fastapi.HTTPException(status_code=404, detail="Test not found") test = sessions[session_id].tests[request.node_id] - if test.report is None: + if test.reports[phase] is None: raise fastapi.HTTPException(status_code=404, detail="Test report not found") - return test.report + return test.reports[phase] @app.get("/worker/{worker_id}/get-session") From d8986ef145594ce44e5b4c9d12b8efba7d8feeff Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 25 Feb 2025 18:07:33 -0800 Subject: [PATCH 50/71] quiet --- software/hil/dist_plugin.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index 764c83ed..35043043 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -12,6 +12,9 @@ logger = logging.getLogger(__name__) +httpx_logger = logging.getLogger("httpx") +httpx_logger.setLevel(logging.WARNING) + PLUGIN_NAME = "httpdist" @@ -145,8 +148,6 @@ async def signal_done(self): ... async def fetch_work(self, worker_id: WorkerId) -> tuple[NodeId, NodeId | None]: data = await self._get(f"worker/{worker_id}/session/{self.session_id}/tests") - logger.info(f"Received work: {data}") - if data["action"] == "stop": raise EndOfSession() @@ -217,10 +218,8 @@ async def _run_tests(): nodeid_now, nodeid_next = await self.api_client.fetch_work( self.worker_id ) - logger.info(f"Received work: {nodeid_now}, {nodeid_next}") self.process_test(nodeid_now, nodeid_next) except EndOfSession: - logger.info("Received end of session signal") break await self.api_client.signal_done() From 261467363d8b1e198a3eea5511a3da3c985bfe8d Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 25 Feb 2025 18:10:37 -0800 Subject: [PATCH 51/71] TestPhase enum --- software/hil/dist_plugin.py | 47 +++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index 35043043..e1e7b799 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from enum import StrEnum, auto from pathlib import Path -from typing import Literal, TypedDict, cast +from typing import TypedDict import cloudpickle import httpx @@ -35,19 +35,17 @@ def check(self, node) -> bool: WorkerId = str +class TestPhase(StrEnum): + Setup = auto() + Call = auto() + Teardown = auto() + + class TestSpec(TypedDict): node_id: NodeId worker_requirements: list[RunsOn] | None -class TestStatus(StrEnum): - # FIXME - Passed = auto() - Failed = auto() - Skipped = auto() - Error = auto() - - class Events: @dataclass class Start: @@ -116,15 +114,13 @@ async def submit_tests(self, tests: list[TestSpec]): async def fetch_statuses( self, - ) -> dict[NodeId, list[Literal["setup", "call", "teardown"]]]: + ) -> dict[NodeId, list[TestPhase]]: if self.session_id is None: raise SessionNotStartedError("Must have an active session") response = await self._get(f"session/{self.session_id}/finished-tests") return response["test_status"] - async def fetch_report( - self, nodeid: NodeId, phase: Literal["setup", "call", "teardown"] - ) -> pytest.TestReport: + async def fetch_report(self, nodeid: NodeId, phase: TestPhase) -> pytest.TestReport: if self.session_id is None: raise SessionNotStartedError("Must have an active session") @@ -154,10 +150,7 @@ async def fetch_work(self, worker_id: WorkerId) -> tuple[NodeId, NodeId | None]: return data["test_now"], data["test_next"] async def report_result( - self, - nodeid: NodeId, - report: pytest.TestReport, - phase: Literal["setup", "call", "teardown"], + self, nodeid: NodeId, report: pytest.TestReport, phase: TestPhase ): data = { "node_id": nodeid, @@ -236,7 +229,9 @@ def pytest_runtest_logreport(self, report: pytest.TestReport) -> None: # it in a task instead of calling it directly self._reporting_tasks.append( asyncio.create_task( - self.api_client.report_result(report.nodeid, report, phase=report.when) + self.api_client.report_result( + report.nodeid, report, phase=TestPhase(report.when) + ) ) ) @@ -247,7 +242,7 @@ def pytest_sessionfinish(self, session: pytest.Session, exitstatus: int): class TestResults: nodeids: set[NodeId] - reports: dict[NodeId, dict[Literal["setup", "call", "teardown"], pytest.TestReport]] + reports: dict[NodeId, dict[TestPhase, pytest.TestReport]] def __init__(self, nodeids: set[NodeId]): self.nodeids = nodeids @@ -256,23 +251,23 @@ def __init__(self, nodeids: set[NodeId]): @property def all_done(self) -> bool: return len(self.reports) == len(self.nodeids) and all( - "teardown" in phases for phases in self.reports.values() + TestPhase.Teardown in phases for phases in self.reports.values() ) def add(self, nodeid: NodeId, report: pytest.TestReport): if nodeid not in self.nodeids: raise ValueError(f"Unknown nodeid: {nodeid}") - if report.when not in ["setup", "call", "teardown"]: + try: + phase = TestPhase(report.when) + except ValueError: raise ValueError(f"Unknown phase: {report.when}") - phase = cast(Literal["setup", "call", "teardown"], report.when) - if (reports := self.reports.get(nodeid)) is not None: if phase in reports: raise ValueError(f"Test result already set for {nodeid}, phase {phase}") - self.reports[nodeid] = self.reports.get(nodeid, {}) | {phase: report} # type: ignore # TODO + self.reports[nodeid] = self.reports.get(nodeid, {}) | {phase: report} class Client: @@ -288,7 +283,7 @@ class Client: runs_on_key = pytest.StashKey[dict[str, list[RunsOn]]]() results: TestResults - statuses: dict[NodeId, list[Literal["setup", "call", "teardown"]]] + statuses: dict[NodeId, list[TestPhase]] def __init__(self, config: pytest.Config): self.config = config @@ -309,7 +304,7 @@ async def submit_tests(self, session: pytest.Session): async def fetch_results(self) -> list[pytest.TestReport]: new_statuses: dict[ - NodeId, list[Literal["setup", "call", "teardown"]] + NodeId, list[TestPhase] ] = await self.api_client.fetch_statuses() new_reports: list[pytest.TestReport] = [] From b70030389a6adcff91f02b9f845acf76ef18a755 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 25 Feb 2025 18:19:33 -0800 Subject: [PATCH 52/71] more enums --- software/httpdist_server/models.py | 33 ++++++++++++++--- software/httpdist_server/server.py | 58 ++++++++++++++++-------------- 2 files changed, 60 insertions(+), 31 deletions(-) diff --git a/software/httpdist_server/models.py b/software/httpdist_server/models.py index b2dfa9ca..1f315efe 100644 --- a/software/httpdist_server/models.py +++ b/software/httpdist_server/models.py @@ -1,5 +1,30 @@ +from enum import StrEnum, auto from pydantic import BaseModel -from typing import Literal + +NodeId = str + + +class TestStatus(StrEnum): + Pending = auto() + Running = auto() + Finished = auto() + + +class TestPhase(StrEnum): + Setup = auto() + Call = auto() + Teardown = auto() + + +class SessionState(StrEnum): + Setup = auto() + Running = auto() + Stopped = auto() + + +class WorkerAction(StrEnum): + Run = auto() + Stop = auto() class GetSessionResponse(BaseModel): @@ -17,7 +42,7 @@ class Test(BaseModel): class GetSessionTestsResponse(BaseModel): - test_status: dict[str, list[Literal["setup", "call", "teardown"]]] + test_status: dict[NodeId, list[TestPhase]] class PostWorkerRegisterRequest(BaseModel): @@ -32,11 +57,11 @@ class GetSessionTestReportRequest(BaseModel): class PostWorkerSessionTestReportRequest(BaseModel): node_id: str - phase: Literal["setup", "call", "teardown"] + phase: TestPhase report: str class GetWorkerSessionTestsResponse(BaseModel): - action: Literal["run", "stop"] + action: WorkerAction test_now: str | None test_next: str | None diff --git a/software/httpdist_server/server.py b/software/httpdist_server/server.py index d877a26c..50cfbe08 100644 --- a/software/httpdist_server/server.py +++ b/software/httpdist_server/server.py @@ -1,7 +1,6 @@ import logging import uuid from dataclasses import dataclass, field -from typing import Literal import fastapi import uvicorn @@ -14,6 +13,11 @@ GetWorkerSessionTestsResponse, PostSessionsTestsRequest, PostWorkerSessionTestReportRequest, + SessionState, + TestPhase, + TestStatus, + NodeId, + WorkerAction, ) logger = logging.getLogger(__name__) @@ -33,22 +37,26 @@ class Session: @dataclass class Test: worker_requirements: set[str] - node_id: str + nodeid: NodeId - status: Literal["pending", "running", "finished"] = "pending" + status: TestStatus = TestStatus.Pending assigned_worker: Worker | None = None - reports: dict[Literal["setup", "call", "teardown"], str | None] = field( - default_factory=lambda: {"setup": None, "call": None, "teardown": None} + reports: dict[TestPhase, str | None] = field( + default_factory=lambda: { + TestPhase.Setup: None, + TestPhase.Call: None, + TestPhase.Teardown: None, + } ) session_id: str - state: Literal["setup", "running", "stopped"] = "setup" + state: SessionState = SessionState.Setup tests: dict[str, Test] = field(default_factory=dict) env: UploadFile | None = None async def stop(self): - self.state = "stopped" + self.state = SessionState.Stopped if self.env is not None: await self.env.close() @@ -59,11 +67,11 @@ async def stop(self): session_id="test-session", tests={ "tests/test_nothing.py::test_nothing": Session.Test( - node_id="tests/test_nothing.py::test_nothing", + nodeid="tests/test_nothing.py::test_nothing", worker_requirements={"cellsim"}, ), "tests/test_nothing.py::test_fail": Session.Test( - node_id="tests/test_nothing.py::test_fail", + nodeid="tests/test_nothing.py::test_fail", worker_requirements={"cellsim"}, ), }, @@ -150,8 +158,8 @@ async def get_finished_tests(session_id: str) -> GetSessionTestsResponse: if session_id not in sessions: raise fastapi.HTTPException(status_code=404, detail="Session not found") - completed_phases: dict[str, list[Literal["setup", "call", "teardown"]]] = { - test.node_id: [ + completed_phases: dict[str, list[TestPhase]] = { + test.nodeid: [ phase for phase in test.reports.keys() if test.reports[phase] is not None ] for test in sessions[session_id].tests.values() # type: ignore @@ -172,17 +180,15 @@ async def submit_test_report( test = sessions[session_id].tests[request.node_id] test.reports[request.phase] = request.report - if request.phase == "teardown": # TODO - test.status = "finished" + if request.phase == TestPhase.Teardown: # TODO + test.status = TestStatus.Finished return {"message": "Test result uploaded successfully"} @app.post("/session/{session_id}/test/report/{phase}") async def query_test_report( - session_id: str, - phase: Literal["setup", "call", "teardown"], - request: GetSessionTestReportRequest, + session_id: str, phase: TestPhase, request: GetSessionTestReportRequest ): """Get the report for a test""" if session_id not in sessions: @@ -204,7 +210,7 @@ async def get_session(worker_id: str) -> str | None: for worker in workers: if worker.worker_id == worker_id: for session in sessions.values(): - if session.state == "running": + if session.state == SessionState.Running: if any( test.worker_requirements.issubset(worker.tags) for test in session.tests.values() @@ -231,13 +237,11 @@ async def get_session_tests(worker_id: str, session_id: str): if session_id not in sessions: raise fastapi.HTTPException(status_code=404, detail="Session not found") - if sessions[session_id].state == "setup": - sessions[session_id].state = "running" - elif sessions[session_id].state == "stopped": + if sessions[session_id].state == SessionState.Setup: + sessions[session_id].state = SessionState.Running + elif sessions[session_id].state == SessionState.Stopped: return GetWorkerSessionTestsResponse( - action="stop", - test_now=None, - test_next=None, + action=WorkerAction.Stop, test_now=None, test_next=None ) for worker in workers: @@ -249,19 +253,19 @@ async def get_session_tests(worker_id: str, session_id: str): worker_testable: list[str] = [] for test in sessions[session_id].tests.values(): if ( - test.status == "pending" + test.status == TestStatus.Pending and test.assigned_worker is None and test.worker_requirements.issubset(worker.tags) ): - worker_testable.append(test.node_id) + worker_testable.append(test.nodeid) if len(worker_testable) == 1: test.assigned_worker = worker - test.status = "running" + test.status = TestStatus.Running elif len(worker_testable) >= 2: break return GetWorkerSessionTestsResponse( - action="run" if len(worker_testable) > 0 else "stop", + action=WorkerAction.Run if len(worker_testable) > 0 else WorkerAction.Stop, test_now=worker_testable[0] if len(worker_testable) > 0 else None, test_next=worker_testable[1] if len(worker_testable) > 1 else None, ) From 15b76eab186a631a1a7deb292461136931762fb6 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 25 Feb 2025 18:20:20 -0800 Subject: [PATCH 53/71] rm duplicate model --- software/httpdist_server/server.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/software/httpdist_server/server.py b/software/httpdist_server/server.py index 50cfbe08..98604048 100644 --- a/software/httpdist_server/server.py +++ b/software/httpdist_server/server.py @@ -5,9 +5,9 @@ import fastapi import uvicorn from fastapi import UploadFile -from pydantic import BaseModel from httpdist_server.models import ( + GetSessionResponse, GetSessionTestReportRequest, GetSessionTestsResponse, GetWorkerSessionTestsResponse, @@ -91,12 +91,6 @@ async def root(): return {"message": "Hello World!"} -class GetSessionResponse(BaseModel): - """Response to a request to start a new session""" - - session_id: str - - @app.get("/get-session") async def start_session(): session_id = str(uuid.uuid4()) From 04972027aae9a2bd22bc9e030a3da3e90db1b58e Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 25 Feb 2025 18:44:05 -0800 Subject: [PATCH 54/71] API design updates --- software/hil/dist_plugin.py | 19 ++--- software/httpdist_server/models.py | 57 +++++++++------ software/httpdist_server/server.py | 108 ++++++++++++++++------------- 3 files changed, 104 insertions(+), 80 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index e1e7b799..0d68f813 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -42,7 +42,7 @@ class TestPhase(StrEnum): class TestSpec(TypedDict): - node_id: NodeId + nodeid: NodeId worker_requirements: list[RunsOn] | None @@ -96,7 +96,7 @@ async def _get(self, path: str, params: dict | None = None): class ClientApi(ApiBase): async def get_client_session(self) -> SessionId: - session = await self._get("get-session") + session = await self._get("session") session_id = session["session_id"] self.session_id = session_id return session_id @@ -117,16 +117,17 @@ async def fetch_statuses( ) -> dict[NodeId, list[TestPhase]]: if self.session_id is None: raise SessionNotStartedError("Must have an active session") - response = await self._get(f"session/{self.session_id}/finished-tests") - return response["test_status"] + response = await self._get(f"session/{self.session_id}/tests") + return response["statuses"] async def fetch_report(self, nodeid: NodeId, phase: TestPhase) -> pytest.TestReport: if self.session_id is None: raise SessionNotStartedError("Must have an active session") - report = await self._post( - f"session/{self.session_id}/test/report/{phase}", {"node_id": nodeid} + response = await self._post( + f"session/{self.session_id}/test/{phase}", {"nodeid": nodeid} ) + report = response["report"] return cloudpickle.loads(base64.b64decode(report)) @@ -153,11 +154,11 @@ async def report_result( self, nodeid: NodeId, report: pytest.TestReport, phase: TestPhase ): data = { - "node_id": nodeid, + "nodeid": nodeid, "report": base64.b64encode(cloudpickle.dumps(report)).decode(), "phase": phase, } - await self._post(f"worker/session/{self.session_id}/test/report", data) + await self._post(f"worker/session/{self.session_id}/test", data) async def upload_artifacts(self): ... @@ -297,7 +298,7 @@ async def submit_tests(self, session: pytest.Session): runs_on = session.config.stash[self.runs_on_key] await self.api_client.submit_tests( [ - TestSpec(node_id=nodeid, worker_requirements=runs_on.get(nodeid)) + TestSpec(nodeid=nodeid, worker_requirements=runs_on.get(nodeid)) for nodeid in nodeids ] ) diff --git a/software/httpdist_server/models.py b/software/httpdist_server/models.py index 1f315efe..dc8de808 100644 --- a/software/httpdist_server/models.py +++ b/software/httpdist_server/models.py @@ -2,6 +2,7 @@ from pydantic import BaseModel NodeId = str +SessionId = str class TestStatus(StrEnum): @@ -27,41 +28,55 @@ class WorkerAction(StrEnum): Stop = auto() -class GetSessionResponse(BaseModel): +class SuccessResponse(BaseModel): + message: str + + +class SessionResponse(BaseModel): """Response to a request to start a new session""" - session_id: str + session_id: SessionId -class PostSessionsTestsRequest(BaseModel): - class Test(BaseModel): - worker_requirements: set[str] - node_id: str +class NoSessionResponse(BaseModel): + """Indicates no session available for the worker""" - tests: list[Test] + pass -class GetSessionTestsResponse(BaseModel): - test_status: dict[NodeId, list[TestPhase]] +class TestsResponse(BaseModel): + statuses: dict[NodeId, list[TestPhase]] -class PostWorkerRegisterRequest(BaseModel): - worker_id: str - pet_name: str - tags: list[str] +class TestReportResponse(BaseModel): + report: str + + +class WorkerActionResponse(BaseModel): + action: WorkerAction + test_now: str | None + test_next: str | None + + +class SubmitTestsRequest(BaseModel): + class Test(BaseModel): + worker_requirements: set[str] + nodeid: NodeId + + tests: list[Test] -class GetSessionTestReportRequest(BaseModel): - node_id: str +class TestReportRequest(BaseModel): + nodeid: NodeId -class PostWorkerSessionTestReportRequest(BaseModel): - node_id: str +class SubmitTestReportRequest(BaseModel): + nodeid: NodeId phase: TestPhase report: str -class GetWorkerSessionTestsResponse(BaseModel): - action: WorkerAction - test_now: str | None - test_next: str | None +class WorkerRegisterRequest(BaseModel): + worker_id: str + pet_name: str + tags: list[str] diff --git a/software/httpdist_server/server.py b/software/httpdist_server/server.py index 98604048..7098279b 100644 --- a/software/httpdist_server/server.py +++ b/software/httpdist_server/server.py @@ -7,17 +7,20 @@ from fastapi import UploadFile from httpdist_server.models import ( - GetSessionResponse, - GetSessionTestReportRequest, - GetSessionTestsResponse, - GetWorkerSessionTestsResponse, - PostSessionsTestsRequest, - PostWorkerSessionTestReportRequest, + NoSessionResponse, + SessionResponse, SessionState, + SubmitTestReportRequest, + SubmitTestsRequest, + SuccessResponse, TestPhase, + TestReportRequest, + TestReportResponse, TestStatus, NodeId, + TestsResponse, WorkerAction, + WorkerActionResponse, ) logger = logging.getLogger(__name__) @@ -91,25 +94,25 @@ async def root(): return {"message": "Hello World!"} -@app.get("/get-session") -async def start_session(): +@app.get("/session") +async def start_session() -> SessionResponse: session_id = str(uuid.uuid4()) sessions[session_id] = Session(session_id=session_id) - return GetSessionResponse(session_id=session_id) + return SessionResponse(session_id=session_id) @app.post("/session/{session_id}/stop") -async def stop_session(session_id: str): +async def stop_session(session_id: str) -> SuccessResponse: if session_id not in sessions: raise fastapi.HTTPException(status_code=404, detail="Session not found") await sessions[session_id].stop() - return {"message": "Stop signal sent"} + return SuccessResponse(message="Stop signal sent") @app.post("/session/{session_id}/env") -async def upload_session_env(session_id: str, env: UploadFile): +async def submit_session_env(session_id: str, env: UploadFile) -> SuccessResponse: """Upload environment file for a test session""" if session_id not in sessions: raise fastapi.HTTPException(status_code=404, detail="Session not found") @@ -117,12 +120,12 @@ async def upload_session_env(session_id: str, env: UploadFile): # TODO: Store this in a proper storage backend sessions[session_id].env = env - return {"message": "Environment file uploaded successfully"} + return SuccessResponse(message="Environment file uploaded successfully") @app.post("/session/{session_id}/tests") -async def add_tests(session_id: str, request: PostSessionsTestsRequest): - """Add a test to a session""" +async def submit_tests(session_id: str, request: SubmitTestsRequest) -> SuccessResponse: + """Add collected tests to a session""" if session_id not in sessions: raise fastapi.HTTPException(status_code=404, detail="Session not found") @@ -133,73 +136,78 @@ async def add_tests(session_id: str, request: PostSessionsTestsRequest): if tag not in worker_tags: unprocessable_tags.add(tag) - sessions[session_id].tests[test.node_id] = Session.Test( - test.worker_requirements, test.node_id + sessions[session_id].tests[test.nodeid] = Session.Test( + test.worker_requirements, test.nodeid ) if unprocessable_tags: + # TODO: implement as a validator on the model raise fastapi.HTTPException( status_code=422, detail=f"Tests with unprocessable tags: {unprocessable_tags}", ) - return {"message": "Tests added successfully"} + return SuccessResponse(message="Tests added successfully") -@app.get("/session/{session_id}/finished-tests") -async def get_finished_tests(session_id: str) -> GetSessionTestsResponse: - """Get the tests for a session""" +@app.get("/session/{session_id}/tests") +async def fetch_tests(session_id: str) -> TestsResponse: + """Get test statuses for a session""" if session_id not in sessions: raise fastapi.HTTPException(status_code=404, detail="Session not found") - completed_phases: dict[str, list[TestPhase]] = { - test.nodeid: [ - phase for phase in test.reports.keys() if test.reports[phase] is not None - ] - for test in sessions[session_id].tests.values() # type: ignore - } - return GetSessionTestsResponse(test_status=completed_phases) + return TestsResponse( + statuses={ + test.nodeid: [ + phase + for phase in test.reports.keys() + if test.reports[phase] is not None + ] + for test in sessions[session_id].tests.values() + } + ) -@app.post("/worker/session/{session_id}/test/report") +@app.post("/worker/session/{session_id}/test") async def submit_test_report( - session_id: str, request: PostWorkerSessionTestReportRequest -): + session_id: str, request: SubmitTestReportRequest +) -> SuccessResponse: """Upload the result for a test""" if session_id not in sessions: raise fastapi.HTTPException(status_code=404, detail="Session not found") - if request.node_id not in sessions[session_id].tests: + if request.nodeid not in sessions[session_id].tests: raise fastapi.HTTPException(status_code=404, detail="Test not found") - test = sessions[session_id].tests[request.node_id] + test = sessions[session_id].tests[request.nodeid] test.reports[request.phase] = request.report if request.phase == TestPhase.Teardown: # TODO test.status = TestStatus.Finished - return {"message": "Test result uploaded successfully"} + return SuccessResponse(message="Test result uploaded successfully") -@app.post("/session/{session_id}/test/report/{phase}") +@app.post("/session/{session_id}/test/{phase}") async def query_test_report( - session_id: str, phase: TestPhase, request: GetSessionTestReportRequest -): - """Get the report for a test""" + session_id: str, phase: TestPhase, request: TestReportRequest +) -> TestReportResponse: + """Get the report for a test at a particular phase""" if session_id not in sessions: raise fastapi.HTTPException(status_code=404, detail="Session not found") - if request.node_id not in sessions[session_id].tests: + if request.nodeid not in sessions[session_id].tests: raise fastapi.HTTPException(status_code=404, detail="Test not found") - test = sessions[session_id].tests[request.node_id] - if test.reports[phase] is None: + test = sessions[session_id].tests[request.nodeid] + + if (report := test.reports[phase]) is None: raise fastapi.HTTPException(status_code=404, detail="Test report not found") - return test.reports[phase] + return TestReportResponse(report=report) -@app.get("/worker/{worker_id}/get-session") -async def get_session(worker_id: str) -> str | None: +@app.get(path="/worker/{worker_id}/session") +async def get_worker_session(worker_id: str) -> SessionResponse | NoSessionResponse: """Get the session for a worker""" for worker in workers: if worker.worker_id == worker_id: @@ -209,15 +217,15 @@ async def get_session(worker_id: str) -> str | None: test.worker_requirements.issubset(worker.tags) for test in session.tests.values() ): - return session.session_id + return SessionResponse(session_id=session.session_id) - return None + return NoSessionResponse() raise fastapi.HTTPException(status_code=404, detail="Worker not found") @app.get("/worker/session/{session_id}/env") -async def get_session_env(session_id: str): +async def fetch_worker_session_env(session_id: str): """Get the environment for a session""" if session_id not in sessions: raise fastapi.HTTPException(status_code=404, detail="Session not found") @@ -226,7 +234,7 @@ async def get_session_env(session_id: str): @app.get("/worker/{worker_id}/session/{session_id}/tests") -async def get_session_tests(worker_id: str, session_id: str): +async def fetch_session_tests(worker_id: str, session_id: str) -> WorkerActionResponse: """Get the tests for a session""" if session_id not in sessions: raise fastapi.HTTPException(status_code=404, detail="Session not found") @@ -234,7 +242,7 @@ async def get_session_tests(worker_id: str, session_id: str): if sessions[session_id].state == SessionState.Setup: sessions[session_id].state = SessionState.Running elif sessions[session_id].state == SessionState.Stopped: - return GetWorkerSessionTestsResponse( + return WorkerActionResponse( action=WorkerAction.Stop, test_now=None, test_next=None ) @@ -258,7 +266,7 @@ async def get_session_tests(worker_id: str, session_id: str): elif len(worker_testable) >= 2: break - return GetWorkerSessionTestsResponse( + return WorkerActionResponse( action=WorkerAction.Run if len(worker_testable) > 0 else WorkerAction.Stop, test_now=worker_testable[0] if len(worker_testable) > 0 else None, test_next=worker_testable[1] if len(worker_testable) > 1 else None, From c1591aba756c7a2580743cfe24a9681ffef48549 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 25 Feb 2025 19:42:33 -0800 Subject: [PATCH 55/71] artifacts (wip) --- software/hil/dist_plugin.py | 79 ++++++++++++++++++++++++++++-- software/httpdist_server/models.py | 9 ++++ software/httpdist_server/server.py | 54 +++++++++++++++++++- 3 files changed, 138 insertions(+), 4 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index 0d68f813..3e704e31 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -9,6 +9,8 @@ import httpx import pytest import logging +import tempfile +import shutil logger = logging.getLogger(__name__) @@ -16,6 +18,7 @@ httpx_logger.setLevel(logging.WARNING) PLUGIN_NAME = "httpdist" +ARTIFACTS_DIR = Path("./artifacts") @dataclass @@ -130,6 +133,35 @@ async def fetch_report(self, nodeid: NodeId, phase: TestPhase) -> pytest.TestRep report = response["report"] return cloudpickle.loads(base64.b64decode(report)) + async def list_artifacts(self) -> dict: + """List all artifacts available on the server for this session""" + if self.session_id is None: + raise SessionNotStartedError("Must have an active session") + + response = await self._get(f"session/{self.session_id}/artifacts") + return response["artifact_ids"] + + async def download_artifact( + self, artifact_id: str, artifacts_dir: Path = ARTIFACTS_DIR + ): + """Download a specific artifact and save it to the given path""" + if self.session_id is None: + raise SessionNotStartedError("Must have an active session") + + artifacts_dir.parent.mkdir(parents=True, exist_ok=True) + + response = await self._get(f"session/{self.session_id}/artifacts/{artifact_id}") + response.raise_for_status() + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) / artifact_id + with temp_path.open("wb") as f: + f.write(response.content) + shutil.unpack_archive(temp_path, artifacts_dir) + temp_path.unlink() + + return artifacts_dir + class WorkerApi(ApiBase): def __init__(self, config: pytest.Config): @@ -160,7 +192,32 @@ async def report_result( } await self._post(f"worker/session/{self.session_id}/test", data) - async def upload_artifacts(self): ... + async def upload_artifacts(self, worker_id: str): + """Upload all artifact files""" + if not ARTIFACTS_DIR.exists(): + return + + with tempfile.TemporaryDirectory() as temp_dir: + zip_path = Path(temp_dir) / f"{worker_id}.zip" + try: + shutil.make_archive( + str(zip_path.with_suffix("")), + "zip", + root_dir=ARTIFACTS_DIR, + base_dir=".", + ) + + with open(zip_path, "rb") as zip_file: + await self._post( + f"worker/session/{self.session_id}/artifacts", + { + "worker_id": worker_id, + "content": base64.b64encode(zip_file.read()).decode(), + }, + ) + + finally: + zip_path.unlink() class Worker: @@ -238,7 +295,8 @@ def pytest_runtest_logreport(self, report: pytest.TestReport) -> None: @pytest.hookimpl def pytest_sessionfinish(self, session: pytest.Session, exitstatus: int): - asyncio.run(self.api_client.upload_artifacts()) + loop = asyncio.get_event_loop() + loop.run_until_complete(self.api_client.upload_artifacts(self.worker_id)) class TestResults: @@ -320,7 +378,22 @@ async def fetch_results(self) -> list[pytest.TestReport]: return new_reports - def download_artifacts(self): ... + def download_artifacts(self): + """Download artifact files produced by workers""" + + loop = asyncio.get_event_loop() + artifact_ids = loop.run_until_complete(self.api_client.list_artifacts()) + if not artifact_ids: + return + + ARTIFACTS_DIR.mkdir(exist_ok=True) + + tasks = [ + self.api_client.download_artifact(artifact_id) + for artifact_id in artifact_ids + ] + + loop.run_until_complete(asyncio.gather(*tasks)) @pytest.hookimpl def pytest_sessionfinish(self, session: pytest.Session, exitstatus: int): diff --git a/software/httpdist_server/models.py b/software/httpdist_server/models.py index dc8de808..48fe822f 100644 --- a/software/httpdist_server/models.py +++ b/software/httpdist_server/models.py @@ -58,6 +58,10 @@ class WorkerActionResponse(BaseModel): test_next: str | None +class ArtifactListResponse(BaseModel): + artifact_ids: list[str] + + class SubmitTestsRequest(BaseModel): class Test(BaseModel): worker_requirements: set[str] @@ -80,3 +84,8 @@ class WorkerRegisterRequest(BaseModel): worker_id: str pet_name: str tags: list[str] + + +class ArtifactUploadRequest(BaseModel): + worker_id: str + content: str diff --git a/software/httpdist_server/server.py b/software/httpdist_server/server.py index 7098279b..d6d3aff2 100644 --- a/software/httpdist_server/server.py +++ b/software/httpdist_server/server.py @@ -1,12 +1,16 @@ import logging -import uuid +import base64 from dataclasses import dataclass, field +import uuid import fastapi import uvicorn from fastapi import UploadFile +from fastapi.responses import StreamingResponse from httpdist_server.models import ( + ArtifactListResponse, + ArtifactUploadRequest, NoSessionResponse, SessionResponse, SessionState, @@ -57,6 +61,7 @@ class Test: state: SessionState = SessionState.Setup tests: dict[str, Test] = field(default_factory=dict) env: UploadFile | None = None + artifacts: dict[str, bytes] = field(default_factory=dict) async def stop(self): self.state = SessionState.Stopped @@ -273,5 +278,52 @@ async def fetch_session_tests(worker_id: str, session_id: str) -> WorkerActionRe ) +@app.post("/worker/session/{session_id}/artifacts") +async def upload_artifact( + session_id: str, request: ArtifactUploadRequest +) -> SuccessResponse: + """Upload an artifact file from a worker""" + if session_id not in sessions: + raise fastapi.HTTPException(status_code=404, detail="Session not found") + + try: + file_content = base64.b64decode(request.content) + except Exception as e: + raise fastapi.HTTPException( + status_code=400, detail=f"Invalid base64 content: {str(e)}" + ) + + sessions[session_id].artifacts[request.worker_id] = file_content + + return SuccessResponse(message="Artifact uploaded successfully") + + +@app.get("/session/{session_id}/artifacts") +async def list_artifacts(session_id: str) -> ArtifactListResponse: + """List all artifacts for a session""" + if session_id not in sessions: + raise fastapi.HTTPException(status_code=404, detail="Session not found") + + return ArtifactListResponse( + artifact_ids=list(sessions[session_id].artifacts.keys()) + ) + + +@app.get("/session/{session_id}/artifacts/{artifact_id}") +async def download_artifact(session_id: str, artifact_id: str) -> StreamingResponse: + """Download an artifact file""" + if session_id not in sessions: + raise fastapi.HTTPException(status_code=404, detail="Session not found") + + if artifact_id not in sessions[session_id].artifacts: + raise fastapi.HTTPException(status_code=404, detail="Artifact not found") + + artifact_content = sessions[session_id].artifacts[artifact_id] + + return StreamingResponse( + content=artifact_content.decode(), media_type="application/octet-stream" + ) + + if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000) From 7b8d30403d9922933779a60ddce41f4d0742e744 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Tue, 25 Feb 2025 20:01:43 -0800 Subject: [PATCH 56/71] artifacts --- software/hil/dist_plugin.py | 40 +++++++++++++++++------------- software/httpdist_server/server.py | 9 +++---- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index 3e704e31..8fe220f4 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -84,17 +84,24 @@ class ApiBase: def __init__(self, config: pytest.Config): self.config = config - self._client = httpx.AsyncClient() async def _post(self, path: str, data: dict): - response = await self._client.post(f"{self.API_URL}/{path}", json=data) - response.raise_for_status() - return response.json() + async with httpx.AsyncClient() as client: + response = await client.post(f"{self.API_URL}/{path}", json=data) + response.raise_for_status() + return response.json() async def _get(self, path: str, params: dict | None = None): - response = await self._client.get(f"{self.API_URL}/{path}", params=params) - response.raise_for_status() - return response.json() + async with httpx.AsyncClient() as client: + response = await client.get(f"{self.API_URL}/{path}", params=params) + response.raise_for_status() + return response.json() + + async def _get_raw(self, path: str, params: dict | None = None): + async with httpx.AsyncClient() as client: + response = await client.get(f"{self.API_URL}/{path}", params=params) + response.raise_for_status() + return response class ClientApi(ApiBase): @@ -150,11 +157,12 @@ async def download_artifact( artifacts_dir.parent.mkdir(parents=True, exist_ok=True) - response = await self._get(f"session/{self.session_id}/artifacts/{artifact_id}") - response.raise_for_status() + response = await self._get_raw( + f"session/{self.session_id}/artifacts/{artifact_id}" + ) with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) / artifact_id + temp_path = (Path(temp_dir) / artifact_id).with_suffix(".zip") with temp_path.open("wb") as f: f.write(response.content) shutil.unpack_archive(temp_path, artifacts_dir) @@ -295,8 +303,7 @@ def pytest_runtest_logreport(self, report: pytest.TestReport) -> None: @pytest.hookimpl def pytest_sessionfinish(self, session: pytest.Session, exitstatus: int): - loop = asyncio.get_event_loop() - loop.run_until_complete(self.api_client.upload_artifacts(self.worker_id)) + asyncio.run(self.api_client.upload_artifacts(self.worker_id)) class TestResults: @@ -378,11 +385,10 @@ async def fetch_results(self) -> list[pytest.TestReport]: return new_reports - def download_artifacts(self): + async def download_artifacts(self): """Download artifact files produced by workers""" - loop = asyncio.get_event_loop() - artifact_ids = loop.run_until_complete(self.api_client.list_artifacts()) + artifact_ids = await self.api_client.list_artifacts() if not artifact_ids: return @@ -393,11 +399,11 @@ def download_artifacts(self): for artifact_id in artifact_ids ] - loop.run_until_complete(asyncio.gather(*tasks)) + await asyncio.gather(*tasks) @pytest.hookimpl def pytest_sessionfinish(self, session: pytest.Session, exitstatus: int): - self.download_artifacts() + asyncio.run(self.download_artifacts()) @pytest.hookimpl(tryfirst=True) def pytest_collection(self, session: pytest.Session): diff --git a/software/httpdist_server/server.py b/software/httpdist_server/server.py index d6d3aff2..13c7a759 100644 --- a/software/httpdist_server/server.py +++ b/software/httpdist_server/server.py @@ -5,8 +5,7 @@ import fastapi import uvicorn -from fastapi import UploadFile -from fastapi.responses import StreamingResponse +from fastapi import Response, UploadFile from httpdist_server.models import ( ArtifactListResponse, @@ -310,7 +309,7 @@ async def list_artifacts(session_id: str) -> ArtifactListResponse: @app.get("/session/{session_id}/artifacts/{artifact_id}") -async def download_artifact(session_id: str, artifact_id: str) -> StreamingResponse: +async def download_artifact(session_id: str, artifact_id: str) -> Response: """Download an artifact file""" if session_id not in sessions: raise fastapi.HTTPException(status_code=404, detail="Session not found") @@ -320,9 +319,7 @@ async def download_artifact(session_id: str, artifact_id: str) -> StreamingRespo artifact_content = sessions[session_id].artifacts[artifact_id] - return StreamingResponse( - content=artifact_content.decode(), media_type="application/octet-stream" - ) + return Response(content=artifact_content, media_type="application/octet-stream") if __name__ == "__main__": From dbf5ed2f7a27717beed4ec3dcdadb668b771529e Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Tue, 25 Feb 2025 22:28:50 -0800 Subject: [PATCH 57/71] Add supabase dependency --- pyproject.toml | 1 + uv.lock | 370 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 355 insertions(+), 16 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 95e6c252..00106bde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ dependencies = [ "uvicorn>=0.34.0", "httpx>=0.28.1", "fastapi[standard]>=0.115.8", + "supabase>=2.13.0", ] [project.entry-points."pytest11"] diff --git a/uv.lock b/uv.lock index 4bc6abfd..f7bd1c91 100644 --- a/uv.lock +++ b/uv.lock @@ -5,6 +5,60 @@ resolution-markers = [ "sys_platform != 'darwin'", ] +[[package]] +name = "aiohappyeyeballs" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/07/508f9ebba367fc3370162e53a3cfd12f5652ad79f0e0bfdf9f9847c6f159/aiohappyeyeballs-2.4.6.tar.gz", hash = "sha256:9b05052f9042985d32ecbe4b59a77ae19c006a78f1344d7fdad69d28ded3d0b0", size = 21726 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/4c/03fb05f56551828ec67ceb3665e5dc51638042d204983a03b0a1541475b6/aiohappyeyeballs-2.4.6-py3-none-any.whl", hash = "sha256:147ec992cf873d74f5062644332c539fcd42956dc69453fe5204195e560517e1", size = 14543 }, +] + +[[package]] +name = "aiohttp" +version = "3.11.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/3f/c4a667d184c69667b8f16e0704127efc5f1e60577df429382b4d95fd381e/aiohttp-3.11.13.tar.gz", hash = "sha256:8ce789231404ca8fff7f693cdce398abf6d90fd5dae2b1847477196c243b1fbb", size = 7674284 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/dc/7d58d33cec693f1ddf407d4ab975445f5cb507af95600f137b81683a18d8/aiohttp-3.11.13-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9862d077b9ffa015dbe3ce6c081bdf35135948cb89116e26667dd183550833d1", size = 698372 }, + { url = "https://files.pythonhosted.org/packages/84/e7/5d88514c9e24fbc8dd6117350a8ec4a9314f4adae6e89fe32e3e639b0c37/aiohttp-3.11.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbfef0666ae9e07abfa2c54c212ac18a1f63e13e0760a769f70b5717742f3ece", size = 461057 }, + { url = "https://files.pythonhosted.org/packages/96/1a/8143c48a929fa00c6324f85660cb0f47a55ed9385f0c1b72d4b8043acf8e/aiohttp-3.11.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a1f7d857c4fcf7cabb1178058182c789b30d85de379e04f64c15b7e88d66fb", size = 453340 }, + { url = "https://files.pythonhosted.org/packages/2f/1c/b8010e4d65c5860d62681088e5376f3c0a940c5e3ca8989cae36ce8c3ea8/aiohttp-3.11.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba40b7ae0f81c7029583a338853f6607b6d83a341a3dcde8bed1ea58a3af1df9", size = 1665561 }, + { url = "https://files.pythonhosted.org/packages/19/ed/a68c3ab2f92fdc17dfc2096117d1cfaa7f7bdded2a57bacbf767b104165b/aiohttp-3.11.13-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5b95787335c483cd5f29577f42bbe027a412c5431f2f80a749c80d040f7ca9f", size = 1718335 }, + { url = "https://files.pythonhosted.org/packages/27/4f/3a0b6160ce663b8ebdb65d1eedff60900cd7108838c914d25952fe2b909f/aiohttp-3.11.13-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7d474c5c1f0b9405c1565fafdc4429fa7d986ccbec7ce55bc6a330f36409cad", size = 1775522 }, + { url = "https://files.pythonhosted.org/packages/0b/58/9da09291e19696c452e7224c1ce8c6d23a291fe8cd5c6b247b51bcda07db/aiohttp-3.11.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e83fb1991e9d8982b3b36aea1e7ad27ea0ce18c14d054c7a404d68b0319eebb", size = 1677566 }, + { url = "https://files.pythonhosted.org/packages/3d/18/6184f2bf8bbe397acbbbaa449937d61c20a6b85765f48e5eddc6d84957fe/aiohttp-3.11.13-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4586a68730bd2f2b04a83e83f79d271d8ed13763f64b75920f18a3a677b9a7f0", size = 1603590 }, + { url = "https://files.pythonhosted.org/packages/04/94/91e0d1ca0793012ccd927e835540aa38cca98bdce2389256ab813ebd64a3/aiohttp-3.11.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fe4eb0e7f50cdb99b26250d9328faef30b1175a5dbcfd6d0578d18456bac567", size = 1618688 }, + { url = "https://files.pythonhosted.org/packages/71/85/d13c3ea2e48a10b43668305d4903838834c3d4112e5229177fbcc23a56cd/aiohttp-3.11.13-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2a8a6bc19818ac3e5596310ace5aa50d918e1ebdcc204dc96e2f4d505d51740c", size = 1658053 }, + { url = "https://files.pythonhosted.org/packages/12/6a/3242a35100de23c1e8d9e05e8605e10f34268dee91b00d9d1e278c58eb80/aiohttp-3.11.13-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f27eec42f6c3c1df09cfc1f6786308f8b525b8efaaf6d6bd76c1f52c6511f6a", size = 1616917 }, + { url = "https://files.pythonhosted.org/packages/f5/b3/3f99b6f0a9a79590a7ba5655dbde8408c685aa462247378c977603464d0a/aiohttp-3.11.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2a4a13dfbb23977a51853b419141cd0a9b9573ab8d3a1455c6e63561387b52ff", size = 1685872 }, + { url = "https://files.pythonhosted.org/packages/8a/2e/99672181751f280a85e24fcb9a2c2469e8b1a0de1746b7b5c45d1eb9a999/aiohttp-3.11.13-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:02876bf2f69b062584965507b07bc06903c2dc93c57a554b64e012d636952654", size = 1715719 }, + { url = "https://files.pythonhosted.org/packages/7a/cd/68030356eb9a7d57b3e2823c8a852709d437abb0fbff41a61ebc351b7625/aiohttp-3.11.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b992778d95b60a21c4d8d4a5f15aaab2bd3c3e16466a72d7f9bfd86e8cea0d4b", size = 1673166 }, + { url = "https://files.pythonhosted.org/packages/03/61/425397a9a2839c609d09fdb53d940472f316a2dbeaa77a35b2628dae6284/aiohttp-3.11.13-cp313-cp313-win32.whl", hash = "sha256:507ab05d90586dacb4f26a001c3abf912eb719d05635cbfad930bdbeb469b36c", size = 410615 }, + { url = "https://files.pythonhosted.org/packages/9c/54/ebb815bc0fe057d8e7a11c086c479e972e827082f39aeebc6019dd4f0862/aiohttp-3.11.13-cp313-cp313-win_amd64.whl", hash = "sha256:5ceb81a4db2decdfa087381b5fc5847aa448244f973e5da232610304e199e7b2", size = 436452 }, +] + +[[package]] +name = "aiosignal" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597 }, +] + [[package]] name = "altair" version = "5.5.0" @@ -100,6 +154,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "deprecation" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178 }, +] + [[package]] name = "distlib" version = "0.3.9" @@ -183,6 +249,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164 }, ] +[[package]] +name = "frozenlist" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/ed/0f4cec13a93c02c47ec32d81d11c0c1efbadf4a471e3f3ce7cad366cbbd3/frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", size = 39930 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/3b/915f0bca8a7ea04483622e84a9bd90033bab54bdf485479556c74fd5eaf5/frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953", size = 91538 }, + { url = "https://files.pythonhosted.org/packages/c7/d1/a7c98aad7e44afe5306a2b068434a5830f1470675f0e715abb86eb15f15b/frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0", size = 52849 }, + { url = "https://files.pythonhosted.org/packages/3a/c8/76f23bf9ab15d5f760eb48701909645f686f9c64fbb8982674c241fbef14/frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2", size = 50583 }, + { url = "https://files.pythonhosted.org/packages/1f/22/462a3dd093d11df623179d7754a3b3269de3b42de2808cddef50ee0f4f48/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f", size = 265636 }, + { url = "https://files.pythonhosted.org/packages/80/cf/e075e407fc2ae7328155a1cd7e22f932773c8073c1fc78016607d19cc3e5/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608", size = 270214 }, + { url = "https://files.pythonhosted.org/packages/a1/58/0642d061d5de779f39c50cbb00df49682832923f3d2ebfb0fedf02d05f7f/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b", size = 273905 }, + { url = "https://files.pythonhosted.org/packages/ab/66/3fe0f5f8f2add5b4ab7aa4e199f767fd3b55da26e3ca4ce2cc36698e50c4/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840", size = 250542 }, + { url = "https://files.pythonhosted.org/packages/f6/b8/260791bde9198c87a465224e0e2bb62c4e716f5d198fc3a1dacc4895dbd1/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439", size = 267026 }, + { url = "https://files.pythonhosted.org/packages/2e/a4/3d24f88c527f08f8d44ade24eaee83b2627793fa62fa07cbb7ff7a2f7d42/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de", size = 257690 }, + { url = "https://files.pythonhosted.org/packages/de/9a/d311d660420b2beeff3459b6626f2ab4fb236d07afbdac034a4371fe696e/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641", size = 253893 }, + { url = "https://files.pythonhosted.org/packages/c6/23/e491aadc25b56eabd0f18c53bb19f3cdc6de30b2129ee0bc39cd387cd560/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e", size = 267006 }, + { url = "https://files.pythonhosted.org/packages/08/c4/ab918ce636a35fb974d13d666dcbe03969592aeca6c3ab3835acff01f79c/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9", size = 276157 }, + { url = "https://files.pythonhosted.org/packages/c0/29/3b7a0bbbbe5a34833ba26f686aabfe982924adbdcafdc294a7a129c31688/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03", size = 264642 }, + { url = "https://files.pythonhosted.org/packages/ab/42/0595b3dbffc2e82d7fe658c12d5a5bafcd7516c6bf2d1d1feb5387caa9c1/frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c", size = 44914 }, + { url = "https://files.pythonhosted.org/packages/17/c4/b7db1206a3fea44bf3b838ca61deb6f74424a8a5db1dd53ecb21da669be6/frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28", size = 51167 }, + { url = "https://files.pythonhosted.org/packages/c6/c8/a5be5b7550c10858fcf9b0ea054baccab474da77d37f1e828ce043a3a5d4/frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", size = 11901 }, +] + +[[package]] +name = "gotrue" +version = "2.11.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", extra = ["http2"] }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/9c/62c3241731b59c1c403377abef17b5e3782f6385b0317f6d7083271db501/gotrue-2.11.4.tar.gz", hash = "sha256:a9ced242b16c6d6bedc43bca21bbefea1ba5fb35fcdaad7d529342099d3b1767", size = 35353 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/3a/1a7cac16438f4e5319a0c879416d5e5032c98c3db2874e6e5300b3b475e6/gotrue-2.11.4-py3-none-any.whl", hash = "sha256:712e5018acc00d93cfc6d7bfddc3114eb3c420ab03b945757a8ba38c5fc3caa8", size = 41106 }, +] + [[package]] name = "h11" version = "0.14.0" @@ -192,6 +295,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, ] +[[package]] +name = "h2" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/38/d7f80fd13e6582fb8e0df8c9a653dcc02b03ca34f4d72f34869298c5baf8/h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f", size = 2150682 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/9e/984486f2d0a0bd2b024bf4bc1c62688fcafa9e61991f041fb0e2def4a982/h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0", size = 60957 }, +] + [[package]] name = "hil" version = "0.1.0" @@ -213,6 +329,7 @@ dependencies = [ { name = "python-multipart" }, { name = "rich" }, { name = "smbus2" }, + { name = "supabase" }, { name = "typer" }, { name = "uvicorn" }, ] @@ -242,6 +359,7 @@ requires-dist = [ { name = "python-multipart", specifier = ">=0.0.20" }, { name = "rich", specifier = ">=13.9.4" }, { name = "smbus2", specifier = ">=0.5.0" }, + { name = "supabase", specifier = ">=2.13.0" }, { name = "typer", specifier = ">=0.15.1" }, { name = "uvicorn", specifier = ">=0.34.0" }, ] @@ -253,6 +371,15 @@ dev = [ { name = "ruff", specifier = ">=0.9.6" }, ] +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357 }, +] + [[package]] name = "httpcore" version = "1.0.7" @@ -296,6 +423,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, ] +[package.optional-dependencies] +http2 = [ + { name = "h2" }, +] + +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007 }, +] + [[package]] name = "identify" version = "2.6.8" @@ -411,6 +552,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, ] +[[package]] +name = "multidict" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 }, + { url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 }, + { url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 }, + { url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 }, + { url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 }, + { url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 }, + { url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 }, + { url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 }, + { url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 }, + { url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 }, + { url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 }, + { url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 }, + { url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 }, + { url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 }, + { url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 }, + { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, +] + [[package]] name = "narwhals" version = "1.28.0" @@ -507,6 +672,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cf/77/f03719e1d2699e175ff43ed376d8049a145ce0d080934dc50f435d2975b2/polars-1.23.0-cp39-abi3-win_arm64.whl", hash = "sha256:701df6381665c56da18704305119ce73983b1bead3378fbc8fac7ffd24817e93", size = 30771854 }, ] +[[package]] +name = "postgrest" +version = "0.19.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecation" }, + { name = "httpx", extra = ["http2"] }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/80/b0306469da7ad89db165ce4c76de2f12eccc7fadb900cab9cbaff760a587/postgrest-0.19.3.tar.gz", hash = "sha256:28a70f03bf3a975aa865a10487b1ce09b7195f56453f7c318a70d3117a3d323c", size = 15095 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/82/f1825a85745912cdd8956aad8ebc4b797d2f891c380c2b8825b35914dbd1/postgrest-0.19.3-py3-none-any.whl", hash = "sha256:03a7e638962454d10bb712c35e63a8a4bc452917917a4e9eb7427bd5b3c6c485", size = 22198 }, +] + [[package]] name = "pre-commit" version = "4.1.0" @@ -523,6 +702,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/b3/df14c580d82b9627d173ceea305ba898dca135feb360b6d84019d0803d3b/pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b", size = 220560 }, ] +[[package]] +name = "propcache" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/76/f941e63d55c0293ff7829dd21e7cf1147e90a526756869a9070f287a68c9/propcache-0.3.0.tar.gz", hash = "sha256:a8fd93de4e1d278046345f49e2238cdb298589325849b2645d4a94c53faeffc5", size = 42722 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/0f/a79dd23a0efd6ee01ab0dc9750d8479b343bfd0c73560d59d271eb6a99d4/propcache-0.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a2b9bf8c79b660d0ca1ad95e587818c30ccdb11f787657458d6f26a1ea18c568", size = 77287 }, + { url = "https://files.pythonhosted.org/packages/b8/51/76675703c90de38ac75adb8deceb3f3ad99b67ff02a0fa5d067757971ab8/propcache-0.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0c1a133d42c6fc1f5fbcf5c91331657a1ff822e87989bf4a6e2e39b818d0ee9", size = 44923 }, + { url = "https://files.pythonhosted.org/packages/01/9b/fd5ddbee66cf7686e73c516227c2fd9bf471dbfed0f48329d095ea1228d3/propcache-0.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bb2f144c6d98bb5cbc94adeb0447cfd4c0f991341baa68eee3f3b0c9c0e83767", size = 44325 }, + { url = "https://files.pythonhosted.org/packages/13/1c/6961f11eb215a683b34b903b82bde486c606516c1466bf1fa67f26906d51/propcache-0.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1323cd04d6e92150bcc79d0174ce347ed4b349d748b9358fd2e497b121e03c8", size = 225116 }, + { url = "https://files.pythonhosted.org/packages/ef/ea/f8410c40abcb2e40dffe9adeed017898c930974650a63e5c79b886aa9f73/propcache-0.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b812b3cb6caacd072276ac0492d249f210006c57726b6484a1e1805b3cfeea0", size = 229905 }, + { url = "https://files.pythonhosted.org/packages/ef/5a/a9bf90894001468bf8e6ea293bb00626cc9ef10f8eb7996e9ec29345c7ed/propcache-0.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:742840d1d0438eb7ea4280f3347598f507a199a35a08294afdcc560c3739989d", size = 233221 }, + { url = "https://files.pythonhosted.org/packages/dd/ce/fffdddd9725b690b01d345c1156b4c2cc6dca09ab5c23a6d07b8f37d6e2f/propcache-0.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c6e7e4f9167fddc438cd653d826f2222222564daed4116a02a184b464d3ef05", size = 227627 }, + { url = "https://files.pythonhosted.org/packages/58/ae/45c89a5994a334735a3032b48e8e4a98c05d9536ddee0719913dc27da548/propcache-0.3.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a94ffc66738da99232ddffcf7910e0f69e2bbe3a0802e54426dbf0714e1c2ffe", size = 214217 }, + { url = "https://files.pythonhosted.org/packages/01/84/bc60188c3290ff8f5f4a92b9ca2d93a62e449c8daf6fd11ad517ad136926/propcache-0.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c6ec957025bf32b15cbc6b67afe233c65b30005e4c55fe5768e4bb518d712f1", size = 212921 }, + { url = "https://files.pythonhosted.org/packages/14/b3/39d60224048feef7a96edabb8217dc3f75415457e5ebbef6814f8b2a27b5/propcache-0.3.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:549722908de62aa0b47a78b90531c022fa6e139f9166be634f667ff45632cc92", size = 208200 }, + { url = "https://files.pythonhosted.org/packages/9d/b3/0a6720b86791251273fff8a01bc8e628bc70903513bd456f86cde1e1ef84/propcache-0.3.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5d62c4f6706bff5d8a52fd51fec6069bef69e7202ed481486c0bc3874912c787", size = 208400 }, + { url = "https://files.pythonhosted.org/packages/e9/4f/bb470f3e687790547e2e78105fb411f54e0cdde0d74106ccadd2521c6572/propcache-0.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:24c04f8fbf60094c531667b8207acbae54146661657a1b1be6d3ca7773b7a545", size = 218116 }, + { url = "https://files.pythonhosted.org/packages/34/71/277f7f9add469698ac9724c199bfe06f85b199542121a71f65a80423d62a/propcache-0.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7c5f5290799a3f6539cc5e6f474c3e5c5fbeba74a5e1e5be75587746a940d51e", size = 222911 }, + { url = "https://files.pythonhosted.org/packages/92/e3/a7b9782aef5a2fc765b1d97da9ec7aed2f25a4e985703608e73232205e3f/propcache-0.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4fa0e7c9c3cf7c276d4f6ab9af8adddc127d04e0fcabede315904d2ff76db626", size = 216563 }, + { url = "https://files.pythonhosted.org/packages/ab/76/0583ca2c551aa08ffcff87b2c6849c8f01c1f6fb815a5226f0c5c202173e/propcache-0.3.0-cp313-cp313-win32.whl", hash = "sha256:ee0bd3a7b2e184e88d25c9baa6a9dc609ba25b76daae942edfb14499ac7ec374", size = 39763 }, + { url = "https://files.pythonhosted.org/packages/80/ec/c6a84f9a36f608379b95f0e786c111d5465926f8c62f12be8cdadb02b15c/propcache-0.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1c8f7d896a16da9455f882870a507567d4f58c53504dc2d4b1e1d386dfe4588a", size = 43650 }, + { url = "https://files.pythonhosted.org/packages/ee/95/7d32e3560f5bf83fc2f2a4c1b0c181d327d53d5f85ebd045ab89d4d97763/propcache-0.3.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e560fd75aaf3e5693b91bcaddd8b314f4d57e99aef8a6c6dc692f935cc1e6bbf", size = 82140 }, + { url = "https://files.pythonhosted.org/packages/86/89/752388f12e6027a5e63f5d075f15291ded48e2d8311314fff039da5a9b11/propcache-0.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65a37714b8ad9aba5780325228598a5b16c47ba0f8aeb3dc0514701e4413d7c0", size = 47296 }, + { url = "https://files.pythonhosted.org/packages/1b/4c/b55c98d586c69180d3048984a57a5ea238bdeeccf82dbfcd598e935e10bb/propcache-0.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:07700939b2cbd67bfb3b76a12e1412405d71019df00ca5697ce75e5ef789d829", size = 46724 }, + { url = "https://files.pythonhosted.org/packages/0f/b6/67451a437aed90c4e951e320b5b3d7eb584ade1d5592f6e5e8f678030989/propcache-0.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c0fdbdf6983526e269e5a8d53b7ae3622dd6998468821d660d0daf72779aefa", size = 291499 }, + { url = "https://files.pythonhosted.org/packages/ee/ff/e4179facd21515b24737e1e26e02615dfb5ed29416eed4cf5bc6ac5ce5fb/propcache-0.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:794c3dd744fad478b6232289c866c25406ecdfc47e294618bdf1697e69bd64a6", size = 293911 }, + { url = "https://files.pythonhosted.org/packages/76/8d/94a8585992a064a23bd54f56c5e58c3b8bf0c0a06ae10e56f2353ae16c3d/propcache-0.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4544699674faf66fb6b4473a1518ae4999c1b614f0b8297b1cef96bac25381db", size = 293301 }, + { url = "https://files.pythonhosted.org/packages/b0/b8/2c860c92b4134f68c7716c6f30a0d723973f881c32a6d7a24c4ddca05fdf/propcache-0.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fddb8870bdb83456a489ab67c6b3040a8d5a55069aa6f72f9d872235fbc52f54", size = 281947 }, + { url = "https://files.pythonhosted.org/packages/cd/72/b564be7411b525d11757b713c757c21cd4dc13b6569c3b2b8f6d3c96fd5e/propcache-0.3.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f857034dc68d5ceb30fb60afb6ff2103087aea10a01b613985610e007053a121", size = 268072 }, + { url = "https://files.pythonhosted.org/packages/37/68/d94649e399e8d7fc051e5a4f2334efc567993525af083db145a70690a121/propcache-0.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:02df07041e0820cacc8f739510078f2aadcfd3fc57eaeeb16d5ded85c872c89e", size = 275190 }, + { url = "https://files.pythonhosted.org/packages/d8/3c/446e125f5bbbc1922964dd67cb541c01cdb678d811297b79a4ff6accc843/propcache-0.3.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f47d52fd9b2ac418c4890aad2f6d21a6b96183c98021f0a48497a904199f006e", size = 254145 }, + { url = "https://files.pythonhosted.org/packages/f4/80/fd3f741483dc8e59f7ba7e05eaa0f4e11677d7db2077522b92ff80117a2a/propcache-0.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9ff4e9ecb6e4b363430edf2c6e50173a63e0820e549918adef70515f87ced19a", size = 257163 }, + { url = "https://files.pythonhosted.org/packages/dc/cf/6292b5ce6ed0017e6a89024a827292122cc41b6259b30ada0c6732288513/propcache-0.3.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ecc2920630283e0783c22e2ac94427f8cca29a04cfdf331467d4f661f4072dac", size = 280249 }, + { url = "https://files.pythonhosted.org/packages/e8/f0/fd9b8247b449fe02a4f96538b979997e229af516d7462b006392badc59a1/propcache-0.3.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:c441c841e82c5ba7a85ad25986014be8d7849c3cfbdb6004541873505929a74e", size = 288741 }, + { url = "https://files.pythonhosted.org/packages/64/71/cf831fdc2617f86cfd7f414cfc487d018e722dac8acc098366ce9bba0941/propcache-0.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c929916cbdb540d3407c66f19f73387f43e7c12fa318a66f64ac99da601bcdf", size = 277061 }, + { url = "https://files.pythonhosted.org/packages/42/78/9432542a35d944abeca9e02927a0de38cd7a298466d8ffa171536e2381c3/propcache-0.3.0-cp313-cp313t-win32.whl", hash = "sha256:0c3e893c4464ebd751b44ae76c12c5f5c1e4f6cbd6fbf67e3783cd93ad221863", size = 42252 }, + { url = "https://files.pythonhosted.org/packages/6f/45/960365f4f8978f48ebb56b1127adf33a49f2e69ecd46ac1f46d6cf78a79d/propcache-0.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:75e872573220d1ee2305b35c9813626e620768248425f58798413e9c39741f46", size = 46425 }, + { url = "https://files.pythonhosted.org/packages/b5/35/6c4c6fc8774a9e3629cd750dc24a7a4fb090a25ccd5c3246d127b70f9e22/propcache-0.3.0-py3-none-any.whl", hash = "sha256:67dda3c7325691c2081510e92c561f465ba61b975f481735aefdfc845d2cd043", size = 12101 }, +] + [[package]] name = "py-cpuinfo" version = "9.0.0" @@ -676,6 +896,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/27/14af9ef8321f5edc7527e47def2a21d8118c6f329a9342cc61387a0c0599/pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e", size = 14148 }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + [[package]] name = "python-dotenv" version = "1.0.1" @@ -711,6 +943,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, ] +[[package]] +name = "realtime" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "python-dateutil" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/f0/ccbf80c405302880562d677a42a90780d13f13a4bca54f67615478da5253/realtime-2.4.0.tar.gz", hash = "sha256:4ffc61a9c0f8dbda7e6a48496254a018d5b2d90569f56d1d89c9618f56616c3b", size = 18787 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/75/25ff8093e1d8dc872b2f4e47ff9a8f1d1fd1b1e893a7708fc00da2c8200a/realtime-2.4.0-py3-none-any.whl", hash = "sha256:0015219bb398edfdd5e993bc77a42424ed6d6890b7234a0114fe0de4d21e4f8b", size = 22014 }, +] + [[package]] name = "referencing" version = "0.36.2" @@ -819,6 +1066,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + [[package]] name = "smbus2" version = "0.5.0" @@ -849,6 +1105,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/61/f2b52e107b1fc8944b33ef56bf6ac4ebbe16d91b94d2b87ce013bf63fb84/starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d", size = 71507 }, ] +[[package]] +name = "storage3" +version = "0.11.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", extra = ["http2"] }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/25/83eb4e4612dc07a3bb3cab96253c9c83752d4816f2cf38aa832dfb8d8813/storage3-0.11.3.tar.gz", hash = "sha256:883637132aad36d9d92b7c497a8a56dff7c51f15faf2ff7acbccefbbd5e97347", size = 9930 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/8d/ff89f85c4b48285ac7cddf0fafe5e55bb3742d374672b2fbd2627c213fa6/storage3-0.11.3-py3-none-any.whl", hash = "sha256:090c42152217d5d39bd94af3ddeb60c8982f3a283dcd90b53d058f2db33e6007", size = 17831 }, +] + +[[package]] +name = "strenum" +version = "0.4.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/ad/430fb60d90e1d112a62ff57bdd1f286ec73a2a0331272febfddd21f330e1/StrEnum-0.4.15.tar.gz", hash = "sha256:878fb5ab705442070e4dd1929bb5e2249511c0bcf2b0eeacf3bcd80875c82eff", size = 23384 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/69/297302c5f5f59c862faa31e6cb9a4cd74721cd1e052b38e464c5b402df8b/StrEnum-0.4.15-py3-none-any.whl", hash = "sha256:a30cda4af7cc6b5bf52c8055bc4bf4b2b6b14a93b574626da33df53cf7740659", size = 8851 }, +] + +[[package]] +name = "supabase" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gotrue" }, + { name = "httpx" }, + { name = "postgrest" }, + { name = "realtime" }, + { name = "storage3" }, + { name = "supafunc" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/0e/3d2f01d465b4636deb78f102e6feff47568aae5873946184afb75ff5abe3/supabase-2.13.0.tar.gz", hash = "sha256:452574d34bd978c8d11b5f02b0182b48e8854e511c969483c83875ec01495f11", size = 14251 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/a7/2ffbd3bea564927e74966a1a3a512a68b491d602d77890daa67e3033bdf4/supabase-2.13.0-py3-none-any.whl", hash = "sha256:6cfccc055be21dab311afc5e9d5b37f3a4966f8394703763fbc8f8e86f36eaa6", size = 17171 }, +] + +[[package]] +name = "supafunc" +version = "0.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", extra = ["http2"] }, + { name = "strenum" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/03/2ca4dddd4a8d28f5dbe204ea0350fb3e4fbf16156ef446f12e0a73d9e718/supafunc-0.9.3.tar.gz", hash = "sha256:29a06d0dc9fe049ecc1249e53ccf3d2a80d72239200f69b510740217aca6497c", size = 4730 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/ec/56e3de38ee99f11c6d645ce8f2a1c29c4561adcb47e53e7781b9c073aa7e/supafunc-0.9.3-py3-none-any.whl", hash = "sha256:83e36ed5e94d2dd0484011aad0b09337d35a87992adbc97acc31c8201aca05d0", size = 7690 }, +] + [[package]] name = "typer" version = "0.15.1" @@ -950,20 +1258,50 @@ wheels = [ [[package]] name = "websockets" -version = "15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/7a/8bc4d15af7ff30f7ba34f9a172063bfcee9f5001d7cef04bee800a658f33/websockets-15.0.tar.gz", hash = "sha256:ca36151289a15b39d8d683fd8b7abbe26fc50be311066c5f8dcf3cb8cee107ab", size = 175574 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/23/be28dc1023707ac51768f848d28a946443041a348ee3a54abdf9f6283372/websockets-15.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d2244d8ab24374bed366f9ff206e2619345f9cd7fe79aad5225f53faac28b6b1", size = 174714 }, - { url = "https://files.pythonhosted.org/packages/8f/ff/02b5e9fbb078e7666bf3d25c18c69b499747a12f3e7f2776063ef3fb7061/websockets-15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3a302241fbe825a3e4fe07666a2ab513edfdc6d43ce24b79691b45115273b5e7", size = 172374 }, - { url = "https://files.pythonhosted.org/packages/8e/61/901c8d4698e0477eff4c3c664d53f898b601fa83af4ce81946650ec2a4cb/websockets-15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:10552fed076757a70ba2c18edcbc601c7637b30cdfe8c24b65171e824c7d6081", size = 172605 }, - { url = "https://files.pythonhosted.org/packages/d2/4b/dc47601a80dff317aecf8da7b4ab278d11d3494b2c373b493e4887561f90/websockets-15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c53f97032b87a406044a1c33d1e9290cc38b117a8062e8a8b285175d7e2f99c9", size = 182380 }, - { url = "https://files.pythonhosted.org/packages/83/f7/b155d2b38f05ed47a0b8de1c9ea245fcd7fc625d89f35a37eccba34b42de/websockets-15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1caf951110ca757b8ad9c4974f5cac7b8413004d2f29707e4d03a65d54cedf2b", size = 181325 }, - { url = "https://files.pythonhosted.org/packages/d3/ff/040a20c01c294695cac0e361caf86f33347acc38f164f6d2be1d3e007d9f/websockets-15.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bf1ab71f9f23b0a1d52ec1682a3907e0c208c12fef9c3e99d2b80166b17905f", size = 181763 }, - { url = "https://files.pythonhosted.org/packages/cb/6a/af23e93678fda8341ac8775e85123425e45c608389d3514863c702896ea5/websockets-15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bfcd3acc1a81f106abac6afd42327d2cf1e77ec905ae11dc1d9142a006a496b6", size = 182097 }, - { url = "https://files.pythonhosted.org/packages/7e/3e/1069e159c30129dc03c01513b5830237e576f47cedb888777dd885cae583/websockets-15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c8c5c8e1bac05ef3c23722e591ef4f688f528235e2480f157a9cfe0a19081375", size = 181485 }, - { url = "https://files.pythonhosted.org/packages/9a/a7/c91c47103f1cd941b576bbc452601e9e01f67d5c9be3e0a9abe726491ab5/websockets-15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:86bfb52a9cfbcc09aba2b71388b0a20ea5c52b6517c0b2e316222435a8cdab72", size = 181466 }, - { url = "https://files.pythonhosted.org/packages/16/32/a4ca6e3d56c24aac46b0cf5c03b841379f6409d07fc2044b244f90f54105/websockets-15.0-cp313-cp313-win32.whl", hash = "sha256:26ba70fed190708551c19a360f9d7eca8e8c0f615d19a574292b7229e0ae324c", size = 175673 }, - { url = "https://files.pythonhosted.org/packages/c0/31/25a417a23e985b61ffa5544f9facfe4a118cb64d664c886f1244a8baeca5/websockets-15.0-cp313-cp313-win_amd64.whl", hash = "sha256:ae721bcc8e69846af00b7a77a220614d9b2ec57d25017a6bbde3a99473e41ce8", size = 176115 }, - { url = "https://files.pythonhosted.org/packages/e8/b2/31eec524b53f01cd8343f10a8e429730c52c1849941d1f530f8253b6d934/websockets-15.0-py3-none-any.whl", hash = "sha256:51ffd53c53c4442415b613497a34ba0aa7b99ac07f1e4a62db5dcd640ae6c3c3", size = 169023 }, +version = "14.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/54/8359678c726243d19fae38ca14a334e740782336c9f19700858c4eb64a1e/websockets-14.2.tar.gz", hash = "sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5", size = 164394 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/94/4f9b55099a4603ac53c2912e1f043d6c49d23e94dd82a9ce1eb554a90215/websockets-14.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f1372e511c7409a542291bce92d6c83320e02c9cf392223272287ce55bc224e", size = 163102 }, + { url = "https://files.pythonhosted.org/packages/8e/b7/7484905215627909d9a79ae07070057afe477433fdacb59bf608ce86365a/websockets-14.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4da98b72009836179bb596a92297b1a61bb5a830c0e483a7d0766d45070a08ad", size = 160766 }, + { url = "https://files.pythonhosted.org/packages/a3/a4/edb62efc84adb61883c7d2c6ad65181cb087c64252138e12d655989eec05/websockets-14.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8a86a269759026d2bde227652b87be79f8a734e582debf64c9d302faa1e9f03", size = 160998 }, + { url = "https://files.pythonhosted.org/packages/f5/79/036d320dc894b96af14eac2529967a6fc8b74f03b83c487e7a0e9043d842/websockets-14.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86cf1aaeca909bf6815ea714d5c5736c8d6dd3a13770e885aafe062ecbd04f1f", size = 170780 }, + { url = "https://files.pythonhosted.org/packages/63/75/5737d21ee4dd7e4b9d487ee044af24a935e36a9ff1e1419d684feedcba71/websockets-14.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b0f6c3ba3b1240f602ebb3971d45b02cc12bd1845466dd783496b3b05783a5", size = 169717 }, + { url = "https://files.pythonhosted.org/packages/2c/3c/bf9b2c396ed86a0b4a92ff4cdaee09753d3ee389be738e92b9bbd0330b64/websockets-14.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669c3e101c246aa85bc8534e495952e2ca208bd87994650b90a23d745902db9a", size = 170155 }, + { url = "https://files.pythonhosted.org/packages/75/2d/83a5aca7247a655b1da5eb0ee73413abd5c3a57fc8b92915805e6033359d/websockets-14.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eabdb28b972f3729348e632ab08f2a7b616c7e53d5414c12108c29972e655b20", size = 170495 }, + { url = "https://files.pythonhosted.org/packages/79/dd/699238a92761e2f943885e091486378813ac8f43e3c84990bc394c2be93e/websockets-14.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2066dc4cbcc19f32c12a5a0e8cc1b7ac734e5b64ac0a325ff8353451c4b15ef2", size = 169880 }, + { url = "https://files.pythonhosted.org/packages/c8/c9/67a8f08923cf55ce61aadda72089e3ed4353a95a3a4bc8bf42082810e580/websockets-14.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab95d357cd471df61873dadf66dd05dd4709cae001dd6342edafc8dc6382f307", size = 169856 }, + { url = "https://files.pythonhosted.org/packages/17/b1/1ffdb2680c64e9c3921d99db460546194c40d4acbef999a18c37aa4d58a3/websockets-14.2-cp313-cp313-win32.whl", hash = "sha256:a9e72fb63e5f3feacdcf5b4ff53199ec8c18d66e325c34ee4c551ca748623bbc", size = 163974 }, + { url = "https://files.pythonhosted.org/packages/14/13/8b7fc4cb551b9cfd9890f0fd66e53c18a06240319915533b033a56a3d520/websockets-14.2-cp313-cp313-win_amd64.whl", hash = "sha256:b439ea828c4ba99bb3176dc8d9b933392a2413c0f6b149fdcba48393f573377f", size = 164420 }, + { url = "https://files.pythonhosted.org/packages/7b/c8/d529f8a32ce40d98309f4470780631e971a5a842b60aec864833b3615786/websockets-14.2-py3-none-any.whl", hash = "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b", size = 157416 }, +] + +[[package]] +name = "yarl" +version = "1.18.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/4b94a8e6d2b51b599516a5cb88e5bc99b4d8d4583e468057eaa29d5f0918/yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", size = 181062 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/c7/c790513d5328a8390be8f47be5d52e141f78b66c6c48f48d241ca6bd5265/yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb", size = 140789 }, + { url = "https://files.pythonhosted.org/packages/30/aa/a2f84e93554a578463e2edaaf2300faa61c8701f0898725842c704ba5444/yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa", size = 94144 }, + { url = "https://files.pythonhosted.org/packages/c6/fc/d68d8f83714b221a85ce7866832cba36d7c04a68fa6a960b908c2c84f325/yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782", size = 91974 }, + { url = "https://files.pythonhosted.org/packages/56/4e/d2563d8323a7e9a414b5b25341b3942af5902a2263d36d20fb17c40411e2/yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0", size = 333587 }, + { url = "https://files.pythonhosted.org/packages/25/c9/cfec0bc0cac8d054be223e9f2c7909d3e8442a856af9dbce7e3442a8ec8d/yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482", size = 344386 }, + { url = "https://files.pythonhosted.org/packages/ab/5d/4c532190113b25f1364d25f4c319322e86232d69175b91f27e3ebc2caf9a/yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186", size = 345421 }, + { url = "https://files.pythonhosted.org/packages/23/d1/6cdd1632da013aa6ba18cee4d750d953104a5e7aac44e249d9410a972bf5/yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58", size = 339384 }, + { url = "https://files.pythonhosted.org/packages/9a/c4/6b3c39bec352e441bd30f432cda6ba51681ab19bb8abe023f0d19777aad1/yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53", size = 326689 }, + { url = "https://files.pythonhosted.org/packages/23/30/07fb088f2eefdc0aa4fc1af4e3ca4eb1a3aadd1ce7d866d74c0f124e6a85/yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2", size = 345453 }, + { url = "https://files.pythonhosted.org/packages/63/09/d54befb48f9cd8eec43797f624ec37783a0266855f4930a91e3d5c7717f8/yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8", size = 341872 }, + { url = "https://files.pythonhosted.org/packages/91/26/fd0ef9bf29dd906a84b59f0cd1281e65b0c3e08c6aa94b57f7d11f593518/yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1", size = 347497 }, + { url = "https://files.pythonhosted.org/packages/d9/b5/14ac7a256d0511b2ac168d50d4b7d744aea1c1aa20c79f620d1059aab8b2/yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a", size = 359981 }, + { url = "https://files.pythonhosted.org/packages/ca/b3/d493221ad5cbd18bc07e642894030437e405e1413c4236dd5db6e46bcec9/yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10", size = 366229 }, + { url = "https://files.pythonhosted.org/packages/04/56/6a3e2a5d9152c56c346df9b8fb8edd2c8888b1e03f96324d457e5cf06d34/yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8", size = 360383 }, + { url = "https://files.pythonhosted.org/packages/fd/b7/4b3c7c7913a278d445cc6284e59b2e62fa25e72758f888b7a7a39eb8423f/yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d", size = 310152 }, + { url = "https://files.pythonhosted.org/packages/f5/d5/688db678e987c3e0fb17867970700b92603cadf36c56e5fb08f23e822a0c/yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c", size = 315723 }, + { url = "https://files.pythonhosted.org/packages/f5/4b/a06e0ec3d155924f77835ed2d167ebd3b211a7b0853da1cf8d8414d784ef/yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", size = 45109 }, ] From 3ae842205daad24e6434ae396228ba96d6729837 Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Wed, 26 Feb 2025 11:06:09 -0800 Subject: [PATCH 58/71] Zipping and uploading env works --- .hilignore | 1 + pyproject.toml | 1 + software/hil/dist_plugin.py | 64 ++++++++++++++++++++++++++++-- software/httpdist_server/server.py | 43 +++++++++++++++----- uv.lock | 11 +++++ 5 files changed, 107 insertions(+), 13 deletions(-) create mode 100644 .hilignore diff --git a/.hilignore b/.hilignore new file mode 100644 index 00000000..ccd7a704 --- /dev/null +++ b/.hilignore @@ -0,0 +1 @@ +elec/ diff --git a/pyproject.toml b/pyproject.toml index 00106bde..893952d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ "httpx>=0.28.1", "fastapi[standard]>=0.115.8", "supabase>=2.13.0", + "pathspec>=0.12.1", ] [project.entry-points."pytest11"] diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index 8fe220f4..7d3b2640 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -2,8 +2,11 @@ import base64 from dataclasses import dataclass from enum import StrEnum, auto +import itertools +from os import PathLike from pathlib import Path from typing import TypedDict +import zipfile import cloudpickle import httpx @@ -12,6 +15,10 @@ import tempfile import shutil +import pathspec + +import rich.progress + logger = logging.getLogger(__name__) httpx_logger = logging.getLogger("httpx") @@ -91,6 +98,14 @@ async def _post(self, path: str, data: dict): response.raise_for_status() return response.json() + async def _post_files(self, path: str, file_path: PathLike): + file_path = Path(file_path) + async with httpx.AsyncClient() as client: + with open(file_path, "rb") as f: + response = await client.post(f"{self.API_URL}/{path}", files={"env": f}) + response.raise_for_status() + return response.json() + async def _get(self, path: str, params: dict | None = None): async with httpx.AsyncClient() as client: response = await client.get(f"{self.API_URL}/{path}", params=params) @@ -111,6 +126,15 @@ async def get_client_session(self) -> SessionId: self.session_id = session_id return session_id + async def submit_env(self, env: Path): + if self.session_id is None: + raise SessionNotStartedError("Must have an active session") + + await self._post_files( + f"session/{self.session_id}/env", + env, + ) + async def submit_tests(self, tests: list[TestSpec]): if self.session_id is None: raise SessionNotStartedError("Must have an active session") @@ -356,7 +380,37 @@ def __init__(self, config: pytest.Config): self.api_client = ClientApi(config) self.statuses = {} - def submit_env(self): ... + async def submit_env(self, env: Path): + # Create a pathspec to exclude certain files + # FIXME: this will ignore all but the top-level `.git/hilignore` + # Always ignore .git/ to avoid including the entire repo in the env + ignore_pattern_lines = [".git/"] + for ignore_file in itertools.chain( + env.glob("*.gitignore"), env.glob("*.hilignore") + ): + if not ignore_file.is_file(): + continue + with open(ignore_file, "r") as f: + ignore_pattern_lines.extend(line.strip() for line in f.readlines()) + ignore_spec = pathspec.GitIgnoreSpec.from_lines(ignore_pattern_lines) + matched_files = list(ignore_spec.match_tree_files(env, negate=True)) + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + zip_path = temp_path / "env.zip" + with zipfile.ZipFile(zip_path, "w") as zip_file: + for file in rich.progress.track( + matched_files, description="zipping env..." + ): + zip_file.write(env / file, file) + + size_mb = zip_path.stat().st_size / 1024 / 1024 + if size_mb > 5: + rich.print( + f"[yellow]WARNING:[/yellow] Large env size: {size_mb:.1f}MB. Consider adding more to [blue].hilignore[/blue]." + ) + + await self.api_client.submit_env(zip_path) async def submit_tests(self, session: pytest.Session): nodeids = {item.nodeid for item in session.items} @@ -403,7 +457,8 @@ async def download_artifacts(self): @pytest.hookimpl def pytest_sessionfinish(self, session: pytest.Session, exitstatus: int): - asyncio.run(self.download_artifacts()) + if exitstatus == pytest.ExitCode.OK: + asyncio.run(self.download_artifacts()) @pytest.hookimpl(tryfirst=True) def pytest_collection(self, session: pytest.Session): @@ -424,8 +479,8 @@ def pytest_runtestloop(self, session: pytest.Session): self.results = TestResults(set(item.nodeid for item in session.items)) async def run(): - # self.submit_env() # TODO await self.api_client.get_client_session() + await self.submit_env(session.config.rootpath) await self.submit_tests(session) while True: @@ -436,7 +491,8 @@ async def run(): if self.results.all_done: break - await asyncio.sleep(1) # FIXME + # Loop delay to avoid overwhelming server + await asyncio.sleep(1) asyncio.run(run()) diff --git a/software/httpdist_server/server.py b/software/httpdist_server/server.py index 13c7a759..96edfed2 100644 --- a/software/httpdist_server/server.py +++ b/software/httpdist_server/server.py @@ -1,6 +1,7 @@ import logging import base64 from dataclasses import dataclass, field +from pathlib import Path import uuid import fastapi @@ -30,6 +31,8 @@ app = fastapi.FastAPI() +ENV_DIR = Path(".envs") + @dataclass class Worker: @@ -59,13 +62,13 @@ class Test: state: SessionState = SessionState.Setup tests: dict[str, Test] = field(default_factory=dict) - env: UploadFile | None = None + env: Path | None = None artifacts: dict[str, bytes] = field(default_factory=dict) - async def stop(self): + def stop(self): self.state = SessionState.Stopped if self.env is not None: - await self.env.close() + self.env.unlink() # TODO: stick this in a database or something @@ -94,8 +97,11 @@ async def stop(self): @app.get("/") -async def root(): - return {"message": "Hello World!"} +async def root() -> dict[str, list[str]]: + return { + "sessions": list(sessions.keys()), + "workers": [worker.worker_id for worker in workers], + } @app.get("/session") @@ -110,7 +116,7 @@ async def stop_session(session_id: str) -> SuccessResponse: if session_id not in sessions: raise fastapi.HTTPException(status_code=404, detail="Session not found") - await sessions[session_id].stop() + sessions[session_id].stop() return SuccessResponse(message="Stop signal sent") @@ -122,7 +128,13 @@ async def submit_session_env(session_id: str, env: UploadFile) -> SuccessRespons raise fastapi.HTTPException(status_code=404, detail="Session not found") # TODO: Store this in a proper storage backend - sessions[session_id].env = env + env_path = ENV_DIR / session_id + env_path.parent.mkdir(parents=True, exist_ok=True) + with env_path.open("wb") as f: + f.write(await env.read()) + + sessions[session_id].env = env_path + logger.info(f"Uploaded environment file for session {session_id}") return SuccessResponse(message="Environment file uploaded successfully") @@ -229,12 +241,25 @@ async def get_worker_session(worker_id: str) -> SessionResponse | NoSessionRespo @app.get("/worker/session/{session_id}/env") -async def fetch_worker_session_env(session_id: str): +async def fetch_worker_session_env(session_id: str) -> Response: """Get the environment for a session""" if session_id not in sessions: raise fastapi.HTTPException(status_code=404, detail="Session not found") - return sessions[session_id].env + session = sessions[session_id] + + if session.env is None: + raise fastapi.HTTPException( + status_code=404, detail="Environment file not found" + ) + + file_content = session.env.read_bytes() + + return Response( + content=file_content, + media_type="application/zip", + headers={"Content-Disposition": 'attachment; filename="env.zip"'}, + ) @app.get("/worker/{worker_id}/session/{session_id}/tests") diff --git a/uv.lock b/uv.lock index f7bd1c91..830a4e2a 100644 --- a/uv.lock +++ b/uv.lock @@ -318,6 +318,7 @@ dependencies = [ { name = "fastapi", extra = ["standard"] }, { name = "httpx" }, { name = "numpy" }, + { name = "pathspec" }, { name = "pathvalidate" }, { name = "polars" }, { name = "pydantic" }, @@ -348,6 +349,7 @@ requires-dist = [ { name = "fastapi", extras = ["standard"], specifier = ">=0.115.8" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "numpy", specifier = ">=2.2.2" }, + { name = "pathspec", specifier = ">=0.12.1" }, { name = "pathvalidate", specifier = ">=3.2.3" }, { name = "polars", specifier = ">=1.22.0" }, { name = "pydantic", specifier = ">=2.10.6" }, @@ -631,6 +633,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + [[package]] name = "pathvalidate" version = "3.2.3" From 669187799fcb0e60b03f0470ca4bdacb3614c68b Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Wed, 26 Feb 2025 11:46:25 -0800 Subject: [PATCH 59/71] Add comments on speedup of zipping --- software/hil/dist_plugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index 7d3b2640..f1097739 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -384,6 +384,8 @@ async def submit_env(self, env: Path): # Create a pathspec to exclude certain files # FIXME: this will ignore all but the top-level `.git/hilignore` # Always ignore .git/ to avoid including the entire repo in the env + # FIXME: this would be way faster if it filtered as it iterated, but + # pathspec iters all the files, and then filters things that don't match ignore_pattern_lines = [".git/"] for ignore_file in itertools.chain( env.glob("*.gitignore"), env.glob("*.hilignore") From edc1b5c495f84cdd6ef4c8e004e22f42954d9c26 Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Wed, 26 Feb 2025 12:26:54 -0800 Subject: [PATCH 60/71] Add message for largest files --- .gitignore | 1 + software/hil/dist_plugin.py | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/.gitignore b/.gitignore index 8cab42d7..3bfdd806 100644 --- a/.gitignore +++ b/.gitignore @@ -210,3 +210,4 @@ cython_debug/ *.code-workspace artifacts/ +.envs/ diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index f1097739..921f99f2 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -411,6 +411,14 @@ async def submit_env(self, env: Path): rich.print( f"[yellow]WARNING:[/yellow] Large env size: {size_mb:.1f}MB. Consider adding more to [blue].hilignore[/blue]." ) + largest_files = sorted( + matched_files, + key=lambda x: (env / x).stat().st_size, + reverse=True, + )[:10] + rich.print( + f"Largest files: {', '.join(str(file) for file in largest_files)}" + ) await self.api_client.submit_env(zip_path) From f30dae859a2d10f5ee9627fee2422e9943a4d8c4 Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Wed, 26 Feb 2025 14:02:04 -0800 Subject: [PATCH 61/71] Persist worker information --- software/hil/dist_plugin.py | 8 +- software/hil/utils/pet_name.py | 10 +- software/httpdist_server/models.py | 10 ++ software/httpdist_server/server.py | 188 +++++++++++++++++++++++------ 4 files changed, 171 insertions(+), 45 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index 921f99f2..40265e3a 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -84,6 +84,10 @@ class SessionNotStartedError(ApiUsageError): pass +class NoWorkersAvailableError(ApiUsageError): + pass + + class ApiBase: # FIXME API_URL = "http://localhost:8000" @@ -142,8 +146,8 @@ async def submit_tests(self, tests: list[TestSpec]): try: await self._post(f"session/{self.session_id}/tests", {"tests": tests}) except httpx.HTTPStatusError as e: - if e.response.status_code == 422: - raise ApiUsageError(e.response.text) + if e.response.status_code == 503: + raise NoWorkersAvailableError(e.response.text) raise async def fetch_statuses( diff --git a/software/hil/utils/pet_name.py b/software/hil/utils/pet_name.py index 485a1a1f..ba6ccf6c 100644 --- a/software/hil/utils/pet_name.py +++ b/software/hil/utils/pet_name.py @@ -90,7 +90,7 @@ def looks_like_a_pet_name(name: str) -> bool: return adjective in ADJECTIVES and animal in ANIMALS -def get_pet_name(identifier: int | None = None) -> str: +def get_pet_name(identifier: int | str | None = None) -> str: """ Generate a deterministic pet name, typically from a MAC address. Returns a combination of an adjective and an animal name. @@ -100,10 +100,14 @@ def get_pet_name(identifier: int | None = None) -> str: 'chunky-otter' """ if identifier is None: - identifier = uuid.getnode() + id_bytes = uuid.getnode().to_bytes(6) + elif isinstance(identifier, str): + id_bytes = identifier.encode("utf-8") + else: + id_bytes = identifier.to_bytes(6) # MACs aren't evenly distributed, so we hash them to get a more even distribution - hashed = hashlib.md5(identifier.to_bytes(6)).digest() + hashed = hashlib.md5(id_bytes).digest() # Extract first 3 bytes for adjective (24 bits) adj_hash = int.from_bytes(hashed[:3]) diff --git a/software/httpdist_server/models.py b/software/httpdist_server/models.py index 48fe822f..0a58be79 100644 --- a/software/httpdist_server/models.py +++ b/software/httpdist_server/models.py @@ -82,6 +82,16 @@ class SubmitTestReportRequest(BaseModel): class WorkerRegisterRequest(BaseModel): worker_id: str + + +class WorkerUpdateRequest(BaseModel): + worker_id: str + pet_name: str + tags: list[str] + + +class WorkerInfoResponse(BaseModel): + worker_id: str pet_name: str tags: list[str] diff --git a/software/httpdist_server/server.py b/software/httpdist_server/server.py index 96edfed2..00e6ae3f 100644 --- a/software/httpdist_server/server.py +++ b/software/httpdist_server/server.py @@ -1,12 +1,16 @@ +from datetime import datetime, timedelta import logging import base64 from dataclasses import dataclass, field +import os from pathlib import Path import uuid import fastapi +from hil.utils.pet_name import get_pet_name import uvicorn -from fastapi import Response, UploadFile +from fastapi import BackgroundTasks, Response, UploadFile +from supabase import create_client from httpdist_server.models import ( ArtifactListResponse, @@ -25,6 +29,9 @@ TestsResponse, WorkerAction, WorkerActionResponse, + WorkerInfoResponse, + WorkerRegisterRequest, + WorkerUpdateRequest, ) logger = logging.getLogger(__name__) @@ -33,12 +40,22 @@ ENV_DIR = Path(".envs") +SUPABASE_URL = os.getenv("SUPABASE_URL", "https://ynesgbuoxmszjrkzazxz.supabase.co") +SUPABASE_KEY = os.getenv( + "SUPABASE_KEY", + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InluZXNnYnVveG1zempya3phenh6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzQzNzg5NDYsImV4cCI6MjA0OTk1NDk0Nn0.6KxEoSHTgyV4jKnnLAG5-Y9tWfHOzpl0qnA_NPzGUBo", +) +WORKER_TIMEOUT = timedelta(minutes=2) + +supabase = create_client(SUPABASE_URL, SUPABASE_KEY) + @dataclass -class Worker: +class ConfiguredWorker: worker_id: str # Typically the worker's mac address pet_name: str tags: set[str] + last_seen: datetime @dataclass @@ -49,7 +66,6 @@ class Test: nodeid: NodeId status: TestStatus = TestStatus.Pending - assigned_worker: Worker | None = None reports: dict[TestPhase, str | None] = field( default_factory=lambda: { TestPhase.Setup: None, @@ -87,21 +103,53 @@ def stop(self): }, ), } -workers: list[Worker] = [ - Worker( - worker_id="2ccf6728745b", - pet_name="chunky-otter", - tags={"cellsim"}, + + +async def _get_worker(worker_id: str) -> ConfiguredWorker: + workers_response = ( + supabase.table("workers").select("*").eq("id", worker_id).execute() + ) + if len(workers_response.data) == 0: + raise fastapi.HTTPException(status_code=404, detail="Worker not found") + + return ConfiguredWorker( + worker_id=workers_response.data[0]["id"], + pet_name=workers_response.data[0]["pet_name"], + tags=workers_response.data[0]["tags"], + last_seen=datetime.fromisoformat(workers_response.data[0]["last_seen"]), ) -] + + +async def _get_active_workers() -> list[ConfiguredWorker]: + workers = [] + workers_response = ( + supabase.table("workers") + .select("*") + .gte("last_seen", datetime.now() - WORKER_TIMEOUT) + .neq("tags", None) + .execute() + ) + for worker in workers_response.data: + workers.append( + ConfiguredWorker( + worker_id=worker["id"], + pet_name=worker["pet_name"], + tags=worker["tags"], + last_seen=datetime.fromisoformat(worker["last_seen"]), + ) + ) + return workers + + +async def _worker_seen(worker_id: str): + supabase.table("workers").update({"last_seen": datetime.now()}).eq( + "id", worker_id + ).execute() @app.get("/") -async def root() -> dict[str, list[str]]: - return { - "sessions": list(sessions.keys()), - "workers": [worker.worker_id for worker in workers], - } +async def root(): + return {"message": "Hello, World!"} @app.get("/session") @@ -145,6 +193,11 @@ async def submit_tests(session_id: str, request: SubmitTestsRequest) -> SuccessR if session_id not in sessions: raise fastapi.HTTPException(status_code=404, detail="Session not found") + workers = await _get_active_workers() + + if not workers: + raise fastapi.HTTPException(status_code=503, detail="No workers available") + unprocessable_tags = set() worker_tags = {tag for worker in workers for tag in worker.tags} for test in request.tests: @@ -159,7 +212,7 @@ async def submit_tests(session_id: str, request: SubmitTestsRequest) -> SuccessR if unprocessable_tags: # TODO: implement as a validator on the model raise fastapi.HTTPException( - status_code=422, + status_code=503, detail=f"Tests with unprocessable tags: {unprocessable_tags}", ) @@ -223,21 +276,21 @@ async def query_test_report( @app.get(path="/worker/{worker_id}/session") -async def get_worker_session(worker_id: str) -> SessionResponse | NoSessionResponse: +async def get_worker_session( + worker_id: str, background_tasks: BackgroundTasks +) -> SessionResponse | NoSessionResponse: """Get the session for a worker""" - for worker in workers: - if worker.worker_id == worker_id: - for session in sessions.values(): - if session.state == SessionState.Running: - if any( - test.worker_requirements.issubset(worker.tags) - for test in session.tests.values() - ): - return SessionResponse(session_id=session.session_id) - - return NoSessionResponse() + worker = await _get_worker(worker_id) + background_tasks.add_task(_worker_seen, worker_id) + for session in sessions.values(): + if session.state == SessionState.Running: + if any( + test.worker_requirements.issubset(worker.tags) + for test in session.tests.values() + ): + return SessionResponse(session_id=session.session_id) - raise fastapi.HTTPException(status_code=404, detail="Worker not found") + return NoSessionResponse() @app.get("/worker/session/{session_id}/env") @@ -263,7 +316,9 @@ async def fetch_worker_session_env(session_id: str) -> Response: @app.get("/worker/{worker_id}/session/{session_id}/tests") -async def fetch_session_tests(worker_id: str, session_id: str) -> WorkerActionResponse: +async def fetch_session_tests( + worker_id: str, session_id: str, background_tasks: BackgroundTasks +) -> WorkerActionResponse: """Get the tests for a session""" if session_id not in sessions: raise fastapi.HTTPException(status_code=404, detail="Session not found") @@ -275,22 +330,15 @@ async def fetch_session_tests(worker_id: str, session_id: str) -> WorkerActionRe action=WorkerAction.Stop, test_now=None, test_next=None ) - for worker in workers: - if worker.worker_id == worker_id: - break - else: - raise fastapi.HTTPException(status_code=404, detail="Worker not found") - + background_tasks.add_task(_worker_seen, worker_id) + worker = await _get_worker(worker_id) worker_testable: list[str] = [] for test in sessions[session_id].tests.values(): - if ( - test.status == TestStatus.Pending - and test.assigned_worker is None - and test.worker_requirements.issubset(worker.tags) + if test.status == TestStatus.Pending and test.worker_requirements.issubset( + worker.tags ): worker_testable.append(test.nodeid) if len(worker_testable) == 1: - test.assigned_worker = worker test.status = TestStatus.Running elif len(worker_testable) >= 2: break @@ -347,5 +395,65 @@ async def download_artifact(session_id: str, artifact_id: str) -> Response: return Response(content=artifact_content, media_type="application/octet-stream") +@app.post("/worker/register") +async def register_worker(request: WorkerRegisterRequest) -> SuccessResponse: + """Register a worker with the server""" + # If the worker is already registered, update the last seen time and return + if supabase.table("workers").select("*").eq("id", request.worker_id).execute().data: + await _worker_seen(request.worker_id) + return SuccessResponse(message="Worker registered successfully") + + # Otherwise, insert the worker into the database + pet_name = get_pet_name(request.worker_id) + supabase.table("workers").insert( + {"id": request.worker_id, "pet_name": pet_name, "last_seen": datetime.now()} + ).execute() + + return SuccessResponse(message="Worker registered successfully") + + +@app.post("/worker/update") +async def update_worker(request: WorkerUpdateRequest) -> SuccessResponse: + """Update a worker's information""" + if ( + not supabase.table("workers") + .select("*") + .eq("id", request.worker_id) + .execute() + .data + ): + raise fastapi.HTTPException(status_code=404, detail="Worker not found") + + supabase.table("workers").update( + {"pet_name": request.pet_name, "tags": request.tags} + ).eq("id", request.worker_id).execute() + return SuccessResponse(message="Worker updated successfully") + + +@app.get("/worker/{worker_id}/info") +async def get_worker_info(worker_id: str) -> WorkerInfoResponse: + """Get information about a worker""" + worker = await _get_worker(worker_id) + return WorkerInfoResponse( + worker_id=worker.worker_id, + pet_name=worker.pet_name, + tags=list(worker.tags), + ) + + +@app.get("/worker/list") +async def list_workers() -> list[WorkerInfoResponse]: + """List all workers""" + workers = await _get_active_workers() + return [ + WorkerInfoResponse( + worker_id=worker.worker_id, + pet_name=worker.pet_name, + tags=list(worker.tags), + ) + for worker in workers + ] + + if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000) From 623ce6510e9900f8affb6b1bfda8c33112d9c5cb Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Wed, 26 Feb 2025 14:34:22 -0800 Subject: [PATCH 62/71] Synchronous worker API --- software/hil/dist_plugin.py | 68 +++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 37 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index 40265e3a..4df2f50e 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -88,7 +88,7 @@ class NoWorkersAvailableError(ApiUsageError): pass -class ApiBase: +class ClientApi: # FIXME API_URL = "http://localhost:8000" session_id: SessionId | None = None @@ -122,8 +122,6 @@ async def _get_raw(self, path: str, params: dict | None = None): response.raise_for_status() return response - -class ClientApi(ApiBase): async def get_client_session(self) -> SessionId: session = await self._get("session") session_id = session["session_id"] @@ -199,26 +197,37 @@ async def download_artifact( return artifacts_dir -class WorkerApi(ApiBase): +class WorkerApi: + # FIXME + API_URL = "http://localhost:8000" + session_id: SessionId | None = None + def __init__(self, config: pytest.Config): - super().__init__(config) session_id = config.getoption("httpdist_session_id") assert isinstance(session_id, str) self.session_id = session_id - async def signal_ready(self): ... + def _get(self, path: str, params: dict | None = None): + with httpx.Client() as client: + response = client.get(f"{self.API_URL}/{path}", params=params) + response.raise_for_status() + return response.json() - async def signal_done(self): ... + def _post(self, path: str, data: dict): + with httpx.Client() as client: + response = client.post(f"{self.API_URL}/{path}", json=data) + response.raise_for_status() + return response.json() - async def fetch_work(self, worker_id: WorkerId) -> tuple[NodeId, NodeId | None]: - data = await self._get(f"worker/{worker_id}/session/{self.session_id}/tests") + def fetch_work(self, worker_id: WorkerId) -> tuple[NodeId, NodeId | None]: + data = self._get(f"worker/{worker_id}/session/{self.session_id}/tests") if data["action"] == "stop": raise EndOfSession() return data["test_now"], data["test_next"] - async def report_result( + def report_result( self, nodeid: NodeId, report: pytest.TestReport, phase: TestPhase ): data = { @@ -226,9 +235,9 @@ async def report_result( "report": base64.b64encode(cloudpickle.dumps(report)).decode(), "phase": phase, } - await self._post(f"worker/session/{self.session_id}/test", data) + self._post(f"worker/session/{self.session_id}/test", data) - async def upload_artifacts(self, worker_id: str): + def upload_artifacts(self, worker_id: str): """Upload all artifact files""" if not ARTIFACTS_DIR.exists(): return @@ -244,7 +253,7 @@ async def upload_artifacts(self, worker_id: str): ) with open(zip_path, "rb") as zip_file: - await self._post( + self._post( f"worker/session/{self.session_id}/artifacts", { "worker_id": worker_id, @@ -297,23 +306,12 @@ def pytest_runtestloop(self, session: pytest.Session): self.session = session self._items_by_nodeid = {item.nodeid: item for item in session.items} - async def _run_tests(): - await self.api_client.signal_ready() - - while True: - try: - nodeid_now, nodeid_next = await self.api_client.fetch_work( - self.worker_id - ) - self.process_test(nodeid_now, nodeid_next) - except EndOfSession: - break - - await self.api_client.signal_done() - - await asyncio.gather(*self._reporting_tasks) - - asyncio.run(_run_tests()) + while True: + try: + nodeid_now, nodeid_next = self.api_client.fetch_work(self.worker_id) + self.process_test(nodeid_now, nodeid_next) + except EndOfSession: + break return True @@ -321,17 +319,13 @@ async def _run_tests(): def pytest_runtest_logreport(self, report: pytest.TestReport) -> None: # This hook is called from within the runtestloop, so we need to run # it in a task instead of calling it directly - self._reporting_tasks.append( - asyncio.create_task( - self.api_client.report_result( - report.nodeid, report, phase=TestPhase(report.when) - ) - ) + self.api_client.report_result( + report.nodeid, report, phase=TestPhase(report.when) ) @pytest.hookimpl def pytest_sessionfinish(self, session: pytest.Session, exitstatus: int): - asyncio.run(self.api_client.upload_artifacts(self.worker_id)) + self.api_client.upload_artifacts(self.worker_id) class TestResults: From 1b7b2b218afa8798dab549e394f952a4a18a4a54 Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Wed, 26 Feb 2025 14:35:08 -0800 Subject: [PATCH 63/71] Use worker_id field, rather than column ID, and add heartbeat --- software/httpdist_server/models.py | 1 - software/httpdist_server/server.py | 39 ++++++++++++++++++++++-------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/software/httpdist_server/models.py b/software/httpdist_server/models.py index 0a58be79..3535a28a 100644 --- a/software/httpdist_server/models.py +++ b/software/httpdist_server/models.py @@ -85,7 +85,6 @@ class WorkerRegisterRequest(BaseModel): class WorkerUpdateRequest(BaseModel): - worker_id: str pet_name: str tags: list[str] diff --git a/software/httpdist_server/server.py b/software/httpdist_server/server.py index 00e6ae3f..a805a088 100644 --- a/software/httpdist_server/server.py +++ b/software/httpdist_server/server.py @@ -107,13 +107,13 @@ def stop(self): async def _get_worker(worker_id: str) -> ConfiguredWorker: workers_response = ( - supabase.table("workers").select("*").eq("id", worker_id).execute() + supabase.table("workers").select("*").eq("worker_id", worker_id).execute() ) if len(workers_response.data) == 0: raise fastapi.HTTPException(status_code=404, detail="Worker not found") return ConfiguredWorker( - worker_id=workers_response.data[0]["id"], + worker_id=workers_response.data[0]["worker_id"], pet_name=workers_response.data[0]["pet_name"], tags=workers_response.data[0]["tags"], last_seen=datetime.fromisoformat(workers_response.data[0]["last_seen"]), @@ -132,7 +132,7 @@ async def _get_active_workers() -> list[ConfiguredWorker]: for worker in workers_response.data: workers.append( ConfiguredWorker( - worker_id=worker["id"], + worker_id=worker["worker_id"], pet_name=worker["pet_name"], tags=worker["tags"], last_seen=datetime.fromisoformat(worker["last_seen"]), @@ -143,7 +143,7 @@ async def _get_active_workers() -> list[ConfiguredWorker]: async def _worker_seen(worker_id: str): supabase.table("workers").update({"last_seen": datetime.now()}).eq( - "id", worker_id + "worker_id", worker_id ).execute() @@ -399,26 +399,38 @@ async def download_artifact(session_id: str, artifact_id: str) -> Response: async def register_worker(request: WorkerRegisterRequest) -> SuccessResponse: """Register a worker with the server""" # If the worker is already registered, update the last seen time and return - if supabase.table("workers").select("*").eq("id", request.worker_id).execute().data: + if ( + supabase.table("workers") + .select("*") + .eq("worker_id", request.worker_id) + .execute() + .data + ): await _worker_seen(request.worker_id) return SuccessResponse(message="Worker registered successfully") # Otherwise, insert the worker into the database pet_name = get_pet_name(request.worker_id) supabase.table("workers").insert( - {"id": request.worker_id, "pet_name": pet_name, "last_seen": datetime.now()} + { + "worker_id": request.worker_id, + "pet_name": pet_name, + "last_seen": datetime.now(), + } ).execute() return SuccessResponse(message="Worker registered successfully") -@app.post("/worker/update") -async def update_worker(request: WorkerUpdateRequest) -> SuccessResponse: +@app.post("/worker/{worker_id}/update") +async def update_worker( + worker_id: str, request: WorkerUpdateRequest +) -> SuccessResponse: """Update a worker's information""" if ( not supabase.table("workers") .select("*") - .eq("id", request.worker_id) + .eq("worker_id", worker_id) .execute() .data ): @@ -426,10 +438,17 @@ async def update_worker(request: WorkerUpdateRequest) -> SuccessResponse: supabase.table("workers").update( {"pet_name": request.pet_name, "tags": request.tags} - ).eq("id", request.worker_id).execute() + ).eq("worker_id", worker_id).execute() return SuccessResponse(message="Worker updated successfully") +@app.post("/worker/{worker_id}/heartbeat") +async def worker_heartbeat(worker_id: str) -> SuccessResponse: + """Update a worker's information""" + await _worker_seen(worker_id) + return SuccessResponse(message="Heartbeat received") + + @app.get("/worker/{worker_id}/info") async def get_worker_info(worker_id: str) -> WorkerInfoResponse: """Get information about a worker""" From e47f8fd4cccf8e57b9eea1f023aff02324ed7f46 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Wed, 26 Feb 2025 14:41:04 -0800 Subject: [PATCH 64/71] init supervisor --- software/httpdist_supervisor/api.go | 58 +++++++++++++++++++++ software/httpdist_supervisor/go.mod | 3 ++ software/httpdist_supervisor/main.go | 75 ++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 software/httpdist_supervisor/api.go create mode 100644 software/httpdist_supervisor/go.mod create mode 100644 software/httpdist_supervisor/main.go diff --git a/software/httpdist_supervisor/api.go b/software/httpdist_supervisor/api.go new file mode 100644 index 00000000..e1adc8ec --- /dev/null +++ b/software/httpdist_supervisor/api.go @@ -0,0 +1,58 @@ +package main + +import ( + "bytes" + "encoding/json" + "io" + "log" + "net/http" +) + +type ApiClient struct { + BaseUrl string +} + +func (c *ApiClient) httpGet(path string) []byte { + request, err := http.NewRequest("GET", c.BaseUrl+path, nil) + if err != nil { + log.Fatal(err) + } + + client := &http.Client{} + response, err := client.Do(request) + if err != nil { + log.Fatal(err) + } + + body, err := io.ReadAll(response.Body) + if err != nil { + log.Fatal(err) + } + + return body +} + +func (c *ApiClient) httpPost(path string, jsonData map[string]string) []byte { + jsonBytes, err := json.Marshal(jsonData) + if err != nil { + log.Fatal(err) + } + + request, err := http.NewRequest("POST", c.BaseUrl+path, bytes.NewBuffer(jsonBytes)) + if err != nil { + log.Fatal(err) + } + + client := &http.Client{} + response, err := client.Do(request) + if err != nil { + log.Fatal(err) + } + + responseBody, err := io.ReadAll(response.Body) + if err != nil { + log.Fatal(err) + } + + return responseBody +} diff --git a/software/httpdist_supervisor/go.mod b/software/httpdist_supervisor/go.mod new file mode 100644 index 00000000..1ff51d1c --- /dev/null +++ b/software/httpdist_supervisor/go.mod @@ -0,0 +1,3 @@ +module httpdist_supervisor + +go 1.23.5 diff --git a/software/httpdist_supervisor/main.go b/software/httpdist_supervisor/main.go new file mode 100644 index 00000000..074efd79 --- /dev/null +++ b/software/httpdist_supervisor/main.go @@ -0,0 +1,75 @@ +package main + +// set worker_id +// register worker with server +// start a wait loop +// poll for session +// when given a session: +// - pull env for session +// - spawn pytest in session env + +import ( + "fmt" + "log" + "net" + "time" +) + +const ( + ifaceName = "en0" + apiUrl = "http://localhost:8000" +) + +func getWorkerId() (string, error) { + // get mac address + interfaces, err := net.Interfaces() + if err != nil { + return "", fmt.Errorf("failed to get interfaces: %w", err) + } + for _, iface := range interfaces { + if iface.Name == ifaceName { + macAddr := iface.HardwareAddr.String() + macAddrNoColons := "" + for _, c := range macAddr { + if c != ':' { + macAddrNoColons += string(c) + } + } + return macAddrNoColons, nil + } + } + + return "", fmt.Errorf("interface not found: %s", ifaceName) +} + +func (c *ApiClient) registerWorker(workerId string) { + jsonData := map[string]string{ + "worker_id": workerId, + } + + responseBody := c.httpPost("/worker/register", jsonData) + fmt.Println(string(responseBody)) +} + +func pollForSession(c *ApiClient, workerId string) { + for { + responseBody := c.httpGet(fmt.Sprintf("/worker/%s/session", workerId)) + fmt.Println(string(responseBody)) + time.Sleep(1 * time.Second) + } +} + +func main() { + workerId, err := getWorkerId() + if err != nil { + log.Fatal(err) + } + fmt.Println("Starting worker with ID:", workerId) + + apiClient := &ApiClient{ + BaseUrl: apiUrl, + } + + apiClient.registerWorker(workerId) + pollForSession(apiClient, workerId) +} From 458fc01cde4bc97fe8ed5c28fd779ec4fb630a34 Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Wed, 26 Feb 2025 14:55:15 -0800 Subject: [PATCH 65/71] Register workers on session query --- software/httpdist_server/server.py | 75 ++++++++++++++++-------------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/software/httpdist_server/server.py b/software/httpdist_server/server.py index a805a088..6be0f934 100644 --- a/software/httpdist_server/server.py +++ b/software/httpdist_server/server.py @@ -30,7 +30,6 @@ WorkerAction, WorkerActionResponse, WorkerInfoResponse, - WorkerRegisterRequest, WorkerUpdateRequest, ) @@ -105,17 +104,35 @@ def stop(self): } +class WorkerNotFound(fastapi.HTTPException): + def __init__(self, worker_id: str): + super().__init__(status_code=404, detail=f"Worker {worker_id} not found") + + +class WorkerUnconfigured(fastapi.HTTPException): + def __init__(self, worker_id: str): + super().__init__(status_code=422, detail=f"Worker {worker_id} is unconfigured") + + async def _get_worker(worker_id: str) -> ConfiguredWorker: workers_response = ( supabase.table("workers").select("*").eq("worker_id", worker_id).execute() ) if len(workers_response.data) == 0: - raise fastapi.HTTPException(status_code=404, detail="Worker not found") + raise WorkerNotFound(worker_id) + + tags = workers_response.data[0]["tags"] + if tags is None: + raise WorkerUnconfigured(worker_id) + + pet_name = workers_response.data[0]["pet_name"] + if pet_name is None: + pet_name = get_pet_name(worker_id) return ConfiguredWorker( worker_id=workers_response.data[0]["worker_id"], - pet_name=workers_response.data[0]["pet_name"], - tags=workers_response.data[0]["tags"], + pet_name=pet_name, + tags=tags, last_seen=datetime.fromisoformat(workers_response.data[0]["last_seen"]), ) @@ -142,7 +159,7 @@ async def _get_active_workers() -> list[ConfiguredWorker]: async def _worker_seen(worker_id: str): - supabase.table("workers").update({"last_seen": datetime.now()}).eq( + supabase.table("workers").update({"last_seen": datetime.now().isoformat()}).eq( "worker_id", worker_id ).execute() @@ -279,9 +296,26 @@ async def query_test_report( async def get_worker_session( worker_id: str, background_tasks: BackgroundTasks ) -> SessionResponse | NoSessionResponse: - """Get the session for a worker""" + """Get the session for a worker, or if the worker isn't registered, register it.""" + worker_exists = bool( + supabase.table("workers").select("*").eq("worker_id", worker_id).execute().data + ) + + if worker_exists: + await _worker_seen(worker_id) + + else: + pet_name = get_pet_name(worker_id) + supabase.table("workers").insert( + { + "worker_id": worker_id, + "pet_name": pet_name, + "last_seen": datetime.now().isoformat(), + } + ).execute() + worker = await _get_worker(worker_id) - background_tasks.add_task(_worker_seen, worker_id) + for session in sessions.values(): if session.state == SessionState.Running: if any( @@ -395,33 +429,6 @@ async def download_artifact(session_id: str, artifact_id: str) -> Response: return Response(content=artifact_content, media_type="application/octet-stream") -@app.post("/worker/register") -async def register_worker(request: WorkerRegisterRequest) -> SuccessResponse: - """Register a worker with the server""" - # If the worker is already registered, update the last seen time and return - if ( - supabase.table("workers") - .select("*") - .eq("worker_id", request.worker_id) - .execute() - .data - ): - await _worker_seen(request.worker_id) - return SuccessResponse(message="Worker registered successfully") - - # Otherwise, insert the worker into the database - pet_name = get_pet_name(request.worker_id) - supabase.table("workers").insert( - { - "worker_id": request.worker_id, - "pet_name": pet_name, - "last_seen": datetime.now(), - } - ).execute() - - return SuccessResponse(message="Worker registered successfully") - - @app.post("/worker/{worker_id}/update") async def update_worker( worker_id: str, request: WorkerUpdateRequest From 14f74f426e8cbae4678d8a1965d9f95cec95b7e0 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Wed, 26 Feb 2025 17:05:28 -0800 Subject: [PATCH 66/71] supervised test execution --- software/httpdist_supervisor/api.go | 58 +++++++++++++---- software/httpdist_supervisor/main.go | 83 ++++++++++++++++++++---- software/httpdist_supervisor/session.go | 86 +++++++++++++++++++++++++ software/httpdist_supervisor/util.go | 59 +++++++++++++++++ 4 files changed, 261 insertions(+), 25 deletions(-) create mode 100644 software/httpdist_supervisor/session.go create mode 100644 software/httpdist_supervisor/util.go diff --git a/software/httpdist_supervisor/api.go b/software/httpdist_supervisor/api.go index e1adc8ec..546f2a2e 100644 --- a/software/httpdist_supervisor/api.go +++ b/software/httpdist_supervisor/api.go @@ -3,8 +3,8 @@ package main import ( "bytes" "encoding/json" + "fmt" "io" - "log" "net/http" ) @@ -12,47 +12,81 @@ type ApiClient struct { BaseUrl string } -func (c *ApiClient) httpGet(path string) []byte { +func (c *ApiClient) httpGetRaw(path string) ([]byte, int, error) { request, err := http.NewRequest("GET", c.BaseUrl+path, nil) if err != nil { - log.Fatal(err) + return nil, 0, fmt.Errorf("failed to create request: %w", err) } client := &http.Client{} response, err := client.Do(request) if err != nil { - log.Fatal(err) + return nil, 0, fmt.Errorf("failed to send request: %w", err) } body, err := io.ReadAll(response.Body) if err != nil { - log.Fatal(err) + return nil, 0, fmt.Errorf("failed to read response body: %w", err) } - return body + return body, response.StatusCode, nil } -func (c *ApiClient) httpPost(path string, jsonData map[string]string) []byte { +func (c *ApiClient) httpGet(path string) (map[string]interface{}, int, error) { + body, statusCode, err := c.httpGetRaw(path) + if err != nil { + return nil, 0, fmt.Errorf("failed to get raw response: %w", err) + } + + if statusCode == http.StatusNoContent { + return nil, statusCode, nil + } + + var jsonResponse map[string]interface{} + err = json.Unmarshal(body, &jsonResponse) + if err != nil { + return nil, 0, fmt.Errorf("failed to unmarshal response body: %w", err) + } + + return jsonResponse, statusCode, nil +} + +func (c *ApiClient) httpPostRaw(path string, jsonData map[string]string) ([]byte, int, error) { jsonBytes, err := json.Marshal(jsonData) if err != nil { - log.Fatal(err) + return nil, 0, fmt.Errorf("failed to marshal json: %w", err) } request, err := http.NewRequest("POST", c.BaseUrl+path, bytes.NewBuffer(jsonBytes)) if err != nil { - log.Fatal(err) + return nil, 0, fmt.Errorf("failed to create request: %w", err) } client := &http.Client{} response, err := client.Do(request) if err != nil { - log.Fatal(err) + return nil, 0, fmt.Errorf("failed to send request: %w", err) } responseBody, err := io.ReadAll(response.Body) if err != nil { - log.Fatal(err) + return nil, 0, fmt.Errorf("failed to read response body: %w", err) + } + + return responseBody, response.StatusCode, nil +} + +func (c *ApiClient) httpPost(path string, jsonData map[string]string) (map[string]interface{}, int, error) { + body, statusCode, err := c.httpPostRaw(path, jsonData) + if err != nil { + return nil, 0, fmt.Errorf("failed to post raw response: %w", err) + } + + var jsonResponse map[string]interface{} + err = json.Unmarshal(body, &jsonResponse) + if err != nil { + return nil, 0, fmt.Errorf("failed to unmarshal response body: %w", err) } - return responseBody + return jsonResponse, statusCode, nil } diff --git a/software/httpdist_supervisor/main.go b/software/httpdist_supervisor/main.go index 074efd79..041debb9 100644 --- a/software/httpdist_supervisor/main.go +++ b/software/httpdist_supervisor/main.go @@ -12,12 +12,14 @@ import ( "fmt" "log" "net" + "net/http" "time" ) const ( - ifaceName = "en0" - apiUrl = "http://localhost:8000" + programName = "httpdist-supervisor" + ifaceName = "en0" + apiUrl = "http://localhost:8000" ) func getWorkerId() (string, error) { @@ -47,29 +49,84 @@ func (c *ApiClient) registerWorker(workerId string) { "worker_id": workerId, } - responseBody := c.httpPost("/worker/register", jsonData) - fmt.Println(string(responseBody)) + responseJson, statusCode, err := c.httpPost("/worker/register", jsonData) + if err != nil { + log.Fatal(err) + } + if statusCode != http.StatusOK { + log.Fatalf("failed to register worker: %s", responseJson["detail"]) + } + + fmt.Printf("Registered worker: %s\n", responseJson["worker_id"]) } -func pollForSession(c *ApiClient, workerId string) { +func sendHeartbeat(c *ApiClient, workerId string) { for { - responseBody := c.httpGet(fmt.Sprintf("/worker/%s/session", workerId)) - fmt.Println(string(responseBody)) - time.Sleep(1 * time.Second) + c.httpPost(fmt.Sprintf("/worker/%s/heartbeat", workerId), nil) + time.Sleep(10 * time.Second) } } -func main() { - workerId, err := getWorkerId() +func pollForSession(c *ApiClient, workerId string) (*TestSession, error) { + spinnerIdx := 0 + + for { + spinnerIdx = updateSpinner("Waiting for session", spinnerIdx) + + responseJson, statusCode, err := c.httpGet(fmt.Sprintf("/worker/%s/session", workerId)) + if err != nil { + log.Fatal(err) + } + + if statusCode == http.StatusNoContent { + time.Sleep(1 * time.Second) + continue + } else if statusCode != http.StatusOK { + log.Fatalf("failed to get session: %d (%s)", statusCode, responseJson["detail"]) + } + + sessionId := responseJson["session_id"].(string) + + clearSpinner() + fmt.Printf("Received session: %s\n", sessionId) + return &TestSession{WorkerId: workerId, SessionId: sessionId}, nil + } +} + +func runSession(apiClient *ApiClient, workerId string) { + session, err := pollForSession(apiClient, workerId) if err != nil { log.Fatal(err) } - fmt.Println("Starting worker with ID:", workerId) + err = session.prepareEnv(apiClient) + if err != nil { + log.Fatal(err) + } + defer session.cleanup() + + err = session.spawnWorker() + if err != nil { + log.Fatal(err) + } +} + +func main() { apiClient := &ApiClient{ BaseUrl: apiUrl, } - apiClient.registerWorker(workerId) - pollForSession(apiClient, workerId) + workerId, err := getWorkerId() + if err != nil { + log.Fatal(err) + } + fmt.Println("Starting worker with ID:", workerId) + + go sendHeartbeat(apiClient, workerId) + + // apiClient.registerWorker(workerId) + + for { + runSession(apiClient, workerId) + } } diff --git a/software/httpdist_supervisor/session.go b/software/httpdist_supervisor/session.go new file mode 100644 index 00000000..6595f22a --- /dev/null +++ b/software/httpdist_supervisor/session.go @@ -0,0 +1,86 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "time" +) + +type TestSession struct { + WorkerId string + SessionId string + EnvDir string +} + +func (session *TestSession) prepareEnv(apiClient *ApiClient) error { + tempDir, err := os.MkdirTemp("", programName) + if err != nil { + return fmt.Errorf("failed to createTempDir(): %w", err) + } + + zipPath := filepath.Join(tempDir, "env.zip") + zipFile, err := os.Create(zipPath) + if err != nil { + return fmt.Errorf("failed to create zip file: %w", err) + } + defer zipFile.Close() + + zipContent, statusCode, err := apiClient.httpGetRaw(fmt.Sprintf("/worker/%s/session/%s/env", session.WorkerId, session.SessionId)) + if err != nil { + return fmt.Errorf("failed to download env.zip: %w", err) + } + if statusCode != 200 { + return fmt.Errorf("failed to download env.zip: status code %d", statusCode) + } + + _, err = zipFile.Write(zipContent) + if err != nil { + return fmt.Errorf("failed to write zip file: %w", err) + } + zipFile.Close() + + envDir := filepath.Join(tempDir, "env") + err = os.MkdirAll(envDir, 0755) + if err != nil { + return fmt.Errorf("failed to create env directory: %w", err) + } + + err = extractZip(zipPath, envDir) + if err != nil { + return fmt.Errorf("failed to extract zip file: %w", err) + } + + session.EnvDir = envDir + return nil +} + +func (session *TestSession) spawnWorker() error { + // TOOD: pytest args + cmd := exec.Command( + "uv", + "run", + "--isolated", + "pytest", + "--httpdist-worker-id", + session.WorkerId, + "--httpdist-session-id", + session.SessionId, + ) + + cmd.Dir = session.EnvDir + + start := time.Now() + cmd.Run() + + elapsed := time.Since(start) + fmt.Printf("Executed test session %s in %.2fs\n", session.SessionId, elapsed.Seconds()) + + return nil +} + +func (session *TestSession) cleanup() error { + os.RemoveAll(session.EnvDir) + return nil +} diff --git a/software/httpdist_supervisor/util.go b/software/httpdist_supervisor/util.go new file mode 100644 index 00000000..19e14754 --- /dev/null +++ b/software/httpdist_supervisor/util.go @@ -0,0 +1,59 @@ +package main + +import ( + "archive/zip" + "fmt" + "io" + "os" + "path/filepath" +) + +func updateSpinner(message string, spinnerIdx int) int { + spinnerChars := []string{"|", "/", "-", "\\"} + fmt.Printf("\r%s %s", message, spinnerChars[spinnerIdx]) + return (spinnerIdx + 1) % len(spinnerChars) +} + +func clearSpinner() { + fmt.Print("\r \r") +} + +func extractZip(zipPath, destPath string) error { + reader, err := zip.OpenReader(zipPath) + if err != nil { + return fmt.Errorf("failed to open zip file: %w", err) + } + defer reader.Close() + + for _, file := range reader.File { + filePath := filepath.Join(destPath, file.Name) + + if file.FileInfo().IsDir() { + os.MkdirAll(filePath, 0755) + continue + } + if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + + srcFile, err := file.Open() + if err != nil { + return fmt.Errorf("failed to open file in zip: %w", err) + } + + destFile, err := os.Create(filePath) + if err != nil { + srcFile.Close() + return fmt.Errorf("failed to create destination file: %w", err) + } + + _, err = io.Copy(destFile, srcFile) + srcFile.Close() + destFile.Close() + if err != nil { + return fmt.Errorf("failed to copy file contents: %w", err) + } + } + + return nil +} From e2623c713185bdad6ffc63157bdf2a8a61bfa53c Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Wed, 26 Feb 2025 18:11:05 -0800 Subject: [PATCH 67/71] fix --- software/hil/dist_plugin.py | 14 ++--- software/hil/pytest_plugin.py | 18 +------ software/httpdist_server/models.py | 8 ++- software/httpdist_server/server.py | 68 ++++++++++++++----------- software/httpdist_supervisor/main.go | 30 +++++------ software/httpdist_supervisor/session.go | 4 ++ tests/conftest.py | 2 +- tests/test_drivers/test_cell.py | 10 ++-- 8 files changed, 72 insertions(+), 82 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index 4df2f50e..44511ab8 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -28,16 +28,8 @@ ARTIFACTS_DIR = Path("./artifacts") -@dataclass -class RunsOn: - hostname: str | None - - def __init__(self, *args, hostname: str | None = None): - self.hostname = hostname - - def check(self, node) -> bool: - # FIXME - return self.hostname is None or self.hostname == node.gateway.id +class RunsOn(TypedDict): + tags: list[str] NodeId = str @@ -145,7 +137,7 @@ async def submit_tests(self, tests: list[TestSpec]): await self._post(f"session/{self.session_id}/tests", {"tests": tests}) except httpx.HTTPStatusError as e: if e.response.status_code == 503: - raise NoWorkersAvailableError(e.response.text) + raise NoWorkersAvailableError(e.response.json()["detail"]) raise async def fetch_statuses( diff --git a/software/hil/pytest_plugin.py b/software/hil/pytest_plugin.py index c206dacb..856f66d1 100644 --- a/software/hil/pytest_plugin.py +++ b/software/hil/pytest_plugin.py @@ -95,26 +95,10 @@ def pytest_configure(config: _Config): # Based on https://docs.pytest.org/en/stable/example/markers.html#custom-marker-and-command-line-option-to-control-test-runs config.addinivalue_line( "markers", - "runs_on(hostname: str | None = None) - mark test to run only on specific hostname", + "runs_on(tags: list[str]) - mark test to run only on specific tags", ) -def _should_runs_on(*, hostname: str | None = None) -> bool: - if hostname is not None and socket.gethostname() != hostname: - return False - - return True - - -def pytest_runtest_setup(item): - # Check the runs_on marker - runs_on_markers = list(item.iter_markers(name="runs_on")) - if runs_on_markers and not any( - _should_runs_on(*m.args, **m.kwargs) for m in runs_on_markers - ): - pytest.skip("Skipping test because it is not tagged to run on this environment") - - def _save_request_traces( request: _Request, traces: list[Trace], logs: list[logging.LogRecord] ) -> None: diff --git a/software/httpdist_server/models.py b/software/httpdist_server/models.py index 3535a28a..c8740a3c 100644 --- a/software/httpdist_server/models.py +++ b/software/httpdist_server/models.py @@ -35,7 +35,7 @@ class SuccessResponse(BaseModel): class SessionResponse(BaseModel): """Response to a request to start a new session""" - session_id: SessionId + session_id: SessionId | None class NoSessionResponse(BaseModel): @@ -62,9 +62,13 @@ class ArtifactListResponse(BaseModel): artifact_ids: list[str] +class WorkerRequirements(BaseModel): + tags: set[str] + + class SubmitTestsRequest(BaseModel): class Test(BaseModel): - worker_requirements: set[str] + worker_requirements: list[WorkerRequirements] nodeid: NodeId tests: list[Test] diff --git a/software/httpdist_server/server.py b/software/httpdist_server/server.py index 6be0f934..35b51bef 100644 --- a/software/httpdist_server/server.py +++ b/software/httpdist_server/server.py @@ -9,7 +9,7 @@ import fastapi from hil.utils.pet_name import get_pet_name import uvicorn -from fastapi import BackgroundTasks, Response, UploadFile +from fastapi import BackgroundTasks, Response, UploadFile, status from supabase import create_client from httpdist_server.models import ( @@ -30,13 +30,15 @@ WorkerAction, WorkerActionResponse, WorkerInfoResponse, + WorkerRequirements, WorkerUpdateRequest, ) -logger = logging.getLogger(__name__) app = fastapi.FastAPI() +logger = logging.getLogger("uvicorn") + ENV_DIR = Path(".envs") SUPABASE_URL = os.getenv("SUPABASE_URL", "https://ynesgbuoxmszjrkzazxz.supabase.co") @@ -61,7 +63,7 @@ class ConfiguredWorker: class Session: @dataclass class Test: - worker_requirements: set[str] + worker_requirements: list[WorkerRequirements] nodeid: NodeId status: TestStatus = TestStatus.Pending @@ -87,20 +89,21 @@ def stop(self): # TODO: stick this in a database or something +# https://supabase.com/blog/supabase-queues sessions: dict[str, Session] = { - "test-session": Session( - session_id="test-session", - tests={ - "tests/test_nothing.py::test_nothing": Session.Test( - nodeid="tests/test_nothing.py::test_nothing", - worker_requirements={"cellsim"}, - ), - "tests/test_nothing.py::test_fail": Session.Test( - nodeid="tests/test_nothing.py::test_fail", - worker_requirements={"cellsim"}, - ), - }, - ), + # "test-session": Session( + # session_id="test-session", + # tests={ + # "tests/test_nothing.py::test_nothing": Session.Test( + # nodeid="tests/test_nothing.py::test_nothing", + # worker_requirements={"dev"}, + # ), + # "tests/test_nothing.py::test_fail": Session.Test( + # nodeid="tests/test_nothing.py::test_fail", + # worker_requirements={"dev"}, + # ), + # }, + # ), } @@ -121,7 +124,7 @@ async def _get_worker(worker_id: str) -> ConfiguredWorker: if len(workers_response.data) == 0: raise WorkerNotFound(worker_id) - tags = workers_response.data[0]["tags"] + tags = set(workers_response.data[0]["details"]["tags"]) if tags is None: raise WorkerUnconfigured(worker_id) @@ -143,7 +146,7 @@ async def _get_active_workers() -> list[ConfiguredWorker]: supabase.table("workers") .select("*") .gte("last_seen", datetime.now() - WORKER_TIMEOUT) - .neq("tags", None) + .neq("details->>tags", None) .execute() ) for worker in workers_response.data: @@ -151,7 +154,7 @@ async def _get_active_workers() -> list[ConfiguredWorker]: ConfiguredWorker( worker_id=worker["worker_id"], pet_name=worker["pet_name"], - tags=worker["tags"], + tags=set(worker["details"]["tags"]), last_seen=datetime.fromisoformat(worker["last_seen"]), ) ) @@ -218,9 +221,10 @@ async def submit_tests(session_id: str, request: SubmitTestsRequest) -> SuccessR unprocessable_tags = set() worker_tags = {tag for worker in workers for tag in worker.tags} for test in request.tests: - for tag in test.worker_requirements: - if tag not in worker_tags: - unprocessable_tags.add(tag) + for req in test.worker_requirements: + for tag in req.tags: + if tag not in worker_tags: + unprocessable_tags.add(tag) sessions[session_id].tests[test.nodeid] = Session.Test( test.worker_requirements, test.nodeid @@ -294,7 +298,7 @@ async def query_test_report( @app.get(path="/worker/{worker_id}/session") async def get_worker_session( - worker_id: str, background_tasks: BackgroundTasks + worker_id: str, background_tasks: BackgroundTasks, response: Response ) -> SessionResponse | NoSessionResponse: """Get the session for a worker, or if the worker isn't registered, register it.""" worker_exists = bool( @@ -317,18 +321,24 @@ async def get_worker_session( worker = await _get_worker(worker_id) for session in sessions.values(): - if session.state == SessionState.Running: + if session.state == SessionState.Setup: if any( - test.worker_requirements.issubset(worker.tags) + all( + set(req.tags).issubset(worker.tags) + for req in test.worker_requirements + ) for test in session.tests.values() ): + # TODO: lock session to worker + response.status_code = status.HTTP_200_OK return SessionResponse(session_id=session.session_id) + response.status_code = status.HTTP_204_NO_CONTENT return NoSessionResponse() -@app.get("/worker/session/{session_id}/env") -async def fetch_worker_session_env(session_id: str) -> Response: +@app.get("/worker/{worker_id}/session/{session_id}/env") +async def fetch_worker_session_env(worker_id: str, session_id: str) -> Response: """Get the environment for a session""" if session_id not in sessions: raise fastapi.HTTPException(status_code=404, detail="Session not found") @@ -368,8 +378,8 @@ async def fetch_session_tests( worker = await _get_worker(worker_id) worker_testable: list[str] = [] for test in sessions[session_id].tests.values(): - if test.status == TestStatus.Pending and test.worker_requirements.issubset( - worker.tags + if test.status == TestStatus.Pending and all( + set(req.tags).issubset(worker.tags) for req in test.worker_requirements ): worker_testable.append(test.nodeid) if len(worker_testable) == 1: diff --git a/software/httpdist_supervisor/main.go b/software/httpdist_supervisor/main.go index 041debb9..993610c2 100644 --- a/software/httpdist_supervisor/main.go +++ b/software/httpdist_supervisor/main.go @@ -1,13 +1,5 @@ package main -// set worker_id -// register worker with server -// start a wait loop -// poll for session -// when given a session: -// - pull env for session -// - spawn pytest in session env - import ( "fmt" "log" @@ -18,30 +10,34 @@ import ( const ( programName = "httpdist-supervisor" - ifaceName = "en0" apiUrl = "http://localhost:8000" ) +var ifaceNames = []string{"eth0", "en0", "wlan0"} + func getWorkerId() (string, error) { // get mac address interfaces, err := net.Interfaces() if err != nil { return "", fmt.Errorf("failed to get interfaces: %w", err) } + for _, iface := range interfaces { - if iface.Name == ifaceName { - macAddr := iface.HardwareAddr.String() - macAddrNoColons := "" - for _, c := range macAddr { - if c != ':' { - macAddrNoColons += string(c) + for _, ifaceName := range ifaceNames { + if iface.Name == ifaceName { + macAddr := iface.HardwareAddr.String() + macAddrNoColons := "" + for _, c := range macAddr { + if c != ':' { + macAddrNoColons += string(c) + } } + return macAddrNoColons, nil } - return macAddrNoColons, nil } } - return "", fmt.Errorf("interface not found: %s", ifaceName) + return "", fmt.Errorf("no matching interface found from: %v", ifaceNames) } func (c *ApiClient) registerWorker(workerId string) { diff --git a/software/httpdist_supervisor/session.go b/software/httpdist_supervisor/session.go index 6595f22a..89b41871 100644 --- a/software/httpdist_supervisor/session.go +++ b/software/httpdist_supervisor/session.go @@ -69,6 +69,10 @@ func (session *TestSession) spawnWorker() error { session.SessionId, ) + // TODO: to log file + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Dir = session.EnvDir start := time.Now() diff --git a/tests/conftest.py b/tests/conftest.py index d2e23137..389c27a0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -69,7 +69,7 @@ async def __aexit__(self, exc_type, exc_value, traceback): await self.aclose() -@pytest.fixture(scope="session") +@pytest.fixture(scope="function") async def hil(machine_config: ConfigDict): # Create HIL instance hil = await Hil.create(machine_config) diff --git a/tests/test_drivers/test_cell.py b/tests/test_drivers/test_cell.py index 5d5149f0..f7a1ec87 100644 --- a/tests/test_drivers/test_cell.py +++ b/tests/test_drivers/test_cell.py @@ -14,13 +14,13 @@ from ..conftest import Hil -@pytest.mark.runs_on(hostname="chunky-otter") +@pytest.mark.runs_on(tags=["chunky-otter"]) async def test_calibration(hil: "Hil"): async with hil: await asyncio.gather(*[cell.calibrate() for cell in hil.cellsim.cells]) -@pytest.mark.runs_on(hostname="chunky-otter") +@pytest.mark.runs_on(tags=["chunky-otter"]) async def test_performance(hil: "Hil"): async with hil: for cell in hil.cellsim.cells: @@ -43,7 +43,7 @@ async def test_performance(hil: "Hil"): await cell.disable() -@pytest.mark.runs_on(hostname="chunky-otter") +@pytest.mark.runs_on(tags=["chunky-otter"]) async def test_output_voltage(hil: "Hil", record: Recorder): """ Set output voltage (0.5- 4.3V, 0.1V steps) @@ -104,7 +104,7 @@ async def test_output_voltage(hil: "Hil", record: Recorder): table.finalize() -@pytest.mark.runs_on(hostname="chunky-otter") +@pytest.mark.runs_on(tags=["chunky-otter"]) async def test_buck_voltage(hil: "Hil", record: Recorder): """ Set Buck voltage (1.5 - 4.4V, 0.1V steps) @@ -167,7 +167,7 @@ async def _get_voltage(): table.finalize() -@pytest.mark.runs_on(hostname="chunky-otter") +@pytest.mark.runs_on(tags=["chunky-otter"]) async def test_mux(hil: "Hil"): async with hil: # Write binary to the mux for each cell From 26e0ee887b7907339e24368aa56614a7ceed9c6e Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Wed, 26 Feb 2025 18:19:35 -0800 Subject: [PATCH 68/71] api url from env --- software/hil/dist_plugin.py | 7 +++---- software/httpdist_supervisor/main.go | 5 +++-- software/httpdist_supervisor/util.go | 8 ++++++++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index 44511ab8..eba815c9 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -4,6 +4,7 @@ from enum import StrEnum, auto import itertools from os import PathLike +import os from pathlib import Path from typing import TypedDict import zipfile @@ -81,8 +82,7 @@ class NoWorkersAvailableError(ApiUsageError): class ClientApi: - # FIXME - API_URL = "http://localhost:8000" + API_URL = os.getenv("HTTPDIST_API_URL", "http://localhost:8000") session_id: SessionId | None = None def __init__(self, config: pytest.Config): @@ -190,8 +190,7 @@ async def download_artifact( class WorkerApi: - # FIXME - API_URL = "http://localhost:8000" + API_URL = os.getenv("HTTPDIST_API_URL", "http://localhost:8000") session_id: SessionId | None = None def __init__(self, config: pytest.Config): diff --git a/software/httpdist_supervisor/main.go b/software/httpdist_supervisor/main.go index 993610c2..8b930e42 100644 --- a/software/httpdist_supervisor/main.go +++ b/software/httpdist_supervisor/main.go @@ -9,10 +9,11 @@ import ( ) const ( - programName = "httpdist-supervisor" - apiUrl = "http://localhost:8000" + programName = "httpdist-supervisor" + defaultApiUrl = "http://localhost:8000" ) +var apiUrl = getEnvOrDefault("HTTPDIST_API_URL", defaultApiUrl) var ifaceNames = []string{"eth0", "en0", "wlan0"} func getWorkerId() (string, error) { diff --git a/software/httpdist_supervisor/util.go b/software/httpdist_supervisor/util.go index 19e14754..010fb35b 100644 --- a/software/httpdist_supervisor/util.go +++ b/software/httpdist_supervisor/util.go @@ -18,6 +18,14 @@ func clearSpinner() { fmt.Print("\r \r") } +func getEnvOrDefault(key, defaultValue string) string { + value := os.Getenv(key) + if value == "" { + return defaultValue + } + return value +} + func extractZip(zipPath, destPath string) error { reader, err := zip.OpenReader(zipPath) if err != nil { From 9b1bf17ae0f0935c3a4869978e1f6bc14ce7e923 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Wed, 26 Feb 2025 18:29:39 -0800 Subject: [PATCH 69/71] tag by capability --- tests/test_drivers/test_cell.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_drivers/test_cell.py b/tests/test_drivers/test_cell.py index f7a1ec87..93c2b1aa 100644 --- a/tests/test_drivers/test_cell.py +++ b/tests/test_drivers/test_cell.py @@ -14,13 +14,13 @@ from ..conftest import Hil -@pytest.mark.runs_on(tags=["chunky-otter"]) +@pytest.mark.runs_on(tags=["cellsim"]) async def test_calibration(hil: "Hil"): async with hil: await asyncio.gather(*[cell.calibrate() for cell in hil.cellsim.cells]) -@pytest.mark.runs_on(tags=["chunky-otter"]) +@pytest.mark.runs_on(tags=["cellsim"]) async def test_performance(hil: "Hil"): async with hil: for cell in hil.cellsim.cells: @@ -43,7 +43,7 @@ async def test_performance(hil: "Hil"): await cell.disable() -@pytest.mark.runs_on(tags=["chunky-otter"]) +@pytest.mark.runs_on(tags=["cellsim"]) async def test_output_voltage(hil: "Hil", record: Recorder): """ Set output voltage (0.5- 4.3V, 0.1V steps) @@ -104,7 +104,7 @@ async def test_output_voltage(hil: "Hil", record: Recorder): table.finalize() -@pytest.mark.runs_on(tags=["chunky-otter"]) +@pytest.mark.runs_on(tags=["cellsim"]) async def test_buck_voltage(hil: "Hil", record: Recorder): """ Set Buck voltage (1.5 - 4.4V, 0.1V steps) @@ -167,7 +167,7 @@ async def _get_voltage(): table.finalize() -@pytest.mark.runs_on(tags=["chunky-otter"]) +@pytest.mark.runs_on(tags=["cellsim"]) async def test_mux(hil: "Hil"): async with hil: # Write binary to the mux for each cell From bcdc995d04e36c102e9e96ed9a3695e746eed3d1 Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Thu, 27 Feb 2025 12:39:24 -0800 Subject: [PATCH 70/71] Move worker seen into sequence of worker//session instead of specific registration --- software/httpdist_server/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/software/httpdist_server/server.py b/software/httpdist_server/server.py index 35b51bef..8744aac2 100644 --- a/software/httpdist_server/server.py +++ b/software/httpdist_server/server.py @@ -298,7 +298,7 @@ async def query_test_report( @app.get(path="/worker/{worker_id}/session") async def get_worker_session( - worker_id: str, background_tasks: BackgroundTasks, response: Response + worker_id: str, response: Response ) -> SessionResponse | NoSessionResponse: """Get the session for a worker, or if the worker isn't registered, register it.""" worker_exists = bool( From 8e73cc64ef48850bb4738a051a7a3ed75ec094af Mon Sep 17 00:00:00 2001 From: Matt Wildoer Date: Wed, 16 Apr 2025 15:52:43 -0700 Subject: [PATCH 71/71] Update dist plugin to match backend --- software/hil/dist_plugin.py | 90 +++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 49 deletions(-) diff --git a/software/hil/dist_plugin.py b/software/hil/dist_plugin.py index eba815c9..13209794 100644 --- a/software/hil/dist_plugin.py +++ b/software/hil/dist_plugin.py @@ -2,6 +2,7 @@ import base64 from dataclasses import dataclass from enum import StrEnum, auto +from io import BytesIO import itertools from os import PathLike import os @@ -114,31 +115,15 @@ async def _get_raw(self, path: str, params: dict | None = None): response.raise_for_status() return response - async def get_client_session(self) -> SessionId: - session = await self._get("session") - session_id = session["session_id"] - self.session_id = session_id - return session_id - - async def submit_env(self, env: Path): - if self.session_id is None: - raise SessionNotStartedError("Must have an active session") - - await self._post_files( - f"session/{self.session_id}/env", - env, - ) - - async def submit_tests(self, tests: list[TestSpec]): - if self.session_id is None: - raise SessionNotStartedError("Must have an active session") - + async def create_session(self, tests: list[TestSpec], env: str): try: - await self._post(f"session/{self.session_id}/tests", {"tests": tests}) + response = await self._post("session/create", {"tests": tests, "env": env}) except httpx.HTTPStatusError as e: if e.response.status_code == 503: raise NoWorkersAvailableError(e.response.json()["detail"]) raise + self.session_id = response["session_id"] + return self.session_id async def fetch_statuses( self, @@ -219,14 +204,18 @@ def fetch_work(self, worker_id: WorkerId) -> tuple[NodeId, NodeId | None]: return data["test_now"], data["test_next"] def report_result( - self, nodeid: NodeId, report: pytest.TestReport, phase: TestPhase + self, + worker_id: WorkerId, + nodeid: NodeId, + report: pytest.TestReport, + phase: TestPhase, ): data = { "nodeid": nodeid, "report": base64.b64encode(cloudpickle.dumps(report)).decode(), "phase": phase, } - self._post(f"worker/session/{self.session_id}/test", data) + self._post(f"worker/{worker_id}/session/{self.session_id}/test", data) def upload_artifacts(self, worker_id: str): """Upload all artifact files""" @@ -311,7 +300,7 @@ def pytest_runtest_logreport(self, report: pytest.TestReport) -> None: # This hook is called from within the runtestloop, so we need to run # it in a task instead of calling it directly self.api_client.report_result( - report.nodeid, report, phase=TestPhase(report.when) + self.worker_id, report.nodeid, report, phase=TestPhase(report.when) ) @pytest.hookimpl @@ -369,12 +358,15 @@ def __init__(self, config: pytest.Config): self.api_client = ClientApi(config) self.statuses = {} - async def submit_env(self, env: Path): - # Create a pathspec to exclude certain files + def _zip_env(self, env: Path) -> str: + """Zip up the environment as a base64 encoded byte-string.""" + # FIXME: this will ignore all but the top-level `.git/hilignore` + # TODO: this would be way faster if it filtered as it iterated, but + # pathspec iters all the files, and then filters things that don't match + + # Create a pathspec to exclude certain files # Always ignore .git/ to avoid including the entire repo in the env - # FIXME: this would be way faster if it filtered as it iterated, but - # pathspec iters all the files, and then filters things that don't match ignore_pattern_lines = [".git/"] for ignore_file in itertools.chain( env.glob("*.gitignore"), env.glob("*.hilignore") @@ -386,19 +378,21 @@ async def submit_env(self, env: Path): ignore_spec = pathspec.GitIgnoreSpec.from_lines(ignore_pattern_lines) matched_files = list(ignore_spec.match_tree_files(env, negate=True)) - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) - zip_path = temp_path / "env.zip" - with zipfile.ZipFile(zip_path, "w") as zip_file: - for file in rich.progress.track( - matched_files, description="zipping env..." - ): - zip_file.write(env / file, file) - - size_mb = zip_path.stat().st_size / 1024 / 1024 + bytes_io = BytesIO() + + with zipfile.ZipFile( + bytes_io, "w", compression=zipfile.ZIP_BZIP2, compresslevel=9 + ) as zip_file: + for file in rich.progress.track( + matched_files, description="zipping env..." + ): + zip_file.write(env / file, file) + + size_mb = bytes_io.tell() / 1024 / 1024 if size_mb > 5: rich.print( - f"[yellow]WARNING:[/yellow] Large env size: {size_mb:.1f}MB. Consider adding more to [blue].hilignore[/blue]." + f"[yellow]WARNING:[/yellow] Large env size: {size_mb:.1f}MB. " + "Consider adding more to [blue].hilignore[/blue]." ) largest_files = sorted( matched_files, @@ -409,17 +403,17 @@ async def submit_env(self, env: Path): f"Largest files: {', '.join(str(file) for file in largest_files)}" ) - await self.api_client.submit_env(zip_path) + return base64.b64encode(bytes_io.getvalue()).decode() - async def submit_tests(self, session: pytest.Session): + async def create_session(self, session: pytest.Session): nodeids = {item.nodeid for item in session.items} runs_on = session.config.stash[self.runs_on_key] - await self.api_client.submit_tests( - [ - TestSpec(nodeid=nodeid, worker_requirements=runs_on.get(nodeid)) - for nodeid in nodeids - ] - ) + test = [ + TestSpec(nodeid=nodeid, worker_requirements=runs_on.get(nodeid)) + for nodeid in nodeids + ] + env_zip = self._zip_env(session.config.rootpath) + await self.api_client.create_session(test, env_zip) async def fetch_results(self) -> list[pytest.TestReport]: new_statuses: dict[ @@ -478,9 +472,7 @@ def pytest_runtestloop(self, session: pytest.Session): self.results = TestResults(set(item.nodeid for item in session.items)) async def run(): - await self.api_client.get_client_session() - await self.submit_env(session.config.rootpath) - await self.submit_tests(session) + await self.create_session(session) while True: new_reports = await self.fetch_results()