feat: assert minimum hyperliquid-python-sdk version at bot startup#155
Conversation
If a stale venv ends up with an older SDK than what pyproject.toml pins, the bot can otherwise crash deep inside the SDK during exchange initialization with an opaque traceback. Validating the installed version up front turns that into a single, actionable error message and prevents watchdog crash loops on a deployment that is silently out of sync. The minimum is a code-level invariant (like `requires-python`) rather than a runtime-tunable parameter, so it is kept as a module-scope constant in `bot.py` and bumped in lockstep with the pyproject pin. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
keitaj
left a comment
There was a problem hiding this comment.
Review: SDK minimum version startup gate
Small, focused PR with a clear motivation (fail fast on SDK drift instead of dying inside the SDK with an opaque traceback). The gate is properly placed before any hyperliquid.* import, the constant is paired with a test that pins it to pyproject.toml, and the message text gives the operator a directly-usable remediation command. CI is clean (flake8 + 1108 tests). No blockers.
1. Lower-bound construction has unreachable but incorrect branch (nit)
tests/test_sdk_version_check.py L141-L147 (in test_below_minimum_exits_with_status_2):
else:
lower = f"{major - 1}.0.0" if major > 0 else "0.0.0rc1" # pragma: no coverIf MINIMUM_HYPERLIQUID_SDK_VERSION ever became (0, 0, 0), the fallback "0.0.0rc1" parses to (0, 0, 0) (suffixes are stripped) — i.e. equal to the minimum, not strictly less. The pragma: no cover keeps coverage clean, but the branch would silently produce a false positive if it ever fired.
Suggestion: since (0, 0, 0) is not a sensible minimum, drop the unreachable arm entirely and assert in the test setup that the minimum is non-zero:
major, minor, patch_ = gate.MINIMUM_HYPERLIQUID_SDK_VERSION
assert (major, minor, patch_) > (0, 0, 0), "minimum must be non-zero for this test"
if patch_ > 0:
lower = f"{major}.{minor}.{patch_ - 1}"
elif minor > 0:
lower = f"{major}.{minor - 1}.0"
else:
lower = f"{major - 1}.0.0" # major > 0 guaranteed2. Module-scope invocation imposes an import-time side effect (nit)
bot.py L77:
_assert_sdk_version()Any tool that imports bot (including future tests or scripts that want to reuse a helper from bot.py) will trip this gate. That's the intended behavior for the bot entry point, and the test file handles it by slicing bot.py's source — but it's a non-obvious constraint.
Suggestion: add a one-liner near the call site noting that the assert runs at import time, so a future contributor importing bot for its helpers isn't surprised:
# Runs at import time so any process that loads bot.py (including the bot
# itself under watchdog) is gated. Tests that need access to the helpers
# without triggering the assert load the gate block standalone.
_assert_sdk_version()3. Minor DRY: min_str formatted three times (nit)
bot.py L55, L65: min_str = ".".join(str(n) for n in MINIMUM_HYPERLIQUID_SDK_VERSION) appears in both error branches. Not worth a dedicated helper for two call sites, but a module-scope constant MINIMUM_HYPERLIQUID_SDK_VERSION_STR would let the message and the constant stay in lockstep without rebuilding the string each time. Optional.
Verdict: LGTM
Three nits, no blockers and no should-fix. Author may merge as-is or address #1 in a follow-up; the unreachable branch is not exercised under the current minimum. CI passes locally (full flake8 clean, 1108 tests green including 16 new ones).
Summary
hyperliquid-python-sdkversion at startup, before anyhyperliquid.*import. If it is below the required minimum (currently0.23.0), exit immediately with a clear remediation message instead of crashing later inside the SDK with an opaque traceback.MINIMUM_HYPERLIQUID_SDK_VERSION) and is bumped in lockstep with thepyproject.tomlpin. It is treated as a code-level invariant (analogous torequires-python), not a runtime-tunable parameter, so it is not exposed via config.pyproject.toml, the SDK can fail on upstream API metadata changes deep insideExchange.__init__. A fast, named failure at startup avoids opaque crash loops and points the operator straight at the fix.Changes
bot.py— AddMINIMUM_HYPERLIQUID_SDK_VERSION,_parse_version_tuple,_assert_sdk_version. The guard runs immediately aftersetup_logging()and before the firsthyperliquid.*import.tests/test_sdk_version_check.py— Unit tests for the version parser (standard, pre-release suffixes, padding, edge cases) and the gate (pass / exit / missing-package). One invariant test asserts the gate's minimum is consistent with the pin inpyproject.toml, so future SDK bumps cannot drift between the two.Test plan
tests/test_sdk_version_check.py, 16 cases)flake8passes (full project)pytestpasses (full project, 1108 tests)MINIMUM_HYPERLIQUID_SDK_VERSIONmatches thehyperliquid-python-sdk==X.Y.Zpin inpyproject.toml🤖 Generated with Claude Code