feat(selection): M6.3 选股结果落库 + /api/selections(快照 + 逐候选行)
- domain/repositories/selection.py + SelectionMeta:选股 Repository Protocol
- models/selection.py + migration a6c91d4e7f20:selection_snapshot(查询/统计快照)+
selection_result(逐候选:rank/score/factor_values/filter_status/reason JSON)
- repositories/selection_impl.py:save/get/list_recent(读回重建 SelectionResult)
- api/selections.py:POST /api/selections(同步执行+落库,返回 selection_id+result)、
GET /{id}、GET 列表(as_of/method 过滤);deps 装配 SelectionService/Repo
- config._build_mysql_url:仅编码破坏 URL 结构的字符(修 Alembic configparser 遇 %21 崩)
- 迁移已在 MySQL 应用(alembic head a6c91d4e7f20);tests/test_selections_api.py 4 例
提交→读回一致/404/列表过滤/condition;全量 pytest 通过
This commit is contained in:
@@ -10,7 +10,6 @@ import os
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
import yaml
|
||||
|
||||
@@ -100,6 +99,26 @@ def _normalize_sqlite_url(url: str) -> str:
|
||||
return f"{_SQLITE_PREFIX}{(PROJECT_ROOT / rest).resolve()}"
|
||||
|
||||
|
||||
_URL_RESERVED = set("@:/?#%")
|
||||
|
||||
|
||||
def _quote_userinfo(value: str) -> str:
|
||||
"""仅编码会破坏 SQLAlchemy URL 解析的字符(@ : / ? # % 与空白)。
|
||||
|
||||
其余字符(含 ! - _ . 等非保留符)原样保留:避免 URL 出现 %XX 干扰
|
||||
Alembic configparser 的 interpolation。
|
||||
"""
|
||||
out: list[str] = []
|
||||
for ch in value:
|
||||
if ch in _URL_RESERVED or ch.isspace():
|
||||
out.append(f"%{ord(ch):02X}")
|
||||
elif ord(ch) > 127: # 非 ASCII:按 UTF-8 逐字节 percent 编码
|
||||
out.append("".join(f"%{b:02X}" for b in ch.encode("utf-8")))
|
||||
else:
|
||||
out.append(ch)
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def _build_mysql_url(mysql: dict | None) -> str | None:
|
||||
"""由 config.yaml database.mysql 段组装 mysql+pymysql URL。
|
||||
|
||||
@@ -116,7 +135,11 @@ def _build_mysql_url(mysql: dict | None) -> str | None:
|
||||
port = mysql.get("port") or 3306
|
||||
charset = mysql.get("charset") or "utf8mb4"
|
||||
password = os.environ.get(mysql.get("password_env") or "MYSQL_PASSWORD", "")
|
||||
auth = f"{quote_plus(user)}:{quote_plus(password)}" if password else quote_plus(user)
|
||||
auth = (
|
||||
f"{_quote_userinfo(user)}:{_quote_userinfo(password)}"
|
||||
if password
|
||||
else _quote_userinfo(user)
|
||||
)
|
||||
return f"mysql+pymysql://{auth}@{host}:{port}/{db}?charset={charset}"
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user