diff --git a/Pipfile b/Pipfile index 096fb9b3..c3b940f5 100644 --- a/Pipfile +++ b/Pipfile @@ -8,6 +8,9 @@ name = "pypi" aiodns = "*" aiohttp = "<2.3.0,>=2.0.0" websockets = ">=4.0,<5.0" +fuzzywuzzy = "*" +python-levenshtein = "*" +"discord.py" = {git = "https://github.com/Rapptz/discord.py", ref = "rewrite", extras = ["voice"]} [dev-packages] "flake8" = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 4e5214bb..7542a142 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d797e580ddcddc99bf058109ab0306ad584c2902752a3d4076ba713fdc580fb7" + "sha256": "ac057fc426bc367b4b56fca1b642a5927dd80389f86005108821769a61fd8850" }, "pipfile-spec": 6, "requires": { @@ -60,6 +60,21 @@ ], "version": "==3.0.4" }, + "discord.py": { + "extras": [ + "voice" + ], + "git": "https://github.com/Rapptz/discord.py", + "ref": "rewrite" + }, + "fuzzywuzzy": { + "hashes": [ + "sha256:d40c22d2744dff84885b30bbfc07fab7875f641d070374331777a4d1808b8d4e", + "sha256:ecf490216fb4d76b558a03042ff8f45a8782f17326caca1384d834cbaa2c7e6f" + ], + "index": "pypi", + "version": "==0.16.0" + }, "idna": { "hashes": [ "sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f", @@ -120,6 +135,13 @@ ], "version": "==2.3.0" }, + "python-levenshtein": { + "hashes": [ + "sha256:033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1" + ], + "index": "pypi", + "version": "==0.12.0" + }, "websockets": { "hashes": [ "sha256:0c31bc832d529dc7583d324eb6c836a4f362032a1902723c112cf57883488d8c", @@ -282,10 +304,10 @@ }, "gitpython": { "hashes": [ - "sha256:ad61bc25deadb535b047684d06f3654c001d9415e1971e51c9c20f5b510076e9", - "sha256:b8367c432de995dc330b5b146c5bfdc0926b8496e100fda6692134e00c0dcdc5" + "sha256:05069e26177c650b3cb945dd543a7ef7ca449f8db5b73038b465105673c1ef61", + "sha256:c47cc31af6e88979c57a33962cbc30a7c25508d74a1b3a19ec5aa7ed64b03129" ], - "version": "==2.1.8" + "version": "==2.1.9" }, "idna": { "hashes": [ diff --git a/README.md b/README.md index 76c385a4..315287ef 100644 --- a/README.md +++ b/README.md @@ -4,66 +4,23 @@ This is the repository for all code relating to our first code jam, in March 201 **This code jam runs from the 23rd of March to the 25th of March, measured using the UTC timezone.** Make sure you open your pull request by then. Once the deadline is up, stop pushing commits - we will not accept any submissions made after this date. -## How To Participate +## What does it do? +Searches Wikipedia for snake information, but first it **converts** your search to a valid snake result. You get a nice table when you invoke the search command. +**bot.snakes.get("viper")** -First things first - set up your repository. Read [this guide on our site](https://pythondiscord.com/info/jams) for information on how to set yourself up for a code jam. -Remember, only one teammate needs to fork the repository - everyone else should be granted access to that fork as a contributor, so that they can work on it directly. +![Multiple_search_results](https://i.imgur.com/9Lij5Jp.png) -Make sure you have the following things installed: +You can off-course search directly for the scientific name. +**bot.snakes.get("Bothriechis schlegelii") -* Python 3.6 or later (installed with the PATH option enabled if you're on Windows) -* Pip - make sure you can run `pip` in a terminal or command prompt -* Pipenv - you can install this by running `pip install pipenv` in a terminal or command prompt - * Like before, make sure you can run `pipenv` in a terminal or command prompt +![Scientific_search](https://i.imgur.com/M7WdO18.png) -Next up, set up your project with `pipenv`. We've [compiled some documentation](./doc) for you to read over if you get stuck - you can find it in the `doc/` folder, -and you absolutely should read all of it, and it will likely answer some of the questions that you have. +**bot.snakes.get()** returns a random snake from wikipedia -Use `pipenv run run.py` to start your project. You can press `CTRL+C` with the bot window selected to stop it. +There is also a guessing game here -Remember, if you need help, you can always ask on the server! +**bot.snakes.guess()** -## The Task +![guess_the_snake](https://i.imgur.com/JWHrDbk.png) -This month's theme is: **Snakes**. - -For this code jam, your task will be to create a Snake cog for a [Discord.py rewrite bot](https://github.com/Rapptz/discord.py/tree/rewrite). -You can find the [documentation for Discord.py rewrite here](https://discordpy.readthedocs.io/en/rewrite/). The best cog commands will be -added to the official Python Discord bot and made available to everyone on the server. The overall best cog will be awarded custom Code Jam -Champion roles, but the best commands from the teams who did not win will also be added to our bot, and any users who write something that -ends up in the bot will be awarded Contributor roles on the server. - -We have prepared some Discord.py rewrite boilerplate for you in this repo. Fork the repo and work in the file called **snakes.py**, in **bot/cogs**. - -This means you won't have to write the basic bot itself, you'll just have to write the stuff that goes in the cog. For those of you with no -discord.py experience, cogs are like little modules that the bot can load, and contain a class with methods that are hooked up to bot commands -(like **bot.tags.get**). That way, when you type `bot.snakes.get('python')`, it will run the method inside the cog that corresponds to this command. - -Your initial task will be to write **get_snek**. This is the minimum requirement for this contest, and everyone must do it. **get_snek** will be a -method that goes online and fetches information about a snake. If you run it without providing an argument, it should fetch information about a -random snake, including the name of the snake, a picture of the snake, and various information about it. Is it venomous? Where can it be found? -What information you choose to get is up to you. - -`get_snek()` should also take an optional argument `name`, which should be a string that contains the name of a snake. For example, if you do -`get_snek('cobra')`, it should get information about a cobra. `name` should be case insensitive. - -If `get_snek('Python')` is called, the method should instead return information about the programming language, but making sure to return the -same type of information as for all the other snakes. Fill in this information in any way you want, try to have some fun with it. - -The information should be returned as a dictionary, so that other methods in the Snake class can call it and make use of it. - -Once you have finished `get_snek()`, you should make at least two bot commands. The first command, `get()`, should simply call `get_snek()` -with whatever arguments the user provided, and then make a nice embed that it returns to Discord. For example, if the user in the Discord -channel says `bot.snakes.get('anaconda')`, the bot should post an embed that shows a picture of an anaconda and some information about the -snake. - -The second command is entirely up to you. You can choose to use `get_snek` for this command as well, or you can come up with something entirely -different. The only requirement is that it is snake related in some way or other. Here is your chance to be creative. It is these commands that -will win or lose you this code jam. The best original ideas for these commands will probably walk away with the victory. - -You are allowed to make as many additional commands as you want, but try to keep it a reasonable amount. The team that writes the most commands is -not automatically going to win. One really excellent command is much better than 10 mediocre ones. - ---- - -Have fun, and don't be afraid to ask for help in the usual places if you need it! +Alas, if you are in a voice channel and type **bot.zen** you are greeted with a little easter-egg diff --git a/bot/cogs/snakes.py b/bot/cogs/snakes.py index c9ed8042..7b331af5 100644 --- a/bot/cogs/snakes.py +++ b/bot/cogs/snakes.py @@ -1,10 +1,22 @@ # coding=utf-8 +import asyncio import logging +import random +import re +import textwrap from typing import Any, Dict -from discord.ext.commands import AutoShardedBot, Context, command +import aiohttp +import async_timeout +import discord +from discord.ext.commands import AutoShardedBot, Context, command, bot_has_permissions + +from bot.converters import Snake +from bot.decorators import locked +from bot.utils import disambiguate log = logging.getLogger(__name__) +URL = "https://en.wikipedia.org/w/api.php?" class Snakes: @@ -12,10 +24,24 @@ class Snakes: Snake-related commands """ + # I really hope this works + wiki_sects = re.compile(r'(?:=+ (.*?) =+)(.*?\n\n)', flags=re.DOTALL) + wiki_brief = re.compile(r'(.*?)(=+ (.*?) =+)', flags=re.DOTALL) + + valid = ('gif', 'png', 'jpeg', 'jpg', 'webp') + def __init__(self, bot: AutoShardedBot): self.bot = bot - async def get_snek(self, name: str = None) -> Dict[str, Any]: + async def fetch(self, session, url, params=None): + if params is None: + params = {} + + async with async_timeout.timeout(10): + async with session.get(url, params=params) as response: + return await response.json() + + async def get_snek(self, name: str) -> Dict[str, Any]: """ Go online and fetch information about a snake @@ -25,23 +51,191 @@ async def get_snek(self, name: str = None) -> Dict[str, Any]: If "python" is given as the snake name, you should return information about the programming language, but with all the information you'd provide for a real snake. Try to have some fun with this! - :param name: Optional, the name of the snake to get information for - omit for a random snake + :param name: The name of the snake to get information for - omit for a random snake :return: A dict containing information on a snake """ + snake_info = {} - @command() - async def get(self, ctx: Context, name: str = None): - """ - Go online and fetch information about a snake + async with aiohttp.ClientSession() as session: + params = { + 'format': 'json', + 'action': 'query', + 'list': 'search', + 'srsearch': name, + 'utf8': '', + 'srlimit': '1', + } + + json = await self.fetch(session, URL, params=params) + + # wikipedia does have a error page + try: + pageid = json["query"]["search"][0]["pageid"] + except KeyError: + # Wikipedia error page ID(?) + pageid = 41118 + + params = { + 'format': 'json', + 'action': 'query', + 'prop': 'extracts|images|info', + 'exlimit': 'max', + 'explaintext': '', + 'inprop': 'url', + 'pageids': pageid + } - This should make use of your `get_snek` method, using it to get information about a snake. This information - should be sent back to Discord in an embed. + json = await self.fetch(session, URL, params=params) + + # constructing dict - handle exceptions later + try: + snake_info["title"] = json["query"]["pages"][f"{pageid}"]["title"] + snake_info["extract"] = json["query"]["pages"][f"{pageid}"]["extract"] + snake_info["images"] = json["query"]["pages"][f"{pageid}"]["images"] + snake_info["fullurl"] = json["query"]["pages"][f"{pageid}"]["fullurl"] + snake_info["pageid"] = json["query"]["pages"][f"{pageid}"]["pageid"] + except KeyError: + snake_info["error"] = True + if snake_info["images"]: + i_url = 'https://commons.wikimedia.org/wiki/Special:FilePath/' + image_list = [] + map_list = [] + thumb_list = [] + + # Wikipedia has arbitrary images that are not snakes + banned = [ + 'Commons-logo.svg', + 'Red%20Pencil%20Icon.png', + 'distribution', + 'The%20Death%20of%20Cleopatra%20arthur.jpg', + 'Head%20of%20holotype', + 'locator', + 'Woma.png', + '-map.', + '.svg', + 'ange.', + 'Adder%20(PSF).png' + ] + + for image in snake_info["images"]: + # images come in the format of `File:filename.extension` + file, sep, filename = image["title"].partition(':') + filename = filename.replace(" ", "%20") # Wikipedia returns good data! + + if not filename.startswith('Map'): + if any(ban in filename for ban in banned): + log.info("the image is banned") + else: + image_list.append(f"{i_url}{filename}") + thumb_list.append(f"{i_url}{filename}?width=100") + else: + map_list.append(f"{i_url}{filename}") + + snake_info["image_list"] = image_list + snake_info["map_list"] = map_list + snake_info["thumb_list"] = thumb_list + return snake_info + + @command(name="snakes.get()", aliases=["snakes.get"]) + @bot_has_permissions(manage_messages=True) + @locked() + async def get(self, ctx: Context, name: Snake = None): + """ + Fetches information about a snake from Wikipedia. :param ctx: Context object passed from discord.py :param name: Optional, the name of the snake to get information for - omit for a random snake """ + if name is None: + name = Snake.random() + + data = await self.get_snek(name) + + if data.get('error'): + return await ctx.send('Could not fetch data from Wikipedia.') + + match = self.wiki_brief.match(data['extract']) + embed = discord.Embed( + title=data['title'], + description=match.group(1) if match else None, + url=data['fullurl'], + colour=0x59982F + ) + + fields = self.wiki_sects.findall(data['extract']) + excluded = ('see also', 'further reading', 'subspecies') + + for title, body in fields: + if title.lower() in excluded: + continue + if not body.strip(): + continue + # Only takes the first sentence + title, dot, _ = title.partition('.') + # There's probably a better way to do this + value = textwrap.shorten(body.strip(), width=200) + embed.add_field(name=title + dot, value=value + '\n\u200b', inline=False) + + embed.set_footer(text='Powered by Wikipedia') + + emoji = 'https://emojipedia-us.s3.amazonaws.com/thumbs/60/google/3/snake_1f40d.png' + image = next((url for url in data['image_list'] if url.endswith(self.valid)), emoji) + embed.set_thumbnail(url=image) + + await ctx.send(embed=embed) + + @command(hidden=True) + async def zen(self, ctx): + """ + >>> import this + + Long time Pythoneer Tim Peters succinctly channels the BDFL's guiding principles + for Python's design into 20 aphorisms, only 19 of which have been written down. + + You must be connected to a voice channel in order to use this command. + """ + channel = ctx.author.voice.channel + if channel is None: + return + + state = ctx.guild.voice_client + if state is not None: + # Already playing + return + + voice = await channel.connect() + source = discord.FFmpegPCMAudio('zen.mp3') + voice.play(source, after=lambda *args: asyncio.run_coroutine_threadsafe( + voice.disconnect(), loop=ctx.bot.loop + )) + + @command(name="snakes.guess()", aliases=["snakes.guess", "identify"]) + @locked() + async def guess(self, ctx): + """ + Snake identifying game! + """ + image = None + + while image is None: + snakes = [Snake.random() for _ in range(5)] + answer = random.choice(snakes) + + data = await self.get_snek(answer) + + image = next((url for url in data['image_list'] if url.endswith(self.valid)), None) + + embed = discord.Embed( + title='Which of the following is the snake in the image?', + colour=random.randint(1, 0xFFFFFF) + ) + embed.set_image(url=image) + + guess = await disambiguate(ctx, snakes, timeout=60, embed=embed) - # Any additional commands can be placed here. Be creative, but keep it to a reasonable amount! + if guess == answer: + return await ctx.send('You guessed correctly!') + await ctx.send(f'You guessed wrong. The correct answer was {answer}.') def setup(bot): diff --git a/bot/converters.py b/bot/converters.py new file mode 100644 index 00000000..48a02254 --- /dev/null +++ b/bot/converters.py @@ -0,0 +1,49 @@ +import json +import random + +import discord +from discord.ext.commands import Converter +from fuzzywuzzy import fuzz + +from bot.utils import disambiguate + + +class Snake(Converter): + with open('snakes.json', 'r') as f: + snakes = json.load(f) + + async def convert(self, ctx, name): + name = name.lower() + + if name == 'python': + return 'Python (programming language)' + + def get_potential(iterable, *, threshold=80): + nonlocal name + potential = [] + + for item in iterable: + original, item = item, item.lower() + + if name == item: + return [original] + + a, b = fuzz.ratio(name, item), fuzz.partial_ratio(name, item) + if a >= threshold or b >= threshold: + potential.append(original) + + return potential + + all_names = self.snakes.keys() | self.snakes.values() + timeout = len(all_names) * (3 / 4) + + embed = discord.Embed(title='Found multiple choices. Please choose the correct one.', colour=0x59982F) + embed.set_author(name=ctx.author.display_name, icon_url=ctx.author.avatar_url) + + name = await disambiguate(ctx, get_potential(all_names), timeout=timeout, embed=embed) + return self.snakes.get(name, name) + + @classmethod + def random(cls): + # list cast necessary because choice() uses indexing internally + return random.choice(list(cls.snakes.values())) diff --git a/bot/decorators.py b/bot/decorators.py index 7009e259..e0875b92 100644 --- a/bot/decorators.py +++ b/bot/decorators.py @@ -1,5 +1,8 @@ # coding=utf-8 import logging +from asyncio import Lock +from functools import wraps +from weakref import WeakValueDictionary from discord.ext import commands from discord.ext.commands import Context @@ -47,3 +50,26 @@ async def predicate(ctx: Context): f"The result of the in_channel check was {check}.") return check return commands.check(predicate) + + +def locked(): + """ + Allows the user to only run one instance of the decorated command at a time. + + Subsequent calls to the command from the same author are + ignored until the command has completed invocation. + + This decorator has to go before (below) the `command` decorator. + """ + def wrap(func): + func.__locks = WeakValueDictionary() + + @wraps(func) + async def inner(self, ctx, *args, **kwargs): + lock = func.__locks.setdefault(ctx.author.id, Lock()) + if lock.locked(): + return + async with func.__locks.setdefault(ctx.author.id, Lock()): + return await func(self, ctx, *args, **kwargs) + return inner + return wrap diff --git a/bot/utils.py b/bot/utils.py index eac37a4b..91d57bf8 100644 --- a/bot/utils.py +++ b/bot/utils.py @@ -1,4 +1,78 @@ # coding=utf-8 +import asyncio +from typing import List + +import discord +from discord.ext.commands import BadArgument, Context + +from bot.pagination import LinePaginator + + +async def disambiguate(ctx: Context, entries: List[str], + *, timeout: float = 30, per_page: int = 20, empty: bool = False, embed: discord.Embed = None): + """ + Has the user choose between multiple entries in case one could not be chosen automatically. + + :param ctx: Context object from discord.py + :param entries: List of items for user to choose from + :param timeout: Number of seconds to wait before canceling disambiguation + :param per_page: Entries per embed page + :param empty: Whether the paginator should have an extra line between items + :param embed: The embed that the paginator will use. + :return: Users choice for correct entry. + """ + if len(entries) == 0: + raise BadArgument('No matches found.') + + if len(entries) == 1: + return entries[0] + + choices = (f'{index}: {entry}' for index, entry in enumerate(entries, start=1)) + + def check(message): + return (message.content.isdigit() and + message.author == ctx.author and + message.channel == ctx.channel) + + try: + if embed is None: + embed = discord.Embed() + + coro1 = ctx.bot.wait_for('message', check=check, timeout=timeout) + coro2 = LinePaginator.paginate(choices, ctx, embed=embed, max_lines=per_page, + empty=empty, max_size=6000, timeout=9000) + + # wait_for timeout will go to except instead of the wait_for thing as I expected + futures = [asyncio.ensure_future(coro1), asyncio.ensure_future(coro2)] + done, pending = await asyncio.wait(futures, return_when=asyncio.FIRST_COMPLETED, loop=ctx.bot.loop) + + # :yert: + result = list(done)[0].result() + + # Pagination was canceled - result is None + if result is None: + for coro in pending: + coro.cancel() + raise BadArgument('Canceled.') + + # Pagination was not initiated, only one page + if result.author == ctx.bot.user: + # Continue the wait_for + result = await list(pending)[0] + + # Love that duplicate code + for coro in pending: + coro.cancel() + except asyncio.TimeoutError: + raise BadArgument('Timed out.') + + # Guaranteed to not error because of isdigit() in check + index = int(result.content) + + try: + return entries[index - 1] + except IndexError: + raise BadArgument('Invalid choice.') class CaseInsensitiveDict(dict): diff --git a/run.py b/run.py index 2ec711fd..22edf4f2 100644 --- a/run.py +++ b/run.py @@ -2,7 +2,6 @@ import os from aiohttp import AsyncResolver, ClientSession, TCPConnector - from discord import Game from discord.ext.commands import AutoShardedBot, when_mentioned_or diff --git a/snakes.json b/snakes.json new file mode 100644 index 00000000..8306ee8f --- /dev/null +++ b/snakes.json @@ -0,0 +1,544 @@ +{ + "Acanthophis": "Acanthophis", + "Aesculapian snake": "Aesculapian snake", + "African beaked snake": "Rufous beaked snake", + "African puff adder": "Bitis arietans", + "African rock python": "African rock python", + "African twig snake": "Twig snake", + "Agkistrodon piscivorus": "Agkistrodon piscivorus", + "Ahaetulla": "Ahaetulla", + "Amazonian palm viper": "Bothriopsis bilineata", + "American copperhead": "Agkistrodon contortrix", + "Amethystine python": "Amethystine python", + "Anaconda": "Anaconda", + "Andaman cat snake": "Boiga andamanensis", + "Andrea's keelback": "Amphiesma andreae", + "Annulated sea snake": "Hydrophis cyanocinctus", + "Arafura file snake": "Acrochordus arafurae", + "Arizona black rattlesnake": "Crotalus oreganus cerberus", + "Arizona coral snake": "Coral snake", + "Aruba rattlesnake": "Crotalus durissus unicolor", + "Asian cobra": "Indian cobra", + "Asian keelback": "Amphiesma vibakari", + "Asp (reptile)": "Asp (reptile)", + "Assam keelback": "Amphiesma pealii", + "Australian copperhead": "Austrelaps", + "Australian scrub python": "Amethystine python", + "Baird's rat snake": "Pantherophis bairdi", + "Banded Flying Snake": "Banded flying snake", + "Banded cat-eyed snake": "Banded cat-eyed snake", + "Banded krait": "Banded krait", + "Barred wolf snake": "Lycodon striatus", + "Beaked sea snake": "Enhydrina schistosa", + "Beauty rat snake": "Beauty rat snake", + "Beddome's cat snake": "Boiga beddomei", + "Beddome's coral snake": "Beddome's coral snake", + "Bird snake": "Twig snake", + "Black-banded trinket snake": "Oreocryptophis porphyraceus", + "Black-headed snake": "Western black-headed snake", + "Black-necked cobra": "Black-necked spitting cobra", + "Black-necked spitting cobra": "Black-necked spitting cobra", + "Black-striped keelback": "Buff striped keelback", + "Black-tailed horned pit viper": "Mixcoatlus melanurus", + "Black headed python": "Black-headed python", + "Black krait": "Greater black krait", + "Black mamba": "Black mamba", + "Black rat snake": "Rat snake", + "Black tree cobra": "Cobra", + "Blind snake": "Scolecophidia", + "Blonde hognose snake": "Hognose", + "Blood python": "Python brongersmai", + "Blue krait": "Bungarus candidus", + "Blunt-headed tree snake": "Imantodes cenchoa", + "Boa constrictor": "Boa constrictor", + "Bocourt's water snake": "Subsessor", + "Boelen python": "Morelia boeleni", + "Boidae": "Boidae", + "Boiga": "Boiga", + "Boomslang": "Boomslang", + "Brahminy blind snake": "Indotyphlops braminus", + "Brazilian coral snake": "Coral snake", + "Brazilian smooth snake": "Hydrodynastes gigas", + "Brown snake (disambiguation)": "Brown snake", + "Brown tree snake": "Brown tree snake", + "Brown white-lipped python": "Leiopython", + "Buff striped keelback": "Buff striped keelback", + "Bull snake": "Bull snake", + "Burmese keelback": "Burmese keelback water snake", + "Burmese krait": "Burmese krait", + "Burmese python": "Burmese python", + "Burrowing viper": "Atractaspidinae", + "Buttermilk racer": "Coluber constrictor anthicus", + "California kingsnake": "California kingsnake", + "Cantor's pitviper": "Trimeresurus cantori", + "Cape cobra": "Cape cobra", + "Cape coral snake": "Aspidelaps lubricus", + "Cape gopher snake": "Cape gopher snake", + "Carpet viper": "Echis", + "Cat-eyed night snake": "Banded cat-eyed snake", + "Cat-eyed snake": "Banded cat-eyed snake", + "Cat snake": "Boiga", + "Central American lyre snake": "Trimorphodon biscutatus", + "Central ranges taipan": "Taipan", + "Chappell Island tiger snake": "Tiger snake", + "Checkered garter snake": "Checkered garter snake", + "Checkered keelback": "Checkered keelback", + "Children's python": "Children's python", + "Chinese cobra": "Chinese cobra", + "Coachwhip snake": "Masticophis flagellum", + "Coastal taipan": "Coastal taipan", + "Cobra": "Cobra", + "Collett's snake": "Collett's snake", + "Common adder": "Vipera berus", + "Common cobra": "Chinese cobra", + "Common garter snake": "Common garter snake", + "Common ground snake": "Western ground snake", + "Common keelback (disambiguation)": "Common keelback", + "Common tiger snake": "Tiger snake", + "Common worm snake": "Indotyphlops braminus", + "Congo snake": "Amphiuma", + "Congo water cobra": "Naja christyi", + "Coral snake": "Coral snake", + "Corn snake": "Corn snake", + "Coronado Island rattlesnake": "Crotalus oreganus caliginis", + "Crossed viper": "Vipera berus", + "Crotalus cerastes": "Crotalus cerastes", + "Crotalus durissus": "Crotalus durissus", + "Crotalus horridus": "Timber rattlesnake", + "Crowned snake": "Tantilla", + "Cuban boa": "Chilabothrus angulifer", + "Cuban wood snake": "Tropidophis melanurus", + "Dasypeltis": "Dasypeltis", + "Desert death adder": "Desert death adder", + "Desert kingsnake": "Desert kingsnake", + "Desert woma python": "Woma python", + "Diamond python": "Morelia spilota spilota", + "Dog-toothed cat snake": "Boiga cynodon", + "Down's tiger snake": "Tiger snake", + "Dubois's sea snake": "Aipysurus duboisii", + "Durango rock rattlesnake": "Crotalus lepidus klauberi", + "Dusty hognose snake": "Hognose", + "Dwarf beaked snake": "Dwarf beaked snake", + "Dwarf boa": "Boa constrictor", + "Dwarf pipe snake": "Anomochilus", + "Eastern brown snake": "Eastern brown snake", + "Eastern coral snake": "Micrurus fulvius", + "Eastern diamondback rattlesnake": "Eastern diamondback rattlesnake", + "Eastern green mamba": "Eastern green mamba", + "Eastern hognose snake": "Eastern hognose snake", + "Eastern mud snake": "Mud snake", + "Eastern racer": "Coluber constrictor", + "Eastern tiger snake": "Tiger snake", + "Eastern water cobra": "Cobra", + "Elaps harlequin snake": "Micrurus fulvius", + "Eunectes": "Eunectes", + "European Smooth Snake": "Smooth snake", + "False cobra": "False cobra", + "False coral snake": "Coral snake", + "False water cobra": "Hydrodynastes gigas", + "Fierce snake": "Inland taipan", + "Flying snake": "Chrysopelea", + "Forest cobra": "Forest cobra", + "Forsten's cat snake": "Boiga forsteni", + "Fox snake": "Fox snake", + "Gaboon viper": "Gaboon viper", + "Garter snake": "Garter snake", + "Giant Malagasy hognose snake": "Hognose", + "Glossy snake": "Glossy snake", + "Gold-ringed cat snake": "Boiga dendrophila", + "Gold tree cobra": "Pseudohaje goldii", + "Golden tree snake": "Chrysopelea ornata", + "Gopher snake": "Pituophis catenifer", + "Grand Canyon rattlesnake": "Crotalus oreganus abyssus", + "Grass snake": "Grass snake", + "Gray cat snake": "Boiga ocellata", + "Great Plains rat snake": "Pantherophis emoryi", + "Green anaconda": "Green anaconda", + "Green rat snake": "Rat snake", + "Green tree python": "Green tree python", + "Grey-banded kingsnake": "Gray-banded kingsnake", + "Grey Lora": "Leptophis stimsoni", + "Halmahera python": "Morelia tracyae", + "Harlequin coral snake": "Micrurus fulvius", + "Herald snake": "Caduceus", + "High Woods coral snake": "Coral snake", + "Hill keelback": "Amphiesma monticola", + "Himalayan keelback": "Amphiesma platyceps", + "Hognose snake": "Hognose", + "Hognosed viper": "Porthidium", + "Hook Nosed Sea Snake": "Enhydrina schistosa", + "Hoop snake": "Hoop snake", + "Hopi rattlesnake": "Crotalus viridis nuntius", + "Indian cobra": "Indian cobra", + "Indian egg-eater": "Indian egg-eating snake", + "Indian flying snake": "Chrysopelea ornata", + "Indian krait": "Bungarus", + "Indigo snake": "Drymarchon", + "Inland carpet python": "Morelia spilota metcalfei", + "Inland taipan": "Inland taipan", + "Jamaican boa": "Jamaican boa", + "Jan's hognose snake": "Hognose", + "Japanese forest rat snake": "Euprepiophis conspicillatus", + "Japanese rat snake": "Japanese rat snake", + "Japanese striped snake": "Japanese striped snake", + "Kayaudi dwarf reticulated python": "Reticulated python", + "Keelback": "Natricinae", + "Khasi Hills keelback": "Amphiesma khasiense", + "King Island tiger snake": "Tiger snake", + "King brown": "Mulga snake", + "King cobra": "King cobra", + "King rat snake": "Rat snake", + "King snake": "Kingsnake", + "Krait": "Bungarus", + "Krefft's tiger snake": "Tiger snake", + "Lance-headed rattlesnake": "Crotalus polystictus", + "Lancehead": "Bothrops", + "Large shield snake": "Pseudotyphlops", + "Leptophis ahaetulla": "Leptophis ahaetulla", + "Lesser black krait": "Lesser black krait", + "Long-nosed adder": "Eastern hognose snake", + "Long-nosed tree snake": "Western hognose snake", + "Long-nosed whip snake": "Ahaetulla nasuta", + "Long-tailed rattlesnake": "Rattlesnake", + "Longnosed worm snake": "Leptotyphlops macrorhynchus", + "Lyre snake": "Trimorphodon", + "Madagascar ground boa": "Acrantophis madagascariensis", + "Malayan krait": "Bungarus candidus", + "Malayan long-glanded coral snake": "Calliophis bivirgata", + "Malayan pit viper": "Pit viper", + "Mamba": "Mamba", + "Mamushi": "Mamushi", + "Manchurian Black Water Snake": "Elaphe schrenckii", + "Mandarin rat snake": "Mandarin rat snake", + "Mangrove snake (disambiguation)": "Mangrove snake", + "Many-banded krait": "Many-banded krait", + "Many-banded tree snake": "Many-banded tree snake", + "Many-spotted cat snake": "Boiga multomaculata", + "Massasauga rattlesnake": "Massasauga", + "Mexican black kingsnake": "Mexican black kingsnake", + "Mexican green rattlesnake": "Crotalus basiliscus", + "Mexican hognose snake": "Hognose", + "Mexican parrot snake": "Leptophis mexicanus", + "Mexican racer": "Coluber constrictor oaxaca", + "Mexican vine snake": "Oxybelis aeneus", + "Mexican west coast rattlesnake": "Crotalus basiliscus", + "Micropechis ikaheka": "Micropechis ikaheka", + "Midget faded rattlesnake": "Crotalus oreganus concolor", + "Milk snake": "Milk snake", + "Moccasin snake": "Agkistrodon piscivorus", + "Modest keelback": "Amphiesma modestum", + "Mojave desert sidewinder": "Crotalus cerastes", + "Mojave rattlesnake": "Crotalus scutulatus", + "Mole viper": "Atractaspidinae", + "Moluccan flying snake": "Chrysopelea", + "Montpellier snake": "Malpolon monspessulanus", + "Mud adder": "Mud adder", + "Mud snake": "Mud snake", + "Mussurana": "Mussurana", + "Narrowhead Garter Snake": "Garter snake", + "Nicobar Island keelback": "Amphiesma nicobariense", + "Nicobar cat snake": "Boiga wallachi", + "Night snake": "Night snake", + "Nilgiri keelback": "Nilgiri keelback", + "North eastern king snake": "Eastern hognose snake", + "Northeastern hill krait": "Northeastern hill krait", + "Northern black-tailed rattlesnake": "Crotalus molossus", + "Northern tree snake": "Dendrelaphis calligastra", + "Northern water snake": "Northern water snake", + "Northern white-lipped python": "Leiopython", + "Oaxacan small-headed rattlesnake": "Crotalus intermedius gloydi", + "Okinawan habu": "Okinawan habu", + "Olive sea snake": "Aipysurus laevis", + "Opheodrys": "Opheodrys", + "Orange-collared keelback": "Rhabdophis himalayanus", + "Ornate flying snake": "Chrysopelea ornata", + "Oxybelis": "Oxybelis", + "Palestine viper": "Vipera palaestinae", + "Paradise flying snake": "Chrysopelea paradisi", + "Parrot snake": "Leptophis ahaetulla", + "Patchnose snake": "Salvadora (snake)", + "Pelagic sea snake": "Yellow-bellied sea snake", + "Peninsula tiger snake": "Tiger snake", + "Perrotet's shieldtail snake": "Plectrurus perrotetii", + "Persian rat snake": "Rat snake", + "Pine snake": "Pine snake", + "Pit viper": "Pit viper", + "Plains hognose snake": "Western hognose snake", + "Prairie kingsnake": "Lampropeltis calligaster", + "Pygmy python": "Pygmy python", + "Pythonidae": "Pythonidae", + "Queen snake": "Queen snake", + "Rat snake": "Rat snake", + "Rattler": "Rattlesnake", + "Rattlesnake": "Rattlesnake", + "Red-bellied black snake": "Red-bellied black snake", + "Red-headed krait": "Red-headed krait", + "Red-necked keelback": "Rhabdophis subminiatus", + "Red-tailed bamboo pitviper": "Trimeresurus erythrurus", + "Red-tailed boa": "Boa constrictor", + "Red-tailed pipe snake": "Cylindrophis ruffus", + "Red blood python": "Python brongersmai", + "Red diamond rattlesnake": "Crotalus ruber", + "Reticulated python": "Reticulated python", + "Ribbon snake": "Ribbon snake", + "Ringed hognose snake": "Hognose", + "Rosy boa": "Rosy boa", + "Rough green snake": "Opheodrys aestivus", + "Rubber boa": "Rubber boa", + "Rufous beaked snake": "Rufous beaked snake", + "Russell's viper": "Russell's viper", + "San Francisco garter snake": "San Francisco garter snake", + "Sand boa": "Erycinae", + "Sand viper": "Sand viper", + "Saw-scaled viper": "Echis", + "Scarlet kingsnake": "Scarlet kingsnake", + "Sea snake": "Hydrophiinae", + "Selayer reticulated python": "Reticulated python", + "Shield-nosed cobra": "Shield-nosed cobra", + "Shield-tailed snake": "Uropeltidae", + "Sikkim keelback": "Sikkim keelback", + "Sind krait": "Sind krait", + "Smooth green snake": "Smooth green snake", + "South American hognose snake": "Hognose", + "South Andaman krait": "South Andaman krait", + "South eastern corn snake": "Corn snake", + "Southern Pacific rattlesnake": "Crotalus oreganus helleri", + "Southern black racer": "Southern black racer", + "Southern hognose snake": "Southern hognose snake", + "Southern white-lipped python": "Leiopython", + "Southwestern blackhead snake": "Tantilla hobartsmithi", + "Southwestern carpet python": "Morelia spilota imbricata", + "Southwestern speckled rattlesnake": "Crotalus mitchellii pyrrhus", + "Speckled hognose snake": "Hognose", + "Speckled kingsnake": "Lampropeltis getula holbrooki", + "Spectacled cobra": "Indian cobra", + "Sri Lanka cat snake": "Boiga ceylonensis", + "Stiletto snake": "Atractaspidinae", + "Stimson's python": "Stimson's python", + "Striped snake": "Japanese striped snake", + "Sumatran short-tailed python": "Python curtus", + "Sunbeam snake": "Xenopeltis", + "Taipan": "Taipan", + "Tan racer": "Coluber constrictor etheridgei", + "Tancitaran dusky rattlesnake": "Crotalus pusillus", + "Tanimbar python": "Reticulated python", + "Tasmanian tiger snake": "Tiger snake", + "Tawny cat snake": "Boiga ochracea", + "Temple pit viper": "Pit viper", + "Tentacled snake": "Erpeton tentaculatum", + "Texas Coral Snake": "Coral snake", + "Texas blind snake": "Leptotyphlops dulcis", + "Texas garter snake": "Texas garter snake", + "Texas lyre snake": "Trimorphodon biscutatus vilkinsonii", + "Texas night snake": "Hypsiglena jani", + "Thai cobra": "King cobra", + "Three-lined ground snake": "Atractus trilineatus", + "Tic polonga": "Russell's viper", + "Tiger rattlesnake": "Crotalus tigris", + "Tiger snake": "Tiger snake", + "Tigre snake": "Spilotes pullatus", + "Timber rattlesnake": "Timber rattlesnake", + "Tree snake": "Brown tree snake", + "Tri-color hognose snake": "Hognose", + "Trinket snake": "Trinket snake", + "Tropical rattlesnake": "Crotalus durissus", + "Twig snake": "Twig snake", + "Twin-Barred tree snake": "Banded flying snake", + "Twin-spotted rat snake": "Rat snake", + "Twin-spotted rattlesnake": "Crotalus pricei", + "Uracoan rattlesnake": "Crotalus durissus vegrandis", + "Viperidae": "Viperidae", + "Wall's keelback": "Amphiesma xenura", + "Wart snake": "Acrochordidae", + "Water adder": "Agkistrodon piscivorus", + "Water moccasin": "Agkistrodon piscivorus", + "West Indian racer": "Antiguan racer", + "Western blind snake": "Leptotyphlops humilis", + "Western carpet python": "Morelia spilota", + "Western coral snake": "Coral snake", + "Western diamondback rattlesnake": "Western diamondback rattlesnake", + "Western green mamba": "Western green mamba", + "Western ground snake": "Western ground snake", + "Western hognose snake": "Western hognose snake", + "Western mud snake": "Mud snake", + "Western tiger snake": "Tiger snake", + "Western woma python": "Woma python", + "White-lipped keelback": "Amphiesma leucomystax", + "Wolf snake": "Lycodon capucinus", + "Woma python": "Woma python", + "Wutu": "Bothrops alternatus", + "Wynaad keelback": "Amphiesma monticola", + "Yellow-banded sea snake": "Yellow-bellied sea snake", + "Yellow-bellied sea snake": "Yellow-bellied sea snake", + "Yellow-lipped sea snake": "Yellow-lipped sea krait", + "Yellow-striped rat snake": "Rat snake", + "Yellow anaconda": "Yellow anaconda", + "Yellow cobra": "Cape cobra", + "Yunnan keelback": "Amphiesma parallelum", + "Abaco Island boa": "Epicrates exsul", + "Agkistrodon bilineatus": "Agkistrodon bilineatus", + "Amazon tree boa": "Corallus hortulanus", + "Andaman cobra": "Andaman cobra", + "Angolan python": "Python anchietae", + "Arabian cobra": "Arabian cobra", + "Asp viper": "Vipera aspis", + "Ball Python": "Ball python", + "Ball python": "Ball python", + "Bamboo pitviper": "Trimeresurus gramineus", + "Banded pitviper": "Trimeresurus fasciatus", + "Banded water cobra": "Naja annulata", + "Barbour's pit viper": "Mixcoatlus barbouri", + "Bismarck ringed python": "Bothrochilus", + "Black-speckled palm-pitviper": "Bothriechis nigroviridis", + "Bluntnose viper": "Macrovipera lebetina", + "Bornean pitviper": "Trimeresurus borneensis", + "Borneo short-tailed python": "Borneo python", + "Bothrops jararacussu": "Bothrops jararacussu", + "Bredl's python": "Morelia bredli", + "Brongersma's pitviper": "Trimeresurus brongersmai", + "Brown spotted pitviper": "Trimeresurus mucrosquamatus", + "Brown water python": "Liasis fuscus", + "Burrowing cobra": "Egyptian cobra", + "Bush viper": "Atheris", + "Calabar python": "Calabar python", + "Caspian cobra": "Caspian cobra", + "Centralian carpet python": "Morelia bredli", + "Chinese tree viper": "Trimeresurus stejnegeri", + "Coastal carpet python": "Morelia spilota mcdowelli", + "Colorado desert sidewinder": "Crotalus cerastes laterorepens", + "Common lancehead": "Bothrops atrox", + "Cyclades blunt-nosed viper": "Macrovipera schweizeri", + "Dauan Island water python": "Liasis fuscus", + "De Schauensee's anaconda": "Eunectes deschauenseei", + "Dumeril's boa": "Acrantophis dumerili", + "Dusky pigmy rattlesnake": "Sistrurus miliarius barbouri", + "Dwarf sand adder": "Bitis peringueyi", + "Egyptian cobra": "Egyptian cobra", + "Elegant pitviper": "Trimeresurus elegans", + "Emerald tree boa": "Emerald tree boa", + "Equatorial spitting cobra": "Equatorial spitting cobra", + "European asp": "Vipera aspis", + "Eyelash palm-pitviper": "Bothriechis schlegelii", + "Eyelash pit viper": "Bothriechis schlegelii", + "Eyelash viper": "Bothriechis schlegelii", + "False horned viper": "Pseudocerastes", + "Fan-Si-Pan horned pitviper": "Trimeresurus cornutus", + "Fea's viper": "Azemiops", + "Fifty pacer": "Deinagkistrodon", + "Flat-nosed pitviper": "Trimeresurus puniceus", + "Godman's pit viper": "Cerrophidion godmani", + "Great Lakes bush viper": "Atheris nitschei", + "Green palm viper": "Bothriechis lateralis", + "Green tree pit viper": "Trimeresurus gramineus", + "Guatemalan palm viper": "Bothriechis aurifer", + "Guatemalan tree viper": "Bothriechis bicolor", + "Hagen's pitviper": "Trimeresurus hageni", + "Hairy bush viper": "Atheris hispida", + "Himehabu": "Ovophis okinavensis", + "Hogg Island boa": "Boa constrictor imperator", + "Honduran palm viper": "Bothriechis marchi", + "Horned desert viper": "Cerastes cerastes", + "Horseshoe pitviper": "Trimeresurus strigatus", + "Hundred pacer": "Deinagkistrodon", + "Hutton's tree viper": "Tropidolaemus huttoni", + "Indian python": "Python molurus", + "Indian tree viper": "Trimeresurus gramineus", + "Indochinese spitting cobra": "Indochinese spitting cobra", + "Indonesian water python": "Liasis mackloti", + "Javan spitting cobra": "Javan spitting cobra", + "Jerdon's pitviper": "Trimeresurus jerdonii", + "Jumping viper": "Atropoides", + "Jungle carpet python": "Morelia spilota cheynei", + "Kanburian pit viper": "Trimeresurus kanburiensis", + "Kaulback's lance-headed pitviper": "Trimeresurus kaulbacki", + "Kaznakov's viper": "Vipera kaznakovi", + "Kham Plateau pitviper": "Protobothrops xiangchengensis", + "Lachesis (genus)": "Lachesis (genus)", + "Large-eyed pitviper": "Trimeresurus macrops", + "Large-scaled tree viper": "Trimeresurus macrolepis", + "Leaf-nosed viper": "Eristicophis", + "Leaf viper": "Atheris squamigera", + "Levant viper": "Macrovipera lebetina", + "Long-nosed viper": "Vipera ammodytes", + "Macklot's python": "Liasis mackloti", + "Madagascar tree boa": "Sanzinia", + "Malabar rock pitviper": "Trimeresurus malabaricus", + "Malcolm's tree viper": "Trimeresurus sumatranus malcolmi", + "Mandalay cobra": "Mandalay spitting cobra", + "Mangrove pit viper": "Trimeresurus purpureomaculatus", + "Mangshan pitviper": "Trimeresurus mangshanensis", + "McMahon's viper": "Eristicophis", + "Mexican palm-pitviper": "Bothriechis rowleyi", + "Monocled cobra": "Monocled cobra", + "Motuo bamboo pitviper": "Trimeresurus medoensis", + "Mozambique spitting cobra": "Mozambique spitting cobra", + "Namaqua dwarf adder": "Bitis schneideri", + "Namib dwarf sand adder": "Bitis peringueyi", + "New Guinea carpet python": "Morelia spilota variegata", + "Nicobar bamboo pitviper": "Trimeresurus labialis", + "Nitsche's bush viper": "Atheris nitschei", + "Nitsche's tree viper": "Atheris nitschei", + "Northwestern carpet python": "Morelia spilota variegata", + "Nubian spitting cobra": "Nubian spitting cobra", + "Oenpelli python": "Oenpelli python", + "Olive python": "Liasis olivaceus", + "Pallas' viper": "Gloydius halys", + "Palm viper": "Bothriechis lateralis", + "Papuan python": "Apodora", + "Peringuey's adder": "Bitis peringueyi", + "Philippine cobra": "Philippine cobra", + "Philippine pitviper": "Trimeresurus flavomaculatus", + "Pope's tree viper": "Trimeresurus popeorum", + "Portuguese viper": "Vipera seoanei", + "Puerto Rican boa": "Puerto Rican boa", + "Rainbow boa": "Rainbow boa", + "Red spitting cobra": "Red spitting cobra", + "Rhinoceros viper": "Bitis nasicornis", + "Rhombic night adder": "Causus maculatus", + "Rinkhals": "Rinkhals", + "Rinkhals cobra": "Rinkhals", + "River jack": "Bitis nasicornis", + "Rough-scaled bush viper": "Atheris hispida", + "Rough-scaled python": "Rough-scaled python", + "Rough-scaled tree viper": "Atheris hispida", + "Royal python": "Ball python", + "Rungwe tree viper": "Atheris nitschei rungweensis", + "Sakishima habu": "Trimeresurus elegans", + "Savu python": "Liasis mackloti savuensis", + "Schlegel's viper": "Bothriechis schlegelii", + "Schultze's pitviper": "Trimeresurus schultzei", + "Sedge viper": "Atheris nitschei", + "Sharp-nosed viper": "Deinagkistrodon", + "Siamese palm viper": "Trimeresurus puniceus", + "Side-striped palm-pitviper": "Bothriechis lateralis", + "Snorkel viper": "Deinagkistrodon", + "Snouted cobra": "Snouted cobra", + "Sonoran sidewinder": "Crotalus cerastes cercobombus", + "Southern Indonesian spitting cobra": "Javan spitting cobra", + "Southern Philippine cobra": "Samar cobra", + "Spiny bush viper": "Atheris hispida", + "Spitting cobra": "Spitting cobra", + "Spotted python": "Spotted python", + "Sri Lankan pit viper": "Trimeresurus trigonocephalus", + "Stejneger's bamboo pitviper": "Trimeresurus stejnegeri", + "Storm water cobra": "Naja annulata", + "Sumatran tree viper": "Trimeresurus sumatranus", + "Temple viper": "Tropidolaemus wagleri", + "Tibetan bamboo pitviper": "Trimeresurus tibetanus", + "Tiger pit viper": "Trimeresurus kanburiensis", + "Timor python": "Python timoriensis", + "Tokara habu": "Trimeresurus tokarensis", + "Tree boa": "Emerald tree boa", + "Undulated pit viper": "Ophryacus undulatus", + "Ursini's viper": "Vipera ursinii", + "Wagler's pit viper": "Tropidolaemus wagleri", + "West African brown spitting cobra": "Mozambique spitting cobra", + "White-lipped tree viper": "Trimeresurus albolabris", + "Wirot's pit viper": "Trimeresurus puniceus", + "Yellow-lined palm viper": "Bothriechis lateralis", + "Zebra spitting cobra": "Naja nigricincta", + "Yarara": "Bothrops jararaca", + "Wetar Island python": "Liasis macklot", + "Urutus": "Bothrops alternatus", + "Titanboa": "Titanoboa" +} diff --git a/tox.ini b/tox.ini index c9de86ca..a178dd12 100644 --- a/tox.ini +++ b/tox.ini @@ -3,3 +3,4 @@ max-line-length=120 application_import_names=bot exclude=.venv ignore=B311,W503,E226 +import-order-style=pep8 diff --git a/zen.mp3 b/zen.mp3 new file mode 100644 index 00000000..4c10f691 Binary files /dev/null and b/zen.mp3 differ