Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
be37aed
update models and run migrations
kingMonkeh Nov 16, 2025
1445aa6
Move address from user to driver
kingMonkeh Nov 17, 2025
e607303
migration
kingMonkeh Nov 17, 2025
add47d2
add user service and update driver service :sob:
kingMonkeh Nov 17, 2025
a38cac4
IT WORKS DWD AOWD OAWJD OAWDJ OAWJD OAWJDO WAJDOA
kingMonkeh Nov 20, 2025
06ec709
fix logout button :sob:
kingMonkeh Nov 21, 2025
1d7cd18
ITS WORKING :sob:
kingMonkeh Nov 21, 2025
cecf320
add admin to seed script
kingMonkeh Nov 24, 2025
0207340
tests fix maybe ?
kingMonkeh Nov 24, 2025
d34d6f7
lint
kingMonkeh Nov 24, 2025
89b0ce6
format
kingMonkeh Nov 24, 2025
3614115
Fix type checking issues
kingMonkeh Nov 28, 2025
3a513aa
Merge branch 'main' of https://github.com/uwblueprint/food4kids into …
ludavidca Dec 27, 2025
cb65fd2
Edited Claude yml to debug
ludavidca Dec 27, 2025
6a4d7d0
removed secret
ludavidca Dec 27, 2025
4a39dc3
Testing Claude Code
ludavidca Dec 27, 2025
716f128
fix for pull request finding 'Unused local variable'
ludavidca Dec 27, 2025
4f17865
Merge branch 'main' of https://github.com/uwblueprint/food4kids into …
ludavidca Dec 27, 2025
141cc32
Merge branch 'hylac/restructure-user-system' of https://github.com/uw…
ludavidca Dec 27, 2025
75627ed
Order migration after existing migrations
ColinToft Dec 30, 2025
4cc45e5
Add tests
ColinToft Dec 30, 2025
aa89a74
Merge branch 'main' of https://github.com/uwblueprint/food4kids into …
ludavidca Dec 31, 2025
1189c97
Merge branch 'main' of https://github.com/uwblueprint/food4kids into …
ludavidca Feb 12, 2026
280816a
Removing incorrect diffs
ludavidca Feb 12, 2026
27c3246
Moved to Python 3.11 and fixed remaining bugs)
ludavidca Feb 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
if: steps.changes.outputs.python-backend == 'true'
uses: actions/setup-python@v5
with:
python-version: '3.10'
python-version: '3.11'

- name: Cache pip dependencies
if: steps.changes.outputs.python-backend == 'true'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
python-version: "3.11"
cache: "pip"
cache-dependency-path: backend/python/requirements.txt

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,7 @@ The project uses **GitHub Actions** for continuous integration. All workflows ar
- **Triggers:** Push or PR to `main` for `backend/python/**` paths
- Sets up PostgreSQL service container
- Runs `pytest -q --disable-warnings -ra`
- Python 3.10
- Python 3.11

3. **`claude-code-review.yml`** - Automated Code Review
- **Triggers:** PR ready for review, or `@claude review` comment
Expand Down
2 changes: 1 addition & 1 deletion backend/python/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.10
FROM python:3.11

WORKDIR /app

Expand Down
32 changes: 26 additions & 6 deletions backend/python/app/routers/driver_routes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from typing import Any
from uuid import UUID

from fastapi import APIRouter, Depends, HTTPException, Query, status
Expand All @@ -15,6 +16,25 @@
router = APIRouter(prefix="/drivers", tags=["drivers"])


def driver_to_driver_read(driver: Any) -> DriverRead:
"""Convert a Driver model instance to DriverRead."""
return DriverRead(
driver_id=driver.driver_id,
user_id=driver.user_id,
phone=driver.phone,
license_plate=driver.license_plate,
car_make_model=driver.car_make_model,
active=driver.active,
notes=driver.notes,
address=driver.address,
# User fields
auth_id=driver.user.auth_id,
name=driver.user.name,
email=driver.user.email,
role=driver.user.role,
)


