From fb3850d98d20c3cb9ef963fd114583bba55e179f Mon Sep 17 00:00:00 2001 From: sankalp_j Date: Tue, 18 Sep 2018 15:10:59 +0530 Subject: [PATCH] initial commit --- .gitignore | 1 + LICENSE | 21 ++++++++++++++++++++ README.md | 42 +++++++++++++++++++++++++++++++++++++++ looper/__init__.py | 1 + looper/app.py | 47 ++++++++++++++++++++++++++++++++++++++++++++ looper/exceptions.py | 6 ++++++ looper/helpers.py | 4 ++++ looper/job.py | 26 ++++++++++++++++++++++++ setup.py | 20 +++++++++++++++++++ 9 files changed, 168 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 looper/__init__.py create mode 100644 looper/app.py create mode 100644 looper/exceptions.py create mode 100644 looper/helpers.py create mode 100644 looper/job.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..30302f4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 sankalpjonn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..56af91a --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# Looper +Looper is a service that can be used to run periodic tasks after a certain interval. + +Each job runs on a separate thread and when the service is shut down, it waits till all tasks currently being executed are completed. + +Inspired by this blog [`here`](https://www.g-loaded.eu/2016/11/24/how-to-terminate-running-python-threads-using-signals/) + +## Installation +```sh +python setup.py install +``` + +## writing a job looks like this + +```python +import time + +from looper import Looper +from datetime import timedelta + +loop = Looper() + +@loop.job(interval=timedelta(seconds=2)) +def sample_job_every_2s(): + print "2s job current time : {}".format(time.ctime()) + +@loop.job(interval=timedelta(seconds=5)) +def sample_job_every_5s(): + print "5s job current time : {}".format(time.ctime()) + + +@loop.job(interval=timedelta(seconds=10)) +def sample_job_every_10s(): + print "10s job current time : {}".format(time.ctime()) + +loop.start() +``` + +## Author +* **Sankalp Jonna** + +Email me with any queries: [sankalpjonna@gmail.com](sankalpjonna@gmail.com). diff --git a/looper/__init__.py b/looper/__init__.py new file mode 100644 index 0000000..d9ca9a3 --- /dev/null +++ b/looper/__init__.py @@ -0,0 +1 @@ +from app import Looper diff --git a/looper/app.py b/looper/app.py new file mode 100644 index 0000000..34eebfd --- /dev/null +++ b/looper/app.py @@ -0,0 +1,47 @@ +import logging, sys, signal, time + +from exceptions import ServiceExit +from job import Job +from helpers import service_shutdown + +class Looper(): + def __init__(self): + self.jobs = [] + logger = logging.getLogger('looperr') + ch = logging.StreamHandler(sys.stdout) + ch.setLevel(logging.INFO) + formatter = logging.Formatter('[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s') + ch.setFormatter(formatter) + logger.addHandler(ch) + logger.setLevel(logging.INFO) + self.logger = logger + + def _add_task(self, func, interval, *args, **kwargs): + j = Job(interval, func, *args, **kwargs) + self.jobs.append(j) + + def job(self, interval): + def decorator(f): + self._add_task(f, interval) + return f + return decorator + + def start(self): + try: + signal.signal(signal.SIGTERM, service_shutdown) + signal.signal(signal.SIGINT, service_shutdown) + + self.logger.info("Starting looper..") + for j in self.jobs: + self.logger.info("Registered task {}".format(j.execute)) + j.start() + self.logger.info("Looper now started. Tasks will run based on the interval set") + + # block main thead + while True: + time.sleep(1) + + except ServiceExit: + for j in self.jobs: + self.logger.info("Stopping task {}".format(j.execute)) + j.stop() diff --git a/looper/exceptions.py b/looper/exceptions.py new file mode 100644 index 0000000..4585852 --- /dev/null +++ b/looper/exceptions.py @@ -0,0 +1,6 @@ +class ServiceExit(Exception): + """ + Custom exception which is used to trigger the clean exit + of all running threads and the main program. + """ + pass diff --git a/looper/helpers.py b/looper/helpers.py new file mode 100644 index 0000000..4a3cb36 --- /dev/null +++ b/looper/helpers.py @@ -0,0 +1,4 @@ +from exceptions import ServiceExit + +def service_shutdown(signum, frame): + raise ServiceExit diff --git a/looper/job.py b/looper/job.py new file mode 100644 index 0000000..3e6761d --- /dev/null +++ b/looper/job.py @@ -0,0 +1,26 @@ +from threading import Thread, Event +from datetime import timedelta + +class Job(Thread): + def __init__(self, interval, execute, *args, **kwargs): + Thread.__init__(self) + self.stopped = Event() + self.interval = interval + self.execute = execute + self.args = args + self.kwargs = kwargs + + def stop(self): + self.stopped.set() + self.join() + + def run(self): + while not self.stopped.wait(self.interval.total_seconds()): + self.execute(*self.args, **self.kwargs) + +def sample_func(arg1, arg2, arg3): + print "returning {} {} {}".format(arg1, arg2, arg3) + +if __name__ == "__main__": + j = Job(timedelta(seconds=5), sample_func, **{"arg1": "sankalp", "arg2": "jonna", "arg3": "newsomething"}) + j.start() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ebd46d8 --- /dev/null +++ b/setup.py @@ -0,0 +1,20 @@ +# please install python if it is not present in the system +from setuptools import setup + +with open("README.md", "r") as fh: + long_description = fh.read() + +setup( + name='looper', + version='1.0', + packages=['looper'], + license = 'MIT', + description = 'An elegant way to run period tasks.', + author = 'Sankalp Jonna', + author_email = 'sankalpjonna@gmail.com', + keywords = ['tasks','jobs','periodic task','interval','periodic job', 'flask style', 'decorator'], + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/sankalpjonn/looper", + include_package_data=True, +)