feat(V2): pydantic settings con secret str + test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
|||||||
|
"""Pydantic Settings: legge .env e variabili d'ambiente."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pydantic import Field, SecretStr
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class _Sub(BaseSettings):
|
||||||
|
"""Base per sub-settings, condivide model_config con env_file."""
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
extra="ignore",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DeribitSettings(_Sub):
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
env_prefix="DERIBIT_",
|
||||||
|
extra="ignore",
|
||||||
|
)
|
||||||
|
client_id: str
|
||||||
|
client_secret: SecretStr
|
||||||
|
url_live: str
|
||||||
|
url_testnet: str
|
||||||
|
max_leverage: int = 3
|
||||||
|
|
||||||
|
|
||||||
|
class BybitSettings(_Sub):
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
env_prefix="BYBIT_",
|
||||||
|
extra="ignore",
|
||||||
|
)
|
||||||
|
api_key: str
|
||||||
|
api_secret: SecretStr
|
||||||
|
url_live: str
|
||||||
|
url_testnet: str
|
||||||
|
max_leverage: int = 3
|
||||||
|
|
||||||
|
|
||||||
|
class HyperliquidSettings(_Sub):
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
env_prefix="HYPERLIQUID_",
|
||||||
|
extra="ignore",
|
||||||
|
)
|
||||||
|
wallet_address: str
|
||||||
|
api_wallet_address: str
|
||||||
|
private_key: SecretStr
|
||||||
|
url_live: str
|
||||||
|
url_testnet: str
|
||||||
|
max_leverage: int = 3
|
||||||
|
|
||||||
|
|
||||||
|
class AlpacaSettings(_Sub):
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
env_prefix="ALPACA_",
|
||||||
|
extra="ignore",
|
||||||
|
)
|
||||||
|
api_key_id: str
|
||||||
|
secret_key: SecretStr
|
||||||
|
url_live: str
|
||||||
|
url_testnet: str
|
||||||
|
max_leverage: int = 1
|
||||||
|
|
||||||
|
|
||||||
|
class MacroSettings(_Sub):
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
extra="ignore",
|
||||||
|
)
|
||||||
|
fred_api_key: SecretStr
|
||||||
|
finnhub_api_key: SecretStr
|
||||||
|
|
||||||
|
|
||||||
|
class SentimentSettings(_Sub):
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
extra="ignore",
|
||||||
|
)
|
||||||
|
cryptopanic_key: SecretStr
|
||||||
|
lunarcrush_key: SecretStr
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(_Sub):
|
||||||
|
host: str = "0.0.0.0"
|
||||||
|
port: int = 9000
|
||||||
|
log_level: str = "info"
|
||||||
|
|
||||||
|
testnet_token: SecretStr
|
||||||
|
mainnet_token: SecretStr
|
||||||
|
|
||||||
|
deribit: DeribitSettings = Field(default_factory=DeribitSettings)
|
||||||
|
bybit: BybitSettings = Field(default_factory=BybitSettings)
|
||||||
|
hyperliquid: HyperliquidSettings = Field(default_factory=HyperliquidSettings)
|
||||||
|
alpaca: AlpacaSettings = Field(default_factory=AlpacaSettings)
|
||||||
|
macro: MacroSettings = Field(default_factory=MacroSettings)
|
||||||
|
sentiment: SentimentSettings = Field(default_factory=SentimentSettings)
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
def _minimal_env(**overrides) -> dict:
|
||||||
|
base = {
|
||||||
|
"TESTNET_TOKEN": "t_test_123",
|
||||||
|
"MAINNET_TOKEN": "t_live_456",
|
||||||
|
"DERIBIT_CLIENT_ID": "id",
|
||||||
|
"DERIBIT_CLIENT_SECRET": "secret",
|
||||||
|
"DERIBIT_URL_LIVE": "https://www.deribit.com/api/v2",
|
||||||
|
"DERIBIT_URL_TESTNET": "https://test.deribit.com/api/v2",
|
||||||
|
"BYBIT_API_KEY": "k",
|
||||||
|
"BYBIT_API_SECRET": "s",
|
||||||
|
"BYBIT_URL_LIVE": "https://api.bybit.com",
|
||||||
|
"BYBIT_URL_TESTNET": "https://api-testnet.bybit.com",
|
||||||
|
"HYPERLIQUID_WALLET_ADDRESS": "0xabc",
|
||||||
|
"HYPERLIQUID_API_WALLET_ADDRESS": "0xdef",
|
||||||
|
"HYPERLIQUID_PRIVATE_KEY": "0x123",
|
||||||
|
"HYPERLIQUID_URL_LIVE": "https://api.hyperliquid.xyz",
|
||||||
|
"HYPERLIQUID_URL_TESTNET": "https://api.hyperliquid-testnet.xyz",
|
||||||
|
"ALPACA_API_KEY_ID": "k",
|
||||||
|
"ALPACA_SECRET_KEY": "s",
|
||||||
|
"ALPACA_URL_LIVE": "https://api.alpaca.markets",
|
||||||
|
"ALPACA_URL_TESTNET": "https://paper-api.alpaca.markets",
|
||||||
|
"FRED_API_KEY": "x",
|
||||||
|
"FINNHUB_API_KEY": "y",
|
||||||
|
"CRYPTOPANIC_KEY": "z",
|
||||||
|
"LUNARCRUSH_KEY": "w",
|
||||||
|
}
|
||||||
|
base.update(overrides)
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
def test_settings_load_minimal(monkeypatch):
|
||||||
|
for k, v in _minimal_env().items():
|
||||||
|
monkeypatch.setenv(k, v)
|
||||||
|
monkeypatch.setenv("PORT", "9123")
|
||||||
|
|
||||||
|
from cerbero_mcp.settings import Settings
|
||||||
|
|
||||||
|
s = Settings()
|
||||||
|
assert s.port == 9123
|
||||||
|
assert s.host == "0.0.0.0"
|
||||||
|
assert s.testnet_token.get_secret_value() == "t_test_123"
|
||||||
|
assert s.mainnet_token.get_secret_value() == "t_live_456"
|
||||||
|
assert s.deribit.url_testnet.endswith("test.deribit.com/api/v2")
|
||||||
|
assert s.bybit.max_leverage == 3
|
||||||
|
assert s.alpaca.max_leverage == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_settings_missing_token_fails(monkeypatch):
|
||||||
|
env = _minimal_env()
|
||||||
|
env.pop("TESTNET_TOKEN")
|
||||||
|
for k, v in env.items():
|
||||||
|
monkeypatch.setenv(k, v)
|
||||||
|
monkeypatch.delenv("TESTNET_TOKEN", raising=False)
|
||||||
|
|
||||||
|
from cerbero_mcp.settings import Settings
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
Settings()
|
||||||
|
|
||||||
|
|
||||||
|
def test_settings_extras_ignored(monkeypatch):
|
||||||
|
for k, v in _minimal_env().items():
|
||||||
|
monkeypatch.setenv(k, v)
|
||||||
|
monkeypatch.setenv("UNRELATED_VAR", "ignored")
|
||||||
|
|
||||||
|
from cerbero_mcp.settings import Settings
|
||||||
|
|
||||||
|
s = Settings()
|
||||||
|
assert s.testnet_token.get_secret_value() == "t_test_123"
|
||||||
|
|
||||||
|
|
||||||
|
def test_settings_secret_str_no_leak(monkeypatch):
|
||||||
|
for k, v in _minimal_env().items():
|
||||||
|
monkeypatch.setenv(k, v)
|
||||||
|
|
||||||
|
from cerbero_mcp.settings import Settings
|
||||||
|
|
||||||
|
s = Settings()
|
||||||
|
assert "t_test_123" not in repr(s)
|
||||||
|
assert "t_live_456" not in repr(s)
|
||||||
Reference in New Issue
Block a user