@router.get("/", response_model=list[DriverRead])
async def get_drivers(
session: AsyncSession = Depends(get_session),
Expand All @@ -39,7 +59,7 @@ async def get_drivers(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Driver with id {driver_id} not found",
)
return [DriverRead.model_validate(driver)]
return [driver_to_driver_read(driver)]

elif email:
driver = await driver_service.get_driver_by_email(session, email)
Expand All @@ -48,11 +68,11 @@ async def get_drivers(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Driver with email {email} not found",
)
return [DriverRead.model_validate(driver)]
return [driver_to_driver_read(driver)]

else:
drivers = await driver_service.get_drivers(session)
return [DriverRead.model_validate(driver) for driver in drivers]
return [driver_to_driver_read(driver) for driver in drivers]

except HTTPException:
raise
Expand All @@ -77,7 +97,7 @@ async def get_driver(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Driver with id {driver_id} not found",
)
return DriverRead.model_validate(driver)
return driver_to_driver_read(driver)


@router.post("/", response_model=DriverRead, status_code=status.HTTP_201_CREATED)
Expand All @@ -91,7 +111,7 @@ async def create_driver(
"""
try:
created_driver = await driver_service.create_driver(session, driver)
return DriverRead.model_validate(created_driver)
return driver_to_driver_read(created_driver)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
Expand All @@ -116,7 +136,7 @@ async def update_driver(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Driver with id {driver_id} not found",
)
return DriverRead.model_validate(updated_driver)
return driver_to_driver_read(updated_driver)


@router.delete("/{driver_id}", status_code=status.HTTP_204_NO_CONTENT)
Expand Down
22 changes: 9 additions & 13 deletions backend/python/app/routers/route_group_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ async def get_route_groups(
)
result = []
for route_group in route_groups:
data = RouteGroupRead.model_validate(route_group).model_dump()
membership_count = len(route_group.route_group_memberships)
data["num_routes"] = membership_count
data = RouteGroupRead.model_validate(
route_group, from_attributes=True
).model_dump()
if include_routes:
data["routes"] = [
{
Expand Down Expand Up @@ -77,15 +77,7 @@ async def create_route_group(
created_route_group = await route_group_service.create_route_group(
session, route_group
)
return RouteGroupRead(
route_group_id=created_route_group.route_group_id,
name=created_route_group.name,
notes=created_route_group.notes,
drive_date=created_route_group.drive_date,
created_at=created_route_group.created_at,
updated_at=created_route_group.updated_at,
num_routes=0,
)
return RouteGroupRead.model_validate(created_route_group, from_attributes=True)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
Expand All @@ -111,7 +103,9 @@ async def update_route_group(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"RouteGroup with id {route_group_id} not found",
)
return RouteGroupRead.model_validate(updated_route_group)
return RouteGroupRead.model_validate(updated_route_group, from_attributes=True)
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
Expand All @@ -134,6 +128,8 @@ async def delete_route_group(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"RouteGroup with id {route_group_id} not found",
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
Expand Down
3 changes: 2 additions & 1 deletion backend/python/app/seed_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,8 @@ def main() -> None:

# Create locations from CSV
print("Creating locations from CSV...")
csv_path = "app/data/locations.csv"
# Allow CSV path to be overridden via environment variable for testing
csv_path = os.getenv("LOCATIONS_CSV_PATH", "app/data/locations.csv")
locations_created = 0

non_school_groups = [
Expand Down
10 changes: 7 additions & 3 deletions backend/python/app/services/implementations/driver_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ async def create_driver(
try:
session.add(driver)
await session.commit()
await session.refresh(driver)
await session.refresh(driver, attribute_names=["user"])
return driver

except Exception as db_error:
Expand All @@ -128,7 +128,11 @@ async def update_driver_by_id(
) -> Driver | None:
"""Update driver by ID"""
try:
statement = select(Driver).where(Driver.driver_id == driver_id)
statement = (
select(Driver)
.options(selectinload(Driver.user)) # type: ignore[arg-type]
.where(Driver.driver_id == driver_id)
)
result = await session.execute(statement)
driver = result.scalars().first()

Expand Down Expand Up @@ -159,7 +163,7 @@ async def update_driver_by_id(
driver.notes = driver_data.notes

await session.commit()
await session.refresh(driver)
await session.refresh(driver, attribute_names=["user"])
return driver

except Exception as e:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ async def create_route_group(
route_group = RouteGroup.model_validate(route_group_data)
session.add(route_group)
await session.commit()
await session.refresh(route_group)
await session.refresh(route_group, ["route_group_memberships"])
return route_group

async def update_route_group(
Expand All @@ -46,7 +46,7 @@ async def update_route_group(
setattr(route_group, field, value)

await session.commit()
await session.refresh(route_group)
await session.refresh(route_group, ["route_group_memberships"])

return route_group

Expand Down
7 changes: 4 additions & 3 deletions backend/python/app/services/jobs/email_reminder_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from app.models.driver import Driver
from app.models.driver_assignment import DriverAssignment
from app.models.route import Route
from app.models.user import User
from app.services.implementations.email_service import EmailService


Expand Down Expand Up @@ -40,21 +41,21 @@ async def process_daily_reminder_emails() -> None:
# Get all drivers assigned to routes tomorrow
statement = (
select(
Driver.user.email,
User.email,
DriverAssignment.time,
Route.length,
)
.join(Route, DriverAssignment.route_id == Route.route_id) # type: ignore[arg-type]
.join(Driver, DriverAssignment.driver_id == Driver.driver_id) # type: ignore[arg-type]
.join(User, Driver.user_id == User.user_id) # type: ignore[arg-type]
.where(
and_(
Driver.user.email is not None, # type: ignore[arg-type]
DriverAssignment.time >= start_of_day, # type: ignore[arg-type]
DriverAssignment.time <= end_of_day, # type: ignore[arg-type]
DriverAssignment.completed.is_(False), # type: ignore[attr-defined]
)
)
.order_by(Driver.user.email)
.order_by(User.email)
)

result = await session.execute(statement)
Expand Down
2 changes: 1 addition & 1 deletion backend/python/mypy.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[mypy]
python_version = 3.10
python_version = 3.11
incremental = True
cache_dir = .mypy_cache
warn_return_any = True
Expand Down
2 changes: 1 addition & 1 deletion backend/python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ exclude = [
line-length = 88
indent-width = 4

# Assume Python 3.10+
# Assume Python 3.11+
target-version = "py310"

[tool.ruff.lint]
Expand Down
2 changes: 1 addition & 1 deletion backend/python/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ numpy==1.26.4
scikit-learn==1.5.0
scikit-learn-extra==0.2.0
seaborn==0.13.2
matplotlib==3.10.0
matplotlib>=3.10.8
pandas==2.3.3
pandas-stubs
types-seaborn
Expand Down
16 changes: 5 additions & 11 deletions backend/python/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ async def test_db_engine() -> AsyncGenerator[Any, None]:

# Use PostgreSQL for testing to support ARRAY types
database_url = os.getenv(
"TEST_DATABASE_URL", "postgresql+asyncpg://postgres:postgres@db:5432/f4k"
"TEST_DATABASE_URL", "postgresql+asyncpg://postgres:postgres@db:5432/f4k_test"
)

engine = create_async_engine(
Expand Down Expand Up @@ -105,11 +105,8 @@ def client(test_session: AsyncSession) -> Generator[TestClient, None, None]:
app = create_app()

# Override the database session dependency
def override_get_session() -> AsyncGenerator[AsyncSession, None]:
async def _get_session() -> AsyncGenerator[AsyncSession, None]:
yield test_session

return _get_session()
async def override_get_session() -> AsyncGenerator[AsyncSession, None]:
yield test_session

app.dependency_overrides[get_session] = override_get_session

Expand All @@ -125,11 +122,8 @@ async def async_client(
app = create_app()

# Override the database session dependency
def override_get_session() -> AsyncGenerator[AsyncSession, None]:
async def _get_session() -> AsyncGenerator[AsyncSession, None]:
yield test_session

return _get_session()
async def override_get_session() -> AsyncGenerator[AsyncSession, None]:
yield test_session

app.dependency_overrides[get_session] = override_get_session

Expand Down
32 changes: 32 additions & 0 deletions backend/python/tests/data/test_locations.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
address,city,postal_code,province,latitude,longitude,formatted_address,place_id,apartment_unit
123 Fake Street,Test City,N1A 1A1,Ontario,43.4500000,-80.5000000,"123 Fake St, Test City, ON N1A 1A1, Canada",TEST_PLACE_ID_001,
456 Mock Avenue,Test City,N1B 2B2,Ontario,43.4510000,-80.5010000,"456 Mock Ave, Test City, ON N1B 2B2, Canada",TEST_PLACE_ID_002,
789 Sample Road,Test City,N1C 3C3,Ontario,43.4520000,-80.5020000,"789 Sample Rd, Test City, ON N1C 3C3, Canada",TEST_PLACE_ID_003,
321 Test Boulevard,Test City,N1D 4D4,Ontario,43.4530000,-80.5030000,"321 Test Blvd, Test City, ON N1D 4D4, Canada",TEST_PLACE_ID_004,
654 Example Lane,Test City,N1E 5E5,Ontario,43.4540000,-80.5040000,"654 Example Ln, Test City, ON N1E 5E5, Canada",TEST_PLACE_ID_005,
987 Demo Drive,Test City,N1F 6F6,Ontario,43.4550000,-80.5050000,"987 Demo Dr, Test City, ON N1F 6F6, Canada",TEST_PLACE_ID_006,
147 Virtual Way,Test City,N1G 7G7,Ontario,43.4560000,-80.5060000,"147 Virtual Way, Test City, ON N1G 7G7, Canada",TEST_PLACE_ID_007,
258 Simulated Street,Test City,N1H 8H8,Ontario,43.4570000,-80.5070000,"258 Simulated St, Test City, ON N1H 8H8, Canada",TEST_PLACE_ID_008,
369 Dummy Avenue,Test City,N1I 9I9,Ontario,43.4580000,-80.5080000,"369 Dummy Ave, Test City, ON N1I 9I9, Canada",TEST_PLACE_ID_009,
741 Placeholder Road,Test City,N1J 0J0,Ontario,43.4590000,-80.5090000,"741 Placeholder Rd, Test City, ON N1J 0J0, Canada",TEST_PLACE_ID_010,
852 Synthetic Boulevard,Test City,N1K 1K1,Ontario,43.4600000,-80.5100000,"852 Synthetic Blvd, Test City, ON N1K 1K1, Canada",TEST_PLACE_ID_011,
963 Artificial Lane,Test City,N1L 2L2,Ontario,43.4610000,-80.5110000,"963 Artificial Ln, Test City, ON N1L 2L2, Canada",TEST_PLACE_ID_012,
159 Constructed Drive,Test City,N1M 3M3,Ontario,43.4620000,-80.5120000,"159 Constructed Dr, Test City, ON N1M 3M3, Canada",TEST_PLACE_ID_013,
357 Generated Way,Test City,N1N 4N4,Ontario,43.4630000,-80.5130000,"357 Generated Way, Test City, ON N1N 4N4, Canada",TEST_PLACE_ID_014,
468 Fabricated Street,Test City,N1O 5O5,Ontario,43.4640000,-80.5140000,"468 Fabricated St, Test City, ON N1O 5O5, Canada",TEST_PLACE_ID_015,
579 Invented Avenue,Test City,N1P 6P6,Ontario,43.4650000,-80.5150000,"579 Invented Ave, Test City, ON N1P 6P6, Canada",TEST_PLACE_ID_016,
680 Manufactured Road,Test City,N1Q 7Q7,Ontario,43.4660000,-80.5160000,"680 Manufactured Rd, Test City, ON N1Q 7Q7, Canada",TEST_PLACE_ID_017,
791 Created Boulevard,Test City,N1R 8R8,Ontario,43.4670000,-80.5170000,"791 Created Blvd, Test City, ON N1R 8R8, Canada",TEST_PLACE_ID_018,
802 Built Lane,Test City,N1S 9S9,Ontario,43.4680000,-80.5180000,"802 Built Ln, Test City, ON N1S 9S9, Canada",TEST_PLACE_ID_019,
913 Designed Drive,Test City,N1T 0T0,Ontario,43.4690000,-80.5190000,"913 Designed Dr, Test City, ON N1T 0T0, Canada",TEST_PLACE_ID_020,
124 Formed Way,Test City,N1U 1U1,Ontario,43.4700000,-80.5200000,"124 Formed Way, Test City, ON N1U 1U1, Canada",TEST_PLACE_ID_021,
235 Shaped Street,Test City,N1V 2V2,Ontario,43.4710000,-80.5210000,"235 Shaped St, Test City, ON N1V 2V2, Canada",TEST_PLACE_ID_022,
346 Molded Avenue,Test City,N1W 3W3,Ontario,43.4720000,-80.5220000,"346 Molded Ave, Test City, ON N1W 3W3, Canada",TEST_PLACE_ID_023,
457 Crafted Road,Test City,N1X 4X4,Ontario,43.4730000,-80.5230000,"457 Crafted Rd, Test City, ON N1X 4X4, Canada",TEST_PLACE_ID_024,
568 Forged Boulevard,Test City,N1Y 5Y5,Ontario,43.4740000,-80.5240000,"568 Forged Blvd, Test City, ON N1Y 5Y5, Canada",TEST_PLACE_ID_025,
679 Assembled Lane,Test City,N1Z 6Z6,Ontario,43.4750000,-80.5250000,"679 Assembled Ln, Test City, ON N1Z 6Z6, Canada",TEST_PLACE_ID_026,
780 Produced Drive,Test City,N2A 7A7,Ontario,43.4760000,-80.5260000,"780 Produced Dr, Test City, ON N2A 7A7, Canada",TEST_PLACE_ID_027,
891 Developed Way,Test City,N2B 8B8,Ontario,43.4770000,-80.5270000,"891 Developed Way, Test City, ON N2B 8B8, Canada",TEST_PLACE_ID_028,
902 Established Street,Test City,N2C 9C9,Ontario,43.4780000,-80.5280000,"902 Established St, Test City, ON N2C 9C9, Canada",TEST_PLACE_ID_029,
103 Organized Avenue,Test City,N2D 0D0,Ontario,43.4790000,-80.5290000,"103 Organized Ave, Test City, ON N2D 0D0, Canada",TEST_PLACE_ID_030,

Loading