Files
qlib/backend/app/infrastructure/persistence/sqlalchemy/repositories/composite_impl.py
T
Simon 4fa2bb748e feat(composite): M7.2b 因子组合落库 + /api/composites CRUD
- factor_composite 表(migration c3e9a0d1f4b5,MySQL 已应用;name 唯一)
- CompositeDefinition/Component 实体 + CompositeRepository Protocol + SQLAlchemy 实现
- /api/composites:POST(注册表自动填充组件 direction;未注册因子 400)、GET 列表/详情、DELETE
- tests/test_composites_api.py(repo CRUD/同名拒绝/删除、API 方向填充/404/400);全量 pytest 通过
2026-09-09 00:30:37 +08:00

81 lines
2.9 KiB
Python

"""因子组合 Repository 的 SQLAlchemy 实现(M7.2b)。"""
from __future__ import annotations
import json
from datetime import datetime
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.domain.entities.composite import CompositeComponent, CompositeDefinition
from app.infrastructure.persistence.sqlalchemy.models.composite import FactorCompositeModel
class SqlAlchemyCompositeRepository:
def __init__(self, session: Session) -> None:
self._session = session
def save(self, definition: CompositeDefinition) -> CompositeDefinition:
if not definition.id:
raise ValueError("需要 id(由调用方生成)")
exists = self._session.get(FactorCompositeModel, definition.id)
dup = self._session.scalar(
select(FactorCompositeModel)
.where(FactorCompositeModel.name == definition.name)
.limit(1)
)
if dup is not None and dup.id != definition.id:
raise ValueError(f"组合名已存在:{definition.name}")
now = definition.created_at or datetime.now()
if exists is None:
self._session.add(
FactorCompositeModel(
id=definition.id,
name=definition.name,
method=definition.method,
description=definition.description,
components_json=json.dumps(
[c.model_dump() for c in definition.components], ensure_ascii=False
),
created_at=now,
)
)
else:
exists.name = definition.name
exists.method = definition.method
exists.description = definition.description
exists.components_json = json.dumps(
[c.model_dump() for c in definition.components], ensure_ascii=False
)
self._session.flush()
return definition
def get(self, composite_id: str) -> CompositeDefinition | None:
row = self._session.get(FactorCompositeModel, composite_id)
return _to_entity(row) if row else None
def list(self) -> list[CompositeDefinition]:
rows = self._session.scalars(
select(FactorCompositeModel).order_by(FactorCompositeModel.name)
).all()
return [_to_entity(r) for r in rows]
def delete(self, composite_id: str) -> bool:
row = self._session.get(FactorCompositeModel, composite_id)
if row is None:
return False
self._session.delete(row)
return True
def _to_entity(row: FactorCompositeModel) -> CompositeDefinition:
return CompositeDefinition(
id=row.id,
name=row.name,
method=row.method,
description=row.description,
components=[CompositeComponent(**c) for c in json.loads(row.components_json)],
created_at=row.created_at,
)