refactor: unify data preparations and research input snapshots
Deploy production / deploy (push) Successful in 53s

This commit is contained in:
yuxuanhui
2026-09-12 01:24:02 +08:00
parent 849f86fef7
commit 394438e753
82 changed files with 4146 additions and 2076 deletions
+10 -50
View File
@@ -5,12 +5,9 @@ not FASTEXPR operator semantics or the account's current platform permissions.
"""
from fastapi import HTTPException
from sqlalchemy import select
from ..backtests.contracts import Candidate, DraftInput, PreviewInput, Source
from ..catalog.contracts import EntryOutput, InputPreparation
from ..catalog.service import Catalog
from ..models import CatalogEntry
from .expressions import analyze, expand
@@ -21,55 +18,18 @@ class ResearchBuilder:
self.backtests = backtests
async def select_input(self, body):
"""Fix explicit fields in one published version; reject missing or stale members."""
collection = await self.catalog.collection(body.scope, body.dataset_id)
chosen = set(body.field_ids)
if len(chosen) != len(body.field_ids) or not chosen.issubset(collection["field_ids"]):
raise HTTPException(422, "字段选择含重复、未知或其他数据集字段")
saved = await self.catalog.prepare(
InputPreparation(
scope=body.scope,
dataset_id=body.dataset_id,
collection_version=body.collection_version,
selection="explicit",
excluded_ids=[field for field in collection["field_ids"] if field not in chosen],
)
)
from ..preparations.service import Preparations
saved = (await Preparations(self.db).freeze(body.items))[0]
return await self.input_page(saved["id"])
async def input_page(self, input_id, limit=25, offset=0, q="", field_type=None):
"""Read the saved version, including field descriptions, with explicit pagination."""
saved = await self.catalog.input(input_id)
ids = [
field
for field in saved["field_ids"]
if q.lower() in field.lower()
and (field_type is None or saved["field_types"].get(field) == field_type)
]
page = ids[offset : offset + limit]
entries = {
row.id: row
for row in await self.db.scalars(
select(CatalogEntry).where(
CatalogEntry.batch_id == saved["collection_version"], CatalogEntry.id.in_(page)
)
)
}
return {
**{
k: saved[k]
for k in ("id", "scope", "dataset_id", "collection_version", "selection", "created_at")
},
"field_count": len(saved["field_ids"]),
"items": [
EntryOutput.model_validate(entries[field], from_attributes=True).model_dump()
for field in page
],
"total": len(ids),
"limit": limit,
"offset": offset,
"has_more": offset + limit < len(ids),
}
fields = [f for f in saved["fields"] if (not q or q.lower() in
" ".join(str(f.get(k) or "") for k in ("id", "name", "description", "dataset_id")).lower())
and (not field_type or f["field_type"] == field_type)]
return {**{k: v for k, v in saved.items() if k not in ("fields", "field_ids", "field_types")},
"field_count": len(saved["fields"]), "items": fields[offset:offset + limit],
"total": len(fields), "limit": limit, "offset": offset, "has_more": offset + limit < len(fields)}
async def prepare(self, body):
"""Bind templates against an immutable input, then reuse the fixed-preview interface.
@@ -77,7 +37,7 @@ class ResearchBuilder:
Raises HTTPException(422) for wrong scope, membership or declared type.
No expression execution or implicit cleaning/aggregation takes place here.
"""
saved = await self.catalog.input(body.template_input_id)
saved = await self.catalog.input(body.input_snapshot_id)
scope = saved["scope"]
candidates = []
for item in body.candidates:
@@ -114,7 +74,7 @@ class ResearchBuilder:
source = Source.model_validate(
{
**body.source.model_dump(),
"template_input_id": saved["id"],
"input_snapshot_id": saved["id"],
"hypothesis": body.hypothesis,
}
)