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/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 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 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] 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