diff --git a/README.md b/README.md index 64ad1e079..83b109275 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Subnet 1 is the most intelligent inference model on Bittensor. As the first agen --- -## Installation +## Run Validator 1. **Clone the repository:** ```bash @@ -32,33 +32,26 @@ Subnet 1 is the most intelligent inference model on Bittensor. As the first agen cd apex ``` -2. **Install `uv`:** - Follow the instructions at [https://github.com/astral-sh/uv](https://github.com/astral-sh/uv) to install `uv`. For example: + +2. **Prepare config file:** ```bash - curl -LsSf https://astral.sh/uv/install.sh | sh + cp config/mainnet.yaml.example config/mainnet.yaml + # Fill in the required values in config/mainnet.yaml ``` -3. **Install the project and its development dependencies:** +3. **[Recommended] Run validator with auto-updater:** ```bash - uv venv --python=3.11 && uv pip install '.[dev]' + python scripts/autoupdater.py -c config/mainnet.yaml ``` -4. **Activate python environment:** - ```bash - . .venv/bin/activate - ``` - -## Run Mainnet Validator - -1. Prepare config file: +4. **[Alternative #1] Run validator with pm2 and auto-updater:** ```bash - cp config/mainnet.yaml.example config/mainnet.yaml - # Fill in the required values in config/mainnet.yaml + bash scripts/autoupdater_pm2.sh ``` -2. **Run the validator:** +5. **[Alternative #2] Install dependencies and run validator without auto-updater:** ```bash - python validator.py -c config/mainnet.yaml + uv venv --python 3.11 && uv pip install '.[dev]' && python validator.py -c config/mainnet.yaml ``` ## Run Testnet Validator @@ -69,9 +62,9 @@ Subnet 1 is the most intelligent inference model on Bittensor. As the first agen # Fill in the required values in config/testnet.yaml ``` -2. **Run the validator:** +2. Install dependencies and run validator: ```bash - python validator.py -c config/testnet.yaml + uv venv --python 3.11 && uv pip install '.[dev]' && python validator.py -c config/testnet.yaml ``` ## Base Miner (for showcase purposes only) diff --git a/apex/validator/pipeline.py b/apex/validator/pipeline.py index 5b695d7ea..a87b9cf28 100644 --- a/apex/validator/pipeline.py +++ b/apex/validator/pipeline.py @@ -24,8 +24,8 @@ def __init__( deep_research: DeepResearchBase, logger_apex: LoggerApex | None = None, num_consumers: int = 5, - timeout_consumer: float = 180, - timeout_producer: float = 36, + timeout_consumer: float = 1200, + timeout_producer: float = 240, queue_size: int = 10_000, redundancy_rate: float = 0.05, # The rate that references are generated in addition to generator steps reference_rate: float = 0.5, # The rate that references are generated as opposed to generator steps diff --git a/apex/validator/weight_syncer.py b/apex/validator/weight_syncer.py index 05b85599c..72a46974b 100644 --- a/apex/validator/weight_syncer.py +++ b/apex/validator/weight_syncer.py @@ -1,5 +1,4 @@ import asyncio -import os import time from typing import cast @@ -71,7 +70,7 @@ async def start(self) -> None: ) self.server = uvicorn.Server(config) self.server_task = asyncio.create_task(self.server.serve()) - logger.info(f"Started weight synchronization API on port {self.port}. pid={os.getpid()} self_id={id(self)}") + logger.info(f"Started weight synchronization API on port {self.port}") # Announce the axon on the network. external_ip = requests.get("https://checkip.amazonaws.com").text.strip() @@ -124,10 +123,6 @@ async def compute_weighted_rewards(self, hotkey_rewards: dict[str, float]) -> di """Computes weighted rewards by fetching rewards from other validators and averaging them by stake.""" self.hotkey_rewards = hotkey_rewards self.last_update_time = time.time() - # logger.debug(f"Updating rewards at: {self.last_update_time}") - import os - - logger.debug(f"Updating rewards at: {self.last_update_time} pid={os.getpid()} self_id={id(self)}") if not self.receive_enabled: logger.warning("Rewards weight averaging is disable, using raw rewards") return hotkey_rewards diff --git a/pyproject.toml b/pyproject.toml index 832ad83e3..f8602d46a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,10 +16,11 @@ dependencies = [ "aiohttp>=3.8.3", "beautifulsoup4>=4.13.3", "langchain>=0.3.26", + "langchain-core>=0.3.68", "langchain-community>=0.0.59", - "faiss-cpu>=1.8.0", "langchain-openai>=0.3.28", "langchain-sandbox>=0.0.6", + "faiss-cpu>=1.8.0", "dotenv>=0.9.9", "rich>=14.0.0", "loguru>=0.7.3", @@ -29,6 +30,10 @@ dependencies = [ "rouge>=1.0.1", "substrate-interface>=1.7.11", "types-netaddr>=1.3.0.20240530", + "types-pyyaml>=6.0.12.20250516", + "types-cachetools>=6.0.0.20250525", + "dotenv>=0.9.9", + "pytest-mock>=3.14.1", ] @@ -36,13 +41,6 @@ dependencies = [ dev = [ "mypy==1.17.0", "ruff==0.12.5", - "types-pyyaml>=6.0.12.20250516", - "types-cachetools>=6.0.0.20250525", - "langchain>=0.3.26", - "dotenv>=0.9.9", - "langchain-openai>=0.3.28", - "langchain-core>=0.3.68", - "langchain-sandbox>=0.0.6", "pytest>=8.4.1", "pytest-asyncio>=1.0.0", "pytest-cov>=5.0.0", @@ -203,5 +201,6 @@ dev = [ "pydantic>=2.11.7", "pytest>=8.4.1", "pytest-asyncio>=1.0.0", + "pytest-mock>=3.14.1", "types-pyyaml>=6.0.12.20250516", ] diff --git a/scripts/autoupdater.py b/scripts/autoupdater.py index e69de29bb..b700db9d5 100644 --- a/scripts/autoupdater.py +++ b/scripts/autoupdater.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +import argparse +import os +import signal +import subprocess +import sys +import time +from pathlib import Path + +CHECK_INTERVAL = 15 * 60 + + +def venv_python() -> str: + return os.path.join(".venv", "bin", "python") + + +def read_python_version() -> str | None: + try: + with open(".python-version", encoding="utf-8") as f: + # Take first non-empty token (pyenv format e.g. "3.11.9"). + return f.read().strip().split()[0] + except FileNotFoundError: + return None + + +def start_proc(config: Path) -> subprocess.Popen: + py_ver = read_python_version() + if py_ver: + subprocess.run(["uv", "venv", "--python", py_ver], check=True) + else: + subprocess.run(["uv", "venv"], check=True) + + # Install project in dev mode into the venv. + subprocess.run(["uv", "pip", "install", ".[dev]"], check=True) + + # Run validator. + return subprocess.Popen([venv_python(), "validator.py", "-c", str(config)]) + + +def stop_proc(process: subprocess.Popen) -> None: + if process and process.poll() is None: + process.terminate() + try: + process.wait(timeout=10) + except subprocess.TimeoutExpired: + process.kill() + + +def remote_has_updates() -> bool: + try: + subprocess.run(["git", "fetch", "--quiet"], check=True) + out = subprocess.check_output( + ["git", "rev-list", "--left-right", "--count", "@{u}...HEAD"], stderr=subprocess.STDOUT, text=True + ).strip() + left, right = map(int, out.split()) + # Remote is ahead. + return left > 0 + except subprocess.CalledProcessError: + # No upstream or git issue; treat as no updates. + return False + + +def git_pull_ff_only() -> None: + try: + subprocess.run(["git", "pull", "--ff-only"], check=True) + except subprocess.CalledProcessError as e: + print(f"Error: Git pull failed due to conflicts or other issues: {e}", file=sys.stderr) + print("Staying on the current version.", file=sys.stderr) + + +def read_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Apex validator") + parser.add_argument( + "-c", + "--config", + # default="config/testnet.yaml", + default="config/mainnet.yaml", + help="Config file path (e.g. config/mainnet.yaml).", + type=Path, + ) + args = parser.parse_args() + return args + + +def main() -> None: + args = read_args() + proc = start_proc(config=args.config) + + def handle_sigint(sig, frame): + stop_proc(proc) + sys.exit(0) + + signal.signal(signal.SIGINT, handle_sigint) + + while True: + time.sleep(CHECK_INTERVAL) + print("Checking for updates...") + + # If child exited, propagate its code. + if proc.poll() is not None: + sys.exit(proc.returncode) + + if remote_has_updates(): + print("Updates detected, restarting validator") + stop_proc(proc) + git_pull_ff_only() + proc = start_proc(config=args.config) + + +if __name__ == "__main__": + main() diff --git a/scripts/autoupdater_pm2.sh b/scripts/autoupdater_pm2.sh new file mode 100644 index 000000000..7adca2022 --- /dev/null +++ b/scripts/autoupdater_pm2.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +APP_NAME="sn1" +CONFIG="config/mainnet.yaml" +# CONFIG="config/testnet.yaml" + +UV_INSTALL_URL="https://astral.sh/uv/install.sh" + +# Ensure common user bin dirs are in PATH. +export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$HOME/bin:$HOME/.npm-global/bin:$PATH" + +# 1) Ensure uv exists. +if ! command -v uv >/dev/null 2>&1; then + echo "[info] uv not found; installing..." + if ! command -v curl >/dev/null 2>&1; then + echo "[error] curl is required to install uv." >&2 + exit 1 + fi + curl -LsSf "$UV_INSTALL_URL" | sh + hash -r + if ! command -v uv >/dev/null 2>&1; then + echo "[error] uv installation completed but 'uv' not found in PATH." >&2 + exit 1 + fi +else + echo "[info] uv found: $(command -v uv)" +fi + +# 2) Ensure pm2 exists. +if ! command -v pm2 >/dev/null 2>&1; then + echo "[info] pm2 not found; installing globally with npm..." + if ! command -v npm >/dev/null 2>&1; then + echo "[error] npm is required to install pm2. Please install Node.js first." >&2 + exit 1 + fi + npm install -g pm2 + hash -r + if ! command -v pm2 >/dev/null 2>&1; then + echo "[error] pm2 installation completed but 'pm2' not found in PATH." >&2 + exit 1 + fi +else + echo "[info] pm2 found: $(command -v pm2)" +fi + +pm2 start scripts/autoupdater.py --interpreter .venv/bin/python --name sn1 -- -c $CONFIG +pm2 logs sn1 diff --git a/tests/scripts/test_autoupdater.py b/tests/scripts/test_autoupdater.py new file mode 100644 index 000000000..84412aa7d --- /dev/null +++ b/tests/scripts/test_autoupdater.py @@ -0,0 +1,203 @@ +import os +import subprocess +import sys +from unittest import mock + +import pytest + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../scripts"))) +import autoupdater # isort: skip + + +def test_venv_python(mocker): + mocker.patch("autoupdater.os.path.join", return_value=".venv/bin/python") + assert autoupdater.venv_python() == ".venv/bin/python" + autoupdater.os.path.join.assert_called_once_with(".venv", "bin", "python") + + +def test_read_python_version_exists(mocker): + mocker.patch("autoupdater.open", mocker.mock_open(read_data="3.11.9")) + assert autoupdater.read_python_version() == "3.11.9" + autoupdater.open.assert_called_once_with(".python-version", encoding="utf-8") + + +def test_read_python_version_not_found(mocker): + mocker.patch("autoupdater.open", side_effect=FileNotFoundError) + assert autoupdater.read_python_version() is None + autoupdater.open.assert_called_once_with(".python-version", encoding="utf-8") + + +def test_start_proc_with_version(mocker): + mocker.patch("autoupdater.read_python_version", return_value="3.11.9") + mock_run = mocker.patch("subprocess.run") + mock_popen = mocker.patch("subprocess.Popen", return_value=mocker.Mock()) + mocker.patch("autoupdater.venv_python", return_value="mock_python") + + proc = autoupdater.start_proc(config="mock.yaml") + autoupdater.read_python_version.assert_called_once() + mock_run.assert_has_calls( + [ + mock.call(["uv", "venv", "--python", "3.11.9"], check=True), + mock.call(["uv", "pip", "install", ".[dev]"], check=True), + ] + ) + mock_popen.assert_called_once_with(["mock_python", "validator.py", "-c", "mock.yaml"]) + assert proc is not None + + +def test_start_proc_without_version(mocker): + mocker.patch("autoupdater.read_python_version", return_value=None) + mock_run = mocker.patch("subprocess.run") + mock_popen = mocker.patch("subprocess.Popen", return_value=mocker.Mock()) + mocker.patch("autoupdater.venv_python", return_value="mock_python") + + proc = autoupdater.start_proc(config="mock.yaml") + autoupdater.read_python_version.assert_called_once() + mock_run.assert_has_calls( + [ + mock.call(["uv", "venv"], check=True), + mock.call(["uv", "pip", "install", ".[dev]"], check=True), + ] + ) + mock_popen.assert_called_once_with(["mock_python", "validator.py", "-c", "mock.yaml"]) + assert proc is not None + + +def test_stop_proc_running(): + mock_proc = mock.Mock() + mock_proc.poll.return_value = None # Process is running + autoupdater.stop_proc(mock_proc) + mock_proc.terminate.assert_called_once() + mock_proc.wait.assert_called_once_with(timeout=10) + mock_proc.kill.assert_not_called() + + +def test_stop_proc_timeout(): + mock_proc = mock.Mock() + mock_proc.poll.return_value = None # Process is running + mock_proc.wait.side_effect = subprocess.TimeoutExpired(cmd="test", timeout=10) + autoupdater.stop_proc(mock_proc) + mock_proc.terminate.assert_called_once() + mock_proc.wait.assert_called_once_with(timeout=10) + mock_proc.kill.assert_called_once() + + +def test_stop_proc_already_stopped(): + mock_proc = mock.Mock() + mock_proc.poll.return_value = 0 # Process already stopped + autoupdater.stop_proc(mock_proc) + mock_proc.terminate.assert_not_called() + mock_proc.wait.assert_not_called() + mock_proc.kill.assert_not_called() + + +def test_remote_has_updates_true(mocker): + mock_run = mocker.patch("subprocess.run") + mock_check_output = mocker.patch("subprocess.check_output", return_value="1\t0") + assert autoupdater.remote_has_updates() is True + mock_run.assert_called_once_with(["git", "fetch", "--quiet"], check=True) + mock_check_output.assert_called_once_with( + ["git", "rev-list", "--left-right", "--count", "@{u}...HEAD"], stderr=subprocess.STDOUT, text=True + ) + + +def test_remote_has_updates_false_no_diff(mocker): + mocker.patch("subprocess.run") + mocker.patch("subprocess.check_output", return_value="0\t0") + assert autoupdater.remote_has_updates() is False + + +def test_remote_has_updates_error(mocker): + mock_run = mocker.patch("subprocess.run") + mocker.patch("subprocess.check_output", side_effect=subprocess.CalledProcessError(1, "cmd")) + assert autoupdater.remote_has_updates() is False + mock_run.assert_called_once_with(["git", "fetch", "--quiet"], check=True) + autoupdater.subprocess.check_output.assert_called_once() + + +def test_git_pull_ff_only_success(mocker): + mock_run = mocker.patch("subprocess.run") + autoupdater.git_pull_ff_only() + mock_run.assert_called_once_with(["git", "pull", "--ff-only"], check=True) + + +def test_git_pull_ff_only_conflict(mocker): + mocker.patch("subprocess.run", side_effect=subprocess.CalledProcessError(1, "cmd", stderr="conflict")) + mock_stderr = mocker.patch("sys.stderr", new_callable=mock.MagicMock) + autoupdater.git_pull_ff_only() + autoupdater.subprocess.run.assert_called_once_with(["git", "pull", "--ff-only"], check=True) + # Check for individual calls to write, as print adds newlines separately + mock_stderr.write.assert_any_call( + "Error: Git pull failed due to conflicts or other issues: Command 'cmd' returned non-zero exit status 1." + ) + mock_stderr.write.assert_any_call("\n") + mock_stderr.write.assert_any_call("Staying on the current version.") + mock_stderr.write.assert_any_call("\n") + + +def test_main_loop_with_update(mocker): + # start_proc returns the same Mock proc; we drive its poll() via side_effect + mock_start_proc = mocker.patch("autoupdater.start_proc", return_value=mocker.Mock()) + mock_start_proc.return_value.returncode = 0 + mock_stop_proc = mocker.patch("autoupdater.stop_proc") + mock_remote_updates = mocker.patch("autoupdater.remote_has_updates", side_effect=[False, True]) + mock_git_pull = mocker.patch("autoupdater.git_pull_ff_only") + mock_sleep = mocker.patch("time.sleep", return_value=None) + + def _raise(code=0): + raise SystemExit(code) + + mock_sys_exit = mocker.patch("sys.exit", side_effect=_raise) + + mock_args = mocker.Mock() + mock_args.config = "mock.yaml" + mocker.patch("autoupdater.read_args", return_value=mock_args) + + # First loop: running (None), no update + # Second loop: still running (None), update happens -> restart + # Third loop: process seen as exited (0) -> sys.exit(0) + mock_start_proc.return_value.poll.side_effect = [None, None, 0] + + with pytest.raises(SystemExit) as pytest_wrapped_e: + autoupdater.main() + assert pytest_wrapped_e.type is SystemExit + assert pytest_wrapped_e.value.code == 0 + + assert mock_sleep.call_count == 3 # before each iteration, including after restart + assert mock_remote_updates.call_count == 2 + mock_stop_proc.assert_called_once_with(mock_start_proc.return_value) + mock_git_pull.assert_called_once() + mock_start_proc.assert_called_with(config=mock.ANY) + mock_sys_exit.assert_called_once_with(0) + + +def test_main_loop_no_update(mocker): + mock_start_proc = mocker.patch("autoupdater.start_proc", return_value=mocker.Mock()) + mock_start_proc.return_value.returncode = 0 + mock_stop_proc = mocker.patch("autoupdater.stop_proc") + mock_remote_updates = mocker.patch("autoupdater.remote_has_updates", return_value=False) + mock_git_pull = mocker.patch("autoupdater.git_pull_ff_only") + mock_sleep = mocker.patch("time.sleep", return_value=None) + + def _raise(code=0): + raise SystemExit(code) + + mock_sys_exit = mocker.patch("sys.exit", side_effect=_raise) + + mock_args = mocker.Mock() + mock_args.config = "mock.yaml" + mocker.patch("autoupdater.read_args", return_value=mock_args) + + # First loop running; second loop exits -> sys.exit(0) before checking for updates + mock_start_proc.return_value.poll.side_effect = [None, 0] + + with pytest.raises(SystemExit) as pytest_wrapped_e: + autoupdater.main() + assert pytest_wrapped_e.type is SystemExit + assert pytest_wrapped_e.value.code == 0 + + assert mock_sleep.call_count == 2 + assert mock_remote_updates.call_count == 1 # second loop exits before calling this + mock_stop_proc.assert_not_called() + mock_git_pull.assert_not_called() + mock_sys_exit.assert_called_once_with(0) diff --git a/uv.lock b/uv.lock index f81505ed0..70e3f580f 100644 --- a/uv.lock +++ b/uv.lock @@ -153,6 +153,7 @@ dependencies = [ { name = "faiss-cpu" }, { name = "langchain" }, { name = "langchain-community" }, + { name = "langchain-core" }, { name = "langchain-openai" }, { name = "langchain-sandbox" }, { name = "loguru" }, @@ -162,30 +163,26 @@ dependencies = [ { name = "numpy", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "pip" }, { name = "pydantic" }, + { name = "pytest-mock" }, { name = "pyyaml" }, { name = "requests" }, { name = "rich" }, { name = "rouge" }, { name = "substrate-interface" }, { name = "tavily-python" }, + { name = "types-cachetools" }, { name = "types-netaddr" }, + { name = "types-pyyaml" }, ] [package.optional-dependencies] dev = [ - { name = "dotenv" }, - { name = "langchain" }, - { name = "langchain-core" }, - { name = "langchain-openai" }, - { name = "langchain-sandbox" }, { name = "mypy" }, { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "ruff" }, - { name = "types-cachetools" }, - { name = "types-pyyaml" }, ] [package.dev-dependencies] @@ -196,6 +193,7 @@ dev = [ { name = "pydantic" }, { name = "pytest" }, { name = "pytest-asyncio" }, + { name = "pytest-mock" }, { name = "types-pyyaml" }, ] @@ -207,16 +205,12 @@ requires-dist = [ { name = "bittensor", specifier = ">=9.7.0" }, { name = "cachetools", specifier = ">=5.0.0" }, { name = "dotenv", specifier = ">=0.9.9" }, - { name = "dotenv", marker = "extra == 'dev'", specifier = ">=0.9.9" }, { name = "faiss-cpu", specifier = ">=1.8.0" }, { name = "langchain", specifier = ">=0.3.26" }, - { name = "langchain", marker = "extra == 'dev'", specifier = ">=0.3.26" }, { name = "langchain-community", specifier = ">=0.0.59" }, - { name = "langchain-core", marker = "extra == 'dev'", specifier = ">=0.3.68" }, + { name = "langchain-core", specifier = ">=0.3.68" }, { name = "langchain-openai", specifier = ">=0.3.28" }, - { name = "langchain-openai", marker = "extra == 'dev'", specifier = ">=0.3.28" }, { name = "langchain-sandbox", specifier = ">=0.0.6" }, - { name = "langchain-sandbox", marker = "extra == 'dev'", specifier = ">=0.0.6" }, { name = "loguru", specifier = ">=0.7.3" }, { name = "macrocosmos", specifier = ">=0.1.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = "==1.17.0" }, @@ -228,6 +222,7 @@ requires-dist = [ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.4.1" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.0.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0.0" }, + { name = "pytest-mock", specifier = ">=3.14.1" }, { name = "pyyaml", specifier = ">=6.0.0" }, { name = "requests", specifier = ">=2.31.0" }, { name = "rich", specifier = ">=14.0.0" }, @@ -235,9 +230,9 @@ requires-dist = [ { name = "ruff", marker = "extra == 'dev'", specifier = "==0.12.5" }, { name = "substrate-interface", specifier = ">=1.7.11" }, { name = "tavily-python", specifier = ">=0.7.10" }, - { name = "types-cachetools", marker = "extra == 'dev'", specifier = ">=6.0.0.20250525" }, + { name = "types-cachetools", specifier = ">=6.0.0.20250525" }, { name = "types-netaddr", specifier = ">=1.3.0.20240530" }, - { name = "types-pyyaml", marker = "extra == 'dev'", specifier = ">=6.0.12.20250516" }, + { name = "types-pyyaml", specifier = ">=6.0.12.20250516" }, ] provides-extras = ["dev"] @@ -249,6 +244,7 @@ dev = [ { name = "pydantic", specifier = ">=2.11.7" }, { name = "pytest", specifier = ">=8.4.1" }, { name = "pytest-asyncio", specifier = ">=1.0.0" }, + { name = "pytest-mock", specifier = ">=3.14.1" }, { name = "types-pyyaml", specifier = ">=6.0.12.20250516" }, ] @@ -2414,6 +2410,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644 }, ] +[[package]] +name = "pytest-mock" +version = "3.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923 }, +] + [[package]] name = "python-dotenv" version = "1.1.1" diff --git a/validator.py b/validator.py index 177c58ff7..8076fec97 100644 --- a/validator.py +++ b/validator.py @@ -91,6 +91,7 @@ async def main() -> None: await chain.shutdown() await logger_db.shutdown() await miner_scorer.shutdown() + await weight_syncer.shutdown() if __name__ == "__main__":