bots: Allow users to supply ZULIP_API_KEY, etc.

This will make it easier to run standalone
bots in containers like Heroku that prefer
env-var-style configuration.

For now this is undocumented, but we should
update the server docs once a few folks have
tried it out.

(The history behind requiring the config file
is that I wanted to keep things simple and
be strongly opinionated about how you run
bots, so that the docs didn't overwhelm folks,
but this use case has come up more frequently.)
This commit is contained in:
Steve Howell 2018-12-09 18:26:02 +00:00 committed by showell
parent 3d68e394dc
commit 0fd093a698
3 changed files with 50 additions and 13 deletions

View file

@ -30,6 +30,22 @@ def get_bots_directory_path() -> str:
current_dir = os.path.dirname(os.path.abspath(__file__))
return os.path.join(current_dir, 'bots')
def zulip_env_vars_are_present() -> bool:
# We generally require a Zulip config file, but if
# the user supplies the correct environment vars, we
# waive the requirement. This can be helpful for
# containers like Heroku that prefer env vars to config
# files.
if os.environ.get('ZULIP_EMAIL') is None:
return False
if os.environ.get('ZULIP_API_KEY') is None:
return False
if os.environ.get('ZULIP_SITE') is None:
return False
# If none of the absolutely critical env vars are
# missing, we can proceed without a config file.
return True
class RateLimit(object):
def __init__(self, message_limit: int, interval_limit: int) -> None:

View file

@ -7,12 +7,15 @@ import os
from os.path import basename, splitext
from zulip_bots.lib import (
zulip_env_vars_are_present,
run_message_handler_for_bot,
NoBotConfigException,
)
from zulip_bots import finder
from zulip_bots.provision import provision_bot
from typing import Optional
current_dir = os.path.dirname(os.path.abspath(__file__))
def parse_args() -> argparse.Namespace:
@ -32,7 +35,6 @@ def parse_args() -> argparse.Namespace:
parser.add_argument('--config-file', '-c',
action='store',
required=True,
help='zulip configuration file (e.g. ~/Downloads/zuliprc)')
parser.add_argument('--bot-config-file', '-b',
@ -51,17 +53,36 @@ def parse_args() -> argparse.Namespace:
return args
def exit_gracefully_if_zulip_config_file_does_not_exist(config_file: str) -> None:
if not os.path.exists(config_file):
print('''
ERROR: %s does not exist.
def exit_gracefully_if_zulip_config_is_missing(config_file: Optional[str]) -> None:
error_msg = None
if config_file:
if os.path.exists(config_file):
# We're good. (There may be problems with the config file,
# but we'll catch those later.
return
else:
error_msg = 'ERROR: %s does not exist.' % (config_file,)
else:
if zulip_env_vars_are_present():
return
else:
error_msg = 'ERROR: You did not supply a Zulip config file.'
if error_msg:
print('\n')
print(error_msg)
print('''
You may need to download a config file from the Zulip app, or
if you have already done that, you need to specify the file
location correctly.
''' % (config_file,))
sys.exit(1)
location correctly on the command line.
If you don't want to use a config file, you must set
these env vars: ZULIP_EMAIL, ZULIP_API_KEY, ZULIP_SITE.
''')
sys.exit(1)
def exit_gracefully_if_bot_config_file_does_not_exist(bot_config_file: str) -> None:
if bot_config_file is None:
@ -120,7 +141,7 @@ def main() -> None:
# It's a bit unfortunate that we have two config files, but the
# alternative would be way worse for people running multiple bots
# or testing against multiple Zulip servers.
exit_gracefully_if_zulip_config_file_does_not_exist(args.config_file)
exit_gracefully_if_zulip_config_is_missing(args.config_file)
exit_gracefully_if_bot_config_file_does_not_exist(args.bot_config_file)
try:

View file

@ -19,7 +19,7 @@ class TestDefaultArguments(TestCase):
@patch('sys.argv', ['zulip-run-bot', 'giphy', '--config-file', '/foo/bar/baz.conf'])
@patch('zulip_bots.run.run_message_handler_for_bot')
def test_argument_parsing_with_bot_name(self, mock_run_message_handler_for_bot: mock.Mock) -> None:
with patch('zulip_bots.run.exit_gracefully_if_zulip_config_file_does_not_exist'):
with patch('zulip_bots.run.exit_gracefully_if_zulip_config_is_missing'):
zulip_bots.run.main()
mock_run_message_handler_for_bot.assert_called_with(bot_name='giphy',
@ -31,7 +31,7 @@ class TestDefaultArguments(TestCase):
@patch('sys.argv', ['zulip-run-bot', path_to_bot, '--config-file', '/foo/bar/baz.conf'])
@patch('zulip_bots.run.run_message_handler_for_bot')
def test_argument_parsing_with_bot_path(self, mock_run_message_handler_for_bot: mock.Mock) -> None:
with patch('zulip_bots.run.exit_gracefully_if_zulip_config_file_does_not_exist'):
with patch('zulip_bots.run.exit_gracefully_if_zulip_config_is_missing'):
zulip_bots.run.main()
mock_run_message_handler_for_bot.assert_called_with(
@ -61,7 +61,7 @@ class TestDefaultArguments(TestCase):
with patch('sys.argv', ['zulip-run-bot', bot_qualifier, '--config-file', '/path/to/config']):
with patch('zulip_bots.finder.import_module_from_source', return_value=mock.Mock()):
with patch('zulip_bots.run.run_message_handler_for_bot'):
with patch('zulip_bots.run.exit_gracefully_if_zulip_config_file_does_not_exist'):
with patch('zulip_bots.run.exit_gracefully_if_zulip_config_is_missing'):
zulip_bots.run.main()
self.assertIn(bot_dir_path, sys.path)
@ -74,7 +74,7 @@ class TestDefaultArguments(TestCase):
with patch('sys.argv', ['zulip-run-bot', 'bot.module.name', '--config-file', '/path/to/config']):
with patch('importlib.import_module', return_value=mock_bot_module) as mock_import_module:
with patch('zulip_bots.run.run_message_handler_for_bot'):
with patch('zulip_bots.run.exit_gracefully_if_zulip_config_file_does_not_exist'):
with patch('zulip_bots.run.exit_gracefully_if_zulip_config_is_missing'):
zulip_bots.run.main()
mock_import_module.assert_called_once_with(bot_module_name)