├── .coveragerc ├── .gitignore ├── .gitlab-ci.yml ├── aschedule ├── __init__.py ├── api.py ├── ext.py └── helpers.py ├── build_script.sh ├── docs ├── Makefile ├── make.bat └── source │ ├── conf.py │ ├── index.rst │ ├── intro.rst │ └── tutorial.rst ├── examples └── repeated_job.py ├── license ├── readme.md ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── test_cancel.py ├── test_every.py ├── test_once_at.py ├── test_schedule_manager.py └── testing_ext.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | exclude_lines = 3 | raise AssertionError 4 | raise NotImplementedError 5 | raise StopIteration 6 | raise StopAsyncIteration 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | _.py 3 | .venv/ 4 | dist/ 5 | .idea/ 6 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | before_script: 2 | - "date" 3 | 4 | after_script: 5 | - "date" 6 | 7 | job_tests: 8 | image: python:3.5.2 9 | stage: test 10 | script: 11 | - "bash build_script.sh run_tests" 12 | 13 | pages: 14 | image: python:3.5.2 15 | script: 16 | - "bash build_script.sh build_pages" 17 | artifacts: 18 | paths: 19 | - public 20 | only: 21 | - master 22 | -------------------------------------------------------------------------------- /aschedule/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .api import every, once_at, cancel, \ 4 | JobSchedule, ScheduleManager, AScheduleException 5 | 6 | __all__ = ['every', 'once_at', 'cancel', 7 | 'ScheduleManager', 'JobSchedule', 'AScheduleException'] 8 | -------------------------------------------------------------------------------- /aschedule/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from uuid import uuid4 4 | from datetime import datetime, timedelta as _timedelta_cls 5 | from math import ceil 6 | from functools import partial 7 | import itertools 8 | import asyncio 9 | 10 | from .helpers import WaitAsyncIterator 11 | 12 | 13 | class JobSchedule(object): 14 | def __init__(self, get_coro_or_fut, intervals, loop=None): 15 | """ 16 | :param get_coro_or_fut: a callable which returns a co-routine object or a future 17 | or an awaitable. 18 | :param intervals: an iterator which gives interval times between consecutive 19 | jobs. 20 | :param loop: loop passed to asyncio loop 21 | """ 22 | self.uuid = uuid4() 23 | self.job = get_coro_or_fut 24 | self.intervals = intervals 25 | self.loop = loop 26 | self.running_jobs = set() 27 | self.wait_iter = WaitAsyncIterator(intervals, loop=loop) 28 | self.future = asyncio.ensure_future(self._run(), loop=loop) 29 | 30 | def _job_future_done_callback(self, future: asyncio.Future): 31 | self.running_jobs.remove(future) 32 | 33 | async def _run(self): 34 | async for _ in self.wait_iter: 35 | future = asyncio.ensure_future(self.job(), loop=self.loop) 36 | self.running_jobs.add(future) 37 | future.add_done_callback(self._job_future_done_callback) 38 | await asyncio.gather(*list(self.running_jobs), loop=self.loop) 39 | 40 | def _cancel(self, running_jobs=False): 41 | self.future.cancel() 42 | if running_jobs: 43 | running_jobs = list(self.running_jobs) 44 | for job_future in running_jobs: 45 | job_future.cancel() 46 | 47 | def __hash__(self): 48 | return hash(self.uuid) 49 | 50 | 51 | class ScheduleManager(object): 52 | 53 | def __init__(self): 54 | """ 55 | :var self.schedules: set of active schedules managed by this 56 | schedule manager 57 | """ 58 | self.schedules = set() 59 | 60 | def _schedule_done_callback(self, schedule, _): 61 | self.schedules.remove(schedule) # this must never give KeyError as 62 | 63 | def add_schedule(self, schedule): 64 | """ 65 | add a schedule to this manager so to safely cancel it. 66 | 67 | :param schedule: the schedule to add to this manager 68 | :return: None 69 | """ 70 | self.schedules.add(schedule) 71 | schedule.future.add_done_callback( 72 | partial(self._schedule_done_callback, schedule)) 73 | 74 | def every(self, get_coro_or_fut, interval: _timedelta_cls, start_at: datetime, 75 | count=float('inf'), loop=None): 76 | """ 77 | executes the given job at (start_at, start_at + interval, start_at + 2 * interval, ....) 78 | 79 | :param get_coro_or_fut: a callable which returns a co-routine or a future 80 | :param interval: a datetime.timedelta object denoting the interval between 81 | consecutive job excutions 82 | :param start_at: a datetime.datetime object denoting the start time to start 83 | this schedule 84 | :param count: the number of time to execute this job, by default the job will be executed 85 | continously 86 | :param loop: event loop if the given future by get_coro_or_fut is hooked up 87 | with a custom event loop 88 | :return: JobSchedule object so that the user can control the schedule based 89 | on it's future 90 | """ 91 | if interval.total_seconds() < 0: 92 | raise AScheduleException("error: invalid interval ({} seconds).".format( 93 | interval.total_seconds())) 94 | if count <= 0: 95 | raise AScheduleException("error: invalid count({}).".format(count)) 96 | diff = start_at - datetime.now() 97 | if round(diff.total_seconds()) < 0: 98 | start_at += interval * ceil((-diff) / interval) 99 | intervals = itertools.chain( 100 | iter([round((start_at - datetime.now()).total_seconds())]), 101 | map(lambda x: x[1], itertools.takewhile( 102 | lambda x: x[0] < (count - 1), 103 | enumerate(itertools.repeat(round(interval.total_seconds())))))) 104 | schedule = JobSchedule(get_coro_or_fut, intervals, loop=loop) 105 | self.add_schedule(schedule) 106 | return schedule 107 | 108 | def once_at(self, get_coro_or_fut, run_at: datetime, strict=False, loop=None): 109 | """ 110 | executes the job at run_at datetime, if the run_at is in the past then 111 | it will raise an exception if strict is set to True else it will schedule 112 | it to execute immediately. 113 | 114 | :param get_coro_or_fut: a callable which returns a co-routine or a future 115 | :param run_at: datetime object specifying the time at which the job should be 116 | executed 117 | :param strict: if this flag is set to True and if the run_at is in the past 118 | the job is scheduled to execute immediately else if the flag 119 | is False and the run_at is in the past then it will raise an 120 | exception 121 | :param loop: event loop if the given future by get_coro_or_fut is hooked up 122 | with a custom event loop 123 | :return: JobSchedule object so that the user can control the schedule based 124 | on it's future 125 | """ 126 | diff = run_at - datetime.now() 127 | if round(diff.total_seconds()) < 0 and strict: 128 | raise AScheduleException( 129 | "the given time({given}) is in past. " 130 | "current time is {now}. ".format(given=run_at, now=datetime.now())) 131 | intervals = iter([max(round(diff.total_seconds()), 0)]) 132 | schedule = JobSchedule(get_coro_or_fut, intervals, loop=loop) 133 | self.add_schedule(schedule) 134 | return schedule 135 | 136 | def cancel(self, schedule: JobSchedule, running_jobs=False): 137 | """ 138 | cancel's a schedule 139 | if running_jobs is set to true the currently scheduled jobs 140 | by this schedule are also cancelled else just the schedule is cancelled. 141 | 142 | :param schedule: the schedule (generated by self.every or self.once_at) 143 | that needs to be cancelled. 144 | :param running_jobs: if this flag is set to true, the currently scheduled jobs 145 | by this schedule are also cancelled 146 | :raises AScheduleException: if the given schedule is not generated by asyncio.every 147 | but by another instance of ScheduleManager then 148 | this will raise AScheduleException. 149 | if the given schedule is already cancelled then too it 150 | will raise the exception acting as if it doesn't know 151 | about it. 152 | :return: None 153 | """ 154 | if schedule not in self.schedules: 155 | raise AScheduleException("given schedule doesn't belong to this " 156 | "ScheduleManager instance") 157 | schedule._cancel(running_jobs) 158 | 159 | def shutdown(self): 160 | for schedule in self.schedules: 161 | schedule._cancel(running_jobs=True) 162 | 163 | 164 | class AScheduleException(Exception): 165 | pass 166 | 167 | 168 | default_schedule_manager = ScheduleManager() 169 | 170 | 171 | def cancel(schedule: JobSchedule, running_jobs=False): 172 | """ 173 | cancel's a schedule 174 | if running_jobs is set to true the currently scheduled jobs 175 | by this schedule are also cancelled else just the schedule is cancelled. 176 | 177 | :param schedule: the schedule(generated by aschedule.every or aschedule.once_at) 178 | that needs to be cancelled. 179 | :param running_jobs: if this flag is set to true, the currently scheduled jobs 180 | by this schedule are also cancelled 181 | :raises AScheduleException: if the given schedule is not generated by asyncio.every 182 | but by another instance of ScheduleManager then 183 | this will raise AScheduleException. 184 | if the given schedule is already cancelled then too it 185 | will raise the exception acting as if it doesn't know 186 | about it. 187 | """ 188 | default_schedule_manager.cancel(schedule, running_jobs) 189 | 190 | 191 | def every(job, seconds=0, minutes=0, hours=0, days=0, weeks=0, 192 | count=float('inf'), timedelta=None, start_at=None, loop=None): 193 | """ 194 | default execution schedule is (now, now + interval, now + 2 * interval, ....) 195 | if start_at is provided (start, start + interval, start + 2 * interval, ....) 196 | if start_at is less than now then the executions starts from 197 | start + interval * x such that it is greater than now. 198 | example: now = 2PM, start_at = 1PM, interval = 2 hr, then 199 | execution will be 3PM, 5PM, 7PM, ... 200 | 201 | Usage: 202 | >>> import aschedule 203 | >>> import asyncio 204 | >>> loop = asyncio.get_event_loop() 205 | >>> start = loop.time() 206 | >>> async def job(): 207 | >>> print(round(loop.time()), round(loop.time() - start)) 208 | >>> schedule = aschedule.every(job, seconds=5) 209 | >>> print(round(start), 0) 210 | >>> loop.run_forever() 211 | >>> 212 | 213 | :param job: a callable(co-routine function) which returns 214 | a co-routine or a future or an awaitable 215 | :param seconds: number of seconds, which will passed onto timedelta 216 | :param minutes: number of minutes, which will passed onto timedelta 217 | :param hours: number of hours, which will passed onto timedelta 218 | :param days: number of days, which will passed onto timedelta 219 | :param weeks: number of weeks, which will passed onto timedelta 220 | :param count: number of times to execute the job, by default it will be run infinite number of times. 221 | :param timedelta: the interval can also be given in the format of datetime.timedelta, 222 | then seconds, minutes, hours, days, weeks parameters are ignored. 223 | :param start_at: datetime at which the schedule starts if not 224 | provided schedule starts at (now). 225 | :param loop: io loop if the provided job is a custom future linked up 226 | with a different event loop. 227 | :return: schedule object, so it could be cancelled at will of the user 228 | 229 | """ 230 | if timedelta is None: 231 | timedelta = _timedelta_cls(seconds=seconds, minutes=minutes, hours=hours, 232 | days=days, weeks=weeks) 233 | if round(timedelta.total_seconds()) <= 0: 234 | raise AScheduleException("given interval is invalid i.e " 235 | "one of seconds, minutes, hours, days, weeks is invalid") 236 | if start_at is None: 237 | start_at = datetime.now() 238 | return default_schedule_manager.every(job, timedelta, start_at, loop=loop) 239 | 240 | 241 | def once_at(job, run_at: datetime, loop=None, strict=False): 242 | """ 243 | schedules a job at the given time 244 | 245 | :param job: a callable(co-routine function) which returns 246 | a co-routine or a future or an awaitable 247 | :param run_at: datetime object at which the job should be executed once 248 | even if it is past it will be executed. 249 | :param loop: event loop if provided will be given to asyncio helper methods 250 | :param strict: if the run_at is in the past this will raise an exception if strict 251 | is True, if strict is False it will assume it to be a pending job 252 | and will schedule it to execute asap. 253 | :return: future of the schedule, so it could be cancelled at will of the user 254 | """ 255 | return default_schedule_manager.once_at(job, run_at, strict=strict, loop=loop) 256 | -------------------------------------------------------------------------------- /aschedule/ext.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from datetime import timedelta, datetime 3 | import asyncio 4 | import random 5 | from .api import every, once_at, JobSchedule, default_schedule_manager 6 | 7 | __all__ = ['every_day', 'every_week', 'every_monday', 'every_tuesday', 'every_wednesday', 8 | 'every_thursday', 'every_friday', 'every_saturday', 'every_sunday', 9 | 'once_at_next_monday', 'once_at_next_tuesday', 'once_at_next_wednesday', 10 | 'once_at_next_thursday', 'once_at_next_friday', 'once_at_next_saturday', 11 | 'once_at_next_sunday', 'every_random_interval'] 12 | 13 | 14 | def every_random_interval(job, interval: timedelta, loop=None): 15 | """ 16 | executes the job randomly once in the specified interval. 17 | example: 18 | run a job every day at random time 19 | run a job every hour at random time 20 | :param job: a callable(co-routine function) which returns 21 | a co-routine or a future or an awaitable 22 | :param interval: the interval can also be given in the format of datetime.timedelta, 23 | then seconds, minutes, hours, days, weeks parameters are ignored. 24 | :param loop: io loop if the provided job is a custom future linked up 25 | with a different event loop. 26 | :return: schedule object, so it could be cancelled at will of the user by 27 | aschedule.cancel(schedule) 28 | """ 29 | if loop is None: 30 | loop = asyncio.get_event_loop() 31 | start = loop.time() 32 | 33 | def wait_time_gen(): 34 | count = 0 35 | while True: 36 | rand = random.randrange(round(interval.total_seconds())) 37 | tmp = round(start + interval.total_seconds() * count + rand - loop.time()) 38 | yield tmp 39 | count += 1 40 | 41 | schedule = JobSchedule(job, wait_time_gen(), loop=loop) 42 | # add it to default_schedule_manager, so that user can aschedule.cancel it 43 | default_schedule_manager.add_schedule(schedule) 44 | return schedule 45 | 46 | 47 | def every_day(job, loop=None): 48 | return every(job, timedelta=timedelta(days=1), loop=loop) 49 | 50 | 51 | def every_week(job, loop=None): 52 | return every(job, timedelta=timedelta(days=7), loop=loop) 53 | 54 | every_monday = lambda job, loop=None: _every_weekday(job, 0, loop=loop) 55 | every_tuesday = lambda job, loop=None: _every_weekday(job, 1, loop=loop) 56 | every_wednesday = lambda job, loop=None: _every_weekday(job, 2, loop=loop) 57 | every_thursday = lambda job, loop=None: _every_weekday(job, 3, loop=loop) 58 | every_friday = lambda job, loop=None: _every_weekday(job, 4, loop=loop) 59 | every_saturday = lambda job, loop=None: _every_weekday(job, 5, loop=loop) 60 | every_sunday = lambda job, loop=None: _every_weekday(job, 6, loop=loop) 61 | 62 | once_at_next_monday = lambda job, loop=None: _once_at_weekday(job, 0, loop=loop) 63 | once_at_next_tuesday = lambda job, loop=None: _once_at_weekday(job, 1, loop=loop) 64 | once_at_next_wednesday = lambda job, loop=None: _once_at_weekday(job, 2, loop=loop) 65 | once_at_next_thursday = lambda job, loop=None: _once_at_weekday(job, 3, loop=loop) 66 | once_at_next_friday = lambda job, loop=None: _once_at_weekday(job, 4, loop=loop) 67 | once_at_next_saturday = lambda job, loop=None: _once_at_weekday(job, 5, loop=loop) 68 | once_at_next_sunday = lambda job, loop=None: _once_at_weekday(job, 6, loop=loop) 69 | 70 | 71 | def _nearest_weekday(weekday): 72 | return datetime.now() + timedelta(days=(weekday - datetime.now().weekday()) % 7) 73 | 74 | 75 | def _every_weekday(job, weekday, loop=None): 76 | return every(job, timedelta=timedelta(days=7), start_at=_nearest_weekday(weekday), loop=loop) 77 | 78 | 79 | def _once_at_weekday(job, weekday, loop=None): 80 | return once_at(job, _nearest_weekday(weekday), loop=loop) 81 | -------------------------------------------------------------------------------- /aschedule/helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | import itertools 4 | 5 | 6 | class WaitAsyncIterator(object): 7 | def __init__(self, wait_times, results=None, loop=None): 8 | self.wait_times = wait_times 9 | self.results = results 10 | if self.results is None: 11 | self.results = itertools.repeat(None) 12 | self.loop = loop 13 | 14 | def __aiter__(self): 15 | return self 16 | 17 | async def __anext__(self): 18 | try: 19 | return await asyncio.sleep(next(self.wait_times), 20 | result=next(self.results, None), 21 | loop=self.loop) 22 | except StopIteration: 23 | raise StopAsyncIteration 24 | -------------------------------------------------------------------------------- /build_script.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | run_tests() { 4 | apt-get update -qy && 5 | apt-get install -y python3-dev python3-pip && 6 | pip3 install -r requirements.txt && 7 | python3.5 -c "if not hasattr(__import__('sys'), 'real_prefix'): print('WARNING: tests running outside virtualenv')" && 8 | nosetests --with-coverage --cover-package=aschedule --cover-min-percentage=90 --cover-config-file=.coveragerc --processes=50 --process-timeout=600 --cover-inclusive 9 | return $? 10 | } 11 | 12 | install() { 13 | apt-get update -qy && 14 | apt-get install -y python3-dev python3-pip && 15 | python3.5 setup.py install 16 | return $? 17 | } 18 | 19 | build_pages() { 20 | apt-get update -qy && 21 | apt-get install -y python3-dev python3-pip && 22 | pip3 install -r requirements.txt && 23 | make -C docs html && 24 | mkdir public && 25 | cp -rv docs/build/html/* public/ 26 | return $? 27 | } 28 | 29 | date 30 | "$@" 31 | exit_code=$? 32 | date 33 | 34 | exit $exit_code 35 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 16 | 17 | .PHONY: help 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " dirhtml to make HTML files named index.html in directories" 22 | @echo " singlehtml to make a single large HTML file" 23 | @echo " pickle to make pickle files" 24 | @echo " json to make JSON files" 25 | @echo " htmlhelp to make HTML files and a HTML help project" 26 | @echo " qthelp to make HTML files and a qthelp project" 27 | @echo " applehelp to make an Apple Help Book" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " epub3 to make an epub3" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 34 | @echo " text to make text files" 35 | @echo " man to make manual pages" 36 | @echo " texinfo to make Texinfo files" 37 | @echo " info to make Texinfo files and run them through makeinfo" 38 | @echo " gettext to make PO message catalogs" 39 | @echo " changes to make an overview of all changed/added/deprecated items" 40 | @echo " xml to make Docutils-native XML files" 41 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 42 | @echo " linkcheck to check all external links for integrity" 43 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 44 | @echo " coverage to run coverage check of the documentation (if enabled)" 45 | @echo " dummy to check syntax errors of document sources" 46 | 47 | .PHONY: clean 48 | clean: 49 | rm -rf $(BUILDDIR)/* 50 | 51 | .PHONY: html 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | .PHONY: dirhtml 58 | dirhtml: 59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 60 | @echo 61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 62 | 63 | .PHONY: singlehtml 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | .PHONY: pickle 70 | pickle: 71 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 72 | @echo 73 | @echo "Build finished; now you can process the pickle files." 74 | 75 | .PHONY: json 76 | json: 77 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 78 | @echo 79 | @echo "Build finished; now you can process the JSON files." 80 | 81 | .PHONY: htmlhelp 82 | htmlhelp: 83 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 84 | @echo 85 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 86 | ".hhp project file in $(BUILDDIR)/htmlhelp." 87 | 88 | .PHONY: qthelp 89 | qthelp: 90 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 91 | @echo 92 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 93 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 94 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/aschedule.qhcp" 95 | @echo "To view the help file:" 96 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/aschedule.qhc" 97 | 98 | .PHONY: applehelp 99 | applehelp: 100 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 101 | @echo 102 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 103 | @echo "N.B. You won't be able to view it unless you put it in" \ 104 | "~/Library/Documentation/Help or install it in your application" \ 105 | "bundle." 106 | 107 | .PHONY: devhelp 108 | devhelp: 109 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 110 | @echo 111 | @echo "Build finished." 112 | @echo "To view the help file:" 113 | @echo "# mkdir -p $$HOME/.local/share/devhelp/aschedule" 114 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/aschedule" 115 | @echo "# devhelp" 116 | 117 | .PHONY: epub 118 | epub: 119 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 120 | @echo 121 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 122 | 123 | .PHONY: epub3 124 | epub3: 125 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 126 | @echo 127 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 128 | 129 | .PHONY: latex 130 | latex: 131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 132 | @echo 133 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 134 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 135 | "(use \`make latexpdf' here to do that automatically)." 136 | 137 | .PHONY: latexpdf 138 | latexpdf: 139 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 140 | @echo "Running LaTeX files through pdflatex..." 141 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 142 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 143 | 144 | .PHONY: latexpdfja 145 | latexpdfja: 146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 147 | @echo "Running LaTeX files through platex and dvipdfmx..." 148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 150 | 151 | .PHONY: text 152 | text: 153 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 154 | @echo 155 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 156 | 157 | .PHONY: man 158 | man: 159 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 160 | @echo 161 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 162 | 163 | .PHONY: texinfo 164 | texinfo: 165 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 166 | @echo 167 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 168 | @echo "Run \`make' in that directory to run these through makeinfo" \ 169 | "(use \`make info' here to do that automatically)." 170 | 171 | .PHONY: info 172 | info: 173 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 174 | @echo "Running Texinfo files through makeinfo..." 175 | make -C $(BUILDDIR)/texinfo info 176 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 177 | 178 | .PHONY: gettext 179 | gettext: 180 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 181 | @echo 182 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 183 | 184 | .PHONY: changes 185 | changes: 186 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 187 | @echo 188 | @echo "The overview file is in $(BUILDDIR)/changes." 189 | 190 | .PHONY: linkcheck 191 | linkcheck: 192 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 193 | @echo 194 | @echo "Link check complete; look for any errors in the above output " \ 195 | "or in $(BUILDDIR)/linkcheck/output.txt." 196 | 197 | .PHONY: doctest 198 | doctest: 199 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 200 | @echo "Testing of doctests in the sources finished, look at the " \ 201 | "results in $(BUILDDIR)/doctest/output.txt." 202 | 203 | .PHONY: coverage 204 | coverage: 205 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 206 | @echo "Testing of coverage in the sources finished, look at the " \ 207 | "results in $(BUILDDIR)/coverage/python.txt." 208 | 209 | .PHONY: xml 210 | xml: 211 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 212 | @echo 213 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 214 | 215 | .PHONY: pseudoxml 216 | pseudoxml: 217 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 218 | @echo 219 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 220 | 221 | .PHONY: dummy 222 | dummy: 223 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 224 | @echo 225 | @echo "Build finished. Dummy builder generates no files." 226 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | echo. dummy to check syntax errors of document sources 43 | goto end 44 | ) 45 | 46 | if "%1" == "clean" ( 47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 48 | del /q /s %BUILDDIR%\* 49 | goto end 50 | ) 51 | 52 | 53 | REM Check if sphinx-build is available and fallback to Python version if any 54 | %SPHINXBUILD% 1>NUL 2>NUL 55 | if errorlevel 9009 goto sphinx_python 56 | goto sphinx_ok 57 | 58 | :sphinx_python 59 | 60 | set SPHINXBUILD=python -m sphinx.__init__ 61 | %SPHINXBUILD% 2> nul 62 | if errorlevel 9009 ( 63 | echo. 64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 65 | echo.installed, then set the SPHINXBUILD environment variable to point 66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 67 | echo.may add the Sphinx directory to PATH. 68 | echo. 69 | echo.If you don't have Sphinx installed, grab it from 70 | echo.http://sphinx-doc.org/ 71 | exit /b 1 72 | ) 73 | 74 | :sphinx_ok 75 | 76 | 77 | if "%1" == "html" ( 78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 79 | if errorlevel 1 exit /b 1 80 | echo. 81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 82 | goto end 83 | ) 84 | 85 | if "%1" == "dirhtml" ( 86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 87 | if errorlevel 1 exit /b 1 88 | echo. 89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 90 | goto end 91 | ) 92 | 93 | if "%1" == "singlehtml" ( 94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 95 | if errorlevel 1 exit /b 1 96 | echo. 97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 98 | goto end 99 | ) 100 | 101 | if "%1" == "pickle" ( 102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 103 | if errorlevel 1 exit /b 1 104 | echo. 105 | echo.Build finished; now you can process the pickle files. 106 | goto end 107 | ) 108 | 109 | if "%1" == "json" ( 110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished; now you can process the JSON files. 114 | goto end 115 | ) 116 | 117 | if "%1" == "htmlhelp" ( 118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished; now you can run HTML Help Workshop with the ^ 122 | .hhp project file in %BUILDDIR%/htmlhelp. 123 | goto end 124 | ) 125 | 126 | if "%1" == "qthelp" ( 127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 128 | if errorlevel 1 exit /b 1 129 | echo. 130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 131 | .qhcp project file in %BUILDDIR%/qthelp, like this: 132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\aschedule.qhcp 133 | echo.To view the help file: 134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\aschedule.ghc 135 | goto end 136 | ) 137 | 138 | if "%1" == "devhelp" ( 139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished. 143 | goto end 144 | ) 145 | 146 | if "%1" == "epub" ( 147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 148 | if errorlevel 1 exit /b 1 149 | echo. 150 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 151 | goto end 152 | ) 153 | 154 | if "%1" == "epub3" ( 155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 156 | if errorlevel 1 exit /b 1 157 | echo. 158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 159 | goto end 160 | ) 161 | 162 | if "%1" == "latex" ( 163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 164 | if errorlevel 1 exit /b 1 165 | echo. 166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdf" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "latexpdfja" ( 181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 182 | cd %BUILDDIR%/latex 183 | make all-pdf-ja 184 | cd %~dp0 185 | echo. 186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 187 | goto end 188 | ) 189 | 190 | if "%1" == "text" ( 191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 192 | if errorlevel 1 exit /b 1 193 | echo. 194 | echo.Build finished. The text files are in %BUILDDIR%/text. 195 | goto end 196 | ) 197 | 198 | if "%1" == "man" ( 199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 200 | if errorlevel 1 exit /b 1 201 | echo. 202 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 203 | goto end 204 | ) 205 | 206 | if "%1" == "texinfo" ( 207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 208 | if errorlevel 1 exit /b 1 209 | echo. 210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 211 | goto end 212 | ) 213 | 214 | if "%1" == "gettext" ( 215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 216 | if errorlevel 1 exit /b 1 217 | echo. 218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 219 | goto end 220 | ) 221 | 222 | if "%1" == "changes" ( 223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 224 | if errorlevel 1 exit /b 1 225 | echo. 226 | echo.The overview file is in %BUILDDIR%/changes. 227 | goto end 228 | ) 229 | 230 | if "%1" == "linkcheck" ( 231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 232 | if errorlevel 1 exit /b 1 233 | echo. 234 | echo.Link check complete; look for any errors in the above output ^ 235 | or in %BUILDDIR%/linkcheck/output.txt. 236 | goto end 237 | ) 238 | 239 | if "%1" == "doctest" ( 240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 241 | if errorlevel 1 exit /b 1 242 | echo. 243 | echo.Testing of doctests in the sources finished, look at the ^ 244 | results in %BUILDDIR%/doctest/output.txt. 245 | goto end 246 | ) 247 | 248 | if "%1" == "coverage" ( 249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 250 | if errorlevel 1 exit /b 1 251 | echo. 252 | echo.Testing of coverage in the sources finished, look at the ^ 253 | results in %BUILDDIR%/coverage/python.txt. 254 | goto end 255 | ) 256 | 257 | if "%1" == "xml" ( 258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 259 | if errorlevel 1 exit /b 1 260 | echo. 261 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 262 | goto end 263 | ) 264 | 265 | if "%1" == "pseudoxml" ( 266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 267 | if errorlevel 1 exit /b 1 268 | echo. 269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 270 | goto end 271 | ) 272 | 273 | if "%1" == "dummy" ( 274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 275 | if errorlevel 1 exit /b 1 276 | echo. 277 | echo.Build finished. Dummy builder generates no files. 278 | goto end 279 | ) 280 | 281 | :end 282 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # aschedule documentation build configuration file, created by 5 | # sphinx-quickstart on Sat Sep 3 21:51:06 2016. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | sys.path.insert(0, os.path.abspath('../../')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | # 28 | # needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.todo', 36 | 'sphinx.ext.mathjax', 37 | 'sphinx.ext.ifconfig', 38 | 'sphinx.ext.viewcode', 39 | ] 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ['_templates'] 43 | 44 | # The suffix(es) of source filenames. 45 | # You can specify multiple suffix as a list of string: 46 | # 47 | # source_suffix = ['.rst', '.md'] 48 | source_suffix = '.rst' 49 | 50 | # The encoding of source files. 51 | # 52 | # source_encoding = 'utf-8-sig' 53 | 54 | # The master toctree document. 55 | master_doc = 'index' 56 | 57 | # General information about the project. 58 | project = 'aschedule' 59 | copyright = '2016, Srinivas Devaki' 60 | author = 'Srinivas Devaki' 61 | 62 | # The version info for the project you're documenting, acts as replacement for 63 | # |version| and |release|, also used in various other places throughout the 64 | # built documents. 65 | # 66 | # The short X.Y version. 67 | version = '0.0.2.dev0' 68 | # The full version, including alpha/beta/rc tags. 69 | release = 'development' 70 | 71 | # The language for content autogenerated by Sphinx. Refer to documentation 72 | # for a list of supported languages. 73 | # 74 | # This is also used if you do content translation via gettext catalogs. 75 | # Usually you set "language" from the command line for these cases. 76 | language = None 77 | 78 | # There are two options for replacing |today|: either, you set today to some 79 | # non-false value, then it is used: 80 | # 81 | # today = '' 82 | # 83 | # Else, today_fmt is used as the format for a strftime call. 84 | # 85 | # today_fmt = '%B %d, %Y' 86 | 87 | # List of patterns, relative to source directory, that match files and 88 | # directories to ignore when looking for source files. 89 | # This patterns also effect to html_static_path and html_extra_path 90 | exclude_patterns = [] 91 | 92 | # The reST default role (used for this markup: `text`) to use for all 93 | # documents. 94 | # 95 | # default_role = None 96 | 97 | # If true, '()' will be appended to :func: etc. cross-reference text. 98 | # 99 | # add_function_parentheses = True 100 | 101 | # If true, the current module name will be prepended to all description 102 | # unit titles (such as .. function::). 103 | # 104 | # add_module_names = True 105 | 106 | # If true, sectionauthor and moduleauthor directives will be shown in the 107 | # output. They are ignored by default. 108 | # 109 | # show_authors = False 110 | 111 | # The name of the Pygments (syntax highlighting) style to use. 112 | pygments_style = 'sphinx' 113 | 114 | # A list of ignored prefixes for module index sorting. 115 | # modindex_common_prefix = [] 116 | 117 | # If true, keep warnings as "system message" paragraphs in the built documents. 118 | # keep_warnings = False 119 | 120 | # If true, `todo` and `todoList` produce output, else they produce nothing. 121 | todo_include_todos = True 122 | 123 | 124 | # -- Options for HTML output ---------------------------------------------- 125 | 126 | # The theme to use for HTML and HTML Help pages. See the documentation for 127 | # a list of builtin themes. 128 | # 129 | html_theme = 'alabaster' 130 | 131 | # Theme options are theme-specific and customize the look and feel of a theme 132 | # further. For a list of options available for each theme, see the 133 | # documentation. 134 | # 135 | # html_theme_options = {} 136 | 137 | # Add any paths that contain custom themes here, relative to this directory. 138 | # html_theme_path = [] 139 | 140 | # The name for this set of Sphinx documents. 141 | # " v documentation" by default. 142 | # 143 | # html_title = 'aschedule vdevelopment' 144 | 145 | # A shorter title for the navigation bar. Default is the same as html_title. 146 | # 147 | # html_short_title = None 148 | 149 | # The name of an image file (relative to this directory) to place at the top 150 | # of the sidebar. 151 | # 152 | # html_logo = None 153 | 154 | # The name of an image file (relative to this directory) to use as a favicon of 155 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 156 | # pixels large. 157 | # 158 | # html_favicon = None 159 | 160 | # Add any paths that contain custom static files (such as style sheets) here, 161 | # relative to this directory. They are copied after the builtin static files, 162 | # so a file named "default.css" will overwrite the builtin "default.css". 163 | html_static_path = ['_static'] 164 | 165 | # Add any extra paths that contain custom files (such as robots.txt or 166 | # .htaccess) here, relative to this directory. These files are copied 167 | # directly to the root of the documentation. 168 | # 169 | # html_extra_path = [] 170 | 171 | # If not None, a 'Last updated on:' timestamp is inserted at every page 172 | # bottom, using the given strftime format. 173 | # The empty string is equivalent to '%b %d, %Y'. 174 | # 175 | # html_last_updated_fmt = None 176 | 177 | # If true, SmartyPants will be used to convert quotes and dashes to 178 | # typographically correct entities. 179 | # 180 | # html_use_smartypants = True 181 | 182 | # Custom sidebar templates, maps document names to template names. 183 | # 184 | # html_sidebars = {} 185 | 186 | # Additional templates that should be rendered to pages, maps page names to 187 | # template names. 188 | # 189 | # html_additional_pages = {} 190 | 191 | # If false, no module index is generated. 192 | # 193 | # html_domain_indices = True 194 | 195 | # If false, no index is generated. 196 | # 197 | # html_use_index = True 198 | 199 | # If true, the index is split into individual pages for each letter. 200 | # 201 | # html_split_index = False 202 | 203 | # If true, links to the reST sources are added to the pages. 204 | # 205 | # html_show_sourcelink = True 206 | 207 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 208 | # 209 | # html_show_sphinx = True 210 | 211 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 212 | # 213 | # html_show_copyright = True 214 | 215 | # If true, an OpenSearch description file will be output, and all pages will 216 | # contain a tag referring to it. The value of this option must be the 217 | # base URL from which the finished HTML is served. 218 | # 219 | # html_use_opensearch = '' 220 | 221 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 222 | # html_file_suffix = None 223 | 224 | # Language to be used for generating the HTML full-text search index. 225 | # Sphinx supports the following languages: 226 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 227 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' 228 | # 229 | # html_search_language = 'en' 230 | 231 | # A dictionary with options for the search language support, empty by default. 232 | # 'ja' uses this config value. 233 | # 'zh' user can custom change `jieba` dictionary path. 234 | # 235 | # html_search_options = {'type': 'default'} 236 | 237 | # The name of a javascript file (relative to the configuration directory) that 238 | # implements a search results scorer. If empty, the default will be used. 239 | # 240 | # html_search_scorer = 'scorer.js' 241 | 242 | # Output file base name for HTML help builder. 243 | htmlhelp_basename = 'ascheduledoc' 244 | 245 | # -- Options for LaTeX output --------------------------------------------- 246 | 247 | latex_elements = { 248 | # The paper size ('letterpaper' or 'a4paper'). 249 | # 250 | # 'papersize': 'letterpaper', 251 | 252 | # The font size ('10pt', '11pt' or '12pt'). 253 | # 254 | # 'pointsize': '10pt', 255 | 256 | # Additional stuff for the LaTeX preamble. 257 | # 258 | # 'preamble': '', 259 | 260 | # Latex figure (float) alignment 261 | # 262 | # 'figure_align': 'htbp', 263 | } 264 | 265 | # Grouping the document tree into LaTeX files. List of tuples 266 | # (source start file, target name, title, 267 | # author, documentclass [howto, manual, or own class]). 268 | latex_documents = [ 269 | (master_doc, 'aschedule.tex', 'aschedule Documentation', 270 | 'Srinivas Devaki', 'manual'), 271 | ] 272 | 273 | # The name of an image file (relative to this directory) to place at the top of 274 | # the title page. 275 | # 276 | # latex_logo = None 277 | 278 | # For "manual" documents, if this is true, then toplevel headings are parts, 279 | # not chapters. 280 | # 281 | # latex_use_parts = False 282 | 283 | # If true, show page references after internal links. 284 | # 285 | # latex_show_pagerefs = False 286 | 287 | # If true, show URL addresses after external links. 288 | # 289 | # latex_show_urls = False 290 | 291 | # Documents to append as an appendix to all manuals. 292 | # 293 | # latex_appendices = [] 294 | 295 | # It false, will not define \strong, \code, itleref, \crossref ... but only 296 | # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added 297 | # packages. 298 | # 299 | # latex_keep_old_macro_names = True 300 | 301 | # If false, no module index is generated. 302 | # 303 | # latex_domain_indices = True 304 | 305 | 306 | # -- Options for manual page output --------------------------------------- 307 | 308 | # One entry per manual page. List of tuples 309 | # (source start file, name, description, authors, manual section). 310 | man_pages = [ 311 | (master_doc, 'aschedule', 'aschedule Documentation', 312 | [author], 1) 313 | ] 314 | 315 | # If true, show URL addresses after external links. 316 | # 317 | # man_show_urls = False 318 | 319 | 320 | # -- Options for Texinfo output ------------------------------------------- 321 | 322 | # Grouping the document tree into Texinfo files. List of tuples 323 | # (source start file, target name, title, author, 324 | # dir menu entry, description, category) 325 | texinfo_documents = [ 326 | (master_doc, 'aschedule', 'aschedule Documentation', 327 | author, 'aschedule', 'One line description of project.', 328 | 'Miscellaneous'), 329 | ] 330 | 331 | # Documents to append as an appendix to all manuals. 332 | # 333 | # texinfo_appendices = [] 334 | 335 | # If false, no module index is generated. 336 | # 337 | # texinfo_domain_indices = True 338 | 339 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 340 | # 341 | # texinfo_show_urls = 'footnote' 342 | 343 | # If true, do not generate a @detailmenu in the "Top" node's menu. 344 | # 345 | # texinfo_no_detailmenu = False 346 | 347 | 348 | # -- Options for Epub output ---------------------------------------------- 349 | 350 | # Bibliographic Dublin Core info. 351 | epub_title = project 352 | epub_author = author 353 | epub_publisher = author 354 | epub_copyright = copyright 355 | 356 | # The basename for the epub file. It defaults to the project name. 357 | # epub_basename = project 358 | 359 | # The HTML theme for the epub output. Since the default themes are not 360 | # optimized for small screen space, using the same theme for HTML and epub 361 | # output is usually not wise. This defaults to 'epub', a theme designed to save 362 | # visual space. 363 | # 364 | # epub_theme = 'epub' 365 | 366 | # The language of the text. It defaults to the language option 367 | # or 'en' if the language is not set. 368 | # 369 | # epub_language = '' 370 | 371 | # The scheme of the identifier. Typical schemes are ISBN or URL. 372 | # epub_scheme = '' 373 | 374 | # The unique identifier of the text. This can be a ISBN number 375 | # or the project homepage. 376 | # 377 | # epub_identifier = '' 378 | 379 | # A unique identification for the text. 380 | # 381 | # epub_uid = '' 382 | 383 | # A tuple containing the cover image and cover page html template filenames. 384 | # 385 | # epub_cover = () 386 | 387 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 388 | # 389 | # epub_guide = () 390 | 391 | # HTML files that should be inserted before the pages created by sphinx. 392 | # The format is a list of tuples containing the path and title. 393 | # 394 | # epub_pre_files = [] 395 | 396 | # HTML files that should be inserted after the pages created by sphinx. 397 | # The format is a list of tuples containing the path and title. 398 | # 399 | # epub_post_files = [] 400 | 401 | # A list of files that should not be packed into the epub file. 402 | epub_exclude_files = ['search.html'] 403 | 404 | # The depth of the table of contents in toc.ncx. 405 | # 406 | # epub_tocdepth = 3 407 | 408 | # Allow duplicate toc entries. 409 | # 410 | # epub_tocdup = True 411 | 412 | # Choose between 'default' and 'includehidden'. 413 | # 414 | # epub_tocscope = 'default' 415 | 416 | # Fix unsupported image types using the Pillow. 417 | # 418 | # epub_fix_images = False 419 | 420 | # Scale large images. 421 | # 422 | # epub_max_image_width = 0 423 | 424 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 425 | # 426 | # epub_show_urls = 'inline' 427 | 428 | # If false, no index is generated. 429 | # 430 | # epub_use_index = True 431 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. aschedule documentation master file, created by 2 | sphinx-quickstart on Sat Sep 3 21:51:06 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | aschedule - Async Schedule 7 | ========================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | intro 15 | tutorial 16 | 17 | Aschedule API 18 | ------------- 19 | 20 | .. automodule:: aschedule 21 | :members: 22 | 23 | JobSchedule 24 | ----------- 25 | 26 | .. autoclass:: aschedule.api.JobSchedule 27 | :members: 28 | 29 | ScheduleManager 30 | --------------- 31 | 32 | .. autoclass:: aschedule.api.ScheduleManager 33 | :members: 34 | 35 | 36 | 37 | Indices and tables 38 | ================== 39 | 40 | * :ref:`genindex` 41 | * :ref:`modindex` 42 | * :ref:`search` 43 | 44 | -------------------------------------------------------------------------------- /docs/source/intro.rst: -------------------------------------------------------------------------------- 1 | .. intro.rst 2 | 3 | Intro 4 | ===== 5 | 6 | -------------------------------------------------------------------------------- /docs/source/tutorial.rst: -------------------------------------------------------------------------------- 1 | .. tutorial.rst 2 | 3 | Tutorial 4 | ======== 5 | 6 | -------------------------------------------------------------------------------- /examples/repeated_job.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import asyncio 4 | import datetime 5 | import aschedule 6 | 7 | 8 | def main(): 9 | async def job1(): 10 | job1.counter += 1 11 | if job1.counter == 10: 12 | future1.cancel() 13 | return 14 | print('job1:', job1.counter, datetime.datetime.now()) 15 | 16 | async def job2(): 17 | job2.counter += 1 18 | if job2.counter == 10: 19 | future2.cancel() 20 | return 21 | print('job2:', job2.counter, datetime.datetime.now()) 22 | 23 | job1.counter = 0 24 | job2.counter = -10 25 | loop = asyncio.get_event_loop() 26 | future1 = aschedule.every(job1, seconds=1) 27 | future2 = aschedule.every(job2, seconds=1) 28 | loop.run_until_complete(asyncio.gather(future1, future2, return_exceptions=True)) 29 | 30 | if __name__ == '__main__': 31 | main() 32 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Srinivas Devaki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # aschedule 2 | 3 | [![build status][2]][1] [![coverage][3]][1] [![codacy][5]][4] [![mitlicence][7]][6] 4 | 5 | Python asyncio Job Scheduling. 6 | 7 | aschedule (async schedule) allows you to schedule python co-routines periodically or 8 | at a specific time. 9 | 10 | ### Install 11 | ```bash 12 | pip install aschedule==0.0.2.dev0 13 | ``` 14 | 15 | ### Quick Start 16 | 17 | ###### Print most cpu consumption percentage every second 18 | ```python 19 | import asyncio, psutil 20 | import aschedule 21 | 22 | async def print_cpu(): 23 | print('report', round(loop.time()), ':', psutil.cpu_percent()) 24 | 25 | schedule = aschedule.every(print_cpu, seconds=1) 26 | loop = asyncio.get_event_loop() 27 | loop.run_until_complete(schedule.future) 28 | ``` 29 | 30 | ###### Send a message to developer's telegram account about website's analytics 31 | 32 | ```python 33 | BOT_TOKEN, CHAT_ID = '123456789:ABC_DEFGHJsfafs-fasdfs32sdfs7sEW', 124254321 34 | import aiohttp, json 35 | import aschedule 36 | 37 | URL = 'https://api.telegram.org/bot{}/sendmessage'.format(BOT_TOKEN) 38 | number_of_requests = 0 39 | 40 | async def send_stats(): 41 | payload = { 42 | 'chat_id': CHAT_ID, 43 | 'text': str(number_of_requests), 44 | } 45 | async with aiohttp.ClientSession() as session: 46 | async with session.get(URL, params=payload) as response: 47 | print(await response.text()) # log the reponse 48 | 49 | async def handle(request): 50 | global number_of_requests 51 | number_of_requests += 1 52 | text = "Hello, " + request.match_info.get('name', "Anonymous") 53 | return aiohttp.web.Response(body=text.encode('utf-8')) 54 | 55 | app = aiohttp.web.Application() 56 | app.router.add_route('GET', '/{name}', handle) 57 | 58 | aschedule.every(send_stats, seconds=30) # send stats to the developer every 30 seconds 59 | 60 | aiohttp.web.run_app(app) 61 | ``` 62 | 63 | ###### Send an email to the users everyday 64 | 65 | ```python 66 | import asyncio 67 | import aschedule 68 | 69 | async def send_email_to_users(): 70 | # send an email to the users 71 | pass 72 | 73 | schedule = aschedule.every(send_email_to_users, days=1) 74 | asyncio.get_event_loop().run_until_complete(schedule.future) 75 | ``` 76 | 77 | ### Testing 78 | 79 | ```bash 80 | nosetests --with-coverage --cover-package=aschedule --cover-min-percentage=90 --cover-config-file=.coveragerc --processes=50 --process-timeout=600 --cover-inclusive 81 | ``` 82 | 83 | [1]: https://gitlab.com/eightnoteight/aschedule/builds 84 | [2]: https://gitlab.com/eightnoteight/aschedule/badges/master/build.svg 85 | [3]: https://gitlab.com/eightnoteight/aschedule/badges/master/coverage.svg 86 | [4]: https://www.codacy.com/app/eightnoteight/aschedule 87 | [5]: https://api.codacy.com/project/badge/Grade/d505fa35e59a4c52937302fc63190487 88 | [6]: https://gitlab.com/eightnoteight/aschedule/blob/master/license 89 | [7]: https://img.shields.io/badge/license-MIT-blue.svg 90 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | # nose pkg from github master as latest pypi version doesn't have --cover-config-file option 3 | # and the last release is in some 2015 june 4 | https://github.com/nose-devs/nose/archive/master.zip 5 | coverage 6 | sphinx 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = readme.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | from setuptools import setup 3 | except ImportError: 4 | from distutils.core import setup 5 | 6 | version = '0.0.2.dev0' 7 | packages = ['aschedule'] 8 | install_requires = [ 9 | ] 10 | classifiers = [ 11 | 'Development Status :: 2 - Pre-Alpha', 12 | 'Intended Audience :: Developers', 13 | 'License :: OSI Approved :: MIT License', 14 | 'Natural Language :: English', 15 | 'Programming Language :: Python :: 3.5', 16 | 'Programming Language :: Python :: Implementation :: CPython', 17 | 'Topic :: Software Development :: Libraries :: Python Modules', 18 | 'Topic :: Utilities' 19 | ] 20 | 21 | config = { 22 | 'description': 'schedule your co-routines', 23 | 'author': 'Srinivas Devaki', 24 | 'url': 'https://gitlab.com/eightnoteight/aschedule', 25 | 'download_url': 'https://gitlab.com/eightnoteight/aschedule/repository/archive.zip?ref=master', 26 | 'author_email': 'mr.eightnoteight@gmail.com', 27 | 'version': version, 28 | 'install_requires': install_requires, 29 | 'packages': packages, 30 | 'scripts': [], 31 | 'classifiers': classifiers, 32 | 'name': 'aschedule' 33 | } 34 | 35 | setup(**config) 36 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eightnoteight/aschedule/f2e201a425f8b214bd76fc4eb715f1c4632d37e4/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_cancel.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from datetime import timedelta, datetime 4 | import unittest 5 | import asyncio 6 | 7 | import aschedule 8 | 9 | 10 | class TestCancel(unittest.TestCase): 11 | _multiprocess_shared_ = True 12 | 13 | def setUp(self): 14 | self.loop = asyncio.get_event_loop() 15 | self.count = 0 16 | self.schedule = None 17 | self.schedule_manager = None 18 | 19 | async def sample_job1(self): 20 | await asyncio.sleep(10) 21 | self.count += 1 22 | aschedule.cancel(self.schedule, running_jobs=True) 23 | 24 | async def sample_job2(self): 25 | await asyncio.sleep(10) 26 | self.count += 1 27 | try: 28 | aschedule.cancel(self.schedule, running_jobs=False) 29 | except aschedule.AScheduleException: 30 | pass 31 | 32 | def test_cancel_by_sleep_including_jobs(self): 33 | self.schedule = aschedule.every(self.sample_job1, seconds=2) 34 | self.loop.run_until_complete(asyncio.sleep(15)) 35 | 36 | # just to makes sure that the jobs are indeed cancelled. 37 | self.loop.run_until_complete(asyncio.sleep(20)) 38 | self.assertEqual(1, self.count, "more than 1 job got executed") 39 | 40 | def test_cancel_including_jobs(self): 41 | self.cancel_running_jobs = True 42 | self.schedule = aschedule.every(self.sample_job1, seconds=2) 43 | try: 44 | self.loop.run_until_complete(self.schedule.future) 45 | except asyncio.CancelledError: 46 | pass 47 | 48 | # just to makes sure that the jobs are indeed cancelled. 49 | self.loop.run_until_complete(asyncio.sleep(20)) 50 | self.assertEqual(1, self.count, "more than 1 job got executed") 51 | 52 | def test_cancel_just_schedule(self): 53 | self.cancel_running_jobs = False 54 | self.schedule = aschedule.every(self.sample_job2, seconds=2) 55 | try: 56 | self.loop.run_until_complete(self.schedule.future) 57 | except asyncio.CancelledError: 58 | pass 59 | 60 | self.loop.run_until_complete(asyncio.sleep(20)) 61 | self.assertEqual(5, self.count) 62 | 63 | async def _dummy_job(self): 64 | self.schedule_manager.cancel(self.schedule, running_jobs=True) 65 | 66 | def test_unknown_cancel(self): 67 | self.schedule_manager = aschedule.ScheduleManager() 68 | self.schedule = self.schedule_manager.every(self._dummy_job, timedelta(seconds=1), datetime.now()) 69 | with self.assertRaises(aschedule.AScheduleException): 70 | aschedule.cancel(self.schedule) 71 | with self.assertRaises(asyncio.CancelledError): 72 | self.loop.run_until_complete(self.schedule.future) 73 | 74 | 75 | if __name__ == '__main__': 76 | unittest.main() 77 | -------------------------------------------------------------------------------- /tests/test_every.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | import asyncio 5 | import datetime 6 | 7 | import aschedule 8 | 9 | 10 | class TestEveryFunction(unittest.TestCase): 11 | _multiprocess_shared_ = True 12 | 13 | def setUp(self): 14 | self.loop = asyncio.get_event_loop() 15 | self.schedule = None 16 | self.count = 0 17 | self.count_max = 5 18 | self.interval_in_seconds = 2 19 | 20 | async def sample_job(self): 21 | self.count += 1 22 | if self.count == self.count_max: 23 | self.schedule._cancel(running_jobs=True) 24 | 25 | def test_seconds(self): 26 | self.schedule = aschedule.every(self.sample_job, 27 | seconds=self.interval_in_seconds) 28 | start_time = self.loop.time() 29 | # error if: the future doesn't exit or produces other than CancelledError 30 | with self.assertRaises(asyncio.CancelledError): 31 | self.loop.run_until_complete(future=self.schedule.future) 32 | end_time = self.loop.time() 33 | 34 | self.assertAlmostEqual(start_time + self.interval_in_seconds * (self.count_max - 1), 35 | end_time, places=0) 36 | 37 | def test_start_at_now(self): 38 | self.schedule = aschedule.every(self.sample_job, 39 | seconds=self.interval_in_seconds, 40 | start_at=datetime.datetime.now()) 41 | start_time = self.loop.time() 42 | # error if: the future doesn't exit or produces other than CancelledError 43 | with self.assertRaises(asyncio.CancelledError): 44 | self.loop.run_until_complete(future=self.schedule.future) 45 | end_time = self.loop.time() 46 | 47 | # error if: given start_at, the job doesn't execute 5 times within 8 seconds. 48 | self.assertAlmostEqual(start_time + 49 | self.interval_in_seconds * (self.count_max - 1), 50 | end_time, places=0) 51 | 52 | # should behave the same as test_start_at 53 | def test_start_at_after(self): 54 | after_in_seconds = 10 55 | start_at = datetime.datetime.now() + datetime.timedelta(seconds=after_in_seconds) 56 | 57 | self.schedule = aschedule.every(self.sample_job, 58 | seconds=self.interval_in_seconds, 59 | start_at=start_at) 60 | start_time = self.loop.time() 61 | # error if: the future doesn't exit or produces other than CancelledError 62 | with self.assertRaises(asyncio.CancelledError): 63 | self.loop.run_until_complete(future=self.schedule.future) 64 | end_time = self.loop.time() 65 | 66 | # error if: given start_at, the job doesn't execute 5 times within 8 seconds. 67 | expected_end_time = (start_time + 68 | self.interval_in_seconds * (self.count_max - 1) + 69 | after_in_seconds) 70 | self.assertAlmostEqual(expected_end_time, 71 | end_time, places=0) 72 | 73 | def test_timedelta(self): 74 | self.schedule = aschedule.every(self.sample_job, 75 | timedelta=datetime.timedelta( 76 | seconds=self.interval_in_seconds)) 77 | start_time = self.loop.time() 78 | # error if: the future doesn't exit or produces other than CancelledError 79 | with self.assertRaises(asyncio.CancelledError): 80 | self.loop.run_until_complete(future=self.schedule.future) 81 | end_time = self.loop.time() 82 | 83 | self.assertAlmostEqual(start_time + self.interval_in_seconds * (self.count_max - 1), 84 | end_time, places=0) 85 | 86 | def test_bad_options(self): 87 | with self.assertRaises(aschedule.api.AScheduleException): 88 | aschedule.every(self.sample_job, timedelta=datetime.timedelta(seconds=0)) 89 | with self.assertRaises(aschedule.api.AScheduleException): 90 | aschedule.every(self.sample_job, timedelta=datetime.timedelta(minutes=-10)) 91 | 92 | def test_loop(self): 93 | asyncio.set_event_loop(None) 94 | self.schedule = aschedule.every(self.sample_job, 95 | seconds=self.interval_in_seconds, loop=self.loop) 96 | start_time = self.loop.time() 97 | # error if: the future doesn't exit or produces other than CancelledError 98 | with self.assertRaises(asyncio.CancelledError): 99 | self.loop.run_until_complete(future=self.schedule.future) 100 | end_time = self.loop.time() 101 | 102 | self.assertAlmostEqual(start_time + self.interval_in_seconds * (self.count_max - 1), 103 | end_time, places=0) 104 | 105 | asyncio.set_event_loop(self.loop) 106 | 107 | def tearDown(self): 108 | self.loop = None 109 | -------------------------------------------------------------------------------- /tests/test_once_at.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | import asyncio 5 | from datetime import datetime, timedelta 6 | 7 | import aschedule 8 | 9 | 10 | class TestOnceAt(unittest.TestCase): 11 | _multiprocess_shared_ = True 12 | 13 | def setUp(self): 14 | self.loop = asyncio.get_event_loop() 15 | self.future = None 16 | self.trigger_time = 0 17 | self.count = 0 18 | self.schedule = None 19 | 20 | async def sample_job(self): 21 | self.trigger_time = self.loop.time() 22 | if self.count >= 1: 23 | self.schedule._cancel(running_jobs=True) 24 | self.count += 1 25 | 26 | def _test_util(self, td, delay): 27 | scheduled_time = self.loop.time() 28 | self.schedule = aschedule.once_at(self.sample_job, 29 | datetime.now() + td) 30 | self.loop.run_until_complete(self.schedule.future) 31 | expected_time = scheduled_time + delay 32 | self.assertAlmostEqual(expected_time, self.trigger_time, places=0) 33 | 34 | def test_once_at_after(self): 35 | self._test_util(timedelta(seconds=10), 10) 36 | 37 | def test_once_at_now(self): 38 | self._test_util(timedelta(seconds=0), 0) 39 | 40 | def test_once_at_before(self): 41 | self._test_util(timedelta(minutes=-10), 0) 42 | 43 | def tearDown(self): 44 | self.loop = None 45 | self.trigger_time = 0 46 | self.count = 0 47 | -------------------------------------------------------------------------------- /tests/test_schedule_manager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from datetime import timedelta, datetime 4 | from functools import partial 5 | import asyncio 6 | import unittest 7 | 8 | import aschedule 9 | 10 | 11 | class TestScheduleManager(unittest.TestCase): 12 | _multiprocess_shared_ = True 13 | 14 | def setUp(self): 15 | self.loop = asyncio.get_event_loop() 16 | self.count = 0 17 | self.schedule = None 18 | self.schedule_manager = aschedule.ScheduleManager() 19 | self.cancel_in_seconds = 10 20 | 21 | async def schedule_canceller(self): 22 | await asyncio.sleep(self.cancel_in_seconds) 23 | self.schedule_manager.cancel(self.schedule, running_jobs=True) 24 | 25 | async def get_coroutine(self): 26 | self.count += 1 27 | 28 | async def get_future(self): 29 | async def task(_future): 30 | self.count += 1 31 | _future.set_result(None) 32 | future = asyncio.Future() 33 | asyncio.ensure_future(task(future), loop=self.loop) 34 | return future 35 | 36 | def test_every_param_coroutine(self): 37 | # scheduled executions 1, 3, 5, 7, 9 38 | self.schedule = self.schedule_manager.every(self.get_coroutine, 39 | timedelta(seconds=2), 40 | datetime.now() + timedelta(seconds=1)) 41 | # will be cancelled at 42 | self.cancel_in_seconds = 10 43 | try: 44 | self.loop.run_until_complete( 45 | asyncio.gather(self.schedule_canceller(), self.schedule.future)) 46 | except asyncio.CancelledError: 47 | pass 48 | # making sure that all running jobs and the schedule are cancelled 49 | self.loop.run_until_complete(asyncio.sleep(10)) 50 | self.assertEqual(5, self.count) 51 | 52 | def test_every_param_future(self): 53 | # scheduled executions 1, 3, 5, 7, 9 54 | self.schedule = self.schedule_manager.every(self.get_future, 55 | timedelta(seconds=2), 56 | datetime.now() + timedelta(seconds=1)) 57 | # will be cancelled at 58 | self.cancel_in_seconds = 10 59 | try: 60 | self.loop.run_until_complete( 61 | asyncio.gather(self.schedule_canceller(), self.schedule.future)) 62 | except asyncio.CancelledError: 63 | pass 64 | # making sure that all running jobs and the schedules are cancelled 65 | self.loop.run_until_complete(asyncio.sleep(10)) 66 | self.assertEqual(5, self.count) 67 | 68 | def test_every_param_interval(self): 69 | # scheduled executions for schedule1, 0, 3, 6, 9 70 | # scheduled executions for schedule1, 0, 4, 8 71 | times1, times2 = [], [] 72 | cancel_in_seconds = 10 73 | start = self.loop.time() 74 | 75 | async def record_times(times): 76 | times.append(round(self.loop.time() - start)) 77 | 78 | async def cancel_schedules(): 79 | await asyncio.sleep(cancel_in_seconds) 80 | self.schedule_manager.cancel(schedule1, running_jobs=True) 81 | self.schedule_manager.cancel(schedule2, running_jobs=True) 82 | 83 | schedule1 = self.schedule_manager.every(partial(record_times, times1), 84 | timedelta(seconds=3), 85 | datetime.now()) 86 | schedule2 = self.schedule_manager.every(partial(record_times, times2), 87 | timedelta(seconds=4), 88 | datetime.now()) 89 | try: 90 | self.loop.run_until_complete( 91 | asyncio.gather(schedule1.future, schedule2.future, cancel_schedules())) 92 | except asyncio.CancelledError: 93 | pass 94 | # making sure that all running jobs and the schedules are cancelled 95 | self.loop.run_until_complete(asyncio.sleep(10)) 96 | 97 | self.assertEqual([0, 3, 6, 9], times1) 98 | self.assertEqual([0, 4, 8], times2) 99 | 100 | with self.assertRaises(aschedule.AScheduleException): 101 | self.schedule_manager.every(self.get_coroutine, 102 | timedelta(seconds=-2), 103 | datetime.now()) 104 | 105 | def test_every_param_start_at(self): 106 | # scheduled executions 2, 4, 6, 8; 1, 4, 7 107 | times1 = [] 108 | times2 = [] 109 | start = self.loop.time() 110 | cancel_in_seconds = 9 111 | 112 | async def record_times(times): 113 | times.append(round(self.loop.time() - start)) 114 | 115 | async def cancel_schedule(): 116 | await asyncio.sleep(cancel_in_seconds) 117 | self.schedule_manager.cancel(schedule1, running_jobs=True) 118 | self.schedule_manager.cancel(schedule2, running_jobs=True) 119 | 120 | schedule1 = self.schedule_manager.every(partial(record_times, times1), 121 | interval=timedelta(seconds=2), 122 | start_at=datetime.now() + timedelta(seconds=2)) 123 | schedule2 = self.schedule_manager.every(partial(record_times, times2), 124 | interval=timedelta(seconds=3), 125 | start_at=datetime.now() + timedelta(seconds=-2)) 126 | try: 127 | self.loop.run_until_complete( 128 | asyncio.gather(schedule2.future, schedule1.future, cancel_schedule())) 129 | except asyncio.CancelledError: 130 | pass 131 | # making sure that all running jobs and the schedules are cancelled 132 | self.loop.run_until_complete(asyncio.sleep(10)) 133 | 134 | self.assertEqual([2, 4, 6, 8], times1) 135 | self.assertEqual([1, 4, 7], times2) 136 | 137 | def test_every_param_count(self): 138 | # scheduled executions 0, 2, 4, 6, 8; without cancelling the future 139 | times = [] 140 | start = self.loop.time() 141 | cancel_in_seconds = 16 142 | 143 | async def record_times(): 144 | times.append(round(self.loop.time() - start)) 145 | 146 | async def cancel_schedule(): 147 | await asyncio.sleep(cancel_in_seconds) 148 | # should report an error that the given schedule doesn't belong to this schedule manager 149 | # as the schedule is completed before this arises 150 | with self.assertRaises(aschedule.AScheduleException): 151 | self.schedule_manager.cancel(schedule, running_jobs=True) 152 | 153 | schedule = self.schedule_manager.every(record_times, interval=timedelta(seconds=2), 154 | start_at=datetime.now(), count=5) 155 | 156 | self.loop.run_until_complete( 157 | asyncio.gather(schedule.future, cancel_schedule())) 158 | # making sure that all running jobs and the schedules are cancelled 159 | self.loop.run_until_complete(asyncio.sleep(10)) 160 | 161 | self.assertEqual([0, 2, 4, 6, 8], times) 162 | with self.assertRaises(aschedule.AScheduleException): 163 | self.schedule_manager.every(self.get_coroutine, 164 | timedelta(seconds=2), 165 | datetime.now(), count=0) 166 | with self.assertRaises(aschedule.AScheduleException): 167 | self.schedule_manager.every(self.get_coroutine, 168 | timedelta(seconds=2), 169 | datetime.now(), count=-1) 170 | 171 | def test_every_param_loop(self): 172 | asyncio.set_event_loop(None) 173 | # scheduled executions 1, 3, 5, 7, 9 174 | schedule = self.schedule_manager.every(self.get_coroutine, 175 | timedelta(seconds=2), 176 | datetime.now() + timedelta(seconds=1), 177 | loop=self.loop) 178 | # will be cancelled at 179 | cancel_in_seconds = 10 180 | 181 | async def cancel_schedule(): 182 | await asyncio.sleep(cancel_in_seconds, loop=self.loop) 183 | self.schedule_manager.cancel(schedule, running_jobs=True) 184 | 185 | try: 186 | self.loop.run_until_complete( 187 | asyncio.gather(cancel_schedule(), schedule.future, loop=self.loop)) 188 | except asyncio.CancelledError: 189 | pass 190 | 191 | # making sure that all running jobs and the schedule are cancelled 192 | self.loop.run_until_complete(asyncio.sleep(10, loop=self.loop)) 193 | self.assertEqual(5, self.count) 194 | asyncio.set_event_loop(self.loop) 195 | 196 | def test_once_at_param_coroutine(self): 197 | start = self.loop.time() 198 | times = [] 199 | cancel_in_seconds = 10 200 | async def record_times(): 201 | times.append(round(self.loop.time() - start)) 202 | 203 | async def cancel_schedule(): 204 | await asyncio.sleep(cancel_in_seconds) 205 | # should report an error that the given schedule doesn't belong to this schedule manager 206 | # as the schedule is completed before this arises 207 | with self.assertRaises(aschedule.AScheduleException): 208 | self.schedule_manager.cancel(schedule, running_jobs=True) 209 | 210 | schedule = self.schedule_manager.once_at(record_times, 211 | datetime.now() + timedelta(seconds=5)) 212 | self.loop.run_until_complete( 213 | asyncio.gather(cancel_schedule(), schedule.future)) 214 | 215 | # making sure that all running jobs and the schedule are cancelled 216 | self.loop.run_until_complete(asyncio.sleep(10)) 217 | self.assertEqual([5], times) 218 | 219 | def test_once_at_param_future(self): 220 | start = self.loop.time() 221 | cancel_in_seconds = 10 222 | times = [] 223 | 224 | def record_times(): 225 | async def task(_future1): 226 | times.append(round(self.loop.time() - start)) 227 | _future1.set_result(None) 228 | _future = asyncio.Future() 229 | asyncio.ensure_future(task(_future)) 230 | return _future 231 | 232 | async def cancel_schedule(): 233 | await asyncio.sleep(cancel_in_seconds) 234 | # should report an error that the given schedule doesn't belong to this schedule manager 235 | # as the schedule is completed before this arises 236 | with self.assertRaises(aschedule.AScheduleException): 237 | self.schedule_manager.cancel(schedule, running_jobs=True) 238 | 239 | schedule = self.schedule_manager.once_at(record_times, 240 | datetime.now() + timedelta(seconds=5)) 241 | self.loop.run_until_complete( 242 | asyncio.gather(cancel_schedule(), schedule.future)) 243 | 244 | # making sure that all running jobs and the schedule are cancelled 245 | self.loop.run_until_complete(asyncio.sleep(10)) 246 | self.assertEqual([5], times) 247 | 248 | def test_once_at_param_run_at(self): 249 | # scheduled executions 5, 7, 3 250 | start = self.loop.time() 251 | times = [] 252 | cancel_in_seconds = 10 253 | async def record_times(): 254 | times.append(round(self.loop.time() - start)) 255 | 256 | async def cancel_schedule(): 257 | await asyncio.sleep(cancel_in_seconds) 258 | # should report an error that the given schedule doesn't belong to this schedule manager 259 | # as the schedule is completed before this arises 260 | with self.assertRaises(aschedule.AScheduleException): 261 | self.schedule_manager.cancel(schedule1, running_jobs=True) 262 | with self.assertRaises(aschedule.AScheduleException): 263 | self.schedule_manager.cancel(schedule2, running_jobs=True) 264 | with self.assertRaises(aschedule.AScheduleException): 265 | self.schedule_manager.cancel(schedule3, running_jobs=True) 266 | 267 | schedule1 = self.schedule_manager.once_at(record_times, 268 | datetime.now() + timedelta(seconds=5)) 269 | schedule2 = self.schedule_manager.once_at(record_times, 270 | datetime.now() + timedelta(seconds=7)) 271 | schedule3 = self.schedule_manager.once_at(record_times, 272 | datetime.now() + timedelta(seconds=3)) 273 | self.loop.run_until_complete( 274 | asyncio.gather(cancel_schedule(), schedule1.future, 275 | schedule2.future, schedule3.future)) 276 | 277 | # making sure that all running jobs and the schedule are cancelled 278 | self.loop.run_until_complete(asyncio.sleep(10)) 279 | self.assertEqual([3, 5, 7], times) 280 | 281 | def test_once_at_param_strict(self): 282 | # scheduled executions 0 283 | start = self.loop.time() 284 | times = [] 285 | cancel_in_seconds = 2 286 | async def record_times(): 287 | times.append(round(self.loop.time() - start)) 288 | 289 | async def cancel_schedule(): 290 | await asyncio.sleep(cancel_in_seconds) 291 | # should report an error that the given schedule doesn't belong to this schedule manager 292 | # as the schedule is completed before this arises 293 | with self.assertRaises(aschedule.AScheduleException): 294 | self.schedule_manager.cancel(schedule, running_jobs=True) 295 | 296 | with self.assertRaises(aschedule.AScheduleException): 297 | self.schedule_manager.once_at(record_times, 298 | datetime.now() + timedelta(seconds=-10), 299 | strict=True) 300 | 301 | schedule = self.schedule_manager.once_at(record_times, 302 | datetime.now() + timedelta(seconds=-10), 303 | strict=False) 304 | self.loop.run_until_complete(asyncio.gather(cancel_schedule(), schedule.future)) 305 | 306 | # making sure that all running jobs and the schedule are cancelled 307 | self.loop.run_until_complete(asyncio.sleep(10)) 308 | self.assertEqual([0], times) 309 | 310 | def test_once_at_param_loop(self): 311 | asyncio.set_event_loop(None) 312 | start = self.loop.time() 313 | times = [] 314 | cancel_in_seconds = 10 315 | async def record_times(): 316 | times.append(round(self.loop.time() - start)) 317 | 318 | async def cancel_schedule(): 319 | await asyncio.sleep(cancel_in_seconds, loop=self.loop) 320 | # should report an error that the given schedule doesn't belong to this schedule manager 321 | # as the schedule is completed before this arises 322 | with self.assertRaises(aschedule.AScheduleException): 323 | self.schedule_manager.cancel(schedule, running_jobs=True) 324 | 325 | schedule = self.schedule_manager.once_at(record_times, 326 | datetime.now() + timedelta(seconds=5), 327 | loop=self.loop) 328 | self.loop.run_until_complete( 329 | asyncio.gather(cancel_schedule(), schedule.future, loop=self.loop)) 330 | 331 | # making sure that all running jobs and the schedule are cancelled 332 | self.loop.run_until_complete(asyncio.sleep(10, loop=self.loop)) 333 | self.assertEqual([5], times) 334 | asyncio.set_event_loop(self.loop) 335 | 336 | def test_cancel(self): 337 | start = self.loop.time() 338 | times = [] 339 | cancel_in_seconds1 = 5 340 | cancel_in_seconds2 = 5 341 | 342 | async def record_times(): 343 | await asyncio.sleep(4) 344 | times.append(round(self.loop.time() - start)) 345 | 346 | async def cancel_schedule1(): 347 | await asyncio.sleep(cancel_in_seconds1) 348 | # should report an error that the given schedule doesn't belong to this 349 | # schedule manager as the schedule is created by a new schedule manager instance 350 | with self.assertRaises(aschedule.AScheduleException): 351 | self.schedule_manager.cancel(schedule1, running_jobs=True) 352 | schedule1._cancel(running_jobs=True) 353 | 354 | async def cancel_schedule2(): 355 | await asyncio.sleep(cancel_in_seconds2) 356 | # should report an error that the given schedule doesn't belong to this 357 | # schedule manager as the schedule is created by a new schedule manager instance 358 | self.schedule_manager.cancel(schedule2, running_jobs=True) 359 | 360 | schedule1 = aschedule.ScheduleManager().every(record_times, 361 | timedelta(seconds=2), datetime.now()) 362 | schedule2 = self.schedule_manager.every(record_times, 363 | timedelta(seconds=3), datetime.now()) 364 | # cancellation of schedule1 will give out CancelledError 365 | with self.assertRaises(asyncio.CancelledError): 366 | self.loop.run_until_complete( 367 | asyncio.gather(cancel_schedule1(), cancel_schedule2(), 368 | schedule1.future, schedule2.future)) 369 | # making sure that all running jobs and the schedule are cancelled 370 | self.loop.run_until_complete(asyncio.sleep(10)) 371 | 372 | self.assertEqual([4, 4], times) 373 | 374 | def test_shutdown(self): 375 | start = self.loop.time() 376 | times = [] 377 | 378 | async def record_times(): 379 | times.append(round(self.loop.time() - start)) 380 | 381 | schedules = [ 382 | self.schedule_manager.every(record_times, 383 | timedelta(seconds=5), 384 | datetime.now() + timedelta(seconds=x)) 385 | for x in range(0, 5, 2) 386 | ] 387 | schedules.append( 388 | self.schedule_manager.once_at(record_times, 389 | datetime.now() + timedelta(seconds=3)) 390 | ) 391 | async def cancel_schedules(): 392 | await asyncio.sleep(11) 393 | self.schedule_manager.shutdown() 394 | futures = [schedule.future for schedule in schedules] 395 | try: 396 | self.loop.run_until_complete(asyncio.gather(*(futures + [cancel_schedules()]))) 397 | except asyncio.CancelledError: 398 | pass 399 | # making sure that all running jobs and the schedule are cancelled 400 | self.loop.run_until_complete(asyncio.sleep(10)) 401 | self.assertEqual([0, 2, 3, 4, 5, 7, 9, 10], times) 402 | 403 | def tearDown(self): 404 | asyncio.set_event_loop(self.loop) 405 | -------------------------------------------------------------------------------- /tests/testing_ext.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from unittest.mock import patch 3 | from datetime import timedelta 4 | import unittest 5 | import asyncio 6 | 7 | import aschedule 8 | 9 | 10 | class TestingExt(unittest.TestCase): 11 | _multiprocess_shared_ = True 12 | 13 | def setUp(self): 14 | self.loop = asyncio.get_event_loop() 15 | 16 | async def get_coro(self): 17 | pass 18 | 19 | def test_every_day(self): 20 | self.every_patcher = patch('aschedule.ext.every') 21 | self.addCleanup(self.every_patcher.stop) 22 | self.every_mock = self.every_patcher.start() 23 | from aschedule.ext import every_day 24 | 25 | schedule1 = every_day(self.get_coro) 26 | self.every_mock.assert_called_with(self.get_coro, timedelta=timedelta(days=1), loop=None) 27 | schedule2 = every_day(self.get_coro, loop=self.loop) 28 | self.every_mock.assert_called_with(self.get_coro, timedelta=timedelta(days=1), loop=self.loop) 29 | self.loop.run_until_complete(asyncio.sleep(1)) 30 | schedule1._cancel(running_jobs=True) 31 | schedule2._cancel(running_jobs=True) 32 | self.loop.run_until_complete(asyncio.sleep(1)) 33 | self.assertEqual(2, self.every_mock.call_count) 34 | 35 | def test_every_week(self): 36 | self.every_patcher = patch('aschedule.ext.every') 37 | self.addCleanup(self.every_patcher.stop) 38 | self.every_mock = self.every_patcher.start() 39 | from aschedule.ext import every_week 40 | 41 | schedule1 = every_week(self.get_coro) 42 | self.every_mock.assert_called_with(self.get_coro, timedelta=timedelta(days=7), loop=None) 43 | schedule2 = every_week(self.get_coro, loop=self.loop) 44 | self.every_mock.assert_called_with(self.get_coro, timedelta=timedelta(days=7), loop=self.loop) 45 | self.loop.run_until_complete(asyncio.sleep(1)) 46 | schedule1._cancel(running_jobs=True) 47 | schedule2._cancel(running_jobs=True) 48 | self.loop.run_until_complete(asyncio.sleep(1)) 49 | self.assertEqual(2, self.every_mock.call_count) 50 | 51 | def test_every_random_interval(self): 52 | from aschedule.ext import every_random_interval 53 | 54 | # scheduled executions: randrange(0, 5), randrange(5, 10), randrange(10, 15) 55 | times = [] 56 | start = self.loop.time() 57 | cancel_in_seconds = 16 58 | # set the seed to avoid a scheduled execution on 16th second. 59 | __import__('random').seed(38042) 60 | async def record_times(): 61 | times.append(round(self.loop.time() - start)) 62 | schedule = every_random_interval(record_times, timedelta(seconds=5)) 63 | 64 | async def schedule_canceller(): 65 | await asyncio.sleep(cancel_in_seconds) 66 | aschedule.cancel(schedule) 67 | 68 | try: 69 | self.loop.run_until_complete( 70 | asyncio.gather(schedule_canceller(), schedule.future)) 71 | except asyncio.CancelledError: 72 | pass 73 | 74 | self.assertEqual(3, len(times)) 75 | for i, x in enumerate(times): 76 | self.assertTrue(i * 5 <= x < (i + 1) * 5) 77 | 78 | def tearDown(self): 79 | asyncio.set_event_loop(self.loop) 80 | --------------------------------------------------------------------------------