Skip to content

Commit deff30f

Browse files
author
Jimmy Bradshaw
committed
initial commit
0 parents  commit deff30f

File tree

6 files changed

+440
-0
lines changed

6 files changed

+440
-0
lines changed

.gitignore

+144
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
.idea/
2+
3+
# Created by .ignore support plugin (hsz.mobi)
4+
### Python template
5+
# Byte-compiled / optimized / DLL files
6+
__pycache__/
7+
*.py[cod]
8+
*$py.class
9+
10+
# C extensions
11+
*.so
12+
13+
# Distribution / packaging
14+
.Python
15+
build/
16+
develop-eggs/
17+
dist/
18+
downloads/
19+
eggs/
20+
.eggs/
21+
lib/
22+
lib64/
23+
parts/
24+
sdist/
25+
var/
26+
wheels/
27+
pip-wheel-metadata/
28+
share/python-wheels/
29+
*.egg-info/
30+
.installed.cfg
31+
*.egg
32+
MANIFEST
33+
34+
# PyInstaller
35+
# Usually these files are written by a python script from a template
36+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
37+
*.manifest
38+
*.spec
39+
40+
# Installer logs
41+
pip-log.txt
42+
pip-delete-this-directory.txt
43+
44+
# Unit test / coverage reports
45+
htmlcov/
46+
.tox/
47+
.nox/
48+
.coverage
49+
.coverage.*
50+
.cache
51+
nosetests.xml
52+
coverage.xml
53+
*.cover
54+
*.py,cover
55+
.hypothesis/
56+
.pytest_cache/
57+
cover/
58+
59+
# Translations
60+
*.mo
61+
*.pot
62+
63+
# Django stuff:
64+
*.log
65+
local_settings.py
66+
db.sqlite3
67+
db.sqlite3-journal
68+
69+
# Flask stuff:
70+
instance/
71+
.webassets-cache
72+
73+
# Scrapy stuff:
74+
.scrapy
75+
76+
# Sphinx documentation
77+
docs/_build/
78+
79+
# PyBuilder
80+
.pybuilder/
81+
target/
82+
83+
# Jupyter Notebook
84+
.ipynb_checkpoints
85+
86+
# IPython
87+
profile_default/
88+
ipython_config.py
89+
90+
# pyenv
91+
# For a library or package, you might want to ignore these files since the code is
92+
# intended to run in multiple environments; otherwise, check them in:
93+
# .python-version
94+
95+
# pipenv
96+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
97+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
98+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
99+
# install all needed dependencies.
100+
#Pipfile.lock
101+
102+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
103+
__pypackages__/
104+
105+
# Celery stuff
106+
celerybeat-schedule
107+
celerybeat.pid
108+
109+
# SageMath parsed files
110+
*.sage.py
111+
112+
# Environments
113+
.env
114+
.venv
115+
env/
116+
venv/
117+
ENV/
118+
env.bak/
119+
venv.bak/
120+
121+
# Spyder project settings
122+
.spyderproject
123+
.spyproject
124+
125+
# Rope project settings
126+
.ropeproject
127+
128+
# mkdocs documentation
129+
/site
130+
131+
# mypy
132+
.mypy_cache/
133+
.dmypy.json
134+
dmypy.json
135+
136+
# Pyre type checker
137+
.pyre/
138+
139+
# pytype static type analyzer
140+
.pytype/
141+
142+
# Cython debug symbols
143+
cython_debug/
144+

