From 4ab9ecbeecfb56429fb1eec1dde246de28f1d28f Mon Sep 17 00:00:00 2001 From: Baptiste-Ferrand Date: Tue, 2 Sep 2025 18:08:43 +0200 Subject: [PATCH 1/7] add lib for env tasks --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4cf94fd..991080d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "tracknatrainapi" version = "0.8.0" 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", "asyncpg==0.30.0", "pytest-mock>=3.10,<4.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", "pytest-mock>=3.10,<4.0", "apscheduler==3.10.4",] [build-system] requires = [ "setuptools>=42", "wheel",] From 4962e0af7a137dbc77a43ecb86039c0674df7808 Mon Sep 17 00:00:00 2001 From: Baptiste-Ferrand Date: Tue, 2 Sep 2025 18:09:09 +0200 Subject: [PATCH 2/7] create event tasks who create notification every day --- src/adapters/scheduler/daily_notification.py | 28 ++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/adapters/scheduler/daily_notification.py diff --git a/src/adapters/scheduler/daily_notification.py b/src/adapters/scheduler/daily_notification.py new file mode 100644 index 0000000..3b3cd7c --- /dev/null +++ b/src/adapters/scheduler/daily_notification.py @@ -0,0 +1,28 @@ +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from apscheduler.triggers.cron import CronTrigger +import logging + +from src.container import container +from src.adapters.sqlalchemy.repositories.notification import SqlAlchemyNotificationRepository +from src.domain.services.notification import NotificationService + +logger = logging.getLogger("apscheduler") + +def start_daily_notification_scheduler(): + scheduler = AsyncIOScheduler() + trigger = CronTrigger(hour=17, minute=10) + print(f"Daily notification scheduler started 🚀 at {trigger}") + + async def job(): + print("Running daily notification job...") + try: + async with container.SessionFactory() as session: + repo = SqlAlchemyNotificationRepository(session) + notification_service = NotificationService(repo) + notifications = await notification_service.create_daily_notifications_for_all_users() + logger.info(f"[Scheduler] {len(notifications)} daily notifications created successfully") + except Exception as e: + logger.error(f"[Scheduler] Error creating daily notifications: {e}") + + scheduler.add_job(job, trigger) + scheduler.start() \ No newline at end of file From e0fd690084766607a822d603da37e34226a4ead7 Mon Sep 17 00:00:00 2001 From: Baptiste-Ferrand Date: Tue, 2 Sep 2025 18:09:24 +0200 Subject: [PATCH 3/7] create inmemeory and sql adapter for notification --- .../inmemory/repositories/notification.py | 41 +++++++++ .../sqlalchemy/repositories/notification.py | 89 +++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 src/adapters/inmemory/repositories/notification.py create mode 100644 src/adapters/sqlalchemy/repositories/notification.py diff --git a/src/adapters/inmemory/repositories/notification.py b/src/adapters/inmemory/repositories/notification.py new file mode 100644 index 0000000..7208df1 --- /dev/null +++ b/src/adapters/inmemory/repositories/notification.py @@ -0,0 +1,41 @@ +from typing import List, Optional +from uuid import UUID + +from src.domain.model.notification import Notification as DomainNotification +from src.domain.ports.notification_repository import NotificationRepository + +class InMemoryNotificationRepository(NotificationRepository): + def __init__(self, initial: List[DomainNotification] = None): + self.notifications = initial or [] + + async def add_notification(self, notification: DomainNotification) -> DomainNotification: + self.notifications.append(notification) + return notification + + async def find_by_id(self, id: UUID) -> Optional[DomainNotification]: + for notification in self.notifications: + if notification.id == id: + return notification + return None + + async def find_unread_by_profile_id(self, profile_id: UUID) -> List[DomainNotification]: + unread_notifications = [] + for notification in self.notifications: + if notification.profile_id == profile_id and not notification.is_read(): + unread_notifications.append(notification) + + unread_notifications.sort(key=lambda x: x.created_at, reverse=True) + return unread_notifications + + async def update_notification(self, notification: DomainNotification) -> DomainNotification: + for i, existing_notification in enumerate(self.notifications): + if existing_notification.id == notification.id: + self.notifications[i] = notification + return notification + return notification + + async def find_all_profile_ids(self) -> List[UUID]: + profile_ids = set() + for notification in self.notifications: + profile_ids.add(notification.profile_id) + return list(profile_ids) \ No newline at end of file diff --git a/src/adapters/sqlalchemy/repositories/notification.py b/src/adapters/sqlalchemy/repositories/notification.py new file mode 100644 index 0000000..a1c667b --- /dev/null +++ b/src/adapters/sqlalchemy/repositories/notification.py @@ -0,0 +1,89 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +from typing import List, Optional +from uuid import UUID + +from src.domain.model.notification import Notification as DomainNotification +from src.domain.ports.notification_repository import NotificationRepository +from src.adapters.sqlalchemy.models import Notification as SqlAlchemyNotification, Profile as SqlAlchemyProfile + +class SqlAlchemyNotificationRepository(NotificationRepository): + def __init__(self, session: AsyncSession): + self.session = session + + async def add_notification(self, notification: DomainNotification) -> DomainNotification: + orm_notification = SqlAlchemyNotification(**notification.to_orm_dict()) + self.session.add(orm_notification) + await self.session.commit() + await self.session.refresh(orm_notification) + + return DomainNotification( + id=orm_notification.id, + profile_id=orm_notification.profile_id, + title=orm_notification.title, + description=orm_notification.description, + created_at=orm_notification.created_at, + read_at=orm_notification.read_at + ) + + async def find_by_id(self, id: UUID) -> Optional[DomainNotification]: + result = await self.session.execute( + select(SqlAlchemyNotification).where(SqlAlchemyNotification.id == id) + ) + orm_notification = result.scalar_one_or_none() + + if not orm_notification: + return None + + return DomainNotification( + id=orm_notification.id, + profile_id=orm_notification.profile_id, + title=orm_notification.title, + description=orm_notification.description, + created_at=orm_notification.created_at, + read_at=orm_notification.read_at + ) + + async def find_unread_by_profile_id(self, profile_id: UUID) -> List[DomainNotification]: + result = await self.session.execute( + select(SqlAlchemyNotification) + .where(SqlAlchemyNotification.profile_id == profile_id) + .where(SqlAlchemyNotification.read_at.is_(None)) + .order_by(SqlAlchemyNotification.created_at.desc()) + ) + orm_notifications = result.scalars().all() + + return [ + DomainNotification( + id=orm_notification.id, + profile_id=orm_notification.profile_id, + title=orm_notification.title, + description=orm_notification.description, + created_at=orm_notification.created_at, + read_at=orm_notification.read_at + ) + for orm_notification in orm_notifications + ] + + async def update_notification(self, notification: DomainNotification) -> DomainNotification: + result = await self.session.execute( + select(SqlAlchemyNotification).where(SqlAlchemyNotification.id == notification.id) + ) + orm_notification = result.scalar_one_or_none() + + if orm_notification: + orm_notification.title = notification.title + orm_notification.description = notification.description + orm_notification.read_at = notification.read_at + + await self.session.commit() + await self.session.refresh(orm_notification) + + return notification + + async def find_all_profile_ids(self) -> List[UUID]: + result = await self.session.execute( + select(SqlAlchemyProfile.id) + ) + profile_ids = result.scalars().all() + return list(profile_ids) \ No newline at end of file From c7a210682cb6c455be07974461e33922f392f7f0 Mon Sep 17 00:00:00 2001 From: Baptiste-Ferrand Date: Tue, 2 Sep 2025 18:09:46 +0200 Subject: [PATCH 4/7] create port model and service for notification --- src/domain/model/notification.py | 30 ++++++++++++++ src/domain/ports/notification_repository.py | 25 +++++++++++ src/domain/services/notification.py | 46 +++++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 src/domain/model/notification.py create mode 100644 src/domain/ports/notification_repository.py create mode 100644 src/domain/services/notification.py diff --git a/src/domain/model/notification.py b/src/domain/model/notification.py new file mode 100644 index 0000000..fe9284b --- /dev/null +++ b/src/domain/model/notification.py @@ -0,0 +1,30 @@ +from dataclasses import dataclass, field +from typing import Optional +from uuid import UUID, uuid4 +from datetime import datetime + +@dataclass +class Notification: + id: UUID = field(default_factory=uuid4) + profile_id: UUID = field(default_factory=uuid4) + title: str = "" + description: Optional[str] = None + created_at: datetime = field(default_factory=datetime.utcnow) + read_at: Optional[datetime] = None + + def to_orm_dict(self) -> dict: + return { + "id": str(self.id), + "profile_id": str(self.profile_id), + "title": self.title, + "description": self.description, + "created_at": self.created_at, + "read_at": self.read_at, + } + + def is_read(self) -> bool: + return self.read_at is not None + + def mark_as_read(self) -> None: + if self.read_at is None: + self.read_at = datetime.utcnow() \ No newline at end of file diff --git a/src/domain/ports/notification_repository.py b/src/domain/ports/notification_repository.py new file mode 100644 index 0000000..25f4ae5 --- /dev/null +++ b/src/domain/ports/notification_repository.py @@ -0,0 +1,25 @@ +from abc import ABC, abstractmethod +from uuid import UUID +from typing import List, Optional +from src.domain.model.notification import Notification as DomainNotification + +class NotificationRepository(ABC): + @abstractmethod + async def add_notification(self, notification: DomainNotification) -> DomainNotification: + pass + + @abstractmethod + async def find_by_id(self, id: UUID) -> Optional[DomainNotification]: + pass + + @abstractmethod + async def find_unread_by_profile_id(self, profile_id: UUID) -> List[DomainNotification]: + pass + + @abstractmethod + async def update_notification(self, notification: DomainNotification) -> DomainNotification: + pass + + @abstractmethod + async def find_all_profile_ids(self) -> List[UUID]: + pass \ No newline at end of file diff --git a/src/domain/services/notification.py b/src/domain/services/notification.py new file mode 100644 index 0000000..260ad0f --- /dev/null +++ b/src/domain/services/notification.py @@ -0,0 +1,46 @@ +from uuid import UUID, uuid4 +from datetime import datetime +from typing import List + +from src.domain.model.notification import Notification as DomainNotification +from src.domain.ports.notification_repository import NotificationRepository +from src.domain.exceptions import NotFoundError + +class NotificationService: + def __init__(self, repo: NotificationRepository): + self._repo = repo + + async def get_unread_notifications(self, profile_id: UUID) -> List[DomainNotification]: + return await self._repo.find_unread_by_profile_id(profile_id) + + async def mark_notification_as_read(self, notification_id: UUID, profile_id: UUID) -> DomainNotification: + notification = await self._repo.find_by_id(notification_id) + if not notification: + raise NotFoundError(f"Notification with id {notification_id} not found") + + if notification.profile_id != profile_id: + raise NotFoundError(f"Notification with id {notification_id} not found") + + if not notification.is_read(): + notification.mark_as_read() + notification = await self._repo.update_notification(notification) + + return notification + + async def create_daily_notifications_for_all_users(self) -> List[DomainNotification]: + profile_ids = await self._repo.find_all_profile_ids() + + notifications = [] + for profile_id in profile_ids: + notification = DomainNotification( + id=uuid4(), + profile_id=profile_id, + title="Daily Checkup", + description="votre checkup journalié a remplir vous attends, ne faite pas attendre votre coach donner lui votre ressentie quautidien ;)", + created_at=datetime.utcnow(), + read_at=None + ) + created_notification = await self._repo.add_notification(notification) + notifications.append(created_notification) + + return notifications \ No newline at end of file From 975f05059e8cd973979dd87aae20011141821e84 Mon Sep 17 00:00:00 2001 From: Baptiste-Ferrand Date: Tue, 2 Sep 2025 18:09:57 +0200 Subject: [PATCH 5/7] create router and schema for notification --- src/entrypoints/api/routers/notification.py | 71 +++++++++++++++++++++ src/entrypoints/api/schemas/notification.py | 23 +++++++ 2 files changed, 94 insertions(+) create mode 100644 src/entrypoints/api/routers/notification.py create mode 100644 src/entrypoints/api/schemas/notification.py diff --git a/src/entrypoints/api/routers/notification.py b/src/entrypoints/api/routers/notification.py new file mode 100644 index 0000000..e1c40dd --- /dev/null +++ b/src/entrypoints/api/routers/notification.py @@ -0,0 +1,71 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from typing import List +from uuid import UUID + +from src.entrypoints.api.schemas.notification import NotificationRead, DailyNotificationResponse +from src.entrypoints.api.deps.roles import get_current_user, require_roles +from src.domain.exceptions import NotFoundError +from src.container import container + +router = APIRouter(prefix="/notifications", tags=["notifications"]) + +@router.get("/mine", response_model=List[NotificationRead]) +async def get_my_unread_notifications( + user: dict = Depends(get_current_user) +): + try: + current_user_id = UUID(user["sub"]) + notification_service = container.get_notification_service() + notifications = await notification_service.get_unread_notifications(current_user_id) + if not notifications: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="No unread notifications found" + ) + return notifications + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error when fetching notifications: {str(e)}" + ) + +@router.patch("/{notification_id}/read", response_model=NotificationRead) +async def mark_notification_as_read( + notification_id: UUID, + user: dict = Depends(get_current_user) +): + try: + current_user_id = UUID(user["sub"]) + notification_service = container.get_notification_service() + notification = await notification_service.mark_notification_as_read( + notification_id, current_user_id + ) + return notification + except NotFoundError: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Notification not found" + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error when updating notification: {str(e)}" + ) + +@router.post("/create-daily", response_model=DailyNotificationResponse, dependencies=[Depends(require_roles("admin"))]) +async def create_daily_notifications(): + try: + notification_service = container.get_notification_service() + notifications = await notification_service.create_daily_notifications_for_all_users() + + return DailyNotificationResponse( + message="Daily notifications created successfully", + notifications_created=len(notifications) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error when creating daily notifications: {str(e)}" + ) \ No newline at end of file diff --git a/src/entrypoints/api/schemas/notification.py b/src/entrypoints/api/schemas/notification.py new file mode 100644 index 0000000..0aa671d --- /dev/null +++ b/src/entrypoints/api/schemas/notification.py @@ -0,0 +1,23 @@ +from pydantic import BaseModel +from typing import Optional, List +from uuid import UUID +from datetime import datetime + +class NotificationRead(BaseModel): + id: UUID + profile_id: UUID + title: str + description: Optional[str] = None + created_at: datetime + read_at: Optional[datetime] = None + + model_config = { + "from_attributes": True + } + +class NotificationUpdate(BaseModel): + read_at: datetime + +class DailyNotificationResponse(BaseModel): + message: str + notifications_created: int \ No newline at end of file From 4f1514adf9178e71b34d0a5a6db0a5fba32d6693 Mon Sep 17 00:00:00 2001 From: Baptiste-Ferrand Date: Tue, 2 Sep 2025 18:10:22 +0200 Subject: [PATCH 6/7] add router in main and add service gestion on container --- src/container.py | 25 +++++++++++++++++++++++++ src/main.py | 10 +++++++++- uv.lock | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/container.py b/src/container.py index 688372d..1ce7eee 100644 --- a/src/container.py +++ b/src/container.py @@ -9,6 +9,7 @@ from src.domain.services.exercise import ExerciseService from src.domain.services.diet import DietService from src.domain.services.daily_checkup import DailyCheckupService +from src.domain.services.notification import NotificationService class Container: def __init__(self, env: str | None = None): self.env = env if env is not None else os.getenv("ENV", "dev") @@ -41,6 +42,7 @@ def __init__(self, env: str | None = None): from src.adapters.inmemory.repositories.diet import InMemoryDietRepository from src.adapters.inmemory.repositories.image_storage import InMemoryImageStorage from src.adapters.inmemory.repositories.daily_checkup import InMemoryDailyCheckupRepository + from src.adapters.inmemory.repositories.notification import InMemoryNotificationRepository self.profile_repo = InMemoryProfileRepository(initial=[admin]) self.group_repo = InMemoryGroupRepository(self.profile_repo) self.training_repo = InMemoryTrainingRepository() @@ -48,6 +50,7 @@ def __init__(self, env: str | None = None): self.diet_repo = InMemoryDietRepository() self.image_repo = InMemoryImageStorage() self.daily_checkup_repo = InMemoryDailyCheckupRepository() + self.notification_repo = InMemoryNotificationRepository() else: from src.adapters.sqlalchemy.db import SessionLocal from src.adapters.minio.image_storage import MinioImageStorage @@ -191,5 +194,27 @@ async def method(*args, **kwargs): repo = SessionManagedRepository(SqlAlchemyDailyCheckupRepository, self.SessionFactory) return DailyCheckupService(repo, self.daily_checkup_image_storage) + def get_notification_service(self): + if self.env in ("dev", "test"): + repo = self.notification_repo + return NotificationService(repo) + else: + from src.adapters.sqlalchemy.repositories.notification import SqlAlchemyNotificationRepository + + 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(SqlAlchemyNotificationRepository, self.SessionFactory) + return NotificationService(repo) container = Container() \ No newline at end of file diff --git a/src/main.py b/src/main.py index 34b1501..7f2599b 100644 --- a/src/main.py +++ b/src/main.py @@ -5,11 +5,13 @@ import os from dotenv import load_dotenv +from src.adapters.sqlalchemy.db import create_tables load_dotenv() print("DEBUG ENV =", os.getenv("ENV")) print("DEBUG DATABASE_URL =", os.getenv("DATABASE_URL")) app = FastAPI() +from src.adapters.scheduler.daily_notification import start_daily_notification_scheduler from src.entrypoints.api.routers.profile import router as profile_router from src.entrypoints.api.routers.group import router as group_router @@ -17,6 +19,7 @@ from src.entrypoints.api.routers.training import router as training_router from src.entrypoints.api.routers.diet import router as diet_router from src.entrypoints.api.routers.daily_checkup import router as daily_checkup_router +from src.entrypoints.api.routers.notification import router as notification_router bearer_scheme = HTTPBearer() @@ -74,5 +77,10 @@ def custom_openapi(): app.include_router(training_router) app.include_router(diet_router) app.include_router(daily_checkup_router) +app.include_router(notification_router) - +@app.on_event("startup") +async def startup_event(): + await create_tables() + print("[APP] FastAPI startup, launching scheduler...") + start_daily_notification_scheduler() diff --git a/uv.lock b/uv.lock index 9aa585b..960a02d 100644 --- a/uv.lock +++ b/uv.lock @@ -25,6 +25,20 @@ 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 = "apscheduler" +version = "3.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytz" }, + { name = "six" }, + { name = "tzlocal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/34/5dcb368cf89f93132d9a31bd3747962a9dc874480e54333b0c09fa7d56ac/APScheduler-3.10.4.tar.gz", hash = "sha256:e6df071b27d9be898e486bc7940a7be50b4af2e9da7c08f0744a96d4bd4cef4a", size = 100832, upload-time = "2023-08-19T16:44:58.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/b5/7af0cb920a476dccd612fbc9a21a3745fb29b1fcd74636078db8f7ba294c/APScheduler-3.10.4-py3-none-any.whl", hash = "sha256:fb91e8a768632a4756a585f79ec834e0e27aad5860bac7eaa523d9ccefd87661", size = 59303, upload-time = "2023-08-19T16:44:56.814Z" }, +] + [[package]] name = "asyncpg" version = "0.30.0" @@ -666,6 +680,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, ] +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + [[package]] name = "rsa" version = "4.9" @@ -751,11 +774,12 @@ wheels = [ [[package]] name = "tracknatrainapi" -version = "0.7.0" +version = "0.8.0" source = { editable = "." } dependencies = [ { name = "annotated-types" }, { name = "anyio" }, + { name = "apscheduler" }, { name = "asyncpg" }, { name = "bcrypt" }, { name = "boto3" }, @@ -813,6 +837,7 @@ testing = [ requires-dist = [ { name = "annotated-types", specifier = "==0.7.0" }, { name = "anyio", specifier = "==4.9.0" }, + { name = "apscheduler", specifier = "==3.10.4" }, { name = "asyncpg", specifier = "==0.30.0" }, { name = "bcrypt", specifier = "==4.3.0" }, { name = "boto3", specifier = "==1.37.37" }, @@ -884,6 +909,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload-time = "2025-02-25T17:27:57.754Z" }, ] +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + +[[package]] +name = "tzlocal" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, +] + [[package]] name = "urllib3" version = "2.4.0" From 3cf5e7e0f034ca2f9318a83cf7ac69279ae88b31 Mon Sep 17 00:00:00 2001 From: Baptiste-Ferrand Date: Tue, 2 Sep 2025 18:14:28 +0200 Subject: [PATCH 7/7] update time for event to 00h00 --- src/adapters/scheduler/daily_notification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/adapters/scheduler/daily_notification.py b/src/adapters/scheduler/daily_notification.py index 3b3cd7c..a711f66 100644 --- a/src/adapters/scheduler/daily_notification.py +++ b/src/adapters/scheduler/daily_notification.py @@ -10,7 +10,7 @@ def start_daily_notification_scheduler(): scheduler = AsyncIOScheduler() - trigger = CronTrigger(hour=17, minute=10) + trigger = CronTrigger(hour=0, minute=0) print(f"Daily notification scheduler started 🚀 at {trigger}") async def job():