feat: unify backtest table controls and add refresh action
Deploy production / deploy (push) Successful in 53s
Deploy production / deploy (push) Successful in 53s
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
"""Authenticated adapters; every mutation is committed before the execution lane wakes."""
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
|
||||
from ..business import Business
|
||||
@@ -48,9 +50,16 @@ async def configure(body: SchedulerInput, request: Request):
|
||||
|
||||
|
||||
@router.get("/drafts")
|
||||
async def drafts(request: Request, limit: int = Query(25, ge=1, le=100), offset: int = Query(0, ge=0)):
|
||||
async def drafts(
|
||||
request: Request,
|
||||
limit: int = Query(25, ge=1, le=100),
|
||||
offset: int = Query(0, ge=0),
|
||||
q: str = Query("", max_length=200),
|
||||
sort: Literal["name", "updated_at"] = "updated_at",
|
||||
direction: Literal["asc", "desc"] = "desc",
|
||||
):
|
||||
async with request.app.state.sessions() as db:
|
||||
return await Business(db).backtests.drafts(limit, offset)
|
||||
return await Business(db).backtests.drafts(limit, offset, q, sort, direction)
|
||||
|
||||
|
||||
@router.post("/drafts", status_code=201)
|
||||
@@ -108,9 +117,14 @@ async def runs(
|
||||
source: str | None = Query(None, max_length=100),
|
||||
reference: str | None = Query(None, max_length=200),
|
||||
research_id: str | None = Query(None, max_length=200),
|
||||
q: str = Query("", max_length=200),
|
||||
sort: Literal["name", "created_at"] = "created_at",
|
||||
direction: Literal["asc", "desc"] = "desc",
|
||||
):
|
||||
async with request.app.state.sessions() as db:
|
||||
return await Business(db).backtests.runs(limit, offset, source, reference, research_id)
|
||||
return await Business(db).backtests.runs(
|
||||
limit, offset, source, reference, research_id, q, sort, direction
|
||||
)
|
||||
|
||||
|
||||
@router.get("/sources", response_model=list[str])
|
||||
|
||||
@@ -151,11 +151,16 @@ class Backtests:
|
||||
await self.db.flush()
|
||||
return await self.draft(draft_id)
|
||||
|
||||
async def drafts(self, limit=25, offset=0):
|
||||
async def drafts(self, limit=25, offset=0, q="", sort="updated_at", direction="desc"):
|
||||
query = select(BacktestDraft)
|
||||
if q:
|
||||
query = query.where(BacktestDraft.name.contains(q, autoescape=True))
|
||||
column = {"name": BacktestDraft.name, "updated_at": BacktestDraft.updated_at}[sort]
|
||||
order = column.asc() if direction == "asc" else column.desc()
|
||||
rows = (
|
||||
await self.db.scalars(
|
||||
select(BacktestDraft)
|
||||
.order_by(BacktestDraft.updated_at.desc(), BacktestDraft.id)
|
||||
query
|
||||
.order_by(order, BacktestDraft.id)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
)
|
||||
@@ -173,7 +178,7 @@ class Backtests:
|
||||
)
|
||||
for r in rows
|
||||
],
|
||||
"total": await self.db.scalar(select(func.count()).select_from(BacktestDraft)),
|
||||
"total": await self.db.scalar(select(func.count()).select_from(query.subquery())),
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
@@ -355,15 +360,19 @@ class Backtests:
|
||||
await self.db.flush()
|
||||
return await self.run(run.id)
|
||||
|
||||
async def runs(self, limit=25, offset=0, source=None, reference=None, research_id=None):
|
||||
async def runs(self, limit=25, offset=0, source=None, reference=None, research_id=None, q="", sort="created_at", direction="desc"):
|
||||
query = select(BacktestRun)
|
||||
if q:
|
||||
query = query.where(BacktestRun.name.contains(q, autoescape=True))
|
||||
column = {"name": BacktestRun.name, "created_at": BacktestRun.created_at}[sort]
|
||||
order = column.asc() if direction == "asc" else column.desc()
|
||||
for key, value in (("kind", source), ("reference", reference), ("research_id", research_id)):
|
||||
if value:
|
||||
query = query.where(BacktestRun.source[key].as_string() == value)
|
||||
total = await self.db.scalar(select(func.count()).select_from(query.subquery()))
|
||||
rows = (
|
||||
await self.db.scalars(
|
||||
query.order_by(BacktestRun.created_at.desc(), BacktestRun.id).limit(limit).offset(offset)
|
||||
query.order_by(order, BacktestRun.id).limit(limit).offset(offset)
|
||||
)
|
||||
).all()
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"""List filtering and ordering happen before pagination, without platform execution."""
|
||||
|
||||
from tests.test_backtests import PREFIX, candidate, setup, start
|
||||
|
||||
|
||||
async def test_list_sorting(app, logged_in):
|
||||
await setup(app)
|
||||
for name in ["sort-C", "sort-A", "sort-B", "unrelated"]:
|
||||
body = {"name": name, "source": {"kind": "test"}, "candidates": [candidate()]}
|
||||
assert (await logged_in.post(f"{PREFIX}/drafts", json=body)).status_code == 201
|
||||
preview = await logged_in.post(f"{PREFIX}/previews", json={"inline": body})
|
||||
assert preview.status_code == 201
|
||||
await start(logged_in, preview.json(), key=name)
|
||||
for path in ["runs", "drafts"]:
|
||||
for direction, expected in [
|
||||
("asc", ["sort-A", "sort-B", "sort-C"]),
|
||||
("desc", ["sort-C", "sort-B", "sort-A"]),
|
||||
]:
|
||||
names = []
|
||||
for offset in range(3):
|
||||
r = await logged_in.get(
|
||||
f"{PREFIX}/{path}",
|
||||
params={
|
||||
"q": "sort-",
|
||||
"sort": "name",
|
||||
"direction": direction,
|
||||
"limit": 1,
|
||||
"offset": offset,
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.json()["total"] == 3
|
||||
names.append(r.json()["items"][0]["name"])
|
||||
assert names == expected
|
||||
for params in [{"sort": "unknown"}, {"direction": "unknown"}]:
|
||||
assert (await logged_in.get(f"{PREFIX}/{path}", params=params)).status_code == 422
|
||||
assert (await logged_in.get(f"{PREFIX}/{path}", params={"q": "%"})).json()["total"] == 0
|
||||
assert (await logged_in.get(f"{PREFIX}/runs", params={"q": "sort-", "source": "missing"})).json()[
|
||||
"total"
|
||||
] == 0
|
||||
Reference in New Issue
Block a user