Skip to content

Commit eb2252f

Browse files
committed
#824 Backend changes
1 parent 57c3bbe commit eb2252f

17 files changed

Lines changed: 133 additions & 122 deletions

File tree

src/backend_api/app/alembic/versions/965e766db0a0_added_league_table.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,15 @@ def upgrade() -> None:
8383
sa.Column("name", sa.Text(), nullable=False),
8484
sa.Column("validFrom", sa.DateTime(timezone=True), nullable=False),
8585
sa.Column("validTo", sa.DateTime(timezone=True), nullable=True),
86+
sa.Column("version", sa.Float(), nullable=False),
8687
sa.PrimaryKeyConstraint("leagueId"),
8788
)
8889
op.execute("""
89-
INSERT INTO league (name)
90-
SELECT DISTINCT league
90+
INSERT INTO league (name, "validFrom", version)
91+
SELECT DISTINCT
92+
league,
93+
TIMESTAMP '1970-01-01 00:00:00',
94+
0.0
9195
FROM item
9296
WHERE league IS NOT NULL
9397
""")

src/backend_api/app/api/routes/currency.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,9 @@ async def get_latest_currency_id(
108108

109109

110110
@router.get(
111-
"/latest_hour/",
112-
response_model=int,
113-
tags=["latest_hour"],
111+
"/latest_hours/",
112+
response_model=dict[int, int],
113+
tags=["latest_hours"],
114114
dependencies=[
115115
Depends(get_current_active_user),
116116
],
@@ -121,20 +121,23 @@ async def get_latest_currency_id(
121121
rate_limit_settings.DEFAULT_USER_RATE_LIMIT_HOUR,
122122
rate_limit_settings.DEFAULT_USER_RATE_LIMIT_DAY,
123123
)
124-
async def get_latest_hour(
124+
async def get_latest_hours(
125+
league_ids: Annotated[list[int], Query(min_length=1)],
125126
request: Request, # noqa: ARG001
126127
response: Response, # noqa: ARG001
127128
db: Session = Depends(get_db),
128129
):
129130
"""
130-
Return -1 if database is empty
131+
Returns a dict mapping league id to the most recent hour relating to that league. Dict is empty if no currencies exist.
132+
133+
Does not guarantee an entry for every league id.
131134
"""
132-
return await CRUD_currency.get_latest_hour(db)
135+
return await CRUD_currency.get_latest_hours(db, league_ids)
133136

134137

135138
@router.get(
136139
"/latest_currencies/",
137-
response_model=list[schemas.Currency],
140+
response_model=dict[int, list[schemas.Currency]],
138141
tags=["latest_currencies"],
139142
dependencies=[
140143
Depends(get_current_active_user),
@@ -147,14 +150,17 @@ async def get_latest_hour(
147150
rate_limit_settings.DEFAULT_USER_RATE_LIMIT_DAY,
148151
)
149152
async def get_latest_currencies(
153+
league_ids: Annotated[list[int], Query(min_length=1)],
150154
request: Request, # noqa: ARG001
151155
response: Response, # noqa: ARG001
152156
db: Session = Depends(get_db),
153157
):
154158
"""
155-
Returns a list of the latest currencies, which all share the same `createdHoursSinceLaunch` as defined by `latest_hour` endpoint.
159+
Returns a dict mapping league id to most recent currencies relating to that league. Dict is empty if no currencies exist.
160+
161+
Does not guarantee an entry for every league id.
156162
"""
157-
return await CRUD_currency.get_latest_currencies(db)
163+
return await CRUD_currency.get_latest_currencies(db, league_ids)
158164

159165

160166
@router.post(

src/backend_api/app/core/models/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class League(Base):
3333
name: Mapped[str] = mapped_column(Text, nullable=False)
3434
validFrom: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
3535
validTo: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True)
36+
version: Mapped[float] = mapped_column(Float, nullable=False)
3637

3738

3839
class Currency(Base):

src/backend_api/app/core/schemas/league.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class _BaseLeague(_pydantic.BaseModel):
1010
name: str
1111
validFrom: _dt.datetime
1212
validTo: _dt.datetime | None = None
13+
version: float
1314

1415

1516
# Properties to receive on League creation
@@ -18,11 +19,8 @@ class LeagueCreate(_BaseLeague):
1819

1920

2021
# Properties to receive on update
21-
class LeagueUpdate(_pydantic.BaseModel):
22+
class LeagueUpdate(_BaseLeague):
2223
leagueId: int
23-
name: str
24-
validFrom: _dt.datetime
25-
validTo: _dt.datetime | None = None
2624

2725

2826
# Properties shared by models stored in DB

src/backend_api/app/core/schemas/plot/output.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class Datum(_pydantic.BaseModel):
1111

1212

1313
class TimeseriesData(_pydantic.BaseModel):
14-
name: int
14+
leagueId: int
1515
data: list[Datum]
1616
confidenceRating: Literal["low", "medium", "high"]
1717

src/backend_api/app/core/schemas/unidentified_item.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ class UnidentifiedItemUpdate(_BaseUnidentifiedItem):
3131
class UnidentifiedItemInDBBase(_BaseUnidentifiedItem):
3232
createdHoursSinceLaunch: int
3333
itemId: int
34-
nItems: int
35-
aggregated: bool
3634

3735

3836
# Properties to return to client

src/backend_api/app/crud/extensions/crud_currency.py

Lines changed: 33 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from collections import defaultdict
2+
13
from pydantic import TypeAdapter
24
from sqlalchemy import select
35
from sqlalchemy.orm import Session
@@ -36,61 +38,45 @@ async def get_latest_currency_id(self, db: Session) -> int:
3638

3739
return validate(latest_currency_id)
3840

39-
async def get_latest_hour(self, db: Session) -> int:
40-
stmt = (
41-
select(model_Currency.createdHoursSinceLaunch)
42-
.order_by(model_Currency.currencyId.desc())
43-
.limit(1)
41+
def _latest_hours_stmt(self, league_ids: list[int]):
42+
return (
43+
select(
44+
model_Currency.leagueId,
45+
func.max(model_Currency.createdHoursSinceLaunch).label("latest_hour"),
46+
)
47+
.where(model_Currency.leagueId.in_(league_ids))
48+
.group_by(model_Currency.leagueId)
4449
)
4550

46-
db_latest_hour = db.execute(stmt).mappings().first()
47-
if db_latest_hour is not None:
48-
latest_hour = db_latest_hour["createdHoursSinceLaunch"]
49-
else:
50-
latest_hour = -1
51+
async def get_latest_hours(
52+
self, db: Session, league_ids: list[int]
53+
) -> dict[int, int]:
54+
stmt = self._latest_hours_stmt(league_ids)
5155

52-
validate = TypeAdapter(int).validate_python
56+
objs = db.execute(stmt).mappings().all()
57+
id_hour_map = {obj["leagueId"]: obj["latest_hour"] for obj in objs}
5358

54-
return validate(latest_hour)
59+
validate = TypeAdapter(dict[int, int]).validate_python
5560

56-
async def get_latest_currencies(self, db: Session) -> list[Currency]:
57-
latest_hour = await self.get_latest_hour(db)
61+
return validate(id_hour_map)
5862

59-
# All rows from that hour, plus the next lower currencyId
60-
hour_rows = (
61-
select(
62-
model_Currency.currencyId,
63-
model_Currency.createdHoursSinceLaunch,
64-
func.lead(model_Currency.currencyId)
65-
.over(order_by=model_Currency.currencyId.desc())
66-
.label("next_id"),
67-
).where(model_Currency.createdHoursSinceLaunch == latest_hour)
68-
).cte("hour_rows")
69-
70-
# The first place where the IDs stop being consecutive
71-
boundary = (
72-
select(hour_rows.c.next_id)
73-
.where(hour_rows.c.currencyId - hour_rows.c.next_id > 1)
74-
.order_by(hour_rows.c.currencyId.desc())
75-
.limit(1)
76-
.scalar_subquery()
77-
)
78-
# If no gap exists, include all rows for that hour
79-
min_id = func.coalesce(
80-
boundary,
81-
select(func.min(hour_rows.c.currencyId)).scalar_subquery(),
82-
)
83-
stmt = (
84-
select(model_Currency.__table__)
85-
.where(
86-
model_Currency.createdHoursSinceLaunch == latest_hour,
87-
model_Currency.currencyId >= min_id,
88-
)
89-
.order_by(model_Currency.currencyId.desc())
63+
async def get_latest_currencies(
64+
self, db: Session, league_ids: list[int]
65+
) -> dict[int, list[Currency]]:
66+
latest_hours = self._latest_hours_stmt(league_ids).subquery()
67+
68+
stmt = select(model_Currency).join(
69+
latest_hours,
70+
(model_Currency.leagueId == latest_hours.c.leagueId)
71+
& (model_Currency.createdHoursSinceLaunch == latest_hours.c.latest_hour),
9072
)
91-
latest_currencies = db.execute(stmt).mappings().all()
73+
currencies = db.scalars(stmt).all()
9274

93-
validate = TypeAdapter(list[Currency]).validate_python
75+
latest_currencies: dict[int, list[Currency]] = defaultdict(list)
76+
for currency in currencies:
77+
latest_currencies[currency.leagueId].append(currency)
78+
79+
validate = TypeAdapter(dict[int, list[Currency]]).validate_python
9480

9581
return validate(latest_currencies)
9682

src/backend_api/app/crud/extensions/crud_unidentifiedItem.py

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,9 @@ class CRUDUnidentifiedItem(
2121
):
2222
async def get_non_aggregated(self, db: Session) -> list[UnidentifiedItem]:
2323
"""
24-
Returns the non aggregated unidentified items for the last 10 recorded hours
25-
(not necessarily the last 10 hours)
24+
Returns the non aggregated unidentified items
2625
"""
27-
last_10_recorded_hours = (
28-
select(model_UnidentifiedItem.createdHoursSinceLaunch.distinct())
29-
.where(model_UnidentifiedItem.aggregated.isnot(True))
30-
.order_by(model_UnidentifiedItem.createdHoursSinceLaunch.desc())
31-
.limit(10)
32-
)
3326
stmt = select(model_UnidentifiedItem).where(
34-
model_UnidentifiedItem.createdHoursSinceLaunch.in_(last_10_recorded_hours),
3527
model_UnidentifiedItem.aggregated.isnot(True),
3628
)
3729

@@ -44,14 +36,7 @@ async def get_non_aggregated(self, db: Session) -> list[UnidentifiedItem]:
4436
async def add_aggregated(
4537
self, db: Session, aggregated_objs: list[UnidentifiedItemCreate]
4638
) -> list[UnidentifiedItem]:
47-
last_10_recorded_hours = (
48-
select(model_UnidentifiedItem.createdHoursSinceLaunch.distinct())
49-
.where(model_UnidentifiedItem.aggregated.isnot(True))
50-
.order_by(model_UnidentifiedItem.createdHoursSinceLaunch.desc())
51-
.limit(10)
52-
)
5339
stmt = delete(model_UnidentifiedItem).where(
54-
model_UnidentifiedItem.createdHoursSinceLaunch.in_(last_10_recorded_hours),
5540
model_UnidentifiedItem.aggregated.isnot(True),
5641
)
5742
non_aggregated_objs = db.execute(stmt)

src/backend_api/app/plotting/plotter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ def _create_plot_data(self, df: pd.DataFrame) -> PlotData:
234234
]
235235
].to_dict(orient="records") # type: ignore
236236
timeseries_data = {
237-
"name": league,
237+
"leagueId": league,
238238
"confidenceRating": league_df["confidence"].mode()[0],
239239
"data": league_data,
240240
}

src/backend_api/app/tests/utils/model_utils/league.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from app.core.schemas import LeagueCreate
66
from app.tests.utils.utils import (
77
random_datetime,
8+
random_float,
89
random_lower_string,
910
)
1011

@@ -18,11 +19,13 @@ def create_random_league_dict() -> dict:
1819
name = random_lower_string()
1920
validFrom = random_datetime()
2021
validTo = random_datetime(min_time=validFrom)
22+
version = random_float(small_float=True, max_value=3.99)
2123

2224
league = {
2325
"name": name,
2426
"validFrom": validFrom,
2527
"validTo": validTo,
28+
"version": version,
2629
}
2730

2831
return league

0 commit comments

Comments
 (0)