feat(V2): Deribit credenziali per env (CLIENT_ID/SECRET _TESTNET / _LIVE)
DeribitSettings ora supporta coppie credenziali distinte per testnet e mainnet via DERIBIT_CLIENT_ID_TESTNET/_LIVE e DERIBIT_CLIENT_SECRET_TESTNET/_LIVE. Le coppie env-specifiche prevalgono sulla coppia base DERIBIT_CLIENT_ID/DERIBIT_CLIENT_SECRET (mantenuta per backward compat). build_client risolve la coppia giusta tramite settings.deribit.credentials(env); ValueError esplicito se nessuna coppia configurata per l'env richiesto. +4 test (legacy single, per-env, override, missing). Fix anche isolation da .env reale via monkeypatch.chdir(tmp_path). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,8 +17,14 @@ TESTNET_TOKEN=
|
|||||||
MAINNET_TOKEN=
|
MAINNET_TOKEN=
|
||||||
|
|
||||||
# ─── EXCHANGE — DERIBIT ───────────────────────────────────
|
# ─── EXCHANGE — DERIBIT ───────────────────────────────────
|
||||||
|
# Coppia singola (usata sia per testnet sia per mainnet):
|
||||||
DERIBIT_CLIENT_ID=
|
DERIBIT_CLIENT_ID=
|
||||||
DERIBIT_CLIENT_SECRET=
|
DERIBIT_CLIENT_SECRET=
|
||||||
|
# Oppure coppie distinte per env (prevalgono se valorizzate):
|
||||||
|
# DERIBIT_CLIENT_ID_TESTNET=
|
||||||
|
# DERIBIT_CLIENT_SECRET_TESTNET=
|
||||||
|
# DERIBIT_CLIENT_ID_LIVE=
|
||||||
|
# DERIBIT_CLIENT_SECRET_LIVE=
|
||||||
DERIBIT_URL_LIVE=https://www.deribit.com/api/v2
|
DERIBIT_URL_LIVE=https://www.deribit.com/api/v2
|
||||||
DERIBIT_URL_TESTNET=https://test.deribit.com/api/v2
|
DERIBIT_URL_TESTNET=https://test.deribit.com/api/v2
|
||||||
DERIBIT_MAX_LEVERAGE=3
|
DERIBIT_MAX_LEVERAGE=3
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ async def build_client(
|
|||||||
from cerbero_mcp.exchanges.deribit.client import DeribitClient
|
from cerbero_mcp.exchanges.deribit.client import DeribitClient
|
||||||
|
|
||||||
url = settings.deribit.url_testnet if env == "testnet" else settings.deribit.url_live
|
url = settings.deribit.url_testnet if env == "testnet" else settings.deribit.url_live
|
||||||
|
cid, csec = settings.deribit.credentials(env)
|
||||||
return DeribitClient(
|
return DeribitClient(
|
||||||
client_id=settings.deribit.client_id,
|
client_id=cid,
|
||||||
client_secret=settings.deribit.client_secret.get_secret_value(),
|
client_secret=csec,
|
||||||
testnet=(env == "testnet"),
|
testnet=(env == "testnet"),
|
||||||
base_url_override=url,
|
base_url_override=url,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,12 +21,33 @@ class DeribitSettings(_Sub):
|
|||||||
env_prefix="DERIBIT_",
|
env_prefix="DERIBIT_",
|
||||||
extra="ignore",
|
extra="ignore",
|
||||||
)
|
)
|
||||||
client_id: str
|
client_id: str | None = None
|
||||||
client_secret: SecretStr
|
client_secret: SecretStr | None = None
|
||||||
|
client_id_testnet: str | None = None
|
||||||
|
client_secret_testnet: SecretStr | None = None
|
||||||
|
client_id_live: str | None = None
|
||||||
|
client_secret_live: SecretStr | None = None
|
||||||
url_live: str
|
url_live: str
|
||||||
url_testnet: str
|
url_testnet: str
|
||||||
max_leverage: int = 3
|
max_leverage: int = 3
|
||||||
|
|
||||||
|
def credentials(self, env: str) -> tuple[str, str]:
|
||||||
|
"""Return (client_id, client_secret) for the given env.
|
||||||
|
Prefers env-specific (_TESTNET / _LIVE) pair; falls back to base
|
||||||
|
(DERIBIT_CLIENT_ID / DERIBIT_CLIENT_SECRET) for legacy single-pair setups.
|
||||||
|
"""
|
||||||
|
if env == "testnet":
|
||||||
|
cid = self.client_id_testnet or self.client_id
|
||||||
|
csec = self.client_secret_testnet or self.client_secret
|
||||||
|
elif env == "mainnet":
|
||||||
|
cid = self.client_id_live or self.client_id
|
||||||
|
csec = self.client_secret_live or self.client_secret
|
||||||
|
else:
|
||||||
|
raise ValueError(f"unknown deribit env: {env}")
|
||||||
|
if not cid or csec is None:
|
||||||
|
raise ValueError(f"Deribit credentials not configured for env={env}")
|
||||||
|
return cid, csec.get_secret_value()
|
||||||
|
|
||||||
|
|
||||||
class BybitSettings(_Sub):
|
class BybitSettings(_Sub):
|
||||||
model_config = SettingsConfigDict(
|
model_config = SettingsConfigDict(
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ def test_settings_load_minimal(monkeypatch):
|
|||||||
assert s.alpaca.max_leverage == 1
|
assert s.alpaca.max_leverage == 1
|
||||||
|
|
||||||
|
|
||||||
def test_settings_missing_token_fails(monkeypatch):
|
def test_settings_missing_token_fails(monkeypatch, tmp_path):
|
||||||
|
monkeypatch.chdir(tmp_path) # isola dal .env reale del working dir
|
||||||
env = _minimal_env()
|
env = _minimal_env()
|
||||||
env.pop("TESTNET_TOKEN")
|
env.pop("TESTNET_TOKEN")
|
||||||
for k, v in env.items():
|
for k, v in env.items():
|
||||||
@@ -84,3 +85,73 @@ def test_settings_secret_str_no_leak(monkeypatch):
|
|||||||
s = Settings()
|
s = Settings()
|
||||||
assert "t_test_123" not in repr(s)
|
assert "t_test_123" not in repr(s)
|
||||||
assert "t_live_456" not in repr(s)
|
assert "t_live_456" not in repr(s)
|
||||||
|
|
||||||
|
|
||||||
|
def _isolated(monkeypatch, tmp_path, env: dict) -> None:
|
||||||
|
"""Isola Settings dal .env reale in working dir e setta solo env passato."""
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
for k in (
|
||||||
|
"DERIBIT_CLIENT_ID", "DERIBIT_CLIENT_SECRET",
|
||||||
|
"DERIBIT_CLIENT_ID_TESTNET", "DERIBIT_CLIENT_SECRET_TESTNET",
|
||||||
|
"DERIBIT_CLIENT_ID_LIVE", "DERIBIT_CLIENT_SECRET_LIVE",
|
||||||
|
):
|
||||||
|
monkeypatch.delenv(k, raising=False)
|
||||||
|
for k, v in env.items():
|
||||||
|
monkeypatch.setenv(k, v)
|
||||||
|
|
||||||
|
|
||||||
|
def test_deribit_credentials_legacy_single_pair(monkeypatch, tmp_path):
|
||||||
|
"""Solo DERIBIT_CLIENT_ID/SECRET → entrambi gli env usano la stessa coppia."""
|
||||||
|
_isolated(monkeypatch, tmp_path, _minimal_env())
|
||||||
|
|
||||||
|
from cerbero_mcp.settings import Settings
|
||||||
|
|
||||||
|
s = Settings()
|
||||||
|
assert s.deribit.credentials("testnet") == ("id", "secret")
|
||||||
|
assert s.deribit.credentials("mainnet") == ("id", "secret")
|
||||||
|
|
||||||
|
|
||||||
|
def test_deribit_credentials_per_env_pairs(monkeypatch, tmp_path):
|
||||||
|
"""Coppie _TESTNET e _LIVE → ognuna serve l'env corrispondente."""
|
||||||
|
env = _minimal_env()
|
||||||
|
env.pop("DERIBIT_CLIENT_ID")
|
||||||
|
env.pop("DERIBIT_CLIENT_SECRET")
|
||||||
|
env["DERIBIT_CLIENT_ID_TESTNET"] = "tid"
|
||||||
|
env["DERIBIT_CLIENT_SECRET_TESTNET"] = "tsec"
|
||||||
|
env["DERIBIT_CLIENT_ID_LIVE"] = "lid"
|
||||||
|
env["DERIBIT_CLIENT_SECRET_LIVE"] = "lsec"
|
||||||
|
_isolated(monkeypatch, tmp_path, env)
|
||||||
|
|
||||||
|
from cerbero_mcp.settings import Settings
|
||||||
|
|
||||||
|
s = Settings()
|
||||||
|
assert s.deribit.credentials("testnet") == ("tid", "tsec")
|
||||||
|
assert s.deribit.credentials("mainnet") == ("lid", "lsec")
|
||||||
|
|
||||||
|
|
||||||
|
def test_deribit_credentials_env_specific_overrides_fallback(monkeypatch, tmp_path):
|
||||||
|
"""_LIVE presente prevale sulla coppia base anche se entrambe configurate."""
|
||||||
|
env = _minimal_env()
|
||||||
|
env["DERIBIT_CLIENT_ID_LIVE"] = "lid"
|
||||||
|
env["DERIBIT_CLIENT_SECRET_LIVE"] = "lsec"
|
||||||
|
_isolated(monkeypatch, tmp_path, env)
|
||||||
|
|
||||||
|
from cerbero_mcp.settings import Settings
|
||||||
|
|
||||||
|
s = Settings()
|
||||||
|
assert s.deribit.credentials("mainnet") == ("lid", "lsec")
|
||||||
|
assert s.deribit.credentials("testnet") == ("id", "secret") # fallback
|
||||||
|
|
||||||
|
|
||||||
|
def test_deribit_credentials_missing_raises(monkeypatch, tmp_path):
|
||||||
|
"""Nessuna coppia configurata → ValueError esplicito."""
|
||||||
|
env = _minimal_env()
|
||||||
|
env.pop("DERIBIT_CLIENT_ID")
|
||||||
|
env.pop("DERIBIT_CLIENT_SECRET")
|
||||||
|
_isolated(monkeypatch, tmp_path, env)
|
||||||
|
|
||||||
|
from cerbero_mcp.settings import Settings
|
||||||
|
|
||||||
|
s = Settings()
|
||||||
|
with pytest.raises(ValueError, match="not configured for env=mainnet"):
|
||||||
|
s.deribit.credentials("mainnet")
|
||||||
|
|||||||
Reference in New Issue
Block a user