├── .gitignore
├── LICENSE.MD
├── README.md
├── docs
├── images
│ ├── logo.png
│ └── logo3.png
└── index.md
├── easyschedule
├── __init__.py
├── cron.py
└── scheduler.py
├── images
└── logo.png
├── mkdocs.yml
├── nextbuild.py
├── setup.py
└── tests
└── test.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
--------------------------------------------------------------------------------
/LICENSE.MD:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Joshua Jamison
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 |
An asynchronus scheduler for python tasks, which uses Cron schedules for precise scheduling recurring tasks
4 |
5 | [](https://easyschedule.readthedocs.io/en/latest/?badge=latest) [](https://pypi.org/project/easyschedule/)
6 | #
7 |
8 | ## Documenation
9 | [https://easyschedule.readthedocs.io/en/latest/](https://easyschedule.readthedocs.io/en/latest/)
10 |
11 | #
12 |
13 | ## Get Started
14 | ```bash
15 | pip install easyschedule
16 | ```
17 |
18 | ```python
19 | import asyncio
20 | from easyschedule import EasyScheduler
21 |
22 | scheduler = EasyScheduler()
23 |
24 | default_args = {'args': [1, 2, 3]}
25 | weekday_every_minute = '* * * * MON-FRI'
26 |
27 | @scheduler(schedule=weekday_every_minute, default_args=default_args)
28 | def weekday_stuff(a, b, c):
29 | print(f"a {a} b: {b} c {c}")
30 |
31 | @scheduler.delayed_start(delay_in_seconds=30)
32 | async def delay_startup():
33 | print(f"## startup task - started ##")
34 | await asyncio.sleep(10)
35 | print(f"## startup task - ended ##")
36 |
37 | @scheduler.shutdown()
38 | async def shutdown():
39 | print(f"## shutdown task - started ##")
40 | await asyncio.sleep(10)
41 | print(f"## shutdown task - ended ##")
42 |
43 | @scheduler.once(date_string='2022-03-12 16:18:03')
44 | async def next_year():
45 | print(f"That was a long year")
46 |
47 | async def main():
48 | # start scheduler
49 | sched = asyncio.create_task(scheduler.start())
50 | await asyncio.sleep(10)
51 |
52 | # dynamicly schedule task
53 | wk_end_args = {'kwargs': {'count': 5}}
54 | weekend = '30 17-23,0-5 * * SAT,SUN'
55 |
56 | def weekend_stuff(count: int):
57 | for _ in range(count):
58 | weekday_stuff(3,4,5)
59 | weekday_stuff(5,6,7)
60 |
61 | scheduler.schedule(
62 | weekend_stuff,
63 | schedule=weekend,
64 | default_args=wk_end_args
65 | )
66 | await sched
67 |
68 | asyncio.run(main())
69 | ```
70 | ```bash
71 | 03-13 09:09:25 EasyScheduler WARNING weekday_stuff next_run_time: 2021-03-15 00:01:00.143645
72 | 03-13 09:09:25 EasyScheduler WARNING single task delay_startup scheduled to run at 2021-03-13 09:09:55.143337 in 30.0 s
73 | 03-13 09:09:25 EasyScheduler WARNING single task next_year scheduled to run at 2022-03-12 16:18:03 in 31475317.856636 s
74 | 03-13 09:09:35 EasyScheduler WARNING weekend_stuff next_run_time: 2021-03-13 17:31:00.152428
75 | 03-13 09:09:48 EasyScheduler WARNING shutdown task shutdown triggered at 2021-03-13 09:09:48.937516
76 | ## shutdown task - started ##
77 | ## shutdown task - ended ##
78 | Traceback (most recent call last):
79 | File "test.py", line 50, in
80 | asyncio.run(main())
81 | KeyboardInterrupt
82 | ```
83 | ## Cron syntax Compatability
84 |
85 | EasySchedule is capable of parsing most cron schedule syntax
86 |
87 | #
88 | ## Monthly
89 | First of month at 11:00 PM
90 | ```bash
91 | 0 23 1 * *
92 | ```
93 | #
94 |
95 | ## Daily
96 | Every 2 Hours
97 | ```bash
98 | 0 */2 * *
99 | ```
100 | #
101 |
102 | ## Weekends Only
103 | Every Hour Between 5:30 PM - 5:30 AM ##
104 | ```bash
105 | 30 17-23,0-5 * * SAT,SUN
106 | ```
107 | ## Cron Generator
108 | An easy & interactive way to build a cron schedule is available via [crontab.guru](https://crontab.guru/)
109 |
110 | ### Note: unsupported syntax (currently)
111 | ```bash
112 | @(non-standard)
113 | @hourly
114 | @daily
115 | @anually
116 | ```
117 |
118 | #
119 | ## Scheduluing Single Tasks
120 | EasySchedule is complete with single task scheduling
121 |
122 | ### Usage with 'once' decorator
123 | ```python
124 | from datetime import datetime, timedelta
125 |
126 | next_year = datetime.now() + timedelta(days=365)
127 |
128 | @scheduler.once(date=next_year)
129 | async def future_task():
130 | ## future work
131 | pass
132 |
133 | # current month: 2021-03-13 00:00:00
134 | @scheduler.once(date_string='2021-04-13 00:00:00')
135 | async def run_at_date():
136 | ## future work
137 | pass
138 |
139 | # current month: 2021-03-13 00:00:00
140 | @scheduler.once(delta=timedelta(days=3))
141 | async def run_after_delta():
142 | ## future work
143 | pass
144 |
145 | now_args={'kwargs': {'work': "Lots of work"}}
146 |
147 | @scheduler.once(now=True, default_args=now_args)
148 | async def run_now(work):
149 | ## future work
150 | print(f"starting {work}")
151 | pass
152 | ```
153 | #
154 | ## Schedule a task at or near application startup
155 | ```python
156 | notify = {
157 | 'kwargs': { 'emails': ['admin@company.org'] }
158 | }
159 |
160 | @scheduler.delayed_start(delay_in_seconds=30, default_args=notify)
161 | async def notify_online(emails: str):
162 | message = f"server is operational"
163 | await send_emails(message, emails)
164 | #something else
165 |
166 | async def get_data():
167 | return await requests.get('http://data-source')
168 |
169 | @scheduler.startup()
170 | async def update_database():
171 | data = await get_data()
172 | await db.update(data)
173 | #something else
174 | ```
175 | #
176 | ## Schedule a task to run at application shutdown
177 | ```python
178 | notify = {
179 | 'kwargs': { 'emails': ['admin@company.org'] }
180 | }
181 |
182 | @scheduler.shutdown(default_args=notify)
183 | async def notify_shutdown(emails: str):
184 | message = f"server is shutting down"
185 | await send_emails(message, emails)
186 | #something else?
187 | ```
--------------------------------------------------------------------------------
/docs/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codemation/easyschedule/4023332247ed3645b623b28388d38e13419bf7c4/docs/images/logo.png
--------------------------------------------------------------------------------
/docs/images/logo3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codemation/easyschedule/4023332247ed3645b623b28388d38e13419bf7c4/docs/images/logo3.png
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | An asynchronus scheduler for python tasks, which uses Cron schedules for precise scheduling recurring tasks
4 |
5 | #
6 |
7 | ### Get Started
8 |
9 | #### Installation
10 | ```bash
11 | pip install easyschedule
12 | ```
13 |
14 | ### Usage
15 |
16 | ```python
17 | import asyncio
18 | from easyschedule import EasyScheduler
19 |
20 | scheduler = EasyScheduler()
21 |
22 | default_args = {'args': [1, 2, 3]}
23 | weekday_every_minute = '* * * * MON-FRI'
24 |
25 | @scheduler(schedule=weekday_every_minute, default_args=default_args)
26 | def weekday_stuff(a, b, c):
27 | print(f"a {a} b: {b} c {c}")
28 |
29 | @scheduler.delayed_start(delay_in_seconds=30)
30 | async def delay_startup():
31 | print(f"## startup task - started ##")
32 | await asyncio.sleep(10)
33 | print(f"## startup task - ended ##")
34 |
35 | @scheduler.shutdown()
36 | async def shutdown():
37 | print(f"## shutdown task - started ##")
38 | await asyncio.sleep(10)
39 | print(f"## shutdown task - ended ##")
40 |
41 | @scheduler.once(date_string='2022-03-12 16:18:03')
42 | async def next_year():
43 | print(f"That was a long year")
44 |
45 | async def main():
46 | # start scheduler
47 | sched = asyncio.create_task(scheduler.start())
48 | await asyncio.sleep(10)
49 |
50 | # dynamicly schedule task
51 | wk_end_args = {'kwargs': {'count': 5}}
52 | weekend = '30 17-23,0-5 * * SAT,SUN'
53 |
54 | def weekend_stuff(count: int):
55 | for _ in range(count):
56 | weekday_stuff(3,4,5)
57 | weekday_stuff(5,6,7)
58 |
59 | scheduler.schedule(
60 | weekend_stuff,
61 | schedule=weekend,
62 | default_args=wk_end_args
63 | )
64 | await sched
65 |
66 | asyncio.run(main())
67 | ```
68 | ```bash
69 | 03-13 09:09:25 EasyScheduler WARNING weekday_stuff next_run_time: 2021-03-15 00:01:00.143645
70 | 03-13 09:09:25 EasyScheduler WARNING single task delay_startup scheduled to run at 2021-03-13 09:09:55.143337 in 30.0 s
71 | 03-13 09:09:25 EasyScheduler WARNING single task next_year scheduled to run at 2022-03-12 16:18:03 in 31475317.856636 s
72 | 03-13 09:09:35 EasyScheduler WARNING weekend_stuff next_run_time: 2021-03-13 17:31:00.152428
73 | 03-13 09:09:48 EasyScheduler WARNING shutdown task shutdown triggered at 2021-03-13 09:09:48.937516
74 | ## shutdown task - started ##
75 | ## shutdown task - ended ##
76 | Traceback (most recent call last):
77 | File "test.py", line 50, in
78 | asyncio.run(main())
79 | KeyboardInterrupt
80 | ```
81 | ### Cron syntax Compatability
82 |
83 | EasySchedule is capable of parsing most cron schedule syntax
84 |
85 | ### Cron Schedules
86 |
87 | #### Monthly
88 | !!! INFO "Monthly - First of month at 11:00 PM"
89 | 0 23 1 * *
90 |
91 | #### Daily
92 | !!! INFO "Daily - Every 2 Hours"
93 | 0 */2 * *
94 |
95 | #### Weekends Only
96 | !!! INFO "Every Hour Between 5:30 PM - 5:30 AM ##"
97 | 30 17-23,0-5 * * SAT,SUN
98 |
99 | ### Cron Generator
100 | An easy & interactive way to build a cron schedule is available via [crontab.guru](https://crontab.guru/)
101 |
102 | !!! DANGER "Unsupported syntax (currently)"
103 | @(non-standard)
104 | @hourly
105 | @daily
106 | @anually
107 |
108 |
109 |
110 | ### Scheduluing Single Tasks
111 | EasySchedule is complete with single task scheduling
112 |
113 |
114 | #### @scheduler.once()
115 | Usage with 'once' decorator
116 |
117 | ```python
118 | from datetime import datetime, timedelta
119 |
120 | next_year = datetime.now() + timedelta(days=365)
121 |
122 | @scheduler.once(date=next_year)
123 | async def future_task():
124 | ## future work
125 | pass
126 |
127 | # current month: 2021-03-13 00:00:00
128 | @scheduler.once(date_string='2021-04-13 00:00:00')
129 | async def run_at_date():
130 | ## future work
131 | pass
132 |
133 | # current month: 2021-03-13 00:00:00
134 | @scheduler.once(delta=timedelta(days=3))
135 | async def run_after_delta():
136 | ## future work
137 | pass
138 |
139 | now_args={'kwargs': {'work': "Lots of work"}}
140 |
141 | @scheduler.once(now=True, default_args=now_args)
142 | async def run_now(work):
143 | ## future work
144 | print(f"starting {work}")
145 | pass
146 | ```
147 | #### @scheduler.delayed_start()
148 | Schedule a task at or near application startup
149 |
150 | ```python
151 | notify = {
152 | 'kwargs': { 'emails': ['admin@company.org'] }
153 | }
154 |
155 | @scheduler.delayed_start(delay_in_seconds=30, default_args=notify)
156 | async def notify_online(emails: str):
157 | message = f"server is operational"
158 | await send_emails(message, emails)
159 | #something else
160 |
161 | async def get_data():
162 | return await requests.get('http://data-source')
163 | ```
164 | #### @scheduler.startup()
165 |
166 | ```python
167 | @scheduler.startup()
168 | async def update_database():
169 | data = await get_data()
170 | await db.update(data)
171 | #something else
172 | ```
173 | #
174 | ## Schedule a task to run at application shutdown
175 | ```python
176 | notify = {
177 | 'kwargs': { 'emails': ['admin@company.org'] }
178 | }
179 |
180 | @scheduler.shutdown(default_args=notify)
181 | async def notify_shutdown(emails: str):
182 | message = f"server is shutting down"
183 | await send_emails(message, emails)
184 | #something else?
185 | ```
--------------------------------------------------------------------------------
/easyschedule/__init__.py:
--------------------------------------------------------------------------------
1 | from .scheduler import EasyScheduler as EasyScheduler
--------------------------------------------------------------------------------
/easyschedule/cron.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | # days of week are purposely -1 of standard CRON
4 | # integers
5 | DAYS_OF_WEEK = {
6 | 'MON': 0, 'TUE': 1, 'WED': 2,
7 | 'THU': 3, 'FRI': 4, 'SAT': 5,
8 | 'SUN': 6, '7': 6, 7: 6, 0: 6,
9 | '0': 6, '1': 0, '2': 1, '3': 2,
10 | '4': 3, '5': 4, '6': 5
11 | }
12 |
13 | MONTHS_OF_YEAR = {
14 | 'JAN': 1, 'FEB': 2, 'MAR': 3,
15 | 'APR': 4, 'MAY': 5, 'JUN': 6,
16 | 'JUL': 7, 'AUG': 8, 'SEP': 9,
17 | 'OCT': 10,'NOV': 11,'DEC': 12
18 | }
19 | def parse_schedule_items(items: str, max_range: int):
20 | if not '*' in items:
21 | if ',' in items:
22 | items_split = items.split(',')
23 | items = []
24 | for i in items_split:
25 | step = 1
26 | if '/' in i:
27 | i, step = i.split('/')
28 | if '-' in i:
29 | start, end = i.split('-')
30 | items = items + [i for i in range(int(start), int(end)+1, int(step))]
31 | else:
32 | items.append(i)
33 |
34 | items = [int(i) for i in items]
35 | elif '-' in items:
36 | step = 1
37 | if '/' in items:
38 | items, step = items.split('/')
39 | start, end = items.split('-')
40 | items = [i for i in range(int(start), int(end)+1, int(step))]
41 | else:
42 | items = [int(items)]
43 | else:
44 | step = 1
45 | if '/' in items:
46 | items, step = items.split('/')
47 | if '-' in items:
48 | start, end = items.split('-')
49 | items = [i for i in range(int(start), int(end)+1, int(step))]
50 | else:
51 | items = [i for i in range(0, max_range, int(step))]
52 | return items
53 |
54 | def get_schedule_delay(schedule: str) -> tuple:
55 | """
56 | receives cron schedule and calculates next run time
57 | * * * * *
58 | min hour day month day(week)
59 | 0-59 0-23 1-31 0-12(JAN-DEC) 0-6(SUN-SAT)
60 | """
61 | now = datetime.datetime.now()
62 |
63 | minutes, hours, days, months, weekdays = schedule.split(' ')
64 |
65 | delta = datetime.datetime.now()
66 |
67 | if not weekdays == '*':
68 | if ',' in weekdays:
69 | weekdays = weekdays.split(',')
70 | if weekdays[0] in DAYS_OF_WEEK:
71 | weekdays = [DAYS_OF_WEEK[d] for d in weekdays]
72 |
73 | elif '-' in weekdays:
74 | weekdays = weekdays.split('-')
75 | if weekdays[0] in DAYS_OF_WEEK:
76 | start = DAYS_OF_WEEK[weekdays[0]]
77 | end = DAYS_OF_WEEK[weekdays[1]]
78 | weekdays = [i for i in range(start, end+1)]
79 | else:
80 | if weekdays in DAYS_OF_WEEK:
81 | weekdays = DAYS_OF_WEEK[weekdays]
82 | weekdays = [weekdays]
83 | else:
84 | weekdays = [int(v) for _,v in DAYS_OF_WEEK.items()]
85 |
86 | # months
87 |
88 | if not '*' in months:
89 |
90 | if ',' in months:
91 | months = months.split(',')
92 | if months[0] in MONTHS_OF_YEAR:
93 | months = [MONTHS_OF_YEAR[m] for m in months]
94 | if '-' in months:
95 | months = months.split('-')
96 | if months[0] in MONTHS_OF_YEAR:
97 | start = MONTHS_OF_YEAR[months[0]]
98 | end = MONTHS_OF_YEAR[months[1]]
99 | assert start < end, f"months start cannot be less than end"
100 | months = [i for i in range(int(start), int(end)+1)]
101 | else:
102 | if months.upper() in MONTHS_OF_YEAR:
103 | months = [MONTHS_OF_YEAR[months.upper()]]
104 | else:
105 | months = [int(months)]
106 | else:
107 | step =1
108 | if '/' in months:
109 | months, step = months.split('/')
110 | if '-' in months:
111 | months = months.split('-')
112 | if months[0] in MONTHS_OF_YEAR:
113 | start = MONTHS_OF_YEAR[months[0]]
114 | end = MONTHS_OF_YEAR[months[1]]
115 | assert start < end, f"months start cannot be less than end"
116 | months = [i for i in range(int(start), int(end)+1)]
117 | else:
118 | if '/' in months:
119 | months, step = months.split('/')
120 | start, end = months.split('-')
121 | months = [i for i in range(int(start), int(end)+1, int(step))]
122 | else:
123 | months = [i for i in range(1,13, int(step))]
124 |
125 | months = [int(i) for i in months]
126 |
127 | # days
128 | days = parse_schedule_items(days, 32)
129 | # Hours
130 | hours = parse_schedule_items(hours, 24)
131 | # minutes
132 | minutes = parse_schedule_items(minutes, 60)
133 |
134 | current = datetime.datetime.now()
135 |
136 | while True:
137 | if current.month not in months:
138 | current = current + datetime.timedelta(hours=23-current.hour, minutes=60-current.minute)
139 | continue
140 | if not current.weekday() in weekdays:
141 | current = current + datetime.timedelta(hours=23-current.hour, minutes=60-current.minute)
142 | continue
143 |
144 | if not current.day in days:
145 | current = current + datetime.timedelta(hours=23-current.hour, minutes=60-current.minute)
146 | continue
147 | if current.hour not in hours:
148 | current = current + datetime.timedelta(minutes=59-current.minute, seconds=60-current.second)
149 | continue
150 |
151 | if current.minute not in minutes:
152 | current = current + datetime.timedelta(seconds=60-current.second)
153 | continue
154 | else:
155 | current = current #+ datetime.timedelta(seconds=60-current.second)
156 | break
157 | delta = current - datetime.datetime.now()
158 | delta_seconds = delta.total_seconds()
159 |
160 | if delta_seconds < 60:
161 | delta_seconds = 60 - current.second
162 | current = current + datetime.timedelta(seconds=delta_seconds)
163 |
164 | return current, delta_seconds
--------------------------------------------------------------------------------
/easyschedule/scheduler.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import logging
3 | from datetime import datetime, timedelta
4 | from typing import Callable, Optional
5 | from easyschedule.cron import get_schedule_delay
6 |
7 | class EasyScheduler:
8 | def __init__(
9 | self,
10 | logger: Optional[logging.Logger] = None,
11 | debug: Optional[bool] = False
12 | ):
13 | self.scheduled_tasks = {}
14 | self.single_tasks = []
15 | self.loop = None
16 | ## logging
17 | level = 'DEBUG' if debug else None
18 | self.setup_logger(logger=logger, level=level)
19 |
20 | def setup_logger(
21 | self,
22 | logger: logging.Logger = None,
23 | level: str = None
24 | ) -> None:
25 | if logger == None:
26 | level = logging.DEBUG if level == 'DEBUG' else logging.WARNING
27 | logging.basicConfig(
28 | level=level,
29 | format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s',
30 | datefmt='%m-%d %H:%M:%S'
31 | )
32 | self.log = logging.getLogger(f'EasyScheduler')
33 | self.log.propogate = False
34 | else:
35 | self.log = logger
36 | def __call__(
37 | self,
38 | schedule: str,
39 | default_args: dict = {'args': [], 'kwargs': {}}
40 | ) -> Callable:
41 | if not 'args' in default_args:
42 | default_args['args'] = []
43 | if not 'kwargs' in default_args:
44 | default_args['kwargs'] = {}
45 | def scheduler(func):
46 | async def scheduled(*args, **kwargs):
47 | result = func(*args, **kwargs)
48 | if asyncio.iscoroutine(result):
49 | result = await result
50 | return result
51 | scheduled.__name__ = func.__name__
52 | self.scheduled_tasks[scheduled.__name__] = {
53 | 'func': scheduled,
54 | 'schedule': schedule,
55 | 'default_args': default_args
56 | }
57 | if self.loop:
58 | self.schedule_task(scheduled.__name__)
59 | return func
60 | return scheduler
61 | def schedule(
62 | self,
63 | task: Callable,
64 | schedule: str,
65 | default_args: dict = {}
66 | ):
67 | """
68 | schedule a recurring task
69 | """
70 | self(schedule=schedule, default_args=default_args)(task)
71 | def run_once(
72 | self,
73 | func: Callable,
74 | date: Optional[datetime] = None,
75 | date_string: Optional[str] = None,
76 | delta: Optional[timedelta] = None,
77 | now: Optional[bool] = False,
78 | on_event: Optional[str] = None,
79 | default_args: Optional[dict] = None,
80 | ):
81 | time_now = datetime.now()
82 | delay = None
83 | if date and date >= time_now:
84 | delay = date - time_now
85 | if date_string:
86 | date = datetime.fromisoformat(date_string)
87 | if date >= time_now:
88 | delay = date - time_now
89 | if delta:
90 | if not isinstance(delta, timedelta):
91 | raise Exception(f"delta is type {type(delta)}, expected {timedelta}")
92 | delay = delta.total_seconds()
93 | if now:
94 | delay = datetime.now() - time_now
95 | if isinstance(delay, timedelta):
96 | delay = delay.total_seconds()
97 |
98 | if not default_args:
99 | default_args = {}
100 | if not 'args' in default_args:
101 | default_args['args'] = []
102 | if not 'kwargs' in default_args:
103 | default_args['kwargs'] = {}
104 |
105 | if on_event and on_event == 'shutdown':
106 | async def shutdown_task():
107 | try:
108 | while True:
109 | await asyncio.sleep(60)
110 | except asyncio.CancelledError:
111 | time_now = datetime.now()
112 | self.log.warning(f"shutdown task {func.__name__} triggered at {time_now}")
113 | try:
114 | await func(*default_args['args'], **default_args['kwargs'])
115 | except Exception as e:
116 | self.log.exception(f"error running shutdown task - {task}")
117 | return
118 |
119 | if self.loop:
120 | asyncio.create_task(shutdown_task())
121 | else:
122 | self.single_tasks.append(shutdown_task)
123 | return
124 |
125 | async def single_task():
126 | try:
127 | run_time = time_now + timedelta(seconds=delay)
128 | self.log.warning(f"single task {func.__name__} scheduled to run at {run_time} in {delay} s")
129 | await asyncio.sleep(delay)
130 | try:
131 | await func(*default_args['args'], **default_args['kwargs'])
132 | except Exception as e:
133 | if isinstance(e, asyncio.CancelledError):
134 | raise e
135 | self.log.exception(f"error running single task - {func.__name__}")
136 | except asyncio.CancelledError:
137 | return
138 |
139 | if self.loop:
140 | asyncio.create_task(single_task())
141 | else:
142 | self.single_tasks.append(single_task)
143 | return
144 | def once(
145 | self,
146 | date: Optional[datetime] = None,
147 | date_string: Optional[str] = None,
148 | delta: Optional[timedelta] = None,
149 | now: Optional[bool] = False,
150 | default_args: Optional[dict] = None
151 | ):
152 | """
153 | Decoractor
154 | runs a single task at at input date, date_string, delta, now
155 | """
156 | def once_decorator(func):
157 | self.run_once(
158 | func,
159 | date=date,
160 | date_string=date_string,
161 | delta=delta,
162 | now=now,
163 | default_args=default_args
164 | )
165 | return func
166 | return once_decorator
167 | def startup(
168 | self,
169 | default_args: Optional[dict] = None
170 | ) -> Callable:
171 | """
172 | decorator
173 | runs a single task right after scheduler.start()
174 | Optional:
175 | default_args = {'args': [], 'kwargs': {}}
176 | """
177 | if self.loop and self.loop.is_running():
178 | raise Exception(f"scheduler has already started - cannot add startup tasks")
179 | def startup_decor(func):
180 |
181 | self.run_once(
182 | func,
183 | now=True,
184 | default_args=default_args
185 | )
186 | return func
187 | return startup_decor
188 | def delayed_start(
189 | self,
190 | delay_in_seconds: int = 60,
191 | default_args: Optional[dict] = None
192 | ):
193 | """
194 | decorator
195 | runs a single task after scheduler.start() with a delay
196 |
197 | delay_in_seconds: int = 60 #Default
198 | Optional:
199 | default_args = {'args': [], 'kwargs': {}}
200 | """
201 | def delayed_start_decor(func):
202 | self.run_once(
203 | func,
204 | delta=timedelta(seconds=delay_in_seconds),
205 | default_args=default_args
206 | )
207 | return func
208 | return delayed_start_decor
209 | def shutdown(
210 | self,
211 | default_args: Optional[dict] = None
212 | ):
213 | """
214 | decorator
215 | runs a single task after shutdown is detected
216 |
217 | Optional:
218 | default_args = {'args': [], 'kwargs': {}}
219 | """
220 | def shutdown_decor(func):
221 | self.run_once(
222 | func,
223 | default_args=default_args,
224 | on_event='shutdown'
225 | )
226 | return func
227 | return shutdown_decor
228 |
229 |
230 | def schedule_task(self, task: str) -> None:
231 | async def scheduled_task():
232 | try:
233 | while True:
234 | func = self.scheduled_tasks[task]['func']
235 | schedule = self.scheduled_tasks[task]['schedule']
236 | default_args = self.scheduled_tasks[task]['default_args']
237 | next_run_time, delay = get_schedule_delay(schedule)
238 | self.log.warning(f"{task} next_run_time: {next_run_time} - default_args: {default_args}")
239 | await asyncio.sleep(delay)
240 | try:
241 | await func(*default_args['args'], **default_args['kwargs'])
242 | except Exception as e:
243 | self.log.exception(f"error running scheduled task - {task}")
244 | except asyncio.CancelledError:
245 | return
246 | self.log.debug(f"schedule_task called for {self.scheduled_tasks[task]}")
247 | self.scheduled_tasks[task]['task'] = self.loop.create_task(
248 | scheduled_task()
249 | )
250 | async def start(self):
251 | if not self.loop:
252 | self.loop = asyncio.get_running_loop()
253 | for task in self.scheduled_tasks:
254 | self.schedule_task(task)
255 | for task in self.single_tasks:
256 | asyncio.create_task(task())
257 | try:
258 | while True:
259 | await asyncio.sleep(60)
260 | except asyncio.CancelledError:
261 | return
--------------------------------------------------------------------------------
/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codemation/easyschedule/4023332247ed3645b623b28388d38e13419bf7c4/images/logo.png
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: EasySchedule
2 | theme: readthedocs
3 | markdown_extensions:
4 | - admonition
--------------------------------------------------------------------------------
/nextbuild.py:
--------------------------------------------------------------------------------
1 | """
2 | Purpose:
3 | Increments current Pypi version by .001
4 |
5 | Usage:
6 | pip3 download easyschedule && ls easy_auth*.whl | sed 's/-/" "/g' | awk '{print "(" $2 ")"}' | python3 python/easyschedule/easyschedule/nextbuild.py
7 | """
8 | if __name__=='__main__':
9 | import sys
10 | version = sys.stdin.readline().rstrip()
11 | if '(' in version and ')' in version:
12 | right_i = version.index('(')
13 | left_i = version.index(')')
14 | version = version[right_i+2:left_i-1]
15 | print(f"{float(version)+0.001:.3f}")
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | BASE_REQUIREMENTS = []
4 | SERVER_REQUIREMENTS = []
5 | CLIENT_REQUIREMENTS = []
6 |
7 | with open("README.md", "r") as fh:
8 | long_description = fh.read()
9 | setuptools.setup(
10 | name='easyschedule',
11 | version='NEXTVERSION',
12 | packages=setuptools.find_packages(include=['easyschedule'], exclude=['build']),
13 | author="Joshua Jamison",
14 | author_email="joshjamison1@gmail.com",
15 | description="Easily schedule single or recurring sync/async tasks",
16 | long_description=long_description,
17 | long_description_content_type="text/markdown",
18 | url="https://github.com/codemation/easyschedule",
19 | classifiers=[
20 | "Programming Language :: Python :: 3",
21 | "License :: OSI Approved :: MIT License",
22 | "Operating System :: OS Independent",
23 | ],
24 | python_requires='>=3.7, <4',
25 | install_requires=BASE_REQUIREMENTS
26 | )
--------------------------------------------------------------------------------
/tests/test.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from easyschedule import EasyScheduler
3 |
4 | scheduler = EasyScheduler()
5 |
6 | default_args = {'args': [1, 2, 3]}
7 | weekday_every_minute = '* * * * MON-FRI'
8 |
9 | @scheduler(schedule=weekday_every_minute, default_args={'kwargs': {'url': 'test'}})
10 | def minute_stuff(url):
11 | print(f"minute_stuff: url - {url}")
12 |
13 | @scheduler(schedule=weekday_every_minute, default_args=default_args)
14 | def weekday_stuff(a, b, c):
15 | print(f"a {a} b: {b} c {c}")
16 |
17 | @scheduler.delayed_start(delay_in_seconds=30)
18 | async def delay_startup():
19 | print(f"## startup task - started ##")
20 | await asyncio.sleep(10)
21 | print(f"## startup task - ended ##")
22 |
23 | @scheduler.shutdown()
24 | async def shutdown():
25 | print(f"## shutdown task - started ##")
26 | await asyncio.sleep(10)
27 | print(f"## shutdown task - ended ##")
28 |
29 | @scheduler.once(date_string='2022-03-12 16:18:03')
30 | async def next_year():
31 | print(f"That was a long year")
32 |
33 | async def main():
34 | # start scheduler
35 | sched = asyncio.create_task(scheduler.start())
36 | await asyncio.sleep(10)
37 |
38 | # dynamicly schedule task
39 | wk_end_args = {'kwargs': {'count': 5}}
40 | weekend = '30 17-23,0-5 * * SAT,SUN'
41 |
42 | def weekend_stuff(count: int):
43 | for _ in range(count):
44 | print_stuff(3,4,5)
45 | print_stuff(5,6,7)
46 |
47 | scheduler.schedule(
48 | weekend_stuff,
49 | schedule=weekend,
50 | default_args=wk_end_args
51 | )
52 | await sched
53 |
54 | asyncio.run(main())
--------------------------------------------------------------------------------