flask_server: Move the server to its own package.

This commit is contained in:
Eeshan Garg 2017-07-18 01:31:54 -02:30
parent 9d1253ff0d
commit 928d5ca16d
7 changed files with 96 additions and 10 deletions

16
zulip_botserver/README.md Normal file
View file

@ -0,0 +1,16 @@
```
zulip-bot-server --config-file <path to flaskbotrc> --hostname <address> --port <port>
```
Example: `zulip-bot-server --config-file ~/flaskbotrc`
This program loads the bot configurations from the
config file (flaskbotrc here) and loads the bot modules.
It then starts the server and fetches the requests to the
above loaded modules and returns the success/failure result.
Please make sure you have a current flaskbotrc file with the
configurations of the required bots.
Hostname and Port are optional arguments. Default hostname is
127.0.0.1 and default port is 5002.

70
zulip_botserver/setup.py Executable file
View file

@ -0,0 +1,70 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import print_function
import sys
# We should be installable with either setuptools or distutils.
package_info = dict(
name='zulip_botserver',
version='0.3.1',
description='Zulip\'s Flask server for running bots',
author='Zulip Open Source Project',
author_email='zulip-devel@googlegroups.com',
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Topic :: Communications :: Chat',
],
url='https://www.zulip.org/',
entry_points={
'console_scripts': [
'zulip-bot-server=zulip_botserver.server:main',
],
},
test_suite='tests',
) # type: Dict[str, Any]
setuptools_info = dict(
install_requires=[
'zulip>=0.3.1',
'zulip_bots>=0.3.1',
'flask>=0.12.2',
],
)
try:
from setuptools import setup, find_packages
package_info.update(setuptools_info)
package_info['packages'] = find_packages(exclude=['tests'])
except ImportError:
from distutils.core import setup
from distutils.version import LooseVersion
from importlib import import_module
# Manual dependency check
def check_dependency_manually(module_name, version=None):
try:
module = import_module(module_name)
if version is not None:
assert(LooseVersion(module.__version__) >= LooseVersion(version))
except (ImportError, AssertionError):
if version is not None:
print("{name}>={version} is not installed.".format(
req=req, version=version), file=sys.stderr)
else:
print("{name} is not installed.".format(name=module_name), file=sys.stderr)
sys.exit(1)
check_dependency_manually('zulip', '0.3.1')
check_dependency_manually('zulip_bots', '0.3.1')
check_dependency_manually('flask', '0.12.2')
package_info['packages'] = ['zulip_botserver']
setup(**package_info)

View file

View file

@ -0,0 +1,37 @@
from unittest import TestCase
import zulip_botserver.server
import json
from typing import Any, List, Dict, Mapping
class BotServerTestCase(TestCase):
def setUp(self):
# type: () -> None
zulip_botserver.server.app.testing = True
self.app = zulip_botserver.server.app.test_client()
def assert_bot_server_response(self,
available_bots=None,
bots_config=None,
bots_lib_module=None,
payload_url="/bots/testbot",
message=dict(message={'key': "test message"}),
check_success=False,
):
# type: (List[str], Dict[str, Any], Dict[str, Any], str, Dict[str, Dict[str, Any]], bool) -> None
if available_bots is not None:
zulip_botserver.server.available_bots = available_bots
if bots_config is not None:
zulip_botserver.server.bots_config = bots_config
if bots_lib_module is not None:
zulip_botserver.server.bots_lib_module = bots_lib_module
response = self.app.post(payload_url, data=json.dumps(message))
if check_success:
assert response.status_code >= 200 and response.status_code < 300
else:
assert response.status_code >= 400 and response.status_code < 500

View file

