Skip to content

Commit dfa5f64

Browse files
add deploy docker + caddy + cron job scripts
1 parent d0d4a9f commit dfa5f64

File tree

16 files changed

+338
-103
lines changed

16 files changed

+338
-103
lines changed

.github/workflows/deploy.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Deploy to Hetzner
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
8+
jobs:
9+
deploy:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout code
14+
uses: actions/checkout@v4
15+
16+
- name: Deploy to Server via SSH
17+
uses: appleboy/ssh-action@v1.0.3
18+
with:
19+
host: ${{ secrets.SSH_HOST }}
20+
username: ${{ secrets.SSH_USERNAME }}
21+
key: ${{ secrets.SSH_PRIVATE_KEY }}
22+
script: |
23+
cd /root/app
24+
25+
git fetch origin
26+
27+
git reset --hard origin/master
28+
29+
docker compose down
30+
31+
docker compose up -d --build
32+
33+
docker image prune -af

Caddyfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{$DOMAIN} {
2+
reverse_proxy web:8000
3+
}

Dockerfile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
FROM python:3.11-slim
2+
3+
WORKDIR /app
4+
5+
COPY ./backend/requirements.txt /app/requirements.txt
6+
COPY ./static/favicon.ico /app/static/favicon.ico
7+
8+
# Install system dependencies and then Python dependencies
9+
RUN apt-get update && apt-get install -y build-essential libpq-dev && \
10+
pip install --no-cache-dir -r /app/requirements.txt && \
11+
rm -rf /var/lib/apt/lists/*
12+
13+
# Copy the rest of backend application
14+
COPY ./backend /app/backend
15+
16+
EXPOSE 8000
17+
18+
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"]

backend/.dockerignore

Lines changed: 0 additions & 4 deletions
This file was deleted.

backend/Dockerfile

Lines changed: 0 additions & 13 deletions
This file was deleted.

backend/db/db_utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
SessionDep = Annotated[Session, Depends(get_session)]
2020
URL_BASE = "https://api.openf1.org/v1/"
21-
FALLBACK_COMPOUND = "Not provided"
21+
FALLBACK_COMPOUND = "UNKNOWN"
2222

2323
def get_data(url: str, retries: int = 5, backoff: float = 1.0):
2424
"""
@@ -84,6 +84,8 @@ def map_stints_laps(stints: list[dict]):
8484
:return: Dictionary containing the lap-to-compound mapping.
8585
"""
8686
stints_hashmap = {}
87+
if not stints:
88+
return {}
8789
for stint in stints:
8890
lap_start = stint.get('lap_start')
8991
lap_end = stint.get('lap_end')

backend/db/update_db.py

Lines changed: 132 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,26 @@
66
from backend.models.session_laps import SessionLaps
77
from backend.models.session_result import SessionResult
88
from backend.models.sessions import F1Session
9-
from sqlalchemy import select
9+
from backend.models.teams import Teams
10+
11+
from sqlalchemy import select, exists
1012
from sqlmodel import Session, distinct, select, desc
13+
from sqlalchemy.dialects.postgresql import insert
1114
from collections import defaultdict
12-
13-
from backend.models.teams import Teams
15+
from sqlalchemy import bindparam, text
1416

1517
"""
1618
This is the script that updates the database.
1719
"""
1820

19-
def fetch_latest_session(session: Session) -> str:
21+
def fetch_latest_session(session: Session) -> F1Session | None:
2022
"""
2123
Queries the database to find the latest session we have a record of.
2224
:param session: Database session
2325
:return: Latest 'F1Session' session
2426
"""
2527
result = session.exec(
26-
select(F1Session.date).order_by(desc(F1Session.date))
28+
select(F1Session).order_by(desc(F1Session.date))
2729
).first()
2830
return result
2931

@@ -58,6 +60,32 @@ def add_meetings_to_db(session: Session):
5860
session.add(new_meeting)
5961
logger.info(f"Staged meeting {str(meeting['meeting_key'])} for addition.")
6062

63+
def add_current_meeting(session, meeting_key):
64+
existing = session.get(Event, meeting_key)
65+
66+
if existing:
67+
return
68+
69+
meeting = get_data(f'{URL_BASE}meetings?meeting_key={meeting_key}')[0]
70+
71+
if not meeting:
72+
logger.warning(f"No meeting data for meeting_key={meeting_key}")
73+
return
74+
75+
new_meeting = Event(
76+
meeting_key=meeting.get('meeting_key'),
77+
circuit_key=meeting.get('circuit_key'),
78+
location=meeting.get('location'),
79+
country_name=meeting.get('country_name'),
80+
circuit_name=meeting.get('circuit_short_name'),
81+
meeting_official_name=meeting.get('meeting_official_name'),
82+
year=meeting.get('year')
83+
)
84+
85+
session.add(new_meeting)
86+
session.flush()
87+
88+
6189
def add_session_to_db(session:Session, f1session: dict):
6290
"""
6391
Adds an F1Session to the database.
@@ -162,71 +190,118 @@ def add_drivers_and_session_links(session: Session, session_key: int, all_driver
162190

163191
def add_all_laps_for_session(session: Session, session_key: int):
164192
"""
165-
Efficiently fetches and adds all laps for all drivers in a session.
193+
Fetch lap/stint/driver data, ensure drivers are linked, then batch upsert laps.
194+
Assumes the caller manages transactions (e.g. with session.begin()).
166195
"""
167196
logger.info(f"Starting bulk lap/stint processing for session {session_key}.")
168197

169-
# bulk fetch all lap and stint data for a session
170-
laps_url = URL_BASE + f'laps?session_key={str(session_key)}'
171-
all_laps_data = get_data(laps_url)
198+
# fetch remote data
199+
laps_url = URL_BASE + f'laps?session_key={session_key}'
200+
all_laps_data = get_data(laps_url) or []
172201

173-
stints_url = URL_BASE + f"stints?session_key={str(session_key)}"
174-
all_stints_data = get_data(stints_url)
202+
stints_url = URL_BASE + f"stints?session_key={session_key}"
203+
all_stints_data = get_data(stints_url) or []
175204

176-
# group data for ease of access
205+
drivers_url = URL_BASE + f"drivers?session_key={session_key}"
206+
all_drivers_data = get_data(drivers_url) or []
207+
208+
# group by driver_number for quick lookup
177209
laps_by_driver = defaultdict(list)
178210
for lap in all_laps_data:
179-
laps_by_driver[lap['driver_number']].append(lap)
211+
dn = lap.get('driver_number')
212+
if dn is None:
213+
continue
214+
laps_by_driver[dn].append(lap)
180215

181216
stints_by_driver = defaultdict(list)
182217
for stint in all_stints_data:
183-
stints_by_driver[stint['driver_number']].append(stint)
184-
185-
drivers_url = URL_BASE + f"drivers?session_key={str(session_key)}"
186-
all_drivers_data = get_data(drivers_url)
218+
dn = stint.get('driver_number')
219+
if dn is None:
220+
continue
221+
stints_by_driver[dn].append(stint)
187222

188-
# ensure all drivers and their session links are in the DB
223+
# ensure drivers/session links exist
189224
add_drivers_and_session_links(session, session_key, all_drivers_data)
190225

191-
# make one query to fetch all laps for that session to check existence
192-
existing_driver_laps = session.exec(
226+
# fetch existing laps for this session that already have a compound
227+
rows = session.exec(
193228
select(SessionLaps.driver_id, SessionLaps.lap_number).where(
194-
SessionLaps.session_key == session_key
195-
))
229+
(SessionLaps.session_key == session_key) & (SessionLaps.compound != None)
230+
)
231+
).all()
232+
existing_laps = {(r[0], r[1]) for r in rows} if rows else set()
196233

197-
existing_laps = {(lap.driver_id, lap.lap_number) for lap in existing_driver_laps}
234+
# collect the parameter dicts we want to upsert
235+
values_to_upsert: list[dict] = []
198236

199237
for driver_data in all_drivers_data:
200-
driver_number = driver_data['driver_number']
201-
driver_id = driver_data['driver_id']
238+
driver_number = driver_data.get('driver_number')
239+
driver_id = driver_data.get('driver_id')
202240

203-
# get laps and stints for this driver from our in-memory groups
204-
driver_laps = laps_by_driver.get(driver_number, [])
205-
driver_stints = stints_by_driver.get(driver_number, [])
241+
if driver_number is None or driver_id is None:
242+
continue
206243

244+
driver_laps = laps_by_driver.get(driver_number, [])
207245
if not driver_laps:
208246
continue
209247

210-
stints_hashmap = map_stints_laps(driver_stints)
248+
stints_hashmap = map_stints_laps(stints_by_driver.get(driver_number, []))
211249

212250
for lap in driver_laps:
251+
lap_num = lap.get('lap_number')
252+
if lap_num is None:
253+
continue
213254

214-
if (driver_id, lap.get('lap_number')) in existing_laps:
255+
if (driver_id, lap_num) in existing_laps:
215256
continue
216257

217-
compound = stints_hashmap.get(lap['lap_number'], FALLBACK_COMPOUND)
258+
compound = stints_hashmap.get(lap_num, None)
218259

219-
new_lap = SessionLaps(
220-
driver_id=driver_id,
221-
session_key=lap.get('session_key'),
222-
lap_number=lap.get('lap_number'),
223-
is_pit_out_lap=lap.get('is_pit_out_lap'),
224-
lap_time=lap.get('lap_duration', 0.0),
225-
st_speed=lap.get('st_speed', 0),
226-
compound=compound
227-
)
228-
session.add(new_lap)
229-
logger.info(f"Staged all laps for session {session_key}.")
260+
values_to_upsert.append({
261+
'driver_id': driver_id,
262+
'session_key': lap.get('session_key'),
263+
'lap_number': lap_num,
264+
'is_pit_out_lap': lap.get('is_pit_out_lap'),
265+
'lap_time': lap.get('lap_duration', 0.0),
266+
'st_speed': lap.get('st_speed', 0),
267+
'compound': compound,
268+
})
269+
270+
if not values_to_upsert:
271+
logger.info(f"No new laps to upsert for session {session_key}.")
272+
return
273+
274+
stmt = insert(SessionLaps).values(
275+
driver_id=bindparam('driver_id'),
276+
session_key=bindparam('session_key'),
277+
lap_number=bindparam('lap_number'),
278+
is_pit_out_lap=bindparam('is_pit_out_lap'),
279+
lap_time=bindparam('lap_time'),
280+
st_speed=bindparam('st_speed'),
281+
compound=bindparam('compound'),
282+
)
283+
284+
where_clause_expr = (
285+
SessionLaps.compound.is_distinct_from(stmt.excluded.compound)
286+
| SessionLaps.lap_time.is_distinct_from(stmt.excluded.lap_time)
287+
| SessionLaps.st_speed.is_distinct_from(stmt.excluded.st_speed)
288+
| SessionLaps.is_pit_out_lap.is_distinct_from(stmt.excluded.is_pit_out_lap)
289+
)
290+
291+
do_update = stmt.on_conflict_do_update(
292+
index_elements=['driver_id', 'session_key', 'lap_number'],
293+
set_={
294+
'is_pit_out_lap': stmt.excluded.is_pit_out_lap,
295+
'lap_time': stmt.excluded.lap_time,
296+
'st_speed': stmt.excluded.st_speed,
297+
'compound': stmt.excluded.compound,
298+
},
299+
where=where_clause_expr
300+
)
301+
302+
session.execute(do_update, values_to_upsert)
303+
304+
logger.info(f"Upserted {len(values_to_upsert)} laps for session {session_key}.")
230305

231306
def add_session_result_to_db(session:Session, session_key:int):
232307
"""
@@ -278,7 +353,7 @@ def add_session_result_to_db(session:Session, session_key:int):
278353
session.add(sr_entry)
279354
logger.info(f"Staged session results of session {str(session_key)} for addition.")
280355

281-
def add_teams_colors(session:Session):
356+
def add_teams_colors(session:Session, year=None):
282357
try:
283358
teams = session.exec(select(distinct(SessionDriver.team)))
284359
existing_teams_query = session.exec(select(Teams.name))
@@ -310,27 +385,29 @@ def update_db():
310385
Controls the flow to update the database, calling all necessary methods.
311386
"""
312387
with Session(engine) as session:
313-
latest_session_date = fetch_latest_session(session)
314-
315-
if latest_session_date:
316-
url = URL_BASE + f'sessions?date_start>={latest_session_date}'
317-
# if this is the first time populating the script (no latest session in db), get all sessions
318-
else:
319-
url = URL_BASE + f'sessions'
320-
data = get_data(url)
321-
388+
data_url = ""
322389
try:
323-
add_meetings_to_db(session)
390+
latest_session= fetch_latest_session(session)
391+
392+
if latest_session:
393+
data_url = URL_BASE + f'sessions?date_start>={latest_session.date}'
394+
# if this is the first time populating the script (no latest session in db), get all sessions and meetings
395+
else:
396+
data_url = URL_BASE + 'sessions'
397+
add_meetings_to_db(session)
398+
add_teams_colors(session)
324399
session.commit()
325400
except Exception as e:
326-
logger.error(f"Failed to add meetings to db. Error: {e}")
401+
logger.error(f"Failed to fetch data: {e}")
327402

403+
data = get_data(data_url)
328404
data_size = len(data)
329-
c= 0
405+
c = 0
330406
for f1session in data:
331407
try:
332408
with session.begin():
333409
session_key = f1session['session_key']
410+
add_current_meeting(session, f1session['meeting_key'])
334411
add_session_to_db(session, f1session)
335412
add_all_laps_for_session(session, session_key)
336413
add_session_result_to_db(session, session_key)
@@ -342,7 +419,6 @@ def update_db():
342419
exc_info=True,
343420
)
344421
break
345-
add_teams_colors(session)
346422

347423
if __name__ == "__main__":
348424
from .database import create_db_and_tables

backend/fly.toml

Lines changed: 0 additions & 23 deletions
This file was deleted.

0 commit comments

Comments
 (0)