black: Reformat without skipping string normalization.

This commit is contained in:
PIG208 2021-05-28 17:05:11 +08:00 committed by Tim Abbott
parent fba21bb00d
commit 6f3f9bf7e4
178 changed files with 5242 additions and 5242 deletions

View file

@ -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'((?<!\s)\s$)|(\s\s\s+$)|(^\s+$)',
'strip': '\n',
'description': 'Fix trailing whitespace',
"pattern": r"((?<!\s)\s$)|(\s\s\s+$)|(^\s+$)",
"strip": "\n",
"description": "Fix trailing whitespace",
},
{
'pattern': r'^#+[A-Za-z0-9]',
'strip': '\n',
'description': 'Missing space after # in heading',
"pattern": r"^#+[A-Za-z0-9]",
"strip": "\n",
"description": "Missing space after # in heading",
},
]
python_rules = RuleList(
langs=['py'],
langs=["py"],
rules=[
{'pattern': r'".*"%\([a-z_].*\)?$', 'description': 'Missing space around "%"'},
{'pattern': r"'.*'%\([a-z_].*\)?$", 'description': 'Missing space around "%"'},
{"pattern": r'".*"%\([a-z_].*\)?$', "description": 'Missing space around "%"'},
{"pattern": r"'.*'%\([a-z_].*\)?$", "description": 'Missing space around "%"'},
# This rule is constructed with + to avoid triggering on itself
{'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" =" + 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<url>[^\]]+)\]\((?P=url)\)',
'description': 'Linkified markdown URLs should use cleaner <http://example.com> syntax.',
"pattern": r"\[(?P<url>[^\]]+)\]\((?P=url)\)",
"description": "Linkified markdown URLs should use cleaner <http://example.com> syntax.",
},
],
max_length=120,
@ -168,7 +168,7 @@ markdown_rules = RuleList(
)
txt_rules = RuleList(
langs=['txt'],
langs=["txt"],
rules=whitespace_rules,
)

View file

@ -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()

View file

@ -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 `<section tag>: <message body>.`)
words = line.split(': ', 1)[-1].split()
words = line.split(": ", 1)[-1].split()
first_word = words[0].lower()
if first_word in WORD_SET:

View file

@ -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()

View file

@ -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()

View file

