From a4ed45117d31d48a42b18306fa0a010cfa3f03f8 Mon Sep 17 00:00:00 2001 From: Maxson Almeida Date: Thu, 17 Oct 2024 22:02:03 -0300 Subject: [PATCH 01/11] Update requirements-dev.txt, requirements-release.txt, and requirements-tests.txt Add APScheduler, pytz, six, and tzlocal dependencies to the requirements files. --- requirements-dev.txt | 6 +++++- requirements-release.txt | 4 ++++ requirements-tests.txt | 4 ++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index aa977bb..7e2b788 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,4 +7,8 @@ click==8.1.6 PyYAML==6.0.1 injector==0.21.0 pydantic<2.0.0 -astor>=0.8.1 \ No newline at end of file +astor>=0.8.1 +APScheduler==3.10.4 +pytz==2024.2 +six==1.16.0 +tzlocal==5.2 \ No newline at end of file diff --git a/requirements-release.txt b/requirements-release.txt index 0d30d21..38111fe 100644 --- a/requirements-release.txt +++ b/requirements-release.txt @@ -6,6 +6,10 @@ beanie==1.20.0 PyYAML==6.0.1 injector==0.21.0 astor==0.8.1 +APScheduler==3.10.4 +pytz==2024.2 +six==1.16.0 +tzlocal==5.2 # package release setuptools diff --git a/requirements-tests.txt b/requirements-tests.txt index a17e03c..f68f948 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -10,3 +10,7 @@ pydantic<2.0.0 python-dotenv>=1.0.0 uvicorn>=0.23.1 astor>=0.8.1 +APScheduler==3.10.4 +pytz==2024.2 +six==1.16.0 +tzlocal==5.2 \ No newline at end of file From 2c63075b606e43a7aebcc576bd96c795fbbbf9f6 Mon Sep 17 00:00:00 2001 From: Maxson Almeida Date: Thu, 17 Oct 2024 22:02:40 -0300 Subject: [PATCH 02/11] Add BackgroundScheduler instance and decorators for scheduling tasks This commit adds a new file `apscheduler.py` to the `nest/core/apscheduler` directory. The file contains the implementation of an instance of the `BackgroundScheduler` class from the APScheduler library. Additionally, a new file `scheduler.py` is added to the `nest/core/decorators` directory. This file contains decorators `Cron` and `Interval` for scheduling functions to run at specific times or intervals. --- nest/core/apscheduler/__init__.py | 0 nest/core/apscheduler/apscheduler.py | 9 ++++ nest/core/decorators/scheduler.py | 73 ++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 nest/core/apscheduler/__init__.py create mode 100644 nest/core/apscheduler/apscheduler.py create mode 100644 nest/core/decorators/scheduler.py diff --git a/nest/core/apscheduler/__init__.py b/nest/core/apscheduler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nest/core/apscheduler/apscheduler.py b/nest/core/apscheduler/apscheduler.py new file mode 100644 index 0000000..a4e87c8 --- /dev/null +++ b/nest/core/apscheduler/apscheduler.py @@ -0,0 +1,9 @@ +from pytz import utc +from apscheduler.schedulers.background import BackgroundScheduler + +""" +An instance of the BackgroundScheduler class from the APScheduler library. +""" + +scheduler = BackgroundScheduler() +scheduler.configure(timezone=utc) diff --git a/nest/core/decorators/scheduler.py b/nest/core/decorators/scheduler.py new file mode 100644 index 0000000..531e7b9 --- /dev/null +++ b/nest/core/decorators/scheduler.py @@ -0,0 +1,73 @@ +from typing import Optional, Callable, Type + +from nest.core.apscheduler import scheduler + + +from nest.core.apscheduler.enums.cron_expression import CronExpression +from nest.core.apscheduler.enums.scheduler_type import SchedulerType + +def Cron(expression: CronExpression = CronExpression.EVERY_MINUTE) -> Callable: + """ + Decorator that schedules a function to run at a specific time. + + Args: + expression (CronExpression): A cron expression. + + Returns: + function: The decorated function. + """ + def decorated(func: Callable) -> Callable: + def wrapper(*args, **kwargs): + """ + Wrapper function that schedules the function to run at a specific time. + """ + try: + if not isinstance(expression, CronExpression): + raise ValueError("Invalid cron expression.") + scheduler.add_job( + func, + trigger = expression.value, + id = func.__name__, + ) + except Exception as e: + raise ValueError(f"Invalid cron expression: {e}") + + + return wrapper + + return decorated + +def Interval(seconds: Optional[int] = 10, minutes: Optional[int] = None, hours: Optional[int] = None, days: Optional[int] = None) -> Callable: + """ + Decorator that schedules a function to run at a specific interval. + + Args: + seconds (int): The number of seconds between each run. + minutes (int): The number of minutes between each run. + hours (int): The number of hours between each run. + days (int): The number of days between each run. + + Returns: + function: The decorated function. + """ + def decorated(func: Callable) -> Callable: + def wrapper(*args, **kwargs): + """ + Wrapper function that schedules the function to run at a specific interval. + """ + try: + scheduler.add_job( + func, + trigger = SchedulerType.INTERVAL.value, + seconds = seconds, + minutes = minutes, + hours = hours, + days = days, + id = func.__name__ + ) + except Exception as e: + raise ValueError(f"Invalid interval: {e}") + + return wrapper + + return decorated \ No newline at end of file From c7cb539ea02cd40451dadf9baed2e4fec0730d60 Mon Sep 17 00:00:00 2001 From: Maxson Almeida Date: Thu, 17 Oct 2024 22:02:51 -0300 Subject: [PATCH 03/11] Add enums for cron expressions and scheduler types --- .../core/apscheduler/enums/cron_expression.py | 100 ++++++++++++++++++ nest/core/apscheduler/enums/scheduler_type.py | 10 ++ 2 files changed, 110 insertions(+) create mode 100644 nest/core/apscheduler/enums/cron_expression.py create mode 100644 nest/core/apscheduler/enums/scheduler_type.py diff --git a/nest/core/apscheduler/enums/cron_expression.py b/nest/core/apscheduler/enums/cron_expression.py new file mode 100644 index 0000000..a2ece54 --- /dev/null +++ b/nest/core/apscheduler/enums/cron_expression.py @@ -0,0 +1,100 @@ +from enum import Enum +from apscheduler.triggers.cron import CronTrigger + +class CronExpression(Enum): + """ + Enum that contains cron expressions. + A cron expression is a string representing a set of times, using 6 space-separated fields. + Fields: + - second (0-59) + - minute (0-59) + - hour (0-23) + - day of month (1-31) + - month (1-12) + - day of week (0-6) (Sunday to Saturday) + - year (optional) + - timezone (optional) + """ + EVERY_SECOND = CronTrigger(second="*") + EVERY_5_SECONDS = CronTrigger(second="*/5") + EVERY_10_SECONDS = CronTrigger(second="*/10") + EVERY_30_SECONDS = CronTrigger(second="*/30") + EVERY_MINUTE = CronTrigger(minute="*/1") + EVERY_5_MINUTES = CronTrigger(minute="*/5") + EVERY_10_MINUTES = CronTrigger(minute="*/10") + EVERY_30_MINUTES = CronTrigger(minute="*/30") + EVERY_HOUR = CronTrigger(minute=0, hour="0-23/1") + EVERY_2_HOURS = CronTrigger(minute=0, hour="0-23/2") + EVERY_3_HOURS = CronTrigger(minute=0, hour="0-23/3") + EVERY_4_HOURS = CronTrigger(minute=0, hour="0-23/4") + EVERY_5_HOURS = CronTrigger(minute=0, hour="0-23/5") + EVERY_6_HOURS = CronTrigger(minute=0, hour="0-23/6") + EVERY_7_HOURS = CronTrigger(minute=0, hour="0-23/7") + EVERY_8_HOURS = CronTrigger(minute=0, hour="0-23/8") + EVERY_9_HOURS = CronTrigger(minute=0, hour="0-23/9") + EVERY_10_HOURS = CronTrigger(minute=0, hour="0-23/10") + EVERY_11_HOURS = CronTrigger(minute=0, hour="0-23/11") + EVERY_12_HOURS = CronTrigger(minute=0, hour="0-23/12") + EVERY_DAY_AT_1AM = CronTrigger(minute=0, hour=1) + EVERY_DAY_AT_2AM = CronTrigger(minute=0, hour=2) + EVERY_DAY_AT_3AM = CronTrigger(minute=0, hour=3) + EVERY_DAY_AT_4AM = CronTrigger(minute=0, hour=4) + EVERY_DAY_AT_5AM = CronTrigger(minute=0, hour=5) + EVERY_DAY_AT_6AM = CronTrigger(minute=0, hour=6) + EVERY_DAY_AT_7AM = CronTrigger(minute=0, hour=7) + EVERY_DAY_AT_8AM = CronTrigger(minute=0, hour=8) + EVERY_DAY_AT_9AM = CronTrigger(minute=0, hour=9) + EVERY_DAY_AT_10AM = CronTrigger(minute=0, hour=10) + EVERY_DAY_AT_11AM = CronTrigger(minute=0, hour=11) + EVERY_DAY_AT_NOON = CronTrigger(minute=0, hour=12) + EVERY_DAY_AT_1PM = CronTrigger(minute=0, hour=13) + EVERY_DAY_AT_2PM = CronTrigger(minute=0, hour=14) + EVERY_DAY_AT_3PM = CronTrigger(minute=0, hour=15) + EVERY_DAY_AT_4PM = CronTrigger(minute=0, hour=16) + EVERY_DAY_AT_5PM = CronTrigger(minute=0, hour=17) + EVERY_DAY_AT_6PM = CronTrigger(minute=0, hour=18) + EVERY_DAY_AT_7PM = CronTrigger(minute=0, hour=19) + EVERY_DAY_AT_8PM = CronTrigger(minute=0, hour=20) + EVERY_DAY_AT_9PM = CronTrigger(minute=0, hour=21) + EVERY_DAY_AT_10PM = CronTrigger(minute=0, hour=22) + EVERY_DAY_AT_11PM = CronTrigger(minute=0, hour=23) + EVERY_DAY_AT_MIDNIGHT = CronTrigger(minute=0, hour=0) + EVERY_WEEK = CronTrigger(minute=0, hour=0, day_of_week=0) + EVERY_WEEKDAY = CronTrigger(minute=0, hour=0, day_of_week="1-5") + EVERY_WEEKEND = CronTrigger(minute=0, hour=0, day_of_week="6,0") + EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT = CronTrigger(minute=0, hour=0, day=1) + EVERY_1ST_DAY_OF_MONTH_AT_NOON = CronTrigger(minute=0, hour=12, day=1) + EVERY_2ND_HOUR = CronTrigger(minute=0, hour="*/2") + EVERY_2ND_HOUR_FROM_1AM_THROUGH_11PM = CronTrigger(minute=0, hour="1-23/2") + EVERY_2ND_MONTH = CronTrigger(minute=0, hour=0, day=1, month="*/2") + EVERY_QUARTER = CronTrigger(minute=0, hour=0, day=1, month="*/3") + EVERY_6_MONTHS = CronTrigger(minute=0, hour=0, day=1, month="*/6") + EVERY_YEAR = CronTrigger(minute=0, hour=0, day=1, month=1) + EVERY_30_MINUTES_BETWEEN_9AM_AND_5PM = CronTrigger(minute="*/30", hour="9-17") + EVERY_30_MINUTES_BETWEEN_9AM_AND_6PM = CronTrigger(minute="*/30", hour="9-18") + EVERY_30_MINUTES_BETWEEN_10AM_AND_7PM = CronTrigger(minute="*/30", hour="10-19") + MONDAY_TO_FRIDAY_AT_1AM = CronTrigger(minute=0, hour=1, day_of_week="1-5") + MONDAY_TO_FRIDAY_AT_2AM = CronTrigger(minute=0, hour=2, day_of_week="1-5") + MONDAY_TO_FRIDAY_AT_3AM = CronTrigger(minute=0, hour=3, day_of_week="1-5") + MONDAY_TO_FRIDAY_AT_4AM = CronTrigger(minute=0, hour=4, day_of_week="1-5") + MONDAY_TO_FRIDAY_AT_5AM = CronTrigger(minute=0, hour=5, day_of_week="1-5") + MONDAY_TO_FRIDAY_AT_6AM = CronTrigger(minute=0, hour=6, day_of_week="1-5") + MONDAY_TO_FRIDAY_AT_7AM = CronTrigger(minute=0, hour=7, day_of_week="1-5") + MONDAY_TO_FRIDAY_AT_8AM = CronTrigger(minute=0, hour=8, day_of_week="1-5") + MONDAY_TO_FRIDAY_AT_9AM = CronTrigger(minute=0, hour=9, day_of_week="1-5") + MONDAY_TO_FRIDAY_AT_09_30AM = CronTrigger(minute=30, hour=9, day_of_week="1-5") + MONDAY_TO_FRIDAY_AT_10AM = CronTrigger(minute=0, hour=10, day_of_week="1-5") + MONDAY_TO_FRIDAY_AT_11AM = CronTrigger(minute=0, hour=11, day_of_week="1-5") + MONDAY_TO_FRIDAY_AT_11_30AM = CronTrigger(minute=30, hour=11, day_of_week="1-5") + MONDAY_TO_FRIDAY_AT_12PM = CronTrigger(minute=0, hour=12, day_of_week="1-5") + MONDAY_TO_FRIDAY_AT_1PM = CronTrigger(minute=0, hour=13, day_of_week="1-5") + MONDAY_TO_FRIDAY_AT_2PM = CronTrigger(minute=0, hour=14, day_of_week="1-5") + MONDAY_TO_FRIDAY_AT_3PM = CronTrigger(minute=0, hour=15, day_of_week="1-5") + MONDAY_TO_FRIDAY_AT_4PM = CronTrigger(minute=0, hour=16, day_of_week="1-5") + MONDAY_TO_FRIDAY_AT_5PM = CronTrigger(minute=0, hour=17, day_of_week="1-5") + MONDAY_TO_FRIDAY_AT_6PM = CronTrigger(minute=0, hour=18, day_of_week="1-5") + MONDAY_TO_FRIDAY_AT_7PM = CronTrigger(minute=0, hour=19, day_of_week="1-5") + MONDAY_TO_FRIDAY_AT_8PM = CronTrigger(minute=0, hour=20, day_of_week="1-5") + MONDAY_TO_FRIDAY_AT_9PM = CronTrigger(minute=0, hour=21, day_of_week="1-5") + MONDAY_TO_FRIDAY_AT_10PM = CronTrigger(minute=0, hour=22, day_of_week="1-5") + MONDAY_TO_FRIDAY_AT_11PM = CronTrigger(minute=0, hour=23, day_of_week="1-5") \ No newline at end of file diff --git a/nest/core/apscheduler/enums/scheduler_type.py b/nest/core/apscheduler/enums/scheduler_type.py new file mode 100644 index 0000000..ecd8bcd --- /dev/null +++ b/nest/core/apscheduler/enums/scheduler_type.py @@ -0,0 +1,10 @@ +from enum import Enum + +class SchedulerTypes(Enum): + """ + An enumeration of scheduler types. + """ + CRON = 'cron', + DATE = 'date' + INTERVAL = 'interval', + \ No newline at end of file From 49efe3e2c33b11c263e7985a620d61a4bda8792d Mon Sep 17 00:00:00 2001 From: Maxson Almeida Date: Thu, 17 Oct 2024 22:05:34 -0300 Subject: [PATCH 04/11] Update dependencies in pyproject.toml --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 9acd99e..8893ec3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,10 @@ dependencies = [ "pydantic<2.0.0", "sqlalchemy == 2.0.19", "alembic == 1.7.5", + "APScheduler >= "3.10.4", + "pytz >= "2024.2", + "six >= "1.16.0", + "tzlocal >= "5.2", ] [tool.setuptools.dynamic] From 93449e5f5ea4dfdcd5046299503f255f82f231fd Mon Sep 17 00:00:00 2001 From: Maxson Almeida Date: Wed, 17 Sep 2025 17:22:20 -0300 Subject: [PATCH 05/11] Add scheduler lifecycle management and activation of scheduled methods This commit introduces lifecycle management for the scheduler in the `PyNestApp` class, ensuring the scheduler starts and stops with the application. It also adds functionality to activate scheduled methods for instantiated services, enhancing the scheduling capabilities of the application. New methods `_setup_scheduler_lifecycle` and `_activate_scheduled_methods` are implemented to facilitate these features. Additionally, the `apscheduler` module is updated to include start and stop functions for the scheduler, and the decorators for scheduling tasks are enhanced to ensure proper initialization. --- nest/core/apscheduler/__init__.py | 7 + nest/core/apscheduler/apscheduler.py | 29 +- nest/core/apscheduler/enums/scheduler_type.py | 5 +- nest/core/decorators/scheduler.py | 302 +++++++++++++++--- nest/core/pynest_application.py | 50 ++- 5 files changed, 339 insertions(+), 54 deletions(-) diff --git a/nest/core/apscheduler/__init__.py b/nest/core/apscheduler/__init__.py index e69de29..cac494f 100644 --- a/nest/core/apscheduler/__init__.py +++ b/nest/core/apscheduler/__init__.py @@ -0,0 +1,7 @@ +from .apscheduler import scheduler, start_scheduler, stop_scheduler + +__all__ = ['scheduler', 'start_scheduler', 'stop_scheduler'] + + + + diff --git a/nest/core/apscheduler/apscheduler.py b/nest/core/apscheduler/apscheduler.py index a4e87c8..a18cfa9 100644 --- a/nest/core/apscheduler/apscheduler.py +++ b/nest/core/apscheduler/apscheduler.py @@ -1,9 +1,36 @@ from pytz import utc from apscheduler.schedulers.background import BackgroundScheduler +import atexit """ -An instance of the BackgroundScheduler class from the APScheduler library. +Instance of BackgroundScheduler from the APScheduler library. +Configured to initialize automatically and terminate properly. """ +# Create scheduler instance scheduler = BackgroundScheduler() scheduler.configure(timezone=utc) + +# Flag to control if scheduler has been started +_scheduler_started = False + +def start_scheduler(): + """ + Starts the scheduler if it hasn't been started yet. + """ + global _scheduler_started + if not _scheduler_started and not scheduler.running: + scheduler.start() + _scheduler_started = True + +def stop_scheduler(): + """ + Stops the scheduler safely. + """ + global _scheduler_started + if scheduler.running: + scheduler.shutdown(wait=True) + _scheduler_started = False + +# Register shutdown function to be called when program ends +atexit.register(stop_scheduler) diff --git a/nest/core/apscheduler/enums/scheduler_type.py b/nest/core/apscheduler/enums/scheduler_type.py index ecd8bcd..919f6d2 100644 --- a/nest/core/apscheduler/enums/scheduler_type.py +++ b/nest/core/apscheduler/enums/scheduler_type.py @@ -4,7 +4,6 @@ class SchedulerTypes(Enum): """ An enumeration of scheduler types. """ - CRON = 'cron', + CRON = 'cron' DATE = 'date' - INTERVAL = 'interval', - \ No newline at end of file + INTERVAL = 'interval' \ No newline at end of file diff --git a/nest/core/decorators/scheduler.py b/nest/core/decorators/scheduler.py index 531e7b9..ffa7b19 100644 --- a/nest/core/decorators/scheduler.py +++ b/nest/core/decorators/scheduler.py @@ -1,73 +1,277 @@ -from typing import Optional, Callable, Type +from typing import Optional, Callable, Union +from functools import wraps +import inspect +import weakref -from nest.core.apscheduler import scheduler +from nest.core.apscheduler import scheduler, start_scheduler +from nest.core.apscheduler.enums.cron_expression import CronExpression +from nest.core.apscheduler.enums.scheduler_type import SchedulerTypes -from nest.core.apscheduler.enums.cron_expression import CronExpression -from nest.core.apscheduler.enums.scheduler_type import SchedulerType +# Global registry for scheduled methods and their instances +_scheduled_methods_registry = {} +_pending_scheduled_methods = [] -def Cron(expression: CronExpression = CronExpression.EVERY_MINUTE) -> Callable: +def _execute_scheduled_method(instance_ref, method_name, *args, **kwargs): """ - Decorator that schedules a function to run at a specific time. + Execute a scheduled method with the correct instance. + """ + instance = instance_ref() + if instance is None: + return + + try: + method = getattr(instance, method_name) + return method(*args, **kwargs) + except Exception as e: + raise - Args: - expression (CronExpression): A cron expression. +def _register_pending_method(cls, method_name, job_config): + """ + Register a method to be scheduled when an instance is created. + """ + if cls not in _pending_scheduled_methods: + _pending_scheduled_methods.append((cls, method_name, job_config)) + +def activate_scheduled_methods_for_instance(instance): + """ + Activate all scheduled methods for a given instance. + This is called automatically by PyNest when services are instantiated. + """ + cls = instance.__class__ + + # Check for scheduled methods in this class and its base classes + for method_name in dir(instance): + if method_name.startswith('_'): + continue + + attr = getattr(cls, method_name, None) + if isinstance(attr, ScheduledMethod): + # Trigger the descriptor to schedule the method + getattr(instance, method_name) + +class ScheduledMethod: + """ + Descriptor for scheduled methods that automatically schedules them when accessed. + """ + def __init__(self, func, job_config): + self.func = func + self.job_config = job_config + self.scheduled_instances = weakref.WeakSet() + wraps(func)(self) + + def __set_name__(self, owner, name): + self.name = name + self.owner = owner + + def __get__(self, instance, owner): + if instance is None: + return self + + # Check if this method has already been scheduled for this instance + if instance not in self.scheduled_instances: + self._schedule_for_instance(instance) + self.scheduled_instances.add(instance) + + return self.func.__get__(instance, owner) + + def _schedule_for_instance(self, instance): + """ + Schedule the method for a specific instance. + """ + job_id = f"{self.job_config['type']}_{instance.__class__.__module__}_{instance.__class__.__name__}_{self.func.__name__}_{id(instance)}" + + # Create weak reference to instance to avoid memory leaks + instance_ref = weakref.ref(instance) + + # Create wrapper function that calls the method with correct instance + def method_wrapper(): + return _execute_scheduled_method(instance_ref, self.func.__name__) + + method_wrapper.__name__ = f"{instance.__class__.__name__}.{self.func.__name__}" + + try: + if self.job_config['type'] == 'cron': + scheduler.add_job( + method_wrapper, + trigger=self.job_config['expression'].value, + id=job_id, + replace_existing=True, + name=f"Cron Task: {instance.__class__.__name__}.{self.func.__name__}" + ) + + elif self.job_config['type'] == 'interval': + scheduler.add_job( + method_wrapper, + trigger='interval', + id=job_id, + replace_existing=True, + name=f"Interval Task: {instance.__class__.__name__}.{self.func.__name__}", + **self.job_config['trigger_kwargs'] + ) + + # Create interval description for logging + interval_desc = [] + if self.job_config['trigger_kwargs'].get('days'): + interval_desc.append(f"{self.job_config['trigger_kwargs']['days']} days") + if self.job_config['trigger_kwargs'].get('hours'): + interval_desc.append(f"{self.job_config['trigger_kwargs']['hours']} hours") + if self.job_config['trigger_kwargs'].get('minutes'): + interval_desc.append(f"{self.job_config['trigger_kwargs']['minutes']} minutes") + if self.job_config['trigger_kwargs'].get('seconds'): + interval_desc.append(f"{self.job_config['trigger_kwargs']['seconds']} seconds") + + + # Start scheduler if not running + start_scheduler() + + except Exception as e: + raise ValueError(f"Error scheduling method: {e}") +def Cron(expression: CronExpression = CronExpression.EVERY_MINUTE) -> Callable: + """ + Decorator to schedule a function or method to execute at specific times using cron expressions. + + Args: + expression (CronExpression): A predefined cron expression. + Returns: - function: The decorated function. + Callable: The decorated function or method. + + Example: + @Cron(expression=CronExpression.EVERY_MINUTE) + def my_task(): + print("Running every minute") + + # Or in a class: + class MyService: + @Cron(expression=CronExpression.EVERY_MINUTE) + def my_method(self): + print("Method running every minute") """ - def decorated(func: Callable) -> Callable: - def wrapper(*args, **kwargs): - """ - Wrapper function that schedules the function to run at a specific time. - """ + def decorator(func: Callable) -> Callable: + # Validate if the expression is valid + if not isinstance(expression, CronExpression): + raise ValueError("Invalid cron expression.") + + # Check if it's a class method (has 'self' as first parameter) + sig = inspect.signature(func) + params = list(sig.parameters.keys()) + is_method = len(params) > 0 and params[0] == 'self' + + if is_method: + # For class methods, return a ScheduledMethod + return ScheduledMethod(func, { + 'type': 'cron', + 'expression': expression + }) + else: + # For standalone functions, schedule directly + job_id = f"cron_{func.__module__}_{func.__name__}_{id(func)}" + try: - if not isinstance(expression, CronExpression): - raise ValueError("Invalid cron expression.") scheduler.add_job( func, - trigger = expression.value, - id = func.__name__, + trigger=expression.value, + id=job_id, + replace_existing=True, + name=f"Cron Task: {func.__name__}" ) - except Exception as e: - raise ValueError(f"Invalid cron expression: {e}") - - - return wrapper + + # Start scheduler if not running + start_scheduler() + + except Exception as e: + raise ValueError(f"Error scheduling cron function: {e}") + + return func - return decorated + return decorator -def Interval(seconds: Optional[int] = 10, minutes: Optional[int] = None, hours: Optional[int] = None, days: Optional[int] = None) -> Callable: - """ - Decorator that schedules a function to run at a specific interval. +def Interval( + seconds: Optional[int] = None, + minutes: Optional[int] = None, + hours: Optional[int] = None, + days: Optional[int] = None +) -> Callable: + """ + Decorator to schedule a function or method to execute at regular intervals. + Args: - seconds (int): The number of seconds between each run. - minutes (int): The number of minutes between each run. - hours (int): The number of hours between each run. - days (int): The number of days between each run. - + seconds (int, optional): Number of seconds between executions. + minutes (int, optional): Number of minutes between executions. + hours (int, optional): Number of hours between executions. + days (int, optional): Number of days between executions. + Returns: - function: The decorated function. + Callable: The decorated function or method. + + Example: + @Interval(seconds=30) + def my_task(): + print("Running every 30 seconds") + + # Or in a class: + class MyService: + @Interval(minutes=5) + def my_method(self): + print("Method running every 5 minutes") """ - def decorated(func: Callable) -> Callable: - def wrapper(*args, **kwargs): - """ - Wrapper function that schedules the function to run at a specific interval. - """ + def decorator(func: Callable) -> Callable: + # Validate that at least one time parameter was provided + if not any([seconds, minutes, hours, days]): + raise ValueError("At least one time parameter must be provided.") + + # Prepare trigger parameters + trigger_kwargs = {} + if seconds is not None: + trigger_kwargs['seconds'] = seconds + if minutes is not None: + trigger_kwargs['minutes'] = minutes + if hours is not None: + trigger_kwargs['hours'] = hours + if days is not None: + trigger_kwargs['days'] = days + + # Check if it's a class method (has 'self' as first parameter) + sig = inspect.signature(func) + params = list(sig.parameters.keys()) + is_method = len(params) > 0 and params[0] == 'self' + + if is_method: + # For class methods, return a ScheduledMethod + return ScheduledMethod(func, { + 'type': 'interval', + 'trigger_kwargs': trigger_kwargs + }) + else: + # For standalone functions, schedule directly + job_id = f"interval_{func.__module__}_{func.__name__}_{id(func)}" + try: scheduler.add_job( func, - trigger = SchedulerType.INTERVAL.value, - seconds = seconds, - minutes = minutes, - hours = hours, - days = days, - id = func.__name__ + trigger='interval', + id=job_id, + replace_existing=True, + name=f"Interval Task: {func.__name__}", + **trigger_kwargs ) - except Exception as e: - raise ValueError(f"Invalid interval: {e}") - - return wrapper + + # Start scheduler if not running + start_scheduler() + + # Create interval description for logging + interval_desc = [] + if days: interval_desc.append(f"{days} days") + if hours: interval_desc.append(f"{hours} hours") + if minutes: interval_desc.append(f"{minutes} minutes") + if seconds: interval_desc.append(f"{seconds} seconds") + + except Exception as e: + raise ValueError(f"Error scheduling interval function: {e}") + + return func - return decorated \ No newline at end of file + return decorator \ No newline at end of file diff --git a/nest/core/pynest_application.py b/nest/core/pynest_application.py index 22e2669..6774d48 100644 --- a/nest/core/pynest_application.py +++ b/nest/core/pynest_application.py @@ -1,10 +1,13 @@ from typing import Any +from contextlib import asynccontextmanager from fastapi import FastAPI from nest.common.route_resolver import RoutesResolver from nest.core.pynest_app_context import PyNestApplicationContext from nest.core.pynest_container import PyNestContainer +from nest.core.apscheduler import start_scheduler, stop_scheduler +from nest.core.decorators.scheduler import activate_scheduled_methods_for_instance class PyNestApp(PyNestApplicationContext): @@ -33,6 +36,51 @@ def __init__(self, container: PyNestContainer, http_server: FastAPI): self.routes_resolver = RoutesResolver(self.container, self.http_server) self.select_context_module() self.register_routes() + + # Activate scheduled methods for all instantiated services + self._activate_scheduled_methods() + + # Setup lifecycle events for scheduler + self._setup_scheduler_lifecycle() + + def _setup_scheduler_lifecycle(self): + """ + Sets up lifecycle events to start and stop the scheduler. + """ + @asynccontextmanager + async def lifespan(app: FastAPI): + # Startup + start_scheduler() + yield + # Shutdown + stop_scheduler() + + self.http_server.router.lifespan_context = lifespan + + def _activate_scheduled_methods(self): + """ + Activate scheduled methods for all instantiated services in the container. + """ + try: + # Get all modules from the container + for module in self.container.modules.values(): + # Get all providers from each module + for provider_name, provider_class in module.providers.items(): + try: + # Get the instance from the container + instance = self.container.get_instance(provider_class) + if instance is not None: + activate_scheduled_methods_for_instance(instance) + except Exception as e: + # Log the error for this specific provider but continue + import logging + logger = logging.getLogger(__name__) + logger.debug(f"Could not activate scheduled methods for {provider_name}: {e}") + except Exception as e: + # Log the error but don't fail the application startup + import logging + logger = logging.getLogger(__name__) + logger.error(f"Error activating scheduled methods: {e}") def use(self, middleware: type, **options: Any) -> "PyNestApp": """ @@ -61,4 +109,4 @@ def register_routes(self): """ Register the routes using the RoutesResolver. """ - self.routes_resolver.register_routes() + self.routes_resolver.register_routes() \ No newline at end of file From 6e83de4dbb5950b9d21772805247556ba62529bc Mon Sep 17 00:00:00 2001 From: Maxson Almeida Date: Wed, 17 Sep 2025 17:23:10 -0300 Subject: [PATCH 06/11] Add unit tests for task scheduling decorators This commit introduces a new test file `test_scheduler.py` that contains unit tests for the `@Cron` and `@Interval` decorators. The tests cover various scenarios, including valid and invalid cron expressions, as well as different parameter configurations for the interval decorator. The tests ensure that the decorators correctly register jobs with the scheduler and handle errors appropriately. --- .../test_decorators/test_scheduler.py | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 tests/test_core/test_decorators/test_scheduler.py diff --git a/tests/test_core/test_decorators/test_scheduler.py b/tests/test_core/test_decorators/test_scheduler.py new file mode 100644 index 0000000..594e9c4 --- /dev/null +++ b/tests/test_core/test_decorators/test_scheduler.py @@ -0,0 +1,119 @@ +import pytest +import time +from unittest.mock import patch, MagicMock + +from nest.core.decorators.scheduler import Cron, Interval +from nest.core.apscheduler.enums.cron_expression import CronExpression +from nest.core.apscheduler import scheduler + + +class TestSchedulerDecorators: + """Tests for task scheduling decorators.""" + + def setup_method(self): + """Setup executed before each test.""" + # Clear all jobs from scheduler + scheduler.remove_all_jobs() + + def teardown_method(self): + """Cleanup executed after each test.""" + # Remove all jobs from scheduler + scheduler.remove_all_jobs() + + @patch('nest.core.decorators.scheduler.scheduler') + def test_cron_decorator_valid_expression(self, mock_scheduler): + """Tests the @Cron decorator with valid expression.""" + # Arrange + mock_scheduler.add_job = MagicMock() + + # Act + @Cron(expression=CronExpression.EVERY_MINUTE) + def test_function(): + return "test" + + result = test_function() + + # Assert + assert result == "test" + mock_scheduler.add_job.assert_called_once() + call_args = mock_scheduler.add_job.call_args + assert call_args[0][0] == test_function + assert call_args[1]['trigger'] == CronExpression.EVERY_MINUTE.value + assert 'id' in call_args[1] + + def test_cron_decorator_invalid_expression(self): + """Tests the @Cron decorator with invalid expression.""" + # Act & Assert + with pytest.raises(ValueError, match="Invalid cron expression"): + @Cron(expression="invalid") + def test_function(): + return "test" + + @patch('nest.core.decorators.scheduler.scheduler') + def test_interval_decorator_with_seconds(self, mock_scheduler): + """Tests the @Interval decorator with seconds.""" + # Arrange + mock_scheduler.add_job = MagicMock() + + # Act + @Interval(seconds=30) + def test_function(): + return "test" + + result = test_function() + + # Assert + assert result == "test" + mock_scheduler.add_job.assert_called_once() + call_args = mock_scheduler.add_job.call_args + assert call_args[0][0] == test_function + assert call_args[1]['trigger'] == 'interval' + assert call_args[1]['seconds'] == 30 + + @patch('nest.core.decorators.scheduler.scheduler') + def test_interval_decorator_with_minutes(self, mock_scheduler): + """Tests the @Interval decorator with minutes.""" + # Arrange + mock_scheduler.add_job = MagicMock() + + # Act + @Interval(minutes=5) + def test_function(): + return "test" + + result = test_function() + + # Assert + assert result == "test" + mock_scheduler.add_job.assert_called_once() + call_args = mock_scheduler.add_job.call_args + assert call_args[1]['minutes'] == 5 + + def test_interval_decorator_no_parameters(self): + """Tests the @Interval decorator without parameters.""" + # Act & Assert + with pytest.raises(ValueError, match="At least one time parameter must be provided"): + @Interval() + def test_function(): + return "test" + + @patch('nest.core.decorators.scheduler.scheduler') + def test_interval_decorator_multiple_parameters(self, mock_scheduler): + """Tests the @Interval decorator with multiple parameters.""" + # Arrange + mock_scheduler.add_job = MagicMock() + + # Act + @Interval(seconds=30, minutes=1, hours=2) + def test_function(): + return "test" + + result = test_function() + + # Assert + assert result == "test" + mock_scheduler.add_job.assert_called_once() + call_args = mock_scheduler.add_job.call_args + assert call_args[1]['seconds'] == 30 + assert call_args[1]['minutes'] == 1 + assert call_args[1]['hours'] == 2 \ No newline at end of file From 1dc048667a7074b46a661fc60f421b1ee4cca4dd Mon Sep 17 00:00:00 2001 From: Maxson Almeida Date: Wed, 17 Sep 2025 17:23:20 -0300 Subject: [PATCH 07/11] Add documentation for task scheduling in PyNest --- docs/task_scheduling.md | 235 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 docs/task_scheduling.md diff --git a/docs/task_scheduling.md b/docs/task_scheduling.md new file mode 100644 index 0000000..cbca67e --- /dev/null +++ b/docs/task_scheduling.md @@ -0,0 +1,235 @@ +# Task Scheduling in PyNest ⏰ + +## Introduction + +PyNest provides a robust task scheduling system using the APScheduler library. This feature enables you to execute functions at specific times or regular intervals, making it perfect for background tasks, periodic reports, data cleanup, and automated operations. The scheduling system integrates seamlessly with PyNest's dependency injection and module system. + +## Defining Scheduled Tasks + +PyNest offers two main decorators for task scheduling: `@Cron` for time-based scheduling and `@Interval` for interval-based execution. + +### @Cron Decorator + +The `@Cron` decorator allows you to schedule tasks using predefined cron expressions. It works with both standalone functions and class methods. + +```python +from nest.core.decorators.scheduler import Cron +from nest.core.apscheduler.enums.cron_expression import CronExpression + +@Cron(expression=CronExpression.EVERY_DAY_AT_MIDNIGHT) +def daily_cleanup(): + """Execute daily cleanup at midnight.""" + print("Running daily cleanup...") + +@Cron(expression=CronExpression.EVERY_WEEKDAY) +def weekday_report(): + """Generate reports on weekdays.""" + print("Generating weekday report...") +``` + +### @Interval Decorator + +The `@Interval` decorator schedules tasks to run at regular intervals. You can specify intervals using seconds, minutes, hours, or days. + +```python +from nest.core.decorators.scheduler import Interval + +@Interval(minutes=5) +def health_check(): + """Check system health every 5 minutes.""" + print("Performing health check...") + +@Interval(hours=2) +def cache_refresh(): + """Refresh cache every 2 hours.""" + print("Refreshing cache...") +``` + +## Predefined Cron Expressions + +PyNest provides a comprehensive set of predefined cron expressions for common scheduling needs: + +**Time Intervals:** +- `EVERY_SECOND`, `EVERY_5_SECONDS`, `EVERY_30_SECONDS` +- `EVERY_MINUTE`, `EVERY_5_MINUTES`, `EVERY_30_MINUTES` +- `EVERY_HOUR`, `EVERY_2_HOURS`, `EVERY_6_HOURS`, `EVERY_12_HOURS` + +**Daily Schedules:** +- `EVERY_DAY_AT_MIDNIGHT`, `EVERY_DAY_AT_NOON` +- `EVERY_DAY_AT_1AM`, `EVERY_DAY_AT_6PM` + +**Weekly Schedules:** +- `EVERY_WEEK`, `EVERY_WEEKDAY`, `EVERY_WEEKEND` +- `MONDAY_TO_FRIDAY_AT_9AM`, `MONDAY_TO_FRIDAY_AT_5PM` + +**Monthly and Yearly:** +- `EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT` +- `EVERY_QUARTER`, `EVERY_6_MONTHS`, `EVERY_YEAR` + +## Using Scheduled Tasks in Services + +The scheduling decorators integrate seamlessly with PyNest's dependency injection system. When you decorate methods in an `@Injectable` service, PyNest automatically detects and schedules them during application startup. + +### Basic Service with Scheduling + +```python +from nest.core import Injectable +from nest.core.decorators.scheduler import Cron, Interval +from nest.core.apscheduler.enums.cron_expression import CronExpression +import logging + +@Injectable +class TaskService: + def __init__(self): + self.execution_count = 0 + logging.info("TaskService initialized - scheduled methods activated automatically") + + @Cron(expression=CronExpression.EVERY_DAY_AT_MIDNIGHT) + def daily_cleanup(self): + """Execute daily cleanup at midnight.""" + self.execution_count += 1 + logging.info(f"Daily cleanup executed #{self.execution_count}") + + # Cleanup logic here + return f"Cleanup completed - execution #{self.execution_count}" + + @Interval(minutes=5) + def health_check(self): + """Perform health check every 5 minutes.""" + logging.info("Performing system health check") + + # Health check logic here + return "System healthy" +``` + +### Dependency Injection with Scheduled Tasks + +```python +from nest.core import Injectable +from nest.core.decorators.scheduler import Cron +from nest.core.apscheduler.enums.cron_expression import CronExpression + +@Injectable +class NotificationService: + def send_notification(self, message: str): + """Send notification.""" + print(f"Notification sent: {message}") + return True + +@Injectable +class ReportService: + def __init__(self, notification_service: NotificationService): + self.notification_service = notification_service + + @Cron(expression=CronExpression.EVERY_WEEKDAY) + def generate_daily_report(self): + """Generate and notify daily report.""" + # Generate report logic + report = "Daily report generated successfully" + + # Send notification using injected service + self.notification_service.send_notification(report) + return report +``` + +## Creating a Complete Application + +Here's how to create a complete PyNest application with scheduled tasks: + +### Module Setup + +```python +# task_module.py +from nest.core import Module +from .task_service import TaskService +from .notification_service import NotificationService + +@Module( + providers=[TaskService, NotificationService], + exports=[TaskService], +) +class TaskModule: + pass +``` + +### Application Module + +```python +# app_module.py +from nest.core import Module, PyNestFactory +from .task_module import TaskModule + +@Module( + imports=[TaskModule], +) +class AppModule: + pass + +app = PyNestFactory.create(AppModule, title="Scheduler App", version="1.0.0") +``` + +### Running the Application + +```python +# main.py +import uvicorn +from app_module import app + +if __name__ == "__main__": + uvicorn.run("app_module:app", host="0.0.0.0", port=8000, reload=True) +``` + +## Configuration + +Task scheduling is automatically configured when you use the decorators. The scheduler starts automatically with your PyNest application and terminates gracefully on shutdown. + +### Required Dependencies + +Ensure the following dependencies are installed: + +```bash +pip install APScheduler==3.10.4 pytz==2024.2 six==1.16.0 tzlocal==5.2 +``` + +## Best Practices + +1. **Use Predefined Expressions**: Leverage PyNest's predefined cron expressions for reliability. + +2. **Handle Exceptions**: Always implement proper error handling in scheduled methods. + +3. **Keep Tasks Lightweight**: Scheduled tasks should be quick to execute and non-blocking. + +4. **Use Logging**: Implement comprehensive logging for monitoring and debugging. + +5. **Test Thoroughly**: Test scheduled tasks in development before deploying to production. + +## Troubleshooting + +### Common Issues + +**Tasks Not Running:** +- Verify the PyNest application is running +- Check that services are properly registered in modules +- Ensure scheduled methods have the correct signature + +**Multiple Executions:** +- Avoid running multiple application instances +- Check for application restart loops +- Verify job ID generation is working correctly + +## Conclusion 🎉 + +PyNest's task scheduling system provides a powerful and intuitive way to automate tasks in your applications. By leveraging the `@Cron` and `@Interval` decorators along with PyNest's dependency injection system, you can create robust scheduled tasks that integrate seamlessly with your application architecture. + +The automatic detection and scheduling of decorated methods eliminates the need for manual configuration, making it easy to implement complex scheduling scenarios while maintaining clean, maintainable code. + +--- + + \ No newline at end of file From 0913692302385b7d00bece21313c577dc5126b18 Mon Sep 17 00:00:00 2001 From: Maxson Almeida Date: Wed, 17 Sep 2025 17:23:33 -0300 Subject: [PATCH 08/11] Add SchedulerApp module with core components including app controller, service, and scheduler service This commit introduces the `SchedulerApp` module, which includes the main application controller (`AppController`), application service (`AppService`), and scheduler service (`SchedulerService`). The app controller provides endpoints for application information, scheduler statistics, logs, and health checks. The application service offers detailed information about the scheduling system, while the scheduler service implements task scheduling using decorators. A new entry point is also created in `main.py` to run the application with Uvicorn. --- examples/SchedulerApp/main.py | 8 + examples/SchedulerApp/src/app_controller.py | 48 ++++++ examples/SchedulerApp/src/app_module.py | 22 +++ examples/SchedulerApp/src/app_service.py | 65 ++++++++ .../SchedulerApp/src/scheduler_service.py | 157 ++++++++++++++++++ 5 files changed, 300 insertions(+) create mode 100644 examples/SchedulerApp/main.py create mode 100644 examples/SchedulerApp/src/app_controller.py create mode 100644 examples/SchedulerApp/src/app_module.py create mode 100644 examples/SchedulerApp/src/app_service.py create mode 100644 examples/SchedulerApp/src/scheduler_service.py diff --git a/examples/SchedulerApp/main.py b/examples/SchedulerApp/main.py new file mode 100644 index 0000000..6782914 --- /dev/null +++ b/examples/SchedulerApp/main.py @@ -0,0 +1,8 @@ +if __name__ == "__main__": + import uvicorn + uvicorn.run("main:http_server", host="0.0.0.0", port=8000, reload=True) + +# Import at module level for uvicorn +from src.app_module import http_server + + diff --git a/examples/SchedulerApp/src/app_controller.py b/examples/SchedulerApp/src/app_controller.py new file mode 100644 index 0000000..24ae966 --- /dev/null +++ b/examples/SchedulerApp/src/app_controller.py @@ -0,0 +1,48 @@ +from nest.core import Controller, Get +from .app_service import AppService +from .scheduler_service import SchedulerService + +@Controller("/") +class AppController: + """ + Main application controller with endpoints to monitor scheduled tasks. + """ + + def __init__(self, app_service: AppService, scheduler_service: SchedulerService): + self.app_service = app_service + self.scheduler_service = scheduler_service + + @Get("/") + def get_app_info(self): + """ + Returns application information. + """ + return self.app_service.get_app_info() + + @Get("/scheduler/stats") + def get_scheduler_stats(self): + """ + Returns statistics of scheduled tasks. + """ + return self.scheduler_service.get_statistics() + + @Get("/scheduler/logs") + def get_scheduler_logs(self): + """ + Returns recent logs of task executions. + """ + return self.scheduler_service.get_recent_logs() + + @Get("/health") + def health_check(self): + """ + Health check endpoint for application and scheduler. + """ + from nest.core.apscheduler import scheduler + + return { + "status": "healthy", + "scheduler_running": scheduler.running, + "active_jobs": len(scheduler.get_jobs()), + "app_info": self.app_service.get_app_info() + } \ No newline at end of file diff --git a/examples/SchedulerApp/src/app_module.py b/examples/SchedulerApp/src/app_module.py new file mode 100644 index 0000000..0d4f582 --- /dev/null +++ b/examples/SchedulerApp/src/app_module.py @@ -0,0 +1,22 @@ +from .app_controller import AppController +from .app_service import AppService +from .scheduler_service import SchedulerService + +from nest.core import Module, PyNestFactory + +@Module( + controllers=[AppController], + providers=[AppService, SchedulerService], +) +class AppModule: + pass + +app = PyNestFactory.create( + AppModule, + description="PyNest Application with Task Scheduling", + title="Scheduler App", + version="1.0.0", + debug=True +) + +http_server = app.get_server() \ No newline at end of file diff --git a/examples/SchedulerApp/src/app_service.py b/examples/SchedulerApp/src/app_service.py new file mode 100644 index 0000000..9b8112b --- /dev/null +++ b/examples/SchedulerApp/src/app_service.py @@ -0,0 +1,65 @@ +from nest.core import Injectable +from datetime import datetime + +@Injectable +class AppService: + """ + Main application service that provides information about the scheduling system. + """ + + def __init__(self): + self.app_name = "SchedulerApp" + self.app_version = "1.0.0" + self.description = "PyNest Application with Task Scheduling System" + self.started_at = datetime.now() + + def get_app_info(self): + """ + Returns detailed application information. + """ + uptime = datetime.now() - self.started_at + + return { + "app_name": self.app_name, + "app_version": self.app_version, + "description": self.description, + "started_at": self.started_at.strftime("%Y-%m-%d %H:%M:%S"), + "uptime_seconds": int(uptime.total_seconds()), + "uptime_formatted": str(uptime).split('.')[0], # Remove microseconds + "features": [ + "🕒 Task scheduling with @Cron", + "⏰ Task scheduling with @Interval", + "📅 Predefined cron expressions", + "🔄 Automatic integration with application lifecycle", + "📊 Real-time task monitoring", + "📝 Detailed execution logging" + ], + "endpoints": [ + { + "path": "/", + "method": "GET", + "description": "Application information" + }, + { + "path": "/scheduler/stats", + "method": "GET", + "description": "Scheduled tasks statistics" + }, + { + "path": "/scheduler/logs", + "method": "GET", + "description": "Recent execution logs" + }, + { + "path": "/health", + "method": "GET", + "description": "Application health check" + } + ], + "scheduler_info": { + "library": "APScheduler", + "background_scheduler": True, + "timezone": "UTC", + "auto_start": True + } + } \ No newline at end of file diff --git a/examples/SchedulerApp/src/scheduler_service.py b/examples/SchedulerApp/src/scheduler_service.py new file mode 100644 index 0000000..5874335 --- /dev/null +++ b/examples/SchedulerApp/src/scheduler_service.py @@ -0,0 +1,157 @@ +from nest.core import Injectable +from nest.core.decorators.scheduler import Cron, Interval +from nest.core.apscheduler.enums.cron_expression import CronExpression +from nest.core.apscheduler import scheduler +import logging +from datetime import datetime + +logger = logging.getLogger(__name__) + +@Injectable +class SchedulerService: + """ + Service that demonstrates the use of task scheduling decorators. + + This service contains examples of scheduled tasks using: + - @Cron: for tasks based on cron expressions + - @Interval: for tasks that run at regular intervals + """ + + def __init__(self): + self.execution_count = 0 + self.task_logs = [] + logger.info("SchedulerService initialized - scheduled methods will be activated automatically by PyNest") + + @Interval(seconds=10) + def task_every_10_seconds(self): + """ + Task that runs every 10 seconds for quick demonstration. + """ + self.execution_count += 1 + timestamp = datetime.now().strftime("%H:%M:%S") + message = f"[{timestamp}] Task executed every 10 seconds - Execution #{self.execution_count}" + + self.task_logs.append({ + "task": "task_every_10_seconds", + "timestamp": timestamp, + "execution_number": self.execution_count + }) + + # Keep only the last 10 logs + if len(self.task_logs) > 10: + self.task_logs = self.task_logs[-10:] + + logger.info(message) + print(message) # For console visualization + return message + + @Cron(expression=CronExpression.EVERY_MINUTE) + def task_every_minute(self): + """ + Task that runs every minute using cron expression. + """ + timestamp = datetime.now().strftime("%H:%M:%S") + message = f"[{timestamp}] Cron task executed - EVERY_MINUTE" + + self.task_logs.append({ + "task": "task_every_minute", + "timestamp": timestamp, + "type": "cron" + }) + + logger.info(message) + print(message) + return message + + @Cron(expression=CronExpression.EVERY_30_SECONDS) + def task_every_30_seconds_cron(self): + """ + Task that runs every 30 seconds using cron expression. + """ + timestamp = datetime.now().strftime("%H:%M:%S") + message = f"[{timestamp}] Cron task executed - EVERY_30_SECONDS" + + self.task_logs.append({ + "task": "task_every_30_seconds_cron", + "timestamp": timestamp, + "type": "cron" + }) + + logger.info(message) + print(message) + return message + + @Interval(minutes=1) + def task_every_minute_interval(self): + """ + Task that runs every minute using interval. + """ + timestamp = datetime.now().strftime("%H:%M:%S") + message = f"[{timestamp}] Interval task executed - every 1 minute" + + self.task_logs.append({ + "task": "task_every_minute_interval", + "timestamp": timestamp, + "type": "interval" + }) + + logger.info(message) + print(message) + return message + + def get_statistics(self): + """ + Returns statistics of scheduled tasks. + """ + # Get information from active scheduler jobs + jobs_info = [] + for job in scheduler.get_jobs(): + jobs_info.append({ + "id": job.id, + "name": job.name, + "next_run_time": str(job.next_run_time) if job.next_run_time else None, + "trigger": str(job.trigger) + }) + + return { + "execution_count": self.execution_count, + "active_jobs": len(scheduler.get_jobs()), + "scheduler_running": scheduler.running, + "jobs_details": jobs_info, + "recent_logs": self.task_logs[-5:], # Last 5 logs + "scheduled_tasks": [ + { + "name": "task_every_10_seconds", + "type": "interval", + "description": "Runs every 10 seconds" + }, + { + "name": "task_every_minute", + "type": "cron", + "description": "Runs every minute (cron)" + }, + { + "name": "task_every_30_seconds_cron", + "type": "cron", + "description": "Runs every 30 seconds (cron)" + }, + { + "name": "task_every_minute_interval", + "type": "interval", + "description": "Runs every minute (interval)" + } + ] + } + + def get_recent_logs(self): + """ + Returns recent logs of task executions. + """ + return { + "total_executions": self.execution_count, + "recent_logs": self.task_logs, + "scheduler_status": { + "running": scheduler.running, + "jobs_count": len(scheduler.get_jobs()) + } + } \ No newline at end of file From ba36a395cede8e0592cd4347159432deb6cf9551 Mon Sep 17 00:00:00 2001 From: Maxson Almeida Date: Wed, 17 Sep 2025 17:26:17 -0300 Subject: [PATCH 09/11] Refactor pyproject.toml to use Poetry for dependency management This commit updates the `pyproject.toml` file to transition from setuptools to Poetry as the build system. It includes the addition of versioning, authorship, and enhanced dependency management, specifying core and optional dependencies. The project metadata has been updated to reflect the new structure, including homepage and documentation links. The previous optional dependencies have been restructured under Poetry's extras and groups. --- pyproject.toml | 140 ++++++++++++++++++++++++++++--------------------- 1 file changed, 79 insertions(+), 61 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8893ec3..3e1a24e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,52 +1,89 @@ [build-system] -requires = ["setuptools>=61.0", "wheel>=0.37.0"] -build-backend = "setuptools.build_meta" +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" -[project] +[tool.poetry] name = "pynest-api" +version = "0.4.0" description = "PyNest is a FastAPI Abstraction for building microservices, influenced by NestJS." +authors = ["itay.dar "] readme = "README.md" -requires-python = ">=3.8.1" -license = { file = "LICENSE" } -authors = [ - { name = "Itay Dar", email = "itay2803@gmail.com" }, +homepage = "https://github.com/PythonNest/PyNest" +documentation = "https://pythonnest.github.io/PyNest/" +packages = [ + { include = "nest" } ] -dynamic = ["version"] classifiers = [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: OS Independent", + "License :: OSI Approved :: MIT License", + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries :: Python Modules", ] -dependencies = [ - "click>=8.1.6", - "fastapi>=0.88.0,<1.0.0", - "python-dotenv>=1.0.0", - "uvicorn>=0.23.1", - "PyYAML>=6.0.1", - "astor>=0.8.1", - "black>=23.11.0", - "injector>=0.20.1", - "pydantic<2.0.0", - "sqlalchemy == 2.0.19", - "alembic == 1.7.5", - "APScheduler >= "3.10.4", - "pytz >= "2024.2", - "six >= "1.16.0", - "tzlocal >= "5.2", -] -[tool.setuptools.dynamic] -version = { attr = "nest.__init__.__version__" } -[tool.pip] -index-url = "https://pypi.org/simple" -trusted-host = ["pypi.org", "files.pythonhosted.org"] -[tools.black] +[tool.poetry.dependencies] +python = "^3.9" +# Core dependencies +click = "^8.1.7" +injector = "^0.22.0" +astor = "^0.8.1" +pyyaml = "^6.0.2" +fastapi = "^0.115.4" +pydantic = "^2.9.2" +uvicorn = "^0.32.0" +APScheduler= "^3.10.4" +pytz= "^2024.2" +six= "^1.16.0" +tzlocal= "^5.2" + + +# Optional dependencies +sqlalchemy = { version = "^2.0.36", optional = true } +asyncpg = { version = "^0.30.0", optional = true } +psycopg2 = { version = "^2.9.3", optional = true } +alembic = { version = "^1.13.3", optional = true } +beanie = { version = "^1.27.0", optional = true } +python-dotenv = { version = "^1.0.1", optional = true } +greenlet = { version = "^3.1.1", optional = true } +black = "^24.10.0" + + + +[tool.poetry.extras] +postgres = ["sqlalchemy", "asyncpg", "psycopg2", "alembic", "greenlet", "python-dotenv"] +mongo = ["beanie", "python-dotenv"] +test = ["pytest"] + +[tool.poetry.group.build.dependencies] +setuptools = "^75.3.0" +wheel = "^0.44.0" +build = "^1.2.2.post1" +twine = "^5.1.1" +git-changelog = "^2.5.2" + +[tool.poetry.group.test.dependencies] +pytest = "^7.0.1" +fastapi = "^0.115.4" +sqlalchemy = "^2.0.36" +motor = "^3.2.0" +beanie = "^1.27.0" +pydantic = "^2.9.2" +python-dotenv = "^1.0.1" +uvicorn = "^0.32.0" + +[tool.poetry.group.docs.dependencies] +mkdocs-material = "^9.5.43" +mkdocstrings-python = "^1.12.2" + + +[tool.black] force-exclude = ''' /( | /*venv* @@ -62,34 +99,15 @@ force-exclude = ''' )/ ''' -[project.optional-dependencies] -test = [ - "pytest == 6.2.5", -] - -orm = [ - "sqlalchemy == 2.0.19", - "alembic == 1.7.4", -] -mongo = [ - "pymongo == 3.12.0", - "motor == 3.2.0", - "beanie == 1.20.0", -] - -[project.scripts] -pynest = "nest.cli.cli:nest_cli" - -[tool.setuptools.packages.find] -include = ["nest*"] -namespaces = false - [tool.mypy] exclude = [ "/*venv*" ] ignore_missing_imports = true -[project.urls] -"Homepage" = "https://github.com/PythonNest/PyNest" -"Documentation" = "https://pythonnest.github.io/PyNest/" +[tool.poetry.urls] +Homepage = "https://github.com/PythonNest/PyNest" +Documentation = "https://pythonnest.github.io/PyNest/" + +[tool.poetry.scripts] +pynest = "nest.cli.cli:nest_cli" From 213986dbbd03bf13826d8da49a4474d9149ae856 Mon Sep 17 00:00:00 2001 From: Maxson Almeida Date: Wed, 17 Sep 2025 17:40:25 -0300 Subject: [PATCH 10/11] Update formatting of dependencies in pyproject.toml for consistency --- pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3e1a24e..a20f11c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,10 +38,10 @@ pyyaml = "^6.0.2" fastapi = "^0.115.4" pydantic = "^2.9.2" uvicorn = "^0.32.0" -APScheduler= "^3.10.4" -pytz= "^2024.2" -six= "^1.16.0" -tzlocal= "^5.2" +APScheduler = "^3.10.4" +pytz = "^2024.2" +six = "^1.16.0" +tzlocal = "^5.2" # Optional dependencies From b9f7f5a7c14dfbd787d73833e14c31c0d0613136 Mon Sep 17 00:00:00 2001 From: Maxson Almeida Date: Wed, 17 Sep 2025 17:44:45 -0300 Subject: [PATCH 11/11] Remove astor dependency from requirements-dev.txt --- requirements-dev.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 7e2b788..3e2c94a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,6 @@ click==8.1.6 PyYAML==6.0.1 injector==0.21.0 pydantic<2.0.0 -astor>=0.8.1 APScheduler==3.10.4 pytz==2024.2 six==1.16.0