From 6f3f9bf7e42cd51bc1569017a4d3a795efa64af8 Mon Sep 17 00:00:00 2001 From: PIG208 <359101898@qq.com> Date: Fri, 28 May 2021 17:05:11 +0800 Subject: [PATCH] black: Reformat without skipping string normalization. --- tools/custom_check.py | 140 ++-- tools/deploy | 204 ++--- tools/gitlint-rules.py | 482 +++++------ tools/lint | 16 +- tools/provision | 64 +- tools/release-packages | 114 +-- tools/review | 38 +- tools/run-mypy | 54 +- tools/server_lib/test_handler.py | 34 +- tools/test-bots | 46 +- tools/test-botserver | 4 +- tools/test-lib | 4 +- tools/test-zulip | 4 +- .../run-interrealm-bridge | 4 +- .../bridge_with_irc/irc-mirror.py | 14 +- .../bridge_with_irc/irc_mirror_backend.py | 6 +- .../bridge_with_matrix/matrix_bridge.py | 84 +- .../bridge_with_matrix/test_matrix.py | 36 +- .../bridge_with_slack/run-slack-bridge | 28 +- .../codebase/zulip_codebase_mirror | 114 +-- zulip/integrations/git/post-receive | 34 +- zulip/integrations/git/zulip_git_config.py | 4 +- .../google/get-google-credentials | 12 +- zulip/integrations/google/google-calendar | 54 +- zulip/integrations/hg/zulip_changegroup.py | 2 +- .../jabber/jabber_mirror_backend.py | 126 +-- zulip/integrations/log2zulip/log2zulip | 2 +- zulip/integrations/nagios/nagios-notify-zulip | 32 +- zulip/integrations/openshift/post_deploy | 22 +- .../openshift/zulip_openshift_config.py | 24 +- .../perforce/zulip_change-commit.py | 10 +- .../perforce/zulip_perforce_config.py | 2 +- zulip/integrations/rss/rss-bot | 50 +- zulip/integrations/svn/post-commit | 2 +- zulip/integrations/svn/zulip_svn_config.py | 2 +- zulip/integrations/trac/zulip_trac.py | 8 +- zulip/integrations/trello/zulip_trello.py | 58 +- zulip/integrations/twitter/twitter-bot | 66 +- zulip/integrations/zephyr/check-mirroring | 26 +- zulip/integrations/zephyr/sync-public-streams | 30 +- .../zephyr/zephyr_mirror_backend.py | 150 ++-- zulip/setup.py | 62 +- zulip/tests/test_default_arguments.py | 16 +- zulip/tests/test_hash_util_decode.py | 14 +- zulip/zulip/__init__.py | 756 +++++++++--------- zulip/zulip/api_examples.py | 6 +- zulip/zulip/examples/alert-words | 10 +- zulip/zulip/examples/create-user | 16 +- zulip/zulip/examples/delete-message | 2 +- zulip/zulip/examples/delete-stream | 2 +- zulip/zulip/examples/edit-message | 6 +- zulip/zulip/examples/edit-stream | 24 +- zulip/zulip/examples/get-history | 32 +- zulip/zulip/examples/get-messages | 28 +- zulip/zulip/examples/get-raw-message | 2 +- zulip/zulip/examples/get-stream-topics | 2 +- zulip/zulip/examples/get-user-presence | 2 +- zulip/zulip/examples/message-history | 2 +- zulip/zulip/examples/mute-topic | 10 +- zulip/zulip/examples/send-message | 16 +- zulip/zulip/examples/subscribe | 2 +- zulip/zulip/examples/unsubscribe | 2 +- zulip/zulip/examples/update-message-flags | 8 +- zulip/zulip/examples/upload-file | 14 +- zulip/zulip/examples/welcome-message | 44 +- zulip/zulip/send.py | 64 +- zulip_bots/setup.py | 80 +- .../bots/baremetrics/baremetrics.py | 206 ++--- .../bots/baremetrics/test_baremetrics.py | 126 +-- .../zulip_bots/bots/beeminder/beeminder.py | 28 +- .../bots/beeminder/test_beeminder.py | 72 +- .../zulip_bots/bots/chessbot/chessbot.py | 186 ++--- .../zulip_bots/bots/chessbot/test_chessbot.py | 32 +- .../bots/connect_four/connect_four.py | 36 +- .../bots/connect_four/controller.py | 12 +- .../bots/connect_four/test_connect_four.py | 46 +- .../zulip_bots/bots/converter/converter.py | 38 +- .../bots/converter/test_converter.py | 8 +- zulip_bots/zulip_bots/bots/converter/utils.py | 276 +++---- zulip_bots/zulip_bots/bots/define/define.py | 30 +- .../zulip_bots/bots/define/test_define.py | 22 +- .../zulip_bots/bots/dialogflow/dialogflow.py | 44 +- .../bots/dialogflow/test_dialogflow.py | 30 +- .../bots/dropbox_share/dropbox_share.py | 94 +-- .../bots/dropbox_share/test_dropbox_share.py | 104 +-- zulip_bots/zulip_bots/bots/encrypt/encrypt.py | 16 +- .../zulip_bots/bots/encrypt/test_encrypt.py | 2 +- .../bots/file_uploader/file_uploader.py | 26 +- .../bots/file_uploader/test_file_uploader.py | 32 +- zulip_bots/zulip_bots/bots/flock/flock.py | 36 +- .../zulip_bots/bots/flock/test_flock.py | 52 +- .../zulip_bots/bots/followup/followup.py | 26 +- .../zulip_bots/bots/followup/test_followup.py | 38 +- zulip_bots/zulip_bots/bots/front/front.py | 38 +- .../zulip_bots/bots/front/test_front.py | 70 +- .../bots/game_handler_bot/game_handler_bot.py | 36 +- .../game_handler_bot/test_game_handler_bot.py | 502 ++++++------ .../bots/game_of_fifteen/game_of_fifteen.py | 56 +- .../game_of_fifteen/test_game_of_fifteen.py | 52 +- zulip_bots/zulip_bots/bots/giphy/giphy.py | 40 +- .../zulip_bots/bots/giphy/test_giphy.py | 52 +- .../bots/github_detail/github_detail.py | 46 +- .../bots/github_detail/test_github_detail.py | 150 ++-- .../bots/google_search/google_search.py | 32 +- .../bots/google_search/test_google_search.py | 26 +- .../bots/google_translate/google_translate.py | 58 +- .../google_translate/test_google_translate.py | 50 +- .../zulip_bots/bots/helloworld/helloworld.py | 8 +- .../bots/helloworld/test_helloworld.py | 6 +- zulip_bots/zulip_bots/bots/help/help.py | 4 +- .../zulip_bots/bots/idonethis/idonethis.py | 36 +- .../bots/idonethis/test_idonethis.py | 130 +-- .../zulip_bots/bots/incident/incident.py | 60 +- .../bots/incrementor/incrementor.py | 26 +- .../bots/incrementor/test_incrementor.py | 20 +- zulip_bots/zulip_bots/bots/jira/jira.py | 250 +++--- zulip_bots/zulip_bots/bots/jira/test_jira.py | 86 +- .../bots/link_shortener/link_shortener.py | 62 +- .../link_shortener/test_link_shortener.py | 44 +- zulip_bots/zulip_bots/bots/mention/mention.py | 76 +- .../zulip_bots/bots/mention/test_mention.py | 42 +- .../bots/merels/libraries/game_data.py | 2 +- .../bots/merels/libraries/interface.py | 4 +- zulip_bots/zulip_bots/bots/merels/merels.py | 22 +- .../bots/merels/test/test_database.py | 12 +- .../bots/merels/test/test_interface.py | 12 +- .../zulip_bots/bots/merels/test_merels.py | 36 +- .../bots/monkeytestit/lib/report.py | 14 +- .../bots/monkeytestit/monkeytestit.py | 8 +- .../bots/monkeytestit/test_monkeytestit.py | 34 +- .../zulip_bots/bots/salesforce/salesforce.py | 132 +-- .../bots/salesforce/test_salesforce.py | 114 +-- .../zulip_bots/bots/salesforce/utils.py | 72 +- .../bots/stack_overflow/stack_overflow.py | 50 +- .../stack_overflow/test_stack_overflow.py | 38 +- zulip_bots/zulip_bots/bots/susi/susi.py | 14 +- zulip_bots/zulip_bots/bots/susi/test_susi.py | 14 +- .../bots/tictactoe/test_tictactoe.py | 36 +- .../zulip_bots/bots/tictactoe/tictactoe.py | 90 +-- .../zulip_bots/bots/trello/test_trello.py | 106 +-- zulip_bots/zulip_bots/bots/trello/trello.py | 108 +-- .../bots/trivia_quiz/test_trivia_quiz.py | 108 +-- .../bots/trivia_quiz/trivia_quiz.py | 94 +-- .../zulip_bots/bots/twitpost/test_twitpost.py | 22 +- .../zulip_bots/bots/twitpost/twitpost.py | 16 +- .../bots/virtual_fs/test_virtual_fs.py | 50 +- .../zulip_bots/bots/virtual_fs/virtual_fs.py | 278 +++---- .../zulip_bots/bots/weather/test_weather.py | 22 +- zulip_bots/zulip_bots/bots/weather/weather.py | 38 +- .../bots/wikipedia/test_wikipedia.py | 48 +- .../zulip_bots/bots/wikipedia/wikipedia.py | 58 +- .../zulip_bots/bots/witai/test_witai.py | 32 +- zulip_bots/zulip_bots/bots/witai/witai.py | 34 +- zulip_bots/zulip_bots/bots/xkcd/test_xkcd.py | 30 +- zulip_bots/zulip_bots/bots/xkcd/xkcd.py | 46 +- zulip_bots/zulip_bots/bots/yoda/test_yoda.py | 34 +- zulip_bots/zulip_bots/bots/yoda/yoda.py | 40 +- .../zulip_bots/bots/youtube/test_youtube.py | 66 +- zulip_bots/zulip_bots/bots/youtube/youtube.py | 72 +- zulip_bots/zulip_bots/custom_exceptions.py | 4 +- zulip_bots/zulip_bots/finder.py | 2 +- zulip_bots/zulip_bots/game_handler.py | 482 +++++------ zulip_bots/zulip_bots/lib.py | 126 +-- zulip_bots/zulip_bots/provision.py | 30 +- zulip_bots/zulip_bots/request_test_lib.py | 40 +- zulip_bots/zulip_bots/run.py | 52 +- zulip_bots/zulip_bots/simple_lib.py | 32 +- zulip_bots/zulip_bots/terminal.py | 22 +- zulip_bots/zulip_bots/test_file_utils.py | 12 +- zulip_bots/zulip_bots/test_lib.py | 42 +- zulip_bots/zulip_bots/tests/test_finder.py | 6 +- zulip_bots/zulip_bots/tests/test_lib.py | 114 +-- zulip_bots/zulip_bots/tests/test_run.py | 56 +- zulip_botserver/setup.py | 56 +- zulip_botserver/tests/server_test_lib.py | 2 +- zulip_botserver/tests/test_server.py | 204 ++--- .../zulip_botserver/input_parameters.py | 52 +- zulip_botserver/zulip_botserver/server.py | 40 +- 178 files changed, 5242 insertions(+), 5242 deletions(-) diff --git a/tools/custom_check.py b/tools/custom_check.py index ce89004..c4ae2f5 100644 --- a/tools/custom_check.py +++ b/tools/custom_check.py @@ -8,96 +8,96 @@ if MYPY: whitespace_rules = [ # This linter should be first since bash_rules depends on it. - {'pattern': r'\s+$', 'strip': '\n', 'description': 'Fix trailing whitespace'}, - {'pattern': '\t', 'strip': '\n', 'description': 'Fix tab-based whitespace'}, + {"pattern": r"\s+$", "strip": "\n", "description": "Fix trailing whitespace"}, + {"pattern": "\t", "strip": "\n", "description": "Fix tab-based whitespace"}, ] # type: List[Rule] markdown_whitespace_rules = list( - [rule for rule in whitespace_rules if rule['pattern'] != r'\s+$'] + [rule for rule in whitespace_rules if rule["pattern"] != r"\s+$"] ) + [ # Two spaces trailing a line with other content is okay--it's a markdown line break. # This rule finds one space trailing a non-space, three or more trailing spaces, and # spaces on an empty line. { - 'pattern': r'((?~"]', 'description': 'Missing whitespace after "="'}, - {'pattern': r'":\w[^"]*$', 'description': 'Missing whitespace after ":"'}, - {'pattern': r"':\w[^']*$", 'description': 'Missing whitespace after ":"'}, - {'pattern': r"^\s+[#]\w", 'strip': '\n', 'description': 'Missing whitespace after "#"'}, + {"pattern": r" =" + r'[^ =>~"]', "description": 'Missing whitespace after "="'}, + {"pattern": r'":\w[^"]*$', "description": 'Missing whitespace after ":"'}, + {"pattern": r"':\w[^']*$", "description": 'Missing whitespace after ":"'}, + {"pattern": r"^\s+[#]\w", "strip": "\n", "description": 'Missing whitespace after "#"'}, { - 'pattern': r"assertEquals[(]", - 'description': 'Use assertEqual, not assertEquals (which is deprecated).', + "pattern": r"assertEquals[(]", + "description": "Use assertEqual, not assertEquals (which is deprecated).", }, { - 'pattern': r'self: Any', - 'description': 'you can omit Any annotation for self', - 'good_lines': ['def foo (self):'], - 'bad_lines': ['def foo(self: Any):'], + "pattern": r"self: Any", + "description": "you can omit Any annotation for self", + "good_lines": ["def foo (self):"], + "bad_lines": ["def foo(self: Any):"], }, - {'pattern': r"== None", 'description': 'Use `is None` to check whether something is None'}, - {'pattern': r"type:[(]", 'description': 'Missing whitespace after ":" in type annotation'}, - {'pattern': r"# type [(]", 'description': 'Missing : after type in type annotation'}, - {'pattern': r"#type", 'description': 'Missing whitespace after "#" in type annotation'}, - {'pattern': r'if[(]', 'description': 'Missing space between if and ('}, - {'pattern': r", [)]", 'description': 'Unnecessary whitespace between "," and ")"'}, - {'pattern': r"% [(]", 'description': 'Unnecessary whitespace between "%" and "("'}, + {"pattern": r"== None", "description": "Use `is None` to check whether something is None"}, + {"pattern": r"type:[(]", "description": 'Missing whitespace after ":" in type annotation'}, + {"pattern": r"# type [(]", "description": "Missing : after type in type annotation"}, + {"pattern": r"#type", "description": 'Missing whitespace after "#" in type annotation'}, + {"pattern": r"if[(]", "description": "Missing space between if and ("}, + {"pattern": r", [)]", "description": 'Unnecessary whitespace between "," and ")"'}, + {"pattern": r"% [(]", "description": 'Unnecessary whitespace between "%" and "("'}, # This next check could have false positives, but it seems pretty # rare; if we find any, they can be added to the exclude list for # this rule. { - 'pattern': r' % [a-zA-Z0-9_.]*\)?$', - 'description': 'Used % comprehension without a tuple', + "pattern": r" % [a-zA-Z0-9_.]*\)?$", + "description": "Used % comprehension without a tuple", }, { - 'pattern': r'.*%s.* % \([a-zA-Z0-9_.]*\)$', - 'description': 'Used % comprehension without a tuple', + "pattern": r".*%s.* % \([a-zA-Z0-9_.]*\)$", + "description": "Used % comprehension without a tuple", }, { - 'pattern': r'__future__', - 'include_only': {'zulip_bots/zulip_bots/bots/'}, - 'description': 'Bots no longer need __future__ imports.', + "pattern": r"__future__", + "include_only": {"zulip_bots/zulip_bots/bots/"}, + "description": "Bots no longer need __future__ imports.", }, { - 'pattern': r'#!/usr/bin/env python$', - 'include_only': {'zulip_bots/'}, - 'description': 'Python shebangs must be python3', + "pattern": r"#!/usr/bin/env python$", + "include_only": {"zulip_bots/"}, + "description": "Python shebangs must be python3", }, { - 'pattern': r'(^|\s)open\s*\(', - 'description': 'open() should not be used in Zulip\'s bots. Use functions' - ' provided by the bots framework to access the filesystem.', - 'include_only': {'zulip_bots/zulip_bots/bots/'}, + "pattern": r"(^|\s)open\s*\(", + "description": "open() should not be used in Zulip's bots. Use functions" + " provided by the bots framework to access the filesystem.", + "include_only": {"zulip_bots/zulip_bots/bots/"}, }, { - 'pattern': r'pprint', - 'description': 'Used pprint, which is most likely a debugging leftover. For user output, use print().', + "pattern": r"pprint", + "description": "Used pprint, which is most likely a debugging leftover. For user output, use print().", }, { - 'pattern': r'\(BotTestCase\)', - 'bad_lines': ['class TestSomeBot(BotTestCase):'], - 'description': 'Bot test cases should directly inherit from BotTestCase *and* DefaultTests.', + "pattern": r"\(BotTestCase\)", + "bad_lines": ["class TestSomeBot(BotTestCase):"], + "description": "Bot test cases should directly inherit from BotTestCase *and* DefaultTests.", }, { - 'pattern': r'\(DefaultTests, BotTestCase\)', - 'bad_lines': ['class TestSomeBot(DefaultTests, BotTestCase):'], - 'good_lines': ['class TestSomeBot(BotTestCase, DefaultTests):'], - 'description': 'Bot test cases should inherit from BotTestCase before DefaultTests.', + "pattern": r"\(DefaultTests, BotTestCase\)", + "bad_lines": ["class TestSomeBot(DefaultTests, BotTestCase):"], + "good_lines": ["class TestSomeBot(BotTestCase, DefaultTests):"], + "description": "Bot test cases should inherit from BotTestCase before DefaultTests.", }, *whitespace_rules, ], @@ -105,12 +105,12 @@ python_rules = RuleList( ) bash_rules = RuleList( - langs=['sh'], + langs=["sh"], rules=[ { - 'pattern': r'#!.*sh [-xe]', - 'description': 'Fix shebang line with proper call to /usr/bin/env for Bash path, change -x|-e switches' - ' to set -x|set -e', + "pattern": r"#!.*sh [-xe]", + "description": "Fix shebang line with proper call to /usr/bin/env for Bash path, change -x|-e switches" + " to set -x|set -e", }, *whitespace_rules[0:1], ], @@ -118,7 +118,7 @@ bash_rules = RuleList( json_rules = RuleList( - langs=['json'], + langs=["json"], # Here, we don't check tab-based whitespace, because the tab-based # whitespace rule flags a lot of third-party JSON fixtures # under zerver/webhooks that we want preserved verbatim. So @@ -131,21 +131,21 @@ json_rules = RuleList( prose_style_rules = [ { - 'pattern': r'[^\/\#\-"]([jJ]avascript)', # exclude usage in hrefs/divs - 'description': "javascript should be spelled JavaScript", + "pattern": r'[^\/\#\-"]([jJ]avascript)', # exclude usage in hrefs/divs + "description": "javascript should be spelled JavaScript", }, { - 'pattern': r'''[^\/\-\."'\_\=\>]([gG]ithub)[^\.\-\_"\<]''', # exclude usage in hrefs/divs - 'description': "github should be spelled GitHub", + "pattern": r"""[^\/\-\."'\_\=\>]([gG]ithub)[^\.\-\_"\<]""", # exclude usage in hrefs/divs + "description": "github should be spelled GitHub", }, { - 'pattern': r'[oO]rganisation', # exclude usage in hrefs/divs - 'description': "Organization is spelled with a z", + "pattern": r"[oO]rganisation", # exclude usage in hrefs/divs + "description": "Organization is spelled with a z", }, - {'pattern': r'!!! warning', 'description': "!!! warning is invalid; it's spelled '!!! warn'"}, + {"pattern": r"!!! warning", "description": "!!! warning is invalid; it's spelled '!!! warn'"}, { - 'pattern': r'[^-_]botserver(?!rc)|bot server', - 'description': "Use Botserver instead of botserver or Botserver.", + "pattern": r"[^-_]botserver(?!rc)|bot server", + "description": "Use Botserver instead of botserver or Botserver.", }, ] # type: List[Rule] @@ -154,13 +154,13 @@ markdown_docs_length_exclude = { } markdown_rules = RuleList( - langs=['md'], + langs=["md"], rules=[ *markdown_whitespace_rules, *prose_style_rules, { - 'pattern': r'\[(?P[^\]]+)\]\((?P=url)\)', - 'description': 'Linkified markdown URLs should use cleaner syntax.', + "pattern": r"\[(?P[^\]]+)\]\((?P=url)\)", + "description": "Linkified markdown URLs should use cleaner syntax.", }, ], max_length=120, @@ -168,7 +168,7 @@ markdown_rules = RuleList( ) txt_rules = RuleList( - langs=['txt'], + langs=["txt"], rules=whitespace_rules, ) diff --git a/tools/deploy b/tools/deploy index 03d2eb1..8afd721 100755 --- a/tools/deploy +++ b/tools/deploy @@ -11,69 +11,69 @@ from typing import Any, Callable, Dict, List import requests from requests import Response -red = '\033[91m' # type: str -green = '\033[92m' # type: str -end_format = '\033[0m' # type: str -bold = '\033[1m' # type: str +red = "\033[91m" # type: str +green = "\033[92m" # type: str +end_format = "\033[0m" # type: str +bold = "\033[1m" # type: str -bots_dir = '.bots' # type: str +bots_dir = ".bots" # type: str def pack(options: argparse.Namespace) -> None: # Basic sanity checks for input. if not options.path: - print('tools/deploy: Path to bot folder not specified.') + print("tools/deploy: Path to bot folder not specified.") sys.exit(1) if not options.config: - print('tools/deploy: Path to zuliprc not specified.') + print("tools/deploy: Path to zuliprc not specified.") sys.exit(1) if not options.main: - print('tools/deploy: No main bot file specified.') + print("tools/deploy: No main bot file specified.") sys.exit(1) if not os.path.isfile(options.config): - print('pack: Config file not found at path: {}.'.format(options.config)) + print("pack: Config file not found at path: {}.".format(options.config)) sys.exit(1) if not os.path.isdir(options.path): - print('pack: Bot folder not found at path: {}.'.format(options.path)) + print("pack: Bot folder not found at path: {}.".format(options.path)) sys.exit(1) main_path = os.path.join(options.path, options.main) if not os.path.isfile(main_path): - print('pack: Bot main file not found at path: {}.'.format(main_path)) + print("pack: Bot main file not found at path: {}.".format(main_path)) sys.exit(1) # Main logic for packing the bot. if not os.path.exists(bots_dir): os.makedirs(bots_dir) zip_file_path = os.path.join(bots_dir, options.botname + ".zip") - zip_file = zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_DEFLATED) + zip_file = zipfile.ZipFile(zip_file_path, "w", zipfile.ZIP_DEFLATED) # Pack the complete bot folder for root, dirs, files in os.walk(options.path): for file in files: file_path = os.path.join(root, file) zip_file.write(file_path, os.path.relpath(file_path, options.path)) # Pack the zuliprc - zip_file.write(options.config, 'zuliprc') + zip_file.write(options.config, "zuliprc") # Pack the config file for the botfarm. bot_config = textwrap.dedent( - '''\ + """\ [deploy] bot={} zuliprc=zuliprc - '''.format( + """.format( options.main ) ) - zip_file.writestr('config.ini', bot_config) + zip_file.writestr("config.ini", bot_config) zip_file.close() - print('pack: Created zip file at: {}.'.format(zip_file_path)) + print("pack: Created zip file at: {}.".format(zip_file_path)) def check_common_options(options: argparse.Namespace) -> None: if not options.server: - print('tools/deploy: URL to Botfarm server not specified.') + print("tools/deploy: URL to Botfarm server not specified.") sys.exit(1) if not options.token: - print('tools/deploy: Botfarm deploy token not specified.') + print("tools/deploy: Botfarm deploy token not specified.") sys.exit(1) @@ -83,7 +83,7 @@ def handle_common_response_without_data( return handle_common_response( response=response, operation=operation, - success_handler=lambda r: print('{}: {}'.format(operation, success_message)), + success_handler=lambda r: print("{}: {}".format(operation, success_message)), ) @@ -92,56 +92,56 @@ def handle_common_response( ) -> bool: if response.status_code == requests.codes.ok: response_data = response.json() - if response_data['status'] == 'success': + if response_data["status"] == "success": success_handler(response_data) return True - elif response_data['status'] == 'error': - print('{}: {}'.format(operation, response_data['message'])) + elif response_data["status"] == "error": + print("{}: {}".format(operation, response_data["message"])) return False else: - print('{}: Unexpected success response format'.format(operation)) + print("{}: Unexpected success response format".format(operation)) return False if response.status_code == requests.codes.unauthorized: - print('{}: Authentication error with the server. Aborting.'.format(operation)) + print("{}: Authentication error with the server. Aborting.".format(operation)) else: - print('{}: Error {}. Aborting.'.format(operation, response.status_code)) + print("{}: Error {}. Aborting.".format(operation, response.status_code)) return False def upload(options: argparse.Namespace) -> None: check_common_options(options) - file_path = os.path.join(bots_dir, options.botname + '.zip') + file_path = os.path.join(bots_dir, options.botname + ".zip") if not os.path.exists(file_path): - print('upload: Could not find bot package at {}.'.format(file_path)) + print("upload: Could not find bot package at {}.".format(file_path)) sys.exit(1) - files = {'file': open(file_path, 'rb')} - headers = {'key': options.token} - url = urllib.parse.urljoin(options.server, 'bots/upload') + files = {"file": open(file_path, "rb")} + headers = {"key": options.token} + url = urllib.parse.urljoin(options.server, "bots/upload") response = requests.post(url, files=files, headers=headers) result = handle_common_response_without_data( - response, 'upload', 'Uploaded the bot package to botfarm.' + response, "upload", "Uploaded the bot package to botfarm." ) if result is False: sys.exit(1) def clean(options: argparse.Namespace) -> None: - file_path = os.path.join(bots_dir, options.botname + '.zip') + file_path = os.path.join(bots_dir, options.botname + ".zip") if os.path.exists(file_path): os.remove(file_path) - print('clean: Removed {}.'.format(file_path)) + print("clean: Removed {}.".format(file_path)) else: - print('clean: File \'{}\' not found.'.format(file_path)) + print("clean: File '{}' not found.".format(file_path)) def process(options: argparse.Namespace) -> None: check_common_options(options) - headers = {'key': options.token} - url = urllib.parse.urljoin(options.server, 'bots/process') - payload = {'name': options.botname} + headers = {"key": options.token} + url = urllib.parse.urljoin(options.server, "bots/process") + payload = {"name": options.botname} response = requests.post(url, headers=headers, json=payload) result = handle_common_response_without_data( - response, 'process', 'The bot has been processed by the botfarm.' + response, "process", "The bot has been processed by the botfarm." ) if result is False: sys.exit(1) @@ -149,12 +149,12 @@ def process(options: argparse.Namespace) -> None: def start(options: argparse.Namespace) -> None: check_common_options(options) - headers = {'key': options.token} - url = urllib.parse.urljoin(options.server, 'bots/start') - payload = {'name': options.botname} + headers = {"key": options.token} + url = urllib.parse.urljoin(options.server, "bots/start") + payload = {"name": options.botname} response = requests.post(url, headers=headers, json=payload) result = handle_common_response_without_data( - response, 'start', 'The bot has been started by the botfarm.' + response, "start", "The bot has been started by the botfarm." ) if result is False: sys.exit(1) @@ -162,12 +162,12 @@ def start(options: argparse.Namespace) -> None: def stop(options: argparse.Namespace) -> None: check_common_options(options) - headers = {'key': options.token} - url = urllib.parse.urljoin(options.server, 'bots/stop') - payload = {'name': options.botname} + headers = {"key": options.token} + url = urllib.parse.urljoin(options.server, "bots/stop") + payload = {"name": options.botname} response = requests.post(url, headers=headers, json=payload) result = handle_common_response_without_data( - response, 'stop', 'The bot has been stopped by the botfarm.' + response, "stop", "The bot has been stopped by the botfarm." ) if result is False: sys.exit(1) @@ -182,27 +182,27 @@ def prepare(options: argparse.Namespace) -> None: def log(options: argparse.Namespace) -> None: check_common_options(options) - headers = {'key': options.token} + headers = {"key": options.token} if options.lines: lines = options.lines else: lines = None - payload = {'name': options.botname, 'lines': lines} - url = urllib.parse.urljoin(options.server, 'bots/logs/' + options.botname) + payload = {"name": options.botname, "lines": lines} + url = urllib.parse.urljoin(options.server, "bots/logs/" + options.botname) response = requests.get(url, json=payload, headers=headers) - result = handle_common_response(response, 'log', lambda r: print(r['logs']['content'])) + result = handle_common_response(response, "log", lambda r: print(r["logs"]["content"])) if result is False: sys.exit(1) def delete(options: argparse.Namespace) -> None: check_common_options(options) - headers = {'key': options.token} - url = urllib.parse.urljoin(options.server, 'bots/delete') - payload = {'name': options.botname} + headers = {"key": options.token} + url = urllib.parse.urljoin(options.server, "bots/delete") + payload = {"name": options.botname} response = requests.post(url, headers=headers, json=payload) result = handle_common_response_without_data( - response, 'delete', 'The bot has been removed from the botfarm.' + response, "delete", "The bot has been removed from the botfarm." ) if result is False: sys.exit(1) @@ -210,15 +210,15 @@ def delete(options: argparse.Namespace) -> None: def list_bots(options: argparse.Namespace) -> None: check_common_options(options) - headers = {'key': options.token} + headers = {"key": options.token} if options.format: pretty_print = True else: pretty_print = False - url = urllib.parse.urljoin(options.server, 'bots/list') + url = urllib.parse.urljoin(options.server, "bots/list") response = requests.get(url, headers=headers) result = handle_common_response( - response, 'ls', lambda r: print_bots(r['bots']['list'], pretty_print) + response, "ls", lambda r: print_bots(r["bots"]["list"], pretty_print) ) if result is False: sys.exit(1) @@ -229,36 +229,36 @@ def print_bots(bots: List[Any], pretty_print: bool) -> None: print_bots_pretty(bots) else: for bot in bots: - print('{}\t{}\t{}\t{}'.format(bot['name'], bot['status'], bot['email'], bot['site'])) + print("{}\t{}\t{}\t{}".format(bot["name"], bot["status"], bot["email"], bot["site"])) def print_bots_pretty(bots: List[Any]) -> None: if len(bots) == 0: - print('ls: No bots found on the botfarm') + print("ls: No bots found on the botfarm") else: - print('ls: There are the following bots on the botfarm:') + print("ls: There are the following bots on the botfarm:") name_col_len, status_col_len, email_col_len, site_col_len = 25, 15, 35, 35 - row_format = '{0} {1} {2} {3}' + row_format = "{0} {1} {2} {3}" header = row_format.format( - 'NAME'.rjust(name_col_len), - 'STATUS'.rjust(status_col_len), - 'EMAIL'.rjust(email_col_len), - 'SITE'.rjust(site_col_len), + "NAME".rjust(name_col_len), + "STATUS".rjust(status_col_len), + "EMAIL".rjust(email_col_len), + "SITE".rjust(site_col_len), ) header_bottom = row_format.format( - '-' * name_col_len, - '-' * status_col_len, - '-' * email_col_len, - '-' * site_col_len, + "-" * name_col_len, + "-" * status_col_len, + "-" * email_col_len, + "-" * site_col_len, ) print(header) print(header_bottom) for bot in bots: row = row_format.format( - bot['name'].rjust(name_col_len), - bot['status'].rjust(status_col_len), - bot['email'].rjust(email_col_len), - bot['site'].rjust(site_col_len), + bot["name"].rjust(name_col_len), + bot["status"].rjust(status_col_len), + bot["email"].rjust(email_col_len), + bot["site"].rjust(site_col_len), ) print(row) @@ -297,52 +297,52 @@ To list user's bots, use: """ parser = argparse.ArgumentParser(usage=usage) - parser.add_argument('command', help='Command to run.') - parser.add_argument('botname', nargs='?', help='Name of bot to operate on.') + parser.add_argument("command", help="Command to run.") + parser.add_argument("botname", nargs="?", help="Name of bot to operate on.") parser.add_argument( - '--server', - '-s', - metavar='SERVERURL', - default=os.environ.get('SERVER', ''), - help='Url of the Zulip Botfarm server.', + "--server", + "-s", + metavar="SERVERURL", + default=os.environ.get("SERVER", ""), + help="Url of the Zulip Botfarm server.", ) parser.add_argument( - '--token', '-t', default=os.environ.get('TOKEN', ''), help='Deploy Token for the Botfarm.' + "--token", "-t", default=os.environ.get("TOKEN", ""), help="Deploy Token for the Botfarm." ) - parser.add_argument('--path', '-p', help='Path to the bot directory.') - parser.add_argument('--config', '-c', help='Path to the zuliprc file.') + parser.add_argument("--path", "-p", help="Path to the bot directory.") + parser.add_argument("--config", "-c", help="Path to the zuliprc file.") parser.add_argument( - '--main', '-m', help='Path to the bot\'s main file, relative to the bot\'s directory.' + "--main", "-m", help="Path to the bot's main file, relative to the bot's directory." ) - parser.add_argument('--lines', '-l', help='Number of lines in log required.') + parser.add_argument("--lines", "-l", help="Number of lines in log required.") parser.add_argument( - '--format', '-f', action='store_true', help='Print user\'s bots in human readable format' + "--format", "-f", action="store_true", help="Print user's bots in human readable format" ) options = parser.parse_args() if not options.command: - print('tools/deploy: No command specified.') + print("tools/deploy: No command specified.") sys.exit(1) - if not options.botname and options.command not in ['ls']: - print('tools/deploy: No bot name specified. Please specify a name like \'my-custom-bot\'') + if not options.botname and options.command not in ["ls"]: + print("tools/deploy: No bot name specified. Please specify a name like 'my-custom-bot'") sys.exit(1) commands = { - 'pack': pack, - 'upload': upload, - 'clean': clean, - 'prepare': prepare, - 'process': process, - 'start': start, - 'stop': stop, - 'log': log, - 'delete': delete, - 'ls': list_bots, + "pack": pack, + "upload": upload, + "clean": clean, + "prepare": prepare, + "process": process, + "start": start, + "stop": stop, + "log": log, + "delete": delete, + "ls": list_bots, } if options.command in commands: commands[options.command](options) else: - print('tools/deploy: No command \'{}\' found.'.format(options.command)) + print("tools/deploy: No command '{}' found.".format(options.command)) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/tools/gitlint-rules.py b/tools/gitlint-rules.py index 2b53054..709372b 100644 --- a/tools/gitlint-rules.py +++ b/tools/gitlint-rules.py @@ -11,246 +11,246 @@ from gitlint.rules import CommitMessageTitle, LineRule, RuleViolation # License: MIT # Ref: fit_commit/validators/tense.rb WORD_SET = { - 'adds', - 'adding', - 'added', - 'allows', - 'allowing', - 'allowed', - 'amends', - 'amending', - 'amended', - 'bumps', - 'bumping', - 'bumped', - 'calculates', - 'calculating', - 'calculated', - 'changes', - 'changing', - 'changed', - 'cleans', - 'cleaning', - 'cleaned', - 'commits', - 'committing', - 'committed', - 'corrects', - 'correcting', - 'corrected', - 'creates', - 'creating', - 'created', - 'darkens', - 'darkening', - 'darkened', - 'disables', - 'disabling', - 'disabled', - 'displays', - 'displaying', - 'displayed', - 'documents', - 'documenting', - 'documented', - 'drys', - 'drying', - 'dryed', - 'ends', - 'ending', - 'ended', - 'enforces', - 'enforcing', - 'enforced', - 'enqueues', - 'enqueuing', - 'enqueued', - 'extracts', - 'extracting', - 'extracted', - 'finishes', - 'finishing', - 'finished', - 'fixes', - 'fixing', - 'fixed', - 'formats', - 'formatting', - 'formatted', - 'guards', - 'guarding', - 'guarded', - 'handles', - 'handling', - 'handled', - 'hides', - 'hiding', - 'hid', - 'increases', - 'increasing', - 'increased', - 'ignores', - 'ignoring', - 'ignored', - 'implements', - 'implementing', - 'implemented', - 'improves', - 'improving', - 'improved', - 'keeps', - 'keeping', - 'kept', - 'kills', - 'killing', - 'killed', - 'makes', - 'making', - 'made', - 'merges', - 'merging', - 'merged', - 'moves', - 'moving', - 'moved', - 'permits', - 'permitting', - 'permitted', - 'prevents', - 'preventing', - 'prevented', - 'pushes', - 'pushing', - 'pushed', - 'rebases', - 'rebasing', - 'rebased', - 'refactors', - 'refactoring', - 'refactored', - 'removes', - 'removing', - 'removed', - 'renames', - 'renaming', - 'renamed', - 'reorders', - 'reordering', - 'reordered', - 'replaces', - 'replacing', - 'replaced', - 'requires', - 'requiring', - 'required', - 'restores', - 'restoring', - 'restored', - 'sends', - 'sending', - 'sent', - 'sets', - 'setting', - 'separates', - 'separating', - 'separated', - 'shows', - 'showing', - 'showed', - 'simplifies', - 'simplifying', - 'simplified', - 'skips', - 'skipping', - 'skipped', - 'sorts', - 'sorting', - 'speeds', - 'speeding', - 'sped', - 'starts', - 'starting', - 'started', - 'supports', - 'supporting', - 'supported', - 'takes', - 'taking', - 'took', - 'testing', - 'tested', # 'tests' excluded to reduce false negative - 'truncates', - 'truncating', - 'truncated', - 'updates', - 'updating', - 'updated', - 'uses', - 'using', - 'used', + "adds", + "adding", + "added", + "allows", + "allowing", + "allowed", + "amends", + "amending", + "amended", + "bumps", + "bumping", + "bumped", + "calculates", + "calculating", + "calculated", + "changes", + "changing", + "changed", + "cleans", + "cleaning", + "cleaned", + "commits", + "committing", + "committed", + "corrects", + "correcting", + "corrected", + "creates", + "creating", + "created", + "darkens", + "darkening", + "darkened", + "disables", + "disabling", + "disabled", + "displays", + "displaying", + "displayed", + "documents", + "documenting", + "documented", + "drys", + "drying", + "dryed", + "ends", + "ending", + "ended", + "enforces", + "enforcing", + "enforced", + "enqueues", + "enqueuing", + "enqueued", + "extracts", + "extracting", + "extracted", + "finishes", + "finishing", + "finished", + "fixes", + "fixing", + "fixed", + "formats", + "formatting", + "formatted", + "guards", + "guarding", + "guarded", + "handles", + "handling", + "handled", + "hides", + "hiding", + "hid", + "increases", + "increasing", + "increased", + "ignores", + "ignoring", + "ignored", + "implements", + "implementing", + "implemented", + "improves", + "improving", + "improved", + "keeps", + "keeping", + "kept", + "kills", + "killing", + "killed", + "makes", + "making", + "made", + "merges", + "merging", + "merged", + "moves", + "moving", + "moved", + "permits", + "permitting", + "permitted", + "prevents", + "preventing", + "prevented", + "pushes", + "pushing", + "pushed", + "rebases", + "rebasing", + "rebased", + "refactors", + "refactoring", + "refactored", + "removes", + "removing", + "removed", + "renames", + "renaming", + "renamed", + "reorders", + "reordering", + "reordered", + "replaces", + "replacing", + "replaced", + "requires", + "requiring", + "required", + "restores", + "restoring", + "restored", + "sends", + "sending", + "sent", + "sets", + "setting", + "separates", + "separating", + "separated", + "shows", + "showing", + "showed", + "simplifies", + "simplifying", + "simplified", + "skips", + "skipping", + "skipped", + "sorts", + "sorting", + "speeds", + "speeding", + "sped", + "starts", + "starting", + "started", + "supports", + "supporting", + "supported", + "takes", + "taking", + "took", + "testing", + "tested", # 'tests' excluded to reduce false negative + "truncates", + "truncating", + "truncated", + "updates", + "updating", + "updated", + "uses", + "using", + "used", } imperative_forms = [ - 'add', - 'allow', - 'amend', - 'bump', - 'calculate', - 'change', - 'clean', - 'commit', - 'correct', - 'create', - 'darken', - 'disable', - 'display', - 'document', - 'dry', - 'end', - 'enforce', - 'enqueue', - 'extract', - 'finish', - 'fix', - 'format', - 'guard', - 'handle', - 'hide', - 'ignore', - 'implement', - 'improve', - 'increase', - 'keep', - 'kill', - 'make', - 'merge', - 'move', - 'permit', - 'prevent', - 'push', - 'rebase', - 'refactor', - 'remove', - 'rename', - 'reorder', - 'replace', - 'require', - 'restore', - 'send', - 'separate', - 'set', - 'show', - 'simplify', - 'skip', - 'sort', - 'speed', - 'start', - 'support', - 'take', - 'test', - 'truncate', - 'update', - 'use', + "add", + "allow", + "amend", + "bump", + "calculate", + "change", + "clean", + "commit", + "correct", + "create", + "darken", + "disable", + "display", + "document", + "dry", + "end", + "enforce", + "enqueue", + "extract", + "finish", + "fix", + "format", + "guard", + "handle", + "hide", + "ignore", + "implement", + "improve", + "increase", + "keep", + "kill", + "make", + "merge", + "move", + "permit", + "prevent", + "push", + "rebase", + "refactor", + "remove", + "rename", + "reorder", + "replace", + "require", + "restore", + "send", + "separate", + "set", + "show", + "simplify", + "skip", + "sort", + "speed", + "start", + "support", + "take", + "test", + "truncate", + "update", + "use", ] imperative_forms.sort() @@ -260,8 +260,8 @@ def head_binary_search(key: str, words: List[str]) -> str: 3 characters.""" # Edge case: 'disable' and 'display' have the same 3 starting letters. - if key in ['displays', 'displaying', 'displayed']: - return 'display' + if key in ["displays", "displaying", "displayed"]: + return "display" lower = 0 upper = len(words) - 1 @@ -292,7 +292,7 @@ class ImperativeMood(LineRule): target = CommitMessageTitle error_msg = ( - 'The first word in commit title should be in imperative mood ' + "The first word in commit title should be in imperative mood " '("{word}" -> "{imperative}"): "{title}"' ) @@ -300,7 +300,7 @@ class ImperativeMood(LineRule): violations = [] # Ignore the section tag (ie `
: .`) - words = line.split(': ', 1)[-1].split() + words = line.split(": ", 1)[-1].split() first_word = words[0].lower() if first_word in WORD_SET: diff --git a/tools/lint b/tools/lint index ca67aab..61e8935 100755 --- a/tools/lint +++ b/tools/lint @@ -9,7 +9,7 @@ from custom_check import non_py_rules, python_rules EXCLUDED_FILES = [ # This is an external file that doesn't comply with our codestyle - 'zulip/integrations/perforce/git_p4.py', + "zulip/integrations/perforce/git_p4.py", ] @@ -21,21 +21,21 @@ def run() -> None: linter_config = LinterConfig(args) by_lang = linter_config.list_files( - file_types=['py', 'sh', 'json', 'md', 'txt'], exclude=EXCLUDED_FILES + file_types=["py", "sh", "json", "md", "txt"], exclude=EXCLUDED_FILES ) linter_config.external_linter( - 'mypy', - [sys.executable, 'tools/run-mypy'], - ['py'], + "mypy", + [sys.executable, "tools/run-mypy"], + ["py"], pass_targets=False, description="Static type checker for Python (config: mypy.ini)", ) linter_config.external_linter( - 'flake8', ['flake8'], ['py'], description="Standard Python linter (config: .flake8)" + "flake8", ["flake8"], ["py"], description="Standard Python linter (config: .flake8)" ) linter_config.external_linter( - 'gitlint', ['tools/lint-commits'], description="Git Lint for commit messages" + "gitlint", ["tools/lint-commits"], description="Git Lint for commit messages" ) @linter_config.lint @@ -55,5 +55,5 @@ def run() -> None: linter_config.do_lint() -if __name__ == '__main__': +if __name__ == "__main__": run() diff --git a/tools/provision b/tools/provision index 83bb73d..b110f36 100755 --- a/tools/provision +++ b/tools/provision @@ -7,13 +7,13 @@ import subprocess import sys CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) -ZULIP_BOTS_DIR = os.path.join(CURRENT_DIR, '..', 'zulip_bots') +ZULIP_BOTS_DIR = os.path.join(CURRENT_DIR, "..", "zulip_bots") sys.path.append(ZULIP_BOTS_DIR) -red = '\033[91m' -green = '\033[92m' -end_format = '\033[0m' -bold = '\033[1m' +red = "\033[91m" +green = "\033[92m" +end_format = "\033[0m" +bold = "\033[1m" def main(): @@ -23,25 +23,25 @@ Creates a Python virtualenv. Its Python version is equal to the Python version this command is executed with.""" parser = argparse.ArgumentParser(usage=usage) parser.add_argument( - '--python-interpreter', - '-p', - metavar='PATH_TO_PYTHON_INTERPRETER', + "--python-interpreter", + "-p", + metavar="PATH_TO_PYTHON_INTERPRETER", default=os.path.abspath(sys.executable), - help='Path to the Python interpreter to use when provisioning.', + help="Path to the Python interpreter to use when provisioning.", ) parser.add_argument( - '--force', '-f', action='store_true', help='create venv even with outdated Python version.' + "--force", "-f", action="store_true", help="create venv even with outdated Python version." ) options = parser.parse_args() - base_dir = os.path.abspath(os.path.join(__file__, '..', '..')) + base_dir = os.path.abspath(os.path.join(__file__, "..", "..")) py_version_output = subprocess.check_output( - [options.python_interpreter, '--version'], stderr=subprocess.STDOUT, universal_newlines=True + [options.python_interpreter, "--version"], stderr=subprocess.STDOUT, universal_newlines=True ) # The output has the format "Python 1.2.3" - py_version_list = py_version_output.split()[1].split('.') + py_version_list = py_version_output.split()[1].split(".") py_version = tuple(int(num) for num in py_version_list[0:2]) - venv_name = 'zulip-api-py{}-venv'.format(py_version[0]) + venv_name = "zulip-api-py{}-venv".format(py_version[0]) if py_version <= (3, 1) and (not options.force): print( @@ -53,7 +53,7 @@ the Python version this command is executed with.""" venv_dir = os.path.join(base_dir, venv_name) if not os.path.isdir(venv_dir): try: - return_code = subprocess.call([options.python_interpreter, '-m', 'venv', venv_dir]) + return_code = subprocess.call([options.python_interpreter, "-m", "venv", venv_dir]) except OSError: print( "{red}Installation with venv failed. Probable errors are: " @@ -77,34 +77,34 @@ the Python version this command is executed with.""" else: print("Virtualenv already exists.") - if os.path.isdir(os.path.join(venv_dir, 'Scripts')): + if os.path.isdir(os.path.join(venv_dir, "Scripts")): # POSIX compatibility layer and Linux environment emulation for Windows # venv uses /Scripts instead of /bin on Windows cmd and Power Shell. # Read https://docs.python.org/3/library/venv.html - venv_exec_dir = 'Scripts' + venv_exec_dir = "Scripts" else: - venv_exec_dir = 'bin' + venv_exec_dir = "bin" # On OS X, ensure we use the virtualenv version of the python binary for # future subprocesses instead of the version that this script was launched with. See # https://stackoverflow.com/questions/26323852/whats-the-meaning-of-pyvenv-launcher-environment-variable - if '__PYVENV_LAUNCHER__' in os.environ: - del os.environ['__PYVENV_LAUNCHER__'] + if "__PYVENV_LAUNCHER__" in os.environ: + del os.environ["__PYVENV_LAUNCHER__"] # In order to install all required packages for the venv, `pip` needs to be executed by # the venv's Python interpreter. `--prefix venv_dir` ensures that all modules are installed # in the right place. def install_dependencies(requirements_filename): - pip_path = os.path.join(venv_dir, venv_exec_dir, 'pip') + pip_path = os.path.join(venv_dir, venv_exec_dir, "pip") # We first install a modern version of pip that supports --prefix - subprocess.call([pip_path, 'install', 'pip>=9.0']) + subprocess.call([pip_path, "install", "pip>=9.0"]) if subprocess.call( [ pip_path, - 'install', - '--prefix', + "install", + "--prefix", venv_dir, - '-r', + "-r", os.path.join(base_dir, requirements_filename), ] ): @@ -114,7 +114,7 @@ the Python version this command is executed with.""" ) ) - install_dependencies('requirements.txt') + install_dependencies("requirements.txt") # Install all requirements for all bots. get_bot_paths() # has requirements that must be satisfied prior to calling @@ -127,15 +127,15 @@ the Python version this command is executed with.""" relative_path = os.path.join(*path_split) install_dependencies(relative_path) - print(green + 'Success!' + end_format) + print(green + "Success!" + end_format) - activate_command = os.path.join(base_dir, venv_dir, venv_exec_dir, 'activate') + activate_command = os.path.join(base_dir, venv_dir, venv_exec_dir, "activate") # We make the path look like a Unix path, because most Windows users # are likely to be running in a bash shell. - activate_command = activate_command.replace(os.sep, '/') - print('\nRun the following to enter into the virtualenv:\n') - print(bold + ' source ' + activate_command + end_format + "\n") + activate_command = activate_command.replace(os.sep, "/") + print("\nRun the following to enter into the virtualenv:\n") + print(bold + " source " + activate_command + end_format + "\n") -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/tools/release-packages b/tools/release-packages index 85b20d3..73bd45a 100755 --- a/tools/release-packages +++ b/tools/release-packages @@ -25,7 +25,7 @@ def cd(newdir): def _generate_dist(dist_type, setup_file, package_name, setup_args): - message = 'Generating {dist_type} for {package_name}.'.format( + message = "Generating {dist_type} for {package_name}.".format( dist_type=dist_type, package_name=package_name, ) @@ -35,7 +35,7 @@ def _generate_dist(dist_type, setup_file, package_name, setup_args): with cd(setup_dir): setuptools.sandbox.run_setup(setup_file, setup_args) - message = '{dist_type} for {package_name} generated under {dir}.\n'.format( + message = "{dist_type} for {package_name} generated under {dir}.\n".format( dist_type=dist_type, package_name=package_name, dir=setup_dir, @@ -45,13 +45,13 @@ def _generate_dist(dist_type, setup_file, package_name, setup_args): def generate_bdist_wheel(setup_file, package_name, universal=False): if universal: - _generate_dist('bdist_wheel', setup_file, package_name, ['bdist_wheel', '--universal']) + _generate_dist("bdist_wheel", setup_file, package_name, ["bdist_wheel", "--universal"]) else: - _generate_dist('bdist_wheel', setup_file, package_name, ['bdist_wheel']) + _generate_dist("bdist_wheel", setup_file, package_name, ["bdist_wheel"]) def twine_upload(dist_dirs): - message = 'Uploading distributions under the following directories:' + message = "Uploading distributions under the following directories:" print(crayons.green(message, bold=True)) for dist_dir in dist_dirs: print(crayons.yellow(dist_dir)) @@ -59,14 +59,14 @@ def twine_upload(dist_dirs): def cleanup(package_dir): - build_dir = os.path.join(package_dir, 'build') - temp_dir = os.path.join(package_dir, 'temp') - dist_dir = os.path.join(package_dir, 'dist') - egg_info = os.path.join(package_dir, '{}.egg-info'.format(os.path.basename(package_dir))) + build_dir = os.path.join(package_dir, "build") + temp_dir = os.path.join(package_dir, "temp") + dist_dir = os.path.join(package_dir, "dist") + egg_info = os.path.join(package_dir, "{}.egg-info".format(os.path.basename(package_dir))) def _rm_if_it_exists(directory): if os.path.isdir(directory): - print(crayons.green('Removing {}/*'.format(directory), bold=True)) + print(crayons.green("Removing {}/*".format(directory), bold=True)) shutil.rmtree(directory) _rm_if_it_exists(build_dir) @@ -77,11 +77,11 @@ def cleanup(package_dir): def set_variable(fp, variable, value): fh, temp_abs_path = tempfile.mkstemp() - with os.fdopen(fh, 'w') as new_file, open(fp) as old_file: + with os.fdopen(fh, "w") as new_file, open(fp) as old_file: for line in old_file: if line.startswith(variable): if isinstance(value, bool): - template = '{variable} = {value}\n' + template = "{variable} = {value}\n" else: template = '{variable} = "{value}"\n' new_file.write(template.format(variable=variable, value=value)) @@ -91,22 +91,22 @@ def set_variable(fp, variable, value): os.remove(fp) shutil.move(temp_abs_path, fp) - message = 'Set {variable} in {fp} to {value}.'.format(fp=fp, variable=variable, value=value) + message = "Set {variable} in {fp} to {value}.".format(fp=fp, variable=variable, value=value) print(crayons.white(message, bold=True)) def update_requirements_in_zulip_repo(zulip_repo_dir, version, hash_or_tag): - common = os.path.join(zulip_repo_dir, 'requirements', 'common.in') - prod = os.path.join(zulip_repo_dir, 'requirements', 'prod.txt') - dev = os.path.join(zulip_repo_dir, 'requirements', 'dev.txt') + common = os.path.join(zulip_repo_dir, "requirements", "common.in") + prod = os.path.join(zulip_repo_dir, "requirements", "prod.txt") + dev = os.path.join(zulip_repo_dir, "requirements", "dev.txt") def _edit_reqs_file(reqs, zulip_bots_line, zulip_line): fh, temp_abs_path = tempfile.mkstemp() - with os.fdopen(fh, 'w') as new_file, open(reqs) as old_file: + with os.fdopen(fh, "w") as new_file, open(reqs) as old_file: for line in old_file: - if 'python-zulip-api' in line and 'zulip==' in line: + if "python-zulip-api" in line and "zulip==" in line: new_file.write(zulip_line) - elif 'python-zulip-api' in line and 'zulip_bots' in line: + elif "python-zulip-api" in line and "zulip_bots" in line: new_file.write(zulip_bots_line) else: new_file.write(line) @@ -114,10 +114,10 @@ def update_requirements_in_zulip_repo(zulip_repo_dir, version, hash_or_tag): os.remove(reqs) shutil.move(temp_abs_path, reqs) - url_zulip = 'git+https://github.com/zulip/python-zulip-api.git@{tag}#egg={name}=={version}_git&subdirectory={name}\n' - url_zulip_bots = 'git+https://github.com/zulip/python-zulip-api.git@{tag}#egg={name}=={version}+git&subdirectory={name}\n' - zulip_bots_line = url_zulip_bots.format(tag=hash_or_tag, name='zulip_bots', version=version) - zulip_line = url_zulip.format(tag=hash_or_tag, name='zulip', version=version) + url_zulip = "git+https://github.com/zulip/python-zulip-api.git@{tag}#egg={name}=={version}_git&subdirectory={name}\n" + url_zulip_bots = "git+https://github.com/zulip/python-zulip-api.git@{tag}#egg={name}=={version}+git&subdirectory={name}\n" + zulip_bots_line = url_zulip_bots.format(tag=hash_or_tag, name="zulip_bots", version=version) + zulip_line = url_zulip.format(tag=hash_or_tag, name="zulip", version=version) _edit_reqs_file(prod, zulip_bots_line, zulip_line) _edit_reqs_file(dev, zulip_bots_line, zulip_line) @@ -127,11 +127,11 @@ def update_requirements_in_zulip_repo(zulip_repo_dir, version, hash_or_tag): _edit_reqs_file( common, - editable_zulip_bots.format(tag=hash_or_tag, name='zulip_bots', version=version), - editable_zulip.format(tag=hash_or_tag, name='zulip', version=version), + editable_zulip_bots.format(tag=hash_or_tag, name="zulip_bots", version=version), + editable_zulip.format(tag=hash_or_tag, name="zulip", version=version), ) - message = 'Updated zulip API package requirements in the main repo.' + message = "Updated zulip API package requirements in the main repo." print(crayons.white(message, bold=True)) @@ -177,39 +177,39 @@ And you're done! Congrats! parser = argparse.ArgumentParser(usage=usage) parser.add_argument( - '--cleanup', - '-c', - action='store_true', + "--cleanup", + "-c", + action="store_true", default=False, - help='Remove build directories (dist/, build/, egg-info/, etc).', + help="Remove build directories (dist/, build/, egg-info/, etc).", ) parser.add_argument( - '--build', - '-b', - metavar='VERSION_NUM', + "--build", + "-b", + metavar="VERSION_NUM", help=( - 'Build sdists and wheels for all packages with the' - 'specified version number.' - ' sdists and wheels are stored in /dist/*.' + "Build sdists and wheels for all packages with the" + "specified version number." + " sdists and wheels are stored in /dist/*." ), ) parser.add_argument( - '--release', - '-r', - action='store_true', + "--release", + "-r", + action="store_true", default=False, - help='Upload the packages to PyPA using twine.', + help="Upload the packages to PyPA using twine.", ) - subparsers = parser.add_subparsers(dest='subcommand') + subparsers = parser.add_subparsers(dest="subcommand") parser_main_repo = subparsers.add_parser( - 'update-main-repo', help='Update the zulip/requirements/* in the main zulip repo.' + "update-main-repo", help="Update the zulip/requirements/* in the main zulip repo." ) - parser_main_repo.add_argument('repo', metavar='PATH_TO_ZULIP_DIR') - parser_main_repo.add_argument('version', metavar='version number of the packages') - parser_main_repo.add_argument('--hash', metavar='COMMIT_HASH') + parser_main_repo.add_argument("repo", metavar="PATH_TO_ZULIP_DIR") + parser_main_repo.add_argument("version", metavar="version number of the packages") + parser_main_repo.add_argument("--hash", metavar="COMMIT_HASH") return parser.parse_args() @@ -217,7 +217,7 @@ And you're done! Congrats! def main(): options = parse_args() - glob_pattern = os.path.join(REPO_DIR, '*', 'setup.py') + glob_pattern = os.path.join(REPO_DIR, "*", "setup.py") setup_py_files = glob.glob(glob_pattern) if options.cleanup: @@ -230,30 +230,30 @@ def main(): for package_dir in package_dirs: cleanup(package_dir) - zulip_init = os.path.join(REPO_DIR, 'zulip', 'zulip', '__init__.py') - set_variable(zulip_init, '__version__', options.build) - bots_setup = os.path.join(REPO_DIR, 'zulip_bots', 'setup.py') - set_variable(bots_setup, 'ZULIP_BOTS_VERSION', options.build) - set_variable(bots_setup, 'IS_PYPA_PACKAGE', True) - botserver_setup = os.path.join(REPO_DIR, 'zulip_botserver', 'setup.py') - set_variable(botserver_setup, 'ZULIP_BOTSERVER_VERSION', options.build) + zulip_init = os.path.join(REPO_DIR, "zulip", "zulip", "__init__.py") + set_variable(zulip_init, "__version__", options.build) + bots_setup = os.path.join(REPO_DIR, "zulip_bots", "setup.py") + set_variable(bots_setup, "ZULIP_BOTS_VERSION", options.build) + set_variable(bots_setup, "IS_PYPA_PACKAGE", True) + botserver_setup = os.path.join(REPO_DIR, "zulip_botserver", "setup.py") + set_variable(botserver_setup, "ZULIP_BOTSERVER_VERSION", options.build) for setup_file in setup_py_files: package_name = os.path.basename(os.path.dirname(setup_file)) generate_bdist_wheel(setup_file, package_name) - set_variable(bots_setup, 'IS_PYPA_PACKAGE', False) + set_variable(bots_setup, "IS_PYPA_PACKAGE", False) if options.release: - dist_dirs = glob.glob(os.path.join(REPO_DIR, '*', 'dist', '*')) + dist_dirs = glob.glob(os.path.join(REPO_DIR, "*", "dist", "*")) twine_upload(dist_dirs) - if options.subcommand == 'update-main-repo': + if options.subcommand == "update-main-repo": if options.hash: update_requirements_in_zulip_repo(options.repo, options.version, options.hash) else: update_requirements_in_zulip_repo(options.repo, options.version, options.version) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/tools/review b/tools/review index acffa9a..fff44e8 100755 --- a/tools/review +++ b/tools/review @@ -5,63 +5,63 @@ import sys def exit(message: str) -> None: - print('PROBLEM!') + print("PROBLEM!") print(message) sys.exit(1) def run(command: str) -> None: - print('\n>>> ' + command) + print("\n>>> " + command) subprocess.check_call(command.split()) def check_output(command: str) -> str: - return subprocess.check_output(command.split()).decode('ascii') + return subprocess.check_output(command.split()).decode("ascii") def get_git_branch() -> str: - command = 'git rev-parse --abbrev-ref HEAD' + command = "git rev-parse --abbrev-ref HEAD" output = check_output(command) return output.strip() def check_git_pristine() -> None: - command = 'git status --porcelain' + command = "git status --porcelain" output = check_output(command) if output.strip(): - exit('Git is not pristine:\n' + output) + exit("Git is not pristine:\n" + output) def ensure_on_clean_master() -> None: branch = get_git_branch() - if branch != 'master': - exit('You are still on a feature branch: %s' % (branch,)) + if branch != "master": + exit("You are still on a feature branch: %s" % (branch,)) check_git_pristine() - run('git fetch upstream master') - run('git rebase upstream/master') + run("git fetch upstream master") + run("git rebase upstream/master") def create_pull_branch(pull_id: int) -> None: - run('git fetch upstream pull/%d/head' % (pull_id,)) - run('git checkout -B review-%s FETCH_HEAD' % (pull_id,)) - run('git rebase upstream/master') - run('git log upstream/master.. --oneline') - run('git diff upstream/master.. --name-status') + run("git fetch upstream pull/%d/head" % (pull_id,)) + run("git checkout -B review-%s FETCH_HEAD" % (pull_id,)) + run("git rebase upstream/master") + run("git log upstream/master.. --oneline") + run("git diff upstream/master.. --name-status") print() - print('PR: %d' % (pull_id,)) - print(subprocess.check_output(['git', 'log', 'HEAD~..', '--pretty=format:Author: %an'])) + print("PR: %d" % (pull_id,)) + print(subprocess.check_output(["git", "log", "HEAD~..", "--pretty=format:Author: %an"])) def review_pr() -> None: try: pull_id = int(sys.argv[1]) except Exception: - exit('please provide an integer pull request id') + exit("please provide an integer pull request id") ensure_on_clean_master() create_pull_branch(pull_id) -if __name__ == '__main__': +if __name__ == "__main__": review_pr() diff --git a/tools/run-mypy b/tools/run-mypy index f4f671e..dbaf03c 100755 --- a/tools/run-mypy +++ b/tools/run-mypy @@ -104,54 +104,54 @@ force_include = [ parser = argparse.ArgumentParser(description="Run mypy on files tracked by git.") parser.add_argument( - 'targets', - nargs='*', + "targets", + nargs="*", default=[], help="""files and directories to include in the result. If this is not specified, the current directory is used""", ) parser.add_argument( - '-m', '--modified', action='store_true', default=False, help='list only modified files' + "-m", "--modified", action="store_true", default=False, help="list only modified files" ) parser.add_argument( - '-a', - '--all', - dest='all', - action='store_true', + "-a", + "--all", + dest="all", + action="store_true", default=False, help="""run mypy on all python files, ignoring the exclude list. This is useful if you have to find out which files fail mypy check.""", ) parser.add_argument( - '--no-disallow-untyped-defs', - dest='disallow_untyped_defs', - action='store_false', + "--no-disallow-untyped-defs", + dest="disallow_untyped_defs", + action="store_false", default=True, help="""Don't throw errors when functions are not annotated""", ) parser.add_argument( - '--scripts-only', - dest='scripts_only', - action='store_true', + "--scripts-only", + dest="scripts_only", + action="store_true", default=False, help="""Only type check extensionless python scripts""", ) parser.add_argument( - '--warn-unused-ignores', - dest='warn_unused_ignores', - action='store_true', + "--warn-unused-ignores", + dest="warn_unused_ignores", + action="store_true", default=False, help="""Use the --warn-unused-ignores flag with mypy""", ) parser.add_argument( - '--no-ignore-missing-imports', - dest='ignore_missing_imports', - action='store_false', + "--no-ignore-missing-imports", + dest="ignore_missing_imports", + action="store_false", default=True, help="""Don't use the --ignore-missing-imports flag with mypy""", ) parser.add_argument( - '--quick', action='store_true', default=False, help="""Use the --quick flag with mypy""" + "--quick", action="store_true", default=False, help="""Use the --quick flag with mypy""" ) args = parser.parse_args() @@ -163,10 +163,10 @@ files_dict = cast( Dict[str, List[str]], lister.list_files( targets=args.targets, - ftypes=['py', 'pyi'], + ftypes=["py", "pyi"], use_shebang=True, modified_only=args.modified, - exclude=exclude + ['stubs'], + exclude=exclude + ["stubs"], group_by_ftype=True, extless_only=args.scripts_only, ), @@ -174,18 +174,18 @@ files_dict = cast( for inpath in force_include: try: - ext = os.path.splitext(inpath)[1].split('.')[1] + ext = os.path.splitext(inpath)[1].split(".")[1] except IndexError: - ext = 'py' # type: str + ext = "py" # type: str files_dict[ext].append(inpath) -pyi_files = set(files_dict['pyi']) +pyi_files = set(files_dict["pyi"]) python_files = [ - fpath for fpath in files_dict['py'] if not fpath.endswith('.py') or fpath + 'i' not in pyi_files + fpath for fpath in files_dict["py"] if not fpath.endswith(".py") or fpath + "i" not in pyi_files ] repo_python_files = OrderedDict( - [('zulip', []), ('zulip_bots', []), ('zulip_botserver', []), ('tools', [])] + [("zulip", []), ("zulip_bots", []), ("zulip_botserver", []), ("tools", [])] ) for file_path in python_files: repo = PurePath(file_path).parts[0] diff --git a/tools/server_lib/test_handler.py b/tools/server_lib/test_handler.py index 2752142..fc235e0 100644 --- a/tools/server_lib/test_handler.py +++ b/tools/server_lib/test_handler.py @@ -13,45 +13,45 @@ os.chdir(os.path.dirname(TOOLS_DIR)) def handle_input_and_run_tests_for_package(package_name, path_list): parser = argparse.ArgumentParser(description="Run tests for {}.".format(package_name)) parser.add_argument( - '--coverage', - nargs='?', + "--coverage", + nargs="?", const=True, default=False, - help='compute test coverage (--coverage combine to combine with previous reports)', + help="compute test coverage (--coverage combine to combine with previous reports)", ) parser.add_argument( - '--pytest', '-p', default=False, action='store_true', help="run tests with pytest" + "--pytest", "-p", default=False, action="store_true", help="run tests with pytest" ) parser.add_argument( - '--verbose', - '-v', + "--verbose", + "-v", default=False, - action='store_true', - help='show verbose output (with pytest)', + action="store_true", + help="show verbose output (with pytest)", ) options = parser.parse_args() - test_session_title = ' Running tests for {} '.format(package_name) - header = test_session_title.center(shutil.get_terminal_size().columns, '#') + test_session_title = " Running tests for {} ".format(package_name) + header = test_session_title.center(shutil.get_terminal_size().columns, "#") print(header) if options.coverage: import coverage cov = coverage.Coverage(config_file="tools/.coveragerc") - if options.coverage == 'combine': + if options.coverage == "combine": cov.load() cov.start() if options.pytest: - location_to_run_in = os.path.join(TOOLS_DIR, '..', *path_list) - paths_to_test = ['.'] + location_to_run_in = os.path.join(TOOLS_DIR, "..", *path_list) + paths_to_test = ["."] pytest_options = [ - '-s', # show output from tests; this hides the progress bar though - '-x', # stop on first test failure - '--ff', # runs last failure first + "-s", # show output from tests; this hides the progress bar though + "-x", # stop on first test failure + "--ff", # runs last failure first ] - pytest_options += ['-v'] if options.verbose else [] + pytest_options += ["-v"] if options.verbose else [] os.chdir(location_to_run_in) result = pytest.main(paths_to_test + pytest_options) if result != 0: diff --git a/tools/test-bots b/tools/test-bots index 5a8bfa4..ba83289 100755 --- a/tools/test-bots +++ b/tools/test-bots @@ -32,35 +32,35 @@ the tests for xkcd and wikipedia bots): parser = argparse.ArgumentParser(description=description) parser.add_argument( - 'bots_to_test', - metavar='bot', - nargs='*', + "bots_to_test", + metavar="bot", + nargs="*", default=[], - help='specific bots to test (default is all)', + help="specific bots to test (default is all)", ) parser.add_argument( - '--coverage', - nargs='?', + "--coverage", + nargs="?", const=True, default=False, - help='compute test coverage (--coverage combine to combine with previous reports)', + help="compute test coverage (--coverage combine to combine with previous reports)", ) - parser.add_argument('--exclude', metavar='bot', nargs='*', default=[], help='bot(s) to exclude') + parser.add_argument("--exclude", metavar="bot", nargs="*", default=[], help="bot(s) to exclude") parser.add_argument( - '--error-on-no-init', + "--error-on-no-init", default=False, action="store_true", help="whether to exit if a bot has tests which won't run due to no __init__.py", ) parser.add_argument( - '--pytest', '-p', default=False, action='store_true', help="run tests with pytest" + "--pytest", "-p", default=False, action="store_true", help="run tests with pytest" ) parser.add_argument( - '--verbose', - '-v', + "--verbose", + "-v", default=False, - action='store_true', - help='show verbose output (with pytest)', + action="store_true", + help="show verbose output (with pytest)", ) return parser.parse_args() @@ -69,8 +69,8 @@ def main(): TOOLS_DIR = os.path.dirname(os.path.abspath(__file__)) os.chdir(os.path.dirname(TOOLS_DIR)) sys.path.insert(0, TOOLS_DIR) - bots_dir = os.path.join(TOOLS_DIR, '..', 'zulip_bots/zulip_bots/bots') - glob_pattern = bots_dir + '/*/test_*.py' + bots_dir = os.path.join(TOOLS_DIR, "..", "zulip_bots/zulip_bots/bots") + glob_pattern = bots_dir + "/*/test_*.py" test_modules = glob.glob(glob_pattern) # get only the names of bots that have tests @@ -82,7 +82,7 @@ def main(): import coverage cov = coverage.Coverage(config_file="tools/.coveragerc") - if options.coverage == 'combine': + if options.coverage == "combine": cov.load() cov.start() @@ -96,14 +96,14 @@ def main(): bots_to_test = {bot for bot in specified_bots if bot not in options.exclude} if options.pytest: - excluded_bots = ['merels'] + excluded_bots = ["merels"] pytest_bots_to_test = sorted([bot for bot in bots_to_test if bot not in excluded_bots]) pytest_options = [ - '-s', # show output from tests; this hides the progress bar though - '-x', # stop on first test failure - '--ff', # runs last failure first + "-s", # show output from tests; this hides the progress bar though + "-x", # stop on first test failure + "--ff", # runs last failure first ] - pytest_options += ['-v'] if options.verbose else [] + pytest_options += ["-v"] if options.verbose else [] os.chdir(bots_dir) result = pytest.main(pytest_bots_to_test + pytest_options) if result != 0: @@ -142,5 +142,5 @@ def main(): print("HTML report saved under directory 'htmlcov'.") -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/tools/test-botserver b/tools/test-botserver index ee9025b..7f0614f 100755 --- a/tools/test-botserver +++ b/tools/test-botserver @@ -2,5 +2,5 @@ from server_lib.test_handler import handle_input_and_run_tests_for_package -if __name__ == '__main__': - handle_input_and_run_tests_for_package('Botserver', ['zulip_botserver']) +if __name__ == "__main__": + handle_input_and_run_tests_for_package("Botserver", ["zulip_botserver"]) diff --git a/tools/test-lib b/tools/test-lib index 4ee0e88..01fc959 100755 --- a/tools/test-lib +++ b/tools/test-lib @@ -2,5 +2,5 @@ from server_lib.test_handler import handle_input_and_run_tests_for_package -if __name__ == '__main__': - handle_input_and_run_tests_for_package('Bot library', ['zulip_bots', 'zulip_bots', 'tests']) +if __name__ == "__main__": + handle_input_and_run_tests_for_package("Bot library", ["zulip_bots", "zulip_bots", "tests"]) diff --git a/tools/test-zulip b/tools/test-zulip index 4d22d74..490e73d 100755 --- a/tools/test-zulip +++ b/tools/test-zulip @@ -2,5 +2,5 @@ from server_lib.test_handler import handle_input_and_run_tests_for_package -if __name__ == '__main__': - handle_input_and_run_tests_for_package('API', ['zulip']) +if __name__ == "__main__": + handle_input_and_run_tests_for_package("API", ["zulip"]) diff --git a/zulip/integrations/bridge_between_zulips/run-interrealm-bridge b/zulip/integrations/bridge_between_zulips/run-interrealm-bridge index 6a8d5cb..562c168 100755 --- a/zulip/integrations/bridge_between_zulips/run-interrealm-bridge +++ b/zulip/integrations/bridge_between_zulips/run-interrealm-bridge @@ -71,10 +71,10 @@ if __name__ == "__main__": all topics within the stream are mirrored as-is without translation. """ - sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + sys.path.append(os.path.join(os.path.dirname(__file__), "..")) parser = argparse.ArgumentParser(usage=usage) - parser.add_argument('--stream', action='store_true', help="", default=False) + parser.add_argument("--stream", action="store_true", help="", default=False) args = parser.parse_args() options = interrealm_bridge_config.config diff --git a/zulip/integrations/bridge_with_irc/irc-mirror.py b/zulip/integrations/bridge_with_irc/irc-mirror.py index de81307..2f30a13 100755 --- a/zulip/integrations/bridge_with_irc/irc-mirror.py +++ b/zulip/integrations/bridge_with_irc/irc-mirror.py @@ -29,13 +29,13 @@ if __name__ == "__main__": parser = zulip.add_default_arguments( argparse.ArgumentParser(usage=usage), allow_provisioning=True ) - parser.add_argument('--irc-server', default=None) - parser.add_argument('--port', default=6667) - parser.add_argument('--nick-prefix', default=None) - parser.add_argument('--channel', default=None) - parser.add_argument('--stream', default="general") - parser.add_argument('--topic', default="IRC") - parser.add_argument('--nickserv-pw', default='') + parser.add_argument("--irc-server", default=None) + parser.add_argument("--port", default=6667) + parser.add_argument("--nick-prefix", default=None) + parser.add_argument("--channel", default=None) + parser.add_argument("--stream", default="general") + parser.add_argument("--topic", default="IRC") + parser.add_argument("--nickserv-pw", default="") options = parser.parse_args() # Setting the client to irc_mirror is critical for this to work diff --git a/zulip/integrations/bridge_with_irc/irc_mirror_backend.py b/zulip/integrations/bridge_with_irc/irc_mirror_backend.py index 5ee98a8..c9ba7e1 100644 --- a/zulip/integrations/bridge_with_irc/irc_mirror_backend.py +++ b/zulip/integrations/bridge_with_irc/irc_mirror_backend.py @@ -18,7 +18,7 @@ class IRCBot(irc.bot.SingleServerIRCBot): channel: irc.bot.Channel, nickname: str, server: str, - nickserv_password: str = '', + nickserv_password: str = "", port: int = 6667, ) -> None: self.channel = channel # type: irc.bot.Channel @@ -61,8 +61,8 @@ class IRCBot(irc.bot.SingleServerIRCBot): def on_welcome(self, c: ServerConnection, e: Event) -> None: if len(self.nickserv_password) > 0: - msg = 'identify %s' % (self.nickserv_password,) - c.privmsg('NickServ', msg) + msg = "identify %s" % (self.nickserv_password,) + c.privmsg("NickServ", msg) c.join(self.channel) def forward_to_irc(msg: Dict[str, Any]) -> None: diff --git a/zulip/integrations/bridge_with_matrix/matrix_bridge.py b/zulip/integrations/bridge_with_matrix/matrix_bridge.py index 6c89145..79df2f2 100644 --- a/zulip/integrations/bridge_with_matrix/matrix_bridge.py +++ b/zulip/integrations/bridge_with_matrix/matrix_bridge.py @@ -17,8 +17,8 @@ from requests.exceptions import MissingSchema import zulip -GENERAL_NETWORK_USERNAME_REGEX = '@_?[a-zA-Z0-9]+_([a-zA-Z0-9-_]+):[a-zA-Z0-9.]+' -MATRIX_USERNAME_REGEX = '@([a-zA-Z0-9-_]+):matrix.org' +GENERAL_NETWORK_USERNAME_REGEX = "@_?[a-zA-Z0-9]+_([a-zA-Z0-9-_]+):[a-zA-Z0-9.]+" +MATRIX_USERNAME_REGEX = "@([a-zA-Z0-9-_]+):matrix.org" # change these templates to change the format of displayed message ZULIP_MESSAGE_TEMPLATE = "**{username}**: {message}" @@ -77,10 +77,10 @@ def matrix_to_zulip( """ content = get_message_content_from_event(event, no_noise) - zulip_bot_user = '@%s:matrix.org' % (matrix_config['username'],) + zulip_bot_user = "@%s:matrix.org" % (matrix_config["username"],) # We do this to identify the messages generated from Zulip -> Matrix # and we make sure we don't forward it again to the Zulip stream. - not_from_zulip_bot = event['sender'] != zulip_bot_user + not_from_zulip_bot = event["sender"] != zulip_bot_user if not_from_zulip_bot and content: try: @@ -95,31 +95,31 @@ def matrix_to_zulip( except Exception as exception: # XXX This should be more specific # Generally raised when user is forbidden raise Bridge_ZulipFatalException(exception) - if result['result'] != 'success': + if result["result"] != "success": # Generally raised when API key is invalid - raise Bridge_ZulipFatalException(result['msg']) + raise Bridge_ZulipFatalException(result["msg"]) return _matrix_to_zulip def get_message_content_from_event(event: Dict[str, Any], no_noise: bool) -> Optional[str]: - irc_nick = shorten_irc_nick(event['sender']) - if event['type'] == "m.room.member": + irc_nick = shorten_irc_nick(event["sender"]) + if event["type"] == "m.room.member": if no_noise: return None # Join and leave events can be noisy. They are ignored by default. # To enable these events pass `no_noise` as `False` as the script argument - if event['membership'] == "join": + if event["membership"] == "join": content = ZULIP_MESSAGE_TEMPLATE.format(username=irc_nick, message="joined") - elif event['membership'] == "leave": + elif event["membership"] == "leave": content = ZULIP_MESSAGE_TEMPLATE.format(username=irc_nick, message="quit") - elif event['type'] == "m.room.message": - if event['content']['msgtype'] == "m.text" or event['content']['msgtype'] == "m.emote": + elif event["type"] == "m.room.message": + if event["content"]["msgtype"] == "m.text" or event["content"]["msgtype"] == "m.emote": content = ZULIP_MESSAGE_TEMPLATE.format( - username=irc_nick, message=event['content']['body'] + username=irc_nick, message=event["content"]["body"] ) else: - content = event['type'] + content = event["type"] return content @@ -147,7 +147,7 @@ def zulip_to_matrix(config: Dict[str, Any], room: Any) -> Callable[[Dict[str, An """ message_valid = check_zulip_message_validity(msg, config) if message_valid: - matrix_username = msg["sender_full_name"].replace(' ', '') + matrix_username = msg["sender_full_name"].replace(" ", "") matrix_text = MATRIX_MESSAGE_TEMPLATE.format( username=matrix_username, message=msg["content"] ) @@ -186,25 +186,25 @@ def generate_parser() -> argparse.ArgumentParser: description=description, formatter_class=argparse.RawTextHelpFormatter ) parser.add_argument( - '-c', '--config', required=False, help="Path to the config file for the bridge." + "-c", "--config", required=False, help="Path to the config file for the bridge." ) parser.add_argument( - '--write-sample-config', - metavar='PATH', - dest='sample_config', + "--write-sample-config", + metavar="PATH", + dest="sample_config", help="Generate a configuration template at the specified location.", ) parser.add_argument( - '--from-zuliprc', - metavar='ZULIPRC', - dest='zuliprc', + "--from-zuliprc", + metavar="ZULIPRC", + dest="zuliprc", help="Optional path to zuliprc file for bot, when using --write-sample-config", ) parser.add_argument( - '--show-join-leave', - dest='no_noise', + "--show-join-leave", + dest="no_noise", default=True, - action='store_false', + action="store_false", help="Enable IRC join/leave events.", ) return parser @@ -218,7 +218,7 @@ def read_configuration(config_file: str) -> Dict[str, Dict[str, str]]: except configparser.Error as exception: raise Bridge_ConfigException(str(exception)) - if set(config.sections()) != {'matrix', 'zulip'}: + if set(config.sections()) != {"matrix", "zulip"}: raise Bridge_ConfigException("Please ensure the configuration has zulip & matrix sections.") # TODO Could add more checks for configuration content here @@ -235,25 +235,25 @@ def write_sample_config(target_path: str, zuliprc: Optional[str]) -> None: sample_dict = OrderedDict( ( ( - 'matrix', + "matrix", OrderedDict( ( - ('host', 'https://matrix.org'), - ('username', 'username'), - ('password', 'password'), - ('room_id', '#zulip:matrix.org'), + ("host", "https://matrix.org"), + ("username", "username"), + ("password", "password"), + ("room_id", "#zulip:matrix.org"), ) ), ), ( - 'zulip', + "zulip", OrderedDict( ( - ('email', 'glitch-bot@chat.zulip.org'), - ('api_key', 'aPiKeY'), - ('site', 'https://chat.zulip.org'), - ('stream', 'test here'), - ('topic', 'matrix'), + ("email", "glitch-bot@chat.zulip.org"), + ("api_key", "aPiKeY"), + ("site", "https://chat.zulip.org"), + ("stream", "test here"), + ("topic", "matrix"), ) ), ), @@ -272,13 +272,13 @@ def write_sample_config(target_path: str, zuliprc: Optional[str]) -> None: # Can add more checks for validity of zuliprc file here - sample_dict['zulip']['email'] = zuliprc_config['api']['email'] - sample_dict['zulip']['site'] = zuliprc_config['api']['site'] - sample_dict['zulip']['api_key'] = zuliprc_config['api']['key'] + sample_dict["zulip"]["email"] = zuliprc_config["api"]["email"] + sample_dict["zulip"]["site"] = zuliprc_config["api"]["site"] + sample_dict["zulip"]["api_key"] = zuliprc_config["api"]["key"] sample = configparser.ConfigParser() sample.read_dict(sample_dict) - with open(target_path, 'w') as target: + with open(target_path, "w") as target: sample.write(target) @@ -357,5 +357,5 @@ def main() -> None: backoff.fail() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/zulip/integrations/bridge_with_matrix/test_matrix.py b/zulip/integrations/bridge_with_matrix/test_matrix.py index 8cac64b..43cd51f 100644 --- a/zulip/integrations/bridge_with_matrix/test_matrix.py +++ b/zulip/integrations/bridge_with_matrix/test_matrix.py @@ -59,7 +59,7 @@ class MatrixBridgeScriptTests(TestCase): usage = "usage: {} [-h]".format(script_file) description = "Script to bridge" self.assertIn(usage, output_lines[0]) - blank_lines = [num for num, line in enumerate(output_lines) if line == ''] + blank_lines = [num for num, line in enumerate(output_lines) if line == ""] # There should be blank lines in the output self.assertTrue(blank_lines) # There should be finite output @@ -79,9 +79,9 @@ class MatrixBridgeScriptTests(TestCase): def test_write_sample_config_from_zuliprc(self) -> None: zuliprc_template = ["[api]", "email={email}", "key={key}", "site={site}"] zulip_params = { - 'email': 'foo@bar', - 'key': 'some_api_key', - 'site': 'https://some.chat.serverplace', + "email": "foo@bar", + "key": "some_api_key", + "site": "https://some.chat.serverplace", } with new_temp_dir() as tempdir: path = os.path.join(tempdir, sample_config_path) @@ -103,9 +103,9 @@ class MatrixBridgeScriptTests(TestCase): with open(path) as sample_file: sample_lines = [line.strip() for line in sample_file.readlines()] expected_lines = sample_config_text.split("\n") - expected_lines[7] = 'email = {}'.format(zulip_params['email']) - expected_lines[8] = 'api_key = {}'.format(zulip_params['key']) - expected_lines[9] = 'site = {}'.format(zulip_params['site']) + expected_lines[7] = "email = {}".format(zulip_params["email"]) + expected_lines[8] = "api_key = {}".format(zulip_params["key"]) + expected_lines[9] = "site = {}".format(zulip_params["site"]) self.assertEqual(sample_lines, expected_lines[:-1]) def test_detect_zuliprc_does_not_exist(self) -> None: @@ -131,31 +131,31 @@ class MatrixBridgeZulipToMatrixTests(TestCase): valid_msg = dict( sender_email="John@Smith.smith", # must not be equal to config:email type="stream", # Can only mirror Zulip streams - display_recipient=valid_zulip_config['stream'], - subject=valid_zulip_config['topic'], + display_recipient=valid_zulip_config["stream"], + subject=valid_zulip_config["topic"], ) def test_zulip_message_validity_success(self) -> None: zulip_config = self.valid_zulip_config msg = self.valid_msg # Ensure the test inputs are valid for success - assert msg['sender_email'] != zulip_config['email'] + assert msg["sender_email"] != zulip_config["email"] self.assertTrue(check_zulip_message_validity(msg, zulip_config)) def test_zulip_message_validity_failure(self) -> None: zulip_config = self.valid_zulip_config - msg_wrong_stream = dict(self.valid_msg, display_recipient='foo') + msg_wrong_stream = dict(self.valid_msg, display_recipient="foo") self.assertFalse(check_zulip_message_validity(msg_wrong_stream, zulip_config)) - msg_wrong_topic = dict(self.valid_msg, subject='foo') + msg_wrong_topic = dict(self.valid_msg, subject="foo") self.assertFalse(check_zulip_message_validity(msg_wrong_topic, zulip_config)) msg_not_stream = dict(self.valid_msg, type="private") self.assertFalse(check_zulip_message_validity(msg_not_stream, zulip_config)) - msg_from_bot = dict(self.valid_msg, sender_email=zulip_config['email']) + msg_from_bot = dict(self.valid_msg, sender_email=zulip_config["email"]) self.assertFalse(check_zulip_message_validity(msg_from_bot, zulip_config)) def test_zulip_to_matrix(self) -> None: @@ -166,14 +166,14 @@ class MatrixBridgeZulipToMatrixTests(TestCase): msg = dict(self.valid_msg, sender_full_name="John Smith") expected = { - 'hi': '{} hi', - '*hi*': '{} *hi*', - '**hi**': '{} **hi**', + "hi": "{} hi", + "*hi*": "{} *hi*", + "**hi**": "{} **hi**", } for content in expected: send_msg(dict(msg, content=content)) for (method, params, _), expect in zip(room.method_calls, expected.values()): - self.assertEqual(method, 'send_text') - self.assertEqual(params[0], expect.format('')) + self.assertEqual(method, "send_text") + self.assertEqual(params[0], expect.format("")) diff --git a/zulip/integrations/bridge_with_slack/run-slack-bridge b/zulip/integrations/bridge_with_slack/run-slack-bridge index 31d67c6..a61cc6b 100755 --- a/zulip/integrations/bridge_with_slack/run-slack-bridge +++ b/zulip/integrations/bridge_with_slack/run-slack-bridge @@ -55,17 +55,17 @@ class SlackBridge: self.slack_webclient = slack_sdk.WebClient(token=self.slack_config["token"]) def wrap_slack_mention_with_bracket(self, zulip_msg: Dict[str, Any]) -> None: - words = zulip_msg["content"].split(' ') + words = zulip_msg["content"].split(" ") for w in words: - if w.startswith('@'): - zulip_msg["content"] = zulip_msg["content"].replace(w, '<' + w + '>') + if w.startswith("@"): + zulip_msg["content"] = zulip_msg["content"].replace(w, "<" + w + ">") def replace_slack_id_with_name(self, msg: Dict[str, Any]) -> None: - words = msg['text'].split(' ') + words = msg["text"].split(" ") for w in words: - if w.startswith('<@') and w.endswith('>'): + if w.startswith("<@") and w.endswith(">"): _id = w[2:-1] - msg['text'] = msg['text'].replace(_id, self.slack_id_to_name[_id]) + msg["text"] = msg["text"].replace(_id, self.slack_id_to_name[_id]) def zulip_to_slack(self) -> Callable[[Dict[str, Any]], None]: def _zulip_to_slack(msg: Dict[str, Any]) -> None: @@ -83,25 +83,25 @@ class SlackBridge: return _zulip_to_slack def run_slack_listener(self) -> None: - members = self.slack_webclient.users_list()['members'] + members = self.slack_webclient.users_list()["members"] # See also https://api.slack.com/changelog/2017-09-the-one-about-usernames self.slack_id_to_name = { u["id"]: u["profile"].get("display_name", u["profile"]["real_name"]) for u in members } self.slack_name_to_id = {v: k for k, v in self.slack_id_to_name.items()} - @RTMClient.run_on(event='message') + @RTMClient.run_on(event="message") def slack_to_zulip(**payload: Any) -> None: - msg = payload['data'] - if msg['channel'] != self.channel: + msg = payload["data"] + if msg["channel"] != self.channel: return - user_id = msg['user'] + user_id = msg["user"] user = self.slack_id_to_name[user_id] - from_bot = user == self.slack_config['username'] + from_bot = user == self.slack_config["username"] if from_bot: return self.replace_slack_id_with_name(msg) - content = ZULIP_MESSAGE_TEMPLATE.format(username=user, message=msg['text']) + content = ZULIP_MESSAGE_TEMPLATE.format(username=user, message=msg["text"]) msg_data = dict( type="stream", to=self.zulip_stream, subject=self.zulip_subject, content=content ) @@ -117,7 +117,7 @@ if __name__ == "__main__": the first realm to a channel in a Slack workspace. """ - sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + sys.path.append(os.path.join(os.path.dirname(__file__), "..")) parser = argparse.ArgumentParser(usage=usage) print("Starting slack mirroring bot") diff --git a/zulip/integrations/codebase/zulip_codebase_mirror b/zulip/integrations/codebase/zulip_codebase_mirror index d787b9c..fd9d717 100755 --- a/zulip/integrations/codebase/zulip_codebase_mirror +++ b/zulip/integrations/codebase/zulip_codebase_mirror @@ -44,7 +44,7 @@ client = zulip.Client( user_agent = "Codebase To Zulip Mirroring script (zulip-devel@googlegroups.com)" # find some form of JSON loader/dumper, with a preference order for speed. -json_implementations = ['ujson', 'cjson', 'simplejson', 'json'] +json_implementations = ["ujson", "cjson", "simplejson", "json"] while len(json_implementations): try: @@ -58,7 +58,7 @@ def make_api_call(path: str) -> Optional[List[Dict[str, Any]]]: response = requests.get( "https://api3.codebasehq.com/%s" % (path,), auth=(config.CODEBASE_API_USERNAME, config.CODEBASE_API_KEY), - params={'raw': 'True'}, + params={"raw": "True"}, headers={ "User-Agent": user_agent, "Content-Type": "application/json", @@ -86,36 +86,36 @@ def make_url(path: str) -> str: def handle_event(event: Dict[str, Any]) -> None: - event = event['event'] - event_type = event['type'] - actor_name = event['actor_name'] + event = event["event"] + event_type = event["type"] + actor_name = event["actor_name"] - raw_props = event.get('raw_properties', {}) + raw_props = event.get("raw_properties", {}) - project_link = raw_props.get('project_permalink') + project_link = raw_props.get("project_permalink") subject = None content = None - if event_type == 'repository_creation': + if event_type == "repository_creation": stream = config.ZULIP_COMMITS_STREAM_NAME - project_name = raw_props.get('name') - project_repo_type = raw_props.get('scm_type') + project_name = raw_props.get("name") + project_repo_type = raw_props.get("scm_type") url = make_url("projects/%s" % (project_link,)) scm = "of type %s" % (project_repo_type,) if project_repo_type else "" subject = "Repository %s Created" % (project_name,) content = "%s created a new repository %s [%s](%s)" % (actor_name, scm, project_name, url) - elif event_type == 'push': + elif event_type == "push": stream = config.ZULIP_COMMITS_STREAM_NAME - num_commits = raw_props.get('commits_count') - branch = raw_props.get('ref_name') - project = raw_props.get('project_name') - repo_link = raw_props.get('repository_permalink') - deleted_ref = raw_props.get('deleted_ref') - new_ref = raw_props.get('new_ref') + num_commits = raw_props.get("commits_count") + branch = raw_props.get("ref_name") + project = raw_props.get("project_name") + repo_link = raw_props.get("repository_permalink") + deleted_ref = raw_props.get("deleted_ref") + new_ref = raw_props.get("new_ref") subject = "Push to %s on %s" % (branch, project) @@ -130,20 +130,20 @@ def handle_event(event: Dict[str, Any]) -> None: branch, project, ) - for commit in raw_props.get('commits'): - ref = commit.get('ref') + for commit in raw_props.get("commits"): + ref = commit.get("ref") url = make_url( "projects/%s/repositories/%s/commit/%s" % (project_link, repo_link, ref) ) - message = commit.get('message') + message = commit.get("message") content += "* [%s](%s): %s\n" % (ref, url, message) - elif event_type == 'ticketing_ticket': + elif event_type == "ticketing_ticket": stream = config.ZULIP_TICKETS_STREAM_NAME - num = raw_props.get('number') - name = raw_props.get('subject') - assignee = raw_props.get('assignee') - priority = raw_props.get('priority') + num = raw_props.get("number") + name = raw_props.get("subject") + assignee = raw_props.get("assignee") + priority = raw_props.get("priority") url = make_url("projects/%s/tickets/%s" % (project_link, num)) if assignee is None: @@ -153,13 +153,13 @@ def handle_event(event: Dict[str, Any]) -> None: """%s created a new ticket [#%s](%s) priority **%s** assigned to %s:\n\n~~~ quote\n %s""" % (actor_name, num, url, priority, assignee, name) ) - elif event_type == 'ticketing_note': + elif event_type == "ticketing_note": stream = config.ZULIP_TICKETS_STREAM_NAME - num = raw_props.get('number') - name = raw_props.get('subject') - body = raw_props.get('content') - changes = raw_props.get('changes') + num = raw_props.get("number") + name = raw_props.get("subject") + body = raw_props.get("content") + changes = raw_props.get("changes") url = make_url("projects/%s/tickets/%s" % (project_link, num)) subject = "#%s: %s" % (num, name) @@ -173,33 +173,33 @@ def handle_event(event: Dict[str, Any]) -> None: body, ) - if 'status_id' in changes: - status_change = changes.get('status_id') + if "status_id" in changes: + status_change = changes.get("status_id") content += "Status changed from **%s** to **%s**\n\n" % ( status_change[0], status_change[1], ) - elif event_type == 'ticketing_milestone': + elif event_type == "ticketing_milestone": stream = config.ZULIP_TICKETS_STREAM_NAME - name = raw_props.get('name') - identifier = raw_props.get('identifier') + name = raw_props.get("name") + identifier = raw_props.get("identifier") url = make_url("projects/%s/milestone/%s" % (project_link, identifier)) subject = name content = "%s created a new milestone [%s](%s)" % (actor_name, name, url) - elif event_type == 'comment': + elif event_type == "comment": stream = config.ZULIP_COMMITS_STREAM_NAME - comment = raw_props.get('content') - commit = raw_props.get('commit_ref') + comment = raw_props.get("content") + commit = raw_props.get("commit_ref") # If there's a commit id, it's a comment to a commit if commit: - repo_link = raw_props.get('repository_permalink') + repo_link = raw_props.get("repository_permalink") url = make_url( - 'projects/%s/repositories/%s/commit/%s' % (project_link, repo_link, commit) + "projects/%s/repositories/%s/commit/%s" % (project_link, repo_link, commit) ) subject = "%s commented on %s" % (actor_name, commit) @@ -223,14 +223,14 @@ def handle_event(event: Dict[str, Any]) -> None: else: content = "%s posted:\n\n~~~ quote\n%s\n~~~" % (actor_name, comment_content) - elif event_type == 'deployment': + elif event_type == "deployment": stream = config.ZULIP_COMMITS_STREAM_NAME - start_ref = raw_props.get('start_ref') - end_ref = raw_props.get('end_ref') - environment = raw_props.get('environment') - servers = raw_props.get('servers') - repo_link = raw_props.get('repository_permalink') + start_ref = raw_props.get("start_ref") + end_ref = raw_props.get("end_ref") + environment = raw_props.get("environment") + servers = raw_props.get("servers") + repo_link = raw_props.get("repository_permalink") start_ref_url = make_url( "projects/%s/repositories/%s/commit/%s" % (project_link, repo_link, start_ref) @@ -259,30 +259,30 @@ def handle_event(event: Dict[str, Any]) -> None: ", ".join(["`%s`" % (server,) for server in servers]) ) - elif event_type == 'named_tree': + elif event_type == "named_tree": # Docs say named_tree type used for new/deleting branches and tags, # but experimental testing showed that they were all sent as 'push' events pass - elif event_type == 'wiki_page': + elif event_type == "wiki_page": logging.warn("Wiki page notifications not yet implemented") - elif event_type == 'sprint_creation': + elif event_type == "sprint_creation": logging.warn("Sprint notifications not yet implemented") - elif event_type == 'sprint_ended': + elif event_type == "sprint_ended": logging.warn("Sprint notifications not yet implemented") else: logging.info("Unknown event type %s, ignoring!" % (event_type,)) if subject and content: if len(subject) > 60: - subject = subject[:57].rstrip() + '...' + subject = subject[:57].rstrip() + "..." res = client.send_message( {"type": "stream", "to": stream, "subject": subject, "content": content} ) - if res['result'] == 'success': - logging.info("Successfully sent Zulip with id: %s" % (res['id'],)) + if res["result"] == "success": + logging.info("Successfully sent Zulip with id: %s" % (res["id"],)) else: - logging.warn("Failed to send Zulip: %s %s" % (res['result'], res['msg'])) + logging.warn("Failed to send Zulip: %s %s" % (res["result"], res["msg"])) # the main run loop for this mirror script @@ -295,7 +295,7 @@ def run_mirror() -> None: try: with open(config.RESUME_FILE) as f: timestamp = f.read() - if timestamp == '': + if timestamp == "": since = default_since() else: since = datetime.fromtimestamp(float(timestamp), tz=pytz.utc) @@ -310,7 +310,7 @@ def run_mirror() -> None: if events is not None: sleepInterval = 1 for event in events[::-1]: - timestamp = event.get('event', {}).get('timestamp', '') + timestamp = event.get("event", {}).get("timestamp", "") event_date = dateutil.parser.parse(timestamp) if event_date > since: handle_event(event) @@ -322,7 +322,7 @@ def run_mirror() -> None: time.sleep(sleepInterval) except KeyboardInterrupt: - open(config.RESUME_FILE, 'w').write(since.strftime("%s")) + open(config.RESUME_FILE, "w").write(since.strftime("%s")) logging.info("Shutting down Codebase mirror") diff --git a/zulip/integrations/git/post-receive b/zulip/integrations/git/post-receive index 624d843..2e91d10 100755 --- a/zulip/integrations/git/post-receive +++ b/zulip/integrations/git/post-receive @@ -43,19 +43,19 @@ def git_repository_name() -> Text: def git_commit_range(oldrev: str, newrev: str) -> str: log_cmd = ["git", "log", "--reverse", "--pretty=%aE %H %s", "%s..%s" % (oldrev, newrev)] - commits = '' + commits = "" for ln in subprocess.check_output(log_cmd, universal_newlines=True).splitlines(): author_email, commit_id, subject = ln.split(None, 2) if hasattr(config, "format_commit_message"): commits += config.format_commit_message(author_email, subject, commit_id) else: - commits += '!avatar(%s) %s\n' % (author_email, subject) + commits += "!avatar(%s) %s\n" % (author_email, subject) return commits def send_bot_message(oldrev: str, newrev: str, refname: str) -> None: repo_name = git_repository_name() - branch = refname.replace('refs/heads/', '') + branch = refname.replace("refs/heads/", "") destination = config.commit_notice_destination(repo_name, branch, newrev) if destination is None: # Don't forward the notice anywhere @@ -65,30 +65,30 @@ def send_bot_message(oldrev: str, newrev: str, refname: str) -> None: old_head = oldrev[:12] if ( - oldrev == '0000000000000000000000000000000000000000' - or newrev == '0000000000000000000000000000000000000000' + oldrev == "0000000000000000000000000000000000000000" + or newrev == "0000000000000000000000000000000000000000" ): # New branch pushed or old branch removed - added = '' - removed = '' + added = "" + removed = "" else: added = git_commit_range(oldrev, newrev) removed = git_commit_range(newrev, oldrev) - if oldrev == '0000000000000000000000000000000000000000': - message = '`%s` was pushed to new branch `%s`' % (new_head, branch) - elif newrev == '0000000000000000000000000000000000000000': - message = 'branch `%s` was removed (was `%s`)' % (branch, old_head) + if oldrev == "0000000000000000000000000000000000000000": + message = "`%s` was pushed to new branch `%s`" % (new_head, branch) + elif newrev == "0000000000000000000000000000000000000000": + message = "branch `%s` was removed (was `%s`)" % (branch, old_head) elif removed: - message = '`%s` was pushed to `%s`, **REMOVING**:\n\n%s' % (new_head, branch, removed) + message = "`%s` was pushed to `%s`, **REMOVING**:\n\n%s" % (new_head, branch, removed) if added: - message += '\n**and adding**:\n\n' + added - message += '\n**A HISTORY REWRITE HAS OCCURRED!**' - message += '\n@everyone: Please check your local branches to deal with this.' + message += "\n**and adding**:\n\n" + added + message += "\n**A HISTORY REWRITE HAS OCCURRED!**" + message += "\n@everyone: Please check your local branches to deal with this." elif added: - message = '`%s` was deployed to `%s` with:\n\n%s' % (new_head, branch, added) + message = "`%s` was deployed to `%s` with:\n\n%s" % (new_head, branch, added) else: - message = '`%s` was pushed to `%s`... but nothing changed?' % (new_head, branch) + message = "`%s` was pushed to `%s`... but nothing changed?" % (new_head, branch) message_data = { "type": "stream", diff --git a/zulip/integrations/git/zulip_git_config.py b/zulip/integrations/git/zulip_git_config.py index 282d156..1fd9e13 100644 --- a/zulip/integrations/git/zulip_git_config.py +++ b/zulip/integrations/git/zulip_git_config.py @@ -2,7 +2,7 @@ from typing import Dict, Optional, Text # Name of the stream to send notifications to, default is "commits" -STREAM_NAME = 'commits' +STREAM_NAME = "commits" # Change these values to configure authentication for the plugin ZULIP_USER = "git-bot@example.com" @@ -37,7 +37,7 @@ def commit_notice_destination(repo: Text, branch: Text, commit: Text) -> Optiona # # return '!avatar(%s) [%s](https://example.com/commits/%s)\n' % (author, subject, commit_id) def format_commit_message(author: Text, subject: Text, commit_id: Text) -> Text: - return '!avatar(%s) %s\n' % (author, subject) + return "!avatar(%s) %s\n" % (author, subject) ## If properly installed, the Zulip API should be in your import diff --git a/zulip/integrations/google/get-google-credentials b/zulip/integrations/google/get-google-credentials index bbe286e..558366a 100644 --- a/zulip/integrations/google/get-google-credentials +++ b/zulip/integrations/google/get-google-credentials @@ -18,12 +18,12 @@ except ImportError: # at zulip/bots/gcal/ # NOTE: When adding more scopes, add them after the previous one in the same field, with a space # seperating them. -SCOPES = 'https://www.googleapis.com/auth/calendar.readonly' +SCOPES = "https://www.googleapis.com/auth/calendar.readonly" # This file contains the information that google uses to figure out which application is requesting # this client's data. -CLIENT_SECRET_FILE = 'client_secret.json' -APPLICATION_NAME = 'Zulip Calendar Bot' -HOME_DIR = os.path.expanduser('~') +CLIENT_SECRET_FILE = "client_secret.json" +APPLICATION_NAME = "Zulip Calendar Bot" +HOME_DIR = os.path.expanduser("~") def get_credentials() -> client.Credentials: @@ -36,7 +36,7 @@ def get_credentials() -> client.Credentials: Credentials, the obtained credential. """ - credential_path = os.path.join(HOME_DIR, 'google-credentials.json') + credential_path = os.path.join(HOME_DIR, "google-credentials.json") store = Storage(credential_path) credentials = store.get() @@ -50,7 +50,7 @@ def get_credentials() -> client.Credentials: credentials = tools.run_flow(flow, store, flags) else: # Needed only for compatibility with Python 2.6 credentials = tools.run(flow, store) - print('Storing credentials to ' + credential_path) + print("Storing credentials to " + credential_path) get_credentials() diff --git a/zulip/integrations/google/google-calendar b/zulip/integrations/google/google-calendar index ca13670..d19b737 100755 --- a/zulip/integrations/google/google-calendar +++ b/zulip/integrations/google/google-calendar @@ -20,15 +20,15 @@ from oauth2client.file import Storage try: from googleapiclient import discovery except ImportError: - logging.exception('Install google-api-python-client') + logging.exception("Install google-api-python-client") -sys.path.append(os.path.join(os.path.dirname(__file__), '../../')) +sys.path.append(os.path.join(os.path.dirname(__file__), "../../")) import zulip -SCOPES = 'https://www.googleapis.com/auth/calendar.readonly' -CLIENT_SECRET_FILE = 'client_secret.json' -APPLICATION_NAME = 'Zulip' -HOME_DIR = os.path.expanduser('~') +SCOPES = "https://www.googleapis.com/auth/calendar.readonly" +CLIENT_SECRET_FILE = "client_secret.json" +APPLICATION_NAME = "Zulip" +HOME_DIR = os.path.expanduser("~") # Our cached view of the calendar, updated periodically. events = [] # type: List[Tuple[int, datetime.datetime, str]] @@ -61,28 +61,28 @@ google-calendar --calendar calendarID@example.calendar.google.com parser.add_argument( - '--interval', - dest='interval', + "--interval", + dest="interval", default=30, type=int, - action='store', - help='Minutes before event for reminder [default: 30]', - metavar='MINUTES', + action="store", + help="Minutes before event for reminder [default: 30]", + metavar="MINUTES", ) parser.add_argument( - '--calendar', - dest='calendarID', - default='primary', + "--calendar", + dest="calendarID", + default="primary", type=str, - action='store', - help='Calendar ID for the calendar you want to receive reminders from.', + action="store", + help="Calendar ID for the calendar you want to receive reminders from.", ) options = parser.parse_args() if not (options.zulip_email): - parser.error('You must specify --user') + parser.error("You must specify --user") zulip_client = zulip.init_from_options(options) @@ -98,14 +98,14 @@ def get_credentials() -> client.Credentials: Credentials, the obtained credential. """ try: - credential_path = os.path.join(HOME_DIR, 'google-credentials.json') + credential_path = os.path.join(HOME_DIR, "google-credentials.json") store = Storage(credential_path) credentials = store.get() return credentials except client.Error: - logging.exception('Error while trying to open the `google-credentials.json` file.') + logging.exception("Error while trying to open the `google-credentials.json` file.") except OSError: logging.error("Run the get-google-credentials script from this directory first.") @@ -115,7 +115,7 @@ def populate_events() -> Optional[None]: credentials = get_credentials() creds = credentials.authorize(httplib2.Http()) - service = discovery.build('calendar', 'v3', http=creds) + service = discovery.build("calendar", "v3", http=creds) now = datetime.datetime.now(pytz.utc).isoformat() feed = ( @@ -125,7 +125,7 @@ def populate_events() -> Optional[None]: timeMin=now, maxResults=5, singleEvents=True, - orderBy='startTime', + orderBy="startTime", ) .execute() ) @@ -174,10 +174,10 @@ def send_reminders() -> Optional[None]: key = (id, start) if key not in sent: if start.hour == 0 and start.minute == 0: - line = '%s is today.' % (summary,) + line = "%s is today." % (summary,) else: - line = '%s starts at %s' % (summary, start.strftime('%H:%M')) - print('Sending reminder:', line) + line = "%s starts at %s" % (summary, start.strftime("%H:%M")) + print("Sending reminder:", line) messages.append(line) keys.add(key) @@ -185,12 +185,12 @@ def send_reminders() -> Optional[None]: return if len(messages) == 1: - message = 'Reminder: ' + messages[0] + message = "Reminder: " + messages[0] else: - message = 'Reminder:\n\n' + '\n'.join('* ' + m for m in messages) + message = "Reminder:\n\n" + "\n".join("* " + m for m in messages) zulip_client.send_message( - dict(type='private', to=options.zulip_email, sender=options.zulip_email, content=message) + dict(type="private", to=options.zulip_email, sender=options.zulip_email, content=message) ) sent.update(keys) diff --git a/zulip/integrations/hg/zulip_changegroup.py b/zulip/integrations/hg/zulip_changegroup.py index 4bd9f7e..99b5e47 100755 --- a/zulip/integrations/hg/zulip_changegroup.py +++ b/zulip/integrations/hg/zulip_changegroup.py @@ -94,7 +94,7 @@ def send_zulip( def get_config(ui: ui, item: str) -> str: try: # config returns configuration value. - return ui.config('zulip', item) + return ui.config("zulip", item) except IndexError: ui.warn("Zulip: Could not find required item {} in hg config.".format(item)) sys.exit(1) diff --git a/zulip/integrations/jabber/jabber_mirror_backend.py b/zulip/integrations/jabber/jabber_mirror_backend.py index d9b6cae..0d29c6b 100755 --- a/zulip/integrations/jabber/jabber_mirror_backend.py +++ b/zulip/integrations/jabber/jabber_mirror_backend.py @@ -62,7 +62,7 @@ def stream_to_room(stream: str) -> str: def jid_to_zulip(jid: JID) -> str: - suffix = '' + suffix = "" if not jid.username.endswith("-bot"): suffix = options.zulip_email_suffix return "%s%s@%s" % (jid.username, suffix, options.zulip_domain) @@ -94,10 +94,10 @@ class JabberToZulipBot(ClientXMPP): self.zulip = None self.use_ipv6 = False - self.register_plugin('xep_0045') # Jabber chatrooms - self.register_plugin('xep_0199') # XMPP Ping + self.register_plugin("xep_0045") # Jabber chatrooms + self.register_plugin("xep_0199") # XMPP Ping - def set_zulip_client(self, zulipToJabberClient: 'ZulipToJabberBot') -> None: + def set_zulip_client(self, zulipToJabberClient: "ZulipToJabberBot") -> None: self.zulipToJabber = zulipToJabberClient def session_start(self, event: Dict[str, Any]) -> None: @@ -112,7 +112,7 @@ class JabberToZulipBot(ClientXMPP): logging.debug("Joining " + room) self.rooms.add(room) muc_jid = JID(local=room, domain=options.conference_domain) - xep0045 = self.plugin['xep_0045'] + xep0045 = self.plugin["xep_0045"] try: xep0045.joinMUC(muc_jid, self.nick, wait=True) except InvalidJID: @@ -137,7 +137,7 @@ class JabberToZulipBot(ClientXMPP): logging.debug("Leaving " + room) self.rooms.remove(room) muc_jid = JID(local=room, domain=options.conference_domain) - self.plugin['xep_0045'].leaveMUC(muc_jid, self.nick) + self.plugin["xep_0045"].leaveMUC(muc_jid, self.nick) def message(self, msg: JabberMessage) -> Any: try: @@ -152,7 +152,7 @@ class JabberToZulipBot(ClientXMPP): logging.exception("Error forwarding Jabber => Zulip") def private(self, msg: JabberMessage) -> None: - if options.mode == 'public' or msg['thread'] == '\u1FFFE': + if options.mode == "public" or msg["thread"] == "\u1FFFE": return sender = jid_to_zulip(msg["from"]) recipient = jid_to_zulip(msg["to"]) @@ -168,13 +168,13 @@ class JabberToZulipBot(ClientXMPP): logging.error(str(ret)) def group(self, msg: JabberMessage) -> None: - if options.mode == 'personal' or msg["thread"] == '\u1FFFE': + if options.mode == "personal" or msg["thread"] == "\u1FFFE": return subject = msg["subject"] if len(subject) == 0: subject = "(no topic)" - stream = room_to_stream(msg['from'].local) + stream = room_to_stream(msg["from"].local) sender_nick = msg.get_mucnick() if not sender_nick: # Messages from the room itself have no nickname. We should not try @@ -195,9 +195,9 @@ class JabberToZulipBot(ClientXMPP): logging.error(str(ret)) def nickname_to_jid(self, room: str, nick: str) -> JID: - jid = self.plugin['xep_0045'].getJidProperty(room, nick, "jid") - if jid is None or jid == '': - return JID(local=nick.replace(' ', ''), domain=self.boundjid.domain) + jid = self.plugin["xep_0045"].getJidProperty(room, nick, "jid") + if jid is None or jid == "": + return JID(local=nick.replace(" ", ""), domain=self.boundjid.domain) else: return jid @@ -211,59 +211,59 @@ class ZulipToJabberBot: self.jabber = client def process_event(self, event: Dict[str, Any]) -> None: - if event['type'] == 'message': + if event["type"] == "message": message = event["message"] - if message['sender_email'] != self.client.email: + if message["sender_email"] != self.client.email: return try: - if message['type'] == 'stream': + if message["type"] == "stream": self.stream_message(message) - elif message['type'] == 'private': + elif message["type"] == "private": self.private_message(message) except Exception: logging.exception("Exception forwarding Zulip => Jabber") - elif event['type'] == 'subscription': + elif event["type"] == "subscription": self.process_subscription(event) def stream_message(self, msg: Dict[str, str]) -> None: assert self.jabber is not None - stream = msg['display_recipient'] + stream = msg["display_recipient"] if not stream.endswith("/xmpp"): return room = stream_to_room(stream) jabber_recipient = JID(local=room, domain=options.conference_domain) outgoing = self.jabber.make_message( - mto=jabber_recipient, mbody=msg['content'], mtype='groupchat' + mto=jabber_recipient, mbody=msg["content"], mtype="groupchat" ) - outgoing['thread'] = '\u1FFFE' + outgoing["thread"] = "\u1FFFE" outgoing.send() def private_message(self, msg: Dict[str, Any]) -> None: assert self.jabber is not None - for recipient in msg['display_recipient']: + for recipient in msg["display_recipient"]: if recipient["email"] == self.client.email: continue if not recipient["is_mirror_dummy"]: continue - recip_email = recipient['email'] + recip_email = recipient["email"] jabber_recipient = zulip_to_jid(recip_email, self.jabber.boundjid.domain) outgoing = self.jabber.make_message( - mto=jabber_recipient, mbody=msg['content'], mtype='chat' + mto=jabber_recipient, mbody=msg["content"], mtype="chat" ) - outgoing['thread'] = '\u1FFFE' + outgoing["thread"] = "\u1FFFE" outgoing.send() def process_subscription(self, event: Dict[str, Any]) -> None: assert self.jabber is not None - if event['op'] == 'add': - streams = [s['name'].lower() for s in event['subscriptions']] + if event["op"] == "add": + streams = [s["name"].lower() for s in event["subscriptions"]] streams = [s for s in streams if s.endswith("/xmpp")] for stream in streams: self.jabber.join_muc(stream_to_room(stream)) - if event['op'] == 'remove': - streams = [s['name'].lower() for s in event['subscriptions']] + if event["op"] == "remove": + streams = [s["name"].lower() for s in event["subscriptions"]] streams = [s for s in streams if s.endswith("/xmpp")] for stream in streams: self.jabber.leave_muc(stream_to_room(stream)) @@ -277,14 +277,14 @@ def get_rooms(zulipToJabber: ZulipToJabberBot) -> List[str]: sys.exit("Could not get initial list of Zulip %s" % (key,)) return ret[key] - if options.mode == 'public': + if options.mode == "public": stream_infos = get_stream_infos("streams", zulipToJabber.client.get_streams) else: stream_infos = get_stream_infos("subscriptions", zulipToJabber.client.get_subscriptions) rooms = [] # type: List[str] for stream_info in stream_infos: - stream = stream_info['name'] + stream = stream_info["name"] if stream.endswith("/xmpp"): rooms.append(stream_to_room(stream)) return rooms @@ -295,20 +295,20 @@ def config_error(msg: str) -> None: sys.exit(2) -if __name__ == '__main__': +if __name__ == "__main__": parser = optparse.OptionParser( - epilog='''Most general and Jabber configuration options may also be specified in the + epilog="""Most general and Jabber configuration options may also be specified in the zulip configuration file under the jabber_mirror section (exceptions are noted in their help sections). Keys have the same name as options with hyphens replaced with underscores. Zulip configuration options go in the api section, -as normal.'''.replace( +as normal.""".replace( "\n", " " ) ) parser.add_option( - '--mode', + "--mode", default=None, - action='store', + action="store", help='''Which mode to run in. Valid options are "personal" and "public". In "personal" mode, the mirror uses an individual users' credentials and mirrors all messages they send on Zulip to Jabber and all private Jabber messages to @@ -319,33 +319,33 @@ user and mirrors messages sent to Jabber rooms to Zulip. Defaults to ), ) parser.add_option( - '--zulip-email-suffix', + "--zulip-email-suffix", default=None, - action='store', - help='''Add the specified suffix to the local part of email addresses constructed + action="store", + help="""Add the specified suffix to the local part of email addresses constructed from JIDs and nicks before sending requests to the Zulip server, and remove the suffix before sending requests to the Jabber server. For example, specifying "+foo" will cause messages that are sent to the "bar" room by nickname "qux" to be mirrored to the "bar/xmpp" stream in Zulip by user "qux+foo@example.com". This -option does not affect login credentials.'''.replace( +option does not affect login credentials.""".replace( "\n", " " ), ) parser.add_option( - '-d', - '--debug', - help='set logging to DEBUG. Can not be set via config file.', - action='store_const', - dest='log_level', + "-d", + "--debug", + help="set logging to DEBUG. Can not be set via config file.", + action="store_const", + dest="log_level", const=logging.DEBUG, default=logging.INFO, ) jabber_group = optparse.OptionGroup(parser, "Jabber configuration") jabber_group.add_option( - '--jid', + "--jid", default=None, - action='store', + action="store", help="Your Jabber JID. If a resource is specified, " "it will be used as the nickname when joining MUCs. " "Specifying the nickname is mostly useful if you want " @@ -353,27 +353,27 @@ option does not affect login credentials.'''.replace( "from a dedicated account.", ) jabber_group.add_option( - '--jabber-password', default=None, action='store', help="Your Jabber password" + "--jabber-password", default=None, action="store", help="Your Jabber password" ) jabber_group.add_option( - '--conference-domain', + "--conference-domain", default=None, - action='store', + action="store", help="Your Jabber conference domain (E.g. conference.jabber.example.com). " - "If not specifed, \"conference.\" will be prepended to your JID's domain.", + 'If not specifed, "conference." will be prepended to your JID\'s domain.', ) - jabber_group.add_option('--no-use-tls', default=None, action='store_true') + jabber_group.add_option("--no-use-tls", default=None, action="store_true") jabber_group.add_option( - '--jabber-server-address', + "--jabber-server-address", default=None, - action='store', + action="store", help="The hostname of your Jabber server. This is only needed if " "your server is missing SRV records", ) jabber_group.add_option( - '--jabber-server-port', - default='5222', - action='store', + "--jabber-server-port", + default="5222", + action="store", help="The port of your Jabber server. This is only needed if " "your server is missing SRV records", ) @@ -382,7 +382,7 @@ option does not affect login credentials.'''.replace( parser.add_option_group(zulip.generate_option_group(parser, "zulip-")) (options, args) = parser.parse_args() - logging.basicConfig(level=options.log_level, format='%(levelname)-8s %(message)s') + logging.basicConfig(level=options.log_level, format="%(levelname)-8s %(message)s") if options.zulip_config_file is None: default_config_file = zulip.get_default_config_filename() @@ -422,9 +422,9 @@ option does not affect login credentials.'''.replace( options.mode = "personal" if options.zulip_email_suffix is None: - options.zulip_email_suffix = '' + options.zulip_email_suffix = "" - if options.mode not in ('public', 'personal'): + if options.mode not in ("public", "personal"): config_error("Bad value for --mode: must be one of 'public' or 'personal'") if None in (options.jid, options.jabber_password): @@ -437,7 +437,7 @@ option does not affect login credentials.'''.replace( zulip.init_from_options(options, "JabberMirror/" + __version__) ) # This won't work for open realms that don't have a consistent domain - options.zulip_domain = zulipToJabber.client.email.partition('@')[-1] + options.zulip_domain = zulipToJabber.client.email.partition("@")[-1] try: jid = JID(options.jid) @@ -460,10 +460,10 @@ option does not affect login credentials.'''.replace( zulipToJabber.set_jabber_client(xmpp) xmpp.process(block=False) - if options.mode == 'public': - event_types = ['stream'] + if options.mode == "public": + event_types = ["stream"] else: - event_types = ['message', 'subscription'] + event_types = ["message", "subscription"] try: logging.info("Connecting to Zulip.") diff --git a/zulip/integrations/log2zulip/log2zulip b/zulip/integrations/log2zulip/log2zulip index d0b5bcf..55a4aaf 100755 --- a/zulip/integrations/log2zulip/log2zulip +++ b/zulip/integrations/log2zulip/log2zulip @@ -96,7 +96,7 @@ def process_logs() -> None: # immediately after rotation, this tool won't notice. file_data["last"] = 1 output = subprocess.check_output(["tail", "-n+%s" % (file_data["last"],), log_file]) - new_lines = output.decode('utf-8', errors='replace').split('\n')[:-1] + new_lines = output.decode("utf-8", errors="replace").split("\n")[:-1] if len(new_lines) > 0: process_lines(new_lines, log_file) file_data["last"] += len(new_lines) diff --git a/zulip/integrations/nagios/nagios-notify-zulip b/zulip/integrations/nagios/nagios-notify-zulip index 105ae1e..acb1930 100755 --- a/zulip/integrations/nagios/nagios-notify-zulip +++ b/zulip/integrations/nagios/nagios-notify-zulip @@ -9,19 +9,19 @@ VERSION = "0.9" # In Nagios, "output" means "first line of output", and "long # output" means "other lines of output". parser = zulip.add_default_arguments(argparse.ArgumentParser()) # type: argparse.ArgumentParser -parser.add_argument('--output', default='') -parser.add_argument('--long-output', default='') -parser.add_argument('--stream', default='nagios') -parser.add_argument('--config', default='/etc/nagios3/zuliprc') -for opt in ('type', 'host', 'service', 'state'): - parser.add_argument('--' + opt) +parser.add_argument("--output", default="") +parser.add_argument("--long-output", default="") +parser.add_argument("--stream", default="nagios") +parser.add_argument("--config", default="/etc/nagios3/zuliprc") +for opt in ("type", "host", "service", "state"): + parser.add_argument("--" + opt) opts = parser.parse_args() client = zulip.Client( config_file=opts.config, client="ZulipNagios/" + VERSION ) # type: zulip.Client -msg = dict(type='stream', to=opts.stream) # type: Dict[str, Any] +msg = dict(type="stream", to=opts.stream) # type: Dict[str, Any] # Set a subject based on the host or service in question. This enables # threaded discussion of multiple concurrent issues, and provides useful @@ -30,24 +30,24 @@ msg = dict(type='stream', to=opts.stream) # type: Dict[str, Any] # We send PROBLEM and RECOVERY messages to the same subject. if opts.service is None: # Host notification - thing = 'host' # type: Text - msg['subject'] = 'host %s' % (opts.host,) + thing = "host" # type: Text + msg["subject"] = "host %s" % (opts.host,) else: # Service notification - thing = 'service' - msg['subject'] = 'service %s on %s' % (opts.service, opts.host) + thing = "service" + msg["subject"] = "service %s on %s" % (opts.service, opts.host) -if len(msg['subject']) > 60: - msg['subject'] = msg['subject'][0:57].rstrip() + "..." +if len(msg["subject"]) > 60: + msg["subject"] = msg["subject"][0:57].rstrip() + "..." # e.g. **PROBLEM**: service is CRITICAL -msg['content'] = '**%s**: %s is %s' % (opts.type, thing, opts.state) +msg["content"] = "**%s**: %s is %s" % (opts.type, thing, opts.state) # The "long output" can contain newlines represented by "\n" escape sequences. # The Nagios mail command uses /usr/bin/printf "%b" to expand these. # We will be more conservative and handle just this one escape sequence. -output = (opts.output + '\n' + opts.long_output.replace(r'\n', '\n')).strip() # type: Text +output = (opts.output + "\n" + opts.long_output.replace(r"\n", "\n")).strip() # type: Text if output: # Put any command output in a code block. - msg['content'] += '\n\n~~~~\n' + output + "\n~~~~\n" + msg["content"] += "\n\n~~~~\n" + output + "\n~~~~\n" client.send_message(msg) diff --git a/zulip/integrations/openshift/post_deploy b/zulip/integrations/openshift/post_deploy index 1f3c73f..62e4a33 100755 --- a/zulip/integrations/openshift/post_deploy +++ b/zulip/integrations/openshift/post_deploy @@ -10,7 +10,7 @@ from typing import Dict sys.path.insert(0, os.path.dirname(__file__)) import zulip_openshift_config as config -VERSION = '0.1' +VERSION = "0.1" if config.ZULIP_API_PATH is not None: sys.path.append(config.ZULIP_API_PATH) @@ -21,7 +21,7 @@ client = zulip.Client( email=config.ZULIP_USER, site=config.ZULIP_SITE, api_key=config.ZULIP_API_KEY, - client='ZulipOpenShift/' + VERSION, + client="ZulipOpenShift/" + VERSION, ) @@ -29,19 +29,19 @@ def get_deployment_details() -> Dict[str, str]: # "gear deployments" output example: # Activation time - Deployment ID - Git Ref - Git SHA1 # 2017-01-07 15:40:30 -0500 - 9e2b7143 - master - b9ce57c - ACTIVE - dep = subprocess.check_output(['gear', 'deployments'], universal_newlines=True).splitlines()[1] - splits = dep.split(' - ') + dep = subprocess.check_output(["gear", "deployments"], universal_newlines=True).splitlines()[1] + splits = dep.split(" - ") return dict( - app_name=os.environ['OPENSHIFT_APP_NAME'], - url=os.environ['OPENSHIFT_APP_DNS'], + app_name=os.environ["OPENSHIFT_APP_NAME"], + url=os.environ["OPENSHIFT_APP_DNS"], branch=splits[2], commit_id=splits[3], ) def send_bot_message(deployment: Dict[str, str]) -> None: - destination = config.deployment_notice_destination(deployment['branch']) + destination = config.deployment_notice_destination(deployment["branch"]) if destination is None: # No message should be sent return @@ -49,10 +49,10 @@ def send_bot_message(deployment: Dict[str, str]) -> None: client.send_message( { - 'type': 'stream', - 'to': destination['stream'], - 'subject': destination['subject'], - 'content': message, + "type": "stream", + "to": destination["stream"], + "subject": destination["subject"], + "content": message, } ) diff --git a/zulip/integrations/openshift/zulip_openshift_config.py b/zulip/integrations/openshift/zulip_openshift_config.py index d63b230..09be54c 100755 --- a/zulip/integrations/openshift/zulip_openshift_config.py +++ b/zulip/integrations/openshift/zulip_openshift_config.py @@ -2,8 +2,8 @@ from typing import Dict, Optional, Text # Change these values to configure authentication for the plugin -ZULIP_USER = 'openshift-bot@example.com' -ZULIP_API_KEY = '0123456789abcdef0123456789abcdef' +ZULIP_USER = "openshift-bot@example.com" +ZULIP_API_KEY = "0123456789abcdef0123456789abcdef" # deployment_notice_destination() lets you customize where deployment notices # are sent to with the full power of a Python function. @@ -20,8 +20,8 @@ ZULIP_API_KEY = '0123456789abcdef0123456789abcdef' # * topic "master" # And similarly for branch "test-post-receive" (for use when testing). def deployment_notice_destination(branch: str) -> Optional[Dict[str, Text]]: - if branch in ['master', 'test-post-receive']: - return dict(stream='deployments', subject='%s' % (branch,)) + if branch in ["master", "test-post-receive"]: + return dict(stream="deployments", subject="%s" % (branch,)) # Return None for cases where you don't want a notice sent return None @@ -39,14 +39,14 @@ def deployment_notice_destination(branch: str) -> Optional[Dict[str, Text]]: # * dep_id = deployment id # * dep_time = deployment timestamp def format_deployment_message( - app_name: str = '', - url: str = '', - branch: str = '', - commit_id: str = '', - dep_id: str = '', - dep_time: str = '', + app_name: str = "", + url: str = "", + branch: str = "", + commit_id: str = "", + dep_id: str = "", + dep_time: str = "", ) -> str: - return 'Deployed commit `%s` (%s) in [%s](%s)' % (commit_id, branch, app_name, url) + return "Deployed commit `%s` (%s) in [%s](%s)" % (commit_id, branch, app_name, url) ## If properly installed, the Zulip API should be in your import @@ -54,4 +54,4 @@ def format_deployment_message( ZULIP_API_PATH = None # type: Optional[str] # Set this to your Zulip server's API URI -ZULIP_SITE = 'https://zulip.example.com' +ZULIP_SITE = "https://zulip.example.com" diff --git a/zulip/integrations/perforce/zulip_change-commit.py b/zulip/integrations/perforce/zulip_change-commit.py index fed1de4..ea77260 100755 --- a/zulip/integrations/perforce/zulip_change-commit.py +++ b/zulip/integrations/perforce/zulip_change-commit.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -'''Zulip notification change-commit hook. +"""Zulip notification change-commit hook. In Perforce, The "change-commit" trigger is fired after a metadata has been created, files have been transferred, and the changelist committed to the depot @@ -12,7 +12,7 @@ This specific trigger expects command-line arguments in the form: For example: 1234 //depot/security/src/ -''' +""" import os import os.path @@ -43,11 +43,11 @@ try: changelist = int(sys.argv[1]) # type: int changeroot = sys.argv[2] # type: str except IndexError: - print("Wrong number of arguments.\n\n", end=' ', file=sys.stderr) + print("Wrong number of arguments.\n\n", end=" ", file=sys.stderr) print(__doc__, file=sys.stderr) sys.exit(-1) except ValueError: - print("First argument must be an integer.\n\n", end=' ', file=sys.stderr) + print("First argument must be an integer.\n\n", end=" ", file=sys.stderr) print(__doc__, file=sys.stderr) sys.exit(-1) @@ -79,7 +79,7 @@ if hasattr(config, "P4_WEB"): if p4web is not None: # linkify the change number - change = '[{change}]({p4web}/{change}?ac=10)'.format(p4web=p4web, change=change) + change = "[{change}]({p4web}/{change}?ac=10)".format(p4web=p4web, change=change) message = """**{user}** committed revision @{change} to `{path}`. diff --git a/zulip/integrations/perforce/zulip_perforce_config.py b/zulip/integrations/perforce/zulip_perforce_config.py index 417ac2c..66a59a1 100644 --- a/zulip/integrations/perforce/zulip_perforce_config.py +++ b/zulip/integrations/perforce/zulip_perforce_config.py @@ -29,7 +29,7 @@ P4_WEB: Optional[str] = None # * stream "depot_subdirectory-commits" # * subject "change_root" def commit_notice_destination(path: Text, changelist: int) -> Optional[Dict[Text, Text]]: - dirs = path.split('/') + dirs = path.split("/") if len(dirs) >= 4 and dirs[3] not in ("*", "..."): directory = dirs[3] else: diff --git a/zulip/integrations/rss/rss-bot b/zulip/integrations/rss/rss-bot index 45dbce3..f03d7d7 100755 --- a/zulip/integrations/rss/rss-bot +++ b/zulip/integrations/rss/rss-bot @@ -21,7 +21,7 @@ import feedparser import zulip VERSION = "0.9" # type: str -RSS_DATA_DIR = os.path.expanduser(os.path.join('~', '.cache', 'zulip-rss')) # type: str +RSS_DATA_DIR = os.path.expanduser(os.path.join("~", ".cache", "zulip-rss")) # type: str OLDNESS_THRESHOLD = 30 # type: int usage = """Usage: Send summaries of RSS entries for your favorite feeds to Zulip. @@ -52,38 +52,38 @@ parser = zulip.add_default_arguments( argparse.ArgumentParser(usage) ) # type: argparse.ArgumentParser parser.add_argument( - '--stream', - dest='stream', - help='The stream to which to send RSS messages.', + "--stream", + dest="stream", + help="The stream to which to send RSS messages.", default="rss", - action='store', + action="store", ) parser.add_argument( - '--data-dir', - dest='data_dir', - help='The directory where feed metadata is stored', + "--data-dir", + dest="data_dir", + help="The directory where feed metadata is stored", default=os.path.join(RSS_DATA_DIR), - action='store', + action="store", ) parser.add_argument( - '--feed-file', - dest='feed_file', - help='The file containing a list of RSS feed URLs to follow, one URL per line', + "--feed-file", + dest="feed_file", + help="The file containing a list of RSS feed URLs to follow, one URL per line", default=os.path.join(RSS_DATA_DIR, "rss-feeds"), - action='store', + action="store", ) parser.add_argument( - '--unwrap', - dest='unwrap', - action='store_true', - help='Convert word-wrapped paragraphs into single lines', + "--unwrap", + dest="unwrap", + action="store_true", + help="Convert word-wrapped paragraphs into single lines", default=False, ) parser.add_argument( - '--math', - dest='math', - action='store_true', - help='Convert $ to $$ (for KaTeX processing)', + "--math", + dest="math", + action="store_true", + help="Convert $ to $$ (for KaTeX processing)", default=False, ) @@ -137,7 +137,7 @@ class MLStripper(HTMLParser): self.fed.append(data) def get_data(self) -> str: - return ''.join(self.fed) + return "".join(self.fed) def strip_tags(html: str) -> str: @@ -155,13 +155,13 @@ def compute_entry_hash(entry: Dict[str, Any]) -> str: def unwrap_text(body: str) -> str: # Replace \n by space if it is preceded and followed by a non-\n. # Example: '\na\nb\nc\n\nd\n' -> '\na b c\n\nd\n' - return re.sub('(?<=[^\n])\n(?=[^\n])', ' ', body) + return re.sub("(?<=[^\n])\n(?=[^\n])", " ", body) def elide_subject(subject: str) -> str: MAX_TOPIC_LENGTH = 60 if len(subject) > MAX_TOPIC_LENGTH: - subject = subject[: MAX_TOPIC_LENGTH - 3].rstrip() + '...' + subject = subject[: MAX_TOPIC_LENGTH - 3].rstrip() + "..." return subject @@ -178,7 +178,7 @@ def send_zulip(entry: Any, feed_name: str) -> Dict[str, Any]: ) # type: str if opts.math: - content = content.replace('$', '$$') + content = content.replace("$", "$$") message = { "type": "stream", diff --git a/zulip/integrations/svn/post-commit b/zulip/integrations/svn/post-commit index 0025818..b58b410 100755 --- a/zulip/integrations/svn/post-commit +++ b/zulip/integrations/svn/post-commit @@ -43,7 +43,7 @@ entry = svn.log(path, revision_end=pysvn.Revision(pysvn.opt_revision_kind.number 0 ] # type: Dict[Text, Any] message = "**{}** committed revision r{} to `{}`.\n\n> {}".format( - entry['author'], rev, path.split('/')[-1], entry['revprops']['svn:log'] + entry["author"], rev, path.split("/")[-1], entry["revprops"]["svn:log"] ) # type: Text destination = config.commit_notice_destination(path, rev) # type: Optional[Dict[Text, Text]] diff --git a/zulip/integrations/svn/zulip_svn_config.py b/zulip/integrations/svn/zulip_svn_config.py index 0479e92..8104bee 100644 --- a/zulip/integrations/svn/zulip_svn_config.py +++ b/zulip/integrations/svn/zulip_svn_config.py @@ -19,7 +19,7 @@ ZULIP_API_KEY = "0123456789abcdef0123456789abcdef" # * stream "commits" # * topic "branch_name" def commit_notice_destination(path: Text, commit: Text) -> Optional[Dict[Text, Text]]: - repo = path.split('/')[-1] + repo = path.split("/")[-1] if repo not in ["evil-master-plan", "my-super-secret-repository"]: return dict(stream="commits", subject="%s" % (repo,)) diff --git a/zulip/integrations/trac/zulip_trac.py b/zulip/integrations/trac/zulip_trac.py index 202dc3c..fb92fdd 100644 --- a/zulip/integrations/trac/zulip_trac.py +++ b/zulip/integrations/trac/zulip_trac.py @@ -100,24 +100,24 @@ class ZulipPlugin(Component): content = "%s updated %s" % (author, markdown_ticket_url(ticket)) if comment: - content += ' with comment: %s\n\n' % (markdown_block(comment),) + content += " with comment: %s\n\n" % (markdown_block(comment),) else: content += ":\n\n" field_changes = [] for key, value in old_values.items(): if key == "description": - content += '- Changed %s from %s\n\nto %s' % ( + content += "- Changed %s from %s\n\nto %s" % ( key, markdown_block(value), markdown_block(ticket.values.get(key)), ) elif old_values.get(key) == "": - field_changes.append('%s: => **%s**' % (key, ticket.values.get(key))) + field_changes.append("%s: => **%s**" % (key, ticket.values.get(key))) elif ticket.values.get(key) == "": field_changes.append('%s: **%s** => ""' % (key, old_values.get(key))) else: field_changes.append( - '%s: **%s** => **%s**' % (key, old_values.get(key), ticket.values.get(key)) + "%s: **%s** => **%s**" % (key, old_values.get(key), ticket.values.get(key)) ) content += ", ".join(field_changes) diff --git a/zulip/integrations/trello/zulip_trello.py b/zulip/integrations/trello/zulip_trello.py index cd335a0..f9a5678 100755 --- a/zulip/integrations/trello/zulip_trello.py +++ b/zulip/integrations/trello/zulip_trello.py @@ -25,22 +25,22 @@ def get_model_id(options): """ - trello_api_url = 'https://api.trello.com/1/board/{}'.format(options.trello_board_id) + trello_api_url = "https://api.trello.com/1/board/{}".format(options.trello_board_id) params = { - 'key': options.trello_api_key, - 'token': options.trello_token, + "key": options.trello_api_key, + "token": options.trello_token, } trello_response = requests.get(trello_api_url, params=params) if trello_response.status_code != 200: - print('Error: Can\'t get the idModel. Please check the configuration') + print("Error: Can't get the idModel. Please check the configuration") sys.exit(1) board_info_json = trello_response.json() - return board_info_json['id'] + return board_info_json["id"] def get_webhook_id(options, id_model): @@ -55,27 +55,27 @@ def get_webhook_id(options, id_model): """ - trello_api_url = 'https://api.trello.com/1/webhooks/' + trello_api_url = "https://api.trello.com/1/webhooks/" data = { - 'key': options.trello_api_key, - 'token': options.trello_token, - 'description': 'Webhook for Zulip integration (From Trello {} to Zulip)'.format( + "key": options.trello_api_key, + "token": options.trello_token, + "description": "Webhook for Zulip integration (From Trello {} to Zulip)".format( options.trello_board_name, ), - 'callbackURL': options.zulip_webhook_url, - 'idModel': id_model, + "callbackURL": options.zulip_webhook_url, + "idModel": id_model, } trello_response = requests.post(trello_api_url, data=data) if trello_response.status_code != 200: - print('Error: Can\'t create the Webhook:', trello_response.text) + print("Error: Can't create the Webhook:", trello_response.text) sys.exit(1) webhook_info_json = trello_response.json() - return webhook_info_json['id'] + return webhook_info_json["id"] def create_webhook(options): @@ -88,20 +88,20 @@ def create_webhook(options): """ # first, we need to get the idModel - print('Getting Trello idModel for the {} board...'.format(options.trello_board_name)) + print("Getting Trello idModel for the {} board...".format(options.trello_board_name)) id_model = get_model_id(options) if id_model: - print('Success! The idModel is', id_model) + print("Success! The idModel is", id_model) id_webhook = get_webhook_id(options, id_model) if id_webhook: - print('Success! The webhook ID is', id_webhook) + print("Success! The webhook ID is", id_webhook) print( - 'Success! The webhook for the {} Trello board was successfully created.'.format( + "Success! The webhook for the {} Trello board was successfully created.".format( options.trello_board_name ) ) @@ -118,36 +118,36 @@ at . """ parser = argparse.ArgumentParser(description=description) - parser.add_argument('--trello-board-name', required=True, help='The Trello board name.') + parser.add_argument("--trello-board-name", required=True, help="The Trello board name.") parser.add_argument( - '--trello-board-id', + "--trello-board-id", required=True, - help=('The Trello board short ID. Can usually be found ' 'in the URL of the Trello board.'), + help=("The Trello board short ID. Can usually be found " "in the URL of the Trello board."), ) parser.add_argument( - '--trello-api-key', + "--trello-api-key", required=True, help=( - 'Visit https://trello.com/1/appkey/generate to generate ' - 'an APPLICATION_KEY (need to be logged into Trello).' + "Visit https://trello.com/1/appkey/generate to generate " + "an APPLICATION_KEY (need to be logged into Trello)." ), ) parser.add_argument( - '--trello-token', + "--trello-token", required=True, help=( - 'Visit https://trello.com/1/appkey/generate and under ' - '`Developer API Keys`, click on `Token` and generate ' - 'a Trello access token.' + "Visit https://trello.com/1/appkey/generate and under " + "`Developer API Keys`, click on `Token` and generate " + "a Trello access token." ), ) parser.add_argument( - '--zulip-webhook-url', required=True, help='The webhook URL that Trello will query.' + "--zulip-webhook-url", required=True, help="The webhook URL that Trello will query." ) options = parser.parse_args() create_webhook(options) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/zulip/integrations/twitter/twitter-bot b/zulip/integrations/twitter/twitter-bot index 7cc13a4..2aa591b 100755 --- a/zulip/integrations/twitter/twitter-bot +++ b/zulip/integrations/twitter/twitter-bot @@ -69,32 +69,32 @@ access token" as well. Fill in the values displayed. def write_config(config: ConfigParser, configfile_path: str) -> None: - with open(configfile_path, 'w') as configfile: + with open(configfile_path, "w") as configfile: config.write(configfile) parser = zulip.add_default_arguments(argparse.ArgumentParser("Fetch tweets from Twitter.")) parser.add_argument( - '--instructions', - action='store_true', - help='Show instructions for the twitter bot setup and exit', + "--instructions", + action="store_true", + help="Show instructions for the twitter bot setup and exit", ) parser.add_argument( - '--limit-tweets', default=15, type=int, help='Maximum number of tweets to send at once' + "--limit-tweets", default=15, type=int, help="Maximum number of tweets to send at once" ) -parser.add_argument('--search', dest='search_terms', help='Terms to search on', action='store') +parser.add_argument("--search", dest="search_terms", help="Terms to search on", action="store") parser.add_argument( - '--stream', - dest='stream', - help='The stream to which to send tweets', + "--stream", + dest="stream", + help="The stream to which to send tweets", default="twitter", - action='store', + action="store", ) parser.add_argument( - '--twitter-name', dest='twitter_name', help='Twitter username to poll new tweets from"' + "--twitter-name", dest="twitter_name", help='Twitter username to poll new tweets from"' ) -parser.add_argument('--excluded-terms', dest='excluded_terms', help='Terms to exclude tweets on') -parser.add_argument('--excluded-users', dest='excluded_users', help='Users to exclude tweets on') +parser.add_argument("--excluded-terms", dest="excluded_terms", help="Terms to exclude tweets on") +parser.add_argument("--excluded-users", dest="excluded_users", help="Users to exclude tweets on") opts = parser.parse_args() @@ -103,15 +103,15 @@ if opts.instructions: sys.exit() if all([opts.search_terms, opts.twitter_name]): - parser.error('You must only specify either a search term or a username.') + parser.error("You must only specify either a search term or a username.") if opts.search_terms: - client_type = 'ZulipTwitterSearch/' + client_type = "ZulipTwitterSearch/" CONFIGFILE_INTERNAL = os.path.expanduser("~/.zulip_twitterrc_fetchsearch") elif opts.twitter_name: - client_type = 'ZulipTwitter/' + client_type = "ZulipTwitter/" CONFIGFILE_INTERNAL = os.path.expanduser("~/.zulip_twitteruserrc_fetchuser") else: - parser.error('You must either specify a search term or a username.') + parser.error("You must either specify a search term or a username.") try: config = ConfigParser() @@ -119,10 +119,10 @@ try: config_internal = ConfigParser() config_internal.read(CONFIGFILE_INTERNAL) - consumer_key = config.get('twitter', 'consumer_key') - consumer_secret = config.get('twitter', 'consumer_secret') - access_token_key = config.get('twitter', 'access_token_key') - access_token_secret = config.get('twitter', 'access_token_secret') + consumer_key = config.get("twitter", "consumer_key") + consumer_secret = config.get("twitter", "consumer_secret") + access_token_key = config.get("twitter", "access_token_key") + access_token_secret = config.get("twitter", "access_token_secret") except (NoSectionError, NoOptionError): parser.error("Please provide a ~/.zulip_twitterrc") @@ -130,17 +130,17 @@ if not all([consumer_key, consumer_secret, access_token_key, access_token_secret parser.error("Please provide a ~/.zulip_twitterrc") try: - since_id = config_internal.getint('twitter', 'since_id') + since_id = config_internal.getint("twitter", "since_id") except (NoOptionError, NoSectionError): since_id = 0 try: - previous_twitter_name = config_internal.get('twitter', 'twitter_name') + previous_twitter_name = config_internal.get("twitter", "twitter_name") except (NoOptionError, NoSectionError): - previous_twitter_name = '' + previous_twitter_name = "" try: - previous_search_terms = config_internal.get('twitter', 'search_terms') + previous_search_terms = config_internal.get("twitter", "search_terms") except (NoOptionError, NoSectionError): - previous_search_terms = '' + previous_search_terms = "" try: import twitter @@ -242,17 +242,17 @@ for status in statuses[::-1][: opts.limit_tweets]: ret = client.send_message(message) - if ret['result'] == 'error': + if ret["result"] == "error": # If sending failed (e.g. no such stream), abort and retry next time - print("Error sending message to zulip: %s" % ret['msg']) + print("Error sending message to zulip: %s" % ret["msg"]) break else: since_id = status.id -if 'twitter' not in config_internal.sections(): - config_internal.add_section('twitter') -config_internal.set('twitter', 'since_id', str(since_id)) -config_internal.set('twitter', 'search_terms', str(opts.search_terms)) -config_internal.set('twitter', 'twitter_name', str(opts.twitter_name)) +if "twitter" not in config_internal.sections(): + config_internal.add_section("twitter") +config_internal.set("twitter", "since_id", str(since_id)) +config_internal.set("twitter", "search_terms", str(opts.search_terms)) +config_internal.set("twitter", "twitter_name", str(opts.twitter_name)) write_config(config_internal, CONFIGFILE_INTERNAL) diff --git a/zulip/integrations/zephyr/check-mirroring b/zulip/integrations/zephyr/check-mirroring index 1af229d..c70a5e6 100755 --- a/zulip/integrations/zephyr/check-mirroring +++ b/zulip/integrations/zephyr/check-mirroring @@ -13,12 +13,12 @@ import zephyr import zulip parser = optparse.OptionParser() -parser.add_option('--verbose', dest='verbose', default=False, action='store_true') -parser.add_option('--site', dest='site', default=None, action='store') -parser.add_option('--sharded', default=False, action='store_true') +parser.add_option("--verbose", dest="verbose", default=False, action="store_true") +parser.add_option("--site", dest="site", default=None, action="store") +parser.add_option("--sharded", default=False, action="store_true") (options, args) = parser.parse_args() -mit_user = 'tabbott/extra@ATHENA.MIT.EDU' +mit_user = "tabbott/extra@ATHENA.MIT.EDU" zulip_client = zulip.Client(verbose=True, client="ZulipMonitoring/0.1", site=options.site) @@ -116,11 +116,11 @@ def send_zephyr(zwrite_args: List[str], content: str) -> bool: # Subscribe to Zulip try: res = zulip_client.register(event_types=["message"]) - if 'error' in res['result']: + if "error" in res["result"]: logging.error("Error subscribing to Zulips!") - logging.error(res['msg']) + logging.error(res["msg"]) print_status_and_exit(1) - queue_id, last_event_id = (res['queue_id'], res['last_event_id']) + queue_id, last_event_id = (res["queue_id"], res["last_event_id"]) except Exception: logger.exception("Unexpected error subscribing to Zulips") print_status_and_exit(1) @@ -129,9 +129,9 @@ except Exception: zephyr_subs_to_add = [] for (stream, test) in test_streams: if stream == "message": - zephyr_subs_to_add.append((stream, 'personal', mit_user)) + zephyr_subs_to_add.append((stream, "personal", mit_user)) else: - zephyr_subs_to_add.append((stream, '*', '*')) + zephyr_subs_to_add.append((stream, "*", "*")) actually_subscribed = False for tries in range(10): @@ -263,11 +263,11 @@ logger.info("Starting receiving messages!") # receive zulips res = zulip_client.get_events(queue_id=queue_id, last_event_id=last_event_id) -if 'error' in res['result']: +if "error" in res["result"]: logging.error("Error receiving Zulips!") - logging.error(res['msg']) + logging.error(res["msg"]) print_status_and_exit(1) -messages = [event['message'] for event in res['events']] +messages = [event["message"] for event in res["events"]] logger.info("Finished receiving Zulip messages!") receive_zephyrs() @@ -296,7 +296,7 @@ def process_keys(content_list: List[str]) -> Tuple[Dict[str, int], Set[str], Set # The h_foo variables are about the messages we _received_ in Zulip # The z_foo variables are about the messages we _received_ in Zephyr h_contents = [message["content"] for message in messages] -z_contents = [notice.message.split('\0')[1] for notice in notices] +z_contents = [notice.message.split("\0")[1] for notice in notices] (h_key_counts, h_missing_z, h_missing_h, h_duplicates, h_success) = process_keys(h_contents) (z_key_counts, z_missing_z, z_missing_h, z_duplicates, z_success) = process_keys(z_contents) diff --git a/zulip/integrations/zephyr/sync-public-streams b/zulip/integrations/zephyr/sync-public-streams index a11fcfb..f8c8856 100755 --- a/zulip/integrations/zephyr/sync-public-streams +++ b/zulip/integrations/zephyr/sync-public-streams @@ -6,7 +6,7 @@ import os import sys import unicodedata -sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'api')) +sys.path.append(os.path.join(os.path.dirname(__file__), "..", "api")) import zulip @@ -18,20 +18,20 @@ def write_public_streams() -> None: # normalization and then lower-casing server-side canonical_cls = unicodedata.normalize("NFKC", stream_name).lower() if canonical_cls in [ - 'security', - 'login', - 'network', - 'ops', - 'user_locate', - 'mit', - 'moof', - 'wsmonitor', - 'wg_ctl', - 'winlogger', - 'hm_ctl', - 'hm_stat', - 'zephyr_admin', - 'zephyr_ctl', + "security", + "login", + "network", + "ops", + "user_locate", + "mit", + "moof", + "wsmonitor", + "wg_ctl", + "winlogger", + "hm_ctl", + "hm_stat", + "zephyr_admin", + "zephyr_ctl", ]: # These zephyr classes cannot be subscribed to by us, due # to MIT's Zephyr access control settings diff --git a/zulip/integrations/zephyr/zephyr_mirror_backend.py b/zulip/integrations/zephyr/zephyr_mirror_backend.py index a06a56c..b524953 100755 --- a/zulip/integrations/zephyr/zephyr_mirror_backend.py +++ b/zulip/integrations/zephyr/zephyr_mirror_backend.py @@ -39,8 +39,8 @@ def to_zulip_username(zephyr_username: str) -> str: (user, realm) = (zephyr_username, "ATHENA.MIT.EDU") if realm.upper() == "ATHENA.MIT.EDU": # Hack to make ctl's fake username setup work :) - if user.lower() == 'golem': - user = 'ctl' + if user.lower() == "golem": + user = "ctl" return user.lower() + "@mit.edu" return user.lower() + "|" + realm.upper() + "@mit.edu" @@ -49,10 +49,10 @@ def to_zephyr_username(zulip_username: str) -> str: (user, realm) = zulip_username.split("@") if "|" not in user: # Hack to make ctl's fake username setup work :) - if user.lower() == 'ctl': - user = 'golem' + if user.lower() == "ctl": + user = "golem" return user.lower() + "@ATHENA.MIT.EDU" - match_user = re.match(r'([a-zA-Z0-9_]+)\|(.+)', user) + match_user = re.match(r"([a-zA-Z0-9_]+)\|(.+)", user) if not match_user: raise Exception("Could not parse Zephyr realm for cross-realm user %s" % (zulip_username,)) return match_user.group(1).lower() + "@" + match_user.group(2).upper() @@ -85,14 +85,14 @@ def unwrap_lines(body: str) -> str: previous_line = lines[0] for line in lines[1:]: line = line.rstrip() - if re.match(r'^\W', line, flags=re.UNICODE) and re.match( - r'^\W', previous_line, flags=re.UNICODE + if re.match(r"^\W", line, flags=re.UNICODE) and re.match( + r"^\W", previous_line, flags=re.UNICODE ): result += previous_line + "\n" elif ( line == "" or previous_line == "" - or re.match(r'^\W', line, flags=re.UNICODE) + or re.match(r"^\W", line, flags=re.UNICODE) or different_paragraph(previous_line, line) ): # Use 2 newlines to separate sections so that we @@ -122,31 +122,31 @@ def send_zulip(zeph: ZephyrDict) -> Dict[str, Any]: message = {} if options.forward_class_messages: message["forged"] = "yes" - message['type'] = zeph['type'] - message['time'] = zeph['time'] - message['sender'] = to_zulip_username(zeph['sender']) + message["type"] = zeph["type"] + message["time"] = zeph["time"] + message["sender"] = to_zulip_username(zeph["sender"]) if "subject" in zeph: # Truncate the subject to the current limit in Zulip. No # need to do this for stream names, since we're only # subscribed to valid stream names. message["subject"] = zeph["subject"][:60] - if zeph['type'] == 'stream': + if zeph["type"] == "stream": # Forward messages sent to -c foo -i bar to stream bar subject "instance" if zeph["stream"] == "message": - message['to'] = zeph['subject'].lower() - message['subject'] = "instance %s" % (zeph['subject'],) + message["to"] = zeph["subject"].lower() + message["subject"] = "instance %s" % (zeph["subject"],) elif zeph["stream"] == "tabbott-test5": - message['to'] = zeph['subject'].lower() - message['subject'] = "test instance %s" % (zeph['subject'],) + message["to"] = zeph["subject"].lower() + message["subject"] = "test instance %s" % (zeph["subject"],) else: message["to"] = zeph["stream"] else: message["to"] = zeph["recipient"] - message['content'] = unwrap_lines(zeph['content']) + message["content"] = unwrap_lines(zeph["content"]) if options.test_mode and options.site == DEFAULT_SITE: logger.debug("Message is: %s" % (str(message),)) - return {'result': "success"} + return {"result": "success"} return zulip_client.send_message(message) @@ -311,13 +311,13 @@ def parse_zephyr_body(zephyr_data: str, notice_format: str) -> Tuple[str, str]: try: (zsig, body) = zephyr_data.split("\x00", 1) if ( - notice_format == 'New transaction [$1] entered in $2\nFrom: $3 ($5)\nSubject: $4' - or notice_format == 'New transaction [$1] entered in $2\nFrom: $3\nSubject: $4' + notice_format == "New transaction [$1] entered in $2\nFrom: $3 ($5)\nSubject: $4" + or notice_format == "New transaction [$1] entered in $2\nFrom: $3\nSubject: $4" ): # Logic based off of owl_zephyr_get_message in barnowl - fields = body.split('\x00') + fields = body.split("\x00") if len(fields) == 5: - body = 'New transaction [%s] entered in %s\nFrom: %s (%s)\nSubject: %s' % ( + body = "New transaction [%s] entered in %s\nFrom: %s (%s)\nSubject: %s" % ( fields[0], fields[1], fields[2], @@ -327,7 +327,7 @@ def parse_zephyr_body(zephyr_data: str, notice_format: str) -> Tuple[str, str]: except ValueError: (zsig, body) = ("", zephyr_data) # Clean body of any null characters, since they're invalid in our protocol. - body = body.replace('\x00', '') + body = body.replace("\x00", "") return (zsig, body) @@ -350,8 +350,8 @@ def parse_crypt_table(zephyr_class: str, instance: str) -> Optional[str]: continue groups = match.groupdict() if ( - groups['class'].lower() == zephyr_class - and 'keypath' in groups + groups["class"].lower() == zephyr_class + and "keypath" in groups and groups.get("algorithm") == "AES" ): return groups["keypath"] @@ -453,23 +453,23 @@ def process_notice(notice: "zephyr.ZNotice", log: Optional[IO[str]]) -> None: zeph: ZephyrDict zeph = { - 'time': str(notice.time), - 'sender': notice.sender, - 'zsig': zsig, # logged here but not used by app - 'content': body, + "time": str(notice.time), + "sender": notice.sender, + "zsig": zsig, # logged here but not used by app + "content": body, } if is_huddle: - zeph['type'] = 'private' - zeph['recipient'] = huddle_recipients + zeph["type"] = "private" + zeph["recipient"] = huddle_recipients elif is_personal: assert notice.recipient is not None - zeph['type'] = 'private' - zeph['recipient'] = to_zulip_username(notice.recipient) + zeph["type"] = "private" + zeph["recipient"] = to_zulip_username(notice.recipient) else: - zeph['type'] = 'stream' - zeph['stream'] = zephyr_class + zeph["type"] = "stream" + zeph["stream"] = zephyr_class if notice.instance.strip() != "": - zeph['subject'] = notice.instance + zeph["subject"] = notice.instance else: zeph["subject"] = '(instance "%s")' % (notice.instance,) @@ -489,7 +489,7 @@ def process_notice(notice: "zephyr.ZNotice", log: Optional[IO[str]]) -> None: "Received a message on %s/%s from %s..." % (zephyr_class, notice.instance, notice.sender) ) if log is not None: - log.write(json.dumps(zeph) + '\n') + log.write(json.dumps(zeph) + "\n") log.flush() if os.fork() == 0: @@ -593,7 +593,7 @@ def zephyr_to_zulip(options: optparse.Values) -> None: zeph["subject"] = zeph["instance"] logger.info( "sending saved message to %s from %s..." - % (zeph.get('stream', zeph.get('recipient')), zeph['sender']) + % (zeph.get("stream", zeph.get("recipient")), zeph["sender"]) ) send_zulip(zeph) except Exception: @@ -603,7 +603,7 @@ def zephyr_to_zulip(options: optparse.Values) -> None: logger.info("Successfully initialized; Starting receive loop.") if options.resend_log_path is not None: - with open(options.resend_log_path, 'a') as log: + with open(options.resend_log_path, "a") as log: process_loop(log) else: process_loop(None) @@ -700,10 +700,10 @@ Feedback button or at support@zulip.com.""" ] # Hack to make ctl's fake username setup work :) - if message['type'] == "stream" and zulip_account_email == "ctl@mit.edu": + if message["type"] == "stream" and zulip_account_email == "ctl@mit.edu": zwrite_args.extend(["-S", "ctl"]) - if message['type'] == "stream": + if message["type"] == "stream": zephyr_class = message["display_recipient"] instance = message["subject"] @@ -725,11 +725,11 @@ Feedback button or at support@zulip.com.""" zephyr_class = "message" zwrite_args.extend(["-c", zephyr_class, "-i", instance]) logger.info("Forwarding message to class %s, instance %s" % (zephyr_class, instance)) - elif message['type'] == "private": - if len(message['display_recipient']) == 1: + elif message["type"] == "private": + if len(message["display_recipient"]) == 1: recipient = to_zephyr_username(message["display_recipient"][0]["email"]) recipients = [recipient] - elif len(message['display_recipient']) == 2: + elif len(message["display_recipient"]) == 2: recipient = "" for r in message["display_recipient"]: if r["email"].lower() != zulip_account_email.lower(): @@ -1085,62 +1085,62 @@ def configure_logger(logger: logging.Logger, direction_name: Optional[str]) -> N def parse_args() -> Tuple[optparse.Values, List[str]]: parser = optparse.OptionParser() parser.add_option( - '--forward-class-messages', default=False, help=optparse.SUPPRESS_HELP, action='store_true' + "--forward-class-messages", default=False, help=optparse.SUPPRESS_HELP, action="store_true" ) - parser.add_option('--shard', help=optparse.SUPPRESS_HELP) - parser.add_option('--noshard', default=False, help=optparse.SUPPRESS_HELP, action='store_true') - parser.add_option('--resend-log', dest='logs_to_resend', help=optparse.SUPPRESS_HELP) - parser.add_option('--enable-resend-log', dest='resend_log_path', help=optparse.SUPPRESS_HELP) - parser.add_option('--log-path', dest='log_path', help=optparse.SUPPRESS_HELP) + parser.add_option("--shard", help=optparse.SUPPRESS_HELP) + parser.add_option("--noshard", default=False, help=optparse.SUPPRESS_HELP, action="store_true") + parser.add_option("--resend-log", dest="logs_to_resend", help=optparse.SUPPRESS_HELP) + parser.add_option("--enable-resend-log", dest="resend_log_path", help=optparse.SUPPRESS_HELP) + parser.add_option("--log-path", dest="log_path", help=optparse.SUPPRESS_HELP) parser.add_option( - '--stream-file-path', - dest='stream_file_path', + "--stream-file-path", + dest="stream_file_path", default="/home/zulip/public_streams", help=optparse.SUPPRESS_HELP, ) parser.add_option( - '--no-forward-personals', - dest='forward_personals', + "--no-forward-personals", + dest="forward_personals", help=optparse.SUPPRESS_HELP, default=True, - action='store_false', + action="store_false", ) parser.add_option( - '--forward-mail-zephyrs', - dest='forward_mail_zephyrs', + "--forward-mail-zephyrs", + dest="forward_mail_zephyrs", help=optparse.SUPPRESS_HELP, default=False, - action='store_true', + action="store_true", ) parser.add_option( - '--no-forward-from-zulip', + "--no-forward-from-zulip", default=True, - dest='forward_from_zulip', + dest="forward_from_zulip", help=optparse.SUPPRESS_HELP, - action='store_false', + action="store_false", ) - parser.add_option('--verbose', default=False, help=optparse.SUPPRESS_HELP, action='store_true') - parser.add_option('--sync-subscriptions', default=False, action='store_true') - parser.add_option('--ignore-expired-tickets', default=False, action='store_true') - parser.add_option('--site', default=DEFAULT_SITE, help=optparse.SUPPRESS_HELP) - parser.add_option('--on-startup-command', default=None, help=optparse.SUPPRESS_HELP) - parser.add_option('--user', default=os.environ["USER"], help=optparse.SUPPRESS_HELP) + parser.add_option("--verbose", default=False, help=optparse.SUPPRESS_HELP, action="store_true") + parser.add_option("--sync-subscriptions", default=False, action="store_true") + parser.add_option("--ignore-expired-tickets", default=False, action="store_true") + parser.add_option("--site", default=DEFAULT_SITE, help=optparse.SUPPRESS_HELP) + parser.add_option("--on-startup-command", default=None, help=optparse.SUPPRESS_HELP) + parser.add_option("--user", default=os.environ["USER"], help=optparse.SUPPRESS_HELP) parser.add_option( - '--stamp-path', + "--stamp-path", default="/afs/athena.mit.edu/user/t/a/tabbott/for_friends", help=optparse.SUPPRESS_HELP, ) - parser.add_option('--session-path', default=None, help=optparse.SUPPRESS_HELP) - parser.add_option('--nagios-class', default=None, help=optparse.SUPPRESS_HELP) - parser.add_option('--nagios-path', default=None, help=optparse.SUPPRESS_HELP) + parser.add_option("--session-path", default=None, help=optparse.SUPPRESS_HELP) + parser.add_option("--nagios-class", default=None, help=optparse.SUPPRESS_HELP) + parser.add_option("--nagios-path", default=None, help=optparse.SUPPRESS_HELP) parser.add_option( - '--use-sessions', default=False, action='store_true', help=optparse.SUPPRESS_HELP + "--use-sessions", default=False, action="store_true", help=optparse.SUPPRESS_HELP ) parser.add_option( - '--test-mode', default=False, help=optparse.SUPPRESS_HELP, action='store_true' + "--test-mode", default=False, help=optparse.SUPPRESS_HELP, action="store_true" ) parser.add_option( - '--api-key-file', default=os.path.join(os.environ["HOME"], "Private", ".humbug-api-key") + "--api-key-file", default=os.path.join(os.environ["HOME"], "Private", ".humbug-api-key") ) return parser.parse_args() @@ -1235,7 +1235,7 @@ or specify the --api-key-file option.""" # Personals mirror on behalf of another user. pgrep_query = "%s.*--user=%s" % (pgrep_query, options.user) proc = subprocess.Popen( - ['pgrep', '-U', os.environ["USER"], "-f", pgrep_query], + ["pgrep", "-U", os.environ["USER"], "-f", pgrep_query], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) diff --git a/zulip/setup.py b/zulip/setup.py index 48616a6..5898e0d 100755 --- a/zulip/setup.py +++ b/zulip/setup.py @@ -15,7 +15,7 @@ def version() -> str: version_line = next( itertools.dropwhile(lambda x: not x.startswith("__version__"), in_handle) ) - version = version_line.split('=')[-1].strip().replace('"', '') + version = version_line.split("=")[-1].strip().replace('"', "") return version @@ -28,50 +28,50 @@ def recur_expand(target_root: Any, dir: Any) -> Generator[Tuple[str, List[str]], # We should be installable with either setuptools or distutils. package_info = dict( - name='zulip', + name="zulip", version=version(), - description='Bindings for the Zulip message API', + description="Bindings for the Zulip message API", long_description=long_description, long_description_content_type="text/markdown", - author='Zulip Open Source Project', - author_email='zulip-devel@googlegroups.com', + 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 :: Apache Software License', - 'Topic :: Communications :: Chat', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', + "Development Status :: 4 - Beta", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Topic :: Communications :: Chat", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", ], - python_requires='>=3.6', - url='https://www.zulip.org/', + python_requires=">=3.6", + url="https://www.zulip.org/", project_urls={ "Source": "https://github.com/zulip/python-zulip-api/", "Documentation": "https://zulip.com/api", }, - data_files=list(recur_expand('share/zulip', 'integrations')), + data_files=list(recur_expand("share/zulip", "integrations")), include_package_data=True, entry_points={ - 'console_scripts': [ - 'zulip-send=zulip.send:main', - 'zulip-api-examples=zulip.api_examples:main', - 'zulip-matrix-bridge=integrations.bridge_with_matrix.matrix_bridge:main', - 'zulip-api=zulip.cli:cli', + "console_scripts": [ + "zulip-send=zulip.send:main", + "zulip-api-examples=zulip.api_examples:main", + "zulip-matrix-bridge=integrations.bridge_with_matrix.matrix_bridge:main", + "zulip-api=zulip.cli:cli", ], }, - package_data={'zulip': ["py.typed"]}, + package_data={"zulip": ["py.typed"]}, ) # type: Dict[str, Any] setuptools_info = dict( install_requires=[ - 'requests[security]>=0.12.1', - 'matrix_client', - 'distro', - 'click', + "requests[security]>=0.12.1", + "matrix_client", + "distro", + "click", ], ) @@ -79,7 +79,7 @@ try: from setuptools import find_packages, setup package_info.update(setuptools_info) - package_info['packages'] = find_packages(exclude=['tests']) + package_info["packages"] = find_packages(exclude=["tests"]) except ImportError: from distutils.core import setup @@ -89,12 +89,12 @@ except ImportError: try: import requests - assert LooseVersion(requests.__version__) >= LooseVersion('0.12.1') + assert LooseVersion(requests.__version__) >= LooseVersion("0.12.1") except (ImportError, AssertionError): print("requests >=0.12.1 is not installed", file=sys.stderr) sys.exit(1) - package_info['packages'] = ['zulip'] + package_info["packages"] = ["zulip"] setup(**package_info) diff --git a/zulip/tests/test_default_arguments.py b/zulip/tests/test_default_arguments.py index 4385abe..e79f795 100755 --- a/zulip/tests/test_default_arguments.py +++ b/zulip/tests/test_default_arguments.py @@ -15,8 +15,8 @@ class TestDefaultArguments(TestCase): def test_invalid_arguments(self) -> None: parser = zulip.add_default_arguments(argparse.ArgumentParser(usage="lorem ipsum")) with self.assertRaises(SystemExit) as cm: - with patch('sys.stderr', new=io.StringIO()) as mock_stderr: - parser.parse_args(['invalid argument']) + with patch("sys.stderr", new=io.StringIO()) as mock_stderr: + parser.parse_args(["invalid argument"]) self.assertEqual(cm.exception.code, 2) # Assert that invalid arguments exit with printing the full usage (non-standard behavior) self.assertTrue( @@ -32,20 +32,20 @@ Zulip API configuration: ) ) - @patch('os.path.exists', return_value=False) + @patch("os.path.exists", return_value=False) def test_config_path_with_tilde(self, mock_os_path_exists: bool) -> None: parser = zulip.add_default_arguments(argparse.ArgumentParser(usage="lorem ipsum")) - test_path = '~/zuliprc' - args = parser.parse_args(['--config-file', test_path]) + test_path = "~/zuliprc" + args = parser.parse_args(["--config-file", test_path]) with self.assertRaises(ZulipError) as cm: zulip.init_from_options(args) expanded_test_path = os.path.abspath(os.path.expanduser(test_path)) self.assertEqual( str(cm.exception), - 'api_key or email not specified and ' - 'file {} does not exist'.format(expanded_test_path), + "api_key or email not specified and " + "file {} does not exist".format(expanded_test_path), ) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/zulip/tests/test_hash_util_decode.py b/zulip/tests/test_hash_util_decode.py index 512fcca..9129226 100644 --- a/zulip/tests/test_hash_util_decode.py +++ b/zulip/tests/test_hash_util_decode.py @@ -9,17 +9,17 @@ import zulip class TestHashUtilDecode(TestCase): def test_hash_util_decode(self) -> None: tests = [ - ('topic', 'topic'), - ('.2Edot', '.dot'), - ('.23stream.20name', '#stream name'), - ('(no.20topic)', '(no topic)'), - ('.3Cstrong.3Ebold.3C.2Fstrong.3E', 'bold'), - ('.3Asome_emoji.3A', ':some_emoji:'), + ("topic", "topic"), + (".2Edot", ".dot"), + (".23stream.20name", "#stream name"), + ("(no.20topic)", "(no topic)"), + (".3Cstrong.3Ebold.3C.2Fstrong.3E", "bold"), + (".3Asome_emoji.3A", ":some_emoji:"), ] for encoded_string, decoded_string in tests: with self.subTest(encoded_string=encoded_string): self.assertEqual(zulip.hash_util_decode(encoded_string), decoded_string) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/zulip/zulip/__init__.py b/zulip/zulip/__init__.py index 811fd81..5404d3f 100644 --- a/zulip/zulip/__init__.py +++ b/zulip/zulip/__init__.py @@ -39,7 +39,7 @@ logger = logging.getLogger(__name__) # Check that we have a recent enough version # Older versions don't provide the 'json' attribute on responses. -assert LooseVersion(requests.__version__) >= LooseVersion('0.12.1') +assert LooseVersion(requests.__version__) >= LooseVersion("0.12.1") # In newer versions, the 'json' attribute is a function, not a property requests_json_is_function = callable(requests.Response.json) @@ -125,66 +125,66 @@ def add_default_arguments( def custom_error_handling(self: argparse.ArgumentParser, message: str) -> None: self.print_help(sys.stderr) - self.exit(2, '{}: error: {}\n'.format(self.prog, message)) + self.exit(2, "{}: error: {}\n".format(self.prog, message)) parser.error = types.MethodType(custom_error_handling, parser) # type: ignore # patching function if allow_provisioning: parser.add_argument( - '--provision', - action='store_true', + "--provision", + action="store_true", dest="provision", help="install dependencies for this script (found in requirements.txt)", ) - group = parser.add_argument_group('Zulip API configuration') - group.add_argument('--site', dest="zulip_site", help="Zulip server URI", default=None) - group.add_argument('--api-key', dest="zulip_api_key", action='store') + group = parser.add_argument_group("Zulip API configuration") + group.add_argument("--site", dest="zulip_site", help="Zulip server URI", default=None) + group.add_argument("--api-key", dest="zulip_api_key", action="store") group.add_argument( - '--user', dest='zulip_email', help='Email address of the calling bot or user.' + "--user", dest="zulip_email", help="Email address of the calling bot or user." ) group.add_argument( - '--config-file', - action='store', + "--config-file", + action="store", dest="zulip_config_file", - help='''Location of an ini file containing the above - information. (default ~/.zuliprc)''', + help="""Location of an ini file containing the above + information. (default ~/.zuliprc)""", ) - group.add_argument('-v', '--verbose', action='store_true', help='Provide detailed output.') + group.add_argument("-v", "--verbose", action="store_true", help="Provide detailed output.") group.add_argument( - '--client', action='store', default=None, dest="zulip_client", help=argparse.SUPPRESS + "--client", action="store", default=None, dest="zulip_client", help=argparse.SUPPRESS ) group.add_argument( - '--insecure', - action='store_true', - dest='insecure', - help='''Do not verify the server certificate. - The https connection will not be secure.''', + "--insecure", + action="store_true", + dest="insecure", + help="""Do not verify the server certificate. + The https connection will not be secure.""", ) group.add_argument( - '--cert-bundle', - action='store', - dest='cert_bundle', - help='''Specify a file containing either the + "--cert-bundle", + action="store", + dest="cert_bundle", + help="""Specify a file containing either the server certificate, or a set of trusted CA certificates. This will be used to verify the server's identity. All - certificates should be PEM encoded.''', + certificates should be PEM encoded.""", ) group.add_argument( - '--client-cert', - action='store', - dest='client_cert', - help='''Specify a file containing a client - certificate (not needed for most deployments).''', + "--client-cert", + action="store", + dest="client_cert", + help="""Specify a file containing a client + certificate (not needed for most deployments).""", ) group.add_argument( - '--client-cert-key', - action='store', - dest='client_cert_key', - help='''Specify a file containing the client + "--client-cert-key", + action="store", + dest="client_cert_key", + help="""Specify a file containing the client certificate's key (if it is in a separate - file).''', + file).""", ) return parser @@ -193,74 +193,74 @@ def add_default_arguments( # except for the fact that is uses the deprecated `optparse` module. # We still keep it for legacy support of out-of-tree bots and integrations # depending on it. -def generate_option_group(parser: optparse.OptionParser, prefix: str = '') -> optparse.OptionGroup: +def generate_option_group(parser: optparse.OptionParser, prefix: str = "") -> optparse.OptionGroup: logging.warning( """zulip.generate_option_group is based on optparse, which is now deprecated. We recommend migrating to argparse and using zulip.add_default_arguments instead.""" ) - group = optparse.OptionGroup(parser, 'Zulip API configuration') + group = optparse.OptionGroup(parser, "Zulip API configuration") group.add_option( - '--%ssite' % (prefix,), dest="zulip_site", help="Zulip server URI", default=None + "--%ssite" % (prefix,), dest="zulip_site", help="Zulip server URI", default=None ) - group.add_option('--%sapi-key' % (prefix,), dest="zulip_api_key", action='store') + group.add_option("--%sapi-key" % (prefix,), dest="zulip_api_key", action="store") group.add_option( - '--%suser' % (prefix,), dest='zulip_email', help='Email address of the calling bot or user.' + "--%suser" % (prefix,), dest="zulip_email", help="Email address of the calling bot or user." ) group.add_option( - '--%sconfig-file' % (prefix,), - action='store', + "--%sconfig-file" % (prefix,), + action="store", dest="zulip_config_file", - help='Location of an ini file containing the\nabove information. (default ~/.zuliprc)', + help="Location of an ini file containing the\nabove information. (default ~/.zuliprc)", ) - group.add_option('-v', '--verbose', action='store_true', help='Provide detailed output.') + group.add_option("-v", "--verbose", action="store_true", help="Provide detailed output.") group.add_option( - '--%sclient' % (prefix,), - action='store', + "--%sclient" % (prefix,), + action="store", default=None, dest="zulip_client", help=optparse.SUPPRESS_HELP, ) group.add_option( - '--insecure', - action='store_true', - dest='insecure', - help='''Do not verify the server certificate. - The https connection will not be secure.''', + "--insecure", + action="store_true", + dest="insecure", + help="""Do not verify the server certificate. + The https connection will not be secure.""", ) group.add_option( - '--cert-bundle', - action='store', - dest='cert_bundle', - help='''Specify a file containing either the + "--cert-bundle", + action="store", + dest="cert_bundle", + help="""Specify a file containing either the server certificate, or a set of trusted CA certificates. This will be used to verify the server's identity. All - certificates should be PEM encoded.''', + certificates should be PEM encoded.""", ) group.add_option( - '--client-cert', - action='store', - dest='client_cert', - help='''Specify a file containing a client - certificate (not needed for most deployments).''', + "--client-cert", + action="store", + dest="client_cert", + help="""Specify a file containing a client + certificate (not needed for most deployments).""", ) group.add_option( - '--client-cert-key', - action='store', - dest='client_cert_key', - help='''Specify a file containing the client + "--client-cert-key", + action="store", + dest="client_cert_key", + help="""Specify a file containing the client certificate's key (if it is in a separate - file).''', + file).""", ) return group -def init_from_options(options: Any, client: Optional[str] = None) -> 'Client': +def init_from_options(options: Any, client: Optional[str] = None) -> "Client": - if getattr(options, 'provision', False): - requirements_path = os.path.abspath(os.path.join(sys.path[0], 'requirements.txt')) + if getattr(options, "provision", False): + requirements_path = os.path.abspath(os.path.join(sys.path[0], "requirements.txt")) try: import pip except ImportError: @@ -270,11 +270,11 @@ def init_from_options(options: Any, client: Optional[str] = None) -> 'Client': "https://pip.pypa.io/en/stable/installing/" ) sys.exit(1) - if not pip.main(['install', '--upgrade', '--requirement', requirements_path]): + if not pip.main(["install", "--upgrade", "--requirement", requirements_path]): print( "{color_green}You successfully provisioned the dependencies for {script}.{end_color}".format( - color_green='\033[92m', - end_color='\033[0m', + color_green="\033[92m", + end_color="\033[0m", script=os.path.splitext(os.path.basename(sys.argv[0]))[0], ) ) @@ -382,7 +382,7 @@ class Client: if insecure is None: # Be quite strict about what is accepted so that users don't # disable security unintentionally. - insecure_setting = os.environ.get('ZULIP_ALLOW_INSECURE') + insecure_setting = os.environ.get("ZULIP_ALLOW_INSECURE") if insecure_setting is not None: insecure = validate_boolean_field(insecure_setting) @@ -415,7 +415,7 @@ class Client: if insecure is None and config.has_option("api", "insecure"): # Be quite strict about what is accepted so that users don't # disable security unintentionally. - insecure_setting = config.get('api', 'insecure') + insecure_setting = config.get("api", "insecure") insecure = validate_boolean_field(insecure_setting) @@ -455,9 +455,9 @@ class Client: if insecure: logger.warning( - 'Insecure mode enabled. The server\'s SSL/TLS ' - 'certificate will not be validated, making the ' - 'HTTPS connection potentially insecure' + "Insecure mode enabled. The server's SSL/TLS " + "certificate will not be validated, making the " + "HTTPS connection potentially insecure" ) self.tls_verification = False # type: Union[bool, str] elif cert_bundle is not None: @@ -515,8 +515,8 @@ class Client: self.session = session def get_user_agent(self) -> str: - vendor = '' - vendor_version = '' + vendor = "" + vendor_version = "" try: vendor = platform.system() vendor_version = platform.release() @@ -575,9 +575,9 @@ class Client: assert self.session is not None query_state = { - 'had_error_retry': False, - 'request': request, - 'failures': 0, + "had_error_retry": False, + "request": request, + "failures": 0, } # type: Dict[str, Any] def error_retry(error_string: str) -> bool: @@ -618,7 +618,7 @@ class Client: kwargs = {kwarg: query_state["request"]} if files: - kwargs['files'] = req_files + kwargs["files"] = req_files # Actually make the request! res = self.session.request( @@ -631,7 +631,7 @@ class Client: self.has_connected = True # On 50x errors, try again after a short sleep - if str(res.status_code).startswith('5'): + if str(res.status_code).startswith("5"): if error_retry(" (server %s)" % (res.status_code,)): continue # Otherwise fall through and process the python-requests error normally @@ -643,7 +643,7 @@ class Client: isinstance(e, requests.exceptions.SSLError) and str(e) != "The read operation timed out" ): - raise UnrecoverableNetworkError('SSL Error') + raise UnrecoverableNetworkError("SSL Error") if longpolling: # When longpolling, we expect the timeout to fire, # and the correct response is to just retry @@ -651,7 +651,7 @@ class Client: else: end_error_retry(False) return { - 'msg': "Connection error:\n%s" % (traceback.format_exc(),), + "msg": "Connection error:\n%s" % (traceback.format_exc(),), "result": "connection-error", } except requests.exceptions.ConnectionError: @@ -660,19 +660,19 @@ class Client: # go into retry logic, because the most likely scenario here is # that somebody just hasn't started their server, or they passed # in an invalid site. - raise UnrecoverableNetworkError('cannot connect to server ' + self.base_url) + raise UnrecoverableNetworkError("cannot connect to server " + self.base_url) if error_retry(""): continue end_error_retry(False) return { - 'msg': "Connection error:\n%s" % (traceback.format_exc(),), + "msg": "Connection error:\n%s" % (traceback.format_exc(),), "result": "connection-error", } except Exception: # We'll split this out into more cases as we encounter new bugs. return { - 'msg': "Unexpected error:\n%s" % (traceback.format_exc(),), + "msg": "Unexpected error:\n%s" % (traceback.format_exc(),), "result": "unexpected-error", } @@ -689,7 +689,7 @@ class Client: return json_result end_error_retry(False) return { - 'msg': "Unexpected error from the server", + "msg": "Unexpected error from the server", "result": "http-error", "status_code": res.status_code, } @@ -736,12 +736,12 @@ class Client: res = self.register(None, None, **kwargs) else: res = self.register(event_types, narrow, **kwargs) - if 'error' in res['result']: + if "error" in res["result"]: if self.verbose: - print("Server returned error:\n%s" % (res['msg'],)) + print("Server returned error:\n%s" % (res["msg"],)) time.sleep(1) else: - return (res['queue_id'], res['last_event_id']) + return (res["queue_id"], res["last_event_id"]) queue_id = None # Make long-polling requests with `get_events`. Once a request @@ -752,7 +752,7 @@ class Client: (queue_id, last_event_id) = do_register() res = self.get_events(queue_id=queue_id, last_event_id=last_event_id) - if 'error' in res['result']: + if "error" in res["result"]: if res["result"] == "http-error": if self.verbose: print("HTTP error fetching events -- probably a server restart") @@ -787,138 +787,138 @@ class Client: time.sleep(1) continue - for event in res['events']: - last_event_id = max(last_event_id, int(event['id'])) + for event in res["events"]: + last_event_id = max(last_event_id, int(event["id"])) callback(event) def call_on_each_message( self, callback: Callable[[Dict[str, Any]], None], **kwargs: object ) -> None: def event_callback(event: Dict[str, Any]) -> None: - if event['type'] == 'message': - callback(event['message']) + if event["type"] == "message": + callback(event["message"]) - self.call_on_each_event(event_callback, ['message'], None, **kwargs) + self.call_on_each_event(event_callback, ["message"], None, **kwargs) def get_messages(self, message_filters: Dict[str, Any]) -> Dict[str, Any]: - ''' + """ See examples/get-messages for example usage - ''' - return self.call_endpoint(url='messages', method='GET', request=message_filters) + """ + return self.call_endpoint(url="messages", method="GET", request=message_filters) def check_messages_match_narrow(self, **request: Dict[str, Any]) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.check_messages_match_narrow(msg_ids=[11, 12], narrow=[{'operator': 'has', 'operand': 'link'}] ) {'result': 'success', 'msg': '', 'messages': [{...}, {...}]} - ''' - return self.call_endpoint(url='messages/matches_narrow', method='GET', request=request) + """ + return self.call_endpoint(url="messages/matches_narrow", method="GET", request=request) def get_raw_message(self, message_id: int) -> Dict[str, str]: - ''' + """ See examples/get-raw-message for example usage - ''' - return self.call_endpoint(url='messages/{}'.format(message_id), method='GET') + """ + return self.call_endpoint(url="messages/{}".format(message_id), method="GET") def send_message(self, message_data: Dict[str, Any]) -> Dict[str, Any]: - ''' + """ See examples/send-message for example usage. - ''' + """ return self.call_endpoint( - url='messages', + url="messages", request=message_data, ) def upload_file(self, file: IO[Any]) -> Dict[str, Any]: - ''' + """ See examples/upload-file for example usage. - ''' - return self.call_endpoint(url='user_uploads', files=[file]) + """ + return self.call_endpoint(url="user_uploads", files=[file]) def get_attachments(self) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.get_attachments() {'result': 'success', 'msg': '', 'attachments': [{...}, {...}]} - ''' - return self.call_endpoint(url='attachments', method='GET') + """ + return self.call_endpoint(url="attachments", method="GET") def update_message(self, message_data: Dict[str, Any]) -> Dict[str, Any]: - ''' + """ See examples/edit-message for example usage. - ''' + """ return self.call_endpoint( - url='messages/%d' % (message_data['message_id'],), - method='PATCH', + url="messages/%d" % (message_data["message_id"],), + method="PATCH", request=message_data, ) def delete_message(self, message_id: int) -> Dict[str, Any]: - ''' + """ See examples/delete-message for example usage. - ''' - return self.call_endpoint(url='messages/{}'.format(message_id), method='DELETE') + """ + return self.call_endpoint(url="messages/{}".format(message_id), method="DELETE") def update_message_flags(self, update_data: Dict[str, Any]) -> Dict[str, Any]: - ''' + """ See examples/update-flags for example usage. - ''' - return self.call_endpoint(url='messages/flags', method='POST', request=update_data) + """ + return self.call_endpoint(url="messages/flags", method="POST", request=update_data) def mark_all_as_read(self) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.mark_all_as_read() {'result': 'success', 'msg': ''} - ''' + """ return self.call_endpoint( - url='mark_all_as_read', - method='POST', + url="mark_all_as_read", + method="POST", ) def mark_stream_as_read(self, stream_id: int) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.mark_stream_as_read(42) {'result': 'success', 'msg': ''} - ''' + """ return self.call_endpoint( - url='mark_stream_as_read', - method='POST', - request={'stream_id': stream_id}, + url="mark_stream_as_read", + method="POST", + request={"stream_id": stream_id}, ) def mark_topic_as_read(self, stream_id: int, topic_name: str) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.mark_all_as_read(42, 'new coffee machine') {'result': 'success', 'msg': ''} - ''' + """ return self.call_endpoint( - url='mark_topic_as_read', - method='POST', + url="mark_topic_as_read", + method="POST", request={ - 'stream_id': stream_id, - 'topic_name': topic_name, + "stream_id": stream_id, + "topic_name": topic_name, }, ) def get_message_history(self, message_id: int) -> Dict[str, Any]: - ''' + """ See examples/message-history for example usage. - ''' - return self.call_endpoint(url='messages/{}/history'.format(message_id), method='GET') + """ + return self.call_endpoint(url="messages/{}/history".format(message_id), method="GET") def add_reaction(self, reaction_data: Dict[str, Any]) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.add_reaction({ @@ -928,15 +928,15 @@ class Client: 'reaction_type': 'unicode_emoji' }) {'result': 'success', 'msg': ''} - ''' + """ return self.call_endpoint( - url='messages/{}/reactions'.format(reaction_data['message_id']), - method='POST', + url="messages/{}/reactions".format(reaction_data["message_id"]), + method="POST", request=reaction_data, ) def remove_reaction(self, reaction_data: Dict[str, Any]) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.remove_reaction({ @@ -946,44 +946,44 @@ class Client: 'reaction_type': 'unicode_emoji' }) {'msg': '', 'result': 'success'} - ''' + """ return self.call_endpoint( - url='messages/{}/reactions'.format(reaction_data['message_id']), - method='DELETE', + url="messages/{}/reactions".format(reaction_data["message_id"]), + method="DELETE", request=reaction_data, ) def get_realm_emoji(self) -> Dict[str, Any]: - ''' + """ See examples/realm-emoji for example usage. - ''' - return self.call_endpoint(url='realm/emoji', method='GET') + """ + return self.call_endpoint(url="realm/emoji", method="GET") def upload_custom_emoji(self, emoji_name: str, file_obj: IO[Any]) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.upload_custom_emoji(emoji_name, file_obj) {'result': 'success', 'msg': ''} - ''' + """ return self.call_endpoint( - 'realm/emoji/{}'.format(emoji_name), method='POST', files=[file_obj] + "realm/emoji/{}".format(emoji_name), method="POST", files=[file_obj] ) def delete_custom_emoji(self, emoji_name: str) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.delete_custom_emoji("green_tick") {'result': 'success', 'msg': ''} - ''' + """ return self.call_endpoint( - url='realm/emoji/{}'.format(emoji_name), - method='DELETE', + url="realm/emoji/{}".format(emoji_name), + method="DELETE", ) def get_realm_linkifiers(self) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.get_realm_linkifiers() @@ -998,122 +998,122 @@ class Client: }, ] } - ''' + """ return self.call_endpoint( - url='realm/linkifiers', - method='GET', + url="realm/linkifiers", + method="GET", ) def add_realm_filter(self, pattern: str, url_format_string: str) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.add_realm_filter('#(?P[0-9]+)', 'https://github.com/zulip/zulip/issues/%(id)s') {'result': 'success', 'msg': '', 'id': 42} - ''' + """ return self.call_endpoint( - url='realm/filters', - method='POST', + url="realm/filters", + method="POST", request={ - 'pattern': pattern, - 'url_format_string': url_format_string, + "pattern": pattern, + "url_format_string": url_format_string, }, ) def remove_realm_filter(self, filter_id: int) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.remove_realm_filter(42) {'result': 'success', 'msg': ''} - ''' + """ return self.call_endpoint( - url='realm/filters/{}'.format(filter_id), - method='DELETE', + url="realm/filters/{}".format(filter_id), + method="DELETE", ) def get_realm_profile_fields(self) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.get_realm_profile_fields() {'result': 'success', 'msg': '', 'custom_fields': [{...}, {...}, {...}, {...}]} - ''' + """ return self.call_endpoint( - url='realm/profile_fields', - method='GET', + url="realm/profile_fields", + method="GET", ) def create_realm_profile_field(self, **request: Any) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.create_realm_profile_field(name='Phone', hint='Contact No.', field_type=1) {'result': 'success', 'msg': '', 'id': 9} - ''' + """ return self.call_endpoint( - url='realm/profile_fields', - method='POST', + url="realm/profile_fields", + method="POST", request=request, ) def remove_realm_profile_field(self, field_id: int) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.remove_realm_profile_field(field_id=9) {'result': 'success', 'msg': ''} - ''' + """ return self.call_endpoint( - url='realm/profile_fields/{}'.format(field_id), - method='DELETE', + url="realm/profile_fields/{}".format(field_id), + method="DELETE", ) def reorder_realm_profile_fields(self, **request: Any) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.reorder_realm_profile_fields(order=[8, 7, 6, 5, 4, 3, 2, 1]) {'result': 'success', 'msg': ''} - ''' + """ return self.call_endpoint( - url='realm/profile_fields', - method='PATCH', + url="realm/profile_fields", + method="PATCH", request=request, ) def update_realm_profile_field(self, field_id: int, **request: Any) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.update_realm_profile_field(field_id=1, name='Email') {'result': 'success', 'msg': ''} - ''' + """ return self.call_endpoint( - url='realm/profile_fields/{}'.format(field_id), - method='PATCH', + url="realm/profile_fields/{}".format(field_id), + method="PATCH", request=request, ) def get_server_settings(self) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.get_server_settings() {'msg': '', 'result': 'success', 'zulip_version': '1.9.0', 'push_notifications_enabled': False, ...} - ''' + """ return self.call_endpoint( - url='server_settings', - method='GET', + url="server_settings", + method="GET", ) def get_events(self, **request: Any) -> Dict[str, Any]: - ''' + """ See the register() method for example usage. - ''' + """ return self.call_endpoint( - url='events', - method='GET', + url="events", + method="GET", longpolling=True, request=request, ) @@ -1124,14 +1124,14 @@ class Client: narrow: Optional[List[List[str]]] = None, **kwargs: object, ) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.register(['message']) {u'msg': u'', u'max_message_id': 112, u'last_event_id': -1, u'result': u'success', u'queue_id': u'1482093786:2'} >>> client.get_events(queue_id='1482093786:2', last_event_id=0) {...} - ''' + """ if narrow is None: narrow = [] @@ -1139,19 +1139,19 @@ class Client: request = dict(event_types=event_types, narrow=narrow, **kwargs) return self.call_endpoint( - url='register', + url="register", request=request, ) def deregister(self, queue_id: str, timeout: Optional[float] = None) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.register(['message']) {u'msg': u'', u'max_message_id': 113, u'last_event_id': -1, u'result': u'success', u'queue_id': u'1482093786:3'} >>> client.deregister('1482093786:3') {u'msg': u'', u'result': u'success'} - ''' + """ request = dict(queue_id=queue_id) return self.call_endpoint( @@ -1162,44 +1162,44 @@ class Client: ) def get_profile(self, request: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.get_profile() {u'user_id': 5, u'full_name': u'Iago', u'short_name': u'iago', ...} - ''' + """ return self.call_endpoint( - url='users/me', - method='GET', + url="users/me", + method="GET", request=request, ) def get_user_presence(self, email: str) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.get_user_presence('iago@zulip.com') {'presence': {'website': {'timestamp': 1486799122, 'status': 'active'}}, 'result': 'success', 'msg': ''} - ''' + """ return self.call_endpoint( - url='users/%s/presence' % (email,), - method='GET', + url="users/%s/presence" % (email,), + method="GET", ) def get_realm_presence(self) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.get_realm_presence() {'presences': {...}, 'result': 'success', 'msg': ''} - ''' + """ return self.call_endpoint( - url='realm/presence', - method='GET', + url="realm/presence", + method="GET", ) def update_presence(self, request: Dict[str, Any]) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.update_presence({ @@ -1208,118 +1208,118 @@ class Client: new_user_input=False, }) {'result': 'success', 'server_timestamp': 1333649180.7073195, 'presences': {'iago@zulip.com': { ... }}, 'msg': ''} - ''' + """ return self.call_endpoint( - url='users/me/presence', - method='POST', + url="users/me/presence", + method="POST", request=request, ) def get_streams(self, **request: Any) -> Dict[str, Any]: - ''' + """ See examples/get-public-streams for example usage. - ''' + """ return self.call_endpoint( - url='streams', - method='GET', + url="streams", + method="GET", request=request, ) def update_stream(self, stream_data: Dict[str, Any]) -> Dict[str, Any]: - ''' + """ See examples/edit-stream for example usage. - ''' + """ return self.call_endpoint( - url='streams/{}'.format(stream_data['stream_id']), - method='PATCH', + url="streams/{}".format(stream_data["stream_id"]), + method="PATCH", request=stream_data, ) def delete_stream(self, stream_id: int) -> Dict[str, Any]: - ''' + """ See examples/delete-stream for example usage. - ''' + """ return self.call_endpoint( - url='streams/{}'.format(stream_id), - method='DELETE', + url="streams/{}".format(stream_id), + method="DELETE", ) def add_default_stream(self, stream_id: int) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.add_default_stream(5) {'result': 'success', 'msg': ''} - ''' + """ return self.call_endpoint( - url='default_streams', - method='POST', - request={'stream_id': stream_id}, + url="default_streams", + method="POST", + request={"stream_id": stream_id}, ) def get_user_by_id(self, user_id: int, **request: Any) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.get_user_by_id(8, include_custom_profile_fields=True) {'result': 'success', 'msg': '', 'user': [{...}, {...}]} - ''' + """ return self.call_endpoint( - url='users/{}'.format(user_id), - method='GET', + url="users/{}".format(user_id), + method="GET", request=request, ) def deactivate_user_by_id(self, user_id: int) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.deactivate_user_by_id(8) {'result': 'success', 'msg': ''} - ''' + """ return self.call_endpoint( - url='users/{}'.format(user_id), - method='DELETE', + url="users/{}".format(user_id), + method="DELETE", ) def reactivate_user_by_id(self, user_id: int) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.reactivate_user_by_id(8) {'result': 'success', 'msg': ''} - ''' + """ return self.call_endpoint( - url='users/{}/reactivate'.format(user_id), - method='POST', + url="users/{}/reactivate".format(user_id), + method="POST", ) def update_user_by_id(self, user_id: int, **request: Any) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.update_user_by_id(8, full_name="New Name") {'result': 'success', 'msg': ''} - ''' + """ for key, value in request.items(): request[key] = json.dumps(value) - return self.call_endpoint(url='users/{}'.format(user_id), method='PATCH', request=request) + return self.call_endpoint(url="users/{}".format(user_id), method="PATCH", request=request) def get_users(self, request: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - ''' + """ See examples/list-users for example usage. - ''' + """ return self.call_endpoint( - url='users', - method='GET', + url="users", + method="GET", request=request, ) @@ -1331,34 +1331,34 @@ class Client: return self.get_users(request=request) def get_alert_words(self) -> Dict[str, Any]: - ''' + """ See examples/alert-words for example usage. - ''' - return self.call_endpoint(url='users/me/alert_words', method='GET') + """ + return self.call_endpoint(url="users/me/alert_words", method="GET") def add_alert_words(self, alert_words: List[str]) -> Dict[str, Any]: - ''' + """ See examples/alert-words for example usage. - ''' + """ return self.call_endpoint( - url='users/me/alert_words', method='POST', request={'alert_words': alert_words} + url="users/me/alert_words", method="POST", request={"alert_words": alert_words} ) def remove_alert_words(self, alert_words: List[str]) -> Dict[str, Any]: - ''' + """ See examples/alert-words for example usage. - ''' + """ return self.call_endpoint( - url='users/me/alert_words', method='DELETE', request={'alert_words': alert_words} + url="users/me/alert_words", method="DELETE", request={"alert_words": alert_words} ) def get_subscriptions(self, request: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - ''' + """ See examples/get-subscriptions for example usage. - ''' + """ return self.call_endpoint( - url='users/me/subscriptions', - method='GET', + url="users/me/subscriptions", + method="GET", request=request, ) @@ -1369,53 +1369,53 @@ class Client: return self.get_subscriptions(request) def add_subscriptions(self, streams: Iterable[Dict[str, Any]], **kwargs: Any) -> Dict[str, Any]: - ''' + """ See examples/subscribe for example usage. - ''' + """ request = dict(subscriptions=streams, **kwargs) return self.call_endpoint( - url='users/me/subscriptions', + url="users/me/subscriptions", request=request, ) def remove_subscriptions( self, streams: Iterable[str], principals: Union[Sequence[str], Sequence[int]] = [] ) -> Dict[str, Any]: - ''' + """ See examples/unsubscribe for example usage. - ''' + """ request = dict(subscriptions=streams, principals=principals) return self.call_endpoint( - url='users/me/subscriptions', - method='DELETE', + url="users/me/subscriptions", + method="DELETE", request=request, ) def get_subscription_status(self, user_id: int, stream_id: int) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.get_subscription_status(user_id=7, stream_id=1) {'result': 'success', 'msg': '', 'is_subscribed': False} - ''' + """ return self.call_endpoint( - url='users/{}/subscriptions/{}'.format(user_id, stream_id), - method='GET', + url="users/{}/subscriptions/{}".format(user_id, stream_id), + method="GET", ) def mute_topic(self, request: Dict[str, Any]) -> Dict[str, Any]: - ''' + """ See examples/mute-topic for example usage. - ''' + """ return self.call_endpoint( - url='users/me/subscriptions/muted_topics', method='PATCH', request=request + url="users/me/subscriptions/muted_topics", method="PATCH", request=request ) def update_subscription_settings( self, subscription_data: List[Dict[str, Any]] ) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.update_subscription_settings([{ @@ -1429,15 +1429,15 @@ class Client: 'value': 'f00' }]) {'result': 'success', 'msg': '', 'subscription_data': [{...}, {...}]} - ''' + """ return self.call_endpoint( - url='users/me/subscriptions/properties', - method='POST', - request={'subscription_data': subscription_data}, + url="users/me/subscriptions/properties", + method="POST", + request={"subscription_data": subscription_data}, ) def update_notification_settings(self, notification_settings: Dict[str, Any]) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.update_notification_settings({ @@ -1445,44 +1445,44 @@ class Client: 'enable_offline_push_notifications': False, }) {'enable_offline_push_notifications': False, 'enable_stream_push_notifications': True, 'msg': '', 'result': 'success'} - ''' + """ return self.call_endpoint( - url='settings/notifications', - method='PATCH', + url="settings/notifications", + method="PATCH", request=notification_settings, ) def get_stream_id(self, stream: str) -> Dict[str, Any]: - ''' + """ Example usage: client.get_stream_id('devel') - ''' - stream_encoded = urllib.parse.quote(stream, safe='') - url = 'get_stream_id?stream=%s' % (stream_encoded,) + """ + stream_encoded = urllib.parse.quote(stream, safe="") + url = "get_stream_id?stream=%s" % (stream_encoded,) return self.call_endpoint( url=url, - method='GET', + method="GET", request=None, ) def get_stream_topics(self, stream_id: int) -> Dict[str, Any]: - ''' + """ See examples/get-stream-topics for example usage. - ''' - return self.call_endpoint(url='users/me/{}/topics'.format(stream_id), method='GET') + """ + return self.call_endpoint(url="users/me/{}/topics".format(stream_id), method="GET") def get_user_groups(self) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.get_user_groups() {'result': 'success', 'msg': '', 'user_groups': [{...}, {...}]} - ''' + """ return self.call_endpoint( - url='user_groups', - method='GET', + url="user_groups", + method="GET", ) def create_user_group(self, group_data: Dict[str, Any]) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.create_user_group({ 'name': 'marketing', @@ -1490,15 +1490,15 @@ class Client: 'members': [4, 8, 15, 16, 23, 42], }) {'msg': '', 'result': 'success'} - ''' + """ return self.call_endpoint( - url='user_groups/create', - method='POST', + url="user_groups/create", + method="POST", request=group_data, ) def update_user_group(self, group_data: Dict[str, Any]) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.update_user_group({ @@ -1507,29 +1507,29 @@ class Client: 'description': "Members of ACME Corp.'s marketing team.", }) {'description': 'Description successfully updated.', 'name': 'Name successfully updated.', 'result': 'success', 'msg': ''} - ''' + """ return self.call_endpoint( - url='user_groups/{}'.format(group_data['group_id']), - method='PATCH', + url="user_groups/{}".format(group_data["group_id"]), + method="PATCH", request=group_data, ) def remove_user_group(self, group_id: int) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.remove_user_group(42) {'msg': '', 'result': 'success'} - ''' + """ return self.call_endpoint( - url='user_groups/{}'.format(group_id), - method='DELETE', + url="user_groups/{}".format(group_id), + method="DELETE", ) def update_user_group_members( self, user_group_id: int, group_data: Dict[str, Any] ) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.update_user_group_members(1, { @@ -1537,68 +1537,68 @@ class Client: 'add': [11], }) {'msg': '', 'result': 'success'} - ''' + """ return self.call_endpoint( - url='user_groups/{}/members'.format(user_group_id), - method='POST', + url="user_groups/{}/members".format(user_group_id), + method="POST", request=group_data, ) def get_subscribers(self, **request: Any) -> Dict[str, Any]: - ''' + """ Example usage: client.get_subscribers(stream='devel') - ''' - response = self.get_stream_id(request['stream']) - if response['result'] == 'error': + """ + response = self.get_stream_id(request["stream"]) + if response["result"] == "error": return response - stream_id = response['stream_id'] - url = 'streams/%d/members' % (stream_id,) + stream_id = response["stream_id"] + url = "streams/%d/members" % (stream_id,) return self.call_endpoint( url=url, - method='GET', + method="GET", request=request, ) def render_message(self, request: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.render_message(request=dict(content='foo **bar**')) {u'msg': u'', u'rendered': u'

foo bar

', u'result': u'success'} - ''' + """ return self.call_endpoint( - url='messages/render', - method='POST', + url="messages/render", + method="POST", request=request, ) def create_user(self, request: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - ''' + """ See examples/create-user for example usage. - ''' + """ return self.call_endpoint( - method='POST', - url='users', + method="POST", + url="users", request=request, ) def update_storage(self, request: Dict[str, Any]) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.update_storage({'storage': {"entry 1": "value 1", "entry 2": "value 2", "entry 3": "value 3"}}) >>> client.get_storage({'keys': ["entry 1", "entry 3"]}) {'result': 'success', 'storage': {'entry 1': 'value 1', 'entry 3': 'value 3'}, 'msg': ''} - ''' + """ return self.call_endpoint( - url='bot_storage', - method='PUT', + url="bot_storage", + method="PUT", request=request, ) def get_storage(self, request: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.update_storage({'storage': {"entry 1": "value 1", "entry 2": "value 2", "entry 3": "value 3"}}) @@ -1606,23 +1606,23 @@ class Client: {'result': 'success', 'storage': {"entry 1": "value 1", "entry 2": "value 2", "entry 3": "value 3"}, 'msg': ''} >>> client.get_storage({'keys': ["entry 1", "entry 3"]}) {'result': 'success', 'storage': {'entry 1': 'value 1', 'entry 3': 'value 3'}, 'msg': ''} - ''' + """ return self.call_endpoint( - url='bot_storage', - method='GET', + url="bot_storage", + method="GET", request=request, ) def set_typing_status(self, request: Dict[str, Any]) -> Dict[str, Any]: - ''' + """ Example usage: >>> client.set_typing_status({ 'op': 'start', 'to': [9, 10], }) {'result': 'success', 'msg': ''} - ''' - return self.call_endpoint(url='typing', method='POST', request=request) + """ + return self.call_endpoint(url="typing", method="POST", request=request) def move_topic( self, @@ -1631,11 +1631,11 @@ class Client: topic: str, new_topic: Optional[str] = None, message_id: Optional[int] = None, - propagate_mode: str = 'change_all', + propagate_mode: str = "change_all", notify_old_topic: bool = True, notify_new_topic: bool = True, ) -> Dict[str, Any]: - ''' + """ Move a topic from ``stream`` to ``new_stream`` The topic will be renamed if ``new_topic`` is provided. @@ -1649,56 +1649,56 @@ class Client: >>> client.move_topic('stream_a', 'stream_b', 'my_topic') {'result': 'success', 'msg': ''} - ''' + """ # get IDs for source and target streams result = self.get_stream_id(stream) - if result['result'] != 'success': + if result["result"] != "success": return result - stream = result['stream_id'] + stream = result["stream_id"] result = self.get_stream_id(new_stream) - if result['result'] != 'success': + if result["result"] != "success": return result - new_stream = result['stream_id'] + new_stream = result["stream_id"] if message_id is None: - if propagate_mode != 'change_all': + if propagate_mode != "change_all": raise AttributeError( - 'A message_id must be provided if ' 'propagate_mode isn\'t "change_all"' + "A message_id must be provided if " 'propagate_mode isn\'t "change_all"' ) # ask the server for the latest message ID in the topic. result = self.get_messages( { - 'anchor': 'newest', - 'narrow': [ - {'operator': 'stream', 'operand': stream}, - {'operator': 'topic', 'operand': topic}, + "anchor": "newest", + "narrow": [ + {"operator": "stream", "operand": stream}, + {"operator": "topic", "operand": topic}, ], - 'num_before': 1, - 'num_after': 0, + "num_before": 1, + "num_after": 0, } ) - if result['result'] != 'success': + if result["result"] != "success": return result - if len(result['messages']) <= 0: - return {'result': 'error', 'msg': 'No messages found in topic: "{}"'.format(topic)} + if len(result["messages"]) <= 0: + return {"result": "error", "msg": 'No messages found in topic: "{}"'.format(topic)} - message_id = result['messages'][0]['id'] + message_id = result["messages"][0]["id"] # move topic containing message to new stream request = { - 'stream_id': new_stream, - 'propagate_mode': propagate_mode, - 'topic': new_topic, - 'send_notification_to_old_thread': notify_old_topic, - 'send_notification_to_new_thread': notify_new_topic, + "stream_id": new_stream, + "propagate_mode": propagate_mode, + "topic": new_topic, + "send_notification_to_old_thread": notify_old_topic, + "send_notification_to_new_thread": notify_new_topic, } return self.call_endpoint( - url='messages/{}'.format(message_id), - method='PATCH', + url="messages/{}".format(message_id), + method="PATCH", request=request, ) @@ -1732,4 +1732,4 @@ def hash_util_decode(string: str) -> str: """ # Acknowledge custom string replacements in zulip/zulip's zerver/lib/url_encoding.py before unquoting. # NOTE: urllib.parse.unquote already does .replace('%2E', '.'). - return urllib.parse.unquote(string.replace('.', '%')) + return urllib.parse.unquote(string.replace(".", "%")) diff --git a/zulip/zulip/api_examples.py b/zulip/zulip/api_examples.py index acb3159..791edd5 100644 --- a/zulip/zulip/api_examples.py +++ b/zulip/zulip/api_examples.py @@ -11,11 +11,11 @@ def main() -> None: Prints the path to the Zulip API example scripts.""" parser = argparse.ArgumentParser(usage=usage) parser.add_argument( - 'script_name', nargs='?', default='', help='print path to the script ' + "script_name", nargs="?", default="", help="print path to the script " ) args = parser.parse_args() zulip_path = os.path.abspath(os.path.dirname(zulip.__file__)) - examples_path = os.path.abspath(os.path.join(zulip_path, 'examples', args.script_name)) + examples_path = os.path.abspath(os.path.join(zulip_path, "examples", args.script_name)) if os.path.isdir(examples_path) or (args.script_name and os.path.isfile(examples_path)): print(examples_path) else: @@ -26,5 +26,5 @@ Prints the path to the Zulip API example scripts.""" ) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/zulip/zulip/examples/alert-words b/zulip/zulip/examples/alert-words index 03278e8..4255eed 100755 --- a/zulip/zulip/examples/alert-words +++ b/zulip/zulip/examples/alert-words @@ -14,17 +14,17 @@ Example: alert-words remove banana parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage)) -parser.add_argument('operation', choices=['get', 'add', 'remove'], type=str) -parser.add_argument('words', type=str, nargs='*') +parser.add_argument("operation", choices=["get", "add", "remove"], type=str) +parser.add_argument("words", type=str, nargs="*") options = parser.parse_args() client = zulip.init_from_options(options) -if options.operation == 'get': +if options.operation == "get": result = client.get_alert_words() -elif options.operation == 'add': +elif options.operation == "add": result = client.add_alert_words(options.words) -elif options.operation == 'remove': +elif options.operation == "remove": result = client.remove_alert_words(options.words) print(result) diff --git a/zulip/zulip/examples/create-user b/zulip/zulip/examples/create-user index 1da83ef..d58f28b 100755 --- a/zulip/zulip/examples/create-user +++ b/zulip/zulip/examples/create-user @@ -15,10 +15,10 @@ Specify your Zulip API credentials and server in a ~/.zuliprc file or using the import zulip parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage)) -parser.add_argument('--new-email', required=True) -parser.add_argument('--new-password', required=True) -parser.add_argument('--new-full-name', required=True) -parser.add_argument('--new-short-name', required=True) +parser.add_argument("--new-email", required=True) +parser.add_argument("--new-password", required=True) +parser.add_argument("--new-full-name", required=True) +parser.add_argument("--new-short-name", required=True) options = parser.parse_args() client = zulip.init_from_options(options) @@ -26,10 +26,10 @@ client = zulip.init_from_options(options) print( client.create_user( { - 'email': options.new_email, - 'password': options.new_password, - 'full_name': options.new_full_name, - 'short_name': options.new_short_name, + "email": options.new_email, + "password": options.new_password, + "full_name": options.new_full_name, + "short_name": options.new_short_name, } ) ) diff --git a/zulip/zulip/examples/delete-message b/zulip/zulip/examples/delete-message index b657f06..d94a631 100755 --- a/zulip/zulip/examples/delete-message +++ b/zulip/zulip/examples/delete-message @@ -13,7 +13,7 @@ Example: delete-message 42 parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage)) -parser.add_argument('message_id', type=int) +parser.add_argument("message_id", type=int) options = parser.parse_args() client = zulip.init_from_options(options) diff --git a/zulip/zulip/examples/delete-stream b/zulip/zulip/examples/delete-stream index ca0eb7d..eb16001 100755 --- a/zulip/zulip/examples/delete-stream +++ b/zulip/zulip/examples/delete-stream @@ -11,7 +11,7 @@ Example: delete-stream 42 parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage)) -parser.add_argument('stream_id', type=int) +parser.add_argument("stream_id", type=int) options = parser.parse_args() client = zulip.init_from_options(options) diff --git a/zulip/zulip/examples/edit-message b/zulip/zulip/examples/edit-message index b1a8c53..46b95b6 100755 --- a/zulip/zulip/examples/edit-message +++ b/zulip/zulip/examples/edit-message @@ -15,9 +15,9 @@ Specify your Zulip API credentials and server in a ~/.zuliprc file or using the import zulip parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage)) -parser.add_argument('--message-id', type=int, required=True) -parser.add_argument('--subject', default="") -parser.add_argument('--content', default="") +parser.add_argument("--message-id", type=int, required=True) +parser.add_argument("--subject", default="") +parser.add_argument("--content", default="") options = parser.parse_args() client = zulip.init_from_options(options) diff --git a/zulip/zulip/examples/edit-stream b/zulip/zulip/examples/edit-stream index 8554ce5..306010e 100755 --- a/zulip/zulip/examples/edit-stream +++ b/zulip/zulip/examples/edit-stream @@ -19,12 +19,12 @@ def quote(string: str) -> str: parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage)) -parser.add_argument('--stream-id', type=int, required=True) -parser.add_argument('--description') -parser.add_argument('--new-name') -parser.add_argument('--private', action='store_true') -parser.add_argument('--announcement-only', action='store_true') -parser.add_argument('--history-public-to-subscribers', action='store_true') +parser.add_argument("--stream-id", type=int, required=True) +parser.add_argument("--description") +parser.add_argument("--new-name") +parser.add_argument("--private", action="store_true") +parser.add_argument("--announcement-only", action="store_true") +parser.add_argument("--history-public-to-subscribers", action="store_true") options = parser.parse_args() client = zulip.init_from_options(options) @@ -32,12 +32,12 @@ client = zulip.init_from_options(options) print( client.update_stream( { - 'stream_id': options.stream_id, - 'description': quote(options.description), - 'new_name': quote(options.new_name), - 'is_private': options.private, - 'is_announcement_only': options.announcement_only, - 'history_public_to_subscribers': options.history_public_to_subscribers, + "stream_id": options.stream_id, + "description": quote(options.description), + "new_name": quote(options.new_name), + "is_private": options.private, + "is_announcement_only": options.announcement_only, + "history_public_to_subscribers": options.history_public_to_subscribers, } ) ) diff --git a/zulip/zulip/examples/get-history b/zulip/zulip/examples/get-history index 6bdd858..c77c81b 100644 --- a/zulip/zulip/examples/get-history +++ b/zulip/zulip/examples/get-history @@ -13,11 +13,11 @@ and store them in JSON format. Example: get-history --stream announce --topic important""" parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage)) -parser.add_argument('--stream', required=True, help="The stream name to get the history") -parser.add_argument('--topic', help="The topic name to get the history") +parser.add_argument("--stream", required=True, help="The stream name to get the history") +parser.add_argument("--topic", help="The topic name to get the history") parser.add_argument( - '--filename', - default='history.json', + "--filename", + default="history.json", help="The file name to store the fetched \ history.\n Default 'history.json'", ) @@ -25,19 +25,19 @@ options = parser.parse_args() client = zulip.init_from_options(options) -narrow = [{'operator': 'stream', 'operand': options.stream}] +narrow = [{"operator": "stream", "operand": options.stream}] if options.topic: - narrow.append({'operator': 'topic', 'operand': options.topic}) + narrow.append({"operator": "topic", "operand": options.topic}) request = { # Initially we have the anchor as 0, so that it starts fetching # from the oldest message in the narrow - 'anchor': 0, - 'num_before': 0, - 'num_after': 1000, - 'narrow': narrow, - 'client_gravatar': False, - 'apply_markdown': False, + "anchor": 0, + "num_before": 0, + "num_after": 1000, + "narrow": narrow, + "client_gravatar": False, + "apply_markdown": False, } all_messages = [] # type: List[Dict[str, Any]] @@ -47,17 +47,17 @@ while not found_newest: result = client.get_messages(request) try: found_newest = result["found_newest"] - if result['messages']: + if result["messages"]: # Setting the anchor to the next immediate message after the last fetched message. - request['anchor'] = result['messages'][-1]['id'] + 1 + request["anchor"] = result["messages"][-1]["id"] + 1 all_messages.extend(result["messages"]) except KeyError: # Might occur when the request is not returned with a success status - print('Error occured: Payload was:') + print("Error occured: Payload was:") print(result) quit() with open(options.filename, "w+") as f: - print('Writing %d messages...' % len(all_messages)) + print("Writing %d messages..." % len(all_messages)) f.write(json.dumps(all_messages)) diff --git a/zulip/zulip/examples/get-messages b/zulip/zulip/examples/get-messages index 96faa95..db245d4 100755 --- a/zulip/zulip/examples/get-messages +++ b/zulip/zulip/examples/get-messages @@ -17,13 +17,13 @@ Example: get-messages --use-first-unread-anchor --num-before=5 \\ parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage)) -parser.add_argument('--anchor', type=int) -parser.add_argument('--use-first-unread-anchor', action='store_true') -parser.add_argument('--num-before', type=int, required=True) -parser.add_argument('--num-after', type=int, required=True) -parser.add_argument('--client-gravatar', action='store_true') -parser.add_argument('--apply-markdown', action='store_true') -parser.add_argument('--narrow') +parser.add_argument("--anchor", type=int) +parser.add_argument("--use-first-unread-anchor", action="store_true") +parser.add_argument("--num-before", type=int, required=True) +parser.add_argument("--num-after", type=int, required=True) +parser.add_argument("--client-gravatar", action="store_true") +parser.add_argument("--apply-markdown", action="store_true") +parser.add_argument("--narrow") options = parser.parse_args() client = zulip.init_from_options(options) @@ -31,13 +31,13 @@ client = zulip.init_from_options(options) print( client.get_messages( { - 'anchor': options.anchor, - 'use_first_unread_anchor': options.use_first_unread_anchor, - 'num_before': options.num_before, - 'num_after': options.num_after, - 'narrow': options.narrow, - 'client_gravatar': options.client_gravatar, - 'apply_markdown': options.apply_markdown, + "anchor": options.anchor, + "use_first_unread_anchor": options.use_first_unread_anchor, + "num_before": options.num_before, + "num_after": options.num_after, + "narrow": options.narrow, + "client_gravatar": options.client_gravatar, + "apply_markdown": options.apply_markdown, } ) ) diff --git a/zulip/zulip/examples/get-raw-message b/zulip/zulip/examples/get-raw-message index 7302679..486970f 100755 --- a/zulip/zulip/examples/get-raw-message +++ b/zulip/zulip/examples/get-raw-message @@ -12,7 +12,7 @@ Example: get-raw-message 42 parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage)) -parser.add_argument('message_id', type=int) +parser.add_argument("message_id", type=int) options = parser.parse_args() client = zulip.init_from_options(options) diff --git a/zulip/zulip/examples/get-stream-topics b/zulip/zulip/examples/get-stream-topics index 2d29b5d..2cc5726 100755 --- a/zulip/zulip/examples/get-stream-topics +++ b/zulip/zulip/examples/get-stream-topics @@ -10,7 +10,7 @@ Get all the topics for a specific stream. import zulip parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage)) -parser.add_argument('--stream-id', required=True) +parser.add_argument("--stream-id", required=True) options = parser.parse_args() client = zulip.init_from_options(options) diff --git a/zulip/zulip/examples/get-user-presence b/zulip/zulip/examples/get-user-presence index 4e70350..a6dec2a 100755 --- a/zulip/zulip/examples/get-user-presence +++ b/zulip/zulip/examples/get-user-presence @@ -10,7 +10,7 @@ Get presence data for another user. import zulip parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage)) -parser.add_argument('--email', required=True) +parser.add_argument("--email", required=True) options = parser.parse_args() client = zulip.init_from_options(options) diff --git a/zulip/zulip/examples/message-history b/zulip/zulip/examples/message-history index ba2aea7..7689591 100755 --- a/zulip/zulip/examples/message-history +++ b/zulip/zulip/examples/message-history @@ -10,7 +10,7 @@ Example: message-history 42 """ parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage)) -parser.add_argument('message_id', type=int) +parser.add_argument("message_id", type=int) options = parser.parse_args() client = zulip.init_from_options(options) diff --git a/zulip/zulip/examples/mute-topic b/zulip/zulip/examples/mute-topic index 3f313ae..36ff07f 100755 --- a/zulip/zulip/examples/mute-topic +++ b/zulip/zulip/examples/mute-topic @@ -11,17 +11,17 @@ Example: mute-topic unmute Denmark party """ parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage)) -parser.add_argument('op', choices=['mute', 'unmute']) -parser.add_argument('stream') -parser.add_argument('topic') +parser.add_argument("op", choices=["mute", "unmute"]) +parser.add_argument("stream") +parser.add_argument("topic") options = parser.parse_args() client = zulip.init_from_options(options) -OPERATIONS = {'mute': 'add', 'unmute': 'remove'} +OPERATIONS = {"mute": "add", "unmute": "remove"} print( client.mute_topic( - {'op': OPERATIONS[options.op], 'stream': options.stream, 'topic': options.topic} + {"op": OPERATIONS[options.op], "stream": options.stream, "topic": options.topic} ) ) diff --git a/zulip/zulip/examples/send-message b/zulip/zulip/examples/send-message index 4a29830..5d9fca3 100755 --- a/zulip/zulip/examples/send-message +++ b/zulip/zulip/examples/send-message @@ -12,18 +12,18 @@ Example: send-message --type=stream commits --subject="my subject" --message="te Example: send-message user1@example.com user2@example.com """ parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage)) -parser.add_argument('recipients', nargs='+') -parser.add_argument('--subject', default='test') -parser.add_argument('--message', default='test message') -parser.add_argument('--type', default='private') +parser.add_argument("recipients", nargs="+") +parser.add_argument("--subject", default="test") +parser.add_argument("--message", default="test message") +parser.add_argument("--type", default="private") options = parser.parse_args() client = zulip.init_from_options(options) message_data = { - 'type': options.type, - 'content': options.message, - 'subject': options.subject, - 'to': options.recipients, + "type": options.type, + "content": options.message, + "subject": options.subject, + "to": options.recipients, } print(client.send_message(message_data)) diff --git a/zulip/zulip/examples/subscribe b/zulip/zulip/examples/subscribe index 07566a4..a1436e4 100755 --- a/zulip/zulip/examples/subscribe +++ b/zulip/zulip/examples/subscribe @@ -15,7 +15,7 @@ Specify your Zulip API credentials and server in a ~/.zuliprc file or using the import zulip parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage)) -parser.add_argument('--streams', action='store', required=True) +parser.add_argument("--streams", action="store", required=True) options = parser.parse_args() client = zulip.init_from_options(options) diff --git a/zulip/zulip/examples/unsubscribe b/zulip/zulip/examples/unsubscribe index d55b90e..d226e2d 100755 --- a/zulip/zulip/examples/unsubscribe +++ b/zulip/zulip/examples/unsubscribe @@ -15,7 +15,7 @@ Specify your Zulip API credentials and server in a ~/.zuliprc file or using the import zulip parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage)) -parser.add_argument('--streams', action='store', required=True) +parser.add_argument("--streams", action="store", required=True) options = parser.parse_args() client = zulip.init_from_options(options) diff --git a/zulip/zulip/examples/update-message-flags b/zulip/zulip/examples/update-message-flags index 98b8c66..ada15a6 100755 --- a/zulip/zulip/examples/update-message-flags +++ b/zulip/zulip/examples/update-message-flags @@ -12,15 +12,15 @@ Example: update-message-flags remove starred 16 23 42 """ parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage)) -parser.add_argument('op', choices=['add', 'remove']) -parser.add_argument('flag') -parser.add_argument('messages', type=int, nargs='+') +parser.add_argument("op", choices=["add", "remove"]) +parser.add_argument("flag") +parser.add_argument("messages", type=int, nargs="+") options = parser.parse_args() client = zulip.init_from_options(options) print( client.update_message_flags( - {'op': options.op, 'flag': options.flag, 'messages': options.messages} + {"op": options.op, "flag": options.flag, "messages": options.messages} ) ) diff --git a/zulip/zulip/examples/upload-file b/zulip/zulip/examples/upload-file index 212803c..48fe8aa 100755 --- a/zulip/zulip/examples/upload-file +++ b/zulip/zulip/examples/upload-file @@ -8,7 +8,7 @@ import zulip class StringIO(_StringIO): - name = '' # https://github.com/python/typeshed/issues/598 + name = "" # https://github.com/python/typeshed/issues/598 usage = """upload-file [options] @@ -22,20 +22,20 @@ If no --file-path is specified, a placeholder text file will be used instead. """ parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage)) -parser.add_argument('--file-path', required=True) +parser.add_argument("--file-path", required=True) options = parser.parse_args() client = zulip.init_from_options(options) if options.file_path: - file = open(options.file_path, 'rb') # type: IO[Any] + file = open(options.file_path, "rb") # type: IO[Any] else: - file = StringIO('This is a test file.') - file.name = 'test.txt' + file = StringIO("This is a test file.") + file.name = "test.txt" response = client.upload_file(file) try: - print('File URI: {}'.format(response['uri'])) + print("File URI: {}".format(response["uri"])) except KeyError: - print('Error! API response was: {}'.format(response)) + print("Error! API response was: {}".format(response)) diff --git a/zulip/zulip/examples/welcome-message b/zulip/zulip/examples/welcome-message index 73f12a1..b6e8986 100755 --- a/zulip/zulip/examples/welcome-message +++ b/zulip/zulip/examples/welcome-message @@ -4,7 +4,7 @@ from typing import Any, Dict, List import zulip -welcome_text = 'Hello {}, Welcome to Zulip!\n \ +welcome_text = "Hello {}, Welcome to Zulip!\n \ * The first thing you should do is to install the development environment. \ We recommend following the vagrant setup as it is well documented and used \ by most of the contributors. If you face any trouble during installation \ @@ -21,7 +21,7 @@ of the main projects you can contribute to are Zulip \ a [bot](https://github.com/zulip/zulipbot) that you can contribute to!!\n \ * We host our source code on GitHub. If you are not familiar with Git or \ GitHub checkout [this](http://zulip.readthedocs.io/en/latest/git-guide.html) \ -guide. You don\'t have to learn everything but please go through it and learn \ +guide. You don't have to learn everything but please go through it and learn \ the basics. We are here to help you if you are having any trouble. Post your \ questions in #git help . \ * Once you have completed these steps you can start contributing. You \ @@ -33,55 +33,55 @@ but if you want a bite size issue for mobile or electron feel free post in #mobi or #electron .\n \ * Solving the first issue can be difficult. The key is to not give up. If you spend \ enough time on the issue you should be able to solve it no matter what.\n \ -* Use `grep` command when you can\'t figure out what files to change :) For example \ +* Use `grep` command when you can't figure out what files to change :) For example \ if you want know what files to modify in order to change Invite more users to Add \ -more users which you can see below the user status list, grep for "Invite more \ -users" in terminal.\n \ -* If you are stuck with something and can\'t figure out what to do you can ask \ +more users which you can see below the user status list, grep for \"Invite more \ +users\" in terminal.\n \ +* If you are stuck with something and can't figure out what to do you can ask \ for help in #development help . But make sure that you tried your best to figure \ out the issue by yourself\n \ -* If you are here for #Outreachy 2017-2018 or #GSoC don\'t worry much about \ +* If you are here for #Outreachy 2017-2018 or #GSoC don't worry much about \ whether you will get selected or not. You will learn a lot contributing to \ Zulip in course of next few months and if you do a good job at that you \ will get selected too :)\n \ -* Most important of all welcome to the Zulip family :octopus:' +* Most important of all welcome to the Zulip family :octopus:" # These streams will cause the message to be sent -streams_to_watch = ['new members'] +streams_to_watch = ["new members"] # These streams will cause anyone who sends a message there to be removed from the watchlist -streams_to_cancel = ['development help'] +streams_to_cancel = ["development help"] def get_watchlist() -> List[Any]: storage = client.get_storage() - return list(storage['storage'].values()) + return list(storage["storage"].values()) def set_watchlist(watchlist: List[str]) -> None: - client.update_storage({'storage': dict(enumerate(watchlist))}) + client.update_storage({"storage": dict(enumerate(watchlist))}) def handle_event(event: Dict[str, Any]) -> None: try: - if event['type'] == 'realm_user' and event['op'] == 'add': + if event["type"] == "realm_user" and event["op"] == "add": watchlist = get_watchlist() - watchlist.append(event['person']['email']) + watchlist.append(event["person"]["email"]) set_watchlist(watchlist) return - if event['type'] == 'message': - stream = event['message']['display_recipient'] + if event["type"] == "message": + stream = event["message"]["display_recipient"] if stream not in streams_to_watch and stream not in streams_to_cancel: return watchlist = get_watchlist() - if event['message']['sender_email'] in watchlist: - watchlist.remove(event['message']['sender_email']) + if event["message"]["sender_email"] in watchlist: + watchlist.remove(event["message"]["sender_email"]) if stream not in streams_to_cancel: client.send_message( { - 'type': 'private', - 'to': event['message']['sender_email'], - 'content': welcome_text.format(event['message']['sender_short_name']), + "type": "private", + "to": event["message"]["sender_email"], + "content": welcome_text.format(event["message"]["sender_short_name"]), } ) set_watchlist(watchlist) @@ -92,7 +92,7 @@ def handle_event(event: Dict[str, Any]) -> None: def start_event_handler() -> None: print("Starting event handler...") - client.call_on_each_event(handle_event, event_types=['realm_user', 'message']) + client.call_on_each_event(handle_event, event_types=["realm_user", "message"]) client = zulip.Client() diff --git a/zulip/zulip/send.py b/zulip/zulip/send.py index ca0f281..fb846f7 100755 --- a/zulip/zulip/send.py +++ b/zulip/zulip/send.py @@ -10,25 +10,25 @@ import zulip logging.basicConfig() -log = logging.getLogger('zulip-send') +log = logging.getLogger("zulip-send") def do_send_message(client: zulip.Client, message_data: Dict[str, Any]) -> bool: - '''Sends a message and optionally prints status about the same.''' + """Sends a message and optionally prints status about the same.""" - if message_data['type'] == 'stream': + if message_data["type"] == "stream": log.info( 'Sending message to stream "%s", subject "%s"... ' - % (message_data['to'], message_data['subject']) + % (message_data["to"], message_data["subject"]) ) else: - log.info('Sending message to %s... ' % (message_data['to'],)) + log.info("Sending message to %s... " % (message_data["to"],)) response = client.send_message(message_data) - if response['result'] == 'success': - log.info('Message sent.') + if response["result"] == "success": + log.info("Message sent.") return True else: - log.error(response['msg']) + log.error(response["msg"]) return False @@ -46,27 +46,27 @@ def main() -> int: parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage)) parser.add_argument( - 'recipients', nargs='*', help='email addresses of the recipients of the message' + "recipients", nargs="*", help="email addresses of the recipients of the message" ) parser.add_argument( - '-m', '--message', help='Specifies the message to send, prevents interactive prompting.' + "-m", "--message", help="Specifies the message to send, prevents interactive prompting." ) - group = parser.add_argument_group('Stream parameters') + group = parser.add_argument_group("Stream parameters") group.add_argument( - '-s', - '--stream', - dest='stream', - action='store', - help='Allows the user to specify a stream for the message.', + "-s", + "--stream", + dest="stream", + action="store", + help="Allows the user to specify a stream for the message.", ) group.add_argument( - '-S', - '--subject', - dest='subject', - action='store', - help='Allows the user to specify a subject for the message.', + "-S", + "--subject", + dest="subject", + action="store", + help="Allows the user to specify a subject for the message.", ) options = parser.parse_args() @@ -75,11 +75,11 @@ def main() -> int: logging.getLogger().setLevel(logging.INFO) # Sanity check user data if len(options.recipients) != 0 and (options.stream or options.subject): - parser.error('You cannot specify both a username and a stream/subject.') + parser.error("You cannot specify both a username and a stream/subject.") if len(options.recipients) == 0 and (bool(options.stream) != bool(options.subject)): - parser.error('Stream messages must have a subject') + parser.error("Stream messages must have a subject") if len(options.recipients) == 0 and not (options.stream and options.subject): - parser.error('You must specify a stream/subject or at least one recipient.') + parser.error("You must specify a stream/subject or at least one recipient.") client = zulip.init_from_options(options) @@ -88,16 +88,16 @@ def main() -> int: if options.stream: message_data = { - 'type': 'stream', - 'content': options.message, - 'subject': options.subject, - 'to': options.stream, + "type": "stream", + "content": options.message, + "subject": options.subject, + "to": options.stream, } else: message_data = { - 'type': 'private', - 'content': options.message, - 'to': options.recipients, + "type": "private", + "content": options.message, + "to": options.recipients, } if not do_send_message(client, message_data): @@ -105,5 +105,5 @@ def main() -> int: return 0 -if __name__ == '__main__': +if __name__ == "__main__": sys.exit(main()) diff --git a/zulip_bots/setup.py b/zulip_bots/setup.py index 6f808bf..ddeafe6 100644 --- a/zulip_bots/setup.py +++ b/zulip_bots/setup.py @@ -9,50 +9,50 @@ IS_PYPA_PACKAGE = False package_data = { - '': ['doc.md', '*.conf', 'assets/*'], - 'zulip_bots': ['py.typed'], + "": ["doc.md", "*.conf", "assets/*"], + "zulip_bots": ["py.typed"], } # IS_PYPA_PACKAGE is set to True by tools/release-packages # before making a PyPA release. if not IS_PYPA_PACKAGE: - package_data[''].append('fixtures/*.json') - package_data[''].append('logo.*') + package_data[""].append("fixtures/*.json") + package_data[""].append("logo.*") with open("README.md") as fh: long_description = fh.read() # We should be installable with either setuptools or distutils. package_info = dict( - name='zulip_bots', + name="zulip_bots", version=ZULIP_BOTS_VERSION, - description='Zulip\'s Bot framework', + description="Zulip's Bot framework", long_description=long_description, long_description_content_type="text/markdown", - author='Zulip Open Source Project', - author_email='zulip-devel@googlegroups.com', + 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 :: Apache Software License', - 'Topic :: Communications :: Chat', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', + "Development Status :: 4 - Beta", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Topic :: Communications :: Chat", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", ], - python_requires='>=3.6', - url='https://www.zulip.org/', + python_requires=">=3.6", + url="https://www.zulip.org/", project_urls={ "Source": "https://github.com/zulip/python-zulip-api/", "Documentation": "https://zulip.com/api", }, entry_points={ - 'console_scripts': [ - 'zulip-run-bot=zulip_bots.run:main', - 'zulip-terminal=zulip_bots.terminal:main', + "console_scripts": [ + "zulip-run-bot=zulip_bots.run:main", + "zulip-terminal=zulip_bots.terminal:main", ], }, include_package_data=True, @@ -60,12 +60,12 @@ package_info = dict( setuptools_info = dict( install_requires=[ - 'pip', - 'zulip', - 'html2text', - 'lxml', - 'BeautifulSoup4', - 'typing_extensions', + "pip", + "zulip", + "html2text", + "lxml", + "BeautifulSoup4", + "typing_extensions", ], ) @@ -73,8 +73,8 @@ try: from setuptools import find_packages, setup package_info.update(setuptools_info) - package_info['packages'] = find_packages() - package_info['package_data'] = package_data + package_info["packages"] = find_packages() + package_info["package_data"] = package_data except ImportError: from distutils.core import setup @@ -97,17 +97,17 @@ except ImportError: print("{name} is not installed.".format(name=module_name), file=sys.stderr) sys.exit(1) - check_dependency_manually('zulip') - check_dependency_manually('mock', '2.0.0') - check_dependency_manually('html2text') - check_dependency_manually('PyDictionary') + check_dependency_manually("zulip") + check_dependency_manually("mock", "2.0.0") + check_dependency_manually("html2text") + check_dependency_manually("PyDictionary") # Include all submodules under bots/ - package_list = ['zulip_bots'] - dirs = os.listdir('zulip_bots/bots/') + package_list = ["zulip_bots"] + dirs = os.listdir("zulip_bots/bots/") for dir_name in dirs: - if os.path.isdir(os.path.join('zulip_bots/bots/', dir_name)): - package_list.append('zulip_bots.bots.' + dir_name) - package_info['packages'] = package_list + if os.path.isdir(os.path.join("zulip_bots/bots/", dir_name)): + package_list.append("zulip_bots.bots." + dir_name) + package_info["packages"] = package_list setup(**package_info) diff --git a/zulip_bots/zulip_bots/bots/baremetrics/baremetrics.py b/zulip_bots/zulip_bots/bots/baremetrics/baremetrics.py index 062b5ca..0bf6a41 100644 --- a/zulip_bots/zulip_bots/bots/baremetrics/baremetrics.py +++ b/zulip_bots/zulip_bots/bots/baremetrics/baremetrics.py @@ -9,31 +9,31 @@ from zulip_bots.lib import BotHandler class BaremetricsHandler: def initialize(self, bot_handler: BotHandler) -> None: - self.config_info = bot_handler.get_config_info('baremetrics') - self.api_key = self.config_info['api_key'] + self.config_info = bot_handler.get_config_info("baremetrics") + self.api_key = self.config_info["api_key"] - self.auth_header = {'Authorization': 'Bearer ' + self.api_key} + self.auth_header = {"Authorization": "Bearer " + self.api_key} self.commands = [ - 'help', - 'list-commands', - 'account-info', - 'list-sources', - 'list-plans ', - 'list-customers ', - 'list-subscriptions ', - 'create-plan ', + "help", + "list-commands", + "account-info", + "list-sources", + "list-plans ", + "list-customers ", + "list-subscriptions ", + "create-plan ", ] self.descriptions = [ - 'Display bot info', - 'Display the list of available commands', - 'Display the account info', - 'List the sources', - 'List the plans for the source', - 'List the customers in the source', - 'List the subscriptions in the source', - 'Create a plan in the given source', + "Display bot info", + "Display the list of available commands", + "Display the account info", + "List the sources", + "List the plans for the source", + "List the customers in the source", + "List the subscriptions in the source", + "Create a plan in the given source", ] self.check_api_key(bot_handler) @@ -44,36 +44,36 @@ class BaremetricsHandler: test_query_data = test_query_response.json() try: - if test_query_data['error'] == "Unauthorized. Token not found (001)": - bot_handler.quit('API Key not valid. Please see doc.md to find out how to get it.') + if test_query_data["error"] == "Unauthorized. Token not found (001)": + bot_handler.quit("API Key not valid. Please see doc.md to find out how to get it.") except KeyError: pass def usage(self) -> str: - return ''' + return """ This bot gives updates about customer behavior, financial performance, and analytics for an organization using the Baremetrics Api.\n Enter `list-commands` to show the list of available commands. Version 1.0 - ''' + """ def handle_message(self, message: Dict[str, Any], bot_handler: BotHandler) -> None: - content = message['content'].strip().split() + content = message["content"].strip().split() if content == []: - bot_handler.send_reply(message, 'No Command Specified') + bot_handler.send_reply(message, "No Command Specified") return content[0] = content[0].lower() - if content == ['help']: + if content == ["help"]: bot_handler.send_reply(message, self.usage()) return - if content == ['list-commands']: - response = '**Available Commands:** \n' + if content == ["list-commands"]: + response = "**Available Commands:** \n" for command, description in zip(self.commands, self.descriptions): - response += ' - {} : {}\n'.format(command, description) + response += " - {} : {}\n".format(command, description) bot_handler.send_reply(message, response) return @@ -85,177 +85,177 @@ class BaremetricsHandler: try: instruction = commands[0] - if instruction == 'account-info': + if instruction == "account-info": return self.get_account_info() - if instruction == 'list-sources': + if instruction == "list-sources": return self.get_sources() try: - if instruction == 'list-plans': + if instruction == "list-plans": return self.get_plans(commands[1]) - if instruction == 'list-customers': + if instruction == "list-customers": return self.get_customers(commands[1]) - if instruction == 'list-subscriptions': + if instruction == "list-subscriptions": return self.get_subscriptions(commands[1]) - if instruction == 'create-plan': + if instruction == "create-plan": if len(commands) == 8: return self.create_plan(commands[1:]) else: - return 'Invalid number of arguments.' + return "Invalid number of arguments." except IndexError: - return 'Missing Params.' + return "Missing Params." except KeyError: - return 'Invalid Response From API.' + return "Invalid Response From API." - return 'Invalid Command.' + return "Invalid Command." def get_account_info(self) -> str: url = "https://api.baremetrics.com/v1/account" account_response = requests.get(url, headers=self.auth_header) account_data = account_response.json() - account_data = account_data['account'] + account_data = account_data["account"] template = [ - '**Your account information:**', - 'Id: {id}', - 'Company: {company}', - 'Default Currency: {currency}', + "**Your account information:**", + "Id: {id}", + "Company: {company}", + "Default Currency: {currency}", ] return "\n".join(template).format( - currency=account_data['default_currency']['name'], **account_data + currency=account_data["default_currency"]["name"], **account_data ) def get_sources(self) -> str: - url = 'https://api.baremetrics.com/v1/sources' + url = "https://api.baremetrics.com/v1/sources" sources_response = requests.get(url, headers=self.auth_header) sources_data = sources_response.json() - sources_data = sources_data['sources'] + sources_data = sources_data["sources"] - response = '**Listing sources:** \n' + response = "**Listing sources:** \n" for index, source in enumerate(sources_data): response += ( - '{_count}.ID: {id}\n' 'Provider: {provider}\n' 'Provider ID: {provider_id}\n\n' + "{_count}.ID: {id}\n" "Provider: {provider}\n" "Provider ID: {provider_id}\n\n" ).format(_count=index + 1, **source) return response def get_plans(self, source_id: str) -> str: - url = 'https://api.baremetrics.com/v1/{}/plans'.format(source_id) + url = "https://api.baremetrics.com/v1/{}/plans".format(source_id) plans_response = requests.get(url, headers=self.auth_header) plans_data = plans_response.json() - plans_data = plans_data['plans'] + plans_data = plans_data["plans"] - template = '\n'.join( + template = "\n".join( [ - '{_count}.Name: {name}', - 'Active: {active}', - 'Interval: {interval}', - 'Interval Count: {interval_count}', - 'Amounts:', + "{_count}.Name: {name}", + "Active: {active}", + "Interval: {interval}", + "Interval Count: {interval_count}", + "Amounts:", ] ) - response = ['**Listing plans:**'] + response = ["**Listing plans:**"] for index, plan in enumerate(plans_data): response += ( [template.format(_count=index + 1, **plan)] - + [' - {amount} {currency}'.format(**amount) for amount in plan['amounts']] - + [''] + + [" - {amount} {currency}".format(**amount) for amount in plan["amounts"]] + + [""] ) - return '\n'.join(response) + return "\n".join(response) def get_customers(self, source_id: str) -> str: - url = 'https://api.baremetrics.com/v1/{}/customers'.format(source_id) + url = "https://api.baremetrics.com/v1/{}/customers".format(source_id) customers_response = requests.get(url, headers=self.auth_header) customers_data = customers_response.json() - customers_data = customers_data['customers'] + customers_data = customers_data["customers"] # FIXME BUG here? mismatch of name and display name? - template = '\n'.join( + template = "\n".join( [ - '{_count}.Name: {display_name}', - 'Display Name: {name}', - 'OID: {oid}', - 'Active: {is_active}', - 'Email: {email}', - 'Notes: {notes}', - 'Current Plans:', + "{_count}.Name: {display_name}", + "Display Name: {name}", + "OID: {oid}", + "Active: {is_active}", + "Email: {email}", + "Notes: {notes}", + "Current Plans:", ] ) - response = ['**Listing customers:**'] + response = ["**Listing customers:**"] for index, customer in enumerate(customers_data): response += ( [template.format(_count=index + 1, **customer)] - + [' - {name}'.format(**plan) for plan in customer['current_plans']] - + [''] + + [" - {name}".format(**plan) for plan in customer["current_plans"]] + + [""] ) - return '\n'.join(response) + return "\n".join(response) def get_subscriptions(self, source_id: str) -> str: - url = 'https://api.baremetrics.com/v1/{}/subscriptions'.format(source_id) + url = "https://api.baremetrics.com/v1/{}/subscriptions".format(source_id) subscriptions_response = requests.get(url, headers=self.auth_header) subscriptions_data = subscriptions_response.json() - subscriptions_data = subscriptions_data['subscriptions'] + subscriptions_data = subscriptions_data["subscriptions"] - template = '\n'.join( + template = "\n".join( [ - '{_count}.Customer Name: {name}', - 'Customer Display Name: {display_name}', - 'Customer OID: {oid}', - 'Customer Email: {email}', - 'Active: {_active}', - 'Plan Name: {_plan_name}', - 'Plan Amounts:', + "{_count}.Customer Name: {name}", + "Customer Display Name: {display_name}", + "Customer OID: {oid}", + "Customer Email: {email}", + "Active: {_active}", + "Plan Name: {_plan_name}", + "Plan Amounts:", ] ) - response = ['**Listing subscriptions:**'] + response = ["**Listing subscriptions:**"] for index, subscription in enumerate(subscriptions_data): response += ( [ template.format( _count=index + 1, - _active=subscription['active'], - _plan_name=subscription['plan']['name'], - **subscription['customer'], + _active=subscription["active"], + _plan_name=subscription["plan"]["name"], + **subscription["customer"], ) ] + [ - ' - {amount} {symbol}'.format(**amount) - for amount in subscription['plan']['amounts'] + " - {amount} {symbol}".format(**amount) + for amount in subscription["plan"]["amounts"] ] - + [''] + + [""] ) - return '\n'.join(response) + return "\n".join(response) def create_plan(self, parameters: List[str]) -> str: data_header = { - 'oid': parameters[1], - 'name': parameters[2], - 'currency': parameters[3], - 'amount': int(parameters[4]), - 'interval': parameters[5], - 'interval_count': int(parameters[6]), + "oid": parameters[1], + "name": parameters[2], + "currency": parameters[3], + "amount": int(parameters[4]), + "interval": parameters[5], + "interval_count": int(parameters[6]), } # type: Any - url = 'https://api.baremetrics.com/v1/{}/plans'.format(parameters[0]) + url = "https://api.baremetrics.com/v1/{}/plans".format(parameters[0]) create_plan_response = requests.post(url, data=data_header, headers=self.auth_header) - if 'error' not in create_plan_response.json(): - return 'Plan Created.' + if "error" not in create_plan_response.json(): + return "Plan Created." else: - return 'Invalid Arguments Error.' + return "Invalid Arguments Error." handler_class = BaremetricsHandler diff --git a/zulip_bots/zulip_bots/bots/baremetrics/test_baremetrics.py b/zulip_bots/zulip_bots/bots/baremetrics/test_baremetrics.py index 08c16bd..6aeb6e2 100644 --- a/zulip_bots/zulip_bots/bots/baremetrics/test_baremetrics.py +++ b/zulip_bots/zulip_bots/bots/baremetrics/test_baremetrics.py @@ -8,121 +8,121 @@ class TestBaremetricsBot(BotTestCase, DefaultTests): bot_name = "baremetrics" def test_bot_responds_to_empty_message(self) -> None: - with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'): - self.verify_reply('', 'No Command Specified') + with self.mock_config_info({"api_key": "TEST"}), patch("requests.get"): + self.verify_reply("", "No Command Specified") def test_help_query(self) -> None: - with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'): + with self.mock_config_info({"api_key": "TEST"}), patch("requests.get"): self.verify_reply( - 'help', - ''' + "help", + """ This bot gives updates about customer behavior, financial performance, and analytics for an organization using the Baremetrics Api.\n Enter `list-commands` to show the list of available commands. Version 1.0 - ''', + """, ) def test_list_commands_command(self) -> None: - with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'): + with self.mock_config_info({"api_key": "TEST"}), patch("requests.get"): self.verify_reply( - 'list-commands', - '**Available Commands:** \n' - ' - help : Display bot info\n' - ' - list-commands : Display the list of available commands\n' - ' - account-info : Display the account info\n' - ' - list-sources : List the sources\n' - ' - list-plans : List the plans for the source\n' - ' - list-customers : List the customers in the source\n' - ' - list-subscriptions : List the subscriptions in the ' - 'source\n' - ' - create-plan ' - ' : Create a plan in the given source\n', + "list-commands", + "**Available Commands:** \n" + " - help : Display bot info\n" + " - list-commands : Display the list of available commands\n" + " - account-info : Display the account info\n" + " - list-sources : List the sources\n" + " - list-plans : List the plans for the source\n" + " - list-customers : List the customers in the source\n" + " - list-subscriptions : List the subscriptions in the " + "source\n" + " - create-plan " + " : Create a plan in the given source\n", ) def test_account_info_command(self) -> None: - with self.mock_config_info({'api_key': 'TEST'}): - with self.mock_http_conversation('account_info'): + with self.mock_config_info({"api_key": "TEST"}): + with self.mock_http_conversation("account_info"): self.verify_reply( - 'account-info', - '**Your account information:**\nId: 376418\nCompany: NA\nDefault ' - 'Currency: United States Dollar', + "account-info", + "**Your account information:**\nId: 376418\nCompany: NA\nDefault " + "Currency: United States Dollar", ) def test_list_sources_command(self) -> None: - with self.mock_config_info({'api_key': 'TEST'}): - with self.mock_http_conversation('list_sources'): + with self.mock_config_info({"api_key": "TEST"}): + with self.mock_http_conversation("list_sources"): self.verify_reply( - 'list-sources', - '**Listing sources:** \n1.ID: 5f7QC5NC0Ywgcu\nProvider: ' - 'baremetrics\nProvider ID: None\n\n', + "list-sources", + "**Listing sources:** \n1.ID: 5f7QC5NC0Ywgcu\nProvider: " + "baremetrics\nProvider ID: None\n\n", ) def test_list_plans_command(self) -> None: r = ( - '**Listing plans:**\n1.Name: Plan 1\nActive: True\nInterval: year\nInterval Count: 1\nAmounts:\n' - ' - 450000 USD\n\n2.Name: Plan 2\nActive: True\nInterval: year\nInterval Count: 1\nAmounts:\n' - ' - 450000 USD\n' + "**Listing plans:**\n1.Name: Plan 1\nActive: True\nInterval: year\nInterval Count: 1\nAmounts:\n" + " - 450000 USD\n\n2.Name: Plan 2\nActive: True\nInterval: year\nInterval Count: 1\nAmounts:\n" + " - 450000 USD\n" ) - with self.mock_config_info({'api_key': 'TEST'}): - with self.mock_http_conversation('list_plans'): - self.verify_reply('list-plans TEST', r) + with self.mock_config_info({"api_key": "TEST"}): + with self.mock_http_conversation("list_plans"): + self.verify_reply("list-plans TEST", r) def test_list_customers_command(self) -> None: r = ( - '**Listing customers:**\n1.Name: Customer 1\nDisplay Name: Customer 1\nOID: customer_1\nActive: True\n' - 'Email: customer_1@baremetrics.com\nNotes: Here are some notes\nCurrent Plans:\n - Plan 1\n' + "**Listing customers:**\n1.Name: Customer 1\nDisplay Name: Customer 1\nOID: customer_1\nActive: True\n" + "Email: customer_1@baremetrics.com\nNotes: Here are some notes\nCurrent Plans:\n - Plan 1\n" ) - with self.mock_config_info({'api_key': 'TEST'}): - with self.mock_http_conversation('list_customers'): - self.verify_reply('list-customers TEST', r) + with self.mock_config_info({"api_key": "TEST"}): + with self.mock_http_conversation("list_customers"): + self.verify_reply("list-customers TEST", r) def test_list_subscriptions_command(self) -> None: r = ( - '**Listing subscriptions:**\n1.Customer Name: Customer 1\nCustomer Display Name: Customer 1\n' - 'Customer OID: customer_1\nCustomer Email: customer_1@baremetrics.com\nActive: True\n' - 'Plan Name: Plan 1\nPlan Amounts:\n - 1000 $\n' + "**Listing subscriptions:**\n1.Customer Name: Customer 1\nCustomer Display Name: Customer 1\n" + "Customer OID: customer_1\nCustomer Email: customer_1@baremetrics.com\nActive: True\n" + "Plan Name: Plan 1\nPlan Amounts:\n - 1000 $\n" ) - with self.mock_config_info({'api_key': 'TEST'}): - with self.mock_http_conversation('list_subscriptions'): - self.verify_reply('list-subscriptions TEST', r) + with self.mock_config_info({"api_key": "TEST"}): + with self.mock_http_conversation("list_subscriptions"): + self.verify_reply("list-subscriptions TEST", r) def test_exception_when_api_key_is_invalid(self) -> None: bot_test_instance = BaremetricsHandler() - with self.mock_config_info({'api_key': 'TEST'}): - with self.mock_http_conversation('invalid_api_key'): + with self.mock_config_info({"api_key": "TEST"}): + with self.mock_http_conversation("invalid_api_key"): with self.assertRaises(StubBotHandler.BotQuitException): bot_test_instance.initialize(StubBotHandler()) def test_invalid_command(self) -> None: - with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'): - self.verify_reply('abcd', 'Invalid Command.') + with self.mock_config_info({"api_key": "TEST"}), patch("requests.get"): + self.verify_reply("abcd", "Invalid Command.") def test_missing_params(self) -> None: - with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'): - self.verify_reply('list-plans', 'Missing Params.') + with self.mock_config_info({"api_key": "TEST"}), patch("requests.get"): + self.verify_reply("list-plans", "Missing Params.") def test_key_error(self) -> None: - with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'): - with self.mock_http_conversation('test_key_error'): - self.verify_reply('list-plans TEST', 'Invalid Response From API.') + with self.mock_config_info({"api_key": "TEST"}), patch("requests.get"): + with self.mock_http_conversation("test_key_error"): + self.verify_reply("list-plans TEST", "Invalid Response From API.") def test_create_plan_command(self) -> None: - with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'): - with self.mock_http_conversation('create_plan'): - self.verify_reply('create-plan TEST 1 TEST USD 123 TEST 123', 'Plan Created.') + with self.mock_config_info({"api_key": "TEST"}), patch("requests.get"): + with self.mock_http_conversation("create_plan"): + self.verify_reply("create-plan TEST 1 TEST USD 123 TEST 123", "Plan Created.") def test_create_plan_error_command(self) -> None: - with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'): - with self.mock_http_conversation('create_plan_error'): + with self.mock_config_info({"api_key": "TEST"}), patch("requests.get"): + with self.mock_http_conversation("create_plan_error"): self.verify_reply( - 'create-plan TEST 1 TEST USD 123 TEST 123', 'Invalid Arguments Error.' + "create-plan TEST 1 TEST USD 123 TEST 123", "Invalid Arguments Error." ) def test_create_plan_argnum_error_command(self) -> None: - with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'): - self.verify_reply('create-plan alpha beta', 'Invalid number of arguments.') + with self.mock_config_info({"api_key": "TEST"}), patch("requests.get"): + self.verify_reply("create-plan alpha beta", "Invalid number of arguments.") diff --git a/zulip_bots/zulip_bots/bots/beeminder/beeminder.py b/zulip_bots/zulip_bots/bots/beeminder/beeminder.py index bddadd3..468fb7f 100644 --- a/zulip_bots/zulip_bots/bots/beeminder/beeminder.py +++ b/zulip_bots/zulip_bots/bots/beeminder/beeminder.py @@ -6,7 +6,7 @@ from requests.exceptions import ConnectionError from zulip_bots.lib import BotHandler -help_message = ''' +help_message = """ You can add datapoints towards your beeminder goals \ following the syntax shown below :smile:.\n \ \n**@mention-botname daystamp, value, comment**\ @@ -14,22 +14,22 @@ following the syntax shown below :smile:.\n \ [**NOTE:** Optional field, default is *current daystamp*],\ \n* `value`**:** Enter a value [**NOTE:** Required field, can be any number],\ \n* `comment`**:** Add a comment [**NOTE:** Optional field, default is *None*]\ -''' +""" def get_beeminder_response(message_content: str, config_info: Dict[str, str]) -> str: - username = config_info['username'] - goalname = config_info['goalname'] - auth_token = config_info['auth_token'] + username = config_info["username"] + goalname = config_info["goalname"] + auth_token = config_info["auth_token"] message_content = message_content.strip() - if message_content == '' or message_content == 'help': + if message_content == "" or message_content == "help": return help_message url = "https://www.beeminder.com/api/v1/users/{}/goals/{}/datapoints.json".format( username, goalname ) - message_pieces = message_content.split(',') + message_pieces = message_content.split(",") for i in range(len(message_pieces)): message_pieces[i] = message_pieces[i].strip() @@ -81,21 +81,21 @@ right now.\nPlease try again later" class BeeminderHandler: - ''' + """ This plugin allows users to easily add datapoints towards their beeminder goals via zulip - ''' + """ def initialize(self, bot_handler: BotHandler) -> None: - self.config_info = bot_handler.get_config_info('beeminder') + self.config_info = bot_handler.get_config_info("beeminder") # Check for valid auth_token - auth_token = self.config_info['auth_token'] + auth_token = self.config_info["auth_token"] try: r = requests.get( - "https://www.beeminder.com/api/v1/users/me.json", params={'auth_token': auth_token} + "https://www.beeminder.com/api/v1/users/me.json", params={"auth_token": auth_token} ) if r.status_code == 401: - bot_handler.quit('Invalid key!') + bot_handler.quit("Invalid key!") except ConnectionError as e: logging.exception(str(e)) @@ -103,7 +103,7 @@ class BeeminderHandler: return "This plugin allows users to add datapoints towards their Beeminder goals" def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: - response = get_beeminder_response(message['content'], self.config_info) + response = get_beeminder_response(message["content"], self.config_info) bot_handler.send_reply(message, response) diff --git a/zulip_bots/zulip_bots/bots/beeminder/test_beeminder.py b/zulip_bots/zulip_bots/bots/beeminder/test_beeminder.py index a8de2c3..516ed1d 100644 --- a/zulip_bots/zulip_bots/bots/beeminder/test_beeminder.py +++ b/zulip_bots/zulip_bots/bots/beeminder/test_beeminder.py @@ -9,7 +9,7 @@ class TestBeeminderBot(BotTestCase, DefaultTests): bot_name = "beeminder" normal_config = {"auth_token": "XXXXXX", "username": "aaron", "goalname": "goal"} - help_message = ''' + help_message = """ You can add datapoints towards your beeminder goals \ following the syntax shown below :smile:.\n \ \n**@mention-botname daystamp, value, comment**\ @@ -17,44 +17,44 @@ following the syntax shown below :smile:.\n \ [**NOTE:** Optional field, default is *current daystamp*],\ \n* `value`**:** Enter a value [**NOTE:** Required field, can be any number],\ \n* `comment`**:** Add a comment [**NOTE:** Optional field, default is *None*]\ -''' +""" def test_bot_responds_to_empty_message(self) -> None: with self.mock_config_info(self.normal_config), self.mock_http_conversation( - 'test_valid_auth_token' + "test_valid_auth_token" ): - self.verify_reply('', self.help_message) + self.verify_reply("", self.help_message) def test_help_message(self) -> None: with self.mock_config_info(self.normal_config), self.mock_http_conversation( - 'test_valid_auth_token' + "test_valid_auth_token" ): - self.verify_reply('help', self.help_message) + self.verify_reply("help", self.help_message) def test_message_with_daystamp_and_value(self) -> None: - bot_response = '[Datapoint](https://www.beeminder.com/aaron/goal) created.' + bot_response = "[Datapoint](https://www.beeminder.com/aaron/goal) created." with self.mock_config_info(self.normal_config), self.mock_http_conversation( - 'test_valid_auth_token' - ), self.mock_http_conversation('test_message_with_daystamp_and_value'): - self.verify_reply('20180602, 2', bot_response) + "test_valid_auth_token" + ), self.mock_http_conversation("test_message_with_daystamp_and_value"): + self.verify_reply("20180602, 2", bot_response) def test_message_with_value_and_comment(self) -> None: - bot_response = '[Datapoint](https://www.beeminder.com/aaron/goal) created.' + bot_response = "[Datapoint](https://www.beeminder.com/aaron/goal) created." with self.mock_config_info(self.normal_config), self.mock_http_conversation( - 'test_valid_auth_token' - ), self.mock_http_conversation('test_message_with_value_and_comment'): - self.verify_reply('2, hi there!', bot_response) + "test_valid_auth_token" + ), self.mock_http_conversation("test_message_with_value_and_comment"): + self.verify_reply("2, hi there!", bot_response) def test_message_with_daystamp_and_value_and_comment(self) -> None: - bot_response = '[Datapoint](https://www.beeminder.com/aaron/goal) created.' + bot_response = "[Datapoint](https://www.beeminder.com/aaron/goal) created." with self.mock_config_info(self.normal_config), self.mock_http_conversation( - 'test_valid_auth_token' - ), self.mock_http_conversation('test_message_with_daystamp_and_value_and_comment'): - self.verify_reply('20180602, 2, hi there!', bot_response) + "test_valid_auth_token" + ), self.mock_http_conversation("test_message_with_daystamp_and_value_and_comment"): + self.verify_reply("20180602, 2, hi there!", bot_response) def test_syntax_error(self) -> None: with self.mock_config_info(self.normal_config), self.mock_http_conversation( - 'test_valid_auth_token' + "test_valid_auth_token" ): bot_response = "Make sure you follow the syntax.\n You can take a look \ at syntax by: @mention-botname help" @@ -62,12 +62,12 @@ at syntax by: @mention-botname help" def test_connection_error_when_handle_message(self) -> None: with self.mock_config_info(self.normal_config), self.mock_http_conversation( - 'test_valid_auth_token' - ), patch('requests.post', side_effect=ConnectionError()), patch('logging.exception'): + "test_valid_auth_token" + ), patch("requests.post", side_effect=ConnectionError()), patch("logging.exception"): self.verify_reply( - '?$!', - 'Uh-Oh, couldn\'t process the request \ -right now.\nPlease try again later', + "?$!", + "Uh-Oh, couldn't process the request \ +right now.\nPlease try again later", ) def test_invalid_when_handle_message(self) -> None: @@ -75,20 +75,20 @@ right now.\nPlease try again later', StubBotHandler() with self.mock_config_info( - {'auth_token': 'someInvalidKey', 'username': 'aaron', 'goalname': 'goal'} - ), patch('requests.get', side_effect=ConnectionError()), self.mock_http_conversation( - 'test_invalid_when_handle_message' + {"auth_token": "someInvalidKey", "username": "aaron", "goalname": "goal"} + ), patch("requests.get", side_effect=ConnectionError()), self.mock_http_conversation( + "test_invalid_when_handle_message" ), patch( - 'logging.exception' + "logging.exception" ): - self.verify_reply('5', 'Error. Check your key!') + self.verify_reply("5", "Error. Check your key!") def test_error(self) -> None: - bot_request = 'notNumber' + bot_request = "notNumber" bot_response = "Error occured : 422" with self.mock_config_info(self.normal_config), self.mock_http_conversation( - 'test_valid_auth_token' - ), self.mock_http_conversation('test_error'): + "test_valid_auth_token" + ), self.mock_http_conversation("test_error"): self.verify_reply(bot_request, bot_response) def test_invalid_when_initialize(self) -> None: @@ -96,8 +96,8 @@ right now.\nPlease try again later', bot_handler = StubBotHandler() with self.mock_config_info( - {'auth_token': 'someInvalidKey', 'username': 'aaron', 'goalname': 'goal'} - ), self.mock_http_conversation('test_invalid_when_initialize'), self.assertRaises( + {"auth_token": "someInvalidKey", "username": "aaron", "goalname": "goal"} + ), self.mock_http_conversation("test_invalid_when_initialize"), self.assertRaises( bot_handler.BotQuitException ): bot.initialize(bot_handler) @@ -107,7 +107,7 @@ right now.\nPlease try again later', bot_handler = StubBotHandler() with self.mock_config_info(self.normal_config), patch( - 'requests.get', side_effect=ConnectionError() - ), patch('logging.exception') as mock_logging: + "requests.get", side_effect=ConnectionError() + ), patch("logging.exception") as mock_logging: bot.initialize(bot_handler) self.assertTrue(mock_logging.called) diff --git a/zulip_bots/zulip_bots/bots/chessbot/chessbot.py b/zulip_bots/zulip_bots/bots/chessbot/chessbot.py index 938ba75..d43e6af 100644 --- a/zulip_bots/zulip_bots/bots/chessbot/chessbot.py +++ b/zulip_bots/zulip_bots/bots/chessbot/chessbot.py @@ -7,38 +7,38 @@ import chess.uci from zulip_bots.lib import BotHandler -START_REGEX = re.compile('start with other user$') -START_COMPUTER_REGEX = re.compile('start as (?Pwhite|black) with computer') -MOVE_REGEX = re.compile('do (?P.+)$') -RESIGN_REGEX = re.compile('resign$') +START_REGEX = re.compile("start with other user$") +START_COMPUTER_REGEX = re.compile("start as (?Pwhite|black) with computer") +MOVE_REGEX = re.compile("do (?P.+)$") +RESIGN_REGEX = re.compile("resign$") class ChessHandler: def usage(self) -> str: return ( - 'Chess Bot is a bot that allows you to play chess against either ' - 'another user or the computer. Use `start with other user` or ' - '`start as with computer` to start a game.\n\n' - 'In order to play against a computer, `chess.conf` must be set ' - 'with the key `stockfish_location` set to the location of the ' - 'Stockfish program on this computer.' + "Chess Bot is a bot that allows you to play chess against either " + "another user or the computer. Use `start with other user` or " + "`start as with computer` to start a game.\n\n" + "In order to play against a computer, `chess.conf` must be set " + "with the key `stockfish_location` set to the location of the " + "Stockfish program on this computer." ) def initialize(self, bot_handler: BotHandler) -> None: - self.config_info = bot_handler.get_config_info('chess') + self.config_info = bot_handler.get_config_info("chess") try: - self.engine = chess.uci.popen_engine(self.config_info['stockfish_location']) + self.engine = chess.uci.popen_engine(self.config_info["stockfish_location"]) self.engine.uci() except FileNotFoundError: # It is helpful to allow for fake Stockfish locations if the bot # runner is testing or knows they won't be using an engine. - print('That Stockfish doesn\'t exist. Continuing.') + print("That Stockfish doesn't exist. Continuing.") def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: - content = message['content'] + content = message["content"] - if content == '': + if content == "": bot_handler.send_reply(message, self.usage()) return @@ -50,29 +50,29 @@ class ChessHandler: is_with_computer = False last_fen = chess.Board().fen() - if bot_handler.storage.contains('is_with_computer'): + if bot_handler.storage.contains("is_with_computer"): is_with_computer = ( # `bot_handler`'s `storage` only accepts `str` values. - bot_handler.storage.get('is_with_computer') + bot_handler.storage.get("is_with_computer") == str(True) ) - if bot_handler.storage.contains('last_fen'): - last_fen = bot_handler.storage.get('last_fen') + if bot_handler.storage.contains("last_fen"): + last_fen = bot_handler.storage.get("last_fen") if start_regex_match: self.start(message, bot_handler) elif start_computer_regex_match: self.start_computer( - message, bot_handler, start_computer_regex_match.group('user_color') == 'white' + message, bot_handler, start_computer_regex_match.group("user_color") == "white" ) elif move_regex_match: if is_with_computer: self.move_computer( - message, bot_handler, last_fen, move_regex_match.group('move_san') + message, bot_handler, last_fen, move_regex_match.group("move_san") ) else: - self.move(message, bot_handler, last_fen, move_regex_match.group('move_san')) + self.move(message, bot_handler, last_fen, move_regex_match.group("move_san")) elif resign_regex_match: self.resign(message, bot_handler, last_fen) @@ -88,9 +88,9 @@ class ChessHandler: bot_handler.send_reply(message, make_start_reponse(new_board)) # `bot_handler`'s `storage` only accepts `str` values. - bot_handler.storage.put('is_with_computer', str(False)) + bot_handler.storage.put("is_with_computer", str(False)) - bot_handler.storage.put('last_fen', new_board.fen()) + bot_handler.storage.put("last_fen", new_board.fen()) def start_computer( self, message: Dict[str, str], bot_handler: BotHandler, is_white_user: bool @@ -112,9 +112,9 @@ class ChessHandler: bot_handler.send_reply(message, make_start_computer_reponse(new_board)) # `bot_handler`'s `storage` only accepts `str` values. - bot_handler.storage.put('is_with_computer', str(True)) + bot_handler.storage.put("is_with_computer", str(True)) - bot_handler.storage.put('last_fen', new_board.fen()) + bot_handler.storage.put("last_fen", new_board.fen()) else: self.move_computer_first( message, @@ -204,18 +204,18 @@ class ChessHandler: # wants the game to be a draw, after 3 or 75 it a draw. For now, # just assume that the players would want the draw. if new_board.is_game_over(True): - game_over_output = '' + game_over_output = "" if new_board.is_checkmate(): - game_over_output = make_loss_response(new_board, 'was checkmated') + game_over_output = make_loss_response(new_board, "was checkmated") elif new_board.is_stalemate(): - game_over_output = make_draw_response('stalemate') + game_over_output = make_draw_response("stalemate") elif new_board.is_insufficient_material(): - game_over_output = make_draw_response('insufficient material') + game_over_output = make_draw_response("insufficient material") elif new_board.can_claim_fifty_moves(): - game_over_output = make_draw_response('50 moves without a capture or pawn move') + game_over_output = make_draw_response("50 moves without a capture or pawn move") elif new_board.can_claim_threefold_repetition(): - game_over_output = make_draw_response('3-fold repetition') + game_over_output = make_draw_response("3-fold repetition") bot_handler.send_reply(message, game_over_output) @@ -253,7 +253,7 @@ class ChessHandler: bot_handler.send_reply(message, make_move_reponse(last_board, new_board, move)) - bot_handler.storage.put('last_fen', new_board.fen()) + bot_handler.storage.put("last_fen", new_board.fen()) def move_computer( self, message: Dict[str, str], bot_handler: BotHandler, last_fen: str, move_san: str @@ -299,7 +299,7 @@ class ChessHandler: message, make_move_reponse(new_board, new_board_after_computer_move, computer_move) ) - bot_handler.storage.put('last_fen', new_board_after_computer_move.fen()) + bot_handler.storage.put("last_fen", new_board_after_computer_move.fen()) def move_computer_first( self, message: Dict[str, str], bot_handler: BotHandler, last_fen: str @@ -329,10 +329,10 @@ class ChessHandler: message, make_move_reponse(last_board, new_board_after_computer_move, computer_move) ) - bot_handler.storage.put('last_fen', new_board_after_computer_move.fen()) + bot_handler.storage.put("last_fen", new_board_after_computer_move.fen()) # `bot_handler`'s `storage` only accepts `str` values. - bot_handler.storage.put('is_with_computer', str(True)) + bot_handler.storage.put("is_with_computer", str(True)) def resign(self, message: Dict[str, str], bot_handler: BotHandler, last_fen: str) -> None: """Resigns the game for the current player. @@ -347,7 +347,7 @@ class ChessHandler: if not last_board: return - bot_handler.send_reply(message, make_loss_response(last_board, 'resigned')) + bot_handler.send_reply(message, make_loss_response(last_board, "resigned")) handler_class = ChessHandler @@ -376,7 +376,7 @@ def make_draw_response(reason: str) -> str: Returns: The draw response string. """ - return 'It\'s a draw because of {}!'.format(reason) + return "It's a draw because of {}!".format(reason) def make_loss_response(board: chess.Board, reason: str) -> str: @@ -389,10 +389,10 @@ def make_loss_response(board: chess.Board, reason: str) -> str: Returns: The loss response string. """ - return ('*{}* {}. **{}** wins!\n\n' '{}').format( - 'White' if board.turn else 'Black', + return ("*{}* {}. **{}** wins!\n\n" "{}").format( + "White" if board.turn else "Black", reason, - 'Black' if board.turn else 'White', + "Black" if board.turn else "White", make_str(board, board.turn), ) @@ -406,7 +406,7 @@ def make_not_legal_response(board: chess.Board, move_san: str) -> str: Returns: The not-legal-move response string. """ - return ('Sorry, the move *{}* isn\'t legal.\n\n' '{}' '\n\n\n' '{}').format( + return ("Sorry, the move *{}* isn't legal.\n\n" "{}" "\n\n\n" "{}").format( move_san, make_str(board, board.turn), make_footer() ) @@ -417,8 +417,8 @@ def make_copied_wrong_response() -> str: Returns: The copied-wrong response string. """ return ( - 'Sorry, it seems like you copied down the response wrong.\n\n' - 'Please try to copy the response again from the last message!' + "Sorry, it seems like you copied down the response wrong.\n\n" + "Please try to copy the response again from the last message!" ) @@ -433,13 +433,13 @@ def make_start_reponse(board: chess.Board) -> str: Returns: The starting response string. """ return ( - 'New game! The board looks like this:\n\n' - '{}' - '\n\n\n' - 'Now it\'s **{}**\'s turn.' - '\n\n\n' - '{}' - ).format(make_str(board, True), 'white' if board.turn else 'black', make_footer()) + "New game! The board looks like this:\n\n" + "{}" + "\n\n\n" + "Now it's **{}**'s turn." + "\n\n\n" + "{}" + ).format(make_str(board, True), "white" if board.turn else "black", make_footer()) def make_start_computer_reponse(board: chess.Board) -> str: @@ -454,13 +454,13 @@ def make_start_computer_reponse(board: chess.Board) -> str: Returns: The starting response string. """ return ( - 'New game with computer! The board looks like this:\n\n' - '{}' - '\n\n\n' - 'Now it\'s **{}**\'s turn.' - '\n\n\n' - '{}' - ).format(make_str(board, True), 'white' if board.turn else 'black', make_footer()) + "New game with computer! The board looks like this:\n\n" + "{}" + "\n\n\n" + "Now it's **{}**'s turn." + "\n\n\n" + "{}" + ).format(make_str(board, True), "white" if board.turn else "black", make_footer()) def make_move_reponse(last_board: chess.Board, new_board: chess.Board, move: chess.Move) -> str: @@ -474,21 +474,21 @@ def make_move_reponse(last_board: chess.Board, new_board: chess.Board, move: che Returns: The move response string. """ return ( - 'The board was like this:\n\n' - '{}' - '\n\n\n' - 'Then *{}* moved *{}*:\n\n' - '{}' - '\n\n\n' - 'Now it\'s **{}**\'s turn.' - '\n\n\n' - '{}' + "The board was like this:\n\n" + "{}" + "\n\n\n" + "Then *{}* moved *{}*:\n\n" + "{}" + "\n\n\n" + "Now it's **{}**'s turn." + "\n\n\n" + "{}" ).format( make_str(last_board, new_board.turn), - 'white' if last_board.turn else 'black', + "white" if last_board.turn else "black", last_board.san(move), make_str(new_board, new_board.turn), - 'white' if new_board.turn else 'black', + "white" if new_board.turn else "black", make_footer(), ) @@ -498,10 +498,10 @@ def make_footer() -> str: responses. """ return ( - 'To make your next move, respond to Chess Bot with\n\n' - '```do ```\n\n' - '*Remember to @-mention Chess Bot at the beginning of your ' - 'response.*' + "To make your next move, respond to Chess Bot with\n\n" + "```do ```\n\n" + "*Remember to @-mention Chess Bot at the beginning of your " + "response.*" ) @@ -525,7 +525,7 @@ def make_str(board: chess.Board, is_white_on_bottom: bool) -> str: replaced_and_guided_str if is_white_on_bottom else replaced_and_guided_str[::-1] ) trimmed_str = trim_whitespace_before_newline(properly_flipped_str) - monospaced_str = '```\n{}\n```'.format(trimmed_str) + monospaced_str = "```\n{}\n```".format(trimmed_str) return monospaced_str @@ -542,11 +542,11 @@ def guide_with_numbers(board_str: str) -> str: # Spaces and newlines would mess up the loop because they add extra indexes # between pieces. Newlines are added later by the loop and spaces are added # back in at the end. - board_without_whitespace_str = board_str.replace(' ', '').replace('\n', '') + board_without_whitespace_str = board_str.replace(" ", "").replace("\n", "") # The first number, 8, needs to be added first because it comes before a # newline. From then on, numbers are inserted at newlines. - row_list = list('8' + board_without_whitespace_str) + row_list = list("8" + board_without_whitespace_str) for i, char in enumerate(row_list): # `(i + 1) % 10 == 0` if it is the end of a row, i.e., the 10th column @@ -563,14 +563,14 @@ def guide_with_numbers(board_str: str) -> str: # the newline isn't counted by the loop. If they were split into 3, # or combined into just 1 string, the counter would become off # because it would be counting what is really 2 rows as 3 or 1. - row_list[i:i] = [str(row_num) + '\n', str(row_num - 1)] + row_list[i:i] = [str(row_num) + "\n", str(row_num - 1)] # 1 is appended to the end because it isn't created in the loop, and lines # that begin with spaces have their spaces removed for aesthetics. - row_str = (' '.join(row_list) + ' 1').replace('\n ', '\n') + row_str = (" ".join(row_list) + " 1").replace("\n ", "\n") # a, b, c, d, e, f, g, and h are easy to add in. - row_and_col_str = ' a b c d e f g h \n' + row_str + '\n a b c d e f g h ' + row_and_col_str = " a b c d e f g h \n" + row_str + "\n a b c d e f g h " return row_and_col_str @@ -586,21 +586,21 @@ def replace_with_unicode(board_str: str) -> str: """ replaced_str = board_str - replaced_str = replaced_str.replace('P', '♙') - replaced_str = replaced_str.replace('N', '♘') - replaced_str = replaced_str.replace('B', '♗') - replaced_str = replaced_str.replace('R', '♖') - replaced_str = replaced_str.replace('Q', '♕') - replaced_str = replaced_str.replace('K', '♔') + replaced_str = replaced_str.replace("P", "♙") + replaced_str = replaced_str.replace("N", "♘") + replaced_str = replaced_str.replace("B", "♗") + replaced_str = replaced_str.replace("R", "♖") + replaced_str = replaced_str.replace("Q", "♕") + replaced_str = replaced_str.replace("K", "♔") - replaced_str = replaced_str.replace('p', '♟') - replaced_str = replaced_str.replace('n', '♞') - replaced_str = replaced_str.replace('b', '♝') - replaced_str = replaced_str.replace('r', '♜') - replaced_str = replaced_str.replace('q', '♛') - replaced_str = replaced_str.replace('k', '♚') + replaced_str = replaced_str.replace("p", "♟") + replaced_str = replaced_str.replace("n", "♞") + replaced_str = replaced_str.replace("b", "♝") + replaced_str = replaced_str.replace("r", "♜") + replaced_str = replaced_str.replace("q", "♛") + replaced_str = replaced_str.replace("k", "♚") - replaced_str = replaced_str.replace('.', '·') + replaced_str = replaced_str.replace(".", "·") return replaced_str @@ -613,4 +613,4 @@ def trim_whitespace_before_newline(str_to_trim: str) -> str: Returns: The trimmed string. """ - return re.sub(r'\s+$', '', str_to_trim, flags=re.M) + return re.sub(r"\s+$", "", str_to_trim, flags=re.M) diff --git a/zulip_bots/zulip_bots/bots/chessbot/test_chessbot.py b/zulip_bots/zulip_bots/bots/chessbot/test_chessbot.py index 5d00256..03ff057 100644 --- a/zulip_bots/zulip_bots/bots/chessbot/test_chessbot.py +++ b/zulip_bots/zulip_bots/bots/chessbot/test_chessbot.py @@ -4,7 +4,7 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests class TestChessBot(BotTestCase, DefaultTests): bot_name = "chessbot" - START_RESPONSE = '''New game! The board looks like this: + START_RESPONSE = """New game! The board looks like this: ``` a b c d e f g h @@ -27,9 +27,9 @@ To make your next move, respond to Chess Bot with ```do ``` -*Remember to @-mention Chess Bot at the beginning of your response.*''' +*Remember to @-mention Chess Bot at the beginning of your response.*""" - DO_E4_RESPONSE = '''The board was like this: + DO_E4_RESPONSE = """The board was like this: ``` h g f e d c b a @@ -68,9 +68,9 @@ To make your next move, respond to Chess Bot with ```do ``` -*Remember to @-mention Chess Bot at the beginning of your response.*''' +*Remember to @-mention Chess Bot at the beginning of your response.*""" - DO_KE4_RESPONSE = '''Sorry, the move *Ke4* isn't legal. + DO_KE4_RESPONSE = """Sorry, the move *Ke4* isn't legal. ``` h g f e d c b a @@ -90,9 +90,9 @@ To make your next move, respond to Chess Bot with ```do ``` -*Remember to @-mention Chess Bot at the beginning of your response.*''' +*Remember to @-mention Chess Bot at the beginning of your response.*""" - RESIGN_RESPONSE = '''*Black* resigned. **White** wins! + RESIGN_RESPONSE = """*Black* resigned. **White** wins! ``` h g f e d c b a @@ -105,20 +105,20 @@ To make your next move, respond to Chess Bot with 7 ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟ 7 8 ♜ ♞ ♝ ♚ ♛ ♝ ♞ ♜ 8 h g f e d c b a -```''' +```""" def test_bot_responds_to_empty_message(self) -> None: - with self.mock_config_info({'stockfish_location': '/foo/bar'}): - response = self.get_response(dict(content='')) - self.assertIn('play chess', response['content']) + with self.mock_config_info({"stockfish_location": "/foo/bar"}): + response = self.get_response(dict(content="")) + self.assertIn("play chess", response["content"]) def test_main(self) -> None: - with self.mock_config_info({'stockfish_location': '/foo/bar'}): + with self.mock_config_info({"stockfish_location": "/foo/bar"}): self.verify_dialog( [ - ('start with other user', self.START_RESPONSE), - ('do e4', self.DO_E4_RESPONSE), - ('do Ke4', self.DO_KE4_RESPONSE), - ('resign', self.RESIGN_RESPONSE), + ("start with other user", self.START_RESPONSE), + ("do e4", self.DO_E4_RESPONSE), + ("do Ke4", self.DO_KE4_RESPONSE), + ("resign", self.RESIGN_RESPONSE), ] ) diff --git a/zulip_bots/zulip_bots/bots/connect_four/connect_four.py b/zulip_bots/zulip_bots/bots/connect_four/connect_four.py index fe5d25c..4a653a8 100644 --- a/zulip_bots/zulip_bots/bots/connect_four/connect_four.py +++ b/zulip_bots/zulip_bots/bots/connect_four/connect_four.py @@ -5,21 +5,21 @@ from zulip_bots.game_handler import GameAdapter class ConnectFourMessageHandler: - tokens = [':blue_circle:', ':red_circle:'] + tokens = [":blue_circle:", ":red_circle:"] def parse_board(self, board: Any) -> str: # Header for the top of the board - board_str = ':one: :two: :three: :four: :five: :six: :seven:' + board_str = ":one: :two: :three: :four: :five: :six: :seven:" for row in range(0, 6): - board_str += '\n\n' + board_str += "\n\n" for column in range(0, 7): if board[row][column] == 0: - board_str += ':white_circle: ' + board_str += ":white_circle: " elif board[row][column] == 1: - board_str += self.tokens[0] + ' ' + board_str += self.tokens[0] + " " elif board[row][column] == -1: - board_str += self.tokens[1] + ' ' + board_str += self.tokens[1] + " " return board_str @@ -27,33 +27,33 @@ class ConnectFourMessageHandler: return self.tokens[turn] def alert_move_message(self, original_player: str, move_info: str) -> str: - column_number = move_info.replace('move ', '') - return original_player + ' moved in column ' + column_number + column_number = move_info.replace("move ", "") + return original_player + " moved in column " + column_number def game_start_message(self) -> str: - return 'Type `move ` or `` to place a token.\n\ -The first player to get 4 in a row wins!\n Good Luck!' + return "Type `move ` or `` to place a token.\n\ +The first player to get 4 in a row wins!\n Good Luck!" class ConnectFourBotHandler(GameAdapter): - ''' + """ Bot that uses the Game Adapter class to allow users to play other users or the comptuer in a game of Connect Four - ''' + """ def __init__(self) -> None: - game_name = 'Connect Four' - bot_name = 'connect_four' + game_name = "Connect Four" + bot_name = "connect_four" move_help_message = ( - '* To make your move during a game, type\n' - '```move ``` or ``````' + "* To make your move during a game, type\n" + "```move ``` or ``````" ) - move_regex = '(move ([1-7])$)|(([1-7])$)' + move_regex = "(move ([1-7])$)|(([1-7])$)" model = ConnectFourModel gameMessageHandler = ConnectFourMessageHandler - rules = '''Try to get four pieces in row, Diagonals count too!''' + rules = """Try to get four pieces in row, Diagonals count too!""" super().__init__( game_name, diff --git a/zulip_bots/zulip_bots/bots/connect_four/controller.py b/zulip_bots/zulip_bots/bots/connect_four/controller.py index 20bc5a0..eed70b4 100644 --- a/zulip_bots/zulip_bots/bots/connect_four/controller.py +++ b/zulip_bots/zulip_bots/bots/connect_four/controller.py @@ -5,10 +5,10 @@ from zulip_bots.game_handler import BadMoveException class ConnectFourModel: - ''' + """ Object that manages running the Connect Four logic for the Connect Four Bot - ''' + """ def __init__(self): self.blank_board = [ @@ -54,11 +54,11 @@ class ConnectFourModel: token_number = 1 finding_move = True row = 5 - column = int(move.replace('move ', '')) - 1 + column = int(move.replace("move ", "")) - 1 while finding_move: if row < 0: - raise BadMoveException('Make sure your move is in a column with free space.') + raise BadMoveException("Make sure your move is in a column with free space.") if self.current_board[row][column] == 0: self.current_board[row][column] = token_number finding_move = False @@ -143,7 +143,7 @@ class ConnectFourModel: top_row_multiple = reduce(lambda x, y: x * y, self.current_board[0]) if top_row_multiple != 0: - return 'draw' + return "draw" winner = ( get_horizontal_wins(self.current_board) @@ -156,4 +156,4 @@ class ConnectFourModel: elif winner == -1: return second_player - return '' + return "" diff --git a/zulip_bots/zulip_bots/bots/connect_four/test_connect_four.py b/zulip_bots/zulip_bots/bots/connect_four/test_connect_four.py index cb0cffa..48a547b 100644 --- a/zulip_bots/zulip_bots/bots/connect_four/test_connect_four.py +++ b/zulip_bots/zulip_bots/bots/connect_four/test_connect_four.py @@ -6,10 +6,10 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests class TestConnectFourBot(BotTestCase, DefaultTests): - bot_name = 'connect_four' + bot_name = "connect_four" def make_request_message( - self, content: str, user: str = 'foo@example.com', user_name: str = 'foo' + self, content: str, user: str = "foo@example.com", user_name: str = "foo" ) -> Dict[str, str]: message = dict(sender_email=user, content=content, sender_full_name=user_name) return message @@ -20,14 +20,14 @@ class TestConnectFourBot(BotTestCase, DefaultTests): request: str, expected_response: str, response_number: int, - user: str = 'foo@example.com', + user: str = "foo@example.com", ) -> None: - ''' + """ This function serves a similar purpose to BotTestCase.verify_dialog, but allows for multiple responses to be validated, and for mocking of the bot's internal data - ''' + """ bot, bot_handler = self._get_handlers() message = self.make_request_message(request, user) @@ -38,10 +38,10 @@ class TestConnectFourBot(BotTestCase, DefaultTests): responses = [message for (method, message) in bot_handler.transcript] first_response = responses[response_number] - self.assertEqual(expected_response, first_response['content']) + self.assertEqual(expected_response, first_response["content"]) def help_message(self) -> str: - return '''** Connect Four Bot Help:** + return """** Connect Four Bot Help:** *Preface all commands with @**test-bot*** * To start a game in a stream (*recommended*), type `start game` @@ -62,15 +62,15 @@ class TestConnectFourBot(BotTestCase, DefaultTests): * To see rules of this game, type `rules` * To make your move during a game, type -```move ``` or ``````''' +```move ``` or ``````""" def test_static_responses(self) -> None: - self.verify_response('help', self.help_message(), 0) + self.verify_response("help", self.help_message(), 0) def test_game_message_handler_responses(self) -> None: board = ( - ':one: :two: :three: :four: :five: :six: :seven:\n\n' - + '\ + ":one: :two: :three: :four: :five: :six: :seven:\n\n" + + "\ :white_circle: :white_circle: :white_circle: :white_circle: \ :white_circle: :white_circle: :white_circle: \n\n\ :white_circle: :white_circle: :white_circle: :white_circle: \ @@ -82,18 +82,18 @@ class TestConnectFourBot(BotTestCase, DefaultTests): :blue_circle: :red_circle: :white_circle: :white_circle: :white_circle: \ :white_circle: :white_circle: \n\n\ :blue_circle: :red_circle: :white_circle: :white_circle: :white_circle: \ -:white_circle: :white_circle: ' +:white_circle: :white_circle: " ) bot, bot_handler = self._get_handlers() self.assertEqual(bot.gameMessageHandler.parse_board(self.almost_win_board), board) - self.assertEqual(bot.gameMessageHandler.get_player_color(1), ':red_circle:') + self.assertEqual(bot.gameMessageHandler.get_player_color(1), ":red_circle:") self.assertEqual( - bot.gameMessageHandler.alert_move_message('foo', 'move 6'), 'foo moved in column 6' + bot.gameMessageHandler.alert_move_message("foo", "move 6"), "foo moved in column 6" ) self.assertEqual( bot.gameMessageHandler.game_start_message(), - 'Type `move ` or `` to place a token.\n\ -The first player to get 4 in a row wins!\n Good Luck!', + "Type `move ` or `` to place a token.\n\ +The first player to get 4 in a row wins!\n Good Luck!", ) blank_board = [ @@ -142,22 +142,22 @@ The first player to get 4 in a row wins!\n Good Luck!', final_board: List[List[int]], ) -> None: connectFourModel.update_board(initial_board) - test_board = connectFourModel.make_move('move ' + str(column_number), token_number) + test_board = connectFourModel.make_move("move " + str(column_number), token_number) self.assertEqual(test_board, final_board) def confirmGameOver(board: List[List[int]], result: str) -> None: connectFourModel.update_board(board) - game_over = connectFourModel.determine_game_over(['first_player', 'second_player']) + game_over = connectFourModel.determine_game_over(["first_player", "second_player"]) self.assertEqual(game_over, result) def confirmWinStates(array: List[List[List[List[int]]]]) -> None: for board in array[0]: - confirmGameOver(board, 'first_player') + confirmGameOver(board, "first_player") for board in array[1]: - confirmGameOver(board, 'second_player') + confirmGameOver(board, "second_player") connectFourModel = ConnectFourModel() @@ -553,8 +553,8 @@ The first player to get 4 in a row wins!\n Good Luck!', ) # Test Game Over Logic: - confirmGameOver(blank_board, '') - confirmGameOver(full_board, 'draw') + confirmGameOver(blank_board, "") + confirmGameOver(full_board, "draw") # Test Win States: confirmWinStates(horizontal_win_boards) @@ -564,7 +564,7 @@ The first player to get 4 in a row wins!\n Good Luck!', def test_more_logic(self) -> None: model = ConnectFourModel() - move = 'move 4' + move = "move 4" col = 3 # zero-indexed self.assertEqual(model.get_column(col), [0, 0, 0, 0, 0, 0]) diff --git a/zulip_bots/zulip_bots/bots/converter/converter.py b/zulip_bots/zulip_bots/bots/converter/converter.py index 272b889..815d815 100644 --- a/zulip_bots/zulip_bots/bots/converter/converter.py +++ b/zulip_bots/zulip_bots/bots/converter/converter.py @@ -28,17 +28,17 @@ def round_to(x: float, digits: int) -> float: class ConverterHandler: - ''' + """ This plugin allows users to make conversions between various units, e.g. Celsius to Fahrenheit, or kilobytes to gigabytes. It looks for messages of the format '@mention-bot ' The message '@mention-bot help' posts a short description of how to use the plugin, along with a list of all supported units. - ''' + """ def usage(self) -> str: - return ''' + return """ This plugin allows users to make conversions between various units, e.g. Celsius to Fahrenheit, or kilobytes to gigabytes. It looks for messages of @@ -46,7 +46,7 @@ class ConverterHandler: The message '@mention-bot help' posts a short description of how to use the plugin, along with a list of all supported units. - ''' + """ def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: bot_response = get_bot_converter_response(message, bot_handler) @@ -54,7 +54,7 @@ class ConverterHandler: def get_bot_converter_response(message: Dict[str, str], bot_handler: BotHandler) -> str: - content = message['content'] + content = message["content"] words = content.lower().split() convert_indexes = [i for i, word in enumerate(words) if word == "@convert"] @@ -62,7 +62,7 @@ def get_bot_converter_response(message: Dict[str, str], bot_handler: BotHandler) results = [] for convert_index in convert_indexes: - if (convert_index + 1) < len(words) and words[convert_index + 1] == 'help': + if (convert_index + 1) < len(words) and words[convert_index + 1] == "help": results.append(utils.HELP_MESSAGE) continue if (convert_index + 3) < len(words): @@ -72,7 +72,7 @@ def get_bot_converter_response(message: Dict[str, str], bot_handler: BotHandler) exponent = 0 if not is_float(number): - results.append('`' + number + '` is not a valid number. ' + utils.QUICK_HELP) + results.append("`" + number + "` is not a valid number. " + utils.QUICK_HELP) continue # cannot reassign "number" as a float after using as string, so changed name @@ -91,22 +91,22 @@ def get_bot_converter_response(message: Dict[str, str], bot_handler: BotHandler) ut_to_std = utils.UNITS.get(unit_to, []) # type: List[Any] if not uf_to_std: - results.append('`' + unit_from + '` is not a valid unit. ' + utils.QUICK_HELP) + results.append("`" + unit_from + "` is not a valid unit. " + utils.QUICK_HELP) if not ut_to_std: - results.append('`' + unit_to + '` is not a valid unit.' + utils.QUICK_HELP) + results.append("`" + unit_to + "` is not a valid unit." + utils.QUICK_HELP) if not uf_to_std or not ut_to_std: continue base_unit = uf_to_std[2] if uf_to_std[2] != ut_to_std[2]: - unit_from = unit_from.capitalize() if uf_to_std[2] == 'kelvin' else unit_from + unit_from = unit_from.capitalize() if uf_to_std[2] == "kelvin" else unit_from results.append( - '`' + "`" + unit_to.capitalize() - + '` and `' + + "` and `" + unit_from - + '`' - + ' are not from the same category. ' + + "`" + + " are not from the same category. " + utils.QUICK_HELP ) continue @@ -117,24 +117,24 @@ def get_bot_converter_response(message: Dict[str, str], bot_handler: BotHandler) number_res -= ut_to_std[0] number_res /= ut_to_std[1] - if base_unit == 'bit': + if base_unit == "bit": number_res *= 1024 ** (exponent // 3) else: number_res *= 10 ** exponent number_res = round_to(number_res, 7) results.append( - '{} {} = {} {}'.format( + "{} {} = {} {}".format( number, words[convert_index + 2], number_res, words[convert_index + 3] ) ) else: - results.append('Too few arguments given. ' + utils.QUICK_HELP) + results.append("Too few arguments given. " + utils.QUICK_HELP) - new_content = '' + new_content = "" for idx, result in enumerate(results, 1): - new_content += ((str(idx) + '. conversion: ') if len(results) > 1 else '') + result + '\n' + new_content += ((str(idx) + ". conversion: ") if len(results) > 1 else "") + result + "\n" return new_content diff --git a/zulip_bots/zulip_bots/bots/converter/test_converter.py b/zulip_bots/zulip_bots/bots/converter/test_converter.py index 6409eb6..0692332 100755 --- a/zulip_bots/zulip_bots/bots/converter/test_converter.py +++ b/zulip_bots/zulip_bots/bots/converter/test_converter.py @@ -9,13 +9,13 @@ class TestConverterBot(BotTestCase, DefaultTests): dialog = [ ( "", - 'Too few arguments given. Enter `@convert help` ' - 'for help on using the converter.\n', + "Too few arguments given. Enter `@convert help` " + "for help on using the converter.\n", ), ( "foo bar", - 'Too few arguments given. Enter `@convert help` ' - 'for help on using the converter.\n', + "Too few arguments given. Enter `@convert help` " + "for help on using the converter.\n", ), ("2 m cm", "2 m = 200.0 cm\n"), ("12.0 celsius fahrenheit", "12.0 celsius = 53.600054 fahrenheit\n"), diff --git a/zulip_bots/zulip_bots/bots/converter/utils.py b/zulip_bots/zulip_bots/bots/converter/utils.py index 0e87b58..3a32f8c 100644 --- a/zulip_bots/zulip_bots/bots/converter/utils.py +++ b/zulip_bots/zulip_bots/bots/converter/utils.py @@ -3,152 +3,152 @@ # factor that need to be added and multiplied to convert the unit into # the base unit in the last parameter. UNITS = { - 'bit': [0, 1, 'bit'], - 'byte': [0, 8, 'bit'], - 'cubic-centimeter': [0, 0.000001, 'cubic-meter'], - 'cubic-decimeter': [0, 0.001, 'cubic-meter'], - 'liter': [0, 0.001, 'cubic-meter'], - 'cubic-meter': [0, 1, 'cubic-meter'], - 'cubic-inch': [0, 0.000016387064, 'cubic-meter'], - 'fluid-ounce': [0, 0.000029574, 'cubic-meter'], - 'cubic-foot': [0, 0.028316846592, 'cubic-meter'], - 'cubic-yard': [0, 0.764554857984, 'cubic-meter'], - 'teaspoon': [0, 0.0000049289216, 'cubic-meter'], - 'tablespoon': [0, 0.000014787, 'cubic-meter'], - 'cup': [0, 0.00023658823648491, 'cubic-meter'], - 'gram': [0, 1, 'gram'], - 'kilogram': [0, 1000, 'gram'], - 'ton': [0, 1000000, 'gram'], - 'ounce': [0, 28.349523125, 'gram'], - 'pound': [0, 453.59237, 'gram'], - 'kelvin': [0, 1, 'kelvin'], - 'celsius': [273.15, 1, 'kelvin'], - 'fahrenheit': [255.372222, 0.555555, 'kelvin'], - 'centimeter': [0, 0.01, 'meter'], - 'decimeter': [0, 0.1, 'meter'], - 'meter': [0, 1, 'meter'], - 'kilometer': [0, 1000, 'meter'], - 'inch': [0, 0.0254, 'meter'], - 'foot': [0, 0.3048, 'meter'], - 'yard': [0, 0.9144, 'meter'], - 'mile': [0, 1609.344, 'meter'], - 'nautical-mile': [0, 1852, 'meter'], - 'square-centimeter': [0, 0.0001, 'square-meter'], - 'square-decimeter': [0, 0.01, 'square-meter'], - 'square-meter': [0, 1, 'square-meter'], - 'square-kilometer': [0, 1000000, 'square-meter'], - 'square-inch': [0, 0.00064516, 'square-meter'], - 'square-foot': [0, 0.09290304, 'square-meter'], - 'square-yard': [0, 0.83612736, 'square-meter'], - 'square-mile': [0, 2589988.110336, 'square-meter'], - 'are': [0, 100, 'square-meter'], - 'hectare': [0, 10000, 'square-meter'], - 'acre': [0, 4046.8564224, 'square-meter'], + "bit": [0, 1, "bit"], + "byte": [0, 8, "bit"], + "cubic-centimeter": [0, 0.000001, "cubic-meter"], + "cubic-decimeter": [0, 0.001, "cubic-meter"], + "liter": [0, 0.001, "cubic-meter"], + "cubic-meter": [0, 1, "cubic-meter"], + "cubic-inch": [0, 0.000016387064, "cubic-meter"], + "fluid-ounce": [0, 0.000029574, "cubic-meter"], + "cubic-foot": [0, 0.028316846592, "cubic-meter"], + "cubic-yard": [0, 0.764554857984, "cubic-meter"], + "teaspoon": [0, 0.0000049289216, "cubic-meter"], + "tablespoon": [0, 0.000014787, "cubic-meter"], + "cup": [0, 0.00023658823648491, "cubic-meter"], + "gram": [0, 1, "gram"], + "kilogram": [0, 1000, "gram"], + "ton": [0, 1000000, "gram"], + "ounce": [0, 28.349523125, "gram"], + "pound": [0, 453.59237, "gram"], + "kelvin": [0, 1, "kelvin"], + "celsius": [273.15, 1, "kelvin"], + "fahrenheit": [255.372222, 0.555555, "kelvin"], + "centimeter": [0, 0.01, "meter"], + "decimeter": [0, 0.1, "meter"], + "meter": [0, 1, "meter"], + "kilometer": [0, 1000, "meter"], + "inch": [0, 0.0254, "meter"], + "foot": [0, 0.3048, "meter"], + "yard": [0, 0.9144, "meter"], + "mile": [0, 1609.344, "meter"], + "nautical-mile": [0, 1852, "meter"], + "square-centimeter": [0, 0.0001, "square-meter"], + "square-decimeter": [0, 0.01, "square-meter"], + "square-meter": [0, 1, "square-meter"], + "square-kilometer": [0, 1000000, "square-meter"], + "square-inch": [0, 0.00064516, "square-meter"], + "square-foot": [0, 0.09290304, "square-meter"], + "square-yard": [0, 0.83612736, "square-meter"], + "square-mile": [0, 2589988.110336, "square-meter"], + "are": [0, 100, "square-meter"], + "hectare": [0, 10000, "square-meter"], + "acre": [0, 4046.8564224, "square-meter"], } PREFIXES = { - 'atto': -18, - 'femto': -15, - 'pico': -12, - 'nano': -9, - 'micro': -6, - 'milli': -3, - 'centi': -2, - 'deci': -1, - 'deca': 1, - 'hecto': 2, - 'kilo': 3, - 'mega': 6, - 'giga': 9, - 'tera': 12, - 'peta': 15, - 'exa': 18, + "atto": -18, + "femto": -15, + "pico": -12, + "nano": -9, + "micro": -6, + "milli": -3, + "centi": -2, + "deci": -1, + "deca": 1, + "hecto": 2, + "kilo": 3, + "mega": 6, + "giga": 9, + "tera": 12, + "peta": 15, + "exa": 18, } ALIASES = { - 'a': 'are', - 'ac': 'acre', - 'c': 'celsius', - 'cm': 'centimeter', - 'cm2': 'square-centimeter', - 'cm3': 'cubic-centimeter', - 'cm^2': 'square-centimeter', - 'cm^3': 'cubic-centimeter', - 'dm': 'decimeter', - 'dm2': 'square-decimeter', - 'dm3': 'cubic-decimeter', - 'dm^2': 'square-decimeter', - 'dm^3': 'cubic-decimeter', - 'f': 'fahrenheit', - 'fl-oz': 'fluid-ounce', - 'ft': 'foot', - 'ft2': 'square-foot', - 'ft3': 'cubic-foot', - 'ft^2': 'square-foot', - 'ft^3': 'cubic-foot', - 'g': 'gram', - 'ha': 'hectare', - 'in': 'inch', - 'in2': 'square-inch', - 'in3': 'cubic-inch', - 'in^2': 'square-inch', - 'in^3': 'cubic-inch', - 'k': 'kelvin', - 'kg': 'kilogram', - 'km': 'kilometer', - 'km2': 'square-kilometer', - 'km^2': 'square-kilometer', - 'l': 'liter', - 'lb': 'pound', - 'm': 'meter', - 'm2': 'square-meter', - 'm3': 'cubic-meter', - 'm^2': 'square-meter', - 'm^3': 'cubic-meter', - 'mi': 'mile', - 'mi2': 'square-mile', - 'mi^2': 'square-mile', - 'nmi': 'nautical-mile', - 'oz': 'ounce', - 't': 'ton', - 'tbsp': 'tablespoon', - 'tsp': 'teaspoon', - 'y': 'yard', - 'y2': 'square-yard', - 'y3': 'cubic-yard', - 'y^2': 'square-yard', - 'y^3': 'cubic-yard', + "a": "are", + "ac": "acre", + "c": "celsius", + "cm": "centimeter", + "cm2": "square-centimeter", + "cm3": "cubic-centimeter", + "cm^2": "square-centimeter", + "cm^3": "cubic-centimeter", + "dm": "decimeter", + "dm2": "square-decimeter", + "dm3": "cubic-decimeter", + "dm^2": "square-decimeter", + "dm^3": "cubic-decimeter", + "f": "fahrenheit", + "fl-oz": "fluid-ounce", + "ft": "foot", + "ft2": "square-foot", + "ft3": "cubic-foot", + "ft^2": "square-foot", + "ft^3": "cubic-foot", + "g": "gram", + "ha": "hectare", + "in": "inch", + "in2": "square-inch", + "in3": "cubic-inch", + "in^2": "square-inch", + "in^3": "cubic-inch", + "k": "kelvin", + "kg": "kilogram", + "km": "kilometer", + "km2": "square-kilometer", + "km^2": "square-kilometer", + "l": "liter", + "lb": "pound", + "m": "meter", + "m2": "square-meter", + "m3": "cubic-meter", + "m^2": "square-meter", + "m^3": "cubic-meter", + "mi": "mile", + "mi2": "square-mile", + "mi^2": "square-mile", + "nmi": "nautical-mile", + "oz": "ounce", + "t": "ton", + "tbsp": "tablespoon", + "tsp": "teaspoon", + "y": "yard", + "y2": "square-yard", + "y3": "cubic-yard", + "y^2": "square-yard", + "y^3": "cubic-yard", } HELP_MESSAGE = ( - 'Converter usage:\n' - '`@convert `\n' - 'Converts `number` in the unit to ' - 'the and prints the result\n' - '`number`: integer or floating point number, e.g. 12, 13.05, 0.002\n' - ' and are two of the following units:\n' - '* square-centimeter (cm^2, cm2), square-decimeter (dm^2, dm2), ' - 'square-meter (m^2, m2), square-kilometer (km^2, km2),' - ' square-inch (in^2, in2), square-foot (ft^2, ft2), square-yard (y^2, y2), ' - ' square-mile(mi^2, mi2), are (a), hectare (ha), acre (ac)\n' - '* bit, byte\n' - '* centimeter (cm), decimeter(dm), meter (m),' - ' kilometer (km), inch (in), foot (ft), yard (y),' - ' mile (mi), nautical-mile (nmi)\n' - '* Kelvin (K), Celsius(C), Fahrenheit (F)\n' - '* cubic-centimeter (cm^3, cm3), cubic-decimeter (dm^3, dm3), liter (l), ' - 'cubic-meter (m^3, m3), cubic-inch (in^3, in3), fluid-ounce (fl-oz), ' - 'cubic-foot (ft^3, ft3), cubic-yard (y^3, y3)\n' - '* gram (g), kilogram (kg), ton (t), ounce (oz), pound(lb)\n' - '* (metric only, U.S. and imperial units differ slightly:) teaspoon (tsp), tablespoon (tbsp), cup\n\n\n' - 'Allowed prefixes are:\n' - '* atto, pico, femto, nano, micro, milli, centi, deci\n' - '* deca, hecto, kilo, mega, giga, tera, peta, exa\n\n\n' - 'Usage examples:\n' - '* `@convert 12 celsius fahrenheit`\n' - '* `@convert 0.002 kilomile millimeter`\n' - '* `@convert 31.5 square-mile ha`\n' - '* `@convert 56 g lb`\n' + "Converter usage:\n" + "`@convert `\n" + "Converts `number` in the unit to " + "the and prints the result\n" + "`number`: integer or floating point number, e.g. 12, 13.05, 0.002\n" + " and are two of the following units:\n" + "* square-centimeter (cm^2, cm2), square-decimeter (dm^2, dm2), " + "square-meter (m^2, m2), square-kilometer (km^2, km2)," + " square-inch (in^2, in2), square-foot (ft^2, ft2), square-yard (y^2, y2), " + " square-mile(mi^2, mi2), are (a), hectare (ha), acre (ac)\n" + "* bit, byte\n" + "* centimeter (cm), decimeter(dm), meter (m)," + " kilometer (km), inch (in), foot (ft), yard (y)," + " mile (mi), nautical-mile (nmi)\n" + "* Kelvin (K), Celsius(C), Fahrenheit (F)\n" + "* cubic-centimeter (cm^3, cm3), cubic-decimeter (dm^3, dm3), liter (l), " + "cubic-meter (m^3, m3), cubic-inch (in^3, in3), fluid-ounce (fl-oz), " + "cubic-foot (ft^3, ft3), cubic-yard (y^3, y3)\n" + "* gram (g), kilogram (kg), ton (t), ounce (oz), pound(lb)\n" + "* (metric only, U.S. and imperial units differ slightly:) teaspoon (tsp), tablespoon (tbsp), cup\n\n\n" + "Allowed prefixes are:\n" + "* atto, pico, femto, nano, micro, milli, centi, deci\n" + "* deca, hecto, kilo, mega, giga, tera, peta, exa\n\n\n" + "Usage examples:\n" + "* `@convert 12 celsius fahrenheit`\n" + "* `@convert 0.002 kilomile millimeter`\n" + "* `@convert 31.5 square-mile ha`\n" + "* `@convert 56 g lb`\n" ) -QUICK_HELP = 'Enter `@convert help` for help on using the converter.' +QUICK_HELP = "Enter `@convert help` for help on using the converter." diff --git a/zulip_bots/zulip_bots/bots/define/define.py b/zulip_bots/zulip_bots/bots/define/define.py index 34fe45d..522747c 100644 --- a/zulip_bots/zulip_bots/bots/define/define.py +++ b/zulip_bots/zulip_bots/bots/define/define.py @@ -10,31 +10,31 @@ from zulip_bots.lib import BotHandler class DefineHandler: - ''' + """ This plugin define a word that the user inputs. It looks for messages starting with '@mention-bot'. - ''' + """ - DEFINITION_API_URL = 'https://owlbot.info/api/v2/dictionary/{}?format=json' - REQUEST_ERROR_MESSAGE = 'Could not load definition.' - EMPTY_WORD_REQUEST_ERROR_MESSAGE = 'Please enter a word to define.' - PHRASE_ERROR_MESSAGE = 'Definitions for phrases are not available.' - SYMBOLS_PRESENT_ERROR_MESSAGE = 'Definitions of words with symbols are not possible.' + DEFINITION_API_URL = "https://owlbot.info/api/v2/dictionary/{}?format=json" + REQUEST_ERROR_MESSAGE = "Could not load definition." + EMPTY_WORD_REQUEST_ERROR_MESSAGE = "Please enter a word to define." + PHRASE_ERROR_MESSAGE = "Definitions for phrases are not available." + SYMBOLS_PRESENT_ERROR_MESSAGE = "Definitions of words with symbols are not possible." def usage(self) -> str: - return ''' + return """ This plugin will allow users to define a word. Users should preface messages with @mention-bot. - ''' + """ def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: - original_content = message['content'].strip() + original_content = message["content"].strip() bot_response = self.get_bot_define_response(original_content) bot_handler.send_reply(message, bot_response) def get_bot_define_response(self, original_content: str) -> str: - split_content = original_content.split(' ') + split_content = original_content.split(" ") # If there are more than one word (a phrase) if len(split_content) > 1: return DefineHandler.PHRASE_ERROR_MESSAGE @@ -51,7 +51,7 @@ class DefineHandler: if not to_define_lower: return self.EMPTY_WORD_REQUEST_ERROR_MESSAGE else: - response = '**{}**:\n'.format(to_define) + response = "**{}**:\n".format(to_define) try: # Use OwlBot API to fetch definition. @@ -65,9 +65,9 @@ class DefineHandler: else: # Definitions available. # Show definitions line by line. for d in definitions: - example = d['example'] if d['example'] else '*No example available.*' - response += '\n' + '* (**{}**) {}\n  {}'.format( - d['type'], d['definition'], html2text.html2text(example) + example = d["example"] if d["example"] else "*No example available.*" + response += "\n" + "* (**{}**) {}\n  {}".format( + d["type"], d["definition"], html2text.html2text(example) ) except Exception: diff --git a/zulip_bots/zulip_bots/bots/define/test_define.py b/zulip_bots/zulip_bots/bots/define/test_define.py index e46261c..62a1deb 100755 --- a/zulip_bots/zulip_bots/bots/define/test_define.py +++ b/zulip_bots/zulip_bots/bots/define/test_define.py @@ -15,8 +15,8 @@ class TestDefineBot(BotTestCase, DefaultTests): "kept as a pet or for catching mice, and many breeds have been " "developed.\n  their pet cat\n\n" ) - with self.mock_http_conversation('test_single_type_word'): - self.verify_reply('cat', bot_response) + with self.mock_http_conversation("test_single_type_word"): + self.verify_reply("cat", bot_response) # Multi-type word. bot_response = ( @@ -32,26 +32,26 @@ class TestDefineBot(BotTestCase, DefaultTests): "* (**exclamation**) used as an appeal for urgent assistance.\n" "  Help! I'm drowning!\n\n" ) - with self.mock_http_conversation('test_multi_type_word'): - self.verify_reply('help', bot_response) + with self.mock_http_conversation("test_multi_type_word"): + self.verify_reply("help", bot_response) # Incorrect word. bot_response = "**foo**:\nCould not load definition." - with self.mock_http_conversation('test_incorrect_word'): - self.verify_reply('foo', bot_response) + with self.mock_http_conversation("test_incorrect_word"): + self.verify_reply("foo", bot_response) # Phrases are not defined. No request is sent to the Internet. bot_response = "Definitions for phrases are not available." - self.verify_reply('The sky is blue', bot_response) + self.verify_reply("The sky is blue", bot_response) # Symbols are considered invalid for words bot_response = "Definitions of words with symbols are not possible." - self.verify_reply('#', bot_response) + self.verify_reply("#", bot_response) # Empty messages are returned with a prompt to reply. No request is sent to the Internet. bot_response = "Please enter a word to define." - self.verify_reply('', bot_response) + self.verify_reply("", bot_response) def test_connection_error(self) -> None: - with patch('requests.get', side_effect=Exception), patch('logging.exception'): - self.verify_reply('aeroplane', '**aeroplane**:\nCould not load definition.') + with patch("requests.get", side_effect=Exception), patch("logging.exception"): + self.verify_reply("aeroplane", "**aeroplane**:\nCould not load definition.") diff --git a/zulip_bots/zulip_bots/bots/dialogflow/dialogflow.py b/zulip_bots/zulip_bots/bots/dialogflow/dialogflow.py index 031db0e..b3f667b 100644 --- a/zulip_bots/zulip_bots/bots/dialogflow/dialogflow.py +++ b/zulip_bots/zulip_bots/bots/dialogflow/dialogflow.py @@ -7,55 +7,55 @@ import apiai from zulip_bots.lib import BotHandler -help_message = '''DialogFlow bot +help_message = """DialogFlow bot This bot will interact with dialogflow bots. Simply send this bot a message, and it will respond depending on the configured bot's behaviour. -''' +""" def get_bot_result(message_content: str, config: Dict[str, str], sender_id: str) -> str: - if message_content.strip() == '' or message_content.strip() == 'help': - return config['bot_info'] - ai = apiai.ApiAI(config['key']) + if message_content.strip() == "" or message_content.strip() == "help": + return config["bot_info"] + ai = apiai.ApiAI(config["key"]) try: request = ai.text_request() request.session_id = sender_id request.query = message_content response = request.getresponse() - res_str = response.read().decode('utf8', 'ignore') + res_str = response.read().decode("utf8", "ignore") res_json = json.loads(res_str) - if res_json['status']['errorType'] != 'success' and 'result' not in res_json.keys(): - return 'Error {}: {}.'.format( - res_json['status']['code'], res_json['status']['errorDetails'] + if res_json["status"]["errorType"] != "success" and "result" not in res_json.keys(): + return "Error {}: {}.".format( + res_json["status"]["code"], res_json["status"]["errorDetails"] ) - if res_json['result']['fulfillment']['speech'] == '': - if 'alternateResult' in res_json.keys(): - if res_json['alternateResult']['fulfillment']['speech'] != '': - return res_json['alternateResult']['fulfillment']['speech'] - return 'Error. No result.' - return res_json['result']['fulfillment']['speech'] + if res_json["result"]["fulfillment"]["speech"] == "": + if "alternateResult" in res_json.keys(): + if res_json["alternateResult"]["fulfillment"]["speech"] != "": + return res_json["alternateResult"]["fulfillment"]["speech"] + return "Error. No result." + return res_json["result"]["fulfillment"]["speech"] except Exception as e: logging.exception(str(e)) - return 'Error. {}.'.format(str(e)) + return "Error. {}.".format(str(e)) class DialogFlowHandler: - ''' + """ This plugin allows users to easily add their own DialogFlow bots to zulip - ''' + """ def initialize(self, bot_handler: BotHandler) -> None: - self.config_info = bot_handler.get_config_info('dialogflow') + self.config_info = bot_handler.get_config_info("dialogflow") def usage(self) -> str: - return ''' + return """ This plugin will allow users to easily add their own DialogFlow bots to zulip - ''' + """ def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: - result = get_bot_result(message['content'], self.config_info, message['sender_id']) + result = get_bot_result(message["content"], self.config_info, message["sender_id"]) bot_handler.send_reply(message, result) diff --git a/zulip_bots/zulip_bots/bots/dialogflow/test_dialogflow.py b/zulip_bots/zulip_bots/bots/dialogflow/test_dialogflow.py index 40ed971..faac3d6 100644 --- a/zulip_bots/zulip_bots/bots/dialogflow/test_dialogflow.py +++ b/zulip_bots/zulip_bots/bots/dialogflow/test_dialogflow.py @@ -28,13 +28,13 @@ class MockTextRequest: def mock_dialogflow(test_name: str, bot_name: str) -> Iterator[None]: response_data = read_bot_fixture_data(bot_name, test_name) try: - response_data['request'] - df_response = response_data['response'] + response_data["request"] + df_response = response_data["response"] except KeyError: print("ERROR: 'request' or 'response' field not found in fixture.") raise - with patch('apiai.ApiAI.text_request') as mock_text_request: + with patch("apiai.ApiAI.text_request") as mock_text_request: request = MockTextRequest() request.response = df_response mock_text_request.return_value = request @@ -42,34 +42,34 @@ def mock_dialogflow(test_name: str, bot_name: str) -> Iterator[None]: class TestDialogFlowBot(BotTestCase, DefaultTests): - bot_name = 'dialogflow' + bot_name = "dialogflow" def _test(self, test_name: str, message: str, response: str) -> None: with self.mock_config_info( - {'key': 'abcdefg', 'bot_info': 'bot info foo bar'} - ), mock_dialogflow(test_name, 'dialogflow'): + {"key": "abcdefg", "bot_info": "bot info foo bar"} + ), mock_dialogflow(test_name, "dialogflow"): self.verify_reply(message, response) def test_normal(self) -> None: - self._test('test_normal', 'hello', 'how are you?') + self._test("test_normal", "hello", "how are you?") def test_403(self) -> None: - self._test('test_403', 'hello', 'Error 403: Access Denied.') + self._test("test_403", "hello", "Error 403: Access Denied.") def test_empty_response(self) -> None: - self._test('test_empty_response', 'hello', 'Error. No result.') + self._test("test_empty_response", "hello", "Error. No result.") def test_exception(self) -> None: - with patch('logging.exception'): - self._test('test_exception', 'hello', 'Error. \'status\'.') + with patch("logging.exception"): + self._test("test_exception", "hello", "Error. 'status'.") def test_help(self) -> None: - self._test('test_normal', 'help', 'bot info foo bar') - self._test('test_normal', '', 'bot info foo bar') + self._test("test_normal", "help", "bot info foo bar") + self._test("test_normal", "", "bot info foo bar") def test_alternate_response(self) -> None: - self._test('test_alternate_result', 'hello', 'alternate result') + self._test("test_alternate_result", "hello", "alternate result") def test_bot_responds_to_empty_message(self) -> None: - with self.mock_config_info({'key': 'abcdefg', 'bot_info': 'bot info foo bar'}): + with self.mock_config_info({"key": "abcdefg", "bot_info": "bot info foo bar"}): pass diff --git a/zulip_bots/zulip_bots/bots/dropbox_share/dropbox_share.py b/zulip_bots/zulip_bots/bots/dropbox_share/dropbox_share.py index b221abd..9fbdceb 100644 --- a/zulip_bots/zulip_bots/bots/dropbox_share/dropbox_share.py +++ b/zulip_bots/zulip_bots/bots/dropbox_share/dropbox_share.py @@ -9,21 +9,21 @@ URL = "[{name}](https://www.dropbox.com/home{path})" class DropboxHandler: - ''' + """ This bot allows you to easily share, search and upload files between zulip and your dropbox account. - ''' + """ def initialize(self, bot_handler: BotHandler) -> None: - self.config_info = bot_handler.get_config_info('dropbox_share') - self.ACCESS_TOKEN = self.config_info.get('access_token') + self.config_info = bot_handler.get_config_info("dropbox_share") + self.ACCESS_TOKEN = self.config_info.get("access_token") self.client = Dropbox(self.ACCESS_TOKEN) def usage(self) -> str: return get_help() def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: - command = message['content'] + command = message["content"] if command == "": command = "help" msg = dbx_command(self.client, command) @@ -31,7 +31,7 @@ class DropboxHandler: def get_help() -> str: - return ''' + return """ Example commands: ``` @@ -44,11 +44,11 @@ def get_help() -> str: @mention-bot search: search a file/folder @mention-bot share: get a shareable link for the file/folder ``` - ''' + """ def get_usage_examples() -> str: - return ''' + return """ Usage: ``` @dropbox ls - Shows files/folders in the root folder. @@ -62,62 +62,62 @@ def get_usage_examples() -> str: @dropbox search boo --mr 10 - Search for boo and get at max 10 results. @dropbox search boo --fd foo - Search for boo in folder foo. ``` - ''' + """ REGEXES = dict( - command='(ls|mkdir|read|rm|write|search|usage|help)', - path=r'(\S+)', - optional_path=r'(\S*)', - some_text='(.+?)', - folder=r'?(?:--fd (\S+))?', - max_results=r'?(?:--mr (\d+))?', + command="(ls|mkdir|read|rm|write|search|usage|help)", + path=r"(\S+)", + optional_path=r"(\S*)", + some_text="(.+?)", + folder=r"?(?:--fd (\S+))?", + max_results=r"?(?:--mr (\d+))?", ) def get_commands() -> Dict[str, Tuple[Any, List[str]]]: return { - 'help': (dbx_help, ['command']), - 'ls': (dbx_ls, ['optional_path']), - 'mkdir': (dbx_mkdir, ['path']), - 'rm': (dbx_rm, ['path']), - 'write': (dbx_write, ['path', 'some_text']), - 'read': (dbx_read, ['path']), - 'search': (dbx_search, ['some_text', 'folder', 'max_results']), - 'share': (dbx_share, ['path']), - 'usage': (dbx_usage, []), + "help": (dbx_help, ["command"]), + "ls": (dbx_ls, ["optional_path"]), + "mkdir": (dbx_mkdir, ["path"]), + "rm": (dbx_rm, ["path"]), + "write": (dbx_write, ["path", "some_text"]), + "read": (dbx_read, ["path"]), + "search": (dbx_search, ["some_text", "folder", "max_results"]), + "share": (dbx_share, ["path"]), + "usage": (dbx_usage, []), } def dbx_command(client: Any, cmd: str) -> str: cmd = cmd.strip() - if cmd == 'help': + if cmd == "help": return get_help() cmd_name = cmd.split()[0] cmd_args = cmd[len(cmd_name) :].strip() commands = get_commands() if cmd_name not in commands: - return 'ERROR: unrecognized command\n' + get_help() + return "ERROR: unrecognized command\n" + get_help() f, arg_names = commands[cmd_name] partial_regexes = [REGEXES[a] for a in arg_names] - regex = ' '.join(partial_regexes) - regex += '$' + regex = " ".join(partial_regexes) + regex += "$" m = re.match(regex, cmd_args) if m: return f(client, *m.groups()) else: - return 'ERROR: ' + syntax_help(cmd_name) + return "ERROR: " + syntax_help(cmd_name) def syntax_help(cmd_name: str) -> str: commands = get_commands() f, arg_names = commands[cmd_name] - arg_syntax = ' '.join('<' + a + '>' for a in arg_names) + arg_syntax = " ".join("<" + a + ">" for a in arg_names) if arg_syntax: - cmd = cmd_name + ' ' + arg_syntax + cmd = cmd_name + " " + arg_syntax else: cmd = cmd_name - return 'syntax: {}'.format(cmd) + return "syntax: {}".format(cmd) def dbx_help(client: Any, cmd_name: str) -> str: @@ -129,7 +129,7 @@ def dbx_usage(client: Any) -> str: def dbx_mkdir(client: Any, fn: str) -> str: - fn = '/' + fn # foo/boo -> /foo/boo + fn = "/" + fn # foo/boo -> /foo/boo try: result = client.files_create_folder(fn) msg = "CREATED FOLDER: " + URL.format(name=result.name, path=result.path_lower) @@ -143,8 +143,8 @@ def dbx_mkdir(client: Any, fn: str) -> str: def dbx_ls(client: Any, fn: str) -> str: - if fn != '': - fn = '/' + fn + if fn != "": + fn = "/" + fn try: result = client.files_list_folder(fn) @@ -152,9 +152,9 @@ def dbx_ls(client: Any, fn: str) -> str: for meta in result.entries: files_list += [" - " + URL.format(name=meta.name, path=meta.path_lower)] - msg = '\n'.join(files_list) - if msg == '': - msg = '`No files available`' + msg = "\n".join(files_list) + if msg == "": + msg = "`No files available`" except Exception: msg = ( @@ -167,7 +167,7 @@ def dbx_ls(client: Any, fn: str) -> str: def dbx_rm(client: Any, fn: str) -> str: - fn = '/' + fn + fn = "/" + fn try: result = client.files_delete(fn) @@ -181,7 +181,7 @@ def dbx_rm(client: Any, fn: str) -> str: def dbx_write(client: Any, fn: str, content: str) -> str: - fn = '/' + fn + fn = "/" + fn try: result = client.files_upload(content.encode(), fn) @@ -193,7 +193,7 @@ def dbx_write(client: Any, fn: str, content: str) -> str: def dbx_read(client: Any, fn: str) -> str: - fn = '/' + fn + fn = "/" + fn try: result = client.files_download(fn) @@ -208,11 +208,11 @@ def dbx_read(client: Any, fn: str) -> str: def dbx_search(client: Any, query: str, folder: str, max_results: str) -> str: if folder is None: - folder = '' + folder = "" else: - folder = '/' + folder + folder = "/" + folder if max_results is None: - max_results = '20' + max_results = "20" try: result = client.files_search(folder, query, max_results=int(max_results)) msg_list = [] @@ -221,7 +221,7 @@ def dbx_search(client: Any, query: str, folder: str, max_results: str) -> str: file_info = entry.metadata count += 1 msg_list += [" - " + URL.format(name=file_info.name, path=file_info.path_lower)] - msg = '\n'.join(msg_list) + msg = "\n".join(msg_list) except Exception: msg = ( @@ -230,7 +230,7 @@ def dbx_search(client: Any, query: str, folder: str, max_results: str) -> str: " `--fd ` to search in specific folder." ) - if msg == '': + if msg == "": msg = ( "No files/folders found matching your query.\n" "For file name searching, the last token is used for prefix matching" @@ -241,7 +241,7 @@ def dbx_search(client: Any, query: str, folder: str, max_results: str) -> str: def dbx_share(client: Any, fn: str): - fn = '/' + fn + fn = "/" + fn try: result = client.sharing_create_shared_link(fn) msg = result.url diff --git a/zulip_bots/zulip_bots/bots/dropbox_share/test_dropbox_share.py b/zulip_bots/zulip_bots/bots/dropbox_share/test_dropbox_share.py index ba93e28..d1fead3 100644 --- a/zulip_bots/zulip_bots/bots/dropbox_share/test_dropbox_share.py +++ b/zulip_bots/zulip_bots/bots/dropbox_share/test_dropbox_share.py @@ -13,15 +13,15 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests def get_root_files_list(*args, **kwargs): return MockListFolderResult( - entries=[MockFileMetadata('foo', '/foo'), MockFileMetadata('boo', '/boo')], has_more=False + entries=[MockFileMetadata("foo", "/foo"), MockFileMetadata("boo", "/boo")], has_more=False ) def get_folder_files_list(*args, **kwargs): return MockListFolderResult( entries=[ - MockFileMetadata('moo', '/foo/moo'), - MockFileMetadata('noo', '/foo/noo'), + MockFileMetadata("moo", "/foo/moo"), + MockFileMetadata("noo", "/foo/noo"), ], has_more=False, ) @@ -32,18 +32,18 @@ def get_empty_files_list(*args, **kwargs): def create_file(*args, **kwargs): - return MockFileMetadata('foo', '/foo') + return MockFileMetadata("foo", "/foo") def download_file(*args, **kwargs): - return [MockFileMetadata('foo', '/foo'), MockHttpResponse('boo')] + return [MockFileMetadata("foo", "/foo"), MockHttpResponse("boo")] def search_files(*args, **kwargs): return MockSearchResult( [ - MockSearchMatch(MockFileMetadata('foo', '/foo')), - MockSearchMatch(MockFileMetadata('fooboo', '/fooboo')), + MockSearchMatch(MockFileMetadata("foo", "/foo")), + MockSearchMatch(MockFileMetadata("fooboo", "/fooboo")), ] ) @@ -53,11 +53,11 @@ def get_empty_search_result(*args, **kwargs): def get_shared_link(*args, **kwargs): - return MockPathLinkMetadata('http://www.foo.com/boo') + return MockPathLinkMetadata("http://www.foo.com/boo") def get_help() -> str: - return ''' + return """ Example commands: ``` @@ -70,7 +70,7 @@ def get_help() -> str: @mention-bot search: search a file/folder @mention-bot share: get a shareable link for the file/folder ``` - ''' + """ class TestDropboxBot(BotTestCase, DefaultTests): @@ -79,8 +79,8 @@ class TestDropboxBot(BotTestCase, DefaultTests): def test_bot_responds_to_empty_message(self): with self.mock_config_info(self.config_info): - self.verify_reply('', get_help()) - self.verify_reply('help', get_help()) + self.verify_reply("", get_help()) + self.verify_reply("help", get_help()) def test_dbx_ls_root(self): bot_response = ( @@ -88,7 +88,7 @@ class TestDropboxBot(BotTestCase, DefaultTests): " - [boo](https://www.dropbox.com/home/boo)" ) with patch( - 'dropbox.Dropbox.files_list_folder', side_effect=get_root_files_list + "dropbox.Dropbox.files_list_folder", side_effect=get_root_files_list ), self.mock_config_info(self.config_info): self.verify_reply("ls", bot_response) @@ -98,14 +98,14 @@ class TestDropboxBot(BotTestCase, DefaultTests): " - [noo](https://www.dropbox.com/home/foo/noo)" ) with patch( - 'dropbox.Dropbox.files_list_folder', side_effect=get_folder_files_list + "dropbox.Dropbox.files_list_folder", side_effect=get_folder_files_list ), self.mock_config_info(self.config_info): self.verify_reply("ls foo", bot_response) def test_dbx_ls_empty(self): - bot_response = '`No files available`' + bot_response = "`No files available`" with patch( - 'dropbox.Dropbox.files_list_folder', side_effect=get_empty_files_list + "dropbox.Dropbox.files_list_folder", side_effect=get_empty_files_list ), self.mock_config_info(self.config_info): self.verify_reply("ls", bot_response) @@ -116,16 +116,16 @@ class TestDropboxBot(BotTestCase, DefaultTests): "or simply `ls` for listing folders in the root directory" ) with patch( - 'dropbox.Dropbox.files_list_folder', side_effect=Exception() + "dropbox.Dropbox.files_list_folder", side_effect=Exception() ), self.mock_config_info(self.config_info): self.verify_reply("ls", bot_response) def test_dbx_mkdir(self): bot_response = "CREATED FOLDER: [foo](https://www.dropbox.com/home/foo)" with patch( - 'dropbox.Dropbox.files_create_folder', side_effect=create_file + "dropbox.Dropbox.files_create_folder", side_effect=create_file ), self.mock_config_info(self.config_info): - self.verify_reply('mkdir foo', bot_response) + self.verify_reply("mkdir foo", bot_response) def test_dbx_mkdir_error(self): bot_response = ( @@ -133,49 +133,49 @@ class TestDropboxBot(BotTestCase, DefaultTests): "Usage: `mkdir ` to create a folder." ) with patch( - 'dropbox.Dropbox.files_create_folder', side_effect=Exception() + "dropbox.Dropbox.files_create_folder", side_effect=Exception() ), self.mock_config_info(self.config_info): - self.verify_reply('mkdir foo/bar', bot_response) + self.verify_reply("mkdir foo/bar", bot_response) def test_dbx_rm(self): bot_response = "DELETED File/Folder : [foo](https://www.dropbox.com/home/foo)" - with patch('dropbox.Dropbox.files_delete', side_effect=create_file), self.mock_config_info( + with patch("dropbox.Dropbox.files_delete", side_effect=create_file), self.mock_config_info( self.config_info ): - self.verify_reply('rm foo', bot_response) + self.verify_reply("rm foo", bot_response) def test_dbx_rm_error(self): bot_response = ( "Please provide a correct folder path and name.\n" "Usage: `rm ` to delete a folder in root directory." ) - with patch('dropbox.Dropbox.files_delete', side_effect=Exception()), self.mock_config_info( + with patch("dropbox.Dropbox.files_delete", side_effect=Exception()), self.mock_config_info( self.config_info ): - self.verify_reply('rm foo', bot_response) + self.verify_reply("rm foo", bot_response) def test_dbx_write(self): bot_response = "Written to file: [foo](https://www.dropbox.com/home/foo)" - with patch('dropbox.Dropbox.files_upload', side_effect=create_file), self.mock_config_info( + with patch("dropbox.Dropbox.files_upload", side_effect=create_file), self.mock_config_info( self.config_info ): - self.verify_reply('write foo boo', bot_response) + self.verify_reply("write foo boo", bot_response) def test_dbx_write_error(self): bot_response = ( "Incorrect file path or file already exists.\nUsage: `write CONTENT`" ) - with patch('dropbox.Dropbox.files_upload', side_effect=Exception()), self.mock_config_info( + with patch("dropbox.Dropbox.files_upload", side_effect=Exception()), self.mock_config_info( self.config_info ): - self.verify_reply('write foo boo', bot_response) + self.verify_reply("write foo boo", bot_response) def test_dbx_read(self): bot_response = "**foo** :\nboo" with patch( - 'dropbox.Dropbox.files_download', side_effect=download_file + "dropbox.Dropbox.files_download", side_effect=download_file ), self.mock_config_info(self.config_info): - self.verify_reply('read foo', bot_response) + self.verify_reply("read foo", bot_response) def test_dbx_read_error(self): bot_response = ( @@ -183,16 +183,16 @@ class TestDropboxBot(BotTestCase, DefaultTests): "Usage: `read ` to read content of a file" ) with patch( - 'dropbox.Dropbox.files_download', side_effect=Exception() + "dropbox.Dropbox.files_download", side_effect=Exception() ), self.mock_config_info(self.config_info): - self.verify_reply('read foo', bot_response) + self.verify_reply("read foo", bot_response) def test_dbx_search(self): bot_response = " - [foo](https://www.dropbox.com/home/foo)\n - [fooboo](https://www.dropbox.com/home/fooboo)" - with patch('dropbox.Dropbox.files_search', side_effect=search_files), self.mock_config_info( + with patch("dropbox.Dropbox.files_search", side_effect=search_files), self.mock_config_info( self.config_info ): - self.verify_reply('search foo', bot_response) + self.verify_reply("search foo", bot_response) def test_dbx_search_empty(self): bot_response = ( @@ -201,9 +201,9 @@ class TestDropboxBot(BotTestCase, DefaultTests): " (i.e. “bat c” matches “bat cave” but not “batman car”)." ) with patch( - 'dropbox.Dropbox.files_search', side_effect=get_empty_search_result + "dropbox.Dropbox.files_search", side_effect=get_empty_search_result ), self.mock_config_info(self.config_info): - self.verify_reply('search boo --fd foo', bot_response) + self.verify_reply("search boo --fd foo", bot_response) def test_dbx_search_error(self): bot_response = ( @@ -211,32 +211,32 @@ class TestDropboxBot(BotTestCase, DefaultTests): "Note:`--mr ` is optional and is used to specify maximun results.\n" " `--fd ` to search in specific folder." ) - with patch('dropbox.Dropbox.files_search', side_effect=Exception()), self.mock_config_info( + with patch("dropbox.Dropbox.files_search", side_effect=Exception()), self.mock_config_info( self.config_info ): - self.verify_reply('search foo', bot_response) + self.verify_reply("search foo", bot_response) def test_dbx_share(self): - bot_response = 'http://www.foo.com/boo' + bot_response = "http://www.foo.com/boo" with patch( - 'dropbox.Dropbox.sharing_create_shared_link', side_effect=get_shared_link + "dropbox.Dropbox.sharing_create_shared_link", side_effect=get_shared_link ), self.mock_config_info(self.config_info): - self.verify_reply('share boo', bot_response) + self.verify_reply("share boo", bot_response) def test_dbx_share_error(self): bot_response = "Please provide a correct file name.\nUsage: `share `" with patch( - 'dropbox.Dropbox.sharing_create_shared_link', side_effect=Exception() + "dropbox.Dropbox.sharing_create_shared_link", side_effect=Exception() ), self.mock_config_info(self.config_info): - self.verify_reply('share boo', bot_response) + self.verify_reply("share boo", bot_response) def test_dbx_help(self): - bot_response = 'syntax: ls ' + bot_response = "syntax: ls " with self.mock_config_info(self.config_info): - self.verify_reply('help ls', bot_response) + self.verify_reply("help ls", bot_response) def test_dbx_usage(self): - bot_response = ''' + bot_response = """ Usage: ``` @dropbox ls - Shows files/folders in the root folder. @@ -250,9 +250,9 @@ class TestDropboxBot(BotTestCase, DefaultTests): @dropbox search boo --mr 10 - Search for boo and get at max 10 results. @dropbox search boo --fd foo - Search for boo in folder foo. ``` - ''' + """ with self.mock_config_info(self.config_info): - self.verify_reply('usage', bot_response) + self.verify_reply("usage", bot_response) def test_invalid_commands(self): ls_error_response = "ERROR: syntax: ls " @@ -277,7 +277,7 @@ class TestDropboxBot(BotTestCase, DefaultTests): self.verify_reply("usage foo", usage_error_response) def test_unkown_command(self): - bot_response = '''ERROR: unrecognized command + bot_response = """ERROR: unrecognized command Example commands: @@ -291,6 +291,6 @@ class TestDropboxBot(BotTestCase, DefaultTests): @mention-bot search: search a file/folder @mention-bot share: get a shareable link for the file/folder ``` - ''' + """ with self.mock_config_info(self.config_info): - self.verify_reply('unknown command', bot_response) + self.verify_reply("unknown command", bot_response) diff --git a/zulip_bots/zulip_bots/bots/encrypt/encrypt.py b/zulip_bots/zulip_bots/bots/encrypt/encrypt.py index 69fa479..9d08a04 100755 --- a/zulip_bots/zulip_bots/bots/encrypt/encrypt.py +++ b/zulip_bots/zulip_bots/bots/encrypt/encrypt.py @@ -7,9 +7,9 @@ def encrypt(text: str) -> str: # This is where the actual ROT13 is applied # WHY IS .JOIN NOT WORKING?! textlist = list(text) - newtext = '' - firsthalf = 'abcdefghijklmABCDEFGHIJKLM' - lasthalf = 'nopqrstuvwxyzNOPQRSTUVWXYZ' + newtext = "" + firsthalf = "abcdefghijklmABCDEFGHIJKLM" + lasthalf = "nopqrstuvwxyzNOPQRSTUVWXYZ" for char in textlist: if char in firsthalf: newtext += lasthalf[firsthalf.index(char)] @@ -22,24 +22,24 @@ def encrypt(text: str) -> str: class EncryptHandler: - ''' + """ This bot allows users to quickly encrypt messages using ROT13 encryption. It encrypts/decrypts messages starting with @mention-bot. - ''' + """ def usage(self) -> str: - return ''' + return """ This bot uses ROT13 encryption for its purposes. It responds to me starting with @mention-bot. Feeding encrypted messages into the bot decrypts them. - ''' + """ def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: bot_response = self.get_bot_encrypt_response(message) bot_handler.send_reply(message, bot_response) def get_bot_encrypt_response(self, message: Dict[str, str]) -> str: - original_content = message['content'] + original_content = message["content"] temp_content = encrypt(original_content) send_content = "Encrypted/Decrypted text: " + temp_content return send_content diff --git a/zulip_bots/zulip_bots/bots/encrypt/test_encrypt.py b/zulip_bots/zulip_bots/bots/encrypt/test_encrypt.py index 1be3ab2..93a2c10 100755 --- a/zulip_bots/zulip_bots/bots/encrypt/test_encrypt.py +++ b/zulip_bots/zulip_bots/bots/encrypt/test_encrypt.py @@ -7,7 +7,7 @@ class TestEncryptBot(BotTestCase, DefaultTests): def test_bot(self) -> None: dialog = [ ("", "Encrypted/Decrypted text: "), - ("Let\'s Do It", "Encrypted/Decrypted text: Yrg\'f Qb Vg"), + ("Let's Do It", "Encrypted/Decrypted text: Yrg'f Qb Vg"), ("me&mom together..!!", "Encrypted/Decrypted text: zr&zbz gbtrgure..!!"), ("foo bar", "Encrypted/Decrypted text: sbb one"), ("Please encrypt this", "Encrypted/Decrypted text: Cyrnfr rapelcg guvf"), diff --git a/zulip_bots/zulip_bots/bots/file_uploader/file_uploader.py b/zulip_bots/zulip_bots/bots/file_uploader/file_uploader.py index 758baad..ccac2ac 100644 --- a/zulip_bots/zulip_bots/bots/file_uploader/file_uploader.py +++ b/zulip_bots/zulip_bots/bots/file_uploader/file_uploader.py @@ -8,36 +8,36 @@ from zulip_bots.lib import BotHandler class FileUploaderHandler: def usage(self) -> str: return ( - 'This interactive bot is used to upload files (such as images) to the Zulip server:' - '\n- @uploader : Upload a file, where is the path to the file' - '\n- @uploader help : Display help message' + "This interactive bot is used to upload files (such as images) to the Zulip server:" + "\n- @uploader : Upload a file, where is the path to the file" + "\n- @uploader help : Display help message" ) def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: HELP_STR = ( - 'Use this bot with any of the following commands:' - '\n* `@uploader ` : Upload a file, where `` is the path to the file' - '\n* `@uploader help` : Display help message' + "Use this bot with any of the following commands:" + "\n* `@uploader ` : Upload a file, where `` is the path to the file" + "\n* `@uploader help` : Display help message" ) - content = message['content'].strip() - if content == 'help': + content = message["content"].strip() + if content == "help": bot_handler.send_reply(message, HELP_STR) return path = Path(os.path.expanduser(content)) if not path.is_file(): - bot_handler.send_reply(message, 'File `{}` not found'.format(content)) + bot_handler.send_reply(message, "File `{}` not found".format(content)) return path = path.resolve() upload = bot_handler.upload_file_from_path(str(path)) - if upload['result'] != 'success': - msg = upload['msg'] - bot_handler.send_reply(message, 'Failed to upload `{}` file: {}'.format(path, msg)) + if upload["result"] != "success": + msg = upload["msg"] + bot_handler.send_reply(message, "Failed to upload `{}` file: {}".format(path, msg)) return - uploaded_file_reply = '[{}]({})'.format(path.name, upload['uri']) + uploaded_file_reply = "[{}]({})".format(path.name, upload["uri"]) bot_handler.send_reply(message, uploaded_file_reply) diff --git a/zulip_bots/zulip_bots/bots/file_uploader/test_file_uploader.py b/zulip_bots/zulip_bots/bots/file_uploader/test_file_uploader.py index 54889a2..6d84fec 100644 --- a/zulip_bots/zulip_bots/bots/file_uploader/test_file_uploader.py +++ b/zulip_bots/zulip_bots/bots/file_uploader/test_file_uploader.py @@ -7,36 +7,36 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests class TestFileUploaderBot(BotTestCase, DefaultTests): bot_name = "file_uploader" - @patch('pathlib.Path.is_file', return_value=False) + @patch("pathlib.Path.is_file", return_value=False) def test_file_not_found(self, is_file: Mock) -> None: - self.verify_reply('file.txt', 'File `file.txt` not found') + self.verify_reply("file.txt", "File `file.txt` not found") - @patch('pathlib.Path.resolve', return_value=Path('/file.txt')) - @patch('pathlib.Path.is_file', return_value=True) + @patch("pathlib.Path.resolve", return_value=Path("/file.txt")) + @patch("pathlib.Path.is_file", return_value=True) def test_file_upload_failed(self, is_file: Mock, resolve: Mock) -> None: - server_reply = dict(result='', msg='error') + server_reply = dict(result="", msg="error") with patch( - 'zulip_bots.test_lib.StubBotHandler.upload_file_from_path', return_value=server_reply + "zulip_bots.test_lib.StubBotHandler.upload_file_from_path", return_value=server_reply ): self.verify_reply( - 'file.txt', 'Failed to upload `{}` file: error'.format(Path('file.txt').resolve()) + "file.txt", "Failed to upload `{}` file: error".format(Path("file.txt").resolve()) ) - @patch('pathlib.Path.resolve', return_value=Path('/file.txt')) - @patch('pathlib.Path.is_file', return_value=True) + @patch("pathlib.Path.resolve", return_value=Path("/file.txt")) + @patch("pathlib.Path.is_file", return_value=True) def test_file_upload_success(self, is_file: Mock, resolve: Mock) -> None: - server_reply = dict(result='success', uri='https://file/uri') + server_reply = dict(result="success", uri="https://file/uri") with patch( - 'zulip_bots.test_lib.StubBotHandler.upload_file_from_path', return_value=server_reply + "zulip_bots.test_lib.StubBotHandler.upload_file_from_path", return_value=server_reply ): - self.verify_reply('file.txt', '[file.txt](https://file/uri)') + self.verify_reply("file.txt", "[file.txt](https://file/uri)") def test_help(self): self.verify_reply( - 'help', + "help", ( - 'Use this bot with any of the following commands:' - '\n* `@uploader ` : Upload a file, where `` is the path to the file' - '\n* `@uploader help` : Display help message' + "Use this bot with any of the following commands:" + "\n* `@uploader ` : Upload a file, where `` is the path to the file" + "\n* `@uploader help` : Display help message" ), ) diff --git a/zulip_bots/zulip_bots/bots/flock/flock.py b/zulip_bots/zulip_bots/bots/flock/flock.py index c4748d0..69485e0 100644 --- a/zulip_bots/zulip_bots/bots/flock/flock.py +++ b/zulip_bots/zulip_bots/bots/flock/flock.py @@ -6,20 +6,20 @@ from requests.exceptions import ConnectionError from zulip_bots.lib import BotHandler -USERS_LIST_URL = 'https://api.flock.co/v1/roster.listContacts' -SEND_MESSAGE_URL = 'https://api.flock.co/v1/chat.sendMessage' +USERS_LIST_URL = "https://api.flock.co/v1/roster.listContacts" +SEND_MESSAGE_URL = "https://api.flock.co/v1/chat.sendMessage" -help_message = ''' +help_message = """ You can send messages to any Flock user associated with your account from Zulip. *Syntax*: **@botname to: message** where `to` is **firstName** of recipient. -''' +""" # Matches the recipient name provided by user with list of users in his contacts. # If matches, returns the matched User's ID def find_recipient_id(users: List[Any], recipient_name: str) -> str: for user in users: - if recipient_name == user['firstName']: - return user['id'] + if recipient_name == user["firstName"]: + return user["id"] # Make request to given flock URL and return a two-element tuple @@ -42,8 +42,8 @@ right now.\nPlease try again later" def get_recipient_id( recipient_name: str, config: Dict[str, str] ) -> Tuple[Optional[str], Optional[str]]: - token = config['token'] - payload = {'token': token} + token = config["token"] + payload = {"token": token} users, error = make_flock_request(USERS_LIST_URL, payload) if users is None: return (None, error) @@ -58,8 +58,8 @@ def get_recipient_id( # This handles the message sending work. def get_flock_response(content: str, config: Dict[str, str]) -> str: - token = config['token'] - content_pieces = content.split(':') + token = config["token"] + content_pieces = content.split(":") recipient_name = content_pieces[0].strip() message = content_pieces[1].strip() @@ -70,7 +70,7 @@ def get_flock_response(content: str, config: Dict[str, str]) -> str: if len(str(recipient_id)) > 30: return "Found user is invalid." - payload = {'to': recipient_id, 'text': message, 'token': token} + payload = {"to": recipient_id, "text": message, "token": token} res, error = make_flock_request(SEND_MESSAGE_URL, payload) if res is None: return error @@ -83,7 +83,7 @@ def get_flock_response(content: str, config: Dict[str, str]) -> str: def get_flock_bot_response(content: str, config: Dict[str, str]) -> None: content = content.strip() - if content == '' or content == 'help': + if content == "" or content == "help": return help_message else: result = get_flock_response(content, config) @@ -91,20 +91,20 @@ def get_flock_bot_response(content: str, config: Dict[str, str]) -> None: class FlockHandler: - ''' + """ This is flock bot. Now you can send messages to any of your flock user without having to leave Zulip. - ''' + """ def initialize(self, bot_handler: BotHandler) -> None: - self.config_info = bot_handler.get_config_info('flock') + self.config_info = bot_handler.get_config_info("flock") def usage(self) -> str: - return '''Hello from Flock Bot. You can send messages to any Flock user -right from Zulip.''' + return """Hello from Flock Bot. You can send messages to any Flock user +right from Zulip.""" def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: - response = get_flock_bot_response(message['content'], self.config_info) + response = get_flock_bot_response(message["content"], self.config_info) bot_handler.send_reply(message, response) diff --git a/zulip_bots/zulip_bots/bots/flock/test_flock.py b/zulip_bots/zulip_bots/bots/flock/test_flock.py index 5dfca8d..9efb1c8 100644 --- a/zulip_bots/zulip_bots/bots/flock/test_flock.py +++ b/zulip_bots/zulip_bots/bots/flock/test_flock.py @@ -11,74 +11,74 @@ class TestFlockBot(BotTestCase, DefaultTests): message_config = {"token": "12345", "text": "Ricky: test message", "to": "u:somekey"} - help_message = ''' + help_message = """ You can send messages to any Flock user associated with your account from Zulip. *Syntax*: **@botname to: message** where `to` is **firstName** of recipient. -''' +""" def test_bot_responds_to_empty_message(self) -> None: - self.verify_reply('', self.help_message) + self.verify_reply("", self.help_message) def test_help_message(self) -> None: - self.verify_reply('', self.help_message) + self.verify_reply("", self.help_message) def test_fetch_id_connection_error(self) -> None: with self.mock_config_info(self.normal_config), patch( - 'requests.get', side_effect=ConnectionError() - ), patch('logging.exception'): + "requests.get", side_effect=ConnectionError() + ), patch("logging.exception"): self.verify_reply( - 'tyler: Hey tyler', - "Uh-Oh, couldn\'t process the request \ + "tyler: Hey tyler", + "Uh-Oh, couldn't process the request \ right now.\nPlease try again later", ) def test_response_connection_error(self) -> None: with self.mock_config_info(self.message_config), patch( - 'requests.get', side_effect=ConnectionError() - ), patch('logging.exception'): + "requests.get", side_effect=ConnectionError() + ), patch("logging.exception"): self.verify_reply( - 'Ricky: test message', - "Uh-Oh, couldn\'t process the request \ + "Ricky: test message", + "Uh-Oh, couldn't process the request \ right now.\nPlease try again later", ) def test_no_recipient_found(self) -> None: bot_response = "No user found. Make sure you typed it correctly." with self.mock_config_info(self.normal_config), self.mock_http_conversation( - 'test_no_recipient_found' + "test_no_recipient_found" ): - self.verify_reply('david: hello', bot_response) + self.verify_reply("david: hello", bot_response) def test_found_invalid_recipient(self) -> None: bot_response = "Found user is invalid." with self.mock_config_info(self.normal_config), self.mock_http_conversation( - 'test_found_invalid_recipient' + "test_found_invalid_recipient" ): - self.verify_reply('david: hello', bot_response) + self.verify_reply("david: hello", bot_response) - @patch('zulip_bots.bots.flock.flock.get_recipient_id') + @patch("zulip_bots.bots.flock.flock.get_recipient_id") def test_message_send_connection_error(self, get_recipient_id: str) -> None: bot_response = "Uh-Oh, couldn't process the request right now.\nPlease try again later" get_recipient_id.return_value = ["u:userid", None] with self.mock_config_info(self.normal_config), patch( - 'requests.get', side_effect=ConnectionError() - ), patch('logging.exception'): - self.verify_reply('Rishabh: hi there', bot_response) + "requests.get", side_effect=ConnectionError() + ), patch("logging.exception"): + self.verify_reply("Rishabh: hi there", bot_response) - @patch('zulip_bots.bots.flock.flock.get_recipient_id') + @patch("zulip_bots.bots.flock.flock.get_recipient_id") def test_message_send_success(self, get_recipient_id: str) -> None: bot_response = "Message sent." get_recipient_id.return_value = ["u:userid", None] with self.mock_config_info(self.normal_config), self.mock_http_conversation( - 'test_message_send_success' + "test_message_send_success" ): - self.verify_reply('Rishabh: hi there', bot_response) + self.verify_reply("Rishabh: hi there", bot_response) - @patch('zulip_bots.bots.flock.flock.get_recipient_id') + @patch("zulip_bots.bots.flock.flock.get_recipient_id") def test_message_send_failed(self, get_recipient_id: str) -> None: bot_response = "Message sending failed :slightly_frowning_face:. Please try again." get_recipient_id.return_value = ["u:invalid", None] with self.mock_config_info(self.normal_config), self.mock_http_conversation( - 'test_message_send_failed' + "test_message_send_failed" ): - self.verify_reply('Rishabh: hi there', bot_response) + self.verify_reply("Rishabh: hi there", bot_response) diff --git a/zulip_bots/zulip_bots/bots/followup/followup.py b/zulip_bots/zulip_bots/bots/followup/followup.py index 3abe73b..8f49256 100644 --- a/zulip_bots/zulip_bots/bots/followup/followup.py +++ b/zulip_bots/zulip_bots/bots/followup/followup.py @@ -5,7 +5,7 @@ from zulip_bots.lib import BotHandler class FollowupHandler: - ''' + """ This plugin facilitates creating follow-up tasks when you are using Zulip to conduct a virtual meeting. It looks for messages starting with '@mention-bot'. @@ -14,45 +14,45 @@ class FollowupHandler: Zulip stream called "followup," but this code could be adapted to write follow up items to some kind of external issue tracker as well. - ''' + """ def usage(self) -> str: - return ''' + return """ This plugin will allow users to flag messages as being follow-up items. Users should preface messages with "@mention-bot". Before running this, make sure to create a stream called "followup" that your API user can send to. - ''' + """ def initialize(self, bot_handler: BotHandler) -> None: - self.config_info = bot_handler.get_config_info('followup', optional=False) - self.stream = self.config_info.get("stream", 'followup') + self.config_info = bot_handler.get_config_info("followup", optional=False) + self.stream = self.config_info.get("stream", "followup") def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: - if message['content'] == '': + if message["content"] == "": bot_response = ( "Please specify the message you want to send to followup stream after @mention-bot" ) bot_handler.send_reply(message, bot_response) - elif message['content'] == 'help': + elif message["content"] == "help": bot_handler.send_reply(message, self.usage()) else: bot_response = self.get_bot_followup_response(message) bot_handler.send_message( dict( - type='stream', + type="stream", to=self.stream, - subject=message['sender_email'], + subject=message["sender_email"], content=bot_response, ) ) def get_bot_followup_response(self, message: Dict[str, str]) -> str: - original_content = message['content'] - original_sender = message['sender_email'] - temp_content = 'from %s: ' % (original_sender,) + original_content = message["content"] + original_sender = message["sender_email"] + temp_content = "from %s: " % (original_sender,) new_content = temp_content + original_content return new_content diff --git a/zulip_bots/zulip_bots/bots/followup/test_followup.py b/zulip_bots/zulip_bots/bots/followup/test_followup.py index 6548e97..41d29ce 100755 --- a/zulip_bots/zulip_bots/bots/followup/test_followup.py +++ b/zulip_bots/zulip_bots/bots/followup/test_followup.py @@ -6,48 +6,48 @@ class TestFollowUpBot(BotTestCase, DefaultTests): def test_followup_stream(self) -> None: message = dict( - content='feed the cat', - type='stream', - sender_email='foo@example.com', + content="feed the cat", + type="stream", + sender_email="foo@example.com", ) - with self.mock_config_info({'stream': 'followup'}): + with self.mock_config_info({"stream": "followup"}): response = self.get_response(message) - self.assertEqual(response['content'], 'from foo@example.com: feed the cat') - self.assertEqual(response['to'], 'followup') + self.assertEqual(response["content"], "from foo@example.com: feed the cat") + self.assertEqual(response["to"], "followup") def test_different_stream(self) -> None: message = dict( - content='feed the cat', - type='stream', - sender_email='foo@example.com', + content="feed the cat", + type="stream", + sender_email="foo@example.com", ) - with self.mock_config_info({'stream': 'issue'}): + with self.mock_config_info({"stream": "issue"}): response = self.get_response(message) - self.assertEqual(response['content'], 'from foo@example.com: feed the cat') - self.assertEqual(response['to'], 'issue') + self.assertEqual(response["content"], "from foo@example.com: feed the cat") + self.assertEqual(response["to"], "issue") def test_bot_responds_to_empty_message(self) -> None: bot_response = ( - 'Please specify the message you want to send to followup stream after @mention-bot' + "Please specify the message you want to send to followup stream after @mention-bot" ) - with self.mock_config_info({'stream': 'followup'}): - self.verify_reply('', bot_response) + with self.mock_config_info({"stream": "followup"}): + self.verify_reply("", bot_response) def test_help_text(self) -> None: - request = 'help' - bot_response = ''' + request = "help" + bot_response = """ This plugin will allow users to flag messages as being follow-up items. Users should preface messages with "@mention-bot". Before running this, make sure to create a stream called "followup" that your API user can send to. - ''' + """ - with self.mock_config_info({'stream': 'followup'}): + with self.mock_config_info({"stream": "followup"}): self.verify_reply(request, bot_response) diff --git a/zulip_bots/zulip_bots/bots/front/front.py b/zulip_bots/zulip_bots/bots/front/front.py index 8458ffa..d17f9e7 100644 --- a/zulip_bots/zulip_bots/bots/front/front.py +++ b/zulip_bots/zulip_bots/bots/front/front.py @@ -9,24 +9,24 @@ from zulip_bots.lib import BotHandler class FrontHandler: FRONT_API = "https://api2.frontapp.com/conversations/{}" COMMANDS = [ - ('archive', "Archive a conversation."), - ('delete', "Delete a conversation."), - ('spam', "Mark a conversation as spam."), - ('open', "Restore a conversation."), - ('comment ', "Leave a comment."), + ("archive", "Archive a conversation."), + ("delete", "Delete a conversation."), + ("spam", "Mark a conversation as spam."), + ("open", "Restore a conversation."), + ("comment ", "Leave a comment."), ] - CNV_ID_REGEXP = 'cnv_(?P[0-9a-z]+)' + CNV_ID_REGEXP = "cnv_(?P[0-9a-z]+)" COMMENT_PREFIX = "comment " def usage(self) -> str: - return ''' + return """ Front Bot uses the Front REST API to interact with Front. In order to use Front Bot, `front.conf` must be set up. See `doc.md` for more details. - ''' + """ def initialize(self, bot_handler: BotHandler) -> None: - config = bot_handler.get_config_info('front') - api_key = config.get('api_key') + config = bot_handler.get_config_info("front") + api_key = config.get("api_key") if not api_key: raise KeyError("No API key specified.") @@ -100,9 +100,9 @@ class FrontHandler: return "Comment was sent." def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: - command = message['content'] + command = message["content"] - result = re.search(self.CNV_ID_REGEXP, message['subject']) + result = re.search(self.CNV_ID_REGEXP, message["subject"]) if not result: bot_handler.send_reply( message, @@ -114,25 +114,25 @@ class FrontHandler: self.conversation_id = result.group() - if command == 'help': + if command == "help": bot_handler.send_reply(message, self.help(bot_handler)) - elif command == 'archive': + elif command == "archive": bot_handler.send_reply(message, self.archive(bot_handler)) - elif command == 'delete': + elif command == "delete": bot_handler.send_reply(message, self.delete(bot_handler)) - elif command == 'spam': + elif command == "spam": bot_handler.send_reply(message, self.spam(bot_handler)) - elif command == 'open': + elif command == "open": bot_handler.send_reply(message, self.restore(bot_handler)) elif command.startswith(self.COMMENT_PREFIX): kwargs = { - 'author_id': "alt:email:" + message['sender_email'], - 'body': command[len(self.COMMENT_PREFIX) :], + "author_id": "alt:email:" + message["sender_email"], + "body": command[len(self.COMMENT_PREFIX) :], } bot_handler.send_reply(message, self.comment(bot_handler, **kwargs)) else: diff --git a/zulip_bots/zulip_bots/bots/front/test_front.py b/zulip_bots/zulip_bots/bots/front/test_front.py index fc388a6..3e0e369 100644 --- a/zulip_bots/zulip_bots/bots/front/test_front.py +++ b/zulip_bots/zulip_bots/bots/front/test_front.py @@ -4,28 +4,28 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests class TestFrontBot(BotTestCase, DefaultTests): - bot_name = 'front' + bot_name = "front" def make_request_message(self, content: str) -> Dict[str, Any]: message = super().make_request_message(content) - message['subject'] = "cnv_kqatm2" - message['sender_email'] = "leela@planet-express.com" + message["subject"] = "cnv_kqatm2" + message["sender_email"] = "leela@planet-express.com" return message def test_bot_invalid_api_key(self) -> None: - invalid_api_key = '' - with self.mock_config_info({'api_key': invalid_api_key}): + invalid_api_key = "" + with self.mock_config_info({"api_key": invalid_api_key}): with self.assertRaises(KeyError): bot, bot_handler = self._get_handlers() def test_bot_responds_to_empty_message(self) -> None: - with self.mock_config_info({'api_key': "TEST"}): + with self.mock_config_info({"api_key": "TEST"}): self.verify_reply("", "Unknown command. Use `help` for instructions.") def test_help(self) -> None: - with self.mock_config_info({'api_key': "TEST"}): + with self.mock_config_info({"api_key": "TEST"}): self.verify_reply( - 'help', + "help", "`archive` Archive a conversation.\n" "`delete` Delete a conversation.\n" "`spam` Mark a conversation as spam.\n" @@ -34,71 +34,71 @@ class TestFrontBot(BotTestCase, DefaultTests): ) def test_archive(self) -> None: - with self.mock_config_info({'api_key': "TEST"}): - with self.mock_http_conversation('archive'): - self.verify_reply('archive', "Conversation was archived.") + with self.mock_config_info({"api_key": "TEST"}): + with self.mock_http_conversation("archive"): + self.verify_reply("archive", "Conversation was archived.") def test_archive_error(self) -> None: - self._test_command_error('archive') + self._test_command_error("archive") def test_delete(self) -> None: - with self.mock_config_info({'api_key': "TEST"}): - with self.mock_http_conversation('delete'): - self.verify_reply('delete', "Conversation was deleted.") + with self.mock_config_info({"api_key": "TEST"}): + with self.mock_http_conversation("delete"): + self.verify_reply("delete", "Conversation was deleted.") def test_delete_error(self) -> None: - self._test_command_error('delete') + self._test_command_error("delete") def test_spam(self) -> None: - with self.mock_config_info({'api_key': "TEST"}): - with self.mock_http_conversation('spam'): - self.verify_reply('spam', "Conversation was marked as spam.") + with self.mock_config_info({"api_key": "TEST"}): + with self.mock_http_conversation("spam"): + self.verify_reply("spam", "Conversation was marked as spam.") def test_spam_error(self) -> None: - self._test_command_error('spam') + self._test_command_error("spam") def test_restore(self) -> None: - with self.mock_config_info({'api_key': "TEST"}): - with self.mock_http_conversation('open'): - self.verify_reply('open', "Conversation was restored.") + with self.mock_config_info({"api_key": "TEST"}): + with self.mock_http_conversation("open"): + self.verify_reply("open", "Conversation was restored.") def test_restore_error(self) -> None: - self._test_command_error('open') + self._test_command_error("open") def test_comment(self) -> None: body = "@bender, I thought you were supposed to be cooking for this party." - with self.mock_config_info({'api_key': "TEST"}): - with self.mock_http_conversation('comment'): + with self.mock_config_info({"api_key": "TEST"}): + with self.mock_http_conversation("comment"): self.verify_reply("comment " + body, "Comment was sent.") def test_comment_error(self) -> None: body = "@bender, I thought you were supposed to be cooking for this party." - self._test_command_error('comment', body) + self._test_command_error("comment", body) def _test_command_error(self, command_name: str, command_arg: Optional[str] = None) -> None: bot_command = command_name if command_arg: - bot_command += ' {}'.format(command_arg) - with self.mock_config_info({'api_key': "TEST"}): - with self.mock_http_conversation('{}_error'.format(command_name)): - self.verify_reply(bot_command, 'Something went wrong.') + bot_command += " {}".format(command_arg) + with self.mock_config_info({"api_key": "TEST"}): + with self.mock_http_conversation("{}_error".format(command_name)): + self.verify_reply(bot_command, "Something went wrong.") class TestFrontBotWrongTopic(BotTestCase, DefaultTests): - bot_name = 'front' + bot_name = "front" def make_request_message(self, content: str) -> Dict[str, Any]: message = super().make_request_message(content) - message['subject'] = "kqatm2" + message["subject"] = "kqatm2" return message def test_bot_responds_to_empty_message(self) -> None: pass def test_no_conversation_id(self) -> None: - with self.mock_config_info({'api_key': "TEST"}): + with self.mock_config_info({"api_key": "TEST"}): self.verify_reply( - 'archive', + "archive", "No coversation ID found. Please make " "sure that the name of the topic " "contains a valid coversation ID.", diff --git a/zulip_bots/zulip_bots/bots/game_handler_bot/game_handler_bot.py b/zulip_bots/zulip_bots/bots/game_handler_bot/game_handler_bot.py index 454b25d..300fd23 100644 --- a/zulip_bots/zulip_bots/bots/game_handler_bot/game_handler_bot.py +++ b/zulip_bots/zulip_bots/bots/game_handler_bot/game_handler_bot.py @@ -4,54 +4,54 @@ from zulip_bots.game_handler import BadMoveException, GameAdapter class GameHandlerBotMessageHandler: - tokens = [':blue_circle:', ':red_circle:'] + tokens = [":blue_circle:", ":red_circle:"] def parse_board(self, board: Any) -> str: - return 'foo' + return "foo" def get_player_color(self, turn: int) -> str: return self.tokens[turn] def alert_move_message(self, original_player: str, move_info: str) -> str: - column_number = move_info.replace('move ', '') - return original_player + ' moved in column ' + column_number + column_number = move_info.replace("move ", "") + return original_player + " moved in column " + column_number def game_start_message(self) -> str: - return 'Type `move ` to place a token.\n \ + return "Type `move ` to place a token.\n \ The first player to get 4 in a row wins!\n \ -Good Luck!' +Good Luck!" class MockModel: def __init__(self) -> None: - self.current_board = 'mock board' + self.current_board = "mock board" def make_move(self, move: str, player: int, is_computer: bool = False) -> Any: if not is_computer: - if int(move.replace('move ', '')) < 9: - return 'mock board' + if int(move.replace("move ", "")) < 9: + return "mock board" else: - raise BadMoveException('Invalid Move.') - return 'mock board' + raise BadMoveException("Invalid Move.") + return "mock board" def determine_game_over(self, players: List[str]) -> None: return None class GameHandlerBotHandler(GameAdapter): - ''' + """ DO NOT USE THIS BOT This bot is used to test game_handler.py - ''' + """ def __init__(self) -> None: - game_name = 'foo test game' - bot_name = 'game_handler_bot' - move_help_message = '* To make your move during a game, type\n```move ```' - move_regex = r'move (\d)$' + game_name = "foo test game" + bot_name = "game_handler_bot" + move_help_message = "* To make your move during a game, type\n```move ```" + move_regex = r"move (\d)$" model = MockModel gameMessageHandler = GameHandlerBotMessageHandler - rules = '' + rules = "" super().__init__( game_name, diff --git a/zulip_bots/zulip_bots/bots/game_handler_bot/test_game_handler_bot.py b/zulip_bots/zulip_bots/bots/game_handler_bot/test_game_handler_bot.py index f9519e0..e38011e 100644 --- a/zulip_bots/zulip_bots/bots/game_handler_bot/test_game_handler_bot.py +++ b/zulip_bots/zulip_bots/bots/game_handler_bot/test_game_handler_bot.py @@ -7,16 +7,16 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests class TestGameHandlerBot(BotTestCase, DefaultTests): - bot_name = 'game_handler_bot' + bot_name = "game_handler_bot" def make_request_message( self, content: str, - user: str = 'foo@example.com', - user_name: str = 'foo', - type: str = 'private', - stream: str = '', - subject: str = '', + user: str = "foo@example.com", + user_name: str = "foo", + type: str = "private", + stream: str = "", + subject: str = "", ) -> Dict[str, str]: message = dict( sender_email=user, @@ -35,58 +35,58 @@ class TestGameHandlerBot(BotTestCase, DefaultTests): expected_response: str, response_number: int, bot: Any = None, - user_name: str = 'foo', - stream: str = '', - subject: str = '', + user_name: str = "foo", + stream: str = "", + subject: str = "", max_messages: int = 20, ) -> None: - ''' + """ This function serves a similar purpose to BotTestCase.verify_dialog, but allows for multiple responses to be validated, and for mocking of the bot's internal data - ''' + """ if bot is None: bot, bot_handler = self._get_handlers() else: _b, bot_handler = self._get_handlers() - type = 'private' if stream == '' else 'stream' + type = "private" if stream == "" else "stream" message = self.make_request_message( - request, user_name + '@example.com', user_name, type, stream, subject + request, user_name + "@example.com", user_name, type, stream, subject ) bot_handler.reset_transcript() bot.handle_message(message, bot_handler) responses = [message for (method, message) in bot_handler.transcript] first_response = responses[response_number] - self.assertEqual(expected_response, first_response['content']) + self.assertEqual(expected_response, first_response["content"]) self.assertLessEqual(len(responses), max_messages) def add_user_to_cache(self, name: str, bot: Any = None) -> Any: if bot is None: bot, bot_handler = self._get_handlers() message = { - 'sender_email': '{}@example.com'.format(name), - 'sender_full_name': '{}'.format(name), + "sender_email": "{}@example.com".format(name), + "sender_full_name": "{}".format(name), } bot.add_user_to_cache(message) return bot def setup_game( self, - id: str = '', + id: str = "", bot: Any = None, - players: List[str] = ['foo', 'baz'], - subject: str = 'test game', - stream: str = 'test', + players: List[str] = ["foo", "baz"], + subject: str = "test game", + stream: str = "test", ) -> Any: if bot is None: bot, bot_handler = self._get_handlers() for p in players: self.add_user_to_cache(p, bot) - players_emails = [p + '@example.com' for p in players] - game_id = 'abc123' - if id != '': + players_emails = [p + "@example.com" for p in players] + game_id = "abc123" + if id != "": game_id = id instance = GameInstance(bot, False, subject, game_id, players_emails, stream) bot.instances.update({game_id: instance}) @@ -95,18 +95,18 @@ class TestGameHandlerBot(BotTestCase, DefaultTests): return bot def setup_computer_game(self) -> Any: - bot = self.add_user_to_cache('foo') - bot.email = 'test-bot@example.com' - self.add_user_to_cache('test-bot', bot) + bot = self.add_user_to_cache("foo") + bot.email = "test-bot@example.com" + self.add_user_to_cache("test-bot", bot) instance = GameInstance( - bot, False, 'test game', 'abc123', ['foo@example.com', 'test-bot@example.com'], 'test' + bot, False, "test game", "abc123", ["foo@example.com", "test-bot@example.com"], "test" ) - bot.instances.update({'abc123': instance}) + bot.instances.update({"abc123": instance}) instance.start() return bot def help_message(self) -> str: - return '''** foo test game Bot Help:** + return """** foo test game Bot Help:** *Preface all commands with @**test-bot*** * To start a game in a stream (*recommended*), type `start game` @@ -129,319 +129,319 @@ class TestGameHandlerBot(BotTestCase, DefaultTests): * To see rules of this game, type `rules` * To make your move during a game, type -```move ```''' +```move ```""" def test_help_message(self) -> None: - self.verify_response('help', self.help_message(), 0) - self.verify_response('foo bar baz', self.help_message(), 0) + self.verify_response("help", self.help_message(), 0) + self.verify_response("foo bar baz", self.help_message(), 0) def test_exception_handling(self) -> None: - with patch('logging.exception'), patch( - 'zulip_bots.game_handler.GameAdapter.command_quit', side_effect=Exception + with patch("logging.exception"), patch( + "zulip_bots.game_handler.GameAdapter.command_quit", side_effect=Exception ): - self.verify_response('quit', 'Error .', 0) + self.verify_response("quit", "Error .", 0) def test_not_in_game_messages(self) -> None: self.verify_response( - 'move 3', - 'You are not in a game at the moment. Type `help` for help.', + "move 3", + "You are not in a game at the moment. Type `help` for help.", 0, max_messages=1, ) self.verify_response( - 'quit', 'You are not in a game. Type `help` for all commands.', 0, max_messages=1 + "quit", "You are not in a game. Type `help` for all commands.", 0, max_messages=1 ) def test_start_game_with_name(self) -> None: - bot = self.add_user_to_cache('baz') + bot = self.add_user_to_cache("baz") self.verify_response( - 'start game with @**baz**', - 'You\'ve sent an invitation to play foo test game with @**baz**', + "start game with @**baz**", + "You've sent an invitation to play foo test game with @**baz**", 1, bot=bot, ) self.assertEqual(len(bot.invites), 1) def test_start_game_with_email(self) -> None: - bot = self.add_user_to_cache('baz') + bot = self.add_user_to_cache("baz") self.verify_response( - 'start game with baz@example.com', - 'You\'ve sent an invitation to play foo test game with @**baz**', + "start game with baz@example.com", + "You've sent an invitation to play foo test game with @**baz**", 1, bot=bot, ) self.assertEqual(len(bot.invites), 1) def test_join_game_and_start_in_stream(self) -> None: - bot = self.add_user_to_cache('baz') - self.add_user_to_cache('foo', bot) - bot.invites = {'abc': {'stream': 'test', 'subject': 'test game', 'host': 'foo@example.com'}} + bot = self.add_user_to_cache("baz") + self.add_user_to_cache("foo", bot) + bot.invites = {"abc": {"stream": "test", "subject": "test game", "host": "foo@example.com"}} self.verify_response( - 'join', - '@**baz** has joined the game', + "join", + "@**baz** has joined the game", 0, bot=bot, - stream='test', - subject='test game', - user_name='baz', + stream="test", + subject="test game", + user_name="baz", ) self.assertEqual(len(bot.instances.keys()), 1) def test_start_game_in_stream(self) -> None: self.verify_response( - 'start game', - '**foo** wants to play **foo test game**. Type @**test-bot** join to play them!', + "start game", + "**foo** wants to play **foo test game**. Type @**test-bot** join to play them!", 0, - stream='test', - subject='test game', + stream="test", + subject="test game", ) def test_start_invite_game_in_stream(self) -> None: - bot = self.add_user_to_cache('baz') + bot = self.add_user_to_cache("baz") self.verify_response( - 'start game with @**baz**', + "start game with @**baz**", 'If you were invited, and you\'re here, type "@**test-bot** accept" to accept the invite!', 2, bot=bot, - stream='test', - subject='game test', + stream="test", + subject="game test", ) def test_join_no_game(self) -> None: self.verify_response( - 'join', - 'There is not a game in this subject. Type `help` for all commands.', + "join", + "There is not a game in this subject. Type `help` for all commands.", 0, - stream='test', - subject='test game', - user_name='baz', + stream="test", + subject="test game", + user_name="baz", max_messages=1, ) def test_accept_invitation(self) -> None: - bot = self.add_user_to_cache('baz') - self.add_user_to_cache('foo', bot) + bot = self.add_user_to_cache("baz") + self.add_user_to_cache("foo", bot) bot.invites = { - 'abc': { - 'subject': '###private###', - 'stream': 'games', - 'host': 'foo@example.com', - 'baz@example.com': 'p', + "abc": { + "subject": "###private###", + "stream": "games", + "host": "foo@example.com", + "baz@example.com": "p", } } self.verify_response( - 'accept', 'Accepted invitation to play **foo test game** from @**foo**.', 0, bot, 'baz' + "accept", "Accepted invitation to play **foo test game** from @**foo**.", 0, bot, "baz" ) def test_decline_invitation(self) -> None: - bot = self.add_user_to_cache('baz') - self.add_user_to_cache('foo', bot) + bot = self.add_user_to_cache("baz") + self.add_user_to_cache("foo", bot) bot.invites = { - 'abc': {'subject': '###private###', 'host': 'foo@example.com', 'baz@example.com': 'p'} + "abc": {"subject": "###private###", "host": "foo@example.com", "baz@example.com": "p"} } self.verify_response( - 'decline', 'Declined invitation to play **foo test game** from @**foo**.', 0, bot, 'baz' + "decline", "Declined invitation to play **foo test game** from @**foo**.", 0, bot, "baz" ) def test_quit_invite(self) -> None: - bot = self.add_user_to_cache('foo') - bot.invites = {'abc': {'subject': '###private###', 'host': 'foo@example.com'}} - self.verify_response('quit', 'Game cancelled.\n**foo** quit.', 0, bot, 'foo') + bot = self.add_user_to_cache("foo") + bot.invites = {"abc": {"subject": "###private###", "host": "foo@example.com"}} + self.verify_response("quit", "Game cancelled.\n**foo** quit.", 0, bot, "foo") def test_user_already_in_game_errors(self) -> None: bot = self.setup_game() self.verify_response( - 'start game with @**baz**', - 'You are already in a game. Type `quit` to leave.', + "start game with @**baz**", + "You are already in a game. Type `quit` to leave.", 0, bot=bot, max_messages=1, ) self.verify_response( - 'start game', - 'You are already in a game. Type `quit` to leave.', + "start game", + "You are already in a game. Type `quit` to leave.", 0, bot=bot, - stream='test', + stream="test", max_messages=1, ) self.verify_response( - 'accept', 'You are already in a game. Type `quit` to leave.', 0, bot=bot, max_messages=1 + "accept", "You are already in a game. Type `quit` to leave.", 0, bot=bot, max_messages=1 ) self.verify_response( - 'decline', - 'You are already in a game. Type `quit` to leave.', + "decline", + "You are already in a game. Type `quit` to leave.", 0, bot=bot, max_messages=1, ) self.verify_response( - 'join', 'You are already in a game. Type `quit` to leave.', 0, bot=bot, max_messages=1 + "join", "You are already in a game. Type `quit` to leave.", 0, bot=bot, max_messages=1 ) def test_register_command(self) -> None: - bot = self.add_user_to_cache('foo') - self.verify_response('register', 'Hello @**foo**. Thanks for registering!', 0, bot, 'foo') - self.assertIn('foo@example.com', bot.user_cache.keys()) + bot = self.add_user_to_cache("foo") + self.verify_response("register", "Hello @**foo**. Thanks for registering!", 0, bot, "foo") + self.assertIn("foo@example.com", bot.user_cache.keys()) def test_no_active_invite_errors(self) -> None: - self.verify_response('accept', 'No active invites. Type `help` for commands.', 0) - self.verify_response('decline', 'No active invites. Type `help` for commands.', 0) + self.verify_response("accept", "No active invites. Type `help` for commands.", 0) + self.verify_response("decline", "No active invites. Type `help` for commands.", 0) def test_wrong_number_of_players_message(self) -> None: - bot = self.add_user_to_cache('baz') + bot = self.add_user_to_cache("baz") bot.min_players = 5 self.verify_response( - 'start game with @**baz**', - 'You must have at least 5 players to play.\nGame cancelled.', + "start game with @**baz**", + "You must have at least 5 players to play.\nGame cancelled.", 0, bot=bot, ) bot.min_players = 2 bot.max_players = 1 self.verify_response( - 'start game with @**baz**', - 'The maximum number of players for this game is 1.', + "start game with @**baz**", + "The maximum number of players for this game is 1.", 0, bot=bot, ) bot.max_players = 1 - bot.invites = {'abc': {'stream': 'test', 'subject': 'test game', 'host': 'foo@example.com'}} + bot.invites = {"abc": {"stream": "test", "subject": "test game", "host": "foo@example.com"}} self.verify_response( - 'join', - 'This game is full.', + "join", + "This game is full.", 0, bot=bot, - stream='test', - subject='test game', - user_name='baz', + stream="test", + subject="test game", + user_name="baz", ) def test_public_accept(self) -> None: - bot = self.add_user_to_cache('baz') - self.add_user_to_cache('foo', bot) + bot = self.add_user_to_cache("baz") + self.add_user_to_cache("foo", bot) bot.invites = { - 'abc': { - 'stream': 'test', - 'subject': 'test game', - 'host': 'baz@example.com', - 'foo@example.com': 'p', + "abc": { + "stream": "test", + "subject": "test game", + "host": "baz@example.com", + "foo@example.com": "p", } } self.verify_response( - 'accept', - '@**foo** has accepted the invitation.', + "accept", + "@**foo** has accepted the invitation.", 0, bot=bot, - stream='test', - subject='test game', + stream="test", + subject="test game", ) def test_start_game_with_computer(self) -> None: self.verify_response( - 'start game with @**test-bot**', - 'Wait... That\'s me!', + "start game with @**test-bot**", + "Wait... That's me!", 4, - stream='test', - subject='test game', + stream="test", + subject="test game", ) def test_sent_by_bot(self) -> None: with self.assertRaises(IndexError): self.verify_response( - 'foo', '', 0, user_name='test-bot', stream='test', subject='test game' + "foo", "", 0, user_name="test-bot", stream="test", subject="test game" ) def test_forfeit(self) -> None: bot = self.setup_game() self.verify_response( - 'forfeit', '**foo** forfeited!', 0, bot=bot, stream='test', subject='test game' + "forfeit", "**foo** forfeited!", 0, bot=bot, stream="test", subject="test game" ) def test_draw(self) -> None: bot = self.setup_game() self.verify_response( - 'draw', - '**foo** has voted for a draw!\nType `draw` to accept', + "draw", + "**foo** has voted for a draw!\nType `draw` to accept", 0, bot=bot, - stream='test', - subject='test game', + stream="test", + subject="test game", ) self.verify_response( - 'draw', - 'It was a draw!', + "draw", + "It was a draw!", 0, bot=bot, - stream='test', - subject='test game', - user_name='baz', + stream="test", + subject="test game", + user_name="baz", ) def test_normal_turns(self) -> None: bot = self.setup_game() self.verify_response( - 'move 3', - '**foo** moved in column 3\n\nfoo\n\nIt\'s **baz**\'s (:red_circle:) turn.', + "move 3", + "**foo** moved in column 3\n\nfoo\n\nIt's **baz**'s (:red_circle:) turn.", 0, bot=bot, - stream='test', - subject='test game', + stream="test", + subject="test game", ) self.verify_response( - 'move 3', - '**baz** moved in column 3\n\nfoo\n\nIt\'s **foo**\'s (:blue_circle:) turn.', + "move 3", + "**baz** moved in column 3\n\nfoo\n\nIt's **foo**'s (:blue_circle:) turn.", 0, bot=bot, - stream='test', - subject='test game', - user_name='baz', + stream="test", + subject="test game", + user_name="baz", ) def test_wrong_turn(self) -> None: bot = self.setup_game() self.verify_response( - 'move 5', - 'It\'s **foo**\'s (:blue_circle:) turn.', + "move 5", + "It's **foo**'s (:blue_circle:) turn.", 0, bot=bot, - stream='test', - subject='test game', - user_name='baz', + stream="test", + subject="test game", + user_name="baz", ) def test_private_message_error(self) -> None: self.verify_response( - 'start game', - 'If you are starting a game in private messages, you must invite players. Type `help` for commands.', + "start game", + "If you are starting a game in private messages, you must invite players. Type `help` for commands.", 0, max_messages=1, ) - bot = self.add_user_to_cache('bar') + bot = self.add_user_to_cache("bar") bot.invites = { - 'abcdefg': {'host': 'bar@example.com', 'stream': 'test', 'subject': 'test game'} + "abcdefg": {"host": "bar@example.com", "stream": "test", "subject": "test game"} } self.verify_response( - 'join', - 'You cannot join games in private messages. Type `help` for all commands.', + "join", + "You cannot join games in private messages. Type `help` for all commands.", 0, bot=bot, max_messages=1, ) def test_game_already_in_subject(self) -> None: - bot = self.add_user_to_cache('foo') + bot = self.add_user_to_cache("foo") bot.invites = { - 'abcdefg': {'host': 'foo@example.com', 'stream': 'test', 'subject': 'test game'} + "abcdefg": {"host": "foo@example.com", "stream": "test", "subject": "test game"} } self.verify_response( - 'start game', - 'There is already a game in this stream.', + "start game", + "There is already a game in this stream.", 0, bot=bot, - stream='test', - subject='test game', - user_name='baz', + stream="test", + subject="test game", + user_name="baz", max_messages=1, ) @@ -452,219 +452,219 @@ class TestGameHandlerBot(BotTestCase, DefaultTests): def test_unknown_user(self) -> None: self.verify_response( - 'start game with @**bar**', - 'I don\'t know @**bar**. Tell them to say @**test-bot** register', + "start game with @**bar**", + "I don't know @**bar**. Tell them to say @**test-bot** register", 0, ) self.verify_response( - 'start game with bar@example.com', - 'I don\'t know bar@example.com. Tell them to use @**test-bot** register', + "start game with bar@example.com", + "I don't know bar@example.com. Tell them to use @**test-bot** register", 0, ) def test_is_user_not_player(self) -> None: - bot = self.add_user_to_cache('foo') - self.add_user_to_cache('baz', bot) - bot.invites = {'abcdefg': {'host': 'foo@example.com', 'baz@example.com': 'a'}} - self.assertFalse(bot.is_user_not_player('foo@example.com')) - self.assertFalse(bot.is_user_not_player('baz@example.com')) + bot = self.add_user_to_cache("foo") + self.add_user_to_cache("baz", bot) + bot.invites = {"abcdefg": {"host": "foo@example.com", "baz@example.com": "a"}} + self.assertFalse(bot.is_user_not_player("foo@example.com")) + self.assertFalse(bot.is_user_not_player("baz@example.com")) def test_move_help_message(self) -> None: bot = self.setup_game() self.verify_response( - 'move 123', - '* To make your move during a game, type\n```move ```', + "move 123", + "* To make your move during a game, type\n```move ```", 0, bot=bot, - stream='test', - subject='test game', + stream="test", + subject="test game", ) def test_invalid_move_message(self) -> None: bot = self.setup_game() self.verify_response( - 'move 9', - 'Invalid Move.', + "move 9", + "Invalid Move.", 0, bot=bot, - stream='test', - subject='test game', + stream="test", + subject="test game", max_messages=2, ) def test_get_game_id_by_email(self) -> None: bot = self.setup_game() - self.assertEqual(bot.get_game_id_by_email('foo@example.com'), 'abc123') + self.assertEqual(bot.get_game_id_by_email("foo@example.com"), "abc123") def test_game_over_and_leaderboard(self) -> None: bot = self.setup_game() bot.put_user_cache() with patch( - 'zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over', - return_value='foo@example.com', + "zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over", + return_value="foo@example.com", ): self.verify_response( - 'move 3', '**foo** won! :tada:', 1, bot=bot, stream='test', subject='test game' + "move 3", "**foo** won! :tada:", 1, bot=bot, stream="test", subject="test game" ) - leaderboard = '**Most wins**\n\n\ + leaderboard = "**Most wins**\n\n\ Player | Games Won | Games Drawn | Games Lost | Total Games\n\ --- | --- | --- | --- | --- \n\ **foo** | 1 | 0 | 0 | 1\n\ **baz** | 0 | 0 | 1 | 1\n\ - **test-bot** | 0 | 0 | 0 | 0' - self.verify_response('leaderboard', leaderboard, 0, bot=bot) + **test-bot** | 0 | 0 | 0 | 0" + self.verify_response("leaderboard", leaderboard, 0, bot=bot) def test_current_turn_winner(self) -> None: bot = self.setup_game() with patch( - 'zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over', - return_value='current turn', + "zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over", + return_value="current turn", ): self.verify_response( - 'move 3', '**foo** won! :tada:', 1, bot=bot, stream='test', subject='test game' + "move 3", "**foo** won! :tada:", 1, bot=bot, stream="test", subject="test game" ) def test_computer_turn(self) -> None: bot = self.setup_computer_game() self.verify_response( - 'move 3', - '**foo** moved in column 3\n\nfoo\n\nIt\'s **test-bot**\'s (:red_circle:) turn.', + "move 3", + "**foo** moved in column 3\n\nfoo\n\nIt's **test-bot**'s (:red_circle:) turn.", 0, bot=bot, - stream='test', - subject='test game', + stream="test", + subject="test game", ) with patch( - 'zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over', - return_value='test-bot@example.com', + "zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over", + return_value="test-bot@example.com", ): self.verify_response( - 'move 5', 'I won! Well Played!', 2, bot=bot, stream='test', subject='test game' + "move 5", "I won! Well Played!", 2, bot=bot, stream="test", subject="test game" ) def test_computer_endgame_responses(self) -> None: bot = self.setup_computer_game() with patch( - 'zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over', - return_value='foo@example.com', + "zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over", + return_value="foo@example.com", ): self.verify_response( - 'move 5', 'You won! Nice!', 2, bot=bot, stream='test', subject='test game' + "move 5", "You won! Nice!", 2, bot=bot, stream="test", subject="test game" ) bot = self.setup_computer_game() with patch( - 'zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over', - return_value='draw', + "zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over", + return_value="draw", ): self.verify_response( - 'move 5', - 'It was a draw! Well Played!', + "move 5", + "It was a draw! Well Played!", 2, bot=bot, - stream='test', - subject='test game', + stream="test", + subject="test game", ) def test_add_user_statistics(self) -> None: - bot = self.add_user_to_cache('foo') - bot.add_user_statistics('foo@example.com', {'foo': 3}) - self.assertEqual(bot.user_cache['foo@example.com']['stats']['foo'], 3) + bot = self.add_user_to_cache("foo") + bot.add_user_statistics("foo@example.com", {"foo": 3}) + self.assertEqual(bot.user_cache["foo@example.com"]["stats"]["foo"], 3) def test_get_players(self) -> None: bot = self.setup_game() - players = bot.get_players('abc123') - self.assertEqual(players, ['foo@example.com', 'baz@example.com']) + players = bot.get_players("abc123") + self.assertEqual(players, ["foo@example.com", "baz@example.com"]) def test_none_function_responses(self) -> None: bot, bot_handler = self._get_handlers() - self.assertEqual(bot.get_players('abc'), []) - self.assertEqual(bot.get_user_by_name('no one'), {}) - self.assertEqual(bot.get_user_by_email('no one'), {}) + self.assertEqual(bot.get_players("abc"), []) + self.assertEqual(bot.get_user_by_name("no one"), {}) + self.assertEqual(bot.get_user_by_email("no one"), {}) def test_get_game_info(self) -> None: - bot = self.add_user_to_cache('foo') - self.add_user_to_cache('baz', bot) + bot = self.add_user_to_cache("foo") + self.add_user_to_cache("baz", bot) bot.invites = { - 'abcdefg': { - 'host': 'foo@example.com', - 'baz@example.com': 'a', - 'stream': 'test', - 'subject': 'test game', + "abcdefg": { + "host": "foo@example.com", + "baz@example.com": "a", + "stream": "test", + "subject": "test game", } } self.assertEqual( - bot.get_game_info('abcdefg'), + bot.get_game_info("abcdefg"), { - 'game_id': 'abcdefg', - 'type': 'invite', - 'stream': 'test', - 'subject': 'test game', - 'players': ['foo@example.com', 'baz@example.com'], + "game_id": "abcdefg", + "type": "invite", + "stream": "test", + "subject": "test game", + "players": ["foo@example.com", "baz@example.com"], }, ) def test_parse_message(self) -> None: bot = self.setup_game() self.verify_response( - 'move 3', - 'Join your game using the link below!\n\n> **Game `abc123`**\n\ + "move 3", + "Join your game using the link below!\n\n> **Game `abc123`**\n\ > foo test game\n\ > 2/2 players\n\ -> **[Join Game](/#narrow/stream/test/topic/test game)**', +> **[Join Game](/#narrow/stream/test/topic/test game)**", 0, bot=bot, ) bot = self.setup_game() self.verify_response( - 'move 3', - '''Your current game is not in this subject. \n\ + "move 3", + """Your current game is not in this subject. \n\ To move subjects, send your message again, otherwise join the game using the link below. > **Game `abc123`** > foo test game > 2/2 players -> **[Join Game](/#narrow/stream/test/topic/test game)**''', +> **[Join Game](/#narrow/stream/test/topic/test game)**""", 0, bot=bot, - stream='test 2', - subject='game 2', + stream="test 2", + subject="game 2", ) - self.verify_response('move 3', 'foo', 0, bot=bot, stream='test 2', subject='game 2') + self.verify_response("move 3", "foo", 0, bot=bot, stream="test 2", subject="game 2") def test_change_game_subject(self) -> None: - bot = self.setup_game('abc123') - self.setup_game('abcdefg', bot, ['bar', 'abc'], 'test game 2', 'test2') + bot = self.setup_game("abc123") + self.setup_game("abcdefg", bot, ["bar", "abc"], "test game 2", "test2") self.verify_response( - 'move 3', - '''Your current game is not in this subject. \n\ + "move 3", + """Your current game is not in this subject. \n\ To move subjects, send your message again, otherwise join the game using the link below. > **Game `abcdefg`** > foo test game > 2/2 players -> **[Join Game](/#narrow/stream/test2/topic/test game 2)**''', +> **[Join Game](/#narrow/stream/test2/topic/test game 2)**""", 0, bot=bot, - user_name='bar', - stream='test game', - subject='test2', + user_name="bar", + stream="test game", + subject="test2", ) self.verify_response( - 'move 3', - 'There is already a game in this subject.', + "move 3", + "There is already a game in this subject.", 0, bot=bot, - user_name='bar', - stream='test game', - subject='test', + user_name="bar", + stream="test game", + subject="test", ) bot.invites = { - 'foo bar baz': { - 'host': 'foo@example.com', - 'baz@example.com': 'a', - 'stream': 'test', - 'subject': 'test game', + "foo bar baz": { + "host": "foo@example.com", + "baz@example.com": "a", + "stream": "test", + "subject": "test game", } } - bot.change_game_subject('foo bar baz', 'test2', 'game2', self.make_request_message('foo')) - self.assertEqual(bot.invites['foo bar baz']['stream'], 'test2') + bot.change_game_subject("foo bar baz", "test2", "game2", self.make_request_message("foo")) + self.assertEqual(bot.invites["foo bar baz"]["stream"], "test2") diff --git a/zulip_bots/zulip_bots/bots/game_of_fifteen/game_of_fifteen.py b/zulip_bots/zulip_bots/bots/game_of_fifteen/game_of_fifteen.py index f53ebbb..0c0e013 100644 --- a/zulip_bots/zulip_bots/bots/game_of_fifteen/game_of_fifteen.py +++ b/zulip_bots/zulip_bots/bots/game_of_fifteen/game_of_fifteen.py @@ -31,8 +31,8 @@ class GameOfFifteenModel: def determine_game_over(self, players: List[str]) -> str: if self.won(self.current_board): - return 'current turn' - return '' + return "current turn" + return "" def won(self, board: Any) -> bool: for i in range(3): @@ -52,16 +52,16 @@ class GameOfFifteenModel: def make_move(self, move: str, player_number: int, computer_move: bool = False) -> Any: board = self.current_board move = move.strip() - move = move.split(' ') + move = move.split(" ") - if '' in move: - raise BadMoveException('You should enter space separated digits.') + if "" in move: + raise BadMoveException("You should enter space separated digits.") moves = len(move) for m in range(1, moves): tile = int(move[m]) coordinates = self.get_coordinates(board) if tile not in coordinates: - raise BadMoveException('You can only move tiles which exist in the board.') + raise BadMoveException("You can only move tiles which exist in the board.") i, j = coordinates[tile] if (j - 1) > -1 and board[i][j - 1] == 0: board[i][j - 1] = tile @@ -77,7 +77,7 @@ class GameOfFifteenModel: board[i][j] = 0 else: raise BadMoveException( - 'You can only move tiles which are adjacent to :grey_question:.' + "You can only move tiles which are adjacent to :grey_question:." ) if m == moves - 1: return board @@ -86,30 +86,30 @@ class GameOfFifteenModel: class GameOfFifteenMessageHandler: tiles = { - '0': ':grey_question:', - '1': ':one:', - '2': ':two:', - '3': ':three:', - '4': ':four:', - '5': ':five:', - '6': ':six:', - '7': ':seven:', - '8': ':eight:', + "0": ":grey_question:", + "1": ":one:", + "2": ":two:", + "3": ":three:", + "4": ":four:", + "5": ":five:", + "6": ":six:", + "7": ":seven:", + "8": ":eight:", } def parse_board(self, board: Any) -> str: # Header for the top of the board - board_str = '' + board_str = "" for row in range(3): - board_str += '\n\n' + board_str += "\n\n" for column in range(3): board_str += self.tiles[str(board[row][column])] return board_str def alert_move_message(self, original_player: str, move_info: str) -> str: - tile = move_info.replace('move ', '') - return original_player + ' moved ' + tile + tile = move_info.replace("move ", "") + return original_player + " moved " + tile def game_start_message(self) -> str: return ( @@ -119,23 +119,23 @@ class GameOfFifteenMessageHandler: class GameOfFifteenBotHandler(GameAdapter): - ''' + """ Bot that uses the Game Adapter class to allow users to play Game of Fifteen - ''' + """ def __init__(self) -> None: - game_name = 'Game of Fifteen' - bot_name = 'Game of Fifteen' + game_name = "Game of Fifteen" + bot_name = "Game of Fifteen" move_help_message = ( - '* To make your move during a game, type\n```move ...```' + "* To make your move during a game, type\n```move ...```" ) - move_regex = r'move [\d{1}\s]+$' + move_regex = r"move [\d{1}\s]+$" model = GameOfFifteenModel gameMessageHandler = GameOfFifteenMessageHandler - rules = '''Arrange the board’s tiles from smallest to largest, left to right, + rules = """Arrange the board’s tiles from smallest to largest, left to right, top to bottom, and tiles adjacent to :grey_question: can only be moved. - Final configuration will have :grey_question: in top left.''' + Final configuration will have :grey_question: in top left.""" super().__init__( game_name, diff --git a/zulip_bots/zulip_bots/bots/game_of_fifteen/test_game_of_fifteen.py b/zulip_bots/zulip_bots/bots/game_of_fifteen/test_game_of_fifteen.py index ac0f935..b3d6767 100644 --- a/zulip_bots/zulip_bots/bots/game_of_fifteen/test_game_of_fifteen.py +++ b/zulip_bots/zulip_bots/bots/game_of_fifteen/test_game_of_fifteen.py @@ -6,10 +6,10 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests class TestGameOfFifteenBot(BotTestCase, DefaultTests): - bot_name = 'game_of_fifteen' + bot_name = "game_of_fifteen" def make_request_message( - self, content: str, user: str = 'foo@example.com', user_name: str = 'foo' + self, content: str, user: str = "foo@example.com", user_name: str = "foo" ) -> Dict[str, str]: message = dict(sender_email=user, content=content, sender_full_name=user_name) return message @@ -20,14 +20,14 @@ class TestGameOfFifteenBot(BotTestCase, DefaultTests): request: str, expected_response: str, response_number: int, - user: str = 'foo@example.com', + user: str = "foo@example.com", ) -> None: - ''' + """ This function serves a similar purpose to BotTestCase.verify_dialog, but allows for multiple responses to be validated, and for mocking of the bot's internal data - ''' + """ bot, bot_handler = self._get_handlers() message = self.make_request_message(request, user) @@ -38,10 +38,10 @@ class TestGameOfFifteenBot(BotTestCase, DefaultTests): responses = [message for (method, message) in bot_handler.transcript] first_response = responses[response_number] - self.assertEqual(expected_response, first_response['content']) + self.assertEqual(expected_response, first_response["content"]) def help_message(self) -> str: - return '''** Game of Fifteen Bot Help:** + return """** Game of Fifteen Bot Help:** *Preface all commands with @**test-bot*** * To start a game in a stream, type `start game` @@ -50,16 +50,16 @@ class TestGameOfFifteenBot(BotTestCase, DefaultTests): * To see rules of this game, type `rules` * To make your move during a game, type -```move ...```''' +```move ...```""" def test_static_responses(self) -> None: - self.verify_response('help', self.help_message(), 0) + self.verify_response("help", self.help_message(), 0) def test_game_message_handler_responses(self) -> None: - board = '\n\n:grey_question::one::two:\n\n:three::four::five:\n\n:six::seven::eight:' + board = "\n\n:grey_question::one::two:\n\n:three::four::five:\n\n:six::seven::eight:" bot, bot_handler = self._get_handlers() self.assertEqual(bot.gameMessageHandler.parse_board(self.winning_board), board) - self.assertEqual(bot.gameMessageHandler.alert_move_message('foo', 'move 1'), 'foo moved 1') + self.assertEqual(bot.gameMessageHandler.alert_move_message("foo", "move 1"), "foo moved 1") self.assertEqual( bot.gameMessageHandler.game_start_message(), "Welcome to Game of Fifteen!" @@ -86,13 +86,13 @@ class TestGameOfFifteenBot(BotTestCase, DefaultTests): final_board: List[List[int]], ) -> None: gameOfFifteenModel.update_board(initial_board) - test_board = gameOfFifteenModel.make_move('move ' + tile, token_number) + test_board = gameOfFifteenModel.make_move("move " + tile, token_number) self.assertEqual(test_board, final_board) def confirmGameOver(board: List[List[int]], result: str) -> None: gameOfFifteenModel.update_board(board) - game_over = gameOfFifteenModel.determine_game_over(['first_player']) + game_over = gameOfFifteenModel.determine_game_over(["first_player"]) self.assertEqual(game_over, result) @@ -114,17 +114,17 @@ class TestGameOfFifteenBot(BotTestCase, DefaultTests): confirmAvailableMoves([1, 2, 3, 4, 5, 6, 7, 8], [0, 9, -1], initial_board) # Test Move Logic - confirmMove('1', 0, initial_board, [[8, 7, 6], [5, 4, 3], [2, 0, 1]]) + confirmMove("1", 0, initial_board, [[8, 7, 6], [5, 4, 3], [2, 0, 1]]) - confirmMove('1 2', 0, initial_board, [[8, 7, 6], [5, 4, 3], [0, 2, 1]]) + confirmMove("1 2", 0, initial_board, [[8, 7, 6], [5, 4, 3], [0, 2, 1]]) - confirmMove('1 2 5', 0, initial_board, [[8, 7, 6], [0, 4, 3], [5, 2, 1]]) + confirmMove("1 2 5", 0, initial_board, [[8, 7, 6], [0, 4, 3], [5, 2, 1]]) - confirmMove('1 2 5 4', 0, initial_board, [[8, 7, 6], [4, 0, 3], [5, 2, 1]]) + confirmMove("1 2 5 4", 0, initial_board, [[8, 7, 6], [4, 0, 3], [5, 2, 1]]) - confirmMove('3', 0, sample_board, [[7, 6, 8], [0, 3, 1], [2, 4, 5]]) + confirmMove("3", 0, sample_board, [[7, 6, 8], [0, 3, 1], [2, 4, 5]]) - confirmMove('3 7', 0, sample_board, [[0, 6, 8], [7, 3, 1], [2, 4, 5]]) + confirmMove("3 7", 0, sample_board, [[0, 6, 8], [7, 3, 1], [2, 4, 5]]) # Test coordinates logic: confirm_coordinates( @@ -143,16 +143,16 @@ class TestGameOfFifteenBot(BotTestCase, DefaultTests): ) # Test Game Over Logic: - confirmGameOver(winning_board, 'current turn') - confirmGameOver(sample_board, '') + confirmGameOver(winning_board, "current turn") + confirmGameOver(sample_board, "") def test_invalid_moves(self) -> None: model = GameOfFifteenModel() - move1 = 'move 2' - move2 = 'move 5' - move3 = 'move 23' - move4 = 'move 0' - move5 = 'move 1 2' + move1 = "move 2" + move2 = "move 5" + move3 = "move 23" + move4 = "move 0" + move5 = "move 1 2" initial_board = [[8, 7, 6], [5, 4, 3], [2, 1, 0]] model.update_board(initial_board) diff --git a/zulip_bots/zulip_bots/bots/giphy/giphy.py b/zulip_bots/zulip_bots/bots/giphy/giphy.py index 037085d..a0a5a42 100644 --- a/zulip_bots/zulip_bots/bots/giphy/giphy.py +++ b/zulip_bots/zulip_bots/bots/giphy/giphy.py @@ -7,8 +7,8 @@ from requests.exceptions import ConnectionError, HTTPError from zulip_bots.custom_exceptions import ConfigValidationError from zulip_bots.lib import BotHandler -GIPHY_TRANSLATE_API = 'http://api.giphy.com/v1/gifs/translate' -GIPHY_RANDOM_API = 'http://api.giphy.com/v1/gifs/random' +GIPHY_TRANSLATE_API = "http://api.giphy.com/v1/gifs/translate" +GIPHY_RANDOM_API = "http://api.giphy.com/v1/gifs/random" class GiphyHandler: @@ -21,15 +21,15 @@ class GiphyHandler: """ def usage(self) -> str: - return ''' + return """ This plugin allows users to post GIFs provided by Giphy. Users should preface keywords with the Giphy-bot @mention. The bot responds also to private messages. - ''' + """ @staticmethod def validate_config(config_info: Dict[str, str]) -> None: - query = {'s': 'Hello', 'api_key': config_info['key']} + query = {"s": "Hello", "api_key": config_info["key"]} try: data = requests.get(GIPHY_TRANSLATE_API, params=query) data.raise_for_status() @@ -39,13 +39,13 @@ class GiphyHandler: error_message = str(e) if data.status_code == 403: error_message += ( - 'This is likely due to an invalid key.\n' - 'Follow the instructions in doc.md for setting an API key.' + "This is likely due to an invalid key.\n" + "Follow the instructions in doc.md for setting an API key." ) raise ConfigValidationError(error_message) def initialize(self, bot_handler: BotHandler) -> None: - self.config_info = bot_handler.get_config_info('giphy') + self.config_info = bot_handler.get_config_info("giphy") def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: bot_response = get_bot_giphy_response(message, bot_handler, self.config_info) @@ -60,9 +60,9 @@ def get_url_gif_giphy(keyword: str, api_key: str) -> Union[int, str]: # Return a URL for a Giphy GIF based on keywords given. # In case of error, e.g. failure to fetch a GIF URL, it will # return a number. - query = {'api_key': api_key} + query = {"api_key": api_key} if len(keyword) > 0: - query['s'] = keyword + query["s"] = keyword url = GIPHY_TRANSLATE_API else: url = GIPHY_RANDOM_API @@ -70,12 +70,12 @@ def get_url_gif_giphy(keyword: str, api_key: str) -> Union[int, str]: try: data = requests.get(url, params=query) except requests.exceptions.ConnectionError: # Usually triggered by bad connection. - logging.exception('Bad connection') + logging.exception("Bad connection") raise data.raise_for_status() try: - gif_url = data.json()['data']['images']['original']['url'] + gif_url = data.json()["data"]["images"]["original"]["url"] except (TypeError, KeyError): # Usually triggered by no result in Giphy. raise GiphyNoResultException() return gif_url @@ -86,20 +86,20 @@ def get_bot_giphy_response( ) -> str: # Each exception has a specific reply should "gif_url" return a number. # The bot will post the appropriate message for the error. - keyword = message['content'] + keyword = message["content"] try: - gif_url = get_url_gif_giphy(keyword, config_info['key']) + gif_url = get_url_gif_giphy(keyword, config_info["key"]) except requests.exceptions.ConnectionError: return ( - 'Uh oh, sorry :slightly_frowning_face:, I ' - 'cannot process your request right now. But, ' - 'let\'s try again later! :grin:' + "Uh oh, sorry :slightly_frowning_face:, I " + "cannot process your request right now. But, " + "let's try again later! :grin:" ) except GiphyNoResultException: - return 'Sorry, I don\'t have a GIF for "%s"! ' ':astonished:' % (keyword,) + return 'Sorry, I don\'t have a GIF for "%s"! ' ":astonished:" % (keyword,) return ( - '[Click to enlarge](%s)' - '[](/static/images/interactive-bot/giphy/powered-by-giphy.png)' % (gif_url,) + "[Click to enlarge](%s)" + "[](/static/images/interactive-bot/giphy/powered-by-giphy.png)" % (gif_url,) ) diff --git a/zulip_bots/zulip_bots/bots/giphy/test_giphy.py b/zulip_bots/zulip_bots/bots/giphy/test_giphy.py index 244f553..fb21d40 100755 --- a/zulip_bots/zulip_bots/bots/giphy/test_giphy.py +++ b/zulip_bots/zulip_bots/bots/giphy/test_giphy.py @@ -11,58 +11,58 @@ class TestGiphyBot(BotTestCase, DefaultTests): # Test for bot response to empty message def test_bot_responds_to_empty_message(self) -> None: bot_response = ( - '[Click to enlarge]' - '(https://media0.giphy.com/media/ISumMYQyX4sSI/giphy.gif)' - '[](/static/images/interactive-bot/giphy/powered-by-giphy.png)' + "[Click to enlarge]" + "(https://media0.giphy.com/media/ISumMYQyX4sSI/giphy.gif)" + "[](/static/images/interactive-bot/giphy/powered-by-giphy.png)" ) - with self.mock_config_info({'key': '12345678'}), self.mock_http_conversation('test_random'): - self.verify_reply('', bot_response) + with self.mock_config_info({"key": "12345678"}), self.mock_http_conversation("test_random"): + self.verify_reply("", bot_response) def test_normal(self) -> None: bot_response = ( - '[Click to enlarge]' - '(https://media4.giphy.com/media/3o6ZtpxSZbQRRnwCKQ/giphy.gif)' - '[](/static/images/interactive-bot/giphy/powered-by-giphy.png)' + "[Click to enlarge]" + "(https://media4.giphy.com/media/3o6ZtpxSZbQRRnwCKQ/giphy.gif)" + "[](/static/images/interactive-bot/giphy/powered-by-giphy.png)" ) - with self.mock_config_info({'key': '12345678'}), self.mock_http_conversation('test_normal'): - self.verify_reply('Hello', bot_response) + with self.mock_config_info({"key": "12345678"}), self.mock_http_conversation("test_normal"): + self.verify_reply("Hello", bot_response) def test_no_result(self) -> None: - with self.mock_config_info({'key': '12345678'}), self.mock_http_conversation( - 'test_no_result' + with self.mock_config_info({"key": "12345678"}), self.mock_http_conversation( + "test_no_result" ): self.verify_reply( - 'world without zulip', + "world without zulip", 'Sorry, I don\'t have a GIF for "world without zulip"! :astonished:', ) def test_invalid_config(self) -> None: get_bot_message_handler(self.bot_name) StubBotHandler() - with self.mock_http_conversation('test_403'): + with self.mock_http_conversation("test_403"): self.validate_invalid_config( - {'key': '12345678'}, "This is likely due to an invalid key.\n" + {"key": "12345678"}, "This is likely due to an invalid key.\n" ) def test_connection_error_when_validate_config(self) -> None: error = ConnectionError() - with patch('requests.get', side_effect=ConnectionError()): - self.validate_invalid_config({'key': '12345678'}, str(error)) + with patch("requests.get", side_effect=ConnectionError()): + self.validate_invalid_config({"key": "12345678"}, str(error)) def test_valid_config(self) -> None: get_bot_message_handler(self.bot_name) StubBotHandler() - with self.mock_http_conversation('test_normal'): - self.validate_valid_config({'key': '12345678'}) + with self.mock_http_conversation("test_normal"): + self.validate_valid_config({"key": "12345678"}) def test_connection_error_while_running(self) -> None: - with self.mock_config_info({'key': '12345678'}), patch( - 'requests.get', side_effect=[ConnectionError()] - ), patch('logging.exception'): + with self.mock_config_info({"key": "12345678"}), patch( + "requests.get", side_effect=[ConnectionError()] + ), patch("logging.exception"): self.verify_reply( - 'world without chocolate', - 'Uh oh, sorry :slightly_frowning_face:, I ' - 'cannot process your request right now. But, ' - 'let\'s try again later! :grin:', + "world without chocolate", + "Uh oh, sorry :slightly_frowning_face:, I " + "cannot process your request right now. But, " + "let's try again later! :grin:", ) diff --git a/zulip_bots/zulip_bots/bots/github_detail/github_detail.py b/zulip_bots/zulip_bots/bots/github_detail/github_detail.py index 0c71ad8..df30135 100644 --- a/zulip_bots/zulip_bots/bots/github_detail/github_detail.py +++ b/zulip_bots/zulip_bots/bots/github_detail/github_detail.py @@ -8,16 +8,16 @@ from zulip_bots.lib import BotHandler class GithubHandler: - ''' + """ This bot provides details on github issues and pull requests when they're referenced in the chat. - ''' + """ - GITHUB_ISSUE_URL_TEMPLATE = 'https://api.github.com/repos/{owner}/{repo}/issues/{id}' + GITHUB_ISSUE_URL_TEMPLATE = "https://api.github.com/repos/{owner}/{repo}/issues/{id}" HANDLE_MESSAGE_REGEX = re.compile(r"(?:([\w-]+)\/)?([\w-]+)?#(\d+)") def initialize(self, bot_handler: BotHandler) -> None: - self.config_info = bot_handler.get_config_info('github_detail', optional=True) + self.config_info = bot_handler.get_config_info("github_detail", optional=True) self.owner = self.config_info.get("owner", False) self.repo = self.config_info.get("repo", False) @@ -31,25 +31,25 @@ class GithubHandler: ) def format_message(self, details: Dict[str, Any]) -> str: - number = details['number'] - title = details['title'] - link = details['html_url'] - author = details['user']['login'] - owner = details['owner'] - repo = details['repo'] + number = details["number"] + title = details["title"] + link = details["html_url"] + author = details["user"]["login"] + owner = details["owner"] + repo = details["repo"] - description = details['body'] - status = details['state'].title() + description = details["body"] + status = details["state"].title() message_string = ( - '**[{owner}/{repo}#{id}]'.format(owner=owner, repo=repo, id=number), - '({link}) - {title}**\n'.format(title=title, link=link), - 'Created by **[{author}](https://github.com/{author})**\n'.format(author=author), - 'Status - **{status}**\n```quote\n{description}\n```'.format( + "**[{owner}/{repo}#{id}]".format(owner=owner, repo=repo, id=number), + "({link}) - {title}**\n".format(title=title, link=link), + "Created by **[{author}](https://github.com/{author})**\n".format(author=author), + "Status - **{status}**\n```quote\n{description}\n```".format( status=status, description=description ), ) - return ''.join(message_string) + return "".join(message_string) def get_details_from_github( self, owner: str, repo: str, number: str @@ -77,16 +77,16 @@ class GithubHandler: def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: # Send help message - if message['content'] == 'help': + if message["content"] == "help": bot_handler.send_reply(message, self.usage()) return # Capture owner, repo, id - issue_prs = list(re.finditer(self.HANDLE_MESSAGE_REGEX, message['content'])) + issue_prs = list(re.finditer(self.HANDLE_MESSAGE_REGEX, message["content"])) bot_messages = [] if len(issue_prs) > 5: # We limit to 5 requests to prevent denial-of-service - bot_message = 'Please ask for <=5 links in any one request' + bot_message = "Please ask for <=5 links in any one request" bot_handler.send_reply(message, bot_message) return @@ -95,8 +95,8 @@ class GithubHandler: if owner and repo: details = self.get_details_from_github(owner, repo, issue_pr.group(3)) if details is not None: - details['owner'] = owner - details['repo'] = repo + details["owner"] = owner + details["repo"] = repo bot_messages.append(self.format_message(details)) else: bot_messages.append( @@ -108,7 +108,7 @@ class GithubHandler: bot_messages.append("Failed to detect owner and repository name.") if len(bot_messages) == 0: bot_messages.append("Failed to find any issue or PR.") - bot_message = '\n'.join(bot_messages) + bot_message = "\n".join(bot_messages) bot_handler.send_reply(message, bot_message) diff --git a/zulip_bots/zulip_bots/bots/github_detail/test_github_detail.py b/zulip_bots/zulip_bots/bots/github_detail/test_github_detail.py index a023a93..b555d55 100755 --- a/zulip_bots/zulip_bots/bots/github_detail/test_github_detail.py +++ b/zulip_bots/zulip_bots/bots/github_detail/test_github_detail.py @@ -3,8 +3,8 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests, StubBotHandler, get_b class TestGithubDetailBot(BotTestCase, DefaultTests): bot_name = "github_detail" - mock_config = {'owner': 'zulip', 'repo': 'zulip'} - empty_config = {'owner': '', 'repo': ''} + mock_config = {"owner": "zulip", "repo": "zulip"} + empty_config = {"owner": "", "repo": ""} # Overrides default test_bot_usage(). def test_bot_usage(self) -> None: @@ -14,79 +14,79 @@ class TestGithubDetailBot(BotTestCase, DefaultTests): with self.mock_config_info(self.mock_config): bot.initialize(bot_handler) - self.assertIn('displays details on github issues', bot.usage()) + self.assertIn("displays details on github issues", bot.usage()) # Override default function in BotTestCase def test_bot_responds_to_empty_message(self) -> None: with self.mock_config_info(self.mock_config): - self.verify_reply('', 'Failed to find any issue or PR.') + self.verify_reply("", "Failed to find any issue or PR.") def test_issue(self) -> None: - request = 'zulip/zulip#5365' + request = "zulip/zulip#5365" bot_response = ( - '**[zulip/zulip#5365](https://github.com/zulip/zulip/issues/5365)' - ' - frontend: Enable hot-reloading of CSS in development**\n' - 'Created by **[timabbott](https://github.com/timabbott)**\n' - 'Status - **Open**\n' - '```quote\n' - 'There\'s strong interest among folks working on the frontend in being ' - 'able to use the hot-reloading feature of webpack for managing our CSS.\r\n\r\n' - 'In order to do this, step 1 is to move our CSS minification pipeline ' - 'from django-pipeline to Webpack. \n```' + "**[zulip/zulip#5365](https://github.com/zulip/zulip/issues/5365)" + " - frontend: Enable hot-reloading of CSS in development**\n" + "Created by **[timabbott](https://github.com/timabbott)**\n" + "Status - **Open**\n" + "```quote\n" + "There's strong interest among folks working on the frontend in being " + "able to use the hot-reloading feature of webpack for managing our CSS.\r\n\r\n" + "In order to do this, step 1 is to move our CSS minification pipeline " + "from django-pipeline to Webpack. \n```" ) - with self.mock_http_conversation('test_issue'): + with self.mock_http_conversation("test_issue"): with self.mock_config_info(self.mock_config): self.verify_reply(request, bot_response) def test_pull_request(self) -> None: - request = 'zulip/zulip#5345' + request = "zulip/zulip#5345" bot_response = ( - '**[zulip/zulip#5345](https://github.com/zulip/zulip/pull/5345)' - ' - [WIP] modal: Replace bootstrap modal with custom modal class**\n' - 'Created by **[jackrzhang](https://github.com/jackrzhang)**\n' - 'Status - **Open**\n```quote\nAn interaction bug (#4811) ' - 'between our settings UI and the bootstrap modals breaks hotkey ' - 'support for `Esc` when multiple modals are open.\r\n\r\ntodo:\r\n[x]' - ' Create `Modal` class in `modal.js` (drafted by @brockwhittaker)\r\n[x]' - ' Reimplement change_email_modal utilizing `Modal` class\r\n[] Dump ' - 'using bootstrap for the account settings modal and all other modals,' - ' replace with `Modal` class\r\n[] Add hotkey support for closing the' - ' top modal for `Esc`\r\n\r\nThis should also be a helpful step in' - ' removing dependencies from Bootstrap.\n```' + "**[zulip/zulip#5345](https://github.com/zulip/zulip/pull/5345)" + " - [WIP] modal: Replace bootstrap modal with custom modal class**\n" + "Created by **[jackrzhang](https://github.com/jackrzhang)**\n" + "Status - **Open**\n```quote\nAn interaction bug (#4811) " + "between our settings UI and the bootstrap modals breaks hotkey " + "support for `Esc` when multiple modals are open.\r\n\r\ntodo:\r\n[x]" + " Create `Modal` class in `modal.js` (drafted by @brockwhittaker)\r\n[x]" + " Reimplement change_email_modal utilizing `Modal` class\r\n[] Dump " + "using bootstrap for the account settings modal and all other modals," + " replace with `Modal` class\r\n[] Add hotkey support for closing the" + " top modal for `Esc`\r\n\r\nThis should also be a helpful step in" + " removing dependencies from Bootstrap.\n```" ) - with self.mock_http_conversation('test_pull'): + with self.mock_http_conversation("test_pull"): with self.mock_config_info(self.mock_config): self.verify_reply(request, bot_response) def test_404(self) -> None: - request = 'zulip/zulip#0' - bot_response = 'Failed to find issue/pr: zulip/zulip#0' - with self.mock_http_conversation('test_404'): + request = "zulip/zulip#0" + bot_response = "Failed to find issue/pr: zulip/zulip#0" + with self.mock_http_conversation("test_404"): with self.mock_config_info(self.mock_config): self.verify_reply(request, bot_response) def test_exception(self) -> None: - request = 'zulip/zulip#0' - bot_response = 'Failed to find issue/pr: zulip/zulip#0' + request = "zulip/zulip#0" + bot_response = "Failed to find issue/pr: zulip/zulip#0" with self.mock_request_exception(): with self.mock_config_info(self.mock_config): self.verify_reply(request, bot_response) def test_random_text(self) -> None: - request = 'some random text' - bot_response = 'Failed to find any issue or PR.' + request = "some random text" + bot_response = "Failed to find any issue or PR." with self.mock_config_info(self.mock_config): self.verify_reply(request, bot_response) def test_help_text(self) -> None: - request = 'help' + request = "help" bot_response = ( - 'This plugin displays details on github issues and pull requests. ' - 'To reference an issue or pull request usename mention the bot then ' - 'anytime in the message type its id, for example:\n@**Github detail** ' - '#3212 zulip#3212 zulip/zulip#3212\nThe default owner is zulip and ' - 'the default repo is zulip.' + "This plugin displays details on github issues and pull requests. " + "To reference an issue or pull request usename mention the bot then " + "anytime in the message type its id, for example:\n@**Github detail** " + "#3212 zulip#3212 zulip/zulip#3212\nThe default owner is zulip and " + "the default repo is zulip." ) with self.mock_config_info(self.mock_config): @@ -94,56 +94,56 @@ class TestGithubDetailBot(BotTestCase, DefaultTests): def test_too_many_request(self) -> None: request = ( - 'zulip/zulip#1 zulip/zulip#1 zulip/zulip#1 zulip/zulip#1 ' - 'zulip/zulip#1 zulip/zulip#1 zulip/zulip#1 zulip/zulip#1' + "zulip/zulip#1 zulip/zulip#1 zulip/zulip#1 zulip/zulip#1 " + "zulip/zulip#1 zulip/zulip#1 zulip/zulip#1 zulip/zulip#1" ) - bot_response = 'Please ask for <=5 links in any one request' + bot_response = "Please ask for <=5 links in any one request" with self.mock_config_info(self.mock_config): self.verify_reply(request, bot_response) def test_owner_and_repo_not_specified(self) -> None: - request = '/#1' - bot_response = 'Failed to detect owner and repository name.' + request = "/#1" + bot_response = "Failed to detect owner and repository name." with self.mock_config_info(self.empty_config): self.verify_reply(request, bot_response) def test_owner_and_repo_specified_in_config_file(self) -> None: - request = '/#5345' + request = "/#5345" bot_response = ( - '**[zulip/zulip#5345](https://github.com/zulip/zulip/pull/5345)' - ' - [WIP] modal: Replace bootstrap modal with custom modal class**\n' - 'Created by **[jackrzhang](https://github.com/jackrzhang)**\n' - 'Status - **Open**\n```quote\nAn interaction bug (#4811) ' - 'between our settings UI and the bootstrap modals breaks hotkey ' - 'support for `Esc` when multiple modals are open.\r\n\r\ntodo:\r\n[x]' - ' Create `Modal` class in `modal.js` (drafted by @brockwhittaker)\r\n[x]' - ' Reimplement change_email_modal utilizing `Modal` class\r\n[] Dump ' - 'using bootstrap for the account settings modal and all other modals,' - ' replace with `Modal` class\r\n[] Add hotkey support for closing the' - ' top modal for `Esc`\r\n\r\nThis should also be a helpful step in' - ' removing dependencies from Bootstrap.\n```' + "**[zulip/zulip#5345](https://github.com/zulip/zulip/pull/5345)" + " - [WIP] modal: Replace bootstrap modal with custom modal class**\n" + "Created by **[jackrzhang](https://github.com/jackrzhang)**\n" + "Status - **Open**\n```quote\nAn interaction bug (#4811) " + "between our settings UI and the bootstrap modals breaks hotkey " + "support for `Esc` when multiple modals are open.\r\n\r\ntodo:\r\n[x]" + " Create `Modal` class in `modal.js` (drafted by @brockwhittaker)\r\n[x]" + " Reimplement change_email_modal utilizing `Modal` class\r\n[] Dump " + "using bootstrap for the account settings modal and all other modals," + " replace with `Modal` class\r\n[] Add hotkey support for closing the" + " top modal for `Esc`\r\n\r\nThis should also be a helpful step in" + " removing dependencies from Bootstrap.\n```" ) - with self.mock_http_conversation('test_pull'): + with self.mock_http_conversation("test_pull"): with self.mock_config_info(self.mock_config): self.verify_reply(request, bot_response) def test_owner_and_repo_specified_in_message(self) -> None: - request = 'zulip/zulip#5345' + request = "zulip/zulip#5345" bot_response = ( - '**[zulip/zulip#5345](https://github.com/zulip/zulip/pull/5345)' - ' - [WIP] modal: Replace bootstrap modal with custom modal class**\n' - 'Created by **[jackrzhang](https://github.com/jackrzhang)**\n' - 'Status - **Open**\n```quote\nAn interaction bug (#4811) ' - 'between our settings UI and the bootstrap modals breaks hotkey ' - 'support for `Esc` when multiple modals are open.\r\n\r\ntodo:\r\n[x]' - ' Create `Modal` class in `modal.js` (drafted by @brockwhittaker)\r\n[x]' - ' Reimplement change_email_modal utilizing `Modal` class\r\n[] Dump ' - 'using bootstrap for the account settings modal and all other modals,' - ' replace with `Modal` class\r\n[] Add hotkey support for closing the' - ' top modal for `Esc`\r\n\r\nThis should also be a helpful step in' - ' removing dependencies from Bootstrap.\n```' + "**[zulip/zulip#5345](https://github.com/zulip/zulip/pull/5345)" + " - [WIP] modal: Replace bootstrap modal with custom modal class**\n" + "Created by **[jackrzhang](https://github.com/jackrzhang)**\n" + "Status - **Open**\n```quote\nAn interaction bug (#4811) " + "between our settings UI and the bootstrap modals breaks hotkey " + "support for `Esc` when multiple modals are open.\r\n\r\ntodo:\r\n[x]" + " Create `Modal` class in `modal.js` (drafted by @brockwhittaker)\r\n[x]" + " Reimplement change_email_modal utilizing `Modal` class\r\n[] Dump " + "using bootstrap for the account settings modal and all other modals," + " replace with `Modal` class\r\n[] Add hotkey support for closing the" + " top modal for `Esc`\r\n\r\nThis should also be a helpful step in" + " removing dependencies from Bootstrap.\n```" ) - with self.mock_http_conversation('test_pull'): + with self.mock_http_conversation("test_pull"): with self.mock_config_info(self.empty_config): self.verify_reply(request, bot_response) diff --git a/zulip_bots/zulip_bots/bots/google_search/google_search.py b/zulip_bots/zulip_bots/bots/google_search/google_search.py index 3d99465..a3cafc7 100644 --- a/zulip_bots/zulip_bots/bots/google_search/google_search.py +++ b/zulip_bots/zulip_bots/bots/google_search/google_search.py @@ -9,30 +9,30 @@ from zulip_bots.lib import BotHandler def google_search(keywords: str) -> List[Dict[str, str]]: - query = {'q': keywords} + query = {"q": keywords} # Gets the page - page = requests.get('http://www.google.com/search', params=query) + page = requests.get("http://www.google.com/search", params=query) # Parses the page into BeautifulSoup soup = BeautifulSoup(page.text, "lxml") # Gets all search URLs - anchors = soup.find(id='search').findAll('a') + anchors = soup.find(id="search").findAll("a") results = [] for a in anchors: try: # Tries to get the href property of the URL - link = a['href'] + link = a["href"] except KeyError: continue # Link must start with '/url?', as these are the search result links - if not link.startswith('/url?'): + if not link.startswith("/url?"): continue # Makes sure a hidden 'cached' result isn't displayed - if a.text.strip() == 'Cached' and 'webcache.googleusercontent.com' in a['href']: + if a.text.strip() == "Cached" and "webcache.googleusercontent.com" in a["href"]: continue # a.text: The name of the page - result = {'url': "https://www.google.com{}".format(link), 'name': a.text} + result = {"url": "https://www.google.com{}".format(link), "name": a.text} results.append(result) return results @@ -49,42 +49,42 @@ def get_google_result(search_keywords: str) -> str: search_keywords = search_keywords.strip() - if search_keywords == 'help': + if search_keywords == "help": return help_message - elif search_keywords == '' or search_keywords is None: + elif search_keywords == "" or search_keywords is None: return help_message else: try: results = google_search(search_keywords) if len(results) == 0: return "Found no results." - return "Found Result: [{}]({})".format(results[0]['name'], results[0]['url']) + return "Found Result: [{}]({})".format(results[0]["name"], results[0]["url"]) except Exception as e: logging.exception(str(e)) - return 'Error: Search failed. {}.'.format(e) + return "Error: Search failed. {}.".format(e) class GoogleSearchHandler: - ''' + """ This plugin allows users to enter a search term in Zulip and get the top URL sent back to the context (stream or private) in which it was called. It looks for messages starting with @mentioned-bot. - ''' + """ def usage(self) -> str: - return ''' + return """ This plugin will allow users to search for a given search term on Google from Zulip. Use '@mentioned-bot help' to get more information on the bot usage. Users should preface messages with @mentioned-bot. - ''' + """ def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: - original_content = message['content'] + original_content = message["content"] result = get_google_result(original_content) bot_handler.send_reply(message, result) diff --git a/zulip_bots/zulip_bots/bots/google_search/test_google_search.py b/zulip_bots/zulip_bots/bots/google_search/test_google_search.py index dbc4b24..c6f0b4e 100644 --- a/zulip_bots/zulip_bots/bots/google_search/test_google_search.py +++ b/zulip_bots/zulip_bots/bots/google_search/test_google_search.py @@ -4,14 +4,14 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests class TestGoogleSearchBot(BotTestCase, DefaultTests): - bot_name = 'google_search' + bot_name = "google_search" # Simple query def test_normal(self) -> None: - with self.mock_http_conversation('test_normal'): + with self.mock_http_conversation("test_normal"): self.verify_reply( - 'zulip', - 'Found Result: [Zulip](https://www.google.com/url?url=https%3A%2F%2Fzulip.com%2F)', + "zulip", + "Found Result: [Zulip](https://www.google.com/url?url=https%3A%2F%2Fzulip.com%2F)", ) def test_bot_help(self) -> None: @@ -23,26 +23,26 @@ class TestGoogleSearchBot(BotTestCase, DefaultTests): An example message that could be sent is:\ '@mentioned-bot zulip' or \ '@mentioned-bot how to create a chatbot'." - self.verify_reply('', help_message) - self.verify_reply('help', help_message) + self.verify_reply("", help_message) + self.verify_reply("help", help_message) def test_bot_no_results(self) -> None: - with self.mock_http_conversation('test_no_result'): - self.verify_reply('no res', 'Found no results.') + with self.mock_http_conversation("test_no_result"): + self.verify_reply("no res", "Found no results.") def test_attribute_error(self) -> None: - with self.mock_http_conversation('test_attribute_error'), patch('logging.exception'): + with self.mock_http_conversation("test_attribute_error"), patch("logging.exception"): self.verify_reply( - 'test', 'Error: Search failed. \'NoneType\' object has no attribute \'findAll\'.' + "test", "Error: Search failed. 'NoneType' object has no attribute 'findAll'." ) # Makes sure cached results, irrelevant links, or empty results are not displayed def test_ignore_links(self) -> None: - with self.mock_http_conversation('test_ignore_links'): + with self.mock_http_conversation("test_ignore_links"): # The bot should ignore all links, apart from the zulip link at the end (googlesearch.py lines 23-38) # Then it should send the zulip link # See test_ignore_links.json self.verify_reply( - 'zulip', - 'Found Result: [Zulip](https://www.google.com/url?url=https%3A%2F%2Fzulip.com%2F)', + "zulip", + "Found Result: [Zulip](https://www.google.com/url?url=https%3A%2F%2Fzulip.com%2F)", ) diff --git a/zulip_bots/zulip_bots/bots/google_translate/google_translate.py b/zulip_bots/zulip_bots/bots/google_translate/google_translate.py index fbed215..7552065 100644 --- a/zulip_bots/zulip_bots/bots/google_translate/google_translate.py +++ b/zulip_bots/zulip_bots/bots/google_translate/google_translate.py @@ -5,57 +5,57 @@ import requests class GoogleTranslateHandler: - ''' + """ This bot will translate any messages sent to it using google translate. Before using it, make sure you set up google api keys, and enable google cloud translate from the google cloud console. - ''' + """ def usage(self): - return ''' + return """ This plugin allows users translate messages Users should @-mention the bot with the format @-mention "" - ''' + """ def initialize(self, bot_handler): - self.config_info = bot_handler.get_config_info('googletranslate') + self.config_info = bot_handler.get_config_info("googletranslate") # Retrieving the supported languages also serves as a check whether # the bot is properly connected to the Google Translate API. try: - self.supported_languages = get_supported_languages(self.config_info['key']) + self.supported_languages = get_supported_languages(self.config_info["key"]) except TranslateError as e: bot_handler.quit(str(e)) def handle_message(self, message, bot_handler): bot_response = get_translate_bot_response( - message['content'], + message["content"], self.config_info, - message['sender_full_name'], + message["sender_full_name"], self.supported_languages, ) bot_handler.send_reply(message, bot_response) -api_url = 'https://translation.googleapis.com/language/translate/v2' +api_url = "https://translation.googleapis.com/language/translate/v2" -help_text = ''' +help_text = """ Google translate bot Please format your message like: `@-mention "" ` Visit [here](https://cloud.google.com/translate/docs/languages) for all languages -''' +""" -language_not_found_text = '{} language not found. Visit [here](https://cloud.google.com/translate/docs/languages) for all languages' +language_not_found_text = "{} language not found. Visit [here](https://cloud.google.com/translate/docs/languages) for all languages" def get_supported_languages(key): - parameters = {'key': key, 'target': 'en'} - response = requests.get(api_url + '/languages', params=parameters) + parameters = {"key": key, "target": "en"} + response = requests.get(api_url + "/languages", params=parameters) if response.status_code == requests.codes.ok: - languages = response.json()['data']['languages'] - return {lang['name'].lower(): lang['language'].lower() for lang in languages} - raise TranslateError(response.json()['error']['message']) + languages = response.json()["data"]["languages"] + return {lang["name"].lower(): lang["language"].lower() for lang in languages} + raise TranslateError(response.json()["error"]["message"]) class TranslateError(Exception): @@ -63,31 +63,31 @@ class TranslateError(Exception): def translate(text_to_translate, key, dest, src): - parameters = {'q': text_to_translate, 'target': dest, 'key': key} - if src != '': - parameters.update({'source': src}) + parameters = {"q": text_to_translate, "target": dest, "key": key} + if src != "": + parameters.update({"source": src}) response = requests.post(api_url, params=parameters) if response.status_code == requests.codes.ok: - return response.json()['data']['translations'][0]['translatedText'] - raise TranslateError(response.json()['error']['message']) + return response.json()["data"]["translations"][0]["translatedText"] + raise TranslateError(response.json()["error"]["message"]) def get_code_for_language(language, all_languages): if language.lower() not in all_languages.values(): if language.lower() not in all_languages.keys(): - return '' + return "" language = all_languages[language.lower()] return language def get_translate_bot_response(message_content, config_file, author, all_languages): message_content = message_content.strip() - if message_content == 'help' or message_content is None or not message_content.startswith('"'): + if message_content == "help" or message_content is None or not message_content.startswith('"'): return help_text split_text = message_content.rsplit('" ', 1) if len(split_text) == 1: return help_text - split_text += split_text.pop(1).split(' ') + split_text += split_text.pop(1).split(" ") if len(split_text) == 2: # There is no source language split_text.append("") @@ -96,15 +96,15 @@ def get_translate_bot_response(message_content, config_file, author, all_languag (text_to_translate, target_language, source_language) = split_text text_to_translate = text_to_translate[1:] target_language = get_code_for_language(target_language, all_languages) - if target_language == '': + if target_language == "": return language_not_found_text.format("Target") - if source_language != '': + if source_language != "": source_language = get_code_for_language(source_language, all_languages) - if source_language == '': + if source_language == "": return language_not_found_text.format("Source") try: translated_text = translate( - text_to_translate, config_file['key'], target_language, source_language + text_to_translate, config_file["key"], target_language, source_language ) except requests.exceptions.ConnectionError as conn_err: return "Could not connect to Google Translate. {}.".format(conn_err) diff --git a/zulip_bots/zulip_bots/bots/google_translate/test_google_translate.py b/zulip_bots/zulip_bots/bots/google_translate/test_google_translate.py index b51076e..f40ef6f 100644 --- a/zulip_bots/zulip_bots/bots/google_translate/test_google_translate.py +++ b/zulip_bots/zulip_bots/bots/google_translate/test_google_translate.py @@ -4,19 +4,19 @@ from requests.exceptions import ConnectionError from zulip_bots.test_lib import BotTestCase, DefaultTests, StubBotHandler -help_text = ''' +help_text = """ Google translate bot Please format your message like: `@-mention "" ` Visit [here](https://cloud.google.com/translate/docs/languages) for all languages -''' +""" class TestGoogleTranslateBot(BotTestCase, DefaultTests): bot_name = "google_translate" def _test(self, message, response, http_config_fixture, http_fixture=None): - with self.mock_config_info({'key': 'abcdefg'}), self.mock_http_conversation( + with self.mock_config_info({"key": "abcdefg"}), self.mock_http_conversation( http_config_fixture ): if http_fixture: @@ -26,71 +26,71 @@ class TestGoogleTranslateBot(BotTestCase, DefaultTests): self.verify_reply(message, response) def test_normal(self): - self._test('"hello" de', 'Hallo (from Foo Test User)', 'test_languages', 'test_normal') + self._test('"hello" de', "Hallo (from Foo Test User)", "test_languages", "test_normal") def test_source_language_not_found(self): self._test( '"hello" german foo', ( - 'Source language not found. Visit [here]' - '(https://cloud.google.com/translate/docs/languages) for all languages' + "Source language not found. Visit [here]" + "(https://cloud.google.com/translate/docs/languages) for all languages" ), - 'test_languages', + "test_languages", ) def test_target_language_not_found(self): self._test( '"hello" bar english', ( - 'Target language not found. Visit [here]' - '(https://cloud.google.com/translate/docs/languages) for all languages' + "Target language not found. Visit [here]" + "(https://cloud.google.com/translate/docs/languages) for all languages" ), - 'test_languages', + "test_languages", ) def test_403(self): self._test( '"hello" german english', - 'Translate Error. Invalid API Key..', - 'test_languages', - 'test_403', + "Translate Error. Invalid API Key..", + "test_languages", + "test_403", ) # Override default function in BotTestCase def test_bot_responds_to_empty_message(self): - self._test('', help_text, 'test_languages') + self._test("", help_text, "test_languages") def test_help_command(self): - self._test('help', help_text, 'test_languages') + self._test("help", help_text, "test_languages") def test_help_too_many_args(self): - self._test('"hello" de english foo bar', help_text, 'test_languages') + self._test('"hello" de english foo bar', help_text, "test_languages") def test_help_no_langs(self): - self._test('"hello"', help_text, 'test_languages') + self._test('"hello"', help_text, "test_languages") def test_quotation_in_text(self): self._test( '"this has "quotation" marks in" english', 'this has "quotation" marks in (from Foo Test User)', - 'test_languages', - 'test_quotation', + "test_languages", + "test_quotation", ) def test_exception(self): with patch( - 'zulip_bots.bots.google_translate.google_translate.translate', side_effect=Exception + "zulip_bots.bots.google_translate.google_translate.translate", side_effect=Exception ): - self._test('"hello" de', 'Error. .', 'test_languages') + self._test('"hello" de', "Error. .", "test_languages") def test_invalid_api_key(self): with self.assertRaises(StubBotHandler.BotQuitException): - self._test(None, None, 'test_invalid_api_key') + self._test(None, None, "test_invalid_api_key") def test_api_access_not_configured(self): with self.assertRaises(StubBotHandler.BotQuitException): - self._test(None, None, 'test_api_access_not_configured') + self._test(None, None, "test_api_access_not_configured") def test_connection_error(self): - with patch('requests.post', side_effect=ConnectionError()), patch('logging.warning'): - self._test('"test" en', 'Could not connect to Google Translate. .', 'test_languages') + with patch("requests.post", side_effect=ConnectionError()), patch("logging.warning"): + self._test('"test" en', "Could not connect to Google Translate. .", "test_languages") diff --git a/zulip_bots/zulip_bots/bots/helloworld/helloworld.py b/zulip_bots/zulip_bots/bots/helloworld/helloworld.py index a012ec0..79e9437 100644 --- a/zulip_bots/zulip_bots/bots/helloworld/helloworld.py +++ b/zulip_bots/zulip_bots/bots/helloworld/helloworld.py @@ -7,19 +7,19 @@ from zulip_bots.lib import BotHandler class HelloWorldHandler: def usage(self) -> str: - return ''' + return """ This is a boilerplate bot that responds to a user query with "beep boop", which is robot for "Hello World". This bot can be used as a template for other, more sophisticated, bots. - ''' + """ def handle_message(self, message: Dict[str, Any], bot_handler: BotHandler) -> None: - content = 'beep boop' # type: str + content = "beep boop" # type: str bot_handler.send_reply(message, content) - emoji_name = 'wave' # type: str + emoji_name = "wave" # type: str bot_handler.react(message, emoji_name) return diff --git a/zulip_bots/zulip_bots/bots/helloworld/test_helloworld.py b/zulip_bots/zulip_bots/bots/helloworld/test_helloworld.py index 442dcaf..c3cd804 100755 --- a/zulip_bots/zulip_bots/bots/helloworld/test_helloworld.py +++ b/zulip_bots/zulip_bots/bots/helloworld/test_helloworld.py @@ -6,9 +6,9 @@ class TestHelpBot(BotTestCase, DefaultTests): def test_bot(self) -> None: dialog = [ - ('', 'beep boop'), - ('help', 'beep boop'), - ('foo', 'beep boop'), + ("", "beep boop"), + ("help", "beep boop"), + ("foo", "beep boop"), ] self.verify_dialog(dialog) diff --git a/zulip_bots/zulip_bots/bots/help/help.py b/zulip_bots/zulip_bots/bots/help/help.py index 96a3bd5..1d95112 100644 --- a/zulip_bots/zulip_bots/bots/help/help.py +++ b/zulip_bots/zulip_bots/bots/help/help.py @@ -6,14 +6,14 @@ from zulip_bots.lib import BotHandler class HelpHandler: def usage(self) -> str: - return ''' + return """ This plugin will give info about Zulip to any user that types a message saying "help". This is example code; ideally, you would flesh this out for more useful help pertaining to your Zulip instance. - ''' + """ def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: help_content = "Info on Zulip can be found here:\nhttps://github.com/zulip/zulip" diff --git a/zulip_bots/zulip_bots/bots/idonethis/idonethis.py b/zulip_bots/zulip_bots/bots/idonethis/idonethis.py index 98a740c..870d17d 100644 --- a/zulip_bots/zulip_bots/bots/idonethis/idonethis.py +++ b/zulip_bots/zulip_bots/bots/idonethis/idonethis.py @@ -36,7 +36,7 @@ def make_API_request( body: Optional[Dict[str, str]] = None, params: Optional[Dict[str, str]] = None, ) -> Any: - headers = {'Authorization': 'Token ' + api_key} + headers = {"Authorization": "Token " + api_key} if method == "GET": r = requests.get(API_BASE_URL + endpoint, headers=headers, params=params) elif method == "POST": @@ -45,13 +45,13 @@ def make_API_request( return r.json() elif ( r.status_code == 401 - and 'error' in r.json() - and r.json()['error'] == "Invalid API Authentication" + and "error" in r.json() + and r.json()["error"] == "Invalid API Authentication" ): - logging.error('Error authenticating, please check key ' + str(r.url)) + logging.error("Error authenticating, please check key " + str(r.url)) raise AuthenticationException() else: - logging.error('Error make API request, code ' + str(r.status_code) + '. json: ' + r.json()) + logging.error("Error make API request, code " + str(r.status_code) + ". json: " + r.json()) raise UnspecifiedProblemException() @@ -84,14 +84,14 @@ def api_create_entry(body: str, team_id: str) -> Dict[str, Any]: def list_teams() -> str: - response = ["Teams:"] + [" * " + team['name'] for team in api_list_team()] + response = ["Teams:"] + [" * " + team["name"] for team in api_list_team()] return "\n".join(response) def get_team_hash(team_name: str) -> str: for team in api_list_team(): - if team['name'].lower() == team_name.lower() or team['hash_id'] == team_name: - return team['hash_id'] + if team["name"].lower() == team_name.lower() or team["hash_id"] == team_name: + return team["hash_id"] raise TeamNotFoundException(team_name) @@ -120,7 +120,7 @@ def entries_list(team_name: str) -> str: " * Team: {teamname}", " * ID: {hash_id}", ] - ).format(username=entry['user']['full_name'], teamname=entry['team']['name'], **entry) + ).format(username=entry["user"]["full_name"], teamname=entry["team"]["name"], **entry) return response @@ -151,15 +151,15 @@ More information in my help""" team_id = get_team_hash(team) data = api_create_entry(new_message, team_id) - return "Great work :thumbs_up:. New entry `{}` created!".format(data['body_formatted']) + return "Great work :thumbs_up:. New entry `{}` created!".format(data["body_formatted"]) class IDoneThisHandler: def initialize(self, bot_handler: BotHandler) -> None: global api_key, default_team - self.config_info = bot_handler.get_config_info('idonethis') - if 'api_key' in self.config_info: - api_key = self.config_info['api_key'] + self.config_info = bot_handler.get_config_info("idonethis") + if "api_key" in self.config_info: + api_key = self.config_info["api_key"] else: logging.error("An API key must be specified for this bot to run.") logging.error( @@ -167,8 +167,8 @@ class IDoneThisHandler: ) bot_handler.quit() - if 'default_team' in self.config_info: - default_team = self.config_info['default_team'] + if "default_team" in self.config_info: + default_team = self.config_info["default_team"] else: logging.error( "Cannot find default team. Users will need to manually specify a team each time an entry is created." @@ -192,7 +192,7 @@ class IDoneThisHandler: else: default_team_message = "There is currently no default team set up :frowning:." return ( - ''' + """ This bot allows for interaction with idonethis, a collaboration tool to increase a team's productivity. Below are some of the commands you can use, and what they do. @@ -211,7 +211,7 @@ Below are some of the commands you can use, and what they do. Create a new entry. Optionally supply `--team=` for teams with no spaces or `"--team="` for teams with spaces. For example `@mention i did "--team=product team" something` will create a new entry `something` for the product team. - ''' + """ + default_team_message ) @@ -219,7 +219,7 @@ Below are some of the commands you can use, and what they do. bot_handler.send_reply(message, self.get_response(message)) def get_response(self, message: Dict[str, Any]) -> str: - message_content = message['content'].strip().split() + message_content = message["content"].strip().split() reply = "" try: command = " ".join(message_content[:2]) diff --git a/zulip_bots/zulip_bots/bots/idonethis/test_idonethis.py b/zulip_bots/zulip_bots/bots/idonethis/test_idonethis.py index 2524f6e..b3399b3 100644 --- a/zulip_bots/zulip_bots/bots/idonethis/test_idonethis.py +++ b/zulip_bots/zulip_bots/bots/idonethis/test_idonethis.py @@ -8,116 +8,116 @@ class TestIDoneThisBot(BotTestCase, DefaultTests): def test_create_entry_default_team(self) -> None: with self.mock_config_info( - {'api_key': '12345678', 'default_team': 'testing team 1'} - ), self.mock_http_conversation('test_create_entry'), self.mock_http_conversation( - 'team_list' + {"api_key": "12345678", "default_team": "testing team 1"} + ), self.mock_http_conversation("test_create_entry"), self.mock_http_conversation( + "team_list" ): self.verify_reply( - 'i did something and something else', - 'Great work :thumbs_up:. New entry `something and something else` created!', + "i did something and something else", + "Great work :thumbs_up:. New entry `something and something else` created!", ) def test_create_entry_quoted_team(self) -> None: with self.mock_config_info( - {'api_key': '12345678', 'default_team': 'test_team_2'} - ), self.mock_http_conversation('test_create_entry'), self.mock_http_conversation( - 'team_list' + {"api_key": "12345678", "default_team": "test_team_2"} + ), self.mock_http_conversation("test_create_entry"), self.mock_http_conversation( + "team_list" ): self.verify_reply( 'i did something and something else "--team=testing team 1"', - 'Great work :thumbs_up:. New entry `something and something else` created!', + "Great work :thumbs_up:. New entry `something and something else` created!", ) def test_create_entry_single_word_team(self) -> None: with self.mock_config_info( - {'api_key': '12345678', 'default_team': 'testing team 1'} - ), self.mock_http_conversation('test_create_entry_team_2'), self.mock_http_conversation( - 'team_list' + {"api_key": "12345678", "default_team": "testing team 1"} + ), self.mock_http_conversation("test_create_entry_team_2"), self.mock_http_conversation( + "team_list" ): self.verify_reply( - 'i did something and something else --team=test_team_2', - 'Great work :thumbs_up:. New entry `something and something else` created!', + "i did something and something else --team=test_team_2", + "Great work :thumbs_up:. New entry `something and something else` created!", ) def test_bad_key(self) -> None: with self.mock_config_info( - {'api_key': '87654321', 'default_team': 'testing team 1'} - ), self.mock_http_conversation('test_401'), patch( - 'zulip_bots.bots.idonethis.idonethis.api_noop' + {"api_key": "87654321", "default_team": "testing team 1"} + ), self.mock_http_conversation("test_401"), patch( + "zulip_bots.bots.idonethis.idonethis.api_noop" ), patch( - 'logging.error' + "logging.error" ): self.verify_reply( - 'list teams', - 'I can\'t currently authenticate with idonethis. Can you check that your API key is correct? ' - 'For more information see my documentation.', + "list teams", + "I can't currently authenticate with idonethis. Can you check that your API key is correct? " + "For more information see my documentation.", ) def test_list_team(self) -> None: with self.mock_config_info( - {'api_key': '12345678', 'default_team': 'testing team 1'} - ), self.mock_http_conversation('team_list'): - self.verify_reply('list teams', 'Teams:\n * testing team 1\n * test_team_2') + {"api_key": "12345678", "default_team": "testing team 1"} + ), self.mock_http_conversation("team_list"): + self.verify_reply("list teams", "Teams:\n * testing team 1\n * test_team_2") def test_show_team_no_team(self) -> None: with self.mock_config_info( - {'api_key': '12345678', 'default_team': 'testing team 1'} - ), self.mock_http_conversation('api_noop'): + {"api_key": "12345678", "default_team": "testing team 1"} + ), self.mock_http_conversation("api_noop"): self.verify_reply( - 'team info', - 'Sorry, I don\'t understand what your trying to say. Use `@mention help` to see my help. ' - 'You must specify the team in which you request information from.', + "team info", + "Sorry, I don't understand what your trying to say. Use `@mention help` to see my help. " + "You must specify the team in which you request information from.", ) def test_show_team(self) -> None: with self.mock_config_info( - {'api_key': '12345678', 'default_team': 'testing team 1'} - ), self.mock_http_conversation('test_show_team'), patch( - 'zulip_bots.bots.idonethis.idonethis.get_team_hash', return_value='31415926535' + {"api_key": "12345678", "default_team": "testing team 1"} + ), self.mock_http_conversation("test_show_team"), patch( + "zulip_bots.bots.idonethis.idonethis.get_team_hash", return_value="31415926535" ) as get_team_hashFunction: self.verify_reply( - 'team info testing team 1', - 'Team Name: testing team 1\n' - 'ID: `31415926535`\n' - 'Created at: 2017-12-28T19:12:55.121+11:00', + "team info testing team 1", + "Team Name: testing team 1\n" + "ID: `31415926535`\n" + "Created at: 2017-12-28T19:12:55.121+11:00", ) - get_team_hashFunction.assert_called_with('testing team 1') + get_team_hashFunction.assert_called_with("testing team 1") def test_entries_list(self) -> None: with self.mock_config_info( - {'api_key': '12345678', 'default_team': 'testing team 1'} - ), self.mock_http_conversation('test_entries_list'), patch( - 'zulip_bots.bots.idonethis.idonethis.get_team_hash', return_value='31415926535' + {"api_key": "12345678", "default_team": "testing team 1"} + ), self.mock_http_conversation("test_entries_list"), patch( + "zulip_bots.bots.idonethis.idonethis.get_team_hash", return_value="31415926535" ): self.verify_reply( - 'entries list testing team 1', - 'Entries for testing team 1:\n' - ' * TESTING\n' - ' * Created at: 2018-01-04T21:10:13.084+11:00\n' - ' * Status: done\n' - ' * User: John Doe\n' - ' * Team: testing team 1\n' - ' * ID: 65e1b21fd8f63adede1daae0bdf28c0e47b84923\n' - ' * Grabbing some more data...\n' - ' * Created at: 2018-01-04T20:07:58.078+11:00\n' - ' * Status: done\n' - ' * User: John Doe\n' - ' * Team: testing team 1\n' - ' * ID: fa974ad8c1acb9e81361a051a697f9dae22908d6\n' - ' * GRABBING HTTP DATA\n' - ' * Created at: 2018-01-04T19:07:17.214+11:00\n' - ' * Status: done\n' - ' * User: John Doe\n' - ' * Team: testing team 1\n' - ' * ID: 72c8241d2218464433268c5abd6625ac104e3d8f', + "entries list testing team 1", + "Entries for testing team 1:\n" + " * TESTING\n" + " * Created at: 2018-01-04T21:10:13.084+11:00\n" + " * Status: done\n" + " * User: John Doe\n" + " * Team: testing team 1\n" + " * ID: 65e1b21fd8f63adede1daae0bdf28c0e47b84923\n" + " * Grabbing some more data...\n" + " * Created at: 2018-01-04T20:07:58.078+11:00\n" + " * Status: done\n" + " * User: John Doe\n" + " * Team: testing team 1\n" + " * ID: fa974ad8c1acb9e81361a051a697f9dae22908d6\n" + " * GRABBING HTTP DATA\n" + " * Created at: 2018-01-04T19:07:17.214+11:00\n" + " * Status: done\n" + " * User: John Doe\n" + " * Team: testing team 1\n" + " * ID: 72c8241d2218464433268c5abd6625ac104e3d8f", ) def test_bot_responds_to_empty_message(self) -> None: with self.mock_config_info( - {'api_key': '12345678', 'bot_info': 'team'} - ), self.mock_http_conversation('api_noop'): + {"api_key": "12345678", "bot_info": "team"} + ), self.mock_http_conversation("api_noop"): self.verify_reply( - '', - 'Sorry, I don\'t understand what your trying to say. Use `@mention help` to see my help. ' - 'I can\'t understand the command you sent me :confused: ', + "", + "Sorry, I don't understand what your trying to say. Use `@mention help` to see my help. " + "I can't understand the command you sent me :confused: ", ) diff --git a/zulip_bots/zulip_bots/bots/incident/incident.py b/zulip_bots/zulip_bots/bots/incident/incident.py index e1588ec..9a78fc2 100644 --- a/zulip_bots/zulip_bots/bots/incident/incident.py +++ b/zulip_bots/zulip_bots/bots/incident/incident.py @@ -4,13 +4,13 @@ from typing import Any, Dict, Tuple from zulip_bots.lib import BotHandler -QUESTION = 'How should we handle this?' +QUESTION = "How should we handle this?" ANSWERS = { - '1': 'known issue', - '2': 'ignore', - '3': 'in process', - '4': 'escalate', + "1": "known issue", + "2": "ignore", + "3": "in process", + "4": "escalate", } @@ -20,26 +20,26 @@ class InvalidAnswerException(Exception): class IncidentHandler: def usage(self) -> str: - return ''' + return """ This plugin lets folks reports incidents and triage them. It is intended to be sample code. In the real world you'd modify this code to talk to some kind of issue tracking system. But the glue code here should be pretty portable. - ''' + """ def handle_message(self, message: Dict[str, Any], bot_handler: BotHandler) -> None: - query = message['content'] - if query.startswith('new '): + query = message["content"] + if query.startswith("new "): start_new_incident(query, message, bot_handler) - elif query.startswith('answer '): + elif query.startswith("answer "): try: (ticket_id, answer) = parse_answer(query) except InvalidAnswerException: - bot_response = 'Invalid answer format' + bot_response = "Invalid answer format" bot_handler.send_reply(message, bot_response) return - bot_response = 'Incident %s\n status = %s' % (ticket_id, answer) + bot_response = "Incident %s\n status = %s" % (ticket_id, answer) bot_handler.send_reply(message, bot_response) else: bot_response = 'type "new " for a new incident' @@ -51,7 +51,7 @@ def start_new_incident(query: str, message: Dict[str, Any], bot_handler: BotHand # system. We just simulate everything by having an incident id that # we generate here. - incident = query[len('new ') :] + incident = query[len("new ") :] ticket_id = generate_ticket_id(bot_handler.storage) bot_response = format_incident_for_markdown(ticket_id, incident) @@ -61,7 +61,7 @@ def start_new_incident(query: str, message: Dict[str, Any], bot_handler: BotHand def parse_answer(query: str) -> Tuple[str, str]: - m = re.match(r'answer\s+(TICKET....)\s+(.)', query) + m = re.match(r"answer\s+(TICKET....)\s+(.)", query) if not m: raise InvalidAnswerException() @@ -73,7 +73,7 @@ def parse_answer(query: str) -> Tuple[str, str]: # of systems that specialize in incident management.) answer = m.group(2).upper() - if answer not in '1234': + if answer not in "1234": raise InvalidAnswerException() return (ticket_id, ANSWERS[answer]) @@ -81,36 +81,36 @@ def parse_answer(query: str) -> Tuple[str, str]: def generate_ticket_id(storage: Any) -> str: try: - incident_num = storage.get('ticket_id') + incident_num = storage.get("ticket_id") except (KeyError): incident_num = 0 incident_num += 1 incident_num = incident_num % (1000) - storage.put('ticket_id', incident_num) - ticket_id = 'TICKET%04d' % (incident_num,) + storage.put("ticket_id", incident_num) + ticket_id = "TICKET%04d" % (incident_num,) return ticket_id def format_incident_for_widget(ticket_id: str, incident: Dict[str, Any]) -> str: - widget_type = 'zform' + widget_type = "zform" - heading = ticket_id + ': ' + incident + heading = ticket_id + ": " + incident def get_choice(code: str) -> Dict[str, str]: answer = ANSWERS[code] - reply = 'answer ' + ticket_id + ' ' + code + reply = "answer " + ticket_id + " " + code return dict( - type='multiple_choice', + type="multiple_choice", short_name=code, long_name=answer, reply=reply, ) - choices = [get_choice(code) for code in '1234'] + choices = [get_choice(code) for code in "1234"] extra_data = dict( - type='choices', + type="choices", heading=heading, choices=choices, ) @@ -124,23 +124,23 @@ def format_incident_for_widget(ticket_id: str, incident: Dict[str, Any]) -> str: def format_incident_for_markdown(ticket_id: str, incident: Dict[str, Any]) -> str: - answer_list = '\n'.join( + answer_list = "\n".join( [ - '* **{code}** {answer}'.format( + "* **{code}** {answer}".format( code=code, answer=ANSWERS[code], ) - for code in '1234' + for code in "1234" ] ) - how_to_respond = '''**reply**: answer {ticket_id} '''.format(ticket_id=ticket_id) + how_to_respond = """**reply**: answer {ticket_id} """.format(ticket_id=ticket_id) - content = ''' + content = """ Incident: {incident} Q: {question} {answer_list} -{how_to_respond}'''.format( +{how_to_respond}""".format( question=QUESTION, answer_list=answer_list, how_to_respond=how_to_respond, diff --git a/zulip_bots/zulip_bots/bots/incrementor/incrementor.py b/zulip_bots/zulip_bots/bots/incrementor/incrementor.py index aec5060..ea42a08 100644 --- a/zulip_bots/zulip_bots/bots/incrementor/incrementor.py +++ b/zulip_bots/zulip_bots/bots/incrementor/incrementor.py @@ -7,46 +7,46 @@ from zulip_bots.lib import BotHandler, use_storage class IncrementorHandler: META = { - 'name': 'Incrementor', - 'description': 'Example bot to test the update_message() function.', + "name": "Incrementor", + "description": "Example bot to test the update_message() function.", } def usage(self) -> str: - return ''' + return """ This is a boilerplate bot that makes use of the update_message function. For the first @-mention, it initially replies with one message containing a `1`. Every time the bot is @-mentioned, this number will be incremented in the same message. - ''' + """ def initialize(self, bot_handler: BotHandler) -> None: storage = bot_handler.storage - if not storage.contains('number') or not storage.contains('message_id'): - storage.put('number', 0) - storage.put('message_id', None) + if not storage.contains("number") or not storage.contains("message_id"): + storage.put("number", 0) + storage.put("message_id", None) def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: - with use_storage(bot_handler.storage, ['number']) as storage: + with use_storage(bot_handler.storage, ["number"]) as storage: num = storage.get("number") # num should already be an int, but we do `int()` to force an # explicit type check num = int(num) + 1 - storage.put('number', num) - if storage.get('message_id') is not None: + storage.put("number", num) + if storage.get("message_id") is not None: result = bot_handler.update_message( - dict(message_id=storage.get('message_id'), content=str(num)) + dict(message_id=storage.get("message_id"), content=str(num)) ) # When there isn't an error while updating the message, we won't # attempt to send the it again. - if result is None or result.get('result') != 'error': + if result is None or result.get("result") != "error": return message_info = bot_handler.send_reply(message, str(num)) if message_info is not None: - storage.put('message_id', message_info['id']) + storage.put("message_id", message_info["id"]) handler_class = IncrementorHandler diff --git a/zulip_bots/zulip_bots/bots/incrementor/test_incrementor.py b/zulip_bots/zulip_bots/bots/incrementor/test_incrementor.py index 0471c1f..f1100a0 100644 --- a/zulip_bots/zulip_bots/bots/incrementor/test_incrementor.py +++ b/zulip_bots/zulip_bots/bots/incrementor/test_incrementor.py @@ -10,36 +10,36 @@ class TestIncrementorBot(BotTestCase, DefaultTests): bot = get_bot_message_handler(self.bot_name) bot_handler = StubBotHandler() - message = dict(type='stream') + message = dict(type="stream") bot.initialize(bot_handler) bot.handle_message(message, bot_handler) - with patch('zulip_bots.simple_lib.MockMessageServer.update') as m: + with patch("zulip_bots.simple_lib.MockMessageServer.update") as m: bot.handle_message(message, bot_handler) bot.handle_message(message, bot_handler) bot.handle_message(message, bot_handler) - content_updates = [item[0][0]['content'] for item in m.call_args_list] - self.assertEqual(content_updates, ['2', '3', '4']) + content_updates = [item[0][0]["content"] for item in m.call_args_list] + self.assertEqual(content_updates, ["2", "3", "4"]) def test_bot_edit_timeout(self) -> None: bot = get_bot_message_handler(self.bot_name) bot_handler = StubBotHandler() - message = dict(type='stream') + message = dict(type="stream") bot.initialize(bot_handler) bot.handle_message(message, bot_handler) - error_msg = dict(msg='The time limit for editing this message has passed', result='error') - with patch('zulip_bots.test_lib.StubBotHandler.update_message', return_value=error_msg): - with patch('zulip_bots.simple_lib.MockMessageServer.send') as m: + error_msg = dict(msg="The time limit for editing this message has passed", result="error") + with patch("zulip_bots.test_lib.StubBotHandler.update_message", return_value=error_msg): + with patch("zulip_bots.simple_lib.MockMessageServer.send") as m: bot.handle_message(message, bot_handler) bot.handle_message(message, bot_handler) # When there is an error, the bot should resend the message with the new value. self.assertEqual(m.call_count, 2) - content_updates = [item[0][0]['content'] for item in m.call_args_list] - self.assertEqual(content_updates, ['2', '3']) + content_updates = [item[0][0]["content"] for item in m.call_args_list] + self.assertEqual(content_updates, ["2", "3"]) diff --git a/zulip_bots/zulip_bots/bots/jira/jira.py b/zulip_bots/zulip_bots/bots/jira/jira.py index 903e8dc..8176e7a 100644 --- a/zulip_bots/zulip_bots/bots/jira/jira.py +++ b/zulip_bots/zulip_bots/bots/jira/jira.py @@ -16,7 +16,7 @@ CREATE_REGEX = re.compile( '( with priority "(?P.+?)")?' '( labeled "(?P.+?)")?' '( due "(?P.+?)")?' - '$' + "$" ) EDIT_REGEX = re.compile( 'edit issue "(?P.+?)"' @@ -28,13 +28,13 @@ EDIT_REGEX = re.compile( '( to use priority "(?P.+?)")?' '( by labeling "(?P.+?)")?' '( by making due "(?P.+?)")?' - '$' + "$" ) SEARCH_REGEX = re.compile('search "(?P.+)"$') JQL_REGEX = re.compile('jql "(?P.+)"$') -HELP_REGEX = re.compile('help$') +HELP_REGEX = re.compile("help$") -HELP_RESPONSE = ''' +HELP_RESPONSE = """ **get** `get` takes in an issue key and sends back information about that issue. For example, @@ -143,71 +143,71 @@ labeling "new, labels" by making due "2018-12-5" Jira Bot: > Issue *BOTS-16* was edited! https://example.atlassian.net/browse/BOTS-16 -''' +""" class JiraHandler: def usage(self) -> str: - return ''' + return """ Jira Bot uses the Jira REST API to interact with Jira. In order to use Jira Bot, `jira.conf` must be set up. See `doc.md` for more details. - ''' + """ def initialize(self, bot_handler: BotHandler) -> None: - config = bot_handler.get_config_info('jira') + config = bot_handler.get_config_info("jira") - username = config.get('username') - password = config.get('password') - domain = config.get('domain') + username = config.get("username") + password = config.get("password") + domain = config.get("domain") if not username: - raise KeyError('No `username` was specified') + raise KeyError("No `username` was specified") if not password: - raise KeyError('No `password` was specified') + raise KeyError("No `password` was specified") if not domain: - raise KeyError('No `domain` was specified') + raise KeyError("No `domain` was specified") self.auth = make_jira_auth(username, password) # Allow users to override the HTTP scheme - if re.match(r'^https?://', domain, re.IGNORECASE): + if re.match(r"^https?://", domain, re.IGNORECASE): self.domain_with_protocol = domain else: - self.domain_with_protocol = 'https://' + domain + self.domain_with_protocol = "https://" + domain # Use the front facing URL in output - self.display_url = config.get('display_url') + self.display_url = config.get("display_url") if not self.display_url: self.display_url = self.domain_with_protocol def jql_search(self, jql_query: str) -> str: - UNKNOWN_VAL = '*unknown*' + UNKNOWN_VAL = "*unknown*" jira_response = requests.get( self.domain_with_protocol - + '/rest/api/2/search?jql={}&fields=key,summary,status'.format(jql_query), - headers={'Authorization': self.auth}, + + "/rest/api/2/search?jql={}&fields=key,summary,status".format(jql_query), + headers={"Authorization": self.auth}, ).json() - url = self.display_url + '/browse/' - errors = jira_response.get('errorMessages', []) - results = jira_response.get('total', 0) + url = self.display_url + "/browse/" + errors = jira_response.get("errorMessages", []) + results = jira_response.get("total", 0) if errors: - response = 'Oh no! Jira raised an error:\n > ' + ', '.join(errors) + response = "Oh no! Jira raised an error:\n > " + ", ".join(errors) else: - response = '*Found {} results*\n\n'.format(results) - for issue in jira_response.get('issues', []): - fields = issue.get('fields', {}) - summary = fields.get('summary', UNKNOWN_VAL) - status_name = fields.get('status', {}).get('name', UNKNOWN_VAL) + response = "*Found {} results*\n\n".format(results) + for issue in jira_response.get("issues", []): + fields = issue.get("fields", {}) + summary = fields.get("summary", UNKNOWN_VAL) + status_name = fields.get("status", {}).get("name", UNKNOWN_VAL) response += "\n - {}: [{}]({}) **[{}]**".format( - issue['key'], summary, url + issue['key'], status_name + issue["key"], summary, url + issue["key"], status_name ) return response def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: - content = message.get('content') - response = '' + content = message.get("content") + response = "" get_match = GET_REGEX.match(content) create_match = CREATE_REGEX.match(content) @@ -217,39 +217,39 @@ class JiraHandler: help_match = HELP_REGEX.match(content) if get_match: - UNKNOWN_VAL = '*unknown*' + UNKNOWN_VAL = "*unknown*" - key = get_match.group('issue_key') + key = get_match.group("issue_key") jira_response = requests.get( - self.domain_with_protocol + '/rest/api/2/issue/' + key, - headers={'Authorization': self.auth}, + self.domain_with_protocol + "/rest/api/2/issue/" + key, + headers={"Authorization": self.auth}, ).json() - url = self.display_url + '/browse/' + key - errors = jira_response.get('errorMessages', []) - fields = jira_response.get('fields', {}) + url = self.display_url + "/browse/" + key + errors = jira_response.get("errorMessages", []) + fields = jira_response.get("fields", {}) - creator_name = fields.get('creator', {}).get('name', UNKNOWN_VAL) - description = fields.get('description', UNKNOWN_VAL) - priority_name = fields.get('priority', {}).get('name', UNKNOWN_VAL) - project_name = fields.get('project', {}).get('name', UNKNOWN_VAL) - type_name = fields.get('issuetype', {}).get('name', UNKNOWN_VAL) - status_name = fields.get('status', {}).get('name', UNKNOWN_VAL) - summary = fields.get('summary', UNKNOWN_VAL) + creator_name = fields.get("creator", {}).get("name", UNKNOWN_VAL) + description = fields.get("description", UNKNOWN_VAL) + priority_name = fields.get("priority", {}).get("name", UNKNOWN_VAL) + project_name = fields.get("project", {}).get("name", UNKNOWN_VAL) + type_name = fields.get("issuetype", {}).get("name", UNKNOWN_VAL) + status_name = fields.get("status", {}).get("name", UNKNOWN_VAL) + summary = fields.get("summary", UNKNOWN_VAL) if errors: - response = 'Oh no! Jira raised an error:\n > ' + ', '.join(errors) + response = "Oh no! Jira raised an error:\n > " + ", ".join(errors) else: response = ( - '**Issue *[{}]({})*: {}**\n\n' - ' - Type: *{}*\n' - ' - Description:\n' - ' > {}\n' - ' - Creator: *{}*\n' - ' - Project: *{}*\n' - ' - Priority: *{}*\n' - ' - Status: *{}*\n' + "**Issue *[{}]({})*: {}**\n\n" + " - Type: *{}*\n" + " - Description:\n" + " > {}\n" + " - Creator: *{}*\n" + " - Project: *{}*\n" + " - Priority: *{}*\n" + " - Status: *{}*\n" ).format( key, url, @@ -263,81 +263,81 @@ class JiraHandler: ) elif create_match: jira_response = requests.post( - self.domain_with_protocol + '/rest/api/2/issue', - headers={'Authorization': self.auth}, + self.domain_with_protocol + "/rest/api/2/issue", + headers={"Authorization": self.auth}, json=make_create_json( - create_match.group('summary'), - create_match.group('project_key'), - create_match.group('type_name'), - create_match.group('description'), - create_match.group('assignee'), - create_match.group('priority_name'), - create_match.group('labels'), - create_match.group('due_date'), + create_match.group("summary"), + create_match.group("project_key"), + create_match.group("type_name"), + create_match.group("description"), + create_match.group("assignee"), + create_match.group("priority_name"), + create_match.group("labels"), + create_match.group("due_date"), ), ) jira_response_json = jira_response.json() if jira_response.text else {} - key = jira_response_json.get('key', '') - url = self.display_url + '/browse/' + key - errors = list(jira_response_json.get('errors', {}).values()) + key = jira_response_json.get("key", "") + url = self.display_url + "/browse/" + key + errors = list(jira_response_json.get("errors", {}).values()) if errors: - response = 'Oh no! Jira raised an error:\n > ' + ', '.join(errors) + response = "Oh no! Jira raised an error:\n > " + ", ".join(errors) else: - response = 'Issue *' + key + '* is up! ' + url + response = "Issue *" + key + "* is up! " + url elif edit_match and check_is_editing_something(edit_match): - key = edit_match.group('issue_key') + key = edit_match.group("issue_key") jira_response = requests.put( - self.domain_with_protocol + '/rest/api/2/issue/' + key, - headers={'Authorization': self.auth}, + self.domain_with_protocol + "/rest/api/2/issue/" + key, + headers={"Authorization": self.auth}, json=make_edit_json( - edit_match.group('summary'), - edit_match.group('project_key'), - edit_match.group('type_name'), - edit_match.group('description'), - edit_match.group('assignee'), - edit_match.group('priority_name'), - edit_match.group('labels'), - edit_match.group('due_date'), + edit_match.group("summary"), + edit_match.group("project_key"), + edit_match.group("type_name"), + edit_match.group("description"), + edit_match.group("assignee"), + edit_match.group("priority_name"), + edit_match.group("labels"), + edit_match.group("due_date"), ), ) jira_response_json = jira_response.json() if jira_response.text else {} - url = self.display_url + '/browse/' + key - errors = list(jira_response_json.get('errors', {}).values()) + url = self.display_url + "/browse/" + key + errors = list(jira_response_json.get("errors", {}).values()) if errors: - response = 'Oh no! Jira raised an error:\n > ' + ', '.join(errors) + response = "Oh no! Jira raised an error:\n > " + ", ".join(errors) else: - response = 'Issue *' + key + '* was edited! ' + url + response = "Issue *" + key + "* was edited! " + url elif search_match: - search_term = search_match.group('search_term') + search_term = search_match.group("search_term") search_results = self.jql_search("summary ~ {}".format(search_term)) response = '**Search results for "{}"**\n\n{}'.format(search_term, search_results) elif jql_match: - jql_query = jql_match.group('jql_query') + jql_query = jql_match.group("jql_query") search_results = self.jql_search(jql_query) response = '**Search results for "{}"**\n\n{}'.format(jql_query, search_results) elif help_match: response = HELP_RESPONSE else: - response = 'Sorry, I don\'t understand that! Send me `help` for instructions.' + response = "Sorry, I don't understand that! Send me `help` for instructions." bot_handler.send_reply(message, response) def make_jira_auth(username: str, password: str) -> str: - '''Makes an auth header for Jira in the form 'Basic: '. + """Makes an auth header for Jira in the form 'Basic: '. Parameters: - username: The Jira email address. - password: The Jira password. - ''' - combo = username + ':' + password - encoded = base64.b64encode(combo.encode('utf-8')).decode('utf-8') - return 'Basic ' + encoded + """ + combo = username + ":" + password + encoded = base64.b64encode(combo.encode("utf-8")).decode("utf-8") + return "Basic " + encoded def make_create_json( @@ -350,7 +350,7 @@ def make_create_json( labels: Optional[str], due_date: Optional[str], ) -> Any: - '''Makes a JSON string for the Jira REST API editing endpoint based on + """Makes a JSON string for the Jira REST API editing endpoint based on fields that could be edited. Parameters: @@ -363,24 +363,24 @@ def make_create_json( - labels (optional): The Jira labels property, as a string of labels separated by comma-spaces. - due_date (optional): The Jira due date property. - ''' + """ json_fields = { - 'summary': summary, - 'project': {'key': project_key}, - 'issuetype': {'name': type_name}, + "summary": summary, + "project": {"key": project_key}, + "issuetype": {"name": type_name}, } if description: - json_fields['description'] = description + json_fields["description"] = description if assignee: - json_fields['assignee'] = {'name': assignee} + json_fields["assignee"] = {"name": assignee} if priority_name: - json_fields['priority'] = {'name': priority_name} + json_fields["priority"] = {"name": priority_name} if labels: - json_fields['labels'] = labels.split(', ') + json_fields["labels"] = labels.split(", ") if due_date: - json_fields['duedate'] = due_date + json_fields["duedate"] = due_date - json = {'fields': json_fields} + json = {"fields": json_fields} return json @@ -395,7 +395,7 @@ def make_edit_json( labels: Optional[str], due_date: Optional[str], ) -> Any: - '''Makes a JSON string for the Jira REST API editing endpoint based on + """Makes a JSON string for the Jira REST API editing endpoint based on fields that could be edited. Parameters: @@ -408,49 +408,49 @@ def make_edit_json( - labels (optional): The Jira labels property, as a string of labels separated by comma-spaces. - due_date (optional): The Jira due date property. - ''' + """ json_fields = {} if summary: - json_fields['summary'] = summary + json_fields["summary"] = summary if project_key: - json_fields['project'] = {'key': project_key} + json_fields["project"] = {"key": project_key} if type_name: - json_fields['issuetype'] = {'name': type_name} + json_fields["issuetype"] = {"name": type_name} if description: - json_fields['description'] = description + json_fields["description"] = description if assignee: - json_fields['assignee'] = {'name': assignee} + json_fields["assignee"] = {"name": assignee} if priority_name: - json_fields['priority'] = {'name': priority_name} + json_fields["priority"] = {"name": priority_name} if labels: - json_fields['labels'] = labels.split(', ') + json_fields["labels"] = labels.split(", ") if due_date: - json_fields['duedate'] = due_date + json_fields["duedate"] = due_date - json = {'fields': json_fields} + json = {"fields": json_fields} return json def check_is_editing_something(match: Any) -> bool: - '''Checks if an editing match is actually going to do editing. It is + """Checks if an editing match is actually going to do editing. It is possible for an edit regex to match without doing any editing because each editing field is optional. For example, 'edit issue "BOTS-13"' would pass but wouldn't preform any actions. Parameters: - match: The regex match object. - ''' + """ return bool( - match.group('summary') - or match.group('project_key') - or match.group('type_name') - or match.group('description') - or match.group('assignee') - or match.group('priority_name') - or match.group('labels') - or match.group('due_date') + match.group("summary") + or match.group("project_key") + or match.group("type_name") + or match.group("description") + or match.group("assignee") + or match.group("priority_name") + or match.group("labels") + or match.group("due_date") ) diff --git a/zulip_bots/zulip_bots/bots/jira/test_jira.py b/zulip_bots/zulip_bots/bots/jira/test_jira.py index e1959fa..1c7c092 100644 --- a/zulip_bots/zulip_bots/bots/jira/test_jira.py +++ b/zulip_bots/zulip_bots/bots/jira/test_jira.py @@ -2,28 +2,28 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests class TestJiraBot(BotTestCase, DefaultTests): - bot_name = 'jira' + bot_name = "jira" MOCK_CONFIG_INFO = { - 'username': 'example@example.com', - 'password': 'qwerty!123', - 'domain': 'example.atlassian.net', + "username": "example@example.com", + "password": "qwerty!123", + "domain": "example.atlassian.net", } MOCK_SCHEME_CONFIG_INFO = { - 'username': 'example@example.com', - 'password': 'qwerty!123', - 'domain': 'http://example.atlassian.net', + "username": "example@example.com", + "password": "qwerty!123", + "domain": "http://example.atlassian.net", } MOCK_DISPLAY_CONFIG_INFO = { - 'username': 'example@example.com', - 'password': 'qwerty!123', - 'domain': 'example.atlassian.net', - 'display_url': 'http://test.com', + "username": "example@example.com", + "password": "qwerty!123", + "domain": "example.atlassian.net", + "display_url": "http://test.com", } - MOCK_GET_RESPONSE = '''\ + MOCK_GET_RESPONSE = """\ **Issue *[TEST-13](https://example.atlassian.net/browse/TEST-13)*: summary** - Type: *Task* @@ -33,15 +33,15 @@ class TestJiraBot(BotTestCase, DefaultTests): - Project: *Tests* - Priority: *Medium* - Status: *To Do* -''' +""" - MOCK_CREATE_RESPONSE = 'Issue *TEST-16* is up! https://example.atlassian.net/browse/TEST-16' + MOCK_CREATE_RESPONSE = "Issue *TEST-16* is up! https://example.atlassian.net/browse/TEST-16" - MOCK_EDIT_RESPONSE = 'Issue *TEST-16* was edited! https://example.atlassian.net/browse/TEST-16' + MOCK_EDIT_RESPONSE = "Issue *TEST-16* was edited! https://example.atlassian.net/browse/TEST-16" - MOCK_NOTHING_RESPONSE = 'Sorry, I don\'t understand that! Send me `help` for instructions.' + MOCK_NOTHING_RESPONSE = "Sorry, I don't understand that! Send me `help` for instructions." - MOCK_HELP_RESPONSE = ''' + MOCK_HELP_RESPONSE = """ **get** `get` takes in an issue key and sends back information about that issue. For example, @@ -150,7 +150,7 @@ labeling "new, labels" by making due "2018-12-5" Jira Bot: > Issue *BOTS-16* was edited! https://example.atlassian.net/browse/BOTS-16 -''' +""" MOCK_SEARCH_RESPONSE = '**Search results for "TEST"**\n\n*Found 2 results*\n\n\n - TEST-1: [summary test 1](https://example.atlassian.net/browse/TEST-1) **[To Do]**\n - TEST-2: [summary test 2](https://example.atlassian.net/browse/TEST-2) **[To Do]**' MOCK_SEARCH_RESPONSE_URL = '**Search results for "TEST"**\n\n*Found 2 results*\n\n\n - TEST-1: [summary test 1](http://test.com/browse/TEST-1) **[To Do]**\n - TEST-2: [summary test 2](http://test.com/browse/TEST-2) **[To Do]**' @@ -163,38 +163,38 @@ Jira Bot: def test_config_without_username(self) -> None: config_without_username = { - 'password': 'qwerty!123', - 'domain': 'example.atlassian.net', + "password": "qwerty!123", + "domain": "example.atlassian.net", } - self._test_invalid_config(config_without_username, 'No `username` was specified') + self._test_invalid_config(config_without_username, "No `username` was specified") def test_config_without_password(self) -> None: config_without_password = { - 'username': 'example@example.com', - 'domain': 'example.atlassian.net', + "username": "example@example.com", + "domain": "example.atlassian.net", } - self._test_invalid_config(config_without_password, 'No `password` was specified') + self._test_invalid_config(config_without_password, "No `password` was specified") def test_config_without_domain(self) -> None: config_without_domain = { - 'username': 'example@example.com', - 'password': 'qwerty!123', + "username": "example@example.com", + "password": "qwerty!123", } - self._test_invalid_config(config_without_domain, 'No `domain` was specified') + self._test_invalid_config(config_without_domain, "No `domain` was specified") def test_get(self) -> None: - with self.mock_config_info(self.MOCK_CONFIG_INFO), self.mock_http_conversation('test_get'): + with self.mock_config_info(self.MOCK_CONFIG_INFO), self.mock_http_conversation("test_get"): self.verify_reply('get "TEST-13"', self.MOCK_GET_RESPONSE) def test_get_error(self) -> None: with self.mock_config_info(self.MOCK_CONFIG_INFO), self.mock_http_conversation( - 'test_get_error' + "test_get_error" ): - self.verify_reply('get "TEST-13"', 'Oh no! Jira raised an error:\n > error1') + self.verify_reply('get "TEST-13"', "Oh no! Jira raised an error:\n > error1") def test_create(self) -> None: with self.mock_config_info(self.MOCK_CONFIG_INFO), self.mock_http_conversation( - 'test_create' + "test_create" ): self.verify_reply( 'create issue "Testing" in project "TEST" with type "Task"', @@ -203,66 +203,66 @@ Jira Bot: def test_create_error(self) -> None: with self.mock_config_info(self.MOCK_CONFIG_INFO), self.mock_http_conversation( - 'test_create_error' + "test_create_error" ): self.verify_reply( 'create issue "Testing" in project "TEST" with type "Task" ' 'with description "This is a test description" assigned to "testuser" ' 'with priority "Medium" labeled "issues, testing" due "2018-06-11"', - 'Oh no! Jira raised an error:\n > error1', + "Oh no! Jira raised an error:\n > error1", ) def test_edit(self) -> None: - with self.mock_config_info(self.MOCK_CONFIG_INFO), self.mock_http_conversation('test_edit'): + with self.mock_config_info(self.MOCK_CONFIG_INFO), self.mock_http_conversation("test_edit"): self.verify_reply( 'edit issue "TEST-16" to use description "description"', self.MOCK_EDIT_RESPONSE ) def test_edit_error(self) -> None: with self.mock_config_info(self.MOCK_CONFIG_INFO), self.mock_http_conversation( - 'test_edit_error' + "test_edit_error" ): self.verify_reply( 'edit issue "TEST-13" to use summary "Change the summary" ' 'to use project "TEST" to use type "Bug" to use description "This is a test description" ' 'by assigning to "testuser" to use priority "Low" by labeling "issues, testing" ' 'by making due "2018-06-11"', - 'Oh no! Jira raised an error:\n > error1', + "Oh no! Jira raised an error:\n > error1", ) def test_search(self) -> None: with self.mock_config_info(self.MOCK_CONFIG_INFO), self.mock_http_conversation( - 'test_search' + "test_search" ): self.verify_reply('search "TEST"', self.MOCK_SEARCH_RESPONSE) def test_jql(self) -> None: with self.mock_config_info(self.MOCK_CONFIG_INFO), self.mock_http_conversation( - 'test_search' + "test_search" ): self.verify_reply('jql "summary ~ TEST"', self.MOCK_JQL_RESPONSE) def test_search_url(self) -> None: with self.mock_config_info(self.MOCK_DISPLAY_CONFIG_INFO), self.mock_http_conversation( - 'test_search' + "test_search" ): self.verify_reply('search "TEST"', self.MOCK_SEARCH_RESPONSE_URL) def test_search_scheme(self) -> None: with self.mock_config_info(self.MOCK_SCHEME_CONFIG_INFO), self.mock_http_conversation( - 'test_search_scheme' + "test_search_scheme" ): self.verify_reply('search "TEST"', self.MOCK_SEARCH_RESPONSE_SCHEME) def test_help(self) -> None: with self.mock_config_info(self.MOCK_CONFIG_INFO): - self.verify_reply('help', self.MOCK_HELP_RESPONSE) + self.verify_reply("help", self.MOCK_HELP_RESPONSE) # This overrides the default one in `BotTestCase`. def test_bot_responds_to_empty_message(self) -> None: with self.mock_config_info(self.MOCK_CONFIG_INFO): - self.verify_reply('', self.MOCK_NOTHING_RESPONSE) + self.verify_reply("", self.MOCK_NOTHING_RESPONSE) def test_no_command(self) -> None: with self.mock_config_info(self.MOCK_CONFIG_INFO): - self.verify_reply('qwertyuiop', self.MOCK_NOTHING_RESPONSE) + self.verify_reply("qwertyuiop", self.MOCK_NOTHING_RESPONSE) diff --git a/zulip_bots/zulip_bots/bots/link_shortener/link_shortener.py b/zulip_bots/zulip_bots/bots/link_shortener/link_shortener.py index fa6d75f..39707df 100644 --- a/zulip_bots/zulip_bots/bots/link_shortener/link_shortener.py +++ b/zulip_bots/zulip_bots/bots/link_shortener/link_shortener.py @@ -7,56 +7,56 @@ from zulip_bots.lib import BotHandler class LinkShortenerHandler: - '''A Zulip bot that will shorten URLs ("links") in a conversation using the + """A Zulip bot that will shorten URLs ("links") in a conversation using the goo.gl URL shortener. - ''' + """ def usage(self) -> str: return ( - 'Mention the link shortener bot in a conversation and then enter ' - 'any URLs you want to shorten in the body of the message. \n\n' - '`key` must be set in `link_shortener.conf`.' + "Mention the link shortener bot in a conversation and then enter " + "any URLs you want to shorten in the body of the message. \n\n" + "`key` must be set in `link_shortener.conf`." ) def initialize(self, bot_handler: BotHandler) -> None: - self.config_info = bot_handler.get_config_info('link_shortener') + self.config_info = bot_handler.get_config_info("link_shortener") self.check_api_key(bot_handler) def check_api_key(self, bot_handler: BotHandler) -> None: - test_request_data = self.call_link_shorten_service('www.youtube.com/watch') # type: Any + test_request_data = self.call_link_shorten_service("www.youtube.com/watch") # type: Any try: if self.is_invalid_token_error(test_request_data): bot_handler.quit( - 'Invalid key. Follow the instructions in doc.md for setting API key.' + "Invalid key. Follow the instructions in doc.md for setting API key." ) except KeyError: pass def is_invalid_token_error(self, response_json: Any) -> bool: return ( - response_json['status_code'] == 500 - and response_json['status_txt'] == 'INVALID_ARG_ACCESS_TOKEN' + response_json["status_code"] == 500 + and response_json["status_txt"] == "INVALID_ARG_ACCESS_TOKEN" ) def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: REGEX_STR = ( - r'(' - r'(?:http|https):\/\/' # This allows for the HTTP or HTTPS + r"(" + r"(?:http|https):\/\/" # This allows for the HTTP or HTTPS # protocol. r'[^"<>\{\}|\^~[\]` ]+' # This allows for any character except # for certain non-URL-safe ones. - r')' + r")" ) HELP_STR = ( - 'Mention the link shortener bot in a conversation and ' - 'then enter any URLs you want to shorten in the body of ' - 'the message.' + "Mention the link shortener bot in a conversation and " + "then enter any URLs you want to shorten in the body of " + "the message." ) - content = message['content'] + content = message["content"] - if content.strip() == 'help': + if content.strip() == "help": bot_handler.send_reply(message, HELP_STR) return @@ -64,45 +64,45 @@ class LinkShortenerHandler: shortened_links = [self.shorten_link(link) for link in link_matches] link_pairs = [ - (link_match + ': ' + shortened_link) + (link_match + ": " + shortened_link) for link_match, shortened_link in zip(link_matches, shortened_links) - if shortened_link != '' + if shortened_link != "" ] - final_response = '\n'.join(link_pairs) + final_response = "\n".join(link_pairs) - if final_response == '': - bot_handler.send_reply(message, 'No links found. ' + HELP_STR) + if final_response == "": + bot_handler.send_reply(message, "No links found. " + HELP_STR) return bot_handler.send_reply(message, final_response) def shorten_link(self, long_url: str) -> str: - '''Shortens a link using goo.gl Link Shortener and returns it, or + """Shortens a link using goo.gl Link Shortener and returns it, or returns an empty string if something goes wrong. Parameters: long_url (str): The original URL to shorten. - ''' + """ response_json = self.call_link_shorten_service(long_url) - if response_json['status_code'] == 200 and self.has_shorten_url(response_json): + if response_json["status_code"] == 200 and self.has_shorten_url(response_json): shorten_url = self.get_shorten_url(response_json) else: - shorten_url = '' + shorten_url = "" return shorten_url def call_link_shorten_service(self, long_url: str) -> Any: response = requests.get( - 'https://api-ssl.bitly.com/v3/shorten', - params={'access_token': self.config_info['key'], 'longUrl': long_url}, + "https://api-ssl.bitly.com/v3/shorten", + params={"access_token": self.config_info["key"], "longUrl": long_url}, ) return response.json() def has_shorten_url(self, response_json: Any) -> bool: - return 'data' in response_json and 'url' in response_json['data'] + return "data" in response_json and "url" in response_json["data"] def get_shorten_url(self, response_json: Any) -> str: - return response_json['data']['url'] + return response_json["data"]["url"] handler_class = LinkShortenerHandler diff --git a/zulip_bots/zulip_bots/bots/link_shortener/test_link_shortener.py b/zulip_bots/zulip_bots/bots/link_shortener/test_link_shortener.py index bc7702c..01a6fd5 100644 --- a/zulip_bots/zulip_bots/bots/link_shortener/test_link_shortener.py +++ b/zulip_bots/zulip_bots/bots/link_shortener/test_link_shortener.py @@ -8,40 +8,40 @@ class TestLinkShortenerBot(BotTestCase, DefaultTests): bot_name = "link_shortener" def _test(self, message: str, response: str) -> None: - with self.mock_config_info({'key': 'qwertyuiop'}): + with self.mock_config_info({"key": "qwertyuiop"}): self.verify_reply(message, response) def test_bot_responds_to_empty_message(self) -> None: - with patch('requests.get'): + with patch("requests.get"): self._test( - '', + "", ( - 'No links found. ' - 'Mention the link shortener bot in a conversation and ' - 'then enter any URLs you want to shorten in the body of ' - 'the message.' + "No links found. " + "Mention the link shortener bot in a conversation and " + "then enter any URLs you want to shorten in the body of " + "the message." ), ) def test_normal(self) -> None: - with self.mock_http_conversation('test_normal'): + with self.mock_http_conversation("test_normal"): self._test( - 'Shorten https://www.github.com/zulip/zulip please.', - 'https://www.github.com/zulip/zulip: http://bit.ly/2Ht2hOI', + "Shorten https://www.github.com/zulip/zulip please.", + "https://www.github.com/zulip/zulip: http://bit.ly/2Ht2hOI", ) def test_no_links(self) -> None: # No `mock_http_conversation` is necessary because the bot will # recognize that no links are in the message and won't make any HTTP # requests. - with patch('requests.get'): + with patch("requests.get"): self._test( - 'Shorten nothing please.', + "Shorten nothing please.", ( - 'No links found. ' - 'Mention the link shortener bot in a conversation and ' - 'then enter any URLs you want to shorten in the body of ' - 'the message.' + "No links found. " + "Mention the link shortener bot in a conversation and " + "then enter any URLs you want to shorten in the body of " + "the message." ), ) @@ -49,18 +49,18 @@ class TestLinkShortenerBot(BotTestCase, DefaultTests): # No `mock_http_conversation` is necessary because the bot will # recognize that the message is 'help' and won't make any HTTP # requests. - with patch('requests.get'): + with patch("requests.get"): self._test( - 'help', + "help", ( - 'Mention the link shortener bot in a conversation and then ' - 'enter any URLs you want to shorten in the body of the message.' + "Mention the link shortener bot in a conversation and then " + "enter any URLs you want to shorten in the body of the message." ), ) def test_exception_when_api_key_is_invalid(self) -> None: bot_test_instance = LinkShortenerHandler() - with self.mock_config_info({'key': 'qwertyuiopx'}): - with self.mock_http_conversation('test_invalid_access_token'): + with self.mock_config_info({"key": "qwertyuiopx"}): + with self.mock_http_conversation("test_invalid_access_token"): with self.assertRaises(StubBotHandler.BotQuitException): bot_test_instance.initialize(StubBotHandler()) diff --git a/zulip_bots/zulip_bots/bots/mention/mention.py b/zulip_bots/zulip_bots/bots/mention/mention.py index b676c3f..253dcc5 100644 --- a/zulip_bots/zulip_bots/bots/mention/mention.py +++ b/zulip_bots/zulip_bots/bots/mention/mention.py @@ -9,107 +9,107 @@ from zulip_bots.lib import BotHandler class MentionHandler: def initialize(self, bot_handler: BotHandler) -> None: - self.config_info = bot_handler.get_config_info('mention') - self.access_token = self.config_info['access_token'] - self.account_id = '' + self.config_info = bot_handler.get_config_info("mention") + self.access_token = self.config_info["access_token"] + self.account_id = "" self.check_access_token(bot_handler) def check_access_token(self, bot_handler: BotHandler) -> None: test_query_header = { - 'Authorization': 'Bearer ' + self.access_token, - 'Accept-Version': '1.15', + "Authorization": "Bearer " + self.access_token, + "Accept-Version": "1.15", } test_query_response = requests.get( - 'https://api.mention.net/api/accounts/me', headers=test_query_header + "https://api.mention.net/api/accounts/me", headers=test_query_header ) try: test_query_data = test_query_response.json() if ( - test_query_data['error'] == 'invalid_grant' - and test_query_data['error_description'] == 'The access token provided is invalid.' + test_query_data["error"] == "invalid_grant" + and test_query_data["error_description"] == "The access token provided is invalid." ): bot_handler.quit( - 'Access Token Invalid. Please see doc.md to find out how to get it.' + "Access Token Invalid. Please see doc.md to find out how to get it." ) except KeyError: pass def usage(self) -> str: - return ''' + return """ This is a Mention API Bot which will find mentions of the given keyword throughout the web. Version 1.00 - ''' + """ def handle_message(self, message: Dict[str, Any], bot_handler: BotHandler) -> None: - message['content'] = message['content'].strip() + message["content"] = message["content"].strip() - if message['content'].lower() == 'help': + if message["content"].lower() == "help": bot_handler.send_reply(message, self.usage()) return - if message['content'] == '': - bot_handler.send_reply(message, 'Empty Mention Query') + if message["content"] == "": + bot_handler.send_reply(message, "Empty Mention Query") return - keyword = message['content'] + keyword = message["content"] content = self.generate_response(keyword) bot_handler.send_reply(message, content) def get_account_id(self) -> str: get_ac_id_header = { - 'Authorization': 'Bearer ' + self.access_token, - 'Accept-Version': '1.15', + "Authorization": "Bearer " + self.access_token, + "Accept-Version": "1.15", } - response = requests.get('https://api.mention.net/api/accounts/me', headers=get_ac_id_header) + response = requests.get("https://api.mention.net/api/accounts/me", headers=get_ac_id_header) data_json = response.json() - account_id = data_json['account']['id'] + account_id = data_json["account"]["id"] return account_id def get_alert_id(self, keyword: str) -> str: create_alert_header = { - 'Authorization': 'Bearer ' + self.access_token, - 'Content-Type': 'application/json', - 'Accept-Version': '1.15', + "Authorization": "Bearer " + self.access_token, + "Content-Type": "application/json", + "Accept-Version": "1.15", } create_alert_data = { - 'name': keyword, - 'query': {'type': 'basic', 'included_keywords': [keyword]}, - 'languages': ['en'], - 'sources': ['web'], + "name": keyword, + "query": {"type": "basic", "included_keywords": [keyword]}, + "languages": ["en"], + "sources": ["web"], } # type: Any response = requests.post( - 'https://api.mention.net/api/accounts/' + self.account_id + '/alerts', + "https://api.mention.net/api/accounts/" + self.account_id + "/alerts", data=create_alert_data, headers=create_alert_header, ) data_json = response.json() - alert_id = data_json['alert']['id'] + alert_id = data_json["alert"]["id"] return alert_id def get_mentions(self, alert_id: str) -> List[Any]: get_mentions_header = { - 'Authorization': 'Bearer ' + self.access_token, - 'Accept-Version': '1.15', + "Authorization": "Bearer " + self.access_token, + "Accept-Version": "1.15", } response = requests.get( - 'https://api.mention.net/api/accounts/' + "https://api.mention.net/api/accounts/" + self.account_id - + '/alerts/' + + "/alerts/" + alert_id - + '/mentions', + + "/mentions", headers=get_mentions_header, ) data_json = response.json() - mentions = data_json['mentions'] + mentions = data_json["mentions"] return mentions def generate_response(self, keyword: str) -> str: - if self.account_id == '': + if self.account_id == "": self.account_id = self.get_account_id() try: @@ -124,9 +124,9 @@ class MentionHandler: # Usually triggered by no response or json parse error when account quota is finished. raise MentionNoResponseException() - reply = 'The most recent mentions of `' + keyword + '` on the web are: \n' + reply = "The most recent mentions of `" + keyword + "` on the web are: \n" for mention in mentions: - reply += "[{title}]({id})\n".format(title=mention['title'], id=mention['original_url']) + reply += "[{title}]({id})\n".format(title=mention["title"], id=mention["original_url"]) return reply diff --git a/zulip_bots/zulip_bots/bots/mention/test_mention.py b/zulip_bots/zulip_bots/bots/mention/test_mention.py index 4b4280d..5a9b5a4 100644 --- a/zulip_bots/zulip_bots/bots/mention/test_mention.py +++ b/zulip_bots/zulip_bots/bots/mention/test_mention.py @@ -8,49 +8,49 @@ class TestMentionBot(BotTestCase, DefaultTests): bot_name = "mention" def test_bot_responds_to_empty_message(self) -> None: - with self.mock_config_info({'access_token': '12345'}), patch('requests.get'): - self.verify_reply('', 'Empty Mention Query') + with self.mock_config_info({"access_token": "12345"}), patch("requests.get"): + self.verify_reply("", "Empty Mention Query") def test_help_query(self) -> None: - with self.mock_config_info({'access_token': '12345'}), patch('requests.get'): + with self.mock_config_info({"access_token": "12345"}), patch("requests.get"): self.verify_reply( - 'help', - ''' + "help", + """ This is a Mention API Bot which will find mentions of the given keyword throughout the web. Version 1.00 - ''', + """, ) def test_get_account_id(self) -> None: bot_test_instance = MentionHandler() - bot_test_instance.access_token = 'TEST' + bot_test_instance.access_token = "TEST" - with self.mock_http_conversation('get_account_id'): - self.assertEqual(bot_test_instance.get_account_id(), 'TEST') + with self.mock_http_conversation("get_account_id"): + self.assertEqual(bot_test_instance.get_account_id(), "TEST") def test_get_alert_id(self) -> None: bot_test_instance = MentionHandler() - bot_test_instance.access_token = 'TEST' - bot_test_instance.account_id = 'TEST' + bot_test_instance.access_token = "TEST" + bot_test_instance.account_id = "TEST" - with self.mock_http_conversation('get_alert_id'): - self.assertEqual(bot_test_instance.get_alert_id('TEST'), 'TEST') + with self.mock_http_conversation("get_alert_id"): + self.assertEqual(bot_test_instance.get_alert_id("TEST"), "TEST") def test_get_mentions(self) -> None: bot_test_instance = MentionHandler() - bot_test_instance.access_token = 'TEST' - bot_test_instance.account_id = 'TEST' + bot_test_instance.access_token = "TEST" + bot_test_instance.account_id = "TEST" - with self.mock_http_conversation('get_mentions'): - bot_response = bot_test_instance.get_mentions('TEST')[0] - self.assertEqual(bot_response['title'], 'TEST') - self.assertEqual(bot_response['original_url'], 'TEST') + with self.mock_http_conversation("get_mentions"): + bot_response = bot_test_instance.get_mentions("TEST")[0] + self.assertEqual(bot_response["title"], "TEST") + self.assertEqual(bot_response["original_url"], "TEST") def test_exception_when_api_key_is_invalid(self) -> None: bot_test_instance = MentionHandler() - with self.mock_config_info({'access_token': 'TEST'}): - with self.mock_http_conversation('invalid_api_key'): + with self.mock_config_info({"access_token": "TEST"}): + with self.mock_http_conversation("invalid_api_key"): with self.assertRaises(StubBotHandler.BotQuitException): bot_test_instance.initialize(StubBotHandler()) diff --git a/zulip_bots/zulip_bots/bots/merels/libraries/game_data.py b/zulip_bots/zulip_bots/bots/merels/libraries/game_data.py index d33ef5e..c2eadf2 100644 --- a/zulip_bots/zulip_bots/bots/merels/libraries/game_data.py +++ b/zulip_bots/zulip_bots/bots/merels/libraries/game_data.py @@ -10,7 +10,7 @@ from .interface import construct_grid class GameData: - def __init__(self, game_data=('merels', 'X', 0, 0, 'NNNNNNNNNNNNNNNNNNNNNNNN', '', 0)): + def __init__(self, game_data=("merels", "X", 0, 0, "NNNNNNNNNNNNNNNNNNNNNNNN", "", 0)): self.topic_name = game_data[0] self.turn = game_data[1] self.x_taken = game_data[2] diff --git a/zulip_bots/zulip_bots/bots/merels/libraries/interface.py b/zulip_bots/zulip_bots/bots/merels/libraries/interface.py index 66a51bd..842d864 100644 --- a/zulip_bots/zulip_bots/bots/merels/libraries/interface.py +++ b/zulip_bots/zulip_bots/bots/merels/libraries/interface.py @@ -42,7 +42,7 @@ def graph_grid(grid): :return: A nicer display of the grid """ - return '''` 0 1 2 3 4 5 6 + return """` 0 1 2 3 4 5 6 0 [{}]---------------[{}]---------------[{}] | | | 1 | [{}]---------[{}]---------[{}] | @@ -55,7 +55,7 @@ def graph_grid(grid): | | | | | 5 | [{}]---------[{}]---------[{}] | | | | - 6 [{}]---------------[{}]---------------[{}]`'''.format( + 6 [{}]---------------[{}]---------------[{}]`""".format( grid[0][0], grid[0][3], grid[0][6], diff --git a/zulip_bots/zulip_bots/bots/merels/merels.py b/zulip_bots/zulip_bots/bots/merels/merels.py index 96581ce..900a078 100644 --- a/zulip_bots/zulip_bots/bots/merels/merels.py +++ b/zulip_bots/zulip_bots/bots/merels/merels.py @@ -22,12 +22,12 @@ class MerelsModel: self.topic = "merels" self.storage = Storage(self.topic) self.current_board = mechanics.display_game(self.topic, self.storage) - self.token = ['O', 'X'] + self.token = ["O", "X"] def determine_game_over(self, players: List[str]) -> str: if self.contains_winning_move(self.current_board): - return 'current turn' - return '' + return "current turn" + return "" def contains_winning_move(self, board: Any) -> bool: merels = database.MerelsStorage(self.topic, self.storage) @@ -53,7 +53,7 @@ class MerelsModel: class MerelsMessageHandler: - tokens = [':o_button:', ':cross_mark_button:'] + tokens = [":o_button:", ":cross_mark_button:"] def parse_board(self, board: Any) -> str: return board @@ -69,24 +69,24 @@ class MerelsMessageHandler: class MerelsHandler(GameAdapter): - ''' + """ You can play merels! Make sure your message starts with "@mention-bot". - ''' + """ META = { - 'name': 'merels', - 'description': 'Lets you play merels against any player.', + "name": "merels", + "description": "Lets you play merels against any player.", } def usage(self) -> str: return game.getInfo() def __init__(self) -> None: - game_name = 'Merels' - bot_name = 'merels' + game_name = "Merels" + bot_name = "merels" move_help_message = "" - move_regex = '.*' + move_regex = ".*" model = MerelsModel rules = game.getInfo() gameMessageHandler = MerelsMessageHandler diff --git a/zulip_bots/zulip_bots/bots/merels/test/test_database.py b/zulip_bots/zulip_bots/bots/merels/test/test_database.py index 4918ebe..8a7cfa1 100644 --- a/zulip_bots/zulip_bots/bots/merels/test/test_database.py +++ b/zulip_bots/zulip_bots/bots/merels/test/test_database.py @@ -5,16 +5,16 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests class DatabaseTest(BotTestCase, DefaultTests): - bot_name = 'merels' + bot_name = "merels" def setUp(self): self.storage = SimpleStorage() self.merels = database.MerelsStorage("", self.storage) def test_obtain_gamedata(self): - self.merels.update_game("topic1", "X", 0, 0, 'NNNNNNNNNNNNNNNNNNNNNNNN', "", 0) + self.merels.update_game("topic1", "X", 0, 0, "NNNNNNNNNNNNNNNNNNNNNNNN", "", 0) res = self.merels.get_game_data("topic1") - self.assertTupleEqual(res, ('topic1', 'X', 0, 0, 'NNNNNNNNNNNNNNNNNNNNNNNN', "", 0)) + self.assertTupleEqual(res, ("topic1", "X", 0, 0, "NNNNNNNNNNNNNNNNNNNNNNNN", "", 0)) self.assertEqual(len(res), 7) def test_obtain_nonexisting_gamedata(self): @@ -22,13 +22,13 @@ class DatabaseTest(BotTestCase, DefaultTests): self.assertEqual(res, None) def test_game_session(self): - self.merels.update_game("topic1", "X", 0, 0, 'NNNNNNNNNNNNNNNNNNNNNNNN', "", 0) - self.merels.update_game("topic2", "O", 5, 4, 'XXXXOOOOONNNNNNNNNNNNNNN', "", 0) + self.merels.update_game("topic1", "X", 0, 0, "NNNNNNNNNNNNNNNNNNNNNNNN", "", 0) + self.merels.update_game("topic2", "O", 5, 4, "XXXXOOOOONNNNNNNNNNNNNNN", "", 0) self.assertTrue(self.storage.contains("topic1"), self.storage.contains("topic2")) topic2Board = game_data.GameData(self.merels.get_game_data("topic2")) self.assertEqual(topic2Board.board, "XXXXOOOOONNNNNNNNNNNNNNN") def test_remove_game(self): - self.merels.update_game("topic1", "X", 0, 0, 'NNNNNNNNNNNNNNNNNNNNNNNN', "", 0) + self.merels.update_game("topic1", "X", 0, 0, "NNNNNNNNNNNNNNNNNNNNNNNN", "", 0) self.merels.remove_game("topic1") self.assertEqual(self.merels.get_game_data("topic1"), None) diff --git a/zulip_bots/zulip_bots/bots/merels/test/test_interface.py b/zulip_bots/zulip_bots/bots/merels/test/test_interface.py index 832c550..cf6e3d5 100644 --- a/zulip_bots/zulip_bots/bots/merels/test/test_interface.py +++ b/zulip_bots/zulip_bots/bots/merels/test/test_interface.py @@ -8,7 +8,7 @@ class BoardLayoutTest(unittest.TestCase): grid = interface.construct_grid("NNNNNNNNNNNNNNNNNNNNNNNN") self.assertEqual( interface.graph_grid(grid), - '''` 0 1 2 3 4 5 6 + """` 0 1 2 3 4 5 6 0 [ ]---------------[ ]---------------[ ] | | | 1 | [ ]---------[ ]---------[ ] | @@ -21,14 +21,14 @@ class BoardLayoutTest(unittest.TestCase): | | | | | 5 | [ ]---------[ ]---------[ ] | | | | - 6 [ ]---------------[ ]---------------[ ]`''', + 6 [ ]---------------[ ]---------------[ ]`""", ) def test_full_layout_arragement(self): grid = interface.construct_grid("NXONXONXONXONXONXONXONXO") self.assertEqual( interface.graph_grid(grid), - '''` 0 1 2 3 4 5 6 + """` 0 1 2 3 4 5 6 0 [ ]---------------[X]---------------[O] | | | 1 | [ ]---------[X]---------[O] | @@ -41,14 +41,14 @@ class BoardLayoutTest(unittest.TestCase): | | | | | 5 | [ ]---------[X]---------[O] | | | | - 6 [ ]---------------[X]---------------[O]`''', + 6 [ ]---------------[X]---------------[O]`""", ) def test_illegal_character_arrangement(self): grid = interface.construct_grid("ABCDABCDABCDABCDABCDXXOO") self.assertEqual( interface.graph_grid(grid), - '''` 0 1 2 3 4 5 6 + """` 0 1 2 3 4 5 6 0 [ ]---------------[ ]---------------[ ] | | | 1 | [ ]---------[ ]---------[ ] | @@ -61,7 +61,7 @@ class BoardLayoutTest(unittest.TestCase): | | | | | 5 | [ ]---------[ ]---------[X] | | | | - 6 [X]---------------[O]---------------[O]`''', + 6 [X]---------------[O]---------------[O]`""", ) diff --git a/zulip_bots/zulip_bots/bots/merels/test_merels.py b/zulip_bots/zulip_bots/bots/merels/test_merels.py index 4066798..71deb89 100644 --- a/zulip_bots/zulip_bots/bots/merels/test_merels.py +++ b/zulip_bots/zulip_bots/bots/merels/test_merels.py @@ -7,15 +7,15 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests class TestMerelsBot(BotTestCase, DefaultTests): - bot_name = 'merels' + bot_name = "merels" def test_no_command(self): message = dict( - content='magic', type='stream', sender_email="boo@email.com", sender_full_name="boo" + content="magic", type="stream", sender_email="boo@email.com", sender_full_name="boo" ) res = self.get_response(message) self.assertEqual( - res['content'], 'You are not in a game at the moment.' ' Type `help` for help.' + res["content"], "You are not in a game at the moment." " Type `help` for help." ) # FIXME: Add tests for computer moves @@ -28,22 +28,22 @@ class TestMerelsBot(BotTestCase, DefaultTests): self.assertNotEqual(message_handler.get_player_color(0), None) self.assertNotEqual(message_handler.game_start_message(), None) self.assertEqual( - message_handler.alert_move_message('foo', 'moved right'), 'foo :moved right' + message_handler.alert_move_message("foo", "moved right"), "foo :moved right" ) # Test to see if the attributes exist def test_has_attributes(self) -> None: model, message_handler = self._get_game_handlers() # Attributes from the Merels Handler - self.assertTrue(hasattr(message_handler, 'parse_board') is not None) - self.assertTrue(hasattr(message_handler, 'get_player_color') is not None) - self.assertTrue(hasattr(message_handler, 'alert_move_message') is not None) - self.assertTrue(hasattr(message_handler, 'game_start_message') is not None) - self.assertTrue(hasattr(message_handler, 'alert_move_message') is not None) + self.assertTrue(hasattr(message_handler, "parse_board") is not None) + self.assertTrue(hasattr(message_handler, "get_player_color") is not None) + self.assertTrue(hasattr(message_handler, "alert_move_message") is not None) + self.assertTrue(hasattr(message_handler, "game_start_message") is not None) + self.assertTrue(hasattr(message_handler, "alert_move_message") is not None) # Attributes from the Merels Model - self.assertTrue(hasattr(model, 'determine_game_over') is not None) - self.assertTrue(hasattr(model, 'contains_winning_move') is not None) - self.assertTrue(hasattr(model, 'make_move') is not None) + self.assertTrue(hasattr(model, "determine_game_over") is not None) + self.assertTrue(hasattr(model, "contains_winning_move") is not None) + self.assertTrue(hasattr(model, "make_move") is not None) def test_parse_board(self) -> None: board = EMPTY_BOARD @@ -60,19 +60,19 @@ class TestMerelsBot(BotTestCase, DefaultTests): if bot is None: bot, bot_handler = self._get_handlers() message = { - 'sender_email': '{}@example.com'.format(name), - 'sender_full_name': '{}'.format(name), + "sender_email": "{}@example.com".format(name), + "sender_full_name": "{}".format(name), } bot.add_user_to_cache(message) return bot def setup_game(self) -> None: - bot = self.add_user_to_cache('foo') - self.add_user_to_cache('baz', bot) + bot = self.add_user_to_cache("foo") + self.add_user_to_cache("baz", bot) instance = GameInstance( - bot, False, 'test game', 'abc123', ['foo@example.com', 'baz@example.com'], 'test' + bot, False, "test game", "abc123", ["foo@example.com", "baz@example.com"], "test" ) - bot.instances.update({'abc123': instance}) + bot.instances.update({"abc123": instance}) instance.start() return bot diff --git a/zulip_bots/zulip_bots/bots/monkeytestit/lib/report.py b/zulip_bots/zulip_bots/bots/monkeytestit/lib/report.py index 7b8c3e6..84a8899 100644 --- a/zulip_bots/zulip_bots/bots/monkeytestit/lib/report.py +++ b/zulip_bots/zulip_bots/bots/monkeytestit/lib/report.py @@ -21,7 +21,7 @@ def compose(results: Dict) -> Text: :return: A response string containing the full report """ if "error" in results: - return "Error: {}".format(results['error']) + return "Error: {}".format(results["error"]) response = "" @@ -48,7 +48,7 @@ def print_more_info_url(results: Dict) -> Text: :param results: A dictionary containing the results of a check :return: A response string containing the url info """ - return "More info: {}".format(results['results_url']) + return "More info: {}".format(results["results_url"]) def print_test_id(results: Dict) -> Text: @@ -57,7 +57,7 @@ def print_test_id(results: Dict) -> Text: :param results: A dictionary containing the results of a check :return: A response string containing the test id """ - return "Test: https://monkeytest.it/test/{}".format(results['test_id']) + return "Test: https://monkeytest.it/test/{}".format(results["test_id"]) def print_failures_checkers(results: Dict) -> Text: @@ -75,9 +75,9 @@ def print_failures_checkers(results: Dict) -> Text: checkers """ failures_checkers = [ - (checker, len(results['failures'][checker])) + (checker, len(results["failures"][checker])) for checker in get_enabled_checkers(results) - if checker in results['failures'] + if checker in results["failures"] ] # [('seo', 3), ..] failures_checkers_messages = [ @@ -97,7 +97,7 @@ def get_enabled_checkers(results: Dict) -> List: :param results: A dictionary containing the results of a check :return: A list containing enabled checkers """ - checkers = results['enabled_checkers'] + checkers = results["enabled_checkers"] enabled_checkers = [] for checker in checkers.keys(): if checkers[checker]: # == True/False @@ -126,4 +126,4 @@ def print_status(results: Dict) -> Text: :param results: A dictionary containing the results of a check :return: A response string containing check status """ - return "Status: {}".format(results['status']) + return "Status: {}".format(results["status"]) diff --git a/zulip_bots/zulip_bots/bots/monkeytestit/monkeytestit.py b/zulip_bots/zulip_bots/bots/monkeytestit/monkeytestit.py index adcb9cc..0aab188 100644 --- a/zulip_bots/zulip_bots/bots/monkeytestit/monkeytestit.py +++ b/zulip_bots/zulip_bots/bots/monkeytestit/monkeytestit.py @@ -19,18 +19,18 @@ class MonkeyTestitBot: def initialize(self, bot_handler: BotHandler) -> None: try: - self.config = bot_handler.get_config_info('monkeytestit') + self.config = bot_handler.get_config_info("monkeytestit") except NoBotConfigException: bot_handler.quit( "Quitting because there's no config file " "supplied. See doc.md for a guide on setting up " "one. If you already know the drill, just create " - "a .conf file with \"monkeytestit\" as the " + 'a .conf file with "monkeytestit" as the ' "section header and api_key = for " "the api key." ) - self.api_key = self.config.get('api_key') + self.api_key = self.config.get("api_key") if not self.api_key: bot_handler.quit( @@ -48,7 +48,7 @@ class MonkeyTestitBot: ) def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: - content = message['content'] + content = message["content"] response = parse.execute(content, self.api_key) diff --git a/zulip_bots/zulip_bots/bots/monkeytestit/test_monkeytestit.py b/zulip_bots/zulip_bots/bots/monkeytestit/test_monkeytestit.py index 07bdce6..1ee4fc1 100644 --- a/zulip_bots/zulip_bots/bots/monkeytestit/test_monkeytestit.py +++ b/zulip_bots/zulip_bots/bots/monkeytestit/test_monkeytestit.py @@ -14,32 +14,32 @@ class TestMonkeyTestitBot(BotTestCase, DefaultTests): def test_bot_responds_to_empty_message(self): message = dict( - content='', - type='stream', + content="", + type="stream", ) - with patch.object(self.monkeytestit_class, 'initialize', return_value=None): - with self.mock_config_info({'api_key': "magic"}): + with patch.object(self.monkeytestit_class, "initialize", return_value=None): + with self.mock_config_info({"api_key": "magic"}): res = self.get_response(message) - self.assertTrue("Unknown command" in res['content']) + self.assertTrue("Unknown command" in res["content"]) def test_website_fail(self): message = dict( - content='check https://website.com', - type='stream', + content="check https://website.com", + type="stream", ) - with patch.object(self.monkeytestit_class, 'initialize', return_value=None): - with self.mock_config_info({'api_key': "magic"}): - with self.mock_http_conversation('website_result_fail'): + with patch.object(self.monkeytestit_class, "initialize", return_value=None): + with self.mock_config_info({"api_key": "magic"}): + with self.mock_http_conversation("website_result_fail"): res = self.get_response(message) - self.assertTrue("Status: tests_failed" in res['content']) + self.assertTrue("Status: tests_failed" in res["content"]) def test_website_success(self): message = dict( - content='check https://website.com', - type='stream', + content="check https://website.com", + type="stream", ) - with patch.object(self.monkeytestit_class, 'initialize', return_value=None): - with self.mock_config_info({'api_key': "magic"}): - with self.mock_http_conversation('website_result_success'): + with patch.object(self.monkeytestit_class, "initialize", return_value=None): + with self.mock_config_info({"api_key": "magic"}): + with self.mock_http_conversation("website_result_success"): res = self.get_response(message) - self.assertTrue("success" in res['content']) + self.assertTrue("success" in res["content"]) diff --git a/zulip_bots/zulip_bots/bots/salesforce/salesforce.py b/zulip_bots/zulip_bots/bots/salesforce/salesforce.py index c85b310..aff079e 100644 --- a/zulip_bots/zulip_bots/bots/salesforce/salesforce.py +++ b/zulip_bots/zulip_bots/bots/salesforce/salesforce.py @@ -9,7 +9,7 @@ import simple_salesforce from zulip_bots.bots.salesforce.utils import commands, default_query, link_query, object_types from zulip_bots.lib import BotHandler -base_help_text = '''Salesforce bot +base_help_text = """Salesforce bot This bot can do simple salesforce query requests **All commands must be @-mentioned to the bot.** Commands: @@ -23,21 +23,21 @@ This bot can also show details about any Salesforce links sent to it. Supported Object types: These are the types of Salesforce object supported by this bot. The bot cannot show the details of any other object types. -{}''' +{}""" -login_url = 'https://login.salesforce.com/' +login_url = "https://login.salesforce.com/" def get_help_text() -> str: - command_text = '' + command_text = "" for command in commands: - if 'template' in command.keys() and 'description' in command.keys(): - command_text += '**{}**: {}\n'.format( - '{} [arguments]'.format(command['template']), command['description'] + if "template" in command.keys() and "description" in command.keys(): + command_text += "**{}**: {}\n".format( + "{} [arguments]".format(command["template"]), command["description"] ) - object_type_text = '' + object_type_text = "" for object_type in object_types.values(): - object_type_text += '{}\n'.format(object_type['table']) + object_type_text += "{}\n".format(object_type["table"]) return base_help_text.format(command_text, object_type_text) @@ -48,28 +48,28 @@ def format_result( rank_output: bool = False, show_all_keys: bool = False, ) -> str: - exclude_keys += ['Name', 'attributes', 'Id'] - output = '' - if result['totalSize'] == 0: - return 'No records found.' - if result['totalSize'] == 1: - record = result['records'][0] - output += '**[{}]({}{})**\n'.format(record['Name'], login_url, record['Id']) + exclude_keys += ["Name", "attributes", "Id"] + output = "" + if result["totalSize"] == 0: + return "No records found." + if result["totalSize"] == 1: + record = result["records"][0] + output += "**[{}]({}{})**\n".format(record["Name"], login_url, record["Id"]) for key, value in record.items(): if key not in exclude_keys: - output += '>**{}**: {}\n'.format(key, value) + output += ">**{}**: {}\n".format(key, value) else: - for i, record in enumerate(result['records']): + for i, record in enumerate(result["records"]): if rank_output: - output += '{}) '.format(i + 1) - output += '**[{}]({}{})**\n'.format(record['Name'], login_url, record['Id']) + output += "{}) ".format(i + 1) + output += "**[{}]({}{})**\n".format(record["Name"], login_url, record["Id"]) added_keys = False for key, value in record.items(): if key in force_keys or (show_all_keys and key not in exclude_keys): added_keys = True - output += '>**{}**: {}\n'.format(key, value) + output += ">**{}**: {}\n".format(key, value) if added_keys: - output += '\n' + output += "\n" return output @@ -77,37 +77,37 @@ def query_salesforce( arg: str, salesforce: simple_salesforce.Salesforce, command: Dict[str, Any] ) -> str: arg = arg.strip() - qarg = arg.split(' -', 1)[0] + qarg = arg.split(" -", 1)[0] split_args = [] # type: List[str] - raw_arg = '' - if len(arg.split(' -', 1)) > 1: - raw_arg = ' -' + arg.split(' -', 1)[1] - split_args = raw_arg.split(' -') + raw_arg = "" + if len(arg.split(" -", 1)) > 1: + raw_arg = " -" + arg.split(" -", 1)[1] + split_args = raw_arg.split(" -") limit_num = 5 - re_limit = re.compile(r'-limit \d+') + re_limit = re.compile(r"-limit \d+") limit = re_limit.search(raw_arg) if limit: - limit_num = int(limit.group().rsplit(' ', 1)[1]) - logging.info('Searching with limit {}'.format(limit_num)) + limit_num = int(limit.group().rsplit(" ", 1)[1]) + logging.info("Searching with limit {}".format(limit_num)) query = default_query - if 'query' in command.keys(): - query = command['query'] - object_type = object_types[command['object']] + if "query" in command.keys(): + query = command["query"] + object_type = object_types[command["object"]] res = salesforce.query( - query.format(object_type['fields'], object_type['table'], qarg, limit_num) + query.format(object_type["fields"], object_type["table"], qarg, limit_num) ) exclude_keys = [] # type: List[str] - if 'exclude_keys' in command.keys(): - exclude_keys = command['exclude_keys'] + if "exclude_keys" in command.keys(): + exclude_keys = command["exclude_keys"] force_keys = [] # type: List[str] - if 'force_keys' in command.keys(): - force_keys = command['force_keys'] + if "force_keys" in command.keys(): + force_keys = command["force_keys"] rank_output = False - if 'rank_output' in command.keys(): - rank_output = command['rank_output'] - show_all_keys = 'show' in split_args - if 'show_all_keys' in command.keys(): - show_all_keys = command['show_all_keys'] or 'show' in split_args + if "rank_output" in command.keys(): + rank_output = command["rank_output"] + show_all_keys = "show" in split_args + if "show_all_keys" in command.keys(): + show_all_keys = command["show_all_keys"] or "show" in split_args return format_result( res, exclude_keys=exclude_keys, @@ -118,21 +118,21 @@ def query_salesforce( def get_salesforce_link_details(link: str, sf: Any) -> str: - re_id = re.compile('/[A-Za-z0-9]{18}') + re_id = re.compile("/[A-Za-z0-9]{18}") re_id_res = re_id.search(link) if re_id_res is None: - return 'Invalid salesforce link' - id = re_id_res.group().strip('/') + return "Invalid salesforce link" + id = re_id_res.group().strip("/") for object_type in object_types.values(): - res = sf.query(link_query.format(object_type['fields'], object_type['table'], id)) - if res['totalSize'] == 1: + res = sf.query(link_query.format(object_type["fields"], object_type["table"], id)) + if res["totalSize"] == 1: return format_result(res) - return 'No object found. Make sure it is of the supported types. Type `help` for more info.' + return "No object found. Make sure it is of the supported types. Type `help` for more info." class SalesforceHandler: def usage(self) -> str: - return ''' + return """ This is a Salesforce bot, which can search for Contacts, Accounts and Opportunities. And can be configured for any other object types. @@ -140,44 +140,44 @@ class SalesforceHandler: It will also show details of any Salesforce links posted. @-mention the bot with 'help' to see available commands. - ''' + """ def get_salesforce_response(self, content: str) -> str: content = content.strip() - if content == '' or content == 'help': + if content == "" or content == "help": return get_help_text() - if content.startswith('http') and 'force' in content: + if content.startswith("http") and "force" in content: return get_salesforce_link_details(content, self.sf) for command in commands: - for command_keyword in command['commands']: + for command_keyword in command["commands"]: if content.startswith(command_keyword): - args = content.replace(command_keyword, '').strip() - if args is not None and args != '': - if 'callback' in command.keys(): - return command['callback'](args, self.sf, command) + args = content.replace(command_keyword, "").strip() + if args is not None and args != "": + if "callback" in command.keys(): + return command["callback"](args, self.sf, command) else: return query_salesforce(args, self.sf, command) else: - return 'Usage: {} [arguments]'.format(command['template']) + return "Usage: {} [arguments]".format(command["template"]) return get_help_text() def initialize(self, bot_handler: BotHandler) -> None: - self.config_info = bot_handler.get_config_info('salesforce') + self.config_info = bot_handler.get_config_info("salesforce") try: self.sf = simple_salesforce.Salesforce( - username=self.config_info['username'], - password=self.config_info['password'], - security_token=self.config_info['security_token'], + username=self.config_info["username"], + password=self.config_info["password"], + security_token=self.config_info["security_token"], ) except simple_salesforce.exceptions.SalesforceAuthenticationFailed as err: - bot_handler.quit('Failed to log in to Salesforce. {} {}'.format(err.code, err.message)) + bot_handler.quit("Failed to log in to Salesforce. {} {}".format(err.code, err.message)) def handle_message(self, message: Dict[str, Any], bot_handler: BotHandler) -> None: try: - bot_response = self.get_salesforce_response(message['content']) + bot_response = self.get_salesforce_response(message["content"]) bot_handler.send_reply(message, bot_response) except Exception as e: - bot_handler.send_reply(message, 'Error. {}.'.format(e), bot_response) + bot_handler.send_reply(message, "Error. {}.".format(e), bot_response) handler_class = SalesforceHandler diff --git a/zulip_bots/zulip_bots/bots/salesforce/test_salesforce.py b/zulip_bots/zulip_bots/bots/salesforce/test_salesforce.py index 21be0c9..23ab65f 100644 --- a/zulip_bots/zulip_bots/bots/salesforce/test_salesforce.py +++ b/zulip_bots/zulip_bots/bots/salesforce/test_salesforce.py @@ -10,9 +10,9 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests, StubBotHandler, read_ @contextmanager def mock_salesforce_query(test_name: str, bot_name: str) -> Iterator[None]: response_data = read_bot_fixture_data(bot_name, test_name) - sf_response = response_data.get('response') + sf_response = response_data.get("response") - with patch('simple_salesforce.api.Salesforce.query') as mock_query: + with patch("simple_salesforce.api.Salesforce.query") as mock_query: mock_query.return_value = sf_response yield @@ -20,13 +20,13 @@ def mock_salesforce_query(test_name: str, bot_name: str) -> Iterator[None]: @contextmanager def mock_salesforce_auth(is_success: bool) -> Iterator[None]: if is_success: - with patch('simple_salesforce.api.Salesforce.__init__') as mock_sf_init: + with patch("simple_salesforce.api.Salesforce.__init__") as mock_sf_init: mock_sf_init.return_value = None yield else: with patch( - 'simple_salesforce.api.Salesforce.__init__', - side_effect=SalesforceAuthenticationFailed(403, 'auth failed'), + "simple_salesforce.api.Salesforce.__init__", + side_effect=SalesforceAuthenticationFailed(403, "auth failed"), ) as mock_sf_init: mock_sf_init.return_value = None yield @@ -34,15 +34,15 @@ def mock_salesforce_auth(is_success: bool) -> Iterator[None]: @contextmanager def mock_salesforce_commands_types() -> Iterator[None]: - with patch('zulip_bots.bots.salesforce.utils.commands', mock_commands), patch( - 'zulip_bots.bots.salesforce.utils.object_types', mock_object_types + with patch("zulip_bots.bots.salesforce.utils.commands", mock_commands), patch( + "zulip_bots.bots.salesforce.utils.object_types", mock_object_types ): yield -mock_config = {'username': 'name@example.com', 'password': 'foo', 'security_token': 'abcdefg'} +mock_config = {"username": "name@example.com", "password": "foo", "security_token": "abcdefg"} -help_text = '''Salesforce bot +help_text = """Salesforce bot This bot can do simple salesforce query requests **All commands must be @-mentioned to the bot.** Commands: @@ -60,7 +60,7 @@ These are the types of Salesforce object supported by this bot. The bot cannot show the details of any other object types. Table Table -''' +""" def echo(arg: str, sf: Any, command: Dict[str, Any]) -> str: @@ -69,29 +69,29 @@ def echo(arg: str, sf: Any, command: Dict[str, Any]) -> str: mock_commands = [ { - 'commands': ['find contact'], - 'object': 'contact', - 'description': 'finds contacts', - 'template': 'find contact ', + "commands": ["find contact"], + "object": "contact", + "description": "finds contacts", + "template": "find contact ", }, { - 'commands': ['find top opportunities'], - 'object': 'opportunity', - 'query': 'SELECT {} FROM {} WHERE isClosed=false ORDER BY amount DESC LIMIT {}', - 'description': 'finds opportunities', - 'template': 'find top opportunities ', - 'rank_output': True, - 'force_keys': ['Amount'], - 'exclude_keys': ['Status'], - 'show_all_keys': True, + "commands": ["find top opportunities"], + "object": "opportunity", + "query": "SELECT {} FROM {} WHERE isClosed=false ORDER BY amount DESC LIMIT {}", + "description": "finds opportunities", + "template": "find top opportunities ", + "rank_output": True, + "force_keys": ["Amount"], + "exclude_keys": ["Status"], + "show_all_keys": True, }, - {'commands': ['echo'], 'callback': echo}, + {"commands": ["echo"], "callback": echo}, ] mock_object_types = { - 'contact': {'fields': 'Id, Name, Phone', 'table': 'Table'}, - 'opportunity': {'fields': 'Id, Name, Amount, Status', 'table': 'Table'}, + "contact": {"fields": "Id, Name, Phone", "table": "Table"}, + "opportunity": {"fields": "Id, Name, Amount, Status", "table": "Table"}, } @@ -101,7 +101,7 @@ class TestSalesforceBot(BotTestCase, DefaultTests): def _test(self, test_name: str, message: str, response: str, auth_success: bool = True) -> None: with self.mock_config_info(mock_config), mock_salesforce_auth( auth_success - ), mock_salesforce_query(test_name, 'salesforce'), mock_salesforce_commands_types(): + ), mock_salesforce_query(test_name, "salesforce"), mock_salesforce_commands_types(): self.verify_reply(message, response) def _test_initialize(self, auth_success: bool = True) -> None: @@ -111,74 +111,74 @@ class TestSalesforceBot(BotTestCase, DefaultTests): bot, bot_handler = self._get_handlers() def test_bot_responds_to_empty_message(self) -> None: - self._test('test_one_result', '', help_text) + self._test("test_one_result", "", help_text) def test_one_result(self) -> None: - res = '''**[foo](https://login.salesforce.com/foo_id)** + res = """**[foo](https://login.salesforce.com/foo_id)** >**Phone**: 020 1234 5678 -''' - self._test('test_one_result', 'find contact foo', res) +""" + self._test("test_one_result", "find contact foo", res) def test_multiple_results(self) -> None: - res = '**[foo](https://login.salesforce.com/foo_id)**\n**[bar](https://login.salesforce.com/bar_id)**\n' - self._test('test_multiple_results', 'find contact foo', res) + res = "**[foo](https://login.salesforce.com/foo_id)**\n**[bar](https://login.salesforce.com/bar_id)**\n" + self._test("test_multiple_results", "find contact foo", res) def test_arg_show(self) -> None: - res = '''**[foo](https://login.salesforce.com/foo_id)** + res = """**[foo](https://login.salesforce.com/foo_id)** >**Phone**: 020 1234 5678 **[bar](https://login.salesforce.com/bar_id)** >**Phone**: 020 5678 1234 -''' - self._test('test_multiple_results', 'find contact foo -show', res) +""" + self._test("test_multiple_results", "find contact foo -show", res) def test_no_results(self) -> None: - self._test('test_no_results', 'find contact foo', 'No records found.') + self._test("test_no_results", "find contact foo", "No records found.") def test_rank_and_force_keys(self) -> None: - res = '''1) **[foo](https://login.salesforce.com/foo_id)** + res = """1) **[foo](https://login.salesforce.com/foo_id)** >**Amount**: 2 2) **[bar](https://login.salesforce.com/bar_id)** >**Amount**: 1 -''' - self._test('test_top_opportunities', 'find top opportunities 2', res) +""" + self._test("test_top_opportunities", "find top opportunities 2", res) def test_limit_arg(self) -> None: - res = '''**[foo](https://login.salesforce.com/foo_id)** + res = """**[foo](https://login.salesforce.com/foo_id)** >**Phone**: 020 1234 5678 -''' - with self.assertLogs(level='INFO') as log: - self._test('test_one_result', 'find contact foo -limit 1', res) - self.assertIn('INFO:root:Searching with limit 1', log.output) +""" + with self.assertLogs(level="INFO") as log: + self._test("test_one_result", "find contact foo -limit 1", res) + self.assertIn("INFO:root:Searching with limit 1", log.output) def test_help(self) -> None: - self._test('test_one_result', 'help', help_text) - self._test('test_one_result', 'foo bar baz', help_text) - self._test('test_one_result', 'find contact', 'Usage: find contact [arguments]') + self._test("test_one_result", "help", help_text) + self._test("test_one_result", "foo bar baz", help_text) + self._test("test_one_result", "find contact", "Usage: find contact [arguments]") def test_bad_auth(self) -> None: with self.assertRaises(StubBotHandler.BotQuitException): self._test_initialize(auth_success=False) def test_callback(self) -> None: - self._test('test_one_result', 'echo hello', 'hello') + self._test("test_one_result", "echo hello", "hello") def test_link_normal(self) -> None: - res = '''**[foo](https://login.salesforce.com/foo_id)** + res = """**[foo](https://login.salesforce.com/foo_id)** >**Phone**: 020 1234 5678 -''' - self._test('test_one_result', 'https://login.salesforce.com/1c3e5g7i9k1m3o5q7s', res) +""" + self._test("test_one_result", "https://login.salesforce.com/1c3e5g7i9k1m3o5q7s", res) def test_link_invalid(self) -> None: self._test( - 'test_one_result', - 'https://login.salesforce.com/foo/bar/1c3e5g7$i9k1m3o5q7', - 'Invalid salesforce link', + "test_one_result", + "https://login.salesforce.com/foo/bar/1c3e5g7$i9k1m3o5q7", + "Invalid salesforce link", ) def test_link_no_results(self) -> None: - res = 'No object found. Make sure it is of the supported types. Type `help` for more info.' - self._test('test_no_results', 'https://login.salesforce.com/1c3e5g7i9k1m3o5q7s', res) + res = "No object found. Make sure it is of the supported types. Type `help` for more info." + self._test("test_no_results", "https://login.salesforce.com/1c3e5g7i9k1m3o5q7s", res) diff --git a/zulip_bots/zulip_bots/bots/salesforce/utils.py b/zulip_bots/zulip_bots/bots/salesforce/utils.py index 457b9cf..313fb43 100644 --- a/zulip_bots/zulip_bots/bots/salesforce/utils.py +++ b/zulip_bots/zulip_bots/bots/salesforce/utils.py @@ -1,56 +1,56 @@ from typing import Any, Dict, List -link_query = 'SELECT {} FROM {} WHERE Id=\'{}\'' -default_query = 'SELECT {} FROM {} WHERE Name LIKE \'%{}%\' LIMIT {}' +link_query = "SELECT {} FROM {} WHERE Id='{}'" +default_query = "SELECT {} FROM {} WHERE Name LIKE '%{}%' LIMIT {}" commands = [ { - 'commands': ['search account', 'find account', 'search accounts', 'find accounts'], - 'object': 'account', - 'description': 'Returns a list of accounts of the name specified', - 'template': 'search account ', + "commands": ["search account", "find account", "search accounts", "find accounts"], + "object": "account", + "description": "Returns a list of accounts of the name specified", + "template": "search account ", }, { - 'commands': ['search contact', 'find contact', 'search contacts', 'find contacts'], - 'object': 'contact', - 'description': 'Returns a list of contacts of the name specified', - 'template': 'search contact ', + "commands": ["search contact", "find contact", "search contacts", "find contacts"], + "object": "contact", + "description": "Returns a list of contacts of the name specified", + "template": "search contact ", }, { - 'commands': [ - 'search opportunity', - 'find opportunity', - 'search opportunities', - 'find opportunities', + "commands": [ + "search opportunity", + "find opportunity", + "search opportunities", + "find opportunities", ], - 'object': 'opportunity', - 'description': 'Returns a list of opportunities of the name specified', - 'template': 'search opportunity ', + "object": "opportunity", + "description": "Returns a list of opportunities of the name specified", + "template": "search opportunity ", }, { - 'commands': [ - 'search top opportunity', - 'find top opportunity', - 'search top opportunities', - 'find top opportunities', + "commands": [ + "search top opportunity", + "find top opportunity", + "search top opportunities", + "find top opportunities", ], - 'object': 'opportunity', - 'query': 'SELECT {} FROM {} WHERE isClosed=false ORDER BY amount DESC LIMIT {}', - 'description': 'Returns a list of opportunities organised by amount', - 'template': 'search top opportunities ', - 'rank_output': True, - 'force_keys': ['Amount'], + "object": "opportunity", + "query": "SELECT {} FROM {} WHERE isClosed=false ORDER BY amount DESC LIMIT {}", + "description": "Returns a list of opportunities organised by amount", + "template": "search top opportunities ", + "rank_output": True, + "force_keys": ["Amount"], }, ] # type: List[Dict[str, Any]] object_types = { - 'account': { - 'fields': 'Id, Name, Phone, BillingStreet, BillingCity, BillingState', - 'table': 'Account', + "account": { + "fields": "Id, Name, Phone, BillingStreet, BillingCity, BillingState", + "table": "Account", }, - 'contact': {'fields': 'Id, Name, Phone, MobilePhone, Email', 'table': 'Contact'}, - 'opportunity': { - 'fields': 'Id, Name, Amount, Probability, StageName, CloseDate', - 'table': 'Opportunity', + "contact": {"fields": "Id, Name, Phone, MobilePhone, Email", "table": "Contact"}, + "opportunity": { + "fields": "Id, Name, Amount, Probability, StageName, CloseDate", + "table": "Opportunity", }, } # type: Dict[str, Dict[str, str]] diff --git a/zulip_bots/zulip_bots/bots/stack_overflow/stack_overflow.py b/zulip_bots/zulip_bots/bots/stack_overflow/stack_overflow.py index 61c7be4..6315001 100644 --- a/zulip_bots/zulip_bots/bots/stack_overflow/stack_overflow.py +++ b/zulip_bots/zulip_bots/bots/stack_overflow/stack_overflow.py @@ -9,27 +9,27 @@ from zulip_bots.lib import BotHandler class StackOverflowHandler: - ''' + """ This plugin facilitates searching Stack Overflow for a specific query and returns the top 3 questions from the search. It looks for messages starting with '@mention-bot' In this example, we write all Stack Overflow searches into the same stream that it was called from. - ''' + """ META = { - 'name': 'StackOverflow', - 'description': 'Searches Stack Overflow for a query and returns the top 3 articles.', + "name": "StackOverflow", + "description": "Searches Stack Overflow for a query and returns the top 3 articles.", } def usage(self) -> str: - return ''' + return """ This plugin will allow users to directly search Stack Overflow for a specific query and get the top 3 articles that are returned from the search. Users should preface query with "@mention-bot". - @mention-bot ''' + @mention-bot """ def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: bot_response = self.get_bot_stackoverflow_response(message, bot_handler) @@ -38,47 +38,47 @@ class StackOverflowHandler: def get_bot_stackoverflow_response( self, message: Dict[str, str], bot_handler: BotHandler ) -> Optional[str]: - '''This function returns the URLs of the requested topic.''' + """This function returns the URLs of the requested topic.""" - help_text = 'Please enter your query after @mention-bot to search StackOverflow' + help_text = "Please enter your query after @mention-bot to search StackOverflow" # Checking if the link exists. - query = message['content'] - if query == '' or query == 'help': + query = message["content"] + if query == "" or query == "help": return help_text - query_stack_url = 'http://api.stackexchange.com/2.2/search/advanced' - query_stack_params = dict(order='desc', sort='relevance', site='stackoverflow', title=query) + query_stack_url = "http://api.stackexchange.com/2.2/search/advanced" + query_stack_params = dict(order="desc", sort="relevance", site="stackoverflow", title=query) try: data = requests.get(query_stack_url, params=query_stack_params) except requests.exceptions.RequestException: - logging.error('broken link') + logging.error("broken link") return ( - 'Uh-Oh ! Sorry ,couldn\'t process the request right now.:slightly_frowning_face:\n' - 'Please try again later.' + "Uh-Oh ! Sorry ,couldn't process the request right now.:slightly_frowning_face:\n" + "Please try again later." ) # Checking if the bot accessed the link. if data.status_code != 200: - logging.error('Page not found.') + logging.error("Page not found.") return ( - 'Uh-Oh ! Sorry ,couldn\'t process the request right now.:slightly_frowning_face:\n' - 'Please try again later.' + "Uh-Oh ! Sorry ,couldn't process the request right now.:slightly_frowning_face:\n" + "Please try again later." ) - new_content = 'For search term:' + query + '\n' + new_content = "For search term:" + query + "\n" # Checking if there is content for the searched term - if len(data.json()['items']) == 0: + if len(data.json()["items"]) == 0: new_content = ( - 'I am sorry. The search term you provided is not found :slightly_frowning_face:' + "I am sorry. The search term you provided is not found :slightly_frowning_face:" ) else: - for i in range(min(3, len(data.json()['items']))): - search_string = data.json()['items'][i]['title'] - link = data.json()['items'][i]['link'] - new_content += str(i + 1) + ' : ' + '[' + search_string + ']' + '(' + link + ')\n' + for i in range(min(3, len(data.json()["items"]))): + search_string = data.json()["items"][i]["title"] + link = data.json()["items"][i]["link"] + new_content += str(i + 1) + " : " + "[" + search_string + "]" + "(" + link + ")\n" return new_content diff --git a/zulip_bots/zulip_bots/bots/stack_overflow/test_stack_overflow.py b/zulip_bots/zulip_bots/bots/stack_overflow/test_stack_overflow.py index 351491f..9636a05 100755 --- a/zulip_bots/zulip_bots/bots/stack_overflow/test_stack_overflow.py +++ b/zulip_bots/zulip_bots/bots/stack_overflow/test_stack_overflow.py @@ -8,52 +8,52 @@ class TestStackoverflowBot(BotTestCase, DefaultTests): def test_bot(self) -> None: # Single-word query - bot_request = 'restful' - bot_response = '''For search term:restful + bot_request = "restful" + bot_response = """For search term:restful 1 : [What exactly is RESTful programming?](https://stackoverflow.com/questions/671118/what-exactly-is-restful-programming) 2 : [RESTful Authentication](https://stackoverflow.com/questions/319530/restful-authentication) 3 : [RESTful URL design for search](https://stackoverflow.com/questions/319530/restful-authentication) -''' - with self.mock_http_conversation('test_single_word'): +""" + with self.mock_http_conversation("test_single_word"): self.verify_reply(bot_request, bot_response) # Multi-word query - bot_request = 'what is flutter' - bot_response = '''For search term:what is flutter + bot_request = "what is flutter" + bot_response = """For search term:what is flutter 1 : [What is flutter/dart and what are its benefits over other tools?](https://stackoverflow.com/questions/49023008/what-is-flutter-dart-and-what-are-its-benefits-over-other-tools) -''' - with self.mock_http_conversation('test_multi_word'): +""" + with self.mock_http_conversation("test_multi_word"): self.verify_reply(bot_request, bot_response) # Number query - bot_request = '113' - bot_response = '''For search term:113 + bot_request = "113" + bot_response = """For search term:113 1 : [INSTALL_FAILED_NO_MATCHING_ABIS res-113](https://stackoverflow.com/questions/47117788/install-failed-no-matching-abis-res-113) 2 : [com.sun.tools.xjc.reader.Ring.get(Ring.java:113)](https://stackoverflow.com/questions/12848282/com-sun-tools-xjc-reader-ring-getring-java113) 3 : [no route to host error 113](https://stackoverflow.com/questions/10516222/no-route-to-host-error-113) -''' - with self.mock_http_conversation('test_number_query'): +""" + with self.mock_http_conversation("test_number_query"): self.verify_reply(bot_request, bot_response) # Incorrect word - bot_request = 'narendra' + bot_request = "narendra" bot_response = ( "I am sorry. The search term you provided is not found :slightly_frowning_face:" ) - with self.mock_http_conversation('test_incorrect_query'): + with self.mock_http_conversation("test_incorrect_query"): self.verify_reply(bot_request, bot_response) # 404 status code - bot_request = 'Zulip' + bot_request = "Zulip" bot_response = ( - 'Uh-Oh ! Sorry ,couldn\'t process the request right now.:slightly_frowning_face:\n' - 'Please try again later.' + "Uh-Oh ! Sorry ,couldn't process the request right now.:slightly_frowning_face:\n" + "Please try again later." ) - with self.mock_http_conversation('test_status_code'): + with self.mock_http_conversation("test_status_code"): self.verify_reply(bot_request, bot_response) # Request Exception - bot_request = 'Z' + bot_request = "Z" with mock_request_exception(): self.verify_reply(bot_request, bot_response) diff --git a/zulip_bots/zulip_bots/bots/susi/susi.py b/zulip_bots/zulip_bots/bots/susi/susi.py index 31f09db..ab36122 100644 --- a/zulip_bots/zulip_bots/bots/susi/susi.py +++ b/zulip_bots/zulip_bots/bots/susi/susi.py @@ -6,13 +6,13 @@ from zulip_bots.lib import BotHandler class SusiHandler: - ''' + """ Susi AI Bot To create and know more of SUSI skills go to `https://skills.susi.ai/` - ''' + """ def usage(self) -> str: - return ''' + return """ Hi, I am Susi, people generally ask me these questions: ``` What is the exchange rate of USD to BTC @@ -36,16 +36,16 @@ class SusiHandler: distance between india and singapore tell me latest phone by LG ``` - ''' + """ def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: - msg = message['content'] - if msg == 'help' or msg == '': + msg = message["content"] + if msg == "help" or msg == "": bot_handler.send_reply(message, self.usage()) return reply = requests.get("https://api.susi.ai/susi/chat.json", params=dict(q=msg)) try: - answer = reply.json()['answers'][0]['actions'][0]['expression'] + answer = reply.json()["answers"][0]["actions"][0]["expression"] except Exception: answer = "I don't understand. Can you rephrase?" bot_handler.send_reply(message, answer) diff --git a/zulip_bots/zulip_bots/bots/susi/test_susi.py b/zulip_bots/zulip_bots/bots/susi/test_susi.py index 6f60792..194a389 100644 --- a/zulip_bots/zulip_bots/bots/susi/test_susi.py +++ b/zulip_bots/zulip_bots/bots/susi/test_susi.py @@ -5,7 +5,7 @@ class TestSusiBot(BotTestCase, DefaultTests): bot_name = "susi" def test_help(self) -> None: - bot_response = ''' + bot_response = """ Hi, I am Susi, people generally ask me these questions: ``` What is the exchange rate of USD to BTC @@ -29,14 +29,14 @@ class TestSusiBot(BotTestCase, DefaultTests): distance between india and singapore tell me latest phone by LG ``` - ''' + """ - self.verify_reply('', bot_response) - self.verify_reply('help', bot_response) + self.verify_reply("", bot_response) + self.verify_reply("help", bot_response) def test_issue(self) -> None: - request = 'hi' - bot_response = 'Hello!' + request = "hi" + bot_response = "Hello!" - with self.mock_http_conversation('test_reply'): + with self.mock_http_conversation("test_reply"): self.verify_reply(request, bot_response) diff --git a/zulip_bots/zulip_bots/bots/tictactoe/test_tictactoe.py b/zulip_bots/zulip_bots/bots/tictactoe/test_tictactoe.py index 70f4824..fb2a9ab 100644 --- a/zulip_bots/zulip_bots/bots/tictactoe/test_tictactoe.py +++ b/zulip_bots/zulip_bots/bots/tictactoe/test_tictactoe.py @@ -5,7 +5,7 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests class TestTicTacToeBot(BotTestCase, DefaultTests): - bot_name = 'tictactoe' + bot_name = "tictactoe" # FIXME: Add tests for computer moves # FIXME: Add test lib for game_handler @@ -32,8 +32,8 @@ class TestTicTacToeBot(BotTestCase, DefaultTests): def test_determine_game_over_with_win(self) -> None: board = [[1, 1, 1], [0, 2, 0], [2, 0, 2]] - players = ['Human', 'Computer'] - response = 'current turn' + players = ["Human", "Computer"] + response = "current turn" self._test_determine_game_over_with_win(board, players, response) def _test_determine_game_over_with_win( @@ -46,8 +46,8 @@ class TestTicTacToeBot(BotTestCase, DefaultTests): def test_determine_game_over_with_draw(self) -> None: board = [[1, 2, 1], [1, 2, 1], [2, 1, 2]] - players = ['Human', 'Computer'] - response = 'draw' + players = ["Human", "Computer"] + response = "draw" self._test_determine_game_over_with_draw(board, players, response) def _test_determine_game_over_with_draw( @@ -113,7 +113,7 @@ class TestTicTacToeBot(BotTestCase, DefaultTests): def test_player_color(self) -> None: turn = 0 - response = ':x:' + response = ":x:" self._test_player_color(turn, response) def _test_player_color(self, turn: int, expected_response: str) -> None: @@ -127,19 +127,19 @@ class TestTicTacToeBot(BotTestCase, DefaultTests): self.assertNotEqual(message_handler.get_player_color(0), None) self.assertNotEqual(message_handler.game_start_message(), None) self.assertEqual( - message_handler.alert_move_message('foo', 'move 3'), 'foo put a token at 3' + message_handler.alert_move_message("foo", "move 3"), "foo put a token at 3" ) def test_has_attributes(self) -> None: model, message_handler = self._get_game_handlers() - self.assertTrue(hasattr(message_handler, 'parse_board') is not None) - self.assertTrue(hasattr(message_handler, 'alert_move_message') is not None) - self.assertTrue(hasattr(model, 'current_board') is not None) - self.assertTrue(hasattr(model, 'determine_game_over') is not None) + self.assertTrue(hasattr(message_handler, "parse_board") is not None) + self.assertTrue(hasattr(message_handler, "alert_move_message") is not None) + self.assertTrue(hasattr(model, "current_board") is not None) + self.assertTrue(hasattr(model, "determine_game_over") is not None) def test_parse_board(self) -> None: board = [[0, 1, 0], [0, 0, 0], [0, 0, 2]] - response = ':one: :x: :three:\n\n' + ':four: :five: :six:\n\n' + ':seven: :eight: :o:\n\n' + response = ":one: :x: :three:\n\n" + ":four: :five: :six:\n\n" + ":seven: :eight: :o:\n\n" self._test_parse_board(board, response) def _test_parse_board(self, board: List[List[int]], expected_response: str) -> None: @@ -151,19 +151,19 @@ class TestTicTacToeBot(BotTestCase, DefaultTests): if bot is None: bot, bot_handler = self._get_handlers() message = { - 'sender_email': '{}@example.com'.format(name), - 'sender_full_name': '{}'.format(name), + "sender_email": "{}@example.com".format(name), + "sender_full_name": "{}".format(name), } bot.add_user_to_cache(message) return bot def setup_game(self) -> None: - bot = self.add_user_to_cache('foo') - self.add_user_to_cache('baz', bot) + bot = self.add_user_to_cache("foo") + self.add_user_to_cache("baz", bot) instance = GameInstance( - bot, False, 'test game', 'abc123', ['foo@example.com', 'baz@example.com'], 'test' + bot, False, "test game", "abc123", ["foo@example.com", "baz@example.com"], "test" ) - bot.instances.update({'abc123': instance}) + bot.instances.update({"abc123": instance}) instance.start() return bot diff --git a/zulip_bots/zulip_bots/bots/tictactoe/tictactoe.py b/zulip_bots/zulip_bots/bots/tictactoe/tictactoe.py index 053a831..11f07b6 100644 --- a/zulip_bots/zulip_bots/bots/tictactoe/tictactoe.py +++ b/zulip_bots/zulip_bots/bots/tictactoe/tictactoe.py @@ -37,13 +37,13 @@ class TicTacToeModel: def determine_game_over(self, players: List[str]) -> str: if self.contains_winning_move(self.current_board): - return 'current turn' + return "current turn" if self.board_is_full(self.current_board): - return 'draw' - return '' + return "draw" + return "" def board_is_full(self, board: Any) -> bool: - '''Determines if the board is full or not.''' + """Determines if the board is full or not.""" for row in board: for element in row: if element == 0: @@ -52,8 +52,8 @@ class TicTacToeModel: # Used for current board & trial computer board def contains_winning_move(self, board: Any) -> bool: - '''Returns true if all coordinates in a triplet have the same value in them (x or o) and no coordinates - in the triplet are blank.''' + """Returns true if all coordinates in a triplet have the same value in them (x or o) and no coordinates + in the triplet are blank.""" for triplet in self.triplets: if ( self.get_value(board, triplet[0]) @@ -65,7 +65,7 @@ class TicTacToeModel: return False def get_locations_of_char(self, board: Any, char: int) -> List[List[int]]: - '''Gets the locations of the board that have char in them.''' + """Gets the locations of the board that have char in them.""" locations = [] for row in range(3): for col in range(3): @@ -74,8 +74,8 @@ class TicTacToeModel: return locations def two_blanks(self, triplet: List[Tuple[int, int]], board: Any) -> List[Tuple[int, int]]: - '''Determines which rows/columns/diagonals have two blank spaces and an 2 already in them. It's more advantageous - for the computer to move there. This is used when the computer makes its move.''' + """Determines which rows/columns/diagonals have two blank spaces and an 2 already in them. It's more advantageous + for the computer to move there. This is used when the computer makes its move.""" o_found = False for position in triplet: @@ -94,7 +94,7 @@ class TicTacToeModel: return [] def computer_move(self, board: Any, player_number: Any) -> Any: - '''The computer's logic for making its move.''' + """The computer's logic for making its move.""" my_board = copy.deepcopy(board) # First the board is copied; used later on blank_locations = self.get_locations_of_char(my_board, 0) # Gets the locations that already have x's @@ -184,7 +184,7 @@ class TicTacToeModel: return board def is_valid_move(self, move: str) -> bool: - '''Checks the validity of the coordinate input passed in to make sure it's not out-of-bounds (ex. 5, 5)''' + """Checks the validity of the coordinate input passed in to make sure it's not out-of-bounds (ex. 5, 5)""" try: split_move = move.split(",") row = split_move[0].strip() @@ -201,54 +201,54 @@ class TicTacToeModel: return self.computer_move(self.current_board, player_number + 1) move_coords_str = coords_from_command(move) if not self.is_valid_move(move_coords_str): - raise BadMoveException('Make sure your move is from 0-9') + raise BadMoveException("Make sure your move is from 0-9") board = self.current_board - move_coords = move_coords_str.split(',') + move_coords = move_coords_str.split(",") # Subtraction must be done to convert to the right indices, # since computers start numbering at 0. row = (int(move_coords[1])) - 1 column = (int(move_coords[0])) - 1 if board[row][column] != 0: - raise BadMoveException('Make sure your space hasn\'t already been filled.') + raise BadMoveException("Make sure your space hasn't already been filled.") board[row][column] = player_number + 1 return board class TicTacToeMessageHandler: - tokens = [':x:', ':o:'] + tokens = [":x:", ":o:"] def parse_row(self, row: Tuple[int, int], row_num: int) -> str: - '''Takes the row passed in as a list and returns it as a string.''' + """Takes the row passed in as a list and returns it as a string.""" row_chars = [] num_symbols = [ - ':one:', - ':two:', - ':three:', - ':four:', - ':five:', - ':six:', - ':seven:', - ':eight:', - ':nine:', + ":one:", + ":two:", + ":three:", + ":four:", + ":five:", + ":six:", + ":seven:", + ":eight:", + ":nine:", ] for i, e in enumerate(row): if e == 0: row_chars.append(num_symbols[row_num * 3 + i]) else: row_chars.append(self.get_player_color(e - 1)) - row_string = ' '.join(row_chars) - return row_string + '\n\n' + row_string = " ".join(row_chars) + return row_string + "\n\n" def parse_board(self, board: Any) -> str: - '''Takes the board as a nested list and returns a nice version for the user.''' + """Takes the board as a nested list and returns a nice version for the user.""" return "".join([self.parse_row(r, r_num) for r_num, r in enumerate(board)]) def get_player_color(self, turn: int) -> str: return self.tokens[turn] def alert_move_message(self, original_player: str, move_info: str) -> str: - move_info = move_info.replace('move ', '') - return '{} put a token at {}'.format(original_player, move_info) + move_info = move_info.replace("move ", "") + return "{} put a token at {}".format(original_player, move_info) def game_start_message(self) -> str: return ( @@ -257,30 +257,30 @@ class TicTacToeMessageHandler: class ticTacToeHandler(GameAdapter): - ''' + """ You can play tic-tac-toe! Make sure your message starts with "@mention-bot". - ''' + """ META = { - 'name': 'TicTacToe', - 'description': 'Lets you play Tic-tac-toe against a computer.', + "name": "TicTacToe", + "description": "Lets you play Tic-tac-toe against a computer.", } def usage(self) -> str: - return ''' + return """ You can play tic-tac-toe now! Make sure your message starts with @mention-bot. - ''' + """ def __init__(self) -> None: - game_name = 'Tic Tac Toe' - bot_name = 'tictactoe' - move_help_message = '* To move during a game, type\n`move ` or ``' - move_regex = r'(move (\d)$)|((\d)$)' + game_name = "Tic Tac Toe" + bot_name = "tictactoe" + move_help_message = "* To move during a game, type\n`move ` or ``" + move_regex = r"(move (\d)$)|((\d)$)" model = TicTacToeModel gameMessageHandler = TicTacToeMessageHandler - rules = '''Try to get three in horizontal or vertical or diagonal row to win the game.''' + rules = """Try to get three in horizontal or vertical or diagonal row to win the game.""" super().__init__( game_name, bot_name, @@ -296,10 +296,10 @@ class ticTacToeHandler(GameAdapter): def coords_from_command(cmd: str) -> str: # This function translates the input command into a TicTacToeGame move. # It should return two indices, each one of (1,2,3), separated by a comma, eg. "3,2" - '''As there are various ways to input a coordinate (with/without parentheses, with/without spaces, etc.) the - input is stripped to just the numbers before being used in the program.''' - cmd_num = int(cmd.replace('move ', '')) - 1 - cmd = '{},{}'.format((cmd_num % 3) + 1, (cmd_num // 3) + 1) + """As there are various ways to input a coordinate (with/without parentheses, with/without spaces, etc.) the + input is stripped to just the numbers before being used in the program.""" + cmd_num = int(cmd.replace("move ", "")) - 1 + cmd = "{},{}".format((cmd_num % 3) + 1, (cmd_num // 3) + 1) return cmd diff --git a/zulip_bots/zulip_bots/bots/trello/test_trello.py b/zulip_bots/zulip_bots/bots/trello/test_trello.py index 91c5cdb..60af417 100644 --- a/zulip_bots/zulip_bots/bots/trello/test_trello.py +++ b/zulip_bots/zulip_bots/bots/trello/test_trello.py @@ -3,111 +3,111 @@ from unittest.mock import patch from zulip_bots.bots.trello.trello import TrelloHandler from zulip_bots.test_lib import BotTestCase, DefaultTests, StubBotHandler -mock_config = {'api_key': 'TEST', 'access_token': 'TEST', 'user_name': 'TEST'} +mock_config = {"api_key": "TEST", "access_token": "TEST", "user_name": "TEST"} class TestTrelloBot(BotTestCase, DefaultTests): bot_name = "trello" # type: str def test_bot_responds_to_empty_message(self) -> None: - with self.mock_config_info(mock_config), patch('requests.get'): - self.verify_reply('', 'Empty Query') + with self.mock_config_info(mock_config), patch("requests.get"): + self.verify_reply("", "Empty Query") def test_bot_usage(self) -> None: - with self.mock_config_info(mock_config), patch('requests.get'): + with self.mock_config_info(mock_config), patch("requests.get"): self.verify_reply( - 'help', - ''' + "help", + """ This interactive bot can be used to interact with Trello. Use `list-commands` to get information about the supported commands. - ''', + """, ) def test_bot_quit_with_invalid_config(self) -> None: with self.mock_config_info(mock_config), self.assertRaises(StubBotHandler.BotQuitException): - with self.mock_http_conversation('invalid_key'): + with self.mock_http_conversation("invalid_key"): TrelloHandler().initialize(StubBotHandler()) def test_invalid_command(self) -> None: - with self.mock_config_info(mock_config), patch('requests.get'): - self.verify_reply('abcd', 'Command not supported') + with self.mock_config_info(mock_config), patch("requests.get"): + self.verify_reply("abcd", "Command not supported") def test_list_commands_command(self) -> None: expected_reply = ( - '**Commands:** \n' - '1. **help**: Get the bot usage information.\n' - '2. **list-commands**: Get information about the commands supported by the bot.\n' - '3. **get-all-boards**: Get all the boards under the configured account.\n' - '4. **get-all-cards **: Get all the cards in the given board.\n' - '5. **get-all-checklists **: Get all the checklists in the given card.\n' - '6. **get-all-lists **: Get all the lists in the given board.\n' + "**Commands:** \n" + "1. **help**: Get the bot usage information.\n" + "2. **list-commands**: Get information about the commands supported by the bot.\n" + "3. **get-all-boards**: Get all the boards under the configured account.\n" + "4. **get-all-cards **: Get all the cards in the given board.\n" + "5. **get-all-checklists **: Get all the checklists in the given card.\n" + "6. **get-all-lists **: Get all the lists in the given board.\n" ) - with self.mock_config_info(mock_config), patch('requests.get'): - self.verify_reply('list-commands', expected_reply) + with self.mock_config_info(mock_config), patch("requests.get"): + self.verify_reply("list-commands", expected_reply) def test_get_all_boards_command(self) -> None: - with self.mock_config_info(mock_config), patch('requests.get'): - with self.mock_http_conversation('get_all_boards'): - self.verify_reply('get-all-boards', '**Boards:**\n') + with self.mock_config_info(mock_config), patch("requests.get"): + with self.mock_http_conversation("get_all_boards"): + self.verify_reply("get-all-boards", "**Boards:**\n") - with self.mock_http_conversation('get_board_descs'): + with self.mock_http_conversation("get_board_descs"): bot_instance = TrelloHandler() bot_instance.initialize(StubBotHandler()) - self.assertEqual(bot_instance.get_board_descs(['TEST']), '1.[TEST](TEST) (`TEST`)') + self.assertEqual(bot_instance.get_board_descs(["TEST"]), "1.[TEST](TEST) (`TEST`)") def test_get_all_cards_command(self) -> None: - with self.mock_config_info(mock_config), patch('requests.get'): - with self.mock_http_conversation('get_cards'): - self.verify_reply('get-all-cards TEST', '**Cards:**\n1. [TEST](TEST) (`TEST`)') + with self.mock_config_info(mock_config), patch("requests.get"): + with self.mock_http_conversation("get_cards"): + self.verify_reply("get-all-cards TEST", "**Cards:**\n1. [TEST](TEST) (`TEST`)") def test_get_all_checklists_command(self) -> None: - with self.mock_config_info(mock_config), patch('requests.get'): - with self.mock_http_conversation('get_checklists'): + with self.mock_config_info(mock_config), patch("requests.get"): + with self.mock_http_conversation("get_checklists"): self.verify_reply( - 'get-all-checklists TEST', - '**Checklists:**\n' - '1. `TEST`:\n' - ' * [X] TEST_1\n * [X] TEST_2\n' - ' * [-] TEST_3\n * [-] TEST_4', + "get-all-checklists TEST", + "**Checklists:**\n" + "1. `TEST`:\n" + " * [X] TEST_1\n * [X] TEST_2\n" + " * [-] TEST_3\n * [-] TEST_4", ) def test_get_all_lists_command(self) -> None: - with self.mock_config_info(mock_config), patch('requests.get'): - with self.mock_http_conversation('get_lists'): + with self.mock_config_info(mock_config), patch("requests.get"): + with self.mock_http_conversation("get_lists"): self.verify_reply( - 'get-all-lists TEST', - ('**Lists:**\n' '1. TEST_A\n' ' * TEST_1\n' '2. TEST_B\n' ' * TEST_2'), + "get-all-lists TEST", + ("**Lists:**\n" "1. TEST_A\n" " * TEST_1\n" "2. TEST_B\n" " * TEST_2"), ) def test_command_exceptions(self) -> None: """Add appropriate tests here for all additional commands with try/except blocks. This ensures consistency.""" - expected_error_response = 'Invalid Response. Please check configuration and parameters.' + expected_error_response = "Invalid Response. Please check configuration and parameters." - with self.mock_config_info(mock_config), patch('requests.get'): - with self.mock_http_conversation('exception_boards'): - self.verify_reply('get-all-boards', expected_error_response) + with self.mock_config_info(mock_config), patch("requests.get"): + with self.mock_http_conversation("exception_boards"): + self.verify_reply("get-all-boards", expected_error_response) - with self.mock_http_conversation('exception_cards'): - self.verify_reply('get-all-cards TEST', expected_error_response) + with self.mock_http_conversation("exception_cards"): + self.verify_reply("get-all-cards TEST", expected_error_response) - with self.mock_http_conversation('exception_checklists'): - self.verify_reply('get-all-checklists TEST', expected_error_response) + with self.mock_http_conversation("exception_checklists"): + self.verify_reply("get-all-checklists TEST", expected_error_response) - with self.mock_http_conversation('exception_lists'): - self.verify_reply('get-all-lists TEST', expected_error_response) + with self.mock_http_conversation("exception_lists"): + self.verify_reply("get-all-lists TEST", expected_error_response) def test_command_invalid_arguments(self) -> None: """Add appropriate tests here for all additional commands with more than one arguments. This ensures consistency.""" - expected_error_response = 'Invalid Arguments.' + expected_error_response = "Invalid Arguments." - with self.mock_config_info(mock_config), patch('requests.get'): - self.verify_reply('get-all-cards', expected_error_response) - self.verify_reply('get-all-checklists', expected_error_response) - self.verify_reply('get-all-lists', expected_error_response) + with self.mock_config_info(mock_config), patch("requests.get"): + self.verify_reply("get-all-cards", expected_error_response) + self.verify_reply("get-all-checklists", expected_error_response) + self.verify_reply("get-all-lists", expected_error_response) diff --git a/zulip_bots/zulip_bots/bots/trello/trello.py b/zulip_bots/zulip_bots/bots/trello/trello.py index 767fc16..9dd0340 100644 --- a/zulip_bots/zulip_bots/bots/trello/trello.py +++ b/zulip_bots/zulip_bots/bots/trello/trello.py @@ -5,87 +5,87 @@ import requests from zulip_bots.lib import BotHandler supported_commands = [ - ('help', 'Get the bot usage information.'), - ('list-commands', 'Get information about the commands supported by the bot.'), - ('get-all-boards', 'Get all the boards under the configured account.'), - ('get-all-cards ', 'Get all the cards in the given board.'), - ('get-all-checklists ', 'Get all the checklists in the given card.'), - ('get-all-lists ', 'Get all the lists in the given board.'), + ("help", "Get the bot usage information."), + ("list-commands", "Get information about the commands supported by the bot."), + ("get-all-boards", "Get all the boards under the configured account."), + ("get-all-cards ", "Get all the cards in the given board."), + ("get-all-checklists ", "Get all the checklists in the given card."), + ("get-all-lists ", "Get all the lists in the given board."), ] -INVALID_ARGUMENTS_ERROR_MESSAGE = 'Invalid Arguments.' -RESPONSE_ERROR_MESSAGE = 'Invalid Response. Please check configuration and parameters.' +INVALID_ARGUMENTS_ERROR_MESSAGE = "Invalid Arguments." +RESPONSE_ERROR_MESSAGE = "Invalid Response. Please check configuration and parameters." class TrelloHandler: def initialize(self, bot_handler: BotHandler) -> None: - self.config_info = bot_handler.get_config_info('trello') - self.api_key = self.config_info['api_key'] - self.access_token = self.config_info['access_token'] - self.user_name = self.config_info['user_name'] + self.config_info = bot_handler.get_config_info("trello") + self.api_key = self.config_info["api_key"] + self.access_token = self.config_info["access_token"] + self.user_name = self.config_info["user_name"] - self.auth_params = {'key': self.api_key, 'token': self.access_token} + self.auth_params = {"key": self.api_key, "token": self.access_token} self.check_access_token(bot_handler) def check_access_token(self, bot_handler: BotHandler) -> None: test_query_response = requests.get( - 'https://api.trello.com/1/members/{}/'.format(self.user_name), params=self.auth_params + "https://api.trello.com/1/members/{}/".format(self.user_name), params=self.auth_params ) - if test_query_response.text == 'invalid key': - bot_handler.quit('Invalid Credentials. Please see doc.md to find out how to get them.') + if test_query_response.text == "invalid key": + bot_handler.quit("Invalid Credentials. Please see doc.md to find out how to get them.") def usage(self) -> str: - return ''' + return """ This interactive bot can be used to interact with Trello. Use `list-commands` to get information about the supported commands. - ''' + """ def handle_message(self, message: Dict[str, Any], bot_handler: BotHandler) -> None: - content = message['content'].strip().split() + content = message["content"].strip().split() if content == []: - bot_handler.send_reply(message, 'Empty Query') + bot_handler.send_reply(message, "Empty Query") return content[0] = content[0].lower() - if content == ['help']: + if content == ["help"]: bot_handler.send_reply(message, self.usage()) return - if content == ['list-commands']: + if content == ["list-commands"]: bot_reply = self.get_all_supported_commands() - elif content == ['get-all-boards']: + elif content == ["get-all-boards"]: bot_reply = self.get_all_boards() else: - if content[0] == 'get-all-cards': + if content[0] == "get-all-cards": bot_reply = self.get_all_cards(content) - elif content[0] == 'get-all-checklists': + elif content[0] == "get-all-checklists": bot_reply = self.get_all_checklists(content) - elif content[0] == 'get-all-lists': + elif content[0] == "get-all-lists": bot_reply = self.get_all_lists(content) else: - bot_reply = 'Command not supported' + bot_reply = "Command not supported" bot_handler.send_reply(message, bot_reply) def get_all_supported_commands(self) -> str: - bot_response = '**Commands:** \n' + bot_response = "**Commands:** \n" for index, (command, desc) in enumerate(supported_commands): - bot_response += '{}. **{}**: {}\n'.format(index + 1, command, desc) + bot_response += "{}. **{}**: {}\n".format(index + 1, command, desc) return bot_response def get_all_boards(self) -> str: - get_board_ids_url = 'https://api.trello.com/1/members/{}/'.format(self.user_name) + get_board_ids_url = "https://api.trello.com/1/members/{}/".format(self.user_name) board_ids_response = requests.get(get_board_ids_url, params=self.auth_params) try: - boards = board_ids_response.json()['idBoards'] - bot_response = '**Boards:**\n' + self.get_board_descs(boards) + boards = board_ids_response.json()["idBoards"] + bot_response = "**Boards:**\n" + self.get_board_descs(boards) except (KeyError, ValueError, TypeError): return RESPONSE_ERROR_MESSAGE @@ -94,7 +94,7 @@ class TrelloHandler: def get_board_descs(self, boards: List[str]) -> str: bot_response = [] # type: List[str] - get_board_desc_url = 'https://api.trello.com/1/boards/{}/' + get_board_desc_url = "https://api.trello.com/1/boards/{}/" for index, board in enumerate(boards): board_desc_response = requests.get( get_board_desc_url.format(board), params=self.auth_params @@ -102,82 +102,82 @@ class TrelloHandler: board_data = board_desc_response.json() bot_response += [ - '{_count}.[{name}]({url}) (`{id}`)'.format(_count=index + 1, **board_data) + "{_count}.[{name}]({url}) (`{id}`)".format(_count=index + 1, **board_data) ] - return '\n'.join(bot_response) + return "\n".join(bot_response) def get_all_cards(self, content: List[str]) -> str: if len(content) != 2: return INVALID_ARGUMENTS_ERROR_MESSAGE board_id = content[1] - get_cards_url = 'https://api.trello.com/1/boards/{}/cards'.format(board_id) + get_cards_url = "https://api.trello.com/1/boards/{}/cards".format(board_id) cards_response = requests.get(get_cards_url, params=self.auth_params) try: cards = cards_response.json() - bot_response = ['**Cards:**'] + bot_response = ["**Cards:**"] for index, card in enumerate(cards): bot_response += [ - '{_count}. [{name}]({url}) (`{id}`)'.format(_count=index + 1, **card) + "{_count}. [{name}]({url}) (`{id}`)".format(_count=index + 1, **card) ] except (KeyError, ValueError, TypeError): return RESPONSE_ERROR_MESSAGE - return '\n'.join(bot_response) + return "\n".join(bot_response) def get_all_checklists(self, content: List[str]) -> str: if len(content) != 2: return INVALID_ARGUMENTS_ERROR_MESSAGE card_id = content[1] - get_checklists_url = 'https://api.trello.com/1/cards/{}/checklists/'.format(card_id) + get_checklists_url = "https://api.trello.com/1/cards/{}/checklists/".format(card_id) checklists_response = requests.get(get_checklists_url, params=self.auth_params) try: checklists = checklists_response.json() - bot_response = ['**Checklists:**'] + bot_response = ["**Checklists:**"] for index, checklist in enumerate(checklists): - bot_response += ['{}. `{}`:'.format(index + 1, checklist['name'])] + bot_response += ["{}. `{}`:".format(index + 1, checklist["name"])] - if 'checkItems' in checklist: - for item in checklist['checkItems']: + if "checkItems" in checklist: + for item in checklist["checkItems"]: bot_response += [ - ' * [{}] {}'.format( - 'X' if item['state'] == 'complete' else '-', item['name'] + " * [{}] {}".format( + "X" if item["state"] == "complete" else "-", item["name"] ) ] except (KeyError, ValueError, TypeError): return RESPONSE_ERROR_MESSAGE - return '\n'.join(bot_response) + return "\n".join(bot_response) def get_all_lists(self, content: List[str]) -> str: if len(content) != 2: return INVALID_ARGUMENTS_ERROR_MESSAGE board_id = content[1] - get_lists_url = 'https://api.trello.com/1/boards/{}/lists'.format(board_id) + get_lists_url = "https://api.trello.com/1/boards/{}/lists".format(board_id) lists_response = requests.get(get_lists_url, params=self.auth_params) try: lists = lists_response.json() - bot_response = ['**Lists:**'] + bot_response = ["**Lists:**"] for index, _list in enumerate(lists): - bot_response += ['{}. {}'.format(index + 1, _list['name'])] + bot_response += ["{}. {}".format(index + 1, _list["name"])] - if 'cards' in _list: - for card in _list['cards']: - bot_response += [' * {}'.format(card['name'])] + if "cards" in _list: + for card in _list["cards"]: + bot_response += [" * {}".format(card["name"])] except (KeyError, ValueError, TypeError): return RESPONSE_ERROR_MESSAGE - return '\n'.join(bot_response) + return "\n".join(bot_response) handler_class = TrelloHandler diff --git a/zulip_bots/zulip_bots/bots/trivia_quiz/test_trivia_quiz.py b/zulip_bots/zulip_bots/bots/trivia_quiz/test_trivia_quiz.py index 08b87cc..4730a3f 100644 --- a/zulip_bots/zulip_bots/bots/trivia_quiz/test_trivia_quiz.py +++ b/zulip_bots/zulip_bots/bots/trivia_quiz/test_trivia_quiz.py @@ -18,18 +18,18 @@ class TestTriviaQuizBot(BotTestCase, DefaultTests): bot_name = "trivia_quiz" # type: str new_question_response = ( - '\nQ: Which class of animals are newts members of?\n\n' - + '* **A** Amphibian\n' - + '* **B** Fish\n' - + '* **C** Reptiles\n' - + '* **D** Mammals\n' - + '**reply**: answer Q001 ' + "\nQ: Which class of animals are newts members of?\n\n" + + "* **A** Amphibian\n" + + "* **B** Fish\n" + + "* **C** Reptiles\n" + + "* **D** Mammals\n" + + "**reply**: answer Q001 " ) def get_test_quiz(self) -> Tuple[Dict[str, Any], Any]: bot_handler = StubBotHandler() - quiz_payload = read_bot_fixture_data('trivia_quiz', 'test_new_question')['response'] - with patch('random.shuffle'): + quiz_payload = read_bot_fixture_data("trivia_quiz", "test_new_question")["response"] + with patch("random.shuffle"): quiz = get_quiz_from_payload(quiz_payload) return quiz, bot_handler @@ -41,119 +41,119 @@ class TestTriviaQuizBot(BotTestCase, DefaultTests): self.verify_reply(message, response) def test_bot_responds_to_empty_message(self) -> None: - self._test('', 'type "new" for a new question') + self._test("", 'type "new" for a new question') def test_bot_new_question(self) -> None: - with patch('random.shuffle'): - self._test('new', self.new_question_response, 'test_new_question') + with patch("random.shuffle"): + self._test("new", self.new_question_response, "test_new_question") def test_question_not_available(self) -> None: - self._test('new', 'Uh-Oh! Trivia service is down.', 'test_status_code') + self._test("new", "Uh-Oh! Trivia service is down.", "test_status_code") with mock_request_exception(): - self.verify_reply('new', 'Uh-Oh! Trivia service is down.') + self.verify_reply("new", "Uh-Oh! Trivia service is down.") def test_fix_quotes(self) -> None: - self.assertEqual(fix_quotes('test & test'), html.unescape('test & test')) - print('f') - with patch('html.unescape') as mock_html_unescape: + self.assertEqual(fix_quotes("test & test"), html.unescape("test & test")) + print("f") + with patch("html.unescape") as mock_html_unescape: mock_html_unescape.side_effect = Exception with self.assertRaises(Exception) as exception: - fix_quotes('test') + fix_quotes("test") self.assertEqual( str(exception.exception), "Please use python3.4 or later for this bot." ) def test_invalid_answer(self) -> None: - invalid_replies = ['answer A', 'answer A Q10', 'answer Q001 K', 'answer 001 A'] + invalid_replies = ["answer A", "answer A Q10", "answer Q001 K", "answer 001 A"] for reply in invalid_replies: - self._test(reply, 'Invalid answer format') + self._test(reply, "Invalid answer format") def test_invalid_quiz_id(self) -> None: - self._test('answer Q100 A', 'Invalid quiz id') + self._test("answer Q100 A", "Invalid quiz id") def test_answers(self) -> None: - quiz_payload = read_bot_fixture_data('trivia_quiz', 'test_new_question')['response'] - with patch('random.shuffle'): + quiz_payload = read_bot_fixture_data("trivia_quiz", "test_new_question")["response"] + with patch("random.shuffle"): quiz = get_quiz_from_payload(quiz_payload) # Test initial storage - self.assertEqual(quiz['question'], 'Which class of animals are newts members of?') - self.assertEqual(quiz['correct_letter'], 'A') - self.assertEqual(quiz['answers']['D'], 'Mammals') - self.assertEqual(quiz['answered_options'], []) - self.assertEqual(quiz['pending'], True) + self.assertEqual(quiz["question"], "Which class of animals are newts members of?") + self.assertEqual(quiz["correct_letter"], "A") + self.assertEqual(quiz["answers"]["D"], "Mammals") + self.assertEqual(quiz["answered_options"], []) + self.assertEqual(quiz["pending"], True) # test incorrect answer with patch( - 'zulip_bots.bots.trivia_quiz.trivia_quiz.get_quiz_from_id', + "zulip_bots.bots.trivia_quiz.trivia_quiz.get_quiz_from_id", return_value=json.dumps(quiz), ): - self._test('answer Q001 B', ':disappointed: WRONG, Foo Test User! B is not correct.') + self._test("answer Q001 B", ":disappointed: WRONG, Foo Test User! B is not correct.") # test correct answer with patch( - 'zulip_bots.bots.trivia_quiz.trivia_quiz.get_quiz_from_id', + "zulip_bots.bots.trivia_quiz.trivia_quiz.get_quiz_from_id", return_value=json.dumps(quiz), ): - with patch('zulip_bots.bots.trivia_quiz.trivia_quiz.start_new_quiz'): - self._test('answer Q001 A', ':tada: **Amphibian** is correct, Foo Test User!') + with patch("zulip_bots.bots.trivia_quiz.trivia_quiz.start_new_quiz"): + self._test("answer Q001 A", ":tada: **Amphibian** is correct, Foo Test User!") def test_update_quiz(self) -> None: quiz, bot_handler = self.get_test_quiz() - update_quiz(quiz, 'Q001', bot_handler) - test_quiz = json.loads(bot_handler.storage.get('Q001')) + update_quiz(quiz, "Q001", bot_handler) + test_quiz = json.loads(bot_handler.storage.get("Q001")) self.assertEqual(test_quiz, quiz) def test_get_quiz_from_id(self) -> None: quiz, bot_handler = self.get_test_quiz() - bot_handler.storage.put('Q001', quiz) - self.assertEqual(get_quiz_from_id('Q001', bot_handler), quiz) + bot_handler.storage.put("Q001", quiz) + self.assertEqual(get_quiz_from_id("Q001", bot_handler), quiz) def test_handle_answer(self) -> None: quiz, bot_handler = self.get_test_quiz() # create test initial storage - update_quiz(quiz, 'Q001', bot_handler) + update_quiz(quiz, "Q001", bot_handler) # test for a correct answer - start_new_question, response = handle_answer(quiz, 'A', 'Q001', bot_handler, 'Test user') + start_new_question, response = handle_answer(quiz, "A", "Q001", bot_handler, "Test user") self.assertTrue(start_new_question) - self.assertEqual(response, ':tada: **Amphibian** is correct, Test user!') + self.assertEqual(response, ":tada: **Amphibian** is correct, Test user!") # test for an incorrect answer - start_new_question, response = handle_answer(quiz, 'D', 'Q001', bot_handler, 'Test User') + start_new_question, response = handle_answer(quiz, "D", "Q001", bot_handler, "Test User") self.assertFalse(start_new_question) - self.assertEqual(response, ':disappointed: WRONG, Test User! D is not correct.') + self.assertEqual(response, ":disappointed: WRONG, Test User! D is not correct.") def test_handle_answer_three_failed_attempts(self) -> None: quiz, bot_handler = self.get_test_quiz() # create test storage for a question which has been incorrectly answered twice - quiz['answered_options'] = ['C', 'B'] - update_quiz(quiz, 'Q001', bot_handler) + quiz["answered_options"] = ["C", "B"] + update_quiz(quiz, "Q001", bot_handler) # test response and storage after three failed attempts - start_new_question, response = handle_answer(quiz, 'D', 'Q001', bot_handler, 'Test User') + start_new_question, response = handle_answer(quiz, "D", "Q001", bot_handler, "Test User") self.assertEqual( - response, ':disappointed: WRONG, Test User! The correct answer is **Amphibian**.' + response, ":disappointed: WRONG, Test User! The correct answer is **Amphibian**." ) self.assertTrue(start_new_question) - quiz_reset = json.loads(bot_handler.storage.get('Q001')) - self.assertEqual(quiz_reset['pending'], False) + quiz_reset = json.loads(bot_handler.storage.get("Q001")) + self.assertEqual(quiz_reset["pending"], False) # test response after question has ended - incorrect_answers = ['B', 'C', 'D'] + incorrect_answers = ["B", "C", "D"] for ans in incorrect_answers: start_new_question, response = handle_answer( - quiz, ans, 'Q001', bot_handler, 'Test User' + quiz, ans, "Q001", bot_handler, "Test User" ) self.assertEqual( - response, ':disappointed: WRONG, Test User! The correct answer is **Amphibian**.' + response, ":disappointed: WRONG, Test User! The correct answer is **Amphibian**." ) self.assertFalse(start_new_question) - start_new_question, response = handle_answer(quiz, 'A', 'Q001', bot_handler, 'Test User') - self.assertEqual(response, ':tada: **Amphibian** is correct, Test User!') + start_new_question, response = handle_answer(quiz, "A", "Q001", bot_handler, "Test User") + self.assertEqual(response, ":tada: **Amphibian** is correct, Test User!") self.assertFalse(start_new_question) # test storage after question has ended - quiz_reset = json.loads(bot_handler.storage.get('Q001')) - self.assertEqual(quiz_reset['pending'], False) + quiz_reset = json.loads(bot_handler.storage.get("Q001")) + self.assertEqual(quiz_reset["pending"], False) diff --git a/zulip_bots/zulip_bots/bots/trivia_quiz/trivia_quiz.py b/zulip_bots/zulip_bots/bots/trivia_quiz/trivia_quiz.py index 468022a..1ebc07e 100644 --- a/zulip_bots/zulip_bots/bots/trivia_quiz/trivia_quiz.py +++ b/zulip_bots/zulip_bots/bots/trivia_quiz/trivia_quiz.py @@ -19,36 +19,36 @@ class InvalidAnswerException(Exception): class TriviaQuizHandler: def usage(self) -> str: - return ''' + return """ This plugin will give users a trivia question from - the open trivia database at opentdb.com.''' + the open trivia database at opentdb.com.""" def handle_message(self, message: Dict[str, Any], bot_handler: BotHandler) -> None: - query = message['content'] - if query == 'new': + query = message["content"] + if query == "new": try: start_new_quiz(message, bot_handler) return except NotAvailableException: - bot_response = 'Uh-Oh! Trivia service is down.' + bot_response = "Uh-Oh! Trivia service is down." bot_handler.send_reply(message, bot_response) return - elif query.startswith('answer'): + elif query.startswith("answer"): try: (quiz_id, answer) = parse_answer(query) except InvalidAnswerException: - bot_response = 'Invalid answer format' + bot_response = "Invalid answer format" bot_handler.send_reply(message, bot_response) return try: quiz_payload = get_quiz_from_id(quiz_id, bot_handler) except (KeyError, TypeError): - bot_response = 'Invalid quiz id' + bot_response = "Invalid quiz id" bot_handler.send_reply(message, bot_response) return quiz = json.loads(quiz_payload) start_new_question, bot_response = handle_answer( - quiz, answer, quiz_id, bot_handler, message['sender_full_name'] + quiz, answer, quiz_id, bot_handler, message["sender_full_name"] ) bot_handler.send_reply(message, bot_response) if start_new_question: @@ -73,13 +73,13 @@ def start_new_quiz(message: Dict[str, Any], bot_handler: BotHandler) -> None: def parse_answer(query: str) -> Tuple[str, str]: - m = re.match(r'answer\s+(Q...)\s+(.)', query) + m = re.match(r"answer\s+(Q...)\s+(.)", query) if not m: raise InvalidAnswerException() quiz_id = m.group(1) answer = m.group(2).upper() - if answer not in 'ABCD': + if answer not in "ABCD": raise InvalidAnswerException() return (quiz_id, answer) @@ -93,7 +93,7 @@ def get_trivia_quiz() -> Dict[str, Any]: def get_trivia_payload() -> Dict[str, Any]: - url = 'https://opentdb.com/api.php?amount=1&type=multiple' + url = "https://opentdb.com/api.php?amount=1&type=multiple" try: data = requests.get(url) @@ -117,19 +117,19 @@ def fix_quotes(s: str) -> Optional[str]: try: return html.unescape(s) except Exception: - raise Exception('Please use python3.4 or later for this bot.') + raise Exception("Please use python3.4 or later for this bot.") def get_quiz_from_payload(payload: Dict[str, Any]) -> Dict[str, Any]: - result = payload['results'][0] - question = result['question'] - letters = ['A', 'B', 'C', 'D'] + result = payload["results"][0] + question = result["question"] + letters = ["A", "B", "C", "D"] random.shuffle(letters) correct_letter = letters[0] answers = dict() - answers[correct_letter] = result['correct_answer'] + answers[correct_letter] = result["correct_answer"] for i in range(3): - answers[letters[i + 1]] = result['incorrect_answers'][i] + answers[letters[i + 1]] = result["incorrect_answers"][i] answers = {letter: fix_quotes(answer) for letter, answer in answers.items()} quiz = dict( question=fix_quotes(question), @@ -143,38 +143,38 @@ def get_quiz_from_payload(payload: Dict[str, Any]) -> Dict[str, Any]: def generate_quiz_id(storage: Any) -> str: try: - quiz_num = storage.get('quiz_id') + quiz_num = storage.get("quiz_id") except (KeyError, TypeError): quiz_num = 0 quiz_num += 1 quiz_num = quiz_num % (1000) - storage.put('quiz_id', quiz_num) - quiz_id = 'Q%03d' % (quiz_num,) + storage.put("quiz_id", quiz_num) + quiz_id = "Q%03d" % (quiz_num,) return quiz_id def format_quiz_for_widget(quiz_id: str, quiz: Dict[str, Any]) -> str: - widget_type = 'zform' - question = quiz['question'] - answers = quiz['answers'] + widget_type = "zform" + question = quiz["question"] + answers = quiz["answers"] - heading = quiz_id + ': ' + question + heading = quiz_id + ": " + question def get_choice(letter: str) -> Dict[str, str]: answer = answers[letter] - reply = 'answer ' + quiz_id + ' ' + letter + reply = "answer " + quiz_id + " " + letter return dict( - type='multiple_choice', + type="multiple_choice", short_name=letter, long_name=answer, reply=reply, ) - choices = [get_choice(letter) for letter in 'ABCD'] + choices = [get_choice(letter) for letter in "ABCD"] extra_data = dict( - type='choices', + type="choices", heading=heading, choices=choices, ) @@ -188,24 +188,24 @@ def format_quiz_for_widget(quiz_id: str, quiz: Dict[str, Any]) -> str: def format_quiz_for_markdown(quiz_id: str, quiz: Dict[str, Any]) -> str: - question = quiz['question'] - answers = quiz['answers'] - answer_list = '\n'.join( + question = quiz["question"] + answers = quiz["answers"] + answer_list = "\n".join( [ - '* **{letter}** {answer}'.format( + "* **{letter}** {answer}".format( letter=letter, answer=answers[letter], ) - for letter in 'ABCD' + for letter in "ABCD" ] ) - how_to_respond = '''**reply**: answer {quiz_id} '''.format(quiz_id=quiz_id) + how_to_respond = """**reply**: answer {quiz_id} """.format(quiz_id=quiz_id) - content = ''' + content = """ Q: {question} {answer_list} -{how_to_respond}'''.format( +{how_to_respond}""".format( question=question, answer_list=answer_list, how_to_respond=how_to_respond, @@ -219,29 +219,29 @@ def update_quiz(quiz: Dict[str, Any], quiz_id: str, bot_handler: BotHandler) -> def build_response(is_correct: bool, num_answers: int) -> str: if is_correct: - response = ':tada: **{answer}** is correct, {sender_name}!' + response = ":tada: **{answer}** is correct, {sender_name}!" else: if num_answers >= 3: - response = ':disappointed: WRONG, {sender_name}! The correct answer is **{answer}**.' + response = ":disappointed: WRONG, {sender_name}! The correct answer is **{answer}**." else: - response = ':disappointed: WRONG, {sender_name}! {option} is not correct.' + response = ":disappointed: WRONG, {sender_name}! {option} is not correct." return response def handle_answer( quiz: Dict[str, Any], option: str, quiz_id: str, bot_handler: BotHandler, sender_name: str ) -> Tuple[bool, str]: - answer = quiz['answers'][quiz['correct_letter']] - is_new_answer = option not in quiz['answered_options'] + answer = quiz["answers"][quiz["correct_letter"]] + is_new_answer = option not in quiz["answered_options"] if is_new_answer: - quiz['answered_options'].append(option) + quiz["answered_options"].append(option) - num_answers = len(quiz['answered_options']) - is_correct = option == quiz['correct_letter'] + num_answers = len(quiz["answered_options"]) + is_correct = option == quiz["correct_letter"] - start_new_question = quiz['pending'] and (is_correct or num_answers >= 3) + start_new_question = quiz["pending"] and (is_correct or num_answers >= 3) if start_new_question or is_correct: - quiz['pending'] = False + quiz["pending"] = False if is_new_answer or start_new_question: update_quiz(quiz, quiz_id, bot_handler) diff --git a/zulip_bots/zulip_bots/bots/twitpost/test_twitpost.py b/zulip_bots/zulip_bots/bots/twitpost/test_twitpost.py index 34ec197..89a0d68 100644 --- a/zulip_bots/zulip_bots/bots/twitpost/test_twitpost.py +++ b/zulip_bots/zulip_bots/bots/twitpost/test_twitpost.py @@ -7,12 +7,12 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests, StubBotHandler, get_b class TestTwitpostBot(BotTestCase, DefaultTests): bot_name = "twitpost" mock_config = { - 'consumer_key': 'abcdefghijklmnopqrstuvwxy', - 'consumer_secret': 'aabbccddeeffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyy', - 'access_token': '123456789012345678-ABCDefgh1234afdsa678lKj6gHhslsi', - 'access_token_secret': 'yf0SI0x6Ct2OmF0cDQc1E0eLKXrVAPFx4QkZF2f9PfFCt', + "consumer_key": "abcdefghijklmnopqrstuvwxy", + "consumer_secret": "aabbccddeeffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyy", + "access_token": "123456789012345678-ABCDefgh1234afdsa678lKj6gHhslsi", + "access_token_secret": "yf0SI0x6Ct2OmF0cDQc1E0eLKXrVAPFx4QkZF2f9PfFCt", } - api_response = read_bot_fixture_data('twitpost', 'api_response') + api_response = read_bot_fixture_data("twitpost", "api_response") def test_bot_usage(self) -> None: bot = get_bot_message_handler(self.bot_name) @@ -21,16 +21,16 @@ class TestTwitpostBot(BotTestCase, DefaultTests): with self.mock_config_info(self.mock_config): bot.initialize(bot_handler) - self.assertIn('This bot posts on twitter from zulip chat itself', bot.usage()) + self.assertIn("This bot posts on twitter from zulip chat itself", bot.usage()) def test_bot_responds_to_empty_message(self) -> None: with self.mock_config_info(self.mock_config): - self.verify_reply('', 'Please check help for usage.') + self.verify_reply("", "Please check help for usage.") def test_help(self) -> None: with self.mock_config_info(self.mock_config): self.verify_reply( - 'help', + "help", "*Help for Twitter-post bot* :twitter: : \n\n" "The bot tweets on twitter when message starts with @twitpost.\n\n" "`@twitpost tweet ` will tweet on twitter with given ``.\n" @@ -38,9 +38,9 @@ class TestTwitpostBot(BotTestCase, DefaultTests): " * @twitpost tweet hey batman\n", ) - @patch('tweepy.API.update_status', return_value=api_response) + @patch("tweepy.API.update_status", return_value=api_response) def test_tweet(self, mockedarg): - test_message = 'tweet Maybe he\'ll finally find his keys. #peterfalk' - bot_response = 'Tweet Posted\nhttps://twitter.com/jasoncosta/status/243145735212777472' + test_message = "tweet Maybe he'll finally find his keys. #peterfalk" + bot_response = "Tweet Posted\nhttps://twitter.com/jasoncosta/status/243145735212777472" with self.mock_config_info(self.mock_config): self.verify_reply(test_message, bot_response) diff --git a/zulip_bots/zulip_bots/bots/twitpost/twitpost.py b/zulip_bots/zulip_bots/bots/twitpost/twitpost.py index fd0683f..6c2f0ad 100644 --- a/zulip_bots/zulip_bots/bots/twitpost/twitpost.py +++ b/zulip_bots/zulip_bots/bots/twitpost/twitpost.py @@ -7,9 +7,9 @@ from zulip_bots.lib import BotHandler class TwitpostBot: def usage(self) -> str: - return ''' This bot posts on twitter from zulip chat itself. + return """ This bot posts on twitter from zulip chat itself. Use '@twitpost help' to get more information - on the bot usage. ''' + on the bot usage. """ help_content = ( "*Help for Twitter-post bot* :twitter: : \n\n" @@ -22,23 +22,23 @@ class TwitpostBot: ) def initialize(self, bot_handler: BotHandler) -> None: - self.config_info = bot_handler.get_config_info('twitter') + self.config_info = bot_handler.get_config_info("twitter") auth = tweepy.OAuthHandler( - self.config_info['consumer_key'], self.config_info['consumer_secret'] + self.config_info["consumer_key"], self.config_info["consumer_secret"] ) auth.set_access_token( - self.config_info['access_token'], self.config_info['access_token_secret'] + self.config_info["access_token"], self.config_info["access_token_secret"] ) self.api = tweepy.API(auth, parser=tweepy.parsers.JSONParser()) def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: content = message["content"] - if content.strip() == '': - bot_handler.send_reply(message, 'Please check help for usage.') + if content.strip() == "": + bot_handler.send_reply(message, "Please check help for usage.") return - if content.strip() == 'help': + if content.strip() == "help": bot_handler.send_reply(message, self.help_content) return diff --git a/zulip_bots/zulip_bots/bots/virtual_fs/test_virtual_fs.py b/zulip_bots/zulip_bots/bots/virtual_fs/test_virtual_fs.py index 3801f7f..7b3c0e1 100755 --- a/zulip_bots/zulip_bots/bots/virtual_fs/test_virtual_fs.py +++ b/zulip_bots/zulip_bots/bots/virtual_fs/test_virtual_fs.py @@ -7,21 +7,21 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests class TestVirtualFsBot(BotTestCase, DefaultTests): bot_name = "virtual_fs" help_txt = ( - 'foo@example.com:\n\nThis bot implements a virtual file system for a stream.\n' - 'The locations of text are persisted for the lifetime of the bot\n' - 'running, and if you rename a stream, you will lose the info.\n' - 'Example commands:\n\n```\n' - '@mention-bot sample_conversation: sample conversation with the bot\n' - '@mention-bot mkdir: create a directory\n' - '@mention-bot ls: list a directory\n' - '@mention-bot cd: change directory\n' - '@mention-bot pwd: show current path\n' - '@mention-bot write: write text\n' - '@mention-bot read: read text\n' - '@mention-bot rm: remove a file\n' - '@mention-bot rmdir: remove a directory\n' - '```\n' - 'Use commands like `@mention-bot help write` for more details on specific\ncommands.\n' + "foo@example.com:\n\nThis bot implements a virtual file system for a stream.\n" + "The locations of text are persisted for the lifetime of the bot\n" + "running, and if you rename a stream, you will lose the info.\n" + "Example commands:\n\n```\n" + "@mention-bot sample_conversation: sample conversation with the bot\n" + "@mention-bot mkdir: create a directory\n" + "@mention-bot ls: list a directory\n" + "@mention-bot cd: change directory\n" + "@mention-bot pwd: show current path\n" + "@mention-bot write: write text\n" + "@mention-bot read: read text\n" + "@mention-bot rm: remove a file\n" + "@mention-bot rmdir: remove a directory\n" + "```\n" + "Use commands like `@mention-bot help write` for more details on specific\ncommands.\n" ) def test_multiple_recipient_conversation(self) -> None: @@ -29,23 +29,23 @@ class TestVirtualFsBot(BotTestCase, DefaultTests): ("mkdir home", "foo@example.com:\ndirectory created"), ] message = dict( - display_recipient=[{'email': 'foo@example.com'}, {'email': 'boo@example.com'}], - sender_email='foo@example.com', - sender_full_name='Foo Test User', - sender_id='123', + display_recipient=[{"email": "foo@example.com"}, {"email": "boo@example.com"}], + sender_email="foo@example.com", + sender_full_name="Foo Test User", + sender_id="123", content="mkdir home", ) - with patch('zulip_bots.test_lib.BotTestCase.make_request_message', return_value=message): + with patch("zulip_bots.test_lib.BotTestCase.make_request_message", return_value=message): self.verify_dialog(expected) def test_sample_conversation_help(self) -> None: # There's nothing terribly tricky about the "sample conversation," # so we just do a quick sanity check. - reply = self.get_reply_dict('sample_conversation') - content = reply['content'] - frag = 'foo@example.com:\ncd /\nCurrent path: /\n\n' + reply = self.get_reply_dict("sample_conversation") + content = reply["content"] + frag = "foo@example.com:\ncd /\nCurrent path: /\n\n" self.assertTrue(content.startswith(frag)) - frag = 'read home/stuff/file1\nERROR: file does not exist\n\n' + frag = "read home/stuff/file1\nERROR: file does not exist\n\n" self.assertIn(frag, content) def test_sample_conversation(self) -> None: @@ -54,7 +54,7 @@ class TestVirtualFsBot(BotTestCase, DefaultTests): # for the user's benefit if they ask. But then we can also # use it to test that the bot works as advertised. expected = [ - (request, 'foo@example.com:\n' + reply) for (request, reply) in sample_conversation() + (request, "foo@example.com:\n" + reply) for (request, reply) in sample_conversation() ] self.verify_dialog(expected) diff --git a/zulip_bots/zulip_bots/bots/virtual_fs/virtual_fs.py b/zulip_bots/zulip_bots/bots/virtual_fs/virtual_fs.py index 5b7968c..68fe83c 100644 --- a/zulip_bots/zulip_bots/bots/virtual_fs/virtual_fs.py +++ b/zulip_bots/zulip_bots/bots/virtual_fs/virtual_fs.py @@ -9,32 +9,32 @@ from zulip_bots.lib import BotHandler class VirtualFsHandler: META = { - 'name': 'VirtualFs', - 'description': 'Provides a simple, permanent file system to store and retrieve strings.', + "name": "VirtualFs", + "description": "Provides a simple, permanent file system to store and retrieve strings.", } def usage(self) -> str: return get_help() def handle_message(self, message: Dict[str, Any], bot_handler: BotHandler) -> None: - command = message['content'] + command = message["content"] if command == "": command = "help" - sender = message['sender_email'] + sender = message["sender_email"] - recipient = message['display_recipient'] + recipient = message["display_recipient"] if isinstance(recipient, list): # If not a stream, then hash on list of emails - recipient = " ".join([x['email'] for x in recipient]) + recipient = " ".join([x["email"] for x in recipient]) storage = bot_handler.storage if not storage.contains(recipient): storage.put(recipient, fs_new()) fs = storage.get(recipient) - if sender not in fs['user_paths']: - fs['user_paths'][sender] = '/' + if sender not in fs["user_paths"]: + fs["user_paths"][sender] = "/" fs, msg = fs_command(fs, sender, command) - prependix = '{}:\n'.format(sender) + prependix = "{}:\n".format(sender) msg = prependix + msg storage.put(recipient, fs) @@ -42,7 +42,7 @@ class VirtualFsHandler: def get_help() -> str: - return ''' + return """ This bot implements a virtual file system for a stream. The locations of text are persisted for the lifetime of the bot running, and if you rename a stream, you will lose the info. @@ -61,120 +61,120 @@ Example commands: ``` Use commands like `@mention-bot help write` for more details on specific commands. -''' +""" def sample_conversation() -> List[Tuple[str, str]]: return [ - ('cd /', 'Current path: /'), - ('cd /home', 'ERROR: invalid path'), - ('cd .', 'ERROR: invalid path'), - ('mkdir home', 'directory created'), - ('cd home', 'Current path: /home/'), - ('cd /home/', 'Current path: /home/'), - ('mkdir stuff/', 'ERROR: stuff/ is not a valid name'), - ('mkdir stuff', 'directory created'), - ('write stuff/file1 something', 'file written'), - ('read stuff/file1', 'something'), - ('read /home/stuff/file1', 'something'), - ('read home/stuff/file1', 'ERROR: file does not exist'), - ('pwd ', '/home/'), - ('pwd bla', 'ERROR: syntax: pwd'), - ('ls bla foo', 'ERROR: syntax: ls '), - ('cd /', 'Current path: /'), - ('rm home', 'ERROR: /home/ is a directory, file required'), - ('rmdir home', 'removed'), - ('ls ', 'WARNING: directory is empty'), - ('cd home', 'ERROR: invalid path'), - ('read /home/stuff/file1', 'ERROR: file does not exist'), - ('cd /', 'Current path: /'), - ('write /foo contents of /foo', 'file written'), - ('read /foo', 'contents of /foo'), - ('write /bar Contents: bar bar', 'file written'), - ('read /bar', 'Contents: bar bar'), - ('write /bar invalid', 'ERROR: file already exists'), - ('rm /bar', 'removed'), - ('rm /bar', 'ERROR: file does not exist'), - ('write /bar new bar', 'file written'), - ('read /bar', 'new bar'), - ('write /yo/invalid whatever', 'ERROR: /yo is not a directory'), - ('mkdir /yo', 'directory created'), - ('read /yo', 'ERROR: /yo/ is a directory, file required'), - ('ls /yo', 'WARNING: directory is empty'), - ('read /yo/nada', 'ERROR: file does not exist'), - ('write /yo whatever', 'ERROR: file already exists'), - ('write /yo/apple red', 'file written'), - ('read /yo/apple', 'red'), - ('mkdir /yo/apple', 'ERROR: file already exists'), - ('ls /invalid', 'ERROR: file does not exist'), - ('ls /foo', 'ERROR: /foo is not a directory'), - ('ls /', '* /*bar*\n* /*foo*\n* /yo/'), - ('invalid command', 'ERROR: unrecognized command'), - ('write', 'ERROR: syntax: write '), + ("cd /", "Current path: /"), + ("cd /home", "ERROR: invalid path"), + ("cd .", "ERROR: invalid path"), + ("mkdir home", "directory created"), + ("cd home", "Current path: /home/"), + ("cd /home/", "Current path: /home/"), + ("mkdir stuff/", "ERROR: stuff/ is not a valid name"), + ("mkdir stuff", "directory created"), + ("write stuff/file1 something", "file written"), + ("read stuff/file1", "something"), + ("read /home/stuff/file1", "something"), + ("read home/stuff/file1", "ERROR: file does not exist"), + ("pwd ", "/home/"), + ("pwd bla", "ERROR: syntax: pwd"), + ("ls bla foo", "ERROR: syntax: ls "), + ("cd /", "Current path: /"), + ("rm home", "ERROR: /home/ is a directory, file required"), + ("rmdir home", "removed"), + ("ls ", "WARNING: directory is empty"), + ("cd home", "ERROR: invalid path"), + ("read /home/stuff/file1", "ERROR: file does not exist"), + ("cd /", "Current path: /"), + ("write /foo contents of /foo", "file written"), + ("read /foo", "contents of /foo"), + ("write /bar Contents: bar bar", "file written"), + ("read /bar", "Contents: bar bar"), + ("write /bar invalid", "ERROR: file already exists"), + ("rm /bar", "removed"), + ("rm /bar", "ERROR: file does not exist"), + ("write /bar new bar", "file written"), + ("read /bar", "new bar"), + ("write /yo/invalid whatever", "ERROR: /yo is not a directory"), + ("mkdir /yo", "directory created"), + ("read /yo", "ERROR: /yo/ is a directory, file required"), + ("ls /yo", "WARNING: directory is empty"), + ("read /yo/nada", "ERROR: file does not exist"), + ("write /yo whatever", "ERROR: file already exists"), + ("write /yo/apple red", "file written"), + ("read /yo/apple", "red"), + ("mkdir /yo/apple", "ERROR: file already exists"), + ("ls /invalid", "ERROR: file does not exist"), + ("ls /foo", "ERROR: /foo is not a directory"), + ("ls /", "* /*bar*\n* /*foo*\n* /yo/"), + ("invalid command", "ERROR: unrecognized command"), + ("write", "ERROR: syntax: write "), ] REGEXES = dict( - command='(cd|ls|mkdir|read|rmdir|rm|write|pwd)', - path=r'(\S+)', - optional_path=r'(\S*)', - some_text='(.+)', + command="(cd|ls|mkdir|read|rmdir|rm|write|pwd)", + path=r"(\S+)", + optional_path=r"(\S*)", + some_text="(.+)", ) def get_commands() -> Dict[str, Tuple[Any, List[str]]]: return { - 'help': (fs_help, ['command']), - 'ls': (fs_ls, ['optional_path']), - 'mkdir': (fs_mkdir, ['path']), - 'read': (fs_read, ['path']), - 'rm': (fs_rm, ['path']), - 'rmdir': (fs_rmdir, ['path']), - 'write': (fs_write, ['path', 'some_text']), - 'cd': (fs_cd, ['path']), - 'pwd': (fs_pwd, []), + "help": (fs_help, ["command"]), + "ls": (fs_ls, ["optional_path"]), + "mkdir": (fs_mkdir, ["path"]), + "read": (fs_read, ["path"]), + "rm": (fs_rm, ["path"]), + "rmdir": (fs_rmdir, ["path"]), + "write": (fs_write, ["path", "some_text"]), + "cd": (fs_cd, ["path"]), + "pwd": (fs_pwd, []), } def fs_command(fs: str, user: str, cmd: str) -> Tuple[str, Any]: cmd = cmd.strip() - if cmd == 'help': + if cmd == "help": return fs, get_help() - if cmd == 'sample_conversation': - sample = '\n\n'.join('\n'.join(tup) for tup in sample_conversation()) + if cmd == "sample_conversation": + sample = "\n\n".join("\n".join(tup) for tup in sample_conversation()) return fs, sample cmd_name = cmd.split()[0] cmd_args = cmd[len(cmd_name) :].strip() commands = get_commands() if cmd_name not in commands: - return fs, 'ERROR: unrecognized command' + return fs, "ERROR: unrecognized command" f, arg_names = commands[cmd_name] partial_regexes = [REGEXES[a] for a in arg_names] - regex = ' '.join(partial_regexes) - regex += '$' + regex = " ".join(partial_regexes) + regex += "$" m = re.match(regex, cmd_args) if m: return f(fs, user, *m.groups()) - elif cmd_name == 'help': + elif cmd_name == "help": return fs, get_help() else: - return fs, 'ERROR: ' + syntax_help(cmd_name) + return fs, "ERROR: " + syntax_help(cmd_name) def syntax_help(cmd_name: str) -> str: commands = get_commands() f, arg_names = commands[cmd_name] - arg_syntax = ' '.join('<' + a + '>' for a in arg_names) + arg_syntax = " ".join("<" + a + ">" for a in arg_names) if arg_syntax: - cmd = cmd_name + ' ' + arg_syntax + cmd = cmd_name + " " + arg_syntax else: cmd = cmd_name - return 'syntax: {}'.format(cmd) + return "syntax: {}".format(cmd) def fs_new() -> Dict[str, Any]: - fs = {'/': directory([]), 'user_paths': dict()} + fs = {"/": directory([]), "user_paths": dict()} return fs @@ -187,40 +187,40 @@ def fs_mkdir(fs: Dict[str, Any], user: str, fn: str) -> Tuple[Dict[str, Any], An if msg: return fs, msg if path in fs: - return fs, 'ERROR: file already exists' + return fs, "ERROR: file already exists" dir_path = os.path.dirname(path) if not is_directory(fs, dir_path): - msg = 'ERROR: {} is not a directory'.format(dir_path) + msg = "ERROR: {} is not a directory".format(dir_path) return fs, msg new_fs = fs.copy() - new_dir = directory({path}.union(fs[dir_path]['fns'])) + new_dir = directory({path}.union(fs[dir_path]["fns"])) new_fs[dir_path] = new_dir new_fs[path] = directory([]) - msg = 'directory created' + msg = "directory created" return new_fs, msg def fs_ls(fs: Dict[str, Any], user: str, fn: str) -> Tuple[Dict[str, Any], Any]: - if fn == '.' or fn == '': - path = fs['user_paths'][user] + if fn == "." or fn == "": + path = fs["user_paths"][user] else: path, msg = make_path(fs, user, fn) if msg: return fs, msg if path not in fs: - msg = 'ERROR: file does not exist' + msg = "ERROR: file does not exist" return fs, msg if not is_directory(fs, path): - return fs, 'ERROR: {} is not a directory'.format(path) - fns = fs[path]['fns'] + return fs, "ERROR: {} is not a directory".format(path) + fns = fs[path]["fns"] if not fns: - return fs, 'WARNING: directory is empty' - msg = '\n'.join('* ' + nice_path(fs, path) for path in sorted(fns)) + return fs, "WARNING: directory is empty" + msg = "\n".join("* " + nice_path(fs, path) for path in sorted(fns)) return fs, msg def fs_pwd(fs: Dict[str, Any], user: str) -> Tuple[Dict[str, Any], Any]: - path = fs['user_paths'][user] + path = fs["user_paths"][user] msg = nice_path(fs, path) return fs, msg @@ -230,16 +230,16 @@ def fs_rm(fs: Dict[str, Any], user: str, fn: str) -> Tuple[Dict[str, Any], Any]: if msg: return fs, msg if path not in fs: - msg = 'ERROR: file does not exist' + msg = "ERROR: file does not exist" return fs, msg - if fs[path]['kind'] == 'dir': - msg = 'ERROR: {} is a directory, file required'.format(nice_path(fs, path)) + if fs[path]["kind"] == "dir": + msg = "ERROR: {} is a directory, file required".format(nice_path(fs, path)) return fs, msg new_fs = fs.copy() new_fs.pop(path) directory = get_directory(path) - new_fs[directory]['fns'].remove(path) - msg = 'removed' + new_fs[directory]["fns"].remove(path) + msg = "removed" return new_fs, msg @@ -248,19 +248,19 @@ def fs_rmdir(fs: Dict[str, Any], user: str, fn: str) -> Tuple[Dict[str, Any], An if msg: return fs, msg if path not in fs: - msg = 'ERROR: directory does not exist' + msg = "ERROR: directory does not exist" return fs, msg - if fs[path]['kind'] == 'text': - msg = 'ERROR: {} is a file, directory required'.format(nice_path(fs, path)) + if fs[path]["kind"] == "text": + msg = "ERROR: {} is a file, directory required".format(nice_path(fs, path)) return fs, msg new_fs = fs.copy() new_fs.pop(path) directory = get_directory(path) - new_fs[directory]['fns'].remove(path) + new_fs[directory]["fns"].remove(path) for sub_path in list(new_fs.keys()): - if sub_path.startswith(path + '/'): + if sub_path.startswith(path + "/"): new_fs.pop(sub_path) - msg = 'removed' + msg = "removed" return new_fs, msg @@ -269,17 +269,17 @@ def fs_write(fs: Dict[str, Any], user: str, fn: str, content: str) -> Tuple[Dict if msg: return fs, msg if path in fs: - msg = 'ERROR: file already exists' + msg = "ERROR: file already exists" return fs, msg dir_path = os.path.dirname(path) if not is_directory(fs, dir_path): - msg = 'ERROR: {} is not a directory'.format(dir_path) + msg = "ERROR: {} is not a directory".format(dir_path) return fs, msg new_fs = fs.copy() - new_dir = directory({path}.union(fs[dir_path]['fns'])) + new_dir = directory({path}.union(fs[dir_path]["fns"])) new_fs[dir_path] = new_dir new_fs[path] = text_file(content) - msg = 'file written' + msg = "file written" return new_fs, msg @@ -288,75 +288,75 @@ def fs_read(fs: Dict[str, Any], user: str, fn: str) -> Tuple[Dict[str, Any], Any if msg: return fs, msg if path not in fs: - msg = 'ERROR: file does not exist' + msg = "ERROR: file does not exist" return fs, msg - if fs[path]['kind'] == 'dir': - msg = 'ERROR: {} is a directory, file required'.format(nice_path(fs, path)) + if fs[path]["kind"] == "dir": + msg = "ERROR: {} is a directory, file required".format(nice_path(fs, path)) return fs, msg - val = fs[path]['content'] + val = fs[path]["content"] return fs, val def fs_cd(fs: Dict[str, Any], user: str, fn: str) -> Tuple[Dict[str, Any], Any]: - if len(fn) > 1 and fn[-1] == '/': + if len(fn) > 1 and fn[-1] == "/": fn = fn[:-1] - path = fn if len(fn) > 0 and fn[0] == '/' else make_path(fs, user, fn)[0] + path = fn if len(fn) > 0 and fn[0] == "/" else make_path(fs, user, fn)[0] if path not in fs: - msg = 'ERROR: invalid path' + msg = "ERROR: invalid path" return fs, msg - if fs[path]['kind'] == 'text': - msg = 'ERROR: {} is a file, directory required'.format(nice_path(fs, path)) + if fs[path]["kind"] == "text": + msg = "ERROR: {} is a file, directory required".format(nice_path(fs, path)) return fs, msg - fs['user_paths'][user] = path + fs["user_paths"][user] = path return fs, "Current path: {}".format(nice_path(fs, path)) def make_path(fs: Dict[str, Any], user: str, leaf: str) -> List[str]: - if leaf == '/': - return ['/', ''] - if leaf.endswith('/'): - return ['', 'ERROR: {} is not a valid name'.format(leaf)] - if leaf.startswith('/'): - return [leaf, ''] - path = fs['user_paths'][user] - if not path.endswith('/'): - path += '/' + if leaf == "/": + return ["/", ""] + if leaf.endswith("/"): + return ["", "ERROR: {} is not a valid name".format(leaf)] + if leaf.startswith("/"): + return [leaf, ""] + path = fs["user_paths"][user] + if not path.endswith("/"): + path += "/" path += leaf - return [path, ''] + return [path, ""] def nice_path(fs: Dict[str, Any], path: str) -> str: path_nice = path - slash = path.rfind('/') + slash = path.rfind("/") if path not in fs: - return 'ERROR: the current directory does not exist' - if fs[path]['kind'] == 'text': - path_nice = '{}*{}*'.format(path[: slash + 1], path[slash + 1 :]) - elif path != '/': - path_nice = '{}/'.format(path) + return "ERROR: the current directory does not exist" + if fs[path]["kind"] == "text": + path_nice = "{}*{}*".format(path[: slash + 1], path[slash + 1 :]) + elif path != "/": + path_nice = "{}/".format(path) return path_nice def get_directory(path: str) -> str: - slash = path.rfind('/') + slash = path.rfind("/") if slash == 0: - return '/' + return "/" else: return path[:slash] def directory(fns: Union[Set[str], List[Any]]) -> Dict[str, Union[str, List[Any]]]: - return dict(kind='dir', fns=list(fns)) + return dict(kind="dir", fns=list(fns)) def text_file(content: str) -> Dict[str, str]: - return dict(kind='text', content=content) + return dict(kind="text", content=content) def is_directory(fs: Dict[str, Any], fn: str) -> bool: if fn not in fs: return False - return fs[fn]['kind'] == 'dir' + return fs[fn]["kind"] == "dir" handler_class = VirtualFsHandler diff --git a/zulip_bots/zulip_bots/bots/weather/test_weather.py b/zulip_bots/zulip_bots/bots/weather/test_weather.py index fe1611c..f0e822e 100644 --- a/zulip_bots/zulip_bots/bots/weather/test_weather.py +++ b/zulip_bots/zulip_bots/bots/weather/test_weather.py @@ -7,7 +7,7 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests class TestWeatherBot(BotTestCase, DefaultTests): bot_name = "weather" - help_content = ''' + help_content = """ This bot returns weather info for specified city. You specify city in the following format: city, state/country @@ -15,10 +15,10 @@ class TestWeatherBot(BotTestCase, DefaultTests): For example: @**Weather Bot** Portland @**Weather Bot** Portland, Me - '''.strip() + """.strip() def _test(self, message: str, response: str, fixture: Optional[str] = None) -> None: - with self.mock_config_info({'key': '123456'}): + with self.mock_config_info({"key": "123456"}): if fixture: with self.mock_http_conversation(fixture): self.verify_reply(message, response) @@ -27,27 +27,27 @@ class TestWeatherBot(BotTestCase, DefaultTests): # Override default function in BotTestCase def test_bot_responds_to_empty_message(self) -> None: - with patch('requests.get'): - self._test('', self.help_content) + with patch("requests.get"): + self._test("", self.help_content) def test_bot(self) -> None: # City query bot_response = "Weather in New York, US:\n71.33 F / 21.85 C\nMist" - self._test('New York', bot_response, 'test_only_city') + self._test("New York", bot_response, "test_only_city") # City with country query bot_response = "Weather in New Delhi, IN:\n80.33 F / 26.85 C\nMist" - self._test('New Delhi, India', bot_response, 'test_city_with_country') + self._test("New Delhi, India", bot_response, "test_city_with_country") # Only country query: returns the weather of the capital city bot_response = "Weather in London, GB:\n58.73 F / 14.85 C\nShower Rain" - self._test('United Kingdom', bot_response, 'test_only_country') + self._test("United Kingdom", bot_response, "test_only_country") # City not found query bot_response = "Sorry, city not found" - self._test('fghjklasdfgh', bot_response, 'test_city_not_found') + self._test("fghjklasdfgh", bot_response, "test_city_not_found") # help message - with patch('requests.get'): - self._test('help', self.help_content) + with patch("requests.get"): + self._test("help", self.help_content) diff --git a/zulip_bots/zulip_bots/bots/weather/weather.py b/zulip_bots/zulip_bots/bots/weather/weather.py index 3551311..46540fa 100644 --- a/zulip_bots/zulip_bots/bots/weather/weather.py +++ b/zulip_bots/zulip_bots/bots/weather/weather.py @@ -5,32 +5,32 @@ import requests from zulip_bots.lib import BotHandler -api_url = 'http://api.openweathermap.org/data/2.5/weather' +api_url = "http://api.openweathermap.org/data/2.5/weather" class WeatherHandler: def initialize(self, bot_handler: BotHandler) -> None: - self.api_key = bot_handler.get_config_info('weather')['key'] - self.response_pattern = 'Weather in {}, {}:\n{:.2f} F / {:.2f} C\n{}' + self.api_key = bot_handler.get_config_info("weather")["key"] + self.response_pattern = "Weather in {}, {}:\n{:.2f} F / {:.2f} C\n{}" self.check_api_key(bot_handler) def check_api_key(self, bot_handler: BotHandler) -> None: - api_params = dict(q='nyc', APPID=self.api_key) + api_params = dict(q="nyc", APPID=self.api_key) test_response = requests.get(api_url, params=api_params) try: test_response_data = test_response.json() - if test_response_data['cod'] == 401: - bot_handler.quit('API Key not valid. Please see doc.md to find out how to get it.') + if test_response_data["cod"] == 401: + bot_handler.quit("API Key not valid. Please see doc.md to find out how to get it.") except KeyError: pass def usage(self) -> str: - return ''' + return """ This plugin will give info about weather in a specified city - ''' + """ def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: - help_content = ''' + help_content = """ This bot returns weather info for specified city. You specify city in the following format: city, state/country @@ -38,28 +38,28 @@ class WeatherHandler: For example: @**Weather Bot** Portland @**Weather Bot** Portland, Me - '''.strip() + """.strip() - if (message['content'] == 'help') or (message['content'] == ''): + if (message["content"] == "help") or (message["content"] == ""): response = help_content else: - api_params = dict(q=message['content'], APPID=self.api_key) + api_params = dict(q=message["content"], APPID=self.api_key) r = requests.get(api_url, params=api_params) - if r.json()['cod'] == "404": + if r.json()["cod"] == "404": response = "Sorry, city not found" else: - response = format_response(r, message['content'], self.response_pattern) + response = format_response(r, message["content"], self.response_pattern) bot_handler.send_reply(message, response) def format_response(text: Any, city: str, response_pattern: str) -> str: j = text.json() - city = j['name'] - country = j['sys']['country'] - fahrenheit = to_fahrenheit(j['main']['temp']) - celsius = to_celsius(j['main']['temp']) - description = j['weather'][0]['description'].title() + city = j["name"] + country = j["sys"]["country"] + fahrenheit = to_fahrenheit(j["main"]["temp"]) + celsius = to_celsius(j["main"]["temp"]) + description = j["weather"][0]["description"].title() return response_pattern.format(city, country, fahrenheit, celsius, description) diff --git a/zulip_bots/zulip_bots/bots/wikipedia/test_wikipedia.py b/zulip_bots/zulip_bots/bots/wikipedia/test_wikipedia.py index 3320788..307bc29 100755 --- a/zulip_bots/zulip_bots/bots/wikipedia/test_wikipedia.py +++ b/zulip_bots/zulip_bots/bots/wikipedia/test_wikipedia.py @@ -8,67 +8,67 @@ class TestWikipediaBot(BotTestCase, DefaultTests): def test_bot(self) -> None: # Single-word query - bot_request = 'happy' - bot_response = '''For search term:happy + bot_request = "happy" + bot_response = """For search term:happy 1:[Happiness](https://en.wikipedia.org/wiki/Happiness) 2:[Happy!](https://en.wikipedia.org/wiki/Happy!) 3:[Happy,_Happy](https://en.wikipedia.org/wiki/Happy,_Happy) -''' - with self.mock_http_conversation('test_single_word'): +""" + with self.mock_http_conversation("test_single_word"): self.verify_reply(bot_request, bot_response) # Multi-word query - bot_request = 'The sky is blue' - bot_response = '''For search term:The sky is blue + bot_request = "The sky is blue" + bot_response = """For search term:The sky is blue 1:[Sky_blue](https://en.wikipedia.org/wiki/Sky_blue) 2:[Sky_Blue_Sky](https://en.wikipedia.org/wiki/Sky_Blue_Sky) 3:[Blue_Sky](https://en.wikipedia.org/wiki/Blue_Sky) -''' - with self.mock_http_conversation('test_multi_word'): +""" + with self.mock_http_conversation("test_multi_word"): self.verify_reply(bot_request, bot_response) # Number query - bot_request = '123' - bot_response = '''For search term:123 + bot_request = "123" + bot_response = """For search term:123 1:[123](https://en.wikipedia.org/wiki/123) 2:[Japan_Airlines_Flight_123](https://en.wikipedia.org/wiki/Japan_Airlines_Flight_123) 3:[Iodine-123](https://en.wikipedia.org/wiki/Iodine-123) -''' - with self.mock_http_conversation('test_number_query'): +""" + with self.mock_http_conversation("test_number_query"): self.verify_reply(bot_request, bot_response) # Hash query - bot_request = '#' - bot_response = '''For search term:# + bot_request = "#" + bot_response = """For search term:# 1:[Number_sign](https://en.wikipedia.org/wiki/Number_sign) -''' - with self.mock_http_conversation('test_hash_query'): +""" + with self.mock_http_conversation("test_hash_query"): self.verify_reply(bot_request, bot_response) # Incorrect word - bot_request = 'sssssss kkkkk' + bot_request = "sssssss kkkkk" bot_response = ( "I am sorry. The search term you provided is not found :slightly_frowning_face:" ) - with self.mock_http_conversation('test_incorrect_query'): + with self.mock_http_conversation("test_incorrect_query"): self.verify_reply(bot_request, bot_response) # Empty query, no request made to the Internet. - bot_request = '' + bot_request = "" bot_response = "Please enter your search term after @**test-bot**" self.verify_reply(bot_request, bot_response) # Incorrect status code - bot_request = 'Zulip' + bot_request = "Zulip" bot_response = ( - 'Uh-Oh ! Sorry ,couldn\'t process the request right now.:slightly_frowning_face:\n' - 'Please try again later.' + "Uh-Oh ! Sorry ,couldn't process the request right now.:slightly_frowning_face:\n" + "Please try again later." ) - with self.mock_http_conversation('test_status_code'): + with self.mock_http_conversation("test_status_code"): self.verify_reply(bot_request, bot_response) # Request Exception - bot_request = 'Z' + bot_request = "Z" with mock_request_exception(): self.verify_reply(bot_request, bot_response) diff --git a/zulip_bots/zulip_bots/bots/wikipedia/wikipedia.py b/zulip_bots/zulip_bots/bots/wikipedia/wikipedia.py index f296165..ff707fb 100644 --- a/zulip_bots/zulip_bots/bots/wikipedia/wikipedia.py +++ b/zulip_bots/zulip_bots/bots/wikipedia/wikipedia.py @@ -9,7 +9,7 @@ from zulip_bots.lib import BotHandler class WikipediaHandler: - ''' + """ This plugin facilitates searching Wikipedia for a specific key term and returns the top 3 articles from the search. It looks for messages starting with '@mention-bot' @@ -18,75 +18,75 @@ class WikipediaHandler: the same stream that it was called from, but this code could be adapted to write Wikipedia searches to some kind of external issue tracker as well. - ''' + """ META = { - 'name': 'Wikipedia', - 'description': 'Searches Wikipedia for a term and returns the top 3 articles.', + "name": "Wikipedia", + "description": "Searches Wikipedia for a term and returns the top 3 articles.", } def usage(self) -> str: - return ''' + return """ This plugin will allow users to directly search Wikipedia for a specific key term and get the top 3 articles that is returned from the search. Users should preface searches with "@mention-bot". - @mention-bot ''' + @mention-bot """ def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: bot_response = self.get_bot_wiki_response(message, bot_handler) bot_handler.send_reply(message, bot_response) def get_bot_wiki_response(self, message: Dict[str, str], bot_handler: BotHandler) -> str: - '''This function returns the URLs of the requested topic.''' + """This function returns the URLs of the requested topic.""" - help_text = 'Please enter your search term after {}' + help_text = "Please enter your search term after {}" # Checking if the link exists. - query = message['content'] - if query == '': + query = message["content"] + if query == "": return help_text.format(bot_handler.identity().mention) - query_wiki_url = 'https://en.wikipedia.org/w/api.php' - query_wiki_params = dict(action='query', list='search', srsearch=query, format='json') + query_wiki_url = "https://en.wikipedia.org/w/api.php" + query_wiki_params = dict(action="query", list="search", srsearch=query, format="json") try: data = requests.get(query_wiki_url, params=query_wiki_params) except requests.exceptions.RequestException: - logging.error('broken link') + logging.error("broken link") return ( - 'Uh-Oh ! Sorry ,couldn\'t process the request right now.:slightly_frowning_face:\n' - 'Please try again later.' + "Uh-Oh ! Sorry ,couldn't process the request right now.:slightly_frowning_face:\n" + "Please try again later." ) # Checking if the bot accessed the link. if data.status_code != 200: - logging.error('Page not found.') + logging.error("Page not found.") return ( - 'Uh-Oh ! Sorry ,couldn\'t process the request right now.:slightly_frowning_face:\n' - 'Please try again later.' + "Uh-Oh ! Sorry ,couldn't process the request right now.:slightly_frowning_face:\n" + "Please try again later." ) - new_content = 'For search term:' + query + '\n' + new_content = "For search term:" + query + "\n" # Checking if there is content for the searched term - if len(data.json()['query']['search']) == 0: + if len(data.json()["query"]["search"]) == 0: new_content = ( - 'I am sorry. The search term you provided is not found :slightly_frowning_face:' + "I am sorry. The search term you provided is not found :slightly_frowning_face:" ) else: - for i in range(min(3, len(data.json()['query']['search']))): - search_string = data.json()['query']['search'][i]['title'].replace(' ', '_') - url = 'https://en.wikipedia.org/wiki/' + search_string + for i in range(min(3, len(data.json()["query"]["search"]))): + search_string = data.json()["query"]["search"][i]["title"].replace(" ", "_") + url = "https://en.wikipedia.org/wiki/" + search_string new_content += ( str(i + 1) - + ':' - + '[' + + ":" + + "[" + search_string - + ']' - + '(' + + "]" + + "(" + url.replace('"', "%22") - + ')\n' + + ")\n" ) return new_content diff --git a/zulip_bots/zulip_bots/bots/witai/test_witai.py b/zulip_bots/zulip_bots/bots/witai/test_witai.py index 85869ca..4d7c6a6 100644 --- a/zulip_bots/zulip_bots/bots/witai/test_witai.py +++ b/zulip_bots/zulip_bots/bots/witai/test_witai.py @@ -5,40 +5,40 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests, StubBotHandler, get_b class TestWitaiBot(BotTestCase, DefaultTests): - bot_name = 'witai' + bot_name = "witai" MOCK_CONFIG_INFO = { - 'token': '12345678', - 'handler_location': '/Users/abcd/efgh', - 'help_message': 'Qwertyuiop!', + "token": "12345678", + "handler_location": "/Users/abcd/efgh", + "help_message": "Qwertyuiop!", } MOCK_WITAI_RESPONSE = { - '_text': 'What is your favorite food?', - 'entities': {'intent': [{'confidence': 1.0, 'value': 'favorite_food'}]}, + "_text": "What is your favorite food?", + "entities": {"intent": [{"confidence": 1.0, "value": "favorite_food"}]}, } def test_normal(self) -> None: - with patch('zulip_bots.bots.witai.witai.get_handle', return_value=mock_handle): + with patch("zulip_bots.bots.witai.witai.get_handle", return_value=mock_handle): with self.mock_config_info(self.MOCK_CONFIG_INFO): get_bot_message_handler(self.bot_name).initialize(StubBotHandler()) - with patch('wit.Wit.message', return_value=self.MOCK_WITAI_RESPONSE): - self.verify_reply('What is your favorite food?', 'pizza') + with patch("wit.Wit.message", return_value=self.MOCK_WITAI_RESPONSE): + self.verify_reply("What is your favorite food?", "pizza") # This overrides the default one in `BotTestCase`. def test_bot_responds_to_empty_message(self) -> None: - with patch('zulip_bots.bots.witai.witai.get_handle', return_value=mock_handle): + with patch("zulip_bots.bots.witai.witai.get_handle", return_value=mock_handle): with self.mock_config_info(self.MOCK_CONFIG_INFO): get_bot_message_handler(self.bot_name).initialize(StubBotHandler()) - with patch('wit.Wit.message', return_value=self.MOCK_WITAI_RESPONSE): - self.verify_reply('', 'Qwertyuiop!') + with patch("wit.Wit.message", return_value=self.MOCK_WITAI_RESPONSE): + self.verify_reply("", "Qwertyuiop!") def mock_handle(res: Dict[str, Any]) -> Optional[str]: - if res['entities']['intent'][0]['value'] == 'favorite_food': - return 'pizza' - if res['entities']['intent'][0]['value'] == 'favorite_drink': - return 'coffee' + if res["entities"]["intent"][0]["value"] == "favorite_food": + return "pizza" + if res["entities"]["intent"][0]["value"] == "favorite_drink": + return "coffee" return None diff --git a/zulip_bots/zulip_bots/bots/witai/witai.py b/zulip_bots/zulip_bots/bots/witai/witai.py index 9a4da37..1adb950 100644 --- a/zulip_bots/zulip_bots/bots/witai/witai.py +++ b/zulip_bots/zulip_bots/bots/witai/witai.py @@ -11,51 +11,51 @@ from zulip_bots.lib import BotHandler class WitaiHandler: def usage(self) -> str: - return ''' + return """ Wit.ai bot uses pywit API to interact with Wit.ai. In order to use Wit.ai bot, `witai.conf` must be set up. See `doc.md` for more details. - ''' + """ def initialize(self, bot_handler: BotHandler) -> None: - config = bot_handler.get_config_info('witai') + config = bot_handler.get_config_info("witai") - token = config.get('token') + token = config.get("token") if not token: - raise KeyError('No `token` was specified') + raise KeyError("No `token` was specified") # `handler_location` should be the location of a module which contains # the function `handle`. See `doc.md` for more details. - handler_location = config.get('handler_location') + handler_location = config.get("handler_location") if not handler_location: - raise KeyError('No `handler_location` was specified') + raise KeyError("No `handler_location` was specified") handle = get_handle(handler_location) if handle is None: - raise Exception('Could not get handler from handler_location.') + raise Exception("Could not get handler from handler_location.") else: self.handle = handle - help_message = config.get('help_message') + help_message = config.get("help_message") if not help_message: - raise KeyError('No `help_message` was specified') + raise KeyError("No `help_message` was specified") self.help_message = help_message self.client = wit.Wit(token) def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: - if message['content'] == '' or message['content'] == 'help': + if message["content"] == "" or message["content"] == "help": bot_handler.send_reply(message, self.help_message) return try: - res = self.client.message(message['content']) + res = self.client.message(message["content"]) message_for_user = self.handle(res) if message_for_user: bot_handler.send_reply(message, message_for_user) except wit.wit.WitError: - bot_handler.send_reply(message, 'Sorry, I don\'t know how to respond to that!') + bot_handler.send_reply(message, "Sorry, I don't know how to respond to that!") except Exception as e: - bot_handler.send_reply(message, 'Sorry, there was an internal error.') + bot_handler.send_reply(message, "Sorry, there was an internal error.") print(e) return @@ -64,7 +64,7 @@ handler_class = WitaiHandler def get_handle(location: str) -> Optional[Callable[[Dict[str, Any]], Optional[str]]]: - '''Returns a function to be used when generating a response from Wit.ai + """Returns a function to be used when generating a response from Wit.ai bot. This function is the function named `handle` in the module at the given `location`. For an example of a `handle` function, see `doc.md`. @@ -77,9 +77,9 @@ def get_handle(location: str) -> Optional[Callable[[Dict[str, Any]], Optional[st Parameters: - location: The absolute path to the module to look for `handle` in. - ''' + """ try: - spec = importlib.util.spec_from_file_location('module.name', location) + spec = importlib.util.spec_from_file_location("module.name", location) handler = importlib.util.module_from_spec(spec) loader = spec.loader if not isinstance(loader, importlib.abc.Loader): diff --git a/zulip_bots/zulip_bots/bots/xkcd/test_xkcd.py b/zulip_bots/zulip_bots/bots/xkcd/test_xkcd.py index f3029f4..7a363b8 100755 --- a/zulip_bots/zulip_bots/bots/xkcd/test_xkcd.py +++ b/zulip_bots/zulip_bots/bots/xkcd/test_xkcd.py @@ -13,8 +13,8 @@ class TestXkcdBot(BotTestCase, DefaultTests): "payloads can only be launched within launch vehicles which do not launch " "themselves.](https://imgs.xkcd.com/comics/russells_teapot.png)" ) - with self.mock_http_conversation('test_latest'): - self.verify_reply('latest', bot_response) + with self.mock_http_conversation("test_latest"): + self.verify_reply("latest", bot_response) def test_random_command(self) -> None: bot_response = ( @@ -22,29 +22,29 @@ class TestXkcdBot(BotTestCase, DefaultTests): "[I've decided to score all my conversations using chess win-loss " "notation. (??)](https://imgs.xkcd.com/comics/chess_notation.png)" ) - with self.mock_http_conversation('test_random'): + with self.mock_http_conversation("test_random"): # Mock randint function. - with patch('zulip_bots.bots.xkcd.xkcd.random.randint') as randint: + with patch("zulip_bots.bots.xkcd.xkcd.random.randint") as randint: mock_rand_value = MagicMock() mock_rand_value.return_value = 1800 randint.return_value = mock_rand_value.return_value - self.verify_reply('random', bot_response) + self.verify_reply("random", bot_response) def test_numeric_comic_id_command_1(self) -> None: bot_response = ( "#1: **Barrel - Part 1**\n[Don't we all.]" "(https://imgs.xkcd.com/comics/barrel_cropped_(1).jpg)" ) - with self.mock_http_conversation('test_specific_id'): - self.verify_reply('1', bot_response) + with self.mock_http_conversation("test_specific_id"): + self.verify_reply("1", bot_response) - @patch('logging.exception') + @patch("logging.exception") def test_invalid_comic_ids(self, mock_logging_exception: MagicMock) -> None: invalid_id_txt = "Sorry, there is likely no xkcd comic strip with id: #" for comic_id, fixture in ( - ('0', 'test_not_existing_id_2'), - ('999999999', 'test_not_existing_id'), + ("0", "test_not_existing_id_2"), + ("999999999", "test_not_existing_id"), ): with self.mock_http_conversation(fixture): self.verify_reply(comic_id, invalid_id_txt + comic_id) @@ -52,14 +52,14 @@ class TestXkcdBot(BotTestCase, DefaultTests): def test_help_responses(self) -> None: help_txt = "xkcd bot supports these commands:" err_txt = "xkcd bot only supports these commands, not `{}`:" - commands = ''' + commands = """ * `{0} help` to show this help message. * `{0} latest` to fetch the latest comic strip from xkcd. * `{0} random` to fetch a random comic strip from xkcd. -* `{0} ` to fetch a comic strip based on `` e.g `{0} 1234`.'''.format( +* `{0} ` to fetch a comic strip based on `` e.g `{0} 1234`.""".format( "@**test-bot**" ) - self.verify_reply('', err_txt.format('') + commands) - self.verify_reply('help', help_txt + commands) + self.verify_reply("", err_txt.format("") + commands) + self.verify_reply("help", help_txt + commands) # Example invalid command - self.verify_reply('x', err_txt.format('x') + commands) + self.verify_reply("x", err_txt.format("x") + commands) diff --git a/zulip_bots/zulip_bots/bots/xkcd/xkcd.py b/zulip_bots/zulip_bots/bots/xkcd/xkcd.py index 4caab6a..a4c484d 100644 --- a/zulip_bots/zulip_bots/bots/xkcd/xkcd.py +++ b/zulip_bots/zulip_bots/bots/xkcd/xkcd.py @@ -6,25 +6,25 @@ import requests from zulip_bots.lib import BotHandler -XKCD_TEMPLATE_URL = 'https://xkcd.com/%s/info.0.json' -LATEST_XKCD_URL = 'https://xkcd.com/info.0.json' +XKCD_TEMPLATE_URL = "https://xkcd.com/%s/info.0.json" +LATEST_XKCD_URL = "https://xkcd.com/info.0.json" class XkcdHandler: - ''' + """ This plugin provides several commands that can be used for fetch a comic strip from https://xkcd.com. The bot looks for messages starting with "@mention-bot" and responds with a message with the comic based on provided commands. - ''' + """ META = { - 'name': 'XKCD', - 'description': 'Fetches comic strips from https://xkcd.com.', + "name": "XKCD", + "description": "Fetches comic strips from https://xkcd.com.", } def usage(self) -> str: - return ''' + return """ This plugin allows users to fetch a comic strip provided by https://xkcd.com. Users should preface the command with "@mention-bot". @@ -34,7 +34,7 @@ class XkcdHandler: - @mention-bot random -> To fetch a random comic strip from xkcd. - @mention-bot -> To fetch a comic strip based on ``, e.g `@mention-bot 1234`. - ''' + """ def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: quoted_name = bot_handler.identity().mention @@ -57,7 +57,7 @@ class XkcdServerError(Exception): def get_xkcd_bot_response(message: Dict[str, str], quoted_name: str) -> str: - original_content = message['content'].strip() + original_content = message["content"].strip() command = original_content.strip() commands_help = ( @@ -70,11 +70,11 @@ def get_xkcd_bot_response(message: Dict[str, str], quoted_name: str) -> str: ) try: - if command == 'help': - return commands_help % ('xkcd bot supports these commands:',) - elif command == 'latest': + if command == "help": + return commands_help % ("xkcd bot supports these commands:",) + elif command == "latest": fetched = fetch_xkcd_query(XkcdBotCommand.LATEST) - elif command == 'random': + elif command == "random": fetched = fetch_xkcd_query(XkcdBotCommand.RANDOM) elif command.isdigit(): fetched = fetch_xkcd_query(XkcdBotCommand.COMIC_ID, command) @@ -83,19 +83,19 @@ def get_xkcd_bot_response(message: Dict[str, str], quoted_name: str) -> str: "xkcd bot only supports these commands, not `%s`:" % (command,), ) except (requests.exceptions.ConnectionError, XkcdServerError): - logging.exception('Connection error occurred when trying to connect to xkcd server') - return 'Sorry, I cannot process your request right now, please try again later!' + logging.exception("Connection error occurred when trying to connect to xkcd server") + return "Sorry, I cannot process your request right now, please try again later!" except XkcdNotFoundError: logging.exception( - 'XKCD server responded 404 when trying to fetch comic with id %s' % (command,) + "XKCD server responded 404 when trying to fetch comic with id %s" % (command,) ) - return 'Sorry, there is likely no xkcd comic strip with id: #%s' % (command,) + return "Sorry, there is likely no xkcd comic strip with id: #%s" % (command,) else: return "#%s: **%s**\n[%s](%s)" % ( - fetched['num'], - fetched['title'], - fetched['alt'], - fetched['img'], + fetched["num"], + fetched["title"], + fetched["alt"], + fetched["img"], ) @@ -110,13 +110,13 @@ def fetch_xkcd_query(mode: int, comic_id: Optional[str] = None) -> Dict[str, str if latest.status_code != 200: raise XkcdServerError() - latest_id = latest.json()['num'] + latest_id = latest.json()["num"] random_id = random.randint(1, latest_id) url = XKCD_TEMPLATE_URL % (str(random_id),) elif mode == XkcdBotCommand.COMIC_ID: # Fetch specific comic strip by id number. if comic_id is None: - raise Exception('Missing comic_id argument') + raise Exception("Missing comic_id argument") url = XKCD_TEMPLATE_URL % (comic_id,) fetched = requests.get(url) diff --git a/zulip_bots/zulip_bots/bots/yoda/test_yoda.py b/zulip_bots/zulip_bots/bots/yoda/test_yoda.py index fcfa41c..b37696d 100644 --- a/zulip_bots/zulip_bots/bots/yoda/test_yoda.py +++ b/zulip_bots/zulip_bots/bots/yoda/test_yoda.py @@ -7,7 +7,7 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests class TestYodaBot(BotTestCase, DefaultTests): bot_name = "yoda" - help_text = ''' + help_text = """ This bot allows users to translate a sentence into 'Yoda speak'. Users should preface messages with '@mention-bot'. @@ -19,10 +19,10 @@ class TestYodaBot(BotTestCase, DefaultTests): directory. Example input: @mention-bot You will learn how to speak like me someday. - ''' + """ def _test(self, message: str, response: str, fixture: Optional[str] = None) -> None: - with self.mock_config_info({'api_key': '12345678'}): + with self.mock_config_info({"api_key": "12345678"}): if fixture is not None: with self.mock_http_conversation(fixture): self.verify_reply(message, response) @@ -31,50 +31,50 @@ class TestYodaBot(BotTestCase, DefaultTests): # Override default function in BotTestCase def test_bot_responds_to_empty_message(self) -> None: - self._test('', self.help_text) + self._test("", self.help_text) def test_bot(self) -> None: # Test normal sentence (1). self._test( - 'You will learn how to speak like me someday.', + "You will learn how to speak like me someday.", "Learn how to speak like me someday, you will. Yes, hmmm.", - 'test_1', + "test_1", ) # Test normal sentence (2). - self._test('you still have much to learn', "Much to learn, you still have.", 'test_2') + self._test("you still have much to learn", "Much to learn, you still have.", "test_2") # Test only numbers. - self._test('23456', "23456. Herh herh herh.", 'test_only_numbers') + self._test("23456", "23456. Herh herh herh.", "test_only_numbers") # Test help. - self._test('help', self.help_text) + self._test("help", self.help_text) # Test invalid input. self._test( - '@#$%^&*', + "@#$%^&*", "Invalid input, please check the sentence you have entered.", - 'test_invalid_input', + "test_invalid_input", ) # Test 403 response. self._test( - 'You will learn how to speak like me someday.', + "You will learn how to speak like me someday.", "Invalid Api Key. Did you follow the instructions in the `readme.md` file?", - 'test_api_key_error', + "test_api_key_error", ) # Test 503 response. with self.assertRaises(ServiceUnavailableError): self._test( - 'You will learn how to speak like me someday.', + "You will learn how to speak like me someday.", "The service is temporarily unavailable, please try again.", - 'test_service_unavailable_error', + "test_service_unavailable_error", ) # Test unknown response. self._test( - 'You will learn how to speak like me someday.', + "You will learn how to speak like me someday.", "Unknown Error.Error code: 123 Did you follow the instructions in the `readme.md` file?", - 'test_unknown_error', + "test_unknown_error", ) diff --git a/zulip_bots/zulip_bots/bots/yoda/yoda.py b/zulip_bots/zulip_bots/bots/yoda/yoda.py index cf9c1fc..4e1cfc5 100644 --- a/zulip_bots/zulip_bots/bots/yoda/yoda.py +++ b/zulip_bots/zulip_bots/bots/yoda/yoda.py @@ -7,7 +7,7 @@ import requests from zulip_bots.lib import BotHandler -HELP_MESSAGE = ''' +HELP_MESSAGE = """ This bot allows users to translate a sentence into 'Yoda speak'. Users should preface messages with '@mention-bot'. @@ -19,28 +19,28 @@ HELP_MESSAGE = ''' directory. Example input: @mention-bot You will learn how to speak like me someday. - ''' + """ class ApiKeyError(Exception): - '''raise this when there is an error with the Mashape Api Key''' + """raise this when there is an error with the Mashape Api Key""" class ServiceUnavailableError(Exception): - '''raise this when the service is unavailable.''' + """raise this when the service is unavailable.""" class YodaSpeakHandler: - ''' + """ This bot will allow users to translate a sentence into 'Yoda speak'. It looks for messages starting with '@mention-bot'. - ''' + """ def initialize(self, bot_handler: BotHandler) -> None: - self.api_key = bot_handler.get_config_info('yoda')['api_key'] + self.api_key = bot_handler.get_config_info("yoda")["api_key"] def usage(self) -> str: - return ''' + return """ This bot will allow users to translate a sentence into 'Yoda speak'. Users should preface messages with '@mention-bot'. @@ -51,7 +51,7 @@ class YodaSpeakHandler: The 'yoda.conf' file should be located in this bot's directory. Example input: @mention-bot You will learn how to speak like me someday. - ''' + """ def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: self.handle_input(message, bot_handler) @@ -65,20 +65,20 @@ class YodaSpeakHandler: ) if response.status_code == 200: - return response.json()['text'] + return response.json()["text"] if response.status_code == 403: raise ApiKeyError if response.status_code == 503: raise ServiceUnavailableError else: - error_message = response.json()['message'] + error_message = response.json()["message"] logging.error(error_message) error_code = response.status_code error_message = ( error_message - + 'Error code: ' + + "Error code: " + str(error_code) - + ' Did you follow the instructions in the `readme.md` file?' + + " Did you follow the instructions in the `readme.md` file?" ) return error_message @@ -86,11 +86,11 @@ class YodaSpeakHandler: # gets rid of whitespace around the edges, so that they aren't a problem in the future message_content = original_content.strip() # replaces all spaces with '+' to be in the format the api requires - sentence = message_content.replace(' ', '+') + sentence = message_content.replace(" ", "+") return sentence def handle_input(self, message: Dict[str, str], bot_handler: BotHandler) -> None: - original_content = message['content'] + original_content = message["content"] if self.is_help(original_content) or (original_content == ""): bot_handler.send_reply(message, HELP_MESSAGE) @@ -101,15 +101,15 @@ class YodaSpeakHandler: reply_message = self.send_to_yoda_api(sentence) if len(reply_message) == 0: - reply_message = 'Invalid input, please check the sentence you have entered.' + reply_message = "Invalid input, please check the sentence you have entered." except (ssl.SSLError, TypeError): - reply_message = 'The service is temporarily unavailable, please try again.' + reply_message = "The service is temporarily unavailable, please try again." logging.error(reply_message) except ApiKeyError: reply_message = ( - 'Invalid Api Key. Did you follow the instructions in the `readme.md` file?' + "Invalid Api Key. Did you follow the instructions in the `readme.md` file?" ) logging.error(reply_message) @@ -119,12 +119,12 @@ class YodaSpeakHandler: self, bot_handler: BotHandler, message: str, stream: str, subject: str ) -> None: # function for sending a message - bot_handler.send_message(dict(type='stream', to=stream, subject=subject, content=message)) + bot_handler.send_message(dict(type="stream", to=stream, subject=subject, content=message)) def is_help(self, original_content: str) -> bool: # gets rid of whitespace around the edges, so that they aren't a problem in the future message_content = original_content.strip() - if message_content == 'help': + if message_content == "help": return True else: return False diff --git a/zulip_bots/zulip_bots/bots/youtube/test_youtube.py b/zulip_bots/zulip_bots/bots/youtube/test_youtube.py index b5b997a..99655c2 100644 --- a/zulip_bots/zulip_bots/bots/youtube/test_youtube.py +++ b/zulip_bots/zulip_bots/bots/youtube/test_youtube.py @@ -9,9 +9,9 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests, StubBotHandler, get_b class TestYoutubeBot(BotTestCase, DefaultTests): bot_name = "youtube" normal_config = { - 'key': '12345678', - 'number_of_results': '5', - 'video_region': 'US', + "key": "12345678", + "number_of_results": "5", + "video_region": "US", } # type: Dict[str,str] help_content = ( @@ -27,26 +27,26 @@ class TestYoutubeBot(BotTestCase, DefaultTests): # Override default function in BotTestCase def test_bot_responds_to_empty_message(self) -> None: - with self.mock_config_info(self.normal_config), self.mock_http_conversation('test_keyok'): - self.verify_reply('', self.help_content) + with self.mock_config_info(self.normal_config), self.mock_http_conversation("test_keyok"): + self.verify_reply("", self.help_content) def test_single(self) -> None: bot_response = ( - 'Here is what I found for `funny cats` : \n' - 'Cats are so funny you will die laughing - ' - 'Funny cat compilation - [Watch now](https://www.youtube.com/watch?v=5dsGWM5XGdg)' + "Here is what I found for `funny cats` : \n" + "Cats are so funny you will die laughing - " + "Funny cat compilation - [Watch now](https://www.youtube.com/watch?v=5dsGWM5XGdg)" ) - with self.mock_config_info(self.normal_config), self.mock_http_conversation('test_single'): - self.verify_reply('funny cats', bot_response) + with self.mock_config_info(self.normal_config), self.mock_http_conversation("test_single"): + self.verify_reply("funny cats", bot_response) def test_invalid_key(self) -> None: bot = get_bot_message_handler(self.bot_name) bot_handler = StubBotHandler() with self.mock_config_info( - {'key': 'somethinginvalid', 'number_of_results': '5', 'video_region': 'US'} - ), self.mock_http_conversation('test_invalid_key'), self.assertRaises( + {"key": "somethinginvalid", "number_of_results": "5", "video_region": "US"} + ), self.mock_http_conversation("test_invalid_key"), self.assertRaises( bot_handler.BotQuitException ): bot.initialize(bot_handler) @@ -56,7 +56,7 @@ class TestYoutubeBot(BotTestCase, DefaultTests): bot_handler = StubBotHandler() with self.mock_config_info(self.normal_config), self.mock_http_conversation( - 'test_unknown_error' + "test_unknown_error" ), self.assertRaises(HTTPError): bot.initialize(bot_handler) @@ -65,45 +65,45 @@ class TestYoutubeBot(BotTestCase, DefaultTests): StubBotHandler() bot_response = ( - 'Here is what I found for `marvel` : ' - '\n * Marvel Studios\' Avengers: Infinity War Official Trailer - [Watch now](https://www.youtube.com/watch/6ZfuNTqbHE8)' - '\n * Marvel Studios\' Black Panther - Official Trailer - [Watch now](https://www.youtube.com/watch/xjDjIWPwcPU)' - '\n * MARVEL RISING BEGINS! | The Next Generation of Marvel Heroes (EXCLUSIVE) - [Watch now](https://www.youtube.com/watch/6HTPCTtkWoA)' - '\n * Marvel Contest of Champions Taskmaster Spotlight - [Watch now](https://www.youtube.com/watch/-8uqxdcJ9WM)' - '\n * 5* Crystal Opening! SO LUCKY! - Marvel Contest Of Champions - [Watch now](https://www.youtube.com/watch/l7rrsGKJ_O4)' + "Here is what I found for `marvel` : " + "\n * Marvel Studios' Avengers: Infinity War Official Trailer - [Watch now](https://www.youtube.com/watch/6ZfuNTqbHE8)" + "\n * Marvel Studios' Black Panther - Official Trailer - [Watch now](https://www.youtube.com/watch/xjDjIWPwcPU)" + "\n * MARVEL RISING BEGINS! | The Next Generation of Marvel Heroes (EXCLUSIVE) - [Watch now](https://www.youtube.com/watch/6HTPCTtkWoA)" + "\n * Marvel Contest of Champions Taskmaster Spotlight - [Watch now](https://www.youtube.com/watch/-8uqxdcJ9WM)" + "\n * 5* Crystal Opening! SO LUCKY! - Marvel Contest Of Champions - [Watch now](https://www.youtube.com/watch/l7rrsGKJ_O4)" ) with self.mock_config_info(self.normal_config), self.mock_http_conversation( - 'test_multiple' + "test_multiple" ): - self.verify_reply('list marvel', bot_response) + self.verify_reply("list marvel", bot_response) def test_noresult(self) -> None: bot_response = ( - 'Oops ! Sorry I couldn\'t find any video for `somethingrandomwithnoresult` ' - ':slightly_frowning_face:' + "Oops ! Sorry I couldn't find any video for `somethingrandomwithnoresult` " + ":slightly_frowning_face:" ) with self.mock_config_info(self.normal_config), self.mock_http_conversation( - 'test_noresult' + "test_noresult" ): self.verify_reply( - 'somethingrandomwithnoresult', + "somethingrandomwithnoresult", bot_response, ) def test_help(self) -> None: help_content = self.help_content - with self.mock_config_info(self.normal_config), self.mock_http_conversation('test_keyok'): - self.verify_reply('help', help_content) - self.verify_reply('list', help_content) - self.verify_reply('help list', help_content) - self.verify_reply('top', help_content) + with self.mock_config_info(self.normal_config), self.mock_http_conversation("test_keyok"): + self.verify_reply("help", help_content) + self.verify_reply("list", help_content) + self.verify_reply("help list", help_content) + self.verify_reply("top", help_content) def test_connection_error(self) -> None: with self.mock_config_info(self.normal_config), patch( - 'requests.get', side_effect=ConnectionError() - ), patch('logging.exception'): + "requests.get", side_effect=ConnectionError() + ), patch("logging.exception"): self.verify_reply( - 'Wow !', 'Uh-Oh, couldn\'t process the request ' 'right now.\nPlease again later' + "Wow !", "Uh-Oh, couldn't process the request " "right now.\nPlease again later" ) diff --git a/zulip_bots/zulip_bots/bots/youtube/youtube.py b/zulip_bots/zulip_bots/bots/youtube/youtube.py index afc2370..8bd368b 100644 --- a/zulip_bots/zulip_bots/bots/youtube/youtube.py +++ b/zulip_bots/zulip_bots/bots/youtube/youtube.py @@ -6,16 +6,16 @@ from requests.exceptions import ConnectionError, HTTPError from zulip_bots.lib import BotHandler -commands_list = ('list', 'top', 'help') +commands_list = ("list", "top", "help") class YoutubeHandler: def usage(self) -> str: - return ''' + return """ This plugin will allow users to search for a given search term on Youtube. Use '@mention-bot help' to get more information on the bot usage. - ''' + """ help_content = ( "*Help for YouTube bot* :robot_face: : \n\n" @@ -29,23 +29,23 @@ class YoutubeHandler: ) def initialize(self, bot_handler: BotHandler) -> None: - self.config_info = bot_handler.get_config_info('youtube') + self.config_info = bot_handler.get_config_info("youtube") # Check if API key is valid. If it is not valid, don't run the bot. try: - search_youtube('test', self.config_info['key'], self.config_info['video_region']) + search_youtube("test", self.config_info["key"], self.config_info["video_region"]) except HTTPError as e: - if e.response.json()['error']['errors'][0]['reason'] == 'keyInvalid': + if e.response.json()["error"]["errors"][0]["reason"] == "keyInvalid": bot_handler.quit( - 'Invalid key.' 'Follow the instructions in doc.md for setting API key.' + "Invalid key." "Follow the instructions in doc.md for setting API key." ) else: raise except ConnectionError: - logging.warning('Bad connection') + logging.warning("Bad connection") def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: - if message['content'] == '' or message['content'] == 'help': + if message["content"] == "" or message["content"] == "help": bot_handler.send_reply(message, self.help_content) else: cmd, query = get_command_query(message) @@ -58,79 +58,79 @@ def search_youtube(query: str, key: str, region: str, max_results: int = 1) -> L videos = [] params = { - 'part': 'id,snippet', - 'maxResults': max_results, - 'key': key, - 'q': query, - 'alt': 'json', - 'type': 'video', - 'regionCode': region, + "part": "id,snippet", + "maxResults": max_results, + "key": key, + "q": query, + "alt": "json", + "type": "video", + "regionCode": region, } # type: Dict[str, Union[str, int]] - url = 'https://www.googleapis.com/youtube/v3/search' + url = "https://www.googleapis.com/youtube/v3/search" try: r = requests.get(url, params=params) except ConnectionError: # Usually triggered by bad connection. - logging.exception('Bad connection') + logging.exception("Bad connection") raise r.raise_for_status() search_response = r.json() # Add each result to the appropriate list, and then display the lists of # matching videos, channels, and playlists. - for search_result in search_response.get('items', []): - if search_result['id']['kind'] == 'youtube#video': - videos.append([search_result['snippet']['title'], search_result['id']['videoId']]) + for search_result in search_response.get("items", []): + if search_result["id"]["kind"] == "youtube#video": + videos.append([search_result["snippet"]["title"], search_result["id"]["videoId"]]) return videos def get_command_query(message: Dict[str, str]) -> Tuple[Optional[str], str]: - blocks = message['content'].lower().split() + blocks = message["content"].lower().split() command = blocks[0] if command in commands_list: - query = message['content'][len(command) + 1 :].lstrip() + query = message["content"][len(command) + 1 :].lstrip() return command, query else: - return None, message['content'] + return None, message["content"] def get_bot_response( query: Optional[str], command: Optional[str], config_info: Dict[str, str] ) -> str: - key = config_info['key'] - max_results = int(config_info['number_of_results']) - region = config_info['video_region'] + key = config_info["key"] + max_results = int(config_info["number_of_results"]) + region = config_info["video_region"] video_list = [] # type: List[List[str]] try: - if query == '' or query is None: + if query == "" or query is None: return YoutubeHandler.help_content - if command is None or command == 'top': + if command is None or command == "top": video_list = search_youtube(query, key, region) - elif command == 'list': + elif command == "list": video_list = search_youtube(query, key, region, max_results) - elif command == 'help': + elif command == "help": return YoutubeHandler.help_content except (ConnectionError, HTTPError): - return 'Uh-Oh, couldn\'t process the request ' 'right now.\nPlease again later' + return "Uh-Oh, couldn't process the request " "right now.\nPlease again later" - reply = 'Here is what I found for `' + query + '` : ' + reply = "Here is what I found for `" + query + "` : " if len(video_list) == 0: return ( - 'Oops ! Sorry I couldn\'t find any video for `' + query + '` :slightly_frowning_face:' + "Oops ! Sorry I couldn't find any video for `" + query + "` :slightly_frowning_face:" ) elif len(video_list) == 1: return ( reply - + '\n%s - [Watch now](https://www.youtube.com/watch?v=%s)' + + "\n%s - [Watch now](https://www.youtube.com/watch?v=%s)" % (video_list[0][0], video_list[0][1]) ).strip() for title, id in video_list: - reply = reply + '\n * %s - [Watch now](https://www.youtube.com/watch/%s)' % (title, id) + reply = reply + "\n * %s - [Watch now](https://www.youtube.com/watch/%s)" % (title, id) # Using link https://www.youtube.com/watch/ to # prevent showing multiple previews return reply diff --git a/zulip_bots/zulip_bots/custom_exceptions.py b/zulip_bots/zulip_bots/custom_exceptions.py index 2b4fd7e..3af95c7 100644 --- a/zulip_bots/zulip_bots/custom_exceptions.py +++ b/zulip_bots/zulip_bots/custom_exceptions.py @@ -6,7 +6,7 @@ class ConfigValidationError(Exception): - ''' + """ Raise if the config data passed to a bot's validate_config() is invalid (e.g. wrong API key, invalid email, etc.). - ''' + """ diff --git a/zulip_bots/zulip_bots/finder.py b/zulip_bots/zulip_bots/finder.py index 5f15801..c9ee7b8 100644 --- a/zulip_bots/zulip_bots/finder.py +++ b/zulip_bots/zulip_bots/finder.py @@ -32,7 +32,7 @@ def resolve_bot_path(name: Text) -> Optional[Tuple[Path, Text]]: return (bot_path, bot_name) else: bot_name = name - bot_path = Path(current_dir, 'bots', bot_name, bot_name + '.py') + bot_path = Path(current_dir, "bots", bot_name, bot_name + ".py") if os.path.isfile(bot_path): return (bot_path, bot_name) diff --git a/zulip_bots/zulip_bots/game_handler.py b/zulip_bots/zulip_bots/game_handler.py index 265fa0d..b3ede21 100644 --- a/zulip_bots/zulip_bots/game_handler.py +++ b/zulip_bots/zulip_bots/game_handler.py @@ -25,12 +25,12 @@ class SamePlayerMove(Exception): class GameAdapter: - ''' + """ Class that serves as a template to easily create multiplayer games. This class handles all commands, and creates GameInstances which run the actual game logic. - ''' + """ def __init__( self, @@ -59,24 +59,24 @@ class GameAdapter: self.instances = {} # type: Dict[str, Any] self.user_cache = {} # type: Dict[str, Dict[str, Any]] self.pending_subject_changes = [] # type: List[str] - self.stream = 'games' + self.stream = "games" self.rules = rules # Values are [won, lost, drawn, total] new values can be added, but MUST be added to the end of the list. def add_user_statistics(self, user: str, values: Dict[str, int]) -> None: self.get_user_cache() current_values = {} # type: Dict[str, int] - if 'stats' in self.get_user_by_email(user).keys(): - current_values = self.user_cache[user]['stats'] + if "stats" in self.get_user_by_email(user).keys(): + current_values = self.user_cache[user]["stats"] for key, value in values.items(): if key not in current_values.keys(): current_values.update({key: 0}) current_values[key] += value - self.user_cache[user].update({'stats': current_values}) + self.user_cache[user].update({"stats": current_values}) self.put_user_cache() def help_message(self) -> str: - return '''** {} Bot Help:** + return """** {} Bot Help:** *Preface all commands with @**{}*** * To start a game in a stream (*recommended*), type `start game` @@ -96,7 +96,7 @@ class GameAdapter: `cancel game` * To see rules of this game, type `rules` -{}'''.format( +{}""".format( self.game_name, self.get_bot_username(), self.play_with_computer_help(), @@ -104,7 +104,7 @@ class GameAdapter: ) def help_message_single_player(self) -> str: - return '''** {} Bot Help:** + return """** {} Bot Help:** *Preface all commands with @**{}*** * To start a game in a stream, type `start game` @@ -112,20 +112,20 @@ class GameAdapter: `quit` * To see rules of this game, type `rules` -{}'''.format( +{}""".format( self.game_name, self.get_bot_username(), self.move_help_message ) def get_commands(self) -> Dict[str, str]: action = self.help_message_single_player() return { - 'accept': action, - 'decline': action, - 'register': action, - 'draw': action, - 'forfeit': action, - 'leaderboard': action, - 'join': action, + "accept": action, + "decline": action, + "register": action, + "draw": action, + "forfeit": action, + "leaderboard": action, + "join": action, } def manage_command(self, command: str, message: Dict[str, Any]) -> int: @@ -137,56 +137,56 @@ class GameAdapter: return 0 def already_in_game_message(self) -> str: - return 'You are already in a game. Type `quit` to leave.' + return "You are already in a game. Type `quit` to leave." def confirm_new_invitation(self, opponent: str) -> str: return ( - 'You\'ve sent an invitation to play ' + "You've sent an invitation to play " + self.game_name - + ' with @**' - + self.get_user_by_email(opponent)['full_name'] - + '**' + + " with @**" + + self.get_user_by_email(opponent)["full_name"] + + "**" ) def play_with_computer_help(self) -> str: if self.supports_computer: - return '\n* To start a game with the computer, type\n`start game with` @**{}**'.format( + return "\n* To start a game with the computer, type\n`start game with` @**{}**".format( self.get_bot_username() ) - return '' + return "" def alert_new_invitation(self, game_id: str) -> str: # Since the first player invites, the challenger is always the first player player_email = self.get_players(game_id)[0] sender_name = self.get_username_by_email(player_email) return ( - '**' + "**" + sender_name - + ' has invited you to play a game of ' + + " has invited you to play a game of " + self.game_name - + '.**\n' + + ".**\n" + self.get_formatted_game_object(game_id) - + '\n\n' - + 'Type ```accept``` to accept the game invitation\n' - + 'Type ```decline``` to decline the game invitation.' + + "\n\n" + + "Type ```accept``` to accept the game invitation\n" + + "Type ```decline``` to decline the game invitation." ) def confirm_invitation_accepted(self, game_id: str) -> str: - host = self.invites[game_id]['host'] - return 'Accepted invitation to play **{}** from @**{}**.'.format( + host = self.invites[game_id]["host"] + return "Accepted invitation to play **{}** from @**{}**.".format( self.game_name, self.get_username_by_email(host) ) def confirm_invitation_declined(self, game_id: str) -> str: - host = self.invites[game_id]['host'] - return 'Declined invitation to play **{}** from @**{}**.'.format( + host = self.invites[game_id]["host"] + return "Declined invitation to play **{}** from @**{}**.".format( self.game_name, self.get_username_by_email(host) ) - def send_message(self, to: str, content: str, is_private: bool, subject: str = '') -> None: + def send_message(self, to: str, content: str, is_private: bool, subject: str = "") -> None: self.bot_handler.send_message( dict( - type='private' if is_private else 'stream', to=to, content=content, subject=subject + type="private" if is_private else "stream", to=to, content=content, subject=subject ) ) @@ -195,15 +195,15 @@ class GameAdapter: def usage(self) -> str: return ( - ''' + """ Bot that allows users to play another user - or the computer in a game of ''' + or the computer in a game of """ + self.game_name - + ''' + + """ To see the entire list of commands, type @bot-name help - ''' + """ ) def initialize(self, bot_handler: BotHandler) -> None: @@ -215,13 +215,13 @@ class GameAdapter: def handle_message(self, message: Dict[str, Any], bot_handler: BotHandler) -> None: try: self.bot_handler = bot_handler - content = message['content'].strip() - sender = message['sender_email'].lower() - message['sender_email'] = message['sender_email'].lower() + content = message["content"].strip() + sender = message["sender_email"].lower() + message["sender_email"] = message["sender_email"].lower() if self.email not in self.user_cache.keys() and self.supports_computer: self.add_user_to_cache( - {'sender_email': self.email, 'sender_full_name': self.full_name} + {"sender_email": self.email, "sender_full_name": self.full_name} ) if sender == self.email: @@ -229,11 +229,11 @@ class GameAdapter: if sender not in self.user_cache.keys(): self.add_user_to_cache(message) - logging.info('Added {} to user cache'.format(sender)) + logging.info("Added {} to user cache".format(sender)) if self.is_single_player: - if content.lower().startswith('start game with') or content.lower().startswith( - 'play game' + if content.lower().startswith("start game with") or content.lower().startswith( + "play game" ): self.send_reply(message, self.help_message_single_player()) return @@ -242,56 +242,56 @@ class GameAdapter: if val == 0: return - if content.lower() == 'help' or content == '': + if content.lower() == "help" or content == "": if self.is_single_player: self.send_reply(message, self.help_message_single_player()) else: self.send_reply(message, self.help_message()) return - elif content.lower() == 'rules': + elif content.lower() == "rules": self.send_reply(message, self.rules) - elif content.lower().startswith('start game with '): + elif content.lower().startswith("start game with "): self.command_start_game_with(message, sender, content) - elif content.lower() == 'start game': + elif content.lower() == "start game": self.command_start_game(message, sender, content) - elif content.lower().startswith('play game'): + elif content.lower().startswith("play game"): self.command_play(message, sender, content) - elif content.lower() == 'accept': + elif content.lower() == "accept": self.command_accept(message, sender, content) - elif content.lower() == 'decline': + elif content.lower() == "decline": self.command_decline(message, sender, content) - elif content.lower() == 'quit': + elif content.lower() == "quit": self.command_quit(message, sender, content) - elif content.lower() == 'register': + elif content.lower() == "register": self.send_reply( message, - 'Hello @**{}**. Thanks for registering!'.format(message['sender_full_name']), + "Hello @**{}**. Thanks for registering!".format(message["sender_full_name"]), ) - elif content.lower() == 'leaderboard': + elif content.lower() == "leaderboard": self.command_leaderboard(message, sender, content) - elif content.lower() == 'join': + elif content.lower() == "join": self.command_join(message, sender, content) - elif self.is_user_in_game(sender) != '': + elif self.is_user_in_game(sender) != "": self.parse_message(message) elif ( self.move_regex.match(content) is not None - or content.lower() == 'draw' - or content.lower() == 'forfeit' + or content.lower() == "draw" + or content.lower() == "forfeit" ): self.send_reply( - message, 'You are not in a game at the moment. Type `help` for help.' + message, "You are not in a game at the moment. Type `help` for help." ) else: if self.is_single_player: @@ -300,30 +300,30 @@ class GameAdapter: self.send_reply(message, self.help_message()) except Exception as e: logging.exception(str(e)) - self.bot_handler.send_reply(message, 'Error {}.'.format(e)) + self.bot_handler.send_reply(message, "Error {}.".format(e)) def is_user_in_game(self, user_email: str) -> str: for instance in self.instances.values(): if user_email in instance.players: return instance.game_id - return '' + return "" def command_start_game_with(self, message: Dict[str, Any], sender: str, content: str) -> None: if not self.is_user_not_player(sender, message): self.send_reply(message, self.already_in_game_message()) return - users = content.replace('start game with ', '').strip().split(', ') + users = content.replace("start game with ", "").strip().split(", ") self.create_game_lobby(message, users) def command_start_game(self, message: Dict[str, Any], sender: str, content: str) -> None: - if message['type'] == 'private': + if message["type"] == "private": if self.is_single_player: - self.send_reply(message, 'You are not allowed to play games in private messages.') + self.send_reply(message, "You are not allowed to play games in private messages.") return else: self.send_reply( message, - 'If you are starting a game in private messages, you must invite players. Type `help` for commands.', + "If you are starting a game in private messages, you must invite players. Type `help` for commands.", ) if not self.is_user_not_player(sender, message): self.send_reply(message, self.already_in_game_message()) @@ -337,27 +337,27 @@ class GameAdapter: self.send_reply(message, self.already_in_game_message()) return game_id = self.set_invite_by_user(sender, True, message) - if game_id == '': - self.send_reply(message, 'No active invites. Type `help` for commands.') + if game_id == "": + self.send_reply(message, "No active invites. Type `help` for commands.") return - if message['type'] == 'private': + if message["type"] == "private": self.send_reply(message, self.confirm_invitation_accepted(game_id)) self.broadcast( game_id, - '@**{}** has accepted the invitation.'.format(self.get_username_by_email(sender)), + "@**{}** has accepted the invitation.".format(self.get_username_by_email(sender)), ) self.start_game_if_ready(game_id) def create_game_lobby(self, message: Dict[str, Any], users: List[str] = []) -> None: - if self.is_game_in_subject(message['subject'], message['display_recipient']): - self.send_reply(message, 'There is already a game in this stream.') + if self.is_game_in_subject(message["subject"], message["display_recipient"]): + self.send_reply(message, "There is already a game in this stream.") return if len(users) > 0: users = self.verify_users(users, message=message) if len(users) + 1 < self.min_players: self.send_reply( message, - 'You must have at least {} players to play.\nGame cancelled.'.format( + "You must have at least {} players to play.\nGame cancelled.".format( self.min_players ), ) @@ -365,23 +365,23 @@ class GameAdapter: if len(users) + 1 > self.max_players: self.send_reply( message, - 'The maximum number of players for this game is {}.'.format(self.max_players), + "The maximum number of players for this game is {}.".format(self.max_players), ) return game_id = self.generate_game_id() - stream_subject = '###private###' - if message['type'] == 'stream': - stream_subject = message['subject'] + stream_subject = "###private###" + if message["type"] == "stream": + stream_subject = message["subject"] self.invites[game_id] = { - 'host': message['sender_email'].lower(), - 'subject': stream_subject, - 'stream': message['display_recipient'], + "host": message["sender_email"].lower(), + "subject": stream_subject, + "stream": message["display_recipient"], } - if message['type'] == 'private': - self.invites[game_id]['stream'] = 'games' + if message["type"] == "private": + self.invites[game_id]["stream"] = "games" for user in users: self.send_invite(game_id, user, message) - if message['type'] == 'stream': + if message["type"] == "stream": if len(users) > 0: self.broadcast( game_id, @@ -393,8 +393,8 @@ class GameAdapter: if len(users) + 1 < self.max_players: self.broadcast( game_id, - '**{}** wants to play **{}**. Type @**{}** join to play them!'.format( - self.get_username_by_email(message['sender_email']), + "**{}** wants to play **{}**. Type @**{}** join to play them!".format( + self.get_username_by_email(message["sender_email"]), self.game_name, self.get_bot_username(), ), @@ -402,18 +402,18 @@ class GameAdapter: if self.is_single_player: self.broadcast( game_id, - '**{}** is now going to play {}!'.format( - self.get_username_by_email(message['sender_email']), self.game_name + "**{}** is now going to play {}!".format( + self.get_username_by_email(message["sender_email"]), self.game_name ), ) if self.email in users: - self.broadcast(game_id, 'Wait... That\'s me!', include_private=True) - if message['type'] == 'stream': + self.broadcast(game_id, "Wait... That's me!", include_private=True) + if message["type"] == "stream": self.broadcast( - game_id, '@**{}** accept'.format(self.get_bot_username()), include_private=False + game_id, "@**{}** accept".format(self.get_bot_username()), include_private=False ) - game_id = self.set_invite_by_user(self.email, True, {'type': 'stream'}) + game_id = self.set_invite_by_user(self.email, True, {"type": "stream"}) self.start_game_if_ready(game_id) def command_decline(self, message: Dict[str, Any], sender: str, content: str) -> None: @@ -421,49 +421,49 @@ class GameAdapter: self.send_reply(message, self.already_in_game_message()) return game_id = self.set_invite_by_user(sender, False, message) - if game_id == '': - self.send_reply(message, 'No active invites. Type `help` for commands.') + if game_id == "": + self.send_reply(message, "No active invites. Type `help` for commands.") return self.send_reply(message, self.confirm_invitation_declined(game_id)) self.broadcast( game_id, - '@**{}** has declined the invitation.'.format(self.get_username_by_email(sender)), + "@**{}** has declined the invitation.".format(self.get_username_by_email(sender)), ) - if len(self.get_players(game_id, parameter='')) < self.min_players: + if len(self.get_players(game_id, parameter="")) < self.min_players: self.cancel_game(game_id) def command_quit(self, message: Dict[str, Any], sender: str, content: str) -> None: game_id = self.get_game_id_by_email(sender) - if message['type'] == 'private' and self.is_single_player: - self.send_reply(message, 'You are not allowed to play games in private messages.') + if message["type"] == "private" and self.is_single_player: + self.send_reply(message, "You are not allowed to play games in private messages.") return - if game_id == '': - self.send_reply(message, 'You are not in a game. Type `help` for all commands.') + if game_id == "": + self.send_reply(message, "You are not in a game. Type `help` for all commands.") sender_name = self.get_username_by_email(sender) - self.cancel_game(game_id, reason='**{}** quit.'.format(sender_name)) + self.cancel_game(game_id, reason="**{}** quit.".format(sender_name)) def command_join(self, message: Dict[str, Any], sender: str, content: str) -> None: if not self.is_user_not_player(sender, message): self.send_reply(message, self.already_in_game_message()) return - if message['type'] == 'private': + if message["type"] == "private": self.send_reply( - message, 'You cannot join games in private messages. Type `help` for all commands.' + message, "You cannot join games in private messages. Type `help` for all commands." ) return - game_id = self.get_invite_in_subject(message['subject'], message['display_recipient']) - if game_id == '': + game_id = self.get_invite_in_subject(message["subject"], message["display_recipient"]) + if game_id == "": self.send_reply( - message, 'There is not a game in this subject. Type `help` for all commands.' + message, "There is not a game in this subject. Type `help` for all commands." ) return self.join_game(game_id, sender, message) def command_play(self, message: Dict[str, Any], sender: str, content: str) -> None: - game_id = self.get_invite_in_subject(message['subject'], message['display_recipient']) - if game_id == '': + game_id = self.get_invite_in_subject(message["subject"], message["display_recipient"]) + if game_id == "": self.send_reply( - message, 'There is not a game in this subject. Type `help` for all commands.' + message, "There is not a game in this subject. Type `help` for all commands." ) return num_players = len(self.get_players(game_id)) @@ -472,53 +472,53 @@ class GameAdapter: else: self.send_reply( message, - 'Join {} more players to start the game'.format(self.max_players - num_players), + "Join {} more players to start the game".format(self.max_players - num_players), ) def command_leaderboard(self, message: Dict[str, Any], sender: str, content: str) -> None: stats = self.get_sorted_player_statistics() num = 5 if len(stats) > 5 else len(stats) top_stats = stats[0:num] - response = '**Most wins**\n\n' - raw_headers = ['games_won', 'games_drawn', 'games_lost', 'total_games'] - headers = ['Player'] + [key.replace('_', ' ').title() for key in raw_headers] - response += ' | '.join(headers) - response += '\n' + ' | '.join([' --- ' for header in headers]) + response = "**Most wins**\n\n" + raw_headers = ["games_won", "games_drawn", "games_lost", "total_games"] + headers = ["Player"] + [key.replace("_", " ").title() for key in raw_headers] + response += " | ".join(headers) + response += "\n" + " | ".join([" --- " for header in headers]) for player, stat in top_stats: - response += '\n **{}** | '.format(self.get_username_by_email(player)) + response += "\n **{}** | ".format(self.get_username_by_email(player)) values = [str(stat[key]) for key in raw_headers] - response += ' | '.join(values) + response += " | ".join(values) self.send_reply(message, response) return def get_sorted_player_statistics(self) -> List[Tuple[str, Dict[str, int]]]: players = [] for user_name, u in self.user_cache.items(): - if 'stats' in u.keys(): - players.append((user_name, u['stats'])) + if "stats" in u.keys(): + players.append((user_name, u["stats"])) return sorted( players, key=lambda player: ( - player[1]['games_won'], - player[1]['games_drawn'], - player[1]['total_games'], + player[1]["games_won"], + player[1]["games_drawn"], + player[1]["total_games"], ), reverse=True, ) def send_invite(self, game_id: str, user_email: str, message: Dict[str, Any] = {}) -> None: - self.invites[game_id].update({user_email.lower(): 'p'}) + self.invites[game_id].update({user_email.lower(): "p"}) self.send_message(user_email, self.alert_new_invitation(game_id), True) if message != {}: self.send_reply(message, self.confirm_new_invitation(user_email)) - def cancel_game(self, game_id: str, reason: str = '') -> None: + def cancel_game(self, game_id: str, reason: str = "") -> None: if game_id in self.invites.keys(): - self.broadcast(game_id, 'Game cancelled.\n' + reason) + self.broadcast(game_id, "Game cancelled.\n" + reason) del self.invites[game_id] return if game_id in self.instances.keys(): - self.instances[game_id].broadcast('Game ended.\n' + reason) + self.instances[game_id].broadcast("Game ended.\n" + reason) del self.instances[game_id] return @@ -530,29 +530,29 @@ class GameAdapter: def start_game(self, game_id: str) -> None: players = self.get_players(game_id) subject = game_id - stream = self.invites[game_id]['stream'] - if self.invites[game_id]['subject'] != '###private###': - subject = self.invites[game_id]['subject'] + stream = self.invites[game_id]["stream"] + if self.invites[game_id]["subject"] != "###private###": + subject = self.invites[game_id]["subject"] self.instances[game_id] = GameInstance(self, False, subject, game_id, players, stream) self.broadcast( game_id, - 'The game has started in #{} {}'.format(stream, self.instances[game_id].subject) - + '\n' + "The game has started in #{} {}".format(stream, self.instances[game_id].subject) + + "\n" + self.get_formatted_game_object(game_id), ) del self.invites[game_id] self.instances[game_id].start() def get_formatted_game_object(self, game_id: str) -> str: - object = '''> **Game `{}`** + object = """> **Game `{}`** > {} -> {}/{} players'''.format( +> {}/{} players""".format( game_id, self.game_name, self.get_number_of_players(game_id), self.max_players ) if game_id in self.instances.keys(): instance = self.instances[game_id] if not self.is_single_player: - object += '\n> **[Join Game](/#narrow/stream/{}/topic/{})**'.format( + object += "\n> **[Join Game](/#narrow/stream/{}/topic/{})**".format( instance.stream, instance.subject ) return object @@ -560,28 +560,28 @@ class GameAdapter: def join_game(self, game_id: str, user_email: str, message: Dict[str, Any] = {}) -> None: if len(self.get_players(game_id)) >= self.max_players: if message != {}: - self.send_reply(message, 'This game is full.') + self.send_reply(message, "This game is full.") return - self.invites[game_id].update({user_email: 'a'}) + self.invites[game_id].update({user_email: "a"}) self.broadcast( - game_id, '@**{}** has joined the game'.format(self.get_username_by_email(user_email)) + game_id, "@**{}** has joined the game".format(self.get_username_by_email(user_email)) ) self.start_game_if_ready(game_id) - def get_players(self, game_id: str, parameter: str = 'a') -> List[str]: + def get_players(self, game_id: str, parameter: str = "a") -> List[str]: if game_id in self.invites.keys(): players = [] # type: List[str] if ( - self.invites[game_id]['subject'] == '###private###' and 'p' in parameter - ) or 'p' not in parameter: - players = [self.invites[game_id]['host']] + self.invites[game_id]["subject"] == "###private###" and "p" in parameter + ) or "p" not in parameter: + players = [self.invites[game_id]["host"]] for player, accepted in self.invites[game_id].items(): - if player == 'host' or player == 'subject' or player == 'stream': + if player == "host" or player == "subject" or player == "stream": continue if parameter in accepted: players.append(player) return players - if game_id in self.instances.keys() and 'p' not in parameter: + if game_id in self.instances.keys() and "p" not in parameter: players = self.instances[game_id].players return players return [] @@ -591,28 +591,28 @@ class GameAdapter: if game_id in self.instances.keys(): instance = self.instances[game_id] game_info = { - 'game_id': game_id, - 'type': 'instance', - 'stream': instance.stream, - 'subject': instance.subject, - 'players': self.get_players(game_id), + "game_id": game_id, + "type": "instance", + "stream": instance.stream, + "subject": instance.subject, + "players": self.get_players(game_id), } if game_id in self.invites.keys(): invite = self.invites[game_id] game_info = { - 'game_id': game_id, - 'type': 'invite', - 'stream': invite['stream'], - 'subject': invite['subject'], - 'players': self.get_players(game_id), + "game_id": game_id, + "type": "invite", + "stream": invite["stream"], + "subject": invite["subject"], + "players": self.get_players(game_id), } return game_info def get_user_by_name(self, name: str) -> Dict[str, Any]: name = name.strip() for user in self.user_cache.values(): - if 'full_name' in user.keys(): - if user['full_name'].lower() == name.lower(): + if "full_name" in user.keys(): + if user["full_name"].lower() == name.lower(): return user return {} @@ -621,26 +621,26 @@ class GameAdapter: return num def parse_message(self, message: Dict[str, Any]) -> None: - game_id = self.is_user_in_game(message['sender_email']) + game_id = self.is_user_in_game(message["sender_email"]) game = self.get_game_info(game_id) - if message['type'] == 'private': + if message["type"] == "private": if self.is_single_player: self.send_reply(message, self.help_message_single_player()) return self.send_reply( message, - 'Join your game using the link below!\n\n{}'.format( + "Join your game using the link below!\n\n{}".format( self.get_formatted_game_object(game_id) ), ) return - if game['subject'] != message['subject'] or game['stream'] != message['display_recipient']: + if game["subject"] != message["subject"] or game["stream"] != message["display_recipient"]: if game_id not in self.pending_subject_changes: self.send_reply( message, - 'Your current game is not in this subject. \n\ + "Your current game is not in this subject. \n\ To move subjects, send your message again, otherwise join the game using the link below.\n\n\ -{}'.format( +{}".format( self.get_formatted_game_object(game_id) ), ) @@ -648,23 +648,23 @@ To move subjects, send your message again, otherwise join the game using the lin return self.pending_subject_changes.remove(game_id) self.change_game_subject( - game_id, message['display_recipient'], message['subject'], message + game_id, message["display_recipient"], message["subject"], message ) - self.instances[game_id].handle_message(message['content'], message['sender_email']) + self.instances[game_id].handle_message(message["content"], message["sender_email"]) def change_game_subject( self, game_id: str, stream_name: str, subject_name: str, message: Dict[str, Any] = {} ) -> None: if self.get_game_instance_by_subject(stream_name, subject_name) is not None: if message != {}: - self.send_reply(message, 'There is already a game in this subject.') + self.send_reply(message, "There is already a game in this subject.") return if game_id in self.instances.keys(): self.instances[game_id].change_subject(stream_name, subject_name) if game_id in self.invites.keys(): invite = self.invites[game_id] - invite['stream'] = stream_name - invite['subject'] = stream_name + invite["stream"] = stream_name + invite["subject"] = stream_name def set_invite_by_user( self, user_email: str, is_accepted: bool, message: Dict[str, Any] @@ -673,32 +673,32 @@ To move subjects, send your message again, otherwise join the game using the lin for game, users in self.invites.items(): if user_email in users.keys(): if is_accepted: - if message['type'] == 'private': - users[user_email] = 'pa' + if message["type"] == "private": + users[user_email] = "pa" else: - users[user_email] = 'a' + users[user_email] = "a" else: users.pop(user_email) return game - return '' + return "" def add_user_to_cache(self, message: Dict[str, Any]) -> None: user = { - 'email': message['sender_email'].lower(), - 'full_name': message['sender_full_name'], - 'stats': {'total_games': 0, 'games_won': 0, 'games_lost': 0, 'games_drawn': 0}, + "email": message["sender_email"].lower(), + "full_name": message["sender_full_name"], + "stats": {"total_games": 0, "games_won": 0, "games_lost": 0, "games_drawn": 0}, } - self.user_cache.update({message['sender_email'].lower(): user}) + self.user_cache.update({message["sender_email"].lower(): user}) self.put_user_cache() def put_user_cache(self) -> Dict[str, Any]: user_cache_str = json.dumps(self.user_cache) - self.bot_handler.storage.put('users', user_cache_str) + self.bot_handler.storage.put("users", user_cache_str) return self.user_cache def get_user_cache(self) -> Dict[str, Any]: try: - user_cache_str = self.bot_handler.storage.get('users') + user_cache_str = self.bot_handler.storage.get("users") except KeyError: return {} self.user_cache = json.loads(user_cache_str) @@ -708,23 +708,23 @@ To move subjects, send your message again, otherwise join the game using the lin verified_users = [] failed = False for u in users: - user = u.strip().lstrip('@**').rstrip('**') + user = u.strip().lstrip("@**").rstrip("**") if ( user == self.get_bot_username() or user == self.email ) and not self.supports_computer: - self.send_reply(message, 'You cannot play against the computer in this game.') - if '@' not in user: + self.send_reply(message, "You cannot play against the computer in this game.") + if "@" not in user: user_obj = self.get_user_by_name(user) if user_obj == {}: self.send_reply( message, - 'I don\'t know {}. Tell them to say @**{}** register'.format( + "I don't know {}. Tell them to say @**{}** register".format( u, self.get_bot_username() ), ) failed = True continue - user = user_obj['email'] + user = user_obj["email"] if self.is_user_not_player(user, message): verified_users.append(user) else: @@ -742,13 +742,13 @@ To move subjects, send your message again, otherwise join the game using the lin def get_invite_in_subject(self, subject_name: str, stream_name: str) -> str: for key, invite in self.invites.items(): - if invite['subject'] == subject_name and invite['stream'] == stream_name: + if invite["subject"] == subject_name and invite["stream"] == stream_name: return key - return '' + return "" def is_game_in_subject(self, subject_name: str, stream_name: str) -> bool: return ( - self.get_invite_in_subject(subject_name, stream_name) != '' + self.get_invite_in_subject(subject_name, stream_name) != "" or self.get_game_instance_by_subject(subject_name, stream_name) is not None ) @@ -758,7 +758,7 @@ To move subjects, send your message again, otherwise join the game using the lin if message != {}: self.send_reply( message, - 'I don\'t know {}. Tell them to use @**{}** register'.format( + "I don't know {}. Tell them to use @**{}** register".format( user_email, self.get_bot_username() ), ) @@ -768,33 +768,33 @@ To move subjects, send your message again, otherwise join the game using the lin return False for invite in self.invites.values(): for u in invite.keys(): - if u == 'host': - if user_email == invite['host']: + if u == "host": + if user_email == invite["host"]: return False - if u == user_email and 'a' in invite[u]: + if u == user_email and "a" in invite[u]: return False return True def generate_game_id(self) -> str: - id = '' - valid_characters = 'abcdefghijklmnopqrstuvwxyz0123456789' + id = "" + valid_characters = "abcdefghijklmnopqrstuvwxyz0123456789" for i in range(6): id += valid_characters[random.randrange(0, len(valid_characters))] return id def broadcast(self, game_id: str, content: str, include_private: bool = True) -> bool: if include_private: - private_recipients = self.get_players(game_id, parameter='p') + private_recipients = self.get_players(game_id, parameter="p") if private_recipients is not None: for user in private_recipients: self.send_message(user, content, True) if game_id in self.invites.keys(): - if self.invites[game_id]['subject'] != '###private###': + if self.invites[game_id]["subject"] != "###private###": self.send_message( - self.invites[game_id]['stream'], + self.invites[game_id]["stream"], content, False, - self.invites[game_id]['subject'], + self.invites[game_id]["subject"], ) return True if game_id in self.instances.keys(): @@ -805,7 +805,7 @@ To move subjects, send your message again, otherwise join the game using the lin return False def get_username_by_email(self, user_email: str) -> str: - return self.get_user_by_email(user_email)['full_name'] + return self.get_user_by_email(user_email)["full_name"] def get_user_by_email(self, user_email: str) -> Dict[str, Any]: if user_email in self.user_cache: @@ -820,14 +820,14 @@ To move subjects, send your message again, otherwise join the game using the lin players = self.get_players(game_id) if user_email in players: return game_id - return '' + return "" def get_bot_username(self) -> str: return self.bot_handler.full_name class GameInstance: - ''' + """ The GameInstance class handles the game logic for a certain game, and is associated with a certain stream. @@ -835,7 +835,7 @@ class GameInstance: It only runs when the game is being played, not in the invite or waiting states. - ''' + """ def __init__( self, @@ -871,50 +871,50 @@ class GameInstance: self.broadcast_current_message() def get_player_text(self) -> str: - player_text = '' + player_text = "" for player in self.players: - player_text += ' @**{}**'.format(self.gameAdapter.get_username_by_email(player)) + player_text += " @**{}**".format(self.gameAdapter.get_username_by_email(player)) return player_text def get_start_message(self) -> str: - start_message = 'Game `{}` started.\n*Remember to start your message with* @**{}**'.format( + start_message = "Game `{}` started.\n*Remember to start your message with* @**{}**".format( self.game_id, self.gameAdapter.get_bot_username() ) if not self.is_private: - player_text = '\n**Players**' + player_text = "\n**Players**" player_text += self.get_player_text() start_message += player_text - start_message += '\n' + self.gameAdapter.gameMessageHandler.game_start_message() + start_message += "\n" + self.gameAdapter.gameMessageHandler.game_start_message() return start_message def handle_message(self, content: str, player_email: str) -> None: - if content == 'forfeit': + if content == "forfeit": player_name = self.gameAdapter.get_username_by_email(player_email) - self.broadcast('**{}** forfeited!'.format(player_name)) - self.end_game('except:' + player_email) + self.broadcast("**{}** forfeited!".format(player_name)) + self.end_game("except:" + player_email) return - if content == 'draw': + if content == "draw": if player_email in self.current_draw.keys(): self.current_draw[player_email] = True else: self.current_draw = {p: False for p in self.players} self.broadcast( - '**{}** has voted for a draw!\nType `draw` to accept'.format( + "**{}** has voted for a draw!\nType `draw` to accept".format( self.gameAdapter.get_username_by_email(player_email) ) ) self.current_draw[player_email] = True if self.check_draw(): - self.end_game('draw') + self.end_game("draw") return if self.is_turn_of(player_email): self.handle_current_player_command(content) else: if self.gameAdapter.is_single_player: - self.broadcast('It\'s your turn') + self.broadcast("It's your turn") else: self.broadcast( - 'It\'s **{}**\'s ({}) turn.'.format( + "It's **{}**'s ({}) turn.".format( self.gameAdapter.get_username_by_email(self.players[self.turn]), self.gameAdapter.gameMessageHandler.get_player_color(self.turn), ) @@ -950,7 +950,7 @@ class GameInstance: if not is_computer: self.current_messages.append( self.gameAdapter.gameMessageHandler.alert_move_message( - '**{}**'.format( + "**{}**".format( self.gameAdapter.get_username_by_email(self.players[self.turn]) ), content, @@ -960,7 +960,7 @@ class GameInstance: game_over = self.model.determine_game_over(self.players) if game_over: self.broadcast_current_message() - if game_over == 'current turn': + if game_over == "current turn": game_over = self.players[self.turn] self.end_game(game_over) return @@ -973,7 +973,7 @@ class GameInstance: if not is_computer: self.current_messages.append( self.gameAdapter.gameMessageHandler.alert_move_message( - '**{}**'.format( + "**{}**".format( self.gameAdapter.get_username_by_email(self.players[self.turn]) ), content, @@ -985,39 +985,39 @@ class GameInstance: game_over = self.model.determine_game_over(self.players) if game_over: self.broadcast_current_message() - if game_over == 'current turn': + if game_over == "current turn": game_over = self.players[self.turn] self.end_game(game_over) return self.current_messages.append( - 'It\'s **{}**\'s ({}) turn.'.format( + "It's **{}**'s ({}) turn.".format( self.gameAdapter.get_username_by_email(self.players[self.turn]), self.gameAdapter.gameMessageHandler.get_player_color(self.turn), ) ) self.broadcast_current_message() if self.players[self.turn] == self.gameAdapter.email: - self.make_move('', True) + self.make_move("", True) def next_turn(self) -> None: self.turn += 1 if self.turn >= len(self.players): self.turn = 0 if self.gameAdapter.is_single_player: - self.current_messages.append('It\'s your turn.') + self.current_messages.append("It's your turn.") else: self.current_messages.append( - 'It\'s **{}**\'s ({}) turn.'.format( + "It's **{}**'s ({}) turn.".format( self.gameAdapter.get_username_by_email(self.players[self.turn]), self.gameAdapter.gameMessageHandler.get_player_color(self.turn), ) ) self.broadcast_current_message() if self.players[self.turn] == self.gameAdapter.email: - self.make_move('', True) + self.make_move("", True) def broadcast_current_message(self) -> None: - content = '\n\n'.join(self.current_messages) + content = "\n\n".join(self.current_messages) self.broadcast(content) self.current_messages = [] @@ -1025,28 +1025,28 @@ class GameInstance: return self.gameAdapter.gameMessageHandler.parse_board(self.model.current_board) def end_game(self, winner: str) -> None: - loser = '' - if winner == 'draw': - self.broadcast('It was a draw!') - elif winner.startswith('except:'): - loser = winner.lstrip('except:') + loser = "" + if winner == "draw": + self.broadcast("It was a draw!") + elif winner.startswith("except:"): + loser = winner.lstrip("except:") else: winner_name = self.gameAdapter.get_username_by_email(winner) - self.broadcast('**{}** won! :tada:'.format(winner_name)) + self.broadcast("**{}** won! :tada:".format(winner_name)) for u in self.players: - values = {'total_games': 1, 'games_won': 0, 'games_lost': 0, 'games_drawn': 0} - if loser == '': + values = {"total_games": 1, "games_won": 0, "games_lost": 0, "games_drawn": 0} + if loser == "": if u == winner: - values.update({'games_won': 1}) - elif winner == 'draw': - values.update({'games_drawn': 1}) + values.update({"games_won": 1}) + elif winner == "draw": + values.update({"games_drawn": 1}) else: - values.update({'games_lost': 1}) + values.update({"games_lost": 1}) else: if u == loser: - values.update({'games_lost': 1}) + values.update({"games_lost": 1}) else: - values.update({'games_won': 1}) + values.update({"games_won": 1}) self.gameAdapter.add_user_statistics(u, values) if self.gameAdapter.email in self.players: self.send_win_responses(winner) @@ -1054,8 +1054,8 @@ class GameInstance: def send_win_responses(self, winner: str) -> None: if winner == self.gameAdapter.email: - self.broadcast('I won! Well Played!') - elif winner == 'draw': - self.broadcast('It was a draw! Well Played!') + self.broadcast("I won! Well Played!") + elif winner == "draw": + self.broadcast("It was a draw! Well Played!") else: - self.broadcast('You won! Nice!') + self.broadcast("You won! Nice!") diff --git a/zulip_bots/zulip_bots/lib.py b/zulip_bots/zulip_bots/lib.py index ffe8662..9346dfb 100644 --- a/zulip_bots/zulip_bots/lib.py +++ b/zulip_bots/zulip_bots/lib.py @@ -28,7 +28,7 @@ def exit_gracefully(signum: int, frame: Optional[Any]) -> None: def get_bots_directory_path() -> str: current_dir = os.path.dirname(os.path.abspath(__file__)) - return os.path.join(current_dir, 'bots') + return os.path.join(current_dir, "bots") def zulip_env_vars_are_present() -> bool: @@ -37,11 +37,11 @@ def zulip_env_vars_are_present() -> bool: # 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: + if os.environ.get("ZULIP_EMAIL") is None: return False - if os.environ.get('ZULIP_API_KEY') is None: + if os.environ.get("ZULIP_API_KEY") is None: return False - if os.environ.get('ZULIP_SITE') is None: + if os.environ.get("ZULIP_SITE") is None: return False # If none of the absolutely critical env vars are @@ -54,8 +54,8 @@ class RateLimit: self.message_limit = message_limit self.interval_limit = interval_limit self.message_list = [] # type: List[float] - self.error_message = '-----> !*!*!*MESSAGE RATE LIMIT REACHED, EXITING*!*!*! <-----\n' - 'Is your bot trapped in an infinite loop by reacting to its own messages?' + self.error_message = "-----> !*!*!*MESSAGE RATE LIMIT REACHED, EXITING*!*!*! <-----\n" + "Is your bot trapped in an infinite loop by reacting to its own messages?" def is_legal(self) -> bool: self.message_list.append(time.time()) @@ -75,7 +75,7 @@ class BotIdentity: def __init__(self, name: str, email: str) -> None: self.name = name self.email = email - self.mention = '@**' + name + '**' + self.mention = "@**" + name + "**" class BotStorage(Protocol): @@ -143,19 +143,19 @@ class StateHandler: def put(self, key: Text, value: Any) -> None: self.state_[key] = self.marshal(value) - response = self._client.update_storage({'storage': {key: self.state_[key]}}) - if response['result'] != 'success': + response = self._client.update_storage({"storage": {key: self.state_[key]}}) + if response["result"] != "success": raise StateHandlerError("Error updating state: {}".format(str(response))) def get(self, key: Text) -> Any: if key in self.state_: return self.demarshal(self.state_[key]) - response = self._client.get_storage({'keys': [key]}) - if response['result'] != 'success': - raise KeyError('key not found: ' + key) + response = self._client.get_storage({"keys": [key]}) + if response["result"] != "success": + raise KeyError("key not found: " + key) - marshalled_value = response['storage'][key] + marshalled_value = response["storage"][key] self.state_[key] = marshalled_value return self.demarshal(marshalled_value) @@ -223,23 +223,23 @@ class ExternalBotHandler: user_profile = client.get_profile() except ZulipError as e: print( - ''' + """ ERROR: {} Have you not started the server? Or did you mis-specify the URL? - '''.format( + """.format( e ) ) sys.exit(1) - if user_profile.get('result') == 'error': - msg = user_profile.get('msg', 'unknown') + if user_profile.get("result") == "error": + msg = user_profile.get("msg", "unknown") print( - ''' + """ ERROR: {} - '''.format( + """.format( msg ) ) @@ -253,13 +253,13 @@ class ExternalBotHandler: self._bot_config_parser = bot_config_parser self._storage = StateHandler(client) try: - self.user_id = user_profile['user_id'] - self.full_name = user_profile['full_name'] - self.email = user_profile['email'] + self.user_id = user_profile["user_id"] + self.full_name = user_profile["full_name"] + self.email = user_profile["email"] except KeyError: logging.error( - 'Cannot fetch user profile, make sure you have set' - ' up the zuliprc file correctly.' + "Cannot fetch user profile, make sure you have set" + " up the zuliprc file correctly." ) sys.exit(1) @@ -272,25 +272,25 @@ class ExternalBotHandler: def react(self, message: Dict[str, Any], emoji_name: str) -> Dict[str, Any]: return self._client.add_reaction( - dict(message_id=message['id'], emoji_name=emoji_name, reaction_type='unicode_emoji') + dict(message_id=message["id"], emoji_name=emoji_name, reaction_type="unicode_emoji") ) def send_message(self, message: Dict[str, Any]) -> Dict[str, Any]: if not self._rate_limit.is_legal(): self._rate_limit.show_error_and_exit() resp = self._client.send_message(message) - if resp.get('result') == 'error': + if resp.get("result") == "error": print("ERROR!: " + str(resp)) return resp def send_reply( self, message: Dict[str, Any], response: str, widget_content: Optional[str] = None ) -> Dict[str, Any]: - if message['type'] == 'private': + if message["type"] == "private": return self.send_message( dict( - type='private', - to=[x['id'] for x in message['display_recipient']], + type="private", + to=[x["id"] for x in message["display_recipient"]], content=response, widget_content=widget_content, ) @@ -298,9 +298,9 @@ class ExternalBotHandler: else: return self.send_message( dict( - type='stream', - to=message['display_recipient'], - subject=message['subject'], + type="stream", + to=message["display_recipient"], + subject=message["subject"], content=response, widget_content=widget_content, ) @@ -328,7 +328,7 @@ class ExternalBotHandler: if bot_name not in self.bot_config_file: print( - ''' + """ WARNING! {} does not adhere to the @@ -339,7 +339,7 @@ class ExternalBotHandler: The suggested name is {}.conf We will proceed anyway. - '''.format( + """.format( self.bot_config_file, bot_name ) ) @@ -360,7 +360,7 @@ class ExternalBotHandler: return dict(config_parser.items(bot_name)) def upload_file_from_path(self, file_path: str) -> Dict[str, Any]: - with open(file_path, 'rb') as file: + with open(file_path, "rb") as file: return self.upload_file(file) def upload_file(self, file: IO[Any]) -> Dict[str, Any]: @@ -375,7 +375,7 @@ class ExternalBotHandler: return open(abs_filepath) else: raise PermissionError( - "Cannot open file \"{}\". Bots may only access " + 'Cannot open file "{}". Bots may only access ' "files in their local directory.".format(abs_filepath) ) @@ -388,9 +388,9 @@ def extract_query_without_mention(message: Dict[str, Any], client: BotHandler) - If the bot is the first @mention in the message, then this function returns the stripped message with the bot's @mention removed. Otherwise, it returns None. """ - content = message['content'] - mention = '@**' + client.full_name + '**' - extended_mention_regex = re.compile(r'^@\*\*.*\|' + str(client.user_id) + r'\*\*') + content = message["content"] + mention = "@**" + client.full_name + "**" + extended_mention_regex = re.compile(r"^@\*\*.*\|" + str(client.user_id) + r"\*\*") extended_mention_match = extended_mention_regex.match(content) if extended_mention_match: @@ -412,27 +412,27 @@ def is_private_message_but_not_group_pm( zulip/zulip project, so refactor with care. See the comments in extract_query_without_mention. """ - if not message_dict['type'] == 'private': + if not message_dict["type"] == "private": return False - is_message_from_self = current_user.user_id == message_dict['sender_id'] + is_message_from_self = current_user.user_id == message_dict["sender_id"] recipients = [ - x['email'] for x in message_dict['display_recipient'] if current_user.email != x['email'] + x["email"] for x in message_dict["display_recipient"] if current_user.email != x["email"] ] return len(recipients) == 1 and not is_message_from_self def display_config_file_errors(error_msg: str, config_file: str) -> None: file_contents = open(config_file).read() - print('\nERROR: {} seems to be broken:\n\n{}'.format(config_file, file_contents)) - print('\nMore details here:\n\n{}\n'.format(error_msg)) + print("\nERROR: {} seems to be broken:\n\n{}".format(config_file, file_contents)) + print("\nMore details here:\n\n{}\n".format(error_msg)) def prepare_message_handler(bot: str, bot_handler: BotHandler, bot_lib_module: Any) -> Any: message_handler = bot_lib_module.handler_class() - if hasattr(message_handler, 'validate_config'): + if hasattr(message_handler, "validate_config"): config_data = bot_handler.get_config_info(bot) bot_lib_module.handler_class.validate_config(config_data) - if hasattr(message_handler, 'initialize'): + if hasattr(message_handler, "initialize"): message_handler.initialize(bot_handler=bot_handler) return message_handler @@ -453,10 +453,10 @@ def run_message_handler_for_bot( Set default bot_details, then override from class, if provided """ bot_details = { - 'name': bot_name.capitalize(), - 'description': "", + "name": bot_name.capitalize(), + "description": "", } - bot_details.update(getattr(lib_module.handler_class, 'META', {})) + bot_details.update(getattr(lib_module.handler_class, "META", {})) # Make sure you set up your ~/.zuliprc client_name = "Zulip{}Bot".format(bot_name.capitalize()) @@ -473,33 +473,33 @@ def run_message_handler_for_bot( message_handler = prepare_message_handler(bot_name, restricted_client, lib_module) if not quiet: - print("Running {} Bot:".format(bot_details['name'])) - if bot_details['description'] != "": - print("\n\t{}".format(bot_details['description'])) - if hasattr(message_handler, 'usage'): + print("Running {} Bot:".format(bot_details["name"])) + if bot_details["description"] != "": + print("\n\t{}".format(bot_details["description"])) + if hasattr(message_handler, "usage"): print(message_handler.usage()) else: print( - 'WARNING: {} is missing usage handler, please add one eventually'.format(bot_name) + "WARNING: {} is missing usage handler, please add one eventually".format(bot_name) ) def handle_message(message: Dict[str, Any], flags: List[str]) -> None: - logging.info('waiting for next message') + logging.info("waiting for next message") # `mentioned` will be in `flags` if the bot is mentioned at ANY position # (not necessarily the first @mention in the message). - is_mentioned = 'mentioned' in flags + is_mentioned = "mentioned" in flags is_private_message = is_private_message_but_not_group_pm(message, restricted_client) # Provide bots with a way to access the full, unstripped message - message['full_content'] = message['content'] + message["full_content"] = message["content"] # Strip at-mention botname from the message if is_mentioned: # message['content'] will be None when the bot's @-mention is not at the beginning. # In that case, the message shall not be handled. - message['content'] = extract_query_without_mention( + message["content"] = extract_query_without_mention( message=message, client=restricted_client ) - if message['content'] is None: + if message["content"] is None: return if is_private_message or is_mentioned: @@ -507,10 +507,10 @@ def run_message_handler_for_bot( signal.signal(signal.SIGINT, exit_gracefully) - logging.info('starting message handling...') + logging.info("starting message handling...") def event_callback(event: Dict[str, Any]) -> None: - if event['type'] == 'message': - handle_message(event['message'], event['flags']) + if event["type"] == "message": + handle_message(event["message"], event["flags"]) - client.call_on_each_event(event_callback, ['message']) + client.call_on_each_event(event_callback, ["message"]) diff --git a/zulip_bots/zulip_bots/provision.py b/zulip_bots/zulip_bots/provision.py index ff75937..2f873e9 100755 --- a/zulip_bots/zulip_bots/provision.py +++ b/zulip_bots/zulip_bots/provision.py @@ -12,27 +12,27 @@ from typing import Iterator def get_bot_paths() -> Iterator[str]: current_dir = os.path.dirname(os.path.abspath(__file__)) bots_dir = os.path.join(current_dir, "bots") - bots_subdirs = map(lambda d: os.path.abspath(d), glob.glob(bots_dir + '/*')) + bots_subdirs = map(lambda d: os.path.abspath(d), glob.glob(bots_dir + "/*")) paths = filter(lambda d: os.path.isdir(d), bots_subdirs) return paths def provision_bot(path_to_bot: str, force: bool) -> None: - req_path = os.path.join(path_to_bot, 'requirements.txt') + req_path = os.path.join(path_to_bot, "requirements.txt") if os.path.isfile(req_path): bot_name = os.path.basename(path_to_bot) - logging.info('Installing dependencies for {}...'.format(bot_name)) + logging.info("Installing dependencies for {}...".format(bot_name)) # pip install -r $BASEDIR/requirements.txt -t $BASEDIR/bot_dependencies --quiet - rcode = subprocess.call(['pip', 'install', '-r', req_path]) + rcode = subprocess.call(["pip", "install", "-r", req_path]) if rcode != 0: - logging.error('Error. Check output of `pip install` above for details.') + logging.error("Error. Check output of `pip install` above for details.") if not force: - logging.error('Use --force to try running anyway.') + logging.error("Use --force to try running anyway.") sys.exit(rcode) # Use pip's exit code else: - logging.info('Installed dependencies successfully.') + logging.info("Installed dependencies successfully.") def parse_args(available_bots: Iterator[str]) -> argparse.Namespace: @@ -51,22 +51,22 @@ Example: ./provision.py helloworld xkcd wikipedia parser = argparse.ArgumentParser(usage=usage) parser.add_argument( - 'bots_to_provision', - metavar='bots', - nargs='*', + "bots_to_provision", + metavar="bots", + nargs="*", default=available_bots, - help='specific bots to provision (default is all)', + help="specific bots to provision (default is all)", ) parser.add_argument( - '--force', + "--force", default=False, action="store_true", - help='Continue installation despite pip errors.', + help="Continue installation despite pip errors.", ) parser.add_argument( - '--quiet', '-q', action='store_true', default=False, help='Turn off logging output.' + "--quiet", "-q", action="store_true", default=False, help="Turn off logging output." ) return parser.parse_args() @@ -82,5 +82,5 @@ def main() -> None: provision_bot(bot, options.force) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/zulip_bots/zulip_bots/request_test_lib.py b/zulip_bots/zulip_bots/request_test_lib.py index 6d23a91..a66bc67 100644 --- a/zulip_bots/zulip_bots/request_test_lib.py +++ b/zulip_bots/zulip_bots/request_test_lib.py @@ -29,7 +29,7 @@ def mock_http_conversation(http_data: Dict[str, Any]) -> Any: mock_result._content = http_response.encode() # type: ignore # This modifies a "hidden" attribute. else: mock_result._content = json.dumps(http_response).encode() - mock_result.status_code = http_headers.get('status', 200) + mock_result.status_code = http_headers.get("status", 200) return mock_result def assert_called_with_fields( @@ -46,46 +46,46 @@ def mock_http_conversation(http_data: Dict[str, Any]) -> Any: if field in http_request: args[field] = http_request[field] - mock_result.assert_called_with(http_request['api_url'], **args) + mock_result.assert_called_with(http_request["api_url"], **args) try: - http_request = http_data['request'] - http_response = http_data['response'] - http_headers = http_data['response-headers'] + http_request = http_data["request"] + http_response = http_data["response"] + http_headers = http_data["response-headers"] except KeyError: print("ERROR: Failed to find 'request', 'response' or 'response-headers' fields in fixture") raise - meta = http_data.get('meta', dict()) - is_raw_response = meta.get('is_raw_response', False) + meta = http_data.get("meta", dict()) + is_raw_response = meta.get("is_raw_response", False) - http_method = http_request.get('method', 'GET') + http_method = http_request.get("method", "GET") - if http_method == 'GET': - with patch('requests.get') as mock_get: + if http_method == "GET": + with patch("requests.get") as mock_get: mock_get.return_value = get_response(http_response, http_headers, is_raw_response) yield - assert_called_with_fields(mock_get, http_request, ['params', 'headers'], meta) - elif http_method == 'PATCH': - with patch('requests.patch') as mock_patch: + assert_called_with_fields(mock_get, http_request, ["params", "headers"], meta) + elif http_method == "PATCH": + with patch("requests.patch") as mock_patch: mock_patch.return_value = get_response(http_response, http_headers, is_raw_response) yield assert_called_with_fields( - mock_patch, http_request, ['params', 'headers', 'json', 'data'], meta + mock_patch, http_request, ["params", "headers", "json", "data"], meta ) - elif http_method == 'PUT': - with patch('requests.put') as mock_post: + elif http_method == "PUT": + with patch("requests.put") as mock_post: mock_post.return_value = get_response(http_response, http_headers, is_raw_response) yield assert_called_with_fields( - mock_post, http_request, ['params', 'headers', 'json', 'data'], meta + mock_post, http_request, ["params", "headers", "json", "data"], meta ) else: - with patch('requests.post') as mock_post: + with patch("requests.post") as mock_post: mock_post.return_value = get_response(http_response, http_headers, is_raw_response) yield assert_called_with_fields( - mock_post, http_request, ['params', 'headers', 'json', 'data'], meta + mock_post, http_request, ["params", "headers", "json", "data"], meta ) @@ -94,7 +94,7 @@ def mock_request_exception() -> Any: def assert_mock_called(mock_result: Any) -> None: assert mock_result.called - with patch('requests.get') as mock_get: + with patch("requests.get") as mock_get: mock_get.return_value = True mock_get.side_effect = requests.exceptions.RequestException yield diff --git a/zulip_bots/zulip_bots/run.py b/zulip_bots/zulip_bots/run.py index 0e746a2..9b2a5a4 100755 --- a/zulip_bots/zulip_bots/run.py +++ b/zulip_bots/zulip_bots/run.py @@ -18,37 +18,37 @@ current_dir = os.path.dirname(os.path.abspath(__file__)) def parse_args() -> argparse.Namespace: - usage = ''' + usage = """ zulip-run-bot --config-file ~/zuliprc zulip-run-bot --help - ''' + """ parser = argparse.ArgumentParser(usage=usage) - parser.add_argument('bot', action='store', help='the name or path of an existing bot to run') + parser.add_argument("bot", action="store", help="the name or path of an existing bot to run") - parser.add_argument('--quiet', '-q', action='store_true', help='turn off logging output') + parser.add_argument("--quiet", "-q", action="store_true", help="turn off logging output") parser.add_argument( - '--config-file', - '-c', - action='store', - help='zulip configuration file (e.g. ~/Downloads/zuliprc)', + "--config-file", + "-c", + action="store", + help="zulip configuration file (e.g. ~/Downloads/zuliprc)", ) parser.add_argument( - '--bot-config-file', - '-b', - action='store', - help='third party configuration file (e.g. ~/giphy.conf', + "--bot-config-file", + "-b", + action="store", + help="third party configuration file (e.g. ~/giphy.conf", ) parser.add_argument( - '--force', - action='store_true', - help='try running the bot even if dependencies install fails', + "--force", + action="store_true", + help="try running the bot even if dependencies install fails", ) - parser.add_argument('--provision', action='store_true', help='install dependencies for the bot') + parser.add_argument("--provision", action="store_true", help="install dependencies for the bot") args = parser.parse_args() return args @@ -63,26 +63,26 @@ def exit_gracefully_if_zulip_config_is_missing(config_file: Optional[str]) -> No # but we'll catch those later. return else: - error_msg = 'ERROR: %s does not exist.' % (config_file,) + 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.' + error_msg = "ERROR: You did not supply a Zulip config file." if error_msg: - print('\n') + 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 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) @@ -96,11 +96,11 @@ def exit_gracefully_if_bot_config_file_does_not_exist(bot_config_file: Optional[ if not os.path.exists(bot_config_file): print( - ''' + """ ERROR: %s does not exist. You probably just specified the wrong file location here. - ''' + """ % (bot_config_file,) ) sys.exit(1) @@ -163,15 +163,15 @@ def main() -> None: ) except NoBotConfigException: print( - ''' + """ ERROR: Your bot requires you to specify a third party config file with the --bot-config-file option. Exiting now. - ''' + """ ) sys.exit(1) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/zulip_bots/zulip_bots/simple_lib.py b/zulip_bots/zulip_bots/simple_lib.py index 59b090f..8b908cb 100644 --- a/zulip_bots/zulip_bots/simple_lib.py +++ b/zulip_bots/zulip_bots/simple_lib.py @@ -28,20 +28,20 @@ class MockMessageServer: def send(self, message): self.message_id += 1 - message['id'] = self.message_id + message["id"] = self.message_id self.messages[self.message_id] = message return message def add_reaction(self, reaction_data): return dict( - result='success', msg='', uri='https://server/messages/{}/reactions'.format(uuid4()) + result="success", msg="", uri="https://server/messages/{}/reactions".format(uuid4()) ) def update(self, message): - self.messages[message['message_id']] = message + self.messages[message["message_id"]] = message def upload_file(self, file): - return dict(result='success', msg='', uri='https://server/user_uploads/{}'.format(uuid4())) + return dict(result="success", msg="", uri="https://server/user_uploads/{}".format(uuid4())) class TerminalBotHandler: @@ -63,29 +63,29 @@ class TerminalBotHandler: """ print("""The bot reacts to message #{}: {}""".format(message["id"], emoji_name)) return self.message_server.add_reaction( - dict(message_id=message['id'], emoji_name=emoji_name, reaction_type='unicode_emoji') + dict(message_id=message["id"], emoji_name=emoji_name, reaction_type="unicode_emoji") ) def send_message(self, message): """ Print the message sent in the terminal and store it in a mock message server. """ - if message['type'] == 'stream': + if message["type"] == "stream": print( - ''' + """ stream: {} topic: {} {} - '''.format( - message['to'], message['subject'], message['content'] + """.format( + message["to"], message["subject"], message["content"] ) ) else: print( - ''' + """ PM response: {} - '''.format( - message['content'] + """.format( + message["content"] ) ) # Note that message_server is only responsible for storing and assigning an @@ -111,11 +111,11 @@ class TerminalBotHandler: """ self.message_server.update(message) print( - ''' + """ update to message #{}: {} - '''.format( - message['message_id'], message['content'] + """.format( + message["message_id"], message["content"] ) ) @@ -131,7 +131,7 @@ class TerminalBotHandler: if optional: return dict() else: - print('Please supply --bot-config-file argument.') + print("Please supply --bot-config-file argument.") sys.exit(1) config = configparser.ConfigParser() diff --git a/zulip_bots/zulip_bots/terminal.py b/zulip_bots/zulip_bots/terminal.py index ec87777..9e327a0 100644 --- a/zulip_bots/zulip_bots/terminal.py +++ b/zulip_bots/zulip_bots/terminal.py @@ -10,22 +10,22 @@ current_dir = os.path.dirname(os.path.abspath(__file__)) def parse_args(): - description = ''' + description = """ This tool allows you to test a bot using the terminal (and no Zulip server). Examples: %(prog)s followup - ''' + """ parser = argparse.ArgumentParser( description=description, formatter_class=argparse.RawDescriptionHelpFormatter ) - parser.add_argument('bot', action='store', help='the name or path an existing bot to run') + parser.add_argument("bot", action="store", help="the name or path an existing bot to run") parser.add_argument( - '--bot-config-file', - '-b', - action='store', - help='optional third party config file (e.g. ~/giphy.conf)', + "--bot-config-file", + "-b", + action="store", + help="optional third party config file (e.g. ~/giphy.conf)", ) args = parser.parse_args() @@ -55,14 +55,14 @@ def main(): message_server = MockMessageServer() bot_handler = TerminalBotHandler(args.bot_config_file, message_server) - if hasattr(message_handler, 'initialize') and callable(message_handler.initialize): + if hasattr(message_handler, "initialize") and callable(message_handler.initialize): message_handler.initialize(bot_handler) - sender_email = 'foo_sender@zulip.com' + sender_email = "foo_sender@zulip.com" try: while True: - content = input('Enter your message: ') + content = input("Enter your message: ") message = message_server.send( dict( @@ -84,5 +84,5 @@ def main(): sys.exit(1) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/zulip_bots/zulip_bots/test_file_utils.py b/zulip_bots/zulip_bots/test_file_utils.py index 12d8b3a..f109377 100644 --- a/zulip_bots/zulip_bots/test_file_utils.py +++ b/zulip_bots/zulip_bots/test_file_utils.py @@ -3,7 +3,7 @@ import os from importlib import import_module from typing import Any, Dict -''' +""" This module helps us find files in the bots directory. Our directory structure is currently: @@ -11,7 +11,7 @@ directory structure is currently: / .py fixtures/ -''' +""" def get_bot_message_handler(bot_name: str) -> Any: @@ -19,16 +19,16 @@ def get_bot_message_handler(bot_name: str) -> Any: # handler class. Eventually, we want bot's handler classes to # inherit from a common prototype specifying the handle_message # function. - lib_module = import_module('zulip_bots.bots.{bot}.{bot}'.format(bot=bot_name)) # type: Any + lib_module = import_module("zulip_bots.bots.{bot}.{bot}".format(bot=bot_name)) # type: Any return lib_module.handler_class() def read_bot_fixture_data(bot_name: str, test_name: str) -> Dict[str, Any]: base_path = os.path.realpath( - os.path.join(os.path.dirname(os.path.abspath(__file__)), 'bots', bot_name, 'fixtures') + os.path.join(os.path.dirname(os.path.abspath(__file__)), "bots", bot_name, "fixtures") ) - http_data_path = os.path.join(base_path, '{}.json'.format(test_name)) - with open(http_data_path, encoding='utf-8') as f: + http_data_path = os.path.join(base_path, "{}.json".format(test_name)) + with open(http_data_path, encoding="utf-8") as f: content = f.read() http_data = json.loads(content) return http_data diff --git a/zulip_bots/zulip_bots/test_lib.py b/zulip_bots/zulip_bots/test_lib.py index b0985f2..c377ee0 100755 --- a/zulip_bots/zulip_bots/test_lib.py +++ b/zulip_bots/zulip_bots/test_lib.py @@ -11,8 +11,8 @@ from zulip_bots.test_file_utils import get_bot_message_handler, read_bot_fixture class StubBotHandler: def __init__(self) -> None: self.storage = SimpleStorage() - self.full_name = 'test-bot' - self.email = 'test-bot@example.com' + self.full_name = "test-bot" + self.email = "test-bot@example.com" self.user_id = 0 self.message_server = MockMessageServer() self.reset_transcript() @@ -24,14 +24,14 @@ class StubBotHandler: return BotIdentity(self.full_name, self.email) def send_message(self, message: Dict[str, Any]) -> Dict[str, Any]: - self.transcript.append(('send_message', message)) + self.transcript.append(("send_message", message)) return self.message_server.send(message) def send_reply( self, message: Dict[str, Any], response: str, widget_content: Optional[str] = None ) -> Dict[str, Any]: response_message = dict(content=response, widget_content=widget_content) - self.transcript.append(('send_reply', response_message)) + self.transcript.append(("send_reply", response_message)) return self.message_server.send(response_message) def react(self, message: Dict[str, Any], emoji_name: str) -> Dict[str, Any]: @@ -41,7 +41,7 @@ class StubBotHandler: self.message_server.update(message) def upload_file_from_path(self, file_path: str) -> Dict[str, Any]: - with open(file_path, 'rb') as file: + with open(file_path, "rb") as file: return self.message_server.upload_file(file) def upload_file(self, file: IO[Any]) -> Dict[str, Any]: @@ -57,7 +57,7 @@ class StubBotHandler: return {} def unique_reply(self) -> Dict[str, Any]: - responses = [message for (method, message) in self.transcript if method == 'send_reply'] + responses = [message for (method, message) in self.transcript if method == "send_reply"] self.ensure_unique_response(responses) return responses[0] @@ -68,13 +68,13 @@ class StubBotHandler: def ensure_unique_response(self, responses: List[Dict[str, Any]]) -> None: if not responses: - raise Exception('The bot is not responding for some reason.') + raise Exception("The bot is not responding for some reason.") if len(responses) > 1: - raise Exception('The bot is giving too many responses for some reason.') + raise Exception("The bot is giving too many responses for some reason.") class DefaultTests: - bot_name = '' + bot_name = "" def make_request_message(self, content: str) -> Dict[str, Any]: raise NotImplementedError() @@ -84,26 +84,26 @@ class DefaultTests: def test_bot_usage(self) -> None: bot = get_bot_message_handler(self.bot_name) - assert bot.usage() != '' + assert bot.usage() != "" def test_bot_responds_to_empty_message(self) -> None: - message = self.make_request_message('') + message = self.make_request_message("") # get_response will fail if we don't respond at all response = self.get_response(message) # we also want a non-blank response - assert len(response['content']) >= 1 + assert len(response["content"]) >= 1 class BotTestCase(unittest.TestCase): - bot_name = '' + bot_name = "" def _get_handlers(self) -> Tuple[Any, StubBotHandler]: bot = get_bot_message_handler(self.bot_name) bot_handler = StubBotHandler() - if hasattr(bot, 'initialize'): + if hasattr(bot, "initialize"): bot.initialize(bot_handler) return bot, bot_handler @@ -121,10 +121,10 @@ class BotTestCase(unittest.TestCase): mocking/subclassing. """ message = dict( - display_recipient='foo_stream', - sender_email='foo@example.com', - sender_full_name='Foo Test User', - sender_id='123', + display_recipient="foo_stream", + sender_email="foo@example.com", + sender_full_name="Foo Test User", + sender_id="123", content=content, ) return message @@ -139,7 +139,7 @@ class BotTestCase(unittest.TestCase): def verify_reply(self, request: str, response: str) -> None: reply = self.get_reply_dict(request) - self.assertEqual(response, reply['content']) + self.assertEqual(response, reply["content"]) def verify_dialog(self, conversation: List[Tuple[str, str]]) -> None: # Start a new message handler for the full conversation. @@ -150,7 +150,7 @@ class BotTestCase(unittest.TestCase): bot_handler.reset_transcript() bot.handle_message(message, bot_handler) response = bot_handler.unique_response() - self.assertEqual(expected_response, response['content']) + self.assertEqual(expected_response, response["content"]) def validate_invalid_config(self, config_data: Dict[str, str], error_regexp: str) -> None: bot_class = type(get_bot_message_handler(self.bot_name)) @@ -171,5 +171,5 @@ class BotTestCase(unittest.TestCase): def mock_config_info(self, config_info: Dict[str, str]) -> Any: return unittest.mock.patch( - 'zulip_bots.test_lib.StubBotHandler.get_config_info', return_value=config_info + "zulip_bots.test_lib.StubBotHandler.get_config_info", return_value=config_info ) diff --git a/zulip_bots/zulip_bots/tests/test_finder.py b/zulip_bots/zulip_bots/tests/test_finder.py index 8b82980..6d4551b 100644 --- a/zulip_bots/zulip_bots/tests/test_finder.py +++ b/zulip_bots/zulip_bots/tests/test_finder.py @@ -7,8 +7,8 @@ from zulip_bots import finder class FinderTestCase(TestCase): def test_resolve_bot_path(self) -> None: current_directory = Path(__file__).parents[1].as_posix() - expected_bot_path = Path(current_directory + '/bots/helloworld/helloworld.py') - expected_bot_name = 'helloworld' + expected_bot_path = Path(current_directory + "/bots/helloworld/helloworld.py") + expected_bot_name = "helloworld" expected_bot_path_and_name = (expected_bot_path, expected_bot_name) - actual_bot_path_and_name = finder.resolve_bot_path('helloworld') + actual_bot_path_and_name = finder.resolve_bot_path("helloworld") self.assertEqual(expected_bot_path_and_name, actual_bot_path_and_name) diff --git a/zulip_bots/zulip_bots/tests/test_lib.py b/zulip_bots/zulip_bots/tests/test_lib.py index 8819201..e60e77b 100644 --- a/zulip_bots/zulip_bots/tests/test_lib.py +++ b/zulip_bots/zulip_bots/tests/test_lib.py @@ -17,29 +17,29 @@ class FakeClient: def get_profile(self): return dict( - user_id='alice', - full_name='Alice', - email='alice@example.com', + user_id="alice", + full_name="Alice", + email="alice@example.com", id=42, ) def update_storage(self, payload): - new_data = payload['storage'] + new_data = payload["storage"] self.storage.update(new_data) return dict( - result='success', + result="success", ) def get_storage(self, request): return dict( - result='success', + result="success", storage=self.storage, ) def send_message(self, message): return dict( - result='success', + result="success", ) def upload_file(self, file): @@ -48,10 +48,10 @@ class FakeClient: class FakeBotHandler: def usage(self): - return ''' + return """ This is a fake bot handler that is used to spec BotHandler mocks. - ''' + """ def handle_message(self, message, bot_handler): pass @@ -72,13 +72,13 @@ class LibTest(TestCase): client = FakeClient() state_handler = StateHandler(client) - state_handler.put('key', [1, 2, 3]) - val = state_handler.get('key') + state_handler.put("key", [1, 2, 3]) + val = state_handler.get("key") self.assertEqual(val, [1, 2, 3]) # force us to get non-cached values state_handler = StateHandler(client) - val = state_handler.get('key') + val = state_handler.get("key") self.assertEqual(val, [1, 2, 3]) def test_state_handler_by_mock(self): @@ -87,25 +87,25 @@ class LibTest(TestCase): state_handler = StateHandler(client) client.get_storage.assert_not_called() - client.update_storage = MagicMock(return_value=dict(result='success')) - state_handler.put('key', [1, 2, 3]) - client.update_storage.assert_called_with(dict(storage=dict(key='[1, 2, 3]'))) + client.update_storage = MagicMock(return_value=dict(result="success")) + state_handler.put("key", [1, 2, 3]) + client.update_storage.assert_called_with(dict(storage=dict(key="[1, 2, 3]"))) - val = state_handler.get('key') + val = state_handler.get("key") client.get_storage.assert_not_called() self.assertEqual(val, [1, 2, 3]) # force us to get non-cached values client.get_storage = MagicMock( - return_value=dict(result='success', storage=dict(non_cached_key='[5]')) + return_value=dict(result="success", storage=dict(non_cached_key="[5]")) ) - val = state_handler.get('non_cached_key') - client.get_storage.assert_called_with({'keys': ['non_cached_key']}) + val = state_handler.get("non_cached_key") + client.get_storage.assert_called_with({"keys": ["non_cached_key"]}) self.assertEqual(val, [5]) # value must already be cached client.get_storage = MagicMock() - val = state_handler.get('non_cached_key') + val = state_handler.get("non_cached_key") client.get_storage.assert_not_called() self.assertEqual(val, [5]) @@ -114,12 +114,12 @@ class LibTest(TestCase): handler = ExternalBotHandler( client=client, root_dir=None, bot_details=None, bot_config_file=None ) - emoji_name = 'wave' - message = {'id': 10} + emoji_name = "wave" + message = {"id": 10} expected = { - 'message_id': message['id'], - 'emoji_name': 'wave', - 'reaction_type': 'unicode_emoji', + "message_id": message["id"], + "emoji_name": "wave", + "reaction_type": "unicode_emoji", } client.add_reaction = MagicMock() handler.react(message, emoji_name) @@ -131,22 +131,22 @@ class LibTest(TestCase): handler = ExternalBotHandler( client=client, root_dir=None, bot_details=None, bot_config_file=None ) - to = {'id': 43} + to = {"id": 43} expected = [ ( - {'type': 'private', 'display_recipient': [to]}, - {'type': 'private', 'to': [to['id']]}, + {"type": "private", "display_recipient": [to]}, + {"type": "private", "to": [to["id"]]}, None, ), ( - {'type': 'private', 'display_recipient': [to, profile]}, - {'type': 'private', 'to': [to['id'], profile['id']]}, - 'widget_content', + {"type": "private", "display_recipient": [to, profile]}, + {"type": "private", "to": [to["id"], profile["id"]]}, + "widget_content", ), ( - {'type': 'stream', 'display_recipient': 'Stream name', 'subject': 'Topic'}, - {'type': 'stream', 'to': 'Stream name', 'subject': 'Topic'}, - 'test widget', + {"type": "stream", "display_recipient": "Stream name", "subject": "Topic"}, + {"type": "stream", "to": "Stream name", "subject": "Topic"}, + "test widget", ), ] response_text = "Response" @@ -163,7 +163,7 @@ class LibTest(TestCase): ExternalBotHandler(client=client, root_dir=None, bot_details=None, bot_config_file=None) def test_run_message_handler_for_bot(self): - with patch('zulip_bots.lib.Client', new=FakeClient) as fake_client: + with patch("zulip_bots.lib.Client", new=FakeClient) as fake_client: mock_lib_module = MagicMock() # __file__ is not mocked by MagicMock(), so we assign a mock value manually. mock_lib_module.__file__ = "foo" @@ -172,18 +172,18 @@ class LibTest(TestCase): def call_on_each_event_mock(self, callback, event_types=None, narrow=None): def test_message(message, flags): - event = {'message': message, 'flags': flags, 'type': 'message'} + event = {"message": message, "flags": flags, "type": "message"} callback(event) # In the following test, expected_message is the dict that we expect # to be passed to the bot's handle_message function. - original_message = {'content': '@**Alice** bar', 'type': 'stream'} + original_message = {"content": "@**Alice** bar", "type": "stream"} expected_message = { - 'type': 'stream', - 'content': 'bar', - 'full_content': '@**Alice** bar', + "type": "stream", + "content": "bar", + "full_content": "@**Alice** bar", } - test_message(original_message, {'mentioned'}) + test_message(original_message, {"mentioned"}) mock_bot_handler.handle_message.assert_called_with( message=expected_message, bot_handler=ANY ) @@ -196,12 +196,12 @@ class LibTest(TestCase): quiet=True, config_file=None, bot_config_file=None, - bot_name='testbot', + bot_name="testbot", ) def test_upload_file(self): client, handler = self._create_client_and_handler_for_file_upload() - file = io.BytesIO(b'binary') + file = io.BytesIO(b"binary") handler.upload_file(file) @@ -209,10 +209,10 @@ class LibTest(TestCase): def test_upload_file_from_path(self): client, handler = self._create_client_and_handler_for_file_upload() - file = io.BytesIO(b'binary') + file = io.BytesIO(b"binary") - with patch('builtins.open', return_value=file): - handler.upload_file_from_path('file.txt') + with patch("builtins.open", return_value=file): + handler.upload_file_from_path("file.txt") client.upload_file.assert_called_once_with(file) @@ -221,13 +221,13 @@ class LibTest(TestCase): handler = ExternalBotHandler( client=client, root_dir=None, bot_details=None, bot_config_file=None ) - message = {'content': "@**Alice** Hello World"} + message = {"content": "@**Alice** Hello World"} self.assertEqual(extract_query_without_mention(message, handler), "Hello World") - message = {'content': "@**Alice|alice** Hello World"} + message = {"content": "@**Alice|alice** Hello World"} self.assertEqual(extract_query_without_mention(message, handler), "Hello World") - message = {'content': "@**Alice Renamed|alice** Hello World"} + message = {"content": "@**Alice Renamed|alice** Hello World"} self.assertEqual(extract_query_without_mention(message, handler), "Hello World") - message = {'content': "Not at start @**Alice|alice** Hello World"} + message = {"content": "Not at start @**Alice|alice** Hello World"} self.assertEqual(extract_query_without_mention(message, handler), None) def test_is_private_message_but_not_group_pm(self): @@ -236,16 +236,16 @@ class LibTest(TestCase): client=client, root_dir=None, bot_details=None, bot_config_file=None ) message = {} - message['display_recipient'] = 'some stream' - message['type'] = 'stream' + message["display_recipient"] = "some stream" + message["type"] = "stream" self.assertFalse(is_private_message_but_not_group_pm(message, handler)) - message['type'] = 'private' - message['display_recipient'] = [{'email': 'a1@b.com'}] - message['sender_id'] = handler.user_id + message["type"] = "private" + message["display_recipient"] = [{"email": "a1@b.com"}] + message["sender_id"] = handler.user_id self.assertFalse(is_private_message_but_not_group_pm(message, handler)) - message['sender_id'] = 0 # someone else + message["sender_id"] = 0 # someone else self.assertTrue(is_private_message_but_not_group_pm(message, handler)) - message['display_recipient'] = [{'email': 'a1@b.com'}, {'email': 'a2@b.com'}] + message["display_recipient"] = [{"email": "a1@b.com"}, {"email": "a2@b.com"}] self.assertFalse(is_private_message_but_not_group_pm(message, handler)) def _create_client_and_handler_for_file_upload(self): diff --git a/zulip_bots/zulip_bots/tests/test_run.py b/zulip_bots/zulip_bots/tests/test_run.py index 316c284..6b233d5 100644 --- a/zulip_bots/zulip_bots/tests/test_run.py +++ b/zulip_bots/zulip_bots/tests/test_run.py @@ -14,55 +14,55 @@ from zulip_bots.lib import extract_query_without_mention class TestDefaultArguments(TestCase): our_dir = os.path.dirname(__file__) - path_to_bot = os.path.abspath(os.path.join(our_dir, '../bots/giphy/giphy.py')) + path_to_bot = os.path.abspath(os.path.join(our_dir, "../bots/giphy/giphy.py")) - @patch('sys.argv', ['zulip-run-bot', 'giphy', '--config-file', '/foo/bar/baz.conf']) - @patch('zulip_bots.run.run_message_handler_for_bot') + @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_is_missing'): + 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', - config_file='/foo/bar/baz.conf', + bot_name="giphy", + config_file="/foo/bar/baz.conf", bot_config_file=None, lib_module=mock.ANY, quiet=False, ) - @patch('sys.argv', ['zulip-run-bot', path_to_bot, '--config-file', '/foo/bar/baz.conf']) - @patch('zulip_bots.run.run_message_handler_for_bot') + @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_is_missing'): + 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', - config_file='/foo/bar/baz.conf', + bot_name="giphy", + config_file="/foo/bar/baz.conf", bot_config_file=None, lib_module=mock.ANY, quiet=False, ) def test_adding_bot_parent_dir_to_sys_path_when_bot_name_specified(self) -> None: - bot_name = 'helloworld' # existing bot's name + bot_name = "helloworld" # existing bot's name expected_bot_dir_path = Path( - os.path.dirname(zulip_bots.run.__file__), 'bots', bot_name + os.path.dirname(zulip_bots.run.__file__), "bots", bot_name ).as_posix() self._test_adding_bot_parent_dir_to_sys_path( bot_qualifier=bot_name, bot_dir_path=expected_bot_dir_path ) - @patch('os.path.isfile', return_value=True) + @patch("os.path.isfile", return_value=True) def test_adding_bot_parent_dir_to_sys_path_when_bot_path_specified( self, mock_os_path_isfile: mock.Mock ) -> None: - bot_path = '/path/to/bot' - expected_bot_dir_path = Path('/path/to').as_posix() + bot_path = "/path/to/bot" + expected_bot_dir_path = Path("/path/to").as_posix() self._test_adding_bot_parent_dir_to_sys_path( bot_qualifier=bot_path, bot_dir_path=expected_bot_dir_path ) @@ -71,29 +71,29 @@ class TestDefaultArguments(TestCase): self, bot_qualifier: str, bot_dir_path: str ) -> None: with patch( - 'sys.argv', ['zulip-run-bot', bot_qualifier, '--config-file', '/path/to/config'] + "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_is_missing'): + 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_is_missing"): zulip_bots.run.main() sys_path = [Path(path).as_posix() for path in sys.path] self.assertIn(bot_dir_path, sys_path) - @patch('os.path.isfile', return_value=False) + @patch("os.path.isfile", return_value=False) def test_run_bot_by_module_name(self, mock_os_path_isfile: mock.Mock) -> None: - bot_module_name = 'bot.module.name' + bot_module_name = "bot.module.name" mock_bot_module = mock.Mock() mock_bot_module.__name__ = bot_module_name with patch( - 'sys.argv', ['zulip-run-bot', 'bot.module.name', '--config-file', '/path/to/config'] + "sys.argv", ["zulip-run-bot", "bot.module.name", "--config-file", "/path/to/config"] ): with patch( - 'importlib.import_module', return_value=mock_bot_module + "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_is_missing'): + with patch("zulip_bots.run.run_message_handler_for_bot"): + 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) @@ -103,7 +103,7 @@ class TestBotLib(TestCase): def test_message(name: str, message: str, expected_return: Optional[str]) -> None: mock_client = mock.MagicMock() mock_client.full_name = name - mock_message = {'content': message} + mock_message = {"content": message} self.assertEqual( expected_return, extract_query_without_mention(mock_message, mock_client) ) @@ -119,5 +119,5 @@ class TestBotLib(TestCase): test_message(r"Max (Mustermann)#(*$&12]\]", r"@**Max (Mustermann)#(*$&12]\]** foo", "foo") -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/zulip_botserver/setup.py b/zulip_botserver/setup.py index 63fe8a3..5764f76 100644 --- a/zulip_botserver/setup.py +++ b/zulip_botserver/setup.py @@ -10,45 +10,45 @@ with open("README.md") as fh: # We should be installable with either setuptools or distutils. package_info = dict( - name='zulip_botserver', + name="zulip_botserver", version=ZULIP_BOTSERVER_VERSION, - description='Zulip\'s Flask server for running bots', + description="Zulip's Flask server for running bots", long_description=long_description, long_description_content_type="text/markdown", - author='Zulip Open Source Project', - author_email='zulip-devel@googlegroups.com', + 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 :: Apache Software License', - 'Topic :: Communications :: Chat', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', + "Development Status :: 4 - Beta", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Topic :: Communications :: Chat", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", ], - python_requires='>=3.6', - url='https://www.zulip.org/', + python_requires=">=3.6", + url="https://www.zulip.org/", project_urls={ "Source": "https://github.com/zulip/python-zulip-api/", "Documentation": "https://zulip.com/api", }, entry_points={ - 'console_scripts': [ - 'zulip-botserver=zulip_botserver.server:main', + "console_scripts": [ + "zulip-botserver=zulip_botserver.server:main", ], }, - test_suite='tests', - package_data={'zulip_botserver': ['py.typed']}, + test_suite="tests", + package_data={"zulip_botserver": ["py.typed"]}, ) # type: Dict[str, Any] setuptools_info = dict( install_requires=[ - 'zulip', - 'zulip_bots', - 'flask>=0.12.2', + "zulip", + "zulip_bots", + "flask>=0.12.2", ], ) @@ -56,7 +56,7 @@ try: from setuptools import find_packages, setup package_info.update(setuptools_info) - package_info['packages'] = find_packages(exclude=['tests']) + package_info["packages"] = find_packages(exclude=["tests"]) except ImportError: from distutils.core import setup @@ -79,11 +79,11 @@ except ImportError: print("{name} is not installed.".format(name=module_name), file=sys.stderr) sys.exit(1) - check_dependency_manually('zulip') - check_dependency_manually('zulip_bots') - check_dependency_manually('flask', '0.12.2') + check_dependency_manually("zulip") + check_dependency_manually("zulip_bots") + check_dependency_manually("flask", "0.12.2") - package_info['packages'] = ['zulip_botserver'] + package_info["packages"] = ["zulip_botserver"] setup(**package_info) diff --git a/zulip_botserver/tests/server_test_lib.py b/zulip_botserver/tests/server_test_lib.py index 30769a9..1dce222 100644 --- a/zulip_botserver/tests/server_test_lib.py +++ b/zulip_botserver/tests/server_test_lib.py @@ -13,7 +13,7 @@ class BotServerTestCase(TestCase): server.app.testing = True self.app = server.app.test_client() - @mock.patch('zulip_bots.lib.ExternalBotHandler') + @mock.patch("zulip_bots.lib.ExternalBotHandler") def assert_bot_server_response( self, mock_ExternalBotHandler: mock.Mock, diff --git a/zulip_botserver/tests/test_server.py b/zulip_botserver/tests/test_server.py index a9b4f0b..3077e09 100644 --- a/zulip_botserver/tests/test_server.py +++ b/zulip_botserver/tests/test_server.py @@ -19,58 +19,58 @@ from .server_test_lib import BotServerTestCase class BotServerTests(BotServerTestCase): class MockMessageHandler: def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None: - assert message == {'key': "test message"} + assert message == {"key": "test message"} class MockLibModule: def handler_class(self) -> Any: return BotServerTests.MockMessageHandler() def test_successful_request(self) -> None: - available_bots = ['helloworld'] + available_bots = ["helloworld"] bots_config = { - 'helloworld': { - 'email': 'helloworld-bot@zulip.com', - 'key': '123456789qwertyuiop', - 'site': 'http://localhost', - 'token': 'abcd1234', + "helloworld": { + "email": "helloworld-bot@zulip.com", + "key": "123456789qwertyuiop", + "site": "http://localhost", + "token": "abcd1234", } } self.assert_bot_server_response( available_bots=available_bots, bots_config=bots_config, event=dict( - message={'content': "@**test** test message"}, - bot_email='helloworld-bot@zulip.com', - trigger='mention', - token='abcd1234', + message={"content": "@**test** test message"}, + bot_email="helloworld-bot@zulip.com", + trigger="mention", + token="abcd1234", ), expected_response="beep boop", check_success=True, ) def test_successful_request_from_two_bots(self) -> None: - available_bots = ['helloworld', 'help'] + available_bots = ["helloworld", "help"] bots_config = { - 'helloworld': { - 'email': 'helloworld-bot@zulip.com', - 'key': '123456789qwertyuiop', - 'site': 'http://localhost', - 'token': 'abcd1234', + "helloworld": { + "email": "helloworld-bot@zulip.com", + "key": "123456789qwertyuiop", + "site": "http://localhost", + "token": "abcd1234", }, - 'help': { - 'email': 'help-bot@zulip.com', - 'key': '123456789qwertyuiop', - 'site': 'http://localhost', - 'token': 'abcd1234', + "help": { + "email": "help-bot@zulip.com", + "key": "123456789qwertyuiop", + "site": "http://localhost", + "token": "abcd1234", }, } self.assert_bot_server_response( available_bots=available_bots, event=dict( - message={'content': "@**test** test message"}, - bot_email='helloworld-bot@zulip.com', - trigger='mention', - token='abcd1234', + message={"content": "@**test** test message"}, + bot_email="helloworld-bot@zulip.com", + trigger="mention", + token="abcd1234", ), expected_response="beep boop", bots_config=bots_config, @@ -79,54 +79,54 @@ class BotServerTests(BotServerTestCase): def test_request_for_unkown_bot(self) -> None: bots_config = { - 'helloworld': { - 'email': 'helloworld-bot@zulip.com', - 'key': '123456789qwertyuiop', - 'site': 'http://localhost', - 'token': 'abcd1234', + "helloworld": { + "email": "helloworld-bot@zulip.com", + "key": "123456789qwertyuiop", + "site": "http://localhost", + "token": "abcd1234", }, } self.assert_bot_server_response( - available_bots=['helloworld'], - event=dict(message={'content': "test message"}, bot_email='unknown-bot@zulip.com'), + available_bots=["helloworld"], + event=dict(message={"content": "test message"}, bot_email="unknown-bot@zulip.com"), bots_config=bots_config, check_success=False, ) def test_wrong_bot_token(self) -> None: - available_bots = ['helloworld'] + available_bots = ["helloworld"] bots_config = { - 'helloworld': { - 'email': 'helloworld-bot@zulip.com', - 'key': '123456789qwertyuiop', - 'site': 'http://localhost', - 'token': 'abcd1234', + "helloworld": { + "email": "helloworld-bot@zulip.com", + "key": "123456789qwertyuiop", + "site": "http://localhost", + "token": "abcd1234", } } self.assert_bot_server_response( available_bots=available_bots, bots_config=bots_config, event=dict( - message={'content': "@**test** test message"}, - bot_email='helloworld-bot@zulip.com', - trigger='mention', - token='wrongtoken', + message={"content": "@**test** test message"}, + bot_email="helloworld-bot@zulip.com", + trigger="mention", + token="wrongtoken", ), check_success=False, ) - @mock.patch('logging.error') - @mock.patch('zulip_bots.lib.StateHandler') + @mock.patch("logging.error") + @mock.patch("zulip_bots.lib.StateHandler") def test_wrong_bot_credentials( self, mock_StateHandler: mock.Mock, mock_LoggingError: mock.Mock ) -> None: - available_bots = ['nonexistent-bot'] + available_bots = ["nonexistent-bot"] bots_config = { - 'nonexistent-bot': { - 'email': 'helloworld-bot@zulip.com', - 'key': '123456789qwertyuiop', - 'site': 'http://localhost', - 'token': 'abcd1234', + "nonexistent-bot": { + "email": "helloworld-bot@zulip.com", + "key": "123456789qwertyuiop", + "site": "http://localhost", + "token": "abcd1234", } } # This works, but mypy still complains: @@ -135,26 +135,26 @@ class BotServerTests(BotServerTestCase): with self.assertRaisesRegexp( SystemExit, 'Error: Bot "nonexistent-bot" doesn\'t exist. Please make ' - 'sure you have set up the botserverrc file correctly.', + "sure you have set up the botserverrc file correctly.", ): self.assert_bot_server_response( available_bots=available_bots, event=dict( - message={'content': "@**test** test message"}, - bot_email='helloworld-bot@zulip.com', - trigger='mention', - token='abcd1234', + message={"content": "@**test** test message"}, + bot_email="helloworld-bot@zulip.com", + trigger="mention", + token="abcd1234", ), bots_config=bots_config, ) - @mock.patch('sys.argv', ['zulip-botserver', '--config-file', '/foo/bar/baz.conf']) + @mock.patch("sys.argv", ["zulip-botserver", "--config-file", "/foo/bar/baz.conf"]) def test_argument_parsing_defaults(self) -> None: opts = parse_args() - assert opts.config_file == '/foo/bar/baz.conf' + assert opts.config_file == "/foo/bar/baz.conf" assert opts.bot_name is None assert opts.bot_config_file is None - assert opts.hostname == '127.0.0.1' + assert opts.hostname == "127.0.0.1" assert opts.port == 5002 def test_read_config_from_env_vars(self) -> None: @@ -162,29 +162,29 @@ class BotServerTests(BotServerTestCase): # the stringified environment variable is standard even on # Python 3.7 and earlier. bots_config = OrderedDict() - bots_config['hello_world'] = { - 'email': 'helloworld-bot@zulip.com', - 'key': 'value', - 'site': 'http://localhost', - 'token': 'abcd1234', + bots_config["hello_world"] = { + "email": "helloworld-bot@zulip.com", + "key": "value", + "site": "http://localhost", + "token": "abcd1234", } - bots_config['giphy'] = { - 'email': 'giphy-bot@zulip.com', - 'key': 'value2', - 'site': 'http://localhost', - 'token': 'abcd1234', + bots_config["giphy"] = { + "email": "giphy-bot@zulip.com", + "key": "value2", + "site": "http://localhost", + "token": "abcd1234", } - os.environ['ZULIP_BOTSERVER_CONFIG'] = json.dumps(bots_config) + os.environ["ZULIP_BOTSERVER_CONFIG"] = json.dumps(bots_config) # No bot specified; should read all bot configs assert server.read_config_from_env_vars() == bots_config # Specified bot exists; should read only that section. - assert server.read_config_from_env_vars("giphy") == {'giphy': bots_config['giphy']} + assert server.read_config_from_env_vars("giphy") == {"giphy": bots_config["giphy"]} # Specified bot doesn't exist; should read the first section of the config. assert server.read_config_from_env_vars("redefined_bot") == { - 'redefined_bot': bots_config['hello_world'] + "redefined_bot": bots_config["hello_world"] } def test_read_config_file(self) -> None: @@ -195,17 +195,17 @@ class BotServerTests(BotServerTestCase): # No bot specified; should read all bot configs. bot_conf1 = server.read_config_file(os.path.join(current_dir, "test.conf")) expected_config1 = { - 'helloworld': { - 'email': 'helloworld-bot@zulip.com', - 'key': 'value', - 'site': 'http://localhost', - 'token': 'abcd1234', + "helloworld": { + "email": "helloworld-bot@zulip.com", + "key": "value", + "site": "http://localhost", + "token": "abcd1234", }, - 'giphy': { - 'email': 'giphy-bot@zulip.com', - 'key': 'value2', - 'site': 'http://localhost', - 'token': 'abcd1234', + "giphy": { + "email": "giphy-bot@zulip.com", + "key": "value2", + "site": "http://localhost", + "token": "abcd1234", }, } assert json.dumps(bot_conf1, sort_keys=True) == json.dumps(expected_config1, sort_keys=True) @@ -213,11 +213,11 @@ class BotServerTests(BotServerTestCase): # Specified bot exists; should read only that section. bot_conf3 = server.read_config_file(os.path.join(current_dir, "test.conf"), "giphy") expected_config3 = { - 'giphy': { - 'email': 'giphy-bot@zulip.com', - 'key': 'value2', - 'site': 'http://localhost', - 'token': 'abcd1234', + "giphy": { + "email": "giphy-bot@zulip.com", + "key": "value2", + "site": "http://localhost", + "token": "abcd1234", } } assert json.dumps(bot_conf3, sort_keys=True) == json.dumps(expected_config3, sort_keys=True) @@ -225,11 +225,11 @@ class BotServerTests(BotServerTestCase): # Specified bot doesn't exist; should read the first section of the config. bot_conf2 = server.read_config_file(os.path.join(current_dir, "test.conf"), "redefined_bot") expected_config2 = { - 'redefined_bot': { - 'email': 'helloworld-bot@zulip.com', - 'key': 'value', - 'site': 'http://localhost', - 'token': 'abcd1234', + "redefined_bot": { + "email": "helloworld-bot@zulip.com", + "key": "value", + "site": "http://localhost", + "token": "abcd1234", } } assert json.dumps(bot_conf2, sort_keys=True) == json.dumps(expected_config2, sort_keys=True) @@ -238,18 +238,18 @@ class BotServerTests(BotServerTestCase): # This testcase requires hardcoded paths, which here is a good thing so if we ever # restructure zulip_bots, this test would fail and we would also update Botserver # at the same time. - helloworld = import_module('zulip_bots.bots.{bot}.{bot}'.format(bot='helloworld')) + helloworld = import_module("zulip_bots.bots.{bot}.{bot}".format(bot="helloworld")) root_dir = Path(__file__).parents[2].as_posix() # load valid module name - module = server.load_lib_modules(['helloworld'])['helloworld'] + module = server.load_lib_modules(["helloworld"])["helloworld"] assert module == helloworld # load valid file path path = Path( - root_dir, 'zulip_bots/zulip_bots/bots/{bot}/{bot}.py'.format(bot='helloworld') + root_dir, "zulip_bots/zulip_bots/bots/{bot}/{bot}.py".format(bot="helloworld") ).as_posix() module = server.load_lib_modules([path])[path] - assert module.__name__ == 'custom_bot_module' + assert module.__name__ == "custom_bot_module" assert module.__file__ == path assert isinstance(module, ModuleType) @@ -257,23 +257,23 @@ class BotServerTests(BotServerTestCase): with self.assertRaisesRegexp( SystemExit, 'Error: Bot "botserver-test-case-random-bot" doesn\'t exist. ' - 'Please make sure you have set up the botserverrc file correctly.', + "Please make sure you have set up the botserverrc file correctly.", ): - module = server.load_lib_modules(['botserver-test-case-random-bot'])[ - 'botserver-test-case-random-bot' + module = server.load_lib_modules(["botserver-test-case-random-bot"])[ + "botserver-test-case-random-bot" ] # load invalid file path with self.assertRaisesRegexp( SystemExit, 'Error: Bot "{}/zulip_bots/zulip_bots/bots/helloworld.py" doesn\'t exist. ' - 'Please make sure you have set up the botserverrc file correctly.'.format(root_dir), + "Please make sure you have set up the botserverrc file correctly.".format(root_dir), ): path = Path( - root_dir, 'zulip_bots/zulip_bots/bots/{bot}.py'.format(bot='helloworld') + root_dir, "zulip_bots/zulip_bots/bots/{bot}.py".format(bot="helloworld") ).as_posix() module = server.load_lib_modules([path])[path] -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/zulip_botserver/zulip_botserver/input_parameters.py b/zulip_botserver/zulip_botserver/input_parameters.py index 04b94e1..68b9627 100644 --- a/zulip_botserver/zulip_botserver/input_parameters.py +++ b/zulip_botserver/zulip_botserver/input_parameters.py @@ -2,51 +2,51 @@ import argparse def parse_args() -> argparse.Namespace: - usage = ''' + usage = """ zulip-botserver --config-file [--hostname=
] [--port=] - ''' + """ parser = argparse.ArgumentParser(usage=usage) mutually_exclusive_args = parser.add_mutually_exclusive_group(required=True) # config-file or use-env-vars made mutually exclusive to prevent conflicts mutually_exclusive_args.add_argument( - '--config-file', - '-c', - action='store', - help='Config file for the Botserver. Use your `botserverrc` for multiple bots or' - '`zuliprc` for a single bot.', + "--config-file", + "-c", + action="store", + help="Config file for the Botserver. Use your `botserverrc` for multiple bots or" + "`zuliprc` for a single bot.", ) mutually_exclusive_args.add_argument( - '--use-env-vars', - '-e', - action='store_true', - help='Load configuration from JSON in ZULIP_BOTSERVER_CONFIG environment variable.', + "--use-env-vars", + "-e", + action="store_true", + help="Load configuration from JSON in ZULIP_BOTSERVER_CONFIG environment variable.", ) parser.add_argument( - '--bot-config-file', - action='store', + "--bot-config-file", + action="store", default=None, - help='Config file for bots. Only needed when one of ' - 'the bots you want to run requires a config file.', + help="Config file for bots. Only needed when one of " + "the bots you want to run requires a config file.", ) parser.add_argument( - '--bot-name', - '-b', - action='store', - help='Run a single bot BOT_NAME. Use this option to run the Botserver ' - 'with a `zuliprc` config file.', + "--bot-name", + "-b", + action="store", + help="Run a single bot BOT_NAME. Use this option to run the Botserver " + "with a `zuliprc` config file.", ) parser.add_argument( - '--hostname', - action='store', + "--hostname", + action="store", default="127.0.0.1", - help='Address on which you want to run the Botserver. (default: %(default)s)', + help="Address on which you want to run the Botserver. (default: %(default)s)", ) parser.add_argument( - '--port', - action='store', + "--port", + action="store", default=5002, type=int, - help='Port on which you want to run the Botserver. (default: %(default)d)', + help="Port on which you want to run the Botserver. (default: %(default)d)", ) return parser.parse_args() diff --git a/zulip_botserver/zulip_botserver/server.py b/zulip_botserver/zulip_botserver/server.py index a8a8884..59fe9cd 100644 --- a/zulip_botserver/zulip_botserver/server.py +++ b/zulip_botserver/zulip_botserver/server.py @@ -23,17 +23,17 @@ from zulip_botserver.input_parameters import parse_args def read_config_section(parser: configparser.ConfigParser, section: str) -> Dict[str, str]: section_info = { - "email": parser.get(section, 'email'), - "key": parser.get(section, 'key'), - "site": parser.get(section, 'site'), - "token": parser.get(section, 'token'), + "email": parser.get(section, "email"), + "key": parser.get(section, "key"), + "site": parser.get(section, "site"), + "token": parser.get(section, "token"), } return section_info def read_config_from_env_vars(bot_name: Optional[str] = None) -> Dict[str, Dict[str, str]]: bots_config = {} # type: Dict[str, Dict[str, str]] - json_config = os.environ.get('ZULIP_BOTSERVER_CONFIG') + json_config = os.environ.get("ZULIP_BOTSERVER_CONFIG") if json_config is None: raise OSError( @@ -126,15 +126,15 @@ def load_lib_modules(available_bots: List[str]) -> Dict[str, Any]: bots_lib_module = {} for bot in available_bots: try: - if bot.endswith('.py') and os.path.isfile(bot): + if bot.endswith(".py") and os.path.isfile(bot): lib_module = load_module_from_file(bot) else: - module_name = 'zulip_bots.bots.{bot}.{bot}'.format(bot=bot) + module_name = "zulip_bots.bots.{bot}.{bot}".format(bot=bot) lib_module = import_module(module_name) bots_lib_module[bot] = lib_module except ImportError: error_message = ( - "Error: Bot \"{}\" doesn't exist. Please make sure " + 'Error: Bot "{}" doesn\'t exist. Please make sure ' "you have set up the botserverrc file correctly.\n".format(bot) ) if bot == "api": @@ -157,7 +157,7 @@ def load_bot_handlers( api_key=bots_config[bot]["key"], site=bots_config[bot]["site"], ) - bot_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'bots', bot) + bot_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bots", bot) bot_handler = lib.ExternalBotHandler( client, bot_dir, bot_details={}, bot_config_parser=third_party_bot_conf ) @@ -184,12 +184,12 @@ app = Flask(__name__) bots_config = {} # type: Dict[str, Dict[str, str]] -@app.route('/', methods=['POST']) +@app.route("/", methods=["POST"]) def handle_bot() -> str: event = request.get_json(force=True) assert event is not None for bot_name, config in bots_config.items(): - if config['email'] == event['bot_email']: + if config["email"] == event["bot_email"]: bot = bot_name bot_config = config break @@ -197,27 +197,27 @@ def handle_bot() -> str: raise BadRequest( "Cannot find a bot with email {} in the Botserver " "configuration file. Do the emails in your botserverrc " - "match the bot emails on the server?".format(event['bot_email']) + "match the bot emails on the server?".format(event["bot_email"]) ) - if bot_config['token'] != event['token']: + if bot_config["token"] != event["token"]: raise Unauthorized( "Request token does not match token found for bot {} in the " "Botserver configuration file. Do the outgoing webhooks in " - "Zulip point to the right Botserver?".format(event['bot_email']) + "Zulip point to the right Botserver?".format(event["bot_email"]) ) app.config.get("BOTS_LIB_MODULES", {})[bot] bot_handler = app.config.get("BOT_HANDLERS", {})[bot] message_handler = app.config.get("MESSAGE_HANDLERS", {})[bot] - is_mentioned = event['trigger'] == "mention" - is_private_message = event['trigger'] == "private_message" + is_mentioned = event["trigger"] == "mention" + is_private_message = event["trigger"] == "private_message" message = event["message"] - message['full_content'] = message['content'] + message["full_content"] = message["content"] # Strip at-mention botname from the message if is_mentioned: # message['content'] will be None when the bot's @-mention is not at the beginning. # In that case, the message shall not be handled. - message['content'] = lib.extract_query_without_mention(message=message, client=bot_handler) - if message['content'] is None: + message["content"] = lib.extract_query_without_mention(message=message, client=bot_handler) + if message["content"] is None: return json.dumps(dict(response_not_required=True)) if is_private_message or is_mentioned: @@ -261,5 +261,5 @@ def main() -> None: app.run(host=options.hostname, port=int(options.port), debug=True) -if __name__ == '__main__': +if __name__ == "__main__": main()