From f2e91ce5598e1777ea642baaca291d0565b3e4f2 Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Mon, 23 Mar 2026 00:12:45 -0500 Subject: [PATCH 01/27] server - update makefile --- server/makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/makefile b/server/makefile index 1502ccab..5b1ac774 100644 --- a/server/makefile +++ b/server/makefile @@ -1,4 +1,4 @@ -.PHONY: dev test cov open-cov check check_migrations manual_migration auto_migration migrate +.PHONY: dev test cov open-cov check check_migrations manual_migration auto_migration migrate downgrade dev: uv run uvicorn app.main:app \ From 6e20b33b4147eedd705ea20a2ef430ee9505e00a Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Mon, 23 Mar 2026 09:33:34 -0500 Subject: [PATCH 02/27] server - validate non-nullable fields in update requests --- server/app/models/schemas/exercise.py | 14 ++++++- server/app/models/schemas/workout.py | 9 ++++- .../api/exercise/test_update_exercise.py | 38 +++++++++++++++++++ .../tests/api/workout/test_update_workout.py | 20 ++++++++++ .../app/tests/models/schemas/test_exercise.py | 13 +++++++ .../app/tests/models/schemas/test_workout.py | 8 ++++ 6 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 server/app/tests/models/schemas/test_exercise.py create mode 100644 server/app/tests/models/schemas/test_workout.py diff --git a/server/app/models/schemas/exercise.py b/server/app/models/schemas/exercise.py index 6226b91d..4b64d770 100644 --- a/server/app/models/schemas/exercise.py +++ b/server/app/models/schemas/exercise.py @@ -1,6 +1,7 @@ from datetime import datetime +from typing import Self -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, model_validator from .muscle_group import MuscleGroupPublic from .types import ExerciseName @@ -29,3 +30,14 @@ class UpdateExerciseRequest(BaseModel): name: ExerciseName | None = None description: str | None = None muscle_group_ids: list[int] | None = None + + @model_validator(mode="after") + def validate_non_nullable_fields(self) -> Self: + if "name" in self.model_fields_set and self.name is None: + raise ValueError("name cannot be null") + if ( + "muscle_group_ids" in self.model_fields_set + and self.muscle_group_ids is None + ): + raise ValueError("muscle_group_ids cannot be null") + return self diff --git a/server/app/models/schemas/workout.py b/server/app/models/schemas/workout.py index ce054bd8..2d788afa 100644 --- a/server/app/models/schemas/workout.py +++ b/server/app/models/schemas/workout.py @@ -1,6 +1,7 @@ from datetime import datetime +from typing import Self -from pydantic import BaseModel +from pydantic import BaseModel, model_validator from .workout_exercise import WorkoutExercisePublic @@ -29,3 +30,9 @@ class UpdateWorkoutRequest(BaseModel): started_at: datetime | None = None ended_at: datetime | None = None notes: str | None = None + + @model_validator(mode="after") + def validate_non_nullable_fields(self) -> Self: + if "started_at" in self.model_fields_set and self.started_at is None: + raise ValueError("started_at cannot be null") + return self diff --git a/server/app/tests/api/exercise/test_update_exercise.py b/server/app/tests/api/exercise/test_update_exercise.py index 0d07ac95..b1d3359d 100644 --- a/server/app/tests/api/exercise/test_update_exercise.py +++ b/server/app/tests/api/exercise/test_update_exercise.py @@ -139,3 +139,41 @@ async def test_update_exercise_name_conflict( assert resp.status_code == ExerciseNameConflict.status_code body = resp.json() assert body["detail"] == ExerciseNameConflict.detail + + +# 422 +async def test_update_exercise_name_null( + client: AsyncClient, + session: AsyncSession, + settings: Settings, +): + await login_admin(client, settings) + created = await create_exercise_via_api(client, session, name="Old Name") + + resp = await make_http_request( + client, + method=HttpMethod.PATCH, + endpoint=f"/api/exercises/{created.id}", + json={"name": None}, + ) + + assert resp.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT + + +# 422 +async def test_update_exercise_muscle_group_ids_null( + client: AsyncClient, + session: AsyncSession, + settings: Settings, +): + await login_admin(client, settings) + created = await create_exercise_via_api(client, session, name="Old Name") + + resp = await make_http_request( + client, + method=HttpMethod.PATCH, + endpoint=f"/api/exercises/{created.id}", + json={"muscle_group_ids": None}, + ) + + assert resp.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT diff --git a/server/app/tests/api/workout/test_update_workout.py b/server/app/tests/api/workout/test_update_workout.py index 8b35b280..cac58ecb 100644 --- a/server/app/tests/api/workout/test_update_workout.py +++ b/server/app/tests/api/workout/test_update_workout.py @@ -109,3 +109,23 @@ async def test_update_workout_not_allowed( assert resp.status_code == WorkoutNotFound.status_code body = resp.json() assert body["detail"] == WorkoutNotFound.detail + + +# 422 +async def test_update_workout_started_at_null( + client: AsyncClient, + session: AsyncSession, + settings: Settings, +): + await login_admin(client, settings) + admin = await get_admin(session, settings) + workout = await create_workout(session, user_id=admin.id) + + resp = await make_http_request( + client, + method=HttpMethod.PATCH, + endpoint=f"/api/workouts/{workout.id}", + json={"started_at": None}, + ) + + assert resp.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT diff --git a/server/app/tests/models/schemas/test_exercise.py b/server/app/tests/models/schemas/test_exercise.py new file mode 100644 index 00000000..4f8226ad --- /dev/null +++ b/server/app/tests/models/schemas/test_exercise.py @@ -0,0 +1,13 @@ +import pytest + +from app.models.schemas.exercise import UpdateExerciseRequest + + +def test_update_exercise_request_null_name(): + with pytest.raises(ValueError, match="name cannot be null"): + UpdateExerciseRequest(name=None) + + +def test_update_exercise_request_null_muscle_group_ids(): + with pytest.raises(ValueError, match="muscle_group_ids cannot be null"): + UpdateExerciseRequest(muscle_group_ids=None) diff --git a/server/app/tests/models/schemas/test_workout.py b/server/app/tests/models/schemas/test_workout.py new file mode 100644 index 00000000..a45481ca --- /dev/null +++ b/server/app/tests/models/schemas/test_workout.py @@ -0,0 +1,8 @@ +import pytest + +from app.models.schemas.workout import UpdateWorkoutRequest + + +def test_update_workout_request_null_started_at(): + with pytest.raises(ValueError, match="started_at cannot be null"): + UpdateWorkoutRequest(started_at=None) From 6196b274a255bb79aa1a6233fc16ee35dcfdd0fd Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Mon, 23 Mar 2026 11:05:22 -0500 Subject: [PATCH 03/27] server - implement unique constraint handling --- server/app/core/database.py | 25 +++++++++++ server/app/models/database/exercise.py | 4 +- server/app/models/database/set.py | 4 +- .../app/models/database/workout_exercise.py | 4 +- server/app/services/exercise.py | 11 +++-- server/app/services/set.py | 7 +++- server/app/services/workout_exercise.py | 10 ++++- .../core/database/test_is_unique_violation.py | 41 ++++++++++++++++++ .../services/exercise/test_create_exercise.py | 20 +++++++++ .../services/exercise/test_update_exercise.py | 26 ++++++++++++ .../app/tests/services/set/test_create_set.py | 42 +++++++++++++++++++ .../test_create_workout_exercise.py | 36 ++++++++++++++++ 12 files changed, 220 insertions(+), 10 deletions(-) create mode 100644 server/app/tests/core/database/test_is_unique_violation.py diff --git a/server/app/core/database.py b/server/app/core/database.py index fa2b68a5..05c27aee 100644 --- a/server/app/core/database.py +++ b/server/app/core/database.py @@ -1,5 +1,30 @@ +import logging + +from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import DeclarativeBase +UNIQUE_VIOLATION_CODE = "23505" + + +logger = logging.getLogger(__name__) + class Base(DeclarativeBase): pass + + +def is_unique_violation( + error: IntegrityError, + constraint_name: str, +) -> bool: + sqlstate = getattr(getattr(error, "orig", None), "sqlstate", None) + if sqlstate != UNIQUE_VIOLATION_CODE: + logger.info(f"SQL state {sqlstate} does not indicate unique violation") + return False + + logger.info("SQL state indicates unique violation, checking constraint name") + if constraint_name in str(error): + logger.info(f"Constraint name {constraint_name} found in error message") + return True + logger.info(f"Constraint name {constraint_name} not found in error message") + return False diff --git a/server/app/models/database/exercise.py b/server/app/models/database/exercise.py index 502f435c..185f9ba7 100644 --- a/server/app/models/database/exercise.py +++ b/server/app/models/database/exercise.py @@ -9,6 +9,8 @@ if TYPE_CHECKING: from app.models.database.exercise_muscle_group import ExerciseMuscleGroup +EXERCISE_UNIQUE_CONSTRAINT = "uq_exercises_user_name" + class Exercise(Base): __tablename__ = "exercises" @@ -20,7 +22,7 @@ class Exercise(Base): UniqueConstraint( "user_id", "name", - name="uq_exercises_user_name", + name=EXERCISE_UNIQUE_CONSTRAINT, postgresql_nulls_not_distinct=True, ), ) diff --git a/server/app/models/database/set.py b/server/app/models/database/set.py index 4cad7fb4..243212c8 100644 --- a/server/app/models/database/set.py +++ b/server/app/models/database/set.py @@ -22,6 +22,8 @@ if TYPE_CHECKING: from app.models.database.workout_exercise import WorkoutExercise +SET_UNIQUE_CONSTRAINT = "uq_sets_workout_exercise_set_number" + class Set(Base): __tablename__ = "sets" @@ -30,7 +32,7 @@ class Set(Base): UniqueConstraint( "workout_exercise_id", "set_number", - name="uq_sets_workout_exercise_set_number", + name=SET_UNIQUE_CONSTRAINT, ), CheckConstraint("set_number > 0", name="ck_sets_set_number_positive"), CheckConstraint("reps IS NULL OR reps >= 0", name="ck_sets_reps_non_negative"), diff --git a/server/app/models/database/workout_exercise.py b/server/app/models/database/workout_exercise.py index 08a3e8f3..d90feff8 100644 --- a/server/app/models/database/workout_exercise.py +++ b/server/app/models/database/workout_exercise.py @@ -20,6 +20,8 @@ from app.models.database.set import Set from app.models.database.workout import Workout +WORKOUT_EXERCISE_UNIQUE_CONSTRAINT = "uq_workout_exercises_workout_position" + class WorkoutExercise(Base): __tablename__ = "workout_exercises" @@ -35,7 +37,7 @@ class WorkoutExercise(Base): UniqueConstraint( "workout_id", "position", - name="uq_workout_exercises_workout_position", + name=WORKOUT_EXERCISE_UNIQUE_CONSTRAINT, ), CheckConstraint( "position > 0", diff --git a/server/app/services/exercise.py b/server/app/services/exercise.py index c4d57402..e8536010 100644 --- a/server/app/services/exercise.py +++ b/server/app/services/exercise.py @@ -7,7 +7,8 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload -from app.models.database.exercise import Exercise +from app.core.database import is_unique_violation +from app.models.database.exercise import EXERCISE_UNIQUE_CONSTRAINT, Exercise from app.models.database.exercise_muscle_group import ExerciseMuscleGroup from app.models.errors import ( ExerciseNameConflict, @@ -102,7 +103,9 @@ async def create_exercise( except IntegrityError as e: logger.error(f"Integrity error creating exercise: {e}") await db.rollback() - raise ExerciseNameConflict() + if is_unique_violation(e, EXERCISE_UNIQUE_CONSTRAINT): + raise ExerciseNameConflict() + raise for mg in muscle_groups: db.add( @@ -189,7 +192,9 @@ async def update_exercise( except IntegrityError as e: logger.error(f"Integrity error updating exercise: {e}") await db.rollback() - raise ExerciseNameConflict() + if is_unique_violation(e, EXERCISE_UNIQUE_CONSTRAINT): + raise ExerciseNameConflict() + raise async def delete_exercise( diff --git a/server/app/services/set.py b/server/app/services/set.py index b380def3..651d7bbb 100644 --- a/server/app/services/set.py +++ b/server/app/services/set.py @@ -6,7 +6,8 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession -from app.models.database.set import Set +from app.core.database import is_unique_violation +from app.models.database.set import SET_UNIQUE_CONSTRAINT, Set from app.models.database.workout_exercise import WorkoutExercise from app.models.errors import ( SetNotFound, @@ -95,7 +96,9 @@ async def create_set( except IntegrityError as e: logger.error(f"Integrity error creating set: {e}") await db.rollback() - raise SetNumberConflict() + if is_unique_violation(e, SET_UNIQUE_CONSTRAINT): + raise SetNumberConflict() + raise async def update_set( diff --git a/server/app/services/workout_exercise.py b/server/app/services/workout_exercise.py index 59a23113..7dc5a4cf 100644 --- a/server/app/services/workout_exercise.py +++ b/server/app/services/workout_exercise.py @@ -7,8 +7,12 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload +from app.core.database import is_unique_violation from app.models.database.exercise import Exercise -from app.models.database.workout_exercise import WorkoutExercise +from app.models.database.workout_exercise import ( + WORKOUT_EXERCISE_UNIQUE_CONSTRAINT, + WorkoutExercise, +) from app.models.errors import ( ExerciseNotFound, WorkoutExerciseNotFound, @@ -105,7 +109,9 @@ async def create_workout_exercise( except IntegrityError as e: logger.error(f"Integrity error creating workout exercise: {e}") await db.rollback() - raise WorkoutExercisePositionConflict() + if is_unique_violation(e, WORKOUT_EXERCISE_UNIQUE_CONSTRAINT): + raise WorkoutExercisePositionConflict() + raise async def delete_workout_exercise( diff --git a/server/app/tests/core/database/test_is_unique_violation.py b/server/app/tests/core/database/test_is_unique_violation.py new file mode 100644 index 00000000..cee1496c --- /dev/null +++ b/server/app/tests/core/database/test_is_unique_violation.py @@ -0,0 +1,41 @@ +from sqlalchemy.exc import IntegrityError + +from app.core.database import UNIQUE_VIOLATION_CODE, is_unique_violation + + +class _DummyOrig: + def __init__(self, sqlstate: str, msg: str): + self.sqlstate = sqlstate + self.msg = msg + + def __str__(self): + return self.msg + + +def _make_error(sqlstate: str, msg: str): + orig = _DummyOrig(sqlstate, msg) + return IntegrityError( + statement=None, + params=None, + orig=orig, # pyright: ignore[reportArgumentType] + ) + + +def test_is_unique_violation(): + err = _make_error( + UNIQUE_VIOLATION_CODE, + "duplicate key value violates unique constraint 'uq_test' foo", + ) + assert is_unique_violation(err, "uq_test") is True + + +def test_is_unique_violation_wrong_code(): + err = _make_error( + "99999", "duplicate key value violates unique constraint 'uq_test' foo" + ) + assert is_unique_violation(err, "uq_test") is False + + +def test_is_unique_violation_wrong_message(): + err = _make_error(UNIQUE_VIOLATION_CODE, "some other error") + assert is_unique_violation(err, "uq_test") is False diff --git a/server/app/tests/services/exercise/test_create_exercise.py b/server/app/tests/services/exercise/test_create_exercise.py index 9bb7da51..62e507c2 100644 --- a/server/app/tests/services/exercise/test_create_exercise.py +++ b/server/app/tests/services/exercise/test_create_exercise.py @@ -1,4 +1,7 @@ +from unittest.mock import patch + import pytest +from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from app.models.database.exercise import Exercise @@ -66,3 +69,20 @@ async def test_create_exercise_name_conflict(session: AsyncSession): CreateExerciseRequest(name="Bench", muscle_group_ids=[]), session, ) + + +async def test_create_exercise_unhandled_integrity_error(session: AsyncSession): + user = await create_user(session) + await create_exercise( + user.id, + CreateExerciseRequest(name="Bench", muscle_group_ids=[]), + session, + ) + + with patch("app.services.exercise.is_unique_violation", return_value=False): + with pytest.raises(IntegrityError): + await create_exercise( + user.id, + CreateExerciseRequest(name="Bench", muscle_group_ids=[]), + session, + ) diff --git a/server/app/tests/services/exercise/test_update_exercise.py b/server/app/tests/services/exercise/test_update_exercise.py index c6f94f99..b78a0cb9 100644 --- a/server/app/tests/services/exercise/test_update_exercise.py +++ b/server/app/tests/services/exercise/test_update_exercise.py @@ -1,4 +1,7 @@ +from unittest.mock import patch + import pytest +from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from app.models.errors import ( @@ -199,3 +202,26 @@ async def test_update_exercise_name_conflict(session: AsyncSession): UpdateExerciseRequest(name="Bench"), session, ) + + +async def test_update_exercise_unhandled_integrity_error(session: AsyncSession): + user = await create_user(session) + await create_exercise( + session, + name="Bench", + user_id=user.id, + ) + exercise = await create_exercise( + session, + name="Press", + user_id=user.id, + ) + + with patch("app.services.exercise.is_unique_violation", return_value=False): + with pytest.raises(IntegrityError): + await update_exercise( + exercise.id, + user.id, + UpdateExerciseRequest(name="Bench"), + session, + ) diff --git a/server/app/tests/services/set/test_create_set.py b/server/app/tests/services/set/test_create_set.py index efebe41b..bdf8d8ce 100644 --- a/server/app/tests/services/set/test_create_set.py +++ b/server/app/tests/services/set/test_create_set.py @@ -1,8 +1,10 @@ from decimal import Decimal +from unittest.mock import patch import pytest from pytest import MonkeyPatch from sqlalchemy import select +from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from app.models.database.set import Set @@ -162,3 +164,43 @@ async def mock_get_next_set_number( req=CreateSetRequest(), db=session, ) + + +async def test_create_set_unhandled_integrity_error( + session: AsyncSession, + monkeypatch: MonkeyPatch, +): + user = await create_user(session) + workout = await create_workout(session, user_id=user.id) + exercise = await create_exercise(session, name="Bench Press") + workout_exercise = await create_workout_exercise( + session, + workout_id=workout.id, + exercise_id=exercise.id, + position=1, + ) + + await create_set_util( + session, + workout_exercise_id=workout_exercise.id, + set_number=1, + ) + + async def mock_get_next_set_number( + workout_exercise_id: int, db: AsyncSession + ) -> int: + return 1 + + monkeypatch.setattr( + "app.services.set._get_next_set_number", mock_get_next_set_number + ) + + with patch("app.services.set.is_unique_violation", return_value=False): + with pytest.raises(IntegrityError): + await create_set( + workout_id=workout.id, + workout_exercise_id=workout_exercise.id, + user_id=user.id, + req=CreateSetRequest(), + db=session, + ) diff --git a/server/app/tests/services/workout_exercise/test_create_workout_exercise.py b/server/app/tests/services/workout_exercise/test_create_workout_exercise.py index f1079ca4..d0cd730a 100644 --- a/server/app/tests/services/workout_exercise/test_create_workout_exercise.py +++ b/server/app/tests/services/workout_exercise/test_create_workout_exercise.py @@ -1,6 +1,9 @@ +from unittest.mock import patch + import pytest from pytest import MonkeyPatch from sqlalchemy import select +from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from app.models.database.workout_exercise import WorkoutExercise @@ -132,3 +135,36 @@ async def mock_get_next_position(workout_id: int, db: AsyncSession) -> int: CreateWorkoutExerciseRequest(exercise_id=exercise.id), session, ) + + +async def test_create_workout_exercise_unhandled_integrity_error( + session: AsyncSession, + monkeypatch: MonkeyPatch, +): + user = await create_user(session) + workout = await create_workout(session, user_id=user.id) + exercise = await create_exercise(session, name="Exercise 1") + + await create_workout_exercise_util( + session, + workout_id=workout.id, + exercise_id=exercise.id, + position=1, + ) + + async def mock_get_next_position(workout_id: int, db: AsyncSession) -> int: + return 1 + + monkeypatch.setattr( + "app.services.workout_exercise._get_next_workout_exercise_position", + mock_get_next_position, + ) + + with patch("app.services.workout_exercise.is_unique_violation", return_value=False): + with pytest.raises(IntegrityError): + await create_workout_exercise( + workout.id, + user.id, + CreateWorkoutExerciseRequest(exercise_id=exercise.id), + session, + ) From 7ceb3f441bad9ea33f459b7d7bc127fe26d6451c Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Mon, 23 Mar 2026 11:13:31 -0500 Subject: [PATCH 04/27] server - refactor api routes --- PROJECT_OVERVIEW.md | 10 +++++----- client/src/api/generated/sdk.gen.ts | 10 +++++----- client/src/api/generated/types.gen.ts | 10 +++++----- server/app/api/endpoints/set.py | 8 ++++---- server/app/api/endpoints/workout_exercise.py | 6 +++--- server/app/tests/api/set/test_create_set.py | 2 +- server/app/tests/api/set/test_delete_set.py | 2 +- server/app/tests/api/set/test_update_set.py | 2 +- .../workout_exercise/test_create_workout_exercise.py | 2 +- .../workout_exercise/test_delete_workout_exercise.py | 2 +- 10 files changed, 27 insertions(+), 27 deletions(-) diff --git a/PROJECT_OVERVIEW.md b/PROJECT_OVERVIEW.md index 53f740a1..b0afd804 100644 --- a/PROJECT_OVERVIEW.md +++ b/PROJECT_OVERVIEW.md @@ -114,14 +114,14 @@ Basic relationships: **Workout Exercises** -- `POST /api/workout-exercises/{workout_id}/exercises` — add an exercise to a workout -- `DELETE /api/workout-exercises/{workout_id}/exercises/{workout_exercise_id}` — remove exercise from workout +- `POST /api/workouts/{workout_id}/exercises` — add an exercise to a workout +- `DELETE /api/workouts/{workout_id}/exercises/{workout_exercise_id}` — remove exercise from workout **Sets** -- `POST /api/sets/{workout_id}/exercises/{workout_exercise_id}/sets` — log a set -- `PATCH /api/sets/{workout_id}/exercises/{workout_exercise_id}/sets/{set_id}` — update a set -- `DELETE /api/sets/{workout_id}/exercises/{workout_exercise_id}/sets/{set_id}` — delete a set +- `POST /api/workouts/{workout_id}/exercises/{workout_exercise_id}/sets` — log a set +- `PATCH /api/workouts/{workout_id}/exercises/{workout_exercise_id}/sets/{set_id}` — update a set +- `DELETE /api/workouts/{workout_id}/exercises/{workout_exercise_id}/sets/{set_id}` — delete a set ## Infrastructure & Deployment diff --git a/client/src/api/generated/sdk.gen.ts b/client/src/api/generated/sdk.gen.ts index c04be93e..5782819a 100644 --- a/client/src/api/generated/sdk.gen.ts +++ b/client/src/api/generated/sdk.gen.ts @@ -329,7 +329,7 @@ export class SetService { name: 'access_token', type: 'apiKey' }], - url: '/api/sets/{workout_id}/exercises/{workout_exercise_id}/sets', + url: '/api/workouts/{workout_id}/exercises/{workout_exercise_id}/sets', ...options, headers: { 'Content-Type': 'application/json', @@ -348,7 +348,7 @@ export class SetService { name: 'access_token', type: 'apiKey' }], - url: '/api/sets/{workout_id}/exercises/{workout_exercise_id}/sets/{set_id}', + url: '/api/workouts/{workout_id}/exercises/{workout_exercise_id}/sets/{set_id}', ...options }); } @@ -363,7 +363,7 @@ export class SetService { name: 'access_token', type: 'apiKey' }], - url: '/api/sets/{workout_id}/exercises/{workout_exercise_id}/sets/{set_id}', + url: '/api/workouts/{workout_id}/exercises/{workout_exercise_id}/sets/{set_id}', ...options, headers: { 'Content-Type': 'application/json', @@ -402,7 +402,7 @@ export class WorkoutExerciseService { name: 'access_token', type: 'apiKey' }], - url: '/api/workout-exercises/{workout_id}/exercises', + url: '/api/workouts/{workout_id}/exercises', ...options, headers: { 'Content-Type': 'application/json', @@ -421,7 +421,7 @@ export class WorkoutExerciseService { name: 'access_token', type: 'apiKey' }], - url: '/api/workout-exercises/{workout_id}/exercises/{workout_exercise_id}', + url: '/api/workouts/{workout_id}/exercises/{workout_exercise_id}', ...options }); } diff --git a/client/src/api/generated/types.gen.ts b/client/src/api/generated/types.gen.ts index 615ecb73..abaf620b 100644 --- a/client/src/api/generated/types.gen.ts +++ b/client/src/api/generated/types.gen.ts @@ -1212,7 +1212,7 @@ export type CreateSetData = { workout_exercise_id: number; }; query?: never; - url: '/api/sets/{workout_id}/exercises/{workout_exercise_id}/sets'; + url: '/api/workouts/{workout_id}/exercises/{workout_exercise_id}/sets'; }; export type CreateSetErrors = { @@ -1262,7 +1262,7 @@ export type DeleteSetData = { set_id: number; }; query?: never; - url: '/api/sets/{workout_id}/exercises/{workout_exercise_id}/sets/{set_id}'; + url: '/api/workouts/{workout_id}/exercises/{workout_exercise_id}/sets/{set_id}'; }; export type DeleteSetErrors = { @@ -1308,7 +1308,7 @@ export type UpdateSetData = { set_id: number; }; query?: never; - url: '/api/sets/{workout_id}/exercises/{workout_exercise_id}/sets/{set_id}'; + url: '/api/workouts/{workout_id}/exercises/{workout_exercise_id}/sets/{set_id}'; }; export type UpdateSetErrors = { @@ -1371,7 +1371,7 @@ export type CreateWorkoutExerciseData = { workout_id: number; }; query?: never; - url: '/api/workout-exercises/{workout_id}/exercises'; + url: '/api/workouts/{workout_id}/exercises'; }; export type CreateWorkoutExerciseErrors = { @@ -1417,7 +1417,7 @@ export type DeleteWorkoutExerciseData = { workout_exercise_id: number; }; query?: never; - url: '/api/workout-exercises/{workout_id}/exercises/{workout_exercise_id}'; + url: '/api/workouts/{workout_id}/exercises/{workout_exercise_id}'; }; export type DeleteWorkoutExerciseErrors = { diff --git a/server/app/api/endpoints/set.py b/server/app/api/endpoints/set.py index da9a1eed..2beb4100 100644 --- a/server/app/api/endpoints/set.py +++ b/server/app/api/endpoints/set.py @@ -10,14 +10,14 @@ from app.services.set import create_set, delete_set, update_set api_router = APIRouter( - prefix="/sets", + prefix="/workouts/{workout_id}/exercises/{workout_exercise_id}/sets", tags=["Set"], dependencies=[Depends(get_current_user)], ) @api_router.post( - "/{workout_id}/exercises/{workout_exercise_id}/sets", + "", operation_id="createSet", status_code=status.HTTP_204_NO_CONTENT, responses={ @@ -37,7 +37,7 @@ async def create_set_endpoint( @api_router.patch( - "/{workout_id}/exercises/{workout_exercise_id}/sets/{set_id}", + "/{set_id}", operation_id="updateSet", status_code=status.HTTP_204_NO_CONTENT, responses={ @@ -57,7 +57,7 @@ async def update_set_endpoint( @api_router.delete( - "/{workout_id}/exercises/{workout_exercise_id}/sets/{set_id}", + "/{set_id}", operation_id="deleteSet", status_code=status.HTTP_204_NO_CONTENT, responses={ diff --git a/server/app/api/endpoints/workout_exercise.py b/server/app/api/endpoints/workout_exercise.py index 6576da23..db8514b0 100644 --- a/server/app/api/endpoints/workout_exercise.py +++ b/server/app/api/endpoints/workout_exercise.py @@ -15,14 +15,14 @@ ) api_router = APIRouter( - prefix="/workout-exercises", + prefix="/workouts/{workout_id}/exercises", tags=["Workout Exercise"], dependencies=[Depends(get_current_user)], ) @api_router.post( - "/{workout_id}/exercises", + "", operation_id="createWorkoutExercise", status_code=status.HTTP_204_NO_CONTENT, responses={ @@ -41,7 +41,7 @@ async def create_workout_exercise_endpoint( @api_router.delete( - "/{workout_id}/exercises/{workout_exercise_id}", + "/{workout_exercise_id}", operation_id="deleteWorkoutExercise", status_code=status.HTTP_204_NO_CONTENT, responses={ diff --git a/server/app/tests/api/set/test_create_set.py b/server/app/tests/api/set/test_create_set.py index 26fbeb72..f3957b72 100644 --- a/server/app/tests/api/set/test_create_set.py +++ b/server/app/tests/api/set/test_create_set.py @@ -35,7 +35,7 @@ async def _make_request( return await make_http_request( client, method=HttpMethod.POST, - endpoint=f"/api/sets/{workout_id}/exercises/{workout_exercise_id}/sets", + endpoint=f"/api/workouts/{workout_id}/exercises/{workout_exercise_id}/sets", json={ "reps": reps, "weight": weight, diff --git a/server/app/tests/api/set/test_delete_set.py b/server/app/tests/api/set/test_delete_set.py index 4ccc7227..2c18fba3 100644 --- a/server/app/tests/api/set/test_delete_set.py +++ b/server/app/tests/api/set/test_delete_set.py @@ -26,7 +26,7 @@ async def _make_request( return await make_http_request( client, method=HttpMethod.DELETE, - endpoint=f"/api/sets/{workout_id}/exercises/{workout_exercise_id}/sets/{set_id}", + endpoint=f"/api/workouts/{workout_id}/exercises/{workout_exercise_id}/sets/{set_id}", ) diff --git a/server/app/tests/api/set/test_update_set.py b/server/app/tests/api/set/test_update_set.py index 4c992e5c..a00384e3 100644 --- a/server/app/tests/api/set/test_update_set.py +++ b/server/app/tests/api/set/test_update_set.py @@ -30,7 +30,7 @@ async def _make_request( return await make_http_request( client, method=HttpMethod.PATCH, - endpoint=f"/api/sets/{workout_id}/exercises/{workout_exercise_id}/sets/{set_id}", + endpoint=f"/api/workouts/{workout_id}/exercises/{workout_exercise_id}/sets/{set_id}", json={ "reps": reps, "weight": weight, diff --git a/server/app/tests/api/workout_exercise/test_create_workout_exercise.py b/server/app/tests/api/workout_exercise/test_create_workout_exercise.py index 69513e2c..ac563166 100644 --- a/server/app/tests/api/workout_exercise/test_create_workout_exercise.py +++ b/server/app/tests/api/workout_exercise/test_create_workout_exercise.py @@ -31,7 +31,7 @@ async def _make_request( return await make_http_request( client, method=HttpMethod.POST, - endpoint=f"/api/workout-exercises/{workout_id}/exercises", + endpoint=f"/api/workouts/{workout_id}/exercises", json={ "exercise_id": exercise_id, "notes": notes, diff --git a/server/app/tests/api/workout_exercise/test_delete_workout_exercise.py b/server/app/tests/api/workout_exercise/test_delete_workout_exercise.py index e21768bd..10dd710f 100644 --- a/server/app/tests/api/workout_exercise/test_delete_workout_exercise.py +++ b/server/app/tests/api/workout_exercise/test_delete_workout_exercise.py @@ -25,7 +25,7 @@ async def _make_request( return await make_http_request( client, method=HttpMethod.DELETE, - endpoint=f"/api/workout-exercises/{workout_id}/exercises/{workout_exercise_id}", + endpoint=f"/api/workouts/{workout_id}/exercises/{workout_exercise_id}", ) From fc113ffc8989b1b554b6f6822f4052faf3469ef0 Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Mon, 23 Mar 2026 11:40:21 -0500 Subject: [PATCH 05/27] server - standardize schema types for string fields --- client/src/api/generated/schemas.gen.ts | 31 ++++++++++++------- client/src/api/generated/zod.gen.ts | 24 +++++++------- server/app/models/schemas/exercise.py | 6 ++-- server/app/models/schemas/feedback.py | 7 +++-- server/app/models/schemas/set.py | 6 ++-- server/app/models/schemas/types.py | 14 +++++++-- server/app/models/schemas/workout.py | 5 +-- server/app/models/schemas/workout_exercise.py | 3 +- 8 files changed, 58 insertions(+), 38 deletions(-) diff --git a/client/src/api/generated/schemas.gen.ts b/client/src/api/generated/schemas.gen.ts index 6339cfda..8667d263 100644 --- a/client/src/api/generated/schemas.gen.ts +++ b/client/src/api/generated/schemas.gen.ts @@ -90,7 +90,8 @@ export const CreateExerciseRequestSchema = { description: { anyOf: [ { - type: 'string' + type: 'string', + maxLength: 1000 }, { type: 'null' @@ -126,7 +127,7 @@ export const CreateFeedbackRequestSchema = { }, title: { type: 'string', - maxLength: 100, + maxLength: 1000, minLength: 1, title: 'Title' }, @@ -198,7 +199,8 @@ export const CreateSetRequestSchema = { notes: { anyOf: [ { - type: 'string' + type: 'string', + maxLength: 1000 }, { type: 'null' @@ -220,7 +222,8 @@ export const CreateWorkoutExerciseRequestSchema = { notes: { anyOf: [ { - type: 'string' + type: 'string', + maxLength: 1000 }, { type: 'null' @@ -265,7 +268,8 @@ export const CreateWorkoutRequestSchema = { notes: { anyOf: [ { - type: 'string' + type: 'string', + maxLength: 1000 }, { type: 'null' @@ -460,7 +464,7 @@ export const LoginRequestSchema = { anyOf: [ { type: 'string', - maxLength: 50, + maxLength: 255, minLength: 3 }, { @@ -530,7 +534,7 @@ export const RegisterRequestSchema = { }, username: { type: 'string', - maxLength: 50, + maxLength: 255, minLength: 3, title: 'Username' }, @@ -560,13 +564,13 @@ export const RequestAccessRequestSchema = { }, first_name: { type: 'string', - maxLength: 50, + maxLength: 255, minLength: 1, title: 'First Name' }, last_name: { type: 'string', - maxLength: 50, + maxLength: 255, minLength: 1, title: 'Last Name' } @@ -751,7 +755,8 @@ export const UpdateExerciseRequestSchema = { description: { anyOf: [ { - type: 'string' + type: 'string', + maxLength: 1000 }, { type: 'null' @@ -821,7 +826,8 @@ export const UpdateSetRequestSchema = { notes: { anyOf: [ { - type: 'string' + type: 'string', + maxLength: 1000 }, { type: 'null' @@ -863,7 +869,8 @@ export const UpdateWorkoutRequestSchema = { notes: { anyOf: [ { - type: 'string' + type: 'string', + maxLength: 1000 }, { type: 'null' diff --git a/client/src/api/generated/zod.gen.ts b/client/src/api/generated/zod.gen.ts index 8651f751..6bce8ba9 100644 --- a/client/src/api/generated/zod.gen.ts +++ b/client/src/api/generated/zod.gen.ts @@ -16,7 +16,7 @@ export const zAccessRequestStatus = z.enum([ */ export const zCreateExerciseRequest = z.object({ name: z.string().min(1).max(255), - description: z.string().nullish(), + description: z.string().max(1000).nullish(), muscle_group_ids: z.array(z.int()).optional() }); @@ -25,7 +25,7 @@ export const zCreateExerciseRequest = z.object({ */ export const zCreateWorkoutExerciseRequest = z.object({ exercise_id: z.int(), - notes: z.string().nullish() + notes: z.string().max(1000).nullish() }); /** @@ -34,7 +34,7 @@ export const zCreateWorkoutExerciseRequest = z.object({ export const zCreateWorkoutRequest = z.object({ started_at: z.iso.datetime().nullish(), ended_at: z.iso.datetime().nullish(), - notes: z.string().nullish() + notes: z.string().max(1000).nullish() }); /** @@ -68,7 +68,7 @@ export const zFeedbackType = z.enum(['feedback', 'feature']); export const zCreateFeedbackRequest = z.object({ type: zFeedbackType, url: z.string().min(1).max(1000), - title: z.string().min(1).max(100), + title: z.string().min(1).max(1000), description: z.string().min(1).max(10000), files: z.array(z.string()).optional() }); @@ -84,7 +84,7 @@ export const zForgotPasswordRequest = z.object({ * LoginRequest */ export const zLoginRequest = z.object({ - username: z.string().min(3).max(50).nullish(), + username: z.string().min(3).max(255).nullish(), email: z.email().max(255).nullish(), password: z.string().min(8).max(64) }); @@ -116,7 +116,7 @@ export const zExercisePublic = z.object({ */ export const zRegisterRequest = z.object({ token: z.string().min(1).max(64), - username: z.string().min(3).max(50), + username: z.string().min(3).max(255), password: z.string().min(8).max(64) }); @@ -125,8 +125,8 @@ export const zRegisterRequest = z.object({ */ export const zRequestAccessRequest = z.object({ email: z.email().max(255), - first_name: z.string().min(1).max(50), - last_name: z.string().min(1).max(50) + first_name: z.string().min(1).max(255), + last_name: z.string().min(1).max(255) }); /** @@ -190,7 +190,7 @@ export const zCreateSetRequest = z.object({ z.string().regex(/^(?!^[-+.]*$)[+-]?0*(?:\d{0,4}|(?=[\d.]{1,7}0*$)\d{0,4}\.\d{0,2}0*$)/) ]).nullish(), unit: zSetUnit.nullish(), - notes: z.string().nullish() + notes: z.string().max(1000).nullish() }); /** @@ -205,7 +205,7 @@ export const zUpdateAccessRequestStatusRequest = z.object({ */ export const zUpdateExerciseRequest = z.object({ name: z.string().min(1).max(255).nullish(), - description: z.string().nullish(), + description: z.string().max(1000).nullish(), muscle_group_ids: z.array(z.int()).nullish() }); @@ -219,7 +219,7 @@ export const zUpdateSetRequest = z.object({ z.string().regex(/^(?!^[-+.]*$)[+-]?0*(?:\d{0,4}|(?=[\d.]{1,7}0*$)\d{0,4}\.\d{0,2}0*$)/) ]).nullish(), unit: zSetUnit.nullish(), - notes: z.string().nullish() + notes: z.string().max(1000).nullish() }); /** @@ -228,7 +228,7 @@ export const zUpdateSetRequest = z.object({ export const zUpdateWorkoutRequest = z.object({ started_at: z.iso.datetime().nullish(), ended_at: z.iso.datetime().nullish(), - notes: z.string().nullish() + notes: z.string().max(1000).nullish() }); /** diff --git a/server/app/models/schemas/exercise.py b/server/app/models/schemas/exercise.py index 4b64d770..2cfba51c 100644 --- a/server/app/models/schemas/exercise.py +++ b/server/app/models/schemas/exercise.py @@ -4,7 +4,7 @@ from pydantic import BaseModel, Field, model_validator from .muscle_group import MuscleGroupPublic -from .types import ExerciseName +from .types import ExerciseDescription, ExerciseName class ExerciseBase(BaseModel): @@ -22,13 +22,13 @@ class ExercisePublic(ExerciseBase): class CreateExerciseRequest(BaseModel): name: ExerciseName - description: str | None = None + description: ExerciseDescription | None = None muscle_group_ids: list[int] = Field(default_factory=list[int]) class UpdateExerciseRequest(BaseModel): name: ExerciseName | None = None - description: str | None = None + description: ExerciseDescription | None = None muscle_group_ids: list[int] | None = None @model_validator(mode="after") diff --git a/server/app/models/schemas/feedback.py b/server/app/models/schemas/feedback.py index b59e29b0..9a8c4e59 100644 --- a/server/app/models/schemas/feedback.py +++ b/server/app/models/schemas/feedback.py @@ -4,13 +4,14 @@ from pydantic import BaseModel, Field, model_validator from app.models.enums import FeedbackType +from app.models.schemas.types import FeedbackDescription, FeedbackTitle, FeedbackUrl class CreateFeedbackRequest(BaseModel): type: FeedbackType - url: str = Field(min_length=1, max_length=1000) - title: str = Field(min_length=1, max_length=100) - description: str = Field(min_length=1, max_length=10000) + url: FeedbackUrl + title: FeedbackTitle + description: FeedbackDescription files: list[Annotated[UploadFile, File()]] = Field(default_factory=list[UploadFile]) @model_validator(mode="after") diff --git a/server/app/models/schemas/set.py b/server/app/models/schemas/set.py index 2abe9101..1c79abe0 100644 --- a/server/app/models/schemas/set.py +++ b/server/app/models/schemas/set.py @@ -3,7 +3,7 @@ from pydantic import BaseModel from app.models.enums import SetUnit -from app.models.schemas.types import SetReps, SetWeight +from app.models.schemas.types import SetNotes, SetReps, SetWeight class SetPublic(BaseModel): @@ -22,11 +22,11 @@ class CreateSetRequest(BaseModel): reps: SetReps | None = None weight: SetWeight | None = None unit: SetUnit | None = None - notes: str | None = None + notes: SetNotes | None = None class UpdateSetRequest(BaseModel): reps: SetReps | None = None weight: SetWeight | None = None unit: SetUnit | None = None - notes: str | None = None + notes: SetNotes | None = None diff --git a/server/app/models/schemas/types.py b/server/app/models/schemas/types.py index 789024b1..226067e6 100644 --- a/server/app/models/schemas/types.py +++ b/server/app/models/schemas/types.py @@ -14,13 +14,23 @@ def is_email_identifier(identifier: str) -> bool: return False -Name = Annotated[str, StringConstraints(min_length=1, max_length=50)] -Username = Annotated[str, StringConstraints(min_length=3, max_length=50)] +Name = Annotated[str, StringConstraints(min_length=1, max_length=255)] +Username = Annotated[str, StringConstraints(min_length=3, max_length=255)] Password = Annotated[str, StringConstraints(min_length=8, max_length=64)] Token = Annotated[str, StringConstraints(min_length=1, max_length=64)] Email = Annotated[EmailStr, Field(max_length=255)] +FeedbackUrl = Annotated[str, Field(min_length=1, max_length=1000)] +FeedbackTitle = Annotated[str, Field(min_length=1, max_length=1000)] +FeedbackDescription = Annotated[str, Field(min_length=1, max_length=10000)] + ExerciseName = Annotated[str, StringConstraints(min_length=1, max_length=255)] +ExerciseDescription = Annotated[str, StringConstraints(max_length=1000)] SetReps = Annotated[int, Field(ge=0)] SetWeight = Annotated[Decimal, Field(max_digits=6, decimal_places=2, ge=0)] +SetNotes = Annotated[str, Field(max_length=1000)] + +WorkoutExerciseNotes = Annotated[str, Field(max_length=1000)] + +WorkoutNotes = Annotated[str, Field(max_length=1000)] diff --git a/server/app/models/schemas/workout.py b/server/app/models/schemas/workout.py index 2d788afa..40767adf 100644 --- a/server/app/models/schemas/workout.py +++ b/server/app/models/schemas/workout.py @@ -3,6 +3,7 @@ from pydantic import BaseModel, model_validator +from .types import WorkoutNotes from .workout_exercise import WorkoutExercisePublic @@ -23,13 +24,13 @@ class WorkoutPublic(WorkoutBase): class CreateWorkoutRequest(BaseModel): started_at: datetime | None = None ended_at: datetime | None = None - notes: str | None = None + notes: WorkoutNotes | None = None class UpdateWorkoutRequest(BaseModel): started_at: datetime | None = None ended_at: datetime | None = None - notes: str | None = None + notes: WorkoutNotes | None = None @model_validator(mode="after") def validate_non_nullable_fields(self) -> Self: diff --git a/server/app/models/schemas/workout_exercise.py b/server/app/models/schemas/workout_exercise.py index 6856cad8..577c386f 100644 --- a/server/app/models/schemas/workout_exercise.py +++ b/server/app/models/schemas/workout_exercise.py @@ -4,6 +4,7 @@ from app.models.schemas.exercise import ExerciseBase from app.models.schemas.set import SetPublic +from app.models.schemas.types import WorkoutExerciseNotes class WorkoutExercisePublic(BaseModel): @@ -21,4 +22,4 @@ class WorkoutExercisePublic(BaseModel): class CreateWorkoutExerciseRequest(BaseModel): exercise_id: int - notes: str | None = None + notes: WorkoutExerciseNotes | None = None From 7c4d675d65862729d1b27447fa1856649186cab8 Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Mon, 23 Mar 2026 12:13:01 -0500 Subject: [PATCH 06/27] server - centralize service utility functions --- server/app/core/dependencies.py | 3 +- server/app/services/access_request.py | 5 - server/app/services/admin.py | 4 +- server/app/services/exercise.py | 46 +------- server/app/services/muscle_group.py | 5 +- server/app/services/set.py | 33 +----- server/app/services/user.py | 5 - server/app/services/utilities/queries.py | 77 +++++++++++++ server/app/services/utilities/serializers.py | 103 ++++++++++++++++++ server/app/services/workout.py | 38 +------ server/app/services/workout_exercise.py | 43 +------- server/app/tests/api/exercise/utilities.py | 3 +- .../dependencies/test_get_current_admin.py | 2 +- .../app/tests/services/_utilities/__init__.py | 0 .../services/_utilities/queries/__init__.py | 0 .../queries}/test_get_owned_workout.py | 6 +- .../queries}/test_query_exercises.py | 8 +- .../queries}/test_query_sets.py | 12 +- .../queries}/test_query_workout_exercises.py | 12 +- .../test_to_access_request_public.py | 2 +- .../serializers}/test_to_exercise_base.py | 2 +- .../serializers}/test_to_exercise_public.py | 2 +- .../test_to_muscle_group_public.py | 25 +++++ .../serializers}/test_to_set_public.py | 2 +- .../serializers}/test_to_user_public.py | 2 +- .../serializers}/test_to_workout_base.py | 2 +- .../test_to_workout_exercise_public.py | 2 +- .../serializers}/test_to_workout_public.py | 2 +- .../services/exercise/test_create_exercise.py | 6 +- server/app/tests/services/utilities.py | 2 +- 30 files changed, 255 insertions(+), 199 deletions(-) create mode 100644 server/app/services/utilities/queries.py create mode 100644 server/app/services/utilities/serializers.py create mode 100644 server/app/tests/services/_utilities/__init__.py create mode 100644 server/app/tests/services/_utilities/queries/__init__.py rename server/app/tests/services/{workout => _utilities/queries}/test_get_owned_workout.py (85%) rename server/app/tests/services/{exercise => _utilities/queries}/test_query_exercises.py (93%) rename server/app/tests/services/{set => _utilities/queries}/test_query_sets.py (86%) rename server/app/tests/services/{workout_exercise => _utilities/queries}/test_query_workout_exercises.py (92%) rename server/app/tests/services/{access_request => _utilities/serializers}/test_to_access_request_public.py (93%) rename server/app/tests/services/{exercise => _utilities/serializers}/test_to_exercise_base.py (92%) rename server/app/tests/services/{exercise => _utilities/serializers}/test_to_exercise_public.py (97%) create mode 100644 server/app/tests/services/_utilities/serializers/test_to_muscle_group_public.py rename server/app/tests/services/{set => _utilities/serializers}/test_to_set_public.py (93%) rename server/app/tests/services/{user => _utilities/serializers}/test_to_user_public.py (93%) rename server/app/tests/services/{workout => _utilities/serializers}/test_to_workout_base.py (93%) rename server/app/tests/services/{workout_exercise => _utilities/serializers}/test_to_workout_exercise_public.py (98%) rename server/app/tests/services/{workout => _utilities/serializers}/test_to_workout_public.py (97%) diff --git a/server/app/core/dependencies.py b/server/app/core/dependencies.py index dbfd018d..d030b1cc 100644 --- a/server/app/core/dependencies.py +++ b/server/app/core/dependencies.py @@ -11,7 +11,8 @@ from app.core.security import ACCESS_JWT_KEY, REFRESH_JWT_KEY, verify_jwt from app.models.errors import InsufficientPermissions, InvalidCredentials from app.models.schemas.user import UserPublic -from app.services.user import get_user_by_username, to_user_public +from app.services.user import get_user_by_username +from app.services.utilities.serializers import to_user_public logger = logging.getLogger(__name__) diff --git a/server/app/services/access_request.py b/server/app/services/access_request.py index 4eeaae7f..4b16e848 100644 --- a/server/app/services/access_request.py +++ b/server/app/services/access_request.py @@ -6,7 +6,6 @@ from app.models.database.access_request import AccessRequest from app.models.enums import AccessRequestStatus -from app.models.schemas.access_request import AccessRequestPublic STATUS_PRIORITY = case( (AccessRequest.status == AccessRequestStatus.PENDING, 1), @@ -15,10 +14,6 @@ ) -def to_access_request_public(access_request: AccessRequest) -> AccessRequestPublic: - return AccessRequestPublic.model_validate(access_request, from_attributes=True) - - async def get_latest_access_request_by_email( email: str, db: AsyncSession ) -> AccessRequest | None: diff --git a/server/app/services/admin.py b/server/app/services/admin.py index 4beebdf3..a1e6fd08 100644 --- a/server/app/services/admin.py +++ b/server/app/services/admin.py @@ -14,10 +14,10 @@ from app.services.access_request import ( get_access_request_by_id, get_access_requests_with_reviewer, - to_access_request_public, ) from app.services.email import EmailService -from app.services.user import get_users_ordered_by_username, to_user_public +from app.services.user import get_users_ordered_by_username +from app.services.utilities.serializers import to_access_request_public, to_user_public logger = logging.getLogger(__name__) diff --git a/server/app/services/exercise.py b/server/app/services/exercise.py index e8536010..49b9e774 100644 --- a/server/app/services/exercise.py +++ b/server/app/services/exercise.py @@ -1,11 +1,8 @@ import logging -from collections.abc import Sequence -from typing import Any from sqlalchemy import delete, select from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.orm import selectinload from app.core.database import is_unique_violation from app.models.database.exercise import EXERCISE_UNIQUE_CONSTRAINT, Exercise @@ -17,31 +14,16 @@ ) from app.models.schemas.exercise import ( CreateExerciseRequest, - ExerciseBase, ExercisePublic, UpdateExerciseRequest, ) -from app.services.muscle_group import get_muscle_groups_by_ids, to_muscle_group_public +from app.services.muscle_group import get_muscle_groups_by_ids +from app.services.utilities.queries import query_exercises +from app.services.utilities.serializers import to_exercise_public logger = logging.getLogger(__name__) -async def query_exercises( - db: AsyncSession, - base: bool, - *where_clauses: Any, -) -> Sequence[Exercise]: - query = select(Exercise).where(*where_clauses).order_by(Exercise.name) - if not base: - query = query.options( - selectinload(Exercise.muscle_groups).selectinload( - ExerciseMuscleGroup.muscle_group - ) - ) - result = await db.execute(query) - return result.scalars().all() - - async def _get_owned_exercise( exercise_id: int, user_id: int, @@ -58,28 +40,6 @@ async def _get_owned_exercise( return exercise -def to_exercise_base(exercise: Exercise) -> ExerciseBase: - return ExerciseBase.model_validate(exercise, from_attributes=True) - - -def to_exercise_public(exercise: Exercise) -> ExercisePublic: - sorted_muscle_groups = sorted( - exercise.muscle_groups, - key=lambda emg: emg.muscle_group.name, - ) - return ExercisePublic( - id=exercise.id, - user_id=exercise.user_id, - name=exercise.name, - description=exercise.description, - created_at=exercise.created_at, - updated_at=exercise.updated_at, - muscle_groups=[ - to_muscle_group_public(emg.muscle_group) for emg in sorted_muscle_groups - ], - ) - - async def create_exercise( user_id: int, req: CreateExerciseRequest, diff --git a/server/app/services/muscle_group.py b/server/app/services/muscle_group.py index cebb7ca1..76a865f0 100644 --- a/server/app/services/muscle_group.py +++ b/server/app/services/muscle_group.py @@ -3,10 +3,7 @@ from app.models.database.muscle_group import MuscleGroup from app.models.schemas.muscle_group import MuscleGroupPublic - - -def to_muscle_group_public(muscle_group: MuscleGroup) -> MuscleGroupPublic: - return MuscleGroupPublic.model_validate(muscle_group, from_attributes=True) +from app.services.utilities.serializers import to_muscle_group_public async def get_muscle_groups_ordered_by_name( diff --git a/server/app/services/set.py b/server/app/services/set.py index 651d7bbb..57129a50 100644 --- a/server/app/services/set.py +++ b/server/app/services/set.py @@ -1,6 +1,4 @@ import logging -from collections.abc import Sequence -from typing import Any from sqlalchemy import func, select from sqlalchemy.exc import IntegrityError @@ -14,33 +12,16 @@ SetNumberConflict, WorkoutExerciseNotFound, ) -from app.models.schemas.set import CreateSetRequest, SetPublic, UpdateSetRequest -from app.services.workout import get_owned_workout +from app.models.schemas.set import CreateSetRequest, UpdateSetRequest +from app.services.utilities.queries import ( + get_owned_workout, + query_sets, + query_workout_exercises, +) logger = logging.getLogger(__name__) -def to_set_public(set_: Set) -> SetPublic: - return SetPublic.model_validate(set_, from_attributes=True) - - -async def query_sets( - db: AsyncSession, - *where_clauses: Any, -) -> Sequence[Set]: - query = ( - select(Set) - .join( - WorkoutExercise, - Set.workout_exercise_id == WorkoutExercise.id, - ) - .where(*where_clauses) - .order_by(Set.set_number) - ) - result = await db.execute(query) - return result.scalars().all() - - async def _get_next_set_number( workout_exercise_id: int, db: AsyncSession, @@ -69,8 +50,6 @@ async def create_set( # validate workout existence & ownership await get_owned_workout(workout_id, user_id, db) - from app.services.workout_exercise import query_workout_exercises - result = await query_workout_exercises( db, WorkoutExercise.id == workout_exercise_id, diff --git a/server/app/services/user.py b/server/app/services/user.py index f37aa084..d5fc374b 100644 --- a/server/app/services/user.py +++ b/server/app/services/user.py @@ -5,11 +5,6 @@ from app.models.database.user import User from app.models.schemas.types import is_email_identifier -from app.models.schemas.user import UserPublic - - -def to_user_public(user: User) -> UserPublic: - return UserPublic.model_validate(user, from_attributes=True) async def get_admin_users(db: AsyncSession) -> Sequence[User]: diff --git a/server/app/services/utilities/queries.py b/server/app/services/utilities/queries.py new file mode 100644 index 00000000..ebce02f3 --- /dev/null +++ b/server/app/services/utilities/queries.py @@ -0,0 +1,77 @@ +from collections.abc import Sequence +from typing import Any + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + +from app.models.database.exercise import Exercise +from app.models.database.exercise_muscle_group import ExerciseMuscleGroup +from app.models.database.set import Set +from app.models.database.workout import Workout +from app.models.database.workout_exercise import WorkoutExercise +from app.models.errors import WorkoutNotFound + + +async def get_owned_workout( + workout_id: int, + user_id: int, + db: AsyncSession, +) -> Workout: + result = await db.execute( + select(Workout).where( + Workout.id == workout_id, + ), + ) + workout = result.scalar_one_or_none() + if not workout or workout.user_id != user_id: + raise WorkoutNotFound() + return workout + + +async def query_exercises( + db: AsyncSession, + base: bool, + *where_clauses: Any, +) -> Sequence[Exercise]: + query = select(Exercise).where(*where_clauses).order_by(Exercise.name) + if not base: + query = query.options( + selectinload(Exercise.muscle_groups).selectinload( + ExerciseMuscleGroup.muscle_group + ) + ) + result = await db.execute(query) + return result.scalars().all() + + +async def query_workout_exercises( + db: AsyncSession, + *where_clauses: Any, +) -> Sequence[WorkoutExercise]: + query = ( + select(WorkoutExercise).where(*where_clauses).order_by(WorkoutExercise.position) + ) + query = query.options( + selectinload(WorkoutExercise.exercise), + selectinload(WorkoutExercise.sets), + ) + result = await db.execute(query) + return result.scalars().all() + + +async def query_sets( + db: AsyncSession, + *where_clauses: Any, +) -> Sequence[Set]: + query = ( + select(Set) + .join( + WorkoutExercise, + Set.workout_exercise_id == WorkoutExercise.id, + ) + .where(*where_clauses) + .order_by(Set.set_number) + ) + result = await db.execute(query) + return result.scalars().all() diff --git a/server/app/services/utilities/serializers.py b/server/app/services/utilities/serializers.py new file mode 100644 index 00000000..ccc65fea --- /dev/null +++ b/server/app/services/utilities/serializers.py @@ -0,0 +1,103 @@ +from app.models.database.access_request import AccessRequest +from app.models.database.exercise import Exercise +from app.models.database.muscle_group import MuscleGroup +from app.models.database.set import Set +from app.models.database.user import User +from app.models.database.workout import Workout +from app.models.database.workout_exercise import WorkoutExercise +from app.models.schemas.access_request import AccessRequestPublic, ReviewerPublic +from app.models.schemas.exercise import ExerciseBase, ExercisePublic +from app.models.schemas.muscle_group import MuscleGroupPublic +from app.models.schemas.set import SetPublic +from app.models.schemas.user import UserPublic +from app.models.schemas.workout import WorkoutBase, WorkoutPublic +from app.models.schemas.workout_exercise import WorkoutExercisePublic + + +def to_access_request_public(access_request: AccessRequest) -> AccessRequestPublic: + reviewer = ( + ReviewerPublic( + id=access_request.reviewer.id, + username=access_request.reviewer.username, + ) + if access_request.reviewer + else None + ) + return AccessRequestPublic( + id=access_request.id, + email=access_request.email, + first_name=access_request.first_name, + last_name=access_request.last_name, + status=access_request.status, + reviewed_at=access_request.reviewed_at, + reviewer=reviewer, + created_at=access_request.created_at, + updated_at=access_request.updated_at, + ) + + +def to_user_public(user: User) -> UserPublic: + return UserPublic.model_validate(user, from_attributes=True) + + +def to_muscle_group_public(muscle_group: MuscleGroup) -> MuscleGroupPublic: + return MuscleGroupPublic.model_validate(muscle_group, from_attributes=True) + + +def to_exercise_base(exercise: Exercise) -> ExerciseBase: + return ExerciseBase.model_validate(exercise, from_attributes=True) + + +def to_exercise_public(exercise: Exercise) -> ExercisePublic: + sorted_muscle_groups = sorted( + exercise.muscle_groups, + key=lambda emg: emg.muscle_group.name, + ) + return ExercisePublic( + id=exercise.id, + user_id=exercise.user_id, + name=exercise.name, + description=exercise.description, + created_at=exercise.created_at, + updated_at=exercise.updated_at, + muscle_groups=[ + to_muscle_group_public(emg.muscle_group) for emg in sorted_muscle_groups + ], + ) + + +def to_set_public(set_: Set) -> SetPublic: + return SetPublic.model_validate(set_, from_attributes=True) + + +def to_workout_exercise_public( + workout_exercise: WorkoutExercise, +) -> WorkoutExercisePublic: + return WorkoutExercisePublic( + id=workout_exercise.id, + workout_id=workout_exercise.workout_id, + exercise_id=workout_exercise.exercise_id, + position=workout_exercise.position, + notes=workout_exercise.notes, + created_at=workout_exercise.created_at, + updated_at=workout_exercise.updated_at, + exercise=to_exercise_base(workout_exercise.exercise), + sets=[to_set_public(s) for s in workout_exercise.sets], + ) + + +def to_workout_base(workout: Workout) -> WorkoutBase: + return WorkoutBase.model_validate(workout, from_attributes=True) + + +def to_workout_public(workout: Workout) -> WorkoutPublic: + return WorkoutPublic( + id=workout.id, + user_id=workout.user_id, + started_at=workout.started_at, + ended_at=workout.ended_at, + notes=workout.notes, + created_at=workout.created_at, + updated_at=workout.updated_at, + exercises=[to_workout_exercise_public(we) for we in workout.exercises], + ) diff --git a/server/app/services/workout.py b/server/app/services/workout.py index 8320c7de..2bbed907 100644 --- a/server/app/services/workout.py +++ b/server/app/services/workout.py @@ -16,6 +16,8 @@ WorkoutBase, WorkoutPublic, ) +from app.services.utilities.queries import get_owned_workout +from app.services.utilities.serializers import to_workout_base, to_workout_public logger = logging.getLogger(__name__) @@ -35,42 +37,6 @@ async def _query_workouts( return result.scalars().all() -async def get_owned_workout( - workout_id: int, - user_id: int, - db: AsyncSession, -) -> Workout: - result = await db.execute( - select(Workout).where( - Workout.id == workout_id, - ), - ) - workout = result.scalar_one_or_none() - if not workout or workout.user_id != user_id: - raise WorkoutNotFound() - return workout - - -def to_workout_base(workout: Workout) -> WorkoutBase: - return WorkoutBase.model_validate(workout, from_attributes=True) - - -def to_workout_public(workout: Workout) -> WorkoutPublic: - # prevent circular import - from app.services.workout_exercise import to_workout_exercise_public - - return WorkoutPublic( - id=workout.id, - user_id=workout.user_id, - started_at=workout.started_at, - ended_at=workout.ended_at, - notes=workout.notes, - created_at=workout.created_at, - updated_at=workout.updated_at, - exercises=[to_workout_exercise_public(we) for we in workout.exercises], - ) - - async def create_workout( user_id: int, req: CreateWorkoutRequest, diff --git a/server/app/services/workout_exercise.py b/server/app/services/workout_exercise.py index 7dc5a4cf..3fe9f48f 100644 --- a/server/app/services/workout_exercise.py +++ b/server/app/services/workout_exercise.py @@ -1,11 +1,8 @@ import logging -from collections.abc import Sequence -from typing import Any from sqlalchemy import func, select from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.orm import selectinload from app.core.database import is_unique_violation from app.models.database.exercise import Exercise @@ -18,48 +15,12 @@ WorkoutExerciseNotFound, WorkoutExercisePositionConflict, ) -from app.models.schemas.workout_exercise import ( - CreateWorkoutExerciseRequest, - WorkoutExercisePublic, -) -from app.services.exercise import query_exercises, to_exercise_base -from app.services.set import to_set_public -from app.services.workout import get_owned_workout +from app.models.schemas.workout_exercise import CreateWorkoutExerciseRequest +from app.services.utilities.queries import get_owned_workout, query_exercises logger = logging.getLogger(__name__) -def to_workout_exercise_public( - workout_exercise: WorkoutExercise, -) -> WorkoutExercisePublic: - return WorkoutExercisePublic( - id=workout_exercise.id, - workout_id=workout_exercise.workout_id, - exercise_id=workout_exercise.exercise_id, - position=workout_exercise.position, - notes=workout_exercise.notes, - created_at=workout_exercise.created_at, - updated_at=workout_exercise.updated_at, - exercise=to_exercise_base(workout_exercise.exercise), - sets=[to_set_public(s) for s in workout_exercise.sets], - ) - - -async def query_workout_exercises( - db: AsyncSession, - *where_clauses: Any, -) -> Sequence[WorkoutExercise]: - query = ( - select(WorkoutExercise).where(*where_clauses).order_by(WorkoutExercise.position) - ) - query = query.options( - selectinload(WorkoutExercise.exercise), - selectinload(WorkoutExercise.sets), - ) - result = await db.execute(query) - return result.scalars().all() - - async def _get_next_workout_exercise_position( workout_id: int, db: AsyncSession, diff --git a/server/app/tests/api/exercise/utilities.py b/server/app/tests/api/exercise/utilities.py index 80092a35..7697bcbf 100644 --- a/server/app/tests/api/exercise/utilities.py +++ b/server/app/tests/api/exercise/utilities.py @@ -5,7 +5,8 @@ from app.models.database.exercise import Exercise from app.models.database.muscle_group import MuscleGroup from app.models.schemas.exercise import ExercisePublic -from app.services.exercise import query_exercises, to_exercise_public +from app.services.utilities.queries import query_exercises +from app.services.utilities.serializers import to_exercise_public from ..utilities import HttpMethod, make_http_request diff --git a/server/app/tests/core/dependencies/test_get_current_admin.py b/server/app/tests/core/dependencies/test_get_current_admin.py index 3b7ffe9c..961f774f 100644 --- a/server/app/tests/core/dependencies/test_get_current_admin.py +++ b/server/app/tests/core/dependencies/test_get_current_admin.py @@ -7,7 +7,7 @@ from app.models.database.user import User from app.models.errors import InsufficientPermissions from app.models.schemas.user import UserPublic -from app.services.user import to_user_public +from app.services.utilities.serializers import to_user_public async def _get_admin(session: AsyncSession, settings: Settings) -> UserPublic: diff --git a/server/app/tests/services/_utilities/__init__.py b/server/app/tests/services/_utilities/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/app/tests/services/_utilities/queries/__init__.py b/server/app/tests/services/_utilities/queries/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/app/tests/services/workout/test_get_owned_workout.py b/server/app/tests/services/_utilities/queries/test_get_owned_workout.py similarity index 85% rename from server/app/tests/services/workout/test_get_owned_workout.py rename to server/app/tests/services/_utilities/queries/test_get_owned_workout.py index 472bed0a..3c562de3 100644 --- a/server/app/tests/services/workout/test_get_owned_workout.py +++ b/server/app/tests/services/_utilities/queries/test_get_owned_workout.py @@ -2,10 +2,10 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.models.errors import WorkoutNotFound -from app.services.workout import get_owned_workout +from app.services.utilities.queries import get_owned_workout -from ..utilities import create_user -from .utilities import create_workout +from ...utilities import create_user +from ...workout.utilities import create_workout async def test_get_owned_workouts( diff --git a/server/app/tests/services/exercise/test_query_exercises.py b/server/app/tests/services/_utilities/queries/test_query_exercises.py similarity index 93% rename from server/app/tests/services/exercise/test_query_exercises.py rename to server/app/tests/services/_utilities/queries/test_query_exercises.py index 996e5028..50bd42db 100644 --- a/server/app/tests/services/exercise/test_query_exercises.py +++ b/server/app/tests/services/_utilities/queries/test_query_exercises.py @@ -3,12 +3,10 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.models.database.exercise import Exercise -from app.services.exercise import ( - query_exercises, -) +from app.services.utilities.queries import query_exercises -from ..utilities import create_user -from .utilities import create_exercise, get_muscle_group_id +from ...exercise.utilities import create_exercise, get_muscle_group_id +from ...utilities import create_user async def test_query_exercises_base( diff --git a/server/app/tests/services/set/test_query_sets.py b/server/app/tests/services/_utilities/queries/test_query_sets.py similarity index 86% rename from server/app/tests/services/set/test_query_sets.py rename to server/app/tests/services/_utilities/queries/test_query_sets.py index 3baad8ad..89cd0e09 100644 --- a/server/app/tests/services/set/test_query_sets.py +++ b/server/app/tests/services/_utilities/queries/test_query_sets.py @@ -1,13 +1,13 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.models.database.set import Set -from app.services.set import query_sets +from app.services.utilities.queries import query_sets -from ..exercise.utilities import create_exercise -from ..utilities import create_user -from ..workout.utilities import create_workout -from ..workout_exercise.utilities import create_workout_exercise -from .utilities import create_set +from ...exercise.utilities import create_exercise +from ...set.utilities import create_set +from ...utilities import create_user +from ...workout.utilities import create_workout +from ...workout_exercise.utilities import create_workout_exercise async def test_query_sets_no_where_clause( diff --git a/server/app/tests/services/workout_exercise/test_query_workout_exercises.py b/server/app/tests/services/_utilities/queries/test_query_workout_exercises.py similarity index 92% rename from server/app/tests/services/workout_exercise/test_query_workout_exercises.py rename to server/app/tests/services/_utilities/queries/test_query_workout_exercises.py index 68a63800..f188a80c 100644 --- a/server/app/tests/services/workout_exercise/test_query_workout_exercises.py +++ b/server/app/tests/services/_utilities/queries/test_query_workout_exercises.py @@ -1,13 +1,13 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.models.database.workout_exercise import WorkoutExercise -from app.services.workout_exercise import query_workout_exercises +from app.services.utilities.queries import query_workout_exercises -from ..exercise.utilities import create_exercise -from ..set.utilities import create_set -from ..utilities import create_user -from ..workout.utilities import create_workout -from .utilities import create_workout_exercise +from ...exercise.utilities import create_exercise +from ...set.utilities import create_set +from ...utilities import create_user +from ...workout.utilities import create_workout +from ...workout_exercise.utilities import create_workout_exercise async def test_query_workout_exercises_no_where_clause( diff --git a/server/app/tests/services/access_request/test_to_access_request_public.py b/server/app/tests/services/_utilities/serializers/test_to_access_request_public.py similarity index 93% rename from server/app/tests/services/access_request/test_to_access_request_public.py rename to server/app/tests/services/_utilities/serializers/test_to_access_request_public.py index 074ebb5b..7bb79736 100644 --- a/server/app/tests/services/access_request/test_to_access_request_public.py +++ b/server/app/tests/services/_utilities/serializers/test_to_access_request_public.py @@ -3,7 +3,7 @@ from app.models.database.access_request import AccessRequest from app.models.enums import AccessRequestStatus from app.models.schemas.access_request import AccessRequestPublic -from app.services.access_request import to_access_request_public +from app.services.utilities.serializers import to_access_request_public def test_to_access_request_public() -> None: diff --git a/server/app/tests/services/exercise/test_to_exercise_base.py b/server/app/tests/services/_utilities/serializers/test_to_exercise_base.py similarity index 92% rename from server/app/tests/services/exercise/test_to_exercise_base.py rename to server/app/tests/services/_utilities/serializers/test_to_exercise_base.py index 6550c4eb..e11c0795 100644 --- a/server/app/tests/services/exercise/test_to_exercise_base.py +++ b/server/app/tests/services/_utilities/serializers/test_to_exercise_base.py @@ -2,7 +2,7 @@ from app.models.database.exercise import Exercise from app.models.schemas.exercise import ExerciseBase -from app.services.exercise import to_exercise_base +from app.services.utilities.serializers import to_exercise_base def test_to_exercise_base() -> None: diff --git a/server/app/tests/services/exercise/test_to_exercise_public.py b/server/app/tests/services/_utilities/serializers/test_to_exercise_public.py similarity index 97% rename from server/app/tests/services/exercise/test_to_exercise_public.py rename to server/app/tests/services/_utilities/serializers/test_to_exercise_public.py index 2cc07b09..c39e9744 100644 --- a/server/app/tests/services/exercise/test_to_exercise_public.py +++ b/server/app/tests/services/_utilities/serializers/test_to_exercise_public.py @@ -5,7 +5,7 @@ from app.models.database.muscle_group import MuscleGroup from app.models.schemas.exercise import ExercisePublic from app.models.schemas.muscle_group import MuscleGroupPublic -from app.services.exercise import to_exercise_public +from app.services.utilities.serializers import to_exercise_public def test_to_exercise_public_no_muscle_groups() -> None: diff --git a/server/app/tests/services/_utilities/serializers/test_to_muscle_group_public.py b/server/app/tests/services/_utilities/serializers/test_to_muscle_group_public.py new file mode 100644 index 00000000..12490006 --- /dev/null +++ b/server/app/tests/services/_utilities/serializers/test_to_muscle_group_public.py @@ -0,0 +1,25 @@ +from app.models.database.muscle_group import MuscleGroup +from app.models.schemas.muscle_group import MuscleGroupPublic +from app.services.utilities.serializers import to_muscle_group_public + + +def test_to_muscle_group_public() -> None: + user = MuscleGroup( + id=1, + name="Chest", + description="Chest muscles", + ) + + result = to_muscle_group_public(user) + + assert isinstance(result, MuscleGroupPublic) + assert result.id == 1 + assert result.name == "Chest" + assert result.description == "Chest muscles" + + result = to_muscle_group_public(user) + + assert isinstance(result, MuscleGroupPublic) + assert result.id == 1 + assert result.name == "Chest" + assert result.description == "Chest muscles" diff --git a/server/app/tests/services/set/test_to_set_public.py b/server/app/tests/services/_utilities/serializers/test_to_set_public.py similarity index 93% rename from server/app/tests/services/set/test_to_set_public.py rename to server/app/tests/services/_utilities/serializers/test_to_set_public.py index 2284e948..2e8cde45 100644 --- a/server/app/tests/services/set/test_to_set_public.py +++ b/server/app/tests/services/_utilities/serializers/test_to_set_public.py @@ -2,7 +2,7 @@ from app.models.database.set import Set from app.models.schemas.set import SetPublic -from app.services.set import to_set_public +from app.services.utilities.serializers import to_set_public def test_to_set_public() -> None: diff --git a/server/app/tests/services/user/test_to_user_public.py b/server/app/tests/services/_utilities/serializers/test_to_user_public.py similarity index 93% rename from server/app/tests/services/user/test_to_user_public.py rename to server/app/tests/services/_utilities/serializers/test_to_user_public.py index b75b5240..fae2129d 100644 --- a/server/app/tests/services/user/test_to_user_public.py +++ b/server/app/tests/services/_utilities/serializers/test_to_user_public.py @@ -2,7 +2,7 @@ from app.models.database.user import User from app.models.schemas.user import UserPublic -from app.services.user import to_user_public +from app.services.utilities.serializers import to_user_public def test_to_user_public() -> None: diff --git a/server/app/tests/services/workout/test_to_workout_base.py b/server/app/tests/services/_utilities/serializers/test_to_workout_base.py similarity index 93% rename from server/app/tests/services/workout/test_to_workout_base.py rename to server/app/tests/services/_utilities/serializers/test_to_workout_base.py index 500277a0..55ad86b6 100644 --- a/server/app/tests/services/workout/test_to_workout_base.py +++ b/server/app/tests/services/_utilities/serializers/test_to_workout_base.py @@ -2,7 +2,7 @@ from app.models.database.workout import Workout from app.models.schemas.workout import WorkoutBase -from app.services.workout import to_workout_base +from app.services.utilities.serializers import to_workout_base def test_to_workout_base() -> None: diff --git a/server/app/tests/services/workout_exercise/test_to_workout_exercise_public.py b/server/app/tests/services/_utilities/serializers/test_to_workout_exercise_public.py similarity index 98% rename from server/app/tests/services/workout_exercise/test_to_workout_exercise_public.py rename to server/app/tests/services/_utilities/serializers/test_to_workout_exercise_public.py index d7d6307d..512b1a97 100644 --- a/server/app/tests/services/workout_exercise/test_to_workout_exercise_public.py +++ b/server/app/tests/services/_utilities/serializers/test_to_workout_exercise_public.py @@ -6,7 +6,7 @@ from app.models.schemas.exercise import ExerciseBase from app.models.schemas.set import SetPublic from app.models.schemas.workout_exercise import WorkoutExercisePublic -from app.services.workout_exercise import to_workout_exercise_public +from app.services.utilities.serializers import to_workout_exercise_public def test_to_workout_exercise_public_no_sets() -> None: diff --git a/server/app/tests/services/workout/test_to_workout_public.py b/server/app/tests/services/_utilities/serializers/test_to_workout_public.py similarity index 97% rename from server/app/tests/services/workout/test_to_workout_public.py rename to server/app/tests/services/_utilities/serializers/test_to_workout_public.py index 69b2ce3c..31bd6f81 100644 --- a/server/app/tests/services/workout/test_to_workout_public.py +++ b/server/app/tests/services/_utilities/serializers/test_to_workout_public.py @@ -5,7 +5,7 @@ from app.models.database.workout_exercise import WorkoutExercise from app.models.schemas.workout import WorkoutPublic from app.models.schemas.workout_exercise import WorkoutExercisePublic -from app.services.workout import to_workout_public +from app.services.utilities.serializers import to_workout_public def test_to_workout_public_no_exercises() -> None: diff --git a/server/app/tests/services/exercise/test_create_exercise.py b/server/app/tests/services/exercise/test_create_exercise.py index 62e507c2..b38bcd07 100644 --- a/server/app/tests/services/exercise/test_create_exercise.py +++ b/server/app/tests/services/exercise/test_create_exercise.py @@ -7,10 +7,8 @@ from app.models.database.exercise import Exercise from app.models.errors import ExerciseNameConflict, MuscleGroupNotFound from app.models.schemas.exercise import CreateExerciseRequest -from app.services.exercise import ( - create_exercise, - query_exercises, -) +from app.services.exercise import create_exercise +from app.services.utilities.queries import query_exercises from ..utilities import create_user from .utilities import get_muscle_group_id diff --git a/server/app/tests/services/utilities.py b/server/app/tests/services/utilities.py index 2dafc536..c1995dc8 100644 --- a/server/app/tests/services/utilities.py +++ b/server/app/tests/services/utilities.py @@ -4,7 +4,7 @@ from app.core.config import Settings from app.models.database.user import User from app.models.schemas.user import UserPublic -from app.services.user import to_user_public +from app.services.utilities.serializers import to_user_public async def get_admin_user_public( From 6ac2ec27d3ce2ce2c78467f041f5fc825623f806 Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Mon, 23 Mar 2026 12:27:44 -0500 Subject: [PATCH 07/27] server - update test to use dynamic ids --- .../_utilities/queries/test_query_exercises.py | 3 ++- .../services/exercise/test_create_exercise.py | 2 +- .../services/exercise/test_delete_exercise.py | 3 ++- .../services/exercise/test_update_exercise.py | 3 ++- server/app/tests/services/exercise/utilities.py | 10 ---------- .../app/tests/services/muscle_group/__init__.py | 0 .../muscle_group/test_get_muscle_groups_by_ids.py | 15 +++++++++++---- .../app/tests/services/muscle_group/utilities.py | 12 ++++++++++++ 8 files changed, 30 insertions(+), 18 deletions(-) create mode 100644 server/app/tests/services/muscle_group/__init__.py create mode 100644 server/app/tests/services/muscle_group/utilities.py diff --git a/server/app/tests/services/_utilities/queries/test_query_exercises.py b/server/app/tests/services/_utilities/queries/test_query_exercises.py index 50bd42db..02e0d0b3 100644 --- a/server/app/tests/services/_utilities/queries/test_query_exercises.py +++ b/server/app/tests/services/_utilities/queries/test_query_exercises.py @@ -5,7 +5,8 @@ from app.models.database.exercise import Exercise from app.services.utilities.queries import query_exercises -from ...exercise.utilities import create_exercise, get_muscle_group_id +from ...exercise.utilities import create_exercise +from ...muscle_group.utilities import get_muscle_group_id from ...utilities import create_user diff --git a/server/app/tests/services/exercise/test_create_exercise.py b/server/app/tests/services/exercise/test_create_exercise.py index b38bcd07..b7d18ef3 100644 --- a/server/app/tests/services/exercise/test_create_exercise.py +++ b/server/app/tests/services/exercise/test_create_exercise.py @@ -10,8 +10,8 @@ from app.services.exercise import create_exercise from app.services.utilities.queries import query_exercises +from ..muscle_group.utilities import get_muscle_group_id from ..utilities import create_user -from .utilities import get_muscle_group_id async def test_create_exercise(session: AsyncSession): diff --git a/server/app/tests/services/exercise/test_delete_exercise.py b/server/app/tests/services/exercise/test_delete_exercise.py index 3350470e..7979d52f 100644 --- a/server/app/tests/services/exercise/test_delete_exercise.py +++ b/server/app/tests/services/exercise/test_delete_exercise.py @@ -7,8 +7,9 @@ from app.models.errors import ExerciseNotFound from app.services.exercise import delete_exercise +from ..muscle_group.utilities import get_muscle_group_id from ..utilities import create_user -from .utilities import create_exercise, get_muscle_group_id +from .utilities import create_exercise async def test_delete_exercise(session: AsyncSession): diff --git a/server/app/tests/services/exercise/test_update_exercise.py b/server/app/tests/services/exercise/test_update_exercise.py index b78a0cb9..d22be4ab 100644 --- a/server/app/tests/services/exercise/test_update_exercise.py +++ b/server/app/tests/services/exercise/test_update_exercise.py @@ -12,8 +12,9 @@ from app.models.schemas.exercise import UpdateExerciseRequest from app.services.exercise import get_exercise, update_exercise +from ..muscle_group.utilities import get_muscle_group_id from ..utilities import create_user -from .utilities import create_exercise, get_muscle_group_id +from .utilities import create_exercise async def test_update_exercise(session: AsyncSession): diff --git a/server/app/tests/services/exercise/utilities.py b/server/app/tests/services/exercise/utilities.py index a9e16293..9887cbbc 100644 --- a/server/app/tests/services/exercise/utilities.py +++ b/server/app/tests/services/exercise/utilities.py @@ -1,9 +1,7 @@ -from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.database.exercise import Exercise from app.models.database.exercise_muscle_group import ExerciseMuscleGroup -from app.models.database.muscle_group import MuscleGroup async def create_exercise( @@ -31,11 +29,3 @@ async def create_exercise( await session.commit() return exercise - - -async def get_muscle_group_id(session: AsyncSession, name: str) -> int: - result = await session.execute( - select(MuscleGroup).where(MuscleGroup.name == name), - ) - muscle_group = result.scalar_one() - return muscle_group.id diff --git a/server/app/tests/services/muscle_group/__init__.py b/server/app/tests/services/muscle_group/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/app/tests/services/muscle_group/test_get_muscle_groups_by_ids.py b/server/app/tests/services/muscle_group/test_get_muscle_groups_by_ids.py index a55add72..08b0f160 100644 --- a/server/app/tests/services/muscle_group/test_get_muscle_groups_by_ids.py +++ b/server/app/tests/services/muscle_group/test_get_muscle_groups_by_ids.py @@ -3,20 +3,27 @@ from app.models.database.muscle_group import MuscleGroup from app.services.muscle_group import get_muscle_groups_by_ids +from .utilities import get_muscle_group_id + async def test_get_muscle_groups_by_ids(session: AsyncSession): - result = await get_muscle_groups_by_ids([7, 8], session) + mg_1 = await get_muscle_group_id(session, name="biceps") + mg_2 = await get_muscle_group_id(session, name="triceps") + + result = await get_muscle_groups_by_ids([mg_1, mg_2], session) assert len(result) == 2 assert all(isinstance(muscle_group, MuscleGroup) for muscle_group in result) - assert {muscle_group.id for muscle_group in result} == {7, 8} + assert {muscle_group.id for muscle_group in result} == {mg_1, mg_2} async def test_get_muscle_groups_by_ids_missing_ids(session: AsyncSession): - result = await get_muscle_groups_by_ids([1, 99999], session) + mg = await get_muscle_group_id(session, name="biceps") + + result = await get_muscle_groups_by_ids([mg, 99999], session) assert len(result) == 1 - assert result[0].id == 1 + assert result[0].id == mg async def test_get_muscle_groups_by_ids_empty(session: AsyncSession): diff --git a/server/app/tests/services/muscle_group/utilities.py b/server/app/tests/services/muscle_group/utilities.py new file mode 100644 index 00000000..ddfef1b7 --- /dev/null +++ b/server/app/tests/services/muscle_group/utilities.py @@ -0,0 +1,12 @@ +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.database.muscle_group import MuscleGroup + + +async def get_muscle_group_id(session: AsyncSession, name: str) -> int: + result = await session.execute( + select(MuscleGroup).where(MuscleGroup.name == name), + ) + muscle_group = result.scalar_one() + return muscle_group.id From 1c6901cf9d9312fb112a99a234effa3c5cc35357 Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Mon, 23 Mar 2026 13:04:28 -0500 Subject: [PATCH 08/27] client - update deps --- client/package-lock.json | 748 ++++++++++++++++++++------------------- client/package.json | 10 +- 2 files changed, 388 insertions(+), 370 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 0dadb5b7..76d18a21 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -25,7 +25,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "lucide-react": "^0.577.0", + "lucide-react": "^1.0.1", "mermaid": "^11.13.0", "next-themes": "^0.4.6", "radix-ui": "^1.4.3", @@ -33,8 +33,8 @@ "react-dom": "^19.2.4", "react-hook-form": "^7.72.0", "react-markdown": "^10.1.0", - "react-router": "^7.13.1", - "react-router-dom": "^7.13.1", + "react-router": "^7.13.2", + "react-router-dom": "^7.13.2", "rehype-highlight": "^7.0.2", "remark-gfm": "^4.0.1", "sonner": "^2.0.7", @@ -55,8 +55,8 @@ "globals": "^17.4.0", "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", - "typescript-eslint": "^8.57.1", - "vite": "^8.0.1", + "typescript-eslint": "^8.57.2", + "vite": "^8.0.2", "vite-tsconfig-paths": "^6.1.1" } }, @@ -878,9 +878,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.120.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.120.0.tgz", - "integrity": "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==", + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -3948,9 +3948,9 @@ "license": "MIT" }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz", - "integrity": "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.11.tgz", + "integrity": "sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A==", "cpu": [ "arm64" ], @@ -3964,9 +3964,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.10.tgz", - "integrity": "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.11.tgz", + "integrity": "sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg==", "cpu": [ "arm64" ], @@ -3980,9 +3980,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.10.tgz", - "integrity": "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.11.tgz", + "integrity": "sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ==", "cpu": [ "x64" ], @@ -3996,9 +3996,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.10.tgz", - "integrity": "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.11.tgz", + "integrity": "sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag==", "cpu": [ "x64" ], @@ -4012,9 +4012,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.10.tgz", - "integrity": "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.11.tgz", + "integrity": "sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ==", "cpu": [ "arm" ], @@ -4028,12 +4028,15 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.11.tgz", + "integrity": "sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q==", "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4044,12 +4047,15 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.10.tgz", - "integrity": "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.11.tgz", + "integrity": "sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4060,12 +4066,15 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.11.tgz", + "integrity": "sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw==", "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4076,12 +4085,15 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.11.tgz", + "integrity": "sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw==", "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4092,12 +4104,15 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.11.tgz", + "integrity": "sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA==", "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4108,12 +4123,15 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.10.tgz", - "integrity": "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.11.tgz", + "integrity": "sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4124,9 +4142,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.10.tgz", - "integrity": "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.11.tgz", + "integrity": "sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g==", "cpu": [ "arm64" ], @@ -4140,9 +4158,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.10.tgz", - "integrity": "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.11.tgz", + "integrity": "sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw==", "cpu": [ "wasm32" ], @@ -4156,9 +4174,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.10.tgz", - "integrity": "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.11.tgz", + "integrity": "sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q==", "cpu": [ "arm64" ], @@ -4172,9 +4190,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.10.tgz", - "integrity": "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.11.tgz", + "integrity": "sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA==", "cpu": [ "x64" ], @@ -4867,139 +4885,15 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", - "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/type-utils": "8.57.1", - "@typescript-eslint/utils": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.57.1", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz", - "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", - "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.1", - "@typescript-eslint/types": "^8.57.1", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", - "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", - "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz", - "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/utils": "8.57.1", - "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5007,138 +4901,30 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", - "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", - "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.57.1", - "@typescript-eslint/tsconfig-utils": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" - }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz", - "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", - "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/types": "8.57.2", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -7868,9 +7654,9 @@ } }, "node_modules/lucide-react": { - "version": "0.577.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", - "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.0.1.tgz", + "integrity": "sha512-lih7tKEczCYOQjVEzpFuxEuNzlwf+1yhvlMlEkGWJM3va8Pugv8bYXc/pRtcjPncaP7k84X0Pt/71ufxvqEPtQ==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -9507,9 +9293,9 @@ } }, "node_modules/react-router": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", - "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", + "version": "7.13.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.2.tgz", + "integrity": "sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -9529,12 +9315,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", - "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", + "version": "7.13.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.2.tgz", + "integrity": "sha512-aR7SUORwTqAW0JDeiWF07e9SBE9qGpByR9I8kJT5h/FrBKxPMS6TiC7rmVO+gC0q52Bx7JnjWe8Z1sR9faN4YA==", "license": "MIT", "dependencies": { - "react-router": "7.13.1" + "react-router": "7.13.2" }, "engines": { "node": ">=20.0.0" @@ -9688,13 +9474,13 @@ "license": "Unlicense" }, "node_modules/rolldown": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.10.tgz", - "integrity": "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.11.tgz", + "integrity": "sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw==", "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.120.0", - "@rolldown/pluginutils": "1.0.0-rc.10" + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.11" }, "bin": { "rolldown": "bin/cli.mjs" @@ -9703,27 +9489,27 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.10", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.10", - "@rolldown/binding-darwin-x64": "1.0.0-rc.10", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.10", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" + "@rolldown/binding-android-arm64": "1.0.0-rc.11", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.11", + "@rolldown/binding-darwin-x64": "1.0.0-rc.11", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.11", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.11", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.11", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.11", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.11", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.11", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.11", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.11", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.11", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.11", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.11", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.11" } }, "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.10.tgz", - "integrity": "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.11.tgz", + "integrity": "sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ==", "license": "MIT" }, "node_modules/roughjs": { @@ -9973,9 +9759,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -9994,27 +9780,6 @@ "node": ">=6.10" } }, - "node_modules/tsconfck": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", - "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", - "dev": true, - "license": "MIT", - "bin": { - "tsconfck": "bin/tsconfck.js" - }, - "engines": { - "node": "^18 || >=20" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -10058,16 +9823,44 @@ } }, "node_modules/typescript-eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.1.tgz", - "integrity": "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz", + "integrity": "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.2", + "@typescript-eslint/parser": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.57.1", - "@typescript-eslint/parser": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/utils": "8.57.1" + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10077,10 +9870,214 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { + "@typescript-eslint/parser": "^8.57.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/typescript-eslint/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/typescript-eslint/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/typescript-eslint/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/typescript-eslint/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typescript-eslint/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ufo": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.2.tgz", @@ -10336,15 +10333,15 @@ } }, "node_modules/vite": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz", - "integrity": "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.2.tgz", + "integrity": "sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA==", "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.10", + "rolldown": "1.0.0-rc.11", "tinyglobby": "^0.2.15" }, "bin": { @@ -10427,6 +10424,27 @@ "vite": "*" } }, + "node_modules/vite-tsconfig-paths/node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/vscode-jsonrpc": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", diff --git a/client/package.json b/client/package.json index 0fec4c12..3ff73c83 100644 --- a/client/package.json +++ b/client/package.json @@ -35,7 +35,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "lucide-react": "^0.577.0", + "lucide-react": "^1.0.1", "mermaid": "^11.13.0", "next-themes": "^0.4.6", "radix-ui": "^1.4.3", @@ -43,8 +43,8 @@ "react-dom": "^19.2.4", "react-hook-form": "^7.72.0", "react-markdown": "^10.1.0", - "react-router": "^7.13.1", - "react-router-dom": "^7.13.1", + "react-router": "^7.13.2", + "react-router-dom": "^7.13.2", "rehype-highlight": "^7.0.2", "remark-gfm": "^4.0.1", "sonner": "^2.0.7", @@ -65,8 +65,8 @@ "globals": "^17.4.0", "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", - "typescript-eslint": "^8.57.1", - "vite": "^8.0.1", + "typescript-eslint": "^8.57.2", + "vite": "^8.0.2", "vite-tsconfig-paths": "^6.1.1" } } From fd40c9858e71584be338d3b67d80f9b13d700051 Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Mon, 23 Mar 2026 13:16:28 -0500 Subject: [PATCH 09/27] client - fix lint issues --- client/src/api/axios.ts | 1 + client/src/components/AccessRequestsTable.tsx | 1 + client/src/components/data-table/DataTable.tsx | 2 +- client/src/components/exercises/ExerciseFormDialog.tsx | 2 +- client/src/components/exercises/ExercisesTable.tsx | 1 + 5 files changed, 5 insertions(+), 2 deletions(-) diff --git a/client/src/api/axios.ts b/client/src/api/axios.ts index b6981945..e6d10234 100644 --- a/client/src/api/axios.ts +++ b/client/src/api/axios.ts @@ -9,6 +9,7 @@ import { logger } from '@/lib/logger' import axios, { AxiosError } from 'axios' // created for type safety +// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-arguments const refreshUrl = client.buildUrl({ url: '/api/auth/refresh-token', }) diff --git a/client/src/components/AccessRequestsTable.tsx b/client/src/components/AccessRequestsTable.tsx index d62ad4f8..efd65ae0 100644 --- a/client/src/components/AccessRequestsTable.tsx +++ b/client/src/components/AccessRequestsTable.tsx @@ -73,6 +73,7 @@ export function AccessRequestsTable({ onReloadRequests, }: AccessRequestsTableProps) { const { user } = useSession() + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-arguments const [isLoadingRequestIds, setIsLoadingRequestIds] = useState>( new Set() ) diff --git a/client/src/components/data-table/DataTable.tsx b/client/src/components/data-table/DataTable.tsx index 69794f4e..845f153e 100644 --- a/client/src/components/data-table/DataTable.tsx +++ b/client/src/components/data-table/DataTable.tsx @@ -48,7 +48,7 @@ export function DataTable({ lastColumnPaddingExcludeIds = ['actions'], }: DataTableProps) { const [rowSelection, setRowSelection] = useState({}) - const [columnVisibility, setColumnVisibility] = useState( + const [columnVisibility, setColumnVisibility] = useState( initialColumnVisibility ?? {} ) const [columnFilters, setColumnFilters] = useState([]) diff --git a/client/src/components/exercises/ExerciseFormDialog.tsx b/client/src/components/exercises/ExerciseFormDialog.tsx index 1c06ece0..06769889 100644 --- a/client/src/components/exercises/ExerciseFormDialog.tsx +++ b/client/src/components/exercises/ExerciseFormDialog.tsx @@ -98,7 +98,7 @@ export function ExerciseFormDialog({ isSubmitting: isEditSubmitting, }, reset: resetEdit, - } = useForm({ + } = useForm({ resolver: zodResolver(zUpdateExerciseRequest), defaultValues: defaultUpdateExerciseFormValues, mode: 'onSubmit', diff --git a/client/src/components/exercises/ExercisesTable.tsx b/client/src/components/exercises/ExercisesTable.tsx index 3282197f..8bc02f65 100644 --- a/client/src/components/exercises/ExercisesTable.tsx +++ b/client/src/components/exercises/ExercisesTable.tsx @@ -53,6 +53,7 @@ export function ExercisesTable({ onReloadMuscleGroups, }: ExercisesTableProps) { const [isLoadingExerciseIds, setIsLoadingExerciseIds] = useState< + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-arguments Set >(new Set()) const [isDeleting, setIsDeleting] = useState(false) From c29ce1d9ddd543100f824e170e9b3aeaec3fbaed Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Mon, 23 Mar 2026 14:48:04 -0500 Subject: [PATCH 10/27] client - use vite built in tsconfig paths --- client/package-lock.json | 46 +--------------------------------------- client/package.json | 3 +-- client/vite.config.ts | 6 ++++-- 3 files changed, 6 insertions(+), 49 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 76d18a21..84f95aea 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -56,8 +56,7 @@ "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", "typescript-eslint": "^8.57.2", - "vite": "^8.0.2", - "vite-tsconfig-paths": "^6.1.1" + "vite": "^8.0.2" } }, "node_modules/@antfu/install-pkg": { @@ -6794,13 +6793,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true, - "license": "MIT" - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -10409,42 +10401,6 @@ } } }, - "node_modules/vite-tsconfig-paths": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-6.1.1.tgz", - "integrity": "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "globrex": "^0.1.2", - "tsconfck": "^3.0.3" - }, - "peerDependencies": { - "vite": "*" - } - }, - "node_modules/vite-tsconfig-paths/node_modules/tsconfck": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", - "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", - "dev": true, - "license": "MIT", - "bin": { - "tsconfck": "bin/tsconfck.js" - }, - "engines": { - "node": "^18 || >=20" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/vscode-jsonrpc": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", diff --git a/client/package.json b/client/package.json index 3ff73c83..fb6a4948 100644 --- a/client/package.json +++ b/client/package.json @@ -66,7 +66,6 @@ "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", "typescript-eslint": "^8.57.2", - "vite": "^8.0.2", - "vite-tsconfig-paths": "^6.1.1" + "vite": "^8.0.2" } } diff --git a/client/vite.config.ts b/client/vite.config.ts index 259d5b4f..c0d20064 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,10 +1,12 @@ import tailwindcss from '@tailwindcss/vite' import react from '@vitejs/plugin-react' import { defineConfig } from 'vite' -import tsconfigPaths from 'vite-tsconfig-paths' // https://vite.dev/config/ export default defineConfig({ - plugins: [react(), tsconfigPaths(), tailwindcss()], + plugins: [react(), tailwindcss()], envDir: '../config/env', + resolve: { + tsconfigPaths: true, + }, }) From c2d3785108a0e865d74e0941f53d434a447db955 Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Mon, 23 Mar 2026 15:02:07 -0500 Subject: [PATCH 11/27] client - forward console logs --- client/vite.config.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/vite.config.ts b/client/vite.config.ts index c0d20064..ba285beb 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -9,4 +9,10 @@ export default defineConfig({ resolve: { tsconfigPaths: true, }, + server: { + forwardConsole: { + unhandledErrors: true, + logLevels: ['debug', 'log', 'info', 'warn', 'error'], + }, + }, }) From ff4e71f1a3a6a033790fa63da0f0c6f10a874b17 Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Mon, 23 Mar 2026 15:13:59 -0500 Subject: [PATCH 12/27] client - improve header actions on small screens --- client/src/components/FeedbackFormDialog.tsx | 11 ++-- client/src/components/HeaderActions.tsx | 57 ++++++++++++++++++++ client/src/layout/AppLayout.tsx | 33 ++---------- 3 files changed, 68 insertions(+), 33 deletions(-) create mode 100644 client/src/components/HeaderActions.tsx diff --git a/client/src/components/FeedbackFormDialog.tsx b/client/src/components/FeedbackFormDialog.tsx index f0a74fad..d2638407 100644 --- a/client/src/components/FeedbackFormDialog.tsx +++ b/client/src/components/FeedbackFormDialog.tsx @@ -17,6 +17,7 @@ import { Textarea } from '@/components/ui/textarea' import { handleApiError } from '@/lib/http' import { notify } from '@/lib/notify' import { zodResolver } from '@hookform/resolvers/zod' +import type { ReactElement } from 'react' import { useState } from 'react' import { useForm } from 'react-hook-form' import { z } from 'zod' @@ -27,7 +28,11 @@ const feedbackFormSchema = zCreateFeedbackRequest.omit({ }) type FeedbackForm = z.infer -export function FeedbackFormDialog() { +interface FeedbackFormDialogProps { + trigger: ReactElement +} + +export function FeedbackFormDialog({ trigger }: FeedbackFormDialogProps) { const [open, setOpen] = useState(false) const [files, setFiles] = useState([]) @@ -83,9 +88,7 @@ export function FeedbackFormDialog() { return ( - - - + {trigger} { onAttemptClose(e) diff --git a/client/src/components/HeaderActions.tsx b/client/src/components/HeaderActions.tsx new file mode 100644 index 00000000..8613009d --- /dev/null +++ b/client/src/components/HeaderActions.tsx @@ -0,0 +1,57 @@ +import { AuthService } from '@/api/generated' +import { useSession } from '@/auth/session' +import { FeedbackFormDialog } from '@/components/FeedbackFormDialog' +import { ModeToggle } from '@/components/ModeToggle' +import { Button } from '@/components/ui/overrides/button' +import { notify } from '@/lib/notify' +import { LogOut, MessageCircle } from 'lucide-react' +import { useNavigate } from 'react-router-dom' + +export function HeaderActions() { + const { refresh } = useSession() + const navigate = useNavigate() + + const handleLogout = async () => { + const { error } = await AuthService.logout() + if (error) { + notify.error('Failed to log out') + return + } + notify.success('Logged out') + await refresh() + void navigate('/login', { replace: true }) + } + + return ( + <> +
+ + Feedback} /> + +
+
+ + + + + } + /> + +
+ + ) +} diff --git a/client/src/layout/AppLayout.tsx b/client/src/layout/AppLayout.tsx index cbcfaac3..4e05b59a 100644 --- a/client/src/layout/AppLayout.tsx +++ b/client/src/layout/AppLayout.tsx @@ -1,26 +1,10 @@ -import { AuthService } from '@/api/generated' import { useSession } from '@/auth/session' -import { FeedbackFormDialog } from '@/components/FeedbackFormDialog' -import { ModeToggle } from '@/components/ModeToggle' -import { Button } from '@/components/ui/overrides/button' +import { HeaderActions } from '@/components/HeaderActions' import { NavItem } from '@/lib/nav' -import { notify } from '@/lib/notify' -import { NavLink, Outlet, useNavigate } from 'react-router-dom' +import { NavLink, Outlet } from 'react-router-dom' export function AppLayout() { - const { refresh, user } = useSession() - const navigate = useNavigate() - - const handleLogout = async () => { - const { error } = await AuthService.logout() - if (error) { - notify.error('Failed to log out') - return - } - notify.success('Logged out') - await refresh() - void navigate('/login', { replace: true }) - } + const { user } = useSession() return (
@@ -39,16 +23,7 @@ export function AppLayout() { )}
-
- - - -
+
From 71da77f96f0868b1cb0bef60de8aeff09daecebb Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Mon, 23 Mar 2026 15:24:28 -0500 Subject: [PATCH 13/27] client - add dropdown menu for mobile --- client/src/components/HeaderActions.tsx | 2 +- client/src/layout/AppLayout.tsx | 60 ++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/client/src/components/HeaderActions.tsx b/client/src/components/HeaderActions.tsx index 8613009d..b4616a33 100644 --- a/client/src/components/HeaderActions.tsx +++ b/client/src/components/HeaderActions.tsx @@ -34,7 +34,7 @@ export function HeaderActions() { Logout -
+
RepTrack -
- +
+
+ + + + + + {navLinks.map((link) => ( + + + {link.label} + + + ))} + {user?.is_admin && ( + <> + + + + Admin + + + + )} + + +
+ +
From 0a452bfe5f9d4704fbd2d66c9ae3f719dfb910d1 Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Tue, 24 Mar 2026 09:20:34 -0500 Subject: [PATCH 14/27] add browser automation instructions --- .github/copilot-instructions.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f90e8147..cc772076 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -55,6 +55,17 @@ RepTrack is a full-stack strength-training tracker: - SDK generation strategy is `byTags` + `operationId` nesting, so endpoint `operation_id` values drive generated method names. - Generated files live in `client/src/api/generated/` and should not be hand-edited. +## Browser Automation + +Use `agent-browser` for web automation. Run `agent-browser --help` for all commands. + +Core workflow: + +1. `agent-browser open ` - Navigate to page +2. `agent-browser snapshot -i` - Get interactive elements with refs (@e1, @e2) +3. `agent-browser click @e1` / `fill @e2 "text"` - Interact using refs +4. Re-snapshot after page changes + ## Key Conventions - Always set explicit `operation_id` on FastAPI endpoints. Missing or unstable IDs cause churn/breakage in generated TS SDK method names. From 2579b389fc57237ebdc07e12927e5aec444a676c Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Wed, 25 Mar 2026 10:55:19 -0500 Subject: [PATCH 15/27] add meilisearch setup --- config/env/.env.example | 4 ++ config/infra/docker-compose.override.yml | 9 +++ config/infra/docker-compose.yml | 37 ++++++++++ server/app/core/config.py | 2 + server/app/core/dependencies.py | 22 +++++- server/app/models/schemas/config.py | 6 ++ server/pyproject.toml | 2 + server/uv.lock | 91 ++++++++++++++++++++++++ 8 files changed, 172 insertions(+), 1 deletion(-) diff --git a/config/env/.env.example b/config/env/.env.example index 29fa6e98..689deff4 100644 --- a/config/env/.env.example +++ b/config/env/.env.example @@ -46,3 +46,7 @@ GH__BACKEND= GH__REPO_OWNER= GH__TOKEN= GH__ISSUE_ASSIGNEE= + +MS__HOST= +MS__PORT= +MS__MASTER_KEY= diff --git a/config/infra/docker-compose.override.yml b/config/infra/docker-compose.override.yml index 755ca34b..7dffb2f8 100644 --- a/config/infra/docker-compose.override.yml +++ b/config/infra/docker-compose.override.yml @@ -13,6 +13,15 @@ services: - traefik.http.routers.${PROJECT?}-${ENV?}-adminer.rule=Host(`db.${DOMAIN?}`) - traefik.http.routers.${PROJECT?}-${ENV?}-adminer.entrypoints=web + meilisearch: + restart: no + ports: + - ${MS__PORT?}:7700 + + labels: + - traefik.http.routers.${PROJECT?}-${ENV?}-meilisearch.rule=Host(`search.${DOMAIN?}`) + - traefik.http.routers.${PROJECT?}-${ENV?}-meilisearch.entrypoints=web + mailcatcher: image: dockage/mailcatcher:0.9.0 container_name: ${PROJECT?}-${ENV?}-mailcatcher diff --git a/config/infra/docker-compose.yml b/config/infra/docker-compose.yml index 04a9b8da..06bf916f 100644 --- a/config/infra/docker-compose.yml +++ b/config/infra/docker-compose.yml @@ -40,6 +40,32 @@ services: - traefik.http.routers.${PROJECT?}-${ENV?}-adminer.rule=Host(`db-${DOMAIN?}`) - traefik.http.routers.${PROJECT?}-${ENV?}-adminer.entrypoints=websecure + meilisearch: + image: getmeili/meilisearch:v1.40.0 + container_name: ${PROJECT?}-${ENV?}-meilisearch + restart: unless-stopped + networks: + - traefik-public + - default + + volumes: + - meili-data:/meili_data + environment: + - MEILI_MASTER_KEY=${MS__MASTER_KEY?} + + healthcheck: + test: 'curl -f http://localhost:7700/health || exit 1' + start_period: 5s + timeout: 5s + retries: 3 + interval: 1m + + labels: + - traefik.enable=true + - traefik.prod=${PROD?} + - traefik.http.routers.${PROJECT?}-${ENV?}-meilisearch.rule=Host(`search-${DOMAIN?}`) + - traefik.http.routers.${PROJECT?}-${ENV?}-meilisearch.entrypoints=websecure + migrations: image: ${DOCKER_REGISTRY?}/${PROJECT?}-${ENV?}-server:${TAG?} container_name: ${PROJECT?}-${ENV?}-migrations @@ -75,6 +101,11 @@ services: - DB__USER=${DB__USER?} - DB__PASSWORD=${DB__PASSWORD?} + # internal host & port + - MS__HOST=meilisearch + - MS__PORT=7700 + - MS__MASTER_KEY=${MS__MASTER_KEY?} + - EMAIL__BACKEND=${EMAIL__BACKEND?} - EMAIL__EMAIL_FROM=${EMAIL__EMAIL_FROM?} # internal host @@ -135,6 +166,11 @@ services: - DB__USER=${DB__USER?} - DB__PASSWORD=${DB__PASSWORD?} + # internal host & port + - MS__HOST=meilisearch + - MS__PORT=7700 + - MS__MASTER_KEY=${MS__MASTER_KEY?} + - EMAIL__BACKEND=${EMAIL__BACKEND?} - EMAIL__EMAIL_FROM=${EMAIL__EMAIL_FROM?} # internal host @@ -187,6 +223,7 @@ services: volumes: pg-data: + meili-data: server-data: server-logs: diff --git a/server/app/core/config.py b/server/app/core/config.py index cb6965ef..1751596b 100644 --- a/server/app/core/config.py +++ b/server/app/core/config.py @@ -16,6 +16,7 @@ GitHubApiSettings, GitHubConsoleSettings, JWTSettings, + MeilisearchSettings, ) EmailSettings = ( @@ -36,6 +37,7 @@ class Settings(BaseSettings): admin: AdminSettings jwt: JWTSettings db: DatabaseSettings + ms: MeilisearchSettings # discriminator with any caps does not work email: Annotated[EmailSettings, Field(discriminator="backend")] gh: Annotated[GitHubSettings, Field(discriminator="backend")] diff --git a/server/app/core/dependencies.py b/server/app/core/dependencies.py index d030b1cc..94865396 100644 --- a/server/app/core/dependencies.py +++ b/server/app/core/dependencies.py @@ -5,6 +5,7 @@ from fastapi import Depends from fastapi.security import APIKeyCookie +from meilisearch_python_sdk import AsyncClient from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from app.core.config import Settings, get_settings @@ -32,7 +33,10 @@ def get_sessionmaker(db_url: str, is_prod: bool): async def get_db( settings: Annotated[Settings, Depends(get_settings)], ) -> AsyncGenerator[AsyncSession]: - async with get_sessionmaker(settings.db.url, settings.is_prod_like)() as session: + async with get_sessionmaker( + settings.db.url, + settings.is_prod_like, + )() as session: yield session @@ -45,6 +49,22 @@ async def get_db( ) +@cache +def build_ms_client(host: str, port: int, master_key: str) -> AsyncClient: + url = f"http://{host}:{port}" + return AsyncClient(url, master_key) + + +def get_ms_client( + settings: Annotated[Settings, Depends(get_settings)], +) -> AsyncClient: + return build_ms_client( + settings.ms.host, + settings.ms.port, + settings.ms.master_key, + ) + + async def get_current_user( token: Annotated[str, Depends(access_token_cookie)], db: Annotated[AsyncSession, Depends(get_db)], diff --git a/server/app/models/schemas/config.py b/server/app/models/schemas/config.py index 37ebb6a9..ca713749 100644 --- a/server/app/models/schemas/config.py +++ b/server/app/models/schemas/config.py @@ -35,6 +35,12 @@ def url(self) -> str: ) +class MeilisearchSettings(BaseModel): + host: str + port: int + master_key: str + + class EmailSmtpSettings(BaseModel): backend: Literal["smtp"] # allow arbitrary string diff --git a/server/pyproject.toml b/server/pyproject.toml index b520fecf..7e146cd1 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -8,6 +8,8 @@ dependencies = [ "asyncpg>=0.31.0", "fastapi-swagger-dark>=0.0.9", "fastapi[standard]>=0.124.4", + "meilisearch>=0.40.0", + "meilisearch-python-sdk>=7.1.1", "pwdlib[argon2]>=0.3.0", "pydantic-settings>=2.12.0", "pyjwt>=2.10.1", diff --git a/server/uv.lock b/server/uv.lock index 43f8c5a4..d46be9f7 100644 --- a/server/uv.lock +++ b/server/uv.lock @@ -2,6 +2,15 @@ version = 1 revision = 3 requires-python = ">=3.14" +[[package]] +name = "aiofiles" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, +] + [[package]] name = "aiosmtplib" version = "5.1.0" @@ -119,6 +128,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" }, ] +[[package]] +name = "camel-converter" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/cb/55feabf13527c464db60145e2e0dccd2f190b0a4a417cac2473bd2b18508/camel_converter-5.1.0.tar.gz", hash = "sha256:d979f23de232aa262dcf15a263ed9af934ff4378644dae23a7c2f8d40f4fff8f", size = 71481, upload-time = "2026-03-05T15:30:58.957Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/c3/8fc18a914da9747b7a652180758a46a3d5f22efd0dffd5a8b97acfb1ebf3/camel_converter-5.1.0-py3-none-any.whl", hash = "sha256:93f05d7d84a4353ddfd87bca909d1de45c11f5757f6a460de10534397146fe88", size = 6960, upload-time = "2026-03-05T15:31:00.135Z" }, +] + +[package.optional-dependencies] +pydantic = [{ name = "pydantic" }] + [[package]] name = "certifi" version = "2025.11.12" @@ -434,6 +455,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "h2" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [{ name = "hpack" }, { name = "hyperframe" }] +sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, +] + +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -474,6 +514,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[package.optional-dependencies] +http2 = [{ name = "h2" }] + +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -561,6 +613,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "meilisearch" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "camel-converter", extra = [ + "pydantic", + ] }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/94/b69c53be760d49d38a3e933aabcc506e48938fc1834c32cbf23a8b795a33/meilisearch-0.40.0.tar.gz", hash = "sha256:4348ea6e4bacb4b1391b9b577956a03090ff9327d6ebcddcf479f39b2d4b5b05", size = 31259, upload-time = "2026-01-15T06:29:18.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/06/0f29c374f7fa14ad8843eb458e258dacc215e584c0d77523b7e7cf23c24b/meilisearch-0.40.0-py3-none-any.whl", hash = "sha256:ae7378e448f01116f4fcbd0029f5383a2b17ed971ba2241c631408c45c86ce9d", size = 31776, upload-time = "2026-01-15T06:29:15.851Z" }, +] + +[[package]] +name = "meilisearch-python-sdk" +version = "7.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "camel-converter", extra = [ + "pydantic", + ] }, + { name = "httpx", extra = [ + "http2", + ] }, + { name = "pydantic" }, + { name = "pyjwt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/d2/adb8f0f81ffe9fd2344221b65aa422083ee9da76a88f2ba96b013f7ef8ad/meilisearch_python_sdk-7.1.1.tar.gz", hash = "sha256:b50448453ff75b66bd2ba7881ee1afe0c2404c419a0db80da021804cb17a2424", size = 257752, upload-time = "2026-03-24T19:17:23.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/b3/9d78964962090e4f1e1b0016d6459f0e17b47eaf747ca4150607a2476b68/meilisearch_python_sdk-7.1.1-py3-none-any.whl", hash = "sha256:bdbd709feaf9b8bf3dd1b6f2057016eadd4245f73bf33b3a1d11eeefe3b9d744", size = 76694, upload-time = "2026-03-24T19:17:24.978Z" }, +] + [[package]] name = "nodeenv" version = "1.10.0" @@ -931,6 +1018,8 @@ dependencies = [ "standard", ] }, { name = "fastapi-swagger-dark" }, + { name = "meilisearch" }, + { name = "meilisearch-python-sdk" }, { name = "pwdlib", extra = [ "argon2", ] }, @@ -964,6 +1053,8 @@ requires-dist = [ "standard", ], specifier = ">=0.124.4" }, { name = "fastapi-swagger-dark", specifier = ">=0.0.9" }, + { name = "meilisearch", specifier = ">=0.40.0" }, + { name = "meilisearch-python-sdk", specifier = ">=7.1.1" }, { name = "pwdlib", extras = [ "argon2", ], specifier = ">=0.3.0" }, From c71740ed56e604861405b06c2e56923be3822639 Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Wed, 25 Mar 2026 11:05:55 -0500 Subject: [PATCH 16/27] server - implement search functionality --- client/src/api/generated/index.ts | 4 +- client/src/api/generated/schemas.gen.ts | 21 ++++ client/src/api/generated/sdk.gen.ts | 59 +++++++++- client/src/api/generated/types.gen.ts | 97 +++++++++++++++++ client/src/api/generated/zod.gen.ts | 31 ++++++ server/app/api/endpoints/search.py | 78 ++++++++++++++ server/app/api/router.py | 2 + server/app/models/enums.py | 5 + server/app/models/schemas/search.py | 15 +++ server/app/models/schemas/types.py | 2 + server/app/services/search.py | 136 ++++++++++++++++++++++++ 11 files changed, 447 insertions(+), 3 deletions(-) create mode 100644 server/app/api/endpoints/search.py create mode 100644 server/app/models/schemas/search.py create mode 100644 server/app/services/search.py diff --git a/client/src/api/generated/index.ts b/client/src/api/generated/index.ts index 5621e472..3777a830 100644 --- a/client/src/api/generated/index.ts +++ b/client/src/api/generated/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export { AdminService, AuthService, ExerciseService, FeedbackService, HealthService, MuscleGroupService, type Options, SetService, UserService, WorkoutExerciseService, WorkoutService } from './sdk.gen'; -export type { AccessRequestPublic, AccessRequestStatus, ClientOptions, CreateExerciseData, CreateExerciseError, CreateExerciseErrors, CreateExerciseRequest, CreateExerciseResponse, CreateExerciseResponses, CreateFeedbackData, CreateFeedbackError, CreateFeedbackErrors, CreateFeedbackRequest, CreateFeedbackResponses, CreateSetData, CreateSetError, CreateSetErrors, CreateSetRequest, CreateSetResponse, CreateSetResponses, CreateWorkoutData, CreateWorkoutError, CreateWorkoutErrors, CreateWorkoutExerciseData, CreateWorkoutExerciseError, CreateWorkoutExerciseErrors, CreateWorkoutExerciseRequest, CreateWorkoutExerciseResponse, CreateWorkoutExerciseResponses, CreateWorkoutRequest, CreateWorkoutResponse, CreateWorkoutResponses, DeleteExerciseData, DeleteExerciseError, DeleteExerciseErrors, DeleteExerciseResponse, DeleteExerciseResponses, DeleteSetData, DeleteSetError, DeleteSetErrors, DeleteSetResponse, DeleteSetResponses, DeleteWorkoutData, DeleteWorkoutError, DeleteWorkoutErrors, DeleteWorkoutExerciseData, DeleteWorkoutExerciseError, DeleteWorkoutExerciseErrors, DeleteWorkoutExerciseResponse, DeleteWorkoutExerciseResponses, DeleteWorkoutResponse, DeleteWorkoutResponses, ErrorResponse, ExerciseBase, ExercisePublic, FeedbackType, ForgotPasswordData, ForgotPasswordError, ForgotPasswordErrors, ForgotPasswordRequest, ForgotPasswordResponse, ForgotPasswordResponses, GetAccessRequestsData, GetAccessRequestsError, GetAccessRequestsErrors, GetAccessRequestsResponse, GetAccessRequestsResponses, GetCurrentUserData, GetCurrentUserError, GetCurrentUserErrors, GetCurrentUserResponse, GetCurrentUserResponses, GetDbHealthData, GetDbHealthResponse, GetDbHealthResponses, GetExerciseData, GetExerciseError, GetExerciseErrors, GetExerciseResponse, GetExerciseResponses, GetExercisesData, GetExercisesError, GetExercisesErrors, GetExercisesResponse, GetExercisesResponses, GetHealthData, GetHealthResponse, GetHealthResponses, GetMuscleGroupsData, GetMuscleGroupsError, GetMuscleGroupsErrors, GetMuscleGroupsResponse, GetMuscleGroupsResponses, GetUsersData, GetUsersError, GetUsersErrors, GetUsersResponse, GetUsersResponses, GetWorkoutData, GetWorkoutError, GetWorkoutErrors, GetWorkoutResponse, GetWorkoutResponses, GetWorkoutsData, GetWorkoutsError, GetWorkoutsErrors, GetWorkoutsResponse, GetWorkoutsResponses, HttpValidationError, LoginData, LoginError, LoginErrors, LoginRequest, LoginResponse, LoginResponses, LogoutData, LogoutResponse, LogoutResponses, MuscleGroupPublic, RefreshTokenData, RefreshTokenError, RefreshTokenErrors, RefreshTokenResponse, RefreshTokenResponses, RegisterData, RegisterError, RegisterErrors, RegisterRequest, RegisterResponse, RegisterResponses, RequestAccessData, RequestAccessError, RequestAccessErrors, RequestAccessRequest, RequestAccessResponse, RequestAccessResponses, ResetPasswordData, ResetPasswordError, ResetPasswordErrors, ResetPasswordRequest, ResetPasswordResponse, ResetPasswordResponses, ReviewerPublic, SetPublic, SetUnit, UpdateAccessRequestStatusData, UpdateAccessRequestStatusError, UpdateAccessRequestStatusErrors, UpdateAccessRequestStatusRequest, UpdateAccessRequestStatusResponse, UpdateAccessRequestStatusResponses, UpdateExerciseData, UpdateExerciseError, UpdateExerciseErrors, UpdateExerciseRequest, UpdateExerciseResponse, UpdateExerciseResponses, UpdateSetData, UpdateSetError, UpdateSetErrors, UpdateSetRequest, UpdateSetResponse, UpdateSetResponses, UpdateWorkoutData, UpdateWorkoutError, UpdateWorkoutErrors, UpdateWorkoutRequest, UpdateWorkoutResponse, UpdateWorkoutResponses, UserPublic, ValidationError, WorkoutBase, WorkoutExercisePublic, WorkoutPublic } from './types.gen'; +export { AdminService, AuthService, ExerciseService, FeedbackService, HealthService, MuscleGroupService, type Options, SearchService, SetService, UserService, WorkoutExerciseService, WorkoutService } from './sdk.gen'; +export type { AccessRequestPublic, AccessRequestStatus, ClientOptions, CreateExerciseData, CreateExerciseError, CreateExerciseErrors, CreateExerciseRequest, CreateExerciseResponse, CreateExerciseResponses, CreateFeedbackData, CreateFeedbackError, CreateFeedbackErrors, CreateFeedbackRequest, CreateFeedbackResponses, CreateSetData, CreateSetError, CreateSetErrors, CreateSetRequest, CreateSetResponse, CreateSetResponses, CreateWorkoutData, CreateWorkoutError, CreateWorkoutErrors, CreateWorkoutExerciseData, CreateWorkoutExerciseError, CreateWorkoutExerciseErrors, CreateWorkoutExerciseRequest, CreateWorkoutExerciseResponse, CreateWorkoutExerciseResponses, CreateWorkoutRequest, CreateWorkoutResponse, CreateWorkoutResponses, DeleteExerciseData, DeleteExerciseError, DeleteExerciseErrors, DeleteExerciseResponse, DeleteExerciseResponses, DeleteSetData, DeleteSetError, DeleteSetErrors, DeleteSetResponse, DeleteSetResponses, DeleteWorkoutData, DeleteWorkoutError, DeleteWorkoutErrors, DeleteWorkoutExerciseData, DeleteWorkoutExerciseError, DeleteWorkoutExerciseErrors, DeleteWorkoutExerciseResponse, DeleteWorkoutExerciseResponses, DeleteWorkoutResponse, DeleteWorkoutResponses, ErrorResponse, ExerciseBase, ExercisePublic, FeedbackType, ForgotPasswordData, ForgotPasswordError, ForgotPasswordErrors, ForgotPasswordRequest, ForgotPasswordResponse, ForgotPasswordResponses, GetAccessRequestsData, GetAccessRequestsError, GetAccessRequestsErrors, GetAccessRequestsResponse, GetAccessRequestsResponses, GetCurrentUserData, GetCurrentUserError, GetCurrentUserErrors, GetCurrentUserResponse, GetCurrentUserResponses, GetDbHealthData, GetDbHealthResponse, GetDbHealthResponses, GetExerciseData, GetExerciseError, GetExerciseErrors, GetExerciseResponse, GetExerciseResponses, GetExercisesData, GetExercisesError, GetExercisesErrors, GetExercisesResponse, GetExercisesResponses, GetHealthData, GetHealthResponse, GetHealthResponses, GetMuscleGroupsData, GetMuscleGroupsError, GetMuscleGroupsErrors, GetMuscleGroupsResponse, GetMuscleGroupsResponses, GetUsersData, GetUsersError, GetUsersErrors, GetUsersResponse, GetUsersResponses, GetWorkoutData, GetWorkoutError, GetWorkoutErrors, GetWorkoutResponse, GetWorkoutResponses, GetWorkoutsData, GetWorkoutsError, GetWorkoutsErrors, GetWorkoutsResponse, GetWorkoutsResponses, HttpValidationError, LoginData, LoginError, LoginErrors, LoginRequest, LoginResponse, LoginResponses, LogoutData, LogoutResponse, LogoutResponses, MuscleGroupPublic, RefreshTokenData, RefreshTokenError, RefreshTokenErrors, RefreshTokenResponse, RefreshTokenResponses, RegisterData, RegisterError, RegisterErrors, RegisterRequest, RegisterResponse, RegisterResponses, ReindexData, ReindexError, ReindexErrors, ReindexResponse, ReindexResponses, RequestAccessData, RequestAccessError, RequestAccessErrors, RequestAccessRequest, RequestAccessResponse, RequestAccessResponses, ResetPasswordData, ResetPasswordError, ResetPasswordErrors, ResetPasswordRequest, ResetPasswordResponse, ResetPasswordResponses, ReviewerPublic, SearchExercisesData, SearchExercisesError, SearchExercisesErrors, SearchExercisesResponses, SearchMuscleGroupsData, SearchMuscleGroupsError, SearchMuscleGroupsErrors, SearchMuscleGroupsResponses, SearchRequest, SetPublic, SetUnit, UpdateAccessRequestStatusData, UpdateAccessRequestStatusError, UpdateAccessRequestStatusErrors, UpdateAccessRequestStatusRequest, UpdateAccessRequestStatusResponse, UpdateAccessRequestStatusResponses, UpdateExerciseData, UpdateExerciseError, UpdateExerciseErrors, UpdateExerciseRequest, UpdateExerciseResponse, UpdateExerciseResponses, UpdateSetData, UpdateSetError, UpdateSetErrors, UpdateSetRequest, UpdateSetResponse, UpdateSetResponses, UpdateWorkoutData, UpdateWorkoutError, UpdateWorkoutErrors, UpdateWorkoutRequest, UpdateWorkoutResponse, UpdateWorkoutResponses, UserPublic, ValidationError, WorkoutBase, WorkoutExercisePublic, WorkoutPublic } from './types.gen'; diff --git a/client/src/api/generated/schemas.gen.ts b/client/src/api/generated/schemas.gen.ts index 8667d263..1e256a25 100644 --- a/client/src/api/generated/schemas.gen.ts +++ b/client/src/api/generated/schemas.gen.ts @@ -626,6 +626,27 @@ export const ReviewerPublicSchema = { title: 'ReviewerPublic' } as const; +export const SearchRequestSchema = { + properties: { + query: { + type: 'string', + maxLength: 255, + minLength: 1, + title: 'Query' + }, + limit: { + type: 'integer', + title: 'Limit' + } + }, + type: 'object', + required: [ + 'query', + 'limit' + ], + title: 'SearchRequest' +} as const; + export const SetPublicSchema = { properties: { id: { diff --git a/client/src/api/generated/sdk.gen.ts b/client/src/api/generated/sdk.gen.ts index 5782819a..2fcb8c66 100644 --- a/client/src/api/generated/sdk.gen.ts +++ b/client/src/api/generated/sdk.gen.ts @@ -2,7 +2,7 @@ import { type Client, formDataBodySerializer, type Options as Options2, type TDataShape } from './client'; import { client } from './client.gen'; -import type { CreateExerciseData, CreateExerciseErrors, CreateExerciseResponses, CreateFeedbackData, CreateFeedbackErrors, CreateFeedbackResponses, CreateSetData, CreateSetErrors, CreateSetResponses, CreateWorkoutData, CreateWorkoutErrors, CreateWorkoutExerciseData, CreateWorkoutExerciseErrors, CreateWorkoutExerciseResponses, CreateWorkoutResponses, DeleteExerciseData, DeleteExerciseErrors, DeleteExerciseResponses, DeleteSetData, DeleteSetErrors, DeleteSetResponses, DeleteWorkoutData, DeleteWorkoutErrors, DeleteWorkoutExerciseData, DeleteWorkoutExerciseErrors, DeleteWorkoutExerciseResponses, DeleteWorkoutResponses, ForgotPasswordData, ForgotPasswordErrors, ForgotPasswordResponses, GetAccessRequestsData, GetAccessRequestsErrors, GetAccessRequestsResponses, GetCurrentUserData, GetCurrentUserErrors, GetCurrentUserResponses, GetDbHealthData, GetDbHealthResponses, GetExerciseData, GetExerciseErrors, GetExerciseResponses, GetExercisesData, GetExercisesErrors, GetExercisesResponses, GetHealthData, GetHealthResponses, GetMuscleGroupsData, GetMuscleGroupsErrors, GetMuscleGroupsResponses, GetUsersData, GetUsersErrors, GetUsersResponses, GetWorkoutData, GetWorkoutErrors, GetWorkoutResponses, GetWorkoutsData, GetWorkoutsErrors, GetWorkoutsResponses, LoginData, LoginErrors, LoginResponses, LogoutData, LogoutResponses, RefreshTokenData, RefreshTokenErrors, RefreshTokenResponses, RegisterData, RegisterErrors, RegisterResponses, RequestAccessData, RequestAccessErrors, RequestAccessResponses, ResetPasswordData, ResetPasswordErrors, ResetPasswordResponses, UpdateAccessRequestStatusData, UpdateAccessRequestStatusErrors, UpdateAccessRequestStatusResponses, UpdateExerciseData, UpdateExerciseErrors, UpdateExerciseResponses, UpdateSetData, UpdateSetErrors, UpdateSetResponses, UpdateWorkoutData, UpdateWorkoutErrors, UpdateWorkoutResponses } from './types.gen'; +import type { CreateExerciseData, CreateExerciseErrors, CreateExerciseResponses, CreateFeedbackData, CreateFeedbackErrors, CreateFeedbackResponses, CreateSetData, CreateSetErrors, CreateSetResponses, CreateWorkoutData, CreateWorkoutErrors, CreateWorkoutExerciseData, CreateWorkoutExerciseErrors, CreateWorkoutExerciseResponses, CreateWorkoutResponses, DeleteExerciseData, DeleteExerciseErrors, DeleteExerciseResponses, DeleteSetData, DeleteSetErrors, DeleteSetResponses, DeleteWorkoutData, DeleteWorkoutErrors, DeleteWorkoutExerciseData, DeleteWorkoutExerciseErrors, DeleteWorkoutExerciseResponses, DeleteWorkoutResponses, ForgotPasswordData, ForgotPasswordErrors, ForgotPasswordResponses, GetAccessRequestsData, GetAccessRequestsErrors, GetAccessRequestsResponses, GetCurrentUserData, GetCurrentUserErrors, GetCurrentUserResponses, GetDbHealthData, GetDbHealthResponses, GetExerciseData, GetExerciseErrors, GetExerciseResponses, GetExercisesData, GetExercisesErrors, GetExercisesResponses, GetHealthData, GetHealthResponses, GetMuscleGroupsData, GetMuscleGroupsErrors, GetMuscleGroupsResponses, GetUsersData, GetUsersErrors, GetUsersResponses, GetWorkoutData, GetWorkoutErrors, GetWorkoutResponses, GetWorkoutsData, GetWorkoutsErrors, GetWorkoutsResponses, LoginData, LoginErrors, LoginResponses, LogoutData, LogoutResponses, RefreshTokenData, RefreshTokenErrors, RefreshTokenResponses, RegisterData, RegisterErrors, RegisterResponses, ReindexData, ReindexErrors, ReindexResponses, RequestAccessData, RequestAccessErrors, RequestAccessResponses, ResetPasswordData, ResetPasswordErrors, ResetPasswordResponses, SearchExercisesData, SearchExercisesErrors, SearchExercisesResponses, SearchMuscleGroupsData, SearchMuscleGroupsErrors, SearchMuscleGroupsResponses, UpdateAccessRequestStatusData, UpdateAccessRequestStatusErrors, UpdateAccessRequestStatusResponses, UpdateExerciseData, UpdateExerciseErrors, UpdateExerciseResponses, UpdateSetData, UpdateSetErrors, UpdateSetResponses, UpdateWorkoutData, UpdateWorkoutErrors, UpdateWorkoutResponses } from './types.gen'; export type Options = Options2 & { /** @@ -318,6 +318,63 @@ export class MuscleGroupService { } } +export class SearchService { + /** + * Reindex Endpoint + */ + public static reindex(options?: Options) { + return (options?.client ?? client).post({ + security: [{ + in: 'cookie', + name: 'access_token', + type: 'apiKey' + }], + url: '/api/search/reindex', + ...options + }); + } + + /** + * Search Muscle Groups Endpoint + */ + public static searchMuscleGroups(options: Options) { + return (options.client ?? client).post({ + responseType: 'json', + security: [{ + in: 'cookie', + name: 'access_token', + type: 'apiKey' + }], + url: '/api/search/muscle-groups', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); + } + + /** + * Search Exercises Endpoint + */ + public static searchExercises(options: Options) { + return (options.client ?? client).post({ + responseType: 'json', + security: [{ + in: 'cookie', + name: 'access_token', + type: 'apiKey' + }], + url: '/api/search/exercises', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); + } +} + export class SetService { /** * Create Set Endpoint diff --git a/client/src/api/generated/types.gen.ts b/client/src/api/generated/types.gen.ts index abaf620b..a61377ff 100644 --- a/client/src/api/generated/types.gen.ts +++ b/client/src/api/generated/types.gen.ts @@ -340,6 +340,20 @@ export type ReviewerPublic = { username: string; }; +/** + * SearchRequest + */ +export type SearchRequest = { + /** + * Query + */ + query: string; + /** + * Limit + */ + limit: number; +}; + /** * SetPublic */ @@ -1199,6 +1213,89 @@ export type GetMuscleGroupsResponses = { export type GetMuscleGroupsResponse = GetMuscleGroupsResponses[keyof GetMuscleGroupsResponses]; +export type ReindexData = { + body?: never; + path?: never; + query?: never; + url: '/api/search/reindex'; +}; + +export type ReindexErrors = { + /** + * Unauthorized + */ + 401: ErrorResponse; + /** + * Forbidden + */ + 403: ErrorResponse; +}; + +export type ReindexError = ReindexErrors[keyof ReindexErrors]; + +export type ReindexResponses = { + /** + * Successful Response + */ + 204: void; +}; + +export type ReindexResponse = ReindexResponses[keyof ReindexResponses]; + +export type SearchMuscleGroupsData = { + body: SearchRequest; + path?: never; + query?: never; + url: '/api/search/muscle-groups'; +}; + +export type SearchMuscleGroupsErrors = { + /** + * Unauthorized + */ + 401: ErrorResponse; + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type SearchMuscleGroupsError = SearchMuscleGroupsErrors[keyof SearchMuscleGroupsErrors]; + +export type SearchMuscleGroupsResponses = { + /** + * Successful Response + */ + 200: unknown; +}; + +export type SearchExercisesData = { + body: SearchRequest; + path?: never; + query?: never; + url: '/api/search/exercises'; +}; + +export type SearchExercisesErrors = { + /** + * Unauthorized + */ + 401: ErrorResponse; + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type SearchExercisesError = SearchExercisesErrors[keyof SearchExercisesErrors]; + +export type SearchExercisesResponses = { + /** + * Successful Response + */ + 200: unknown; +}; + export type CreateSetData = { body: CreateSetRequest; path: { diff --git a/client/src/api/generated/zod.gen.ts b/client/src/api/generated/zod.gen.ts index 6bce8ba9..408b8b46 100644 --- a/client/src/api/generated/zod.gen.ts +++ b/client/src/api/generated/zod.gen.ts @@ -160,6 +160,14 @@ export const zAccessRequestPublic = z.object({ updated_at: z.iso.datetime() }); +/** + * SearchRequest + */ +export const zSearchRequest = z.object({ + query: z.string().min(1).max(255), + limit: z.int() +}); + /** * SetPublic */ @@ -529,6 +537,29 @@ export const zGetMuscleGroupsData = z.object({ */ export const zGetMuscleGroupsResponse = z.array(zMuscleGroupPublic); +export const zReindexData = z.object({ + body: z.never().optional(), + path: z.never().optional(), + query: z.never().optional() +}); + +/** + * Successful Response + */ +export const zReindexResponse = z.void(); + +export const zSearchMuscleGroupsData = z.object({ + body: zSearchRequest, + path: z.never().optional(), + query: z.never().optional() +}); + +export const zSearchExercisesData = z.object({ + body: zSearchRequest, + path: z.never().optional(), + query: z.never().optional() +}); + export const zCreateSetData = z.object({ body: zCreateSetRequest, path: z.object({ diff --git a/server/app/api/endpoints/search.py b/server/app/api/endpoints/search.py new file mode 100644 index 00000000..403b98c2 --- /dev/null +++ b/server/app/api/endpoints/search.py @@ -0,0 +1,78 @@ +from typing import Annotated + +from fastapi import APIRouter, Depends, status +from meilisearch_python_sdk import AsyncClient +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.dependencies import ( + get_current_admin, + get_current_user, + get_db, + get_ms_client, +) +from app.models.schemas.errors import ErrorResponseModel +from app.models.schemas.search import SearchRequest +from app.models.schemas.user import UserPublic +from app.services.search import reindex_data, search_exercises, search_muscle_groups + +api_router = APIRouter( + prefix="/search", + tags=["Search"], + dependencies=[Depends(get_current_user)], +) + + +@api_router.post( + "/reindex", + operation_id="reindex", + status_code=status.HTTP_204_NO_CONTENT, + responses={ + status.HTTP_401_UNAUTHORIZED: ErrorResponseModel, + status.HTTP_403_FORBIDDEN: ErrorResponseModel, + }, +) +async def reindex_endpoint( + _: Annotated[UserPublic, Depends(get_current_admin)], + db: Annotated[AsyncSession, Depends(get_db)], + ms_client: Annotated[AsyncClient, Depends(get_ms_client)], +): + await reindex_data( + db=db, + ms_client=ms_client, + ) + + +@api_router.post( + "/muscle-groups", + operation_id="searchMuscleGroups", + responses={ + status.HTTP_401_UNAUTHORIZED: ErrorResponseModel, + }, +) +async def search_muscle_groups_endpoint( + req: SearchRequest, + ms_client: Annotated[AsyncClient, Depends(get_ms_client)], +): + return await search_muscle_groups( + req=req, + ms_client=ms_client, + ) + + +@api_router.post( + "/exercises", + operation_id="searchExercises", + responses={ + status.HTTP_401_UNAUTHORIZED: ErrorResponseModel, + }, +) +async def search_exercises_endpoint( + req: SearchRequest, + user: Annotated[UserPublic, Depends(get_current_user)], + ms_client: Annotated[AsyncClient, Depends(get_ms_client)], +): + return await search_exercises( + req=req, + user_id=user.id, + ms_client=ms_client, + ) diff --git a/server/app/api/router.py b/server/app/api/router.py index 23cde1bd..fbf2ab06 100644 --- a/server/app/api/router.py +++ b/server/app/api/router.py @@ -6,6 +6,7 @@ from .endpoints.feedback import api_router as feedback_router from .endpoints.health import api_router as health_router from .endpoints.muscle_group import api_router as muscle_group_router +from .endpoints.search import api_router as search_router from .endpoints.set import api_router as set_router from .endpoints.user import api_router as user_router from .endpoints.workout import api_router as workout_router @@ -18,6 +19,7 @@ api_router.include_router(feedback_router) api_router.include_router(health_router) api_router.include_router(muscle_group_router) +api_router.include_router(search_router) api_router.include_router(set_router) api_router.include_router(user_router) api_router.include_router(workout_exercise_router) diff --git a/server/app/models/enums.py b/server/app/models/enums.py index 57fc41d3..523e8963 100644 --- a/server/app/models/enums.py +++ b/server/app/models/enums.py @@ -15,3 +15,8 @@ class FeedbackType(StrEnum): class SetUnit(StrEnum): kg = "kg" lb = "lb" + + +class SearchIndex(StrEnum): + MUSCLE_GROUPS = "muscle_groups" + EXERCISES = "exercises" diff --git a/server/app/models/schemas/search.py b/server/app/models/schemas/search.py new file mode 100644 index 00000000..bf3e62c1 --- /dev/null +++ b/server/app/models/schemas/search.py @@ -0,0 +1,15 @@ +from meilisearch_python_sdk.models.search import SearchResults +from pydantic import BaseModel +from pydantic.generics import GenericModel + +from app.models.schemas.types import SearchQuery + + +class SearchRequest(BaseModel): + query: SearchQuery + limit: int + + +class SearchResponse[T: BaseModel](GenericModel): + query: str + results: SearchResults[T] diff --git a/server/app/models/schemas/types.py b/server/app/models/schemas/types.py index 226067e6..dc562bc6 100644 --- a/server/app/models/schemas/types.py +++ b/server/app/models/schemas/types.py @@ -34,3 +34,5 @@ def is_email_identifier(identifier: str) -> bool: WorkoutExerciseNotes = Annotated[str, Field(max_length=1000)] WorkoutNotes = Annotated[str, Field(max_length=1000)] + +SearchQuery = Annotated[str, Field(min_length=1, max_length=255)] diff --git a/server/app/services/search.py b/server/app/services/search.py new file mode 100644 index 00000000..eda68a20 --- /dev/null +++ b/server/app/services/search.py @@ -0,0 +1,136 @@ +import logging +from typing import cast + +from meilisearch_python_sdk import AsyncClient +from meilisearch_python_sdk.models.search import SearchResults +from meilisearch_python_sdk.models.settings import MeilisearchSettings +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.database.muscle_group import MuscleGroup +from app.models.enums import SearchIndex +from app.models.schemas.exercise import ExercisePublic +from app.models.schemas.muscle_group import MuscleGroupPublic +from app.models.schemas.search import SearchRequest +from app.services.utilities.queries import query_exercises +from app.services.utilities.serializers import ( + to_exercise_public, + to_muscle_group_public, +) + +logger = logging.getLogger(__name__) + + +async def reindex_data( + db: AsyncSession, + ms_client: AsyncClient, +): + await _index_muscle_groups(db, ms_client) + await _index_exercises(db, ms_client) + + +async def _index_muscle_groups( + db: AsyncSession, + ms_client: AsyncClient, +): + result = await db.execute(select(MuscleGroup)) + muscle_groups = result.scalars().all() + public = [to_muscle_group_public(mg) for mg in muscle_groups] + + await ms_client.delete_index_if_exists(SearchIndex.MUSCLE_GROUPS) + index = await ms_client.get_or_create_index(SearchIndex.MUSCLE_GROUPS) + await index.add_documents([mg.model_dump() for mg in public]) + + +async def _index_exercises( + db: AsyncSession, + ms_client: AsyncClient, +): + exercises = await query_exercises(db, base=False) + public = [to_exercise_public(e) for e in exercises] + + await ms_client.delete_index_if_exists(SearchIndex.EXERCISES) + + settings = MeilisearchSettings( + searchable_attributes=[ + "name", + "description", + "muscle_groups.name", + ], + displayed_attributes=[ + "id", + "user_id", + "name", + "description", + "muscle_groups.id", + "muscle_groups.name", + ], + filterable_attributes=[ + "user_id", + ], + ) + + index = await ms_client.get_or_create_index(SearchIndex.EXERCISES) + await index.update_settings(settings) + await index.add_documents( + [ + e.model_dump( + exclude={"created_at", "updated_at"}, + ) + for e in public + ], + primary_key="id", + ) + + +async def search_muscle_groups( + req: SearchRequest, + ms_client: AsyncClient, +): + logger.info(f"Searching muscle groups with query: '{req.query}'") + return await _search( + model=MuscleGroupPublic, + ms_client=ms_client, + index=SearchIndex.MUSCLE_GROUPS, + query=req.query, + limit=req.limit, + ) + + +async def search_exercises( + req: SearchRequest, + user_id: int, + ms_client: AsyncClient, +): + logger.info(f"Searching exercises with query: '{req.query}' and user_id: {user_id}") + return await _search( + model=ExercisePublic, + ms_client=ms_client, + index=SearchIndex.EXERCISES, + query=req.query, + filter=f"user_id IS NULL OR user_id = {user_id}", + limit=req.limit, + ) + + +async def _search[T]( + model: type[T], + ms_client: AsyncClient, + index: SearchIndex, + query: str, + filter: str | None = None, + limit: int = 20, +) -> SearchResults[T]: + _index = await ms_client.get_or_create_index( + index, + hits_type=model, + ) + raw = await _index.search( # type: ignore + query=query, + filter=filter, + limit=limit, + ) + logger.info(f"Search returned {raw}") + typed_hits = [model(**hit) for hit in raw.hits] # type: ignore + raw.hits = typed_hits + return cast(SearchResults[T], raw) From b0f68225e99499c7e397848b7f74f0e0f9425a26 Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Wed, 25 Mar 2026 11:34:30 -0500 Subject: [PATCH 17/27] server - rename db methods --- server/app/core/dependencies.py | 4 +- .../api/admin/test_get_access_requests.py | 6 +- server/app/tests/api/admin/test_get_users.py | 6 +- .../test_update_access_request_status.py | 22 +-- server/app/tests/api/auth/test_register.py | 28 ++-- .../app/tests/api/auth/test_request_access.py | 32 ++-- .../app/tests/api/auth/test_reset_password.py | 8 +- .../api/exercise/test_create_exercise.py | 4 +- .../api/exercise/test_delete_exercise.py | 12 +- .../tests/api/exercise/test_get_exercise.py | 12 +- .../tests/api/exercise/test_get_exercises.py | 10 +- .../api/exercise/test_update_exercise.py | 32 ++-- server/app/tests/api/exercise/utilities.py | 18 +-- server/app/tests/api/set/test_create_set.py | 50 +++---- server/app/tests/api/set/test_delete_set.py | 22 +-- server/app/tests/api/set/test_update_set.py | 22 +-- .../tests/api/user/test_get_current_user.py | 8 +- server/app/tests/api/utilities.py | 10 +- .../tests/api/workout/test_delete_workout.py | 18 +-- .../app/tests/api/workout/test_get_workout.py | 18 +-- .../tests/api/workout/test_get_workouts.py | 10 +- .../tests/api/workout/test_update_workout.py | 26 ++-- server/app/tests/api/workout/utilities.py | 8 +- .../test_create_workout_exercise.py | 42 +++--- .../test_delete_workout_exercise.py | 24 +-- .../tests/api/workout_exercise/utilities.py | 8 +- .../dependencies/test_get_current_admin.py | 12 +- .../dependencies/test_get_current_user.py | 30 ++-- .../tests/core/dependencies/test_get_db.py | 14 +- .../dependencies/test_get_db_sessionmaker.py | 19 +++ .../dependencies/test_get_sessionmaker.py | 19 --- .../core/security/test_authenticate_user.py | 16 +- .../tests/core/security/test_expire_tokens.py | 40 ++--- .../app/tests/core/security/test_get_token.py | 64 ++++---- server/app/tests/core/security/utilities.py | 6 +- server/app/tests/core/utilities.py | 4 +- server/app/tests/fixtures/client.py | 10 +- server/app/tests/fixtures/database.py | 23 +-- .../queries/test_get_owned_workout.py | 22 +-- .../queries/test_query_exercises.py | 46 +++--- .../_utilities/queries/test_query_sets.py | 32 ++-- .../queries/test_query_workout_exercises.py | 58 ++++---- .../test_get_access_request_by_id.py | 12 +- .../test_get_access_requests_with_reviewer.py | 56 +++---- ...test_get_latest_access_request_by_email.py | 12 +- .../admin/test_get_access_requests.py | 18 +-- .../tests/services/admin/test_get_users.py | 8 +- .../test_update_access_request_status.py | 44 +++--- server/app/tests/services/auth/test_login.py | 16 +- .../app/tests/services/auth/test_refresh.py | 12 +- .../app/tests/services/auth/test_register.py | 84 +++++------ .../services/auth/test_request_access.py | 44 +++--- .../auth/test_request_password_reset.py | 22 +-- .../services/auth/test_reset_password.py | 42 +++--- .../services/exercise/test_create_exercise.py | 32 ++-- .../services/exercise/test_delete_exercise.py | 30 ++-- .../services/exercise/test_get_exercise.py | 32 ++-- .../services/exercise/test_get_exercises.py | 24 +-- .../exercise/test_get_owned_exercise.py | 24 +-- .../services/exercise/test_update_exercise.py | 96 ++++++------ .../app/tests/services/exercise/utilities.py | 10 +- .../feedback/test_create_feedback_service.py | 20 ++- .../test_get_muscle_groups_by_ids.py | 18 +-- .../test_get_muscle_groups_ordered_by_name.py | 18 ++- .../tests/services/muscle_group/utilities.py | 4 +- .../app/tests/services/set/test_create_set.py | 80 +++++----- .../app/tests/services/set/test_delete_set.py | 42 +++--- .../services/set/test_get_next_set_number.py | 30 ++-- .../app/tests/services/set/test_update_set.py | 138 +++++++++--------- server/app/tests/services/set/utilities.py | 8 +- .../services/token/test_expire_tokens.py | 32 ++-- .../tests/services/token/test_get_tokens.py | 38 ++--- .../services/user/test_get_admin_users.py | 8 +- .../services/user/test_get_user_by_email.py | 12 +- .../user/test_get_user_by_identifier.py | 36 ++--- .../user/test_get_user_by_username.py | 12 +- .../test_get_users_ordered_by_username.py | 24 +-- server/app/tests/services/utilities.py | 10 +- .../services/workout/test_create_workout.py | 8 +- .../services/workout/test_delete_workout.py | 26 ++-- .../services/workout/test_get_workout.py | 24 +-- .../services/workout/test_get_workouts.py | 12 +- .../services/workout/test_query_workouts.py | 52 +++---- .../services/workout/test_update_workout.py | 74 +++++----- .../app/tests/services/workout/utilities.py | 8 +- .../test_create_workout_exercise.py | 72 ++++----- .../test_delete_workout_exercise.py | 38 ++--- ...test_get_next_workout_exercise_position.py | 28 ++-- .../services/workout_exercise/utilities.py | 8 +- 89 files changed, 1201 insertions(+), 1172 deletions(-) create mode 100644 server/app/tests/core/dependencies/test_get_db_sessionmaker.py delete mode 100644 server/app/tests/core/dependencies/test_get_sessionmaker.py diff --git a/server/app/core/dependencies.py b/server/app/core/dependencies.py index 94865396..51b45d4a 100644 --- a/server/app/core/dependencies.py +++ b/server/app/core/dependencies.py @@ -19,7 +19,7 @@ @cache -def get_sessionmaker(db_url: str, is_prod: bool): +def get_db_sessionmaker(db_url: str, is_prod: bool): engine = create_async_engine( db_url, echo=not is_prod, @@ -33,7 +33,7 @@ def get_sessionmaker(db_url: str, is_prod: bool): async def get_db( settings: Annotated[Settings, Depends(get_settings)], ) -> AsyncGenerator[AsyncSession]: - async with get_sessionmaker( + async with get_db_sessionmaker( settings.db.url, settings.is_prod_like, )() as session: diff --git a/server/app/tests/api/admin/test_get_access_requests.py b/server/app/tests/api/admin/test_get_access_requests.py index 6bcc3f30..c668cfd8 100644 --- a/server/app/tests/api/admin/test_get_access_requests.py +++ b/server/app/tests/api/admin/test_get_access_requests.py @@ -42,14 +42,14 @@ async def test_get_access_requests_not_logged_in(client: AsyncClient): # 403 async def test_get_access_requests_non_admin_user( - client: AsyncClient, session: AsyncSession, settings: Settings + client: AsyncClient, db_session: AsyncSession, settings: Settings ): - await session.execute( + await db_session.execute( update(User) .where(User.username == settings.admin.username) .values(is_admin=False) ) - await session.commit() + await db_session.commit() await login_admin(client, settings) resp = await _make_request(client) diff --git a/server/app/tests/api/admin/test_get_users.py b/server/app/tests/api/admin/test_get_users.py index 0216ec44..f37b312a 100644 --- a/server/app/tests/api/admin/test_get_users.py +++ b/server/app/tests/api/admin/test_get_users.py @@ -45,14 +45,14 @@ async def test_get_users_not_logged_in(client: AsyncClient): # 403 async def test_get_users_non_admin_user( - client: AsyncClient, session: AsyncSession, settings: Settings + client: AsyncClient, db_session: AsyncSession, settings: Settings ): - await session.execute( + await db_session.execute( update(User) .where(User.username == settings.admin.username) .values(is_admin=False) ) - await session.commit() + await db_session.commit() await login_admin(client, settings) resp = await _make_request(client) diff --git a/server/app/tests/api/admin/test_update_access_request_status.py b/server/app/tests/api/admin/test_update_access_request_status.py index f3a895a1..29cfec2d 100644 --- a/server/app/tests/api/admin/test_update_access_request_status.py +++ b/server/app/tests/api/admin/test_update_access_request_status.py @@ -36,7 +36,7 @@ async def _make_request( # 204 async def test_update_access_request_status( - client: AsyncClient, session: AsyncSession, settings: Settings + client: AsyncClient, db_session: AsyncSession, settings: Settings ): access_request = AccessRequest( email="pending@example.com", @@ -44,17 +44,17 @@ async def test_update_access_request_status( last_name="User", status=AccessRequestStatus.PENDING, ) - session.add(access_request) - await session.commit() + db_session.add(access_request) + await db_session.commit() - admin = await get_admin(session, settings) + admin = await get_admin(db_session, settings) await login_admin(client, settings) resp = await _make_request(client, access_request.id, AccessRequestStatus.APPROVED) assert resp.status_code == status.HTTP_204_NO_CONTENT - await session.refresh(access_request) + await db_session.refresh(access_request) assert access_request.status == AccessRequestStatus.APPROVED assert access_request.reviewed_at is not None assert access_request.reviewed_by == admin.id @@ -62,7 +62,7 @@ async def test_update_access_request_status( # 400 async def test_update_access_request_status_not_pending( - client: AsyncClient, session: AsyncSession, settings: Settings + client: AsyncClient, db_session: AsyncSession, settings: Settings ): access_request = AccessRequest( email="approved@example.com", @@ -70,8 +70,8 @@ async def test_update_access_request_status_not_pending( last_name="User", status=AccessRequestStatus.APPROVED, ) - session.add(access_request) - await session.commit() + db_session.add(access_request) + await db_session.commit() await login_admin(client, settings) resp = await _make_request(client, access_request.id, AccessRequestStatus.REJECTED) @@ -92,14 +92,14 @@ async def test_update_access_request_status_not_logged_in(client: AsyncClient): # 403 async def test_update_access_request_status_non_admin_user( - client: AsyncClient, session: AsyncSession, settings: Settings + client: AsyncClient, db_session: AsyncSession, settings: Settings ): - await session.execute( + await db_session.execute( update(User) .where(User.username == settings.admin.username) .values(is_admin=False) ) - await session.commit() + await db_session.commit() await login_admin(client, settings) resp = await _make_request(client, 1, AccessRequestStatus.APPROVED) diff --git a/server/app/tests/api/auth/test_register.py b/server/app/tests/api/auth/test_register.py index 594d6df5..927bbc55 100644 --- a/server/app/tests/api/auth/test_register.py +++ b/server/app/tests/api/auth/test_register.py @@ -24,26 +24,28 @@ async def _make_request(client: AsyncClient, token: str, username: str, password ) -async def _create_approved_request_with_token(session: AsyncSession) -> tuple[str, str]: +async def _create_approved_request_with_token( + db_session: AsyncSession, +) -> tuple[str, str]: access_request = AccessRequest( email="approved@example.com", first_name="Approved", last_name="User", status=AccessRequestStatus.APPROVED, ) - session.add(access_request) - await session.commit() + db_session.add(access_request) + await db_session.commit() token_str, token = create_registration_token(access_request.id) - session.add(token) - await session.commit() + db_session.add(token) + await db_session.commit() return access_request.email, token_str # 204 -async def test_register(client: AsyncClient, session: AsyncSession): - _, token_str = await _create_approved_request_with_token(session) +async def test_register(client: AsyncClient, db_session: AsyncSession): + _, token_str = await _create_approved_request_with_token(db_session) resp = await _make_request( client, @@ -71,9 +73,9 @@ async def test_register_invalid_token(client: AsyncClient): # 409 async def test_register_username_taken( - client: AsyncClient, session: AsyncSession, settings: Settings + client: AsyncClient, db_session: AsyncSession, settings: Settings ): - _, token_str = await _create_approved_request_with_token(session) + _, token_str = await _create_approved_request_with_token(db_session) resp = await _make_request( client, @@ -90,10 +92,10 @@ async def test_register_username_taken( # 409 async def test_register_username_matches_email( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, ): collision_identifier = "identifier_collision" - session.add( + db_session.add( User( email=collision_identifier, username="existing_user", @@ -103,9 +105,9 @@ async def test_register_username_matches_email( is_admin=False, ) ) - await session.commit() + await db_session.commit() - _, token_str = await _create_approved_request_with_token(session) + _, token_str = await _create_approved_request_with_token(db_session) resp = await _make_request( client, token=token_str, diff --git a/server/app/tests/api/auth/test_request_access.py b/server/app/tests/api/auth/test_request_access.py index 7edb28fd..0f81214d 100644 --- a/server/app/tests/api/auth/test_request_access.py +++ b/server/app/tests/api/auth/test_request_access.py @@ -45,7 +45,7 @@ async def test_request_access(client: AsyncClient): # 200 async def test_request_access_status_approved( - client: AsyncClient, session: AsyncSession + client: AsyncClient, db_session: AsyncSession ): approved_email = "approved@example.com" req = AccessRequest( @@ -54,8 +54,8 @@ async def test_request_access_status_approved( last_name="User", status=AccessRequestStatus.APPROVED, ) - session.add(req) - await session.commit() + db_session.add(req) + await db_session.commit() resp = await _make_request( client, email=approved_email, first_name="Test", last_name="User" @@ -68,7 +68,7 @@ async def test_request_access_status_approved( # 403 async def test_request_access_status_rejected( - client: AsyncClient, session: AsyncSession + client: AsyncClient, db_session: AsyncSession ): rejected_email = "rejected@example.com" req = AccessRequest( @@ -77,8 +77,8 @@ async def test_request_access_status_rejected( last_name="User", status=AccessRequestStatus.REJECTED, ) - session.add(req) - await session.commit() + db_session.add(req) + await db_session.commit() resp = await _make_request( client, email=rejected_email, first_name="Test", last_name="User" @@ -91,7 +91,7 @@ async def test_request_access_status_rejected( # 409 async def test_request_access_status_pending( - client: AsyncClient, session: AsyncSession + client: AsyncClient, db_session: AsyncSession ): pending_email = "pending@example.com" req = AccessRequest( @@ -100,8 +100,8 @@ async def test_request_access_status_pending( last_name="User", status=AccessRequestStatus.PENDING, ) - session.add(req) - await session.commit() + db_session.add(req) + await db_session.commit() resp = await _make_request( client, email=pending_email, first_name="Test", last_name="User" @@ -113,7 +113,9 @@ async def test_request_access_status_pending( # 409 -async def test_request_access_existing_user(client: AsyncClient, session: AsyncSession): +async def test_request_access_existing_user( + client: AsyncClient, db_session: AsyncSession +): existing_email = "existing@example.com" user = User( email=existing_email, @@ -123,8 +125,8 @@ async def test_request_access_existing_user(client: AsyncClient, session: AsyncS password_hash="hash", is_admin=False, ) - session.add(user) - await session.commit() + db_session.add(user) + await db_session.commit() resp = await _make_request( client, email=existing_email, first_name="Test", last_name="User" @@ -138,10 +140,10 @@ async def test_request_access_existing_user(client: AsyncClient, session: AsyncS # 409 async def test_request_access_email_matches_username( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, ): collision_identifier = "existing@example.com" - session.add( + db_session.add( User( email="different@example.com", username=collision_identifier, @@ -151,7 +153,7 @@ async def test_request_access_email_matches_username( is_admin=False, ) ) - await session.commit() + await db_session.commit() resp = await _make_request( client, diff --git a/server/app/tests/api/auth/test_reset_password.py b/server/app/tests/api/auth/test_reset_password.py index a6f33dff..31594d54 100644 --- a/server/app/tests/api/auth/test_reset_password.py +++ b/server/app/tests/api/auth/test_reset_password.py @@ -23,13 +23,13 @@ async def _make_request(client: AsyncClient, *, token: str, password: str): # 204 async def test_reset_password( - client: AsyncClient, session: AsyncSession, settings: Settings + client: AsyncClient, db_session: AsyncSession, settings: Settings ): - admin = await get_admin(session, settings) + admin = await get_admin(db_session, settings) token_str, token = create_password_reset_token(admin.id) - session.add(token) - await session.commit() + db_session.add(token) + await db_session.commit() resp = await _make_request(client, token=token_str, password="NewPassword123") diff --git a/server/app/tests/api/exercise/test_create_exercise.py b/server/app/tests/api/exercise/test_create_exercise.py index 83fd287e..a8abff66 100644 --- a/server/app/tests/api/exercise/test_create_exercise.py +++ b/server/app/tests/api/exercise/test_create_exercise.py @@ -30,12 +30,12 @@ async def _make_request( # 204 async def test_create_exercise( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - muscle_group_id = await get_muscle_group_id(session, name="chest") + muscle_group_id = await get_muscle_group_id(db_session, name="chest") resp = await _make_request( client, name="Overhead Press", diff --git a/server/app/tests/api/exercise/test_delete_exercise.py b/server/app/tests/api/exercise/test_delete_exercise.py index 6e88f92d..999fff7e 100644 --- a/server/app/tests/api/exercise/test_delete_exercise.py +++ b/server/app/tests/api/exercise/test_delete_exercise.py @@ -26,11 +26,11 @@ async def _make_request( # 204 async def test_delete_exercise( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - created = await create_exercise_via_api(client, session, name="Exercise") + created = await create_exercise_via_api(client, db_session, name="Exercise") resp = await _make_request(client, created.id) @@ -40,9 +40,9 @@ async def test_delete_exercise( # 401 async def test_delete_exercise_not_logged_in( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, ): - system_ex = await create_system_exercise(session, name="System Exercise") + system_ex = await create_system_exercise(db_session, name="System Exercise") resp = await _make_request(client, system_ex.id) @@ -66,11 +66,11 @@ async def test_delete_exercise_not_found( # 404 async def test_delete_exercise_not_allowed( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - system_ex = await create_system_exercise(session, name="System Exercise") + system_ex = await create_system_exercise(db_session, name="System Exercise") resp = await _make_request(client, system_ex.id) diff --git a/server/app/tests/api/exercise/test_get_exercise.py b/server/app/tests/api/exercise/test_get_exercise.py index 5c9cb1ca..22b73c5d 100644 --- a/server/app/tests/api/exercise/test_get_exercise.py +++ b/server/app/tests/api/exercise/test_get_exercise.py @@ -21,11 +21,11 @@ async def _make_request(client: AsyncClient, exercise_id: int): # 200 async def test_get_exercise( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - created = await create_exercise_via_api(client, session, name="Bench Press") + created = await create_exercise_via_api(client, db_session, name="Bench Press") resp = await _make_request(client, created.id) @@ -38,10 +38,10 @@ async def test_get_exercise( # 200 async def test_get_exercise_system_exercise( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): - system_ex = await create_system_exercise(session, name="Deadlift") + system_ex = await create_system_exercise(db_session, name="Deadlift") await login_admin(client, settings) resp = await _make_request(client, system_ex.id) @@ -56,9 +56,9 @@ async def test_get_exercise_system_exercise( # 401 async def test_get_exercise_not_logged_in( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, ): - system_ex = await create_system_exercise(session, name="Pull-up") + system_ex = await create_system_exercise(db_session, name="Pull-up") resp = await _make_request(client, system_ex.id) diff --git a/server/app/tests/api/exercise/test_get_exercises.py b/server/app/tests/api/exercise/test_get_exercises.py index 6173a22c..2fed14df 100644 --- a/server/app/tests/api/exercise/test_get_exercises.py +++ b/server/app/tests/api/exercise/test_get_exercises.py @@ -24,16 +24,16 @@ async def _make_request(client: AsyncClient): # 200 async def test_get_exercises( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - user = await create_user(session) + user = await create_user(db_session) - await create_system_exercise(session, name="System Exercise") - await create_exercise_via_api(client, session, name="User Exercise") + await create_system_exercise(db_session, name="System Exercise") + await create_exercise_via_api(client, db_session, name="User Exercise") await create_exercise( - session, + db_session, name="Another User's Exercise", user_id=user.id, ) diff --git a/server/app/tests/api/exercise/test_update_exercise.py b/server/app/tests/api/exercise/test_update_exercise.py index b1d3359d..e0ea8958 100644 --- a/server/app/tests/api/exercise/test_update_exercise.py +++ b/server/app/tests/api/exercise/test_update_exercise.py @@ -45,12 +45,12 @@ async def _make_request( # 204 async def test_update_exercise( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - created = await create_exercise_via_api(client, session, name="Old Name") - muscle_group_id = await get_muscle_group_id(session, name="chest") + created = await create_exercise_via_api(client, db_session, name="Old Name") + muscle_group_id = await get_muscle_group_id(db_session, name="chest") resp = await _make_request( client, @@ -66,9 +66,9 @@ async def test_update_exercise( # 401 async def test_update_exercise_not_logged_in( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, ): - system_ex = await create_system_exercise(session, name="Anonymous Row") + system_ex = await create_system_exercise(db_session, name="Anonymous Row") resp = await _make_request(client, system_ex.id, name="Renamed") @@ -94,11 +94,11 @@ async def test_update_exercise_not_found( # 404 async def test_update_exercise_muscle_group_not_found( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - created = await create_exercise_via_api(client, session, name="Flye") + created = await create_exercise_via_api(client, db_session, name="Flye") resp = await _make_request(client, created.id, muscle_group_ids=[99999]) @@ -110,11 +110,11 @@ async def test_update_exercise_muscle_group_not_found( # 404 async def test_update_exercise_not_allowed( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - system_ex = await create_system_exercise(session, name="System Row") + system_ex = await create_system_exercise(db_session, name="System Row") resp = await _make_request(client, system_ex.id, name="Attempted Rename") @@ -126,13 +126,13 @@ async def test_update_exercise_not_allowed( # 409 async def test_update_exercise_name_conflict( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - await create_exercise_via_api(client, session, name="Taken Name") - other = await create_exercise_via_api(client, session, name="To Rename") + await create_exercise_via_api(client, db_session, name="Taken Name") + other = await create_exercise_via_api(client, db_session, name="To Rename") resp = await _make_request(client, other.id, name="Taken Name") @@ -144,11 +144,11 @@ async def test_update_exercise_name_conflict( # 422 async def test_update_exercise_name_null( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - created = await create_exercise_via_api(client, session, name="Old Name") + created = await create_exercise_via_api(client, db_session, name="Old Name") resp = await make_http_request( client, @@ -163,11 +163,11 @@ async def test_update_exercise_name_null( # 422 async def test_update_exercise_muscle_group_ids_null( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - created = await create_exercise_via_api(client, session, name="Old Name") + created = await create_exercise_via_api(client, db_session, name="Old Name") resp = await make_http_request( client, diff --git a/server/app/tests/api/exercise/utilities.py b/server/app/tests/api/exercise/utilities.py index 7697bcbf..902a56ab 100644 --- a/server/app/tests/api/exercise/utilities.py +++ b/server/app/tests/api/exercise/utilities.py @@ -11,8 +11,8 @@ from ..utilities import HttpMethod, make_http_request -async def get_muscle_group_id(session: AsyncSession, name: str) -> int: - result = await session.execute( +async def get_muscle_group_id(db_session: AsyncSession, name: str) -> int: + result = await db_session.execute( select(MuscleGroup).where(MuscleGroup.name == name), ) muscle_group = result.scalar_one() @@ -21,7 +21,7 @@ async def get_muscle_group_id(session: AsyncSession, name: str) -> int: async def create_exercise_via_api( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, name: str, description: str | None = None, muscle_group_ids: list[int] | None = None, @@ -37,7 +37,7 @@ async def create_exercise_via_api( }, ) exercises = await query_exercises( - session, + db_session, False, Exercise.name == name, ) @@ -45,24 +45,24 @@ async def create_exercise_via_api( async def create_system_exercise( - session: AsyncSession, + db_session: AsyncSession, name: str, description: str | None = None, ) -> Exercise: return await create_exercise( - session, + db_session, name=name, description=description, ) async def create_exercise( - session: AsyncSession, + db_session: AsyncSession, name: str, description: str | None = None, user_id: int | None = None, ) -> Exercise: exercise = Exercise(user_id=user_id, name=name, description=description) - session.add(exercise) - await session.commit() + db_session.add(exercise) + await db_session.commit() return exercise diff --git a/server/app/tests/api/set/test_create_set.py b/server/app/tests/api/set/test_create_set.py index f3957b72..fa0b0ebe 100644 --- a/server/app/tests/api/set/test_create_set.py +++ b/server/app/tests/api/set/test_create_set.py @@ -48,16 +48,16 @@ async def _make_request( # 204 async def test_create_set( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - admin = await get_admin(session, settings) - workout = await create_workout(session, user_id=admin.id) - exercise = await create_exercise(session, name="Bench Press") + admin = await get_admin(db_session, settings) + workout = await create_workout(db_session, user_id=admin.id) + exercise = await create_exercise(db_session, name="Bench Press") workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, @@ -110,13 +110,13 @@ async def test_create_set_workout_not_found( # 404 async def test_create_set_workout_not_allowed( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - user = await create_user(session) + user = await create_user(db_session) - workout = await create_workout(session, user_id=user.id) + workout = await create_workout(db_session, user_id=user.id) resp = await _make_request( client, @@ -132,13 +132,13 @@ async def test_create_set_workout_not_allowed( # 404 async def test_create_set_workout_exercise_not_found( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - admin = await get_admin(session, settings) + admin = await get_admin(db_session, settings) - workout = await create_workout(session, user_id=admin.id) + workout = await create_workout(db_session, user_id=admin.id) resp = await _make_request( client, @@ -154,18 +154,18 @@ async def test_create_set_workout_exercise_not_found( # 404 async def test_create_set_workout_exercise_not_allowed( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - admin = await get_admin(session, settings) - user = await create_user(session) + admin = await get_admin(db_session, settings) + user = await create_user(db_session) - workout_1 = await create_workout(session, user_id=admin.id) - workout_2 = await create_workout(session, user_id=user.id) - exercise = await create_exercise(session, name="Squat") + workout_1 = await create_workout(db_session, user_id=admin.id) + workout_2 = await create_workout(db_session, user_id=user.id) + exercise = await create_exercise(db_session, name="Squat") workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout_2.id, exercise_id=exercise.id, position=1, @@ -185,17 +185,17 @@ async def test_create_set_workout_exercise_not_allowed( # 409 async def test_create_set_number_conflict( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, monkeypatch: MonkeyPatch, ): await login_admin(client, settings) - admin = await get_admin(session, settings) - workout = await create_workout(session, user_id=admin.id) - exercise = await create_exercise(session, name="Bench Press") + admin = await get_admin(db_session, settings) + workout = await create_workout(db_session, user_id=admin.id) + exercise = await create_exercise(db_session, name="Bench Press") workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, @@ -205,8 +205,8 @@ async def test_create_set_number_conflict( workout_exercise_id=workout_exercise.id, set_number=1, ) - session.add(existing) - await session.commit() + db_session.add(existing) + await db_session.commit() async def mock_get_next_set_number( workout_exercise_id: int, db: AsyncSession diff --git a/server/app/tests/api/set/test_delete_set.py b/server/app/tests/api/set/test_delete_set.py index 2c18fba3..f586f5a5 100644 --- a/server/app/tests/api/set/test_delete_set.py +++ b/server/app/tests/api/set/test_delete_set.py @@ -33,22 +33,22 @@ async def _make_request( # 204 async def test_delete_set( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - admin = await get_admin(session, settings) - workout = await create_workout(session, user_id=admin.id) - exercise = await create_exercise(session, name="Bench Press") + admin = await get_admin(db_session, settings) + workout = await create_workout(db_session, user_id=admin.id) + exercise = await create_exercise(db_session, name="Bench Press") workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, ) set_ = await create_set( - session, + db_session, workout_exercise_id=workout_exercise.id, set_number=1, ) @@ -99,16 +99,16 @@ async def test_delete_set_workout_not_found( # 404 async def test_delete_set_not_found( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - admin = await get_admin(session, settings) - workout = await create_workout(session, user_id=admin.id) - exercise = await create_exercise(session, name="Bench Press") + admin = await get_admin(db_session, settings) + workout = await create_workout(db_session, user_id=admin.id) + exercise = await create_exercise(db_session, name="Bench Press") workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, diff --git a/server/app/tests/api/set/test_update_set.py b/server/app/tests/api/set/test_update_set.py index a00384e3..9fd88296 100644 --- a/server/app/tests/api/set/test_update_set.py +++ b/server/app/tests/api/set/test_update_set.py @@ -43,22 +43,22 @@ async def _make_request( # 204 async def test_update_set( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - admin = await get_admin(session, settings) - workout = await create_workout(session, user_id=admin.id) - exercise = await create_exercise(session, name="Bench Press") + admin = await get_admin(db_session, settings) + workout = await create_workout(db_session, user_id=admin.id) + exercise = await create_exercise(db_session, name="Bench Press") workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, ) set_ = await create_set( - session, + db_session, workout_exercise_id=workout_exercise.id, set_number=1, ) @@ -113,16 +113,16 @@ async def test_update_set_workout_not_found( # 404 async def test_update_set_not_found( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - admin = await get_admin(session, settings) - workout = await create_workout(session, user_id=admin.id) - exercise = await create_exercise(session, name="Bench Press") + admin = await get_admin(db_session, settings) + workout = await create_workout(db_session, user_id=admin.id) + exercise = await create_exercise(db_session, name="Bench Press") workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, diff --git a/server/app/tests/api/user/test_get_current_user.py b/server/app/tests/api/user/test_get_current_user.py index f878e9fa..003d1763 100644 --- a/server/app/tests/api/user/test_get_current_user.py +++ b/server/app/tests/api/user/test_get_current_user.py @@ -53,12 +53,14 @@ async def test_get_current_user_invalid_cookie(client: AsyncClient, settings: Se # 401 async def test_get_current_user_deleted_user( - client: AsyncClient, session: AsyncSession, settings: Settings + client: AsyncClient, db_session: AsyncSession, settings: Settings ): await login_admin(client, settings) - await session.execute(delete(User).where(User.username == settings.admin.username)) - await session.commit() + await db_session.execute( + delete(User).where(User.username == settings.admin.username) + ) + await db_session.commit() resp = await _make_request(client) diff --git a/server/app/tests/api/utilities.py b/server/app/tests/api/utilities.py index 6f21b150..8e09866d 100644 --- a/server/app/tests/api/utilities.py +++ b/server/app/tests/api/utilities.py @@ -76,15 +76,15 @@ async def login_admin(client: AsyncClient, settings: Settings): ) -async def get_admin(session: AsyncSession, settings: Settings) -> User: - result = await session.execute( +async def get_admin(db_session: AsyncSession, settings: Settings) -> User: + result = await db_session.execute( select(User).where(User.username == settings.admin.username) ) return result.scalar_one() async def create_user( - session: AsyncSession, + db_session: AsyncSession, username: str = "user", password: str = "password", ) -> User: @@ -96,6 +96,6 @@ async def create_user( password_hash=PASSWORD_HASH.hash(password), is_admin=False, ) - session.add(user) - await session.commit() + db_session.add(user) + await db_session.commit() return user diff --git a/server/app/tests/api/workout/test_delete_workout.py b/server/app/tests/api/workout/test_delete_workout.py index ea81b4a2..3a11b6b1 100644 --- a/server/app/tests/api/workout/test_delete_workout.py +++ b/server/app/tests/api/workout/test_delete_workout.py @@ -29,12 +29,12 @@ async def _make_request( # 204 async def test_delete_workout( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - admin = await get_admin(session, settings) - workout = await create_workout(session, user_id=admin.id) + admin = await get_admin(db_session, settings) + workout = await create_workout(db_session, user_id=admin.id) resp = await _make_request(client, workout.id) @@ -44,11 +44,11 @@ async def test_delete_workout( # 401 async def test_delete_workout_not_logged_in( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): - admin = await get_admin(session, settings) - workout = await create_workout(session, user_id=admin.id) + admin = await get_admin(db_session, settings) + workout = await create_workout(db_session, user_id=admin.id) resp = await _make_request(client, workout.id) @@ -74,13 +74,13 @@ async def test_delete_workout_not_found( # 404 async def test_delete_workout_not_allowed( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - user = await create_user(session) + user = await create_user(db_session) - workout = await create_workout(session, user_id=user.id) + workout = await create_workout(db_session, user_id=user.id) resp = await _make_request(client, workout.id) diff --git a/server/app/tests/api/workout/test_get_workout.py b/server/app/tests/api/workout/test_get_workout.py index b9bd2c66..195e6c62 100644 --- a/server/app/tests/api/workout/test_get_workout.py +++ b/server/app/tests/api/workout/test_get_workout.py @@ -27,13 +27,13 @@ async def _make_request(client: AsyncClient, workout_id: int): # 200 async def test_get_workout( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - admin = await get_admin(session, settings) + admin = await get_admin(db_session, settings) - workout = await create_workout(session, user_id=admin.id) + workout = await create_workout(db_session, user_id=admin.id) resp = await _make_request(client, workout.id) @@ -47,11 +47,11 @@ async def test_get_workout( # 401 async def test_get_workout_not_logged_in( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): - admin = await get_admin(session, settings) - workout = await create_workout(session, user_id=admin.id) + admin = await get_admin(db_session, settings) + workout = await create_workout(db_session, user_id=admin.id) resp = await _make_request(client, workout.id) @@ -77,13 +77,13 @@ async def test_get_workout_not_found( # 404 async def test_get_workout_not_allowed( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - user = await create_user(session) + user = await create_user(db_session) - workout = await create_workout(session, user_id=user.id) + workout = await create_workout(db_session, user_id=user.id) resp = await _make_request(client, workout.id) diff --git a/server/app/tests/api/workout/test_get_workouts.py b/server/app/tests/api/workout/test_get_workouts.py index 84e5c94e..9befcfd2 100644 --- a/server/app/tests/api/workout/test_get_workouts.py +++ b/server/app/tests/api/workout/test_get_workouts.py @@ -26,15 +26,15 @@ async def _make_request(client: AsyncClient): # 200 async def test_get_workouts( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - admin = await get_admin(session, settings) - user = await create_user(session) + admin = await get_admin(db_session, settings) + user = await create_user(db_session) - admin_workout = await create_workout(session, user_id=admin.id) - user_workout = await create_workout(session, user_id=user.id) + admin_workout = await create_workout(db_session, user_id=admin.id) + user_workout = await create_workout(db_session, user_id=user.id) resp = await _make_request(client) diff --git a/server/app/tests/api/workout/test_update_workout.py b/server/app/tests/api/workout/test_update_workout.py index cac58ecb..015ccd67 100644 --- a/server/app/tests/api/workout/test_update_workout.py +++ b/server/app/tests/api/workout/test_update_workout.py @@ -44,12 +44,12 @@ async def _make_request( # 204 async def test_update_workout( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - admin = await get_admin(session, settings) - workout = await create_workout(session, user_id=admin.id) + admin = await get_admin(db_session, settings) + workout = await create_workout(db_session, user_id=admin.id) resp = await _make_request( client, @@ -65,11 +65,11 @@ async def test_update_workout( # 401 async def test_update_workout_not_logged_in( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): - admin = await get_admin(session, settings) - workout = await create_workout(session, user_id=admin.id) + admin = await get_admin(db_session, settings) + workout = await create_workout(db_session, user_id=admin.id) resp = await _make_request(client, workout.id) @@ -81,7 +81,7 @@ async def test_update_workout_not_logged_in( # 404 async def test_update_workout_not_found( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) @@ -96,13 +96,13 @@ async def test_update_workout_not_found( # 404 async def test_update_workout_not_allowed( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - user = await create_user(session) + user = await create_user(db_session) - workout = await create_workout(session, user_id=user.id) + workout = await create_workout(db_session, user_id=user.id) resp = await _make_request(client, workout.id) @@ -114,12 +114,12 @@ async def test_update_workout_not_allowed( # 422 async def test_update_workout_started_at_null( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - admin = await get_admin(session, settings) - workout = await create_workout(session, user_id=admin.id) + admin = await get_admin(db_session, settings) + workout = await create_workout(db_session, user_id=admin.id) resp = await make_http_request( client, diff --git a/server/app/tests/api/workout/utilities.py b/server/app/tests/api/workout/utilities.py index b8b387d5..0ce9c1cf 100644 --- a/server/app/tests/api/workout/utilities.py +++ b/server/app/tests/api/workout/utilities.py @@ -6,7 +6,7 @@ async def create_workout( - session: AsyncSession, + db_session: AsyncSession, user_id: int, started_at: datetime = datetime.now(), ended_at: datetime | None = None, @@ -18,7 +18,7 @@ async def create_workout( ended_at=ended_at, notes=notes, ) - session.add(workout) - await session.commit() - await session.refresh(workout) + db_session.add(workout) + await db_session.commit() + await db_session.refresh(workout) return workout diff --git a/server/app/tests/api/workout_exercise/test_create_workout_exercise.py b/server/app/tests/api/workout_exercise/test_create_workout_exercise.py index ac563166..c79f092c 100644 --- a/server/app/tests/api/workout_exercise/test_create_workout_exercise.py +++ b/server/app/tests/api/workout_exercise/test_create_workout_exercise.py @@ -42,14 +42,14 @@ async def _make_request( # 204 async def test_create_workout_exercise( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - admin = await get_admin(session, settings) - workout = await create_workout(session, user_id=admin.id) - exercise = await create_exercise(session, name="Bench Press") + admin = await get_admin(db_session, settings) + workout = await create_workout(db_session, user_id=admin.id) + exercise = await create_exercise(db_session, name="Bench Press") resp = await _make_request( client, @@ -94,13 +94,13 @@ async def test_create_workout_exercise_workout_not_found( # 404 async def test_create_workout_exercise_exercise_not_found( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - admin = await get_admin(session, settings) - workout = await create_workout(session, user_id=admin.id) + admin = await get_admin(db_session, settings) + workout = await create_workout(db_session, user_id=admin.id) resp = await _make_request( client, @@ -116,14 +116,14 @@ async def test_create_workout_exercise_exercise_not_found( # 404 async def test_create_workout_exercise_workout_not_allowed( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - user = await create_user(session) + user = await create_user(db_session) - workout = await create_workout(session, user_id=user.id) - exercise = await create_exercise(session, name="Squat") + workout = await create_workout(db_session, user_id=user.id) + exercise = await create_exercise(db_session, name="Squat") resp = await _make_request( client, @@ -139,15 +139,15 @@ async def test_create_workout_exercise_workout_not_allowed( # 404 async def test_create_workout_exercise_exercise_not_allowed( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - admin = await get_admin(session, settings) - user = await create_user(session) + admin = await get_admin(db_session, settings) + user = await create_user(db_session) - workout = await create_workout(session, user_id=admin.id) - exercise = await create_exercise(session, name="Deadlift", user_id=user.id) + workout = await create_workout(db_session, user_id=admin.id) + exercise = await create_exercise(db_session, name="Deadlift", user_id=user.id) resp = await _make_request( client, @@ -163,18 +163,18 @@ async def test_create_workout_exercise_exercise_not_allowed( # 409 async def test_create_workout_exercise_position_conflict( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, monkeypatch: MonkeyPatch, ): await login_admin(client, settings) - admin = await get_admin(session, settings) - workout = await create_workout(session, user_id=admin.id) - exercise = await create_exercise(session, name="Row") + admin = await get_admin(db_session, settings) + workout = await create_workout(db_session, user_id=admin.id) + exercise = await create_exercise(db_session, name="Row") await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, diff --git a/server/app/tests/api/workout_exercise/test_delete_workout_exercise.py b/server/app/tests/api/workout_exercise/test_delete_workout_exercise.py index 10dd710f..f8b28456 100644 --- a/server/app/tests/api/workout_exercise/test_delete_workout_exercise.py +++ b/server/app/tests/api/workout_exercise/test_delete_workout_exercise.py @@ -32,16 +32,16 @@ async def _make_request( # 204 async def test_delete_workout_exercise( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - admin = await get_admin(session, settings) - workout = await create_workout(session, user_id=admin.id) - exercise = await create_exercise(session, name="Pull-up") + admin = await get_admin(db_session, settings) + workout = await create_workout(db_session, user_id=admin.id) + exercise = await create_exercise(db_session, name="Pull-up") workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, @@ -74,7 +74,7 @@ async def test_delete_workout_exercise_not_logged_in( # 404 async def test_delete_workout_exercise_workout_not_found( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) @@ -93,13 +93,13 @@ async def test_delete_workout_exercise_workout_not_found( # 404 async def test_delete_workout_exercise_not_found( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - admin = await get_admin(session, settings) - workout = await create_workout(session, user_id=admin.id) + admin = await get_admin(db_session, settings) + workout = await create_workout(db_session, user_id=admin.id) resp = await _make_request( client, @@ -115,13 +115,13 @@ async def test_delete_workout_exercise_not_found( # 404 async def test_delete_workout_exercise_workout_not_allowed( client: AsyncClient, - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): await login_admin(client, settings) - user = await create_user(session) + user = await create_user(db_session) - workout = await create_workout(session, user_id=user.id) + workout = await create_workout(db_session, user_id=user.id) resp = await _make_request( client, diff --git a/server/app/tests/api/workout_exercise/utilities.py b/server/app/tests/api/workout_exercise/utilities.py index 90175f61..5fc9c29f 100644 --- a/server/app/tests/api/workout_exercise/utilities.py +++ b/server/app/tests/api/workout_exercise/utilities.py @@ -4,7 +4,7 @@ async def create_workout_exercise( - session: AsyncSession, + db_session: AsyncSession, workout_id: int, exercise_id: int, position: int, @@ -14,7 +14,7 @@ async def create_workout_exercise( exercise_id=exercise_id, position=position, ) - session.add(workout_exercise) - await session.commit() - await session.refresh(workout_exercise) + db_session.add(workout_exercise) + await db_session.commit() + await db_session.refresh(workout_exercise) return workout_exercise diff --git a/server/app/tests/core/dependencies/test_get_current_admin.py b/server/app/tests/core/dependencies/test_get_current_admin.py index 961f774f..acb924d9 100644 --- a/server/app/tests/core/dependencies/test_get_current_admin.py +++ b/server/app/tests/core/dependencies/test_get_current_admin.py @@ -10,8 +10,8 @@ from app.services.utilities.serializers import to_user_public -async def _get_admin(session: AsyncSession, settings: Settings) -> UserPublic: - result = await session.execute( +async def _get_admin(db_session: AsyncSession, settings: Settings) -> UserPublic: + result = await db_session.execute( select(User).where(User.username == settings.admin.username) ) admin = result.scalar_one() @@ -19,8 +19,8 @@ async def _get_admin(session: AsyncSession, settings: Settings) -> UserPublic: return adminPublic -async def test_get_current_admin(session: AsyncSession, settings: Settings): - admin = await _get_admin(session, settings) +async def test_get_current_admin(db_session: AsyncSession, settings: Settings): + admin = await _get_admin(db_session, settings) admin = await get_current_admin(user=admin) assert admin.username == settings.admin.username @@ -28,9 +28,9 @@ async def test_get_current_admin(session: AsyncSession, settings: Settings): async def test_get_current_admin_insufficient_permissions( - session: AsyncSession, settings: Settings + db_session: AsyncSession, settings: Settings ): - admin = await _get_admin(session, settings) + admin = await _get_admin(db_session, settings) admin.is_admin = False with pytest.raises(InsufficientPermissions): diff --git a/server/app/tests/core/dependencies/test_get_current_user.py b/server/app/tests/core/dependencies/test_get_current_user.py index fa9d618d..375e4360 100644 --- a/server/app/tests/core/dependencies/test_get_current_user.py +++ b/server/app/tests/core/dependencies/test_get_current_user.py @@ -17,13 +17,13 @@ def _make_token(payload: dict[str, Any], secret: str, algorithm: str) -> str: return str(token) -async def test_get_current_user(session: AsyncSession, settings: Settings): +async def test_get_current_user(db_session: AsyncSession, settings: Settings): token = _make_token( {"sub": settings.admin.username}, secret=settings.jwt.secret_key, algorithm=settings.jwt.algorithm, ) - user = await get_current_user(token=token, db=session, settings=settings) + user = await get_current_user(token=token, db=db_session, settings=settings) assert user.username == settings.admin.username assert user.email == settings.admin.email @@ -32,17 +32,19 @@ async def test_get_current_user(session: AsyncSession, settings: Settings): assert user.is_admin is True -async def test_get_current_user_missing_sub(session: AsyncSession, settings: Settings): +async def test_get_current_user_missing_sub( + db_session: AsyncSession, settings: Settings +): token = _make_token( {}, secret=settings.jwt.secret_key, algorithm=settings.jwt.algorithm ) with pytest.raises(InvalidCredentials): - await get_current_user(token=token, db=session, settings=settings) + await get_current_user(token=token, db=db_session, settings=settings) async def test_get_current_user_invalid_secret( - session: AsyncSession, settings: Settings + db_session: AsyncSession, settings: Settings ): token = _make_token( {"sub": settings.admin.username}, @@ -51,11 +53,11 @@ async def test_get_current_user_invalid_secret( ) with pytest.raises(InvalidCredentials): - await get_current_user(token=token, db=session, settings=settings) + await get_current_user(token=token, db=db_session, settings=settings) async def test_get_current_user_expired_token( - session: AsyncSession, settings: Settings + db_session: AsyncSession, settings: Settings ): past_time = int(time.time()) - 3600 token = _make_token( @@ -65,18 +67,22 @@ async def test_get_current_user_expired_token( ) with pytest.raises(InvalidCredentials): - await get_current_user(token=token, db=session, settings=settings) + await get_current_user(token=token, db=db_session, settings=settings) -async def test_get_current_user_deleted_user(session: AsyncSession, settings: Settings): +async def test_get_current_user_deleted_user( + db_session: AsyncSession, settings: Settings +): token = _make_token( {"sub": settings.admin.username}, secret=settings.jwt.secret_key, algorithm=settings.jwt.algorithm, ) - await session.execute(delete(User).where(User.username == settings.admin.username)) - await session.commit() + await db_session.execute( + delete(User).where(User.username == settings.admin.username) + ) + await db_session.commit() with pytest.raises(InvalidCredentials): - await get_current_user(token=token, db=session, settings=settings) + await get_current_user(token=token, db=db_session, settings=settings) diff --git a/server/app/tests/core/dependencies/test_get_db.py b/server/app/tests/core/dependencies/test_get_db.py index 102b1b60..984fcf33 100644 --- a/server/app/tests/core/dependencies/test_get_db.py +++ b/server/app/tests/core/dependencies/test_get_db.py @@ -9,13 +9,13 @@ async def test_get_db(anyio_backend: str, settings: Settings): _ = anyio_backend generator = get_db(settings) - session = await anext(generator) - assert isinstance(session, AsyncSession) - assert session.bind.url.drivername == "postgresql+asyncpg" # type: ignore - assert session.bind.url.username == settings.db.user # type: ignore - assert session.bind.url.host == settings.db.host # type: ignore - assert session.bind.url.port == settings.db.port # type: ignore - assert session.bind.url.database == settings.db.name # type: ignore + db_session = await anext(generator) + assert isinstance(db_session, AsyncSession) + assert db_session.bind.url.drivername == "postgresql+asyncpg" # type: ignore + assert db_session.bind.url.username == settings.db.user # type: ignore + assert db_session.bind.url.host == settings.db.host # type: ignore + assert db_session.bind.url.port == settings.db.port # type: ignore + assert db_session.bind.url.database == settings.db.name # type: ignore with pytest.raises(StopAsyncIteration): await anext(generator) diff --git a/server/app/tests/core/dependencies/test_get_db_sessionmaker.py b/server/app/tests/core/dependencies/test_get_db_sessionmaker.py new file mode 100644 index 00000000..d9b6a831 --- /dev/null +++ b/server/app/tests/core/dependencies/test_get_db_sessionmaker.py @@ -0,0 +1,19 @@ +from app.core.dependencies import get_db_sessionmaker + + +def test_get_db_sessionmaker_cached(): + db_url = "postgresql+asyncpg://user:pw@localhost:5432/db" + + db_sessionmaker_a = get_db_sessionmaker(db_url, False) + db_sessionmaker_b = get_db_sessionmaker(db_url, False) + + assert db_sessionmaker_a is db_sessionmaker_b + + +def test_get_db_sessionmaker_cache_key(): + db_url = "postgresql+asyncpg://user:pw@localhost:5432/db" + + non_prod_db_sessionmaker = get_db_sessionmaker(db_url, is_prod=False) + prod_db_sessionmaker = get_db_sessionmaker(db_url, is_prod=True) + + assert non_prod_db_sessionmaker is not prod_db_sessionmaker diff --git a/server/app/tests/core/dependencies/test_get_sessionmaker.py b/server/app/tests/core/dependencies/test_get_sessionmaker.py deleted file mode 100644 index def5b14f..00000000 --- a/server/app/tests/core/dependencies/test_get_sessionmaker.py +++ /dev/null @@ -1,19 +0,0 @@ -from app.core.dependencies import get_sessionmaker - - -def test_get_sessionmaker_cached(): - db_url = "postgresql+asyncpg://user:pw@localhost:5432/db" - - sessionmaker_a = get_sessionmaker(db_url, False) - sessionmaker_b = get_sessionmaker(db_url, False) - - assert sessionmaker_a is sessionmaker_b - - -def test_get_sessionmaker_cache_key(): - db_url = "postgresql+asyncpg://user:pw@localhost:5432/db" - - non_prod_sessionmaker = get_sessionmaker(db_url, is_prod=False) - prod_sessionmaker = get_sessionmaker(db_url, is_prod=True) - - assert non_prod_sessionmaker is not prod_sessionmaker diff --git a/server/app/tests/core/security/test_authenticate_user.py b/server/app/tests/core/security/test_authenticate_user.py index a2763c61..86c4c089 100644 --- a/server/app/tests/core/security/test_authenticate_user.py +++ b/server/app/tests/core/security/test_authenticate_user.py @@ -4,11 +4,11 @@ from app.core.security import authenticate_user -async def test_authenticate_user(session: AsyncSession, settings: Settings): +async def test_authenticate_user(db_session: AsyncSession, settings: Settings): user = await authenticate_user( identifier=settings.admin.username, password=settings.admin.password, - db=session, + db=db_session, ) assert user is not None @@ -16,36 +16,36 @@ async def test_authenticate_user(session: AsyncSession, settings: Settings): async def test_authenticate_user_with_email( - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): user = await authenticate_user( identifier=settings.admin.email, password=settings.admin.password, - db=session, + db=db_session, ) assert user is not None assert user.email == settings.admin.email -async def test_authenticate_user_not_found(session: AsyncSession): +async def test_authenticate_user_not_found(db_session: AsyncSession): user = await authenticate_user( identifier="non_existent_user", password="some_password", - db=session, + db=db_session, ) assert user is None async def test_authenticate_user_invalid_password( - session: AsyncSession, settings: Settings + db_session: AsyncSession, settings: Settings ): user = await authenticate_user( identifier=settings.admin.username, password="some_password", - db=session, + db=db_session, ) assert user is None diff --git a/server/app/tests/core/security/test_expire_tokens.py b/server/app/tests/core/security/test_expire_tokens.py index 236ed3fe..6e08c89e 100644 --- a/server/app/tests/core/security/test_expire_tokens.py +++ b/server/app/tests/core/security/test_expire_tokens.py @@ -15,7 +15,7 @@ from ..utilities import get_admin -async def _create_user(session: AsyncSession, email: str, username: str) -> User: +async def _create_user(db_session: AsyncSession, email: str, username: str) -> User: user = User( email=email, username=username, @@ -23,27 +23,29 @@ async def _create_user(session: AsyncSession, email: str, username: str) -> User last_name="User", password_hash="hash", ) - session.add(user) - await session.flush() + db_session.add(user) + await db_session.flush() return user async def test_expire_existing_registration_tokens( - session: AsyncSession, + db_session: AsyncSession, ): - target_request = await create_access_request(session, "wrapper-target@example.com") - other_request = await create_access_request(session, "wrapper-other@example.com") + target_request = await create_access_request( + db_session, "wrapper-target@example.com" + ) + other_request = await create_access_request(db_session, "wrapper-other@example.com") _, target_token = create_registration_token(target_request.id) _, other_token = create_registration_token(other_request.id) - session.add_all([target_token, other_token]) - await session.commit() + db_session.add_all([target_token, other_token]) + await db_session.commit() - await expire_existing_registration_tokens(target_request.id, session) + await expire_existing_registration_tokens(target_request.id, db_session) - await session.refresh(target_token) - await session.refresh(other_token) + await db_session.refresh(target_token) + await db_session.refresh(other_token) now = datetime.now(UTC) assert target_token.expires_at <= now @@ -51,12 +53,12 @@ async def test_expire_existing_registration_tokens( async def test_expire_existing_password_reset_tokens( - session: AsyncSession, + db_session: AsyncSession, settings: Settings, ): - admin = await get_admin(session, settings) + admin = await get_admin(db_session, settings) other_user = await _create_user( - session, + db_session, email="other-user@example.com", username="otheruser", ) @@ -64,13 +66,13 @@ async def test_expire_existing_password_reset_tokens( _, target_token = create_password_reset_token(admin.id) _, other_token = create_password_reset_token(other_user.id) - session.add_all([target_token, other_token]) - await session.commit() + db_session.add_all([target_token, other_token]) + await db_session.commit() - await expire_existing_password_reset_tokens(admin.id, session) + await expire_existing_password_reset_tokens(admin.id, db_session) - await session.refresh(target_token) - await session.refresh(other_token) + await db_session.refresh(target_token) + await db_session.refresh(other_token) now = datetime.now(UTC) assert target_token.expires_at <= now diff --git a/server/app/tests/core/security/test_get_token.py b/server/app/tests/core/security/test_get_token.py index 840c340f..79bdb38e 100644 --- a/server/app/tests/core/security/test_get_token.py +++ b/server/app/tests/core/security/test_get_token.py @@ -20,17 +20,17 @@ # wrappers are tested separately -async def test_get_token_registration(session: AsyncSession): - access_request = await create_access_request(session, "token-user@example.com") +async def test_get_token_registration(db_session: AsyncSession): + access_request = await create_access_request(db_session, "token-user@example.com") token_str, _token = create_registration_token(access_request.id) - session.add(_token) - await session.commit() + db_session.add(_token) + await db_session.commit() token = await _get_token( token_str, model=RegistrationToken, load_option=RegistrationToken.access_request, - db=session, + db=db_session, ) assert token is not None @@ -39,82 +39,82 @@ async def test_get_token_registration(session: AsyncSession): async def test_get_token_registration_invalid_token( - session: AsyncSession, + db_session: AsyncSession, ): token = await _get_token( "invalid-token", model=RegistrationToken, load_option=RegistrationToken.access_request, - db=session, + db=db_session, ) assert token is None async def test_get_token_registration_used_token( - session: AsyncSession, + db_session: AsyncSession, ): - access_request = await create_access_request(session, "used@example.com") + access_request = await create_access_request(db_session, "used@example.com") token_str, _token = create_registration_token(access_request.id) _token.used_at = datetime.now(UTC) - session.add(_token) - await session.commit() + db_session.add(_token) + await db_session.commit() token = await _get_token( token_str, model=RegistrationToken, load_option=RegistrationToken.access_request, - db=session, + db=db_session, ) assert token is None async def test_get_token_registration_expired_token( - session: AsyncSession, + db_session: AsyncSession, ): - access_request = await create_access_request(session, "expired@example.com") + access_request = await create_access_request(db_session, "expired@example.com") token_str, _token = create_registration_token(access_request.id) _token.expires_at = datetime.now(UTC) - timedelta(minutes=1) - session.add(_token) - await session.commit() + db_session.add(_token) + await db_session.commit() token = await _get_token( token_str, model=RegistrationToken, load_option=RegistrationToken.access_request, - db=session, + db=db_session, ) assert token is None async def test_get_token_registration_invalid_hash( - session: AsyncSession, + db_session: AsyncSession, ): - access_request = await create_access_request(session, "invalid-hash@example.com") + access_request = await create_access_request(db_session, "invalid-hash@example.com") token_str, _token = create_registration_token(access_request.id) _token.token_hash = PASSWORD_HASH.hash(token_str + "tampered") - session.add(_token) - await session.commit() + db_session.add(_token) + await db_session.commit() token = await _get_token( token_str, model=RegistrationToken, load_option=RegistrationToken.access_request, - db=session, + db=db_session, ) assert token is None -async def test_get_registration_token(session: AsyncSession): - access_request = await create_access_request(session, "registered@example.com") +async def test_get_registration_token(db_session: AsyncSession): + access_request = await create_access_request(db_session, "registered@example.com") token_str, _token = create_registration_token(access_request.id) - session.add(_token) - await session.commit() + db_session.add(_token) + await db_session.commit() token = await _get_token( token_str, model=RegistrationToken, load_option=RegistrationToken.access_request, - db=session, + db=db_session, ) assert token is not None @@ -122,17 +122,17 @@ async def test_get_registration_token(session: AsyncSession): assert token.access_request.id == access_request.id -async def test_get_password_reset_token(session: AsyncSession, settings: Settings): - admin = await get_admin(session, settings) +async def test_get_password_reset_token(db_session: AsyncSession, settings: Settings): + admin = await get_admin(db_session, settings) token_str, _token = create_password_reset_token(admin.id) - session.add(_token) - await session.commit() + db_session.add(_token) + await db_session.commit() token = await _get_token( token_str, model=type(_token), load_option=PasswordResetToken.user, - db=session, + db=db_session, ) assert token is not None diff --git a/server/app/tests/core/security/utilities.py b/server/app/tests/core/security/utilities.py index a171ca43..c47c1146 100644 --- a/server/app/tests/core/security/utilities.py +++ b/server/app/tests/core/security/utilities.py @@ -3,13 +3,13 @@ from app.models.database.access_request import AccessRequest, AccessRequestStatus -async def create_access_request(session: AsyncSession, email: str) -> AccessRequest: +async def create_access_request(db_session: AsyncSession, email: str) -> AccessRequest: access_request = AccessRequest( email=email, first_name="Test", last_name="User", status=AccessRequestStatus.APPROVED, ) - session.add(access_request) - await session.flush() + db_session.add(access_request) + await db_session.flush() return access_request diff --git a/server/app/tests/core/utilities.py b/server/app/tests/core/utilities.py index 5314c363..89cf92dd 100644 --- a/server/app/tests/core/utilities.py +++ b/server/app/tests/core/utilities.py @@ -5,8 +5,8 @@ from app.models.database.user import User -async def get_admin(session: AsyncSession, settings: Settings) -> User: - result = await session.execute( +async def get_admin(db_session: AsyncSession, settings: Settings) -> User: + result = await db_session.execute( select(User).where(User.username == settings.admin.username) ) return result.scalar_one() diff --git a/server/app/tests/fixtures/client.py b/server/app/tests/fixtures/client.py index 630bbc80..3ab15588 100644 --- a/server/app/tests/fixtures/client.py +++ b/server/app/tests/fixtures/client.py @@ -30,8 +30,8 @@ def fastapi_app(settings: Settings) -> FastAPI: async def client( fastapi_app: FastAPI, settings: Settings, - connection: AsyncConnection, - transaction: AsyncTransaction, + db_connection: AsyncConnection, + db_transaction: AsyncTransaction, mock_email_svc: EmailService, mock_github_svc: GitHubService, ) -> AsyncGenerator[AsyncClient]: @@ -42,7 +42,7 @@ async def override_get_settings() -> Settings: async def override_get_db() -> AsyncGenerator[AsyncSession]: async_session = AsyncSession( - bind=connection, + bind=db_connection, join_transaction_mode="create_savepoint", expire_on_commit=False, ) @@ -64,5 +64,5 @@ async def override_get_db() -> AsyncGenerator[AsyncSession]: del fastapi_app.dependency_overrides[get_email_service] del fastapi_app.dependency_overrides[get_github_service] - if transaction.is_active: - await transaction.rollback() + if db_transaction.is_active: + await db_transaction.rollback() diff --git a/server/app/tests/fixtures/database.py b/server/app/tests/fixtures/database.py index ef2fa972..8a469e29 100644 --- a/server/app/tests/fixtures/database.py +++ b/server/app/tests/fixtures/database.py @@ -49,7 +49,7 @@ def upgrade(rev: str, _: EnvironmentContext): @pytest.fixture(scope="session") -async def engine(anyio_backend: str) -> AsyncGenerator[AsyncEngine]: +async def db_engine(anyio_backend: str) -> AsyncGenerator[AsyncEngine]: _ = anyio_backend with PostgresContainer(image="postgres:18", driver="asyncpg") as postgres: url = postgres.get_connection_url() @@ -63,29 +63,30 @@ async def engine(anyio_backend: str) -> AsyncGenerator[AsyncEngine]: @pytest.fixture(scope="session") -async def connection(engine: AsyncEngine) -> AsyncGenerator[AsyncConnection]: - async with engine.connect() as connection: +async def db_connection(db_engine: AsyncEngine) -> AsyncGenerator[AsyncConnection]: + async with db_engine.connect() as connection: yield connection @pytest.fixture() -async def transaction( - connection: AsyncConnection, +async def db_transaction( + db_connection: AsyncConnection, ) -> AsyncGenerator[AsyncTransaction]: - async with connection.begin() as transaction: + async with db_connection.begin() as transaction: yield transaction @pytest.fixture() -async def session( - connection: AsyncConnection, transaction: AsyncTransaction +async def db_session( + db_connection: AsyncConnection, + db_transaction: AsyncTransaction, ) -> AsyncGenerator[AsyncSession]: async_session = AsyncSession( - bind=connection, + bind=db_connection, join_transaction_mode="create_savepoint", expire_on_commit=False, ) yield async_session - if transaction.is_active: - await transaction.rollback() + if db_transaction.is_active: + await db_transaction.rollback() diff --git a/server/app/tests/services/_utilities/queries/test_get_owned_workout.py b/server/app/tests/services/_utilities/queries/test_get_owned_workout.py index 3c562de3..67d80449 100644 --- a/server/app/tests/services/_utilities/queries/test_get_owned_workout.py +++ b/server/app/tests/services/_utilities/queries/test_get_owned_workout.py @@ -9,30 +9,30 @@ async def test_get_owned_workouts( - session: AsyncSession, + db_session: AsyncSession, ): - user = await create_user(session) - workout = await create_workout(session, user.id) + user = await create_user(db_session) + workout = await create_workout(db_session, user.id) - result = await get_owned_workout(workout.id, user.id, session) + result = await get_owned_workout(workout.id, user.id, db_session) assert result == workout async def test_get_owned_workout_not_found( - session: AsyncSession, + db_session: AsyncSession, ): with pytest.raises(WorkoutNotFound): - await get_owned_workout(999, 1, session) + await get_owned_workout(999, 1, db_session) async def test_get_owned_workout_not_owned( - session: AsyncSession, + db_session: AsyncSession, ): - user_1 = await create_user(session, "user_1") - user_2 = await create_user(session, "user_2") + user_1 = await create_user(db_session, "user_1") + user_2 = await create_user(db_session, "user_2") - workout = await create_workout(session, user_1.id) + workout = await create_workout(db_session, user_1.id) with pytest.raises(WorkoutNotFound): - await get_owned_workout(workout.id, user_2.id, session) + await get_owned_workout(workout.id, user_2.id, db_session) diff --git a/server/app/tests/services/_utilities/queries/test_query_exercises.py b/server/app/tests/services/_utilities/queries/test_query_exercises.py index 02e0d0b3..9970850d 100644 --- a/server/app/tests/services/_utilities/queries/test_query_exercises.py +++ b/server/app/tests/services/_utilities/queries/test_query_exercises.py @@ -11,10 +11,10 @@ async def test_query_exercises_base( - session: AsyncSession, + db_session: AsyncSession, ): - exercise = await create_exercise(session, "Bench") - result = await query_exercises(session, True) + exercise = await create_exercise(db_session, "Bench") + result = await query_exercises(db_session, True) assert len(result) == 1 assert exercise in result @@ -23,10 +23,10 @@ async def test_query_exercises_base( async def test_query_exercises_public( - session: AsyncSession, + db_session: AsyncSession, ): - exercise = await create_exercise(session, "Bench") - result = await query_exercises(session, False) + exercise = await create_exercise(db_session, "Bench") + result = await query_exercises(db_session, False) assert len(result) == 1 assert exercise in result @@ -34,26 +34,26 @@ async def test_query_exercises_public( async def test_query_exercises_no_where_clause( - session: AsyncSession, + db_session: AsyncSession, ): - user_1 = await create_user(session, username="user_1") - user_2 = await create_user(session, username="user_2") + user_1 = await create_user(db_session, username="user_1") + user_2 = await create_user(db_session, username="user_2") - mg_id = await get_muscle_group_id(session, name="chest") + mg_id = await get_muscle_group_id(db_session, name="chest") await create_exercise( - session, + db_session, name="Bench", user_id=user_1.id, muscle_group_ids=[mg_id], ) await create_exercise( - session, + db_session, name="Squat", user_id=user_2.id, ) - result = await query_exercises(session, False) + result = await query_exercises(db_session, False) names = [e.name for e in result] assert len(result) == 2 @@ -67,22 +67,22 @@ async def test_query_exercises_no_where_clause( async def test_query_exercises_with_where_clause( - session: AsyncSession, + db_session: AsyncSession, ): - user = await create_user(session) + user = await create_user(db_session) exercise = await create_exercise( - session, + db_session, name="Deadlift", user_id=user.id, ) await create_exercise( - session, + db_session, name="Curl", user_id=user.id, ) result = await query_exercises( - session, + db_session, True, Exercise.id == exercise.id, ) @@ -91,22 +91,22 @@ async def test_query_exercises_with_where_clause( assert result[0].id == exercise.id -async def test_query_exercises_ordering(session: AsyncSession): - user = await create_user(session) +async def test_query_exercises_ordering(db_session: AsyncSession): + user = await create_user(db_session) await create_exercise( - session, + db_session, name="Z Row", user_id=user.id, ) await create_exercise( - session, + db_session, name="A Row", user_id=user.id, ) result = await query_exercises( - session, + db_session, False, Exercise.user_id == user.id, ) diff --git a/server/app/tests/services/_utilities/queries/test_query_sets.py b/server/app/tests/services/_utilities/queries/test_query_sets.py index 89cd0e09..85e73d22 100644 --- a/server/app/tests/services/_utilities/queries/test_query_sets.py +++ b/server/app/tests/services/_utilities/queries/test_query_sets.py @@ -11,30 +11,30 @@ async def test_query_sets_no_where_clause( - session: AsyncSession, + db_session: AsyncSession, ): - user = await create_user(session) - exercise = await create_exercise(session, "Bench") - workout = await create_workout(session, user_id=user.id, notes="Workout 1") + user = await create_user(db_session) + exercise = await create_exercise(db_session, "Bench") + workout = await create_workout(db_session, user_id=user.id, notes="Workout 1") workout_exercise = await create_workout_exercise( - session=session, + db_session=db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, ) set_1 = await create_set( - session=session, + db_session=db_session, workout_exercise_id=workout_exercise.id, set_number=1, ) set_2 = await create_set( - session=session, + db_session=db_session, workout_exercise_id=workout_exercise.id, set_number=2, ) - sets = await query_sets(session) + sets = await query_sets(db_session) assert len(sets) == 2 @@ -46,30 +46,30 @@ async def test_query_sets_no_where_clause( async def test_query_sets_with_where_clause( - session: AsyncSession, + db_session: AsyncSession, ): - user = await create_user(session) - exercise = await create_exercise(session, "Bench") - workout = await create_workout(session, user_id=user.id, notes="Workout 1") + user = await create_user(db_session) + exercise = await create_exercise(db_session, "Bench") + workout = await create_workout(db_session, user_id=user.id, notes="Workout 1") workout_exercise = await create_workout_exercise( - session=session, + db_session=db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, ) set = await create_set( - session=session, + db_session=db_session, workout_exercise_id=workout_exercise.id, set_number=1, ) await create_set( - session=session, + db_session=db_session, workout_exercise_id=workout_exercise.id, set_number=2, ) - sets = await query_sets(session, Set.id == set.id) + sets = await query_sets(db_session, Set.id == set.id) assert len(sets) == 1 assert sets[0] == set diff --git a/server/app/tests/services/_utilities/queries/test_query_workout_exercises.py b/server/app/tests/services/_utilities/queries/test_query_workout_exercises.py index f188a80c..c7b2278a 100644 --- a/server/app/tests/services/_utilities/queries/test_query_workout_exercises.py +++ b/server/app/tests/services/_utilities/queries/test_query_workout_exercises.py @@ -11,24 +11,24 @@ async def test_query_workout_exercises_no_where_clause( - session: AsyncSession, + db_session: AsyncSession, ): - user_1 = await create_user(session, username="user_1") - user_2 = await create_user(session, username="user_2") - exercise_1 = await create_exercise(session, "Bench") - exercise_2 = await create_exercise(session, "Squat") - workout_1 = await create_workout(session, user_id=user_1.id, notes="Workout 1") - workout_2 = await create_workout(session, user_id=user_2.id, notes="Workout 2") + user_1 = await create_user(db_session, username="user_1") + user_2 = await create_user(db_session, username="user_2") + exercise_1 = await create_exercise(db_session, "Bench") + exercise_2 = await create_exercise(db_session, "Squat") + workout_1 = await create_workout(db_session, user_id=user_1.id, notes="Workout 1") + workout_2 = await create_workout(db_session, user_id=user_2.id, notes="Workout 2") workout_exercise_1 = await create_workout_exercise( - session, + db_session, workout_id=workout_1.id, exercise_id=exercise_1.id, position=1, notes="Workout 1 Exercise", ) workout_exercise_2 = await create_workout_exercise( - session, + db_session, workout_id=workout_2.id, exercise_id=exercise_2.id, position=2, @@ -36,17 +36,17 @@ async def test_query_workout_exercises_no_where_clause( ) set_1 = await create_set( - session=session, + db_session=db_session, workout_exercise_id=workout_exercise_1.id, set_number=1, ) set_2 = await create_set( - session=session, + db_session=db_session, workout_exercise_id=workout_exercise_2.id, set_number=2, ) - workout_exercises = await query_workout_exercises(session) + workout_exercises = await query_workout_exercises(db_session) assert len(workout_exercises) == 2 @@ -64,24 +64,24 @@ async def test_query_workout_exercises_no_where_clause( async def test_query_workout_exercises_with_where_clause( - session: AsyncSession, + db_session: AsyncSession, ): - user_1 = await create_user(session, username="user_1") - user_2 = await create_user(session, username="user_2") - exercise_1 = await create_exercise(session, "Bench") - exercise_2 = await create_exercise(session, "Squat") - workout_1 = await create_workout(session, user_id=user_1.id, notes="Workout 1") - workout_2 = await create_workout(session, user_id=user_2.id, notes="Workout 2") + user_1 = await create_user(db_session, username="user_1") + user_2 = await create_user(db_session, username="user_2") + exercise_1 = await create_exercise(db_session, "Bench") + exercise_2 = await create_exercise(db_session, "Squat") + workout_1 = await create_workout(db_session, user_id=user_1.id, notes="Workout 1") + workout_2 = await create_workout(db_session, user_id=user_2.id, notes="Workout 2") workout_exercise_1 = await create_workout_exercise( - session, + db_session, workout_id=workout_1.id, exercise_id=exercise_1.id, position=1, notes="Workout 1 Exercise", ) await create_workout_exercise( - session, + db_session, workout_id=workout_2.id, exercise_id=exercise_2.id, position=2, @@ -89,7 +89,7 @@ async def test_query_workout_exercises_with_where_clause( ) workout_exercises = await query_workout_exercises( - session, + db_session, WorkoutExercise.workout_id == workout_1.id, ) @@ -98,26 +98,26 @@ async def test_query_workout_exercises_with_where_clause( async def test_query_workout_exercises_ordering( - session: AsyncSession, + db_session: AsyncSession, ): - user = await create_user(session) - exercise = await create_exercise(session, "Bench") - workout = await create_workout(session, user_id=user.id) + user = await create_user(db_session) + exercise = await create_exercise(db_session, "Bench") + workout = await create_workout(db_session, user_id=user.id) await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=2, ) await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, ) - result = await query_workout_exercises(session) + result = await query_workout_exercises(db_session) positions = [we.position for we in result] assert positions == [1, 2] diff --git a/server/app/tests/services/access_request/test_get_access_request_by_id.py b/server/app/tests/services/access_request/test_get_access_request_by_id.py index d2eec4f1..78ad7144 100644 --- a/server/app/tests/services/access_request/test_get_access_request_by_id.py +++ b/server/app/tests/services/access_request/test_get_access_request_by_id.py @@ -4,18 +4,18 @@ from app.tests.core.security.utilities import create_access_request -async def test_get_access_request_by_id(session: AsyncSession): - created = await create_access_request(session, "by-id@example.com") - await session.commit() +async def test_get_access_request_by_id(db_session: AsyncSession): + created = await create_access_request(db_session, "by-id@example.com") + await db_session.commit() - result = await get_access_request_by_id(created.id, session) + result = await get_access_request_by_id(created.id, db_session) assert result is not None assert result.id == created.id assert result.email == "by-id@example.com" -async def test_get_access_request_by_id_not_found(session: AsyncSession): - result = await get_access_request_by_id(999999, session) +async def test_get_access_request_by_id_not_found(db_session: AsyncSession): + result = await get_access_request_by_id(999999, db_session) assert result is None diff --git a/server/app/tests/services/access_request/test_get_access_requests_with_reviewer.py b/server/app/tests/services/access_request/test_get_access_requests_with_reviewer.py index ec4058fc..594e9096 100644 --- a/server/app/tests/services/access_request/test_get_access_requests_with_reviewer.py +++ b/server/app/tests/services/access_request/test_get_access_requests_with_reviewer.py @@ -9,17 +9,17 @@ from app.services.access_request import get_access_requests_with_reviewer -async def test_get_access_requests_with_reviewer(session: AsyncSession): +async def test_get_access_requests_with_reviewer(db_session: AsyncSession): access_request = AccessRequest( email="shape@example.com", first_name="Shape", last_name="Test", status=AccessRequestStatus.PENDING, ) - session.add(access_request) - await session.commit() + db_session.add(access_request) + await db_session.commit() - result = await get_access_requests_with_reviewer(session) + result = await get_access_requests_with_reviewer(db_session) assert isinstance(result[0], AccessRequest) assert result[0].email == "shape@example.com" @@ -30,7 +30,9 @@ async def test_get_access_requests_with_reviewer(session: AsyncSession): assert result[0].reviewed_at is None -async def test_get_access_requests_with_reviewer_status_ordering(session: AsyncSession): +async def test_get_access_requests_with_reviewer_status_ordering( + db_session: AsyncSession, +): pending = AccessRequest( email="pending@example.com", first_name="Pending", @@ -50,10 +52,10 @@ async def test_get_access_requests_with_reviewer_status_ordering(session: AsyncS status=AccessRequestStatus.REJECTED, ) - session.add_all([approved, rejected, pending]) - await session.commit() + db_session.add_all([approved, rejected, pending]) + await db_session.commit() - result = await get_access_requests_with_reviewer(session) + result = await get_access_requests_with_reviewer(db_session) statuses = [item.status for item in result] assert statuses == [ @@ -64,7 +66,7 @@ async def test_get_access_requests_with_reviewer_status_ordering(session: AsyncS async def test_get_access_requests_with_reviewer_updated_at_ordering( - session: AsyncSession, + db_session: AsyncSession, ): now = datetime.now(UTC) ar1 = AccessRequest( @@ -82,17 +84,17 @@ async def test_get_access_requests_with_reviewer_updated_at_ordering( updated_at=now + timedelta(seconds=1), ) - session.add_all([ar1, ar2]) - await session.commit() + db_session.add_all([ar1, ar2]) + await db_session.commit() - result = await get_access_requests_with_reviewer(session) + result = await get_access_requests_with_reviewer(db_session) assert result[0].id == ar2.id assert result[1].id == ar1.id assert result[0].updated_at > result[1].updated_at -async def test_get_access_requests_with_reviewer_id_ordering(session: AsyncSession): +async def test_get_access_requests_with_reviewer_id_ordering(db_session: AsyncSession): now = datetime.now(UTC) ar1 = AccessRequest( email="user1@example.com", @@ -109,18 +111,18 @@ async def test_get_access_requests_with_reviewer_id_ordering(session: AsyncSessi updated_at=now, ) - session.add_all([ar1, ar2]) - await session.commit() + db_session.add_all([ar1, ar2]) + await db_session.commit() - result = await get_access_requests_with_reviewer(session) + result = await get_access_requests_with_reviewer(db_session) assert result[0].id == ar2.id assert result[1].id == ar1.id -async def test_get_access_requests_with_reviewer_reviewer(session: AsyncSession): +async def test_get_access_requests_with_reviewer_reviewer(db_session: AsyncSession): reviewer = ( - await session.execute(select(User).where(User.username == "admin")) + await db_session.execute(select(User).where(User.username == "admin")) ).scalar_one() reviewed = AccessRequest( @@ -131,10 +133,10 @@ async def test_get_access_requests_with_reviewer_reviewer(session: AsyncSession) reviewed_by=reviewer.id, reviewed_at=datetime.now(UTC), ) - session.add(reviewed) - await session.commit() + db_session.add(reviewed) + await db_session.commit() - result = await get_access_requests_with_reviewer(session) + result = await get_access_requests_with_reviewer(db_session) assert isinstance(result[0].reviewer, User) assert result[0].reviewer.id == reviewer.id @@ -142,10 +144,14 @@ async def test_get_access_requests_with_reviewer_reviewer(session: AsyncSession) assert result[0].reviewed_at is not None -async def test_get_access_requests_with_reviewer_read_only(session: AsyncSession): - before_count = await session.scalar(select(func.count()).select_from(AccessRequest)) +async def test_get_access_requests_with_reviewer_read_only(db_session: AsyncSession): + before_count = await db_session.scalar( + select(func.count()).select_from(AccessRequest) + ) - _ = await get_access_requests_with_reviewer(session) + _ = await get_access_requests_with_reviewer(db_session) - after_count = await session.scalar(select(func.count()).select_from(AccessRequest)) + after_count = await db_session.scalar( + select(func.count()).select_from(AccessRequest) + ) assert before_count == after_count diff --git a/server/app/tests/services/access_request/test_get_latest_access_request_by_email.py b/server/app/tests/services/access_request/test_get_latest_access_request_by_email.py index db42148f..7cc0f250 100644 --- a/server/app/tests/services/access_request/test_get_latest_access_request_by_email.py +++ b/server/app/tests/services/access_request/test_get_latest_access_request_by_email.py @@ -7,7 +7,7 @@ from app.services.access_request import get_latest_access_request_by_email -async def test_get_latest_access_request_by_email(session: AsyncSession): +async def test_get_latest_access_request_by_email(db_session: AsyncSession): now = datetime.now(UTC) older = AccessRequest( email="latest@example.com", @@ -24,17 +24,17 @@ async def test_get_latest_access_request_by_email(session: AsyncSession): created_at=now + timedelta(seconds=1), ) - session.add_all([older, newer]) - await session.commit() + db_session.add_all([older, newer]) + await db_session.commit() - result = await get_latest_access_request_by_email("latest@example.com", session) + result = await get_latest_access_request_by_email("latest@example.com", db_session) assert result is not None assert result.id == newer.id assert result.first_name == "Newer" -async def test_get_latest_access_request_by_email_not_found(session: AsyncSession): - result = await get_latest_access_request_by_email("missing@example.com", session) +async def test_get_latest_access_request_by_email_not_found(db_session: AsyncSession): + result = await get_latest_access_request_by_email("missing@example.com", db_session) assert result is None diff --git a/server/app/tests/services/admin/test_get_access_requests.py b/server/app/tests/services/admin/test_get_access_requests.py index 960357ee..103e7e01 100644 --- a/server/app/tests/services/admin/test_get_access_requests.py +++ b/server/app/tests/services/admin/test_get_access_requests.py @@ -11,7 +11,7 @@ async def test_get_access_requests( - session: AsyncSession, + db_session: AsyncSession, ): access_request = AccessRequest( email="shape@example.com", @@ -19,10 +19,10 @@ async def test_get_access_requests( last_name="Test", status=AccessRequestStatus.PENDING, ) - session.add(access_request) - await session.commit() + db_session.add(access_request) + await db_session.commit() - result = await get_access_requests(session) + result = await get_access_requests(db_session) assert isinstance(result[0], AccessRequestPublic) assert result[0].email == "shape@example.com" @@ -33,9 +33,9 @@ async def test_get_access_requests( assert result[0].reviewed_at is None -async def test_get_access_requests_reviewer(session: AsyncSession): +async def test_get_access_requests_reviewer(db_session: AsyncSession): reviewer = ( - await session.execute(select(User).where(User.username == "admin")) + await db_session.execute(select(User).where(User.username == "admin")) ).scalar_one() reviewed = AccessRequest( @@ -46,10 +46,10 @@ async def test_get_access_requests_reviewer(session: AsyncSession): reviewed_by=reviewer.id, reviewed_at=datetime.now(UTC), ) - session.add(reviewed) - await session.commit() + db_session.add(reviewed) + await db_session.commit() - result = await get_access_requests(session) + result = await get_access_requests(db_session) assert isinstance(result[0].reviewer, ReviewerPublic) assert result[0].reviewer.id == reviewer.id diff --git a/server/app/tests/services/admin/test_get_users.py b/server/app/tests/services/admin/test_get_users.py index 4edec9da..0757156a 100644 --- a/server/app/tests/services/admin/test_get_users.py +++ b/server/app/tests/services/admin/test_get_users.py @@ -5,7 +5,7 @@ from app.services.admin import get_users -async def test_get_users(session: AsyncSession): +async def test_get_users(db_session: AsyncSession): user = User( email="shape-user@example.com", username="shape_user", @@ -13,10 +13,10 @@ async def test_get_users(session: AsyncSession): last_name="User", password_hash="hash", ) - session.add(user) - await session.commit() + db_session.add(user) + await db_session.commit() - result = await get_users(session) + result = await get_users(db_session) item = next(entry for entry in result if entry.id == user.id) assert isinstance(item, UserPublic) diff --git a/server/app/tests/services/admin/test_update_access_request_status.py b/server/app/tests/services/admin/test_update_access_request_status.py index 0ef21b0f..6a90e7c2 100644 --- a/server/app/tests/services/admin/test_update_access_request_status.py +++ b/server/app/tests/services/admin/test_update_access_request_status.py @@ -17,7 +17,7 @@ async def test_update_access_request_status_approved( - session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings + db_session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings ): access_request = AccessRequest( email="pending@example.com", @@ -25,23 +25,23 @@ async def test_update_access_request_status_approved( last_name="User", status=AccessRequestStatus.PENDING, ) - session.add(access_request) - await session.commit() + db_session.add(access_request) + await db_session.commit() - admin_user = await get_admin_user_public(session, settings) + admin_user = await get_admin_user_public(db_session, settings) background_tasks = BackgroundTasks() await update_access_request_status( access_request_id=access_request.id, status=AccessRequestStatus.APPROVED, - db=session, + db=db_session, user=admin_user, background_tasks=background_tasks, email_svc=mock_email_svc, settings=settings, ) - await session.refresh(access_request) + await db_session.refresh(access_request) assert access_request.status == AccessRequestStatus.APPROVED assert isinstance(access_request.reviewed_at, datetime) @@ -49,7 +49,7 @@ async def test_update_access_request_status_approved( tokens = ( ( - await session.execute( + await db_session.execute( select(RegistrationToken).where( RegistrationToken.access_request_id == access_request.id ) @@ -69,7 +69,7 @@ async def test_update_access_request_status_approved( async def test_update_access_request_status_rejected( - session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings + db_session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings ): access_request = AccessRequest( email="pending2@example.com", @@ -77,28 +77,28 @@ async def test_update_access_request_status_rejected( last_name="User", status=AccessRequestStatus.PENDING, ) - session.add(access_request) - await session.commit() + db_session.add(access_request) + await db_session.commit() - admin_user = await get_admin_user_public(session, settings) + admin_user = await get_admin_user_public(db_session, settings) background_tasks = BackgroundTasks() await update_access_request_status( access_request_id=access_request.id, status=AccessRequestStatus.REJECTED, - db=session, + db=db_session, user=admin_user, background_tasks=background_tasks, email_svc=mock_email_svc, settings=settings, ) - await session.refresh(access_request) + await db_session.refresh(access_request) assert access_request.status == AccessRequestStatus.REJECTED tokens = ( ( - await session.execute( + await db_session.execute( select(RegistrationToken).where( RegistrationToken.access_request_id == access_request.id ) @@ -117,14 +117,14 @@ async def test_update_access_request_status_rejected( async def test_update_access_request_status_not_found( - session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings + db_session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings ): with pytest.raises(AccessRequestNotFound): await update_access_request_status( access_request_id=9999, status=AccessRequestStatus.APPROVED, - db=session, - user=await get_admin_user_public(session, settings), + db=db_session, + user=await get_admin_user_public(db_session, settings), background_tasks=BackgroundTasks(), email_svc=mock_email_svc, settings=settings, @@ -132,7 +132,7 @@ async def test_update_access_request_status_not_found( async def test_update_access_request_status_not_pending( - session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings + db_session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings ): access_request = AccessRequest( email="approved@example.com", @@ -140,15 +140,15 @@ async def test_update_access_request_status_not_pending( last_name="User", status=AccessRequestStatus.APPROVED, ) - session.add(access_request) - await session.commit() + db_session.add(access_request) + await db_session.commit() with pytest.raises(AccessRequestNotPending): await update_access_request_status( access_request_id=access_request.id, status=AccessRequestStatus.REJECTED, - db=session, - user=await get_admin_user_public(session, settings), + db=db_session, + user=await get_admin_user_public(db_session, settings), background_tasks=BackgroundTasks(), email_svc=mock_email_svc, settings=settings, diff --git a/server/app/tests/services/auth/test_login.py b/server/app/tests/services/auth/test_login.py index 512115d7..1f037ae2 100644 --- a/server/app/tests/services/auth/test_login.py +++ b/server/app/tests/services/auth/test_login.py @@ -7,11 +7,11 @@ from app.services.auth import login -async def test_login(session: AsyncSession, settings: Settings): +async def test_login(db_session: AsyncSession, settings: Settings): result = await login( identifier=settings.admin.username, password=settings.admin.password, - db=session, + db=db_session, settings=settings, ) payload = jwt.decode( @@ -24,11 +24,11 @@ async def test_login(session: AsyncSession, settings: Settings): assert "exp" in payload -async def test_login_with_email(session: AsyncSession, settings: Settings): +async def test_login_with_email(db_session: AsyncSession, settings: Settings): result = await login( identifier=settings.admin.email, password=settings.admin.password, - db=session, + db=db_session, settings=settings, ) payload = jwt.decode( @@ -41,21 +41,21 @@ async def test_login_with_email(session: AsyncSession, settings: Settings): assert "exp" in payload -async def test_login_user_not_found(session: AsyncSession, settings: Settings): +async def test_login_user_not_found(db_session: AsyncSession, settings: Settings): with pytest.raises(InvalidCredentials): await login( identifier="non_existent_user", password="some_password", - db=session, + db=db_session, settings=settings, ) -async def test_login_invalid_password(session: AsyncSession, settings: Settings): +async def test_login_invalid_password(db_session: AsyncSession, settings: Settings): with pytest.raises(InvalidCredentials): await login( identifier=settings.admin.username, password="some_password", - db=session, + db=db_session, settings=settings, ) diff --git a/server/app/tests/services/auth/test_refresh.py b/server/app/tests/services/auth/test_refresh.py index 243f43fd..e77b643e 100644 --- a/server/app/tests/services/auth/test_refresh.py +++ b/server/app/tests/services/auth/test_refresh.py @@ -8,9 +8,9 @@ from app.services.auth import refresh -async def test_refresh(session: AsyncSession, settings: Settings): +async def test_refresh(db_session: AsyncSession, settings: Settings): token = create_refresh_jwt(settings.admin.username, settings) - new_access_token = await refresh(session, token, settings) + new_access_token = await refresh(db_session, token, settings) payload = jwt.decode( new_access_token, @@ -21,13 +21,13 @@ async def test_refresh(session: AsyncSession, settings: Settings): assert "exp" in payload -async def test_refresh_user_not_found(session: AsyncSession, settings: Settings): +async def test_refresh_user_not_found(db_session: AsyncSession, settings: Settings): token = create_refresh_jwt("missing_user", settings) with pytest.raises(InvalidCredentials): - await refresh(session, token, settings) + await refresh(db_session, token, settings) -async def test_refresh_invalid_token(session: AsyncSession, settings: Settings): +async def test_refresh_invalid_token(db_session: AsyncSession, settings: Settings): with pytest.raises(InvalidCredentials): - await refresh(session, "invalid-token", settings) + await refresh(db_session, "invalid-token", settings) diff --git a/server/app/tests/services/auth/test_register.py b/server/app/tests/services/auth/test_register.py index f0978f89..2aa3d525 100644 --- a/server/app/tests/services/auth/test_register.py +++ b/server/app/tests/services/auth/test_register.py @@ -12,123 +12,123 @@ from app.services.auth import register -async def test_register(session: AsyncSession): +async def test_register(db_session: AsyncSession): access_request = AccessRequest( email="approved2@example.com", first_name="Approved", last_name="User", status=AccessRequestStatus.APPROVED, ) - session.add(access_request) - await session.flush() + db_session.add(access_request) + await db_session.flush() token_str, token = create_registration_token(access_request.id) - session.add(token) - await session.commit() + db_session.add(token) + await db_session.commit() await register( token_str=token_str, username="new_user", password="new_password", - db=session, + db=db_session, ) user = ( - await session.execute(select(User).where(User.username == "new_user")) + await db_session.execute(select(User).where(User.username == "new_user")) ).scalar_one() assert user.email == access_request.email assert user.first_name == access_request.first_name assert user.last_name == access_request.last_name assert PASSWORD_HASH.verify("new_password", user.password_hash) - await session.refresh(token) + await db_session.refresh(token) assert token.is_used() assert token.is_expired() -async def test_register_invalid_token(session: AsyncSession): +async def test_register_invalid_token(db_session: AsyncSession): with pytest.raises(InvalidToken): await register( token_str="invalid-token", username="new_user", password="new_password", - db=session, + db=db_session, ) -async def test_register_used_token(session: AsyncSession): +async def test_register_used_token(db_session: AsyncSession): access_request = AccessRequest( email="approved@example.com", first_name="Approved", last_name="User", status=AccessRequestStatus.APPROVED, ) - session.add(access_request) - await session.flush() + db_session.add(access_request) + await db_session.flush() token_str, token = create_registration_token(access_request.id) token.used_at = datetime.now(UTC) - session.add(token) - await session.commit() + db_session.add(token) + await db_session.commit() with pytest.raises(InvalidToken): await register( token_str=token_str, username="new_user", password="new_password", - db=session, + db=db_session, ) -async def test_register_expired_token(session: AsyncSession): +async def test_register_expired_token(db_session: AsyncSession): access_request = AccessRequest( email="approved@example.com", first_name="Approved", last_name="User", status=AccessRequestStatus.APPROVED, ) - session.add(access_request) - await session.flush() + db_session.add(access_request) + await db_session.flush() token_str, token = create_registration_token(access_request.id) token.expires_at = datetime.now(UTC) - session.add(token) - await session.commit() + db_session.add(token) + await db_session.commit() with pytest.raises(InvalidToken): await register( token_str=token_str, username="new_user", password="new_password", - db=session, + db=db_session, ) -async def test_register_access_request_not_approved(session: AsyncSession): +async def test_register_access_request_not_approved(db_session: AsyncSession): access_request = AccessRequest( email="pending@example.com", first_name="Pending", last_name="User", status=AccessRequestStatus.PENDING, ) - session.add(access_request) - await session.flush() + db_session.add(access_request) + await db_session.flush() token_str, token = create_registration_token(access_request.id) - session.add(token) - await session.commit() + db_session.add(token) + await db_session.commit() with pytest.raises(InvalidToken): await register( token_str=token_str, username="pending_user", password="new_password", - db=session, + db=db_session, ) -async def test_register_username_taken(session: AsyncSession): - session.add( +async def test_register_username_taken(db_session: AsyncSession): + db_session.add( User( email="existing@example.com", username="taken", @@ -144,25 +144,25 @@ async def test_register_username_taken(session: AsyncSession): last_name="User", status=AccessRequestStatus.APPROVED, ) - session.add(access_request) - await session.flush() + db_session.add(access_request) + await db_session.flush() token_str, token = create_registration_token(access_request.id) - session.add(token) - await session.commit() + db_session.add(token) + await db_session.commit() with pytest.raises(UsernameTaken): await register( token_str=token_str, username="taken", password="new_password", - db=session, + db=db_session, ) -async def test_register_username_matches_email(session: AsyncSession): +async def test_register_username_matches_email(db_session: AsyncSession): collision_identifier = "identifier_collision" - session.add( + db_session.add( User( email=collision_identifier, username="existing_user", @@ -178,17 +178,17 @@ async def test_register_username_matches_email(session: AsyncSession): last_name="User", status=AccessRequestStatus.APPROVED, ) - session.add(access_request) - await session.flush() + db_session.add(access_request) + await db_session.flush() token_str, token = create_registration_token(access_request.id) - session.add(token) - await session.commit() + db_session.add(token) + await db_session.commit() with pytest.raises(UsernameTaken): await register( token_str=token_str, username=collision_identifier, password="new_password", - db=session, + db=db_session, ) diff --git a/server/app/tests/services/auth/test_request_access.py b/server/app/tests/services/auth/test_request_access.py index 1e7562a8..b83c5832 100644 --- a/server/app/tests/services/auth/test_request_access.py +++ b/server/app/tests/services/auth/test_request_access.py @@ -16,7 +16,7 @@ async def test_request_access( - session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings + db_session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings ): new_email = "newuser@example.com" background_tasks = BackgroundTasks() @@ -25,7 +25,7 @@ async def test_request_access( first_name="New", last_name="User", background_tasks=background_tasks, - db=session, + db=db_session, email_svc=mock_email_svc, settings=settings, ) @@ -41,7 +41,7 @@ async def test_request_access( async def test_request_access_approved( - session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings + db_session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings ): approved_email = "approved@example.com" req = AccessRequest( @@ -50,8 +50,8 @@ async def test_request_access_approved( last_name="User", status=AccessRequestStatus.APPROVED, ) - session.add(req) - await session.commit() + db_session.add(req) + await db_session.commit() background_tasks = BackgroundTasks() already_approved = await request_access( @@ -59,7 +59,7 @@ async def test_request_access_approved( first_name="Test", last_name="User", background_tasks=background_tasks, - db=session, + db=db_session, email_svc=mock_email_svc, settings=settings, ) @@ -75,7 +75,7 @@ async def test_request_access_approved( async def test_request_access_existing_user( - session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings + db_session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings ): user = User( email="existing@example.com", @@ -84,8 +84,8 @@ async def test_request_access_existing_user( last_name="User", password_hash="fakehash", ) - session.add(user) - await session.commit() + db_session.add(user) + await db_session.commit() background_tasks = BackgroundTasks() with pytest.raises(EmailInUse): @@ -94,7 +94,7 @@ async def test_request_access_existing_user( first_name="Test", last_name="User", background_tasks=background_tasks, - db=session, + db=db_session, email_svc=mock_email_svc, settings=settings, ) @@ -103,12 +103,12 @@ async def test_request_access_existing_user( async def test_request_access_email_matches_username( - session: AsyncSession, + db_session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings, ): collision_identifier = "existing@example.com" - session.add( + db_session.add( User( email="different@example.com", username=collision_identifier, @@ -117,7 +117,7 @@ async def test_request_access_email_matches_username( password_hash="fakehash", ) ) - await session.commit() + await db_session.commit() background_tasks = BackgroundTasks() with pytest.raises(EmailInUse): @@ -126,7 +126,7 @@ async def test_request_access_email_matches_username( first_name="Test", last_name="User", background_tasks=background_tasks, - db=session, + db=db_session, email_svc=mock_email_svc, settings=settings, ) @@ -135,7 +135,7 @@ async def test_request_access_email_matches_username( async def test_request_access_pending( - session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings + db_session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings ): req = AccessRequest( email="pending@example.com", @@ -143,8 +143,8 @@ async def test_request_access_pending( last_name="User", status=AccessRequestStatus.PENDING, ) - session.add(req) - await session.commit() + db_session.add(req) + await db_session.commit() background_tasks = BackgroundTasks() with pytest.raises(AccessRequestPending): @@ -153,7 +153,7 @@ async def test_request_access_pending( first_name="Test", last_name="User", background_tasks=background_tasks, - db=session, + db=db_session, email_svc=mock_email_svc, settings=settings, ) @@ -162,7 +162,7 @@ async def test_request_access_pending( async def test_request_access_rejected( - session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings + db_session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings ): req = AccessRequest( email="rejected@example.com", @@ -170,8 +170,8 @@ async def test_request_access_rejected( last_name="User", status=AccessRequestStatus.REJECTED, ) - session.add(req) - await session.commit() + db_session.add(req) + await db_session.commit() background_tasks = BackgroundTasks() with pytest.raises(AccessRequestRejected): @@ -180,7 +180,7 @@ async def test_request_access_rejected( first_name="Test", last_name="User", background_tasks=background_tasks, - db=session, + db=db_session, email_svc=mock_email_svc, settings=settings, ) diff --git a/server/app/tests/services/auth/test_request_password_reset.py b/server/app/tests/services/auth/test_request_password_reset.py index cb91ab41..38b75f20 100644 --- a/server/app/tests/services/auth/test_request_password_reset.py +++ b/server/app/tests/services/auth/test_request_password_reset.py @@ -12,7 +12,7 @@ async def test_request_password_reset( - session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings + db_session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings ): user = User( email="reset@example.com", @@ -21,22 +21,22 @@ async def test_request_password_reset( last_name="User", password_hash=PASSWORD_HASH.hash("password"), ) - session.add(user) - await session.commit() + db_session.add(user) + await db_session.commit() background_tasks = BackgroundTasks() await request_password_reset( email=user.email, background_tasks=background_tasks, - db=session, + db=db_session, email_svc=mock_email_svc, settings=settings, ) tokens = ( ( - await session.execute( + await db_session.execute( select(PasswordResetToken).where(PasswordResetToken.user_id == user.id) ) ) @@ -54,36 +54,36 @@ async def test_request_password_reset( async def test_request_password_reset_unregistered_email( - session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings + db_session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings ): background_tasks = BackgroundTasks() await request_password_reset( email="missing@example.com", background_tasks=background_tasks, - db=session, + db=db_session, email_svc=mock_email_svc, settings=settings, ) - tokens = (await session.execute(select(PasswordResetToken))).scalars().all() + tokens = (await db_session.execute(select(PasswordResetToken))).scalars().all() assert len(tokens) == 0 assert len(background_tasks.tasks) == 0 async def test_request_password_reset_admin_email( - session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings + db_session: AsyncSession, mock_email_svc: AsyncMock, settings: Settings ): background_tasks = BackgroundTasks() await request_password_reset( email=settings.admin.email, background_tasks=background_tasks, - db=session, + db=db_session, email_svc=mock_email_svc, settings=settings, ) - tokens = (await session.execute(select(PasswordResetToken))).scalars().all() + tokens = (await db_session.execute(select(PasswordResetToken))).scalars().all() assert len(tokens) == 0 assert len(background_tasks.tasks) == 0 diff --git a/server/app/tests/services/auth/test_reset_password.py b/server/app/tests/services/auth/test_reset_password.py index 9062ce25..b4f5437f 100644 --- a/server/app/tests/services/auth/test_reset_password.py +++ b/server/app/tests/services/auth/test_reset_password.py @@ -9,7 +9,7 @@ from app.services.auth import reset_password -async def test_reset_password(session: AsyncSession): +async def test_reset_password(db_session: AsyncSession): user = User( email="reset2@example.com", username="reset_user2", @@ -17,12 +17,12 @@ async def test_reset_password(session: AsyncSession): last_name="User", password_hash=PASSWORD_HASH.hash("old_password"), ) - session.add(user) - await session.flush() + db_session.add(user) + await db_session.flush() token_str, token = create_password_reset_token(user.id) - session.add(token) - await session.commit() + db_session.add(token) + await db_session.commit() assert not token.is_expired() @@ -31,27 +31,27 @@ async def test_reset_password(session: AsyncSession): await reset_password( token_str=token_str, password="new_password", - db=session, + db=db_session, ) assert user.password_hash != old_hash assert PASSWORD_HASH.verify("new_password", user.password_hash) - await session.refresh(token) + await db_session.refresh(token) assert token.is_used() assert token.is_expired() -async def test_reset_password_invalid_token(session: AsyncSession): +async def test_reset_password_invalid_token(db_session: AsyncSession): with pytest.raises(InvalidToken): await reset_password( token_str="invalid-token", password="new_password", - db=session, + db=db_session, ) -async def test_reset_password_used_token(session: AsyncSession): +async def test_reset_password_used_token(db_session: AsyncSession): user = User( email="reset@example.com", username="reset_user", @@ -59,23 +59,23 @@ async def test_reset_password_used_token(session: AsyncSession): last_name="User", password_hash=PASSWORD_HASH.hash("password"), ) - session.add(user) - await session.flush() + db_session.add(user) + await db_session.flush() token_str, token = create_password_reset_token(user.id) token.used_at = datetime.now(UTC) - session.add(token) - await session.commit() + db_session.add(token) + await db_session.commit() with pytest.raises(InvalidToken): await reset_password( token_str=token_str, password="new_password", - db=session, + db=db_session, ) -async def test_reset_password_expired_token(session: AsyncSession): +async def test_reset_password_expired_token(db_session: AsyncSession): user = User( email="expired@example.com", username="expired_user", @@ -83,17 +83,17 @@ async def test_reset_password_expired_token(session: AsyncSession): last_name="User", password_hash=PASSWORD_HASH.hash("password"), ) - session.add(user) - await session.flush() + db_session.add(user) + await db_session.flush() token_str, token = create_password_reset_token(user.id) token.expires_at = datetime.now(UTC) - session.add(token) - await session.commit() + db_session.add(token) + await db_session.commit() with pytest.raises(InvalidToken): await reset_password( token_str=token_str, password="new_password", - db=session, + db=db_session, ) diff --git a/server/app/tests/services/exercise/test_create_exercise.py b/server/app/tests/services/exercise/test_create_exercise.py index b7d18ef3..883a7fc8 100644 --- a/server/app/tests/services/exercise/test_create_exercise.py +++ b/server/app/tests/services/exercise/test_create_exercise.py @@ -14,9 +14,9 @@ from ..utilities import create_user -async def test_create_exercise(session: AsyncSession): - user = await create_user(session) - muscle_group_id = await get_muscle_group_id(session, name="chest") +async def test_create_exercise(db_session: AsyncSession): + user = await create_user(db_session) + muscle_group_id = await get_muscle_group_id(db_session, name="chest") await create_exercise( user.id, @@ -25,11 +25,11 @@ async def test_create_exercise(session: AsyncSession): description="Upper chest press", muscle_group_ids=[muscle_group_id], ), - session, + db_session, ) exercises = await query_exercises( - session, + db_session, False, Exercise.name == "Incline Bench", ) @@ -42,39 +42,39 @@ async def test_create_exercise(session: AsyncSession): assert [mg.muscle_group_id for mg in exercise.muscle_groups] == [muscle_group_id] -async def test_create_exercise_muscle_group_not_found(session: AsyncSession): - user = await create_user(session) +async def test_create_exercise_muscle_group_not_found(db_session: AsyncSession): + user = await create_user(db_session) with pytest.raises(MuscleGroupNotFound): await create_exercise( user.id, CreateExerciseRequest(name="Bench", muscle_group_ids=[99999]), - session, + db_session, ) -async def test_create_exercise_name_conflict(session: AsyncSession): - user = await create_user(session) +async def test_create_exercise_name_conflict(db_session: AsyncSession): + user = await create_user(db_session) await create_exercise( user.id, CreateExerciseRequest(name="Bench", muscle_group_ids=[]), - session, + db_session, ) with pytest.raises(ExerciseNameConflict): await create_exercise( user.id, CreateExerciseRequest(name="Bench", muscle_group_ids=[]), - session, + db_session, ) -async def test_create_exercise_unhandled_integrity_error(session: AsyncSession): - user = await create_user(session) +async def test_create_exercise_unhandled_integrity_error(db_session: AsyncSession): + user = await create_user(db_session) await create_exercise( user.id, CreateExerciseRequest(name="Bench", muscle_group_ids=[]), - session, + db_session, ) with patch("app.services.exercise.is_unique_violation", return_value=False): @@ -82,5 +82,5 @@ async def test_create_exercise_unhandled_integrity_error(session: AsyncSession): await create_exercise( user.id, CreateExerciseRequest(name="Bench", muscle_group_ids=[]), - session, + db_session, ) diff --git a/server/app/tests/services/exercise/test_delete_exercise.py b/server/app/tests/services/exercise/test_delete_exercise.py index 7979d52f..c4d810d0 100644 --- a/server/app/tests/services/exercise/test_delete_exercise.py +++ b/server/app/tests/services/exercise/test_delete_exercise.py @@ -12,24 +12,24 @@ from .utilities import create_exercise -async def test_delete_exercise(session: AsyncSession): - user = await create_user(session) - mg_id = await get_muscle_group_id(session, name="chest") +async def test_delete_exercise(db_session: AsyncSession): + user = await create_user(db_session) + mg_id = await get_muscle_group_id(db_session, name="chest") exercise = await create_exercise( - session, + db_session, name="Bench", user_id=user.id, muscle_group_ids=[mg_id], ) - await delete_exercise(exercise.id, user.id, session) + await delete_exercise(exercise.id, user.id, db_session) - exercises = await session.execute( + exercises = await db_session.execute( select(Exercise).where(Exercise.id == exercise.id), ) assert exercises.scalar_one_or_none() is None - emgs = await session.execute( + emgs = await db_session.execute( select(ExerciseMuscleGroup).where( ExerciseMuscleGroup.exercise_id == exercise.id, ), @@ -37,22 +37,22 @@ async def test_delete_exercise(session: AsyncSession): assert emgs.scalars().all() == [] -async def test_delete_exercise_not_found(session: AsyncSession): - user = await create_user(session) +async def test_delete_exercise_not_found(db_session: AsyncSession): + user = await create_user(db_session) with pytest.raises(ExerciseNotFound): - await delete_exercise(99999, user.id, session) + await delete_exercise(99999, user.id, db_session) -async def test_delete_exercise_not_allowed(session: AsyncSession): - user = await create_user(session) - user_2 = await create_user(session, username="user_2") +async def test_delete_exercise_not_allowed(db_session: AsyncSession): + user = await create_user(db_session) + user_2 = await create_user(db_session, username="user_2") exercise = await create_exercise( - session, + db_session, name="Bench", user_id=user.id, ) with pytest.raises(ExerciseNotFound): - await delete_exercise(exercise.id, user_2.id, session) + await delete_exercise(exercise.id, user_2.id, db_session) diff --git a/server/app/tests/services/exercise/test_get_exercise.py b/server/app/tests/services/exercise/test_get_exercise.py index 3e0b380d..2c824f70 100644 --- a/server/app/tests/services/exercise/test_get_exercise.py +++ b/server/app/tests/services/exercise/test_get_exercise.py @@ -8,48 +8,48 @@ from .utilities import create_exercise -async def test_get_exercise(session: AsyncSession): - user = await create_user(session) +async def test_get_exercise(db_session: AsyncSession): + user = await create_user(db_session) exercise = await create_exercise( - session, + db_session, name="Bench", user_id=user.id, ) - result = await get_exercise(exercise.id, user.id, session) + result = await get_exercise(exercise.id, user.id, db_session) assert result.id == exercise.id assert result.name == "Bench" -async def test_get_exercise_system_exercise(session: AsyncSession): - user = await create_user(session) +async def test_get_exercise_system_exercise(db_session: AsyncSession): + user = await create_user(db_session) exercise = await create_exercise( - session, + db_session, name="System Deadlift", ) - result = await get_exercise(exercise.id, user.id, session) + result = await get_exercise(exercise.id, user.id, db_session) assert result.id == exercise.id assert result.user_id is None -async def test_get_exercise_not_found(session: AsyncSession): - user = await create_user(session) +async def test_get_exercise_not_found(db_session: AsyncSession): + user = await create_user(db_session) with pytest.raises(ExerciseNotFound): - await get_exercise(99999, user.id, session) + await get_exercise(99999, user.id, db_session) -async def test_get_exercise_not_allowed(session: AsyncSession): - owner = await create_user(session, username="owner") - other = await create_user(session, username="other") +async def test_get_exercise_not_allowed(db_session: AsyncSession): + owner = await create_user(db_session, username="owner") + other = await create_user(db_session, username="other") exercise = await create_exercise( - session, + db_session, name="Other Exercise", user_id=owner.id, ) with pytest.raises(ExerciseNotFound): - await get_exercise(exercise.id, other.id, session) + await get_exercise(exercise.id, other.id, db_session) diff --git a/server/app/tests/services/exercise/test_get_exercises.py b/server/app/tests/services/exercise/test_get_exercises.py index 690c899d..c4c988ea 100644 --- a/server/app/tests/services/exercise/test_get_exercises.py +++ b/server/app/tests/services/exercise/test_get_exercises.py @@ -6,23 +6,23 @@ from .utilities import create_exercise -async def test_get_exercises(session: AsyncSession): - user = await create_user(session) - user_2 = await create_user(session, username="user_2") +async def test_get_exercises(db_session: AsyncSession): + user = await create_user(db_session) + user_2 = await create_user(db_session, username="user_2") - await create_exercise(session, name="System Squat") + await create_exercise(db_session, name="System Squat") user_exercise = await create_exercise( - session, + db_session, name="User Curl", user_id=user.id, ) await create_exercise( - session, + db_session, name="Other Curl", user_id=user_2.id, ) - result = await get_exercises(user.id, session) + result = await get_exercises(user.id, db_session) ids = [exercise.id for exercise in result] assert len(ids) == 2 @@ -31,21 +31,21 @@ async def test_get_exercises(session: AsyncSession): assert all(exercise.name != "Other Curl" for exercise in result) -async def test_get_exercises_ordering(session: AsyncSession): - user = await create_user(session) +async def test_get_exercises_ordering(db_session: AsyncSession): + user = await create_user(db_session) await create_exercise( - session, + db_session, name="Z Press", user_id=user.id, ) await create_exercise( - session, + db_session, name="A Press", user_id=user.id, ) - result = await get_exercises(user.id, session) + result = await get_exercises(user.id, db_session) names = [exercise.name for exercise in result] assert names == sorted(names) diff --git a/server/app/tests/services/exercise/test_get_owned_exercise.py b/server/app/tests/services/exercise/test_get_owned_exercise.py index 3428f5ad..52f465e5 100644 --- a/server/app/tests/services/exercise/test_get_owned_exercise.py +++ b/server/app/tests/services/exercise/test_get_owned_exercise.py @@ -10,37 +10,37 @@ from .utilities import create_exercise -async def test_get_owned_exercise(session: AsyncSession): - user = await create_user(session) +async def test_get_owned_exercise(db_session: AsyncSession): + user = await create_user(db_session) exercise = await create_exercise( - session, + db_session, name="Owned Exercise", user_id=user.id, ) - result = await _get_owned_exercise(exercise.id, user.id, session) + result = await _get_owned_exercise(exercise.id, user.id, db_session) assert result.id == exercise.id assert result.user_id == user.id async def test_get_owned_exercise_not_found( - session: AsyncSession, + db_session: AsyncSession, ): - user = await create_user(session) + user = await create_user(db_session) with pytest.raises(ExerciseNotFound): - await _get_owned_exercise(99999, user.id, session) + await _get_owned_exercise(99999, user.id, db_session) -async def test_get_owned_exercise_not_allowed(session: AsyncSession): - owner = await create_user(session, username="owner") - other = await create_user(session, username="other") +async def test_get_owned_exercise_not_allowed(db_session: AsyncSession): + owner = await create_user(db_session, username="owner") + other = await create_user(db_session, username="other") exercise = await create_exercise( - session, + db_session, name="Other Exercise", user_id=owner.id, ) with pytest.raises(ExerciseNotFound): - await _get_owned_exercise(exercise.id, other.id, session) + await _get_owned_exercise(exercise.id, other.id, db_session) diff --git a/server/app/tests/services/exercise/test_update_exercise.py b/server/app/tests/services/exercise/test_update_exercise.py index d22be4ab..fe3dedfd 100644 --- a/server/app/tests/services/exercise/test_update_exercise.py +++ b/server/app/tests/services/exercise/test_update_exercise.py @@ -17,14 +17,14 @@ from .utilities import create_exercise -async def test_update_exercise(session: AsyncSession): - user = await create_user(session) +async def test_update_exercise(db_session: AsyncSession): + user = await create_user(db_session) exercise = await create_exercise( - session, + db_session, name="Bench", user_id=user.id, ) - muscle_group_id = await get_muscle_group_id(session, name="chest") + muscle_group_id = await get_muscle_group_id(db_session, name="chest") await update_exercise( exercise.id, @@ -34,10 +34,10 @@ async def test_update_exercise(session: AsyncSession): description="Updated description", muscle_group_ids=[muscle_group_id], ), - session, + db_session, ) - exercise = await get_exercise(exercise.id, user.id, session) + exercise = await get_exercise(exercise.id, user.id, db_session) assert exercise.name == "Incline Bench" assert exercise.description == "Updated description" @@ -46,24 +46,24 @@ async def test_update_exercise(session: AsyncSession): ] -async def test_update_exercise_not_found(session: AsyncSession): - user = await create_user(session) +async def test_update_exercise_not_found(db_session: AsyncSession): + user = await create_user(db_session) with pytest.raises(ExerciseNotFound): await update_exercise( 99999, user.id, UpdateExerciseRequest(), - session, + db_session, ) -async def test_update_exercise_not_allowed(session: AsyncSession): - user = await create_user(session) - user_2 = await create_user(session, username="user_2") +async def test_update_exercise_not_allowed(db_session: AsyncSession): + user = await create_user(db_session) + user_2 = await create_user(db_session, username="user_2") exercise = await create_exercise( - session, + db_session, name="Bench", user_id=user.id, ) @@ -73,14 +73,14 @@ async def test_update_exercise_not_allowed(session: AsyncSession): exercise.id, user_2.id, UpdateExerciseRequest(), - session, + db_session, ) -async def test_update_exercise_no_changes(session: AsyncSession): - user = await create_user(session) +async def test_update_exercise_no_changes(db_session: AsyncSession): + user = await create_user(db_session) exercise = await create_exercise( - session, + db_session, name="Bench", user_id=user.id, ) @@ -89,20 +89,20 @@ async def test_update_exercise_no_changes(session: AsyncSession): exercise.id, user.id, UpdateExerciseRequest(), - session, + db_session, ) - exercise = await get_exercise(exercise.id, user.id, session) + exercise = await get_exercise(exercise.id, user.id, db_session) assert exercise.name == "Bench" assert exercise.description is None assert len(exercise.muscle_groups) == 0 -async def test_update_exercise_no_name(session: AsyncSession): - user = await create_user(session) +async def test_update_exercise_no_name(db_session: AsyncSession): + user = await create_user(db_session) exercise = await create_exercise( - session, + db_session, name="Bench", user_id=user.id, ) @@ -111,20 +111,20 @@ async def test_update_exercise_no_name(session: AsyncSession): exercise.id, user.id, UpdateExerciseRequest(description="Updated description"), - session, + db_session, ) - exercise = await get_exercise(exercise.id, user.id, session) + exercise = await get_exercise(exercise.id, user.id, db_session) assert exercise.name == "Bench" assert exercise.description == "Updated description" assert len(exercise.muscle_groups) == 0 -async def test_update_exercise_no_description(session: AsyncSession): - user = await create_user(session) +async def test_update_exercise_no_description(db_session: AsyncSession): + user = await create_user(db_session) exercise = await create_exercise( - session, + db_session, name="Bench", user_id=user.id, ) @@ -133,20 +133,20 @@ async def test_update_exercise_no_description(session: AsyncSession): exercise.id, user.id, UpdateExerciseRequest(name="Incline Bench"), - session, + db_session, ) - exercise = await get_exercise(exercise.id, user.id, session) + exercise = await get_exercise(exercise.id, user.id, db_session) assert exercise.name == "Incline Bench" assert exercise.description is None assert len(exercise.muscle_groups) == 0 -async def test_update_exercise_null_values(session: AsyncSession): - user = await create_user(session) +async def test_update_exercise_null_values(db_session: AsyncSession): + user = await create_user(db_session) exercise = await create_exercise( - session, + db_session, name="Bench", user_id=user.id, description="Initial description", @@ -156,20 +156,20 @@ async def test_update_exercise_null_values(session: AsyncSession): exercise.id, user.id, UpdateExerciseRequest(description=None), - session, + db_session, ) - exercise = await get_exercise(exercise.id, user.id, session) + exercise = await get_exercise(exercise.id, user.id, db_session) assert exercise.name == "Bench" assert exercise.description is None assert len(exercise.muscle_groups) == 0 -async def test_update_exercise_muscle_group_not_found(session: AsyncSession): - user = await create_user(session) +async def test_update_exercise_muscle_group_not_found(db_session: AsyncSession): + user = await create_user(db_session) exercise = await create_exercise( - session, + db_session, name="Bench", user_id=user.id, ) @@ -179,19 +179,19 @@ async def test_update_exercise_muscle_group_not_found(session: AsyncSession): exercise.id, user.id, UpdateExerciseRequest(muscle_group_ids=[99999]), - session, + db_session, ) -async def test_update_exercise_name_conflict(session: AsyncSession): - user = await create_user(session) +async def test_update_exercise_name_conflict(db_session: AsyncSession): + user = await create_user(db_session) await create_exercise( - session, + db_session, name="Bench", user_id=user.id, ) exercise = await create_exercise( - session, + db_session, name="Press", user_id=user.id, ) @@ -201,19 +201,19 @@ async def test_update_exercise_name_conflict(session: AsyncSession): exercise.id, user.id, UpdateExerciseRequest(name="Bench"), - session, + db_session, ) -async def test_update_exercise_unhandled_integrity_error(session: AsyncSession): - user = await create_user(session) +async def test_update_exercise_unhandled_integrity_error(db_session: AsyncSession): + user = await create_user(db_session) await create_exercise( - session, + db_session, name="Bench", user_id=user.id, ) exercise = await create_exercise( - session, + db_session, name="Press", user_id=user.id, ) @@ -224,5 +224,5 @@ async def test_update_exercise_unhandled_integrity_error(session: AsyncSession): exercise.id, user.id, UpdateExerciseRequest(name="Bench"), - session, + db_session, ) diff --git a/server/app/tests/services/exercise/utilities.py b/server/app/tests/services/exercise/utilities.py index 9887cbbc..aea5310a 100644 --- a/server/app/tests/services/exercise/utilities.py +++ b/server/app/tests/services/exercise/utilities.py @@ -5,7 +5,7 @@ async def create_exercise( - session: AsyncSession, + db_session: AsyncSession, name: str, user_id: int | None = None, description: str | None = None, @@ -16,16 +16,16 @@ async def create_exercise( name=name, description=description, ) - session.add(exercise) - await session.flush() + db_session.add(exercise) + await db_session.flush() for muscle_group_id in muscle_group_ids or []: - session.add( + db_session.add( ExerciseMuscleGroup( exercise_id=exercise.id, muscle_group_id=muscle_group_id, ) ) - await session.commit() + await db_session.commit() return exercise diff --git a/server/app/tests/services/feedback/test_create_feedback_service.py b/server/app/tests/services/feedback/test_create_feedback_service.py index 06045e28..26151779 100644 --- a/server/app/tests/services/feedback/test_create_feedback_service.py +++ b/server/app/tests/services/feedback/test_create_feedback_service.py @@ -15,7 +15,7 @@ async def test_create_feedback( - session: AsyncSession, + db_session: AsyncSession, mock_github_svc: AsyncMock, settings: Settings, monkeypatch: pytest.MonkeyPatch, @@ -30,7 +30,7 @@ async def test_create_feedback( store_files_mock = AsyncMock(return_value=stored_files) monkeypatch.setattr("app.services.feedback.store_files", store_files_mock) - user = await get_admin_user_public(session, settings) + user = await get_admin_user_public(db_session, settings) upload_file = UploadFile(filename="screen.png", file=AsyncMock()) request = CreateFeedbackRequest( type=FeedbackType.feedback, @@ -43,13 +43,15 @@ async def test_create_feedback( await create_feedback( user=user, req=request, - db=session, + db=db_session, github_svc=mock_github_svc, settings=settings, ) feedback = ( - await session.execute(select(Feedback).where(Feedback.title == request.title)) + await db_session.execute( + select(Feedback).where(Feedback.title == request.title) + ) ).scalar_one() assert feedback.user_id == user.id assert feedback.type == request.type @@ -69,7 +71,7 @@ async def test_create_feedback( async def test_create_feedback_no_files( - session: AsyncSession, + db_session: AsyncSession, mock_github_svc: AsyncMock, settings: Settings, monkeypatch: pytest.MonkeyPatch, @@ -77,7 +79,7 @@ async def test_create_feedback_no_files( store_files_mock = AsyncMock(return_value=[]) monkeypatch.setattr("app.services.feedback.store_files", store_files_mock) - user = await get_admin_user_public(session, settings) + user = await get_admin_user_public(db_session, settings) request = CreateFeedbackRequest( type=FeedbackType.feature, url="https://example.com/feature", @@ -88,13 +90,15 @@ async def test_create_feedback_no_files( await create_feedback( user=user, req=request, - db=session, + db=db_session, github_svc=mock_github_svc, settings=settings, ) feedback = ( - await session.execute(select(Feedback).where(Feedback.title == request.title)) + await db_session.execute( + select(Feedback).where(Feedback.title == request.title) + ) ).scalar_one() assert feedback.files == [] diff --git a/server/app/tests/services/muscle_group/test_get_muscle_groups_by_ids.py b/server/app/tests/services/muscle_group/test_get_muscle_groups_by_ids.py index 08b0f160..54581d47 100644 --- a/server/app/tests/services/muscle_group/test_get_muscle_groups_by_ids.py +++ b/server/app/tests/services/muscle_group/test_get_muscle_groups_by_ids.py @@ -6,27 +6,27 @@ from .utilities import get_muscle_group_id -async def test_get_muscle_groups_by_ids(session: AsyncSession): - mg_1 = await get_muscle_group_id(session, name="biceps") - mg_2 = await get_muscle_group_id(session, name="triceps") +async def test_get_muscle_groups_by_ids(db_session: AsyncSession): + mg_1 = await get_muscle_group_id(db_session, name="biceps") + mg_2 = await get_muscle_group_id(db_session, name="triceps") - result = await get_muscle_groups_by_ids([mg_1, mg_2], session) + result = await get_muscle_groups_by_ids([mg_1, mg_2], db_session) assert len(result) == 2 assert all(isinstance(muscle_group, MuscleGroup) for muscle_group in result) assert {muscle_group.id for muscle_group in result} == {mg_1, mg_2} -async def test_get_muscle_groups_by_ids_missing_ids(session: AsyncSession): - mg = await get_muscle_group_id(session, name="biceps") +async def test_get_muscle_groups_by_ids_missing_ids(db_session: AsyncSession): + mg = await get_muscle_group_id(db_session, name="biceps") - result = await get_muscle_groups_by_ids([mg, 99999], session) + result = await get_muscle_groups_by_ids([mg, 99999], db_session) assert len(result) == 1 assert result[0].id == mg -async def test_get_muscle_groups_by_ids_empty(session: AsyncSession): - result = await get_muscle_groups_by_ids([], session) +async def test_get_muscle_groups_by_ids_empty(db_session: AsyncSession): + result = await get_muscle_groups_by_ids([], db_session) assert result == [] diff --git a/server/app/tests/services/muscle_group/test_get_muscle_groups_ordered_by_name.py b/server/app/tests/services/muscle_group/test_get_muscle_groups_ordered_by_name.py index d875110c..e1e7f8d1 100644 --- a/server/app/tests/services/muscle_group/test_get_muscle_groups_ordered_by_name.py +++ b/server/app/tests/services/muscle_group/test_get_muscle_groups_ordered_by_name.py @@ -6,8 +6,8 @@ from app.services.muscle_group import get_muscle_groups_ordered_by_name -async def test_get_muscle_groups_ordered_by_name(session: AsyncSession): - result = await get_muscle_groups_ordered_by_name(session) +async def test_get_muscle_groups_ordered_by_name(db_session: AsyncSession): + result = await get_muscle_groups_ordered_by_name(db_session) item = next(r for r in result if r.name == "chest") assert item is not None @@ -16,8 +16,8 @@ async def test_get_muscle_groups_ordered_by_name(session: AsyncSession): assert "pushing and pressing" in item.description -async def test_get_muscle_groups_ordered_by_name_ordering(session: AsyncSession): - result = await get_muscle_groups_ordered_by_name(session) +async def test_get_muscle_groups_ordered_by_name_ordering(db_session: AsyncSession): + result = await get_muscle_groups_ordered_by_name(db_session) names = [mg.name for mg in result] # case-insensitive sorting to match db @@ -26,10 +26,12 @@ async def test_get_muscle_groups_ordered_by_name_ordering(session: AsyncSession) assert names[-1] == "upper traps" -async def test_read_only(session: AsyncSession): - before_count = await session.scalar(select(func.count()).select_from(MuscleGroup)) +async def test_read_only(db_session: AsyncSession): + before_count = await db_session.scalar( + select(func.count()).select_from(MuscleGroup) + ) - await get_muscle_groups_ordered_by_name(session) + await get_muscle_groups_ordered_by_name(db_session) - after_count = await session.scalar(select(func.count()).select_from(MuscleGroup)) + after_count = await db_session.scalar(select(func.count()).select_from(MuscleGroup)) assert before_count == after_count diff --git a/server/app/tests/services/muscle_group/utilities.py b/server/app/tests/services/muscle_group/utilities.py index ddfef1b7..acd1e751 100644 --- a/server/app/tests/services/muscle_group/utilities.py +++ b/server/app/tests/services/muscle_group/utilities.py @@ -4,8 +4,8 @@ from app.models.database.muscle_group import MuscleGroup -async def get_muscle_group_id(session: AsyncSession, name: str) -> int: - result = await session.execute( +async def get_muscle_group_id(db_session: AsyncSession, name: str) -> int: + result = await db_session.execute( select(MuscleGroup).where(MuscleGroup.name == name), ) muscle_group = result.scalar_one() diff --git a/server/app/tests/services/set/test_create_set.py b/server/app/tests/services/set/test_create_set.py index bdf8d8ce..1f27d8b6 100644 --- a/server/app/tests/services/set/test_create_set.py +++ b/server/app/tests/services/set/test_create_set.py @@ -24,12 +24,12 @@ from .utilities import create_set as create_set_util -async def test_create_set(session: AsyncSession): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) - exercise = await create_exercise(session, name="Bench Press") +async def test_create_set(db_session: AsyncSession): + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) + exercise = await create_exercise(db_session, name="Bench Press") workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, @@ -45,10 +45,10 @@ async def test_create_set(session: AsyncSession): unit=SetUnit.lb, notes="Test set", ), - db=session, + db=db_session, ) - result = await session.execute( + result = await db_session.execute( select(Set).where( Set.workout_exercise_id == workout_exercise.id, ) @@ -63,21 +63,21 @@ async def test_create_set(session: AsyncSession): assert set_.notes == "Test set" -async def test_create_set_workout_not_found(session: AsyncSession): +async def test_create_set_workout_not_found(db_session: AsyncSession): with pytest.raises(WorkoutNotFound): await create_set( workout_id=1, workout_exercise_id=2, user_id=3, req=CreateSetRequest(), - db=session, + db=db_session, ) -async def test_create_set_workout_not_allowed(session: AsyncSession): - user_1 = await create_user(session, username="user_1") - user_2 = await create_user(session, username="user_2") - workout = await create_workout(session, user_id=user_2.id) +async def test_create_set_workout_not_allowed(db_session: AsyncSession): + user_1 = await create_user(db_session, username="user_1") + user_2 = await create_user(db_session, username="user_2") + workout = await create_workout(db_session, user_id=user_2.id) with pytest.raises(WorkoutNotFound): await create_set( @@ -85,13 +85,13 @@ async def test_create_set_workout_not_allowed(session: AsyncSession): workout_exercise_id=2, user_id=user_1.id, req=CreateSetRequest(), - db=session, + db=db_session, ) -async def test_create_set_workout_exercise_not_found(session: AsyncSession): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) +async def test_create_set_workout_exercise_not_found(db_session: AsyncSession): + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) with pytest.raises(WorkoutExerciseNotFound): await create_set( @@ -99,19 +99,19 @@ async def test_create_set_workout_exercise_not_found(session: AsyncSession): workout_exercise_id=2, user_id=user.id, req=CreateSetRequest(), - db=session, + db=db_session, ) -async def test_create_set_workout_exercise_not_allowed(session: AsyncSession): - exercise = await create_exercise(session, name="Squat") +async def test_create_set_workout_exercise_not_allowed(db_session: AsyncSession): + exercise = await create_exercise(db_session, name="Squat") - user_1 = await create_user(session, username="user_1") - user_2 = await create_user(session, username="user_2") - workout_1 = await create_workout(session, user_id=user_1.id) - workout_2 = await create_workout(session, user_id=user_2.id) + user_1 = await create_user(db_session, username="user_1") + user_2 = await create_user(db_session, username="user_2") + workout_1 = await create_workout(db_session, user_id=user_1.id) + workout_2 = await create_workout(db_session, user_id=user_2.id) workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout_2.id, exercise_id=exercise.id, position=1, @@ -123,26 +123,26 @@ async def test_create_set_workout_exercise_not_allowed(session: AsyncSession): workout_exercise_id=workout_exercise.id, user_id=user_1.id, req=CreateSetRequest(), - db=session, + db=db_session, ) async def test_create_set_set_number_conflict( - session: AsyncSession, + db_session: AsyncSession, monkeypatch: MonkeyPatch, ): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) - exercise = await create_exercise(session, name="Bench Press") + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) + exercise = await create_exercise(db_session, name="Bench Press") workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, ) await create_set_util( - session, + db_session, workout_exercise_id=workout_exercise.id, set_number=1, ) @@ -162,26 +162,26 @@ async def mock_get_next_set_number( workout_exercise_id=workout_exercise.id, user_id=user.id, req=CreateSetRequest(), - db=session, + db=db_session, ) async def test_create_set_unhandled_integrity_error( - session: AsyncSession, + db_session: AsyncSession, monkeypatch: MonkeyPatch, ): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) - exercise = await create_exercise(session, name="Bench Press") + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) + exercise = await create_exercise(db_session, name="Bench Press") workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, ) await create_set_util( - session, + db_session, workout_exercise_id=workout_exercise.id, set_number=1, ) @@ -202,5 +202,5 @@ async def mock_get_next_set_number( workout_exercise_id=workout_exercise.id, user_id=user.id, req=CreateSetRequest(), - db=session, + db=db_session, ) diff --git a/server/app/tests/services/set/test_delete_set.py b/server/app/tests/services/set/test_delete_set.py index 8f847a54..d7f27a1a 100644 --- a/server/app/tests/services/set/test_delete_set.py +++ b/server/app/tests/services/set/test_delete_set.py @@ -16,19 +16,19 @@ from .utilities import create_set -async def test_delete_set(session: AsyncSession): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) - exercise = await create_exercise(session, name="Bench Press") +async def test_delete_set(db_session: AsyncSession): + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) + exercise = await create_exercise(db_session, name="Bench Press") workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, ) set_ = await create_set( - session, + db_session, workout_exercise_id=workout_exercise.id, set_number=1, ) @@ -38,10 +38,10 @@ async def test_delete_set(session: AsyncSession): workout_exercise_id=workout_exercise.id, set_id=set_.id, user_id=user.id, - db=session, + db=db_session, ) - result = await session.execute( + result = await db_session.execute( select(Set).where( Set.id == set_.id, ) @@ -49,21 +49,21 @@ async def test_delete_set(session: AsyncSession): assert result.scalar_one_or_none() is None -async def test_delete_set_workout_not_found(session: AsyncSession): +async def test_delete_set_workout_not_found(db_session: AsyncSession): with pytest.raises(WorkoutNotFound): await delete_set( workout_id=1, workout_exercise_id=2, set_id=3, user_id=4, - db=session, + db=db_session, ) -async def test_delete_set_workout_not_allowed(session: AsyncSession): - user_1 = await create_user(session, username="user_1") - user_2 = await create_user(session, username="user_2") - workout = await create_workout(session, user_id=user_2.id) +async def test_delete_set_workout_not_allowed(db_session: AsyncSession): + user_1 = await create_user(db_session, username="user_1") + user_2 = await create_user(db_session, username="user_2") + workout = await create_workout(db_session, user_id=user_2.id) with pytest.raises(WorkoutNotFound): await delete_set( @@ -71,16 +71,16 @@ async def test_delete_set_workout_not_allowed(session: AsyncSession): workout_exercise_id=2, set_id=3, user_id=user_1.id, - db=session, + db=db_session, ) -async def test_delete_set_not_found(session: AsyncSession): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) - exercise = await create_exercise(session, name="Bench Press") +async def test_delete_set_not_found(db_session: AsyncSession): + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) + exercise = await create_exercise(db_session, name="Bench Press") workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, @@ -92,5 +92,5 @@ async def test_delete_set_not_found(session: AsyncSession): workout_exercise_id=workout_exercise.id, set_id=3, user_id=user.id, - db=session, + db=db_session, ) diff --git a/server/app/tests/services/set/test_get_next_set_number.py b/server/app/tests/services/set/test_get_next_set_number.py index d99a1f42..3918ca26 100644 --- a/server/app/tests/services/set/test_get_next_set_number.py +++ b/server/app/tests/services/set/test_get_next_set_number.py @@ -11,46 +11,46 @@ from ..workout.utilities import create_workout -async def test_get_next_set_number_empty(session: AsyncSession): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) - exercise = await create_exercise(session, name="Squat") +async def test_get_next_set_number_empty(db_session: AsyncSession): + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) + exercise = await create_exercise(db_session, name="Squat") workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, ) - set_number = await _get_next_set_number(workout_exercise.id, session) + set_number = await _get_next_set_number(workout_exercise.id, db_session) assert set_number == 1 -async def test_get_next_set_number_max(session: AsyncSession): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) - exercise = await create_exercise(session, name="Squat") +async def test_get_next_set_number_max(db_session: AsyncSession): + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) + exercise = await create_exercise(db_session, name="Squat") workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, ) other_workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=2, ) - session.add_all( + db_session.add_all( [ Set(workout_exercise_id=workout_exercise.id, set_number=1), Set(workout_exercise_id=workout_exercise.id, set_number=3), Set(workout_exercise_id=other_workout_exercise.id, set_number=5), ] ) - await session.commit() + await db_session.commit() - set_number = await _get_next_set_number(workout_exercise.id, session) + set_number = await _get_next_set_number(workout_exercise.id, db_session) assert set_number == 4 diff --git a/server/app/tests/services/set/test_update_set.py b/server/app/tests/services/set/test_update_set.py index 0ed0866e..cc783531 100644 --- a/server/app/tests/services/set/test_update_set.py +++ b/server/app/tests/services/set/test_update_set.py @@ -19,18 +19,18 @@ from .utilities import create_set -async def test_update_set(session: AsyncSession): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) - exercise = await create_exercise(session, name="Bench Press") +async def test_update_set(db_session: AsyncSession): + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) + exercise = await create_exercise(db_session, name="Bench Press") workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, ) set_ = await create_set( - session, + db_session, workout_exercise_id=workout_exercise.id, set_number=1, reps=10, @@ -50,10 +50,10 @@ async def test_update_set(session: AsyncSession): unit=SetUnit.kg, notes="Updated set", ), - db=session, + db=db_session, ) - set_ = await session.get(Set, set_.id) + set_ = await db_session.get(Set, set_.id) assert set_ is not None assert set_.reps == 12 @@ -62,7 +62,7 @@ async def test_update_set(session: AsyncSession): assert set_.notes == "Updated set" -async def test_update_set_workout_not_found(session: AsyncSession): +async def test_update_set_workout_not_found(db_session: AsyncSession): with pytest.raises(WorkoutNotFound): await update_set( workout_id=1, @@ -70,14 +70,14 @@ async def test_update_set_workout_not_found(session: AsyncSession): set_id=3, user_id=4, req=UpdateSetRequest(), - db=session, + db=db_session, ) -async def test_update_set_workout_not_allowed(session: AsyncSession): - user_1 = await create_user(session, username="user_1") - user_2 = await create_user(session, username="user_2") - workout = await create_workout(session, user_id=user_2.id) +async def test_update_set_workout_not_allowed(db_session: AsyncSession): + user_1 = await create_user(db_session, username="user_1") + user_2 = await create_user(db_session, username="user_2") + workout = await create_workout(db_session, user_id=user_2.id) with pytest.raises(WorkoutNotFound): await update_set( @@ -86,16 +86,16 @@ async def test_update_set_workout_not_allowed(session: AsyncSession): set_id=3, user_id=user_1.id, req=UpdateSetRequest(), - db=session, + db=db_session, ) -async def test_update_set_not_found(session: AsyncSession): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) - exercise = await create_exercise(session, name="Bench Press") +async def test_update_set_not_found(db_session: AsyncSession): + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) + exercise = await create_exercise(db_session, name="Bench Press") workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, @@ -108,22 +108,22 @@ async def test_update_set_not_found(session: AsyncSession): set_id=3, user_id=user.id, req=UpdateSetRequest(), - db=session, + db=db_session, ) -async def test_update_set_no_changes(session: AsyncSession): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) - exercise = await create_exercise(session, name="Bench Press") +async def test_update_set_no_changes(db_session: AsyncSession): + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) + exercise = await create_exercise(db_session, name="Bench Press") workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, ) set_ = await create_set( - session, + db_session, workout_exercise_id=workout_exercise.id, set_number=1, reps=10, @@ -138,10 +138,10 @@ async def test_update_set_no_changes(session: AsyncSession): set_id=set_.id, user_id=user.id, req=UpdateSetRequest(), - db=session, + db=db_session, ) - set_ = await session.get(Set, set_.id) + set_ = await db_session.get(Set, set_.id) assert set_ is not None assert set_.reps == 10 @@ -150,18 +150,18 @@ async def test_update_set_no_changes(session: AsyncSession): assert set_.notes == "First set" -async def test_update_set_no_reps(session: AsyncSession): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) - exercise = await create_exercise(session, name="Bench Press") +async def test_update_set_no_reps(db_session: AsyncSession): + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) + exercise = await create_exercise(db_session, name="Bench Press") workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, ) set_ = await create_set( - session, + db_session, workout_exercise_id=workout_exercise.id, set_number=1, reps=10, @@ -180,10 +180,10 @@ async def test_update_set_no_reps(session: AsyncSession): unit=SetUnit.kg, notes="Updated set", ), - db=session, + db=db_session, ) - set_ = await session.get(Set, set_.id) + set_ = await db_session.get(Set, set_.id) assert set_ is not None assert set_.reps == 10 @@ -192,18 +192,18 @@ async def test_update_set_no_reps(session: AsyncSession): assert set_.notes == "Updated set" -async def test_update_set_no_weight(session: AsyncSession): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) - exercise = await create_exercise(session, name="Bench Press") +async def test_update_set_no_weight(db_session: AsyncSession): + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) + exercise = await create_exercise(db_session, name="Bench Press") workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, ) set_ = await create_set( - session, + db_session, workout_exercise_id=workout_exercise.id, set_number=1, reps=10, @@ -222,10 +222,10 @@ async def test_update_set_no_weight(session: AsyncSession): unit=SetUnit.kg, notes="Updated set", ), - db=session, + db=db_session, ) - set_ = await session.get(Set, set_.id) + set_ = await db_session.get(Set, set_.id) assert set_ is not None assert set_.reps == 12 @@ -234,18 +234,18 @@ async def test_update_set_no_weight(session: AsyncSession): assert set_.notes == "Updated set" -async def test_update_set_no_unit(session: AsyncSession): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) - exercise = await create_exercise(session, name="Bench Press") +async def test_update_set_no_unit(db_session: AsyncSession): + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) + exercise = await create_exercise(db_session, name="Bench Press") workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, ) set_ = await create_set( - session, + db_session, workout_exercise_id=workout_exercise.id, set_number=1, reps=10, @@ -264,10 +264,10 @@ async def test_update_set_no_unit(session: AsyncSession): weight=Decimal(150), notes="Updated set", ), - db=session, + db=db_session, ) - set_ = await session.get(Set, set_.id) + set_ = await db_session.get(Set, set_.id) assert set_ is not None assert set_.reps == 12 @@ -276,18 +276,18 @@ async def test_update_set_no_unit(session: AsyncSession): assert set_.notes == "Updated set" -async def test_update_set_no_notes(session: AsyncSession): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) - exercise = await create_exercise(session, name="Bench Press") +async def test_update_set_no_notes(db_session: AsyncSession): + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) + exercise = await create_exercise(db_session, name="Bench Press") workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, ) set_ = await create_set( - session, + db_session, workout_exercise_id=workout_exercise.id, set_number=1, reps=10, @@ -306,10 +306,10 @@ async def test_update_set_no_notes(session: AsyncSession): weight=Decimal(150), unit=SetUnit.kg, ), - db=session, + db=db_session, ) - set_ = await session.get(Set, set_.id) + set_ = await db_session.get(Set, set_.id) assert set_ is not None assert set_.reps == 12 @@ -318,18 +318,18 @@ async def test_update_set_no_notes(session: AsyncSession): assert set_.notes == "First set" -async def test_update_set_null_values(session: AsyncSession): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) - exercise = await create_exercise(session, name="Bench Press") +async def test_update_set_null_values(db_session: AsyncSession): + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) + exercise = await create_exercise(db_session, name="Bench Press") workout_exercise = await create_workout_exercise( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, ) set_ = await create_set( - session, + db_session, workout_exercise_id=workout_exercise.id, set_number=1, reps=10, @@ -349,10 +349,10 @@ async def test_update_set_null_values(session: AsyncSession): unit=None, notes=None, ), - db=session, + db=db_session, ) - set_ = await session.get(Set, set_.id) + set_ = await db_session.get(Set, set_.id) assert set_ is not None assert set_.reps is None diff --git a/server/app/tests/services/set/utilities.py b/server/app/tests/services/set/utilities.py index b75e4477..44cc73f9 100644 --- a/server/app/tests/services/set/utilities.py +++ b/server/app/tests/services/set/utilities.py @@ -4,7 +4,7 @@ async def create_set( - session: AsyncSession, + db_session: AsyncSession, workout_exercise_id: int, set_number: int, reps: int | None = None, @@ -20,7 +20,7 @@ async def create_set( unit=unit, notes=notes, ) - session.add(set_) - await session.commit() - await session.refresh(set_) + db_session.add(set_) + await db_session.commit() + await db_session.refresh(set_) return set_ diff --git a/server/app/tests/services/token/test_expire_tokens.py b/server/app/tests/services/token/test_expire_tokens.py index f237ec98..2b61cc82 100644 --- a/server/app/tests/services/token/test_expire_tokens.py +++ b/server/app/tests/services/token/test_expire_tokens.py @@ -12,51 +12,53 @@ async def test_expire_tokens_registration( - session: AsyncSession, + db_session: AsyncSession, ): - access_request = await create_access_request(session, "expire@example.com") + access_request = await create_access_request(db_session, "expire@example.com") _, active_token = create_registration_token(access_request.id) _, expired_token = create_registration_token(access_request.id) expired_token.expires_at = datetime.now(UTC) - timedelta(minutes=1) - session.add_all([active_token, expired_token]) - await session.commit() + db_session.add_all([active_token, expired_token]) + await db_session.commit() previous_expired_value = expired_token.expires_at await expire_tokens( RegistrationToken, [RegistrationToken.access_request_id == access_request.id], - session, + db_session, ) - await session.refresh(active_token) - await session.refresh(expired_token) + await db_session.refresh(active_token) + await db_session.refresh(expired_token) assert active_token.expires_at <= datetime.now(UTC) assert expired_token.expires_at == previous_expired_value async def test_expire_tokens_registration_condition( - session: AsyncSession, + db_session: AsyncSession, ): - target_request = await create_access_request(session, "target-expire@example.com") - other_request = await create_access_request(session, "other-expire@example.com") + target_request = await create_access_request( + db_session, "target-expire@example.com" + ) + other_request = await create_access_request(db_session, "other-expire@example.com") _, target_token = create_registration_token(target_request.id) _, other_token = create_registration_token(other_request.id) - session.add_all([target_token, other_token]) - await session.commit() + db_session.add_all([target_token, other_token]) + await db_session.commit() await expire_tokens( RegistrationToken, [RegistrationToken.access_request_id == target_request.id], - session, + db_session, ) - await session.refresh(target_token) - await session.refresh(other_token) + await db_session.refresh(target_token) + await db_session.refresh(other_token) now = datetime.now(UTC) assert target_token.expires_at <= now diff --git a/server/app/tests/services/token/test_get_tokens.py b/server/app/tests/services/token/test_get_tokens.py index 8f5c0c4d..2671cd80 100644 --- a/server/app/tests/services/token/test_get_tokens.py +++ b/server/app/tests/services/token/test_get_tokens.py @@ -10,7 +10,7 @@ # RegistrationToken behavior is identical -async def _create_user(session: AsyncSession, suffix: str) -> User: +async def _create_user(db_session: AsyncSession, suffix: str) -> User: user = User( email=f"get-token-{suffix}@example.com", username=f"get_token_{suffix}", @@ -18,22 +18,22 @@ async def _create_user(session: AsyncSession, suffix: str) -> User: last_name="Tester", password_hash=PASSWORD_HASH.hash("password"), ) - session.add(user) - await session.flush() + db_session.add(user) + await db_session.flush() return user -async def test_get_tokens_by_prefix_password_reset(session: AsyncSession): - user = await _create_user(session, "basic") +async def test_get_tokens_by_prefix_password_reset(db_session: AsyncSession): + user = await _create_user(db_session, "basic") _, token = create_password_reset_token(user.id) - session.add(token) - await session.commit() + db_session.add(token) + await db_session.commit() tokens = await get_tokens_by_prefix( type(token), load_option=type(token).user, prefix=token.token_prefix, - db=session, + db=db_session, ) assert len(tokens) == 1 @@ -42,9 +42,9 @@ async def test_get_tokens_by_prefix_password_reset(session: AsyncSession): assert tokens[0].user.id == user.id -async def test_get_tokens_by_prefix_password_reset_condition(session: AsyncSession): - target_user = await _create_user(session, "target") - other_user = await _create_user(session, "other") +async def test_get_tokens_by_prefix_password_reset_condition(db_session: AsyncSession): + target_user = await _create_user(db_session, "target") + other_user = await _create_user(db_session, "other") _, active_token = create_password_reset_token(target_user.id) _, used_token = create_password_reset_token(target_user.id) @@ -61,21 +61,21 @@ async def test_get_tokens_by_prefix_password_reset_condition(session: AsyncSessi expired_token.expires_at = now - timedelta(minutes=1) other_prefix_token.token_prefix = "prefix2" - session.add_all([active_token, used_token, expired_token, other_prefix_token]) - await session.commit() + db_session.add_all([active_token, used_token, expired_token, other_prefix_token]) + await db_session.commit() tokens = await get_tokens_by_prefix( type(active_token), load_option=type(active_token).user, prefix=prefix, - db=session, + db=db_session, ) assert [token.id for token in tokens] == [active_token.id] -async def test_get_tokens_by_prefix_password_reset_ordering(session: AsyncSession): - user = await _create_user(session, "ordering") +async def test_get_tokens_by_prefix_password_reset_ordering(db_session: AsyncSession): + user = await _create_user(db_session, "ordering") _, older_token = create_password_reset_token(user.id) _, newer_token = create_password_reset_token(user.id) @@ -87,14 +87,14 @@ async def test_get_tokens_by_prefix_password_reset_ordering(session: AsyncSessio older_token.created_at = now - timedelta(minutes=5) newer_token.created_at = now - session.add_all([older_token, newer_token]) - await session.commit() + db_session.add_all([older_token, newer_token]) + await db_session.commit() tokens = await get_tokens_by_prefix( type(older_token), load_option=type(older_token).user, prefix=prefix, - db=session, + db=db_session, ) assert [token.id for token in tokens] == [newer_token.id, older_token.id] diff --git a/server/app/tests/services/user/test_get_admin_users.py b/server/app/tests/services/user/test_get_admin_users.py index 1c18288d..e7661a6a 100644 --- a/server/app/tests/services/user/test_get_admin_users.py +++ b/server/app/tests/services/user/test_get_admin_users.py @@ -4,7 +4,7 @@ from app.services.user import get_admin_users -async def test_get_admin_users(session: AsyncSession): +async def test_get_admin_users(db_session: AsyncSession): admin_user = User( email="svc-admin@example.com", username="svc_admin", @@ -21,10 +21,10 @@ async def test_get_admin_users(session: AsyncSession): password_hash="hash", is_admin=False, ) - session.add_all([admin_user, regular_user]) - await session.commit() + db_session.add_all([admin_user, regular_user]) + await db_session.commit() - result = await get_admin_users(session) + result = await get_admin_users(db_session) usernames = [user.username for user in result] assert "svc_admin" in usernames diff --git a/server/app/tests/services/user/test_get_user_by_email.py b/server/app/tests/services/user/test_get_user_by_email.py index 00e4c984..ef2578ea 100644 --- a/server/app/tests/services/user/test_get_user_by_email.py +++ b/server/app/tests/services/user/test_get_user_by_email.py @@ -4,7 +4,7 @@ from app.services.user import get_user_by_email -async def test_get_user_by_email(session: AsyncSession): +async def test_get_user_by_email(db_session: AsyncSession): user = User( email="by-email@example.com", username="by_email", @@ -12,17 +12,17 @@ async def test_get_user_by_email(session: AsyncSession): last_name="Email", password_hash="hash", ) - session.add(user) - await session.commit() + db_session.add(user) + await db_session.commit() - result = await get_user_by_email("by-email@example.com", session) + result = await get_user_by_email("by-email@example.com", db_session) assert result is not None assert result.id == user.id assert result.username == "by_email" -async def test_get_user_by_email_not_found(session: AsyncSession): - result = await get_user_by_email("missing-email@example.com", session) +async def test_get_user_by_email_not_found(db_session: AsyncSession): + result = await get_user_by_email("missing-email@example.com", db_session) assert result is None diff --git a/server/app/tests/services/user/test_get_user_by_identifier.py b/server/app/tests/services/user/test_get_user_by_identifier.py index aa14e7cd..2a6342f7 100644 --- a/server/app/tests/services/user/test_get_user_by_identifier.py +++ b/server/app/tests/services/user/test_get_user_by_identifier.py @@ -5,7 +5,7 @@ async def test_get_user_by_identifier_with_email( - session: AsyncSession, + db_session: AsyncSession, ): collision_identifier = "collision@example.com" username_match = User( @@ -22,17 +22,17 @@ async def test_get_user_by_identifier_with_email( last_name="Match", password_hash="hash", ) - session.add_all([username_match, email_match]) - await session.commit() + db_session.add_all([username_match, email_match]) + await db_session.commit() - result = await get_user_by_identifier(collision_identifier, session) + result = await get_user_by_identifier(collision_identifier, db_session) assert result is not None assert result.id == email_match.id async def test_get_user_by_identifier_with_email_fallback( - session: AsyncSession, + db_session: AsyncSession, ): identifier = "fallback@example.com" username_match = User( @@ -42,25 +42,25 @@ async def test_get_user_by_identifier_with_email_fallback( last_name="Fallback", password_hash="hash", ) - session.add(username_match) - await session.commit() + db_session.add(username_match) + await db_session.commit() - result = await get_user_by_identifier(identifier, session) + result = await get_user_by_identifier(identifier, db_session) assert result is not None assert result.id == username_match.id async def test_get_user_by_identifier_with_email_not_found( - session: AsyncSession, + db_session: AsyncSession, ): - result = await get_user_by_identifier("not_found@example.com", session) + result = await get_user_by_identifier("not_found@example.com", db_session) assert result is None async def test_get_user_by_identifier_with_username( - session: AsyncSession, + db_session: AsyncSession, ): collision_identifier = "collision_value" username_match = User( @@ -77,17 +77,17 @@ async def test_get_user_by_identifier_with_username( last_name="Match", password_hash="hash", ) - session.add_all([username_match, email_match]) - await session.commit() + db_session.add_all([username_match, email_match]) + await db_session.commit() - result = await get_user_by_identifier(collision_identifier, session) + result = await get_user_by_identifier(collision_identifier, db_session) assert result is not None assert result.id == username_match.id async def test_get_user_by_identifier_with_username_not_found( - session: AsyncSession, + db_session: AsyncSession, ): collision_identifier = "collision_value" email_match = User( @@ -97,9 +97,9 @@ async def test_get_user_by_identifier_with_username_not_found( last_name="Match", password_hash="hash", ) - session.add(email_match) - await session.commit() + db_session.add(email_match) + await db_session.commit() - result = await get_user_by_identifier(collision_identifier, session) + result = await get_user_by_identifier(collision_identifier, db_session) assert result is None diff --git a/server/app/tests/services/user/test_get_user_by_username.py b/server/app/tests/services/user/test_get_user_by_username.py index 78d3ef2f..9ac0a1f8 100644 --- a/server/app/tests/services/user/test_get_user_by_username.py +++ b/server/app/tests/services/user/test_get_user_by_username.py @@ -4,7 +4,7 @@ from app.services.user import get_user_by_username -async def test_get_user_by_username(session: AsyncSession): +async def test_get_user_by_username(db_session: AsyncSession): user = User( email="by-username@example.com", username="by_username", @@ -12,17 +12,17 @@ async def test_get_user_by_username(session: AsyncSession): last_name="Username", password_hash="hash", ) - session.add(user) - await session.commit() + db_session.add(user) + await db_session.commit() - result = await get_user_by_username("by_username", session) + result = await get_user_by_username("by_username", db_session) assert result is not None assert result.id == user.id assert result.email == "by-username@example.com" -async def test_get_user_by_username_not_found(session: AsyncSession): - result = await get_user_by_username("missing_username", session) +async def test_get_user_by_username_not_found(db_session: AsyncSession): + result = await get_user_by_username("missing_username", db_session) assert result is None diff --git a/server/app/tests/services/user/test_get_users_ordered_by_username.py b/server/app/tests/services/user/test_get_users_ordered_by_username.py index 8bcb7b03..b8d7abad 100644 --- a/server/app/tests/services/user/test_get_users_ordered_by_username.py +++ b/server/app/tests/services/user/test_get_users_ordered_by_username.py @@ -5,7 +5,7 @@ from app.services.user import get_users_ordered_by_username -async def test_get_users_ordered_by_username(session: AsyncSession): +async def test_get_users_ordered_by_username(db_session: AsyncSession): user = User( email="shape-user@example.com", username="shape_user", @@ -13,10 +13,10 @@ async def test_get_users_ordered_by_username(session: AsyncSession): last_name="User", password_hash="hash", ) - session.add(user) - await session.commit() + db_session.add(user) + await db_session.commit() - result = await get_users_ordered_by_username(session) + result = await get_users_ordered_by_username(db_session) item = next(entry for entry in result if entry.id == user.id) assert isinstance(item, User) @@ -27,8 +27,8 @@ async def test_get_users_ordered_by_username(session: AsyncSession): assert isinstance(item.is_admin, bool) -async def test_get_users_ordered_by_username_ordering(session: AsyncSession): - session.add_all( +async def test_get_users_ordered_by_username_ordering(db_session: AsyncSession): + db_session.add_all( [ User( email="zeta@example.com", @@ -46,9 +46,9 @@ async def test_get_users_ordered_by_username_ordering(session: AsyncSession): ), ] ) - await session.commit() + await db_session.commit() - result = await get_users_ordered_by_username(session) + result = await get_users_ordered_by_username(db_session) usernames = [user.username for user in result] assert usernames == sorted(usernames) @@ -57,10 +57,10 @@ async def test_get_users_ordered_by_username_ordering(session: AsyncSession): assert alpha_index < zeta_index -async def test_get_users_ordered_by_username_read_only(session: AsyncSession): - before_count = await session.scalar(select(func.count()).select_from(User)) +async def test_get_users_ordered_by_username_read_only(db_session: AsyncSession): + before_count = await db_session.scalar(select(func.count()).select_from(User)) - _ = await get_users_ordered_by_username(session) + _ = await get_users_ordered_by_username(db_session) - after_count = await session.scalar(select(func.count()).select_from(User)) + after_count = await db_session.scalar(select(func.count()).select_from(User)) assert before_count == after_count diff --git a/server/app/tests/services/utilities.py b/server/app/tests/services/utilities.py index c1995dc8..c805d301 100644 --- a/server/app/tests/services/utilities.py +++ b/server/app/tests/services/utilities.py @@ -8,9 +8,9 @@ async def get_admin_user_public( - session: AsyncSession, settings: Settings + db_session: AsyncSession, settings: Settings ) -> UserPublic: - result = await session.execute( + result = await db_session.execute( select(User).where(User.username == settings.admin.username) ) admin = result.scalar_one() @@ -18,7 +18,7 @@ async def get_admin_user_public( return to_user_public(admin) -async def create_user(session: AsyncSession, username: str = "user") -> User: +async def create_user(db_session: AsyncSession, username: str = "user") -> User: user = User( username=username, email=f"{username}@example.com", @@ -27,6 +27,6 @@ async def create_user(session: AsyncSession, username: str = "user") -> User: password_hash="hash", is_admin=False, ) - session.add(user) - await session.commit() + db_session.add(user) + await db_session.commit() return user diff --git a/server/app/tests/services/workout/test_create_workout.py b/server/app/tests/services/workout/test_create_workout.py index d917721c..09e9c0e5 100644 --- a/server/app/tests/services/workout/test_create_workout.py +++ b/server/app/tests/services/workout/test_create_workout.py @@ -12,8 +12,8 @@ from ..utilities import create_user -async def test_create_workout(session: AsyncSession): - user = await create_user(session) +async def test_create_workout(db_session: AsyncSession): + user = await create_user(db_session) started_at = datetime(2024, 1, 1, tzinfo=UTC) ended_at = datetime(2024, 1, 1, 1, tzinfo=UTC) await create_workout( @@ -23,10 +23,10 @@ async def test_create_workout(session: AsyncSession): ended_at=ended_at, notes="Test workout", ), - session, + db_session, ) - workouts = await _query_workouts(session, False, Workout.user_id == user.id) + workouts = await _query_workouts(db_session, False, Workout.user_id == user.id) workout = workouts[0] if workouts else None assert workout is not None diff --git a/server/app/tests/services/workout/test_delete_workout.py b/server/app/tests/services/workout/test_delete_workout.py index 1c887eb3..5d383f8e 100644 --- a/server/app/tests/services/workout/test_delete_workout.py +++ b/server/app/tests/services/workout/test_delete_workout.py @@ -12,28 +12,28 @@ from .utilities import create_workout -async def test_delete_workout(session: AsyncSession): - user = await create_user(session) - workout = await create_workout(session, user.id) +async def test_delete_workout(db_session: AsyncSession): + user = await create_user(db_session) + workout = await create_workout(db_session, user.id) - await delete_workout(workout.id, user.id, session) + await delete_workout(workout.id, user.id, db_session) - workouts = await _query_workouts(session, True, Workout.id == workout.id) + workouts = await _query_workouts(db_session, True, Workout.id == workout.id) assert workouts == [] -async def test_delete_workout_not_found(session: AsyncSession): - user = await create_user(session) +async def test_delete_workout_not_found(db_session: AsyncSession): + user = await create_user(db_session) with pytest.raises(WorkoutNotFound): - await delete_workout(99999, user.id, session) + await delete_workout(99999, user.id, db_session) -async def test_delete_workout_not_allowed(session: AsyncSession): - user = await create_user(session) - user_2 = await create_user(session, username="user_2") +async def test_delete_workout_not_allowed(db_session: AsyncSession): + user = await create_user(db_session) + user_2 = await create_user(db_session, username="user_2") - workout = await create_workout(session, user.id) + workout = await create_workout(db_session, user.id) with pytest.raises(WorkoutNotFound): - await delete_workout(workout.id, user_2.id, session) + await delete_workout(workout.id, user_2.id, db_session) diff --git a/server/app/tests/services/workout/test_get_workout.py b/server/app/tests/services/workout/test_get_workout.py index 64361dab..1b99bf5e 100644 --- a/server/app/tests/services/workout/test_get_workout.py +++ b/server/app/tests/services/workout/test_get_workout.py @@ -8,31 +8,31 @@ from .utilities import create_workout -async def test_get_workout(session: AsyncSession): - user = await create_user(session) +async def test_get_workout(db_session: AsyncSession): + user = await create_user(db_session) workout = await create_workout( - session, + db_session, user_id=user.id, notes="Test workout", ) - result = await get_workout(workout.id, user.id, session) + result = await get_workout(workout.id, user.id, db_session) assert result.id == workout.id assert result.notes == "Test workout" -async def test_get_workout_not_found(session: AsyncSession): - user = await create_user(session) +async def test_get_workout_not_found(db_session: AsyncSession): + user = await create_user(db_session) with pytest.raises(WorkoutNotFound): - await get_workout(999, user.id, session) + await get_workout(999, user.id, db_session) -async def test_get_workout_not_allowed(session: AsyncSession): - user = await create_user(session) - user_2 = await create_user(session, username="user_2") - workout = await create_workout(session, user.id) +async def test_get_workout_not_allowed(db_session: AsyncSession): + user = await create_user(db_session) + user_2 = await create_user(db_session, username="user_2") + workout = await create_workout(db_session, user.id) with pytest.raises(WorkoutNotFound): - await get_workout(workout.id, user_2.id, session) + await get_workout(workout.id, user_2.id, db_session) diff --git a/server/app/tests/services/workout/test_get_workouts.py b/server/app/tests/services/workout/test_get_workouts.py index 90f37586..8c4072e4 100644 --- a/server/app/tests/services/workout/test_get_workouts.py +++ b/server/app/tests/services/workout/test_get_workouts.py @@ -7,15 +7,15 @@ async def test_get_workouts( - session: AsyncSession, + db_session: AsyncSession, ): - user_1 = await create_user(session, username="user_1") - user_2 = await create_user(session, username="user_2") + user_1 = await create_user(db_session, username="user_1") + user_2 = await create_user(db_session, username="user_2") - workout_1 = await create_workout(session, user_1.id) - await create_workout(session, user_2.id) + workout_1 = await create_workout(db_session, user_1.id) + await create_workout(db_session, user_2.id) - result = await get_workouts(user_1.id, session) + result = await get_workouts(user_1.id, db_session) assert len(result) == 1 assert result[0].id == workout_1.id diff --git a/server/app/tests/services/workout/test_query_workouts.py b/server/app/tests/services/workout/test_query_workouts.py index a518d4fb..e4fb1476 100644 --- a/server/app/tests/services/workout/test_query_workouts.py +++ b/server/app/tests/services/workout/test_query_workouts.py @@ -12,12 +12,12 @@ async def test_query_workouts_base( - session: AsyncSession, + db_session: AsyncSession, ): - user = await create_user(session) - workout = await create_workout(session, user.id) + user = await create_user(db_session) + workout = await create_workout(db_session, user.id) - result = await _query_workouts(session, True) + result = await _query_workouts(db_session, True) assert len(result) == 1 assert workout in result @@ -26,12 +26,12 @@ async def test_query_workouts_base( async def test_query_workouts_public( - session: AsyncSession, + db_session: AsyncSession, ): - user = await create_user(session) - workout = await create_workout(session, user.id) + user = await create_user(db_session) + workout = await create_workout(db_session, user.id) - result = await _query_workouts(session, False) + result = await _query_workouts(db_session, False) assert len(result) == 1 assert workout in result @@ -39,15 +39,15 @@ async def test_query_workouts_public( async def test_query_workouts_no_where_clause( - session: AsyncSession, + db_session: AsyncSession, ): - user_1 = await create_user(session, "user_1") - user_2 = await create_user(session, "user_2") + user_1 = await create_user(db_session, "user_1") + user_2 = await create_user(db_session, "user_2") - workout_1 = await create_workout(session, user_1.id) - workout_2 = await create_workout(session, user_2.id) + workout_1 = await create_workout(db_session, user_1.id) + workout_2 = await create_workout(db_session, user_2.id) - result = await _query_workouts(session, True) + result = await _query_workouts(db_session, True) assert len(result) == 2 assert workout_1 in result @@ -55,15 +55,15 @@ async def test_query_workouts_no_where_clause( async def test_query_workouts_with_where_clause( - session: AsyncSession, + db_session: AsyncSession, ): - user_1 = await create_user(session, "user_1") - user_2 = await create_user(session, "user_2") + user_1 = await create_user(db_session, "user_1") + user_2 = await create_user(db_session, "user_2") - workout_1 = await create_workout(session, user_1.id) - workout_2 = await create_workout(session, user_2.id) + workout_1 = await create_workout(db_session, user_1.id) + workout_2 = await create_workout(db_session, user_2.id) - result = await _query_workouts(session, True, Workout.user_id == user_1.id) + result = await _query_workouts(db_session, True, Workout.user_id == user_1.id) assert len(result) == 1 assert workout_1 in result @@ -71,22 +71,22 @@ async def test_query_workouts_with_where_clause( async def test_query_workouts_ordering( - session: AsyncSession, + db_session: AsyncSession, ): - user_1 = await create_user(session, "user_1") - user_2 = await create_user(session, "user_2") + user_1 = await create_user(db_session, "user_1") + user_2 = await create_user(db_session, "user_2") await create_workout( - session, + db_session, user_1.id, started_at=datetime.now(), ) await create_workout( - session, + db_session, user_2.id, started_at=datetime.now() + timedelta(minutes=5), ) - result = await _query_workouts(session, True) + result = await _query_workouts(db_session, True) assert result == sorted(result, key=lambda w: w.started_at, reverse=True) diff --git a/server/app/tests/services/workout/test_update_workout.py b/server/app/tests/services/workout/test_update_workout.py index 33a525a8..c5122890 100644 --- a/server/app/tests/services/workout/test_update_workout.py +++ b/server/app/tests/services/workout/test_update_workout.py @@ -11,9 +11,9 @@ from .utilities import create_workout -async def test_update_workout(session: AsyncSession): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) +async def test_update_workout(db_session: AsyncSession): + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) started_at = datetime(2024, 1, 1, tzinfo=UTC) ended_at = datetime(2024, 1, 1, 1, tzinfo=UTC) @@ -25,7 +25,7 @@ async def test_update_workout(session: AsyncSession): ended_at=ended_at, notes="Updated notes", ), - session, + db_session, ) assert workout.started_at == started_at @@ -33,38 +33,38 @@ async def test_update_workout(session: AsyncSession): assert workout.notes == "Updated notes" -async def test_update_workout_not_found(session: AsyncSession): - user = await create_user(session) +async def test_update_workout_not_found(db_session: AsyncSession): + user = await create_user(db_session) with pytest.raises(WorkoutNotFound): await update_workout( 99999, user.id, UpdateWorkoutRequest(), - session, + db_session, ) -async def test_update_workout_not_allowed(session: AsyncSession): - user = await create_user(session) - user_2 = await create_user(session, username="user_2") - workout = await create_workout(session, user_id=user.id) +async def test_update_workout_not_allowed(db_session: AsyncSession): + user = await create_user(db_session) + user_2 = await create_user(db_session, username="user_2") + workout = await create_workout(db_session, user_id=user.id) with pytest.raises(WorkoutNotFound): await update_workout( workout.id, user_2.id, UpdateWorkoutRequest(), - session, + db_session, ) -async def test_update_workout_no_changes(session: AsyncSession): - user = await create_user(session) +async def test_update_workout_no_changes(db_session: AsyncSession): + user = await create_user(db_session) started_at = datetime(2024, 1, 1, tzinfo=UTC) ended_at = datetime(2024, 1, 1, 1, tzinfo=UTC) workout = await create_workout( - session, + db_session, user_id=user.id, started_at=started_at, ended_at=ended_at, @@ -75,21 +75,21 @@ async def test_update_workout_no_changes(session: AsyncSession): workout.id, user.id, UpdateWorkoutRequest(), - session, + db_session, ) - workout = await get_workout(workout.id, user.id, session) + workout = await get_workout(workout.id, user.id, db_session) assert workout.started_at == started_at assert workout.ended_at == ended_at assert workout.notes == "Test workout" -async def test_update_workout_no_started_at(session: AsyncSession): - user = await create_user(session) +async def test_update_workout_no_started_at(db_session: AsyncSession): + user = await create_user(db_session) started_at = datetime(2024, 1, 1, tzinfo=UTC) workout = await create_workout( - session, + db_session, user_id=user.id, started_at=started_at, notes="Test workout", @@ -103,21 +103,21 @@ async def test_update_workout_no_started_at(session: AsyncSession): ended_at=new_ended_at, notes="Test workout updated", ), - session, + db_session, ) - workout = await get_workout(workout.id, user.id, session) + workout = await get_workout(workout.id, user.id, db_session) assert workout.started_at == started_at assert workout.ended_at == new_ended_at assert workout.notes == "Test workout updated" -async def test_update_workout_no_ended_at(session: AsyncSession): - user = await create_user(session) +async def test_update_workout_no_ended_at(db_session: AsyncSession): + user = await create_user(db_session) ended_at = datetime(2024, 1, 1, tzinfo=UTC) workout = await create_workout( - session, + db_session, user_id=user.id, ended_at=ended_at, notes="Test workout", @@ -131,20 +131,20 @@ async def test_update_workout_no_ended_at(session: AsyncSession): started_at=new_started_at, notes="Test workout updated", ), - session, + db_session, ) - workout = await get_workout(workout.id, user.id, session) + workout = await get_workout(workout.id, user.id, db_session) assert workout.started_at == new_started_at assert workout.ended_at == ended_at assert workout.notes == "Test workout updated" -async def test_update_workout_no_notes(session: AsyncSession): - user = await create_user(session) +async def test_update_workout_no_notes(db_session: AsyncSession): + user = await create_user(db_session) workout = await create_workout( - session, + db_session, user_id=user.id, notes="Test workout", ) @@ -158,22 +158,22 @@ async def test_update_workout_no_notes(session: AsyncSession): started_at=new_started_at, ended_at=new_ended_at, ), - session, + db_session, ) - workout = await get_workout(workout.id, user.id, session) + workout = await get_workout(workout.id, user.id, db_session) assert workout.started_at == new_started_at assert workout.ended_at == new_ended_at assert workout.notes == "Test workout" -async def test_update_workout_null_values(session: AsyncSession): - user = await create_user(session) +async def test_update_workout_null_values(db_session: AsyncSession): + user = await create_user(db_session) started_at = datetime(2024, 1, 1, tzinfo=UTC) ended_at = datetime(2024, 1, 1, 1, tzinfo=UTC) workout = await create_workout( - session, + db_session, user_id=user.id, started_at=started_at, ended_at=ended_at, @@ -187,10 +187,10 @@ async def test_update_workout_null_values(session: AsyncSession): ended_at=None, notes=None, ), - session, + db_session, ) - workout = await get_workout(workout.id, user.id, session) + workout = await get_workout(workout.id, user.id, db_session) assert workout.started_at == started_at assert workout.ended_at is None diff --git a/server/app/tests/services/workout/utilities.py b/server/app/tests/services/workout/utilities.py index e3d6395e..6a4b09a6 100644 --- a/server/app/tests/services/workout/utilities.py +++ b/server/app/tests/services/workout/utilities.py @@ -6,7 +6,7 @@ async def create_workout( - session: AsyncSession, + db_session: AsyncSession, user_id: int, started_at: datetime | None = None, ended_at: datetime | None = None, @@ -18,7 +18,7 @@ async def create_workout( ended_at=ended_at, notes=notes, ) - session.add(workout) - await session.commit() - await session.refresh(workout) + db_session.add(workout) + await db_session.commit() + await db_session.refresh(workout) return workout diff --git a/server/app/tests/services/workout_exercise/test_create_workout_exercise.py b/server/app/tests/services/workout_exercise/test_create_workout_exercise.py index d0cd730a..6fde3b52 100644 --- a/server/app/tests/services/workout_exercise/test_create_workout_exercise.py +++ b/server/app/tests/services/workout_exercise/test_create_workout_exercise.py @@ -21,10 +21,10 @@ from .utilities import create_workout_exercise as create_workout_exercise_util -async def test_create_workout_exercise(session: AsyncSession): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) - exercise = await create_exercise(session, name="Deadlift") +async def test_create_workout_exercise(db_session: AsyncSession): + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) + exercise = await create_exercise(db_session, name="Deadlift") await create_workout_exercise( workout.id, @@ -33,10 +33,10 @@ async def test_create_workout_exercise(session: AsyncSession): exercise_id=exercise.id, notes="Example exercise", ), - session, + db_session, ) - result = await session.execute( + result = await db_session.execute( select(WorkoutExercise).where( WorkoutExercise.workout_id == workout.id, WorkoutExercise.exercise_id == exercise.id, @@ -48,50 +48,50 @@ async def test_create_workout_exercise(session: AsyncSession): assert workout_exercise.notes == "Example exercise" -async def test_create_workout_exercise_workout_not_found(session: AsyncSession): +async def test_create_workout_exercise_workout_not_found(db_session: AsyncSession): with pytest.raises(WorkoutNotFound): await create_workout_exercise( workout_id=1, user_id=2, req=CreateWorkoutExerciseRequest(exercise_id=3), - db=session, + db=db_session, ) -async def test_create_workout_exercise_workout_not_allowed(session: AsyncSession): - user_1 = await create_user(session, username="user_1") - user_2 = await create_user(session, username="user_2") - workout = await create_workout(session, user_id=user_2.id) - exercise = await create_exercise(session, name="Squat") +async def test_create_workout_exercise_workout_not_allowed(db_session: AsyncSession): + user_1 = await create_user(db_session, username="user_1") + user_2 = await create_user(db_session, username="user_2") + workout = await create_workout(db_session, user_id=user_2.id) + exercise = await create_exercise(db_session, name="Squat") with pytest.raises(WorkoutNotFound): await create_workout_exercise( workout_id=workout.id, user_id=user_1.id, req=CreateWorkoutExerciseRequest(exercise_id=exercise.id), - db=session, + db=db_session, ) -async def test_create_workout_exercise_exercise_not_found(session: AsyncSession): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) +async def test_create_workout_exercise_exercise_not_found(db_session: AsyncSession): + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) with pytest.raises(ExerciseNotFound): await create_workout_exercise( workout_id=workout.id, user_id=user.id, req=CreateWorkoutExerciseRequest(exercise_id=99999), - db=session, + db=db_session, ) -async def test_create_workout_exercise_exercise_not_allowed(session: AsyncSession): - user_1 = await create_user(session, username="user_1") - user_2 = await create_user(session, username="user_2") - workout = await create_workout(session, user_id=user_1.id) +async def test_create_workout_exercise_exercise_not_allowed(db_session: AsyncSession): + user_1 = await create_user(db_session, username="user_1") + user_2 = await create_user(db_session, username="user_2") + workout = await create_workout(db_session, user_id=user_1.id) exercise = await create_exercise( - session, + db_session, name="Owned by other", user_id=user_2.id, ) @@ -101,20 +101,20 @@ async def test_create_workout_exercise_exercise_not_allowed(session: AsyncSessio workout.id, user_1.id, CreateWorkoutExerciseRequest(exercise_id=exercise.id), - session, + db_session, ) async def test_create_workout_exercise_position_conflict( - session: AsyncSession, + db_session: AsyncSession, monkeypatch: MonkeyPatch, ): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) - exercise = await create_exercise(session, name="Exercise 1") + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) + exercise = await create_exercise(db_session, name="Exercise 1") await create_workout_exercise_util( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, @@ -133,20 +133,20 @@ async def mock_get_next_position(workout_id: int, db: AsyncSession) -> int: workout.id, user.id, CreateWorkoutExerciseRequest(exercise_id=exercise.id), - session, + db_session, ) async def test_create_workout_exercise_unhandled_integrity_error( - session: AsyncSession, + db_session: AsyncSession, monkeypatch: MonkeyPatch, ): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) - exercise = await create_exercise(session, name="Exercise 1") + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) + exercise = await create_exercise(db_session, name="Exercise 1") await create_workout_exercise_util( - session, + db_session, workout_id=workout.id, exercise_id=exercise.id, position=1, @@ -166,5 +166,5 @@ async def mock_get_next_position(workout_id: int, db: AsyncSession) -> int: workout.id, user.id, CreateWorkoutExerciseRequest(exercise_id=exercise.id), - session, + db_session, ) diff --git a/server/app/tests/services/workout_exercise/test_delete_workout_exercise.py b/server/app/tests/services/workout_exercise/test_delete_workout_exercise.py index ab54a24b..539d8c20 100644 --- a/server/app/tests/services/workout_exercise/test_delete_workout_exercise.py +++ b/server/app/tests/services/workout_exercise/test_delete_workout_exercise.py @@ -16,21 +16,21 @@ from ..workout.utilities import create_workout -async def test_delete_workout_exercise(session: AsyncSession): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) - exercise = await create_exercise(session, name="Squat") +async def test_delete_workout_exercise(db_session: AsyncSession): + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) + exercise = await create_exercise(db_session, name="Squat") workout_exercise = WorkoutExercise( workout_id=workout.id, exercise_id=exercise.id, position=1, ) - session.add(workout_exercise) - await session.commit() + db_session.add(workout_exercise) + await db_session.commit() - await delete_workout_exercise(workout.id, workout_exercise.id, user.id, session) + await delete_workout_exercise(workout.id, workout_exercise.id, user.id, db_session) - result = await session.execute( + result = await db_session.execute( select(WorkoutExercise).where( WorkoutExercise.id == workout_exercise.id, ) @@ -38,33 +38,33 @@ async def test_delete_workout_exercise(session: AsyncSession): assert result.scalar_one_or_none() is None -async def test_delete_workout_exercise_workout_not_found(session: AsyncSession): +async def test_delete_workout_exercise_workout_not_found(db_session: AsyncSession): with pytest.raises(WorkoutNotFound): await delete_workout_exercise( workout_id=1, workout_exercise_id=2, user_id=3, - db=session, + db=db_session, ) -async def test_delete_workout_exercise_workout_not_allowed(session: AsyncSession): - user_1 = await create_user(session, username="user_1") - user_2 = await create_user(session, username="user_2") - workout = await create_workout(session, user_id=user_2.id) +async def test_delete_workout_exercise_workout_not_allowed(db_session: AsyncSession): + user_1 = await create_user(db_session, username="user_1") + user_2 = await create_user(db_session, username="user_2") + workout = await create_workout(db_session, user_id=user_2.id) with pytest.raises(WorkoutNotFound): await delete_workout_exercise( workout_id=workout.id, workout_exercise_id=1, user_id=user_1.id, - db=session, + db=db_session, ) -async def test_delete_workout_exercise_not_found(session: AsyncSession): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) +async def test_delete_workout_exercise_not_found(db_session: AsyncSession): + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) with pytest.raises(WorkoutExerciseNotFound): - await delete_workout_exercise(workout.id, 99999, user.id, session) + await delete_workout_exercise(workout.id, 99999, user.id, db_session) diff --git a/server/app/tests/services/workout_exercise/test_get_next_workout_exercise_position.py b/server/app/tests/services/workout_exercise/test_get_next_workout_exercise_position.py index fc23d283..3066eb53 100644 --- a/server/app/tests/services/workout_exercise/test_get_next_workout_exercise_position.py +++ b/server/app/tests/services/workout_exercise/test_get_next_workout_exercise_position.py @@ -10,23 +10,23 @@ from ..workout.utilities import create_workout -async def test_get_next_workout_exercise_position_empty(session: AsyncSession): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) +async def test_get_next_workout_exercise_position_empty(db_session: AsyncSession): + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) - position = await _get_next_workout_exercise_position(workout.id, session) + position = await _get_next_workout_exercise_position(workout.id, db_session) assert position == 1 -async def test_get_next_workout_exercise_position_max(session: AsyncSession): - user = await create_user(session) - workout = await create_workout(session, user_id=user.id) - other_workout = await create_workout(session, user_id=user.id) - exercise_1 = await create_exercise(session, name="Squat") - exercise_2 = await create_exercise(session, name="Bench") - exercise_3 = await create_exercise(session, name="Deadlift") +async def test_get_next_workout_exercise_position_max(db_session: AsyncSession): + user = await create_user(db_session) + workout = await create_workout(db_session, user_id=user.id) + other_workout = await create_workout(db_session, user_id=user.id) + exercise_1 = await create_exercise(db_session, name="Squat") + exercise_2 = await create_exercise(db_session, name="Bench") + exercise_3 = await create_exercise(db_session, name="Deadlift") - session.add_all( + db_session.add_all( [ WorkoutExercise( workout_id=workout.id, @@ -45,7 +45,7 @@ async def test_get_next_workout_exercise_position_max(session: AsyncSession): ), ] ) - await session.commit() + await db_session.commit() - position = await _get_next_workout_exercise_position(workout.id, session) + position = await _get_next_workout_exercise_position(workout.id, db_session) assert position == 4 diff --git a/server/app/tests/services/workout_exercise/utilities.py b/server/app/tests/services/workout_exercise/utilities.py index 00654015..cb814bda 100644 --- a/server/app/tests/services/workout_exercise/utilities.py +++ b/server/app/tests/services/workout_exercise/utilities.py @@ -4,7 +4,7 @@ async def create_workout_exercise( - session: AsyncSession, + db_session: AsyncSession, workout_id: int, exercise_id: int, position: int, @@ -16,7 +16,7 @@ async def create_workout_exercise( position=position, notes=notes, ) - session.add(workout_exercise) - await session.commit() - await session.refresh(workout_exercise) + db_session.add(workout_exercise) + await db_session.commit() + await db_session.refresh(workout_exercise) return workout_exercise From 13715cf269e9200f769e762f8557185f3a0c0408 Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Wed, 25 Mar 2026 18:08:59 -0500 Subject: [PATCH 18/27] server - add search service tests --- client/src/api/generated/index.ts | 2 +- client/src/api/generated/sdk.gen.ts | 18 ++- client/src/api/generated/types.gen.ts | 36 ++++++ client/src/api/generated/zod.gen.ts | 8 ++ server/app/api/endpoints/search.py | 23 +++- server/app/core/dependencies.py | 2 +- server/app/models/schemas/exercise.py | 8 ++ server/app/models/schemas/search.py | 3 +- server/app/services/search.py | 68 ++++++---- server/app/services/utilities/serializers.py | 12 +- .../core/dependencies/test_build_ms_client.py | 12 ++ .../core/dependencies/test_get_ms_client.py | 17 +++ server/app/tests/fixtures/client.py | 32 ++--- server/app/tests/fixtures/meilisearch.py | 39 ++++++ server/app/tests/fixtures/settings.py | 7 + .../serializers/test_to_exercise_document.py | 47 +++++++ .../tests/services/muscle_group/utilities.py | 15 +++ server/app/tests/services/search/__init__.py | 0 .../tests/services/search/test_get_task.py | 23 ++++ .../services/search/test_index_exercises.py | 40 ++++++ .../search/test_index_muscle_groups.py | 33 +++++ .../services/search/test_reindex_data.py | 26 ++++ .../services/search/test_search_exercises.py | 120 ++++++++++++++++++ .../search/test_search_muscle_groups.py | 45 +++++++ server/app/tests/services/search/utilities.py | 17 +++ server/conftest.py | 1 + server/pyproject.toml | 1 - server/uv.lock | 17 --- 28 files changed, 600 insertions(+), 72 deletions(-) create mode 100644 server/app/tests/core/dependencies/test_build_ms_client.py create mode 100644 server/app/tests/core/dependencies/test_get_ms_client.py create mode 100644 server/app/tests/fixtures/meilisearch.py create mode 100644 server/app/tests/services/_utilities/serializers/test_to_exercise_document.py create mode 100644 server/app/tests/services/search/__init__.py create mode 100644 server/app/tests/services/search/test_get_task.py create mode 100644 server/app/tests/services/search/test_index_exercises.py create mode 100644 server/app/tests/services/search/test_index_muscle_groups.py create mode 100644 server/app/tests/services/search/test_reindex_data.py create mode 100644 server/app/tests/services/search/test_search_exercises.py create mode 100644 server/app/tests/services/search/test_search_muscle_groups.py create mode 100644 server/app/tests/services/search/utilities.py diff --git a/client/src/api/generated/index.ts b/client/src/api/generated/index.ts index 3777a830..d7c4b70c 100644 --- a/client/src/api/generated/index.ts +++ b/client/src/api/generated/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts export { AdminService, AuthService, ExerciseService, FeedbackService, HealthService, MuscleGroupService, type Options, SearchService, SetService, UserService, WorkoutExerciseService, WorkoutService } from './sdk.gen'; -export type { AccessRequestPublic, AccessRequestStatus, ClientOptions, CreateExerciseData, CreateExerciseError, CreateExerciseErrors, CreateExerciseRequest, CreateExerciseResponse, CreateExerciseResponses, CreateFeedbackData, CreateFeedbackError, CreateFeedbackErrors, CreateFeedbackRequest, CreateFeedbackResponses, CreateSetData, CreateSetError, CreateSetErrors, CreateSetRequest, CreateSetResponse, CreateSetResponses, CreateWorkoutData, CreateWorkoutError, CreateWorkoutErrors, CreateWorkoutExerciseData, CreateWorkoutExerciseError, CreateWorkoutExerciseErrors, CreateWorkoutExerciseRequest, CreateWorkoutExerciseResponse, CreateWorkoutExerciseResponses, CreateWorkoutRequest, CreateWorkoutResponse, CreateWorkoutResponses, DeleteExerciseData, DeleteExerciseError, DeleteExerciseErrors, DeleteExerciseResponse, DeleteExerciseResponses, DeleteSetData, DeleteSetError, DeleteSetErrors, DeleteSetResponse, DeleteSetResponses, DeleteWorkoutData, DeleteWorkoutError, DeleteWorkoutErrors, DeleteWorkoutExerciseData, DeleteWorkoutExerciseError, DeleteWorkoutExerciseErrors, DeleteWorkoutExerciseResponse, DeleteWorkoutExerciseResponses, DeleteWorkoutResponse, DeleteWorkoutResponses, ErrorResponse, ExerciseBase, ExercisePublic, FeedbackType, ForgotPasswordData, ForgotPasswordError, ForgotPasswordErrors, ForgotPasswordRequest, ForgotPasswordResponse, ForgotPasswordResponses, GetAccessRequestsData, GetAccessRequestsError, GetAccessRequestsErrors, GetAccessRequestsResponse, GetAccessRequestsResponses, GetCurrentUserData, GetCurrentUserError, GetCurrentUserErrors, GetCurrentUserResponse, GetCurrentUserResponses, GetDbHealthData, GetDbHealthResponse, GetDbHealthResponses, GetExerciseData, GetExerciseError, GetExerciseErrors, GetExerciseResponse, GetExerciseResponses, GetExercisesData, GetExercisesError, GetExercisesErrors, GetExercisesResponse, GetExercisesResponses, GetHealthData, GetHealthResponse, GetHealthResponses, GetMuscleGroupsData, GetMuscleGroupsError, GetMuscleGroupsErrors, GetMuscleGroupsResponse, GetMuscleGroupsResponses, GetUsersData, GetUsersError, GetUsersErrors, GetUsersResponse, GetUsersResponses, GetWorkoutData, GetWorkoutError, GetWorkoutErrors, GetWorkoutResponse, GetWorkoutResponses, GetWorkoutsData, GetWorkoutsError, GetWorkoutsErrors, GetWorkoutsResponse, GetWorkoutsResponses, HttpValidationError, LoginData, LoginError, LoginErrors, LoginRequest, LoginResponse, LoginResponses, LogoutData, LogoutResponse, LogoutResponses, MuscleGroupPublic, RefreshTokenData, RefreshTokenError, RefreshTokenErrors, RefreshTokenResponse, RefreshTokenResponses, RegisterData, RegisterError, RegisterErrors, RegisterRequest, RegisterResponse, RegisterResponses, ReindexData, ReindexError, ReindexErrors, ReindexResponse, ReindexResponses, RequestAccessData, RequestAccessError, RequestAccessErrors, RequestAccessRequest, RequestAccessResponse, RequestAccessResponses, ResetPasswordData, ResetPasswordError, ResetPasswordErrors, ResetPasswordRequest, ResetPasswordResponse, ResetPasswordResponses, ReviewerPublic, SearchExercisesData, SearchExercisesError, SearchExercisesErrors, SearchExercisesResponses, SearchMuscleGroupsData, SearchMuscleGroupsError, SearchMuscleGroupsErrors, SearchMuscleGroupsResponses, SearchRequest, SetPublic, SetUnit, UpdateAccessRequestStatusData, UpdateAccessRequestStatusError, UpdateAccessRequestStatusErrors, UpdateAccessRequestStatusRequest, UpdateAccessRequestStatusResponse, UpdateAccessRequestStatusResponses, UpdateExerciseData, UpdateExerciseError, UpdateExerciseErrors, UpdateExerciseRequest, UpdateExerciseResponse, UpdateExerciseResponses, UpdateSetData, UpdateSetError, UpdateSetErrors, UpdateSetRequest, UpdateSetResponse, UpdateSetResponses, UpdateWorkoutData, UpdateWorkoutError, UpdateWorkoutErrors, UpdateWorkoutRequest, UpdateWorkoutResponse, UpdateWorkoutResponses, UserPublic, ValidationError, WorkoutBase, WorkoutExercisePublic, WorkoutPublic } from './types.gen'; +export type { AccessRequestPublic, AccessRequestStatus, ClientOptions, CreateExerciseData, CreateExerciseError, CreateExerciseErrors, CreateExerciseRequest, CreateExerciseResponse, CreateExerciseResponses, CreateFeedbackData, CreateFeedbackError, CreateFeedbackErrors, CreateFeedbackRequest, CreateFeedbackResponses, CreateSetData, CreateSetError, CreateSetErrors, CreateSetRequest, CreateSetResponse, CreateSetResponses, CreateWorkoutData, CreateWorkoutError, CreateWorkoutErrors, CreateWorkoutExerciseData, CreateWorkoutExerciseError, CreateWorkoutExerciseErrors, CreateWorkoutExerciseRequest, CreateWorkoutExerciseResponse, CreateWorkoutExerciseResponses, CreateWorkoutRequest, CreateWorkoutResponse, CreateWorkoutResponses, DeleteExerciseData, DeleteExerciseError, DeleteExerciseErrors, DeleteExerciseResponse, DeleteExerciseResponses, DeleteSetData, DeleteSetError, DeleteSetErrors, DeleteSetResponse, DeleteSetResponses, DeleteWorkoutData, DeleteWorkoutError, DeleteWorkoutErrors, DeleteWorkoutExerciseData, DeleteWorkoutExerciseError, DeleteWorkoutExerciseErrors, DeleteWorkoutExerciseResponse, DeleteWorkoutExerciseResponses, DeleteWorkoutResponse, DeleteWorkoutResponses, ErrorResponse, ExerciseBase, ExercisePublic, FeedbackType, ForgotPasswordData, ForgotPasswordError, ForgotPasswordErrors, ForgotPasswordRequest, ForgotPasswordResponse, ForgotPasswordResponses, GetAccessRequestsData, GetAccessRequestsError, GetAccessRequestsErrors, GetAccessRequestsResponse, GetAccessRequestsResponses, GetCurrentUserData, GetCurrentUserError, GetCurrentUserErrors, GetCurrentUserResponse, GetCurrentUserResponses, GetDbHealthData, GetDbHealthResponse, GetDbHealthResponses, GetExerciseData, GetExerciseError, GetExerciseErrors, GetExerciseResponse, GetExerciseResponses, GetExercisesData, GetExercisesError, GetExercisesErrors, GetExercisesResponse, GetExercisesResponses, GetHealthData, GetHealthResponse, GetHealthResponses, GetMuscleGroupsData, GetMuscleGroupsError, GetMuscleGroupsErrors, GetMuscleGroupsResponse, GetMuscleGroupsResponses, GetTaskData, GetTaskError, GetTaskErrors, GetTaskResponses, GetUsersData, GetUsersError, GetUsersErrors, GetUsersResponse, GetUsersResponses, GetWorkoutData, GetWorkoutError, GetWorkoutErrors, GetWorkoutResponse, GetWorkoutResponses, GetWorkoutsData, GetWorkoutsError, GetWorkoutsErrors, GetWorkoutsResponse, GetWorkoutsResponses, HttpValidationError, LoginData, LoginError, LoginErrors, LoginRequest, LoginResponse, LoginResponses, LogoutData, LogoutResponse, LogoutResponses, MuscleGroupPublic, RefreshTokenData, RefreshTokenError, RefreshTokenErrors, RefreshTokenResponse, RefreshTokenResponses, RegisterData, RegisterError, RegisterErrors, RegisterRequest, RegisterResponse, RegisterResponses, ReindexData, ReindexError, ReindexErrors, ReindexResponse, ReindexResponses, RequestAccessData, RequestAccessError, RequestAccessErrors, RequestAccessRequest, RequestAccessResponse, RequestAccessResponses, ResetPasswordData, ResetPasswordError, ResetPasswordErrors, ResetPasswordRequest, ResetPasswordResponse, ResetPasswordResponses, ReviewerPublic, SearchExercisesData, SearchExercisesError, SearchExercisesErrors, SearchExercisesResponses, SearchMuscleGroupsData, SearchMuscleGroupsError, SearchMuscleGroupsErrors, SearchMuscleGroupsResponses, SearchRequest, SetPublic, SetUnit, UpdateAccessRequestStatusData, UpdateAccessRequestStatusError, UpdateAccessRequestStatusErrors, UpdateAccessRequestStatusRequest, UpdateAccessRequestStatusResponse, UpdateAccessRequestStatusResponses, UpdateExerciseData, UpdateExerciseError, UpdateExerciseErrors, UpdateExerciseRequest, UpdateExerciseResponse, UpdateExerciseResponses, UpdateSetData, UpdateSetError, UpdateSetErrors, UpdateSetRequest, UpdateSetResponse, UpdateSetResponses, UpdateWorkoutData, UpdateWorkoutError, UpdateWorkoutErrors, UpdateWorkoutRequest, UpdateWorkoutResponse, UpdateWorkoutResponses, UserPublic, ValidationError, WorkoutBase, WorkoutExercisePublic, WorkoutPublic } from './types.gen'; diff --git a/client/src/api/generated/sdk.gen.ts b/client/src/api/generated/sdk.gen.ts index 2fcb8c66..ff9f89e3 100644 --- a/client/src/api/generated/sdk.gen.ts +++ b/client/src/api/generated/sdk.gen.ts @@ -2,7 +2,7 @@ import { type Client, formDataBodySerializer, type Options as Options2, type TDataShape } from './client'; import { client } from './client.gen'; -import type { CreateExerciseData, CreateExerciseErrors, CreateExerciseResponses, CreateFeedbackData, CreateFeedbackErrors, CreateFeedbackResponses, CreateSetData, CreateSetErrors, CreateSetResponses, CreateWorkoutData, CreateWorkoutErrors, CreateWorkoutExerciseData, CreateWorkoutExerciseErrors, CreateWorkoutExerciseResponses, CreateWorkoutResponses, DeleteExerciseData, DeleteExerciseErrors, DeleteExerciseResponses, DeleteSetData, DeleteSetErrors, DeleteSetResponses, DeleteWorkoutData, DeleteWorkoutErrors, DeleteWorkoutExerciseData, DeleteWorkoutExerciseErrors, DeleteWorkoutExerciseResponses, DeleteWorkoutResponses, ForgotPasswordData, ForgotPasswordErrors, ForgotPasswordResponses, GetAccessRequestsData, GetAccessRequestsErrors, GetAccessRequestsResponses, GetCurrentUserData, GetCurrentUserErrors, GetCurrentUserResponses, GetDbHealthData, GetDbHealthResponses, GetExerciseData, GetExerciseErrors, GetExerciseResponses, GetExercisesData, GetExercisesErrors, GetExercisesResponses, GetHealthData, GetHealthResponses, GetMuscleGroupsData, GetMuscleGroupsErrors, GetMuscleGroupsResponses, GetUsersData, GetUsersErrors, GetUsersResponses, GetWorkoutData, GetWorkoutErrors, GetWorkoutResponses, GetWorkoutsData, GetWorkoutsErrors, GetWorkoutsResponses, LoginData, LoginErrors, LoginResponses, LogoutData, LogoutResponses, RefreshTokenData, RefreshTokenErrors, RefreshTokenResponses, RegisterData, RegisterErrors, RegisterResponses, ReindexData, ReindexErrors, ReindexResponses, RequestAccessData, RequestAccessErrors, RequestAccessResponses, ResetPasswordData, ResetPasswordErrors, ResetPasswordResponses, SearchExercisesData, SearchExercisesErrors, SearchExercisesResponses, SearchMuscleGroupsData, SearchMuscleGroupsErrors, SearchMuscleGroupsResponses, UpdateAccessRequestStatusData, UpdateAccessRequestStatusErrors, UpdateAccessRequestStatusResponses, UpdateExerciseData, UpdateExerciseErrors, UpdateExerciseResponses, UpdateSetData, UpdateSetErrors, UpdateSetResponses, UpdateWorkoutData, UpdateWorkoutErrors, UpdateWorkoutResponses } from './types.gen'; +import type { CreateExerciseData, CreateExerciseErrors, CreateExerciseResponses, CreateFeedbackData, CreateFeedbackErrors, CreateFeedbackResponses, CreateSetData, CreateSetErrors, CreateSetResponses, CreateWorkoutData, CreateWorkoutErrors, CreateWorkoutExerciseData, CreateWorkoutExerciseErrors, CreateWorkoutExerciseResponses, CreateWorkoutResponses, DeleteExerciseData, DeleteExerciseErrors, DeleteExerciseResponses, DeleteSetData, DeleteSetErrors, DeleteSetResponses, DeleteWorkoutData, DeleteWorkoutErrors, DeleteWorkoutExerciseData, DeleteWorkoutExerciseErrors, DeleteWorkoutExerciseResponses, DeleteWorkoutResponses, ForgotPasswordData, ForgotPasswordErrors, ForgotPasswordResponses, GetAccessRequestsData, GetAccessRequestsErrors, GetAccessRequestsResponses, GetCurrentUserData, GetCurrentUserErrors, GetCurrentUserResponses, GetDbHealthData, GetDbHealthResponses, GetExerciseData, GetExerciseErrors, GetExerciseResponses, GetExercisesData, GetExercisesErrors, GetExercisesResponses, GetHealthData, GetHealthResponses, GetMuscleGroupsData, GetMuscleGroupsErrors, GetMuscleGroupsResponses, GetTaskData, GetTaskErrors, GetTaskResponses, GetUsersData, GetUsersErrors, GetUsersResponses, GetWorkoutData, GetWorkoutErrors, GetWorkoutResponses, GetWorkoutsData, GetWorkoutsErrors, GetWorkoutsResponses, LoginData, LoginErrors, LoginResponses, LogoutData, LogoutResponses, RefreshTokenData, RefreshTokenErrors, RefreshTokenResponses, RegisterData, RegisterErrors, RegisterResponses, ReindexData, ReindexErrors, ReindexResponses, RequestAccessData, RequestAccessErrors, RequestAccessResponses, ResetPasswordData, ResetPasswordErrors, ResetPasswordResponses, SearchExercisesData, SearchExercisesErrors, SearchExercisesResponses, SearchMuscleGroupsData, SearchMuscleGroupsErrors, SearchMuscleGroupsResponses, UpdateAccessRequestStatusData, UpdateAccessRequestStatusErrors, UpdateAccessRequestStatusResponses, UpdateExerciseData, UpdateExerciseErrors, UpdateExerciseResponses, UpdateSetData, UpdateSetErrors, UpdateSetResponses, UpdateWorkoutData, UpdateWorkoutErrors, UpdateWorkoutResponses } from './types.gen'; export type Options = Options2 & { /** @@ -319,6 +319,22 @@ export class MuscleGroupService { } export class SearchService { + /** + * Get Task Endpoint + */ + public static getTask(options: Options) { + return (options.client ?? client).get({ + responseType: 'json', + security: [{ + in: 'cookie', + name: 'access_token', + type: 'apiKey' + }], + url: '/api/search/tasks/{task_id}', + ...options + }); + } + /** * Reindex Endpoint */ diff --git a/client/src/api/generated/types.gen.ts b/client/src/api/generated/types.gen.ts index a61377ff..9e231763 100644 --- a/client/src/api/generated/types.gen.ts +++ b/client/src/api/generated/types.gen.ts @@ -1213,6 +1213,42 @@ export type GetMuscleGroupsResponses = { export type GetMuscleGroupsResponse = GetMuscleGroupsResponses[keyof GetMuscleGroupsResponses]; +export type GetTaskData = { + body?: never; + path: { + /** + * Task Id + */ + task_id: number; + }; + query?: never; + url: '/api/search/tasks/{task_id}'; +}; + +export type GetTaskErrors = { + /** + * Unauthorized + */ + 401: ErrorResponse; + /** + * Forbidden + */ + 403: ErrorResponse; + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetTaskError = GetTaskErrors[keyof GetTaskErrors]; + +export type GetTaskResponses = { + /** + * Successful Response + */ + 200: unknown; +}; + export type ReindexData = { body?: never; path?: never; diff --git a/client/src/api/generated/zod.gen.ts b/client/src/api/generated/zod.gen.ts index 408b8b46..6517e94e 100644 --- a/client/src/api/generated/zod.gen.ts +++ b/client/src/api/generated/zod.gen.ts @@ -537,6 +537,14 @@ export const zGetMuscleGroupsData = z.object({ */ export const zGetMuscleGroupsResponse = z.array(zMuscleGroupPublic); +export const zGetTaskData = z.object({ + body: z.never().optional(), + path: z.object({ + task_id: z.int() + }), + query: z.never().optional() +}); + export const zReindexData = z.object({ body: z.never().optional(), path: z.never().optional(), diff --git a/server/app/api/endpoints/search.py b/server/app/api/endpoints/search.py index 403b98c2..cdd26464 100644 --- a/server/app/api/endpoints/search.py +++ b/server/app/api/endpoints/search.py @@ -13,7 +13,12 @@ from app.models.schemas.errors import ErrorResponseModel from app.models.schemas.search import SearchRequest from app.models.schemas.user import UserPublic -from app.services.search import reindex_data, search_exercises, search_muscle_groups +from app.services.search import ( + get_task, + reindex_data, + search_exercises, + search_muscle_groups, +) api_router = APIRouter( prefix="/search", @@ -22,6 +27,22 @@ ) +@api_router.get( + "/tasks/{task_id}", + operation_id="getTask", + responses={ + status.HTTP_401_UNAUTHORIZED: ErrorResponseModel, + status.HTTP_403_FORBIDDEN: ErrorResponseModel, + }, +) +async def get_task_endpoint( + task_id: int, + _: Annotated[UserPublic, Depends(get_current_admin)], + ms_client: Annotated[AsyncClient, Depends(get_ms_client)], +): + return await get_task(ms_client, task_id) + + @api_router.post( "/reindex", operation_id="reindex", diff --git a/server/app/core/dependencies.py b/server/app/core/dependencies.py index 51b45d4a..f8b2a9e6 100644 --- a/server/app/core/dependencies.py +++ b/server/app/core/dependencies.py @@ -55,7 +55,7 @@ def build_ms_client(host: str, port: int, master_key: str) -> AsyncClient: return AsyncClient(url, master_key) -def get_ms_client( +async def get_ms_client( settings: Annotated[Settings, Depends(get_settings)], ) -> AsyncClient: return build_ms_client( diff --git a/server/app/models/schemas/exercise.py b/server/app/models/schemas/exercise.py index 2cfba51c..5439ffa7 100644 --- a/server/app/models/schemas/exercise.py +++ b/server/app/models/schemas/exercise.py @@ -20,6 +20,14 @@ class ExercisePublic(ExerciseBase): muscle_groups: list[MuscleGroupPublic] +class ExerciseDocument(BaseModel): + id: int + user_id: int | None + name: str + description: str | None + muscle_group_names: list[str] + + class CreateExerciseRequest(BaseModel): name: ExerciseName description: ExerciseDescription | None = None diff --git a/server/app/models/schemas/search.py b/server/app/models/schemas/search.py index bf3e62c1..10964d9d 100644 --- a/server/app/models/schemas/search.py +++ b/server/app/models/schemas/search.py @@ -1,6 +1,5 @@ from meilisearch_python_sdk.models.search import SearchResults from pydantic import BaseModel -from pydantic.generics import GenericModel from app.models.schemas.types import SearchQuery @@ -10,6 +9,6 @@ class SearchRequest(BaseModel): limit: int -class SearchResponse[T: BaseModel](GenericModel): +class SearchResponse[T: BaseModel](BaseModel): query: str results: SearchResults[T] diff --git a/server/app/services/search.py b/server/app/services/search.py index eda68a20..60170dc9 100644 --- a/server/app/services/search.py +++ b/server/app/services/search.py @@ -9,45 +9,66 @@ from app.models.database.muscle_group import MuscleGroup from app.models.enums import SearchIndex -from app.models.schemas.exercise import ExercisePublic +from app.models.schemas.exercise import ExerciseDocument from app.models.schemas.muscle_group import MuscleGroupPublic from app.models.schemas.search import SearchRequest from app.services.utilities.queries import query_exercises from app.services.utilities.serializers import ( - to_exercise_public, + to_exercise_document, to_muscle_group_public, ) logger = logging.getLogger(__name__) +async def get_task( + ms_client: AsyncClient, + task_id: int, +): + return await ms_client.get_task(task_id) + + async def reindex_data( db: AsyncSession, ms_client: AsyncClient, ): - await _index_muscle_groups(db, ms_client) - await _index_exercises(db, ms_client) + task = await _index_muscle_groups(db, ms_client) + logger.info(f"Reindexing muscle groups with task id: {task}") + + task = await _index_exercises(db, ms_client) + logger.info(f"Reindexing exercises with task id: {task}") async def _index_muscle_groups( db: AsyncSession, ms_client: AsyncClient, -): +) -> int: result = await db.execute(select(MuscleGroup)) muscle_groups = result.scalars().all() - public = [to_muscle_group_public(mg) for mg in muscle_groups] + docs = [to_muscle_group_public(mg) for mg in muscle_groups] await ms_client.delete_index_if_exists(SearchIndex.MUSCLE_GROUPS) + + settings = MeilisearchSettings( + searchable_attributes=[ + "name", + "description", + ], + ) + index = await ms_client.get_or_create_index(SearchIndex.MUSCLE_GROUPS) - await index.add_documents([mg.model_dump() for mg in public]) + await index.update_settings(settings) + + task = await index.add_documents([doc.model_dump() for doc in docs]) + return task.task_uid async def _index_exercises( db: AsyncSession, ms_client: AsyncClient, -): +) -> int: exercises = await query_exercises(db, base=False) - public = [to_exercise_public(e) for e in exercises] + docs = [to_exercise_document(e) for e in exercises] await ms_client.delete_index_if_exists(SearchIndex.EXERCISES) @@ -55,15 +76,7 @@ async def _index_exercises( searchable_attributes=[ "name", "description", - "muscle_groups.name", - ], - displayed_attributes=[ - "id", - "user_id", - "name", - "description", - "muscle_groups.id", - "muscle_groups.name", + "muscle_group_names", ], filterable_attributes=[ "user_id", @@ -72,15 +85,12 @@ async def _index_exercises( index = await ms_client.get_or_create_index(SearchIndex.EXERCISES) await index.update_settings(settings) - await index.add_documents( - [ - e.model_dump( - exclude={"created_at", "updated_at"}, - ) - for e in public - ], + + task = await index.add_documents( + [doc.model_dump() for doc in docs], primary_key="id", ) + return task.task_uid async def search_muscle_groups( @@ -99,16 +109,18 @@ async def search_muscle_groups( async def search_exercises( req: SearchRequest, - user_id: int, + user_id: int | None, ms_client: AsyncClient, ): logger.info(f"Searching exercises with query: '{req.query}' and user_id: {user_id}") return await _search( - model=ExercisePublic, + model=ExerciseDocument, ms_client=ms_client, index=SearchIndex.EXERCISES, query=req.query, - filter=f"user_id IS NULL OR user_id = {user_id}", + filter=f"user_id IS NULL OR user_id = {user_id}" + if user_id is not None + else None, limit=req.limit, ) diff --git a/server/app/services/utilities/serializers.py b/server/app/services/utilities/serializers.py index ccc65fea..d0251c0b 100644 --- a/server/app/services/utilities/serializers.py +++ b/server/app/services/utilities/serializers.py @@ -6,7 +6,7 @@ from app.models.database.workout import Workout from app.models.database.workout_exercise import WorkoutExercise from app.models.schemas.access_request import AccessRequestPublic, ReviewerPublic -from app.models.schemas.exercise import ExerciseBase, ExercisePublic +from app.models.schemas.exercise import ExerciseBase, ExerciseDocument, ExercisePublic from app.models.schemas.muscle_group import MuscleGroupPublic from app.models.schemas.set import SetPublic from app.models.schemas.user import UserPublic @@ -66,6 +66,16 @@ def to_exercise_public(exercise: Exercise) -> ExercisePublic: ) +def to_exercise_document(exercise: Exercise) -> ExerciseDocument: + return ExerciseDocument( + id=exercise.id, + user_id=exercise.user_id, + name=exercise.name, + description=exercise.description, + muscle_group_names=[emg.muscle_group.name for emg in exercise.muscle_groups], + ) + + def to_set_public(set_: Set) -> SetPublic: return SetPublic.model_validate(set_, from_attributes=True) diff --git a/server/app/tests/core/dependencies/test_build_ms_client.py b/server/app/tests/core/dependencies/test_build_ms_client.py new file mode 100644 index 00000000..17dbe0a0 --- /dev/null +++ b/server/app/tests/core/dependencies/test_build_ms_client.py @@ -0,0 +1,12 @@ +from app.core.dependencies import build_ms_client + + +def test_build_ms_client_cached(): + host = "localhost" + port = 7700 + master_key = "masterkey" + + client_a = build_ms_client(host, port, master_key) + client_b = build_ms_client(host, port, master_key) + + assert client_a is client_b diff --git a/server/app/tests/core/dependencies/test_get_ms_client.py b/server/app/tests/core/dependencies/test_get_ms_client.py new file mode 100644 index 00000000..100873c2 --- /dev/null +++ b/server/app/tests/core/dependencies/test_get_ms_client.py @@ -0,0 +1,17 @@ +from meilisearch_python_sdk import AsyncClient + +from app.core.config import Settings +from app.core.dependencies import get_ms_client + + +async def test_get_ms_client(anyio_backend: str, settings: Settings): + _ = anyio_backend + + client = await get_ms_client(settings) + assert isinstance(client, AsyncClient) + + url = f"http://{settings.ms.host}:{settings.ms.port}" + assert client.http_client.base_url == url + + headers = {"Authorization": f"Bearer {settings.ms.master_key}"} + assert client._headers == headers # pyright: ignore[reportPrivateUsage] diff --git a/server/app/tests/fixtures/client.py b/server/app/tests/fixtures/client.py index 3ab15588..afe79472 100644 --- a/server/app/tests/fixtures/client.py +++ b/server/app/tests/fixtures/client.py @@ -4,15 +4,12 @@ import pytest from fastapi import FastAPI from httpx import ASGITransport, AsyncClient -from sqlalchemy.ext.asyncio import ( - AsyncConnection, - AsyncSession, - AsyncTransaction, -) +from meilisearch_python_sdk import AsyncClient as MSAsyncClient +from sqlalchemy.ext.asyncio import AsyncSession from app import create_app from app.core.config import Settings, get_settings -from app.core.dependencies import get_db +from app.core.dependencies import get_db, get_ms_client from app.services.email import EmailService, get_email_service from app.services.github import GitHubService, get_github_service @@ -30,8 +27,8 @@ def fastapi_app(settings: Settings) -> FastAPI: async def client( fastapi_app: FastAPI, settings: Settings, - db_connection: AsyncConnection, - db_transaction: AsyncTransaction, + db_session: AsyncSession, + ms_client: MSAsyncClient, mock_email_svc: EmailService, mock_github_svc: GitHubService, ) -> AsyncGenerator[AsyncClient]: @@ -41,28 +38,25 @@ async def override_get_settings() -> Settings: return settings async def override_get_db() -> AsyncGenerator[AsyncSession]: - async_session = AsyncSession( - bind=db_connection, - join_transaction_mode="create_savepoint", - expire_on_commit=False, - ) - async with async_session: - yield async_session + yield db_session + + async def override_get_ms_client() -> AsyncGenerator[MSAsyncClient]: + yield ms_client fastapi_app.dependency_overrides[get_settings] = override_get_settings fastapi_app.dependency_overrides[get_db] = override_get_db + fastapi_app.dependency_overrides[get_ms_client] = override_get_ms_client fastapi_app.dependency_overrides[get_email_service] = lambda: mock_email_svc fastapi_app.dependency_overrides[get_github_service] = lambda: mock_github_svc try: yield AsyncClient( - transport=ASGITransport(app=fastapi_app), base_url="http://test" + transport=ASGITransport(app=fastapi_app), + base_url="http://test", ) finally: del fastapi_app.dependency_overrides[get_settings] del fastapi_app.dependency_overrides[get_db] + del fastapi_app.dependency_overrides[get_ms_client] del fastapi_app.dependency_overrides[get_email_service] del fastapi_app.dependency_overrides[get_github_service] - - if db_transaction.is_active: - await db_transaction.rollback() diff --git a/server/app/tests/fixtures/meilisearch.py b/server/app/tests/fixtures/meilisearch.py new file mode 100644 index 00000000..1363d8a5 --- /dev/null +++ b/server/app/tests/fixtures/meilisearch.py @@ -0,0 +1,39 @@ +from collections.abc import AsyncGenerator + +import pytest +from meilisearch_python_sdk import AsyncClient +from testcontainers.core.container import ( # pyright: ignore[reportMissingTypeStubs] + DockerContainer, +) +from testcontainers.core.wait_strategies import ( # pyright: ignore[reportMissingTypeStubs] + LogMessageWaitStrategy, +) + + +@pytest.fixture(scope="session") +async def ms_container(): + with ( + DockerContainer("getmeili/meilisearch:v1.40.0") + .with_exposed_ports(7700) + .with_env("MEILI_MASTER_KEY", "masterkey") + .waiting_for(LogMessageWaitStrategy("listening on:")) + ) as container: + host = container.get_container_host_ip() + port = container.get_exposed_port(7700) + yield host, port, "masterkey" + + +@pytest.fixture() +async def ms_client( + ms_container: tuple[str, int, str], +) -> AsyncGenerator[AsyncClient]: + host, port, master_key = ms_container + + client = AsyncClient(f"http://{host}:{port}", master_key) + + try: + yield client + finally: + indexes = await client.get_indexes() or [] + for idx in indexes: + await client.delete_index_if_exists(idx.uid) diff --git a/server/app/tests/fixtures/settings.py b/server/app/tests/fixtures/settings.py index ebeb1046..3ffbf4ad 100644 --- a/server/app/tests/fixtures/settings.py +++ b/server/app/tests/fixtures/settings.py @@ -7,6 +7,7 @@ EmailConsoleSettings, GitHubConsoleSettings, JWTSettings, + MeilisearchSettings, Settings, ) @@ -30,6 +31,11 @@ user="test", password="pw", ) +TEST_MS_SETTINGS = MeilisearchSettings( + host="localhost", + port=7700, + master_key="masterkey", +) TEST_EMAIL_SETTINGS = EmailConsoleSettings( backend="console", ) @@ -43,6 +49,7 @@ admin=TEST_ADMIN_SETTINGS, jwt=TEST_JWT_SETTINGS, db=TEST_DB_SETTINGS, + ms=TEST_MS_SETTINGS, email=TEST_EMAIL_SETTINGS, gh=TEST_GH_SETTINGS, ) diff --git a/server/app/tests/services/_utilities/serializers/test_to_exercise_document.py b/server/app/tests/services/_utilities/serializers/test_to_exercise_document.py new file mode 100644 index 00000000..0da03f09 --- /dev/null +++ b/server/app/tests/services/_utilities/serializers/test_to_exercise_document.py @@ -0,0 +1,47 @@ +from datetime import UTC, datetime + +from app.models.database.exercise import Exercise +from app.models.database.exercise_muscle_group import ExerciseMuscleGroup +from app.models.database.muscle_group import MuscleGroup +from app.models.schemas.exercise import ExerciseDocument +from app.services.utilities.serializers import to_exercise_document + + +def test_to_exercise_document() -> None: + exercise = Exercise( + id=1, + user_id=2, + name="Bench Press", + description="Flat bench", + ) + + result = to_exercise_document(exercise) + + assert isinstance(result, ExerciseDocument) + assert result.id == 1 + assert result.user_id == 2 + assert result.name == "Bench Press" + assert result.description == "Flat bench" + assert result.muscle_group_names == [] + + +def test_to_exercise_document_with_muscle_groups() -> None: + chest = MuscleGroup(id=1, name="chest", description="Chest muscles") + triceps = MuscleGroup(id=2, name="triceps", description="Triceps muscles") + + exercise = Exercise( + id=11, + user_id=None, + name="Push-up", + description=None, + created_at=datetime(2026, 1, 3, tzinfo=UTC), + updated_at=datetime(2026, 1, 4, tzinfo=UTC), + ) + exercise.muscle_groups = [ + ExerciseMuscleGroup(exercise_id=11, muscle_group_id=1, muscle_group=chest), + ExerciseMuscleGroup(exercise_id=11, muscle_group_id=2, muscle_group=triceps), + ] + + result = to_exercise_document(exercise) + + assert result.muscle_group_names == ["chest", "triceps"] diff --git a/server/app/tests/services/muscle_group/utilities.py b/server/app/tests/services/muscle_group/utilities.py index acd1e751..dc9f5c68 100644 --- a/server/app/tests/services/muscle_group/utilities.py +++ b/server/app/tests/services/muscle_group/utilities.py @@ -10,3 +10,18 @@ async def get_muscle_group_id(db_session: AsyncSession, name: str) -> int: ) muscle_group = result.scalar_one() return muscle_group.id + + +async def create_muscle_group( + db_session: AsyncSession, + name: str, + description: str, +) -> MuscleGroup: + muscle_group = MuscleGroup( + name=name, + description=description, + ) + db_session.add(muscle_group) + await db_session.commit() + await db_session.refresh(muscle_group) + return muscle_group diff --git a/server/app/tests/services/search/__init__.py b/server/app/tests/services/search/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/app/tests/services/search/test_get_task.py b/server/app/tests/services/search/test_get_task.py new file mode 100644 index 00000000..4d95a00e --- /dev/null +++ b/server/app/tests/services/search/test_get_task.py @@ -0,0 +1,23 @@ +from meilisearch_python_sdk import AsyncClient +from meilisearch_python_sdk.models.task import TaskResult +from sqlalchemy.ext.asyncio import AsyncSession + +from app.services.search import ( # pyright: ignore[reportPrivateUsage] + _index_muscle_groups, + get_task, +) + +from .utilities import wait_for_task + + +async def test_get_task( + db_session: AsyncSession, + ms_client: AsyncClient, +): + task = await _index_muscle_groups(db_session, ms_client) + await wait_for_task(ms_client, task) + + task = await get_task(ms_client, task) + assert task is not None + assert isinstance(task, TaskResult) + assert task.status == "succeeded" diff --git a/server/app/tests/services/search/test_index_exercises.py b/server/app/tests/services/search/test_index_exercises.py new file mode 100644 index 00000000..293bf700 --- /dev/null +++ b/server/app/tests/services/search/test_index_exercises.py @@ -0,0 +1,40 @@ +from meilisearch_python_sdk import AsyncClient +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.enums import SearchIndex +from app.models.schemas.exercise import ExerciseDocument +from app.services.search import _index_exercises # pyright: ignore[reportPrivateUsage] + +from ..exercise.utilities import create_exercise +from ..muscle_group.utilities import create_muscle_group +from .utilities import wait_for_task + + +async def test_index_exercises( + db_session: AsyncSession, + ms_client: AsyncClient, +): + mg = await create_muscle_group( + db_session, + name="muscle group 1", + description="Muscle group 1", + ) + exercise = await create_exercise( + db_session, + name="exercise 1", + description="Exercise 1", + muscle_group_ids=[mg.id], + ) + + task = await _index_exercises(db_session, ms_client) + await wait_for_task(ms_client, task) + + index = await ms_client.get_or_create_index(SearchIndex.EXERCISES) + doc = await index.get_document(str(exercise.id)) + + ExerciseDocument.model_validate(doc) + assert doc["id"] == exercise.id + assert doc["user_id"] == exercise.user_id + assert doc["name"] == exercise.name + assert doc["description"] == exercise.description + assert doc["muscle_group_names"] == [mg.name] diff --git a/server/app/tests/services/search/test_index_muscle_groups.py b/server/app/tests/services/search/test_index_muscle_groups.py new file mode 100644 index 00000000..1fe0768f --- /dev/null +++ b/server/app/tests/services/search/test_index_muscle_groups.py @@ -0,0 +1,33 @@ +from meilisearch_python_sdk import AsyncClient +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.enums import SearchIndex +from app.models.schemas.muscle_group import MuscleGroupPublic +from app.services.search import ( + _index_muscle_groups, # pyright: ignore[reportPrivateUsage] +) + +from ..muscle_group.utilities import create_muscle_group +from .utilities import wait_for_task + + +async def test_index_muscle_groups( + db_session: AsyncSession, + ms_client: AsyncClient, +): + mg = await create_muscle_group( + db_session, + name="muscle group 1", + description="Muscle group 1", + ) + + task = await _index_muscle_groups(db_session, ms_client) + await wait_for_task(ms_client, task) + + index = await ms_client.get_or_create_index(SearchIndex.MUSCLE_GROUPS) + doc = await index.get_document(str(mg.id)) + + MuscleGroupPublic.model_validate(doc) + assert doc["id"] == mg.id + assert doc["name"] == mg.name + assert doc["description"] == mg.description diff --git a/server/app/tests/services/search/test_reindex_data.py b/server/app/tests/services/search/test_reindex_data.py new file mode 100644 index 00000000..c198ac2e --- /dev/null +++ b/server/app/tests/services/search/test_reindex_data.py @@ -0,0 +1,26 @@ +from meilisearch_python_sdk import AsyncClient +from pytest import MonkeyPatch +from sqlalchemy.ext.asyncio import AsyncSession + +from app.services.search import reindex_data + + +async def test_reindex_data( + db_session: AsyncSession, + ms_client: AsyncClient, + monkeypatch: MonkeyPatch, +): + calls: list[str] = [] + + async def fake_muscle_groups(db: AsyncSession, client: AsyncClient) -> None: + calls.append("muscle") + + async def fake_exercises(db: AsyncSession, client: AsyncClient) -> None: + calls.append("exercise") + + monkeypatch.setattr("app.services.search._index_muscle_groups", fake_muscle_groups) + monkeypatch.setattr("app.services.search._index_exercises", fake_exercises) + + await reindex_data(db_session, ms_client) + + assert calls == ["muscle", "exercise"] diff --git a/server/app/tests/services/search/test_search_exercises.py b/server/app/tests/services/search/test_search_exercises.py new file mode 100644 index 00000000..c1f974f9 --- /dev/null +++ b/server/app/tests/services/search/test_search_exercises.py @@ -0,0 +1,120 @@ +import logging + +from meilisearch_python_sdk import AsyncClient +from meilisearch_python_sdk.models.search import SearchResults +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.schemas.exercise import ExerciseDocument +from app.models.schemas.search import SearchRequest +from app.services.search import ( + _index_exercises, # pyright: ignore[reportPrivateUsage] + search_exercises, +) +from app.tests.api.utilities import create_user + +from ..exercise.utilities import create_exercise +from ..muscle_group.utilities import create_muscle_group +from .utilities import wait_for_task + +logger = logging.getLogger(__name__) + + +async def test_search_exercises( + db_session: AsyncSession, + ms_client: AsyncClient, +): + mg = await create_muscle_group( + db_session, + name="muscle group 1", + description="Muscle group 1", + ) + exercise = await create_exercise( + db_session, + name="exercise 1", + description="Exercise 1", + muscle_group_ids=[mg.id], + ) + + task = await _index_exercises(db_session, ms_client) + await wait_for_task(ms_client, task) + + results = await search_exercises( + SearchRequest(query="1", limit=10), + user_id=None, + ms_client=ms_client, + ) + + assert isinstance(results, SearchResults) + assert isinstance(results.hits, list) + assert len(results.hits) == 1 + + hit = results.hits[0] + ExerciseDocument.model_validate(hit) + assert hit.id == exercise.id + assert hit.user_id == exercise.user_id + assert hit.name == exercise.name + assert hit.description == exercise.description + assert hit.muscle_group_names == [mg.name] + + +async def test_search_exercises_user_filter( + db_session: AsyncSession, + ms_client: AsyncClient, +): + user_1 = await create_user(db_session, username="user1") + user_2 = await create_user(db_session, username="user2") + + exercise_1 = await create_exercise( + db_session, + name="exercise 1", + description="Exercise 1", + user_id=user_1.id, + ) + await create_exercise( + db_session, + name="exercise 2", + description="Exercise 2", + user_id=user_2.id, + ) + + task = await _index_exercises(db_session, ms_client) + await wait_for_task(ms_client, task) + + results = await search_exercises( + SearchRequest(query="exercise", limit=10), + user_id=user_1.id, + ms_client=ms_client, + ) + + assert isinstance(results, SearchResults) + assert isinstance(results.hits, list) + assert len(results.hits) == 1 + + hit = results.hits[0] + ExerciseDocument.model_validate(hit) + assert hit.id == exercise_1.id + + +async def test_search_exercises_limit( + db_session: AsyncSession, + ms_client: AsyncClient, +): + for i in range(5): + await create_exercise( + db_session, + name=f"exercise {i}", + description=f"Exercise {i}", + ) + + task = await _index_exercises(db_session, ms_client) + await wait_for_task(ms_client, task) + + results = await search_exercises( + SearchRequest(query="exercise", limit=3), + user_id=None, + ms_client=ms_client, + ) + + assert isinstance(results, SearchResults) + assert isinstance(results.hits, list) + assert len(results.hits) == 3 diff --git a/server/app/tests/services/search/test_search_muscle_groups.py b/server/app/tests/services/search/test_search_muscle_groups.py new file mode 100644 index 00000000..35db9438 --- /dev/null +++ b/server/app/tests/services/search/test_search_muscle_groups.py @@ -0,0 +1,45 @@ +import logging + +from meilisearch_python_sdk import AsyncClient +from meilisearch_python_sdk.models.search import SearchResults +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.schemas.muscle_group import MuscleGroupPublic +from app.models.schemas.search import SearchRequest +from app.services.search import ( + _index_muscle_groups, # pyright: ignore[reportPrivateUsage] + search_muscle_groups, +) + +from ..muscle_group.utilities import create_muscle_group +from .utilities import wait_for_task + +logger = logging.getLogger(__name__) + + +async def test_search_muscle_groups( + db_session: AsyncSession, + ms_client: AsyncClient, +): + mg = await create_muscle_group( + db_session, + name="muscle group 1", + description="Muscle group 1", + ) + + task = await _index_muscle_groups(db_session, ms_client) + await wait_for_task(ms_client, task) + + results = await search_muscle_groups( + SearchRequest(query="1", limit=10), + ms_client, + ) + + assert isinstance(results, SearchResults) + assert isinstance(results.hits, list) + assert len(results.hits) == 1 + + hit = results.hits[0] + MuscleGroupPublic.model_validate(hit) + assert hit.name == mg.name + assert hit.description == mg.description diff --git a/server/app/tests/services/search/utilities.py b/server/app/tests/services/search/utilities.py new file mode 100644 index 00000000..19d06983 --- /dev/null +++ b/server/app/tests/services/search/utilities.py @@ -0,0 +1,17 @@ +import logging + +from meilisearch_python_sdk import AsyncClient +from meilisearch_python_sdk.errors import MeilisearchTimeoutError + +logger = logging.getLogger(__name__) + + +async def wait_for_task(client: AsyncClient, task_uid: int, timeout_in_ms: int = 5000): + try: + await client.wait_for_task( + task_uid, + timeout_in_ms=timeout_in_ms, + ) + except MeilisearchTimeoutError: + logger.error(f"Task {task_uid} did not complete within {timeout_in_ms} ms") + raise diff --git a/server/conftest.py b/server/conftest.py index a219b6ea..fdfd7bb3 100644 --- a/server/conftest.py +++ b/server/conftest.py @@ -2,6 +2,7 @@ "app.tests.fixtures.client", "app.tests.fixtures.database", "app.tests.fixtures.logging", + "app.tests.fixtures.meilisearch", "app.tests.fixtures.mocks", "app.tests.fixtures.overrides", "app.tests.fixtures.settings", diff --git a/server/pyproject.toml b/server/pyproject.toml index 7e146cd1..f94ecb1e 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -8,7 +8,6 @@ dependencies = [ "asyncpg>=0.31.0", "fastapi-swagger-dark>=0.0.9", "fastapi[standard]>=0.124.4", - "meilisearch>=0.40.0", "meilisearch-python-sdk>=7.1.1", "pwdlib[argon2]>=0.3.0", "pydantic-settings>=2.12.0", diff --git a/server/uv.lock b/server/uv.lock index d46be9f7..e23bce14 100644 --- a/server/uv.lock +++ b/server/uv.lock @@ -613,21 +613,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] -[[package]] -name = "meilisearch" -version = "0.40.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "camel-converter", extra = [ - "pydantic", - ] }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/10/94/b69c53be760d49d38a3e933aabcc506e48938fc1834c32cbf23a8b795a33/meilisearch-0.40.0.tar.gz", hash = "sha256:4348ea6e4bacb4b1391b9b577956a03090ff9327d6ebcddcf479f39b2d4b5b05", size = 31259, upload-time = "2026-01-15T06:29:18.305Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/06/0f29c374f7fa14ad8843eb458e258dacc215e584c0d77523b7e7cf23c24b/meilisearch-0.40.0-py3-none-any.whl", hash = "sha256:ae7378e448f01116f4fcbd0029f5383a2b17ed971ba2241c631408c45c86ce9d", size = 31776, upload-time = "2026-01-15T06:29:15.851Z" }, -] - [[package]] name = "meilisearch-python-sdk" version = "7.1.1" @@ -1018,7 +1003,6 @@ dependencies = [ "standard", ] }, { name = "fastapi-swagger-dark" }, - { name = "meilisearch" }, { name = "meilisearch-python-sdk" }, { name = "pwdlib", extra = [ "argon2", @@ -1053,7 +1037,6 @@ requires-dist = [ "standard", ], specifier = ">=0.124.4" }, { name = "fastapi-swagger-dark", specifier = ">=0.0.9" }, - { name = "meilisearch", specifier = ">=0.40.0" }, { name = "meilisearch-python-sdk", specifier = ">=7.1.1" }, { name = "pwdlib", extras = [ "argon2", From 83dc75511c88f77fc329f58dea7bf62c3ac7d3cb Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Wed, 25 Mar 2026 18:19:49 -0500 Subject: [PATCH 19/27] server - rename db to db_session --- server/app/api/endpoints/admin.py | 14 ++--- server/app/api/endpoints/auth.py | 28 +++++---- server/app/api/endpoints/exercise.py | 22 +++---- server/app/api/endpoints/feedback.py | 6 +- server/app/api/endpoints/health.py | 6 +- server/app/api/endpoints/muscle_group.py | 6 +- server/app/api/endpoints/search.py | 6 +- server/app/api/endpoints/set.py | 14 ++--- server/app/api/endpoints/workout.py | 22 +++---- server/app/api/endpoints/workout_exercise.py | 10 +-- server/app/core/dependencies.py | 6 +- server/app/core/security.py | 24 +++---- server/app/services/access_request.py | 12 ++-- server/app/services/admin.py | 16 ++--- server/app/services/auth.py | 62 ++++++++++--------- server/app/services/exercise.py | 48 +++++++------- server/app/services/feedback.py | 8 +-- server/app/services/muscle_group.py | 10 +-- server/app/services/search.py | 14 ++--- server/app/services/set.py | 36 +++++------ server/app/services/token.py | 8 +-- server/app/services/user.py | 24 +++---- server/app/services/utilities/queries.py | 16 ++--- server/app/services/workout.py | 32 +++++----- server/app/services/workout_exercise.py | 28 ++++----- server/app/tests/api/set/test_create_set.py | 2 +- .../test_create_workout_exercise.py | 2 +- .../dependencies/test_get_current_user.py | 10 +-- ...{test_get_db.py => test_get_db_session.py} | 6 +- .../core/security/test_authenticate_user.py | 8 +-- .../app/tests/core/security/test_get_token.py | 14 ++--- server/app/tests/fixtures/client.py | 8 +-- .../test_update_access_request_status.py | 8 +-- server/app/tests/services/auth/test_login.py | 8 +-- .../app/tests/services/auth/test_register.py | 14 ++--- .../services/auth/test_request_access.py | 12 ++-- .../auth/test_request_password_reset.py | 6 +- .../services/auth/test_reset_password.py | 8 +-- .../feedback/test_create_feedback_service.py | 4 +- .../tests/services/search/test_get_task.py | 4 +- .../services/search/test_reindex_data.py | 4 +- .../app/tests/services/set/test_create_set.py | 18 +++--- .../app/tests/services/set/test_delete_set.py | 8 +-- .../app/tests/services/set/test_update_set.py | 20 +++--- .../tests/services/token/test_get_tokens.py | 6 +- .../test_create_workout_exercise.py | 10 +-- .../test_delete_workout_exercise.py | 4 +- 47 files changed, 334 insertions(+), 328 deletions(-) rename server/app/tests/core/dependencies/{test_get_db.py => test_get_db_session.py} (80%) diff --git a/server/app/api/endpoints/admin.py b/server/app/api/endpoints/admin.py index 1d03a582..acaf1452 100644 --- a/server/app/api/endpoints/admin.py +++ b/server/app/api/endpoints/admin.py @@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import Settings, get_settings -from app.core.dependencies import get_current_admin, get_db +from app.core.dependencies import get_current_admin, get_db_session from app.models.schemas.access_request import ( AccessRequestPublic, UpdateAccessRequestStatusRequest, @@ -34,9 +34,9 @@ }, ) async def get_access_requests_endpoint( - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], ) -> list[AccessRequestPublic]: - return await get_access_requests(db) + return await get_access_requests(db_session) @api_router.patch( @@ -54,7 +54,7 @@ async def update_access_request_status_endpoint( access_request_id: int, req: UpdateAccessRequestStatusRequest, user: Annotated[UserPublic, Depends(get_current_admin)], - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], background_tasks: BackgroundTasks, email_svc: Annotated[EmailService, Depends(get_email_service)], settings: Annotated[Settings, Depends(get_settings)], @@ -63,7 +63,7 @@ async def update_access_request_status_endpoint( access_request_id=access_request_id, status=req.status, user=user, - db=db, + db_session=db_session, background_tasks=background_tasks, email_svc=email_svc, settings=settings, @@ -79,6 +79,6 @@ async def update_access_request_status_endpoint( }, ) async def get_users_endpoint( - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], ) -> list[UserPublic]: - return await get_users(db) + return await get_users(db_session) diff --git a/server/app/api/endpoints/auth.py b/server/app/api/endpoints/auth.py index 480efcc6..f76d9a2c 100644 --- a/server/app/api/endpoints/auth.py +++ b/server/app/api/endpoints/auth.py @@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import Settings, get_settings -from app.core.dependencies import get_db, refresh_token_cookie +from app.core.dependencies import get_db_session, refresh_token_cookie from app.core.security import ACCESS_JWT_KEY, REFRESH_JWT_KEY from app.models.api import ( REQUEST_ACCESS_APPROVED_MESSAGE, @@ -45,7 +45,7 @@ async def request_access_endpoint( req: RequestAccessRequest, background_tasks: BackgroundTasks, - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], email_svc: Annotated[EmailService, Depends(get_email_service)], settings: Annotated[Settings, Depends(get_settings)], ) -> str: @@ -54,7 +54,7 @@ async def request_access_endpoint( last_name=req.last_name, email_svc=email_svc, background_tasks=background_tasks, - db=db, + db_session=db_session, email=req.email, settings=settings, ) @@ -74,13 +74,13 @@ async def request_access_endpoint( ) async def register_endpoint( req: RegisterRequest, - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], ): await register( token_str=req.token, username=req.username, password=req.password, - db=db, + db_session=db_session, ) @@ -92,14 +92,14 @@ async def register_endpoint( async def forgot_password_endpoint( req: ForgotPasswordRequest, background_tasks: BackgroundTasks, - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], email_svc: Annotated[EmailService, Depends(get_email_service)], settings: Annotated[Settings, Depends(get_settings)], ): await request_password_reset( email=req.email, background_tasks=background_tasks, - db=db, + db_session=db_session, email_svc=email_svc, settings=settings, ) @@ -115,12 +115,12 @@ async def forgot_password_endpoint( ) async def reset_password_endpoint( req: ResetPasswordRequest, - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], ): await reset_password( token_str=req.token, password=req.password, - db=db, + db_session=db_session, ) @@ -134,14 +134,14 @@ async def reset_password_endpoint( ) async def login_endpoint( req: LoginRequest, - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], settings: Annotated[Settings, Depends(get_settings)], res: Response, ): result = await login( identifier=req.identifier, password=req.password, - db=db, + db_session=db_session, settings=settings, ) res.set_cookie( @@ -171,12 +171,14 @@ async def login_endpoint( }, ) async def refresh_token_endpoint( - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], refresh_token: Annotated[str, Depends(refresh_token_cookie)], settings: Annotated[Settings, Depends(get_settings)], res: Response, ): - access_token = await refresh(db=db, token=refresh_token, settings=settings) + access_token = await refresh( + db_session=db_session, token=refresh_token, settings=settings + ) res.set_cookie( key=ACCESS_JWT_KEY, value=access_token, diff --git a/server/app/api/endpoints/exercise.py b/server/app/api/endpoints/exercise.py index 4699a88e..67c9974b 100644 --- a/server/app/api/endpoints/exercise.py +++ b/server/app/api/endpoints/exercise.py @@ -3,7 +3,7 @@ from fastapi import APIRouter, Depends, status from sqlalchemy.ext.asyncio import AsyncSession -from app.core.dependencies import get_current_user, get_db +from app.core.dependencies import get_current_user, get_db_session from app.models.schemas.errors import ErrorResponseModel from app.models.schemas.exercise import ( CreateExerciseRequest, @@ -39,9 +39,9 @@ async def create_exercise_endpoint( req: CreateExerciseRequest, user: Annotated[UserPublic, Depends(get_current_user)], - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], ): - await create_exercise(user.id, req, db) + await create_exercise(user.id, req, db_session) @api_router.get( @@ -51,9 +51,9 @@ async def create_exercise_endpoint( ) async def get_exercises_endpoint( user: Annotated[UserPublic, Depends(get_current_user)], - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], ) -> list[ExercisePublic]: - return await get_exercises(user.id, db) + return await get_exercises(user.id, db_session) @api_router.get( @@ -67,9 +67,9 @@ async def get_exercises_endpoint( async def get_exercise_endpoint( exercise_id: int, user: Annotated[UserPublic, Depends(get_current_user)], - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], ) -> ExercisePublic: - return await get_exercise(exercise_id, user.id, db) + return await get_exercise(exercise_id, user.id, db_session) @api_router.patch( @@ -87,9 +87,9 @@ async def update_exercise_endpoint( exercise_id: int, req: UpdateExerciseRequest, user: Annotated[UserPublic, Depends(get_current_user)], - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], ): - await update_exercise(exercise_id, user.id, req, db) + await update_exercise(exercise_id, user.id, req, db_session) @api_router.delete( @@ -105,6 +105,6 @@ async def update_exercise_endpoint( async def delete_exercise_endpoint( exercise_id: int, user: Annotated[UserPublic, Depends(get_current_user)], - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], ): - await delete_exercise(exercise_id, user.id, db) + await delete_exercise(exercise_id, user.id, db_session) diff --git a/server/app/api/endpoints/feedback.py b/server/app/api/endpoints/feedback.py index 5ce2515f..5a87a1ce 100644 --- a/server/app/api/endpoints/feedback.py +++ b/server/app/api/endpoints/feedback.py @@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import Settings, get_settings -from app.core.dependencies import get_current_user, get_db +from app.core.dependencies import get_current_user, get_db_session from app.models.schemas.errors import ErrorResponseModel from app.models.schemas.feedback import CreateFeedbackRequest from app.models.schemas.user import UserPublic @@ -29,7 +29,7 @@ def create_feedback_endpoint( user: Annotated[UserPublic, Depends(get_current_user)], background_tasks: BackgroundTasks, - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], github_svc: Annotated[GitHubService, Depends(get_github_service)], settings: Annotated[Settings, Depends(get_settings)], req: CreateFeedbackRequest = Form(..., media_type="multipart/form-data"), @@ -38,7 +38,7 @@ def create_feedback_endpoint( create_feedback, user=user, req=req, - db=db, + db_session=db_session, github_svc=github_svc, settings=settings, ) diff --git a/server/app/api/endpoints/health.py b/server/app/api/endpoints/health.py index f8057e5b..e425bacf 100644 --- a/server/app/api/endpoints/health.py +++ b/server/app/api/endpoints/health.py @@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.sql import text -from app.core.dependencies import get_db +from app.core.dependencies import get_db_session api_router = APIRouter( prefix="/health", @@ -19,7 +19,7 @@ def get_health_endpoint() -> str: @api_router.get("/db", operation_id="getDbHealth") async def get_db_health_endpoint( - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], ) -> str: - await db.execute(text("SELECT 1")) + await db_session.execute(text("SELECT 1")) return "ok" diff --git a/server/app/api/endpoints/muscle_group.py b/server/app/api/endpoints/muscle_group.py index e8115866..a2a267a4 100644 --- a/server/app/api/endpoints/muscle_group.py +++ b/server/app/api/endpoints/muscle_group.py @@ -3,7 +3,7 @@ from fastapi import APIRouter, Depends, status from sqlalchemy.ext.asyncio import AsyncSession -from app.core.dependencies import get_current_user, get_db +from app.core.dependencies import get_current_user, get_db_session from app.models.schemas.errors import ErrorResponseModel from app.models.schemas.muscle_group import MuscleGroupPublic from app.services.muscle_group import get_muscle_groups_ordered_by_name @@ -23,6 +23,6 @@ }, ) async def get_muscle_groups_endpoint( - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], ) -> list[MuscleGroupPublic]: - return await get_muscle_groups_ordered_by_name(db) + return await get_muscle_groups_ordered_by_name(db_session) diff --git a/server/app/api/endpoints/search.py b/server/app/api/endpoints/search.py index cdd26464..e39984d5 100644 --- a/server/app/api/endpoints/search.py +++ b/server/app/api/endpoints/search.py @@ -7,7 +7,7 @@ from app.core.dependencies import ( get_current_admin, get_current_user, - get_db, + get_db_session, get_ms_client, ) from app.models.schemas.errors import ErrorResponseModel @@ -54,11 +54,11 @@ async def get_task_endpoint( ) async def reindex_endpoint( _: Annotated[UserPublic, Depends(get_current_admin)], - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], ms_client: Annotated[AsyncClient, Depends(get_ms_client)], ): await reindex_data( - db=db, + db_session=db_session, ms_client=ms_client, ) diff --git a/server/app/api/endpoints/set.py b/server/app/api/endpoints/set.py index 2beb4100..1d8ee085 100644 --- a/server/app/api/endpoints/set.py +++ b/server/app/api/endpoints/set.py @@ -3,7 +3,7 @@ from fastapi import APIRouter, Depends, status from sqlalchemy.ext.asyncio import AsyncSession -from app.core.dependencies import get_current_user, get_db +from app.core.dependencies import get_current_user, get_db_session from app.models.schemas.errors import ErrorResponseModel from app.models.schemas.set import CreateSetRequest, UpdateSetRequest from app.models.schemas.user import UserPublic @@ -31,9 +31,9 @@ async def create_set_endpoint( workout_exercise_id: int, req: CreateSetRequest, user: Annotated[UserPublic, Depends(get_current_user)], - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], ): - await create_set(workout_id, workout_exercise_id, user.id, req, db) + await create_set(workout_id, workout_exercise_id, user.id, req, db_session) @api_router.patch( @@ -51,9 +51,9 @@ async def update_set_endpoint( set_id: int, req: UpdateSetRequest, user: Annotated[UserPublic, Depends(get_current_user)], - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], ): - await update_set(workout_id, workout_exercise_id, set_id, user.id, req, db) + await update_set(workout_id, workout_exercise_id, set_id, user.id, req, db_session) @api_router.delete( @@ -70,6 +70,6 @@ async def delete_set_endpoint( workout_exercise_id: int, set_id: int, user: Annotated[UserPublic, Depends(get_current_user)], - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], ): - await delete_set(workout_id, workout_exercise_id, set_id, user.id, db) + await delete_set(workout_id, workout_exercise_id, set_id, user.id, db_session) diff --git a/server/app/api/endpoints/workout.py b/server/app/api/endpoints/workout.py index a2be5ab3..d5a0b4c1 100644 --- a/server/app/api/endpoints/workout.py +++ b/server/app/api/endpoints/workout.py @@ -3,7 +3,7 @@ from fastapi import APIRouter, Depends, status from sqlalchemy.ext.asyncio import AsyncSession -from app.core.dependencies import get_current_user, get_db +from app.core.dependencies import get_current_user, get_db_session from app.models.schemas.errors import ErrorResponseModel from app.models.schemas.user import UserPublic from app.models.schemas.workout import ( @@ -38,9 +38,9 @@ async def create_workout_endpoint( req: CreateWorkoutRequest, user: Annotated[UserPublic, Depends(get_current_user)], - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], ): - await create_workout(user.id, req, db) + await create_workout(user.id, req, db_session) @api_router.get( @@ -50,9 +50,9 @@ async def create_workout_endpoint( ) async def get_workouts_endpoint( user: Annotated[UserPublic, Depends(get_current_user)], - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], ) -> list[WorkoutBase]: - return await get_workouts(user.id, db) + return await get_workouts(user.id, db_session) @api_router.get( @@ -66,9 +66,9 @@ async def get_workouts_endpoint( async def get_workout_endpoint( workout_id: int, user: Annotated[UserPublic, Depends(get_current_user)], - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], ) -> WorkoutPublic: - return await get_workout(workout_id, user.id, db) + return await get_workout(workout_id, user.id, db_session) @api_router.patch( @@ -84,9 +84,9 @@ async def update_workout_endpoint( workout_id: int, req: UpdateWorkoutRequest, user: Annotated[UserPublic, Depends(get_current_user)], - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], ): - await update_workout(workout_id, user.id, req, db) + await update_workout(workout_id, user.id, req, db_session) @api_router.delete( @@ -101,6 +101,6 @@ async def update_workout_endpoint( async def delete_workout_endpoint( workout_id: int, user: Annotated[UserPublic, Depends(get_current_user)], - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], ): - await delete_workout(workout_id, user.id, db) + await delete_workout(workout_id, user.id, db_session) diff --git a/server/app/api/endpoints/workout_exercise.py b/server/app/api/endpoints/workout_exercise.py index db8514b0..bdc2687e 100644 --- a/server/app/api/endpoints/workout_exercise.py +++ b/server/app/api/endpoints/workout_exercise.py @@ -3,7 +3,7 @@ from fastapi import APIRouter, Depends, status from sqlalchemy.ext.asyncio import AsyncSession -from app.core.dependencies import get_current_user, get_db +from app.core.dependencies import get_current_user, get_db_session from app.models.schemas.errors import ErrorResponseModel from app.models.schemas.user import UserPublic from app.models.schemas.workout_exercise import ( @@ -35,9 +35,9 @@ async def create_workout_exercise_endpoint( workout_id: int, req: CreateWorkoutExerciseRequest, user: Annotated[UserPublic, Depends(get_current_user)], - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], ): - await create_workout_exercise(workout_id, user.id, req, db) + await create_workout_exercise(workout_id, user.id, req, db_session) @api_router.delete( @@ -53,6 +53,6 @@ async def delete_workout_exercise_endpoint( workout_id: int, workout_exercise_id: int, user: Annotated[UserPublic, Depends(get_current_user)], - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], ): - await delete_workout_exercise(workout_id, workout_exercise_id, user.id, db) + await delete_workout_exercise(workout_id, workout_exercise_id, user.id, db_session) diff --git a/server/app/core/dependencies.py b/server/app/core/dependencies.py index f8b2a9e6..3e47f328 100644 --- a/server/app/core/dependencies.py +++ b/server/app/core/dependencies.py @@ -30,7 +30,7 @@ def get_db_sessionmaker(db_url: str, is_prod: bool): ) -async def get_db( +async def get_db_session( settings: Annotated[Settings, Depends(get_settings)], ) -> AsyncGenerator[AsyncSession]: async with get_db_sessionmaker( @@ -67,13 +67,13 @@ async def get_ms_client( async def get_current_user( token: Annotated[str, Depends(access_token_cookie)], - db: Annotated[AsyncSession, Depends(get_db)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], settings: Annotated[Settings, Depends(get_settings)], ) -> UserPublic: logger.info("Getting current user using jwt") username = verify_jwt(token, settings) - user = await get_user_by_username(username, db) + user = await get_user_by_username(username, db_session) if not user: raise InvalidCredentials() diff --git a/server/app/core/security.py b/server/app/core/security.py index 5081dd7a..b8553885 100644 --- a/server/app/core/security.py +++ b/server/app/core/security.py @@ -42,10 +42,10 @@ async def _get_token[T: (RegistrationToken, PasswordResetToken)]( token_str: str, model: type[T], load_option: InstrumentedAttribute[Any], - db: AsyncSession, + db_session: AsyncSession, ) -> T | None: token_prefix = token_str[:TOKEN_PREFIX_LENGTH] - tokens = await get_tokens_by_prefix(model, load_option, token_prefix, db) + tokens = await get_tokens_by_prefix(model, load_option, token_prefix, db_session) for token in tokens: if verify_secret(token_str, token.token_hash): return token @@ -53,31 +53,31 @@ async def _get_token[T: (RegistrationToken, PasswordResetToken)]( async def get_registration_token( token_str: str, - db: AsyncSession, + db_session: AsyncSession, ) -> RegistrationToken | None: return await _get_token( token_str, model=RegistrationToken, load_option=RegistrationToken.access_request, - db=db, + db_session=db_session, ) async def get_password_reset_token( token_str: str, - db: AsyncSession, + db_session: AsyncSession, ) -> PasswordResetToken | None: return await _get_token( token_str, model=PasswordResetToken, load_option=PasswordResetToken.user, - db=db, + db_session=db_session, ) async def expire_existing_registration_tokens( access_request_id: int, - db: AsyncSession, + db_session: AsyncSession, ) -> None: logger.info( f"Expiring existing registration tokens for access request {access_request_id}" @@ -85,19 +85,19 @@ async def expire_existing_registration_tokens( await expire_tokens( RegistrationToken, [RegistrationToken.access_request_id == access_request_id], - db, + db_session, ) async def expire_existing_password_reset_tokens( user_id: int, - db: AsyncSession, + db_session: AsyncSession, ) -> None: logger.info(f"Expiring existing password reset tokens for user {user_id}") await expire_tokens( PasswordResetToken, [PasswordResetToken.user_id == user_id], - db, + db_session, ) @@ -138,9 +138,9 @@ def create_password_reset_token( async def authenticate_user( identifier: str, password: str, - db: AsyncSession, + db_session: AsyncSession, ) -> User | None: - user = await get_user_by_identifier(identifier, db) + user = await get_user_by_identifier(identifier, db_session) if not user or not verify_secret(password, user.password_hash): return None return user diff --git a/server/app/services/access_request.py b/server/app/services/access_request.py index 4b16e848..04fc9cae 100644 --- a/server/app/services/access_request.py +++ b/server/app/services/access_request.py @@ -15,9 +15,9 @@ async def get_latest_access_request_by_email( - email: str, db: AsyncSession + email: str, db_session: AsyncSession ) -> AccessRequest | None: - result = await db.execute( + result = await db_session.execute( select(AccessRequest) .where(AccessRequest.email == email) .order_by(AccessRequest.created_at.desc()) @@ -27,18 +27,18 @@ async def get_latest_access_request_by_email( async def get_access_request_by_id( - access_request_id: int, db: AsyncSession + access_request_id: int, db_session: AsyncSession ) -> AccessRequest | None: - result = await db.execute( + result = await db_session.execute( select(AccessRequest).where(AccessRequest.id == access_request_id) ) return result.scalar_one_or_none() async def get_access_requests_with_reviewer( - db: AsyncSession, + db_session: AsyncSession, ) -> Sequence[AccessRequest]: - result = await db.execute( + result = await db_session.execute( select(AccessRequest) .options(selectinload(AccessRequest.reviewer)) .order_by(STATUS_PRIORITY) diff --git a/server/app/services/admin.py b/server/app/services/admin.py index a1e6fd08..1d78a36e 100644 --- a/server/app/services/admin.py +++ b/server/app/services/admin.py @@ -22,17 +22,17 @@ logger = logging.getLogger(__name__) -async def get_access_requests(db: AsyncSession) -> list[AccessRequestPublic]: +async def get_access_requests(db_session: AsyncSession) -> list[AccessRequestPublic]: logger.info("Getting access requests") - requests = await get_access_requests_with_reviewer(db) + requests = await get_access_requests_with_reviewer(db_session) return [to_access_request_public(r) for r in requests] async def update_access_request_status( access_request_id: int, status: Literal[AccessRequestStatus.APPROVED, AccessRequestStatus.REJECTED], - db: AsyncSession, + db_session: AsyncSession, user: UserPublic, background_tasks: BackgroundTasks, email_svc: EmailService, @@ -40,7 +40,7 @@ async def update_access_request_status( ) -> None: logger.info(f"Updating access request {access_request_id} to status {status}") - access_request = await get_access_request_by_id(access_request_id, db) + access_request = await get_access_request_by_id(access_request_id, db_session) if not access_request: logger.error(f"Access request {access_request_id} not found") @@ -56,9 +56,9 @@ async def update_access_request_status( token_str: str | None = None if status == AccessRequestStatus.APPROVED: token_str, token = create_registration_token(access_request.id) - db.add(token) + db_session.add(token) - await db.commit() + await db_session.commit() if status == AccessRequestStatus.APPROVED: assert token_str is not None @@ -74,8 +74,8 @@ async def update_access_request_status( ) -async def get_users(db: AsyncSession) -> list[UserPublic]: +async def get_users(db_session: AsyncSession) -> list[UserPublic]: logger.info("Getting users") - users = await get_users_ordered_by_username(db) + users = await get_users_ordered_by_username(db_session) return [to_user_public(user) for user in users] diff --git a/server/app/services/auth.py b/server/app/services/auth.py index a5ee3067..751d1341 100644 --- a/server/app/services/auth.py +++ b/server/app/services/auth.py @@ -42,19 +42,19 @@ async def request_access( first_name: str, last_name: str, background_tasks: BackgroundTasks, - db: AsyncSession, + db_session: AsyncSession, email_svc: EmailService, settings: Settings, ) -> bool: """Returns True if access was already approved, False otherwise""" logger.info(f"Requesting access for email: {email}") - existing_user_by_email = await get_user_by_email(email, db) - existing_user_by_username = await get_user_by_username(email, db) + existing_user_by_email = await get_user_by_email(email, db_session) + existing_user_by_username = await get_user_by_username(email, db_session) if existing_user_by_email or existing_user_by_username: raise EmailInUse() - existing_request = await get_latest_access_request_by_email(email, db) + existing_request = await get_latest_access_request_by_email(email, db_session) if existing_request: logger.info( f"Found existing access request for email {email} with id {existing_request.id}" @@ -65,11 +65,13 @@ async def request_access( case AccessRequestStatus.REJECTED: raise AccessRequestRejected() case _: - await expire_existing_registration_tokens(existing_request.id, db) + await expire_existing_registration_tokens( + existing_request.id, db_session + ) token_str, token = create_registration_token(existing_request.id) - db.add(token) - await db.commit() + db_session.add(token) + await db_session.commit() background_tasks.add_task( email_svc.send_access_request_approved_email, @@ -85,10 +87,10 @@ async def request_access( first_name=first_name, last_name=last_name, ) - db.add(access_request) - await db.commit() + db_session.add(access_request) + await db_session.commit() - admins = await get_admin_users(db) + admins = await get_admin_users(db_session) for admin in admins: background_tasks.add_task( email_svc.send_access_request_notification, @@ -104,11 +106,11 @@ async def register( token_str: str, username: str, password: str, - db: AsyncSession, + db_session: AsyncSession, ) -> None: logger.info(f"Registering new user {username}") - token = await get_registration_token(token_str, db) + token = await get_registration_token(token_str, db_session) if not token or token.is_used() or token.is_expired(): raise InvalidToken() @@ -116,13 +118,13 @@ async def register( if access_request.status != AccessRequestStatus.APPROVED: raise InvalidToken() - existing_user_by_username = await get_user_by_username(username, db) - existing_user_by_email = await get_user_by_email(username, db) + existing_user_by_username = await get_user_by_username(username, db_session) + existing_user_by_email = await get_user_by_email(username, db_session) if existing_user_by_username or existing_user_by_email: raise UsernameTaken() token.used_at = datetime.now(UTC) - await expire_existing_registration_tokens(access_request.id, db) + await expire_existing_registration_tokens(access_request.id, db_session) user = User( username=username, @@ -131,14 +133,14 @@ async def register( last_name=access_request.last_name, password_hash=hash_secret(password), ) - db.add(user) - await db.commit() + db_session.add(user) + await db_session.commit() async def request_password_reset( email: str, background_tasks: BackgroundTasks, - db: AsyncSession, + db_session: AsyncSession, email_svc: EmailService, settings: Settings, ) -> None: @@ -148,16 +150,16 @@ async def request_password_reset( logger.warning("Password reset requested for admin email, ignoring") return - user = await get_user_by_email(email, db) + user = await get_user_by_email(email, db_session) if not user: logger.info(f"Password reset requested for unregistered email: {email}") return - await expire_existing_password_reset_tokens(user.id, db) + await expire_existing_password_reset_tokens(user.id, db_session) token_str, token = create_password_reset_token(user.id) - db.add(token) - await db.commit() + db_session.add(token) + await db_session.commit() background_tasks.add_task( email_svc.send_password_reset_email, @@ -170,31 +172,31 @@ async def request_password_reset( async def reset_password( token_str: str, password: str, - db: AsyncSession, + db_session: AsyncSession, ) -> None: logger.info("Resetting password") - token = await get_password_reset_token(token_str, db) + token = await get_password_reset_token(token_str, db_session) if not token or token.is_used() or token.is_expired(): raise InvalidToken() user = token.user token.used_at = datetime.now(UTC) - await expire_existing_password_reset_tokens(user.id, db) + await expire_existing_password_reset_tokens(user.id, db_session) user.password_hash = hash_secret(password) - await db.commit() + await db_session.commit() async def login( identifier: str, password: str, - db: AsyncSession, + db_session: AsyncSession, settings: Settings, ) -> LoginResult: logger.info(f"Logging in user with identifier {identifier}") - user = await authenticate_user(identifier, password, db) + user = await authenticate_user(identifier, password, db_session) if not user: raise InvalidCredentials() @@ -207,11 +209,11 @@ async def login( ) -async def refresh(db: AsyncSession, token: str, settings: Settings) -> str: +async def refresh(db_session: AsyncSession, token: str, settings: Settings) -> str: logger.info("Refreshing access token") username = verify_jwt(token, settings) - user = await get_user_by_username(username, db) + user = await get_user_by_username(username, db_session) if not user: raise InvalidCredentials() diff --git a/server/app/services/exercise.py b/server/app/services/exercise.py index 49b9e774..afc058c3 100644 --- a/server/app/services/exercise.py +++ b/server/app/services/exercise.py @@ -27,9 +27,9 @@ async def _get_owned_exercise( exercise_id: int, user_id: int, - db: AsyncSession, + db_session: AsyncSession, ) -> Exercise: - result = await db.execute( + result = await db_session.execute( select(Exercise).where( Exercise.id == exercise_id, ) @@ -43,11 +43,11 @@ async def _get_owned_exercise( async def create_exercise( user_id: int, req: CreateExerciseRequest, - db: AsyncSession, + db_session: AsyncSession, ) -> None: logger.info(f"Creating exercise '{req.name}' for user {user_id}") - muscle_groups = await get_muscle_groups_by_ids(req.muscle_group_ids, db) + muscle_groups = await get_muscle_groups_by_ids(req.muscle_group_ids, db_session) if len(muscle_groups) != len(req.muscle_group_ids): raise MuscleGroupNotFound() @@ -56,36 +56,36 @@ async def create_exercise( name=req.name, description=req.description, ) - db.add(exercise) + db_session.add(exercise) try: - await db.flush() + await db_session.flush() except IntegrityError as e: logger.error(f"Integrity error creating exercise: {e}") - await db.rollback() + await db_session.rollback() if is_unique_violation(e, EXERCISE_UNIQUE_CONSTRAINT): raise ExerciseNameConflict() raise for mg in muscle_groups: - db.add( + db_session.add( ExerciseMuscleGroup( exercise_id=exercise.id, muscle_group_id=mg.id, ) ) - await db.commit() + await db_session.commit() async def get_exercises( user_id: int, - db: AsyncSession, + db_session: AsyncSession, ) -> list[ExercisePublic]: logger.info(f"Getting exercises for user {user_id}") exercises = await query_exercises( - db, + db_session, False, (Exercise.user_id.is_(None)) | (Exercise.user_id == user_id), ) @@ -95,12 +95,12 @@ async def get_exercises( async def get_exercise( exercise_id: int, user_id: int, - db: AsyncSession, + db_session: AsyncSession, ) -> ExercisePublic: logger.info(f"Getting exercise {exercise_id} for user {user_id}") exercises = await query_exercises( - db, + db_session, False, Exercise.id == exercise_id, (Exercise.user_id.is_(None)) | (Exercise.user_id == user_id), @@ -114,11 +114,11 @@ async def update_exercise( exercise_id: int, user_id: int, req: UpdateExerciseRequest, - db: AsyncSession, + db_session: AsyncSession, ) -> None: logger.info(f"Updating exercise {exercise_id} for user {user_id}") - exercise = await _get_owned_exercise(exercise_id, user_id, db) + exercise = await _get_owned_exercise(exercise_id, user_id, db_session) if not req.model_fields_set: logger.info("No changes provided, skipping update") @@ -133,25 +133,25 @@ async def update_exercise( if "muscle_group_ids" in req.model_fields_set: assert req.muscle_group_ids is not None - muscle_groups = await get_muscle_groups_by_ids(req.muscle_group_ids, db) + muscle_groups = await get_muscle_groups_by_ids(req.muscle_group_ids, db_session) if len(muscle_groups) != len(req.muscle_group_ids): raise MuscleGroupNotFound() - await db.execute( + await db_session.execute( delete(ExerciseMuscleGroup).where( ExerciseMuscleGroup.exercise_id == exercise_id ), ) for mg in muscle_groups: - db.add( + db_session.add( ExerciseMuscleGroup(exercise_id=exercise_id, muscle_group_id=mg.id), ) try: - await db.commit() + await db_session.commit() except IntegrityError as e: logger.error(f"Integrity error updating exercise: {e}") - await db.rollback() + await db_session.rollback() if is_unique_violation(e, EXERCISE_UNIQUE_CONSTRAINT): raise ExerciseNameConflict() raise @@ -160,10 +160,10 @@ async def update_exercise( async def delete_exercise( exercise_id: int, user_id: int, - db: AsyncSession, + db_session: AsyncSession, ) -> None: logger.info(f"Deleting exercise {exercise_id} for user {user_id}") - exercise = await _get_owned_exercise(exercise_id, user_id, db) - await db.delete(exercise) - await db.commit() + exercise = await _get_owned_exercise(exercise_id, user_id, db_session) + await db_session.delete(exercise) + await db_session.commit() diff --git a/server/app/services/feedback.py b/server/app/services/feedback.py index 556f31c1..2ff8bc07 100644 --- a/server/app/services/feedback.py +++ b/server/app/services/feedback.py @@ -15,7 +15,7 @@ async def create_feedback( user: UserPublic, req: CreateFeedbackRequest, - db: AsyncSession, + db_session: AsyncSession, github_svc: GitHubService, settings: Settings, ): @@ -36,9 +36,9 @@ async def create_feedback( files=stored_files, ) - db.add(feedback) - await db.commit() - await db.refresh(feedback) + db_session.add(feedback) + await db_session.commit() + await db_session.refresh(feedback) await github_svc.create_feedback_issue(feedback, settings) diff --git a/server/app/services/muscle_group.py b/server/app/services/muscle_group.py index 76a865f0..79a428ec 100644 --- a/server/app/services/muscle_group.py +++ b/server/app/services/muscle_group.py @@ -7,20 +7,22 @@ async def get_muscle_groups_ordered_by_name( - db: AsyncSession, + db_session: AsyncSession, ) -> list[MuscleGroupPublic]: - result = await db.execute(select(MuscleGroup).order_by(MuscleGroup.name.asc())) + result = await db_session.execute( + select(MuscleGroup).order_by(MuscleGroup.name.asc()) + ) muscle_groups = result.scalars().all() return [to_muscle_group_public(mg) for mg in muscle_groups] async def get_muscle_groups_by_ids( ids: list[int], - db: AsyncSession, + db_session: AsyncSession, ) -> list[MuscleGroup]: if not ids: return [] - result = await db.execute( + result = await db_session.execute( select(MuscleGroup).where(MuscleGroup.id.in_(ids)), ) return list(result.scalars().all()) diff --git a/server/app/services/search.py b/server/app/services/search.py index 60170dc9..eae21e6e 100644 --- a/server/app/services/search.py +++ b/server/app/services/search.py @@ -29,21 +29,21 @@ async def get_task( async def reindex_data( - db: AsyncSession, + db_session: AsyncSession, ms_client: AsyncClient, ): - task = await _index_muscle_groups(db, ms_client) + task = await _index_muscle_groups(db_session, ms_client) logger.info(f"Reindexing muscle groups with task id: {task}") - task = await _index_exercises(db, ms_client) + task = await _index_exercises(db_session, ms_client) logger.info(f"Reindexing exercises with task id: {task}") async def _index_muscle_groups( - db: AsyncSession, + db_session: AsyncSession, ms_client: AsyncClient, ) -> int: - result = await db.execute(select(MuscleGroup)) + result = await db_session.execute(select(MuscleGroup)) muscle_groups = result.scalars().all() docs = [to_muscle_group_public(mg) for mg in muscle_groups] @@ -64,10 +64,10 @@ async def _index_muscle_groups( async def _index_exercises( - db: AsyncSession, + db_session: AsyncSession, ms_client: AsyncClient, ) -> int: - exercises = await query_exercises(db, base=False) + exercises = await query_exercises(db_session, base=False) docs = [to_exercise_document(e) for e in exercises] await ms_client.delete_index_if_exists(SearchIndex.EXERCISES) diff --git a/server/app/services/set.py b/server/app/services/set.py index 57129a50..1ab99c6d 100644 --- a/server/app/services/set.py +++ b/server/app/services/set.py @@ -24,9 +24,9 @@ async def _get_next_set_number( workout_exercise_id: int, - db: AsyncSession, + db_session: AsyncSession, ) -> int: - result = await db.execute( + result = await db_session.execute( select( func.coalesce(func.max(Set.set_number), 0), ).where( @@ -41,17 +41,17 @@ async def create_set( workout_exercise_id: int, user_id: int, req: CreateSetRequest, - db: AsyncSession, + db_session: AsyncSession, ) -> None: logger.info( f"Creating set for workout exercise {workout_exercise_id} in workout {workout_id}" ) # validate workout existence & ownership - await get_owned_workout(workout_id, user_id, db) + await get_owned_workout(workout_id, user_id, db_session) result = await query_workout_exercises( - db, + db_session, WorkoutExercise.id == workout_exercise_id, WorkoutExercise.workout_id == workout_id, ) @@ -59,7 +59,7 @@ async def create_set( if not workout_exercise: raise WorkoutExerciseNotFound() - set_number = await _get_next_set_number(workout_exercise_id, db) + set_number = await _get_next_set_number(workout_exercise_id, db_session) set_ = Set( workout_exercise_id=workout_exercise_id, set_number=set_number, @@ -68,13 +68,13 @@ async def create_set( unit=req.unit, notes=req.notes, ) - db.add(set_) + db_session.add(set_) try: - await db.commit() + await db_session.commit() except IntegrityError as e: logger.error(f"Integrity error creating set: {e}") - await db.rollback() + await db_session.rollback() if is_unique_violation(e, SET_UNIQUE_CONSTRAINT): raise SetNumberConflict() raise @@ -86,14 +86,14 @@ async def update_set( set_id: int, user_id: int, req: UpdateSetRequest, - db: AsyncSession, + db_session: AsyncSession, ) -> None: logger.info(f"Updating set {set_id} for workout exercise {workout_exercise_id}") - await get_owned_workout(workout_id, user_id, db) + await get_owned_workout(workout_id, user_id, db_session) result = await query_sets( - db, + db_session, Set.id == set_id, WorkoutExercise.id == workout_exercise_id, WorkoutExercise.workout_id == workout_id, @@ -115,7 +115,7 @@ async def update_set( if "notes" in req.model_fields_set: set_.notes = req.notes - await db.commit() + await db_session.commit() async def delete_set( @@ -123,14 +123,14 @@ async def delete_set( workout_exercise_id: int, set_id: int, user_id: int, - db: AsyncSession, + db_session: AsyncSession, ) -> None: logger.info(f"Deleting set {set_id} from workout {workout_id}") - await get_owned_workout(workout_id, user_id, db) + await get_owned_workout(workout_id, user_id, db_session) result = await query_sets( - db, + db_session, Set.id == set_id, WorkoutExercise.id == workout_exercise_id, WorkoutExercise.workout_id == workout_id, @@ -139,5 +139,5 @@ async def delete_set( if not set_: raise SetNotFound() - await db.delete(set_) - await db.commit() + await db_session.delete(set_) + await db_session.commit() diff --git a/server/app/services/token.py b/server/app/services/token.py index 20d48b8d..e572fb1c 100644 --- a/server/app/services/token.py +++ b/server/app/services/token.py @@ -13,9 +13,9 @@ async def get_tokens_by_prefix[T: (RegistrationToken, PasswordResetToken)]( model: type[T], load_option: InstrumentedAttribute[Any], prefix: str, - db: AsyncSession, + db_session: AsyncSession, ) -> Sequence[T]: - result = await db.execute( + result = await db_session.execute( select(model) .options(selectinload(load_option)) .where(model.token_prefix == prefix) @@ -29,9 +29,9 @@ async def get_tokens_by_prefix[T: (RegistrationToken, PasswordResetToken)]( async def expire_tokens[T: (RegistrationToken, PasswordResetToken)]( model: type[T], where_clauses: list[Any], - db: AsyncSession, + db_session: AsyncSession, ) -> None: - await db.execute( + await db_session.execute( update(model) .where(*where_clauses, model.expires_at > func.now()) .values(expires_at=func.now()) diff --git a/server/app/services/user.py b/server/app/services/user.py index d5fc374b..8b120598 100644 --- a/server/app/services/user.py +++ b/server/app/services/user.py @@ -7,40 +7,40 @@ from app.models.schemas.types import is_email_identifier -async def get_admin_users(db: AsyncSession) -> Sequence[User]: - result = await db.execute(select(User).where(User.is_admin)) +async def get_admin_users(db_session: AsyncSession) -> Sequence[User]: + result = await db_session.execute(select(User).where(User.is_admin)) return result.scalars().all() async def get_user_by_username( username: str, - db: AsyncSession, + db_session: AsyncSession, ) -> User | None: - result = await db.execute(select(User).where(User.username == username)) + result = await db_session.execute(select(User).where(User.username == username)) return result.scalar_one_or_none() async def get_user_by_email( email: str, - db: AsyncSession, + db_session: AsyncSession, ) -> User | None: - result = await db.execute(select(User).where(User.email == email)) + result = await db_session.execute(select(User).where(User.email == email)) return result.scalar_one_or_none() async def get_user_by_identifier( identifier: str, - db: AsyncSession, + db_session: AsyncSession, ) -> User | None: if is_email_identifier(identifier): - user = await get_user_by_email(identifier, db) + user = await get_user_by_email(identifier, db_session) if user: return user - return await get_user_by_username(identifier, db) + return await get_user_by_username(identifier, db_session) - return await get_user_by_username(identifier, db) + return await get_user_by_username(identifier, db_session) -async def get_users_ordered_by_username(db: AsyncSession) -> Sequence[User]: - result = await db.execute(select(User).order_by(User.username.asc())) +async def get_users_ordered_by_username(db_session: AsyncSession) -> Sequence[User]: + result = await db_session.execute(select(User).order_by(User.username.asc())) return result.scalars().all() diff --git a/server/app/services/utilities/queries.py b/server/app/services/utilities/queries.py index ebce02f3..62365367 100644 --- a/server/app/services/utilities/queries.py +++ b/server/app/services/utilities/queries.py @@ -16,9 +16,9 @@ async def get_owned_workout( workout_id: int, user_id: int, - db: AsyncSession, + db_session: AsyncSession, ) -> Workout: - result = await db.execute( + result = await db_session.execute( select(Workout).where( Workout.id == workout_id, ), @@ -30,7 +30,7 @@ async def get_owned_workout( async def query_exercises( - db: AsyncSession, + db_session: AsyncSession, base: bool, *where_clauses: Any, ) -> Sequence[Exercise]: @@ -41,12 +41,12 @@ async def query_exercises( ExerciseMuscleGroup.muscle_group ) ) - result = await db.execute(query) + result = await db_session.execute(query) return result.scalars().all() async def query_workout_exercises( - db: AsyncSession, + db_session: AsyncSession, *where_clauses: Any, ) -> Sequence[WorkoutExercise]: query = ( @@ -56,12 +56,12 @@ async def query_workout_exercises( selectinload(WorkoutExercise.exercise), selectinload(WorkoutExercise.sets), ) - result = await db.execute(query) + result = await db_session.execute(query) return result.scalars().all() async def query_sets( - db: AsyncSession, + db_session: AsyncSession, *where_clauses: Any, ) -> Sequence[Set]: query = ( @@ -73,5 +73,5 @@ async def query_sets( .where(*where_clauses) .order_by(Set.set_number) ) - result = await db.execute(query) + result = await db_session.execute(query) return result.scalars().all() diff --git a/server/app/services/workout.py b/server/app/services/workout.py index 2bbed907..2f8b6b80 100644 --- a/server/app/services/workout.py +++ b/server/app/services/workout.py @@ -23,7 +23,7 @@ async def _query_workouts( - db: AsyncSession, + db_session: AsyncSession, base: bool, *where_clauses: Any, ) -> Sequence[Workout]: @@ -33,14 +33,14 @@ async def _query_workouts( selectinload(Workout.exercises).selectinload(WorkoutExercise.exercise), selectinload(Workout.exercises).selectinload(WorkoutExercise.sets), ) - result = await db.execute(query) + result = await db_session.execute(query) return result.scalars().all() async def create_workout( user_id: int, req: CreateWorkoutRequest, - db: AsyncSession, + db_session: AsyncSession, ) -> None: logger.info(f"Creating workout for user {user_id}") @@ -50,18 +50,18 @@ async def create_workout( ended_at=req.ended_at, notes=req.notes, ) - db.add(workout) - await db.commit() + db_session.add(workout) + await db_session.commit() async def get_workouts( user_id: int, - db: AsyncSession, + db_session: AsyncSession, ) -> list[WorkoutBase]: logger.info(f"Getting workouts for user {user_id}") workouts = await _query_workouts( - db, + db_session, True, Workout.user_id == user_id, ) @@ -71,12 +71,12 @@ async def get_workouts( async def get_workout( workout_id: int, user_id: int, - db: AsyncSession, + db_session: AsyncSession, ) -> WorkoutPublic: logger.info(f"Getting workout {workout_id} for user {user_id}") workouts = await _query_workouts( - db, + db_session, False, Workout.id == workout_id, Workout.user_id == user_id, @@ -90,11 +90,11 @@ async def update_workout( workout_id: int, user_id: int, req: UpdateWorkoutRequest, - db: AsyncSession, + db_session: AsyncSession, ) -> None: logger.info(f"Updating workout {workout_id} for user {user_id}") - workout = await get_owned_workout(workout_id, user_id, db) + workout = await get_owned_workout(workout_id, user_id, db_session) if not req.model_fields_set: logger.info("No changes provided, skipping update") @@ -110,16 +110,16 @@ async def update_workout( if "notes" in req.model_fields_set: workout.notes = req.notes - await db.commit() + await db_session.commit() async def delete_workout( workout_id: int, user_id: int, - db: AsyncSession, + db_session: AsyncSession, ) -> None: logger.info(f"Deleting workout {workout_id} for user {user_id}") - workout = await get_owned_workout(workout_id, user_id, db) - await db.delete(workout) - await db.commit() + workout = await get_owned_workout(workout_id, user_id, db_session) + await db_session.delete(workout) + await db_session.commit() diff --git a/server/app/services/workout_exercise.py b/server/app/services/workout_exercise.py index 3fe9f48f..6474d78c 100644 --- a/server/app/services/workout_exercise.py +++ b/server/app/services/workout_exercise.py @@ -23,9 +23,9 @@ async def _get_next_workout_exercise_position( workout_id: int, - db: AsyncSession, + db_session: AsyncSession, ) -> int: - result = await db.execute( + result = await db_session.execute( select( func.coalesce(func.max(WorkoutExercise.position), 0), ).where( @@ -39,15 +39,15 @@ async def create_workout_exercise( workout_id: int, user_id: int, req: CreateWorkoutExerciseRequest, - db: AsyncSession, + db_session: AsyncSession, ) -> None: logger.info(f"Adding exercise {req.exercise_id} to workout {workout_id}") # validate workout existence & ownership - await get_owned_workout(workout_id, user_id, db) + await get_owned_workout(workout_id, user_id, db_session) exercises = await query_exercises( - db, + db_session, False, Exercise.id == req.exercise_id, (Exercise.user_id.is_(None)) | (Exercise.user_id == user_id), @@ -56,20 +56,20 @@ async def create_workout_exercise( if not exercise: raise ExerciseNotFound() - position = await _get_next_workout_exercise_position(workout_id, db) + position = await _get_next_workout_exercise_position(workout_id, db_session) workout_exercise = WorkoutExercise( workout_id=workout_id, exercise_id=req.exercise_id, position=position, notes=req.notes, ) - db.add(workout_exercise) + db_session.add(workout_exercise) try: - await db.commit() + await db_session.commit() except IntegrityError as e: logger.error(f"Integrity error creating workout exercise: {e}") - await db.rollback() + await db_session.rollback() if is_unique_violation(e, WORKOUT_EXERCISE_UNIQUE_CONSTRAINT): raise WorkoutExercisePositionConflict() raise @@ -79,13 +79,13 @@ async def delete_workout_exercise( workout_id: int, workout_exercise_id: int, user_id: int, - db: AsyncSession, + db_session: AsyncSession, ) -> None: logger.info(f"Removing workout exercise {workout_exercise_id} from {workout_id}") - await get_owned_workout(workout_id, user_id, db) + await get_owned_workout(workout_id, user_id, db_session) - result = await db.execute( + result = await db_session.execute( select(WorkoutExercise).where( WorkoutExercise.id == workout_exercise_id, WorkoutExercise.workout_id == workout_id, @@ -95,5 +95,5 @@ async def delete_workout_exercise( if not workout_exercise: raise WorkoutExerciseNotFound() - await db.delete(workout_exercise) - await db.commit() + await db_session.delete(workout_exercise) + await db_session.commit() diff --git a/server/app/tests/api/set/test_create_set.py b/server/app/tests/api/set/test_create_set.py index fa0b0ebe..c3994372 100644 --- a/server/app/tests/api/set/test_create_set.py +++ b/server/app/tests/api/set/test_create_set.py @@ -209,7 +209,7 @@ async def test_create_set_number_conflict( await db_session.commit() async def mock_get_next_set_number( - workout_exercise_id: int, db: AsyncSession + workout_exercise_id: int, db_session: AsyncSession ) -> int: return 1 diff --git a/server/app/tests/api/workout_exercise/test_create_workout_exercise.py b/server/app/tests/api/workout_exercise/test_create_workout_exercise.py index c79f092c..7b7d6f58 100644 --- a/server/app/tests/api/workout_exercise/test_create_workout_exercise.py +++ b/server/app/tests/api/workout_exercise/test_create_workout_exercise.py @@ -180,7 +180,7 @@ async def test_create_workout_exercise_position_conflict( position=1, ) - async def mock_get_next_position(workout_id: int, db: AsyncSession) -> int: + async def mock_get_next_position(workout_id: int, db_session: AsyncSession) -> int: return 1 monkeypatch.setattr( diff --git a/server/app/tests/core/dependencies/test_get_current_user.py b/server/app/tests/core/dependencies/test_get_current_user.py index 375e4360..bb464f38 100644 --- a/server/app/tests/core/dependencies/test_get_current_user.py +++ b/server/app/tests/core/dependencies/test_get_current_user.py @@ -23,7 +23,7 @@ async def test_get_current_user(db_session: AsyncSession, settings: Settings): secret=settings.jwt.secret_key, algorithm=settings.jwt.algorithm, ) - user = await get_current_user(token=token, db=db_session, settings=settings) + user = await get_current_user(token=token, db_session=db_session, settings=settings) assert user.username == settings.admin.username assert user.email == settings.admin.email @@ -40,7 +40,7 @@ async def test_get_current_user_missing_sub( ) with pytest.raises(InvalidCredentials): - await get_current_user(token=token, db=db_session, settings=settings) + await get_current_user(token=token, db_session=db_session, settings=settings) async def test_get_current_user_invalid_secret( @@ -53,7 +53,7 @@ async def test_get_current_user_invalid_secret( ) with pytest.raises(InvalidCredentials): - await get_current_user(token=token, db=db_session, settings=settings) + await get_current_user(token=token, db_session=db_session, settings=settings) async def test_get_current_user_expired_token( @@ -67,7 +67,7 @@ async def test_get_current_user_expired_token( ) with pytest.raises(InvalidCredentials): - await get_current_user(token=token, db=db_session, settings=settings) + await get_current_user(token=token, db_session=db_session, settings=settings) async def test_get_current_user_deleted_user( @@ -85,4 +85,4 @@ async def test_get_current_user_deleted_user( await db_session.commit() with pytest.raises(InvalidCredentials): - await get_current_user(token=token, db=db_session, settings=settings) + await get_current_user(token=token, db_session=db_session, settings=settings) diff --git a/server/app/tests/core/dependencies/test_get_db.py b/server/app/tests/core/dependencies/test_get_db_session.py similarity index 80% rename from server/app/tests/core/dependencies/test_get_db.py rename to server/app/tests/core/dependencies/test_get_db_session.py index 984fcf33..52ce9245 100644 --- a/server/app/tests/core/dependencies/test_get_db.py +++ b/server/app/tests/core/dependencies/test_get_db_session.py @@ -2,12 +2,12 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import Settings -from app.core.dependencies import get_db +from app.core.dependencies import get_db_session -async def test_get_db(anyio_backend: str, settings: Settings): +async def test_get_db_session(anyio_backend: str, settings: Settings): _ = anyio_backend - generator = get_db(settings) + generator = get_db_session(settings) db_session = await anext(generator) assert isinstance(db_session, AsyncSession) diff --git a/server/app/tests/core/security/test_authenticate_user.py b/server/app/tests/core/security/test_authenticate_user.py index 86c4c089..f37a523b 100644 --- a/server/app/tests/core/security/test_authenticate_user.py +++ b/server/app/tests/core/security/test_authenticate_user.py @@ -8,7 +8,7 @@ async def test_authenticate_user(db_session: AsyncSession, settings: Settings): user = await authenticate_user( identifier=settings.admin.username, password=settings.admin.password, - db=db_session, + db_session=db_session, ) assert user is not None @@ -22,7 +22,7 @@ async def test_authenticate_user_with_email( user = await authenticate_user( identifier=settings.admin.email, password=settings.admin.password, - db=db_session, + db_session=db_session, ) assert user is not None @@ -33,7 +33,7 @@ async def test_authenticate_user_not_found(db_session: AsyncSession): user = await authenticate_user( identifier="non_existent_user", password="some_password", - db=db_session, + db_session=db_session, ) assert user is None @@ -45,7 +45,7 @@ async def test_authenticate_user_invalid_password( user = await authenticate_user( identifier=settings.admin.username, password="some_password", - db=db_session, + db_session=db_session, ) assert user is None diff --git a/server/app/tests/core/security/test_get_token.py b/server/app/tests/core/security/test_get_token.py index 79bdb38e..3070dfc4 100644 --- a/server/app/tests/core/security/test_get_token.py +++ b/server/app/tests/core/security/test_get_token.py @@ -30,7 +30,7 @@ async def test_get_token_registration(db_session: AsyncSession): token_str, model=RegistrationToken, load_option=RegistrationToken.access_request, - db=db_session, + db_session=db_session, ) assert token is not None @@ -45,7 +45,7 @@ async def test_get_token_registration_invalid_token( "invalid-token", model=RegistrationToken, load_option=RegistrationToken.access_request, - db=db_session, + db_session=db_session, ) assert token is None @@ -63,7 +63,7 @@ async def test_get_token_registration_used_token( token_str, model=RegistrationToken, load_option=RegistrationToken.access_request, - db=db_session, + db_session=db_session, ) assert token is None @@ -81,7 +81,7 @@ async def test_get_token_registration_expired_token( token_str, model=RegistrationToken, load_option=RegistrationToken.access_request, - db=db_session, + db_session=db_session, ) assert token is None @@ -99,7 +99,7 @@ async def test_get_token_registration_invalid_hash( token_str, model=RegistrationToken, load_option=RegistrationToken.access_request, - db=db_session, + db_session=db_session, ) assert token is None @@ -114,7 +114,7 @@ async def test_get_registration_token(db_session: AsyncSession): token_str, model=RegistrationToken, load_option=RegistrationToken.access_request, - db=db_session, + db_session=db_session, ) assert token is not None @@ -132,7 +132,7 @@ async def test_get_password_reset_token(db_session: AsyncSession, settings: Sett token_str, model=type(_token), load_option=PasswordResetToken.user, - db=db_session, + db_session=db_session, ) assert token is not None diff --git a/server/app/tests/fixtures/client.py b/server/app/tests/fixtures/client.py index afe79472..cae96cd0 100644 --- a/server/app/tests/fixtures/client.py +++ b/server/app/tests/fixtures/client.py @@ -9,7 +9,7 @@ from app import create_app from app.core.config import Settings, get_settings -from app.core.dependencies import get_db, get_ms_client +from app.core.dependencies import get_db_session, get_ms_client from app.services.email import EmailService, get_email_service from app.services.github import GitHubService, get_github_service @@ -37,14 +37,14 @@ async def client( async def override_get_settings() -> Settings: return settings - async def override_get_db() -> AsyncGenerator[AsyncSession]: + async def override_get_db_session() -> AsyncGenerator[AsyncSession]: yield db_session async def override_get_ms_client() -> AsyncGenerator[MSAsyncClient]: yield ms_client fastapi_app.dependency_overrides[get_settings] = override_get_settings - fastapi_app.dependency_overrides[get_db] = override_get_db + fastapi_app.dependency_overrides[get_db_session] = override_get_db_session fastapi_app.dependency_overrides[get_ms_client] = override_get_ms_client fastapi_app.dependency_overrides[get_email_service] = lambda: mock_email_svc fastapi_app.dependency_overrides[get_github_service] = lambda: mock_github_svc @@ -56,7 +56,7 @@ async def override_get_ms_client() -> AsyncGenerator[MSAsyncClient]: ) finally: del fastapi_app.dependency_overrides[get_settings] - del fastapi_app.dependency_overrides[get_db] + del fastapi_app.dependency_overrides[get_db_session] del fastapi_app.dependency_overrides[get_ms_client] del fastapi_app.dependency_overrides[get_email_service] del fastapi_app.dependency_overrides[get_github_service] diff --git a/server/app/tests/services/admin/test_update_access_request_status.py b/server/app/tests/services/admin/test_update_access_request_status.py index 6a90e7c2..a64aabc5 100644 --- a/server/app/tests/services/admin/test_update_access_request_status.py +++ b/server/app/tests/services/admin/test_update_access_request_status.py @@ -34,7 +34,7 @@ async def test_update_access_request_status_approved( await update_access_request_status( access_request_id=access_request.id, status=AccessRequestStatus.APPROVED, - db=db_session, + db_session=db_session, user=admin_user, background_tasks=background_tasks, email_svc=mock_email_svc, @@ -86,7 +86,7 @@ async def test_update_access_request_status_rejected( await update_access_request_status( access_request_id=access_request.id, status=AccessRequestStatus.REJECTED, - db=db_session, + db_session=db_session, user=admin_user, background_tasks=background_tasks, email_svc=mock_email_svc, @@ -123,7 +123,7 @@ async def test_update_access_request_status_not_found( await update_access_request_status( access_request_id=9999, status=AccessRequestStatus.APPROVED, - db=db_session, + db_session=db_session, user=await get_admin_user_public(db_session, settings), background_tasks=BackgroundTasks(), email_svc=mock_email_svc, @@ -147,7 +147,7 @@ async def test_update_access_request_status_not_pending( await update_access_request_status( access_request_id=access_request.id, status=AccessRequestStatus.REJECTED, - db=db_session, + db_session=db_session, user=await get_admin_user_public(db_session, settings), background_tasks=BackgroundTasks(), email_svc=mock_email_svc, diff --git a/server/app/tests/services/auth/test_login.py b/server/app/tests/services/auth/test_login.py index 1f037ae2..c93352c7 100644 --- a/server/app/tests/services/auth/test_login.py +++ b/server/app/tests/services/auth/test_login.py @@ -11,7 +11,7 @@ async def test_login(db_session: AsyncSession, settings: Settings): result = await login( identifier=settings.admin.username, password=settings.admin.password, - db=db_session, + db_session=db_session, settings=settings, ) payload = jwt.decode( @@ -28,7 +28,7 @@ async def test_login_with_email(db_session: AsyncSession, settings: Settings): result = await login( identifier=settings.admin.email, password=settings.admin.password, - db=db_session, + db_session=db_session, settings=settings, ) payload = jwt.decode( @@ -46,7 +46,7 @@ async def test_login_user_not_found(db_session: AsyncSession, settings: Settings await login( identifier="non_existent_user", password="some_password", - db=db_session, + db_session=db_session, settings=settings, ) @@ -56,6 +56,6 @@ async def test_login_invalid_password(db_session: AsyncSession, settings: Settin await login( identifier=settings.admin.username, password="some_password", - db=db_session, + db_session=db_session, settings=settings, ) diff --git a/server/app/tests/services/auth/test_register.py b/server/app/tests/services/auth/test_register.py index 2aa3d525..44fb7eed 100644 --- a/server/app/tests/services/auth/test_register.py +++ b/server/app/tests/services/auth/test_register.py @@ -30,7 +30,7 @@ async def test_register(db_session: AsyncSession): token_str=token_str, username="new_user", password="new_password", - db=db_session, + db_session=db_session, ) user = ( @@ -52,7 +52,7 @@ async def test_register_invalid_token(db_session: AsyncSession): token_str="invalid-token", username="new_user", password="new_password", - db=db_session, + db_session=db_session, ) @@ -76,7 +76,7 @@ async def test_register_used_token(db_session: AsyncSession): token_str=token_str, username="new_user", password="new_password", - db=db_session, + db_session=db_session, ) @@ -100,7 +100,7 @@ async def test_register_expired_token(db_session: AsyncSession): token_str=token_str, username="new_user", password="new_password", - db=db_session, + db_session=db_session, ) @@ -123,7 +123,7 @@ async def test_register_access_request_not_approved(db_session: AsyncSession): token_str=token_str, username="pending_user", password="new_password", - db=db_session, + db_session=db_session, ) @@ -156,7 +156,7 @@ async def test_register_username_taken(db_session: AsyncSession): token_str=token_str, username="taken", password="new_password", - db=db_session, + db_session=db_session, ) @@ -190,5 +190,5 @@ async def test_register_username_matches_email(db_session: AsyncSession): token_str=token_str, username=collision_identifier, password="new_password", - db=db_session, + db_session=db_session, ) diff --git a/server/app/tests/services/auth/test_request_access.py b/server/app/tests/services/auth/test_request_access.py index b83c5832..4aaf2398 100644 --- a/server/app/tests/services/auth/test_request_access.py +++ b/server/app/tests/services/auth/test_request_access.py @@ -25,7 +25,7 @@ async def test_request_access( first_name="New", last_name="User", background_tasks=background_tasks, - db=db_session, + db_session=db_session, email_svc=mock_email_svc, settings=settings, ) @@ -59,7 +59,7 @@ async def test_request_access_approved( first_name="Test", last_name="User", background_tasks=background_tasks, - db=db_session, + db_session=db_session, email_svc=mock_email_svc, settings=settings, ) @@ -94,7 +94,7 @@ async def test_request_access_existing_user( first_name="Test", last_name="User", background_tasks=background_tasks, - db=db_session, + db_session=db_session, email_svc=mock_email_svc, settings=settings, ) @@ -126,7 +126,7 @@ async def test_request_access_email_matches_username( first_name="Test", last_name="User", background_tasks=background_tasks, - db=db_session, + db_session=db_session, email_svc=mock_email_svc, settings=settings, ) @@ -153,7 +153,7 @@ async def test_request_access_pending( first_name="Test", last_name="User", background_tasks=background_tasks, - db=db_session, + db_session=db_session, email_svc=mock_email_svc, settings=settings, ) @@ -180,7 +180,7 @@ async def test_request_access_rejected( first_name="Test", last_name="User", background_tasks=background_tasks, - db=db_session, + db_session=db_session, email_svc=mock_email_svc, settings=settings, ) diff --git a/server/app/tests/services/auth/test_request_password_reset.py b/server/app/tests/services/auth/test_request_password_reset.py index 38b75f20..b9022bb1 100644 --- a/server/app/tests/services/auth/test_request_password_reset.py +++ b/server/app/tests/services/auth/test_request_password_reset.py @@ -29,7 +29,7 @@ async def test_request_password_reset( await request_password_reset( email=user.email, background_tasks=background_tasks, - db=db_session, + db_session=db_session, email_svc=mock_email_svc, settings=settings, ) @@ -61,7 +61,7 @@ async def test_request_password_reset_unregistered_email( await request_password_reset( email="missing@example.com", background_tasks=background_tasks, - db=db_session, + db_session=db_session, email_svc=mock_email_svc, settings=settings, ) @@ -79,7 +79,7 @@ async def test_request_password_reset_admin_email( await request_password_reset( email=settings.admin.email, background_tasks=background_tasks, - db=db_session, + db_session=db_session, email_svc=mock_email_svc, settings=settings, ) diff --git a/server/app/tests/services/auth/test_reset_password.py b/server/app/tests/services/auth/test_reset_password.py index b4f5437f..e9cb692a 100644 --- a/server/app/tests/services/auth/test_reset_password.py +++ b/server/app/tests/services/auth/test_reset_password.py @@ -31,7 +31,7 @@ async def test_reset_password(db_session: AsyncSession): await reset_password( token_str=token_str, password="new_password", - db=db_session, + db_session=db_session, ) assert user.password_hash != old_hash @@ -47,7 +47,7 @@ async def test_reset_password_invalid_token(db_session: AsyncSession): await reset_password( token_str="invalid-token", password="new_password", - db=db_session, + db_session=db_session, ) @@ -71,7 +71,7 @@ async def test_reset_password_used_token(db_session: AsyncSession): await reset_password( token_str=token_str, password="new_password", - db=db_session, + db_session=db_session, ) @@ -95,5 +95,5 @@ async def test_reset_password_expired_token(db_session: AsyncSession): await reset_password( token_str=token_str, password="new_password", - db=db_session, + db_session=db_session, ) diff --git a/server/app/tests/services/feedback/test_create_feedback_service.py b/server/app/tests/services/feedback/test_create_feedback_service.py index 26151779..a6c46889 100644 --- a/server/app/tests/services/feedback/test_create_feedback_service.py +++ b/server/app/tests/services/feedback/test_create_feedback_service.py @@ -43,7 +43,7 @@ async def test_create_feedback( await create_feedback( user=user, req=request, - db=db_session, + db_session=db_session, github_svc=mock_github_svc, settings=settings, ) @@ -90,7 +90,7 @@ async def test_create_feedback_no_files( await create_feedback( user=user, req=request, - db=db_session, + db_session=db_session, github_svc=mock_github_svc, settings=settings, ) diff --git a/server/app/tests/services/search/test_get_task.py b/server/app/tests/services/search/test_get_task.py index 4d95a00e..9a1bc3de 100644 --- a/server/app/tests/services/search/test_get_task.py +++ b/server/app/tests/services/search/test_get_task.py @@ -2,8 +2,8 @@ from meilisearch_python_sdk.models.task import TaskResult from sqlalchemy.ext.asyncio import AsyncSession -from app.services.search import ( # pyright: ignore[reportPrivateUsage] - _index_muscle_groups, +from app.services.search import ( + _index_muscle_groups, # pyright: ignore[reportPrivateUsage] get_task, ) diff --git a/server/app/tests/services/search/test_reindex_data.py b/server/app/tests/services/search/test_reindex_data.py index c198ac2e..0df171c3 100644 --- a/server/app/tests/services/search/test_reindex_data.py +++ b/server/app/tests/services/search/test_reindex_data.py @@ -12,10 +12,10 @@ async def test_reindex_data( ): calls: list[str] = [] - async def fake_muscle_groups(db: AsyncSession, client: AsyncClient) -> None: + async def fake_muscle_groups(db_session: AsyncSession, client: AsyncClient) -> None: calls.append("muscle") - async def fake_exercises(db: AsyncSession, client: AsyncClient) -> None: + async def fake_exercises(db_session: AsyncSession, client: AsyncClient) -> None: calls.append("exercise") monkeypatch.setattr("app.services.search._index_muscle_groups", fake_muscle_groups) diff --git a/server/app/tests/services/set/test_create_set.py b/server/app/tests/services/set/test_create_set.py index 1f27d8b6..8f065f36 100644 --- a/server/app/tests/services/set/test_create_set.py +++ b/server/app/tests/services/set/test_create_set.py @@ -45,7 +45,7 @@ async def test_create_set(db_session: AsyncSession): unit=SetUnit.lb, notes="Test set", ), - db=db_session, + db_session=db_session, ) result = await db_session.execute( @@ -70,7 +70,7 @@ async def test_create_set_workout_not_found(db_session: AsyncSession): workout_exercise_id=2, user_id=3, req=CreateSetRequest(), - db=db_session, + db_session=db_session, ) @@ -85,7 +85,7 @@ async def test_create_set_workout_not_allowed(db_session: AsyncSession): workout_exercise_id=2, user_id=user_1.id, req=CreateSetRequest(), - db=db_session, + db_session=db_session, ) @@ -99,7 +99,7 @@ async def test_create_set_workout_exercise_not_found(db_session: AsyncSession): workout_exercise_id=2, user_id=user.id, req=CreateSetRequest(), - db=db_session, + db_session=db_session, ) @@ -123,7 +123,7 @@ async def test_create_set_workout_exercise_not_allowed(db_session: AsyncSession) workout_exercise_id=workout_exercise.id, user_id=user_1.id, req=CreateSetRequest(), - db=db_session, + db_session=db_session, ) @@ -148,7 +148,7 @@ async def test_create_set_set_number_conflict( ) async def mock_get_next_set_number( - workout_exercise_id: int, db: AsyncSession + workout_exercise_id: int, db_session: AsyncSession ) -> int: return 1 @@ -162,7 +162,7 @@ async def mock_get_next_set_number( workout_exercise_id=workout_exercise.id, user_id=user.id, req=CreateSetRequest(), - db=db_session, + db_session=db_session, ) @@ -187,7 +187,7 @@ async def test_create_set_unhandled_integrity_error( ) async def mock_get_next_set_number( - workout_exercise_id: int, db: AsyncSession + workout_exercise_id: int, db_session: AsyncSession ) -> int: return 1 @@ -202,5 +202,5 @@ async def mock_get_next_set_number( workout_exercise_id=workout_exercise.id, user_id=user.id, req=CreateSetRequest(), - db=db_session, + db_session=db_session, ) diff --git a/server/app/tests/services/set/test_delete_set.py b/server/app/tests/services/set/test_delete_set.py index d7f27a1a..ae7eb735 100644 --- a/server/app/tests/services/set/test_delete_set.py +++ b/server/app/tests/services/set/test_delete_set.py @@ -38,7 +38,7 @@ async def test_delete_set(db_session: AsyncSession): workout_exercise_id=workout_exercise.id, set_id=set_.id, user_id=user.id, - db=db_session, + db_session=db_session, ) result = await db_session.execute( @@ -56,7 +56,7 @@ async def test_delete_set_workout_not_found(db_session: AsyncSession): workout_exercise_id=2, set_id=3, user_id=4, - db=db_session, + db_session=db_session, ) @@ -71,7 +71,7 @@ async def test_delete_set_workout_not_allowed(db_session: AsyncSession): workout_exercise_id=2, set_id=3, user_id=user_1.id, - db=db_session, + db_session=db_session, ) @@ -92,5 +92,5 @@ async def test_delete_set_not_found(db_session: AsyncSession): workout_exercise_id=workout_exercise.id, set_id=3, user_id=user.id, - db=db_session, + db_session=db_session, ) diff --git a/server/app/tests/services/set/test_update_set.py b/server/app/tests/services/set/test_update_set.py index cc783531..3c17101c 100644 --- a/server/app/tests/services/set/test_update_set.py +++ b/server/app/tests/services/set/test_update_set.py @@ -50,7 +50,7 @@ async def test_update_set(db_session: AsyncSession): unit=SetUnit.kg, notes="Updated set", ), - db=db_session, + db_session=db_session, ) set_ = await db_session.get(Set, set_.id) @@ -70,7 +70,7 @@ async def test_update_set_workout_not_found(db_session: AsyncSession): set_id=3, user_id=4, req=UpdateSetRequest(), - db=db_session, + db_session=db_session, ) @@ -86,7 +86,7 @@ async def test_update_set_workout_not_allowed(db_session: AsyncSession): set_id=3, user_id=user_1.id, req=UpdateSetRequest(), - db=db_session, + db_session=db_session, ) @@ -108,7 +108,7 @@ async def test_update_set_not_found(db_session: AsyncSession): set_id=3, user_id=user.id, req=UpdateSetRequest(), - db=db_session, + db_session=db_session, ) @@ -138,7 +138,7 @@ async def test_update_set_no_changes(db_session: AsyncSession): set_id=set_.id, user_id=user.id, req=UpdateSetRequest(), - db=db_session, + db_session=db_session, ) set_ = await db_session.get(Set, set_.id) @@ -180,7 +180,7 @@ async def test_update_set_no_reps(db_session: AsyncSession): unit=SetUnit.kg, notes="Updated set", ), - db=db_session, + db_session=db_session, ) set_ = await db_session.get(Set, set_.id) @@ -222,7 +222,7 @@ async def test_update_set_no_weight(db_session: AsyncSession): unit=SetUnit.kg, notes="Updated set", ), - db=db_session, + db_session=db_session, ) set_ = await db_session.get(Set, set_.id) @@ -264,7 +264,7 @@ async def test_update_set_no_unit(db_session: AsyncSession): weight=Decimal(150), notes="Updated set", ), - db=db_session, + db_session=db_session, ) set_ = await db_session.get(Set, set_.id) @@ -306,7 +306,7 @@ async def test_update_set_no_notes(db_session: AsyncSession): weight=Decimal(150), unit=SetUnit.kg, ), - db=db_session, + db_session=db_session, ) set_ = await db_session.get(Set, set_.id) @@ -349,7 +349,7 @@ async def test_update_set_null_values(db_session: AsyncSession): unit=None, notes=None, ), - db=db_session, + db_session=db_session, ) set_ = await db_session.get(Set, set_.id) diff --git a/server/app/tests/services/token/test_get_tokens.py b/server/app/tests/services/token/test_get_tokens.py index 2671cd80..2754d3b4 100644 --- a/server/app/tests/services/token/test_get_tokens.py +++ b/server/app/tests/services/token/test_get_tokens.py @@ -33,7 +33,7 @@ async def test_get_tokens_by_prefix_password_reset(db_session: AsyncSession): type(token), load_option=type(token).user, prefix=token.token_prefix, - db=db_session, + db_session=db_session, ) assert len(tokens) == 1 @@ -68,7 +68,7 @@ async def test_get_tokens_by_prefix_password_reset_condition(db_session: AsyncSe type(active_token), load_option=type(active_token).user, prefix=prefix, - db=db_session, + db_session=db_session, ) assert [token.id for token in tokens] == [active_token.id] @@ -94,7 +94,7 @@ async def test_get_tokens_by_prefix_password_reset_ordering(db_session: AsyncSes type(older_token), load_option=type(older_token).user, prefix=prefix, - db=db_session, + db_session=db_session, ) assert [token.id for token in tokens] == [newer_token.id, older_token.id] diff --git a/server/app/tests/services/workout_exercise/test_create_workout_exercise.py b/server/app/tests/services/workout_exercise/test_create_workout_exercise.py index 6fde3b52..a7078c00 100644 --- a/server/app/tests/services/workout_exercise/test_create_workout_exercise.py +++ b/server/app/tests/services/workout_exercise/test_create_workout_exercise.py @@ -54,7 +54,7 @@ async def test_create_workout_exercise_workout_not_found(db_session: AsyncSessio workout_id=1, user_id=2, req=CreateWorkoutExerciseRequest(exercise_id=3), - db=db_session, + db_session=db_session, ) @@ -69,7 +69,7 @@ async def test_create_workout_exercise_workout_not_allowed(db_session: AsyncSess workout_id=workout.id, user_id=user_1.id, req=CreateWorkoutExerciseRequest(exercise_id=exercise.id), - db=db_session, + db_session=db_session, ) @@ -82,7 +82,7 @@ async def test_create_workout_exercise_exercise_not_found(db_session: AsyncSessi workout_id=workout.id, user_id=user.id, req=CreateWorkoutExerciseRequest(exercise_id=99999), - db=db_session, + db_session=db_session, ) @@ -120,7 +120,7 @@ async def test_create_workout_exercise_position_conflict( position=1, ) - async def mock_get_next_position(workout_id: int, db: AsyncSession) -> int: + async def mock_get_next_position(workout_id: int, db_session: AsyncSession) -> int: return 1 monkeypatch.setattr( @@ -152,7 +152,7 @@ async def test_create_workout_exercise_unhandled_integrity_error( position=1, ) - async def mock_get_next_position(workout_id: int, db: AsyncSession) -> int: + async def mock_get_next_position(workout_id: int, db_session: AsyncSession) -> int: return 1 monkeypatch.setattr( diff --git a/server/app/tests/services/workout_exercise/test_delete_workout_exercise.py b/server/app/tests/services/workout_exercise/test_delete_workout_exercise.py index 539d8c20..96c33624 100644 --- a/server/app/tests/services/workout_exercise/test_delete_workout_exercise.py +++ b/server/app/tests/services/workout_exercise/test_delete_workout_exercise.py @@ -44,7 +44,7 @@ async def test_delete_workout_exercise_workout_not_found(db_session: AsyncSessio workout_id=1, workout_exercise_id=2, user_id=3, - db=db_session, + db_session=db_session, ) @@ -58,7 +58,7 @@ async def test_delete_workout_exercise_workout_not_allowed(db_session: AsyncSess workout_id=workout.id, workout_exercise_id=1, user_id=user_1.id, - db=db_session, + db_session=db_session, ) From a7a049c91e428bb3eb6cafbfeb658605d1f2de09 Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Wed, 25 Mar 2026 18:22:51 -0500 Subject: [PATCH 20/27] client - update deps --- client/package-lock.json | 46 +++++++++++++------- client/package.json | 4 +- client/src/api/generated/client/utils.gen.ts | 4 +- client/src/api/generated/core/types.gen.ts | 2 +- client/src/api/generated/core/utils.gen.ts | 2 +- 5 files changed, 36 insertions(+), 22 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 84f95aea..4df3941b 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,7 +8,7 @@ "name": "client", "version": "0.0.0", "dependencies": { - "@hey-api/openapi-ts": "^0.94.4", + "@hey-api/openapi-ts": "^0.94.5", "@hookform/resolvers": "^5.2.2", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", @@ -25,7 +25,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "lucide-react": "^1.0.1", + "lucide-react": "^1.7.0", "mermaid": "^11.13.0", "next-themes": "^0.4.6", "radix-ui": "^1.4.3", @@ -653,14 +653,15 @@ } }, "node_modules/@hey-api/openapi-ts": { - "version": "0.94.4", - "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.94.4.tgz", - "integrity": "sha512-943f7wlLAQ0KHVx8CeD3xYJzBsCRQYtr+lgMYrAdfV48j32loqsqiMAM4fsMxvSO7mkz0lqcJkIb+YZIXO8Ubg==", + "version": "0.94.5", + "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.94.5.tgz", + "integrity": "sha512-fCR/kIexbDarnt/WGKvjJb4K30JaFzO2F/528kHpyWT7vopPS0JeqtRQMjJg+Gk09N/05nbv1OaFOQXcy0BiVQ==", "license": "MIT", "dependencies": { "@hey-api/codegen-core": "0.7.4", "@hey-api/json-schema-ref-parser": "1.3.1", - "@hey-api/shared": "0.2.5", + "@hey-api/shared": "0.2.6", + "@hey-api/spec-types": "0.1.0", "@hey-api/types": "0.1.4", "ansi-colors": "4.1.3", "color-support": "1.1.3", @@ -677,17 +678,18 @@ "url": "https://github.com/sponsors/hey-api" }, "peerDependencies": { - "typescript": ">=5.5.3 || 6.0.1-rc" + "typescript": ">=5.5.3 || >=6.0.0 || 6.0.1-rc" } }, "node_modules/@hey-api/shared": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@hey-api/shared/-/shared-0.2.5.tgz", - "integrity": "sha512-NS57dHXxhwBtenPWAzljA0I493ZxzjcZZtr8KFr8SsLboofdjcAbVRFAfOQ0iv2JMhOe9oyiWEEc1QOJ/9WWaw==", + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@hey-api/shared/-/shared-0.2.6.tgz", + "integrity": "sha512-ZZrsWbazJcJO688tJVEBeei03B4miPI7OauW+qLMYP/9KL6NadmA5MjqsIIwgfvb0HKMAR7lt4AINKzv0Zwdgw==", "license": "MIT", "dependencies": { "@hey-api/codegen-core": "0.7.4", "@hey-api/json-schema-ref-parser": "1.3.1", + "@hey-api/spec-types": "0.1.0", "@hey-api/types": "0.1.4", "ansi-colors": "4.1.3", "cross-spawn": "7.0.6", @@ -713,6 +715,18 @@ "node": ">=10" } }, + "node_modules/@hey-api/spec-types": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@hey-api/spec-types/-/spec-types-0.1.0.tgz", + "integrity": "sha512-StS4RrAO5pyJCBwe6uF9MAuPflkztriW+FPnVb7oEjzDYv1sxPwP+f7fL6u6D+UVrKpZ/9bPNx/xXVdkeWPU6A==", + "license": "MIT", + "dependencies": { + "@hey-api/types": "0.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + } + }, "node_modules/@hey-api/types": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/@hey-api/types/-/types-0.1.4.tgz", @@ -7646,9 +7660,9 @@ } }, "node_modules/lucide-react": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.0.1.tgz", - "integrity": "sha512-lih7tKEczCYOQjVEzpFuxEuNzlwf+1yhvlMlEkGWJM3va8Pugv8bYXc/pRtcjPncaP7k84X0Pt/71ufxvqEPtQ==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.7.0.tgz", + "integrity": "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -8872,9 +8886,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" diff --git a/client/package.json b/client/package.json index fb6a4948..1757ef55 100644 --- a/client/package.json +++ b/client/package.json @@ -18,7 +18,7 @@ ] }, "dependencies": { - "@hey-api/openapi-ts": "^0.94.4", + "@hey-api/openapi-ts": "^0.94.5", "@hookform/resolvers": "^5.2.2", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", @@ -35,7 +35,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "lucide-react": "^1.0.1", + "lucide-react": "^1.7.0", "mermaid": "^11.13.0", "next-themes": "^0.4.6", "radix-ui": "^1.4.3", diff --git a/client/src/api/generated/client/utils.gen.ts b/client/src/api/generated/client/utils.gen.ts index 1b4600f7..ee02336a 100644 --- a/client/src/api/generated/client/utils.gen.ts +++ b/client/src/api/generated/client/utils.gen.ts @@ -130,7 +130,7 @@ export const buildUrl: Client['buildUrl'] = (options) => { const instanceBaseUrl = options.axios?.defaults?.baseURL; const baseUrl = - !!options.baseURL && typeof options.baseURL === 'string' ? options.baseURL : instanceBaseUrl; + options.baseURL && typeof options.baseURL === 'string' ? options.baseURL : instanceBaseUrl; return getUrl({ baseUrl: baseUrl as string, @@ -192,7 +192,7 @@ export const mergeHeaders = ( mergedHeaders[key] = [...(mergedHeaders[key] ?? []), v as string]; } } else if (value !== undefined) { - // assume object headers are meant to be JSON stringified, i.e. their + // assume object headers are meant to be JSON stringified, i.e., their // content value in OpenAPI specification is 'application/json' mergedHeaders[key] = typeof value === 'object' ? JSON.stringify(value) : (value as string); } diff --git a/client/src/api/generated/core/types.gen.ts b/client/src/api/generated/core/types.gen.ts index 97463257..9efe71d4 100644 --- a/client/src/api/generated/core/types.gen.ts +++ b/client/src/api/generated/core/types.gen.ts @@ -80,7 +80,7 @@ export interface Config { requestValidator?: (data: unknown) => Promise; /** * A function transforming response data before it's returned. This is useful - * for post-processing data, e.g. converting ISO strings into Date objects. + * for post-processing data, e.g., converting ISO strings into Date objects. */ responseTransformer?: (data: unknown) => Promise; /** diff --git a/client/src/api/generated/core/utils.gen.ts b/client/src/api/generated/core/utils.gen.ts index e7ddbe35..9a4fec78 100644 --- a/client/src/api/generated/core/utils.gen.ts +++ b/client/src/api/generated/core/utils.gen.ts @@ -126,7 +126,7 @@ export function getValidRequestBody(options: { return hasSerializedBody ? options.serializedBody : null; } - // not all clients implement a serializedBody property (i.e. client-axios) + // not all clients implement a serializedBody property (i.e., client-axios) return options.body !== '' ? options.body : null; } From bc61e73827f0ec1d929fa68b256cf454d8f280cc Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Wed, 25 Mar 2026 18:40:33 -0500 Subject: [PATCH 21/27] server - add deptry --- .github/workflows/test-server.yml | 4 ++ server/makefile | 3 ++ server/pyproject.toml | 10 +++++ server/uv.lock | 68 +++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+) diff --git a/.github/workflows/test-server.yml b/.github/workflows/test-server.yml index 34bdbbbc..2e3bbf96 100644 --- a/.github/workflows/test-server.yml +++ b/.github/workflows/test-server.yml @@ -36,6 +36,10 @@ jobs: working-directory: server run: uv sync + - name: run deptry + working-directory: server + run: make deptry + - name: run type checks working-directory: server run: make check diff --git a/server/makefile b/server/makefile index 5b1ac774..feafda59 100644 --- a/server/makefile +++ b/server/makefile @@ -17,6 +17,9 @@ cov: open-cov: open -a "Google Chrome" htmlcov/index.html +deptry: + uv run deptry . + check: uv run pyright diff --git a/server/pyproject.toml b/server/pyproject.toml index f94ecb1e..8e0e1d49 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -8,17 +8,21 @@ dependencies = [ "asyncpg>=0.31.0", "fastapi-swagger-dark>=0.0.9", "fastapi[standard]>=0.124.4", + "httpx>=0.28.1", "meilisearch-python-sdk>=7.1.1", "pwdlib[argon2]>=0.3.0", + "pydantic>=2.12.5", "pydantic-settings>=2.12.0", "pyjwt>=2.10.1", "python-dotenv>=1.2.1", "python-json-logger>=4.0.0", "sqlalchemy>=2.0.45", + "starlette>=0.50.0", ] [dependency-groups] dev = [ + "deptry>=0.25.1", "greenlet>=3.3.0", "pyright>=1.1.408", "pytest>=9.0.2", @@ -46,3 +50,9 @@ show_missing = true [tool.coverage.html] directory = "htmlcov" + +[tool.deptry] +exclude = ["app/tests/**", ".venv/"] + +[tool.deptry.per_rule_ignores] +DEP002 = ["asyncpg"] diff --git a/server/uv.lock b/server/uv.lock index e23bce14..d1f5a612 100644 --- a/server/uv.lock +++ b/server/uv.lock @@ -265,6 +265,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, ] +[[package]] +name = "deptry" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "packaging" }, + { name = "requirements-parser" }, + { name = "tomli", marker = "python_full_version < '3.15'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/b2/50ccc99362ae7757342978b7ecb3b98e47fade721fd617d74db1948ec3a1/deptry-0.25.1.tar.gz", hash = "sha256:45c8cd982c85cd4faae573ddff6920de7eec735336db6973f26a765ae7950f7d", size = 509748, upload-time = "2026-03-18T23:22:18.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/1d/b538dc635e873b25360d761cfe1fa0ccd7d6c69b698047e552f33401e60d/deptry-0.25.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:a4dd1148db24a1ddacfa8b840836c6019c2f864fcb7579dd089fd217606338c8", size = 1850319, upload-time = "2026-03-18T23:22:15.65Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a9/511477a8f0ae4f6021d68a80bdca77e7ffb0722008dc24ee5d9ef49f5c88/deptry-0.25.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c67c666d916ef12013c0772e40d78be0f21577a495d8d99ec5fcb18c332d393d", size = 1759259, upload-time = "2026-03-18T23:22:30.853Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4b/c9f0bdda410912a6df79a789cb118fa29acae02a397794ead3c84adcda5c/deptry-0.25.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58d39279828dbf4efc1abb40bf50a71b21499c36759bed5a8d8a3c0e3149b091", size = 1872012, upload-time = "2026-03-18T23:22:19.145Z" }, + { url = "https://files.pythonhosted.org/packages/72/9c/6f6f9125bac74b5d5d2af89536cbdb3fa159b6466aa097b74e7e85e8e030/deptry-0.25.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14bfcc28b4326ed8c6abb30691b19077d4ef8613cfba6c37ef5b1f471775bf6f", size = 1926575, upload-time = "2026-03-18T23:22:11.269Z" }, + { url = "https://files.pythonhosted.org/packages/52/48/2a5e705a7f898295966ade67bd1223e2af96da433e25b39f6b9483ba2c7b/deptry-0.25.1-cp310-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:555f5f9a487899ec9bf301eecba1745e14d212c4b354f4d3a5fd691e907366d3", size = 2050816, upload-time = "2026-03-18T23:22:27.439Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c6/50f189a894e1f3bf21266299112c8a06cb731838976e1b9a9cadd0b4a86e/deptry-0.25.1-cp310-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:18d21b3545ab2bfec53f3f45c6f5f201d55f713323327f8d12674505469ae6b7", size = 2145416, upload-time = "2026-03-18T23:22:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/7a/6a/3f82f7a06217778282bc4456af1b4ffb3bc4b2c8e7891d00e8323f9ad0b8/deptry-0.25.1-cp310-abi3-win_amd64.whl", hash = "sha256:b59a560cb7dffb21832a98bb80d33d614cfb5630ea36ce21833eabf4eae3df99", size = 1718489, upload-time = "2026-03-18T23:22:28.589Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7f/cd6b3ac8cf95f2f1c5c7a74ff6452e9098af89a9b56607381f677880641e/deptry-0.25.1-cp310-abi3-win_arm64.whl", hash = "sha256:6efffd8116fb9d2c45a251382ce4ce1c38dbb17179f581ec9231ed5390f7fc12", size = 1647020, upload-time = "2026-03-18T23:22:23.311Z" }, +] + [[package]] name = "dnspython" version = "2.8.0" @@ -892,6 +915,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "requirements-parser" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [{ name = "packaging" }] +sdist = { url = "https://files.pythonhosted.org/packages/95/96/fb6dbfebb524d5601d359a47c78fe7ba1eef90fc4096404aa60c9a906fbb/requirements_parser-0.13.0.tar.gz", hash = "sha256:0843119ca2cb2331de4eb31b10d70462e39ace698fd660a915c247d2301a4418", size = 22630, upload-time = "2025-05-21T13:42:05.464Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/60/50fbb6ffb35f733654466f1a90d162bcbea358adc3b0871339254fbc37b2/requirements_parser-0.13.0-py3-none-any.whl", hash = "sha256:2b3173faecf19ec5501971b7222d38f04cb45bb9d87d0ad629ca71e2e62ded14", size = 14782, upload-time = "2025-05-21T13:42:04.007Z" }, +] + [[package]] name = "rich" version = "14.2.0" @@ -1003,19 +1036,23 @@ dependencies = [ "standard", ] }, { name = "fastapi-swagger-dark" }, + { name = "httpx" }, { name = "meilisearch-python-sdk" }, { name = "pwdlib", extra = [ "argon2", ] }, + { name = "pydantic" }, { name = "pydantic-settings" }, { name = "pyjwt" }, { name = "python-dotenv" }, { name = "python-json-logger" }, { name = "sqlalchemy" }, + { name = "starlette" }, ] [package.dev-dependencies] dev = [ + { name = "deptry" }, { name = "greenlet" }, { name = "pyright" }, { name = "pytest" }, @@ -1037,19 +1074,23 @@ requires-dist = [ "standard", ], specifier = ">=0.124.4" }, { name = "fastapi-swagger-dark", specifier = ">=0.0.9" }, + { name = "httpx", specifier = ">=0.28.1" }, { name = "meilisearch-python-sdk", specifier = ">=7.1.1" }, { name = "pwdlib", extras = [ "argon2", ], specifier = ">=0.3.0" }, + { name = "pydantic", specifier = ">=2.12.5" }, { name = "pydantic-settings", specifier = ">=2.12.0" }, { name = "pyjwt", specifier = ">=2.10.1" }, { name = "python-dotenv", specifier = ">=1.2.1" }, { name = "python-json-logger", specifier = ">=4.0.0" }, { name = "sqlalchemy", specifier = ">=2.0.45" }, + { name = "starlette", specifier = ">=0.50.0" }, ] [package.metadata.requires-dev] dev = [ + { name = "deptry", specifier = ">=0.25.1" }, { name = "greenlet", specifier = ">=3.3.0" }, { name = "pyright", specifier = ">=1.1.408" }, { name = "pytest", specifier = ">=9.0.2" }, @@ -1121,6 +1162,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/2d/26b8b30067d94339afee62c3edc9b803a6eb9332f521ba77d8aaab5de873/testcontainers-4.14.2-py3-none-any.whl", hash = "sha256:0d0522c3cd8f8d9627cda41f7a6b51b639fa57bdc492923c045117933c668d68", size = 125712, upload-time = "2026-03-18T05:19:15.29Z" }, ] +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + [[package]] name = "typer" version = "0.20.0" From 837002cd602a5b353fff64d1e192eff6fcb8e4f3 Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Wed, 25 Mar 2026 18:48:11 -0500 Subject: [PATCH 22/27] server - update deps --- client/src/api/generated/schemas.gen.ts | 9 +- client/src/api/generated/types.gen.ts | 10 + client/src/api/generated/zod.gen.ts | 4 +- server/uv.lock | 393 +++++++++++++----------- 4 files changed, 241 insertions(+), 175 deletions(-) diff --git a/client/src/api/generated/schemas.gen.ts b/client/src/api/generated/schemas.gen.ts index 1e256a25..209f0009 100644 --- a/client/src/api/generated/schemas.gen.ts +++ b/client/src/api/generated/schemas.gen.ts @@ -140,7 +140,7 @@ export const CreateFeedbackRequestSchema = { files: { items: { type: 'string', - format: 'binary' + contentMediaType: 'application/octet-stream' }, type: 'array', title: 'Files' @@ -978,6 +978,13 @@ export const ValidationErrorSchema = { type: { type: 'string', title: 'Error Type' + }, + input: { + title: 'Input' + }, + ctx: { + type: 'object', + title: 'Context' } }, type: 'object', diff --git a/client/src/api/generated/types.gen.ts b/client/src/api/generated/types.gen.ts index 9e231763..fdd6804f 100644 --- a/client/src/api/generated/types.gen.ts +++ b/client/src/api/generated/types.gen.ts @@ -520,6 +520,16 @@ export type ValidationError = { * Error Type */ type: string; + /** + * Input + */ + input?: unknown; + /** + * Context + */ + ctx?: { + [key: string]: unknown; + }; }; /** diff --git a/client/src/api/generated/zod.gen.ts b/client/src/api/generated/zod.gen.ts index 6517e94e..d24816a6 100644 --- a/client/src/api/generated/zod.gen.ts +++ b/client/src/api/generated/zod.gen.ts @@ -259,7 +259,9 @@ export const zUserPublic = z.object({ export const zValidationError = z.object({ loc: z.array(z.union([z.string(), z.int()])), msg: z.string(), - type: z.string() + type: z.string(), + input: z.unknown().optional(), + ctx: z.record(z.string(), z.unknown()).optional() }); /** diff --git a/server/uv.lock b/server/uv.lock index d1f5a612..bad40ccd 100644 --- a/server/uv.lock +++ b/server/uv.lock @@ -22,16 +22,16 @@ wheels = [ [[package]] name = "alembic" -version = "1.17.2" +version = "1.18.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mako" }, { name = "sqlalchemy" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/a6/74c8cadc2882977d80ad756a13857857dbcf9bd405bc80b662eb10651282/alembic-1.17.2.tar.gz", hash = "sha256:bbe9751705c5e0f14877f02d46c53d10885e377e3d90eda810a016f9baa19e8e", size = 1988064, upload-time = "2025-11-14T20:35:04.057Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/88/6237e97e3385b57b5f1528647addea5cc03d4d65d5979ab24327d41fb00d/alembic-1.17.2-py3-none-any.whl", hash = "sha256:f483dd1fe93f6c5d49217055e4d15b905b425b6af906746abb35b69c1996c4e6", size = 248554, upload-time = "2025-11-14T20:35:05.699Z" }, + { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, ] [package.optional-dependencies] @@ -57,12 +57,12 @@ wheels = [ [[package]] name = "anyio" -version = "4.12.0" +version = "4.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [{ name = "idna" }] -sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, ] [[package]] @@ -142,11 +142,11 @@ pydantic = [{ name = "pydantic" }] [[package]] name = "certifi" -version = "2025.11.12" +version = "2026.2.25" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] [[package]] @@ -184,27 +184,43 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +version = "3.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, + { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, + { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, + { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, + { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, + { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, + { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, + { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, + { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, + { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, + { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, + { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, + { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, + { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, ] [[package]] @@ -228,41 +244,41 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, - { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, - { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, - { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, - { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, - { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, - { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, - { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, - { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, - { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, - { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, - { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, - { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, - { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, - { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, - { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, - { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, - { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, - { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, - { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, - { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, - { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, - { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, - { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, - { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, - { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, - { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, - { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, ] [[package]] @@ -323,17 +339,18 @@ wheels = [ [[package]] name = "fastapi" -version = "0.124.4" +version = "0.135.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/21/ade3ff6745a82ea8ad88552b4139d27941549e4f19125879f848ac8f3c3d/fastapi-0.124.4.tar.gz", hash = "sha256:0e9422e8d6b797515f33f500309f6e1c98ee4e85563ba0f2debb282df6343763", size = 378460, upload-time = "2025-12-12T15:00:43.891Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/73/5903c4b13beae98618d64eb9870c3fac4f605523dd0312ca5c80dadbd5b9/fastapi-0.135.2.tar.gz", hash = "sha256:88a832095359755527b7f63bb4c6bc9edb8329a026189eed83d6c1afcf419d56", size = 395833, upload-time = "2026-03-23T14:12:41.697Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/57/aa70121b5008f44031be645a61a7c4abc24e0e888ad3fc8fda916f4d188e/fastapi-0.124.4-py3-none-any.whl", hash = "sha256:6d1e703698443ccb89e50abe4893f3c84d9d6689c0cf1ca4fad6d3c15cf69f15", size = 113281, upload-time = "2025-12-12T15:00:42.44Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ea/18f6d0457f9efb2fc6fa594857f92810cadb03024975726db6546b3d6fcf/fastapi-0.135.2-py3-none-any.whl", hash = "sha256:0af0447d541867e8db2a6a25c23a8c4bd80e2394ac5529bd87501bbb9e240ca5", size = 117407, upload-time = "2026-03-23T14:12:43.284Z" }, ] [package.optional-dependencies] @@ -344,6 +361,8 @@ standard = [ ] }, { name = "httpx" }, { name = "jinja2" }, + { name = "pydantic-extra-types" }, + { name = "pydantic-settings" }, { name = "python-multipart" }, { name = "uvicorn", extra = [ "standard", @@ -352,7 +371,7 @@ standard = [ [[package]] name = "fastapi-cli" -version = "0.0.16" +version = "0.0.24" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "rich-toolkit" }, @@ -361,9 +380,9 @@ dependencies = [ "standard", ] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/75/9407a6b452be4c988feacec9c9d2f58d8f315162a6c7258d5a649d933ebe/fastapi_cli-0.0.16.tar.gz", hash = "sha256:e8a2a1ecf7a4e062e3b2eec63ae34387d1e142d4849181d936b23c4bdfe29073", size = 19447, upload-time = "2025-11-10T19:01:07.856Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/58/74797ae9e4610cfa0c6b34c8309096d3b20bb29be3b8b5fbf1004d10fa5f/fastapi_cli-0.0.24.tar.gz", hash = "sha256:1afc9c9e21d7ebc8a3ca5e31790cd8d837742be7e4f8b9236e99cb3451f0de00", size = 19043, upload-time = "2026-02-24T10:45:10.476Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/43/678528c19318394320ee43757648d5e0a8070cf391b31f69d931e5c840d2/fastapi_cli-0.0.16-py3-none-any.whl", hash = "sha256:addcb6d130b5b9c91adbbf3f2947fe115991495fdb442fe3e51b5fc6327df9f4", size = 12312, upload-time = "2025-11-10T19:01:06.728Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4b/68f9fe268e535d79c76910519530026a4f994ce07189ac0dded45c6af825/fastapi_cli-0.0.24-py3-none-any.whl", hash = "sha256:4a1f78ed798f106b4fee85ca93b85d8fe33c0a3570f775964d37edb80b8f0edc", size = 12304, upload-time = "2026-02-24T10:45:09.552Z" }, ] [package.optional-dependencies] @@ -376,7 +395,7 @@ standard = [ [[package]] name = "fastapi-cloud-cli" -version = "0.6.0" +version = "0.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastar" }, @@ -392,9 +411,9 @@ dependencies = [ "standard", ] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/dd/e5890bb4ee63f9d8988660b755490e346cf5769aaa7f5f3ced9afb9f090a/fastapi_cloud_cli-0.6.0.tar.gz", hash = "sha256:2c333fff2e4b93b9efbec7896ce3ffa3e77ce4cf3d8cb14e35b4f823dfddac02", size = 30579, upload-time = "2025-12-04T15:04:07.008Z" } +sdist = { url = "https://files.pythonhosted.org/packages/63/e1/05c44e7bbc619e980fab0236cff9f5f323ac1aaa79434b4906febf98b1d3/fastapi_cloud_cli-0.15.0.tar.gz", hash = "sha256:d02515231f3f505f7669c20920343934570a88a08af9f9a6463ca2807f27ffe5", size = 45309, upload-time = "2026-03-11T22:31:32.455Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/2f/5ba9b5faa75067e30ff48e3c454263ebc2d2301d5509cfefe12cf9fc8156/fastapi_cloud_cli-0.6.0-py3-none-any.whl", hash = "sha256:b654890b5302c90d2f347b123a35186096328838a526316c470b6005cabd4983", size = 23215, upload-time = "2025-12-04T15:04:08.121Z" }, + { url = "https://files.pythonhosted.org/packages/40/cc/1ccca747f5609be27186ea8c9219449142f40e3eded2c6089bba6a6ecc82/fastapi_cloud_cli-0.15.0-py3-none-any.whl", hash = "sha256:9ffcf90bd713747efa65447620d29cfbb7b3f7de38d97467952ca6346e418d70", size = 32267, upload-time = "2026-03-11T22:31:33.499Z" }, ] [[package]] @@ -408,40 +427,42 @@ wheels = [ [[package]] name = "fastar" -version = "0.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/e7/f89d54fb04104114dd0552836dc2b47914f416cc0e200b409dd04a33de5e/fastar-0.8.0.tar.gz", hash = "sha256:f4d4d68dbf1c4c2808f0e730fac5843493fc849f70fe3ad3af60dfbaf68b9a12", size = 68524, upload-time = "2025-11-26T02:36:00.72Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/4a/9573b87a0ef07580ed111e7230259aec31bb33ca3667963ebee77022ec61/fastar-0.8.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:50b36ce654ba44b0e13fae607ae17ee6e1597b69f71df1bee64bb8328d881dfc", size = 706041, upload-time = "2025-11-26T02:34:40.638Z" }, - { url = "https://files.pythonhosted.org/packages/4a/19/f95444a1d4f375333af49300aa75ee93afa3335c0e40fda528e460ed859c/fastar-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63a892762683d7ab00df0227d5ea9677c62ff2cde9b875e666c0be569ed940f3", size = 628617, upload-time = "2025-11-26T02:34:24.893Z" }, - { url = "https://files.pythonhosted.org/packages/b3/c9/b51481b38b7e3f16ef2b9e233b1a3623386c939d745d6e41bbd389eaae30/fastar-0.8.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4ae6a145c1bff592644bde13f2115e0239f4b7babaf506d14e7d208483cf01a5", size = 869299, upload-time = "2025-11-26T02:33:54.274Z" }, - { url = "https://files.pythonhosted.org/packages/bf/02/3ba1267ee5ba7314e29c431cf82eaa68586f2c40cdfa08be3632b7d07619/fastar-0.8.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ae0ff7c0a1c7e1428404b81faee8aebef466bfd0be25bfe4dabf5d535c68741", size = 764667, upload-time = "2025-11-26T02:32:49.606Z" }, - { url = "https://files.pythonhosted.org/packages/1b/84/bf33530fd015b5d7c2cc69e0bce4a38d736754a6955487005aab1af6adcd/fastar-0.8.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dbfd87dbd217b45c898b2dbcd0169aae534b2c1c5cbe3119510881f6a5ac8ef5", size = 763993, upload-time = "2025-11-26T02:33:05.782Z" }, - { url = "https://files.pythonhosted.org/packages/da/e0/9564d24e7cea6321a8d921c6d2a457044a476ef197aa4708e179d3d97f0d/fastar-0.8.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5abd99fcba83ef28c8fe6ae2927edc79053db43a0457a962ed85c9bf150d37", size = 930153, upload-time = "2025-11-26T02:33:21.53Z" }, - { url = "https://files.pythonhosted.org/packages/35/b1/6f57fcd8d6e192cfebf97e58eb27751640ad93784c857b79039e84387b51/fastar-0.8.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91d4c685620c3a9d6b5ae091dbabab4f98b20049b7ecc7976e19cc9016c0d5d6", size = 821177, upload-time = "2025-11-26T02:33:35.839Z" }, - { url = "https://files.pythonhosted.org/packages/b3/78/9e004ea9f3aa7466f5ddb6f9518780e1d2f0ed3ca55f093632982598bace/fastar-0.8.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f77c2f2cad76e9dc7b6701297adb1eba87d0485944b416fc2ccf5516c01219a3", size = 820652, upload-time = "2025-11-26T02:34:09.776Z" }, - { url = "https://files.pythonhosted.org/packages/42/95/b604ed536544005c9f1aee7c4c74b00150db3d8d535cd8232dc20f947063/fastar-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e7f07c4a3dada7757a8fc430a5b4a29e6ef696d2212747213f57086ffd970316", size = 985961, upload-time = "2025-11-26T02:34:56.401Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7b/fa9d4d96a5d494bdb8699363bb9de8178c0c21a02e1d89cd6f913d127018/fastar-0.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:90c0c3fe55105c0aed8a83135dbdeb31e683455dbd326a1c48fa44c378b85616", size = 1039316, upload-time = "2025-11-26T02:35:13.807Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f9/8462789243bc3f33e8401378ec6d54de4e20cfa60c96a0e15e3e9d1389bb/fastar-0.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fb9ee51e5bffe0dab3d3126d3a4fac8d8f7235cedcb4b8e74936087ce1c157f3", size = 1045028, upload-time = "2025-11-26T02:35:31.079Z" }, - { url = "https://files.pythonhosted.org/packages/a5/71/9abb128777e616127194b509e98fcda3db797d76288c1a8c23dd22afc14f/fastar-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e380b1e8d30317f52406c43b11e98d11e1d68723bbd031e18049ea3497b59a6d", size = 994677, upload-time = "2025-11-26T02:35:49.391Z" }, - { url = "https://files.pythonhosted.org/packages/de/c1/b81b3f194853d7ad232a67a1d768f5f51a016f165cfb56cb31b31bbc6177/fastar-0.8.0-cp314-cp314-win32.whl", hash = "sha256:1c4ffc06e9c4a8ca498c07e094670d8d8c0d25b17ca6465b9774da44ea997ab1", size = 456687, upload-time = "2025-11-26T02:36:30.205Z" }, - { url = "https://files.pythonhosted.org/packages/cb/87/9e0cd4768a98181d56f0cdbab2363404cc15deb93f4aad3b99cd2761bbaa/fastar-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:5517a8ad4726267c57a3e0e2a44430b782e00b230bf51c55b5728e758bb3a692", size = 490578, upload-time = "2025-11-26T02:36:16.218Z" }, - { url = "https://files.pythonhosted.org/packages/aa/1e/580a76cf91847654f2ad6520e956e93218f778540975bc4190d363f709e2/fastar-0.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:58030551046ff4a8616931e52a36c83545ff05996db5beb6e0cd2b7e748aa309", size = 461473, upload-time = "2025-11-26T02:36:06.373Z" }, - { url = "https://files.pythonhosted.org/packages/58/4c/bdb5c6efe934f68708529c8c9d4055ebef5c4be370621966438f658b29bd/fastar-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:1e7d29b6bfecb29db126a08baf3c04a5ab667f6cea2b7067d3e623a67729c4a6", size = 705570, upload-time = "2025-11-26T02:34:42.01Z" }, - { url = "https://files.pythonhosted.org/packages/6d/78/f01ac7e71d5a37621bd13598a26e948a12b85ca8042f7ee1a0a8c9f59cda/fastar-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05eb7b96940f9526b485f1d0b02393839f0f61cac4b1f60024984f8b326d2640", size = 627761, upload-time = "2025-11-26T02:34:26.152Z" }, - { url = "https://files.pythonhosted.org/packages/06/45/6df0ecda86ea9d2e95053c1a655d153dee55fc121b6e13ea6d1e246a50b6/fastar-0.8.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:619352d8ac011794e2345c462189dc02ba634750d23cd9d86a9267dd71b1f278", size = 869414, upload-time = "2025-11-26T02:33:55.618Z" }, - { url = "https://files.pythonhosted.org/packages/b2/72/486421f5a8c0c377cc82e7a50c8a8ea899a6ec2aa72bde8f09fb667a2dc8/fastar-0.8.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74ebfecef3fe6d7a90355fac1402fd30636988332a1d33f3e80019a10782bb24", size = 763863, upload-time = "2025-11-26T02:32:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/d4/64/39f654dbb41a3867fb1f2c8081c014d8f1d32ea10585d84cacbef0b32995/fastar-0.8.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2975aca5a639e26a3ab0d23b4b0628d6dd6d521146c3c11486d782be621a35aa", size = 763065, upload-time = "2025-11-26T02:33:07.274Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bd/c011a34fb3534c4c3301f7c87c4ffd7e47f6113c904c092ddc8a59a303ea/fastar-0.8.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afc438eaed8ff0dcdd9308268be5cb38c1db7e94c3ccca7c498ca13a4a4535a3", size = 930530, upload-time = "2025-11-26T02:33:23.117Z" }, - { url = "https://files.pythonhosted.org/packages/55/9d/aa6e887a7033c571b1064429222bbe09adc9a3c1e04f3d1788ba5838ebd5/fastar-0.8.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ced0a5399cc0a84a858ef0a31ca2d0c24d3bbec4bcda506a9192d8119f3590a", size = 820572, upload-time = "2025-11-26T02:33:37.542Z" }, - { url = "https://files.pythonhosted.org/packages/ad/9c/7a3a2278a1052e1a5d98646de7c095a00cffd2492b3b84ce730e2f1cd93a/fastar-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec9b23da8c4c039da3fe2e358973c66976a0c8508aa06d6626b4403cb5666c19", size = 820649, upload-time = "2025-11-26T02:34:11.108Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/d38edc1f4438cd047e56137c26d94783ffade42e1b3bde620ccf17b771ef/fastar-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dfba078fcd53478032fd0ceed56960ec6b7ff0511cfc013a8a3a4307e3a7bac4", size = 985653, upload-time = "2025-11-26T02:34:57.884Z" }, - { url = "https://files.pythonhosted.org/packages/69/d9/2147d0c19757e165cd62d41cec3f7b38fad2ad68ab784978b5f81716c7ea/fastar-0.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ade56c94c14be356d295fecb47a3fcd473dd43a8803ead2e2b5b9e58feb6dcfa", size = 1038140, upload-time = "2025-11-26T02:35:15.778Z" }, - { url = "https://files.pythonhosted.org/packages/7f/1d/ec4c717ffb8a308871e9602ec3197d957e238dc0227127ac573ec9bca952/fastar-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e48d938f9366db5e59441728f70b7f6c1ccfab7eff84f96f9b7e689b07786c52", size = 1045195, upload-time = "2025-11-26T02:35:32.865Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9f/637334dc8c8f3bb391388b064ae13f0ad9402bc5a6c3e77b8887d0c31921/fastar-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:79c441dc1482ff51a54fb3f57ae6f7bb3d2cff88fa2cc5d196c519f8aab64a56", size = 994686, upload-time = "2025-11-26T02:35:51.392Z" }, - { url = "https://files.pythonhosted.org/packages/c9/e2/dfa19a4b260b8ab3581b7484dcb80c09b25324f4daa6b6ae1c7640d1607a/fastar-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:187f61dc739afe45ac8e47ed7fd1adc45d52eac110cf27d579155720507d6fbe", size = 455767, upload-time = "2025-11-26T02:36:34.758Z" }, - { url = "https://files.pythonhosted.org/packages/51/47/df65c72afc1297797b255f90c4778b5d6f1f0f80282a134d5ab610310ed9/fastar-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:40e9d763cf8bf85ce2fa256e010aa795c0fe3d3bd1326d5c3084e6ce7857127e", size = 489971, upload-time = "2025-11-26T02:36:22.081Z" }, - { url = "https://files.pythonhosted.org/packages/85/11/0aa8455af26f0ae89e42be67f3a874255ee5d7f0f026fc86e8d56f76b428/fastar-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e59673307b6a08210987059a2bdea2614fe26e3335d0e5d1a3d95f49a05b1418", size = 460467, upload-time = "2025-11-26T02:36:07.978Z" }, +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/00/dab9ca274cf1fde19223fea7104631bea254751026e75bf99f2b6d0d1568/fastar-0.9.0.tar.gz", hash = "sha256:d49114d5f0b76c5cc242875d90fa4706de45e0456ddedf416608ecd0787fb410", size = 70124, upload-time = "2026-03-20T14:26:34.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/00/99700dd33273c118d7d9ab7ad5db6650b430448d4cfae62aec6ef6ca4cb7/fastar-0.9.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ccb2289f24ee6555330eb77149486d3a2ec8926450a96157dd20c636a0eec085", size = 707059, upload-time = "2026-03-20T14:25:35.086Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a4/4808dcfa8dddb9d7f50d830a39a9084d9d148ed06fcac8b040620848bc24/fastar-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2bfee749a46666785151b33980aef8f916e6e0341c3d241bde4d3de6be23f00c", size = 627135, upload-time = "2026-03-20T14:25:23.134Z" }, + { url = "https://files.pythonhosted.org/packages/da/cb/9c92e97d760d769846cae6ce53332a5f2a9246eb07b369ac2a4ebf10480c/fastar-0.9.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f6096ec3f216a21fa9ac430ce509447f56c5bd979170c4c0c3b4f3cb2051c1a8", size = 864974, upload-time = "2026-03-20T14:24:58.624Z" }, + { url = "https://files.pythonhosted.org/packages/84/38/9dadebd0b7408b4f415827db35169bbd0741e726e38e3afd3e491b589c61/fastar-0.9.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7a806e54d429f7f57e35dc709e801da8c0ba9095deb7331d6574c05ae4537ea", size = 760262, upload-time = "2026-03-20T14:23:53.275Z" }, + { url = "https://files.pythonhosted.org/packages/d6/7d/7afc5721429515aa0873b268513f656f905d27ff1ca54d875af6be9e9bc6/fastar-0.9.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9a06abf8c7f74643a75003334683eb6e94fabef05f60449b7841eeb093a47b0", size = 757575, upload-time = "2026-03-20T14:24:06.143Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5d/7498842c62bd6057553aa598cd175a0db41fdfeda7bdfde48dab63ffb285/fastar-0.9.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e9b5c155946f20ce3f999fb1362ed102876156ad6539e1b73a921f14efb758c", size = 924827, upload-time = "2026-03-20T14:24:19.364Z" }, + { url = "https://files.pythonhosted.org/packages/69/ab/13322e98fe1a00ed6efbfa5bf06fcfff8a6979804ef7fcef884b5e0c6f85/fastar-0.9.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbdedac6a84ef9ebc1cee6d777599ad51c9e98ceb8ebb386159483dcd60d0e16", size = 816536, upload-time = "2026-03-20T14:24:44.844Z" }, + { url = "https://files.pythonhosted.org/packages/fe/fd/0aa5b9994c8dba75b73a9527be4178423cb926db9f7eca562559e27ccdfd/fastar-0.9.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51df60a2f7af09f75b2a4438b25cb903d8774e24c492acf2bca8b0863026f34c", size = 818686, upload-time = "2026-03-20T14:25:10.799Z" }, + { url = "https://files.pythonhosted.org/packages/46/d6/e000cd49ef85c11a8350e461e6c48a4345ace94fb52242ac8c1d5dad1dfc/fastar-0.9.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:15016d0da7dbc664f09145fc7db549ba8fe32628c6e44e20926655b82de10658", size = 885043, upload-time = "2026-03-20T14:24:32.231Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/ee734fe273475b9b25554370d92a21fc809376cf79aa072de29d23c17518/fastar-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c66a8e1f7dae6357be8c1f83ce6330febbc08e49fc40a5a2e91061e7867bbcbf", size = 967965, upload-time = "2026-03-20T14:25:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/165b3a75f1ee8045af9478c8aae5b5e20913cca2d4a5adb1be445e8d015a/fastar-0.9.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1c6829be3f55d2978cb62921ef4d7c3dd58fe68ee994f81d49bd0a3c5240c977", size = 1034507, upload-time = "2026-03-20T14:26:01.518Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4e/4097b5015da02484468c16543db2f8dec2fe827d321a798acbd9068e0f13/fastar-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:68db849e01d49543f31d56ef2fe15527afe2b9e0fb21794edc4d772553d83407", size = 1073388, upload-time = "2026-03-20T14:26:14.448Z" }, + { url = "https://files.pythonhosted.org/packages/07/d7/3b86af4e63a551398763a1bbbbac91e1c0754ece7ac7157218b33a065f4c/fastar-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5569510407c0ded580cfeec99e46ebe85ce27e199e020c5c1ea6f570e302c946", size = 1025190, upload-time = "2026-03-20T14:26:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/39/07/8c50a60f03e095053306fcf57d9d99343bce0e99d5b758bf96de31aec849/fastar-0.9.0-cp314-cp314-win32.whl", hash = "sha256:3f7be0a34ffbead52ab5f4a1e445e488bf39736acb006298d3b3c5b4f2c5915e", size = 452301, upload-time = "2026-03-20T14:26:59.234Z" }, + { url = "https://files.pythonhosted.org/packages/ee/69/aa6d67b09485ba031408296d6ff844c7d83cdcb9f8fcc240422c6f83be87/fastar-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:cf7f68b98ed34ce628994c9bbd4f56cf6b4b175b3f7b8cbe35c884c8efec0a5b", size = 484948, upload-time = "2026-03-20T14:26:48.45Z" }, + { url = "https://files.pythonhosted.org/packages/20/6d/dba29d87ca929f95a5a7025c7d30720ad8478beed29fff482f29e1e8b045/fastar-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:155dae97aca4b245eabb25e23fd16bfd42a0447f9db7f7789ab1299b02d94487", size = 461170, upload-time = "2026-03-20T14:26:39.191Z" }, + { url = "https://files.pythonhosted.org/packages/96/8f/c3ea0adac50a8037987ee7f15ff94767ebb604faf6008cbd2b8efa46c372/fastar-0.9.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a63df018232623e136178953031057c7ac0dbf0acc6f0e8c1dc7dbc19e64c22f", size = 705857, upload-time = "2026-03-20T14:25:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b3/e0e1aad1778065559680a73cdf982ed07b04300c2e5bf778dec8668eda6f/fastar-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6fb44f8675ef87087cb08f9bf4dfa15e818571a5f567ff692f3ea007cff867b5", size = 626210, upload-time = "2026-03-20T14:25:24.361Z" }, + { url = "https://files.pythonhosted.org/packages/94/f3/3c117335cbea26b3bc05382c27e6028278ed048d610b8de427c68f2fec84/fastar-0.9.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:81092daa991d0f095424e0e28ed589e03c81a21eeddc9b981184ddda5869bf9d", size = 864879, upload-time = "2026-03-20T14:25:00.131Z" }, + { url = "https://files.pythonhosted.org/packages/26/5d/e8d00ec3b2692d14ea111ddae25bf10e0cb60d5d79915c3d8ea393a87d5c/fastar-0.9.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e8793e2618d0d6d5a7762d6007371f57f02544364864e40e6b9d304b0f151b2", size = 759117, upload-time = "2026-03-20T14:23:54.826Z" }, + { url = "https://files.pythonhosted.org/packages/1a/61/6e080fdbc28c72dded8b6ff396035d6dc292f9b1c67b8797ac2372ca5733/fastar-0.9.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83f7ef7056791fc95b6afa987238368c9a73ad0edcedc6bc80076f9fbd3a2a78", size = 756527, upload-time = "2026-03-20T14:24:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/e8/97/2cf1a07884d171c028bd4ae5ecf7ded6f31581f79ab26711dcdad0a3d5ab/fastar-0.9.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3a456230fcc0e560823f5d04ae8e4c867300d8ee710b14ddcdd1b316ac3dd8d", size = 921763, upload-time = "2026-03-20T14:24:20.787Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e3/c1d698a45f9f5dc892ed7d64badc9c38f1e5c1667048191969c438d2b428/fastar-0.9.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a60b117ebadc46c10c87852d2158a4d6489adbfbbec37be036b4cfbeca07b449", size = 815493, upload-time = "2026-03-20T14:24:46.482Z" }, + { url = "https://files.pythonhosted.org/packages/25/38/e124a404043fba75a8cb2f755ca49e4f01e18400bb6607a5f76526e07164/fastar-0.9.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a6199b4ca0c092a7ae47f5f387492d46a0a2d82cb3b7aa0bf50d7f7d5d8d57f", size = 819166, upload-time = "2026-03-20T14:25:12.027Z" }, + { url = "https://files.pythonhosted.org/packages/85/4a/5b1ea5c8d0dbdfcec2fd1e6a243d6bb5a1c7cd55e132cc532eb8b1cbd6d9/fastar-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:34efe114caf10b4d5ea404069ff1f6cc0e55a708c7091059b0fc087f65c0a331", size = 883618, upload-time = "2026-03-20T14:24:33.552Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/ae46e5722a67a3c2e0ff83d539b0907d6e5092f6395840c0eb6ede81c5d6/fastar-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4d44c1f8d9c5a3e4e58e6ffb77f4ca023ba9d9ddd88e7c613b3419a8feaa3db7", size = 966294, upload-time = "2026-03-20T14:25:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/98/58/b161cf8711f4a50a3e57b6f89bc703c1aed282cad50434b3bc8524738b20/fastar-0.9.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d2af970a1f773965b05f1765017a417380ad080ea49590516eb25b23c039158a", size = 1033177, upload-time = "2026-03-20T14:26:02.868Z" }, + { url = "https://files.pythonhosted.org/packages/e2/76/faac7292bce9b30106a6b6a9f5ddb658fdb03abe2644688b82023c8f76b9/fastar-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:1675346d7cbdde0d21869c3b597be19b5e31a36442bdf3a48d83a49765b269dc", size = 1073620, upload-time = "2026-03-20T14:26:16.121Z" }, + { url = "https://files.pythonhosted.org/packages/b8/be/dd55ffcc302d6f0ff4aba1616a0da3edc8fcefb757869cad81de74604a35/fastar-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dc440daa28591aeb4d387c171e824f179ad2ab256ce7a315472395b8d5f80392", size = 1025147, upload-time = "2026-03-20T14:26:28.767Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c7/080bbb2b3c4e739fe6486fd765a09905f6c16c1068b2fcf2bb51a5e83937/fastar-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:32787880600a988d11547628034993ef948499ae4514a30509817242c4eb98b1", size = 452317, upload-time = "2026-03-20T14:27:03.243Z" }, + { url = "https://files.pythonhosted.org/packages/42/39/00553739a7e9e35f78a0c5911d181acf6b6e132337adc9bbc3575f5f6f04/fastar-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92fa18ec4958f33473259980685d29248ac44c96eed34026ad7550f93dd9ee23", size = 483994, upload-time = "2026-03-20T14:26:52.76Z" }, + { url = "https://files.pythonhosted.org/packages/4f/36/a7af08d233624515d9a0f5d41b7a01a51fd825b8c795e41800215a3200e7/fastar-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:34f646ac4f5bed3661a106ca56c1744e7146a02aacf517d47b24fd3f25dc1ff6", size = 460604, upload-time = "2026-03-20T14:26:40.771Z" }, ] [[package]] @@ -667,11 +688,11 @@ wheels = [ [[package]] name = "packaging" -version = "25.0" +version = "26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] @@ -697,11 +718,11 @@ argon2 = [{ name = "argon2-cffi" }] [[package]] name = "pycparser" -version = "2.23" +version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] @@ -759,6 +780,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, ] +[[package]] +name = "pydantic-extra-types" +version = "2.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [{ name = "pydantic" }, { name = "typing-extensions" }] +sdist = { url = "https://files.pythonhosted.org/packages/66/71/dba38ee2651f84f7842206adbd2233d8bbdb59fb85e9fa14232486a8c471/pydantic_extra_types-2.11.1.tar.gz", hash = "sha256:46792d2307383859e923d8fcefa82108b1a141f8a9c0198982b3832ab5ef1049", size = 172002, upload-time = "2026-03-16T08:08:03.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/c1/3226e6d7f5a4f736f38ac11a6fbb262d701889802595cdb0f53a885ac2e0/pydantic_extra_types-2.11.1-py3-none-any.whl", hash = "sha256:1722ea2bddae5628ace25f2aa685b69978ef533123e5638cfbddb999e0100ec1", size = 79526, upload-time = "2026-03-16T08:08:02.533Z" }, +] + [[package]] name = "pydantic-settings" version = "2.13.1" @@ -902,7 +933,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.5" +version = "2.33.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -910,9 +941,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" }, ] [[package]] @@ -927,26 +958,26 @@ wheels = [ [[package]] name = "rich" -version = "14.2.0" +version = "14.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [{ name = "markdown-it-py" }, { name = "pygments" }] -sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, ] [[package]] name = "rich-toolkit" -version = "0.17.0" +version = "0.19.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/d0/8f8de36e1abf8339b497ce700dd7251ca465ffca4a1976969b0eaeb596fb/rich_toolkit-0.17.0.tar.gz", hash = "sha256:17ca7a32e613001aa0945ddea27a246f6de01dfc4c12403254c057a8ee542977", size = 187955, upload-time = "2025-11-27T11:10:24.863Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/ba/dae9e3096651042754da419a4042bc1c75e07d615f9b15066d738838e4df/rich_toolkit-0.19.7.tar.gz", hash = "sha256:133c0915872da91d4c25d85342d5ec1dfacc69b63448af1a08a0d4b4f23ef46e", size = 195877, upload-time = "2026-02-24T16:06:20.555Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/42/ef2ed40699567661d03b0b511ac46cf6cee736de8f3666819c12d6d20696/rich_toolkit-0.17.0-py3-none-any.whl", hash = "sha256:06fb47a5c5259d6b480287cd38aff5f551b6e1a307f90ed592453dd360e4e71e", size = 31412, upload-time = "2025-11-27T11:10:23.847Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3c/c923619f6d2f5fafcc96fec0aaf9550a46cd5b6481f06e0c6b66a2a4fed0/rich_toolkit-0.19.7-py3-none-any.whl", hash = "sha256:0288e9203728c47c5a4eb60fd2f0692d9df7455a65901ab6f898437a2ba5989d", size = 32963, upload-time = "2026-02-24T16:06:22.066Z" }, ] [[package]] @@ -1014,12 +1045,12 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.47.0" +version = "2.56.0" source = { registry = "https://pypi.org/simple" } dependencies = [{ name = "certifi" }, { name = "urllib3" }] -sdist = { url = "https://files.pythonhosted.org/packages/4a/2a/d225cbf87b6c8ecce5664db7bcecb82c317e448e3b24a2dcdaacb18ca9a7/sentry_sdk-2.47.0.tar.gz", hash = "sha256:8218891d5e41b4ea8d61d2aed62ed10c80e39d9f2959d6f939efbf056857e050", size = 381895, upload-time = "2025-12-03T14:06:36.846Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/df/5008954f5466085966468612a7d1638487596ee6d2fd7fb51783a85351bf/sentry_sdk-2.56.0.tar.gz", hash = "sha256:fdab72030b69625665b2eeb9738bdde748ad254e8073085a0ce95382678e8168", size = 426820, upload-time = "2026-03-24T09:56:36.575Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/ac/d6286ea0d49e7b58847faf67b00e56bb4ba3d525281e2ac306e1f1f353da/sentry_sdk-2.47.0-py2.py3-none-any.whl", hash = "sha256:d72f8c61025b7d1d9e52510d03a6247b280094a327dd900d987717a4fce93412", size = 411088, upload-time = "2025-12-03T14:06:35.374Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1a/b3a3e9f6520493fed7997af4d2de7965d71549c62f994a8fd15f2ecd519e/sentry_sdk-2.56.0-py2.py3-none-any.whl", hash = "sha256:5afafb744ceb91d22f4cc650c6bd048ac6af5f7412dcc6c59305a2e36f4dbc02", size = 451568, upload-time = "2026-03-24T09:56:34.807Z" }, ] [[package]] @@ -1138,12 +1169,12 @@ wheels = [ [[package]] name = "starlette" -version = "0.50.0" +version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [{ name = "anyio" }] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, ] [[package]] @@ -1191,17 +1222,17 @@ wheels = [ [[package]] name = "typer" -version = "0.20.0" +version = "0.24.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "annotated-doc" }, { name = "click" }, { name = "rich" }, { name = "shellingham" }, - { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8f/28/7c85c8032b91dbe79725b6f17d2fffc595dff06a35c7a30a37bef73a1ab4/typer-0.20.0.tar.gz", hash = "sha256:1aaf6494031793e4876fb0bacfa6a912b551cf43c1e63c800df8b1a866720c37", size = 106492, upload-time = "2025-10-20T17:03:49.445Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/64/7713ffe4b5983314e9d436a90d5bd4f63b6054e2aca783a3cfc44cb95bbf/typer-0.20.0-py3-none-any.whl", hash = "sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a", size = 47028, upload-time = "2025-10-20T17:03:47.617Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, ] [[package]] @@ -1244,11 +1275,11 @@ wheels = [ [[package]] name = "tzdata" -version = "2025.2" +version = "2025.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] [[package]] @@ -1262,12 +1293,12 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.38.0" +version = "0.42.0" source = { registry = "https://pypi.org/simple" } dependencies = [{ name = "click" }, { name = "h11" }] -sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/ad/4a96c425be6fb67e0621e62d86c402b4a17ab2be7f7c055d9bd2f638b9e2/uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775", size = 85393, upload-time = "2026-03-16T06:19:50.077Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, + { url = "https://files.pythonhosted.org/packages/0a/89/f8827ccff89c1586027a105e5630ff6139a64da2515e24dafe860bd9ae4d/uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359", size = 68830, upload-time = "2026-03-16T06:19:48.325Z" }, ] [package.optional-dependencies] @@ -1335,42 +1366,58 @@ wheels = [ [[package]] name = "websockets" -version = "15.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, ] [[package]] name = "wrapt" -version = "2.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/81/d08d83c102709258e7730d3cd25befd114c60e43ef3891d7e6877971c514/wrapt-2.0.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5e53b428f65ece6d9dad23cb87e64506392b720a0b45076c05354d27a13351a1", size = 78290, upload-time = "2025-11-07T00:44:34.691Z" }, - { url = "https://files.pythonhosted.org/packages/f6/14/393afba2abb65677f313aa680ff0981e829626fed39b6a7e3ec807487790/wrapt-2.0.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ad3ee9d0f254851c71780966eb417ef8e72117155cff04821ab9b60549694a55", size = 61255, upload-time = "2025-11-07T00:44:35.762Z" }, - { url = "https://files.pythonhosted.org/packages/c4/10/a4a1f2fba205a9462e36e708ba37e5ac95f4987a0f1f8fd23f0bf1fc3b0f/wrapt-2.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7b822c61ed04ee6ad64bc90d13368ad6eb094db54883b5dde2182f67a7f22c0", size = 61797, upload-time = "2025-11-07T00:44:37.22Z" }, - { url = "https://files.pythonhosted.org/packages/12/db/99ba5c37cf1c4fad35349174f1e38bd8d992340afc1ff27f526729b98986/wrapt-2.0.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7164a55f5e83a9a0b031d3ffab4d4e36bbec42e7025db560f225489fa929e509", size = 120470, upload-time = "2025-11-07T00:44:39.425Z" }, - { url = "https://files.pythonhosted.org/packages/30/3f/a1c8d2411eb826d695fc3395a431757331582907a0ec59afce8fe8712473/wrapt-2.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e60690ba71a57424c8d9ff28f8d006b7ad7772c22a4af432188572cd7fa004a1", size = 122851, upload-time = "2025-11-07T00:44:40.582Z" }, - { url = "https://files.pythonhosted.org/packages/b3/8d/72c74a63f201768d6a04a8845c7976f86be6f5ff4d74996c272cefc8dafc/wrapt-2.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3cd1a4bd9a7a619922a8557e1318232e7269b5fb69d4ba97b04d20450a6bf970", size = 117433, upload-time = "2025-11-07T00:44:38.313Z" }, - { url = "https://files.pythonhosted.org/packages/c7/5a/df37cf4042cb13b08256f8e27023e2f9b3d471d553376616591bb99bcb31/wrapt-2.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4c2e3d777e38e913b8ce3a6257af72fb608f86a1df471cb1d4339755d0a807c", size = 121280, upload-time = "2025-11-07T00:44:41.69Z" }, - { url = "https://files.pythonhosted.org/packages/54/34/40d6bc89349f9931e1186ceb3e5fbd61d307fef814f09fbbac98ada6a0c8/wrapt-2.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3d366aa598d69416b5afedf1faa539fac40c1d80a42f6b236c88c73a3c8f2d41", size = 116343, upload-time = "2025-11-07T00:44:43.013Z" }, - { url = "https://files.pythonhosted.org/packages/70/66/81c3461adece09d20781dee17c2366fdf0cb8754738b521d221ca056d596/wrapt-2.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c235095d6d090aa903f1db61f892fffb779c1eaeb2a50e566b52001f7a0f66ed", size = 119650, upload-time = "2025-11-07T00:44:44.523Z" }, - { url = "https://files.pythonhosted.org/packages/46/3a/d0146db8be8761a9e388cc9cc1c312b36d583950ec91696f19bbbb44af5a/wrapt-2.0.1-cp314-cp314-win32.whl", hash = "sha256:bfb5539005259f8127ea9c885bdc231978c06b7a980e63a8a61c8c4c979719d0", size = 58701, upload-time = "2025-11-07T00:44:48.277Z" }, - { url = "https://files.pythonhosted.org/packages/1a/38/5359da9af7d64554be63e9046164bd4d8ff289a2dd365677d25ba3342c08/wrapt-2.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:4ae879acc449caa9ed43fc36ba08392b9412ee67941748d31d94e3cedb36628c", size = 60947, upload-time = "2025-11-07T00:44:46.086Z" }, - { url = "https://files.pythonhosted.org/packages/aa/3f/96db0619276a833842bf36343685fa04f987dd6e3037f314531a1e00492b/wrapt-2.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:8639b843c9efd84675f1e100ed9e99538ebea7297b62c4b45a7042edb84db03e", size = 59359, upload-time = "2025-11-07T00:44:47.164Z" }, - { url = "https://files.pythonhosted.org/packages/71/49/5f5d1e867bf2064bf3933bc6cf36ade23505f3902390e175e392173d36a2/wrapt-2.0.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:9219a1d946a9b32bb23ccae66bdb61e35c62773ce7ca6509ceea70f344656b7b", size = 82031, upload-time = "2025-11-07T00:44:49.4Z" }, - { url = "https://files.pythonhosted.org/packages/2b/89/0009a218d88db66ceb83921e5685e820e2c61b59bbbb1324ba65342668bc/wrapt-2.0.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fa4184e74197af3adad3c889a1af95b53bb0466bced92ea99a0c014e48323eec", size = 62952, upload-time = "2025-11-07T00:44:50.74Z" }, - { url = "https://files.pythonhosted.org/packages/ae/18/9b968e920dd05d6e44bcc918a046d02afea0fb31b2f1c80ee4020f377cbe/wrapt-2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c5ef2f2b8a53b7caee2f797ef166a390fef73979b15778a4a153e4b5fedce8fa", size = 63688, upload-time = "2025-11-07T00:44:52.248Z" }, - { url = "https://files.pythonhosted.org/packages/a6/7d/78bdcb75826725885d9ea26c49a03071b10c4c92da93edda612910f150e4/wrapt-2.0.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e042d653a4745be832d5aa190ff80ee4f02c34b21f4b785745eceacd0907b815", size = 152706, upload-time = "2025-11-07T00:44:54.613Z" }, - { url = "https://files.pythonhosted.org/packages/dd/77/cac1d46f47d32084a703df0d2d29d47e7eb2a7d19fa5cbca0e529ef57659/wrapt-2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2afa23318136709c4b23d87d543b425c399887b4057936cd20386d5b1422b6fa", size = 158866, upload-time = "2025-11-07T00:44:55.79Z" }, - { url = "https://files.pythonhosted.org/packages/8a/11/b521406daa2421508903bf8d5e8b929216ec2af04839db31c0a2c525eee0/wrapt-2.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c72328f668cf4c503ffcf9434c2b71fdd624345ced7941bc6693e61bbe36bef", size = 146148, upload-time = "2025-11-07T00:44:53.388Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c0/340b272bed297baa7c9ce0c98ef7017d9c035a17a6a71dce3184b8382da2/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3793ac154afb0e5b45d1233cb94d354ef7a983708cc3bb12563853b1d8d53747", size = 155737, upload-time = "2025-11-07T00:44:56.971Z" }, - { url = "https://files.pythonhosted.org/packages/f3/93/bfcb1fb2bdf186e9c2883a4d1ab45ab099c79cbf8f4e70ea453811fa3ea7/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fec0d993ecba3991645b4857837277469c8cc4c554a7e24d064d1ca291cfb81f", size = 144451, upload-time = "2025-11-07T00:44:58.515Z" }, - { url = "https://files.pythonhosted.org/packages/d2/6b/dca504fb18d971139d232652656180e3bd57120e1193d9a5899c3c0b7cdd/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:949520bccc1fa227274da7d03bf238be15389cd94e32e4297b92337df9b7a349", size = 150353, upload-time = "2025-11-07T00:44:59.753Z" }, - { url = "https://files.pythonhosted.org/packages/1d/f6/a1de4bd3653afdf91d250ca5c721ee51195df2b61a4603d4b373aa804d1d/wrapt-2.0.1-cp314-cp314t-win32.whl", hash = "sha256:be9e84e91d6497ba62594158d3d31ec0486c60055c49179edc51ee43d095f79c", size = 60609, upload-time = "2025-11-07T00:45:03.315Z" }, - { url = "https://files.pythonhosted.org/packages/01/3a/07cd60a9d26fe73efead61c7830af975dfdba8537632d410462672e4432b/wrapt-2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:61c4956171c7434634401db448371277d07032a81cc21c599c22953374781395", size = 64038, upload-time = "2025-11-07T00:45:00.948Z" }, - { url = "https://files.pythonhosted.org/packages/41/99/8a06b8e17dddbf321325ae4eb12465804120f699cd1b8a355718300c62da/wrapt-2.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:35cdbd478607036fee40273be8ed54a451f5f23121bd9d4be515158f9498f7ad", size = 60634, upload-time = "2025-11-07T00:45:02.087Z" }, - { url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" }, +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/25/e7ea0b417db02bb796182a5316398a75792cd9a22528783d868755e1f669/wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9", size = 61418, upload-time = "2026-03-06T02:53:55.706Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0f/fa539e2f6a770249907757eaeb9a5ff4deb41c026f8466c1c6d799088a9b/wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9", size = 61914, upload-time = "2026-03-06T02:52:53.37Z" }, + { url = "https://files.pythonhosted.org/packages/53/37/02af1867f5b1441aaeda9c82deed061b7cd1372572ddcd717f6df90b5e93/wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e", size = 120417, upload-time = "2026-03-06T02:54:30.74Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b7/0138a6238c8ba7476c77cf786a807f871672b37f37a422970342308276e7/wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c", size = 122797, upload-time = "2026-03-06T02:54:51.539Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ad/819ae558036d6a15b7ed290d5b14e209ca795dd4da9c58e50c067d5927b0/wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a", size = 117350, upload-time = "2026-03-06T02:54:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/8b/2d/afc18dc57a4600a6e594f77a9ae09db54f55ba455440a54886694a84c71b/wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90", size = 121223, upload-time = "2026-03-06T02:54:35.221Z" }, + { url = "https://files.pythonhosted.org/packages/b9/5b/5ec189b22205697bc56eb3b62aed87a1e0423e9c8285d0781c7a83170d15/wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586", size = 116287, upload-time = "2026-03-06T02:54:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2d/f84939a7c9b5e6cdd8a8d0f6a26cabf36a0f7e468b967720e8b0cd2bdf69/wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19", size = 119593, upload-time = "2026-03-06T02:54:16.697Z" }, + { url = "https://files.pythonhosted.org/packages/0b/fe/ccd22a1263159c4ac811ab9374c061bcb4a702773f6e06e38de5f81a1bdc/wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508", size = 58631, upload-time = "2026-03-06T02:53:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/65/0a/6bd83be7bff2e7efaac7b4ac9748da9d75a34634bbbbc8ad077d527146df/wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04", size = 60875, upload-time = "2026-03-06T02:53:50.252Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c0/0b3056397fe02ff80e5a5d72d627c11eb885d1ca78e71b1a5c1e8c7d45de/wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575", size = 59164, upload-time = "2026-03-06T02:53:59.128Z" }, + { url = "https://files.pythonhosted.org/packages/71/ed/5d89c798741993b2371396eb9d4634f009ff1ad8a6c78d366fe2883ea7a6/wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb", size = 63163, upload-time = "2026-03-06T02:52:54.873Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8c/05d277d182bf36b0a13d6bd393ed1dec3468a25b59d01fba2dd70fe4d6ae/wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22", size = 63723, upload-time = "2026-03-06T02:52:56.374Z" }, + { url = "https://files.pythonhosted.org/packages/f4/27/6c51ec1eff4413c57e72d6106bb8dec6f0c7cdba6503d78f0fa98767bcc9/wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596", size = 152652, upload-time = "2026-03-06T02:53:23.79Z" }, + { url = "https://files.pythonhosted.org/packages/db/4c/d7dd662d6963fc7335bfe29d512b02b71cdfa23eeca7ab3ac74a67505deb/wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044", size = 158807, upload-time = "2026-03-06T02:53:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4d/1e5eea1a78d539d346765727422976676615814029522c76b87a95f6bcdd/wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b", size = 146061, upload-time = "2026-03-06T02:52:57.574Z" }, + { url = "https://files.pythonhosted.org/packages/89/bc/62cabea7695cd12a288023251eeefdcb8465056ddaab6227cb78a2de005b/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf", size = 155667, upload-time = "2026-03-06T02:53:39.422Z" }, + { url = "https://files.pythonhosted.org/packages/e9/99/6f2888cd68588f24df3a76572c69c2de28287acb9e1972bf0c83ce97dbc1/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2", size = 144392, upload-time = "2026-03-06T02:54:22.41Z" }, + { url = "https://files.pythonhosted.org/packages/40/51/1dfc783a6c57971614c48e361a82ca3b6da9055879952587bc99fe1a7171/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3", size = 150296, upload-time = "2026-03-06T02:54:07.848Z" }, + { url = "https://files.pythonhosted.org/packages/6c/38/cbb8b933a0201076c1f64fc42883b0023002bdc14a4964219154e6ff3350/wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7", size = 60539, upload-time = "2026-03-06T02:54:00.594Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/e5176e4b241c9f528402cebb238a36785a628179d7d8b71091154b3e4c9e/wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5", size = 63969, upload-time = "2026-03-06T02:54:39Z" }, + { url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, ] From b2bb04d0941828dc2b0939cf68a6a8c3aa0007c2 Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Wed, 25 Mar 2026 19:11:11 -0500 Subject: [PATCH 23/27] server - refactor admin endpoints --- client/src/api/generated/index.ts | 2 +- client/src/api/generated/sdk.gen.ts | 38 ++++----- client/src/api/generated/types.gen.ts | 66 +++++++-------- client/src/api/generated/zod.gen.ts | 26 +++--- client/src/components/AccessRequestsTable.tsx | 19 ++--- client/src/pages/Admin.tsx | 8 +- .../endpoints/{admin.py => access_request.py} | 25 ++---- server/app/api/endpoints/user.py | 19 ++++- server/app/api/router.py | 4 +- server/app/services/access_request.py | 72 ++++++++++++++++- server/app/services/admin.py | 81 ------------------- server/app/services/user.py | 12 +++ .../api/{admin => access_request}/__init__.py | 0 .../test_get_access_requests.py | 2 +- .../test_update_access_request_status.py | 2 +- .../api/{admin => user}/test_get_users.py | 2 +- .../{admin => access_request}/__init__.py | 0 .../test_get_access_request_by_id.py | 8 +- .../test_get_access_requests.py | 2 +- .../test_get_access_requests_with_reviewer.py | 16 ++-- .../test_update_access_request_status.py | 2 +- .../{admin => user}/test_get_users.py | 2 +- 22 files changed, 208 insertions(+), 200 deletions(-) rename server/app/api/endpoints/{admin.py => access_request.py} (80%) delete mode 100644 server/app/services/admin.py rename server/app/tests/api/{admin => access_request}/__init__.py (100%) rename server/app/tests/api/{admin => access_request}/test_get_access_requests.py (97%) rename server/app/tests/api/{admin => access_request}/test_update_access_request_status.py (98%) rename server/app/tests/api/{admin => user}/test_get_users.py (97%) rename server/app/tests/services/{admin => access_request}/__init__.py (100%) rename server/app/tests/services/{admin => access_request}/test_get_access_requests.py (96%) rename server/app/tests/services/{admin => access_request}/test_update_access_request_status.py (98%) rename server/app/tests/services/{admin => user}/test_get_users.py (95%) diff --git a/client/src/api/generated/index.ts b/client/src/api/generated/index.ts index d7c4b70c..ce43d738 100644 --- a/client/src/api/generated/index.ts +++ b/client/src/api/generated/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export { AdminService, AuthService, ExerciseService, FeedbackService, HealthService, MuscleGroupService, type Options, SearchService, SetService, UserService, WorkoutExerciseService, WorkoutService } from './sdk.gen'; +export { AccessRequestService, AuthService, ExerciseService, FeedbackService, HealthService, MuscleGroupService, type Options, SearchService, SetService, UserService, WorkoutExerciseService, WorkoutService } from './sdk.gen'; export type { AccessRequestPublic, AccessRequestStatus, ClientOptions, CreateExerciseData, CreateExerciseError, CreateExerciseErrors, CreateExerciseRequest, CreateExerciseResponse, CreateExerciseResponses, CreateFeedbackData, CreateFeedbackError, CreateFeedbackErrors, CreateFeedbackRequest, CreateFeedbackResponses, CreateSetData, CreateSetError, CreateSetErrors, CreateSetRequest, CreateSetResponse, CreateSetResponses, CreateWorkoutData, CreateWorkoutError, CreateWorkoutErrors, CreateWorkoutExerciseData, CreateWorkoutExerciseError, CreateWorkoutExerciseErrors, CreateWorkoutExerciseRequest, CreateWorkoutExerciseResponse, CreateWorkoutExerciseResponses, CreateWorkoutRequest, CreateWorkoutResponse, CreateWorkoutResponses, DeleteExerciseData, DeleteExerciseError, DeleteExerciseErrors, DeleteExerciseResponse, DeleteExerciseResponses, DeleteSetData, DeleteSetError, DeleteSetErrors, DeleteSetResponse, DeleteSetResponses, DeleteWorkoutData, DeleteWorkoutError, DeleteWorkoutErrors, DeleteWorkoutExerciseData, DeleteWorkoutExerciseError, DeleteWorkoutExerciseErrors, DeleteWorkoutExerciseResponse, DeleteWorkoutExerciseResponses, DeleteWorkoutResponse, DeleteWorkoutResponses, ErrorResponse, ExerciseBase, ExercisePublic, FeedbackType, ForgotPasswordData, ForgotPasswordError, ForgotPasswordErrors, ForgotPasswordRequest, ForgotPasswordResponse, ForgotPasswordResponses, GetAccessRequestsData, GetAccessRequestsError, GetAccessRequestsErrors, GetAccessRequestsResponse, GetAccessRequestsResponses, GetCurrentUserData, GetCurrentUserError, GetCurrentUserErrors, GetCurrentUserResponse, GetCurrentUserResponses, GetDbHealthData, GetDbHealthResponse, GetDbHealthResponses, GetExerciseData, GetExerciseError, GetExerciseErrors, GetExerciseResponse, GetExerciseResponses, GetExercisesData, GetExercisesError, GetExercisesErrors, GetExercisesResponse, GetExercisesResponses, GetHealthData, GetHealthResponse, GetHealthResponses, GetMuscleGroupsData, GetMuscleGroupsError, GetMuscleGroupsErrors, GetMuscleGroupsResponse, GetMuscleGroupsResponses, GetTaskData, GetTaskError, GetTaskErrors, GetTaskResponses, GetUsersData, GetUsersError, GetUsersErrors, GetUsersResponse, GetUsersResponses, GetWorkoutData, GetWorkoutError, GetWorkoutErrors, GetWorkoutResponse, GetWorkoutResponses, GetWorkoutsData, GetWorkoutsError, GetWorkoutsErrors, GetWorkoutsResponse, GetWorkoutsResponses, HttpValidationError, LoginData, LoginError, LoginErrors, LoginRequest, LoginResponse, LoginResponses, LogoutData, LogoutResponse, LogoutResponses, MuscleGroupPublic, RefreshTokenData, RefreshTokenError, RefreshTokenErrors, RefreshTokenResponse, RefreshTokenResponses, RegisterData, RegisterError, RegisterErrors, RegisterRequest, RegisterResponse, RegisterResponses, ReindexData, ReindexError, ReindexErrors, ReindexResponse, ReindexResponses, RequestAccessData, RequestAccessError, RequestAccessErrors, RequestAccessRequest, RequestAccessResponse, RequestAccessResponses, ResetPasswordData, ResetPasswordError, ResetPasswordErrors, ResetPasswordRequest, ResetPasswordResponse, ResetPasswordResponses, ReviewerPublic, SearchExercisesData, SearchExercisesError, SearchExercisesErrors, SearchExercisesResponses, SearchMuscleGroupsData, SearchMuscleGroupsError, SearchMuscleGroupsErrors, SearchMuscleGroupsResponses, SearchRequest, SetPublic, SetUnit, UpdateAccessRequestStatusData, UpdateAccessRequestStatusError, UpdateAccessRequestStatusErrors, UpdateAccessRequestStatusRequest, UpdateAccessRequestStatusResponse, UpdateAccessRequestStatusResponses, UpdateExerciseData, UpdateExerciseError, UpdateExerciseErrors, UpdateExerciseRequest, UpdateExerciseResponse, UpdateExerciseResponses, UpdateSetData, UpdateSetError, UpdateSetErrors, UpdateSetRequest, UpdateSetResponse, UpdateSetResponses, UpdateWorkoutData, UpdateWorkoutError, UpdateWorkoutErrors, UpdateWorkoutRequest, UpdateWorkoutResponse, UpdateWorkoutResponses, UserPublic, ValidationError, WorkoutBase, WorkoutExercisePublic, WorkoutPublic } from './types.gen'; diff --git a/client/src/api/generated/sdk.gen.ts b/client/src/api/generated/sdk.gen.ts index ff9f89e3..4499089d 100644 --- a/client/src/api/generated/sdk.gen.ts +++ b/client/src/api/generated/sdk.gen.ts @@ -18,7 +18,7 @@ export type Options; }; -export class AdminService { +export class AccessRequestService { /** * Get Access Requests Endpoint */ @@ -30,7 +30,7 @@ export class AdminService { name: 'access_token', type: 'apiKey' }], - url: '/api/admin/access-requests', + url: '/api/access-requests', ...options }); } @@ -45,7 +45,7 @@ export class AdminService { name: 'access_token', type: 'apiKey' }], - url: '/api/admin/access-requests/{access_request_id}', + url: '/api/access-requests/{access_request_id}', ...options, headers: { 'Content-Type': 'application/json', @@ -53,22 +53,6 @@ export class AdminService { } }); } - - /** - * Get Users Endpoint - */ - public static getUsers(options?: Options) { - return (options?.client ?? client).get({ - responseType: 'json', - security: [{ - in: 'cookie', - name: 'access_token', - type: 'apiKey' - }], - url: '/api/admin/users', - ...options - }); - } } export class AuthService { @@ -462,6 +446,22 @@ export class UserService { ...options }); } + + /** + * Get Users Endpoint + */ + public static getUsers(options?: Options) { + return (options?.client ?? client).get({ + responseType: 'json', + security: [{ + in: 'cookie', + name: 'access_token', + type: 'apiKey' + }], + url: '/api/users', + ...options + }); + } } export class WorkoutExerciseService { diff --git a/client/src/api/generated/types.gen.ts b/client/src/api/generated/types.gen.ts index fdd6804f..74d35b72 100644 --- a/client/src/api/generated/types.gen.ts +++ b/client/src/api/generated/types.gen.ts @@ -647,7 +647,7 @@ export type GetAccessRequestsData = { body?: never; path?: never; query?: never; - url: '/api/admin/access-requests'; + url: '/api/access-requests'; }; export type GetAccessRequestsErrors = { @@ -683,7 +683,7 @@ export type UpdateAccessRequestStatusData = { access_request_id: number; }; query?: never; - url: '/api/admin/access-requests/{access_request_id}'; + url: '/api/access-requests/{access_request_id}'; }; export type UpdateAccessRequestStatusErrors = { @@ -720,37 +720,6 @@ export type UpdateAccessRequestStatusResponses = { export type UpdateAccessRequestStatusResponse = UpdateAccessRequestStatusResponses[keyof UpdateAccessRequestStatusResponses]; -export type GetUsersData = { - body?: never; - path?: never; - query?: never; - url: '/api/admin/users'; -}; - -export type GetUsersErrors = { - /** - * Unauthorized - */ - 401: ErrorResponse; - /** - * Forbidden - */ - 403: ErrorResponse; -}; - -export type GetUsersError = GetUsersErrors[keyof GetUsersErrors]; - -export type GetUsersResponses = { - /** - * Response Getusers - * - * Successful Response - */ - 200: Array; -}; - -export type GetUsersResponse = GetUsersResponses[keyof GetUsersResponses]; - export type RequestAccessData = { body: RequestAccessRequest; path?: never; @@ -1505,6 +1474,37 @@ export type GetCurrentUserResponses = { export type GetCurrentUserResponse = GetCurrentUserResponses[keyof GetCurrentUserResponses]; +export type GetUsersData = { + body?: never; + path?: never; + query?: never; + url: '/api/users'; +}; + +export type GetUsersErrors = { + /** + * Unauthorized + */ + 401: ErrorResponse; + /** + * Forbidden + */ + 403: ErrorResponse; +}; + +export type GetUsersError = GetUsersErrors[keyof GetUsersErrors]; + +export type GetUsersResponses = { + /** + * Response Getusers + * + * Successful Response + */ + 200: Array; +}; + +export type GetUsersResponse = GetUsersResponses[keyof GetUsersResponses]; + export type CreateWorkoutExerciseData = { body: CreateWorkoutExerciseRequest; path: { diff --git a/client/src/api/generated/zod.gen.ts b/client/src/api/generated/zod.gen.ts index d24816a6..da102d9e 100644 --- a/client/src/api/generated/zod.gen.ts +++ b/client/src/api/generated/zod.gen.ts @@ -339,19 +339,6 @@ export const zUpdateAccessRequestStatusData = z.object({ */ export const zUpdateAccessRequestStatusResponse = z.void(); -export const zGetUsersData = z.object({ - body: z.never().optional(), - path: z.never().optional(), - query: z.never().optional() -}); - -/** - * Response Getusers - * - * Successful Response - */ -export const zGetUsersResponse = z.array(zUserPublic); - export const zRequestAccessData = z.object({ body: zRequestAccessRequest, path: z.never().optional(), @@ -625,6 +612,19 @@ export const zGetCurrentUserData = z.object({ */ export const zGetCurrentUserResponse = zUserPublic; +export const zGetUsersData = z.object({ + body: z.never().optional(), + path: z.never().optional(), + query: z.never().optional() +}); + +/** + * Response Getusers + * + * Successful Response + */ +export const zGetUsersResponse = z.array(zUserPublic); + export const zCreateWorkoutExerciseData = z.object({ body: zCreateWorkoutExerciseRequest, path: z.object({ diff --git a/client/src/components/AccessRequestsTable.tsx b/client/src/components/AccessRequestsTable.tsx index efd65ae0..7eefad3c 100644 --- a/client/src/components/AccessRequestsTable.tsx +++ b/client/src/components/AccessRequestsTable.tsx @@ -1,4 +1,4 @@ -import { AdminService } from '@/api/generated' +import { AccessRequestService } from '@/api/generated' import { AccessRequestStatusSchema } from '@/api/generated/schemas.gen' import type { AccessRequestPublic, @@ -118,14 +118,15 @@ export function AccessRequestsTable({ ) => { setIsLoadingRequestIds((prev) => new Set(prev).add(request.id)) try { - const { error } = await AdminService.updateAccessRequestStatus({ - path: { - access_request_id: request.id, - }, - body: { - status: status, - }, - }) + const { error } = + await AccessRequestService.updateAccessRequestStatus({ + path: { + access_request_id: request.id, + }, + body: { + status: status, + }, + }) if (error) { await handleApiError(error, { httpErrorHandlers: { diff --git a/client/src/pages/Admin.tsx b/client/src/pages/Admin.tsx index 38ebbae3..92a4139a 100644 --- a/client/src/pages/Admin.tsx +++ b/client/src/pages/Admin.tsx @@ -1,7 +1,8 @@ import { type AccessRequestPublic, - AdminService, + AccessRequestService, type UserPublic, + UserService, } from '@/api/generated' import { AccessRequestsTable } from '@/components/AccessRequestsTable' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' @@ -19,7 +20,8 @@ export function Admin() { const loadAccessRequests = async () => { setIsLoadingRequests(true) try { - const { data, error } = await AdminService.getAccessRequests() + const { data, error } = + await AccessRequestService.getAccessRequests() if (error) { await handleApiError(error, { fallbackMessage: 'Failed to fetch access requests', @@ -43,7 +45,7 @@ export function Admin() { const loadUsers = async () => { setIsLoadingUsers(true) try { - const { data, error } = await AdminService.getUsers() + const { data, error } = await UserService.getUsers() if (error) { await handleApiError(error, { fallbackMessage: 'Failed to fetch users', diff --git a/server/app/api/endpoints/admin.py b/server/app/api/endpoints/access_request.py similarity index 80% rename from server/app/api/endpoints/admin.py rename to server/app/api/endpoints/access_request.py index acaf1452..08984ff2 100644 --- a/server/app/api/endpoints/admin.py +++ b/server/app/api/endpoints/access_request.py @@ -11,22 +11,21 @@ ) from app.models.schemas.errors import ErrorResponseModel from app.models.schemas.user import UserPublic -from app.services.admin import ( +from app.services.access_request import ( get_access_requests, - get_users, update_access_request_status, ) from app.services.email import EmailService, get_email_service api_router = APIRouter( - prefix="/admin", - tags=["Admin"], + prefix="/access-requests", + tags=["Access Request"], dependencies=[Depends(get_current_admin)], ) @api_router.get( - "/access-requests", + "", operation_id="getAccessRequests", responses={ status.HTTP_401_UNAUTHORIZED: ErrorResponseModel, @@ -40,7 +39,7 @@ async def get_access_requests_endpoint( @api_router.patch( - "/access-requests/{access_request_id}", + "/{access_request_id}", operation_id="updateAccessRequestStatus", status_code=status.HTTP_204_NO_CONTENT, responses={ @@ -68,17 +67,3 @@ async def update_access_request_status_endpoint( email_svc=email_svc, settings=settings, ) - - -@api_router.get( - "/users", - operation_id="getUsers", - responses={ - status.HTTP_401_UNAUTHORIZED: ErrorResponseModel, - status.HTTP_403_FORBIDDEN: ErrorResponseModel, - }, -) -async def get_users_endpoint( - db_session: Annotated[AsyncSession, Depends(get_db_session)], -) -> list[UserPublic]: - return await get_users(db_session) diff --git a/server/app/api/endpoints/user.py b/server/app/api/endpoints/user.py index 0ff7827a..898f274f 100644 --- a/server/app/api/endpoints/user.py +++ b/server/app/api/endpoints/user.py @@ -1,10 +1,12 @@ from typing import Annotated from fastapi import APIRouter, Depends, status +from sqlalchemy.ext.asyncio import AsyncSession -from app.core.dependencies import get_current_user +from app.core.dependencies import get_current_admin, get_current_user, get_db_session from app.models.schemas.errors import ErrorResponseModel from app.models.schemas.user import UserPublic +from app.services.user import get_users api_router = APIRouter( prefix="/users", @@ -24,3 +26,18 @@ def get_current_user_endpoint( user: Annotated[UserPublic, Depends(get_current_user)], ) -> UserPublic: return user + + +@api_router.get( + "", + operation_id="getUsers", + responses={ + status.HTTP_401_UNAUTHORIZED: ErrorResponseModel, + status.HTTP_403_FORBIDDEN: ErrorResponseModel, + }, +) +async def get_users_endpoint( + _: Annotated[UserPublic, Depends(get_current_admin)], + db_session: Annotated[AsyncSession, Depends(get_db_session)], +) -> list[UserPublic]: + return await get_users(db_session) diff --git a/server/app/api/router.py b/server/app/api/router.py index fbf2ab06..fb945b45 100644 --- a/server/app/api/router.py +++ b/server/app/api/router.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from .endpoints.admin import api_router as admin_router +from .endpoints.access_request import api_router as access_request_router from .endpoints.auth import api_router as auth_router from .endpoints.exercise import api_router as exercise_router from .endpoints.feedback import api_router as feedback_router @@ -13,7 +13,7 @@ from .endpoints.workout_exercise import api_router as workout_exercise_router api_router = APIRouter() -api_router.include_router(admin_router) +api_router.include_router(access_request_router) api_router.include_router(auth_router) api_router.include_router(exercise_router) api_router.include_router(feedback_router) diff --git a/server/app/services/access_request.py b/server/app/services/access_request.py index 04fc9cae..7f4ea61e 100644 --- a/server/app/services/access_request.py +++ b/server/app/services/access_request.py @@ -1,11 +1,25 @@ +import logging from collections.abc import Sequence +from typing import Literal +from fastapi import BackgroundTasks from sqlalchemy import case, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload +from sqlalchemy.sql import func +from app.core.config import Settings +from app.core.security import create_registration_token from app.models.database.access_request import AccessRequest from app.models.enums import AccessRequestStatus +from app.models.errors import AccessRequestNotFound, AccessRequestNotPending +from app.models.schemas.access_request import AccessRequestPublic +from app.models.schemas.user import UserPublic +from app.services.email import EmailService +from app.services.utilities.serializers import to_access_request_public + +logger = logging.getLogger(__name__) + STATUS_PRIORITY = case( (AccessRequest.status == AccessRequestStatus.PENDING, 1), @@ -26,7 +40,7 @@ async def get_latest_access_request_by_email( return result.scalar_one_or_none() -async def get_access_request_by_id( +async def _get_access_request_by_id( access_request_id: int, db_session: AsyncSession ) -> AccessRequest | None: result = await db_session.execute( @@ -35,7 +49,7 @@ async def get_access_request_by_id( return result.scalar_one_or_none() -async def get_access_requests_with_reviewer( +async def _get_access_requests_with_reviewer( db_session: AsyncSession, ) -> Sequence[AccessRequest]: result = await db_session.execute( @@ -46,3 +60,57 @@ async def get_access_requests_with_reviewer( .order_by(AccessRequest.id.desc()) ) return result.scalars().all() + + +async def get_access_requests( + db_session: AsyncSession, +) -> list[AccessRequestPublic]: + logger.info("Getting access requests") + + requests = await _get_access_requests_with_reviewer(db_session) + return [to_access_request_public(r) for r in requests] + + +async def update_access_request_status( + access_request_id: int, + status: Literal[AccessRequestStatus.APPROVED, AccessRequestStatus.REJECTED], + db_session: AsyncSession, + user: UserPublic, + background_tasks: BackgroundTasks, + email_svc: EmailService, + settings: Settings, +) -> None: + logger.info(f"Updating access request {access_request_id} to status {status}") + + access_request = await _get_access_request_by_id(access_request_id, db_session) + + if not access_request: + logger.error(f"Access request {access_request_id} not found") + raise AccessRequestNotFound() + + if access_request.status != AccessRequestStatus.PENDING: + raise AccessRequestNotPending() + + access_request.status = status + access_request.reviewed_at = func.now() + access_request.reviewed_by = user.id + + token_str: str | None = None + if status == AccessRequestStatus.APPROVED: + token_str, token = create_registration_token(access_request.id) + db_session.add(token) + + await db_session.commit() + + if status == AccessRequestStatus.APPROVED: + assert token_str is not None + background_tasks.add_task( + email_svc.send_access_request_approved_email, + settings, + access_request, + token_str, + ) + else: + background_tasks.add_task( + email_svc.send_access_request_rejected_email, settings, access_request + ) diff --git a/server/app/services/admin.py b/server/app/services/admin.py deleted file mode 100644 index 1d78a36e..00000000 --- a/server/app/services/admin.py +++ /dev/null @@ -1,81 +0,0 @@ -import logging -from typing import Literal - -from fastapi import BackgroundTasks -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.sql import func - -from app.core.config import Settings -from app.core.security import create_registration_token -from app.models.enums import AccessRequestStatus -from app.models.errors import AccessRequestNotFound, AccessRequestNotPending -from app.models.schemas.access_request import AccessRequestPublic -from app.models.schemas.user import UserPublic -from app.services.access_request import ( - get_access_request_by_id, - get_access_requests_with_reviewer, -) -from app.services.email import EmailService -from app.services.user import get_users_ordered_by_username -from app.services.utilities.serializers import to_access_request_public, to_user_public - -logger = logging.getLogger(__name__) - - -async def get_access_requests(db_session: AsyncSession) -> list[AccessRequestPublic]: - logger.info("Getting access requests") - - requests = await get_access_requests_with_reviewer(db_session) - return [to_access_request_public(r) for r in requests] - - -async def update_access_request_status( - access_request_id: int, - status: Literal[AccessRequestStatus.APPROVED, AccessRequestStatus.REJECTED], - db_session: AsyncSession, - user: UserPublic, - background_tasks: BackgroundTasks, - email_svc: EmailService, - settings: Settings, -) -> None: - logger.info(f"Updating access request {access_request_id} to status {status}") - - access_request = await get_access_request_by_id(access_request_id, db_session) - - if not access_request: - logger.error(f"Access request {access_request_id} not found") - raise AccessRequestNotFound() - - if access_request.status != AccessRequestStatus.PENDING: - raise AccessRequestNotPending() - - access_request.status = status - access_request.reviewed_at = func.now() - access_request.reviewed_by = user.id - - token_str: str | None = None - if status == AccessRequestStatus.APPROVED: - token_str, token = create_registration_token(access_request.id) - db_session.add(token) - - await db_session.commit() - - if status == AccessRequestStatus.APPROVED: - assert token_str is not None - background_tasks.add_task( - email_svc.send_access_request_approved_email, - settings, - access_request, - token_str, - ) - else: - background_tasks.add_task( - email_svc.send_access_request_rejected_email, settings, access_request - ) - - -async def get_users(db_session: AsyncSession) -> list[UserPublic]: - logger.info("Getting users") - - users = await get_users_ordered_by_username(db_session) - return [to_user_public(user) for user in users] diff --git a/server/app/services/user.py b/server/app/services/user.py index 8b120598..dedf7c07 100644 --- a/server/app/services/user.py +++ b/server/app/services/user.py @@ -1,3 +1,4 @@ +import logging from collections.abc import Sequence from sqlalchemy import select @@ -5,6 +6,10 @@ from app.models.database.user import User from app.models.schemas.types import is_email_identifier +from app.models.schemas.user import UserPublic +from app.services.utilities.serializers import to_user_public + +logger = logging.getLogger(__name__) async def get_admin_users(db_session: AsyncSession) -> Sequence[User]: @@ -44,3 +49,10 @@ async def get_user_by_identifier( async def get_users_ordered_by_username(db_session: AsyncSession) -> Sequence[User]: result = await db_session.execute(select(User).order_by(User.username.asc())) return result.scalars().all() + + +async def get_users(db_session: AsyncSession) -> list[UserPublic]: + logger.info("Getting users") + + users = await get_users_ordered_by_username(db_session) + return [to_user_public(user) for user in users] diff --git a/server/app/tests/api/admin/__init__.py b/server/app/tests/api/access_request/__init__.py similarity index 100% rename from server/app/tests/api/admin/__init__.py rename to server/app/tests/api/access_request/__init__.py diff --git a/server/app/tests/api/admin/test_get_access_requests.py b/server/app/tests/api/access_request/test_get_access_requests.py similarity index 97% rename from server/app/tests/api/admin/test_get_access_requests.py rename to server/app/tests/api/access_request/test_get_access_requests.py index c668cfd8..e28ee963 100644 --- a/server/app/tests/api/admin/test_get_access_requests.py +++ b/server/app/tests/api/access_request/test_get_access_requests.py @@ -15,7 +15,7 @@ async def _make_request(client: AsyncClient): return await make_http_request( client, method=HttpMethod.GET, - endpoint="/api/admin/access-requests", + endpoint="/api/access-requests", ) diff --git a/server/app/tests/api/admin/test_update_access_request_status.py b/server/app/tests/api/access_request/test_update_access_request_status.py similarity index 98% rename from server/app/tests/api/admin/test_update_access_request_status.py rename to server/app/tests/api/access_request/test_update_access_request_status.py index 29cfec2d..5784eae9 100644 --- a/server/app/tests/api/admin/test_update_access_request_status.py +++ b/server/app/tests/api/access_request/test_update_access_request_status.py @@ -27,7 +27,7 @@ async def _make_request( return await make_http_request( client, method=HttpMethod.PATCH, - endpoint=f"/api/admin/access-requests/{access_request_id}", + endpoint=f"/api/access-requests/{access_request_id}", json={ "status": status_value.value, }, diff --git a/server/app/tests/api/admin/test_get_users.py b/server/app/tests/api/user/test_get_users.py similarity index 97% rename from server/app/tests/api/admin/test_get_users.py rename to server/app/tests/api/user/test_get_users.py index f37b312a..71050a9e 100644 --- a/server/app/tests/api/admin/test_get_users.py +++ b/server/app/tests/api/user/test_get_users.py @@ -15,7 +15,7 @@ async def _make_request(client: AsyncClient): return await make_http_request( client, method=HttpMethod.GET, - endpoint="/api/admin/users", + endpoint="/api/users", ) diff --git a/server/app/tests/services/admin/__init__.py b/server/app/tests/services/access_request/__init__.py similarity index 100% rename from server/app/tests/services/admin/__init__.py rename to server/app/tests/services/access_request/__init__.py diff --git a/server/app/tests/services/access_request/test_get_access_request_by_id.py b/server/app/tests/services/access_request/test_get_access_request_by_id.py index 78ad7144..5d80d6ce 100644 --- a/server/app/tests/services/access_request/test_get_access_request_by_id.py +++ b/server/app/tests/services/access_request/test_get_access_request_by_id.py @@ -1,6 +1,8 @@ from sqlalchemy.ext.asyncio import AsyncSession -from app.services.access_request import get_access_request_by_id +from app.services.access_request import ( + _get_access_request_by_id, # pyright: ignore[reportPrivateUsage] +) from app.tests.core.security.utilities import create_access_request @@ -8,7 +10,7 @@ async def test_get_access_request_by_id(db_session: AsyncSession): created = await create_access_request(db_session, "by-id@example.com") await db_session.commit() - result = await get_access_request_by_id(created.id, db_session) + result = await _get_access_request_by_id(created.id, db_session) assert result is not None assert result.id == created.id @@ -16,6 +18,6 @@ async def test_get_access_request_by_id(db_session: AsyncSession): async def test_get_access_request_by_id_not_found(db_session: AsyncSession): - result = await get_access_request_by_id(999999, db_session) + result = await _get_access_request_by_id(999999, db_session) assert result is None diff --git a/server/app/tests/services/admin/test_get_access_requests.py b/server/app/tests/services/access_request/test_get_access_requests.py similarity index 96% rename from server/app/tests/services/admin/test_get_access_requests.py rename to server/app/tests/services/access_request/test_get_access_requests.py index 103e7e01..ac4fb56a 100644 --- a/server/app/tests/services/admin/test_get_access_requests.py +++ b/server/app/tests/services/access_request/test_get_access_requests.py @@ -7,7 +7,7 @@ from app.models.database.user import User from app.models.enums import AccessRequestStatus from app.models.schemas.access_request import AccessRequestPublic, ReviewerPublic -from app.services.admin import get_access_requests +from app.services.access_request import get_access_requests async def test_get_access_requests( diff --git a/server/app/tests/services/access_request/test_get_access_requests_with_reviewer.py b/server/app/tests/services/access_request/test_get_access_requests_with_reviewer.py index 594e9096..4931aaaa 100644 --- a/server/app/tests/services/access_request/test_get_access_requests_with_reviewer.py +++ b/server/app/tests/services/access_request/test_get_access_requests_with_reviewer.py @@ -6,7 +6,9 @@ from app.models.database.access_request import AccessRequest from app.models.database.user import User from app.models.enums import AccessRequestStatus -from app.services.access_request import get_access_requests_with_reviewer +from app.services.access_request import ( + _get_access_requests_with_reviewer, # pyright: ignore[reportPrivateUsage] +) async def test_get_access_requests_with_reviewer(db_session: AsyncSession): @@ -19,7 +21,7 @@ async def test_get_access_requests_with_reviewer(db_session: AsyncSession): db_session.add(access_request) await db_session.commit() - result = await get_access_requests_with_reviewer(db_session) + result = await _get_access_requests_with_reviewer(db_session) assert isinstance(result[0], AccessRequest) assert result[0].email == "shape@example.com" @@ -55,7 +57,7 @@ async def test_get_access_requests_with_reviewer_status_ordering( db_session.add_all([approved, rejected, pending]) await db_session.commit() - result = await get_access_requests_with_reviewer(db_session) + result = await _get_access_requests_with_reviewer(db_session) statuses = [item.status for item in result] assert statuses == [ @@ -87,7 +89,7 @@ async def test_get_access_requests_with_reviewer_updated_at_ordering( db_session.add_all([ar1, ar2]) await db_session.commit() - result = await get_access_requests_with_reviewer(db_session) + result = await _get_access_requests_with_reviewer(db_session) assert result[0].id == ar2.id assert result[1].id == ar1.id @@ -114,7 +116,7 @@ async def test_get_access_requests_with_reviewer_id_ordering(db_session: AsyncSe db_session.add_all([ar1, ar2]) await db_session.commit() - result = await get_access_requests_with_reviewer(db_session) + result = await _get_access_requests_with_reviewer(db_session) assert result[0].id == ar2.id assert result[1].id == ar1.id @@ -136,7 +138,7 @@ async def test_get_access_requests_with_reviewer_reviewer(db_session: AsyncSessi db_session.add(reviewed) await db_session.commit() - result = await get_access_requests_with_reviewer(db_session) + result = await _get_access_requests_with_reviewer(db_session) assert isinstance(result[0].reviewer, User) assert result[0].reviewer.id == reviewer.id @@ -149,7 +151,7 @@ async def test_get_access_requests_with_reviewer_read_only(db_session: AsyncSess select(func.count()).select_from(AccessRequest) ) - _ = await get_access_requests_with_reviewer(db_session) + _ = await _get_access_requests_with_reviewer(db_session) after_count = await db_session.scalar( select(func.count()).select_from(AccessRequest) diff --git a/server/app/tests/services/admin/test_update_access_request_status.py b/server/app/tests/services/access_request/test_update_access_request_status.py similarity index 98% rename from server/app/tests/services/admin/test_update_access_request_status.py rename to server/app/tests/services/access_request/test_update_access_request_status.py index a64aabc5..15144ee2 100644 --- a/server/app/tests/services/admin/test_update_access_request_status.py +++ b/server/app/tests/services/access_request/test_update_access_request_status.py @@ -11,7 +11,7 @@ from app.models.database.registration_token import RegistrationToken from app.models.enums import AccessRequestStatus from app.models.errors import AccessRequestNotFound, AccessRequestNotPending -from app.services.admin import update_access_request_status +from app.services.access_request import update_access_request_status from ..utilities import get_admin_user_public diff --git a/server/app/tests/services/admin/test_get_users.py b/server/app/tests/services/user/test_get_users.py similarity index 95% rename from server/app/tests/services/admin/test_get_users.py rename to server/app/tests/services/user/test_get_users.py index 0757156a..9eee8049 100644 --- a/server/app/tests/services/admin/test_get_users.py +++ b/server/app/tests/services/user/test_get_users.py @@ -2,7 +2,7 @@ from app.models.database.user import User from app.models.schemas.user import UserPublic -from app.services.admin import get_users +from app.services.user import get_users async def test_get_users(db_session: AsyncSession): From c80ec1fe45e32618eb6b05d6ffdbd86cd6c15c70 Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Wed, 25 Mar 2026 19:17:28 -0500 Subject: [PATCH 24/27] update api surface in overview file --- PROJECT_OVERVIEW.md | 65 ++++++++++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/PROJECT_OVERVIEW.md b/PROJECT_OVERVIEW.md index b0afd804..1b48f944 100644 --- a/PROJECT_OVERVIEW.md +++ b/PROJECT_OVERVIEW.md @@ -96,32 +96,61 @@ Basic relationships: ## API Surface (Current) -- `POST /api/auth/*`: request-access, register, login, refresh-token, logout, forgot/reset-password -- `GET /api/users/current`: current user -- `GET/PATCH /api/admin/*`: access request management and user list -- `GET/POST /api/exercises`, `GET/PATCH/DELETE /api/exercises/{id}`: exercise library CRUD -- `GET /api/muscle-groups`: system muscle group reference data -- `POST /api/feedback`: feedback submission -- `GET /api/health`, `GET /api/health/db`: health checks +**Auth & onboarding** + +- `POST /api/auth/request-access` — submit an access request (returns a friendly message if already approved). +- `POST /api/auth/register` — consume a registration token and create credentials. +- `POST /api/auth/login` — issue access + refresh tokens (HTTP-only cookies). +- `POST /api/auth/refresh-token` — rotate the access token using the refresh cookie. +- `POST /api/auth/logout` — clear auth cookies. +- `POST /api/auth/forgot-password` + `POST /api/auth/reset-password` — request and fulfill password-reset tokens. + +**User / admin** + +- `GET /api/users/current` — returns the authenticated user. +- `GET /api/users` — admin-only user list. +- `GET /api/access-requests` — admin-only list of pending/approved access requests. +- `PATCH /api/access-requests/{access_request_id}` — admin-only status update (generates registration token, emails user, etc.). + +**Exercise & catalog** + +- `GET /api/exercises` — list the current user's exercises. +- `POST /api/exercises` — create a new exercise with optional muscle-group tags. +- `GET /api/exercises/{exercise_id}` — fetch a specific exercise (403 if not owned). +- `PATCH /api/exercises/{exercise_id}` — update exercise metadata. +- `DELETE /api/exercises/{exercise_id}` — delete an exercise (owned-only). +- `GET /api/muscle-groups` — reference data sorted by name. + +**Search / indexing** + +- `POST /api/search/exercises` — Meilisearch-powered search scoped to the current user. +- `POST /api/search/muscle-groups` — search muscle groups. +- `POST /api/search/reindex` — admin-only reindex of exercises + muscle groups. +- `GET /api/search/tasks/{task_id}` — admin-only Meilisearch task status lookup. + +**Feedback & health** + +- `POST /api/feedback` — multipart/form-data feedback submission (creates GitHub issue via background task). +- `GET /api/health` and `GET /api/health/db` — basic API and Postgres liveness checks. **Workouts** -- `GET /api/workouts` — list current user's workouts -- `POST /api/workouts` — create a workout -- `GET /api/workouts/{id}` — get workout with exercises and sets -- `PATCH /api/workouts/{id}` — update workout (started_at, ended_at, notes) -- `DELETE /api/workouts/{id}` — delete workout +- `GET /api/workouts` — list workouts for the current user. +- `POST /api/workouts` — create a workout shell (started_at, ended_at, notes). +- `GET /api/workouts/{workout_id}` — full workout with exercises and sets. +- `PATCH /api/workouts/{workout_id}` — update metadata (started, ended, notes). +- `DELETE /api/workouts/{workout_id}` — delete a workout (user-owned). -**Workout Exercises** +**Workout exercises** -- `POST /api/workouts/{workout_id}/exercises` — add an exercise to a workout -- `DELETE /api/workouts/{workout_id}/exercises/{workout_exercise_id}` — remove exercise from workout +- `POST /api/workouts/{workout_id}/exercises` — attach an exercise to a workout. +- `DELETE /api/workouts/{workout_id}/exercises/{workout_exercise_id}` — remove the workout-specific exercise. **Sets** -- `POST /api/workouts/{workout_id}/exercises/{workout_exercise_id}/sets` — log a set -- `PATCH /api/workouts/{workout_id}/exercises/{workout_exercise_id}/sets/{set_id}` — update a set -- `DELETE /api/workouts/{workout_id}/exercises/{workout_exercise_id}/sets/{set_id}` — delete a set +- `POST /api/workouts/{workout_id}/exercises/{workout_exercise_id}/sets` — log a set for the workout exercise. +- `PATCH /api/workouts/{workout_id}/exercises/{workout_exercise_id}/sets/{set_id}` — adjust reps/weight. +- `DELETE /api/workouts/{workout_id}/exercises/{workout_exercise_id}/sets/{set_id}` — delete a set from a workout exercise. ## Infrastructure & Deployment From bd4e6f320ab56141ba6032340b592d2b9a727178 Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Wed, 25 Mar 2026 19:48:28 -0500 Subject: [PATCH 25/27] server - add endpoint return types --- client/src/api/generated/index.ts | 2 +- client/src/api/generated/schemas.gen.ts | 498 ++++++++++++++++++++++++ client/src/api/generated/types.gen.ts | 234 ++++++++++- client/src/api/generated/zod.gen.ts | 88 +++++ server/app/api/endpoints/search.py | 10 +- 5 files changed, 825 insertions(+), 7 deletions(-) diff --git a/client/src/api/generated/index.ts b/client/src/api/generated/index.ts index ce43d738..a0c95b93 100644 --- a/client/src/api/generated/index.ts +++ b/client/src/api/generated/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts export { AccessRequestService, AuthService, ExerciseService, FeedbackService, HealthService, MuscleGroupService, type Options, SearchService, SetService, UserService, WorkoutExerciseService, WorkoutService } from './sdk.gen'; -export type { AccessRequestPublic, AccessRequestStatus, ClientOptions, CreateExerciseData, CreateExerciseError, CreateExerciseErrors, CreateExerciseRequest, CreateExerciseResponse, CreateExerciseResponses, CreateFeedbackData, CreateFeedbackError, CreateFeedbackErrors, CreateFeedbackRequest, CreateFeedbackResponses, CreateSetData, CreateSetError, CreateSetErrors, CreateSetRequest, CreateSetResponse, CreateSetResponses, CreateWorkoutData, CreateWorkoutError, CreateWorkoutErrors, CreateWorkoutExerciseData, CreateWorkoutExerciseError, CreateWorkoutExerciseErrors, CreateWorkoutExerciseRequest, CreateWorkoutExerciseResponse, CreateWorkoutExerciseResponses, CreateWorkoutRequest, CreateWorkoutResponse, CreateWorkoutResponses, DeleteExerciseData, DeleteExerciseError, DeleteExerciseErrors, DeleteExerciseResponse, DeleteExerciseResponses, DeleteSetData, DeleteSetError, DeleteSetErrors, DeleteSetResponse, DeleteSetResponses, DeleteWorkoutData, DeleteWorkoutError, DeleteWorkoutErrors, DeleteWorkoutExerciseData, DeleteWorkoutExerciseError, DeleteWorkoutExerciseErrors, DeleteWorkoutExerciseResponse, DeleteWorkoutExerciseResponses, DeleteWorkoutResponse, DeleteWorkoutResponses, ErrorResponse, ExerciseBase, ExercisePublic, FeedbackType, ForgotPasswordData, ForgotPasswordError, ForgotPasswordErrors, ForgotPasswordRequest, ForgotPasswordResponse, ForgotPasswordResponses, GetAccessRequestsData, GetAccessRequestsError, GetAccessRequestsErrors, GetAccessRequestsResponse, GetAccessRequestsResponses, GetCurrentUserData, GetCurrentUserError, GetCurrentUserErrors, GetCurrentUserResponse, GetCurrentUserResponses, GetDbHealthData, GetDbHealthResponse, GetDbHealthResponses, GetExerciseData, GetExerciseError, GetExerciseErrors, GetExerciseResponse, GetExerciseResponses, GetExercisesData, GetExercisesError, GetExercisesErrors, GetExercisesResponse, GetExercisesResponses, GetHealthData, GetHealthResponse, GetHealthResponses, GetMuscleGroupsData, GetMuscleGroupsError, GetMuscleGroupsErrors, GetMuscleGroupsResponse, GetMuscleGroupsResponses, GetTaskData, GetTaskError, GetTaskErrors, GetTaskResponses, GetUsersData, GetUsersError, GetUsersErrors, GetUsersResponse, GetUsersResponses, GetWorkoutData, GetWorkoutError, GetWorkoutErrors, GetWorkoutResponse, GetWorkoutResponses, GetWorkoutsData, GetWorkoutsError, GetWorkoutsErrors, GetWorkoutsResponse, GetWorkoutsResponses, HttpValidationError, LoginData, LoginError, LoginErrors, LoginRequest, LoginResponse, LoginResponses, LogoutData, LogoutResponse, LogoutResponses, MuscleGroupPublic, RefreshTokenData, RefreshTokenError, RefreshTokenErrors, RefreshTokenResponse, RefreshTokenResponses, RegisterData, RegisterError, RegisterErrors, RegisterRequest, RegisterResponse, RegisterResponses, ReindexData, ReindexError, ReindexErrors, ReindexResponse, ReindexResponses, RequestAccessData, RequestAccessError, RequestAccessErrors, RequestAccessRequest, RequestAccessResponse, RequestAccessResponses, ResetPasswordData, ResetPasswordError, ResetPasswordErrors, ResetPasswordRequest, ResetPasswordResponse, ResetPasswordResponses, ReviewerPublic, SearchExercisesData, SearchExercisesError, SearchExercisesErrors, SearchExercisesResponses, SearchMuscleGroupsData, SearchMuscleGroupsError, SearchMuscleGroupsErrors, SearchMuscleGroupsResponses, SearchRequest, SetPublic, SetUnit, UpdateAccessRequestStatusData, UpdateAccessRequestStatusError, UpdateAccessRequestStatusErrors, UpdateAccessRequestStatusRequest, UpdateAccessRequestStatusResponse, UpdateAccessRequestStatusResponses, UpdateExerciseData, UpdateExerciseError, UpdateExerciseErrors, UpdateExerciseRequest, UpdateExerciseResponse, UpdateExerciseResponses, UpdateSetData, UpdateSetError, UpdateSetErrors, UpdateSetRequest, UpdateSetResponse, UpdateSetResponses, UpdateWorkoutData, UpdateWorkoutError, UpdateWorkoutErrors, UpdateWorkoutRequest, UpdateWorkoutResponse, UpdateWorkoutResponses, UserPublic, ValidationError, WorkoutBase, WorkoutExercisePublic, WorkoutPublic } from './types.gen'; +export type { AccessRequestPublic, AccessRequestStatus, ClientOptions, CreateExerciseData, CreateExerciseError, CreateExerciseErrors, CreateExerciseRequest, CreateExerciseResponse, CreateExerciseResponses, CreateFeedbackData, CreateFeedbackError, CreateFeedbackErrors, CreateFeedbackRequest, CreateFeedbackResponses, CreateSetData, CreateSetError, CreateSetErrors, CreateSetRequest, CreateSetResponse, CreateSetResponses, CreateWorkoutData, CreateWorkoutError, CreateWorkoutErrors, CreateWorkoutExerciseData, CreateWorkoutExerciseError, CreateWorkoutExerciseErrors, CreateWorkoutExerciseRequest, CreateWorkoutExerciseResponse, CreateWorkoutExerciseResponses, CreateWorkoutRequest, CreateWorkoutResponse, CreateWorkoutResponses, DeleteExerciseData, DeleteExerciseError, DeleteExerciseErrors, DeleteExerciseResponse, DeleteExerciseResponses, DeleteSetData, DeleteSetError, DeleteSetErrors, DeleteSetResponse, DeleteSetResponses, DeleteWorkoutData, DeleteWorkoutError, DeleteWorkoutErrors, DeleteWorkoutExerciseData, DeleteWorkoutExerciseError, DeleteWorkoutExerciseErrors, DeleteWorkoutExerciseResponse, DeleteWorkoutExerciseResponses, DeleteWorkoutResponse, DeleteWorkoutResponses, ErrorResponse, ExerciseBase, ExerciseDocument, ExercisePublic, FeedbackType, ForgotPasswordData, ForgotPasswordError, ForgotPasswordErrors, ForgotPasswordRequest, ForgotPasswordResponse, ForgotPasswordResponses, GetAccessRequestsData, GetAccessRequestsError, GetAccessRequestsErrors, GetAccessRequestsResponse, GetAccessRequestsResponses, GetCurrentUserData, GetCurrentUserError, GetCurrentUserErrors, GetCurrentUserResponse, GetCurrentUserResponses, GetDbHealthData, GetDbHealthResponse, GetDbHealthResponses, GetExerciseData, GetExerciseError, GetExerciseErrors, GetExerciseResponse, GetExerciseResponses, GetExercisesData, GetExercisesError, GetExercisesErrors, GetExercisesResponse, GetExercisesResponses, GetHealthData, GetHealthResponse, GetHealthResponses, GetMuscleGroupsData, GetMuscleGroupsError, GetMuscleGroupsErrors, GetMuscleGroupsResponse, GetMuscleGroupsResponses, GetTaskData, GetTaskError, GetTaskErrors, GetTaskResponse, GetTaskResponses, GetUsersData, GetUsersError, GetUsersErrors, GetUsersResponse, GetUsersResponses, GetWorkoutData, GetWorkoutError, GetWorkoutErrors, GetWorkoutResponse, GetWorkoutResponses, GetWorkoutsData, GetWorkoutsError, GetWorkoutsErrors, GetWorkoutsResponse, GetWorkoutsResponses, HttpValidationError, LoginData, LoginError, LoginErrors, LoginRequest, LoginResponse, LoginResponses, LogoutData, LogoutResponse, LogoutResponses, MuscleGroupPublic, RefreshTokenData, RefreshTokenError, RefreshTokenErrors, RefreshTokenResponse, RefreshTokenResponses, RegisterData, RegisterError, RegisterErrors, RegisterRequest, RegisterResponse, RegisterResponses, ReindexData, ReindexError, ReindexErrors, ReindexResponse, ReindexResponses, RequestAccessData, RequestAccessError, RequestAccessErrors, RequestAccessRequest, RequestAccessResponse, RequestAccessResponses, ResetPasswordData, ResetPasswordError, ResetPasswordErrors, ResetPasswordRequest, ResetPasswordResponse, ResetPasswordResponses, ReviewerPublic, SearchExercisesData, SearchExercisesError, SearchExercisesErrors, SearchExercisesResponse, SearchExercisesResponses, SearchMuscleGroupsData, SearchMuscleGroupsError, SearchMuscleGroupsErrors, SearchMuscleGroupsResponse, SearchMuscleGroupsResponses, SearchRequest, SearchResultsExerciseDocument, SearchResultsMuscleGroupPublic, SetPublic, SetUnit, TaskResult, UpdateAccessRequestStatusData, UpdateAccessRequestStatusError, UpdateAccessRequestStatusErrors, UpdateAccessRequestStatusRequest, UpdateAccessRequestStatusResponse, UpdateAccessRequestStatusResponses, UpdateExerciseData, UpdateExerciseError, UpdateExerciseErrors, UpdateExerciseRequest, UpdateExerciseResponse, UpdateExerciseResponses, UpdateSetData, UpdateSetError, UpdateSetErrors, UpdateSetRequest, UpdateSetResponse, UpdateSetResponses, UpdateWorkoutData, UpdateWorkoutError, UpdateWorkoutErrors, UpdateWorkoutRequest, UpdateWorkoutResponse, UpdateWorkoutResponses, UserPublic, ValidationError, WorkoutBase, WorkoutExercisePublic, WorkoutPublic } from './types.gen'; diff --git a/client/src/api/generated/schemas.gen.ts b/client/src/api/generated/schemas.gen.ts index 209f0009..9a2bf6ff 100644 --- a/client/src/api/generated/schemas.gen.ts +++ b/client/src/api/generated/schemas.gen.ts @@ -356,6 +356,57 @@ export const ExerciseBaseSchema = { title: 'ExerciseBase' } as const; +export const ExerciseDocumentSchema = { + properties: { + id: { + type: 'integer', + title: 'Id' + }, + user_id: { + anyOf: [ + { + type: 'integer' + }, + { + type: 'null' + } + ], + title: 'User Id' + }, + name: { + type: 'string', + title: 'Name' + }, + description: { + anyOf: [ + { + type: 'string' + }, + { + type: 'null' + } + ], + title: 'Description' + }, + muscle_group_names: { + items: { + type: 'string' + }, + type: 'array', + title: 'Muscle Group Names' + } + }, + type: 'object', + required: [ + 'id', + 'user_id', + 'name', + 'description', + 'muscle_group_names' + ], + title: 'ExerciseDocument' +} as const; + export const ExercisePublicSchema = { properties: { id: { @@ -647,6 +698,312 @@ export const SearchRequestSchema = { title: 'SearchRequest' } as const; +export const SearchResults_ExerciseDocument_Schema = { + properties: { + hits: { + items: { + $ref: '#/components/schemas/ExerciseDocument' + }, + type: 'array', + title: 'Hits' + }, + offset: { + anyOf: [ + { + type: 'integer' + }, + { + type: 'null' + } + ], + title: 'Offset' + }, + limit: { + anyOf: [ + { + type: 'integer' + }, + { + type: 'null' + } + ], + title: 'Limit' + }, + estimatedTotalHits: { + anyOf: [ + { + type: 'integer' + }, + { + type: 'null' + } + ], + title: 'Estimatedtotalhits' + }, + processingTimeMs: { + type: 'integer', + title: 'Processingtimems' + }, + query: { + type: 'string', + title: 'Query' + }, + facetDistribution: { + anyOf: [ + { + additionalProperties: true, + type: 'object' + }, + { + type: 'null' + } + ], + title: 'Facetdistribution' + }, + totalPages: { + anyOf: [ + { + type: 'integer' + }, + { + type: 'null' + } + ], + title: 'Totalpages' + }, + totalHits: { + anyOf: [ + { + type: 'integer' + }, + { + type: 'null' + } + ], + title: 'Totalhits' + }, + page: { + anyOf: [ + { + type: 'integer' + }, + { + type: 'null' + } + ], + title: 'Page' + }, + hitsPerPage: { + anyOf: [ + { + type: 'integer' + }, + { + type: 'null' + } + ], + title: 'Hitsperpage' + }, + semanticHitCount: { + anyOf: [ + { + type: 'integer' + }, + { + type: 'null' + } + ], + title: 'Semantichitcount' + }, + queryVector: { + anyOf: [ + { + items: { + type: 'number' + }, + type: 'array' + }, + { + type: 'null' + } + ], + title: 'Queryvector' + }, + performanceDetails: { + anyOf: [ + { + additionalProperties: true, + type: 'object' + }, + { + type: 'null' + } + ], + title: 'Performancedetails' + } + }, + type: 'object', + required: [ + 'hits', + 'processingTimeMs', + 'query' + ], + title: 'SearchResults[ExerciseDocument]' +} as const; + +export const SearchResults_MuscleGroupPublic_Schema = { + properties: { + hits: { + items: { + $ref: '#/components/schemas/MuscleGroupPublic' + }, + type: 'array', + title: 'Hits' + }, + offset: { + anyOf: [ + { + type: 'integer' + }, + { + type: 'null' + } + ], + title: 'Offset' + }, + limit: { + anyOf: [ + { + type: 'integer' + }, + { + type: 'null' + } + ], + title: 'Limit' + }, + estimatedTotalHits: { + anyOf: [ + { + type: 'integer' + }, + { + type: 'null' + } + ], + title: 'Estimatedtotalhits' + }, + processingTimeMs: { + type: 'integer', + title: 'Processingtimems' + }, + query: { + type: 'string', + title: 'Query' + }, + facetDistribution: { + anyOf: [ + { + additionalProperties: true, + type: 'object' + }, + { + type: 'null' + } + ], + title: 'Facetdistribution' + }, + totalPages: { + anyOf: [ + { + type: 'integer' + }, + { + type: 'null' + } + ], + title: 'Totalpages' + }, + totalHits: { + anyOf: [ + { + type: 'integer' + }, + { + type: 'null' + } + ], + title: 'Totalhits' + }, + page: { + anyOf: [ + { + type: 'integer' + }, + { + type: 'null' + } + ], + title: 'Page' + }, + hitsPerPage: { + anyOf: [ + { + type: 'integer' + }, + { + type: 'null' + } + ], + title: 'Hitsperpage' + }, + semanticHitCount: { + anyOf: [ + { + type: 'integer' + }, + { + type: 'null' + } + ], + title: 'Semantichitcount' + }, + queryVector: { + anyOf: [ + { + items: { + type: 'number' + }, + type: 'array' + }, + { + type: 'null' + } + ], + title: 'Queryvector' + }, + performanceDetails: { + anyOf: [ + { + additionalProperties: true, + type: 'object' + }, + { + type: 'null' + } + ], + title: 'Performancedetails' + } + }, + type: 'object', + required: [ + 'hits', + 'processingTimeMs', + 'query' + ], + title: 'SearchResults[MuscleGroupPublic]' +} as const; + export const SetPublicSchema = { properties: { id: { @@ -740,6 +1097,147 @@ export const SetUnitSchema = { title: 'SetUnit' } as const; +export const TaskResultSchema = { + properties: { + uid: { + type: 'integer', + title: 'Uid' + }, + indexUid: { + anyOf: [ + { + type: 'string' + }, + { + type: 'null' + } + ], + title: 'Indexuid' + }, + status: { + type: 'string', + title: 'Status' + }, + type: { + anyOf: [ + { + type: 'string' + }, + { + additionalProperties: true, + type: 'object' + } + ], + title: 'Type' + }, + details: { + anyOf: [ + { + additionalProperties: true, + type: 'object' + }, + { + type: 'null' + } + ], + title: 'Details' + }, + error: { + anyOf: [ + { + additionalProperties: true, + type: 'object' + }, + { + type: 'null' + } + ], + title: 'Error' + }, + canceledBy: { + anyOf: [ + { + type: 'integer' + }, + { + type: 'null' + } + ], + title: 'Canceledby' + }, + duration: { + anyOf: [ + { + type: 'string' + }, + { + type: 'null' + } + ], + title: 'Duration' + }, + enqueuedAt: { + type: 'string', + format: 'date-time', + title: 'Enqueuedat' + }, + startedAt: { + anyOf: [ + { + type: 'string', + format: 'date-time' + }, + { + type: 'null' + } + ], + title: 'Startedat' + }, + finishedAt: { + anyOf: [ + { + type: 'string', + format: 'date-time' + }, + { + type: 'null' + } + ], + title: 'Finishedat' + }, + batchUid: { + anyOf: [ + { + type: 'integer' + }, + { + type: 'null' + } + ], + title: 'Batchuid' + }, + customMetadata: { + anyOf: [ + { + type: 'string' + }, + { + type: 'null' + } + ], + title: 'Custommetadata' + } + }, + type: 'object', + required: [ + 'uid', + 'status', + 'type', + 'enqueuedAt' + ], + title: 'TaskResult' +} as const; + export const UpdateAccessRequestStatusRequestSchema = { properties: { status: { diff --git a/client/src/api/generated/types.gen.ts b/client/src/api/generated/types.gen.ts index 74d35b72..5bc0c841 100644 --- a/client/src/api/generated/types.gen.ts +++ b/client/src/api/generated/types.gen.ts @@ -181,6 +181,32 @@ export type ExerciseBase = { updated_at: string; }; +/** + * ExerciseDocument + */ +export type ExerciseDocument = { + /** + * Id + */ + id: number; + /** + * User Id + */ + user_id: number | null; + /** + * Name + */ + name: string; + /** + * Description + */ + description: string | null; + /** + * Muscle Group Names + */ + muscle_group_names: Array; +}; + /** * ExercisePublic */ @@ -354,6 +380,138 @@ export type SearchRequest = { limit: number; }; +/** + * SearchResults[ExerciseDocument] + */ +export type SearchResultsExerciseDocument = { + /** + * Hits + */ + hits: Array; + /** + * Offset + */ + offset?: number | null; + /** + * Limit + */ + limit?: number | null; + /** + * Estimatedtotalhits + */ + estimatedTotalHits?: number | null; + /** + * Processingtimems + */ + processingTimeMs: number; + /** + * Query + */ + query: string; + /** + * Facetdistribution + */ + facetDistribution?: { + [key: string]: unknown; + } | null; + /** + * Totalpages + */ + totalPages?: number | null; + /** + * Totalhits + */ + totalHits?: number | null; + /** + * Page + */ + page?: number | null; + /** + * Hitsperpage + */ + hitsPerPage?: number | null; + /** + * Semantichitcount + */ + semanticHitCount?: number | null; + /** + * Queryvector + */ + queryVector?: Array | null; + /** + * Performancedetails + */ + performanceDetails?: { + [key: string]: unknown; + } | null; +}; + +/** + * SearchResults[MuscleGroupPublic] + */ +export type SearchResultsMuscleGroupPublic = { + /** + * Hits + */ + hits: Array; + /** + * Offset + */ + offset?: number | null; + /** + * Limit + */ + limit?: number | null; + /** + * Estimatedtotalhits + */ + estimatedTotalHits?: number | null; + /** + * Processingtimems + */ + processingTimeMs: number; + /** + * Query + */ + query: string; + /** + * Facetdistribution + */ + facetDistribution?: { + [key: string]: unknown; + } | null; + /** + * Totalpages + */ + totalPages?: number | null; + /** + * Totalhits + */ + totalHits?: number | null; + /** + * Page + */ + page?: number | null; + /** + * Hitsperpage + */ + hitsPerPage?: number | null; + /** + * Semantichitcount + */ + semanticHitCount?: number | null; + /** + * Queryvector + */ + queryVector?: Array | null; + /** + * Performancedetails + */ + performanceDetails?: { + [key: string]: unknown; + } | null; +}; + /** * SetPublic */ @@ -401,6 +559,70 @@ export type SetPublic = { */ export type SetUnit = 'kg' | 'lb'; +/** + * TaskResult + */ +export type TaskResult = { + /** + * Uid + */ + uid: number; + /** + * Indexuid + */ + indexUid?: string | null; + /** + * Status + */ + status: string; + /** + * Type + */ + type: string | { + [key: string]: unknown; + }; + /** + * Details + */ + details?: { + [key: string]: unknown; + } | null; + /** + * Error + */ + error?: { + [key: string]: unknown; + } | null; + /** + * Canceledby + */ + canceledBy?: number | null; + /** + * Duration + */ + duration?: string | null; + /** + * Enqueuedat + */ + enqueuedAt: string; + /** + * Startedat + */ + startedAt?: string | null; + /** + * Finishedat + */ + finishedAt?: string | null; + /** + * Batchuid + */ + batchUid?: number | null; + /** + * Custommetadata + */ + customMetadata?: string | null; +}; + /** * UpdateAccessRequestStatusRequest */ @@ -1225,9 +1447,11 @@ export type GetTaskResponses = { /** * Successful Response */ - 200: unknown; + 200: TaskResult; }; +export type GetTaskResponse = GetTaskResponses[keyof GetTaskResponses]; + export type ReindexData = { body?: never; path?: never; @@ -1281,9 +1505,11 @@ export type SearchMuscleGroupsResponses = { /** * Successful Response */ - 200: unknown; + 200: SearchResultsMuscleGroupPublic; }; +export type SearchMuscleGroupsResponse = SearchMuscleGroupsResponses[keyof SearchMuscleGroupsResponses]; + export type SearchExercisesData = { body: SearchRequest; path?: never; @@ -1308,9 +1534,11 @@ export type SearchExercisesResponses = { /** * Successful Response */ - 200: unknown; + 200: SearchResultsExerciseDocument; }; +export type SearchExercisesResponse = SearchExercisesResponses[keyof SearchExercisesResponses]; + export type CreateSetData = { body: CreateSetRequest; path: { diff --git a/client/src/api/generated/zod.gen.ts b/client/src/api/generated/zod.gen.ts index da102d9e..db0875bc 100644 --- a/client/src/api/generated/zod.gen.ts +++ b/client/src/api/generated/zod.gen.ts @@ -57,6 +57,17 @@ export const zExerciseBase = z.object({ updated_at: z.iso.datetime() }); +/** + * ExerciseDocument + */ +export const zExerciseDocument = z.object({ + id: z.int(), + user_id: z.int().nullable(), + name: z.string(), + description: z.string().nullable(), + muscle_group_names: z.array(z.string()) +}); + /** * FeedbackType */ @@ -168,6 +179,46 @@ export const zSearchRequest = z.object({ limit: z.int() }); +/** + * SearchResults[ExerciseDocument] + */ +export const zSearchResultsExerciseDocument = z.object({ + hits: z.array(zExerciseDocument), + offset: z.int().nullish(), + limit: z.int().nullish(), + estimatedTotalHits: z.int().nullish(), + processingTimeMs: z.int(), + query: z.string(), + facetDistribution: z.record(z.string(), z.unknown()).nullish(), + totalPages: z.int().nullish(), + totalHits: z.int().nullish(), + page: z.int().nullish(), + hitsPerPage: z.int().nullish(), + semanticHitCount: z.int().nullish(), + queryVector: z.array(z.number()).nullish(), + performanceDetails: z.record(z.string(), z.unknown()).nullish() +}); + +/** + * SearchResults[MuscleGroupPublic] + */ +export const zSearchResultsMuscleGroupPublic = z.object({ + hits: z.array(zMuscleGroupPublic), + offset: z.int().nullish(), + limit: z.int().nullish(), + estimatedTotalHits: z.int().nullish(), + processingTimeMs: z.int(), + query: z.string(), + facetDistribution: z.record(z.string(), z.unknown()).nullish(), + totalPages: z.int().nullish(), + totalHits: z.int().nullish(), + page: z.int().nullish(), + hitsPerPage: z.int().nullish(), + semanticHitCount: z.int().nullish(), + queryVector: z.array(z.number()).nullish(), + performanceDetails: z.record(z.string(), z.unknown()).nullish() +}); + /** * SetPublic */ @@ -201,6 +252,28 @@ export const zCreateSetRequest = z.object({ notes: z.string().max(1000).nullish() }); +/** + * TaskResult + */ +export const zTaskResult = z.object({ + uid: z.int(), + indexUid: z.string().nullish(), + status: z.string(), + type: z.union([ + z.string(), + z.record(z.string(), z.unknown()) + ]), + details: z.record(z.string(), z.unknown()).nullish(), + error: z.record(z.string(), z.unknown()).nullish(), + canceledBy: z.int().nullish(), + duration: z.string().nullish(), + enqueuedAt: z.iso.datetime(), + startedAt: z.iso.datetime().nullish(), + finishedAt: z.iso.datetime().nullish(), + batchUid: z.int().nullish(), + customMetadata: z.string().nullish() +}); + /** * UpdateAccessRequestStatusRequest */ @@ -534,6 +607,11 @@ export const zGetTaskData = z.object({ query: z.never().optional() }); +/** + * Successful Response + */ +export const zGetTaskResponse = zTaskResult; + export const zReindexData = z.object({ body: z.never().optional(), path: z.never().optional(), @@ -551,12 +629,22 @@ export const zSearchMuscleGroupsData = z.object({ query: z.never().optional() }); +/** + * Successful Response + */ +export const zSearchMuscleGroupsResponse = zSearchResultsMuscleGroupPublic; + export const zSearchExercisesData = z.object({ body: zSearchRequest, path: z.never().optional(), query: z.never().optional() }); +/** + * Successful Response + */ +export const zSearchExercisesResponse = zSearchResultsExerciseDocument; + export const zCreateSetData = z.object({ body: zCreateSetRequest, path: z.object({ diff --git a/server/app/api/endpoints/search.py b/server/app/api/endpoints/search.py index e39984d5..2bf7d3de 100644 --- a/server/app/api/endpoints/search.py +++ b/server/app/api/endpoints/search.py @@ -2,6 +2,8 @@ from fastapi import APIRouter, Depends, status from meilisearch_python_sdk import AsyncClient +from meilisearch_python_sdk.models.search import SearchResults +from meilisearch_python_sdk.models.task import TaskResult from sqlalchemy.ext.asyncio import AsyncSession from app.core.dependencies import ( @@ -11,6 +13,8 @@ get_ms_client, ) from app.models.schemas.errors import ErrorResponseModel +from app.models.schemas.exercise import ExerciseDocument +from app.models.schemas.muscle_group import MuscleGroupPublic from app.models.schemas.search import SearchRequest from app.models.schemas.user import UserPublic from app.services.search import ( @@ -39,7 +43,7 @@ async def get_task_endpoint( task_id: int, _: Annotated[UserPublic, Depends(get_current_admin)], ms_client: Annotated[AsyncClient, Depends(get_ms_client)], -): +) -> TaskResult: return await get_task(ms_client, task_id) @@ -73,7 +77,7 @@ async def reindex_endpoint( async def search_muscle_groups_endpoint( req: SearchRequest, ms_client: Annotated[AsyncClient, Depends(get_ms_client)], -): +) -> SearchResults[MuscleGroupPublic]: return await search_muscle_groups( req=req, ms_client=ms_client, @@ -91,7 +95,7 @@ async def search_exercises_endpoint( req: SearchRequest, user: Annotated[UserPublic, Depends(get_current_user)], ms_client: Annotated[AsyncClient, Depends(get_ms_client)], -): +) -> SearchResults[ExerciseDocument]: return await search_exercises( req=req, user_id=user.id, From 81be2202edb0c5993aa285c623cbaab7b8f28d32 Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Thu, 26 Mar 2026 08:06:54 -0500 Subject: [PATCH 26/27] server - add agent browser config --- .github/copilot-instructions.md | 23 +++++++++++++++-------- .gitignore | 1 + agent-browser.json | 5 +++++ 3 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 agent-browser.json diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index cc772076..593e6a81 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -57,14 +57,21 @@ RepTrack is a full-stack strength-training tracker: ## Browser Automation -Use `agent-browser` for web automation. Run `agent-browser --help` for all commands. - -Core workflow: - -1. `agent-browser open ` - Navigate to page -2. `agent-browser snapshot -i` - Get interactive elements with refs (@e1, @e2) -3. `agent-browser click @e1` / `fill @e2 "text"` - Interact using refs -4. Re-snapshot after page changes +Use `agent-browser` for web automation. +Run `agent-browser --help` for all commands. +Run `agent-browser` commands from the project root to use the correct config and browser profile. + +Example workflow (assuming dev servers are running on default ports): + +``` +agent-browser open http://localhost:5173 +agent-browser snapshot -i +agent-browser click @e1 +agent-browser snapshot -i +agent-browser fill @e2 "hello world" +agent-browser snapshot -i +agent-browser close +``` ## Key Conventions diff --git a/.gitignore b/.gitignore index 904716c6..865c7d4a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ openapi_spec.json todo.md sandbox/ +browser-profile/ diff --git a/agent-browser.json b/agent-browser.json new file mode 100644 index 00000000..4ff07e47 --- /dev/null +++ b/agent-browser.json @@ -0,0 +1,5 @@ +{ + "headed": true, + "profile": "./browser-profile", + "colorScheme": "dark" +} From 263ad6d436b94db49e5d54b9cd1cc9941bb49285 Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Thu, 26 Mar 2026 09:17:30 -0500 Subject: [PATCH 27/27] server - add search api tests --- client/src/api/generated/schemas.gen.ts | 6 +- client/src/api/generated/types.gen.ts | 2 +- client/src/api/generated/zod.gen.ts | 2 +- server/app/api/endpoints/search.py | 4 +- server/app/models/schemas/search.py | 2 +- server/app/services/search.py | 2 +- .../test_get_access_requests.py | 2 +- .../test_update_access_request_status.py | 2 +- server/app/tests/api/search/__init__.py | 0 server/app/tests/api/search/test_get_task.py | 68 +++++++++++++++++++ server/app/tests/api/search/test_reindex.py | 61 +++++++++++++++++ .../tests/api/search/test_search_exercises.py | 27 ++++++++ .../api/search/test_search_muscle_groups.py | 49 +++++++++++++ server/app/tests/api/search/utilities.py | 13 ++++ server/app/tests/api/user/test_get_users.py | 2 +- .../{test_reindex_data.py => test_reindex.py} | 6 +- 16 files changed, 233 insertions(+), 15 deletions(-) create mode 100644 server/app/tests/api/search/__init__.py create mode 100644 server/app/tests/api/search/test_get_task.py create mode 100644 server/app/tests/api/search/test_reindex.py create mode 100644 server/app/tests/api/search/test_search_exercises.py create mode 100644 server/app/tests/api/search/test_search_muscle_groups.py create mode 100644 server/app/tests/api/search/utilities.py rename server/app/tests/services/search/{test_reindex_data.py => test_reindex.py} (85%) diff --git a/client/src/api/generated/schemas.gen.ts b/client/src/api/generated/schemas.gen.ts index 9a2bf6ff..bb22e896 100644 --- a/client/src/api/generated/schemas.gen.ts +++ b/client/src/api/generated/schemas.gen.ts @@ -687,13 +687,13 @@ export const SearchRequestSchema = { }, limit: { type: 'integer', - title: 'Limit' + title: 'Limit', + default: 25 } }, type: 'object', required: [ - 'query', - 'limit' + 'query' ], title: 'SearchRequest' } as const; diff --git a/client/src/api/generated/types.gen.ts b/client/src/api/generated/types.gen.ts index 5bc0c841..de656086 100644 --- a/client/src/api/generated/types.gen.ts +++ b/client/src/api/generated/types.gen.ts @@ -377,7 +377,7 @@ export type SearchRequest = { /** * Limit */ - limit: number; + limit?: number; }; /** diff --git a/client/src/api/generated/zod.gen.ts b/client/src/api/generated/zod.gen.ts index db0875bc..85a62297 100644 --- a/client/src/api/generated/zod.gen.ts +++ b/client/src/api/generated/zod.gen.ts @@ -176,7 +176,7 @@ export const zAccessRequestPublic = z.object({ */ export const zSearchRequest = z.object({ query: z.string().min(1).max(255), - limit: z.int() + limit: z.int().optional().default(25) }); /** diff --git a/server/app/api/endpoints/search.py b/server/app/api/endpoints/search.py index 2bf7d3de..070acbfc 100644 --- a/server/app/api/endpoints/search.py +++ b/server/app/api/endpoints/search.py @@ -19,7 +19,7 @@ from app.models.schemas.user import UserPublic from app.services.search import ( get_task, - reindex_data, + reindex, search_exercises, search_muscle_groups, ) @@ -61,7 +61,7 @@ async def reindex_endpoint( db_session: Annotated[AsyncSession, Depends(get_db_session)], ms_client: Annotated[AsyncClient, Depends(get_ms_client)], ): - await reindex_data( + await reindex( db_session=db_session, ms_client=ms_client, ) diff --git a/server/app/models/schemas/search.py b/server/app/models/schemas/search.py index 10964d9d..239410f4 100644 --- a/server/app/models/schemas/search.py +++ b/server/app/models/schemas/search.py @@ -6,7 +6,7 @@ class SearchRequest(BaseModel): query: SearchQuery - limit: int + limit: int = 25 class SearchResponse[T: BaseModel](BaseModel): diff --git a/server/app/services/search.py b/server/app/services/search.py index eae21e6e..d6dc2f11 100644 --- a/server/app/services/search.py +++ b/server/app/services/search.py @@ -28,7 +28,7 @@ async def get_task( return await ms_client.get_task(task_id) -async def reindex_data( +async def reindex( db_session: AsyncSession, ms_client: AsyncClient, ): diff --git a/server/app/tests/api/access_request/test_get_access_requests.py b/server/app/tests/api/access_request/test_get_access_requests.py index e28ee963..1adde507 100644 --- a/server/app/tests/api/access_request/test_get_access_requests.py +++ b/server/app/tests/api/access_request/test_get_access_requests.py @@ -41,7 +41,7 @@ async def test_get_access_requests_not_logged_in(client: AsyncClient): # 403 -async def test_get_access_requests_non_admin_user( +async def test_get_access_requests_not_admin( client: AsyncClient, db_session: AsyncSession, settings: Settings ): await db_session.execute( diff --git a/server/app/tests/api/access_request/test_update_access_request_status.py b/server/app/tests/api/access_request/test_update_access_request_status.py index 5784eae9..5debc3b2 100644 --- a/server/app/tests/api/access_request/test_update_access_request_status.py +++ b/server/app/tests/api/access_request/test_update_access_request_status.py @@ -91,7 +91,7 @@ async def test_update_access_request_status_not_logged_in(client: AsyncClient): # 403 -async def test_update_access_request_status_non_admin_user( +async def test_update_access_request_status_not_admin( client: AsyncClient, db_session: AsyncSession, settings: Settings ): await db_session.execute( diff --git a/server/app/tests/api/search/__init__.py b/server/app/tests/api/search/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/app/tests/api/search/test_get_task.py b/server/app/tests/api/search/test_get_task.py new file mode 100644 index 00000000..df3d74aa --- /dev/null +++ b/server/app/tests/api/search/test_get_task.py @@ -0,0 +1,68 @@ +from fastapi import status +from httpx import AsyncClient +from sqlalchemy import update +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.config import Settings +from app.models.database.user import User +from app.models.errors import InsufficientPermissions + +from ..utilities import HttpMethod, login_admin, make_http_request +from .utilities import reindex_via_api + + +async def _make_request( + client: AsyncClient, + task_id: int, +): + return await make_http_request( + client, + method=HttpMethod.GET, + endpoint=f"/api/search/tasks/{task_id}", + ) + + +# 200 +async def test_get_search_task( + client: AsyncClient, + settings: Settings, +): + await login_admin(client, settings) + + await reindex_via_api(client) + resp = await _make_request(client, task_id=0) + + assert resp.status_code == status.HTTP_200_OK + body = resp.json() + assert body["uid"] == 0 + assert "status" in body + + +# 401 +async def test_get_search_task_not_logged_in(client: AsyncClient): + resp = await _make_request(client, task_id=0) + + assert resp.status_code == status.HTTP_401_UNAUTHORIZED + body = resp.json() + assert body["detail"] == "Not authenticated" + + +# 403 +async def test_get_search_task_not_admin( + client: AsyncClient, + db_session: AsyncSession, + settings: Settings, +): + await db_session.execute( + update(User) + .where(User.username == settings.admin.username) + .values(is_admin=False) + ) + await db_session.commit() + + await login_admin(client, settings) + resp = await _make_request(client, task_id=0) + + assert resp.status_code == InsufficientPermissions.status_code + body = resp.json() + assert body["detail"] == InsufficientPermissions.detail diff --git a/server/app/tests/api/search/test_reindex.py b/server/app/tests/api/search/test_reindex.py new file mode 100644 index 00000000..ac9d3b5a --- /dev/null +++ b/server/app/tests/api/search/test_reindex.py @@ -0,0 +1,61 @@ +from fastapi import status +from httpx import AsyncClient +from sqlalchemy import update +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.config import Settings +from app.models.database.user import User +from app.models.errors import InsufficientPermissions + +from ..utilities import HttpMethod, login_admin, make_http_request + + +async def _make_request( + client: AsyncClient, +): + return await make_http_request( + client, + method=HttpMethod.POST, + endpoint="/api/search/reindex", + ) + + +# 200 +async def test_reindex( + client: AsyncClient, + settings: Settings, +): + await login_admin(client, settings) + resp = await _make_request(client) + + assert resp.status_code == status.HTTP_204_NO_CONTENT + + +# 401 +async def test_reindex_not_logged_in(client: AsyncClient): + resp = await _make_request(client) + + assert resp.status_code == status.HTTP_401_UNAUTHORIZED + body = resp.json() + assert body["detail"] == "Not authenticated" + + +# 403 +async def test_reindex_not_admin( + client: AsyncClient, + db_session: AsyncSession, + settings: Settings, +): + await db_session.execute( + update(User) + .where(User.username == settings.admin.username) + .values(is_admin=False) + ) + await db_session.commit() + + await login_admin(client, settings) + resp = await _make_request(client) + + assert resp.status_code == InsufficientPermissions.status_code + body = resp.json() + assert body["detail"] == InsufficientPermissions.detail diff --git a/server/app/tests/api/search/test_search_exercises.py b/server/app/tests/api/search/test_search_exercises.py new file mode 100644 index 00000000..a791315a --- /dev/null +++ b/server/app/tests/api/search/test_search_exercises.py @@ -0,0 +1,27 @@ +from fastapi import status +from httpx import AsyncClient + +from ..utilities import HttpMethod, make_http_request + + +async def _make_request( + client: AsyncClient, +): + return await make_http_request( + client, + method=HttpMethod.POST, + endpoint="/api/search/exercises", + ) + + +# 200 +# TODO + + +# 401 +async def test_search_exercises_not_logged_in(client: AsyncClient): + resp = await _make_request(client) + + assert resp.status_code == status.HTTP_401_UNAUTHORIZED + body = resp.json() + assert body["detail"] == "Not authenticated" diff --git a/server/app/tests/api/search/test_search_muscle_groups.py b/server/app/tests/api/search/test_search_muscle_groups.py new file mode 100644 index 00000000..28760cdc --- /dev/null +++ b/server/app/tests/api/search/test_search_muscle_groups.py @@ -0,0 +1,49 @@ +from fastapi import status +from httpx import AsyncClient +from meilisearch_python_sdk.models.search import SearchResults + +from app.core.config import Settings + +from ..utilities import HttpMethod, login_admin, make_http_request + + +async def _make_request( + client: AsyncClient, + query: str, + limit: int, +): + return await make_http_request( + client, + method=HttpMethod.POST, + endpoint="/api/search/muscle-groups", + json={ + "query": query, + "limit": limit, + }, + ) + + +# 200 +async def test_search_muscle_groups( + client: AsyncClient, + settings: Settings, +): + await login_admin(client, settings) + + resp = await _make_request(client, query="chest", limit=5) + + assert resp.status_code == status.HTTP_200_OK + body = resp.json() + results = SearchResults.model_validate(body) # pyright: ignore[reportUnknownVariableType] + + assert results.query == "chest" + assert results.limit == 5 + + +# 401 +async def test_search_muscle_groups_not_logged_in(client: AsyncClient): + resp = await _make_request(client, "", 0) + + assert resp.status_code == status.HTTP_401_UNAUTHORIZED + body = resp.json() + assert body["detail"] == "Not authenticated" diff --git a/server/app/tests/api/search/utilities.py b/server/app/tests/api/search/utilities.py new file mode 100644 index 00000000..4a079d3b --- /dev/null +++ b/server/app/tests/api/search/utilities.py @@ -0,0 +1,13 @@ +from httpx import AsyncClient + +from ..utilities import HttpMethod, make_http_request + + +async def reindex_via_api( + client: AsyncClient, +) -> None: + await make_http_request( + client, + method=HttpMethod.POST, + endpoint="/api/search/reindex", + ) diff --git a/server/app/tests/api/user/test_get_users.py b/server/app/tests/api/user/test_get_users.py index 71050a9e..6f9cba68 100644 --- a/server/app/tests/api/user/test_get_users.py +++ b/server/app/tests/api/user/test_get_users.py @@ -44,7 +44,7 @@ async def test_get_users_not_logged_in(client: AsyncClient): # 403 -async def test_get_users_non_admin_user( +async def test_get_users_not_admin( client: AsyncClient, db_session: AsyncSession, settings: Settings ): await db_session.execute( diff --git a/server/app/tests/services/search/test_reindex_data.py b/server/app/tests/services/search/test_reindex.py similarity index 85% rename from server/app/tests/services/search/test_reindex_data.py rename to server/app/tests/services/search/test_reindex.py index 0df171c3..e8d5b555 100644 --- a/server/app/tests/services/search/test_reindex_data.py +++ b/server/app/tests/services/search/test_reindex.py @@ -2,10 +2,10 @@ from pytest import MonkeyPatch from sqlalchemy.ext.asyncio import AsyncSession -from app.services.search import reindex_data +from app.services.search import reindex -async def test_reindex_data( +async def test_reindex( db_session: AsyncSession, ms_client: AsyncClient, monkeypatch: MonkeyPatch, @@ -21,6 +21,6 @@ async def fake_exercises(db_session: AsyncSession, client: AsyncClient) -> None: monkeypatch.setattr("app.services.search._index_muscle_groups", fake_muscle_groups) monkeypatch.setattr("app.services.search._index_exercises", fake_exercises) - await reindex_data(db_session, ms_client) + await reindex(db_session, ms_client) assert calls == ["muscle", "exercise"]