Harden env save against empty numerics; default coin margin and hedge off.
Reject blank float/int/select on save and parse env with safe helpers so options config edits cannot crash into 502; default OKX_TRADE_MODE=options and OKX_OPTIONS_MARGIN_MODE=coin. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+2
-2
@@ -156,8 +156,8 @@ OKX_OPTIONS_OPEN_FILL_TIMEOUT_SEC=12
|
||||
# =============================================================================
|
||||
# 以下三项已由 OKX_TRADE_MODE 取代,保留兼容旧部署(未配置 TRADE_MODE 时仍可读)
|
||||
HEDGE_PLAN_ENABLED=false
|
||||
HEDGE_PLAN_SHOW_PERP_OPTIONS=true
|
||||
HEDGE_PLAN_SHOW_OPTIONS_OPTIONS=true
|
||||
HEDGE_PLAN_SHOW_PERP_OPTIONS=false
|
||||
HEDGE_PLAN_SHOW_OPTIONS_OPTIONS=false
|
||||
HEDGE_PLAN_LIVE_ORDER=false
|
||||
# 永期子模式:true=以期权为主;false=保险模式(页面标题前标识,不可页内切换)
|
||||
HEDGE_PLAN_OPTION_PRIMARY=true
|
||||
|
||||
@@ -258,19 +258,41 @@ if not AUTH_DISABLED and (not USERNAME or not PASSWORD):
|
||||
WECHAT_WEBHOOK = os.getenv("WECHAT_WEBHOOK", "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=replace-me")
|
||||
SYSTEM_TYPE = "CRYPTO"
|
||||
HOST = os.getenv("APP_HOST", "0.0.0.0")
|
||||
PORT = int(os.getenv("APP_PORT", "5000"))
|
||||
|
||||
|
||||
def _env_float(name: str, default: float) -> float:
|
||||
try:
|
||||
raw = os.getenv(name)
|
||||
if raw is None or str(raw).strip() == "":
|
||||
return float(default)
|
||||
return float(raw)
|
||||
except (TypeError, ValueError):
|
||||
return float(default)
|
||||
|
||||
|
||||
def _env_int(name: str, default: int) -> int:
|
||||
try:
|
||||
raw = os.getenv(name)
|
||||
if raw is None or str(raw).strip() == "":
|
||||
return int(default)
|
||||
return int(float(raw))
|
||||
except (TypeError, ValueError):
|
||||
return int(default)
|
||||
|
||||
|
||||
PORT = _env_int("APP_PORT", 5000)
|
||||
DEBUG = os.getenv("APP_DEBUG", "false").lower() == "true"
|
||||
DB_PATH = resolve_path(os.getenv("DB_PATH", "crypto.db"))
|
||||
|
||||
# 训练参数(可由 .env 覆盖)
|
||||
TOTAL_CAPITAL = float(os.getenv("TOTAL_CAPITAL", "100"))
|
||||
DAILY_START_CAPITAL = float(os.getenv("DAILY_START_CAPITAL", "30"))
|
||||
DAILY_LOSS_CAPITAL = float(os.getenv("DAILY_LOSS_CAPITAL", "20"))
|
||||
DAILY_PROFIT_CAPITAL = float(os.getenv("DAILY_PROFIT_CAPITAL", "50"))
|
||||
BTC_LEVERAGE = int(os.getenv("BTC_LEVERAGE", "10"))
|
||||
ALT_LEVERAGE = int(os.getenv("ALT_LEVERAGE", "5"))
|
||||
TOTAL_CAPITAL = _env_float("TOTAL_CAPITAL", 100)
|
||||
DAILY_START_CAPITAL = _env_float("DAILY_START_CAPITAL", 30)
|
||||
DAILY_LOSS_CAPITAL = _env_float("DAILY_LOSS_CAPITAL", 20)
|
||||
DAILY_PROFIT_CAPITAL = _env_float("DAILY_PROFIT_CAPITAL", 50)
|
||||
BTC_LEVERAGE = _env_int("BTC_LEVERAGE", 10)
|
||||
ALT_LEVERAGE = _env_int("ALT_LEVERAGE", 5)
|
||||
# 交易日滚动与「可开仓」整点:按应用本地时区 wall clock(默认北京时间 UTC+8)
|
||||
TRADING_DAY_RESET_HOUR = int(os.getenv("TRADING_DAY_RESET_HOUR", "8"))
|
||||
TRADING_DAY_RESET_HOUR = _env_int("TRADING_DAY_RESET_HOUR", 8)
|
||||
TRADING_DAY_RESET_OPEN_GUARD_ENABLED = os.getenv(
|
||||
"TRADING_DAY_RESET_OPEN_GUARD_ENABLED", "true"
|
||||
).lower() in ("1", "true", "yes", "on")
|
||||
@@ -310,47 +332,45 @@ OKX_API_KEY = os.getenv("OKX_API_KEY", "")
|
||||
OKX_API_SECRET = os.getenv("OKX_API_SECRET", "")
|
||||
OKX_API_PASSPHRASE = os.getenv("OKX_API_PASSPHRASE", "")
|
||||
OKX_OPTIONS_ENABLED = os.getenv("OKX_OPTIONS_ENABLED", "true").lower() in ("1", "true", "yes", "on")
|
||||
OKX_OPTIONS_TRADE_BUDGET_USDC = float(os.getenv("OKX_OPTIONS_TRADE_BUDGET_USDC", "10"))
|
||||
OKX_OPTIONS_TRADE_BUDGET_USDC = _env_float("OKX_OPTIONS_TRADE_BUDGET_USDC", 10)
|
||||
OKX_OPTIONS_DEFAULT_UNDERLY = (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper()
|
||||
OKX_TD_MODE = os.getenv("OKX_TD_MODE", "cross")
|
||||
OKX_POS_MODE = os.getenv("OKX_POS_MODE", "hedge")
|
||||
EXCHANGE_DISPLAY_NAME = (os.getenv("EXCHANGE_DISPLAY_NAME") or "OKX").strip() or "OKX"
|
||||
BALANCE_REFRESH_SECONDS = int(os.getenv("BALANCE_REFRESH_SECONDS", "60"))
|
||||
PRICE_REFRESH_SECONDS = int(os.getenv("PRICE_REFRESH_SECONDS", "5"))
|
||||
KEY_ALERT_MAX_TIMES = int(os.getenv("KEY_ALERT_MAX_TIMES", "3"))
|
||||
KEY_ALERT_INTERVAL_MINUTES = int(os.getenv("KEY_ALERT_INTERVAL_MINUTES", "5"))
|
||||
KEY_BREAKOUT_LIMIT_PCT = float(os.getenv("KEY_BREAKOUT_LIMIT_PCT", "1.5"))
|
||||
BALANCE_REFRESH_SECONDS = _env_int("BALANCE_REFRESH_SECONDS", 60)
|
||||
PRICE_REFRESH_SECONDS = _env_int("PRICE_REFRESH_SECONDS", 5)
|
||||
KEY_ALERT_MAX_TIMES = _env_int("KEY_ALERT_MAX_TIMES", 3)
|
||||
KEY_ALERT_INTERVAL_MINUTES = _env_int("KEY_ALERT_INTERVAL_MINUTES", 5)
|
||||
KEY_BREAKOUT_LIMIT_PCT = _env_float("KEY_BREAKOUT_LIMIT_PCT", 1.5)
|
||||
AUTO_TRANSFER_ENABLED = os.getenv("AUTO_TRANSFER_ENABLED", "false").lower() == "true"
|
||||
AUTO_TRANSFER_AMOUNT = float(os.getenv("AUTO_TRANSFER_AMOUNT", "30"))
|
||||
AUTO_TRANSFER_AMOUNT = _env_float("AUTO_TRANSFER_AMOUNT", 30)
|
||||
AUTO_TRANSFER_FROM = os.getenv("AUTO_TRANSFER_FROM", "funding")
|
||||
AUTO_TRANSFER_TO = os.getenv("AUTO_TRANSFER_TO", "swap")
|
||||
FORCE_CLOSE_ENABLED = os.getenv("FORCE_CLOSE_ENABLED", "false").lower() == "true"
|
||||
FORCE_CLOSE_BJ_HOUR = int(os.getenv("FORCE_CLOSE_BJ_HOUR", "0"))
|
||||
FORCE_CLOSE_BJ_HOUR = _env_int("FORCE_CLOSE_BJ_HOUR", 0)
|
||||
# 自动划转:仅在北京时间该整点「小时」内尝试;transfer_logs.transfer_day 存 UTC 自然日(与 OKX 日界一致便于对账)
|
||||
AUTO_TRANSFER_BJ_HOUR = int(os.getenv("AUTO_TRANSFER_BJ_HOUR", "8"))
|
||||
AUTO_TRANSFER_BJ_HOUR = _env_int("AUTO_TRANSFER_BJ_HOUR", 8)
|
||||
POSITION_SIZING_MODE = load_position_sizing_mode()
|
||||
TRADE_POLICY = load_trade_policy()
|
||||
WECHAT_TIMEOUT_SECONDS = int(os.getenv("WECHAT_TIMEOUT_SECONDS", "10"))
|
||||
MONITOR_POLL_SECONDS = int(os.getenv("MONITOR_POLL_SECONDS", "3"))
|
||||
RECONCILE_STARTUP_GRACE_SEC = int(os.getenv("RECONCILE_STARTUP_GRACE_SEC", "90"))
|
||||
RECONCILE_FLAT_CONFIRM_POLLS = max(1, int(os.getenv("RECONCILE_FLAT_CONFIRM_POLLS", "3")))
|
||||
WECHAT_TIMEOUT_SECONDS = _env_int("WECHAT_TIMEOUT_SECONDS", 10)
|
||||
MONITOR_POLL_SECONDS = _env_int("MONITOR_POLL_SECONDS", 3)
|
||||
RECONCILE_STARTUP_GRACE_SEC = _env_int("RECONCILE_STARTUP_GRACE_SEC", 90)
|
||||
RECONCILE_FLAT_CONFIRM_POLLS = max(1, _env_int("RECONCILE_FLAT_CONFIRM_POLLS", 3))
|
||||
_APP_STARTED_AT = time.time()
|
||||
_RECONCILE_FLAT_STREAK = {}
|
||||
BREAKEVEN_EXCHANGE_MIN_INTERVAL_SEC = max(
|
||||
15, int(os.getenv("BREAKEVEN_EXCHANGE_MIN_INTERVAL_SEC", "60"))
|
||||
)
|
||||
BREAKEVEN_EXCHANGE_MIN_INTERVAL_SEC = max(15, _env_int("BREAKEVEN_EXCHANGE_MIN_INTERVAL_SEC", 60))
|
||||
_BREAKEVEN_LAST_EX_SYNC: dict[int, float] = {}
|
||||
KLINE_TIMEFRAME = os.getenv("KLINE_TIMEFRAME", "5m")
|
||||
FULL_MARGIN_BUFFER_RATIO = float(os.getenv("FULL_MARGIN_BUFFER_RATIO") or "0.98")
|
||||
FULL_MARGIN_BUFFER_RATIO = _env_float("FULL_MARGIN_BUFFER_RATIO", 0.98)
|
||||
TRANSFER_CCY = (os.getenv("TRANSFER_CCY", "USDT") or "USDT").strip().upper() or "USDT"
|
||||
OKX_POSITION_INST_TYPE = os.getenv("OKX_POSITION_INST_TYPE", "SWAP")
|
||||
EXCHANGE_POSITION_SYNC_FROM_BJ = (os.getenv("EXCHANGE_POSITION_SYNC_FROM_BJ") or "").strip()
|
||||
EXCHANGE_POSITION_HISTORY_LIMIT = max(50, min(1000, int(os.getenv("EXCHANGE_POSITION_HISTORY_LIMIT", "200"))))
|
||||
EXCHANGE_POSITION_HISTORY_LIMIT = max(50, min(1000, _env_int("EXCHANGE_POSITION_HISTORY_LIMIT", 200)))
|
||||
_LAST_EXCHANGE_PNL_SYNC_AT = 0.0
|
||||
UPLOAD_FOLDER = resolve_path(os.getenv("UPLOAD_DIR", "static/images"))
|
||||
ORDER_CHART_ENABLED = os.getenv("ORDER_CHART_ENABLED", "true").lower() == "true"
|
||||
ORDER_CHART_TFS = [x.strip() for x in (os.getenv("ORDER_CHART_TFS", "4h,1h,15m,5m") or "").split(",") if x.strip()]
|
||||
ORDER_CHART_LIMIT = int(os.getenv("ORDER_CHART_LIMIT", "100"))
|
||||
ORDER_CHART_LIMIT = _env_int("ORDER_CHART_LIMIT", 100)
|
||||
ORDER_CHART_DIR = resolve_path(os.getenv("ORDER_CHART_DIR", "static/images/order_charts"))
|
||||
from lib.trade.daily_open_limit_lib import (
|
||||
build_daily_open_alert_prompt,
|
||||
@@ -364,25 +384,25 @@ from lib.trade.daily_open_limit_lib import (
|
||||
)
|
||||
|
||||
DAILY_OPEN_ALERT_THRESHOLD, DAILY_OPEN_HARD_LIMIT = load_daily_open_limits_from_env()
|
||||
RISK_PERCENT = float(os.getenv("RISK_PERCENT") or "2")
|
||||
BREAKEVEN_RR_TRIGGER = float(os.getenv("BREAKEVEN_RR_TRIGGER") or "1.0")
|
||||
BREAKEVEN_OFFSET_PCT = float(os.getenv("BREAKEVEN_OFFSET_PCT") or "0.02")
|
||||
BREAKEVEN_STEP_R = float(os.getenv("BREAKEVEN_STEP_R") or "1.0")
|
||||
RISK_PERCENT = _env_float("RISK_PERCENT", 2)
|
||||
BREAKEVEN_RR_TRIGGER = _env_float("BREAKEVEN_RR_TRIGGER", 1.0)
|
||||
BREAKEVEN_OFFSET_PCT = _env_float("BREAKEVEN_OFFSET_PCT", 0.02)
|
||||
BREAKEVEN_STEP_R = _env_float("BREAKEVEN_STEP_R", 1.0)
|
||||
ORDER_MONITOR_TYPE_MANUAL = "下单监控"
|
||||
ORDER_MONITOR_TYPE_KEY_AUTO = "关键位监控"
|
||||
KEY_AUTO_MIN_PLANNED_RR = float(os.getenv("KEY_AUTO_MIN_PLANNED_RR") or "1.5")
|
||||
KEY_STOP_OUTSIDE_BREAKOUT_PCT = float(os.getenv("KEY_STOP_OUTSIDE_BREAKOUT_PCT") or "0.5")
|
||||
KEY_TREND_STOP_OUTSIDE_PCT = float(os.getenv("KEY_TREND_STOP_OUTSIDE_PCT") or "1")
|
||||
KEY_DAILY_VOLUME_RANK_MAX = max(1, int(os.getenv("KEY_DAILY_VOLUME_RANK_MAX") or "30"))
|
||||
KEY_AUTO_MIN_PLANNED_RR = _env_float("KEY_AUTO_MIN_PLANNED_RR", 1.5)
|
||||
KEY_STOP_OUTSIDE_BREAKOUT_PCT = _env_float("KEY_STOP_OUTSIDE_BREAKOUT_PCT", 0.5)
|
||||
KEY_TREND_STOP_OUTSIDE_PCT = _env_float("KEY_TREND_STOP_OUTSIDE_PCT", 1)
|
||||
KEY_DAILY_VOLUME_RANK_MAX = max(1, _env_int("KEY_DAILY_VOLUME_RANK_MAX", 30))
|
||||
|
||||
MANUAL_MIN_PLANNED_RR = float(os.getenv("MANUAL_MIN_PLANNED_RR") or "1.4")
|
||||
MAX_ACTIVE_POSITIONS = max(1, int(os.getenv("MAX_ACTIVE_POSITIONS") or "1"))
|
||||
KEY_VOLUME_MA_BARS = max(1, int(os.getenv("KEY_VOLUME_MA_BARS") or "20"))
|
||||
KEY_VOLUME_RATIO_MIN = float(os.getenv("KEY_VOLUME_RATIO_MIN") or "1.3")
|
||||
KEY_BREAKOUT_AMP_MIN_PCT = float(os.getenv("KEY_BREAKOUT_AMP_MIN_PCT") or "0.03")
|
||||
KEY_BREAKOUT_AMP_MAX_PCT = float(os.getenv("KEY_BREAKOUT_AMP_MAX_PCT") or "0.5")
|
||||
KEY_CONFIRM_BREAKOUT_BAR = int(os.getenv("KEY_CONFIRM_BREAKOUT_BAR") or "-2")
|
||||
KEY_CONFIRM_BAR = int(os.getenv("KEY_CONFIRM_BAR") or "-1")
|
||||
MANUAL_MIN_PLANNED_RR = _env_float("MANUAL_MIN_PLANNED_RR", 1.4)
|
||||
MAX_ACTIVE_POSITIONS = max(1, _env_int("MAX_ACTIVE_POSITIONS", 1))
|
||||
KEY_VOLUME_MA_BARS = max(1, _env_int("KEY_VOLUME_MA_BARS", 20))
|
||||
KEY_VOLUME_RATIO_MIN = _env_float("KEY_VOLUME_RATIO_MIN", 1.3)
|
||||
KEY_BREAKOUT_AMP_MIN_PCT = _env_float("KEY_BREAKOUT_AMP_MIN_PCT", 0.03)
|
||||
KEY_BREAKOUT_AMP_MAX_PCT = _env_float("KEY_BREAKOUT_AMP_MAX_PCT", 0.5)
|
||||
KEY_CONFIRM_BREAKOUT_BAR = _env_int("KEY_CONFIRM_BREAKOUT_BAR", -2)
|
||||
KEY_CONFIRM_BAR = _env_int("KEY_CONFIRM_BAR", -1)
|
||||
KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT = os.getenv("KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT", "true").lower() in (
|
||||
"1",
|
||||
"true",
|
||||
|
||||
Vendored
+54
@@ -378,6 +378,32 @@ def validate_env_updates(groups: list[dict], updates: dict[str, str]) -> tuple[d
|
||||
allowed[field["key"]] = field
|
||||
clean: dict[str, str] = {}
|
||||
errors: list[str] = []
|
||||
|
||||
def _looks_numeric_key(k: str) -> bool:
|
||||
u = (k or "").upper()
|
||||
return any(
|
||||
s in u
|
||||
for s in (
|
||||
"_BUFFER",
|
||||
"_RATIO",
|
||||
"_PERCENT",
|
||||
"_SECONDS",
|
||||
"_HOURS",
|
||||
"_MINUTES",
|
||||
"_LIMIT",
|
||||
"_AMOUNT",
|
||||
"_BUDGET",
|
||||
"_CAP_",
|
||||
"_MAX_",
|
||||
"_MIN_",
|
||||
"LEVERAGE",
|
||||
"_DTE_",
|
||||
"_USDT",
|
||||
"_USDC",
|
||||
"RISK_PERCENT",
|
||||
)
|
||||
)
|
||||
|
||||
for key, value in (updates or {}).items():
|
||||
if key not in allowed:
|
||||
errors.append(f"未知配置项: {key}")
|
||||
@@ -392,6 +418,17 @@ def validate_env_updates(groups: list[dict], updates: dict[str, str]) -> tuple[d
|
||||
errors.append(f"{key} 长度异常,疑似自动填充;留空则不修改已有密钥")
|
||||
continue
|
||||
ftype = allowed[key].get("type")
|
||||
if val == "":
|
||||
# 空值写入会导致 float('') / int('') 启动崩溃(曾出现 502)
|
||||
if ftype in ("float", "int") or _looks_numeric_key(key):
|
||||
errors.append(f"{key} 不能为空,请填写数字")
|
||||
continue
|
||||
if ftype == "select" or key in SELECT_OPTIONS:
|
||||
errors.append(f"{key} 不能为空,请从下拉选择")
|
||||
continue
|
||||
if ftype == "bool":
|
||||
errors.append(f"{key} 不能为空,请选择 true/false")
|
||||
continue
|
||||
if ftype == "bool":
|
||||
low = val.lower()
|
||||
if low not in ("true", "false", "1", "0", "yes", "no", "on", "off"):
|
||||
@@ -412,6 +449,23 @@ def validate_env_updates(groups: list[dict], updates: dict[str, str]) -> tuple[d
|
||||
errors.append(f"{key} 须为: {labels}")
|
||||
continue
|
||||
val = norm
|
||||
elif ftype == "float" or (ftype != "int" and _looks_numeric_key(key) and "." in val):
|
||||
try:
|
||||
float(val)
|
||||
except ValueError:
|
||||
errors.append(f"{key} 须为数字")
|
||||
continue
|
||||
ftype = "float"
|
||||
elif ftype == "int" or _looks_numeric_key(key):
|
||||
try:
|
||||
# 允许 0.98 类缓冲写成 float 键名但值是 int 也可
|
||||
if "." in val:
|
||||
float(val)
|
||||
else:
|
||||
int(val)
|
||||
except ValueError:
|
||||
errors.append(f"{key} 须为数字")
|
||||
continue
|
||||
clean[key] = val
|
||||
return clean, errors
|
||||
|
||||
|
||||
Vendored
+4
-2
@@ -251,14 +251,16 @@ _RUNTIME_ENV_DEFAULTS: dict[str, str] = {
|
||||
"AUTO_TRANSFER_FROM": "funding",
|
||||
"AUTO_TRANSFER_TO": "swap",
|
||||
"TRANSFER_CCY": "USDT",
|
||||
"HEDGE_PLAN_SHOW_PERP_OPTIONS": "true",
|
||||
"HEDGE_PLAN_SHOW_OPTIONS_OPTIONS": "true",
|
||||
"HEDGE_PLAN_SHOW_PERP_OPTIONS": "false",
|
||||
"HEDGE_PLAN_SHOW_OPTIONS_OPTIONS": "false",
|
||||
"OKX_SHOW_PERP_FUNDS": "false",
|
||||
"OKX_OPTIONS_CHAIN_ASK_LIQ_FILTER_ENABLED": "true",
|
||||
"OKX_OPTIONS_CHAIN_MAX_DTE_DAYS": "14",
|
||||
"OKX_OPTIONS_MAX_DTE_DAYS": "2",
|
||||
"OKX_OPTIONS_MAX_ACTIVE_POSITIONS": "0",
|
||||
"OKX_OPTIONS_MARGIN_MODE": "coin",
|
||||
"OKX_TRADE_MODE": "options",
|
||||
"HEDGE_PLAN_ENABLED": "false",
|
||||
"MAX_ACTIVE_HEDGE_PLANS": "1",
|
||||
"HEDGE_PLAN_OO_CLOSE_MODE_ENABLED": "true",
|
||||
"HEDGE_PLAN_OO_BIAS_SPLIT_BY": "budget",
|
||||
|
||||
@@ -48,19 +48,10 @@ def normalize_okx_trade_mode(raw: Optional[str]) -> str:
|
||||
|
||||
|
||||
def legacy_infer_okx_trade_mode() -> str:
|
||||
"""未配置 OKX_TRADE_MODE 时,按旧开关推断,避免已有部署行为突变."""
|
||||
if not _env_bool("HEDGE_PLAN_ENABLED", False):
|
||||
return MODE_OPTIONS
|
||||
show_po = _env_bool("HEDGE_PLAN_SHOW_PERP_OPTIONS", True)
|
||||
show_oo = _env_bool("HEDGE_PLAN_SHOW_OPTIONS_OPTIONS", True)
|
||||
if show_po and not show_oo:
|
||||
return MODE_PERP
|
||||
if show_oo and not show_po:
|
||||
return MODE_OO
|
||||
if show_po:
|
||||
return MODE_PERP
|
||||
if show_oo:
|
||||
return MODE_OO
|
||||
"""未配置 OKX_TRADE_MODE 时默认单独期权(对冲关闭).
|
||||
|
||||
旧 HEDGE_PLAN_* 开关不再自动打开对冲,避免误开;需显式设 OKX_TRADE_MODE=options_options.
|
||||
"""
|
||||
return MODE_OPTIONS
|
||||
|
||||
|
||||
|
||||
@@ -34,9 +34,9 @@ def _env_bool(key: str, default: bool = False) -> bool:
|
||||
|
||||
def _env_float(key: str, default: float) -> float:
|
||||
try:
|
||||
return float(os.getenv(key, str(default)))
|
||||
return float(os.getenv(key) or default)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
return float(default)
|
||||
|
||||
|
||||
def attach_options_templates(app: Flask, repo_root: str) -> None:
|
||||
|
||||
Reference in New Issue
Block a user