@ -0,0 +1,60 @@
from __future__ import absolute_import
import mock
import unittest
from typing import Any
from .server_test_lib import BotServerTestCase
class BotServerTests(BotServerTestCase):
class MockMessageHandler(object):
def handle_message(self, message, bot_handler, state_handler):
# type: (Any, Any, Any) -> None
assert message == {'key': "test message"}
class MockLibModule(object):
def handler_class(self):
# type: () -> Any
return BotServerTests.MockMessageHandler()
@mock.patch('zulip_botserver.server.ExternalBotHandler')
def test_successful_request(self, mock_ExternalBotHandler):
# type: (mock.Mock) -> None
available_bots = ['testbot']
bots_config = {
'testbot': {
'email': 'testbot-bot@zulip.com',
'key': '123456789qwertyuiop',
'site': 'http://localhost',
}
}
bots_lib_module = {
'testbot': BotServerTests.MockLibModule()
}
self.assert_bot_server_response(available_bots=available_bots,
bots_config=bots_config,
bots_lib_module=bots_lib_module,
check_success=True)
assert mock_ExternalBotHandler.called
def test_bot_not_supported(self):
# type: () -> None
available_bots = ['testbot']
self.assert_bot_server_response(available_bots=available_bots,
payload_url="/bots/not_supported_bot",
check_success=False)
def test_wrong_bot_credentials(self):
# type: () -> None
available_bots = ['testbot']
bots_config = {
'testbot': {
'email': 'testbot-bot@zulip.com',
'key': '123456789qwertyuiop',
'site': 'http://localhost',
}
}
self.assert_bot_server_response(available_bots=available_bots,
bots_config=bots_config,
check_success=False)
if __name__ == '__main__':
unittest.main()

View file

@ -0,0 +1,115 @@
from __future__ import absolute_import
from __future__ import print_function
import sys
import json
import optparse
from flask import Flask, request
from importlib import import_module
from typing import Any, Dict, Mapping, Union, List, Tuple
from werkzeug.exceptions import BadRequest
from six.moves.configparser import SafeConfigParser
from zulip import Client
from zulip_bots.lib import ExternalBotHandler, StateHandler
bots_config = {} # type: Dict[str, Mapping[str, str]]
available_bots = [] # type: List[str]
bots_lib_module = {} # type: Dict[str, Any]
def read_config_file(config_file_path):
# type: (str) -> None
parser = SafeConfigParser()
parser.read(config_file_path)
for section in parser.sections():
bots_config[section] = {
"email": parser.get(section, 'email'),
"key": parser.get(section, 'key'),
"site": parser.get(section, 'site'),
}
def load_lib_modules():
# type: () -> None
for bot in available_bots:
try:
module_name = 'zulip_bots.{bot}.{bot}'.format(bot=bot)
bots_lib_module[bot] = import_module(module_name)
except ImportError:
print("\n Import Error: Bot \"{}\" doesn't exists. Please make sure you have set up the flaskbotrc "
"file correctly.\n".format(bot))
sys.exit(1)
app = Flask(__name__)
@app.route('/bots/<bot>', methods=['POST'])
def handle_bot(bot):
# type: (str) -> Union[str, BadRequest]
if bot not in available_bots:
return BadRequest("requested bot service {} not supported".format(bot))
client = Client(email=bots_config[bot]["email"],
api_key=bots_config[bot]["key"],
site=bots_config[bot]["site"])
try:
restricted_client = ExternalBotHandler(client)
except SystemExit:
return BadRequest("Cannot fetch user profile for bot {}, make sure you have set up the flaskbotrc "
"file correctly.".format(bot))
message_handler = bots_lib_module[bot].handler_class()
# TODO: Handle stateful bots properly.
state_handler = StateHandler()
event = json.loads(request.data)
message_handler.handle_message(message=event["message"],
bot_handler=restricted_client,
state_handler=state_handler)
return "Success!"
def parse_args():
# type: () -> Tuple[Any, Any]
usage = '''
zulip-bot-server --config-file <path to flaskbotrc> --hostname <address> --port <port>
Example: zulip-bot-server --config-file ~/flaskbotrc
(This program loads the bot configurations from the
config file (flaskbotrc here) and loads the bot modules.
It then starts the server and fetches the requests to the
above loaded modules and returns the success/failure result)
Please make sure you have a current flaskbotrc file with the
configurations of the required bots.
Hostname and Port are optional arguments. Default hostname is
127.0.0.1 and default port is 5002.
See lib/readme.md for more context.
'''
parser = optparse.OptionParser(usage=usage)
parser.add_option('--config-file',
action='store',
help='(config file for the zulip bot server (flaskbotrc))')
parser.add_option('--hostname',
action='store',
default="127.0.0.1",
help='(address on which you want to run the server)')
parser.add_option('--port',
action='store',
default=5002,
help='(port on which you want to run the server)')
(options, args) = parser.parse_args()
if not options.config_file: # if flaskbotrc is not given
parser.error('Flaskbotrc not given')
return (options, args)
def main():
# type: () -> None
(options, args) = parse_args()
read_config_file(options.config_file)
global available_bots
available_bots = list(bots_config.keys())
load_lib_modules()
app.run(host=options.hostname, port=options.port, debug=True)
if __name__ == '__main__':
main()