4d9db750be
- pyproject.toml: ruff target-version py311 → py313 (auto-fix 42 lint warnings via UP rules); aggiunto consider_namespace_packages = true che risolve la collisione conftest tra servizi e permette di lanciare pytest sull'intera suite cross-servizio. - mcp_common.audit: nuovo helper audit_write_op() con logger dedicato mcp.audit. Wirato su tutti i write endpoint di deribit, bybit, alpaca e hyperliquid (place_order, place_combo_order, cancel_*, set_*, close_*, transfer_*, switch_*, amend_*) con principal + target + payload non-sensibile + result summarizzato. - mcp_common.app_factory: ExchangeAppSpec + run_exchange_main() centralizza il boilerplate dei __main__.py (configure_root_logging, fail_fast_if_missing, summarize, load creds, resolve_environment, load token store, uvicorn). I 4 __main__.py exchange ridotti da ~60 LOC ognuno a ~25 LOC dichiarativi. mcp_common.env_validation promosso da mcp_deribit (mantenuto re-export shim per back-compat test_env_validation). - 8 test nuovi (4 audit + 4 app_factory). Suite full: 450/450 verdi. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
70 lines
2.0 KiB
Python
70 lines
2.0 KiB
Python
"""Env validation policy: fail-fast per mandatory, soft per optional.
|
|
|
|
Usage al boot di ogni mcp `__main__.py`:
|
|
|
|
from mcp_common.env_validation import require_env, optional_env, summarize
|
|
|
|
creds_file = require_env("CREDENTIALS_FILE", "deribit credentials JSON path")
|
|
host = optional_env("HOST", default="0.0.0.0")
|
|
summarize(["CREDENTIALS_FILE", "HOST", "PORT"])
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import sys
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class MissingEnvError(RuntimeError):
|
|
"""Mandatory env var absent or empty."""
|
|
|
|
|
|
def require_env(name: str, description: str = "") -> str:
|
|
val = (os.environ.get(name) or "").strip()
|
|
if not val:
|
|
msg = f"missing mandatory env var: {name}"
|
|
if description:
|
|
msg += f" ({description})"
|
|
logger.error(msg)
|
|
raise MissingEnvError(msg)
|
|
return val
|
|
|
|
|
|
def optional_env(name: str, *, default: str = "") -> str:
|
|
val = (os.environ.get(name) or "").strip()
|
|
if not val:
|
|
if default:
|
|
logger.info("env %s not set, using default=%r", name, default)
|
|
return default
|
|
return val
|
|
|
|
|
|
def summarize(names: list[str]) -> None:
|
|
sensitive_tokens = ("SECRET", "KEY", "TOKEN", "PASSWORD", "CREDENTIAL", "WALLET")
|
|
for n in names:
|
|
val = os.environ.get(n)
|
|
if val is None:
|
|
logger.info("env[%s]: <unset>", n)
|
|
continue
|
|
if any(t in n.upper() for t in sensitive_tokens):
|
|
logger.info("env[%s]: <set, %d chars>", n, len(val))
|
|
else:
|
|
logger.info("env[%s]: %s", n, val)
|
|
|
|
|
|
def fail_fast_if_missing(names: list[str]) -> None:
|
|
missing: list[str] = []
|
|
for n in names:
|
|
if not (os.environ.get(n) or "").strip():
|
|
missing.append(n)
|
|
if missing:
|
|
logger.error("boot aborted: missing mandatory env vars: %s", missing)
|
|
print(
|
|
f"FATAL: missing mandatory env vars: {missing}",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(2)
|