Skip to content

Commit 119aa34

Browse files
committed
Initial commit
1 parent 64e0a2c commit 119aa34

File tree

4 files changed

+213
-2
lines changed

4 files changed

+213
-2
lines changed

Diff for: .gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
logs/
2+
13
# Byte-compiled / optimized / DLL files
24
__pycache__/
35
*.py[cod]

Diff for: README.md

+62-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,62 @@
1-
# Webhooks
2-
Webserver module to listen for webhooks and forward requests to a predefined function.
1+
# Webhook Listener
2+
Very basic webserver module to listen for webhooks and forward requests to predefined functions.
3+
4+
Author: Todd Roberts
5+
6+
https://pypi.org/project/webhook_listener/
7+
8+
https://github.com/toddrob99/Webhooks
9+
10+
## Install
11+
12+
Install from PyPI using pip
13+
14+
`pip install webhook_listener`
15+
16+
## Use
17+
18+
* Define a function to process requests
19+
* `request` parameter will be a cherrypy request object
20+
* `*args` parameter will be a tuple of URL path components
21+
* `**kwargs` parameter will be a dictionary of URL parameters
22+
* Get the body of a `POST` request from `request.body.read` passing the length of `request.headers['Content-Length']`: `request.body.read(int(request.headers['Content-Length'])) if int(request.headers.get('Content-Length',0)) > 0 else ''`
23+
24+
* Include webhook-listener in your project
25+
26+
* Create an instance of the webhook_listener.Listener class
27+
* handlers = Dictionary of functions/callables for each supported HTTP method. (Example: {'POST':process_post_request, 'GET':process_get_request})
28+
* port = Port for the web server to listen on (default: 8090)
29+
* host = Host for the web server to listen on (default: '0.0.0.0')
30+
* threadPool = Number of worker threads for the web server (default: 10)
31+
* logScreen = Setting for cherrypy to log to screen (default: False)
32+
* autoReload = Setting for cherrypy to auto reload when python files are changed (default: False)
33+
34+
* Start the Listener
35+
36+
* Keep your application running so the Listener can run in a separate thread
37+
38+
## Example
39+
40+
import time
41+
import webhook_listener
42+
43+
def process_post_request(request, *args, **kwargs):
44+
print('Received request:\n' +
45+
'Method: {}\n'.format(request.method) +
46+
'Headers: {}\n'.format(request.headers) +
47+
'Args (url path): {}\n'.format(args) +
48+
'Keyword Args (url parameters): {}\n'.format(kwargs) +
49+
'Body: {}'.format(request.body.read(int(request.headers['Content-Length'])) if int(request.headers.get('Content-Length',0)) > 0 else '')
50+
)
51+
52+
# Process the request!
53+
# ...
54+
55+
return
56+
57+
webhooks = webhook_listener.Listener(handlers={'POST':process_post_request})
58+
webhooks.start()
59+
60+
while True:
61+
print('Still alive...')
62+
time.sleep(300)

Diff for: example.py

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/usr/bin/env python
2+
"""
3+
Webhook Listener Example
4+
5+
Author: Todd Roberts
6+
7+
https://github.com/toddrob99/Webhooks
8+
"""
9+
import sys
10+
import os
11+
import logging
12+
import time
13+
14+
import webhook_listener
15+
16+
import argparse
17+
18+
parser = argparse.ArgumentParser(prog='Webhook Listener', description='Start the webhook listener.')
19+
parser.add_argument('--verbose', '-v', action='store_true', dest='verbose', help='Enable debug logging.')
20+
parser.add_argument('--port', '-p', type=int, nargs=1, dest='port', help='Port for the web server to listen on (default: 8090).')
21+
args = parser.parse_args()
22+
port = args.port[0] if args.port and args.port[0]>=0 else 8090
23+
24+
rootLogger = logging.getLogger()
25+
rootLogger.setLevel(logging.DEBUG if args.verbose else logging.INFO)
26+
27+
logger = logging.getLogger('webhooks')
28+
29+
console = logging.StreamHandler(sys.stdout)
30+
formatter = logging.Formatter('%(asctime)s :: %(levelname)8s :: %(module)s(%(lineno)d) :: %(message)s', datefmt='%Y-%m-%d %I:%M:%S %p')
31+
console.setFormatter(formatter)
32+
logger.addHandler(console)
33+
34+
logPath = os.path.join(os.path.dirname(os.path.realpath(__file__)),'logs')
35+
if not os.path.exists(logPath):
36+
os.makedirs(logPath)
37+
38+
file = logging.handlers.TimedRotatingFileHandler(os.path.join(logPath,'webhooks.log'), when='midnight', interval=1, backupCount=7)
39+
file.setFormatter(formatter)
40+
logger.addHandler(file)
41+
logger.debug('Logging started!')
42+
43+
def parse_request(request, *args, **kwargs):
44+
logger.debug('Received request:\n' +
45+
'Method: {}\n'.format(request.method) +
46+
'Headers: {}\n'.format(request.headers) +
47+
'Args (url path): {}\n'.format(args) +
48+
'Keyword Args (url parameters): {}\n'.format(kwargs) +
49+
'Body: {}'.format(request.body.read(int(request.headers['Content-Length'])) if int(request.headers.get('Content-Length',0)) > 0 else '')
50+
)
51+
52+
# Process the request!
53+
# ...
54+
55+
return
56+
57+
webhooks = webhook_listener.Listener(port=port, handlers={'POST':parse_request})
58+
webhooks.start()
59+
60+
while True:
61+
logger.debug('Still alive...')
62+
time.sleep(300)