README.md

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Nameko Cron
2+
3+
Nameko `Cron` entrypoint fires based on a cron expression. It is not cluster-aware and
4+
will fire on all service instances. The cron schedule is based on [croniter](http://github.com/kiorky/croniter).
5+
6+
## Usage
7+
8+
```python
9+
from nameko_cron import cron
10+
11+
12+
class Service:
13+
name ="service"
14+
15+
@cron('*/5 * * * *')
16+
def ping(self):
17+
# executes every 5 minutes
18+
print("pong")
19+
```
20+
21+
timezone-aware cron schedules are also available
22+
23+
```python
24+
from nameko_cron import cron
25+
26+
27+
class Service:
28+
name ="service"
29+
30+
@cron('0 12 * * *', tz='America/Chicago')
31+
def ping(self):
32+
# executes every day at noon America/Chicago time
33+
print("pong")
34+
```

nameko_cron/__init__.py

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import datetime
2+
import time
3+
from logging import getLogger
4+
5+
from croniter import croniter
6+
from eventlet import Timeout
7+
from eventlet.event import Event
8+
import pytz
9+
10+
from nameko.extensions import Entrypoint
11+
12+
13+
_log = getLogger(__name__)
14+
15+
16+
class Cron(Entrypoint):
17+
def __init__(self, schedule, tz=None, **kwargs):
18+
"""
19+
Cron entrypoint. Fires according to a (possibly timezone-aware)
20+
cron schedule. If no timezone info is passed, the default is UTC.
21+
22+
Example::
23+
24+
class Service(object):
25+
name = "service"
26+
27+
@cron(schedule='0 12 * * *', tz='America/Chicago')
28+
def ping(self):
29+
# method executes every day at noon America/Chicago time
30+
print("pong")
31+
32+
"""
33+
self.schedule = schedule
34+
self.tz = tz
35+
self.should_stop = Event()
36+
self.worker_complete = Event()
37+
self.gt = None
38+
super().__init__(**kwargs)
39+
40+
def start(self):
41+
_log.debug('starting %s', self)
42+
self.gt = self.container.spawn_managed_thread(self._run)
43+
44+
def stop(self):
45+
_log.debug('stopping %s', self)
46+
self.should_stop.send(True)
47+
self.gt.wait()
48+
49+
def kill(self):
50+
_log.debug('killing %s', self)
51+
self.gt.kill()
52+
53+
def _get_next_interval(self):
54+
now_utc = datetime.datetime.now(tz=pytz.UTC)
55+
if self.tz:
56+
tz = pytz.timezone(self.tz)
57+
base = now_utc.astimezone(tz)
58+
else:
59+
base = now_utc
60+
cron_schedule = croniter(self.schedule, base)
61+
while True:
62+
yield max(cron_schedule.get_next() - time.time(), 0)
63+
64+
def _run(self):
65+
""" Runs the schedule loop. """
66+
interval = self._get_next_interval()
67+
sleep_time = next(interval)
68+
while True:
69+
# sleep for `sleep_time`, unless `should_stop` fires, in which
70+
# case we leave the while loop and stop entirely
71+
with Timeout(sleep_time, exception=False):
72+
self.should_stop.wait()
73+
break
74+
75+
self.handle_timer_tick()
76+
77+
self.worker_complete.wait()
78+
self.worker_complete.reset()
79+
80+
sleep_time = next(interval)
81+
82+
def handle_timer_tick(self):
83+
args = ()
84+
kwargs = {}
85+
86+
# Note that we don't catch ContainerBeingKilled here. If that's raised,
87+
# there is nothing for us to do anyway. The exception bubbles, and is
88+
# caught by :meth:`Container._handle_thread_exited`, though the
89+
# triggered `kill` is a no-op, since the container is already
90+
# `_being_killed`.
91+
self.container.spawn_worker(
92+
self, args, kwargs, handle_result=self.handle_result)
93+
94+
def handle_result(self, worker_ctx, result, exc_info):
95+
self.worker_complete.send()
96+
return result, exc_info
97+
98+
99+
cron = Cron.decorator

setup.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import setuptools
2+
3+
with open("README.md", "r") as fh:
4+
long_description = fh.read()
5+
6+
setuptools.setup(
7+
name="nameko-cron",
8+
version="0.0.1",
9+
author="bradshjg",
10+
author_email="[email protected]",
11+
description="Nameko cron extension",
12+
long_description=long_description,
13+
long_description_content_type="text/markdown",
14+
url="https://github.com/bradshjg/nameko-cron",
15+
packages=setuptools.find_packages(exclude=['tests']),
16+
classifiers=[
17+
"Programming Language :: Python :: 3",
18+
"License :: OSI Approved :: MIT License",
19+
"Operating System :: OS Independent",
20+
],
21+
python_requires='>=3.6',
22+
install_requires=[
23+
'croniter',
24+
'nameko',
25+
'pytz'
26+
]
27+
)

0 commit comments

Comments
 (0)