diff --git a/docker-compose.yml b/docker-compose.yml index 33ddb56..7ff1040 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,7 +23,7 @@ services: ports: - '8000:8000' environment: - ENV: prod + ENV: test SECRET_KEY: 123456789 ACCESS_TOKEN_EXPIRE_MINUTES: 60 - DATABASE_URL: postgresql://user:user@91.169.178.154:5400/postgres \ No newline at end of file + DATABASE_URL: postgresql+asyncpg://user:user@91.169.178.154:5400/postgres \ No newline at end of file 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..bb9ff01 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,19 +21,19 @@ 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: @@ -43,51 +43,51 @@ def delete_diet(self, id: UUID) -> None: 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..38ac76f 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,27 @@ 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: + try: + profile = await self._profile_repo.find_by_id(uid) + if profile: + members.append(profile) + except: + pass + 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..4ae24b0 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) - 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) - 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..b3ed5de 100644 --- a/src/adapters/sqlalchemy/db.py +++ b/src/adapters/sqlalchemy/db.py @@ -1,22 +1,36 @@ # 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, 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" -engine = create_engine( +# 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, ) -Base.metadata.create_all(bind=engine) +# 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 +) -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +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 1c36e3d..2efe027 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 import select from src.domain.exceptions import NotFoundError from uuid import UUID @@ -43,139 +44,130 @@ def meal_plan_from_orm(orm: ORMMealPlan) -> DomainMealPlan: ) class SqlAlchemyDietRepository(DietRepository): - def __init__(self, session: Session): + def __init__(self, session: AsyncSession): self._session = session - 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) + await self._session.commit() + await self._session.refresh(orm) return diet_from_orm(orm) - def find_by_id(self, id: UUID) -> Optional[DomainDiet]: - orm = self._session.get(ORMDiet, id) + async def find_by_id(self, id: UUID) -> Optional[DomainDiet]: + orm = await 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() - ) + async def find_all_owner_diets(self, owner_id: UUID) -> List[DomainDiet]: + 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] - def update_diet(self, diet: DomainDiet) -> DomainDiet: - orm = self._session.get(ORMDiet, diet.id) + async def update_diet(self, diet: DomainDiet) -> DomainDiet: + 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) - self._session.commit() + await self._session.commit() return diet_from_orm(orm) - def delete_diet(self, id: UUID) -> None: - orm = self._session.get(ORMDiet, id) + async def delete_diet(self, id: UUID) -> None: + orm = await self._session.get(ORMDiet, id) if not orm: return - self._session.delete(orm) - self._session.commit() + await self._session.delete(orm) + await self._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) + await self._session.commit() + await 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) + async def find_macro_plan_by_id(self, id: UUID) -> Optional[DomainMacroPlan]: + orm = await 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() - ) + async def find_macro_plans_by_diet_id(self, diet_id: UUID) -> List[DomainMacroPlan]: + 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] - def find_macro_plans_by_user_id(self, user_id: UUID) -> List[DomainMacroPlan]: - orms = ( - self._session - .query(ORMMacroPlan) + async def find_macro_plans_by_user_id(self, user_id: UUID) -> List[DomainMacroPlan]: + stmt = ( + select(ORMMacroPlan) .join(ORMDiet, ORMDiet.id == ORMMacroPlan.diet_id) - .filter(ORMDiet.owner_id == user_id) - .all() + .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] - def update_macro_plan(self, macro_plan: DomainMacroPlan) -> DomainMacroPlan: - orm = self._session.get(ORMMacroPlan, macro_plan.id) + async def update_macro_plan(self, macro_plan: DomainMacroPlan) -> DomainMacroPlan: + 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) - self._session.commit() + await self._session.commit() return macro_plan_from_orm(orm) - def delete_macro_plan(self, id: UUID) -> None: - orm = self._session.get(ORMMacroPlan, id) + async def delete_macro_plan(self, id: UUID) -> None: + orm = await self._session.get(ORMMacroPlan, id) if not orm: return - self._session.delete(orm) - self._session.commit() + await self._session.delete(orm) + await self._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) + await self._session.commit() + await 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) + async def find_meal_plan_by_id(self, id: UUID) -> Optional[DomainMealPlan]: + orm = await 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() - ) + async def find_meal_plans_by_diet_id(self, diet_id: UUID) -> List[DomainMealPlan]: + 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] - 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() + async def find_meal_plans_by_user_id(self, user_id: UUID) -> List[DomainMealPlan]: + 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] - def update_meal_plan(self, mp: DomainMealPlan) -> DomainMealPlan: - orm = self._session.get(ORMMealPlan, mp.id) + async def update_meal_plan(self, mp: DomainMealPlan) -> DomainMealPlan: + 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) - self._session.commit() + await self._session.commit() return meal_plan_from_orm(orm) - def delete_meal_plan(self, id: UUID) -> None: - orm = self._session.get(ORMMealPlan, id) + async def delete_meal_plan(self, id: UUID) -> None: + orm = await self._session.get(ORMMealPlan, id) if orm: - self._session.delete(orm) - self._session.commit() \ No newline at end of file + 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 4e04c08..ebdcb27 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 import select from src.domain.exceptions import NotFoundError from uuid import UUID @@ -19,42 +20,46 @@ def exercise_from_orm(orm_exercise) -> DomainExercise: ) class SqlAlchemyExerciseRepository(ExerciseRepository): - def __init__(self, session: Session): + def __init__(self, session: AsyncSession): self._session = session # 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) + await self._session.commit() + await self._session.refresh(orm) return exercise_from_orm(orm) if orm else None - def delete(self, id: UUID) -> None: - orm = self._session.get(ORMExercise, id) + async def delete(self, id: UUID) -> None: + orm = await self._session.get(ORMExercise, id) if not orm: return - self._session.delete(orm) - self._session.commit() + await self._session.delete(orm) + await self._session.commit() - def update(self, exercise: DomainExercise) -> Optional[DomainExercise]: - orm = self._session.get(ORMExercise, exercise.id) + async def update(self, exercise: DomainExercise) -> Optional[DomainExercise]: + 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) - self._session.commit() + await self._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() + async def find_all_owner(self, owner_id: UUID) -> Optional[List[DomainExercise]]: + 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 [] - def find_all(self) -> Optional[List[DomainExercise]]: - orms = self._session.query(ORMExercise).all() + async def find_all(self) -> Optional[List[DomainExercise]]: + 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 [] - def find_by_id(self, id: UUID) -> Optional[DomainExercise]: - orm = self._session.get(ORMExercise, id) + async def find_by_id(self, id: UUID) -> Optional[DomainExercise]: + 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 0553465..d9c9a29 100644 --- a/src/adapters/sqlalchemy/repositories/group.py +++ b/src/adapters/sqlalchemy/repositories/group.py @@ -1,5 +1,6 @@ from typing import Optional, List -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession +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 @@ -18,74 +19,106 @@ def group_from_orm(orm_group) -> DomainGroup: ) class SqlAlchemyGroupRepository(GroupRepository): - def __init__(self, session: Session): + def __init__(self, session: AsyncSession): self._session = session - def find_by_id(self, id: UUID) -> Optional[DomainGroup]: - orm = self._session.get(ORMGroup, id) + async def find_by_id(self, id: UUID) -> Optional[DomainGroup]: + orm = await self._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) + await self._session.commit() + await self._session.refresh(orm) return group_from_orm(orm) if orm else None - def delete(self, id: UUID) -> None: - orm = self._session.get(ORMGroup, id) + async def delete(self, id: UUID) -> None: + orm = await self._session.get(ORMGroup, id) if not orm: return - self._session.delete(orm) - self._session.commit() + await self._session.delete(orm) + await self._session.commit() - def update(self, group: DomainGroup) -> Optional[DomainGroup]: - orm = self._session.get(ORMGroup, group.id) + async def update(self, group: DomainGroup) -> Optional[DomainGroup]: + 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) - self._session.commit() + await self._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: + # Vérifier que le groupe existe + 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") + + # Vérifier que l'utilisateur existe + 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") + + # Vérifier que l'utilisateur n'est pas déjà membre + 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 # Déjà membre, on ne fait rien + + # Ajouter la relation + insert_stmt = group_users.insert().values( + group_id=group_id, + profile_id=user_id + ) + await session.execute(insert_stmt) + await session.commit() - def remove_member(self, group_id: UUID, user_id: UUID) -> None: - orm_group = self._session.get(ORMGroup, group_id) + 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 = self._session.get(ORMProfile, user_id) + 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) - self._session.commit() + await self._session.commit() - def list_members(self, group_id: UUID) -> List[DomainProfile]: - orm_grp = self._session.get(ORMGroup, group_id) + async def list_members(self, group_id: UUID) -> List[DomainProfile]: + 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] - def find_by_owner_id(self, owner_id: UUID) -> Optional[List[DomainGroup]]: - orms = self._session.query(ORMGroup).filter(ORMGroup.owner_id == owner_id).all() + async def find_by_owner_id(self, owner_id: UUID) -> Optional[List[DomainGroup]]: + 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 [] - def find_all_groups(self) -> Optional[List[DomainGroup]]: - orms = self._session.query(ORMGroup).all() + async def find_all_groups(self) -> Optional[List[DomainGroup]]: + 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 [] - 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() + async def find_groups_by_member_id(self, user_id: UUID) -> Optional[List[DomainGroup]]: + 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 87ab2bc..e600d1d 100644 --- a/src/adapters/sqlalchemy/repositories/profile.py +++ b/src/adapters/sqlalchemy/repositories/profile.py @@ -1,5 +1,6 @@ from typing import Optional -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select 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 @@ -24,47 +25,53 @@ def profil_from_orm(orm_profile) ->DomainProfile: class SqlAlchemyProfileRepository(ProfileRepository): - def __init__(self, session: Session): + def __init__(self, session: AsyncSession): self._session = session - def find_by_email(self, email: str) -> Optional[DomainProfile]: - orm = self._session.query(ORMProfile).filter(ORMProfile.email == email).one_or_none() + 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 - def add(self, profile: DomainProfile) -> Optional[DomainProfile]: + async 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) + await self._session.commit() + await self._session.refresh(orm) return profil_from_orm(orm) if orm else None - def find_by_id(self, id: UUID) -> Optional[DomainProfile]: + async def find_by_id(self, id: UUID) -> Optional[DomainProfile]: # SQLAlchemy 1.4+ : session.get - orm = self._session.get(ORMProfile, id) + orm = await 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) + async def delete(self, id: UUID) -> None: + orm = await self._session.get(ORMProfile, id) if not orm: return - self._session.delete(orm) - self._session.commit() + await self._session.delete(orm) + await self._session.commit() - def update(self, profile: DomainProfile) -> Optional[DomainProfile]: - orm = self._session.get(ORMProfile, profile.id) + async def update(self, profile: DomainProfile) -> Optional[DomainProfile]: + orm = await self._session.get(ORMProfile, profile.id) if not orm: return None for key, value in profile.to_orm_dict().items(): setattr(orm, key, value) - self._session.commit() - self._session.refresh(orm) + await self._session.commit() + await 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() + 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 [] - def find_all_coachs(self)-> list[DomainProfile]: - orms = self._session.query(ORMProfile).filter(ORMProfile.roles.any('coach')).all() + 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 6f20763..6cbfd42 100644 --- a/src/adapters/sqlalchemy/repositories/training.py +++ b/src/adapters/sqlalchemy/repositories/training.py @@ -1,5 +1,6 @@ from typing import List, Optional -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession +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 @@ -44,107 +45,113 @@ def training_from_orm(orm_training) -> DomainTraining: ) class SqlAlchemyTrainingRepository(TrainingRepository): - def __init__(self, session: Session): + def __init__(self, session: AsyncSession): self._session = session - def find_by_id(self, id: UUID) -> Optional[DomainTraining]: - orm = self._session.get(ORMTraining, id) + async def find_by_id(self, id: UUID) -> Optional[DomainTraining]: + orm = await self._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) + await self._session.commit() + await self._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) + async def delete_training(self, id: UUID) -> None: + orm = await self._session.get(ORMTraining, id) if not orm: return - self._session.delete(orm) - self._session.commit() + await self._session.delete(orm) + await self._session.commit() - def update_training(self, training: DomainTraining) -> Optional[DomainTraining]: - orm = self._session.get(ORMTraining, training.id) + async def update_training(self, training: DomainTraining) -> Optional[DomainTraining]: + 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) - self._session.commit() + await self._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() + async def find_all_owner_trainings(self, owner_id: UUID) -> Optional[List[DomainTraining]]: + 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 - 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) + await self._session.commit() + await self._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) + 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 - def delete_task(self, id: UUID) -> None: - orm = self._session.get(ORMTask, id) + async def delete_task(self, id: UUID) -> None: + orm = await self._session.get(ORMTask, id) if not orm: return - self._session.delete(orm) - self._session.commit() + await self._session.delete(orm) + await self._session.commit() - def update_task(self, task: DomainTask) -> Optional[DomainTask]: - orm = self._session.get(ORMTask, task.id) + async def update_task(self, task: DomainTask) -> Optional[DomainTask]: + 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) - self._session.commit() + await self._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() + async def find_tasks_by_training_id(self, training_id: UUID) -> Optional[List[DomainTask]]: + 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 - def add_validate(self, validate: DomainValidate) -> Optional[DomainValidate]: + 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) + await self._session.commit() + await self._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]]: + 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 - def delete_validate(self, id: UUID) -> None: - orm = self._session.get(ORMValidate, id) + async def delete_validate(self, id: UUID) -> None: + orm = await self._session.get(ORMValidate, id) if not orm: return - self._session.delete(orm) - self._session.commit() + await self._session.delete(orm) + await self._session.commit() - def find_validate_by_id(self, id: UUID) -> Optional[DomainValidate]: - orm = self._session.get(ORMValidate, id) + async def find_validate_by_id(self, id: UUID) -> Optional[DomainValidate]: + orm = await self._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) - .join(ORMTask, ORMValidate.task_id == ORMTask.id) - .filter(ORMTask.training_id == training_id) - .all() + async def find_all_validates_by_training_id(self, training_id: UUID) -> list[DomainValidate]: + 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 63a3e31..be71baa 100644 --- a/src/container.py +++ b/src/container.py @@ -1,5 +1,6 @@ import os from uuid import uuid4, UUID +import asyncio from src.domain.lib.security import BcryptPasswordHasher from src.domain.services.profile import ProfileService @@ -15,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( @@ -27,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 @@ -48,46 +51,116 @@ def __init__(self, env: str | None = None): 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() diff --git a/src/domain/model/diet.py b/src/domain/model/diet.py index b612f14..bb0c968 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, 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 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..b235f96 100644 --- a/src/domain/ports/training_repository.py +++ b/src/domain/ports/training_repository.py @@ -5,66 +5,66 @@ 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..9f495eb 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,23 @@ 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 + + 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 cdbc19b..23f6f39 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,80 @@ 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: + # Vérifier que le groupe existe + 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") + + # Vérifier que l'utilisateur existe + from src.container import container + profile_service = container.get_profile_service() # ou injection via constructeur + try: + user = await profile_service.get_by_id(user_id) + except NotFoundError: + raise NotFoundError(f"User {user_id} not found") - self._repo.add_member(group_id, user_id) + # Ajouter le membre + 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_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: raise NotFoundError(f"No groups found for user {user_id}") @@ -97,7 +113,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.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 8be040c..afe4cc3 100644 --- a/src/domain/services/profile.py +++ b/src/domain/services/profile.py @@ -14,7 +14,7 @@ def __init__(self, repo: ProfileRepository, hasher: PasswordHasher): self._hasher = hasher - def create(self, + async def create(self, *, email: str, raw_password: str, @@ -33,7 +33,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,18 +56,18 @@ 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) + await self._repo.delete(profile_id) - def login(self, email: str, password: str) -> DomainProfile: - profile = self._repo.find_by_email(email) + 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): @@ -81,28 +81,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 +114,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 +125,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..23fb903 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,17 @@ 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 +106,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 +123,26 @@ 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,7 +150,7 @@ 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") @@ -166,21 +166,21 @@ def create_validate( succeeded_at=datetime.utcnow(), # This can be set later when validation is successful ) - 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..f66065b 100644 --- a/src/entrypoints/api/deps/auth.py +++ b/src/entrypoints/api/deps/auth.py @@ -40,18 +40,15 @@ 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) - 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" @@ -66,18 +63,15 @@ def require_group_owner_or_admin( return group -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) - 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" @@ -91,14 +85,14 @@ def require_exercice_owner_or_admin( return exercise -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) # ✅ Ajout d'await except NotFoundError: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -109,7 +103,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 +113,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)) # ✅ Ajout d'await 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) # ✅ Ajout d'await 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 +131,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 +142,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) # ✅ Ajout d'await 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)) # ✅ Ajout d'await + for grp in coach_groups: + try: + 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: + 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") @@ -186,14 +191,14 @@ def require_training_owner_or_admin( return training -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, @@ -209,6 +214,4 @@ def require_owner_coach_for_user_or_admin( if "admin" in roles: return target_profile - 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/routers/diet.py b/src/entrypoints/api/routers/diet.py index 99f4967..f4edd27 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"])) # ✅ Ajout d'await + 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) # ✅ Ajout d'await 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..efdf2e0 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,13 +88,13 @@ 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..7a7f003 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) # ✅ Ajout d'await 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) # ✅ Ajout d'await 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) # ✅ Ajout d'await 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..0ec8688 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,15 @@ 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) + validations = await service.get_validates_for_task(task_id) + return [ValidateRead.model_validate(v) for v in validations] except NotFoundError: return [] @@ -267,7 +268,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 +276,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 +286,13 @@ 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) + 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/training.py b/src/entrypoints/api/tests/training.py index f11e401..703f2c7 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']}"} ) 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" },