@ -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 <package_name>/dist/*.'
"Build sdists and wheels for all packages with the"
"specified version number."
" sdists and wheels are stored in <package_name>/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()

View file

@ -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()

View file

@ -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]

View file

@ -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:

View file

@ -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()

View file

@ -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"])

View file

@ -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"])

View file

@ -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"])

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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()

View file

@ -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('<JohnSmith>'))
self.assertEqual(method, "send_text")
self.assertEqual(params[0], expect.format("<JohnSmith>"))

View file

@ -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")

View file

@ -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")

View file

@ -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",

View file

@ -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

View file

@ -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()

View file

@ -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)

View file

@ -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)

View file

@ -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.")

View file

@ -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)

View file

@ -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)

View file

@ -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,
}
)

View file

@ -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"

View file

@ -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}`.

View file

@ -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:

View file

@ -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",

View file

@ -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]]

View file

@ -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,))

View file

@ -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)

View file

@ -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 <https://zulip.com/integrations/doc/trello>.
"""
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()

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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,
)

View file

@ -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)

View file

@ -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()

View file

@ -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', '<strong>bold</strong>'),
('.3Asome_emoji.3A', ':some_emoji:'),
("topic", "topic"),
(".2Edot", ".dot"),
(".23stream.20name", "#stream name"),
("(no.20topic)", "(no topic)"),
(".3Cstrong.3Ebold.3C.2Fstrong.3E", "<strong>bold</strong>"),
(".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()

File diff suppressed because it is too large Load diff

View file

@ -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>'
"script_name", nargs="?", default="", help="print path to the script <script_name>"
)
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()

View file

@ -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)

View file

@ -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,
}
)
)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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,
}
)
)

View file

@ -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))

View file

@ -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,
}
)
)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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}
)
)

View file

@ -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))

View file

@ -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)

View file

@ -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)

View file

@ -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}
)
)

View 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))

View file

@ -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()

View file

@ -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())

View file

@ -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)

View file

@ -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 <source_id>',
'list-customers <source_id>',
'list-subscriptions <source_id>',
'create-plan <source_id> <oid> <name> <currency> <amount> <interval> <interval_count>',
"help",
"list-commands",
"account-info",
"list-sources",
"list-plans <source_id>",
"list-customers <source_id>",
"list-subscriptions <source_id>",
"create-plan <source_id> <oid> <name> <currency> <amount> <interval> <interval_count>",
]
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

View file

@ -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 <source_id> : List the plans for the source\n'
' - list-customers <source_id> : List the customers in the source\n'
' - list-subscriptions <source_id> : List the subscriptions in the '
'source\n'
' - create-plan <source_id> <oid> <name> <currency> <amount> <interval> '
'<interval_count> : 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 <source_id> : List the plans for the source\n"
" - list-customers <source_id> : List the customers in the source\n"
" - list-subscriptions <source_id> : List the subscriptions in the "
"source\n"
" - create-plan <source_id> <oid> <name> <currency> <amount> <interval> "
"<interval_count> : 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.")

View file

@ -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)

View file

@ -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)

View file

@ -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 (?P<user_color>white|black) with computer')
MOVE_REGEX = re.compile('do (?P<move_san>.+)$')
RESIGN_REGEX = re.compile('resign$')
START_REGEX = re.compile("start with other user$")
START_COMPUTER_REGEX = re.compile("start as (?P<user_color>white|black) with computer")
MOVE_REGEX = re.compile("do (?P<move_san>.+)$")
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 <color> 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 <color> 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 <your move>```\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 <your move>```\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)

View file

@ -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 <your move>```
*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 <your move>```
*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 <your move>```
*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),
]
)

View file

@ -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 <column-number>` or `<column-number>` to place a token.\n\
The first player to get 4 in a row wins!\n Good Luck!'
return "Type `move <column-number>` or `<column-number>` 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 <column-number>``` or ```<column-number>```'
"* To make your move during a game, type\n"
"```move <column-number>``` or ```<column-number>```"
)
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,

View file

@ -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 ""

View file

@ -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 <column-number>``` or ```<column-number>```'''
```move <column-number>``` or ```<column-number>```"""
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 <column-number>` or `<column-number>` to place a token.\n\
The first player to get 4 in a row wins!\n Good Luck!',
"Type `move <column-number>` or `<column-number>` 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])

View file

@ -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 <number> <unit_from> <unit_to>'
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

View file

@ -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"),

View file

@ -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 <number> <unit_from> <unit_to>`\n'
'Converts `number` in the unit <unit_from> to '
'the <unit_to> and prints the result\n'
'`number`: integer or floating point number, e.g. 12, 13.05, 0.002\n'
'<unit_from> and <unit_to> 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 <number> <unit_from> <unit_to>`\n"
"Converts `number` in the unit <unit_from> to "
"the <unit_to> and prints the result\n"
"`number`: integer or floating point number, e.g. 12, 13.05, 0.002\n"
"<unit_from> and <unit_to> 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."

View file

@ -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&nbsp;&nbsp;{}'.format(
d['type'], d['definition'], html2text.html2text(example)
example = d["example"] if d["example"] else "*No example available.*"
response += "\n" + "* (**{}**) {}\n&nbsp;&nbsp;{}".format(
d["type"], d["definition"], html2text.html2text(example)
)
except Exception:

View file

@ -15,8 +15,8 @@ class TestDefineBot(BotTestCase, DefaultTests):
"kept as a pet or for catching mice, and many breeds have been "
"developed.\n&nbsp;&nbsp;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"
"&nbsp;&nbsp;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.")

View file

@ -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)

View file

@ -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

View file

@ -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 <folderName>` 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

View file

@ -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 <foldername>` 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 <foldername>` 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 <filename> 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 <filename>` 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 <int>` is optional and is used to specify maximun results.\n"
" `--fd <folderName>` 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 <filename>`"
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 <optional_path>'
bot_response = "syntax: ls <optional_path>"
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 <optional_path>"
@ -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)

View file

@ -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

View file

@ -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"),

View file

@ -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 <local_file_path> : Upload a file, where <local_file_path> 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 <local_file_path> : Upload a file, where <local_file_path> 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 <local_file_path>` : Upload a file, where `<local_file_path>` is the path to the file'
'\n* `@uploader help` : Display help message'
"Use this bot with any of the following commands:"
"\n* `@uploader <local_file_path>` : Upload a file, where `<local_file_path>` 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)

View file

@ -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 <local_file_path>` : Upload a file, where `<local_file_path>` is the path to the file'
'\n* `@uploader help` : Display help message'
"Use this bot with any of the following commands:"
"\n* `@uploader <local_file_path>` : Upload a file, where `<local_file_path>` is the path to the file"
"\n* `@uploader help` : Display help message"
),
)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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 <text>', "Leave a comment."),
("archive", "Archive a conversation."),
("delete", "Delete a conversation."),
("spam", "Mark a conversation as spam."),
("open", "Restore a conversation."),
("comment <text>", "Leave a comment."),
]
CNV_ID_REGEXP = 'cnv_(?P<id>[0-9a-z]+)'
CNV_ID_REGEXP = "cnv_(?P<id>[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:

View file

@ -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.",

View file

@ -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 <column>` to place a token.\n \
return "Type `move <column>` 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 <column-number>```'
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 <column-number>```"
move_regex = r"move (\d)$"
model = MockModel
gameMessageHandler = GameHandlerBotMessageHandler
rules = ''
rules = ""
super().__init__(
game_name,

View file

@ -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 <column-number>```'''
```move <column-number>```"""
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 <column-number>```',
"move 123",
"* To make your move during a game, type\n```move <column-number>```",
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")

View file

@ -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 <tile1> <tile2> ...```'
"* To make your move during a game, type\n```move <tile1> <tile2> ...```"
)
move_regex = r'move [\d{1}\s]+$'
move_regex = r"move [\d{1}\s]+$"
model = GameOfFifteenModel
gameMessageHandler = GameOfFifteenMessageHandler
rules = '''Arrange the boards tiles from smallest to largest, left to right,
rules = """Arrange the boards 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,

View file

@ -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 <tile1> <tile2> ...```'''
```move <tile1> <tile2> ...```"""
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)

View file

@ -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,)
)

Some files were not shown because too many files have changed in this diff Show more