From 8831f9fc8c2e3abb98f4d0e968e8cd5de09a9298 Mon Sep 17 00:00:00 2001 From: Baptiste-Ferrand Date: Sat, 9 Aug 2025 20:09:18 +0200 Subject: [PATCH 1/6] add async lib and make app async --- pyproject.toml | 2 +- src/adapters/inmemory/repositories/diet.py | 38 +-- .../inmemory/repositories/exercise.py | 12 +- src/adapters/inmemory/repositories/group.py | 29 +- src/adapters/inmemory/repositories/profile.py | 16 +- .../inmemory/repositories/training.py | 34 +-- src/adapters/sqlalchemy/db.py | 27 +- src/adapters/sqlalchemy/repositories/diet.py | 252 +++++++++--------- .../sqlalchemy/repositories/exercise.py | 71 ++--- src/adapters/sqlalchemy/repositories/group.py | 143 +++++----- .../sqlalchemy/repositories/profile.py | 140 +++++----- .../sqlalchemy/repositories/training.py | 187 +++++++------ src/container.py | 112 ++++++-- src/domain/ports/diet_repository.py | 34 +-- src/domain/ports/exercise_repository.py | 12 +- src/domain/ports/group_repository.py | 20 +- src/domain/ports/profile_repository.py | 14 +- src/domain/ports/training_repository.py | 31 ++- src/domain/services/diet.py | 80 +++--- src/domain/services/exercise.py | 23 +- src/domain/services/group.py | 50 ++-- src/domain/services/profile.py | 58 ++-- src/domain/services/training.py | 79 +++--- src/entrypoints/api/deps/auth.py | 75 +++--- src/entrypoints/api/deps/roles.py | 2 +- src/entrypoints/api/routers/diet.py | 69 ++--- src/entrypoints/api/routers/exercise.py | 26 +- src/entrypoints/api/routers/group.py | 48 ++-- src/entrypoints/api/routers/profile.py | 22 +- src/entrypoints/api/routers/training.py | 60 ++--- src/entrypoints/api/tests/conftest.py | 67 +++-- src/main.py | 6 + uv.lock | 96 ++++++- 33 files changed, 1105 insertions(+), 830 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dc8b574..93f1f60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "tracknatrainapi" version = "0.5.7" requires-python = ">=3.12" -dependencies = [ "annotated-types==0.7.0", "anyio==4.9.0", "bcrypt==4.3.0", "boto3==1.37.37", "botocore==1.37.37", "cffi==1.17.1", "click==8.1.8", "cryptography==44.0.2", "dnspython==2.7.0", "ecdsa==0.19.1", "email-validator==2.2.0", "exceptiongroup==1.2.2", "fastapi==0.115.12", "greenlet==3.1.1", "h11==0.14.0", "idna==3.10", "jmespath==1.0.1", "passlib[bcrypt]>=1.7.4", "psycopg2-binary==2.9.10", "pyasn1==0.4.8", "pycparser==2.22", "pydantic==2.11.3", "pydantic-core==2.33.1", "python-dateutil==2.9.0.post0", "python-dotenv==1.1.0", "python-jose==3.4.0", "python-multipart==0.0.20", "rsa==4.9", "s3transfer==0.11.5", "six==1.17.0", "sniffio==1.3.1", "sqlalchemy==2.0.40", "starlette==0.46.2", "typing-extensions==4.13.2", "typing-inspection==0.4.0", "urllib3==2.4.0", "uvicorn==0.34.1", "pytest>=7.0", "pytest-asyncio>=0.20", "httpx>=0.24", "pytest-cov>=4.0", "coverage>=6.0",] +dependencies = [ "annotated-types==0.7.0", "anyio==4.9.0", "bcrypt==4.3.0", "boto3==1.37.37", "botocore==1.37.37", "cffi==1.17.1", "click==8.1.8", "cryptography==44.0.2", "dnspython==2.7.0", "ecdsa==0.19.1", "email-validator==2.2.0", "exceptiongroup==1.2.2", "fastapi==0.115.12", "greenlet==3.1.1", "h11==0.14.0", "idna==3.10", "jmespath==1.0.1", "passlib[bcrypt]>=1.7.4", "psycopg2-binary==2.9.10", "pyasn1==0.4.8", "pycparser==2.22", "pydantic==2.11.3", "pydantic-core==2.33.1", "python-dateutil==2.9.0.post0", "python-dotenv==1.1.0", "python-jose==3.4.0", "python-multipart==0.0.20", "rsa==4.9", "s3transfer==0.11.5", "six==1.17.0", "sniffio==1.3.1", "sqlalchemy==2.0.40", "starlette==0.46.2", "typing-extensions==4.13.2", "typing-inspection==0.4.0", "urllib3==2.4.0", "uvicorn==0.34.1", "pytest>=7.0", "pytest-asyncio>=0.20", "httpx>=0.24", "pytest-cov>=4.0", "coverage>=6.0", "asyncpg==0.30.0"] [build-system] requires = [ "setuptools>=42", "wheel",] diff --git a/src/adapters/inmemory/repositories/diet.py b/src/adapters/inmemory/repositories/diet.py index 4038090..6691bd2 100644 --- a/src/adapters/inmemory/repositories/diet.py +++ b/src/adapters/inmemory/repositories/diet.py @@ -13,7 +13,7 @@ def __init__(self): self._meal_plans: dict[UUID, DomainMealPlan] = {} # Diet methods - def add_diet(self, diet: DomainDiet) -> DomainDiet: + async def add_diet(self, diet: DomainDiet) -> DomainDiet: new_id = uuid4() diet.id = new_id if not getattr(diet, 'created_at', None): @@ -21,73 +21,73 @@ def add_diet(self, diet: DomainDiet) -> DomainDiet: self._diets[new_id] = diet return diet - def find_by_id(self, id: UUID) -> Optional[DomainDiet]: + async def find_by_id(self, id: UUID) -> Optional[DomainDiet]: return self._diets.get(id) - def find_all_owner_diets(self, owner_id: UUID) -> List[DomainDiet]: + async def find_all_owner_diets(self, owner_id: UUID) -> List[DomainDiet]: return [d for d in self._diets.values() if d.owner_id == owner_id] - def update_diet(self, diet: DomainDiet) -> DomainDiet: + async def update_diet(self, diet: DomainDiet) -> DomainDiet: if diet.id not in self._diets: raise NotFoundError(f"Diet {diet.id} not found") self._diets[diet.id] = diet return diet - def delete_diet(self, id: UUID) -> None: + async def delete_diet(self, id: UUID) -> None: self._diets.pop(id, None) for mid in list(self._macro_plans.keys()): if self._macro_plans[mid].diet_id == id: - self.delete_macro_plan(mid) + await self.delete_macro_plan(mid) for pid in list(self._meal_plans.keys()): if self._meal_plans[pid].diet_id == id: - self.delete_meal_plan(pid) + await self.delete_meal_plan(pid) # MacroPlan methods - def add_macro_plan(self, macro_plan: DomainMacroPlan) -> DomainMacroPlan: + async def add_macro_plan(self, macro_plan: DomainMacroPlan) -> DomainMacroPlan: new_id = uuid4() macro_plan.id = new_id self._macro_plans[new_id] = macro_plan return macro_plan - def find_macro_plan_by_id(self, id: UUID) -> Optional[DomainMacroPlan]: + async def find_macro_plan_by_id(self, id: UUID) -> Optional[DomainMacroPlan]: return self._macro_plans.get(id) - def find_macro_plans_by_diet_id(self, diet_id: UUID) -> List[DomainMacroPlan]: + async def find_macro_plans_by_diet_id(self, diet_id: UUID) -> List[DomainMacroPlan]: return [mp for mp in self._macro_plans.values() if mp.diet_id == diet_id] - def find_macro_plans_by_user_id(self, user_id: UUID) -> List[DomainMacroPlan]: + async def find_macro_plans_by_user_id(self, user_id: UUID) -> List[DomainMacroPlan]: return [mp for mp in self._macro_plans.values() if self._diets.get(mp.diet_id) and self._diets[mp.diet_id].owner_id == user_id] - def update_macro_plan(self, macro_plan: DomainMacroPlan) -> DomainMacroPlan: + async def update_macro_plan(self, macro_plan: DomainMacroPlan) -> DomainMacroPlan: if macro_plan.id not in self._macro_plans: raise NotFoundError(f"MacroPlan {macro_plan.id} not found") self._macro_plans[macro_plan.id] = macro_plan return macro_plan - def delete_macro_plan(self, id: UUID) -> None: + async def delete_macro_plan(self, id: UUID) -> None: self._macro_plans.pop(id, None) # MealPlan methods - def add_meal_plan(self, meal_plan: DomainMealPlan) -> DomainMealPlan: + async def add_meal_plan(self, meal_plan: DomainMealPlan) -> DomainMealPlan: new_id = uuid4() meal_plan.id = new_id self._meal_plans[new_id] = meal_plan return meal_plan - def find_meal_plan_by_id(self, id: UUID) -> Optional[DomainMealPlan]: + async def find_meal_plan_by_id(self, id: UUID) -> Optional[DomainMealPlan]: return self._meal_plans.get(id) - def find_meal_plans_by_diet_id(self, diet_id: UUID) -> List[DomainMealPlan]: + async def find_meal_plans_by_diet_id(self, diet_id: UUID) -> List[DomainMealPlan]: return [mp for mp in self._meal_plans.values() if mp.diet_id == diet_id] - def find_meal_plans_by_user_id(self, user_id: UUID) -> List[DomainMealPlan]: + async def find_meal_plans_by_user_id(self, user_id: UUID) -> List[DomainMealPlan]: return [mp for mp in self._meal_plans.values() if self._diets.get(mp.diet_id) and self._diets[mp.diet_id].owner_id == user_id] - def update_meal_plan(self, meal_plan: DomainMealPlan) -> DomainMealPlan: + async def update_meal_plan(self, meal_plan: DomainMealPlan) -> DomainMealPlan: if meal_plan.id not in self._meal_plans: raise NotFoundError(f"MealPlan {meal_plan.id} not found") self._meal_plans[meal_plan.id] = meal_plan return meal_plan - def delete_meal_plan(self, id: UUID) -> None: + async def delete_meal_plan(self, id: UUID) -> None: self._meal_plans.pop(id, None) diff --git a/src/adapters/inmemory/repositories/exercise.py b/src/adapters/inmemory/repositories/exercise.py index b75de38..74b7b85 100644 --- a/src/adapters/inmemory/repositories/exercise.py +++ b/src/adapters/inmemory/repositories/exercise.py @@ -10,7 +10,7 @@ class InMemoryExerciseRepository(ExerciseRepository): def __init__(self): self._exercises: dict[UUID, DomainExercise] = {} - def add(self, exercise: DomainExercise) -> DomainExercise: + async def add(self, exercise: DomainExercise) -> DomainExercise: new_id = uuid4() exercise.id = new_id if not getattr(exercise, 'created_at', None): @@ -18,20 +18,20 @@ def add(self, exercise: DomainExercise) -> DomainExercise: self._exercises[new_id] = exercise return exercise - def delete(self, id: UUID) -> None: + async def delete(self, id: UUID) -> None: self._exercises.pop(id, None) - def update(self, exercise: DomainExercise) -> Optional[DomainExercise]: + async def update(self, exercise: DomainExercise) -> Optional[DomainExercise]: if exercise.id not in self._exercises: raise NotFoundError(f"Exercise {exercise.id} not found") self._exercises[exercise.id] = exercise return exercise - def find_all_owner(self, owner_id: UUID) -> List[DomainExercise]: + async def find_all_owner(self, owner_id: UUID) -> List[DomainExercise]: return [ex for ex in self._exercises.values() if ex.owner_id == owner_id] - def find_all(self) -> List[DomainExercise]: + async def find_all(self) -> List[DomainExercise]: return list(self._exercises.values()) - def find_by_id(self, id: UUID) -> Optional[DomainExercise]: + async def find_by_id(self, id: UUID) -> Optional[DomainExercise]: return self._exercises.get(id) diff --git a/src/adapters/inmemory/repositories/group.py b/src/adapters/inmemory/repositories/group.py index b69c7c8..2ef6b61 100644 --- a/src/adapters/inmemory/repositories/group.py +++ b/src/adapters/inmemory/repositories/group.py @@ -14,10 +14,10 @@ def __init__(self, profile_repo: InMemoryProfileRepository): self._members: dict[UUID, List[UUID]] = {} self._profile_repo = profile_repo - def find_by_id(self, id: UUID) -> Optional[DomainGroup]: + async def find_by_id(self, id: UUID) -> Optional[DomainGroup]: return self._groups.get(id) - def add(self, group: DomainGroup) -> Optional[DomainGroup]: + async def add(self, group: DomainGroup) -> Optional[DomainGroup]: new_id = uuid4() group.id = new_id if not getattr(group, 'created_at', None): @@ -26,27 +26,27 @@ def add(self, group: DomainGroup) -> Optional[DomainGroup]: self._members[new_id] = [] return group - def delete(self, id: UUID) -> None: + async def delete(self, id: UUID) -> None: self._groups.pop(id, None) self._members.pop(id, None) - def update(self, group: DomainGroup) -> Optional[DomainGroup]: + async def update(self, group: DomainGroup) -> Optional[DomainGroup]: if group.id not in self._groups: raise NotFoundError(f"Groupe {group.id} not found") self._groups[group.id] = group return group - def add_member(self, group_id: UUID, user_id: UUID) -> None: + async def add_member(self, group_id: UUID, user_id: UUID) -> None: if group_id not in self._groups: raise NotFoundError(f"Groupe {group_id} not found") - user = self._profile_repo.find_by_id(user_id) + user = await self._profile_repo.find_by_id(user_id) if not user: raise NotFoundError(f"Profile {user_id} not found") members = self._members.setdefault(group_id, []) if user_id not in members: members.append(user_id) - def remove_member(self, group_id: UUID, user_id: UUID) -> None: + async def remove_member(self, group_id: UUID, user_id: UUID) -> None: if group_id not in self._groups: raise NotFoundError(f"Groupe {group_id} not found") members = self._members.get(group_id, []) @@ -54,19 +54,24 @@ def remove_member(self, group_id: UUID, user_id: UUID) -> None: raise NotFoundError(f"Profile {user_id} not found in group {group_id}") members.remove(user_id) - def list_members(self, group_id: UUID) -> List[DomainProfile]: + async def list_members(self, group_id: UUID) -> List[DomainProfile]: if group_id not in self._groups: raise NotFoundError(f"Groupe {group_id} introuvable") user_ids = self._members.get(group_id, []) - return [self._profile_repo.find_by_id(uid) for uid in user_ids if self._profile_repo.find_by_id(uid)] + members = [] + for uid in user_ids: + member = await self._profile_repo.find_by_id(uid) + if member: + members.append(member) + return members - def find_by_owner_id(self, owner_id: UUID) -> Optional[List[DomainGroup]]: + async def find_by_owner_id(self, owner_id: UUID) -> Optional[List[DomainGroup]]: return [g for g in self._groups.values() if g.owner_id == owner_id] - def find_all_groups(self) -> Optional[List[DomainGroup]]: + async def find_all_groups(self) -> Optional[List[DomainGroup]]: return list(self._groups.values()) - def find_groups_by_member_id(self, user_id: UUID) -> Optional[List[DomainGroup]]: + async def find_groups_by_member_id(self, user_id: UUID) -> Optional[List[DomainGroup]]: """Find all groups where the user is a member""" groups = [] for group_id, member_ids in self._members.items(): diff --git a/src/adapters/inmemory/repositories/profile.py b/src/adapters/inmemory/repositories/profile.py index 2dcf590..614927e 100644 --- a/src/adapters/inmemory/repositories/profile.py +++ b/src/adapters/inmemory/repositories/profile.py @@ -13,13 +13,13 @@ def __init__(self, initial: list[DomainProfile] | None = None): for profile in initial: self._data[profile.id] = profile - def find_by_email(self, email: str) -> Optional[DomainProfile]: + async def find_by_email(self, email: str) -> Optional[DomainProfile]: for profile in self._data.values(): if profile.email == email: return profile return None - def add(self, profile: DomainProfile) -> DomainProfile: + async def add(self, profile: DomainProfile) -> DomainProfile: new_id = uuid4() profile.id = new_id if not getattr(profile, "created_at", None): @@ -27,23 +27,23 @@ def add(self, profile: DomainProfile) -> DomainProfile: self._data[new_id] = profile return profile - def find_by_id(self, id: UUID) -> Optional[DomainProfile]: + async def find_by_id(self, id: UUID) -> Optional[DomainProfile]: return self._data.get(id) - def delete(self, id: UUID) -> None: + async def delete(self, id: UUID) -> None: self._data.pop(id, None) - def update(self, profile: DomainProfile) -> Optional[DomainProfile]: + async def update(self, profile: DomainProfile) -> Optional[DomainProfile]: if profile.id in self._data: self._data[profile.id] = profile return profile return None - def find_all_users(self) -> List[DomainProfile]: + async def find_all_users(self) -> List[DomainProfile]: return [p for p in self._data.values() if "user" in (p.roles or [])] - def find_all_coachs(self) -> List[DomainProfile]: + async def find_all_coachs(self) -> List[DomainProfile]: return [p for p in self._data.values() if "coach" in (p.roles or [])] - def find_all(self) -> List[DomainProfile]: + async def find_all(self) -> List[DomainProfile]: return list(self._data.values()) \ No newline at end of file diff --git a/src/adapters/inmemory/repositories/training.py b/src/adapters/inmemory/repositories/training.py index a63d0a0..8157939 100644 --- a/src/adapters/inmemory/repositories/training.py +++ b/src/adapters/inmemory/repositories/training.py @@ -13,10 +13,10 @@ def __init__(self): self._validates: dict[UUID, DomainValidate] = {} # Training methods - def find_by_id(self, id: UUID) -> Optional[DomainTraining]: + async def find_by_id(self, id: UUID) -> Optional[DomainTraining]: return self._trainings.get(id) - def add_training(self, training: DomainTraining) -> DomainTraining: + async def add_training(self, training: DomainTraining) -> DomainTraining: new_id = uuid4() training.id = new_id if not getattr(training, 'created_at', None): @@ -24,23 +24,23 @@ def add_training(self, training: DomainTraining) -> DomainTraining: self._trainings[new_id] = training return training - def delete_training(self, id: UUID) -> None: + async def delete_training(self, id: UUID) -> None: self._trainings.pop(id, None) for task_id, task in list(self._tasks.items()): if task.training_id == id: - self.delete_task(task_id) + await self.delete_task(task_id) - def update_training(self, training: DomainTraining) -> Optional[DomainTraining]: + async def update_training(self, training: DomainTraining) -> Optional[DomainTraining]: if training.id not in self._trainings: raise NotFoundError(f"Training {training.id} not found") self._trainings[training.id] = training return training - def find_all_owner_trainings(self, owner_id: UUID) -> List[DomainTraining]: + async def find_all_owner_trainings(self, owner_id: UUID) -> List[DomainTraining]: return [t for t in self._trainings.values() if t.owner_id == owner_id] # Task methods - def add_task(self, task: DomainTask) -> DomainTask: + async def add_task(self, task: DomainTask) -> DomainTask: new_id = uuid4() task.id = new_id if not getattr(task, 'updated_at', None): @@ -48,27 +48,27 @@ def add_task(self, task: DomainTask) -> DomainTask: self._tasks[new_id] = task return task - def find_task_by_id(self, id: UUID) -> Optional[DomainTask]: + async def find_task_by_id(self, id: UUID) -> Optional[DomainTask]: return self._tasks.get(id) - def delete_task(self, id: UUID) -> None: + async def delete_task(self, id: UUID) -> None: task = self._tasks.pop(id, None) if task: for vid, validate in list(self._validates.items()): if validate.task_id == id: - self.delete_validate(vid) + await self.delete_validate(vid) - def update_task(self, task: DomainTask) -> Optional[DomainTask]: + async def update_task(self, task: DomainTask) -> Optional[DomainTask]: if task.id not in self._tasks: raise NotFoundError(f"Task {task.id} not found") self._tasks[task.id] = task return task - def find_tasks_by_training_id(self, training_id: UUID) -> List[DomainTask]: + async def find_tasks_by_training_id(self, training_id: UUID) -> List[DomainTask]: return [t for t in self._tasks.values() if t.training_id == training_id] # Validate methods - def add_validate(self, validate: DomainValidate) -> DomainValidate: + async def add_validate(self, validate: DomainValidate) -> DomainValidate: new_id = uuid4() validate.id = new_id if not getattr(validate, 'succeeded_at', None): @@ -76,15 +76,15 @@ def add_validate(self, validate: DomainValidate) -> DomainValidate: self._validates[new_id] = validate return validate - def find_validate_by_id(self, id: UUID) -> Optional[DomainValidate]: + async def find_validate_by_id(self, id: UUID) -> Optional[DomainValidate]: return self._validates.get(id) - def find_validate_by_task_id(self, task_id: UUID) -> List[DomainValidate]: + async def find_validate_by_task_id(self, task_id: UUID) -> List[DomainValidate]: return [v for v in self._validates.values() if v.task_id == task_id] - def find_all_validates_by_training_id(self, training_id: UUID) -> List[DomainValidate]: + async def find_all_validates_by_training_id(self, training_id: UUID) -> List[DomainValidate]: task_ids = {t.id for t in self._tasks.values() if t.training_id == training_id} return [v for v in self._validates.values() if v.task_id in task_ids] - def delete_validate(self, id: UUID) -> None: + async def delete_validate(self, id: UUID) -> None: self._validates.pop(id, None) diff --git a/src/adapters/sqlalchemy/db.py b/src/adapters/sqlalchemy/db.py index e85c748..f8a7577 100644 --- a/src/adapters/sqlalchemy/db.py +++ b/src/adapters/sqlalchemy/db.py @@ -1,22 +1,39 @@ # app/adapters/sqlalchemy/repositories/postgres.py from src.adapters.sqlalchemy.models import Base -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from dotenv import load_dotenv import os load_dotenv() db_url = os.getenv("DATABASE_URL") or "postgresql://user:user@localhost:5432/postgres" -engine = create_engine( +# ✅ Convertir l'URL pour asyncpg +if db_url.startswith("postgresql://") and "asyncpg" not in db_url: + db_url = db_url.replace("postgresql://", "postgresql+asyncpg://", 1) + +# ✅ Moteur asynchrone +engine = create_async_engine( db_url, pool_size=20, max_overflow=20, pool_timeout=30, + echo=False, # Mettre True pour debug ) -Base.metadata.create_all(bind=engine) +# ✅ Factory de sessions asynchrones +SessionLocal = async_sessionmaker( + bind=engine, + class_=AsyncSession, + expire_on_commit=False +) +# ✅ Fonction pour créer les tables (asynchrone) +async def init_db(): + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +# ✅ Fonction pour obtenir une session +async def get_session(): + async with SessionLocal() as session: + yield session diff --git a/src/adapters/sqlalchemy/repositories/diet.py b/src/adapters/sqlalchemy/repositories/diet.py index 1c36e3d..11231db 100644 --- a/src/adapters/sqlalchemy/repositories/diet.py +++ b/src/adapters/sqlalchemy/repositories/diet.py @@ -1,5 +1,6 @@ from typing import List, Optional -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select from src.domain.exceptions import NotFoundError from uuid import UUID @@ -7,8 +8,6 @@ from src.domain.model.diet import Diet as DomainDiet, MealPlan as DomainMealPlan, MacroPlan as DomainMacroPlan, MealItem as DomainMealItem from src.domain.ports.diet_repository import DietRepository - - def diet_from_orm(orm: ORMDiet) -> DomainDiet: return DomainDiet( id=orm.id, @@ -43,139 +42,142 @@ def meal_plan_from_orm(orm: ORMMealPlan) -> DomainMealPlan: ) class SqlAlchemyDietRepository(DietRepository): - def __init__(self, session: Session): - self._session = session + def __init__(self, session_factory): + self._session_factory = session_factory - def add_diet(self, diet: DomainDiet) -> DomainDiet: + async def add_diet(self, diet: DomainDiet) -> DomainDiet: data = diet.to_orm_dict() orm = ORMDiet(**data) - self._session.add(orm) - self._session.commit() - self._session.refresh(orm) - return diet_from_orm(orm) - - def find_by_id(self, id: UUID) -> Optional[DomainDiet]: - orm = self._session.get(ORMDiet, id) - return diet_from_orm(orm) if orm else None - - def find_all_owner_diets(self, owner_id: UUID) -> List[DomainDiet]: - orms = ( - self._session - .query(ORMDiet) - .filter(ORMDiet.owner_id == owner_id) - .all() - ) - return [diet_from_orm(o) for o in orms] - - def update_diet(self, diet: DomainDiet) -> DomainDiet: - orm = self._session.get(ORMDiet, diet.id) - if not orm: - raise NotFoundError(f"Diet {diet.id} not found") - for k, v in diet.to_orm_dict().items(): - setattr(orm, k, v) - self._session.commit() - return diet_from_orm(orm) - - def delete_diet(self, id: UUID) -> None: - orm = self._session.get(ORMDiet, id) - if not orm: - return - self._session.delete(orm) - self._session.commit() + async with self._session_factory() as session: + session.add(orm) + await session.commit() + await session.refresh(orm) + return diet_from_orm(orm) + + async def find_by_id(self, id: UUID) -> Optional[DomainDiet]: + async with self._session_factory() as session: + orm = await session.get(ORMDiet, id) + return diet_from_orm(orm) if orm else None + + async def find_all_owner_diets(self, owner_id: UUID) -> List[DomainDiet]: + async with self._session_factory() as session: + result = await session.execute(select(ORMDiet).filter(ORMDiet.owner_id == owner_id)) + orms = result.scalars().all() + return [diet_from_orm(o) for o in orms] + + async def update_diet(self, diet: DomainDiet) -> DomainDiet: + async with self._session_factory() as session: + orm = await session.get(ORMDiet, diet.id) + if not orm: + raise NotFoundError(f"Diet {diet.id} not found") + for k, v in diet.to_orm_dict().items(): + setattr(orm, k, v) + await session.commit() + return diet_from_orm(orm) + + async def delete_diet(self, id: UUID) -> None: + async with self._session_factory() as session: + orm = await session.get(ORMDiet, id) + if not orm: + return + await session.delete(orm) + await session.commit() # Macro Plan methods - def add_macro_plan(self, macro_plan: DomainMacroPlan) -> DomainMacroPlan: + async def add_macro_plan(self, macro_plan: DomainMacroPlan) -> DomainMacroPlan: data = macro_plan.to_orm_dict() orm = ORMMacroPlan(**data) - self._session.add(orm) - self._session.commit() - self._session.refresh(orm) - return macro_plan_from_orm(orm) - - def find_macro_plan_by_id(self, id: UUID) -> Optional[DomainMacroPlan]: - orm = self._session.get(ORMMacroPlan, id) - return macro_plan_from_orm(orm) if orm else None - - def find_macro_plans_by_diet_id(self, diet_id: UUID) -> List[DomainMacroPlan]: - orms = ( - self._session - .query(ORMMacroPlan) - .filter(ORMMacroPlan.diet_id == diet_id) - .all() - ) - return [macro_plan_from_orm(o) for o in orms] + async with self._session_factory() as session: + session.add(orm) + await session.commit() + await session.refresh(orm) + return macro_plan_from_orm(orm) + + async def find_macro_plan_by_id(self, id: UUID) -> Optional[DomainMacroPlan]: + async with self._session_factory() as session: + orm = await session.get(ORMMacroPlan, id) + return macro_plan_from_orm(orm) if orm else None + + async def find_macro_plans_by_diet_id(self, diet_id: UUID) -> List[DomainMacroPlan]: + async with self._session_factory() as session: + result = await session.execute(select(ORMMacroPlan).filter(ORMMacroPlan.diet_id == diet_id)) + orms = result.scalars().all() + return [macro_plan_from_orm(o) for o in orms] - def find_macro_plans_by_user_id(self, user_id: UUID) -> List[DomainMacroPlan]: - orms = ( - self._session - .query(ORMMacroPlan) - .join(ORMDiet, ORMDiet.id == ORMMacroPlan.diet_id) - .filter(ORMDiet.owner_id == user_id) - .all() - ) - return [macro_plan_from_orm(o) for o in orms] - - def update_macro_plan(self, macro_plan: DomainMacroPlan) -> DomainMacroPlan: - orm = self._session.get(ORMMacroPlan, macro_plan.id) - if not orm: - raise NotFoundError(f"MacroPlan {macro_plan.id} not found") - for k, v in macro_plan.to_orm_dict().items(): - setattr(orm, k, v) - self._session.commit() - return macro_plan_from_orm(orm) - - def delete_macro_plan(self, id: UUID) -> None: - orm = self._session.get(ORMMacroPlan, id) - if not orm: - return - self._session.delete(orm) - self._session.commit() + async def find_macro_plans_by_user_id(self, user_id: UUID) -> List[DomainMacroPlan]: + async with self._session_factory() as session: + result = await session.execute( + select(ORMMacroPlan) + .join(ORMDiet, ORMDiet.id == ORMMacroPlan.diet_id) + .filter(ORMDiet.owner_id == user_id) + ) + orms = result.scalars().all() + return [macro_plan_from_orm(o) for o in orms] + + async def update_macro_plan(self, macro_plan: DomainMacroPlan) -> DomainMacroPlan: + async with self._session_factory() as session: + orm = await session.get(ORMMacroPlan, macro_plan.id) + if not orm: + raise NotFoundError(f"MacroPlan {macro_plan.id} not found") + for k, v in macro_plan.to_orm_dict().items(): + setattr(orm, k, v) + await session.commit() + return macro_plan_from_orm(orm) + + async def delete_macro_plan(self, id: UUID) -> None: + async with self._session_factory() as session: + orm = await session.get(ORMMacroPlan, id) + if not orm: + return + await session.delete(orm) + await session.commit() # Meal Plan methods - def add_meal_plan(self, mp: DomainMealPlan) -> DomainMealPlan: + async def add_meal_plan(self, mp: DomainMealPlan) -> DomainMealPlan: data = mp.to_orm_dict() orm = ORMMealPlan(**data) - self._session.add(orm) - self._session.commit() - self._session.refresh(orm) - return meal_plan_from_orm(orm) - - def find_meal_plan_by_id(self, id: UUID) -> Optional[DomainMealPlan]: - orm = self._session.get(ORMMealPlan, id) - return meal_plan_from_orm(orm) if orm else None - - def find_meal_plans_by_diet_id(self, diet_id: UUID) -> List[DomainMealPlan]: - orms = ( - self._session - .query(ORMMealPlan) - .filter(ORMMealPlan.diet_id == diet_id) - .all() - ) - return [meal_plan_from_orm(o) for o in orms] - - def find_meal_plans_by_user_id(self, user_id: UUID) -> List[DomainMealPlan]: - orms = ( - self._session - .query(ORMMealPlan) - .join(ORMMealPlan.diet) - .filter(ORMMealPlan.diet.has(owner_id=user_id)) - .all() - ) - return [meal_plan_from_orm(o) for o in orms] - - def update_meal_plan(self, mp: DomainMealPlan) -> DomainMealPlan: - orm = self._session.get(ORMMealPlan, mp.id) - if not orm: - raise NotFoundError(f"MealPlan {mp.id} not found") - for k, v in mp.to_orm_dict().items(): - setattr(orm, k, v) - self._session.commit() - return meal_plan_from_orm(orm) - - def delete_meal_plan(self, id: UUID) -> None: - orm = self._session.get(ORMMealPlan, id) - if orm: - self._session.delete(orm) - self._session.commit() \ No newline at end of file + async with self._session_factory() as session: + session.add(orm) + await session.commit() + await session.refresh(orm) + return meal_plan_from_orm(orm) + + async def find_meal_plan_by_id(self, id: UUID) -> Optional[DomainMealPlan]: + async with self._session_factory() as session: + orm = await session.get(ORMMealPlan, id) + return meal_plan_from_orm(orm) if orm else None + + async def find_meal_plans_by_diet_id(self, diet_id: UUID) -> List[DomainMealPlan]: + async with self._session_factory() as session: + result = await session.execute(select(ORMMealPlan).filter(ORMMealPlan.diet_id == diet_id)) + orms = result.scalars().all() + return [meal_plan_from_orm(o) for o in orms] + + async def find_meal_plans_by_user_id(self, user_id: UUID) -> List[DomainMealPlan]: + async with self._session_factory() as session: + result = await session.execute( + select(ORMMealPlan) + .join(ORMDiet) + .filter(ORMDiet.owner_id == user_id) + ) + orms = result.scalars().all() + return [meal_plan_from_orm(o) for o in orms] + + async def update_meal_plan(self, mp: DomainMealPlan) -> DomainMealPlan: + async with self._session_factory() as session: + orm = await session.get(ORMMealPlan, mp.id) + if not orm: + raise NotFoundError(f"MealPlan {mp.id} not found") + for k, v in mp.to_orm_dict().items(): + setattr(orm, k, v) + await session.commit() + return meal_plan_from_orm(orm) + + async def delete_meal_plan(self, id: UUID) -> None: + async with self._session_factory() as session: + orm = await session.get(ORMMealPlan, id) + if orm: + await session.delete(orm) + await session.commit() \ No newline at end of file diff --git a/src/adapters/sqlalchemy/repositories/exercise.py b/src/adapters/sqlalchemy/repositories/exercise.py index 4e04c08..5313ab7 100644 --- a/src/adapters/sqlalchemy/repositories/exercise.py +++ b/src/adapters/sqlalchemy/repositories/exercise.py @@ -1,5 +1,6 @@ from typing import List, Optional -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select from src.domain.exceptions import NotFoundError from uuid import UUID @@ -19,42 +20,50 @@ def exercise_from_orm(orm_exercise) -> DomainExercise: ) class SqlAlchemyExerciseRepository(ExerciseRepository): - def __init__(self, session: Session): - self._session = session + def __init__(self, session_factory): + self._session_factory = session_factory # CRUD operations for Exercise - def add(self, exercise: DomainExercise) -> Optional[DomainExercise]: + async def add(self, exercise: DomainExercise) -> Optional[DomainExercise]: data = exercise.to_orm_dict() orm = ORMExercise(**data) - self._session.add(orm) - self._session.commit() - self._session.refresh(orm) - return exercise_from_orm(orm) if orm else None + async with self._session_factory() as session: + session.add(orm) + await session.commit() + await session.refresh(orm) + return exercise_from_orm(orm) if orm else None - def delete(self, id: UUID) -> None: - orm = self._session.get(ORMExercise, id) - if not orm: - return - self._session.delete(orm) - self._session.commit() + async def delete(self, id: UUID) -> None: + async with self._session_factory() as session: + orm = await session.get(ORMExercise, id) + if not orm: + return + await session.delete(orm) + await session.commit() - def update(self, exercise: DomainExercise) -> Optional[DomainExercise]: - orm = self._session.get(ORMExercise, exercise.id) - if not orm: - raise NotFoundError(f"Exercise {exercise.id} not found") - for key, value in exercise.to_orm_dict().items(): - setattr(orm, key, value) - self._session.commit() - return exercise_from_orm(orm) if orm else None + async def update(self, exercise: DomainExercise) -> Optional[DomainExercise]: + async with self._session_factory() as session: + orm = await session.get(ORMExercise, exercise.id) + if not orm: + raise NotFoundError(f"Exercise {exercise.id} not found") + for key, value in exercise.to_orm_dict().items(): + setattr(orm, key, value) + await session.commit() + return exercise_from_orm(orm) if orm else None - def find_all_owner(self, owner_id: UUID) -> Optional[List[DomainExercise]]: - orms = self._session.query(ORMExercise).filter(ORMExercise.owner_id == owner_id).all() - return [exercise_from_orm(orm) for orm in orms] if orms else [] + async def find_all_owner(self, owner_id: UUID) -> Optional[List[DomainExercise]]: + async with self._session_factory() as session: + result = await session.execute(select(ORMExercise).filter(ORMExercise.owner_id == owner_id)) + orms = result.scalars().all() + return [exercise_from_orm(orm) for orm in orms] if orms else [] - def find_all(self) -> Optional[List[DomainExercise]]: - orms = self._session.query(ORMExercise).all() - return [exercise_from_orm(orm) for orm in orms] if orms else [] + async def find_all(self) -> Optional[List[DomainExercise]]: + async with self._session_factory() as session: + result = await session.execute(select(ORMExercise)) + orms = result.scalars().all() + return [exercise_from_orm(orm) for orm in orms] if orms else [] - def find_by_id(self, id: UUID) -> Optional[DomainExercise]: - orm = self._session.get(ORMExercise, id) - return exercise_from_orm(orm) if orm else None \ No newline at end of file + async def find_by_id(self, id: UUID) -> Optional[DomainExercise]: + async with self._session_factory() as session: + orm = await session.get(ORMExercise, id) + return exercise_from_orm(orm) if orm else None \ No newline at end of file diff --git a/src/adapters/sqlalchemy/repositories/group.py b/src/adapters/sqlalchemy/repositories/group.py index 0553465..6ce3fd2 100644 --- a/src/adapters/sqlalchemy/repositories/group.py +++ b/src/adapters/sqlalchemy/repositories/group.py @@ -1,5 +1,7 @@ from typing import Optional, List -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select +from sqlalchemy.orm import selectinload from src.domain.exceptions import NotFoundError from src.domain.model.group import Group as DomainGroup from src.domain.model.profile import Profile as DomainProfile @@ -18,74 +20,95 @@ def group_from_orm(orm_group) -> DomainGroup: ) class SqlAlchemyGroupRepository(GroupRepository): - def __init__(self, session: Session): - self._session = session + def __init__(self, session_factory): + self._session_factory = session_factory - def find_by_id(self, id: UUID) -> Optional[DomainGroup]: - orm = self._session.get(ORMGroup, id) - return group_from_orm(orm) if orm else None + async def find_by_id(self, id: UUID) -> Optional[DomainGroup]: + async with self._session_factory() as session: + orm = await session.get(ORMGroup, id) + return group_from_orm(orm) if orm else None - def add(self, group: DomainGroup) -> Optional[DomainGroup]: + async def add(self, group: DomainGroup) -> Optional[DomainGroup]: data = group.to_orm_dict() orm = ORMGroup(**data) - self._session.add(orm) - self._session.commit() - self._session.refresh(orm) - return group_from_orm(orm) if orm else None + async with self._session_factory() as session: + session.add(orm) + await session.commit() + await session.refresh(orm) + return group_from_orm(orm) if orm else None - def delete(self, id: UUID) -> None: - orm = self._session.get(ORMGroup, id) - if not orm: - return - self._session.delete(orm) - self._session.commit() + async def delete(self, id: UUID) -> None: + async with self._session_factory() as session: + orm = await session.get(ORMGroup, id) + if not orm: + return + await session.delete(orm) + await session.commit() - def update(self, group: DomainGroup) -> Optional[DomainGroup]: - orm = self._session.get(ORMGroup, group.id) - if not orm: - raise NotFoundError(f"Groupe {group.id} not found") - for key, value in group.to_orm_dict().items(): - setattr(orm, key, value) - self._session.commit() - return group_from_orm(orm) if orm else None + async def update(self, group: DomainGroup) -> Optional[DomainGroup]: + async with self._session_factory() as session: + orm = await session.get(ORMGroup, group.id) + if not orm: + raise NotFoundError(f"Groupe {group.id} not found") + for key, value in group.to_orm_dict().items(): + setattr(orm, key, value) + await session.commit() + return group_from_orm(orm) if orm else None - - def add_member(self, group_id: UUID, user_id: UUID) -> None: - orm_group = self._session.get(ORMGroup, group_id) - if not orm_group: - raise NotFoundError(f"Groupe {group_id} not found") - orm_profile = self._session.get(ORMProfile, user_id) - if not orm_profile: - raise NotFoundError(f"Profile {user_id} not found") - if orm_profile not in orm_group.users: - orm_group.users.append(orm_profile) - self._session.commit() + async def add_member(self, group_id: UUID, user_id: UUID) -> None: + async with self._session_factory() as session: + orm_group = await session.get(ORMGroup, group_id) + if not orm_group: + raise NotFoundError(f"Groupe {group_id} not found") + orm_profile = await session.get(ORMProfile, user_id) + if not orm_profile: + raise NotFoundError(f"Profile {user_id} not found") + if orm_profile not in orm_group.users: + orm_group.users.append(orm_profile) + await session.commit() - def remove_member(self, group_id: UUID, user_id: UUID) -> None: - orm_group = self._session.get(ORMGroup, group_id) - if not orm_group: - raise NotFoundError(f"Groupe {group_id} not found") - orm_profile = self._session.get(ORMProfile, user_id) - if not orm_profile: - raise NotFoundError(f"Profile {user_id} not found") - if orm_profile in orm_group.users: - orm_group.users.remove(orm_profile) - self._session.commit() + async def remove_member(self, group_id: UUID, user_id: UUID) -> None: + async with self._session_factory() as session: + orm_group = await session.get(ORMGroup, group_id) + if not orm_group: + raise NotFoundError(f"Groupe {group_id} not found") + orm_profile = await session.get(ORMProfile, user_id) + if not orm_profile: + raise NotFoundError(f"Profile {user_id} not found") + if orm_profile in orm_group.users: + orm_group.users.remove(orm_profile) + await session.commit() - def list_members(self, group_id: UUID) -> List[DomainProfile]: - orm_grp = self._session.get(ORMGroup, group_id) - if not orm_grp: - raise NotFoundError(f"Groupe {group_id} introuvable") - return [profil_from_orm(p) for p in orm_grp.users] + async def list_members(self, group_id: UUID) -> List[DomainProfile]: + async with self._session_factory() as session: + result = await session.execute( + select(ORMGroup) + .options(selectinload(ORMGroup.users)) + .filter(ORMGroup.id == group_id) + ) + orm_grp = result.scalars().first() + if not orm_grp: + raise NotFoundError(f"Groupe {group_id} introuvable") + return [profil_from_orm(p) for p in orm_grp.users] - def find_by_owner_id(self, owner_id: UUID) -> Optional[List[DomainGroup]]: - orms = self._session.query(ORMGroup).filter(ORMGroup.owner_id == owner_id).all() - return [group_from_orm(orm) for orm in orms] if orms else [] + async def find_by_owner_id(self, owner_id: UUID) -> Optional[List[DomainGroup]]: + async with self._session_factory() as session: + result = await session.execute(select(ORMGroup).filter(ORMGroup.owner_id == owner_id)) + orms = result.scalars().all() + return [group_from_orm(orm) for orm in orms] if orms else [] - def find_all_groups(self) -> Optional[List[DomainGroup]]: - orms = self._session.query(ORMGroup).all() - return [group_from_orm(orm) for orm in orms] if orms else [] + async def find_all_groups(self) -> Optional[List[DomainGroup]]: + async with self._session_factory() as session: + result = await session.execute(select(ORMGroup)) + orms = result.scalars().all() + return [group_from_orm(orm) for orm in orms] if orms else [] - def find_groups_by_member_id(self, user_id: UUID) -> Optional[List[DomainGroup]]: - orms = self._session.query(ORMGroup).join(group_users).filter(group_users.c.profile_id == user_id).all() - return [group_from_orm(orm) for orm in orms] if orms else [] \ No newline at end of file + async def find_groups_by_member_id(self, user_id: UUID) -> Optional[List[DomainGroup]]: + async with self._session_factory() as session: + result = await session.execute( + select(ORMGroup) + .join(group_users) + .filter(group_users.c.profile_id == user_id) + ) + orms = result.scalars().all() + return [group_from_orm(orm) for orm in orms] if orms else [] \ No newline at end of file diff --git a/src/adapters/sqlalchemy/repositories/profile.py b/src/adapters/sqlalchemy/repositories/profile.py index 87ab2bc..dfafd9d 100644 --- a/src/adapters/sqlalchemy/repositories/profile.py +++ b/src/adapters/sqlalchemy/repositories/profile.py @@ -1,70 +1,88 @@ -from typing import Optional -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +from typing import Optional, List +from uuid import UUID + from src.domain.model.profile import Profile as DomainProfile -from src.adapters.sqlalchemy.models import Profile as ORMProfile from src.domain.ports.profile_repository import ProfileRepository -from uuid import UUID +from src.adapters.sqlalchemy.models.profile import Profile as SQLProfile -def profil_from_orm(orm_profile) ->DomainProfile: - return DomainProfile( - id=orm_profile.id, - email=orm_profile.email, - password=orm_profile.password, - name=orm_profile.name, - sex=orm_profile.sex, - age=orm_profile.age, - contact=orm_profile.contact, - pricing=orm_profile.pricing, - description=orm_profile.description, - legacy=orm_profile.legacy, - roles=orm_profile.roles, - created_at=orm_profile.created_at, - ) +class SQLAlchemyProfileRepository(ProfileRepository): + def __init__(self, session_factory): + self._session_factory = session_factory + async def find_by_email(self, email: str) -> Optional[DomainProfile]: + async with self._session_factory() as session: # ✅ Correct usage + stmt = select(SQLProfile).where(SQLProfile.email == email) + result = await session.execute(stmt) + sql_profile = result.scalar_one_or_none() + + if sql_profile: + return sql_profile.to_domain() + return None + async def add(self, profile: DomainProfile) -> DomainProfile: + async with self._session_factory() as session: + sql_profile = SQLProfile.from_domain(profile) + session.add(sql_profile) + await session.commit() + await session.refresh(sql_profile) + return sql_profile.to_domain() -class SqlAlchemyProfileRepository(ProfileRepository): - def __init__(self, session: Session): - self._session = session + async def find_by_id(self, id: UUID) -> Optional[DomainProfile]: + async with self._session_factory() as session: + stmt = select(SQLProfile).where(SQLProfile.id == id) + result = await session.execute(stmt) + sql_profile = result.scalar_one_or_none() + + if sql_profile: + return sql_profile.to_domain() + return None - def find_by_email(self, email: str) -> Optional[DomainProfile]: - orm = self._session.query(ORMProfile).filter(ORMProfile.email == email).one_or_none() - return profil_from_orm(orm) if orm else None + async def delete(self, id: UUID) -> None: + async with self._session_factory() as session: + stmt = select(SQLProfile).where(SQLProfile.id == id) + result = await session.execute(stmt) + sql_profile = result.scalar_one_or_none() + + if sql_profile: + await session.delete(sql_profile) + await session.commit() - def add(self, profile: DomainProfile) -> Optional[DomainProfile]: - data = profile.to_orm_dict() - orm = ORMProfile(**data) - self._session.add(orm) - self._session.commit() - self._session.refresh(orm) - return profil_from_orm(orm) if orm else None - - def find_by_id(self, id: UUID) -> Optional[DomainProfile]: - # SQLAlchemy 1.4+ : session.get - orm = self._session.get(ORMProfile, id) - return profil_from_orm(orm) if orm else None - - def delete(self, id: UUID) -> None: - orm = self._session.get(ORMProfile, id) - if not orm: - return - self._session.delete(orm) - self._session.commit() - - def update(self, profile: DomainProfile) -> Optional[DomainProfile]: - orm = self._session.get(ORMProfile, profile.id) - if not orm: + async def update(self, profile: DomainProfile) -> Optional[DomainProfile]: + async with self._session_factory() as session: + stmt = select(SQLProfile).where(SQLProfile.id == profile.id) + result = await session.execute(stmt) + sql_profile = result.scalar_one_or_none() + + if sql_profile: + # Mettre à jour les champs + sql_profile.email = profile.email + sql_profile.password = profile.password + sql_profile.name = profile.name + sql_profile.sex = profile.sex + sql_profile.age = profile.age + sql_profile.contact = profile.contact + sql_profile.pricing = profile.pricing + sql_profile.description = profile.description + sql_profile.legacy = profile.legacy + sql_profile.roles = profile.roles + + await session.commit() + await session.refresh(sql_profile) + return sql_profile.to_domain() return None - for key, value in profile.to_orm_dict().items(): - setattr(orm, key, value) - self._session.commit() - self._session.refresh(orm) - return profil_from_orm(orm) - - def find_all_users(self)-> list[DomainProfile]: - orms = self._session.query(ORMProfile).filter(ORMProfile.roles.any('user')).all() - return [profil_from_orm(orm) for orm in orms] if orms else [] - - def find_all_coachs(self)-> list[DomainProfile]: - orms = self._session.query(ORMProfile).filter(ORMProfile.roles.any('coach')).all() - return [profil_from_orm(orm) for orm in orms] if orms else [] \ No newline at end of file + + async def find_all_users(self) -> List[DomainProfile]: + async with self._session_factory() as session: + stmt = select(SQLProfile).where(SQLProfile.roles.contains(["user"])) + result = await session.execute(stmt) + sql_profiles = result.scalars().all() + return [profile.to_domain() for profile in sql_profiles] + + async def find_all_coachs(self) -> List[DomainProfile]: + async with self._session_factory() as session: + stmt = select(SQLProfile).where(SQLProfile.roles.contains(["coach"])) + result = await session.execute(stmt) + sql_profiles = result.scalars().all() + return [profile.to_domain() for profile in sql_profiles] \ No newline at end of file diff --git a/src/adapters/sqlalchemy/repositories/training.py b/src/adapters/sqlalchemy/repositories/training.py index 6f20763..64dac41 100644 --- a/src/adapters/sqlalchemy/repositories/training.py +++ b/src/adapters/sqlalchemy/repositories/training.py @@ -1,5 +1,7 @@ from typing import List, Optional -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select +from sqlalchemy.orm import selectinload from src.domain.exceptions import NotFoundError from src.adapters.sqlalchemy.models import Training as ORMTraining, Task as ORMTask, Validation as ORMValidate @@ -44,107 +46,132 @@ def training_from_orm(orm_training) -> DomainTraining: ) class SqlAlchemyTrainingRepository(TrainingRepository): - def __init__(self, session: Session): - self._session = session + def __init__(self, session_factory): + self._session_factory = session_factory - def find_by_id(self, id: UUID) -> Optional[DomainTraining]: - orm = self._session.get(ORMTraining, id) - return training_from_orm(orm) if orm else None + async def find_by_id(self, id: UUID) -> Optional[DomainTraining]: + async with self._session_factory() as session: + orm = await session.get(ORMTraining, id) + return training_from_orm(orm) if orm else None - # CRUD operations for Training - def add_training(self, training: DomainTraining) -> Optional[DomainTraining]: + async def add_training(self, training: DomainTraining) -> Optional[DomainTraining]: data = training.to_orm_dict() orm = ORMTraining(**data) - self._session.add(orm) - self._session.commit() - self._session.refresh(orm) - return training_from_orm(orm) if orm else None + async with self._session_factory() as session: + session.add(orm) + await session.commit() + await session.refresh(orm) + return training_from_orm(orm) if orm else None - def delete_training(self, id: UUID) -> None: - orm = self._session.get(ORMTraining, id) - if not orm: - return - self._session.delete(orm) - self._session.commit() + async def delete_training(self, id: UUID) -> None: + async with self._session_factory() as session: + orm = await session.get(ORMTraining, id) + if not orm: + return + await session.delete(orm) + await session.commit() - def update_training(self, training: DomainTraining) -> Optional[DomainTraining]: - orm = self._session.get(ORMTraining, training.id) - if not orm: - raise NotFoundError(f"Training {training.id} not found") - for key, value in training.to_orm_dict().items(): - setattr(orm, key, value) - self._session.commit() - return training_from_orm(orm) if orm else None + async def update_training(self, training: DomainTraining) -> Optional[DomainTraining]: + async with self._session_factory() as session: + orm = await session.get(ORMTraining, training.id) + if not orm: + raise NotFoundError(f"Training {training.id} not found") + for key, value in training.to_orm_dict().items(): + setattr(orm, key, value) + await session.commit() + return training_from_orm(orm) if orm else None - def find_all_owner_trainings(self, owner_id: UUID) -> Optional[List[DomainTraining]]: - orms = self._session.query(ORMTraining).filter(ORMTraining.owner_id == owner_id).all() - return [training_from_orm(orm) for orm in orms] if orms else [] + async def find_all_owner_trainings(self, owner_id: UUID) -> Optional[List[DomainTraining]]: + async with self._session_factory() as session: + result = await session.execute(select(ORMTraining).filter(ORMTraining.owner_id == owner_id)) + orms = result.scalars().all() + return [training_from_orm(orm) for orm in orms] if orms else [] # CRUD Operation for Task - - def add_task(self, task: DomainTask) -> Optional[DomainTask]: + async def add_task(self, task: DomainTask) -> Optional[DomainTask]: data = task.to_orm_dict() orm = ORMTask(**data) - self._session.add(orm) - self._session.commit() - self._session.refresh(orm) - return task_from_orm(orm) if orm else None + async with self._session_factory() as session: + session.add(orm) + await session.commit() + await session.refresh(orm) + return task_from_orm(orm) if orm else None - def find_task_by_id(self, id: UUID) -> Optional[DomainTask]: - orm = self._session.get(ORMTask, id) - return task_from_orm(orm) if orm else None + async def find_task_by_id(self, id: UUID) -> Optional[DomainTask]: + async with self._session_factory() as session: + # Utilisation de selectinload pour charger les validations en une seule requête + result = await session.execute( + select(ORMTask) + .options(selectinload(ORMTask.validations)) + .filter(ORMTask.id == id) + ) + orm = result.scalars().first() + return task_from_orm(orm) if orm else None - def delete_task(self, id: UUID) -> None: - orm = self._session.get(ORMTask, id) - if not orm: - return - self._session.delete(orm) - self._session.commit() + async def delete_task(self, id: UUID) -> None: + async with self._session_factory() as session: + orm = await session.get(ORMTask, id) + if not orm: + return + await session.delete(orm) + await session.commit() - def update_task(self, task: DomainTask) -> Optional[DomainTask]: - orm = self._session.get(ORMTask, task.id) - if not orm: - raise NotFoundError(f"Task {task.id} not found") - for key, value in task.to_orm_dict().items(): - setattr(orm, key, value) - self._session.commit() - return task_from_orm(orm) if orm else None + async def update_task(self, task: DomainTask) -> Optional[DomainTask]: + async with self._session_factory() as session: + orm = await session.get(ORMTask, task.id) + if not orm: + raise NotFoundError(f"Task {task.id} not found") + for key, value in task.to_orm_dict().items(): + setattr(orm, key, value) + await session.commit() + return task_from_orm(orm) if orm else None - def find_tasks_by_training_id(self, training_id: UUID) -> Optional[List[DomainTask]]: - orms = self._session.query(ORMTask).filter(ORMTask.training_id == training_id).all() - return [task_from_orm(orm) for orm in orms] if orms else [] + async def find_tasks_by_training_id(self, training_id: UUID) -> Optional[List[DomainTask]]: + async with self._session_factory() as session: + result = await session.execute( + select(ORMTask) + .options(selectinload(ORMTask.validations)) + .filter(ORMTask.training_id == training_id) + ) + orms = result.scalars().all() + return [task_from_orm(orm) for orm in orms] if orms else [] -# validate methods - def add_validate(self, validate: DomainValidate) -> Optional[DomainValidate]: + # validate methods + async def add_validate(self, validate: DomainValidate) -> Optional[DomainValidate]: data = validate.to_orm_dict() orm = ORMValidate(**data) - self._session.add(orm) - self._session.commit() - self._session.refresh(orm) - return validate_from_orm(orm) if orm else None + async with self._session_factory() as session: + session.add(orm) + await session.commit() + await session.refresh(orm) + return validate_from_orm(orm) if orm else None - def find_validate_by_task_id(self, task_id: UUID) -> Optional[List[DomainValidate]]: - orm = self._session.query(ORMValidate).filter(ORMValidate.task_id == task_id).all() - return [validate_from_orm(orm) for orm in orm] if orm else None + async def find_validate_by_task_id(self, task_id: UUID) -> Optional[List[DomainValidate]]: + async with self._session_factory() as session: + result = await session.execute(select(ORMValidate).filter(ORMValidate.task_id == task_id)) + orms = result.scalars().all() + return [validate_from_orm(orm) for orm in orms] if orms else None - def delete_validate(self, id: UUID) -> None: - orm = self._session.get(ORMValidate, id) - if not orm: - return - self._session.delete(orm) - self._session.commit() + async def delete_validate(self, id: UUID) -> None: + async with self._session_factory() as session: + orm = await session.get(ORMValidate, id) + if not orm: + return + await session.delete(orm) + await session.commit() - def find_validate_by_id(self, id: UUID) -> Optional[DomainValidate]: - orm = self._session.get(ORMValidate, id) - return validate_from_orm(orm) if orm else None + async def find_validate_by_id(self, id: UUID) -> Optional[DomainValidate]: + async with self._session_factory() as session: + orm = await session.get(ORMValidate, id) + return validate_from_orm(orm) if orm else None - def find_all_validates_by_training_id(self, training_id: UUID) -> list[DomainValidate]: - orms = ( - self._session - .query(ORMValidate) + async def find_all_validates_by_training_id(self, training_id: UUID) -> list[DomainValidate]: + async with self._session_factory() as session: + result = await session.execute( + select(ORMValidate) .join(ORMTask, ORMValidate.task_id == ORMTask.id) .filter(ORMTask.training_id == training_id) - .all() - ) - return [validate_from_orm(v) for v in orms] \ No newline at end of file + ) + orms = result.scalars().all() + return [validate_from_orm(v) for v in orms] \ No newline at end of file diff --git a/src/container.py b/src/container.py index 63a3e31..18771ec 100644 --- a/src/container.py +++ b/src/container.py @@ -1,5 +1,7 @@ import os from uuid import uuid4, UUID +import asyncio +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from src.domain.lib.security import BcryptPasswordHasher from src.domain.services.profile import ProfileService @@ -7,6 +9,7 @@ from src.domain.services.training import TrainingService from src.domain.services.exercise import ExerciseService from src.domain.services.diet import DietService +from src.adapters.sqlalchemy.db import SessionLocal, init_db class Container: def __init__(self, env: str | None = None): @@ -42,52 +45,125 @@ def __init__(self, env: str | None = None): self.exercise_repo = InMemoryExerciseRepository() self.diet_repo = InMemoryDietRepository() else: - from src.adapters.sqlalchemy.db import SessionLocal - self.SessionFactory = SessionLocal + # Pas besoin de setup_database, on utilise db.py + pass + + def get_session_factory(self): + return SessionLocal # Vient de db.py def get_profile_service(self): if self.env in ("dev", "test"): repo = self.profile_repo + return ProfileService(repo, self.hasher) else: from src.adapters.sqlalchemy.repositories.profile import SqlAlchemyProfileRepository - session = self.SessionFactory() - repo = SqlAlchemyProfileRepository(session) - return ProfileService(repo, self.hasher) + + class SessionManagedRepository: + def __init__(self, repo_class, session_factory): + self.repo_class = repo_class + self.session_factory = session_factory + + def __getattr__(self, name): + async def method(*args, **kwargs): + async with self.session_factory() as session: + repo = self.repo_class(session) + repo_method = getattr(repo, name) + return await repo_method(*args, **kwargs) + return method + + repo = SessionManagedRepository(SqlAlchemyProfileRepository, self.SessionFactory) + return ProfileService(repo, self.hasher) def get_group_service(self): if self.env in ("dev", "test"): repo = self.group_repo + return GroupService(repo) else: from src.adapters.sqlalchemy.repositories.group import SqlAlchemyGroupRepository - session = self.SessionFactory() - repo = SqlAlchemyGroupRepository(session) - return GroupService(repo) + + class SessionManagedRepository: + def __init__(self, repo_class, session_factory): + self.repo_class = repo_class + self.session_factory = session_factory + + def __getattr__(self, name): + async def method(*args, **kwargs): + async with self.session_factory() as session: + repo = self.repo_class(session) + repo_method = getattr(repo, name) + return await repo_method(*args, **kwargs) + return method + + repo = SessionManagedRepository(SqlAlchemyGroupRepository, self.SessionFactory) + return GroupService(repo) def get_training_service(self): if self.env in ("dev", "test"): repo = self.training_repo + return TrainingService(repo) else: from src.adapters.sqlalchemy.repositories.training import SqlAlchemyTrainingRepository - session = self.SessionFactory() - repo = SqlAlchemyTrainingRepository(session) - return TrainingService(repo) + + class SessionManagedRepository: + def __init__(self, repo_class, session_factory): + self.repo_class = repo_class + self.session_factory = session_factory + + def __getattr__(self, name): + async def method(*args, **kwargs): + async with self.session_factory() as session: + repo = self.repo_class(session) + repo_method = getattr(repo, name) + return await repo_method(*args, **kwargs) + return method + + repo = SessionManagedRepository(SqlAlchemyTrainingRepository, self.SessionFactory) + return TrainingService(repo) def get_exercise_service(self): if self.env in ("dev", "test"): repo = self.exercise_repo + return ExerciseService(repo) else: from src.adapters.sqlalchemy.repositories.exercise import SqlAlchemyExerciseRepository - session = self.SessionFactory() - repo = SqlAlchemyExerciseRepository(session) - return ExerciseService(repo) + + class SessionManagedRepository: + def __init__(self, repo_class, session_factory): + self.repo_class = repo_class + self.session_factory = session_factory + + def __getattr__(self, name): + async def method(*args, **kwargs): + async with self.session_factory() as session: + repo = self.repo_class(session) + repo_method = getattr(repo, name) + return await repo_method(*args, **kwargs) + return method + + repo = SessionManagedRepository(SqlAlchemyExerciseRepository, self.SessionFactory) + return ExerciseService(repo) def get_diet_service(self): if self.env in ("dev", "test"): repo = self.diet_repo + return DietService(repo) else: from src.adapters.sqlalchemy.repositories.diet import SqlAlchemyDietRepository - session = self.SessionFactory() - repo = SqlAlchemyDietRepository(session) - return DietService(repo) + + class SessionManagedRepository: + def __init__(self, repo_class, session_factory): + self.repo_class = repo_class + self.session_factory = session_factory + + def __getattr__(self, name): + async def method(*args, **kwargs): + async with self.session_factory() as session: + repo = self.repo_class(session) + repo_method = getattr(repo, name) + return await repo_method(*args, **kwargs) + return method + + repo = SessionManagedRepository(SqlAlchemyDietRepository, self.SessionFactory) + return DietService(repo) -container = Container() +container = Container() \ No newline at end of file diff --git a/src/domain/ports/diet_repository.py b/src/domain/ports/diet_repository.py index c951e7e..b820113 100644 --- a/src/domain/ports/diet_repository.py +++ b/src/domain/ports/diet_repository.py @@ -5,75 +5,75 @@ class DietRepository(ABC): @abstractmethod - def add_diet(self, diet: DomainDiet) -> DomainDiet: + async def add_diet(self, diet: DomainDiet) -> DomainDiet: pass @abstractmethod - def find_by_id(self, id: UUID) -> Optional[DomainDiet]: + async def find_by_id(self, id: UUID) -> Optional[DomainDiet]: pass @abstractmethod - def find_all_owner_diets(self, owner_id: UUID) -> List[DomainDiet]: + async def find_all_owner_diets(self, owner_id: UUID) -> List[DomainDiet]: pass @abstractmethod - def update_diet(self, diet: DomainDiet) -> DomainDiet: + async def update_diet(self, diet: DomainDiet) -> DomainDiet: pass @abstractmethod - def delete_diet(self, id: UUID) -> None: + async def delete_diet(self, id: UUID) -> None: pass # Macro Plan methods @abstractmethod - def add_macro_plan(self, macro_plan: DomainMacroPlan) -> DomainMacroPlan: + async def add_macro_plan(self, macro_plan: DomainMacroPlan) -> DomainMacroPlan: pass @abstractmethod - def find_macro_plan_by_id(self, id: UUID) -> Optional[DomainMacroPlan]: + async def find_macro_plan_by_id(self, id: UUID) -> Optional[DomainMacroPlan]: pass @abstractmethod - def find_macro_plans_by_user_id(self, user_id: UUID) -> List[DomainMacroPlan]: + async def find_macro_plans_by_user_id(self, user_id: UUID) -> List[DomainMacroPlan]: pass @abstractmethod - def delete_macro_plan(self, id: UUID) -> None: + async def delete_macro_plan(self, id: UUID) -> None: pass @abstractmethod - def find_macro_plans_by_diet_id(self, diet_id: UUID) -> List[DomainMacroPlan]: + async def find_macro_plans_by_diet_id(self, diet_id: UUID) -> List[DomainMacroPlan]: pass @abstractmethod - def update_macro_plan(self, macro_plan: DomainMacroPlan) -> DomainMacroPlan: + async def update_macro_plan(self, macro_plan: DomainMacroPlan) -> DomainMacroPlan: pass # Meal Plan methods @abstractmethod - def add_meal_plan(self, meal_plan: DomainMealPlan) -> DomainMealPlan: + async def add_meal_plan(self, meal_plan: DomainMealPlan) -> DomainMealPlan: pass @abstractmethod - def find_meal_plan_by_id(self, id: UUID) -> Optional[DomainMealPlan]: + async def find_meal_plan_by_id(self, id: UUID) -> Optional[DomainMealPlan]: pass @abstractmethod - def find_meal_plans_by_user_id(self, user_id: UUID) -> List[DomainMealPlan]: + async def find_meal_plans_by_user_id(self, user_id: UUID) -> List[DomainMealPlan]: pass @abstractmethod - def find_meal_plans_by_diet_id(self, diet_id: UUID) -> List[DomainMealPlan]: + async def find_meal_plans_by_diet_id(self, diet_id: UUID) -> List[DomainMealPlan]: pass @abstractmethod - def update_meal_plan(self, meal_plan: DomainMealPlan) -> DomainMealPlan: + async def update_meal_plan(self, meal_plan: DomainMealPlan) -> DomainMealPlan: pass @abstractmethod - def delete_meal_plan(self, id: UUID) -> None: + async def delete_meal_plan(self, id: UUID) -> None: pass \ No newline at end of file diff --git a/src/domain/ports/exercise_repository.py b/src/domain/ports/exercise_repository.py index 818ef13..60f44bd 100644 --- a/src/domain/ports/exercise_repository.py +++ b/src/domain/ports/exercise_repository.py @@ -5,25 +5,25 @@ class ExerciseRepository(ABC): @abstractmethod - def add(self, exercise: Exercise) -> Optional[Exercise]: + async def add(self, exercise: Exercise) -> Optional[Exercise]: pass @abstractmethod - def delete(self, id: UUID) -> None: + async def delete(self, id: UUID) -> None: pass @abstractmethod - def update(self, exercise: Exercise) -> Optional[Exercise]: + async def update(self, exercise: Exercise) -> Optional[Exercise]: pass @abstractmethod - def find_all_owner(self, owner_id: UUID) -> Optional[List[Exercise]]: + async def find_all_owner(self, owner_id: UUID) -> Optional[List[Exercise]]: pass @abstractmethod - def find_all(self) -> Optional[List[Exercise]]: + async def find_all(self) -> Optional[List[Exercise]]: pass @abstractmethod - def find_by_id(self, id: UUID) -> Optional[Exercise]: + async def find_by_id(self, id: UUID) -> Optional[Exercise]: pass \ No newline at end of file diff --git a/src/domain/ports/group_repository.py b/src/domain/ports/group_repository.py index 741c9fc..9204acf 100644 --- a/src/domain/ports/group_repository.py +++ b/src/domain/ports/group_repository.py @@ -6,41 +6,41 @@ class GroupRepository(ABC): @abstractmethod - def find_by_id(self, id: UUID) -> Optional[Group]: + async def find_by_id(self, id: UUID) -> Optional[Group]: pass @abstractmethod - def add(self, group: Group) -> Optional[Group]: + async def add(self, group: Group) -> Optional[Group]: pass @abstractmethod - def delete(self, id: UUID) -> None: + async def delete(self, id: UUID) -> None: pass @abstractmethod - def update(self, group: Group) -> Optional[Group]: + async def update(self, group: Group) -> Optional[Group]: pass @abstractmethod - def add_member(self, group_id: UUID, user_id: UUID) -> None: + async def add_member(self, group_id: UUID, user_id: UUID) -> None: pass @abstractmethod - def remove_member(self, group_id: UUID, user_id: UUID) -> None: + async def remove_member(self, group_id: UUID, user_id: UUID) -> None: pass @abstractmethod - def find_by_owner_id(self, owner_id: UUID) -> Optional[list[Group]]: + async def find_by_owner_id(self, owner_id: UUID) -> Optional[list[Group]]: pass @abstractmethod - def list_members(self, group_id: UUID) -> Optional[list[UUID]]: + async def list_members(self, group_id: UUID) -> Optional[list[UUID]]: pass @abstractmethod - def find_all_groups(self) -> Optional[list[Group]]: + async def find_all_groups(self) -> Optional[list[Group]]: pass @abstractmethod - def find_groups_by_member_id(self, user_id: UUID) -> Optional[list[Group]]: + async def find_groups_by_member_id(self, user_id: UUID) -> Optional[list[Group]]: pass \ No newline at end of file diff --git a/src/domain/ports/profile_repository.py b/src/domain/ports/profile_repository.py index 08f6633..a0776e7 100644 --- a/src/domain/ports/profile_repository.py +++ b/src/domain/ports/profile_repository.py @@ -5,29 +5,29 @@ class ProfileRepository(ABC): @abstractmethod - def add(self, profile: Profile) -> Profile: + async def add(self, profile: Profile) -> Profile: pass @abstractmethod - def find_by_email(self, email: str) -> Optional[Profile]: + async def find_by_email(self, email: str) -> Optional[Profile]: pass @abstractmethod - def delete(self, id: UUID) -> None: + async def delete(self, id: UUID) -> None: pass @abstractmethod - def find_by_id(self, id: UUID) -> Optional[Profile]: + async def find_by_id(self, id: UUID) -> Optional[Profile]: pass @abstractmethod - def update(self, profile: Profile) -> Profile: + async def update(self, profile: Profile) -> Profile: pass @abstractmethod - def find_all_users(self) -> list[Profile]: + async def find_all_users(self) -> list[Profile]: pass @abstractmethod - def find_all_coachs(self) -> list[Profile]: + async def find_all_coachs(self) -> list[Profile]: pass \ No newline at end of file diff --git a/src/domain/ports/training_repository.py b/src/domain/ports/training_repository.py index 0242aef..ab3fcba 100644 --- a/src/domain/ports/training_repository.py +++ b/src/domain/ports/training_repository.py @@ -5,66 +5,65 @@ class TrainingRepository(ABC): @abstractmethod - def find_by_id(self, id:UUID) -> Optional[Training]: + async def find_by_id(self, id:UUID) -> Optional[Training]: pass @abstractmethod - def add_training(self, training: Training) -> Optional[Training]: + async def add_training(self, training: Training) -> Optional[Training]: pass @abstractmethod - def delete_training(self, id: UUID) -> None: + async def delete_training(self, id: UUID) -> None: pass @abstractmethod - def update_training(self, training: Training) -> Optional[Training]: + async def update_training(self, training: Training) -> Optional[Training]: pass @abstractmethod - def find_all_owner_trainings(self, owner_id: UUID) -> Optional[List[Training]]: + async def find_all_owner_trainings(self, owner_id: UUID) -> Optional[List[Training]]: pass # tasks abstract methods @abstractmethod - def add_task(self, task: Task) -> Optional[Task]: + async def add_task(self, task: Task) -> Optional[Task]: pass @abstractmethod - def find_task_by_id(self, id: UUID) -> Optional[Task]: + async def find_task_by_id(self, id: UUID) -> Optional[Task]: pass @abstractmethod - def delete_task(self, id: UUID) -> None: + async def delete_task(self, id: UUID) -> None: pass @abstractmethod - def update_task(self, task: Task) -> Optional[Task]: + async def update_task(self, task: Task) -> Optional[Task]: pass @abstractmethod - def find_tasks_by_training_id(self, training_id: UUID) -> Optional[List[Task]]: + async def find_tasks_by_training_id(self, training_id: UUID) -> Optional[List[Task]]: pass # validate abstract methods @abstractmethod - def add_validate(self, validate: Validate) -> Optional[Validate]: + async def add_validate(self, validate: Validate) -> Optional[Validate]: pass @abstractmethod - def find_validate_by_task_id(self, id: UUID) -> Optional[Validate]: + async def find_validate_by_task_id(self, id: UUID) -> Optional[Validate]: pass @abstractmethod - def delete_validate(self, id: UUID) -> None: + async def delete_validate(self, id: UUID) -> None: pass @abstractmethod - def find_validate_by_id(self, id: UUID) -> Optional[Validate]: + async def find_validate_by_id(self, id: UUID) -> Optional[Validate]: pass @abstractmethod - def find_all_validates_by_training_id(self, training_id: UUID) -> Optional[List[Validate]]: + async def find_all_validates_by_training_id(self, training_id: UUID) -> Optional[List[Validate]]: pass - diff --git a/src/domain/services/diet.py b/src/domain/services/diet.py index aa3a663..510d5a3 100644 --- a/src/domain/services/diet.py +++ b/src/domain/services/diet.py @@ -9,7 +9,7 @@ class DietService: def __init__(self, repo: DietRepository): self._repo = repo - def create_diet(self, *, owner_id: UUID, name: str, description: str = "") -> DomainDiet: + async def create_diet(self, *, owner_id: UUID, name: str, description: str = "") -> DomainDiet: if not name: raise ValueError("Diet name cannot be empty") diet = DomainDiet( @@ -19,35 +19,35 @@ def create_diet(self, *, owner_id: UUID, name: str, description: str = "") -> Do description=description, created_at=datetime.utcnow(), ) - return self._repo.add_diet(diet) + return await self._repo.add_diet(diet) - def get_diet(self, diet_id: UUID) -> DomainDiet: - diet = self._repo.find_by_id(diet_id) + async def get_diet(self, diet_id: UUID) -> DomainDiet: + diet = await self._repo.find_by_id(diet_id) if not diet: raise NotFoundError(f"Diet {diet_id} not found") return diet - def list_owner_diets(self, owner_id: UUID) -> List[DomainDiet]: - return self._repo.find_all_owner_diets(owner_id) + async def list_owner_diets(self, owner_id: UUID) -> List[DomainDiet]: + return await self._repo.find_all_owner_diets(owner_id) - def update_diet(self, diet_id: UUID, name: str | None = None, description: str | None = None) -> DomainDiet: - diet = self._repo.find_by_id(diet_id) + async def update_diet(self, diet_id: UUID, name: str | None = None, description: str | None = None) -> DomainDiet: + diet = await self._repo.find_by_id(diet_id) if not diet: raise NotFoundError(f"Diet {diet_id} not found") if name is not None: diet.name = name if description is not None: diet.description = description - return self._repo.update_diet(diet) + return await self._repo.update_diet(diet) - def delete_diet(self, diet_id: UUID) -> None: - if not self._repo.find_by_id(diet_id): + async def delete_diet(self, diet_id: UUID) -> None: + if not await self._repo.find_by_id(diet_id): raise NotFoundError(f"Diet {diet_id} not found") - self._repo.delete_diet(diet_id) + await self._repo.delete_diet(diet_id) # Macro Plan methods - def create_macro_plan( + async def create_macro_plan( self, diet_id: UUID, name: str, @@ -71,21 +71,21 @@ def create_macro_plan( water=water, kilocalorie=kilocalorie, ) - return self._repo.add_macro_plan(plan) + return await self._repo.add_macro_plan(plan) - def get_macro_plans_for_diet(self, diet_id: UUID) -> List[DomainMacroPlan]: - return self._repo.find_macro_plans_by_diet_id(diet_id) + async def get_macro_plans_for_diet(self, diet_id: UUID) -> List[DomainMacroPlan]: + return await self._repo.find_macro_plans_by_diet_id(diet_id) - def get_macro_plans_by_user_id(self, user_id: UUID) -> List[DomainMacroPlan]: - return self._repo.find_macro_plans_by_user_id(user_id) + async def get_macro_plans_by_user_id(self, user_id: UUID) -> List[DomainMacroPlan]: + return await self._repo.find_macro_plans_by_user_id(user_id) - def get_macro_plan(self, plan_id: UUID) -> DomainMacroPlan: - plan = self._repo.find_macro_plan_by_id(plan_id) + async def get_macro_plan(self, plan_id: UUID) -> DomainMacroPlan: + plan = await self._repo.find_macro_plan_by_id(plan_id) if not plan: raise NotFoundError(f"MacroPlan {plan_id} not found") return plan - def update_macro_plan( + async def update_macro_plan( self, plan_id: UUID, name: str | None = None, @@ -96,7 +96,7 @@ def update_macro_plan( water: float | None = None, kilocalorie: float | None = None, ) -> DomainMacroPlan: - plan = self.get_macro_plan(plan_id) + plan = await self.get_macro_plan(plan_id) if name is not None: plan.name = name if carbohydrates is not None: @@ -111,15 +111,15 @@ def update_macro_plan( plan.water = water if kilocalorie is not None: plan.kilocalorie = kilocalorie - return self._repo.update_macro_plan(plan) + return await self._repo.update_macro_plan(plan) - def delete_macro_plan(self, plan_id: UUID) -> None: - self.get_macro_plan(plan_id) - self._repo.delete_macro_plan(plan_id) + async def delete_macro_plan(self, plan_id: UUID) -> None: + await self.get_macro_plan(plan_id) + await self._repo.delete_macro_plan(plan_id) # Meal Plan methods - def create_meal_plan( + async def create_meal_plan( self, diet_id: UUID, name: str, @@ -136,27 +136,27 @@ def create_meal_plan( name=name, meals=domain_meals, ) - return self._repo.add_meal_plan(mp) + return await self._repo.add_meal_plan(mp) - def get_meal_plan_by_id(self, id: UUID) -> DomainMealPlan: - mp = self._repo.find_meal_plan_by_id(id) + async def get_meal_plan_by_id(self, id: UUID) -> DomainMealPlan: + mp = await self._repo.find_meal_plan_by_id(id) if not mp: raise NotFoundError(f"MealPlan {id} not found") return mp - def get_meal_plans_by_diet(self, diet_id: UUID) -> list[DomainMealPlan]: - return self._repo.find_meal_plans_by_diet_id(diet_id) + async def get_meal_plans_by_diet(self, diet_id: UUID) -> list[DomainMealPlan]: + return await self._repo.find_meal_plans_by_diet_id(diet_id) - def get_meal_plans_by_user(self, user_id: UUID) -> list[DomainMealPlan]: - return self._repo.find_meal_plans_by_user_id(user_id) + async def get_meal_plans_by_user(self, user_id: UUID) -> list[DomainMealPlan]: + return await self._repo.find_meal_plans_by_user_id(user_id) - def update_meal_plan( + async def update_meal_plan( self, plan_id: UUID, name: str | None = None, meals: list[dict] | None = None, ) -> DomainMealPlan: - mp = self.get_meal_plan_by_id(plan_id) + mp = await self.get_meal_plan_by_id(plan_id) updated_name = name if name is not None else mp.name updated_meals = [DomainMealItem(**m.model_dump()) for m in meals] if meals is not None else mp.meals @@ -168,9 +168,9 @@ def update_meal_plan( mp.name = updated_name if meals is not None: mp.meals = updated_meals - return self._repo.update_meal_plan(mp) + return await self._repo.update_meal_plan(mp) - def delete_meal_plan(self, plan_id: UUID) -> None: - self.get_meal_plan_by_id(plan_id) - self._repo.delete_meal_plan(plan_id) + async def delete_meal_plan(self, plan_id: UUID) -> None: + await self.get_meal_plan_by_id(plan_id) + await self._repo.delete_meal_plan(plan_id) diff --git a/src/domain/services/exercise.py b/src/domain/services/exercise.py index 524b690..3aef160 100644 --- a/src/domain/services/exercise.py +++ b/src/domain/services/exercise.py @@ -10,7 +10,7 @@ class ExerciseService: def __init__(self, repo: ExerciseRepository): self._repo = repo - def create_exercise(self, *, + async def create_exercise(self, *, owner_id: UUID, name: str, description: Optional[str] = None) -> DomainExercise: @@ -25,17 +25,17 @@ def create_exercise(self, *, created_at=datetime.utcnow(), ) - return self._repo.add(exercise) + return await self._repo.add(exercise) - def delete_exercise(self, exercise_id: UUID): - self._repo.delete(exercise_id) + async def delete_exercise(self, exercise_id: UUID): + await self._repo.delete(exercise_id) - def update_exercise(self, + async def update_exercise(self, exercise_id: UUID, name: Optional[str] = None, description: Optional[str] = None ) -> DomainExercise: - exercise = self._repo.find_by_id(exercise_id) + exercise = await self._repo.find_by_id(exercise_id) if not exercise: raise NotFoundError(f"Exercise {exercise_id} not found") @@ -43,17 +43,16 @@ def update_exercise(self, exercise.name = name if description is not None: exercise.description = description - return self._repo.update(exercise) + return await self._repo.update(exercise) - def get_exercises_mine(self, owner_id: UUID) -> List[DomainExercise]: - exercises = self._repo.find_all_owner(owner_id) + async def get_exercises_mine(self, owner_id: UUID) -> List[DomainExercise]: + exercises = await self._repo.find_all_owner(owner_id) if not exercises: raise NotFoundError(f"No exercises found for owner {owner_id}") return exercises - def get_all_exercises(self) -> List[DomainExercise]: - exercises = self._repo.find_all() + async def get_all_exercises(self) -> List[DomainExercise]: + exercises = await self._repo.find_all() if not exercises: raise NotFoundError("No exercises found") return exercises - \ No newline at end of file diff --git a/src/domain/services/group.py b/src/domain/services/group.py index cdbc19b..8714a65 100644 --- a/src/domain/services/group.py +++ b/src/domain/services/group.py @@ -11,7 +11,7 @@ class GroupService: def __init__(self, repo: GroupRepository): self._repo = repo - def create(self, *, + async def create(self, *, owner_id: UUID, name: str, description: Optional[str] = None) -> DomainGroup: @@ -26,64 +26,64 @@ def create(self, *, created_at=datetime.utcnow(), ) - return self._repo.add(group) + return await self._repo.add(group) - def delete(self, group_id: UUID): - group = self._repo.find_by_id(group_id) + async def delete(self, group_id: UUID): + group = await self._repo.find_by_id(group_id) if not group: raise NotFoundError(f"Group with id {group_id} not found") - self._repo.delete(group_id) + await self._repo.delete(group_id) - def update(self, group: DomainGroup) -> DomainGroup: - existing_group = self._repo.find_by_id(group.id) + async def update(self, group: DomainGroup) -> DomainGroup: + existing_group = await self._repo.find_by_id(group.id) if not existing_group: raise NotFoundError(f"Group with id {group.id} not found") - return self._repo.update(group) + return await self._repo.update(group) - def add_member(self, group_id: UUID, user_id: UUID) -> None: - group = self._repo.find_by_id(group_id) + async def add_member(self, group_id: UUID, user_id: UUID) -> None: + group = await self._repo.find_by_id(group_id) if not group: raise NotFoundError(f"Group with id {group_id} not found") - self._repo.add_member(group_id, user_id) + await self._repo.add_member(group_id, user_id) - def remove_member(self, group_id: UUID, user_id: UUID) -> None: - group = self._repo.find_by_id(group_id) + async def remove_member(self, group_id: UUID, user_id: UUID) -> None: + group = await self._repo.find_by_id(group_id) if not group: raise NotFoundError(f"Group with id {group_id} not found") - profiles = self._repo.list_members(group_id) + profiles = await self._repo.list_members(group_id) member_ids = [p.id for p in profiles] if user_id not in member_ids: raise NotFoundError(f"User with id {user_id} is not a member of group {group_id}") - self._repo.remove_member(group_id, user_id) + await self._repo.remove_member(group_id, user_id) - def list_owner_groups(self, owner_id: UUID) -> List[DomainGroup]: - groups = self._repo.find_by_owner_id(owner_id) + async def list_owner_groups(self, owner_id: UUID) -> List[DomainGroup]: + groups = await self._repo.find_by_owner_id(owner_id) if not groups: raise NotFoundError(f"No groups found for owner with id {owner_id}") return groups - def list_members(self, group_id: UUID) -> List[DomainProfile]: - group = self._repo.find_by_id(group_id) + async def list_members(self, group_id: UUID) -> List[DomainProfile]: + group = await self._repo.find_by_id(group_id) if not group: raise NotFoundError(f"Group with id {group_id} not found") - return self._repo.list_members(group_id) or [] + return await self._repo.list_members(group_id) or [] - def get_all_groups(self) -> List[DomainGroup]: - groups = self._repo.find_all_groups() + async def get_all_groups(self) -> List[DomainGroup]: + groups = await self._repo.find_all_groups() if not groups: raise NotFoundError("No groups found") return groups - def get_my_coaches(self, user_id: UUID) -> List[DomainProfile]: - groups = self._repo.find_groups_by_member_id(user_id) + async def get_my_coaches(self, user_id: UUID) -> List[DomainProfile]: + groups = await self._repo.find_groups_by_member_id(user_id) if not groups: raise NotFoundError(f"No groups found for user {user_id}") @@ -97,7 +97,7 @@ def get_my_coaches(self, user_id: UUID) -> List[DomainProfile]: coaches = [] for coach_id in coach_ids: try: - coach = profile_service._repo.find_by_id(coach_id) + coach = await profile_service._repo.find_by_id(coach_id) if coach and "coach" in coach.roles: coaches.append(coach) except NotFoundError: diff --git a/src/domain/services/profile.py b/src/domain/services/profile.py index 8be040c..e5c54ea 100644 --- a/src/domain/services/profile.py +++ b/src/domain/services/profile.py @@ -13,8 +13,7 @@ def __init__(self, repo: ProfileRepository, hasher: PasswordHasher): self._repo = repo self._hasher = hasher - - def create(self, + async def create(self, *, email: str, raw_password: str, @@ -33,7 +32,7 @@ def create(self, if not re.match(email_regex, email): raise InvalidFormatEmailError(f"Email {email} has invalid format") - if self._repo.find_by_email(email): + if await self._repo.find_by_email(email): raise DuplicateProfileError(f"Profile with email {email} already exists") if raw_password != confirm_password: @@ -56,23 +55,22 @@ def create(self, created_at=datetime.utcnow() ) - return self._repo.add(profile) + return await self._repo.add(profile) - def delete(self, profile_id: UUID): - profile = self._repo.find_by_id(profile_id) + async def delete(self, profile_id: UUID): + profile = await self._repo.find_by_id(profile_id) if not profile: raise NotFoundError(f"Profile with id {profile_id} not found") - - self._repo.delete(profile_id) - - def login(self, email: str, password: str) -> DomainProfile: - profile = self._repo.find_by_email(email) + await self._repo.delete(profile_id) + async def login(self, email: str, password: str) -> DomainProfile: email_regex = r"^[\w\.-]+@[\w\.-]+\.\w+$" if not re.match(email_regex, email): raise InvalidFormatEmailError(f"Email {email} has invalid format") + profile = await self._repo.find_by_email(email) + if not profile: raise AuthenticationError(f"Invalid password or email") @@ -81,28 +79,28 @@ def login(self, email: str, password: str) -> DomainProfile: return profile - def get_by_id(self, profile_id: UUID) -> DomainProfile: - profile = self._repo.find_by_id(profile_id) + async def get_by_id(self, profile_id: UUID) -> DomainProfile: + profile = await self._repo.find_by_id(profile_id) if not profile: raise NotFoundError(f"Profile with id {profile_id} not found") return profile - def get_all_users(self) -> List[DomainProfile]: - profiles = self._repo.find_all_users() + async def get_all_users(self) -> List[DomainProfile]: + profiles = await self._repo.find_all_users() if not profiles: raise NotFoundError("No profiles found") return profiles - def get_all_coachs(self) -> List[DomainProfile]: - profiles = self._repo.find_all_coachs() + async def get_all_coachs(self) -> List[DomainProfile]: + profiles = await self._repo.find_all_coachs() if not profiles: raise NotFoundError("No profiles found") return profiles - def update(self, + async def update(self, id: UUID, *, name: Optional[str] = None, @@ -114,7 +112,7 @@ def update(self, legacy: Optional[str] = None, roles: Optional[List[str]] = None ) -> DomainProfile: - profile = self.get_by_id(id) + profile = await self.get_by_id(id) for attr, val in { "name": name, "sex": sex, "age": age, @@ -125,25 +123,25 @@ def update(self, if val is not None: setattr(profile, attr, val) - return self._repo.update(profile) + return await self._repo.update(profile) - def update_email(self, id: UUID, new_email: str) -> DomainProfile: - profile = self.get_by_id(id) - if new_email != profile.email and self._repo.find_by_email(new_email): + async def update_email(self, id: UUID, new_email: str) -> DomainProfile: + profile = await self.get_by_id(id) + if new_email != profile.email and await self._repo.find_by_email(new_email): raise DuplicateProfileError(f"Email {new_email} already use") profile.email = new_email - return self._repo.update(profile) + return await self._repo.update(profile) - def update_password(self, id: UUID, old_password: str, new_password: str) -> None: - profile = self.get_by_id(id) + async def update_password(self, id: UUID, old_password: str, new_password: str) -> None: + profile = await self.get_by_id(id) if not self._hasher.verify(old_password, profile.password): raise AuthenticationError("Wrong password") profile.password = self._hasher.hash(new_password) - self._repo.update(profile) + await self._repo.update(profile) - def update_roles(self, id: UUID, roles: List[str]) -> DomainProfile: - profile = self.get_by_id(id) + async def update_roles(self, id: UUID, roles: List[str]) -> DomainProfile: + profile = await self.get_by_id(id) if not roles: raise ValueError("Roles cannot be empty") profile.roles = roles - return self._repo.update(profile) + return await self._repo.update(profile) diff --git a/src/domain/services/training.py b/src/domain/services/training.py index b850b29..75bf44e 100644 --- a/src/domain/services/training.py +++ b/src/domain/services/training.py @@ -10,13 +10,13 @@ class TrainingService: def __init__(self, repo: TrainingRepository): self._repo = repo - def get_training(self, id: UUID) -> DomainTraining: - training = self._repo.find_by_id(id) + async def get_training(self, id: UUID) -> DomainTraining: + training = await self._repo.find_by_id(id) if not training: raise NotFoundError(f"Training {id} not found") return training - def create_training(self, owner_id: UUID, name: str, description: str) -> DomainTraining: + async def create_training(self, owner_id: UUID, name: str, description: str) -> DomainTraining: training = DomainTraining( id=uuid4(), owner_id=owner_id, @@ -24,21 +24,21 @@ def create_training(self, owner_id: UUID, name: str, description: str) -> Domain description=description, created_at=datetime.utcnow() ) - return self._repo.add_training(training) + return await self._repo.add_training(training) - def delete_training(self, id: UUID) -> None: - training = self._repo.find_by_id(id) + async def delete_training(self, id: UUID) -> None: + training = await self._repo.find_by_id(id) if not training: raise NotFoundError(f"Training {id} not found") - self._repo.delete_training(id) + await self._repo.delete_training(id) - def update_training( + async def update_training( self, training_id: UUID, name: Optional[str] = None, description: Optional[str] = None, ) -> DomainTraining: - training = self._repo.find_by_id(training_id) + training = await self._repo.find_by_id(training_id) if not training: raise NotFoundError(f"Training {training_id} not found") @@ -47,17 +47,17 @@ def update_training( if description is not None: training.description = description - return self._repo.update_training(training) + return await self._repo.update_training(training) - def get_all_owner_trainings(self, owner_id: UUID) -> List[DomainTraining]: - trainings = self._repo.find_all_owner_trainings(owner_id) + async def get_all_owner_trainings(self, owner_id: UUID) -> List[DomainTraining]: + trainings = await self._repo.find_all_owner_trainings(owner_id) if not trainings: return [] return trainings # tasks methods service - def create_task( + async def create_task( self, training_id: UUID, exercise_name: str, @@ -67,7 +67,7 @@ def create_task( method: Optional[str] = None, rir: Optional[int] = None, ) -> DomainTask: - training = self._repo.find_by_id(training_id) + training = await self._repo.find_by_id(training_id) if not training: raise NotFoundError(f"Training {training_id} not found") if not exercise_name: @@ -86,17 +86,15 @@ def create_task( validate=[], ) - return self._repo.add_task(task) + return await self._repo.add_task(task) - - def get_task(self, task_id: UUID) -> DomainTask: - task = self._repo.find_task_by_id(task_id) + async def get_task(self, task_id: UUID) -> DomainTask: + task = await self._repo.find_task_by_id(task_id) if not task: raise NotFoundError(f"Task {task_id} not found") return task - - def update_task( + async def update_task( self, task_id: UUID, exercise_name: Optional[str] = None, @@ -106,7 +104,7 @@ def update_task( method: Optional[str] = None, rir: Optional[int] = None, ) -> DomainTask: - task = self._repo.find_task_by_id(task_id) + task = await self._repo.find_task_by_id(task_id) if not task: raise NotFoundError(f"Task {task_id} not found") @@ -123,26 +121,24 @@ def update_task( if rir is not None: task.rir = rir - return self._repo.update_task(task) + return await self._repo.update_task(task) - - def delete_task(self, task_id: UUID) -> None: - task = self._repo.find_task_by_id(task_id) + async def delete_task(self, task_id: UUID) -> None: + task = await self._repo.find_task_by_id(task_id) if not task: raise NotFoundError(f"Task {task_id} not found") - self._repo.delete_task(task_id) - + await self._repo.delete_task(task_id) - def list_tasks_for_training(self, training_id: UUID) -> List[DomainTask]: - tasks = self._repo.find_tasks_by_training_id(training_id) + async def list_tasks_for_training(self, training_id: UUID) -> List[DomainTask]: + tasks = await self._repo.find_tasks_by_training_id(training_id) if tasks is None: return [] return tasks # validate methods service - def create_validate( + async def create_validate( self, task_id: UUID, rest_time: Optional[int] = None, @@ -150,37 +146,36 @@ def create_validate( set_number: Optional[int] = None, rir: Optional[int] = None, ) -> DomainValidate: - task = self._repo.find_task_by_id(task_id) + task = await self._repo.find_task_by_id(task_id) if not task: raise NotFoundError(f"Task {task_id} not found") validate = DomainValidate( id=uuid4(), task_id=task_id, - exercise_name=task.exercise_name, # Assuming exercise_name is needed rest_time=rest_time, repetitions=repetitions, set_number=set_number, rir=rir, updated_at=datetime.utcnow(), - succeeded_at=datetime.utcnow(), # This can be set later when validation is successful + succeeded_at=datetime.utcnow(), ) - return self._repo.add_validate(validate) + return await self._repo.add_validate(validate) - def get_validates_for_task(self, task_id: UUID) -> List[DomainValidate]: - validates = self._repo.find_validate_by_task_id(task_id) + async def get_validates_for_task(self, task_id: UUID) -> List[DomainValidate]: + validates = await self._repo.find_validate_by_task_id(task_id) if not validates: return [] return validates - def delete_validate(self, validate_id: UUID) -> None: - validate = self._repo.find_validate_by_id(validate_id) + async def delete_validate(self, validate_id: UUID) -> None: + validate = await self._repo.find_validate_by_id(validate_id) if not validate: raise NotFoundError(f"Validation {validate_id} not found") - self._repo.delete_validate(validate_id) + await self._repo.delete_validate(validate_id) - def get_validate_by_training_id(self, training_id: UUID) -> List[DomainValidate]: - if not self._repo.find_by_id(training_id): + async def get_validate_by_training_id(self, training_id: UUID) -> List[DomainValidate]: + if not await self._repo.find_by_id(training_id): raise NotFoundError(f"Training {training_id} not found") - return self._repo.find_all_validates_by_training_id(training_id) \ No newline at end of file + return await self._repo.find_all_validates_by_training_id(training_id) \ No newline at end of file diff --git a/src/entrypoints/api/deps/auth.py b/src/entrypoints/api/deps/auth.py index ce34b5f..e4515de 100644 --- a/src/entrypoints/api/deps/auth.py +++ b/src/entrypoints/api/deps/auth.py @@ -40,14 +40,14 @@ def require_owner_or_admin( ) return user -def require_group_owner_or_admin( +async def require_group_owner_or_admin( group_id: UUID, user: UserPayload = Depends(get_current_user), ) -> UserPayload: svc = container.get_group_service() try: - group = svc._repo.find_by_id(group_id) + group = await svc._repo.find_by_id(group_id) except Exception: group = None @@ -64,16 +64,16 @@ def require_group_owner_or_admin( detail="Access denied: not owner or admin" ) - return group + return user -def require_exercice_owner_or_admin( +async def require_exercice_owner_or_admin( exercise_id: UUID, user: UserPayload = Depends(get_current_user), ) -> UserPayload: svc = container.get_exercise_service() try: - exercise = svc._repo.find_by_id(exercise_id) + exercise = await svc._repo.find_by_id(exercise_id) except Exception: exercise = None @@ -89,16 +89,16 @@ def require_exercice_owner_or_admin( detail="Access denied: not owner or admin" ) - return exercise + return user -def require_coach_for_user_or_admin( +async def require_coach_for_user_or_admin( target_user_id: UUID, user: UserPayload = Depends(get_current_user), ) -> UserPayload: profile_svc = container.get_profile_service() try: - target_profile = profile_svc.get_by_id(target_user_id) + target_profile = await profile_svc.get_by_id(target_user_id) except NotFoundError: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -109,7 +109,7 @@ def require_coach_for_user_or_admin( user_sub = user.get("sub") if "admin" in roles: - return target_profile + return user if "coach" not in roles: raise HTTPException( @@ -119,17 +119,17 @@ def require_coach_for_user_or_admin( group_svc = container.get_group_service() try: - coach_groups = group_svc.list_owner_groups(UUID(user_sub)) + coach_groups = await group_svc.list_owner_groups(UUID(user_sub)) except NotFoundError: coach_groups = [] for grp in coach_groups: try: - members = group_svc.list_members(grp.id) + members = await group_svc.list_members(grp.id) except NotFoundError: continue if any(m.id == target_user_id for m in members): - return target_profile + return user raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, @@ -137,7 +137,7 @@ def require_coach_for_user_or_admin( ) -def require_training_owner_or_coach_or_admin( +async def require_training_owner_or_coach_or_admin( training_id: UUID, user: UserPayload = Depends(get_current_user), ) -> UserPayload: @@ -148,33 +148,44 @@ def require_training_owner_or_coach_or_admin( if "admin" in roles: return user - if "coach" in roles: - return user - svc = container.get_training_service() try: - training = svc.get_training(training_id) + training = await svc.get_training(training_id) except NotFoundError: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Training {training_id} not found" ) - if str(training.owner_id) != sub: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Access denied: not owner, coach, or admin" - ) + if str(training.owner_id) == sub: + return user - return user + if "coach" in roles: + group_svc = container.get_group_service() + try: + coach_groups = await group_svc.list_owner_groups(UUID(sub)) + for grp in coach_groups: + try: + members = await group_svc.list_members(grp.id) + if any(m.id == training.owner_id for m in members): + return user + except NotFoundError: + continue + except NotFoundError: + pass + + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Access denied: not owner, coach, or admin" + ) -def require_training_owner_or_admin( +async def require_training_owner_or_admin( training_id: UUID, user: dict = Depends(get_current_user), ): svc = container.get_training_service() try: - training = svc.get_training(training_id) + training = await svc.get_training(training_id) except NotFoundError: raise HTTPException(status.HTTP_404_NOT_FOUND, f"Training {training_id} not found") @@ -183,17 +194,17 @@ def require_training_owner_or_admin( if str(training.owner_id) != sub and "admin" not in roles: raise HTTPException(status.HTTP_403_FORBIDDEN, "Access denied: not owner, or admin") - return training + return user -def require_owner_coach_for_user_or_admin( +async def require_owner_coach_for_user_or_admin( target_user_id: UUID, user: UserPayload = Depends(get_current_user), ): profile_svc = container.get_profile_service() try: - target_profile = profile_svc.get_by_id(target_user_id) + target_profile = await profile_svc.get_by_id(target_user_id) except NotFoundError: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -204,11 +215,9 @@ def require_owner_coach_for_user_or_admin( sub = user.get("sub") if sub == str(target_user_id): - return target_profile + return user if "admin" in roles: - return target_profile + return user - from src.entrypoints.api.deps.auth import require_coach_for_user_or_admin - - return require_coach_for_user_or_admin(target_user_id, user) \ No newline at end of file + return await require_coach_for_user_or_admin(target_user_id, user) \ No newline at end of file diff --git a/src/entrypoints/api/deps/roles.py b/src/entrypoints/api/deps/roles.py index 9f3f03d..5ad3a24 100644 --- a/src/entrypoints/api/deps/roles.py +++ b/src/entrypoints/api/deps/roles.py @@ -5,7 +5,7 @@ def require_roles(*allowed_roles: str): - def dependency(user: dict = Depends(get_current_user)) -> dict: + async def dependency(user: dict = Depends(get_current_user)) -> dict: user_roles: List[str] = user.get("roles", []) if not any(role in user_roles for role in allowed_roles): raise HTTPException(status_code=403, detail="Access forbidden") diff --git a/src/entrypoints/api/routers/diet.py b/src/entrypoints/api/routers/diet.py index 99f4967..dc8dcde 100644 --- a/src/entrypoints/api/routers/diet.py +++ b/src/entrypoints/api/routers/diet.py @@ -12,27 +12,27 @@ router = APIRouter(prefix="/diets", tags=["diets"]) @router.get("/mine", response_model=List[DietRead], dependencies=[Depends(get_current_user)]) -def get_my_diets(user=Depends(get_current_user)): +async def get_my_diets(user=Depends(get_current_user)): svc = container.get_diet_service() owner_id = UUID(user["sub"]) - diets = svc.list_owner_diets(owner_id) + diets = await svc.list_owner_diets(owner_id) return [DietRead.model_validate(d) for d in diets] @router.get("/user/{target_user_id}", response_model=List[DietRead], dependencies=[Depends(require_coach_for_user_or_admin)]) -def get_user_diets(target_user_id: UUID): +async def get_user_diets(target_user_id: UUID): svc = container.get_diet_service() - diets = svc.list_owner_diets(target_user_id) + diets = await svc.list_owner_diets(target_user_id) return [DietRead.model_validate(d) for d in diets] @router.post("/{target_user_id}", response_model=DietRead, status_code=status.HTTP_201_CREATED) -def create_diet(target_user_id: UUID, +async def create_diet(target_user_id: UUID, dto: DietCreate, _=Depends(require_coach_for_user_or_admin)): svc = container.get_diet_service() try: - d = svc.create_diet( + d = await svc.create_diet( owner_id=target_user_id, name=dto.name, description=dto.description or "", @@ -42,13 +42,13 @@ def create_diet(target_user_id: UUID, return DietRead.model_validate(d) @router.patch("/{diet_id}/user/{target_user_id}", response_model=DietRead) -def update_diet(diet_id: UUID, +async def update_diet(diet_id: UUID, target_user_id: UUID, dto: DietUpdate, _=Depends(require_coach_for_user_or_admin)): svc = container.get_diet_service() try: - updated = svc.update_diet( + updated = await svc.update_diet( diet_id=diet_id, name=dto.name, description=dto.description @@ -58,11 +58,11 @@ def update_diet(diet_id: UUID, return DietRead.model_validate(updated) @router.delete("/{diet_id}/user/{target_user_id}", status_code=status.HTTP_204_NO_CONTENT) -def delete_diet(diet_id: UUID, +async def delete_diet(diet_id: UUID, _=Depends(require_coach_for_user_or_admin)): svc = container.get_diet_service() try: - svc.delete_diet(diet_id) + await svc.delete_diet(diet_id) except NotFoundError: raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="Diet not found") @@ -73,9 +73,9 @@ def delete_diet(diet_id: UUID, response_model=List[MacroPlanRead], dependencies=[Depends(require_owner_coach_for_user_or_admin)] ) -def list_macro_plans(diet_id: UUID, target_user_id: UUID): +async def list_macro_plans(diet_id: UUID, target_user_id: UUID): svc = container.get_diet_service() - plans = svc.get_macro_plans_for_diet(diet_id) + plans = await svc.get_macro_plans_for_diet(diet_id) return [MacroPlanRead.model_validate(p) for p in plans] @router.get( @@ -83,10 +83,10 @@ def list_macro_plans(diet_id: UUID, target_user_id: UUID): response_model=MacroPlanRead, dependencies=[Depends(require_owner_coach_for_user_or_admin)] ) -def get_macro_plan(diet_id: UUID, target_user_id: UUID, plan_id: UUID): +async def get_macro_plan(diet_id: UUID, target_user_id: UUID, plan_id: UUID): svc = container.get_diet_service() try: - p = svc.get_macro_plan(plan_id) + p = await svc.get_macro_plan(plan_id) except NotFoundError: raise HTTPException(HTTP_404_NOT_FOUND, f"MacroPlan {plan_id} not found") return MacroPlanRead.model_validate(p) @@ -97,14 +97,14 @@ def get_macro_plan(diet_id: UUID, target_user_id: UUID, plan_id: UUID): status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_coach_for_user_or_admin)] ) -def create_macro_plan( +async def create_macro_plan( diet_id: UUID, target_user_id: UUID, dto: MacroPlanCreate ): svc = container.get_diet_service() try: - p = svc.create_macro_plan( + p = await svc.create_macro_plan( diet_id=diet_id, name=dto.name, carbohydrates=dto.carbohydrates, @@ -119,16 +119,17 @@ def create_macro_plan( return MacroPlanRead.model_validate(p) @router.get("/macro_plans/mine", response_model=List[MacroPlanRead], dependencies=[Depends(get_current_user)]) -def list_my_macro_plans(user: UserPayload = Depends(get_current_user)): +async def list_my_macro_plans(user: UserPayload = Depends(get_current_user)): svc = container.get_diet_service() - return [MacroPlanRead.model_validate(p) for p in svc.get_macro_plans_by_user_id(user["sub"])] + plans = await svc.get_macro_plans_by_user_id(UUID(user["sub"])) + return [MacroPlanRead.model_validate(p) for p in plans] @router.patch( "/{diet_id}/user/{target_user_id}/macro_plans/{plan_id}", response_model=MacroPlanRead, dependencies=[Depends(require_coach_for_user_or_admin)] ) -def update_macro_plan( +async def update_macro_plan( diet_id: UUID, target_user_id: UUID, plan_id: UUID, @@ -136,7 +137,7 @@ def update_macro_plan( ): svc = container.get_diet_service() try: - updated = svc.update_macro_plan( + updated = await svc.update_macro_plan( plan_id=plan_id, name=dto.name, carbohydrates=dto.carbohydrates, @@ -155,10 +156,10 @@ def update_macro_plan( status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_coach_for_user_or_admin)] ) -def delete_macro_plan(diet_id: UUID, target_user_id: UUID, plan_id: UUID): +async def delete_macro_plan(diet_id: UUID, target_user_id: UUID, plan_id: UUID): svc = container.get_diet_service() try: - svc.delete_macro_plan(plan_id) + await svc.delete_macro_plan(plan_id) except NotFoundError: raise HTTPException(HTTP_404_NOT_FOUND, "MacroPlan not found") @@ -169,9 +170,9 @@ def delete_macro_plan(diet_id: UUID, target_user_id: UUID, plan_id: UUID): response_model=List[MealPlanRead], dependencies=[Depends(require_owner_coach_for_user_or_admin)] ) -def list_meal_plans(diet_id: UUID, target_user_id: UUID): +async def list_meal_plans(diet_id: UUID, target_user_id: UUID): svc = container.get_diet_service() - plans = svc.get_meal_plans_by_diet(diet_id) + plans = await svc.get_meal_plans_by_diet(diet_id) return [MealPlanRead.model_validate(p) for p in plans] @router.get( @@ -179,10 +180,10 @@ def list_meal_plans(diet_id: UUID, target_user_id: UUID): response_model=MealPlanRead, dependencies=[Depends(require_owner_coach_for_user_or_admin)] ) -def get_meal_plan(diet_id: UUID, target_user_id: UUID, plan_id: UUID): +async def get_meal_plan(diet_id: UUID, target_user_id: UUID, plan_id: UUID): svc = container.get_diet_service() try: - mp = svc.get_meal_plan_by_id(plan_id) + mp = await svc.get_meal_plan_by_id(plan_id) except NotFoundError: raise HTTPException( status_code=HTTP_404_NOT_FOUND, @@ -196,14 +197,14 @@ def get_meal_plan(diet_id: UUID, target_user_id: UUID, plan_id: UUID): status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_coach_for_user_or_admin)] ) -def create_meal_plan( +async def create_meal_plan( diet_id: UUID, target_user_id: UUID, dto: MealPlanCreate ): svc = container.get_diet_service() try: - mp = svc.create_meal_plan( + mp = await svc.create_meal_plan( diet_id=diet_id, name=dto.name, meals=dto.meals, @@ -217,10 +218,10 @@ def create_meal_plan( response_model=List[MealPlanRead], dependencies=[Depends(get_current_user)] ) -def list_my_meal_plans(user = Depends(get_current_user)): +async def list_my_meal_plans(user = Depends(get_current_user)): svc = container.get_diet_service() user_id = UUID(user["sub"]) - plans = svc.get_meal_plans_by_user(user_id) + plans = await svc.get_meal_plans_by_user(user_id) return [MealPlanRead.model_validate(p) for p in plans] @router.patch( @@ -228,7 +229,7 @@ def list_my_meal_plans(user = Depends(get_current_user)): response_model=MealPlanRead, dependencies=[Depends(require_coach_for_user_or_admin)] ) -def update_meal_plan( +async def update_meal_plan( diet_id: UUID, target_user_id: UUID, plan_id: UUID, @@ -236,7 +237,7 @@ def update_meal_plan( ): svc = container.get_diet_service() try: - updated = svc.update_meal_plan( + updated = await svc.update_meal_plan( plan_id=plan_id, name=dto.name, meals=dto.meals, @@ -253,10 +254,10 @@ def update_meal_plan( status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_coach_for_user_or_admin)] ) -def delete_meal_plan(diet_id: UUID, target_user_id: UUID, plan_id: UUID): +async def delete_meal_plan(diet_id: UUID, target_user_id: UUID, plan_id: UUID): svc = container.get_diet_service() try: - svc.delete_meal_plan(plan_id) + await svc.delete_meal_plan(plan_id) except NotFoundError: raise HTTPException( status_code=HTTP_404_NOT_FOUND, diff --git a/src/entrypoints/api/routers/exercise.py b/src/entrypoints/api/routers/exercise.py index c4c8f13..582b38b 100644 --- a/src/entrypoints/api/routers/exercise.py +++ b/src/entrypoints/api/routers/exercise.py @@ -13,14 +13,14 @@ router = APIRouter(prefix="/exercises", tags=["exercises"]) @router.post("", response_model=ExerciseRead, status_code=201, dependencies=[Depends(require_roles("admin", "coach"))]) -def create_exercise( +async def create_exercise( dto: ExerciseCreate, user=Depends(get_current_user) ): service = container.get_exercise_service() try: - exercise = service.create_exercise( + exercise = await service.create_exercise( owner_id=UUID(user["sub"]), name=dto.name, description=dto.description @@ -31,55 +31,55 @@ def create_exercise( return ExerciseRead.model_validate(exercise) @router.get("", response_model=List[ExerciseRead], dependencies=[Depends(get_current_user)]) -def get_exercises( +async def get_exercises( user=Depends(get_current_user) ): service = container.get_exercise_service() try: - exercises = service.get_all_exercises() + exercises = await service.get_all_exercises() except NotFoundError: raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="No exercises found") return [ExerciseRead.model_validate(exercise) for exercise in exercises] @router.get("/mine", response_model=List[ExerciseRead], dependencies=[Depends(require_roles("admin", "coach"))]) -def get_my_exercises( +async def get_my_exercises( user=Depends(get_current_user) ): service = container.get_exercise_service() owner_id = UUID(user["sub"]) try: - exercises = service.get_exercises_mine(owner_id) + exercises = await service.get_exercises_mine(owner_id) except NotFoundError: raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="No exercises found for your account") return [ExerciseRead.model_validate(exercise) for exercise in exercises] @router.get("/{exercise_id}", response_model=ExerciseRead, dependencies=[Depends(get_current_user)]) -def get_exercise( +async def get_exercise( exercise_id: UUID, user=Depends(get_current_user) ): service = container.get_exercise_service() try: - exercise = service._repo.find_by_id(exercise_id) + exercise = await service._repo.find_by_id(exercise_id) except NotFoundError: raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="Exercise not found") return ExerciseRead.model_validate(exercise) @router.patch("/{exercise_id}", response_model=ExerciseRead, dependencies=[Depends(require_exercice_owner_or_admin)]) -def update_exercise( +async def update_exercise( exercise_id: UUID, dto: ExerciseUpdate, user=Depends(get_current_user) ): service = container.get_exercise_service() try: - updated = service.update_exercise(exercise_id, name=dto.name, description=dto.description) + updated = await service.update_exercise(exercise_id, name=dto.name, description=dto.description) except NotFoundError: raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="Exercise not found") except ValueError as e: @@ -88,14 +88,14 @@ def update_exercise( return ExerciseRead.model_validate(updated) @router.delete("/{exercise_id}", status_code=204, dependencies=[Depends(require_exercice_owner_or_admin)]) -def delete_exercise( +async def delete_exercise( exercise_id: UUID, user=Depends(get_current_user) ): service = container.get_exercise_service() try: - service.delete_exercise(exercise_id) + await service.delete_exercise(exercise_id) except NotFoundError: raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="Exercise not found") - + diff --git a/src/entrypoints/api/routers/group.py b/src/entrypoints/api/routers/group.py index cc44655..1e9d00c 100644 --- a/src/entrypoints/api/routers/group.py +++ b/src/entrypoints/api/routers/group.py @@ -15,36 +15,36 @@ router = APIRouter(prefix="/groups", tags=["groups"]) @router.post("", response_model=GroupRead, status_code=201, dependencies=[Depends(require_roles("admin", "coach"))]) -def create_group( +async def create_group( dto: GroupCreate, user=Depends(get_current_user) ): service = container.get_group_service() - grp = service.create(owner_id=UUID(user["sub"]), name=dto.name, description=dto.description) + grp = await service.create(owner_id=UUID(user["sub"]), name=dto.name, description=dto.description) return GroupRead.model_validate(grp) @router.get("", response_model=List[GroupRead], dependencies=[Depends(get_current_user)]) -def list_groups(): +async def list_groups(): service = container.get_group_service() try: - groups = service.get_all_groups() + groups = await service.get_all_groups() except NotFoundError as e: raise HTTPException(404, str(e)) return [GroupRead.model_validate(g) for g in groups] @router.get("/{group_id}", response_model=GroupRead, dependencies=[Depends(get_current_user)]) -def get_group(group_id: UUID): +async def get_group(group_id: UUID): service = container.get_group_service() - grp = service._repo.find_by_id(group_id) + grp = await service._repo.find_by_id(group_id) if not grp: raise HTTPException(404, "Group not found") return GroupRead.model_validate(grp) @router.patch("/{group_id}", response_model=GroupRead, dependencies=[Depends(require_group_owner_or_admin)]) -def patch_group(group_id: UUID, dto: GroupUpdate): +async def patch_group(group_id: UUID, dto: GroupUpdate): service = container.get_group_service() - existing = service._repo.find_by_id(group_id) + existing = await service._repo.find_by_id(group_id) if not existing: raise HTTPException(404, "Group not found") updated = existing @@ -52,65 +52,65 @@ def patch_group(group_id: UUID, dto: GroupUpdate): updated.name = dto.name if dto.description is not None: updated.description = dto.description - grp = service.update(updated) + grp = await service.update(updated) return GroupRead.model_validate(grp) @router.delete("/{group_id}", status_code=204, dependencies=[Depends(require_group_owner_or_admin)]) -def delete_group(group_id: UUID): +async def delete_group(group_id: UUID): service = container.get_group_service() try: - service.delete(group_id) + await service.delete(group_id) except NotFoundError as e: raise HTTPException(404, str(e)) @router.post("/{group_id}/members/{user_id}", status_code=204, dependencies=[Depends(require_group_owner_or_admin)]) -def add_member(group_id: UUID, user_id: UUID): +async def add_member(group_id: UUID, user_id: UUID): service = container.get_group_service() try: - service.add_member(group_id, user_id) + await service.add_member(group_id, user_id) except (NotFoundError) as e: raise HTTPException(404, str(e)) @router.delete("/{group_id}/members/{user_id}", status_code=204, dependencies=[Depends(require_group_owner_or_admin)]) -def remove_member(group_id: UUID, user_id: UUID): +async def remove_member(group_id: UUID, user_id: UUID): service = container.get_group_service() try: - service.remove_member(group_id, user_id) + await service.remove_member(group_id, user_id) except (NotFoundError) as e: raise HTTPException(404, str(e)) @router.get("/{group_id}/members", response_model=List[GroupMember], dependencies=[Depends(require_group_owner_or_admin)]) -def list_members(group_id: UUID): +async def list_members(group_id: UUID): service = container.get_group_service() try: - members = service.list_members(group_id) + members = await service.list_members(group_id) except NotFoundError as e: raise HTTPException(404, str(e)) return [GroupMember.model_validate(p) for p in members] @router.get("/owner/{owner_id}", response_model=List[GroupRead], dependencies=[Depends(get_current_user)]) -def list_owner_groups(owner_id: UUID): +async def list_owner_groups(owner_id: UUID): service = container.get_group_service() try: - groups = service.list_owner_groups(owner_id) + groups = await service.list_owner_groups(owner_id) except NotFoundError as e: raise HTTPException(404, str(e)) return [GroupRead.model_validate(g) for g in groups] @router.delete("/{group_id}/leave", status_code=204) -def leave_group(group_id: UUID, user=Depends(get_current_user)): +async def leave_group(group_id: UUID, user=Depends(get_current_user)): service = container.get_group_service() try: - service.remove_member(group_id, UUID(user["sub"])) + await service.remove_member(group_id, UUID(user["sub"])) except NotFoundError as e: raise HTTPException(404, str(e)) @router.get("/coachs/mine", response_model=List[CoachProfileRead]) -def get_my_coaches(user=Depends(get_current_user)): +async def get_my_coaches(user=Depends(get_current_user)): service = container.get_group_service() try: - coaches = service.get_my_coaches(UUID(user["sub"])) + coaches = await service.get_my_coaches(UUID(user["sub"])) return [CoachProfileRead.model_validate(coach) for coach in coaches] except NotFoundError as e: raise HTTPException(404, str(e)) - + diff --git a/src/entrypoints/api/routers/profile.py b/src/entrypoints/api/routers/profile.py index 7f58139..a75820b 100644 --- a/src/entrypoints/api/routers/profile.py +++ b/src/entrypoints/api/routers/profile.py @@ -22,7 +22,7 @@ async def create_profile( service = container.get_profile_service() try: - profile = service.create( + profile = await service.create( email=dto.email, raw_password=dto.password, confirm_password=dto.confirm_password, @@ -55,7 +55,7 @@ async def create_profile( async def get_all_user_profiles(): service = container.get_profile_service() try: - profiles = service.get_all_users() + profiles = await service.get_all_users() except NotFoundError as e: raise HTTPException(status_code=404, detail=str(e)) @@ -65,7 +65,7 @@ async def get_all_user_profiles(): async def get_all_coach_profiles(): service = container.get_profile_service() try: - profiles = service.get_all_coachs() + profiles = await service.get_all_coachs() except NotFoundError as e: raise HTTPException(status_code=404, detail=str(e)) @@ -78,7 +78,7 @@ async def login( ): service = container.get_profile_service() try: - profile = service.login(email=dto.email, password=dto.password) + profile = await service.login(email=dto.email, password=dto.password) except AuthenticationError as e: raise HTTPException(status_code=401, detail=str(e)) except InvalidFormatEmailError as e: @@ -105,7 +105,7 @@ async def get_me( service = container.get_profile_service() try: - profile = service.get_by_id(UUID(user["sub"])) + profile = await service.get_by_id(UUID(user["sub"])) except NotFoundError: raise HTTPException( status_code=HTTP_404_NOT_FOUND, @@ -125,7 +125,7 @@ async def patch_profile( update_data = dto.model_dump(exclude_none=True, by_alias=True) try: - updated = service.update(profile_id, **update_data) + updated = await service.update(profile_id, **update_data) except NotFoundError as e: raise HTTPException(status_code=404, detail=str(e)) @@ -135,7 +135,7 @@ async def patch_profile( async def get_user_profile(profile_id: UUID): service = container.get_profile_service() try: - profile = service.get_by_id(profile_id) + profile = await service.get_by_id(profile_id) except NotFoundError as e: raise HTTPException(status_code=404, detail=str(e)) return ProfileRead.model_validate(profile) @@ -146,7 +146,7 @@ async def delete_profile( ): service = container.get_profile_service() try: - service.delete(profile_id) + await service.delete(profile_id) except NotFoundError as e: raise HTTPException(status_code=404, detail=str(e)) @@ -166,7 +166,7 @@ async def patch_email( ): service = container.get_profile_service() try: - updated = service.update_email(profile_id, dto.email) + updated = await service.update_email(profile_id, dto.email) except NotFoundError as e: raise HTTPException(status_code=404, detail=str(e)) except DuplicateProfileError as e: @@ -185,7 +185,7 @@ async def patch_password( ): service = container.get_profile_service() try: - service.update_password( + await service.update_password( profile_id, old_password=dto.old_password, new_password=dto.new_password @@ -210,7 +210,7 @@ async def patch_roles( ): service = container.get_profile_service() try: - updated = service.update_roles(profile_id, dto.roles) + updated = await service.update_roles(profile_id, dto.roles) except NotFoundError as e: raise HTTPException(status_code=404, detail=str(e)) except ValueError as e: diff --git a/src/entrypoints/api/routers/training.py b/src/entrypoints/api/routers/training.py index 1c47ee8..4f187c6 100644 --- a/src/entrypoints/api/routers/training.py +++ b/src/entrypoints/api/routers/training.py @@ -12,31 +12,31 @@ router = APIRouter(prefix="/trainings", tags=["trainings"]) @router.get("/mine", response_model=List[TrainingRead], dependencies=[Depends(get_current_user)]) -def get_my_trainings(user=Depends(get_current_user)): +async def get_my_trainings(user=Depends(get_current_user)): service = container.get_training_service() try: owner_id = UUID(user["sub"]) - trainings = service.get_all_owner_trainings(owner_id) + trainings = await service.get_all_owner_trainings(owner_id) except NotFoundError: raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail=f"Trainings for user {owner_id} not found") return [TrainingRead.model_validate(t) for t in trainings] @router.get("/user/{target_user_id}", response_model=List[TrainingRead], dependencies=[Depends(require_coach_for_user_or_admin)]) -def get_user_trainings(target_user_id: UUID, user=Depends(get_current_user)): +async def get_user_trainings(target_user_id: UUID, user=Depends(get_current_user)): service = container.get_training_service() try: - trainings = service.get_all_owner_trainings(target_user_id) + trainings = await service.get_all_owner_trainings(target_user_id) except NotFoundError: raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail=f"Trainings for user {target_user_id} not found") return [TrainingRead.model_validate(t) for t in trainings] @router.get("/{training_id}", response_model=TrainingRead, dependencies=[Depends(require_training_owner_or_coach_or_admin)]) -def get_training(training_id: UUID, user=Depends(get_current_user)): +async def get_training(training_id: UUID, user=Depends(get_current_user)): service = container.get_training_service() try: - training = service.get_training(training_id) + training = await service.get_training(training_id) except NotFoundError: raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail=f"Training {training_id} not found") @@ -45,11 +45,11 @@ def get_training(training_id: UUID, user=Depends(get_current_user)): @router.post("/{target_user_id}", response_model=TrainingRead, status_code=201) -def create_training(target_user_id: UUID, dto: TrainingCreate, _=Depends(require_coach_for_user_or_admin)): +async def create_training(target_user_id: UUID, dto: TrainingCreate, _=Depends(require_coach_for_user_or_admin)): service = container.get_training_service() try: - training = service.create_training( + training = await service.create_training( owner_id=target_user_id, name=dto.name, description=dto.description @@ -63,7 +63,7 @@ def create_training(target_user_id: UUID, dto: TrainingCreate, _=Depends(require @router.patch("/{training_id}/user/{target_user_id}", response_model=TrainingRead) -def update_training( +async def update_training( training_id: UUID, target_user_id: UUID, dto: TrainingUpdate, @@ -71,7 +71,7 @@ def update_training( ): service = container.get_training_service() try: - updated = service.update_training( + updated = await service.update_training( training_id=training_id, name=dto.name, description=dto.description, @@ -84,11 +84,11 @@ def update_training( return TrainingRead.model_validate(updated) @router.delete("/{training_id}/user/{target_user_id}", status_code=204) -def delete_training(training_id: UUID, user=Depends(require_coach_for_user_or_admin)): +async def delete_training(training_id: UUID, user=Depends(require_coach_for_user_or_admin)): service = container.get_training_service() try: - service.delete_training(training_id) + await service.delete_training(training_id) except NotFoundError: raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail=f"Training {training_id} not found") @@ -101,13 +101,13 @@ def delete_training(training_id: UUID, user=Depends(require_coach_for_user_or_ad "/{training_id}/tasks", response_model=List[TaskRead], ) -def list_tasks( +async def list_tasks( training_id: UUID, _ = Depends(require_training_owner_or_coach_or_admin), ): service = container.get_training_service() try: - tasks = service.list_tasks_for_training(training_id) + tasks = await service.list_tasks_for_training(training_id) except NotFoundError: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -122,14 +122,14 @@ def list_tasks( status_code=status.HTTP_201_CREATED, summary="Créer une tâche dans un training", ) -def create_task( +async def create_task( training_id: UUID, dto: TaskCreate, _ = Depends(require_coach_for_user_or_admin), ): service = container.get_training_service() try: - task = service.create_task( + task = await service.create_task( training_id=training_id, exercise_name=dto.exercise_name, rest_time=dto.rest_time, @@ -151,14 +151,14 @@ def create_task( response_model=TaskRead, summary="Récupérer une tâche", ) -def get_task( +async def get_task( training_id: UUID, task_id: UUID, _ = Depends(require_training_owner_or_coach_or_admin), ): service = container.get_training_service() try: - task = service.get_task(task_id) + task = await service.get_task(task_id) except NotFoundError: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -172,7 +172,7 @@ def get_task( response_model=TaskRead, summary="Mettre à jour une tâche", ) -def update_task( +async def update_task( training_id: UUID, task_id: UUID, dto: TaskUpdate, @@ -180,7 +180,7 @@ def update_task( ): service = container.get_training_service() try: - updated = service.update_task( + updated = await service.update_task( task_id=task_id, exercise_name=dto.exercise_name, rest_time=dto.rest_time, @@ -202,14 +202,14 @@ def update_task( status_code=status.HTTP_204_NO_CONTENT, summary="Supprimer une tâche", ) -def delete_task( +async def delete_task( training_id: UUID, task_id: UUID, _ = Depends(require_coach_for_user_or_admin), ): service = container.get_training_service() try: - service.delete_task(task_id) + await service.delete_task(task_id) except NotFoundError: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -225,7 +225,7 @@ def delete_task( status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_training_owner_or_admin)] ) -def create_validation( +async def create_validation( training_id: UUID, task_id: UUID, dto: ValidateCreate, @@ -233,7 +233,7 @@ def create_validation( ): service = container.get_training_service() try: - v = service.create_validate( + v = await service.create_validate( task_id=task_id, rest_time=dto.rest_time, repetitions=dto.repetitions, @@ -251,14 +251,14 @@ def create_validation( response_model=List[ValidateRead], dependencies=[Depends(require_training_owner_or_coach_or_admin)] ) -def list_validations( +async def list_validations( training_id: UUID, task_id: UUID, user=Depends(get_current_user), ): service = container.get_training_service() try: - return service.get_validates_for_task(task_id) + return await service.get_validates_for_task(task_id) except NotFoundError: return [] @@ -267,7 +267,7 @@ def list_validations( status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_training_owner_or_admin)] ) -def delete_validation( +async def delete_validation( training_id: UUID, task_id: UUID, validation_id: UUID, @@ -275,7 +275,7 @@ def delete_validation( ): service = container.get_training_service() try: - service.delete_validate(validation_id) + await service.delete_validate(validation_id) except NotFoundError as e: raise HTTPException(status.HTTP_404_NOT_FOUND, detail=str(e)) @@ -285,12 +285,12 @@ def delete_validation( response_model=List[ValidateRead], dependencies=[Depends(require_training_owner_or_coach_or_admin)] ) -def get_validations_by_training( +async def get_validations_by_training( training_id: UUID, user=Depends(get_current_user), ): service = container.get_training_service() try: - return service.get_validate_by_training_id(training_id) + return await service.get_validate_by_training_id(training_id) except NotFoundError: return [] \ No newline at end of file diff --git a/src/entrypoints/api/tests/conftest.py b/src/entrypoints/api/tests/conftest.py index 100c352..10232ad 100644 --- a/src/entrypoints/api/tests/conftest.py +++ b/src/entrypoints/api/tests/conftest.py @@ -1,36 +1,35 @@ -import pytest +# app/adapters/sqlalchemy/repositories/postgres.py +from src.adapters.sqlalchemy.models import Base +from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession +from dotenv import load_dotenv import os -from httpx import AsyncClient, ASGITransport -from src.main import app -from src.container import Container -import pytest_asyncio - - -@pytest.fixture(scope="session", autouse=True) -def set_test_env(): - os.environ["ENV"] = "test" - yield - - -@pytest.fixture(scope="module", autouse=True) -def container(): - c = Container(env="test") - - assert os.getenv("ENV") == "test", "L'environnement n'est pas correctement configuré sur 'test'." - - return c - - -@pytest_asyncio.fixture -async def client(container): - - transport = ASGITransport(app=app) - - async with AsyncClient(transport=transport, base_url="http://testserver") as ac: - yield ac - -@pytest.fixture(scope="session") -def test_state(): - - return {} \ No newline at end of file +load_dotenv() +db_url = os.getenv("DATABASE_URL") or "postgresql://user:user@localhost:5432/postgres" + +# Convert sync URL to async URL for PostgreSQL +if db_url.startswith("postgresql://"): + db_url = db_url.replace("postgresql://", "postgresql+asyncpg://", 1) + +engine = create_async_engine( + db_url, + pool_size=20, + max_overflow=20, + pool_timeout=30, +) + +# Create tables - note: this will need to be called in an async context +async def create_tables(): + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + +SessionLocal = async_sessionmaker( + engine, + class_=AsyncSession, + autocommit=False, + autoflush=False +) + +async def get_async_session() -> AsyncSession: + async with SessionLocal() as session: + yield session diff --git a/src/main.py b/src/main.py index 9aa0194..5f9e904 100644 --- a/src/main.py +++ b/src/main.py @@ -6,6 +6,7 @@ from src.entrypoints.api.routers.exercise import router as exercise_router from src.entrypoints.api.routers.training import router as training_router from src.entrypoints.api.routers.diet import router as diet_router +from src.adapters.sqlalchemy.db import init_db app = FastAPI( @@ -39,4 +40,9 @@ app.include_router(diet_router) +@app.on_event("startup") +async def startup_event(): + await init_db() # ✅ Créer les tables au démarrage + + diff --git a/uv.lock b/uv.lock index 4f8c15b..6fde05f 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 2 -requires-python = ">=3.13" +requires-python = ">=3.12" [[package]] name = "annotated-types" @@ -18,12 +18,37 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, ] +[[package]] +name = "asyncpg" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746, upload-time = "2024-10-20T00:30:41.127Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/64/9d3e887bb7b01535fdbc45fbd5f0a8447539833b97ee69ecdbb7a79d0cb4/asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e", size = 673162, upload-time = "2024-10-20T00:29:41.88Z" }, + { url = "https://files.pythonhosted.org/packages/6e/eb/8b236663f06984f212a087b3e849731f917ab80f84450e943900e8ca4052/asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a", size = 637025, upload-time = "2024-10-20T00:29:43.352Z" }, + { url = "https://files.pythonhosted.org/packages/cc/57/2dc240bb263d58786cfaa60920779af6e8d32da63ab9ffc09f8312bd7a14/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3", size = 3496243, upload-time = "2024-10-20T00:29:44.922Z" }, + { url = "https://files.pythonhosted.org/packages/f4/40/0ae9d061d278b10713ea9021ef6b703ec44698fe32178715a501ac696c6b/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737", size = 3575059, upload-time = "2024-10-20T00:29:46.891Z" }, + { url = "https://files.pythonhosted.org/packages/c3/75/d6b895a35a2c6506952247640178e5f768eeb28b2e20299b6a6f1d743ba0/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a", size = 3473596, upload-time = "2024-10-20T00:29:49.201Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e7/3693392d3e168ab0aebb2d361431375bd22ffc7b4a586a0fc060d519fae7/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af", size = 3641632, upload-time = "2024-10-20T00:29:50.768Z" }, + { url = "https://files.pythonhosted.org/packages/32/ea/15670cea95745bba3f0352341db55f506a820b21c619ee66b7d12ea7867d/asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e", size = 560186, upload-time = "2024-10-20T00:29:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/7e/6b/fe1fad5cee79ca5f5c27aed7bd95baee529c1bf8a387435c8ba4fe53d5c1/asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305", size = 621064, upload-time = "2024-10-20T00:29:53.757Z" }, + { url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373, upload-time = "2024-10-20T00:29:55.165Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745, upload-time = "2024-10-20T00:29:57.14Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103, upload-time = "2024-10-20T00:29:58.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", size = 3592471, upload-time = "2024-10-20T00:30:00.354Z" }, + { url = "https://files.pythonhosted.org/packages/67/e4/ab3ca38f628f53f0fd28d3ff20edff1c975dd1cb22482e0061916b4b9a74/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", size = 3496253, upload-time = "2024-10-20T00:30:02.794Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5f/0bf65511d4eeac3a1f41c54034a492515a707c6edbc642174ae79034d3ba/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", size = 3662720, upload-time = "2024-10-20T00:30:04.501Z" }, + { url = "https://files.pythonhosted.org/packages/e7/31/1513d5a6412b98052c3ed9158d783b1e09d0910f51fbe0e05f56cc370bc4/asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", size = 560404, upload-time = "2024-10-20T00:30:06.537Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623, upload-time = "2024-10-20T00:30:09.024Z" }, +] + [[package]] name = "bcrypt" version = "4.3.0" @@ -120,6 +145,17 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, @@ -160,6 +196,17 @@ version = "7.9.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/04/b7/c0465ca253df10a9e8dae0692a4ae6e9726d245390aaef92360e1d6d3832/coverage-7.9.2.tar.gz", hash = "sha256:997024fa51e3290264ffd7492ec97d0690293ccd2b45a6cd7d82d945a4a80c8b", size = 813556, upload-time = "2025-07-03T10:54:15.101Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/53/d7/7deefc6fd4f0f1d4c58051f4004e366afc9e7ab60217ac393f247a1de70a/coverage-7.9.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae9eb07f1cfacd9cfe8eaee6f4ff4b8a289a668c39c165cd0c8548484920ffc0", size = 212344, upload-time = "2025-07-03T10:53:09.3Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/ee03c95d32be4d519e6a02e601267769ce2e9a91fc8faa1b540e3626c680/coverage-7.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ce85551f9a1119f02adc46d3014b5ee3f765deac166acf20dbb851ceb79b6f3", size = 212580, upload-time = "2025-07-03T10:53:11.52Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9f/826fa4b544b27620086211b87a52ca67592622e1f3af9e0a62c87aea153a/coverage-7.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8f6389ac977c5fb322e0e38885fbbf901743f79d47f50db706e7644dcdcb6e1", size = 246383, upload-time = "2025-07-03T10:53:13.134Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b3/4477aafe2a546427b58b9c540665feff874f4db651f4d3cb21b308b3a6d2/coverage-7.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff0d9eae8cdfcd58fe7893b88993723583a6ce4dfbfd9f29e001922544f95615", size = 243400, upload-time = "2025-07-03T10:53:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c2/efffa43778490c226d9d434827702f2dfbc8041d79101a795f11cbb2cf1e/coverage-7.9.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae939811e14e53ed8a9818dad51d434a41ee09df9305663735f2e2d2d7d959b", size = 245591, upload-time = "2025-07-03T10:53:15.872Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e7/a59888e882c9a5f0192d8627a30ae57910d5d449c80229b55e7643c078c4/coverage-7.9.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:31991156251ec202c798501e0a42bbdf2169dcb0f137b1f5c0f4267f3fc68ef9", size = 245402, upload-time = "2025-07-03T10:53:17.124Z" }, + { url = "https://files.pythonhosted.org/packages/92/a5/72fcd653ae3d214927edc100ce67440ed8a0a1e3576b8d5e6d066ed239db/coverage-7.9.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d0d67963f9cbfc7c7f96d4ac74ed60ecbebd2ea6eeb51887af0f8dce205e545f", size = 243583, upload-time = "2025-07-03T10:53:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f5/84e70e4df28f4a131d580d7d510aa1ffd95037293da66fd20d446090a13b/coverage-7.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49b752a2858b10580969ec6af6f090a9a440a64a301ac1528d7ca5f7ed497f4d", size = 244815, upload-time = "2025-07-03T10:53:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/39/e7/d73d7cbdbd09fdcf4642655ae843ad403d9cbda55d725721965f3580a314/coverage-7.9.2-cp312-cp312-win32.whl", hash = "sha256:88d7598b8ee130f32f8a43198ee02edd16d7f77692fa056cb779616bbea1b355", size = 214719, upload-time = "2025-07-03T10:53:21.521Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d6/7486dcc3474e2e6ad26a2af2db7e7c162ccd889c4c68fa14ea8ec189c9e9/coverage-7.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:9dfb070f830739ee49d7c83e4941cc767e503e4394fdecb3b54bfdac1d7662c0", size = 215509, upload-time = "2025-07-03T10:53:22.853Z" }, + { url = "https://files.pythonhosted.org/packages/b7/34/0439f1ae2593b0346164d907cdf96a529b40b7721a45fdcf8b03c95fcd90/coverage-7.9.2-cp312-cp312-win_arm64.whl", hash = "sha256:4e2c058aef613e79df00e86b6d42a641c877211384ce5bd07585ed7ba71ab31b", size = 213910, upload-time = "2025-07-03T10:53:24.472Z" }, { url = "https://files.pythonhosted.org/packages/94/9d/7a8edf7acbcaa5e5c489a646226bed9591ee1c5e6a84733c0140e9ce1ae1/coverage-7.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:985abe7f242e0d7bba228ab01070fde1d6c8fa12f142e43debe9ed1dde686038", size = 212367, upload-time = "2025-07-03T10:53:25.811Z" }, { url = "https://files.pythonhosted.org/packages/e8/9e/5cd6f130150712301f7e40fb5865c1bc27b97689ec57297e568d972eec3c/coverage-7.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c3939264a76d44fde7f213924021ed31f55ef28111a19649fec90c0f109e6d", size = 212632, upload-time = "2025-07-03T10:53:27.075Z" }, { url = "https://files.pythonhosted.org/packages/a8/de/6287a2c2036f9fd991c61cefa8c64e57390e30c894ad3aa52fac4c1e14a8/coverage-7.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae5d563e970dbe04382f736ec214ef48103d1b875967c89d83c6e3f21706d5b3", size = 245793, upload-time = "2025-07-03T10:53:28.408Z" }, @@ -283,6 +330,15 @@ version = "3.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022, upload-time = "2024-09-20T18:21:04.506Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260, upload-time = "2024-09-20T17:08:07.301Z" }, + { url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064, upload-time = "2024-09-20T17:36:47.628Z" }, + { url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420, upload-time = "2024-09-20T17:39:21.258Z" }, + { url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035, upload-time = "2024-09-20T17:44:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105, upload-time = "2024-09-20T17:08:42.048Z" }, + { url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077, upload-time = "2024-09-20T17:08:33.707Z" }, + { url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975, upload-time = "2024-09-20T17:44:15.989Z" }, + { url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955, upload-time = "2024-09-20T17:09:25.539Z" }, + { url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655, upload-time = "2024-09-20T17:21:22.427Z" }, { url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990, upload-time = "2024-09-20T17:08:26.312Z" }, { url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175, upload-time = "2024-09-20T17:36:48.983Z" }, { url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425, upload-time = "2024-09-20T17:39:22.705Z" }, @@ -403,6 +459,18 @@ version = "2.9.10" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764, upload-time = "2024-10-16T11:24:58.126Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/49/7d/465cc9795cf76f6d329efdafca74693714556ea3891813701ac1fee87545/psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", size = 3044771, upload-time = "2024-10-16T11:20:35.234Z" }, + { url = "https://files.pythonhosted.org/packages/8b/31/6d225b7b641a1a2148e3ed65e1aa74fc86ba3fee850545e27be9e1de893d/psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", size = 3275336, upload-time = "2024-10-16T11:20:38.742Z" }, + { url = "https://files.pythonhosted.org/packages/30/b7/a68c2b4bff1cbb1728e3ec864b2d92327c77ad52edcd27922535a8366f68/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", size = 2851637, upload-time = "2024-10-16T11:20:42.145Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b1/cfedc0e0e6f9ad61f8657fd173b2f831ce261c02a08c0b09c652b127d813/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", size = 3082097, upload-time = "2024-10-16T11:20:46.185Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/0a8e4153c9b769f59c02fb5e7914f20f0b2483a19dae7bf2db54b743d0d0/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", size = 3264776, upload-time = "2024-10-16T11:20:50.879Z" }, + { url = "https://files.pythonhosted.org/packages/10/db/d09da68c6a0cdab41566b74e0a6068a425f077169bed0946559b7348ebe9/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", size = 3020968, upload-time = "2024-10-16T11:20:56.819Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/4d6f8c255f0dfffb410db2b3f9ac5218d959a66c715c34cac31081e19b95/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", size = 2872334, upload-time = "2024-10-16T11:21:02.411Z" }, + { url = "https://files.pythonhosted.org/packages/05/f7/20d7bf796593c4fea95e12119d6cc384ff1f6141a24fbb7df5a668d29d29/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", size = 2822722, upload-time = "2024-10-16T11:21:09.01Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e4/0c407ae919ef626dbdb32835a03b6737013c3cc7240169843965cada2bdf/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", size = 2920132, upload-time = "2024-10-16T11:21:16.339Z" }, + { url = "https://files.pythonhosted.org/packages/2d/70/aa69c9f69cf09a01da224909ff6ce8b68faeef476f00f7ec377e8f03be70/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", size = 2959312, upload-time = "2024-10-16T11:21:25.584Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/213e59854fafe87ba47814bf413ace0dcee33a89c8c8c814faca6bc7cf3c/psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", size = 1025191, upload-time = "2024-10-16T11:21:29.912Z" }, + { url = "https://files.pythonhosted.org/packages/92/29/06261ea000e2dc1e22907dbbc483a1093665509ea586b29b8986a0e56733/psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", size = 1164031, upload-time = "2024-10-16T11:21:34.211Z" }, { url = "https://files.pythonhosted.org/packages/3e/30/d41d3ba765609c0763505d565c4d12d8f3c79793f0d0f044ff5a28bf395b/psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", size = 3044699, upload-time = "2024-10-16T11:21:42.841Z" }, { url = "https://files.pythonhosted.org/packages/35/44/257ddadec7ef04536ba71af6bc6a75ec05c5343004a7ec93006bee66c0bc/psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", size = 3275245, upload-time = "2024-10-16T11:21:51.989Z" }, { url = "https://files.pythonhosted.org/packages/1b/11/48ea1cd11de67f9efd7262085588790a95d9dfcd9b8a687d46caf7305c1a/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", size = 2851631, upload-time = "2024-10-16T11:21:57.584Z" }, @@ -458,6 +526,20 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395, upload-time = "2025-04-02T09:49:41.8Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/ce/3cb22b07c29938f97ff5f5bb27521f95e2ebec399b882392deb68d6c440e/pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8", size = 2026640, upload-time = "2025-04-02T09:47:25.394Z" }, + { url = "https://files.pythonhosted.org/packages/19/78/f381d643b12378fee782a72126ec5d793081ef03791c28a0fd542a5bee64/pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498", size = 1852649, upload-time = "2025-04-02T09:47:27.417Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939", size = 1892472, upload-time = "2025-04-02T09:47:29.006Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d", size = 1977509, upload-time = "2025-04-02T09:47:33.464Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e", size = 2128702, upload-time = "2025-04-02T09:47:34.812Z" }, + { url = "https://files.pythonhosted.org/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3", size = 2679428, upload-time = "2025-04-02T09:47:37.315Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d", size = 2008753, upload-time = "2025-04-02T09:47:39.013Z" }, + { url = "https://files.pythonhosted.org/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b", size = 2114849, upload-time = "2025-04-02T09:47:40.427Z" }, + { url = "https://files.pythonhosted.org/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39", size = 2069541, upload-time = "2025-04-02T09:47:42.01Z" }, + { url = "https://files.pythonhosted.org/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a", size = 2239225, upload-time = "2025-04-02T09:47:43.425Z" }, + { url = "https://files.pythonhosted.org/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db", size = 2248373, upload-time = "2025-04-02T09:47:44.979Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b2/288b3579ffc07e92af66e2f1a11be3b056fe1214aab314748461f21a31c3/pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda", size = 1907034, upload-time = "2025-04-02T09:47:46.843Z" }, + { url = "https://files.pythonhosted.org/packages/02/28/58442ad1c22b5b6742b992ba9518420235adced665513868f99a1c2638a5/pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4", size = 1956848, upload-time = "2025-04-02T09:47:48.404Z" }, + { url = "https://files.pythonhosted.org/packages/a1/eb/f54809b51c7e2a1d9f439f158b8dd94359321abcc98767e16fc48ae5a77e/pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea", size = 1903986, upload-time = "2025-04-02T09:47:49.839Z" }, { url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551, upload-time = "2025-04-02T09:47:51.648Z" }, { url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785, upload-time = "2025-04-02T09:47:53.149Z" }, { url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758, upload-time = "2025-04-02T09:47:55.006Z" }, @@ -624,6 +706,14 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/68/c3/3f2bfa5e4dcd9938405fe2fab5b6ab94a9248a4f9536ea2fd497da20525f/sqlalchemy-2.0.40.tar.gz", hash = "sha256:d827099289c64589418ebbcaead0145cd19f4e3e8a93919a0100247af245fa00", size = 9664299, upload-time = "2025-03-27T17:52:31.876Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/92/06/552c1f92e880b57d8b92ce6619bd569b25cead492389b1d84904b55989d8/sqlalchemy-2.0.40-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9d3b31d0a1c44b74d3ae27a3de422dfccd2b8f0b75e51ecb2faa2bf65ab1ba0d", size = 2112620, upload-time = "2025-03-27T18:40:00.071Z" }, + { url = "https://files.pythonhosted.org/packages/01/72/a5bc6e76c34cebc071f758161dbe1453de8815ae6e662393910d3be6d70d/sqlalchemy-2.0.40-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37f7a0f506cf78c80450ed1e816978643d3969f99c4ac6b01104a6fe95c5490a", size = 2103004, upload-time = "2025-03-27T18:40:04.204Z" }, + { url = "https://files.pythonhosted.org/packages/bf/fd/0e96c8e6767618ed1a06e4d7a167fe13734c2f8113c4cb704443e6783038/sqlalchemy-2.0.40-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bb933a650323e476a2e4fbef8997a10d0003d4da996aad3fd7873e962fdde4d", size = 3252440, upload-time = "2025-03-27T18:51:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/cd/6a/eb82e45b15a64266a2917a6833b51a334ea3c1991728fd905bfccbf5cf63/sqlalchemy-2.0.40-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6959738971b4745eea16f818a2cd086fb35081383b078272c35ece2b07012716", size = 3263277, upload-time = "2025-03-27T18:50:28.142Z" }, + { url = "https://files.pythonhosted.org/packages/45/97/ebe41ab4530f50af99e3995ebd4e0204bf1b0dc0930f32250dde19c389fe/sqlalchemy-2.0.40-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:110179728e442dae85dd39591beb74072ae4ad55a44eda2acc6ec98ead80d5f2", size = 3198591, upload-time = "2025-03-27T18:51:27.543Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1c/a569c1b2b2f5ac20ba6846a1321a2bf52e9a4061001f282bf1c5528dcd69/sqlalchemy-2.0.40-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8040680eaacdce4d635f12c55c714f3d4c7f57da2bc47a01229d115bd319191", size = 3225199, upload-time = "2025-03-27T18:50:30.069Z" }, + { url = "https://files.pythonhosted.org/packages/8f/91/87cc71a6b10065ca0209d19a4bb575378abda6085e72fa0b61ffb2201b84/sqlalchemy-2.0.40-cp312-cp312-win32.whl", hash = "sha256:650490653b110905c10adac69408380688cefc1f536a137d0d69aca1069dc1d1", size = 2082959, upload-time = "2025-03-27T18:45:57.574Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/14c511cda174aa1ad9b0e42b64ff5a71db35d08b0d80dc044dae958921e5/sqlalchemy-2.0.40-cp312-cp312-win_amd64.whl", hash = "sha256:2be94d75ee06548d2fc591a3513422b873490efb124048f50556369a834853b0", size = 2108526, upload-time = "2025-03-27T18:45:58.965Z" }, { url = "https://files.pythonhosted.org/packages/8c/18/4e3a86cc0232377bc48c373a9ba6a1b3fb79ba32dbb4eda0b357f5a2c59d/sqlalchemy-2.0.40-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:915866fd50dd868fdcc18d61d8258db1bf9ed7fbd6dfec960ba43365952f3b01", size = 2107887, upload-time = "2025-03-27T18:40:05.461Z" }, { url = "https://files.pythonhosted.org/packages/cb/60/9fa692b1d2ffc4cbd5f47753731fd332afed30137115d862d6e9a1e962c7/sqlalchemy-2.0.40-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a4c5a2905a9ccdc67a8963e24abd2f7afcd4348829412483695c59e0af9a705", size = 2098367, upload-time = "2025-03-27T18:40:07.182Z" }, { url = "https://files.pythonhosted.org/packages/4c/9f/84b78357ca641714a439eb3fbbddb17297dacfa05d951dbf24f28d7b5c08/sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55028d7a3ebdf7ace492fab9895cbc5270153f75442a0472d8516e03159ab364", size = 3184806, upload-time = "2025-03-27T18:51:29.356Z" }, @@ -649,11 +739,12 @@ wheels = [ [[package]] name = "tracknatrainapi" -version = "0.4.0" +version = "0.5.7" source = { editable = "." } dependencies = [ { name = "annotated-types" }, { name = "anyio" }, + { name = "asyncpg" }, { name = "bcrypt" }, { name = "boto3" }, { name = "botocore" }, @@ -709,6 +800,7 @@ testing = [ requires-dist = [ { name = "annotated-types", specifier = "==0.7.0" }, { name = "anyio", specifier = "==4.9.0" }, + { name = "asyncpg", specifier = "==0.30.0" }, { name = "bcrypt", specifier = "==4.3.0" }, { name = "boto3", specifier = "==1.37.37" }, { name = "botocore", specifier = "==1.37.37" }, From b49a9c5c1341252b5b7570a66e2ec6f9a01cebe8 Mon Sep 17 00:00:00 2001 From: Baptiste-Ferrand Date: Sun, 10 Aug 2025 15:26:03 +0200 Subject: [PATCH 2/6] fix all file whit asyncrhone --- pyproject.toml | 2 +- src/adapters/inmemory/repositories/diet.py | 9 +- .../inmemory/repositories/exercise.py | 2 +- src/adapters/inmemory/repositories/group.py | 11 +- .../inmemory/repositories/training.py | 9 +- src/adapters/sqlalchemy/db.py | 27 +-- src/adapters/sqlalchemy/repositories/diet.py | 184 ++++++++---------- .../sqlalchemy/repositories/exercise.py | 63 +++--- src/adapters/sqlalchemy/repositories/group.py | 148 +++++++------- .../sqlalchemy/repositories/profile.py | 140 ++++++------- .../sqlalchemy/repositories/training.py | 167 +++++++--------- src/container.py | 15 +- src/domain/model/diet.py | 3 +- src/domain/model/exercise.py | 2 +- src/domain/model/group.py | 2 +- src/domain/model/training.py | 8 +- src/domain/ports/diet_repository.py | 2 - src/domain/ports/training_repository.py | 2 - src/domain/services/diet.py | 3 - src/domain/services/exercise.py | 7 + src/domain/services/group.py | 17 +- src/domain/services/profile.py | 10 +- src/domain/services/training.py | 9 +- src/entrypoints/api/deps/auth.py | 40 ++-- src/entrypoints/api/deps/roles.py | 4 +- src/entrypoints/api/routers/diet.py | 4 +- src/entrypoints/api/routers/exercise.py | 3 +- src/entrypoints/api/routers/group.py | 7 +- src/entrypoints/api/routers/training.py | 10 +- src/entrypoints/api/tests/diet.py | 113 +++++++++++ src/entrypoints/api/tests/exercise.py | 66 +++++++ src/entrypoints/api/tests/group.py | 1 - src/entrypoints/api/tests/profile.py | 2 +- src/entrypoints/api/tests/training.py | 76 +++++++- src/main.py | 6 +- 35 files changed, 684 insertions(+), 490 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 93f1f60..ca1f57c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,4 +9,4 @@ requires = [ "setuptools>=42", "wheel",] build-backend = "setuptools.build_meta" [project.optional-dependencies] -testing = [ "pytest>=7.0", "pytest-asyncio>=0.20", "httpx>=0.24", "pytest-cov>=4.0", "coverage>=6.0",] +testing = [ "pytest>=7.0", "pytest-asyncio>=0.20", "httpx>=0.24", "pytest-cov>=4.0", "coverage>=6.0",] \ No newline at end of file diff --git a/src/adapters/inmemory/repositories/diet.py b/src/adapters/inmemory/repositories/diet.py index 6691bd2..cc3066e 100644 --- a/src/adapters/inmemory/repositories/diet.py +++ b/src/adapters/inmemory/repositories/diet.py @@ -12,7 +12,6 @@ def __init__(self): self._macro_plans: dict[UUID, DomainMacroPlan] = {} self._meal_plans: dict[UUID, DomainMealPlan] = {} - # Diet methods async def add_diet(self, diet: DomainDiet) -> DomainDiet: new_id = uuid4() diet.id = new_id @@ -37,12 +36,11 @@ async def delete_diet(self, id: UUID) -> None: self._diets.pop(id, None) for mid in list(self._macro_plans.keys()): if self._macro_plans[mid].diet_id == id: - await self.delete_macro_plan(mid) + self.delete_macro_plan(mid) for pid in list(self._meal_plans.keys()): if self._meal_plans[pid].diet_id == id: - await self.delete_meal_plan(pid) + self.delete_meal_plan(pid) - # MacroPlan methods async def add_macro_plan(self, macro_plan: DomainMacroPlan) -> DomainMacroPlan: new_id = uuid4() macro_plan.id = new_id @@ -67,7 +65,6 @@ async def update_macro_plan(self, macro_plan: DomainMacroPlan) -> DomainMacroPla async def delete_macro_plan(self, id: UUID) -> None: self._macro_plans.pop(id, None) - # MealPlan methods async def add_meal_plan(self, meal_plan: DomainMealPlan) -> DomainMealPlan: new_id = uuid4() meal_plan.id = new_id @@ -90,4 +87,4 @@ async def update_meal_plan(self, meal_plan: DomainMealPlan) -> DomainMealPlan: return meal_plan async def delete_meal_plan(self, id: UUID) -> None: - self._meal_plans.pop(id, None) + self._meal_plans.pop(id, None) \ No newline at end of file diff --git a/src/adapters/inmemory/repositories/exercise.py b/src/adapters/inmemory/repositories/exercise.py index 74b7b85..31a6917 100644 --- a/src/adapters/inmemory/repositories/exercise.py +++ b/src/adapters/inmemory/repositories/exercise.py @@ -34,4 +34,4 @@ async def find_all(self) -> List[DomainExercise]: return list(self._exercises.values()) async def find_by_id(self, id: UUID) -> Optional[DomainExercise]: - return self._exercises.get(id) + return self._exercises.get(id) \ No newline at end of file diff --git a/src/adapters/inmemory/repositories/group.py b/src/adapters/inmemory/repositories/group.py index 2ef6b61..7ec1395 100644 --- a/src/adapters/inmemory/repositories/group.py +++ b/src/adapters/inmemory/repositories/group.py @@ -60,9 +60,12 @@ async def list_members(self, group_id: UUID) -> List[DomainProfile]: user_ids = self._members.get(group_id, []) members = [] for uid in user_ids: - member = await self._profile_repo.find_by_id(uid) - if member: - members.append(member) + try: + profile = await self._profile_repo.find_by_id(uid) + if profile: + members.append(profile) + except: + pass return members async def find_by_owner_id(self, owner_id: UUID) -> Optional[List[DomainGroup]]: @@ -77,4 +80,4 @@ async def find_groups_by_member_id(self, user_id: UUID) -> Optional[List[DomainG for group_id, member_ids in self._members.items(): if user_id in member_ids: groups.append(self._groups[group_id]) - return groups if groups else [] + return groups if groups else [] \ No newline at end of file diff --git a/src/adapters/inmemory/repositories/training.py b/src/adapters/inmemory/repositories/training.py index 8157939..2970d1c 100644 --- a/src/adapters/inmemory/repositories/training.py +++ b/src/adapters/inmemory/repositories/training.py @@ -12,7 +12,6 @@ def __init__(self): self._tasks: dict[UUID, DomainTask] = {} self._validates: dict[UUID, DomainValidate] = {} - # Training methods async def find_by_id(self, id: UUID) -> Optional[DomainTraining]: return self._trainings.get(id) @@ -28,7 +27,7 @@ async def delete_training(self, id: UUID) -> None: self._trainings.pop(id, None) for task_id, task in list(self._tasks.items()): if task.training_id == id: - await self.delete_task(task_id) + self.delete_task(task_id) async def update_training(self, training: DomainTraining) -> Optional[DomainTraining]: if training.id not in self._trainings: @@ -39,7 +38,6 @@ async def update_training(self, training: DomainTraining) -> Optional[DomainTrai async def find_all_owner_trainings(self, owner_id: UUID) -> List[DomainTraining]: return [t for t in self._trainings.values() if t.owner_id == owner_id] - # Task methods async def add_task(self, task: DomainTask) -> DomainTask: new_id = uuid4() task.id = new_id @@ -56,7 +54,7 @@ async def delete_task(self, id: UUID) -> None: if task: for vid, validate in list(self._validates.items()): if validate.task_id == id: - await self.delete_validate(vid) + self.delete_validate(vid) async def update_task(self, task: DomainTask) -> Optional[DomainTask]: if task.id not in self._tasks: @@ -67,7 +65,6 @@ async def update_task(self, task: DomainTask) -> Optional[DomainTask]: async def find_tasks_by_training_id(self, training_id: UUID) -> List[DomainTask]: return [t for t in self._tasks.values() if t.training_id == training_id] - # Validate methods async def add_validate(self, validate: DomainValidate) -> DomainValidate: new_id = uuid4() validate.id = new_id @@ -87,4 +84,4 @@ async def find_all_validates_by_training_id(self, training_id: UUID) -> List[Dom return [v for v in self._validates.values() if v.task_id in task_ids] async def delete_validate(self, id: UUID) -> None: - self._validates.pop(id, None) + self._validates.pop(id, None) \ No newline at end of file diff --git a/src/adapters/sqlalchemy/db.py b/src/adapters/sqlalchemy/db.py index f8a7577..6910307 100644 --- a/src/adapters/sqlalchemy/db.py +++ b/src/adapters/sqlalchemy/db.py @@ -1,39 +1,32 @@ -# app/adapters/sqlalchemy/repositories/postgres.py from src.adapters.sqlalchemy.models import Base -from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker +from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession from dotenv import load_dotenv import os load_dotenv() db_url = os.getenv("DATABASE_URL") or "postgresql://user:user@localhost:5432/postgres" -# ✅ Convertir l'URL pour asyncpg -if db_url.startswith("postgresql://") and "asyncpg" not in db_url: +if db_url.startswith("postgresql://"): db_url = db_url.replace("postgresql://", "postgresql+asyncpg://", 1) -# ✅ Moteur asynchrone engine = create_async_engine( db_url, pool_size=20, max_overflow=20, pool_timeout=30, - echo=False, # Mettre True pour debug ) -# ✅ Factory de sessions asynchrones +async def create_tables(): + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + SessionLocal = async_sessionmaker( - bind=engine, + engine, class_=AsyncSession, - expire_on_commit=False + autocommit=False, + autoflush=False ) -# ✅ Fonction pour créer les tables (asynchrone) -async def init_db(): - async with engine.begin() as conn: - await conn.run_sync(Base.metadata.create_all) - -# ✅ Fonction pour obtenir une session -async def get_session(): +async def get_async_session() -> AsyncSession: async with SessionLocal() as session: yield session - diff --git a/src/adapters/sqlalchemy/repositories/diet.py b/src/adapters/sqlalchemy/repositories/diet.py index 11231db..5a12eb5 100644 --- a/src/adapters/sqlalchemy/repositories/diet.py +++ b/src/adapters/sqlalchemy/repositories/diet.py @@ -1,6 +1,6 @@ from typing import List, Optional from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.future import select +from sqlalchemy import select from src.domain.exceptions import NotFoundError from uuid import UUID @@ -8,6 +8,8 @@ from src.domain.model.diet import Diet as DomainDiet, MealPlan as DomainMealPlan, MacroPlan as DomainMacroPlan, MealItem as DomainMealItem from src.domain.ports.diet_repository import DietRepository + + def diet_from_orm(orm: ORMDiet) -> DomainDiet: return DomainDiet( id=orm.id, @@ -42,142 +44,128 @@ def meal_plan_from_orm(orm: ORMMealPlan) -> DomainMealPlan: ) class SqlAlchemyDietRepository(DietRepository): - def __init__(self, session_factory): - self._session_factory = session_factory + def __init__(self, session: AsyncSession): + self._session = session async def add_diet(self, diet: DomainDiet) -> DomainDiet: data = diet.to_orm_dict() orm = ORMDiet(**data) - async with self._session_factory() as session: - session.add(orm) - await session.commit() - await session.refresh(orm) - return diet_from_orm(orm) + self._session.add(orm) + await self._session.commit() + await self._session.refresh(orm) + return diet_from_orm(orm) async def find_by_id(self, id: UUID) -> Optional[DomainDiet]: - async with self._session_factory() as session: - orm = await session.get(ORMDiet, id) - return diet_from_orm(orm) if orm else None + orm = await self._session.get(ORMDiet, id) + return diet_from_orm(orm) if orm else None async def find_all_owner_diets(self, owner_id: UUID) -> List[DomainDiet]: - async with self._session_factory() as session: - result = await session.execute(select(ORMDiet).filter(ORMDiet.owner_id == owner_id)) - orms = result.scalars().all() - return [diet_from_orm(o) for o in orms] + stmt = select(ORMDiet).where(ORMDiet.owner_id == owner_id) + result = await self._session.execute(stmt) + orms = result.scalars().all() + return [diet_from_orm(o) for o in orms] async def update_diet(self, diet: DomainDiet) -> DomainDiet: - async with self._session_factory() as session: - orm = await session.get(ORMDiet, diet.id) - if not orm: - raise NotFoundError(f"Diet {diet.id} not found") - for k, v in diet.to_orm_dict().items(): - setattr(orm, k, v) - await session.commit() - return diet_from_orm(orm) + orm = await self._session.get(ORMDiet, diet.id) + if not orm: + raise NotFoundError(f"Diet {diet.id} not found") + for k, v in diet.to_orm_dict().items(): + setattr(orm, k, v) + await self._session.commit() + return diet_from_orm(orm) async def delete_diet(self, id: UUID) -> None: - async with self._session_factory() as session: - orm = await session.get(ORMDiet, id) - if not orm: - return - await session.delete(orm) - await session.commit() + orm = await self._session.get(ORMDiet, id) + if not orm: + return + await self._session.delete(orm) + await self._session.commit() -# Macro Plan methods async def add_macro_plan(self, macro_plan: DomainMacroPlan) -> DomainMacroPlan: data = macro_plan.to_orm_dict() orm = ORMMacroPlan(**data) - async with self._session_factory() as session: - session.add(orm) - await session.commit() - await session.refresh(orm) - return macro_plan_from_orm(orm) + self._session.add(orm) + await self._session.commit() + await self._session.refresh(orm) + return macro_plan_from_orm(orm) async def find_macro_plan_by_id(self, id: UUID) -> Optional[DomainMacroPlan]: - async with self._session_factory() as session: - orm = await session.get(ORMMacroPlan, id) - return macro_plan_from_orm(orm) if orm else None + orm = await self._session.get(ORMMacroPlan, id) + return macro_plan_from_orm(orm) if orm else None async def find_macro_plans_by_diet_id(self, diet_id: UUID) -> List[DomainMacroPlan]: - async with self._session_factory() as session: - result = await session.execute(select(ORMMacroPlan).filter(ORMMacroPlan.diet_id == diet_id)) - orms = result.scalars().all() - return [macro_plan_from_orm(o) for o in orms] + stmt = select(ORMMacroPlan).where(ORMMacroPlan.diet_id == diet_id) + result = await self._session.execute(stmt) + orms = result.scalars().all() + return [macro_plan_from_orm(o) for o in orms] async def find_macro_plans_by_user_id(self, user_id: UUID) -> List[DomainMacroPlan]: - async with self._session_factory() as session: - result = await session.execute( - select(ORMMacroPlan) - .join(ORMDiet, ORMDiet.id == ORMMacroPlan.diet_id) - .filter(ORMDiet.owner_id == user_id) - ) - orms = result.scalars().all() - return [macro_plan_from_orm(o) for o in orms] + stmt = ( + select(ORMMacroPlan) + .join(ORMDiet, ORMDiet.id == ORMMacroPlan.diet_id) + .where(ORMDiet.owner_id == user_id) + ) + result = await self._session.execute(stmt) + orms = result.scalars().all() + return [macro_plan_from_orm(o) for o in orms] async def update_macro_plan(self, macro_plan: DomainMacroPlan) -> DomainMacroPlan: - async with self._session_factory() as session: - orm = await session.get(ORMMacroPlan, macro_plan.id) - if not orm: - raise NotFoundError(f"MacroPlan {macro_plan.id} not found") - for k, v in macro_plan.to_orm_dict().items(): - setattr(orm, k, v) - await session.commit() - return macro_plan_from_orm(orm) + orm = await self._session.get(ORMMacroPlan, macro_plan.id) + if not orm: + raise NotFoundError(f"MacroPlan {macro_plan.id} not found") + for k, v in macro_plan.to_orm_dict().items(): + setattr(orm, k, v) + await self._session.commit() + return macro_plan_from_orm(orm) async def delete_macro_plan(self, id: UUID) -> None: - async with self._session_factory() as session: - orm = await session.get(ORMMacroPlan, id) - if not orm: - return - await session.delete(orm) - await session.commit() + orm = await self._session.get(ORMMacroPlan, id) + if not orm: + return + await self._session.delete(orm) + await self._session.commit() -# Meal Plan methods async def add_meal_plan(self, mp: DomainMealPlan) -> DomainMealPlan: data = mp.to_orm_dict() orm = ORMMealPlan(**data) - async with self._session_factory() as session: - session.add(orm) - await session.commit() - await session.refresh(orm) - return meal_plan_from_orm(orm) + self._session.add(orm) + await self._session.commit() + await self._session.refresh(orm) + return meal_plan_from_orm(orm) async def find_meal_plan_by_id(self, id: UUID) -> Optional[DomainMealPlan]: - async with self._session_factory() as session: - orm = await session.get(ORMMealPlan, id) - return meal_plan_from_orm(orm) if orm else None + orm = await self._session.get(ORMMealPlan, id) + return meal_plan_from_orm(orm) if orm else None async def find_meal_plans_by_diet_id(self, diet_id: UUID) -> List[DomainMealPlan]: - async with self._session_factory() as session: - result = await session.execute(select(ORMMealPlan).filter(ORMMealPlan.diet_id == diet_id)) - orms = result.scalars().all() - return [meal_plan_from_orm(o) for o in orms] + stmt = select(ORMMealPlan).where(ORMMealPlan.diet_id == diet_id) + result = await self._session.execute(stmt) + orms = result.scalars().all() + return [meal_plan_from_orm(o) for o in orms] async def find_meal_plans_by_user_id(self, user_id: UUID) -> List[DomainMealPlan]: - async with self._session_factory() as session: - result = await session.execute( - select(ORMMealPlan) - .join(ORMDiet) - .filter(ORMDiet.owner_id == user_id) - ) - orms = result.scalars().all() - return [meal_plan_from_orm(o) for o in orms] + stmt = ( + select(ORMMealPlan) + .join(ORMDiet, ORMDiet.id == ORMMealPlan.diet_id) + .where(ORMDiet.owner_id == user_id) + ) + result = await self._session.execute(stmt) + orms = result.scalars().all() + return [meal_plan_from_orm(o) for o in orms] async def update_meal_plan(self, mp: DomainMealPlan) -> DomainMealPlan: - async with self._session_factory() as session: - orm = await session.get(ORMMealPlan, mp.id) - if not orm: - raise NotFoundError(f"MealPlan {mp.id} not found") - for k, v in mp.to_orm_dict().items(): - setattr(orm, k, v) - await session.commit() - return meal_plan_from_orm(orm) + orm = await self._session.get(ORMMealPlan, mp.id) + if not orm: + raise NotFoundError(f"MealPlan {mp.id} not found") + for k, v in mp.to_orm_dict().items(): + setattr(orm, k, v) + await self._session.commit() + return meal_plan_from_orm(orm) async def delete_meal_plan(self, id: UUID) -> None: - async with self._session_factory() as session: - orm = await session.get(ORMMealPlan, id) - if orm: - await session.delete(orm) - await session.commit() \ No newline at end of file + orm = await self._session.get(ORMMealPlan, id) + if orm: + await self._session.delete(orm) + await self._session.commit() \ No newline at end of file diff --git a/src/adapters/sqlalchemy/repositories/exercise.py b/src/adapters/sqlalchemy/repositories/exercise.py index 5313ab7..2e2dab2 100644 --- a/src/adapters/sqlalchemy/repositories/exercise.py +++ b/src/adapters/sqlalchemy/repositories/exercise.py @@ -1,6 +1,6 @@ from typing import List, Optional from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.future import select +from sqlalchemy import select from src.domain.exceptions import NotFoundError from uuid import UUID @@ -20,50 +20,45 @@ def exercise_from_orm(orm_exercise) -> DomainExercise: ) class SqlAlchemyExerciseRepository(ExerciseRepository): - def __init__(self, session_factory): - self._session_factory = session_factory + def __init__(self, session: AsyncSession): + self._session = session - # CRUD operations for Exercise async def add(self, exercise: DomainExercise) -> Optional[DomainExercise]: data = exercise.to_orm_dict() orm = ORMExercise(**data) - async with self._session_factory() as session: - session.add(orm) - await session.commit() - await session.refresh(orm) - return exercise_from_orm(orm) if orm else None + self._session.add(orm) + await self._session.commit() + await self._session.refresh(orm) + return exercise_from_orm(orm) if orm else None async def delete(self, id: UUID) -> None: - async with self._session_factory() as session: - orm = await session.get(ORMExercise, id) - if not orm: - return - await session.delete(orm) - await session.commit() + orm = await self._session.get(ORMExercise, id) + if not orm: + return + await self._session.delete(orm) + await self._session.commit() async def update(self, exercise: DomainExercise) -> Optional[DomainExercise]: - async with self._session_factory() as session: - orm = await session.get(ORMExercise, exercise.id) - if not orm: - raise NotFoundError(f"Exercise {exercise.id} not found") - for key, value in exercise.to_orm_dict().items(): - setattr(orm, key, value) - await session.commit() - return exercise_from_orm(orm) if orm else None + orm = await self._session.get(ORMExercise, exercise.id) + if not orm: + raise NotFoundError(f"Exercise {exercise.id} not found") + for key, value in exercise.to_orm_dict().items(): + setattr(orm, key, value) + await self._session.commit() + return exercise_from_orm(orm) if orm else None async def find_all_owner(self, owner_id: UUID) -> Optional[List[DomainExercise]]: - async with self._session_factory() as session: - result = await session.execute(select(ORMExercise).filter(ORMExercise.owner_id == owner_id)) - orms = result.scalars().all() - return [exercise_from_orm(orm) for orm in orms] if orms else [] + stmt = select(ORMExercise).where(ORMExercise.owner_id == owner_id) + result = await self._session.execute(stmt) + orms = result.scalars().all() + return [exercise_from_orm(orm) for orm in orms] if orms else [] async def find_all(self) -> Optional[List[DomainExercise]]: - async with self._session_factory() as session: - result = await session.execute(select(ORMExercise)) - orms = result.scalars().all() - return [exercise_from_orm(orm) for orm in orms] if orms else [] + stmt = select(ORMExercise) + result = await self._session.execute(stmt) + orms = result.scalars().all() + return [exercise_from_orm(orm) for orm in orms] if orms else [] async def find_by_id(self, id: UUID) -> Optional[DomainExercise]: - async with self._session_factory() as session: - orm = await session.get(ORMExercise, id) - return exercise_from_orm(orm) if orm else None \ No newline at end of file + orm = await self._session.get(ORMExercise, id) + return exercise_from_orm(orm) if orm else None \ No newline at end of file diff --git a/src/adapters/sqlalchemy/repositories/group.py b/src/adapters/sqlalchemy/repositories/group.py index 6ce3fd2..e47b630 100644 --- a/src/adapters/sqlalchemy/repositories/group.py +++ b/src/adapters/sqlalchemy/repositories/group.py @@ -1,7 +1,6 @@ from typing import Optional, List from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.future import select -from sqlalchemy.orm import selectinload +from sqlalchemy import select from src.domain.exceptions import NotFoundError from src.domain.model.group import Group as DomainGroup from src.domain.model.profile import Profile as DomainProfile @@ -20,95 +19,102 @@ def group_from_orm(orm_group) -> DomainGroup: ) class SqlAlchemyGroupRepository(GroupRepository): - def __init__(self, session_factory): - self._session_factory = session_factory + def __init__(self, session: AsyncSession): + self._session = session async def find_by_id(self, id: UUID) -> Optional[DomainGroup]: - async with self._session_factory() as session: - orm = await session.get(ORMGroup, id) - return group_from_orm(orm) if orm else None + orm = await self._session.get(ORMGroup, id) + return group_from_orm(orm) if orm else None async def add(self, group: DomainGroup) -> Optional[DomainGroup]: data = group.to_orm_dict() orm = ORMGroup(**data) - async with self._session_factory() as session: - session.add(orm) - await session.commit() - await session.refresh(orm) - return group_from_orm(orm) if orm else None + self._session.add(orm) + await self._session.commit() + await self._session.refresh(orm) + return group_from_orm(orm) if orm else None async def delete(self, id: UUID) -> None: - async with self._session_factory() as session: - orm = await session.get(ORMGroup, id) - if not orm: - return - await session.delete(orm) - await session.commit() + orm = await self._session.get(ORMGroup, id) + if not orm: + return + await self._session.delete(orm) + await self._session.commit() async def update(self, group: DomainGroup) -> Optional[DomainGroup]: - async with self._session_factory() as session: - orm = await session.get(ORMGroup, group.id) - if not orm: - raise NotFoundError(f"Groupe {group.id} not found") - for key, value in group.to_orm_dict().items(): - setattr(orm, key, value) - await session.commit() - return group_from_orm(orm) if orm else None + orm = await self._session.get(ORMGroup, group.id) + if not orm: + raise NotFoundError(f"Groupe {group.id} not found") + for key, value in group.to_orm_dict().items(): + setattr(orm, key, value) + await self._session.commit() + return group_from_orm(orm) if orm else None + async def add_member(self, group_id: UUID, user_id: UUID) -> None: async with self._session_factory() as session: - orm_group = await session.get(ORMGroup, group_id) - if not orm_group: - raise NotFoundError(f"Groupe {group_id} not found") - orm_profile = await session.get(ORMProfile, user_id) - if not orm_profile: - raise NotFoundError(f"Profile {user_id} not found") - if orm_profile not in orm_group.users: - orm_group.users.append(orm_profile) - await session.commit() + group_stmt = select(ORMGroup).where(ORMGroup.id == group_id) + group_result = await session.execute(group_stmt) + group = group_result.scalar_one_or_none() + + if not group: + raise NotFoundError(f"Group {group_id} not found") + + user_stmt = select(ORMProfile).where(ORMProfile.id == user_id) + user_result = await session.execute(user_stmt) + user = user_result.scalar_one_or_none() + + if not user: + raise NotFoundError(f"User {user_id} not found") + + existing_stmt = select(group_users).where( + (group_users.c.group_id == group_id) & + (group_users.c.profile_id == user_id) + ) + existing_result = await session.execute(existing_stmt) + existing = existing_result.scalar_one_or_none() + + if existing: + return + + insert_stmt = group_users.insert().values( + group_id=group_id, + profile_id=user_id + ) + await session.execute(insert_stmt) + await session.commit() async def remove_member(self, group_id: UUID, user_id: UUID) -> None: - async with self._session_factory() as session: - orm_group = await session.get(ORMGroup, group_id) - if not orm_group: - raise NotFoundError(f"Groupe {group_id} not found") - orm_profile = await session.get(ORMProfile, user_id) - if not orm_profile: - raise NotFoundError(f"Profile {user_id} not found") - if orm_profile in orm_group.users: - orm_group.users.remove(orm_profile) - await session.commit() + orm_group = await self._session.get(ORMGroup, group_id) + if not orm_group: + raise NotFoundError(f"Groupe {group_id} not found") + orm_profile = await self._session.get(ORMProfile, user_id) + if not orm_profile: + raise NotFoundError(f"Profile {user_id} not found") + if orm_profile in orm_group.users: + orm_group.users.remove(orm_profile) + await self._session.commit() async def list_members(self, group_id: UUID) -> List[DomainProfile]: - async with self._session_factory() as session: - result = await session.execute( - select(ORMGroup) - .options(selectinload(ORMGroup.users)) - .filter(ORMGroup.id == group_id) - ) - orm_grp = result.scalars().first() - if not orm_grp: - raise NotFoundError(f"Groupe {group_id} introuvable") - return [profil_from_orm(p) for p in orm_grp.users] + orm_grp = await self._session.get(ORMGroup, group_id) + if not orm_grp: + raise NotFoundError(f"Groupe {group_id} introuvable") + return [profil_from_orm(p) for p in orm_grp.users] async def find_by_owner_id(self, owner_id: UUID) -> Optional[List[DomainGroup]]: - async with self._session_factory() as session: - result = await session.execute(select(ORMGroup).filter(ORMGroup.owner_id == owner_id)) - orms = result.scalars().all() - return [group_from_orm(orm) for orm in orms] if orms else [] + stmt = select(ORMGroup).where(ORMGroup.owner_id == owner_id) + result = await self._session.execute(stmt) + orms = result.scalars().all() + return [group_from_orm(orm) for orm in orms] if orms else [] async def find_all_groups(self) -> Optional[List[DomainGroup]]: - async with self._session_factory() as session: - result = await session.execute(select(ORMGroup)) - orms = result.scalars().all() - return [group_from_orm(orm) for orm in orms] if orms else [] + stmt = select(ORMGroup) + result = await self._session.execute(stmt) + orms = result.scalars().all() + return [group_from_orm(orm) for orm in orms] if orms else [] async def find_groups_by_member_id(self, user_id: UUID) -> Optional[List[DomainGroup]]: - async with self._session_factory() as session: - result = await session.execute( - select(ORMGroup) - .join(group_users) - .filter(group_users.c.profile_id == user_id) - ) - orms = result.scalars().all() - return [group_from_orm(orm) for orm in orms] if orms else [] \ No newline at end of file + stmt = select(ORMGroup).join(group_users).where(group_users.c.profile_id == user_id) + result = await self._session.execute(stmt) + orms = result.scalars().all() + return [group_from_orm(orm) for orm in orms] if orms else [] \ No newline at end of file diff --git a/src/adapters/sqlalchemy/repositories/profile.py b/src/adapters/sqlalchemy/repositories/profile.py index dfafd9d..dd7b733 100644 --- a/src/adapters/sqlalchemy/repositories/profile.py +++ b/src/adapters/sqlalchemy/repositories/profile.py @@ -1,88 +1,76 @@ +from typing import Optional from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select -from typing import Optional, List -from uuid import UUID - from src.domain.model.profile import Profile as DomainProfile +from src.adapters.sqlalchemy.models import Profile as ORMProfile from src.domain.ports.profile_repository import ProfileRepository -from src.adapters.sqlalchemy.models.profile import Profile as SQLProfile +from uuid import UUID -class SQLAlchemyProfileRepository(ProfileRepository): - def __init__(self, session_factory): - self._session_factory = session_factory +def profil_from_orm(orm_profile) ->DomainProfile: + return DomainProfile( + id=orm_profile.id, + email=orm_profile.email, + password=orm_profile.password, + name=orm_profile.name, + sex=orm_profile.sex, + age=orm_profile.age, + contact=orm_profile.contact, + pricing=orm_profile.pricing, + description=orm_profile.description, + legacy=orm_profile.legacy, + roles=orm_profile.roles, + created_at=orm_profile.created_at, + ) - async def find_by_email(self, email: str) -> Optional[DomainProfile]: - async with self._session_factory() as session: # ✅ Correct usage - stmt = select(SQLProfile).where(SQLProfile.email == email) - result = await session.execute(stmt) - sql_profile = result.scalar_one_or_none() - - if sql_profile: - return sql_profile.to_domain() - return None - async def add(self, profile: DomainProfile) -> DomainProfile: - async with self._session_factory() as session: - sql_profile = SQLProfile.from_domain(profile) - session.add(sql_profile) - await session.commit() - await session.refresh(sql_profile) - return sql_profile.to_domain() - async def find_by_id(self, id: UUID) -> Optional[DomainProfile]: - async with self._session_factory() as session: - stmt = select(SQLProfile).where(SQLProfile.id == id) - result = await session.execute(stmt) - sql_profile = result.scalar_one_or_none() - - if sql_profile: - return sql_profile.to_domain() - return None +class SqlAlchemyProfileRepository(ProfileRepository): + def __init__(self, session: AsyncSession): + self._session = session - async def delete(self, id: UUID) -> None: - async with self._session_factory() as session: - stmt = select(SQLProfile).where(SQLProfile.id == id) - result = await session.execute(stmt) - sql_profile = result.scalar_one_or_none() - - if sql_profile: - await session.delete(sql_profile) - await session.commit() + async def find_by_email(self, email: str) -> Optional[DomainProfile]: + stmt = select(ORMProfile).where(ORMProfile.email == email) + result = await self._session.execute(stmt) + orm = result.scalar_one_or_none() + return profil_from_orm(orm) if orm else None + async def add(self, profile: DomainProfile) -> Optional[DomainProfile]: + data = profile.to_orm_dict() + orm = ORMProfile(**data) + self._session.add(orm) + await self._session.commit() + await self._session.refresh(orm) + return profil_from_orm(orm) if orm else None + + async def find_by_id(self, id: UUID) -> Optional[DomainProfile]: + orm = await self._session.get(ORMProfile, id) + return profil_from_orm(orm) if orm else None + + async def delete(self, id: UUID) -> None: + orm = await self._session.get(ORMProfile, id) + if not orm: + return + await self._session.delete(orm) + await self._session.commit() + async def update(self, profile: DomainProfile) -> Optional[DomainProfile]: - async with self._session_factory() as session: - stmt = select(SQLProfile).where(SQLProfile.id == profile.id) - result = await session.execute(stmt) - sql_profile = result.scalar_one_or_none() - - if sql_profile: - # Mettre à jour les champs - sql_profile.email = profile.email - sql_profile.password = profile.password - sql_profile.name = profile.name - sql_profile.sex = profile.sex - sql_profile.age = profile.age - sql_profile.contact = profile.contact - sql_profile.pricing = profile.pricing - sql_profile.description = profile.description - sql_profile.legacy = profile.legacy - sql_profile.roles = profile.roles - - await session.commit() - await session.refresh(sql_profile) - return sql_profile.to_domain() + orm = await self._session.get(ORMProfile, profile.id) + if not orm: return None - - async def find_all_users(self) -> List[DomainProfile]: - async with self._session_factory() as session: - stmt = select(SQLProfile).where(SQLProfile.roles.contains(["user"])) - result = await session.execute(stmt) - sql_profiles = result.scalars().all() - return [profile.to_domain() for profile in sql_profiles] - - async def find_all_coachs(self) -> List[DomainProfile]: - async with self._session_factory() as session: - stmt = select(SQLProfile).where(SQLProfile.roles.contains(["coach"])) - result = await session.execute(stmt) - sql_profiles = result.scalars().all() - return [profile.to_domain() for profile in sql_profiles] \ No newline at end of file + for key, value in profile.to_orm_dict().items(): + setattr(orm, key, value) + await self._session.commit() + await self._session.refresh(orm) + return profil_from_orm(orm) + + async def find_all_users(self)-> list[DomainProfile]: + stmt = select(ORMProfile).where(ORMProfile.roles.any('user')) + result = await self._session.execute(stmt) + orms = result.scalars().all() + return [profil_from_orm(orm) for orm in orms] if orms else [] + + async def find_all_coachs(self)-> list[DomainProfile]: + stmt = select(ORMProfile).where(ORMProfile.roles.any('coach')) + result = await self._session.execute(stmt) + orms = result.scalars().all() + return [profil_from_orm(orm) for orm in orms] if orms else [] \ No newline at end of file diff --git a/src/adapters/sqlalchemy/repositories/training.py b/src/adapters/sqlalchemy/repositories/training.py index 64dac41..4e6e55f 100644 --- a/src/adapters/sqlalchemy/repositories/training.py +++ b/src/adapters/sqlalchemy/repositories/training.py @@ -1,7 +1,6 @@ from typing import List, Optional from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.future import select -from sqlalchemy.orm import selectinload +from sqlalchemy import select from src.domain.exceptions import NotFoundError from src.adapters.sqlalchemy.models import Training as ORMTraining, Task as ORMTask, Validation as ORMValidate @@ -46,132 +45,110 @@ def training_from_orm(orm_training) -> DomainTraining: ) class SqlAlchemyTrainingRepository(TrainingRepository): - def __init__(self, session_factory): - self._session_factory = session_factory + def __init__(self, session: AsyncSession): + self._session = session async def find_by_id(self, id: UUID) -> Optional[DomainTraining]: - async with self._session_factory() as session: - orm = await session.get(ORMTraining, id) - return training_from_orm(orm) if orm else None + orm = await self._session.get(ORMTraining, id) + return training_from_orm(orm) if orm else None - # CRUD operations for Training + async def add_training(self, training: DomainTraining) -> Optional[DomainTraining]: data = training.to_orm_dict() orm = ORMTraining(**data) - async with self._session_factory() as session: - session.add(orm) - await session.commit() - await session.refresh(orm) - return training_from_orm(orm) if orm else None + self._session.add(orm) + await self._session.commit() + await self._session.refresh(orm) + return training_from_orm(orm) if orm else None async def delete_training(self, id: UUID) -> None: - async with self._session_factory() as session: - orm = await session.get(ORMTraining, id) - if not orm: - return - await session.delete(orm) - await session.commit() + orm = await self._session.get(ORMTraining, id) + if not orm: + return + await self._session.delete(orm) + await self._session.commit() async def update_training(self, training: DomainTraining) -> Optional[DomainTraining]: - async with self._session_factory() as session: - orm = await session.get(ORMTraining, training.id) - if not orm: - raise NotFoundError(f"Training {training.id} not found") - for key, value in training.to_orm_dict().items(): - setattr(orm, key, value) - await session.commit() - return training_from_orm(orm) if orm else None + orm = await self._session.get(ORMTraining, training.id) + if not orm: + raise NotFoundError(f"Training {training.id} not found") + for key, value in training.to_orm_dict().items(): + setattr(orm, key, value) + await self._session.commit() + return training_from_orm(orm) if orm else None async def find_all_owner_trainings(self, owner_id: UUID) -> Optional[List[DomainTraining]]: - async with self._session_factory() as session: - result = await session.execute(select(ORMTraining).filter(ORMTraining.owner_id == owner_id)) - orms = result.scalars().all() - return [training_from_orm(orm) for orm in orms] if orms else [] + stmt = select(ORMTraining).where(ORMTraining.owner_id == owner_id) + result = await self._session.execute(stmt) + orms = result.scalars().all() + return [training_from_orm(orm) for orm in orms] if orms else [] - # CRUD Operation for Task + async def add_task(self, task: DomainTask) -> Optional[DomainTask]: data = task.to_orm_dict() orm = ORMTask(**data) - async with self._session_factory() as session: - session.add(orm) - await session.commit() - await session.refresh(orm) - return task_from_orm(orm) if orm else None + self._session.add(orm) + await self._session.commit() + await self._session.refresh(orm) + return task_from_orm(orm) if orm else None async def find_task_by_id(self, id: UUID) -> Optional[DomainTask]: - async with self._session_factory() as session: - # Utilisation de selectinload pour charger les validations en une seule requête - result = await session.execute( - select(ORMTask) - .options(selectinload(ORMTask.validations)) - .filter(ORMTask.id == id) - ) - orm = result.scalars().first() - return task_from_orm(orm) if orm else None + orm = await self._session.get(ORMTask, id) + return task_from_orm(orm) if orm else None async def delete_task(self, id: UUID) -> None: - async with self._session_factory() as session: - orm = await session.get(ORMTask, id) - if not orm: - return - await session.delete(orm) - await session.commit() + orm = await self._session.get(ORMTask, id) + if not orm: + return + await self._session.delete(orm) + await self._session.commit() async def update_task(self, task: DomainTask) -> Optional[DomainTask]: - async with self._session_factory() as session: - orm = await session.get(ORMTask, task.id) - if not orm: - raise NotFoundError(f"Task {task.id} not found") - for key, value in task.to_orm_dict().items(): - setattr(orm, key, value) - await session.commit() - return task_from_orm(orm) if orm else None + orm = await self._session.get(ORMTask, task.id) + if not orm: + raise NotFoundError(f"Task {task.id} not found") + for key, value in task.to_orm_dict().items(): + setattr(orm, key, value) + await self._session.commit() + return task_from_orm(orm) if orm else None async def find_tasks_by_training_id(self, training_id: UUID) -> Optional[List[DomainTask]]: - async with self._session_factory() as session: - result = await session.execute( - select(ORMTask) - .options(selectinload(ORMTask.validations)) - .filter(ORMTask.training_id == training_id) - ) - orms = result.scalars().all() - return [task_from_orm(orm) for orm in orms] if orms else [] + stmt = select(ORMTask).where(ORMTask.training_id == training_id) + result = await self._session.execute(stmt) + orms = result.scalars().all() + return [task_from_orm(orm) for orm in orms] if orms else [] - # validate methods async def add_validate(self, validate: DomainValidate) -> Optional[DomainValidate]: data = validate.to_orm_dict() orm = ORMValidate(**data) - async with self._session_factory() as session: - session.add(orm) - await session.commit() - await session.refresh(orm) - return validate_from_orm(orm) if orm else None + self._session.add(orm) + await self._session.commit() + await self._session.refresh(orm) + return validate_from_orm(orm) if orm else None async def find_validate_by_task_id(self, task_id: UUID) -> Optional[List[DomainValidate]]: - async with self._session_factory() as session: - result = await session.execute(select(ORMValidate).filter(ORMValidate.task_id == task_id)) - orms = result.scalars().all() - return [validate_from_orm(orm) for orm in orms] if orms else None + stmt = select(ORMValidate).where(ORMValidate.task_id == task_id) + result = await self._session.execute(stmt) + orms = result.scalars().all() + return [validate_from_orm(orm) for orm in orms] if orms else None async def delete_validate(self, id: UUID) -> None: - async with self._session_factory() as session: - orm = await session.get(ORMValidate, id) - if not orm: - return - await session.delete(orm) - await session.commit() + orm = await self._session.get(ORMValidate, id) + if not orm: + return + await self._session.delete(orm) + await self._session.commit() async def find_validate_by_id(self, id: UUID) -> Optional[DomainValidate]: - async with self._session_factory() as session: - orm = await session.get(ORMValidate, id) - return validate_from_orm(orm) if orm else None + orm = await self._session.get(ORMValidate, id) + return validate_from_orm(orm) if orm else None async def find_all_validates_by_training_id(self, training_id: UUID) -> list[DomainValidate]: - async with self._session_factory() as session: - result = await session.execute( - select(ORMValidate) - .join(ORMTask, ORMValidate.task_id == ORMTask.id) - .filter(ORMTask.training_id == training_id) - ) - orms = result.scalars().all() - return [validate_from_orm(v) for v in orms] \ No newline at end of file + stmt = ( + select(ORMValidate) + .join(ORMTask, ORMValidate.task_id == ORMTask.id) + .where(ORMTask.training_id == training_id) + ) + result = await self._session.execute(stmt) + orms = result.scalars().all() + return [validate_from_orm(v) for v in orms] \ No newline at end of file diff --git a/src/container.py b/src/container.py index 18771ec..d0541ef 100644 --- a/src/container.py +++ b/src/container.py @@ -1,7 +1,6 @@ import os from uuid import uuid4, UUID import asyncio -from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from src.domain.lib.security import BcryptPasswordHasher from src.domain.services.profile import ProfileService @@ -9,7 +8,6 @@ from src.domain.services.training import TrainingService from src.domain.services.exercise import ExerciseService from src.domain.services.diet import DietService -from src.adapters.sqlalchemy.db import SessionLocal, init_db class Container: def __init__(self, env: str | None = None): @@ -18,6 +16,8 @@ def __init__(self, env: str | None = None): if self.env in ("dev", "test"): from src.domain.model.profile import Profile as DomainProfile + from datetime import datetime + plain_pw = "123456789" hashed_pw = self.hasher.hash(plain_pw) admin = DomainProfile( @@ -30,9 +30,9 @@ def __init__(self, env: str | None = None): contact=None, pricing=None, description=None, - legacy=False, + legacy=None, roles=["admin"], - created_at=None, + created_at=datetime.now(), ) from src.adapters.inmemory.repositories.profile import InMemoryProfileRepository from src.adapters.inmemory.repositories.group import InMemoryGroupRepository @@ -45,11 +45,8 @@ def __init__(self, env: str | None = None): self.exercise_repo = InMemoryExerciseRepository() self.diet_repo = InMemoryDietRepository() else: - # Pas besoin de setup_database, on utilise db.py - pass - - def get_session_factory(self): - return SessionLocal # Vient de db.py + from src.adapters.sqlalchemy.db import SessionLocal + self.SessionFactory = SessionLocal def get_profile_service(self): if self.env in ("dev", "test"): diff --git a/src/domain/model/diet.py b/src/domain/model/diet.py index b612f14..9aed382 100644 --- a/src/domain/model/diet.py +++ b/src/domain/model/diet.py @@ -14,7 +14,7 @@ class Diet: def to_orm_dict(self) -> dict: return { "id": str(self.id), - "created_at": self.created_at.isoformat(), + "created_at": self.created_at, "owner_id": str(self.owner_id), "name": self.name, "description": self.description, @@ -66,4 +66,3 @@ def to_orm_dict(self) -> dict: } - diff --git a/src/domain/model/exercise.py b/src/domain/model/exercise.py index a7ab59c..7ce539e 100644 --- a/src/domain/model/exercise.py +++ b/src/domain/model/exercise.py @@ -18,5 +18,5 @@ def to_orm_dict(self) -> dict: "name": self.name, "owner_id": str(self.owner_id), "description": self.description, - "created_at": self.created_at.isoformat(), + "created_at": self.created_at, } \ No newline at end of file diff --git a/src/domain/model/group.py b/src/domain/model/group.py index 430a6b3..9d7258e 100644 --- a/src/domain/model/group.py +++ b/src/domain/model/group.py @@ -17,5 +17,5 @@ def to_orm_dict(self) -> dict: "owner_id": str(self.owner_id), "name": self.name, "description": self.description, - "created_at": self.created_at.isoformat(), + "created_at": self.created_at } \ No newline at end of file diff --git a/src/domain/model/training.py b/src/domain/model/training.py index dffe45c..9e7491f 100644 --- a/src/domain/model/training.py +++ b/src/domain/model/training.py @@ -23,8 +23,8 @@ def to_orm_dict(self) -> dict: "repetitions": self.repetitions, "set_number": self.set_number, "rir": self.rir, - "updated_at": self.updated_at.isoformat(), - "succeeded_at": self.succeeded_at.isoformat() if self.succeeded_at else None, + "updated_at": self.updated_at, + "succeeded_at": self.succeeded_at if self.succeeded_at else None, } @dataclass @@ -50,7 +50,7 @@ def to_orm_dict(self) -> dict: "set_number": self.set_number, "method": self.method, "rir": self.rir, - "updated_at": self.updated_at.isoformat(), + "updated_at": self.updated_at, "validate": [v.to_orm_dict() for v in self.validate] if self.validate else None, } @@ -69,6 +69,6 @@ def to_orm_dict(self) -> dict: "owner_id": str(self.owner_id), "name": self.name, "description": self.description, - "created_at": self.created_at.isoformat(), + "created_at": self.created_at, "tasks": [task.to_orm_dict() for task in self.tasks], } \ No newline at end of file diff --git a/src/domain/ports/diet_repository.py b/src/domain/ports/diet_repository.py index b820113..45cc960 100644 --- a/src/domain/ports/diet_repository.py +++ b/src/domain/ports/diet_repository.py @@ -24,7 +24,6 @@ async def update_diet(self, diet: DomainDiet) -> DomainDiet: async def delete_diet(self, id: UUID) -> None: pass -# Macro Plan methods @abstractmethod async def add_macro_plan(self, macro_plan: DomainMacroPlan) -> DomainMacroPlan: @@ -50,7 +49,6 @@ async def find_macro_plans_by_diet_id(self, diet_id: UUID) -> List[DomainMacroPl async def update_macro_plan(self, macro_plan: DomainMacroPlan) -> DomainMacroPlan: pass -# Meal Plan methods @abstractmethod async def add_meal_plan(self, meal_plan: DomainMealPlan) -> DomainMealPlan: diff --git a/src/domain/ports/training_repository.py b/src/domain/ports/training_repository.py index ab3fcba..e7aaeae 100644 --- a/src/domain/ports/training_repository.py +++ b/src/domain/ports/training_repository.py @@ -24,7 +24,6 @@ async def update_training(self, training: Training) -> Optional[Training]: async def find_all_owner_trainings(self, owner_id: UUID) -> Optional[List[Training]]: pass -# tasks abstract methods @abstractmethod async def add_task(self, task: Task) -> Optional[Task]: @@ -46,7 +45,6 @@ async def update_task(self, task: Task) -> Optional[Task]: async def find_tasks_by_training_id(self, training_id: UUID) -> Optional[List[Task]]: pass -# validate abstract methods @abstractmethod async def add_validate(self, validate: Validate) -> Optional[Validate]: diff --git a/src/domain/services/diet.py b/src/domain/services/diet.py index 510d5a3..690535c 100644 --- a/src/domain/services/diet.py +++ b/src/domain/services/diet.py @@ -45,7 +45,6 @@ async def delete_diet(self, diet_id: UUID) -> None: raise NotFoundError(f"Diet {diet_id} not found") await self._repo.delete_diet(diet_id) - # Macro Plan methods async def create_macro_plan( self, @@ -117,7 +116,6 @@ async def delete_macro_plan(self, plan_id: UUID) -> None: await self.get_macro_plan(plan_id) await self._repo.delete_macro_plan(plan_id) - # Meal Plan methods async def create_meal_plan( self, @@ -173,4 +171,3 @@ async def update_meal_plan( async def delete_meal_plan(self, plan_id: UUID) -> None: await self.get_meal_plan_by_id(plan_id) await self._repo.delete_meal_plan(plan_id) - diff --git a/src/domain/services/exercise.py b/src/domain/services/exercise.py index 3aef160..9f495eb 100644 --- a/src/domain/services/exercise.py +++ b/src/domain/services/exercise.py @@ -56,3 +56,10 @@ async def get_all_exercises(self) -> List[DomainExercise]: if not exercises: raise NotFoundError("No exercises found") return exercises + + async def get_by_id(self, exercise_id: UUID) -> DomainExercise: + exercise = await self._repo.find_by_id(exercise_id) + if not exercise: + raise NotFoundError(f"Exercise with id {exercise_id} not found") + return exercise + \ No newline at end of file diff --git a/src/domain/services/group.py b/src/domain/services/group.py index 8714a65..673774d 100644 --- a/src/domain/services/group.py +++ b/src/domain/services/group.py @@ -45,7 +45,14 @@ async def update(self, group: DomainGroup) -> DomainGroup: async def add_member(self, group_id: UUID, user_id: UUID) -> None: group = await self._repo.find_by_id(group_id) if not group: - raise NotFoundError(f"Group with id {group_id} not found") + raise NotFoundError(f"Group {group_id} not found") + + from src.container import container + profile_service = container.get_profile_service() + try: + user = await profile_service.get_by_id(user_id) + except NotFoundError: + raise NotFoundError(f"User {user_id} not found") await self._repo.add_member(group_id, user_id) @@ -82,6 +89,12 @@ async def get_all_groups(self) -> List[DomainGroup]: return groups + async def get_by_id(self, group_id: UUID) -> DomainGroup: + group = await self._repo.find_by_id(group_id) + if not group: + raise NotFoundError(f"Group with id {group_id} not found") + return group + async def get_my_coaches(self, user_id: UUID) -> List[DomainProfile]: groups = await self._repo.find_groups_by_member_id(user_id) if not groups: @@ -97,7 +110,7 @@ async def get_my_coaches(self, user_id: UUID) -> List[DomainProfile]: coaches = [] for coach_id in coach_ids: try: - coach = await profile_service._repo.find_by_id(coach_id) + coach = await profile_service.get_by_id(coach_id) if coach and "coach" in coach.roles: coaches.append(coach) except NotFoundError: diff --git a/src/domain/services/profile.py b/src/domain/services/profile.py index e5c54ea..c5d7d3e 100644 --- a/src/domain/services/profile.py +++ b/src/domain/services/profile.py @@ -13,6 +13,7 @@ def __init__(self, repo: ProfileRepository, hasher: PasswordHasher): self._repo = repo self._hasher = hasher + async def create(self, *, email: str, @@ -55,22 +56,23 @@ async def create(self, created_at=datetime.utcnow() ) - return await self._repo.add(profile) + return await self._repo.add(profile) async def delete(self, profile_id: UUID): profile = await self._repo.find_by_id(profile_id) if not profile: raise NotFoundError(f"Profile with id {profile_id} not found") + await self._repo.delete(profile_id) async def login(self, email: str, password: str) -> DomainProfile: + profile = await self._repo.find_by_email(email) + email_regex = r"^[\w\.-]+@[\w\.-]+\.\w+$" if not re.match(email_regex, email): raise InvalidFormatEmailError(f"Email {email} has invalid format") - profile = await self._repo.find_by_email(email) - if not profile: raise AuthenticationError(f"Invalid password or email") @@ -144,4 +146,4 @@ async def update_roles(self, id: UUID, roles: List[str]) -> DomainProfile: if not roles: raise ValueError("Roles cannot be empty") profile.roles = roles - return await self._repo.update(profile) + return await self._repo.update(profile) \ No newline at end of file diff --git a/src/domain/services/training.py b/src/domain/services/training.py index 75bf44e..b9a935d 100644 --- a/src/domain/services/training.py +++ b/src/domain/services/training.py @@ -55,8 +55,6 @@ async def get_all_owner_trainings(self, owner_id: UUID) -> List[DomainTraining]: return [] return trainings - # tasks methods service - async def create_task( self, training_id: UUID, @@ -88,12 +86,14 @@ async def create_task( return await self._repo.add_task(task) + async def get_task(self, task_id: UUID) -> DomainTask: task = await self._repo.find_task_by_id(task_id) if not task: raise NotFoundError(f"Task {task_id} not found") return task + async def update_task( self, task_id: UUID, @@ -123,6 +123,7 @@ async def update_task( return await self._repo.update_task(task) + async def delete_task(self, task_id: UUID) -> None: task = await self._repo.find_task_by_id(task_id) if not task: @@ -130,14 +131,13 @@ async def delete_task(self, task_id: UUID) -> None: await self._repo.delete_task(task_id) + async def list_tasks_for_training(self, training_id: UUID) -> List[DomainTask]: tasks = await self._repo.find_tasks_by_training_id(training_id) if tasks is None: return [] return tasks - # validate methods service - async def create_validate( self, task_id: UUID, @@ -153,6 +153,7 @@ async def create_validate( validate = DomainValidate( id=uuid4(), task_id=task_id, + exercise_name=task.exercise_name, rest_time=rest_time, repetitions=repetitions, set_number=set_number, diff --git a/src/entrypoints/api/deps/auth.py b/src/entrypoints/api/deps/auth.py index e4515de..f66065b 100644 --- a/src/entrypoints/api/deps/auth.py +++ b/src/entrypoints/api/deps/auth.py @@ -47,11 +47,8 @@ async def require_group_owner_or_admin( svc = container.get_group_service() try: - group = await svc._repo.find_by_id(group_id) - except Exception: - group = None - - if not group: + group = await svc.get_by_id(group_id) + except NotFoundError: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Group {group_id} not found" @@ -64,7 +61,7 @@ async def require_group_owner_or_admin( detail="Access denied: not owner or admin" ) - return user + return group async def require_exercice_owner_or_admin( exercise_id: UUID, @@ -73,11 +70,8 @@ async def require_exercice_owner_or_admin( svc = container.get_exercise_service() try: - exercise = await svc._repo.find_by_id(exercise_id) - except Exception: - exercise = None - - if not exercise: + exercise = await svc.get_by_id(exercise_id) + except NotFoundError: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Exercise {exercise_id} not found" @@ -89,7 +83,7 @@ async def require_exercice_owner_or_admin( detail="Access denied: not owner or admin" ) - return user + return exercise async def require_coach_for_user_or_admin( target_user_id: UUID, @@ -98,7 +92,7 @@ async def require_coach_for_user_or_admin( profile_svc = container.get_profile_service() try: - target_profile = await profile_svc.get_by_id(target_user_id) + target_profile = await profile_svc.get_by_id(target_user_id) # ✅ Ajout d'await except NotFoundError: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -119,13 +113,13 @@ async def require_coach_for_user_or_admin( group_svc = container.get_group_service() try: - coach_groups = await group_svc.list_owner_groups(UUID(user_sub)) + coach_groups = await group_svc.list_owner_groups(UUID(user_sub)) # ✅ Ajout d'await except NotFoundError: coach_groups = [] for grp in coach_groups: try: - members = await group_svc.list_members(grp.id) + members = await group_svc.list_members(grp.id) # ✅ Ajout d'await except NotFoundError: continue if any(m.id == target_user_id for m in members): @@ -150,7 +144,7 @@ async def require_training_owner_or_coach_or_admin( svc = container.get_training_service() try: - training = await svc.get_training(training_id) + training = await svc.get_training(training_id) # ✅ Ajout d'await except NotFoundError: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -163,10 +157,10 @@ async def require_training_owner_or_coach_or_admin( if "coach" in roles: group_svc = container.get_group_service() try: - coach_groups = await group_svc.list_owner_groups(UUID(sub)) + coach_groups = await group_svc.list_owner_groups(UUID(sub)) # ✅ Ajout d'await for grp in coach_groups: try: - members = await group_svc.list_members(grp.id) + members = await group_svc.list_members(grp.id) # ✅ Ajout d'await if any(m.id == training.owner_id for m in members): return user except NotFoundError: @@ -185,7 +179,7 @@ async def require_training_owner_or_admin( ): svc = container.get_training_service() try: - training = await svc.get_training(training_id) + training = await svc.get_training(training_id) except NotFoundError: raise HTTPException(status.HTTP_404_NOT_FOUND, f"Training {training_id} not found") @@ -194,7 +188,7 @@ async def require_training_owner_or_admin( if str(training.owner_id) != sub and "admin" not in roles: raise HTTPException(status.HTTP_403_FORBIDDEN, "Access denied: not owner, or admin") - return user + return training async def require_owner_coach_for_user_or_admin( @@ -204,7 +198,7 @@ async def require_owner_coach_for_user_or_admin( profile_svc = container.get_profile_service() try: - target_profile = await profile_svc.get_by_id(target_user_id) + target_profile = await profile_svc.get_by_id(target_user_id) except NotFoundError: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -215,9 +209,9 @@ async def require_owner_coach_for_user_or_admin( sub = user.get("sub") if sub == str(target_user_id): - return user + return target_profile if "admin" in roles: - return user + return target_profile return await require_coach_for_user_or_admin(target_user_id, user) \ No newline at end of file diff --git a/src/entrypoints/api/deps/roles.py b/src/entrypoints/api/deps/roles.py index 5ad3a24..28a6ba5 100644 --- a/src/entrypoints/api/deps/roles.py +++ b/src/entrypoints/api/deps/roles.py @@ -5,9 +5,9 @@ def require_roles(*allowed_roles: str): - async def dependency(user: dict = Depends(get_current_user)) -> dict: + def dependency(user: dict = Depends(get_current_user)) -> dict: user_roles: List[str] = user.get("roles", []) if not any(role in user_roles for role in allowed_roles): raise HTTPException(status_code=403, detail="Access forbidden") return user - return dependency + return dependency \ No newline at end of file diff --git a/src/entrypoints/api/routers/diet.py b/src/entrypoints/api/routers/diet.py index dc8dcde..0347cd8 100644 --- a/src/entrypoints/api/routers/diet.py +++ b/src/entrypoints/api/routers/diet.py @@ -66,7 +66,7 @@ async def delete_diet(diet_id: UUID, except NotFoundError: raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="Diet not found") -# Macro Plan endpoints + @router.get( "/{diet_id}/user/{target_user_id}/macro_plans", @@ -163,7 +163,7 @@ async def delete_macro_plan(diet_id: UUID, target_user_id: UUID, plan_id: UUID): except NotFoundError: raise HTTPException(HTTP_404_NOT_FOUND, "MacroPlan not found") -# Meal Plan endpoints + @router.get( "/{diet_id}/user/{target_user_id}/meal_plans", diff --git a/src/entrypoints/api/routers/exercise.py b/src/entrypoints/api/routers/exercise.py index 582b38b..9f6907a 100644 --- a/src/entrypoints/api/routers/exercise.py +++ b/src/entrypoints/api/routers/exercise.py @@ -97,5 +97,4 @@ async def delete_exercise( await service.delete_exercise(exercise_id) except NotFoundError: raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="Exercise not found") - - + diff --git a/src/entrypoints/api/routers/group.py b/src/entrypoints/api/routers/group.py index 1e9d00c..5c780d4 100644 --- a/src/entrypoints/api/routers/group.py +++ b/src/entrypoints/api/routers/group.py @@ -35,7 +35,7 @@ async def list_groups(): @router.get("/{group_id}", response_model=GroupRead, dependencies=[Depends(get_current_user)]) async def get_group(group_id: UUID): service = container.get_group_service() - grp = await service._repo.find_by_id(group_id) + grp = await service._repo.find_by_id(group_id) if not grp: raise HTTPException(404, "Group not found") return GroupRead.model_validate(grp) @@ -44,7 +44,7 @@ async def get_group(group_id: UUID): @router.patch("/{group_id}", response_model=GroupRead, dependencies=[Depends(require_group_owner_or_admin)]) async def patch_group(group_id: UUID, dto: GroupUpdate): service = container.get_group_service() - existing = await service._repo.find_by_id(group_id) + existing = await service._repo.find_by_id(group_id) if not existing: raise HTTPException(404, "Group not found") updated = existing @@ -52,7 +52,7 @@ async def patch_group(group_id: UUID, dto: GroupUpdate): updated.name = dto.name if dto.description is not None: updated.description = dto.description - grp = await service.update(updated) + grp = await service.update(updated) return GroupRead.model_validate(grp) @router.delete("/{group_id}", status_code=204, dependencies=[Depends(require_group_owner_or_admin)]) @@ -113,4 +113,3 @@ async def get_my_coaches(user=Depends(get_current_user)): return [CoachProfileRead.model_validate(coach) for coach in coaches] except NotFoundError as e: raise HTTPException(404, str(e)) - diff --git a/src/entrypoints/api/routers/training.py b/src/entrypoints/api/routers/training.py index 4f187c6..a99b361 100644 --- a/src/entrypoints/api/routers/training.py +++ b/src/entrypoints/api/routers/training.py @@ -95,7 +95,7 @@ async def delete_training(training_id: UUID, user=Depends(require_coach_for_user return {"detail": "Training deleted successfully"} - # Router for Task operations + @router.get( "/{training_id}/tasks", @@ -217,7 +217,7 @@ async def delete_task( ) return None -# Router for validate Operations + @router.post( "/{training_id}/tasks/{task_id}/validations", @@ -258,7 +258,8 @@ async def list_validations( ): service = container.get_training_service() try: - return await service.get_validates_for_task(task_id) + validations = await service.get_validates_for_task(task_id) + return [ValidateRead.model_validate(v) for v in validations] except NotFoundError: return [] @@ -291,6 +292,7 @@ async def get_validations_by_training( ): service = container.get_training_service() try: - return await service.get_validate_by_training_id(training_id) + validations = await service.get_validate_by_training_id(training_id) + return [ValidateRead.model_validate(v) for v in validations] except NotFoundError: return [] \ No newline at end of file diff --git a/src/entrypoints/api/tests/diet.py b/src/entrypoints/api/tests/diet.py index 2bc1ebf..69b244b 100644 --- a/src/entrypoints/api/tests/diet.py +++ b/src/entrypoints/api/tests/diet.py @@ -3,6 +3,119 @@ @pytest.mark.asyncio class TestDietScenario: + # Setup: ensure all required tokens exist + async def test_00_setup_prerequisites(self, client, test_state): + # Setup user if not exists + if 'user_token' not in test_state: + payload = { + "email": "alice@example.com", + "password": "Secret123!", + "confirm_password": "Secret123!", + "name": "Alice", + "sex": "F", + "age": 28 + } + r = await client.post("/profiles", json=payload) + assert r.status_code == 201 + body = r.json() + test_state["user_token"] = body["token"]["access_token"] + test_state["user_uuid"] = body["profile"]["id"] + + # Setup coach if not exists + if 'coach_token' not in test_state: + payload = { + "email": "coach@example.com", + "password": "CoachPass123!", + "confirm_password": "CoachPass123!", + "name": "Coach", + "sex": "M", + "age": 35 + } + r = await client.post("/profiles", json=payload) + assert r.status_code == 201 + body = r.json() + test_state["coach_token"] = body["token"]["access_token"] + test_state["coach_uuid"] = body["profile"]["id"] + + # Setup admin if not exists + if 'admin_token' not in test_state: + r = await client.post( + "/profiles/login", + json={"email": "admin@mail.fr", "password": "123456789"} + ) + assert r.status_code == 200 + test_state["admin_token"] = r.json()["access_token"] + + # Promote coach if not done + if 'coach_promoted' not in test_state: + update_data = {"roles": ["user", "coach"]} + r = await client.patch( + f"/profiles/{test_state['coach_uuid']}/roles", + json=update_data, + headers={"Authorization": f"Bearer {test_state['admin_token']}"} + ) + assert r.status_code == 200 + roles = r.json()["roles"] + assert "coach" in roles + test_state['coach_promoted'] = True + + # Re-login coach to get new token with coach role + if 'coach_relogged' not in test_state: + r = await client.post( + "/profiles/login", + json={"email": "coach@example.com", "password": "CoachPass123!"} + ) + assert r.status_code == 200 + test_state["coach_token"] = r.json()["access_token"] + test_state['coach_relogged'] = True + + # Create coach group if not exists + if 'group_id' not in test_state: + payload = {"name": "CoachGroup", "description": "Groupe du coach"} + r = await client.post( + "/groups", + json=payload, + headers={"Authorization": f"Bearer {test_state['coach_token']}"} + ) + assert r.status_code == 201 + grp = r.json() + test_state["group_id"] = grp["id"] + + # Add user to coach group if not done + if 'user_added_to_group' not in test_state: + # Vérifiez d'abord que le groupe existe + r_debug_group = await client.get( + f"/groups/{test_state['group_id']}", + headers={"Authorization": f"Bearer {test_state['coach_token']}"} + ) + print(f"Group exists check: {r_debug_group.status_code}") + if r_debug_group.status_code != 200: + print(f"Group check response: {r_debug_group.text}") + + # Vérifiez que l'utilisateur existe + r_debug_user = await client.get( + f"/profiles/{test_state['user_uuid']}", + headers={"Authorization": f"Bearer {test_state['coach_token']}"} + ) + print(f"User exists check: {r_debug_user.status_code}") + if r_debug_user.status_code != 200: + print(f"User check response: {r_debug_user.text}") + + # Tentative d'ajout du membre + r = await client.post( + f"/groups/{test_state['group_id']}/members/{test_state['user_uuid']}", + headers={"Authorization": f"Bearer {test_state['coach_token']}"} + ) + + # Debug en cas d'échec + if r.status_code != 204: + print(f"Add member failed: {r.status_code} - {r.text}") + print(f"Group UUID: {test_state['group_id']}") + print(f"User UUID: {test_state['user_uuid']}") + + assert r.status_code == 204, f"Failed to add member: {r.status_code} - {r.text}" + test_state['user_added_to_group'] = True + # create new user (user3) not in team async def test_01_create_new_user_not_in_team(self, client, test_state): payload = { diff --git a/src/entrypoints/api/tests/exercise.py b/src/entrypoints/api/tests/exercise.py index dc2d63f..7e1311e 100644 --- a/src/entrypoints/api/tests/exercise.py +++ b/src/entrypoints/api/tests/exercise.py @@ -4,6 +4,72 @@ @pytest.mark.asyncio class TestExercisesScenario: + # Setup: ensure all required tokens exist + async def test_00_setup_prerequisites(self, client, test_state): + # Setup user if not exists + if 'user_token' not in test_state: + payload = { + "email": "alice@example.com", + "password": "Secret123!", + "confirm_password": "Secret123!", + "name": "Alice", + "sex": "F", + "age": 28 + } + r = await client.post("/profiles", json=payload) + assert r.status_code == 201 + body = r.json() + test_state["user_token"] = body["token"]["access_token"] + test_state["user_uuid"] = body["profile"]["id"] + + # Setup coach if not exists + if 'coach_token' not in test_state: + payload = { + "email": "coach@example.com", + "password": "CoachPass123!", + "confirm_password": "CoachPass123!", + "name": "Coach", + "sex": "M", + "age": 35 + } + r = await client.post("/profiles", json=payload) + assert r.status_code == 201 + body = r.json() + test_state["coach_token"] = body["token"]["access_token"] + test_state["coach_uuid"] = body["profile"]["id"] + + # Setup admin if not exists + if 'admin_token' not in test_state: + r = await client.post( + "/profiles/login", + json={"email": "admin@mail.fr", "password": "123456789"} + ) + assert r.status_code == 200 + test_state["admin_token"] = r.json()["access_token"] + + # Promote coach if not done + if 'coach_promoted' not in test_state: + update_data = {"roles": ["user", "coach"]} + r = await client.patch( + f"/profiles/{test_state['coach_uuid']}/roles", + json=update_data, + headers={"Authorization": f"Bearer {test_state['admin_token']}"} + ) + assert r.status_code == 200 + roles = r.json()["roles"] + assert "coach" in roles + test_state['coach_promoted'] = True + + # Re-login coach to get new token with coach role + if 'coach_relogged' not in test_state: + r = await client.post( + "/profiles/login", + json={"email": "coach@example.com", "password": "CoachPass123!"} + ) + assert r.status_code == 200 + test_state["coach_token"] = r.json()["access_token"] + test_state['coach_relogged'] = True + # 05 – user tente de créer un exercice → 403 async def test_01_user_create_exercise_forbidden(self, client, test_state): payload = {"name": "Exo1", "description": "desc exo1"} diff --git a/src/entrypoints/api/tests/group.py b/src/entrypoints/api/tests/group.py index cfead21..1c797d6 100644 --- a/src/entrypoints/api/tests/group.py +++ b/src/entrypoints/api/tests/group.py @@ -459,4 +459,3 @@ async def test_38_user_leave_coach_groups(self, client, test_state): # coach try to delete admin group 403 # admin delete admin group 204 # coach delete own group 204 - diff --git a/src/entrypoints/api/tests/profile.py b/src/entrypoints/api/tests/profile.py index e0717c3..fb09c6a 100644 --- a/src/entrypoints/api/tests/profile.py +++ b/src/entrypoints/api/tests/profile.py @@ -235,4 +235,4 @@ async def test_25_admin_can_delete_coach(self, client): f"/profiles/{self.__class__.coach_uuid}", headers={"Authorization": f"Bearer {self.__class__.admin_token}"} ) - assert r.status_code == 204 + assert r.status_code == 204 \ No newline at end of file diff --git a/src/entrypoints/api/tests/training.py b/src/entrypoints/api/tests/training.py index f11e401..b73f9b4 100644 --- a/src/entrypoints/api/tests/training.py +++ b/src/entrypoints/api/tests/training.py @@ -37,6 +37,72 @@ @pytest.mark.asyncio class TestTrainingScenario: + # Setup: ensure all required tokens exist + async def test_00_setup_prerequisites(self, client, test_state): + # Setup user if not exists + if 'user_token' not in test_state: + payload = { + "email": "alice@example.com", + "password": "Secret123!", + "confirm_password": "Secret123!", + "name": "Alice", + "sex": "F", + "age": 28 + } + r = await client.post("/profiles", json=payload) + assert r.status_code == 201 + body = r.json() + test_state["user_token"] = body["token"]["access_token"] + test_state["user_uuid"] = body["profile"]["id"] + + # Setup coach if not exists + if 'coach_token' not in test_state: + payload = { + "email": "coach@example.com", + "password": "CoachPass123!", + "confirm_password": "CoachPass123!", + "name": "Coach", + "sex": "M", + "age": 35 + } + r = await client.post("/profiles", json=payload) + assert r.status_code == 201 + body = r.json() + test_state["coach_token"] = body["token"]["access_token"] + test_state["coach_uuid"] = body["profile"]["id"] + + # Setup admin if not exists + if 'admin_token' not in test_state: + r = await client.post( + "/profiles/login", + json={"email": "admin@mail.fr", "password": "123456789"} + ) + assert r.status_code == 200 + test_state["admin_token"] = r.json()["access_token"] + + # Promote coach if not done + if 'coach_promoted' not in test_state: + update_data = {"roles": ["user", "coach"]} + r = await client.patch( + f"/profiles/{test_state['coach_uuid']}/roles", + json=update_data, + headers={"Authorization": f"Bearer {test_state['admin_token']}"} + ) + assert r.status_code == 200 + roles = r.json()["roles"] + assert "coach" in roles + test_state['coach_promoted'] = True + + # Re-login coach to get new token with coach role + if 'coach_relogged' not in test_state: + r = await client.post( + "/profiles/login", + json={"email": "coach@example.com", "password": "CoachPass123!"} + ) + assert r.status_code == 200 + test_state["coach_token"] = r.json()["access_token"] + test_state['coach_relogged'] = True + # coach create an group → 201 async def test_01_coach_create_group(self, client, test_state): payload = {"name": "Group1", "description": "Test Group"} @@ -188,7 +254,7 @@ async def test_14_user_get_all_mine_trainings(self, client, test_state): async def test_16_user_update_task_forbidden(self, client, test_state): payload = {"name": "Task1 Updated", "description": "Test Task Updated"} r = await client.patch( - f"trainings/{test_state['training_id']}/user/{test_state['user_uuid']}/tasks/{test_state['task_id']}", + f"/trainings/{test_state['training_id']}/user/{test_state['user_uuid']}/tasks/{test_state['task_id']}", # ✅ Ajout de /trainings json=payload, headers={"Authorization": f"Bearer {test_state['user_token']}"} ) @@ -197,7 +263,7 @@ async def test_16_user_update_task_forbidden(self, client, test_state): async def test_17_admin_update_task(self, client, test_state): payload = {"name": "Task1 Updated", "description": "Test Task Updated"} r = await client.patch( - f"trainings/{test_state['training_id']}/user/{test_state['user_uuid']}/tasks/{test_state['task_id']}", + f"/trainings/{test_state['training_id']}/user/{test_state['user_uuid']}/tasks/{test_state['task_id']}", # ✅ Ajout de /trainings json=payload, headers={"Authorization": f"Bearer {test_state['admin_token']}"} ) @@ -206,7 +272,7 @@ async def test_17_admin_update_task(self, client, test_state): async def test_18_coach_update_task(self, client, test_state): payload = {"name": "Task1 Updated", "description": "Test Task Updated"} r = await client.patch( - f"trainings/{test_state['training_id']}/user/{test_state['user_uuid']}/tasks/{test_state['task_id']}", + f"/trainings/{test_state['training_id']}/user/{test_state['user_uuid']}/tasks/{test_state['task_id']}", # ✅ Ajout de /trainings json=payload, headers={"Authorization": f"Bearer {test_state['coach_token']}"} ) @@ -229,7 +295,7 @@ async def test_19_create_new_user(self, client, test_state): async def test_20_user_create_validation_for_task(self, client, test_state): payload = {"comment": "Good job!", "score": 5} r = await client.post( - f"trainings/{test_state['training_id']}/tasks/{test_state['task_id']}/validations", + f"/trainings/{test_state['training_id']}/tasks/{test_state['task_id']}/validations", # ✅ Ajout de /trainings json=payload, headers={"Authorization": f"Bearer {test_state['user_token']}"} ) @@ -328,4 +394,4 @@ async def test_32_admin_delete_validation_for_task_not_found(self, client, test_ f"/trainings/{test_state['training_id']}/tasks/{test_state['task_id']}/validations/{test_state['validation_uuid']}", headers={"Authorization": f"Bearer {test_state['admin_token']}"} ) - assert r.status_code == 404 + assert r.status_code == 404 \ No newline at end of file diff --git a/src/main.py b/src/main.py index 5f9e904..f8d2d76 100644 --- a/src/main.py +++ b/src/main.py @@ -40,9 +40,9 @@ app.include_router(diet_router) -@app.on_event("startup") -async def startup_event(): - await init_db() # ✅ Créer les tables au démarrage +# @app.on_event("startup") +# async def startup_event(): +# await init_db() # ✅ Créer les tables au démarrage From 8e6f47ae51acb269efb390027bf6126faff0ee5c Mon Sep 17 00:00:00 2001 From: Baptiste-Ferrand Date: Sun, 10 Aug 2025 15:36:50 +0200 Subject: [PATCH 3/6] remove init db in start (main.py) --- src/main.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/main.py b/src/main.py index f8d2d76..cea3c0e 100644 --- a/src/main.py +++ b/src/main.py @@ -6,7 +6,6 @@ from src.entrypoints.api.routers.exercise import router as exercise_router from src.entrypoints.api.routers.training import router as training_router from src.entrypoints.api.routers.diet import router as diet_router -from src.adapters.sqlalchemy.db import init_db app = FastAPI( @@ -37,12 +36,4 @@ app.include_router(group_router) app.include_router(exercise_router) app.include_router(training_router) -app.include_router(diet_router) - - -# @app.on_event("startup") -# async def startup_event(): -# await init_db() # ✅ Créer les tables au démarrage - - - +app.include_router(diet_router) \ No newline at end of file From 9a8bd23c2eee9cffefd0afef000c8e61f9f7db86 Mon Sep 17 00:00:00 2001 From: Baptiste-Ferrand Date: Sun, 10 Aug 2025 15:41:43 +0200 Subject: [PATCH 4/6] put it back the older conftest --- src/entrypoints/api/tests/conftest.py | 67 ++++++++++++++------------- src/main.py | 4 +- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/src/entrypoints/api/tests/conftest.py b/src/entrypoints/api/tests/conftest.py index 10232ad..100c352 100644 --- a/src/entrypoints/api/tests/conftest.py +++ b/src/entrypoints/api/tests/conftest.py @@ -1,35 +1,36 @@ -# app/adapters/sqlalchemy/repositories/postgres.py -from src.adapters.sqlalchemy.models import Base -from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession -from dotenv import load_dotenv +import pytest import os +from httpx import AsyncClient, ASGITransport +from src.main import app +from src.container import Container +import pytest_asyncio -load_dotenv() -db_url = os.getenv("DATABASE_URL") or "postgresql://user:user@localhost:5432/postgres" - -# Convert sync URL to async URL for PostgreSQL -if db_url.startswith("postgresql://"): - db_url = db_url.replace("postgresql://", "postgresql+asyncpg://", 1) - -engine = create_async_engine( - db_url, - pool_size=20, - max_overflow=20, - pool_timeout=30, -) - -# Create tables - note: this will need to be called in an async context -async def create_tables(): - async with engine.begin() as conn: - await conn.run_sync(Base.metadata.create_all) - -SessionLocal = async_sessionmaker( - engine, - class_=AsyncSession, - autocommit=False, - autoflush=False -) - -async def get_async_session() -> AsyncSession: - async with SessionLocal() as session: - yield session + + +@pytest.fixture(scope="session", autouse=True) +def set_test_env(): + os.environ["ENV"] = "test" + yield + + +@pytest.fixture(scope="module", autouse=True) +def container(): + c = Container(env="test") + + assert os.getenv("ENV") == "test", "L'environnement n'est pas correctement configuré sur 'test'." + + return c + + +@pytest_asyncio.fixture +async def client(container): + + transport = ASGITransport(app=app) + + async with AsyncClient(transport=transport, base_url="http://testserver") as ac: + yield ac + +@pytest.fixture(scope="session") +def test_state(): + + return {} \ No newline at end of file diff --git a/src/main.py b/src/main.py index cea3c0e..78aa90d 100644 --- a/src/main.py +++ b/src/main.py @@ -36,4 +36,6 @@ app.include_router(group_router) app.include_router(exercise_router) app.include_router(training_router) -app.include_router(diet_router) \ No newline at end of file +app.include_router(diet_router) + + From d3556757ae91bb0718927d6f9d992b81d5d68902 Mon Sep 17 00:00:00 2001 From: Baptiste-Ferrand Date: Sun, 10 Aug 2025 18:57:52 +0200 Subject: [PATCH 5/6] fix await for delete methode in sql repo and old method was syncrhone --- .../inmemory/repositories/training.py | 3 + src/adapters/sqlalchemy/repositories/diet.py | 7 +- src/adapters/sqlalchemy/repositories/group.py | 88 ++++++++++--------- .../sqlalchemy/repositories/training.py | 24 ++++- 4 files changed, 73 insertions(+), 49 deletions(-) diff --git a/src/adapters/inmemory/repositories/training.py b/src/adapters/inmemory/repositories/training.py index 2970d1c..388f22b 100644 --- a/src/adapters/inmemory/repositories/training.py +++ b/src/adapters/inmemory/repositories/training.py @@ -12,6 +12,7 @@ def __init__(self): self._tasks: dict[UUID, DomainTask] = {} self._validates: dict[UUID, DomainValidate] = {} + # Training methods async def find_by_id(self, id: UUID) -> Optional[DomainTraining]: return self._trainings.get(id) @@ -38,6 +39,7 @@ async def update_training(self, training: DomainTraining) -> Optional[DomainTrai async def find_all_owner_trainings(self, owner_id: UUID) -> List[DomainTraining]: return [t for t in self._trainings.values() if t.owner_id == owner_id] + # Task methods async def add_task(self, task: DomainTask) -> DomainTask: new_id = uuid4() task.id = new_id @@ -65,6 +67,7 @@ async def update_task(self, task: DomainTask) -> Optional[DomainTask]: async def find_tasks_by_training_id(self, training_id: UUID) -> List[DomainTask]: return [t for t in self._tasks.values() if t.training_id == training_id] + # Validate methods async def add_validate(self, validate: DomainValidate) -> DomainValidate: new_id = uuid4() validate.id = new_id diff --git a/src/adapters/sqlalchemy/repositories/diet.py b/src/adapters/sqlalchemy/repositories/diet.py index 5a12eb5..2cf1910 100644 --- a/src/adapters/sqlalchemy/repositories/diet.py +++ b/src/adapters/sqlalchemy/repositories/diet.py @@ -166,6 +166,7 @@ async def update_meal_plan(self, mp: DomainMealPlan) -> DomainMealPlan: async def delete_meal_plan(self, id: UUID) -> None: orm = await self._session.get(ORMMealPlan, id) - if orm: - await self._session.delete(orm) - await self._session.commit() \ No newline at end of file + if not orm: + return + await self._session.delete(orm) + await self._session.commit() \ No newline at end of file diff --git a/src/adapters/sqlalchemy/repositories/group.py b/src/adapters/sqlalchemy/repositories/group.py index e47b630..cc8929e 100644 --- a/src/adapters/sqlalchemy/repositories/group.py +++ b/src/adapters/sqlalchemy/repositories/group.py @@ -52,54 +52,56 @@ async def update(self, group: DomainGroup) -> Optional[DomainGroup]: async def add_member(self, group_id: UUID, user_id: UUID) -> None: - async with self._session_factory() as session: - group_stmt = select(ORMGroup).where(ORMGroup.id == group_id) - group_result = await session.execute(group_stmt) - group = group_result.scalar_one_or_none() - - if not group: - raise NotFoundError(f"Group {group_id} not found") - - user_stmt = select(ORMProfile).where(ORMProfile.id == user_id) - user_result = await session.execute(user_stmt) - user = user_result.scalar_one_or_none() - - if not user: - raise NotFoundError(f"User {user_id} not found") - - existing_stmt = select(group_users).where( - (group_users.c.group_id == group_id) & - (group_users.c.profile_id == user_id) - ) - existing_result = await session.execute(existing_stmt) - existing = existing_result.scalar_one_or_none() - - if existing: - return - - insert_stmt = group_users.insert().values( - group_id=group_id, - profile_id=user_id - ) - await session.execute(insert_stmt) - await session.commit() + group_stmt = select(ORMGroup).where(ORMGroup.id == group_id) + group_result = await self._session.execute(group_stmt) + group = group_result.scalar_one_or_none() + + if not group: + raise NotFoundError(f"Group {group_id} not found") + + user_stmt = select(ORMProfile).where(ORMProfile.id == user_id) + user_result = await self._session.execute(user_stmt) + user = user_result.scalar_one_or_none() + + if not user: + raise NotFoundError(f"User {user_id} not found") + + existing_stmt = select(group_users).where( + (group_users.c.group_id == group_id) & + (group_users.c.profile_id == user_id) + ) + existing_result = await self._session.execute(existing_stmt) + existing = existing_result.scalar_one_or_none() + + if existing: + return + + insert_stmt = group_users.insert().values( + group_id=group_id, + profile_id=user_id + ) + await self._session.execute(insert_stmt) + await self._session.commit() async def remove_member(self, group_id: UUID, user_id: UUID) -> None: - orm_group = await self._session.get(ORMGroup, group_id) - if not orm_group: - raise NotFoundError(f"Groupe {group_id} not found") - orm_profile = await self._session.get(ORMProfile, user_id) - if not orm_profile: - raise NotFoundError(f"Profile {user_id} not found") - if orm_profile in orm_group.users: - orm_group.users.remove(orm_profile) - await self._session.commit() + delete_stmt = group_users.delete().where( + (group_users.c.group_id == group_id) & + (group_users.c.profile_id == user_id) + ) + result = await self._session.execute(delete_stmt) + if result.rowcount == 0: + raise NotFoundError(f"Member {user_id} not found in group {group_id}") + await self._session.commit() async def list_members(self, group_id: UUID) -> List[DomainProfile]: - orm_grp = await self._session.get(ORMGroup, group_id) - if not orm_grp: + group = await self._session.get(ORMGroup, group_id) + if not group: raise NotFoundError(f"Groupe {group_id} introuvable") - return [profil_from_orm(p) for p in orm_grp.users] + + stmt = select(ORMProfile).join(group_users).where(group_users.c.group_id == group_id) + result = await self._session.execute(stmt) + profiles = result.scalars().all() + return [profil_from_orm(p) for p in profiles] async def find_by_owner_id(self, owner_id: UUID) -> Optional[List[DomainGroup]]: stmt = select(ORMGroup).where(ORMGroup.owner_id == owner_id) diff --git a/src/adapters/sqlalchemy/repositories/training.py b/src/adapters/sqlalchemy/repositories/training.py index 4e6e55f..f41023f 100644 --- a/src/adapters/sqlalchemy/repositories/training.py +++ b/src/adapters/sqlalchemy/repositories/training.py @@ -32,7 +32,7 @@ def task_from_orm(orm_task) -> DomainTask: method=orm_task.method, rir=orm_task.rir, updated_at=orm_task.updated_at, - validate=[validate_from_orm(v) for v in orm_task.validations] if orm_task.validations else None, + validate=None, ) def training_from_orm(orm_training) -> DomainTraining: @@ -93,8 +93,26 @@ async def add_task(self, task: DomainTask) -> Optional[DomainTask]: return task_from_orm(orm) if orm else None async def find_task_by_id(self, id: UUID) -> Optional[DomainTask]: - orm = await self._session.get(ORMTask, id) - return task_from_orm(orm) if orm else None + orm_task = await self._session.get(ORMTask, id) + if not orm_task: + return None + + validations_stmt = select(ORMValidate).where(ORMValidate.task_id == id) + validations_result = await self._session.execute(validations_stmt) + validations = validations_result.scalars().all() + + return DomainTask( + id=orm_task.id, + training_id=orm_task.training_id, + exercise_name=orm_task.exercise_name, + rest_time=orm_task.rest_time, + repetitions=orm_task.repetitions, + set_number=orm_task.set_number, + method=orm_task.method, + rir=orm_task.rir, + updated_at=orm_task.updated_at, + validate=None, + ) async def delete_task(self, id: UUID) -> None: orm = await self._session.get(ORMTask, id) From 12efd054ae30faa154f5770ff76704fa5a4418f7 Mon Sep 17 00:00:00 2001 From: Baptiste-Ferrand Date: Sun, 10 Aug 2025 19:08:26 +0200 Subject: [PATCH 6/6] clear code and update .env.example --- .env.example | 2 +- src/entrypoints/api/tests/training.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index 72b6c36..dc68c06 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ # BDD ENvironment Variables -DATABASE_URL = "postgresql://user:password@localhost:5432/mydatabase" +DATABASE_URL = "postgresql+asyncpg://user:password@localhost:5432/mydatabase" ENV = "ENV" SECRET_KEY= "jwt sercret" ACCESS_TOKEN_EXPIRE_MINUTES = "in minutes" \ No newline at end of file diff --git a/src/entrypoints/api/tests/training.py b/src/entrypoints/api/tests/training.py index b73f9b4..2253459 100644 --- a/src/entrypoints/api/tests/training.py +++ b/src/entrypoints/api/tests/training.py @@ -254,7 +254,7 @@ async def test_14_user_get_all_mine_trainings(self, client, test_state): async def test_16_user_update_task_forbidden(self, client, test_state): payload = {"name": "Task1 Updated", "description": "Test Task Updated"} r = await client.patch( - f"/trainings/{test_state['training_id']}/user/{test_state['user_uuid']}/tasks/{test_state['task_id']}", # ✅ Ajout de /trainings + f"/trainings/{test_state['training_id']}/user/{test_state['user_uuid']}/tasks/{test_state['task_id']}", json=payload, headers={"Authorization": f"Bearer {test_state['user_token']}"} ) @@ -263,7 +263,7 @@ async def test_16_user_update_task_forbidden(self, client, test_state): async def test_17_admin_update_task(self, client, test_state): payload = {"name": "Task1 Updated", "description": "Test Task Updated"} r = await client.patch( - f"/trainings/{test_state['training_id']}/user/{test_state['user_uuid']}/tasks/{test_state['task_id']}", # ✅ Ajout de /trainings + f"/trainings/{test_state['training_id']}/user/{test_state['user_uuid']}/tasks/{test_state['task_id']}", json=payload, headers={"Authorization": f"Bearer {test_state['admin_token']}"} ) @@ -272,7 +272,7 @@ async def test_17_admin_update_task(self, client, test_state): async def test_18_coach_update_task(self, client, test_state): payload = {"name": "Task1 Updated", "description": "Test Task Updated"} r = await client.patch( - f"/trainings/{test_state['training_id']}/user/{test_state['user_uuid']}/tasks/{test_state['task_id']}", # ✅ Ajout de /trainings + f"/trainings/{test_state['training_id']}/user/{test_state['user_uuid']}/tasks/{test_state['task_id']}", json=payload, headers={"Authorization": f"Bearer {test_state['coach_token']}"} ) @@ -295,7 +295,7 @@ async def test_19_create_new_user(self, client, test_state): async def test_20_user_create_validation_for_task(self, client, test_state): payload = {"comment": "Good job!", "score": 5} r = await client.post( - f"/trainings/{test_state['training_id']}/tasks/{test_state['task_id']}/validations", # ✅ Ajout de /trainings + f"/trainings/{test_state['training_id']}/tasks/{test_state['task_id']}/validations", json=payload, headers={"Authorization": f"Bearer {test_state['user_token']}"} )