diff --git a/pyproject.toml b/pyproject.toml index de4aaf2cd..d8030857a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,6 @@ requires-python = ">=3.10" "vulture==2.14", "pytest~=8.4", "pytest-cov~=7.0", - "pytest-asyncio~=1.2", "import-linter~=2.5", "pytest-deadfixtures~=2.2", "taplo~=0.9.3", diff --git a/src/cloudai/_core/base_runner.py b/src/cloudai/_core/base_runner.py index a08814f13..10f6bc01c 100644 --- a/src/cloudai/_core/base_runner.py +++ b/src/cloudai/_core/base_runner.py @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,10 +14,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -import asyncio import logging +import time from abc import ABC, abstractmethod -from asyncio import Task from pathlib import Path from typing import Dict, List @@ -71,33 +70,33 @@ def __init__(self, mode: str, system: System, test_scenario: TestScenario, outpu logging.debug(f"{self.__class__.__name__} initialized") self.shutting_down = False - async def shutdown(self): + def shutdown(self): """Gracefully shut down the runner, terminating all outstanding jobs.""" self.shutting_down = True logging.info("Terminating all jobs...") for job in self.jobs: logging.info(f"Terminating job {job.id} for test {job.test_run.name}") self.system.kill(job) - logging.info("All jobs have been killed.") + logging.info("Waiting for all jobs to be killed.") - async def run(self): - """Asynchronously run the test scenario.""" + def run(self): + """Run the test scenario.""" if self.shutting_down: return total_tests = len(self.test_scenario.test_runs) dependency_free_trs = self.find_dependency_free_tests() for tr in dependency_free_trs: - await self.submit_test(tr) + self.submit_test(tr) logging.debug(f"Total tests: {total_tests}, dependency free tests: {[tr.name for tr in dependency_free_trs]}") while self.jobs: - await self.check_start_post_init_dependencies() - await self.monitor_jobs() + self.check_start_post_init_dependencies() + self.monitor_jobs() logging.debug(f"sleeping for {self.monitor_interval} seconds") - await asyncio.sleep(self.monitor_interval) + time.sleep(self.monitor_interval) - async def submit_test(self, tr: TestRun): + def submit_test(self, tr: TestRun): """ Start a dependency-free test. @@ -118,7 +117,7 @@ async def submit_test(self, tr: TestRun): def on_job_submit(self, tr: TestRun) -> None: return - async def delayed_submit_test(self, tr: TestRun, delay: int = 5): + def delayed_submit_test(self, tr: TestRun, delay: int = 5): """ Delay the start of a test based on start_post_comp dependency. @@ -127,8 +126,8 @@ async def delayed_submit_test(self, tr: TestRun, delay: int = 5): delay (int): Delay in seconds before starting the test. """ logging.debug(f"Delayed start for test {tr.name} by {delay} seconds.") - await asyncio.sleep(delay) - await self.submit_test(tr) + time.sleep(delay) + self.submit_test(tr) @abstractmethod def _submit_test(self, tr: TestRun) -> BaseJob: @@ -143,7 +142,7 @@ def _submit_test(self, tr: TestRun) -> BaseJob: """ pass - async def check_start_post_init_dependencies(self): + def check_start_post_init_dependencies(self): """ Check and handle start_post_init dependencies. @@ -164,9 +163,9 @@ async def check_start_post_init_dependencies(self): logging.debug(f"start_post_init for test {tr.name} ({is_running=}, {is_completed=}, {self.mode=})") if is_running or is_completed: - await self.check_and_schedule_start_post_init_dependent_tests(tr) + self.check_and_schedule_start_post_init_dependent_tests(tr) - async def check_and_schedule_start_post_init_dependent_tests(self, started_test_run: TestRun): + def check_and_schedule_start_post_init_dependent_tests(self, started_test_run: TestRun): """ Schedule tests with a start_post_init dependency on the provided started_test. @@ -177,7 +176,7 @@ async def check_and_schedule_start_post_init_dependent_tests(self, started_test_ if tr not in self.testrun_to_job_map: for dep_type, dep in tr.dependencies.items(): if (dep_type == "start_post_init") and (dep.test_run == started_test_run): - await self.delayed_submit_test(tr) + self.delayed_submit_test(tr) def find_dependency_free_tests(self) -> List[TestRun]: """ @@ -229,7 +228,7 @@ def get_job_output_path(self, tr: TestRun) -> Path: return job_output_path - async def monitor_jobs(self) -> int: + def monitor_jobs(self) -> int: """ Monitor the status of jobs, handle end_post_comp dependencies, and schedule start_post_comp dependent jobs. @@ -248,20 +247,20 @@ async def monitor_jobs(self) -> int: if self.mode == "dry-run": successful_jobs_count += 1 - await self.handle_job_completion(job) + self.handle_job_completion(job) else: if self.test_scenario.job_status_check: job_status_result = self.get_job_status(job) if job_status_result.is_successful: successful_jobs_count += 1 - await self.handle_job_completion(job) + self.handle_job_completion(job) else: error_message = ( f"Job {job.id} for test {job.test_run.name} failed: {job_status_result.error_message}" ) logging.error(error_message) - await self.handle_job_completion(job) - await self.shutdown() + self.handle_job_completion(job) + self.shutdown() raise JobFailureError(job.test_run.name, error_message, job_status_result.error_message) else: job_status_result = self.get_job_status(job) @@ -271,7 +270,7 @@ async def monitor_jobs(self) -> int: ) logging.error(error_message) successful_jobs_count += 1 - await self.handle_job_completion(job) + self.handle_job_completion(job) return successful_jobs_count @@ -296,7 +295,7 @@ def get_job_status(self, job: BaseJob) -> JobStatusResult: return workload_run_results return JobStatusResult(is_successful=True) - async def handle_job_completion(self, completed_job: BaseJob): + def handle_job_completion(self, completed_job: BaseJob): """ Handle the completion of a job, including dependency management and iteration control. @@ -316,9 +315,9 @@ async def handle_job_completion(self, completed_job: BaseJob): completed_job.test_run.current_iteration += 1 msg = f"Re-running job for iteration {completed_job.test_run.current_iteration}" logging.info(msg) - await self.submit_test(completed_job.test_run) + self.submit_test(completed_job.test_run) else: - await self.handle_dependencies(completed_job) + self.handle_dependencies(completed_job) def on_job_completion(self, job: BaseJob) -> None: """ @@ -332,37 +331,27 @@ def on_job_completion(self, job: BaseJob) -> None: """ return - async def handle_dependencies(self, completed_job: BaseJob) -> List[Task]: + def handle_dependencies(self, completed_job: BaseJob): """ Handle the start_post_comp and end_post_comp dependencies for a completed job. Args: completed_job (BaseJob): The job that has just been completed. - - Returns: - List[asyncio.Task]: A list of asyncio.Task objects created for handling the dependencies. """ - tasks = [] - # Handling start_post_comp dependencies for tr in self.test_scenario.test_runs: if tr not in self.testrun_to_job_map: for dep_type, dep in tr.dependencies.items(): if dep_type == "start_post_comp" and dep.test_run == completed_job.test_run: - task = await self.delayed_submit_test(tr) - if task: - tasks.append(task) + self.delayed_submit_test(tr) # Handling end_post_comp dependencies for test, dependent_job in self.testrun_to_job_map.items(): for dep_type, dep in test.dependencies.items(): if dep_type == "end_post_comp" and dep.test_run == completed_job.test_run: - task = await self.delayed_kill_job(dependent_job) - tasks.append(task) - - return tasks + self.delayed_kill_job(dependent_job) - async def delayed_kill_job(self, job: BaseJob, delay: int = 0): + def delayed_kill_job(self, job: BaseJob, delay: int = 0): """ Schedule termination of a Standalone job after a specified delay. @@ -371,7 +360,7 @@ async def delayed_kill_job(self, job: BaseJob, delay: int = 0): delay (int): Delay in seconds after which the job should be terminated. """ logging.info(f"Scheduling termination of job {job.id} after {delay} seconds.") - await asyncio.sleep(delay) + time.sleep(delay) job.terminated_by_dependency = True self.system.kill(job) diff --git a/src/cloudai/_core/runner.py b/src/cloudai/_core/runner.py index 79c9bf605..b6647a895 100644 --- a/src/cloudai/_core/runner.py +++ b/src/cloudai/_core/runner.py @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,7 +14,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import asyncio import datetime import logging from types import FrameType @@ -80,39 +79,18 @@ def create_runner(self, mode: str, system: System, test_scenario: TestScenario) return runner_class(mode, system, test_scenario, results_root) - async def run(self): + def run(self): """Run the test scenario using the instantiated runner.""" try: - await self.runner.run() + self.runner.run() logging.debug("All jobs finished successfully.") - except asyncio.CancelledError: - logging.info("Runner cancelled, performing cleanup...") - await self.runner.shutdown() - return except JobFailureError as exc: logging.debug(f"Runner failed JobFailure exception: {exc}", exc_info=True) - def _cancel_all(self): - # the below code might look excessive, this is to address https://docs.astral.sh/ruff/rules/asyncio-dangling-task/ - shutdown_task = asyncio.create_task(self.runner.shutdown()) - tasks = {shutdown_task} - shutdown_task.add_done_callback(tasks.discard) - - for task in asyncio.all_tasks(): - if task == shutdown_task: - continue - - logging.debug(f"Cancelling task: {task}") - try: - task.cancel() - except asyncio.CancelledError as exc: - logging.debug(f"Error cancelling task: {task}, {exc}", exc_info=True) - pass - def cancel_on_signal( self, signum: int, frame: Optional[FrameType], # noqa: Vulture ): logging.info(f"Signal {signum} received, shutting down...") - asyncio.get_running_loop().call_soon_threadsafe(self._cancel_all) + self.runner.shutdown() diff --git a/src/cloudai/cli/handlers.py b/src/cloudai/cli/handlers.py index d474ff421..c5531698f 100644 --- a/src/cloudai/cli/handlers.py +++ b/src/cloudai/cli/handlers.py @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,7 +15,6 @@ # limitations under the License. import argparse -import asyncio import copy import logging import signal @@ -192,7 +191,7 @@ def generate_reports(system: System, test_scenario: TestScenario, result_dir: Pa def handle_non_dse_job(runner: Runner, args: argparse.Namespace) -> None: - asyncio.run(runner.run()) + runner.run() generate_reports(runner.runner.system, runner.runner.test_scenario, runner.runner.scenario_root) logging.info("All jobs are complete.") diff --git a/src/cloudai/configurator/cloudai_gym.py b/src/cloudai/configurator/cloudai_gym.py index 1be1c0f76..b5495b891 100644 --- a/src/cloudai/configurator/cloudai_gym.py +++ b/src/cloudai/configurator/cloudai_gym.py @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,7 +14,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import asyncio import copy import csv import logging @@ -113,7 +112,7 @@ def step(self, action: Any) -> Tuple[list, float, bool, dict]: self.runner.testrun_to_job_map.clear() try: - asyncio.run(self.runner.run()) + self.runner.run() except Exception as e: logging.error(f"Error running step {self.test_run.step}: {e}") diff --git a/src/cloudai/systems/runai/runai_rest_client.py b/src/cloudai/systems/runai/runai_rest_client.py index 7188d3b52..8186dec91 100644 --- a/src/cloudai/systems/runai/runai_rest_client.py +++ b/src/cloudai/systems/runai/runai_rest_client.py @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,7 +20,7 @@ from typing import Any, Dict, Optional import requests -import websockets +from websockets.sync.client import connect as ws_connect class RunAIRestClient: @@ -496,7 +496,7 @@ def is_cluster_api_available(self, cluster_domain: str) -> bool: response = requests.get(url, headers=headers) return "OK" in response.text - async def fetch_training_logs( + def fetch_training_logs( self, cluster_domain: str, project_name: str, training_task_name: str, output_file_path: Path ): if not self.is_cluster_api_available(cluster_domain): @@ -512,9 +512,11 @@ async def fetch_training_logs( } ssl_context = ssl._create_unverified_context() - async with websockets.connect(url, extra_headers=headers, ssl=ssl_context) as websocket: - with output_file_path.open("w") as log_file: - async for message in websocket: - if isinstance(message, bytes): - message = message.decode("utf-8") - log_file.write(str(message)) + with ( + ws_connect(url, additional_headers=headers, ssl=ssl_context) as websocket, + output_file_path.open("w") as log_file, + ): + for message in websocket: + if isinstance(message, bytes): + message = message.decode("utf-8") + log_file.write(str(message)) diff --git a/src/cloudai/systems/runai/runai_runner.py b/src/cloudai/systems/runai/runai_runner.py index 7263a1064..d8f0701e2 100644 --- a/src/cloudai/systems/runai/runai_runner.py +++ b/src/cloudai/systems/runai/runai_runner.py @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -41,12 +41,12 @@ def _submit_test(self, tr: TestRun) -> RunAIJob: else: raise RuntimeError("Invalid mode for submitting a test.") - async def job_completion_callback(self, job: BaseJob) -> None: + def on_job_completion(self, job: BaseJob) -> None: runai_system = cast(RunAISystem, self.system) - job = cast(RunAIJob, job) - workload_id = str(job.id) - runai_system.get_workload_events(workload_id, job.test_run.output_path / "events.txt") - await runai_system.store_logs(workload_id, job.test_run.output_path / "stdout.txt") + runai_job = cast(RunAIJob, job) + workload_id = str(runai_job.id) + runai_system.get_workload_events(workload_id, runai_job.test_run.output_path / "events.txt") + runai_system.store_logs(workload_id, runai_job.test_run.output_path / "stdout.txt") def kill_job(self, job: BaseJob) -> None: runai_system = cast(RunAISystem, self.system) diff --git a/src/cloudai/systems/runai/runai_system.py b/src/cloudai/systems/runai/runai_system.py index 05e5f0135..2cd80f050 100644 --- a/src/cloudai/systems/runai/runai_system.py +++ b/src/cloudai/systems/runai/runai_system.py @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -174,7 +174,7 @@ def resume_training(self, workload_id: str) -> None: self.api_client.resume_training(workload_id) # ============================ Logs ============================ - async def store_logs(self, workload_id: str, output_file_path: Path): + def store_logs(self, workload_id: str, output_file_path: Path): """Store logs for a given workload.""" training_data = self.api_client.get_training(workload_id) training = RunAITraining(**training_data) @@ -202,4 +202,4 @@ async def store_logs(self, workload_id: str, output_file_path: Path): logging.error(f"Domain for cluster {cluster_id} not found.") return - await self.api_client.fetch_training_logs(cluster_domain, project.name, training.name, output_file_path) + self.api_client.fetch_training_logs(cluster_domain, project.name, training.name, output_file_path) diff --git a/src/cloudai/systems/slurm/single_sbatch_runner.py b/src/cloudai/systems/slurm/single_sbatch_runner.py index 51b922fe0..7bb563e26 100644 --- a/src/cloudai/systems/slurm/single_sbatch_runner.py +++ b/src/cloudai/systems/slurm/single_sbatch_runner.py @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,9 +14,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -import asyncio import copy import logging +import time from datetime import timedelta from pathlib import Path from typing import Generator, Optional, cast @@ -180,7 +180,7 @@ def all_trs(self) -> Generator[TestRun, None, None]: tr.output_path = self.get_job_output_path(tr) yield tr - async def run(self): + def run(self): if self.shutting_down: return @@ -193,7 +193,7 @@ async def run(self): if self.shutting_down: break is_completed = True if self.mode == "dry-run" else self.system.is_job_completed(job) - await asyncio.sleep(self.system.monitor_interval) + time.sleep(self.system.monitor_interval) self.handle_dse() diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index df3bbc06b..a07a87365 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -108,7 +108,7 @@ def do_dry_run(self, tmp_path_factory: pytest.TempPathFactory, request: pytest.F log_file="debug.log", ) with ( - patch("asyncio.sleep", return_value=None), + patch("time.sleep", return_value=None), patch("cloudai.systems.slurm.SlurmSystem.is_job_completed", return_value=True), patch("cloudai.systems.slurm.SlurmSystem.is_job_running", return_value=True), patch("cloudai.util.command_shell.CommandShell.execute") as mock_execute, diff --git a/tests/test_base_runner.py b/tests/test_base_runner.py index 07fbb8d98..6598dc87c 100644 --- a/tests/test_base_runner.py +++ b/tests/test_base_runner.py @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,7 +14,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import asyncio from copy import deepcopy from pathlib import Path from typing import cast @@ -50,12 +49,11 @@ def _submit_test(self, tr: TestRun) -> BaseJob: self.submitted_trs.append(tr) return BaseJob(tr, 0) - async def delayed_submit_test(self, tr: TestRun, delay: int = 0): - await super().delayed_submit_test(tr, 0) + def delayed_submit_test(self, tr: TestRun, delay: int = 0): + super().delayed_submit_test(tr, 0) - async def delayed_kill_job(self, job: BaseJob, delay: int = 0): + def delayed_kill_job(self, job: BaseJob, delay: int = 0): self.killed_by_dependency.append(job) - await asyncio.sleep(0) class MyWorkload(TestDefinition): @@ -127,40 +125,37 @@ class TestHandleDependencies: def tr_main(self, runner: MyRunner) -> TestRun: return runner.test_scenario.test_runs[0] - @pytest.mark.asyncio - async def test_no_dependencies(self, runner: MyRunner, tr_main: TestRun): - await runner.handle_dependencies(BaseJob(tr_main, 0)) + def test_no_dependencies(self, runner: MyRunner, tr_main: TestRun): + runner.handle_dependencies(BaseJob(tr_main, 0)) assert len(runner.submitted_trs) == 0 - @pytest.mark.asyncio - async def test_start_post_comp(self, runner: MyRunner, tr_main: TestRun): + def test_start_post_comp(self, runner: MyRunner, tr_main: TestRun): tr_dep = deepcopy(tr_main) tr_dep.dependencies = {"start_post_comp": TestDependency(tr_main)} runner.test_scenario.test_runs.append(tr_dep) - await runner.handle_dependencies(BaseJob(tr_dep, 0)) # self, should not trigger anything + runner.handle_dependencies(BaseJob(tr_dep, 0)) # self, should not trigger anything assert len(runner.submitted_trs) == 0 - await runner.handle_dependencies(BaseJob(tr_main, 0)) + runner.handle_dependencies(BaseJob(tr_main, 0)) assert len(runner.submitted_trs) == 1 assert runner.submitted_trs[0] == tr_dep - @pytest.mark.asyncio - async def test_end_post_comp(self, runner: MyRunner, tr_main: TestRun): + def test_end_post_comp(self, runner: MyRunner, tr_main: TestRun): tr_dep = deepcopy(tr_main) tr_dep.dependencies = {"end_post_comp": TestDependency(tr_main)} runner.test_scenario.test_runs.append(tr_dep) # self not running, main completed -> nothing to kill - await runner.handle_dependencies(BaseJob(tr_main, 0)) + runner.handle_dependencies(BaseJob(tr_main, 0)) assert len(runner.killed_by_dependency) == 0 # self is running, main completed -> should kill - await runner.submit_test(tr_dep) + runner.submit_test(tr_dep) - await runner.handle_dependencies(BaseJob(tr_dep, 0)) # self, should not kill + runner.handle_dependencies(BaseJob(tr_dep, 0)) # self, should not kill assert len(runner.killed_by_dependency) == 0 - await runner.handle_dependencies(BaseJob(tr_main, 0)) + runner.handle_dependencies(BaseJob(tr_main, 0)) assert len(runner.killed_by_dependency) == 1 assert runner.killed_by_dependency[0].test_run == tr_dep diff --git a/uv.lock b/uv.lock index 22a38320e..b9f463120 100644 --- a/uv.lock +++ b/uv.lock @@ -106,15 +106,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, ] -[[package]] -name = "backports-asyncio-runner" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, -] - [[package]] name = "beautifulsoup4" version = "4.14.3" @@ -310,7 +301,6 @@ dev = [ { name = "pandas-stubs" }, { name = "pyright" }, { name = "pytest" }, - { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "pytest-deadfixtures" }, { name = "ruff" }, @@ -350,7 +340,6 @@ requires-dist = [ { name = "pydantic", specifier = "~=2.11.10" }, { name = "pyright", marker = "extra == 'dev'", specifier = "~=1.1" }, { name = "pytest", marker = "extra == 'dev'", specifier = "~=8.4" }, - { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = "~=1.2" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = "~=7.0" }, { name = "pytest-deadfixtures", marker = "extra == 'dev'", specifier = "~=2.2" }, { name = "rich", specifier = "~=14.2" }, @@ -2025,20 +2014,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, ] -[[package]] -name = "pytest-asyncio" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, - { name = "pytest" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, -] - [[package]] name = "pytest-cov" version = "7.0.0"