Diff for: webhook_listener/__init__.py

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/usr/bin/env python
2+
"""
3+
Webhook Listener
4+
5+
Author: Todd Roberts
6+
7+
https://github.com/toddrob99/Webhooks
8+
"""
9+
import os
10+
import sys
11+
import logging, logging.handlers
12+
import cherrypy
13+
import json
14+
import time
15+
import threading
16+
17+
class Listener(object):
18+
def __init__(self, *args, **kwargs):
19+
self.logger = logging.getLogger('webhooks')
20+
self.logger.debug('Logging started!')
21+
self.port = kwargs.get('port', 8090)
22+
self.host = kwargs.get('host', '0.0.0.0')
23+
self.threadPool = kwargs.get('threadPool', 10)
24+
self.logScreen = kwargs.get('logScreen', False)
25+
self.autoReload = kwargs.get('autoReload', False)
26+
self.handlers = kwargs.get('handlers', {})
27+
28+
def start(self):
29+
self.WEBTHREAD = threading.Thread(target=self._startServer, name='webhooks_websever', daemon=True)
30+
self.WEBTHREAD.start()
31+
32+
def stop(self):
33+
cherrypy.engine.stop()
34+
self.WEBTHREAD = None
35+
36+
def _startServer(self):
37+
globalConf = {
38+
'global' : {
39+
'server.socket_host' : self.host,
40+
'server.socket_port' : self.port,
41+
'server.thread_pool' : self.threadPool,
42+
'engine.autoreload.on' : self.autoReload,
43+
'log.screen': self.logScreen
44+
}
45+
}
46+
apiConf = {
47+
'/': {
48+
'request.dispatch': cherrypy.dispatch.Dispatcher(),
49+
'tools.response_headers.on': True,
50+
'tools.response_headers.headers': [('Content-Type', 'text/plain')]
51+
}
52+
}
53+
cherrypy.config.update(globalConf)
54+
cherrypy.tree.mount(WebServer(handlers=self.handlers), '/', config=apiConf)
55+
cherrypy.engine.start()
56+
self.logger.debug('Started web server on {}{}{}...'.format(self.host, ':', self.port))
57+
58+
class WebServer(object):
59+
def __init__(self, *args, **kwargs):
60+
self.logger = logging.getLogger('webhooks')
61+
self.handlers = {}
62+
for m,h in kwargs.get('handlers',{}).items():
63+
if callable(h):
64+
self.logger.debug('Registered callable object {} for the {} method.'.format(h, m))
65+
self.handlers.update({m : h})
66+
else:
67+
self.logger.error('Object {} is not callable; the {} method will not be supported.'.format(h, m))
68+
69+
@cherrypy.expose()
70+
def default(self, *args, **kwargs):
71+
self.logger.debug('Received {} request. args: {}, kwargs: {}'.format(cherrypy.request.method, args, kwargs))
72+
73+
if cherrypy.request.method not in self.handlers.keys():
74+
# Unsupported method
75+
self.logger.debug('Ignoring request due to unsupported method.')
76+
cherrypy.response.status = 405 # Method Not Allowed
77+
return
78+
else:
79+
if self.handlers.get(cherrypy.request.method):
80+
self.logger.debug('Calling {} to process the request...'.format(self.handlers[cherrypy.request.method]))
81+
self.handlers[cherrypy.request.method](cherrypy.request, *args, **kwargs)
82+
else:
83+
self.logger.error('No handler available for method {}. Ignoring request.'.format(cherrypy.request.method))
84+
cherrypy.response.status = 500 # Internal Server Error
85+
return
86+
87+
return 'OK'

0 commit comments

Comments
 (0)