From 1de84d13f3b5358cc342fb2f3f655a4bbbf750df Mon Sep 17 00:00:00 2001 From: Carlos Bermudez Porto Date: Fri, 10 Jul 2026 12:14:35 -0400 Subject: [PATCH 1/4] feat: add client restart retries and orphan container cleanup options --- src/expb/execute_scenario.py | 15 ++ src/expb/execute_scenarios.py | 15 ++ src/expb/payloads/executor/executor.py | 358 +++++++++++++++++-------- 3 files changed, 269 insertions(+), 119 deletions(-) diff --git a/src/expb/execute_scenario.py b/src/expb/execute_scenario.py index b5929b8..6f9712b 100644 --- a/src/expb/execute_scenario.py +++ b/src/expb/execute_scenario.py @@ -83,6 +83,19 @@ def execute_scenario( help="Enable JetBrains dotTrace profiling. Auto-installs if needed. Snapshot saved to outputs directory.", ), ] = False, + client_restart_retries: Annotated[ + int, + typer.Option( + help="Auto-restart the execution client on failure up to N times (0 = never restart). Infrastructure containers (K6, Alloy, payload server) never restart.", + ), + ] = 0, + reap_orphans: Annotated[ + bool, + typer.Option( + "--reap-orphans/--no-reap-orphans", + help="Before running, force-remove leftover containers/networks from prior runs of this same scenario (e.g. orphaned by a hard-killed run). Scoped to the scenario, so it won't disturb other runs on the machine. Off by default.", + ), + ] = False, use_lock: Annotated[ bool, typer.Option( @@ -150,6 +163,8 @@ def execute_scenario( client_metrics=client_metrics, stable_cpu=stable_cpu, dottrace=dottrace, + client_restart_retries=client_restart_retries, + reap_orphans=reap_orphans, ), ) except ExecutionLockError as e: diff --git a/src/expb/execute_scenarios.py b/src/expb/execute_scenarios.py index d112e97..e62eeef 100644 --- a/src/expb/execute_scenarios.py +++ b/src/expb/execute_scenarios.py @@ -89,6 +89,19 @@ def execute_scenarios( help="Enable JetBrains dotTrace profiling. Auto-installs if needed. Snapshot saved to outputs directory.", ), ] = False, + client_restart_retries: Annotated[ + int, + typer.Option( + help="Auto-restart the execution client on failure up to N times (0 = never restart). Infrastructure containers (K6, Alloy, payload server) never restart.", + ), + ] = 0, + reap_orphans: Annotated[ + bool, + typer.Option( + "--reap-orphans/--no-reap-orphans", + help="Before running, force-remove leftover containers/networks from prior runs of this same scenario (e.g. orphaned by a hard-killed run). Scoped to the scenario, so it won't disturb other runs on the machine. Off by default.", + ), + ] = False, use_lock: Annotated[ bool, typer.Option( @@ -186,6 +199,8 @@ def execute_scenarios( client_metrics=client_metrics, stable_cpu=stable_cpu, dottrace=dottrace, + client_restart_retries=client_restart_retries, + reap_orphans=reap_orphans, ), ) if not loop: diff --git a/src/expb/payloads/executor/executor.py b/src/expb/payloads/executor/executor.py index 8528879..b19b93d 100644 --- a/src/expb/payloads/executor/executor.py +++ b/src/expb/payloads/executor/executor.py @@ -6,8 +6,11 @@ import signal import subprocess import time +from collections.abc import Callable from concurrent.futures import Future, ThreadPoolExecutor from pathlib import Path +from types import FrameType +from typing import Any import docker import docker.errors @@ -38,6 +41,17 @@ r'EXPB_PER_PAYLOAD_METRIC idx=(?P\d+) gas_used=(?P[^"\s]+) processing_ms=(?P[^"\s]+)' ) +# Marker label applied to every container and network expb creates, so orphans +# left behind by a hard-killed run can be found and force-removed regardless of +# their (deterministic) names. +EXPB_LABEL = "expb" +NO_RESTART_POLICY = {"Name": "no"} + +# Matches the type signal.getsignal() returns / signal.signal() accepts. +SignalHandler = ( + Callable[[int, FrameType | None], Any] | int | signal.Handlers | None +) + class ExecutorExecuteOptions: def __init__( @@ -51,6 +65,8 @@ def __init__( client_metrics: bool = True, stable_cpu: bool = True, dottrace: bool = False, + client_restart_retries: int = 0, + reap_orphans: bool = False, ): self.collect_per_payload_metrics: bool = collect_per_payload_metrics self.print_logs_to_console: bool = print_logs_to_console @@ -61,6 +77,8 @@ def __init__( self.client_metrics: bool = client_metrics self.stable_cpu: bool = stable_cpu self.dottrace: bool = dottrace + self.client_restart_retries: int = client_restart_retries + self.reap_orphans: bool = reap_orphans class Executor: @@ -323,12 +341,19 @@ def _ensure_dottrace_installed(self) -> str: return path + def _container_labels(self) -> dict[str, str]: + return { + EXPB_LABEL: "true", + f"{EXPB_LABEL}.scenario": self.config.executor_name, + } + def start_execution_client( self, container_network: Network | None = None, pyroscope: Pyroscope | None = None, stop_signal: str | None = None, dottrace: bool = False, + restart_retries: int = 0, ) -> Container: # Command execution_container_command = self.config.get_execution_client_command() @@ -393,18 +418,24 @@ def start_execution_client( ) # Run execution container + restart_policy = ( + {"Name": "on-failure", "MaximumRetryCount": restart_retries} + if restart_retries > 0 + else NO_RESTART_POLICY + ) cpu_count = self.config.resources.cpu if self.config.resources else None mem_limit = self.config.resources.mem if self.config.resources else None run_kwargs = dict( image=self.config.execution_client_image, name=self.config.get_execution_client_container_name(), + labels=self._container_labels(), volumes=execution_container_volumes, ports=execution_container_ports, command=execution_container_command, environment=execution_container_environment, network=container_network.name if container_network else None, detach=True, - restart_policy={"Name": "unless-stopped"}, + restart_policy=restart_policy, cpu_count=cpu_count, # Only works for windows nano_cpus=cpu_count * 10**9 if cpu_count else None, mem_limit=mem_limit, @@ -513,11 +544,12 @@ def start_alloy( run_kwargs = dict( image=self.config.get_alloy_container_image(), name=self.config.get_alloy_container_name(), + labels=self._container_labels(), volumes=self.config.get_alloy_volumes(), ports=self.config.get_alloy_ports(), command=self.config.get_alloy_command(), detach=True, - restart_policy={"Name": "unless-stopped"}, + restart_policy=NO_RESTART_POLICY, network=container_network.name if container_network else None, ) if self.config.resources and self.config.resources.infra_cpuset is not None: @@ -718,6 +750,7 @@ def start_payload_server( run_kwargs = dict( image=self.config.get_payload_server_container_image(), name=self.config.get_payload_server_container_name(), + labels=self._container_labels(), volumes=self.config.get_payload_server_volumes( drop_caches=drop_caches, evm_warmup=evm_warmup, @@ -731,7 +764,7 @@ def start_payload_server( ), command=self.config.get_payload_server_command(), detach=True, - restart_policy={"Name": "unless-stopped"}, + restart_policy=NO_RESTART_POLICY, network=container_network.name if container_network else None, ) if self.config.resources and self.config.resources.infra_cpuset is not None: @@ -813,12 +846,13 @@ def run_k6( run_kwargs = dict( image=self.config.get_k6_container_image(), name=self.config.get_k6_container_name(), + labels=self._container_labels(), volumes=k6_container_volumes, environment=k6_container_environment, command=k6_container_command, network=container_network.name if container_network else None, detach=False, - restart_policy={"Name": "unless-stopped"}, + restart_policy=NO_RESTART_POLICY, user=self.config.docker_user, group_add=self.config.docker_group_add, stop_signal="SIGINT", @@ -973,6 +1007,122 @@ def remove_directories(self) -> None: self.log.error("Failed to delete snapshot", error=e) raise e + def _teardown_container( + self, + name: str, + log_file: Path | None = None, + stop_timeout: int = 3, + print_console: bool = False, + console_filter=None, + line_callback=None, + ) -> list[dict] | None: + """Stop, capture logs from, and force-remove a container by name. + + Never raises: each step is isolated so a failure tearing down one + container can't leave later containers in the cleanup sequence orphaned. + Returns the container's mounts (for volume cleanup) or None. + """ + try: + container = self.config.docker_client.containers.get(name) + except docker.errors.NotFound: + return None + except Exception as e: + self.log.error("Failed to get container", container=name, error=e) + return None + + mounts: list[dict] | None = None + try: + container.reload() + mounts = container.attrs.get("Mounts") + except Exception as e: + self.log.error("Failed to read container attrs", container=name, error=e) + + try: + container.stop(timeout=stop_timeout) + except docker.errors.NotFound: + return mounts + except Exception as e: + self.log.error("Failed to stop container", container=name, error=e) + + if log_file is not None: + try: + self.log.info( + "Saving container logs", container=name, logs_file=log_file + ) + logs_stream = container.logs( + stream=True, follow=False, stdout=True, stderr=True + ) + with open(log_file, "wb") as f: + for line in logs_stream: + f.write(line) + decoded_line = line.decode("utf-8", errors="replace") + if line_callback is not None: + line_callback(decoded_line) + if print_console and ( + console_filter is None or not console_filter(decoded_line) + ): + print(decoded_line, end="") + logs_stream.close() + except Exception as e: + self.log.error( + "Failed to save container logs", container=name, error=e + ) + + try: + container.remove(force=True) + except docker.errors.NotFound: + pass + except Exception as e: + self.log.error("Failed to remove container", container=name, error=e) + + return mounts + + def reap_orphan_containers(self) -> None: + """Force-remove leftover containers and networks from prior runs of *this* + scenario. + + Scoped to this scenario's label (not all expb containers) so it never + disturbs other benchmark runs sharing the machine. Since container names + are deterministic per scenario, this clears exactly the orphans that would + otherwise cause name-collision failures when the scenario runs again. + """ + label_filter = {"label": f"{EXPB_LABEL}.scenario={self.config.executor_name}"} + try: + orphans = self.config.docker_client.containers.list( + all=True, filters=label_filter + ) + except Exception as e: + self.log.error("Failed to list orphan containers", error=e) + orphans = [] + for container in orphans: + try: + container.remove(force=True) + self.log.info("Reaped orphan container", container=container.name) + except docker.errors.NotFound: + pass + except Exception as e: + self.log.error( + "Failed to reap orphan container", + container=container.name, + error=e, + ) + + try: + networks = self.config.docker_client.networks.list(filters=label_filter) + except Exception as e: + self.log.error("Failed to list orphan networks", error=e) + networks = [] + for network in networks: + try: + network.remove() + self.log.info("Reaped orphan network", network=network.name) + except docker.errors.NotFound: + pass + except Exception as e: + self.log.error( + "Failed to reap orphan network", network=network.name, error=e + ) + def cleanup_scenario( self, print_logs_to_console: bool = False, @@ -985,109 +1135,60 @@ def cleanup_scenario( per_payload_metrics_rows: list[tuple[int, str, str]] = [] - # Clean k6 container - try: - k6_container = self.config.docker_client.containers.get( - self.config.get_k6_container_name() - ) - k6_container.stop(timeout=3) - logs_file = self.config.outputs_dir / "k6.log" - self.log.info("Saving k6 logs", logs_file=logs_file) - logs_stream = k6_container.logs( - stream=True, - follow=False, - stdout=True, - stderr=True, - ) - with open(logs_file, "wb") as f: - for line in logs_stream: - f.write(line) - decoded_line = line.decode("utf-8", errors="replace") - metric_row = self._parse_per_payload_metric_row(decoded_line) - if metric_row is not None: - per_payload_metrics_rows.append(metric_row) - if print_logs_to_console: - if not self._should_skip_console_k6_log_line(decoded_line): - print(decoded_line, end="") - logs_stream.close() - k6_container.remove() - except docker.errors.NotFound: - pass + def _collect_k6_metric(decoded_line: str) -> None: + metric_row = self._parse_per_payload_metric_row(decoded_line) + if metric_row is not None: + per_payload_metrics_rows.append(metric_row) + + self._teardown_container( + self.config.get_k6_container_name(), + log_file=self.config.outputs_dir / "k6.log", + stop_timeout=3, + print_console=print_logs_to_console, + console_filter=self._should_skip_console_k6_log_line, + line_callback=_collect_k6_metric, + ) - # Clean execution client container - try: - execution_client_container = self.config.docker_client.containers.get( - self.config.get_execution_client_container_name() - ) - execution_client_container.reload() - execution_client_volumes = execution_client_container.attrs["Mounts"] - execution_client_container.stop( - timeout=60 if self._dottrace_active else 5 - ) - logs_file = ( + execution_client_mounts = self._teardown_container( + self.config.get_execution_client_container_name(), + log_file=( self.config.outputs_dir / f"{self.config.get_execution_client_name()}.log" - ) - self.log.info("Saving execution client logs", logs_file=logs_file) - logs_stream = execution_client_container.logs( - stream=True, - follow=False, - stdout=True, - stderr=True, - ) - with open(logs_file, "wb") as f: - for line in logs_stream: - f.write(line) - if print_logs_to_console: - print(line.decode("utf-8"), end="") - logs_stream.close() - execution_client_container.remove() - # Clean execution client volumes - for volume in execution_client_volumes: - if volume["Type"] == "volume": - self.config.docker_client.volumes.get(volume["Name"]).remove() - self.log.debug( - "Cleaned execution client volume", volume=volume["Name"] - ) - except docker.errors.NotFound: - pass + ), + stop_timeout=60 if self._dottrace_active else 5, + print_console=print_logs_to_console, + ) + if execution_client_mounts: + for volume in execution_client_mounts: + if volume.get("Type") == "volume": + try: + self.config.docker_client.volumes.get( + volume["Name"] + ).remove() + self.log.debug( + "Cleaned execution client volume", volume=volume["Name"] + ) + except Exception as e: + self.log.error( + "Failed to remove execution client volume", + volume=volume.get("Name"), + error=e, + ) if print_logs_to_console and print_per_payload_metrics_table: self._print_per_payload_metrics_table(per_payload_metrics_rows) - # Clean payload server container - try: - payload_server_container = self.config.docker_client.containers.get( - self.config.get_payload_server_container_name() - ) - payload_server_container.stop(timeout=3) - logs_file = self.config.outputs_dir / "payload-server.log" - self.log.info("Saving payload server logs", logs_file=logs_file) - logs_stream = payload_server_container.logs( - stream=True, - follow=False, - stdout=True, - stderr=True, - ) - with open(logs_file, "wb") as f: - for line in logs_stream: - f.write(line) - if print_logs_to_console: - print(line.decode("utf-8", errors="replace"), end="") - logs_stream.close() - payload_server_container.remove() - except docker.errors.NotFound: - pass + self._teardown_container( + self.config.get_payload_server_container_name(), + log_file=self.config.outputs_dir / "payload-server.log", + stop_timeout=3, + print_console=print_logs_to_console, + ) - # Clean alloy container - try: - alloy_container = self.config.docker_client.containers.get( - self.config.get_alloy_container_name() - ) - alloy_container.stop(timeout=3) - alloy_container.remove() - except docker.errors.NotFound: - pass + self._teardown_container( + self.config.get_alloy_container_name(), + stop_timeout=3, + ) # Clean docker network try: @@ -1097,6 +1198,8 @@ def cleanup_scenario( containers_network.remove() except docker.errors.NotFound: pass + except Exception as e: + self.log.error("Failed to remove docker network", error=e) # Clean overlay directories self.remove_directories() @@ -1110,14 +1213,15 @@ def execute_scenario( cpu_stabilizer: CpuStabilizer | None = None timer_stabilizer: TimerStabilizer | None = None smt_stabilizer: SmtStabilizer | None = None - prev_sigterm = None - if os.name != "nt": + prev_handlers: dict[int, SignalHandler] = {} + managed_signals = (signal.SIGTERM, signal.SIGINT) if os.name != "nt" else () - def _sigterm_handler(signum: int, frame: object) -> None: - raise SystemExit(128 + signum) + def _termination_handler(signum: int, frame: FrameType | None) -> None: + raise SystemExit(128 + signum) - prev_sigterm = signal.getsignal(signal.SIGTERM) - signal.signal(signal.SIGTERM, _sigterm_handler) + for sig in managed_signals: + prev_handlers[sig] = signal.getsignal(sig) + signal.signal(sig, _termination_handler) try: self.log.info( "Preparing scenario", @@ -1147,6 +1251,13 @@ def _sigterm_handler(signum: int, frame: object) -> None: smt_stabilizer.apply() self._log_system_diagnostics("System state after stabilizers applied") + if options.reap_orphans: + self.log.info( + "Reaping orphan containers from prior runs of this scenario", + scenario=self.config.executor_name, + ) + self.reap_orphan_containers() + self.clean_system_cache() self.prepare_directories() self.prepare_jwt_secret_file() @@ -1157,6 +1268,7 @@ def _sigterm_handler(signum: int, frame: object) -> None: containers_network = self.config.docker_client.networks.create( name=self.config.get_containers_network_name(), driver="bridge", + labels=self._container_labels(), ) alloy_pyroscope: Pyroscope | None = None @@ -1213,6 +1325,7 @@ def _sigterm_handler(signum: int, frame: object) -> None: pyroscope=alloy_pyroscope, stop_signal=stop_signal, dottrace=dottrace_enabled, + restart_retries=options.client_restart_retries, ) # Get execution client RPC URL immediately (container IP is @@ -1346,20 +1459,27 @@ def _sigterm_handler(signum: int, frame: object) -> None: self.log.error("Failed to execute scenario", error=e) raise e finally: - self.cleanup_scenario( - print_logs_to_console=( - options.print_logs_to_console or options.per_payload_metrics_logs - ), - print_per_payload_metrics_table=options.per_payload_metrics_logs, - ) - if smt_stabilizer is not None: - smt_stabilizer.restore() - if cpu_stabilizer is not None: - cpu_stabilizer.restore() - if timer_stabilizer is not None: - timer_stabilizer.restore() - if prev_sigterm is not None: - signal.signal(signal.SIGTERM, prev_sigterm) + # Ignore termination signals while tearing down so a second + # cancellation signal can't interrupt cleanup and orphan containers. + for sig in managed_signals: + signal.signal(sig, signal.SIG_IGN) + try: + self.cleanup_scenario( + print_logs_to_console=( + options.print_logs_to_console + or options.per_payload_metrics_logs + ), + print_per_payload_metrics_table=options.per_payload_metrics_logs, + ) + if smt_stabilizer is not None: + smt_stabilizer.restore() + if cpu_stabilizer is not None: + cpu_stabilizer.restore() + if timer_stabilizer is not None: + timer_stabilizer.restore() + finally: + for sig, handler in prev_handlers.items(): + signal.signal(sig, handler) @classmethod def from_scenarios( From c28e3bafbbee14163195ad3765b1dbbf7f470b15 Mon Sep 17 00:00:00 2001 From: Carlos Bermudez Porto Date: Wed, 15 Jul 2026 12:15:05 -0400 Subject: [PATCH 2/4] fix: preserve 120s execution client stop timeout in refactored cleanup The cleanup refactor moved the execution client teardown into _teardown_container with stop_timeout=60/5, which would revert the 120s flush window (PGO profiles, RocksDB flush, dotTrace snapshots) introduced on main. Use 120s unconditionally to keep that behavior. --- src/expb/payloads/executor/executor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/expb/payloads/executor/executor.py b/src/expb/payloads/executor/executor.py index b19b93d..cdf035b 100644 --- a/src/expb/payloads/executor/executor.py +++ b/src/expb/payloads/executor/executor.py @@ -1155,7 +1155,10 @@ def _collect_k6_metric(decoded_line: str) -> None: self.config.outputs_dir / f"{self.config.get_execution_client_name()}.log" ), - stop_timeout=60 if self._dottrace_active else 5, + # Give the execution client 120s after SIGTERM to flush data (e.g. PGO + # profiles via WritePGOData, RocksDB flush, and dotTrace snapshot writes) + # before Docker sends SIGKILL (default 10s). + stop_timeout=120, print_console=print_logs_to_console, ) if execution_client_mounts: From e509f03b0b4fe4653979797304f39a8689592044 Mon Sep 17 00:00:00 2001 From: Carlos Bermudez Porto Date: Wed, 15 Jul 2026 12:37:27 -0400 Subject: [PATCH 3/4] fix: validate client_restart_retries and correct run_k6 return type Address PR review: reject negative client_restart_retries with a clear ValueError (and min=0 at the CLI), and annotate run_k6 as returning bytes since detach=False returns the container logs, not a Container. --- src/expb/execute_scenario.py | 1 + src/expb/execute_scenarios.py | 1 + src/expb/payloads/executor/executor.py | 10 +++++++--- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/expb/execute_scenario.py b/src/expb/execute_scenario.py index 6f9712b..faa228d 100644 --- a/src/expb/execute_scenario.py +++ b/src/expb/execute_scenario.py @@ -86,6 +86,7 @@ def execute_scenario( client_restart_retries: Annotated[ int, typer.Option( + min=0, help="Auto-restart the execution client on failure up to N times (0 = never restart). Infrastructure containers (K6, Alloy, payload server) never restart.", ), ] = 0, diff --git a/src/expb/execute_scenarios.py b/src/expb/execute_scenarios.py index e62eeef..96d4e7d 100644 --- a/src/expb/execute_scenarios.py +++ b/src/expb/execute_scenarios.py @@ -92,6 +92,7 @@ def execute_scenarios( client_restart_retries: Annotated[ int, typer.Option( + min=0, help="Auto-restart the execution client on failure up to N times (0 = never restart). Infrastructure containers (K6, Alloy, payload server) never restart.", ), ] = 0, diff --git a/src/expb/payloads/executor/executor.py b/src/expb/payloads/executor/executor.py index cdf035b..d546d9e 100644 --- a/src/expb/payloads/executor/executor.py +++ b/src/expb/payloads/executor/executor.py @@ -77,6 +77,10 @@ def __init__( self.client_metrics: bool = client_metrics self.stable_cpu: bool = stable_cpu self.dottrace: bool = dottrace + if client_restart_retries < 0: + raise ValueError( + f"client_restart_retries must be >= 0, got {client_restart_retries}" + ) self.client_restart_retries: int = client_restart_retries self.reap_orphans: bool = reap_orphans @@ -826,7 +830,7 @@ def run_k6( collect_per_payload_metrics: bool = False, enable_logging: bool = False, per_payload_metrics_logs: bool = False, - ) -> Container: + ) -> bytes: # Prepare k6 container volumes k6_container_volumes = self.config.get_k6_volumes() @@ -859,8 +863,8 @@ def run_k6( ) if self.config.resources and self.config.resources.infra_cpuset is not None: run_kwargs["cpuset_cpus"] = self.config.resources.infra_cpuset - container = self.config.docker_client.containers.run(**run_kwargs) - return container + logs = self.config.docker_client.containers.run(**run_kwargs) + return logs # Extra Commands Execution def _execute_single_command( From eefd01fce845cdb3ba110023e29694d3b904dc88 Mon Sep 17 00:00:00 2001 From: Carlos Bermudez Porto Date: Wed, 15 Jul 2026 12:55:46 -0400 Subject: [PATCH 4/4] ci: add ruff and ty checks and fix existing lint/type issues Add ruff and ty as dev dependencies and run them as dedicated CI jobs alongside pytest. Fix the issues they surface: - drop unused pydantic.NewPath import in scenarios.py - annotate get_execution_client_volumes as list[dict[str, Any]] - guard container.attrs before subscripting in the compressor --- .github/workflows/tests.yml | 34 ++++++++++++ pyproject.toml | 2 + src/expb/configs/scenarios.py | 1 - src/expb/payloads/compressor/compressor.py | 2 + src/expb/payloads/executor/executor_config.py | 3 +- uv.lock | 54 +++++++++++++++++++ 6 files changed, 94 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 108237f..79c6ccc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,3 +22,37 @@ jobs: - name: Run tests run: uv run pytest -v + + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v6 + + - name: Set up Python + run: uv python install + + - name: Install dependencies + run: uv sync --dev + + - name: Run ruff + run: uv run ruff check . + + ty: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v6 + + - name: Set up Python + run: uv python install + + - name: Install dependencies + run: uv sync --dev + + - name: Run ty + run: uv run ty check diff --git a/pyproject.toml b/pyproject.toml index cc24755..b670ffe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,8 @@ packages = ["src/expb"] dev = [ "hatchling>=1.29.0", "pytest>=8.0.0", + "ruff>=0.10.0", + "ty>=0.0.1a1", ] [tool.pytest.ini_options] diff --git a/src/expb/configs/scenarios.py b/src/expb/configs/scenarios.py index 5fabd5f..83f1fb1 100644 --- a/src/expb/configs/scenarios.py +++ b/src/expb/configs/scenarios.py @@ -4,7 +4,6 @@ BaseModel, Field, FilePath, - NewPath, field_serializer, field_validator, model_validator, diff --git a/src/expb/payloads/compressor/compressor.py b/src/expb/payloads/compressor/compressor.py index 3a66b99..c74cb74 100644 --- a/src/expb/payloads/compressor/compressor.py +++ b/src/expb/payloads/compressor/compressor.py @@ -236,6 +236,8 @@ def start_nethermind( ) container.reload() + if container.attrs is None: + raise ValueError("Container attributes are not available") container_ip = container.attrs["NetworkSettings"]["Networks"][ container_network.name ]["IPAddress"] diff --git a/src/expb/payloads/executor/executor_config.py b/src/expb/payloads/executor/executor_config.py index 2411ec0..3d62cd7 100644 --- a/src/expb/payloads/executor/executor_config.py +++ b/src/expb/payloads/executor/executor_config.py @@ -2,6 +2,7 @@ import re import time from pathlib import Path +from typing import Any import docker from docker.client import DockerClient @@ -271,7 +272,7 @@ def get_execution_client_sse_url( else: raise ValueError("Container attributes are not available") - def get_execution_client_volumes(self) -> list[dict[str, dict]]: + def get_execution_client_volumes(self) -> list[dict[str, Any]]: execution_container_volumes = [] container_name = self.get_execution_client_container_name() for volume_name, volume_config in self.execution_client_extra_volumes.items(): diff --git a/uv.lock b/uv.lock index 02482ad..10c3e5d 100644 --- a/uv.lock +++ b/uv.lock @@ -391,6 +391,8 @@ dependencies = [ dev = [ { name = "hatchling" }, { name = "pytest" }, + { name = "ruff" }, + { name = "ty" }, ] [package.metadata] @@ -411,6 +413,8 @@ requires-dist = [ dev = [ { name = "hatchling", specifier = ">=1.29.0" }, { name = "pytest", specifier = ">=8.0.0" }, + { name = "ruff", specifier = ">=0.10.0" }, + { name = "ty", specifier = ">=0.0.1a1" }, ] [[package]] @@ -890,6 +894,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/fb/e4c0ced9893b84ac95b7181d69a9786ce5879aeb3bbbcbba80a164f85d6a/rlp-4.1.0-py3-none-any.whl", hash = "sha256:8eca394c579bad34ee0b937aecb96a57052ff3716e19c7a578883e767bc5da6f", size = 19973, upload-time = "2025-02-04T22:05:57.05Z" }, ] +[[package]] +name = "ruff" +version = "0.15.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/36/6f65aa9989acdec45d417192d8f4e7921931d8a6cf87ac74bce3eed98a8e/ruff-0.15.21.tar.gz", hash = "sha256:d0cfc841c572283c36548f82664a54ce6565567f1b0d5b4cf2caac693d8b7500", size = 4769401, upload-time = "2026-07-09T20:01:34.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/c6/ede15cac6839f3dbce52565c8f5164a8210e669c7bc4decb03e5bdf47d0d/ruff-0.15.21-py3-none-linux_armv6l.whl", hash = "sha256:63ea0e965e5d73c90e95b2434beeafc70820536717f561b32ab6e777cb9bdf5d", size = 10854342, upload-time = "2026-07-09T20:00:53.998Z" }, + { url = "https://files.pythonhosted.org/packages/28/9d/d825b07ee7ea9e2d61df92a860033c94e06e7300d50a1c2653aac27d24fe/ruff-0.15.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0f212c5d7d54c01bbfe6dcab02b724a39300f3e34ed7acbe995ccb320a2c58bd", size = 11139539, upload-time = "2026-07-09T20:00:57.809Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/3b107712e642f063c7a9e0887c427b22cb44097de5aab36c05f2e280670c/ruff-0.15.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e6312e41bc96791299614995ea3a977c5857c3b5662b1ecef6755b02b87cb646", size = 10595437, upload-time = "2026-07-09T20:01:00.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/6f/b4523cc90ba239ede441447a19d0c968846a3012e5a0b0c5b62831a3d5e3/ruff-0.15.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01d65b4831c6b2a4ba8ee6faa84049d44d982b7a706e622c4094c509e51673be", size = 10990053, upload-time = "2026-07-09T20:01:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/92/cc/c6a9872a5375f0628875481cf2f66b13d7d865bf3ca2e57f91c7e762d976/ruff-0.15.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c5a913a589120ce67933d5d05fd6ddbcc2481c6a054980ee767f7414c72b4fd", size = 10666096, upload-time = "2026-07-09T20:01:04.299Z" }, + { url = "https://files.pythonhosted.org/packages/ab/97/c621f7a17e097f1790fa3af6374138823b330b2d03fc38337945daca212c/ruff-0.15.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef04b681d02ad4dc9620f00f83ac5c22f652d0e9a9cfe431d219b16ad5ccc41", size = 11537011, upload-time = "2026-07-09T20:01:06.771Z" }, + { url = "https://files.pythonhosted.org/packages/ea/51/d928727e476e25ccc57c6f449ffd80241a651a973ad949d39cfb2a771d28/ruff-0.15.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16d090c0740916594157e75b80d666eab8e78083b39b3b0e1d698f4670a17b86", size = 12347101, upload-time = "2026-07-09T20:01:08.859Z" }, + { url = "https://files.pythonhosted.org/packages/1e/88/8cd62026802b16018ad06931d87997cf795ba2a6239ab659606c87d96bf0/ruff-0.15.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a10e74757dd65004d779b73e2f3c5210156d9980b41224d50d2ebcf1db51e67", size = 11572001, upload-time = "2026-07-09T20:01:11.092Z" }, + { url = "https://files.pythonhosted.org/packages/b2/97/f63084cf55444fc110e8cb985ebfcc592af47f597d44453d778cb81bc156/ruff-0.15.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bab0905d2f29e0d9fbc3c373ed23db0095edaa3f71f1f4f519ec15134d9e85c8", size = 11549239, upload-time = "2026-07-09T20:01:13.27Z" }, + { url = "https://files.pythonhosted.org/packages/9d/77/f107da4a2874b7715914b03f09ba9c54424de3ff8a1cc5d015d3ee2ce0ac/ruff-0.15.21-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:00eca240af5789fec6fe7df74c088cc1f9644ed83027113468efba7c92b94075", size = 11535340, upload-time = "2026-07-09T20:01:15.206Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e9/601deb322d3303a7bf212b0100ead6f2ee3f6a044d89c30f2f92bf83c731/ruff-0.15.21-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262ab31557a75141325e32d3357f3597645a7f084e732b6b054dde428ecd9341", size = 10964048, upload-time = "2026-07-09T20:01:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2e/0f2176d1e99c15192caea19c8c3a0a955246b4cb4de795042eeb616345cd/ruff-0.15.21-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:659c4e7a4212f83306045ec7c5e5a356d16d9a6ef4ae0c7a4d872914fc655d9d", size = 10667055, upload-time = "2026-07-09T20:01:19.73Z" }, + { url = "https://files.pythonhosted.org/packages/48/60/abd74a02e0c4214f12a68becfd30af7165cfdcb0e661ecdc60bbb949c09a/ruff-0.15.21-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9e866eab611a5f959d36df2d10e446973a3610bc42b0c15b31dc27977d59c233", size = 11242043, upload-time = "2026-07-09T20:01:21.947Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c6/583075d8ccabb4b229345edcaf1545eb3d8d6be90f686a479d7e94088bbf/ruff-0.15.21-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e89bc93c0d3803ba870b55c29671bad9dc6d94bb1eb181b056b52eb05b52854f", size = 11648064, upload-time = "2026-07-09T20:01:24.023Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3c/37d0ecb729a7cc2d393ea7dce316fc585680f35d93b8d62139d7d0a3700c/ruff-0.15.21-py3-none-win32.whl", hash = "sha256:01f8d5be84823c172b389e123174f781f9daf86d6c58719d603f941932195cdd", size = 10896555, upload-time = "2026-07-09T20:01:26.941Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b8/e43466b2a6067ce91e669068f6e28d6c719a920f014b070d5c8731725de3/ruff-0.15.21-py3-none-win_amd64.whl", hash = "sha256:d4b8d9a2f0f12b816b50447f6eccb9f4bb01a6b82c86b50fb3b5354b458dc6d3", size = 12038772, upload-time = "2026-07-09T20:01:29.497Z" }, + { url = "https://files.pythonhosted.org/packages/dd/75/e90ab9aeece218a9fc5a5bc3ec97d0ee6bb3c4ff95869463c1de58e29a1c/ruff-0.15.21-py3-none-win_arm64.whl", hash = "sha256:6e83115d4b9377c1cbc13abf0e051f069fab0ef815ea0504a8a008cee24dd0a8", size = 11375265, upload-time = "2026-07-09T20:01:31.772Z" }, +] + [[package]] name = "shellingham" version = "1.5.4" @@ -926,6 +955,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-py3-none-any.whl", hash = "sha256:1f9553927f18d0513d8e5ff80ab8980b8202ce37ecae0e3274ed2ef11880e74d", size = 14197, upload-time = "2026-01-14T14:54:49.067Z" }, ] +[[package]] +name = "ty" +version = "0.0.59" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/b0/84ae7b3bf6e3e9f57eb9635eeff5a80b36e57aa089f40be0fb5c384fa176/ty-0.0.59.tar.gz", hash = "sha256:53e53ffeed78ad59cd237fa8ea1316d2b94e13efdea9a945698acab549e005aa", size = 6145435, upload-time = "2026-07-12T20:22:02.781Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/e8/650b42fbef4d48e6ca682b0b6e9b68fa8fcf55cbb0a6892ab89990018b6f/ty-0.0.59-py3-none-linux_armv6l.whl", hash = "sha256:f8fb08a767ef8f11ea3c537b9d77860726cc2bc39e6f77ad13c02d5b289f20a7", size = 11700328, upload-time = "2026-07-12T20:21:26.046Z" }, + { url = "https://files.pythonhosted.org/packages/22/ac/0ca3a89d5f59ae5f308e5e83428cac5f9143200767743e052fba90b4b81e/ty-0.0.59-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c7f4d5630836c8a0ba13dd4ac7bdae080a7d6ebe965b817ff642dc961bcf2a53", size = 11494310, upload-time = "2026-07-12T20:21:28.491Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f8/5076de6001cefbccd8e6dc8472262697e43308ff66b0e87c72abba136357/ty-0.0.59-py3-none-macosx_11_0_arm64.whl", hash = "sha256:872f6fb02c6db5553c4d5fb283b3d50f0985fb9a29a910e4fda4793a775c1926", size = 11026797, upload-time = "2026-07-12T20:21:30.879Z" }, + { url = "https://files.pythonhosted.org/packages/2e/0f/fca28481b6a138e2b798ad9fdc98a095475f9104948ba242fce4b477782b/ty-0.0.59-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2af8eefbfe806337770eec12c0c819c5f1b8f5b85f8369cb1cc9fa25234a2208", size = 11475304, upload-time = "2026-07-12T20:21:33.041Z" }, + { url = "https://files.pythonhosted.org/packages/08/4b/1fed8b81b389ef4bbc0400f19e05fc16496b162577779dc0e5fc65ac216c/ty-0.0.59-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0acf8b76a1c9a7ddef460b42475f6c76193164426ab080783af1c3175b4b999b", size = 11533131, upload-time = "2026-07-12T20:21:35.189Z" }, + { url = "https://files.pythonhosted.org/packages/5f/fc/04eec35e05a10e0fea1c6503a290ccc3935efda9c845aff64e83282c1af7/ty-0.0.59-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:043c2e00eb1d7475f928af7dedd71f69b64e69bfca55e36f4c968479e1373fc4", size = 12205932, upload-time = "2026-07-12T20:21:37.324Z" }, + { url = "https://files.pythonhosted.org/packages/a9/dd/a61de859659fa11b55917ad38340a8f2c61f5ae17d1874929f29084c6990/ty-0.0.59-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0d688d857441df57f48fca66c029d85cf737c510e7be1d01144cdad1e58d968", size = 12758406, upload-time = "2026-07-12T20:21:39.525Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e8/fa66f05997eab8ca75fc4f17320140e25467849e0cc75597f898cc22099c/ty-0.0.59-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a96c9f88394a3b42c737e2125b2330543f0d90a43b49761f377d96f8c3ee0d62", size = 12288176, upload-time = "2026-07-12T20:21:41.784Z" }, + { url = "https://files.pythonhosted.org/packages/15/68/0fca59963bd5123f42d5f7da50667e7a52e8e9615e3a16d8c2c0d3b2d143/ty-0.0.59-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f08dbcb268edcafcb152e59475b5b495ce28d0b340a395c09943557678f4d5a6", size = 12028471, upload-time = "2026-07-12T20:21:43.82Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5b/cd7dabbbab392578f11179919da5c25d8c3322e5388a688f539ea0539603/ty-0.0.59-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8812764b9a40fdc98df1272826e73a298ef56b06681135e643bcf90aad1896f7", size = 12297646, upload-time = "2026-07-12T20:21:45.76Z" }, + { url = "https://files.pythonhosted.org/packages/1d/37/2e9c94f0b383d8cbe1a35517ab470b7810bc9d7501603ab532bcd5be5e90/ty-0.0.59-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fd53b8581641d8dad7bfac6d5ea589e91a883d6837e0b9a286fdae30722b7c69", size = 11432519, upload-time = "2026-07-12T20:21:47.694Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0a/af93e9785200f11ac416cc20235fc2464c9bd978e791190684ea0e458795/ty-0.0.59-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:86da5872124a41877d95058bc17d33ddcff034b587eb5f1e2917ab88ba227dac", size = 11554993, upload-time = "2026-07-12T20:21:49.671Z" }, + { url = "https://files.pythonhosted.org/packages/4b/dd/651bf87e20d00376c81b19124756491cffaf20eb8bec05a8794e5a8cf641/ty-0.0.59-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a233eef5f2fd4d894881e4a0aec83c9f172bfae1d787d6596ee1939fcc7723e", size = 11818230, upload-time = "2026-07-12T20:21:51.659Z" }, + { url = "https://files.pythonhosted.org/packages/16/50/c947c4155fea751d135b19affdf734bbce72a94e446b866cf0c62f8bed69/ty-0.0.59-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7ff678c18b5f1e3128b75a35e50dee7908dea55155baa31cd790619d5014cbf5", size = 12135194, upload-time = "2026-07-12T20:21:53.796Z" }, + { url = "https://files.pythonhosted.org/packages/b1/13/e5feb138888de1e95037c843571bbbd4ac21bf0a190507468098599a321f/ty-0.0.59-py3-none-win32.whl", hash = "sha256:cf8abb4b8095c5fe39102b8127f5886db308c8d4600909ddbc905512ce9c8163", size = 11179249, upload-time = "2026-07-12T20:21:55.752Z" }, + { url = "https://files.pythonhosted.org/packages/76/dd/52914dcbeeba92c207de40ef7109a58dcb5527aeb21c8f8feb7402aa9e29/ty-0.0.59-py3-none-win_amd64.whl", hash = "sha256:1dde20a82243d24407869e5a608c2f15efddd5cefc662aef461a5af84bfb3f8b", size = 12251079, upload-time = "2026-07-12T20:21:58.1Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8f/ac36fde77e223297454c1e0aeb8888c169eaacf3163bb609e3af942c88cb/ty-0.0.59-py3-none-win_arm64.whl", hash = "sha256:987043ee9e021f49493d9135891ac69c1affeee0d4ad4480c5fa4d9c975fc91b", size = 11650921, upload-time = "2026-07-12T20:22:00.348Z" }, +] + [[package]] name = "typer" version = "0.16.0"