From 0d51688a87739e57d681727db807bab02766eb55 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Fri, 3 Sep 2021 11:18:15 -0400 Subject: Start from upstream main branch --- bot/exts/evergreen/color.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 bot/exts/evergreen/color.py diff --git a/bot/exts/evergreen/color.py b/bot/exts/evergreen/color.py new file mode 100644 index 00000000..5d86f002 --- /dev/null +++ b/bot/exts/evergreen/color.py @@ -0,0 +1 @@ +# initial creation -- cgit v1.2.3 From 15d545ec8f3c81fa5e06c3fefda7593b03521628 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Fri, 3 Sep 2021 14:35:26 -0400 Subject: Create draft body of file This is a large empty file with lots of comments. My general proposal is outlined in the code stumps. Details will need to be hashed out and decided on with CyberCitizen01. In particular: - How to use URLs that has list of color names? Read those into a dictionary? - How to handle the command call with options? `discord-flags`, parsing, function call like: .colour cmyk(49, 50, 0, 22) .colour hsl(241, 47, 58) .colour rgb 101 99 199 - How to implement fuzzy matching with rapidfuzz based on the color names from those URLs? - How to generate colors in other formats? Is this all possible in pillow? - How to generate photo to use in the embed? Do we temporarily create a file in a cache, send it in embed, then delete? This will be a fun project, and my first collab! Co-authored-by: Mohammad Rafivulla <77384412+CyberCitizen01@users.noreply.github.com> --- bot/exts/evergreen/color.py | 113 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) diff --git a/bot/exts/evergreen/color.py b/bot/exts/evergreen/color.py index 5d86f002..32a25b0d 100644 --- a/bot/exts/evergreen/color.py +++ b/bot/exts/evergreen/color.py @@ -1 +1,112 @@ -# initial creation +# imports +import logging + +import pillow +from discord import Embed +# ! need to install discord-flags and add to poetry.lock file +from discord.ext import commands, flags +from rapidfuzz import process + +from bot.bot import Bot +from bot.constants import Colours + +logger = logging.getLogger(__name__) + +# constants if needed +# TODO Will the color conversions be done only from pillow or will an API / URL be needed? +# Color URLs +COLOR_URL_XKCD = "https://xkcd.com/color/rgb/" +COLOR_URL_NAME_THAT_COLOR = "https://github.com/ryanzec/name-that-color/blob/master/lib/ntc.js#L116-L1681" + + +COLOR_ERROR = Embed( + title="Input color is not possible", + description="The color code {user_color} is not a possible color combination." + "\nThe range of possible values are: " + "\nRGB & HSV: 0-255" + "\nCMYK: 0-100%" + "\nHSL: 0-360 degrees" + "\nHex: #000000-#FFFFFF" +) +COLOR_EMBED = Embed( + title="{color_name}", + description="RGB" + "\n{RGB}" + "\nHSV" + "\n{HSV}" + "\nCMYK" + "\n{CMYK}" + "\nHSL" + "\n{HSL}" + "\nHex" + "\n{Hex}" +) + + +# define color command +class Color(commands.cog): + """User initiated command to receive color information.""" + + def __init__(self, bot: Bot): + self.bot = bot + + # ? possible to use discord-flags to allow user to decide on color + # https://pypi.org/project/discord-flags/ + # @flags.add_flag("--rgb", type=str) + # @flags.add_flag("--hsv", type=str) + # @flags.add_flag("--cmyk", type=str) + # @flags.add_flag("--hsl", type=str) + # @flags.add_flag("--hex", type=str) + # @flags.add_flag("--name", type=str) + # @flags.command() + @commands.command(aliases=["color", "colour"]) + @commands.cooldown(1, 10, commands.cooldowns.BucketType.user) + async def color(self, ctx: commands.Context, *, user_color: str) -> None: + """Send information on input color code or color name.""" + # need to check if user_color is RGB, HSV, CMYK, HSL, Hex or color name + # should we assume the color is RGB if not defined? + # should discord tags be used? + # need to review discord.py V2.0 + + # TODO code to check if color code is possible + await ctx.send(embed=COLOR_ERROR.format(color=user_color)) + # await ctx.send(embed=COLOR_EMBED.format( + # RGB=color_dict["RGB"], + # HSV=color_dict["HSV"], + # HSL=color_dict["HSL"], + # CMYK=color_dict["CMYK"], + # HSL=color_dict["HSL"], + # Hex=color_dict["Hex"], + # color_name=color_dict["color_name"] + # ) + # ) + + # TODO pass for now + pass + + # if user_color in color_lists: + # # TODO fuzzy match for color + # pass + + async def color_converter(self, color: str, code_type: str) -> dict: + """Generate alternative color codes for use in the embed.""" + # TODO add code to take color and code type and return other types + # color_dict = { + # "RGB": color_RGB, + # "HSV": color_HSV, + # "HSL": color_HSL, + # "CMYK": color_CMYK, + # "HSL": color_HSL, + # "Hex": color_Hex, + # "color_name": color_name, + # } + pass + + async def photo_generator(self, color: str) -> None: + """Generate photo to use in embed.""" + # TODO need to find a way to store photo in cache to add to embed, then remove + + +def setup(bot: Bot) -> None: + """Load the Color Cog.""" + bot.add_cog(Color(bot)) -- cgit v1.2.3 From 969ab351b4906593505feaa43229d8bc3c95933c Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Fri, 3 Sep 2021 16:15:34 -0400 Subject: Add colorsys import, verbage for using JSON Co-authored-by: Mohammad Rafivulla <77384412+CyberCitizen01@users.noreply.github.com> --- bot/exts/evergreen/color.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bot/exts/evergreen/color.py b/bot/exts/evergreen/color.py index 32a25b0d..a00a956b 100644 --- a/bot/exts/evergreen/color.py +++ b/bot/exts/evergreen/color.py @@ -1,6 +1,7 @@ # imports import logging +import colorsys import pillow from discord import Embed # ! need to install discord-flags and add to poetry.lock file @@ -13,8 +14,8 @@ from bot.constants import Colours logger = logging.getLogger(__name__) # constants if needed -# TODO Will the color conversions be done only from pillow or will an API / URL be needed? -# Color URLs +# Color URLs - will be replaced by JSON file? +COLOR_JSON_PATH = ".bot//exts//resources//evergreen//" COLOR_URL_XKCD = "https://xkcd.com/color/rgb/" COLOR_URL_NAME_THAT_COLOR = "https://github.com/ryanzec/name-that-color/blob/master/lib/ntc.js#L116-L1681" @@ -78,7 +79,7 @@ class Color(commands.cog): # HSL=color_dict["HSL"], # Hex=color_dict["Hex"], # color_name=color_dict["color_name"] - # ) + # ).set_image() # url for image? # ) # TODO pass for now -- cgit v1.2.3 From b66e61394cd6168c7e74a91c134ceeb669e444f8 Mon Sep 17 00:00:00 2001 From: CyberCitizen01 Date: Sat, 4 Sep 2021 01:47:58 +0530 Subject: Added ryanzec_colours.json constructed from ryanzec/name-that-color Original source: https://github.com/ryanzec/name-that-color/blob/master/lib/ntc.js#L116-L1681 --- bot/resources/evergreen/ryanzec_colours.json | 1568 ++++++++++++++++++++++++++ 1 file changed, 1568 insertions(+) create mode 100644 bot/resources/evergreen/ryanzec_colours.json diff --git a/bot/resources/evergreen/ryanzec_colours.json b/bot/resources/evergreen/ryanzec_colours.json new file mode 100644 index 00000000..63d9be44 --- /dev/null +++ b/bot/resources/evergreen/ryanzec_colours.json @@ -0,0 +1,1568 @@ +{ + "Abbey": "4C4F56", + "Acadia": "1B1404", + "Acapulco": "7CB0A1", + "Aero Blue": "C9FFE5", + "Affair": "714693", + "Akaroa": "D4C4A8", + "Alabaster": "FAFAFA", + "Albescent White": "F5E9D3", + "Algae Green": "93DFB8", + "Alice Blue": "F0F8FF", + "Alizarin Crimson": "E32636", + "Allports": "0076A3", + "Almond": "EED9C4", + "Almond Frost": "907B71", + "Alpine": "AF8F2C", + "Alto": "DBDBDB", + "Aluminium": "A9ACB6", + "Amaranth": "E52B50", + "Amazon": "3B7A57", + "Amber": "FFBF00", + "Americano": "87756E", + "Amethyst": "9966CC", + "Amethyst Smoke": "A397B4", + "Amour": "F9EAF3", + "Amulet": "7B9F80", + "Anakiwa": "9DE5FF", + "Antique Brass": "C88A65", + "Antique Bronze": "704A07", + "Anzac": "E0B646", + "Apache": "DFBE6F", + "Apple": "4FA83D", + "Apple Blossom": "AF4D43", + "Apple Green": "E2F3EC", + "Apricot": "EB9373", + "Apricot Peach": "FBCEB1", + "Apricot White": "FFFEEC", + "Aqua Deep": "014B43", + "Aqua Forest": "5FA777", + "Aqua Haze": "EDF5F5", + "Aqua Island": "A1DAD7", + "Aqua Spring": "EAF9F5", + "Aqua Squeeze": "E8F5F2", + "Aquamarine": "7FFFD4", + "Aquamarine Blue": "71D9E2", + "Arapawa": "110C6C", + "Armadillo": "433E37", + "Arrowtown": "948771", + "Ash": "C6C3B5", + "Asparagus": "7BA05B", + "Asphalt": "130A06", + "Astra": "FAEAB9", + "Astral": "327DA0", + "Astronaut": "283A77", + "Astronaut Blue": "013E62", + "Athens Gray": "EEF0F3", + "Aths Special": "ECEBCE", + "Atlantis": "97CD2D", + "Atoll": "0A6F75", + "Atomic Tangerine": "FF9966", + "Au Chico": "97605D", + "Aubergine": "3B0910", + "Australian Mint": "F5FFBE", + "Avocado": "888D65", + "Axolotl": "4E6649", + "Azalea": "F7C8DA", + "Aztec": "0D1C19", + "Azure": "315BA1", + "Azure Radiance": "007FFF", + "Baby Blue": "E0FFFF", + "Bahama Blue": "026395", + "Bahia": "A5CB0C", + "Baja White": "FFF8D1", + "Bali Hai": "859FAF", + "Baltic Sea": "2A2630", + "Bamboo": "DA6304", + "Banana Mania": "FBE7B2", + "Bandicoot": "858470", + "Barberry": "DED717", + "Barley Corn": "A68B5B", + "Barley White": "FFF4CE", + "Barossa": "44012D", + "Bastille": "292130", + "Battleship Gray": "828F72", + "Bay Leaf": "7DA98D", + "Bay of Many": "273A81", + "Bazaar": "98777B", + "Bean ": "3D0C02", + "Beauty Bush": "EEC1BE", + "Beaver": "926F5B", + "Beeswax": "FEF2C7", + "Beige": "F5F5DC", + "Bermuda": "7DD8C6", + "Bermuda Gray": "6B8BA2", + "Beryl Green": "DEE5C0", + "Bianca": "FCFBF3", + "Big Stone": "162A40", + "Bilbao": "327C14", + "Biloba Flower": "B2A1EA", + "Birch": "373021", + "Bird Flower": "D4CD16", + "Biscay": "1B3162", + "Bismark": "497183", + "Bison Hide": "C1B7A4", + "Bistre": "3D2B1F", + "Bitter": "868974", + "Bitter Lemon": "CAE00D", + "Bittersweet": "FE6F5E", + "Bizarre": "EEDEDA", + "Black": "000000", + "Black Bean": "081910", + "Black Forest": "0B1304", + "Black Haze": "F6F7F7", + "Black Marlin": "3E2C1C", + "Black Olive": "242E16", + "Black Pearl": "041322", + "Black Rock": "0D0332", + "Black Rose": "67032D", + "Black Russian": "0A001C", + "Black Squeeze": "F2FAFA", + "Black White": "FFFEF6", + "Blackberry": "4D0135", + "Blackcurrant": "32293A", + "Blaze Orange": "FF6600", + "Bleach White": "FEF3D8", + "Bleached Cedar": "2C2133", + "Blizzard Blue": "A3E3ED", + "Blossom": "DCB4BC", + "Blue": "0000FF", + "Blue Bayoux": "496679", + "Blue Bell": "9999CC", + "Blue Chalk": "F1E9FF", + "Blue Charcoal": "010D1A", + "Blue Chill": "0C8990", + "Blue Diamond": "380474", + "Blue Dianne": "204852", + "Blue Gem": "2C0E8C", + "Blue Haze": "BFBED8", + "Blue Lagoon": "017987", + "Blue Marguerite": "7666C6", + "Blue Ribbon": "0066FF", + "Blue Romance": "D2F6DE", + "Blue Smoke": "748881", + "Blue Stone": "016162", + "Blue Violet": "6456B7", + "Blue Whale": "042E4C", + "Blue Zodiac": "13264D", + "Blumine": "18587A", + "Blush": "B44668", + "Blush Pink": "FF6FFF", + "Bombay": "AFB1B8", + "Bon Jour": "E5E0E1", + "Bondi Blue": "0095B6", + "Bone": "E4D1C0", + "Bordeaux": "5C0120", + "Bossanova": "4E2A5A", + "Boston Blue": "3B91B4", + "Botticelli": "C7DDE5", + "Bottle Green": "093624", + "Boulder": "7A7A7A", + "Bouquet": "AE809E", + "Bourbon": "BA6F1E", + "Bracken": "4A2A04", + "Brandy": "DEC196", + "Brandy Punch": "CD8429", + "Brandy Rose": "BB8983", + "Breaker Bay": "5DA19F", + "Brick Red": "C62D42", + "Bridal Heath": "FFFAF4", + "Bridesmaid": "FEF0EC", + "Bright Gray": "3C4151", + "Bright Green": "66FF00", + "Bright Red": "B10000", + "Bright Sun": "FED33C", + "Bright Turquoise": "08E8DE", + "Brilliant Rose": "F653A6", + "Brink Pink": "FB607F", + "Bronco": "ABA196", + "Bronze": "3F2109", + "Bronze Olive": "4E420C", + "Bronzetone": "4D400F", + "Broom": "FFEC13", + "Brown": "964B00", + "Brown Bramble": "592804", + "Brown Derby": "492615", + "Brown Pod": "401801", + "Brown Rust": "AF593E", + "Brown Tumbleweed": "37290E", + "Bubbles": "E7FEFF", + "Buccaneer": "622F30", + "Bud": "A8AE9C", + "Buddha Gold": "C1A004", + "Buff": "F0DC82", + "Bulgarian Rose": "480607", + "Bull Shot": "864D1E", + "Bunker": "0D1117", + "Bunting": "151F4C", + "Burgundy": "900020", + "Burnham": "002E20", + "Burning Orange": "FF7034", + "Burning Sand": "D99376", + "Burnt Maroon": "420303", + "Burnt Orange": "CC5500", + "Burnt Sienna": "E97451", + "Burnt Umber": "8A3324", + "Bush": "0D2E1C", + "Buttercup": "F3AD16", + "Buttered Rum": "A1750D", + "Butterfly Bush": "624E9A", + "Buttermilk": "FFF1B5", + "Buttery White": "FFFCEA", + "Cab Sav": "4D0A18", + "Cabaret": "D94972", + "Cabbage Pont": "3F4C3A", + "Cactus": "587156", + "Cadet Blue": "A9B2C3", + "Cadillac": "B04C6A", + "Cafe Royale": "6F440C", + "Calico": "E0C095", + "California": "FE9D04", + "Calypso": "31728D", + "Camarone": "00581A", + "Camelot": "893456", + "Cameo": "D9B99B", + "Camouflage": "3C3910", + "Camouflage Green": "78866B", + "Can Can": "D591A4", + "Canary": "F3FB62", + "Candlelight": "FCD917", + "Candy Corn": "FBEC5D", + "Cannon Black": "251706", + "Cannon Pink": "894367", + "Cape Cod": "3C4443", + "Cape Honey": "FEE5AC", + "Cape Palliser": "A26645", + "Caper": "DCEDB4", + "Caramel": "FFDDAF", + "Cararra": "EEEEE8", + "Cardin Green": "01361C", + "Cardinal": "C41E3A", + "Cardinal Pink": "8C055E", + "Careys Pink": "D29EAA", + "Caribbean Green": "00CC99", + "Carissma": "EA88A8", + "Carla": "F3FFD8", + "Carmine": "960018", + "Carnaby Tan": "5C2E01", + "Carnation": "F95A61", + "Carnation Pink": "FFA6C9", + "Carousel Pink": "F9E0ED", + "Carrot Orange": "ED9121", + "Casablanca": "F8B853", + "Casal": "2F6168", + "Cascade": "8BA9A5", + "Cashmere": "E6BEA5", + "Casper": "ADBED1", + "Castro": "52001F", + "Catalina Blue": "062A78", + "Catskill White": "EEF6F7", + "Cavern Pink": "E3BEBE", + "Cedar": "3E1C14", + "Cedar Wood Finish": "711A00", + "Celadon": "ACE1AF", + "Celery": "B8C25D", + "Celeste": "D1D2CA", + "Cello": "1E385B", + "Celtic": "163222", + "Cement": "8D7662", + "Ceramic": "FCFFF9", + "Cerise": "DA3287", + "Cerise Red": "DE3163", + "Cerulean": "02A4D3", + "Cerulean Blue": "2A52BE", + "Chablis": "FFF4F3", + "Chalet Green": "516E3D", + "Chalky": "EED794", + "Chambray": "354E8C", + "Chamois": "EDDCB1", + "Champagne": "FAECCC", + "Chantilly": "F8C3DF", + "Charade": "292937", + "Chardon": "FFF3F1", + "Chardonnay": "FFCD8C", + "Charlotte": "BAEEF9", + "Charm": "D47494", + "Chartreuse": "7FFF00", + "Chartreuse Yellow": "DFFF00", + "Chateau Green": "40A860", + "Chatelle": "BDB3C7", + "Chathams Blue": "175579", + "Chelsea Cucumber": "83AA5D", + "Chelsea Gem": "9E5302", + "Chenin": "DFCD6F", + "Cherokee": "FCDA98", + "Cherry Pie": "2A0359", + "Cherrywood": "651A14", + "Cherub": "F8D9E9", + "Chestnut": "B94E48", + "Chestnut Rose": "CD5C5C", + "Chetwode Blue": "8581D9", + "Chicago": "5D5C58", + "Chiffon": "F1FFC8", + "Chilean Fire": "F77703", + "Chilean Heath": "FFFDE6", + "China Ivory": "FCFFE7", + "Chino": "CEC7A7", + "Chinook": "A8E3BD", + "Chocolate": "370202", + "Christalle": "33036B", + "Christi": "67A712", + "Christine": "E7730A", + "Chrome White": "E8F1D4", + "Cinder": "0E0E18", + "Cinderella": "FDE1DC", + "Cinnabar": "E34234", + "Cinnamon": "7B3F00", + "Cioccolato": "55280C", + "Citrine White": "FAF7D6", + "Citron": "9EA91F", + "Citrus": "A1C50A", + "Clairvoyant": "480656", + "Clam Shell": "D4B6AF", + "Claret": "7F1734", + "Classic Rose": "FBCCE7", + "Clay Ash": "BDC8B3", + "Clay Creek": "8A8360", + "Clear Day": "E9FFFD", + "Clementine": "E96E00", + "Clinker": "371D09", + "Cloud": "C7C4BF", + "Cloud Burst": "202E54", + "Cloudy": "ACA59F", + "Clover": "384910", + "Cobalt": "0047AB", + "Cocoa Bean": "481C1C", + "Cocoa Brown": "301F1E", + "Coconut Cream": "F8F7DC", + "Cod Gray": "0B0B0B", + "Coffee": "706555", + "Coffee Bean": "2A140E", + "Cognac": "9F381D", + "Cola": "3F2500", + "Cold Purple": "ABA0D9", + "Cold Turkey": "CEBABA", + "Colonial White": "FFEDBC", + "Comet": "5C5D75", + "Como": "517C66", + "Conch": "C9D9D2", + "Concord": "7C7B7A", + "Concrete": "F2F2F2", + "Confetti": "E9D75A", + "Congo Brown": "593737", + "Congress Blue": "02478E", + "Conifer": "ACDD4D", + "Contessa": "C6726B", + "Copper": "B87333", + "Copper Canyon": "7E3A15", + "Copper Rose": "996666", + "Copper Rust": "944747", + "Copperfield": "DA8A67", + "Coral": "FF7F50", + "Coral Red": "FF4040", + "Coral Reef": "C7BCA2", + "Coral Tree": "A86B6B", + "Corduroy": "606E68", + "Coriander": "C4D0B0", + "Cork": "40291D", + "Corn": "E7BF05", + "Corn Field": "F8FACD", + "Corn Harvest": "8B6B0B", + "Cornflower": "93CCEA", + "Cornflower Blue": "6495ED", + "Cornflower Lilac": "FFB0AC", + "Corvette": "FAD3A2", + "Cosmic": "76395D", + "Cosmos": "FFD8D9", + "Costa Del Sol": "615D30", + "Cotton Candy": "FFB7D5", + "Cotton Seed": "C2BDB6", + "County Green": "01371A", + "Cowboy": "4D282D", + "Crail": "B95140", + "Cranberry": "DB5079", + "Crater Brown": "462425", + "Cream": "FFFDD0", + "Cream Brulee": "FFE5A0", + "Cream Can": "F5C85C", + "Creole": "1E0F04", + "Crete": "737829", + "Crimson": "DC143C", + "Crocodile": "736D58", + "Crown of Thorns": "771F1F", + "Crowshead": "1C1208", + "Cruise": "B5ECDF", + "Crusoe": "004816", + "Crusta": "FD7B33", + "Cumin": "924321", + "Cumulus": "FDFFD5", + "Cupid": "FBBEDA", + "Curious Blue": "2596D1", + "Cutty Sark": "507672", + "Cyan / Aqua": "00FFFF", + "Cyprus": "003E40", + "Daintree": "012731", + "Dairy Cream": "F9E4BC", + "Daisy Bush": "4F2398", + "Dallas": "6E4B26", + "Dandelion": "FED85D", + "Danube": "6093D1", + "Dark Blue": "0000C8", + "Dark Burgundy": "770F05", + "Dark Ebony": "3C2005", + "Dark Fern": "0A480D", + "Dark Tan": "661010", + "Dawn": "A6A29A", + "Dawn Pink": "F3E9E5", + "De York": "7AC488", + "Deco": "D2DA97", + "Deep Blue": "220878", + "Deep Blush": "E47698", + "Deep Bronze": "4A3004", + "Deep Cerulean": "007BA7", + "Deep Cove": "051040", + "Deep Fir": "002900", + "Deep Forest Green": "182D09", + "Deep Koamaru": "1B127B", + "Deep Oak": "412010", + "Deep Sapphire": "082567", + "Deep Sea": "01826B", + "Deep Sea Green": "095859", + "Deep Teal": "003532", + "Del Rio": "B09A95", + "Dell": "396413", + "Delta": "A4A49D", + "Deluge": "7563A8", + "Denim": "1560BD", + "Derby": "FFEED8", + "Desert": "AE6020", + "Desert Sand": "EDC9AF", + "Desert Storm": "F8F8F7", + "Dew": "EAFFFE", + "Di Serria": "DB995E", + "Diesel": "130000", + "Dingley": "5D7747", + "Disco": "871550", + "Dixie": "E29418", + "Dodger Blue": "1E90FF", + "Dolly": "F9FF8B", + "Dolphin": "646077", + "Domino": "8E775E", + "Don Juan": "5D4C51", + "Donkey Brown": "A69279", + "Dorado": "6B5755", + "Double Colonial White": "EEE3AD", + "Double Pearl Lusta": "FCF4D0", + "Double Spanish White": "E6D7B9", + "Dove Gray": "6D6C6C", + "Downriver": "092256", + "Downy": "6FD0C5", + "Driftwood": "AF8751", + "Drover": "FDF7AD", + "Dull Lavender": "A899E6", + "Dune": "383533", + "Dust Storm": "E5CCC9", + "Dusty Gray": "A8989B", + "Eagle": "B6BAA4", + "Earls Green": "C9B93B", + "Early Dawn": "FFF9E6", + "East Bay": "414C7D", + "East Side": "AC91CE", + "Eastern Blue": "1E9AB0", + "Ebb": "E9E3E3", + "Ebony": "0C0B1D", + "Ebony Clay": "26283B", + "Eclipse": "311C17", + "Ecru White": "F5F3E5", + "Ecstasy": "FA7814", + "Eden": "105852", + "Edgewater": "C8E3D7", + "Edward": "A2AEAB", + "Egg Sour": "FFF4DD", + "Egg White": "FFEFC1", + "Eggplant": "614051", + "El Paso": "1E1708", + "El Salva": "8F3E33", + "Electric Lime": "CCFF00", + "Electric Violet": "8B00FF", + "Elephant": "123447", + "Elf Green": "088370", + "Elm": "1C7C7D", + "Emerald": "50C878", + "Eminence": "6C3082", + "Emperor": "514649", + "Empress": "817377", + "Endeavour": "0056A7", + "Energy Yellow": "F8DD5C", + "English Holly": "022D15", + "English Walnut": "3E2B23", + "Envy": "8BA690", + "Equator": "E1BC64", + "Espresso": "612718", + "Eternity": "211A0E", + "Eucalyptus": "278A5B", + "Eunry": "CFA39D", + "Evening Sea": "024E46", + "Everglade": "1C402E", + "Faded Jade": "427977", + "Fair Pink": "FFEFEC", + "Falcon": "7F626D", + "Fall Green": "ECEBBD", + "Falu Red": "801818", + "Fantasy": "FAF3F0", + "Fedora": "796A78", + "Feijoa": "9FDD8C", + "Fern": "63B76C", + "Fern Frond": "657220", + "Fern Green": "4F7942", + "Ferra": "704F50", + "Festival": "FBE96C", + "Feta": "F0FCEA", + "Fiery Orange": "B35213", + "Finch": "626649", + "Finlandia": "556D56", + "Finn": "692D54", + "Fiord": "405169", + "Fire": "AA4203", + "Fire Bush": "E89928", + "Firefly": "0E2A30", + "Flame Pea": "DA5B38", + "Flamenco": "FF7D07", + "Flamingo": "F2552A", + "Flax": "EEDC82", + "Flax Smoke": "7B8265", + "Flesh": "FFCBA4", + "Flint": "6F6A61", + "Flirt": "A2006D", + "Flush Mahogany": "CA3435", + "Flush Orange": "FF7F00", + "Foam": "D8FCFA", + "Fog": "D7D0FF", + "Foggy Gray": "CBCAB6", + "Forest Green": "228B22", + "Forget Me Not": "FFF1EE", + "Fountain Blue": "56B4BE", + "Frangipani": "FFDEB3", + "French Gray": "BDBDC6", + "French Lilac": "ECC7EE", + "French Pass": "BDEDFD", + "French Rose": "F64A8A", + "Fresh Eggplant": "990066", + "Friar Gray": "807E79", + "Fringy Flower": "B1E2C1", + "Froly": "F57584", + "Frost": "EDF5DD", + "Frosted Mint": "DBFFF8", + "Frostee": "E4F6E7", + "Fruit Salad": "4F9D5D", + "Fuchsia Blue": "7A58C1", + "Fuchsia Pink": "C154C1", + "Fuego": "BEDE0D", + "Fuel Yellow": "ECA927", + "Fun Blue": "1959A8", + "Fun Green": "016D39", + "Fuscous Gray": "54534D", + "Fuzzy Wuzzy Brown": "C45655", + "Gable Green": "163531", + "Gallery": "EFEFEF", + "Galliano": "DCB20C", + "Gamboge": "E49B0F", + "Geebung": "D18F1B", + "Genoa": "15736B", + "Geraldine": "FB8989", + "Geyser": "D4DFE2", + "Ghost": "C7C9D5", + "Gigas": "523C94", + "Gimblet": "B8B56A", + "Gin": "E8F2EB", + "Gin Fizz": "FFF9E2", + "Givry": "F8E4BF", + "Glacier": "80B3C4", + "Glade Green": "61845F", + "Go Ben": "726D4E", + "Goblin": "3D7D52", + "Gold": "FFD700", + "Gold Drop": "F18200", + "Gold Sand": "E6BE8A", + "Gold Tips": "DEBA13", + "Golden Bell": "E28913", + "Golden Dream": "F0D52D", + "Golden Fizz": "F5FB3D", + "Golden Glow": "FDE295", + "Golden Grass": "DAA520", + "Golden Sand": "F0DB7D", + "Golden Tainoi": "FFCC5C", + "Goldenrod": "FCD667", + "Gondola": "261414", + "Gordons Green": "0B1107", + "Gorse": "FFF14F", + "Gossamer": "069B81", + "Gossip": "D2F8B0", + "Gothic": "6D92A1", + "Governor Bay": "2F3CB3", + "Grain Brown": "E4D5B7", + "Grandis": "FFD38C", + "Granite Green": "8D8974", + "Granny Apple": "D5F6E3", + "Granny Smith": "84A0A0", + "Granny Smith Apple": "9DE093", + "Grape": "381A51", + "Graphite": "251607", + "Gravel": "4A444B", + "Gray": "808080", + "Gray Asparagus": "465945", + "Gray Chateau": "A2AAB3", + "Gray Nickel": "C3C3BD", + "Gray Nurse": "E7ECE6", + "Gray Olive": "A9A491", + "Gray Suit": "C1BECD", + "Green": "00FF00", + "Green Haze": "01A368", + "Green House": "24500F", + "Green Kelp": "25311C", + "Green Leaf": "436A0D", + "Green Mist": "CBD3B0", + "Green Pea": "1D6142", + "Green Smoke": "A4AF6E", + "Green Spring": "B8C1B1", + "Green Vogue": "032B52", + "Green Waterloo": "101405", + "Green White": "E8EBE0", + "Green Yellow": "ADFF2F", + "Grenadier": "D54600", + "Guardsman Red": "BA0101", + "Gulf Blue": "051657", + "Gulf Stream": "80B3AE", + "Gull Gray": "9DACB7", + "Gum Leaf": "B6D3BF", + "Gumbo": "7CA1A6", + "Gun Powder": "414257", + "Gunsmoke": "828685", + "Gurkha": "9A9577", + "Hacienda": "98811B", + "Hairy Heath": "6B2A14", + "Haiti": "1B1035", + "Half Baked": "85C4CC", + "Half Colonial White": "FDF6D3", + "Half Dutch White": "FEF7DE", + "Half Spanish White": "FEF4DB", + "Half and Half": "FFFEE1", + "Hampton": "E5D8AF", + "Harlequin": "3FFF00", + "Harp": "E6F2EA", + "Harvest Gold": "E0B974", + "Havelock Blue": "5590D9", + "Hawaiian Tan": "9D5616", + "Hawkes Blue": "D4E2FC", + "Heath": "541012", + "Heather": "B7C3D0", + "Heathered Gray": "B6B095", + "Heavy Metal": "2B3228", + "Heliotrope": "DF73FF", + "Hemlock": "5E5D3B", + "Hemp": "907874", + "Hibiscus": "B6316C", + "Highland": "6F8E63", + "Hillary": "ACA586", + "Himalaya": "6A5D1B", + "Hint of Green": "E6FFE9", + "Hint of Red": "FBF9F9", + "Hint of Yellow": "FAFDE4", + "Hippie Blue": "589AAF", + "Hippie Green": "53824B", + "Hippie Pink": "AE4560", + "Hit Gray": "A1ADB5", + "Hit Pink": "FFAB81", + "Hokey Pokey": "C8A528", + "Hoki": "65869F", + "Holly": "011D13", + "Hollywood Cerise": "F400A1", + "Honey Flower": "4F1C70", + "Honeysuckle": "EDFC84", + "Hopbush": "D06DA1", + "Horizon": "5A87A0", + "Horses Neck": "604913", + "Hot Cinnamon": "D2691E", + "Hot Pink": "FF69B4", + "Hot Toddy": "B38007", + "Humming Bird": "CFF9F3", + "Hunter Green": "161D10", + "Hurricane": "877C7B", + "Husk": "B7A458", + "Ice Cold": "B1F4E7", + "Iceberg": "DAF4F0", + "Illusion": "F6A4C9", + "Inch Worm": "B0E313", + "Indian Khaki": "C3B091", + "Indian Tan": "4D1E01", + "Indigo": "4F69C6", + "Indochine": "C26B03", + "International Klein Blue": "002FA7", + "International Orange": "FF4F00", + "Irish Coffee": "5F3D26", + "Iroko": "433120", + "Iron": "D4D7D9", + "Ironside Gray": "676662", + "Ironstone": "86483C", + "Island Spice": "FFFCEE", + "Ivory": "FFFFF0", + "Jacaranda": "2E0329", + "Jacarta": "3A2A6A", + "Jacko Bean": "2E1905", + "Jacksons Purple": "20208D", + "Jade": "00A86B", + "Jaffa": "EF863F", + "Jagged Ice": "C2E8E5", + "Jagger": "350E57", + "Jaguar": "080110", + "Jambalaya": "5B3013", + "Janna": "F4EBD3", + "Japanese Laurel": "0A6906", + "Japanese Maple": "780109", + "Japonica": "D87C63", + "Java": "1FC2C2", + "Jazzberry Jam": "A50B5E", + "Jelly Bean": "297B9A", + "Jet Stream": "B5D2CE", + "Jewel": "126B40", + "Jon": "3B1F1F", + "Jonquil": "EEFF9A", + "Jordy Blue": "8AB9F1", + "Judge Gray": "544333", + "Jumbo": "7C7B82", + "Jungle Green": "29AB87", + "Jungle Mist": "B4CFD3", + "Juniper": "6D9292", + "Just Right": "ECCDB9", + "Kabul": "5E483E", + "Kaitoke Green": "004620", + "Kangaroo": "C6C8BD", + "Karaka": "1E1609", + "Karry": "FFEAD4", + "Kashmir Blue": "507096", + "Kelp": "454936", + "Kenyan Copper": "7C1C05", + "Keppel": "3AB09E", + "Key Lime Pie": "BFC921", + "Khaki": "F0E68C", + "Kidnapper": "E1EAD4", + "Kilamanjaro": "240C02", + "Killarney": "3A6A47", + "Kimberly": "736C9F", + "Kingfisher Daisy": "3E0480", + "Kobi": "E79FC4", + "Kokoda": "6E6D57", + "Korma": "8F4B0E", + "Koromiko": "FFBD5F", + "Kournikova": "FFE772", + "Kumera": "886221", + "La Palma": "368716", + "La Rioja": "B3C110", + "Las Palmas": "C6E610", + "Laser": "C8B568", + "Laser Lemon": "FFFF66", + "Laurel": "749378", + "Lavender": "B57EDC", + "Lavender Gray": "BDBBD7", + "Lavender Magenta": "EE82EE", + "Lavender Pink": "FBAED2", + "Lavender Purple": "967BB6", + "Lavender Rose": "FBA0E3", + "Lavender blush": "FFF0F5", + "Leather": "967059", + "Lemon": "FDE910", + "Lemon Chiffon": "FFFACD", + "Lemon Ginger": "AC9E22", + "Lemon Grass": "9B9E8F", + "Light Apricot": "FDD5B1", + "Light Orchid": "E29CD2", + "Light Wisteria": "C9A0DC", + "Lightning Yellow": "FCC01E", + "Lilac": "C8A2C8", + "Lilac Bush": "9874D3", + "Lily": "C8AABF", + "Lily White": "E7F8FF", + "Lima": "76BD17", + "Lime": "BFFF00", + "Limeade": "6F9D02", + "Limed Ash": "747D63", + "Limed Oak": "AC8A56", + "Limed Spruce": "394851", + "Linen": "FAF0E6", + "Link Water": "D9E4F5", + "Lipstick": "AB0563", + "Lisbon Brown": "423921", + "Livid Brown": "4D282E", + "Loafer": "EEF4DE", + "Loblolly": "BDC9CE", + "Lochinvar": "2C8C84", + "Lochmara": "007EC7", + "Locust": "A8AF8E", + "Log Cabin": "242A1D", + "Logan": "AAA9CD", + "Lola": "DFCFDB", + "London Hue": "BEA6C3", + "Lonestar": "6D0101", + "Lotus": "863C3C", + "Loulou": "460B41", + "Lucky": "AF9F1C", + "Lucky Point": "1A1A68", + "Lunar Green": "3C493A", + "Luxor Gold": "A7882C", + "Lynch": "697E9A", + "Mabel": "D9F7FF", + "Macaroni and Cheese": "FFB97B", + "Madang": "B7F0BE", + "Madison": "09255D", + "Madras": "3F3002", + "Magenta / Fuchsia": "FF00FF", + "Magic Mint": "AAF0D1", + "Magnolia": "F8F4FF", + "Mahogany": "4E0606", + "Mai Tai": "B06608", + "Maize": "F5D5A0", + "Makara": "897D6D", + "Mako": "444954", + "Malachite": "0BDA51", + "Malibu": "7DC8F7", + "Mallard": "233418", + "Malta": "BDB2A1", + "Mamba": "8E8190", + "Manatee": "8D90A1", + "Mandalay": "AD781B", + "Mandy": "E25465", + "Mandys Pink": "F2C3B2", + "Mango Tango": "E77200", + "Manhattan": "F5C999", + "Mantis": "74C365", + "Mantle": "8B9C90", + "Manz": "EEEF78", + "Mardi Gras": "350036", + "Marigold": "B98D28", + "Marigold Yellow": "FBE870", + "Mariner": "286ACD", + "Maroon": "800000", + "Maroon Flush": "C32148", + "Maroon Oak": "520C17", + "Marshland": "0B0F08", + "Martini": "AFA09E", + "Martinique": "363050", + "Marzipan": "F8DB9D", + "Masala": "403B38", + "Matisse": "1B659D", + "Matrix": "B05D54", + "Matterhorn": "4E3B41", + "Mauve": "E0B0FF", + "Mauvelous": "F091A9", + "Maverick": "D8C2D5", + "Medium Carmine": "AF4035", + "Medium Purple": "9370DB", + "Medium Red Violet": "BB3385", + "Melanie": "E4C2D5", + "Melanzane": "300529", + "Melon": "FEBAAD", + "Melrose": "C7C1FF", + "Mercury": "E5E5E5", + "Merino": "F6F0E6", + "Merlin": "413C37", + "Merlot": "831923", + "Metallic Bronze": "49371B", + "Metallic Copper": "71291D", + "Meteor": "D07D12", + "Meteorite": "3C1F76", + "Mexican Red": "A72525", + "Mid Gray": "5F5F6E", + "Midnight": "011635", + "Midnight Blue": "003366", + "Midnight Moss": "041004", + "Mikado": "2D2510", + "Milan": "FAFFA4", + "Milano Red": "B81104", + "Milk Punch": "FFF6D4", + "Millbrook": "594433", + "Mimosa": "F8FDD3", + "Mindaro": "E3F988", + "Mine Shaft": "323232", + "Mineral Green": "3F5D53", + "Ming": "36747D", + "Minsk": "3F307F", + "Mint Green": "98FF98", + "Mint Julep": "F1EEC1", + "Mint Tulip": "C4F4EB", + "Mirage": "161928", + "Mischka": "D1D2DD", + "Mist Gray": "C4C4BC", + "Mobster": "7F7589", + "Moccaccino": "6E1D14", + "Mocha": "782D19", + "Mojo": "C04737", + "Mona Lisa": "FFA194", + "Monarch": "8B0723", + "Mondo": "4A3C30", + "Mongoose": "B5A27F", + "Monsoon": "8A8389", + "Monte Carlo": "83D0C6", + "Monza": "C7031E", + "Moody Blue": "7F76D3", + "Moon Glow": "FCFEDA", + "Moon Mist": "DCDDCC", + "Moon Raker": "D6CEF6", + "Morning Glory": "9EDEE0", + "Morocco Brown": "441D00", + "Mortar": "504351", + "Mosque": "036A6E", + "Moss Green": "ADDFAD", + "Mountain Meadow": "1AB385", + "Mountain Mist": "959396", + "Mountbatten Pink": "997A8D", + "Muddy Waters": "B78E5C", + "Muesli": "AA8B5B", + "Mulberry": "C54B8C", + "Mulberry Wood": "5C0536", + "Mule Fawn": "8C472F", + "Mulled Wine": "4E4562", + "Mustard": "FFDB58", + "My Pink": "D69188", + "My Sin": "FFB31F", + "Mystic": "E2EBED", + "Nandor": "4B5D52", + "Napa": "ACA494", + "Narvik": "EDF9F1", + "Natural Gray": "8B8680", + "Navajo White": "FFDEAD", + "Navy Blue": "000080", + "Nebula": "CBDBD6", + "Negroni": "FFE2C5", + "Neon Carrot": "FF9933", + "Nepal": "8EABC1", + "Neptune": "7CB7BB", + "Nero": "140600", + "Nevada": "646E75", + "New Orleans": "F3D69D", + "New York Pink": "D7837F", + "Niagara": "06A189", + "Night Rider": "1F120F", + "Night Shadz": "AA375A", + "Nile Blue": "193751", + "Nobel": "B7B1B1", + "Nomad": "BAB1A2", + "Norway": "A8BD9F", + "Nugget": "C59922", + "Nutmeg": "81422C", + "Nutmeg Wood Finish": "683600", + "Oasis": "FEEFCE", + "Observatory": "02866F", + "Ocean Green": "41AA78", + "Ochre": "CC7722", + "Off Green": "E6F8F3", + "Off Yellow": "FEF9E3", + "Oil": "281E15", + "Old Brick": "901E1E", + "Old Copper": "724A2F", + "Old Gold": "CFB53B", + "Old Lace": "FDF5E6", + "Old Lavender": "796878", + "Old Rose": "C08081", + "Olive": "808000", + "Olive Drab": "6B8E23", + "Olive Green": "B5B35C", + "Olive Haze": "8B8470", + "Olivetone": "716E10", + "Olivine": "9AB973", + "Onahau": "CDF4FF", + "Onion": "2F270E", + "Opal": "A9C6C2", + "Opium": "8E6F70", + "Oracle": "377475", + "Orange": "FF681F", + "Orange Peel": "FFA000", + "Orange Roughy": "C45719", + "Orange White": "FEFCED", + "Orchid": "DA70D6", + "Orchid White": "FFFDF3", + "Oregon": "9B4703", + "Orient": "015E85", + "Oriental Pink": "C69191", + "Orinoco": "F3FBD4", + "Oslo Gray": "878D91", + "Ottoman": "E9F8ED", + "Outer Space": "2D383A", + "Outrageous Orange": "FF6037", + "Oxford Blue": "384555", + "Oxley": "779E86", + "Oyster Bay": "DAFAFF", + "Oyster Pink": "E9CECD", + "Paarl": "A65529", + "Pablo": "776F61", + "Pacific Blue": "009DC4", + "Pacifika": "778120", + "Paco": "411F10", + "Padua": "ADE6C4", + "Pale Canary": "FFFF99", + "Pale Leaf": "C0D3B9", + "Pale Oyster": "988D77", + "Pale Prim": "FDFEB8", + "Pale Rose": "FFE1F2", + "Pale Sky": "6E7783", + "Pale Slate": "C3BFC1", + "Palm Green": "09230F", + "Palm Leaf": "19330E", + "Pampas": "F4F2EE", + "Panache": "EAF6EE", + "Pancho": "EDCDAB", + "Papaya Whip": "FFEFD5", + "Paprika": "8D0226", + "Paradiso": "317D82", + "Parchment": "F1E9D2", + "Paris Daisy": "FFF46E", + "Paris M": "26056A", + "Paris White": "CADCD4", + "Parsley": "134F19", + "Pastel Green": "77DD77", + "Pastel Pink": "FFD1DC", + "Patina": "639A8F", + "Pattens Blue": "DEF5FF", + "Paua": "260368", + "Pavlova": "D7C498", + "Peach": "FFE5B4", + "Peach Cream": "FFF0DB", + "Peach Orange": "FFCC99", + "Peach Schnapps": "FFDCD6", + "Peach Yellow": "FADFAD", + "Peanut": "782F16", + "Pear": "D1E231", + "Pearl Bush": "E8E0D5", + "Pearl Lusta": "FCF4DC", + "Peat": "716B56", + "Pelorous": "3EABBF", + "Peppermint": "E3F5E1", + "Perano": "A9BEF2", + "Perfume": "D0BEF8", + "Periglacial Blue": "E1E6D6", + "Periwinkle": "CCCCFF", + "Periwinkle Gray": "C3CDE6", + "Persian Blue": "1C39BB", + "Persian Green": "00A693", + "Persian Indigo": "32127A", + "Persian Pink": "F77FBE", + "Persian Plum": "701C1C", + "Persian Red": "CC3333", + "Persian Rose": "FE28A2", + "Persimmon": "FF6B53", + "Peru Tan": "7F3A02", + "Pesto": "7C7631", + "Petite Orchid": "DB9690", + "Pewter": "96A8A1", + "Pharlap": "A3807B", + "Picasso": "FFF39D", + "Pickled Bean": "6E4826", + "Pickled Bluewood": "314459", + "Picton Blue": "45B1E8", + "Pig Pink": "FDD7E4", + "Pigeon Post": "AFBDD9", + "Pigment Indigo": "4B0082", + "Pine Cone": "6D5E54", + "Pine Glade": "C7CD90", + "Pine Green": "01796F", + "Pine Tree": "171F04", + "Pink": "FFC0CB", + "Pink Flamingo": "FF66FF", + "Pink Flare": "E1C0C8", + "Pink Lace": "FFDDF4", + "Pink Lady": "FFF1D8", + "Pink Salmon": "FF91A4", + "Pink Swan": "BEB5B7", + "Piper": "C96323", + "Pipi": "FEF4CC", + "Pippin": "FFE1DF", + "Pirate Gold": "BA7F03", + "Pistachio": "9DC209", + "Pixie Green": "C0D8B6", + "Pizazz": "FF9000", + "Pizza": "C99415", + "Plantation": "27504B", + "Plum": "843179", + "Pohutukawa": "8F021C", + "Polar": "E5F9F6", + "Polo Blue": "8DA8CC", + "Pomegranate": "F34723", + "Pompadour": "660045", + "Porcelain": "EFF2F3", + "Porsche": "EAAE69", + "Port Gore": "251F4F", + "Portafino": "FFFFB4", + "Portage": "8B9FEE", + "Portica": "F9E663", + "Pot Pourri": "F5E7E2", + "Potters Clay": "8C5738", + "Powder Ash": "BCC9C2", + "Powder Blue": "B0E0E6", + "Prairie Sand": "9A3820", + "Prelude": "D0C0E5", + "Prim": "F0E2EC", + "Primrose": "EDEA99", + "Provincial Pink": "FEF5F1", + "Prussian Blue": "003153", + "Puce": "CC8899", + "Pueblo": "7D2C14", + "Puerto Rico": "3FC1AA", + "Pumice": "C2CAC4", + "Pumpkin": "FF7518", + "Pumpkin Skin": "B1610B", + "Punch": "DC4333", + "Punga": "4D3D14", + "Purple": "660099", + "Purple Heart": "652DC1", + "Purple Mountain's Majesty": "9678B6", + "Purple Pizzazz": "FF00CC", + "Putty": "E7CD8C", + "Quarter Pearl Lusta": "FFFDF4", + "Quarter Spanish White": "F7F2E1", + "Quicksand": "BD978E", + "Quill Gray": "D6D6D1", + "Quincy": "623F2D", + "Racing Green": "0C1911", + "Radical Red": "FF355E", + "Raffia": "EADAB8", + "Rainee": "B9C8AC", + "Rajah": "F7B668", + "Rangitoto": "2E3222", + "Rangoon Green": "1C1E13", + "Raven": "727B89", + "Raw Sienna": "D27D46", + "Raw Umber": "734A12", + "Razzle Dazzle Rose": "FF33CC", + "Razzmatazz": "E30B5C", + "Rebel": "3C1206", + "Red": "FF0000", + "Red Beech": "7B3801", + "Red Berry": "8E0000", + "Red Damask": "DA6A41", + "Red Devil": "860111", + "Red Orange": "FF3F34", + "Red Oxide": "6E0902", + "Red Ribbon": "ED0A3F", + "Red Robin": "80341F", + "Red Stage": "D05F04", + "Red Violet": "C71585", + "Redwood": "5D1E0F", + "Reef": "C9FFA2", + "Reef Gold": "9F821C", + "Regal Blue": "013F6A", + "Regent Gray": "86949F", + "Regent St Blue": "AAD6E6", + "Remy": "FEEBF3", + "Reno Sand": "A86515", + "Resolution Blue": "002387", + "Revolver": "2C1632", + "Rhino": "2E3F62", + "Rice Cake": "FFFEF0", + "Rice Flower": "EEFFE2", + "Rich Gold": "A85307", + "Rio Grande": "BBD009", + "Ripe Lemon": "F4D81C", + "Ripe Plum": "410056", + "Riptide": "8BE6D8", + "River Bed": "434C59", + "Rob Roy": "EAC674", + "Robin's Egg Blue": "00CCCC", + "Rock": "4D3833", + "Rock Blue": "9EB1CD", + "Rock Spray": "BA450C", + "Rodeo Dust": "C9B29B", + "Rolling Stone": "747D83", + "Roman": "DE6360", + "Roman Coffee": "795D4C", + "Romance": "FFFEFD", + "Romantic": "FFD2B7", + "Ronchi": "ECC54E", + "Roof Terracotta": "A62F20", + "Rope": "8E4D1E", + "Rose": "FF007F", + "Rose Bud": "FBB2A3", + "Rose Bud Cherry": "800B47", + "Rose Fog": "E7BCB4", + "Rose White": "FFF6F5", + "Rose of Sharon": "BF5500", + "Rosewood": "65000B", + "Roti": "C6A84B", + "Rouge": "A23B6C", + "Royal Blue": "4169E1", + "Royal Heath": "AB3472", + "Royal Purple": "6B3FA0", + "Rum": "796989", + "Rum Swizzle": "F9F8E4", + "Russet": "80461B", + "Russett": "755A57", + "Rust": "B7410E", + "Rustic Red": "480404", + "Rusty Nail": "86560A", + "Saddle": "4C3024", + "Saddle Brown": "583401", + "Saffron": "F4C430", + "Saffron Mango": "F9BF58", + "Sage": "9EA587", + "Sahara": "B7A214", + "Sahara Sand": "F1E788", + "Sail": "B8E0F9", + "Salem": "097F4B", + "Salmon": "FF8C69", + "Salomie": "FEDB8D", + "Salt Box": "685E6E", + "Saltpan": "F1F7F2", + "Sambuca": "3A2010", + "San Felix": "0B6207", + "San Juan": "304B6A", + "San Marino": "456CAC", + "Sand Dune": "826F65", + "Sandal": "AA8D6F", + "Sandrift": "AB917A", + "Sandstone": "796D62", + "Sandwisp": "F5E7A2", + "Sandy Beach": "FFEAC8", + "Sandy brown": "F4A460", + "Sangria": "92000A", + "Sanguine Brown": "8D3D38", + "Santa Fe": "B16D52", + "Santas Gray": "9FA0B1", + "Sapling": "DED4A4", + "Sapphire": "2F519E", + "Saratoga": "555B10", + "Satin Linen": "E6E4D4", + "Sauvignon": "FFF5F3", + "Sazerac": "FFF4E0", + "Scampi": "675FA6", + "Scandal": "CFFAF4", + "Scarlet": "FF2400", + "Scarlet Gum": "431560", + "Scarlett": "950015", + "Scarpa Flow": "585562", + "Schist": "A9B497", + "School bus Yellow": "FFD800", + "Schooner": "8B847E", + "Science Blue": "0066CC", + "Scooter": "2EBFD4", + "Scorpion": "695F62", + "Scotch Mist": "FFFBDC", + "Screamin' Green": "66FF66", + "Sea Buckthorn": "FBA129", + "Sea Green": "2E8B57", + "Sea Mist": "C5DBCA", + "Sea Nymph": "78A39C", + "Sea Pink": "ED989E", + "Seagull": "80CCEA", + "Seance": "731E8F", + "Seashell": "F1F1F1", + "Seashell Peach": "FFF5EE", + "Seaweed": "1B2F11", + "Selago": "F0EEFD", + "Selective Yellow": "FFBA00", + "Sepia": "704214", + "Sepia Black": "2B0202", + "Sepia Skin": "9E5B40", + "Serenade": "FFF4E8", + "Shadow": "837050", + "Shadow Green": "9AC2B8", + "Shady Lady": "AAA5A9", + "Shakespeare": "4EABD1", + "Shalimar": "FBFFBA", + "Shamrock": "33CC99", + "Shark": "25272C", + "Sherpa Blue": "004950", + "Sherwood Green": "02402C", + "Shilo": "E8B9B3", + "Shingle Fawn": "6B4E31", + "Ship Cove": "788BBA", + "Ship Gray": "3E3A44", + "Shiraz": "B20931", + "Shocking": "E292C0", + "Shocking Pink": "FC0FC0", + "Shuttle Gray": "5F6672", + "Siam": "646A54", + "Sidecar": "F3E7BB", + "Silk": "BDB1A8", + "Silver": "C0C0C0", + "Silver Chalice": "ACACAC", + "Silver Rust": "C9C0BB", + "Silver Sand": "BFC1C2", + "Silver Tree": "66B58F", + "Sinbad": "9FD7D3", + "Siren": "7A013A", + "Sirocco": "718080", + "Sisal": "D3CBBA", + "Skeptic": "CAE6DA", + "Sky Blue": "76D7EA", + "Slate Gray": "708090", + "Smalt": "003399", + "Smalt Blue": "51808F", + "Smoky": "605B73", + "Snow Drift": "F7FAF7", + "Snow Flurry": "E4FFD1", + "Snowy Mint": "D6FFDB", + "Snuff": "E2D8ED", + "Soapstone": "FFFBF9", + "Soft Amber": "D1C6B4", + "Soft Peach": "F5EDEF", + "Solid Pink": "893843", + "Solitaire": "FEF8E2", + "Solitude": "EAF6FF", + "Sorbus": "FD7C07", + "Sorrell Brown": "CEB98F", + "Soya Bean": "6A6051", + "Spanish Green": "819885", + "Spectra": "2F5A57", + "Spice": "6A442E", + "Spicy Mix": "885342", + "Spicy Mustard": "74640D", + "Spicy Pink": "816E71", + "Spindle": "B6D1EA", + "Spray": "79DEEC", + "Spring Green": "00FF7F", + "Spring Leaves": "578363", + "Spring Rain": "ACCBB1", + "Spring Sun": "F6FFDC", + "Spring Wood": "F8F6F1", + "Sprout": "C1D7B0", + "Spun Pearl": "AAABB7", + "Squirrel": "8F8176", + "St Tropaz": "2D569B", + "Stack": "8A8F8A", + "Star Dust": "9F9F9C", + "Stark White": "E5D7BD", + "Starship": "ECF245", + "Steel Blue": "4682B4", + "Steel Gray": "262335", + "Stiletto": "9C3336", + "Stonewall": "928573", + "Storm Dust": "646463", + "Storm Gray": "717486", + "Stratos": "000741", + "Straw": "D4BF8D", + "Strikemaster": "956387", + "Stromboli": "325D52", + "Studio": "714AB2", + "Submarine": "BAC7C9", + "Sugar Cane": "F9FFF6", + "Sulu": "C1F07C", + "Summer Green": "96BBAB", + "Sun": "FBAC13", + "Sundance": "C9B35B", + "Sundown": "FFB1B3", + "Sunflower": "E4D422", + "Sunglo": "E16865", + "Sunglow": "FFCC33", + "Sunset Orange": "FE4C40", + "Sunshade": "FF9E2C", + "Supernova": "FFC901", + "Surf": "BBD7C1", + "Surf Crest": "CFE5D2", + "Surfie Green": "0C7A79", + "Sushi": "87AB39", + "Suva Gray": "888387", + "Swamp": "001B1C", + "Swamp Green": "ACB78E", + "Swans Down": "DCF0EA", + "Sweet Corn": "FBEA8C", + "Sweet Pink": "FD9FA2", + "Swirl": "D3CDC5", + "Swiss Coffee": "DDD6D5", + "Sycamore": "908D39", + "Tabasco": "A02712", + "Tacao": "EDB381", + "Tacha": "D6C562", + "Tahiti Gold": "E97C07", + "Tahuna Sands": "EEF0C8", + "Tall Poppy": "B32D29", + "Tallow": "A8A589", + "Tamarillo": "991613", + "Tamarind": "341515", + "Tan": "D2B48C", + "Tan Hide": "FA9D5A", + "Tana": "D9DCC1", + "Tangaroa": "03163C", + "Tangerine": "F28500", + "Tango": "ED7A1C", + "Tapa": "7B7874", + "Tapestry": "B05E81", + "Tara": "E1F6E8", + "Tarawera": "073A50", + "Tasman": "CFDCCF", + "Taupe": "483C32", + "Taupe Gray": "B3AF95", + "Tawny Port": "692545", + "Te Papa Green": "1E433C", + "Tea": "C1BAB0", + "Tea Green": "D0F0C0", + "Teak": "B19461", + "Teal": "008080", + "Teal Blue": "044259", + "Temptress": "3B000B", + "Tenn": "CD5700", + "Tequila": "FFE6C7", + "Terracotta": "E2725B", + "Texas": "F8F99C", + "Texas Rose": "FFB555", + "Thatch": "B69D98", + "Thatch Green": "403D19", + "Thistle": "D8BFD8", + "Thistle Green": "CCCAA8", + "Thunder": "33292F", + "Thunderbird": "C02B18", + "Tia Maria": "C1440E", + "Tiara": "C3D1D1", + "Tiber": "063537", + "Tickle Me Pink": "FC80A5", + "Tidal": "F1FFAD", + "Tide": "BFB8B0", + "Timber Green": "16322C", + "Timberwolf": "D9D6CF", + "Titan White": "F0EEFF", + "Toast": "9A6E61", + "Tobacco Brown": "715D47", + "Toledo": "3A0020", + "Tolopea": "1B0245", + "Tom Thumb": "3F583B", + "Tonys Pink": "E79F8C", + "Topaz": "7C778A", + "Torch Red": "FD0E35", + "Torea Bay": "0F2D9E", + "Tory Blue": "1450AA", + "Tosca": "8D3F3F", + "Totem Pole": "991B07", + "Tower Gray": "A9BDBF", + "Tradewind": "5FB3AC", + "Tranquil": "E6FFFF", + "Travertine": "FFFDE8", + "Tree Poppy": "FC9C1D", + "Treehouse": "3B2820", + "Trendy Green": "7C881A", + "Trendy Pink": "8C6495", + "Trinidad": "E64E03", + "Tropical Blue": "C3DDF9", + "Tropical Rain Forest": "00755E", + "Trout": "4A4E5A", + "True V": "8A73D6", + "Tuatara": "363534", + "Tuft Bush": "FFDDCD", + "Tulip Tree": "EAB33B", + "Tumbleweed": "DEA681", + "Tuna": "353542", + "Tundora": "4A4244", + "Turbo": "FAE600", + "Turkish Rose": "B57281", + "Turmeric": "CABB48", + "Turquoise": "30D5C8", + "Turquoise Blue": "6CDAE7", + "Turtle Green": "2A380B", + "Tuscany": "BD5E2E", + "Tusk": "EEF3C3", + "Tussock": "C5994B", + "Tutu": "FFF1F9", + "Twilight": "E4CFDE", + "Twilight Blue": "EEFDFF", + "Twine": "C2955D", + "Tyrian Purple": "66023C", + "Ultramarine": "120A8F", + "Valencia": "D84437", + "Valentino": "350E42", + "Valhalla": "2B194F", + "Van Cleef": "49170C", + "Vanilla": "D1BEA8", + "Vanilla Ice": "F3D9DF", + "Varden": "FFF6DF", + "Venetian Red": "72010F", + "Venice Blue": "055989", + "Venus": "928590", + "Verdigris": "5D5E37", + "Verdun Green": "495400", + "Vermilion": "FF4D00", + "Vesuvius": "B14A0B", + "Victoria": "534491", + "Vida Loca": "549019", + "Viking": "64CCDB", + "Vin Rouge": "983D61", + "Viola": "CB8FA9", + "Violent Violet": "290C5E", + "Violet": "240A40", + "Violet Eggplant": "991199", + "Violet Red": "F7468A", + "Viridian": "40826D", + "Viridian Green": "678975", + "Vis Vis": "FFEFA1", + "Vista Blue": "8FD6B4", + "Vista White": "FCF8F7", + "Vivid Tangerine": "FF9980", + "Vivid Violet": "803790", + "Voodoo": "533455", + "Vulcan": "10121D", + "Wafer": "DECBC6", + "Waikawa Gray": "5A6E9C", + "Waiouru": "363C0D", + "Walnut": "773F1A", + "Wasabi": "788A25", + "Water Leaf": "A1E9DE", + "Watercourse": "056F57", + "Waterloo ": "7B7C94", + "Wattle": "DCD747", + "Watusi": "FFDDCF", + "Wax Flower": "FFC0A8", + "We Peep": "F7DBE6", + "Web Orange": "FFA500", + "Wedgewood": "4E7F9E", + "Well Read": "B43332", + "West Coast": "625119", + "West Side": "FF910F", + "Westar": "DCD9D2", + "Wewak": "F19BAB", + "Wheat": "F5DEB3", + "Wheatfield": "F3EDCF", + "Whiskey": "D59A6F", + "Whisper": "F7F5FA", + "White": "FFFFFF", + "White Ice": "DDF9F1", + "White Lilac": "F8F7FC", + "White Linen": "F8F0E8", + "White Pointer": "FEF8FF", + "White Rock": "EAE8D4", + "Wild Blue Yonder": "7A89B8", + "Wild Rice": "ECE090", + "Wild Sand": "F4F4F4", + "Wild Strawberry": "FF3399", + "Wild Watermelon": "FD5B78", + "Wild Willow": "B9C46A", + "William": "3A686C", + "Willow Brook": "DFECDA", + "Willow Grove": "65745D", + "Windsor": "3C0878", + "Wine Berry": "591D35", + "Winter Hazel": "D5D195", + "Wisp Pink": "FEF4F8", + "Wisteria": "9771B5", + "Wistful": "A4A6D3", + "Witch Haze": "FFFC99", + "Wood Bark": "261105", + "Woodland": "4D5328", + "Woodrush": "302A0F", + "Woodsmoke": "0C0D0F", + "Woody Brown": "483131", + "Xanadu": "738678", + "Yellow": "FFFF00", + "Yellow Green": "C5E17A", + "Yellow Metal": "716338", + "Yellow Orange": "FFAE42", + "Yellow Sea": "FEA904", + "Your Pink": "FFC3C0", + "Yukon Gold": "7B6608", + "Yuma": "CEC291", + "Zambezi": "685558", + "Zanah": "DAECD6", + "Zest": "E5841B", + "Zeus": "292319", + "Ziggurat": "BFDBE2", + "Zinnwaldite": "EBC2AF", + "Zircon": "F4F8FF", + "Zombie": "E4D69B", + "Zorba": "A59B91", + "Zuccini": "044022", + "Zumthor": "EDF6FF" +} \ No newline at end of file -- cgit v1.2.3 From 663b52857913f9fc32d753ed2959afab15a97375 Mon Sep 17 00:00:00 2001 From: CyberCitizen01 Date: Sat, 4 Sep 2021 17:40:02 +0530 Subject: Added ryanzec_colours.json constructed from ryanzec/name-that-color Original source: https://github.com/ryanzec/name-that-color/blob/master/lib/ntc.js#L116-L1681 --- bot/resources/evergreen/ryanzec_colours.json | 1568 ++++++++++++++++++++++++++ 1 file changed, 1568 insertions(+) create mode 100644 bot/resources/evergreen/ryanzec_colours.json diff --git a/bot/resources/evergreen/ryanzec_colours.json b/bot/resources/evergreen/ryanzec_colours.json new file mode 100644 index 00000000..7b89f052 --- /dev/null +++ b/bot/resources/evergreen/ryanzec_colours.json @@ -0,0 +1,1568 @@ +{ + "Abbey": "4C4F56", + "Acadia": "1B1404", + "Acapulco": "7CB0A1", + "Aero Blue": "C9FFE5", + "Affair": "714693", + "Akaroa": "D4C4A8", + "Alabaster": "FAFAFA", + "Albescent White": "F5E9D3", + "Algae Green": "93DFB8", + "Alice Blue": "F0F8FF", + "Alizarin Crimson": "E32636", + "Allports": "0076A3", + "Almond": "EED9C4", + "Almond Frost": "907B71", + "Alpine": "AF8F2C", + "Alto": "DBDBDB", + "Aluminium": "A9ACB6", + "Amaranth": "E52B50", + "Amazon": "3B7A57", + "Amber": "FFBF00", + "Americano": "87756E", + "Amethyst": "9966CC", + "Amethyst Smoke": "A397B4", + "Amour": "F9EAF3", + "Amulet": "7B9F80", + "Anakiwa": "9DE5FF", + "Antique Brass": "C88A65", + "Antique Bronze": "704A07", + "Anzac": "E0B646", + "Apache": "DFBE6F", + "Apple": "4FA83D", + "Apple Blossom": "AF4D43", + "Apple Green": "E2F3EC", + "Apricot": "EB9373", + "Apricot Peach": "FBCEB1", + "Apricot White": "FFFEEC", + "Aqua Deep": "014B43", + "Aqua Forest": "5FA777", + "Aqua Haze": "EDF5F5", + "Aqua Island": "A1DAD7", + "Aqua Spring": "EAF9F5", + "Aqua Squeeze": "E8F5F2", + "Aquamarine": "7FFFD4", + "Aquamarine Blue": "71D9E2", + "Arapawa": "110C6C", + "Armadillo": "433E37", + "Arrowtown": "948771", + "Ash": "C6C3B5", + "Asparagus": "7BA05B", + "Asphalt": "130A06", + "Astra": "FAEAB9", + "Astral": "327DA0", + "Astronaut": "283A77", + "Astronaut Blue": "013E62", + "Athens Gray": "EEF0F3", + "Aths Special": "ECEBCE", + "Atlantis": "97CD2D", + "Atoll": "0A6F75", + "Atomic Tangerine": "FF9966", + "Au Chico": "97605D", + "Aubergine": "3B0910", + "Australian Mint": "F5FFBE", + "Avocado": "888D65", + "Axolotl": "4E6649", + "Azalea": "F7C8DA", + "Aztec": "0D1C19", + "Azure": "315BA1", + "Azure Radiance": "007FFF", + "Baby Blue": "E0FFFF", + "Bahama Blue": "026395", + "Bahia": "A5CB0C", + "Baja White": "FFF8D1", + "Bali Hai": "859FAF", + "Baltic Sea": "2A2630", + "Bamboo": "DA6304", + "Banana Mania": "FBE7B2", + "Bandicoot": "858470", + "Barberry": "DED717", + "Barley Corn": "A68B5B", + "Barley White": "FFF4CE", + "Barossa": "44012D", + "Bastille": "292130", + "Battleship Gray": "828F72", + "Bay Leaf": "7DA98D", + "Bay of Many": "273A81", + "Bazaar": "98777B", + "Bean ": "3D0C02", + "Beauty Bush": "EEC1BE", + "Beaver": "926F5B", + "Beeswax": "FEF2C7", + "Beige": "F5F5DC", + "Bermuda": "7DD8C6", + "Bermuda Gray": "6B8BA2", + "Beryl Green": "DEE5C0", + "Bianca": "FCFBF3", + "Big Stone": "162A40", + "Bilbao": "327C14", + "Biloba Flower": "B2A1EA", + "Birch": "373021", + "Bird Flower": "D4CD16", + "Biscay": "1B3162", + "Bismark": "497183", + "Bison Hide": "C1B7A4", + "Bistre": "3D2B1F", + "Bitter": "868974", + "Bitter Lemon": "CAE00D", + "Bittersweet": "FE6F5E", + "Bizarre": "EEDEDA", + "Black": "000000", + "Black Bean": "081910", + "Black Forest": "0B1304", + "Black Haze": "F6F7F7", + "Black Marlin": "3E2C1C", + "Black Olive": "242E16", + "Black Pearl": "041322", + "Black Rock": "0D0332", + "Black Rose": "67032D", + "Black Russian": "0A001C", + "Black Squeeze": "F2FAFA", + "Black White": "FFFEF6", + "Blackberry": "4D0135", + "Blackcurrant": "32293A", + "Blaze Orange": "FF6600", + "Bleach White": "FEF3D8", + "Bleached Cedar": "2C2133", + "Blizzard Blue": "A3E3ED", + "Blossom": "DCB4BC", + "Blue": "0000FF", + "Blue Bayoux": "496679", + "Blue Bell": "9999CC", + "Blue Chalk": "F1E9FF", + "Blue Charcoal": "010D1A", + "Blue Chill": "0C8990", + "Blue Diamond": "380474", + "Blue Dianne": "204852", + "Blue Gem": "2C0E8C", + "Blue Haze": "BFBED8", + "Blue Lagoon": "017987", + "Blue Marguerite": "7666C6", + "Blue Ribbon": "0066FF", + "Blue Romance": "D2F6DE", + "Blue Smoke": "748881", + "Blue Stone": "016162", + "Blue Violet": "6456B7", + "Blue Whale": "042E4C", + "Blue Zodiac": "13264D", + "Blumine": "18587A", + "Blush": "B44668", + "Blush Pink": "FF6FFF", + "Bombay": "AFB1B8", + "Bon Jour": "E5E0E1", + "Bondi Blue": "0095B6", + "Bone": "E4D1C0", + "Bordeaux": "5C0120", + "Bossanova": "4E2A5A", + "Boston Blue": "3B91B4", + "Botticelli": "C7DDE5", + "Bottle Green": "093624", + "Boulder": "7A7A7A", + "Bouquet": "AE809E", + "Bourbon": "BA6F1E", + "Bracken": "4A2A04", + "Brandy": "DEC196", + "Brandy Punch": "CD8429", + "Brandy Rose": "BB8983", + "Breaker Bay": "5DA19F", + "Brick Red": "C62D42", + "Bridal Heath": "FFFAF4", + "Bridesmaid": "FEF0EC", + "Bright Gray": "3C4151", + "Bright Green": "66FF00", + "Bright Red": "B10000", + "Bright Sun": "FED33C", + "Bright Turquoise": "08E8DE", + "Brilliant Rose": "F653A6", + "Brink Pink": "FB607F", + "Bronco": "ABA196", + "Bronze": "3F2109", + "Bronze Olive": "4E420C", + "Bronzetone": "4D400F", + "Broom": "FFEC13", + "Brown": "964B00", + "Brown Bramble": "592804", + "Brown Derby": "492615", + "Brown Pod": "401801", + "Brown Rust": "AF593E", + "Brown Tumbleweed": "37290E", + "Bubbles": "E7FEFF", + "Buccaneer": "622F30", + "Bud": "A8AE9C", + "Buddha Gold": "C1A004", + "Buff": "F0DC82", + "Bulgarian Rose": "480607", + "Bull Shot": "864D1E", + "Bunker": "0D1117", + "Bunting": "151F4C", + "Burgundy": "900020", + "Burnham": "002E20", + "Burning Orange": "FF7034", + "Burning Sand": "D99376", + "Burnt Maroon": "420303", + "Burnt Orange": "CC5500", + "Burnt Sienna": "E97451", + "Burnt Umber": "8A3324", + "Bush": "0D2E1C", + "Buttercup": "F3AD16", + "Buttered Rum": "A1750D", + "Butterfly Bush": "624E9A", + "Buttermilk": "FFF1B5", + "Buttery White": "FFFCEA", + "Cab Sav": "4D0A18", + "Cabaret": "D94972", + "Cabbage Pont": "3F4C3A", + "Cactus": "587156", + "Cadet Blue": "A9B2C3", + "Cadillac": "B04C6A", + "Cafe Royale": "6F440C", + "Calico": "E0C095", + "California": "FE9D04", + "Calypso": "31728D", + "Camarone": "00581A", + "Camelot": "893456", + "Cameo": "D9B99B", + "Camouflage": "3C3910", + "Camouflage Green": "78866B", + "Can Can": "D591A4", + "Canary": "F3FB62", + "Candlelight": "FCD917", + "Candy Corn": "FBEC5D", + "Cannon Black": "251706", + "Cannon Pink": "894367", + "Cape Cod": "3C4443", + "Cape Honey": "FEE5AC", + "Cape Palliser": "A26645", + "Caper": "DCEDB4", + "Caramel": "FFDDAF", + "Cararra": "EEEEE8", + "Cardin Green": "01361C", + "Cardinal": "C41E3A", + "Cardinal Pink": "8C055E", + "Careys Pink": "D29EAA", + "Caribbean Green": "00CC99", + "Carissma": "EA88A8", + "Carla": "F3FFD8", + "Carmine": "960018", + "Carnaby Tan": "5C2E01", + "Carnation": "F95A61", + "Carnation Pink": "FFA6C9", + "Carousel Pink": "F9E0ED", + "Carrot Orange": "ED9121", + "Casablanca": "F8B853", + "Casal": "2F6168", + "Cascade": "8BA9A5", + "Cashmere": "E6BEA5", + "Casper": "ADBED1", + "Castro": "52001F", + "Catalina Blue": "062A78", + "Catskill White": "EEF6F7", + "Cavern Pink": "E3BEBE", + "Cedar": "3E1C14", + "Cedar Wood Finish": "711A00", + "Celadon": "ACE1AF", + "Celery": "B8C25D", + "Celeste": "D1D2CA", + "Cello": "1E385B", + "Celtic": "163222", + "Cement": "8D7662", + "Ceramic": "FCFFF9", + "Cerise": "DA3287", + "Cerise Red": "DE3163", + "Cerulean": "02A4D3", + "Cerulean Blue": "2A52BE", + "Chablis": "FFF4F3", + "Chalet Green": "516E3D", + "Chalky": "EED794", + "Chambray": "354E8C", + "Chamois": "EDDCB1", + "Champagne": "FAECCC", + "Chantilly": "F8C3DF", + "Charade": "292937", + "Chardon": "FFF3F1", + "Chardonnay": "FFCD8C", + "Charlotte": "BAEEF9", + "Charm": "D47494", + "Chartreuse": "7FFF00", + "Chartreuse Yellow": "DFFF00", + "Chateau Green": "40A860", + "Chatelle": "BDB3C7", + "Chathams Blue": "175579", + "Chelsea Cucumber": "83AA5D", + "Chelsea Gem": "9E5302", + "Chenin": "DFCD6F", + "Cherokee": "FCDA98", + "Cherry Pie": "2A0359", + "Cherrywood": "651A14", + "Cherub": "F8D9E9", + "Chestnut": "B94E48", + "Chestnut Rose": "CD5C5C", + "Chetwode Blue": "8581D9", + "Chicago": "5D5C58", + "Chiffon": "F1FFC8", + "Chilean Fire": "F77703", + "Chilean Heath": "FFFDE6", + "China Ivory": "FCFFE7", + "Chino": "CEC7A7", + "Chinook": "A8E3BD", + "Chocolate": "370202", + "Christalle": "33036B", + "Christi": "67A712", + "Christine": "E7730A", + "Chrome White": "E8F1D4", + "Cinder": "0E0E18", + "Cinderella": "FDE1DC", + "Cinnabar": "E34234", + "Cinnamon": "7B3F00", + "Cioccolato": "55280C", + "Citrine White": "FAF7D6", + "Citron": "9EA91F", + "Citrus": "A1C50A", + "Clairvoyant": "480656", + "Clam Shell": "D4B6AF", + "Claret": "7F1734", + "Classic Rose": "FBCCE7", + "Clay Ash": "BDC8B3", + "Clay Creek": "8A8360", + "Clear Day": "E9FFFD", + "Clementine": "E96E00", + "Clinker": "371D09", + "Cloud": "C7C4BF", + "Cloud Burst": "202E54", + "Cloudy": "ACA59F", + "Clover": "384910", + "Cobalt": "0047AB", + "Cocoa Bean": "481C1C", + "Cocoa Brown": "301F1E", + "Coconut Cream": "F8F7DC", + "Cod Gray": "0B0B0B", + "Coffee": "706555", + "Coffee Bean": "2A140E", + "Cognac": "9F381D", + "Cola": "3F2500", + "Cold Purple": "ABA0D9", + "Cold Turkey": "CEBABA", + "Colonial White": "FFEDBC", + "Comet": "5C5D75", + "Como": "517C66", + "Conch": "C9D9D2", + "Concord": "7C7B7A", + "Concrete": "F2F2F2", + "Confetti": "E9D75A", + "Congo Brown": "593737", + "Congress Blue": "02478E", + "Conifer": "ACDD4D", + "Contessa": "C6726B", + "Copper": "B87333", + "Copper Canyon": "7E3A15", + "Copper Rose": "996666", + "Copper Rust": "944747", + "Copperfield": "DA8A67", + "Coral": "FF7F50", + "Coral Red": "FF4040", + "Coral Reef": "C7BCA2", + "Coral Tree": "A86B6B", + "Corduroy": "606E68", + "Coriander": "C4D0B0", + "Cork": "40291D", + "Corn": "E7BF05", + "Corn Field": "F8FACD", + "Corn Harvest": "8B6B0B", + "Cornflower": "93CCEA", + "Cornflower Blue": "6495ED", + "Cornflower Lilac": "FFB0AC", + "Corvette": "FAD3A2", + "Cosmic": "76395D", + "Cosmos": "FFD8D9", + "Costa Del Sol": "615D30", + "Cotton Candy": "FFB7D5", + "Cotton Seed": "C2BDB6", + "County Green": "01371A", + "Cowboy": "4D282D", + "Crail": "B95140", + "Cranberry": "DB5079", + "Crater Brown": "462425", + "Cream": "FFFDD0", + "Cream Brulee": "FFE5A0", + "Cream Can": "F5C85C", + "Creole": "1E0F04", + "Crete": "737829", + "Crimson": "DC143C", + "Crocodile": "736D58", + "Crown of Thorns": "771F1F", + "Crowshead": "1C1208", + "Cruise": "B5ECDF", + "Crusoe": "004816", + "Crusta": "FD7B33", + "Cumin": "924321", + "Cumulus": "FDFFD5", + "Cupid": "FBBEDA", + "Curious Blue": "2596D1", + "Cutty Sark": "507672", + "Cyan / Aqua": "00FFFF", + "Cyprus": "003E40", + "Daintree": "012731", + "Dairy Cream": "F9E4BC", + "Daisy Bush": "4F2398", + "Dallas": "6E4B26", + "Dandelion": "FED85D", + "Danube": "6093D1", + "Dark Blue": "0000C8", + "Dark Burgundy": "770F05", + "Dark Ebony": "3C2005", + "Dark Fern": "0A480D", + "Dark Tan": "661010", + "Dawn": "A6A29A", + "Dawn Pink": "F3E9E5", + "De York": "7AC488", + "Deco": "D2DA97", + "Deep Blue": "220878", + "Deep Blush": "E47698", + "Deep Bronze": "4A3004", + "Deep Cerulean": "007BA7", + "Deep Cove": "051040", + "Deep Fir": "002900", + "Deep Forest Green": "182D09", + "Deep Koamaru": "1B127B", + "Deep Oak": "412010", + "Deep Sapphire": "082567", + "Deep Sea": "01826B", + "Deep Sea Green": "095859", + "Deep Teal": "003532", + "Del Rio": "B09A95", + "Dell": "396413", + "Delta": "A4A49D", + "Deluge": "7563A8", + "Denim": "1560BD", + "Derby": "FFEED8", + "Desert": "AE6020", + "Desert Sand": "EDC9AF", + "Desert Storm": "F8F8F7", + "Dew": "EAFFFE", + "Di Serria": "DB995E", + "Diesel": "130000", + "Dingley": "5D7747", + "Disco": "871550", + "Dixie": "E29418", + "Dodger Blue": "1E90FF", + "Dolly": "F9FF8B", + "Dolphin": "646077", + "Domino": "8E775E", + "Don Juan": "5D4C51", + "Donkey Brown": "A69279", + "Dorado": "6B5755", + "Double Colonial White": "EEE3AD", + "Double Pearl Lusta": "FCF4D0", + "Double Spanish White": "E6D7B9", + "Dove Gray": "6D6C6C", + "Downriver": "092256", + "Downy": "6FD0C5", + "Driftwood": "AF8751", + "Drover": "FDF7AD", + "Dull Lavender": "A899E6", + "Dune": "383533", + "Dust Storm": "E5CCC9", + "Dusty Gray": "A8989B", + "Eagle": "B6BAA4", + "Earls Green": "C9B93B", + "Early Dawn": "FFF9E6", + "East Bay": "414C7D", + "East Side": "AC91CE", + "Eastern Blue": "1E9AB0", + "Ebb": "E9E3E3", + "Ebony": "0C0B1D", + "Ebony Clay": "26283B", + "Eclipse": "311C17", + "Ecru White": "F5F3E5", + "Ecstasy": "FA7814", + "Eden": "105852", + "Edgewater": "C8E3D7", + "Edward": "A2AEAB", + "Egg Sour": "FFF4DD", + "Egg White": "FFEFC1", + "Eggplant": "614051", + "El Paso": "1E1708", + "El Salva": "8F3E33", + "Electric Lime": "CCFF00", + "Electric Violet": "8B00FF", + "Elephant": "123447", + "Elf Green": "088370", + "Elm": "1C7C7D", + "Emerald": "50C878", + "Eminence": "6C3082", + "Emperor": "514649", + "Empress": "817377", + "Endeavour": "0056A7", + "Energy Yellow": "F8DD5C", + "English Holly": "022D15", + "English Walnut": "3E2B23", + "Envy": "8BA690", + "Equator": "E1BC64", + "Espresso": "612718", + "Eternity": "211A0E", + "Eucalyptus": "278A5B", + "Eunry": "CFA39D", + "Evening Sea": "024E46", + "Everglade": "1C402E", + "Faded Jade": "427977", + "Fair Pink": "FFEFEC", + "Falcon": "7F626D", + "Fall Green": "ECEBBD", + "Falu Red": "801818", + "Fantasy": "FAF3F0", + "Fedora": "796A78", + "Feijoa": "9FDD8C", + "Fern": "63B76C", + "Fern Frond": "657220", + "Fern Green": "4F7942", + "Ferra": "704F50", + "Festival": "FBE96C", + "Feta": "F0FCEA", + "Fiery Orange": "B35213", + "Finch": "626649", + "Finlandia": "556D56", + "Finn": "692D54", + "Fiord": "405169", + "Fire": "AA4203", + "Fire Bush": "E89928", + "Firefly": "0E2A30", + "Flame Pea": "DA5B38", + "Flamenco": "FF7D07", + "Flamingo": "F2552A", + "Flax": "EEDC82", + "Flax Smoke": "7B8265", + "Flesh": "FFCBA4", + "Flint": "6F6A61", + "Flirt": "A2006D", + "Flush Mahogany": "CA3435", + "Flush Orange": "FF7F00", + "Foam": "D8FCFA", + "Fog": "D7D0FF", + "Foggy Gray": "CBCAB6", + "Forest Green": "228B22", + "Forget Me Not": "FFF1EE", + "Fountain Blue": "56B4BE", + "Frangipani": "FFDEB3", + "French Gray": "BDBDC6", + "French Lilac": "ECC7EE", + "French Pass": "BDEDFD", + "French Rose": "F64A8A", + "Fresh Eggplant": "990066", + "Friar Gray": "807E79", + "Fringy Flower": "B1E2C1", + "Froly": "F57584", + "Frost": "EDF5DD", + "Frosted Mint": "DBFFF8", + "Frostee": "E4F6E7", + "Fruit Salad": "4F9D5D", + "Fuchsia Blue": "7A58C1", + "Fuchsia Pink": "C154C1", + "Fuego": "BEDE0D", + "Fuel Yellow": "ECA927", + "Fun Blue": "1959A8", + "Fun Green": "016D39", + "Fuscous Gray": "54534D", + "Fuzzy Wuzzy Brown": "C45655", + "Gable Green": "163531", + "Gallery": "EFEFEF", + "Galliano": "DCB20C", + "Gamboge": "E49B0F", + "Geebung": "D18F1B", + "Genoa": "15736B", + "Geraldine": "FB8989", + "Geyser": "D4DFE2", + "Ghost": "C7C9D5", + "Gigas": "523C94", + "Gimblet": "B8B56A", + "Gin": "E8F2EB", + "Gin Fizz": "FFF9E2", + "Givry": "F8E4BF", + "Glacier": "80B3C4", + "Glade Green": "61845F", + "Go Ben": "726D4E", + "Goblin": "3D7D52", + "Gold": "FFD700", + "Gold Drop": "F18200", + "Gold Sand": "E6BE8A", + "Gold Tips": "DEBA13", + "Golden Bell": "E28913", + "Golden Dream": "F0D52D", + "Golden Fizz": "F5FB3D", + "Golden Glow": "FDE295", + "Golden Grass": "DAA520", + "Golden Sand": "F0DB7D", + "Golden Tainoi": "FFCC5C", + "Goldenrod": "FCD667", + "Gondola": "261414", + "Gordons Green": "0B1107", + "Gorse": "FFF14F", + "Gossamer": "069B81", + "Gossip": "D2F8B0", + "Gothic": "6D92A1", + "Governor Bay": "2F3CB3", + "Grain Brown": "E4D5B7", + "Grandis": "FFD38C", + "Granite Green": "8D8974", + "Granny Apple": "D5F6E3", + "Granny Smith": "84A0A0", + "Granny Smith Apple": "9DE093", + "Grape": "381A51", + "Graphite": "251607", + "Gravel": "4A444B", + "Gray": "808080", + "Gray Asparagus": "465945", + "Gray Chateau": "A2AAB3", + "Gray Nickel": "C3C3BD", + "Gray Nurse": "E7ECE6", + "Gray Olive": "A9A491", + "Gray Suit": "C1BECD", + "Green": "00FF00", + "Green Haze": "01A368", + "Green House": "24500F", + "Green Kelp": "25311C", + "Green Leaf": "436A0D", + "Green Mist": "CBD3B0", + "Green Pea": "1D6142", + "Green Smoke": "A4AF6E", + "Green Spring": "B8C1B1", + "Green Vogue": "032B52", + "Green Waterloo": "101405", + "Green White": "E8EBE0", + "Green Yellow": "ADFF2F", + "Grenadier": "D54600", + "Guardsman Red": "BA0101", + "Gulf Blue": "051657", + "Gulf Stream": "80B3AE", + "Gull Gray": "9DACB7", + "Gum Leaf": "B6D3BF", + "Gumbo": "7CA1A6", + "Gun Powder": "414257", + "Gunsmoke": "828685", + "Gurkha": "9A9577", + "Hacienda": "98811B", + "Hairy Heath": "6B2A14", + "Haiti": "1B1035", + "Half Baked": "85C4CC", + "Half Colonial White": "FDF6D3", + "Half Dutch White": "FEF7DE", + "Half Spanish White": "FEF4DB", + "Half and Half": "FFFEE1", + "Hampton": "E5D8AF", + "Harlequin": "3FFF00", + "Harp": "E6F2EA", + "Harvest Gold": "E0B974", + "Havelock Blue": "5590D9", + "Hawaiian Tan": "9D5616", + "Hawkes Blue": "D4E2FC", + "Heath": "541012", + "Heather": "B7C3D0", + "Heathered Gray": "B6B095", + "Heavy Metal": "2B3228", + "Heliotrope": "DF73FF", + "Hemlock": "5E5D3B", + "Hemp": "907874", + "Hibiscus": "B6316C", + "Highland": "6F8E63", + "Hillary": "ACA586", + "Himalaya": "6A5D1B", + "Hint of Green": "E6FFE9", + "Hint of Red": "FBF9F9", + "Hint of Yellow": "FAFDE4", + "Hippie Blue": "589AAF", + "Hippie Green": "53824B", + "Hippie Pink": "AE4560", + "Hit Gray": "A1ADB5", + "Hit Pink": "FFAB81", + "Hokey Pokey": "C8A528", + "Hoki": "65869F", + "Holly": "011D13", + "Hollywood Cerise": "F400A1", + "Honey Flower": "4F1C70", + "Honeysuckle": "EDFC84", + "Hopbush": "D06DA1", + "Horizon": "5A87A0", + "Horses Neck": "604913", + "Hot Cinnamon": "D2691E", + "Hot Pink": "FF69B4", + "Hot Toddy": "B38007", + "Humming Bird": "CFF9F3", + "Hunter Green": "161D10", + "Hurricane": "877C7B", + "Husk": "B7A458", + "Ice Cold": "B1F4E7", + "Iceberg": "DAF4F0", + "Illusion": "F6A4C9", + "Inch Worm": "B0E313", + "Indian Khaki": "C3B091", + "Indian Tan": "4D1E01", + "Indigo": "4F69C6", + "Indochine": "C26B03", + "International Klein Blue": "002FA7", + "International Orange": "FF4F00", + "Irish Coffee": "5F3D26", + "Iroko": "433120", + "Iron": "D4D7D9", + "Ironside Gray": "676662", + "Ironstone": "86483C", + "Island Spice": "FFFCEE", + "Ivory": "FFFFF0", + "Jacaranda": "2E0329", + "Jacarta": "3A2A6A", + "Jacko Bean": "2E1905", + "Jacksons Purple": "20208D", + "Jade": "00A86B", + "Jaffa": "EF863F", + "Jagged Ice": "C2E8E5", + "Jagger": "350E57", + "Jaguar": "080110", + "Jambalaya": "5B3013", + "Janna": "F4EBD3", + "Japanese Laurel": "0A6906", + "Japanese Maple": "780109", + "Japonica": "D87C63", + "Java": "1FC2C2", + "Jazzberry Jam": "A50B5E", + "Jelly Bean": "297B9A", + "Jet Stream": "B5D2CE", + "Jewel": "126B40", + "Jon": "3B1F1F", + "Jonquil": "EEFF9A", + "Jordy Blue": "8AB9F1", + "Judge Gray": "544333", + "Jumbo": "7C7B82", + "Jungle Green": "29AB87", + "Jungle Mist": "B4CFD3", + "Juniper": "6D9292", + "Just Right": "ECCDB9", + "Kabul": "5E483E", + "Kaitoke Green": "004620", + "Kangaroo": "C6C8BD", + "Karaka": "1E1609", + "Karry": "FFEAD4", + "Kashmir Blue": "507096", + "Kelp": "454936", + "Kenyan Copper": "7C1C05", + "Keppel": "3AB09E", + "Key Lime Pie": "BFC921", + "Khaki": "F0E68C", + "Kidnapper": "E1EAD4", + "Kilamanjaro": "240C02", + "Killarney": "3A6A47", + "Kimberly": "736C9F", + "Kingfisher Daisy": "3E0480", + "Kobi": "E79FC4", + "Kokoda": "6E6D57", + "Korma": "8F4B0E", + "Koromiko": "FFBD5F", + "Kournikova": "FFE772", + "Kumera": "886221", + "La Palma": "368716", + "La Rioja": "B3C110", + "Las Palmas": "C6E610", + "Laser": "C8B568", + "Laser Lemon": "FFFF66", + "Laurel": "749378", + "Lavender": "B57EDC", + "Lavender Gray": "BDBBD7", + "Lavender Magenta": "EE82EE", + "Lavender Pink": "FBAED2", + "Lavender Purple": "967BB6", + "Lavender Rose": "FBA0E3", + "Lavender blush": "FFF0F5", + "Leather": "967059", + "Lemon": "FDE910", + "Lemon Chiffon": "FFFACD", + "Lemon Ginger": "AC9E22", + "Lemon Grass": "9B9E8F", + "Light Apricot": "FDD5B1", + "Light Orchid": "E29CD2", + "Light Wisteria": "C9A0DC", + "Lightning Yellow": "FCC01E", + "Lilac": "C8A2C8", + "Lilac Bush": "9874D3", + "Lily": "C8AABF", + "Lily White": "E7F8FF", + "Lima": "76BD17", + "Lime": "BFFF00", + "Limeade": "6F9D02", + "Limed Ash": "747D63", + "Limed Oak": "AC8A56", + "Limed Spruce": "394851", + "Linen": "FAF0E6", + "Link Water": "D9E4F5", + "Lipstick": "AB0563", + "Lisbon Brown": "423921", + "Livid Brown": "4D282E", + "Loafer": "EEF4DE", + "Loblolly": "BDC9CE", + "Lochinvar": "2C8C84", + "Lochmara": "007EC7", + "Locust": "A8AF8E", + "Log Cabin": "242A1D", + "Logan": "AAA9CD", + "Lola": "DFCFDB", + "London Hue": "BEA6C3", + "Lonestar": "6D0101", + "Lotus": "863C3C", + "Loulou": "460B41", + "Lucky": "AF9F1C", + "Lucky Point": "1A1A68", + "Lunar Green": "3C493A", + "Luxor Gold": "A7882C", + "Lynch": "697E9A", + "Mabel": "D9F7FF", + "Macaroni and Cheese": "FFB97B", + "Madang": "B7F0BE", + "Madison": "09255D", + "Madras": "3F3002", + "Magenta / Fuchsia": "FF00FF", + "Magic Mint": "AAF0D1", + "Magnolia": "F8F4FF", + "Mahogany": "4E0606", + "Mai Tai": "B06608", + "Maize": "F5D5A0", + "Makara": "897D6D", + "Mako": "444954", + "Malachite": "0BDA51", + "Malibu": "7DC8F7", + "Mallard": "233418", + "Malta": "BDB2A1", + "Mamba": "8E8190", + "Manatee": "8D90A1", + "Mandalay": "AD781B", + "Mandy": "E25465", + "Mandys Pink": "F2C3B2", + "Mango Tango": "E77200", + "Manhattan": "F5C999", + "Mantis": "74C365", + "Mantle": "8B9C90", + "Manz": "EEEF78", + "Mardi Gras": "350036", + "Marigold": "B98D28", + "Marigold Yellow": "FBE870", + "Mariner": "286ACD", + "Maroon": "800000", + "Maroon Flush": "C32148", + "Maroon Oak": "520C17", + "Marshland": "0B0F08", + "Martini": "AFA09E", + "Martinique": "363050", + "Marzipan": "F8DB9D", + "Masala": "403B38", + "Matisse": "1B659D", + "Matrix": "B05D54", + "Matterhorn": "4E3B41", + "Mauve": "E0B0FF", + "Mauvelous": "F091A9", + "Maverick": "D8C2D5", + "Medium Carmine": "AF4035", + "Medium Purple": "9370DB", + "Medium Red Violet": "BB3385", + "Melanie": "E4C2D5", + "Melanzane": "300529", + "Melon": "FEBAAD", + "Melrose": "C7C1FF", + "Mercury": "E5E5E5", + "Merino": "F6F0E6", + "Merlin": "413C37", + "Merlot": "831923", + "Metallic Bronze": "49371B", + "Metallic Copper": "71291D", + "Meteor": "D07D12", + "Meteorite": "3C1F76", + "Mexican Red": "A72525", + "Mid Gray": "5F5F6E", + "Midnight": "011635", + "Midnight Blue": "003366", + "Midnight Moss": "041004", + "Mikado": "2D2510", + "Milan": "FAFFA4", + "Milano Red": "B81104", + "Milk Punch": "FFF6D4", + "Millbrook": "594433", + "Mimosa": "F8FDD3", + "Mindaro": "E3F988", + "Mine Shaft": "323232", + "Mineral Green": "3F5D53", + "Ming": "36747D", + "Minsk": "3F307F", + "Mint Green": "98FF98", + "Mint Julep": "F1EEC1", + "Mint Tulip": "C4F4EB", + "Mirage": "161928", + "Mischka": "D1D2DD", + "Mist Gray": "C4C4BC", + "Mobster": "7F7589", + "Moccaccino": "6E1D14", + "Mocha": "782D19", + "Mojo": "C04737", + "Mona Lisa": "FFA194", + "Monarch": "8B0723", + "Mondo": "4A3C30", + "Mongoose": "B5A27F", + "Monsoon": "8A8389", + "Monte Carlo": "83D0C6", + "Monza": "C7031E", + "Moody Blue": "7F76D3", + "Moon Glow": "FCFEDA", + "Moon Mist": "DCDDCC", + "Moon Raker": "D6CEF6", + "Morning Glory": "9EDEE0", + "Morocco Brown": "441D00", + "Mortar": "504351", + "Mosque": "036A6E", + "Moss Green": "ADDFAD", + "Mountain Meadow": "1AB385", + "Mountain Mist": "959396", + "Mountbatten Pink": "997A8D", + "Muddy Waters": "B78E5C", + "Muesli": "AA8B5B", + "Mulberry": "C54B8C", + "Mulberry Wood": "5C0536", + "Mule Fawn": "8C472F", + "Mulled Wine": "4E4562", + "Mustard": "FFDB58", + "My Pink": "D69188", + "My Sin": "FFB31F", + "Mystic": "E2EBED", + "Nandor": "4B5D52", + "Napa": "ACA494", + "Narvik": "EDF9F1", + "Natural Gray": "8B8680", + "Navajo White": "FFDEAD", + "Navy Blue": "000080", + "Nebula": "CBDBD6", + "Negroni": "FFE2C5", + "Neon Carrot": "FF9933", + "Nepal": "8EABC1", + "Neptune": "7CB7BB", + "Nero": "140600", + "Nevada": "646E75", + "New Orleans": "F3D69D", + "New York Pink": "D7837F", + "Niagara": "06A189", + "Night Rider": "1F120F", + "Night Shadz": "AA375A", + "Nile Blue": "193751", + "Nobel": "B7B1B1", + "Nomad": "BAB1A2", + "Norway": "A8BD9F", + "Nugget": "C59922", + "Nutmeg": "81422C", + "Nutmeg Wood Finish": "683600", + "Oasis": "FEEFCE", + "Observatory": "02866F", + "Ocean Green": "41AA78", + "Ochre": "CC7722", + "Off Green": "E6F8F3", + "Off Yellow": "FEF9E3", + "Oil": "281E15", + "Old Brick": "901E1E", + "Old Copper": "724A2F", + "Old Gold": "CFB53B", + "Old Lace": "FDF5E6", + "Old Lavender": "796878", + "Old Rose": "C08081", + "Olive": "808000", + "Olive Drab": "6B8E23", + "Olive Green": "B5B35C", + "Olive Haze": "8B8470", + "Olivetone": "716E10", + "Olivine": "9AB973", + "Onahau": "CDF4FF", + "Onion": "2F270E", + "Opal": "A9C6C2", + "Opium": "8E6F70", + "Oracle": "377475", + "Orange": "FF681F", + "Orange Peel": "FFA000", + "Orange Roughy": "C45719", + "Orange White": "FEFCED", + "Orchid": "DA70D6", + "Orchid White": "FFFDF3", + "Oregon": "9B4703", + "Orient": "015E85", + "Oriental Pink": "C69191", + "Orinoco": "F3FBD4", + "Oslo Gray": "878D91", + "Ottoman": "E9F8ED", + "Outer Space": "2D383A", + "Outrageous Orange": "FF6037", + "Oxford Blue": "384555", + "Oxley": "779E86", + "Oyster Bay": "DAFAFF", + "Oyster Pink": "E9CECD", + "Paarl": "A65529", + "Pablo": "776F61", + "Pacific Blue": "009DC4", + "Pacifika": "778120", + "Paco": "411F10", + "Padua": "ADE6C4", + "Pale Canary": "FFFF99", + "Pale Leaf": "C0D3B9", + "Pale Oyster": "988D77", + "Pale Prim": "FDFEB8", + "Pale Rose": "FFE1F2", + "Pale Sky": "6E7783", + "Pale Slate": "C3BFC1", + "Palm Green": "09230F", + "Palm Leaf": "19330E", + "Pampas": "F4F2EE", + "Panache": "EAF6EE", + "Pancho": "EDCDAB", + "Papaya Whip": "FFEFD5", + "Paprika": "8D0226", + "Paradiso": "317D82", + "Parchment": "F1E9D2", + "Paris Daisy": "FFF46E", + "Paris M": "26056A", + "Paris White": "CADCD4", + "Parsley": "134F19", + "Pastel Green": "77DD77", + "Pastel Pink": "FFD1DC", + "Patina": "639A8F", + "Pattens Blue": "DEF5FF", + "Paua": "260368", + "Pavlova": "D7C498", + "Peach": "FFE5B4", + "Peach Cream": "FFF0DB", + "Peach Orange": "FFCC99", + "Peach Schnapps": "FFDCD6", + "Peach Yellow": "FADFAD", + "Peanut": "782F16", + "Pear": "D1E231", + "Pearl Bush": "E8E0D5", + "Pearl Lusta": "FCF4DC", + "Peat": "716B56", + "Pelorous": "3EABBF", + "Peppermint": "E3F5E1", + "Perano": "A9BEF2", + "Perfume": "D0BEF8", + "Periglacial Blue": "E1E6D6", + "Periwinkle": "CCCCFF", + "Periwinkle Gray": "C3CDE6", + "Persian Blue": "1C39BB", + "Persian Green": "00A693", + "Persian Indigo": "32127A", + "Persian Pink": "F77FBE", + "Persian Plum": "701C1C", + "Persian Red": "CC3333", + "Persian Rose": "FE28A2", + "Persimmon": "FF6B53", + "Peru Tan": "7F3A02", + "Pesto": "7C7631", + "Petite Orchid": "DB9690", + "Pewter": "96A8A1", + "Pharlap": "A3807B", + "Picasso": "FFF39D", + "Pickled Bean": "6E4826", + "Pickled Bluewood": "314459", + "Picton Blue": "45B1E8", + "Pig Pink": "FDD7E4", + "Pigeon Post": "AFBDD9", + "Pigment Indigo": "4B0082", + "Pine Cone": "6D5E54", + "Pine Glade": "C7CD90", + "Pine Green": "01796F", + "Pine Tree": "171F04", + "Pink": "FFC0CB", + "Pink Flamingo": "FF66FF", + "Pink Flare": "E1C0C8", + "Pink Lace": "FFDDF4", + "Pink Lady": "FFF1D8", + "Pink Salmon": "FF91A4", + "Pink Swan": "BEB5B7", + "Piper": "C96323", + "Pipi": "FEF4CC", + "Pippin": "FFE1DF", + "Pirate Gold": "BA7F03", + "Pistachio": "9DC209", + "Pixie Green": "C0D8B6", + "Pizazz": "FF9000", + "Pizza": "C99415", + "Plantation": "27504B", + "Plum": "843179", + "Pohutukawa": "8F021C", + "Polar": "E5F9F6", + "Polo Blue": "8DA8CC", + "Pomegranate": "F34723", + "Pompadour": "660045", + "Porcelain": "EFF2F3", + "Porsche": "EAAE69", + "Port Gore": "251F4F", + "Portafino": "FFFFB4", + "Portage": "8B9FEE", + "Portica": "F9E663", + "Pot Pourri": "F5E7E2", + "Potters Clay": "8C5738", + "Powder Ash": "BCC9C2", + "Powder Blue": "B0E0E6", + "Prairie Sand": "9A3820", + "Prelude": "D0C0E5", + "Prim": "F0E2EC", + "Primrose": "EDEA99", + "Provincial Pink": "FEF5F1", + "Prussian Blue": "003153", + "Puce": "CC8899", + "Pueblo": "7D2C14", + "Puerto Rico": "3FC1AA", + "Pumice": "C2CAC4", + "Pumpkin": "FF7518", + "Pumpkin Skin": "B1610B", + "Punch": "DC4333", + "Punga": "4D3D14", + "Purple": "660099", + "Purple Heart": "652DC1", + "Purple Mountain's Majesty": "9678B6", + "Purple Pizzazz": "FF00CC", + "Putty": "E7CD8C", + "Quarter Pearl Lusta": "FFFDF4", + "Quarter Spanish White": "F7F2E1", + "Quicksand": "BD978E", + "Quill Gray": "D6D6D1", + "Quincy": "623F2D", + "Racing Green": "0C1911", + "Radical Red": "FF355E", + "Raffia": "EADAB8", + "Rainee": "B9C8AC", + "Rajah": "F7B668", + "Rangitoto": "2E3222", + "Rangoon Green": "1C1E13", + "Raven": "727B89", + "Raw Sienna": "D27D46", + "Raw Umber": "734A12", + "Razzle Dazzle Rose": "FF33CC", + "Razzmatazz": "E30B5C", + "Rebel": "3C1206", + "Red": "FF0000", + "Red Beech": "7B3801", + "Red Berry": "8E0000", + "Red Damask": "DA6A41", + "Red Devil": "860111", + "Red Orange": "FF3F34", + "Red Oxide": "6E0902", + "Red Ribbon": "ED0A3F", + "Red Robin": "80341F", + "Red Stage": "D05F04", + "Red Violet": "C71585", + "Redwood": "5D1E0F", + "Reef": "C9FFA2", + "Reef Gold": "9F821C", + "Regal Blue": "013F6A", + "Regent Gray": "86949F", + "Regent St Blue": "AAD6E6", + "Remy": "FEEBF3", + "Reno Sand": "A86515", + "Resolution Blue": "002387", + "Revolver": "2C1632", + "Rhino": "2E3F62", + "Rice Cake": "FFFEF0", + "Rice Flower": "EEFFE2", + "Rich Gold": "A85307", + "Rio Grande": "BBD009", + "Ripe Lemon": "F4D81C", + "Ripe Plum": "410056", + "Riptide": "8BE6D8", + "River Bed": "434C59", + "Rob Roy": "EAC674", + "Robin's Egg Blue": "00CCCC", + "Rock": "4D3833", + "Rock Blue": "9EB1CD", + "Rock Spray": "BA450C", + "Rodeo Dust": "C9B29B", + "Rolling Stone": "747D83", + "Roman": "DE6360", + "Roman Coffee": "795D4C", + "Romance": "FFFEFD", + "Romantic": "FFD2B7", + "Ronchi": "ECC54E", + "Roof Terracotta": "A62F20", + "Rope": "8E4D1E", + "Rose": "FF007F", + "Rose Bud": "FBB2A3", + "Rose Bud Cherry": "800B47", + "Rose Fog": "E7BCB4", + "Rose White": "FFF6F5", + "Rose of Sharon": "BF5500", + "Rosewood": "65000B", + "Roti": "C6A84B", + "Rouge": "A23B6C", + "Royal Blue": "4169E1", + "Royal Heath": "AB3472", + "Royal Purple": "6B3FA0", + "Rum": "796989", + "Rum Swizzle": "F9F8E4", + "Russet": "80461B", + "Russett": "755A57", + "Rust": "B7410E", + "Rustic Red": "480404", + "Rusty Nail": "86560A", + "Saddle": "4C3024", + "Saddle Brown": "583401", + "Saffron": "F4C430", + "Saffron Mango": "F9BF58", + "Sage": "9EA587", + "Sahara": "B7A214", + "Sahara Sand": "F1E788", + "Sail": "B8E0F9", + "Salem": "097F4B", + "Salmon": "FF8C69", + "Salomie": "FEDB8D", + "Salt Box": "685E6E", + "Saltpan": "F1F7F2", + "Sambuca": "3A2010", + "San Felix": "0B6207", + "San Juan": "304B6A", + "San Marino": "456CAC", + "Sand Dune": "826F65", + "Sandal": "AA8D6F", + "Sandrift": "AB917A", + "Sandstone": "796D62", + "Sandwisp": "F5E7A2", + "Sandy Beach": "FFEAC8", + "Sandy brown": "F4A460", + "Sangria": "92000A", + "Sanguine Brown": "8D3D38", + "Santa Fe": "B16D52", + "Santas Gray": "9FA0B1", + "Sapling": "DED4A4", + "Sapphire": "2F519E", + "Saratoga": "555B10", + "Satin Linen": "E6E4D4", + "Sauvignon": "FFF5F3", + "Sazerac": "FFF4E0", + "Scampi": "675FA6", + "Scandal": "CFFAF4", + "Scarlet": "FF2400", + "Scarlet Gum": "431560", + "Scarlett": "950015", + "Scarpa Flow": "585562", + "Schist": "A9B497", + "School bus Yellow": "FFD800", + "Schooner": "8B847E", + "Science Blue": "0066CC", + "Scooter": "2EBFD4", + "Scorpion": "695F62", + "Scotch Mist": "FFFBDC", + "Screamin' Green": "66FF66", + "Sea Buckthorn": "FBA129", + "Sea Green": "2E8B57", + "Sea Mist": "C5DBCA", + "Sea Nymph": "78A39C", + "Sea Pink": "ED989E", + "Seagull": "80CCEA", + "Seance": "731E8F", + "Seashell": "F1F1F1", + "Seashell Peach": "FFF5EE", + "Seaweed": "1B2F11", + "Selago": "F0EEFD", + "Selective Yellow": "FFBA00", + "Sepia": "704214", + "Sepia Black": "2B0202", + "Sepia Skin": "9E5B40", + "Serenade": "FFF4E8", + "Shadow": "837050", + "Shadow Green": "9AC2B8", + "Shady Lady": "AAA5A9", + "Shakespeare": "4EABD1", + "Shalimar": "FBFFBA", + "Shamrock": "33CC99", + "Shark": "25272C", + "Sherpa Blue": "004950", + "Sherwood Green": "02402C", + "Shilo": "E8B9B3", + "Shingle Fawn": "6B4E31", + "Ship Cove": "788BBA", + "Ship Gray": "3E3A44", + "Shiraz": "B20931", + "Shocking": "E292C0", + "Shocking Pink": "FC0FC0", + "Shuttle Gray": "5F6672", + "Siam": "646A54", + "Sidecar": "F3E7BB", + "Silk": "BDB1A8", + "Silver": "C0C0C0", + "Silver Chalice": "ACACAC", + "Silver Rust": "C9C0BB", + "Silver Sand": "BFC1C2", + "Silver Tree": "66B58F", + "Sinbad": "9FD7D3", + "Siren": "7A013A", + "Sirocco": "718080", + "Sisal": "D3CBBA", + "Skeptic": "CAE6DA", + "Sky Blue": "76D7EA", + "Slate Gray": "708090", + "Smalt": "003399", + "Smalt Blue": "51808F", + "Smoky": "605B73", + "Snow Drift": "F7FAF7", + "Snow Flurry": "E4FFD1", + "Snowy Mint": "D6FFDB", + "Snuff": "E2D8ED", + "Soapstone": "FFFBF9", + "Soft Amber": "D1C6B4", + "Soft Peach": "F5EDEF", + "Solid Pink": "893843", + "Solitaire": "FEF8E2", + "Solitude": "EAF6FF", + "Sorbus": "FD7C07", + "Sorrell Brown": "CEB98F", + "Soya Bean": "6A6051", + "Spanish Green": "819885", + "Spectra": "2F5A57", + "Spice": "6A442E", + "Spicy Mix": "885342", + "Spicy Mustard": "74640D", + "Spicy Pink": "816E71", + "Spindle": "B6D1EA", + "Spray": "79DEEC", + "Spring Green": "00FF7F", + "Spring Leaves": "578363", + "Spring Rain": "ACCBB1", + "Spring Sun": "F6FFDC", + "Spring Wood": "F8F6F1", + "Sprout": "C1D7B0", + "Spun Pearl": "AAABB7", + "Squirrel": "8F8176", + "St Tropaz": "2D569B", + "Stack": "8A8F8A", + "Star Dust": "9F9F9C", + "Stark White": "E5D7BD", + "Starship": "ECF245", + "Steel Blue": "4682B4", + "Steel Gray": "262335", + "Stiletto": "9C3336", + "Stonewall": "928573", + "Storm Dust": "646463", + "Storm Gray": "717486", + "Stratos": "000741", + "Straw": "D4BF8D", + "Strikemaster": "956387", + "Stromboli": "325D52", + "Studio": "714AB2", + "Submarine": "BAC7C9", + "Sugar Cane": "F9FFF6", + "Sulu": "C1F07C", + "Summer Green": "96BBAB", + "Sun": "FBAC13", + "Sundance": "C9B35B", + "Sundown": "FFB1B3", + "Sunflower": "E4D422", + "Sunglo": "E16865", + "Sunglow": "FFCC33", + "Sunset Orange": "FE4C40", + "Sunshade": "FF9E2C", + "Supernova": "FFC901", + "Surf": "BBD7C1", + "Surf Crest": "CFE5D2", + "Surfie Green": "0C7A79", + "Sushi": "87AB39", + "Suva Gray": "888387", + "Swamp": "001B1C", + "Swamp Green": "ACB78E", + "Swans Down": "DCF0EA", + "Sweet Corn": "FBEA8C", + "Sweet Pink": "FD9FA2", + "Swirl": "D3CDC5", + "Swiss Coffee": "DDD6D5", + "Sycamore": "908D39", + "Tabasco": "A02712", + "Tacao": "EDB381", + "Tacha": "D6C562", + "Tahiti Gold": "E97C07", + "Tahuna Sands": "EEF0C8", + "Tall Poppy": "B32D29", + "Tallow": "A8A589", + "Tamarillo": "991613", + "Tamarind": "341515", + "Tan": "D2B48C", + "Tan Hide": "FA9D5A", + "Tana": "D9DCC1", + "Tangaroa": "03163C", + "Tangerine": "F28500", + "Tango": "ED7A1C", + "Tapa": "7B7874", + "Tapestry": "B05E81", + "Tara": "E1F6E8", + "Tarawera": "073A50", + "Tasman": "CFDCCF", + "Taupe": "483C32", + "Taupe Gray": "B3AF95", + "Tawny Port": "692545", + "Te Papa Green": "1E433C", + "Tea": "C1BAB0", + "Tea Green": "D0F0C0", + "Teak": "B19461", + "Teal": "008080", + "Teal Blue": "044259", + "Temptress": "3B000B", + "Tenn": "CD5700", + "Tequila": "FFE6C7", + "Terracotta": "E2725B", + "Texas": "F8F99C", + "Texas Rose": "FFB555", + "Thatch": "B69D98", + "Thatch Green": "403D19", + "Thistle": "D8BFD8", + "Thistle Green": "CCCAA8", + "Thunder": "33292F", + "Thunderbird": "C02B18", + "Tia Maria": "C1440E", + "Tiara": "C3D1D1", + "Tiber": "063537", + "Tickle Me Pink": "FC80A5", + "Tidal": "F1FFAD", + "Tide": "BFB8B0", + "Timber Green": "16322C", + "Timberwolf": "D9D6CF", + "Titan White": "F0EEFF", + "Toast": "9A6E61", + "Tobacco Brown": "715D47", + "Toledo": "3A0020", + "Tolopea": "1B0245", + "Tom Thumb": "3F583B", + "Tonys Pink": "E79F8C", + "Topaz": "7C778A", + "Torch Red": "FD0E35", + "Torea Bay": "0F2D9E", + "Tory Blue": "1450AA", + "Tosca": "8D3F3F", + "Totem Pole": "991B07", + "Tower Gray": "A9BDBF", + "Tradewind": "5FB3AC", + "Tranquil": "E6FFFF", + "Travertine": "FFFDE8", + "Tree Poppy": "FC9C1D", + "Treehouse": "3B2820", + "Trendy Green": "7C881A", + "Trendy Pink": "8C6495", + "Trinidad": "E64E03", + "Tropical Blue": "C3DDF9", + "Tropical Rain Forest": "00755E", + "Trout": "4A4E5A", + "True V": "8A73D6", + "Tuatara": "363534", + "Tuft Bush": "FFDDCD", + "Tulip Tree": "EAB33B", + "Tumbleweed": "DEA681", + "Tuna": "353542", + "Tundora": "4A4244", + "Turbo": "FAE600", + "Turkish Rose": "B57281", + "Turmeric": "CABB48", + "Turquoise": "30D5C8", + "Turquoise Blue": "6CDAE7", + "Turtle Green": "2A380B", + "Tuscany": "BD5E2E", + "Tusk": "EEF3C3", + "Tussock": "C5994B", + "Tutu": "FFF1F9", + "Twilight": "E4CFDE", + "Twilight Blue": "EEFDFF", + "Twine": "C2955D", + "Tyrian Purple": "66023C", + "Ultramarine": "120A8F", + "Valencia": "D84437", + "Valentino": "350E42", + "Valhalla": "2B194F", + "Van Cleef": "49170C", + "Vanilla": "D1BEA8", + "Vanilla Ice": "F3D9DF", + "Varden": "FFF6DF", + "Venetian Red": "72010F", + "Venice Blue": "055989", + "Venus": "928590", + "Verdigris": "5D5E37", + "Verdun Green": "495400", + "Vermilion": "FF4D00", + "Vesuvius": "B14A0B", + "Victoria": "534491", + "Vida Loca": "549019", + "Viking": "64CCDB", + "Vin Rouge": "983D61", + "Viola": "CB8FA9", + "Violent Violet": "290C5E", + "Violet": "240A40", + "Violet Eggplant": "991199", + "Violet Red": "F7468A", + "Viridian": "40826D", + "Viridian Green": "678975", + "Vis Vis": "FFEFA1", + "Vista Blue": "8FD6B4", + "Vista White": "FCF8F7", + "Vivid Tangerine": "FF9980", + "Vivid Violet": "803790", + "Voodoo": "533455", + "Vulcan": "10121D", + "Wafer": "DECBC6", + "Waikawa Gray": "5A6E9C", + "Waiouru": "363C0D", + "Walnut": "773F1A", + "Wasabi": "788A25", + "Water Leaf": "A1E9DE", + "Watercourse": "056F57", + "Waterloo ": "7B7C94", + "Wattle": "DCD747", + "Watusi": "FFDDCF", + "Wax Flower": "FFC0A8", + "We Peep": "F7DBE6", + "Web Orange": "FFA500", + "Wedgewood": "4E7F9E", + "Well Read": "B43332", + "West Coast": "625119", + "West Side": "FF910F", + "Westar": "DCD9D2", + "Wewak": "F19BAB", + "Wheat": "F5DEB3", + "Wheatfield": "F3EDCF", + "Whiskey": "D59A6F", + "Whisper": "F7F5FA", + "White": "FFFFFF", + "White Ice": "DDF9F1", + "White Lilac": "F8F7FC", + "White Linen": "F8F0E8", + "White Pointer": "FEF8FF", + "White Rock": "EAE8D4", + "Wild Blue Yonder": "7A89B8", + "Wild Rice": "ECE090", + "Wild Sand": "F4F4F4", + "Wild Strawberry": "FF3399", + "Wild Watermelon": "FD5B78", + "Wild Willow": "B9C46A", + "William": "3A686C", + "Willow Brook": "DFECDA", + "Willow Grove": "65745D", + "Windsor": "3C0878", + "Wine Berry": "591D35", + "Winter Hazel": "D5D195", + "Wisp Pink": "FEF4F8", + "Wisteria": "9771B5", + "Wistful": "A4A6D3", + "Witch Haze": "FFFC99", + "Wood Bark": "261105", + "Woodland": "4D5328", + "Woodrush": "302A0F", + "Woodsmoke": "0C0D0F", + "Woody Brown": "483131", + "Xanadu": "738678", + "Yellow": "FFFF00", + "Yellow Green": "C5E17A", + "Yellow Metal": "716338", + "Yellow Orange": "FFAE42", + "Yellow Sea": "FEA904", + "Your Pink": "FFC3C0", + "Yukon Gold": "7B6608", + "Yuma": "CEC291", + "Zambezi": "685558", + "Zanah": "DAECD6", + "Zest": "E5841B", + "Zeus": "292319", + "Ziggurat": "BFDBE2", + "Zinnwaldite": "EBC2AF", + "Zircon": "F4F8FF", + "Zombie": "E4D69B", + "Zorba": "A59B91", + "Zuccini": "044022", + "Zumthor": "EDF6FF" +} -- cgit v1.2.3 From 3fdbed208356f875464fc21e3f1d153a7daa0359 Mon Sep 17 00:00:00 2001 From: CyberCitizen01 Date: Sun, 5 Sep 2021 20:31:53 +0530 Subject: Fix issues occured while deploying [no ci] - import PIL is the way to import pillow. - discord-flags isn't being used yet. - Fixed some of the linting issues. --- bot/exts/evergreen/color.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bot/exts/evergreen/color.py b/bot/exts/evergreen/color.py index a00a956b..dd922bf9 100644 --- a/bot/exts/evergreen/color.py +++ b/bot/exts/evergreen/color.py @@ -1,16 +1,18 @@ # imports +import colorsys import logging -import colorsys -import pillow +import PIL from discord import Embed -# ! need to install discord-flags and add to poetry.lock file -from discord.ext import commands, flags +from discord.ext import commands from rapidfuzz import process from bot.bot import Bot from bot.constants import Colours +# Planning to use discord-flags, hence require changes to poetry.lock file +# from discord.ext import flags + logger = logging.getLogger(__name__) # constants if needed -- cgit v1.2.3 From 8056ce1c266a7b91c2a905fc7935d9a00b46ffbe Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Sun, 5 Sep 2021 18:39:23 -0400 Subject: Move to bot/exts/fun folder to update restructure --- bot/exts/fun/color.py | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 bot/exts/fun/color.py diff --git a/bot/exts/fun/color.py b/bot/exts/fun/color.py new file mode 100644 index 00000000..dd922bf9 --- /dev/null +++ b/bot/exts/fun/color.py @@ -0,0 +1,115 @@ +# imports +import colorsys +import logging + +import PIL +from discord import Embed +from discord.ext import commands +from rapidfuzz import process + +from bot.bot import Bot +from bot.constants import Colours + +# Planning to use discord-flags, hence require changes to poetry.lock file +# from discord.ext import flags + +logger = logging.getLogger(__name__) + +# constants if needed +# Color URLs - will be replaced by JSON file? +COLOR_JSON_PATH = ".bot//exts//resources//evergreen//" +COLOR_URL_XKCD = "https://xkcd.com/color/rgb/" +COLOR_URL_NAME_THAT_COLOR = "https://github.com/ryanzec/name-that-color/blob/master/lib/ntc.js#L116-L1681" + + +COLOR_ERROR = Embed( + title="Input color is not possible", + description="The color code {user_color} is not a possible color combination." + "\nThe range of possible values are: " + "\nRGB & HSV: 0-255" + "\nCMYK: 0-100%" + "\nHSL: 0-360 degrees" + "\nHex: #000000-#FFFFFF" +) +COLOR_EMBED = Embed( + title="{color_name}", + description="RGB" + "\n{RGB}" + "\nHSV" + "\n{HSV}" + "\nCMYK" + "\n{CMYK}" + "\nHSL" + "\n{HSL}" + "\nHex" + "\n{Hex}" +) + + +# define color command +class Color(commands.cog): + """User initiated command to receive color information.""" + + def __init__(self, bot: Bot): + self.bot = bot + + # ? possible to use discord-flags to allow user to decide on color + # https://pypi.org/project/discord-flags/ + # @flags.add_flag("--rgb", type=str) + # @flags.add_flag("--hsv", type=str) + # @flags.add_flag("--cmyk", type=str) + # @flags.add_flag("--hsl", type=str) + # @flags.add_flag("--hex", type=str) + # @flags.add_flag("--name", type=str) + # @flags.command() + @commands.command(aliases=["color", "colour"]) + @commands.cooldown(1, 10, commands.cooldowns.BucketType.user) + async def color(self, ctx: commands.Context, *, user_color: str) -> None: + """Send information on input color code or color name.""" + # need to check if user_color is RGB, HSV, CMYK, HSL, Hex or color name + # should we assume the color is RGB if not defined? + # should discord tags be used? + # need to review discord.py V2.0 + + # TODO code to check if color code is possible + await ctx.send(embed=COLOR_ERROR.format(color=user_color)) + # await ctx.send(embed=COLOR_EMBED.format( + # RGB=color_dict["RGB"], + # HSV=color_dict["HSV"], + # HSL=color_dict["HSL"], + # CMYK=color_dict["CMYK"], + # HSL=color_dict["HSL"], + # Hex=color_dict["Hex"], + # color_name=color_dict["color_name"] + # ).set_image() # url for image? + # ) + + # TODO pass for now + pass + + # if user_color in color_lists: + # # TODO fuzzy match for color + # pass + + async def color_converter(self, color: str, code_type: str) -> dict: + """Generate alternative color codes for use in the embed.""" + # TODO add code to take color and code type and return other types + # color_dict = { + # "RGB": color_RGB, + # "HSV": color_HSV, + # "HSL": color_HSL, + # "CMYK": color_CMYK, + # "HSL": color_HSL, + # "Hex": color_Hex, + # "color_name": color_name, + # } + pass + + async def photo_generator(self, color: str) -> None: + """Generate photo to use in embed.""" + # TODO need to find a way to store photo in cache to add to embed, then remove + + +def setup(bot: Bot) -> None: + """Load the Color Cog.""" + bot.add_cog(Color(bot)) -- cgit v1.2.3 From 690040ba9cdde3c973a7a89db6eb752bc29ebeee Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Sun, 5 Sep 2021 18:41:23 -0400 Subject: Move to utilities folder as recommended by Xith --- bot/exts/utilities/color.py | 115 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 bot/exts/utilities/color.py diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py new file mode 100644 index 00000000..dd922bf9 --- /dev/null +++ b/bot/exts/utilities/color.py @@ -0,0 +1,115 @@ +# imports +import colorsys +import logging + +import PIL +from discord import Embed +from discord.ext import commands +from rapidfuzz import process + +from bot.bot import Bot +from bot.constants import Colours + +# Planning to use discord-flags, hence require changes to poetry.lock file +# from discord.ext import flags + +logger = logging.getLogger(__name__) + +# constants if needed +# Color URLs - will be replaced by JSON file? +COLOR_JSON_PATH = ".bot//exts//resources//evergreen//" +COLOR_URL_XKCD = "https://xkcd.com/color/rgb/" +COLOR_URL_NAME_THAT_COLOR = "https://github.com/ryanzec/name-that-color/blob/master/lib/ntc.js#L116-L1681" + + +COLOR_ERROR = Embed( + title="Input color is not possible", + description="The color code {user_color} is not a possible color combination." + "\nThe range of possible values are: " + "\nRGB & HSV: 0-255" + "\nCMYK: 0-100%" + "\nHSL: 0-360 degrees" + "\nHex: #000000-#FFFFFF" +) +COLOR_EMBED = Embed( + title="{color_name}", + description="RGB" + "\n{RGB}" + "\nHSV" + "\n{HSV}" + "\nCMYK" + "\n{CMYK}" + "\nHSL" + "\n{HSL}" + "\nHex" + "\n{Hex}" +) + + +# define color command +class Color(commands.cog): + """User initiated command to receive color information.""" + + def __init__(self, bot: Bot): + self.bot = bot + + # ? possible to use discord-flags to allow user to decide on color + # https://pypi.org/project/discord-flags/ + # @flags.add_flag("--rgb", type=str) + # @flags.add_flag("--hsv", type=str) + # @flags.add_flag("--cmyk", type=str) + # @flags.add_flag("--hsl", type=str) + # @flags.add_flag("--hex", type=str) + # @flags.add_flag("--name", type=str) + # @flags.command() + @commands.command(aliases=["color", "colour"]) + @commands.cooldown(1, 10, commands.cooldowns.BucketType.user) + async def color(self, ctx: commands.Context, *, user_color: str) -> None: + """Send information on input color code or color name.""" + # need to check if user_color is RGB, HSV, CMYK, HSL, Hex or color name + # should we assume the color is RGB if not defined? + # should discord tags be used? + # need to review discord.py V2.0 + + # TODO code to check if color code is possible + await ctx.send(embed=COLOR_ERROR.format(color=user_color)) + # await ctx.send(embed=COLOR_EMBED.format( + # RGB=color_dict["RGB"], + # HSV=color_dict["HSV"], + # HSL=color_dict["HSL"], + # CMYK=color_dict["CMYK"], + # HSL=color_dict["HSL"], + # Hex=color_dict["Hex"], + # color_name=color_dict["color_name"] + # ).set_image() # url for image? + # ) + + # TODO pass for now + pass + + # if user_color in color_lists: + # # TODO fuzzy match for color + # pass + + async def color_converter(self, color: str, code_type: str) -> dict: + """Generate alternative color codes for use in the embed.""" + # TODO add code to take color and code type and return other types + # color_dict = { + # "RGB": color_RGB, + # "HSV": color_HSV, + # "HSL": color_HSL, + # "CMYK": color_CMYK, + # "HSL": color_HSL, + # "Hex": color_Hex, + # "color_name": color_name, + # } + pass + + async def photo_generator(self, color: str) -> None: + """Generate photo to use in embed.""" + # TODO need to find a way to store photo in cache to add to embed, then remove + + +def setup(bot: Bot) -> None: + """Load the Color Cog.""" + bot.add_cog(Color(bot)) -- cgit v1.2.3 From cfa2a0c6b985050d11d98badf656a3f2e74efded Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Sun, 5 Sep 2021 23:07:27 -0400 Subject: Remove old files --- bot/exts/evergreen/color.py | 115 -------------------------------------------- bot/exts/fun/color.py | 115 -------------------------------------------- 2 files changed, 230 deletions(-) delete mode 100644 bot/exts/evergreen/color.py delete mode 100644 bot/exts/fun/color.py diff --git a/bot/exts/evergreen/color.py b/bot/exts/evergreen/color.py deleted file mode 100644 index dd922bf9..00000000 --- a/bot/exts/evergreen/color.py +++ /dev/null @@ -1,115 +0,0 @@ -# imports -import colorsys -import logging - -import PIL -from discord import Embed -from discord.ext import commands -from rapidfuzz import process - -from bot.bot import Bot -from bot.constants import Colours - -# Planning to use discord-flags, hence require changes to poetry.lock file -# from discord.ext import flags - -logger = logging.getLogger(__name__) - -# constants if needed -# Color URLs - will be replaced by JSON file? -COLOR_JSON_PATH = ".bot//exts//resources//evergreen//" -COLOR_URL_XKCD = "https://xkcd.com/color/rgb/" -COLOR_URL_NAME_THAT_COLOR = "https://github.com/ryanzec/name-that-color/blob/master/lib/ntc.js#L116-L1681" - - -COLOR_ERROR = Embed( - title="Input color is not possible", - description="The color code {user_color} is not a possible color combination." - "\nThe range of possible values are: " - "\nRGB & HSV: 0-255" - "\nCMYK: 0-100%" - "\nHSL: 0-360 degrees" - "\nHex: #000000-#FFFFFF" -) -COLOR_EMBED = Embed( - title="{color_name}", - description="RGB" - "\n{RGB}" - "\nHSV" - "\n{HSV}" - "\nCMYK" - "\n{CMYK}" - "\nHSL" - "\n{HSL}" - "\nHex" - "\n{Hex}" -) - - -# define color command -class Color(commands.cog): - """User initiated command to receive color information.""" - - def __init__(self, bot: Bot): - self.bot = bot - - # ? possible to use discord-flags to allow user to decide on color - # https://pypi.org/project/discord-flags/ - # @flags.add_flag("--rgb", type=str) - # @flags.add_flag("--hsv", type=str) - # @flags.add_flag("--cmyk", type=str) - # @flags.add_flag("--hsl", type=str) - # @flags.add_flag("--hex", type=str) - # @flags.add_flag("--name", type=str) - # @flags.command() - @commands.command(aliases=["color", "colour"]) - @commands.cooldown(1, 10, commands.cooldowns.BucketType.user) - async def color(self, ctx: commands.Context, *, user_color: str) -> None: - """Send information on input color code or color name.""" - # need to check if user_color is RGB, HSV, CMYK, HSL, Hex or color name - # should we assume the color is RGB if not defined? - # should discord tags be used? - # need to review discord.py V2.0 - - # TODO code to check if color code is possible - await ctx.send(embed=COLOR_ERROR.format(color=user_color)) - # await ctx.send(embed=COLOR_EMBED.format( - # RGB=color_dict["RGB"], - # HSV=color_dict["HSV"], - # HSL=color_dict["HSL"], - # CMYK=color_dict["CMYK"], - # HSL=color_dict["HSL"], - # Hex=color_dict["Hex"], - # color_name=color_dict["color_name"] - # ).set_image() # url for image? - # ) - - # TODO pass for now - pass - - # if user_color in color_lists: - # # TODO fuzzy match for color - # pass - - async def color_converter(self, color: str, code_type: str) -> dict: - """Generate alternative color codes for use in the embed.""" - # TODO add code to take color and code type and return other types - # color_dict = { - # "RGB": color_RGB, - # "HSV": color_HSV, - # "HSL": color_HSL, - # "CMYK": color_CMYK, - # "HSL": color_HSL, - # "Hex": color_Hex, - # "color_name": color_name, - # } - pass - - async def photo_generator(self, color: str) -> None: - """Generate photo to use in embed.""" - # TODO need to find a way to store photo in cache to add to embed, then remove - - -def setup(bot: Bot) -> None: - """Load the Color Cog.""" - bot.add_cog(Color(bot)) diff --git a/bot/exts/fun/color.py b/bot/exts/fun/color.py deleted file mode 100644 index dd922bf9..00000000 --- a/bot/exts/fun/color.py +++ /dev/null @@ -1,115 +0,0 @@ -# imports -import colorsys -import logging - -import PIL -from discord import Embed -from discord.ext import commands -from rapidfuzz import process - -from bot.bot import Bot -from bot.constants import Colours - -# Planning to use discord-flags, hence require changes to poetry.lock file -# from discord.ext import flags - -logger = logging.getLogger(__name__) - -# constants if needed -# Color URLs - will be replaced by JSON file? -COLOR_JSON_PATH = ".bot//exts//resources//evergreen//" -COLOR_URL_XKCD = "https://xkcd.com/color/rgb/" -COLOR_URL_NAME_THAT_COLOR = "https://github.com/ryanzec/name-that-color/blob/master/lib/ntc.js#L116-L1681" - - -COLOR_ERROR = Embed( - title="Input color is not possible", - description="The color code {user_color} is not a possible color combination." - "\nThe range of possible values are: " - "\nRGB & HSV: 0-255" - "\nCMYK: 0-100%" - "\nHSL: 0-360 degrees" - "\nHex: #000000-#FFFFFF" -) -COLOR_EMBED = Embed( - title="{color_name}", - description="RGB" - "\n{RGB}" - "\nHSV" - "\n{HSV}" - "\nCMYK" - "\n{CMYK}" - "\nHSL" - "\n{HSL}" - "\nHex" - "\n{Hex}" -) - - -# define color command -class Color(commands.cog): - """User initiated command to receive color information.""" - - def __init__(self, bot: Bot): - self.bot = bot - - # ? possible to use discord-flags to allow user to decide on color - # https://pypi.org/project/discord-flags/ - # @flags.add_flag("--rgb", type=str) - # @flags.add_flag("--hsv", type=str) - # @flags.add_flag("--cmyk", type=str) - # @flags.add_flag("--hsl", type=str) - # @flags.add_flag("--hex", type=str) - # @flags.add_flag("--name", type=str) - # @flags.command() - @commands.command(aliases=["color", "colour"]) - @commands.cooldown(1, 10, commands.cooldowns.BucketType.user) - async def color(self, ctx: commands.Context, *, user_color: str) -> None: - """Send information on input color code or color name.""" - # need to check if user_color is RGB, HSV, CMYK, HSL, Hex or color name - # should we assume the color is RGB if not defined? - # should discord tags be used? - # need to review discord.py V2.0 - - # TODO code to check if color code is possible - await ctx.send(embed=COLOR_ERROR.format(color=user_color)) - # await ctx.send(embed=COLOR_EMBED.format( - # RGB=color_dict["RGB"], - # HSV=color_dict["HSV"], - # HSL=color_dict["HSL"], - # CMYK=color_dict["CMYK"], - # HSL=color_dict["HSL"], - # Hex=color_dict["Hex"], - # color_name=color_dict["color_name"] - # ).set_image() # url for image? - # ) - - # TODO pass for now - pass - - # if user_color in color_lists: - # # TODO fuzzy match for color - # pass - - async def color_converter(self, color: str, code_type: str) -> dict: - """Generate alternative color codes for use in the embed.""" - # TODO add code to take color and code type and return other types - # color_dict = { - # "RGB": color_RGB, - # "HSV": color_HSV, - # "HSL": color_HSL, - # "CMYK": color_CMYK, - # "HSL": color_HSL, - # "Hex": color_Hex, - # "color_name": color_name, - # } - pass - - async def photo_generator(self, color: str) -> None: - """Generate photo to use in embed.""" - # TODO need to find a way to store photo in cache to add to embed, then remove - - -def setup(bot: Bot) -> None: - """Load the Color Cog.""" - bot.add_cog(Color(bot)) -- cgit v1.2.3 From 0d7bbafe9bf18ef9d1dda65bab5a5ae699d97d72 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 6 Sep 2021 23:48:34 +0200 Subject: Move logging to a separate module --- bot/__init__.py | 63 ++------------------------------------------------------- bot/log.py | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 61 deletions(-) create mode 100644 bot/log.py diff --git a/bot/__init__.py b/bot/__init__.py index c6a48105..b64f4732 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -6,80 +6,21 @@ except ModuleNotFoundError: pass import asyncio -import logging -import logging.handlers import os from functools import partial, partialmethod -from pathlib import Path import arrow from discord.ext import commands +from bot import log from bot.command import Command -from bot.constants import Client from bot.group import Group - -# Configure the "TRACE" logging level (e.g. "log.trace(message)") -logging.TRACE = 5 -logging.addLevelName(logging.TRACE, "TRACE") - - -def monkeypatch_trace(self: logging.Logger, msg: str, *args, **kwargs) -> None: - """ - Log 'msg % args' with severity 'TRACE'. - - To pass exception information, use the keyword argument exc_info with a true value, e.g. - logger.trace("Houston, we have an %s", "interesting problem", exc_info=1) - """ - if self.isEnabledFor(logging.TRACE): - self._log(logging.TRACE, msg, args, **kwargs) - - -logging.Logger.trace = monkeypatch_trace +log.setup() # Set timestamp of when execution started (approximately) start_time = arrow.utcnow() -# Set up file logging -log_dir = Path("bot/log") -log_file = log_dir / "hackbot.log" -os.makedirs(log_dir, exist_ok=True) - -# File handler rotates logs every 5 MB -file_handler = logging.handlers.RotatingFileHandler( - log_file, maxBytes=5 * (2**20), backupCount=10, encoding="utf-8", -) -file_handler.setLevel(logging.TRACE if Client.debug else logging.DEBUG) - -# Console handler prints to terminal -console_handler = logging.StreamHandler() -level = logging.TRACE if Client.debug else logging.INFO -console_handler.setLevel(level) - -# Remove old loggers, if any -root = logging.getLogger() -if root.handlers: - for handler in root.handlers: - root.removeHandler(handler) - -# Silence irrelevant loggers -logging.getLogger("discord").setLevel(logging.ERROR) -logging.getLogger("websockets").setLevel(logging.ERROR) -logging.getLogger("PIL").setLevel(logging.ERROR) -logging.getLogger("matplotlib").setLevel(logging.ERROR) -logging.getLogger("async_rediscache").setLevel(logging.WARNING) - -# Setup new logging configuration -logging.basicConfig( - format="%(asctime)s - %(name)s %(levelname)s: %(message)s", - datefmt="%D %H:%M:%S", - level=logging.TRACE if Client.debug else logging.DEBUG, - handlers=[console_handler, file_handler], -) -logging.getLogger().info("Logging initialization complete") - - # On Windows, the selector event loop is required for aiodns. if os.name == "nt": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) diff --git a/bot/log.py b/bot/log.py new file mode 100644 index 00000000..1f4b9159 --- /dev/null +++ b/bot/log.py @@ -0,0 +1,63 @@ +import logging +import logging.handlers +import os +from pathlib import Path + +from bot.constants import Client + + +def setup() -> None: + """Set up loggers.""" + # Configure the "TRACE" logging level (e.g. "log.trace(message)") + logging.TRACE = 5 + logging.addLevelName(logging.TRACE, "TRACE") + logging.Logger.trace = _monkeypatch_trace + + # Set up file logging + log_dir = Path("bot/log") + log_file = log_dir / "hackbot.log" + os.makedirs(log_dir, exist_ok=True) + + # File handler rotates logs every 5 MB + file_handler = logging.handlers.RotatingFileHandler( + log_file, maxBytes=5 * (2 ** 20), backupCount=10, encoding="utf-8", + ) + file_handler.setLevel(logging.TRACE if Client.debug else logging.DEBUG) + + # Console handler prints to terminal + console_handler = logging.StreamHandler() + level = logging.TRACE if Client.debug else logging.INFO + console_handler.setLevel(level) + + # Remove old loggers, if any + root = logging.getLogger() + if root.handlers: + for handler in root.handlers: + root.removeHandler(handler) + + # Silence irrelevant loggers + logging.getLogger("discord").setLevel(logging.ERROR) + logging.getLogger("websockets").setLevel(logging.ERROR) + logging.getLogger("PIL").setLevel(logging.ERROR) + logging.getLogger("matplotlib").setLevel(logging.ERROR) + logging.getLogger("async_rediscache").setLevel(logging.WARNING) + + # Setup new logging configuration + logging.basicConfig( + format="%(asctime)s - %(name)s %(levelname)s: %(message)s", + datefmt="%D %H:%M:%S", + level=logging.TRACE if Client.debug else logging.DEBUG, + handlers=[console_handler, file_handler], + ) + logging.getLogger().info("Logging initialization complete") + + +def _monkeypatch_trace(self: logging.Logger, msg: str, *args, **kwargs) -> None: + """ + Log 'msg % args' with severity 'TRACE'. + + To pass exception information, use the keyword argument exc_info with a true value, e.g. + logger.trace("Houston, we have an %s", "interesting problem", exc_info=1) + """ + if self.isEnabledFor(logging.TRACE): + self._log(logging.TRACE, msg, args, **kwargs) -- cgit v1.2.3 From c2c0aefae5d80f9daaecb4dcd2bcd776c14660cf Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 6 Sep 2021 23:52:22 +0200 Subject: Move logging to a top level logs directory and update file name --- bot/log.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bot/log.py b/bot/log.py index 1f4b9159..c8de957e 100644 --- a/bot/log.py +++ b/bot/log.py @@ -1,6 +1,5 @@ import logging import logging.handlers -import os from pathlib import Path from bot.constants import Client @@ -14,9 +13,8 @@ def setup() -> None: logging.Logger.trace = _monkeypatch_trace # Set up file logging - log_dir = Path("bot/log") - log_file = log_dir / "hackbot.log" - os.makedirs(log_dir, exist_ok=True) + log_file = Path("logs/sir-lancebot.log") + log_file.parent.mkdir(exist_ok=True) # File handler rotates logs every 5 MB file_handler = logging.handlers.RotatingFileHandler( -- cgit v1.2.3 From 9f2dcc30433d90739bd8a9bbc8f50fabbcd7d7fe Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 6 Sep 2021 23:53:49 +0200 Subject: Remove handler cleanup This doesn't seem to do anything as no handlers are set up beforehand --- bot/log.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/bot/log.py b/bot/log.py index c8de957e..0e0c22f0 100644 --- a/bot/log.py +++ b/bot/log.py @@ -27,12 +27,6 @@ def setup() -> None: level = logging.TRACE if Client.debug else logging.INFO console_handler.setLevel(level) - # Remove old loggers, if any - root = logging.getLogger() - if root.handlers: - for handler in root.handlers: - root.removeHandler(handler) - # Silence irrelevant loggers logging.getLogger("discord").setLevel(logging.ERROR) logging.getLogger("websockets").setLevel(logging.ERROR) -- cgit v1.2.3 From e135189446e4a450376b9c694f1faadd57fef19a Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Tue, 7 Sep 2021 00:04:40 +0200 Subject: Control logging levels through root logger instead of handlers The difference in the logging levels without debug mode enabled was also removed --- bot/log.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/bot/log.py b/bot/log.py index 0e0c22f0..9ae40984 100644 --- a/bot/log.py +++ b/bot/log.py @@ -20,12 +20,8 @@ def setup() -> None: file_handler = logging.handlers.RotatingFileHandler( log_file, maxBytes=5 * (2 ** 20), backupCount=10, encoding="utf-8", ) - file_handler.setLevel(logging.TRACE if Client.debug else logging.DEBUG) - # Console handler prints to terminal console_handler = logging.StreamHandler() - level = logging.TRACE if Client.debug else logging.INFO - console_handler.setLevel(level) # Silence irrelevant loggers logging.getLogger("discord").setLevel(logging.ERROR) @@ -38,7 +34,7 @@ def setup() -> None: logging.basicConfig( format="%(asctime)s - %(name)s %(levelname)s: %(message)s", datefmt="%D %H:%M:%S", - level=logging.TRACE if Client.debug else logging.DEBUG, + level=logging.TRACE if Client.debug else logging.INFO, handlers=[console_handler, file_handler], ) logging.getLogger().info("Logging initialization complete") -- cgit v1.2.3 From 37c4d55275ed1ceadc86ad2c437a6dd337614bea Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Mon, 6 Sep 2021 20:13:17 -0400 Subject: Continue work in progress Implemented the thumbnail creation from CyberCitizen0, worked on adding some features to the program. Notable Changes: -Check if user passes in hex color -Create thumbnail based on rgb_color To-Do: -Create hex color from rgb color -Create readable rgb color from user input Co-authored-by: Mohammad Rafivulla <77384412+CyberCitizen01@users.noreply.github.com> --- bot/exts/utilities/color.py | 140 +++++++++++++++++++------------------------- 1 file changed, 61 insertions(+), 79 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index dd922bf9..1a4f7031 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -1,113 +1,95 @@ # imports import colorsys import logging +import re +from io import BytesIO -import PIL -from discord import Embed +from PIL import Image, ImageColor +from discord import Embed, File from discord.ext import commands from rapidfuzz import process from bot.bot import Bot from bot.constants import Colours -# Planning to use discord-flags, hence require changes to poetry.lock file -# from discord.ext import flags logger = logging.getLogger(__name__) -# constants if needed -# Color URLs - will be replaced by JSON file? -COLOR_JSON_PATH = ".bot//exts//resources//evergreen//" -COLOR_URL_XKCD = "https://xkcd.com/color/rgb/" -COLOR_URL_NAME_THAT_COLOR = "https://github.com/ryanzec/name-that-color/blob/master/lib/ntc.js#L116-L1681" - - -COLOR_ERROR = Embed( - title="Input color is not possible", - description="The color code {user_color} is not a possible color combination." - "\nThe range of possible values are: " - "\nRGB & HSV: 0-255" - "\nCMYK: 0-100%" - "\nHSL: 0-360 degrees" - "\nHex: #000000-#FFFFFF" -) -COLOR_EMBED = Embed( - title="{color_name}", - description="RGB" - "\n{RGB}" - "\nHSV" - "\n{HSV}" - "\nCMYK" - "\n{CMYK}" - "\nHSL" - "\n{HSL}" - "\nHex" - "\n{Hex}" -) + +ERROR_MSG = """The color code {user_color} is not a possible color combination. +\nThe range of possible values are: +\nRGB & HSV: 0-255 +\nCMYK: 0-100% +\nHSL: 0-360 degrees +\nHex: #000000-#FFFFFF +""" # define color command -class Color(commands.cog): +class Color(commands.Cog): """User initiated command to receive color information.""" def __init__(self, bot: Bot): self.bot = bot - # ? possible to use discord-flags to allow user to decide on color - # https://pypi.org/project/discord-flags/ - # @flags.add_flag("--rgb", type=str) - # @flags.add_flag("--hsv", type=str) - # @flags.add_flag("--cmyk", type=str) - # @flags.add_flag("--hsl", type=str) - # @flags.add_flag("--hex", type=str) - # @flags.add_flag("--name", type=str) - # @flags.command() - @commands.command(aliases=["color", "colour"]) + @commands.command(aliases=["colour"]) @commands.cooldown(1, 10, commands.cooldowns.BucketType.user) async def color(self, ctx: commands.Context, *, user_color: str) -> None: """Send information on input color code or color name.""" # need to check if user_color is RGB, HSV, CMYK, HSL, Hex or color name # should we assume the color is RGB if not defined? - # should discord tags be used? - # need to review discord.py V2.0 - - # TODO code to check if color code is possible - await ctx.send(embed=COLOR_ERROR.format(color=user_color)) - # await ctx.send(embed=COLOR_EMBED.format( - # RGB=color_dict["RGB"], - # HSV=color_dict["HSV"], - # HSL=color_dict["HSL"], - # CMYK=color_dict["CMYK"], - # HSL=color_dict["HSL"], - # Hex=color_dict["Hex"], - # color_name=color_dict["color_name"] - # ).set_image() # url for image? - # ) - - # TODO pass for now - pass + + if "#" in user_color: + logger.info(f"{user_color = }") + hex_match = re.search(r"^#(?:[0-9a-fA-F]{3}){1,2}$", user_color) + if hex_match: + hex_color = int(hex(int(user_color.replace("#", ""), 16)), 0) + logger.info(f"{hex_color = }") + rgb_color = ImageColor.getcolor(user_color, "RGB") + else: + await ctx.send(embed=Embed( + title="An error has occured.", + description=ERROR_MSG.format(user_color=user_color) + ) + ) + + elif "RGB" or "rgb" in user_color: + rgb_parse = user_color.split() + rgb = rgb_parse[1:].replace(", ", "") + logger.info(f"{rgb = }") + logger.info(f"{rgb[0] = }") + logger.info(f"{rgb[1] = }") + logger.info(f"{rgb[2] = }") + rgb_color = tuple(rgb) + hex_color = f"0x{int(rgb[0]):02x}{int(rgb[1]):02x}{int(rgb[2]):02x}" + + main_embed = Embed( + title=user_color, + color=hex_color, + ) + async with ctx.typing(): + file = await self._create_thumbnail_attachment(rgb_color) + main_embed.set_thumbnail(url="attachment://color.png") + + await ctx.send(file=file, embed=main_embed) + + async def _create_thumbnail_attachment(self, color: str) -> File: + """Generate a thumbnail from `color`.""" + + thumbnail = Image.new("RGB", (100, 100), color=color) + bufferedio = BytesIO() + thumbnail.save(bufferedio, format="PNG") + bufferedio.seek(0) + + file = File(bufferedio, filename="color.png") + + return file + # if user_color in color_lists: # # TODO fuzzy match for color # pass - async def color_converter(self, color: str, code_type: str) -> dict: - """Generate alternative color codes for use in the embed.""" - # TODO add code to take color and code type and return other types - # color_dict = { - # "RGB": color_RGB, - # "HSV": color_HSV, - # "HSL": color_HSL, - # "CMYK": color_CMYK, - # "HSL": color_HSL, - # "Hex": color_Hex, - # "color_name": color_name, - # } - pass - - async def photo_generator(self, color: str) -> None: - """Generate photo to use in embed.""" - # TODO need to find a way to store photo in cache to add to embed, then remove def setup(bot: Bot) -> None: -- cgit v1.2.3 From a16ee6ecd1b1449e8203e650495809e85ea9ddc8 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Tue, 7 Sep 2021 07:39:21 -0400 Subject: Fixing flake8 errors, code style Still a work in progress but commenting out stub code and unused imports. List of To-Do's still applies. --- bot/exts/utilities/color.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 1a4f7031..b1a77b28 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -1,5 +1,5 @@ # imports -import colorsys +# import colorsys import logging import re from io import BytesIO @@ -7,10 +7,10 @@ from io import BytesIO from PIL import Image, ImageColor from discord import Embed, File from discord.ext import commands -from rapidfuzz import process +# from rapidfuzz import process from bot.bot import Bot -from bot.constants import Colours +# from bot.constants import Colours logger = logging.getLogger(__name__) @@ -46,12 +46,14 @@ class Color(commands.Cog): hex_color = int(hex(int(user_color.replace("#", ""), 16)), 0) logger.info(f"{hex_color = }") rgb_color = ImageColor.getcolor(user_color, "RGB") + logger.info(f"{rgb_color = }") else: - await ctx.send(embed=Embed( + await ctx.send( + embed=Embed( title="An error has occured.", - description=ERROR_MSG.format(user_color=user_color) - ) + description=ERROR_MSG.format(user_color=user_color), ) + ) elif "RGB" or "rgb" in user_color: rgb_parse = user_color.split() @@ -64,7 +66,7 @@ class Color(commands.Cog): hex_color = f"0x{int(rgb[0]):02x}{int(rgb[1]):02x}{int(rgb[2]):02x}" main_embed = Embed( - title=user_color, + title=user_color, # need to replace with fuzzymatch color name color=hex_color, ) async with ctx.typing(): @@ -75,7 +77,6 @@ class Color(commands.Cog): async def _create_thumbnail_attachment(self, color: str) -> File: """Generate a thumbnail from `color`.""" - thumbnail = Image.new("RGB", (100, 100), color=color) bufferedio = BytesIO() thumbnail.save(bufferedio, format="PNG") @@ -85,11 +86,8 @@ class Color(commands.Cog): return file - # if user_color in color_lists: - # # TODO fuzzy match for color - # pass - + # # fuzzy match for color def setup(bot: Bot) -> None: -- cgit v1.2.3 From 86cde23bcec5090e73abaf1289641d299f2e8677 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Tue, 7 Sep 2021 07:48:07 -0400 Subject: Add embed fields for Hex and RGB --- bot/exts/utilities/color.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index b1a77b28..b1b423e7 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -73,6 +73,16 @@ class Color(commands.Cog): file = await self._create_thumbnail_attachment(rgb_color) main_embed.set_thumbnail(url="attachment://color.png") + main_embed.add_field( + name="Hex", + value=f">>Hex #{hex_color}", + inline=False, + ) + main_embed.add_field( + name="RGB", + value=f">>RGB {rgb_color}", + inline=False, + ) await ctx.send(file=file, embed=main_embed) async def _create_thumbnail_attachment(self, color: str) -> File: -- cgit v1.2.3 From 6d7f46fabe530e4cafdffa25d7fced730965e481 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Tue, 7 Sep 2021 00:15:36 +0200 Subject: Add coloredlogs --- bot/log.py | 18 +++++++++++++++++- poetry.lock | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++------- pyproject.toml | 2 ++ 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/bot/log.py b/bot/log.py index 9ae40984..d44e6933 100644 --- a/bot/log.py +++ b/bot/log.py @@ -1,7 +1,11 @@ import logging import logging.handlers +import os +import sys from pathlib import Path +import coloredlogs + from bot.constants import Client @@ -23,6 +27,19 @@ def setup() -> None: # Console handler prints to terminal console_handler = logging.StreamHandler() + if "COLOREDLOGS_LEVEL_STYLES" not in os.environ: + coloredlogs.DEFAULT_LEVEL_STYLES = { + **coloredlogs.DEFAULT_LEVEL_STYLES, + "trace": {"color": 246}, + "critical": {"background": "red"}, + "debug": coloredlogs.DEFAULT_LEVEL_STYLES["info"], + } + + if "COLOREDLOGS_LOG_FORMAT" not in os.environ: + coloredlogs.DEFAULT_LOG_FORMAT = "%(asctime)s - %(name)s %(levelname)s: %(message)s" + + coloredlogs.install(stream=sys.stdout) + # Silence irrelevant loggers logging.getLogger("discord").setLevel(logging.ERROR) logging.getLogger("websockets").setLevel(logging.ERROR) @@ -32,7 +49,6 @@ def setup() -> None: # Setup new logging configuration logging.basicConfig( - format="%(asctime)s - %(name)s %(levelname)s: %(message)s", datefmt="%D %H:%M:%S", level=logging.TRACE if Client.debug else logging.INFO, handlers=[console_handler, file_handler], diff --git a/poetry.lock b/poetry.lock index 289f2039..328a474f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -139,10 +139,24 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" name = "colorama" version = "0.4.4" description = "Cross-platform colored terminal text." -category = "dev" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "coloredlogs" +version = "15.0.1" +description = "Colored terminal output for Python's logging module" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[package.dependencies] +humanfriendly = ">=9.1" + +[package.extras] +cron = ["capturer (>=2.4)"] + [[package]] name = "cycler" version = "0.10.0" @@ -328,6 +342,17 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "humanfriendly" +version = "9.2" +description = "Human friendly output for text interfaces using Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +pyreadline = {version = "*", markers = "sys_platform == \"win32\""} + [[package]] name = "identify" version = "2.2.13" @@ -552,6 +577,14 @@ category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "pyreadline" +version = "2.1" +description = "A python implmementation of GNU readline." +category = "main" +optional = false +python-versions = "*" + [[package]] name = "python-dateutil" version = "2.8.2" @@ -565,11 +598,11 @@ six = ">=1.5" [[package]] name = "python-dotenv" -version = "0.15.0" -description = "Add .env support to your django/flask apps in development and deployments" +version = "0.19.0" +description = "Read key-value pairs from a .env file and set them as environment variables" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.5" [package.extras] cli = ["click (>=5.0)"] @@ -730,7 +763,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "9efbf6be5298ab8ace2588e218be309e105987bfdfa8317453d584a1faac4934" +content-hash = "f519d64a4c9286d08a77e33c6776a7da280494a008826a069daad8bea3d11995" [metadata.files] aiodns = [ @@ -863,6 +896,10 @@ colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] +coloredlogs = [ + {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, + {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, +] cycler = [ {file = "cycler-0.10.0-py2.py3-none-any.whl", hash = "sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d"}, {file = "cycler-0.10.0.tar.gz", hash = "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8"}, @@ -962,6 +999,10 @@ hiredis = [ {file = "hiredis-2.0.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0"}, {file = "hiredis-2.0.0.tar.gz", hash = "sha256:81d6d8e39695f2c37954d1011c0480ef7cf444d4e3ae24bc5e89ee5de360139a"}, ] +humanfriendly = [ + {file = "humanfriendly-9.2-py2.py3-none-any.whl", hash = "sha256:332da98c24cc150efcc91b5508b19115209272bfdf4b0764a56795932f854271"}, + {file = "humanfriendly-9.2.tar.gz", hash = "sha256:f7dba53ac7935fd0b4a2fc9a29e316ddd9ea135fb3052d3d0279d10c18ff9c48"}, +] identify = [ {file = "identify-2.2.13-py2.py3-none-any.whl", hash = "sha256:7199679b5be13a6b40e6e19ea473e789b11b4e3b60986499b1f589ffb03c217c"}, {file = "identify-2.2.13.tar.gz", hash = "sha256:7bc6e829392bd017236531963d2d937d66fc27cadc643ac0aba2ce9f26157c79"}, @@ -1279,13 +1320,18 @@ pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] +pyreadline = [ + {file = "pyreadline-2.1.win-amd64.exe", hash = "sha256:9ce5fa65b8992dfa373bddc5b6e0864ead8f291c94fbfec05fbd5c836162e67b"}, + {file = "pyreadline-2.1.win32.exe", hash = "sha256:65540c21bfe14405a3a77e4c085ecfce88724743a4ead47c66b84defcf82c32e"}, + {file = "pyreadline-2.1.zip", hash = "sha256:4530592fc2e85b25b1a9f79664433da09237c1a270e4d78ea5aa3a2c7229e2d1"}, +] python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] python-dotenv = [ - {file = "python-dotenv-0.15.0.tar.gz", hash = "sha256:587825ed60b1711daea4832cf37524dfd404325b7db5e25ebe88c495c9f807a0"}, - {file = "python_dotenv-0.15.0-py2.py3-none-any.whl", hash = "sha256:0c8d1b80d1a1e91717ea7d526178e3882732420b03f08afea0406db6402e220e"}, + {file = "python-dotenv-0.19.0.tar.gz", hash = "sha256:f521bc2ac9a8e03c736f62911605c5d83970021e3fa95b37d769e2bbbe9b6172"}, + {file = "python_dotenv-0.19.0-py2.py3-none-any.whl", hash = "sha256:aae25dc1ebe97c420f50b81fb0e5c949659af713f31fdb63c749ca68748f34b1"}, ] pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, diff --git a/pyproject.toml b/pyproject.toml index 7848f593..31689468 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,8 @@ PyYAML = "~=5.4" async-rediscache = {extras = ["fakeredis"], version = "~=0.1.4"} emojis = "~=0.6.0" matplotlib = "~=3.4.1" +coloredlogs = "~=15.0" +colorama = { version = "~=0.4.3", markers = "sys_platform == 'win32'" } [tool.poetry.dev-dependencies] flake8 = "~=3.8" -- cgit v1.2.3 From ac63915dd38c920cbe1d2d194ecd1bc2168d7570 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Tue, 7 Sep 2021 00:20:30 +0200 Subject: Remove basicConfig configuration The basicConfig now only controlled the file handler which could've been confusing. The format string was also changed to use the same style as bot --- bot/log.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/bot/log.py b/bot/log.py index d44e6933..c8437505 100644 --- a/bot/log.py +++ b/bot/log.py @@ -16,6 +16,9 @@ def setup() -> None: logging.addLevelName(logging.TRACE, "TRACE") logging.Logger.trace = _monkeypatch_trace + format_string = "%(asctime)s | %(name)s | %(levelname)s | %(message)s" + log_format = logging.Formatter(format_string) + # Set up file logging log_file = Path("logs/sir-lancebot.log") log_file.parent.mkdir(exist_ok=True) @@ -24,8 +27,11 @@ def setup() -> None: file_handler = logging.handlers.RotatingFileHandler( log_file, maxBytes=5 * (2 ** 20), backupCount=10, encoding="utf-8", ) - # Console handler prints to terminal - console_handler = logging.StreamHandler() + file_handler.setFormatter(log_format) + + root_logger = logging.getLogger() + root_logger.setLevel(logging.TRACE if Client.debug else logging.INFO) + root_logger.addHandler(file_handler) if "COLOREDLOGS_LEVEL_STYLES" not in os.environ: coloredlogs.DEFAULT_LEVEL_STYLES = { @@ -36,7 +42,7 @@ def setup() -> None: } if "COLOREDLOGS_LOG_FORMAT" not in os.environ: - coloredlogs.DEFAULT_LOG_FORMAT = "%(asctime)s - %(name)s %(levelname)s: %(message)s" + coloredlogs.DEFAULT_LOG_FORMAT = format_string coloredlogs.install(stream=sys.stdout) @@ -47,13 +53,7 @@ def setup() -> None: logging.getLogger("matplotlib").setLevel(logging.ERROR) logging.getLogger("async_rediscache").setLevel(logging.WARNING) - # Setup new logging configuration - logging.basicConfig( - datefmt="%D %H:%M:%S", - level=logging.TRACE if Client.debug else logging.INFO, - handlers=[console_handler, file_handler], - ) - logging.getLogger().info("Logging initialization complete") + root_logger.info("Logging initialization complete") def _monkeypatch_trace(self: logging.Logger, msg: str, *args, **kwargs) -> None: -- cgit v1.2.3 From ecfe6bc1b89632c1161f5aad83a08a8459f8f13a Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Tue, 7 Sep 2021 00:32:12 +0200 Subject: Add selective trace loggers Add selective enabling of trace loggers as described in python-discord/bot#1529 --- bot/constants.py | 1 + bot/log.py | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 2313bfdb..ae6e02c1 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -138,6 +138,7 @@ class Client(NamedTuple): github_bot_repo = "https://github.com/python-discord/sir-lancebot" # Override seasonal locks: 1 (January) to 12 (December) month_override = int(environ["MONTH_OVERRIDE"]) if "MONTH_OVERRIDE" in environ else None + trace_loggers = environ.get("BOT_TRACE_LOGGERS") class Colours: diff --git a/bot/log.py b/bot/log.py index c8437505..5e0e909d 100644 --- a/bot/log.py +++ b/bot/log.py @@ -30,7 +30,6 @@ def setup() -> None: file_handler.setFormatter(log_format) root_logger = logging.getLogger() - root_logger.setLevel(logging.TRACE if Client.debug else logging.INFO) root_logger.addHandler(file_handler) if "COLOREDLOGS_LEVEL_STYLES" not in os.environ: @@ -44,8 +43,9 @@ def setup() -> None: if "COLOREDLOGS_LOG_FORMAT" not in os.environ: coloredlogs.DEFAULT_LOG_FORMAT = format_string - coloredlogs.install(stream=sys.stdout) + coloredlogs.install(level=logging.TRACE, stream=sys.stdout) + root_logger.setLevel(logging.DEBUG if Client.debug else logging.INFO) # Silence irrelevant loggers logging.getLogger("discord").setLevel(logging.ERROR) logging.getLogger("websockets").setLevel(logging.ERROR) @@ -53,6 +53,8 @@ def setup() -> None: logging.getLogger("matplotlib").setLevel(logging.ERROR) logging.getLogger("async_rediscache").setLevel(logging.WARNING) + _set_trace_loggers() + root_logger.info("Logging initialization complete") @@ -65,3 +67,30 @@ def _monkeypatch_trace(self: logging.Logger, msg: str, *args, **kwargs) -> None: """ if self.isEnabledFor(logging.TRACE): self._log(logging.TRACE, msg, args, **kwargs) + + +def _set_trace_loggers() -> None: + """ + Set loggers to the trace level according to the value from the BOT_TRACE_LOGGERS env var. + + When the env var is a list of logger names delimited by a comma, + each of the listed loggers will be set to the trace level. + + If this list is prefixed with a "!", all of the loggers except the listed ones will be set to the trace level. + + Otherwise if the env var begins with a "*", + the root logger is set to the trace level and other contents are ignored. + """ + level_filter = Client.trace_loggers + if level_filter: + if level_filter.startswith("*"): + logging.getLogger().setLevel(logging.TRACE) + + elif level_filter.startswith("!"): + logging.getLogger().setLevel(logging.TRACE) + for logger_name in level_filter.strip("!,").split(","): + logging.getLogger(logger_name).setLevel(logging.DEBUG) + + else: + for logger_name in level_filter.strip(",").split(","): + logging.getLogger(logger_name).setLevel(logging.TRACE) -- cgit v1.2.3 From 92b63e85501be8169aa3be71d8f58a5ca69d8572 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Tue, 7 Sep 2021 12:51:45 -0400 Subject: Update code to use 'mode' variable Updated the code to parse user_input depending on the color code 'mode' passed to the command. Added stub code for future color codes and embeds if mode is None or wrong code. --- bot/exts/utilities/color.py | 59 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index b1b423e7..1efacead 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -10,7 +10,7 @@ from discord.ext import commands # from rapidfuzz import process from bot.bot import Bot -# from bot.constants import Colours +from bot.constants import Colours logger = logging.getLogger(__name__) @@ -33,13 +33,12 @@ class Color(commands.Cog): self.bot = bot @commands.command(aliases=["colour"]) - @commands.cooldown(1, 10, commands.cooldowns.BucketType.user) - async def color(self, ctx: commands.Context, *, user_color: str) -> None: + async def color(self, ctx: commands.Context, mode: str, user_color: str) -> None: """Send information on input color code or color name.""" # need to check if user_color is RGB, HSV, CMYK, HSL, Hex or color name # should we assume the color is RGB if not defined? - if "#" in user_color: + if mode.lower() == "hex": logger.info(f"{user_color = }") hex_match = re.search(r"^#(?:[0-9a-fA-F]{3}){1,2}$", user_color) if hex_match: @@ -55,15 +54,32 @@ class Color(commands.Cog): ) ) - elif "RGB" or "rgb" in user_color: - rgb_parse = user_color.split() - rgb = rgb_parse[1:].replace(", ", "") - logger.info(f"{rgb = }") - logger.info(f"{rgb[0] = }") - logger.info(f"{rgb[1] = }") - logger.info(f"{rgb[2] = }") - rgb_color = tuple(rgb) - hex_color = f"0x{int(rgb[0]):02x}{int(rgb[1]):02x}{int(rgb[2]):02x}" + elif mode.lower() == "rgb": + logger.info(f"{user_color = }") + # rgb_color = user_color + + elif mode.lower() == "hsv": + pass + elif mode.lower() == "hsl": + pass + elif mode.lower() == "cmyk": + pass + else: + # mode is either None or an invalid code + if mode is None: + no_mode_embed = Embed( + title="No 'mode' was passed, please define a color code.", + color=Colours.soft_red, + ) + await ctx.send(embed=no_mode_embed) + return + wrong_mode_embed = Embed( + title=f"The color code {mode} is not a valid option", + description="Possible modes are: Hex, RGB, HSV, HSL and CMYK.", + color=Colours.soft_red, + ) + await ctx.send(embed=wrong_mode_embed) + return main_embed = Embed( title=user_color, # need to replace with fuzzymatch color name @@ -83,6 +99,23 @@ class Color(commands.Cog): value=f">>RGB {rgb_color}", inline=False, ) + """ + main_embed.add_field( + name="HSV", + value=f">>HSV {hsv_color}", + inline=False, + ) + main_embed.add_field( + name="HSL", + value=f">>HSL {hsl_color}", + inline=False, + ) + main_embed.add_field( + name="CMYK", + value=f">>CMYK {cmyk_color}", + inline=False, + ) + """ await ctx.send(file=file, embed=main_embed) async def _create_thumbnail_attachment(self, color: str) -> File: -- cgit v1.2.3 From 348de13c97baac9813ad6be16a49cffa5c536751 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Wed, 8 Sep 2021 10:06:47 -0400 Subject: Minor fixes --- bot/exts/utilities/color.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 1efacead..d7fff503 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -1,4 +1,3 @@ -# imports # import colorsys import logging import re @@ -35,16 +34,14 @@ class Color(commands.Cog): @commands.command(aliases=["colour"]) async def color(self, ctx: commands.Context, mode: str, user_color: str) -> None: """Send information on input color code or color name.""" - # need to check if user_color is RGB, HSV, CMYK, HSL, Hex or color name - # should we assume the color is RGB if not defined? - + logger.info(f"{mode = }") + logger.info(f"{user_color = }") if mode.lower() == "hex": - logger.info(f"{user_color = }") hex_match = re.search(r"^#(?:[0-9a-fA-F]{3}){1,2}$", user_color) if hex_match: hex_color = int(hex(int(user_color.replace("#", ""), 16)), 0) - logger.info(f"{hex_color = }") rgb_color = ImageColor.getcolor(user_color, "RGB") + logger.info(f"{hex_color = }") logger.info(f"{rgb_color = }") else: await ctx.send( @@ -53,11 +50,8 @@ class Color(commands.Cog): description=ERROR_MSG.format(user_color=user_color), ) ) - elif mode.lower() == "rgb": - logger.info(f"{user_color = }") - # rgb_color = user_color - + pass elif mode.lower() == "hsv": pass elif mode.lower() == "hsl": @@ -66,6 +60,7 @@ class Color(commands.Cog): pass else: # mode is either None or an invalid code + # need to handle whether user passes color name if mode is None: no_mode_embed = Embed( title="No 'mode' was passed, please define a color code.", -- cgit v1.2.3 From 39d10a42d592da3eb8a4f35111c7a6e815ab7bd8 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Fri, 10 Sep 2021 07:47:12 -0400 Subject: Test to capture all user_input --- bot/exts/utilities/color.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index d7fff503..c4df3e10 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -32,7 +32,7 @@ class Color(commands.Cog): self.bot = bot @commands.command(aliases=["colour"]) - async def color(self, ctx: commands.Context, mode: str, user_color: str) -> None: + async def color(self, ctx: commands.Context, mode: str, *, user_color: str) -> None: """Send information on input color code or color name.""" logger.info(f"{mode = }") logger.info(f"{user_color = }") -- cgit v1.2.3 From 854452c60c589fa414b8cac66edcc884caa03bd6 Mon Sep 17 00:00:00 2001 From: CyberCitizen01 Date: Sat, 11 Sep 2021 04:17:18 +0530 Subject: Added "colour information" and "colour conversion" features Details: https://github.com/python-discord/sir-lancebot/issues/677 NOTE: get_color_fields (line 122) method explicity requires a valid tuple of RGB values. --- bot/exts/utilities/color.py | 134 +++++++++++++++++++++++++++++++++----------- 1 file changed, 100 insertions(+), 34 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index c4df3e10..6abfc006 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -1,4 +1,4 @@ -# import colorsys +import colorsys import logging import re from io import BytesIO @@ -76,44 +76,28 @@ class Color(commands.Cog): await ctx.send(embed=wrong_mode_embed) return - main_embed = Embed( - title=user_color, # need to replace with fuzzymatch color name - color=hex_color, - ) async with ctx.typing(): - file = await self._create_thumbnail_attachment(rgb_color) + main_embed = Embed( + title=user_color, # need to replace with fuzzymatch color name + description='(Approx..)', + color=hex_color, + ) + + file = await self.create_thumbnail_attachment(rgb_color) main_embed.set_thumbnail(url="attachment://color.png") - main_embed.add_field( - name="Hex", - value=f">>Hex #{hex_color}", - inline=False, - ) - main_embed.add_field( - name="RGB", - value=f">>RGB {rgb_color}", - inline=False, - ) - """ - main_embed.add_field( - name="HSV", - value=f">>HSV {hsv_color}", - inline=False, - ) - main_embed.add_field( - name="HSL", - value=f">>HSL {hsl_color}", - inline=False, - ) - main_embed.add_field( - name="CMYK", - value=f">>CMYK {cmyk_color}", - inline=False, - ) - """ + fields = self.get_color_fields(rgb_color) + for field in fields: + main_embed.add_field( + name=field['name'], + value=field['value'], + inline=False, + ) + await ctx.send(file=file, embed=main_embed) - async def _create_thumbnail_attachment(self, color: str) -> File: + @staticmethod + async def create_thumbnail_attachment(color: str) -> File: """Generate a thumbnail from `color`.""" thumbnail = Image.new("RGB", (100, 100), color=color) bufferedio = BytesIO() @@ -124,6 +108,88 @@ class Color(commands.Cog): return file + @staticmethod + def get_color_fields(rgb_color: tuple[int, int, int]) -> list[dict]: + """Converts from `RGB` to `CMYK`, `HSV`, `HSL` and returns a list of fields.""" + + def _rgb_to_hex(rgb_color: tuple[int, int, int]) -> str: + """To convert from `RGB` to `Hex` notation.""" + return '#' + ''.join(hex(color)[2:].zfill(2) for color in rgb_color).upper() + + def _rgb_to_cmyk(rgb_color: tuple[int, int, int]) -> tuple[int, int, int, int]: + """To convert from `RGB` to `CMYK` color space.""" + r, g, b = rgb_color + + # RGB_SCALE -> 255 + # CMYK_SCALE -> 100 + + if (r == g == b == 0): + return 0, 0, 0, 100 # Representing Black + + # rgb [0,RGB_SCALE] -> cmy [0,1] + c = 1 - r / 255 + m = 1 - g / 255 + y = 1 - b / 255 + + # extract out k [0, 1] + min_cmy = min(c, m, y) + c = (c - min_cmy) / (1 - min_cmy) + m = (m - min_cmy) / (1 - min_cmy) + y = (y - min_cmy) / (1 - min_cmy) + k = min_cmy + + # rescale to the range [0,CMYK_SCALE] and round off + c = round(c * 100) + m = round(m * 100) + y = round(y * 100) + k = round(k * 100) + + return c, m, y, k + + def _rgb_to_hsv(rgb_color: tuple[int, int, int]) -> tuple[int, int, int]: + """To convert from `RGB` to `HSV` color space.""" + r, g, b = rgb_color + h, v, s = colorsys.rgb_to_hsv(r/float(255), g/float(255), b/float(255)) + h = round(h*360) + s = round(s*100) + v = round(v*100) + return h, s, v + + def _rgb_to_hsl(rgb_color: tuple[int, int, int]) -> tuple[int, int, int]: + """To convert from `RGB` to `HSL` color space.""" + r, g, b = rgb_color + h, l, s = colorsys.rgb_to_hls(r/float(255), g/float(255), b/float(255)) + h = round(h*360) + s = round(s*100) + l = round(l*100) # noqa: E741 It's little `L`, Reason: To maintain consistency. + return h, s, l + + hex_color = _rgb_to_hex(rgb_color) + cmyk_color = _rgb_to_cmyk(rgb_color) + hsv_color = _rgb_to_hsv(rgb_color) + hsl_color = _rgb_to_hsl(rgb_color) + + all_fields = [ + { + "name": "RGB", + "value": f"Β» rgb {rgb_color}\nΒ» hex {hex_color}" + }, + { + "name": "CMYK", + "value": f"Β» cmyk {cmyk_color}" + }, + { + "name": "HSV", + "value": f"Β» hsv {hsv_color}" + }, + { + "name": "HSL", + "value": f"Β» hsl {hsl_color}" + }, + ] + + return all_fields + # if user_color in color_lists: # # fuzzy match for color -- cgit v1.2.3 From 5714e496c331786f4614ce6e2e6e132524d7d72f Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Sat, 11 Sep 2021 09:11:28 -0400 Subject: Move .json file to correct folder --- bot/resources/evergreen/ryanzec_colours.json | 1568 -------------------------- bot/resources/utilities/ryanzec_colours.json | 1568 ++++++++++++++++++++++++++ 2 files changed, 1568 insertions(+), 1568 deletions(-) delete mode 100644 bot/resources/evergreen/ryanzec_colours.json create mode 100644 bot/resources/utilities/ryanzec_colours.json diff --git a/bot/resources/evergreen/ryanzec_colours.json b/bot/resources/evergreen/ryanzec_colours.json deleted file mode 100644 index 7b89f052..00000000 --- a/bot/resources/evergreen/ryanzec_colours.json +++ /dev/null @@ -1,1568 +0,0 @@ -{ - "Abbey": "4C4F56", - "Acadia": "1B1404", - "Acapulco": "7CB0A1", - "Aero Blue": "C9FFE5", - "Affair": "714693", - "Akaroa": "D4C4A8", - "Alabaster": "FAFAFA", - "Albescent White": "F5E9D3", - "Algae Green": "93DFB8", - "Alice Blue": "F0F8FF", - "Alizarin Crimson": "E32636", - "Allports": "0076A3", - "Almond": "EED9C4", - "Almond Frost": "907B71", - "Alpine": "AF8F2C", - "Alto": "DBDBDB", - "Aluminium": "A9ACB6", - "Amaranth": "E52B50", - "Amazon": "3B7A57", - "Amber": "FFBF00", - "Americano": "87756E", - "Amethyst": "9966CC", - "Amethyst Smoke": "A397B4", - "Amour": "F9EAF3", - "Amulet": "7B9F80", - "Anakiwa": "9DE5FF", - "Antique Brass": "C88A65", - "Antique Bronze": "704A07", - "Anzac": "E0B646", - "Apache": "DFBE6F", - "Apple": "4FA83D", - "Apple Blossom": "AF4D43", - "Apple Green": "E2F3EC", - "Apricot": "EB9373", - "Apricot Peach": "FBCEB1", - "Apricot White": "FFFEEC", - "Aqua Deep": "014B43", - "Aqua Forest": "5FA777", - "Aqua Haze": "EDF5F5", - "Aqua Island": "A1DAD7", - "Aqua Spring": "EAF9F5", - "Aqua Squeeze": "E8F5F2", - "Aquamarine": "7FFFD4", - "Aquamarine Blue": "71D9E2", - "Arapawa": "110C6C", - "Armadillo": "433E37", - "Arrowtown": "948771", - "Ash": "C6C3B5", - "Asparagus": "7BA05B", - "Asphalt": "130A06", - "Astra": "FAEAB9", - "Astral": "327DA0", - "Astronaut": "283A77", - "Astronaut Blue": "013E62", - "Athens Gray": "EEF0F3", - "Aths Special": "ECEBCE", - "Atlantis": "97CD2D", - "Atoll": "0A6F75", - "Atomic Tangerine": "FF9966", - "Au Chico": "97605D", - "Aubergine": "3B0910", - "Australian Mint": "F5FFBE", - "Avocado": "888D65", - "Axolotl": "4E6649", - "Azalea": "F7C8DA", - "Aztec": "0D1C19", - "Azure": "315BA1", - "Azure Radiance": "007FFF", - "Baby Blue": "E0FFFF", - "Bahama Blue": "026395", - "Bahia": "A5CB0C", - "Baja White": "FFF8D1", - "Bali Hai": "859FAF", - "Baltic Sea": "2A2630", - "Bamboo": "DA6304", - "Banana Mania": "FBE7B2", - "Bandicoot": "858470", - "Barberry": "DED717", - "Barley Corn": "A68B5B", - "Barley White": "FFF4CE", - "Barossa": "44012D", - "Bastille": "292130", - "Battleship Gray": "828F72", - "Bay Leaf": "7DA98D", - "Bay of Many": "273A81", - "Bazaar": "98777B", - "Bean ": "3D0C02", - "Beauty Bush": "EEC1BE", - "Beaver": "926F5B", - "Beeswax": "FEF2C7", - "Beige": "F5F5DC", - "Bermuda": "7DD8C6", - "Bermuda Gray": "6B8BA2", - "Beryl Green": "DEE5C0", - "Bianca": "FCFBF3", - "Big Stone": "162A40", - "Bilbao": "327C14", - "Biloba Flower": "B2A1EA", - "Birch": "373021", - "Bird Flower": "D4CD16", - "Biscay": "1B3162", - "Bismark": "497183", - "Bison Hide": "C1B7A4", - "Bistre": "3D2B1F", - "Bitter": "868974", - "Bitter Lemon": "CAE00D", - "Bittersweet": "FE6F5E", - "Bizarre": "EEDEDA", - "Black": "000000", - "Black Bean": "081910", - "Black Forest": "0B1304", - "Black Haze": "F6F7F7", - "Black Marlin": "3E2C1C", - "Black Olive": "242E16", - "Black Pearl": "041322", - "Black Rock": "0D0332", - "Black Rose": "67032D", - "Black Russian": "0A001C", - "Black Squeeze": "F2FAFA", - "Black White": "FFFEF6", - "Blackberry": "4D0135", - "Blackcurrant": "32293A", - "Blaze Orange": "FF6600", - "Bleach White": "FEF3D8", - "Bleached Cedar": "2C2133", - "Blizzard Blue": "A3E3ED", - "Blossom": "DCB4BC", - "Blue": "0000FF", - "Blue Bayoux": "496679", - "Blue Bell": "9999CC", - "Blue Chalk": "F1E9FF", - "Blue Charcoal": "010D1A", - "Blue Chill": "0C8990", - "Blue Diamond": "380474", - "Blue Dianne": "204852", - "Blue Gem": "2C0E8C", - "Blue Haze": "BFBED8", - "Blue Lagoon": "017987", - "Blue Marguerite": "7666C6", - "Blue Ribbon": "0066FF", - "Blue Romance": "D2F6DE", - "Blue Smoke": "748881", - "Blue Stone": "016162", - "Blue Violet": "6456B7", - "Blue Whale": "042E4C", - "Blue Zodiac": "13264D", - "Blumine": "18587A", - "Blush": "B44668", - "Blush Pink": "FF6FFF", - "Bombay": "AFB1B8", - "Bon Jour": "E5E0E1", - "Bondi Blue": "0095B6", - "Bone": "E4D1C0", - "Bordeaux": "5C0120", - "Bossanova": "4E2A5A", - "Boston Blue": "3B91B4", - "Botticelli": "C7DDE5", - "Bottle Green": "093624", - "Boulder": "7A7A7A", - "Bouquet": "AE809E", - "Bourbon": "BA6F1E", - "Bracken": "4A2A04", - "Brandy": "DEC196", - "Brandy Punch": "CD8429", - "Brandy Rose": "BB8983", - "Breaker Bay": "5DA19F", - "Brick Red": "C62D42", - "Bridal Heath": "FFFAF4", - "Bridesmaid": "FEF0EC", - "Bright Gray": "3C4151", - "Bright Green": "66FF00", - "Bright Red": "B10000", - "Bright Sun": "FED33C", - "Bright Turquoise": "08E8DE", - "Brilliant Rose": "F653A6", - "Brink Pink": "FB607F", - "Bronco": "ABA196", - "Bronze": "3F2109", - "Bronze Olive": "4E420C", - "Bronzetone": "4D400F", - "Broom": "FFEC13", - "Brown": "964B00", - "Brown Bramble": "592804", - "Brown Derby": "492615", - "Brown Pod": "401801", - "Brown Rust": "AF593E", - "Brown Tumbleweed": "37290E", - "Bubbles": "E7FEFF", - "Buccaneer": "622F30", - "Bud": "A8AE9C", - "Buddha Gold": "C1A004", - "Buff": "F0DC82", - "Bulgarian Rose": "480607", - "Bull Shot": "864D1E", - "Bunker": "0D1117", - "Bunting": "151F4C", - "Burgundy": "900020", - "Burnham": "002E20", - "Burning Orange": "FF7034", - "Burning Sand": "D99376", - "Burnt Maroon": "420303", - "Burnt Orange": "CC5500", - "Burnt Sienna": "E97451", - "Burnt Umber": "8A3324", - "Bush": "0D2E1C", - "Buttercup": "F3AD16", - "Buttered Rum": "A1750D", - "Butterfly Bush": "624E9A", - "Buttermilk": "FFF1B5", - "Buttery White": "FFFCEA", - "Cab Sav": "4D0A18", - "Cabaret": "D94972", - "Cabbage Pont": "3F4C3A", - "Cactus": "587156", - "Cadet Blue": "A9B2C3", - "Cadillac": "B04C6A", - "Cafe Royale": "6F440C", - "Calico": "E0C095", - "California": "FE9D04", - "Calypso": "31728D", - "Camarone": "00581A", - "Camelot": "893456", - "Cameo": "D9B99B", - "Camouflage": "3C3910", - "Camouflage Green": "78866B", - "Can Can": "D591A4", - "Canary": "F3FB62", - "Candlelight": "FCD917", - "Candy Corn": "FBEC5D", - "Cannon Black": "251706", - "Cannon Pink": "894367", - "Cape Cod": "3C4443", - "Cape Honey": "FEE5AC", - "Cape Palliser": "A26645", - "Caper": "DCEDB4", - "Caramel": "FFDDAF", - "Cararra": "EEEEE8", - "Cardin Green": "01361C", - "Cardinal": "C41E3A", - "Cardinal Pink": "8C055E", - "Careys Pink": "D29EAA", - "Caribbean Green": "00CC99", - "Carissma": "EA88A8", - "Carla": "F3FFD8", - "Carmine": "960018", - "Carnaby Tan": "5C2E01", - "Carnation": "F95A61", - "Carnation Pink": "FFA6C9", - "Carousel Pink": "F9E0ED", - "Carrot Orange": "ED9121", - "Casablanca": "F8B853", - "Casal": "2F6168", - "Cascade": "8BA9A5", - "Cashmere": "E6BEA5", - "Casper": "ADBED1", - "Castro": "52001F", - "Catalina Blue": "062A78", - "Catskill White": "EEF6F7", - "Cavern Pink": "E3BEBE", - "Cedar": "3E1C14", - "Cedar Wood Finish": "711A00", - "Celadon": "ACE1AF", - "Celery": "B8C25D", - "Celeste": "D1D2CA", - "Cello": "1E385B", - "Celtic": "163222", - "Cement": "8D7662", - "Ceramic": "FCFFF9", - "Cerise": "DA3287", - "Cerise Red": "DE3163", - "Cerulean": "02A4D3", - "Cerulean Blue": "2A52BE", - "Chablis": "FFF4F3", - "Chalet Green": "516E3D", - "Chalky": "EED794", - "Chambray": "354E8C", - "Chamois": "EDDCB1", - "Champagne": "FAECCC", - "Chantilly": "F8C3DF", - "Charade": "292937", - "Chardon": "FFF3F1", - "Chardonnay": "FFCD8C", - "Charlotte": "BAEEF9", - "Charm": "D47494", - "Chartreuse": "7FFF00", - "Chartreuse Yellow": "DFFF00", - "Chateau Green": "40A860", - "Chatelle": "BDB3C7", - "Chathams Blue": "175579", - "Chelsea Cucumber": "83AA5D", - "Chelsea Gem": "9E5302", - "Chenin": "DFCD6F", - "Cherokee": "FCDA98", - "Cherry Pie": "2A0359", - "Cherrywood": "651A14", - "Cherub": "F8D9E9", - "Chestnut": "B94E48", - "Chestnut Rose": "CD5C5C", - "Chetwode Blue": "8581D9", - "Chicago": "5D5C58", - "Chiffon": "F1FFC8", - "Chilean Fire": "F77703", - "Chilean Heath": "FFFDE6", - "China Ivory": "FCFFE7", - "Chino": "CEC7A7", - "Chinook": "A8E3BD", - "Chocolate": "370202", - "Christalle": "33036B", - "Christi": "67A712", - "Christine": "E7730A", - "Chrome White": "E8F1D4", - "Cinder": "0E0E18", - "Cinderella": "FDE1DC", - "Cinnabar": "E34234", - "Cinnamon": "7B3F00", - "Cioccolato": "55280C", - "Citrine White": "FAF7D6", - "Citron": "9EA91F", - "Citrus": "A1C50A", - "Clairvoyant": "480656", - "Clam Shell": "D4B6AF", - "Claret": "7F1734", - "Classic Rose": "FBCCE7", - "Clay Ash": "BDC8B3", - "Clay Creek": "8A8360", - "Clear Day": "E9FFFD", - "Clementine": "E96E00", - "Clinker": "371D09", - "Cloud": "C7C4BF", - "Cloud Burst": "202E54", - "Cloudy": "ACA59F", - "Clover": "384910", - "Cobalt": "0047AB", - "Cocoa Bean": "481C1C", - "Cocoa Brown": "301F1E", - "Coconut Cream": "F8F7DC", - "Cod Gray": "0B0B0B", - "Coffee": "706555", - "Coffee Bean": "2A140E", - "Cognac": "9F381D", - "Cola": "3F2500", - "Cold Purple": "ABA0D9", - "Cold Turkey": "CEBABA", - "Colonial White": "FFEDBC", - "Comet": "5C5D75", - "Como": "517C66", - "Conch": "C9D9D2", - "Concord": "7C7B7A", - "Concrete": "F2F2F2", - "Confetti": "E9D75A", - "Congo Brown": "593737", - "Congress Blue": "02478E", - "Conifer": "ACDD4D", - "Contessa": "C6726B", - "Copper": "B87333", - "Copper Canyon": "7E3A15", - "Copper Rose": "996666", - "Copper Rust": "944747", - "Copperfield": "DA8A67", - "Coral": "FF7F50", - "Coral Red": "FF4040", - "Coral Reef": "C7BCA2", - "Coral Tree": "A86B6B", - "Corduroy": "606E68", - "Coriander": "C4D0B0", - "Cork": "40291D", - "Corn": "E7BF05", - "Corn Field": "F8FACD", - "Corn Harvest": "8B6B0B", - "Cornflower": "93CCEA", - "Cornflower Blue": "6495ED", - "Cornflower Lilac": "FFB0AC", - "Corvette": "FAD3A2", - "Cosmic": "76395D", - "Cosmos": "FFD8D9", - "Costa Del Sol": "615D30", - "Cotton Candy": "FFB7D5", - "Cotton Seed": "C2BDB6", - "County Green": "01371A", - "Cowboy": "4D282D", - "Crail": "B95140", - "Cranberry": "DB5079", - "Crater Brown": "462425", - "Cream": "FFFDD0", - "Cream Brulee": "FFE5A0", - "Cream Can": "F5C85C", - "Creole": "1E0F04", - "Crete": "737829", - "Crimson": "DC143C", - "Crocodile": "736D58", - "Crown of Thorns": "771F1F", - "Crowshead": "1C1208", - "Cruise": "B5ECDF", - "Crusoe": "004816", - "Crusta": "FD7B33", - "Cumin": "924321", - "Cumulus": "FDFFD5", - "Cupid": "FBBEDA", - "Curious Blue": "2596D1", - "Cutty Sark": "507672", - "Cyan / Aqua": "00FFFF", - "Cyprus": "003E40", - "Daintree": "012731", - "Dairy Cream": "F9E4BC", - "Daisy Bush": "4F2398", - "Dallas": "6E4B26", - "Dandelion": "FED85D", - "Danube": "6093D1", - "Dark Blue": "0000C8", - "Dark Burgundy": "770F05", - "Dark Ebony": "3C2005", - "Dark Fern": "0A480D", - "Dark Tan": "661010", - "Dawn": "A6A29A", - "Dawn Pink": "F3E9E5", - "De York": "7AC488", - "Deco": "D2DA97", - "Deep Blue": "220878", - "Deep Blush": "E47698", - "Deep Bronze": "4A3004", - "Deep Cerulean": "007BA7", - "Deep Cove": "051040", - "Deep Fir": "002900", - "Deep Forest Green": "182D09", - "Deep Koamaru": "1B127B", - "Deep Oak": "412010", - "Deep Sapphire": "082567", - "Deep Sea": "01826B", - "Deep Sea Green": "095859", - "Deep Teal": "003532", - "Del Rio": "B09A95", - "Dell": "396413", - "Delta": "A4A49D", - "Deluge": "7563A8", - "Denim": "1560BD", - "Derby": "FFEED8", - "Desert": "AE6020", - "Desert Sand": "EDC9AF", - "Desert Storm": "F8F8F7", - "Dew": "EAFFFE", - "Di Serria": "DB995E", - "Diesel": "130000", - "Dingley": "5D7747", - "Disco": "871550", - "Dixie": "E29418", - "Dodger Blue": "1E90FF", - "Dolly": "F9FF8B", - "Dolphin": "646077", - "Domino": "8E775E", - "Don Juan": "5D4C51", - "Donkey Brown": "A69279", - "Dorado": "6B5755", - "Double Colonial White": "EEE3AD", - "Double Pearl Lusta": "FCF4D0", - "Double Spanish White": "E6D7B9", - "Dove Gray": "6D6C6C", - "Downriver": "092256", - "Downy": "6FD0C5", - "Driftwood": "AF8751", - "Drover": "FDF7AD", - "Dull Lavender": "A899E6", - "Dune": "383533", - "Dust Storm": "E5CCC9", - "Dusty Gray": "A8989B", - "Eagle": "B6BAA4", - "Earls Green": "C9B93B", - "Early Dawn": "FFF9E6", - "East Bay": "414C7D", - "East Side": "AC91CE", - "Eastern Blue": "1E9AB0", - "Ebb": "E9E3E3", - "Ebony": "0C0B1D", - "Ebony Clay": "26283B", - "Eclipse": "311C17", - "Ecru White": "F5F3E5", - "Ecstasy": "FA7814", - "Eden": "105852", - "Edgewater": "C8E3D7", - "Edward": "A2AEAB", - "Egg Sour": "FFF4DD", - "Egg White": "FFEFC1", - "Eggplant": "614051", - "El Paso": "1E1708", - "El Salva": "8F3E33", - "Electric Lime": "CCFF00", - "Electric Violet": "8B00FF", - "Elephant": "123447", - "Elf Green": "088370", - "Elm": "1C7C7D", - "Emerald": "50C878", - "Eminence": "6C3082", - "Emperor": "514649", - "Empress": "817377", - "Endeavour": "0056A7", - "Energy Yellow": "F8DD5C", - "English Holly": "022D15", - "English Walnut": "3E2B23", - "Envy": "8BA690", - "Equator": "E1BC64", - "Espresso": "612718", - "Eternity": "211A0E", - "Eucalyptus": "278A5B", - "Eunry": "CFA39D", - "Evening Sea": "024E46", - "Everglade": "1C402E", - "Faded Jade": "427977", - "Fair Pink": "FFEFEC", - "Falcon": "7F626D", - "Fall Green": "ECEBBD", - "Falu Red": "801818", - "Fantasy": "FAF3F0", - "Fedora": "796A78", - "Feijoa": "9FDD8C", - "Fern": "63B76C", - "Fern Frond": "657220", - "Fern Green": "4F7942", - "Ferra": "704F50", - "Festival": "FBE96C", - "Feta": "F0FCEA", - "Fiery Orange": "B35213", - "Finch": "626649", - "Finlandia": "556D56", - "Finn": "692D54", - "Fiord": "405169", - "Fire": "AA4203", - "Fire Bush": "E89928", - "Firefly": "0E2A30", - "Flame Pea": "DA5B38", - "Flamenco": "FF7D07", - "Flamingo": "F2552A", - "Flax": "EEDC82", - "Flax Smoke": "7B8265", - "Flesh": "FFCBA4", - "Flint": "6F6A61", - "Flirt": "A2006D", - "Flush Mahogany": "CA3435", - "Flush Orange": "FF7F00", - "Foam": "D8FCFA", - "Fog": "D7D0FF", - "Foggy Gray": "CBCAB6", - "Forest Green": "228B22", - "Forget Me Not": "FFF1EE", - "Fountain Blue": "56B4BE", - "Frangipani": "FFDEB3", - "French Gray": "BDBDC6", - "French Lilac": "ECC7EE", - "French Pass": "BDEDFD", - "French Rose": "F64A8A", - "Fresh Eggplant": "990066", - "Friar Gray": "807E79", - "Fringy Flower": "B1E2C1", - "Froly": "F57584", - "Frost": "EDF5DD", - "Frosted Mint": "DBFFF8", - "Frostee": "E4F6E7", - "Fruit Salad": "4F9D5D", - "Fuchsia Blue": "7A58C1", - "Fuchsia Pink": "C154C1", - "Fuego": "BEDE0D", - "Fuel Yellow": "ECA927", - "Fun Blue": "1959A8", - "Fun Green": "016D39", - "Fuscous Gray": "54534D", - "Fuzzy Wuzzy Brown": "C45655", - "Gable Green": "163531", - "Gallery": "EFEFEF", - "Galliano": "DCB20C", - "Gamboge": "E49B0F", - "Geebung": "D18F1B", - "Genoa": "15736B", - "Geraldine": "FB8989", - "Geyser": "D4DFE2", - "Ghost": "C7C9D5", - "Gigas": "523C94", - "Gimblet": "B8B56A", - "Gin": "E8F2EB", - "Gin Fizz": "FFF9E2", - "Givry": "F8E4BF", - "Glacier": "80B3C4", - "Glade Green": "61845F", - "Go Ben": "726D4E", - "Goblin": "3D7D52", - "Gold": "FFD700", - "Gold Drop": "F18200", - "Gold Sand": "E6BE8A", - "Gold Tips": "DEBA13", - "Golden Bell": "E28913", - "Golden Dream": "F0D52D", - "Golden Fizz": "F5FB3D", - "Golden Glow": "FDE295", - "Golden Grass": "DAA520", - "Golden Sand": "F0DB7D", - "Golden Tainoi": "FFCC5C", - "Goldenrod": "FCD667", - "Gondola": "261414", - "Gordons Green": "0B1107", - "Gorse": "FFF14F", - "Gossamer": "069B81", - "Gossip": "D2F8B0", - "Gothic": "6D92A1", - "Governor Bay": "2F3CB3", - "Grain Brown": "E4D5B7", - "Grandis": "FFD38C", - "Granite Green": "8D8974", - "Granny Apple": "D5F6E3", - "Granny Smith": "84A0A0", - "Granny Smith Apple": "9DE093", - "Grape": "381A51", - "Graphite": "251607", - "Gravel": "4A444B", - "Gray": "808080", - "Gray Asparagus": "465945", - "Gray Chateau": "A2AAB3", - "Gray Nickel": "C3C3BD", - "Gray Nurse": "E7ECE6", - "Gray Olive": "A9A491", - "Gray Suit": "C1BECD", - "Green": "00FF00", - "Green Haze": "01A368", - "Green House": "24500F", - "Green Kelp": "25311C", - "Green Leaf": "436A0D", - "Green Mist": "CBD3B0", - "Green Pea": "1D6142", - "Green Smoke": "A4AF6E", - "Green Spring": "B8C1B1", - "Green Vogue": "032B52", - "Green Waterloo": "101405", - "Green White": "E8EBE0", - "Green Yellow": "ADFF2F", - "Grenadier": "D54600", - "Guardsman Red": "BA0101", - "Gulf Blue": "051657", - "Gulf Stream": "80B3AE", - "Gull Gray": "9DACB7", - "Gum Leaf": "B6D3BF", - "Gumbo": "7CA1A6", - "Gun Powder": "414257", - "Gunsmoke": "828685", - "Gurkha": "9A9577", - "Hacienda": "98811B", - "Hairy Heath": "6B2A14", - "Haiti": "1B1035", - "Half Baked": "85C4CC", - "Half Colonial White": "FDF6D3", - "Half Dutch White": "FEF7DE", - "Half Spanish White": "FEF4DB", - "Half and Half": "FFFEE1", - "Hampton": "E5D8AF", - "Harlequin": "3FFF00", - "Harp": "E6F2EA", - "Harvest Gold": "E0B974", - "Havelock Blue": "5590D9", - "Hawaiian Tan": "9D5616", - "Hawkes Blue": "D4E2FC", - "Heath": "541012", - "Heather": "B7C3D0", - "Heathered Gray": "B6B095", - "Heavy Metal": "2B3228", - "Heliotrope": "DF73FF", - "Hemlock": "5E5D3B", - "Hemp": "907874", - "Hibiscus": "B6316C", - "Highland": "6F8E63", - "Hillary": "ACA586", - "Himalaya": "6A5D1B", - "Hint of Green": "E6FFE9", - "Hint of Red": "FBF9F9", - "Hint of Yellow": "FAFDE4", - "Hippie Blue": "589AAF", - "Hippie Green": "53824B", - "Hippie Pink": "AE4560", - "Hit Gray": "A1ADB5", - "Hit Pink": "FFAB81", - "Hokey Pokey": "C8A528", - "Hoki": "65869F", - "Holly": "011D13", - "Hollywood Cerise": "F400A1", - "Honey Flower": "4F1C70", - "Honeysuckle": "EDFC84", - "Hopbush": "D06DA1", - "Horizon": "5A87A0", - "Horses Neck": "604913", - "Hot Cinnamon": "D2691E", - "Hot Pink": "FF69B4", - "Hot Toddy": "B38007", - "Humming Bird": "CFF9F3", - "Hunter Green": "161D10", - "Hurricane": "877C7B", - "Husk": "B7A458", - "Ice Cold": "B1F4E7", - "Iceberg": "DAF4F0", - "Illusion": "F6A4C9", - "Inch Worm": "B0E313", - "Indian Khaki": "C3B091", - "Indian Tan": "4D1E01", - "Indigo": "4F69C6", - "Indochine": "C26B03", - "International Klein Blue": "002FA7", - "International Orange": "FF4F00", - "Irish Coffee": "5F3D26", - "Iroko": "433120", - "Iron": "D4D7D9", - "Ironside Gray": "676662", - "Ironstone": "86483C", - "Island Spice": "FFFCEE", - "Ivory": "FFFFF0", - "Jacaranda": "2E0329", - "Jacarta": "3A2A6A", - "Jacko Bean": "2E1905", - "Jacksons Purple": "20208D", - "Jade": "00A86B", - "Jaffa": "EF863F", - "Jagged Ice": "C2E8E5", - "Jagger": "350E57", - "Jaguar": "080110", - "Jambalaya": "5B3013", - "Janna": "F4EBD3", - "Japanese Laurel": "0A6906", - "Japanese Maple": "780109", - "Japonica": "D87C63", - "Java": "1FC2C2", - "Jazzberry Jam": "A50B5E", - "Jelly Bean": "297B9A", - "Jet Stream": "B5D2CE", - "Jewel": "126B40", - "Jon": "3B1F1F", - "Jonquil": "EEFF9A", - "Jordy Blue": "8AB9F1", - "Judge Gray": "544333", - "Jumbo": "7C7B82", - "Jungle Green": "29AB87", - "Jungle Mist": "B4CFD3", - "Juniper": "6D9292", - "Just Right": "ECCDB9", - "Kabul": "5E483E", - "Kaitoke Green": "004620", - "Kangaroo": "C6C8BD", - "Karaka": "1E1609", - "Karry": "FFEAD4", - "Kashmir Blue": "507096", - "Kelp": "454936", - "Kenyan Copper": "7C1C05", - "Keppel": "3AB09E", - "Key Lime Pie": "BFC921", - "Khaki": "F0E68C", - "Kidnapper": "E1EAD4", - "Kilamanjaro": "240C02", - "Killarney": "3A6A47", - "Kimberly": "736C9F", - "Kingfisher Daisy": "3E0480", - "Kobi": "E79FC4", - "Kokoda": "6E6D57", - "Korma": "8F4B0E", - "Koromiko": "FFBD5F", - "Kournikova": "FFE772", - "Kumera": "886221", - "La Palma": "368716", - "La Rioja": "B3C110", - "Las Palmas": "C6E610", - "Laser": "C8B568", - "Laser Lemon": "FFFF66", - "Laurel": "749378", - "Lavender": "B57EDC", - "Lavender Gray": "BDBBD7", - "Lavender Magenta": "EE82EE", - "Lavender Pink": "FBAED2", - "Lavender Purple": "967BB6", - "Lavender Rose": "FBA0E3", - "Lavender blush": "FFF0F5", - "Leather": "967059", - "Lemon": "FDE910", - "Lemon Chiffon": "FFFACD", - "Lemon Ginger": "AC9E22", - "Lemon Grass": "9B9E8F", - "Light Apricot": "FDD5B1", - "Light Orchid": "E29CD2", - "Light Wisteria": "C9A0DC", - "Lightning Yellow": "FCC01E", - "Lilac": "C8A2C8", - "Lilac Bush": "9874D3", - "Lily": "C8AABF", - "Lily White": "E7F8FF", - "Lima": "76BD17", - "Lime": "BFFF00", - "Limeade": "6F9D02", - "Limed Ash": "747D63", - "Limed Oak": "AC8A56", - "Limed Spruce": "394851", - "Linen": "FAF0E6", - "Link Water": "D9E4F5", - "Lipstick": "AB0563", - "Lisbon Brown": "423921", - "Livid Brown": "4D282E", - "Loafer": "EEF4DE", - "Loblolly": "BDC9CE", - "Lochinvar": "2C8C84", - "Lochmara": "007EC7", - "Locust": "A8AF8E", - "Log Cabin": "242A1D", - "Logan": "AAA9CD", - "Lola": "DFCFDB", - "London Hue": "BEA6C3", - "Lonestar": "6D0101", - "Lotus": "863C3C", - "Loulou": "460B41", - "Lucky": "AF9F1C", - "Lucky Point": "1A1A68", - "Lunar Green": "3C493A", - "Luxor Gold": "A7882C", - "Lynch": "697E9A", - "Mabel": "D9F7FF", - "Macaroni and Cheese": "FFB97B", - "Madang": "B7F0BE", - "Madison": "09255D", - "Madras": "3F3002", - "Magenta / Fuchsia": "FF00FF", - "Magic Mint": "AAF0D1", - "Magnolia": "F8F4FF", - "Mahogany": "4E0606", - "Mai Tai": "B06608", - "Maize": "F5D5A0", - "Makara": "897D6D", - "Mako": "444954", - "Malachite": "0BDA51", - "Malibu": "7DC8F7", - "Mallard": "233418", - "Malta": "BDB2A1", - "Mamba": "8E8190", - "Manatee": "8D90A1", - "Mandalay": "AD781B", - "Mandy": "E25465", - "Mandys Pink": "F2C3B2", - "Mango Tango": "E77200", - "Manhattan": "F5C999", - "Mantis": "74C365", - "Mantle": "8B9C90", - "Manz": "EEEF78", - "Mardi Gras": "350036", - "Marigold": "B98D28", - "Marigold Yellow": "FBE870", - "Mariner": "286ACD", - "Maroon": "800000", - "Maroon Flush": "C32148", - "Maroon Oak": "520C17", - "Marshland": "0B0F08", - "Martini": "AFA09E", - "Martinique": "363050", - "Marzipan": "F8DB9D", - "Masala": "403B38", - "Matisse": "1B659D", - "Matrix": "B05D54", - "Matterhorn": "4E3B41", - "Mauve": "E0B0FF", - "Mauvelous": "F091A9", - "Maverick": "D8C2D5", - "Medium Carmine": "AF4035", - "Medium Purple": "9370DB", - "Medium Red Violet": "BB3385", - "Melanie": "E4C2D5", - "Melanzane": "300529", - "Melon": "FEBAAD", - "Melrose": "C7C1FF", - "Mercury": "E5E5E5", - "Merino": "F6F0E6", - "Merlin": "413C37", - "Merlot": "831923", - "Metallic Bronze": "49371B", - "Metallic Copper": "71291D", - "Meteor": "D07D12", - "Meteorite": "3C1F76", - "Mexican Red": "A72525", - "Mid Gray": "5F5F6E", - "Midnight": "011635", - "Midnight Blue": "003366", - "Midnight Moss": "041004", - "Mikado": "2D2510", - "Milan": "FAFFA4", - "Milano Red": "B81104", - "Milk Punch": "FFF6D4", - "Millbrook": "594433", - "Mimosa": "F8FDD3", - "Mindaro": "E3F988", - "Mine Shaft": "323232", - "Mineral Green": "3F5D53", - "Ming": "36747D", - "Minsk": "3F307F", - "Mint Green": "98FF98", - "Mint Julep": "F1EEC1", - "Mint Tulip": "C4F4EB", - "Mirage": "161928", - "Mischka": "D1D2DD", - "Mist Gray": "C4C4BC", - "Mobster": "7F7589", - "Moccaccino": "6E1D14", - "Mocha": "782D19", - "Mojo": "C04737", - "Mona Lisa": "FFA194", - "Monarch": "8B0723", - "Mondo": "4A3C30", - "Mongoose": "B5A27F", - "Monsoon": "8A8389", - "Monte Carlo": "83D0C6", - "Monza": "C7031E", - "Moody Blue": "7F76D3", - "Moon Glow": "FCFEDA", - "Moon Mist": "DCDDCC", - "Moon Raker": "D6CEF6", - "Morning Glory": "9EDEE0", - "Morocco Brown": "441D00", - "Mortar": "504351", - "Mosque": "036A6E", - "Moss Green": "ADDFAD", - "Mountain Meadow": "1AB385", - "Mountain Mist": "959396", - "Mountbatten Pink": "997A8D", - "Muddy Waters": "B78E5C", - "Muesli": "AA8B5B", - "Mulberry": "C54B8C", - "Mulberry Wood": "5C0536", - "Mule Fawn": "8C472F", - "Mulled Wine": "4E4562", - "Mustard": "FFDB58", - "My Pink": "D69188", - "My Sin": "FFB31F", - "Mystic": "E2EBED", - "Nandor": "4B5D52", - "Napa": "ACA494", - "Narvik": "EDF9F1", - "Natural Gray": "8B8680", - "Navajo White": "FFDEAD", - "Navy Blue": "000080", - "Nebula": "CBDBD6", - "Negroni": "FFE2C5", - "Neon Carrot": "FF9933", - "Nepal": "8EABC1", - "Neptune": "7CB7BB", - "Nero": "140600", - "Nevada": "646E75", - "New Orleans": "F3D69D", - "New York Pink": "D7837F", - "Niagara": "06A189", - "Night Rider": "1F120F", - "Night Shadz": "AA375A", - "Nile Blue": "193751", - "Nobel": "B7B1B1", - "Nomad": "BAB1A2", - "Norway": "A8BD9F", - "Nugget": "C59922", - "Nutmeg": "81422C", - "Nutmeg Wood Finish": "683600", - "Oasis": "FEEFCE", - "Observatory": "02866F", - "Ocean Green": "41AA78", - "Ochre": "CC7722", - "Off Green": "E6F8F3", - "Off Yellow": "FEF9E3", - "Oil": "281E15", - "Old Brick": "901E1E", - "Old Copper": "724A2F", - "Old Gold": "CFB53B", - "Old Lace": "FDF5E6", - "Old Lavender": "796878", - "Old Rose": "C08081", - "Olive": "808000", - "Olive Drab": "6B8E23", - "Olive Green": "B5B35C", - "Olive Haze": "8B8470", - "Olivetone": "716E10", - "Olivine": "9AB973", - "Onahau": "CDF4FF", - "Onion": "2F270E", - "Opal": "A9C6C2", - "Opium": "8E6F70", - "Oracle": "377475", - "Orange": "FF681F", - "Orange Peel": "FFA000", - "Orange Roughy": "C45719", - "Orange White": "FEFCED", - "Orchid": "DA70D6", - "Orchid White": "FFFDF3", - "Oregon": "9B4703", - "Orient": "015E85", - "Oriental Pink": "C69191", - "Orinoco": "F3FBD4", - "Oslo Gray": "878D91", - "Ottoman": "E9F8ED", - "Outer Space": "2D383A", - "Outrageous Orange": "FF6037", - "Oxford Blue": "384555", - "Oxley": "779E86", - "Oyster Bay": "DAFAFF", - "Oyster Pink": "E9CECD", - "Paarl": "A65529", - "Pablo": "776F61", - "Pacific Blue": "009DC4", - "Pacifika": "778120", - "Paco": "411F10", - "Padua": "ADE6C4", - "Pale Canary": "FFFF99", - "Pale Leaf": "C0D3B9", - "Pale Oyster": "988D77", - "Pale Prim": "FDFEB8", - "Pale Rose": "FFE1F2", - "Pale Sky": "6E7783", - "Pale Slate": "C3BFC1", - "Palm Green": "09230F", - "Palm Leaf": "19330E", - "Pampas": "F4F2EE", - "Panache": "EAF6EE", - "Pancho": "EDCDAB", - "Papaya Whip": "FFEFD5", - "Paprika": "8D0226", - "Paradiso": "317D82", - "Parchment": "F1E9D2", - "Paris Daisy": "FFF46E", - "Paris M": "26056A", - "Paris White": "CADCD4", - "Parsley": "134F19", - "Pastel Green": "77DD77", - "Pastel Pink": "FFD1DC", - "Patina": "639A8F", - "Pattens Blue": "DEF5FF", - "Paua": "260368", - "Pavlova": "D7C498", - "Peach": "FFE5B4", - "Peach Cream": "FFF0DB", - "Peach Orange": "FFCC99", - "Peach Schnapps": "FFDCD6", - "Peach Yellow": "FADFAD", - "Peanut": "782F16", - "Pear": "D1E231", - "Pearl Bush": "E8E0D5", - "Pearl Lusta": "FCF4DC", - "Peat": "716B56", - "Pelorous": "3EABBF", - "Peppermint": "E3F5E1", - "Perano": "A9BEF2", - "Perfume": "D0BEF8", - "Periglacial Blue": "E1E6D6", - "Periwinkle": "CCCCFF", - "Periwinkle Gray": "C3CDE6", - "Persian Blue": "1C39BB", - "Persian Green": "00A693", - "Persian Indigo": "32127A", - "Persian Pink": "F77FBE", - "Persian Plum": "701C1C", - "Persian Red": "CC3333", - "Persian Rose": "FE28A2", - "Persimmon": "FF6B53", - "Peru Tan": "7F3A02", - "Pesto": "7C7631", - "Petite Orchid": "DB9690", - "Pewter": "96A8A1", - "Pharlap": "A3807B", - "Picasso": "FFF39D", - "Pickled Bean": "6E4826", - "Pickled Bluewood": "314459", - "Picton Blue": "45B1E8", - "Pig Pink": "FDD7E4", - "Pigeon Post": "AFBDD9", - "Pigment Indigo": "4B0082", - "Pine Cone": "6D5E54", - "Pine Glade": "C7CD90", - "Pine Green": "01796F", - "Pine Tree": "171F04", - "Pink": "FFC0CB", - "Pink Flamingo": "FF66FF", - "Pink Flare": "E1C0C8", - "Pink Lace": "FFDDF4", - "Pink Lady": "FFF1D8", - "Pink Salmon": "FF91A4", - "Pink Swan": "BEB5B7", - "Piper": "C96323", - "Pipi": "FEF4CC", - "Pippin": "FFE1DF", - "Pirate Gold": "BA7F03", - "Pistachio": "9DC209", - "Pixie Green": "C0D8B6", - "Pizazz": "FF9000", - "Pizza": "C99415", - "Plantation": "27504B", - "Plum": "843179", - "Pohutukawa": "8F021C", - "Polar": "E5F9F6", - "Polo Blue": "8DA8CC", - "Pomegranate": "F34723", - "Pompadour": "660045", - "Porcelain": "EFF2F3", - "Porsche": "EAAE69", - "Port Gore": "251F4F", - "Portafino": "FFFFB4", - "Portage": "8B9FEE", - "Portica": "F9E663", - "Pot Pourri": "F5E7E2", - "Potters Clay": "8C5738", - "Powder Ash": "BCC9C2", - "Powder Blue": "B0E0E6", - "Prairie Sand": "9A3820", - "Prelude": "D0C0E5", - "Prim": "F0E2EC", - "Primrose": "EDEA99", - "Provincial Pink": "FEF5F1", - "Prussian Blue": "003153", - "Puce": "CC8899", - "Pueblo": "7D2C14", - "Puerto Rico": "3FC1AA", - "Pumice": "C2CAC4", - "Pumpkin": "FF7518", - "Pumpkin Skin": "B1610B", - "Punch": "DC4333", - "Punga": "4D3D14", - "Purple": "660099", - "Purple Heart": "652DC1", - "Purple Mountain's Majesty": "9678B6", - "Purple Pizzazz": "FF00CC", - "Putty": "E7CD8C", - "Quarter Pearl Lusta": "FFFDF4", - "Quarter Spanish White": "F7F2E1", - "Quicksand": "BD978E", - "Quill Gray": "D6D6D1", - "Quincy": "623F2D", - "Racing Green": "0C1911", - "Radical Red": "FF355E", - "Raffia": "EADAB8", - "Rainee": "B9C8AC", - "Rajah": "F7B668", - "Rangitoto": "2E3222", - "Rangoon Green": "1C1E13", - "Raven": "727B89", - "Raw Sienna": "D27D46", - "Raw Umber": "734A12", - "Razzle Dazzle Rose": "FF33CC", - "Razzmatazz": "E30B5C", - "Rebel": "3C1206", - "Red": "FF0000", - "Red Beech": "7B3801", - "Red Berry": "8E0000", - "Red Damask": "DA6A41", - "Red Devil": "860111", - "Red Orange": "FF3F34", - "Red Oxide": "6E0902", - "Red Ribbon": "ED0A3F", - "Red Robin": "80341F", - "Red Stage": "D05F04", - "Red Violet": "C71585", - "Redwood": "5D1E0F", - "Reef": "C9FFA2", - "Reef Gold": "9F821C", - "Regal Blue": "013F6A", - "Regent Gray": "86949F", - "Regent St Blue": "AAD6E6", - "Remy": "FEEBF3", - "Reno Sand": "A86515", - "Resolution Blue": "002387", - "Revolver": "2C1632", - "Rhino": "2E3F62", - "Rice Cake": "FFFEF0", - "Rice Flower": "EEFFE2", - "Rich Gold": "A85307", - "Rio Grande": "BBD009", - "Ripe Lemon": "F4D81C", - "Ripe Plum": "410056", - "Riptide": "8BE6D8", - "River Bed": "434C59", - "Rob Roy": "EAC674", - "Robin's Egg Blue": "00CCCC", - "Rock": "4D3833", - "Rock Blue": "9EB1CD", - "Rock Spray": "BA450C", - "Rodeo Dust": "C9B29B", - "Rolling Stone": "747D83", - "Roman": "DE6360", - "Roman Coffee": "795D4C", - "Romance": "FFFEFD", - "Romantic": "FFD2B7", - "Ronchi": "ECC54E", - "Roof Terracotta": "A62F20", - "Rope": "8E4D1E", - "Rose": "FF007F", - "Rose Bud": "FBB2A3", - "Rose Bud Cherry": "800B47", - "Rose Fog": "E7BCB4", - "Rose White": "FFF6F5", - "Rose of Sharon": "BF5500", - "Rosewood": "65000B", - "Roti": "C6A84B", - "Rouge": "A23B6C", - "Royal Blue": "4169E1", - "Royal Heath": "AB3472", - "Royal Purple": "6B3FA0", - "Rum": "796989", - "Rum Swizzle": "F9F8E4", - "Russet": "80461B", - "Russett": "755A57", - "Rust": "B7410E", - "Rustic Red": "480404", - "Rusty Nail": "86560A", - "Saddle": "4C3024", - "Saddle Brown": "583401", - "Saffron": "F4C430", - "Saffron Mango": "F9BF58", - "Sage": "9EA587", - "Sahara": "B7A214", - "Sahara Sand": "F1E788", - "Sail": "B8E0F9", - "Salem": "097F4B", - "Salmon": "FF8C69", - "Salomie": "FEDB8D", - "Salt Box": "685E6E", - "Saltpan": "F1F7F2", - "Sambuca": "3A2010", - "San Felix": "0B6207", - "San Juan": "304B6A", - "San Marino": "456CAC", - "Sand Dune": "826F65", - "Sandal": "AA8D6F", - "Sandrift": "AB917A", - "Sandstone": "796D62", - "Sandwisp": "F5E7A2", - "Sandy Beach": "FFEAC8", - "Sandy brown": "F4A460", - "Sangria": "92000A", - "Sanguine Brown": "8D3D38", - "Santa Fe": "B16D52", - "Santas Gray": "9FA0B1", - "Sapling": "DED4A4", - "Sapphire": "2F519E", - "Saratoga": "555B10", - "Satin Linen": "E6E4D4", - "Sauvignon": "FFF5F3", - "Sazerac": "FFF4E0", - "Scampi": "675FA6", - "Scandal": "CFFAF4", - "Scarlet": "FF2400", - "Scarlet Gum": "431560", - "Scarlett": "950015", - "Scarpa Flow": "585562", - "Schist": "A9B497", - "School bus Yellow": "FFD800", - "Schooner": "8B847E", - "Science Blue": "0066CC", - "Scooter": "2EBFD4", - "Scorpion": "695F62", - "Scotch Mist": "FFFBDC", - "Screamin' Green": "66FF66", - "Sea Buckthorn": "FBA129", - "Sea Green": "2E8B57", - "Sea Mist": "C5DBCA", - "Sea Nymph": "78A39C", - "Sea Pink": "ED989E", - "Seagull": "80CCEA", - "Seance": "731E8F", - "Seashell": "F1F1F1", - "Seashell Peach": "FFF5EE", - "Seaweed": "1B2F11", - "Selago": "F0EEFD", - "Selective Yellow": "FFBA00", - "Sepia": "704214", - "Sepia Black": "2B0202", - "Sepia Skin": "9E5B40", - "Serenade": "FFF4E8", - "Shadow": "837050", - "Shadow Green": "9AC2B8", - "Shady Lady": "AAA5A9", - "Shakespeare": "4EABD1", - "Shalimar": "FBFFBA", - "Shamrock": "33CC99", - "Shark": "25272C", - "Sherpa Blue": "004950", - "Sherwood Green": "02402C", - "Shilo": "E8B9B3", - "Shingle Fawn": "6B4E31", - "Ship Cove": "788BBA", - "Ship Gray": "3E3A44", - "Shiraz": "B20931", - "Shocking": "E292C0", - "Shocking Pink": "FC0FC0", - "Shuttle Gray": "5F6672", - "Siam": "646A54", - "Sidecar": "F3E7BB", - "Silk": "BDB1A8", - "Silver": "C0C0C0", - "Silver Chalice": "ACACAC", - "Silver Rust": "C9C0BB", - "Silver Sand": "BFC1C2", - "Silver Tree": "66B58F", - "Sinbad": "9FD7D3", - "Siren": "7A013A", - "Sirocco": "718080", - "Sisal": "D3CBBA", - "Skeptic": "CAE6DA", - "Sky Blue": "76D7EA", - "Slate Gray": "708090", - "Smalt": "003399", - "Smalt Blue": "51808F", - "Smoky": "605B73", - "Snow Drift": "F7FAF7", - "Snow Flurry": "E4FFD1", - "Snowy Mint": "D6FFDB", - "Snuff": "E2D8ED", - "Soapstone": "FFFBF9", - "Soft Amber": "D1C6B4", - "Soft Peach": "F5EDEF", - "Solid Pink": "893843", - "Solitaire": "FEF8E2", - "Solitude": "EAF6FF", - "Sorbus": "FD7C07", - "Sorrell Brown": "CEB98F", - "Soya Bean": "6A6051", - "Spanish Green": "819885", - "Spectra": "2F5A57", - "Spice": "6A442E", - "Spicy Mix": "885342", - "Spicy Mustard": "74640D", - "Spicy Pink": "816E71", - "Spindle": "B6D1EA", - "Spray": "79DEEC", - "Spring Green": "00FF7F", - "Spring Leaves": "578363", - "Spring Rain": "ACCBB1", - "Spring Sun": "F6FFDC", - "Spring Wood": "F8F6F1", - "Sprout": "C1D7B0", - "Spun Pearl": "AAABB7", - "Squirrel": "8F8176", - "St Tropaz": "2D569B", - "Stack": "8A8F8A", - "Star Dust": "9F9F9C", - "Stark White": "E5D7BD", - "Starship": "ECF245", - "Steel Blue": "4682B4", - "Steel Gray": "262335", - "Stiletto": "9C3336", - "Stonewall": "928573", - "Storm Dust": "646463", - "Storm Gray": "717486", - "Stratos": "000741", - "Straw": "D4BF8D", - "Strikemaster": "956387", - "Stromboli": "325D52", - "Studio": "714AB2", - "Submarine": "BAC7C9", - "Sugar Cane": "F9FFF6", - "Sulu": "C1F07C", - "Summer Green": "96BBAB", - "Sun": "FBAC13", - "Sundance": "C9B35B", - "Sundown": "FFB1B3", - "Sunflower": "E4D422", - "Sunglo": "E16865", - "Sunglow": "FFCC33", - "Sunset Orange": "FE4C40", - "Sunshade": "FF9E2C", - "Supernova": "FFC901", - "Surf": "BBD7C1", - "Surf Crest": "CFE5D2", - "Surfie Green": "0C7A79", - "Sushi": "87AB39", - "Suva Gray": "888387", - "Swamp": "001B1C", - "Swamp Green": "ACB78E", - "Swans Down": "DCF0EA", - "Sweet Corn": "FBEA8C", - "Sweet Pink": "FD9FA2", - "Swirl": "D3CDC5", - "Swiss Coffee": "DDD6D5", - "Sycamore": "908D39", - "Tabasco": "A02712", - "Tacao": "EDB381", - "Tacha": "D6C562", - "Tahiti Gold": "E97C07", - "Tahuna Sands": "EEF0C8", - "Tall Poppy": "B32D29", - "Tallow": "A8A589", - "Tamarillo": "991613", - "Tamarind": "341515", - "Tan": "D2B48C", - "Tan Hide": "FA9D5A", - "Tana": "D9DCC1", - "Tangaroa": "03163C", - "Tangerine": "F28500", - "Tango": "ED7A1C", - "Tapa": "7B7874", - "Tapestry": "B05E81", - "Tara": "E1F6E8", - "Tarawera": "073A50", - "Tasman": "CFDCCF", - "Taupe": "483C32", - "Taupe Gray": "B3AF95", - "Tawny Port": "692545", - "Te Papa Green": "1E433C", - "Tea": "C1BAB0", - "Tea Green": "D0F0C0", - "Teak": "B19461", - "Teal": "008080", - "Teal Blue": "044259", - "Temptress": "3B000B", - "Tenn": "CD5700", - "Tequila": "FFE6C7", - "Terracotta": "E2725B", - "Texas": "F8F99C", - "Texas Rose": "FFB555", - "Thatch": "B69D98", - "Thatch Green": "403D19", - "Thistle": "D8BFD8", - "Thistle Green": "CCCAA8", - "Thunder": "33292F", - "Thunderbird": "C02B18", - "Tia Maria": "C1440E", - "Tiara": "C3D1D1", - "Tiber": "063537", - "Tickle Me Pink": "FC80A5", - "Tidal": "F1FFAD", - "Tide": "BFB8B0", - "Timber Green": "16322C", - "Timberwolf": "D9D6CF", - "Titan White": "F0EEFF", - "Toast": "9A6E61", - "Tobacco Brown": "715D47", - "Toledo": "3A0020", - "Tolopea": "1B0245", - "Tom Thumb": "3F583B", - "Tonys Pink": "E79F8C", - "Topaz": "7C778A", - "Torch Red": "FD0E35", - "Torea Bay": "0F2D9E", - "Tory Blue": "1450AA", - "Tosca": "8D3F3F", - "Totem Pole": "991B07", - "Tower Gray": "A9BDBF", - "Tradewind": "5FB3AC", - "Tranquil": "E6FFFF", - "Travertine": "FFFDE8", - "Tree Poppy": "FC9C1D", - "Treehouse": "3B2820", - "Trendy Green": "7C881A", - "Trendy Pink": "8C6495", - "Trinidad": "E64E03", - "Tropical Blue": "C3DDF9", - "Tropical Rain Forest": "00755E", - "Trout": "4A4E5A", - "True V": "8A73D6", - "Tuatara": "363534", - "Tuft Bush": "FFDDCD", - "Tulip Tree": "EAB33B", - "Tumbleweed": "DEA681", - "Tuna": "353542", - "Tundora": "4A4244", - "Turbo": "FAE600", - "Turkish Rose": "B57281", - "Turmeric": "CABB48", - "Turquoise": "30D5C8", - "Turquoise Blue": "6CDAE7", - "Turtle Green": "2A380B", - "Tuscany": "BD5E2E", - "Tusk": "EEF3C3", - "Tussock": "C5994B", - "Tutu": "FFF1F9", - "Twilight": "E4CFDE", - "Twilight Blue": "EEFDFF", - "Twine": "C2955D", - "Tyrian Purple": "66023C", - "Ultramarine": "120A8F", - "Valencia": "D84437", - "Valentino": "350E42", - "Valhalla": "2B194F", - "Van Cleef": "49170C", - "Vanilla": "D1BEA8", - "Vanilla Ice": "F3D9DF", - "Varden": "FFF6DF", - "Venetian Red": "72010F", - "Venice Blue": "055989", - "Venus": "928590", - "Verdigris": "5D5E37", - "Verdun Green": "495400", - "Vermilion": "FF4D00", - "Vesuvius": "B14A0B", - "Victoria": "534491", - "Vida Loca": "549019", - "Viking": "64CCDB", - "Vin Rouge": "983D61", - "Viola": "CB8FA9", - "Violent Violet": "290C5E", - "Violet": "240A40", - "Violet Eggplant": "991199", - "Violet Red": "F7468A", - "Viridian": "40826D", - "Viridian Green": "678975", - "Vis Vis": "FFEFA1", - "Vista Blue": "8FD6B4", - "Vista White": "FCF8F7", - "Vivid Tangerine": "FF9980", - "Vivid Violet": "803790", - "Voodoo": "533455", - "Vulcan": "10121D", - "Wafer": "DECBC6", - "Waikawa Gray": "5A6E9C", - "Waiouru": "363C0D", - "Walnut": "773F1A", - "Wasabi": "788A25", - "Water Leaf": "A1E9DE", - "Watercourse": "056F57", - "Waterloo ": "7B7C94", - "Wattle": "DCD747", - "Watusi": "FFDDCF", - "Wax Flower": "FFC0A8", - "We Peep": "F7DBE6", - "Web Orange": "FFA500", - "Wedgewood": "4E7F9E", - "Well Read": "B43332", - "West Coast": "625119", - "West Side": "FF910F", - "Westar": "DCD9D2", - "Wewak": "F19BAB", - "Wheat": "F5DEB3", - "Wheatfield": "F3EDCF", - "Whiskey": "D59A6F", - "Whisper": "F7F5FA", - "White": "FFFFFF", - "White Ice": "DDF9F1", - "White Lilac": "F8F7FC", - "White Linen": "F8F0E8", - "White Pointer": "FEF8FF", - "White Rock": "EAE8D4", - "Wild Blue Yonder": "7A89B8", - "Wild Rice": "ECE090", - "Wild Sand": "F4F4F4", - "Wild Strawberry": "FF3399", - "Wild Watermelon": "FD5B78", - "Wild Willow": "B9C46A", - "William": "3A686C", - "Willow Brook": "DFECDA", - "Willow Grove": "65745D", - "Windsor": "3C0878", - "Wine Berry": "591D35", - "Winter Hazel": "D5D195", - "Wisp Pink": "FEF4F8", - "Wisteria": "9771B5", - "Wistful": "A4A6D3", - "Witch Haze": "FFFC99", - "Wood Bark": "261105", - "Woodland": "4D5328", - "Woodrush": "302A0F", - "Woodsmoke": "0C0D0F", - "Woody Brown": "483131", - "Xanadu": "738678", - "Yellow": "FFFF00", - "Yellow Green": "C5E17A", - "Yellow Metal": "716338", - "Yellow Orange": "FFAE42", - "Yellow Sea": "FEA904", - "Your Pink": "FFC3C0", - "Yukon Gold": "7B6608", - "Yuma": "CEC291", - "Zambezi": "685558", - "Zanah": "DAECD6", - "Zest": "E5841B", - "Zeus": "292319", - "Ziggurat": "BFDBE2", - "Zinnwaldite": "EBC2AF", - "Zircon": "F4F8FF", - "Zombie": "E4D69B", - "Zorba": "A59B91", - "Zuccini": "044022", - "Zumthor": "EDF6FF" -} diff --git a/bot/resources/utilities/ryanzec_colours.json b/bot/resources/utilities/ryanzec_colours.json new file mode 100644 index 00000000..7b89f052 --- /dev/null +++ b/bot/resources/utilities/ryanzec_colours.json @@ -0,0 +1,1568 @@ +{ + "Abbey": "4C4F56", + "Acadia": "1B1404", + "Acapulco": "7CB0A1", + "Aero Blue": "C9FFE5", + "Affair": "714693", + "Akaroa": "D4C4A8", + "Alabaster": "FAFAFA", + "Albescent White": "F5E9D3", + "Algae Green": "93DFB8", + "Alice Blue": "F0F8FF", + "Alizarin Crimson": "E32636", + "Allports": "0076A3", + "Almond": "EED9C4", + "Almond Frost": "907B71", + "Alpine": "AF8F2C", + "Alto": "DBDBDB", + "Aluminium": "A9ACB6", + "Amaranth": "E52B50", + "Amazon": "3B7A57", + "Amber": "FFBF00", + "Americano": "87756E", + "Amethyst": "9966CC", + "Amethyst Smoke": "A397B4", + "Amour": "F9EAF3", + "Amulet": "7B9F80", + "Anakiwa": "9DE5FF", + "Antique Brass": "C88A65", + "Antique Bronze": "704A07", + "Anzac": "E0B646", + "Apache": "DFBE6F", + "Apple": "4FA83D", + "Apple Blossom": "AF4D43", + "Apple Green": "E2F3EC", + "Apricot": "EB9373", + "Apricot Peach": "FBCEB1", + "Apricot White": "FFFEEC", + "Aqua Deep": "014B43", + "Aqua Forest": "5FA777", + "Aqua Haze": "EDF5F5", + "Aqua Island": "A1DAD7", + "Aqua Spring": "EAF9F5", + "Aqua Squeeze": "E8F5F2", + "Aquamarine": "7FFFD4", + "Aquamarine Blue": "71D9E2", + "Arapawa": "110C6C", + "Armadillo": "433E37", + "Arrowtown": "948771", + "Ash": "C6C3B5", + "Asparagus": "7BA05B", + "Asphalt": "130A06", + "Astra": "FAEAB9", + "Astral": "327DA0", + "Astronaut": "283A77", + "Astronaut Blue": "013E62", + "Athens Gray": "EEF0F3", + "Aths Special": "ECEBCE", + "Atlantis": "97CD2D", + "Atoll": "0A6F75", + "Atomic Tangerine": "FF9966", + "Au Chico": "97605D", + "Aubergine": "3B0910", + "Australian Mint": "F5FFBE", + "Avocado": "888D65", + "Axolotl": "4E6649", + "Azalea": "F7C8DA", + "Aztec": "0D1C19", + "Azure": "315BA1", + "Azure Radiance": "007FFF", + "Baby Blue": "E0FFFF", + "Bahama Blue": "026395", + "Bahia": "A5CB0C", + "Baja White": "FFF8D1", + "Bali Hai": "859FAF", + "Baltic Sea": "2A2630", + "Bamboo": "DA6304", + "Banana Mania": "FBE7B2", + "Bandicoot": "858470", + "Barberry": "DED717", + "Barley Corn": "A68B5B", + "Barley White": "FFF4CE", + "Barossa": "44012D", + "Bastille": "292130", + "Battleship Gray": "828F72", + "Bay Leaf": "7DA98D", + "Bay of Many": "273A81", + "Bazaar": "98777B", + "Bean ": "3D0C02", + "Beauty Bush": "EEC1BE", + "Beaver": "926F5B", + "Beeswax": "FEF2C7", + "Beige": "F5F5DC", + "Bermuda": "7DD8C6", + "Bermuda Gray": "6B8BA2", + "Beryl Green": "DEE5C0", + "Bianca": "FCFBF3", + "Big Stone": "162A40", + "Bilbao": "327C14", + "Biloba Flower": "B2A1EA", + "Birch": "373021", + "Bird Flower": "D4CD16", + "Biscay": "1B3162", + "Bismark": "497183", + "Bison Hide": "C1B7A4", + "Bistre": "3D2B1F", + "Bitter": "868974", + "Bitter Lemon": "CAE00D", + "Bittersweet": "FE6F5E", + "Bizarre": "EEDEDA", + "Black": "000000", + "Black Bean": "081910", + "Black Forest": "0B1304", + "Black Haze": "F6F7F7", + "Black Marlin": "3E2C1C", + "Black Olive": "242E16", + "Black Pearl": "041322", + "Black Rock": "0D0332", + "Black Rose": "67032D", + "Black Russian": "0A001C", + "Black Squeeze": "F2FAFA", + "Black White": "FFFEF6", + "Blackberry": "4D0135", + "Blackcurrant": "32293A", + "Blaze Orange": "FF6600", + "Bleach White": "FEF3D8", + "Bleached Cedar": "2C2133", + "Blizzard Blue": "A3E3ED", + "Blossom": "DCB4BC", + "Blue": "0000FF", + "Blue Bayoux": "496679", + "Blue Bell": "9999CC", + "Blue Chalk": "F1E9FF", + "Blue Charcoal": "010D1A", + "Blue Chill": "0C8990", + "Blue Diamond": "380474", + "Blue Dianne": "204852", + "Blue Gem": "2C0E8C", + "Blue Haze": "BFBED8", + "Blue Lagoon": "017987", + "Blue Marguerite": "7666C6", + "Blue Ribbon": "0066FF", + "Blue Romance": "D2F6DE", + "Blue Smoke": "748881", + "Blue Stone": "016162", + "Blue Violet": "6456B7", + "Blue Whale": "042E4C", + "Blue Zodiac": "13264D", + "Blumine": "18587A", + "Blush": "B44668", + "Blush Pink": "FF6FFF", + "Bombay": "AFB1B8", + "Bon Jour": "E5E0E1", + "Bondi Blue": "0095B6", + "Bone": "E4D1C0", + "Bordeaux": "5C0120", + "Bossanova": "4E2A5A", + "Boston Blue": "3B91B4", + "Botticelli": "C7DDE5", + "Bottle Green": "093624", + "Boulder": "7A7A7A", + "Bouquet": "AE809E", + "Bourbon": "BA6F1E", + "Bracken": "4A2A04", + "Brandy": "DEC196", + "Brandy Punch": "CD8429", + "Brandy Rose": "BB8983", + "Breaker Bay": "5DA19F", + "Brick Red": "C62D42", + "Bridal Heath": "FFFAF4", + "Bridesmaid": "FEF0EC", + "Bright Gray": "3C4151", + "Bright Green": "66FF00", + "Bright Red": "B10000", + "Bright Sun": "FED33C", + "Bright Turquoise": "08E8DE", + "Brilliant Rose": "F653A6", + "Brink Pink": "FB607F", + "Bronco": "ABA196", + "Bronze": "3F2109", + "Bronze Olive": "4E420C", + "Bronzetone": "4D400F", + "Broom": "FFEC13", + "Brown": "964B00", + "Brown Bramble": "592804", + "Brown Derby": "492615", + "Brown Pod": "401801", + "Brown Rust": "AF593E", + "Brown Tumbleweed": "37290E", + "Bubbles": "E7FEFF", + "Buccaneer": "622F30", + "Bud": "A8AE9C", + "Buddha Gold": "C1A004", + "Buff": "F0DC82", + "Bulgarian Rose": "480607", + "Bull Shot": "864D1E", + "Bunker": "0D1117", + "Bunting": "151F4C", + "Burgundy": "900020", + "Burnham": "002E20", + "Burning Orange": "FF7034", + "Burning Sand": "D99376", + "Burnt Maroon": "420303", + "Burnt Orange": "CC5500", + "Burnt Sienna": "E97451", + "Burnt Umber": "8A3324", + "Bush": "0D2E1C", + "Buttercup": "F3AD16", + "Buttered Rum": "A1750D", + "Butterfly Bush": "624E9A", + "Buttermilk": "FFF1B5", + "Buttery White": "FFFCEA", + "Cab Sav": "4D0A18", + "Cabaret": "D94972", + "Cabbage Pont": "3F4C3A", + "Cactus": "587156", + "Cadet Blue": "A9B2C3", + "Cadillac": "B04C6A", + "Cafe Royale": "6F440C", + "Calico": "E0C095", + "California": "FE9D04", + "Calypso": "31728D", + "Camarone": "00581A", + "Camelot": "893456", + "Cameo": "D9B99B", + "Camouflage": "3C3910", + "Camouflage Green": "78866B", + "Can Can": "D591A4", + "Canary": "F3FB62", + "Candlelight": "FCD917", + "Candy Corn": "FBEC5D", + "Cannon Black": "251706", + "Cannon Pink": "894367", + "Cape Cod": "3C4443", + "Cape Honey": "FEE5AC", + "Cape Palliser": "A26645", + "Caper": "DCEDB4", + "Caramel": "FFDDAF", + "Cararra": "EEEEE8", + "Cardin Green": "01361C", + "Cardinal": "C41E3A", + "Cardinal Pink": "8C055E", + "Careys Pink": "D29EAA", + "Caribbean Green": "00CC99", + "Carissma": "EA88A8", + "Carla": "F3FFD8", + "Carmine": "960018", + "Carnaby Tan": "5C2E01", + "Carnation": "F95A61", + "Carnation Pink": "FFA6C9", + "Carousel Pink": "F9E0ED", + "Carrot Orange": "ED9121", + "Casablanca": "F8B853", + "Casal": "2F6168", + "Cascade": "8BA9A5", + "Cashmere": "E6BEA5", + "Casper": "ADBED1", + "Castro": "52001F", + "Catalina Blue": "062A78", + "Catskill White": "EEF6F7", + "Cavern Pink": "E3BEBE", + "Cedar": "3E1C14", + "Cedar Wood Finish": "711A00", + "Celadon": "ACE1AF", + "Celery": "B8C25D", + "Celeste": "D1D2CA", + "Cello": "1E385B", + "Celtic": "163222", + "Cement": "8D7662", + "Ceramic": "FCFFF9", + "Cerise": "DA3287", + "Cerise Red": "DE3163", + "Cerulean": "02A4D3", + "Cerulean Blue": "2A52BE", + "Chablis": "FFF4F3", + "Chalet Green": "516E3D", + "Chalky": "EED794", + "Chambray": "354E8C", + "Chamois": "EDDCB1", + "Champagne": "FAECCC", + "Chantilly": "F8C3DF", + "Charade": "292937", + "Chardon": "FFF3F1", + "Chardonnay": "FFCD8C", + "Charlotte": "BAEEF9", + "Charm": "D47494", + "Chartreuse": "7FFF00", + "Chartreuse Yellow": "DFFF00", + "Chateau Green": "40A860", + "Chatelle": "BDB3C7", + "Chathams Blue": "175579", + "Chelsea Cucumber": "83AA5D", + "Chelsea Gem": "9E5302", + "Chenin": "DFCD6F", + "Cherokee": "FCDA98", + "Cherry Pie": "2A0359", + "Cherrywood": "651A14", + "Cherub": "F8D9E9", + "Chestnut": "B94E48", + "Chestnut Rose": "CD5C5C", + "Chetwode Blue": "8581D9", + "Chicago": "5D5C58", + "Chiffon": "F1FFC8", + "Chilean Fire": "F77703", + "Chilean Heath": "FFFDE6", + "China Ivory": "FCFFE7", + "Chino": "CEC7A7", + "Chinook": "A8E3BD", + "Chocolate": "370202", + "Christalle": "33036B", + "Christi": "67A712", + "Christine": "E7730A", + "Chrome White": "E8F1D4", + "Cinder": "0E0E18", + "Cinderella": "FDE1DC", + "Cinnabar": "E34234", + "Cinnamon": "7B3F00", + "Cioccolato": "55280C", + "Citrine White": "FAF7D6", + "Citron": "9EA91F", + "Citrus": "A1C50A", + "Clairvoyant": "480656", + "Clam Shell": "D4B6AF", + "Claret": "7F1734", + "Classic Rose": "FBCCE7", + "Clay Ash": "BDC8B3", + "Clay Creek": "8A8360", + "Clear Day": "E9FFFD", + "Clementine": "E96E00", + "Clinker": "371D09", + "Cloud": "C7C4BF", + "Cloud Burst": "202E54", + "Cloudy": "ACA59F", + "Clover": "384910", + "Cobalt": "0047AB", + "Cocoa Bean": "481C1C", + "Cocoa Brown": "301F1E", + "Coconut Cream": "F8F7DC", + "Cod Gray": "0B0B0B", + "Coffee": "706555", + "Coffee Bean": "2A140E", + "Cognac": "9F381D", + "Cola": "3F2500", + "Cold Purple": "ABA0D9", + "Cold Turkey": "CEBABA", + "Colonial White": "FFEDBC", + "Comet": "5C5D75", + "Como": "517C66", + "Conch": "C9D9D2", + "Concord": "7C7B7A", + "Concrete": "F2F2F2", + "Confetti": "E9D75A", + "Congo Brown": "593737", + "Congress Blue": "02478E", + "Conifer": "ACDD4D", + "Contessa": "C6726B", + "Copper": "B87333", + "Copper Canyon": "7E3A15", + "Copper Rose": "996666", + "Copper Rust": "944747", + "Copperfield": "DA8A67", + "Coral": "FF7F50", + "Coral Red": "FF4040", + "Coral Reef": "C7BCA2", + "Coral Tree": "A86B6B", + "Corduroy": "606E68", + "Coriander": "C4D0B0", + "Cork": "40291D", + "Corn": "E7BF05", + "Corn Field": "F8FACD", + "Corn Harvest": "8B6B0B", + "Cornflower": "93CCEA", + "Cornflower Blue": "6495ED", + "Cornflower Lilac": "FFB0AC", + "Corvette": "FAD3A2", + "Cosmic": "76395D", + "Cosmos": "FFD8D9", + "Costa Del Sol": "615D30", + "Cotton Candy": "FFB7D5", + "Cotton Seed": "C2BDB6", + "County Green": "01371A", + "Cowboy": "4D282D", + "Crail": "B95140", + "Cranberry": "DB5079", + "Crater Brown": "462425", + "Cream": "FFFDD0", + "Cream Brulee": "FFE5A0", + "Cream Can": "F5C85C", + "Creole": "1E0F04", + "Crete": "737829", + "Crimson": "DC143C", + "Crocodile": "736D58", + "Crown of Thorns": "771F1F", + "Crowshead": "1C1208", + "Cruise": "B5ECDF", + "Crusoe": "004816", + "Crusta": "FD7B33", + "Cumin": "924321", + "Cumulus": "FDFFD5", + "Cupid": "FBBEDA", + "Curious Blue": "2596D1", + "Cutty Sark": "507672", + "Cyan / Aqua": "00FFFF", + "Cyprus": "003E40", + "Daintree": "012731", + "Dairy Cream": "F9E4BC", + "Daisy Bush": "4F2398", + "Dallas": "6E4B26", + "Dandelion": "FED85D", + "Danube": "6093D1", + "Dark Blue": "0000C8", + "Dark Burgundy": "770F05", + "Dark Ebony": "3C2005", + "Dark Fern": "0A480D", + "Dark Tan": "661010", + "Dawn": "A6A29A", + "Dawn Pink": "F3E9E5", + "De York": "7AC488", + "Deco": "D2DA97", + "Deep Blue": "220878", + "Deep Blush": "E47698", + "Deep Bronze": "4A3004", + "Deep Cerulean": "007BA7", + "Deep Cove": "051040", + "Deep Fir": "002900", + "Deep Forest Green": "182D09", + "Deep Koamaru": "1B127B", + "Deep Oak": "412010", + "Deep Sapphire": "082567", + "Deep Sea": "01826B", + "Deep Sea Green": "095859", + "Deep Teal": "003532", + "Del Rio": "B09A95", + "Dell": "396413", + "Delta": "A4A49D", + "Deluge": "7563A8", + "Denim": "1560BD", + "Derby": "FFEED8", + "Desert": "AE6020", + "Desert Sand": "EDC9AF", + "Desert Storm": "F8F8F7", + "Dew": "EAFFFE", + "Di Serria": "DB995E", + "Diesel": "130000", + "Dingley": "5D7747", + "Disco": "871550", + "Dixie": "E29418", + "Dodger Blue": "1E90FF", + "Dolly": "F9FF8B", + "Dolphin": "646077", + "Domino": "8E775E", + "Don Juan": "5D4C51", + "Donkey Brown": "A69279", + "Dorado": "6B5755", + "Double Colonial White": "EEE3AD", + "Double Pearl Lusta": "FCF4D0", + "Double Spanish White": "E6D7B9", + "Dove Gray": "6D6C6C", + "Downriver": "092256", + "Downy": "6FD0C5", + "Driftwood": "AF8751", + "Drover": "FDF7AD", + "Dull Lavender": "A899E6", + "Dune": "383533", + "Dust Storm": "E5CCC9", + "Dusty Gray": "A8989B", + "Eagle": "B6BAA4", + "Earls Green": "C9B93B", + "Early Dawn": "FFF9E6", + "East Bay": "414C7D", + "East Side": "AC91CE", + "Eastern Blue": "1E9AB0", + "Ebb": "E9E3E3", + "Ebony": "0C0B1D", + "Ebony Clay": "26283B", + "Eclipse": "311C17", + "Ecru White": "F5F3E5", + "Ecstasy": "FA7814", + "Eden": "105852", + "Edgewater": "C8E3D7", + "Edward": "A2AEAB", + "Egg Sour": "FFF4DD", + "Egg White": "FFEFC1", + "Eggplant": "614051", + "El Paso": "1E1708", + "El Salva": "8F3E33", + "Electric Lime": "CCFF00", + "Electric Violet": "8B00FF", + "Elephant": "123447", + "Elf Green": "088370", + "Elm": "1C7C7D", + "Emerald": "50C878", + "Eminence": "6C3082", + "Emperor": "514649", + "Empress": "817377", + "Endeavour": "0056A7", + "Energy Yellow": "F8DD5C", + "English Holly": "022D15", + "English Walnut": "3E2B23", + "Envy": "8BA690", + "Equator": "E1BC64", + "Espresso": "612718", + "Eternity": "211A0E", + "Eucalyptus": "278A5B", + "Eunry": "CFA39D", + "Evening Sea": "024E46", + "Everglade": "1C402E", + "Faded Jade": "427977", + "Fair Pink": "FFEFEC", + "Falcon": "7F626D", + "Fall Green": "ECEBBD", + "Falu Red": "801818", + "Fantasy": "FAF3F0", + "Fedora": "796A78", + "Feijoa": "9FDD8C", + "Fern": "63B76C", + "Fern Frond": "657220", + "Fern Green": "4F7942", + "Ferra": "704F50", + "Festival": "FBE96C", + "Feta": "F0FCEA", + "Fiery Orange": "B35213", + "Finch": "626649", + "Finlandia": "556D56", + "Finn": "692D54", + "Fiord": "405169", + "Fire": "AA4203", + "Fire Bush": "E89928", + "Firefly": "0E2A30", + "Flame Pea": "DA5B38", + "Flamenco": "FF7D07", + "Flamingo": "F2552A", + "Flax": "EEDC82", + "Flax Smoke": "7B8265", + "Flesh": "FFCBA4", + "Flint": "6F6A61", + "Flirt": "A2006D", + "Flush Mahogany": "CA3435", + "Flush Orange": "FF7F00", + "Foam": "D8FCFA", + "Fog": "D7D0FF", + "Foggy Gray": "CBCAB6", + "Forest Green": "228B22", + "Forget Me Not": "FFF1EE", + "Fountain Blue": "56B4BE", + "Frangipani": "FFDEB3", + "French Gray": "BDBDC6", + "French Lilac": "ECC7EE", + "French Pass": "BDEDFD", + "French Rose": "F64A8A", + "Fresh Eggplant": "990066", + "Friar Gray": "807E79", + "Fringy Flower": "B1E2C1", + "Froly": "F57584", + "Frost": "EDF5DD", + "Frosted Mint": "DBFFF8", + "Frostee": "E4F6E7", + "Fruit Salad": "4F9D5D", + "Fuchsia Blue": "7A58C1", + "Fuchsia Pink": "C154C1", + "Fuego": "BEDE0D", + "Fuel Yellow": "ECA927", + "Fun Blue": "1959A8", + "Fun Green": "016D39", + "Fuscous Gray": "54534D", + "Fuzzy Wuzzy Brown": "C45655", + "Gable Green": "163531", + "Gallery": "EFEFEF", + "Galliano": "DCB20C", + "Gamboge": "E49B0F", + "Geebung": "D18F1B", + "Genoa": "15736B", + "Geraldine": "FB8989", + "Geyser": "D4DFE2", + "Ghost": "C7C9D5", + "Gigas": "523C94", + "Gimblet": "B8B56A", + "Gin": "E8F2EB", + "Gin Fizz": "FFF9E2", + "Givry": "F8E4BF", + "Glacier": "80B3C4", + "Glade Green": "61845F", + "Go Ben": "726D4E", + "Goblin": "3D7D52", + "Gold": "FFD700", + "Gold Drop": "F18200", + "Gold Sand": "E6BE8A", + "Gold Tips": "DEBA13", + "Golden Bell": "E28913", + "Golden Dream": "F0D52D", + "Golden Fizz": "F5FB3D", + "Golden Glow": "FDE295", + "Golden Grass": "DAA520", + "Golden Sand": "F0DB7D", + "Golden Tainoi": "FFCC5C", + "Goldenrod": "FCD667", + "Gondola": "261414", + "Gordons Green": "0B1107", + "Gorse": "FFF14F", + "Gossamer": "069B81", + "Gossip": "D2F8B0", + "Gothic": "6D92A1", + "Governor Bay": "2F3CB3", + "Grain Brown": "E4D5B7", + "Grandis": "FFD38C", + "Granite Green": "8D8974", + "Granny Apple": "D5F6E3", + "Granny Smith": "84A0A0", + "Granny Smith Apple": "9DE093", + "Grape": "381A51", + "Graphite": "251607", + "Gravel": "4A444B", + "Gray": "808080", + "Gray Asparagus": "465945", + "Gray Chateau": "A2AAB3", + "Gray Nickel": "C3C3BD", + "Gray Nurse": "E7ECE6", + "Gray Olive": "A9A491", + "Gray Suit": "C1BECD", + "Green": "00FF00", + "Green Haze": "01A368", + "Green House": "24500F", + "Green Kelp": "25311C", + "Green Leaf": "436A0D", + "Green Mist": "CBD3B0", + "Green Pea": "1D6142", + "Green Smoke": "A4AF6E", + "Green Spring": "B8C1B1", + "Green Vogue": "032B52", + "Green Waterloo": "101405", + "Green White": "E8EBE0", + "Green Yellow": "ADFF2F", + "Grenadier": "D54600", + "Guardsman Red": "BA0101", + "Gulf Blue": "051657", + "Gulf Stream": "80B3AE", + "Gull Gray": "9DACB7", + "Gum Leaf": "B6D3BF", + "Gumbo": "7CA1A6", + "Gun Powder": "414257", + "Gunsmoke": "828685", + "Gurkha": "9A9577", + "Hacienda": "98811B", + "Hairy Heath": "6B2A14", + "Haiti": "1B1035", + "Half Baked": "85C4CC", + "Half Colonial White": "FDF6D3", + "Half Dutch White": "FEF7DE", + "Half Spanish White": "FEF4DB", + "Half and Half": "FFFEE1", + "Hampton": "E5D8AF", + "Harlequin": "3FFF00", + "Harp": "E6F2EA", + "Harvest Gold": "E0B974", + "Havelock Blue": "5590D9", + "Hawaiian Tan": "9D5616", + "Hawkes Blue": "D4E2FC", + "Heath": "541012", + "Heather": "B7C3D0", + "Heathered Gray": "B6B095", + "Heavy Metal": "2B3228", + "Heliotrope": "DF73FF", + "Hemlock": "5E5D3B", + "Hemp": "907874", + "Hibiscus": "B6316C", + "Highland": "6F8E63", + "Hillary": "ACA586", + "Himalaya": "6A5D1B", + "Hint of Green": "E6FFE9", + "Hint of Red": "FBF9F9", + "Hint of Yellow": "FAFDE4", + "Hippie Blue": "589AAF", + "Hippie Green": "53824B", + "Hippie Pink": "AE4560", + "Hit Gray": "A1ADB5", + "Hit Pink": "FFAB81", + "Hokey Pokey": "C8A528", + "Hoki": "65869F", + "Holly": "011D13", + "Hollywood Cerise": "F400A1", + "Honey Flower": "4F1C70", + "Honeysuckle": "EDFC84", + "Hopbush": "D06DA1", + "Horizon": "5A87A0", + "Horses Neck": "604913", + "Hot Cinnamon": "D2691E", + "Hot Pink": "FF69B4", + "Hot Toddy": "B38007", + "Humming Bird": "CFF9F3", + "Hunter Green": "161D10", + "Hurricane": "877C7B", + "Husk": "B7A458", + "Ice Cold": "B1F4E7", + "Iceberg": "DAF4F0", + "Illusion": "F6A4C9", + "Inch Worm": "B0E313", + "Indian Khaki": "C3B091", + "Indian Tan": "4D1E01", + "Indigo": "4F69C6", + "Indochine": "C26B03", + "International Klein Blue": "002FA7", + "International Orange": "FF4F00", + "Irish Coffee": "5F3D26", + "Iroko": "433120", + "Iron": "D4D7D9", + "Ironside Gray": "676662", + "Ironstone": "86483C", + "Island Spice": "FFFCEE", + "Ivory": "FFFFF0", + "Jacaranda": "2E0329", + "Jacarta": "3A2A6A", + "Jacko Bean": "2E1905", + "Jacksons Purple": "20208D", + "Jade": "00A86B", + "Jaffa": "EF863F", + "Jagged Ice": "C2E8E5", + "Jagger": "350E57", + "Jaguar": "080110", + "Jambalaya": "5B3013", + "Janna": "F4EBD3", + "Japanese Laurel": "0A6906", + "Japanese Maple": "780109", + "Japonica": "D87C63", + "Java": "1FC2C2", + "Jazzberry Jam": "A50B5E", + "Jelly Bean": "297B9A", + "Jet Stream": "B5D2CE", + "Jewel": "126B40", + "Jon": "3B1F1F", + "Jonquil": "EEFF9A", + "Jordy Blue": "8AB9F1", + "Judge Gray": "544333", + "Jumbo": "7C7B82", + "Jungle Green": "29AB87", + "Jungle Mist": "B4CFD3", + "Juniper": "6D9292", + "Just Right": "ECCDB9", + "Kabul": "5E483E", + "Kaitoke Green": "004620", + "Kangaroo": "C6C8BD", + "Karaka": "1E1609", + "Karry": "FFEAD4", + "Kashmir Blue": "507096", + "Kelp": "454936", + "Kenyan Copper": "7C1C05", + "Keppel": "3AB09E", + "Key Lime Pie": "BFC921", + "Khaki": "F0E68C", + "Kidnapper": "E1EAD4", + "Kilamanjaro": "240C02", + "Killarney": "3A6A47", + "Kimberly": "736C9F", + "Kingfisher Daisy": "3E0480", + "Kobi": "E79FC4", + "Kokoda": "6E6D57", + "Korma": "8F4B0E", + "Koromiko": "FFBD5F", + "Kournikova": "FFE772", + "Kumera": "886221", + "La Palma": "368716", + "La Rioja": "B3C110", + "Las Palmas": "C6E610", + "Laser": "C8B568", + "Laser Lemon": "FFFF66", + "Laurel": "749378", + "Lavender": "B57EDC", + "Lavender Gray": "BDBBD7", + "Lavender Magenta": "EE82EE", + "Lavender Pink": "FBAED2", + "Lavender Purple": "967BB6", + "Lavender Rose": "FBA0E3", + "Lavender blush": "FFF0F5", + "Leather": "967059", + "Lemon": "FDE910", + "Lemon Chiffon": "FFFACD", + "Lemon Ginger": "AC9E22", + "Lemon Grass": "9B9E8F", + "Light Apricot": "FDD5B1", + "Light Orchid": "E29CD2", + "Light Wisteria": "C9A0DC", + "Lightning Yellow": "FCC01E", + "Lilac": "C8A2C8", + "Lilac Bush": "9874D3", + "Lily": "C8AABF", + "Lily White": "E7F8FF", + "Lima": "76BD17", + "Lime": "BFFF00", + "Limeade": "6F9D02", + "Limed Ash": "747D63", + "Limed Oak": "AC8A56", + "Limed Spruce": "394851", + "Linen": "FAF0E6", + "Link Water": "D9E4F5", + "Lipstick": "AB0563", + "Lisbon Brown": "423921", + "Livid Brown": "4D282E", + "Loafer": "EEF4DE", + "Loblolly": "BDC9CE", + "Lochinvar": "2C8C84", + "Lochmara": "007EC7", + "Locust": "A8AF8E", + "Log Cabin": "242A1D", + "Logan": "AAA9CD", + "Lola": "DFCFDB", + "London Hue": "BEA6C3", + "Lonestar": "6D0101", + "Lotus": "863C3C", + "Loulou": "460B41", + "Lucky": "AF9F1C", + "Lucky Point": "1A1A68", + "Lunar Green": "3C493A", + "Luxor Gold": "A7882C", + "Lynch": "697E9A", + "Mabel": "D9F7FF", + "Macaroni and Cheese": "FFB97B", + "Madang": "B7F0BE", + "Madison": "09255D", + "Madras": "3F3002", + "Magenta / Fuchsia": "FF00FF", + "Magic Mint": "AAF0D1", + "Magnolia": "F8F4FF", + "Mahogany": "4E0606", + "Mai Tai": "B06608", + "Maize": "F5D5A0", + "Makara": "897D6D", + "Mako": "444954", + "Malachite": "0BDA51", + "Malibu": "7DC8F7", + "Mallard": "233418", + "Malta": "BDB2A1", + "Mamba": "8E8190", + "Manatee": "8D90A1", + "Mandalay": "AD781B", + "Mandy": "E25465", + "Mandys Pink": "F2C3B2", + "Mango Tango": "E77200", + "Manhattan": "F5C999", + "Mantis": "74C365", + "Mantle": "8B9C90", + "Manz": "EEEF78", + "Mardi Gras": "350036", + "Marigold": "B98D28", + "Marigold Yellow": "FBE870", + "Mariner": "286ACD", + "Maroon": "800000", + "Maroon Flush": "C32148", + "Maroon Oak": "520C17", + "Marshland": "0B0F08", + "Martini": "AFA09E", + "Martinique": "363050", + "Marzipan": "F8DB9D", + "Masala": "403B38", + "Matisse": "1B659D", + "Matrix": "B05D54", + "Matterhorn": "4E3B41", + "Mauve": "E0B0FF", + "Mauvelous": "F091A9", + "Maverick": "D8C2D5", + "Medium Carmine": "AF4035", + "Medium Purple": "9370DB", + "Medium Red Violet": "BB3385", + "Melanie": "E4C2D5", + "Melanzane": "300529", + "Melon": "FEBAAD", + "Melrose": "C7C1FF", + "Mercury": "E5E5E5", + "Merino": "F6F0E6", + "Merlin": "413C37", + "Merlot": "831923", + "Metallic Bronze": "49371B", + "Metallic Copper": "71291D", + "Meteor": "D07D12", + "Meteorite": "3C1F76", + "Mexican Red": "A72525", + "Mid Gray": "5F5F6E", + "Midnight": "011635", + "Midnight Blue": "003366", + "Midnight Moss": "041004", + "Mikado": "2D2510", + "Milan": "FAFFA4", + "Milano Red": "B81104", + "Milk Punch": "FFF6D4", + "Millbrook": "594433", + "Mimosa": "F8FDD3", + "Mindaro": "E3F988", + "Mine Shaft": "323232", + "Mineral Green": "3F5D53", + "Ming": "36747D", + "Minsk": "3F307F", + "Mint Green": "98FF98", + "Mint Julep": "F1EEC1", + "Mint Tulip": "C4F4EB", + "Mirage": "161928", + "Mischka": "D1D2DD", + "Mist Gray": "C4C4BC", + "Mobster": "7F7589", + "Moccaccino": "6E1D14", + "Mocha": "782D19", + "Mojo": "C04737", + "Mona Lisa": "FFA194", + "Monarch": "8B0723", + "Mondo": "4A3C30", + "Mongoose": "B5A27F", + "Monsoon": "8A8389", + "Monte Carlo": "83D0C6", + "Monza": "C7031E", + "Moody Blue": "7F76D3", + "Moon Glow": "FCFEDA", + "Moon Mist": "DCDDCC", + "Moon Raker": "D6CEF6", + "Morning Glory": "9EDEE0", + "Morocco Brown": "441D00", + "Mortar": "504351", + "Mosque": "036A6E", + "Moss Green": "ADDFAD", + "Mountain Meadow": "1AB385", + "Mountain Mist": "959396", + "Mountbatten Pink": "997A8D", + "Muddy Waters": "B78E5C", + "Muesli": "AA8B5B", + "Mulberry": "C54B8C", + "Mulberry Wood": "5C0536", + "Mule Fawn": "8C472F", + "Mulled Wine": "4E4562", + "Mustard": "FFDB58", + "My Pink": "D69188", + "My Sin": "FFB31F", + "Mystic": "E2EBED", + "Nandor": "4B5D52", + "Napa": "ACA494", + "Narvik": "EDF9F1", + "Natural Gray": "8B8680", + "Navajo White": "FFDEAD", + "Navy Blue": "000080", + "Nebula": "CBDBD6", + "Negroni": "FFE2C5", + "Neon Carrot": "FF9933", + "Nepal": "8EABC1", + "Neptune": "7CB7BB", + "Nero": "140600", + "Nevada": "646E75", + "New Orleans": "F3D69D", + "New York Pink": "D7837F", + "Niagara": "06A189", + "Night Rider": "1F120F", + "Night Shadz": "AA375A", + "Nile Blue": "193751", + "Nobel": "B7B1B1", + "Nomad": "BAB1A2", + "Norway": "A8BD9F", + "Nugget": "C59922", + "Nutmeg": "81422C", + "Nutmeg Wood Finish": "683600", + "Oasis": "FEEFCE", + "Observatory": "02866F", + "Ocean Green": "41AA78", + "Ochre": "CC7722", + "Off Green": "E6F8F3", + "Off Yellow": "FEF9E3", + "Oil": "281E15", + "Old Brick": "901E1E", + "Old Copper": "724A2F", + "Old Gold": "CFB53B", + "Old Lace": "FDF5E6", + "Old Lavender": "796878", + "Old Rose": "C08081", + "Olive": "808000", + "Olive Drab": "6B8E23", + "Olive Green": "B5B35C", + "Olive Haze": "8B8470", + "Olivetone": "716E10", + "Olivine": "9AB973", + "Onahau": "CDF4FF", + "Onion": "2F270E", + "Opal": "A9C6C2", + "Opium": "8E6F70", + "Oracle": "377475", + "Orange": "FF681F", + "Orange Peel": "FFA000", + "Orange Roughy": "C45719", + "Orange White": "FEFCED", + "Orchid": "DA70D6", + "Orchid White": "FFFDF3", + "Oregon": "9B4703", + "Orient": "015E85", + "Oriental Pink": "C69191", + "Orinoco": "F3FBD4", + "Oslo Gray": "878D91", + "Ottoman": "E9F8ED", + "Outer Space": "2D383A", + "Outrageous Orange": "FF6037", + "Oxford Blue": "384555", + "Oxley": "779E86", + "Oyster Bay": "DAFAFF", + "Oyster Pink": "E9CECD", + "Paarl": "A65529", + "Pablo": "776F61", + "Pacific Blue": "009DC4", + "Pacifika": "778120", + "Paco": "411F10", + "Padua": "ADE6C4", + "Pale Canary": "FFFF99", + "Pale Leaf": "C0D3B9", + "Pale Oyster": "988D77", + "Pale Prim": "FDFEB8", + "Pale Rose": "FFE1F2", + "Pale Sky": "6E7783", + "Pale Slate": "C3BFC1", + "Palm Green": "09230F", + "Palm Leaf": "19330E", + "Pampas": "F4F2EE", + "Panache": "EAF6EE", + "Pancho": "EDCDAB", + "Papaya Whip": "FFEFD5", + "Paprika": "8D0226", + "Paradiso": "317D82", + "Parchment": "F1E9D2", + "Paris Daisy": "FFF46E", + "Paris M": "26056A", + "Paris White": "CADCD4", + "Parsley": "134F19", + "Pastel Green": "77DD77", + "Pastel Pink": "FFD1DC", + "Patina": "639A8F", + "Pattens Blue": "DEF5FF", + "Paua": "260368", + "Pavlova": "D7C498", + "Peach": "FFE5B4", + "Peach Cream": "FFF0DB", + "Peach Orange": "FFCC99", + "Peach Schnapps": "FFDCD6", + "Peach Yellow": "FADFAD", + "Peanut": "782F16", + "Pear": "D1E231", + "Pearl Bush": "E8E0D5", + "Pearl Lusta": "FCF4DC", + "Peat": "716B56", + "Pelorous": "3EABBF", + "Peppermint": "E3F5E1", + "Perano": "A9BEF2", + "Perfume": "D0BEF8", + "Periglacial Blue": "E1E6D6", + "Periwinkle": "CCCCFF", + "Periwinkle Gray": "C3CDE6", + "Persian Blue": "1C39BB", + "Persian Green": "00A693", + "Persian Indigo": "32127A", + "Persian Pink": "F77FBE", + "Persian Plum": "701C1C", + "Persian Red": "CC3333", + "Persian Rose": "FE28A2", + "Persimmon": "FF6B53", + "Peru Tan": "7F3A02", + "Pesto": "7C7631", + "Petite Orchid": "DB9690", + "Pewter": "96A8A1", + "Pharlap": "A3807B", + "Picasso": "FFF39D", + "Pickled Bean": "6E4826", + "Pickled Bluewood": "314459", + "Picton Blue": "45B1E8", + "Pig Pink": "FDD7E4", + "Pigeon Post": "AFBDD9", + "Pigment Indigo": "4B0082", + "Pine Cone": "6D5E54", + "Pine Glade": "C7CD90", + "Pine Green": "01796F", + "Pine Tree": "171F04", + "Pink": "FFC0CB", + "Pink Flamingo": "FF66FF", + "Pink Flare": "E1C0C8", + "Pink Lace": "FFDDF4", + "Pink Lady": "FFF1D8", + "Pink Salmon": "FF91A4", + "Pink Swan": "BEB5B7", + "Piper": "C96323", + "Pipi": "FEF4CC", + "Pippin": "FFE1DF", + "Pirate Gold": "BA7F03", + "Pistachio": "9DC209", + "Pixie Green": "C0D8B6", + "Pizazz": "FF9000", + "Pizza": "C99415", + "Plantation": "27504B", + "Plum": "843179", + "Pohutukawa": "8F021C", + "Polar": "E5F9F6", + "Polo Blue": "8DA8CC", + "Pomegranate": "F34723", + "Pompadour": "660045", + "Porcelain": "EFF2F3", + "Porsche": "EAAE69", + "Port Gore": "251F4F", + "Portafino": "FFFFB4", + "Portage": "8B9FEE", + "Portica": "F9E663", + "Pot Pourri": "F5E7E2", + "Potters Clay": "8C5738", + "Powder Ash": "BCC9C2", + "Powder Blue": "B0E0E6", + "Prairie Sand": "9A3820", + "Prelude": "D0C0E5", + "Prim": "F0E2EC", + "Primrose": "EDEA99", + "Provincial Pink": "FEF5F1", + "Prussian Blue": "003153", + "Puce": "CC8899", + "Pueblo": "7D2C14", + "Puerto Rico": "3FC1AA", + "Pumice": "C2CAC4", + "Pumpkin": "FF7518", + "Pumpkin Skin": "B1610B", + "Punch": "DC4333", + "Punga": "4D3D14", + "Purple": "660099", + "Purple Heart": "652DC1", + "Purple Mountain's Majesty": "9678B6", + "Purple Pizzazz": "FF00CC", + "Putty": "E7CD8C", + "Quarter Pearl Lusta": "FFFDF4", + "Quarter Spanish White": "F7F2E1", + "Quicksand": "BD978E", + "Quill Gray": "D6D6D1", + "Quincy": "623F2D", + "Racing Green": "0C1911", + "Radical Red": "FF355E", + "Raffia": "EADAB8", + "Rainee": "B9C8AC", + "Rajah": "F7B668", + "Rangitoto": "2E3222", + "Rangoon Green": "1C1E13", + "Raven": "727B89", + "Raw Sienna": "D27D46", + "Raw Umber": "734A12", + "Razzle Dazzle Rose": "FF33CC", + "Razzmatazz": "E30B5C", + "Rebel": "3C1206", + "Red": "FF0000", + "Red Beech": "7B3801", + "Red Berry": "8E0000", + "Red Damask": "DA6A41", + "Red Devil": "860111", + "Red Orange": "FF3F34", + "Red Oxide": "6E0902", + "Red Ribbon": "ED0A3F", + "Red Robin": "80341F", + "Red Stage": "D05F04", + "Red Violet": "C71585", + "Redwood": "5D1E0F", + "Reef": "C9FFA2", + "Reef Gold": "9F821C", + "Regal Blue": "013F6A", + "Regent Gray": "86949F", + "Regent St Blue": "AAD6E6", + "Remy": "FEEBF3", + "Reno Sand": "A86515", + "Resolution Blue": "002387", + "Revolver": "2C1632", + "Rhino": "2E3F62", + "Rice Cake": "FFFEF0", + "Rice Flower": "EEFFE2", + "Rich Gold": "A85307", + "Rio Grande": "BBD009", + "Ripe Lemon": "F4D81C", + "Ripe Plum": "410056", + "Riptide": "8BE6D8", + "River Bed": "434C59", + "Rob Roy": "EAC674", + "Robin's Egg Blue": "00CCCC", + "Rock": "4D3833", + "Rock Blue": "9EB1CD", + "Rock Spray": "BA450C", + "Rodeo Dust": "C9B29B", + "Rolling Stone": "747D83", + "Roman": "DE6360", + "Roman Coffee": "795D4C", + "Romance": "FFFEFD", + "Romantic": "FFD2B7", + "Ronchi": "ECC54E", + "Roof Terracotta": "A62F20", + "Rope": "8E4D1E", + "Rose": "FF007F", + "Rose Bud": "FBB2A3", + "Rose Bud Cherry": "800B47", + "Rose Fog": "E7BCB4", + "Rose White": "FFF6F5", + "Rose of Sharon": "BF5500", + "Rosewood": "65000B", + "Roti": "C6A84B", + "Rouge": "A23B6C", + "Royal Blue": "4169E1", + "Royal Heath": "AB3472", + "Royal Purple": "6B3FA0", + "Rum": "796989", + "Rum Swizzle": "F9F8E4", + "Russet": "80461B", + "Russett": "755A57", + "Rust": "B7410E", + "Rustic Red": "480404", + "Rusty Nail": "86560A", + "Saddle": "4C3024", + "Saddle Brown": "583401", + "Saffron": "F4C430", + "Saffron Mango": "F9BF58", + "Sage": "9EA587", + "Sahara": "B7A214", + "Sahara Sand": "F1E788", + "Sail": "B8E0F9", + "Salem": "097F4B", + "Salmon": "FF8C69", + "Salomie": "FEDB8D", + "Salt Box": "685E6E", + "Saltpan": "F1F7F2", + "Sambuca": "3A2010", + "San Felix": "0B6207", + "San Juan": "304B6A", + "San Marino": "456CAC", + "Sand Dune": "826F65", + "Sandal": "AA8D6F", + "Sandrift": "AB917A", + "Sandstone": "796D62", + "Sandwisp": "F5E7A2", + "Sandy Beach": "FFEAC8", + "Sandy brown": "F4A460", + "Sangria": "92000A", + "Sanguine Brown": "8D3D38", + "Santa Fe": "B16D52", + "Santas Gray": "9FA0B1", + "Sapling": "DED4A4", + "Sapphire": "2F519E", + "Saratoga": "555B10", + "Satin Linen": "E6E4D4", + "Sauvignon": "FFF5F3", + "Sazerac": "FFF4E0", + "Scampi": "675FA6", + "Scandal": "CFFAF4", + "Scarlet": "FF2400", + "Scarlet Gum": "431560", + "Scarlett": "950015", + "Scarpa Flow": "585562", + "Schist": "A9B497", + "School bus Yellow": "FFD800", + "Schooner": "8B847E", + "Science Blue": "0066CC", + "Scooter": "2EBFD4", + "Scorpion": "695F62", + "Scotch Mist": "FFFBDC", + "Screamin' Green": "66FF66", + "Sea Buckthorn": "FBA129", + "Sea Green": "2E8B57", + "Sea Mist": "C5DBCA", + "Sea Nymph": "78A39C", + "Sea Pink": "ED989E", + "Seagull": "80CCEA", + "Seance": "731E8F", + "Seashell": "F1F1F1", + "Seashell Peach": "FFF5EE", + "Seaweed": "1B2F11", + "Selago": "F0EEFD", + "Selective Yellow": "FFBA00", + "Sepia": "704214", + "Sepia Black": "2B0202", + "Sepia Skin": "9E5B40", + "Serenade": "FFF4E8", + "Shadow": "837050", + "Shadow Green": "9AC2B8", + "Shady Lady": "AAA5A9", + "Shakespeare": "4EABD1", + "Shalimar": "FBFFBA", + "Shamrock": "33CC99", + "Shark": "25272C", + "Sherpa Blue": "004950", + "Sherwood Green": "02402C", + "Shilo": "E8B9B3", + "Shingle Fawn": "6B4E31", + "Ship Cove": "788BBA", + "Ship Gray": "3E3A44", + "Shiraz": "B20931", + "Shocking": "E292C0", + "Shocking Pink": "FC0FC0", + "Shuttle Gray": "5F6672", + "Siam": "646A54", + "Sidecar": "F3E7BB", + "Silk": "BDB1A8", + "Silver": "C0C0C0", + "Silver Chalice": "ACACAC", + "Silver Rust": "C9C0BB", + "Silver Sand": "BFC1C2", + "Silver Tree": "66B58F", + "Sinbad": "9FD7D3", + "Siren": "7A013A", + "Sirocco": "718080", + "Sisal": "D3CBBA", + "Skeptic": "CAE6DA", + "Sky Blue": "76D7EA", + "Slate Gray": "708090", + "Smalt": "003399", + "Smalt Blue": "51808F", + "Smoky": "605B73", + "Snow Drift": "F7FAF7", + "Snow Flurry": "E4FFD1", + "Snowy Mint": "D6FFDB", + "Snuff": "E2D8ED", + "Soapstone": "FFFBF9", + "Soft Amber": "D1C6B4", + "Soft Peach": "F5EDEF", + "Solid Pink": "893843", + "Solitaire": "FEF8E2", + "Solitude": "EAF6FF", + "Sorbus": "FD7C07", + "Sorrell Brown": "CEB98F", + "Soya Bean": "6A6051", + "Spanish Green": "819885", + "Spectra": "2F5A57", + "Spice": "6A442E", + "Spicy Mix": "885342", + "Spicy Mustard": "74640D", + "Spicy Pink": "816E71", + "Spindle": "B6D1EA", + "Spray": "79DEEC", + "Spring Green": "00FF7F", + "Spring Leaves": "578363", + "Spring Rain": "ACCBB1", + "Spring Sun": "F6FFDC", + "Spring Wood": "F8F6F1", + "Sprout": "C1D7B0", + "Spun Pearl": "AAABB7", + "Squirrel": "8F8176", + "St Tropaz": "2D569B", + "Stack": "8A8F8A", + "Star Dust": "9F9F9C", + "Stark White": "E5D7BD", + "Starship": "ECF245", + "Steel Blue": "4682B4", + "Steel Gray": "262335", + "Stiletto": "9C3336", + "Stonewall": "928573", + "Storm Dust": "646463", + "Storm Gray": "717486", + "Stratos": "000741", + "Straw": "D4BF8D", + "Strikemaster": "956387", + "Stromboli": "325D52", + "Studio": "714AB2", + "Submarine": "BAC7C9", + "Sugar Cane": "F9FFF6", + "Sulu": "C1F07C", + "Summer Green": "96BBAB", + "Sun": "FBAC13", + "Sundance": "C9B35B", + "Sundown": "FFB1B3", + "Sunflower": "E4D422", + "Sunglo": "E16865", + "Sunglow": "FFCC33", + "Sunset Orange": "FE4C40", + "Sunshade": "FF9E2C", + "Supernova": "FFC901", + "Surf": "BBD7C1", + "Surf Crest": "CFE5D2", + "Surfie Green": "0C7A79", + "Sushi": "87AB39", + "Suva Gray": "888387", + "Swamp": "001B1C", + "Swamp Green": "ACB78E", + "Swans Down": "DCF0EA", + "Sweet Corn": "FBEA8C", + "Sweet Pink": "FD9FA2", + "Swirl": "D3CDC5", + "Swiss Coffee": "DDD6D5", + "Sycamore": "908D39", + "Tabasco": "A02712", + "Tacao": "EDB381", + "Tacha": "D6C562", + "Tahiti Gold": "E97C07", + "Tahuna Sands": "EEF0C8", + "Tall Poppy": "B32D29", + "Tallow": "A8A589", + "Tamarillo": "991613", + "Tamarind": "341515", + "Tan": "D2B48C", + "Tan Hide": "FA9D5A", + "Tana": "D9DCC1", + "Tangaroa": "03163C", + "Tangerine": "F28500", + "Tango": "ED7A1C", + "Tapa": "7B7874", + "Tapestry": "B05E81", + "Tara": "E1F6E8", + "Tarawera": "073A50", + "Tasman": "CFDCCF", + "Taupe": "483C32", + "Taupe Gray": "B3AF95", + "Tawny Port": "692545", + "Te Papa Green": "1E433C", + "Tea": "C1BAB0", + "Tea Green": "D0F0C0", + "Teak": "B19461", + "Teal": "008080", + "Teal Blue": "044259", + "Temptress": "3B000B", + "Tenn": "CD5700", + "Tequila": "FFE6C7", + "Terracotta": "E2725B", + "Texas": "F8F99C", + "Texas Rose": "FFB555", + "Thatch": "B69D98", + "Thatch Green": "403D19", + "Thistle": "D8BFD8", + "Thistle Green": "CCCAA8", + "Thunder": "33292F", + "Thunderbird": "C02B18", + "Tia Maria": "C1440E", + "Tiara": "C3D1D1", + "Tiber": "063537", + "Tickle Me Pink": "FC80A5", + "Tidal": "F1FFAD", + "Tide": "BFB8B0", + "Timber Green": "16322C", + "Timberwolf": "D9D6CF", + "Titan White": "F0EEFF", + "Toast": "9A6E61", + "Tobacco Brown": "715D47", + "Toledo": "3A0020", + "Tolopea": "1B0245", + "Tom Thumb": "3F583B", + "Tonys Pink": "E79F8C", + "Topaz": "7C778A", + "Torch Red": "FD0E35", + "Torea Bay": "0F2D9E", + "Tory Blue": "1450AA", + "Tosca": "8D3F3F", + "Totem Pole": "991B07", + "Tower Gray": "A9BDBF", + "Tradewind": "5FB3AC", + "Tranquil": "E6FFFF", + "Travertine": "FFFDE8", + "Tree Poppy": "FC9C1D", + "Treehouse": "3B2820", + "Trendy Green": "7C881A", + "Trendy Pink": "8C6495", + "Trinidad": "E64E03", + "Tropical Blue": "C3DDF9", + "Tropical Rain Forest": "00755E", + "Trout": "4A4E5A", + "True V": "8A73D6", + "Tuatara": "363534", + "Tuft Bush": "FFDDCD", + "Tulip Tree": "EAB33B", + "Tumbleweed": "DEA681", + "Tuna": "353542", + "Tundora": "4A4244", + "Turbo": "FAE600", + "Turkish Rose": "B57281", + "Turmeric": "CABB48", + "Turquoise": "30D5C8", + "Turquoise Blue": "6CDAE7", + "Turtle Green": "2A380B", + "Tuscany": "BD5E2E", + "Tusk": "EEF3C3", + "Tussock": "C5994B", + "Tutu": "FFF1F9", + "Twilight": "E4CFDE", + "Twilight Blue": "EEFDFF", + "Twine": "C2955D", + "Tyrian Purple": "66023C", + "Ultramarine": "120A8F", + "Valencia": "D84437", + "Valentino": "350E42", + "Valhalla": "2B194F", + "Van Cleef": "49170C", + "Vanilla": "D1BEA8", + "Vanilla Ice": "F3D9DF", + "Varden": "FFF6DF", + "Venetian Red": "72010F", + "Venice Blue": "055989", + "Venus": "928590", + "Verdigris": "5D5E37", + "Verdun Green": "495400", + "Vermilion": "FF4D00", + "Vesuvius": "B14A0B", + "Victoria": "534491", + "Vida Loca": "549019", + "Viking": "64CCDB", + "Vin Rouge": "983D61", + "Viola": "CB8FA9", + "Violent Violet": "290C5E", + "Violet": "240A40", + "Violet Eggplant": "991199", + "Violet Red": "F7468A", + "Viridian": "40826D", + "Viridian Green": "678975", + "Vis Vis": "FFEFA1", + "Vista Blue": "8FD6B4", + "Vista White": "FCF8F7", + "Vivid Tangerine": "FF9980", + "Vivid Violet": "803790", + "Voodoo": "533455", + "Vulcan": "10121D", + "Wafer": "DECBC6", + "Waikawa Gray": "5A6E9C", + "Waiouru": "363C0D", + "Walnut": "773F1A", + "Wasabi": "788A25", + "Water Leaf": "A1E9DE", + "Watercourse": "056F57", + "Waterloo ": "7B7C94", + "Wattle": "DCD747", + "Watusi": "FFDDCF", + "Wax Flower": "FFC0A8", + "We Peep": "F7DBE6", + "Web Orange": "FFA500", + "Wedgewood": "4E7F9E", + "Well Read": "B43332", + "West Coast": "625119", + "West Side": "FF910F", + "Westar": "DCD9D2", + "Wewak": "F19BAB", + "Wheat": "F5DEB3", + "Wheatfield": "F3EDCF", + "Whiskey": "D59A6F", + "Whisper": "F7F5FA", + "White": "FFFFFF", + "White Ice": "DDF9F1", + "White Lilac": "F8F7FC", + "White Linen": "F8F0E8", + "White Pointer": "FEF8FF", + "White Rock": "EAE8D4", + "Wild Blue Yonder": "7A89B8", + "Wild Rice": "ECE090", + "Wild Sand": "F4F4F4", + "Wild Strawberry": "FF3399", + "Wild Watermelon": "FD5B78", + "Wild Willow": "B9C46A", + "William": "3A686C", + "Willow Brook": "DFECDA", + "Willow Grove": "65745D", + "Windsor": "3C0878", + "Wine Berry": "591D35", + "Winter Hazel": "D5D195", + "Wisp Pink": "FEF4F8", + "Wisteria": "9771B5", + "Wistful": "A4A6D3", + "Witch Haze": "FFFC99", + "Wood Bark": "261105", + "Woodland": "4D5328", + "Woodrush": "302A0F", + "Woodsmoke": "0C0D0F", + "Woody Brown": "483131", + "Xanadu": "738678", + "Yellow": "FFFF00", + "Yellow Green": "C5E17A", + "Yellow Metal": "716338", + "Yellow Orange": "FFAE42", + "Yellow Sea": "FEA904", + "Your Pink": "FFC3C0", + "Yukon Gold": "7B6608", + "Yuma": "CEC291", + "Zambezi": "685558", + "Zanah": "DAECD6", + "Zest": "E5841B", + "Zeus": "292319", + "Ziggurat": "BFDBE2", + "Zinnwaldite": "EBC2AF", + "Zircon": "F4F8FF", + "Zombie": "E4D69B", + "Zorba": "A59B91", + "Zuccini": "044022", + "Zumthor": "EDF6FF" +} -- cgit v1.2.3 From 172fd3389481cc690d2eedaf1f70c56efb018bcf Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Sat, 11 Sep 2021 09:57:13 -0400 Subject: Add fuzzy match function I made a few changes, the biggest being the fuzzy match function to return a hex color code based on an input color name. Open items that I can think of so far: -Since the json file has color names and hex values, in order to use fuzzy matching for a color name the color must first be converted to hex. Currently there is only a rgb to anything function which returns values in a dictionary. -The main embed creation references the rgb_color before it is defined, should the command function be moved to the bottom of the file or just the main embed creation and sending? -When using the rgb mode, should the user be forced to do (r, g, b) or should the command handle an input of "r, g, b"? If you are reading this, thank you. --- bot/exts/utilities/color.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 6abfc006..9e199dd4 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -1,4 +1,5 @@ import colorsys +import json import logging import re from io import BytesIO @@ -6,7 +7,7 @@ from io import BytesIO from PIL import Image, ImageColor from discord import Embed, File from discord.ext import commands -# from rapidfuzz import process +from rapidfuzz import process from bot.bot import Bot from bot.constants import Colours @@ -23,6 +24,8 @@ ERROR_MSG = """The color code {user_color} is not a possible color combination. \nHex: #000000-#FFFFFF """ +COLOR_LIST = "bot/resources/utilities/ryanzec_colours.json" + # define color command class Color(commands.Cog): @@ -46,7 +49,7 @@ class Color(commands.Cog): else: await ctx.send( embed=Embed( - title="An error has occured.", + title="There was an issue converting the hex color code.", description=ERROR_MSG.format(user_color=user_color), ) ) @@ -58,9 +61,10 @@ class Color(commands.Cog): pass elif mode.lower() == "cmyk": pass + elif mode.lower() == "name": + color_name, hex_color = self.match_color(user_color) else: # mode is either None or an invalid code - # need to handle whether user passes color name if mode is None: no_mode_embed = Embed( title="No 'mode' was passed, please define a color code.", @@ -70,7 +74,7 @@ class Color(commands.Cog): return wrong_mode_embed = Embed( title=f"The color code {mode} is not a valid option", - description="Possible modes are: Hex, RGB, HSV, HSL and CMYK.", + description="Possible modes are: Name, Hex, RGB, HSV, HSL and CMYK.", color=Colours.soft_red, ) await ctx.send(embed=wrong_mode_embed) @@ -78,15 +82,15 @@ class Color(commands.Cog): async with ctx.typing(): main_embed = Embed( - title=user_color, # need to replace with fuzzymatch color name + title=color_name, description='(Approx..)', - color=hex_color, + color=rgb_color, ) file = await self.create_thumbnail_attachment(rgb_color) main_embed.set_thumbnail(url="attachment://color.png") - fields = self.get_color_fields(rgb_color) + for field in fields: main_embed.add_field( name=field['name'], @@ -94,7 +98,7 @@ class Color(commands.Cog): inline=False, ) - await ctx.send(file=file, embed=main_embed) + await ctx.send(file=file, embed=main_embed) @staticmethod async def create_thumbnail_attachment(color: str) -> File: @@ -192,6 +196,17 @@ class Color(commands.Cog): # if user_color in color_lists: # # fuzzy match for color + @staticmethod + def match_color(user_color: str) -> str: + """Use fuzzy matching to return a hex color code based on the user's input.""" + with open(COLOR_LIST) as f: + color_list = json.load(f) + logger.debug(f"{type(color_list) = }") + match, certainty, _ = process.extractOne(query=user_color, choices=color_list.keys(), score_cutoff=50) + logger.debug(f"{match = }, {certainty = }") + hex_match = color_list[match] + logger.debug(f"{hex_match = }") + return match, hex_match def setup(bot: Bot) -> None: -- cgit v1.2.3 From e77a8319e7e681262c73bdbb1335a708de676e8a Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Sat, 11 Sep 2021 10:09:27 -0400 Subject: Remove placeholder comment --- bot/exts/utilities/color.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 9e199dd4..6452d292 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -194,8 +194,6 @@ class Color(commands.Cog): return all_fields - # if user_color in color_lists: - # # fuzzy match for color @staticmethod def match_color(user_color: str) -> str: """Use fuzzy matching to return a hex color code based on the user's input.""" -- cgit v1.2.3 From 68c0de59f6f31fb1123c7ed3468c4faac202d9f6 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Sun, 12 Sep 2021 08:39:47 -0400 Subject: Load json file once --- bot/exts/utilities/color.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 6452d292..dd470197 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -25,6 +25,8 @@ ERROR_MSG = """The color code {user_color} is not a possible color combination. """ COLOR_LIST = "bot/resources/utilities/ryanzec_colours.json" +with open(COLOR_LIST) as f: + COLOR_JSON = json.load(f) # define color command @@ -197,12 +199,9 @@ class Color(commands.Cog): @staticmethod def match_color(user_color: str) -> str: """Use fuzzy matching to return a hex color code based on the user's input.""" - with open(COLOR_LIST) as f: - color_list = json.load(f) - logger.debug(f"{type(color_list) = }") - match, certainty, _ = process.extractOne(query=user_color, choices=color_list.keys(), score_cutoff=50) + match, certainty, _ = process.extractOne(query=user_color, choices=COLOR_JSON.keys(), score_cutoff=50) logger.debug(f"{match = }, {certainty = }") - hex_match = color_list[match] + hex_match = COLOR_JSON[match] logger.debug(f"{hex_match = }") return match, hex_match -- cgit v1.2.3 From 3f105042ae516b74fcaeca80c79d8f3894e43fb8 Mon Sep 17 00:00:00 2001 From: Kronifer <44979306+Kronifer@users.noreply.github.com> Date: Mon, 13 Sep 2021 18:22:26 -0500 Subject: added .gitpod.yml --- .gitpod.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .gitpod.yml diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 00000000..6ae59c4e --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,7 @@ +tasks: + - name: Initialize Poetry Environment + before: pyenv install 3.9.6 && pyenv global 3.9.6 + env: + PIP_USER: false + init: pip install poetry + command: poetry install && poetry run pre-commit install -- cgit v1.2.3 From e0d04101c772d54d302392e5d8a641be8d1c1199 Mon Sep 17 00:00:00 2001 From: Kronifer <44979306+Kronifer@users.noreply.github.com> Date: Tue, 14 Sep 2021 13:17:30 -0500 Subject: Update .gitpod.yml Co-authored-by: Bluenix --- .gitpod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitpod.yml b/.gitpod.yml index 6ae59c4e..3925d8f3 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -1,5 +1,5 @@ tasks: - - name: Initialize Poetry Environment + - name: Python Environment before: pyenv install 3.9.6 && pyenv global 3.9.6 env: PIP_USER: false -- cgit v1.2.3 From 57daa223af5503f30d86052cce35cb14bb0264be Mon Sep 17 00:00:00 2001 From: Kronifer <44979306+Kronifer@users.noreply.github.com> Date: Tue, 14 Sep 2021 17:18:55 -0500 Subject: chore: added vscode extensions to .gitpod.yml --- .gitpod.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitpod.yml b/.gitpod.yml index 3925d8f3..138bb8bd 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -5,3 +5,8 @@ tasks: PIP_USER: false init: pip install poetry command: poetry install && poetry run pre-commit install + +vscode: + extensions: + - ms-python.vscode-pylance + - ms-python.python -- cgit v1.2.3 From eca8bcebf7c47de7fa661009df583fe79b95334e Mon Sep 17 00:00:00 2001 From: Kronifer <44979306+Kronifer@users.noreply.github.com> Date: Tue, 14 Sep 2021 17:24:29 -0500 Subject: test --- .gitpod.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.gitpod.yml b/.gitpod.yml index 138bb8bd..3925d8f3 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -5,8 +5,3 @@ tasks: PIP_USER: false init: pip install poetry command: poetry install && poetry run pre-commit install - -vscode: - extensions: - - ms-python.vscode-pylance - - ms-python.python -- cgit v1.2.3 From 7b54e96edf3b74e407371a4f4df64c5038106633 Mon Sep 17 00:00:00 2001 From: Kronifer <44979306+Kronifer@users.noreply.github.com> Date: Wed, 15 Sep 2021 08:03:28 -0500 Subject: added double quotes to fix strings --- .gitpod.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitpod.yml b/.gitpod.yml index 3925d8f3..fcc425c5 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -1,7 +1,7 @@ tasks: - - name: Python Environment - before: pyenv install 3.9.6 && pyenv global 3.9.6 + - name: "Python Environment" + before: "pyenv install 3.9.6 && pyenv global 3.9.6" env: - PIP_USER: false - init: pip install poetry - command: poetry install && poetry run pre-commit install + PIP_USER: "false" + init: "pip install poetry" + command: "poetry install && poetry run pre-commit install" -- cgit v1.2.3 From 285993bc1cb5e4a1461e1c704b130d2a4e01a474 Mon Sep 17 00:00:00 2001 From: Kronifer Date: Wed, 15 Sep 2021 13:15:20 +0000 Subject: added vscode extensions --- .gitpod.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitpod.yml b/.gitpod.yml index fcc425c5..0c07be67 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -1,7 +1,9 @@ tasks: - name: "Python Environment" before: "pyenv install 3.9.6 && pyenv global 3.9.6" - env: - PIP_USER: "false" - init: "pip install poetry" + init: "pip install poetry && export PIP_USER=false" command: "poetry install && poetry run pre-commit install" + +vscode: + extensions: + - "ms-python.python" \ No newline at end of file -- cgit v1.2.3 From 674221ead6d5376b22a1ea31bcec0cbadecd6104 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Wed, 15 Sep 2021 16:16:52 -0400 Subject: Fix Flake8 spacing errors --- bot/exts/utilities/color.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index dd470197..eb9d5f4d 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -155,19 +155,19 @@ class Color(commands.Cog): def _rgb_to_hsv(rgb_color: tuple[int, int, int]) -> tuple[int, int, int]: """To convert from `RGB` to `HSV` color space.""" r, g, b = rgb_color - h, v, s = colorsys.rgb_to_hsv(r/float(255), g/float(255), b/float(255)) - h = round(h*360) - s = round(s*100) - v = round(v*100) + h, v, s = colorsys.rgb_to_hsv(r / float(255), g / float(255), b / float(255)) + h = round(h * 360) + s = round(s * 100) + v = round(v * 100) return h, s, v def _rgb_to_hsl(rgb_color: tuple[int, int, int]) -> tuple[int, int, int]: """To convert from `RGB` to `HSL` color space.""" r, g, b = rgb_color - h, l, s = colorsys.rgb_to_hls(r/float(255), g/float(255), b/float(255)) - h = round(h*360) - s = round(s*100) - l = round(l*100) # noqa: E741 It's little `L`, Reason: To maintain consistency. + h, l, s = colorsys.rgb_to_hls(r / float(255), g / float(255), b / float(255)) + h = round(h * 360) + s = round(s * 100) + l = round(l * 100) # noqa: E741 It's little `L`, Reason: To maintain consistency. return h, s, l hex_color = _rgb_to_hex(rgb_color) -- cgit v1.2.3 From 7c08af2655c07d0d13b247905eb23acbbd42e976 Mon Sep 17 00:00:00 2001 From: Kronifer <44979306+Kronifer@users.noreply.github.com> Date: Wed, 15 Sep 2021 19:52:40 -0500 Subject: chore: small quotation change --- .gitpod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitpod.yml b/.gitpod.yml index 0c07be67..ca1b5456 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -6,4 +6,4 @@ tasks: vscode: extensions: - - "ms-python.python" \ No newline at end of file + - ms-python.python -- cgit v1.2.3 From 3d67a305dd5f7a572aa71648c0c1b8b710304bbd Mon Sep 17 00:00:00 2001 From: Kronifer <44979306+Kronifer@users.noreply.github.com> Date: Wed, 15 Sep 2021 19:54:44 -0500 Subject: removed vscode.extensions due to errors --- .gitpod.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitpod.yml b/.gitpod.yml index ca1b5456..a10e6e26 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -3,7 +3,3 @@ tasks: before: "pyenv install 3.9.6 && pyenv global 3.9.6" init: "pip install poetry && export PIP_USER=false" command: "poetry install && poetry run pre-commit install" - -vscode: - extensions: - - ms-python.python -- cgit v1.2.3 From 029b8052b3d038be2808d5995d4f6422158b0084 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Sun, 19 Sep 2021 08:20:18 -0400 Subject: Reword json file variables and mapping --- bot/exts/utilities/color.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index eb9d5f4d..94c9d337 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -24,9 +24,9 @@ ERROR_MSG = """The color code {user_color} is not a possible color combination. \nHex: #000000-#FFFFFF """ -COLOR_LIST = "bot/resources/utilities/ryanzec_colours.json" -with open(COLOR_LIST) as f: - COLOR_JSON = json.load(f) +COLOR_JSON_PATH = "bot/resources/utilities/ryanzec_colours.json" +with open(COLOR_JSON_PATH) as f: + COLOR_MAPPING = json.load(f) # define color command @@ -199,9 +199,9 @@ class Color(commands.Cog): @staticmethod def match_color(user_color: str) -> str: """Use fuzzy matching to return a hex color code based on the user's input.""" - match, certainty, _ = process.extractOne(query=user_color, choices=COLOR_JSON.keys(), score_cutoff=50) + match, certainty, _ = process.extractOne(query=user_color, choices=COLOR_MAPPING.keys(), score_cutoff=50) logger.debug(f"{match = }, {certainty = }") - hex_match = COLOR_JSON[match] + hex_match = COLOR_MAPPING[match] logger.debug(f"{hex_match = }") return match, hex_match -- cgit v1.2.3 From 40e04bf4d304f8b2f231fedcb38db1f532550f0c Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Sun, 19 Sep 2021 10:20:58 -0400 Subject: Continue work on hex and rgb color commands --- bot/exts/utilities/color.py | 87 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 69 insertions(+), 18 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 94c9d337..2bec6ba3 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -31,23 +31,45 @@ with open(COLOR_JSON_PATH) as f: # define color command class Color(commands.Cog): - """User initiated command to receive color information.""" + """User initiated commands to receive color information.""" def __init__(self, bot: Bot): self.bot = bot @commands.command(aliases=["colour"]) async def color(self, ctx: commands.Context, mode: str, *, user_color: str) -> None: - """Send information on input color code or color name.""" - logger.info(f"{mode = }") - logger.info(f"{user_color = }") + """ + Send information on input color code or color name. + + Possible modes are: "hex", "rgb", "hsv", "hsl", "cmyk" or "name". + """ + logger.debug(f"{mode = }") + logger.debug(f"{user_color = }") if mode.lower() == "hex": - hex_match = re.search(r"^#(?:[0-9a-fA-F]{3}){1,2}$", user_color) + hex_match = re.fullmatch(r"(#?[0x]?)((?:[0-9a-fA-F]{3}){1,2})", user_color) if hex_match: hex_color = int(hex(int(user_color.replace("#", ""), 16)), 0) - rgb_color = ImageColor.getcolor(user_color, "RGB") - logger.info(f"{hex_color = }") - logger.info(f"{rgb_color = }") + if "#" in user_color: + rgb_color = ImageColor.getcolor(user_color, "RGB") + elif "0x" in user_color: + hex_ = user_color.replace("0x", "#") + rgb_color = ImageColor.getcolor(hex_, "RGB") + else: + hex_ = "#" + user_color + rgb_color = ImageColor.getcolor(hex_, "RGB") + (r, g, b) = rgb_color + discord_rgb_int = int(f"{r:02x}{g:02x}{b:02x}", 16) + all_colors = self.get_color_fields(rgb_color) + hex_color = all_colors[1]["value"].replace("Β» hex ", "") + cmyk_color = all_colors[2]["value"].replace("Β» cmyk ", "") + hsv_color = all_colors[3]["value"].replace("Β» hsv ", "") + hsl_color = all_colors[4]["value"].replace("Β» hsl ", "") + logger.debug(f"{rgb_color = }") + logger.debug(f"{hex_color = }") + logger.debug(f"{hsv_color = }") + logger.debug(f"{hsl_color = }") + logger.debug(f"{cmyk_color = }") + color_name, _ = self.match_color(hex_color) else: await ctx.send( embed=Embed( @@ -56,7 +78,22 @@ class Color(commands.Cog): ) ) elif mode.lower() == "rgb": - pass + if "(" in user_color: + remove = "[() ]" + rgb_color = re.sub(remove, "", user_color) + rgb_color = tuple(map(int, rgb_color.split(","))) + elif "," in user_color: + rgb_color = tuple(map(int, user_color.split(","))) + else: + rgb_color = tuple(map(int, user_color.split(" "))) + (r, g, b) = rgb_color + discord_rgb_int = int(f"{r:02x}{g:02x}{b:02x}", 16) + all_colors = self.get_color_fields(rgb_color) + hex_color = all_colors[1]["value"].replace("Β» hex ", "") + cmyk_color = all_colors[2]["value"].replace("Β» cmyk ", "") + hsv_color = all_colors[3]["value"].replace("Β» hsv ", "") + hsl_color = all_colors[4]["value"].replace("Β» hsl ", "") + color_name, _ = self.match_color(hex_color) elif mode.lower() == "hsv": pass elif mode.lower() == "hsl": @@ -70,6 +107,7 @@ class Color(commands.Cog): if mode is None: no_mode_embed = Embed( title="No 'mode' was passed, please define a color code.", + description="Possible modes are: Name, Hex, RGB, HSV, HSL and CMYK.", color=Colours.soft_red, ) await ctx.send(embed=no_mode_embed) @@ -86,7 +124,7 @@ class Color(commands.Cog): main_embed = Embed( title=color_name, description='(Approx..)', - color=rgb_color, + color=discord_rgb_int, ) file = await self.create_thumbnail_attachment(rgb_color) @@ -120,7 +158,7 @@ class Color(commands.Cog): def _rgb_to_hex(rgb_color: tuple[int, int, int]) -> str: """To convert from `RGB` to `Hex` notation.""" - return '#' + ''.join(hex(color)[2:].zfill(2) for color in rgb_color).upper() + return '#' + ''.join(hex(int(color))[2:].zfill(2) for color in rgb_color).upper() def _rgb_to_cmyk(rgb_color: tuple[int, int, int]) -> tuple[int, int, int, int]: """To convert from `RGB` to `CMYK` color space.""" @@ -178,7 +216,11 @@ class Color(commands.Cog): all_fields = [ { "name": "RGB", - "value": f"Β» rgb {rgb_color}\nΒ» hex {hex_color}" + "value": f"Β» rgb {rgb_color}" + }, + { + "name": "HEX", + "value": f"Β» hex {hex_color}" }, { "name": "CMYK", @@ -197,13 +239,22 @@ class Color(commands.Cog): return all_fields @staticmethod - def match_color(user_color: str) -> str: + def match_color(input_hex_color: str) -> str: """Use fuzzy matching to return a hex color code based on the user's input.""" - match, certainty, _ = process.extractOne(query=user_color, choices=COLOR_MAPPING.keys(), score_cutoff=50) - logger.debug(f"{match = }, {certainty = }") - hex_match = COLOR_MAPPING[match] - logger.debug(f"{hex_match = }") - return match, hex_match + try: + match, certainty, _ = process.extractOne( + query=input_hex_color, + choices=COLOR_MAPPING.keys(), + score_cutoff=50 + ) + logger.debug(f"{match = }, {certainty = }") + hex_match = COLOR_MAPPING[match] + logger.debug(f"{hex_match = }") + return match, hex_match + except TypeError: + match = "No color name match found." + hex_match = input_hex_color + return match, hex_match def setup(bot: Bot) -> None: -- cgit v1.2.3 From d55b59e5590e701094915bb1aff36d0cd38d061b Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Sun, 19 Sep 2021 17:49:32 -0400 Subject: Add all color modes and name matching --- bot/exts/utilities/color.py | 140 ++++++++++++++++++++++++++++++++------------ 1 file changed, 104 insertions(+), 36 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 2bec6ba3..9b1f3776 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -45,6 +45,7 @@ class Color(commands.Cog): """ logger.debug(f"{mode = }") logger.debug(f"{user_color = }") + color_name = None if mode.lower() == "hex": hex_match = re.fullmatch(r"(#?[0x]?)((?:[0-9a-fA-F]{3}){1,2})", user_color) if hex_match: @@ -57,19 +58,6 @@ class Color(commands.Cog): else: hex_ = "#" + user_color rgb_color = ImageColor.getcolor(hex_, "RGB") - (r, g, b) = rgb_color - discord_rgb_int = int(f"{r:02x}{g:02x}{b:02x}", 16) - all_colors = self.get_color_fields(rgb_color) - hex_color = all_colors[1]["value"].replace("Β» hex ", "") - cmyk_color = all_colors[2]["value"].replace("Β» cmyk ", "") - hsv_color = all_colors[3]["value"].replace("Β» hsv ", "") - hsl_color = all_colors[4]["value"].replace("Β» hsl ", "") - logger.debug(f"{rgb_color = }") - logger.debug(f"{hex_color = }") - logger.debug(f"{hsv_color = }") - logger.debug(f"{hsl_color = }") - logger.debug(f"{cmyk_color = }") - color_name, _ = self.match_color(hex_color) else: await ctx.send( embed=Embed( @@ -78,30 +66,22 @@ class Color(commands.Cog): ) ) elif mode.lower() == "rgb": - if "(" in user_color: - remove = "[() ]" - rgb_color = re.sub(remove, "", user_color) - rgb_color = tuple(map(int, rgb_color.split(","))) - elif "," in user_color: - rgb_color = tuple(map(int, user_color.split(","))) - else: - rgb_color = tuple(map(int, user_color.split(" "))) - (r, g, b) = rgb_color - discord_rgb_int = int(f"{r:02x}{g:02x}{b:02x}", 16) - all_colors = self.get_color_fields(rgb_color) - hex_color = all_colors[1]["value"].replace("Β» hex ", "") - cmyk_color = all_colors[2]["value"].replace("Β» cmyk ", "") - hsv_color = all_colors[3]["value"].replace("Β» hsv ", "") - hsl_color = all_colors[4]["value"].replace("Β» hsl ", "") - color_name, _ = self.match_color(hex_color) + rgb_color = self.tuple_create(user_color) elif mode.lower() == "hsv": - pass + hsv_temp = self.tuple_create(user_color) + rgb_color = self.hsv_to_rgb(hsv_temp) elif mode.lower() == "hsl": - pass + hsl_temp = self.tuple_create(user_color) + rgb_color = self.hsl_to_rgb(hsl_temp) elif mode.lower() == "cmyk": - pass + cmyk_temp = self.tuple_create(user_color) + rgb_color = self.cmyk_to_rgb(cmyk_temp) elif mode.lower() == "name": - color_name, hex_color = self.match_color(user_color) + color_name, hex_color = self.match_color_name(user_color) + if "#" in hex_color: + rgb_color = ImageColor.getcolor(hex_color, "RGB") + else: + rgb_color = ImageColor.getcolor("#" + hex_color, "RGB") else: # mode is either None or an invalid code if mode is None: @@ -120,6 +100,14 @@ class Color(commands.Cog): await ctx.send(embed=wrong_mode_embed) return + (r, g, b) = rgb_color + discord_rgb_int = int(f"{r:02x}{g:02x}{b:02x}", 16) + all_colors = self.get_color_fields(rgb_color) + hex_color = all_colors[1]["value"].replace("Β» hex ", "") + if color_name is None: + logger.debug(f"Find color name from hex color: {hex_color}") + color_name = self.match_color_hex(hex_color) + async with ctx.typing(): main_embed = Embed( title=color_name, @@ -239,11 +227,11 @@ class Color(commands.Cog): return all_fields @staticmethod - def match_color(input_hex_color: str) -> str: + def match_color_name(input_color_name: str) -> str: """Use fuzzy matching to return a hex color code based on the user's input.""" try: match, certainty, _ = process.extractOne( - query=input_hex_color, + query=input_color_name, choices=COLOR_MAPPING.keys(), score_cutoff=50 ) @@ -253,9 +241,89 @@ class Color(commands.Cog): return match, hex_match except TypeError: match = "No color name match found." - hex_match = input_hex_color + hex_match = input_color_name return match, hex_match + @staticmethod + def match_color_hex(input_hex_color: str) -> str: + """Use fuzzy matching to return a hex color code based on the user's input.""" + try: + match, certainty, _ = process.extractOne( + query=input_hex_color, + choices=COLOR_MAPPING.values(), + score_cutoff=80 + ) + logger.debug(f"{match = }, {certainty = }") + color_name = [name for name, _ in COLOR_MAPPING.items() if _ == match][0] + logger.debug(f"{color_name = }") + return color_name + except TypeError: + color_name = "No color name match found." + return color_name + + @staticmethod + def tuple_create(input_color: str) -> tuple[int, int, int]: + """ + Create a tuple of integers based on user's input. + + Can handle inputs of the types: + (100, 100, 100) + 100, 100, 100 + 100 100 100 + """ + if "(" in input_color: + remove = "[() ]" + color_tuple = re.sub(remove, "", input_color) + color_tuple = tuple(map(int, color_tuple.split(","))) + elif "," in input_color: + color_tuple = tuple(map(int, input_color.split(","))) + else: + color_tuple = tuple(map(int, input_color.split(" "))) + return color_tuple + + @staticmethod + def hsv_to_rgb(input_color: tuple[int, int, int]) -> tuple[int, int, int]: + """Function to convert hsv color to rgb color.""" + (h, v, s) = input_color # the function hsv_to_rgb expects v and s to be swapped + h = h / 360 + s = s / 100 + v = v / 100 + rgb_color = colorsys.hsv_to_rgb(h, s, v) + (r, g, b) = rgb_color + r = int(r * 255) + g = int(g * 255) + b = int(b * 255) + rgb_color = (r, g, b) + return rgb_color + + @staticmethod + def hsl_to_rgb(input_color: tuple[int, int, int]) -> tuple[int, int, int]: + """Function to convert hsl color to rgb color.""" + (h, s, l) = input_color + h = h / 360 + s = s / 100 + l = l / 100 # noqa: E741 It's little `L`, Reason: To maintain consistency. + rgb_color = colorsys.hls_to_rgb(h, l, s) + (r, g, b) = rgb_color + r = int(r * 255) + g = int(g * 255) + b = int(b * 255) + rgb_color = (r, g, b) + return rgb_color + + @staticmethod + def cmyk_to_rgb(input_color: tuple[int, int, int, int]) -> tuple[int, int, int]: + """Function to convert cmyk color to rgb color.""" + c = input_color[0] + m = input_color[1] + y = input_color[2] + k = input_color[3] + r = int(255 * (1.0 - c / float(100)) * (1.0 - k / float(100))) + g = int(255 * (1.0 - m / float(100)) * (1.0 - k / float(100))) + b = int(255 * (1.0 - y / float(100)) * (1.0 - k / float(100))) + rgb_color = (r, g, b) + return rgb_color + def setup(bot: Bot) -> None: """Load the Color Cog.""" -- cgit v1.2.3 From 0b59a5e8d110e1aa7e5d76addcf6a92ee5f89ace Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Mon, 20 Sep 2021 17:20:27 +0200 Subject: Emoji: make the datetimes offset-naive You know the drill, due to discord.py 2.0a0 datetimes are now offset-aware, breaking some code. Closes python-discord/sir-lancebot#875 --- bot/exts/utilities/emoji.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/utilities/emoji.py b/bot/exts/utilities/emoji.py index 55d6b8e9..83df39cc 100644 --- a/bot/exts/utilities/emoji.py +++ b/bot/exts/utilities/emoji.py @@ -107,8 +107,8 @@ class Emojis(commands.Cog): title=f"Emoji Information: {emoji.name}", description=textwrap.dedent(f""" **Name:** {emoji.name} - **Created:** {time_since(emoji.created_at, precision="hours")} - **Date:** {datetime.strftime(emoji.created_at, "%d/%m/%Y")} + **Created:** {time_since(emoji.created_at.replace(tzinfo=None), precision="hours")} + **Date:** {datetime.strftime(emoji.created_at.replace(tzinfo=None), "%d/%m/%Y")} **ID:** {emoji.id} """), color=Color.blurple(), -- cgit v1.2.3 From 24e099e8247e51f91d96294f147c6e024bd1db69 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Tue, 21 Sep 2021 09:21:33 +0200 Subject: Hacktoberfest: make datetimes offset-naive --- bot/exts/events/hacktoberfest/hacktober-issue-finder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/events/hacktoberfest/hacktober-issue-finder.py b/bot/exts/events/hacktoberfest/hacktober-issue-finder.py index e3053851..0cbb6df4 100644 --- a/bot/exts/events/hacktoberfest/hacktober-issue-finder.py +++ b/bot/exts/events/hacktoberfest/hacktober-issue-finder.py @@ -52,10 +52,10 @@ class HacktoberIssues(commands.Cog): async def get_issues(self, ctx: commands.Context, option: str) -> Optional[dict]: """Get a list of the python issues with the label 'hacktoberfest' from the Github api.""" if option == "beginner": - if (ctx.message.created_at - self.cache_timer_beginner).seconds <= 60: + if (ctx.message.created_at.replace(tzinfo=None) - self.cache_timer_beginner).seconds <= 60: log.debug("using cache") return self.cache_beginner - elif (ctx.message.created_at - self.cache_timer_normal).seconds <= 60: + elif (ctx.message.created_at.replace(tzinfo=None) - self.cache_timer_normal).seconds <= 60: log.debug("using cache") return self.cache_normal -- cgit v1.2.3 From 54880a121a0117639d935384905d2f1f0c6f606d Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Tue, 21 Sep 2021 06:51:55 -0400 Subject: fix: remove redundant rgb_color variable The conversion functions from hsv, hsl and cmyk now return r, g, b instead of a variable rgb_tuple. --- bot/exts/utilities/color.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 9b1f3776..542a2e19 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -293,8 +293,7 @@ class Color(commands.Cog): r = int(r * 255) g = int(g * 255) b = int(b * 255) - rgb_color = (r, g, b) - return rgb_color + return r, g, b @staticmethod def hsl_to_rgb(input_color: tuple[int, int, int]) -> tuple[int, int, int]: @@ -308,8 +307,7 @@ class Color(commands.Cog): r = int(r * 255) g = int(g * 255) b = int(b * 255) - rgb_color = (r, g, b) - return rgb_color + return r, g, b @staticmethod def cmyk_to_rgb(input_color: tuple[int, int, int, int]) -> tuple[int, int, int]: @@ -321,8 +319,7 @@ class Color(commands.Cog): r = int(255 * (1.0 - c / float(100)) * (1.0 - k / float(100))) g = int(255 * (1.0 - m / float(100)) * (1.0 - k / float(100))) b = int(255 * (1.0 - y / float(100)) * (1.0 - k / float(100))) - rgb_color = (r, g, b) - return rgb_color + return r, g, b def setup(bot: Bot) -> None: -- cgit v1.2.3 From 6e7e65d12392bfa44a13d82fa90631482d9eb653 Mon Sep 17 00:00:00 2001 From: aru Date: Tue, 21 Sep 2021 16:51:27 -0400 Subject: properly blacklist the extensions cog since the restructure of lancebot, this code was incorrect, and is no longer blacklisting itself. --- bot/exts/core/extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/core/extensions.py b/bot/exts/core/extensions.py index 424bacac..dbb9e069 100644 --- a/bot/exts/core/extensions.py +++ b/bot/exts/core/extensions.py @@ -18,7 +18,7 @@ from bot.utils.pagination import LinePaginator log = logging.getLogger(__name__) -UNLOAD_BLACKLIST = {f"{exts.__name__}.utils.extensions"} +UNLOAD_BLACKLIST = {f"{exts.__name__}.core.extensions"} BASE_PATH_LEN = len(exts.__name__.split(".")) -- cgit v1.2.3 From 835e4469938ab2b490dcf752d2fcecf117bd0a62 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Wed, 22 Sep 2021 09:24:23 -0400 Subject: fix: create subcommands and restructure script -Makes "main" function `color_embed` that takes an rgb tuple, calls `all_colors` to get all other color types, gets a name from the hex color, creates embed, calls `create_thumbnail` to get image, and then sends main embed. -Makes functions `xxx_to_rgb` functions to call `color_embed` -Creates new `hex_to_rgb` function -TODO: test all functions and continue restructure. --- bot/exts/utilities/color.py | 81 +++++++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 36 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 542a2e19..5d37cbee 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -47,35 +47,16 @@ class Color(commands.Cog): logger.debug(f"{user_color = }") color_name = None if mode.lower() == "hex": - hex_match = re.fullmatch(r"(#?[0x]?)((?:[0-9a-fA-F]{3}){1,2})", user_color) - if hex_match: - hex_color = int(hex(int(user_color.replace("#", ""), 16)), 0) - if "#" in user_color: - rgb_color = ImageColor.getcolor(user_color, "RGB") - elif "0x" in user_color: - hex_ = user_color.replace("0x", "#") - rgb_color = ImageColor.getcolor(hex_, "RGB") - else: - hex_ = "#" + user_color - rgb_color = ImageColor.getcolor(hex_, "RGB") - else: - await ctx.send( - embed=Embed( - title="There was an issue converting the hex color code.", - description=ERROR_MSG.format(user_color=user_color), - ) - ) + self.hex_to_rgb(ctx, user_color) elif mode.lower() == "rgb": rgb_color = self.tuple_create(user_color) + self.embed_color(rgb_color) elif mode.lower() == "hsv": - hsv_temp = self.tuple_create(user_color) - rgb_color = self.hsv_to_rgb(hsv_temp) + self.hsv_to_rgb(user_color) elif mode.lower() == "hsl": - hsl_temp = self.tuple_create(user_color) - rgb_color = self.hsl_to_rgb(hsl_temp) + self.hsl_to_rgb(user_color) elif mode.lower() == "cmyk": - cmyk_temp = self.tuple_create(user_color) - rgb_color = self.cmyk_to_rgb(cmyk_temp) + self.cmyk_to_rgb(user_color) elif mode.lower() == "name": color_name, hex_color = self.match_color_name(user_color) if "#" in hex_color: @@ -100,6 +81,13 @@ class Color(commands.Cog): await ctx.send(embed=wrong_mode_embed) return + async def color_embed( + self, + ctx: commands.Context, + rgb_color: tuple[int, int, int], + color_name: str = None + ) -> None: + """Take a RGB color tuple, create embed, and send.""" (r, g, b) = rgb_color discord_rgb_int = int(f"{r:02x}{g:02x}{b:02x}", 16) all_colors = self.get_color_fields(rgb_color) @@ -281,9 +269,9 @@ class Color(commands.Cog): color_tuple = tuple(map(int, input_color.split(" "))) return color_tuple - @staticmethod - def hsv_to_rgb(input_color: tuple[int, int, int]) -> tuple[int, int, int]: - """Function to convert hsv color to rgb color.""" + def hsv_to_rgb(self, input_color: tuple[int, int, int]) -> tuple[int, int, int]: + """Function to convert hsv color to rgb color and send main embed..""" + input_color = self.tuple_create(input_color) (h, v, s) = input_color # the function hsv_to_rgb expects v and s to be swapped h = h / 360 s = s / 100 @@ -293,11 +281,11 @@ class Color(commands.Cog): r = int(r * 255) g = int(g * 255) b = int(b * 255) - return r, g, b + self.color_embed((r, g, b)) - @staticmethod - def hsl_to_rgb(input_color: tuple[int, int, int]) -> tuple[int, int, int]: - """Function to convert hsl color to rgb color.""" + def hsl_to_rgb(self, input_color: tuple[int, int, int]) -> tuple[int, int, int]: + """Function to convert hsl color to rgb color and send main embed..""" + input_color = self.tuple_create(input_color) (h, s, l) = input_color h = h / 360 s = s / 100 @@ -307,11 +295,11 @@ class Color(commands.Cog): r = int(r * 255) g = int(g * 255) b = int(b * 255) - return r, g, b + self.color_embed((r, g, b)) - @staticmethod - def cmyk_to_rgb(input_color: tuple[int, int, int, int]) -> tuple[int, int, int]: - """Function to convert cmyk color to rgb color.""" + def cmyk_to_rgb(self, input_color: tuple[int, int, int, int]) -> tuple[int, int, int]: + """Function to convert cmyk color to rgb color and send main embed..""" + input_color = self.tuple_create(input_color) c = input_color[0] m = input_color[1] y = input_color[2] @@ -319,7 +307,28 @@ class Color(commands.Cog): r = int(255 * (1.0 - c / float(100)) * (1.0 - k / float(100))) g = int(255 * (1.0 - m / float(100)) * (1.0 - k / float(100))) b = int(255 * (1.0 - y / float(100)) * (1.0 - k / float(100))) - return r, g, b + self.color_embed((r, g, b)) + + async def hex_to_rgb(self, ctx: commands.Context, hex_string: str) -> None: + """Create rgb color from hex string and send main embed.""" + hex_match = re.fullmatch(r"(#?[0x]?)((?:[0-9a-fA-F]{3}){1,2})", hex_string) + if hex_match: + if "#" in hex_string: + rgb_color = ImageColor.getcolor(hex_string, "RGB") + elif "0x" in hex_string: + hex_ = hex_string.replace("0x", "#") + rgb_color = ImageColor.getcolor(hex_, "RGB") + else: + hex_ = "#" + hex_string + rgb_color = ImageColor.getcolor(hex_, "RGB") + self.color_embed(rgb_color) + else: + await ctx.send( + embed=Embed( + title="There was an issue converting the hex color code.", + description=ERROR_MSG.format(user_color=hex_string), + ) + ) def setup(bot: Bot) -> None: -- cgit v1.2.3 From 7694caccf42ac05c373b2f8fcc180205d43d7883 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Wed, 22 Sep 2021 09:38:34 -0400 Subject: fix: restructure script --- bot/exts/utilities/color.py | 225 ++++++++++++++++++++++---------------------- 1 file changed, 115 insertions(+), 110 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 5d37cbee..67068809 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -50,19 +50,20 @@ class Color(commands.Cog): self.hex_to_rgb(ctx, user_color) elif mode.lower() == "rgb": rgb_color = self.tuple_create(user_color) - self.embed_color(rgb_color) + self.color_embed(ctx, rgb_color) elif mode.lower() == "hsv": - self.hsv_to_rgb(user_color) + self.hsv_to_rgb(ctx, user_color) elif mode.lower() == "hsl": - self.hsl_to_rgb(user_color) + self.hsl_to_rgb(ctx, user_color) elif mode.lower() == "cmyk": - self.cmyk_to_rgb(user_color) + self.cmyk_to_rgb(ctx, user_color) elif mode.lower() == "name": color_name, hex_color = self.match_color_name(user_color) if "#" in hex_color: rgb_color = ImageColor.getcolor(hex_color, "RGB") else: rgb_color = ImageColor.getcolor("#" + hex_color, "RGB") + self.color_embed(ctx, rgb_color, color_name) else: # mode is either None or an invalid code if mode is None: @@ -81,44 +82,94 @@ class Color(commands.Cog): await ctx.send(embed=wrong_mode_embed) return - async def color_embed( - self, - ctx: commands.Context, - rgb_color: tuple[int, int, int], - color_name: str = None - ) -> None: - """Take a RGB color tuple, create embed, and send.""" - (r, g, b) = rgb_color - discord_rgb_int = int(f"{r:02x}{g:02x}{b:02x}", 16) - all_colors = self.get_color_fields(rgb_color) - hex_color = all_colors[1]["value"].replace("Β» hex ", "") - if color_name is None: - logger.debug(f"Find color name from hex color: {hex_color}") - color_name = self.match_color_hex(hex_color) + @staticmethod + def tuple_create(input_color: str) -> tuple[int, int, int]: + """ + Create a tuple of integers based on user's input. - async with ctx.typing(): - main_embed = Embed( - title=color_name, - description='(Approx..)', - color=discord_rgb_int, + Can handle inputs of the types: + (100, 100, 100) + 100, 100, 100 + 100 100 100 + """ + if "(" in input_color: + remove = "[() ]" + color_tuple = re.sub(remove, "", input_color) + color_tuple = tuple(map(int, color_tuple.split(","))) + elif "," in input_color: + color_tuple = tuple(map(int, input_color.split(","))) + else: + color_tuple = tuple(map(int, input_color.split(" "))) + return color_tuple + + async def hex_to_rgb(self, ctx: commands.Context, hex_string: str) -> None: + """Function to convert hex color to rgb color and send main embed.""" + hex_match = re.fullmatch(r"(#?[0x]?)((?:[0-9a-fA-F]{3}){1,2})", hex_string) + if hex_match: + if "#" in hex_string: + rgb_color = ImageColor.getcolor(hex_string, "RGB") + elif "0x" in hex_string: + hex_ = hex_string.replace("0x", "#") + rgb_color = ImageColor.getcolor(hex_, "RGB") + else: + hex_ = "#" + hex_string + rgb_color = ImageColor.getcolor(hex_, "RGB") + self.color_embed(rgb_color) + else: + await ctx.send( + embed=Embed( + title="There was an issue converting the hex color code.", + description=ERROR_MSG.format(user_color=hex_string), + ) ) - file = await self.create_thumbnail_attachment(rgb_color) - main_embed.set_thumbnail(url="attachment://color.png") - fields = self.get_color_fields(rgb_color) + def hsv_to_rgb(self, input_color: tuple[int, int, int]) -> tuple[int, int, int]: + """Function to convert hsv color to rgb color and send main embed.""" + input_color = self.tuple_create(input_color) + (h, v, s) = input_color # the function hsv_to_rgb expects v and s to be swapped + h = h / 360 + s = s / 100 + v = v / 100 + rgb_color = colorsys.hsv_to_rgb(h, s, v) + (r, g, b) = rgb_color + r = int(r * 255) + g = int(g * 255) + b = int(b * 255) + self.color_embed((r, g, b)) - for field in fields: - main_embed.add_field( - name=field['name'], - value=field['value'], - inline=False, - ) + def hsl_to_rgb(self, input_color: tuple[int, int, int]) -> tuple[int, int, int]: + """Function to convert hsl color to rgb color and send main embed.""" + input_color = self.tuple_create(input_color) + (h, s, l) = input_color + h = h / 360 + s = s / 100 + l = l / 100 # noqa: E741 It's little `L`, Reason: To maintain consistency. + rgb_color = colorsys.hls_to_rgb(h, l, s) + (r, g, b) = rgb_color + r = int(r * 255) + g = int(g * 255) + b = int(b * 255) + self.color_embed((r, g, b)) - await ctx.send(file=file, embed=main_embed) + def cmyk_to_rgb(self, input_color: tuple[int, int, int, int]) -> tuple[int, int, int]: + """Function to convert cmyk color to rgb color and send main embed.""" + input_color = self.tuple_create(input_color) + c = input_color[0] + m = input_color[1] + y = input_color[2] + k = input_color[3] + r = int(255 * (1.0 - c / float(100)) * (1.0 - k / float(100))) + g = int(255 * (1.0 - m / float(100)) * (1.0 - k / float(100))) + b = int(255 * (1.0 - y / float(100)) * (1.0 - k / float(100))) + self.color_embed((r, g, b)) @staticmethod - async def create_thumbnail_attachment(color: str) -> File: - """Generate a thumbnail from `color`.""" + async def create_thumbnail_attachment(color: tuple[int, int, int]) -> File: + """ + Generate a thumbnail from `color`. + + Assumes that color is an rgb tuple. + """ thumbnail = Image.new("RGB", (100, 100), color=color) bufferedio = BytesIO() thumbnail.save(bufferedio, format="PNG") @@ -249,86 +300,40 @@ class Color(commands.Cog): color_name = "No color name match found." return color_name - @staticmethod - def tuple_create(input_color: str) -> tuple[int, int, int]: - """ - Create a tuple of integers based on user's input. - - Can handle inputs of the types: - (100, 100, 100) - 100, 100, 100 - 100 100 100 - """ - if "(" in input_color: - remove = "[() ]" - color_tuple = re.sub(remove, "", input_color) - color_tuple = tuple(map(int, color_tuple.split(","))) - elif "," in input_color: - color_tuple = tuple(map(int, input_color.split(","))) - else: - color_tuple = tuple(map(int, input_color.split(" "))) - return color_tuple - - def hsv_to_rgb(self, input_color: tuple[int, int, int]) -> tuple[int, int, int]: - """Function to convert hsv color to rgb color and send main embed..""" - input_color = self.tuple_create(input_color) - (h, v, s) = input_color # the function hsv_to_rgb expects v and s to be swapped - h = h / 360 - s = s / 100 - v = v / 100 - rgb_color = colorsys.hsv_to_rgb(h, s, v) + async def color_embed( + self, + ctx: commands.Context, + rgb_color: tuple[int, int, int], + color_name: str = None + ) -> None: + """Take a RGB color tuple, create embed, and send.""" (r, g, b) = rgb_color - r = int(r * 255) - g = int(g * 255) - b = int(b * 255) - self.color_embed((r, g, b)) + discord_rgb_int = int(f"{r:02x}{g:02x}{b:02x}", 16) + all_colors = self.get_color_fields(rgb_color) + hex_color = all_colors[1]["value"].replace("Β» hex ", "") + if color_name is None: + logger.debug(f"Find color name from hex color: {hex_color}") + color_name = self.match_color_hex(hex_color) - def hsl_to_rgb(self, input_color: tuple[int, int, int]) -> tuple[int, int, int]: - """Function to convert hsl color to rgb color and send main embed..""" - input_color = self.tuple_create(input_color) - (h, s, l) = input_color - h = h / 360 - s = s / 100 - l = l / 100 # noqa: E741 It's little `L`, Reason: To maintain consistency. - rgb_color = colorsys.hls_to_rgb(h, l, s) - (r, g, b) = rgb_color - r = int(r * 255) - g = int(g * 255) - b = int(b * 255) - self.color_embed((r, g, b)) + async with ctx.typing(): + main_embed = Embed( + title=color_name, + description='(Approx..)', + color=discord_rgb_int, + ) - def cmyk_to_rgb(self, input_color: tuple[int, int, int, int]) -> tuple[int, int, int]: - """Function to convert cmyk color to rgb color and send main embed..""" - input_color = self.tuple_create(input_color) - c = input_color[0] - m = input_color[1] - y = input_color[2] - k = input_color[3] - r = int(255 * (1.0 - c / float(100)) * (1.0 - k / float(100))) - g = int(255 * (1.0 - m / float(100)) * (1.0 - k / float(100))) - b = int(255 * (1.0 - y / float(100)) * (1.0 - k / float(100))) - self.color_embed((r, g, b)) + file = await self.create_thumbnail_attachment(rgb_color) + main_embed.set_thumbnail(url="attachment://color.png") + fields = self.get_color_fields(rgb_color) - async def hex_to_rgb(self, ctx: commands.Context, hex_string: str) -> None: - """Create rgb color from hex string and send main embed.""" - hex_match = re.fullmatch(r"(#?[0x]?)((?:[0-9a-fA-F]{3}){1,2})", hex_string) - if hex_match: - if "#" in hex_string: - rgb_color = ImageColor.getcolor(hex_string, "RGB") - elif "0x" in hex_string: - hex_ = hex_string.replace("0x", "#") - rgb_color = ImageColor.getcolor(hex_, "RGB") - else: - hex_ = "#" + hex_string - rgb_color = ImageColor.getcolor(hex_, "RGB") - self.color_embed(rgb_color) - else: - await ctx.send( - embed=Embed( - title="There was an issue converting the hex color code.", - description=ERROR_MSG.format(user_color=hex_string), + for field in fields: + main_embed.add_field( + name=field['name'], + value=field['value'], + inline=False, ) - ) + + await ctx.send(file=file, embed=main_embed) def setup(bot: Bot) -> None: -- cgit v1.2.3 From d9250cf76c6e45df0b2b5c6b2bcab90de7b70520 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Wed, 22 Sep 2021 09:39:37 -0400 Subject: fix: remove `get_color_fields` call in color_embed --- bot/exts/utilities/color.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 67068809..5cdc5083 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -324,9 +324,8 @@ class Color(commands.Cog): file = await self.create_thumbnail_attachment(rgb_color) main_embed.set_thumbnail(url="attachment://color.png") - fields = self.get_color_fields(rgb_color) - for field in fields: + for field in all_colors: main_embed.add_field( name=field['name'], value=field['value'], -- cgit v1.2.3 From 4cf2da8233ed8b10f0bc198fda52c76306ede9ac Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Wed, 22 Sep 2021 09:50:42 -0400 Subject: chore: small code fixes and cleanup --- bot/exts/utilities/color.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 5cdc5083..e0398e02 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -29,7 +29,6 @@ with open(COLOR_JSON_PATH) as f: COLOR_MAPPING = json.load(f) -# define color command class Color(commands.Cog): """User initiated commands to receive color information.""" @@ -45,7 +44,6 @@ class Color(commands.Cog): """ logger.debug(f"{mode = }") logger.debug(f"{user_color = }") - color_name = None if mode.lower() == "hex": self.hex_to_rgb(ctx, user_color) elif mode.lower() == "rgb": @@ -235,11 +233,6 @@ class Color(commands.Cog): l = round(l * 100) # noqa: E741 It's little `L`, Reason: To maintain consistency. return h, s, l - hex_color = _rgb_to_hex(rgb_color) - cmyk_color = _rgb_to_cmyk(rgb_color) - hsv_color = _rgb_to_hsv(rgb_color) - hsl_color = _rgb_to_hsl(rgb_color) - all_fields = [ { "name": "RGB", @@ -247,19 +240,19 @@ class Color(commands.Cog): }, { "name": "HEX", - "value": f"Β» hex {hex_color}" + "value": f"Β» hex {_rgb_to_hex(rgb_color)}" }, { "name": "CMYK", - "value": f"Β» cmyk {cmyk_color}" + "value": f"Β» cmyk {_rgb_to_cmyk(rgb_color)}" }, { "name": "HSV", - "value": f"Β» hsv {hsv_color}" + "value": f"Β» hsv {_rgb_to_hsv(rgb_color)}" }, { "name": "HSL", - "value": f"Β» hsl {hsl_color}" + "value": f"Β» hsl {_rgb_to_hsl(rgb_color)}" }, ] @@ -277,11 +270,11 @@ class Color(commands.Cog): logger.debug(f"{match = }, {certainty = }") hex_match = COLOR_MAPPING[match] logger.debug(f"{hex_match = }") - return match, hex_match except TypeError: match = "No color name match found." hex_match = input_color_name - return match, hex_match + + return match, hex_match @staticmethod def match_color_hex(input_hex_color: str) -> str: @@ -295,10 +288,10 @@ class Color(commands.Cog): logger.debug(f"{match = }, {certainty = }") color_name = [name for name, _ in COLOR_MAPPING.items() if _ == match][0] logger.debug(f"{color_name = }") - return color_name except TypeError: color_name = "No color name match found." - return color_name + + return color_name async def color_embed( self, -- cgit v1.2.3 From 4f52cad537e19b3313b726d099ac223a3fa31c5c Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Thu, 23 Sep 2021 00:22:10 -0400 Subject: chore: create subcommands for sending embed --- bot/exts/utilities/color.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index e0398e02..6cc03c9a 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -45,23 +45,23 @@ class Color(commands.Cog): logger.debug(f"{mode = }") logger.debug(f"{user_color = }") if mode.lower() == "hex": - self.hex_to_rgb(ctx, user_color) + await self.hex_to_rgb(ctx, user_color) elif mode.lower() == "rgb": rgb_color = self.tuple_create(user_color) - self.color_embed(ctx, rgb_color) + await self.color_embed(ctx, rgb_color) elif mode.lower() == "hsv": - self.hsv_to_rgb(ctx, user_color) + await self.hsv_to_rgb(ctx, user_color) elif mode.lower() == "hsl": - self.hsl_to_rgb(ctx, user_color) + await self.hsl_to_rgb(ctx, user_color) elif mode.lower() == "cmyk": - self.cmyk_to_rgb(ctx, user_color) + await self.cmyk_to_rgb(ctx, user_color) elif mode.lower() == "name": color_name, hex_color = self.match_color_name(user_color) if "#" in hex_color: rgb_color = ImageColor.getcolor(hex_color, "RGB") else: rgb_color = ImageColor.getcolor("#" + hex_color, "RGB") - self.color_embed(ctx, rgb_color, color_name) + await self.color_embed(ctx, rgb_color, color_name) else: # mode is either None or an invalid code if mode is None: @@ -112,7 +112,7 @@ class Color(commands.Cog): else: hex_ = "#" + hex_string rgb_color = ImageColor.getcolor(hex_, "RGB") - self.color_embed(rgb_color) + await self.color_embed(ctx, rgb_color) else: await ctx.send( embed=Embed( @@ -121,7 +121,7 @@ class Color(commands.Cog): ) ) - def hsv_to_rgb(self, input_color: tuple[int, int, int]) -> tuple[int, int, int]: + async def hsv_to_rgb(self, ctx: commands.Context, input_color: tuple[int, int, int]) -> tuple[int, int, int]: """Function to convert hsv color to rgb color and send main embed.""" input_color = self.tuple_create(input_color) (h, v, s) = input_color # the function hsv_to_rgb expects v and s to be swapped @@ -133,9 +133,9 @@ class Color(commands.Cog): r = int(r * 255) g = int(g * 255) b = int(b * 255) - self.color_embed((r, g, b)) + await self.color_embed(ctx, (r, g, b)) - def hsl_to_rgb(self, input_color: tuple[int, int, int]) -> tuple[int, int, int]: + async def hsl_to_rgb(self, ctx: commands.Context, input_color: tuple[int, int, int]) -> tuple[int, int, int]: """Function to convert hsl color to rgb color and send main embed.""" input_color = self.tuple_create(input_color) (h, s, l) = input_color @@ -147,9 +147,9 @@ class Color(commands.Cog): r = int(r * 255) g = int(g * 255) b = int(b * 255) - self.color_embed((r, g, b)) + await self.color_embed(ctx, (r, g, b)) - def cmyk_to_rgb(self, input_color: tuple[int, int, int, int]) -> tuple[int, int, int]: + async def cmyk_to_rgb(self, ctx: commands.Context, input_color: tuple[int, int, int, int]) -> tuple[int, int, int]: """Function to convert cmyk color to rgb color and send main embed.""" input_color = self.tuple_create(input_color) c = input_color[0] @@ -159,7 +159,7 @@ class Color(commands.Cog): r = int(255 * (1.0 - c / float(100)) * (1.0 - k / float(100))) g = int(255 * (1.0 - m / float(100)) * (1.0 - k / float(100))) b = int(255 * (1.0 - y / float(100)) * (1.0 - k / float(100))) - self.color_embed((r, g, b)) + await self.color_embed(ctx, (r, g, b)) @staticmethod async def create_thumbnail_attachment(color: tuple[int, int, int]) -> File: -- cgit v1.2.3 From 78cdc0f3e29ecc2571d40a2acb53b98e1b0f0e78 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Thu, 23 Sep 2021 07:48:45 -0400 Subject: chore: make cmyk_to_rgb def multiline --- bot/exts/utilities/color.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 6cc03c9a..9e2af325 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -149,7 +149,11 @@ class Color(commands.Cog): b = int(b * 255) await self.color_embed(ctx, (r, g, b)) - async def cmyk_to_rgb(self, ctx: commands.Context, input_color: tuple[int, int, int, int]) -> tuple[int, int, int]: + async def cmyk_to_rgb( + self, + ctx: commands.Context, + input_color: tuple[int, int, int, int] + ) -> tuple[int, int, int]: """Function to convert cmyk color to rgb color and send main embed.""" input_color = self.tuple_create(input_color) c = input_color[0] -- cgit v1.2.3 From bae934537185c50089d854a22f76596bd4403a39 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Thu, 23 Sep 2021 21:42:24 -0400 Subject: chore: set thumbnail image to 80x80 --- bot/exts/utilities/color.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 6cc03c9a..1652ee19 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -168,7 +168,7 @@ class Color(commands.Cog): Assumes that color is an rgb tuple. """ - thumbnail = Image.new("RGB", (100, 100), color=color) + thumbnail = Image.new("RGB", (80, 80), color=color) bufferedio = BytesIO() thumbnail.save(bufferedio, format="PNG") bufferedio.seek(0) -- cgit v1.2.3 From c893e86b61e5309d4bd6a03259f6e87fa76d4f39 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Fri, 24 Sep 2021 20:40:45 +0700 Subject: Bisect only on love percent thresholds --- bot/exts/holidays/valentines/lovecalculator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/holidays/valentines/lovecalculator.py b/bot/exts/holidays/valentines/lovecalculator.py index 3999db2b..3ef9c3e1 100644 --- a/bot/exts/holidays/valentines/lovecalculator.py +++ b/bot/exts/holidays/valentines/lovecalculator.py @@ -74,7 +74,8 @@ class LoveCalculator(Cog): # We need the -1 due to how bisect returns the point # see the documentation for further detail # https://docs.python.org/3/library/bisect.html#bisect.bisect - index = bisect.bisect(LOVE_DATA, (love_percent,)) - 1 + love_threshold = [threshold for threshold, _ in LOVE_DATA] + index = bisect.bisect(love_threshold, love_percent) - 1 # We already have the nearest "fit" love level # We only need the dict, so we can ditch the first element _, data = LOVE_DATA[index] -- cgit v1.2.3 From 4590c42f8398787902881ef51396142c32366088 Mon Sep 17 00:00:00 2001 From: PythonTryHard <31789326+PythonTryHard@users.noreply.github.com> Date: Fri, 24 Sep 2021 20:47:46 +0700 Subject: Remove trailing whitespace --- bot/exts/holidays/valentines/lovecalculator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/holidays/valentines/lovecalculator.py b/bot/exts/holidays/valentines/lovecalculator.py index 3ef9c3e1..a53014e5 100644 --- a/bot/exts/holidays/valentines/lovecalculator.py +++ b/bot/exts/holidays/valentines/lovecalculator.py @@ -74,7 +74,7 @@ class LoveCalculator(Cog): # We need the -1 due to how bisect returns the point # see the documentation for further detail # https://docs.python.org/3/library/bisect.html#bisect.bisect - love_threshold = [threshold for threshold, _ in LOVE_DATA] + love_threshold = [threshold for threshold, _ in LOVE_DATA] index = bisect.bisect(love_threshold, love_percent) - 1 # We already have the nearest "fit" love level # We only need the dict, so we can ditch the first element -- cgit v1.2.3 From e5111789e553b4c9c1763fc791c875fce57c6fef Mon Sep 17 00:00:00 2001 From: Kronifer <44979306+Kronifer@users.noreply.github.com> Date: Fri, 24 Sep 2021 13:37:57 -0500 Subject: chore: added an open in gitpod badge to the README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index dd8301dc..dcdaf1c4 100755 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![Lint Badge][1]][2] [![Build Badge][3]][4] [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE) +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#/python-discord/sir-lancebot) ![Header](sir-lancebot-logo.png) -- cgit v1.2.3 From 84841f3108c9eb8d731dff5fb17705f1064a5464 Mon Sep 17 00:00:00 2001 From: Kronifer <44979306+Kronifer@users.noreply.github.com> Date: Fri, 24 Sep 2021 13:39:58 -0500 Subject: chore: changed gitpod button --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dcdaf1c4..ed1ff235 100755 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Lint Badge][1]][2] [![Build Badge][3]][4] [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE) -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#/python-discord/sir-lancebot) +[![Open in Gitpod](https://img.shields.io/badge/Gitpod-ready--to--code-908a85?logo=gitpod)](https://gitpod.io/#/python-discord/sir-lancebot) ![Header](sir-lancebot-logo.png) -- cgit v1.2.3 From 4d9ec6c3f8f0212cb45bfa1e7d8b6667dcbf5f4f Mon Sep 17 00:00:00 2001 From: Kronifer <44979306+Kronifer@users.noreply.github.com> Date: Fri, 24 Sep 2021 13:52:06 -0500 Subject: chore: fixed the link in the README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed1ff235..2e2b7aec 100755 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Lint Badge][1]][2] [![Build Badge][3]][4] [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE) -[![Open in Gitpod](https://img.shields.io/badge/Gitpod-ready--to--code-908a85?logo=gitpod)](https://gitpod.io/#/python-discord/sir-lancebot) +[![Open in Gitpod](https://img.shields.io/badge/Gitpod-ready--to--code-908a85?logo=gitpod)](https://gitpod.io/#/github.com/python-discord/sir-lancebot) ![Header](sir-lancebot-logo.png) -- cgit v1.2.3 -- cgit v1.2.3 From 420968c38922f7ebbd763e20fbe916deb3bf5cb6 Mon Sep 17 00:00:00 2001 From: Cam Caswell Date: Sun, 5 Sep 2021 00:53:05 -0400 Subject: Split initial embed in two Board and claimed answers discord.py doesn't let you cleanly edit an embed with an image in it --- bot/exts/fun/duck_game.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/bot/exts/fun/duck_game.py b/bot/exts/fun/duck_game.py index 1ef7513f..eb509e55 100644 --- a/bot/exts/fun/duck_game.py +++ b/bot/exts/fun/duck_game.py @@ -130,6 +130,9 @@ class DuckGame: while len(self.solutions) < minimum_solutions: self.board = random.sample(DECK, size) + self.board_msg = None + self.found_msg = None + @property def board(self) -> list[tuple[int]]: """Accesses board property.""" @@ -191,8 +194,8 @@ class DuckGamesDirector(commands.Cog): game.running = True self.current_games[ctx.channel.id] = game - game.msg_content = "" - game.embed_msg = await self.send_board_embed(ctx, game) + game.board_msg = await self.send_board_embed(ctx, game) + game.found_msg = await self.send_found_embed(ctx) await asyncio.sleep(GAME_DURATION) # Checking for the channel ID in the currently running games is not sufficient. @@ -251,7 +254,7 @@ class DuckGamesDirector(commands.Cog): game.scores[msg.author] += INCORRECT_SOLN async def send_board_embed(self, ctx: commands.Context, game: DuckGame) -> discord.Message: - """Create and send the initial game embed. This will be edited as the game goes on.""" + """Create and send an embed to display the board.""" image = assemble_board_image(game.board, game.rows, game.columns) with BytesIO() as image_stream: image.save(image_stream, format="png") @@ -264,14 +267,21 @@ class DuckGamesDirector(commands.Cog): embed.set_image(url="attachment://board.png") return await ctx.send(embed=embed, file=file) + async def send_found_embed(self, ctx: commands.Context) -> discord.Message: + """Create and send an embed to display claimed answers. This will be edited as the game goes on.""" + # Can't be part of the board embed because of discord.py limitations with editing an embed with an image. + embed = discord.Embed( + title="Flights Found", + color=discord.Color.dark_purple(), + ) + return await ctx.send(embed=embed) + async def display_claimed_answer(self, game: DuckGame, author: discord.Member, answer: tuple[int]) -> None: """Add a claimed answer to the game embed.""" async with game.editing_embed: - # We specifically edit the message contents instead of the embed - # Because we load in the image from the file, editing any portion of the embed - # Does weird things to the image and this works around that weirdness - game.msg_content = f"{game.msg_content}\n{str(answer):12s} - {author.display_name}" - await game.embed_msg.edit(content=game.msg_content) + found_embed, = game.found_msg.embeds + found_embed.description = f"{found_embed.description}\n{str(answer):12s} - {author.display_name}" + await game.found_msg.edit(embed=found_embed) async def end_game(self, channel: discord.TextChannel, game: DuckGame, end_message: str) -> None: """Edit the game embed to reflect the end of the game and mark the game as not running.""" -- cgit v1.2.3 From e7bbd14f95f59593581a8c18da8bc4ef9a0f6208 Mon Sep 17 00:00:00 2001 From: Cam Caswell Date: Sun, 5 Sep 2021 00:56:58 -0400 Subject: Add function for appending to claimed answers embed --- bot/exts/fun/duck_game.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/bot/exts/fun/duck_game.py b/bot/exts/fun/duck_game.py index eb509e55..1fdc05be 100644 --- a/bot/exts/fun/duck_game.py +++ b/bot/exts/fun/duck_game.py @@ -248,7 +248,7 @@ class DuckGamesDirector(commands.Cog): if answer in game.solutions: game.claimed_answers[answer] = msg.author game.scores[msg.author] += CORRECT_SOLN - await self.display_claimed_answer(game, msg.author, answer) + await self.append_to_found_embed(game, f"{str(answer):12s} - {msg.author.display_name}") else: await msg.add_reaction(EMOJI_WRONG) game.scores[msg.author] += INCORRECT_SOLN @@ -276,6 +276,16 @@ class DuckGamesDirector(commands.Cog): ) return await ctx.send(embed=embed) + async def append_to_found_embed(self, game: DuckGame, text: str) -> None: + """Append text to the claimed answers embed.""" + async with game.editing_embed: + found_embed, = game.found_msg.embeds + old_desc = found_embed.description + if old_desc == discord.Embed.Empty: + old_desc = "" + found_embed.description = f"{old_desc.rstrip()}\n{text}" + await game.found_msg.edit(embed=found_embed) + async def display_claimed_answer(self, game: DuckGame, author: discord.Member, answer: tuple[int]) -> None: """Add a claimed answer to the game embed.""" async with game.editing_embed: -- cgit v1.2.3 From 65aec408bc3d50934d543b9124de67720d617bb8 Mon Sep 17 00:00:00 2001 From: Cam Caswell Date: Sun, 5 Sep 2021 01:12:10 -0400 Subject: Bring end_game up to date --- bot/exts/fun/duck_game.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/bot/exts/fun/duck_game.py b/bot/exts/fun/duck_game.py index 1fdc05be..46ea36fe 100644 --- a/bot/exts/fun/duck_game.py +++ b/bot/exts/fun/duck_game.py @@ -311,13 +311,19 @@ class DuckGamesDirector(commands.Cog): scoreboard_embed.description = scoreboard await channel.send(embed=scoreboard_embed) + board_embed, = game.board_msg.embeds + embed_as_dict = board_embed.to_dict() # Cannot set embed color after initialization + embed_as_dict["color"] = discord.Color.red().value + board_embed = discord.Embed.from_dict(embed_as_dict) + await self.edit_embed_with_image(game.board_msg, board_embed) + + found_embed, = game.found_msg.embeds missed = [ans for ans in game.solutions if ans not in game.claimed_answers] if missed: missed_text = "Flights everyone missed:\n" + "\n".join(f"{ans}" for ans in missed) else: missed_text = "All the flights were found!" - - await game.embed_msg.edit(content=f"{missed_text}") + await self.append_to_found_embed(game, f"\n{missed_text}") @start_game.command(name="help") async def show_rules(self, ctx: commands.Context) -> None: -- cgit v1.2.3 From f96ecffb3927acd254158210655524e24db37d3b Mon Sep 17 00:00:00 2001 From: Cam Caswell Date: Sun, 5 Sep 2021 01:13:49 -0400 Subject: Change board embed color Can't update color when the game is over anymore --- bot/exts/fun/duck_game.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/fun/duck_game.py b/bot/exts/fun/duck_game.py index 46ea36fe..a456c86b 100644 --- a/bot/exts/fun/duck_game.py +++ b/bot/exts/fun/duck_game.py @@ -11,7 +11,7 @@ from PIL import Image, ImageDraw, ImageFont from discord.ext import commands from bot.bot import Bot -from bot.constants import Colours, MODERATION_ROLES +from bot.constants import MODERATION_ROLES from bot.utils.decorators import with_role DECK = list(product(*[(0, 1, 2)]*4)) @@ -262,7 +262,7 @@ class DuckGamesDirector(commands.Cog): file = discord.File(fp=image_stream, filename="board.png") embed = discord.Embed( title="Duck Duck Duck Goose!", - color=Colours.bright_green, + color=discord.Color.dark_purple(), ) embed.set_image(url="attachment://board.png") return await ctx.send(embed=embed, file=file) -- cgit v1.2.3 -- cgit v1.2.3 From 00c323222be2aeef010b15607f801b3066b5b1ed Mon Sep 17 00:00:00 2001 From: Cam Caswell Date: Fri, 24 Sep 2021 22:59:24 -0400 Subject: Remove display_claimed_answer Replaced with append_to_found_embed which is more general --- bot/exts/fun/duck_game.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/bot/exts/fun/duck_game.py b/bot/exts/fun/duck_game.py index a456c86b..8b5338f4 100644 --- a/bot/exts/fun/duck_game.py +++ b/bot/exts/fun/duck_game.py @@ -286,13 +286,6 @@ class DuckGamesDirector(commands.Cog): found_embed.description = f"{old_desc.rstrip()}\n{text}" await game.found_msg.edit(embed=found_embed) - async def display_claimed_answer(self, game: DuckGame, author: discord.Member, answer: tuple[int]) -> None: - """Add a claimed answer to the game embed.""" - async with game.editing_embed: - found_embed, = game.found_msg.embeds - found_embed.description = f"{found_embed.description}\n{str(answer):12s} - {author.display_name}" - await game.found_msg.edit(embed=found_embed) - async def end_game(self, channel: discord.TextChannel, game: DuckGame, end_message: str) -> None: """Edit the game embed to reflect the end of the game and mark the game as not running.""" game.running = False -- cgit v1.2.3 From baa4d252b72de1e25bceb15721b475ee9724d47b Mon Sep 17 00:00:00 2001 From: Cam Caswell Date: Fri, 24 Sep 2021 23:00:40 -0400 Subject: Don't change board embed color Can't edit that embed --- bot/exts/fun/duck_game.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/bot/exts/fun/duck_game.py b/bot/exts/fun/duck_game.py index 8b5338f4..f97c712d 100644 --- a/bot/exts/fun/duck_game.py +++ b/bot/exts/fun/duck_game.py @@ -304,13 +304,6 @@ class DuckGamesDirector(commands.Cog): scoreboard_embed.description = scoreboard await channel.send(embed=scoreboard_embed) - board_embed, = game.board_msg.embeds - embed_as_dict = board_embed.to_dict() # Cannot set embed color after initialization - embed_as_dict["color"] = discord.Color.red().value - board_embed = discord.Embed.from_dict(embed_as_dict) - await self.edit_embed_with_image(game.board_msg, board_embed) - - found_embed, = game.found_msg.embeds missed = [ans for ans in game.solutions if ans not in game.claimed_answers] if missed: missed_text = "Flights everyone missed:\n" + "\n".join(f"{ans}" for ans in missed) -- cgit v1.2.3 From debe4a185f1429f5dd139929c7506c04abd9fa34 Mon Sep 17 00:00:00 2001 From: Cam Caswell Date: Fri, 24 Sep 2021 23:00:58 -0400 Subject: Change docstring to be more help with the help command --- bot/exts/fun/duck_game.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/fun/duck_game.py b/bot/exts/fun/duck_game.py index f97c712d..95e12ced 100644 --- a/bot/exts/fun/duck_game.py +++ b/bot/exts/fun/duck_game.py @@ -184,7 +184,7 @@ class DuckGamesDirector(commands.Cog): ) @commands.cooldown(rate=1, per=2, type=commands.BucketType.channel) async def start_game(self, ctx: commands.Context) -> None: - """Generate a board, send the game embed, and end the game after a time limit.""" + """Start a new Duck Duck Duck Goose game.""" if ctx.channel.id in self.current_games: await ctx.send("There's already a game running!") return -- cgit v1.2.3 From b0e9ffb94f05e6ef7619b7440e00363e10928932 Mon Sep 17 00:00:00 2001 From: Objectivitix <79152594+Objectivitix@users.noreply.github.com> Date: Sun, 26 Sep 2021 18:24:30 -0300 Subject: Allow everyone to use the `.bm` command everywhere (#885) * Allow everyone to use the bm command * Add everyone role in Roles constants * Use envvars and re-order Roles section to be more organized * Fix trailing whitespace We might need to squash merge, four commits for a single small fix is too much --- bot/constants.py | 3 ++- bot/exts/utilities/bookmark.py | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 2313bfdb..6e45632f 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -280,11 +280,12 @@ if Client.month_override is not None: class Roles(NamedTuple): + owner = 267627879762755584 admin = int(environ.get("BOT_ADMIN_ROLE_ID", 267628507062992896)) moderator = 267629731250176001 - owner = 267627879762755584 helpers = int(environ.get("ROLE_HELPERS", 267630620367257601)) core_developers = 587606783669829632 + everyone = int(environ.get("BOT_GUILD", 267624335836053506)) class Tokens(NamedTuple): diff --git a/bot/exts/utilities/bookmark.py b/bot/exts/utilities/bookmark.py index a91ef1c0..39d65168 100644 --- a/bot/exts/utilities/bookmark.py +++ b/bot/exts/utilities/bookmark.py @@ -7,7 +7,7 @@ import discord from discord.ext import commands from bot.bot import Bot -from bot.constants import Categories, Colours, ERROR_REPLIES, Icons, WHITELISTED_CHANNELS +from bot.constants import Colours, ERROR_REPLIES, Icons, Roles from bot.utils.converters import WrappedMessageConverter from bot.utils.decorators import whitelist_override @@ -16,7 +16,6 @@ log = logging.getLogger(__name__) # Number of seconds to wait for other users to bookmark the same message TIMEOUT = 120 BOOKMARK_EMOJI = "πŸ“Œ" -WHITELISTED_CATEGORIES = (Categories.help_in_use,) class Bookmark(commands.Cog): @@ -87,8 +86,8 @@ class Bookmark(commands.Cog): await message.add_reaction(BOOKMARK_EMOJI) return message - @whitelist_override(channels=WHITELISTED_CHANNELS, categories=WHITELISTED_CATEGORIES) @commands.command(name="bookmark", aliases=("bm", "pin")) + @whitelist_override(roles=(Roles.everyone,)) async def bookmark( self, ctx: commands.Context, -- cgit v1.2.3 From 443cd600e4060b4b0f58382e7da07ca245cfca00 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Mon, 27 Sep 2021 09:49:42 -0400 Subject: chore: remove doubled new line in ERROR_MSG --- bot/exts/utilities/color.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 9e2af325..7c7f4bba 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -17,11 +17,11 @@ logger = logging.getLogger(__name__) ERROR_MSG = """The color code {user_color} is not a possible color combination. -\nThe range of possible values are: -\nRGB & HSV: 0-255 -\nCMYK: 0-100% -\nHSL: 0-360 degrees -\nHex: #000000-#FFFFFF +The range of possible values are: +RGB & HSV: 0-255 +CMYK: 0-100% +HSL: 0-360 degrees +Hex: #000000-#FFFFFF """ COLOR_JSON_PATH = "bot/resources/utilities/ryanzec_colours.json" -- cgit v1.2.3 From d209638ff2c4f78cdcda49972886d4a7da69165f Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Mon, 27 Sep 2021 09:56:33 -0400 Subject: chore: remove single-use constant for json path --- bot/exts/utilities/color.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 7c7f4bba..ac7b5fc6 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -24,8 +24,7 @@ HSL: 0-360 degrees Hex: #000000-#FFFFFF """ -COLOR_JSON_PATH = "bot/resources/utilities/ryanzec_colours.json" -with open(COLOR_JSON_PATH) as f: +with open("bot/resources/utilities/ryanzec_colours.json") as f: COLOR_MAPPING = json.load(f) -- cgit v1.2.3 From 57554db4fc14f5869168622e1896a80b851e7579 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Mon, 27 Sep 2021 10:46:08 -0400 Subject: chore: remove single-use constant for json path --- bot/exts/utilities/color.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 57510488..6aa0c3cd 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -65,7 +65,7 @@ class Color(commands.Cog): # mode is either None or an invalid code if mode is None: no_mode_embed = Embed( - title="No 'mode' was passed, please define a color code.", + title="No mode was passed, please define a color code.", description="Possible modes are: Name, Hex, RGB, HSV, HSL and CMYK.", color=Colours.soft_red, ) -- cgit v1.2.3 From beb42c28911d8bbc4de5b0926a77619b4454f402 Mon Sep 17 00:00:00 2001 From: Kronifer <44979306+Kronifer@users.noreply.github.com> Date: Fri, 1 Oct 2021 04:32:58 -0500 Subject: `.quack` (#849) * feat: Added quack command * added log.error call for request fails * spacing change Co-authored-by: Bluenix * another spacing change Co-authored-by: Bluenix * Moved description to footer Co-authored-by: Bluenix * whitespace fix * chore: Removed the link from the footer and set it as the url param * chore: moved footer to description Co-authored-by: Bluenix Co-authored-by: Xithrius <15021300+Xithrius@users.noreply.github.com> --- bot/exts/fun/quack.py | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 bot/exts/fun/quack.py diff --git a/bot/exts/fun/quack.py b/bot/exts/fun/quack.py new file mode 100644 index 00000000..0c228aed --- /dev/null +++ b/bot/exts/fun/quack.py @@ -0,0 +1,75 @@ +import logging +import random +from typing import Literal, Optional + +import discord +from discord.ext import commands + +from bot.bot import Bot +from bot.constants import Colours, NEGATIVE_REPLIES + +API_URL = 'https://quackstack.pythondiscord.com' + +log = logging.getLogger(__name__) + + +class Quackstack(commands.Cog): + """Cog used for wrapping Quackstack.""" + + def __init__(self, bot: Bot): + self.bot = bot + + @commands.command() + async def quack( + self, + ctx: commands.Context, + ducktype: Literal["duck", "manduck"] = "duck", + *, + seed: Optional[str] = None + ) -> None: + """ + Use the Quackstack API to generate a random duck. + + If a seed is provided, a duck is generated based on the given seed. + Either "duck" or "manduck" can be provided to change the duck type generated. + """ + ducktype = ducktype.lower() + quackstack_url = f"{API_URL}/{ducktype}" + params = {} + if seed is not None: + try: + seed = int(seed) + except ValueError: + # We just need to turn the string into an integer any way possible + seed = int.from_bytes(seed.encode(), "big") + params["seed"] = seed + + async with self.bot.http_session.get(quackstack_url, params=params) as response: + error_embed = discord.Embed( + title=random.choice(NEGATIVE_REPLIES), + description="The request failed. Please try again later.", + color=Colours.soft_red, + ) + if response.status != 200: + log.error(f"Response to Quackstack returned code {response.status}") + await ctx.send(embed=error_embed) + return + + data = await response.json() + file = data["file"] + + embed = discord.Embed( + title=f"Quack! Here's a {ducktype} for you.", + description=f"A {ducktype} from Quackstack.", + color=Colours.grass_green, + url=f"{API_URL}/docs" + ) + + embed.set_image(url=API_URL + file) + + await ctx.send(embed=embed) + + +def setup(bot: Bot) -> None: + """Loads the Quack cog.""" + bot.add_cog(Quackstack(bot)) -- cgit v1.2.3 From 0246a66a668a5928ec57a40629c1ef740c8fd651 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Fri, 1 Oct 2021 19:17:14 +0200 Subject: Hackto issue finder: make d.py timestamp naive --- bot/exts/events/hacktoberfest/hacktober-issue-finder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/events/hacktoberfest/hacktober-issue-finder.py b/bot/exts/events/hacktoberfest/hacktober-issue-finder.py index 0cbb6df4..088e7e43 100644 --- a/bot/exts/events/hacktoberfest/hacktober-issue-finder.py +++ b/bot/exts/events/hacktoberfest/hacktober-issue-finder.py @@ -88,10 +88,10 @@ class HacktoberIssues(commands.Cog): if option == "beginner": self.cache_beginner = data - self.cache_timer_beginner = ctx.message.created_at + self.cache_timer_beginner = ctx.message.created_at.replace(tzinfo=None) else: self.cache_normal = data - self.cache_timer_normal = ctx.message.created_at + self.cache_timer_normal = ctx.message.created_at.replace(tzinfo=None) return data -- cgit v1.2.3 From 97ccd35d27946abb39b86befdeb36364fea8b5ce Mon Sep 17 00:00:00 2001 From: camcaswell <38672443+camcaswell@users.noreply.github.com> Date: Sat, 2 Oct 2021 00:15:44 -0400 Subject: Make setting the old embed description cleaner --- bot/exts/fun/duck_game.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bot/exts/fun/duck_game.py b/bot/exts/fun/duck_game.py index 95e12ced..10b03a49 100644 --- a/bot/exts/fun/duck_game.py +++ b/bot/exts/fun/duck_game.py @@ -280,9 +280,7 @@ class DuckGamesDirector(commands.Cog): """Append text to the claimed answers embed.""" async with game.editing_embed: found_embed, = game.found_msg.embeds - old_desc = found_embed.description - if old_desc == discord.Embed.Empty: - old_desc = "" + old_desc = found_embed.description or "" found_embed.description = f"{old_desc.rstrip()}\n{text}" await game.found_msg.edit(embed=found_embed) -- cgit v1.2.3 From 26943e5b10fedf9d90d774c921ea8e4c4ceb0b66 Mon Sep 17 00:00:00 2001 From: TizzySaurus <47674925+TizzySaurus@users.noreply.github.com> Date: Sat, 2 Oct 2021 12:15:13 +0100 Subject: Change pascal's triangle image --- bot/resources/fun/trivia_quiz.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/fun/trivia_quiz.json b/bot/resources/fun/trivia_quiz.json index 0b3e6802..99aa5f42 100644 --- a/bot/resources/fun/trivia_quiz.json +++ b/bot/resources/fun/trivia_quiz.json @@ -440,7 +440,7 @@ { "id": 229, "question": "What is this triangle called?", - "img_url": "https://cdn.askpython.com/wp-content/uploads/2020/07/Pascals-triangle.png", + "img_url": "https://wikimedia.org/api/rest_v1/media/math/render/png/23050fcb53d6083d9e42043bebf2863fa9746043", "answer": ["Pascal's triangle", "Pascal"] }, { -- cgit v1.2.3 From 9f07c20f8f2e78051ab9c0d31d45755921007156 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 3 Oct 2021 14:07:25 +0100 Subject: Give the bookmark command a better error message --- bot/exts/utilities/bookmark.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bot/exts/utilities/bookmark.py b/bot/exts/utilities/bookmark.py index 39d65168..a11c366b 100644 --- a/bot/exts/utilities/bookmark.py +++ b/bot/exts/utilities/bookmark.py @@ -98,7 +98,13 @@ class Bookmark(commands.Cog): """Send the author a link to `target_message` via DMs.""" if not target_message: if not ctx.message.reference: - raise commands.UserInputError("You must either provide a valid message to bookmark, or reply to one.") + raise commands.UserInputError( + "You must either provide a valid message to bookmark, or reply to one." + "\n\nThe lookup strategy for a message is as follows (in order):" + "\n1. Lookup by '{channel ID}-{message ID}' (retrieved by shift-clicking on 'Copy ID')" + "\n2. Lookup by message ID (the message **must** have been sent after the bot last started)" + "\n3. Lookup by message URL" + ) target_message = ctx.message.reference.resolved # Prevent users from bookmarking a message in a channel they don't have access to -- cgit v1.2.3 From 42d3e10317a327157ea8a5c04418bd6e26438793 Mon Sep 17 00:00:00 2001 From: Gustav Odinger <65498475+gustavwilliam@users.noreply.github.com> Date: Sun, 3 Oct 2021 15:30:07 +0200 Subject: Ignore bot messages for spooky react Previously only ignored its own messages, but now ignores messages from all bots. --- bot/exts/holidays/halloween/spookyreact.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/exts/holidays/halloween/spookyreact.py b/bot/exts/holidays/halloween/spookyreact.py index 25e783f4..e228b91d 100644 --- a/bot/exts/holidays/halloween/spookyreact.py +++ b/bot/exts/holidays/halloween/spookyreact.py @@ -47,12 +47,12 @@ class SpookyReact(Cog): Short-circuit helper check. Return True if: - * author is the bot + * author is a bot * prefix is not None """ - # Check for self reaction - if message.author == self.bot.user: - log.debug(f"Ignoring reactions on self message. Message ID: {message.id}") + # Check if message author is a bot + if message.author.bot: + log.debug(f"Ignoring reactions on bot message. Message ID: {message.id}") return True # Check for command invocation -- cgit v1.2.3 From 6fbd7883fee19e62898eacea08b4d7b7f7fc74db Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 4 Oct 2021 20:26:59 +0100 Subject: Monkey patch http.send_typing to catch 403s Sometimes discord turns off typing events by throwing 403's, so we should catch those --- bot/__init__.py | 26 +++++------------ bot/command.py | 18 ------------ bot/group.py | 18 ------------ bot/monkey_patches.py | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 55 deletions(-) delete mode 100644 bot/command.py delete mode 100644 bot/group.py create mode 100644 bot/monkey_patches.py diff --git a/bot/__init__.py b/bot/__init__.py index c6a48105..db576cb2 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -15,28 +15,15 @@ from pathlib import Path import arrow from discord.ext import commands -from bot.command import Command +from bot import monkey_patches from bot.constants import Client -from bot.group import Group # Configure the "TRACE" logging level (e.g. "log.trace(message)") logging.TRACE = 5 logging.addLevelName(logging.TRACE, "TRACE") - -def monkeypatch_trace(self: logging.Logger, msg: str, *args, **kwargs) -> None: - """ - Log 'msg % args' with severity 'TRACE'. - - To pass exception information, use the keyword argument exc_info with a true value, e.g. - logger.trace("Houston, we have an %s", "interesting problem", exc_info=1) - """ - if self.isEnabledFor(logging.TRACE): - self._log(logging.TRACE, msg, args, **kwargs) - - -logging.Logger.trace = monkeypatch_trace +logging.Logger.trace = monkey_patches.trace_log # Set timestamp of when execution started (approximately) start_time = arrow.utcnow() @@ -84,11 +71,12 @@ logging.getLogger().info("Logging initialization complete") if os.name == "nt": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) +monkey_patches.patch_typing() # Monkey-patch discord.py decorators to use the both the Command and Group subclasses which supports root aliases. # Must be patched before any cogs are added. -commands.command = partial(commands.command, cls=Command) -commands.GroupMixin.command = partialmethod(commands.GroupMixin.command, cls=Command) +commands.command = partial(commands.command, cls=monkey_patches.Command) +commands.GroupMixin.command = partialmethod(commands.GroupMixin.command, cls=monkey_patches.Command) -commands.group = partial(commands.group, cls=Group) -commands.GroupMixin.group = partialmethod(commands.GroupMixin.group, cls=Group) +commands.group = partial(commands.group, cls=monkey_patches.Group) +commands.GroupMixin.group = partialmethod(commands.GroupMixin.group, cls=monkey_patches.Group) diff --git a/bot/command.py b/bot/command.py deleted file mode 100644 index 0fb900f7..00000000 --- a/bot/command.py +++ /dev/null @@ -1,18 +0,0 @@ -from discord.ext import commands - - -class Command(commands.Command): - """ - A `discord.ext.commands.Command` subclass which supports root aliases. - - A `root_aliases` keyword argument is added, which is a sequence of alias names that will act as - top-level commands rather than being aliases of the command's group. It's stored as an attribute - also named `root_aliases`. - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.root_aliases = kwargs.get("root_aliases", []) - - if not isinstance(self.root_aliases, (list, tuple)): - raise TypeError("Root aliases of a command must be a list or a tuple of strings.") diff --git a/bot/group.py b/bot/group.py deleted file mode 100644 index a7bc59b7..00000000 --- a/bot/group.py +++ /dev/null @@ -1,18 +0,0 @@ -from discord.ext import commands - - -class Group(commands.Group): - """ - A `discord.ext.commands.Group` subclass which supports root aliases. - - A `root_aliases` keyword argument is added, which is a sequence of alias names that will act as - top-level groups rather than being aliases of the command's group. It's stored as an attribute - also named `root_aliases`. - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.root_aliases = kwargs.get("root_aliases", []) - - if not isinstance(self.root_aliases, (list, tuple)): - raise TypeError("Root aliases of a group must be a list or a tuple of strings.") diff --git a/bot/monkey_patches.py b/bot/monkey_patches.py new file mode 100644 index 00000000..fe81f2e3 --- /dev/null +++ b/bot/monkey_patches.py @@ -0,0 +1,78 @@ +import logging +from datetime import datetime, timedelta + +from discord import Forbidden, http +from discord.ext import commands + +log = logging.getLogger(__name__) + + +def trace_log(self: logging.Logger, msg: str, *args, **kwargs) -> None: + """ + Log 'msg % args' with severity 'TRACE'. + + To pass exception information, use the keyword argument exc_info with a true value, e.g. + logger.trace("Houston, we have an %s", "interesting problem", exc_info=1) + """ + if self.isEnabledFor(logging.TRACE): + self._log(logging.TRACE, msg, args, **kwargs) + + +class Command(commands.Command): + """ + A `discord.ext.commands.Command` subclass which supports root aliases. + + A `root_aliases` keyword argument is added, which is a sequence of alias names that will act as + top-level commands rather than being aliases of the command's group. It's stored as an attribute + also named `root_aliases`. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.root_aliases = kwargs.get("root_aliases", []) + + if not isinstance(self.root_aliases, (list, tuple)): + raise TypeError("Root aliases of a command must be a list or a tuple of strings.") + + +class Group(commands.Group): + """ + A `discord.ext.commands.Group` subclass which supports root aliases. + + A `root_aliases` keyword argument is added, which is a sequence of alias names that will act as + top-level groups rather than being aliases of the command's group. It's stored as an attribute + also named `root_aliases`. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.root_aliases = kwargs.get("root_aliases", []) + + if not isinstance(self.root_aliases, (list, tuple)): + raise TypeError("Root aliases of a group must be a list or a tuple of strings.") + + +def patch_typing() -> None: + """ + Sometimes discord turns off typing events by throwing 403's. + + Handle those issues by patching the trigger_typing method so it ignores 403's in general. + """ + log.debug("Patching send_typing, which should fix things breaking when discord disables typing events. Stay safe!") + + original = http.HTTPClient.send_typing + last_403 = None + + async def honeybadger_type(self, channel_id: int) -> None: # noqa: ANN001 + nonlocal last_403 + if last_403 and (datetime.utcnow() - last_403) < timedelta(minutes=5): + log.warning("Not sending typing event, we got a 403 less than 5 minutes ago.") + return + try: + await original(self, channel_id) + except Forbidden: + last_403 = datetime.utcnow() + log.warning("Got a 403 from typing event!") + pass + + http.HTTPClient.send_typing = honeybadger_type -- cgit v1.2.3 From c2650a9e9c2e1ed9fbfcf914c2fefec5fd9f7479 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Fri, 3 Sep 2021 11:18:15 -0400 Subject: Start from upstream main branch --- bot/exts/fun/color.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 bot/exts/fun/color.py diff --git a/bot/exts/fun/color.py b/bot/exts/fun/color.py new file mode 100644 index 00000000..5d86f002 --- /dev/null +++ b/bot/exts/fun/color.py @@ -0,0 +1 @@ +# initial creation -- cgit v1.2.3 From 32b06208a2212f8d596591f50fc8759049fc78e1 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Fri, 3 Sep 2021 14:35:26 -0400 Subject: Create draft body of file This is a large empty file with lots of comments. My general proposal is outlined in the code stumps. Details will need to be hashed out and decided on with CyberCitizen01. In particular: - How to use URLs that has list of color names? Read those into a dictionary? - How to handle the command call with options? `discord-flags`, parsing, function call like: .colour cmyk(49, 50, 0, 22) .colour hsl(241, 47, 58) .colour rgb 101 99 199 - How to implement fuzzy matching with rapidfuzz based on the color names from those URLs? - How to generate colors in other formats? Is this all possible in pillow? - How to generate photo to use in the embed? Do we temporarily create a file in a cache, send it in embed, then delete? This will be a fun project, and my first collab! Co-authored-by: Mohammad Rafivulla <77384412+CyberCitizen01@users.noreply.github.com> --- bot/exts/fun/color.py | 113 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) diff --git a/bot/exts/fun/color.py b/bot/exts/fun/color.py index 5d86f002..32a25b0d 100644 --- a/bot/exts/fun/color.py +++ b/bot/exts/fun/color.py @@ -1 +1,112 @@ -# initial creation +# imports +import logging + +import pillow +from discord import Embed +# ! need to install discord-flags and add to poetry.lock file +from discord.ext import commands, flags +from rapidfuzz import process + +from bot.bot import Bot +from bot.constants import Colours + +logger = logging.getLogger(__name__) + +# constants if needed +# TODO Will the color conversions be done only from pillow or will an API / URL be needed? +# Color URLs +COLOR_URL_XKCD = "https://xkcd.com/color/rgb/" +COLOR_URL_NAME_THAT_COLOR = "https://github.com/ryanzec/name-that-color/blob/master/lib/ntc.js#L116-L1681" + + +COLOR_ERROR = Embed( + title="Input color is not possible", + description="The color code {user_color} is not a possible color combination." + "\nThe range of possible values are: " + "\nRGB & HSV: 0-255" + "\nCMYK: 0-100%" + "\nHSL: 0-360 degrees" + "\nHex: #000000-#FFFFFF" +) +COLOR_EMBED = Embed( + title="{color_name}", + description="RGB" + "\n{RGB}" + "\nHSV" + "\n{HSV}" + "\nCMYK" + "\n{CMYK}" + "\nHSL" + "\n{HSL}" + "\nHex" + "\n{Hex}" +) + + +# define color command +class Color(commands.cog): + """User initiated command to receive color information.""" + + def __init__(self, bot: Bot): + self.bot = bot + + # ? possible to use discord-flags to allow user to decide on color + # https://pypi.org/project/discord-flags/ + # @flags.add_flag("--rgb", type=str) + # @flags.add_flag("--hsv", type=str) + # @flags.add_flag("--cmyk", type=str) + # @flags.add_flag("--hsl", type=str) + # @flags.add_flag("--hex", type=str) + # @flags.add_flag("--name", type=str) + # @flags.command() + @commands.command(aliases=["color", "colour"]) + @commands.cooldown(1, 10, commands.cooldowns.BucketType.user) + async def color(self, ctx: commands.Context, *, user_color: str) -> None: + """Send information on input color code or color name.""" + # need to check if user_color is RGB, HSV, CMYK, HSL, Hex or color name + # should we assume the color is RGB if not defined? + # should discord tags be used? + # need to review discord.py V2.0 + + # TODO code to check if color code is possible + await ctx.send(embed=COLOR_ERROR.format(color=user_color)) + # await ctx.send(embed=COLOR_EMBED.format( + # RGB=color_dict["RGB"], + # HSV=color_dict["HSV"], + # HSL=color_dict["HSL"], + # CMYK=color_dict["CMYK"], + # HSL=color_dict["HSL"], + # Hex=color_dict["Hex"], + # color_name=color_dict["color_name"] + # ) + # ) + + # TODO pass for now + pass + + # if user_color in color_lists: + # # TODO fuzzy match for color + # pass + + async def color_converter(self, color: str, code_type: str) -> dict: + """Generate alternative color codes for use in the embed.""" + # TODO add code to take color and code type and return other types + # color_dict = { + # "RGB": color_RGB, + # "HSV": color_HSV, + # "HSL": color_HSL, + # "CMYK": color_CMYK, + # "HSL": color_HSL, + # "Hex": color_Hex, + # "color_name": color_name, + # } + pass + + async def photo_generator(self, color: str) -> None: + """Generate photo to use in embed.""" + # TODO need to find a way to store photo in cache to add to embed, then remove + + +def setup(bot: Bot) -> None: + """Load the Color Cog.""" + bot.add_cog(Color(bot)) -- cgit v1.2.3 From fbeb74bf9452840a9db8d3013d1f6dd2e350630f Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Fri, 3 Sep 2021 16:15:34 -0400 Subject: Add colorsys import, verbage for using JSON Co-authored-by: Mohammad Rafivulla <77384412+CyberCitizen01@users.noreply.github.com> --- bot/exts/fun/color.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bot/exts/fun/color.py b/bot/exts/fun/color.py index 32a25b0d..a00a956b 100644 --- a/bot/exts/fun/color.py +++ b/bot/exts/fun/color.py @@ -1,6 +1,7 @@ # imports import logging +import colorsys import pillow from discord import Embed # ! need to install discord-flags and add to poetry.lock file @@ -13,8 +14,8 @@ from bot.constants import Colours logger = logging.getLogger(__name__) # constants if needed -# TODO Will the color conversions be done only from pillow or will an API / URL be needed? -# Color URLs +# Color URLs - will be replaced by JSON file? +COLOR_JSON_PATH = ".bot//exts//resources//evergreen//" COLOR_URL_XKCD = "https://xkcd.com/color/rgb/" COLOR_URL_NAME_THAT_COLOR = "https://github.com/ryanzec/name-that-color/blob/master/lib/ntc.js#L116-L1681" @@ -78,7 +79,7 @@ class Color(commands.cog): # HSL=color_dict["HSL"], # Hex=color_dict["Hex"], # color_name=color_dict["color_name"] - # ) + # ).set_image() # url for image? # ) # TODO pass for now -- cgit v1.2.3 From 087d2f279e052871dcd4bb322953b01bf5b965cc Mon Sep 17 00:00:00 2001 From: CyberCitizen01 Date: Sat, 4 Sep 2021 01:47:58 +0530 Subject: Added ryanzec_colours.json constructed from ryanzec/name-that-color Original source: https://github.com/ryanzec/name-that-color/blob/master/lib/ntc.js#L116-L1681 --- bot/resources/fun/ryanzec_colours.json | 1568 ++++++++++++++++++++++++++++++++ 1 file changed, 1568 insertions(+) create mode 100644 bot/resources/fun/ryanzec_colours.json diff --git a/bot/resources/fun/ryanzec_colours.json b/bot/resources/fun/ryanzec_colours.json new file mode 100644 index 00000000..63d9be44 --- /dev/null +++ b/bot/resources/fun/ryanzec_colours.json @@ -0,0 +1,1568 @@ +{ + "Abbey": "4C4F56", + "Acadia": "1B1404", + "Acapulco": "7CB0A1", + "Aero Blue": "C9FFE5", + "Affair": "714693", + "Akaroa": "D4C4A8", + "Alabaster": "FAFAFA", + "Albescent White": "F5E9D3", + "Algae Green": "93DFB8", + "Alice Blue": "F0F8FF", + "Alizarin Crimson": "E32636", + "Allports": "0076A3", + "Almond": "EED9C4", + "Almond Frost": "907B71", + "Alpine": "AF8F2C", + "Alto": "DBDBDB", + "Aluminium": "A9ACB6", + "Amaranth": "E52B50", + "Amazon": "3B7A57", + "Amber": "FFBF00", + "Americano": "87756E", + "Amethyst": "9966CC", + "Amethyst Smoke": "A397B4", + "Amour": "F9EAF3", + "Amulet": "7B9F80", + "Anakiwa": "9DE5FF", + "Antique Brass": "C88A65", + "Antique Bronze": "704A07", + "Anzac": "E0B646", + "Apache": "DFBE6F", + "Apple": "4FA83D", + "Apple Blossom": "AF4D43", + "Apple Green": "E2F3EC", + "Apricot": "EB9373", + "Apricot Peach": "FBCEB1", + "Apricot White": "FFFEEC", + "Aqua Deep": "014B43", + "Aqua Forest": "5FA777", + "Aqua Haze": "EDF5F5", + "Aqua Island": "A1DAD7", + "Aqua Spring": "EAF9F5", + "Aqua Squeeze": "E8F5F2", + "Aquamarine": "7FFFD4", + "Aquamarine Blue": "71D9E2", + "Arapawa": "110C6C", + "Armadillo": "433E37", + "Arrowtown": "948771", + "Ash": "C6C3B5", + "Asparagus": "7BA05B", + "Asphalt": "130A06", + "Astra": "FAEAB9", + "Astral": "327DA0", + "Astronaut": "283A77", + "Astronaut Blue": "013E62", + "Athens Gray": "EEF0F3", + "Aths Special": "ECEBCE", + "Atlantis": "97CD2D", + "Atoll": "0A6F75", + "Atomic Tangerine": "FF9966", + "Au Chico": "97605D", + "Aubergine": "3B0910", + "Australian Mint": "F5FFBE", + "Avocado": "888D65", + "Axolotl": "4E6649", + "Azalea": "F7C8DA", + "Aztec": "0D1C19", + "Azure": "315BA1", + "Azure Radiance": "007FFF", + "Baby Blue": "E0FFFF", + "Bahama Blue": "026395", + "Bahia": "A5CB0C", + "Baja White": "FFF8D1", + "Bali Hai": "859FAF", + "Baltic Sea": "2A2630", + "Bamboo": "DA6304", + "Banana Mania": "FBE7B2", + "Bandicoot": "858470", + "Barberry": "DED717", + "Barley Corn": "A68B5B", + "Barley White": "FFF4CE", + "Barossa": "44012D", + "Bastille": "292130", + "Battleship Gray": "828F72", + "Bay Leaf": "7DA98D", + "Bay of Many": "273A81", + "Bazaar": "98777B", + "Bean ": "3D0C02", + "Beauty Bush": "EEC1BE", + "Beaver": "926F5B", + "Beeswax": "FEF2C7", + "Beige": "F5F5DC", + "Bermuda": "7DD8C6", + "Bermuda Gray": "6B8BA2", + "Beryl Green": "DEE5C0", + "Bianca": "FCFBF3", + "Big Stone": "162A40", + "Bilbao": "327C14", + "Biloba Flower": "B2A1EA", + "Birch": "373021", + "Bird Flower": "D4CD16", + "Biscay": "1B3162", + "Bismark": "497183", + "Bison Hide": "C1B7A4", + "Bistre": "3D2B1F", + "Bitter": "868974", + "Bitter Lemon": "CAE00D", + "Bittersweet": "FE6F5E", + "Bizarre": "EEDEDA", + "Black": "000000", + "Black Bean": "081910", + "Black Forest": "0B1304", + "Black Haze": "F6F7F7", + "Black Marlin": "3E2C1C", + "Black Olive": "242E16", + "Black Pearl": "041322", + "Black Rock": "0D0332", + "Black Rose": "67032D", + "Black Russian": "0A001C", + "Black Squeeze": "F2FAFA", + "Black White": "FFFEF6", + "Blackberry": "4D0135", + "Blackcurrant": "32293A", + "Blaze Orange": "FF6600", + "Bleach White": "FEF3D8", + "Bleached Cedar": "2C2133", + "Blizzard Blue": "A3E3ED", + "Blossom": "DCB4BC", + "Blue": "0000FF", + "Blue Bayoux": "496679", + "Blue Bell": "9999CC", + "Blue Chalk": "F1E9FF", + "Blue Charcoal": "010D1A", + "Blue Chill": "0C8990", + "Blue Diamond": "380474", + "Blue Dianne": "204852", + "Blue Gem": "2C0E8C", + "Blue Haze": "BFBED8", + "Blue Lagoon": "017987", + "Blue Marguerite": "7666C6", + "Blue Ribbon": "0066FF", + "Blue Romance": "D2F6DE", + "Blue Smoke": "748881", + "Blue Stone": "016162", + "Blue Violet": "6456B7", + "Blue Whale": "042E4C", + "Blue Zodiac": "13264D", + "Blumine": "18587A", + "Blush": "B44668", + "Blush Pink": "FF6FFF", + "Bombay": "AFB1B8", + "Bon Jour": "E5E0E1", + "Bondi Blue": "0095B6", + "Bone": "E4D1C0", + "Bordeaux": "5C0120", + "Bossanova": "4E2A5A", + "Boston Blue": "3B91B4", + "Botticelli": "C7DDE5", + "Bottle Green": "093624", + "Boulder": "7A7A7A", + "Bouquet": "AE809E", + "Bourbon": "BA6F1E", + "Bracken": "4A2A04", + "Brandy": "DEC196", + "Brandy Punch": "CD8429", + "Brandy Rose": "BB8983", + "Breaker Bay": "5DA19F", + "Brick Red": "C62D42", + "Bridal Heath": "FFFAF4", + "Bridesmaid": "FEF0EC", + "Bright Gray": "3C4151", + "Bright Green": "66FF00", + "Bright Red": "B10000", + "Bright Sun": "FED33C", + "Bright Turquoise": "08E8DE", + "Brilliant Rose": "F653A6", + "Brink Pink": "FB607F", + "Bronco": "ABA196", + "Bronze": "3F2109", + "Bronze Olive": "4E420C", + "Bronzetone": "4D400F", + "Broom": "FFEC13", + "Brown": "964B00", + "Brown Bramble": "592804", + "Brown Derby": "492615", + "Brown Pod": "401801", + "Brown Rust": "AF593E", + "Brown Tumbleweed": "37290E", + "Bubbles": "E7FEFF", + "Buccaneer": "622F30", + "Bud": "A8AE9C", + "Buddha Gold": "C1A004", + "Buff": "F0DC82", + "Bulgarian Rose": "480607", + "Bull Shot": "864D1E", + "Bunker": "0D1117", + "Bunting": "151F4C", + "Burgundy": "900020", + "Burnham": "002E20", + "Burning Orange": "FF7034", + "Burning Sand": "D99376", + "Burnt Maroon": "420303", + "Burnt Orange": "CC5500", + "Burnt Sienna": "E97451", + "Burnt Umber": "8A3324", + "Bush": "0D2E1C", + "Buttercup": "F3AD16", + "Buttered Rum": "A1750D", + "Butterfly Bush": "624E9A", + "Buttermilk": "FFF1B5", + "Buttery White": "FFFCEA", + "Cab Sav": "4D0A18", + "Cabaret": "D94972", + "Cabbage Pont": "3F4C3A", + "Cactus": "587156", + "Cadet Blue": "A9B2C3", + "Cadillac": "B04C6A", + "Cafe Royale": "6F440C", + "Calico": "E0C095", + "California": "FE9D04", + "Calypso": "31728D", + "Camarone": "00581A", + "Camelot": "893456", + "Cameo": "D9B99B", + "Camouflage": "3C3910", + "Camouflage Green": "78866B", + "Can Can": "D591A4", + "Canary": "F3FB62", + "Candlelight": "FCD917", + "Candy Corn": "FBEC5D", + "Cannon Black": "251706", + "Cannon Pink": "894367", + "Cape Cod": "3C4443", + "Cape Honey": "FEE5AC", + "Cape Palliser": "A26645", + "Caper": "DCEDB4", + "Caramel": "FFDDAF", + "Cararra": "EEEEE8", + "Cardin Green": "01361C", + "Cardinal": "C41E3A", + "Cardinal Pink": "8C055E", + "Careys Pink": "D29EAA", + "Caribbean Green": "00CC99", + "Carissma": "EA88A8", + "Carla": "F3FFD8", + "Carmine": "960018", + "Carnaby Tan": "5C2E01", + "Carnation": "F95A61", + "Carnation Pink": "FFA6C9", + "Carousel Pink": "F9E0ED", + "Carrot Orange": "ED9121", + "Casablanca": "F8B853", + "Casal": "2F6168", + "Cascade": "8BA9A5", + "Cashmere": "E6BEA5", + "Casper": "ADBED1", + "Castro": "52001F", + "Catalina Blue": "062A78", + "Catskill White": "EEF6F7", + "Cavern Pink": "E3BEBE", + "Cedar": "3E1C14", + "Cedar Wood Finish": "711A00", + "Celadon": "ACE1AF", + "Celery": "B8C25D", + "Celeste": "D1D2CA", + "Cello": "1E385B", + "Celtic": "163222", + "Cement": "8D7662", + "Ceramic": "FCFFF9", + "Cerise": "DA3287", + "Cerise Red": "DE3163", + "Cerulean": "02A4D3", + "Cerulean Blue": "2A52BE", + "Chablis": "FFF4F3", + "Chalet Green": "516E3D", + "Chalky": "EED794", + "Chambray": "354E8C", + "Chamois": "EDDCB1", + "Champagne": "FAECCC", + "Chantilly": "F8C3DF", + "Charade": "292937", + "Chardon": "FFF3F1", + "Chardonnay": "FFCD8C", + "Charlotte": "BAEEF9", + "Charm": "D47494", + "Chartreuse": "7FFF00", + "Chartreuse Yellow": "DFFF00", + "Chateau Green": "40A860", + "Chatelle": "BDB3C7", + "Chathams Blue": "175579", + "Chelsea Cucumber": "83AA5D", + "Chelsea Gem": "9E5302", + "Chenin": "DFCD6F", + "Cherokee": "FCDA98", + "Cherry Pie": "2A0359", + "Cherrywood": "651A14", + "Cherub": "F8D9E9", + "Chestnut": "B94E48", + "Chestnut Rose": "CD5C5C", + "Chetwode Blue": "8581D9", + "Chicago": "5D5C58", + "Chiffon": "F1FFC8", + "Chilean Fire": "F77703", + "Chilean Heath": "FFFDE6", + "China Ivory": "FCFFE7", + "Chino": "CEC7A7", + "Chinook": "A8E3BD", + "Chocolate": "370202", + "Christalle": "33036B", + "Christi": "67A712", + "Christine": "E7730A", + "Chrome White": "E8F1D4", + "Cinder": "0E0E18", + "Cinderella": "FDE1DC", + "Cinnabar": "E34234", + "Cinnamon": "7B3F00", + "Cioccolato": "55280C", + "Citrine White": "FAF7D6", + "Citron": "9EA91F", + "Citrus": "A1C50A", + "Clairvoyant": "480656", + "Clam Shell": "D4B6AF", + "Claret": "7F1734", + "Classic Rose": "FBCCE7", + "Clay Ash": "BDC8B3", + "Clay Creek": "8A8360", + "Clear Day": "E9FFFD", + "Clementine": "E96E00", + "Clinker": "371D09", + "Cloud": "C7C4BF", + "Cloud Burst": "202E54", + "Cloudy": "ACA59F", + "Clover": "384910", + "Cobalt": "0047AB", + "Cocoa Bean": "481C1C", + "Cocoa Brown": "301F1E", + "Coconut Cream": "F8F7DC", + "Cod Gray": "0B0B0B", + "Coffee": "706555", + "Coffee Bean": "2A140E", + "Cognac": "9F381D", + "Cola": "3F2500", + "Cold Purple": "ABA0D9", + "Cold Turkey": "CEBABA", + "Colonial White": "FFEDBC", + "Comet": "5C5D75", + "Como": "517C66", + "Conch": "C9D9D2", + "Concord": "7C7B7A", + "Concrete": "F2F2F2", + "Confetti": "E9D75A", + "Congo Brown": "593737", + "Congress Blue": "02478E", + "Conifer": "ACDD4D", + "Contessa": "C6726B", + "Copper": "B87333", + "Copper Canyon": "7E3A15", + "Copper Rose": "996666", + "Copper Rust": "944747", + "Copperfield": "DA8A67", + "Coral": "FF7F50", + "Coral Red": "FF4040", + "Coral Reef": "C7BCA2", + "Coral Tree": "A86B6B", + "Corduroy": "606E68", + "Coriander": "C4D0B0", + "Cork": "40291D", + "Corn": "E7BF05", + "Corn Field": "F8FACD", + "Corn Harvest": "8B6B0B", + "Cornflower": "93CCEA", + "Cornflower Blue": "6495ED", + "Cornflower Lilac": "FFB0AC", + "Corvette": "FAD3A2", + "Cosmic": "76395D", + "Cosmos": "FFD8D9", + "Costa Del Sol": "615D30", + "Cotton Candy": "FFB7D5", + "Cotton Seed": "C2BDB6", + "County Green": "01371A", + "Cowboy": "4D282D", + "Crail": "B95140", + "Cranberry": "DB5079", + "Crater Brown": "462425", + "Cream": "FFFDD0", + "Cream Brulee": "FFE5A0", + "Cream Can": "F5C85C", + "Creole": "1E0F04", + "Crete": "737829", + "Crimson": "DC143C", + "Crocodile": "736D58", + "Crown of Thorns": "771F1F", + "Crowshead": "1C1208", + "Cruise": "B5ECDF", + "Crusoe": "004816", + "Crusta": "FD7B33", + "Cumin": "924321", + "Cumulus": "FDFFD5", + "Cupid": "FBBEDA", + "Curious Blue": "2596D1", + "Cutty Sark": "507672", + "Cyan / Aqua": "00FFFF", + "Cyprus": "003E40", + "Daintree": "012731", + "Dairy Cream": "F9E4BC", + "Daisy Bush": "4F2398", + "Dallas": "6E4B26", + "Dandelion": "FED85D", + "Danube": "6093D1", + "Dark Blue": "0000C8", + "Dark Burgundy": "770F05", + "Dark Ebony": "3C2005", + "Dark Fern": "0A480D", + "Dark Tan": "661010", + "Dawn": "A6A29A", + "Dawn Pink": "F3E9E5", + "De York": "7AC488", + "Deco": "D2DA97", + "Deep Blue": "220878", + "Deep Blush": "E47698", + "Deep Bronze": "4A3004", + "Deep Cerulean": "007BA7", + "Deep Cove": "051040", + "Deep Fir": "002900", + "Deep Forest Green": "182D09", + "Deep Koamaru": "1B127B", + "Deep Oak": "412010", + "Deep Sapphire": "082567", + "Deep Sea": "01826B", + "Deep Sea Green": "095859", + "Deep Teal": "003532", + "Del Rio": "B09A95", + "Dell": "396413", + "Delta": "A4A49D", + "Deluge": "7563A8", + "Denim": "1560BD", + "Derby": "FFEED8", + "Desert": "AE6020", + "Desert Sand": "EDC9AF", + "Desert Storm": "F8F8F7", + "Dew": "EAFFFE", + "Di Serria": "DB995E", + "Diesel": "130000", + "Dingley": "5D7747", + "Disco": "871550", + "Dixie": "E29418", + "Dodger Blue": "1E90FF", + "Dolly": "F9FF8B", + "Dolphin": "646077", + "Domino": "8E775E", + "Don Juan": "5D4C51", + "Donkey Brown": "A69279", + "Dorado": "6B5755", + "Double Colonial White": "EEE3AD", + "Double Pearl Lusta": "FCF4D0", + "Double Spanish White": "E6D7B9", + "Dove Gray": "6D6C6C", + "Downriver": "092256", + "Downy": "6FD0C5", + "Driftwood": "AF8751", + "Drover": "FDF7AD", + "Dull Lavender": "A899E6", + "Dune": "383533", + "Dust Storm": "E5CCC9", + "Dusty Gray": "A8989B", + "Eagle": "B6BAA4", + "Earls Green": "C9B93B", + "Early Dawn": "FFF9E6", + "East Bay": "414C7D", + "East Side": "AC91CE", + "Eastern Blue": "1E9AB0", + "Ebb": "E9E3E3", + "Ebony": "0C0B1D", + "Ebony Clay": "26283B", + "Eclipse": "311C17", + "Ecru White": "F5F3E5", + "Ecstasy": "FA7814", + "Eden": "105852", + "Edgewater": "C8E3D7", + "Edward": "A2AEAB", + "Egg Sour": "FFF4DD", + "Egg White": "FFEFC1", + "Eggplant": "614051", + "El Paso": "1E1708", + "El Salva": "8F3E33", + "Electric Lime": "CCFF00", + "Electric Violet": "8B00FF", + "Elephant": "123447", + "Elf Green": "088370", + "Elm": "1C7C7D", + "Emerald": "50C878", + "Eminence": "6C3082", + "Emperor": "514649", + "Empress": "817377", + "Endeavour": "0056A7", + "Energy Yellow": "F8DD5C", + "English Holly": "022D15", + "English Walnut": "3E2B23", + "Envy": "8BA690", + "Equator": "E1BC64", + "Espresso": "612718", + "Eternity": "211A0E", + "Eucalyptus": "278A5B", + "Eunry": "CFA39D", + "Evening Sea": "024E46", + "Everglade": "1C402E", + "Faded Jade": "427977", + "Fair Pink": "FFEFEC", + "Falcon": "7F626D", + "Fall Green": "ECEBBD", + "Falu Red": "801818", + "Fantasy": "FAF3F0", + "Fedora": "796A78", + "Feijoa": "9FDD8C", + "Fern": "63B76C", + "Fern Frond": "657220", + "Fern Green": "4F7942", + "Ferra": "704F50", + "Festival": "FBE96C", + "Feta": "F0FCEA", + "Fiery Orange": "B35213", + "Finch": "626649", + "Finlandia": "556D56", + "Finn": "692D54", + "Fiord": "405169", + "Fire": "AA4203", + "Fire Bush": "E89928", + "Firefly": "0E2A30", + "Flame Pea": "DA5B38", + "Flamenco": "FF7D07", + "Flamingo": "F2552A", + "Flax": "EEDC82", + "Flax Smoke": "7B8265", + "Flesh": "FFCBA4", + "Flint": "6F6A61", + "Flirt": "A2006D", + "Flush Mahogany": "CA3435", + "Flush Orange": "FF7F00", + "Foam": "D8FCFA", + "Fog": "D7D0FF", + "Foggy Gray": "CBCAB6", + "Forest Green": "228B22", + "Forget Me Not": "FFF1EE", + "Fountain Blue": "56B4BE", + "Frangipani": "FFDEB3", + "French Gray": "BDBDC6", + "French Lilac": "ECC7EE", + "French Pass": "BDEDFD", + "French Rose": "F64A8A", + "Fresh Eggplant": "990066", + "Friar Gray": "807E79", + "Fringy Flower": "B1E2C1", + "Froly": "F57584", + "Frost": "EDF5DD", + "Frosted Mint": "DBFFF8", + "Frostee": "E4F6E7", + "Fruit Salad": "4F9D5D", + "Fuchsia Blue": "7A58C1", + "Fuchsia Pink": "C154C1", + "Fuego": "BEDE0D", + "Fuel Yellow": "ECA927", + "Fun Blue": "1959A8", + "Fun Green": "016D39", + "Fuscous Gray": "54534D", + "Fuzzy Wuzzy Brown": "C45655", + "Gable Green": "163531", + "Gallery": "EFEFEF", + "Galliano": "DCB20C", + "Gamboge": "E49B0F", + "Geebung": "D18F1B", + "Genoa": "15736B", + "Geraldine": "FB8989", + "Geyser": "D4DFE2", + "Ghost": "C7C9D5", + "Gigas": "523C94", + "Gimblet": "B8B56A", + "Gin": "E8F2EB", + "Gin Fizz": "FFF9E2", + "Givry": "F8E4BF", + "Glacier": "80B3C4", + "Glade Green": "61845F", + "Go Ben": "726D4E", + "Goblin": "3D7D52", + "Gold": "FFD700", + "Gold Drop": "F18200", + "Gold Sand": "E6BE8A", + "Gold Tips": "DEBA13", + "Golden Bell": "E28913", + "Golden Dream": "F0D52D", + "Golden Fizz": "F5FB3D", + "Golden Glow": "FDE295", + "Golden Grass": "DAA520", + "Golden Sand": "F0DB7D", + "Golden Tainoi": "FFCC5C", + "Goldenrod": "FCD667", + "Gondola": "261414", + "Gordons Green": "0B1107", + "Gorse": "FFF14F", + "Gossamer": "069B81", + "Gossip": "D2F8B0", + "Gothic": "6D92A1", + "Governor Bay": "2F3CB3", + "Grain Brown": "E4D5B7", + "Grandis": "FFD38C", + "Granite Green": "8D8974", + "Granny Apple": "D5F6E3", + "Granny Smith": "84A0A0", + "Granny Smith Apple": "9DE093", + "Grape": "381A51", + "Graphite": "251607", + "Gravel": "4A444B", + "Gray": "808080", + "Gray Asparagus": "465945", + "Gray Chateau": "A2AAB3", + "Gray Nickel": "C3C3BD", + "Gray Nurse": "E7ECE6", + "Gray Olive": "A9A491", + "Gray Suit": "C1BECD", + "Green": "00FF00", + "Green Haze": "01A368", + "Green House": "24500F", + "Green Kelp": "25311C", + "Green Leaf": "436A0D", + "Green Mist": "CBD3B0", + "Green Pea": "1D6142", + "Green Smoke": "A4AF6E", + "Green Spring": "B8C1B1", + "Green Vogue": "032B52", + "Green Waterloo": "101405", + "Green White": "E8EBE0", + "Green Yellow": "ADFF2F", + "Grenadier": "D54600", + "Guardsman Red": "BA0101", + "Gulf Blue": "051657", + "Gulf Stream": "80B3AE", + "Gull Gray": "9DACB7", + "Gum Leaf": "B6D3BF", + "Gumbo": "7CA1A6", + "Gun Powder": "414257", + "Gunsmoke": "828685", + "Gurkha": "9A9577", + "Hacienda": "98811B", + "Hairy Heath": "6B2A14", + "Haiti": "1B1035", + "Half Baked": "85C4CC", + "Half Colonial White": "FDF6D3", + "Half Dutch White": "FEF7DE", + "Half Spanish White": "FEF4DB", + "Half and Half": "FFFEE1", + "Hampton": "E5D8AF", + "Harlequin": "3FFF00", + "Harp": "E6F2EA", + "Harvest Gold": "E0B974", + "Havelock Blue": "5590D9", + "Hawaiian Tan": "9D5616", + "Hawkes Blue": "D4E2FC", + "Heath": "541012", + "Heather": "B7C3D0", + "Heathered Gray": "B6B095", + "Heavy Metal": "2B3228", + "Heliotrope": "DF73FF", + "Hemlock": "5E5D3B", + "Hemp": "907874", + "Hibiscus": "B6316C", + "Highland": "6F8E63", + "Hillary": "ACA586", + "Himalaya": "6A5D1B", + "Hint of Green": "E6FFE9", + "Hint of Red": "FBF9F9", + "Hint of Yellow": "FAFDE4", + "Hippie Blue": "589AAF", + "Hippie Green": "53824B", + "Hippie Pink": "AE4560", + "Hit Gray": "A1ADB5", + "Hit Pink": "FFAB81", + "Hokey Pokey": "C8A528", + "Hoki": "65869F", + "Holly": "011D13", + "Hollywood Cerise": "F400A1", + "Honey Flower": "4F1C70", + "Honeysuckle": "EDFC84", + "Hopbush": "D06DA1", + "Horizon": "5A87A0", + "Horses Neck": "604913", + "Hot Cinnamon": "D2691E", + "Hot Pink": "FF69B4", + "Hot Toddy": "B38007", + "Humming Bird": "CFF9F3", + "Hunter Green": "161D10", + "Hurricane": "877C7B", + "Husk": "B7A458", + "Ice Cold": "B1F4E7", + "Iceberg": "DAF4F0", + "Illusion": "F6A4C9", + "Inch Worm": "B0E313", + "Indian Khaki": "C3B091", + "Indian Tan": "4D1E01", + "Indigo": "4F69C6", + "Indochine": "C26B03", + "International Klein Blue": "002FA7", + "International Orange": "FF4F00", + "Irish Coffee": "5F3D26", + "Iroko": "433120", + "Iron": "D4D7D9", + "Ironside Gray": "676662", + "Ironstone": "86483C", + "Island Spice": "FFFCEE", + "Ivory": "FFFFF0", + "Jacaranda": "2E0329", + "Jacarta": "3A2A6A", + "Jacko Bean": "2E1905", + "Jacksons Purple": "20208D", + "Jade": "00A86B", + "Jaffa": "EF863F", + "Jagged Ice": "C2E8E5", + "Jagger": "350E57", + "Jaguar": "080110", + "Jambalaya": "5B3013", + "Janna": "F4EBD3", + "Japanese Laurel": "0A6906", + "Japanese Maple": "780109", + "Japonica": "D87C63", + "Java": "1FC2C2", + "Jazzberry Jam": "A50B5E", + "Jelly Bean": "297B9A", + "Jet Stream": "B5D2CE", + "Jewel": "126B40", + "Jon": "3B1F1F", + "Jonquil": "EEFF9A", + "Jordy Blue": "8AB9F1", + "Judge Gray": "544333", + "Jumbo": "7C7B82", + "Jungle Green": "29AB87", + "Jungle Mist": "B4CFD3", + "Juniper": "6D9292", + "Just Right": "ECCDB9", + "Kabul": "5E483E", + "Kaitoke Green": "004620", + "Kangaroo": "C6C8BD", + "Karaka": "1E1609", + "Karry": "FFEAD4", + "Kashmir Blue": "507096", + "Kelp": "454936", + "Kenyan Copper": "7C1C05", + "Keppel": "3AB09E", + "Key Lime Pie": "BFC921", + "Khaki": "F0E68C", + "Kidnapper": "E1EAD4", + "Kilamanjaro": "240C02", + "Killarney": "3A6A47", + "Kimberly": "736C9F", + "Kingfisher Daisy": "3E0480", + "Kobi": "E79FC4", + "Kokoda": "6E6D57", + "Korma": "8F4B0E", + "Koromiko": "FFBD5F", + "Kournikova": "FFE772", + "Kumera": "886221", + "La Palma": "368716", + "La Rioja": "B3C110", + "Las Palmas": "C6E610", + "Laser": "C8B568", + "Laser Lemon": "FFFF66", + "Laurel": "749378", + "Lavender": "B57EDC", + "Lavender Gray": "BDBBD7", + "Lavender Magenta": "EE82EE", + "Lavender Pink": "FBAED2", + "Lavender Purple": "967BB6", + "Lavender Rose": "FBA0E3", + "Lavender blush": "FFF0F5", + "Leather": "967059", + "Lemon": "FDE910", + "Lemon Chiffon": "FFFACD", + "Lemon Ginger": "AC9E22", + "Lemon Grass": "9B9E8F", + "Light Apricot": "FDD5B1", + "Light Orchid": "E29CD2", + "Light Wisteria": "C9A0DC", + "Lightning Yellow": "FCC01E", + "Lilac": "C8A2C8", + "Lilac Bush": "9874D3", + "Lily": "C8AABF", + "Lily White": "E7F8FF", + "Lima": "76BD17", + "Lime": "BFFF00", + "Limeade": "6F9D02", + "Limed Ash": "747D63", + "Limed Oak": "AC8A56", + "Limed Spruce": "394851", + "Linen": "FAF0E6", + "Link Water": "D9E4F5", + "Lipstick": "AB0563", + "Lisbon Brown": "423921", + "Livid Brown": "4D282E", + "Loafer": "EEF4DE", + "Loblolly": "BDC9CE", + "Lochinvar": "2C8C84", + "Lochmara": "007EC7", + "Locust": "A8AF8E", + "Log Cabin": "242A1D", + "Logan": "AAA9CD", + "Lola": "DFCFDB", + "London Hue": "BEA6C3", + "Lonestar": "6D0101", + "Lotus": "863C3C", + "Loulou": "460B41", + "Lucky": "AF9F1C", + "Lucky Point": "1A1A68", + "Lunar Green": "3C493A", + "Luxor Gold": "A7882C", + "Lynch": "697E9A", + "Mabel": "D9F7FF", + "Macaroni and Cheese": "FFB97B", + "Madang": "B7F0BE", + "Madison": "09255D", + "Madras": "3F3002", + "Magenta / Fuchsia": "FF00FF", + "Magic Mint": "AAF0D1", + "Magnolia": "F8F4FF", + "Mahogany": "4E0606", + "Mai Tai": "B06608", + "Maize": "F5D5A0", + "Makara": "897D6D", + "Mako": "444954", + "Malachite": "0BDA51", + "Malibu": "7DC8F7", + "Mallard": "233418", + "Malta": "BDB2A1", + "Mamba": "8E8190", + "Manatee": "8D90A1", + "Mandalay": "AD781B", + "Mandy": "E25465", + "Mandys Pink": "F2C3B2", + "Mango Tango": "E77200", + "Manhattan": "F5C999", + "Mantis": "74C365", + "Mantle": "8B9C90", + "Manz": "EEEF78", + "Mardi Gras": "350036", + "Marigold": "B98D28", + "Marigold Yellow": "FBE870", + "Mariner": "286ACD", + "Maroon": "800000", + "Maroon Flush": "C32148", + "Maroon Oak": "520C17", + "Marshland": "0B0F08", + "Martini": "AFA09E", + "Martinique": "363050", + "Marzipan": "F8DB9D", + "Masala": "403B38", + "Matisse": "1B659D", + "Matrix": "B05D54", + "Matterhorn": "4E3B41", + "Mauve": "E0B0FF", + "Mauvelous": "F091A9", + "Maverick": "D8C2D5", + "Medium Carmine": "AF4035", + "Medium Purple": "9370DB", + "Medium Red Violet": "BB3385", + "Melanie": "E4C2D5", + "Melanzane": "300529", + "Melon": "FEBAAD", + "Melrose": "C7C1FF", + "Mercury": "E5E5E5", + "Merino": "F6F0E6", + "Merlin": "413C37", + "Merlot": "831923", + "Metallic Bronze": "49371B", + "Metallic Copper": "71291D", + "Meteor": "D07D12", + "Meteorite": "3C1F76", + "Mexican Red": "A72525", + "Mid Gray": "5F5F6E", + "Midnight": "011635", + "Midnight Blue": "003366", + "Midnight Moss": "041004", + "Mikado": "2D2510", + "Milan": "FAFFA4", + "Milano Red": "B81104", + "Milk Punch": "FFF6D4", + "Millbrook": "594433", + "Mimosa": "F8FDD3", + "Mindaro": "E3F988", + "Mine Shaft": "323232", + "Mineral Green": "3F5D53", + "Ming": "36747D", + "Minsk": "3F307F", + "Mint Green": "98FF98", + "Mint Julep": "F1EEC1", + "Mint Tulip": "C4F4EB", + "Mirage": "161928", + "Mischka": "D1D2DD", + "Mist Gray": "C4C4BC", + "Mobster": "7F7589", + "Moccaccino": "6E1D14", + "Mocha": "782D19", + "Mojo": "C04737", + "Mona Lisa": "FFA194", + "Monarch": "8B0723", + "Mondo": "4A3C30", + "Mongoose": "B5A27F", + "Monsoon": "8A8389", + "Monte Carlo": "83D0C6", + "Monza": "C7031E", + "Moody Blue": "7F76D3", + "Moon Glow": "FCFEDA", + "Moon Mist": "DCDDCC", + "Moon Raker": "D6CEF6", + "Morning Glory": "9EDEE0", + "Morocco Brown": "441D00", + "Mortar": "504351", + "Mosque": "036A6E", + "Moss Green": "ADDFAD", + "Mountain Meadow": "1AB385", + "Mountain Mist": "959396", + "Mountbatten Pink": "997A8D", + "Muddy Waters": "B78E5C", + "Muesli": "AA8B5B", + "Mulberry": "C54B8C", + "Mulberry Wood": "5C0536", + "Mule Fawn": "8C472F", + "Mulled Wine": "4E4562", + "Mustard": "FFDB58", + "My Pink": "D69188", + "My Sin": "FFB31F", + "Mystic": "E2EBED", + "Nandor": "4B5D52", + "Napa": "ACA494", + "Narvik": "EDF9F1", + "Natural Gray": "8B8680", + "Navajo White": "FFDEAD", + "Navy Blue": "000080", + "Nebula": "CBDBD6", + "Negroni": "FFE2C5", + "Neon Carrot": "FF9933", + "Nepal": "8EABC1", + "Neptune": "7CB7BB", + "Nero": "140600", + "Nevada": "646E75", + "New Orleans": "F3D69D", + "New York Pink": "D7837F", + "Niagara": "06A189", + "Night Rider": "1F120F", + "Night Shadz": "AA375A", + "Nile Blue": "193751", + "Nobel": "B7B1B1", + "Nomad": "BAB1A2", + "Norway": "A8BD9F", + "Nugget": "C59922", + "Nutmeg": "81422C", + "Nutmeg Wood Finish": "683600", + "Oasis": "FEEFCE", + "Observatory": "02866F", + "Ocean Green": "41AA78", + "Ochre": "CC7722", + "Off Green": "E6F8F3", + "Off Yellow": "FEF9E3", + "Oil": "281E15", + "Old Brick": "901E1E", + "Old Copper": "724A2F", + "Old Gold": "CFB53B", + "Old Lace": "FDF5E6", + "Old Lavender": "796878", + "Old Rose": "C08081", + "Olive": "808000", + "Olive Drab": "6B8E23", + "Olive Green": "B5B35C", + "Olive Haze": "8B8470", + "Olivetone": "716E10", + "Olivine": "9AB973", + "Onahau": "CDF4FF", + "Onion": "2F270E", + "Opal": "A9C6C2", + "Opium": "8E6F70", + "Oracle": "377475", + "Orange": "FF681F", + "Orange Peel": "FFA000", + "Orange Roughy": "C45719", + "Orange White": "FEFCED", + "Orchid": "DA70D6", + "Orchid White": "FFFDF3", + "Oregon": "9B4703", + "Orient": "015E85", + "Oriental Pink": "C69191", + "Orinoco": "F3FBD4", + "Oslo Gray": "878D91", + "Ottoman": "E9F8ED", + "Outer Space": "2D383A", + "Outrageous Orange": "FF6037", + "Oxford Blue": "384555", + "Oxley": "779E86", + "Oyster Bay": "DAFAFF", + "Oyster Pink": "E9CECD", + "Paarl": "A65529", + "Pablo": "776F61", + "Pacific Blue": "009DC4", + "Pacifika": "778120", + "Paco": "411F10", + "Padua": "ADE6C4", + "Pale Canary": "FFFF99", + "Pale Leaf": "C0D3B9", + "Pale Oyster": "988D77", + "Pale Prim": "FDFEB8", + "Pale Rose": "FFE1F2", + "Pale Sky": "6E7783", + "Pale Slate": "C3BFC1", + "Palm Green": "09230F", + "Palm Leaf": "19330E", + "Pampas": "F4F2EE", + "Panache": "EAF6EE", + "Pancho": "EDCDAB", + "Papaya Whip": "FFEFD5", + "Paprika": "8D0226", + "Paradiso": "317D82", + "Parchment": "F1E9D2", + "Paris Daisy": "FFF46E", + "Paris M": "26056A", + "Paris White": "CADCD4", + "Parsley": "134F19", + "Pastel Green": "77DD77", + "Pastel Pink": "FFD1DC", + "Patina": "639A8F", + "Pattens Blue": "DEF5FF", + "Paua": "260368", + "Pavlova": "D7C498", + "Peach": "FFE5B4", + "Peach Cream": "FFF0DB", + "Peach Orange": "FFCC99", + "Peach Schnapps": "FFDCD6", + "Peach Yellow": "FADFAD", + "Peanut": "782F16", + "Pear": "D1E231", + "Pearl Bush": "E8E0D5", + "Pearl Lusta": "FCF4DC", + "Peat": "716B56", + "Pelorous": "3EABBF", + "Peppermint": "E3F5E1", + "Perano": "A9BEF2", + "Perfume": "D0BEF8", + "Periglacial Blue": "E1E6D6", + "Periwinkle": "CCCCFF", + "Periwinkle Gray": "C3CDE6", + "Persian Blue": "1C39BB", + "Persian Green": "00A693", + "Persian Indigo": "32127A", + "Persian Pink": "F77FBE", + "Persian Plum": "701C1C", + "Persian Red": "CC3333", + "Persian Rose": "FE28A2", + "Persimmon": "FF6B53", + "Peru Tan": "7F3A02", + "Pesto": "7C7631", + "Petite Orchid": "DB9690", + "Pewter": "96A8A1", + "Pharlap": "A3807B", + "Picasso": "FFF39D", + "Pickled Bean": "6E4826", + "Pickled Bluewood": "314459", + "Picton Blue": "45B1E8", + "Pig Pink": "FDD7E4", + "Pigeon Post": "AFBDD9", + "Pigment Indigo": "4B0082", + "Pine Cone": "6D5E54", + "Pine Glade": "C7CD90", + "Pine Green": "01796F", + "Pine Tree": "171F04", + "Pink": "FFC0CB", + "Pink Flamingo": "FF66FF", + "Pink Flare": "E1C0C8", + "Pink Lace": "FFDDF4", + "Pink Lady": "FFF1D8", + "Pink Salmon": "FF91A4", + "Pink Swan": "BEB5B7", + "Piper": "C96323", + "Pipi": "FEF4CC", + "Pippin": "FFE1DF", + "Pirate Gold": "BA7F03", + "Pistachio": "9DC209", + "Pixie Green": "C0D8B6", + "Pizazz": "FF9000", + "Pizza": "C99415", + "Plantation": "27504B", + "Plum": "843179", + "Pohutukawa": "8F021C", + "Polar": "E5F9F6", + "Polo Blue": "8DA8CC", + "Pomegranate": "F34723", + "Pompadour": "660045", + "Porcelain": "EFF2F3", + "Porsche": "EAAE69", + "Port Gore": "251F4F", + "Portafino": "FFFFB4", + "Portage": "8B9FEE", + "Portica": "F9E663", + "Pot Pourri": "F5E7E2", + "Potters Clay": "8C5738", + "Powder Ash": "BCC9C2", + "Powder Blue": "B0E0E6", + "Prairie Sand": "9A3820", + "Prelude": "D0C0E5", + "Prim": "F0E2EC", + "Primrose": "EDEA99", + "Provincial Pink": "FEF5F1", + "Prussian Blue": "003153", + "Puce": "CC8899", + "Pueblo": "7D2C14", + "Puerto Rico": "3FC1AA", + "Pumice": "C2CAC4", + "Pumpkin": "FF7518", + "Pumpkin Skin": "B1610B", + "Punch": "DC4333", + "Punga": "4D3D14", + "Purple": "660099", + "Purple Heart": "652DC1", + "Purple Mountain's Majesty": "9678B6", + "Purple Pizzazz": "FF00CC", + "Putty": "E7CD8C", + "Quarter Pearl Lusta": "FFFDF4", + "Quarter Spanish White": "F7F2E1", + "Quicksand": "BD978E", + "Quill Gray": "D6D6D1", + "Quincy": "623F2D", + "Racing Green": "0C1911", + "Radical Red": "FF355E", + "Raffia": "EADAB8", + "Rainee": "B9C8AC", + "Rajah": "F7B668", + "Rangitoto": "2E3222", + "Rangoon Green": "1C1E13", + "Raven": "727B89", + "Raw Sienna": "D27D46", + "Raw Umber": "734A12", + "Razzle Dazzle Rose": "FF33CC", + "Razzmatazz": "E30B5C", + "Rebel": "3C1206", + "Red": "FF0000", + "Red Beech": "7B3801", + "Red Berry": "8E0000", + "Red Damask": "DA6A41", + "Red Devil": "860111", + "Red Orange": "FF3F34", + "Red Oxide": "6E0902", + "Red Ribbon": "ED0A3F", + "Red Robin": "80341F", + "Red Stage": "D05F04", + "Red Violet": "C71585", + "Redwood": "5D1E0F", + "Reef": "C9FFA2", + "Reef Gold": "9F821C", + "Regal Blue": "013F6A", + "Regent Gray": "86949F", + "Regent St Blue": "AAD6E6", + "Remy": "FEEBF3", + "Reno Sand": "A86515", + "Resolution Blue": "002387", + "Revolver": "2C1632", + "Rhino": "2E3F62", + "Rice Cake": "FFFEF0", + "Rice Flower": "EEFFE2", + "Rich Gold": "A85307", + "Rio Grande": "BBD009", + "Ripe Lemon": "F4D81C", + "Ripe Plum": "410056", + "Riptide": "8BE6D8", + "River Bed": "434C59", + "Rob Roy": "EAC674", + "Robin's Egg Blue": "00CCCC", + "Rock": "4D3833", + "Rock Blue": "9EB1CD", + "Rock Spray": "BA450C", + "Rodeo Dust": "C9B29B", + "Rolling Stone": "747D83", + "Roman": "DE6360", + "Roman Coffee": "795D4C", + "Romance": "FFFEFD", + "Romantic": "FFD2B7", + "Ronchi": "ECC54E", + "Roof Terracotta": "A62F20", + "Rope": "8E4D1E", + "Rose": "FF007F", + "Rose Bud": "FBB2A3", + "Rose Bud Cherry": "800B47", + "Rose Fog": "E7BCB4", + "Rose White": "FFF6F5", + "Rose of Sharon": "BF5500", + "Rosewood": "65000B", + "Roti": "C6A84B", + "Rouge": "A23B6C", + "Royal Blue": "4169E1", + "Royal Heath": "AB3472", + "Royal Purple": "6B3FA0", + "Rum": "796989", + "Rum Swizzle": "F9F8E4", + "Russet": "80461B", + "Russett": "755A57", + "Rust": "B7410E", + "Rustic Red": "480404", + "Rusty Nail": "86560A", + "Saddle": "4C3024", + "Saddle Brown": "583401", + "Saffron": "F4C430", + "Saffron Mango": "F9BF58", + "Sage": "9EA587", + "Sahara": "B7A214", + "Sahara Sand": "F1E788", + "Sail": "B8E0F9", + "Salem": "097F4B", + "Salmon": "FF8C69", + "Salomie": "FEDB8D", + "Salt Box": "685E6E", + "Saltpan": "F1F7F2", + "Sambuca": "3A2010", + "San Felix": "0B6207", + "San Juan": "304B6A", + "San Marino": "456CAC", + "Sand Dune": "826F65", + "Sandal": "AA8D6F", + "Sandrift": "AB917A", + "Sandstone": "796D62", + "Sandwisp": "F5E7A2", + "Sandy Beach": "FFEAC8", + "Sandy brown": "F4A460", + "Sangria": "92000A", + "Sanguine Brown": "8D3D38", + "Santa Fe": "B16D52", + "Santas Gray": "9FA0B1", + "Sapling": "DED4A4", + "Sapphire": "2F519E", + "Saratoga": "555B10", + "Satin Linen": "E6E4D4", + "Sauvignon": "FFF5F3", + "Sazerac": "FFF4E0", + "Scampi": "675FA6", + "Scandal": "CFFAF4", + "Scarlet": "FF2400", + "Scarlet Gum": "431560", + "Scarlett": "950015", + "Scarpa Flow": "585562", + "Schist": "A9B497", + "School bus Yellow": "FFD800", + "Schooner": "8B847E", + "Science Blue": "0066CC", + "Scooter": "2EBFD4", + "Scorpion": "695F62", + "Scotch Mist": "FFFBDC", + "Screamin' Green": "66FF66", + "Sea Buckthorn": "FBA129", + "Sea Green": "2E8B57", + "Sea Mist": "C5DBCA", + "Sea Nymph": "78A39C", + "Sea Pink": "ED989E", + "Seagull": "80CCEA", + "Seance": "731E8F", + "Seashell": "F1F1F1", + "Seashell Peach": "FFF5EE", + "Seaweed": "1B2F11", + "Selago": "F0EEFD", + "Selective Yellow": "FFBA00", + "Sepia": "704214", + "Sepia Black": "2B0202", + "Sepia Skin": "9E5B40", + "Serenade": "FFF4E8", + "Shadow": "837050", + "Shadow Green": "9AC2B8", + "Shady Lady": "AAA5A9", + "Shakespeare": "4EABD1", + "Shalimar": "FBFFBA", + "Shamrock": "33CC99", + "Shark": "25272C", + "Sherpa Blue": "004950", + "Sherwood Green": "02402C", + "Shilo": "E8B9B3", + "Shingle Fawn": "6B4E31", + "Ship Cove": "788BBA", + "Ship Gray": "3E3A44", + "Shiraz": "B20931", + "Shocking": "E292C0", + "Shocking Pink": "FC0FC0", + "Shuttle Gray": "5F6672", + "Siam": "646A54", + "Sidecar": "F3E7BB", + "Silk": "BDB1A8", + "Silver": "C0C0C0", + "Silver Chalice": "ACACAC", + "Silver Rust": "C9C0BB", + "Silver Sand": "BFC1C2", + "Silver Tree": "66B58F", + "Sinbad": "9FD7D3", + "Siren": "7A013A", + "Sirocco": "718080", + "Sisal": "D3CBBA", + "Skeptic": "CAE6DA", + "Sky Blue": "76D7EA", + "Slate Gray": "708090", + "Smalt": "003399", + "Smalt Blue": "51808F", + "Smoky": "605B73", + "Snow Drift": "F7FAF7", + "Snow Flurry": "E4FFD1", + "Snowy Mint": "D6FFDB", + "Snuff": "E2D8ED", + "Soapstone": "FFFBF9", + "Soft Amber": "D1C6B4", + "Soft Peach": "F5EDEF", + "Solid Pink": "893843", + "Solitaire": "FEF8E2", + "Solitude": "EAF6FF", + "Sorbus": "FD7C07", + "Sorrell Brown": "CEB98F", + "Soya Bean": "6A6051", + "Spanish Green": "819885", + "Spectra": "2F5A57", + "Spice": "6A442E", + "Spicy Mix": "885342", + "Spicy Mustard": "74640D", + "Spicy Pink": "816E71", + "Spindle": "B6D1EA", + "Spray": "79DEEC", + "Spring Green": "00FF7F", + "Spring Leaves": "578363", + "Spring Rain": "ACCBB1", + "Spring Sun": "F6FFDC", + "Spring Wood": "F8F6F1", + "Sprout": "C1D7B0", + "Spun Pearl": "AAABB7", + "Squirrel": "8F8176", + "St Tropaz": "2D569B", + "Stack": "8A8F8A", + "Star Dust": "9F9F9C", + "Stark White": "E5D7BD", + "Starship": "ECF245", + "Steel Blue": "4682B4", + "Steel Gray": "262335", + "Stiletto": "9C3336", + "Stonewall": "928573", + "Storm Dust": "646463", + "Storm Gray": "717486", + "Stratos": "000741", + "Straw": "D4BF8D", + "Strikemaster": "956387", + "Stromboli": "325D52", + "Studio": "714AB2", + "Submarine": "BAC7C9", + "Sugar Cane": "F9FFF6", + "Sulu": "C1F07C", + "Summer Green": "96BBAB", + "Sun": "FBAC13", + "Sundance": "C9B35B", + "Sundown": "FFB1B3", + "Sunflower": "E4D422", + "Sunglo": "E16865", + "Sunglow": "FFCC33", + "Sunset Orange": "FE4C40", + "Sunshade": "FF9E2C", + "Supernova": "FFC901", + "Surf": "BBD7C1", + "Surf Crest": "CFE5D2", + "Surfie Green": "0C7A79", + "Sushi": "87AB39", + "Suva Gray": "888387", + "Swamp": "001B1C", + "Swamp Green": "ACB78E", + "Swans Down": "DCF0EA", + "Sweet Corn": "FBEA8C", + "Sweet Pink": "FD9FA2", + "Swirl": "D3CDC5", + "Swiss Coffee": "DDD6D5", + "Sycamore": "908D39", + "Tabasco": "A02712", + "Tacao": "EDB381", + "Tacha": "D6C562", + "Tahiti Gold": "E97C07", + "Tahuna Sands": "EEF0C8", + "Tall Poppy": "B32D29", + "Tallow": "A8A589", + "Tamarillo": "991613", + "Tamarind": "341515", + "Tan": "D2B48C", + "Tan Hide": "FA9D5A", + "Tana": "D9DCC1", + "Tangaroa": "03163C", + "Tangerine": "F28500", + "Tango": "ED7A1C", + "Tapa": "7B7874", + "Tapestry": "B05E81", + "Tara": "E1F6E8", + "Tarawera": "073A50", + "Tasman": "CFDCCF", + "Taupe": "483C32", + "Taupe Gray": "B3AF95", + "Tawny Port": "692545", + "Te Papa Green": "1E433C", + "Tea": "C1BAB0", + "Tea Green": "D0F0C0", + "Teak": "B19461", + "Teal": "008080", + "Teal Blue": "044259", + "Temptress": "3B000B", + "Tenn": "CD5700", + "Tequila": "FFE6C7", + "Terracotta": "E2725B", + "Texas": "F8F99C", + "Texas Rose": "FFB555", + "Thatch": "B69D98", + "Thatch Green": "403D19", + "Thistle": "D8BFD8", + "Thistle Green": "CCCAA8", + "Thunder": "33292F", + "Thunderbird": "C02B18", + "Tia Maria": "C1440E", + "Tiara": "C3D1D1", + "Tiber": "063537", + "Tickle Me Pink": "FC80A5", + "Tidal": "F1FFAD", + "Tide": "BFB8B0", + "Timber Green": "16322C", + "Timberwolf": "D9D6CF", + "Titan White": "F0EEFF", + "Toast": "9A6E61", + "Tobacco Brown": "715D47", + "Toledo": "3A0020", + "Tolopea": "1B0245", + "Tom Thumb": "3F583B", + "Tonys Pink": "E79F8C", + "Topaz": "7C778A", + "Torch Red": "FD0E35", + "Torea Bay": "0F2D9E", + "Tory Blue": "1450AA", + "Tosca": "8D3F3F", + "Totem Pole": "991B07", + "Tower Gray": "A9BDBF", + "Tradewind": "5FB3AC", + "Tranquil": "E6FFFF", + "Travertine": "FFFDE8", + "Tree Poppy": "FC9C1D", + "Treehouse": "3B2820", + "Trendy Green": "7C881A", + "Trendy Pink": "8C6495", + "Trinidad": "E64E03", + "Tropical Blue": "C3DDF9", + "Tropical Rain Forest": "00755E", + "Trout": "4A4E5A", + "True V": "8A73D6", + "Tuatara": "363534", + "Tuft Bush": "FFDDCD", + "Tulip Tree": "EAB33B", + "Tumbleweed": "DEA681", + "Tuna": "353542", + "Tundora": "4A4244", + "Turbo": "FAE600", + "Turkish Rose": "B57281", + "Turmeric": "CABB48", + "Turquoise": "30D5C8", + "Turquoise Blue": "6CDAE7", + "Turtle Green": "2A380B", + "Tuscany": "BD5E2E", + "Tusk": "EEF3C3", + "Tussock": "C5994B", + "Tutu": "FFF1F9", + "Twilight": "E4CFDE", + "Twilight Blue": "EEFDFF", + "Twine": "C2955D", + "Tyrian Purple": "66023C", + "Ultramarine": "120A8F", + "Valencia": "D84437", + "Valentino": "350E42", + "Valhalla": "2B194F", + "Van Cleef": "49170C", + "Vanilla": "D1BEA8", + "Vanilla Ice": "F3D9DF", + "Varden": "FFF6DF", + "Venetian Red": "72010F", + "Venice Blue": "055989", + "Venus": "928590", + "Verdigris": "5D5E37", + "Verdun Green": "495400", + "Vermilion": "FF4D00", + "Vesuvius": "B14A0B", + "Victoria": "534491", + "Vida Loca": "549019", + "Viking": "64CCDB", + "Vin Rouge": "983D61", + "Viola": "CB8FA9", + "Violent Violet": "290C5E", + "Violet": "240A40", + "Violet Eggplant": "991199", + "Violet Red": "F7468A", + "Viridian": "40826D", + "Viridian Green": "678975", + "Vis Vis": "FFEFA1", + "Vista Blue": "8FD6B4", + "Vista White": "FCF8F7", + "Vivid Tangerine": "FF9980", + "Vivid Violet": "803790", + "Voodoo": "533455", + "Vulcan": "10121D", + "Wafer": "DECBC6", + "Waikawa Gray": "5A6E9C", + "Waiouru": "363C0D", + "Walnut": "773F1A", + "Wasabi": "788A25", + "Water Leaf": "A1E9DE", + "Watercourse": "056F57", + "Waterloo ": "7B7C94", + "Wattle": "DCD747", + "Watusi": "FFDDCF", + "Wax Flower": "FFC0A8", + "We Peep": "F7DBE6", + "Web Orange": "FFA500", + "Wedgewood": "4E7F9E", + "Well Read": "B43332", + "West Coast": "625119", + "West Side": "FF910F", + "Westar": "DCD9D2", + "Wewak": "F19BAB", + "Wheat": "F5DEB3", + "Wheatfield": "F3EDCF", + "Whiskey": "D59A6F", + "Whisper": "F7F5FA", + "White": "FFFFFF", + "White Ice": "DDF9F1", + "White Lilac": "F8F7FC", + "White Linen": "F8F0E8", + "White Pointer": "FEF8FF", + "White Rock": "EAE8D4", + "Wild Blue Yonder": "7A89B8", + "Wild Rice": "ECE090", + "Wild Sand": "F4F4F4", + "Wild Strawberry": "FF3399", + "Wild Watermelon": "FD5B78", + "Wild Willow": "B9C46A", + "William": "3A686C", + "Willow Brook": "DFECDA", + "Willow Grove": "65745D", + "Windsor": "3C0878", + "Wine Berry": "591D35", + "Winter Hazel": "D5D195", + "Wisp Pink": "FEF4F8", + "Wisteria": "9771B5", + "Wistful": "A4A6D3", + "Witch Haze": "FFFC99", + "Wood Bark": "261105", + "Woodland": "4D5328", + "Woodrush": "302A0F", + "Woodsmoke": "0C0D0F", + "Woody Brown": "483131", + "Xanadu": "738678", + "Yellow": "FFFF00", + "Yellow Green": "C5E17A", + "Yellow Metal": "716338", + "Yellow Orange": "FFAE42", + "Yellow Sea": "FEA904", + "Your Pink": "FFC3C0", + "Yukon Gold": "7B6608", + "Yuma": "CEC291", + "Zambezi": "685558", + "Zanah": "DAECD6", + "Zest": "E5841B", + "Zeus": "292319", + "Ziggurat": "BFDBE2", + "Zinnwaldite": "EBC2AF", + "Zircon": "F4F8FF", + "Zombie": "E4D69B", + "Zorba": "A59B91", + "Zuccini": "044022", + "Zumthor": "EDF6FF" +} \ No newline at end of file -- cgit v1.2.3 From 29f6afac7b98e83962136964fbe129886613bef2 Mon Sep 17 00:00:00 2001 From: CyberCitizen01 Date: Sat, 4 Sep 2021 17:40:02 +0530 Subject: Added ryanzec_colours.json constructed from ryanzec/name-that-color Original source: https://github.com/ryanzec/name-that-color/blob/master/lib/ntc.js#L116-L1681 --- bot/resources/fun/ryanzec_colours.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/fun/ryanzec_colours.json b/bot/resources/fun/ryanzec_colours.json index 63d9be44..7b89f052 100644 --- a/bot/resources/fun/ryanzec_colours.json +++ b/bot/resources/fun/ryanzec_colours.json @@ -1565,4 +1565,4 @@ "Zorba": "A59B91", "Zuccini": "044022", "Zumthor": "EDF6FF" -} \ No newline at end of file +} -- cgit v1.2.3 From e5ec6b7a62b1e8ba1679d0488e03c1d9c3af9d0b Mon Sep 17 00:00:00 2001 From: CyberCitizen01 Date: Sun, 5 Sep 2021 20:31:53 +0530 Subject: Fix issues occured while deploying [no ci] - import PIL is the way to import pillow. - discord-flags isn't being used yet. - Fixed some of the linting issues. --- bot/exts/fun/color.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bot/exts/fun/color.py b/bot/exts/fun/color.py index a00a956b..dd922bf9 100644 --- a/bot/exts/fun/color.py +++ b/bot/exts/fun/color.py @@ -1,16 +1,18 @@ # imports +import colorsys import logging -import colorsys -import pillow +import PIL from discord import Embed -# ! need to install discord-flags and add to poetry.lock file -from discord.ext import commands, flags +from discord.ext import commands from rapidfuzz import process from bot.bot import Bot from bot.constants import Colours +# Planning to use discord-flags, hence require changes to poetry.lock file +# from discord.ext import flags + logger = logging.getLogger(__name__) # constants if needed -- cgit v1.2.3 From 6a692f03eface6e87dd5cdc1b1d415d52f9df148 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Sun, 5 Sep 2021 18:41:23 -0400 Subject: Move to utilities folder as recommended by Xith --- bot/exts/utilities/color.py | 115 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 bot/exts/utilities/color.py diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py new file mode 100644 index 00000000..dd922bf9 --- /dev/null +++ b/bot/exts/utilities/color.py @@ -0,0 +1,115 @@ +# imports +import colorsys +import logging + +import PIL +from discord import Embed +from discord.ext import commands +from rapidfuzz import process + +from bot.bot import Bot +from bot.constants import Colours + +# Planning to use discord-flags, hence require changes to poetry.lock file +# from discord.ext import flags + +logger = logging.getLogger(__name__) + +# constants if needed +# Color URLs - will be replaced by JSON file? +COLOR_JSON_PATH = ".bot//exts//resources//evergreen//" +COLOR_URL_XKCD = "https://xkcd.com/color/rgb/" +COLOR_URL_NAME_THAT_COLOR = "https://github.com/ryanzec/name-that-color/blob/master/lib/ntc.js#L116-L1681" + + +COLOR_ERROR = Embed( + title="Input color is not possible", + description="The color code {user_color} is not a possible color combination." + "\nThe range of possible values are: " + "\nRGB & HSV: 0-255" + "\nCMYK: 0-100%" + "\nHSL: 0-360 degrees" + "\nHex: #000000-#FFFFFF" +) +COLOR_EMBED = Embed( + title="{color_name}", + description="RGB" + "\n{RGB}" + "\nHSV" + "\n{HSV}" + "\nCMYK" + "\n{CMYK}" + "\nHSL" + "\n{HSL}" + "\nHex" + "\n{Hex}" +) + + +# define color command +class Color(commands.cog): + """User initiated command to receive color information.""" + + def __init__(self, bot: Bot): + self.bot = bot + + # ? possible to use discord-flags to allow user to decide on color + # https://pypi.org/project/discord-flags/ + # @flags.add_flag("--rgb", type=str) + # @flags.add_flag("--hsv", type=str) + # @flags.add_flag("--cmyk", type=str) + # @flags.add_flag("--hsl", type=str) + # @flags.add_flag("--hex", type=str) + # @flags.add_flag("--name", type=str) + # @flags.command() + @commands.command(aliases=["color", "colour"]) + @commands.cooldown(1, 10, commands.cooldowns.BucketType.user) + async def color(self, ctx: commands.Context, *, user_color: str) -> None: + """Send information on input color code or color name.""" + # need to check if user_color is RGB, HSV, CMYK, HSL, Hex or color name + # should we assume the color is RGB if not defined? + # should discord tags be used? + # need to review discord.py V2.0 + + # TODO code to check if color code is possible + await ctx.send(embed=COLOR_ERROR.format(color=user_color)) + # await ctx.send(embed=COLOR_EMBED.format( + # RGB=color_dict["RGB"], + # HSV=color_dict["HSV"], + # HSL=color_dict["HSL"], + # CMYK=color_dict["CMYK"], + # HSL=color_dict["HSL"], + # Hex=color_dict["Hex"], + # color_name=color_dict["color_name"] + # ).set_image() # url for image? + # ) + + # TODO pass for now + pass + + # if user_color in color_lists: + # # TODO fuzzy match for color + # pass + + async def color_converter(self, color: str, code_type: str) -> dict: + """Generate alternative color codes for use in the embed.""" + # TODO add code to take color and code type and return other types + # color_dict = { + # "RGB": color_RGB, + # "HSV": color_HSV, + # "HSL": color_HSL, + # "CMYK": color_CMYK, + # "HSL": color_HSL, + # "Hex": color_Hex, + # "color_name": color_name, + # } + pass + + async def photo_generator(self, color: str) -> None: + """Generate photo to use in embed.""" + # TODO need to find a way to store photo in cache to add to embed, then remove + + +def setup(bot: Bot) -> None: + """Load the Color Cog.""" + bot.add_cog(Color(bot)) -- cgit v1.2.3 From f9760f2680c607ede4bec14f8cb103a2ea0c302a Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Sun, 5 Sep 2021 23:07:27 -0400 Subject: Remove old files --- bot/exts/fun/color.py | 115 -------------------------------------------------- 1 file changed, 115 deletions(-) delete mode 100644 bot/exts/fun/color.py diff --git a/bot/exts/fun/color.py b/bot/exts/fun/color.py deleted file mode 100644 index dd922bf9..00000000 --- a/bot/exts/fun/color.py +++ /dev/null @@ -1,115 +0,0 @@ -# imports -import colorsys -import logging - -import PIL -from discord import Embed -from discord.ext import commands -from rapidfuzz import process - -from bot.bot import Bot -from bot.constants import Colours - -# Planning to use discord-flags, hence require changes to poetry.lock file -# from discord.ext import flags - -logger = logging.getLogger(__name__) - -# constants if needed -# Color URLs - will be replaced by JSON file? -COLOR_JSON_PATH = ".bot//exts//resources//evergreen//" -COLOR_URL_XKCD = "https://xkcd.com/color/rgb/" -COLOR_URL_NAME_THAT_COLOR = "https://github.com/ryanzec/name-that-color/blob/master/lib/ntc.js#L116-L1681" - - -COLOR_ERROR = Embed( - title="Input color is not possible", - description="The color code {user_color} is not a possible color combination." - "\nThe range of possible values are: " - "\nRGB & HSV: 0-255" - "\nCMYK: 0-100%" - "\nHSL: 0-360 degrees" - "\nHex: #000000-#FFFFFF" -) -COLOR_EMBED = Embed( - title="{color_name}", - description="RGB" - "\n{RGB}" - "\nHSV" - "\n{HSV}" - "\nCMYK" - "\n{CMYK}" - "\nHSL" - "\n{HSL}" - "\nHex" - "\n{Hex}" -) - - -# define color command -class Color(commands.cog): - """User initiated command to receive color information.""" - - def __init__(self, bot: Bot): - self.bot = bot - - # ? possible to use discord-flags to allow user to decide on color - # https://pypi.org/project/discord-flags/ - # @flags.add_flag("--rgb", type=str) - # @flags.add_flag("--hsv", type=str) - # @flags.add_flag("--cmyk", type=str) - # @flags.add_flag("--hsl", type=str) - # @flags.add_flag("--hex", type=str) - # @flags.add_flag("--name", type=str) - # @flags.command() - @commands.command(aliases=["color", "colour"]) - @commands.cooldown(1, 10, commands.cooldowns.BucketType.user) - async def color(self, ctx: commands.Context, *, user_color: str) -> None: - """Send information on input color code or color name.""" - # need to check if user_color is RGB, HSV, CMYK, HSL, Hex or color name - # should we assume the color is RGB if not defined? - # should discord tags be used? - # need to review discord.py V2.0 - - # TODO code to check if color code is possible - await ctx.send(embed=COLOR_ERROR.format(color=user_color)) - # await ctx.send(embed=COLOR_EMBED.format( - # RGB=color_dict["RGB"], - # HSV=color_dict["HSV"], - # HSL=color_dict["HSL"], - # CMYK=color_dict["CMYK"], - # HSL=color_dict["HSL"], - # Hex=color_dict["Hex"], - # color_name=color_dict["color_name"] - # ).set_image() # url for image? - # ) - - # TODO pass for now - pass - - # if user_color in color_lists: - # # TODO fuzzy match for color - # pass - - async def color_converter(self, color: str, code_type: str) -> dict: - """Generate alternative color codes for use in the embed.""" - # TODO add code to take color and code type and return other types - # color_dict = { - # "RGB": color_RGB, - # "HSV": color_HSV, - # "HSL": color_HSL, - # "CMYK": color_CMYK, - # "HSL": color_HSL, - # "Hex": color_Hex, - # "color_name": color_name, - # } - pass - - async def photo_generator(self, color: str) -> None: - """Generate photo to use in embed.""" - # TODO need to find a way to store photo in cache to add to embed, then remove - - -def setup(bot: Bot) -> None: - """Load the Color Cog.""" - bot.add_cog(Color(bot)) -- cgit v1.2.3 From f64fd14537633439e57381ddd884cb8c2aff997c Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Mon, 6 Sep 2021 20:13:17 -0400 Subject: Continue work in progress Implemented the thumbnail creation from CyberCitizen0, worked on adding some features to the program. Notable Changes: -Check if user passes in hex color -Create thumbnail based on rgb_color To-Do: -Create hex color from rgb color -Create readable rgb color from user input Co-authored-by: Mohammad Rafivulla <77384412+CyberCitizen01@users.noreply.github.com> --- bot/exts/utilities/color.py | 140 +++++++++++++++++++------------------------- 1 file changed, 61 insertions(+), 79 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index dd922bf9..1a4f7031 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -1,113 +1,95 @@ # imports import colorsys import logging +import re +from io import BytesIO -import PIL -from discord import Embed +from PIL import Image, ImageColor +from discord import Embed, File from discord.ext import commands from rapidfuzz import process from bot.bot import Bot from bot.constants import Colours -# Planning to use discord-flags, hence require changes to poetry.lock file -# from discord.ext import flags logger = logging.getLogger(__name__) -# constants if needed -# Color URLs - will be replaced by JSON file? -COLOR_JSON_PATH = ".bot//exts//resources//evergreen//" -COLOR_URL_XKCD = "https://xkcd.com/color/rgb/" -COLOR_URL_NAME_THAT_COLOR = "https://github.com/ryanzec/name-that-color/blob/master/lib/ntc.js#L116-L1681" - - -COLOR_ERROR = Embed( - title="Input color is not possible", - description="The color code {user_color} is not a possible color combination." - "\nThe range of possible values are: " - "\nRGB & HSV: 0-255" - "\nCMYK: 0-100%" - "\nHSL: 0-360 degrees" - "\nHex: #000000-#FFFFFF" -) -COLOR_EMBED = Embed( - title="{color_name}", - description="RGB" - "\n{RGB}" - "\nHSV" - "\n{HSV}" - "\nCMYK" - "\n{CMYK}" - "\nHSL" - "\n{HSL}" - "\nHex" - "\n{Hex}" -) + +ERROR_MSG = """The color code {user_color} is not a possible color combination. +\nThe range of possible values are: +\nRGB & HSV: 0-255 +\nCMYK: 0-100% +\nHSL: 0-360 degrees +\nHex: #000000-#FFFFFF +""" # define color command -class Color(commands.cog): +class Color(commands.Cog): """User initiated command to receive color information.""" def __init__(self, bot: Bot): self.bot = bot - # ? possible to use discord-flags to allow user to decide on color - # https://pypi.org/project/discord-flags/ - # @flags.add_flag("--rgb", type=str) - # @flags.add_flag("--hsv", type=str) - # @flags.add_flag("--cmyk", type=str) - # @flags.add_flag("--hsl", type=str) - # @flags.add_flag("--hex", type=str) - # @flags.add_flag("--name", type=str) - # @flags.command() - @commands.command(aliases=["color", "colour"]) + @commands.command(aliases=["colour"]) @commands.cooldown(1, 10, commands.cooldowns.BucketType.user) async def color(self, ctx: commands.Context, *, user_color: str) -> None: """Send information on input color code or color name.""" # need to check if user_color is RGB, HSV, CMYK, HSL, Hex or color name # should we assume the color is RGB if not defined? - # should discord tags be used? - # need to review discord.py V2.0 - - # TODO code to check if color code is possible - await ctx.send(embed=COLOR_ERROR.format(color=user_color)) - # await ctx.send(embed=COLOR_EMBED.format( - # RGB=color_dict["RGB"], - # HSV=color_dict["HSV"], - # HSL=color_dict["HSL"], - # CMYK=color_dict["CMYK"], - # HSL=color_dict["HSL"], - # Hex=color_dict["Hex"], - # color_name=color_dict["color_name"] - # ).set_image() # url for image? - # ) - - # TODO pass for now - pass + + if "#" in user_color: + logger.info(f"{user_color = }") + hex_match = re.search(r"^#(?:[0-9a-fA-F]{3}){1,2}$", user_color) + if hex_match: + hex_color = int(hex(int(user_color.replace("#", ""), 16)), 0) + logger.info(f"{hex_color = }") + rgb_color = ImageColor.getcolor(user_color, "RGB") + else: + await ctx.send(embed=Embed( + title="An error has occured.", + description=ERROR_MSG.format(user_color=user_color) + ) + ) + + elif "RGB" or "rgb" in user_color: + rgb_parse = user_color.split() + rgb = rgb_parse[1:].replace(", ", "") + logger.info(f"{rgb = }") + logger.info(f"{rgb[0] = }") + logger.info(f"{rgb[1] = }") + logger.info(f"{rgb[2] = }") + rgb_color = tuple(rgb) + hex_color = f"0x{int(rgb[0]):02x}{int(rgb[1]):02x}{int(rgb[2]):02x}" + + main_embed = Embed( + title=user_color, + color=hex_color, + ) + async with ctx.typing(): + file = await self._create_thumbnail_attachment(rgb_color) + main_embed.set_thumbnail(url="attachment://color.png") + + await ctx.send(file=file, embed=main_embed) + + async def _create_thumbnail_attachment(self, color: str) -> File: + """Generate a thumbnail from `color`.""" + + thumbnail = Image.new("RGB", (100, 100), color=color) + bufferedio = BytesIO() + thumbnail.save(bufferedio, format="PNG") + bufferedio.seek(0) + + file = File(bufferedio, filename="color.png") + + return file + # if user_color in color_lists: # # TODO fuzzy match for color # pass - async def color_converter(self, color: str, code_type: str) -> dict: - """Generate alternative color codes for use in the embed.""" - # TODO add code to take color and code type and return other types - # color_dict = { - # "RGB": color_RGB, - # "HSV": color_HSV, - # "HSL": color_HSL, - # "CMYK": color_CMYK, - # "HSL": color_HSL, - # "Hex": color_Hex, - # "color_name": color_name, - # } - pass - - async def photo_generator(self, color: str) -> None: - """Generate photo to use in embed.""" - # TODO need to find a way to store photo in cache to add to embed, then remove def setup(bot: Bot) -> None: -- cgit v1.2.3 From 1ee1fb10570b55561fc9b8bf42c01a5cc7e8171a Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Tue, 7 Sep 2021 07:39:21 -0400 Subject: Fixing flake8 errors, code style Still a work in progress but commenting out stub code and unused imports. List of To-Do's still applies. --- bot/exts/utilities/color.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 1a4f7031..b1a77b28 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -1,5 +1,5 @@ # imports -import colorsys +# import colorsys import logging import re from io import BytesIO @@ -7,10 +7,10 @@ from io import BytesIO from PIL import Image, ImageColor from discord import Embed, File from discord.ext import commands -from rapidfuzz import process +# from rapidfuzz import process from bot.bot import Bot -from bot.constants import Colours +# from bot.constants import Colours logger = logging.getLogger(__name__) @@ -46,12 +46,14 @@ class Color(commands.Cog): hex_color = int(hex(int(user_color.replace("#", ""), 16)), 0) logger.info(f"{hex_color = }") rgb_color = ImageColor.getcolor(user_color, "RGB") + logger.info(f"{rgb_color = }") else: - await ctx.send(embed=Embed( + await ctx.send( + embed=Embed( title="An error has occured.", - description=ERROR_MSG.format(user_color=user_color) - ) + description=ERROR_MSG.format(user_color=user_color), ) + ) elif "RGB" or "rgb" in user_color: rgb_parse = user_color.split() @@ -64,7 +66,7 @@ class Color(commands.Cog): hex_color = f"0x{int(rgb[0]):02x}{int(rgb[1]):02x}{int(rgb[2]):02x}" main_embed = Embed( - title=user_color, + title=user_color, # need to replace with fuzzymatch color name color=hex_color, ) async with ctx.typing(): @@ -75,7 +77,6 @@ class Color(commands.Cog): async def _create_thumbnail_attachment(self, color: str) -> File: """Generate a thumbnail from `color`.""" - thumbnail = Image.new("RGB", (100, 100), color=color) bufferedio = BytesIO() thumbnail.save(bufferedio, format="PNG") @@ -85,11 +86,8 @@ class Color(commands.Cog): return file - # if user_color in color_lists: - # # TODO fuzzy match for color - # pass - + # # fuzzy match for color def setup(bot: Bot) -> None: -- cgit v1.2.3 From 0862f56c56ebf2be46b2eb2ee66af52f5c12d70c Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Tue, 7 Sep 2021 07:48:07 -0400 Subject: Add embed fields for Hex and RGB --- bot/exts/utilities/color.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index b1a77b28..b1b423e7 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -73,6 +73,16 @@ class Color(commands.Cog): file = await self._create_thumbnail_attachment(rgb_color) main_embed.set_thumbnail(url="attachment://color.png") + main_embed.add_field( + name="Hex", + value=f">>Hex #{hex_color}", + inline=False, + ) + main_embed.add_field( + name="RGB", + value=f">>RGB {rgb_color}", + inline=False, + ) await ctx.send(file=file, embed=main_embed) async def _create_thumbnail_attachment(self, color: str) -> File: -- cgit v1.2.3 From a97803d05ecadf6621f943c068000f0997c4704a Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Tue, 7 Sep 2021 12:51:45 -0400 Subject: Update code to use 'mode' variable Updated the code to parse user_input depending on the color code 'mode' passed to the command. Added stub code for future color codes and embeds if mode is None or wrong code. --- bot/exts/utilities/color.py | 59 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index b1b423e7..1efacead 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -10,7 +10,7 @@ from discord.ext import commands # from rapidfuzz import process from bot.bot import Bot -# from bot.constants import Colours +from bot.constants import Colours logger = logging.getLogger(__name__) @@ -33,13 +33,12 @@ class Color(commands.Cog): self.bot = bot @commands.command(aliases=["colour"]) - @commands.cooldown(1, 10, commands.cooldowns.BucketType.user) - async def color(self, ctx: commands.Context, *, user_color: str) -> None: + async def color(self, ctx: commands.Context, mode: str, user_color: str) -> None: """Send information on input color code or color name.""" # need to check if user_color is RGB, HSV, CMYK, HSL, Hex or color name # should we assume the color is RGB if not defined? - if "#" in user_color: + if mode.lower() == "hex": logger.info(f"{user_color = }") hex_match = re.search(r"^#(?:[0-9a-fA-F]{3}){1,2}$", user_color) if hex_match: @@ -55,15 +54,32 @@ class Color(commands.Cog): ) ) - elif "RGB" or "rgb" in user_color: - rgb_parse = user_color.split() - rgb = rgb_parse[1:].replace(", ", "") - logger.info(f"{rgb = }") - logger.info(f"{rgb[0] = }") - logger.info(f"{rgb[1] = }") - logger.info(f"{rgb[2] = }") - rgb_color = tuple(rgb) - hex_color = f"0x{int(rgb[0]):02x}{int(rgb[1]):02x}{int(rgb[2]):02x}" + elif mode.lower() == "rgb": + logger.info(f"{user_color = }") + # rgb_color = user_color + + elif mode.lower() == "hsv": + pass + elif mode.lower() == "hsl": + pass + elif mode.lower() == "cmyk": + pass + else: + # mode is either None or an invalid code + if mode is None: + no_mode_embed = Embed( + title="No 'mode' was passed, please define a color code.", + color=Colours.soft_red, + ) + await ctx.send(embed=no_mode_embed) + return + wrong_mode_embed = Embed( + title=f"The color code {mode} is not a valid option", + description="Possible modes are: Hex, RGB, HSV, HSL and CMYK.", + color=Colours.soft_red, + ) + await ctx.send(embed=wrong_mode_embed) + return main_embed = Embed( title=user_color, # need to replace with fuzzymatch color name @@ -83,6 +99,23 @@ class Color(commands.Cog): value=f">>RGB {rgb_color}", inline=False, ) + """ + main_embed.add_field( + name="HSV", + value=f">>HSV {hsv_color}", + inline=False, + ) + main_embed.add_field( + name="HSL", + value=f">>HSL {hsl_color}", + inline=False, + ) + main_embed.add_field( + name="CMYK", + value=f">>CMYK {cmyk_color}", + inline=False, + ) + """ await ctx.send(file=file, embed=main_embed) async def _create_thumbnail_attachment(self, color: str) -> File: -- cgit v1.2.3 From 815f3b56933d7a43dbc93ad357d81febf576a286 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Wed, 8 Sep 2021 10:06:47 -0400 Subject: Minor fixes --- bot/exts/utilities/color.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 1efacead..d7fff503 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -1,4 +1,3 @@ -# imports # import colorsys import logging import re @@ -35,16 +34,14 @@ class Color(commands.Cog): @commands.command(aliases=["colour"]) async def color(self, ctx: commands.Context, mode: str, user_color: str) -> None: """Send information on input color code or color name.""" - # need to check if user_color is RGB, HSV, CMYK, HSL, Hex or color name - # should we assume the color is RGB if not defined? - + logger.info(f"{mode = }") + logger.info(f"{user_color = }") if mode.lower() == "hex": - logger.info(f"{user_color = }") hex_match = re.search(r"^#(?:[0-9a-fA-F]{3}){1,2}$", user_color) if hex_match: hex_color = int(hex(int(user_color.replace("#", ""), 16)), 0) - logger.info(f"{hex_color = }") rgb_color = ImageColor.getcolor(user_color, "RGB") + logger.info(f"{hex_color = }") logger.info(f"{rgb_color = }") else: await ctx.send( @@ -53,11 +50,8 @@ class Color(commands.Cog): description=ERROR_MSG.format(user_color=user_color), ) ) - elif mode.lower() == "rgb": - logger.info(f"{user_color = }") - # rgb_color = user_color - + pass elif mode.lower() == "hsv": pass elif mode.lower() == "hsl": @@ -66,6 +60,7 @@ class Color(commands.Cog): pass else: # mode is either None or an invalid code + # need to handle whether user passes color name if mode is None: no_mode_embed = Embed( title="No 'mode' was passed, please define a color code.", -- cgit v1.2.3 From df4b672916563b8cde98e4d5e332bcb3757d3461 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Sat, 11 Sep 2021 09:11:28 -0400 Subject: Move .json file to correct folder --- bot/resources/fun/ryanzec_colours.json | 1568 -------------------------- bot/resources/utilities/ryanzec_colours.json | 1568 ++++++++++++++++++++++++++ 2 files changed, 1568 insertions(+), 1568 deletions(-) delete mode 100644 bot/resources/fun/ryanzec_colours.json create mode 100644 bot/resources/utilities/ryanzec_colours.json diff --git a/bot/resources/fun/ryanzec_colours.json b/bot/resources/fun/ryanzec_colours.json deleted file mode 100644 index 7b89f052..00000000 --- a/bot/resources/fun/ryanzec_colours.json +++ /dev/null @@ -1,1568 +0,0 @@ -{ - "Abbey": "4C4F56", - "Acadia": "1B1404", - "Acapulco": "7CB0A1", - "Aero Blue": "C9FFE5", - "Affair": "714693", - "Akaroa": "D4C4A8", - "Alabaster": "FAFAFA", - "Albescent White": "F5E9D3", - "Algae Green": "93DFB8", - "Alice Blue": "F0F8FF", - "Alizarin Crimson": "E32636", - "Allports": "0076A3", - "Almond": "EED9C4", - "Almond Frost": "907B71", - "Alpine": "AF8F2C", - "Alto": "DBDBDB", - "Aluminium": "A9ACB6", - "Amaranth": "E52B50", - "Amazon": "3B7A57", - "Amber": "FFBF00", - "Americano": "87756E", - "Amethyst": "9966CC", - "Amethyst Smoke": "A397B4", - "Amour": "F9EAF3", - "Amulet": "7B9F80", - "Anakiwa": "9DE5FF", - "Antique Brass": "C88A65", - "Antique Bronze": "704A07", - "Anzac": "E0B646", - "Apache": "DFBE6F", - "Apple": "4FA83D", - "Apple Blossom": "AF4D43", - "Apple Green": "E2F3EC", - "Apricot": "EB9373", - "Apricot Peach": "FBCEB1", - "Apricot White": "FFFEEC", - "Aqua Deep": "014B43", - "Aqua Forest": "5FA777", - "Aqua Haze": "EDF5F5", - "Aqua Island": "A1DAD7", - "Aqua Spring": "EAF9F5", - "Aqua Squeeze": "E8F5F2", - "Aquamarine": "7FFFD4", - "Aquamarine Blue": "71D9E2", - "Arapawa": "110C6C", - "Armadillo": "433E37", - "Arrowtown": "948771", - "Ash": "C6C3B5", - "Asparagus": "7BA05B", - "Asphalt": "130A06", - "Astra": "FAEAB9", - "Astral": "327DA0", - "Astronaut": "283A77", - "Astronaut Blue": "013E62", - "Athens Gray": "EEF0F3", - "Aths Special": "ECEBCE", - "Atlantis": "97CD2D", - "Atoll": "0A6F75", - "Atomic Tangerine": "FF9966", - "Au Chico": "97605D", - "Aubergine": "3B0910", - "Australian Mint": "F5FFBE", - "Avocado": "888D65", - "Axolotl": "4E6649", - "Azalea": "F7C8DA", - "Aztec": "0D1C19", - "Azure": "315BA1", - "Azure Radiance": "007FFF", - "Baby Blue": "E0FFFF", - "Bahama Blue": "026395", - "Bahia": "A5CB0C", - "Baja White": "FFF8D1", - "Bali Hai": "859FAF", - "Baltic Sea": "2A2630", - "Bamboo": "DA6304", - "Banana Mania": "FBE7B2", - "Bandicoot": "858470", - "Barberry": "DED717", - "Barley Corn": "A68B5B", - "Barley White": "FFF4CE", - "Barossa": "44012D", - "Bastille": "292130", - "Battleship Gray": "828F72", - "Bay Leaf": "7DA98D", - "Bay of Many": "273A81", - "Bazaar": "98777B", - "Bean ": "3D0C02", - "Beauty Bush": "EEC1BE", - "Beaver": "926F5B", - "Beeswax": "FEF2C7", - "Beige": "F5F5DC", - "Bermuda": "7DD8C6", - "Bermuda Gray": "6B8BA2", - "Beryl Green": "DEE5C0", - "Bianca": "FCFBF3", - "Big Stone": "162A40", - "Bilbao": "327C14", - "Biloba Flower": "B2A1EA", - "Birch": "373021", - "Bird Flower": "D4CD16", - "Biscay": "1B3162", - "Bismark": "497183", - "Bison Hide": "C1B7A4", - "Bistre": "3D2B1F", - "Bitter": "868974", - "Bitter Lemon": "CAE00D", - "Bittersweet": "FE6F5E", - "Bizarre": "EEDEDA", - "Black": "000000", - "Black Bean": "081910", - "Black Forest": "0B1304", - "Black Haze": "F6F7F7", - "Black Marlin": "3E2C1C", - "Black Olive": "242E16", - "Black Pearl": "041322", - "Black Rock": "0D0332", - "Black Rose": "67032D", - "Black Russian": "0A001C", - "Black Squeeze": "F2FAFA", - "Black White": "FFFEF6", - "Blackberry": "4D0135", - "Blackcurrant": "32293A", - "Blaze Orange": "FF6600", - "Bleach White": "FEF3D8", - "Bleached Cedar": "2C2133", - "Blizzard Blue": "A3E3ED", - "Blossom": "DCB4BC", - "Blue": "0000FF", - "Blue Bayoux": "496679", - "Blue Bell": "9999CC", - "Blue Chalk": "F1E9FF", - "Blue Charcoal": "010D1A", - "Blue Chill": "0C8990", - "Blue Diamond": "380474", - "Blue Dianne": "204852", - "Blue Gem": "2C0E8C", - "Blue Haze": "BFBED8", - "Blue Lagoon": "017987", - "Blue Marguerite": "7666C6", - "Blue Ribbon": "0066FF", - "Blue Romance": "D2F6DE", - "Blue Smoke": "748881", - "Blue Stone": "016162", - "Blue Violet": "6456B7", - "Blue Whale": "042E4C", - "Blue Zodiac": "13264D", - "Blumine": "18587A", - "Blush": "B44668", - "Blush Pink": "FF6FFF", - "Bombay": "AFB1B8", - "Bon Jour": "E5E0E1", - "Bondi Blue": "0095B6", - "Bone": "E4D1C0", - "Bordeaux": "5C0120", - "Bossanova": "4E2A5A", - "Boston Blue": "3B91B4", - "Botticelli": "C7DDE5", - "Bottle Green": "093624", - "Boulder": "7A7A7A", - "Bouquet": "AE809E", - "Bourbon": "BA6F1E", - "Bracken": "4A2A04", - "Brandy": "DEC196", - "Brandy Punch": "CD8429", - "Brandy Rose": "BB8983", - "Breaker Bay": "5DA19F", - "Brick Red": "C62D42", - "Bridal Heath": "FFFAF4", - "Bridesmaid": "FEF0EC", - "Bright Gray": "3C4151", - "Bright Green": "66FF00", - "Bright Red": "B10000", - "Bright Sun": "FED33C", - "Bright Turquoise": "08E8DE", - "Brilliant Rose": "F653A6", - "Brink Pink": "FB607F", - "Bronco": "ABA196", - "Bronze": "3F2109", - "Bronze Olive": "4E420C", - "Bronzetone": "4D400F", - "Broom": "FFEC13", - "Brown": "964B00", - "Brown Bramble": "592804", - "Brown Derby": "492615", - "Brown Pod": "401801", - "Brown Rust": "AF593E", - "Brown Tumbleweed": "37290E", - "Bubbles": "E7FEFF", - "Buccaneer": "622F30", - "Bud": "A8AE9C", - "Buddha Gold": "C1A004", - "Buff": "F0DC82", - "Bulgarian Rose": "480607", - "Bull Shot": "864D1E", - "Bunker": "0D1117", - "Bunting": "151F4C", - "Burgundy": "900020", - "Burnham": "002E20", - "Burning Orange": "FF7034", - "Burning Sand": "D99376", - "Burnt Maroon": "420303", - "Burnt Orange": "CC5500", - "Burnt Sienna": "E97451", - "Burnt Umber": "8A3324", - "Bush": "0D2E1C", - "Buttercup": "F3AD16", - "Buttered Rum": "A1750D", - "Butterfly Bush": "624E9A", - "Buttermilk": "FFF1B5", - "Buttery White": "FFFCEA", - "Cab Sav": "4D0A18", - "Cabaret": "D94972", - "Cabbage Pont": "3F4C3A", - "Cactus": "587156", - "Cadet Blue": "A9B2C3", - "Cadillac": "B04C6A", - "Cafe Royale": "6F440C", - "Calico": "E0C095", - "California": "FE9D04", - "Calypso": "31728D", - "Camarone": "00581A", - "Camelot": "893456", - "Cameo": "D9B99B", - "Camouflage": "3C3910", - "Camouflage Green": "78866B", - "Can Can": "D591A4", - "Canary": "F3FB62", - "Candlelight": "FCD917", - "Candy Corn": "FBEC5D", - "Cannon Black": "251706", - "Cannon Pink": "894367", - "Cape Cod": "3C4443", - "Cape Honey": "FEE5AC", - "Cape Palliser": "A26645", - "Caper": "DCEDB4", - "Caramel": "FFDDAF", - "Cararra": "EEEEE8", - "Cardin Green": "01361C", - "Cardinal": "C41E3A", - "Cardinal Pink": "8C055E", - "Careys Pink": "D29EAA", - "Caribbean Green": "00CC99", - "Carissma": "EA88A8", - "Carla": "F3FFD8", - "Carmine": "960018", - "Carnaby Tan": "5C2E01", - "Carnation": "F95A61", - "Carnation Pink": "FFA6C9", - "Carousel Pink": "F9E0ED", - "Carrot Orange": "ED9121", - "Casablanca": "F8B853", - "Casal": "2F6168", - "Cascade": "8BA9A5", - "Cashmere": "E6BEA5", - "Casper": "ADBED1", - "Castro": "52001F", - "Catalina Blue": "062A78", - "Catskill White": "EEF6F7", - "Cavern Pink": "E3BEBE", - "Cedar": "3E1C14", - "Cedar Wood Finish": "711A00", - "Celadon": "ACE1AF", - "Celery": "B8C25D", - "Celeste": "D1D2CA", - "Cello": "1E385B", - "Celtic": "163222", - "Cement": "8D7662", - "Ceramic": "FCFFF9", - "Cerise": "DA3287", - "Cerise Red": "DE3163", - "Cerulean": "02A4D3", - "Cerulean Blue": "2A52BE", - "Chablis": "FFF4F3", - "Chalet Green": "516E3D", - "Chalky": "EED794", - "Chambray": "354E8C", - "Chamois": "EDDCB1", - "Champagne": "FAECCC", - "Chantilly": "F8C3DF", - "Charade": "292937", - "Chardon": "FFF3F1", - "Chardonnay": "FFCD8C", - "Charlotte": "BAEEF9", - "Charm": "D47494", - "Chartreuse": "7FFF00", - "Chartreuse Yellow": "DFFF00", - "Chateau Green": "40A860", - "Chatelle": "BDB3C7", - "Chathams Blue": "175579", - "Chelsea Cucumber": "83AA5D", - "Chelsea Gem": "9E5302", - "Chenin": "DFCD6F", - "Cherokee": "FCDA98", - "Cherry Pie": "2A0359", - "Cherrywood": "651A14", - "Cherub": "F8D9E9", - "Chestnut": "B94E48", - "Chestnut Rose": "CD5C5C", - "Chetwode Blue": "8581D9", - "Chicago": "5D5C58", - "Chiffon": "F1FFC8", - "Chilean Fire": "F77703", - "Chilean Heath": "FFFDE6", - "China Ivory": "FCFFE7", - "Chino": "CEC7A7", - "Chinook": "A8E3BD", - "Chocolate": "370202", - "Christalle": "33036B", - "Christi": "67A712", - "Christine": "E7730A", - "Chrome White": "E8F1D4", - "Cinder": "0E0E18", - "Cinderella": "FDE1DC", - "Cinnabar": "E34234", - "Cinnamon": "7B3F00", - "Cioccolato": "55280C", - "Citrine White": "FAF7D6", - "Citron": "9EA91F", - "Citrus": "A1C50A", - "Clairvoyant": "480656", - "Clam Shell": "D4B6AF", - "Claret": "7F1734", - "Classic Rose": "FBCCE7", - "Clay Ash": "BDC8B3", - "Clay Creek": "8A8360", - "Clear Day": "E9FFFD", - "Clementine": "E96E00", - "Clinker": "371D09", - "Cloud": "C7C4BF", - "Cloud Burst": "202E54", - "Cloudy": "ACA59F", - "Clover": "384910", - "Cobalt": "0047AB", - "Cocoa Bean": "481C1C", - "Cocoa Brown": "301F1E", - "Coconut Cream": "F8F7DC", - "Cod Gray": "0B0B0B", - "Coffee": "706555", - "Coffee Bean": "2A140E", - "Cognac": "9F381D", - "Cola": "3F2500", - "Cold Purple": "ABA0D9", - "Cold Turkey": "CEBABA", - "Colonial White": "FFEDBC", - "Comet": "5C5D75", - "Como": "517C66", - "Conch": "C9D9D2", - "Concord": "7C7B7A", - "Concrete": "F2F2F2", - "Confetti": "E9D75A", - "Congo Brown": "593737", - "Congress Blue": "02478E", - "Conifer": "ACDD4D", - "Contessa": "C6726B", - "Copper": "B87333", - "Copper Canyon": "7E3A15", - "Copper Rose": "996666", - "Copper Rust": "944747", - "Copperfield": "DA8A67", - "Coral": "FF7F50", - "Coral Red": "FF4040", - "Coral Reef": "C7BCA2", - "Coral Tree": "A86B6B", - "Corduroy": "606E68", - "Coriander": "C4D0B0", - "Cork": "40291D", - "Corn": "E7BF05", - "Corn Field": "F8FACD", - "Corn Harvest": "8B6B0B", - "Cornflower": "93CCEA", - "Cornflower Blue": "6495ED", - "Cornflower Lilac": "FFB0AC", - "Corvette": "FAD3A2", - "Cosmic": "76395D", - "Cosmos": "FFD8D9", - "Costa Del Sol": "615D30", - "Cotton Candy": "FFB7D5", - "Cotton Seed": "C2BDB6", - "County Green": "01371A", - "Cowboy": "4D282D", - "Crail": "B95140", - "Cranberry": "DB5079", - "Crater Brown": "462425", - "Cream": "FFFDD0", - "Cream Brulee": "FFE5A0", - "Cream Can": "F5C85C", - "Creole": "1E0F04", - "Crete": "737829", - "Crimson": "DC143C", - "Crocodile": "736D58", - "Crown of Thorns": "771F1F", - "Crowshead": "1C1208", - "Cruise": "B5ECDF", - "Crusoe": "004816", - "Crusta": "FD7B33", - "Cumin": "924321", - "Cumulus": "FDFFD5", - "Cupid": "FBBEDA", - "Curious Blue": "2596D1", - "Cutty Sark": "507672", - "Cyan / Aqua": "00FFFF", - "Cyprus": "003E40", - "Daintree": "012731", - "Dairy Cream": "F9E4BC", - "Daisy Bush": "4F2398", - "Dallas": "6E4B26", - "Dandelion": "FED85D", - "Danube": "6093D1", - "Dark Blue": "0000C8", - "Dark Burgundy": "770F05", - "Dark Ebony": "3C2005", - "Dark Fern": "0A480D", - "Dark Tan": "661010", - "Dawn": "A6A29A", - "Dawn Pink": "F3E9E5", - "De York": "7AC488", - "Deco": "D2DA97", - "Deep Blue": "220878", - "Deep Blush": "E47698", - "Deep Bronze": "4A3004", - "Deep Cerulean": "007BA7", - "Deep Cove": "051040", - "Deep Fir": "002900", - "Deep Forest Green": "182D09", - "Deep Koamaru": "1B127B", - "Deep Oak": "412010", - "Deep Sapphire": "082567", - "Deep Sea": "01826B", - "Deep Sea Green": "095859", - "Deep Teal": "003532", - "Del Rio": "B09A95", - "Dell": "396413", - "Delta": "A4A49D", - "Deluge": "7563A8", - "Denim": "1560BD", - "Derby": "FFEED8", - "Desert": "AE6020", - "Desert Sand": "EDC9AF", - "Desert Storm": "F8F8F7", - "Dew": "EAFFFE", - "Di Serria": "DB995E", - "Diesel": "130000", - "Dingley": "5D7747", - "Disco": "871550", - "Dixie": "E29418", - "Dodger Blue": "1E90FF", - "Dolly": "F9FF8B", - "Dolphin": "646077", - "Domino": "8E775E", - "Don Juan": "5D4C51", - "Donkey Brown": "A69279", - "Dorado": "6B5755", - "Double Colonial White": "EEE3AD", - "Double Pearl Lusta": "FCF4D0", - "Double Spanish White": "E6D7B9", - "Dove Gray": "6D6C6C", - "Downriver": "092256", - "Downy": "6FD0C5", - "Driftwood": "AF8751", - "Drover": "FDF7AD", - "Dull Lavender": "A899E6", - "Dune": "383533", - "Dust Storm": "E5CCC9", - "Dusty Gray": "A8989B", - "Eagle": "B6BAA4", - "Earls Green": "C9B93B", - "Early Dawn": "FFF9E6", - "East Bay": "414C7D", - "East Side": "AC91CE", - "Eastern Blue": "1E9AB0", - "Ebb": "E9E3E3", - "Ebony": "0C0B1D", - "Ebony Clay": "26283B", - "Eclipse": "311C17", - "Ecru White": "F5F3E5", - "Ecstasy": "FA7814", - "Eden": "105852", - "Edgewater": "C8E3D7", - "Edward": "A2AEAB", - "Egg Sour": "FFF4DD", - "Egg White": "FFEFC1", - "Eggplant": "614051", - "El Paso": "1E1708", - "El Salva": "8F3E33", - "Electric Lime": "CCFF00", - "Electric Violet": "8B00FF", - "Elephant": "123447", - "Elf Green": "088370", - "Elm": "1C7C7D", - "Emerald": "50C878", - "Eminence": "6C3082", - "Emperor": "514649", - "Empress": "817377", - "Endeavour": "0056A7", - "Energy Yellow": "F8DD5C", - "English Holly": "022D15", - "English Walnut": "3E2B23", - "Envy": "8BA690", - "Equator": "E1BC64", - "Espresso": "612718", - "Eternity": "211A0E", - "Eucalyptus": "278A5B", - "Eunry": "CFA39D", - "Evening Sea": "024E46", - "Everglade": "1C402E", - "Faded Jade": "427977", - "Fair Pink": "FFEFEC", - "Falcon": "7F626D", - "Fall Green": "ECEBBD", - "Falu Red": "801818", - "Fantasy": "FAF3F0", - "Fedora": "796A78", - "Feijoa": "9FDD8C", - "Fern": "63B76C", - "Fern Frond": "657220", - "Fern Green": "4F7942", - "Ferra": "704F50", - "Festival": "FBE96C", - "Feta": "F0FCEA", - "Fiery Orange": "B35213", - "Finch": "626649", - "Finlandia": "556D56", - "Finn": "692D54", - "Fiord": "405169", - "Fire": "AA4203", - "Fire Bush": "E89928", - "Firefly": "0E2A30", - "Flame Pea": "DA5B38", - "Flamenco": "FF7D07", - "Flamingo": "F2552A", - "Flax": "EEDC82", - "Flax Smoke": "7B8265", - "Flesh": "FFCBA4", - "Flint": "6F6A61", - "Flirt": "A2006D", - "Flush Mahogany": "CA3435", - "Flush Orange": "FF7F00", - "Foam": "D8FCFA", - "Fog": "D7D0FF", - "Foggy Gray": "CBCAB6", - "Forest Green": "228B22", - "Forget Me Not": "FFF1EE", - "Fountain Blue": "56B4BE", - "Frangipani": "FFDEB3", - "French Gray": "BDBDC6", - "French Lilac": "ECC7EE", - "French Pass": "BDEDFD", - "French Rose": "F64A8A", - "Fresh Eggplant": "990066", - "Friar Gray": "807E79", - "Fringy Flower": "B1E2C1", - "Froly": "F57584", - "Frost": "EDF5DD", - "Frosted Mint": "DBFFF8", - "Frostee": "E4F6E7", - "Fruit Salad": "4F9D5D", - "Fuchsia Blue": "7A58C1", - "Fuchsia Pink": "C154C1", - "Fuego": "BEDE0D", - "Fuel Yellow": "ECA927", - "Fun Blue": "1959A8", - "Fun Green": "016D39", - "Fuscous Gray": "54534D", - "Fuzzy Wuzzy Brown": "C45655", - "Gable Green": "163531", - "Gallery": "EFEFEF", - "Galliano": "DCB20C", - "Gamboge": "E49B0F", - "Geebung": "D18F1B", - "Genoa": "15736B", - "Geraldine": "FB8989", - "Geyser": "D4DFE2", - "Ghost": "C7C9D5", - "Gigas": "523C94", - "Gimblet": "B8B56A", - "Gin": "E8F2EB", - "Gin Fizz": "FFF9E2", - "Givry": "F8E4BF", - "Glacier": "80B3C4", - "Glade Green": "61845F", - "Go Ben": "726D4E", - "Goblin": "3D7D52", - "Gold": "FFD700", - "Gold Drop": "F18200", - "Gold Sand": "E6BE8A", - "Gold Tips": "DEBA13", - "Golden Bell": "E28913", - "Golden Dream": "F0D52D", - "Golden Fizz": "F5FB3D", - "Golden Glow": "FDE295", - "Golden Grass": "DAA520", - "Golden Sand": "F0DB7D", - "Golden Tainoi": "FFCC5C", - "Goldenrod": "FCD667", - "Gondola": "261414", - "Gordons Green": "0B1107", - "Gorse": "FFF14F", - "Gossamer": "069B81", - "Gossip": "D2F8B0", - "Gothic": "6D92A1", - "Governor Bay": "2F3CB3", - "Grain Brown": "E4D5B7", - "Grandis": "FFD38C", - "Granite Green": "8D8974", - "Granny Apple": "D5F6E3", - "Granny Smith": "84A0A0", - "Granny Smith Apple": "9DE093", - "Grape": "381A51", - "Graphite": "251607", - "Gravel": "4A444B", - "Gray": "808080", - "Gray Asparagus": "465945", - "Gray Chateau": "A2AAB3", - "Gray Nickel": "C3C3BD", - "Gray Nurse": "E7ECE6", - "Gray Olive": "A9A491", - "Gray Suit": "C1BECD", - "Green": "00FF00", - "Green Haze": "01A368", - "Green House": "24500F", - "Green Kelp": "25311C", - "Green Leaf": "436A0D", - "Green Mist": "CBD3B0", - "Green Pea": "1D6142", - "Green Smoke": "A4AF6E", - "Green Spring": "B8C1B1", - "Green Vogue": "032B52", - "Green Waterloo": "101405", - "Green White": "E8EBE0", - "Green Yellow": "ADFF2F", - "Grenadier": "D54600", - "Guardsman Red": "BA0101", - "Gulf Blue": "051657", - "Gulf Stream": "80B3AE", - "Gull Gray": "9DACB7", - "Gum Leaf": "B6D3BF", - "Gumbo": "7CA1A6", - "Gun Powder": "414257", - "Gunsmoke": "828685", - "Gurkha": "9A9577", - "Hacienda": "98811B", - "Hairy Heath": "6B2A14", - "Haiti": "1B1035", - "Half Baked": "85C4CC", - "Half Colonial White": "FDF6D3", - "Half Dutch White": "FEF7DE", - "Half Spanish White": "FEF4DB", - "Half and Half": "FFFEE1", - "Hampton": "E5D8AF", - "Harlequin": "3FFF00", - "Harp": "E6F2EA", - "Harvest Gold": "E0B974", - "Havelock Blue": "5590D9", - "Hawaiian Tan": "9D5616", - "Hawkes Blue": "D4E2FC", - "Heath": "541012", - "Heather": "B7C3D0", - "Heathered Gray": "B6B095", - "Heavy Metal": "2B3228", - "Heliotrope": "DF73FF", - "Hemlock": "5E5D3B", - "Hemp": "907874", - "Hibiscus": "B6316C", - "Highland": "6F8E63", - "Hillary": "ACA586", - "Himalaya": "6A5D1B", - "Hint of Green": "E6FFE9", - "Hint of Red": "FBF9F9", - "Hint of Yellow": "FAFDE4", - "Hippie Blue": "589AAF", - "Hippie Green": "53824B", - "Hippie Pink": "AE4560", - "Hit Gray": "A1ADB5", - "Hit Pink": "FFAB81", - "Hokey Pokey": "C8A528", - "Hoki": "65869F", - "Holly": "011D13", - "Hollywood Cerise": "F400A1", - "Honey Flower": "4F1C70", - "Honeysuckle": "EDFC84", - "Hopbush": "D06DA1", - "Horizon": "5A87A0", - "Horses Neck": "604913", - "Hot Cinnamon": "D2691E", - "Hot Pink": "FF69B4", - "Hot Toddy": "B38007", - "Humming Bird": "CFF9F3", - "Hunter Green": "161D10", - "Hurricane": "877C7B", - "Husk": "B7A458", - "Ice Cold": "B1F4E7", - "Iceberg": "DAF4F0", - "Illusion": "F6A4C9", - "Inch Worm": "B0E313", - "Indian Khaki": "C3B091", - "Indian Tan": "4D1E01", - "Indigo": "4F69C6", - "Indochine": "C26B03", - "International Klein Blue": "002FA7", - "International Orange": "FF4F00", - "Irish Coffee": "5F3D26", - "Iroko": "433120", - "Iron": "D4D7D9", - "Ironside Gray": "676662", - "Ironstone": "86483C", - "Island Spice": "FFFCEE", - "Ivory": "FFFFF0", - "Jacaranda": "2E0329", - "Jacarta": "3A2A6A", - "Jacko Bean": "2E1905", - "Jacksons Purple": "20208D", - "Jade": "00A86B", - "Jaffa": "EF863F", - "Jagged Ice": "C2E8E5", - "Jagger": "350E57", - "Jaguar": "080110", - "Jambalaya": "5B3013", - "Janna": "F4EBD3", - "Japanese Laurel": "0A6906", - "Japanese Maple": "780109", - "Japonica": "D87C63", - "Java": "1FC2C2", - "Jazzberry Jam": "A50B5E", - "Jelly Bean": "297B9A", - "Jet Stream": "B5D2CE", - "Jewel": "126B40", - "Jon": "3B1F1F", - "Jonquil": "EEFF9A", - "Jordy Blue": "8AB9F1", - "Judge Gray": "544333", - "Jumbo": "7C7B82", - "Jungle Green": "29AB87", - "Jungle Mist": "B4CFD3", - "Juniper": "6D9292", - "Just Right": "ECCDB9", - "Kabul": "5E483E", - "Kaitoke Green": "004620", - "Kangaroo": "C6C8BD", - "Karaka": "1E1609", - "Karry": "FFEAD4", - "Kashmir Blue": "507096", - "Kelp": "454936", - "Kenyan Copper": "7C1C05", - "Keppel": "3AB09E", - "Key Lime Pie": "BFC921", - "Khaki": "F0E68C", - "Kidnapper": "E1EAD4", - "Kilamanjaro": "240C02", - "Killarney": "3A6A47", - "Kimberly": "736C9F", - "Kingfisher Daisy": "3E0480", - "Kobi": "E79FC4", - "Kokoda": "6E6D57", - "Korma": "8F4B0E", - "Koromiko": "FFBD5F", - "Kournikova": "FFE772", - "Kumera": "886221", - "La Palma": "368716", - "La Rioja": "B3C110", - "Las Palmas": "C6E610", - "Laser": "C8B568", - "Laser Lemon": "FFFF66", - "Laurel": "749378", - "Lavender": "B57EDC", - "Lavender Gray": "BDBBD7", - "Lavender Magenta": "EE82EE", - "Lavender Pink": "FBAED2", - "Lavender Purple": "967BB6", - "Lavender Rose": "FBA0E3", - "Lavender blush": "FFF0F5", - "Leather": "967059", - "Lemon": "FDE910", - "Lemon Chiffon": "FFFACD", - "Lemon Ginger": "AC9E22", - "Lemon Grass": "9B9E8F", - "Light Apricot": "FDD5B1", - "Light Orchid": "E29CD2", - "Light Wisteria": "C9A0DC", - "Lightning Yellow": "FCC01E", - "Lilac": "C8A2C8", - "Lilac Bush": "9874D3", - "Lily": "C8AABF", - "Lily White": "E7F8FF", - "Lima": "76BD17", - "Lime": "BFFF00", - "Limeade": "6F9D02", - "Limed Ash": "747D63", - "Limed Oak": "AC8A56", - "Limed Spruce": "394851", - "Linen": "FAF0E6", - "Link Water": "D9E4F5", - "Lipstick": "AB0563", - "Lisbon Brown": "423921", - "Livid Brown": "4D282E", - "Loafer": "EEF4DE", - "Loblolly": "BDC9CE", - "Lochinvar": "2C8C84", - "Lochmara": "007EC7", - "Locust": "A8AF8E", - "Log Cabin": "242A1D", - "Logan": "AAA9CD", - "Lola": "DFCFDB", - "London Hue": "BEA6C3", - "Lonestar": "6D0101", - "Lotus": "863C3C", - "Loulou": "460B41", - "Lucky": "AF9F1C", - "Lucky Point": "1A1A68", - "Lunar Green": "3C493A", - "Luxor Gold": "A7882C", - "Lynch": "697E9A", - "Mabel": "D9F7FF", - "Macaroni and Cheese": "FFB97B", - "Madang": "B7F0BE", - "Madison": "09255D", - "Madras": "3F3002", - "Magenta / Fuchsia": "FF00FF", - "Magic Mint": "AAF0D1", - "Magnolia": "F8F4FF", - "Mahogany": "4E0606", - "Mai Tai": "B06608", - "Maize": "F5D5A0", - "Makara": "897D6D", - "Mako": "444954", - "Malachite": "0BDA51", - "Malibu": "7DC8F7", - "Mallard": "233418", - "Malta": "BDB2A1", - "Mamba": "8E8190", - "Manatee": "8D90A1", - "Mandalay": "AD781B", - "Mandy": "E25465", - "Mandys Pink": "F2C3B2", - "Mango Tango": "E77200", - "Manhattan": "F5C999", - "Mantis": "74C365", - "Mantle": "8B9C90", - "Manz": "EEEF78", - "Mardi Gras": "350036", - "Marigold": "B98D28", - "Marigold Yellow": "FBE870", - "Mariner": "286ACD", - "Maroon": "800000", - "Maroon Flush": "C32148", - "Maroon Oak": "520C17", - "Marshland": "0B0F08", - "Martini": "AFA09E", - "Martinique": "363050", - "Marzipan": "F8DB9D", - "Masala": "403B38", - "Matisse": "1B659D", - "Matrix": "B05D54", - "Matterhorn": "4E3B41", - "Mauve": "E0B0FF", - "Mauvelous": "F091A9", - "Maverick": "D8C2D5", - "Medium Carmine": "AF4035", - "Medium Purple": "9370DB", - "Medium Red Violet": "BB3385", - "Melanie": "E4C2D5", - "Melanzane": "300529", - "Melon": "FEBAAD", - "Melrose": "C7C1FF", - "Mercury": "E5E5E5", - "Merino": "F6F0E6", - "Merlin": "413C37", - "Merlot": "831923", - "Metallic Bronze": "49371B", - "Metallic Copper": "71291D", - "Meteor": "D07D12", - "Meteorite": "3C1F76", - "Mexican Red": "A72525", - "Mid Gray": "5F5F6E", - "Midnight": "011635", - "Midnight Blue": "003366", - "Midnight Moss": "041004", - "Mikado": "2D2510", - "Milan": "FAFFA4", - "Milano Red": "B81104", - "Milk Punch": "FFF6D4", - "Millbrook": "594433", - "Mimosa": "F8FDD3", - "Mindaro": "E3F988", - "Mine Shaft": "323232", - "Mineral Green": "3F5D53", - "Ming": "36747D", - "Minsk": "3F307F", - "Mint Green": "98FF98", - "Mint Julep": "F1EEC1", - "Mint Tulip": "C4F4EB", - "Mirage": "161928", - "Mischka": "D1D2DD", - "Mist Gray": "C4C4BC", - "Mobster": "7F7589", - "Moccaccino": "6E1D14", - "Mocha": "782D19", - "Mojo": "C04737", - "Mona Lisa": "FFA194", - "Monarch": "8B0723", - "Mondo": "4A3C30", - "Mongoose": "B5A27F", - "Monsoon": "8A8389", - "Monte Carlo": "83D0C6", - "Monza": "C7031E", - "Moody Blue": "7F76D3", - "Moon Glow": "FCFEDA", - "Moon Mist": "DCDDCC", - "Moon Raker": "D6CEF6", - "Morning Glory": "9EDEE0", - "Morocco Brown": "441D00", - "Mortar": "504351", - "Mosque": "036A6E", - "Moss Green": "ADDFAD", - "Mountain Meadow": "1AB385", - "Mountain Mist": "959396", - "Mountbatten Pink": "997A8D", - "Muddy Waters": "B78E5C", - "Muesli": "AA8B5B", - "Mulberry": "C54B8C", - "Mulberry Wood": "5C0536", - "Mule Fawn": "8C472F", - "Mulled Wine": "4E4562", - "Mustard": "FFDB58", - "My Pink": "D69188", - "My Sin": "FFB31F", - "Mystic": "E2EBED", - "Nandor": "4B5D52", - "Napa": "ACA494", - "Narvik": "EDF9F1", - "Natural Gray": "8B8680", - "Navajo White": "FFDEAD", - "Navy Blue": "000080", - "Nebula": "CBDBD6", - "Negroni": "FFE2C5", - "Neon Carrot": "FF9933", - "Nepal": "8EABC1", - "Neptune": "7CB7BB", - "Nero": "140600", - "Nevada": "646E75", - "New Orleans": "F3D69D", - "New York Pink": "D7837F", - "Niagara": "06A189", - "Night Rider": "1F120F", - "Night Shadz": "AA375A", - "Nile Blue": "193751", - "Nobel": "B7B1B1", - "Nomad": "BAB1A2", - "Norway": "A8BD9F", - "Nugget": "C59922", - "Nutmeg": "81422C", - "Nutmeg Wood Finish": "683600", - "Oasis": "FEEFCE", - "Observatory": "02866F", - "Ocean Green": "41AA78", - "Ochre": "CC7722", - "Off Green": "E6F8F3", - "Off Yellow": "FEF9E3", - "Oil": "281E15", - "Old Brick": "901E1E", - "Old Copper": "724A2F", - "Old Gold": "CFB53B", - "Old Lace": "FDF5E6", - "Old Lavender": "796878", - "Old Rose": "C08081", - "Olive": "808000", - "Olive Drab": "6B8E23", - "Olive Green": "B5B35C", - "Olive Haze": "8B8470", - "Olivetone": "716E10", - "Olivine": "9AB973", - "Onahau": "CDF4FF", - "Onion": "2F270E", - "Opal": "A9C6C2", - "Opium": "8E6F70", - "Oracle": "377475", - "Orange": "FF681F", - "Orange Peel": "FFA000", - "Orange Roughy": "C45719", - "Orange White": "FEFCED", - "Orchid": "DA70D6", - "Orchid White": "FFFDF3", - "Oregon": "9B4703", - "Orient": "015E85", - "Oriental Pink": "C69191", - "Orinoco": "F3FBD4", - "Oslo Gray": "878D91", - "Ottoman": "E9F8ED", - "Outer Space": "2D383A", - "Outrageous Orange": "FF6037", - "Oxford Blue": "384555", - "Oxley": "779E86", - "Oyster Bay": "DAFAFF", - "Oyster Pink": "E9CECD", - "Paarl": "A65529", - "Pablo": "776F61", - "Pacific Blue": "009DC4", - "Pacifika": "778120", - "Paco": "411F10", - "Padua": "ADE6C4", - "Pale Canary": "FFFF99", - "Pale Leaf": "C0D3B9", - "Pale Oyster": "988D77", - "Pale Prim": "FDFEB8", - "Pale Rose": "FFE1F2", - "Pale Sky": "6E7783", - "Pale Slate": "C3BFC1", - "Palm Green": "09230F", - "Palm Leaf": "19330E", - "Pampas": "F4F2EE", - "Panache": "EAF6EE", - "Pancho": "EDCDAB", - "Papaya Whip": "FFEFD5", - "Paprika": "8D0226", - "Paradiso": "317D82", - "Parchment": "F1E9D2", - "Paris Daisy": "FFF46E", - "Paris M": "26056A", - "Paris White": "CADCD4", - "Parsley": "134F19", - "Pastel Green": "77DD77", - "Pastel Pink": "FFD1DC", - "Patina": "639A8F", - "Pattens Blue": "DEF5FF", - "Paua": "260368", - "Pavlova": "D7C498", - "Peach": "FFE5B4", - "Peach Cream": "FFF0DB", - "Peach Orange": "FFCC99", - "Peach Schnapps": "FFDCD6", - "Peach Yellow": "FADFAD", - "Peanut": "782F16", - "Pear": "D1E231", - "Pearl Bush": "E8E0D5", - "Pearl Lusta": "FCF4DC", - "Peat": "716B56", - "Pelorous": "3EABBF", - "Peppermint": "E3F5E1", - "Perano": "A9BEF2", - "Perfume": "D0BEF8", - "Periglacial Blue": "E1E6D6", - "Periwinkle": "CCCCFF", - "Periwinkle Gray": "C3CDE6", - "Persian Blue": "1C39BB", - "Persian Green": "00A693", - "Persian Indigo": "32127A", - "Persian Pink": "F77FBE", - "Persian Plum": "701C1C", - "Persian Red": "CC3333", - "Persian Rose": "FE28A2", - "Persimmon": "FF6B53", - "Peru Tan": "7F3A02", - "Pesto": "7C7631", - "Petite Orchid": "DB9690", - "Pewter": "96A8A1", - "Pharlap": "A3807B", - "Picasso": "FFF39D", - "Pickled Bean": "6E4826", - "Pickled Bluewood": "314459", - "Picton Blue": "45B1E8", - "Pig Pink": "FDD7E4", - "Pigeon Post": "AFBDD9", - "Pigment Indigo": "4B0082", - "Pine Cone": "6D5E54", - "Pine Glade": "C7CD90", - "Pine Green": "01796F", - "Pine Tree": "171F04", - "Pink": "FFC0CB", - "Pink Flamingo": "FF66FF", - "Pink Flare": "E1C0C8", - "Pink Lace": "FFDDF4", - "Pink Lady": "FFF1D8", - "Pink Salmon": "FF91A4", - "Pink Swan": "BEB5B7", - "Piper": "C96323", - "Pipi": "FEF4CC", - "Pippin": "FFE1DF", - "Pirate Gold": "BA7F03", - "Pistachio": "9DC209", - "Pixie Green": "C0D8B6", - "Pizazz": "FF9000", - "Pizza": "C99415", - "Plantation": "27504B", - "Plum": "843179", - "Pohutukawa": "8F021C", - "Polar": "E5F9F6", - "Polo Blue": "8DA8CC", - "Pomegranate": "F34723", - "Pompadour": "660045", - "Porcelain": "EFF2F3", - "Porsche": "EAAE69", - "Port Gore": "251F4F", - "Portafino": "FFFFB4", - "Portage": "8B9FEE", - "Portica": "F9E663", - "Pot Pourri": "F5E7E2", - "Potters Clay": "8C5738", - "Powder Ash": "BCC9C2", - "Powder Blue": "B0E0E6", - "Prairie Sand": "9A3820", - "Prelude": "D0C0E5", - "Prim": "F0E2EC", - "Primrose": "EDEA99", - "Provincial Pink": "FEF5F1", - "Prussian Blue": "003153", - "Puce": "CC8899", - "Pueblo": "7D2C14", - "Puerto Rico": "3FC1AA", - "Pumice": "C2CAC4", - "Pumpkin": "FF7518", - "Pumpkin Skin": "B1610B", - "Punch": "DC4333", - "Punga": "4D3D14", - "Purple": "660099", - "Purple Heart": "652DC1", - "Purple Mountain's Majesty": "9678B6", - "Purple Pizzazz": "FF00CC", - "Putty": "E7CD8C", - "Quarter Pearl Lusta": "FFFDF4", - "Quarter Spanish White": "F7F2E1", - "Quicksand": "BD978E", - "Quill Gray": "D6D6D1", - "Quincy": "623F2D", - "Racing Green": "0C1911", - "Radical Red": "FF355E", - "Raffia": "EADAB8", - "Rainee": "B9C8AC", - "Rajah": "F7B668", - "Rangitoto": "2E3222", - "Rangoon Green": "1C1E13", - "Raven": "727B89", - "Raw Sienna": "D27D46", - "Raw Umber": "734A12", - "Razzle Dazzle Rose": "FF33CC", - "Razzmatazz": "E30B5C", - "Rebel": "3C1206", - "Red": "FF0000", - "Red Beech": "7B3801", - "Red Berry": "8E0000", - "Red Damask": "DA6A41", - "Red Devil": "860111", - "Red Orange": "FF3F34", - "Red Oxide": "6E0902", - "Red Ribbon": "ED0A3F", - "Red Robin": "80341F", - "Red Stage": "D05F04", - "Red Violet": "C71585", - "Redwood": "5D1E0F", - "Reef": "C9FFA2", - "Reef Gold": "9F821C", - "Regal Blue": "013F6A", - "Regent Gray": "86949F", - "Regent St Blue": "AAD6E6", - "Remy": "FEEBF3", - "Reno Sand": "A86515", - "Resolution Blue": "002387", - "Revolver": "2C1632", - "Rhino": "2E3F62", - "Rice Cake": "FFFEF0", - "Rice Flower": "EEFFE2", - "Rich Gold": "A85307", - "Rio Grande": "BBD009", - "Ripe Lemon": "F4D81C", - "Ripe Plum": "410056", - "Riptide": "8BE6D8", - "River Bed": "434C59", - "Rob Roy": "EAC674", - "Robin's Egg Blue": "00CCCC", - "Rock": "4D3833", - "Rock Blue": "9EB1CD", - "Rock Spray": "BA450C", - "Rodeo Dust": "C9B29B", - "Rolling Stone": "747D83", - "Roman": "DE6360", - "Roman Coffee": "795D4C", - "Romance": "FFFEFD", - "Romantic": "FFD2B7", - "Ronchi": "ECC54E", - "Roof Terracotta": "A62F20", - "Rope": "8E4D1E", - "Rose": "FF007F", - "Rose Bud": "FBB2A3", - "Rose Bud Cherry": "800B47", - "Rose Fog": "E7BCB4", - "Rose White": "FFF6F5", - "Rose of Sharon": "BF5500", - "Rosewood": "65000B", - "Roti": "C6A84B", - "Rouge": "A23B6C", - "Royal Blue": "4169E1", - "Royal Heath": "AB3472", - "Royal Purple": "6B3FA0", - "Rum": "796989", - "Rum Swizzle": "F9F8E4", - "Russet": "80461B", - "Russett": "755A57", - "Rust": "B7410E", - "Rustic Red": "480404", - "Rusty Nail": "86560A", - "Saddle": "4C3024", - "Saddle Brown": "583401", - "Saffron": "F4C430", - "Saffron Mango": "F9BF58", - "Sage": "9EA587", - "Sahara": "B7A214", - "Sahara Sand": "F1E788", - "Sail": "B8E0F9", - "Salem": "097F4B", - "Salmon": "FF8C69", - "Salomie": "FEDB8D", - "Salt Box": "685E6E", - "Saltpan": "F1F7F2", - "Sambuca": "3A2010", - "San Felix": "0B6207", - "San Juan": "304B6A", - "San Marino": "456CAC", - "Sand Dune": "826F65", - "Sandal": "AA8D6F", - "Sandrift": "AB917A", - "Sandstone": "796D62", - "Sandwisp": "F5E7A2", - "Sandy Beach": "FFEAC8", - "Sandy brown": "F4A460", - "Sangria": "92000A", - "Sanguine Brown": "8D3D38", - "Santa Fe": "B16D52", - "Santas Gray": "9FA0B1", - "Sapling": "DED4A4", - "Sapphire": "2F519E", - "Saratoga": "555B10", - "Satin Linen": "E6E4D4", - "Sauvignon": "FFF5F3", - "Sazerac": "FFF4E0", - "Scampi": "675FA6", - "Scandal": "CFFAF4", - "Scarlet": "FF2400", - "Scarlet Gum": "431560", - "Scarlett": "950015", - "Scarpa Flow": "585562", - "Schist": "A9B497", - "School bus Yellow": "FFD800", - "Schooner": "8B847E", - "Science Blue": "0066CC", - "Scooter": "2EBFD4", - "Scorpion": "695F62", - "Scotch Mist": "FFFBDC", - "Screamin' Green": "66FF66", - "Sea Buckthorn": "FBA129", - "Sea Green": "2E8B57", - "Sea Mist": "C5DBCA", - "Sea Nymph": "78A39C", - "Sea Pink": "ED989E", - "Seagull": "80CCEA", - "Seance": "731E8F", - "Seashell": "F1F1F1", - "Seashell Peach": "FFF5EE", - "Seaweed": "1B2F11", - "Selago": "F0EEFD", - "Selective Yellow": "FFBA00", - "Sepia": "704214", - "Sepia Black": "2B0202", - "Sepia Skin": "9E5B40", - "Serenade": "FFF4E8", - "Shadow": "837050", - "Shadow Green": "9AC2B8", - "Shady Lady": "AAA5A9", - "Shakespeare": "4EABD1", - "Shalimar": "FBFFBA", - "Shamrock": "33CC99", - "Shark": "25272C", - "Sherpa Blue": "004950", - "Sherwood Green": "02402C", - "Shilo": "E8B9B3", - "Shingle Fawn": "6B4E31", - "Ship Cove": "788BBA", - "Ship Gray": "3E3A44", - "Shiraz": "B20931", - "Shocking": "E292C0", - "Shocking Pink": "FC0FC0", - "Shuttle Gray": "5F6672", - "Siam": "646A54", - "Sidecar": "F3E7BB", - "Silk": "BDB1A8", - "Silver": "C0C0C0", - "Silver Chalice": "ACACAC", - "Silver Rust": "C9C0BB", - "Silver Sand": "BFC1C2", - "Silver Tree": "66B58F", - "Sinbad": "9FD7D3", - "Siren": "7A013A", - "Sirocco": "718080", - "Sisal": "D3CBBA", - "Skeptic": "CAE6DA", - "Sky Blue": "76D7EA", - "Slate Gray": "708090", - "Smalt": "003399", - "Smalt Blue": "51808F", - "Smoky": "605B73", - "Snow Drift": "F7FAF7", - "Snow Flurry": "E4FFD1", - "Snowy Mint": "D6FFDB", - "Snuff": "E2D8ED", - "Soapstone": "FFFBF9", - "Soft Amber": "D1C6B4", - "Soft Peach": "F5EDEF", - "Solid Pink": "893843", - "Solitaire": "FEF8E2", - "Solitude": "EAF6FF", - "Sorbus": "FD7C07", - "Sorrell Brown": "CEB98F", - "Soya Bean": "6A6051", - "Spanish Green": "819885", - "Spectra": "2F5A57", - "Spice": "6A442E", - "Spicy Mix": "885342", - "Spicy Mustard": "74640D", - "Spicy Pink": "816E71", - "Spindle": "B6D1EA", - "Spray": "79DEEC", - "Spring Green": "00FF7F", - "Spring Leaves": "578363", - "Spring Rain": "ACCBB1", - "Spring Sun": "F6FFDC", - "Spring Wood": "F8F6F1", - "Sprout": "C1D7B0", - "Spun Pearl": "AAABB7", - "Squirrel": "8F8176", - "St Tropaz": "2D569B", - "Stack": "8A8F8A", - "Star Dust": "9F9F9C", - "Stark White": "E5D7BD", - "Starship": "ECF245", - "Steel Blue": "4682B4", - "Steel Gray": "262335", - "Stiletto": "9C3336", - "Stonewall": "928573", - "Storm Dust": "646463", - "Storm Gray": "717486", - "Stratos": "000741", - "Straw": "D4BF8D", - "Strikemaster": "956387", - "Stromboli": "325D52", - "Studio": "714AB2", - "Submarine": "BAC7C9", - "Sugar Cane": "F9FFF6", - "Sulu": "C1F07C", - "Summer Green": "96BBAB", - "Sun": "FBAC13", - "Sundance": "C9B35B", - "Sundown": "FFB1B3", - "Sunflower": "E4D422", - "Sunglo": "E16865", - "Sunglow": "FFCC33", - "Sunset Orange": "FE4C40", - "Sunshade": "FF9E2C", - "Supernova": "FFC901", - "Surf": "BBD7C1", - "Surf Crest": "CFE5D2", - "Surfie Green": "0C7A79", - "Sushi": "87AB39", - "Suva Gray": "888387", - "Swamp": "001B1C", - "Swamp Green": "ACB78E", - "Swans Down": "DCF0EA", - "Sweet Corn": "FBEA8C", - "Sweet Pink": "FD9FA2", - "Swirl": "D3CDC5", - "Swiss Coffee": "DDD6D5", - "Sycamore": "908D39", - "Tabasco": "A02712", - "Tacao": "EDB381", - "Tacha": "D6C562", - "Tahiti Gold": "E97C07", - "Tahuna Sands": "EEF0C8", - "Tall Poppy": "B32D29", - "Tallow": "A8A589", - "Tamarillo": "991613", - "Tamarind": "341515", - "Tan": "D2B48C", - "Tan Hide": "FA9D5A", - "Tana": "D9DCC1", - "Tangaroa": "03163C", - "Tangerine": "F28500", - "Tango": "ED7A1C", - "Tapa": "7B7874", - "Tapestry": "B05E81", - "Tara": "E1F6E8", - "Tarawera": "073A50", - "Tasman": "CFDCCF", - "Taupe": "483C32", - "Taupe Gray": "B3AF95", - "Tawny Port": "692545", - "Te Papa Green": "1E433C", - "Tea": "C1BAB0", - "Tea Green": "D0F0C0", - "Teak": "B19461", - "Teal": "008080", - "Teal Blue": "044259", - "Temptress": "3B000B", - "Tenn": "CD5700", - "Tequila": "FFE6C7", - "Terracotta": "E2725B", - "Texas": "F8F99C", - "Texas Rose": "FFB555", - "Thatch": "B69D98", - "Thatch Green": "403D19", - "Thistle": "D8BFD8", - "Thistle Green": "CCCAA8", - "Thunder": "33292F", - "Thunderbird": "C02B18", - "Tia Maria": "C1440E", - "Tiara": "C3D1D1", - "Tiber": "063537", - "Tickle Me Pink": "FC80A5", - "Tidal": "F1FFAD", - "Tide": "BFB8B0", - "Timber Green": "16322C", - "Timberwolf": "D9D6CF", - "Titan White": "F0EEFF", - "Toast": "9A6E61", - "Tobacco Brown": "715D47", - "Toledo": "3A0020", - "Tolopea": "1B0245", - "Tom Thumb": "3F583B", - "Tonys Pink": "E79F8C", - "Topaz": "7C778A", - "Torch Red": "FD0E35", - "Torea Bay": "0F2D9E", - "Tory Blue": "1450AA", - "Tosca": "8D3F3F", - "Totem Pole": "991B07", - "Tower Gray": "A9BDBF", - "Tradewind": "5FB3AC", - "Tranquil": "E6FFFF", - "Travertine": "FFFDE8", - "Tree Poppy": "FC9C1D", - "Treehouse": "3B2820", - "Trendy Green": "7C881A", - "Trendy Pink": "8C6495", - "Trinidad": "E64E03", - "Tropical Blue": "C3DDF9", - "Tropical Rain Forest": "00755E", - "Trout": "4A4E5A", - "True V": "8A73D6", - "Tuatara": "363534", - "Tuft Bush": "FFDDCD", - "Tulip Tree": "EAB33B", - "Tumbleweed": "DEA681", - "Tuna": "353542", - "Tundora": "4A4244", - "Turbo": "FAE600", - "Turkish Rose": "B57281", - "Turmeric": "CABB48", - "Turquoise": "30D5C8", - "Turquoise Blue": "6CDAE7", - "Turtle Green": "2A380B", - "Tuscany": "BD5E2E", - "Tusk": "EEF3C3", - "Tussock": "C5994B", - "Tutu": "FFF1F9", - "Twilight": "E4CFDE", - "Twilight Blue": "EEFDFF", - "Twine": "C2955D", - "Tyrian Purple": "66023C", - "Ultramarine": "120A8F", - "Valencia": "D84437", - "Valentino": "350E42", - "Valhalla": "2B194F", - "Van Cleef": "49170C", - "Vanilla": "D1BEA8", - "Vanilla Ice": "F3D9DF", - "Varden": "FFF6DF", - "Venetian Red": "72010F", - "Venice Blue": "055989", - "Venus": "928590", - "Verdigris": "5D5E37", - "Verdun Green": "495400", - "Vermilion": "FF4D00", - "Vesuvius": "B14A0B", - "Victoria": "534491", - "Vida Loca": "549019", - "Viking": "64CCDB", - "Vin Rouge": "983D61", - "Viola": "CB8FA9", - "Violent Violet": "290C5E", - "Violet": "240A40", - "Violet Eggplant": "991199", - "Violet Red": "F7468A", - "Viridian": "40826D", - "Viridian Green": "678975", - "Vis Vis": "FFEFA1", - "Vista Blue": "8FD6B4", - "Vista White": "FCF8F7", - "Vivid Tangerine": "FF9980", - "Vivid Violet": "803790", - "Voodoo": "533455", - "Vulcan": "10121D", - "Wafer": "DECBC6", - "Waikawa Gray": "5A6E9C", - "Waiouru": "363C0D", - "Walnut": "773F1A", - "Wasabi": "788A25", - "Water Leaf": "A1E9DE", - "Watercourse": "056F57", - "Waterloo ": "7B7C94", - "Wattle": "DCD747", - "Watusi": "FFDDCF", - "Wax Flower": "FFC0A8", - "We Peep": "F7DBE6", - "Web Orange": "FFA500", - "Wedgewood": "4E7F9E", - "Well Read": "B43332", - "West Coast": "625119", - "West Side": "FF910F", - "Westar": "DCD9D2", - "Wewak": "F19BAB", - "Wheat": "F5DEB3", - "Wheatfield": "F3EDCF", - "Whiskey": "D59A6F", - "Whisper": "F7F5FA", - "White": "FFFFFF", - "White Ice": "DDF9F1", - "White Lilac": "F8F7FC", - "White Linen": "F8F0E8", - "White Pointer": "FEF8FF", - "White Rock": "EAE8D4", - "Wild Blue Yonder": "7A89B8", - "Wild Rice": "ECE090", - "Wild Sand": "F4F4F4", - "Wild Strawberry": "FF3399", - "Wild Watermelon": "FD5B78", - "Wild Willow": "B9C46A", - "William": "3A686C", - "Willow Brook": "DFECDA", - "Willow Grove": "65745D", - "Windsor": "3C0878", - "Wine Berry": "591D35", - "Winter Hazel": "D5D195", - "Wisp Pink": "FEF4F8", - "Wisteria": "9771B5", - "Wistful": "A4A6D3", - "Witch Haze": "FFFC99", - "Wood Bark": "261105", - "Woodland": "4D5328", - "Woodrush": "302A0F", - "Woodsmoke": "0C0D0F", - "Woody Brown": "483131", - "Xanadu": "738678", - "Yellow": "FFFF00", - "Yellow Green": "C5E17A", - "Yellow Metal": "716338", - "Yellow Orange": "FFAE42", - "Yellow Sea": "FEA904", - "Your Pink": "FFC3C0", - "Yukon Gold": "7B6608", - "Yuma": "CEC291", - "Zambezi": "685558", - "Zanah": "DAECD6", - "Zest": "E5841B", - "Zeus": "292319", - "Ziggurat": "BFDBE2", - "Zinnwaldite": "EBC2AF", - "Zircon": "F4F8FF", - "Zombie": "E4D69B", - "Zorba": "A59B91", - "Zuccini": "044022", - "Zumthor": "EDF6FF" -} diff --git a/bot/resources/utilities/ryanzec_colours.json b/bot/resources/utilities/ryanzec_colours.json new file mode 100644 index 00000000..7b89f052 --- /dev/null +++ b/bot/resources/utilities/ryanzec_colours.json @@ -0,0 +1,1568 @@ +{ + "Abbey": "4C4F56", + "Acadia": "1B1404", + "Acapulco": "7CB0A1", + "Aero Blue": "C9FFE5", + "Affair": "714693", + "Akaroa": "D4C4A8", + "Alabaster": "FAFAFA", + "Albescent White": "F5E9D3", + "Algae Green": "93DFB8", + "Alice Blue": "F0F8FF", + "Alizarin Crimson": "E32636", + "Allports": "0076A3", + "Almond": "EED9C4", + "Almond Frost": "907B71", + "Alpine": "AF8F2C", + "Alto": "DBDBDB", + "Aluminium": "A9ACB6", + "Amaranth": "E52B50", + "Amazon": "3B7A57", + "Amber": "FFBF00", + "Americano": "87756E", + "Amethyst": "9966CC", + "Amethyst Smoke": "A397B4", + "Amour": "F9EAF3", + "Amulet": "7B9F80", + "Anakiwa": "9DE5FF", + "Antique Brass": "C88A65", + "Antique Bronze": "704A07", + "Anzac": "E0B646", + "Apache": "DFBE6F", + "Apple": "4FA83D", + "Apple Blossom": "AF4D43", + "Apple Green": "E2F3EC", + "Apricot": "EB9373", + "Apricot Peach": "FBCEB1", + "Apricot White": "FFFEEC", + "Aqua Deep": "014B43", + "Aqua Forest": "5FA777", + "Aqua Haze": "EDF5F5", + "Aqua Island": "A1DAD7", + "Aqua Spring": "EAF9F5", + "Aqua Squeeze": "E8F5F2", + "Aquamarine": "7FFFD4", + "Aquamarine Blue": "71D9E2", + "Arapawa": "110C6C", + "Armadillo": "433E37", + "Arrowtown": "948771", + "Ash": "C6C3B5", + "Asparagus": "7BA05B", + "Asphalt": "130A06", + "Astra": "FAEAB9", + "Astral": "327DA0", + "Astronaut": "283A77", + "Astronaut Blue": "013E62", + "Athens Gray": "EEF0F3", + "Aths Special": "ECEBCE", + "Atlantis": "97CD2D", + "Atoll": "0A6F75", + "Atomic Tangerine": "FF9966", + "Au Chico": "97605D", + "Aubergine": "3B0910", + "Australian Mint": "F5FFBE", + "Avocado": "888D65", + "Axolotl": "4E6649", + "Azalea": "F7C8DA", + "Aztec": "0D1C19", + "Azure": "315BA1", + "Azure Radiance": "007FFF", + "Baby Blue": "E0FFFF", + "Bahama Blue": "026395", + "Bahia": "A5CB0C", + "Baja White": "FFF8D1", + "Bali Hai": "859FAF", + "Baltic Sea": "2A2630", + "Bamboo": "DA6304", + "Banana Mania": "FBE7B2", + "Bandicoot": "858470", + "Barberry": "DED717", + "Barley Corn": "A68B5B", + "Barley White": "FFF4CE", + "Barossa": "44012D", + "Bastille": "292130", + "Battleship Gray": "828F72", + "Bay Leaf": "7DA98D", + "Bay of Many": "273A81", + "Bazaar": "98777B", + "Bean ": "3D0C02", + "Beauty Bush": "EEC1BE", + "Beaver": "926F5B", + "Beeswax": "FEF2C7", + "Beige": "F5F5DC", + "Bermuda": "7DD8C6", + "Bermuda Gray": "6B8BA2", + "Beryl Green": "DEE5C0", + "Bianca": "FCFBF3", + "Big Stone": "162A40", + "Bilbao": "327C14", + "Biloba Flower": "B2A1EA", + "Birch": "373021", + "Bird Flower": "D4CD16", + "Biscay": "1B3162", + "Bismark": "497183", + "Bison Hide": "C1B7A4", + "Bistre": "3D2B1F", + "Bitter": "868974", + "Bitter Lemon": "CAE00D", + "Bittersweet": "FE6F5E", + "Bizarre": "EEDEDA", + "Black": "000000", + "Black Bean": "081910", + "Black Forest": "0B1304", + "Black Haze": "F6F7F7", + "Black Marlin": "3E2C1C", + "Black Olive": "242E16", + "Black Pearl": "041322", + "Black Rock": "0D0332", + "Black Rose": "67032D", + "Black Russian": "0A001C", + "Black Squeeze": "F2FAFA", + "Black White": "FFFEF6", + "Blackberry": "4D0135", + "Blackcurrant": "32293A", + "Blaze Orange": "FF6600", + "Bleach White": "FEF3D8", + "Bleached Cedar": "2C2133", + "Blizzard Blue": "A3E3ED", + "Blossom": "DCB4BC", + "Blue": "0000FF", + "Blue Bayoux": "496679", + "Blue Bell": "9999CC", + "Blue Chalk": "F1E9FF", + "Blue Charcoal": "010D1A", + "Blue Chill": "0C8990", + "Blue Diamond": "380474", + "Blue Dianne": "204852", + "Blue Gem": "2C0E8C", + "Blue Haze": "BFBED8", + "Blue Lagoon": "017987", + "Blue Marguerite": "7666C6", + "Blue Ribbon": "0066FF", + "Blue Romance": "D2F6DE", + "Blue Smoke": "748881", + "Blue Stone": "016162", + "Blue Violet": "6456B7", + "Blue Whale": "042E4C", + "Blue Zodiac": "13264D", + "Blumine": "18587A", + "Blush": "B44668", + "Blush Pink": "FF6FFF", + "Bombay": "AFB1B8", + "Bon Jour": "E5E0E1", + "Bondi Blue": "0095B6", + "Bone": "E4D1C0", + "Bordeaux": "5C0120", + "Bossanova": "4E2A5A", + "Boston Blue": "3B91B4", + "Botticelli": "C7DDE5", + "Bottle Green": "093624", + "Boulder": "7A7A7A", + "Bouquet": "AE809E", + "Bourbon": "BA6F1E", + "Bracken": "4A2A04", + "Brandy": "DEC196", + "Brandy Punch": "CD8429", + "Brandy Rose": "BB8983", + "Breaker Bay": "5DA19F", + "Brick Red": "C62D42", + "Bridal Heath": "FFFAF4", + "Bridesmaid": "FEF0EC", + "Bright Gray": "3C4151", + "Bright Green": "66FF00", + "Bright Red": "B10000", + "Bright Sun": "FED33C", + "Bright Turquoise": "08E8DE", + "Brilliant Rose": "F653A6", + "Brink Pink": "FB607F", + "Bronco": "ABA196", + "Bronze": "3F2109", + "Bronze Olive": "4E420C", + "Bronzetone": "4D400F", + "Broom": "FFEC13", + "Brown": "964B00", + "Brown Bramble": "592804", + "Brown Derby": "492615", + "Brown Pod": "401801", + "Brown Rust": "AF593E", + "Brown Tumbleweed": "37290E", + "Bubbles": "E7FEFF", + "Buccaneer": "622F30", + "Bud": "A8AE9C", + "Buddha Gold": "C1A004", + "Buff": "F0DC82", + "Bulgarian Rose": "480607", + "Bull Shot": "864D1E", + "Bunker": "0D1117", + "Bunting": "151F4C", + "Burgundy": "900020", + "Burnham": "002E20", + "Burning Orange": "FF7034", + "Burning Sand": "D99376", + "Burnt Maroon": "420303", + "Burnt Orange": "CC5500", + "Burnt Sienna": "E97451", + "Burnt Umber": "8A3324", + "Bush": "0D2E1C", + "Buttercup": "F3AD16", + "Buttered Rum": "A1750D", + "Butterfly Bush": "624E9A", + "Buttermilk": "FFF1B5", + "Buttery White": "FFFCEA", + "Cab Sav": "4D0A18", + "Cabaret": "D94972", + "Cabbage Pont": "3F4C3A", + "Cactus": "587156", + "Cadet Blue": "A9B2C3", + "Cadillac": "B04C6A", + "Cafe Royale": "6F440C", + "Calico": "E0C095", + "California": "FE9D04", + "Calypso": "31728D", + "Camarone": "00581A", + "Camelot": "893456", + "Cameo": "D9B99B", + "Camouflage": "3C3910", + "Camouflage Green": "78866B", + "Can Can": "D591A4", + "Canary": "F3FB62", + "Candlelight": "FCD917", + "Candy Corn": "FBEC5D", + "Cannon Black": "251706", + "Cannon Pink": "894367", + "Cape Cod": "3C4443", + "Cape Honey": "FEE5AC", + "Cape Palliser": "A26645", + "Caper": "DCEDB4", + "Caramel": "FFDDAF", + "Cararra": "EEEEE8", + "Cardin Green": "01361C", + "Cardinal": "C41E3A", + "Cardinal Pink": "8C055E", + "Careys Pink": "D29EAA", + "Caribbean Green": "00CC99", + "Carissma": "EA88A8", + "Carla": "F3FFD8", + "Carmine": "960018", + "Carnaby Tan": "5C2E01", + "Carnation": "F95A61", + "Carnation Pink": "FFA6C9", + "Carousel Pink": "F9E0ED", + "Carrot Orange": "ED9121", + "Casablanca": "F8B853", + "Casal": "2F6168", + "Cascade": "8BA9A5", + "Cashmere": "E6BEA5", + "Casper": "ADBED1", + "Castro": "52001F", + "Catalina Blue": "062A78", + "Catskill White": "EEF6F7", + "Cavern Pink": "E3BEBE", + "Cedar": "3E1C14", + "Cedar Wood Finish": "711A00", + "Celadon": "ACE1AF", + "Celery": "B8C25D", + "Celeste": "D1D2CA", + "Cello": "1E385B", + "Celtic": "163222", + "Cement": "8D7662", + "Ceramic": "FCFFF9", + "Cerise": "DA3287", + "Cerise Red": "DE3163", + "Cerulean": "02A4D3", + "Cerulean Blue": "2A52BE", + "Chablis": "FFF4F3", + "Chalet Green": "516E3D", + "Chalky": "EED794", + "Chambray": "354E8C", + "Chamois": "EDDCB1", + "Champagne": "FAECCC", + "Chantilly": "F8C3DF", + "Charade": "292937", + "Chardon": "FFF3F1", + "Chardonnay": "FFCD8C", + "Charlotte": "BAEEF9", + "Charm": "D47494", + "Chartreuse": "7FFF00", + "Chartreuse Yellow": "DFFF00", + "Chateau Green": "40A860", + "Chatelle": "BDB3C7", + "Chathams Blue": "175579", + "Chelsea Cucumber": "83AA5D", + "Chelsea Gem": "9E5302", + "Chenin": "DFCD6F", + "Cherokee": "FCDA98", + "Cherry Pie": "2A0359", + "Cherrywood": "651A14", + "Cherub": "F8D9E9", + "Chestnut": "B94E48", + "Chestnut Rose": "CD5C5C", + "Chetwode Blue": "8581D9", + "Chicago": "5D5C58", + "Chiffon": "F1FFC8", + "Chilean Fire": "F77703", + "Chilean Heath": "FFFDE6", + "China Ivory": "FCFFE7", + "Chino": "CEC7A7", + "Chinook": "A8E3BD", + "Chocolate": "370202", + "Christalle": "33036B", + "Christi": "67A712", + "Christine": "E7730A", + "Chrome White": "E8F1D4", + "Cinder": "0E0E18", + "Cinderella": "FDE1DC", + "Cinnabar": "E34234", + "Cinnamon": "7B3F00", + "Cioccolato": "55280C", + "Citrine White": "FAF7D6", + "Citron": "9EA91F", + "Citrus": "A1C50A", + "Clairvoyant": "480656", + "Clam Shell": "D4B6AF", + "Claret": "7F1734", + "Classic Rose": "FBCCE7", + "Clay Ash": "BDC8B3", + "Clay Creek": "8A8360", + "Clear Day": "E9FFFD", + "Clementine": "E96E00", + "Clinker": "371D09", + "Cloud": "C7C4BF", + "Cloud Burst": "202E54", + "Cloudy": "ACA59F", + "Clover": "384910", + "Cobalt": "0047AB", + "Cocoa Bean": "481C1C", + "Cocoa Brown": "301F1E", + "Coconut Cream": "F8F7DC", + "Cod Gray": "0B0B0B", + "Coffee": "706555", + "Coffee Bean": "2A140E", + "Cognac": "9F381D", + "Cola": "3F2500", + "Cold Purple": "ABA0D9", + "Cold Turkey": "CEBABA", + "Colonial White": "FFEDBC", + "Comet": "5C5D75", + "Como": "517C66", + "Conch": "C9D9D2", + "Concord": "7C7B7A", + "Concrete": "F2F2F2", + "Confetti": "E9D75A", + "Congo Brown": "593737", + "Congress Blue": "02478E", + "Conifer": "ACDD4D", + "Contessa": "C6726B", + "Copper": "B87333", + "Copper Canyon": "7E3A15", + "Copper Rose": "996666", + "Copper Rust": "944747", + "Copperfield": "DA8A67", + "Coral": "FF7F50", + "Coral Red": "FF4040", + "Coral Reef": "C7BCA2", + "Coral Tree": "A86B6B", + "Corduroy": "606E68", + "Coriander": "C4D0B0", + "Cork": "40291D", + "Corn": "E7BF05", + "Corn Field": "F8FACD", + "Corn Harvest": "8B6B0B", + "Cornflower": "93CCEA", + "Cornflower Blue": "6495ED", + "Cornflower Lilac": "FFB0AC", + "Corvette": "FAD3A2", + "Cosmic": "76395D", + "Cosmos": "FFD8D9", + "Costa Del Sol": "615D30", + "Cotton Candy": "FFB7D5", + "Cotton Seed": "C2BDB6", + "County Green": "01371A", + "Cowboy": "4D282D", + "Crail": "B95140", + "Cranberry": "DB5079", + "Crater Brown": "462425", + "Cream": "FFFDD0", + "Cream Brulee": "FFE5A0", + "Cream Can": "F5C85C", + "Creole": "1E0F04", + "Crete": "737829", + "Crimson": "DC143C", + "Crocodile": "736D58", + "Crown of Thorns": "771F1F", + "Crowshead": "1C1208", + "Cruise": "B5ECDF", + "Crusoe": "004816", + "Crusta": "FD7B33", + "Cumin": "924321", + "Cumulus": "FDFFD5", + "Cupid": "FBBEDA", + "Curious Blue": "2596D1", + "Cutty Sark": "507672", + "Cyan / Aqua": "00FFFF", + "Cyprus": "003E40", + "Daintree": "012731", + "Dairy Cream": "F9E4BC", + "Daisy Bush": "4F2398", + "Dallas": "6E4B26", + "Dandelion": "FED85D", + "Danube": "6093D1", + "Dark Blue": "0000C8", + "Dark Burgundy": "770F05", + "Dark Ebony": "3C2005", + "Dark Fern": "0A480D", + "Dark Tan": "661010", + "Dawn": "A6A29A", + "Dawn Pink": "F3E9E5", + "De York": "7AC488", + "Deco": "D2DA97", + "Deep Blue": "220878", + "Deep Blush": "E47698", + "Deep Bronze": "4A3004", + "Deep Cerulean": "007BA7", + "Deep Cove": "051040", + "Deep Fir": "002900", + "Deep Forest Green": "182D09", + "Deep Koamaru": "1B127B", + "Deep Oak": "412010", + "Deep Sapphire": "082567", + "Deep Sea": "01826B", + "Deep Sea Green": "095859", + "Deep Teal": "003532", + "Del Rio": "B09A95", + "Dell": "396413", + "Delta": "A4A49D", + "Deluge": "7563A8", + "Denim": "1560BD", + "Derby": "FFEED8", + "Desert": "AE6020", + "Desert Sand": "EDC9AF", + "Desert Storm": "F8F8F7", + "Dew": "EAFFFE", + "Di Serria": "DB995E", + "Diesel": "130000", + "Dingley": "5D7747", + "Disco": "871550", + "Dixie": "E29418", + "Dodger Blue": "1E90FF", + "Dolly": "F9FF8B", + "Dolphin": "646077", + "Domino": "8E775E", + "Don Juan": "5D4C51", + "Donkey Brown": "A69279", + "Dorado": "6B5755", + "Double Colonial White": "EEE3AD", + "Double Pearl Lusta": "FCF4D0", + "Double Spanish White": "E6D7B9", + "Dove Gray": "6D6C6C", + "Downriver": "092256", + "Downy": "6FD0C5", + "Driftwood": "AF8751", + "Drover": "FDF7AD", + "Dull Lavender": "A899E6", + "Dune": "383533", + "Dust Storm": "E5CCC9", + "Dusty Gray": "A8989B", + "Eagle": "B6BAA4", + "Earls Green": "C9B93B", + "Early Dawn": "FFF9E6", + "East Bay": "414C7D", + "East Side": "AC91CE", + "Eastern Blue": "1E9AB0", + "Ebb": "E9E3E3", + "Ebony": "0C0B1D", + "Ebony Clay": "26283B", + "Eclipse": "311C17", + "Ecru White": "F5F3E5", + "Ecstasy": "FA7814", + "Eden": "105852", + "Edgewater": "C8E3D7", + "Edward": "A2AEAB", + "Egg Sour": "FFF4DD", + "Egg White": "FFEFC1", + "Eggplant": "614051", + "El Paso": "1E1708", + "El Salva": "8F3E33", + "Electric Lime": "CCFF00", + "Electric Violet": "8B00FF", + "Elephant": "123447", + "Elf Green": "088370", + "Elm": "1C7C7D", + "Emerald": "50C878", + "Eminence": "6C3082", + "Emperor": "514649", + "Empress": "817377", + "Endeavour": "0056A7", + "Energy Yellow": "F8DD5C", + "English Holly": "022D15", + "English Walnut": "3E2B23", + "Envy": "8BA690", + "Equator": "E1BC64", + "Espresso": "612718", + "Eternity": "211A0E", + "Eucalyptus": "278A5B", + "Eunry": "CFA39D", + "Evening Sea": "024E46", + "Everglade": "1C402E", + "Faded Jade": "427977", + "Fair Pink": "FFEFEC", + "Falcon": "7F626D", + "Fall Green": "ECEBBD", + "Falu Red": "801818", + "Fantasy": "FAF3F0", + "Fedora": "796A78", + "Feijoa": "9FDD8C", + "Fern": "63B76C", + "Fern Frond": "657220", + "Fern Green": "4F7942", + "Ferra": "704F50", + "Festival": "FBE96C", + "Feta": "F0FCEA", + "Fiery Orange": "B35213", + "Finch": "626649", + "Finlandia": "556D56", + "Finn": "692D54", + "Fiord": "405169", + "Fire": "AA4203", + "Fire Bush": "E89928", + "Firefly": "0E2A30", + "Flame Pea": "DA5B38", + "Flamenco": "FF7D07", + "Flamingo": "F2552A", + "Flax": "EEDC82", + "Flax Smoke": "7B8265", + "Flesh": "FFCBA4", + "Flint": "6F6A61", + "Flirt": "A2006D", + "Flush Mahogany": "CA3435", + "Flush Orange": "FF7F00", + "Foam": "D8FCFA", + "Fog": "D7D0FF", + "Foggy Gray": "CBCAB6", + "Forest Green": "228B22", + "Forget Me Not": "FFF1EE", + "Fountain Blue": "56B4BE", + "Frangipani": "FFDEB3", + "French Gray": "BDBDC6", + "French Lilac": "ECC7EE", + "French Pass": "BDEDFD", + "French Rose": "F64A8A", + "Fresh Eggplant": "990066", + "Friar Gray": "807E79", + "Fringy Flower": "B1E2C1", + "Froly": "F57584", + "Frost": "EDF5DD", + "Frosted Mint": "DBFFF8", + "Frostee": "E4F6E7", + "Fruit Salad": "4F9D5D", + "Fuchsia Blue": "7A58C1", + "Fuchsia Pink": "C154C1", + "Fuego": "BEDE0D", + "Fuel Yellow": "ECA927", + "Fun Blue": "1959A8", + "Fun Green": "016D39", + "Fuscous Gray": "54534D", + "Fuzzy Wuzzy Brown": "C45655", + "Gable Green": "163531", + "Gallery": "EFEFEF", + "Galliano": "DCB20C", + "Gamboge": "E49B0F", + "Geebung": "D18F1B", + "Genoa": "15736B", + "Geraldine": "FB8989", + "Geyser": "D4DFE2", + "Ghost": "C7C9D5", + "Gigas": "523C94", + "Gimblet": "B8B56A", + "Gin": "E8F2EB", + "Gin Fizz": "FFF9E2", + "Givry": "F8E4BF", + "Glacier": "80B3C4", + "Glade Green": "61845F", + "Go Ben": "726D4E", + "Goblin": "3D7D52", + "Gold": "FFD700", + "Gold Drop": "F18200", + "Gold Sand": "E6BE8A", + "Gold Tips": "DEBA13", + "Golden Bell": "E28913", + "Golden Dream": "F0D52D", + "Golden Fizz": "F5FB3D", + "Golden Glow": "FDE295", + "Golden Grass": "DAA520", + "Golden Sand": "F0DB7D", + "Golden Tainoi": "FFCC5C", + "Goldenrod": "FCD667", + "Gondola": "261414", + "Gordons Green": "0B1107", + "Gorse": "FFF14F", + "Gossamer": "069B81", + "Gossip": "D2F8B0", + "Gothic": "6D92A1", + "Governor Bay": "2F3CB3", + "Grain Brown": "E4D5B7", + "Grandis": "FFD38C", + "Granite Green": "8D8974", + "Granny Apple": "D5F6E3", + "Granny Smith": "84A0A0", + "Granny Smith Apple": "9DE093", + "Grape": "381A51", + "Graphite": "251607", + "Gravel": "4A444B", + "Gray": "808080", + "Gray Asparagus": "465945", + "Gray Chateau": "A2AAB3", + "Gray Nickel": "C3C3BD", + "Gray Nurse": "E7ECE6", + "Gray Olive": "A9A491", + "Gray Suit": "C1BECD", + "Green": "00FF00", + "Green Haze": "01A368", + "Green House": "24500F", + "Green Kelp": "25311C", + "Green Leaf": "436A0D", + "Green Mist": "CBD3B0", + "Green Pea": "1D6142", + "Green Smoke": "A4AF6E", + "Green Spring": "B8C1B1", + "Green Vogue": "032B52", + "Green Waterloo": "101405", + "Green White": "E8EBE0", + "Green Yellow": "ADFF2F", + "Grenadier": "D54600", + "Guardsman Red": "BA0101", + "Gulf Blue": "051657", + "Gulf Stream": "80B3AE", + "Gull Gray": "9DACB7", + "Gum Leaf": "B6D3BF", + "Gumbo": "7CA1A6", + "Gun Powder": "414257", + "Gunsmoke": "828685", + "Gurkha": "9A9577", + "Hacienda": "98811B", + "Hairy Heath": "6B2A14", + "Haiti": "1B1035", + "Half Baked": "85C4CC", + "Half Colonial White": "FDF6D3", + "Half Dutch White": "FEF7DE", + "Half Spanish White": "FEF4DB", + "Half and Half": "FFFEE1", + "Hampton": "E5D8AF", + "Harlequin": "3FFF00", + "Harp": "E6F2EA", + "Harvest Gold": "E0B974", + "Havelock Blue": "5590D9", + "Hawaiian Tan": "9D5616", + "Hawkes Blue": "D4E2FC", + "Heath": "541012", + "Heather": "B7C3D0", + "Heathered Gray": "B6B095", + "Heavy Metal": "2B3228", + "Heliotrope": "DF73FF", + "Hemlock": "5E5D3B", + "Hemp": "907874", + "Hibiscus": "B6316C", + "Highland": "6F8E63", + "Hillary": "ACA586", + "Himalaya": "6A5D1B", + "Hint of Green": "E6FFE9", + "Hint of Red": "FBF9F9", + "Hint of Yellow": "FAFDE4", + "Hippie Blue": "589AAF", + "Hippie Green": "53824B", + "Hippie Pink": "AE4560", + "Hit Gray": "A1ADB5", + "Hit Pink": "FFAB81", + "Hokey Pokey": "C8A528", + "Hoki": "65869F", + "Holly": "011D13", + "Hollywood Cerise": "F400A1", + "Honey Flower": "4F1C70", + "Honeysuckle": "EDFC84", + "Hopbush": "D06DA1", + "Horizon": "5A87A0", + "Horses Neck": "604913", + "Hot Cinnamon": "D2691E", + "Hot Pink": "FF69B4", + "Hot Toddy": "B38007", + "Humming Bird": "CFF9F3", + "Hunter Green": "161D10", + "Hurricane": "877C7B", + "Husk": "B7A458", + "Ice Cold": "B1F4E7", + "Iceberg": "DAF4F0", + "Illusion": "F6A4C9", + "Inch Worm": "B0E313", + "Indian Khaki": "C3B091", + "Indian Tan": "4D1E01", + "Indigo": "4F69C6", + "Indochine": "C26B03", + "International Klein Blue": "002FA7", + "International Orange": "FF4F00", + "Irish Coffee": "5F3D26", + "Iroko": "433120", + "Iron": "D4D7D9", + "Ironside Gray": "676662", + "Ironstone": "86483C", + "Island Spice": "FFFCEE", + "Ivory": "FFFFF0", + "Jacaranda": "2E0329", + "Jacarta": "3A2A6A", + "Jacko Bean": "2E1905", + "Jacksons Purple": "20208D", + "Jade": "00A86B", + "Jaffa": "EF863F", + "Jagged Ice": "C2E8E5", + "Jagger": "350E57", + "Jaguar": "080110", + "Jambalaya": "5B3013", + "Janna": "F4EBD3", + "Japanese Laurel": "0A6906", + "Japanese Maple": "780109", + "Japonica": "D87C63", + "Java": "1FC2C2", + "Jazzberry Jam": "A50B5E", + "Jelly Bean": "297B9A", + "Jet Stream": "B5D2CE", + "Jewel": "126B40", + "Jon": "3B1F1F", + "Jonquil": "EEFF9A", + "Jordy Blue": "8AB9F1", + "Judge Gray": "544333", + "Jumbo": "7C7B82", + "Jungle Green": "29AB87", + "Jungle Mist": "B4CFD3", + "Juniper": "6D9292", + "Just Right": "ECCDB9", + "Kabul": "5E483E", + "Kaitoke Green": "004620", + "Kangaroo": "C6C8BD", + "Karaka": "1E1609", + "Karry": "FFEAD4", + "Kashmir Blue": "507096", + "Kelp": "454936", + "Kenyan Copper": "7C1C05", + "Keppel": "3AB09E", + "Key Lime Pie": "BFC921", + "Khaki": "F0E68C", + "Kidnapper": "E1EAD4", + "Kilamanjaro": "240C02", + "Killarney": "3A6A47", + "Kimberly": "736C9F", + "Kingfisher Daisy": "3E0480", + "Kobi": "E79FC4", + "Kokoda": "6E6D57", + "Korma": "8F4B0E", + "Koromiko": "FFBD5F", + "Kournikova": "FFE772", + "Kumera": "886221", + "La Palma": "368716", + "La Rioja": "B3C110", + "Las Palmas": "C6E610", + "Laser": "C8B568", + "Laser Lemon": "FFFF66", + "Laurel": "749378", + "Lavender": "B57EDC", + "Lavender Gray": "BDBBD7", + "Lavender Magenta": "EE82EE", + "Lavender Pink": "FBAED2", + "Lavender Purple": "967BB6", + "Lavender Rose": "FBA0E3", + "Lavender blush": "FFF0F5", + "Leather": "967059", + "Lemon": "FDE910", + "Lemon Chiffon": "FFFACD", + "Lemon Ginger": "AC9E22", + "Lemon Grass": "9B9E8F", + "Light Apricot": "FDD5B1", + "Light Orchid": "E29CD2", + "Light Wisteria": "C9A0DC", + "Lightning Yellow": "FCC01E", + "Lilac": "C8A2C8", + "Lilac Bush": "9874D3", + "Lily": "C8AABF", + "Lily White": "E7F8FF", + "Lima": "76BD17", + "Lime": "BFFF00", + "Limeade": "6F9D02", + "Limed Ash": "747D63", + "Limed Oak": "AC8A56", + "Limed Spruce": "394851", + "Linen": "FAF0E6", + "Link Water": "D9E4F5", + "Lipstick": "AB0563", + "Lisbon Brown": "423921", + "Livid Brown": "4D282E", + "Loafer": "EEF4DE", + "Loblolly": "BDC9CE", + "Lochinvar": "2C8C84", + "Lochmara": "007EC7", + "Locust": "A8AF8E", + "Log Cabin": "242A1D", + "Logan": "AAA9CD", + "Lola": "DFCFDB", + "London Hue": "BEA6C3", + "Lonestar": "6D0101", + "Lotus": "863C3C", + "Loulou": "460B41", + "Lucky": "AF9F1C", + "Lucky Point": "1A1A68", + "Lunar Green": "3C493A", + "Luxor Gold": "A7882C", + "Lynch": "697E9A", + "Mabel": "D9F7FF", + "Macaroni and Cheese": "FFB97B", + "Madang": "B7F0BE", + "Madison": "09255D", + "Madras": "3F3002", + "Magenta / Fuchsia": "FF00FF", + "Magic Mint": "AAF0D1", + "Magnolia": "F8F4FF", + "Mahogany": "4E0606", + "Mai Tai": "B06608", + "Maize": "F5D5A0", + "Makara": "897D6D", + "Mako": "444954", + "Malachite": "0BDA51", + "Malibu": "7DC8F7", + "Mallard": "233418", + "Malta": "BDB2A1", + "Mamba": "8E8190", + "Manatee": "8D90A1", + "Mandalay": "AD781B", + "Mandy": "E25465", + "Mandys Pink": "F2C3B2", + "Mango Tango": "E77200", + "Manhattan": "F5C999", + "Mantis": "74C365", + "Mantle": "8B9C90", + "Manz": "EEEF78", + "Mardi Gras": "350036", + "Marigold": "B98D28", + "Marigold Yellow": "FBE870", + "Mariner": "286ACD", + "Maroon": "800000", + "Maroon Flush": "C32148", + "Maroon Oak": "520C17", + "Marshland": "0B0F08", + "Martini": "AFA09E", + "Martinique": "363050", + "Marzipan": "F8DB9D", + "Masala": "403B38", + "Matisse": "1B659D", + "Matrix": "B05D54", + "Matterhorn": "4E3B41", + "Mauve": "E0B0FF", + "Mauvelous": "F091A9", + "Maverick": "D8C2D5", + "Medium Carmine": "AF4035", + "Medium Purple": "9370DB", + "Medium Red Violet": "BB3385", + "Melanie": "E4C2D5", + "Melanzane": "300529", + "Melon": "FEBAAD", + "Melrose": "C7C1FF", + "Mercury": "E5E5E5", + "Merino": "F6F0E6", + "Merlin": "413C37", + "Merlot": "831923", + "Metallic Bronze": "49371B", + "Metallic Copper": "71291D", + "Meteor": "D07D12", + "Meteorite": "3C1F76", + "Mexican Red": "A72525", + "Mid Gray": "5F5F6E", + "Midnight": "011635", + "Midnight Blue": "003366", + "Midnight Moss": "041004", + "Mikado": "2D2510", + "Milan": "FAFFA4", + "Milano Red": "B81104", + "Milk Punch": "FFF6D4", + "Millbrook": "594433", + "Mimosa": "F8FDD3", + "Mindaro": "E3F988", + "Mine Shaft": "323232", + "Mineral Green": "3F5D53", + "Ming": "36747D", + "Minsk": "3F307F", + "Mint Green": "98FF98", + "Mint Julep": "F1EEC1", + "Mint Tulip": "C4F4EB", + "Mirage": "161928", + "Mischka": "D1D2DD", + "Mist Gray": "C4C4BC", + "Mobster": "7F7589", + "Moccaccino": "6E1D14", + "Mocha": "782D19", + "Mojo": "C04737", + "Mona Lisa": "FFA194", + "Monarch": "8B0723", + "Mondo": "4A3C30", + "Mongoose": "B5A27F", + "Monsoon": "8A8389", + "Monte Carlo": "83D0C6", + "Monza": "C7031E", + "Moody Blue": "7F76D3", + "Moon Glow": "FCFEDA", + "Moon Mist": "DCDDCC", + "Moon Raker": "D6CEF6", + "Morning Glory": "9EDEE0", + "Morocco Brown": "441D00", + "Mortar": "504351", + "Mosque": "036A6E", + "Moss Green": "ADDFAD", + "Mountain Meadow": "1AB385", + "Mountain Mist": "959396", + "Mountbatten Pink": "997A8D", + "Muddy Waters": "B78E5C", + "Muesli": "AA8B5B", + "Mulberry": "C54B8C", + "Mulberry Wood": "5C0536", + "Mule Fawn": "8C472F", + "Mulled Wine": "4E4562", + "Mustard": "FFDB58", + "My Pink": "D69188", + "My Sin": "FFB31F", + "Mystic": "E2EBED", + "Nandor": "4B5D52", + "Napa": "ACA494", + "Narvik": "EDF9F1", + "Natural Gray": "8B8680", + "Navajo White": "FFDEAD", + "Navy Blue": "000080", + "Nebula": "CBDBD6", + "Negroni": "FFE2C5", + "Neon Carrot": "FF9933", + "Nepal": "8EABC1", + "Neptune": "7CB7BB", + "Nero": "140600", + "Nevada": "646E75", + "New Orleans": "F3D69D", + "New York Pink": "D7837F", + "Niagara": "06A189", + "Night Rider": "1F120F", + "Night Shadz": "AA375A", + "Nile Blue": "193751", + "Nobel": "B7B1B1", + "Nomad": "BAB1A2", + "Norway": "A8BD9F", + "Nugget": "C59922", + "Nutmeg": "81422C", + "Nutmeg Wood Finish": "683600", + "Oasis": "FEEFCE", + "Observatory": "02866F", + "Ocean Green": "41AA78", + "Ochre": "CC7722", + "Off Green": "E6F8F3", + "Off Yellow": "FEF9E3", + "Oil": "281E15", + "Old Brick": "901E1E", + "Old Copper": "724A2F", + "Old Gold": "CFB53B", + "Old Lace": "FDF5E6", + "Old Lavender": "796878", + "Old Rose": "C08081", + "Olive": "808000", + "Olive Drab": "6B8E23", + "Olive Green": "B5B35C", + "Olive Haze": "8B8470", + "Olivetone": "716E10", + "Olivine": "9AB973", + "Onahau": "CDF4FF", + "Onion": "2F270E", + "Opal": "A9C6C2", + "Opium": "8E6F70", + "Oracle": "377475", + "Orange": "FF681F", + "Orange Peel": "FFA000", + "Orange Roughy": "C45719", + "Orange White": "FEFCED", + "Orchid": "DA70D6", + "Orchid White": "FFFDF3", + "Oregon": "9B4703", + "Orient": "015E85", + "Oriental Pink": "C69191", + "Orinoco": "F3FBD4", + "Oslo Gray": "878D91", + "Ottoman": "E9F8ED", + "Outer Space": "2D383A", + "Outrageous Orange": "FF6037", + "Oxford Blue": "384555", + "Oxley": "779E86", + "Oyster Bay": "DAFAFF", + "Oyster Pink": "E9CECD", + "Paarl": "A65529", + "Pablo": "776F61", + "Pacific Blue": "009DC4", + "Pacifika": "778120", + "Paco": "411F10", + "Padua": "ADE6C4", + "Pale Canary": "FFFF99", + "Pale Leaf": "C0D3B9", + "Pale Oyster": "988D77", + "Pale Prim": "FDFEB8", + "Pale Rose": "FFE1F2", + "Pale Sky": "6E7783", + "Pale Slate": "C3BFC1", + "Palm Green": "09230F", + "Palm Leaf": "19330E", + "Pampas": "F4F2EE", + "Panache": "EAF6EE", + "Pancho": "EDCDAB", + "Papaya Whip": "FFEFD5", + "Paprika": "8D0226", + "Paradiso": "317D82", + "Parchment": "F1E9D2", + "Paris Daisy": "FFF46E", + "Paris M": "26056A", + "Paris White": "CADCD4", + "Parsley": "134F19", + "Pastel Green": "77DD77", + "Pastel Pink": "FFD1DC", + "Patina": "639A8F", + "Pattens Blue": "DEF5FF", + "Paua": "260368", + "Pavlova": "D7C498", + "Peach": "FFE5B4", + "Peach Cream": "FFF0DB", + "Peach Orange": "FFCC99", + "Peach Schnapps": "FFDCD6", + "Peach Yellow": "FADFAD", + "Peanut": "782F16", + "Pear": "D1E231", + "Pearl Bush": "E8E0D5", + "Pearl Lusta": "FCF4DC", + "Peat": "716B56", + "Pelorous": "3EABBF", + "Peppermint": "E3F5E1", + "Perano": "A9BEF2", + "Perfume": "D0BEF8", + "Periglacial Blue": "E1E6D6", + "Periwinkle": "CCCCFF", + "Periwinkle Gray": "C3CDE6", + "Persian Blue": "1C39BB", + "Persian Green": "00A693", + "Persian Indigo": "32127A", + "Persian Pink": "F77FBE", + "Persian Plum": "701C1C", + "Persian Red": "CC3333", + "Persian Rose": "FE28A2", + "Persimmon": "FF6B53", + "Peru Tan": "7F3A02", + "Pesto": "7C7631", + "Petite Orchid": "DB9690", + "Pewter": "96A8A1", + "Pharlap": "A3807B", + "Picasso": "FFF39D", + "Pickled Bean": "6E4826", + "Pickled Bluewood": "314459", + "Picton Blue": "45B1E8", + "Pig Pink": "FDD7E4", + "Pigeon Post": "AFBDD9", + "Pigment Indigo": "4B0082", + "Pine Cone": "6D5E54", + "Pine Glade": "C7CD90", + "Pine Green": "01796F", + "Pine Tree": "171F04", + "Pink": "FFC0CB", + "Pink Flamingo": "FF66FF", + "Pink Flare": "E1C0C8", + "Pink Lace": "FFDDF4", + "Pink Lady": "FFF1D8", + "Pink Salmon": "FF91A4", + "Pink Swan": "BEB5B7", + "Piper": "C96323", + "Pipi": "FEF4CC", + "Pippin": "FFE1DF", + "Pirate Gold": "BA7F03", + "Pistachio": "9DC209", + "Pixie Green": "C0D8B6", + "Pizazz": "FF9000", + "Pizza": "C99415", + "Plantation": "27504B", + "Plum": "843179", + "Pohutukawa": "8F021C", + "Polar": "E5F9F6", + "Polo Blue": "8DA8CC", + "Pomegranate": "F34723", + "Pompadour": "660045", + "Porcelain": "EFF2F3", + "Porsche": "EAAE69", + "Port Gore": "251F4F", + "Portafino": "FFFFB4", + "Portage": "8B9FEE", + "Portica": "F9E663", + "Pot Pourri": "F5E7E2", + "Potters Clay": "8C5738", + "Powder Ash": "BCC9C2", + "Powder Blue": "B0E0E6", + "Prairie Sand": "9A3820", + "Prelude": "D0C0E5", + "Prim": "F0E2EC", + "Primrose": "EDEA99", + "Provincial Pink": "FEF5F1", + "Prussian Blue": "003153", + "Puce": "CC8899", + "Pueblo": "7D2C14", + "Puerto Rico": "3FC1AA", + "Pumice": "C2CAC4", + "Pumpkin": "FF7518", + "Pumpkin Skin": "B1610B", + "Punch": "DC4333", + "Punga": "4D3D14", + "Purple": "660099", + "Purple Heart": "652DC1", + "Purple Mountain's Majesty": "9678B6", + "Purple Pizzazz": "FF00CC", + "Putty": "E7CD8C", + "Quarter Pearl Lusta": "FFFDF4", + "Quarter Spanish White": "F7F2E1", + "Quicksand": "BD978E", + "Quill Gray": "D6D6D1", + "Quincy": "623F2D", + "Racing Green": "0C1911", + "Radical Red": "FF355E", + "Raffia": "EADAB8", + "Rainee": "B9C8AC", + "Rajah": "F7B668", + "Rangitoto": "2E3222", + "Rangoon Green": "1C1E13", + "Raven": "727B89", + "Raw Sienna": "D27D46", + "Raw Umber": "734A12", + "Razzle Dazzle Rose": "FF33CC", + "Razzmatazz": "E30B5C", + "Rebel": "3C1206", + "Red": "FF0000", + "Red Beech": "7B3801", + "Red Berry": "8E0000", + "Red Damask": "DA6A41", + "Red Devil": "860111", + "Red Orange": "FF3F34", + "Red Oxide": "6E0902", + "Red Ribbon": "ED0A3F", + "Red Robin": "80341F", + "Red Stage": "D05F04", + "Red Violet": "C71585", + "Redwood": "5D1E0F", + "Reef": "C9FFA2", + "Reef Gold": "9F821C", + "Regal Blue": "013F6A", + "Regent Gray": "86949F", + "Regent St Blue": "AAD6E6", + "Remy": "FEEBF3", + "Reno Sand": "A86515", + "Resolution Blue": "002387", + "Revolver": "2C1632", + "Rhino": "2E3F62", + "Rice Cake": "FFFEF0", + "Rice Flower": "EEFFE2", + "Rich Gold": "A85307", + "Rio Grande": "BBD009", + "Ripe Lemon": "F4D81C", + "Ripe Plum": "410056", + "Riptide": "8BE6D8", + "River Bed": "434C59", + "Rob Roy": "EAC674", + "Robin's Egg Blue": "00CCCC", + "Rock": "4D3833", + "Rock Blue": "9EB1CD", + "Rock Spray": "BA450C", + "Rodeo Dust": "C9B29B", + "Rolling Stone": "747D83", + "Roman": "DE6360", + "Roman Coffee": "795D4C", + "Romance": "FFFEFD", + "Romantic": "FFD2B7", + "Ronchi": "ECC54E", + "Roof Terracotta": "A62F20", + "Rope": "8E4D1E", + "Rose": "FF007F", + "Rose Bud": "FBB2A3", + "Rose Bud Cherry": "800B47", + "Rose Fog": "E7BCB4", + "Rose White": "FFF6F5", + "Rose of Sharon": "BF5500", + "Rosewood": "65000B", + "Roti": "C6A84B", + "Rouge": "A23B6C", + "Royal Blue": "4169E1", + "Royal Heath": "AB3472", + "Royal Purple": "6B3FA0", + "Rum": "796989", + "Rum Swizzle": "F9F8E4", + "Russet": "80461B", + "Russett": "755A57", + "Rust": "B7410E", + "Rustic Red": "480404", + "Rusty Nail": "86560A", + "Saddle": "4C3024", + "Saddle Brown": "583401", + "Saffron": "F4C430", + "Saffron Mango": "F9BF58", + "Sage": "9EA587", + "Sahara": "B7A214", + "Sahara Sand": "F1E788", + "Sail": "B8E0F9", + "Salem": "097F4B", + "Salmon": "FF8C69", + "Salomie": "FEDB8D", + "Salt Box": "685E6E", + "Saltpan": "F1F7F2", + "Sambuca": "3A2010", + "San Felix": "0B6207", + "San Juan": "304B6A", + "San Marino": "456CAC", + "Sand Dune": "826F65", + "Sandal": "AA8D6F", + "Sandrift": "AB917A", + "Sandstone": "796D62", + "Sandwisp": "F5E7A2", + "Sandy Beach": "FFEAC8", + "Sandy brown": "F4A460", + "Sangria": "92000A", + "Sanguine Brown": "8D3D38", + "Santa Fe": "B16D52", + "Santas Gray": "9FA0B1", + "Sapling": "DED4A4", + "Sapphire": "2F519E", + "Saratoga": "555B10", + "Satin Linen": "E6E4D4", + "Sauvignon": "FFF5F3", + "Sazerac": "FFF4E0", + "Scampi": "675FA6", + "Scandal": "CFFAF4", + "Scarlet": "FF2400", + "Scarlet Gum": "431560", + "Scarlett": "950015", + "Scarpa Flow": "585562", + "Schist": "A9B497", + "School bus Yellow": "FFD800", + "Schooner": "8B847E", + "Science Blue": "0066CC", + "Scooter": "2EBFD4", + "Scorpion": "695F62", + "Scotch Mist": "FFFBDC", + "Screamin' Green": "66FF66", + "Sea Buckthorn": "FBA129", + "Sea Green": "2E8B57", + "Sea Mist": "C5DBCA", + "Sea Nymph": "78A39C", + "Sea Pink": "ED989E", + "Seagull": "80CCEA", + "Seance": "731E8F", + "Seashell": "F1F1F1", + "Seashell Peach": "FFF5EE", + "Seaweed": "1B2F11", + "Selago": "F0EEFD", + "Selective Yellow": "FFBA00", + "Sepia": "704214", + "Sepia Black": "2B0202", + "Sepia Skin": "9E5B40", + "Serenade": "FFF4E8", + "Shadow": "837050", + "Shadow Green": "9AC2B8", + "Shady Lady": "AAA5A9", + "Shakespeare": "4EABD1", + "Shalimar": "FBFFBA", + "Shamrock": "33CC99", + "Shark": "25272C", + "Sherpa Blue": "004950", + "Sherwood Green": "02402C", + "Shilo": "E8B9B3", + "Shingle Fawn": "6B4E31", + "Ship Cove": "788BBA", + "Ship Gray": "3E3A44", + "Shiraz": "B20931", + "Shocking": "E292C0", + "Shocking Pink": "FC0FC0", + "Shuttle Gray": "5F6672", + "Siam": "646A54", + "Sidecar": "F3E7BB", + "Silk": "BDB1A8", + "Silver": "C0C0C0", + "Silver Chalice": "ACACAC", + "Silver Rust": "C9C0BB", + "Silver Sand": "BFC1C2", + "Silver Tree": "66B58F", + "Sinbad": "9FD7D3", + "Siren": "7A013A", + "Sirocco": "718080", + "Sisal": "D3CBBA", + "Skeptic": "CAE6DA", + "Sky Blue": "76D7EA", + "Slate Gray": "708090", + "Smalt": "003399", + "Smalt Blue": "51808F", + "Smoky": "605B73", + "Snow Drift": "F7FAF7", + "Snow Flurry": "E4FFD1", + "Snowy Mint": "D6FFDB", + "Snuff": "E2D8ED", + "Soapstone": "FFFBF9", + "Soft Amber": "D1C6B4", + "Soft Peach": "F5EDEF", + "Solid Pink": "893843", + "Solitaire": "FEF8E2", + "Solitude": "EAF6FF", + "Sorbus": "FD7C07", + "Sorrell Brown": "CEB98F", + "Soya Bean": "6A6051", + "Spanish Green": "819885", + "Spectra": "2F5A57", + "Spice": "6A442E", + "Spicy Mix": "885342", + "Spicy Mustard": "74640D", + "Spicy Pink": "816E71", + "Spindle": "B6D1EA", + "Spray": "79DEEC", + "Spring Green": "00FF7F", + "Spring Leaves": "578363", + "Spring Rain": "ACCBB1", + "Spring Sun": "F6FFDC", + "Spring Wood": "F8F6F1", + "Sprout": "C1D7B0", + "Spun Pearl": "AAABB7", + "Squirrel": "8F8176", + "St Tropaz": "2D569B", + "Stack": "8A8F8A", + "Star Dust": "9F9F9C", + "Stark White": "E5D7BD", + "Starship": "ECF245", + "Steel Blue": "4682B4", + "Steel Gray": "262335", + "Stiletto": "9C3336", + "Stonewall": "928573", + "Storm Dust": "646463", + "Storm Gray": "717486", + "Stratos": "000741", + "Straw": "D4BF8D", + "Strikemaster": "956387", + "Stromboli": "325D52", + "Studio": "714AB2", + "Submarine": "BAC7C9", + "Sugar Cane": "F9FFF6", + "Sulu": "C1F07C", + "Summer Green": "96BBAB", + "Sun": "FBAC13", + "Sundance": "C9B35B", + "Sundown": "FFB1B3", + "Sunflower": "E4D422", + "Sunglo": "E16865", + "Sunglow": "FFCC33", + "Sunset Orange": "FE4C40", + "Sunshade": "FF9E2C", + "Supernova": "FFC901", + "Surf": "BBD7C1", + "Surf Crest": "CFE5D2", + "Surfie Green": "0C7A79", + "Sushi": "87AB39", + "Suva Gray": "888387", + "Swamp": "001B1C", + "Swamp Green": "ACB78E", + "Swans Down": "DCF0EA", + "Sweet Corn": "FBEA8C", + "Sweet Pink": "FD9FA2", + "Swirl": "D3CDC5", + "Swiss Coffee": "DDD6D5", + "Sycamore": "908D39", + "Tabasco": "A02712", + "Tacao": "EDB381", + "Tacha": "D6C562", + "Tahiti Gold": "E97C07", + "Tahuna Sands": "EEF0C8", + "Tall Poppy": "B32D29", + "Tallow": "A8A589", + "Tamarillo": "991613", + "Tamarind": "341515", + "Tan": "D2B48C", + "Tan Hide": "FA9D5A", + "Tana": "D9DCC1", + "Tangaroa": "03163C", + "Tangerine": "F28500", + "Tango": "ED7A1C", + "Tapa": "7B7874", + "Tapestry": "B05E81", + "Tara": "E1F6E8", + "Tarawera": "073A50", + "Tasman": "CFDCCF", + "Taupe": "483C32", + "Taupe Gray": "B3AF95", + "Tawny Port": "692545", + "Te Papa Green": "1E433C", + "Tea": "C1BAB0", + "Tea Green": "D0F0C0", + "Teak": "B19461", + "Teal": "008080", + "Teal Blue": "044259", + "Temptress": "3B000B", + "Tenn": "CD5700", + "Tequila": "FFE6C7", + "Terracotta": "E2725B", + "Texas": "F8F99C", + "Texas Rose": "FFB555", + "Thatch": "B69D98", + "Thatch Green": "403D19", + "Thistle": "D8BFD8", + "Thistle Green": "CCCAA8", + "Thunder": "33292F", + "Thunderbird": "C02B18", + "Tia Maria": "C1440E", + "Tiara": "C3D1D1", + "Tiber": "063537", + "Tickle Me Pink": "FC80A5", + "Tidal": "F1FFAD", + "Tide": "BFB8B0", + "Timber Green": "16322C", + "Timberwolf": "D9D6CF", + "Titan White": "F0EEFF", + "Toast": "9A6E61", + "Tobacco Brown": "715D47", + "Toledo": "3A0020", + "Tolopea": "1B0245", + "Tom Thumb": "3F583B", + "Tonys Pink": "E79F8C", + "Topaz": "7C778A", + "Torch Red": "FD0E35", + "Torea Bay": "0F2D9E", + "Tory Blue": "1450AA", + "Tosca": "8D3F3F", + "Totem Pole": "991B07", + "Tower Gray": "A9BDBF", + "Tradewind": "5FB3AC", + "Tranquil": "E6FFFF", + "Travertine": "FFFDE8", + "Tree Poppy": "FC9C1D", + "Treehouse": "3B2820", + "Trendy Green": "7C881A", + "Trendy Pink": "8C6495", + "Trinidad": "E64E03", + "Tropical Blue": "C3DDF9", + "Tropical Rain Forest": "00755E", + "Trout": "4A4E5A", + "True V": "8A73D6", + "Tuatara": "363534", + "Tuft Bush": "FFDDCD", + "Tulip Tree": "EAB33B", + "Tumbleweed": "DEA681", + "Tuna": "353542", + "Tundora": "4A4244", + "Turbo": "FAE600", + "Turkish Rose": "B57281", + "Turmeric": "CABB48", + "Turquoise": "30D5C8", + "Turquoise Blue": "6CDAE7", + "Turtle Green": "2A380B", + "Tuscany": "BD5E2E", + "Tusk": "EEF3C3", + "Tussock": "C5994B", + "Tutu": "FFF1F9", + "Twilight": "E4CFDE", + "Twilight Blue": "EEFDFF", + "Twine": "C2955D", + "Tyrian Purple": "66023C", + "Ultramarine": "120A8F", + "Valencia": "D84437", + "Valentino": "350E42", + "Valhalla": "2B194F", + "Van Cleef": "49170C", + "Vanilla": "D1BEA8", + "Vanilla Ice": "F3D9DF", + "Varden": "FFF6DF", + "Venetian Red": "72010F", + "Venice Blue": "055989", + "Venus": "928590", + "Verdigris": "5D5E37", + "Verdun Green": "495400", + "Vermilion": "FF4D00", + "Vesuvius": "B14A0B", + "Victoria": "534491", + "Vida Loca": "549019", + "Viking": "64CCDB", + "Vin Rouge": "983D61", + "Viola": "CB8FA9", + "Violent Violet": "290C5E", + "Violet": "240A40", + "Violet Eggplant": "991199", + "Violet Red": "F7468A", + "Viridian": "40826D", + "Viridian Green": "678975", + "Vis Vis": "FFEFA1", + "Vista Blue": "8FD6B4", + "Vista White": "FCF8F7", + "Vivid Tangerine": "FF9980", + "Vivid Violet": "803790", + "Voodoo": "533455", + "Vulcan": "10121D", + "Wafer": "DECBC6", + "Waikawa Gray": "5A6E9C", + "Waiouru": "363C0D", + "Walnut": "773F1A", + "Wasabi": "788A25", + "Water Leaf": "A1E9DE", + "Watercourse": "056F57", + "Waterloo ": "7B7C94", + "Wattle": "DCD747", + "Watusi": "FFDDCF", + "Wax Flower": "FFC0A8", + "We Peep": "F7DBE6", + "Web Orange": "FFA500", + "Wedgewood": "4E7F9E", + "Well Read": "B43332", + "West Coast": "625119", + "West Side": "FF910F", + "Westar": "DCD9D2", + "Wewak": "F19BAB", + "Wheat": "F5DEB3", + "Wheatfield": "F3EDCF", + "Whiskey": "D59A6F", + "Whisper": "F7F5FA", + "White": "FFFFFF", + "White Ice": "DDF9F1", + "White Lilac": "F8F7FC", + "White Linen": "F8F0E8", + "White Pointer": "FEF8FF", + "White Rock": "EAE8D4", + "Wild Blue Yonder": "7A89B8", + "Wild Rice": "ECE090", + "Wild Sand": "F4F4F4", + "Wild Strawberry": "FF3399", + "Wild Watermelon": "FD5B78", + "Wild Willow": "B9C46A", + "William": "3A686C", + "Willow Brook": "DFECDA", + "Willow Grove": "65745D", + "Windsor": "3C0878", + "Wine Berry": "591D35", + "Winter Hazel": "D5D195", + "Wisp Pink": "FEF4F8", + "Wisteria": "9771B5", + "Wistful": "A4A6D3", + "Witch Haze": "FFFC99", + "Wood Bark": "261105", + "Woodland": "4D5328", + "Woodrush": "302A0F", + "Woodsmoke": "0C0D0F", + "Woody Brown": "483131", + "Xanadu": "738678", + "Yellow": "FFFF00", + "Yellow Green": "C5E17A", + "Yellow Metal": "716338", + "Yellow Orange": "FFAE42", + "Yellow Sea": "FEA904", + "Your Pink": "FFC3C0", + "Yukon Gold": "7B6608", + "Yuma": "CEC291", + "Zambezi": "685558", + "Zanah": "DAECD6", + "Zest": "E5841B", + "Zeus": "292319", + "Ziggurat": "BFDBE2", + "Zinnwaldite": "EBC2AF", + "Zircon": "F4F8FF", + "Zombie": "E4D69B", + "Zorba": "A59B91", + "Zuccini": "044022", + "Zumthor": "EDF6FF" +} -- cgit v1.2.3 From c87f4451206cfc1315fd32c33836f81a5e6ea300 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Fri, 10 Sep 2021 07:47:12 -0400 Subject: Test to capture all user_input --- bot/exts/utilities/color.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index d7fff503..c4df3e10 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -32,7 +32,7 @@ class Color(commands.Cog): self.bot = bot @commands.command(aliases=["colour"]) - async def color(self, ctx: commands.Context, mode: str, user_color: str) -> None: + async def color(self, ctx: commands.Context, mode: str, *, user_color: str) -> None: """Send information on input color code or color name.""" logger.info(f"{mode = }") logger.info(f"{user_color = }") -- cgit v1.2.3 From fc5d7ea343336a3fc62617d4042a13a208763c6c Mon Sep 17 00:00:00 2001 From: CyberCitizen01 Date: Sat, 11 Sep 2021 04:17:18 +0530 Subject: Added "colour information" and "colour conversion" features Details: https://github.com/python-discord/sir-lancebot/issues/677 NOTE: get_color_fields (line 122) method explicity requires a valid tuple of RGB values. --- bot/exts/utilities/color.py | 134 +++++++++++++++++++++++++++++++++----------- 1 file changed, 100 insertions(+), 34 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index c4df3e10..6abfc006 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -1,4 +1,4 @@ -# import colorsys +import colorsys import logging import re from io import BytesIO @@ -76,44 +76,28 @@ class Color(commands.Cog): await ctx.send(embed=wrong_mode_embed) return - main_embed = Embed( - title=user_color, # need to replace with fuzzymatch color name - color=hex_color, - ) async with ctx.typing(): - file = await self._create_thumbnail_attachment(rgb_color) + main_embed = Embed( + title=user_color, # need to replace with fuzzymatch color name + description='(Approx..)', + color=hex_color, + ) + + file = await self.create_thumbnail_attachment(rgb_color) main_embed.set_thumbnail(url="attachment://color.png") - main_embed.add_field( - name="Hex", - value=f">>Hex #{hex_color}", - inline=False, - ) - main_embed.add_field( - name="RGB", - value=f">>RGB {rgb_color}", - inline=False, - ) - """ - main_embed.add_field( - name="HSV", - value=f">>HSV {hsv_color}", - inline=False, - ) - main_embed.add_field( - name="HSL", - value=f">>HSL {hsl_color}", - inline=False, - ) - main_embed.add_field( - name="CMYK", - value=f">>CMYK {cmyk_color}", - inline=False, - ) - """ + fields = self.get_color_fields(rgb_color) + for field in fields: + main_embed.add_field( + name=field['name'], + value=field['value'], + inline=False, + ) + await ctx.send(file=file, embed=main_embed) - async def _create_thumbnail_attachment(self, color: str) -> File: + @staticmethod + async def create_thumbnail_attachment(color: str) -> File: """Generate a thumbnail from `color`.""" thumbnail = Image.new("RGB", (100, 100), color=color) bufferedio = BytesIO() @@ -124,6 +108,88 @@ class Color(commands.Cog): return file + @staticmethod + def get_color_fields(rgb_color: tuple[int, int, int]) -> list[dict]: + """Converts from `RGB` to `CMYK`, `HSV`, `HSL` and returns a list of fields.""" + + def _rgb_to_hex(rgb_color: tuple[int, int, int]) -> str: + """To convert from `RGB` to `Hex` notation.""" + return '#' + ''.join(hex(color)[2:].zfill(2) for color in rgb_color).upper() + + def _rgb_to_cmyk(rgb_color: tuple[int, int, int]) -> tuple[int, int, int, int]: + """To convert from `RGB` to `CMYK` color space.""" + r, g, b = rgb_color + + # RGB_SCALE -> 255 + # CMYK_SCALE -> 100 + + if (r == g == b == 0): + return 0, 0, 0, 100 # Representing Black + + # rgb [0,RGB_SCALE] -> cmy [0,1] + c = 1 - r / 255 + m = 1 - g / 255 + y = 1 - b / 255 + + # extract out k [0, 1] + min_cmy = min(c, m, y) + c = (c - min_cmy) / (1 - min_cmy) + m = (m - min_cmy) / (1 - min_cmy) + y = (y - min_cmy) / (1 - min_cmy) + k = min_cmy + + # rescale to the range [0,CMYK_SCALE] and round off + c = round(c * 100) + m = round(m * 100) + y = round(y * 100) + k = round(k * 100) + + return c, m, y, k + + def _rgb_to_hsv(rgb_color: tuple[int, int, int]) -> tuple[int, int, int]: + """To convert from `RGB` to `HSV` color space.""" + r, g, b = rgb_color + h, v, s = colorsys.rgb_to_hsv(r/float(255), g/float(255), b/float(255)) + h = round(h*360) + s = round(s*100) + v = round(v*100) + return h, s, v + + def _rgb_to_hsl(rgb_color: tuple[int, int, int]) -> tuple[int, int, int]: + """To convert from `RGB` to `HSL` color space.""" + r, g, b = rgb_color + h, l, s = colorsys.rgb_to_hls(r/float(255), g/float(255), b/float(255)) + h = round(h*360) + s = round(s*100) + l = round(l*100) # noqa: E741 It's little `L`, Reason: To maintain consistency. + return h, s, l + + hex_color = _rgb_to_hex(rgb_color) + cmyk_color = _rgb_to_cmyk(rgb_color) + hsv_color = _rgb_to_hsv(rgb_color) + hsl_color = _rgb_to_hsl(rgb_color) + + all_fields = [ + { + "name": "RGB", + "value": f"Β» rgb {rgb_color}\nΒ» hex {hex_color}" + }, + { + "name": "CMYK", + "value": f"Β» cmyk {cmyk_color}" + }, + { + "name": "HSV", + "value": f"Β» hsv {hsv_color}" + }, + { + "name": "HSL", + "value": f"Β» hsl {hsl_color}" + }, + ] + + return all_fields + # if user_color in color_lists: # # fuzzy match for color -- cgit v1.2.3 From 4e316e7991862db76b78e16019c02461c3c52fc2 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Sat, 11 Sep 2021 09:57:13 -0400 Subject: Add fuzzy match function I made a few changes, the biggest being the fuzzy match function to return a hex color code based on an input color name. Open items that I can think of so far: -Since the json file has color names and hex values, in order to use fuzzy matching for a color name the color must first be converted to hex. Currently there is only a rgb to anything function which returns values in a dictionary. -The main embed creation references the rgb_color before it is defined, should the command function be moved to the bottom of the file or just the main embed creation and sending? -When using the rgb mode, should the user be forced to do (r, g, b) or should the command handle an input of "r, g, b"? If you are reading this, thank you. --- bot/exts/utilities/color.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 6abfc006..9e199dd4 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -1,4 +1,5 @@ import colorsys +import json import logging import re from io import BytesIO @@ -6,7 +7,7 @@ from io import BytesIO from PIL import Image, ImageColor from discord import Embed, File from discord.ext import commands -# from rapidfuzz import process +from rapidfuzz import process from bot.bot import Bot from bot.constants import Colours @@ -23,6 +24,8 @@ ERROR_MSG = """The color code {user_color} is not a possible color combination. \nHex: #000000-#FFFFFF """ +COLOR_LIST = "bot/resources/utilities/ryanzec_colours.json" + # define color command class Color(commands.Cog): @@ -46,7 +49,7 @@ class Color(commands.Cog): else: await ctx.send( embed=Embed( - title="An error has occured.", + title="There was an issue converting the hex color code.", description=ERROR_MSG.format(user_color=user_color), ) ) @@ -58,9 +61,10 @@ class Color(commands.Cog): pass elif mode.lower() == "cmyk": pass + elif mode.lower() == "name": + color_name, hex_color = self.match_color(user_color) else: # mode is either None or an invalid code - # need to handle whether user passes color name if mode is None: no_mode_embed = Embed( title="No 'mode' was passed, please define a color code.", @@ -70,7 +74,7 @@ class Color(commands.Cog): return wrong_mode_embed = Embed( title=f"The color code {mode} is not a valid option", - description="Possible modes are: Hex, RGB, HSV, HSL and CMYK.", + description="Possible modes are: Name, Hex, RGB, HSV, HSL and CMYK.", color=Colours.soft_red, ) await ctx.send(embed=wrong_mode_embed) @@ -78,15 +82,15 @@ class Color(commands.Cog): async with ctx.typing(): main_embed = Embed( - title=user_color, # need to replace with fuzzymatch color name + title=color_name, description='(Approx..)', - color=hex_color, + color=rgb_color, ) file = await self.create_thumbnail_attachment(rgb_color) main_embed.set_thumbnail(url="attachment://color.png") - fields = self.get_color_fields(rgb_color) + for field in fields: main_embed.add_field( name=field['name'], @@ -94,7 +98,7 @@ class Color(commands.Cog): inline=False, ) - await ctx.send(file=file, embed=main_embed) + await ctx.send(file=file, embed=main_embed) @staticmethod async def create_thumbnail_attachment(color: str) -> File: @@ -192,6 +196,17 @@ class Color(commands.Cog): # if user_color in color_lists: # # fuzzy match for color + @staticmethod + def match_color(user_color: str) -> str: + """Use fuzzy matching to return a hex color code based on the user's input.""" + with open(COLOR_LIST) as f: + color_list = json.load(f) + logger.debug(f"{type(color_list) = }") + match, certainty, _ = process.extractOne(query=user_color, choices=color_list.keys(), score_cutoff=50) + logger.debug(f"{match = }, {certainty = }") + hex_match = color_list[match] + logger.debug(f"{hex_match = }") + return match, hex_match def setup(bot: Bot) -> None: -- cgit v1.2.3 From c6333ab48ddbc37af40cd958cd37494be7ff50e7 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Sat, 11 Sep 2021 10:09:27 -0400 Subject: Remove placeholder comment --- bot/exts/utilities/color.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 9e199dd4..6452d292 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -194,8 +194,6 @@ class Color(commands.Cog): return all_fields - # if user_color in color_lists: - # # fuzzy match for color @staticmethod def match_color(user_color: str) -> str: """Use fuzzy matching to return a hex color code based on the user's input.""" -- cgit v1.2.3 From 2cc1ad0538d4edb69eddfe771bd3068d18ed54f3 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Sun, 12 Sep 2021 08:39:47 -0400 Subject: Load json file once --- bot/exts/utilities/color.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 6452d292..dd470197 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -25,6 +25,8 @@ ERROR_MSG = """The color code {user_color} is not a possible color combination. """ COLOR_LIST = "bot/resources/utilities/ryanzec_colours.json" +with open(COLOR_LIST) as f: + COLOR_JSON = json.load(f) # define color command @@ -197,12 +199,9 @@ class Color(commands.Cog): @staticmethod def match_color(user_color: str) -> str: """Use fuzzy matching to return a hex color code based on the user's input.""" - with open(COLOR_LIST) as f: - color_list = json.load(f) - logger.debug(f"{type(color_list) = }") - match, certainty, _ = process.extractOne(query=user_color, choices=color_list.keys(), score_cutoff=50) + match, certainty, _ = process.extractOne(query=user_color, choices=COLOR_JSON.keys(), score_cutoff=50) logger.debug(f"{match = }, {certainty = }") - hex_match = color_list[match] + hex_match = COLOR_JSON[match] logger.debug(f"{hex_match = }") return match, hex_match -- cgit v1.2.3 From 85de760ad006575bfb61472be0aa346406ac7d2c Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Wed, 15 Sep 2021 16:16:52 -0400 Subject: Fix Flake8 spacing errors --- bot/exts/utilities/color.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index dd470197..eb9d5f4d 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -155,19 +155,19 @@ class Color(commands.Cog): def _rgb_to_hsv(rgb_color: tuple[int, int, int]) -> tuple[int, int, int]: """To convert from `RGB` to `HSV` color space.""" r, g, b = rgb_color - h, v, s = colorsys.rgb_to_hsv(r/float(255), g/float(255), b/float(255)) - h = round(h*360) - s = round(s*100) - v = round(v*100) + h, v, s = colorsys.rgb_to_hsv(r / float(255), g / float(255), b / float(255)) + h = round(h * 360) + s = round(s * 100) + v = round(v * 100) return h, s, v def _rgb_to_hsl(rgb_color: tuple[int, int, int]) -> tuple[int, int, int]: """To convert from `RGB` to `HSL` color space.""" r, g, b = rgb_color - h, l, s = colorsys.rgb_to_hls(r/float(255), g/float(255), b/float(255)) - h = round(h*360) - s = round(s*100) - l = round(l*100) # noqa: E741 It's little `L`, Reason: To maintain consistency. + h, l, s = colorsys.rgb_to_hls(r / float(255), g / float(255), b / float(255)) + h = round(h * 360) + s = round(s * 100) + l = round(l * 100) # noqa: E741 It's little `L`, Reason: To maintain consistency. return h, s, l hex_color = _rgb_to_hex(rgb_color) -- cgit v1.2.3 From 0f777557d67bdf117eafcba3ab98192dd420cf96 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Sun, 19 Sep 2021 08:20:18 -0400 Subject: Reword json file variables and mapping --- bot/exts/utilities/color.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index eb9d5f4d..94c9d337 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -24,9 +24,9 @@ ERROR_MSG = """The color code {user_color} is not a possible color combination. \nHex: #000000-#FFFFFF """ -COLOR_LIST = "bot/resources/utilities/ryanzec_colours.json" -with open(COLOR_LIST) as f: - COLOR_JSON = json.load(f) +COLOR_JSON_PATH = "bot/resources/utilities/ryanzec_colours.json" +with open(COLOR_JSON_PATH) as f: + COLOR_MAPPING = json.load(f) # define color command @@ -199,9 +199,9 @@ class Color(commands.Cog): @staticmethod def match_color(user_color: str) -> str: """Use fuzzy matching to return a hex color code based on the user's input.""" - match, certainty, _ = process.extractOne(query=user_color, choices=COLOR_JSON.keys(), score_cutoff=50) + match, certainty, _ = process.extractOne(query=user_color, choices=COLOR_MAPPING.keys(), score_cutoff=50) logger.debug(f"{match = }, {certainty = }") - hex_match = COLOR_JSON[match] + hex_match = COLOR_MAPPING[match] logger.debug(f"{hex_match = }") return match, hex_match -- cgit v1.2.3 From 4a2b89dbde41057ba14c08f8994c6d02b3c114b8 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Sun, 19 Sep 2021 10:20:58 -0400 Subject: Continue work on hex and rgb color commands --- bot/exts/utilities/color.py | 87 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 69 insertions(+), 18 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 94c9d337..2bec6ba3 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -31,23 +31,45 @@ with open(COLOR_JSON_PATH) as f: # define color command class Color(commands.Cog): - """User initiated command to receive color information.""" + """User initiated commands to receive color information.""" def __init__(self, bot: Bot): self.bot = bot @commands.command(aliases=["colour"]) async def color(self, ctx: commands.Context, mode: str, *, user_color: str) -> None: - """Send information on input color code or color name.""" - logger.info(f"{mode = }") - logger.info(f"{user_color = }") + """ + Send information on input color code or color name. + + Possible modes are: "hex", "rgb", "hsv", "hsl", "cmyk" or "name". + """ + logger.debug(f"{mode = }") + logger.debug(f"{user_color = }") if mode.lower() == "hex": - hex_match = re.search(r"^#(?:[0-9a-fA-F]{3}){1,2}$", user_color) + hex_match = re.fullmatch(r"(#?[0x]?)((?:[0-9a-fA-F]{3}){1,2})", user_color) if hex_match: hex_color = int(hex(int(user_color.replace("#", ""), 16)), 0) - rgb_color = ImageColor.getcolor(user_color, "RGB") - logger.info(f"{hex_color = }") - logger.info(f"{rgb_color = }") + if "#" in user_color: + rgb_color = ImageColor.getcolor(user_color, "RGB") + elif "0x" in user_color: + hex_ = user_color.replace("0x", "#") + rgb_color = ImageColor.getcolor(hex_, "RGB") + else: + hex_ = "#" + user_color + rgb_color = ImageColor.getcolor(hex_, "RGB") + (r, g, b) = rgb_color + discord_rgb_int = int(f"{r:02x}{g:02x}{b:02x}", 16) + all_colors = self.get_color_fields(rgb_color) + hex_color = all_colors[1]["value"].replace("Β» hex ", "") + cmyk_color = all_colors[2]["value"].replace("Β» cmyk ", "") + hsv_color = all_colors[3]["value"].replace("Β» hsv ", "") + hsl_color = all_colors[4]["value"].replace("Β» hsl ", "") + logger.debug(f"{rgb_color = }") + logger.debug(f"{hex_color = }") + logger.debug(f"{hsv_color = }") + logger.debug(f"{hsl_color = }") + logger.debug(f"{cmyk_color = }") + color_name, _ = self.match_color(hex_color) else: await ctx.send( embed=Embed( @@ -56,7 +78,22 @@ class Color(commands.Cog): ) ) elif mode.lower() == "rgb": - pass + if "(" in user_color: + remove = "[() ]" + rgb_color = re.sub(remove, "", user_color) + rgb_color = tuple(map(int, rgb_color.split(","))) + elif "," in user_color: + rgb_color = tuple(map(int, user_color.split(","))) + else: + rgb_color = tuple(map(int, user_color.split(" "))) + (r, g, b) = rgb_color + discord_rgb_int = int(f"{r:02x}{g:02x}{b:02x}", 16) + all_colors = self.get_color_fields(rgb_color) + hex_color = all_colors[1]["value"].replace("Β» hex ", "") + cmyk_color = all_colors[2]["value"].replace("Β» cmyk ", "") + hsv_color = all_colors[3]["value"].replace("Β» hsv ", "") + hsl_color = all_colors[4]["value"].replace("Β» hsl ", "") + color_name, _ = self.match_color(hex_color) elif mode.lower() == "hsv": pass elif mode.lower() == "hsl": @@ -70,6 +107,7 @@ class Color(commands.Cog): if mode is None: no_mode_embed = Embed( title="No 'mode' was passed, please define a color code.", + description="Possible modes are: Name, Hex, RGB, HSV, HSL and CMYK.", color=Colours.soft_red, ) await ctx.send(embed=no_mode_embed) @@ -86,7 +124,7 @@ class Color(commands.Cog): main_embed = Embed( title=color_name, description='(Approx..)', - color=rgb_color, + color=discord_rgb_int, ) file = await self.create_thumbnail_attachment(rgb_color) @@ -120,7 +158,7 @@ class Color(commands.Cog): def _rgb_to_hex(rgb_color: tuple[int, int, int]) -> str: """To convert from `RGB` to `Hex` notation.""" - return '#' + ''.join(hex(color)[2:].zfill(2) for color in rgb_color).upper() + return '#' + ''.join(hex(int(color))[2:].zfill(2) for color in rgb_color).upper() def _rgb_to_cmyk(rgb_color: tuple[int, int, int]) -> tuple[int, int, int, int]: """To convert from `RGB` to `CMYK` color space.""" @@ -178,7 +216,11 @@ class Color(commands.Cog): all_fields = [ { "name": "RGB", - "value": f"Β» rgb {rgb_color}\nΒ» hex {hex_color}" + "value": f"Β» rgb {rgb_color}" + }, + { + "name": "HEX", + "value": f"Β» hex {hex_color}" }, { "name": "CMYK", @@ -197,13 +239,22 @@ class Color(commands.Cog): return all_fields @staticmethod - def match_color(user_color: str) -> str: + def match_color(input_hex_color: str) -> str: """Use fuzzy matching to return a hex color code based on the user's input.""" - match, certainty, _ = process.extractOne(query=user_color, choices=COLOR_MAPPING.keys(), score_cutoff=50) - logger.debug(f"{match = }, {certainty = }") - hex_match = COLOR_MAPPING[match] - logger.debug(f"{hex_match = }") - return match, hex_match + try: + match, certainty, _ = process.extractOne( + query=input_hex_color, + choices=COLOR_MAPPING.keys(), + score_cutoff=50 + ) + logger.debug(f"{match = }, {certainty = }") + hex_match = COLOR_MAPPING[match] + logger.debug(f"{hex_match = }") + return match, hex_match + except TypeError: + match = "No color name match found." + hex_match = input_hex_color + return match, hex_match def setup(bot: Bot) -> None: -- cgit v1.2.3 From 620142ba3586fce88a61886288bafd5f078f95e7 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Sun, 19 Sep 2021 17:49:32 -0400 Subject: Add all color modes and name matching --- bot/exts/utilities/color.py | 140 ++++++++++++++++++++++++++++++++------------ 1 file changed, 104 insertions(+), 36 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 2bec6ba3..9b1f3776 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -45,6 +45,7 @@ class Color(commands.Cog): """ logger.debug(f"{mode = }") logger.debug(f"{user_color = }") + color_name = None if mode.lower() == "hex": hex_match = re.fullmatch(r"(#?[0x]?)((?:[0-9a-fA-F]{3}){1,2})", user_color) if hex_match: @@ -57,19 +58,6 @@ class Color(commands.Cog): else: hex_ = "#" + user_color rgb_color = ImageColor.getcolor(hex_, "RGB") - (r, g, b) = rgb_color - discord_rgb_int = int(f"{r:02x}{g:02x}{b:02x}", 16) - all_colors = self.get_color_fields(rgb_color) - hex_color = all_colors[1]["value"].replace("Β» hex ", "") - cmyk_color = all_colors[2]["value"].replace("Β» cmyk ", "") - hsv_color = all_colors[3]["value"].replace("Β» hsv ", "") - hsl_color = all_colors[4]["value"].replace("Β» hsl ", "") - logger.debug(f"{rgb_color = }") - logger.debug(f"{hex_color = }") - logger.debug(f"{hsv_color = }") - logger.debug(f"{hsl_color = }") - logger.debug(f"{cmyk_color = }") - color_name, _ = self.match_color(hex_color) else: await ctx.send( embed=Embed( @@ -78,30 +66,22 @@ class Color(commands.Cog): ) ) elif mode.lower() == "rgb": - if "(" in user_color: - remove = "[() ]" - rgb_color = re.sub(remove, "", user_color) - rgb_color = tuple(map(int, rgb_color.split(","))) - elif "," in user_color: - rgb_color = tuple(map(int, user_color.split(","))) - else: - rgb_color = tuple(map(int, user_color.split(" "))) - (r, g, b) = rgb_color - discord_rgb_int = int(f"{r:02x}{g:02x}{b:02x}", 16) - all_colors = self.get_color_fields(rgb_color) - hex_color = all_colors[1]["value"].replace("Β» hex ", "") - cmyk_color = all_colors[2]["value"].replace("Β» cmyk ", "") - hsv_color = all_colors[3]["value"].replace("Β» hsv ", "") - hsl_color = all_colors[4]["value"].replace("Β» hsl ", "") - color_name, _ = self.match_color(hex_color) + rgb_color = self.tuple_create(user_color) elif mode.lower() == "hsv": - pass + hsv_temp = self.tuple_create(user_color) + rgb_color = self.hsv_to_rgb(hsv_temp) elif mode.lower() == "hsl": - pass + hsl_temp = self.tuple_create(user_color) + rgb_color = self.hsl_to_rgb(hsl_temp) elif mode.lower() == "cmyk": - pass + cmyk_temp = self.tuple_create(user_color) + rgb_color = self.cmyk_to_rgb(cmyk_temp) elif mode.lower() == "name": - color_name, hex_color = self.match_color(user_color) + color_name, hex_color = self.match_color_name(user_color) + if "#" in hex_color: + rgb_color = ImageColor.getcolor(hex_color, "RGB") + else: + rgb_color = ImageColor.getcolor("#" + hex_color, "RGB") else: # mode is either None or an invalid code if mode is None: @@ -120,6 +100,14 @@ class Color(commands.Cog): await ctx.send(embed=wrong_mode_embed) return + (r, g, b) = rgb_color + discord_rgb_int = int(f"{r:02x}{g:02x}{b:02x}", 16) + all_colors = self.get_color_fields(rgb_color) + hex_color = all_colors[1]["value"].replace("Β» hex ", "") + if color_name is None: + logger.debug(f"Find color name from hex color: {hex_color}") + color_name = self.match_color_hex(hex_color) + async with ctx.typing(): main_embed = Embed( title=color_name, @@ -239,11 +227,11 @@ class Color(commands.Cog): return all_fields @staticmethod - def match_color(input_hex_color: str) -> str: + def match_color_name(input_color_name: str) -> str: """Use fuzzy matching to return a hex color code based on the user's input.""" try: match, certainty, _ = process.extractOne( - query=input_hex_color, + query=input_color_name, choices=COLOR_MAPPING.keys(), score_cutoff=50 ) @@ -253,9 +241,89 @@ class Color(commands.Cog): return match, hex_match except TypeError: match = "No color name match found." - hex_match = input_hex_color + hex_match = input_color_name return match, hex_match + @staticmethod + def match_color_hex(input_hex_color: str) -> str: + """Use fuzzy matching to return a hex color code based on the user's input.""" + try: + match, certainty, _ = process.extractOne( + query=input_hex_color, + choices=COLOR_MAPPING.values(), + score_cutoff=80 + ) + logger.debug(f"{match = }, {certainty = }") + color_name = [name for name, _ in COLOR_MAPPING.items() if _ == match][0] + logger.debug(f"{color_name = }") + return color_name + except TypeError: + color_name = "No color name match found." + return color_name + + @staticmethod + def tuple_create(input_color: str) -> tuple[int, int, int]: + """ + Create a tuple of integers based on user's input. + + Can handle inputs of the types: + (100, 100, 100) + 100, 100, 100 + 100 100 100 + """ + if "(" in input_color: + remove = "[() ]" + color_tuple = re.sub(remove, "", input_color) + color_tuple = tuple(map(int, color_tuple.split(","))) + elif "," in input_color: + color_tuple = tuple(map(int, input_color.split(","))) + else: + color_tuple = tuple(map(int, input_color.split(" "))) + return color_tuple + + @staticmethod + def hsv_to_rgb(input_color: tuple[int, int, int]) -> tuple[int, int, int]: + """Function to convert hsv color to rgb color.""" + (h, v, s) = input_color # the function hsv_to_rgb expects v and s to be swapped + h = h / 360 + s = s / 100 + v = v / 100 + rgb_color = colorsys.hsv_to_rgb(h, s, v) + (r, g, b) = rgb_color + r = int(r * 255) + g = int(g * 255) + b = int(b * 255) + rgb_color = (r, g, b) + return rgb_color + + @staticmethod + def hsl_to_rgb(input_color: tuple[int, int, int]) -> tuple[int, int, int]: + """Function to convert hsl color to rgb color.""" + (h, s, l) = input_color + h = h / 360 + s = s / 100 + l = l / 100 # noqa: E741 It's little `L`, Reason: To maintain consistency. + rgb_color = colorsys.hls_to_rgb(h, l, s) + (r, g, b) = rgb_color + r = int(r * 255) + g = int(g * 255) + b = int(b * 255) + rgb_color = (r, g, b) + return rgb_color + + @staticmethod + def cmyk_to_rgb(input_color: tuple[int, int, int, int]) -> tuple[int, int, int]: + """Function to convert cmyk color to rgb color.""" + c = input_color[0] + m = input_color[1] + y = input_color[2] + k = input_color[3] + r = int(255 * (1.0 - c / float(100)) * (1.0 - k / float(100))) + g = int(255 * (1.0 - m / float(100)) * (1.0 - k / float(100))) + b = int(255 * (1.0 - y / float(100)) * (1.0 - k / float(100))) + rgb_color = (r, g, b) + return rgb_color + def setup(bot: Bot) -> None: """Load the Color Cog.""" -- cgit v1.2.3 From fe2b7d442bec05ab02836e2c5d7613d738fd5151 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Tue, 21 Sep 2021 06:51:55 -0400 Subject: fix: remove redundant rgb_color variable The conversion functions from hsv, hsl and cmyk now return r, g, b instead of a variable rgb_tuple. --- bot/exts/utilities/color.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 9b1f3776..542a2e19 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -293,8 +293,7 @@ class Color(commands.Cog): r = int(r * 255) g = int(g * 255) b = int(b * 255) - rgb_color = (r, g, b) - return rgb_color + return r, g, b @staticmethod def hsl_to_rgb(input_color: tuple[int, int, int]) -> tuple[int, int, int]: @@ -308,8 +307,7 @@ class Color(commands.Cog): r = int(r * 255) g = int(g * 255) b = int(b * 255) - rgb_color = (r, g, b) - return rgb_color + return r, g, b @staticmethod def cmyk_to_rgb(input_color: tuple[int, int, int, int]) -> tuple[int, int, int]: @@ -321,8 +319,7 @@ class Color(commands.Cog): r = int(255 * (1.0 - c / float(100)) * (1.0 - k / float(100))) g = int(255 * (1.0 - m / float(100)) * (1.0 - k / float(100))) b = int(255 * (1.0 - y / float(100)) * (1.0 - k / float(100))) - rgb_color = (r, g, b) - return rgb_color + return r, g, b def setup(bot: Bot) -> None: -- cgit v1.2.3 From c32a3f622be9c5d95602fb455bb557c3cd8a10b8 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Wed, 22 Sep 2021 09:24:23 -0400 Subject: fix: create subcommands and restructure script -Makes "main" function `color_embed` that takes an rgb tuple, calls `all_colors` to get all other color types, gets a name from the hex color, creates embed, calls `create_thumbnail` to get image, and then sends main embed. -Makes functions `xxx_to_rgb` functions to call `color_embed` -Creates new `hex_to_rgb` function -TODO: test all functions and continue restructure. --- bot/exts/utilities/color.py | 81 +++++++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 36 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 542a2e19..5d37cbee 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -47,35 +47,16 @@ class Color(commands.Cog): logger.debug(f"{user_color = }") color_name = None if mode.lower() == "hex": - hex_match = re.fullmatch(r"(#?[0x]?)((?:[0-9a-fA-F]{3}){1,2})", user_color) - if hex_match: - hex_color = int(hex(int(user_color.replace("#", ""), 16)), 0) - if "#" in user_color: - rgb_color = ImageColor.getcolor(user_color, "RGB") - elif "0x" in user_color: - hex_ = user_color.replace("0x", "#") - rgb_color = ImageColor.getcolor(hex_, "RGB") - else: - hex_ = "#" + user_color - rgb_color = ImageColor.getcolor(hex_, "RGB") - else: - await ctx.send( - embed=Embed( - title="There was an issue converting the hex color code.", - description=ERROR_MSG.format(user_color=user_color), - ) - ) + self.hex_to_rgb(ctx, user_color) elif mode.lower() == "rgb": rgb_color = self.tuple_create(user_color) + self.embed_color(rgb_color) elif mode.lower() == "hsv": - hsv_temp = self.tuple_create(user_color) - rgb_color = self.hsv_to_rgb(hsv_temp) + self.hsv_to_rgb(user_color) elif mode.lower() == "hsl": - hsl_temp = self.tuple_create(user_color) - rgb_color = self.hsl_to_rgb(hsl_temp) + self.hsl_to_rgb(user_color) elif mode.lower() == "cmyk": - cmyk_temp = self.tuple_create(user_color) - rgb_color = self.cmyk_to_rgb(cmyk_temp) + self.cmyk_to_rgb(user_color) elif mode.lower() == "name": color_name, hex_color = self.match_color_name(user_color) if "#" in hex_color: @@ -100,6 +81,13 @@ class Color(commands.Cog): await ctx.send(embed=wrong_mode_embed) return + async def color_embed( + self, + ctx: commands.Context, + rgb_color: tuple[int, int, int], + color_name: str = None + ) -> None: + """Take a RGB color tuple, create embed, and send.""" (r, g, b) = rgb_color discord_rgb_int = int(f"{r:02x}{g:02x}{b:02x}", 16) all_colors = self.get_color_fields(rgb_color) @@ -281,9 +269,9 @@ class Color(commands.Cog): color_tuple = tuple(map(int, input_color.split(" "))) return color_tuple - @staticmethod - def hsv_to_rgb(input_color: tuple[int, int, int]) -> tuple[int, int, int]: - """Function to convert hsv color to rgb color.""" + def hsv_to_rgb(self, input_color: tuple[int, int, int]) -> tuple[int, int, int]: + """Function to convert hsv color to rgb color and send main embed..""" + input_color = self.tuple_create(input_color) (h, v, s) = input_color # the function hsv_to_rgb expects v and s to be swapped h = h / 360 s = s / 100 @@ -293,11 +281,11 @@ class Color(commands.Cog): r = int(r * 255) g = int(g * 255) b = int(b * 255) - return r, g, b + self.color_embed((r, g, b)) - @staticmethod - def hsl_to_rgb(input_color: tuple[int, int, int]) -> tuple[int, int, int]: - """Function to convert hsl color to rgb color.""" + def hsl_to_rgb(self, input_color: tuple[int, int, int]) -> tuple[int, int, int]: + """Function to convert hsl color to rgb color and send main embed..""" + input_color = self.tuple_create(input_color) (h, s, l) = input_color h = h / 360 s = s / 100 @@ -307,11 +295,11 @@ class Color(commands.Cog): r = int(r * 255) g = int(g * 255) b = int(b * 255) - return r, g, b + self.color_embed((r, g, b)) - @staticmethod - def cmyk_to_rgb(input_color: tuple[int, int, int, int]) -> tuple[int, int, int]: - """Function to convert cmyk color to rgb color.""" + def cmyk_to_rgb(self, input_color: tuple[int, int, int, int]) -> tuple[int, int, int]: + """Function to convert cmyk color to rgb color and send main embed..""" + input_color = self.tuple_create(input_color) c = input_color[0] m = input_color[1] y = input_color[2] @@ -319,7 +307,28 @@ class Color(commands.Cog): r = int(255 * (1.0 - c / float(100)) * (1.0 - k / float(100))) g = int(255 * (1.0 - m / float(100)) * (1.0 - k / float(100))) b = int(255 * (1.0 - y / float(100)) * (1.0 - k / float(100))) - return r, g, b + self.color_embed((r, g, b)) + + async def hex_to_rgb(self, ctx: commands.Context, hex_string: str) -> None: + """Create rgb color from hex string and send main embed.""" + hex_match = re.fullmatch(r"(#?[0x]?)((?:[0-9a-fA-F]{3}){1,2})", hex_string) + if hex_match: + if "#" in hex_string: + rgb_color = ImageColor.getcolor(hex_string, "RGB") + elif "0x" in hex_string: + hex_ = hex_string.replace("0x", "#") + rgb_color = ImageColor.getcolor(hex_, "RGB") + else: + hex_ = "#" + hex_string + rgb_color = ImageColor.getcolor(hex_, "RGB") + self.color_embed(rgb_color) + else: + await ctx.send( + embed=Embed( + title="There was an issue converting the hex color code.", + description=ERROR_MSG.format(user_color=hex_string), + ) + ) def setup(bot: Bot) -> None: -- cgit v1.2.3 From eeebd656d233541246a407d839a0436f893a4b43 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Wed, 22 Sep 2021 09:38:34 -0400 Subject: fix: restructure script --- bot/exts/utilities/color.py | 225 ++++++++++++++++++++++---------------------- 1 file changed, 115 insertions(+), 110 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 5d37cbee..67068809 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -50,19 +50,20 @@ class Color(commands.Cog): self.hex_to_rgb(ctx, user_color) elif mode.lower() == "rgb": rgb_color = self.tuple_create(user_color) - self.embed_color(rgb_color) + self.color_embed(ctx, rgb_color) elif mode.lower() == "hsv": - self.hsv_to_rgb(user_color) + self.hsv_to_rgb(ctx, user_color) elif mode.lower() == "hsl": - self.hsl_to_rgb(user_color) + self.hsl_to_rgb(ctx, user_color) elif mode.lower() == "cmyk": - self.cmyk_to_rgb(user_color) + self.cmyk_to_rgb(ctx, user_color) elif mode.lower() == "name": color_name, hex_color = self.match_color_name(user_color) if "#" in hex_color: rgb_color = ImageColor.getcolor(hex_color, "RGB") else: rgb_color = ImageColor.getcolor("#" + hex_color, "RGB") + self.color_embed(ctx, rgb_color, color_name) else: # mode is either None or an invalid code if mode is None: @@ -81,44 +82,94 @@ class Color(commands.Cog): await ctx.send(embed=wrong_mode_embed) return - async def color_embed( - self, - ctx: commands.Context, - rgb_color: tuple[int, int, int], - color_name: str = None - ) -> None: - """Take a RGB color tuple, create embed, and send.""" - (r, g, b) = rgb_color - discord_rgb_int = int(f"{r:02x}{g:02x}{b:02x}", 16) - all_colors = self.get_color_fields(rgb_color) - hex_color = all_colors[1]["value"].replace("Β» hex ", "") - if color_name is None: - logger.debug(f"Find color name from hex color: {hex_color}") - color_name = self.match_color_hex(hex_color) + @staticmethod + def tuple_create(input_color: str) -> tuple[int, int, int]: + """ + Create a tuple of integers based on user's input. - async with ctx.typing(): - main_embed = Embed( - title=color_name, - description='(Approx..)', - color=discord_rgb_int, + Can handle inputs of the types: + (100, 100, 100) + 100, 100, 100 + 100 100 100 + """ + if "(" in input_color: + remove = "[() ]" + color_tuple = re.sub(remove, "", input_color) + color_tuple = tuple(map(int, color_tuple.split(","))) + elif "," in input_color: + color_tuple = tuple(map(int, input_color.split(","))) + else: + color_tuple = tuple(map(int, input_color.split(" "))) + return color_tuple + + async def hex_to_rgb(self, ctx: commands.Context, hex_string: str) -> None: + """Function to convert hex color to rgb color and send main embed.""" + hex_match = re.fullmatch(r"(#?[0x]?)((?:[0-9a-fA-F]{3}){1,2})", hex_string) + if hex_match: + if "#" in hex_string: + rgb_color = ImageColor.getcolor(hex_string, "RGB") + elif "0x" in hex_string: + hex_ = hex_string.replace("0x", "#") + rgb_color = ImageColor.getcolor(hex_, "RGB") + else: + hex_ = "#" + hex_string + rgb_color = ImageColor.getcolor(hex_, "RGB") + self.color_embed(rgb_color) + else: + await ctx.send( + embed=Embed( + title="There was an issue converting the hex color code.", + description=ERROR_MSG.format(user_color=hex_string), + ) ) - file = await self.create_thumbnail_attachment(rgb_color) - main_embed.set_thumbnail(url="attachment://color.png") - fields = self.get_color_fields(rgb_color) + def hsv_to_rgb(self, input_color: tuple[int, int, int]) -> tuple[int, int, int]: + """Function to convert hsv color to rgb color and send main embed.""" + input_color = self.tuple_create(input_color) + (h, v, s) = input_color # the function hsv_to_rgb expects v and s to be swapped + h = h / 360 + s = s / 100 + v = v / 100 + rgb_color = colorsys.hsv_to_rgb(h, s, v) + (r, g, b) = rgb_color + r = int(r * 255) + g = int(g * 255) + b = int(b * 255) + self.color_embed((r, g, b)) - for field in fields: - main_embed.add_field( - name=field['name'], - value=field['value'], - inline=False, - ) + def hsl_to_rgb(self, input_color: tuple[int, int, int]) -> tuple[int, int, int]: + """Function to convert hsl color to rgb color and send main embed.""" + input_color = self.tuple_create(input_color) + (h, s, l) = input_color + h = h / 360 + s = s / 100 + l = l / 100 # noqa: E741 It's little `L`, Reason: To maintain consistency. + rgb_color = colorsys.hls_to_rgb(h, l, s) + (r, g, b) = rgb_color + r = int(r * 255) + g = int(g * 255) + b = int(b * 255) + self.color_embed((r, g, b)) - await ctx.send(file=file, embed=main_embed) + def cmyk_to_rgb(self, input_color: tuple[int, int, int, int]) -> tuple[int, int, int]: + """Function to convert cmyk color to rgb color and send main embed.""" + input_color = self.tuple_create(input_color) + c = input_color[0] + m = input_color[1] + y = input_color[2] + k = input_color[3] + r = int(255 * (1.0 - c / float(100)) * (1.0 - k / float(100))) + g = int(255 * (1.0 - m / float(100)) * (1.0 - k / float(100))) + b = int(255 * (1.0 - y / float(100)) * (1.0 - k / float(100))) + self.color_embed((r, g, b)) @staticmethod - async def create_thumbnail_attachment(color: str) -> File: - """Generate a thumbnail from `color`.""" + async def create_thumbnail_attachment(color: tuple[int, int, int]) -> File: + """ + Generate a thumbnail from `color`. + + Assumes that color is an rgb tuple. + """ thumbnail = Image.new("RGB", (100, 100), color=color) bufferedio = BytesIO() thumbnail.save(bufferedio, format="PNG") @@ -249,86 +300,40 @@ class Color(commands.Cog): color_name = "No color name match found." return color_name - @staticmethod - def tuple_create(input_color: str) -> tuple[int, int, int]: - """ - Create a tuple of integers based on user's input. - - Can handle inputs of the types: - (100, 100, 100) - 100, 100, 100 - 100 100 100 - """ - if "(" in input_color: - remove = "[() ]" - color_tuple = re.sub(remove, "", input_color) - color_tuple = tuple(map(int, color_tuple.split(","))) - elif "," in input_color: - color_tuple = tuple(map(int, input_color.split(","))) - else: - color_tuple = tuple(map(int, input_color.split(" "))) - return color_tuple - - def hsv_to_rgb(self, input_color: tuple[int, int, int]) -> tuple[int, int, int]: - """Function to convert hsv color to rgb color and send main embed..""" - input_color = self.tuple_create(input_color) - (h, v, s) = input_color # the function hsv_to_rgb expects v and s to be swapped - h = h / 360 - s = s / 100 - v = v / 100 - rgb_color = colorsys.hsv_to_rgb(h, s, v) + async def color_embed( + self, + ctx: commands.Context, + rgb_color: tuple[int, int, int], + color_name: str = None + ) -> None: + """Take a RGB color tuple, create embed, and send.""" (r, g, b) = rgb_color - r = int(r * 255) - g = int(g * 255) - b = int(b * 255) - self.color_embed((r, g, b)) + discord_rgb_int = int(f"{r:02x}{g:02x}{b:02x}", 16) + all_colors = self.get_color_fields(rgb_color) + hex_color = all_colors[1]["value"].replace("Β» hex ", "") + if color_name is None: + logger.debug(f"Find color name from hex color: {hex_color}") + color_name = self.match_color_hex(hex_color) - def hsl_to_rgb(self, input_color: tuple[int, int, int]) -> tuple[int, int, int]: - """Function to convert hsl color to rgb color and send main embed..""" - input_color = self.tuple_create(input_color) - (h, s, l) = input_color - h = h / 360 - s = s / 100 - l = l / 100 # noqa: E741 It's little `L`, Reason: To maintain consistency. - rgb_color = colorsys.hls_to_rgb(h, l, s) - (r, g, b) = rgb_color - r = int(r * 255) - g = int(g * 255) - b = int(b * 255) - self.color_embed((r, g, b)) + async with ctx.typing(): + main_embed = Embed( + title=color_name, + description='(Approx..)', + color=discord_rgb_int, + ) - def cmyk_to_rgb(self, input_color: tuple[int, int, int, int]) -> tuple[int, int, int]: - """Function to convert cmyk color to rgb color and send main embed..""" - input_color = self.tuple_create(input_color) - c = input_color[0] - m = input_color[1] - y = input_color[2] - k = input_color[3] - r = int(255 * (1.0 - c / float(100)) * (1.0 - k / float(100))) - g = int(255 * (1.0 - m / float(100)) * (1.0 - k / float(100))) - b = int(255 * (1.0 - y / float(100)) * (1.0 - k / float(100))) - self.color_embed((r, g, b)) + file = await self.create_thumbnail_attachment(rgb_color) + main_embed.set_thumbnail(url="attachment://color.png") + fields = self.get_color_fields(rgb_color) - async def hex_to_rgb(self, ctx: commands.Context, hex_string: str) -> None: - """Create rgb color from hex string and send main embed.""" - hex_match = re.fullmatch(r"(#?[0x]?)((?:[0-9a-fA-F]{3}){1,2})", hex_string) - if hex_match: - if "#" in hex_string: - rgb_color = ImageColor.getcolor(hex_string, "RGB") - elif "0x" in hex_string: - hex_ = hex_string.replace("0x", "#") - rgb_color = ImageColor.getcolor(hex_, "RGB") - else: - hex_ = "#" + hex_string - rgb_color = ImageColor.getcolor(hex_, "RGB") - self.color_embed(rgb_color) - else: - await ctx.send( - embed=Embed( - title="There was an issue converting the hex color code.", - description=ERROR_MSG.format(user_color=hex_string), + for field in fields: + main_embed.add_field( + name=field['name'], + value=field['value'], + inline=False, ) - ) + + await ctx.send(file=file, embed=main_embed) def setup(bot: Bot) -> None: -- cgit v1.2.3 From 8a9d71b77a6385bd2dbf8388a732e980fe66dac2 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Wed, 22 Sep 2021 09:39:37 -0400 Subject: fix: remove `get_color_fields` call in color_embed --- bot/exts/utilities/color.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 67068809..5cdc5083 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -324,9 +324,8 @@ class Color(commands.Cog): file = await self.create_thumbnail_attachment(rgb_color) main_embed.set_thumbnail(url="attachment://color.png") - fields = self.get_color_fields(rgb_color) - for field in fields: + for field in all_colors: main_embed.add_field( name=field['name'], value=field['value'], -- cgit v1.2.3 From bacd5119f59c8d425ba9f129aff9191c98063a67 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Wed, 22 Sep 2021 09:50:42 -0400 Subject: chore: small code fixes and cleanup --- bot/exts/utilities/color.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 5cdc5083..e0398e02 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -29,7 +29,6 @@ with open(COLOR_JSON_PATH) as f: COLOR_MAPPING = json.load(f) -# define color command class Color(commands.Cog): """User initiated commands to receive color information.""" @@ -45,7 +44,6 @@ class Color(commands.Cog): """ logger.debug(f"{mode = }") logger.debug(f"{user_color = }") - color_name = None if mode.lower() == "hex": self.hex_to_rgb(ctx, user_color) elif mode.lower() == "rgb": @@ -235,11 +233,6 @@ class Color(commands.Cog): l = round(l * 100) # noqa: E741 It's little `L`, Reason: To maintain consistency. return h, s, l - hex_color = _rgb_to_hex(rgb_color) - cmyk_color = _rgb_to_cmyk(rgb_color) - hsv_color = _rgb_to_hsv(rgb_color) - hsl_color = _rgb_to_hsl(rgb_color) - all_fields = [ { "name": "RGB", @@ -247,19 +240,19 @@ class Color(commands.Cog): }, { "name": "HEX", - "value": f"Β» hex {hex_color}" + "value": f"Β» hex {_rgb_to_hex(rgb_color)}" }, { "name": "CMYK", - "value": f"Β» cmyk {cmyk_color}" + "value": f"Β» cmyk {_rgb_to_cmyk(rgb_color)}" }, { "name": "HSV", - "value": f"Β» hsv {hsv_color}" + "value": f"Β» hsv {_rgb_to_hsv(rgb_color)}" }, { "name": "HSL", - "value": f"Β» hsl {hsl_color}" + "value": f"Β» hsl {_rgb_to_hsl(rgb_color)}" }, ] @@ -277,11 +270,11 @@ class Color(commands.Cog): logger.debug(f"{match = }, {certainty = }") hex_match = COLOR_MAPPING[match] logger.debug(f"{hex_match = }") - return match, hex_match except TypeError: match = "No color name match found." hex_match = input_color_name - return match, hex_match + + return match, hex_match @staticmethod def match_color_hex(input_hex_color: str) -> str: @@ -295,10 +288,10 @@ class Color(commands.Cog): logger.debug(f"{match = }, {certainty = }") color_name = [name for name, _ in COLOR_MAPPING.items() if _ == match][0] logger.debug(f"{color_name = }") - return color_name except TypeError: color_name = "No color name match found." - return color_name + + return color_name async def color_embed( self, -- cgit v1.2.3 From 23cf7cdd40dc162727e0697ccc6f802a84e9a7e0 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Thu, 23 Sep 2021 00:22:10 -0400 Subject: chore: create subcommands for sending embed --- bot/exts/utilities/color.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index e0398e02..6cc03c9a 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -45,23 +45,23 @@ class Color(commands.Cog): logger.debug(f"{mode = }") logger.debug(f"{user_color = }") if mode.lower() == "hex": - self.hex_to_rgb(ctx, user_color) + await self.hex_to_rgb(ctx, user_color) elif mode.lower() == "rgb": rgb_color = self.tuple_create(user_color) - self.color_embed(ctx, rgb_color) + await self.color_embed(ctx, rgb_color) elif mode.lower() == "hsv": - self.hsv_to_rgb(ctx, user_color) + await self.hsv_to_rgb(ctx, user_color) elif mode.lower() == "hsl": - self.hsl_to_rgb(ctx, user_color) + await self.hsl_to_rgb(ctx, user_color) elif mode.lower() == "cmyk": - self.cmyk_to_rgb(ctx, user_color) + await self.cmyk_to_rgb(ctx, user_color) elif mode.lower() == "name": color_name, hex_color = self.match_color_name(user_color) if "#" in hex_color: rgb_color = ImageColor.getcolor(hex_color, "RGB") else: rgb_color = ImageColor.getcolor("#" + hex_color, "RGB") - self.color_embed(ctx, rgb_color, color_name) + await self.color_embed(ctx, rgb_color, color_name) else: # mode is either None or an invalid code if mode is None: @@ -112,7 +112,7 @@ class Color(commands.Cog): else: hex_ = "#" + hex_string rgb_color = ImageColor.getcolor(hex_, "RGB") - self.color_embed(rgb_color) + await self.color_embed(ctx, rgb_color) else: await ctx.send( embed=Embed( @@ -121,7 +121,7 @@ class Color(commands.Cog): ) ) - def hsv_to_rgb(self, input_color: tuple[int, int, int]) -> tuple[int, int, int]: + async def hsv_to_rgb(self, ctx: commands.Context, input_color: tuple[int, int, int]) -> tuple[int, int, int]: """Function to convert hsv color to rgb color and send main embed.""" input_color = self.tuple_create(input_color) (h, v, s) = input_color # the function hsv_to_rgb expects v and s to be swapped @@ -133,9 +133,9 @@ class Color(commands.Cog): r = int(r * 255) g = int(g * 255) b = int(b * 255) - self.color_embed((r, g, b)) + await self.color_embed(ctx, (r, g, b)) - def hsl_to_rgb(self, input_color: tuple[int, int, int]) -> tuple[int, int, int]: + async def hsl_to_rgb(self, ctx: commands.Context, input_color: tuple[int, int, int]) -> tuple[int, int, int]: """Function to convert hsl color to rgb color and send main embed.""" input_color = self.tuple_create(input_color) (h, s, l) = input_color @@ -147,9 +147,9 @@ class Color(commands.Cog): r = int(r * 255) g = int(g * 255) b = int(b * 255) - self.color_embed((r, g, b)) + await self.color_embed(ctx, (r, g, b)) - def cmyk_to_rgb(self, input_color: tuple[int, int, int, int]) -> tuple[int, int, int]: + async def cmyk_to_rgb(self, ctx: commands.Context, input_color: tuple[int, int, int, int]) -> tuple[int, int, int]: """Function to convert cmyk color to rgb color and send main embed.""" input_color = self.tuple_create(input_color) c = input_color[0] @@ -159,7 +159,7 @@ class Color(commands.Cog): r = int(255 * (1.0 - c / float(100)) * (1.0 - k / float(100))) g = int(255 * (1.0 - m / float(100)) * (1.0 - k / float(100))) b = int(255 * (1.0 - y / float(100)) * (1.0 - k / float(100))) - self.color_embed((r, g, b)) + await self.color_embed(ctx, (r, g, b)) @staticmethod async def create_thumbnail_attachment(color: tuple[int, int, int]) -> File: -- cgit v1.2.3 From 8ff23478fb2bd9cfba34686be68513ab3ac7b905 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Thu, 23 Sep 2021 07:48:45 -0400 Subject: chore: make cmyk_to_rgb def multiline --- bot/exts/utilities/color.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 6cc03c9a..9e2af325 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -149,7 +149,11 @@ class Color(commands.Cog): b = int(b * 255) await self.color_embed(ctx, (r, g, b)) - async def cmyk_to_rgb(self, ctx: commands.Context, input_color: tuple[int, int, int, int]) -> tuple[int, int, int]: + async def cmyk_to_rgb( + self, + ctx: commands.Context, + input_color: tuple[int, int, int, int] + ) -> tuple[int, int, int]: """Function to convert cmyk color to rgb color and send main embed.""" input_color = self.tuple_create(input_color) c = input_color[0] -- cgit v1.2.3 From 5907900a80ce7bdeb6ddb97c80dbdd4defe0bb17 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Mon, 27 Sep 2021 09:49:42 -0400 Subject: chore: remove doubled new line in ERROR_MSG --- bot/exts/utilities/color.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 9e2af325..7c7f4bba 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -17,11 +17,11 @@ logger = logging.getLogger(__name__) ERROR_MSG = """The color code {user_color} is not a possible color combination. -\nThe range of possible values are: -\nRGB & HSV: 0-255 -\nCMYK: 0-100% -\nHSL: 0-360 degrees -\nHex: #000000-#FFFFFF +The range of possible values are: +RGB & HSV: 0-255 +CMYK: 0-100% +HSL: 0-360 degrees +Hex: #000000-#FFFFFF """ COLOR_JSON_PATH = "bot/resources/utilities/ryanzec_colours.json" -- cgit v1.2.3 From a78731ad2cf457b25d311460578a778070900385 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Mon, 27 Sep 2021 09:56:33 -0400 Subject: chore: remove single-use constant for json path --- bot/exts/utilities/color.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 7c7f4bba..ac7b5fc6 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -24,8 +24,7 @@ HSL: 0-360 degrees Hex: #000000-#FFFFFF """ -COLOR_JSON_PATH = "bot/resources/utilities/ryanzec_colours.json" -with open(COLOR_JSON_PATH) as f: +with open("bot/resources/utilities/ryanzec_colours.json") as f: COLOR_MAPPING = json.load(f) -- cgit v1.2.3 From 9699e3a62d03d27acaac6e5376c2327cb8abf739 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Thu, 23 Sep 2021 21:42:24 -0400 Subject: chore: set thumbnail image to 80x80 --- bot/exts/utilities/color.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index ac7b5fc6..57510488 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -171,7 +171,7 @@ class Color(commands.Cog): Assumes that color is an rgb tuple. """ - thumbnail = Image.new("RGB", (100, 100), color=color) + thumbnail = Image.new("RGB", (80, 80), color=color) bufferedio = BytesIO() thumbnail.save(bufferedio, format="PNG") bufferedio.seek(0) -- cgit v1.2.3 From 11c1c25a4767086c92b7a438aaee09fb65dcfdf1 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Mon, 27 Sep 2021 10:46:08 -0400 Subject: chore: remove single-use constant for json path --- bot/exts/utilities/color.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 57510488..6aa0c3cd 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -65,7 +65,7 @@ class Color(commands.Cog): # mode is either None or an invalid code if mode is None: no_mode_embed = Embed( - title="No 'mode' was passed, please define a color code.", + title="No mode was passed, please define a color code.", description="Possible modes are: Name, Hex, RGB, HSV, HSL and CMYK.", color=Colours.soft_red, ) -- cgit v1.2.3 From dccd30e27a594dad808a20bacef213c461ea2c48 Mon Sep 17 00:00:00 2001 From: Mayur Odedara Date: Fri, 8 Oct 2021 00:57:57 +0530 Subject: Added Anagrams command (#874) * Added Anagrams command Added 2 files anagram.py - Has the code for anagram command anagram.json - Contains all the words for anagram command * Update bot/exts/fun/anagram.py Using "with" for resource file instead of getting data directly Co-authored-by: brad90four <42116429+brad90four@users.noreply.github.com> * Update bot/exts/fun/anagram.py Updated title text for answer embed Co-authored-by: Bluenix * Update bot/exts/fun/anagram.py Comma separated text for winners list Co-authored-by: Bluenix * Updated anagram.py as per review comments -Removed redundant variables -Updated embed text to avoid 'all' -Updated stale comments * Some minor formatting fixes -Added trailing commas to embed -Updated all embeds to have consistent format * Polish anagram command for multiple channels * Updated docstrings * Allowed command to be used in multiple channels * Create a class for anagram game instances * Lay groundwork for threads Co-Authored-By: Bluenix * Updated resource file for anagram command * Anagrams are now cross referenced with list of common words which should be easy for users to guess * It should not have any slur words * Update bot/exts/fun/anagram.py Co-authored-by: brad90four <42116429+brad90four@users.noreply.github.com> * Update bot/exts/fun/anagram.py Co-authored-by: brad90four <42116429+brad90four@users.noreply.github.com> * Update bot/exts/fun/anagram.py Co-authored-by: brad90four <42116429+brad90four@users.noreply.github.com> * Update bot/exts/fun/anagram.py Co-authored-by: brad90four <42116429+brad90four@users.noreply.github.com> * Linting error fix Linting error fix * Error fix Removed the "seconds" causing issue for anagram command * Revert "Error fix" This reverts commit 8c00d70f9faf62c536eac1fa61877dfab328a83f. * Error fix for seconds Fixed the error by removing "seconds" Co-authored-by: brad90four <42116429+brad90four@users.noreply.github.com> Co-authored-by: Bluenix Co-authored-by: Xithrius <15021300+Xithrius@users.noreply.github.com> --- bot/exts/fun/anagram.py | 110 + bot/resources/fun/anagram.json | 17668 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 17778 insertions(+) create mode 100644 bot/exts/fun/anagram.py create mode 100644 bot/resources/fun/anagram.json diff --git a/bot/exts/fun/anagram.py b/bot/exts/fun/anagram.py new file mode 100644 index 00000000..9aee5f18 --- /dev/null +++ b/bot/exts/fun/anagram.py @@ -0,0 +1,110 @@ +import asyncio +import json +import logging +import random +from pathlib import Path + +import discord +from discord.ext import commands + +from bot.bot import Bot +from bot.constants import Colours + +log = logging.getLogger(__name__) + +TIME_LIMIT = 60 + +# anagram.json file contains all the anagrams +with open(Path("bot/resources/fun/anagram.json"), "r") as f: + ANAGRAMS_ALL = json.load(f) + + +class AnagramGame: + """ + Used for creating instances of anagram games. + + Once multiple games can be run at the same time, this class' instances + can be used for keeping track of each anagram game. + """ + + def __init__(self, scrambled: str, correct: list[str]) -> None: + self.scrambled = scrambled + self.correct = set(correct) + + self.winners = set() + + async def message_creation(self, message: discord.Message) -> None: + """Check if the message is a correct answer and remove it from the list of answers.""" + if message.content.lower() in self.correct: + self.winners.add(message.author.mention) + self.correct.remove(message.content.lower()) + + +class Anagram(commands.Cog): + """Cog for the Anagram game command.""" + + def __init__(self, bot: Bot): + self.bot = bot + + self.games: dict[int, AnagramGame] = {} + + @commands.command(name="anagram", aliases=("anag", "gram", "ag")) + @commands.guild_only() + async def anagram_command(self, ctx: commands.Context) -> None: + """ + Given shuffled letters, rearrange them into anagrams. + + Show an embed with scrambled letters which if rearranged can form words. + After a specific amount of time, list the correct answers and whether someone provided a + correct answer. + """ + if self.games.get(ctx.channel.id): + await ctx.send("An anagram is already being solved in this channel!") + return + + scrambled_letters, correct = random.choice(list(ANAGRAMS_ALL.items())) + + game = AnagramGame(scrambled_letters, correct) + self.games[ctx.channel.id] = game + + anagram_embed = discord.Embed( + title=f"Find anagrams from these letters: '{scrambled_letters.upper()}'", + description=f"You have {TIME_LIMIT} seconds to find correct words.", + colour=Colours.purple, + ) + + await ctx.send(embed=anagram_embed) + await asyncio.sleep(TIME_LIMIT) + + if game.winners: + win_list = ", ".join(game.winners) + content = f"Well done {win_list} for getting it right!" + else: + content = "Nobody got it right." + + answer_embed = discord.Embed( + title=f"The words were: `{'`, `'.join(ANAGRAMS_ALL[game.scrambled])}`!", + colour=Colours.pink, + ) + + await ctx.send(content, embed=answer_embed) + + # Game is finished, let's remove it from the dict + self.games.pop(ctx.channel.id) + + @commands.Cog.listener() + async def on_message(self, message: discord.Message) -> None: + """Check a message for an anagram attempt and pass to an ongoing game.""" + if message.author.bot or not message.guild: + return + + game = self.games.get(message.channel.id) + if not game: + return + + await game.message_creation(message) + + +def setup(bot: Bot) -> None: + """Load the Anagram cog.""" + bot.add_cog(Anagram(bot)) diff --git a/bot/resources/fun/anagram.json b/bot/resources/fun/anagram.json new file mode 100644 index 00000000..ea5a5794 --- /dev/null +++ b/bot/resources/fun/anagram.json @@ -0,0 +1,17668 @@ +{ + "eht": [ + "eth", + "het", + "the" + ], + "adn": [ + "and", + "dan", + "nad" + ], + "for": [ + "for", + "fro", + "orf" + ], + "ahtt": [ + "hatt", + "tath", + "that" + ], + "hist": [ + "hist", + "hits", + "isth", + "shit", + "sith", + "this", + "tshi" + ], + "hitw": [ + "whit", + "with" + ], + "not": [ + "not", + "ont", + "ton" + ], + "aer": [ + "aer", + "are", + "ear", + "era", + "rea" + ], + "fmor": [ + "form", + "from" + ], + "enw": [ + "new", + "wen" + ], + "emor": [ + "mero", + "more", + "omer", + "rome" + ], + "asw": [ + "saw", + "swa", + "was" + ], + "acn": [ + "anc", + "can" + ], + "aegp": [ + "gape", + "page", + "peag", + "pega" + ], + "ahs": [ + "ahs", + "ash", + "has", + "sah", + "sha" + ], + "acehrs": [ + "arches", + "ascher", + "casher", + "chares", + "chaser", + "eschar", + "raches", + "recash", + "search" + ], + "eefr": [ + "feer", + "fere", + "free", + "reef" + ], + "btu": [ + "btu", + "but", + "tub" + ], + "oru": [ + "our", + "uro" + ], + "eno": [ + "eon", + "neo", + "one" + ], + "ehort": [ + "other", + "theor", + "thore", + "throe", + "toher" + ], + "eimt": [ + "emit", + "item", + "mite", + "time" + ], + "ehty": [ + "hyte", + "yeth", + "they" + ], + "eist": [ + "seit", + "site", + "ties" + ], + "amy": [ + "amy", + "yam", + "may", + "mya" + ], + "ahtw": [ + "thaw", + "wath", + "what" + ], + "ehirt": [ + "ither", + "rithe", + "their" + ], + "ensw": [ + "news", + "sewn", + "snew", + "wens" + ], + "otu": [ + "out", + "tou" + ], + "esu": [ + "esu", + "sue", + "use" + ], + "any": [ + "any", + "yan", + "nay" + ], + "eehrt": [ + "ether", + "rethe", + "theer", + "there", + "three" + ], + "ees": [ + "ese", + "see" + ], + "lnoy": [ + "lyon", + "loyn", + "only" + ], + "his": [ + "his", + "hsi", + "ihs", + "ish", + "shi" + ], + "ehnw": [ + "hewn", + "when" + ], + "eehr": [ + "heer", + "here" + ], + "how": [ + "how", + "who" + ], + "alos": [ + "also", + "laos", + "sola" + ], + "now": [ + "now", + "own", + "won" + ], + "egt": [ + "get", + "gte", + "teg" + ], + "eivw": [ + "view", + "wive" + ], + "eilnno": [ + "lionne", + "online" + ], + "first": [ + "first", + "frist", + "frits", + "rifts" + ], + "been": [ + "been", + "bene", + "eben" + ], + "eerw": [ + "ewer", + "weer", + "were" + ], + "ceeirssv": [ + "scrieves", + "services" + ], + "emos": [ + "meso", + "mose", + "some" + ], + "eehst": [ + "sheet", + "these" + ], + "ist": [ + "ist", + "its", + "sit", + "tis", + "tsi" + ], + "eikl": [ + "kiel", + "like" + ], + "ceeirsv": [ + "cerevis", + "cresive", + "scrieve", + "service" + ], + "ahnt": [ + "hant", + "tanh", + "than" + ], + "ceipr": [ + "price", + "recip", + "repic" + ], + "adet": [ + "adet", + "ated", + "date", + "tade", + "tead", + "teda" + ], + "opt": [ + "opt", + "pot", + "top" + ], + "adh": [ + "dah", + "dha", + "had" + ], + "ilst": [ + "list", + "lits", + "silt", + "slit", + "tils" + ], + "aemn": [ + "amen", + "enam", + "mane", + "mean", + "name", + "nema" + ], + "jstu": [ + "just", + "juts" + ], + "eorv": [ + "over", + "rove" + ], + "aestt": [ + "state", + "taste", + "tates", + "teats", + "testa" + ], + "aery": [ + "aery", + "ayre", + "eyra", + "yare", + "year" + ], + "ady": [ + "ady", + "day", + "yad" + ], + "inot": [ + "into", + "nito", + "oint", + "tino" + ], + "aeilm": [ + "email", + "maile", + "malie", + "melia" + ], + "otw": [ + "owt", + "tow", + "two", + "wot" + ], + "desu": [ + "deus", + "dues", + "sude", + "sued", + "used" + ], + "alst": [ + "alts", + "last", + "lats", + "salt", + "slat" + ], + "most": [ + "most", + "mots", + "toms" + ], + "cimsu": [ + "musci", + "music" + ], + "aadt": [ + "adat", + "data" + ], + "aekm": [ + "kame", + "make", + "meak" + ], + "ehmt": [ + "meth", + "them" + ], + "emssty": [ + "mystes", + "system" + ], + "opst": [ + "opts", + "post", + "pots", + "spot", + "stop", + "tops" + ], + "ehr": [ + "her", + "reh", + "rhe" + ], + "add": [ + "add", + "dad" + ], + "chsu": [ + "cush", + "such" + ], + "aeelps": [ + "asleep", + "elapse", + "please", + "sapele" + ], + "aeegmss": [ + "megasse", + "message" + ], + "aefrt": [ + "afret", + "after", + "frate", + "trefa" + ], + "best": [ + "best", + "bets" + ], + "aeforstw": [ + "forwaste", + "software" + ], + "ehnt": [ + "hent", + "neth", + "then" + ], + "ellw": [ + "llew", + "well" + ], + "eehrw": [ + "hewer", + "wheer", + "where" + ], + "fino": [ + "fino", + "foin", + "info" + ], + "ghirst": [ + "girths", + "griths", + "rights" + ], + "bkoos": [ + "bokos", + "books" + ], + "chloos": [ + "cholos", + "school" + ], + "aceh": [ + "ache", + "each", + "haec" + ], + "iklns": [ + "kilns", + "links", + "slink" + ], + "ehs": [ + "hes", + "she" + ], + "eeirvw": [ + "review", + "viewer" + ], + "aersy": [ + "eyras", + "years", + "reasy", + "resay", + "sayer", + "seary" + ], + "bkoo": [ + "boko", + "book" + ], + "eimst": [ + "emits", + "items", + "metis", + "mites", + "smite", + "stime", + "times" + ], + "acmnopy": [ + "company", + "copyman" + ], + "ader": [ + "ared", + "daer", + "dare", + "dear", + "read" + ], + "deen": [ + "dene", + "eden", + "ende", + "ened", + "need" + ], + "amny": [ + "many", + "myna" + ], + "ersu": [ + "rues", + "ruse", + "suer", + "sure", + "user" + ], + "adis": [ + "aids", + "dais", + "dasi", + "dias", + "disa", + "sadi", + "said", + "sida" + ], + "deos": [ + "does", + "dose", + "odes" + ], + "est": [ + "est", + "set" + ], + "denru": [ + "nuder", + "rendu", + "runed", + "under", + "unred" + ], + "aeeglnr": [ + "enlarge", + "general", + "gleaner" + ], + "aceehrrs": [ + "reachers", + "rechaser", + "research", + "searcher" + ], + "ailm": [ + "amil", + "amli", + "lima", + "mail", + "mali", + "mila" + ], + "amp": [ + "amp", + "map", + "pam" + ], + "eeirsvw": [ + "reviews", + "viewers" + ], + "efil": [ + "feil", + "fiel", + "file", + "leif", + "lief", + "life" + ], + "know": [ + "know", + "wonk" + ], + "aegms": [ + "games", + "mages" + ], + "awy": [ + "yaw", + "way" + ], + "adsy": [ + "days", + "dyas" + ], + "aprt": [ + "part", + "prat", + "rapt", + "tarp", + "trap" + ], + "cdlou": [ + "cloud", + "could" + ], + "aegrt": [ + "gater", + "grate", + "great", + "greta", + "retag", + "targe", + "terga" + ], + "deintu": [ + "dunite", + "united", + "untied" + ], + "ehlot": [ + "helot", + "hotel", + "theol", + "thole" + ], + "aelr": [ + "arle", + "earl", + "eral", + "lare", + "lear", + "rale", + "real" + ], + "ceenrt": [ + "center", + "centre", + "entrec", + "recent", + "tenrec" + ], + "abey": [ + "abey", + "abye" + ], + "mstu": [ + "must", + "muts", + "smut", + "stum" + ], + "eorst": [ + "roset", + "rotes", + "rotse", + "soter", + "stero", + "store", + "tores", + "torse" + ], + "aelrtv": [ + "travel", + "varlet" + ], + "adem": [ + "dame", + "edam", + "emda", + "made", + "maed", + "mead" + ], + "eoprrt": [ + "porret", + "porter", + "pretor", + "report", + "troper" + ], + "adeilst": [ + "details", + "dilates", + "distale", + "salited" + ], + "eiln": [ + "lien", + "line", + "neil", + "nile" + ], + "emrst": [ + "mster", + "terms" + ], + "ehlost": [ + "helots", + "hostel", + "hostle", + "hotels", + "tholes" + ], + "dens": [ + "dens", + "ends", + "send", + "sned" + ], + "ghirt": [ + "girth", + "grith", + "right" + ], + "abceesu": [ + "because", + "besauce" + ], + "acllo": [ + "callo", + "colla", + "local" + ], + "ehost": [ + "ethos", + "shote", + "theos", + "those" + ], + "ginsu": [ + "suing", + "using" + ], + "elrsstu": [ + "lusters", + "lustres", + "results", + "rustles", + "sutlers", + "tussler", + "ulsters" + ], + "ceffio": [ + "coiffe", + "office" + ], + "acdeinotu": [ + "auctioned", + "cautioned", + "coadunite", + "education", + "noctuidae" + ], + "aailnnot": [ + "latonian", + "nataloin", + "national" + ], + "acr": [ + "arc", + "car" + ], + "degins": [ + "deigns", + "design", + "sdeign", + "signed", + "singed" + ], + "aekt": [ + "kate", + "keat", + "keta", + "take", + "teak" + ], + "deopst": [ + "depots", + "despot", + "posted", + "stoped" + ], + "eeinnrtt": [ + "internet", + "renitent", + "trentine" + ], + "hiintw": [ + "inwith", + "whitin", + "within" + ], + "aesstt": [ + "estats", + "states", + "tasset", + "tastes" + ], + "antw": [ + "nawt", + "tawn", + "want" + ], + "ehnop": [ + "pheon", + "phone" + ], + "deeerrsv": [ + "deserver", + "reserved", + "reversed" + ], + "abdes": [ + "based", + "beads", + "sabed" + ], + "cdeo": [ + "code", + "coed", + "deco", + "ecod" + ], + "hosw": [ + "hows", + "show" + ], + "eenv": [ + "even", + "neve", + "veen" + ], + "aceilps": [ + "plaices", + "special" + ], + "ceiprs": [ + "crepis", + "cripes", + "persic", + "precis", + "prices", + "spicer" + ], + "deinx": [ + "index", + "nixed" + ], + "begin": [ + "begin", + "being", + "binge" + ], + "emnow": [ + "menow", + "women" + ], + "chmu": [ + "chum", + "much" + ], + "gins": [ + "gins", + "sign", + "sing", + "snig" + ], + "ikln": [ + "kiln", + "link" + ], + "enop": [ + "nope", + "open", + "peon", + "pone" + ], + "adoty": [ + "doaty", + "toady", + "today" + ], + "hostu": [ + "shout", + "south", + "thous" + ], + "aces": [ + "aces", + "aesc", + "case", + "esca" + ], + "aems": [ + "asem", + "maes", + "meas", + "mesa", + "same", + "seam" + ], + "aegps": [ + "gapes", + "pages", + "peags" + ], + "einorsv": [ + "renvois", + "version" + ], + "ceinost": [ + "contise", + "noetics", + "notices", + "section" + ], + "dfnou": [ + "fondu", + "found" + ], + "oprsst": [ + "sports", + "strops" + ], + "adeelrt": [ + "alerted", + "altered", + "delater", + "latrede", + "redealt", + "related", + "treadle" + ], + "bhot": [ + "both", + "thob" + ], + "aaceimnr": [ + "amacrine", + "american", + "camarine", + "camerina", + "cinerama" + ], + "aegm": [ + "egma", + "game", + "mage" + ], + "acer": [ + "acer", + "acre", + "care", + "cera", + "crea", + "race" + ], + "alott": [ + "lotta", + "total" + ], + "acelp": [ + "capel", + "clape", + "place" + ], + "den": [ + "den", + "end", + "ned" + ], + "addlnoow": [ + "download", + "woodland" + ], + "hiottuw": [ + "outwith", + "without" + ], + "epr": [ + "per", + "pre", + "rep" + ], + "hnort": [ + "north", + "thorn" + ], + "ceeorrssu": [ + "recourses", + "resources" + ], + "opsst": [ + "posts", + "spots", + "stops" + ], + "bgi": [ + "big", + "gib" + ], + "adeim": [ + "aimed", + "amide", + "damie", + "media" + ], + "alw": [ + "alw", + "awl", + "law" + ], + "aertw": [ + "tawer", + "water", + "wreat" + ], + "hiorsty": [ + "history", + "toryish" + ], + "ceiprstu": [ + "crepitus", + "cuprites", + "pictures", + "piecrust" + ], + "art": [ + "art", + "rat", + "tar", + "tra" + ], + "ceins": [ + "cines", + "senci", + "since" + ], + "degiu": [ + "digue", + "guide" + ], + "hops": [ + "hops", + "hosp", + "phos", + "posh", + "shop", + "soph" + ], + "abdor": [ + "abord", + "bardo", + "board", + "broad", + "dobra", + "dorab" + ], + "acilnoot": [ + "colation", + "coontail", + "location" + ], + "ehitw": [ + "white", + "withe" + ], + "allms": [ + "malls", + "small" + ], + "aginrt": [ + "gratin", + "rating", + "taring", + "tringa" + ], + "aert": [ + "aret", + "arte", + "erat", + "rate", + "tare", + "tear", + "tera" + ], + "dginru": [ + "during", + "ungird", + "ungrid" + ], + "asu": [ + "aus", + "sau", + "usa" + ], + "enrrtu": [ + "return", + "turner" + ], + "eisst": [ + "sesti", + "siest", + "sites", + "sties" + ], + "eioprsuv": [ + "pervious", + "previous", + "viperous" + ], + "eenstv": [ + "events", + "steven" + ], + "elov": [ + "levo", + "love", + "velo", + "vole" + ], + "dlo": [ + "dol", + "lod", + "old" + ], + "aimn": [ + "amin", + "anim", + "iman", + "main", + "mani", + "mian", + "mina", + "naim" + ], + "cdeiinoprst": [ + "description", + "discerption", + "predictions" + ], + "aceinnrsu": [ + "insurance", + "nuisancer" + ], + "aehnort": [ + "another", + "athenor", + "rheotan" + ], + "hwy": [ + "hwy", + "why" + ], + "ahlls": [ + "halls", + "shall" + ], + "illst": [ + "lilts", + "still", + "tills" + ], + "emnoy": [ + "emony", + "moyen", + "money" + ], + "eervy": [ + "every", + "veery", + "verey" + ], + "giilnst": [ + "listing", + "silting", + "sliting", + "tilings" + ], + "eilltt": [ + "little", + "tillet" + ], + "iistv": [ + "visit", + "vitis" + ], + "aesv": [ + "aves", + "save", + "vase" + ], + "loost": [ + "loots", + "lotos", + "sloot", + "sotol", + "stool", + "tools" + ], + "low": [ + "low", + "lwo", + "owl" + ], + "elpry": [ + "lepry", + "plyer", + "reply" + ], + "cemorstu": [ + "costumer", + "customer" + ], + "acemopr": [ + "compare", + "compear" + ], + "cdeilnu": [ + "include", + "nuclide" + ], + "aeluv": [ + "uveal", + "value" + ], + "aceilrt": [ + "article", + "recital" + ], + "kory": [ + "york", + "kory", + "roky" + ], + "amn": [ + "man", + "mna", + "nam" + ], + "deioprv": [ + "dropvie", + "prevoid", + "provide" + ], + "ceorsu": [ + "cerous", + "course", + "crouse", + "source" + ], + "aelnr": [ + "learn", + "neral", + "renal" + ], + "aels": [ + "ales", + "elsa", + "lase", + "leas", + "sale", + "seal", + "slae" + ], + "adnoru": [ + "around", + "arundo" + ], + "bjo": [ + "job", + "obj" + ], + "ceoprss": [ + "corpses", + "process" + ], + "eent": [ + "eten", + "neet", + "nete", + "teen" + ], + "moor": [ + "moor", + "moro", + "room" + ], + "oot": [ + "oot", + "oto", + "too" + ], + "cdeirt": [ + "credit", + "direct", + "triced" + ], + "inopt": [ + "pinot", + "pinto", + "piton", + "point" + ], + "ijno": [ + "join", + "joni" + ], + "aceegiorst": [ + "categories", + "categorise" + ], + "estw": [ + "stew", + "tews", + "west", + "wets" + ], + "aelss": [ + "lases", + "sales", + "salse", + "seals" + ], + "kloo": [ + "kolo", + "look" + ], + "eghilns": [ + "english", + "shingle" + ], + "eflt": [ + "felt", + "flet", + "left" + ], + "aemt": [ + "mate", + "meat", + "meta", + "tame", + "team", + "tema" + ], + "aeestt": [ + "estate", + "testae" + ], + "ceelst": [ + "elects", + "select" + ], + "hoopst": [ + "photos", + "pothos" + ], + "agy": [ + "agy", + "gay" + ], + "adehrt": [ + "dearth", + "hatred", + "rathed", + "thread" + ], + "acegorty": [ + "category", + "greycoat" + ], + "enot": [ + "eton", + "note", + "tone" + ], + "eilv": [ + "evil", + "levi", + "live", + "veil", + "vile", + "vlei" + ], + "aeglr": [ + "argel", + "argle", + "ergal", + "garle", + "glare", + "lager", + "large", + "regal" + ], + "aegllry": [ + "allergy", + "gallery", + "largely", + "regally" + ], + "abelt": [ + "ablet", + "batel", + "belat", + "blate", + "bleat", + "tabel", + "table" + ], + "eehorvw": [ + "everwho", + "however", + "whoever" + ], + "aellry": [ + "rallye", + "really" + ], + "acinot": [ + "action", + "atonic", + "cation" + ], + "arstt": [ + "start", + "tarts" + ], + "eeirss": [ + "seiser", + "series", + "sirees" + ], + "air": [ + "air", + "ira", + "ria" + ], + "ahmnu": [ + "human", + "nahum" + ], + "esy": [ + "yes", + "sey", + "sye" + ], + "cdenos": [ + "codens", + "second" + ], + "hot": [ + "hot", + "tho" + ], + "cost": [ + "cost", + "cots", + "scot" + ], + "achmr": [ + "charm", + "march" + ], + "asy": [ + "ays", + "yas", + "say" + ], + "acdeilm": [ + "camelid", + "claimed", + "decimal", + "declaim", + "medical" + ], + "estt": [ + "sett", + "stet", + "test" + ], + "definr": [ + "finder", + "friend", + "redfin", + "refind" + ], + "eerrsv": [ + "revers", + "server", + "verser" + ], + "dstuy": [ + "dusty", + "study" + ], + "acrt": [ + "cart", + "trac" + ], + "aceilrst": [ + "altrices", + "articles", + "recitals", + "selictar", + "sterical" + ], + "ans": [ + "ans", + "san" + ], + "aagin": [ + "again", + "angia" + ], + "alpy": [ + "paly", + "pyal", + "pyla", + "play" + ], + "eisssu": [ + "issues", + "suisse" + ], + "ailpr": [ + "april", + "parli", + "pilar", + "ripal" + ], + "eenrv": [ + "nerve", + "never" + ], + "erssu": [ + "ruses", + "russe", + "suers", + "sures", + "users" + ], + "eerstt": [ + "retest", + "setter", + "street", + "tester" + ], + "ciopt": [ + "optic", + "picot", + "topic" + ], + "ghinst": [ + "nights", + "snight", + "things" + ], + "giknorw": [ + "kingrow", + "working" + ], + "aaginst": [ + "against", + "antisag" + ], + "atx": [ + "tax", + "xat" + ], + "enoprs": [ + "person", + "speron" + ], + "below": [ + "below", + "bowel", + "bowle", + "elbow" + ], + "beilmo": [ + "bemoil", + "emboil", + "emboli", + "mobile" + ], + "elss": [ + "less", + "sels" + ], + "got": [ + "got", + "tog" + ], + "aprty": [ + "party", + "trypa" + ], + "gilno": [ + "lingo", + "login" + ], + "densttu": [ + "student", + "stunted" + ], + "elt": [ + "elt", + "let", + "tel" + ], + "effors": [ + "offers", + "reffos" + ], + "aegll": [ + "egall", + "legal" + ], + "eorsst": [ + "retoss", + "rosets", + "sorest", + "sortes", + "stores", + "torses", + "tosser" + ], + "deis": [ + "deis", + "desi", + "dies", + "ides", + "ised", + "seid", + "side" + ], + "act": [ + "act", + "cat" + ], + "der": [ + "der", + "erd", + "red" + ], + "acilos": [ + "colias", + "scolia", + "social" + ], + "eoqtu": [ + "quote", + "toque" + ], + "aaegglnu": [ + "ganguela", + "langauge", + "language" + ], + "orsty": [ + "ryots", + "sorty", + "story", + "stroy", + "tyros", + "troys" + ], + "ells": [ + "ells", + "sell" + ], + "inoopst": [ + "options", + "potions" + ], + "aerst": [ + "arest", + "aster", + "astre", + "rates", + "reast", + "resat", + "serta", + "stare", + "strae", + "tares", + "tarse", + "tears", + "teras", + "treas" + ], + "aceert": [ + "cerate", + "cetera", + "create", + "ecarte" + ], + "eky": [ + "key", + "kye" + ], + "bdoy": [ + "body", + "boyd", + "doby" + ], + "defil": [ + "felid", + "fidel", + "field", + "filed", + "flied" + ], + "efw": [ + "few", + "wef" + ], + "aest": [ + "ates", + "east", + "eats", + "etas", + "sate", + "seat", + "seta", + "teas" + ], + "aeppr": [ + "paper", + "rappe" + ], + "egilns": [ + "glinse", + "ingles", + "lignes", + "seling", + "single", + "slinge" + ], + "aeg": [ + "age", + "gae" + ], + "aeelmpx": [ + "example", + "exempla" + ], + "aelstt": [ + "latest", + "sattle", + "taslet" + ], + "ador": [ + "ador", + "dora", + "orad", + "road" + ], + "ghint": [ + "night", + "thing" + ], + "aestx": [ + "taxes", + "texas" + ], + "cot": [ + "cot", + "cto", + "oct", + "otc" + ], + "apy": [ + "yap", + "pay", + "pya" + ], + "ekopr": [ + "poker", + "proke" + ], + "assttu": [ + "status", + "suttas" + ], + "beorsw": [ + "bowers", + "bowser", + "browse" + ], + "eissu": [ + "issue", + "susie" + ], + "aegnr": [ + "anger", + "areng", + "grane", + "range", + "regna", + "renga" + ], + "eellrs": [ + "resell", + "seller" + ], + "cortu": [ + "court", + "crout", + "turco" + ], + "elrstu": [ + "luster", + "lustre", + "result", + "rustle", + "sutler", + "ulster" + ], + "eirtw": [ + "twier", + "twire", + "write" + ], + "arw": [ + "raw", + "war" + ], + "nov": [ + "nov", + "von" + ], + "effor": [ + "offer", + "reffo" + ], + "belu": [ + "bleu", + "blue", + "lube" + ], + "aesy": [ + "ayes", + "easy", + "eyas", + "yeas" + ], + "efils": [ + "felis", + "files", + "flies" + ], + "eeqrstu": [ + "quester", + "request" + ], + "achin": [ + "chain", + "chian", + "china" + ], + "ceiprtu": [ + "cuprite", + "picture" + ], + "deens": [ + "denes", + "dense", + "needs" + ], + "ety": [ + "ety", + "yet", + "tye" + ], + "ajmor": [ + "jarmo", + "joram", + "major" + ], + "arst": [ + "arts", + "astr", + "rats", + "sart", + "star", + "stra", + "tars", + "tsar" + ], + "aaers": [ + "arase", + "areas" + ], + "aceps": [ + "capes", + "paces", + "scape", + "space" + ], + "adhn": [ + "dhan", + "hand" + ], + "nsu": [ + "nus", + "sun", + "uns" + ], + "aghinnostw": [ + "nowanights", + "washington" + ], + "eegimnt": [ + "meeting", + "teeming", + "tegmine" + ], + "eeinrstt": [ + "insetter", + "interest", + "interset", + "sternite", + "trientes" + ], + "eekp": [ + "keep", + "peek", + "peke" + ], + "eenrt": [ + "enter", + "entre", + "neter", + "renet", + "rente", + "terne", + "treen" + ], + "aehrs": [ + "asher", + "earsh", + "hares", + "hears", + "rheas", + "share", + "shear" + ], + "adegnr": [ + "danger", + "gander", + "garden", + "grande", + "ranged" + ], + "aceimnops": [ + "campesino", + "companies" + ], + "deilst": [ + "delist", + "desilt", + "idlest", + "listed", + "silted", + "tildes" + ], + "abby": [ + "abby", + "baby" + ], + "eegnry": [ + "energy", + "gyrene", + "greeny", + "ygerne" + ], + "nru": [ + "run", + "urn" + ], + "ent": [ + "net", + "ten" + ], + "eiorsst": [ + "isoster", + "rosiest", + "rossite", + "sorites", + "sorties", + "stories", + "trioses" + ], + "ptu": [ + "put", + "tup" + ], + "eoprrst": [ + "porters", + "pretors", + "reports", + "sporter", + "strepor" + ], + "rty": [ + "tyr", + "try" + ], + "aegims": [ + "ageism", + "images" + ], + "deeinprst": [ + "president", + "serpentid" + ], + "ceinot": [ + "conite", + "eciton", + "noetic", + "notice", + "octine" + ], + "adeh": [ + "hade", + "haed", + "head" + ], + "adior": [ + "aroid", + "doria", + "radio" + ], + "ilntu": [ + "unlit", + "until" + ], + "cloor": [ + "color", + "corol", + "crool" + ], + "efls": [ + "fels", + "self" + ], + "cdeilnsu": [ + "includes", + "nuclides", + "unsliced" + ], + "ceno": [ + "cone", + "econ", + "once" + ], + "ehorst": [ + "horste", + "hoster", + "others", + "reshot", + "throes", + "tosher" + ], + "aelst": [ + "astel", + "least", + "salet", + "setal", + "slate", + "stale", + "steal", + "stela", + "taels", + "tales", + "teals", + "tesla" + ], + "glo": [ + "gol", + "log" + ], + "definrs": [ + "finders", + "friends", + "redfins", + "refinds" + ], + "afq": [ + "faq", + "qaf" + ], + "adert": [ + "adret", + "dater", + "derat", + "detar", + "drate", + "rated", + "tarde", + "tared", + "trade", + "tread" + ], + "deiinot": [ + "edition", + "odinite", + "otidine", + "tineoid" + ], + "acrs": [ + "arcs", + "cars", + "scar", + "srac" + ], + "aeegmsss": [ + "megasses", + "messages" + ], + "abel": [ + "abel", + "able", + "albe", + "bael", + "bale", + "beal", + "bela", + "blae", + "blea" + ], + "deioprsv": [ + "disprove", + "provides" + ], + "aadelry": [ + "aleyard", + "already" + ], + "eegnr": [ + "genre", + "green", + "grene", + "neger", + "reneg" + ], + "deisstu": [ + "studies", + "tissued" + ], + "celos": [ + "cloes", + "close", + "coles", + "socle" + ], + "deirv": [ + "deriv", + "diver", + "drive", + "rived", + "verdi" + ], + "aeelrsv": [ + "laveers", + "leavers", + "reveals", + "several", + "vealers" + ], + "dglo": [ + "glod", + "gold" + ], + "eps": [ + "esp", + "pes", + "sep" + ], + "horst": [ + "horst", + "short" + ], + "lot": [ + "lot", + "tlo", + "tol" + ], + "aks": [ + "ask", + "kas", + "sak" + ], + "deiilmt": [ + "delimit", + "limited" + ], + "aemns": [ + "amens", + "manes", + "manse", + "means", + "mensa", + "names", + "nemas", + "samen", + "senam" + ], + "cdeiorrt": [ + "creditor", + "director" + ], + "adily": [ + "daily", + "lydia" + ], + "abceh": [ + "bache", + "beach" + ], + "apst": [ + "apts", + "past", + "pats", + "spat", + "stap", + "taps" + ], + "nopu": [ + "noup", + "puno", + "upon" + ], + "deiopr": [ + "dopier", + "period" + ], + "aeehrtw": [ + "weather", + "whereat", + "wreathe" + ], + "amr": [ + "arm", + "mar", + "ram" + ], + "deno": [ + "done", + "node" + ], + "accehilnt": [ + "catchline", + "technical" + ], + "opr": [ + "por", + "pro" + ], + "eginor": [ + "eringo", + "ignore", + "region" + ], + "cdeorr": [ + "corder", + "record" + ], + "cdeorrs": [ + "corders", + "records" + ], + "aacdelnr": [ + "calander", + "calandre", + "calendar", + "landrace" + ], + "cosst": [ + "costs", + "scots" + ], + "aeemnsttt": [ + "statement", + "testament" + ], + "aprst": [ + "parts", + "prats", + "spart", + "sprat", + "strap", + "tarps", + "traps" + ], + "agu": [ + "aug", + "gau" + ], + "eerv": [ + "ever", + "reve", + "veer" + ], + "addlnoosw": [ + "downloads", + "woodlands" + ], + "aelry": [ + "early", + "layer", + "leary", + "relay" + ], + "eilms": [ + "limes", + "melis", + "miles", + "slime", + "smile" + ], + "dnosu": [ + "nodus", + "ounds", + "sound" + ], + "ceeorrsu": [ + "recourse", + "resource" + ], + "eenprst": [ + "penster", + "present", + "repents", + "serpent", + "strepen" + ], + "ago": [ + "ago", + "goa" + ], + "dorw": [ + "drow", + "word" + ], + "apr": [ + "apr", + "par", + "rap" + ], + "einrttw": [ + "twinter", + "written" + ], + "ghinost": [ + "hosting", + "onsight" + ], + "elrsu": [ + "lures", + "luser", + "rules", + "sluer" + ], + "afiln": [ + "alfin", + "final", + "flain" + ], + "adltu": [ + "adult", + "dault", + "dulat" + ], + "ceikstt": [ + "sticket", + "tickets" + ], + "aiv": [ + "iva", + "vai", + "via" + ], + "acehp": [ + "chape", + "cheap", + "peach" + ], + "diks": [ + "disk", + "kids", + "skid" + ], + "eimnstu": [ + "minuets", + "minutes", + "mistune", + "mutines" + ], + "eels": [ + "eels", + "else", + "lees", + "lese", + "seel", + "sele", + "slee" + ], + "ckor": [ + "cork", + "rock" + ], + "adeginr": [ + "degrain", + "deraign", + "deringa", + "gradine", + "grained", + "reading" + ], + "ciopst": [ + "copist", + "coptis", + "optics", + "picots", + "postic", + "topics" + ], + "abd": [ + "abd", + "bad", + "dab" + ], + "ipst": [ + "pist", + "pits", + "spit", + "tips" + ], + "lpsu": [ + "plus", + "puls", + "slup" + ], + "ceorv": [ + "corve", + "cover" + ], + "deit": [ + "deti", + "diet", + "dite", + "edit", + "tide", + "tied" + ], + "ceenprt": [ + "percent", + "precent" + ], + "afst": [ + "fast", + "fats", + "saft" + ], + "ceht": [ + "chet", + "echt", + "etch", + "tche", + "tech" + ], + "eemt": [ + "meet", + "mete", + "teem" + ], + "afr": [ + "arf", + "far", + "fra" + ], + "aelpry": [ + "parley", + "pearly", + "player", + "rapely", + "replay" + ], + "aegmnry": [ + "germany", + "mangery" + ], + "amnotu": [ + "amount", + "moutan", + "outman" + ], + "eefl": [ + "feel", + "fele", + "flee", + "leef" + ], + "abkn": [ + "bank", + "knab", + "nabk" + ], + "ikrs": [ + "irks", + "kris", + "risk" + ], + "adels": [ + "dales", + "deals", + "lades", + "lased", + "leads", + "slade" + ], + "aiorsuv": [ + "saviour", + "various" + ], + "dorsw": [ + "sword", + "words" + ], + "notw": [ + "nowt", + "town", + "wont" + ], + "aehrt": [ + "earth", + "hater", + "heart", + "herat", + "rathe" + ], + "cdeeeirv": [ + "deceiver", + "received" + ], + "inopst": [ + "instop", + "pintos", + "piston", + "pitons", + "points", + "postin" + ], + "aacemr": [ + "acream", + "camera", + "mareca" + ], + "osty": [ + "toys", + "tosy" + ], + "deeegirrst": [ + "deregister", + "registered" + ], + "acelr": [ + "carle", + "ceral", + "clare", + "clear", + "lacer" + ], + "fglo": [ + "flog", + "golf" + ], + "adimno": [ + "amidon", + "daimon", + "domain", + "domina" + ], + "acehprt": [ + "chapter", + "patcher", + "repatch" + ], + "aekms": [ + "kames", + "makes", + "samek" + ], + "deiw": [ + "wide", + "wied" + ], + "aaegmnr": [ + "gearman", + "manager" + ], + "iinoopst": [ + "position", + "sopition" + ], + "orst": [ + "orts", + "rots", + "sort", + "stor", + "tors" + ], + "delmos": [ + "models", + "seldom", + "somdel" + ], + "acehilm": [ + "michael", + "micheal" + ], + "acess": [ + "cases", + "casse", + "scase" + ], + "epst": [ + "pest", + "pets", + "sept", + "spet", + "step" + ], + "eilmps": [ + "impels", + "mespil", + "simple" + ], + "enno": [ + "neon", + "none" + ], + "eeilrssw": [ + "weirless", + "wireless" + ], + "ceeilns": [ + "license", + "selenic", + "silence" + ], + "alpu": [ + "paul", + "upla" + ], + "aekl": [ + "kale", + "lake", + "leak" + ], + "ehlow": [ + "howel", + "whole" + ], + "aelrt": [ + "alert", + "alter", + "artel", + "later", + "ratel", + "retal", + "taler", + "telar" + ], + "abcis": [ + "bacis", + "basic" + ], + "hossw": [ + "shows", + "swosh" + ], + "dehmot": [ + "method", + "mothed" + ], + "cemorsstu": [ + "costumers", + "customers" + ], + "eenoprss": [ + "pessoner", + "response" + ], + "acceiprt": [ + "accipter", + "practice" + ], + "efir": [ + "fire", + "reif", + "rife" + ], + "adhiloy": [ + "hyaloid", + "hyoidal", + "holiday" + ], + "acht": [ + "cath", + "chat", + "tach" + ], + "aglno": [ + "along", + "anglo", + "gonal", + "lango", + "logan", + "longa", + "nogal" + ], + "agmno": [ + "among", + "mango", + "ngoma" + ], + "adeht": [ + "death", + "hated" + ], + "deeps": [ + "deeps", + "pedes", + "speed" + ], + "ceinorstu": [ + "countries", + "cretinous", + "neurotics" + ], + "loss": [ + "loss", + "sols" + ], + "acef": [ + "cafe", + "face" + ], + "cdinostu": [ + "conduits", + "discount", + "noctuids" + ], + "acdeert": [ + "catered", + "cedrate", + "cerated", + "created", + "reacted" + ], + "bit": [ + "bit", + "tib" + ], + "aceeinrs": [ + "cerasein", + "increase", + "resiance" + ], + "adeeirstv": [ + "advertise", + "derivates" + ], + "abes": [ + "base", + "besa", + "sabe", + "seba" + ], + "aenr": [ + "anre", + "aren", + "arne", + "earn", + "nare", + "near", + "rane" + ], + "ffstu": [ + "stuff", + "tuffs" + ], + "aegorst": [ + "garotes", + "orgeats", + "storage", + "tagsore" + ], + "dgino": [ + "dingo", + "doing", + "gondi", + "gonid" + ], + "alnos": [ + "loans", + "salon", + "sloan", + "solan" + ], + "ehoss": [ + "hoses", + "shoes" + ], + "aenrtu": [ + "aunter", + "auntre", + "nature" + ], + "deorrs": [ + "dorser", + "orders" + ], + "nrtu": [ + "runt", + "trun", + "turn" + ], + "enost": [ + "notes", + "onset", + "seton", + "steno", + "stone", + "tenso", + "tones" + ], + "gikn": [ + "gink", + "king" + ], + "admnoy": [ + "amydon", + "dynamo", + "monday" + ], + "cips": [ + "pics", + "spic" + ], + "aellorv": [ + "allover", + "overall" + ], + "aby": [ + "aby", + "bay" + ], + "eens": [ + "ense", + "esne", + "nese", + "seen", + "snee" + ], + "aelprsy": [ + "asperly", + "parleys", + "parsley", + "pyrales", + "players", + "replays", + "sparely", + "splayer" + ], + "eeginn": [ + "engine", + "ingene" + ], + "oprt": [ + "port", + "trop" + ], + "aegilnor": [ + "geraniol", + "regional" + ], + "aderstt": [ + "started", + "tetrads" + ], + "abr": [ + "abr", + "arb", + "bar", + "bra", + "rab" + ], + "eisvw": [ + "swive", + "views", + "wives" + ], + "bdelou": [ + "dobule", + "double" + ], + "dgo": [ + "dog", + "god" + ], + "ceenrs": [ + "censer", + "scerne", + "screen", + "secern" + ], + "noos": [ + "oons", + "soon" + ], + "eilns": [ + "elsin", + "lenis", + "liens", + "lines", + "niels", + "silen", + "sline" + ], + "ceinnotu": [ + "contineu", + "continue" + ], + "acorss": [ + "across", + "oscars" + ], + "einprrt": [ + "printer", + "reprint" + ], + "eru": [ + "rue", + "ure" + ], + "dimn": [ + "midn", + "mind" + ], + "ceeilnost": [ + "elections", + "selection" + ], + "acinos": [ + "casino", + "sonica" + ], + "lost": [ + "lost", + "lots", + "slot" + ], + "ortu": [ + "outr", + "rout", + "toru", + "tour" + ], + "emnu": [ + "menu", + "neum" + ], + "ehop": [ + "hope", + "peho" + ], + "eilrsv": [ + "ervils", + "livers", + "livres", + "silver", + "sliver" + ], + "deiins": [ + "deisin", + "indies", + "inside" + ], + "aemrtu": [ + "mature", + "tamure" + ], + "elor": [ + "lore", + "orle", + "role" + ], + "acem": [ + "acme", + "came", + "mace" + ], + "aceinrt": [ + "centiar", + "ceratin", + "certain", + "citrean", + "creatin", + "crinate", + "nacrite", + "nectria" + ], + "rsu": [ + "rus", + "sur", + "urs" + ], + "elorw": [ + "lower", + "owler", + "rowel" + ], + "mno": [ + "mon", + "nom" + ], + "cmo": [ + "com", + "moc" + ], + "efin": [ + "enif", + "fine", + "neif", + "nife" + ], + "ags": [ + "asg", + "gas", + "sag" + ], + "isx": [ + "six", + "xis" + ], + "bhsu": [ + "bush", + "hubs" + ], + "acdeiv": [ + "advice", + "vedaic" + ], + "aceerr": [ + "career", + "carree" + ], + "aiilmrty": [ + "limitary", + "military" + ], + "aelnrt": [ + "altern", + "antler", + "learnt", + "rental", + "ternal" + ], + "eenst": [ + "steen", + "teens", + "tense" + ], + "ast": [ + "ast", + "sat", + "sta", + "tas" + ], + "bdi": [ + "bid", + "dib" + ], + "dikn": [ + "dink", + "kind" + ], + "eellrss": [ + "resells", + "sellers" + ], + "abcel": [ + "cable", + "caleb" + ], + "aelsuv": [ + "alveus", + "avulse", + "values" + ], + "cgimno": [ + "coming", + "gnomic" + ], + "abeilns": [ + "albines", + "bensail", + "lesbian" + ], + "acehimn": [ + "chimane", + "machine" + ], + "gloo": [ + "golo", + "gool", + "logo" + ], + "cein": [ + "cine", + "nice" + ], + "ceors": [ + "ceros", + "cores", + "corse", + "crose", + "score" + ], + "ceilnt": [ + "client", + "lentic" + ], + "enrrstu": [ + "returns", + "turners" + ], + "aacilpt": [ + "capital", + "palatic" + ], + "aelmps": [ + "maples", + "sample" + ], + "enst": [ + "nest", + "nets", + "sent", + "sten", + "tens" + ], + "adel": [ + "dale", + "deal", + "lade", + "lead", + "leda" + ], + "ccehio": [ + "choice", + "echoic" + ], + "entw": [ + "newt", + "went" + ], + "ceorssu": [ + "courses", + "rescous", + "secours", + "sources", + "sucrose" + ], + "cemnorsu": [ + "consumer", + "cornmuse", + "mucrones" + ], + "aioprrt": [ + "airport", + "paritor" + ], + "airstt": [ + "artist", + "strait", + "strati", + "traist", + "traits" + ], + "deiostu": [ + "outside", + "tedious" + ], + "demo": [ + "demo", + "dome", + "mode", + "moed" + ], + "adeis": [ + "aides", + "aside", + "ideas", + "sadie" + ], + "emmrsu": [ + "rummes", + "summer" + ], + "dew": [ + "dew", + "wed" + ], + "eprsu": [ + "purse", + "resup", + "sprue", + "super" + ], + "aelm": [ + "alem", + "alme", + "amel", + "lame", + "leam", + "male", + "meal", + "mela" + ], + "aemrtt": [ + "matter", + "mettar" + ], + "cmostu": [ + "custom", + "muscot" + ], + "almost": [ + "almost", + "smalto", + "stomal" + ], + "koot": [ + "koto", + "toko", + "took" + ], + "aains": [ + "asian", + "naias", + "sanai" + ], + "deiort": [ + "dotier", + "editor", + "rioted", + "triode" + ], + "acesu": [ + "cause", + "sauce" + ], + "aeilnoptt": [ + "peltation", + "potential" + ], + "gnos": [ + "nogs", + "snog", + "song" + ], + "aelt": [ + "atle", + "laet", + "late", + "leat", + "tael", + "tale", + "teal", + "tela" + ], + "adei": [ + "aide", + "deia", + "eadi", + "idea" + ], + "moors": [ + "moors", + "rooms" + ], + "accenr": [ + "cancer", + "crance" + ], + "aenors": [ + "arseno", + "reason", + "senora" + ], + "loot": [ + "loot", + "loto", + "tool" + ], + "aenrsw": [ + "answer", + "resawn" + ], + "eopprsu": [ + "peropus", + "purpose" + ], + "bde": [ + "bde", + "bed", + "deb" + ], + "aeginoprt": [ + "operating", + "pignorate" + ], + "amps": [ + "amps", + "maps", + "pams", + "samp" + ], + "aginrst": [ + "gastrin", + "gratins", + "ratings", + "staring" + ], + "aglss": [ + "glass", + "slags" + ], + "etu": [ + "tue", + "ute" + ], + "himst": [ + "isthm", + "smith" + ], + "ahknt": [ + "hankt", + "thank" + ], + "eeehlnopt": [ + "phenetole", + "telephone" + ], + "oprst": [ + "ports", + "prost", + "sport", + "sprot", + "strop" + ], + "adery": [ + "deary", + "deray", + "yeard", + "rayed", + "ready" + ], + "aailmn": [ + "almain", + "animal", + "lamina", + "manila" + ], + "ceersu": [ + "cereus", + "ceruse", + "cesure", + "recuse", + "rescue", + "secure" + ], + "aeinooprst": [ + "anisotrope", + "operations" + ], + "ilmpsy": [ + "limpsy", + "simply" + ], + "achiinrst": [ + "christian", + "christina", + "trichinas" + ], + "addennrstu": [ + "understand", + "unstranded" + ], + "inoopt": [ + "option", + "potion" + ], + "aemrst": [ + "armets", + "martes", + "master", + "maters", + "matres", + "ramets", + "remast", + "stream", + "tamers" + ], + "aelnrst": [ + "antlers", + "rentals", + "saltern", + "slanter", + "starnel", + "sternal" + ], + "aes": [ + "aes", + "ase", + "sae", + "sea" + ], + "bdloo": [ + "blood", + "boldo" + ], + "behilprsu": [ + "publisher", + "republish" + ], + "aint": [ + "aint", + "anti", + "inta", + "tain", + "tina" + ], + "aenprst": [ + "arpents", + "enrapts", + "entraps", + "parents", + "pastern", + "trepans" + ], + "aikno": [ + "ikona", + "konia" + ], + "acimpt": [ + "campit", + "impact" + ], + "cehiknt": [ + "kitchen", + "thicken" + ], + "aacilnor": [ + "carolina", + "colnaria", + "conarial" + ], + "eeiopprrst": [ + "peripteros", + "properties" + ], + "hips": [ + "hips", + "phis", + "pish", + "ship" + ], + "enorsw": [ + "owners", + "resown", + "rowens", + "worsen" + ], + "adeeiss": [ + "disease", + "seaside" + ], + "ailty": [ + "ality", + "italy", + "laity", + "taily" + ], + "ceefprt": [ + "perfect", + "prefect" + ], + "ahir": [ + "ahir", + "hair" + ], + "abiss": [ + "absis", + "basis", + "bassi", + "isbas" + ], + "ceiist": [ + "cities", + "iciest" + ], + "acdeinst": [ + "disenact", + "distance" + ], + "eert": [ + "reet", + "rete", + "teer", + "tree" + ], + "eeprt": [ + "erept", + "peert", + "peter", + "petre" + ], + "eenrsu": [ + "ensuer", + "ensure", + "enures", + "unsere" + ], + "hstu": [ + "hust", + "huts", + "shut", + "thus", + "tush" + ], + "aertx": [ + "extra", + "retax", + "taxer" + ], + "degisu": [ + "guides", + "guised" + ], + "eiqtu": [ + "quiet", + "quite" + ], + "cdeeelst": [ + "deselect", + "selected" + ], + "boy": [ + "boy", + "yob" + ], + "ehors": [ + "heros", + "hoers", + "horse", + "shoer", + "shore" + ], + "eotv": [ + "veto", + "voet", + "vote" + ], + "adforrw": [ + "forward", + "froward" + ], + "eflorsw": [ + "flowers", + "fowlers", + "reflows", + "wolfers" + ], + "arsst": [ + "stars", + "trass", + "tsars" + ], + "ilsst": [ + "lists", + "silts", + "slits" + ], + "enorw": [ + "owner", + "reown", + "rewon", + "rowen" + ], + "aeilrt": [ + "aliter", + "lirate", + "retail", + "retial", + "tailer" + ], + "aailmns": [ + "animals", + "laminas", + "manilas" + ], + "cdeilrty": [ + "directly", + "tridecyl" + ], + "aswy": [ + "yaws", + "sway", + "ways" + ], + "nos": [ + "nos", + "ons", + "son" + ], + "elru": [ + "lure", + "rule" + ], + "acm": [ + "cam", + "mac" + ], + "ghinosu": [ + "housing", + "hugonis" + ], + "aekst": [ + "keats", + "skate", + "skeat", + "stake", + "steak", + "takes", + "teaks" + ], + "gmt": [ + "mgt", + "mtg" + ], + "ginrty": [ + "tyring", + "trigyn", + "trying" + ], + "ehmort": [ + "mother", + "thermo" + ], + "cddeeinors": [ + "considered", + "deconsider" + ], + "dlot": [ + "dolt", + "told" + ], + "inptu": [ + "input", + "punti" + ], + "eeft": [ + "feet", + "fete" + ], + "aegnt": [ + "agent", + "etang" + ], + "bin": [ + "bin", + "nib" + ], + "demnor": [ + "modern", + "morned", + "normed", + "rodmen" + ], + "einors": [ + "irones", + "noires", + "nosier", + "rosine", + "senior", + "soneri" + ], + "adeilnr": [ + "ireland", + "lindera" + ], + "aceghint": [ + "cheating", + "teaching" + ], + "door": [ + "door", + "odor", + "oord", + "ordo", + "rood" + ], + "adgnr": [ + "drang", + "grand" + ], + "eginstt": [ + "setting", + "testing" + ], + "ailrt": [ + "litra", + "trail", + "trial" + ], + "aceghr": [ + "charge", + "creagh" + ], + "instu": [ + "inust", + "sintu", + "suint", + "tunis", + "unist", + "units" + ], + "adeinst": [ + "destain", + "detains", + "instead", + "sainted", + "satined", + "stained" + ], + "cloo": [ + "cool", + "loco" + ], + "eortw": [ + "rowet", + "rowte", + "tower", + "wrote" + ], + "eeinrt": [ + "entier", + "entire", + "nerite", + "triene" + ], + "adegiln": [ + "adeling", + "aligned", + "dealing", + "leading" + ], + "aelmt": [ + "amlet", + "metal" + ], + "efinsst": [ + "fitness", + "infests" + ], + "aais": [ + "aias", + "asia" + ], + "essu": [ + "sues", + "uses" + ], + "opttuu": [ + "output", + "putout" + ], + "aeegrrt": [ + "greater", + "regrate", + "terrage" + ], + "airsstt": [ + "artists", + "straits", + "tsarist" + ], + "eilnoorstu": [ + "resolution", + "solutioner" + ], + "eemss": [ + "messe", + "seems", + "semes" + ], + "apss": [ + "asps", + "pass", + "saps", + "spas" + ], + "aeilnorst": [ + "lairstone", + "orientals", + "orleanist", + "relations", + "serotinal", + "tensorial" + ], + "rsttu": [ + "strut", + "sturt", + "trust" + ], + "anv": [ + "avn", + "nav", + "van" + ], + "acinnost": [ + "actinons", + "canonist", + "contains", + "sanction", + "santonic", + "sonantic" + ], + "einosss": [ + "essoins", + "osseins", + "session", + "sissone" + ], + "ilmtu": [ + "multi", + "tumli" + ], + "aacinotv": [ + "octavian", + "octavina", + "vacation" + ], + "ikns": [ + "inks", + "kins", + "sink", + "skin" + ], + "eprv": [ + "perv", + "prev" + ], + "ads": [ + "ads", + "das", + "sad" + ], + "amry": [ + "army", + "yarm", + "mary", + "myra" + ], + "cdeeeptx": [ + "excepted", + "expected" + ], + "ginr": [ + "girn", + "grin", + "ring" + ], + "adegr": [ + "edgar", + "gader", + "garde", + "grade", + "raged" + ], + "opp": [ + "opp", + "pop" + ], + "efilrt": [ + "fertil", + "filter", + "filtre", + "lifter", + "relift", + "trifle" + ], + "eglnor": [ + "longer", + "relong" + ], + "int": [ + "int", + "nit", + "tin" + ], + "aelnp": [ + "alpen", + "nepal", + "panel", + "penal", + "plane", + "plena" + ], + "aegmnr": [ + "engram", + "german", + "manger", + "ragmen" + ], + "adefltu": [ + "default", + "faulted" + ], + "eeiqrru": [ + "querier", + "require" + ], + "bosy": [ + "boys", + "yobs", + "sybo" + ], + "deep": [ + "deep", + "depe", + "peed" + ], + "allosw": [ + "allows", + "sallow", + "swallo" + ], + "erst": [ + "erst", + "rest", + "rets", + "sert", + "ster", + "stre", + "tres" + ], + "einoprt": [ + "pointer", + "protein", + "pterion", + "repoint", + "tropein", + "tropine" + ], + "deeoprrt": [ + "deporter", + "reported" + ], + "loop": [ + "loop", + "polo", + "pool" + ], + "ciilopst": [ + "colpitis", + "politics", + "psilotic" + ], + "abdors": [ + "adsorb", + "boards", + "broads", + "dobras" + ], + "aeiprst": [ + "parties", + "pastier", + "piaster", + "piastre", + "pirates", + "raspite", + "spirate", + "tapiser", + "traipse" + ], + "eey": [ + "eye", + "yee" + ], + "adeeelrs": [ + "released", + "resealed" + ], + "aegst": [ + "gates", + "geast", + "getas", + "stage" + ], + "aegrtt": [ + "gatter", + "target" + ], + "ceeptx": [ + "except", + "expect" + ], + "bsu": [ + "bus", + "sub" + ], + "abemy": [ + "beamy", + "embay", + "maybe" + ], + "acelps": [ + "places", + "scapel" + ], + "ainps": [ + "nipas", + "pains", + "pians", + "pinas", + "pisan", + "sapin", + "spain", + "spina" + ], + "cet": [ + "cte", + "etc", + "tec" + ], + "einrtw": [ + "twiner", + "winter" + ], + "eeprrssu": [ + "perusers", + "pressure" + ], + "ceor": [ + "cero", + "core" + ], + "abekr": [ + "baker", + "brake", + "break", + "kebar" + ], + "eopprssu": [ + "purposes", + "supposer" + ], + "ghhoorttuu": [ + "outthrough", + "throughout" + ], + "esst": [ + "sets", + "tess" + ], + "acden": [ + "acned", + "caned", + "dance", + "decan" + ], + "efilst": [ + "filets", + "fistle", + "fliest", + "flites", + "itself", + "stifle" + ], + "aepprs": [ + "papers", + "sapper" + ], + "agilnpy": [ + "playing", + "plygain" + ], + "adeerr": [ + "dearer", + "reader", + "reared", + "redare", + "redear", + "reread" + ], + "ailrtuv": [ + "virtual", + "vitular" + ], + "aenrssw": [ + "answers", + "rawness" + ], + "enrt": [ + "entr", + "rent", + "tern" + ], + "als": [ + "als", + "las", + "sal", + "sla" + ], + "eemort": [ + "emoter", + "meteor", + "remote" + ], + "aelpp": [ + "appel", + "apple", + "pepla" + ], + "adegginrr": [ + "regarding", + "regrading" + ], + "imn": [ + "min", + "nim" + ], + "eemorv": [ + "evermo", + "remove" + ], + "adi": [ + "aid", + "dia", + "ida" + ], + "host": [ + "host", + "hots", + "shot", + "soth", + "thos", + "tosh" + ], + "aceehrst": [ + "cheaters", + "hectares", + "recheats", + "teachers" + ], + "bins": [ + "bins", + "nibs", + "snib" + ], + "aalmnu": [ + "alumna", + "manual" + ], + "aegnst": [ + "agents", + "estang", + "stagne" + ], + "acdeeinrs": [ + "ardencies", + "ecardines", + "increased" + ], + "aeiprr": [ + "pairer", + "rapier", + "repair" + ], + "afir": [ + "fair", + "fiar", + "raif" + ], + "eelst": [ + "leets", + "leste", + "sleet", + "slete", + "steel", + "stele", + "teles" + ], + "defix": [ + "defix", + "fixed" + ], + "gnorw": [ + "grown", + "wrong" + ], + "aiprs": [ + "pairs", + "paris", + "parsi", + "sarip", + "spair", + "spira" + ], + "egst": [ + "gest", + "gets", + "steg", + "stge", + "tegs" + ], + "ceorst": [ + "corset", + "cortes", + "coster", + "croset", + "escort", + "recost", + "rectos", + "scoter", + "sector" + ], + "eeiqrrsu": [ + "queriers", + "requires" + ], + "aft": [ + "aft", + "fat" + ], + "aefhrt": [ + "father", + "freath", + "hafter", + "trefah" + ], + "cceeilrt": [ + "electric", + "lectrice" + ], + "eoqstu": [ + "quotes", + "toques" + ], + "adde": [ + "dade", + "dead", + "deda", + "edda" + ], + "ceeprst": [ + "recepts", + "respect", + "scepter", + "sceptre", + "specter", + "spectre" + ], + "eikm": [ + "miek", + "mike" + ], + "pst": [ + "pst", + "pts", + "spt", + "tps", + "tsp" + ], + "hortw": [ + "rowth", + "throw", + "whort", + "worth", + "wroth" + ], + "cdeeoprrsu": [ + "procedures", + "reproduces" + ], + "oopr": [ + "poor", + "proo", + "roop" + ], + "aceehrt": [ + "cheater", + "hectare", + "rechate", + "recheat", + "reteach", + "teacher" + ], + "eesy": [ + "eyes", + "yees", + "yese" + ], + "ekorrsw": [ + "reworks", + "workers" + ], + "afmr": [ + "farm", + "fram" + ], + "mot": [ + "mot", + "tom" + ], + "aceeirtv": [ + "creative", + "reactive" + ], + "acost": [ + "acost", + "actos", + "ascot", + "catso", + "coast", + "coats", + "costa", + "tacos", + "tacso", + "tasco", + "tosca" + ], + "aeegr": [ + "aeger", + "agree", + "eager", + "eagre", + "ragee" + ], + "aehr": [ + "hare", + "hear", + "hera", + "rhea" + ], + "aceerrs": [ + "careers", + "creaser", + "searcer" + ], + "egos": [ + "egos", + "goes", + "sego" + ], + "del": [ + "del", + "eld", + "led" + ], + "afn": [ + "fan", + "naf" + ], + "efmorr": [ + "former", + "reform" + ], + "aeegillrs": [ + "allergies", + "galleries" + ], + "dei": [ + "dei", + "die", + "ide" + ], + "deeenprst": [ + "presented", + "pretensed", + "repetends" + ], + "aflt": [ + "flat", + "laft" + ], + "flow": [ + "flow", + "fowl", + "wolf" + ], + "aceegins": [ + "agencies", + "agenesic", + "genesiac" + ], + "aenprt": [ + "arpent", + "enrapt", + "entrap", + "panter", + "parent", + "parten", + "pretan", + "pterna", + "trepan" + ], + "acghiimn": [ + "michigan", + "minhagic" + ], + "acels": [ + "alces", + "alecs", + "casel", + "claes", + "laces", + "scale" + ], + "adnst": [ + "dasnt", + "stand" + ], + "cemnooy": [ + "economy", + "monoecy" + ], + "eghhist": [ + "eighths", + "heights", + "highest" + ], + "aefmr": [ + "frame", + "fream" + ], + "aeeglns": [ + "angeles", + "senegal" + ], + "ahpt": [ + "path", + "phat" + ], + "aaailnrstu": [ + "australian", + "saturnalia" + ], + "cefhi": [ + "chief", + "fiche" + ], + "adeilt": [ + "detail", + "dietal", + "dilate", + "edital", + "tailed" + ], + "alsw": [ + "awls", + "laws", + "slaw" + ], + "acdeghn": [ + "changed", + "ganched" + ], + "ept": [ + "pet", + "pte" + ], + "adehr": [ + "derah", + "hared", + "heard", + "rheda" + ], + "aceln": [ + "ancle", + "canel", + "clean", + "lance", + "lenca" + ], + "aacfinr": [ + "african", + "francia" + ], + "guy": [ + "guy", + "yug" + ], + "eilstt": [ + "stilet", + "titles" + ], + "aeelnrtv": [ + "levanter", + "relevant", + "revelant" + ], + "ccennot": [ + "concent", + "connect" + ], + "bbeil": [ + "bible", + "blibe" + ], + "cpu": [ + "cpu", + "cup" + ], + "abekst": [ + "basket", + "betask" + ], + "addemn": [ + "damned", + "ddname", + "demand", + "madden" + ], + "eistu": [ + "etuis", + "suite" + ], + "aeinnottt": [ + "attention", + "tentation" + ], + "ikps": [ + "kips", + "pisk", + "skip", + "spik" + ], + "acinotu": [ + "auction", + "caution" + ], + "aegr": [ + "ager", + "agre", + "areg", + "gare", + "gear", + "rage" + ], + "eel": [ + "eel", + "lee" + ], + "acehlrs": [ + "charles", + "clasher", + "larches" + ], + "ainnot": [ + "anoint", + "nation" + ], + "fimr": [ + "firm", + "frim" + ], + "eensv": [ + "evens", + "neves", + "seven" + ], + "delor": [ + "lored", + "older" + ], + "iiillnos": [ + "illinois", + "illision" + ], + "eeelmnst": [ + "elements", + "steelmen" + ], + "eorrst": [ + "resort", + "retros", + "roster", + "sorter", + "storer" + ], + "admnor": [ + "random", + "rodman" + ], + "eiimnrst": [ + "interims", + "minister", + "misinter" + ], + "kloos": [ + "kolos", + "looks" + ], + "cdeiinorst": [ + "directions", + "discretion", + "soricident" + ], + "adginrt": [ + "darting", + "trading" + ], + "eforst": [ + "fetors", + "forest", + "forset", + "fortes", + "foster", + "fstore", + "softer" + ], + "aclls": [ + "calls", + "scall" + ], + "ehosw": [ + "howes", + "whose" + ], + "celopu": [ + "couple", + "culpeo" + ], + "deginn": [ + "ending", + "ginned" + ], + "ceilnst": [ + "clients", + "lentisc", + "scintle", + "stencil" + ], + "acinost": [ + "actions", + "atonics", + "cations" + ], + "eilnst": [ + "enlist", + "inlets", + "listen", + "silent", + "slinte", + "tinsel" + ], + "adekn": [ + "danke", + "kande", + "knead", + "naked" + ], + "aglo": [ + "gaol", + "goal", + "gola", + "olga" + ], + "dlos": [ + "dols", + "olds", + "slod", + "sold" + ], + "aekmrst": [ + "estmark", + "markets" + ], + "elostw": [ + "lowest", + "owlets", + "towels" + ], + "eilsv": [ + "elvis", + "evils", + "levis", + "lives", + "slive", + "veils" + ], + "aeehlrt": [ + "haltere", + "leather", + "tarheel" + ], + "deeeimnrt": [ + "determine", + "intermede" + ], + "almp": [ + "lamp", + "palm" + ], + "bbo": [ + "bob", + "obb" + ], + "aehpprs": [ + "perhaps", + "prehaps" + ], + "aeeillstt": [ + "satellite", + "telestial" + ], + "esstt": [ + "stets", + "tests" + ], + "emt": [ + "met", + "tem" + ], + "ainp": [ + "nipa", + "pain", + "pani", + "pian", + "pina" + ], + "eginsstt": [ + "settings", + "testings" + ], + "beruy": [ + "buyer", + "rebuy" + ], + "aeilsy": [ + "easily", + "elysia", + "sailye" + ], + "alor": [ + "lora", + "oral" + ], + "dfor": [ + "drof", + "ford" + ], + "eoprst": [ + "poster", + "presto", + "repost", + "respot", + "stoper", + "topers", + "tropes" + ], + "deeg": [ + "edge", + "geed" + ], + "oort": [ + "root", + "roto", + "toro" + ], + "adhilosy": [ + "hyaloids", + "holidays" + ], + "cei": [ + "cie", + "ice" + ], + "eeilprs": [ + "replies", + "spieler" + ], + "abell": [ + "bella", + "label" + ], + "ces": [ + "esc", + "sec" + ], + "cdeemmnor": [ + "commender", + "recommend" + ], + "acnno": [ + "ancon", + "canon" + ], + "aestw": [ + "awest", + "etwas", + "sweat", + "tawse", + "twaes", + "waste" + ], + "eimntu": [ + "minuet", + "minute", + "munite", + "mutine", + "untime" + ], + "deioprrv": [ + "overdrip", + "provider" + ], + "ailnoopt": [ + "antipolo", + "antipool", + "optional" + ], + "acdiinorty": [ + "dictionary", + "indicatory" + ], + "cdlo": [ + "clod", + "cold" + ], + "achir": [ + "chair", + "chria" + ], + "aehps": [ + "ephas", + "heaps", + "pesah", + "phase", + "shape" + ], + "defils": [ + "felids", + "fields" + ], + "abg": [ + "bag", + "gab" + ], + "eelrstt": [ + "letters", + "settler", + "sterlet", + "trestle" + ], + "hirst": [ + "hirst", + "shirt" + ], + "cdeinnotu": [ + "continued", + "unnoticed" + ], + "ceimr": [ + "crime", + "merci" + ], + "aberst": [ + "barest", + "baster", + "bestar", + "breast", + "restab", + "tabers", + "trabes" + ], + "bim": [ + "bim", + "ibm", + "mib" + ], + "egiilnor": [ + "ligroine", + "religion", + "reoiling" + ], + "acilm": [ + "claim", + "clima", + "malic" + ], + "eiimnoprss": [ + "impression", + "permission" + ], + "achpt": [ + "chapt", + "pacht", + "patch" + ], + "aeht": [ + "ahet", + "eath", + "haet", + "hate", + "heat", + "thae", + "thea" + ], + "aeemrssu": [ + "measures", + "reassume" + ], + "aeeginnort": [ + "generation", + "renegation" + ], + "imss": [ + "isms", + "miss", + "sims" + ], + "accehilm": [ + "alchemic", + "chemical" + ], + "akst": [ + "kats", + "skat", + "task" + ], + "efhilms": [ + "flemish", + "himself" + ], + "nor": [ + "nor", + "ron" + ], + "abeeln": [ + "baleen", + "enable" + ], + "aanst": [ + "antas", + "nasat", + "santa", + "satan" + ], + "dim": [ + "dim", + "mid" + ], + "adeelr": [ + "dealer", + "leader", + "redeal", + "relade", + "relead" + ], + "aeilrs": [ + "ariels", + "israel", + "laiser", + "relais", + "resail", + "sailer", + "serail", + "serial" + ], + "fost": [ + "soft", + "stof" + ], + "eerrssv": [ + "servers", + "versers" + ], + "aelno": [ + "alone", + "anole", + "olena" + ], + "eegimnst": [ + "gisement", + "meetings" + ], + "aainorz": [ + "arizona", + "azorian", + "zonaria" + ], + "eeinrsstt": [ + "insetters", + "interests", + "resistent", + "sternites", + "triteness" + ], + "eflu": [ + "flue", + "fuel" + ], + "aklw": [ + "lawk", + "walk" + ], + "aaiilnt": [ + "antilia", + "italian" + ], + "ainst": [ + "antis", + "saint", + "sanit", + "satin", + "stain", + "tains" + ], + "eors": [ + "eros", + "ores", + "roes", + "rose", + "seor", + "sero", + "sore" + ], + "aegmnrtu": [ + "argentum", + "argument" + ], + "aceginrt": [ + "antergic", + "argentic", + "catering", + "citrange", + "creating", + "reacting" + ], + "deioprrsv": [ + "disprover", + "providers" + ], + "adegpru": [ + "guepard", + "upgrade" + ], + "acfort": [ + "factor", + "forcat" + ], + "aeenrst": [ + "earnest", + "eastern", + "nearest" + ], + "acinostu": [ + "acontius", + "anticous", + "auctions", + "cautions" + ], + "eeinrst": [ + "entires", + "entries", + "trienes" + ], + "adest": [ + "dates", + "sated", + "sedat", + "stade", + "stead", + "tsade" + ], + "adeeegnrt": [ + "generated", + "greatened", + "renegated" + ], + "eimpr": [ + "imper", + "prime" + ], + "iilmt": [ + "limit", + "milit" + ], + "abegn": [ + "bagne", + "bange", + "began" + ], + "epsst": [ + "pests", + "septs", + "steps" + ], + "hopss": [ + "phoss", + "shops", + "sophs", + "sposh" + ], + "defimnor": [ + "foremind", + "informed" + ], + "abnru": [ + "buran", + "unbar", + "urban" + ], + "deorst": [ + "doters", + "sorted", + "stored", + "strode" + ], + "orstu": [ + "roust", + "routs", + "rusot", + "stour", + "sutor", + "torus", + "tours" + ], + "adlo": [ + "alod", + "dola", + "load", + "odal" + ], + "ablor": [ + "balor", + "bolar", + "boral", + "labor", + "lobar" + ], + "adimn": [ + "admin", + "dimna", + "mandi", + "manid" + ], + "agst": [ + "agst", + "gast", + "gats", + "stag", + "tags" + ], + "dopr": [ + "dorp", + "drop", + "prod" + ], + "dilos": [ + "dilos", + "diols", + "idols", + "lidos", + "sloid", + "soldi", + "solid" + ], + "aeeinnoprstt": [ + "penetrations", + "presentation" + ], + "aaegglnsu": [ + "languages", + "slanguage" + ], + "abceem": [ + "became", + "embace" + ], + "aegnor": [ + "onager", + "orange" + ], + "eehmt": [ + "meeth", + "theme" + ], + "aacgimnp": [ + "campaign", + "pangamic" + ], + "aeimnr": [ + "airmen", + "armine", + "ermani", + "marine", + "remain" + ], + "iiprst": [ + "pitris", + "spirit" + ], + "acilms": [ + "claims", + "miscal" + ], + "eems": [ + "emes", + "mese", + "seem", + "seme", + "smee" + ], + "aaffirs": [ + "affairs", + "raffias" + ], + "chotu": [ + "chout", + "couth", + "thuoc", + "touch" + ], + "ddeeinnt": [ + "indented", + "intended" + ], + "aglos": [ + "gaols", + "goals" + ], + "ehir": [ + "heir", + "hire" + ], + "ceeilnot": [ + "coteline", + "election" + ], + "eersv": [ + "serve", + "sever", + "veers", + "verse" + ], + "aenorss": [ + "reasons", + "senoras" + ], + "acgim": [ + "gamic", + "magic" + ], + "mnotu": [ + "montu", + "mount", + "notum" + ], + "amrst": [ + "marts", + "smart", + "stram", + "trams" + ], + "aegv": [ + "gave", + "vage", + "vega" + ], + "enos": [ + "enos", + "eons", + "noes", + "nose", + "ones", + "sone" + ], + "ailnt": [ + "altin", + "latin" + ], + "cdeefiirt": [ + "certified", + "rectified" + ], + "aaegmn": [ + "agname", + "manage" + ], + "aknr": [ + "karn", + "knar", + "kran", + "krna", + "nark", + "rank" + ], + "egnoor": [ + "oregon", + "orgone", + "orogen" + ], + "eeelmnt": [ + "element", + "leetmen", + "telemen" + ], + "bhirt": [ + "birth", + "brith" + ], + "abesu": [ + "abuse", + "beaus" + ], + "eeqrsstu": [ + "questers", + "requests" + ], + "aaeeprst": [ + "asperate", + "separate" + ], + "cdeeoprru": [ + "procedure", + "reproduce" + ], + "adeehilprs": [ + "dealership", + "leadership" + ], + "abelst": [ + "ablest", + "belast", + "bleats", + "stable", + "tables" + ], + "deefin": [ + "define", + "infeed" + ], + "acginr": [ + "arcing", + "caring", + "racing" + ], + "gkno": [ + "gonk", + "kong" + ], + "deeeloprv": [ + "developer", + "redevelop" + ], + "cdeimost": [ + "comedist", + "demotics", + "docetism", + "domestic" + ], + "aeimpst": [ + "impaste", + "pastime" + ], + "hnoostu": [ + "hontous", + "houston", + "nothous" + ], + "acehr": [ + "acher", + "arche", + "chare", + "chera", + "echar", + "rache", + "reach" + ], + "aelmnt": [ + "lament", + "manlet", + "mantel", + "mantle", + "mental" + ], + "emmnot": [ + "moment", + "montem" + ], + "chin": [ + "chin", + "inch" + ], + "ceenrst": [ + "centers", + "centres", + "scenter", + "tenrecs" + ], + "aadegm": [ + "dagame", + "damage" + ], + "abl": [ + "abl", + "alb", + "bal", + "lab" + ], + "eeerrsv": [ + "reserve", + "resever", + "reveres", + "reverse", + "severer" + ], + "ceeiprs": [ + "piecers", + "pierces", + "precise", + "recipes", + "respice", + "scripee" + ], + "aagmm": [ + "gamma", + "magma" + ], + "nosw": [ + "nows", + "owns", + "snow", + "sown", + "wons" + ], + "hrttu": [ + "thurt", + "truth" + ], + "cenortu": [ + "conteur", + "cornute", + "counter", + "recount", + "trounce" + ], + "ainoort": [ + "ontario", + "oration" + ], + "des": [ + "des", + "eds", + "esd", + "sed" + ], + "aeimnnost": [ + "antimeson", + "mannitose", + "minnesota", + "nominates" + ], + "bdegir": [ + "begird", + "bridge" + ], + "aeintv": [ + "native", + "navite" + ], + "denow": [ + "endow", + "nowed", + "owned", + "woden" + ], + "achrt": [ + "archt", + "chart", + "ratch" + ], + "adeerrs": [ + "readers", + "redears", + "redsear", + "rereads" + ], + "aelqu": [ + "equal", + "quale", + "queal" + ], + "adeenrtuv": [ + "adventure", + "unaverted" + ], + "fioprt": [ + "forpit", + "profit" + ], + "adeelrs": [ + "dealers", + "leaders" + ], + "eoprsst": [ + "posters", + "prestos", + "stopers" + ], + "aainssstt": [ + "assistant", + "satanists" + ], + "aev": [ + "ave", + "eva" + ], + "acdemopr": [ + "compadre", + "compared" + ], + "hkooprsw": [ + "shopwork", + "workshop" + ], + "egno": [ + "geon", + "goen", + "gone" + ], + "cdeos": [ + "codes", + "coeds", + "cosed" + ], + "dikns": [ + "dinks", + "kinds" + ], + "aeelstt": [ + "atelets", + "seattle", + "tsatlee" + ], + "aeemnssttt": [ + "statements", + "testaments" + ], + "deglno": [ + "engold", + "golden", + "longed" + ], + "aemst": [ + "mates", + "meats", + "metas", + "satem", + "steam", + "stema", + "tames", + "teams" + ], + "fort": [ + "fort", + "frot" + ], + "aeenst": [ + "enates", + "ensate", + "enseat", + "santee", + "sateen", + "senate" + ], + "cefors": [ + "forces", + "fresco" + ], + "denrtu": [ + "deturn", + "dunter", + "retund", + "runted", + "tunder", + "turned" + ], + "deirt": [ + "diter", + "tired", + "tried" + ], + "deenrrtu": [ + "returned", + "unterred" + ], + "aenprtt": [ + "pattern", + "reptant" + ], + "abot": [ + "boat", + "bota", + "toba" + ], + "ademn": [ + "admen", + "amend", + "mande", + "maned", + "menad", + "named" + ], + "aeehrtt": [ + "teather", + "theater", + "theatre", + "thereat" + ], + "aelrs": [ + "arles", + "arsle", + "earls", + "lares", + "laser", + "lears", + "rales", + "reals", + "seral", + "slare" + ], + "aeeilrr": [ + "earlier", + "learier" + ], + "cino": [ + "cion", + "coin", + "coni", + "icon" + ], + "aadiinn": [ + "anidian", + "indiana" + ], + "cdeiinort": [ + "cretinoid", + "direction" + ], + "deeelt": [ + "delete", + "teedle" + ], + "acelnru": [ + "crenula", + "lucarne", + "nuclear", + "unclear" + ], + "emosu": [ + "moues", + "mouse" + ], + "agilns": [ + "algins", + "aligns", + "glanis", + "lasing", + "liangs", + "ligans", + "lingas", + "sangil", + "signal" + ], + "deissu": [ + "dissue", + "disuse", + "issued" + ], + "abinr": [ + "abrin", + "bairn", + "brain", + "brian", + "rabin" + ], + "eflopruw": [ + "powerful", + "upflower" + ], + "ademr": [ + "armed", + "derma", + "drame", + "dream", + "madre", + "ramed" + ], + "aefls": [ + "alefs", + "false", + "fleas", + "leafs" + ], + "acst": [ + "acts", + "cast", + "cats", + "scat" + ], + "eflorw": [ + "flower", + "fowler", + "reflow", + "wolfer" + ], + "adepss": [ + "depass", + "passed", + "spades" + ], + "cip": [ + "cpi", + "pci", + "pic" + ], + "emooprt": [ + "promote", + "protome" + ], + "adestt": [ + "stated", + "tasted" + ], + "aaepprs": [ + "appears", + "sappare" + ], + "ceorsv": [ + "corves", + "covers" + ], + "aaiimnnt": [ + "amanitin", + "maintain" + ], + "imorstu": [ + "sumitro", + "tourism" + ], + "amot": [ + "atmo", + "atom", + "moat", + "mota", + "toma" + ], + "adeeimstt": [ + "estimated", + "meditates" + ], + "befir": [ + "bifer", + "brief", + "fiber", + "fibre" + ], + "inor": [ + "inro", + "iron", + "noir", + "nori", + "roin" + ], + "deersv": [ + "served", + "versed" + ], + "anstw": [ + "stawn", + "wants", + "wasnt" + ], + "adeepprr": [ + "dapperer", + "prepared" + ], + "diov": [ + "ovid", + "void" + ], + "dgiinn": [ + "dining", + "indign", + "niding" + ], + "aegiinnortt": [ + "integration", + "orientating" + ], + "agt": [ + "agt", + "gat", + "tag" + ], + "adeillnst": [ + "installed", + "landsleit" + ], + "cdeirst": [ + "credits", + "directs" + ], + "adehln": [ + "handel", + "handle" + ], + "eestw": [ + "ewest", + "sweet", + "weest", + "weets", + "weste" + ], + "deks": [ + "desk", + "sked" + ], + "aceiirrt": [ + "criteria", + "triceria" + ], + "adev": [ + "dave", + "deva", + "vade", + "veda" + ], + "degio": [ + "diego", + "dogie", + "geoid" + ], + "ceiv": [ + "cive", + "vice" + ], + "ary": [ + "ary", + "yar", + "ray", + "rya" + ], + "eeenruv": [ + "revenue", + "unreeve" + ], + "aeemrsu": [ + "measure", + "reamuse" + ], + "acgghinn": [ + "changing", + "ganching" + ], + "eostv": [ + "ovest", + "stove", + "votes" + ], + "aber": [ + "bare", + "bear", + "brae" + ], + "agin": [ + "agin", + "gain", + "inga", + "naig", + "ngai" + ], + "aceno": [ + "acone", + "canoe", + "ocean" + ], + "ginss": [ + "signs", + "sings", + "snigs", + "ssing" + ], + "ackl": [ + "calk", + "kcal", + "lack" + ], + "degglo": [ + "doggle", + "dogleg", + "logged" + ], + "aloppt": [ + "applot", + "laptop" + ], + "aegintv": [ + "vagient", + "vintage" + ], + "ainrt": [ + "intra", + "riant", + "tairn", + "tarin", + "train", + "trina" + ], + "aps": [ + "asp", + "pas", + "sap", + "spa" + ], + "aelnry": [ + "anerly", + "layner", + "nearly" + ], + "aeilrty": [ + "irately", + "reality", + "tearily" + ], + "giinor": [ + "nigori", + "origin" + ], + "aggimn": [ + "gaming", + "gigman" + ], + "aefrst": [ + "afters", + "farset", + "faster", + "strafe" + ], + "cno": [ + "con", + "nco" + ], + "psu": [ + "pus", + "sup", + "ups" + ], + "ainnost": [ + "anoints", + "nations", + "onanist" + ], + "eortu": [ + "outer", + "outre", + "route", + "troue", + "utero" + ], + "mooz": [ + "mozo", + "zoom" + ], + "blow": [ + "blow", + "bowl" + ], + "abeltt": [ + "batlet", + "battel", + "battle", + "tablet" + ], + "aeimn": [ + "amine", + "anime", + "enami", + "maine", + "manei", + "manie", + "minae" + ], + "aekps": [ + "peaks", + "sapek", + "spake", + "speak" + ], + "deiinrsstu": [ + "disuniters", + "industries" + ], + "adeiilort": [ + "editorial", + "radiolite" + ], + "ceehps": [ + "cheeps", + "speech" + ], + "eirw": [ + "weir", + "weri", + "wire" + ], + "alrru": [ + "rural", + "urlar" + ], + "adehrs": [ + "dasher", + "rhedas", + "shader", + "shared", + "sheard" + ], + "aept": [ + "pate", + "peat", + "tape", + "teap", + "tepa" + ], + "cceimnoos": [ + "economics", + "neocosmic" + ], + "acdi": [ + "acid", + "cadi", + "caid" + ], + "besty": [ + "betsy", + "bytes" + ], + "eghhit": [ + "eighth", + "height" + ], + "aeekprs": [ + "respeak", + "speaker" + ], + "abinot": [ + "batino", + "bonita", + "oatbin", + "obtain" + ], + "ceffios": [ + "coiffes", + "offices" + ], + "deeginrs": [ + "designer", + "energids", + "redesign", + "reedings", + "resigned" + ], + "aadegmn": [ + "agnamed", + "managed" + ], + "adefil": [ + "afield", + "defail", + "defial", + "failed" + ], + "ceerst": [ + "certes", + "erects", + "resect", + "screet", + "secret", + "terces" + ], + "abht": [ + "baht", + "bath", + "bhat" + ], + "aeegintv": [ + "agentive", + "negative" + ], + "adenrw": [ + "andrew", + "redawn", + "wander", + "warden", + "warned" + ], + "efmoprr": [ + "perform", + "preform" + ], + "aeeimsstt": [ + "estimates", + "semistate", + "steamiest" + ], + "aessst": [ + "assets", + "stases", + "tasses" + ], + "iimnrsty": [ + "ministry", + "myristin" + ], + "aelrwy": [ + "yawler", + "lawyer", + "warely" + ], + "adeimrr": [ + "admirer", + "madrier", + "married" + ], + "aghinrs": [ + "garnish", + "rashing", + "sharing" + ], + "aloprt": [ + "patrol", + "portal", + "tropal" + ], + "abet": [ + "abet", + "bate", + "beat", + "beta" + ], + "afil": [ + "alif", + "fail", + "fila" + ], + "agirst": [ + "gratis", + "striga" + ], + "aissst": [ + "assist", + "stasis" + ], + "eginrsv": [ + "serving", + "versing" + ], + "abgs": [ + "bags", + "gabs" + ], + "ccimos": [ + "comics", + "cosmic" + ], + "aemrstt": [ + "matster", + "matters", + "smatter" + ], + "ehossu": [ + "houses", + "shouse" + ], + "cdo": [ + "cod", + "doc" + ], + "aerw": [ + "arew", + "waer", + "ware", + "wear" + ], + "abegiknr": [ + "beraking", + "breaking", + "rebaking" + ], + "aeilmttu": [ + "mutilate", + "ultimate" + ], + "aelsw": [ + "swale", + "sweal", + "wales", + "wasel", + "weals" + ], + "imnor": [ + "minor", + "morin" + ], + "deeinrsst": [ + "dissenter", + "residents", + "tiredness", + "triedness" + ], + "denot": [ + "donet", + "noted", + "tendo", + "toned" + ], + "cddeeru": [ + "deducer", + "reduced" + ], + "aerr": [ + "rare", + "rear" + ], + "deefmoprr": [ + "performed", + "preformed" + ], + "adisv": [ + "davis", + "divas", + "vadis" + ], + "adeiln": [ + "aldine", + "alined", + "daniel", + "delian", + "denial", + "enalid", + "leadin", + "nailed" + ], + "abrs": [ + "arbs", + "bars", + "bras" + ], + "orw": [ + "row", + "wro" + ], + "aceforst": [ + "cofaster", + "forecast" + ], + "ehlps": [ + "helps", + "shlep" + ], + "egilnss": [ + "essling", + "singles" + ], + "amnostu": [ + "amounts", + "outmans", + "saumont" + ], + "acinnot": [ + "actinon", + "cantino", + "cantion", + "contain" + ], + "adlu": [ + "auld", + "dual", + "laud", + "udal" + ], + "eirs": [ + "eris", + "ires", + "reis", + "ries", + "rise", + "seri", + "sier", + "sire" + ], + "dsu": [ + "sud", + "uds" + ], + "eelps": [ + "peels", + "peles", + "sleep", + "speel" + ], + "bdir": [ + "bird", + "brid", + "drib" + ], + "aceinort": [ + "actioner", + "anerotic", + "anoretic", + "canotier", + "ceration", + "cortinae", + "creation", + "reaction" + ], + "acistt": [ + "attics", + "static", + "sticta" + ], + "ceens": [ + "cense", + "scene", + "sence" + ], + "adly": [ + "yald", + "lady" + ], + "aet": [ + "aet", + "ate", + "eat", + "eta", + "tae", + "tea" + ], + "acegilnn": [ + "cleaning", + "enlacing" + ], + "eilnt": [ + "inlet", + "intel", + "linet" + ], + "aegilnortu": [ + "regulation", + "urogenital" + ], + "cdeinortu": [ + "introduce", + "reduction" + ], + "aim": [ + "aim", + "ami", + "ima", + "mia" + ], + "bdis": [ + "bids", + "dibs" + ], + "deeefrrr": [ + "deferrer", + "referred" + ], + "eginors": [ + "eringos", + "ignores", + "regions", + "signore" + ], + "els": [ + "els", + "les", + "sel" + ], + "acep": [ + "cape", + "cepa", + "pace" + ], + "ann": [ + "ann", + "nan" + ], + "ginrs": [ + "girns", + "grins", + "rings" + ], + "ipt": [ + "pit", + "tip", + "tpi" + ], + "deflnoruw": [ + "underflow", + "wonderful" + ], + "eimn": [ + "mein", + "mien", + "mine" + ], + "adeils": [ + "aisled", + "deasil", + "ideals", + "ladies", + "sailed" + ], + "adeegr": [ + "agreed", + "dragee", + "geared" + ], + "eeinnoprtv": [ + "prevention", + "provenient" + ], + "iks": [ + "ksi", + "ski" + ], + "cceors": [ + "scorce", + "soccer" + ], + "imoprt": [ + "import", + "promit" + ], + "ginopst": [ + "posting", + "stoping" + ], + "aadeiimnnt": [ + "diamantine", + "inanimated", + "maintained" + ], + "ccdeennot": [ + "condecent", + "connected" + ], + "chirst": [ + "christ", + "strich" + ], + "dgos": [ + "dogs", + "gods" + ], + "cdeiorrst": [ + "creditors", + "directors", + "recordist" + ], + "aadeh": [ + "aahed", + "ahead" + ], + "mnoo": [ + "mono", + "moon" + ], + "ceehms": [ + "scheme", + "smeech" + ], + "deelv": [ + "delve", + "devel" + ], + "eeeinprrsst": [ + "enterprises", + "intersperse" + ], + "adelt": [ + "adlet", + "dealt", + "delta", + "lated", + "taled" + ], + "aefr": [ + "afer", + "fare", + "fear", + "frae", + "rafe" + ], + "eegikns": [ + "seeking", + "skeeing" + ], + "cehins": [ + "chines", + "chinse", + "inches", + "niches" + ], + "aehrss": [ + "rashes", + "shares", + "shears" + ], + "arsw": [ + "raws", + "wars" + ], + "ekpt": [ + "kept", + "tpke" + ], + "aaelpp": [ + "appale", + "appeal" + ], + "ceirsu": [ + "cruise", + "crusie", + "curies" + ], + "bnosu": [ + "bonus", + "bosun" + ], + "accefiiinortt": [ + "certification", + "cretification", + "rectification" + ], + "eiloprsuvy": [ + "perviously", + "previously", + "viperously" + ], + "ehy": [ + "hey", + "hye", + "yeh" + ], + "aceilpss": [ + "slipcase", + "specials" + ], + "deinsy": [ + "disney", + "sidney" + ], + "abdeo": [ + "abode", + "adobe" + ], + "deirsv": [ + "divers", + "drives" + ], + "amrs": [ + "arms", + "mars", + "rams" + ], + "eerst": [ + "ester", + "estre", + "reest", + "reset", + "steer", + "stere", + "stree", + "teres", + "terse", + "trees", + "tsere" + ], + "agv": [ + "avg", + "vag" + ], + "ahtu": [ + "auth", + "haut", + "utah" + ], + "abenry": [ + "barney", + "nearby" + ], + "mor": [ + "mor", + "rom" + ], + "acdeirr": [ + "acrider", + "carried" + ], + "dehi": [ + "hide", + "hied" + ], + "aeginrstu": [ + "gauntries", + "signature" + ], + "eefrr": [ + "freer", + "frere", + "refer", + "rfree" + ], + "eillmr": [ + "miller", + "remill" + ], + "acdesu": [ + "caused", + "sauced" + ], + "abbes": [ + "abbes", + "babes" + ], + "ddeein": [ + "denied", + "indeed" + ], + "oty": [ + "yot", + "toy" + ], + "deinprt": [ + "deprint", + "printed" + ], + "losw": [ + "lows", + "owls", + "slow", + "sowl" + ], + "aelmorv": [ + "removal", + "valorem" + ], + "aeeirs": [ + "aeries", + "easier" + ], + "crs": [ + "crs", + "scr" + ], + "abiiillty": [ + "alibility", + "liability" + ], + "hip": [ + "hip", + "ihp", + "iph", + "phi" + ], + "einprrst": [ + "printers", + "reprints", + "sprinter" + ], + "einn": [ + "inne", + "nein", + "nine" + ], + "addgin": [ + "adding", + "dading" + ], + "ceir": [ + "cire", + "eric", + "rice" + ], + "inprst": [ + "prints", + "sprint" + ], + "denps": [ + "pends", + "psend", + "spend" + ], + "deeirsv": [ + "derives", + "deviser", + "diverse", + "revised" + ], + "acilopt": [ + "capitol", + "coalpit", + "optical", + "topical" + ], + "aeeilrtv": [ + "levirate", + "relative" + ], + "dot": [ + "dot", + "tod" + ], + "eisstu": [ + "suites", + "tissue" + ], + "eefgiln": [ + "feeling", + "fleeing" + ], + "eefilr": [ + "ferlie", + "liefer", + "refile", + "relief" + ], + "eiinorsv": [ + "revision", + "visioner" + ], + "aiort": [ + "ariot", + "ratio" + ], + "adp": [ + "adp", + "dap", + "pad" + ], + "ainr": [ + "airn", + "arni", + "iran", + "nair", + "rain", + "rani" + ], + "noot": [ + "onto", + "oont", + "toon" + ], + "aelnpt": [ + "pantle", + "planet", + "platen" + ], + "ceeipr": [ + "piecer", + "pierce", + "recipe" + ], + "eimprt": [ + "permit", + "premit" + ], + "eegins": [ + "genies", + "seeing", + "signee" + ], + "einnst": [ + "innest", + "intens", + "sennit", + "sinnet", + "tennis" + ], + "abss": [ + "bass", + "sabs" + ], + "bdemoor": [ + "bedroom", + "boerdom", + "boredom", + "broomed" + ], + "aceinnst": [ + "ancients", + "canniest", + "insectan", + "instance" + ], + "deir": [ + "dier", + "dire", + "drie", + "ired", + "reid", + "ride" + ], + "cdeeilns": [ + "declines", + "licensed", + "silenced" + ], + "adlnoor": [ + "lardoon", + "orlando", + "rolando" + ], + "imt": [ + "mit", + "tim" + ], + "eeenprrst": [ + "presenter", + "repenters", + "represent" + ], + "aceinnoorstv": [ + "conservation", + "conversation" + ], + "aipr": [ + "pair", + "pari", + "pria", + "ripa" + ], + "adeil": [ + "adiel", + "ailed", + "delia", + "ideal" + ], + "dno": [ + "don", + "nod" + ], + "ceeips": [ + "pieces", + "specie" + ], + "defhiins": [ + "definish", + "fiendish", + "finished" + ], + "akprs": [ + "parks", + "spark" + ], + "deinnr": [ + "dinner", + "endrin" + ], + "acemr": [ + "cream", + "macer" + ], + "nrsu": [ + "runs", + "snur", + "urns" + ], + "aehy": [ + "ahey", + "eyah", + "haye", + "yeah" + ], + "cdeiorsv": [ + "discover", + "divorces" + ], + "aenprstt": [ + "patterns", + "prestant", + "transept", + "trapnest" + ], + "hills": [ + "hills", + "shill" + ], + "ilnosw": [ + "lowsin", + "wilson" + ], + "hiirs": [ + "irish", + "rishi", + "sirih" + ], + "aeimnrs": [ + "marines", + "remains", + "seminar" + ], + "aeegrstt": [ + "greatest", + "stratege" + ], + "eoru": [ + "euro", + "roue" + ], + "ceeginr": [ + "energic", + "generic" + ], + "aegsu": [ + "agues", + "usage" + ], + "acp": [ + "cap", + "pac" + ], + "ikn": [ + "ink", + "kin" + ], + "achrst": [ + "charts", + "scarth", + "scrath", + "starch" + ], + "cgiinnnotu": [ + "continuing", + "unnoticing" + ], + "aekp": [ + "keap", + "peak" + ], + "eistx": [ + "exist", + "exits", + "sixte" + ], + "eehlw": [ + "hewel", + "wheel" + ], + "ainrstt": [ + "straint", + "transit", + "tristan" + ], + "accmopt": [ + "accompt", + "compact" + ], + "ghilst": [ + "lights", + "slight" + ], + "aegln": [ + "agnel", + "angel", + "angle", + "galen", + "genal", + "glean", + "lagen", + "nagel" + ], + "eegiknp": [ + "keeping", + "peeking" + ], + "aaeinopprrt": [ + "paraprotein", + "preparation" + ], + "einos": [ + "eosin", + "noise" + ], + "eeginns": [ + "engines", + "senegin" + ], + "aaccertu": [ + "accurate", + "carucate" + ], + "inp": [ + "nip", + "pin" + ], + "eirsst": [ + "resist", + "restis", + "sister", + "tresis" + ], + "gnu": [ + "gnu", + "gun", + "ung" + ], + "ahprs": [ + "harps", + "sharp", + "shrap" + ], + "abeilnss": [ + "albiness", + "lesbians" + ], + "ben": [ + "ben", + "neb" + ], + "aeln": [ + "alen", + "elan", + "laen", + "lane", + "lean", + "lena", + "nael", + "nale", + "neal" + ], + "alo": [ + "alo", + "lao", + "loa", + "ola" + ], + "eoprtx": [ + "export", + "torpex" + ], + "eelmopry": [ + "employer", + "polymere", + "reemploy" + ], + "knosw": [ + "knows", + "snowk", + "swonk" + ], + "bcdeeirs": [ + "describe", + "escribed" + ], + "ceiinstz": [ + "citizens", + "zincites" + ], + "aelnoprss": [ + "apronless", + "personals", + "responsal" + ], + "belortu": [ + "boulter", + "trouble" + ], + "adeprs": [ + "drapes", + "padres", + "parsed", + "rasped", + "spader", + "spared", + "spread" + ], + "accho": [ + "chaco", + "choca", + "coach" + ], + "eiknv": [ + "kevin", + "knive" + ], + "adjnor": [ + "jardon", + "jordan" + ], + "aegs": [ + "ages", + "gaes", + "sage" + ], + "glpu": [ + "gulp", + "plug" + ], + "aceiilpsst": [ + "plasticise", + "specialist" + ], + "giinrv": [ + "irving", + "riving", + "virgin" + ], + "aegiiinnosttv": [ + "investigation", + "tenovaginitis" + ], + "adeirs": [ + "aiders", + "arised", + "deairs", + "irades", + "raised", + "redias", + "resaid" + ], + "aht": [ + "aht", + "hat", + "tha" + ], + "cddeeirt": [ + "credited", + "directed" + ], + "elpr": [ + "lerp", + "repl" + ], + "beik": [ + "bike", + "kibe" + ], + "aelpt": [ + "leapt", + "lepta", + "palet", + "patel", + "pelta", + "petal", + "plate", + "pleat", + "tepal" + ], + "acdeiint": [ + "actinide", + "ctenidia", + "diacetin", + "diactine", + "indicate" + ], + "bdelno": [ + "blonde", + "bolden", + "nobled" + ], + "elos": [ + "leos", + "lose", + "oles", + "sloe", + "sole" + ], + "eeks": [ + "ekes", + "kees", + "seek", + "skee" + ], + "ablmsu": [ + "albums", + "sambul", + "sumbal" + ], + "acehst": [ + "chaste", + "cheats", + "sachet", + "scathe", + "scheat", + "taches" + ], + "egsstu": [ + "guests", + "gusset" + ], + "adeeisss": [ + "diseases", + "seasides" + ], + "deeeloprsv": [ + "developers", + "redevelops" + ], + "noty": [ + "yont", + "tony" + ], + "aadenv": [ + "advena", + "nevada", + "vedana", + "venada" + ], + "ikst": [ + "kist", + "kits", + "skit" + ], + "aadegn": [ + "agenda", + "gadean" + ], + "ceinnostu": [ + "continues", + "neustonic" + ], + "ackrst": [ + "strack", + "tracks" + ], + "aeelmptt": [ + "palmette", + "template" + ], + "ceinpr": [ + "pincer", + "prince" + ], + "cceilr": [ + "circle", + "cleric" + ], + "ilos": [ + "lois", + "oils", + "silo", + "siol", + "soil", + "soli" + ], + "agnrst": [ + "grants", + "strang" + ], + "aacilntt": [ + "atlantic", + "tantalic" + ], + "etw": [ + "tew", + "wet" + ], + "adderw": [ + "edward", + "wadder", + "warded" + ], + "aegilnv": [ + "leaving", + "vangeli", + "vealing" + ], + "denoprs": [ + "ponders", + "respond" + ], + "eissz": [ + "sizes", + "zeiss" + ], + "ailnp": [ + "lapin", + "lipan", + "pinal", + "plain" + ], + "eksy": [ + "keys", + "syke", + "skey", + "skye" + ], + "achlnu": [ + "chulan", + "launch", + "nuchal" + ], + "ehms": [ + "hems", + "mesh", + "shem" + ], + "blmosy": [ + "blosmy", + "symbol" + ], + "aden": [ + "aden", + "ande", + "dane", + "dean", + "edna" + ], + "epstu": [ + "setup", + "spute", + "stupe", + "upset" + ], + "acfils": [ + "califs", + "fiscal" + ], + "elssty": [ + "slyest", + "styles" + ], + "deenrv": [ + "denver", + "nerved", + "revend", + "vender" + ], + "finoty": [ + "notify", + "tonify" + ], + "belsu": [ + "blues", + "bulse", + "lubes" + ], + "ceops": [ + "copes", + "copse", + "pecos", + "scope" + ], + "eilpprsu": [ + "periplus", + "supplier" + ], + "adelnt": [ + "dental", + "tandle" + ], + "bdeorr": [ + "border", + "roberd" + ], + "acessu": [ + "causes", + "causse", + "sauces" + ], + "adeelnr": [ + "laender", + "leander", + "learned", + "reladen" + ], + "chiiorst": [ + "historic", + "orchitis" + ], + "deenop": [ + "depone", + "opened" + ], + "ceorss": [ + "cessor", + "corses", + "crosse", + "scores", + "scorse" + ], + "aeilnr": [ + "aliner", + "arline", + "enrail", + "lainer", + "lanier", + "larine", + "linear", + "nailer", + "renail" + ], + "abers": [ + "bares", + "barse", + "baser", + "bears", + "besra", + "braes", + "saber", + "sabre", + "serab" + ], + "aejn": [ + "jane", + "jean" + ], + "hop": [ + "hop", + "pho", + "poh" + ], + "ddeeit": [ + "dieted", + "edited" + ], + "aisv": [ + "avis", + "siva", + "vias", + "visa" + ], + "eemrt": [ + "meter", + "metre", + "remet", + "retem" + ], + "deikln": [ + "kilned", + "kindle", + "linked" + ], + "ccenopst": [ + "concepts", + "conspect" + ], + "epru": [ + "peru", + "prue", + "pure" + ], + "deeilrv": [ + "deliver", + "deviler", + "livered", + "relived", + "reviled", + "riveled" + ], + "denorw": [ + "downer", + "nowder", + "wonder", + "worden" + ], + "elnosss": [ + "lessons", + "sonless" + ], + "begins": [ + "begins", + "beings", + "besing", + "binges" + ], + "aelrst": [ + "alerts", + "alters", + "artels", + "estral", + "laster", + "lastre", + "rastle", + "ratels", + "relast", + "resalt", + "salter", + "slater", + "staler", + "stelar", + "talers" + ], + "adrw": [ + "draw", + "ward" + ], + "aegilnrt": [ + "alerting", + "altering", + "integral", + "relating", + "tanglier", + "teraglin", + "triangle" + ], + "aemssu": [ + "amuses", + "assume", + "seamus" + ], + "aaceilln": [ + "alliance", + "ancillae", + "canaille" + ], + "eehinrt": [ + "enherit", + "etherin", + "neither", + "therein" + ], + "eilsw": [ + "lewis", + "swile", + "wiles" + ], + "aeelsv": [ + "leaves", + "sleave" + ], + "aceelpr": [ + "percale", + "replace" + ], + "ceenorrtv": [ + "converter", + "reconvert" + ], + "abbe": [ + "abbe", + "babe" + ], + "agrsu": [ + "argus", + "gaurs", + "guars", + "sugar" + ], + "egls": [ + "gels", + "legs" + ], + "ams": [ + "mas", + "sam", + "sma" + ], + "cikst": [ + "stick", + "ticks" + ], + "aelln": [ + "allen", + "ellan" + ], + "dpt": [ + "dpt", + "tpd" + ], + "aeilnort": [ + "oriental", + "relation", + "tirolean" + ], + "abdeeln": [ + "enabled", + "endable" + ], + "deils": [ + "deils", + "delis", + "idles", + "isled", + "sidle", + "slide" + ], + "deestt": [ + "detest", + "tested" + ], + "aadeprt": [ + "adapter", + "predata", + "readapt" + ], + "cklo": [ + "colk", + "lock" + ], + "cehkoy": [ + "chokey", + "hockey" + ], + "morst": [ + "morts", + "storm", + "strom" + ], + "cimor": [ + "micro", + "moric", + "romic" + ], + "eilm": [ + "emil", + "lime", + "mile" + ], + "deiorst": [ + "editors", + "oestrid", + "sortied", + "steroid", + "storied", + "triodes" + ], + "adehrst": [ + "dearths", + "hardest", + "hardset", + "hatreds", + "threads", + "trashed" + ], + "eemprsu": [ + "presume", + "supreme" + ], + "eenprsst": [ + "pensters", + "pertness", + "presents", + "serpents" + ], + "efr": [ + "erf", + "fer", + "ref" + ], + "aknt": [ + "kant", + "tank" + ], + "aeeimstt": [ + "estimate", + "etatisme", + "meatiest", + "teatimes" + ], + "ceiinnopst": [ + "cispontine", + "inceptions", + "inspection" + ], + "iilmst": [ + "limits", + "mislit" + ], + "aceehmnrst": [ + "manchester", + "searchment" + ], + "ainpt": [ + "inapt", + "paint", + "patin", + "pinta" + ], + "adely": [ + "delay", + "layed", + "leady" + ], + "ilopt": [ + "pilot", + "polit" + ], + "elottu": [ + "outlet", + "tutelo" + ], + "egilnrstu": [ + "lustering", + "resulting", + "ulstering" + ], + "anp": [ + "nap", + "pan" + ], + "alrtu": [ + "lutra", + "ultra" + ], + "aaeiimnnotx": [ + "examination", + "exanimation" + ], + "eoprtt": [ + "porett", + "potter" + ], + "alpsy": [ + "palsy", + "plays", + "splay" + ], + "beillntu": [ + "bulletin", + "unbillet" + ], + "acdeiinst": [ + "actinides", + "andesitic", + "dianetics", + "indicates" + ], + "dfimoy": [ + "domify", + "modify" + ], + "aadm": [ + "adam", + "dama", + "maad" + ], + "lrtuy": [ + "rutyl", + "truly" + ], + "agiinnpt": [ + "painting", + "patining" + ], + "aenptt": [ + "patent", + "patten", + "tapnet" + ], + "pps": [ + "pps", + "spp" + ], + "aegint": [ + "eating", + "ingate", + "tangie", + "teaing", + "tinage" + ], + "ceeeipprstv": [ + "perspective", + "prespective" + ], + "deglo": [ + "lodge", + "ogled" + ], + "egilnrst": [ + "lingster", + "ringlets", + "sterling", + "tinglers" + ], + "bersuy": [ + "buyers", + "rebusy" + ], + "agry": [ + "gary", + "gray" + ], + "aaceglotu": [ + "catalogue", + "coagulate" + ], + "aaintw": [ + "atwain", + "taiwan" + ], + "cehnos": [ + "cheson", + "chosen", + "cohens", + "schone" + ], + "aahrs": [ + "asarh", + "haars", + "haras", + "raash", + "sarah" + ], + "aeilmnrt": [ + "terminal", + "tramline" + ], + "aelnors": [ + "loaners", + "orleans", + "reloans" + ], + "eeimpr": [ + "empire", + "epimer", + "premie" + ], + "aeirs": [ + "aesir", + "aries", + "arise", + "raise", + "serai" + ], + "aeepprr": [ + "paperer", + "perpera", + "prepare", + "repaper" + ], + "aabr": [ + "arab", + "arba", + "baar", + "bara" + ], + "eeimprr": [ + "premier", + "reprime" + ], + "mooorrtw": [ + "moorwort", + "rootworm", + "tomorrow", + "wormroot" + ], + "cddeei": [ + "decide", + "deiced" + ], + "aadmr": [ + "damar", + "drama" + ], + "efgimnoprr": [ + "performing", + "preforming" + ], + "abeilstu": [ + "sabulite", + "suitable" + ], + "abcehmr": [ + "becharm", + "brecham", + "chamber", + "chambre" + ], + "aeginu": [ + "enigua", + "guinea", + "naigue" + ], + "celmsu": [ + "clumse", + "muscle" + ], + "aefginrtu": [ + "featuring", + "figurante" + ], + "ios": [ + "ios", + "iso", + "osi" + ], + "orsuy": [ + "yours", + "soury" + ], + "msu": [ + "mus", + "sum" + ], + "adentt": [ + "attend", + "detant" + ], + "ehorsw": [ + "reshow", + "shower", + "whores" + ], + "aaln": [ + "alan", + "anal", + "lana" + ], + "deginns": [ + "endings", + "sending" + ], + "ajnos": [ + "janos", + "jason", + "jonas", + "sonja" + ], + "ghinott": [ + "hotting", + "tonight" + ], + "ehlls": [ + "hells", + "shell" + ], + "ako": [ + "ako", + "koa", + "oak", + "oka" + ], + "atv": [ + "tav", + "vat" + ], + "beer": [ + "beer", + "bere", + "bree" + ], + "deeems": [ + "seemed", + "semeed" + ], + "alors": [ + "orals", + "rosal", + "solar", + "soral" + ], + "ejos": [ + "joes", + "jose" + ], + "irs": [ + "irs", + "sir", + "sri" + ], + "abelnu": [ + "nebula", + "unable", + "unbale" + ], + "aksst": [ + "skats", + "tasks" + ], + "aceeinrsst": [ + "ancestries", + "resistance", + "senatrices" + ], + "doors": [ + "doors", + "odors", + "ordos", + "roods", + "soord", + "sordo" + ], + "eorrsst": [ + "resorts", + "ressort", + "rosters", + "sorters" + ], + "iiorstv": [ + "ivorist", + "visitor" + ], + "intw": [ + "twin", + "wint" + ], + "fhort": [ + "forth", + "froth" + ], + "einrst": [ + "estrin", + "inerts", + "insert", + "inters", + "niters", + "nitres", + "sinter", + "sterin", + "triens", + "trines" + ], + "abeilmort": [ + "artmobile", + "baltimore" + ], + "aaegtwy": [ + "gateway", + "getaway", + "waygate" + ], + "ailmnu": [ + "alumin", + "alumni", + "lumina", + "unmail" + ], + "adginrw": [ + "drawing", + "ginward", + "warding" + ], + "acemnor": [ + "cremona", + "romance" + ], + "eimnnrsttu": [ + "instrument", + "nutriments" + ], + "bceru": [ + "bruce", + "cebur", + "cuber" + ], + "ilpst": [ + "slipt", + "spilt", + "split" + ], + "eehmst": [ + "smeeth", + "smethe", + "themes" + ], + "bist": [ + "bist", + "bits", + "stib" + ], + "cdefosu": [ + "defocus", + "focused" + ], + "agikns": [ + "asking", + "gaskin", + "kiangs", + "kisang" + ], + "abdeeist": [ + "beadiest", + "diabetes" + ], + "istu": [ + "situ", + "suit", + "tuis" + ], + "chip": [ + "chip", + "pich" + ], + "ers": [ + "ers", + "res", + "ser" + ], + "bdeios": [ + "bodies", + "dobies", + "obside" + ], + "imnos": [ + "minos", + "osmin", + "simon" + ], + "eirrstw": [ + "wrister", + "writers" + ], + "delov": [ + "loved", + "voled" + ], + "bdirs": [ + "birds", + "dribs" + ], + "eeenprrsst": [ + "presenters", + "represents" + ], + "achr": [ + "arch", + "char", + "rach" + ], + "adesv": [ + "devas", + "saved" + ], + "acnoort": [ + "cartoon", + "coranto" + ], + "hosst": [ + "hosts", + "shots", + "soths", + "stosh" + ], + "emoor": [ + "moore", + "romeo" + ], + "adegnrt": [ + "dragnet", + "granted" + ], + "ccehios": [ + "choices", + "secchio" + ], + "abcnor": [ + "bracon", + "carbon", + "corban" + ], + "egiilnnst": [ + "enlisting", + "listening", + "tinseling" + ], + "kloootu": [ + "lookout", + "outlook" + ], + "aeimssv": [ + "massive", + "mavises" + ], + "aertt": [ + "atter", + "tarte", + "tater", + "teart", + "tetra", + "treat" + ], + "adeehr": [ + "adhere", + "header", + "hedera", + "rehead", + "rhedae" + ], + "defmor": [ + "deform", + "formed" + ], + "dgir": [ + "gird", + "grid" + ], + "eehsst": [ + "sheets", + "theses" + ], + "acikprt": [ + "patrick", + "tripack" + ], + "eoprtu": [ + "pouter", + "puerto", + "roupet", + "troupe", + "uptore" + ], + "aalmps": [ + "lampas", + "plasma" + ], + "aeginnrs": [ + "aginners", + "earnings", + "engrains", + "grannies" + ], + "ikrss": [ + "kriss", + "risks" + ], + "acehrrt": [ + "charter", + "ratcher", + "rechart" + ], + "fgi": [ + "fig", + "gif" + ], + "aaabbrr": [ + "barabra", + "barbara" + ], + "ademrs": [ + "dermas", + "dreams", + "madres" + ], + "begglor": [ + "boggler", + "broggle" + ], + "cegiilnns": [ + "licensing", + "silencing" + ], + "aceht": [ + "cheat", + "tache", + "teach", + "theca" + ], + "adipr": [ + "adrip", + "padri", + "pardi", + "rapid" + ], + "deiopst": [ + "deposit", + "dopiest", + "podites", + "posited", + "sopited", + "topside" + ], + "aailnt": [ + "antlia", + "latian", + "nalita" + ], + "aans": [ + "anas", + "ansa", + "nasa", + "saan" + ], + "eehlsw": [ + "shewel", + "wheels" + ], + "aeelmpstt": [ + "palmettes", + "templates" + ], + "afmorst": [ + "farmost", + "formats" + ], + "abt": [ + "abt", + "bat", + "tab" + ], + "ddeenps": [ + "depends", + "despend" + ], + "boost": [ + "boost", + "boots" + ], + "eorrtu": [ + "retour", + "roture", + "router", + "tourer" + ], + "degiint": [ + "dieting", + "editing", + "ignited" + ], + "deflor": [ + "folder", + "refold" + ], + "elpsu": [ + "lepus", + "pules", + "pulse" + ], + "corstu": [ + "courts", + "scrout", + "scruto" + ], + "deiortt": [ + "detroit", + "dottier" + ], + "emort": [ + "metro", + "moter" + ], + "iprst": [ + "spirt", + "sprit", + "stirp", + "strip", + "trips" + ], + "aelpr": [ + "lepra", + "paler", + "parel", + "parle", + "pearl", + "perla", + "prela", + "relap" + ], + "deeinrst": [ + "disenter", + "indesert", + "inserted", + "resident", + "sentried", + "sintered" + ], + "lopt": [ + "plot", + "polt" + ], + "adegrr": [ + "darger", + "garred", + "gerard", + "grader", + "redrag", + "regard" + ], + "eisstx": [ + "exists", + "sexist", + "sixtes" + ], + "eikrst": [ + "kiters", + "skiter", + "strike" + ], + "aehrtt": [ + "hatter", + "threat" + ], + "ehinoprsw": [ + "ownership", + "shipowner" + ], + "aeilrrt": [ + "retiral", + "retrial", + "trailer" + ], + "aenn": [ + "anne", + "nane" + ], + "acelst": [ + "castle", + "cleats", + "eclats", + "scalet", + "sclate" + ], + "adegnrs": [ + "dangers", + "ganders", + "gardens" + ], + "deimss": [ + "deisms", + "demiss", + "dismes", + "missed" + ], + "aeinqtu": [ + "antique", + "quinate" + ], + "bio": [ + "bio", + "ibo", + "obi" + ], + "acgint": [ + "acting", + "cating" + ], + "adehs": [ + "ashed", + "deash", + "hades", + "heads", + "sadhe", + "shade" + ], + "aemx": [ + "amex", + "exam", + "xema" + ], + "gloos": [ + "gools", + "logos" + ], + "aeinqstu": [ + "antiques", + "quanties" + ], + "deinsty": [ + "density", + "destiny" + ], + "anry": [ + "yarn", + "nary" + ], + "aegnrst": [ + "angster", + "argents", + "garnets", + "nagster", + "strange" + ], + "bdes": [ + "beds", + "debs" + ], + "cps": [ + "cps", + "csp" + ], + "eelmoprsy": [ + "employers", + "reemploys" + ], + "egry": [ + "gery", + "gyre", + "grey" + ], + "addeemn": [ + "amended", + "deadmen" + ], + "bdlo": [ + "bold", + "dobl" + ], + "elnoss": [ + "lesson", + "nossel", + "onless" + ], + "aceimn": [ + "anemic", + "cinema", + "iceman" + ], + "aesst": [ + "asset", + "easts", + "sates", + "seats", + "tasse" + ], + "acns": [ + "cans", + "scan" + ], + "eeersv": [ + "everse", + "reeves", + "severe" + ], + "aeeegnrt": [ + "generate", + "renegate", + "teenager" + ], + "aeilnssst": [ + "saintless", + "saltiness", + "slatiness", + "stainless" + ], + "ahilopsst": [ + "hospitals", + "thalposis" + ], + "hmoru": [ + "humor", + "mohur" + ], + "adeg": [ + "aged", + "egad", + "gade", + "gaed" + ], + "ceeinoptx": [ + "exception", + "expection" + ], + "deilv": [ + "devil", + "divel", + "lived" + ], + "adinortu": [ + "duration", + "unadroit" + ], + "cis": [ + "cis", + "csi", + "sci", + "sic" + ], + "det": [ + "det", + "ted" + ], + "adimnos": [ + "daimons", + "domains", + "madison" + ], + "fgilny": [ + "flying", + "flingy" + ], + "ins": [ + "ins", + "isn", + "nis", + "sin" + ], + "adeginorz": [ + "dragonize", + "organized" + ], + "aapr": [ + "apar", + "paar", + "para" + ], + "eeimnss": [ + "nemesis", + "siemens" + ], + "aemnt": [ + "ament", + "manet", + "meant", + "menat", + "menta", + "teman" + ], + "aceprtu": [ + "capture", + "cuprate", + "uptrace" + ], + "dnopsu": [ + "pondus", + "pounds" + ], + "dees": [ + "dees", + "seed" + ], + "deeirs": [ + "desire", + "eiders", + "reside" + ], + "eemst": [ + "meets", + "metes", + "steem", + "teems", + "temse" + ], + "eepr": [ + "peer", + "pere", + "pree" + ], + "adekmr": [ + "demark", + "marked" + ], + "deinrv": [ + "driven", + "nervid", + "verdin" + ], + "adeemrsu": [ + "madurese", + "measured" + ], + "adhnostu": [ + "handouts", + "thousand" + ], + "acegr": [ + "cager", + "garce", + "grace" + ], + "anssu": [ + "nasus", + "susan" + ], + "gin": [ + "gin", + "ign", + "ing", + "nig" + ], + "aadms": [ + "adams", + "damas" + ], + "hnopty": [ + "phyton", + "python", + "typhon" + ], + "emnorst": [ + "mentors", + "monster" + ], + "aelx": [ + "alex", + "axel", + "axle", + "exla" + ], + "beno": [ + "beno", + "bone", + "ebon" + ], + "bgsu": [ + "bugs", + "subg" + ], + "einnr": [ + "inner", + "renin" + ], + "ailorttu": [ + "outtrail", + "tutorial" + ], + "dem": [ + "dem", + "med" + ], + "eeeginnrs": [ + "engineers", + "geneserin" + ], + "eintty": [ + "entity", + "tinety" + ], + "ceirssu": [ + "ciruses", + "cruises" + ], + "aegt": [ + "aget", + "gaet", + "gate", + "geat", + "geta", + "tega" + ], + "amnor": [ + "manor", + "moran", + "norma", + "ramon", + "roman" + ], + "deistu": [ + "duties", + "eudist", + "setuid", + "suited" + ], + "cehist": [ + "ethics", + "itches", + "sethic" + ], + "adgnor": [ + "dragon", + "gardon", + "grando" + ], + "bsuy": [ + "buys", + "busy" + ], + "aacinpt": [ + "capitan", + "captain", + "panctia" + ], + "aeghint": [ + "gahnite", + "heating" + ], + "egl": [ + "gel", + "leg" + ], + "abc": [ + "abc", + "bac", + "cab" + ], + "ailr": [ + "aril", + "lair", + "lari", + "liar", + "lira", + "rail", + "rial" + ], + "abeillr": [ + "braille", + "liberal" + ], + "betu": [ + "bute", + "tebu", + "tube" + ], + "nrstu": [ + "runts", + "snurt", + "turns" + ], + "acceh": [ + "cache", + "chace" + ], + "belt": [ + "belt", + "blet" + ], + "aaiimnnot": [ + "amination", + "animation" + ], + "acelor": [ + "carole", + "coaler", + "coelar", + "colera", + "oracle", + "recoal" + ], + "aeels": [ + "easel", + "lease" + ], + "adeirsst": [ + "diasters", + "disaster", + "disrates" + ], + "celnoos": [ + "colones", + "console" + ], + "agint": [ + "ating", + "giant", + "tangi", + "tiang" + ], + "aalmr": [ + "alarm", + "malar", + "maral", + "marla", + "ramal" + ], + "alsuu": [ + "luaus", + "usual" + ], + "adgilno": [ + "angloid", + "digonal", + "loading" + ], + "bor": [ + "bor", + "bro", + "orb", + "rob" + ], + "deeginrss": [ + "designers", + "redesigns" + ], + "orstw": [ + "strow", + "trows", + "worst", + "worts" + ], + "aaeginnrt": [ + "argentina", + "tanagrine" + ], + "ceiimmnoorss": [ + "commissioner", + "recommission" + ], + "agmnor": [ + "gorman", + "morgan" + ], + "aeelprsu": [ + "pleasure", + "serpulae" + ], + "deeinort": [ + "enteroid", + "orendite", + "oriented" + ], + "aeegl": [ + "aegle", + "aglee", + "eagle", + "galee" + ], + "enrsu": [ + "nurse", + "resun", + "runes" + ], + "aeprry": [ + "prayer", + "repray" + ], + "acehinrru": [ + "hurricane", + "raunchier" + ], + "aegopst": [ + "gestapo", + "postage", + "potages" + ], + "cdeoprru": [ + "procured", + "producer" + ], + "adil": [ + "dail", + "dali", + "dial", + "laid", + "lida" + ], + "aekmr": [ + "maker", + "marek", + "merak" + ], + "cikps": [ + "picks", + "spick" + ], + "efhist": [ + "fetish", + "fishet" + ], + "acinoss": [ + "caisson", + "casinos", + "cassino", + "cassoni" + ], + "ekmos": [ + "mokes", + "smoke" + ], + "aacehp": [ + "achape", + "apache" + ], + "efilrst": [ + "filters", + "frislet", + "lifters", + "slifter", + "stifler", + "trifles" + ], + "acdeinooprrt": [ + "adrenotropic", + "incorporated" + ], + "acfrt": [ + "craft", + "fract" + ], + "aaprt": [ + "apart", + "trapa" + ], + "eglnou": [ + "longue", + "lounge" + ], + "adm": [ + "adm", + "dam", + "mad" + ], + "aghilmort": [ + "algorithm", + "logarithm" + ], + "eims": [ + "mise", + "semi", + "sime" + ], + "cinos": [ + "cions", + "coins", + "cosin", + "icons", + "oscin", + "scion", + "sonic" + ], + "glnorsty": [ + "strongyl", + "strongly" + ], + "aeeilnntv": [ + "levantine", + "valentine" + ], + "ekn": [ + "ken", + "nek" + ], + "einoprst": [ + "pointers", + "proteins", + "ripstone", + "tropines" + ], + "aabcelp": [ + "capable", + "pacable" + ], + "illt": [ + "itll", + "lilt", + "till" + ], + "enp": [ + "nep", + "pen" + ], + "enops": [ + "opens", + "peons", + "pones" + ], + "ehos": [ + "hoes", + "hose", + "shoe" + ], + "adns": [ + "ands", + "sand" + ], + "deiinost": [ + "desition", + "editions", + "sedition" + ], + "ailmny": [ + "amylin", + "mainly" + ], + "anr": [ + "arn", + "nar", + "ran" + ], + "aaeilmnprt": [ + "palermitan", + "parliament" + ], + "acort": [ + "actor", + "carot", + "coart", + "corta", + "croat", + "rocta", + "taroc", + "troca" + ], + "aacdellot": [ + "acetaldol", + "allocated" + ], + "ceiintz": [ + "citizen", + "zincite" + ], + "ccorsu": [ + "crocus", + "occurs", + "succor" + ], + "aeimnty": [ + "amenity", + "anytime" + ], + "eils": [ + "isle", + "leis", + "lies", + "lise", + "sile" + ], + "alotuy": [ + "layout", + "lutayo", + "outlay" + ], + "bls": [ + "bls", + "lbs" + ], + "aly": [ + "aly", + "lay" + ], + "ehorss": [ + "horses", + "shoers", + "shores" + ], + "aenwy": [ + "wayne", + "waney" + ], + "adenot": [ + "atoned", + "donate" + ], + "ekorrw": [ + "rework", + "worker" + ], + "aeilv": [ + "alive", + "avile" + ], + "eelmpt": [ + "pelmet", + "temple" + ], + "ginsw": [ + "swing", + "wings" + ], + "abekrs": [ + "bakers", + "basker", + "brakes", + "breaks", + "kebars" + ], + "aerstw": [ + "rawest", + "tawers", + "waster", + "waters" + ], + "eimoprs": [ + "imposer", + "promise", + "semipro" + ], + "hint": [ + "hint", + "thin" + ], + "degir": [ + "dirge", + "egrid", + "gride", + "redig", + "ridge" + ], + "ahirrs": [ + "arrish", + "harris", + "rarish", + "shirra", + "sirrah" + ], + "aciloprt": [ + "plicator", + "tropical" + ], + "eersstt": [ + "retests", + "setters", + "streets", + "tersest", + "testers" + ], + "ceortv": [ + "corvet", + "covert", + "vector" + ], + "beffru": [ + "buffer", + "rebuff" + ], + "elppru": [ + "pulper", + "purple" + ], + "def": [ + "def", + "fed" + ], + "adeiinnosstt": [ + "destinations", + "dissentation" + ], + "elst": [ + "lest", + "lets", + "selt" + ], + "almtuu": [ + "mutual", + "umlaut" + ], + "inoprs": [ + "orpins", + "prinos", + "prison", + "spinor" + ], + "iklls": [ + "kills", + "skill" + ], + "achirs": [ + "chairs", + "ischar", + "rachis" + ], + "denrt": [ + "drent", + "trend" + ], + "aeirrs": [ + "airers", + "ariser", + "raiser", + "serrai", + "sierra" + ], + "deerst": [ + "desert", + "deters", + "rested" + ], + "delost": [ + "oldest", + "sloted", + "stoled" + ], + "abn": [ + "abn", + "ban", + "nab" + ], + "abdhknoo": [ + "bandhook", + "handbook" + ], + "aaegintv": [ + "navigate", + "vaginate" + ], + "eorsw": [ + "owser", + "resow", + "serow", + "sower", + "swore", + "worse" + ], + "immstu": [ + "mutism", + "summit" + ], + "aep": [ + "ape", + "epa", + "pea" + ], + "acepss": [ + "scapes", + "spaces" + ], + "aceeps": [ + "escape", + "espace", + "peaces" + ], + "cnoopsu": [ + "coupons", + "soupcon" + ], + "aciils": [ + "sialic", + "silica" + ], + "abost": [ + "basto", + "boast", + "boats", + "botas", + "sabot" + ], + "acegln": [ + "cangle", + "glance" + ], + "ellst": [ + "stell", + "tells" + ], + "bdemoors": [ + "bedrooms", + "boredoms" + ], + "aklst": [ + "stalk", + "talks" + ], + "ailrst": [ + "latris", + "strail", + "strial", + "trails", + "trials" + ], + "aemrsst": [ + "masters", + "streams" + ], + "aabelrt": [ + "alberta", + "latebra", + "ratable" + ], + "gor": [ + "gor", + "gro", + "org", + "rog" + ], + "cmmnoos": [ + "commons", + "consomm" + ], + "adfru": [ + "faurd", + "fraud" + ], + "cemprstu": [ + "crumpets", + "spectrum" + ], + "akoy": [ + "kayo", + "oaky", + "okay" + ], + "aehimpss": [ + "emphasis", + "misshape" + ], + "egorr": [ + "gorer", + "roger" + ], + "acepst": [ + "aspect", + "epacts" + ], + "aeemosw": [ + "awesome", + "waesome" + ], + "cnostu": [ + "counts", + "tucson", + "uncost" + ], + "cdeipr": [ + "percid", + "priced" + ], + "achrs": [ + "chars", + "crash" + ], + "filt": [ + "filt", + "flit", + "lift" + ], + "ddeeirs": [ + "derides", + "desired", + "resided" + ], + "einrt": [ + "inert", + "inter", + "niter", + "nitre", + "retin", + "trine" + ], + "celors": [ + "ceorls", + "closer", + "cresol", + "escrol" + ], + "ails": [ + "ails", + "lasi", + "lias", + "lisa", + "sail", + "sial" + ], + "ceinprss": [ + "crispens", + "princess" + ], + "aprsy": [ + "prays", + "raspy", + "spary", + "spray" + ], + "deentx": [ + "dentex", + "extend" + ], + "adors": [ + "dorsa", + "roads", + "sarod", + "sorda" + ], + "apt": [ + "apt", + "pat", + "pta", + "tap" + ], + "adry": [ + "adry", + "dray", + "yard" + ], + "eeimmors": [ + "memories", + "memorise" + ], + "aceerst": [ + "cerates", + "creates", + "ecartes", + "secreta" + ], + "acefs": [ + "cafes", + "faces" + ], + "amory": [ + "mayor", + "moray" + ], + "aens": [ + "anes", + "sane", + "sean", + "sena" + ], + "aenorst": [ + "atoners", + "noreast", + "orantes", + "rosetan", + "seatron", + "senator", + "treason" + ], + "adegrs": [ + "degras", + "egards", + "grades" + ], + "acnoorst": [ + "cartoons", + "corantos", + "ostracon", + "socotran" + ], + "opru": [ + "pour", + "roup" + ], + "egr": [ + "erg", + "ger", + "gre", + "reg" + ], + "dggilno": [ + "godling", + "golding", + "lodging" + ], + "dstu": [ + "dust", + "stud" + ], + "eeilnrty": [ + "entirely", + "lientery" + ], + "acdeelpr": [ + "parceled", + "replaced" + ], + "elosss": [ + "losses", + "sossle" + ], + "abcmot": [ + "combat", + "tombac" + ], + "aekls": [ + "alkes", + "kales", + "lakes", + "leaks", + "sakel", + "slake" + ], + "adinnoost": [ + "anisodont", + "donations", + "sondation" + ], + "adiry": [ + "dairy", + "diary", + "yaird" + ], + "gikns": [ + "ginks", + "kings" + ], + "ghinoost": [ + "shooting", + "soothing" + ], + "eknt": [ + "kent", + "knet" + ], + "adds": [ + "adds", + "dads" + ], + "aceeinorstvv": [ + "conservative", + "conversative" + ], + "chkos": [ + "hocks", + "shock" + ], + "aabdor": [ + "aboard", + "aborad", + "abroad" + ], + "benoy": [ + "boney", + "ebony" + ], + "ain": [ + "ain", + "ani", + "ian" + ], + "aeehmoprst": [ + "atmosphere", + "shapometer" + ], + "ikss": [ + "kiss", + "skis" + ], + "abest": [ + "abets", + "baste", + "bates", + "beast", + "beats", + "betas", + "estab", + "sebat", + "tabes" + ], + "aegrstt": [ + "tagster", + "targets" + ], + "celnosu": [ + "counsel", + "unclose" + ], + "adrsy": [ + "drays", + "dryas", + "yards" + ], + "dgnoor": [ + "drongo", + "gordon" + ], + "dmo": [ + "dom", + "mod" + ], + "aefmrrs": [ + "farmers", + "framers" + ], + "eeiqrsu": [ + "esquire", + "queries", + "risquee" + ], + "hrsu": [ + "rhus", + "rush" + ], + "celrstu": [ + "cluster", + "custrel", + "cutlers", + "relucts" + ], + "eerssv": [ + "serves", + "severs", + "sevres", + "verses" + ], + "eiprrssu": [ + "spurries", + "surprise", + "uprisers" + ], + "aailprt": [ + "partial", + "patrial" + ], + "celopsu": [ + "closeup", + "couples", + "culpose", + "opuscle", + "ploceus", + "upclose" + ], + "agiknnr": [ + "narking", + "ranking" + ], + "cst": [ + "cst", + "cts", + "sct" + ], + "ceo": [ + "coe", + "eco" + ], + "aacelp": [ + "aplace", + "palace" + ], + "beglo": [ + "bogle", + "globe" + ], + "ackr": [ + "cark", + "rack" + ], + "acdeiimnot": [ + "decimation", + "medication" + ], + "aeehorsuw": [ + "housewear", + "warehouse" + ], + "ceeiprt": [ + "ereptic", + "precite", + "receipt" + ], + "ghost": [ + "ghost", + "goths" + ], + "boss": [ + "boss", + "sobs" + ], + "deipr": [ + "pride", + "pried", + "redip", + "riped" + ], + "ehikt": [ + "keith", + "kithe" + ], + "adiln": [ + "danli", + "ladin", + "linda", + "nidal" + ], + "cehil": [ + "chiel", + "chile" + ], + "aann": [ + "anan", + "anna", + "nana" + ], + "elnpty": [ + "pentyl", + "plenty" + ], + "loos": [ + "loos", + "oslo", + "sloo", + "solo", + "sool" + ], + "ahortt": [ + "athort", + "throat" + ], + "eirstw": [ + "twiers", + "wister", + "wriest", + "writes" + ], + "adps": [ + "daps", + "pads", + "spad" + ], + "eqstu": [ + "quest", + "squet" + ], + "ceeginnrs": [ + "screening", + "secerning" + ], + "anrst": [ + "rants", + "starn", + "tarns", + "trans" + ], + "eeinopr": [ + "pereion", + "peronei", + "pioneer" + ], + "aabcort": [ + "abactor", + "acrobat" + ], + "aelpst": [ + "palest", + "palets", + "pastel", + "petals", + "plates", + "pleats", + "septal", + "staple", + "tepals" + ], + "acers": [ + "acres", + "arces", + "cares", + "carse", + "caser", + "ceras", + "cesar", + "escar", + "races", + "sacre", + "scare", + "scrae", + "serac" + ], + "acehiltt": [ + "athletic", + "thetical" + ], + "egillnt": [ + "gillnet", + "telling" + ], + "aaclost": [ + "catalos", + "coastal", + "salacot" + ], + "demos": [ + "demos", + "domes", + "modes" + ], + "aekw": [ + "wake", + "weak", + "weka" + ], + "aghnruy": [ + "ahungry", + "hungary" + ], + "aeelrrtv": [ + "retravel", + "revertal", + "traveler" + ], + "aln": [ + "aln", + "lan" + ], + "eemny": [ + "enemy", + "yemen" + ], + "giinrs": [ + "rising", + "siring" + ], + "ellsw": [ + "swell", + "wells" + ], + "ghiinst": [ + "histing", + "insight" + ], + "cdeeirrstt": [ + "derestrict", + "restricted" + ], + "ceersst": [ + "cresset", + "resects", + "secrets" + ], + "aelrtt": [ + "artlet", + "latter", + "rattel", + "rattle", + "talter", + "tartle", + "tatler" + ], + "acehmnrst": [ + "merchants", + "starchmen" + ], + "aeilrrst": [ + "retrials", + "trailers" + ], + "aeeprt": [ + "petrea", + "repeat", + "retape" + ], + "aelnpty": [ + "aplenty", + "penalty" + ], + "aeglsss": [ + "gasless", + "glasses", + "sagless" + ], + "abeelns": [ + "baleens", + "enables" + ], + "cen": [ + "cen", + "enc" + ], + "bdeilru": [ + "builder", + "rebuild" + ], + "errty": [ + "retry", + "terry" + ], + "aegmnrstu": [ + "argentums", + "arguments", + "mustanger" + ], + "aaenr": [ + "anear", + "arean", + "arena" + ], + "ilppsu": [ + "pupils", + "slipup", + "upslip" + ], + "aersttw": [ + "stewart", + "swatter" + ], + "abst": [ + "bast", + "bats", + "stab", + "tabs" + ], + "aaclsu": [ + "ascula", + "calusa", + "casual", + "casula", + "causal" + ], + "hilops": [ + "philos", + "polish" + ], + "ellovy": [ + "lovely", + "volley" + ], + "aerstx": [ + "extras", + "sextar", + "taxers" + ], + "acelsu": [ + "caelus", + "casule", + "caules", + "clause" + ], + "einp": [ + "pein", + "pien", + "pine" + ], + "cgilnoo": [ + "cooling", + "locoing" + ], + "dent": [ + "dent", + "detn", + "tend" + ], + "ckrstu": [ + "struck", + "trucks" + ], + "cdeiorv": [ + "cervoid", + "divorce" + ], + "aalru": [ + "aural", + "laura" + ], + "ehopprs": [ + "hoppers", + "shopper" + ], + "kooty": [ + "kyoto", + "tokyo" + ], + "alprty": [ + "paltry", + "partly", + "raptly" + ], + "acdny": [ + "candy", + "dancy" + ], + "illps": [ + "pills", + "spill" + ], + "egirt": [ + "greit", + "tiger", + "tigre" + ], + "enorss": [ + "senors", + "sensor", + "snores" + ], + "aeglns": [ + "angels", + "angles", + "gansel", + "gleans" + ], + "acdiinorst": [ + "ancistroid", + "indicators" + ], + "adeels": [ + "leased", + "sealed" + ], + "ahit": [ + "aith", + "hait", + "hati", + "thai" + ], + "defr": [ + "derf", + "ferd", + "fred" + ], + "acilmnopt": [ + "complaint", + "compliant" + ], + "ceenss": [ + "censes", + "scenes" + ], + "almor": [ + "molar", + "moral", + "romal" + ], + "adu": [ + "aud", + "dau" + ], + "efginr": [ + "finger", + "fringe" + ], + "eekps": [ + "keeps", + "peeks", + "pekes" + ], + "acelot": [ + "acetol", + "colate", + "locate" + ], + "adeinrt": [ + "andrite", + "antired", + "detrain", + "diantre", + "radient", + "randite", + "trained" + ], + "eorss": [ + "roses", + "sores" + ], + "abls": [ + "albs", + "bals", + "blas", + "labs", + "slab" + ], + "abder": [ + "ardeb", + "barde", + "bared", + "beard", + "bread", + "debar" + ], + "denoow": [ + "enwood", + "wooden" + ], + "ghotu": [ + "ought", + "tough" + ], + "eil": [ + "eli", + "ile", + "lei", + "lie" + ], + "cehst": [ + "chest", + "stech" + ], + "einnops": [ + "pension", + "pinones" + ], + "eeenrsuv": [ + "revenues", + "unreeves", + "unsevere" + ], + "acgir": [ + "agric", + "cigar", + "craig" + ], + "eefhlrs": [ + "flesher", + "herself" + ], + "ceiinoprs": [ + "coinspire", + "precision" + ], + "eeerrssv": [ + "reserves", + "reverses", + "severers" + ], + "elosv": [ + "loves", + "solve", + "voles" + ], + "horsst": [ + "horsts", + "shorts" + ], + "cdeinooprrtu": [ + "proreduction", + "reproduction" + ], + "deegiinnrst": [ + "ingredients", + "tenderising" + ], + "cdeeorrr": [ + "recorder", + "rerecord" + ], + "acnny": [ + "canny", + "nancy" + ], + "acly": [ + "acyl", + "clay", + "lacy" + ], + "acehpst": [ + "hepcats", + "patches" + ], + "defnru": [ + "funder", + "refund" + ], + "nostw": [ + "nowts", + "towns", + "wonts" + ], + "ceeinoprt": [ + "prenotice", + "reception" + ], + "aeilms": [ + "amiles", + "asmile", + "mailes", + "mesail", + "mesial", + "samiel" + ], + "cprsuy": [ + "cyprus", + "sprucy" + ], + "ddos": [ + "dods", + "odds" + ], + "deiinrs": [ + "insider", + "siderin" + ], + "aeimnrss": [ + "arsenism", + "seminars" + ], + "aekmrs": [ + "makers", + "masker", + "remask" + ], + "aehrst": [ + "earths", + "haster", + "haters", + "hearst", + "hearts" + ], + "eev": [ + "eve", + "vee" + ], + "acerrt": [ + "arrect", + "carter", + "crater", + "recart", + "tracer" + ], + "acmr": [ + "cram", + "marc" + ], + "adeelps": [ + "delapse", + "elapsed", + "pleased", + "sepaled" + ], + "deilwy": [ + "dewily", + "widely", + "wieldy" + ], + "aehprs": [ + "phaser", + "phrase", + "raphes", + "seraph", + "shaper", + "sherpa", + "shrape" + ], + "eeginnu": [ + "genuine", + "ingenue" + ], + "agiinrs": [ + "airings", + "arising", + "raising" + ], + "aadeiprs": [ + "paradise", + "sparidae" + ], + "aders": [ + "dares", + "dears", + "rased", + "reads" + ], + "elors": [ + "lores", + "loser", + "orles", + "orsel", + "roles", + "rosel", + "soler", + "sorel" + ], + "aefl": [ + "alef", + "feal", + "flea", + "leaf" + ], + "deeils": [ + "delies", + "diesel", + "ediles", + "elides", + "sedile", + "seidel" + ], + "erssuv": [ + "servus", + "versus" + ], + "dor": [ + "dor", + "ord", + "rod" + ], + "adisu": [ + "saudi", + "udasi" + ], + "hrs": [ + "hrs", + "shr" + ], + "ikls": [ + "ilks", + "lisk", + "silk", + "skil", + "slik" + ], + "aeknr": [ + "anker", + "karen", + "kearn", + "naker", + "nerka" + ], + "cdeilmop": [ + "compiled", + "complied" + ], + "acimnort": [ + "macrotin", + "romantic" + ], + "adeeelrv": [ + "laveered", + "revealed" + ], + "abelrt": [ + "albert", + "balter", + "batler", + "labret", + "tabler", + "tarble" + ], + "bilorst": [ + "bristol", + "strobil" + ], + "acl": [ + "alc", + "cal", + "lac", + "lca" + ], + "inooprst": [ + "notropis", + "portions", + "positron", + "sorption" + ], + "ceorsst": [ + "corsets", + "costers", + "escorts", + "scoters", + "sectors" + ], + "aelmsu": [ + "amelus", + "samuel", + "ulemas" + ], + "fist": [ + "fist", + "fits", + "sift" + ], + "adegrrs": [ + "graders", + "regards" + ], + "hrtu": [ + "hurt", + "ruth", + "thru" + ], + "acehimnry": [ + "hemicrany", + "machinery" + ], + "gim": [ + "gim", + "mig" + ], + "aenrrw": [ + "rewarn", + "warner", + "warren" + ], + "ilps": [ + "lips", + "lisp", + "slip" + ], + "ddeistu": [ + "studdie", + "studied" + ], + "aeimr": [ + "aimer", + "amire", + "maire", + "marie", + "ramie" + ], + "cstu": [ + "cust", + "cuts", + "scut" + ], + "aeflnru": [ + "earnful", + "flaneur", + "frenula", + "funeral", + "furlane" + ], + "aeginrrs": [ + "earrings", + "grainers" + ], + "acehprst": [ + "chapters", + "patchers" + ], + "deinns": [ + "dennis", + "sinned" + ], + "aagmn": [ + "amang", + "ganam", + "magna", + "manga" + ], + "cdeinot": [ + "condite", + "ctenoid", + "deontic", + "noticed" + ], + "aeilrrty": [ + "literary", + "trailery" + ], + "agilnss": [ + "glassin", + "signals" + ], + "acps": [ + "caps", + "pacs", + "scap" + ], + "alt": [ + "alt", + "lat", + "tal" + ], + "aaglno": [ + "agonal", + "analog", + "angola" + ], + "aacfil": [ + "cafila", + "facial" + ], + "aelntt": [ + "latent", + "latten", + "nattle", + "talent", + "tantle" + ], + "eeekrs": [ + "kreese", + "reseek", + "seeker", + "sekere" + ], + "hoost": [ + "hoots", + "shoot", + "sooth", + "sotho", + "toosh" + ], + "effost": [ + "offset", + "setoff" + ], + "eeilt": [ + "elite", + "telei" + ], + "inps": [ + "insp", + "nips", + "pins", + "snip", + "spin" + ], + "dehissw": [ + "swedish", + "swished" + ], + "emops": [ + "epsom", + "mopes", + "poems", + "pomes" + ], + "boort": [ + "boort", + "robot" + ], + "einsstw": [ + "wisents", + "witness" + ], + "aegsst": [ + "sagest", + "stages" + ], + "deoprw": [ + "powder", + "prowed" + ], + "aessss": [ + "assess", + "sasses" + ], + "ahsw": [ + "haws", + "shaw", + "shwa", + "wash" + ], + "enosst": [ + "onsets", + "seston", + "setons", + "stenos", + "stones" + ], + "aceennrt": [ + "centenar", + "entrance" + ], + "egmno": [ + "emong", + "genom", + "gnome" + ], + "oorst": [ + "roost", + "roots", + "rotos", + "toros", + "torso" + ], + "aacdeilnort": [ + "declaration", + "redactional" + ], + "gilnos": [ + "logins", + "losing", + "soling" + ], + "adeggst": [ + "gadgets", + "stagged" + ], + "belno": [ + "nobel", + "noble" + ], + "erv": [ + "rev", + "ver" + ], + "eglops": [ + "gospel", + "spogel" + ], + "eloos": [ + "loose", + "oleos" + ], + "aims": [ + "aims", + "amis", + "mias", + "saim", + "siam", + "sima" + ], + "ceeiinprt": [ + "princeite", + "recipient" + ], + "giiklnn": [ + "inkling", + "kilning", + "linking" + ], + "adeenr": [ + "deaner", + "earned", + "endear", + "neared" + ], + "aciilms": [ + "islamic", + "laicism", + "silicam" + ], + "acehilstt": [ + "athletics", + "statelich" + ], + "aekprr": [ + "parker", + "repark" + ], + "copr": [ + "copr", + "corp", + "crop", + "porc", + "proc" + ], + "aops": [ + "asop", + "paso", + "sapo", + "soap" + ], + "eilprt": [ + "pretil", + "tripel", + "triple" + ], + "cdeersu": [ + "descure", + "recused", + "reduces", + "rescued", + "secured", + "seducer" + ], + "einortu": [ + "routine", + "tueiron" + ], + "aabcillsy": [ + "asyllabic", + "basically" + ], + "ckors": [ + "corks", + "rocks" + ], + "ainstt": [ + "astint", + "taints", + "tanist", + "titans" + ], + "ghostu": [ + "oughts", + "sought", + "toughs" + ], + "demnotu": [ + "demount", + "mounted" + ], + "aabhitt": [ + "habitat", + "tabitha" + ], + "adeimn": [ + "aidmen", + "daimen", + "damine", + "demain", + "dimane", + "maiden", + "median", + "medina" + ], + "gnsu": [ + "gnus", + "guns", + "snug", + "sung" + ], + "acennrs": [ + "canners", + "scanner" + ], + "eehinr": [ + "herein", + "inhere" + ], + "aadeimnt": [ + "aminated", + "animated", + "diamante", + "mandaite", + "mantidae" + ], + "ior": [ + "rio", + "roi" + ], + "ehor": [ + "hero", + "hoer", + "hore", + "rheo" + ], + "eeginrt": [ + "giterne", + "integer", + "retinge", + "treeing" + ], + "abcehlor": [ + "bachelor", + "crabhole", + "lochaber" + ], + "afgilln": [ + "falling", + "fingall" + ], + "aceprt": [ + "carpet", + "peract", + "preact" + ], + "eelnss": [ + "lenses", + "lessen" + ], + "abinry": [ + "binary", + "brainy" + ], + "addeentt": [ + "attended", + "dentated" + ], + "aciilnoot": [ + "coalition", + "coitional", + "lociation" + ], + "aelrtw": [ + "lawter", + "walter" + ], + "aegw": [ + "waeg", + "wage", + "wega" + ], + "aalst": [ + "atlas", + "salat", + "salta", + "talas" + ], + "adnw": [ + "dawn", + "wand" + ], + "eefls": [ + "feels", + "flees" + ], + "eorrttu": [ + "torture", + "trouter", + "tutorer" + ], + "aclr": [ + "carl", + "clar" + ], + "acot": [ + "coat", + "taco" + ], + "mrs": [ + "mrs", + "rms" + ], + "aceinnort": [ + "connarite", + "container", + "cotarnine", + "crenation", + "narcotine" + ], + "app": [ + "app", + "pap", + "ppa" + ], + "eioprrssuv": [ + "proviruses", + "supervisor" + ], + "coprs": [ + "corps", + "crops" + ], + "acorst": [ + "actors", + "arctos", + "castor", + "castro", + "costar", + "ostrca", + "scrota", + "tarocs" + ], + "eilrv": [ + "ervil", + "levir", + "liver", + "livre", + "rivel", + "viler" + ], + "abeill": [ + "alible", + "belial", + "labile", + "liable" + ], + "acellr": [ + "caller", + "cellar", + "recall" + ], + "ademssu": [ + "assumed", + "medusas" + ], + "adeeprrtu": [ + "apertured", + "departure" + ], + "beefil": [ + "befile", + "belief" + ], + "dehlorsu": [ + "shoulder", + "shoulerd" + ], + "cdeor": [ + "coder", + "cored", + "credo", + "decor" + ], + "kloopu": [ + "lookup", + "uplook" + ], + "ory": [ + "yor", + "ory", + "roy" + ], + "ino": [ + "ino", + "ion", + "oni" + ], + "adeeimrt": [ + "diameter", + "diatreme" + ], + "eefinr": [ + "enfire", + "ferine", + "fineer", + "infree", + "refine" + ], + "bddeir": [ + "bedrid", + "bidder", + "birded" + ], + "eginrs": [ + "reigns", + "renigs", + "resign", + "resing", + "sering", + "signer", + "singer" + ], + "aensv": [ + "avens", + "evans", + "naves", + "vanes" + ], + "adehlr": [ + "hareld", + "harled", + "herald" + ], + "afils": [ + "alifs", + "fails" + ], + "eikn": [ + "enki", + "kine", + "nike" + ], + "eeiinnnorttv": [ + "intervention", + "introvenient" + ], + "gilnpu": [ + "gulpin", + "puling" + ], + "aacinorttt": [ + "attraction", + "tractation" + ], + "acdfiiimnoot": [ + "domification", + "modification" + ], + "aceil": [ + "alice", + "celia", + "elaic", + "ileac" + ], + "aailnst": [ + "lanista", + "saliant", + "santali" + ], + "deer": [ + "deer", + "dere", + "dree", + "rede", + "reed" + ], + "ceim": [ + "emic", + "mice" + ], + "adilpry": [ + "pyralid", + "rapidly" + ], + "empt": [ + "empt", + "temp" + ], + "inort": [ + "intro", + "iortn", + "nitro", + "norit" + ], + "aacenrssu": [ + "anacruses", + "assurance" + ], + "astv": [ + "tavs", + "vast", + "vats" + ], + "eilnotu": [ + "elution", + "outline" + ], + "aejns": [ + "janes", + "jeans" + ], + "acefiiinortv": [ + "revification", + "verification" + ], + "ddo": [ + "dod", + "odd" + ], + "aprw": [ + "warp", + "wrap" + ], + "eefrrs": [ + "freers", + "freres", + "refers" + ], + "dmoo": [ + "doom", + "modo", + "mood" + ], + "agims": [ + "agism", + "sigma" + ], + "addemns": [ + "demands", + "maddens" + ], + "eegilnps": [ + "peelings", + "sleeping", + "speeling" + ], + "etx": [ + "ext", + "tex" + ], + "abem": [ + "ambe", + "beam", + "bema" + ], + "adegginnr": [ + "dangering", + "deranging", + "gandering", + "gardening" + ], + "aeirrv": [ + "arrive", + "varier" + ], + "acehorrst": [ + "carthorse", + "horsecart", + "orchestra" + ], + "ensstu": [ + "sunset", + "unsets" + ], + "eemoorrv": [ + "moreover", + "overmore" + ], + "adefmr": [ + "farmed", + "framed" + ], + "aacillnoot": [ + "allocation", + "locational" + ], + "aessy": [ + "eyass", + "essay" + ], + "acmps": [ + "camps", + "scamp" + ], + "acert": [ + "caret", + "carte", + "cater", + "cerat", + "crate", + "creat", + "creta", + "ecart", + "react", + "recta", + "trace" + ], + "ackps": [ + "packs", + "spack" + ], + "aaimnor": [ + "amanori", + "moarian" + ], + "hotu": [ + "hout", + "thou" + ], + "aeglrty": [ + "greatly", + "regalty" + ], + "akms": [ + "kasm", + "mask" + ], + "aeghhoopprrt": [ + "photographer", + "rephotograph" + ], + "fimnor": [ + "formin", + "inform" + ], + "aclo": [ + "alco", + "coal", + "cola", + "loca" + ], + "aeggimnss": [ + "gigmaness", + "messaging" + ], + "einntt": [ + "intent", + "nitent", + "tinnet" + ], + "aaelnpst": [ + "platanes", + "pleasant" + ], + "ekops": [ + "pokes", + "spoke" + ], + "agilmnps": [ + "psalming", + "sampling" + ], + "deirw": [ + "weird", + "wider", + "wierd", + "wired", + "wride", + "wried" + ], + "ilno": [ + "lino", + "lion", + "loin", + "noil" + ], + "ehlos": [ + "holes", + "hosel", + "sheol", + "shole" + ], + "abdel": [ + "baled", + "blade" + ], + "aelms": [ + "almes", + "amsel", + "lames", + "males", + "meals", + "melas", + "mesal", + "salem", + "samel" + ], + "acnnoy": [ + "ancony", + "canyon" + ], + "goot": [ + "goto", + "togo" + ], + "eemrst": [ + "merest", + "mester", + "meters", + "metres", + "restem", + "retems", + "temser", + "termes" + ], + "eelmry": [ + "yelmer", + "merely" + ], + "eimprst": [ + "imprest", + "permits" + ], + "ilmmsu": [ + "mulism", + "muslim" + ], + "eeelsv": [ + "levees", + "sleeve" + ], + "aceelnr": [ + "cleaner", + "enclear", + "reclean", + "relance" + ], + "beef": [ + "beef", + "feeb" + ], + "deefgin": [ + "feeding", + "feigned" + ], + "ekorst": [ + "stoker", + "stroke", + "trokes" + ], + "aegimnrsu": [ + "geraniums", + "measuring" + ], + "acd": [ + "adc", + "cad", + "dca" + ], + "ahst": [ + "hast", + "hats", + "shat", + "tash" + ], + "binor": [ + "biron", + "inorb", + "robin" + ], + "ahnors": [ + "rhason", + "sharon", + "shoran" + ], + "cpt": [ + "cpt", + "pct" + ], + "frsu": [ + "furs", + "surf" + ], + "adeeimnr": [ + "adermine", + "remained" + ], + "dir": [ + "dir", + "rid" + ], + "aeiilrs": [ + "alisier", + "israeli", + "resilia" + ], + "cdor": [ + "cord", + "dcor" + ], + "alv": [ + "lav", + "val" + ], + "efhls": [ + "flesh", + "shelf" + ], + "giimnt": [ + "miting", + "timing" + ], + "aeginnprt": [ + "enrapting", + "parenting" + ], + "foost": [ + "foots", + "sfoot", + "stoof" + ], + "adehst": [ + "deaths", + "hasted", + "sdeath" + ], + "aaiknrt": [ + "katrina", + "tarkani" + ], + "ailms": [ + "islam", + "ismal", + "limas", + "mails", + "salmi", + "simal" + ], + "denos": [ + "doesn", + "nodes", + "nosed", + "sonde" + ], + "deess": [ + "deess", + "essed", + "seeds" + ], + "cdeit": [ + "cetid", + "cited", + "edict" + ], + "eilt": [ + "itel", + "lite", + "teil", + "teli", + "tile" + ], + "defnoru": [ + "founder", + "refound", + "underfo" + ], + "adeersv": [ + "adverse", + "evaders" + ], + "egn": [ + "eng", + "gen", + "neg" + ], + "acdeghirs": [ + "discharge", + "scraighed" + ], + "nost": [ + "nots", + "snot", + "tons" + ], + "aclor": [ + "alcor", + "calor", + "carlo", + "carol", + "claro", + "coral" + ], + "ehnost": [ + "ethnos", + "honest" + ], + "ackst": [ + "stack", + "tacks" + ], + "aeehorssuw": [ + "housewares", + "warehouses" + ], + "aeinrsstt": [ + "resistant", + "straitens" + ], + "aghn": [ + "ghan", + "hang" + ], + "aceorrt": [ + "acroter", + "creator", + "reactor" + ], + "abemr": [ + "amber", + "bearm", + "bemar", + "brame", + "bream", + "embar" + ], + "acekrrt": [ + "retrack", + "tracker" + ], + "eeiprr": [ + "perrie", + "pierre" + ], + "adeehst": [ + "headset", + "sethead" + ], + "acelm": [ + "camel", + "clame", + "cleam", + "macle" + ], + "almps": [ + "lamps", + "palms", + "plasm", + "psalm", + "slamp" + ], + "degilnnruy": [ + "enduringly", + "underlying" + ], + "chi": [ + "chi", + "hic", + "ich" + ], + "acehs": [ + "aches", + "chase" + ], + "ceorstuy": [ + "cosurety", + "courtesy" + ], + "aehnst": [ + "athens", + "hasten", + "snathe", + "sneath", + "thanes" + ], + "eos": [ + "eos", + "oes", + "ose", + "soe" + ], + "deeirrt": [ + "retired", + "retried", + "tireder" + ], + "ips": [ + "ips", + "pis", + "psi", + "sip" + ], + "aekmrrs": [ + "markers", + "remarks" + ], + "aceesstt": [ + "casettes", + "cassette" + ], + "acginsu": [ + "causing", + "saucing" + ], + "aeilmnr": [ + "lairmen", + "manlier", + "marline", + "mineral", + "railmen", + "ramline" + ], + "bruy": [ + "bury", + "ruby" + ], + "eeginrst": [ + "energist", + "gentries", + "ingester", + "integers", + "reesting", + "steering" + ], + "ahiprs": [ + "parish", + "raphis", + "rhapis" + ], + "ceenrss": [ + "censers", + "screens", + "secerns" + ], + "flosw": [ + "flows", + "fowls", + "wolfs" + ], + "adimstu": [ + "dumaist", + "stadium" + ], + "imns": [ + "mins", + "nims" + ], + "cnoopu": [ + "coupon", + "uncoop" + ], + "emst": [ + "mest", + "mets", + "stem" + ], + "addersw": [ + "edwards", + "swadder", + "swarded", + "wadders" + ], + "aaelnrstt": [ + "alterants", + "tarletans", + "translate" + ], + "adeggt": [ + "gadget", + "tagged" + ], + "deotv": [ + "devot", + "voted" + ], + "eikllr": [ + "killer", + "rekill" + ], + "beiks": [ + "bikes", + "kibes" + ], + "entu": [ + "neut", + "tune" + ], + "adehps": [ + "hasped", + "pashed", + "phased", + "shaped" + ], + "aefmrr": [ + "farmer", + "framer" + ], + "cenorstu": [ + "construe", + "counters", + "recounts", + "trounces" + ], + "pstu": [ + "puts", + "sput", + "supt", + "tups" + ], + "ceeflprty": [ + "perfectly", + "prefectly" + ], + "aelsv": [ + "laves", + "salve", + "selva", + "slave", + "vales", + "valse", + "veals" + ], + "emo": [ + "eom", + "meo", + "moe" + ], + "eehors": [ + "heroes", + "reshoe" + ], + "adeinpt": [ + "depaint", + "inadept", + "painted", + "patined" + ], + "ahilnoortz": [ + "horizontal", + "notorhizal" + ], + "deelrstu": [ + "deluster", + "lustered", + "resulted", + "ulstered" + ], + "acehilt": [ + "alethic", + "ethical", + "thecial" + ], + "aceirrrs": [ + "carriers", + "scarrier" + ], + "bdeilrsu": [ + "builders", + "rebuilds" + ], + "egglrstu": [ + "gurglets", + "struggle" + ], + "aelnrtu": [ + "laurent", + "naturel", + "neutral", + "unalert" + ], + "efhirs": [ + "fisher", + "sherif" + ], + "aeprss": [ + "aspers", + "parses", + "passer", + "prases", + "repass", + "spares", + "sparse", + "spears" + ], + "abeginr": [ + "bearing", + "begrain", + "brainge", + "gribane", + "rigbane" + ], + "abdr": [ + "bard", + "brad", + "darb", + "drab" + ], + "bcmoo": [ + "combo", + "coomb" + ], + "einorss": [ + "seniors", + "sonsier" + ], + "aaciinottv": [ + "activation", + "cavitation" + ], + "dos": [ + "dos", + "ods", + "sod" + ], + "ailt": [ + "alit", + "atli", + "ital", + "lait", + "lati", + "tail", + "tali" + ], + "eilnotv": [ + "violent", + "volenti" + ], + "abins": [ + "basin", + "nabis", + "sabin" + ], + "opsu": [ + "opus", + "soup" + ], + "cginorss": [ + "crossing", + "scorings" + ], + "ceimrs": [ + "crimes", + "scrime" + ], + "enort": [ + "noter", + "notre", + "tenor", + "toner", + "trone" + ], + "aeltx": [ + "exalt", + "latex" + ], + "abcehnrs": [ + "branches", + "brechans" + ], + "aemnory": [ + "anymore", + "romneya" + ], + "dehil": [ + "delhi", + "heild", + "hidel", + "hield" + ], + "aeiln": [ + "alien", + "aline", + "anile", + "elain", + "elian", + "laine", + "liane", + "linea" + ], + "acloort": [ + "carotol", + "crotalo", + "locator" + ], + "ajnu": [ + "jaun", + "juan" + ], + "aeilmss": [ + "aimless", + "melissa", + "samiels", + "seismal" + ], + "ehisst": [ + "heists", + "shiest", + "sithes", + "thesis" + ], + "lnnoy": [ + "nylon", + "nonyl", + "nonly" + ], + "ckory": [ + "corky", + "rocky" + ], + "aabginrs": [ + "abrasing", + "bargains" + ], + "aegiinr": [ + "igraine", + "nigeria" + ], + "enps": [ + "pens", + "sepn" + ], + "deilnttu": [ + "dilutent", + "untilted", + "untitled" + ], + "ggiinns": [ + "signing", + "singing" + ], + "ailmopt": [ + "optimal", + "palmito" + ], + "cloooprst": [ + "procotols", + "protocols" + ], + "glnu": [ + "gunl", + "lung" + ], + "censt": [ + "cents", + "csnet", + "scent" + ], + "eerrstu": [ + "retruse", + "ureters" + ], + "aadeeltuv": [ + "devaluate", + "evaluated" + ], + "adesty": [ + "stayed", + "steady" + ], + "eess": [ + "eses", + "esse", + "sees" + ], + "aersv": [ + "avers", + "raves", + "saver", + "versa" + ], + "deeemr": [ + "deemer", + "meered", + "redeem", + "remede" + ], + "egorrs": [ + "groser", + "rogers" + ], + "aginr": [ + "agrin", + "argin", + "garni", + "grain" + ], + "eegimr": [ + "emigre", + "regime" + ], + "ehissw": [ + "wishes", + "wisshe" + ], + "ddeenp": [ + "depend", + "pended" + ], + "deffir": [ + "differ", + "riffed" + ], + "achimnost": [ + "macintosh", + "monachist" + ], + "acimno": [ + "anomic", + "camino", + "camion", + "conima", + "manioc", + "monica" + ], + "aeiprrs": [ + "aspirer", + "parries", + "praiser", + "rapiers", + "raspier", + "repairs", + "serpari" + ], + "abehrt": [ + "bather", + "bertha", + "breath" + ], + "celo": [ + "cole", + "ecol" + ], + "amrt": [ + "mart", + "tram" + ], + "acdeln": [ + "calden", + "candle", + "lanced" + ], + "cdeloor": [ + "colored", + "croodle", + "decolor" + ], + "eekss": [ + "kesse", + "seeks", + "skees" + ], + "gilnov": [ + "loving", + "voling" + ], + "ginortu": [ + "outgrin", + "outring", + "routing", + "touring" + ], + "cdos": [ + "cods", + "docs" + ], + "aeeilrrt": [ + "irrelate", + "retailer" + ], + "aiimnstv": [ + "nativism", + "vitamins" + ], + "aeeglnt": [ + "angelet", + "elegant" + ], + "agins": [ + "gains", + "signa" + ], + "eirssst": [ + "resists", + "sisters" + ], + "aghilmorst": [ + "algorithms", + "logarithms" + ], + "addimr": [ + "madrid", + "riddam" + ], + "agimnr": [ + "arming", + "ingram", + "margin" + ], + "aefk": [ + "fake", + "feak" + ], + "adf": [ + "afd", + "fad" + ], + "eorstv": [ + "stover", + "strove", + "troves", + "voters" + ], + "ceru": [ + "cure", + "ecru", + "eruc" + ], + "acdemmnor": [ + "commander", + "recommand" + ], + "deilors": [ + "soldier", + "solider" + ], + "ains": [ + "ains", + "anis", + "ansi", + "nais", + "nasi", + "nias", + "sain", + "sina" + ], + "ijnstu": [ + "injust", + "justin" + ], + "ghilopstt": [ + "spotlight", + "stoplight" + ], + "cikrst": [ + "strick", + "tricks" + ], + "bhrsu": [ + "brush", + "buhrs", + "shrub" + ], + "aelnps": [ + "naples", + "panels", + "planes" + ], + "aeprs": [ + "apers", + "apres", + "asper", + "pares", + "parse", + "pears", + "prase", + "presa", + "rapes", + "reaps", + "repas", + "spaer", + "spare", + "spear" + ], + "cgiilosst": [ + "glossitic", + "logistics" + ], + "bgilnow": [ + "blowing", + "bowling" + ], + "irt": [ + "rit", + "rti", + "tri" + ], + "adhins": [ + "danish", + "sandhi" + ], + "alp": [ + "alp", + "apl", + "lap", + "pal" + ], + "ikrst": [ + "skirt", + "stirk" + ], + "adginrsw": [ + "drawings", + "swarding" + ], + "elorsv": [ + "lovers", + "solver" + ], + "acimot": [ + "atomic", + "matico" + ], + "aabcir": [ + "arabic", + "cairba" + ], + "amt": [ + "amt", + "atm", + "mat", + "tam" + ], + "acehlr": [ + "rachel", + "rechal" + ], + "enov": [ + "nevo", + "oven" + ], + "achins": [ + "chains", + "chinas" + ], + "cghiinstw": [ + "switching", + "witchings" + ], + "aadeprst": [ + "adapters", + "readapts" + ], + "imoprst": [ + "imports", + "primost", + "tropism" + ], + "benorz": [ + "bonzer", + "bronze" + ], + "adnsy": [ + "dansy", + "sandy" + ], + "aaeinoprst": [ + "anisoptera", + "asperation", + "separation" + ], + "cepsstu": [ + "suscept", + "suspect" + ], + "acmor": [ + "carom", + "coram", + "macro", + "marco" + ], + "deenrs": [ + "denser", + "enders", + "resend", + "sender" + ], + "aadmnorty": [ + "damnatory", + "mandatory" + ], + "gmy": [ + "gym", + "myg" + ], + "iinottu": [ + "intuito", + "tuition" + ], + "eopssu": [ + "esopus", + "opuses", + "pousse", + "spouse" + ], + "ceiotx": [ + "coxite", + "exotic" + ], + "ginpsu": [ + "pignus", + "spuing" + ], + "aehrstt": [ + "hatters", + "rathest", + "shatter", + "threats" + ], + "acms": [ + "cams", + "macs", + "masc", + "scam" + ], + "ejlo": [ + "joel", + "jole" + ], + "deorsty": [ + "destroy", + "stroyed" + ], + "aostu": [ + "aotus", + "autos", + "outas" + ], + "eeimprss": [ + "emprises", + "impreses", + "mesprise", + "premises", + "spiremes" + ], + "eprry": [ + "perry", + "pryer", + "repry" + ], + "denoz": [ + "dozen", + "zendo", + "zoned" + ], + "eehtt": [ + "teeth", + "theet", + "thete" + ], + "ampst": [ + "stamp", + "tamps" + ], + "lostu": [ + "lotus", + "louts", + "tolus" + ], + "aadeeprst": [ + "asperated", + "estrapade", + "paederast", + "separated" + ], + "ant": [ + "ant", + "nat", + "tan" + ], + "cdeeiirtv": [ + "creditive", + "directive" + ], + "aerrstt": [ + "ratters", + "restart", + "starter" + ], + "bdenru": [ + "bunder", + "burden", + "burned", + "unbred" + ], + "aepst": [ + "paste", + "pates", + "peats", + "septa", + "spate", + "tapes", + "tepas" + ], + "ilms": [ + "mils", + "slim" + ], + "aelmp": [ + "ample", + "elamp", + "maple" + ], + "eklu": [ + "leuk", + "luke" + ], + "aeeilrrst": [ + "retailers", + "serratile" + ], + "deopt": [ + "depot", + "opted", + "toped" + ], + "eip": [ + "epi", + "pie" + ], + "eiimnoss": [ + "emission", + "misiones", + "simonies" + ], + "eept": [ + "pete", + "tepe" + ], + "ceps": [ + "ceps", + "psec", + "spec" + ], + "efinst": [ + "feints", + "festin", + "finest", + "infest" + ], + "aelrty": [ + "elytra", + "lyrate", + "raylet", + "realty", + "telary" + ], + "bow": [ + "bow", + "wob" + ], + "aaenpprt": [ + "apparent", + "trappean" + ], + "aciilnnorsttu": [ + "instructional", + "nonaltruistic" + ], + "beopr": [ + "probe", + "rebop" + ], + "diim": [ + "imid", + "midi" + ], + "eiimnoprsss": [ + "impressions", + "permissions" + ], + "eilott": [ + "lottie", + "toilet", + "tolite" + ], + "adeknr": [ + "danker", + "darken", + "endark", + "kanred", + "narked", + "ranked" + ], + "eorstu": [ + "ouster", + "outers", + "routes", + "souter", + "stoure", + "touser", + "trouse" + ], + "ceeorrv": [ + "coverer", + "recover" + ], + "aceehinrt": [ + "catherine", + "heritance" + ], + "bdegu": [ + "budge", + "debug" + ], + "cddeeoprru": [ + "procedured", + "reproduced" + ], + "hno": [ + "hon", + "noh" + ], + "aillsv": [ + "vallis", + "villas" + ], + "eeginp": [ + "epigne", + "genepi", + "peeing" + ], + "bgino": [ + "bingo", + "boing" + ], + "notu": [ + "tuno", + "unto" + ], + "acceimr": [ + "ceramic", + "racemic" + ], + "apsy": [ + "aspy", + "yaps", + "pays", + "pyas", + "spay" + ], + "efginrs": [ + "fingers", + "fringes" + ], + "deeilrsv": [ + "delivers", + "desilver", + "silvered", + "slivered" + ], + "deels": [ + "deles", + "leeds", + "lesed" + ], + "acder": [ + "acred", + "arced", + "cader", + "cadre", + "cared", + "cedar", + "cread", + "creda", + "raced" + ], + "aeehrstt": [ + "earthset", + "streahte", + "theaters", + "theatres" + ], + "dflo": [ + "dolf", + "fold" + ], + "abilr": [ + "blair", + "brail", + "libra" + ], + "ehops": [ + "hopes", + "phose", + "shope" + ], + "amnos": [ + "manos", + "manso", + "mason", + "moans", + "monas", + "mosan", + "nomas" + ], + "civ": [ + "civ", + "vic" + ], + "aimor": [ + "maori", + "mario", + "moira" + ], + "ops": [ + "ops", + "pos", + "sop" + ], + "aachtt": [ + "attach", + "chatta" + ], + "ceeilnss": [ + "licenses", + "silences" + ], + "ilstu": [ + "litus", + "sluit", + "tulsi" + ], + "deiprs": [ + "prides", + "prised", + "redips", + "spider", + "spired", + "spried" + ], + "hpsy": [ + "hyps", + "phys", + "syph" + ], + "aegnrs": [ + "angers", + "ganser", + "granes", + "ranges", + "sanger", + "serang" + ], + "dhnosu": [ + "hounds", + "hudson", + "unshod" + ], + "adeilost": [ + "diastole", + "isolated", + "sodalite", + "solidate" + ], + "eiimnrt": [ + "interim", + "mintier", + "termini" + ], + "adeissst": [ + "assisted", + "disseats" + ], + "aegimnrst": [ + "emigrants", + "germanist", + "mastering", + "streaming" + ], + "cehos": [ + "choes", + "chose", + "echos" + ], + "acdeinsty": [ + "asyndetic", + "cystidean", + "syndicate" + ], + "abinoort": [ + "abortion", + "orbation", + "robotian" + ], + "adgilo": [ + "algoid", + "dialog", + "goliad" + ], + "ablst": [ + "blast", + "blats" + ], + "elop": [ + "lope", + "olpe", + "pole" + ], + "cddeinostu": [ + "deductions", + "discounted" + ], + "aehrstv": [ + "harvest", + "thraves" + ], + "ehmorst": [ + "mothers", + "smother", + "thermos" + ], + "acdeiln": [ + "candiel", + "cladine", + "decalin", + "iceland", + "inlaced" + ], + "acdelns": [ + "calends", + "candles" + ], + "aadginortu": [ + "argonautid", + "graduation" + ], + "agiilns": [ + "aisling", + "liasing", + "nilgais", + "sailing" + ], + "acders": [ + "cadres", + "cedars", + "sacred", + "scared" + ], + "cehmor": [ + "chomer", + "chrome" + ], + "eilorv": [ + "lovier", + "oliver", + "violer", + "virole" + ], + "cgnoo": [ + "cogon", + "congo" + ], + "egln": [ + "engl", + "genl", + "glen", + "leng" + ], + "adelsy": [ + "delays", + "slayed" + ], + "eilov": [ + "olive", + "ovile", + "voile" + ], + "bcery": [ + "becry", + "bryce" + ], + "cdeors": [ + "coders", + "credos", + "decors", + "escrod", + "scored" + ], + "celno": [ + "clone", + "colen" + ], + "aioss": [ + "oasis", + "ossia", + "sosia" + ], + "abeeilns": [ + "balinese", + "baseline" + ], + "agnry": [ + "angry", + "rangy" + ], + "acdeiilnt": [ + "ctenidial", + "identical" + ], + "abeelst": [ + "beatles", + "besteal", + "estable" + ], + "eeinnortt": [ + "intertone", + "retention" + ], + "aalmt": [ + "malta", + "talma", + "tamal" + ], + "efrry": [ + "ferry", + "freyr", + "fryer", + "refry" + ], + "aeginst": [ + "easting", + "eatings", + "gainset", + "genista", + "ingates", + "ingesta", + "seating", + "signate", + "teasing" + ], + "aahmo": [ + "haoma", + "omaha" + ], + "eirt": [ + "iter", + "reit", + "rite", + "teri", + "tier", + "tire" + ], + "elmot": [ + "metol", + "molet", + "motel" + ], + "innosu": [ + "nonius", + "unions", + "unison" + ], + "ainrst": [ + "instar", + "santir", + "strain", + "trains" + ], + "adgr": [ + "darg", + "drag", + "gard", + "grad" + ], + "ehmoosw": [ + "somehow", + "whosome" + ], + "adeerrst": [ + "arrested", + "retreads", + "serrated", + "treaders" + ], + "eipr": [ + "irpe", + "peri", + "pier", + "prie", + "ripe" + ], + "elry": [ + "lyre", + "rely" + ], + "acdeiimnost": [ + "daemonistic", + "medications" + ], + "ceehorrst": [ + "orchester", + "orchestre", + "rochester", + "torcheres" + ], + "dginy": [ + "dying", + "dingy" + ], + "ckstu": [ + "stuck", + "tucks" + ], + "acgilnp": [ + "capling", + "placing" + ], + "cefossu": [ + "focuses", + "fucoses" + ], + "eiostv": [ + "soviet", + "sovite" + ], + "aertty": [ + "attery", + "yatter", + "treaty" + ], + "aeinrrt": [ + "arterin", + "retrain", + "terrain", + "trainer" + ], + "agnor": [ + "agron", + "angor", + "argon", + "garon", + "goran", + "grano", + "groan", + "nagor", + "orang", + "organ", + "rogan", + "ronga" + ], + "aacdensv": [ + "advances", + "canvased" + ], + "elmno": [ + "lemon", + "melon", + "monel" + ], + "pty": [ + "pty", + "typ" + ], + "nstu": [ + "nuts", + "stun", + "sunt", + "tsun", + "tuns" + ], + "ailn": [ + "alin", + "anil", + "lain", + "lina", + "nail" + ], + "aeinnv": [ + "avenin", + "vienna" + ], + "anps": [ + "naps", + "pans", + "snap", + "span" + ], + "adfnorst": [ + "forstand", + "stanford" + ], + "aestttu": [ + "statute", + "tautest" + ], + "acehlp": [ + "chapel", + "lepcha", + "pleach" + ], + "aelrsy": [ + "layers", + "relays", + "reslay", + "slayer" + ], + "abceeelrt": [ + "celebrate", + "erectable" + ], + "cdeemoprss": [ + "compressed", + "decompress" + ], + "deirr": [ + "derri", + "direr", + "drier", + "irred", + "rider" + ], + "adirsu": [ + "darius", + "radius" + ], + "achiinrsst": [ + "christians", + "sinarchist" + ], + "eeiimprssv": [ + "impressive", + "permissive" + ], + "ceelrstu": [ + "cruelest", + "lectures" + ], + "einsw": [ + "sewin", + "sinew", + "swine", + "wines", + "wisen" + ], + "cpsu": [ + "cpus", + "cups", + "cusp", + "scup" + ], + "accdeinst": [ + "accidents", + "desiccant" + ], + "aceilnoort": [ + "corelation", + "iconolater", + "relocation" + ], + "arsttu": [ + "astrut", + "rattus", + "stuart" + ], + "ail": [ + "ail", + "ila", + "lai" + ], + "emnoor": [ + "monroe", + "mooner", + "morone" + ], + "deenrt": [ + "denter", + "rented", + "tender", + "tendre", + "terned" + ], + "eeeprrsv": [ + "perverse", + "preserve" + ], + "emop": [ + "mope", + "poem", + "pome" + ], + "deginnsu": [ + "undesign", + "unsigned", + "unsinged" + ], + "aginsty": [ + "staying", + "stygian" + ], + "aeerst": [ + "aretes", + "asteer", + "easter", + "eastre", + "eaters", + "reseat", + "saeter", + "seater", + "staree", + "teaser", + "teresa" + ], + "eehiorst": [ + "isothere", + "theories", + "theorise" + ], + "aeiprs": [ + "aspire", + "paries", + "persia", + "praise", + "sirpea", + "spirae", + "spirea" + ], + "ceeinv": [ + "cevine", + "evince", + "venice" + ], + "aeinost": [ + "aeonist", + "asiento", + "atonies", + "estonia", + "satieno" + ], + "aeenrtv": [ + "aventre", + "nervate", + "veteran" + ], + "adgilnn": [ + "danglin", + "landing" + ], + "aeikt": [ + "katie", + "keita" + ], + "aceiilrst": [ + "clarities", + "eristical", + "realistic" + ], + "aelrx": [ + "laxer", + "relax" + ], + "aeegginnrt": [ + "generating", + "greatening", + "renegating" + ], + "aben": [ + "bane", + "bean", + "bena" + ], + "abenst": [ + "absent", + "basnet", + "basten", + "besant" + ], + "acdeoru": [ + "acuerdo", + "cordeau", + "ecuador" + ], + "iiprsst": [ + "pristis", + "spirits", + "tripsis" + ], + "aflot": [ + "aloft", + "float", + "flota" + ], + "cilno": [ + "colin", + "conli", + "nicol" + ], + "abis": [ + "absi", + "bais", + "bias", + "isba" + ], + "ahpst": [ + "paths", + "spath", + "staph" + ], + "beinrtu": [ + "tribune", + "tuberin", + "turbine" + ], + "eelssv": [ + "selves", + "vessel" + ], + "acdis": [ + "acids", + "asdic", + "cadis", + "caids", + "sadic" + ], + "eirssuv": [ + "servius", + "survise", + "viruses" + ], + "aceehpr": [ + "cheaper", + "peacher" + ], + "adimt": [ + "admit", + "atmid" + ], + "emm": [ + "emm", + "mem" + ], + "aamos": [ + "omasa", + "samoa" + ], + "aceinorst": [ + "atroscine", + "certosina", + "creations", + "narcotise", + "ostracine", + "reactions", + "secration", + "tinoceras", + "tricosane" + ], + "aegilns": [ + "inglesa", + "leasing", + "linages", + "sealing" + ], + "aelnru": [ + "alrune", + "lunare", + "neural", + "ulnare", + "unreal" + ], + "adqsu": [ + "quads", + "squad" + ], + "aeelrt": [ + "earlet", + "elater", + "relate" + ], + "aegsw": [ + "swage", + "wages" + ], + "effrsu": [ + "ruffes", + "suffer" + ], + "eforsst": [ + "forests", + "fosters" + ], + "anno": [ + "anno", + "anon", + "nona", + "onan" + ], + "aailmrt": [ + "marital", + "martial" + ], + "aeinrt": [ + "anteri", + "entria", + "nerita", + "ratine", + "retain", + "retina", + "tanier" + ], + "elnntu": [ + "nunlet", + "tunnel", + "unlent" + ], + "eegnrs": [ + "genres", + "greens" + ], + "aenpstt": [ + "patents", + "pattens" + ], + "achos": [ + "chaos", + "oshac" + ], + "aehtw": [ + "awhet", + "wheat" + ], + "adeginrs": [ + "deraigns", + "disrange", + "gradines", + "readings" + ], + "ceilmopr": [ + "compiler", + "complier" + ], + "abess": [ + "bases", + "sabes" + ], + "accdesu": [ + "accused", + "succade" + ], + "dlou": [ + "loud", + "ludo" + ], + "bdeir": [ + "bider", + "birde", + "bredi", + "bride", + "rebid" + ], + "aceinnsst": [ + "anticness", + "cantiness", + "incessant", + "instances" + ], + "achnor": [ + "anchor", + "archon", + "charon", + "rancho" + ], + "astt": [ + "stat", + "tats" + ], + "eessx": [ + "essex", + "sexes" + ], + "abceehs": [ + "beaches", + "bechase" + ], + "deflors": [ + "folders", + "refolds" + ], + "eorrstu": [ + "rouster", + "routers", + "tourers", + "trouser" + ], + "bios": [ + "bios", + "bois", + "obis" + ], + "cmp": [ + "cpm", + "pcm" + ], + "eeht": [ + "hete", + "thee" + ], + "eopp": [ + "epop", + "pepo", + "pope" + ], + "adlnor": [ + "androl", + "arnold", + "ladron", + "lardon", + "lordan", + "roland", + "ronald" + ], + "cchiks": [ + "chicks", + "schick" + ], + "aceltt": [ + "cattle", + "tectal" + ], + "abm": [ + "amb", + "bam", + "mab" + ], + "aacdilr": [ + "cardial", + "radical" + ], + "aeerrstu": [ + "austerer", + "treasure" + ], + "adelor": [ + "loader", + "ordeal", + "reload" + ], + "aeflm": [ + "flame", + "fleam" + ], + "aknst": [ + "stank", + "tanks" + ], + "aemnorty": [ + "myronate", + "monetary", + "naometry" + ], + "aceilprst": [ + "palestric", + "particles" + ], + "acdeinoort": [ + "aerodontic", + "carotenoid", + "coordinate", + "coronadite", + "decoration" + ], + "aiktuw": [ + "kuwait", + "waukit" + ], + "eilmy": [ + "elymi", + "emily", + "limey" + ], + "aiiilmnott": [ + "limitation", + "militation" + ], + "ceilmop": [ + "compile", + "polemic" + ], + "beerstw": [ + "bestrew", + "webster" + ], + "abdilr": [ + "bildar", + "bridal", + "labrid", + "libard", + "ribald" + ], + "agm": [ + "gam", + "mag" + ], + "efghirt": [ + "fighter", + "freight", + "refight" + ], + "abeert": [ + "beater", + "berate", + "betear", + "rebate", + "rebeat" + ], + "adnsu": [ + "sudan", + "unsad" + ], + "cer": [ + "cre", + "rec" + ], + "aceforsst": [ + "factoress", + "forecasts" + ], + "eekn": [ + "keen", + "knee" + ], + "eppr": [ + "perp", + "prep", + "repp" + ], + "cehm": [ + "chem", + "mech" + ], + "aefsstt": [ + "fastest", + "setfast" + ], + "belrtu": [ + "bulter", + "burlet", + "butler", + "turble" + ], + "acdeginort": [ + "centigrado", + "decorating" + ], + "allopry": [ + "payroll", + "polarly" + ], + "hinst": [ + "hints", + "thins", + "thisn" + ], + "acellops": [ + "collapse", + "escallop" + ], + "aaceimrs": [ + "americas", + "cramasie", + "mesaraic" + ], + "oprs": [ + "pros", + "spor" + ], + "aailtv": [ + "avital", + "latvia" + ], + "aelrry": [ + "rarely", + "rearly" + ], + "aahmrt": [ + "amarth", + "martha", + "matrah" + ], + "eeginss": [ + "genesis", + "seeings" + ], + "aegru": [ + "argue", + "auger", + "gaure", + "rugae" + ], + "aelmst": [ + "lamest", + "metals", + "samlet" + ], + "egilntt": [ + "ettling", + "letting" + ], + "cir": [ + "cir", + "ric" + ], + "aeijm": [ + "jaime", + "jamie" + ], + "aceilprt": [ + "particle", + "plicater", + "prelatic" + ], + "ceeinopprt": [ + "perception", + "preception" + ], + "aeilmnrs": [ + "marlines", + "minerals", + "mislearn" + ], + "adeisv": [ + "advise", + "davies", + "visaed" + ], + "belostt": [ + "bottles", + "setbolt" + ], + "aaceeinnrss": [ + "necessarian", + "renaissance" + ], + "aars": [ + "rasa", + "sara" + ], + "acdeinnor": [ + "cerdonian", + "encardion", + "ordinance" + ], + "eghhsu": [ + "heughs", + "hughes", + "sheugh" + ], + "eeffjry": [ + "jeffery", + "jeffrey" + ], + "aeeoprst": [ + "asterope", + "operates", + "protease" + ], + "acors": [ + "arcos", + "crosa", + "orcas", + "oscar", + "sacro" + ], + "emnsu": [ + "menus", + "neums", + "sumen" + ], + "aeelrv": [ + "laveer", + "leaver", + "reveal", + "vealer" + ], + "aimno": [ + "amino", + "animo", + "inoma", + "naomi", + "omani", + "omina" + ], + "cciilns": [ + "cinclis", + "clinics" + ], + "lms": [ + "msl", + "sml" + ], + "cow": [ + "cow", + "cwo" + ], + "gilny": [ + "lying", + "lingy" + ], + "deiv": [ + "devi", + "dive", + "vide", + "vied" + ], + "abnry": [ + "barny", + "bryan" + ], + "eoprstt": [ + "potters", + "protest", + "spotter" + ], + "eirst": [ + "iters", + "reist", + "resit", + "rites", + "steri", + "stire", + "tiers", + "tires", + "tries" + ], + "aghinsw": [ + "hawsing", + "shawing", + "washing" + ], + "celorsu": [ + "closure", + "colures" + ], + "adir": [ + "arid", + "dari", + "raid" + ], + "beimrt": [ + "betrim", + "timber", + "timbre" + ], + "eeinnst": [ + "ensient", + "intense", + "sennite", + "sentine" + ], + "ehorssw": [ + "reshows", + "showers" + ], + "gilnru": [ + "luring", + "ruling", + "urling" + ], + "dirt": [ + "dirt", + "trid" + ], + "doprs": [ + "dorps", + "drops", + "prods", + "sprod" + ], + "gilnpsu": [ + "pulings", + "pulsing" + ], + "deellnor": [ + "enrolled", + "rondelle" + ], + "cersw": [ + "crews", + "screw" + ], + "eiimnrsst": [ + "ministers", + "misinters" + ], + "abelm": [ + "amble", + "belam", + "blame", + "mabel", + "melba" + ], + "aeegnv": [ + "avenge", + "geneva", + "vangee" + ], + "dist": [ + "dist", + "dits", + "stid" + ], + "addehn": [ + "hadden", + "handed" + ], + "aeiknt": [ + "intake", + "kentia", + "tankie" + ], + "afilmnor": [ + "formalin", + "formnail", + "informal", + "laniform" + ], + "accehimns": [ + "mechanics", + "mischance" + ], + "ffity": [ + "fifty", + "tiffy" + ], + "adeehrs": [ + "adheres", + "headers", + "hearsed", + "sheared" + ], + "aceilmnru": [ + "ceruminal", + "melanuric", + "numerical" + ], + "aerssu": [ + "assure", + "urases" + ], + "dimosu": [ + "modius", + "odiums", + "sodium" + ], + "ehmnoor": [ + "hormone", + "moorhen" + ], + "gipr": [ + "grip", + "prig" + ], + "aalnv": [ + "alvan", + "naval" + ], + "aceilnopr": [ + "oliprance", + "porcelain" + ], + "bdegirs": [ + "begirds", + "bridges" + ], + "attw": [ + "twat", + "watt" + ], + "cdeent": [ + "cedent", + "decent", + "decnet" + ], + "acginst": [ + "actings", + "casting" + ], + "adnoty": [ + "adyton", + "dayton" + ], + "aclors": [ + "carlos", + "carols", + "claros", + "corals" + ], + "enor": [ + "nore", + "oner", + "reno", + "rone" + ], + "adnno": [ + "donna", + "nonda" + ], + "abcin": [ + "bacin", + "cabin" + ], + "acginnns": [ + "cannings", + "scanning" + ], + "ailmuv": [ + "maulvi", + "valium" + ], + "cdelorss": [ + "cordless", + "scolders" + ], + "defir": [ + "fired", + "fried" + ], + "airsy": [ + "sairy", + "syria" + ], + "eikln": [ + "inkle", + "liken" + ], + "glos": [ + "glos", + "logs", + "slog" + ], + "eorrt": [ + "retro", + "roter" + ], + "elo": [ + "leo", + "loe", + "ole" + ], + "accilrru": [ + "circular", + "curricla" + ], + "isstu": [ + "situs", + "suist", + "suits", + "tissu" + ], + "agr": [ + "agr", + "arg", + "gar", + "gra", + "rag" + ], + "abeirrz": [ + "bizarre", + "brazier" + ], + "ego": [ + "ego", + "geo" + ], + "bbinor": [ + "ribbon", + "robbin" + ], + "deo": [ + "doe", + "edo", + "ode" + ], + "aprsttu": [ + "startup", + "upstart" + ], + "ait": [ + "ait", + "ati", + "ita", + "tai" + ], + "giiknss": [ + "kissing", + "skiings" + ], + "adhny": [ + "anhyd", + "haydn", + "handy" + ], + "apsw": [ + "paws", + "swap", + "waps", + "wasp" + ], + "eegmorty": [ + "geoemtry", + "geometry" + ], + "abs": [ + "abs", + "asb", + "bas", + "sab" + ], + "ims": [ + "ism", + "mis", + "sim" + ], + "dehiss": [ + "dishes", + "hissed" + ], + "aceilpr": [ + "caliper", + "earclip", + "picarel", + "replica" + ], + "beirt": [ + "biter", + "brite", + "tiber", + "tribe" + ], + "aderst": [ + "daters", + "derats", + "stader", + "stared", + "sterad", + "strade", + "trades", + "treads" + ], + "eknu": [ + "neuk", + "nuke" + ], + "aclm": [ + "calm", + "clam" + ], + "cdeiop": [ + "copied", + "epodic" + ], + "aciost": [ + "coatis", + "isotac", + "scotia" + ], + "afgimnr": [ + "farming", + "fingram", + "framing" + ], + "bginos": [ + "bingos", + "gibson", + "obsign" + ], + "orty": [ + "ryot", + "royt", + "tyro", + "tory", + "troy" + ], + "ellorr": [ + "reroll", + "roller" + ], + "aeginorz": [ + "agonizer", + "orangize", + "organize" + ], + "aacdeeipprt": [ + "appreciated", + "appredicate" + ], + "ceilno": [ + "cineol", + "clione", + "cloine", + "coelin", + "encoil", + "enolic" + ], + "ailnot": [ + "italon", + "latino", + "lation", + "talion" + ], + "aaghn": [ + "aghan", + "ghana" + ], + "deegs": [ + "edges", + "sedge" + ], + "adehlns": [ + "handles", + "handsel" + ], + "deiklls": [ + "deskill", + "dillesk", + "skilled" + ], + "aahmst": [ + "amsath", + "asthma" + ], + "ipr": [ + "ipr", + "pir", + "rip" + ], + "aderrw": [ + "drawer", + "redraw", + "reward", + "warder", + "warred" + ], + "arty": [ + "arty", + "atry", + "tray" + ], + "inpstu": [ + "inputs", + "ptinus", + "unspit" + ], + "aaeinrrstw": [ + "warranties", + "warrantise" + ], + "adelm": [ + "demal", + "lamed", + "medal" + ], + "aklsw": [ + "lawks", + "walks" + ], + "bhoot": [ + "bhoot", + "booth" + ], + "cdeeeflrt": [ + "redeflect", + "reflected" + ], + "benos": [ + "bones", + "ebons" + ], + "bdeer": [ + "brede", + "breed", + "rebed" + ], + "cdeeortt": [ + "cottered", + "detector" + ], + "deginor": [ + "eroding", + "gironde", + "groined", + "ignored", + "negroid", + "redoing" + ], + "alopr": [ + "parol", + "polar", + "poral", + "proal" + ], + "aaegilntuv": [ + "evaluating", + "vaginulate" + ], + "ilp": [ + "ipl", + "lip", + "pil", + "pli" + ], + "aeghrt": [ + "gareth", + "gather" + ], + "adeflr": [ + "alfred", + "fardel", + "flared" + ], + "acery": [ + "carey", + "craye" + ], + "elmost": [ + "molest", + "motels" + ], + "cdeeoprs": [ + "procedes", + "proceeds" + ], + "cdeiinrt": [ + "indicter", + "indirect", + "reindict" + ], + "aerrst": [ + "arrest", + "astrer", + "rarest", + "raster", + "raters", + "starer", + "tarres", + "terras", + "treasr" + ], + "deelpy": [ + "deeply", + "yelped" + ], + "cit": [ + "cit", + "tic" + ], + "aaimnr": [ + "airman", + "amarin", + "marian", + "marina", + "mirana" + ], + "abinos": [ + "basion", + "bonsai", + "sabino" + ], + "aiopt": [ + "patio", + "taipo", + "topia" + ], + "aceelnort": [ + "antrocele", + "coeternal", + "tolerance" + ], + "aceiirttvy": [ + "creativity", + "reactivity" + ], + "dlloy": [ + "dolly", + "lloyd" + ], + "deey": [ + "eyed", + "yede" + ], + "abgr": [ + "brag", + "garb", + "grab" + ], + "ceinoprst": [ + "inceptors", + "inspector" + ], + "abens": [ + "banes", + "beans", + "besan" + ], + "eills": [ + "liles", + "lisle", + "selli" + ], + "aekns": [ + "kanes", + "skean", + "snake", + "sneak" + ], + "adelnor": [ + "endoral", + "ladrone", + "leonard" + ], + "ailnps": [ + "lapins", + "plains", + "spinal" + ], + "admnory": [ + "doryman", + "raymond" + ], + "adeiiintt": [ + "dietitian", + "initiated" + ], + "floo": [ + "fool", + "loof", + "olof" + ], + "aacelnrst": [ + "ancestral", + "lancaster" + ], + "beeorsv": [ + "observe", + "obverse", + "verbose" + ], + "aceinnorst": [ + "containers", + "resanction", + "sanctioner" + ], + "aklr": [ + "karl", + "kral", + "lark" + ], + "aacilr": [ + "alaric", + "racial" + ], + "aeeginrtt": [ + "argentite", + "grenatite", + "integrate" + ], + "abdemru": [ + "bermuda", + "rumbaed" + ], + "aaadmn": [ + "amadan", + "amanda", + "manada" + ], + "beilmos": [ + "mobiles", + "obelism" + ], + "aceerrt": [ + "caterer", + "recrate", + "retrace", + "terrace" + ], + "deeilpr": [ + "periled", + "replied" + ], + "elnosv": [ + "novels", + "sloven", + "volens" + ], + "aijl": [ + "jail", + "lija" + ], + "aeflsy": [ + "fayles", + "safely" + ], + "deikny": [ + "dinkey", + "kidney" + ], + "denss": [ + "sends", + "sneds" + ], + "abdelru": [ + "delubra", + "durable" + ], + "horstw": [ + "rowths", + "throws", + "whorts", + "worths" + ], + "eimorstu": [ + "moisture", + "semitour" + ], + "eimrt": [ + "ermit", + "merit", + "miter", + "mitre", + "mtier", + "remit", + "timer" + ], + "abelstt": [ + "battels", + "battles", + "tablets" + ], + "acdeorstu": [ + "aeroducts", + "ceratodus", + "croustade", + "educators" + ], + "aegilmnnt": [ + "alignment", + "lamenting" + ], + "aey": [ + "aye", + "yea" + ], + "eorstw": [ + "restow", + "stower", + "towers", + "towser", + "worset" + ], + "ackrs": [ + "carks", + "racks" + ], + "acel": [ + "acle", + "alce", + "alec", + "lace" + ], + "ansty": [ + "antsy", + "nasty", + "santy", + "styan", + "tansy" + ], + "adeilttu": [ + "altitude", + "latitude" + ], + "gluy": [ + "guly", + "ugly" + ], + "deiopsst": [ + "deposits", + "topsides" + ], + "asttw": [ + "twats", + "watts" + ], + "ahrt": [ + "hart", + "rath", + "tahr", + "thar", + "trah" + ], + "abdenrr": [ + "bernard", + "brander", + "rebrand" + ], + "bestu": [ + "stube", + "subet", + "tubes" + ], + "clo": [ + "clo", + "col", + "loc" + ], + "eiprst": [ + "esprit", + "priest", + "pteris", + "ripest", + "sitrep", + "sprite", + "stripe", + "tripes" + ], + "dfloy": [ + "floyd", + "foldy" + ], + "acenrt": [ + "canter", + "carnet", + "centra", + "cranet", + "creant", + "cretan", + "nectar", + "recant", + "tanrec", + "trance" + ], + "achilnos": [ + "lichanos", + "nicholas" + ], + "bilo": [ + "bilo", + "biol", + "boil", + "lobi", + "obli" + ], + "bdelnu": [ + "bundle", + "unbled" + ], + "iknss": [ + "sinks", + "skins" + ], + "adeilm": [ + "aldime", + "mailed", + "medial" + ], + "aderrsw": [ + "drawers", + "redraws", + "resward", + "rewards", + "warders" + ], + "ddeefn": [ + "defend", + "fended" + ], + "acddeiim": [ + "mediacid", + "medicaid" + ], + "eort": [ + "rote", + "tore" + ], + "ehlsw": [ + "welsh", + "whsle" + ], + "elnost": [ + "lentos", + "solent", + "stolen", + "telson" + ], + "aci": [ + "cai", + "cia" + ], + "deeeimnrst": [ + "densimeter", + "determines", + "misentered" + ], + "lopy": [ + "ploy", + "poly" + ], + "aers": [ + "ares", + "arse", + "ears", + "eras", + "rase", + "sare", + "sear", + "sera" + ], + "aalnrstu": [ + "naturals", + "saturnal" + ], + "deelnrs": [ + "lenders", + "relends", + "slender" + ], + "eemr": [ + "emer", + "erme", + "meer", + "mere", + "reem" + ], + "aeegrs": [ + "agrees", + "eagers", + "eagres", + "grease", + "ragees", + "sageer" + ], + "ceehrs": [ + "cheers", + "creesh" + ], + "dgi": [ + "dig", + "gid" + ], + "ehimnnpstu": [ + "punishment", + "unshipment" + ], + "aceinooprrt": [ + "incorporate", + "procreation" + ], + "aeerrrstu": [ + "serrature", + "treasurer" + ], + "ceeenss": [ + "essence", + "necesse", + "senesce" + ], + "ehlmos": [ + "holmes", + "mohels" + ], + "gis": [ + "gis", + "sig" + ], + "ceehrst": [ + "chester", + "etchers", + "retches", + "tresche" + ], + "isttw": [ + "twist", + "twits" + ], + "einstu": [ + "intuse", + "tenuis", + "unites", + "unties" + ], + "bdeiru": [ + "burdie", + "buried", + "rubied" + ], + "adinrw": [ + "darwin", + "inward" + ], + "aknrs": [ + "karns", + "knars", + "krans", + "narks", + "ranks", + "snark" + ], + "bdetu": [ + "debut", + "tubed" + ], + "abdelry": [ + "bradley", + "dryable" + ], + "deny": [ + "deny", + "dyne" + ], + "abil": [ + "albi", + "bail", + "bali" + ], + "iort": [ + "riot", + "roit", + "roti", + "tiro", + "tori", + "trio" + ], + "aekmrr": [ + "marker", + "remark" + ], + "grsu": [ + "grus", + "rugs", + "surg" + ], + "aadnrs": [ + "nasard", + "sandra" + ], + "acmnoo": [ + "mocoan", + "monaco" + ], + "aeeimrst": [ + "emirates", + "steamier" + ], + "aeft": [ + "atef", + "fate", + "feat", + "feta" + ], + "borstu": [ + "robust", + "turbos" + ], + "agrs": [ + "gars", + "gras", + "rags" + ], + "ddeenoprs": [ + "desponder", + "responded" + ], + "imr": [ + "mir", + "rim" + ], + "aeilnp": [ + "alpine", + "nepali", + "penial", + "pineal" + ], + "dis": [ + "dis", + "ids", + "sid" + ], + "eimrx": [ + "mirex", + "mixer", + "remix" + ], + "eenrw": [ + "newer", + "renew", + "weren" + ], + "aky": [ + "yak", + "kay" + ], + "ceips": [ + "epics", + "sepic", + "spice" + ], + "amos": [ + "amos", + "moas", + "soam", + "soma" + ], + "celoor": [ + "cooler", + "recool" + ], + "aciis": [ + "ascii", + "isiac" + ], + "bdeeginr": [ + "beringed", + "breeding" + ], + "aciinostt": [ + "actionist", + "citations" + ], + "dnoor": [ + "donor", + "rondo" + ], + "einnost": [ + "intones", + "stenion", + "tension" + ], + "ahrst": [ + "harst", + "harts", + "tahrs", + "trash" + ], + "aehpss": [ + "pashes", + "phases", + "shapes" + ], + "eeelnopv": [ + "envelope", + "ovenpeel" + ], + "adein": [ + "diane", + "endia", + "idean" + ], + "bdeers": [ + "bredes", + "breeds" + ], + "adiprs": [ + "dispar", + "rapids", + "sparid" + ], + "cdios": [ + "disco", + "sodic" + ], + "defin": [ + "fiend", + "fined", + "indef" + ], + "eimnoost": [ + "emotions", + "mooniest" + ], + "aceelnrs": [ + "cleaners", + "cleanser", + "recleans" + ], + "aabgilnru": [ + "biangular", + "bulgarian" + ], + "aeelnrt": [ + "alterne", + "enteral", + "eternal", + "teleran", + "teneral" + ], + "acehirss": [ + "cashiers", + "rachises" + ], + "agmu": [ + "gaum", + "guam", + "muga" + ], + "ceit": [ + "ceti", + "cite", + "tice" + ], + "gip": [ + "gip", + "pig" + ], + "imnsu": [ + "minus", + "numis", + "unism" + ], + "aeeilnpst": [ + "palestine", + "penalties", + "tapelines" + ], + "aaeimnr": [ + "amarine", + "armenia" + ], + "celosst": [ + "closest", + "closets" + ], + "aacdeittv": [ + "activated", + "cavitated" + ], + "acersst": [ + "actress", + "casters", + "recasts" + ], + "ilt": [ + "lit", + "til" + ], + "deilss": [ + "dessil", + "sidles", + "slides" + ], + "ailmn": [ + "lamin", + "liman", + "milan" + ], + "deelnr": [ + "eldern", + "lender", + "relend" + ], + "chorsu": [ + "chorus", + "urochs" + ], + "cehiinrst": [ + "christine", + "snitchier" + ], + "adegru": [ + "argued", + "dargue" + ], + "hmnopsyy": [ + "physnomy", + "symphony" + ], + "aceklr": [ + "calker", + "clarke", + "lacker", + "rackle", + "recalk", + "reckla" + ], + "ilnos": [ + "insol", + "isoln", + "linos", + "lions", + "loins", + "noils" + ], + "loops": [ + "loops", + "polos", + "pools", + "sloop", + "spool" + ], + "cilry": [ + "cyril", + "lyric" + ], + "aceilr": [ + "acerli", + "carlie", + "claire", + "eclair", + "erical", + "lacier" + ], + "eopr": [ + "pore", + "rope" + ], + "aailnort": [ + "notarial", + "rational", + "rotalian" + ], + "efghirst": [ + "fighters", + "freights", + "refights" + ], + "abcehmrs": [ + "becharms", + "brechams", + "chambers" + ], + "ceeilmnopt": [ + "emplection", + "incomplete" + ], + "benrru": [ + "burner", + "reburn" + ], + "rsy": [ + "yrs", + "syr" + ], + "foo": [ + "foo", + "ofo", + "oof" + ], + "eeglnt": [ + "gentle", + "telegn" + ], + "deeepr": [ + "deeper", + "peered" + ], + "hortwy": [ + "worthy", + "wrothy" + ], + "ainsst": [ + "saints", + "satins", + "stains" + ], + "aceirrs": [ + "carries", + "scarier" + ], + "dou": [ + "duo", + "oud", + "udo" + ], + "denov": [ + "devon", + "doven" + ], + "aeehln": [ + "anhele", + "helena" + ], + "aessv": [ + "saves", + "vases" + ], + "addeegrr": [ + "degrader", + "regarded", + "regraded" + ], + "cdeeenptux": [ + "unexcepted", + "unexpected" + ], + "aimnor": [ + "mainor", + "manoir", + "marion", + "minora", + "morian", + "romain" + ], + "deilnotu": [ + "outlined", + "untoiled" + ], + "aeginrtt": [ + "gnattier", + "treating" + ], + "rst": [ + "str", + "trs" + ], + "aaeinrrtv": [ + "narrative", + "veratrina" + ], + "emnoorsu": [ + "enormous", + "unmorose" + ], + "aakmr": [ + "karma", + "krama", + "makar", + "marka" + ], + "cinosst": [ + "consist", + "tocsins" + ], + "aclsu": [ + "cauls", + "claus", + "scaul" + ], + "beirst": [ + "bestir", + "bister", + "bistre", + "biters", + "bitser", + "tribes" + ], + "adimr": [ + "mardi", + "marid" + ], + "abceinst": [ + "bascinet", + "cabinets" + ], + "aehks": [ + "hakes", + "shake" + ], + "aabeglr": [ + "algebar", + "algebra" + ], + "illsy": [ + "yills", + "silyl", + "silly", + "slily" + ], + "einrssu": [ + "insures", + "rusines", + "russine", + "serinus", + "sunrise" + ], + "fru": [ + "fur", + "urf" + ], + "eeiilmnt": [ + "ilmenite", + "melinite", + "menilite" + ], + "adeilry": [ + "arylide", + "readily" + ], + "cos": [ + "cos", + "osc", + "soc" + ], + "dinstu": [ + "dustin", + "nudist" + ], + "aadin": [ + "andia", + "danai", + "diana", + "naiad" + ], + "eenrssu": [ + "ensures", + "russene" + ], + "aeeilrstv": [ + "levirates", + "relatives", + "versatile" + ], + "adilnsy": [ + "islandy", + "lindsay" + ], + "aehms": [ + "ahems", + "haems", + "hames", + "sahme", + "shame", + "shema" + ], + "eehlnopty": [ + "polythene", + "telephony" + ], + "aaflt": [ + "aflat", + "fatal" + ], + "aelorrst": [ + "realtors", + "relators", + "restoral" + ], + "abeghinrt": [ + "breathing", + "rebathing" + ], + "aacghilpr": [ + "algraphic", + "graphical" + ], + "aeerrtt": [ + "ettarre", + "retreat", + "treater" + ], + "abelry": [ + "barely", + "barley", + "bleary", + "yerbal" + ], + "gru": [ + "gur", + "rug" + ], + "aaiimnnst": [ + "amanitins", + "maintains" + ], + "adeeinrt": [ + "detainer", + "retained" + ], + "aaelmp": [ + "palame", + "palmae", + "pamela" + ], + "adenrsw": [ + "wanders", + "wardens" + ], + "abelmr": [ + "ambler", + "blamer", + "lamber", + "marble", + "ramble" + ], + "cehimnoopr": [ + "microphone", + "neomorphic" + ], + "aginst": [ + "gainst", + "giants", + "gisant", + "sating" + ], + "dehs": [ + "edhs", + "shed" + ], + "emmo": [ + "memo", + "mome" + ], + "ahm": [ + "ham", + "mah" + ], + "celnooss": [ + "consoles", + "coolness" + ], + "bfi": [ + "fbi", + "fib" + ], + "eelr": [ + "leer", + "lere", + "reel" + ], + "eehrs": [ + "heres", + "herse", + "sereh", + "sheer", + "shree" + ], + "ginops": [ + "gipons", + "pingos", + "posing" + ], + "bdin": [ + "bind", + "inbd" + ], + "adnr": [ + "darn", + "nard", + "rand" + ], + "egnrtu": [ + "gunter", + "gurnet", + "urgent" + ], + "chitw": [ + "tchwi", + "wicht", + "witch" + ], + "cehno": [ + "cohen", + "enoch" + ], + "eis": [ + "ise", + "sei", + "sie" + ], + "eeiprsx": [ + "expires", + "prexies" + ], + "ellms": [ + "mells", + "smell" + ], + "acdeinoorst": [ + "coordinates", + "decorations" + ], + "ceikrst": [ + "rickets", + "sticker", + "tickers" + ], + "eimoprss": [ + "imposers", + "promises", + "semipros" + ], + "bno": [ + "bon", + "nob" + ], + "deeersv": [ + "deserve", + "severed" + ], + "ailmot": [ + "lomita", + "tomial" + ], + "achn": [ + "chan", + "nach" + ], + "deiorrw": [ + "rowdier", + "wordier", + "worried" + ], + "enstu": [ + "suent", + "tunes", + "unset", + "usent" + ], + "cegimnopt": [ + "coempting", + "competing" + ], + "beht": [ + "beth", + "theb" + ], + "eln": [ + "enl", + "len" + ], + "aehprss": [ + "phasers", + "phrases", + "seraphs", + "shapers", + "sherpas" + ], + "aik": [ + "aik", + "kai" + ], + "aceehls": [ + "clashee", + "leaches" + ], + "bginor": [ + "boring", + "orbing", + "robing" + ], + "aceehrs": [ + "archsee", + "reaches", + "rechase" + ], + "acehms": [ + "sachem", + "samech", + "schema" + ], + "afos": [ + "oafs", + "sofa" + ], + "efiprx": [ + "perfix", + "prefix" + ], + "acilu": [ + "aulic", + "cauli", + "lucia" + ], + "aelnpst": [ + "planets", + "platens" + ], + "bdeloru": [ + "boulder", + "doubler" + ], + "adhlor": [ + "harold", + "holard" + ], + "ajr": [ + "jar", + "raj" + ], + "entt": [ + "nett", + "tent" + ], + "deefiiinst": [ + "definitise", + "identifies" + ], + "cklos": [ + "locks", + "slock" + ], + "aelmny": [ + "laymen", + "meanly", + "namely" + ], + "eorsu": [ + "euros", + "roues", + "rouse" + ], + "aaeilr": [ + "aerial", + "aralie", + "realia" + ], + "beelr": [ + "blere", + "rebel" + ], + "giinors": [ + "origins", + "signior", + "signori" + ], + "dehir": [ + "dheri", + "hider", + "hired", + "rehid" + ], + "ablm": [ + "balm", + "blam", + "lamb" + ], + "aahnnt": [ + "nathan", + "thanan" + ], + "ceeinrstu": [ + "ceintures", + "centuries" + ], + "dhinu": [ + "hindu", + "hundi", + "unhid" + ], + "aaeehkqrtu": [ + "earthquake", + "heartquake" + ], + "aeginssss": [ + "assessing", + "gassiness" + ], + "agilnst": [ + "anglist", + "lasting", + "salting", + "slating", + "staling" + ], + "cdeinorstu": [ + "discounter", + "introduces", + "rediscount", + "reductions" + ], + "cluy": [ + "cyul", + "lucy" + ], + "ahns": [ + "hans", + "hasn", + "nash", + "shan" + ], + "elops": [ + "elops", + "lopes", + "olpes", + "poles", + "slope", + "spole" + ], + "aeeggr": [ + "agrege", + "raggee", + "reggae" + ], + "eopt": [ + "peto", + "poet", + "pote", + "tope" + ], + "accinoprsy": [ + "conspiracy", + "snipocracy" + ], + "aemnrsu": [ + "manures", + "surname" + ], + "eghlooty": [ + "ethology", + "theology" + ], + "ailns": [ + "anils", + "nails", + "sinal", + "slain", + "snail" + ], + "ahstw": [ + "swath", + "thaws", + "whats" + ], + "deirs": [ + "dries", + "resid", + "rides", + "sider", + "sired" + ], + "ceip": [ + "epic", + "pice" + ], + "anrstu": [ + "saturn", + "unstar" + ], + "ntu": [ + "nut", + "tun" + ], + "aeks": [ + "kaes", + "keas", + "sake", + "seak" + ], + "dikss": [ + "disks", + "skids" + ], + "cdnoo": [ + "codon", + "condo" + ], + "abeimn": [ + "benami", + "bimane", + "embain" + ], + "anss": [ + "assn", + "sans" + ], + "bdilsu": [ + "builds", + "sublid" + ], + "afhst": [ + "hafts", + "shaft" + ], + "bey": [ + "bey", + "bye" + ], + "cdeeorrrs": [ + "recorders", + "rerecords" + ], + "eeills": [ + "eisell", + "leslie", + "sellie" + ], + "aan": [ + "ana", + "naa" + ], + "ginopsst": [ + "postings", + "postsign", + "signpost" + ], + "adinr": [ + "darin", + "dinar", + "drain", + "indra", + "nadir", + "ranid" + ], + "emnot": [ + "entom", + "monte" + ], + "efirs": [ + "fires", + "fries", + "frise", + "reifs", + "serif" + ], + "aaegilr": [ + "algeria", + "argaile", + "lairage", + "railage", + "regalia" + ], + "bdeelss": [ + "bedless", + "blessed" + ], + "aooptt": [ + "pattoo", + "potato", + "topato" + ], + "efmorrs": [ + "formers", + "reforms" + ], + "alot": [ + "alto", + "lota", + "tola" + ], + "cceilrs": [ + "circles", + "clerics" + ], + "aciilt": [ + "clitia", + "italic" + ], + "ilm": [ + "lim", + "mil" + ], + "abcsu": [ + "cubas", + "scuba" + ], + "egor": [ + "ergo", + "goer", + "gore", + "ogre", + "rego" + ], + "adhs": [ + "dahs", + "dash", + "sadh", + "shad" + ], + "aeipssv": [ + "passive", + "pavises", + "pavisse", + "spavies" + ], + "adeluv": [ + "devaul", + "valued" + ], + "deerv": [ + "derve", + "verde" + ], + "aceinorss": [ + "sarcosine", + "scenarios" + ], + "aabmnt": [ + "bantam", + "batman" + ], + "aeghinrs": [ + "hearings", + "hearsing", + "shearing" + ], + "acelmno": [ + "aclemon", + "cloamen" + ], + "aelv": [ + "eval", + "lave", + "leva", + "vale", + "veal", + "vela" + ], + "aaehimn": [ + "anaheim", + "anhimae" + ], + "ddeir": [ + "dried", + "redid" + ], + "aeegiinnnrtt": [ + "entertaining", + "intenerating" + ], + "ehlrtu": [ + "hurtle", + "luther", + "thurle" + ], + "ginoppst": [ + "stopping", + "toppings" + ], + "aelmpr": [ + "ampler", + "emparl", + "lamper", + "palmer", + "relamp" + ], + "acinopt": [ + "caption", + "paction", + "pontiac" + ], + "cdeinort": [ + "centroid", + "doctrine" + ], + "aeinrrst": [ + "restrain", + "retrains", + "strainer", + "terrains", + "trainers", + "transire" + ], + "eginsw": [ + "sewing", + "swinge" + ], + "amno": [ + "mano", + "moan", + "mona", + "noam", + "noma", + "oman" + ], + "cgm": [ + "cgm", + "mcg" + ], + "eoopprs": [ + "opposer", + "propose" + ], + "eghilrt": [ + "lighter", + "relight", + "rightle" + ], + "adls": [ + "lads", + "slad" + ], + "accistt": [ + "tactics", + "tictacs" + ], + "rssttu": [ + "struts", + "sturts", + "trusts" + ], + "aeinn": [ + "annie", + "ennia", + "inane" + ], + "ehorrst": [ + "rhetors", + "shorter" + ], + "adeginprs": [ + "respading", + "spreading" + ], + "acelpr": [ + "carpel", + "parcel", + "placer" + ], + "deefinr": [ + "definer", + "refined" + ], + "aefrs": [ + "fares", + "farse", + "fears", + "frase", + "safer" + ], + "enrtu": [ + "enrut", + "tuner", + "urent" + ], + "arsy": [ + "rays", + "ryas" + ], + "aeikl": [ + "alike", + "kelia", + "lakie" + ], + "altw": [ + "twal", + "walt" + ], + "acen": [ + "acne", + "cane", + "nace" + ], + "bcdeklo": [ + "blocked", + "deblock" + ], + "aailmw": [ + "awalim", + "malawi", + "mawali" + ], + "aadn": [ + "anda", + "dana", + "nada" + ], + "ahlo": [ + "halo", + "hola" + ], + "cirstu": [ + "citrus", + "curtis", + "rictus", + "rustic" + ], + "aaelnprt": [ + "parental", + "parlante", + "paternal", + "prenatal" + ], + "agsy": [ + "gays", + "sagy" + ], + "eginprss": [ + "pressing", + "springes" + ], + "aaaginr": [ + "agrania", + "angaria", + "niagara" + ], + "fin": [ + "fin", + "inf" + ], + "achmrs": [ + "charms", + "ramsch" + ], + "aderrt": [ + "darter", + "dartre", + "dertra", + "redart", + "retard", + "retrad", + "tarred", + "trader" + ], + "aeirss": [ + "arises", + "raises", + "serais" + ], + "egm": [ + "gem", + "meg" + ], + "ceelort": [ + "elector", + "electro" + ], + "ceiinorrt": [ + "cirterion", + "criterion", + "tricerion" + ], + "abdeg": [ + "badge", + "begad", + "debag" + ], + "irstw": [ + "wrist", + "writs" + ], + "aehht": [ + "heath", + "theah" + ], + "deeeimrs": [ + "redemise", + "remedies" + ], + "eersttu": [ + "surette", + "trustee" + ], + "efmoprrs": [ + "performs", + "preforms" + ], + "aelmr": [ + "lamer", + "realm" + ], + "aaeilrss": [ + "assailer", + "reassail", + "salaries" + ], + "aimnstu": [ + "manitus", + "tsunami" + ], + "achlors": [ + "chorals", + "scholar" + ], + "ceikln": [ + "nickel", + "nickle" + ], + "acginot": [ + "coating", + "cognati", + "cotinga" + ], + "aelltw": [ + "wallet", + "wellat" + ], + "acdeelr": [ + "cedrela", + "cleared", + "creedal", + "declare", + "relaced" + ], + "eiilmss": [ + "mislies", + "missile", + "similes" + ], + "aadeginr": [ + "drainage", + "gardenia" + ], + "aahikrs": [ + "kashira", + "shikara", + "sikhara" + ], + "cenorrs": [ + "corners", + "scorner" + ], + "abdeorr": [ + "arbored", + "boarder", + "broader", + "reboard" + ], + "aaeeginrtv": [ + "renavigate", + "vegetarian" + ], + "egoru": [ + "erugo", + "orgue", + "rogue", + "rouge" + ], + "aesty": [ + "yeast", + "teasy" + ], + "acegilnr": [ + "clearing", + "relacing" + ], + "acdeot": [ + "coated", + "decoat" + ], + "deinnt": [ + "dentin", + "indent", + "intend", + "tinned" + ], + "aeehinpst": [ + "ephestian", + "stephanie" + ], + "eilosu": [ + "louies", + "louise" + ], + "enow": [ + "enow", + "owen", + "wone" + ], + "einorstu": [ + "routines", + "rutinose", + "snoutier", + "tursenoi" + ], + "ghiintt": [ + "hitting", + "tithing" + ], + "aceeilnr": [ + "cerealin", + "cinereal", + "raceline", + "reliance" + ], + "abhist": [ + "habits", + "tasbih" + ], + "giiknrst": [ + "skirting", + "striking" + ], + "ghins": [ + "nighs", + "singh" + ], + "opsttuu": [ + "outputs", + "putouts" + ], + "iilnnsu": [ + "insulin", + "inulins" + ], + "deew": [ + "wede", + "weed" + ], + "eiiltuz": [ + "tuilzie", + "utilize" + ], + "aaegln": [ + "alange", + "alnage", + "angela", + "anlage", + "galena", + "lagena" + ], + "aeelrst": [ + "elaters", + "laertes", + "realest", + "relates", + "reslate", + "resteal", + "seletar", + "stealer", + "teasler" + ], + "cdei": [ + "cedi", + "dice", + "iced" + ], + "dmos": [ + "doms", + "mods" + ], + "adeginors": [ + "grandiose", + "organdies", + "organised", + "sargonide" + ], + "orsst": [ + "sorts", + "ssort", + "sstor" + ], + "eiinorssv": [ + "ivoriness", + "revisions" + ], + "deeorrst": [ + "resorted", + "restored" + ], + "amorr": [ + "armor", + "maror", + "morra" + ], + "deirrs": [ + "derris", + "driers", + "riders" + ], + "denosz": [ + "dozens", + "zendos" + ], + "aeirsv": [ + "aivers", + "sairve", + "varies" + ], + "adgrsu": [ + "gradus", + "guards" + ], + "acegilnpr": [ + "parceling", + "replacing" + ], + "adeehrstw": [ + "drawsheet", + "watershed" + ], + "ccilnosu": [ + "councils", + "succinol" + ], + "ailrv": [ + "rival", + "viral" + ], + "eipps": [ + "pepsi", + "pipes" + ], + "adeln": [ + "alden", + "eland", + "laden", + "lande", + "lenad", + "naled" + ], + "aelorrt": [ + "realtor", + "relator" + ], + "aeiimnostt": [ + "estimation", + "testimonia" + ], + "abnr": [ + "barn", + "bran" + ], + "ghinpsu": [ + "gunship", + "pushing" + ], + "acdeiiprt": [ + "dipicrate", + "patricide", + "pediatric" + ], + "bco": [ + "boc", + "cob" + ], + "emprs": [ + "perms", + "sperm" + ], + "abdl": [ + "bald", + "blad" + ], + "acprs": [ + "carps", + "craps", + "scarp", + "scrap" + ], + "forst": [ + "forst", + "forts", + "frost" + ], + "elno": [ + "elon", + "enol", + "leno", + "leon", + "lone", + "noel" + ], + "achty": [ + "cathy", + "cyath", + "yacht" + ], + "acrty": [ + "carty", + "tracy" + ], + "mpt": [ + "pmt", + "tpm" + ], + "abcehr": [ + "barche", + "brache", + "breach", + "chaber" + ], + "aehlw": [ + "halwe", + "whale", + "wheal" + ], + "deil": [ + "deil", + "deli", + "diel", + "eild", + "idle", + "lide", + "lied" + ], + "agmnstu": [ + "mustang", + "stagnum" + ], + "aoprst": [ + "asport", + "pastor", + "portas", + "sproat" + ], + "dmu": [ + "dum", + "mud" + ], + "ahkrs": [ + "harks", + "shark" + ], + "abdeeilrs": [ + "desirable", + "redisable" + ], + "abellt": [ + "ballet", + "batell" + ], + "egiilnors": [ + "ligroines", + "religions" + ], + "ehins": [ + "eshin", + "hsien", + "shine" + ], + "befirs": [ + "briefs", + "febris", + "fibers", + "fibres" + ], + "ceov": [ + "cove", + "voce" + ], + "aceinnoorsstv": [ + "conservations", + "conversations" + ], + "adiors": [ + "aroids", + "radios" + ], + "adiinv": [ + "avidin", + "vidian" + ], + "aapst": [ + "apast", + "pasta", + "patas", + "tapas" + ], + "emrsu": [ + "mures", + "muser", + "remus", + "serum" + ], + "eimnrtu": [ + "minuter", + "runtime", + "unmiter", + "unmitre" + ], + "acflo": [ + "falco", + "focal" + ], + "adinstt": [ + "dantist", + "distant" + ], + "ciln": [ + "clin", + "incl" + ], + "aal": [ + "aal", + "ala" + ], + "aabms": [ + "ambas", + "samba" + ], + "elmopy": [ + "employ", + "pomely" + ], + "aacgilm": [ + "camalig", + "magical" + ], + "aceilmr": [ + "calmier", + "claimer", + "milacre", + "miracle", + "reclaim" + ], + "cdeeenrt": [ + "centered", + "decenter", + "decentre", + "recedent" + ], + "aelryy": [ + "yarely", + "yearly", + "layery" + ], + "aors": [ + "asor", + "oars", + "oras", + "osar", + "rosa", + "soar", + "sora" + ], + "ahhs": [ + "hahs", + "hash", + "sahh", + "shah" + ], + "gmp": [ + "gpm", + "mpg" + ], + "aefhrst": [ + "fathers", + "hafters", + "shafter" + ], + "deiln": [ + "eldin", + "lined" + ], + "dow": [ + "dow", + "owd", + "wod" + ], + "diu": [ + "dui", + "iud", + "udi" + ], + "eny": [ + "eyn", + "yen", + "nye" + ], + "iprsst": [ + "spirts", + "sprits", + "stirps", + "strips" + ], + "aegnrrs": [ + "garners", + "rangers" + ], + "morw": [ + "morw", + "worm" + ], + "cdeeirst": [ + "creedist", + "desertic", + "discreet", + "discrete" + ], + "dil": [ + "dil", + "lid" + ], + "eeloprsty": [ + "polyester", + "proselyte" + ], + "adef": [ + "deaf", + "fade" + ], + "aehipprs": [ + "papisher", + "sapphire" + ], + "aeikns": [ + "kinase", + "sekani", + "skenai" + ], + "ikrsst": [ + "skirts", + "stirks" + ], + "amst": [ + "mast", + "mats", + "stam", + "tams" + ], + "abdeell": [ + "beladle", + "labeled" + ], + "abeirs": [ + "braies", + "braise", + "rabies", + "rebias", + "serbia" + ], + "ffgiinr": [ + "griffin", + "riffing" + ], + "aagnuy": [ + "guanay", + "guyana" + ], + "eipss": [ + "sipes", + "spies", + "spise" + ], + "eimm": [ + "emim", + "mime" + ], + "acceennortt": [ + "concentrate", + "concertante" + ], + "eglnost": [ + "longest", + "songlet" + ], + "inost": [ + "nitos", + "sinto", + "stion" + ], + "ceeennsst": [ + "senescent", + "sentences" + ], + "eflry": [ + "ferly", + "flyer", + "refly" + ], + "aeps": [ + "apes", + "apse", + "pase", + "peas", + "pesa", + "spae" + ], + "adegos": [ + "dagoes", + "dosage", + "seadog" + ], + "ceeeirrsv": [ + "receivers", + "reservice" + ], + "acemnoor": [ + "cameroon", + "coenamor" + ], + "deeeln": [ + "lendee", + "needle" + ], + "abhst": [ + "bahts", + "baths" + ], + "aainnrv": [ + "navarin", + "nirvana" + ], + "ademnss": [ + "desmans", + "dsnames", + "madness" + ], + "acems": [ + "acmes", + "cames", + "maces" + ], + "ahy": [ + "hay", + "yah" + ], + "aaccdir": [ + "arcadic", + "cardiac" + ], + "aailnostv": [ + "lavations", + "salvation" + ], + "deorv": [ + "dover", + "drove", + "roved", + "vedro", + "voder" + ], + "aadinr": [ + "adrian", + "andira", + "andria", + "radian", + "randia" + ], + "aeelnrrs": [ + "learners", + "relearns" + ], + "ceeeilstv": [ + "cleveites", + "electives", + "selective" + ], + "adeiilorst": [ + "editorials", + "idolatries", + "idolatrise", + "radiolites" + ], + "eeekrss": [ + "reseeks", + "seekers" + ], + "ais": [ + "ais", + "sai", + "sia" + ], + "abeelmorv": [ + "overblame", + "removable" + ], + "ceimnru": [ + "micerun", + "numeric", + "uncrime" + ], + "agiknst": [ + "gitksan", + "skating", + "staking", + "takings", + "tasking" + ], + "belst": [ + "belts", + "blest", + "blets" + ], + "abeerst": [ + "beaters", + "berates", + "bestare", + "rebates" + ], + "eeoprrrst": [ + "preresort", + "reporters" + ], + "bekru": [ + "bruke", + "burke" + ], + "ceeinssty": [ + "cysteines", + "necessity" + ], + "ekly": [ + "yelk", + "kyle" + ], + "cersuv": [ + "cervus", + "curves" + ], + "aaclr": [ + "clara", + "craal" + ], + "aelrstv": [ + "travels", + "varlets", + "vestral" + ], + "aeirvw": [ + "waiver", + "wavier" + ], + "aelp": [ + "leap", + "lepa", + "pale", + "peal", + "plea" + ], + "deghilt": [ + "delight", + "lighted" + ], + "ceeimnoos": [ + "economies", + "economise", + "monoecies" + ], + "aabceilrt": [ + "bacterial", + "calibrate" + ], + "agps": [ + "gaps", + "gasp", + "spag" + ], + "dnoors": [ + "donors", + "rondos" + ], + "aat": [ + "ata", + "taa" + ], + "aceehst": [ + "escheat", + "teaches" + ], + "iln": [ + "lin", + "nil" + ], + "aegnrrst": [ + "granters", + "regrants", + "stranger" + ], + "adegrty": [ + "gyrated", + "tragedy" + ], + "derry": [ + "derry", + "dryer", + "redry", + "ryder" + ], + "abilnrtu": [ + "tribunal", + "turbinal", + "untribal" + ], + "delru": [ + "duler", + "lured", + "ruled", + "urled" + ], + "anot": [ + "nato", + "nota", + "tano" + ], + "einnopss": [ + "pensions", + "snipnose" + ], + "aeprrsy": [ + "prayers", + "respray", + "sprayer" + ], + "eehnorw": [ + "nowhere", + "whereon" + ], + "cop": [ + "cop", + "cpo" + ], + "aegl": [ + "egal", + "gael", + "gale", + "geal" + ], + "aellty": [ + "lately", + "lealty" + ], + "acrsy": [ + "ascry", + "sacry", + "scary", + "scray" + ], + "aemrsstt": [ + "mattress", + "smartest", + "smatters" + ], + "beinru": [ + "burnie", + "eburin", + "rubine" + ], + "eeiiklsw": [ + "likewise", + "wiselike" + ], + "anst": [ + "ants", + "nast", + "sant", + "stan", + "tans" + ], + "dilo": [ + "dilo", + "diol", + "doli", + "idol", + "lido", + "olid" + ], + "deimnr": [ + "minder", + "remind" + ], + "acghimnr": [ + "charming", + "marching" + ], + "cdeeeprst": [ + "respected", + "sceptered", + "spectered" + ], + "assty": [ + "sayst", + "stays" + ], + "aaffir": [ + "affair", + "raffia" + ], + "aehrsw": [ + "hawser", + "rewash", + "washer" + ], + "ceirrstt": [ + "critters", + "restrict", + "stricter" + ], + "eginprrs": [ + "respring", + "springer" + ], + "eimns": [ + "miens", + "mines" + ], + "bdenoru": [ + "beround", + "bounder", + "rebound", + "unbored", + "unorbed", + "unrobed" + ], + "emnort": [ + "mentor", + "merton", + "metron", + "montre", + "termon", + "tormen" + ], + "eefmoprrr": [ + "performer", + "prereform", + "reperform" + ], + "deiltt": [ + "tilted", + "titled" + ], + "eehprs": [ + "herpes", + "hesper", + "sphere" + ], + "aiorst": [ + "aorist", + "aristo", + "ratios", + "satori" + ], + "addelr": [ + "ladder", + "larded", + "raddle" + ], + "aenorsst": [ + "assentor", + "essorant", + "senators", + "starnose", + "treasons" + ], + "koortuw": [ + "outwork", + "workout" + ], + "aelns": [ + "ansel", + "elans", + "lanes", + "leans", + "senal", + "slane" + ], + "aginstt": [ + "stating", + "tasting" + ], + "aceilnor": [ + "acrolein", + "arecolin", + "caroline", + "colinear", + "cornelia", + "creolian", + "lonicera" + ], + "elu": [ + "leu", + "lue", + "ule" + ], + "agiln": [ + "algin", + "align", + "langi", + "liang", + "ligan", + "linga" + ], + "adeimnnot": [ + "dentinoma", + "nominated" + ], + "aceellort": [ + "electoral", + "recollate" + ], + "eehl": [ + "heel", + "hele" + ], + "alloy": [ + "alloy", + "loyal" + ], + "cdnoos": [ + "codons", + "condos" + ], + "lopst": [ + "plots", + "topsl" + ], + "dehilops": [ + "depolish", + "polished" + ], + "alstu": [ + "altus", + "latus", + "sault", + "talus", + "tulsa" + ], + "adrsw": [ + "draws", + "sward", + "wards" + ], + "lou": [ + "lou", + "luo" + ], + "cdeeeorrv": [ + "overcreed", + "recovered" + ], + "aefrrs": [ + "farers", + "fraser" + ], + "egrsu": [ + "grues", + "guser", + "surge", + "urges" + ], + "aamrtu": [ + "taruma", + "trauma" + ], + "addegimnn": [ + "demanding", + "maddening" + ], + "lossu": [ + "solus", + "souls" + ], + "aknps": [ + "knaps", + "spank" + ], + "egoorv": [ + "groove", + "overgo" + ], + "aeeilnortv": [ + "relevation", + "revelation" + ], + "adeegilnot": [ + "degelation", + "delegation" + ], + "eirsw": [ + "swire", + "weirs", + "wires", + "wiser", + "wries" + ], + "eelpss": [ + "sleeps", + "speels" + ], + "abekl": [ + "blake", + "bleak", + "kabel" + ], + "eginr": [ + "grein", + "inger", + "nigre", + "regin", + "reign", + "renig", + "ringe" + ], + "abcno": [ + "bacon", + "banco" + ], + "aeehrt": [ + "aether", + "heater", + "hereat", + "reheat" + ], + "ccirsu": [ + "circus", + "crucis" + ], + "demooprt": [ + "promoted", + "toperdom" + ], + "aem": [ + "ame", + "eam", + "mae", + "mea" + ], + "elm": [ + "elm", + "mel" + ], + "einps": [ + "insep", + "peins", + "penis", + "pines", + "snipe", + "spine" + ], + "orttu": [ + "tourt", + "trout", + "tutor" + ], + "aeilmoprrty": [ + "polarimetry", + "premorality", + "temporarily" + ], + "ehillrrt": [ + "rethrill", + "thriller" + ], + "aimnrstt": [ + "tantrism", + "transmit" + ], + "adeglr": [ + "argled", + "gerald", + "glared", + "regald" + ], + "deeprss": [ + "depress", + "pressed" + ], + "eghnru": [ + "hunger", + "rehung" + ], + "pssu": [ + "puss", + "sups" + ], + "aeinnrtt": [ + "antirent", + "internat", + "intranet" + ], + "celoss": [ + "closes", + "socles" + ], + "eqs": [ + "esq", + "seq" + ], + "eeinprsstt": [ + "persistent", + "pinsetters", + "presentist", + "prettiness" + ], + "aeimmrssu": [ + "summaries", + "summarise" + ], + "glow": [ + "glow", + "gowl" + ], + "amw": [ + "awm", + "maw", + "mwa" + ], + "deiox": [ + "doxie", + "oxide" + ], + "akos": [ + "asok", + "koas", + "oaks", + "okas", + "soak", + "soka" + ], + "eikr": [ + "erik", + "keir", + "kier", + "reki" + ], + "ceelnorsu": [ + "enclosure", + "recounsel" + ], + "aeknrw": [ + "newark", + "wanker" + ], + "mnor": [ + "morn", + "norm" + ], + "elrttu": [ + "ruttle", + "turtle", + "tutler" + ], + "abinooprst": [ + "absorption", + "probations", + "saprobiont" + ], + "abdly": [ + "badly", + "baldy", + "blady" + ], + "alloop": [ + "apollo", + "palolo" + ], + "anw": [ + "awn", + "naw", + "wan" + ], + "aeinprs": [ + "paniers", + "persian", + "prasine", + "rapines", + "saprine", + "spirane" + ], + "eeegnr": [ + "neeger", + "reenge", + "renege" + ], + "boorst": [ + "brosot", + "robots" + ], + "adej": [ + "deja", + "jade" + ], + "coops": [ + "coops", + "scoop" + ], + "aeginnr": [ + "aginner", + "earning", + "engrain", + "geranin", + "grannie", + "nearing" + ], + "deenst": [ + "dentes", + "nested", + "sedent", + "tensed" + ], + "emorsv": [ + "movers", + "vomers" + ], + "abelrv": [ + "barvel", + "blaver", + "verbal" + ], + "eelnprsty": [ + "presently", + "serpently" + ], + "aess": [ + "asse", + "seas" + ], + "eilst": [ + "islet", + "istle", + "liest", + "lites", + "slite", + "stile", + "tiles" + ], + "eops": [ + "epos", + "opes", + "peso", + "pose", + "sope" + ], + "ikloott": [ + "kittool", + "toolkit" + ], + "agot": [ + "goat", + "toag", + "toga" + ], + "deenrr": [ + "derner", + "render" + ], + "ens": [ + "ens", + "sen" + ], + "efgor": [ + "forge", + "gofer" + ], + "ceiimmnoorsss": [ + "commissioners", + "recommissions" + ], + "aberv": [ + "brave", + "breva" + ], + "afluw": [ + "awful", + "fulwa" + ], + "aaeilnpr": [ + "airplane", + "perianal" + ], + "eenrst": [ + "enters", + "ernest", + "nester", + "rentes", + "resent", + "streen", + "tenser", + "ternes" + ], + "dop": [ + "dop", + "pod" + ], + "aaegsv": [ + "agaves", + "savage" + ], + "blot": [ + "blot", + "bolt" + ], + "ghinortw": [ + "ingrowth", + "throwing", + "worthing" + ], + "agnow": [ + "gowan", + "wagon", + "wonga" + ], + "adt": [ + "dat", + "tad" + ], + "egru": [ + "grue", + "urge" + ], + "aeeegnrst": [ + "generates", + "teenagers" + ], + "krtu": [ + "kurt", + "turk" + ], + "eeprs": [ + "peers", + "peres", + "perse", + "prees", + "prese", + "speer", + "spere", + "spree" + ], + "orsu": [ + "ours", + "rous", + "sour" + ], + "eefhrrs": [ + "fresher", + "refresh" + ], + "belstu": [ + "bluest", + "bluets", + "bustle", + "butles", + "sublet", + "subtle" + ], + "eiprsst": [ + "esprits", + "persist", + "priests", + "spriest", + "sprites", + "stirpes", + "stripes" + ], + "ceop": [ + "cope", + "opec" + ], + "acdelr": [ + "cardel", + "cradle", + "credal", + "reclad" + ], + "ikkr": [ + "kirk", + "rikk" + ], + "floru": [ + "flour", + "fluor" + ], + "adeeglnry": [ + "enragedly", + "legendary" + ], + "bloo": [ + "bolo", + "bool", + "lobo", + "loob", + "obol" + ], + "ahmpstyy": [ + "sympathy", + "symphyta" + ], + "chior": [ + "chiro", + "choir", + "ichor" + ], + "ceepstx": [ + "excepts", + "expects" + ], + "cho": [ + "cho", + "hoc", + "och" + ], + "itw": [ + "twi", + "wit" + ], + "brstu": [ + "burst", + "strub" + ], + "din": [ + "din", + "ind", + "nid" + ], + "coprsu": [ + "corpus", + "croups" + ], + "adehss": [ + "dashes", + "sadhes", + "sashed", + "shades" + ], + "ejst": [ + "jest", + "jets" + ], + "cdeiins": [ + "incised", + "indices" + ], + "ilnt": [ + "intl", + "lint" + ], + "adisy": [ + "daisy", + "sayid" + ], + "corrsu": [ + "cruors", + "cursor" + ], + "aeeilrst": [ + "ateliers", + "earliest", + "leariest", + "realties", + "tresaiel" + ], + "addenot": [ + "donated", + "nodated" + ], + "deffstu": [ + "destuff", + "stuffed" + ], + "ceinsst": [ + "incests", + "insects", + "scenist" + ], + "aeilmnrst": [ + "mislearnt", + "terminals", + "trailsmen", + "tramlines" + ], + "cderu": [ + "crude", + "cured" + ], + "elmrty": [ + "myrtle", + "termly" + ], + "bdeor": [ + "boder", + "bored", + "orbed", + "robed" + ], + "acelnpu": [ + "cleanup", + "unplace" + ], + "iknt": [ + "knit", + "tink" + ], + "gmu": [ + "gum", + "mug" + ], + "behort": [ + "bother", + "brothe" + ], + "abhntu": [ + "bhutan", + "thuban" + ], + "agimnt": [ + "mating", + "taming" + ], + "addeehr": [ + "adhered", + "redhead" + ], + "aeirrsv": [ + "arrives", + "riserva", + "variers" + ], + "aahll": [ + "allah", + "halal" + ], + "anpruw": [ + "unwarp", + "unwrap" + ], + "dehop": [ + "depoh", + "ephod", + "hoped" + ], + "eikp": [ + "kepi", + "kipe", + "pike" + ], + "ghou": [ + "hugo", + "ough" + ], + "aegnrw": [ + "gnawer", + "wagner", + "wanger" + ], + "aegnrr": [ + "garner", + "ranger" + ], + "ahmrs": [ + "harms", + "marsh", + "shram" + ], + "aehtt": [ + "hatte", + "theat", + "theta" + ] +} -- cgit v1.2.3 From 60b146f9b55af5688b96534884ab350f63da1e28 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 24 Sep 2021 12:13:26 +0100 Subject: Add a 2 minute cooldown to the topic command Using the command while it's on cooldown will hit the error handler, which sends an error message showing how long is left on the cooldown, which is deleted after 7.5 seconds. --- bot/exts/utilities/conversationstarters.py | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/bot/exts/utilities/conversationstarters.py b/bot/exts/utilities/conversationstarters.py index dd537022..07d71f15 100644 --- a/bot/exts/utilities/conversationstarters.py +++ b/bot/exts/utilities/conversationstarters.py @@ -36,32 +36,16 @@ class ConvoStarters(commands.Cog): """General conversation topics.""" @commands.command() + @commands.cooldown(1, 60*2, commands.BucketType.channel) @whitelist_override(channels=ALL_ALLOWED_CHANNELS) async def topic(self, ctx: commands.Context) -> None: """ Responds with a random topic to start a conversation. - If in a Python channel, a python-related topic will be given. - - Otherwise, a random conversation topic will be received by the user. + Allows the refresh of a topic by pressing an emoji. """ - # No matter what, the form will be shown. - embed = Embed(description=f"Suggest more topics [here]({SUGGESTION_FORM})!", color=Color.blurple()) - - try: - # Fetching topics. - channel_topics = TOPICS[ctx.channel.id] - - # If the channel isn't Python-related. - except KeyError: - embed.title = f"**{next(TOPICS['default'])}**" - - # If the channel ID doesn't have any topics. - else: - embed.title = f"**{next(channel_topics)}**" - - finally: - await ctx.send(embed=embed) + message = await ctx.send(embed=self._build_topic_embed(ctx.channel.id)) + self.bot.loop.create_task(self._listen_for_refresh(message)) def setup(bot: Bot) -> None: -- cgit v1.2.3 From 515c6390563f33a02af2e46fe6f3d13b15353a0a Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 24 Sep 2021 13:10:53 +0100 Subject: Allow topics to be refreshed This is done via an emoji as buttons are too big Co-authored-by: Bluenix --- bot/exts/utilities/conversationstarters.py | 65 ++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/bot/exts/utilities/conversationstarters.py b/bot/exts/utilities/conversationstarters.py index 07d71f15..5d62fa83 100644 --- a/bot/exts/utilities/conversationstarters.py +++ b/bot/exts/utilities/conversationstarters.py @@ -1,11 +1,14 @@ +import asyncio +from contextlib import suppress +from functools import partial from pathlib import Path +import discord import yaml -from discord import Color, Embed from discord.ext import commands from bot.bot import Bot -from bot.constants import WHITELISTED_CHANNELS +from bot.constants import MODERATION_ROLES, WHITELISTED_CHANNELS from bot.utils.decorators import whitelist_override from bot.utils.randomization import RandomCycle @@ -35,6 +38,62 @@ TOPICS = { class ConvoStarters(commands.Cog): """General conversation topics.""" + def __init__(self, bot: Bot): + self.bot = bot + + @staticmethod + def _build_topic_embed(channel_id: int) -> discord.Embed: + """ + Build an embed containing a conversation topic. + + If in a Python channel, a python-related topic will be given. + Otherwise, a random conversation topic will be received by the user. + """ + # No matter what, the form will be shown. + embed = discord.Embed( + description=f"Suggest more topics [here]({SUGGESTION_FORM})!", + color=discord.Color.blurple() + ) + + try: + channel_topics = TOPICS[channel_id] + except KeyError: + # Channel doesn't have any topics. + embed.title = f"**{next(TOPICS['default'])}**" + else: + embed.title = f"**{next(channel_topics)}**" + return embed + + def _predicate(self, message: discord.Message, reaction: discord.Reaction, user: discord.User) -> bool: + right_reaction = ( + user != self.bot.user + and reaction.message.id == message.id + and str(reaction.emoji) == "πŸ”„" + ) + if not right_reaction: + return False + + is_moderator = any(role.id in MODERATION_ROLES for role in getattr(user, "roles", [])) + if is_moderator or user.id == message.author.id: + return True + + return False + + async def _listen_for_refresh(self, message: discord.Message) -> None: + await message.add_reaction("πŸ”„") + while True: + try: + reaction, user = await self.bot.wait_for( + "reaction_add", + check=partial(self._predicate, message), + timeout=60.0 + ) + except asyncio.TimeoutError: + with suppress(discord.NotFound): + await message.clear_reaction("πŸ”„") + else: + await message.edit(embed=self._build_topic_embed(message.channel.id)) + @commands.command() @commands.cooldown(1, 60*2, commands.BucketType.channel) @whitelist_override(channels=ALL_ALLOWED_CHANNELS) @@ -50,4 +109,4 @@ class ConvoStarters(commands.Cog): def setup(bot: Bot) -> None: """Load the ConvoStarters cog.""" - bot.add_cog(ConvoStarters()) + bot.add_cog(ConvoStarters(bot)) -- cgit v1.2.3 From 0cc435c1e56260271bb6ff27fddd8d6971837feb Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Fri, 8 Oct 2021 07:58:23 +0200 Subject: Candy collection: fix positional arg being passed as kword This caused a `TypeError` to be raised, as the `id` argument could only be used as a positional argument and not by keyword. --- bot/exts/holidays/halloween/candy_collection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/holidays/halloween/candy_collection.py b/bot/exts/holidays/halloween/candy_collection.py index 4afd5913..09bd0e59 100644 --- a/bot/exts/holidays/halloween/candy_collection.py +++ b/bot/exts/holidays/halloween/candy_collection.py @@ -134,7 +134,7 @@ class CandyCollection(commands.Cog): @property def hacktober_channel(self) -> discord.TextChannel: """Get #hacktoberbot channel from its ID.""" - return self.bot.get_channel(id=Channels.community_bot_commands) + return self.bot.get_channel(Channels.community_bot_commands) @staticmethod async def send_spook_msg( -- cgit v1.2.3 From 999604b335840fe820deddd0cebea7b6b601c218 Mon Sep 17 00:00:00 2001 From: Izan Date: Fri, 8 Oct 2021 11:39:20 +0100 Subject: `.topic` command improvements. - Fix bug where command author couldn't re-roll - Now removes user's reaction up re-roll - Added a missing `break` statement --- bot/exts/utilities/conversationstarters.py | 45 ++++++++++++++++++------------ 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/bot/exts/utilities/conversationstarters.py b/bot/exts/utilities/conversationstarters.py index 5d62fa83..2316c50d 100644 --- a/bot/exts/utilities/conversationstarters.py +++ b/bot/exts/utilities/conversationstarters.py @@ -2,6 +2,7 @@ import asyncio from contextlib import suppress from functools import partial from pathlib import Path +from typing import Union import discord import yaml @@ -64,35 +65,43 @@ class ConvoStarters(commands.Cog): embed.title = f"**{next(channel_topics)}**" return embed - def _predicate(self, message: discord.Message, reaction: discord.Reaction, user: discord.User) -> bool: - right_reaction = ( - user != self.bot.user - and reaction.message.id == message.id - and str(reaction.emoji) == "πŸ”„" - ) - if not right_reaction: - return False - - is_moderator = any(role.id in MODERATION_ROLES for role in getattr(user, "roles", [])) - if is_moderator or user.id == message.author.id: - return True - - return False - - async def _listen_for_refresh(self, message: discord.Message) -> None: + @staticmethod + def _predicate( + command_invoker: Union[discord.User, discord.Member], + message: discord.Message, + reaction: discord.Reaction, + user: discord.User + ) -> bool: + user_is_moderator = any(role.id in MODERATION_ROLES for role in getattr(user, "roles", [])) + user_is_invoker = user.id == command_invoker.id + + is_right_reaction = all(( + reaction.message.id == message.id, + str(reaction.emoji) == "πŸ”„", + user_is_moderator or user_is_invoker + )) + return is_right_reaction + + async def _listen_for_refresh( + self, + command_invoker: Union[discord.User, discord.Member], + message: discord.Message + ) -> None: await message.add_reaction("πŸ”„") while True: try: reaction, user = await self.bot.wait_for( "reaction_add", - check=partial(self._predicate, message), + check=partial(self._predicate, command_invoker, message), timeout=60.0 ) except asyncio.TimeoutError: with suppress(discord.NotFound): await message.clear_reaction("πŸ”„") + break else: await message.edit(embed=self._build_topic_embed(message.channel.id)) + await message.remove_reaction(reaction, user) @commands.command() @commands.cooldown(1, 60*2, commands.BucketType.channel) @@ -104,7 +113,7 @@ class ConvoStarters(commands.Cog): Allows the refresh of a topic by pressing an emoji. """ message = await ctx.send(embed=self._build_topic_embed(ctx.channel.id)) - self.bot.loop.create_task(self._listen_for_refresh(message)) + self.bot.loop.create_task(self._listen_for_refresh(ctx.author, message)) def setup(bot: Bot) -> None: -- cgit v1.2.3 From 3c26b4d2fc4746da13695c31ef4dc7435f35525f Mon Sep 17 00:00:00 2001 From: Izan Date: Fri, 8 Oct 2021 14:24:14 +0100 Subject: Add handling for `discord.NotFound` when re-rolling / removing reaction --- bot/exts/utilities/conversationstarters.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bot/exts/utilities/conversationstarters.py b/bot/exts/utilities/conversationstarters.py index 2316c50d..fcb5f977 100644 --- a/bot/exts/utilities/conversationstarters.py +++ b/bot/exts/utilities/conversationstarters.py @@ -100,8 +100,13 @@ class ConvoStarters(commands.Cog): await message.clear_reaction("πŸ”„") break else: - await message.edit(embed=self._build_topic_embed(message.channel.id)) - await message.remove_reaction(reaction, user) + try: + await message.edit(embed=self._build_topic_embed(message.channel.id)) + except discord.NotFound: + break + + with suppress(discord.NotFound): + await message.remove_reaction(reaction, user) @commands.command() @commands.cooldown(1, 60*2, commands.BucketType.channel) -- cgit v1.2.3 From e01503a61015d483cd70e1e408c647b4054a927f Mon Sep 17 00:00:00 2001 From: Izan Date: Fri, 8 Oct 2021 14:27:15 +0100 Subject: Remove unnecessary `else` --- bot/exts/utilities/conversationstarters.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bot/exts/utilities/conversationstarters.py b/bot/exts/utilities/conversationstarters.py index fcb5f977..dcbfe4d5 100644 --- a/bot/exts/utilities/conversationstarters.py +++ b/bot/exts/utilities/conversationstarters.py @@ -99,14 +99,14 @@ class ConvoStarters(commands.Cog): with suppress(discord.NotFound): await message.clear_reaction("πŸ”„") break - else: - try: - await message.edit(embed=self._build_topic_embed(message.channel.id)) - except discord.NotFound: - break - with suppress(discord.NotFound): - await message.remove_reaction(reaction, user) + try: + await message.edit(embed=self._build_topic_embed(message.channel.id)) + except discord.NotFound: + break + + with suppress(discord.NotFound): + await message.remove_reaction(reaction, user) @commands.command() @commands.cooldown(1, 60*2, commands.BucketType.channel) -- cgit v1.2.3 From febe5b9218f44a2c6abfa6355f063471c0593bc9 Mon Sep 17 00:00:00 2001 From: wookie184 Date: Sat, 9 Oct 2021 16:24:42 +0100 Subject: Replace usage of bot.command_prefix with constants.Client.prefix --- bot/exts/fun/trivia_quiz.py | 4 ++-- bot/exts/holidays/halloween/spookynamerate.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bot/exts/fun/trivia_quiz.py b/bot/exts/fun/trivia_quiz.py index 236586b0..712c8a12 100644 --- a/bot/exts/fun/trivia_quiz.py +++ b/bot/exts/fun/trivia_quiz.py @@ -16,7 +16,7 @@ from discord.ext import commands, tasks from rapidfuzz import fuzz from bot.bot import Bot -from bot.constants import Colours, NEGATIVE_REPLIES, Roles +from bot.constants import Client, Colours, NEGATIVE_REPLIES, Roles logger = logging.getLogger(__name__) @@ -332,7 +332,7 @@ class TriviaQuiz(commands.Cog): if self.game_status[ctx.channel.id]: await ctx.send( "Game is already running... " - f"do `{self.bot.command_prefix}quiz stop`" + f"do `{Client.prefix}quiz stop`" ) return diff --git a/bot/exts/holidays/halloween/spookynamerate.py b/bot/exts/holidays/halloween/spookynamerate.py index 2e59d4a8..a3aa4f13 100644 --- a/bot/exts/holidays/halloween/spookynamerate.py +++ b/bot/exts/holidays/halloween/spookynamerate.py @@ -143,7 +143,7 @@ class SpookyNameRate(Cog): if data["author"] == ctx.author.id: await ctx.send( "But you have already added an entry! Type " - f"`{self.bot.command_prefix}spookynamerate " + f"`{Client.prefix}spookynamerate " "delete` to delete it, and then you can add it again" ) return @@ -185,7 +185,7 @@ class SpookyNameRate(Cog): return await ctx.send( - f"But you don't have an entry... :eyes: Type `{self.bot.command_prefix}spookynamerate add your entry`" + f"But you don't have an entry... :eyes: Type `{Client.prefix}spookynamerate add your entry`" ) @Cog.listener() @@ -225,7 +225,7 @@ class SpookyNameRate(Cog): "Okkey... Welcome to the **Spooky Name Rate Game**! It's a relatively simple game.\n" f"Everyday, a random name will be sent in <#{Channels.community_bot_commands}> " "and you need to try and spookify it!\nRegister your name using " - f"`{self.bot.command_prefix}spookynamerate add spookified name`" + f"`{Client.prefix}spookynamerate add spookified name`" ) await self.data.set("first_time", False) -- cgit v1.2.3 From 0c814ceb2da9d1e8ffe49b902c4fbed377038a39 Mon Sep 17 00:00:00 2001 From: Izan Date: Mon, 11 Oct 2021 13:53:29 +0100 Subject: Set `AI.user` to @Sir Lancebot --- bot/exts/fun/tic_tac_toe.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bot/exts/fun/tic_tac_toe.py b/bot/exts/fun/tic_tac_toe.py index 5c4f8051..49620d02 100644 --- a/bot/exts/fun/tic_tac_toe.py +++ b/bot/exts/fun/tic_tac_toe.py @@ -72,7 +72,8 @@ class Player: class AI: """Tic Tac Toe AI class for against computer gaming.""" - def __init__(self, symbol: str): + def __init__(self, ctx: Context, symbol: str): + self.user = ctx.me self.symbol = symbol async def get_move(self, board: dict[int, str], _: discord.Message) -> tuple[bool, int]: @@ -97,8 +98,8 @@ class AI: return False, random.choice(open_edges) def __str__(self) -> str: - """Return `AI` as user name.""" - return "AI" + """Return mention of @Sir Lancebot.""" + return self.user.mention class Game: @@ -265,7 +266,7 @@ class TicTacToe(Cog): return if opponent is None: game = Game( - [Player(ctx.author, ctx, Emojis.x_square), AI(Emojis.o_square)], + [Player(ctx.author, ctx, Emojis.x_square), AI(ctx, Emojis.o_square)], ctx ) else: -- cgit v1.2.3 From 462b83c8150bbc9121d87cf8ee0e61d850776fc6 Mon Sep 17 00:00:00 2001 From: Izan Date: Mon, 11 Oct 2021 14:17:38 +0100 Subject: Add missing `Game.channel` attribute --- bot/exts/fun/tic_tac_toe.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/exts/fun/tic_tac_toe.py b/bot/exts/fun/tic_tac_toe.py index 49620d02..1ebf8d11 100644 --- a/bot/exts/fun/tic_tac_toe.py +++ b/bot/exts/fun/tic_tac_toe.py @@ -108,6 +108,7 @@ class Game: def __init__(self, players: list[Union[Player, AI]], ctx: Context): self.players = players self.ctx = ctx + self.channel = ctx.channel self.board = { 1: Emojis.number_emojis[1], 2: Emojis.number_emojis[2], -- cgit v1.2.3 From 11d00e7f846748fb87f02b5f0bfecc92feac198c Mon Sep 17 00:00:00 2001 From: Izan Date: Fri, 8 Oct 2021 14:45:02 +0100 Subject: Rename `Roles.moderator` to `Roles.moderation_team` --- bot/constants.py | 4 ++-- bot/exts/fun/snakes/_utils.py | 2 +- bot/exts/fun/trivia_quiz.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 6e45632f..c5443393 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -332,8 +332,8 @@ class Reddit: # Default role combinations -MODERATION_ROLES = Roles.moderator, Roles.admin, Roles.owner -STAFF_ROLES = Roles.helpers, Roles.moderator, Roles.admin, Roles.owner +MODERATION_ROLES = Roles.moderation_team, Roles.admin, Roles.owner +STAFF_ROLES = Roles.helpers, Roles.moderation_team, Roles.admin, Roles.owner # Whitelisted channels WHITELISTED_CHANNELS = ( diff --git a/bot/exts/fun/snakes/_utils.py b/bot/exts/fun/snakes/_utils.py index de51339d..46834c2c 100644 --- a/bot/exts/fun/snakes/_utils.py +++ b/bot/exts/fun/snakes/_utils.py @@ -718,4 +718,4 @@ class SnakeAndLaddersGame: @staticmethod def _is_moderator(user: Member) -> bool: """Return True if the user is a Moderator.""" - return any(Roles.moderator == role.id for role in user.roles) + return any(Roles.moderation_team == role.id for role in user.roles) diff --git a/bot/exts/fun/trivia_quiz.py b/bot/exts/fun/trivia_quiz.py index 712c8a12..60afbb06 100644 --- a/bot/exts/fun/trivia_quiz.py +++ b/bot/exts/fun/trivia_quiz.py @@ -550,7 +550,7 @@ class TriviaQuiz(commands.Cog): if self.game_status[ctx.channel.id]: # Check if the author is the game starter or a moderator. if ctx.author == self.game_owners[ctx.channel.id] or any( - Roles.moderator == role.id for role in ctx.author.roles + Roles.moderation_team == role.id for role in ctx.author.roles ): self.game_status[ctx.channel.id] = False del self.game_owners[ctx.channel.id] -- cgit v1.2.3 From 71d800add94df3d678786ab482747ae904290129 Mon Sep 17 00:00:00 2001 From: Izan Date: Fri, 8 Oct 2021 15:04:35 +0100 Subject: Rename `Roles.admin` to `Roles.admins` --- bot/constants.py | 10 +++++----- bot/exts/core/internal_eval/_internal_eval.py | 6 +++--- bot/exts/events/advent_of_code/_cog.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index c5443393..92b40914 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -280,9 +280,9 @@ if Client.month_override is not None: class Roles(NamedTuple): - owner = 267627879762755584 - admin = int(environ.get("BOT_ADMIN_ROLE_ID", 267628507062992896)) - moderator = 267629731250176001 + owners = 267627879762755584 + admins = int(environ.get("BOT_ADMIN_ROLE_ID", 267628507062992896)) + moderation_team = 267629731250176001 helpers = int(environ.get("ROLE_HELPERS", 267630620367257601)) core_developers = 587606783669829632 everyone = int(environ.get("BOT_GUILD", 267624335836053506)) @@ -332,8 +332,8 @@ class Reddit: # Default role combinations -MODERATION_ROLES = Roles.moderation_team, Roles.admin, Roles.owner -STAFF_ROLES = Roles.helpers, Roles.moderation_team, Roles.admin, Roles.owner +MODERATION_ROLES = Roles.moderation_team, Roles.admins, Roles.owner +STAFF_ROLES = Roles.helpers, Roles.moderation_team, Roles.admins, Roles.owner # Whitelisted channels WHITELISTED_CHANNELS = ( diff --git a/bot/exts/core/internal_eval/_internal_eval.py b/bot/exts/core/internal_eval/_internal_eval.py index 4f6b4321..47e564a5 100644 --- a/bot/exts/core/internal_eval/_internal_eval.py +++ b/bot/exts/core/internal_eval/_internal_eval.py @@ -146,14 +146,14 @@ class InternalEval(commands.Cog): await self._send_output(ctx, eval_context.format_output()) @commands.group(name="internal", aliases=("int",)) - @with_role(Roles.admin) + @with_role(Roles.admins) async def internal_group(self, ctx: commands.Context) -> None: """Internal commands. Top secret!""" if not ctx.invoked_subcommand: await invoke_help_command(ctx) @internal_group.command(name="eval", aliases=("e",)) - @with_role(Roles.admin) + @with_role(Roles.admins) async def eval(self, ctx: commands.Context, *, code: str) -> None: """Run eval in a REPL-like format.""" if match := list(FORMATTED_CODE_REGEX.finditer(code)): @@ -172,7 +172,7 @@ class InternalEval(commands.Cog): await self._eval(ctx, code) @internal_group.command(name="reset", aliases=("clear", "exit", "r", "c")) - @with_role(Roles.admin) + @with_role(Roles.admins) async def reset(self, ctx: commands.Context) -> None: """Reset the context and locals of the eval session.""" self.locals = {} diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index ca60e517..4d811fa4 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -251,7 +251,7 @@ class AdventOfCode(commands.Cog): info_embed = _helpers.get_summary_embed(leaderboard) await ctx.send(f"```\n{table}\n```", embed=info_embed) - @with_role(Roles.admin) + @with_role(Roles.admins) @adventofcode_group.command( name="refresh", aliases=("fetch",), -- cgit v1.2.3 From dc5f73be8c18ef11c701822e8c600c8fca7b149c Mon Sep 17 00:00:00 2001 From: Izan Date: Fri, 8 Oct 2021 15:06:07 +0100 Subject: Rename `Roles.owner` to `Roles.owners` --- bot/constants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 92b40914..b4191c5e 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -332,8 +332,8 @@ class Reddit: # Default role combinations -MODERATION_ROLES = Roles.moderation_team, Roles.admins, Roles.owner -STAFF_ROLES = Roles.helpers, Roles.moderation_team, Roles.admins, Roles.owner +MODERATION_ROLES = Roles.moderation_team, Roles.admins, Roles.owners +STAFF_ROLES = Roles.helpers, Roles.moderation_team, Roles.admins, Roles.owners # Whitelisted channels WHITELISTED_CHANNELS = ( -- cgit v1.2.3 From e2da188b554aba07b6c5a7a25b8af0baa74973cb Mon Sep 17 00:00:00 2001 From: Izan Date: Sat, 9 Oct 2021 21:20:46 +0100 Subject: Check role id in MODERATION_ROLES instead of comparing to moderation_team and safeguard `.roles` attribute --- bot/exts/fun/snakes/_utils.py | 25 +++++++++++++------------ bot/exts/fun/trivia_quiz.py | 4 ++-- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/bot/exts/fun/snakes/_utils.py b/bot/exts/fun/snakes/_utils.py index 46834c2c..182fa9d9 100644 --- a/bot/exts/fun/snakes/_utils.py +++ b/bot/exts/fun/snakes/_utils.py @@ -6,13 +6,14 @@ import math import random from itertools import product from pathlib import Path +from typing import Union from PIL import Image from PIL.ImageDraw import ImageDraw -from discord import File, Member, Reaction +from discord import File, Member, Reaction, User from discord.ext.commands import Cog, Context -from bot.constants import Roles +from bot.constants import MODERATION_ROLES SNAKE_RESOURCES = Path("bot/resources/fun/snakes").absolute() @@ -395,7 +396,7 @@ class SnakeAndLaddersGame: Listen for reactions until players have joined, and the game has been started. """ - def startup_event_check(reaction_: Reaction, user_: Member) -> bool: + def startup_event_check(reaction_: Reaction, user_: Union[User, Member]) -> bool: """Make sure that this reaction is what we want to operate on.""" return ( all(( @@ -460,7 +461,7 @@ class SnakeAndLaddersGame: await self.cancel_game() return # We're done, no reactions for the last 5 minutes - async def _add_player(self, user: Member) -> None: + async def _add_player(self, user: Union[User, Member]) -> None: """Add player to game.""" self.players.append(user) self.player_tiles[user.id] = 1 @@ -469,7 +470,7 @@ class SnakeAndLaddersGame: im = Image.open(io.BytesIO(avatar_bytes)).resize((BOARD_PLAYER_SIZE, BOARD_PLAYER_SIZE)) self.avatar_images[user.id] = im - async def player_join(self, user: Member) -> None: + async def player_join(self, user: Union[User, Member]) -> None: """ Handle players joining the game. @@ -495,7 +496,7 @@ class SnakeAndLaddersGame: delete_after=10 ) - async def player_leave(self, user: Member) -> bool: + async def player_leave(self, user: Union[User, Member]) -> bool: """ Handle players leaving the game. @@ -530,7 +531,7 @@ class SnakeAndLaddersGame: await self.channel.send("**Snakes and Ladders**: Game has been canceled.") self._destruct() - async def start_game(self, user: Member) -> None: + async def start_game(self, user: Union[User, Member]) -> None: """ Allow the game author to begin the game. @@ -551,7 +552,7 @@ class SnakeAndLaddersGame: async def start_round(self) -> None: """Begin the round.""" - def game_event_check(reaction_: Reaction, user_: Member) -> bool: + def game_event_check(reaction_: Reaction, user_: Union[User, Member]) -> bool: """Make sure that this reaction is what we want to operate on.""" return ( all(( @@ -644,7 +645,7 @@ class SnakeAndLaddersGame: if not is_surrendered: await self._complete_round() - async def player_roll(self, user: Member) -> None: + async def player_roll(self, user: Union[User, Member]) -> None: """Handle the player's roll.""" if user.id not in self.player_tiles: await self.channel.send(user.mention + " You are not in the match.", delete_after=10) @@ -691,7 +692,7 @@ class SnakeAndLaddersGame: await self.channel.send("**Snakes and Ladders**: " + winner.mention + " has won the game! :tada:") self._destruct() - def _check_winner(self) -> Member: + def _check_winner(self) -> Union[User, Member]: """Return a winning member if we're in the post-round state and there's a winner.""" if self.state != "post_round": return None @@ -716,6 +717,6 @@ class SnakeAndLaddersGame: return x_level, y_level @staticmethod - def _is_moderator(user: Member) -> bool: + def _is_moderator(user: Union[User, Member]) -> bool: """Return True if the user is a Moderator.""" - return any(Roles.moderation_team == role.id for role in user.roles) + return any(role.id in MODERATION_ROLES for role in getattr(user, 'roles', [])) diff --git a/bot/exts/fun/trivia_quiz.py b/bot/exts/fun/trivia_quiz.py index 60afbb06..4a1cec5b 100644 --- a/bot/exts/fun/trivia_quiz.py +++ b/bot/exts/fun/trivia_quiz.py @@ -16,7 +16,7 @@ from discord.ext import commands, tasks from rapidfuzz import fuzz from bot.bot import Bot -from bot.constants import Client, Colours, NEGATIVE_REPLIES, Roles +from bot.constants import Client, Colours, MODERATION_ROLES, NEGATIVE_REPLIES logger = logging.getLogger(__name__) @@ -550,7 +550,7 @@ class TriviaQuiz(commands.Cog): if self.game_status[ctx.channel.id]: # Check if the author is the game starter or a moderator. if ctx.author == self.game_owners[ctx.channel.id] or any( - Roles.moderation_team == role.id for role in ctx.author.roles + role.id in MODERATION_ROLES for role in getattr(ctx.author, 'roles', []) ): self.game_status[ctx.channel.id] = False del self.game_owners[ctx.channel.id] -- cgit v1.2.3 From aa272dbe789e86013969bc6f4e506d70ddfc63be Mon Sep 17 00:00:00 2001 From: Izan Date: Mon, 11 Oct 2021 14:47:16 +0100 Subject: Check role id in STAFF_ROLES instead of comparing to helpers --- bot/exts/events/advent_of_code/_cog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 4d811fa4..0a18c261 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -9,7 +9,7 @@ from discord.ext import commands from bot.bot import Bot from bot.constants import ( - AdventOfCode as AocConfig, Channels, Colours, Emojis, Month, Roles, WHITELISTED_CHANNELS, + AdventOfCode as AocConfig, Channels, Colours, Emojis, Month, Roles, STAFF_ROLES, WHITELISTED_CHANNELS, ) from bot.exts.events.advent_of_code import _helpers from bot.utils.decorators import InChannelCheckFailure, in_month, whitelist_override, with_role @@ -145,7 +145,7 @@ class AdventOfCode(commands.Cog): author = ctx.author log.info(f"{author.name} ({author.id}) has requested a PyDis AoC leaderboard code") - if AocConfig.staff_leaderboard_id and any(r.id == Roles.helpers for r in author.roles): + if AocConfig.staff_leaderboard_id and any(r.id in STAFF_ROLES for r in author.roles): join_code = AocConfig.leaderboards[AocConfig.staff_leaderboard_id].join_code else: try: -- cgit v1.2.3 From a94ce986832694a572ce31b8fedd2b98c2c7747a Mon Sep 17 00:00:00 2001 From: Izan Date: Mon, 11 Oct 2021 14:59:31 +0100 Subject: Make certain AOC commands guild-only --- bot/exts/events/advent_of_code/_cog.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 0a18c261..3bd4873c 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -55,6 +55,7 @@ class AdventOfCode(commands.Cog): if not ctx.invoked_subcommand: await invoke_help_command(ctx) + @commands.guild_only() @adventofcode_group.command( name="subscribe", aliases=("sub", "notifications", "notify", "notifs"), @@ -84,6 +85,7 @@ class AdventOfCode(commands.Cog): ) @in_month(Month.DECEMBER) + @commands.guild_only() @adventofcode_group.command(name="unsubscribe", aliases=("unsub",), brief="Notifications for new days") @whitelist_override(channels=AOC_WHITELIST) async def aoc_unsubscribe(self, ctx: commands.Context) -> None: @@ -133,6 +135,7 @@ class AdventOfCode(commands.Cog): """Respond with an explanation of all things Advent of Code.""" await ctx.send(embed=self.cached_about_aoc) + @commands.guild_only() @adventofcode_group.command(name="join", aliases=("j",), brief="Learn how to join the leaderboard (via DM)") @whitelist_override(channels=AOC_WHITELIST) async def join_leaderboard(self, ctx: commands.Context) -> None: -- cgit v1.2.3 From b0be25bed15bbfc88d61e2e842b9f894c9cac15c Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 13 Oct 2021 20:44:07 +0100 Subject: update advent of code channel IDs We deleted and re-made the channels so new IDs are needed. --- bot/constants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 6e45632f..567daadd 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -101,8 +101,8 @@ class Cats: class Channels(NamedTuple): - advent_of_code = int(environ.get("AOC_CHANNEL_ID", 782715290437943306)) - advent_of_code_commands = int(environ.get("AOC_COMMANDS_CHANNEL_ID", 607247579608121354)) + advent_of_code = int(environ.get("AOC_CHANNEL_ID", 897932085766004786)) + advent_of_code_commands = int(environ.get("AOC_COMMANDS_CHANNEL_ID", 897932607545823342)) bot = 267659945086812160 organisation = 551789653284356126 devlog = int(environ.get("CHANNEL_DEVLOG", 622895325144940554)) -- cgit v1.2.3 From a0c86e72c0a418f1265a1aa035b45048d8e921ec Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 13 Oct 2021 17:23:30 -0400 Subject: Challenges (#860) * beginning commit creating the base of the hangman, code needs to be linted in the future * updated words list * adding images to show the hangman person * added images, though it is a bit laggy * replacing images with discord attachment urls * adding error if filters aren't found * fixing typo in ``filter_not_found_embed`` * final lints + removing `mode` parameter as it renders useless * linting flake8 errors * adding newline at the end of `top_1000_used_words.txt` * minor change to filter message * beginning commit -- trying to add bs4 to pyproject.toml, though it is currently failing * kata information section done, ready for issue * fixing bugs with the query not being fully picked up, also allowing query only with no kyu * fixing bug where user cannot leave all arguments blank * typo - forgot unary before the level within the `language and not query` if statement * changing to random kata chosen * ensuring that if the user provides a query that won't work, that it won't error out * limiting choice to smaller numbers if a query was provided, so the user gets what they want * improving hangman docstring * removing `bot/resources/evergreen/hangman` directory as file attachments are used * replacing single quotes with double quotes, to adhere to the style guide. * fixing style inconsistencies and other problems with how the code looks - as per requested by Objectivix * fixing `IMAGES` style inconsistency * adding trailing commas and switching to `Colours` for consistency * adding trailing commas and switching to `Colours` for consistency * fixing the remnants of non-trailing commas and allowing specification for single player vs mulitplayer * removing all 2 letter words from the hangman word choosing and removing words that @Objectivix found that shouldn't be in the list of words * removing some inappropriate words from the txt file * Adding space for grammatical errors Co-authored-by: ChrisJL * changing two periods to a full stop & wrapping try and except block to only the part that can raise it * using negative replies instead along with fixing grammatical errors in the sentence * removing words that could be considered inappropirate * removing `TOP_WORDS_FILE_PATH` and making `ALL_WORDS` a global variable. * error handling * fixing the overcomplication of the bs4 portion * adding button and dropdowns to the challenges command * more specific docstring * more specific docstring * finishing dropdowns/buttons * putting the dropdown on top of the link button * replacing ' with a double quote for some strings * Removing more words The words removed shouldn't really belong here * Update bot/exts/utilities/challenges.py Co-authored-by: Bluenix * replacing mapping_of_images with IMAGES and other fixes * Dedenting Co-authored-by: Bluenix * Improving tries logic Co-authored-by: Bluenix * Updating `positions` list to set Co-authored-by: Bluenix * Updating setup docstring Co-authored-by: Bluenix * Updating comment in callback function of the dropdown Co-authored-by: Bluenix * fixing too many blank lines * Hardcode dictionary Co-authored-by: Bluenix * restructuring * fixing errors * Remove unnecessary comments Co-authored-by: Bluenix * Remove unnecessary comments Co-authored-by: Bluenix * Improve comment explanation Co-authored-by: Bluenix * Remove redundant extra membership test Co-authored-by: Bluenix * Removing verbose variable definition Co-authored-by: Bluenix * Redundant list Co-authored-by: Bluenix * Shorten 'social distancing' (too many separations) between related lines Co-authored-by: Bluenix * improving docstring in `kata_id` * sending embed if error occurs with api or bs4, also hardcoding params dictionary * Better comments Co-authored-by: Bluenix * better docstring Co-authored-by: Bluenix * Removing f-string inception and replacing it with more readable code Co-authored-by: Bluenix * More specific docstring Co-authored-by: Bluenix * Removing redundant comments Co-authored-by: Bluenix * Fixing linting errors * mapping of kyu -> constant * adding trailing comma * specific comment regarding where colors are from for `MAPPING_OF_KYU` * changing name to link too along with link button * adding ellipsis to make it more clear for `Read more` * removing redundant sentences from all docstrings of embed creator functions * fixing unboundlocalerror due to kata_url only being defined under a certain condition * only allowing supported languages on codewars.com * fixing url glitch with embed * Delete hangman.py * Delete top_1000_used_words.txt * hangman dependencies leaked into this PR, removing them * add bs4 and lxml back to lock file * Capitalize comments Co-authored-by: Bluenix * Improving comments (capitalization) Co-authored-by: Bluenix * polishing * explaining that self.original_message will be set later in the callback function of the dropdown * fixing nitpicks * cast to integer from hex * removing unnecessary trailing commas * Simplifying L274-L276 Co-authored-by: Bluenix * Add ellipsis to end of description if it's too long Co-authored-by: Bluenix * Changing to hex Co-authored-by: Bluenix * Running blocking function (BeautifulSoup.find_all) to thread Co-authored-by: Bluenix * logger.error errors * Fixing error with to_thread * Fixing errors with MAPPING_OF_KYU Co-authored-by: Bluenix * changing `query` to `-query` if the query is a kata level * changing embed names to add the kata name * Mimicking mailing list's behavior Co-authored-by: Bluenix * url attribute for all embeds & title for all embeds * remove view after a certain amount of tikme * disabling view after waiting instead of just editing it out * styling * remove view to avoid spamming errors * changing `logger` to `log` Co-authored-by: Xithrius <15021300+Xithrius@users.noreply.github.com> * Change `logger` to `log` for logging errors Co-authored-by: ChrisJL Co-authored-by: Bluenix Co-authored-by: Xithrius <15021300+Xithrius@users.noreply.github.com> --- bot/exts/utilities/challenges.py | 335 +++++++++++++++++++++++++++++++++++++++ poetry.lock | 98 +++++++++++- pyproject.toml | 2 + 3 files changed, 434 insertions(+), 1 deletion(-) create mode 100644 bot/exts/utilities/challenges.py diff --git a/bot/exts/utilities/challenges.py b/bot/exts/utilities/challenges.py new file mode 100644 index 00000000..234eb0be --- /dev/null +++ b/bot/exts/utilities/challenges.py @@ -0,0 +1,335 @@ +import logging +from asyncio import to_thread +from random import choice +from typing import Union + +from bs4 import BeautifulSoup +from discord import Embed, Interaction, SelectOption, ui +from discord.ext import commands + +from bot.bot import Bot +from bot.constants import Colours, Emojis, NEGATIVE_REPLIES + +log = logging.getLogger(__name__) +API_ROOT = "https://www.codewars.com/api/v1/code-challenges/{kata_id}" + +# Map difficulty for the kata to color we want to display in the embed. +# These colors are representative of the colors that each kyu's level represents on codewars.com +MAPPING_OF_KYU = { + 8: 0xdddbda, 7: 0xdddbda, 6: 0xecb613, 5: 0xecb613, + 4: 0x3c7ebb, 3: 0x3c7ebb, 2: 0x866cc7, 1: 0x866cc7 +} + +# Supported languages for a kata on codewars.com +SUPPORTED_LANGUAGES = { + "stable": [ + "c", "c#", "c++", "clojure", "coffeescript", "coq", "crystal", "dart", "elixir", + "f#", "go", "groovy", "haskell", "java", "javascript", "kotlin", "lean", "lua", "nasm", + "php", "python", "racket", "ruby", "rust", "scala", "shell", "sql", "swift", "typescript" + ], + "beta": [ + "agda", "bf", "cfml", "cobol", "commonlisp", "elm", "erlang", "factor", + "forth", "fortran", "haxe", "idris", "julia", "nim", "objective-c", "ocaml", + "pascal", "perl", "powershell", "prolog", "purescript", "r", "raku", "reason", "solidity", "vb.net" + ] +} + + +class InformationDropdown(ui.Select): + """A dropdown inheriting from ui.Select that allows finding out other information about the kata.""" + + def __init__(self, language_embed: Embed, tags_embed: Embed, other_info_embed: Embed, main_embed: Embed): + options = [ + SelectOption( + label="Main Information", + description="See the kata's difficulty, description, etc.", + emoji="🌎" + ), + SelectOption( + label="Languages", + description="See what languages this kata supports!", + emoji=Emojis.reddit_post_text + ), + SelectOption( + label="Tags", + description="See what categories this kata falls under!", + emoji=Emojis.stackoverflow_tag + ), + SelectOption( + label="Other Information", + description="See how other people performed on this kata and more!", + emoji="β„Ή" + ) + ] + + # We map the option label to the embed instance so that it can be easily looked up later in O(1) + self.mapping_of_embeds = { + "Main Information": main_embed, + "Languages": language_embed, + "Tags": tags_embed, + "Other Information": other_info_embed, + } + + super().__init__( + placeholder="See more information regarding this kata", + min_values=1, + max_values=1, + options=options + ) + + async def callback(self, interaction: Interaction) -> None: + """Callback for when someone clicks on a dropdown.""" + # Edit the message to the embed selected in the option + # The `original_message` attribute is set just after the message is sent with the view. + # The attribute is not set during initialization. + result_embed = self.mapping_of_embeds[self.values[0]] + await self.original_message.edit(embed=result_embed) + + +class Challenges(commands.Cog): + """ + Cog for the challenge command. + + The challenge command pulls a random kata from codewars.com. + A kata is the name for a challenge, specific to codewars.com. + + The challenge command also has filters to customize the kata that is given. + You can specify the language the kata should be from, difficulty and topic of the kata. + """ + + def __init__(self, bot: Bot): + self.bot = bot + + async def kata_id(self, search_link: str, params: dict) -> Union[str, Embed]: + """ + Uses bs4 to get the HTML code for the page of katas, where the page is the link of the formatted `search_link`. + + This will webscrape the search page with `search_link` and then get the ID of a kata for the + codewars.com API to use. + """ + async with self.bot.http_session.get(search_link, params=params) as response: + if response.status != 200: + error_embed = Embed( + title=choice(NEGATIVE_REPLIES), + description="We ran into an error when getting the kata from codewars.com, try again later.", + color=Colours.soft_red + ) + log.error(f"Unexpected response from codewars.com, status code: {response.status}") + return error_embed + + soup = BeautifulSoup(await response.text(), features="lxml") + first_kata_div = await to_thread(soup.find_all, "div", class_="item-title px-0") + + if not first_kata_div: + raise commands.BadArgument("No katas could be found with the filters provided.") + elif len(first_kata_div) >= 3: + first_kata_div = choice(first_kata_div[:3]) + elif "q=" not in search_link: + first_kata_div = choice(first_kata_div) + else: + first_kata_div = first_kata_div[0] + + # There are numerous divs before arriving at the id of the kata, which can be used for the link. + first_kata_id = first_kata_div.a["href"].split("/")[-1] + return first_kata_id + + async def kata_information(self, kata_id: str) -> Union[dict, Embed]: + """ + Returns the information about the Kata. + + Uses the codewars.com API to get information about the kata using `kata_id`. + """ + async with self.bot.http_session.get(API_ROOT.format(kata_id=kata_id)) as response: + if response.status != 200: + error_embed = Embed( + title=choice(NEGATIVE_REPLIES), + description="We ran into an error when getting the kata information, try again later.", + color=Colours.soft_red + ) + log.error(f"Unexpected response from codewars.com/api/v1, status code: {response.status}") + return error_embed + + return await response.json() + + @staticmethod + def main_embed(kata_information: dict) -> Embed: + """Creates the main embed which displays the name, difficulty and description of the kata.""" + kata_description = kata_information["description"] + kata_url = f"https://codewars.com/kata/{kata_information['id']}" + + # Ensuring it isn't over the length 1024 + if len(kata_description) > 1024: + kata_description = "\n".join(kata_description[:1000].split("\n")[:-1]) + "..." + kata_description += f" [continue reading]({kata_url})" + + kata_embed = Embed( + title=kata_information["name"], + description=kata_description, + color=MAPPING_OF_KYU[int(kata_information["rank"]["name"].replace(" kyu", ""))], + url=kata_url + ) + kata_embed.add_field(name="Difficulty", value=kata_information["rank"]["name"], inline=False) + return kata_embed + + @staticmethod + def language_embed(kata_information: dict) -> Embed: + """Creates the 'language embed' which displays all languages the kata supports.""" + kata_url = f"https://codewars.com/kata/{kata_information['id']}" + + languages = "\n".join(map(str.title, kata_information["languages"])) + language_embed = Embed( + title=kata_information["name"], + description=f"```yaml\nSupported Languages:\n{languages}\n```", + color=Colours.python_blue, + url=kata_url + ) + return language_embed + + @staticmethod + def tags_embed(kata_information: dict) -> Embed: + """ + Creates the 'tags embed' which displays all the tags of the Kata. + + Tags explain what the kata is about, this is what codewars.com calls categories. + """ + kata_url = f"https://codewars.com/kata/{kata_information['id']}" + + tags = "\n".join(kata_information["tags"]) + tags_embed = Embed( + title=kata_information["name"], + description=f"```yaml\nTags:\n{tags}\n```", + color=Colours.grass_green, + url=kata_url + ) + return tags_embed + + @staticmethod + def miscellaneous_embed(kata_information: dict) -> Embed: + """ + Creates the 'other information embed' which displays miscellaneous information about the kata. + + This embed shows statistics such as the total number of people who completed the kata, + the total number of stars of the kata, etc. + """ + kata_url = f"https://codewars.com/kata/{kata_information['id']}" + + embed = Embed( + title=kata_information["name"], + description="```nim\nOther Information\n```", + color=Colours.grass_green, + url=kata_url + ) + embed.add_field( + name="`Total Score`", + value=f"```css\n{kata_information['voteScore']}\n```", + inline=False + ) + embed.add_field( + name="`Total Stars`", + value=f"```css\n{kata_information['totalStars']}\n```", + inline=False + ) + embed.add_field( + name="`Total Completed`", + value=f"```css\n{kata_information['totalCompleted']}\n```", + inline=False + ) + embed.add_field( + name="`Total Attempts`", + value=f"```css\n{kata_information['totalAttempts']}\n```", + inline=False + ) + return embed + + @staticmethod + def create_view(dropdown: InformationDropdown, link: str) -> ui.View: + """ + Creates the discord.py View for the Discord message components (dropdowns and buttons). + + The discord UI is implemented onto the embed, where the user can choose what information about the kata they + want, along with a link button to the kata itself. + """ + view = ui.View() + view.add_item(dropdown) + view.add_item(ui.Button(label="View the Kata", url=link)) + return view + + @commands.command(aliases=["kata"]) + @commands.cooldown(1, 5, commands.BucketType.user) + async def challenge(self, ctx: commands.Context, language: str = "python", *, query: str = None) -> None: + """ + The challenge command pulls a random kata (challenge) from codewars.com. + + The different ways to use this command are: + `.challenge ` - Pulls a random challenge within that language's scope. + `.challenge ` - The difficulty can be from 1-8, + 1 being the hardest, 8 being the easiest. This pulls a random challenge within that difficulty & language. + `.challenge ` - Pulls a random challenge with the query provided under the language + `.challenge , ` - Pulls a random challenge with the query provided, + under that difficulty within the language's scope. + """ + if language.lower() not in SUPPORTED_LANGUAGES["stable"] + SUPPORTED_LANGUAGES["beta"]: + raise commands.BadArgument("This is not a recognized language on codewars.com!") + + get_kata_link = f"https://codewars.com/kata/search/{language}" + params = {} + + if language and not query: + level = f"-{choice([1, 2, 3, 4, 5, 6, 7, 8])}" + params["r[]"] = level + elif "," in query: + query_splitted = query.split("," if ", " not in query else ", ") + + if len(query_splitted) > 2: + raise commands.BadArgument( + "There can only be one comma within the query, separating the difficulty and the query itself." + ) + + query, level = query_splitted + params["q"] = query + params["r[]"] = f"-{level}" + elif query.isnumeric(): + params["r[]"] = f"-{query}" + else: + params["q"] = query + + params["beta"] = str(language in SUPPORTED_LANGUAGES["beta"]).lower() + + first_kata_id = await self.kata_id(get_kata_link, params) + if isinstance(first_kata_id, Embed): + # We ran into an error when retrieving the website link + await ctx.send(embed=first_kata_id) + return + + kata_information = await self.kata_information(first_kata_id) + if isinstance(kata_information, Embed): + # Something went wrong when trying to fetch the kata information + await ctx.d(embed=kata_information) + return + + kata_embed = self.main_embed(kata_information) + language_embed = self.language_embed(kata_information) + tags_embed = self.tags_embed(kata_information) + miscellaneous_embed = self.miscellaneous_embed(kata_information) + + dropdown = InformationDropdown( + main_embed=kata_embed, + language_embed=language_embed, + tags_embed=tags_embed, + other_info_embed=miscellaneous_embed + ) + kata_view = self.create_view(dropdown, f"https://codewars.com/kata/{first_kata_id}") + original_message = await ctx.send( + embed=kata_embed, + view=kata_view + ) + dropdown.original_message = original_message + + wait_for_kata = await kata_view.wait() + if wait_for_kata: + await original_message.edit(embed=kata_embed, view=None) + + +def setup(bot: Bot) -> None: + """Load the Challenges cog.""" + bot.add_cog(Challenges(bot)) diff --git a/poetry.lock b/poetry.lock index 289f2039..21373a92 100644 --- a/poetry.lock +++ b/poetry.lock @@ -100,6 +100,21 @@ python-versions = ">=2.7" docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] +[[package]] +name = "beautifulsoup4" +version = "4.10.0" +description = "Screen-scraping library" +category = "main" +optional = false +python-versions = ">3.0.0" + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +html5lib = ["html5lib"] +lxml = ["lxml"] + [[package]] name = "certifi" version = "2021.5.30" @@ -173,6 +188,7 @@ voice = ["PyNaCl (>=1.3.0,<1.5)"] [package.source] type = "url" url = "https://github.com/Rapptz/discord.py/archive/45d498c1b76deaf3b394d17ccf56112fa691d160.zip" + [[package]] name = "distlib" version = "0.3.2" @@ -355,6 +371,20 @@ category = "main" optional = false python-versions = ">=3.7" +[[package]] +name = "lxml" +version = "4.6.3" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["beautifulsoup4"] +source = ["Cython (>=0.29.7)"] + [[package]] name = "matplotlib" version = "3.4.3" @@ -653,6 +683,14 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "soupsieve" +version = "2.2.1" +description = "A modern CSS selector implementation for Beautiful Soup." +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "taskipy" version = "1.8.1" @@ -730,7 +768,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "9efbf6be5298ab8ace2588e218be309e105987bfdfa8317453d584a1faac4934" +content-hash = "6cced4e3fff83ad6ead9a18b3f585b83426fab34f6e2bcf2466c2ebbbf66dac4" [metadata.files] aiodns = [ @@ -800,6 +838,10 @@ attrs = [ {file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"}, {file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"}, ] +beautifulsoup4 = [ + {file = "beautifulsoup4-4.10.0-py3-none-any.whl", hash = "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf"}, + {file = "beautifulsoup4-4.10.0.tar.gz", hash = "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891"}, +] certifi = [ {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, @@ -1016,6 +1058,56 @@ kiwisolver = [ {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:bcadb05c3d4794eb9eee1dddf1c24215c92fb7b55a80beae7a60530a91060560"}, {file = "kiwisolver-1.3.2.tar.gz", hash = "sha256:fc4453705b81d03568d5b808ad8f09c77c47534f6ac2e72e733f9ca4714aa75c"}, ] +lxml = [ + {file = "lxml-4.6.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2"}, + {file = "lxml-4.6.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f"}, + {file = "lxml-4.6.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d"}, + {file = "lxml-4.6.3-cp27-cp27m-win32.whl", hash = "sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106"}, + {file = "lxml-4.6.3-cp27-cp27m-win_amd64.whl", hash = "sha256:8157dadbb09a34a6bd95a50690595e1fa0af1a99445e2744110e3dca7831c4ee"}, + {file = "lxml-4.6.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7728e05c35412ba36d3e9795ae8995e3c86958179c9770e65558ec3fdfd3724f"}, + {file = "lxml-4.6.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4"}, + {file = "lxml-4.6.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:64812391546a18896adaa86c77c59a4998f33c24788cadc35789e55b727a37f4"}, + {file = "lxml-4.6.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c1a40c06fd5ba37ad39caa0b3144eb3772e813b5fb5b084198a985431c2f1e8d"}, + {file = "lxml-4.6.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51"}, + {file = "lxml-4.6.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f90ba11136bfdd25cae3951af8da2e95121c9b9b93727b1b896e3fa105b2f586"}, + {file = "lxml-4.6.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:4c61b3a0db43a1607d6264166b230438f85bfed02e8cff20c22e564d0faff354"}, + {file = "lxml-4.6.3-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:5c8c163396cc0df3fd151b927e74f6e4acd67160d6c33304e805b84293351d16"}, + {file = "lxml-4.6.3-cp35-cp35m-win32.whl", hash = "sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2"}, + {file = "lxml-4.6.3-cp35-cp35m-win_amd64.whl", hash = "sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4"}, + {file = "lxml-4.6.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4"}, + {file = "lxml-4.6.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3"}, + {file = "lxml-4.6.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d"}, + {file = "lxml-4.6.3-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d916d31fd85b2f78c76400d625076d9124de3e4bda8b016d25a050cc7d603f24"}, + {file = "lxml-4.6.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:820628b7b3135403540202e60551e741f9b6d3304371712521be939470b454ec"}, + {file = "lxml-4.6.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:c47ff7e0a36d4efac9fd692cfa33fbd0636674c102e9e8d9b26e1b93a94e7617"}, + {file = "lxml-4.6.3-cp36-cp36m-win32.whl", hash = "sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04"}, + {file = "lxml-4.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:92e821e43ad382332eade6812e298dc9701c75fe289f2a2d39c7960b43d1e92a"}, + {file = "lxml-4.6.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654"}, + {file = "lxml-4.6.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0"}, + {file = "lxml-4.6.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3"}, + {file = "lxml-4.6.3-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:36108c73739985979bf302006527cf8a20515ce444ba916281d1c43938b8bb96"}, + {file = "lxml-4.6.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2"}, + {file = "lxml-4.6.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:cdaf11d2bd275bf391b5308f86731e5194a21af45fbaaaf1d9e8147b9160ea92"}, + {file = "lxml-4.6.3-cp37-cp37m-win32.whl", hash = "sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade"}, + {file = "lxml-4.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b"}, + {file = "lxml-4.6.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa"}, + {file = "lxml-4.6.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a"}, + {file = "lxml-4.6.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927"}, + {file = "lxml-4.6.3-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:e1cbd3f19a61e27e011e02f9600837b921ac661f0c40560eefb366e4e4fb275e"}, + {file = "lxml-4.6.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791"}, + {file = "lxml-4.6.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:1b38116b6e628118dea5b2186ee6820ab138dbb1e24a13e478490c7db2f326ae"}, + {file = "lxml-4.6.3-cp38-cp38-win32.whl", hash = "sha256:89b8b22a5ff72d89d48d0e62abb14340d9e99fd637d046c27b8b257a01ffbe28"}, + {file = "lxml-4.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7"}, + {file = "lxml-4.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0"}, + {file = "lxml-4.6.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1"}, + {file = "lxml-4.6.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f8380c03e45cf09f8557bdaa41e1fa7c81f3ae22828e1db470ab2a6c96d8bc23"}, + {file = "lxml-4.6.3-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:3082c518be8e97324390614dacd041bb1358c882d77108ca1957ba47738d9d59"}, + {file = "lxml-4.6.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:884ab9b29feaca361f7f88d811b1eea9bfca36cf3da27768d28ad45c3ee6f969"}, + {file = "lxml-4.6.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:6f12e1427285008fd32a6025e38e977d44d6382cf28e7201ed10d6c1698d2a9a"}, + {file = "lxml-4.6.3-cp39-cp39-win32.whl", hash = "sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f"}, + {file = "lxml-4.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83"}, + {file = "lxml-4.6.3.tar.gz", hash = "sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468"}, +] matplotlib = [ {file = "matplotlib-3.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c988bb43414c7c2b0a31bd5187b4d27fd625c080371b463a6d422047df78913"}, {file = "matplotlib-3.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f1c5efc278d996af8a251b2ce0b07bbeccb821f25c8c9846bdcb00ffc7f158aa"}, @@ -1401,6 +1493,10 @@ sortedcontainers = [ {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] +soupsieve = [ + {file = "soupsieve-2.2.1-py3-none-any.whl", hash = "sha256:c2c1c2d44f158cdbddab7824a9af8c4f83c76b1e23e049479aa432feb6c4c23b"}, + {file = "soupsieve-2.2.1.tar.gz", hash = "sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc"}, +] taskipy = [ {file = "taskipy-1.8.1-py3-none-any.whl", hash = "sha256:2b98f499966e40175d1f1306a64587f49dfa41b90d0d86c8f28b067cc58d0a56"}, {file = "taskipy-1.8.1.tar.gz", hash = "sha256:7a2404125817e45d80e13fa663cae35da6e8ba590230094e815633653e25f98f"}, diff --git a/pyproject.toml b/pyproject.toml index 7848f593..08287b23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,12 +12,14 @@ aiodns = "~=2.0" aioredis = "~1.3" rapidfuzz = "~=1.4" arrow = "~=1.1.0" +beautifulsoup4 = "~=4.9" pillow = "~=8.1" sentry-sdk = "~=0.19" PyYAML = "~=5.4" async-rediscache = {extras = ["fakeredis"], version = "~=0.1.4"} emojis = "~=0.6.0" matplotlib = "~=3.4.1" +lxml = "~=4.4" [tool.poetry.dev-dependencies] flake8 = "~=3.8" -- cgit v1.2.3 From bbef4f27a5ebade439e7073f66cac20977b18472 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 13 Oct 2021 18:53:34 -0400 Subject: fixing errors --- bot/exts/utilities/challenges.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/bot/exts/utilities/challenges.py b/bot/exts/utilities/challenges.py index 234eb0be..ff9394ce 100644 --- a/bot/exts/utilities/challenges.py +++ b/bot/exts/utilities/challenges.py @@ -162,10 +162,16 @@ class Challenges(commands.Cog): kata_description = "\n".join(kata_description[:1000].split("\n")[:-1]) + "..." kata_description += f" [continue reading]({kata_url})" + if kata_information["rank"]["name"] is None: + embed_color = 8 + kata_information["rank"]["name"] = "Unable to retrieve difficulty for beta languages." + else: + embed_color = int(kata_information["rank"]["name"].replace(" kyu", "")) + kata_embed = Embed( title=kata_information["name"], description=kata_description, - color=MAPPING_OF_KYU[int(kata_information["rank"]["name"].replace(" kyu", ""))], + color=MAPPING_OF_KYU[embed_color], url=kata_url ) kata_embed.add_field(name="Difficulty", value=kata_information["rank"]["name"], inline=False) @@ -275,8 +281,7 @@ class Challenges(commands.Cog): params = {} if language and not query: - level = f"-{choice([1, 2, 3, 4, 5, 6, 7, 8])}" - params["r[]"] = level + pass elif "," in query: query_splitted = query.split("," if ", " not in query else ", ") -- cgit v1.2.3 From 6eecd5c251da804b38e5521ebd562aa25e6bc61d Mon Sep 17 00:00:00 2001 From: aru Date: Wed, 13 Oct 2021 20:24:20 -0400 Subject: Fix GH-907 Issues can have empty bodies, in this case GitHub doesn't include the key in the API response --- bot/exts/events/hacktoberfest/hacktober-issue-finder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/events/hacktoberfest/hacktober-issue-finder.py b/bot/exts/events/hacktoberfest/hacktober-issue-finder.py index 088e7e43..1774564b 100644 --- a/bot/exts/events/hacktoberfest/hacktober-issue-finder.py +++ b/bot/exts/events/hacktoberfest/hacktober-issue-finder.py @@ -100,7 +100,8 @@ class HacktoberIssues(commands.Cog): """Format the issue data into a embed.""" title = issue["title"] issue_url = issue["url"].replace("api.", "").replace("/repos/", "/") - body = issue["body"] + # issues can have empty bodies, which in that case GitHub doesn't include the key in the API response + body = issue.get("body", "") labels = [label["name"] for label in issue["labels"]] embed = discord.Embed(title=title) -- cgit v1.2.3 From 196c451c867bc78beab2492b963b7d9a1f6a13f5 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Thu, 14 Oct 2021 06:37:27 -0400 Subject: requested changes from TizzySaurus implemented --- bot/exts/utilities/challenges.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/bot/exts/utilities/challenges.py b/bot/exts/utilities/challenges.py index ff9394ce..d3ef82a0 100644 --- a/bot/exts/utilities/challenges.py +++ b/bot/exts/utilities/challenges.py @@ -164,9 +164,10 @@ class Challenges(commands.Cog): if kata_information["rank"]["name"] is None: embed_color = 8 - kata_information["rank"]["name"] = "Unable to retrieve difficulty for beta languages." + kata_difficulty = "Unable to retrieve difficulty for beta languages." else: embed_color = int(kata_information["rank"]["name"].replace(" kyu", "")) + kata_difficulty = kata_information["rank"]["name"] kata_embed = Embed( title=kata_information["name"], @@ -174,7 +175,7 @@ class Challenges(commands.Cog): color=MAPPING_OF_KYU[embed_color], url=kata_url ) - kata_embed.add_field(name="Difficulty", value=kata_information["rank"]["name"], inline=False) + kata_embed.add_field(name="Difficulty", value=kata_difficulty, inline=False) return kata_embed @staticmethod @@ -280,9 +281,7 @@ class Challenges(commands.Cog): get_kata_link = f"https://codewars.com/kata/search/{language}" params = {} - if language and not query: - pass - elif "," in query: + if query is not None and "," in query: query_splitted = query.split("," if ", " not in query else ", ") if len(query_splitted) > 2: @@ -293,9 +292,9 @@ class Challenges(commands.Cog): query, level = query_splitted params["q"] = query params["r[]"] = f"-{level}" - elif query.isnumeric(): + elif query is not None and query.isnumeric(): params["r[]"] = f"-{query}" - else: + elif query is not None: params["q"] = query params["beta"] = str(language in SUPPORTED_LANGUAGES["beta"]).lower() -- cgit v1.2.3 From 3ff315f34721bca59b0820207da6bdd748155293 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Thu, 14 Oct 2021 06:51:23 -0400 Subject: removed repeating query is None --- bot/exts/utilities/challenges.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/bot/exts/utilities/challenges.py b/bot/exts/utilities/challenges.py index d3ef82a0..e4738455 100644 --- a/bot/exts/utilities/challenges.py +++ b/bot/exts/utilities/challenges.py @@ -281,21 +281,22 @@ class Challenges(commands.Cog): get_kata_link = f"https://codewars.com/kata/search/{language}" params = {} - if query is not None and "," in query: - query_splitted = query.split("," if ", " not in query else ", ") - - if len(query_splitted) > 2: - raise commands.BadArgument( - "There can only be one comma within the query, separating the difficulty and the query itself." - ) - - query, level = query_splitted - params["q"] = query - params["r[]"] = f"-{level}" - elif query is not None and query.isnumeric(): - params["r[]"] = f"-{query}" - elif query is not None: - params["q"] = query + if query is not None: + if "," in query: + query_splitted = query.split("," if ", " not in query else ", ") + + if len(query_splitted) > 2: + raise commands.BadArgument( + "There can only be one comma within the query, separating the difficulty and the query itself." + ) + + query, level = query_splitted + params["q"] = query + params["r[]"] = f"-{level}" + elif query.isnumeric(): + params["r[]"] = f"-{query}" + else: + params["q"] = query params["beta"] = str(language in SUPPORTED_LANGUAGES["beta"]).lower() -- cgit v1.2.3 From f18e9c3dda721b9bbbba884e31b34e4ae2831ffc Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Thu, 14 Oct 2021 21:55:41 +0200 Subject: Add support to query AoC results in respect of days and stars (#857) * Add support to query AoC results in respect of days and stars From now on the AoC leaderboard command accepts a total of 2 optional arguments a day and star string (eg.: 1-2, for the second star of the first day) and a number of results they would like to see, with a total maximum of 15. This commit also introduces a few minor fixes in the AoC helper. * Improve overall code consitency in the AoC event Cog and helpers * Improve indenting and code consistency in the AoC cog * Improve code transparency in the AoC helpers * Patch various inconsistencies in the AoC cog and helpers * Migrate AoC Day and Star statistics filtering to Dropdowns From now on when the AoC leadearboard command is used with the DayAndStar argument(bool) the bot will send a View with two dropdowns and a button to Fetch the data based on the value of the Dropdowns. * Improve code and comment consistency in the AoC views and helpers * Patch logic errors, improve consistency in the AoC cog and view. * Add support to delete view from the message after timeout in the AoC cog * Move the day_and_star logic out of the typing context manager in the AoC cog * Revert season-locker in the AoC cog * Improve overall code transparency and indenting in the AoC cog and views * Remove unnecessary returns in the AoC cog and view --- bot/constants.py | 1 + bot/exts/events/advent_of_code/_cog.py | 45 ++++++++++++-- bot/exts/events/advent_of_code/_helpers.py | 9 ++- .../events/advent_of_code/views/dayandstarview.py | 71 ++++++++++++++++++++++ 4 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 bot/exts/events/advent_of_code/views/dayandstarview.py diff --git a/bot/constants.py b/bot/constants.py index 567daadd..0720dd20 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -88,6 +88,7 @@ class AdventOfCode: ignored_days = environ.get("AOC_IGNORED_DAYS", "").split(",") leaderboard_displayed_members = 10 leaderboard_cache_expiry_seconds = 1800 + max_day_and_star_results = 15 year = int(environ.get("AOC_YEAR", datetime.utcnow().year)) role_id = int(environ.get("AOC_ROLE_ID", 518565788744024082)) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index ca60e517..7dd967ec 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -2,6 +2,7 @@ import json import logging from datetime import datetime, timedelta from pathlib import Path +from typing import Optional import arrow import discord @@ -12,6 +13,7 @@ from bot.constants import ( AdventOfCode as AocConfig, Channels, Colours, Emojis, Month, Roles, WHITELISTED_CHANNELS, ) from bot.exts.events.advent_of_code import _helpers +from bot.exts.events.advent_of_code.views.dayandstarview import AoCDropdownView from bot.utils.decorators import InChannelCheckFailure, in_month, whitelist_override, with_role from bot.utils.extensions import invoke_help_command @@ -150,7 +152,7 @@ class AdventOfCode(commands.Cog): else: try: join_code = await _helpers.get_public_join_code(author) - except _helpers.FetchingLeaderboardFailed: + except _helpers.FetchingLeaderboardFailedError: await ctx.send(":x: Failed to get join code! Notified maintainers.") return @@ -185,14 +187,29 @@ class AdventOfCode(commands.Cog): brief="Get a snapshot of the PyDis private AoC leaderboard", ) @whitelist_override(channels=AOC_WHITELIST_RESTRICTED) - async def aoc_leaderboard(self, ctx: commands.Context) -> None: - """Get the current top scorers of the Python Discord Leaderboard.""" + async def aoc_leaderboard( + self, + ctx: commands.Context, + day_and_star: Optional[bool] = False, + maximum_scorers: Optional[int] = 10 + ) -> None: + """ + Get the current top scorers of the Python Discord Leaderboard. + + Additionally, you can provide an argument `day_and_star` (Boolean) to have the bot send a View + that will let you filter by day and star. + """ + if maximum_scorers > AocConfig.max_day_and_star_results or maximum_scorers <= 0: + raise commands.BadArgument( + f"The maximum number of results you can query is {AocConfig.max_day_and_star_results}" + ) async with ctx.typing(): try: leaderboard = await _helpers.fetch_leaderboard() - except _helpers.FetchingLeaderboardFailed: + except _helpers.FetchingLeaderboardFailedError: await ctx.send(":x: Unable to fetch leaderboard!") return + if not day_and_star: number_of_participants = leaderboard["number_of_participants"] @@ -203,6 +220,22 @@ class AdventOfCode(commands.Cog): info_embed = _helpers.get_summary_embed(leaderboard) await ctx.send(content=f"{header}\n\n{table}", embed=info_embed) + return + + # This is a dictionary that contains solvers in respect of day, and star. + # e.g. 1-1 means the solvers of the first star of the first day and their completion time + per_day_and_star = json.loads(leaderboard['leaderboard_per_day_and_star']) + view = AoCDropdownView( + day_and_star_data=per_day_and_star, + maximum_scorers=maximum_scorers, + original_author=ctx.author + ) + message = await ctx.send( + content="Please select a day and a star to filter by!", + view=view + ) + await view.wait() + await message.edit(view=None) @in_month(Month.DECEMBER) @adventofcode_group.command( @@ -231,7 +264,7 @@ class AdventOfCode(commands.Cog): """Send an embed with daily completion statistics for the Python Discord leaderboard.""" try: leaderboard = await _helpers.fetch_leaderboard() - except _helpers.FetchingLeaderboardFailed: + except _helpers.FetchingLeaderboardFailedError: await ctx.send(":x: Can't fetch leaderboard for stats right now!") return @@ -267,7 +300,7 @@ class AdventOfCode(commands.Cog): async with ctx.typing(): try: await _helpers.fetch_leaderboard(invalidate_cache=True) - except _helpers.FetchingLeaderboardFailed: + except _helpers.FetchingLeaderboardFailedError: await ctx.send(":x: Something went wrong while trying to refresh the cache!") else: await ctx.send("\N{OK Hand Sign} Refreshed leaderboard cache!") diff --git a/bot/exts/events/advent_of_code/_helpers.py b/bot/exts/events/advent_of_code/_helpers.py index 5fedb60f..af64bc81 100644 --- a/bot/exts/events/advent_of_code/_helpers.py +++ b/bot/exts/events/advent_of_code/_helpers.py @@ -105,6 +105,7 @@ def _parse_raw_leaderboard_data(raw_leaderboard_data: dict) -> dict: # The data we get from the AoC website is structured by member, not by day/star, # which means we need to iterate over the members to transpose the data to a per # star view. We need that per star view to compute rank scores per star. + per_day_star_stats = collections.defaultdict(list) for member in raw_leaderboard_data.values(): name = member["name"] if member["name"] else f"Anonymous #{member['id']}" member_id = member["id"] @@ -122,6 +123,11 @@ def _parse_raw_leaderboard_data(raw_leaderboard_data: dict) -> dict: star_results[(day, star)].append( StarResult(member_id=member_id, completion_time=completion_time) ) + per_day_star_stats[f"{day}-{star}"].append( + {'completion_time': int(data["get_star_ts"]), 'member_name': name} + ) + for key in per_day_star_stats: + per_day_star_stats[key] = sorted(per_day_star_stats[key], key=operator.itemgetter('completion_time')) # Now that we have a transposed dataset that holds the completion time of all # participants per star, we can compute the rank-based scores each participant @@ -151,7 +157,7 @@ def _parse_raw_leaderboard_data(raw_leaderboard_data: dict) -> dict: # this data to JSON in order to cache it in Redis. daily_stats[day] = {"star_one": star_one, "star_two": star_two} - return {"daily_stats": daily_stats, "leaderboard": sorted_leaderboard} + return {"daily_stats": daily_stats, "leaderboard": sorted_leaderboard, 'per_day_and_star': per_day_star_stats} def _format_leaderboard(leaderboard: dict[str, dict]) -> str: @@ -289,6 +295,7 @@ async def fetch_leaderboard(invalidate_cache: bool = False) -> dict: "leaderboard_fetched_at": leaderboard_fetched_at, "number_of_participants": number_of_participants, "daily_stats": json.dumps(parsed_leaderboard_data["daily_stats"]), + "leaderboard_per_day_and_star": json.dumps(parsed_leaderboard_data["per_day_and_star"]) } # Store the new values in Redis diff --git a/bot/exts/events/advent_of_code/views/dayandstarview.py b/bot/exts/events/advent_of_code/views/dayandstarview.py new file mode 100644 index 00000000..243db32e --- /dev/null +++ b/bot/exts/events/advent_of_code/views/dayandstarview.py @@ -0,0 +1,71 @@ +from datetime import datetime + +import discord + +AOC_DAY_AND_STAR_TEMPLATE = "{rank: >4} | {name:25.25} | {completion_time: >10}" + + +class AoCDropdownView(discord.ui.View): + """Interactive view to filter AoC stats by Day and Star.""" + + def __init__(self, original_author: discord.Member, day_and_star_data: dict[str: dict], maximum_scorers: int): + super().__init__() + self.day = 0 + self.star = 0 + self.data = day_and_star_data + self.maximum_scorers = maximum_scorers + self.original_author = original_author + + def generate_output(self) -> str: + """Generates a formatted codeblock with AoC statistics based on the currently selected day and star.""" + header = AOC_DAY_AND_STAR_TEMPLATE.format( + rank="Rank", + name="Name", completion_time="Completion time (UTC)" + ) + lines = [f"{header}\n{'-' * (len(header) + 2)}"] + + for rank, scorer in enumerate(self.data[f"{self.day}-{self.star}"][:self.maximum_scorers]): + time_data = datetime.fromtimestamp(scorer['completion_time']).strftime("%I:%M:%S %p") + lines.append(AOC_DAY_AND_STAR_TEMPLATE.format( + datastamp="", + rank=rank + 1, + name=scorer['member_name'], + completion_time=time_data) + ) + joined_lines = "\n".join(lines) + return f"Statistics for Day: {self.day}, Star: {self.star}.\n ```\n{joined_lines}\n```" + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + """Global check to ensure that the interacting user is the user who invoked the command originally.""" + return interaction.user == self.original_author + + @discord.ui.select( + placeholder="Day", + options=[discord.SelectOption(label=str(i)) for i in range(1, 26)], + custom_id="day_select" + ) + async def day_select(self, select: discord.ui.Select, interaction: discord.Interaction) -> None: + """Dropdown to choose a Day of the AoC.""" + self.day = select.values[0] + + @discord.ui.select( + placeholder="Star", + options=[discord.SelectOption(label=str(i)) for i in range(1, 3)], + custom_id="star_select" + ) + async def star_select(self, select: discord.ui.Select, interaction: discord.Interaction) -> None: + """Dropdown to choose either the first or the second star.""" + self.star = select.values[0] + + @discord.ui.button(label="Fetch", style=discord.ButtonStyle.blurple) + async def fetch(self, button: discord.ui.Button, interaction: discord.Interaction) -> None: + """Button that fetches the statistics based on the dropdown values.""" + if self.day == 0 or self.star == 0: + await interaction.response.send_message( + "You have to select a value from both of the dropdowns!", + ephemeral=True + ) + else: + await interaction.response.edit_message(content=self.generate_output()) + self.day = 0 + self.star = 0 -- cgit v1.2.3 From a7bb17c3e475594ac2e52a4958f382fe9d26b036 Mon Sep 17 00:00:00 2001 From: TizzySaurus <47674925+TizzySaurus@users.noreply.github.com> Date: Sun, 17 Oct 2021 12:21:09 +0100 Subject: Fix bugs in `.issue` command & add aliases - Now requires at least one issue/PR - No longer continues to send issues/PRs when there's too many listed in the invocation - Added plural aliases (`.issues` and `.prs`) --- bot/exts/utilities/issues.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/bot/exts/utilities/issues.py b/bot/exts/utilities/issues.py index 8a7ebed0..36655e1b 100644 --- a/bot/exts/utilities/issues.py +++ b/bot/exts/utilities/issues.py @@ -185,7 +185,7 @@ class Issues(commands.Cog): return resp @whitelist_override(channels=WHITELISTED_CHANNELS, categories=WHITELISTED_CATEGORIES) - @commands.command(aliases=("pr",)) + @commands.command(aliases=("issues", "pr", "prs")) async def issue( self, ctx: commands.Context, @@ -197,14 +197,23 @@ class Issues(commands.Cog): # Remove duplicates numbers = set(numbers) - if len(numbers) > MAXIMUM_ISSUES: - embed = discord.Embed( + err_message = None + if not numbers: + err_message = "You must have at least one issue/PR!" + + elif len(numbers) > MAXIMUM_ISSUES: + err_message = f"Too many issues/PRs! (maximum of {MAXIMUM_ISSUES})" + + # If there's an error with command invocation then send an error embed + if err_message is not None: + err_embed = discord.Embed( title=random.choice(ERROR_REPLIES), color=Colours.soft_red, - description=f"Too many issues/PRs! (maximum of {MAXIMUM_ISSUES})" + description=err_message ) - await ctx.send(embed=embed) + await ctx.send(embed=err_embed) await invoke_help_command(ctx) + return results = [await self.fetch_issues(number, repository, user) for number in numbers] await ctx.send(embed=self.format_embed(results, user, repository)) -- cgit v1.2.3 From 860c137f868ff9c91df0669a33dd9d551cbcc4a5 Mon Sep 17 00:00:00 2001 From: Izan Date: Sun, 17 Oct 2021 13:09:54 +0100 Subject: Address review & make `AI.get_move` a staticmethod --- bot/exts/fun/tic_tac_toe.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/bot/exts/fun/tic_tac_toe.py b/bot/exts/fun/tic_tac_toe.py index 1ebf8d11..946b6f7b 100644 --- a/bot/exts/fun/tic_tac_toe.py +++ b/bot/exts/fun/tic_tac_toe.py @@ -72,11 +72,12 @@ class Player: class AI: """Tic Tac Toe AI class for against computer gaming.""" - def __init__(self, ctx: Context, symbol: str): - self.user = ctx.me + def __init__(self, bot_user: discord.Member, symbol: str): + self.user = bot_user self.symbol = symbol - async def get_move(self, board: dict[int, str], _: discord.Message) -> tuple[bool, int]: + @staticmethod + async def get_move(board: dict[int, str], _: discord.Message) -> tuple[bool, int]: """Get move from AI. AI use Minimax strategy.""" possible_moves = [i for i, emoji in board.items() if emoji in list(Emojis.number_emojis.values())] @@ -175,7 +176,8 @@ class Game: self.canceled = True return False, "User declined" - async def add_reactions(self, msg: discord.Message) -> None: + @staticmethod + async def add_reactions(msg: discord.Message) -> None: """Add number emojis to message.""" for nr in Emojis.number_emojis.values(): await msg.add_reaction(nr) @@ -267,7 +269,7 @@ class TicTacToe(Cog): return if opponent is None: game = Game( - [Player(ctx.author, ctx, Emojis.x_square), AI(ctx, Emojis.o_square)], + [Player(ctx.author, ctx, Emojis.x_square), AI(ctx.me, Emojis.o_square)], ctx ) else: -- cgit v1.2.3 From cbe05b4d3ce86acfd5c472295df870f9ee996f07 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 17 Oct 2021 16:33:47 +0100 Subject: Add a border around the scary bat When users with dark avatars would use the spookify command, they wouldn't be able to see the bat that was added. By adding a white border around the bat, it is now more visable. --- bot/resources/holidays/halloween/bat-clipart.png | Bin 12313 -> 19006 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/bot/resources/holidays/halloween/bat-clipart.png b/bot/resources/holidays/halloween/bat-clipart.png index 7df26ba9..fc2f77b0 100644 Binary files a/bot/resources/holidays/halloween/bat-clipart.png and b/bot/resources/holidays/halloween/bat-clipart.png differ -- cgit v1.2.3 From 6a52e8080ce8f4d22726dfefec3b57e3e51c62ec Mon Sep 17 00:00:00 2001 From: Karlis Suvi <45097959+ks129@users.noreply.github.com> Date: Wed, 20 Oct 2021 19:55:03 +0300 Subject: Use display_avatar instead of avatar in send_pride_image Discord.py 2.0 returns None for avatar if a user has default avatar. `display_avatar` returns always `Asset`. --- bot/exts/avatar_modification/avatar_modify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/avatar_modification/avatar_modify.py b/bot/exts/avatar_modification/avatar_modify.py index 87eb05e6..fbee96dc 100644 --- a/bot/exts/avatar_modification/avatar_modify.py +++ b/bot/exts/avatar_modification/avatar_modify.py @@ -239,7 +239,7 @@ class AvatarModify(commands.Cog): description=f"Here is your lovely avatar, surrounded by\n a beautiful {option} flag. Enjoy :D" ) embed.set_image(url=f"attachment://{file_name}") - embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=ctx.author.avatar.url) + embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=ctx.author.display_avatar.url) await ctx.send(file=file, embed=embed) @avatar_modify.group( -- cgit v1.2.3 From b55add31fb6baba06f4e4d3879419e4f963a29a6 Mon Sep 17 00:00:00 2001 From: Izan Date: Thu, 21 Oct 2021 20:43:32 +0100 Subject: Migrate to `og_blurple` --- bot/exts/core/extensions.py | 2 +- bot/exts/holidays/halloween/candy_collection.py | 2 +- bot/exts/utilities/conversationstarters.py | 2 +- bot/exts/utilities/emoji.py | 2 +- bot/exts/utilities/githubinfo.py | 4 ++-- bot/exts/utilities/reddit.py | 10 +++++----- bot/exts/utilities/wikipedia.py | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/bot/exts/core/extensions.py b/bot/exts/core/extensions.py index dbb9e069..d809d2b9 100644 --- a/bot/exts/core/extensions.py +++ b/bot/exts/core/extensions.py @@ -152,7 +152,7 @@ class Extensions(commands.Cog): Grey indicates that the extension is unloaded. Green indicates that the extension is currently loaded. """ - embed = Embed(colour=Colour.blurple()) + embed = Embed(colour=Colour.og_blurple()) embed.set_author( name="Extensions List", url=Client.github_bot_repo, diff --git a/bot/exts/holidays/halloween/candy_collection.py b/bot/exts/holidays/halloween/candy_collection.py index 09bd0e59..079d900d 100644 --- a/bot/exts/holidays/halloween/candy_collection.py +++ b/bot/exts/holidays/halloween/candy_collection.py @@ -182,7 +182,7 @@ class CandyCollection(commands.Cog): for index, record in enumerate(top_five) ) if top_five else "No Candies" - e = discord.Embed(colour=discord.Colour.blurple()) + e = discord.Embed(colour=discord.Colour.og_blurple()) e.add_field( name="Top Candy Records", value=generate_leaderboard(), diff --git a/bot/exts/utilities/conversationstarters.py b/bot/exts/utilities/conversationstarters.py index dcbfe4d5..8bf2abfd 100644 --- a/bot/exts/utilities/conversationstarters.py +++ b/bot/exts/utilities/conversationstarters.py @@ -53,7 +53,7 @@ class ConvoStarters(commands.Cog): # No matter what, the form will be shown. embed = discord.Embed( description=f"Suggest more topics [here]({SUGGESTION_FORM})!", - color=discord.Color.blurple() + color=discord.Colour.og_blurple() ) try: diff --git a/bot/exts/utilities/emoji.py b/bot/exts/utilities/emoji.py index 83df39cc..fa438d7f 100644 --- a/bot/exts/utilities/emoji.py +++ b/bot/exts/utilities/emoji.py @@ -111,7 +111,7 @@ class Emojis(commands.Cog): **Date:** {datetime.strftime(emoji.created_at.replace(tzinfo=None), "%d/%m/%Y")} **ID:** {emoji.id} """), - color=Color.blurple(), + color=Color.og_blurple(), url=str(emoji.url), ).set_thumbnail(url=emoji.url) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index d00b408d..539e388b 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -67,7 +67,7 @@ class GithubInfo(commands.Cog): embed = discord.Embed( title=f"`{user_data['login']}`'s GitHub profile info", description=f"```\n{user_data['bio']}\n```\n" if user_data["bio"] else "", - colour=discord.Colour.blurple(), + colour=discord.Colour.og_blurple(), url=user_data["html_url"], timestamp=datetime.strptime(user_data["created_at"], "%Y-%m-%dT%H:%M:%SZ") ) @@ -139,7 +139,7 @@ class GithubInfo(commands.Cog): embed = discord.Embed( title=repo_data["name"], description=repo_data["description"], - colour=discord.Colour.blurple(), + colour=discord.Colour.og_blurple(), url=repo_data["html_url"] ) diff --git a/bot/exts/utilities/reddit.py b/bot/exts/utilities/reddit.py index e6cb5337..782583d2 100644 --- a/bot/exts/utilities/reddit.py +++ b/bot/exts/utilities/reddit.py @@ -244,7 +244,7 @@ class Reddit(Cog): # Use only starting summary page for #reddit channel posts. embed.description = self.build_pagination_pages(posts, paginate=False) - embed.colour = Colour.blurple() + embed.colour = Colour.og_blurple() return embed @loop() @@ -312,7 +312,7 @@ class Reddit(Cog): await ctx.send(f"Here are the top {subreddit} posts of all time!") embed = Embed( - color=Colour.blurple() + color=Colour.og_blurple() ) await ImagePaginator.paginate(pages, ctx, embed) @@ -325,7 +325,7 @@ class Reddit(Cog): await ctx.send(f"Here are today's top {subreddit} posts!") embed = Embed( - color=Colour.blurple() + color=Colour.og_blurple() ) await ImagePaginator.paginate(pages, ctx, embed) @@ -338,7 +338,7 @@ class Reddit(Cog): await ctx.send(f"Here are this week's top {subreddit} posts!") embed = Embed( - color=Colour.blurple() + color=Colour.og_blurple() ) await ImagePaginator.paginate(pages, ctx, embed) @@ -349,7 +349,7 @@ class Reddit(Cog): """Send a paginated embed of all the subreddits we're relaying.""" embed = Embed() embed.title = "Relayed subreddits." - embed.colour = Colour.blurple() + embed.colour = Colour.og_blurple() await LinePaginator.paginate( RedditConfig.subreddits, diff --git a/bot/exts/utilities/wikipedia.py b/bot/exts/utilities/wikipedia.py index eccc1f8c..c5283de0 100644 --- a/bot/exts/utilities/wikipedia.py +++ b/bot/exts/utilities/wikipedia.py @@ -82,7 +82,7 @@ class WikipediaSearch(commands.Cog): if contents: embed = Embed( title="Wikipedia Search Results", - colour=Color.blurple() + colour=Color.og_blurple() ) embed.set_thumbnail(url=WIKI_THUMBNAIL) embed.timestamp = datetime.utcnow() -- cgit v1.2.3 From dde7535fc342ddf025e2049bddd9b52b80788898 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Thu, 21 Oct 2021 21:36:56 +0100 Subject: Bump pip licenses pip licenses used to use an internal method of pip, which got removed and caused errors in any runs. The newer version, which this commit bumps it too, now uses another method. --- poetry.lock | 728 ++++++++++++++++++++++++++++++++------------------------- pyproject.toml | 2 +- 2 files changed, 405 insertions(+), 325 deletions(-) diff --git a/poetry.lock b/poetry.lock index 21373a92..cbfc90fb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -117,7 +117,7 @@ lxml = ["lxml"] [[package]] name = "certifi" -version = "2021.5.30" +version = "2021.10.8" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -125,7 +125,7 @@ python-versions = "*" [[package]] name = "cffi" -version = "1.14.6" +version = "1.15.0" description = "Foreign Function Interface for Python calling C code." category = "main" optional = false @@ -188,10 +188,9 @@ voice = ["PyNaCl (>=1.3.0,<1.5)"] [package.source] type = "url" url = "https://github.com/Rapptz/discord.py/archive/45d498c1b76deaf3b394d17ccf56112fa691d160.zip" - [[package]] name = "distlib" -version = "0.3.2" +version = "0.3.3" description = "Distribution utilities" category = "dev" optional = false @@ -207,13 +206,14 @@ python-versions = "*" [[package]] name = "fakeredis" -version = "1.6.0" +version = "1.6.1" description = "Fake implementation of redis API for testing purposes." category = "main" optional = false python-versions = ">=3.5" [package.dependencies] +packaging = "*" redis = "<3.6.0" six = ">=1.12" sortedcontainers = "*" @@ -224,11 +224,15 @@ lua = ["lupa"] [[package]] name = "filelock" -version = "3.0.12" +version = "3.3.1" description = "A platform independent file lock." category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" + +[package.extras] +docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] +testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] [[package]] name = "flake8" @@ -245,14 +249,14 @@ pyflakes = ">=2.3.0,<2.4.0" [[package]] name = "flake8-annotations" -version = "2.6.2" +version = "2.7.0" description = "Flake8 Type Annotation Checks" category = "dev" optional = false -python-versions = ">=3.6.1,<4.0.0" +python-versions = ">=3.6.2,<4.0.0" [package.dependencies] -flake8 = ">=3.7,<4.0" +flake8 = ">=3.7,<5.0" [[package]] name = "flake8-bugbear" @@ -316,14 +320,14 @@ flake8 = "*" [[package]] name = "flake8-tidy-imports" -version = "4.4.1" +version = "4.5.0" description = "A flake8 plugin that helps you write tidier imports." category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -flake8 = ">=3.8.0,<4" +flake8 = ">=3.8.0,<5" [[package]] name = "flake8-todo" @@ -346,7 +350,7 @@ python-versions = ">=3.6" [[package]] name = "identify" -version = "2.2.13" +version = "2.3.0" description = "File identification library for Python" category = "dev" optional = false @@ -357,7 +361,7 @@ license = ["editdistance-s"] [[package]] name = "idna" -version = "3.2" +version = "3.3" description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false @@ -419,7 +423,7 @@ python-versions = ">=3.5" [[package]] name = "multidict" -version = "5.1.0" +version = "5.2.0" description = "multidict implementation" category = "main" optional = false @@ -441,6 +445,17 @@ category = "main" optional = false python-versions = ">=3.7" +[[package]] +name = "packaging" +version = "21.0" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2" + [[package]] name = "pep8-naming" version = "0.12.1" @@ -455,7 +470,7 @@ flake8-polyfill = ">=1.0.2,<2" [[package]] name = "pillow" -version = "8.3.2" +version = "8.4.0" description = "Python Imaging Library (Fork)" category = "main" optional = false @@ -463,7 +478,7 @@ python-versions = ">=3.6" [[package]] name = "pip-licenses" -version = "3.5.2" +version = "3.5.3" description = "Dump the software license list of Python packages installed with pip." category = "dev" optional = false @@ -477,7 +492,7 @@ test = ["docutils", "pytest-cov", "pytest-pycodestyle", "pytest-runner"] [[package]] name = "platformdirs" -version = "2.3.0" +version = "2.4.0" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false @@ -524,7 +539,7 @@ python-versions = "*" [[package]] name = "pycares" -version = "4.0.0" +version = "4.1.2" description = "Python interface for c-ares" category = "main" optional = false @@ -595,11 +610,11 @@ six = ">=1.5" [[package]] name = "python-dotenv" -version = "0.15.0" -description = "Add .env support to your django/flask apps in development and deployments" +version = "0.19.1" +description = "Read key-value pairs from a .env file and set them as environment variables" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.5" [package.extras] cli = ["click (>=5.0)"] @@ -614,11 +629,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "rapidfuzz" -version = "1.5.1" +version = "1.8.0" description = "rapid fuzzy string matching" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=2.7" + +[package.extras] +full = ["numpy"] [[package]] name = "redis" @@ -693,7 +711,7 @@ python-versions = ">=3.6" [[package]] name = "taskipy" -version = "1.8.1" +version = "1.9.0" description = "tasks runner for python projects" category = "dev" optional = false @@ -701,7 +719,7 @@ python-versions = ">=3.6,<4.0" [package.dependencies] colorama = ">=0.4.4,<0.5.0" -mslex = ">=0.3.0,<0.4.0" +mslex = {version = ">=0.3.0,<0.4.0", markers = "sys_platform == \"win32\""} psutil = ">=5.7.2,<6.0.0" toml = ">=0.10.0,<0.11.0" @@ -723,7 +741,7 @@ python-versions = "*" [[package]] name = "urllib3" -version = "1.26.6" +version = "1.26.7" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false @@ -736,7 +754,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.7.2" +version = "20.8.1" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -755,7 +773,7 @@ testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", [[package]] name = "yarl" -version = "1.6.3" +version = "1.7.0" description = "Yet another URL library" category = "main" optional = false @@ -768,7 +786,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "6cced4e3fff83ad6ead9a18b3f585b83426fab34f6e2bcf2466c2ebbbf66dac4" +content-hash = "3a470d4a9b63f106bfead96094c73a865dfa3d01e8c187e43d415ea6cd83f5bf" [metadata.files] aiodns = [ @@ -843,55 +861,60 @@ beautifulsoup4 = [ {file = "beautifulsoup4-4.10.0.tar.gz", hash = "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891"}, ] certifi = [ - {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, - {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, ] cffi = [ - {file = "cffi-1.14.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c"}, - {file = "cffi-1.14.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99"}, - {file = "cffi-1.14.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819"}, - {file = "cffi-1.14.6-cp27-cp27m-win32.whl", hash = "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20"}, - {file = "cffi-1.14.6-cp27-cp27m-win_amd64.whl", hash = "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224"}, - {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7"}, - {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33"}, - {file = "cffi-1.14.6-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534"}, - {file = "cffi-1.14.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a"}, - {file = "cffi-1.14.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5"}, - {file = "cffi-1.14.6-cp35-cp35m-win32.whl", hash = "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca"}, - {file = "cffi-1.14.6-cp35-cp35m-win_amd64.whl", hash = "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218"}, - {file = "cffi-1.14.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f"}, - {file = "cffi-1.14.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872"}, - {file = "cffi-1.14.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195"}, - {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d"}, - {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b"}, - {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb"}, - {file = "cffi-1.14.6-cp36-cp36m-win32.whl", hash = "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a"}, - {file = "cffi-1.14.6-cp36-cp36m-win_amd64.whl", hash = "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e"}, - {file = "cffi-1.14.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5"}, - {file = "cffi-1.14.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf"}, - {file = "cffi-1.14.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69"}, - {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56"}, - {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c"}, - {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762"}, - {file = "cffi-1.14.6-cp37-cp37m-win32.whl", hash = "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771"}, - {file = "cffi-1.14.6-cp37-cp37m-win_amd64.whl", hash = "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a"}, - {file = "cffi-1.14.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0"}, - {file = "cffi-1.14.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e"}, - {file = "cffi-1.14.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346"}, - {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc"}, - {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd"}, - {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc"}, - {file = "cffi-1.14.6-cp38-cp38-win32.whl", hash = "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548"}, - {file = "cffi-1.14.6-cp38-cp38-win_amd64.whl", hash = "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156"}, - {file = "cffi-1.14.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d"}, - {file = "cffi-1.14.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e"}, - {file = "cffi-1.14.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c"}, - {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202"}, - {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f"}, - {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87"}, - {file = "cffi-1.14.6-cp39-cp39-win32.whl", hash = "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728"}, - {file = "cffi-1.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2"}, - {file = "cffi-1.14.6.tar.gz", hash = "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd"}, + {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"}, + {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"}, + {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"}, + {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"}, + {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"}, + {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"}, + {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"}, + {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"}, + {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"}, + {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"}, + {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"}, + {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"}, + {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"}, + {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"}, + {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"}, + {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, + {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, ] cfgv = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, @@ -911,28 +934,28 @@ cycler = [ ] "discord.py" = [] distlib = [ - {file = "distlib-0.3.2-py2.py3-none-any.whl", hash = "sha256:23e223426b28491b1ced97dc3bbe183027419dfc7982b4fa2f05d5f3ff10711c"}, - {file = "distlib-0.3.2.zip", hash = "sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736"}, + {file = "distlib-0.3.3-py2.py3-none-any.whl", hash = "sha256:c8b54e8454e5bf6237cc84c20e8264c3e991e824ef27e8f1e81049867d861e31"}, + {file = "distlib-0.3.3.zip", hash = "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05"}, ] emojis = [ {file = "emojis-0.6.0-py3-none-any.whl", hash = "sha256:7da34c8a78ae262fd68cef9e2c78a3c1feb59784489eeea0f54ba1d4b7111c7c"}, {file = "emojis-0.6.0.tar.gz", hash = "sha256:bf605d1f1a27a81cd37fe82eb65781c904467f569295a541c33710b97e4225ec"}, ] fakeredis = [ - {file = "fakeredis-1.6.0-py3-none-any.whl", hash = "sha256:3449b306f3a85102b28f8180c24722ef966fcb1e3c744758b6f635ec80321a5c"}, - {file = "fakeredis-1.6.0.tar.gz", hash = "sha256:11ccfc9769d718d37e45b382e64a6ba02586b622afa0371a6bd85766d72255f3"}, + {file = "fakeredis-1.6.1-py3-none-any.whl", hash = "sha256:5eb1516f1fe1813e9da8f6c482178fc067af09f53de587ae03887ef5d9d13024"}, + {file = "fakeredis-1.6.1.tar.gz", hash = "sha256:0d06a9384fb79da9f2164ce96e34eb9d4e2ea46215070805ea6fd3c174590b47"}, ] filelock = [ - {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, - {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, + {file = "filelock-3.3.1-py3-none-any.whl", hash = "sha256:2b5eb3589e7fdda14599e7eb1a50e09b4cc14f34ed98b8ba56d33bfaafcbef2f"}, + {file = "filelock-3.3.1.tar.gz", hash = "sha256:34a9f35f95c441e7b38209775d6e0337f9a3759f3565f6c5798f19618527c76f"}, ] flake8 = [ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] flake8-annotations = [ - {file = "flake8-annotations-2.6.2.tar.gz", hash = "sha256:0d6cd2e770b5095f09689c9d84cc054c51b929c41a68969ea1beb4b825cac515"}, - {file = "flake8_annotations-2.6.2-py3-none-any.whl", hash = "sha256:d10c4638231f8a50c0a597c4efce42bd7b7d85df4f620a0ddaca526138936a4f"}, + {file = "flake8-annotations-2.7.0.tar.gz", hash = "sha256:52e53c05b0c06cac1c2dec192ea2c36e85081238add3bd99421d56f574b9479b"}, + {file = "flake8_annotations-2.7.0-py3-none-any.whl", hash = "sha256:3edfbbfb58e404868834fe6ec3eaf49c139f64f0701259f707d043185545151e"}, ] flake8-bugbear = [ {file = "flake8-bugbear-20.11.1.tar.gz", hash = "sha256:528020129fea2dea33a466b9d64ab650aa3e5f9ffc788b70ea4bc6cf18283538"}, @@ -955,8 +978,8 @@ flake8-string-format = [ {file = "flake8_string_format-0.3.0-py2.py3-none-any.whl", hash = "sha256:812ff431f10576a74c89be4e85b8e075a705be39bc40c4b4278b5b13e2afa9af"}, ] flake8-tidy-imports = [ - {file = "flake8-tidy-imports-4.4.1.tar.gz", hash = "sha256:c18b3351b998787db071e766e318da1f0bd9d5cecc69c4022a69e7aa2efb2c51"}, - {file = "flake8_tidy_imports-4.4.1-py3-none-any.whl", hash = "sha256:631a1ba9daaedbe8bb53f6086c5a92b390e98371205259e0e311a378df8c3dc8"}, + {file = "flake8-tidy-imports-4.5.0.tar.gz", hash = "sha256:ac637961d0f319012d099e49619f8c928e3221f74e00fe6eb89513bc64c40adb"}, + {file = "flake8_tidy_imports-4.5.0-py3-none-any.whl", hash = "sha256:87eed94ae6a2fda6a5918d109746feadf1311e0eb8274ab7a7920f6db00a41c9"}, ] flake8-todo = [ {file = "flake8-todo-0.7.tar.gz", hash = "sha256:6e4c5491ff838c06fe5a771b0e95ee15fc005ca57196011011280fc834a85915"}, @@ -1005,12 +1028,12 @@ hiredis = [ {file = "hiredis-2.0.0.tar.gz", hash = "sha256:81d6d8e39695f2c37954d1011c0480ef7cf444d4e3ae24bc5e89ee5de360139a"}, ] identify = [ - {file = "identify-2.2.13-py2.py3-none-any.whl", hash = "sha256:7199679b5be13a6b40e6e19ea473e789b11b4e3b60986499b1f589ffb03c217c"}, - {file = "identify-2.2.13.tar.gz", hash = "sha256:7bc6e829392bd017236531963d2d937d66fc27cadc643ac0aba2ce9f26157c79"}, + {file = "identify-2.3.0-py2.py3-none-any.whl", hash = "sha256:d1e82c83d063571bb88087676f81261a4eae913c492dafde184067c584bc7c05"}, + {file = "identify-2.3.0.tar.gz", hash = "sha256:fd08c97f23ceee72784081f1ce5125c8f53a02d3f2716dde79a6ab8f1039fea5"}, ] idna = [ - {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, - {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] kiwisolver = [ {file = "kiwisolver-1.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1d819553730d3c2724582124aee8a03c846ec4362ded1034c16fb3ef309264e6"}, @@ -1140,43 +1163,78 @@ mslex = [ {file = "mslex-0.3.0.tar.gz", hash = "sha256:4a1ac3f25025cad78ad2fe499dd16d42759f7a3801645399cce5c404415daa97"}, ] multidict = [ - {file = "multidict-5.1.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f"}, - {file = "multidict-5.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf"}, - {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281"}, - {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d"}, - {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d"}, - {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da"}, - {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224"}, - {file = "multidict-5.1.0-cp36-cp36m-win32.whl", hash = "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26"}, - {file = "multidict-5.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6"}, - {file = "multidict-5.1.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76"}, - {file = "multidict-5.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a"}, - {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f"}, - {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348"}, - {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93"}, - {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9"}, - {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37"}, - {file = "multidict-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5"}, - {file = "multidict-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632"}, - {file = "multidict-5.1.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952"}, - {file = "multidict-5.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79"}, - {file = "multidict-5.1.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456"}, - {file = "multidict-5.1.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7"}, - {file = "multidict-5.1.0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635"}, - {file = "multidict-5.1.0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a"}, - {file = "multidict-5.1.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea"}, - {file = "multidict-5.1.0-cp38-cp38-win32.whl", hash = "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656"}, - {file = "multidict-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3"}, - {file = "multidict-5.1.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93"}, - {file = "multidict-5.1.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647"}, - {file = "multidict-5.1.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d"}, - {file = "multidict-5.1.0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8"}, - {file = "multidict-5.1.0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1"}, - {file = "multidict-5.1.0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841"}, - {file = "multidict-5.1.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda"}, - {file = "multidict-5.1.0-cp39-cp39-win32.whl", hash = "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"}, - {file = "multidict-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359"}, - {file = "multidict-5.1.0.tar.gz", hash = "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5"}, + {file = "multidict-5.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3822c5894c72e3b35aae9909bef66ec83e44522faf767c0ad39e0e2de11d3b55"}, + {file = "multidict-5.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:28e6d883acd8674887d7edc896b91751dc2d8e87fbdca8359591a13872799e4e"}, + {file = "multidict-5.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b61f85101ef08cbbc37846ac0e43f027f7844f3fade9b7f6dd087178caedeee7"}, + {file = "multidict-5.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9b668c065968c5979fe6b6fa6760bb6ab9aeb94b75b73c0a9c1acf6393ac3bf"}, + {file = "multidict-5.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:517d75522b7b18a3385726b54a081afd425d4f41144a5399e5abd97ccafdf36b"}, + {file = "multidict-5.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b4ac3ba7a97b35a5ccf34f41b5a8642a01d1e55454b699e5e8e7a99b5a3acf5"}, + {file = "multidict-5.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:df23c83398715b26ab09574217ca21e14694917a0c857e356fd39e1c64f8283f"}, + {file = "multidict-5.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e58a9b5cc96e014ddf93c2227cbdeca94b56a7eb77300205d6e4001805391747"}, + {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f76440e480c3b2ca7f843ff8a48dc82446b86ed4930552d736c0bac507498a52"}, + {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cfde464ca4af42a629648c0b0d79b8f295cf5b695412451716531d6916461628"}, + {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0fed465af2e0eb6357ba95795d003ac0bdb546305cc2366b1fc8f0ad67cc3fda"}, + {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:b70913cbf2e14275013be98a06ef4b412329fe7b4f83d64eb70dce8269ed1e1a"}, + {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5635bcf1b75f0f6ef3c8a1ad07b500104a971e38d3683167b9454cb6465ac86"}, + {file = "multidict-5.2.0-cp310-cp310-win32.whl", hash = "sha256:77f0fb7200cc7dedda7a60912f2059086e29ff67cefbc58d2506638c1a9132d7"}, + {file = "multidict-5.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:9416cf11bcd73c861267e88aea71e9fcc35302b3943e45e1dbb4317f91a4b34f"}, + {file = "multidict-5.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fd77c8f3cba815aa69cb97ee2b2ef385c7c12ada9c734b0f3b32e26bb88bbf1d"}, + {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98ec9aea6223adf46999f22e2c0ab6cf33f5914be604a404f658386a8f1fba37"}, + {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5283c0a00f48e8cafcecadebfa0ed1dac8b39e295c7248c44c665c16dc1138b"}, + {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5f79c19c6420962eb17c7e48878a03053b7ccd7b69f389d5831c0a4a7f1ac0a1"}, + {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e4a67f1080123de76e4e97a18d10350df6a7182e243312426d508712e99988d4"}, + {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:94b117e27efd8e08b4046c57461d5a114d26b40824995a2eb58372b94f9fca02"}, + {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2e77282fd1d677c313ffcaddfec236bf23f273c4fba7cdf198108f5940ae10f5"}, + {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:116347c63ba049c1ea56e157fa8aa6edaf5e92925c9b64f3da7769bdfa012858"}, + {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:dc3a866cf6c13d59a01878cd806f219340f3e82eed514485e094321f24900677"}, + {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac42181292099d91217a82e3fa3ce0e0ddf3a74fd891b7c2b347a7f5aa0edded"}, + {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:f0bb0973f42ffcb5e3537548e0767079420aefd94ba990b61cf7bb8d47f4916d"}, + {file = "multidict-5.2.0-cp36-cp36m-win32.whl", hash = "sha256:ea21d4d5104b4f840b91d9dc8cbc832aba9612121eaba503e54eaab1ad140eb9"}, + {file = "multidict-5.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:e6453f3cbeb78440747096f239d282cc57a2997a16b5197c9bc839099e1633d0"}, + {file = "multidict-5.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d3def943bfd5f1c47d51fd324df1e806d8da1f8e105cc7f1c76a1daf0f7e17b0"}, + {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35591729668a303a02b06e8dba0eb8140c4a1bfd4c4b3209a436a02a5ac1de11"}, + {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8cacda0b679ebc25624d5de66c705bc53dcc7c6f02a7fb0f3ca5e227d80422"}, + {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:baf1856fab8212bf35230c019cde7c641887e3fc08cadd39d32a421a30151ea3"}, + {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a43616aec0f0d53c411582c451f5d3e1123a68cc7b3475d6f7d97a626f8ff90d"}, + {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25cbd39a9029b409167aa0a20d8a17f502d43f2efebfe9e3ac019fe6796c59ac"}, + {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a2cbcfbea6dc776782a444db819c8b78afe4db597211298dd8b2222f73e9cd0"}, + {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3d2d7d1fff8e09d99354c04c3fd5b560fb04639fd45926b34e27cfdec678a704"}, + {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a37e9a68349f6abe24130846e2f1d2e38f7ddab30b81b754e5a1fde32f782b23"}, + {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:637c1896497ff19e1ee27c1c2c2ddaa9f2d134bbb5e0c52254361ea20486418d"}, + {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9815765f9dcda04921ba467957be543423e5ec6a1136135d84f2ae092c50d87b"}, + {file = "multidict-5.2.0-cp37-cp37m-win32.whl", hash = "sha256:8b911d74acdc1fe2941e59b4f1a278a330e9c34c6c8ca1ee21264c51ec9b67ef"}, + {file = "multidict-5.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:380b868f55f63d048a25931a1632818f90e4be71d2081c2338fcf656d299949a"}, + {file = "multidict-5.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e7d81ce5744757d2f05fc41896e3b2ae0458464b14b5a2c1e87a6a9d69aefaa8"}, + {file = "multidict-5.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d1d55cdf706ddc62822d394d1df53573d32a7a07d4f099470d3cb9323b721b6"}, + {file = "multidict-5.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4771d0d0ac9d9fe9e24e33bed482a13dfc1256d008d101485fe460359476065"}, + {file = "multidict-5.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da7d57ea65744d249427793c042094c4016789eb2562576fb831870f9c878d9e"}, + {file = "multidict-5.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdd68778f96216596218b4e8882944d24a634d984ee1a5a049b300377878fa7c"}, + {file = "multidict-5.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecc99bce8ee42dcad15848c7885197d26841cb24fa2ee6e89d23b8993c871c64"}, + {file = "multidict-5.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:067150fad08e6f2dd91a650c7a49ba65085303fcc3decbd64a57dc13a2733031"}, + {file = "multidict-5.2.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:78c106b2b506b4d895ddc801ff509f941119394b89c9115580014127414e6c2d"}, + {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e6c4fa1ec16e01e292315ba76eb1d012c025b99d22896bd14a66628b245e3e01"}, + {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b227345e4186809d31f22087d0265655114af7cda442ecaf72246275865bebe4"}, + {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:06560fbdcf22c9387100979e65b26fba0816c162b888cb65b845d3def7a54c9b"}, + {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7878b61c867fb2df7a95e44b316f88d5a3742390c99dfba6c557a21b30180cac"}, + {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:246145bff76cc4b19310f0ad28bd0769b940c2a49fc601b86bfd150cbd72bb22"}, + {file = "multidict-5.2.0-cp38-cp38-win32.whl", hash = "sha256:c30ac9f562106cd9e8071c23949a067b10211917fdcb75b4718cf5775356a940"}, + {file = "multidict-5.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:f19001e790013ed580abfde2a4465388950728861b52f0da73e8e8a9418533c0"}, + {file = "multidict-5.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c1ff762e2ee126e6f1258650ac641e2b8e1f3d927a925aafcfde943b77a36d24"}, + {file = "multidict-5.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bd6c9c50bf2ad3f0448edaa1a3b55b2e6866ef8feca5d8dbec10ec7c94371d21"}, + {file = "multidict-5.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc66d4016f6e50ed36fb39cd287a3878ffcebfa90008535c62e0e90a7ab713ae"}, + {file = "multidict-5.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9acb76d5f3dd9421874923da2ed1e76041cb51b9337fd7f507edde1d86535d6"}, + {file = "multidict-5.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dfc924a7e946dd3c6360e50e8f750d51e3ef5395c95dc054bc9eab0f70df4f9c"}, + {file = "multidict-5.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32fdba7333eb2351fee2596b756d730d62b5827d5e1ab2f84e6cbb287cc67fe0"}, + {file = "multidict-5.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b9aad49466b8d828b96b9e3630006234879c8d3e2b0a9d99219b3121bc5cdb17"}, + {file = "multidict-5.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:93de39267c4c676c9ebb2057e98a8138bade0d806aad4d864322eee0803140a0"}, + {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f9bef5cff994ca3026fcc90680e326d1a19df9841c5e3d224076407cc21471a1"}, + {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5f841c4f14331fd1e36cbf3336ed7be2cb2a8f110ce40ea253e5573387db7621"}, + {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:38ba256ee9b310da6a1a0f013ef4e422fca30a685bcbec86a969bd520504e341"}, + {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3bc3b1621b979621cee9f7b09f024ec76ec03cc365e638126a056317470bde1b"}, + {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6ee908c070020d682e9b42c8f621e8bb10c767d04416e2ebe44e37d0f44d9ad5"}, + {file = "multidict-5.2.0-cp39-cp39-win32.whl", hash = "sha256:1c7976cd1c157fa7ba5456ae5d31ccdf1479680dc9b8d8aa28afabc370df42b8"}, + {file = "multidict-5.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:c9631c642e08b9fff1c6255487e62971d8b8e821808ddd013d8ac058087591ac"}, + {file = "multidict-5.2.0.tar.gz", hash = "sha256:0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce"}, ] nodeenv = [ {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, @@ -1212,72 +1270,64 @@ numpy = [ {file = "numpy-1.21.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2d4d1de6e6fb3d28781c73fbde702ac97f03d79e4ffd6598b880b2d95d62ead4"}, {file = "numpy-1.21.1.zip", hash = "sha256:dff4af63638afcc57a3dfb9e4b26d434a7a602d225b42d746ea7fe2edf1342fd"}, ] +packaging = [ + {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, + {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, +] pep8-naming = [ {file = "pep8-naming-0.12.1.tar.gz", hash = "sha256:bb2455947757d162aa4cad55dba4ce029005cd1692f2899a21d51d8630ca7841"}, {file = "pep8_naming-0.12.1-py2.py3-none-any.whl", hash = "sha256:4a8daeaeb33cfcde779309fc0c9c0a68a3bbe2ad8a8308b763c5068f86eb9f37"}, ] pillow = [ - {file = "Pillow-8.3.2-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:c691b26283c3a31594683217d746f1dad59a7ae1d4cfc24626d7a064a11197d4"}, - {file = "Pillow-8.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f514c2717012859ccb349c97862568fdc0479aad85b0270d6b5a6509dbc142e2"}, - {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be25cb93442c6d2f8702c599b51184bd3ccd83adebd08886b682173e09ef0c3f"}, - {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d675a876b295afa114ca8bf42d7f86b5fb1298e1b6bb9a24405a3f6c8338811c"}, - {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59697568a0455764a094585b2551fd76bfd6b959c9f92d4bdec9d0e14616303a"}, - {file = "Pillow-8.3.2-cp310-cp310-win32.whl", hash = "sha256:2d5e9dc0bf1b5d9048a94c48d0813b6c96fccfa4ccf276d9c36308840f40c228"}, - {file = "Pillow-8.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:11c27e74bab423eb3c9232d97553111cc0be81b74b47165f07ebfdd29d825875"}, - {file = "Pillow-8.3.2-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:11eb7f98165d56042545c9e6db3ce394ed8b45089a67124298f0473b29cb60b2"}, - {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f23b2d3079522fdf3c09de6517f625f7a964f916c956527bed805ac043799b8"}, - {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19ec4cfe4b961edc249b0e04b5618666c23a83bc35842dea2bfd5dfa0157f81b"}, - {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5a31c07cea5edbaeb4bdba6f2b87db7d3dc0f446f379d907e51cc70ea375629"}, - {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15ccb81a6ffc57ea0137f9f3ac2737ffa1d11f786244d719639df17476d399a7"}, - {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8f284dc1695caf71a74f24993b7c7473d77bc760be45f776a2c2f4e04c170550"}, - {file = "Pillow-8.3.2-cp36-cp36m-win32.whl", hash = "sha256:4abc247b31a98f29e5224f2d31ef15f86a71f79c7f4d2ac345a5d551d6393073"}, - {file = "Pillow-8.3.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a048dad5ed6ad1fad338c02c609b862dfaa921fcd065d747194a6805f91f2196"}, - {file = "Pillow-8.3.2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:06d1adaa284696785375fa80a6a8eb309be722cf4ef8949518beb34487a3df71"}, - {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd24054aaf21e70a51e2a2a5ed1183560d3a69e6f9594a4bfe360a46f94eba83"}, - {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a330bf7014ee034046db43ccbb05c766aa9e70b8d6c5260bfc38d73103b0ba"}, - {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13654b521fb98abdecec105ea3fb5ba863d1548c9b58831dd5105bb3873569f1"}, - {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1bd983c565f92779be456ece2479840ec39d386007cd4ae83382646293d681b"}, - {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4326ea1e2722f3dc00ed77c36d3b5354b8fb7399fb59230249ea6d59cbed90da"}, - {file = "Pillow-8.3.2-cp37-cp37m-win32.whl", hash = "sha256:085a90a99404b859a4b6c3daa42afde17cb3ad3115e44a75f0d7b4a32f06a6c9"}, - {file = "Pillow-8.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:18a07a683805d32826c09acfce44a90bf474e6a66ce482b1c7fcd3757d588df3"}, - {file = "Pillow-8.3.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4e59e99fd680e2b8b11bbd463f3c9450ab799305d5f2bafb74fefba6ac058616"}, - {file = "Pillow-8.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d89a2e9219a526401015153c0e9dd48319ea6ab9fe3b066a20aa9aee23d9fd3"}, - {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fd98c8294f57636084f4b076b75f86c57b2a63a8410c0cd172bc93695ee979"}, - {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b11c9d310a3522b0fd3c35667914271f570576a0e387701f370eb39d45f08a4"}, - {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0412516dcc9de9b0a1e0ae25a280015809de8270f134cc2c1e32c4eeb397cf30"}, - {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bcb04ff12e79b28be6c9988f275e7ab69f01cc2ba319fb3114f87817bb7c74b6"}, - {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b9911ec70731711c3b6ebcde26caea620cbdd9dcb73c67b0730c8817f24711b"}, - {file = "Pillow-8.3.2-cp38-cp38-win32.whl", hash = "sha256:ce2e5e04bb86da6187f96d7bab3f93a7877830981b37f0287dd6479e27a10341"}, - {file = "Pillow-8.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:35d27687f027ad25a8d0ef45dd5208ef044c588003cdcedf05afb00dbc5c2deb"}, - {file = "Pillow-8.3.2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:04835e68ef12904bc3e1fd002b33eea0779320d4346082bd5b24bec12ad9c3e9"}, - {file = "Pillow-8.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:10e00f7336780ca7d3653cf3ac26f068fa11b5a96894ea29a64d3dc4b810d630"}, - {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cde7a4d3687f21cffdf5bb171172070bb95e02af448c4c8b2f223d783214056"}, - {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c3ff00110835bdda2b1e2b07f4a2548a39744bb7de5946dc8e95517c4fb2ca6"}, - {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d409030bf3bd05fa66fb5fdedc39c521b397f61ad04309c90444e893d05f7d"}, - {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bff50ba9891be0a004ef48828e012babaaf7da204d81ab9be37480b9020a82b"}, - {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7dbfbc0020aa1d9bc1b0b8bcf255a7d73f4ad0336f8fd2533fcc54a4ccfb9441"}, - {file = "Pillow-8.3.2-cp39-cp39-win32.whl", hash = "sha256:963ebdc5365d748185fdb06daf2ac758116deecb2277ec5ae98139f93844bc09"}, - {file = "Pillow-8.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:cc9d0dec711c914ed500f1d0d3822868760954dce98dfb0b7382a854aee55d19"}, - {file = "Pillow-8.3.2-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2c661542c6f71dfd9dc82d9d29a8386287e82813b0375b3a02983feac69ef864"}, - {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:548794f99ff52a73a156771a0402f5e1c35285bd981046a502d7e4793e8facaa"}, - {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b68f565a4175e12e68ca900af8910e8fe48aaa48fd3ca853494f384e11c8bcd"}, - {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:838eb85de6d9307c19c655c726f8d13b8b646f144ca6b3771fa62b711ebf7624"}, - {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:feb5db446e96bfecfec078b943cc07744cc759893cef045aa8b8b6d6aaa8274e"}, - {file = "Pillow-8.3.2-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:fc0db32f7223b094964e71729c0361f93db43664dd1ec86d3df217853cedda87"}, - {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd4fd83aa912d7b89b4b4a1580d30e2a4242f3936882a3f433586e5ab97ed0d5"}, - {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d0c8ebbfd439c37624db98f3877d9ed12c137cadd99dde2d2eae0dab0bbfc355"}, - {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cb3dd7f23b044b0737317f892d399f9e2f0b3a02b22b2c692851fb8120d82c6"}, - {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a66566f8a22561fc1a88dc87606c69b84fa9ce724f99522cf922c801ec68f5c1"}, - {file = "Pillow-8.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ce651ca46d0202c302a535d3047c55a0131a720cf554a578fc1b8a2aff0e7d96"}, - {file = "Pillow-8.3.2.tar.gz", hash = "sha256:dde3f3ed8d00c72631bc19cbfff8ad3b6215062a5eed402381ad365f82f0c18c"}, + {file = "Pillow-8.4.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:81f8d5c81e483a9442d72d182e1fb6dcb9723f289a57e8030811bac9ea3fef8d"}, + {file = "Pillow-8.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3f97cfb1e5a392d75dd8b9fd274d205404729923840ca94ca45a0af57e13dbe6"}, + {file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb9fc393f3c61f9054e1ed26e6fe912c7321af2f41ff49d3f83d05bacf22cc78"}, + {file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d82cdb63100ef5eedb8391732375e6d05993b765f72cb34311fab92103314649"}, + {file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc1afda735a8d109007164714e73771b499768b9bb5afcbbee9d0ff374b43f"}, + {file = "Pillow-8.4.0-cp310-cp310-win32.whl", hash = "sha256:e3dacecfbeec9a33e932f00c6cd7996e62f53ad46fbe677577394aaa90ee419a"}, + {file = "Pillow-8.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:620582db2a85b2df5f8a82ddeb52116560d7e5e6b055095f04ad828d1b0baa39"}, + {file = "Pillow-8.4.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:1bc723b434fbc4ab50bb68e11e93ce5fb69866ad621e3c2c9bdb0cd70e345f55"}, + {file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72cbcfd54df6caf85cc35264c77ede902452d6df41166010262374155947460c"}, + {file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70ad9e5c6cb9b8487280a02c0ad8a51581dcbbe8484ce058477692a27c151c0a"}, + {file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25a49dc2e2f74e65efaa32b153527fc5ac98508d502fa46e74fa4fd678ed6645"}, + {file = "Pillow-8.4.0-cp36-cp36m-win32.whl", hash = "sha256:93ce9e955cc95959df98505e4608ad98281fff037350d8c2671c9aa86bcf10a9"}, + {file = "Pillow-8.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2e4440b8f00f504ee4b53fe30f4e381aae30b0568193be305256b1462216feff"}, + {file = "Pillow-8.4.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8c803ac3c28bbc53763e6825746f05cc407b20e4a69d0122e526a582e3b5e153"}, + {file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8a17b5d948f4ceeceb66384727dde11b240736fddeda54ca740b9b8b1556b29"}, + {file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1394a6ad5abc838c5cd8a92c5a07535648cdf6d09e8e2d6df916dfa9ea86ead8"}, + {file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:792e5c12376594bfcb986ebf3855aa4b7c225754e9a9521298e460e92fb4a488"}, + {file = "Pillow-8.4.0-cp37-cp37m-win32.whl", hash = "sha256:d99ec152570e4196772e7a8e4ba5320d2d27bf22fdf11743dd882936ed64305b"}, + {file = "Pillow-8.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7b7017b61bbcdd7f6363aeceb881e23c46583739cb69a3ab39cb384f6ec82e5b"}, + {file = "Pillow-8.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:d89363f02658e253dbd171f7c3716a5d340a24ee82d38aab9183f7fdf0cdca49"}, + {file = "Pillow-8.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a0956fdc5defc34462bb1c765ee88d933239f9a94bc37d132004775241a7585"}, + {file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b7bb9de00197fb4261825c15551adf7605cf14a80badf1761d61e59da347779"}, + {file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72b9e656e340447f827885b8d7a15fc8c4e68d410dc2297ef6787eec0f0ea409"}, + {file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5a4532a12314149d8b4e4ad8ff09dde7427731fcfa5917ff16d0291f13609df"}, + {file = "Pillow-8.4.0-cp38-cp38-win32.whl", hash = "sha256:82aafa8d5eb68c8463b6e9baeb4f19043bb31fefc03eb7b216b51e6a9981ae09"}, + {file = "Pillow-8.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:066f3999cb3b070a95c3652712cffa1a748cd02d60ad7b4e485c3748a04d9d76"}, + {file = "Pillow-8.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:5503c86916d27c2e101b7f71c2ae2cddba01a2cf55b8395b0255fd33fa4d1f1a"}, + {file = "Pillow-8.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4acc0985ddf39d1bc969a9220b51d94ed51695d455c228d8ac29fcdb25810e6e"}, + {file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b052a619a8bfcf26bd8b3f48f45283f9e977890263e4571f2393ed8898d331b"}, + {file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:493cb4e415f44cd601fcec11c99836f707bb714ab03f5ed46ac25713baf0ff20"}, + {file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8831cb7332eda5dc89b21a7bce7ef6ad305548820595033a4b03cf3091235ed"}, + {file = "Pillow-8.4.0-cp39-cp39-win32.whl", hash = "sha256:5e9ac5f66616b87d4da618a20ab0a38324dbe88d8a39b55be8964eb520021e02"}, + {file = "Pillow-8.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:3eb1ce5f65908556c2d8685a8f0a6e989d887ec4057326f6c22b24e8a172c66b"}, + {file = "Pillow-8.4.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ddc4d832a0f0b4c52fff973a0d44b6c99839a9d016fe4e6a1cb8f3eea96479c2"}, + {file = "Pillow-8.4.0-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3e5ddc44c14042f0844b8cf7d2cd455f6cc80fd7f5eefbe657292cf601d9ad"}, + {file = "Pillow-8.4.0-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70e94281588ef053ae8998039610dbd71bc509e4acbc77ab59d7d2937b10698"}, + {file = "Pillow-8.4.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:3862b7256046fcd950618ed22d1d60b842e3a40a48236a5498746f21189afbbc"}, + {file = "Pillow-8.4.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4901622493f88b1a29bd30ec1a2f683782e57c3c16a2dbc7f2595ba01f639df"}, + {file = "Pillow-8.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84c471a734240653a0ec91dec0996696eea227eafe72a33bd06c92697728046b"}, + {file = "Pillow-8.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:244cf3b97802c34c41905d22810846802a3329ddcb93ccc432870243211c79fc"}, + {file = "Pillow-8.4.0.tar.gz", hash = "sha256:b8e2f83c56e141920c39464b852de3719dfbfb6e3c99a2d8da0edf4fb33176ed"}, ] pip-licenses = [ - {file = "pip-licenses-3.5.2.tar.gz", hash = "sha256:c5e984f461b34ad04dafa151d0048eb9d049e3d6439966c6440bb6b53ad077b6"}, - {file = "pip_licenses-3.5.2-py3-none-any.whl", hash = "sha256:62deafc82d5dccea1a4cab55172706e02f228abcd67f4d53e382fcb1497e9b62"}, + {file = "pip-licenses-3.5.3.tar.gz", hash = "sha256:f44860e00957b791c6c6005a3328f2d5eaeee96ddb8e7d87d4b0aa25b02252e4"}, + {file = "pip_licenses-3.5.3-py3-none-any.whl", hash = "sha256:59c148d6a03784bf945d232c0dc0e9de4272a3675acaa0361ad7712398ca86ba"}, ] platformdirs = [ - {file = "platformdirs-2.3.0-py3-none-any.whl", hash = "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"}, - {file = "platformdirs-2.3.0.tar.gz", hash = "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f"}, + {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, + {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, ] pre-commit = [ {file = "pre_commit-2.15.0-py2.py3-none-any.whl", hash = "sha256:a4ed01000afcb484d9eb8d504272e642c4c4099bbad3a6b27e519bd6a3e928a6"}, @@ -1317,39 +1367,37 @@ ptable = [ {file = "PTable-0.9.2.tar.gz", hash = "sha256:aa7fc151cb40f2dabcd2275ba6f7fd0ff8577a86be3365cd3fb297cbe09cc292"}, ] pycares = [ - {file = "pycares-4.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:db5a533111a3cfd481e7e4fb2bf8bef69f4fa100339803e0504dd5aecafb96a5"}, - {file = "pycares-4.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:fdff88393c25016f417770d82678423fc7a56995abb2df3d2a1e55725db6977d"}, - {file = "pycares-4.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0aa97f900a7ffb259be77d640006585e2a907b0cd4edeee0e85cf16605995d5a"}, - {file = "pycares-4.0.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:a34b0e3e693dceb60b8a1169668d606c75cb100ceba0a2df53c234a0eb067fbc"}, - {file = "pycares-4.0.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7661d6bbd51a337e7373cb356efa8be9b4655fda484e068f9455e939aec8d54e"}, - {file = "pycares-4.0.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:57315b8eb8fdbc56b3ad4932bc4b17132bb7c7fd2bd590f7fb84b6b522098aa9"}, - {file = "pycares-4.0.0-cp36-cp36m-win32.whl", hash = "sha256:dca9dc58845a9d083f302732a3130c68ded845ad5d463865d464e53c75a3dd45"}, - {file = "pycares-4.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c95c964d5dd307e104b44b193095c67bb6b10c9eda1ffe7d44ab7a9e84c476d9"}, - {file = "pycares-4.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26e67e4f81c80a5955dcf6193f3d9bee3c491fc0056299b383b84d792252fba4"}, - {file = "pycares-4.0.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cd3011ffd5e1ad55880f7256791dbab9c43ebeda260474a968f19cd0319e1aef"}, - {file = "pycares-4.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1b959dd5921d207d759d421eece1b60416df33a7f862465739d5f2c363c2f523"}, - {file = "pycares-4.0.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6f258c1b74c048a9501a25f732f11b401564005e5e3c18f1ca6cad0c3dc0fb19"}, - {file = "pycares-4.0.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:b17ef48729786e62b574c6431f675f4cb02b27691b49e7428a605a50cd59c072"}, - {file = "pycares-4.0.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:82b3259cb590ddd107a6d2dc52da2a2e9a986bf242e893d58c786af2f8191047"}, - {file = "pycares-4.0.0-cp37-cp37m-win32.whl", hash = "sha256:4876fc790ae32832ae270c4a010a1a77e12ddf8d8e6ad70ad0b0a9d506c985f7"}, - {file = "pycares-4.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f60c04c5561b1ddf85ca4e626943cc09d7fb684e1adb22abb632095415a40fd7"}, - {file = "pycares-4.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:615406013cdcd1b445e5d1a551d276c6200b3abe77e534f8a7f7e1551208d14f"}, - {file = "pycares-4.0.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6580aef5d1b29a88c3d72fe73c691eacfd454f86e74d3fdd18f4bad8e8def98b"}, - {file = "pycares-4.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8ebb3ba0485f66cae8eed7ce3e9ed6f2c0bfd5e7319d5d0fbbb511064f17e1d4"}, - {file = "pycares-4.0.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c5362b7690ca481440f6b98395ac6df06aa50518ccb183c560464d1e5e2ab5d4"}, - {file = "pycares-4.0.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:eb60be66accc9a9ea1018b591a1f5800cba83491d07e9acc8c56bc6e6607ab54"}, - {file = "pycares-4.0.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:44896d6e191a6b5a914dbe3aa7c748481bf6ad19a9df33c1e76f8f2dc33fc8f0"}, - {file = "pycares-4.0.0-cp38-cp38-win32.whl", hash = "sha256:09b28fc7bc2cc05f7f69bf1636ddf46086e0a1837b62961e2092fcb40477320d"}, - {file = "pycares-4.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4a5081e232c1d181883dcac4675807f3a6cf33911c4173fbea00c0523687ed4"}, - {file = "pycares-4.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:103353577a6266a53e71bfee4cf83825f1401fefa60f0fb8bdec35f13be6a5f2"}, - {file = "pycares-4.0.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ad6caf580ee69806fc6534be93ddbb6e99bf94296d79ab351c37b2992b17abfd"}, - {file = "pycares-4.0.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3d5e50c95849f6905d2a9dbf02ed03f82580173e3c5604a39e2ad054185631f1"}, - {file = "pycares-4.0.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:53bc4f181b19576499b02cea4b45391e8dcbe30abd4cd01492f66bfc15615a13"}, - {file = "pycares-4.0.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:d52f9c725d2a826d5ffa37681eb07ffb996bfe21788590ef257664a3898fc0b5"}, - {file = "pycares-4.0.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:3c7fb8d34ee11971c39acfaf98d0fac66725385ccef3bfe1b174c92b210e1aa4"}, - {file = "pycares-4.0.0-cp39-cp39-win32.whl", hash = "sha256:e9773e07684a55f54657df05237267611a77b294ec3bacb5f851c4ffca38a465"}, - {file = "pycares-4.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:38e54037f36c149146ff15f17a4a963fbdd0f9871d4a21cd94ff9f368140f57e"}, - {file = "pycares-4.0.0.tar.gz", hash = "sha256:d0154fc5753b088758fbec9bc137e1b24bb84fc0c6a09725c8bac25a342311cd"}, + {file = "pycares-4.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:71b99b9e041ae3356b859822c511f286f84c8889ec9ed1fbf6ac30fb4da13e4c"}, + {file = "pycares-4.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c000942f5fc64e6e046aa61aa53b629b576ba11607d108909727c3c8f211a157"}, + {file = "pycares-4.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b0e50ddc78252f2e2b6b5f2c73e5b2449dfb6bea7a5a0e21dfd1e2bcc9e17382"}, + {file = "pycares-4.1.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6831e963a910b0a8cbdd2750ffcdf5f2bb0edb3f53ca69ff18484de2cc3807c4"}, + {file = "pycares-4.1.2-cp310-cp310-win32.whl", hash = "sha256:ad7b28e1b6bc68edd3d678373fa3af84e39d287090434f25055d21b4716b2fc6"}, + {file = "pycares-4.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:27a6f09dbfb69bb79609724c0f90dfaa7c215876a7cd9f12d585574d1f922112"}, + {file = "pycares-4.1.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e5a060f5fa90ae245aa99a4a8ad13ec39c2340400de037c7e8d27b081e1a3c64"}, + {file = "pycares-4.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:056330275dea42b7199494047a745e1d9785d39fb8c4cd469dca043532240b80"}, + {file = "pycares-4.1.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0aa897543a786daba74ec5e19638bd38b2b432d179a0e248eac1e62de5756207"}, + {file = "pycares-4.1.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cbceaa9b2c416aa931627466d3240aecfc905c292c842252e3d77b8630072505"}, + {file = "pycares-4.1.2-cp36-cp36m-win32.whl", hash = "sha256:112e1385c451069112d6b5ea1f9c378544f3c6b89882ff964e9a64be3336d7e4"}, + {file = "pycares-4.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:c6680f7fdc0f1163e8f6c2a11d11b9a0b524a61000d2a71f9ccd410f154fb171"}, + {file = "pycares-4.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58a41a2baabcd95266db776c510d349d417919407f03510fc87ac7488730d913"}, + {file = "pycares-4.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a810d01c9a426ee8b0f36969c2aef5fb966712be9d7e466920beb328cd9cefa3"}, + {file = "pycares-4.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b266cec81dcea2c3efbbd3dda00af8d7eb0693ae9e47e8706518334b21f27d4a"}, + {file = "pycares-4.1.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8319afe4838e09df267c421ca93da408f770b945ec6217dda72f1f6a493e37e4"}, + {file = "pycares-4.1.2-cp37-cp37m-win32.whl", hash = "sha256:4d5da840aa0d9b15fa51107f09270c563a348cb77b14ae9653d0bbdbe326fcc2"}, + {file = "pycares-4.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:5632f21d92cc0225ba5ff906e4e5dec415ef0b3df322c461d138190681cd5d89"}, + {file = "pycares-4.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8fd1ff17a26bb004f0f6bb902ba7dddd810059096ae0cc3b45e4f5be46315d19"}, + {file = "pycares-4.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:439799be4b7576e907139a7f9b3c8a01b90d3e38af4af9cd1fc6c1ee9a42b9e6"}, + {file = "pycares-4.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:40079ed58efa91747c50aac4edf8ecc7e570132ab57dc0a4030eb0d016a6cab8"}, + {file = "pycares-4.1.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e190471a015f8225fa38069617192e06122771cce2b169ac7a60bfdbd3d4ab2"}, + {file = "pycares-4.1.2-cp38-cp38-win32.whl", hash = "sha256:2b837315ed08c7df009b67725fe1f50489e99de9089f58ec1b243dc612f172aa"}, + {file = "pycares-4.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:c7eba3c8354b730a54d23237d0b6445a2f68570fa68d0848887da23a3f3b71f3"}, + {file = "pycares-4.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2f5f84fe9f83eab9cd68544b165b74ba6e3412d029cc9ab20098d9c332869fc5"}, + {file = "pycares-4.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569eef8597b5e02b1bc4644b9f272160304d8c9985357d7ecfcd054da97c0771"}, + {file = "pycares-4.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e1489aa25d14dbf7176110ead937c01176ed5a0ebefd3b092bbd6b202241814c"}, + {file = "pycares-4.1.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dc942692fca0e27081b7bb414bb971d34609c80df5e953f6d0c62ecc8019acd9"}, + {file = "pycares-4.1.2-cp39-cp39-win32.whl", hash = "sha256:ed71dc4290d9c3353945965604ef1f6a4de631733e9819a7ebc747220b27e641"}, + {file = "pycares-4.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:ec00f3594ee775665167b1a1630edceefb1b1283af9ac57480dba2fb6fd6c360"}, + {file = "pycares-4.1.2.tar.gz", hash = "sha256:03490be0e7b51a0c8073f877bec347eff31003f64f57d9518d419d9369452837"}, ] pycodestyle = [ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, @@ -1376,8 +1424,8 @@ python-dateutil = [ {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] python-dotenv = [ - {file = "python-dotenv-0.15.0.tar.gz", hash = "sha256:587825ed60b1711daea4832cf37524dfd404325b7db5e25ebe88c495c9f807a0"}, - {file = "python_dotenv-0.15.0-py2.py3-none-any.whl", hash = "sha256:0c8d1b80d1a1e91717ea7d526178e3882732420b03f08afea0406db6402e220e"}, + {file = "python-dotenv-0.19.1.tar.gz", hash = "sha256:14f8185cc8d494662683e6914addcb7e95374771e707601dfc70166946b4c4b8"}, + {file = "python_dotenv-0.19.1-py2.py3-none-any.whl", hash = "sha256:bbd3da593fc49c249397cbfbcc449cf36cb02e75afc8157fcc6a81df6fb7750a"}, ] pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, @@ -1411,67 +1459,64 @@ pyyaml = [ {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, ] rapidfuzz = [ - {file = "rapidfuzz-1.5.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:6a951ad31ef121bacf40bbe6fbd387740d5038400ec2bfb41d037e9fd2754ef5"}, - {file = "rapidfuzz-1.5.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:b64acce4f6e745417218fc0bb6ff31b26fac0d723506b82ee4b9cad448b85ebb"}, - {file = "rapidfuzz-1.5.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:a834061e6d4dfb9672e89e28583486f60821796cf0d7cc559643a0d597ce33a9"}, - {file = "rapidfuzz-1.5.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:50548b919bc7608f7b9b4780415ddad135cfc3a54135bdb4bd0bb7ff2cdf9fdf"}, - {file = "rapidfuzz-1.5.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:c39c7f200eef49f4f9d6b808950709334e6f1c22262d570f1f77d6d3d373ad81"}, - {file = "rapidfuzz-1.5.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:347973ddf12d66d4d06daf1aca3a096a1bffe12306bcf13b832bdfc8db6d9f4a"}, - {file = "rapidfuzz-1.5.1-cp35-cp35m-manylinux2014_ppc64le.whl", hash = "sha256:e6cd6717d87d02dde2088c080b0851bdba970b77085b68e213a7b786dee4be88"}, - {file = "rapidfuzz-1.5.1-cp35-cp35m-manylinux2014_s390x.whl", hash = "sha256:ba956add4c34da019fb5e8f5e1768604b05569dd68055382795ad9062b9ca55e"}, - {file = "rapidfuzz-1.5.1-cp35-cp35m-win32.whl", hash = "sha256:95164076d8e0433f9f93e218270f19e3020a3a9b8db28a3d74143810d4243600"}, - {file = "rapidfuzz-1.5.1-cp35-cp35m-win_amd64.whl", hash = "sha256:274878c59440d6ad3efca833da61594836306af7dcdd914cc1b6ceb2d4cea23b"}, - {file = "rapidfuzz-1.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3b3df0c1a307a64273f6fd64c0f28218e002768eda1d94b9fffdab9371e38a6a"}, - {file = "rapidfuzz-1.5.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:55472932a8bcf008855b2cc8e5bc47d60066b504ef02dbf8d8fd43ddd8f20a6e"}, - {file = "rapidfuzz-1.5.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:170e71f2ec36a086ce5d2667331721cc9b779370d0ef7248ef6979819cd8fb09"}, - {file = "rapidfuzz-1.5.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:00734857b412afc35b29f0ea2f1d9ee26ff93d4cd3fa5f47bb90f6aef385f2a1"}, - {file = "rapidfuzz-1.5.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:ef0cf038c910a3ed626a3224effde8eb49dd7dcda87af59fcd37bc63b78a9bd1"}, - {file = "rapidfuzz-1.5.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9f454f79bc463e3de08c5d5c0f438fce1b1736cd4da1a1f47f72dc37da156552"}, - {file = "rapidfuzz-1.5.1-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:05e8dede642af1b38ebcf8fb5e9bbfdcdf8debba660ae1aafb5c1b0e6ca3e4de"}, - {file = "rapidfuzz-1.5.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:f08773adb7f21e1f530bad2c6ababaf472f80283650bc265a7e8f614480cd49c"}, - {file = "rapidfuzz-1.5.1-cp36-cp36m-win32.whl", hash = "sha256:4e1a7fb18d4a6c3d471a3ad8f820f179216de61bef74663342340cf9c685a31e"}, - {file = "rapidfuzz-1.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:09e992579c4aae59310e44db99ed848a8437ed1e8810a513d3bbab7ac7a8f215"}, - {file = "rapidfuzz-1.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38e2cd05869bd50f25b6d384e0cc73f8cfd6ebb8f1e7bdf1315384e21611f091"}, - {file = "rapidfuzz-1.5.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b173f8d4c9360b8b32b5ab7a669623f239cb85013e1749bdca03e1b3c297faa7"}, - {file = "rapidfuzz-1.5.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6ab75111e2216a48c7e01d47d8903fc2d0c1df398e7262a6df544d86812e40c7"}, - {file = "rapidfuzz-1.5.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:eb560622d9970eb0c615d5dff26af8a8647ba541a89a927fca1eb0862898f997"}, - {file = "rapidfuzz-1.5.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:59246615b819e4aff685aa57359f5bbaf02441cccc83e8899608037542a3fe36"}, - {file = "rapidfuzz-1.5.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:7a960dfedcf1acdb8435b5b00aebfc2ee8fd53b7b4f7acf613915b4c24fc0ef7"}, - {file = "rapidfuzz-1.5.1-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:3d727a7e09f1a01b61452c86d687d0564bad923d5d209224549ae790408d6449"}, - {file = "rapidfuzz-1.5.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:eb5e43dbef900367b91fb73a4c447efde034656b25b397844c8cf622dae84ac3"}, - {file = "rapidfuzz-1.5.1-cp37-cp37m-win32.whl", hash = "sha256:03ea226734cca3f86bc402fc04b8a38b795795e99dbf02dd834845b80bcf7588"}, - {file = "rapidfuzz-1.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:5e9beeb6643d663c410ad8ccf88eafbe59ba7aa9b34eea5b51c6312976789803"}, - {file = "rapidfuzz-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0d3cdb6ced024ed1567ba0be4b0909b17f691bd6e9e9f29626e4953ecf7cba9e"}, - {file = "rapidfuzz-1.5.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c06f98bb94fbad9b773c38a3e2cf28a315466b41f862917ba4d228052bcc0966"}, - {file = "rapidfuzz-1.5.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7bf51eb2ff342c4a0d77ab22b3d7de461ef9d2c480fd863c57fb139e7578fa7b"}, - {file = "rapidfuzz-1.5.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:6b0c74f60c03eed4b5d19f866df79c1d1bffc4c61f9bf31b114402c47680997f"}, - {file = "rapidfuzz-1.5.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:bbba42e244a0ebc1639c62ab44e4a172767d3721d2af48f2764ca00de7721479"}, - {file = "rapidfuzz-1.5.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:0b9f76f47b6df8c6aaa02a27fdff52e6aaf64d39296683ed06d0ec9acf2515d2"}, - {file = "rapidfuzz-1.5.1-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:c52ce4b4bfe8e0c2cf102f7b71cca00fc3228113e71712597940c9c340ae31b1"}, - {file = "rapidfuzz-1.5.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:0d3ae040c91f500814df6557276462c0c869b16168ef51d01c8a7da150f54513"}, - {file = "rapidfuzz-1.5.1-cp38-cp38-win32.whl", hash = "sha256:112ecc4825c5d362298d1e3c512d3f942c1a74f26ca69dc4b19a4f2cd95cb764"}, - {file = "rapidfuzz-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:b1feec7407df54045bc9d4dce3431ce20855c1ff4dd170480fbace62164f8f9c"}, - {file = "rapidfuzz-1.5.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8b352fe56b92bd2aa4ceae550543a923996c16efecf8f981c955dd5f522d2002"}, - {file = "rapidfuzz-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9454f46bc4007be9148f18143bb1b615a740a99737a38cf7b9baf3c495d5d17c"}, - {file = "rapidfuzz-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4c1a75c87a2f4c9709c6e3ecdbb2317f0964ac96f845f6a331d8a437a2944d24"}, - {file = "rapidfuzz-1.5.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:02f8b282a940cb749b1c51196baab7abb5590fcc8c065ce540c5d8414366036d"}, - {file = "rapidfuzz-1.5.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2def32a4228a717c5e6a699f0742546aee4091eb1e59e79781ceacabfc54452c"}, - {file = "rapidfuzz-1.5.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2e9d494ff51b942ed1504f84c13476319c89fc9bcc6379cc0816b776a7994199"}, - {file = "rapidfuzz-1.5.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:e94a6af7f0cc8ff49ab22842af255d8d927ca3b168b1a7e8e0784f1a2f49bc38"}, - {file = "rapidfuzz-1.5.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:c4ee8b9baaf46447dcaed925ad1d3606d3a375dfc5c67d1f3e33c46a3008cb5a"}, - {file = "rapidfuzz-1.5.1-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:10e236b3ce5851f584bbf178e7fb04ae5d0fbb008f3bc580ef6185bbbb346cd1"}, - {file = "rapidfuzz-1.5.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:068404913619182739fa3fde3079c17e3402744a1117df7f60055db331095a01"}, - {file = "rapidfuzz-1.5.1-cp39-cp39-win32.whl", hash = "sha256:e933e3ce2d88b7584248493abcba2cd27240f42bf73ca040babfd1ce8036750e"}, - {file = "rapidfuzz-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:46f5931f7441e13574d0fe33e897212d00ff63f69c0db1d449afbc5e87bafd7f"}, - {file = "rapidfuzz-1.5.1-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7fb25ee0340cc26dad0bb4a97019bf61b4cefaec67a1be64ac9dac2f98c697cd"}, - {file = "rapidfuzz-1.5.1-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:e738ec4e680bebe4442befda5cdd18020c3721d4cd75f9bfe2fb94e78ef55618"}, - {file = "rapidfuzz-1.5.1-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:cb083609923bc4ac602e6f1d61be61a25b35cccfb5ee208d2aa89eb0be357c69"}, - {file = "rapidfuzz-1.5.1-pp36-pypy36_pp73-win32.whl", hash = "sha256:08ecef2995b6ed1187b375d8f28ba4557522f098a1515b6afb0e3b452997a3a4"}, - {file = "rapidfuzz-1.5.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b6ff10d856fce55e2b1c681e4e7cd7da9b9eb6854571df60d6ed8904c777e64b"}, - {file = "rapidfuzz-1.5.1-pp37-pypy37_pp73-manylinux1_x86_64.whl", hash = "sha256:b41c346f16cd1ee71b259106d3cfad3347bd8fff4ff20f334a12738df6736c01"}, - {file = "rapidfuzz-1.5.1-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:f7148a53a0fd3466b82b81d94ad91aee7ce7947f37f16f9fb54319ea7df7f4af"}, - {file = "rapidfuzz-1.5.1-pp37-pypy37_pp73-win32.whl", hash = "sha256:31d83af9ac39f47f47ce4830ee118e6fa53964cccd8161e9a478a326f2a994cf"}, - {file = "rapidfuzz-1.5.1.tar.gz", hash = "sha256:4ebbd071425ee812548c301c60661a4f8faa5e5bcc97a6f0bef5b562585a8025"}, + {file = "rapidfuzz-1.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:91f094562c683802e6c972bce27a692dad70d6cd1114e626b29d990c3704c653"}, + {file = "rapidfuzz-1.8.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:4a20682121e245cf5ad2dbdd771360763ea11b77520632a1034c4bb9ad1e854c"}, + {file = "rapidfuzz-1.8.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8810e75d8f9c4453bbd6209c372bf97514359b0b5efff555caf85b15f8a9d862"}, + {file = "rapidfuzz-1.8.0-cp27-cp27m-win32.whl", hash = "sha256:00cf713d843735b5958d87294f08b05c653a593ced7c4120be34f5d26d7a320a"}, + {file = "rapidfuzz-1.8.0-cp27-cp27m-win_amd64.whl", hash = "sha256:2baca64e23a623e077f57e5470de21af2765af15aa1088676eb2d475e664eed0"}, + {file = "rapidfuzz-1.8.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:9bf7a6c61bacedd84023be356e057e1d209dd6997cfaa3c1cee77aa21d642f88"}, + {file = "rapidfuzz-1.8.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:61b6434e3341ca5158ecb371b1ceb4c1f6110563a72d28bdce4eb2a084493e47"}, + {file = "rapidfuzz-1.8.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e425e690383f6cf308e8c2e8d630fa9596f67d233344efd8fae11e70a9f5635f"}, + {file = "rapidfuzz-1.8.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:93db5e693b76d616b09df27ca5c79e0dda169af7f1b8f5ab3262826d981e37e2"}, + {file = "rapidfuzz-1.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a8c4f76ed1c8a65892d98dc2913027c9acdb219d18f3a441cfa427a32861af9"}, + {file = "rapidfuzz-1.8.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71e217fd30901214cc96c0c15057278bafb7072aa9b2be4c97459c1fedf3e731"}, + {file = "rapidfuzz-1.8.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d579dd447b8e851462e79054b68f94b66b09df8b3abb2aa5ca07fe00912ef5e8"}, + {file = "rapidfuzz-1.8.0-cp310-cp310-win32.whl", hash = "sha256:5808064555273496dcd594d659bd28ee8d399149dd31575321034424455dc955"}, + {file = "rapidfuzz-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:798fef1671ca66c78b47802228e9583f7ab32b99bdfe3984ebb1f96e93e38b5f"}, + {file = "rapidfuzz-1.8.0-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:c9e0ed210831f5c73533bf11099ea7897db491e76c3443bef281d9c1c67d7f3a"}, + {file = "rapidfuzz-1.8.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:c819bb19eb615a31ddc9cb8248a285bf04f58158b53ce096451178631f99b652"}, + {file = "rapidfuzz-1.8.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:942ee45564f28ef70320d1229f02dc998bd93e3519c1f3a80f33ce144b51039c"}, + {file = "rapidfuzz-1.8.0-cp35-cp35m-win32.whl", hash = "sha256:7e6ae2e5a3bc9acc51e118f25d32b8efcd431c5d8deb408336dd2ed0f21d087c"}, + {file = "rapidfuzz-1.8.0-cp35-cp35m-win_amd64.whl", hash = "sha256:98901fba67c89ad2506f3946642cf6eb8f489592fb7eb307ebdf8bdb0c4e97f9"}, + {file = "rapidfuzz-1.8.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:705e1686f406a0c77ef323cdb7369b7cf9e68f2abfcb83ff5f1e0a5b21f5a534"}, + {file = "rapidfuzz-1.8.0-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:da0c5fe5fdbbd74206c1778af6b8c5ff8dfbe2dd04ae12bbe96642b358acefce"}, + {file = "rapidfuzz-1.8.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:535253bc9224215131ae450aad6c9f7ef1b24f15c685045eab2b52511268bd06"}, + {file = "rapidfuzz-1.8.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acdad83f07d886705fce164b0d1f4e3b56788a205602ed3a7fc8b10ceaf05fbf"}, + {file = "rapidfuzz-1.8.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35097f649831f8375d6c65a237deccac3aceb573aa7fae1e5d3fa942e89de1c8"}, + {file = "rapidfuzz-1.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6f4db142e5b4b44314166a90e11603220db659bd2f9c23dd5db402c13eac8eb7"}, + {file = "rapidfuzz-1.8.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:19a3f55f27411d68360540484874beda0b428b062596d5f0f141663ef0738bfd"}, + {file = "rapidfuzz-1.8.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22b4c1a7f6fe29bd8dae49f7d5ab085dc42c3964f1a78b6dca22fdf83b5c9bfa"}, + {file = "rapidfuzz-1.8.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8bfb2fbc147904b78d5c510ee75dc8704b606e956df23f33a9e89abc03f45c3"}, + {file = "rapidfuzz-1.8.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6dc5111ebfed2c4f2e4d120a9b280ea13ea4fbb60b6915dd239817b4fc092ed"}, + {file = "rapidfuzz-1.8.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db5ee2457d97cb967ffe08446a8c595c03fe747fdc2e145266713f9c516d1c4a"}, + {file = "rapidfuzz-1.8.0-cp37-cp37m-win32.whl", hash = "sha256:12c1b78cc15fc26f555a4bf66088d5afb6354b5a5aa149a123f01a15af6c411b"}, + {file = "rapidfuzz-1.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:693e9579048d8db4ff020715dd6f25aa315fd6445bc94e7400d7a94a227dad27"}, + {file = "rapidfuzz-1.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b4fe19df3edcf7de359448b872aec08e6592b4ca2d3df4d8ee57b5812d68bebf"}, + {file = "rapidfuzz-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f3670b9df0e1f479637cad1577afca7766a02775dc08c14837cf495c82861d7c"}, + {file = "rapidfuzz-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:61d118f36eb942649b0db344f7b7a19ad7e9b5749d831788187eb03b57ce1bfa"}, + {file = "rapidfuzz-1.8.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fce3a2c8a1d10da12aff4a0d367624e8ae9e15c1b84a5144843681d39be0c355"}, + {file = "rapidfuzz-1.8.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1577ef26e3647ccc4cc9754c34ffaa731639779f4d7779e91a761c72adac093e"}, + {file = "rapidfuzz-1.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fec9b7e60fde51990c3b48fc1aa9dba9ac3acaf78f623dbb645a6fe21a9654e"}, + {file = "rapidfuzz-1.8.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b954469d93858bc8b48129bc63fd644382a4df5f3fb1b4b290f48eac1d00a2da"}, + {file = "rapidfuzz-1.8.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:190ba709069a7e5a6b39b7c8bc413a08cfa7f1f4defec5d974c4128b510e0234"}, + {file = "rapidfuzz-1.8.0-cp38-cp38-win32.whl", hash = "sha256:97b2d13d6323649b43d1b113681e4013ba230bd6e9827cc832dcebee447d7250"}, + {file = "rapidfuzz-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:81c3091209b75f6611efe2af18834180946d4ce28f41ca8d44fce816187840d2"}, + {file = "rapidfuzz-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d610afa33e92aa0481a514ffda3ec51ca5df3c684c1c1c795307589c62025931"}, + {file = "rapidfuzz-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d976f33ca6b5fabbb095c0a662f5b86baf706184fc24c7f125d4ddb54b8bf036"}, + {file = "rapidfuzz-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0f5ca7bca2af598d4ddcf5b93b64b50654a9ff684e6f18d865f6e13fee442b3e"}, + {file = "rapidfuzz-1.8.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2aac5ea6b0306dcd28a6d1a89d35ed2c6ac426f2673ee1b92cf3f1d0fd5cd"}, + {file = "rapidfuzz-1.8.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f145c9831c0454a696a3136a6380ea4e01434e9cc2f2bc10d032864c16d1d0e5"}, + {file = "rapidfuzz-1.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4ce53291575b56c9d45add73ea013f43bafcea55eee9d5139aa759918d7685f"}, + {file = "rapidfuzz-1.8.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de5773a39c00a0f23cfc5da9e0e5fd0fb512b0ebe23dc7289a38e1f9a4b5cefc"}, + {file = "rapidfuzz-1.8.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87a802e55792bfbe192e2d557f38867dbe3671b49b3d5ecd873859c7460746ba"}, + {file = "rapidfuzz-1.8.0-cp39-cp39-win32.whl", hash = "sha256:9391abf1121df831316222f28cea37397a0f72bd7978f3be6e7da29a7821e4e5"}, + {file = "rapidfuzz-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:9eeca1b436042b5523dcf314f5822b1131597898c1d967f140d1917541a8a3d1"}, + {file = "rapidfuzz-1.8.0-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:a01f2495aca479b49d3b3a8863d6ba9bea2043447a1ced74ae5ec5270059cbc1"}, + {file = "rapidfuzz-1.8.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:b7d4b1a5d16817f8cdb34365c7b58ae22d5cf1b3207720bb2fa0b55968bdb034"}, + {file = "rapidfuzz-1.8.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c738d0d7f1744646d48d19b4c775926082bcefebd2460f45ca383a0e882f5672"}, + {file = "rapidfuzz-1.8.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0fb9c6078c17c12b52e66b7d0a2a1674f6bbbdc6a76e454c8479b95147018123"}, + {file = "rapidfuzz-1.8.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1482b385d83670eb069577c9667f72b41eec4f005aee32f1a4ff4e71e88afde2"}, + {file = "rapidfuzz-1.8.0.tar.gz", hash = "sha256:83fff37acf0367314879231264169dcbc5e7de969a94f4b82055d06a7fddab9a"}, ] redis = [ {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"}, @@ -1498,8 +1543,8 @@ soupsieve = [ {file = "soupsieve-2.2.1.tar.gz", hash = "sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc"}, ] taskipy = [ - {file = "taskipy-1.8.1-py3-none-any.whl", hash = "sha256:2b98f499966e40175d1f1306a64587f49dfa41b90d0d86c8f28b067cc58d0a56"}, - {file = "taskipy-1.8.1.tar.gz", hash = "sha256:7a2404125817e45d80e13fa663cae35da6e8ba590230094e815633653e25f98f"}, + {file = "taskipy-1.9.0-py3-none-any.whl", hash = "sha256:02bd2c51c7356ed3f7f8853210ada1cd2ab273e68359ee865021c3057eec6615"}, + {file = "taskipy-1.9.0.tar.gz", hash = "sha256:449c160b557cdb1d9c17097a5ea4aa0cd5223723ddbaaa5d5032dd16274fb8f0"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, @@ -1511,49 +1556,84 @@ typing-extensions = [ {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, ] urllib3 = [ - {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, - {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, + {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, + {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, ] virtualenv = [ - {file = "virtualenv-20.7.2-py2.py3-none-any.whl", hash = "sha256:e4670891b3a03eb071748c569a87cceaefbf643c5bac46d996c5a45c34aa0f06"}, - {file = "virtualenv-20.7.2.tar.gz", hash = "sha256:9ef4e8ee4710826e98ff3075c9a4739e2cb1040de6a2a8d35db0055840dc96a0"}, + {file = "virtualenv-20.8.1-py2.py3-none-any.whl", hash = "sha256:10062e34c204b5e4ec5f62e6ef2473f8ba76513a9a617e873f1f8fb4a519d300"}, + {file = "virtualenv-20.8.1.tar.gz", hash = "sha256:bcc17f0b3a29670dd777d6f0755a4c04f28815395bca279cdcb213b97199a6b8"}, ] yarl = [ - {file = "yarl-1.6.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434"}, - {file = "yarl-1.6.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478"}, - {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6"}, - {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e"}, - {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406"}, - {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76"}, - {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366"}, - {file = "yarl-1.6.3-cp36-cp36m-win32.whl", hash = "sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721"}, - {file = "yarl-1.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643"}, - {file = "yarl-1.6.3-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e"}, - {file = "yarl-1.6.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3"}, - {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8"}, - {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a"}, - {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c"}, - {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f"}, - {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970"}, - {file = "yarl-1.6.3-cp37-cp37m-win32.whl", hash = "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e"}, - {file = "yarl-1.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50"}, - {file = "yarl-1.6.3-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2"}, - {file = "yarl-1.6.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec"}, - {file = "yarl-1.6.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"}, - {file = "yarl-1.6.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc"}, - {file = "yarl-1.6.3-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959"}, - {file = "yarl-1.6.3-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2"}, - {file = "yarl-1.6.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2"}, - {file = "yarl-1.6.3-cp38-cp38-win32.whl", hash = "sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896"}, - {file = "yarl-1.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a"}, - {file = "yarl-1.6.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e"}, - {file = "yarl-1.6.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724"}, - {file = "yarl-1.6.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c"}, - {file = "yarl-1.6.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25"}, - {file = "yarl-1.6.3-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96"}, - {file = "yarl-1.6.3-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0"}, - {file = "yarl-1.6.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4"}, - {file = "yarl-1.6.3-cp39-cp39-win32.whl", hash = "sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424"}, - {file = "yarl-1.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6"}, - {file = "yarl-1.6.3.tar.gz", hash = "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10"}, + {file = "yarl-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e35d8230e4b08d86ea65c32450533b906a8267a87b873f2954adeaecede85169"}, + {file = "yarl-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb4b3f277880c314e47720b4b6bb2c85114ab3c04c5442c9bc7006b3787904d8"}, + {file = "yarl-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7015dcedb91d90a138eebdc7e432aec8966e0147ab2a55f2df27b1904fa7291"}, + {file = "yarl-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb3e478175e15e00d659fb0354a6a8db71a7811a2a5052aed98048bc972e5d2b"}, + {file = "yarl-1.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8c409aa3a7966647e7c1c524846b362a6bcbbe120bf8a176431f940d2b9a2e"}, + {file = "yarl-1.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b22ea41c7e98170474a01e3eded1377d46b2dfaef45888a0005c683eaaa49285"}, + {file = "yarl-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a7dfc46add4cfe5578013dbc4127893edc69fe19132d2836ff2f6e49edc5ecd6"}, + {file = "yarl-1.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:82ff6f85f67500a4f74885d81659cd270eb24dfe692fe44e622b8a2fd57e7279"}, + {file = "yarl-1.7.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f3cd2158b2ed0fb25c6811adfdcc47224efe075f2d68a750071dacc03a7a66e4"}, + {file = "yarl-1.7.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:59c0f13f9592820c51280d1cf811294d753e4a18baf90f0139d1dc93d4b6fc5f"}, + {file = "yarl-1.7.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7f7655ad83d1a8afa48435a449bf2f3009293da1604f5dd95b5ddcf5f673bd69"}, + {file = "yarl-1.7.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aa9f0d9b62d15182341b3e9816582f46182cab91c1a57b2d308b9a3c4e2c4f78"}, + {file = "yarl-1.7.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fdd1b90c225a653b1bd1c0cae8edf1957892b9a09c8bf7ee6321eeb8208eac0f"}, + {file = "yarl-1.7.0-cp310-cp310-win32.whl", hash = "sha256:7c8d0bb76eabc5299db203e952ec55f8f4c53f08e0df4285aac8c92bd9e12675"}, + {file = "yarl-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:622a36fa779efb4ff9eff5fe52730ff17521431379851a31e040958fc251670c"}, + {file = "yarl-1.7.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3d461b7a8e139b9e4b41f62eb417ffa0b98d1c46d4caf14c845e6a3b349c0bb1"}, + {file = "yarl-1.7.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81cfacdd1e40bc931b5519499342efa388d24d262c30a3d31187bfa04f4a7001"}, + {file = "yarl-1.7.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:821b978f2152be7695d4331ef0621d207aedf9bbd591ba23a63412a3efc29a01"}, + {file = "yarl-1.7.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b64bd24c8c9a487f4a12260dc26732bf41028816dbf0c458f17864fbebdb3131"}, + {file = "yarl-1.7.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:98c9ddb92b60a83c21be42c776d3d9d5ec632a762a094c41bda37b7dfbd2cd83"}, + {file = "yarl-1.7.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a532d75ca74431c053a88a802e161fb3d651b8bf5821a3440bc3616e38754583"}, + {file = "yarl-1.7.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:053e09817eafb892e94e172d05406c1b3a22a93bc68f6eff5198363a3d764459"}, + {file = "yarl-1.7.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:98c51f02d542945d306c8e934aa2c1e66ba5e9c1c86b5bf37f3a51c8a747067e"}, + {file = "yarl-1.7.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:15ec41a5a5fdb7bace6d7b16701f9440007a82734f69127c0fbf6d87e10f4a1e"}, + {file = "yarl-1.7.0-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:a7f08819dba1e1255d6991ed37448a1bf4b1352c004bcd899b9da0c47958513d"}, + {file = "yarl-1.7.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8e3ffab21db0542ffd1887f3b9575ddd58961f2cf61429cb6458afc00c4581e0"}, + {file = "yarl-1.7.0-cp36-cp36m-win32.whl", hash = "sha256:50127634f519b2956005891507e3aa4ac345f66a7ea7bbc2d7dcba7401f41898"}, + {file = "yarl-1.7.0-cp36-cp36m-win_amd64.whl", hash = "sha256:36ec44f15193f6d5288d42ebb8e751b967ebdfb72d6830983838d45ab18edb4f"}, + {file = "yarl-1.7.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ec1b5a25a25c880c976d0bb3d107def085bb08dbb3db7f4442e0a2b980359d24"}, + {file = "yarl-1.7.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b36f5a63c891f813c6f04ef19675b382efc190fd5ce7e10ab19386d2548bca06"}, + {file = "yarl-1.7.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38173b8c3a29945e7ecade9a3f6ff39581eee8201338ee6a2c8882db5df3e806"}, + {file = "yarl-1.7.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ba402f32184f0b405fb281b93bd0d8ab7e3257735b57b62a6ed2e94cdf4fe50"}, + {file = "yarl-1.7.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:be52bc5208d767cdd8308a9e93059b3b36d1e048fecbea0e0346d0d24a76adc0"}, + {file = "yarl-1.7.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:08c2044a956f4ef30405f2f433ce77f1f57c2c773bf81ae43201917831044d5a"}, + {file = "yarl-1.7.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:484d61c047c45670ef5967653a1d0783e232c54bf9dd786a7737036828fa8d54"}, + {file = "yarl-1.7.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b7de92a4af85cfcaf4081f8aa6165b1d63ee5de150af3ee85f954145f93105a7"}, + {file = "yarl-1.7.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:376e41775aab79c5575534924a386c8e0f1a5d91db69fc6133fd27a489bcaf10"}, + {file = "yarl-1.7.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:8a8b10d0e7bac154f959b709fcea593cda527b234119311eb950096653816a86"}, + {file = "yarl-1.7.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:f46cd4c43e6175030e2a56def8f1d83b64e6706eeb2bb9ab0ef4756f65eab23f"}, + {file = "yarl-1.7.0-cp37-cp37m-win32.whl", hash = "sha256:b28cfb46140efe1a6092b8c5c4994a1fe70dc83c38fbcea4992401e0c6fb9cce"}, + {file = "yarl-1.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9624154ec9c02a776802da1086eed7f5034bd1971977f5146233869c2ac80297"}, + {file = "yarl-1.7.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:69945d13e1bbf81784a9bc48824feb9cd66491e6a503d4e83f6cd7c7cc861361"}, + {file = "yarl-1.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:46a742ed9e363bd01be64160ce7520e92e11989bd4cb224403cfd31c101cc83d"}, + {file = "yarl-1.7.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb4ff1ac7cb4500f43581b3f4cbd627d702143aa6be1fdc1fa3ebffaf4dc1be5"}, + {file = "yarl-1.7.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ad51e17cd65ea3debb0e10f0120cf8dd987c741fe423ed2285087368090b33d"}, + {file = "yarl-1.7.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7e37786ea89a5d3ffbbf318ea9790926f8dfda83858544f128553c347ad143c6"}, + {file = "yarl-1.7.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c63c1e208f800daad71715786bfeb1cecdc595d87e2e9b1cd234fd6e597fd71d"}, + {file = "yarl-1.7.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:91cbe24300c11835ef186436363352b3257db7af165e0a767f4f17aa25761388"}, + {file = "yarl-1.7.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e510dbec7c59d32eaa61ffa48173d5e3d7170a67f4a03e8f5e2e9e3971aca622"}, + {file = "yarl-1.7.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3def6e681cc02397e5d8141ee97b41d02932b2bcf0fb34532ad62855eab7c60e"}, + {file = "yarl-1.7.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:263c81b94e6431942b27f6f671fa62f430a0a5c14bb255f2ab69eeb9b2b66ff7"}, + {file = "yarl-1.7.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e78c91faefe88d601ddd16e3882918dbde20577a2438e2320f8239c8b7507b8f"}, + {file = "yarl-1.7.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:22b2430c49713bfb2f0a0dd4a8d7aab218b28476ba86fd1c78ad8899462cbcf2"}, + {file = "yarl-1.7.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2e7ad9db939082f5d0b9269cfd92c025cb8f2fbbb1f1b9dc5a393c639db5bd92"}, + {file = "yarl-1.7.0-cp38-cp38-win32.whl", hash = "sha256:3a31e4a8dcb1beaf167b7e7af61b88cb961b220db8d3ba1c839723630e57eef7"}, + {file = "yarl-1.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:d579957439933d752358c6a300c93110f84aae67b63dd0c19dde6ecbf4056f6b"}, + {file = "yarl-1.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:87721b549505a546eb003252185103b5ec8147de6d3ad3714d148a5a67b6fe53"}, + {file = "yarl-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1fa866fa24d9f4108f9e58ea8a2135655419885cdb443e36b39a346e1181532"}, + {file = "yarl-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1d3b8449dfedfe94eaff2b77954258b09b24949f6818dfa444b05dbb05ae1b7e"}, + {file = "yarl-1.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db2372e350794ce8b9f810feb094c606b7e0e4aa6807141ac4fadfe5ddd75bb0"}, + {file = "yarl-1.7.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a06d9d0b9a97fa99b84fee71d9dd11e69e21ac8a27229089f07b5e5e50e8d63c"}, + {file = "yarl-1.7.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3455c2456d6307bcfa80bc1157b8603f7d93573291f5bdc7144489ca0df4628"}, + {file = "yarl-1.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d30d67e3486aea61bb2cbf7cf81385364c2e4f7ce7469a76ed72af76a5cdfe6b"}, + {file = "yarl-1.7.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c18a4b286e8d780c3a40c31d7b79836aa93b720f71d5743f20c08b7e049ca073"}, + {file = "yarl-1.7.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d54c925396e7891666cabc0199366ca55b27d003393465acef63fd29b8b7aa92"}, + {file = "yarl-1.7.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:64773840952de17851a1c7346ad7f71688c77e74248d1f0bc230e96680f84028"}, + {file = "yarl-1.7.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:acbf1756d9dc7cd0ae943d883be72e84e04396f6c2ff93a6ddeca929d562039f"}, + {file = "yarl-1.7.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:2e48f27936aa838939c798f466c851ba4ae79e347e8dfce43b009c64b930df12"}, + {file = "yarl-1.7.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1beef4734ca1ad40a9d8c6b20a76ab46e3a2ed09f38561f01e4aa2ea82cafcef"}, + {file = "yarl-1.7.0-cp39-cp39-win32.whl", hash = "sha256:8ee78c9a5f3c642219d4607680a4693b59239c27a3aa608b64ef79ddc9698039"}, + {file = "yarl-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:d750503682605088a14d29a4701548c15c510da4f13c8b17409c4097d5b04c52"}, + {file = "yarl-1.7.0.tar.gz", hash = "sha256:8e7ebaf62e19c2feb097ffb7c94deb0f0c9fab52590784c8cd679d30ab009162"}, ] diff --git a/pyproject.toml b/pyproject.toml index 08287b23..88a974ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ flake8-string-format = "~=0.3" flake8-tidy-imports = "~=4.1" flake8-todo = "~=0.7" pep8-naming = "~=0.11" -pip-licenses = "~=3.5.2" +pip-licenses = "~=3.5" pre-commit = "~=2.1" python-dotenv = "~=0.15" taskipy = "~=1.6" -- cgit v1.2.3 From 93f8385fcaa543b7deb69e7c7740cd148be6297c Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Thu, 21 Oct 2021 22:29:54 -0400 Subject: Add WTF Python Command (#859) * Add WTF Python Command * Fix grammar in docstrings, remove redundant variable, remove the use of a wrapper * Fix indentation issues and make use of triple quotes * Update docstrings and remove redundant list() * Change minimum certainty to 75. * Make 'make_embed' function a non async function * Try to unload WTFPython Extension if max fetch requests hit i.e. 3 else try to load the extension. * Correct log messages. * Make flake8 happy :D * Remove redundant class attributes and async functions. * Apply requested grammar and style changes. * Fix unload and load extension logic. * Fix typo in `WTF_PYTHON_RAW_URL` * Changed fuzzy_wuzzy to rapidfuzz Since rapidfuzz also has an extractOne method, this should be a straight replacement with the import statement. * Move wtf_python.py to bot/exts/utilities, flake8 Moved the file to the correct location after merge with main, made changes from the last open suggestions from the previous PR, had to make WTF lowercase to pass flake8 on lines 54 and 118. * Fix trailing commas and long lines * # This is a combination of 3 commits. # This is the 1st commit message: Squashing small commits Small changes and fixes -Added "the" to setup docstring -Fixed typo for mis-matched WTF and wtf in get_wtf_python_readme -Fixed ext location -Added more information to fuzzy_match_header docstring regarding the MINIMUM_CERTAINTY and what the score / value represents. Add wildcard to capture unused return Updated MINIMUM_CERTAINTY to 75 Change MINIMUM_CERTAINTY to 50 Squash commits from Bluenix suggestions Fix docstring for fuzzy_match_header Swap if / else for match Fix functools import Rename get_wtf_python_readme to fetch_readme Collapse self.headers into one line Fix docstring for fuzzy_match_header Swap if / else for match # This is the commit message #2: Fix functools import # This is the commit message #3: Rename get_wtf_python_readme to fetch_readme * Squashing commits Squashing small commits Small changes and fixes -Added "the" to setup docstring -Fixed typo for mis-matched WTF and wtf in get_wtf_python_readme -Fixed ext location -Added more information to fuzzy_match_header docstring regarding the MINIMUM_CERTAINTY and what the score / value represents. Add wildcard to capture unused return Updated MINIMUM_CERTAINTY to 75 Change MINIMUM_CERTAINTY to 50 Squash commits from Bluenix suggestions Fix docstring for fuzzy_match_header Swap if / else for match Fix functools import Rename get_wtf_python_readme to fetch_readme Collapse self.headers into one line Fix docstring for fuzzy_match_header Swap if / else for match Fix functools import Rename get_wtf_python_readme to fetch_readme Collapse self.headers into one line Fix type hints with dict Add match comment for clarity * Add debug logs, and send embed * Add markdown file creation Big change here is to create a .md file based on the matched header. I save the raw text as a class attribute, then slice it based on the index returned by the .find() method for the header, and the separator "/n---/n". * Move the list(map(str.strip , ...) to for loop * Remove line * Use StringIO for file creation * Update file creation with StringIO * Remove embed file preview * chore: update wtf_python docstring * chore: change regex to search, remove file preview * feat: update caching as recommended Minor fixes to import statements as well. Co-authored-by: Bluenix2 * chore: remove logging statements * feat: scheduled task for fetch_readme * chore: fix hyperlink, remove dead code * fix: capitalization clean up * chore: remove unused code * chore: remove more unused code * feat: add light grey logo image in embed * feat: add light grey image * chore: remove debug log message * feat: add found search result header * feat: limit user query to 50 characters * cleanup: remove debug logging * fix: restructure if not match statement Co-authored-by: Bluenix Co-authored-by: Shivansh-007 Co-authored-by: Shivansh-007 Co-authored-by: Bluenix2 Co-authored-by: Xithrius <15021300+Xithrius@users.noreply.github.com> --- bot/exts/utilities/wtf_python.py | 126 ++++++++++++++++++++++++++++ bot/resources/utilities/wtf_python_logo.jpg | Bin 0 -> 19481 bytes 2 files changed, 126 insertions(+) create mode 100644 bot/exts/utilities/wtf_python.py create mode 100644 bot/resources/utilities/wtf_python_logo.jpg diff --git a/bot/exts/utilities/wtf_python.py b/bot/exts/utilities/wtf_python.py new file mode 100644 index 00000000..66a022d7 --- /dev/null +++ b/bot/exts/utilities/wtf_python.py @@ -0,0 +1,126 @@ +import logging +import random +import re +from typing import Optional + +import rapidfuzz +from discord import Embed, File +from discord.ext import commands, tasks + +from bot import constants +from bot.bot import Bot + +log = logging.getLogger(__name__) + +WTF_PYTHON_RAW_URL = "http://raw.githubusercontent.com/satwikkansal/wtfpython/master/" +BASE_URL = "https://github.com/satwikkansal/wtfpython" +LOGO_PATH = "./bot/resources/utilities/wtf_python_logo.jpg" + +ERROR_MESSAGE = f""" +Unknown WTF Python Query. Please try to reformulate your query. + +**Examples**: +```md +{constants.Client.prefix}wtf wild imports +{constants.Client.prefix}wtf subclass +{constants.Client.prefix}wtf del +``` +If the problem persists send a message in <#{constants.Channels.dev_contrib}> +""" + +MINIMUM_CERTAINTY = 55 + + +class WTFPython(commands.Cog): + """Cog that allows getting WTF Python entries from the WTF Python repository.""" + + def __init__(self, bot: Bot): + self.bot = bot + self.headers: dict[str, str] = {} + self.fetch_readme.start() + + @tasks.loop(minutes=60) + async def fetch_readme(self) -> None: + """Gets the content of README.md from the WTF Python Repository.""" + async with self.bot.http_session.get(f"{WTF_PYTHON_RAW_URL}README.md") as resp: + log.trace("Fetching the latest WTF Python README.md") + if resp.status == 200: + raw = await resp.text() + self.parse_readme(raw) + + def parse_readme(self, data: str) -> None: + """ + Parses the README.md into a dict. + + It parses the readme into the `self.headers` dict, + where the key is the heading and the value is the + link to the heading. + """ + # Match the start of examples, until the end of the table of contents (toc) + table_of_contents = re.search( + r"\[πŸ‘€ Examples\]\(#-examples\)\n([\w\W]*)", data + )[0].split("\n") + + for header in list(map(str.strip, table_of_contents)): + match = re.search(r"\[β–Ά (.*)\]\((.*)\)", header) + if match: + hyper_link = match[0].split("(")[1].replace(")", "") + self.headers[match[0]] = f"{BASE_URL}/{hyper_link}" + + def fuzzy_match_header(self, query: str) -> Optional[str]: + """ + Returns the fuzzy match of a query if its ratio is above "MINIMUM_CERTAINTY" else returns None. + + "MINIMUM_CERTAINTY" is the lowest score at which the fuzzy match will return a result. + The certainty returned by rapidfuzz.process.extractOne is a score between 0 and 100, + with 100 being a perfect match. + """ + match, certainty, _ = rapidfuzz.process.extractOne(query, self.headers.keys()) + return match if certainty > MINIMUM_CERTAINTY else None + + @commands.command(aliases=("wtf", "WTF")) + async def wtf_python(self, ctx: commands.Context, *, query: str) -> None: + """ + Search WTF Python repository. + + Gets the link of the fuzzy matched query from https://github.com/satwikkansal/wtfpython. + Usage: + --> .wtf wild imports + """ + if len(query) > 50: + embed = Embed( + title=random.choice(constants.ERROR_REPLIES), + description=ERROR_MESSAGE, + colour=constants.Colours.soft_red, + ) + match = None + else: + match = self.fuzzy_match_header(query) + + if not match: + embed = Embed( + title=random.choice(constants.ERROR_REPLIES), + description=ERROR_MESSAGE, + colour=constants.Colours.soft_red, + ) + await ctx.send(embed=embed) + return + + embed = Embed( + title="WTF Python?!", + colour=constants.Colours.dark_green, + description=f"""Search result for '{query}': {match.split("]")[0].replace("[", "")} + [Go to Repository Section]({self.headers[match]})""", + ) + logo = File(LOGO_PATH, filename="wtf_logo.jpg") + embed.set_thumbnail(url="attachment://wtf_logo.jpg") + await ctx.send(embed=embed, file=logo) + + def cog_unload(self) -> None: + """Unload the cog and cancel the task.""" + self.fetch_readme.cancel() + + +def setup(bot: Bot) -> None: + """Load the WTFPython Cog.""" + bot.add_cog(WTFPython(bot)) diff --git a/bot/resources/utilities/wtf_python_logo.jpg b/bot/resources/utilities/wtf_python_logo.jpg new file mode 100644 index 00000000..851d7f9a Binary files /dev/null and b/bot/resources/utilities/wtf_python_logo.jpg differ -- cgit v1.2.3 From 817c5769a16081f9ddb2d88b9ed36ea18661b289 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Fri, 22 Oct 2021 07:23:34 +0000 Subject: Add ISort to our toolchain --- poetry.lock | 174 +++++++++++++++++++++++++++++++++++---------------------- pyproject.toml | 12 +++- 2 files changed, 118 insertions(+), 68 deletions(-) diff --git a/poetry.lock b/poetry.lock index cbfc90fb..8a4091f1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -286,15 +286,20 @@ flake8 = ">=3" pydocstyle = ">=2.1" [[package]] -name = "flake8-import-order" -version = "0.18.1" -description = "Flake8 and pylama plugin that checks the ordering of import statements." +name = "flake8-isort" +version = "4.1.1" +description = "flake8 plugin that integrates isort ." category = "dev" optional = false python-versions = "*" [package.dependencies] -pycodestyle = "*" +flake8 = ">=3.2.1,<5" +isort = ">=4.3.5,<6" +testfixtures = ">=6.8.0,<7" + +[package.extras] +test = ["pytest-cov"] [[package]] name = "flake8-polyfill" @@ -367,6 +372,20 @@ category = "main" optional = false python-versions = ">=3.5" +[[package]] +name = "isort" +version = "5.9.3" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] + [[package]] name = "kiwisolver" version = "1.3.2" @@ -629,7 +648,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "rapidfuzz" -version = "1.8.0" +version = "1.8.1" description = "rapid fuzzy string matching" category = "main" optional = false @@ -723,6 +742,19 @@ mslex = {version = ">=0.3.0,<0.4.0", markers = "sys_platform == \"win32\""} psutil = ">=5.7.2,<6.0.0" toml = ">=0.10.0,<0.11.0" +[[package]] +name = "testfixtures" +version = "6.18.3" +description = "A collection of helpers and mock objects for unit tests and doc tests." +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +build = ["setuptools-git", "wheel", "twine"] +docs = ["sphinx", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"] +test = ["pytest (>=3.6)", "pytest-cov", "pytest-django", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"] + [[package]] name = "toml" version = "0.10.2" @@ -786,7 +818,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "3a470d4a9b63f106bfead96094c73a865dfa3d01e8c187e43d415ea6cd83f5bf" +content-hash = "ff67b082d7226e0098fb00a7cefb457da9067d0a5d9ade708e26c8c8362bd5bf" [metadata.files] aiodns = [ @@ -965,9 +997,9 @@ flake8-docstrings = [ {file = "flake8-docstrings-1.6.0.tar.gz", hash = "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"}, {file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"}, ] -flake8-import-order = [ - {file = "flake8-import-order-0.18.1.tar.gz", hash = "sha256:a28dc39545ea4606c1ac3c24e9d05c849c6e5444a50fb7e9cdd430fc94de6e92"}, - {file = "flake8_import_order-0.18.1-py2.py3-none-any.whl", hash = "sha256:90a80e46886259b9c396b578d75c749801a41ee969a235e163cfe1be7afd2543"}, +flake8-isort = [ + {file = "flake8-isort-4.1.1.tar.gz", hash = "sha256:d814304ab70e6e58859bc5c3e221e2e6e71c958e7005239202fee19c24f82717"}, + {file = "flake8_isort-4.1.1-py3-none-any.whl", hash = "sha256:c4e8b6dcb7be9b71a02e6e5d4196cefcef0f3447be51e82730fb336fff164949"}, ] flake8-polyfill = [ {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"}, @@ -1035,6 +1067,10 @@ idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] +isort = [ + {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, + {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"}, +] kiwisolver = [ {file = "kiwisolver-1.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1d819553730d3c2724582124aee8a03c846ec4362ded1034c16fb3ef309264e6"}, {file = "kiwisolver-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d93a1095f83e908fc253f2fb569c2711414c0bfd451cab580466465b235b470"}, @@ -1459,64 +1495,64 @@ pyyaml = [ {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, ] rapidfuzz = [ - {file = "rapidfuzz-1.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:91f094562c683802e6c972bce27a692dad70d6cd1114e626b29d990c3704c653"}, - {file = "rapidfuzz-1.8.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:4a20682121e245cf5ad2dbdd771360763ea11b77520632a1034c4bb9ad1e854c"}, - {file = "rapidfuzz-1.8.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8810e75d8f9c4453bbd6209c372bf97514359b0b5efff555caf85b15f8a9d862"}, - {file = "rapidfuzz-1.8.0-cp27-cp27m-win32.whl", hash = "sha256:00cf713d843735b5958d87294f08b05c653a593ced7c4120be34f5d26d7a320a"}, - {file = "rapidfuzz-1.8.0-cp27-cp27m-win_amd64.whl", hash = "sha256:2baca64e23a623e077f57e5470de21af2765af15aa1088676eb2d475e664eed0"}, - {file = "rapidfuzz-1.8.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:9bf7a6c61bacedd84023be356e057e1d209dd6997cfaa3c1cee77aa21d642f88"}, - {file = "rapidfuzz-1.8.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:61b6434e3341ca5158ecb371b1ceb4c1f6110563a72d28bdce4eb2a084493e47"}, - {file = "rapidfuzz-1.8.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e425e690383f6cf308e8c2e8d630fa9596f67d233344efd8fae11e70a9f5635f"}, - {file = "rapidfuzz-1.8.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:93db5e693b76d616b09df27ca5c79e0dda169af7f1b8f5ab3262826d981e37e2"}, - {file = "rapidfuzz-1.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a8c4f76ed1c8a65892d98dc2913027c9acdb219d18f3a441cfa427a32861af9"}, - {file = "rapidfuzz-1.8.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71e217fd30901214cc96c0c15057278bafb7072aa9b2be4c97459c1fedf3e731"}, - {file = "rapidfuzz-1.8.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d579dd447b8e851462e79054b68f94b66b09df8b3abb2aa5ca07fe00912ef5e8"}, - {file = "rapidfuzz-1.8.0-cp310-cp310-win32.whl", hash = "sha256:5808064555273496dcd594d659bd28ee8d399149dd31575321034424455dc955"}, - {file = "rapidfuzz-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:798fef1671ca66c78b47802228e9583f7ab32b99bdfe3984ebb1f96e93e38b5f"}, - {file = "rapidfuzz-1.8.0-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:c9e0ed210831f5c73533bf11099ea7897db491e76c3443bef281d9c1c67d7f3a"}, - {file = "rapidfuzz-1.8.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:c819bb19eb615a31ddc9cb8248a285bf04f58158b53ce096451178631f99b652"}, - {file = "rapidfuzz-1.8.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:942ee45564f28ef70320d1229f02dc998bd93e3519c1f3a80f33ce144b51039c"}, - {file = "rapidfuzz-1.8.0-cp35-cp35m-win32.whl", hash = "sha256:7e6ae2e5a3bc9acc51e118f25d32b8efcd431c5d8deb408336dd2ed0f21d087c"}, - {file = "rapidfuzz-1.8.0-cp35-cp35m-win_amd64.whl", hash = "sha256:98901fba67c89ad2506f3946642cf6eb8f489592fb7eb307ebdf8bdb0c4e97f9"}, - {file = "rapidfuzz-1.8.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:705e1686f406a0c77ef323cdb7369b7cf9e68f2abfcb83ff5f1e0a5b21f5a534"}, - {file = "rapidfuzz-1.8.0-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:da0c5fe5fdbbd74206c1778af6b8c5ff8dfbe2dd04ae12bbe96642b358acefce"}, - {file = "rapidfuzz-1.8.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:535253bc9224215131ae450aad6c9f7ef1b24f15c685045eab2b52511268bd06"}, - {file = "rapidfuzz-1.8.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acdad83f07d886705fce164b0d1f4e3b56788a205602ed3a7fc8b10ceaf05fbf"}, - {file = "rapidfuzz-1.8.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35097f649831f8375d6c65a237deccac3aceb573aa7fae1e5d3fa942e89de1c8"}, - {file = "rapidfuzz-1.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6f4db142e5b4b44314166a90e11603220db659bd2f9c23dd5db402c13eac8eb7"}, - {file = "rapidfuzz-1.8.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:19a3f55f27411d68360540484874beda0b428b062596d5f0f141663ef0738bfd"}, - {file = "rapidfuzz-1.8.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22b4c1a7f6fe29bd8dae49f7d5ab085dc42c3964f1a78b6dca22fdf83b5c9bfa"}, - {file = "rapidfuzz-1.8.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8bfb2fbc147904b78d5c510ee75dc8704b606e956df23f33a9e89abc03f45c3"}, - {file = "rapidfuzz-1.8.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6dc5111ebfed2c4f2e4d120a9b280ea13ea4fbb60b6915dd239817b4fc092ed"}, - {file = "rapidfuzz-1.8.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db5ee2457d97cb967ffe08446a8c595c03fe747fdc2e145266713f9c516d1c4a"}, - {file = "rapidfuzz-1.8.0-cp37-cp37m-win32.whl", hash = "sha256:12c1b78cc15fc26f555a4bf66088d5afb6354b5a5aa149a123f01a15af6c411b"}, - {file = "rapidfuzz-1.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:693e9579048d8db4ff020715dd6f25aa315fd6445bc94e7400d7a94a227dad27"}, - {file = "rapidfuzz-1.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b4fe19df3edcf7de359448b872aec08e6592b4ca2d3df4d8ee57b5812d68bebf"}, - {file = "rapidfuzz-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f3670b9df0e1f479637cad1577afca7766a02775dc08c14837cf495c82861d7c"}, - {file = "rapidfuzz-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:61d118f36eb942649b0db344f7b7a19ad7e9b5749d831788187eb03b57ce1bfa"}, - {file = "rapidfuzz-1.8.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fce3a2c8a1d10da12aff4a0d367624e8ae9e15c1b84a5144843681d39be0c355"}, - {file = "rapidfuzz-1.8.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1577ef26e3647ccc4cc9754c34ffaa731639779f4d7779e91a761c72adac093e"}, - {file = "rapidfuzz-1.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fec9b7e60fde51990c3b48fc1aa9dba9ac3acaf78f623dbb645a6fe21a9654e"}, - {file = "rapidfuzz-1.8.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b954469d93858bc8b48129bc63fd644382a4df5f3fb1b4b290f48eac1d00a2da"}, - {file = "rapidfuzz-1.8.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:190ba709069a7e5a6b39b7c8bc413a08cfa7f1f4defec5d974c4128b510e0234"}, - {file = "rapidfuzz-1.8.0-cp38-cp38-win32.whl", hash = "sha256:97b2d13d6323649b43d1b113681e4013ba230bd6e9827cc832dcebee447d7250"}, - {file = "rapidfuzz-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:81c3091209b75f6611efe2af18834180946d4ce28f41ca8d44fce816187840d2"}, - {file = "rapidfuzz-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d610afa33e92aa0481a514ffda3ec51ca5df3c684c1c1c795307589c62025931"}, - {file = "rapidfuzz-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d976f33ca6b5fabbb095c0a662f5b86baf706184fc24c7f125d4ddb54b8bf036"}, - {file = "rapidfuzz-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0f5ca7bca2af598d4ddcf5b93b64b50654a9ff684e6f18d865f6e13fee442b3e"}, - {file = "rapidfuzz-1.8.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2aac5ea6b0306dcd28a6d1a89d35ed2c6ac426f2673ee1b92cf3f1d0fd5cd"}, - {file = "rapidfuzz-1.8.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f145c9831c0454a696a3136a6380ea4e01434e9cc2f2bc10d032864c16d1d0e5"}, - {file = "rapidfuzz-1.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4ce53291575b56c9d45add73ea013f43bafcea55eee9d5139aa759918d7685f"}, - {file = "rapidfuzz-1.8.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de5773a39c00a0f23cfc5da9e0e5fd0fb512b0ebe23dc7289a38e1f9a4b5cefc"}, - {file = "rapidfuzz-1.8.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87a802e55792bfbe192e2d557f38867dbe3671b49b3d5ecd873859c7460746ba"}, - {file = "rapidfuzz-1.8.0-cp39-cp39-win32.whl", hash = "sha256:9391abf1121df831316222f28cea37397a0f72bd7978f3be6e7da29a7821e4e5"}, - {file = "rapidfuzz-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:9eeca1b436042b5523dcf314f5822b1131597898c1d967f140d1917541a8a3d1"}, - {file = "rapidfuzz-1.8.0-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:a01f2495aca479b49d3b3a8863d6ba9bea2043447a1ced74ae5ec5270059cbc1"}, - {file = "rapidfuzz-1.8.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:b7d4b1a5d16817f8cdb34365c7b58ae22d5cf1b3207720bb2fa0b55968bdb034"}, - {file = "rapidfuzz-1.8.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c738d0d7f1744646d48d19b4c775926082bcefebd2460f45ca383a0e882f5672"}, - {file = "rapidfuzz-1.8.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0fb9c6078c17c12b52e66b7d0a2a1674f6bbbdc6a76e454c8479b95147018123"}, - {file = "rapidfuzz-1.8.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1482b385d83670eb069577c9667f72b41eec4f005aee32f1a4ff4e71e88afde2"}, - {file = "rapidfuzz-1.8.0.tar.gz", hash = "sha256:83fff37acf0367314879231264169dcbc5e7de969a94f4b82055d06a7fddab9a"}, + {file = "rapidfuzz-1.8.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:7a6c6b161221eb535b2a8fbd74a2c9ef779384d644a2c7a9390957704b80119c"}, + {file = "rapidfuzz-1.8.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:52e936b7fe14808c3d080e0aa91ba6694d2ebb4e723d612e4952f380a13abb2b"}, + {file = "rapidfuzz-1.8.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:edb70e94edbac04d98cbf5524c3ffb0da40fb8d2a32d4cecb24f1bd73341f79f"}, + {file = "rapidfuzz-1.8.1-cp27-cp27m-win32.whl", hash = "sha256:f48c459d549ff0242da832d5f94c8d518532d96e4d9ce8093447c7ad6bc63cf4"}, + {file = "rapidfuzz-1.8.1-cp27-cp27m-win_amd64.whl", hash = "sha256:b1b728846f23b91e5de36bfe851df81006041ca40c70a24feced730e59e67f6d"}, + {file = "rapidfuzz-1.8.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:983c04e8ed2a34d5ad8a9b70ea0bbbfdefe5b5ca27feb616ee0457e52b7b2f1d"}, + {file = "rapidfuzz-1.8.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:04f41f1a55d3d9ff93c0b02b9213b43e04c80863b3be85402e6239be68518656"}, + {file = "rapidfuzz-1.8.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d07ef42555c595d1e8581b6e981ad82d6e9e2a1b09c20ae00f17f70b87e38c50"}, + {file = "rapidfuzz-1.8.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c6420645e40307cd770f8fe9ed9008f5537da887ba808433ec8cb460641513ad"}, + {file = "rapidfuzz-1.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6dc7569eeca5cea5dad14bf1878985a892b27c499746f6d0f9cb259ab8084525"}, + {file = "rapidfuzz-1.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e90e243aece81636e6f24b3e7762148c15bcafe5f320bc860fa6e122dc449a7b"}, + {file = "rapidfuzz-1.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f4d5abde7b9ef9aaa84005d9a0d8337d46c17c5938223de9c5bcb5a20ac3bad"}, + {file = "rapidfuzz-1.8.1-cp310-cp310-win32.whl", hash = "sha256:b6ce46f65574acef77882c47ff9810376b3dc431fed95f28439b4b1d4bb7f9d3"}, + {file = "rapidfuzz-1.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:684fafdd5229e95f6bc26517566ddea971fcb3f0b8bceb94d35b3d4d7adc508a"}, + {file = "rapidfuzz-1.8.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:c092d998e57286a347ffd548368152aece2336fdd7620d433bf3b32fa516ed67"}, + {file = "rapidfuzz-1.8.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:e82bf3ce820a9fb883157bd2fc291be564422def2845d7b09e9347ed43b91a1f"}, + {file = "rapidfuzz-1.8.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:4a6831adbf463dfb6a2395bf4e2c606aa6653caaf6a9f5cfde782971f3296605"}, + {file = "rapidfuzz-1.8.1-cp35-cp35m-win32.whl", hash = "sha256:11a0523742feb9af88d7cb252432e55abbfcdd8548bcef0e9012d6cab71a615b"}, + {file = "rapidfuzz-1.8.1-cp35-cp35m-win_amd64.whl", hash = "sha256:2aab8a168e10134ba7f6851aa4a207a3ac8f3a00e4ccadb87548527fda18bd16"}, + {file = "rapidfuzz-1.8.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a47ec7cd79e2b31c36d724693e836f6d2a75beae2218cf0c5f6c330e64e41295"}, + {file = "rapidfuzz-1.8.1-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c01c71d88e60355f22b1a199f7622204aff149313bfc5985d1d197bdbf1cc19d"}, + {file = "rapidfuzz-1.8.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa4920daef40beb9ad372b76179a023562b660cd760172aa175866972d860629"}, + {file = "rapidfuzz-1.8.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cc17dad5227b85312b8ac58544597df914c030d22a76f113877f2ca0715b401"}, + {file = "rapidfuzz-1.8.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e80ba975f1d2cf9c85341c09e29c95f164502eac828398563f8f0269157103b"}, + {file = "rapidfuzz-1.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d2bac1d343a707e2090c9737e6d5d49e780a43d3541132d96338e42a83524b6d"}, + {file = "rapidfuzz-1.8.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4df8b3b1e322eac4af0894b100591078a707c4dd9bf6d0cb6360fd74f012bcfd"}, + {file = "rapidfuzz-1.8.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f31db70318ff494171f6a95f9a8f2b1542c1ae4b11aee7e7cff9bd5c7f076706"}, + {file = "rapidfuzz-1.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bd95454a2a13c18b39a61ac8b8475f24aaf474c48448115bd81c67f7e421c18"}, + {file = "rapidfuzz-1.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ac47023c4e9aa61984557602f0beea53a623fe32edd2d799294812596f74798"}, + {file = "rapidfuzz-1.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef91bd5a865f237b9ec28d1db06e41bc81ef7dd468569653b34c93717dabbbde"}, + {file = "rapidfuzz-1.8.1-cp37-cp37m-win32.whl", hash = "sha256:19560fc6b025a1ccb9d22e2cac597201fc6979eaffeb39cea9ef171c4e9ae48a"}, + {file = "rapidfuzz-1.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08572f743e89d3bb5ad56839a046bc7b23ae225356fdf9a1b4798d8ed7e15d69"}, + {file = "rapidfuzz-1.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:02a309a1fb103e8db6bf212884777ec22bf9fbcba1413854aa88ecd91bc61a99"}, + {file = "rapidfuzz-1.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:30ba1aca03c300b8c05e0e259fda540ced6bffa22202a8c6d1128f7120df5ee0"}, + {file = "rapidfuzz-1.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:31dbe267927ad82694d117bd66b56f73ed1e3c34cf50b10429446f331e57086c"}, + {file = "rapidfuzz-1.8.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:25e96435b1099dc9d14c5d73ea2b8341090d4315148d204d8b276c24deca46a4"}, + {file = "rapidfuzz-1.8.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:59910b0a00ccbf33252563b67e7c71fdff707d4f1b7533e392131e8cf287f341"}, + {file = "rapidfuzz-1.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257fbb5b5e237c2d43a4c7e6a12b2828b690fe54e98d38d48e33e3acbc5a959a"}, + {file = "rapidfuzz-1.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55af7a8c39f3da0f3a77c2ad1674851d63aeb24df379bf3acc745fd764c3bb08"}, + {file = "rapidfuzz-1.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f9b5650ceb3f5b4ef6a43d5de4e47e121c29537a3eda4e35edaa2347567670c"}, + {file = "rapidfuzz-1.8.1-cp38-cp38-win32.whl", hash = "sha256:508c028d7ab70ea202122b9a41b5393ec4b492877c6cac42a7b23e7bd06abeff"}, + {file = "rapidfuzz-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:e824de20f05dc6392e5d6171bca046db27f1b6d6882b7c7aa4a692a088c97cf7"}, + {file = "rapidfuzz-1.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5a275a625168db7c728f46ab3be3cf7faa5d822567552caec491acb7b577c9d0"}, + {file = "rapidfuzz-1.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b3389f0543c23647511afc87c935933ffc41c9f9c373425946d0b1ab10d9f7ad"}, + {file = "rapidfuzz-1.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d18277c390b548aa282bea5cccf82cce6de621d3b477c46e6bb26678b9745697"}, + {file = "rapidfuzz-1.8.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d78ad5f8bd6b0d514e50a0f92e3db52ce5d3852fdb167c434589a291b26cabb"}, + {file = "rapidfuzz-1.8.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef601cc99b8ae7fd242f212fae3e411f6407bef8a8d47376963c0069ee1408c5"}, + {file = "rapidfuzz-1.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2275b8ad68c55a4e897710cde26693b4ab502f678ccf78056e1b3c1a4f6bdf60"}, + {file = "rapidfuzz-1.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:316ddc3ee3b6e6a02281c66b1553dabe75de05febed613ad3341fd513bf4710e"}, + {file = "rapidfuzz-1.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dc3609ab10d451fd4d8599b06096b75e1a98ac5be90e94d716f1a22f4479e2b8"}, + {file = "rapidfuzz-1.8.1-cp39-cp39-win32.whl", hash = "sha256:696abe5b4386ab2be0ab14283554d65693765114de0cf39a9c4ac9d4aa8ed49c"}, + {file = "rapidfuzz-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:5e1a13268ed05f14fc034feaff2bd7ef25b62dec10f382851df83948b9c2955a"}, + {file = "rapidfuzz-1.8.1-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:30fc0264f59ea3a964a6d973f0f513708c4f128c60c07abfdba1cabf88810491"}, + {file = "rapidfuzz-1.8.1-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:ce6188048bd3d837510dda622199bf4707e6f459a274b259e2228df8f34d393f"}, + {file = "rapidfuzz-1.8.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:993772c6d322695c61ae79566e13182b9a99229307db4e1f34b3f261876f6f34"}, + {file = "rapidfuzz-1.8.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cf2f41be966bd3f1ec9f3aea828fad29c99f2a32cf92a7246f094d09a0b779ae"}, + {file = "rapidfuzz-1.8.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:2f5bfd4852876dd438a7c704d0b3f8efc225f1b5e73bcd35bee52345f1e9b49a"}, + {file = "rapidfuzz-1.8.1.tar.gz", hash = "sha256:73aed694e0f36764b61418a9d5d75e9e428fbd954e154e71ad1e34912f2e85ab"}, ] redis = [ {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"}, @@ -1546,6 +1582,10 @@ taskipy = [ {file = "taskipy-1.9.0-py3-none-any.whl", hash = "sha256:02bd2c51c7356ed3f7f8853210ada1cd2ab273e68359ee865021c3057eec6615"}, {file = "taskipy-1.9.0.tar.gz", hash = "sha256:449c160b557cdb1d9c17097a5ea4aa0cd5223723ddbaaa5d5032dd16274fb8f0"}, ] +testfixtures = [ + {file = "testfixtures-6.18.3-py2.py3-none-any.whl", hash = "sha256:6ddb7f56a123e1a9339f130a200359092bd0a6455e31838d6c477e8729bb7763"}, + {file = "testfixtures-6.18.3.tar.gz", hash = "sha256:2600100ae96ffd082334b378e355550fef8b4a529a6fa4c34f47130905c7426d"}, +] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, diff --git a/pyproject.toml b/pyproject.toml index 88a974ab..91dd65fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,10 +26,10 @@ flake8 = "~=3.8" flake8-annotations = "~=2.3" flake8-bugbear = "~=20.1" flake8-docstrings = "~=1.5" -flake8-import-order = "~=0.18" flake8-string-format = "~=0.3" flake8-tidy-imports = "~=4.1" flake8-todo = "~=0.7" +flake8-isort = "~=4.0" pep8-naming = "~=0.11" pip-licenses = "~=3.5" pre-commit = "~=2.1" @@ -40,7 +40,17 @@ taskipy = "~=1.6" start = "python -m bot" lint = "pre-commit run --all-files" precommit = "pre-commit install" +isort = "isort ." [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" + +[tool.isort] +multi_line_output = 6 +order_by_type = false +case_sensitive = true +combine_as_imports = true +line_length = 120 +atomic = true +known_first_party = ["bot"] -- cgit v1.2.3 From cdaa77830f9bce1529d93990f00415dbde33a0cd Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Fri, 22 Oct 2021 07:25:39 +0000 Subject: Isort: give the codebase a sort --- bot/__init__.py | 1 - bot/exts/core/help.py | 5 +---- bot/exts/core/internal_eval/_internal_eval.py | 1 + bot/exts/events/advent_of_code/_cog.py | 4 +--- bot/exts/holidays/easter/earth_photos.py | 3 +-- bot/exts/holidays/halloween/scarymovie.py | 1 + bot/exts/utilities/issues.py | 9 +-------- bot/utils/checks.py | 9 +-------- bot/utils/halloween/spookifications.py | 3 +-- 9 files changed, 8 insertions(+), 28 deletions(-) diff --git a/bot/__init__.py b/bot/__init__.py index db576cb2..cfaee9f8 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -18,7 +18,6 @@ from discord.ext import commands from bot import monkey_patches from bot.constants import Client - # Configure the "TRACE" logging level (e.g. "log.trace(message)") logging.TRACE = 5 logging.addLevelName(logging.TRACE, "TRACE") diff --git a/bot/exts/core/help.py b/bot/exts/core/help.py index 4b766b50..db3c2aa6 100644 --- a/bot/exts/core/help.py +++ b/bot/exts/core/help.py @@ -13,10 +13,7 @@ from rapidfuzz import process from bot import constants from bot.bot import Bot from bot.constants import Emojis -from bot.utils.pagination import ( - FIRST_EMOJI, LAST_EMOJI, - LEFT_EMOJI, LinePaginator, RIGHT_EMOJI, -) +from bot.utils.pagination import FIRST_EMOJI, LAST_EMOJI, LEFT_EMOJI, LinePaginator, RIGHT_EMOJI DELETE_EMOJI = Emojis.trashcan diff --git a/bot/exts/core/internal_eval/_internal_eval.py b/bot/exts/core/internal_eval/_internal_eval.py index 4f6b4321..12a860fa 100644 --- a/bot/exts/core/internal_eval/_internal_eval.py +++ b/bot/exts/core/internal_eval/_internal_eval.py @@ -10,6 +10,7 @@ from bot.bot import Bot from bot.constants import Client, Roles from bot.utils.decorators import with_role from bot.utils.extensions import invoke_help_command + from ._helpers import EvalContext __all__ = ["InternalEval"] diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 7dd967ec..2c1f4541 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -9,9 +9,7 @@ import discord from discord.ext import commands from bot.bot import Bot -from bot.constants import ( - AdventOfCode as AocConfig, Channels, Colours, Emojis, Month, Roles, WHITELISTED_CHANNELS, -) +from bot.constants import AdventOfCode as AocConfig, Channels, Colours, Emojis, Month, Roles, WHITELISTED_CHANNELS from bot.exts.events.advent_of_code import _helpers from bot.exts.events.advent_of_code.views.dayandstarview import AoCDropdownView from bot.utils.decorators import InChannelCheckFailure, in_month, whitelist_override, with_role diff --git a/bot/exts/holidays/easter/earth_photos.py b/bot/exts/holidays/easter/earth_photos.py index f65790af..27442f1c 100644 --- a/bot/exts/holidays/easter/earth_photos.py +++ b/bot/exts/holidays/easter/earth_photos.py @@ -4,8 +4,7 @@ import discord from discord.ext import commands from bot.bot import Bot -from bot.constants import Colours -from bot.constants import Tokens +from bot.constants import Colours, Tokens log = logging.getLogger(__name__) diff --git a/bot/exts/holidays/halloween/scarymovie.py b/bot/exts/holidays/halloween/scarymovie.py index 33659fd8..89310b97 100644 --- a/bot/exts/holidays/halloween/scarymovie.py +++ b/bot/exts/holidays/halloween/scarymovie.py @@ -6,6 +6,7 @@ from discord.ext import commands from bot.bot import Bot from bot.constants import Tokens + log = logging.getLogger(__name__) diff --git a/bot/exts/utilities/issues.py b/bot/exts/utilities/issues.py index 36655e1b..b6d5a43e 100644 --- a/bot/exts/utilities/issues.py +++ b/bot/exts/utilities/issues.py @@ -9,14 +9,7 @@ from discord.ext import commands from bot.bot import Bot from bot.constants import ( - Categories, - Channels, - Colours, - ERROR_REPLIES, - Emojis, - NEGATIVE_REPLIES, - Tokens, - WHITELISTED_CHANNELS + Categories, Channels, Colours, ERROR_REPLIES, Emojis, NEGATIVE_REPLIES, Tokens, WHITELISTED_CHANNELS ) from bot.utils.decorators import whitelist_override from bot.utils.extensions import invoke_help_command diff --git a/bot/utils/checks.py b/bot/utils/checks.py index 612d1ed6..8c426ed7 100644 --- a/bot/utils/checks.py +++ b/bot/utils/checks.py @@ -4,14 +4,7 @@ from collections.abc import Container, Iterable from typing import Callable, Optional from discord.ext.commands import ( - BucketType, - CheckFailure, - Cog, - Command, - CommandOnCooldown, - Context, - Cooldown, - CooldownMapping, + BucketType, CheckFailure, Cog, Command, CommandOnCooldown, Context, Cooldown, CooldownMapping ) from bot import constants diff --git a/bot/utils/halloween/spookifications.py b/bot/utils/halloween/spookifications.py index 93c5ddb9..c45ef8dc 100644 --- a/bot/utils/halloween/spookifications.py +++ b/bot/utils/halloween/spookifications.py @@ -1,8 +1,7 @@ import logging from random import choice, randint -from PIL import Image -from PIL import ImageOps +from PIL import Image, ImageOps log = logging.getLogger() -- cgit v1.2.3 From 3983d28349fd9be55627f6284b3d2e4b96fe37ce Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Fri, 22 Oct 2021 07:25:58 +0000 Subject: Precommit: add an isort hook --- .pre-commit-config.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7244cb4e..a2e9a398 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,6 +12,11 @@ repos: rev: v1.5.1 hooks: - id: python-check-blanket-noqa + - repo: https://github.com/pycqa/isort + rev: 5.8.0 + hooks: + - id: isort + name: isort (python) - repo: local hooks: - id: flake8 -- cgit v1.2.3 From 74e8bd470356885a5f419999d88960124e6e0584 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Fri, 22 Oct 2021 07:27:49 +0000 Subject: CI: only check licenses of dev deps --- .github/workflows/lint.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 81706e1e..756b3c16 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -74,12 +74,14 @@ jobs: pip install poetry poetry install --no-interaction --no-ansi - # Check all the dependencies are compatible with the MIT license. + # Check all of our dev dependencies are compatible with the MIT license. # If you added a new dependencies that is being rejected, # please make sure it is compatible with the license for this project, # and add it to the ALLOWED_LICENSE variable - name: Check Dependencies License - run: pip-licenses --allow-only="$ALLOWED_LICENSES" + run: | + pip-licenses --allow-only="$ALLOWED_LICENSE" \ + --package $(poetry export -f requirements.txt --without-hashes | sed "s/==.*//g" | tr "\n" " ") # This step caches our pre-commit environment. To make sure we # do create a new environment when our pre-commit setup changes, -- cgit v1.2.3 From a054d50b9fdf18c5d1465016f7bd2d3b0abdc0fa Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Sun, 24 Oct 2021 18:12:11 -0400 Subject: temp: add restructured template as comments --- bot/exts/utilities/color.py | 137 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 136 insertions(+), 1 deletion(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 6aa0c3cd..c1523281 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -4,14 +4,16 @@ import logging import re from io import BytesIO -from PIL import Image, ImageColor from discord import Embed, File from discord.ext import commands +from PIL import Image, ImageColor from rapidfuzz import process from bot.bot import Bot from bot.constants import Colours +# from bot.exts.core.extension import invoke_help_command + logger = logging.getLogger(__name__) @@ -28,6 +30,139 @@ with open("bot/resources/utilities/ryanzec_colours.json") as f: COLOR_MAPPING = json.load(f) +THUMBNAIL_SIZE = 80 + +""" +class Colour(commands.Cog): + + def __init__(self, bot: Bot) -> None: + self.bot = bot + + @commands.group(aliases=["color"]) + async def colour(self, ctx: commands.Context) -> None: + if ctx.invoked_subcommand is None: + await invoke_help_command(ctx) + + @colour.command() + async def rgb(self, ctx: commands.Context, red: int, green: int, blue: int) -> None: + rgb_tuple = ImageColor.getrgb(f"rgb({red}, {green}, {blue})") + await Colour.send_colour_response(ctx, list(rgb_tuple)) + + @colour.command() + async def hsv(self, ctx: commands.Context, hue: int, saturation: int, value: int) -> None: + hsv_tuple = ImageColor.getrgb(f"hsv({hue}, {saturation}%, {value}%)") + await Colour.send_colour_response(ctx, list(hsv_tuple)) + + @colour.command() + async def hsl(self, ctx: commands.Context, hue: int, saturation: int, lightness: int) -> None: + hsl_tuple = ImageColor.getrgb(f"hsl({hue}, {saturation}%, {lightness}%)") + await Colour.send_colour_response(ctx, list(hsl_tuple)) + + @colour.command() + async def cmyk(self, ctx: commands.Context, cyan: int, yellow: int, magenta: int, key: int) -> None: + ... + + @colour.command() + async def hex(self, ctx: commands.Context, hex_code: str) -> None: + hex_tuple = ImageColor.getrgb(hex_code) + await Colour.send_colour_response(ctx, list(hex_tuple)) + + @colour.command() + async def yiq( + self, + ctx: commands.Context, + perceived_luminesence: int, + in_phase: int, + quadrature: int + ) -> None: + yiq_list = list(colorsys.yiq_to_rgb(perceived_luminesence, in_phase, quadrature)) + yiq_tuple = [int(val * 255.0) for val in yiq_list] + await Colour.send_colour_response(ctx, list(yiq_tuple)) + + @staticmethod + async def send_colour_response(ctx: commands.Context, rgb: list[int]) -> Message: + r, g, b = rgb[0], rgb[1], rgb[2] + colour_embed = Embed( + title="Colour", + description="Here lies thy colour", + colour=int(f"{r:02x}{g:02x}{b:02x}", 16) + ) + colour_conversions = Colour.get_colour_conversions(rgb) + for colour_space, value in colour_conversions.items(): + colour_embed.add_field( + name=colour_space.upper(), + value=f"`{value}`", + inline=True + ) + + thumbnail = Image.new("RGB", (THUMBNAIL_SIZE, THUMBNAIL_SIZE), color=tuple(rgb)) + buffer = BytesIO() + thumbnail.save(buffer, "PNG") + buffer.seek(0) + thumbnail_file = File(buffer, filename="colour.png") + + colour_embed.set_thumbnail(url="attachment://colour.png") + + await ctx.send(file=thumbnail_file, embed=colour_embed) + + @staticmethod + def get_colour_conversions(rgb: list[int]) -> dict[str, str]: + return { + "rgb": tuple(rgb), + "hsv": Colour._rgb_to_hsv(rgb), + "hsl": Colour._rgb_to_hsl(rgb), + "cmyk": Colour._rgb_to_cmyk(rgb), + "hex": Colour._rgb_to_hex(rgb), + "yiq": Colour._rgb_to_yiq(rgb) + } + + @staticmethod + def _rgb_to_hsv(rgb: list[int]) -> tuple[int, int, int]: + rgb = [val / 255.0 for val in rgb] + h, v, s = colorsys.rgb_to_hsv(*rgb) + hsv = (round(h * 360), round(s * 100), round(v * 100)) + return hsv + + @staticmethod + def _rgb_to_hsl(rgb: list[int]) -> tuple[int, int, int]: + rgb = [val / 255.0 for val in rgb] + h, l, s = colorsys.rgb_to_hls(*rgb) + hsl = (round(h * 360), round(s * 100), round(l * 100)) + return hsl + + @staticmethod + def _rgb_to_cmyk(rgb: list[int]) -> tuple[int, int, int, int]: + rgb = [val / 255.0 for val in rgb] + + if all(val == 0 for val in rgb): + return 0, 0, 0, 100 + + cmy = [1 - val / 255 for val in rgb] + min_cmy = min(cmy) + + cmyk = [(val - min_cmy) / (1 - min_cmy) for val in cmy] + [min_cmy] + cmyk = [round(val * 100) for val in cmyk] + + return tuple(cmyk) + + @staticmethod + def _rgb_to_hex(rgb: list[int]) -> str: + hex_ = ''.join([hex(val)[2:].zfill(2) for val in rgb]) + hex_code = f"#{hex_}".upper() + return hex_code + + @staticmethod + def _rgb_to_yiq(rgb: list[int]) -> tuple[int, int, int, int]: + rgb = [val / 255.0 for val in rgb] + y, i, q = colorsys.rgb_to_yiq(*rgb) + yiq = (round(y), round(i), round(q)) + return yiq + +def setup(bot: commands.Bot) -> None: + bot.add_cog(Colour(bot)) +""" + + class Color(commands.Cog): """User initiated commands to receive color information.""" -- cgit v1.2.3 From 392616ea9f259ae1bb5f48f2cef366a273436c46 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Sun, 24 Oct 2021 18:29:07 -0400 Subject: temp: add restructured layout in comments --- bot/exts/utilities/color.py | 471 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 471 insertions(+) create mode 100644 bot/exts/utilities/color.py diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py new file mode 100644 index 00000000..c1523281 --- /dev/null +++ b/bot/exts/utilities/color.py @@ -0,0 +1,471 @@ +import colorsys +import json +import logging +import re +from io import BytesIO + +from discord import Embed, File +from discord.ext import commands +from PIL import Image, ImageColor +from rapidfuzz import process + +from bot.bot import Bot +from bot.constants import Colours + +# from bot.exts.core.extension import invoke_help_command + + +logger = logging.getLogger(__name__) + + +ERROR_MSG = """The color code {user_color} is not a possible color combination. +The range of possible values are: +RGB & HSV: 0-255 +CMYK: 0-100% +HSL: 0-360 degrees +Hex: #000000-#FFFFFF +""" + +with open("bot/resources/utilities/ryanzec_colours.json") as f: + COLOR_MAPPING = json.load(f) + + +THUMBNAIL_SIZE = 80 + +""" +class Colour(commands.Cog): + + def __init__(self, bot: Bot) -> None: + self.bot = bot + + @commands.group(aliases=["color"]) + async def colour(self, ctx: commands.Context) -> None: + if ctx.invoked_subcommand is None: + await invoke_help_command(ctx) + + @colour.command() + async def rgb(self, ctx: commands.Context, red: int, green: int, blue: int) -> None: + rgb_tuple = ImageColor.getrgb(f"rgb({red}, {green}, {blue})") + await Colour.send_colour_response(ctx, list(rgb_tuple)) + + @colour.command() + async def hsv(self, ctx: commands.Context, hue: int, saturation: int, value: int) -> None: + hsv_tuple = ImageColor.getrgb(f"hsv({hue}, {saturation}%, {value}%)") + await Colour.send_colour_response(ctx, list(hsv_tuple)) + + @colour.command() + async def hsl(self, ctx: commands.Context, hue: int, saturation: int, lightness: int) -> None: + hsl_tuple = ImageColor.getrgb(f"hsl({hue}, {saturation}%, {lightness}%)") + await Colour.send_colour_response(ctx, list(hsl_tuple)) + + @colour.command() + async def cmyk(self, ctx: commands.Context, cyan: int, yellow: int, magenta: int, key: int) -> None: + ... + + @colour.command() + async def hex(self, ctx: commands.Context, hex_code: str) -> None: + hex_tuple = ImageColor.getrgb(hex_code) + await Colour.send_colour_response(ctx, list(hex_tuple)) + + @colour.command() + async def yiq( + self, + ctx: commands.Context, + perceived_luminesence: int, + in_phase: int, + quadrature: int + ) -> None: + yiq_list = list(colorsys.yiq_to_rgb(perceived_luminesence, in_phase, quadrature)) + yiq_tuple = [int(val * 255.0) for val in yiq_list] + await Colour.send_colour_response(ctx, list(yiq_tuple)) + + @staticmethod + async def send_colour_response(ctx: commands.Context, rgb: list[int]) -> Message: + r, g, b = rgb[0], rgb[1], rgb[2] + colour_embed = Embed( + title="Colour", + description="Here lies thy colour", + colour=int(f"{r:02x}{g:02x}{b:02x}", 16) + ) + colour_conversions = Colour.get_colour_conversions(rgb) + for colour_space, value in colour_conversions.items(): + colour_embed.add_field( + name=colour_space.upper(), + value=f"`{value}`", + inline=True + ) + + thumbnail = Image.new("RGB", (THUMBNAIL_SIZE, THUMBNAIL_SIZE), color=tuple(rgb)) + buffer = BytesIO() + thumbnail.save(buffer, "PNG") + buffer.seek(0) + thumbnail_file = File(buffer, filename="colour.png") + + colour_embed.set_thumbnail(url="attachment://colour.png") + + await ctx.send(file=thumbnail_file, embed=colour_embed) + + @staticmethod + def get_colour_conversions(rgb: list[int]) -> dict[str, str]: + return { + "rgb": tuple(rgb), + "hsv": Colour._rgb_to_hsv(rgb), + "hsl": Colour._rgb_to_hsl(rgb), + "cmyk": Colour._rgb_to_cmyk(rgb), + "hex": Colour._rgb_to_hex(rgb), + "yiq": Colour._rgb_to_yiq(rgb) + } + + @staticmethod + def _rgb_to_hsv(rgb: list[int]) -> tuple[int, int, int]: + rgb = [val / 255.0 for val in rgb] + h, v, s = colorsys.rgb_to_hsv(*rgb) + hsv = (round(h * 360), round(s * 100), round(v * 100)) + return hsv + + @staticmethod + def _rgb_to_hsl(rgb: list[int]) -> tuple[int, int, int]: + rgb = [val / 255.0 for val in rgb] + h, l, s = colorsys.rgb_to_hls(*rgb) + hsl = (round(h * 360), round(s * 100), round(l * 100)) + return hsl + + @staticmethod + def _rgb_to_cmyk(rgb: list[int]) -> tuple[int, int, int, int]: + rgb = [val / 255.0 for val in rgb] + + if all(val == 0 for val in rgb): + return 0, 0, 0, 100 + + cmy = [1 - val / 255 for val in rgb] + min_cmy = min(cmy) + + cmyk = [(val - min_cmy) / (1 - min_cmy) for val in cmy] + [min_cmy] + cmyk = [round(val * 100) for val in cmyk] + + return tuple(cmyk) + + @staticmethod + def _rgb_to_hex(rgb: list[int]) -> str: + hex_ = ''.join([hex(val)[2:].zfill(2) for val in rgb]) + hex_code = f"#{hex_}".upper() + return hex_code + + @staticmethod + def _rgb_to_yiq(rgb: list[int]) -> tuple[int, int, int, int]: + rgb = [val / 255.0 for val in rgb] + y, i, q = colorsys.rgb_to_yiq(*rgb) + yiq = (round(y), round(i), round(q)) + return yiq + +def setup(bot: commands.Bot) -> None: + bot.add_cog(Colour(bot)) +""" + + +class Color(commands.Cog): + """User initiated commands to receive color information.""" + + def __init__(self, bot: Bot): + self.bot = bot + + @commands.command(aliases=["colour"]) + async def color(self, ctx: commands.Context, mode: str, *, user_color: str) -> None: + """ + Send information on input color code or color name. + + Possible modes are: "hex", "rgb", "hsv", "hsl", "cmyk" or "name". + """ + logger.debug(f"{mode = }") + logger.debug(f"{user_color = }") + if mode.lower() == "hex": + await self.hex_to_rgb(ctx, user_color) + elif mode.lower() == "rgb": + rgb_color = self.tuple_create(user_color) + await self.color_embed(ctx, rgb_color) + elif mode.lower() == "hsv": + await self.hsv_to_rgb(ctx, user_color) + elif mode.lower() == "hsl": + await self.hsl_to_rgb(ctx, user_color) + elif mode.lower() == "cmyk": + await self.cmyk_to_rgb(ctx, user_color) + elif mode.lower() == "name": + color_name, hex_color = self.match_color_name(user_color) + if "#" in hex_color: + rgb_color = ImageColor.getcolor(hex_color, "RGB") + else: + rgb_color = ImageColor.getcolor("#" + hex_color, "RGB") + await self.color_embed(ctx, rgb_color, color_name) + else: + # mode is either None or an invalid code + if mode is None: + no_mode_embed = Embed( + title="No mode was passed, please define a color code.", + description="Possible modes are: Name, Hex, RGB, HSV, HSL and CMYK.", + color=Colours.soft_red, + ) + await ctx.send(embed=no_mode_embed) + return + wrong_mode_embed = Embed( + title=f"The color code {mode} is not a valid option", + description="Possible modes are: Name, Hex, RGB, HSV, HSL and CMYK.", + color=Colours.soft_red, + ) + await ctx.send(embed=wrong_mode_embed) + return + + @staticmethod + def tuple_create(input_color: str) -> tuple[int, int, int]: + """ + Create a tuple of integers based on user's input. + + Can handle inputs of the types: + (100, 100, 100) + 100, 100, 100 + 100 100 100 + """ + if "(" in input_color: + remove = "[() ]" + color_tuple = re.sub(remove, "", input_color) + color_tuple = tuple(map(int, color_tuple.split(","))) + elif "," in input_color: + color_tuple = tuple(map(int, input_color.split(","))) + else: + color_tuple = tuple(map(int, input_color.split(" "))) + return color_tuple + + async def hex_to_rgb(self, ctx: commands.Context, hex_string: str) -> None: + """Function to convert hex color to rgb color and send main embed.""" + hex_match = re.fullmatch(r"(#?[0x]?)((?:[0-9a-fA-F]{3}){1,2})", hex_string) + if hex_match: + if "#" in hex_string: + rgb_color = ImageColor.getcolor(hex_string, "RGB") + elif "0x" in hex_string: + hex_ = hex_string.replace("0x", "#") + rgb_color = ImageColor.getcolor(hex_, "RGB") + else: + hex_ = "#" + hex_string + rgb_color = ImageColor.getcolor(hex_, "RGB") + await self.color_embed(ctx, rgb_color) + else: + await ctx.send( + embed=Embed( + title="There was an issue converting the hex color code.", + description=ERROR_MSG.format(user_color=hex_string), + ) + ) + + async def hsv_to_rgb(self, ctx: commands.Context, input_color: tuple[int, int, int]) -> tuple[int, int, int]: + """Function to convert hsv color to rgb color and send main embed.""" + input_color = self.tuple_create(input_color) + (h, v, s) = input_color # the function hsv_to_rgb expects v and s to be swapped + h = h / 360 + s = s / 100 + v = v / 100 + rgb_color = colorsys.hsv_to_rgb(h, s, v) + (r, g, b) = rgb_color + r = int(r * 255) + g = int(g * 255) + b = int(b * 255) + await self.color_embed(ctx, (r, g, b)) + + async def hsl_to_rgb(self, ctx: commands.Context, input_color: tuple[int, int, int]) -> tuple[int, int, int]: + """Function to convert hsl color to rgb color and send main embed.""" + input_color = self.tuple_create(input_color) + (h, s, l) = input_color + h = h / 360 + s = s / 100 + l = l / 100 # noqa: E741 It's little `L`, Reason: To maintain consistency. + rgb_color = colorsys.hls_to_rgb(h, l, s) + (r, g, b) = rgb_color + r = int(r * 255) + g = int(g * 255) + b = int(b * 255) + await self.color_embed(ctx, (r, g, b)) + + async def cmyk_to_rgb( + self, + ctx: commands.Context, + input_color: tuple[int, int, int, int] + ) -> tuple[int, int, int]: + """Function to convert cmyk color to rgb color and send main embed.""" + input_color = self.tuple_create(input_color) + c = input_color[0] + m = input_color[1] + y = input_color[2] + k = input_color[3] + r = int(255 * (1.0 - c / float(100)) * (1.0 - k / float(100))) + g = int(255 * (1.0 - m / float(100)) * (1.0 - k / float(100))) + b = int(255 * (1.0 - y / float(100)) * (1.0 - k / float(100))) + await self.color_embed(ctx, (r, g, b)) + + @staticmethod + async def create_thumbnail_attachment(color: tuple[int, int, int]) -> File: + """ + Generate a thumbnail from `color`. + + Assumes that color is an rgb tuple. + """ + thumbnail = Image.new("RGB", (80, 80), color=color) + bufferedio = BytesIO() + thumbnail.save(bufferedio, format="PNG") + bufferedio.seek(0) + + file = File(bufferedio, filename="color.png") + + return file + + @staticmethod + def get_color_fields(rgb_color: tuple[int, int, int]) -> list[dict]: + """Converts from `RGB` to `CMYK`, `HSV`, `HSL` and returns a list of fields.""" + + def _rgb_to_hex(rgb_color: tuple[int, int, int]) -> str: + """To convert from `RGB` to `Hex` notation.""" + return '#' + ''.join(hex(int(color))[2:].zfill(2) for color in rgb_color).upper() + + def _rgb_to_cmyk(rgb_color: tuple[int, int, int]) -> tuple[int, int, int, int]: + """To convert from `RGB` to `CMYK` color space.""" + r, g, b = rgb_color + + # RGB_SCALE -> 255 + # CMYK_SCALE -> 100 + + if (r == g == b == 0): + return 0, 0, 0, 100 # Representing Black + + # rgb [0,RGB_SCALE] -> cmy [0,1] + c = 1 - r / 255 + m = 1 - g / 255 + y = 1 - b / 255 + + # extract out k [0, 1] + min_cmy = min(c, m, y) + c = (c - min_cmy) / (1 - min_cmy) + m = (m - min_cmy) / (1 - min_cmy) + y = (y - min_cmy) / (1 - min_cmy) + k = min_cmy + + # rescale to the range [0,CMYK_SCALE] and round off + c = round(c * 100) + m = round(m * 100) + y = round(y * 100) + k = round(k * 100) + + return c, m, y, k + + def _rgb_to_hsv(rgb_color: tuple[int, int, int]) -> tuple[int, int, int]: + """To convert from `RGB` to `HSV` color space.""" + r, g, b = rgb_color + h, v, s = colorsys.rgb_to_hsv(r / float(255), g / float(255), b / float(255)) + h = round(h * 360) + s = round(s * 100) + v = round(v * 100) + return h, s, v + + def _rgb_to_hsl(rgb_color: tuple[int, int, int]) -> tuple[int, int, int]: + """To convert from `RGB` to `HSL` color space.""" + r, g, b = rgb_color + h, l, s = colorsys.rgb_to_hls(r / float(255), g / float(255), b / float(255)) + h = round(h * 360) + s = round(s * 100) + l = round(l * 100) # noqa: E741 It's little `L`, Reason: To maintain consistency. + return h, s, l + + all_fields = [ + { + "name": "RGB", + "value": f"Β» rgb {rgb_color}" + }, + { + "name": "HEX", + "value": f"Β» hex {_rgb_to_hex(rgb_color)}" + }, + { + "name": "CMYK", + "value": f"Β» cmyk {_rgb_to_cmyk(rgb_color)}" + }, + { + "name": "HSV", + "value": f"Β» hsv {_rgb_to_hsv(rgb_color)}" + }, + { + "name": "HSL", + "value": f"Β» hsl {_rgb_to_hsl(rgb_color)}" + }, + ] + + return all_fields + + @staticmethod + def match_color_name(input_color_name: str) -> str: + """Use fuzzy matching to return a hex color code based on the user's input.""" + try: + match, certainty, _ = process.extractOne( + query=input_color_name, + choices=COLOR_MAPPING.keys(), + score_cutoff=50 + ) + logger.debug(f"{match = }, {certainty = }") + hex_match = COLOR_MAPPING[match] + logger.debug(f"{hex_match = }") + except TypeError: + match = "No color name match found." + hex_match = input_color_name + + return match, hex_match + + @staticmethod + def match_color_hex(input_hex_color: str) -> str: + """Use fuzzy matching to return a hex color code based on the user's input.""" + try: + match, certainty, _ = process.extractOne( + query=input_hex_color, + choices=COLOR_MAPPING.values(), + score_cutoff=80 + ) + logger.debug(f"{match = }, {certainty = }") + color_name = [name for name, _ in COLOR_MAPPING.items() if _ == match][0] + logger.debug(f"{color_name = }") + except TypeError: + color_name = "No color name match found." + + return color_name + + async def color_embed( + self, + ctx: commands.Context, + rgb_color: tuple[int, int, int], + color_name: str = None + ) -> None: + """Take a RGB color tuple, create embed, and send.""" + (r, g, b) = rgb_color + discord_rgb_int = int(f"{r:02x}{g:02x}{b:02x}", 16) + all_colors = self.get_color_fields(rgb_color) + hex_color = all_colors[1]["value"].replace("Β» hex ", "") + if color_name is None: + logger.debug(f"Find color name from hex color: {hex_color}") + color_name = self.match_color_hex(hex_color) + + async with ctx.typing(): + main_embed = Embed( + title=color_name, + description='(Approx..)', + color=discord_rgb_int, + ) + + file = await self.create_thumbnail_attachment(rgb_color) + main_embed.set_thumbnail(url="attachment://color.png") + + for field in all_colors: + main_embed.add_field( + name=field['name'], + value=field['value'], + inline=False, + ) + + await ctx.send(file=file, embed=main_embed) + + +def setup(bot: Bot) -> None: + """Load the Color Cog.""" + bot.add_cog(Color(bot)) -- cgit v1.2.3 From d28b932f4177f5d3056991a3c1872988f69dc952 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Mon, 25 Oct 2021 19:30:34 -0400 Subject: fix: finish restructure with all functionality Added commands for `name` and `random`. Added the ability to look up the color name based on the hex value. Co-authored-by: Mohammad Rafivulla <77384412+CyberCitizen01@users.noreply.github.com> Co-authored-by: Vivaan Verma <54081925+doublevcodes@users.noreply.github.com> --- bot/exts/utilities/color.py | 395 ++++++++------------------------------------ 1 file changed, 66 insertions(+), 329 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index c1523281..dc63cf84 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -1,7 +1,7 @@ import colorsys import json import logging -import re +import random from io import BytesIO from discord import Embed, File @@ -10,81 +10,97 @@ from PIL import Image, ImageColor from rapidfuzz import process from bot.bot import Bot -from bot.constants import Colours - -# from bot.exts.core.extension import invoke_help_command - +from bot.exts.core.extensions import invoke_help_command logger = logging.getLogger(__name__) -ERROR_MSG = """The color code {user_color} is not a possible color combination. -The range of possible values are: -RGB & HSV: 0-255 -CMYK: 0-100% -HSL: 0-360 degrees -Hex: #000000-#FFFFFF -""" - with open("bot/resources/utilities/ryanzec_colours.json") as f: COLOR_MAPPING = json.load(f) THUMBNAIL_SIZE = 80 -""" + class Colour(commands.Cog): + """Cog for the Colour command.""" def __init__(self, bot: Bot) -> None: self.bot = bot @commands.group(aliases=["color"]) async def colour(self, ctx: commands.Context) -> None: + """ + User initiated command to create an embed that displays color information. + + For the commands `hsl`, `hsv` and `rgb`: input is in the form `.color ` + For the command `cmyk`: input is in the form `.color cmyk ` + For the command `hex`: input is in the form `.color hex #` + For the command `name`: input is in the form `.color name ` + For the command `random`: input is in the form `.color random` + """ if ctx.invoked_subcommand is None: await invoke_help_command(ctx) @colour.command() async def rgb(self, ctx: commands.Context, red: int, green: int, blue: int) -> None: + """Function to create an embed from an RGB input.""" rgb_tuple = ImageColor.getrgb(f"rgb({red}, {green}, {blue})") await Colour.send_colour_response(ctx, list(rgb_tuple)) @colour.command() async def hsv(self, ctx: commands.Context, hue: int, saturation: int, value: int) -> None: + """Function to create an embed from an HSV input.""" hsv_tuple = ImageColor.getrgb(f"hsv({hue}, {saturation}%, {value}%)") await Colour.send_colour_response(ctx, list(hsv_tuple)) @colour.command() async def hsl(self, ctx: commands.Context, hue: int, saturation: int, lightness: int) -> None: + """Function to create an embed from an HSL input.""" hsl_tuple = ImageColor.getrgb(f"hsl({hue}, {saturation}%, {lightness}%)") await Colour.send_colour_response(ctx, list(hsl_tuple)) @colour.command() async def cmyk(self, ctx: commands.Context, cyan: int, yellow: int, magenta: int, key: int) -> None: - ... + """Function to create an embed from a CMYK input.""" + r = int(255 * (1.0 - cyan / float(100)) * (1.0 - key / float(100))) + g = int(255 * (1.0 - magenta / float(100)) * (1.0 - key / float(100))) + b = int(255 * (1.0 - yellow / float(100)) * (1.0 - key / float(100))) + await Colour.send_colour_response(ctx, list((r, g, b))) @colour.command() async def hex(self, ctx: commands.Context, hex_code: str) -> None: + """Function to create an embed from a HEX input. (Requires # as a prefix).""" hex_tuple = ImageColor.getrgb(hex_code) await Colour.send_colour_response(ctx, list(hex_tuple)) @colour.command() - async def yiq( - self, - ctx: commands.Context, - perceived_luminesence: int, - in_phase: int, - quadrature: int - ) -> None: - yiq_list = list(colorsys.yiq_to_rgb(perceived_luminesence, in_phase, quadrature)) - yiq_tuple = [int(val * 255.0) for val in yiq_list] - await Colour.send_colour_response(ctx, list(yiq_tuple)) + async def name(self, ctx: commands.Context, user_color: str) -> None: + """Function to create an embed from a name input.""" + _, hex_color = self.match_color_name(user_color) + hex_tuple = ImageColor.getrgb(hex_color) + await Colour.send_colour_response(ctx, list(hex_tuple)) + + @colour.command() + async def random(self, ctx: commands.Context) -> None: + """Function to create an embed from a randomly chosen color from the ryanzec.json file.""" + color_choices = list(COLOR_MAPPING.values()) + hex_color = random.choice(color_choices) + hex_tuple = ImageColor.getrgb(f"#{hex_color}") + await Colour.send_colour_response(ctx, list(hex_tuple)) @staticmethod - async def send_colour_response(ctx: commands.Context, rgb: list[int]) -> Message: + async def send_colour_response(ctx: commands.Context, rgb: list[int]) -> None: + """Function to create and send embed from color information.""" r, g, b = rgb[0], rgb[1], rgb[2] + name = Colour._rgb_to_name(rgb) + if name is None: + desc = "Color information for the input color." + else: + desc = f"Color information for {name}" colour_embed = Embed( title="Colour", - description="Here lies thy colour", + description=desc, colour=int(f"{r:02x}{g:02x}{b:02x}", 16) ) colour_conversions = Colour.get_colour_conversions(rgb) @@ -107,17 +123,19 @@ class Colour(commands.Cog): @staticmethod def get_colour_conversions(rgb: list[int]) -> dict[str, str]: + """Create a dictionary mapping of color types and their values.""" return { "rgb": tuple(rgb), "hsv": Colour._rgb_to_hsv(rgb), "hsl": Colour._rgb_to_hsl(rgb), "cmyk": Colour._rgb_to_cmyk(rgb), "hex": Colour._rgb_to_hex(rgb), - "yiq": Colour._rgb_to_yiq(rgb) + "name": Colour._rgb_to_name(rgb) } @staticmethod def _rgb_to_hsv(rgb: list[int]) -> tuple[int, int, int]: + """Function to convert an RGB list to a HSV list.""" rgb = [val / 255.0 for val in rgb] h, v, s = colorsys.rgb_to_hsv(*rgb) hsv = (round(h * 360), round(s * 100), round(v * 100)) @@ -125,6 +143,7 @@ class Colour(commands.Cog): @staticmethod def _rgb_to_hsl(rgb: list[int]) -> tuple[int, int, int]: + """Function to convert an RGB list to a HSL list.""" rgb = [val / 255.0 for val in rgb] h, l, s = colorsys.rgb_to_hls(*rgb) hsl = (round(h * 360), round(s * 100), round(l * 100)) @@ -132,269 +151,39 @@ class Colour(commands.Cog): @staticmethod def _rgb_to_cmyk(rgb: list[int]) -> tuple[int, int, int, int]: + """Function to convert an RGB list to a CMYK list.""" rgb = [val / 255.0 for val in rgb] - if all(val == 0 for val in rgb): return 0, 0, 0, 100 - cmy = [1 - val / 255 for val in rgb] min_cmy = min(cmy) - cmyk = [(val - min_cmy) / (1 - min_cmy) for val in cmy] + [min_cmy] cmyk = [round(val * 100) for val in cmyk] - return tuple(cmyk) @staticmethod def _rgb_to_hex(rgb: list[int]) -> str: + """Function to convert an RGB list to a HEX string.""" hex_ = ''.join([hex(val)[2:].zfill(2) for val in rgb]) hex_code = f"#{hex_}".upper() return hex_code @staticmethod - def _rgb_to_yiq(rgb: list[int]) -> tuple[int, int, int, int]: - rgb = [val / 255.0 for val in rgb] - y, i, q = colorsys.rgb_to_yiq(*rgb) - yiq = (round(y), round(i), round(q)) - return yiq - -def setup(bot: commands.Bot) -> None: - bot.add_cog(Colour(bot)) -""" - - -class Color(commands.Cog): - """User initiated commands to receive color information.""" - - def __init__(self, bot: Bot): - self.bot = bot - - @commands.command(aliases=["colour"]) - async def color(self, ctx: commands.Context, mode: str, *, user_color: str) -> None: - """ - Send information on input color code or color name. - - Possible modes are: "hex", "rgb", "hsv", "hsl", "cmyk" or "name". - """ - logger.debug(f"{mode = }") - logger.debug(f"{user_color = }") - if mode.lower() == "hex": - await self.hex_to_rgb(ctx, user_color) - elif mode.lower() == "rgb": - rgb_color = self.tuple_create(user_color) - await self.color_embed(ctx, rgb_color) - elif mode.lower() == "hsv": - await self.hsv_to_rgb(ctx, user_color) - elif mode.lower() == "hsl": - await self.hsl_to_rgb(ctx, user_color) - elif mode.lower() == "cmyk": - await self.cmyk_to_rgb(ctx, user_color) - elif mode.lower() == "name": - color_name, hex_color = self.match_color_name(user_color) - if "#" in hex_color: - rgb_color = ImageColor.getcolor(hex_color, "RGB") - else: - rgb_color = ImageColor.getcolor("#" + hex_color, "RGB") - await self.color_embed(ctx, rgb_color, color_name) - else: - # mode is either None or an invalid code - if mode is None: - no_mode_embed = Embed( - title="No mode was passed, please define a color code.", - description="Possible modes are: Name, Hex, RGB, HSV, HSL and CMYK.", - color=Colours.soft_red, - ) - await ctx.send(embed=no_mode_embed) - return - wrong_mode_embed = Embed( - title=f"The color code {mode} is not a valid option", - description="Possible modes are: Name, Hex, RGB, HSV, HSL and CMYK.", - color=Colours.soft_red, - ) - await ctx.send(embed=wrong_mode_embed) - return - - @staticmethod - def tuple_create(input_color: str) -> tuple[int, int, int]: - """ - Create a tuple of integers based on user's input. - - Can handle inputs of the types: - (100, 100, 100) - 100, 100, 100 - 100 100 100 - """ - if "(" in input_color: - remove = "[() ]" - color_tuple = re.sub(remove, "", input_color) - color_tuple = tuple(map(int, color_tuple.split(","))) - elif "," in input_color: - color_tuple = tuple(map(int, input_color.split(","))) - else: - color_tuple = tuple(map(int, input_color.split(" "))) - return color_tuple - - async def hex_to_rgb(self, ctx: commands.Context, hex_string: str) -> None: - """Function to convert hex color to rgb color and send main embed.""" - hex_match = re.fullmatch(r"(#?[0x]?)((?:[0-9a-fA-F]{3}){1,2})", hex_string) - if hex_match: - if "#" in hex_string: - rgb_color = ImageColor.getcolor(hex_string, "RGB") - elif "0x" in hex_string: - hex_ = hex_string.replace("0x", "#") - rgb_color = ImageColor.getcolor(hex_, "RGB") - else: - hex_ = "#" + hex_string - rgb_color = ImageColor.getcolor(hex_, "RGB") - await self.color_embed(ctx, rgb_color) - else: - await ctx.send( - embed=Embed( - title="There was an issue converting the hex color code.", - description=ERROR_MSG.format(user_color=hex_string), - ) + def _rgb_to_name(rgb: list[int]) -> str: + """Function to convert from an RGB list to a fuzzy matched color name.""" + input_hex_color = Colour._rgb_to_hex(rgb) + try: + match, certainty, _ = process.extractOne( + query=input_hex_color, + choices=COLOR_MAPPING.values(), + score_cutoff=80 ) - - async def hsv_to_rgb(self, ctx: commands.Context, input_color: tuple[int, int, int]) -> tuple[int, int, int]: - """Function to convert hsv color to rgb color and send main embed.""" - input_color = self.tuple_create(input_color) - (h, v, s) = input_color # the function hsv_to_rgb expects v and s to be swapped - h = h / 360 - s = s / 100 - v = v / 100 - rgb_color = colorsys.hsv_to_rgb(h, s, v) - (r, g, b) = rgb_color - r = int(r * 255) - g = int(g * 255) - b = int(b * 255) - await self.color_embed(ctx, (r, g, b)) - - async def hsl_to_rgb(self, ctx: commands.Context, input_color: tuple[int, int, int]) -> tuple[int, int, int]: - """Function to convert hsl color to rgb color and send main embed.""" - input_color = self.tuple_create(input_color) - (h, s, l) = input_color - h = h / 360 - s = s / 100 - l = l / 100 # noqa: E741 It's little `L`, Reason: To maintain consistency. - rgb_color = colorsys.hls_to_rgb(h, l, s) - (r, g, b) = rgb_color - r = int(r * 255) - g = int(g * 255) - b = int(b * 255) - await self.color_embed(ctx, (r, g, b)) - - async def cmyk_to_rgb( - self, - ctx: commands.Context, - input_color: tuple[int, int, int, int] - ) -> tuple[int, int, int]: - """Function to convert cmyk color to rgb color and send main embed.""" - input_color = self.tuple_create(input_color) - c = input_color[0] - m = input_color[1] - y = input_color[2] - k = input_color[3] - r = int(255 * (1.0 - c / float(100)) * (1.0 - k / float(100))) - g = int(255 * (1.0 - m / float(100)) * (1.0 - k / float(100))) - b = int(255 * (1.0 - y / float(100)) * (1.0 - k / float(100))) - await self.color_embed(ctx, (r, g, b)) - - @staticmethod - async def create_thumbnail_attachment(color: tuple[int, int, int]) -> File: - """ - Generate a thumbnail from `color`. - - Assumes that color is an rgb tuple. - """ - thumbnail = Image.new("RGB", (80, 80), color=color) - bufferedio = BytesIO() - thumbnail.save(bufferedio, format="PNG") - bufferedio.seek(0) - - file = File(bufferedio, filename="color.png") - - return file - - @staticmethod - def get_color_fields(rgb_color: tuple[int, int, int]) -> list[dict]: - """Converts from `RGB` to `CMYK`, `HSV`, `HSL` and returns a list of fields.""" - - def _rgb_to_hex(rgb_color: tuple[int, int, int]) -> str: - """To convert from `RGB` to `Hex` notation.""" - return '#' + ''.join(hex(int(color))[2:].zfill(2) for color in rgb_color).upper() - - def _rgb_to_cmyk(rgb_color: tuple[int, int, int]) -> tuple[int, int, int, int]: - """To convert from `RGB` to `CMYK` color space.""" - r, g, b = rgb_color - - # RGB_SCALE -> 255 - # CMYK_SCALE -> 100 - - if (r == g == b == 0): - return 0, 0, 0, 100 # Representing Black - - # rgb [0,RGB_SCALE] -> cmy [0,1] - c = 1 - r / 255 - m = 1 - g / 255 - y = 1 - b / 255 - - # extract out k [0, 1] - min_cmy = min(c, m, y) - c = (c - min_cmy) / (1 - min_cmy) - m = (m - min_cmy) / (1 - min_cmy) - y = (y - min_cmy) / (1 - min_cmy) - k = min_cmy - - # rescale to the range [0,CMYK_SCALE] and round off - c = round(c * 100) - m = round(m * 100) - y = round(y * 100) - k = round(k * 100) - - return c, m, y, k - - def _rgb_to_hsv(rgb_color: tuple[int, int, int]) -> tuple[int, int, int]: - """To convert from `RGB` to `HSV` color space.""" - r, g, b = rgb_color - h, v, s = colorsys.rgb_to_hsv(r / float(255), g / float(255), b / float(255)) - h = round(h * 360) - s = round(s * 100) - v = round(v * 100) - return h, s, v - - def _rgb_to_hsl(rgb_color: tuple[int, int, int]) -> tuple[int, int, int]: - """To convert from `RGB` to `HSL` color space.""" - r, g, b = rgb_color - h, l, s = colorsys.rgb_to_hls(r / float(255), g / float(255), b / float(255)) - h = round(h * 360) - s = round(s * 100) - l = round(l * 100) # noqa: E741 It's little `L`, Reason: To maintain consistency. - return h, s, l - - all_fields = [ - { - "name": "RGB", - "value": f"Β» rgb {rgb_color}" - }, - { - "name": "HEX", - "value": f"Β» hex {_rgb_to_hex(rgb_color)}" - }, - { - "name": "CMYK", - "value": f"Β» cmyk {_rgb_to_cmyk(rgb_color)}" - }, - { - "name": "HSV", - "value": f"Β» hsv {_rgb_to_hsv(rgb_color)}" - }, - { - "name": "HSL", - "value": f"Β» hsl {_rgb_to_hsl(rgb_color)}" - }, - ] - - return all_fields + logger.debug(f"{match = }, {certainty = }") + color_name = [name for name, _ in COLOR_MAPPING.items() if _ == match][0] + logger.debug(f"{color_name = }") + except TypeError: + color_name = None + return color_name @staticmethod def match_color_name(input_color_name: str) -> str: @@ -406,66 +195,14 @@ class Color(commands.Cog): score_cutoff=50 ) logger.debug(f"{match = }, {certainty = }") - hex_match = COLOR_MAPPING[match] + hex_match = f"#{COLOR_MAPPING[match]}" logger.debug(f"{hex_match = }") except TypeError: match = "No color name match found." hex_match = input_color_name - return match, hex_match - @staticmethod - def match_color_hex(input_hex_color: str) -> str: - """Use fuzzy matching to return a hex color code based on the user's input.""" - try: - match, certainty, _ = process.extractOne( - query=input_hex_color, - choices=COLOR_MAPPING.values(), - score_cutoff=80 - ) - logger.debug(f"{match = }, {certainty = }") - color_name = [name for name, _ in COLOR_MAPPING.items() if _ == match][0] - logger.debug(f"{color_name = }") - except TypeError: - color_name = "No color name match found." - - return color_name - - async def color_embed( - self, - ctx: commands.Context, - rgb_color: tuple[int, int, int], - color_name: str = None - ) -> None: - """Take a RGB color tuple, create embed, and send.""" - (r, g, b) = rgb_color - discord_rgb_int = int(f"{r:02x}{g:02x}{b:02x}", 16) - all_colors = self.get_color_fields(rgb_color) - hex_color = all_colors[1]["value"].replace("Β» hex ", "") - if color_name is None: - logger.debug(f"Find color name from hex color: {hex_color}") - color_name = self.match_color_hex(hex_color) - - async with ctx.typing(): - main_embed = Embed( - title=color_name, - description='(Approx..)', - color=discord_rgb_int, - ) - - file = await self.create_thumbnail_attachment(rgb_color) - main_embed.set_thumbnail(url="attachment://color.png") - - for field in all_colors: - main_embed.add_field( - name=field['name'], - value=field['value'], - inline=False, - ) - - await ctx.send(file=file, embed=main_embed) - def setup(bot: Bot) -> None: - """Load the Color Cog.""" - bot.add_cog(Color(bot)) + """Load the Colour cog.""" + bot.add_cog(Colour(bot)) -- cgit v1.2.3 From d6400337320124ed8ac48445aa2938ac74fb41c5 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Mon, 25 Oct 2021 19:40:55 -0400 Subject: chore: fix import order due to isort --- bot/exts/utilities/color.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index dc63cf84..b5caf357 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -4,9 +4,9 @@ import logging import random from io import BytesIO +from PIL import Image, ImageColor from discord import Embed, File from discord.ext import commands -from PIL import Image, ImageColor from rapidfuzz import process from bot.bot import Bot -- cgit v1.2.3 From fed6c1315a04ae8970b1a9d1f23f1b8ed4615d86 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Tue, 26 Oct 2021 07:02:37 -0400 Subject: chore: code cleanup --- bot/exts/utilities/color.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index b5caf357..618970df 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -1,6 +1,6 @@ import colorsys import json -import logging +import pathlib import random from io import BytesIO @@ -12,23 +12,16 @@ from rapidfuzz import process from bot.bot import Bot from bot.exts.core.extensions import invoke_help_command -logger = logging.getLogger(__name__) - - -with open("bot/resources/utilities/ryanzec_colours.json") as f: +with open(pathlib.Path("bot/resources/utilities/ryanzec_colours.json")) as f: COLOR_MAPPING = json.load(f) - THUMBNAIL_SIZE = 80 class Colour(commands.Cog): """Cog for the Colour command.""" - def __init__(self, bot: Bot) -> None: - self.bot = bot - - @commands.group(aliases=["color"]) + @commands.group(aliases=("color",)) async def colour(self, ctx: commands.Context) -> None: """ User initiated command to create an embed that displays color information. @@ -97,7 +90,7 @@ class Colour(commands.Cog): if name is None: desc = "Color information for the input color." else: - desc = f"Color information for {name}" + desc = f"Color information for {name}." colour_embed = Embed( title="Colour", description=desc, @@ -178,9 +171,7 @@ class Colour(commands.Cog): choices=COLOR_MAPPING.values(), score_cutoff=80 ) - logger.debug(f"{match = }, {certainty = }") color_name = [name for name, _ in COLOR_MAPPING.items() if _ == match][0] - logger.debug(f"{color_name = }") except TypeError: color_name = None return color_name @@ -194,9 +185,7 @@ class Colour(commands.Cog): choices=COLOR_MAPPING.keys(), score_cutoff=50 ) - logger.debug(f"{match = }, {certainty = }") hex_match = f"#{COLOR_MAPPING[match]}" - logger.debug(f"{hex_match = }") except TypeError: match = "No color name match found." hex_match = input_color_name -- cgit v1.2.3 From 626df00917a191eb3758a33ec112f4d87bcbb0af Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Tue, 26 Oct 2021 08:15:34 -0400 Subject: chore: code cleanup --- bot/exts/utilities/color.py | 152 +++++++++++++++++++++++++------------------- 1 file changed, 86 insertions(+), 66 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 618970df..606f5fd4 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -12,91 +12,112 @@ from rapidfuzz import process from bot.bot import Bot from bot.exts.core.extensions import invoke_help_command -with open(pathlib.Path("bot/resources/utilities/ryanzec_colours.json")) as f: - COLOR_MAPPING = json.load(f) - -THUMBNAIL_SIZE = 80 +THUMBNAIL_SIZE = (80, 80) class Colour(commands.Cog): """Cog for the Colour command.""" + def __init__(self): + with open(pathlib.Path("bot/resources/utilities/ryanzec_colours.json")) as f: + self.COLOUR_MAPPING = json.load(f) + @commands.group(aliases=("color",)) async def colour(self, ctx: commands.Context) -> None: - """ - User initiated command to create an embed that displays color information. - - For the commands `hsl`, `hsv` and `rgb`: input is in the form `.color ` - For the command `cmyk`: input is in the form `.color cmyk ` - For the command `hex`: input is in the form `.color hex #` - For the command `name`: input is in the form `.color name ` - For the command `random`: input is in the form `.color random` - """ + """User initiated command to create an embed that displays colour information.""" if ctx.invoked_subcommand is None: await invoke_help_command(ctx) @colour.command() async def rgb(self, ctx: commands.Context, red: int, green: int, blue: int) -> None: - """Function to create an embed from an RGB input.""" + """ + Command to create an embed from an RGB input. + + Input is in the form `.colour rgb ` + """ rgb_tuple = ImageColor.getrgb(f"rgb({red}, {green}, {blue})") - await Colour.send_colour_response(ctx, list(rgb_tuple)) + await self.send_colour_response(ctx, list(rgb_tuple)) @colour.command() async def hsv(self, ctx: commands.Context, hue: int, saturation: int, value: int) -> None: - """Function to create an embed from an HSV input.""" + """ + Command to create an embed from an HSV input. + + Input is in the form `.colour hsv ` + """ hsv_tuple = ImageColor.getrgb(f"hsv({hue}, {saturation}%, {value}%)") - await Colour.send_colour_response(ctx, list(hsv_tuple)) + await self.send_colour_response(ctx, list(hsv_tuple)) @colour.command() async def hsl(self, ctx: commands.Context, hue: int, saturation: int, lightness: int) -> None: - """Function to create an embed from an HSL input.""" + """ + Command to create an embed from an HSL input. + + Input is in the form `.colour hsl ` + """ hsl_tuple = ImageColor.getrgb(f"hsl({hue}, {saturation}%, {lightness}%)") - await Colour.send_colour_response(ctx, list(hsl_tuple)) + await self.send_colour_response(ctx, list(hsl_tuple)) @colour.command() async def cmyk(self, ctx: commands.Context, cyan: int, yellow: int, magenta: int, key: int) -> None: - """Function to create an embed from a CMYK input.""" + """ + Command to create an embed from a CMYK input. + + Input is in the form `.colour cmyk ` + """ r = int(255 * (1.0 - cyan / float(100)) * (1.0 - key / float(100))) g = int(255 * (1.0 - magenta / float(100)) * (1.0 - key / float(100))) b = int(255 * (1.0 - yellow / float(100)) * (1.0 - key / float(100))) - await Colour.send_colour_response(ctx, list((r, g, b))) + await self.send_colour_response(ctx, list((r, g, b))) @colour.command() async def hex(self, ctx: commands.Context, hex_code: str) -> None: - """Function to create an embed from a HEX input. (Requires # as a prefix).""" + """ + Command to create an embed from a HEX input. + + Input is in the form `.colour hex #` + """ hex_tuple = ImageColor.getrgb(hex_code) - await Colour.send_colour_response(ctx, list(hex_tuple)) + await self.send_colour_response(ctx, list(hex_tuple)) @colour.command() - async def name(self, ctx: commands.Context, user_color: str) -> None: - """Function to create an embed from a name input.""" - _, hex_color = self.match_color_name(user_color) - hex_tuple = ImageColor.getrgb(hex_color) - await Colour.send_colour_response(ctx, list(hex_tuple)) + async def name(self, ctx: commands.Context, user_colour: str) -> None: + """ + Command to create an embed from a name input. + + Input is in the form `.colour name ` + """ + _, hex_colour = self.match_colour_name(user_colour) + hex_tuple = ImageColor.getrgb(hex_colour) + await self.send_colour_response(ctx, list(hex_tuple)) @colour.command() async def random(self, ctx: commands.Context) -> None: - """Function to create an embed from a randomly chosen color from the ryanzec.json file.""" - color_choices = list(COLOR_MAPPING.values()) - hex_color = random.choice(color_choices) - hex_tuple = ImageColor.getrgb(f"#{hex_color}") - await Colour.send_colour_response(ctx, list(hex_tuple)) + """ + Command to create an embed from a randomly chosen colour from the reference file. - @staticmethod - async def send_colour_response(ctx: commands.Context, rgb: list[int]) -> None: - """Function to create and send embed from color information.""" + Input is in the form `.colour random` + """ + colour_choices = list(self.COLOUR_MAPPING.values()) + hex_colour = random.choice(colour_choices) + hex_tuple = ImageColor.getrgb(f"#{hex_colour}") + await self.send_colour_response(ctx, list(hex_tuple)) + + async def send_colour_response(self, ctx: commands.Context, rgb: list[int]) -> None: + """Function to create and send embed from colour information.""" r, g, b = rgb[0], rgb[1], rgb[2] - name = Colour._rgb_to_name(rgb) + name = self._rgb_to_name(rgb) + colour_mode = ctx.invoked_command if name is None: - desc = "Color information for the input color." + desc = f"{colour_mode.title()} information for the input colour." else: - desc = f"Color information for {name}." + desc = f"{colour_mode.title()} information for {name}." colour_embed = Embed( title="Colour", description=desc, colour=int(f"{r:02x}{g:02x}{b:02x}", 16) ) - colour_conversions = Colour.get_colour_conversions(rgb) + colour_conversions = self.get_colour_conversions(rgb) for colour_space, value in colour_conversions.items(): colour_embed.add_field( name=colour_space.upper(), @@ -104,7 +125,7 @@ class Colour(commands.Cog): inline=True ) - thumbnail = Image.new("RGB", (THUMBNAIL_SIZE, THUMBNAIL_SIZE), color=tuple(rgb)) + thumbnail = Image.new("RGB", THUMBNAIL_SIZE, color=tuple(rgb)) buffer = BytesIO() thumbnail.save(buffer, "PNG") buffer.seek(0) @@ -114,16 +135,15 @@ class Colour(commands.Cog): await ctx.send(file=thumbnail_file, embed=colour_embed) - @staticmethod - def get_colour_conversions(rgb: list[int]) -> dict[str, str]: - """Create a dictionary mapping of color types and their values.""" + def get_colour_conversions(self, rgb: list[int]) -> dict[str, str]: + """Create a dictionary mapping of colour types and their values.""" return { "rgb": tuple(rgb), - "hsv": Colour._rgb_to_hsv(rgb), - "hsl": Colour._rgb_to_hsl(rgb), - "cmyk": Colour._rgb_to_cmyk(rgb), - "hex": Colour._rgb_to_hex(rgb), - "name": Colour._rgb_to_name(rgb) + "hsv": self._rgb_to_hsv(rgb), + "hsl": self._rgb_to_hsl(rgb), + "cmyk": self._rgb_to_cmyk(rgb), + "hex": self._rgb_to_hex(rgb), + "name": self._rgb_to_name(rgb) } @staticmethod @@ -161,34 +181,34 @@ class Colour(commands.Cog): hex_code = f"#{hex_}".upper() return hex_code - @staticmethod - def _rgb_to_name(rgb: list[int]) -> str: - """Function to convert from an RGB list to a fuzzy matched color name.""" - input_hex_color = Colour._rgb_to_hex(rgb) + @classmethod + def _rgb_to_name(cls, rgb: list[int]) -> str: + """Function to convert from an RGB list to a fuzzy matched colour name.""" + input_hex_colour = cls._rgb_to_hex(rgb) try: match, certainty, _ = process.extractOne( - query=input_hex_color, - choices=COLOR_MAPPING.values(), + query=input_hex_colour, + choices=cls.COLOUR_MAPPING.values(), score_cutoff=80 ) - color_name = [name for name, _ in COLOR_MAPPING.items() if _ == match][0] + colour_name = [name for name, _ in cls.COLOUR_MAPPING.items() if _ == match][0] except TypeError: - color_name = None - return color_name + colour_name = None + return colour_name - @staticmethod - def match_color_name(input_color_name: str) -> str: - """Use fuzzy matching to return a hex color code based on the user's input.""" + @classmethod + def match_colour_name(cls, input_colour_name: str) -> str: + """Use fuzzy matching to return a hex colour code based on the user's input.""" try: match, certainty, _ = process.extractOne( - query=input_color_name, - choices=COLOR_MAPPING.keys(), + query=input_colour_name, + choices=cls.COLOUR_MAPPING.keys(), score_cutoff=50 ) - hex_match = f"#{COLOR_MAPPING[match]}" + hex_match = f"#{cls.COLOUR_MAPPING[match]}" except TypeError: - match = "No color name match found." - hex_match = input_color_name + match = "No colour name match found." + hex_match = input_colour_name return match, hex_match -- cgit v1.2.3 From d6d8992e68a18819706bcb2a6e40a7ee1e581ca9 Mon Sep 17 00:00:00 2001 From: TizzySaurus <47674925+TizzySaurus@users.noreply.github.com> Date: Wed, 27 Oct 2021 00:13:25 +0100 Subject: Migrate to `og_blurple` (#924) --- bot/exts/core/extensions.py | 2 +- bot/exts/holidays/halloween/candy_collection.py | 2 +- bot/exts/utilities/conversationstarters.py | 2 +- bot/exts/utilities/emoji.py | 2 +- bot/exts/utilities/githubinfo.py | 4 ++-- bot/exts/utilities/reddit.py | 10 +++++----- bot/exts/utilities/wikipedia.py | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/bot/exts/core/extensions.py b/bot/exts/core/extensions.py index dbb9e069..d809d2b9 100644 --- a/bot/exts/core/extensions.py +++ b/bot/exts/core/extensions.py @@ -152,7 +152,7 @@ class Extensions(commands.Cog): Grey indicates that the extension is unloaded. Green indicates that the extension is currently loaded. """ - embed = Embed(colour=Colour.blurple()) + embed = Embed(colour=Colour.og_blurple()) embed.set_author( name="Extensions List", url=Client.github_bot_repo, diff --git a/bot/exts/holidays/halloween/candy_collection.py b/bot/exts/holidays/halloween/candy_collection.py index 09bd0e59..079d900d 100644 --- a/bot/exts/holidays/halloween/candy_collection.py +++ b/bot/exts/holidays/halloween/candy_collection.py @@ -182,7 +182,7 @@ class CandyCollection(commands.Cog): for index, record in enumerate(top_five) ) if top_five else "No Candies" - e = discord.Embed(colour=discord.Colour.blurple()) + e = discord.Embed(colour=discord.Colour.og_blurple()) e.add_field( name="Top Candy Records", value=generate_leaderboard(), diff --git a/bot/exts/utilities/conversationstarters.py b/bot/exts/utilities/conversationstarters.py index dcbfe4d5..8bf2abfd 100644 --- a/bot/exts/utilities/conversationstarters.py +++ b/bot/exts/utilities/conversationstarters.py @@ -53,7 +53,7 @@ class ConvoStarters(commands.Cog): # No matter what, the form will be shown. embed = discord.Embed( description=f"Suggest more topics [here]({SUGGESTION_FORM})!", - color=discord.Color.blurple() + color=discord.Colour.og_blurple() ) try: diff --git a/bot/exts/utilities/emoji.py b/bot/exts/utilities/emoji.py index 83df39cc..fa438d7f 100644 --- a/bot/exts/utilities/emoji.py +++ b/bot/exts/utilities/emoji.py @@ -111,7 +111,7 @@ class Emojis(commands.Cog): **Date:** {datetime.strftime(emoji.created_at.replace(tzinfo=None), "%d/%m/%Y")} **ID:** {emoji.id} """), - color=Color.blurple(), + color=Color.og_blurple(), url=str(emoji.url), ).set_thumbnail(url=emoji.url) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index d00b408d..539e388b 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -67,7 +67,7 @@ class GithubInfo(commands.Cog): embed = discord.Embed( title=f"`{user_data['login']}`'s GitHub profile info", description=f"```\n{user_data['bio']}\n```\n" if user_data["bio"] else "", - colour=discord.Colour.blurple(), + colour=discord.Colour.og_blurple(), url=user_data["html_url"], timestamp=datetime.strptime(user_data["created_at"], "%Y-%m-%dT%H:%M:%SZ") ) @@ -139,7 +139,7 @@ class GithubInfo(commands.Cog): embed = discord.Embed( title=repo_data["name"], description=repo_data["description"], - colour=discord.Colour.blurple(), + colour=discord.Colour.og_blurple(), url=repo_data["html_url"] ) diff --git a/bot/exts/utilities/reddit.py b/bot/exts/utilities/reddit.py index e6cb5337..782583d2 100644 --- a/bot/exts/utilities/reddit.py +++ b/bot/exts/utilities/reddit.py @@ -244,7 +244,7 @@ class Reddit(Cog): # Use only starting summary page for #reddit channel posts. embed.description = self.build_pagination_pages(posts, paginate=False) - embed.colour = Colour.blurple() + embed.colour = Colour.og_blurple() return embed @loop() @@ -312,7 +312,7 @@ class Reddit(Cog): await ctx.send(f"Here are the top {subreddit} posts of all time!") embed = Embed( - color=Colour.blurple() + color=Colour.og_blurple() ) await ImagePaginator.paginate(pages, ctx, embed) @@ -325,7 +325,7 @@ class Reddit(Cog): await ctx.send(f"Here are today's top {subreddit} posts!") embed = Embed( - color=Colour.blurple() + color=Colour.og_blurple() ) await ImagePaginator.paginate(pages, ctx, embed) @@ -338,7 +338,7 @@ class Reddit(Cog): await ctx.send(f"Here are this week's top {subreddit} posts!") embed = Embed( - color=Colour.blurple() + color=Colour.og_blurple() ) await ImagePaginator.paginate(pages, ctx, embed) @@ -349,7 +349,7 @@ class Reddit(Cog): """Send a paginated embed of all the subreddits we're relaying.""" embed = Embed() embed.title = "Relayed subreddits." - embed.colour = Colour.blurple() + embed.colour = Colour.og_blurple() await LinePaginator.paginate( RedditConfig.subreddits, diff --git a/bot/exts/utilities/wikipedia.py b/bot/exts/utilities/wikipedia.py index eccc1f8c..c5283de0 100644 --- a/bot/exts/utilities/wikipedia.py +++ b/bot/exts/utilities/wikipedia.py @@ -82,7 +82,7 @@ class WikipediaSearch(commands.Cog): if contents: embed = Embed( title="Wikipedia Search Results", - colour=Color.blurple() + colour=Color.og_blurple() ) embed.set_thumbnail(url=WIKI_THUMBNAIL) embed.timestamp = datetime.utcnow() -- cgit v1.2.3 From 05b56c0d58829b188f55490f9d6825ef95b9d9fb Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Wed, 27 Oct 2021 06:56:35 -0400 Subject: fix: testing fixes --- bot/exts/utilities/color.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 606f5fd4..495a1fd4 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -18,7 +18,8 @@ THUMBNAIL_SIZE = (80, 80) class Colour(commands.Cog): """Cog for the Colour command.""" - def __init__(self): + def __init__(self, bot: Bot): + self.bot = bot with open(pathlib.Path("bot/resources/utilities/ryanzec_colours.json")) as f: self.COLOUR_MAPPING = json.load(f) @@ -107,7 +108,7 @@ class Colour(commands.Cog): """Function to create and send embed from colour information.""" r, g, b = rgb[0], rgb[1], rgb[2] name = self._rgb_to_name(rgb) - colour_mode = ctx.invoked_command + colour_mode = ctx.invoked_with if name is None: desc = f"{colour_mode.title()} information for the input colour." else: @@ -181,31 +182,29 @@ class Colour(commands.Cog): hex_code = f"#{hex_}".upper() return hex_code - @classmethod - def _rgb_to_name(cls, rgb: list[int]) -> str: + def _rgb_to_name(self, rgb: list[int]) -> str: """Function to convert from an RGB list to a fuzzy matched colour name.""" - input_hex_colour = cls._rgb_to_hex(rgb) + input_hex_colour = self._rgb_to_hex(rgb) try: match, certainty, _ = process.extractOne( query=input_hex_colour, - choices=cls.COLOUR_MAPPING.values(), + choices=self.COLOUR_MAPPING.values(), score_cutoff=80 ) - colour_name = [name for name, _ in cls.COLOUR_MAPPING.items() if _ == match][0] + colour_name = [name for name, _ in self.COLOUR_MAPPING.items() if _ == match][0] except TypeError: colour_name = None return colour_name - @classmethod - def match_colour_name(cls, input_colour_name: str) -> str: + def match_colour_name(self, input_colour_name: str) -> str: """Use fuzzy matching to return a hex colour code based on the user's input.""" try: match, certainty, _ = process.extractOne( query=input_colour_name, - choices=cls.COLOUR_MAPPING.keys(), + choices=self.COLOUR_MAPPING.keys(), score_cutoff=50 ) - hex_match = f"#{cls.COLOUR_MAPPING[match]}" + hex_match = f"#{self.COLOUR_MAPPING[match]}" except TypeError: match = "No colour name match found." hex_match = input_colour_name -- cgit v1.2.3 From 0fd2b8f960bcef646a16661f63e0fc21742fb8ab Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Wed, 27 Oct 2021 08:01:02 -0400 Subject: chore: embed format tweaking --- bot/exts/utilities/color.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 495a1fd4..a5b374d6 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -110,9 +110,9 @@ class Colour(commands.Cog): name = self._rgb_to_name(rgb) colour_mode = ctx.invoked_with if name is None: - desc = f"{colour_mode.title()} information for the input colour." + desc = f"{colour_mode.upper()} information for the input colour." else: - desc = f"{colour_mode.title()} information for {name}." + desc = f"{colour_mode.upper()} information for `{name}`." colour_embed = Embed( title="Colour", description=desc, -- cgit v1.2.3 From 20a3d55ef27a84e1911ea748e6edbe8b244d3317 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Wed, 27 Oct 2021 14:49:42 -0400 Subject: chore: move send_colour_response to top of class --- bot/exts/utilities/color.py | 64 ++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index a5b374d6..ef2421e4 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -23,6 +23,38 @@ class Colour(commands.Cog): with open(pathlib.Path("bot/resources/utilities/ryanzec_colours.json")) as f: self.COLOUR_MAPPING = json.load(f) + async def send_colour_response(self, ctx: commands.Context, rgb: list[int]) -> None: + """Function to create and send embed from colour information.""" + r, g, b = rgb[0], rgb[1], rgb[2] + name = self._rgb_to_name(rgb) + colour_mode = ctx.invoked_with + if name is None: + desc = f"{colour_mode.upper()} information for the input colour." + else: + desc = f"{colour_mode.upper()} information for `{name}`." + colour_embed = Embed( + title="Colour", + description=desc, + colour=int(f"{r:02x}{g:02x}{b:02x}", 16) + ) + colour_conversions = self.get_colour_conversions(rgb) + for colour_space, value in colour_conversions.items(): + colour_embed.add_field( + name=colour_space.upper(), + value=f"`{value}`", + inline=True + ) + + thumbnail = Image.new("RGB", THUMBNAIL_SIZE, color=tuple(rgb)) + buffer = BytesIO() + thumbnail.save(buffer, "PNG") + buffer.seek(0) + thumbnail_file = File(buffer, filename="colour.png") + + colour_embed.set_thumbnail(url="attachment://colour.png") + + await ctx.send(file=thumbnail_file, embed=colour_embed) + @commands.group(aliases=("color",)) async def colour(self, ctx: commands.Context) -> None: """User initiated command to create an embed that displays colour information.""" @@ -104,38 +136,6 @@ class Colour(commands.Cog): hex_tuple = ImageColor.getrgb(f"#{hex_colour}") await self.send_colour_response(ctx, list(hex_tuple)) - async def send_colour_response(self, ctx: commands.Context, rgb: list[int]) -> None: - """Function to create and send embed from colour information.""" - r, g, b = rgb[0], rgb[1], rgb[2] - name = self._rgb_to_name(rgb) - colour_mode = ctx.invoked_with - if name is None: - desc = f"{colour_mode.upper()} information for the input colour." - else: - desc = f"{colour_mode.upper()} information for `{name}`." - colour_embed = Embed( - title="Colour", - description=desc, - colour=int(f"{r:02x}{g:02x}{b:02x}", 16) - ) - colour_conversions = self.get_colour_conversions(rgb) - for colour_space, value in colour_conversions.items(): - colour_embed.add_field( - name=colour_space.upper(), - value=f"`{value}`", - inline=True - ) - - thumbnail = Image.new("RGB", THUMBNAIL_SIZE, color=tuple(rgb)) - buffer = BytesIO() - thumbnail.save(buffer, "PNG") - buffer.seek(0) - thumbnail_file = File(buffer, filename="colour.png") - - colour_embed.set_thumbnail(url="attachment://colour.png") - - await ctx.send(file=thumbnail_file, embed=colour_embed) - def get_colour_conversions(self, rgb: list[int]) -> dict[str, str]: """Create a dictionary mapping of colour types and their values.""" return { -- cgit v1.2.3 From 7d70bb40c164d176e3cb5653335c13196515f854 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Thu, 28 Oct 2021 07:09:01 -0400 Subject: chore: general code fixes and cleanup --- bot/exts/utilities/color.py | 91 +++++++++++++++------------------------------ 1 file changed, 29 insertions(+), 62 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index ef2421e4..66dc78de 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -24,14 +24,11 @@ class Colour(commands.Cog): self.COLOUR_MAPPING = json.load(f) async def send_colour_response(self, ctx: commands.Context, rgb: list[int]) -> None: - """Function to create and send embed from colour information.""" + """Create and send embed from user given colour information.""" r, g, b = rgb[0], rgb[1], rgb[2] name = self._rgb_to_name(rgb) colour_mode = ctx.invoked_with - if name is None: - desc = f"{colour_mode.upper()} information for the input colour." - else: - desc = f"{colour_mode.upper()} information for `{name}`." + desc = f"{colour_mode.upper()} information for `{name or 'the input colour'}`." colour_embed = Embed( title="Colour", description=desc, @@ -40,7 +37,7 @@ class Colour(commands.Cog): colour_conversions = self.get_colour_conversions(rgb) for colour_space, value in colour_conversions.items(): colour_embed.add_field( - name=colour_space.upper(), + name=colour_space, value=f"`{value}`", inline=True ) @@ -63,41 +60,25 @@ class Colour(commands.Cog): @colour.command() async def rgb(self, ctx: commands.Context, red: int, green: int, blue: int) -> None: - """ - Command to create an embed from an RGB input. - - Input is in the form `.colour rgb ` - """ + """Command to create an embed from an RGB input.""" rgb_tuple = ImageColor.getrgb(f"rgb({red}, {green}, {blue})") await self.send_colour_response(ctx, list(rgb_tuple)) @colour.command() async def hsv(self, ctx: commands.Context, hue: int, saturation: int, value: int) -> None: - """ - Command to create an embed from an HSV input. - - Input is in the form `.colour hsv ` - """ + """Command to create an embed from an HSV input.""" hsv_tuple = ImageColor.getrgb(f"hsv({hue}, {saturation}%, {value}%)") await self.send_colour_response(ctx, list(hsv_tuple)) @colour.command() async def hsl(self, ctx: commands.Context, hue: int, saturation: int, lightness: int) -> None: - """ - Command to create an embed from an HSL input. - - Input is in the form `.colour hsl ` - """ + """Command to create an embed from an HSL input.""" hsl_tuple = ImageColor.getrgb(f"hsl({hue}, {saturation}%, {lightness}%)") await self.send_colour_response(ctx, list(hsl_tuple)) @colour.command() async def cmyk(self, ctx: commands.Context, cyan: int, yellow: int, magenta: int, key: int) -> None: - """ - Command to create an embed from a CMYK input. - - Input is in the form `.colour cmyk ` - """ + """Command to create an embed from a CMYK input.""" r = int(255 * (1.0 - cyan / float(100)) * (1.0 - key / float(100))) g = int(255 * (1.0 - magenta / float(100)) * (1.0 - key / float(100))) b = int(255 * (1.0 - yellow / float(100)) * (1.0 - key / float(100))) @@ -105,51 +86,38 @@ class Colour(commands.Cog): @colour.command() async def hex(self, ctx: commands.Context, hex_code: str) -> None: - """ - Command to create an embed from a HEX input. - - Input is in the form `.colour hex #` - """ + """Command to create an embed from a HEX input.""" hex_tuple = ImageColor.getrgb(hex_code) await self.send_colour_response(ctx, list(hex_tuple)) @colour.command() async def name(self, ctx: commands.Context, user_colour: str) -> None: - """ - Command to create an embed from a name input. - - Input is in the form `.colour name ` - """ - _, hex_colour = self.match_colour_name(user_colour) + """Command to create an embed from a name input.""" + hex_colour = self.match_colour_name(user_colour) hex_tuple = ImageColor.getrgb(hex_colour) await self.send_colour_response(ctx, list(hex_tuple)) @colour.command() async def random(self, ctx: commands.Context) -> None: - """ - Command to create an embed from a randomly chosen colour from the reference file. - - Input is in the form `.colour random` - """ - colour_choices = list(self.COLOUR_MAPPING.values()) - hex_colour = random.choice(colour_choices) + """Command to create an embed from a randomly chosen colour from the reference file.""" + hex_colour = random.choice(list(self.COLOUR_MAPPING.values())) hex_tuple = ImageColor.getrgb(f"#{hex_colour}") await self.send_colour_response(ctx, list(hex_tuple)) def get_colour_conversions(self, rgb: list[int]) -> dict[str, str]: """Create a dictionary mapping of colour types and their values.""" return { - "rgb": tuple(rgb), - "hsv": self._rgb_to_hsv(rgb), - "hsl": self._rgb_to_hsl(rgb), - "cmyk": self._rgb_to_cmyk(rgb), - "hex": self._rgb_to_hex(rgb), - "name": self._rgb_to_name(rgb) + "RGB": tuple(rgb), + "HSV": self._rgb_to_hsv(rgb), + "HSL": self._rgb_to_hsl(rgb), + "CMYK": self._rgb_to_cmyk(rgb), + "Hex": self._rgb_to_hex(rgb), + "Name": self._rgb_to_name(rgb) } @staticmethod def _rgb_to_hsv(rgb: list[int]) -> tuple[int, int, int]: - """Function to convert an RGB list to a HSV list.""" + """Convert RGB values to HSV values.""" rgb = [val / 255.0 for val in rgb] h, v, s = colorsys.rgb_to_hsv(*rgb) hsv = (round(h * 360), round(s * 100), round(v * 100)) @@ -157,7 +125,7 @@ class Colour(commands.Cog): @staticmethod def _rgb_to_hsl(rgb: list[int]) -> tuple[int, int, int]: - """Function to convert an RGB list to a HSL list.""" + """Convert RGB values to HSL values.""" rgb = [val / 255.0 for val in rgb] h, l, s = colorsys.rgb_to_hls(*rgb) hsl = (round(h * 360), round(s * 100), round(l * 100)) @@ -165,7 +133,7 @@ class Colour(commands.Cog): @staticmethod def _rgb_to_cmyk(rgb: list[int]) -> tuple[int, int, int, int]: - """Function to convert an RGB list to a CMYK list.""" + """Convert RGB values to CMYK values.""" rgb = [val / 255.0 for val in rgb] if all(val == 0 for val in rgb): return 0, 0, 0, 100 @@ -177,13 +145,13 @@ class Colour(commands.Cog): @staticmethod def _rgb_to_hex(rgb: list[int]) -> str: - """Function to convert an RGB list to a HEX string.""" + """Convert RGB values to HEX code.""" hex_ = ''.join([hex(val)[2:].zfill(2) for val in rgb]) hex_code = f"#{hex_}".upper() return hex_code def _rgb_to_name(self, rgb: list[int]) -> str: - """Function to convert from an RGB list to a fuzzy matched colour name.""" + """Convert RGB values to a fuzzy matched name.""" input_hex_colour = self._rgb_to_hex(rgb) try: match, certainty, _ = process.extractOne( @@ -191,24 +159,23 @@ class Colour(commands.Cog): choices=self.COLOUR_MAPPING.values(), score_cutoff=80 ) - colour_name = [name for name, _ in self.COLOUR_MAPPING.items() if _ == match][0] + colour_name = [name for name, hex_code in self.COLOUR_MAPPING.items() if hex_code == match][0] except TypeError: colour_name = None return colour_name def match_colour_name(self, input_colour_name: str) -> str: - """Use fuzzy matching to return a hex colour code based on the user's input.""" + """Convert a colour name to HEX code.""" try: match, certainty, _ = process.extractOne( query=input_colour_name, choices=self.COLOUR_MAPPING.keys(), - score_cutoff=50 + score_cutoff=90 ) hex_match = f"#{self.COLOUR_MAPPING[match]}" - except TypeError: - match = "No colour name match found." - hex_match = input_colour_name - return match, hex_match + except (ValueError, TypeError): + hex_match = None + return hex_match def setup(bot: Bot) -> None: -- cgit v1.2.3 From a742fe985eaee3757e565b6f8880c014eda15e10 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Thu, 28 Oct 2021 08:49:57 -0400 Subject: chore: change to tuples, various suggested changes --- bot/exts/utilities/color.py | 60 ++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 66dc78de..e9e8dcf6 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -23,9 +23,9 @@ class Colour(commands.Cog): with open(pathlib.Path("bot/resources/utilities/ryanzec_colours.json")) as f: self.COLOUR_MAPPING = json.load(f) - async def send_colour_response(self, ctx: commands.Context, rgb: list[int]) -> None: + async def send_colour_response(self, ctx: commands.Context, rgb: tuple[int, int, int]) -> None: """Create and send embed from user given colour information.""" - r, g, b = rgb[0], rgb[1], rgb[2] + r, g, b = rgb name = self._rgb_to_name(rgb) colour_mode = ctx.invoked_with desc = f"{colour_mode.upper()} information for `{name or 'the input colour'}`." @@ -42,7 +42,7 @@ class Colour(commands.Cog): inline=True ) - thumbnail = Image.new("RGB", THUMBNAIL_SIZE, color=tuple(rgb)) + thumbnail = Image.new("RGB", THUMBNAIL_SIZE, color=rgb) buffer = BytesIO() thumbnail.save(buffer, "PNG") buffer.seek(0) @@ -61,53 +61,53 @@ class Colour(commands.Cog): @colour.command() async def rgb(self, ctx: commands.Context, red: int, green: int, blue: int) -> None: """Command to create an embed from an RGB input.""" - rgb_tuple = ImageColor.getrgb(f"rgb({red}, {green}, {blue})") - await self.send_colour_response(ctx, list(rgb_tuple)) + rgb_tuple = (red, green, blue) + await self.send_colour_response(ctx, rgb_tuple) @colour.command() async def hsv(self, ctx: commands.Context, hue: int, saturation: int, value: int) -> None: """Command to create an embed from an HSV input.""" hsv_tuple = ImageColor.getrgb(f"hsv({hue}, {saturation}%, {value}%)") - await self.send_colour_response(ctx, list(hsv_tuple)) + await self.send_colour_response(ctx, hsv_tuple) @colour.command() async def hsl(self, ctx: commands.Context, hue: int, saturation: int, lightness: int) -> None: """Command to create an embed from an HSL input.""" hsl_tuple = ImageColor.getrgb(f"hsl({hue}, {saturation}%, {lightness}%)") - await self.send_colour_response(ctx, list(hsl_tuple)) + await self.send_colour_response(ctx, hsl_tuple) @colour.command() async def cmyk(self, ctx: commands.Context, cyan: int, yellow: int, magenta: int, key: int) -> None: """Command to create an embed from a CMYK input.""" - r = int(255 * (1.0 - cyan / float(100)) * (1.0 - key / float(100))) - g = int(255 * (1.0 - magenta / float(100)) * (1.0 - key / float(100))) - b = int(255 * (1.0 - yellow / float(100)) * (1.0 - key / float(100))) - await self.send_colour_response(ctx, list((r, g, b))) + r = int(255 * (1.0 - cyan / 100) * (1.0 - key / 100)) + g = int(255 * (1.0 - magenta / 100) * (1.0 - key / 100)) + b = int(255 * (1.0 - yellow / 100) * (1.0 - key / 100)) + await self.send_colour_response(ctx, (r, g, b)) @colour.command() async def hex(self, ctx: commands.Context, hex_code: str) -> None: """Command to create an embed from a HEX input.""" hex_tuple = ImageColor.getrgb(hex_code) - await self.send_colour_response(ctx, list(hex_tuple)) + await self.send_colour_response(ctx, hex_tuple) @colour.command() - async def name(self, ctx: commands.Context, user_colour: str) -> None: + async def name(self, ctx: commands.Context, user_colour_name: str) -> None: """Command to create an embed from a name input.""" - hex_colour = self.match_colour_name(user_colour) + hex_colour = self.match_colour_name(user_colour_name) hex_tuple = ImageColor.getrgb(hex_colour) - await self.send_colour_response(ctx, list(hex_tuple)) + await self.send_colour_response(ctx, hex_tuple) @colour.command() async def random(self, ctx: commands.Context) -> None: """Command to create an embed from a randomly chosen colour from the reference file.""" hex_colour = random.choice(list(self.COLOUR_MAPPING.values())) hex_tuple = ImageColor.getrgb(f"#{hex_colour}") - await self.send_colour_response(ctx, list(hex_tuple)) + await self.send_colour_response(ctx, hex_tuple) - def get_colour_conversions(self, rgb: list[int]) -> dict[str, str]: + def get_colour_conversions(self, rgb: tuple[int, int, int]) -> dict[str, str]: """Create a dictionary mapping of colour types and their values.""" return { - "RGB": tuple(rgb), + "RGB": rgb, "HSV": self._rgb_to_hsv(rgb), "HSL": self._rgb_to_hsl(rgb), "CMYK": self._rgb_to_cmyk(rgb), @@ -116,41 +116,41 @@ class Colour(commands.Cog): } @staticmethod - def _rgb_to_hsv(rgb: list[int]) -> tuple[int, int, int]: + def _rgb_to_hsv(rgb: tuple[int, int, int]) -> tuple[int, int, int]: """Convert RGB values to HSV values.""" - rgb = [val / 255.0 for val in rgb] - h, v, s = colorsys.rgb_to_hsv(*rgb) + rgb_list = [val / 255.0 for val in rgb] + h, v, s = colorsys.rgb_to_hsv(*rgb_list) hsv = (round(h * 360), round(s * 100), round(v * 100)) return hsv @staticmethod - def _rgb_to_hsl(rgb: list[int]) -> tuple[int, int, int]: + def _rgb_to_hsl(rgb: tuple[int, int, int]) -> tuple[int, int, int]: """Convert RGB values to HSL values.""" - rgb = [val / 255.0 for val in rgb] - h, l, s = colorsys.rgb_to_hls(*rgb) + rgb_list = [val / 255.0 for val in rgb] + h, l, s = colorsys.rgb_to_hls(*rgb_list) hsl = (round(h * 360), round(s * 100), round(l * 100)) return hsl @staticmethod - def _rgb_to_cmyk(rgb: list[int]) -> tuple[int, int, int, int]: + def _rgb_to_cmyk(rgb: tuple[int, int, int, int]) -> tuple[int, int, int, int]: """Convert RGB values to CMYK values.""" - rgb = [val / 255.0 for val in rgb] - if all(val == 0 for val in rgb): + rgb_list = [val / 255.0 for val in rgb] + if not any(rgb_list): return 0, 0, 0, 100 - cmy = [1 - val / 255 for val in rgb] + cmy = [1 - val / 255 for val in rgb_list] min_cmy = min(cmy) cmyk = [(val - min_cmy) / (1 - min_cmy) for val in cmy] + [min_cmy] cmyk = [round(val * 100) for val in cmyk] return tuple(cmyk) @staticmethod - def _rgb_to_hex(rgb: list[int]) -> str: + def _rgb_to_hex(rgb: tuple[int, int, int]) -> str: """Convert RGB values to HEX code.""" hex_ = ''.join([hex(val)[2:].zfill(2) for val in rgb]) hex_code = f"#{hex_}".upper() return hex_code - def _rgb_to_name(self, rgb: list[int]) -> str: + def _rgb_to_name(self, rgb: tuple[int, int, int]) -> str: """Convert RGB values to a fuzzy matched name.""" input_hex_colour = self._rgb_to_hex(rgb) try: -- cgit v1.2.3 From b2151ebc86a538b6e8eb492a2b10a7cd2c12f900 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Thu, 28 Oct 2021 09:01:57 -0400 Subject: chore: correct capitalization of command name in embed --- bot/exts/utilities/color.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index e9e8dcf6..573039fa 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -28,9 +28,14 @@ class Colour(commands.Cog): r, g, b = rgb name = self._rgb_to_name(rgb) colour_mode = ctx.invoked_with - desc = f"{colour_mode.upper()} information for `{name or 'the input colour'}`." + colour_or_color = ctx.command.name + if colour_mode not in ("name", "hex", "random"): + colour_mode = colour_mode.upper() + else: + colour_mode = colour_mode.title() + desc = f"{colour_mode} information for `{name or 'the input colour'}`." colour_embed = Embed( - title="Colour", + title=colour_or_color.title(), description=desc, colour=int(f"{r:02x}{g:02x}{b:02x}", 16) ) -- cgit v1.2.3 From 364a7083ab38c3055c6db514e5203d68d61eb597 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Thu, 28 Oct 2021 09:13:49 -0400 Subject: chore: change COLOUR_MAPPING to colour_mapping --- bot/exts/utilities/color.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 573039fa..9356577f 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -21,7 +21,7 @@ class Colour(commands.Cog): def __init__(self, bot: Bot): self.bot = bot with open(pathlib.Path("bot/resources/utilities/ryanzec_colours.json")) as f: - self.COLOUR_MAPPING = json.load(f) + self.colour_mapping = json.load(f) async def send_colour_response(self, ctx: commands.Context, rgb: tuple[int, int, int]) -> None: """Create and send embed from user given colour information.""" @@ -105,7 +105,7 @@ class Colour(commands.Cog): @colour.command() async def random(self, ctx: commands.Context) -> None: """Command to create an embed from a randomly chosen colour from the reference file.""" - hex_colour = random.choice(list(self.COLOUR_MAPPING.values())) + hex_colour = random.choice(list(self.colour_mapping.values())) hex_tuple = ImageColor.getrgb(f"#{hex_colour}") await self.send_colour_response(ctx, hex_tuple) @@ -161,10 +161,10 @@ class Colour(commands.Cog): try: match, certainty, _ = process.extractOne( query=input_hex_colour, - choices=self.COLOUR_MAPPING.values(), + choices=self.colour_mapping.values(), score_cutoff=80 ) - colour_name = [name for name, hex_code in self.COLOUR_MAPPING.items() if hex_code == match][0] + colour_name = [name for name, hex_code in self.colour_mapping.items() if hex_code == match][0] except TypeError: colour_name = None return colour_name @@ -174,10 +174,10 @@ class Colour(commands.Cog): try: match, certainty, _ = process.extractOne( query=input_colour_name, - choices=self.COLOUR_MAPPING.keys(), + choices=self.colour_mapping.keys(), score_cutoff=90 ) - hex_match = f"#{self.COLOUR_MAPPING[match]}" + hex_match = f"#{self.colour_mapping[match]}" except (ValueError, TypeError): hex_match = None return hex_match -- cgit v1.2.3 From 869f21a7cc376ef2382bc88980b660226b621cdd Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Thu, 28 Oct 2021 19:00:31 -0400 Subject: fix: added error handling and other cleanup chores --- bot/exts/utilities/color.py | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 9356577f..fe57d388 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -7,6 +7,7 @@ from io import BytesIO from PIL import Image, ImageColor from discord import Embed, File from discord.ext import commands +from discord.ext.commands.errors import BadArgument from rapidfuzz import process from bot.bot import Bot @@ -28,12 +29,16 @@ class Colour(commands.Cog): r, g, b = rgb name = self._rgb_to_name(rgb) colour_mode = ctx.invoked_with - colour_or_color = ctx.command.name + try: + colour_or_color = ctx.invoked_parents[0] + except IndexError: + colour_or_color = "colour" + input_colour = ctx.args[2:] if colour_mode not in ("name", "hex", "random"): colour_mode = colour_mode.upper() else: colour_mode = colour_mode.title() - desc = f"{colour_mode} information for `{name or 'the input colour'}`." + desc = f"{colour_mode} information for `{name or input_colour}`." colour_embed = Embed( title=colour_or_color.title(), description=desc, @@ -66,32 +71,50 @@ class Colour(commands.Cog): @colour.command() async def rgb(self, ctx: commands.Context, red: int, green: int, blue: int) -> None: """Command to create an embed from an RGB input.""" + if (red or green or blue) > 250 or (red or green or blue) < 0: + raise BadArgument( + message=f"RGB values can only be from 0 to 250. User input was: `{red, green, blue}`" + ) rgb_tuple = (red, green, blue) await self.send_colour_response(ctx, rgb_tuple) @colour.command() async def hsv(self, ctx: commands.Context, hue: int, saturation: int, value: int) -> None: """Command to create an embed from an HSV input.""" + if (hue or saturation or value) > 250 or (hue or saturation or value) < 0: + raise BadArgument( + message=f"HSV values can only be from 0 to 250. User input was: `{hue, saturation, value}`" + ) hsv_tuple = ImageColor.getrgb(f"hsv({hue}, {saturation}%, {value}%)") await self.send_colour_response(ctx, hsv_tuple) @colour.command() async def hsl(self, ctx: commands.Context, hue: int, saturation: int, lightness: int) -> None: """Command to create an embed from an HSL input.""" + if (hue or saturation or lightness) > 360 or (hue or saturation or lightness) < 0: + raise BadArgument( + message=f"HSL values can only be from 0 to 360. User input was: `{hue, saturation, lightness}`" + ) hsl_tuple = ImageColor.getrgb(f"hsl({hue}, {saturation}%, {lightness}%)") await self.send_colour_response(ctx, hsl_tuple) @colour.command() async def cmyk(self, ctx: commands.Context, cyan: int, yellow: int, magenta: int, key: int) -> None: """Command to create an embed from a CMYK input.""" - r = int(255 * (1.0 - cyan / 100) * (1.0 - key / 100)) - g = int(255 * (1.0 - magenta / 100) * (1.0 - key / 100)) - b = int(255 * (1.0 - yellow / 100) * (1.0 - key / 100)) + if (cyan or magenta or yellow or key) > 100 or (cyan or magenta or yellow or key) < 0: + raise BadArgument( + message=f"CMYK values can only be from 0 to 100. User input was: `{cyan, magenta, yellow, key}`" + ) + r = int(255 * (1.0 - cyan / float(100)) * (1.0 - key / float(100))) + g = int(255 * (1.0 - magenta / float(100)) * (1.0 - key / float(100))) + b = int(255 * (1.0 - yellow / float(100)) * (1.0 - key / float(100))) await self.send_colour_response(ctx, (r, g, b)) @colour.command() async def hex(self, ctx: commands.Context, hex_code: str) -> None: """Command to create an embed from a HEX input.""" + if "#" not in hex_code: + hex_code = f"#{hex_code}" hex_tuple = ImageColor.getrgb(hex_code) await self.send_colour_response(ctx, hex_tuple) @@ -175,11 +198,12 @@ class Colour(commands.Cog): match, certainty, _ = process.extractOne( query=input_colour_name, choices=self.colour_mapping.keys(), - score_cutoff=90 + score_cutoff=80 ) hex_match = f"#{self.colour_mapping[match]}" except (ValueError, TypeError): hex_match = None + raise BadArgument(message=f"No color found for: `{input_colour_name}`") return hex_match -- cgit v1.2.3 From 5437712fe6fb5bc2149207f342afdfd37edece6e Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Thu, 28 Oct 2021 19:01:11 -0400 Subject: fix: add periods to error messages --- bot/exts/utilities/color.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index fe57d388..1ad6df27 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -73,7 +73,7 @@ class Colour(commands.Cog): """Command to create an embed from an RGB input.""" if (red or green or blue) > 250 or (red or green or blue) < 0: raise BadArgument( - message=f"RGB values can only be from 0 to 250. User input was: `{red, green, blue}`" + message=f"RGB values can only be from 0 to 250. User input was: `{red, green, blue}`." ) rgb_tuple = (red, green, blue) await self.send_colour_response(ctx, rgb_tuple) @@ -83,7 +83,7 @@ class Colour(commands.Cog): """Command to create an embed from an HSV input.""" if (hue or saturation or value) > 250 or (hue or saturation or value) < 0: raise BadArgument( - message=f"HSV values can only be from 0 to 250. User input was: `{hue, saturation, value}`" + message=f"HSV values can only be from 0 to 250. User input was: `{hue, saturation, value}`." ) hsv_tuple = ImageColor.getrgb(f"hsv({hue}, {saturation}%, {value}%)") await self.send_colour_response(ctx, hsv_tuple) @@ -93,7 +93,7 @@ class Colour(commands.Cog): """Command to create an embed from an HSL input.""" if (hue or saturation or lightness) > 360 or (hue or saturation or lightness) < 0: raise BadArgument( - message=f"HSL values can only be from 0 to 360. User input was: `{hue, saturation, lightness}`" + message=f"HSL values can only be from 0 to 360. User input was: `{hue, saturation, lightness}`." ) hsl_tuple = ImageColor.getrgb(f"hsl({hue}, {saturation}%, {lightness}%)") await self.send_colour_response(ctx, hsl_tuple) @@ -103,7 +103,7 @@ class Colour(commands.Cog): """Command to create an embed from a CMYK input.""" if (cyan or magenta or yellow or key) > 100 or (cyan or magenta or yellow or key) < 0: raise BadArgument( - message=f"CMYK values can only be from 0 to 100. User input was: `{cyan, magenta, yellow, key}`" + message=f"CMYK values can only be from 0 to 100. User input was: `{cyan, magenta, yellow, key}`." ) r = int(255 * (1.0 - cyan / float(100)) * (1.0 - key / float(100))) g = int(255 * (1.0 - magenta / float(100)) * (1.0 - key / float(100))) -- cgit v1.2.3 From 1d1dff113de6c276d614a945789755d3c4f9c3ab Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Thu, 28 Oct 2021 19:40:09 -0400 Subject: fix: bug in cmyk and hsv --- bot/exts/utilities/color.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 1ad6df27..f594a0df 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -85,7 +85,8 @@ class Colour(commands.Cog): raise BadArgument( message=f"HSV values can only be from 0 to 250. User input was: `{hue, saturation, value}`." ) - hsv_tuple = ImageColor.getrgb(f"hsv({hue}, {saturation}%, {value}%)") + # ImageColor HSV expects S and V to be swapped + hsv_tuple = ImageColor.getrgb(f"hsv({hue}, {value}%, {saturation}%)") await self.send_colour_response(ctx, hsv_tuple) @colour.command() @@ -99,15 +100,15 @@ class Colour(commands.Cog): await self.send_colour_response(ctx, hsl_tuple) @colour.command() - async def cmyk(self, ctx: commands.Context, cyan: int, yellow: int, magenta: int, key: int) -> None: + async def cmyk(self, ctx: commands.Context, cyan: int, magenta: int, yellow: int, key: int) -> None: """Command to create an embed from a CMYK input.""" if (cyan or magenta or yellow or key) > 100 or (cyan or magenta or yellow or key) < 0: raise BadArgument( message=f"CMYK values can only be from 0 to 100. User input was: `{cyan, magenta, yellow, key}`." ) - r = int(255 * (1.0 - cyan / float(100)) * (1.0 - key / float(100))) - g = int(255 * (1.0 - magenta / float(100)) * (1.0 - key / float(100))) - b = int(255 * (1.0 - yellow / float(100)) * (1.0 - key / float(100))) + r = round(255 * (1 - (cyan / 100)) * (1 - (key / 100))) + g = round(255 * (1 - (magenta / 100)) * (1 - (key / 100))) + b = round(255 * (1 - (yellow / 100)) * (1 - (key / 100))) await self.send_colour_response(ctx, (r, g, b)) @colour.command() @@ -165,11 +166,12 @@ class Colour(commands.Cog): rgb_list = [val / 255.0 for val in rgb] if not any(rgb_list): return 0, 0, 0, 100 - cmy = [1 - val / 255 for val in rgb_list] - min_cmy = min(cmy) - cmyk = [(val - min_cmy) / (1 - min_cmy) for val in cmy] + [min_cmy] - cmyk = [round(val * 100) for val in cmyk] - return tuple(cmyk) + k = 1 - max(val for val in rgb_list) + c = round((1 - rgb_list[0] - k) * 100 / (1 - k)) + m = round((1 - rgb_list[1] - k) * 100 / (1 - k)) + y = round((1 - rgb_list[2] - k) * 100 / (1 - k)) + cmyk = (c, m, y, round(k * 100)) + return cmyk @staticmethod def _rgb_to_hex(rgb: tuple[int, int, int]) -> str: -- cgit v1.2.3 From 36d8f996575d99e08256b0336ae75e9e491c381a Mon Sep 17 00:00:00 2001 From: Hedy Li Date: Fri, 29 Oct 2021 02:45:48 +0000 Subject: Candy Game: Ignore reactions to bot messages when adding candies Check whether a reaction is for a bot message when adding candies upon reactions. Previously you could use bot's reaction buttons which would trigger `on_reaction_add` and have a high chance of getting candies (or skulls). It can easily be abused to spam reactions, which apparently doesn't trigger an auto-mute like spamming messages do, AFAIK. In any case, I don't really feel good about reactions triggering candies. Despite this fix, the game *can* still be abused (but I won't tell you how). Though this occuring by accident is less likely than before. Either figure it out yourself or don't try to cheat :P This patch can be tested using the `.snake antidote` game when you react to the recipe buttons. Using `.help` works too but it produces a lot of noise in the logs. Tic tac toe may be helpful as well. Anyway, you could just react to bot messages yourself. --- bot/exts/holidays/halloween/candy_collection.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bot/exts/holidays/halloween/candy_collection.py b/bot/exts/holidays/halloween/candy_collection.py index 079d900d..bb9c93be 100644 --- a/bot/exts/holidays/halloween/candy_collection.py +++ b/bot/exts/holidays/halloween/candy_collection.py @@ -83,6 +83,11 @@ class CandyCollection(commands.Cog): # if its not a candy or skull, and it is one of 10 most recent messages, # proceed to add a skull/candy with higher chance if str(reaction.emoji) not in (EMOJIS["SKULL"], EMOJIS["CANDY"]): + # Ensure the reaction is not for a bot's message so users can't spam + # reaction buttons like in .help to get candies. + if message.author.bot: + return + recent_message_ids = map( lambda m: m.id, await self.hacktober_channel.history(limit=10).flatten() -- cgit v1.2.3 From 3ba2dfdfe51b543ac427b1dd50a5b3dcd73e8afc Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Fri, 29 Oct 2021 06:37:56 -0400 Subject: fix: correct ranges and logic for color error handling --- bot/exts/utilities/color.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index f594a0df..587d4e18 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -33,7 +33,7 @@ class Colour(commands.Cog): colour_or_color = ctx.invoked_parents[0] except IndexError: colour_or_color = "colour" - input_colour = ctx.args[2:] + input_colour = ctx.args[2:][0] if colour_mode not in ("name", "hex", "random"): colour_mode = colour_mode.upper() else: @@ -71,9 +71,9 @@ class Colour(commands.Cog): @colour.command() async def rgb(self, ctx: commands.Context, red: int, green: int, blue: int) -> None: """Command to create an embed from an RGB input.""" - if (red or green or blue) > 250 or (red or green or blue) < 0: + if any(c not in range(0, 256) for c in (red, green, blue)): raise BadArgument( - message=f"RGB values can only be from 0 to 250. User input was: `{red, green, blue}`." + message=f"RGB values can only be from 0 to 255. User input was: `{red, green, blue}`." ) rgb_tuple = (red, green, blue) await self.send_colour_response(ctx, rgb_tuple) @@ -81,9 +81,10 @@ class Colour(commands.Cog): @colour.command() async def hsv(self, ctx: commands.Context, hue: int, saturation: int, value: int) -> None: """Command to create an embed from an HSV input.""" - if (hue or saturation or value) > 250 or (hue or saturation or value) < 0: + if (hue not in range(0, 361)) or any(c not in range(0, 101) for c in (saturation, value)): raise BadArgument( - message=f"HSV values can only be from 0 to 250. User input was: `{hue, saturation, value}`." + message="Hue can only be from 0 to 360. Saturation and Value can only be from 0 to 100. " + f"User input was: `{hue, saturation, value}`." ) # ImageColor HSV expects S and V to be swapped hsv_tuple = ImageColor.getrgb(f"hsv({hue}, {value}%, {saturation}%)") @@ -92,9 +93,10 @@ class Colour(commands.Cog): @colour.command() async def hsl(self, ctx: commands.Context, hue: int, saturation: int, lightness: int) -> None: """Command to create an embed from an HSL input.""" - if (hue or saturation or lightness) > 360 or (hue or saturation or lightness) < 0: + if (hue not in range(0, 361)) or any(c not in range(0, 101) for c in (saturation, lightness)): raise BadArgument( - message=f"HSL values can only be from 0 to 360. User input was: `{hue, saturation, lightness}`." + message="Hue can only be from 0 to 360. Saturation and Lightness can only be from 0 to 100. " + f"User input was: `{hue, saturation, lightness}`." ) hsl_tuple = ImageColor.getrgb(f"hsl({hue}, {saturation}%, {lightness}%)") await self.send_colour_response(ctx, hsl_tuple) @@ -102,7 +104,7 @@ class Colour(commands.Cog): @colour.command() async def cmyk(self, ctx: commands.Context, cyan: int, magenta: int, yellow: int, key: int) -> None: """Command to create an embed from a CMYK input.""" - if (cyan or magenta or yellow or key) > 100 or (cyan or magenta or yellow or key) < 0: + if any(c not in range(0, 101) for c in (cyan or magenta or yellow or key)): raise BadArgument( message=f"CMYK values can only be from 0 to 100. User input was: `{cyan, magenta, yellow, key}`." ) @@ -191,7 +193,7 @@ class Colour(commands.Cog): ) colour_name = [name for name, hex_code in self.colour_mapping.items() if hex_code == match][0] except TypeError: - colour_name = None + colour_name = "No match found" return colour_name def match_colour_name(self, input_colour_name: str) -> str: @@ -204,7 +206,6 @@ class Colour(commands.Cog): ) hex_match = f"#{self.colour_mapping[match]}" except (ValueError, TypeError): - hex_match = None raise BadArgument(message=f"No color found for: `{input_colour_name}`") return hex_match -- cgit v1.2.3 From 930c9649eaec79cebfa2969501b4614ccd26d069 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Fri, 29 Oct 2021 07:03:59 -0400 Subject: chore: add some conditions for the embed variables --- bot/exts/utilities/color.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 587d4e18..a21b74e4 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -28,16 +28,25 @@ class Colour(commands.Cog): """Create and send embed from user given colour information.""" r, g, b = rgb name = self._rgb_to_name(rgb) - colour_mode = ctx.invoked_with + if name == "No match found": + name = None + try: colour_or_color = ctx.invoked_parents[0] except IndexError: colour_or_color = "colour" - input_colour = ctx.args[2:][0] + + colour_mode = ctx.invoked_with + if colour_mode == "random": + input_colour = "random" + else: + input_colour = ctx.args[2:][0] + if colour_mode not in ("name", "hex", "random"): colour_mode = colour_mode.upper() else: colour_mode = colour_mode.title() + desc = f"{colour_mode} information for `{name or input_colour}`." colour_embed = Embed( title=colour_or_color.title(), -- cgit v1.2.3 From 409434dbf9c14fb47d20c1329f86a3863f19a2be Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Fri, 29 Oct 2021 15:49:57 -0400 Subject: fix: iterate through tuple Co-authored-by: Sn4u <35849006+Sn4u@users.noreply.github.com> --- bot/exts/utilities/color.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index a21b74e4..035f7d6a 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -113,7 +113,7 @@ class Colour(commands.Cog): @colour.command() async def cmyk(self, ctx: commands.Context, cyan: int, magenta: int, yellow: int, key: int) -> None: """Command to create an embed from a CMYK input.""" - if any(c not in range(0, 101) for c in (cyan or magenta or yellow or key)): + if any(c not in range(0, 101) for c in (cyan, magenta, yellow, key)): raise BadArgument( message=f"CMYK values can only be from 0 to 100. User input was: `{cyan, magenta, yellow, key}`." ) -- cgit v1.2.3 From 88089140dab024b43428b31ef7c729219ff19b4a Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Fri, 29 Oct 2021 21:00:35 -0400 Subject: fix: invoke without command, hsv fix --- bot/exts/utilities/color.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 035f7d6a..5f5708b2 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -39,10 +39,12 @@ class Colour(commands.Cog): colour_mode = ctx.invoked_with if colour_mode == "random": input_colour = "random" + elif colour_mode in ("colour", "color"): + input_colour = rgb else: input_colour = ctx.args[2:][0] - if colour_mode not in ("name", "hex", "random"): + if colour_mode not in ("name", "hex", "random", "color", "colour"): colour_mode = colour_mode.upper() else: colour_mode = colour_mode.title() @@ -71,11 +73,15 @@ class Colour(commands.Cog): await ctx.send(file=thumbnail_file, embed=colour_embed) - @commands.group(aliases=("color",)) - async def colour(self, ctx: commands.Context) -> None: + @commands.group(aliases=("color",), invoke_without_command=True) + async def colour(self, ctx: commands.Context, *, extra: str) -> None: """User initiated command to create an embed that displays colour information.""" if ctx.invoked_subcommand is None: - await invoke_help_command(ctx) + try: + extra_colour = ImageColor.getrgb(extra) + await self.send_colour_response(ctx, extra_colour) + except ValueError: + await invoke_help_command(ctx) @colour.command() async def rgb(self, ctx: commands.Context, red: int, green: int, blue: int) -> None: @@ -95,8 +101,7 @@ class Colour(commands.Cog): message="Hue can only be from 0 to 360. Saturation and Value can only be from 0 to 100. " f"User input was: `{hue, saturation, value}`." ) - # ImageColor HSV expects S and V to be swapped - hsv_tuple = ImageColor.getrgb(f"hsv({hue}, {value}%, {saturation}%)") + hsv_tuple = ImageColor.getrgb(f"hsv({hue}, {saturation}%, {value}%)") await self.send_colour_response(ctx, hsv_tuple) @colour.command() @@ -158,8 +163,8 @@ class Colour(commands.Cog): @staticmethod def _rgb_to_hsv(rgb: tuple[int, int, int]) -> tuple[int, int, int]: """Convert RGB values to HSV values.""" - rgb_list = [val / 255.0 for val in rgb] - h, v, s = colorsys.rgb_to_hsv(*rgb_list) + rgb_list = [val / 255 for val in rgb] + h, s, v = colorsys.rgb_to_hsv(*rgb_list) hsv = (round(h * 360), round(s * 100), round(v * 100)) return hsv -- cgit v1.2.3 From eac9d3b4194004894d4f4e50862f184db695d402 Mon Sep 17 00:00:00 2001 From: CyberCitizen01 Date: Mon, 1 Nov 2021 20:17:15 +0530 Subject: Add source credit and Color.from_rgb method to get discord color https://github.com/python-discord/sir-lancebot/pull/842/files#r739619938 https://github.com/python-discord/sir-lancebot/pull/842/files#r739295253 --- bot/exts/utilities/color.py | 6 +++--- bot/resources/utilities/ryanzec_colours.json | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 5f5708b2..563376fe 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -5,7 +5,7 @@ import random from io import BytesIO from PIL import Image, ImageColor -from discord import Embed, File +from discord import Embed, File, Color from discord.ext import commands from discord.ext.commands.errors import BadArgument from rapidfuzz import process @@ -23,10 +23,10 @@ class Colour(commands.Cog): self.bot = bot with open(pathlib.Path("bot/resources/utilities/ryanzec_colours.json")) as f: self.colour_mapping = json.load(f) + del self.colour_mapping['_'] # Delete source credit entry async def send_colour_response(self, ctx: commands.Context, rgb: tuple[int, int, int]) -> None: """Create and send embed from user given colour information.""" - r, g, b = rgb name = self._rgb_to_name(rgb) if name == "No match found": name = None @@ -53,7 +53,7 @@ class Colour(commands.Cog): colour_embed = Embed( title=colour_or_color.title(), description=desc, - colour=int(f"{r:02x}{g:02x}{b:02x}", 16) + colour= Color.from_rgb(*rgb) ) colour_conversions = self.get_colour_conversions(rgb) for colour_space, value in colour_conversions.items(): diff --git a/bot/resources/utilities/ryanzec_colours.json b/bot/resources/utilities/ryanzec_colours.json index 7b89f052..ad8f05fd 100644 --- a/bot/resources/utilities/ryanzec_colours.json +++ b/bot/resources/utilities/ryanzec_colours.json @@ -1,4 +1,5 @@ { + "_": "Source: https://github.com/ryanzec/name-that-color/blob/0bb5ec7f37e4f6e7f2c164f39f7f08cca7c8e621/lib/ntc.js#L116-L1681", "Abbey": "4C4F56", "Acadia": "1B1404", "Acapulco": "7CB0A1", -- cgit v1.2.3 From 15a346bc726f6ca9075f6692b6deb2f486cc66aa Mon Sep 17 00:00:00 2001 From: CyberCitizen01 Date: Mon, 1 Nov 2021 20:23:09 +0530 Subject: Fix lint errors of eac9d3b4194004894d4f4e50862f184db695d402 --- bot/exts/utilities/color.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 563376fe..c79e7620 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -5,7 +5,7 @@ import random from io import BytesIO from PIL import Image, ImageColor -from discord import Embed, File, Color +from discord import Color, Embed, File from discord.ext import commands from discord.ext.commands.errors import BadArgument from rapidfuzz import process @@ -23,7 +23,7 @@ class Colour(commands.Cog): self.bot = bot with open(pathlib.Path("bot/resources/utilities/ryanzec_colours.json")) as f: self.colour_mapping = json.load(f) - del self.colour_mapping['_'] # Delete source credit entry + del self.colour_mapping['_'] # Delete source credit entry async def send_colour_response(self, ctx: commands.Context, rgb: tuple[int, int, int]) -> None: """Create and send embed from user given colour information.""" @@ -53,7 +53,7 @@ class Colour(commands.Cog): colour_embed = Embed( title=colour_or_color.title(), description=desc, - colour= Color.from_rgb(*rgb) + colour=Color.from_rgb(*rgb) ) colour_conversions = self.get_colour_conversions(rgb) for colour_space, value in colour_conversions.items(): -- cgit v1.2.3 From fedd30b0aa9afdddb33b20630096b67370b3ac89 Mon Sep 17 00:00:00 2001 From: SavagePastaMan <69145546+SavagePastaMan@users.noreply.github.com> Date: Mon, 1 Nov 2021 20:38:42 -0400 Subject: Remove the "what's your skill level topic" --- bot/resources/utilities/py_topics.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/resources/utilities/py_topics.yaml b/bot/resources/utilities/py_topics.yaml index a3fb2ccc..1cd2c325 100644 --- a/bot/resources/utilities/py_topics.yaml +++ b/bot/resources/utilities/py_topics.yaml @@ -33,7 +33,6 @@ - How often do you program in Python? - How would you learn a new library if needed to do so? - Have you ever worked with a microcontroller or anything physical with Python before? - - How good would you say you are at Python so far? Beginner, intermediate, or advanced? - Have you ever tried making your own programming language? - Has a recently discovered Python module changed your general use of Python? -- cgit v1.2.3 From c869faf32ceeadc94a8a38b5370706a4b6da32ae Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Wed, 3 Nov 2021 20:40:15 -0400 Subject: fix: return None if no name match is found --- bot/exts/utilities/color.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index c79e7620..9c801413 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -28,8 +28,8 @@ class Colour(commands.Cog): async def send_colour_response(self, ctx: commands.Context, rgb: tuple[int, int, int]) -> None: """Create and send embed from user given colour information.""" name = self._rgb_to_name(rgb) - if name == "No match found": - name = None + if name is None: + name = "No match found" try: colour_or_color = ctx.invoked_parents[0] @@ -207,7 +207,7 @@ class Colour(commands.Cog): ) colour_name = [name for name, hex_code in self.colour_mapping.items() if hex_code == match][0] except TypeError: - colour_name = "No match found" + colour_name = None return colour_name def match_colour_name(self, input_colour_name: str) -> str: -- cgit v1.2.3 From 6869fa330ea67bced27df68d3b730e5fdbdb7aa4 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Wed, 3 Nov 2021 21:01:36 -0400 Subject: fix: use and handle conversions with name value --- bot/exts/utilities/color.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 9c801413..56036cd0 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -151,13 +151,16 @@ class Colour(commands.Cog): def get_colour_conversions(self, rgb: tuple[int, int, int]) -> dict[str, str]: """Create a dictionary mapping of colour types and their values.""" + colour_name = self._rgb_to_name(rgb) + if colour_name is None: + colour_name = "No match found" return { "RGB": rgb, "HSV": self._rgb_to_hsv(rgb), "HSL": self._rgb_to_hsl(rgb), "CMYK": self._rgb_to_cmyk(rgb), "Hex": self._rgb_to_hex(rgb), - "Name": self._rgb_to_name(rgb) + "Name": colour_name } @staticmethod @@ -182,7 +185,7 @@ class Colour(commands.Cog): rgb_list = [val / 255.0 for val in rgb] if not any(rgb_list): return 0, 0, 0, 100 - k = 1 - max(val for val in rgb_list) + k = 1 - max(rgb_list) c = round((1 - rgb_list[0] - k) * 100 / (1 - k)) m = round((1 - rgb_list[1] - k) * 100 / (1 - k)) y = round((1 - rgb_list[2] - k) * 100 / (1 - k)) -- cgit v1.2.3 From 3280188ecb025c64597484b145aff9c19916a4e6 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Wed, 3 Nov 2021 21:02:20 -0400 Subject: fix: have description in-line --- bot/exts/utilities/color.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 56036cd0..2e18884c 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -49,10 +49,9 @@ class Colour(commands.Cog): else: colour_mode = colour_mode.title() - desc = f"{colour_mode} information for `{name or input_colour}`." colour_embed = Embed( title=colour_or_color.title(), - description=desc, + description=f"{colour_mode} information for `{name or input_colour}`.", colour=Color.from_rgb(*rgb) ) colour_conversions = self.get_colour_conversions(rgb) -- cgit v1.2.3 From a66cdd48784baf948e294b10f7f81746c0c4452d Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Wed, 3 Nov 2021 21:08:50 -0400 Subject: fix: reduce 'from' imports --- bot/exts/utilities/color.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py index 2e18884c..5ff1e2b8 100644 --- a/bot/exts/utilities/color.py +++ b/bot/exts/utilities/color.py @@ -4,11 +4,10 @@ import pathlib import random from io import BytesIO +import discord +import rapidfuzz from PIL import Image, ImageColor -from discord import Color, Embed, File from discord.ext import commands -from discord.ext.commands.errors import BadArgument -from rapidfuzz import process from bot.bot import Bot from bot.exts.core.extensions import invoke_help_command @@ -49,10 +48,10 @@ class Colour(commands.Cog): else: colour_mode = colour_mode.title() - colour_embed = Embed( + colour_embed = discord.Embed( title=colour_or_color.title(), description=f"{colour_mode} information for `{name or input_colour}`.", - colour=Color.from_rgb(*rgb) + colour=discord.Color.from_rgb(*rgb) ) colour_conversions = self.get_colour_conversions(rgb) for colour_space, value in colour_conversions.items(): @@ -66,7 +65,7 @@ class Colour(commands.Cog): buffer = BytesIO() thumbnail.save(buffer, "PNG") buffer.seek(0) - thumbnail_file = File(buffer, filename="colour.png") + thumbnail_file = discord.File(buffer, filename="colour.png") colour_embed.set_thumbnail(url="attachment://colour.png") @@ -86,7 +85,7 @@ class Colour(commands.Cog): async def rgb(self, ctx: commands.Context, red: int, green: int, blue: int) -> None: """Command to create an embed from an RGB input.""" if any(c not in range(0, 256) for c in (red, green, blue)): - raise BadArgument( + raise commands.BadArgument( message=f"RGB values can only be from 0 to 255. User input was: `{red, green, blue}`." ) rgb_tuple = (red, green, blue) @@ -96,7 +95,7 @@ class Colour(commands.Cog): async def hsv(self, ctx: commands.Context, hue: int, saturation: int, value: int) -> None: """Command to create an embed from an HSV input.""" if (hue not in range(0, 361)) or any(c not in range(0, 101) for c in (saturation, value)): - raise BadArgument( + raise commands.BadArgument( message="Hue can only be from 0 to 360. Saturation and Value can only be from 0 to 100. " f"User input was: `{hue, saturation, value}`." ) @@ -107,7 +106,7 @@ class Colour(commands.Cog): async def hsl(self, ctx: commands.Context, hue: int, saturation: int, lightness: int) -> None: """Command to create an embed from an HSL input.""" if (hue not in range(0, 361)) or any(c not in range(0, 101) for c in (saturation, lightness)): - raise BadArgument( + raise commands.BadArgument( message="Hue can only be from 0 to 360. Saturation and Lightness can only be from 0 to 100. " f"User input was: `{hue, saturation, lightness}`." ) @@ -118,7 +117,7 @@ class Colour(commands.Cog): async def cmyk(self, ctx: commands.Context, cyan: int, magenta: int, yellow: int, key: int) -> None: """Command to create an embed from a CMYK input.""" if any(c not in range(0, 101) for c in (cyan, magenta, yellow, key)): - raise BadArgument( + raise commands.BadArgument( message=f"CMYK values can only be from 0 to 100. User input was: `{cyan, magenta, yellow, key}`." ) r = round(255 * (1 - (cyan / 100)) * (1 - (key / 100))) @@ -202,7 +201,7 @@ class Colour(commands.Cog): """Convert RGB values to a fuzzy matched name.""" input_hex_colour = self._rgb_to_hex(rgb) try: - match, certainty, _ = process.extractOne( + match, certainty, _ = rapidfuzz.process.extractOne( query=input_hex_colour, choices=self.colour_mapping.values(), score_cutoff=80 @@ -215,14 +214,14 @@ class Colour(commands.Cog): def match_colour_name(self, input_colour_name: str) -> str: """Convert a colour name to HEX code.""" try: - match, certainty, _ = process.extractOne( + match, certainty, _ = rapidfuzz.process.extractOne( query=input_colour_name, choices=self.colour_mapping.keys(), score_cutoff=80 ) hex_match = f"#{self.colour_mapping[match]}" except (ValueError, TypeError): - raise BadArgument(message=f"No color found for: `{input_colour_name}`") + raise commands.BadArgument(message=f"No color found for: `{input_colour_name}`") return hex_match -- cgit v1.2.3 From edeb48ab0489c0ac0fcbf0a3a45dde11236f613c Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Wed, 3 Nov 2021 21:13:24 -0400 Subject: rename to colour --- bot/exts/utilities/color.py | 230 ------------------------------------------- bot/exts/utilities/colour.py | 230 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 230 insertions(+), 230 deletions(-) delete mode 100644 bot/exts/utilities/color.py create mode 100644 bot/exts/utilities/colour.py diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py deleted file mode 100644 index 5ff1e2b8..00000000 --- a/bot/exts/utilities/color.py +++ /dev/null @@ -1,230 +0,0 @@ -import colorsys -import json -import pathlib -import random -from io import BytesIO - -import discord -import rapidfuzz -from PIL import Image, ImageColor -from discord.ext import commands - -from bot.bot import Bot -from bot.exts.core.extensions import invoke_help_command - -THUMBNAIL_SIZE = (80, 80) - - -class Colour(commands.Cog): - """Cog for the Colour command.""" - - def __init__(self, bot: Bot): - self.bot = bot - with open(pathlib.Path("bot/resources/utilities/ryanzec_colours.json")) as f: - self.colour_mapping = json.load(f) - del self.colour_mapping['_'] # Delete source credit entry - - async def send_colour_response(self, ctx: commands.Context, rgb: tuple[int, int, int]) -> None: - """Create and send embed from user given colour information.""" - name = self._rgb_to_name(rgb) - if name is None: - name = "No match found" - - try: - colour_or_color = ctx.invoked_parents[0] - except IndexError: - colour_or_color = "colour" - - colour_mode = ctx.invoked_with - if colour_mode == "random": - input_colour = "random" - elif colour_mode in ("colour", "color"): - input_colour = rgb - else: - input_colour = ctx.args[2:][0] - - if colour_mode not in ("name", "hex", "random", "color", "colour"): - colour_mode = colour_mode.upper() - else: - colour_mode = colour_mode.title() - - colour_embed = discord.Embed( - title=colour_or_color.title(), - description=f"{colour_mode} information for `{name or input_colour}`.", - colour=discord.Color.from_rgb(*rgb) - ) - colour_conversions = self.get_colour_conversions(rgb) - for colour_space, value in colour_conversions.items(): - colour_embed.add_field( - name=colour_space, - value=f"`{value}`", - inline=True - ) - - thumbnail = Image.new("RGB", THUMBNAIL_SIZE, color=rgb) - buffer = BytesIO() - thumbnail.save(buffer, "PNG") - buffer.seek(0) - thumbnail_file = discord.File(buffer, filename="colour.png") - - colour_embed.set_thumbnail(url="attachment://colour.png") - - await ctx.send(file=thumbnail_file, embed=colour_embed) - - @commands.group(aliases=("color",), invoke_without_command=True) - async def colour(self, ctx: commands.Context, *, extra: str) -> None: - """User initiated command to create an embed that displays colour information.""" - if ctx.invoked_subcommand is None: - try: - extra_colour = ImageColor.getrgb(extra) - await self.send_colour_response(ctx, extra_colour) - except ValueError: - await invoke_help_command(ctx) - - @colour.command() - async def rgb(self, ctx: commands.Context, red: int, green: int, blue: int) -> None: - """Command to create an embed from an RGB input.""" - if any(c not in range(0, 256) for c in (red, green, blue)): - raise commands.BadArgument( - message=f"RGB values can only be from 0 to 255. User input was: `{red, green, blue}`." - ) - rgb_tuple = (red, green, blue) - await self.send_colour_response(ctx, rgb_tuple) - - @colour.command() - async def hsv(self, ctx: commands.Context, hue: int, saturation: int, value: int) -> None: - """Command to create an embed from an HSV input.""" - if (hue not in range(0, 361)) or any(c not in range(0, 101) for c in (saturation, value)): - raise commands.BadArgument( - message="Hue can only be from 0 to 360. Saturation and Value can only be from 0 to 100. " - f"User input was: `{hue, saturation, value}`." - ) - hsv_tuple = ImageColor.getrgb(f"hsv({hue}, {saturation}%, {value}%)") - await self.send_colour_response(ctx, hsv_tuple) - - @colour.command() - async def hsl(self, ctx: commands.Context, hue: int, saturation: int, lightness: int) -> None: - """Command to create an embed from an HSL input.""" - if (hue not in range(0, 361)) or any(c not in range(0, 101) for c in (saturation, lightness)): - raise commands.BadArgument( - message="Hue can only be from 0 to 360. Saturation and Lightness can only be from 0 to 100. " - f"User input was: `{hue, saturation, lightness}`." - ) - hsl_tuple = ImageColor.getrgb(f"hsl({hue}, {saturation}%, {lightness}%)") - await self.send_colour_response(ctx, hsl_tuple) - - @colour.command() - async def cmyk(self, ctx: commands.Context, cyan: int, magenta: int, yellow: int, key: int) -> None: - """Command to create an embed from a CMYK input.""" - if any(c not in range(0, 101) for c in (cyan, magenta, yellow, key)): - raise commands.BadArgument( - message=f"CMYK values can only be from 0 to 100. User input was: `{cyan, magenta, yellow, key}`." - ) - r = round(255 * (1 - (cyan / 100)) * (1 - (key / 100))) - g = round(255 * (1 - (magenta / 100)) * (1 - (key / 100))) - b = round(255 * (1 - (yellow / 100)) * (1 - (key / 100))) - await self.send_colour_response(ctx, (r, g, b)) - - @colour.command() - async def hex(self, ctx: commands.Context, hex_code: str) -> None: - """Command to create an embed from a HEX input.""" - if "#" not in hex_code: - hex_code = f"#{hex_code}" - hex_tuple = ImageColor.getrgb(hex_code) - await self.send_colour_response(ctx, hex_tuple) - - @colour.command() - async def name(self, ctx: commands.Context, user_colour_name: str) -> None: - """Command to create an embed from a name input.""" - hex_colour = self.match_colour_name(user_colour_name) - hex_tuple = ImageColor.getrgb(hex_colour) - await self.send_colour_response(ctx, hex_tuple) - - @colour.command() - async def random(self, ctx: commands.Context) -> None: - """Command to create an embed from a randomly chosen colour from the reference file.""" - hex_colour = random.choice(list(self.colour_mapping.values())) - hex_tuple = ImageColor.getrgb(f"#{hex_colour}") - await self.send_colour_response(ctx, hex_tuple) - - def get_colour_conversions(self, rgb: tuple[int, int, int]) -> dict[str, str]: - """Create a dictionary mapping of colour types and their values.""" - colour_name = self._rgb_to_name(rgb) - if colour_name is None: - colour_name = "No match found" - return { - "RGB": rgb, - "HSV": self._rgb_to_hsv(rgb), - "HSL": self._rgb_to_hsl(rgb), - "CMYK": self._rgb_to_cmyk(rgb), - "Hex": self._rgb_to_hex(rgb), - "Name": colour_name - } - - @staticmethod - def _rgb_to_hsv(rgb: tuple[int, int, int]) -> tuple[int, int, int]: - """Convert RGB values to HSV values.""" - rgb_list = [val / 255 for val in rgb] - h, s, v = colorsys.rgb_to_hsv(*rgb_list) - hsv = (round(h * 360), round(s * 100), round(v * 100)) - return hsv - - @staticmethod - def _rgb_to_hsl(rgb: tuple[int, int, int]) -> tuple[int, int, int]: - """Convert RGB values to HSL values.""" - rgb_list = [val / 255.0 for val in rgb] - h, l, s = colorsys.rgb_to_hls(*rgb_list) - hsl = (round(h * 360), round(s * 100), round(l * 100)) - return hsl - - @staticmethod - def _rgb_to_cmyk(rgb: tuple[int, int, int, int]) -> tuple[int, int, int, int]: - """Convert RGB values to CMYK values.""" - rgb_list = [val / 255.0 for val in rgb] - if not any(rgb_list): - return 0, 0, 0, 100 - k = 1 - max(rgb_list) - c = round((1 - rgb_list[0] - k) * 100 / (1 - k)) - m = round((1 - rgb_list[1] - k) * 100 / (1 - k)) - y = round((1 - rgb_list[2] - k) * 100 / (1 - k)) - cmyk = (c, m, y, round(k * 100)) - return cmyk - - @staticmethod - def _rgb_to_hex(rgb: tuple[int, int, int]) -> str: - """Convert RGB values to HEX code.""" - hex_ = ''.join([hex(val)[2:].zfill(2) for val in rgb]) - hex_code = f"#{hex_}".upper() - return hex_code - - def _rgb_to_name(self, rgb: tuple[int, int, int]) -> str: - """Convert RGB values to a fuzzy matched name.""" - input_hex_colour = self._rgb_to_hex(rgb) - try: - match, certainty, _ = rapidfuzz.process.extractOne( - query=input_hex_colour, - choices=self.colour_mapping.values(), - score_cutoff=80 - ) - colour_name = [name for name, hex_code in self.colour_mapping.items() if hex_code == match][0] - except TypeError: - colour_name = None - return colour_name - - def match_colour_name(self, input_colour_name: str) -> str: - """Convert a colour name to HEX code.""" - try: - match, certainty, _ = rapidfuzz.process.extractOne( - query=input_colour_name, - choices=self.colour_mapping.keys(), - score_cutoff=80 - ) - hex_match = f"#{self.colour_mapping[match]}" - except (ValueError, TypeError): - raise commands.BadArgument(message=f"No color found for: `{input_colour_name}`") - return hex_match - - -def setup(bot: Bot) -> None: - """Load the Colour cog.""" - bot.add_cog(Colour(bot)) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py new file mode 100644 index 00000000..5ff1e2b8 --- /dev/null +++ b/bot/exts/utilities/colour.py @@ -0,0 +1,230 @@ +import colorsys +import json +import pathlib +import random +from io import BytesIO + +import discord +import rapidfuzz +from PIL import Image, ImageColor +from discord.ext import commands + +from bot.bot import Bot +from bot.exts.core.extensions import invoke_help_command + +THUMBNAIL_SIZE = (80, 80) + + +class Colour(commands.Cog): + """Cog for the Colour command.""" + + def __init__(self, bot: Bot): + self.bot = bot + with open(pathlib.Path("bot/resources/utilities/ryanzec_colours.json")) as f: + self.colour_mapping = json.load(f) + del self.colour_mapping['_'] # Delete source credit entry + + async def send_colour_response(self, ctx: commands.Context, rgb: tuple[int, int, int]) -> None: + """Create and send embed from user given colour information.""" + name = self._rgb_to_name(rgb) + if name is None: + name = "No match found" + + try: + colour_or_color = ctx.invoked_parents[0] + except IndexError: + colour_or_color = "colour" + + colour_mode = ctx.invoked_with + if colour_mode == "random": + input_colour = "random" + elif colour_mode in ("colour", "color"): + input_colour = rgb + else: + input_colour = ctx.args[2:][0] + + if colour_mode not in ("name", "hex", "random", "color", "colour"): + colour_mode = colour_mode.upper() + else: + colour_mode = colour_mode.title() + + colour_embed = discord.Embed( + title=colour_or_color.title(), + description=f"{colour_mode} information for `{name or input_colour}`.", + colour=discord.Color.from_rgb(*rgb) + ) + colour_conversions = self.get_colour_conversions(rgb) + for colour_space, value in colour_conversions.items(): + colour_embed.add_field( + name=colour_space, + value=f"`{value}`", + inline=True + ) + + thumbnail = Image.new("RGB", THUMBNAIL_SIZE, color=rgb) + buffer = BytesIO() + thumbnail.save(buffer, "PNG") + buffer.seek(0) + thumbnail_file = discord.File(buffer, filename="colour.png") + + colour_embed.set_thumbnail(url="attachment://colour.png") + + await ctx.send(file=thumbnail_file, embed=colour_embed) + + @commands.group(aliases=("color",), invoke_without_command=True) + async def colour(self, ctx: commands.Context, *, extra: str) -> None: + """User initiated command to create an embed that displays colour information.""" + if ctx.invoked_subcommand is None: + try: + extra_colour = ImageColor.getrgb(extra) + await self.send_colour_response(ctx, extra_colour) + except ValueError: + await invoke_help_command(ctx) + + @colour.command() + async def rgb(self, ctx: commands.Context, red: int, green: int, blue: int) -> None: + """Command to create an embed from an RGB input.""" + if any(c not in range(0, 256) for c in (red, green, blue)): + raise commands.BadArgument( + message=f"RGB values can only be from 0 to 255. User input was: `{red, green, blue}`." + ) + rgb_tuple = (red, green, blue) + await self.send_colour_response(ctx, rgb_tuple) + + @colour.command() + async def hsv(self, ctx: commands.Context, hue: int, saturation: int, value: int) -> None: + """Command to create an embed from an HSV input.""" + if (hue not in range(0, 361)) or any(c not in range(0, 101) for c in (saturation, value)): + raise commands.BadArgument( + message="Hue can only be from 0 to 360. Saturation and Value can only be from 0 to 100. " + f"User input was: `{hue, saturation, value}`." + ) + hsv_tuple = ImageColor.getrgb(f"hsv({hue}, {saturation}%, {value}%)") + await self.send_colour_response(ctx, hsv_tuple) + + @colour.command() + async def hsl(self, ctx: commands.Context, hue: int, saturation: int, lightness: int) -> None: + """Command to create an embed from an HSL input.""" + if (hue not in range(0, 361)) or any(c not in range(0, 101) for c in (saturation, lightness)): + raise commands.BadArgument( + message="Hue can only be from 0 to 360. Saturation and Lightness can only be from 0 to 100. " + f"User input was: `{hue, saturation, lightness}`." + ) + hsl_tuple = ImageColor.getrgb(f"hsl({hue}, {saturation}%, {lightness}%)") + await self.send_colour_response(ctx, hsl_tuple) + + @colour.command() + async def cmyk(self, ctx: commands.Context, cyan: int, magenta: int, yellow: int, key: int) -> None: + """Command to create an embed from a CMYK input.""" + if any(c not in range(0, 101) for c in (cyan, magenta, yellow, key)): + raise commands.BadArgument( + message=f"CMYK values can only be from 0 to 100. User input was: `{cyan, magenta, yellow, key}`." + ) + r = round(255 * (1 - (cyan / 100)) * (1 - (key / 100))) + g = round(255 * (1 - (magenta / 100)) * (1 - (key / 100))) + b = round(255 * (1 - (yellow / 100)) * (1 - (key / 100))) + await self.send_colour_response(ctx, (r, g, b)) + + @colour.command() + async def hex(self, ctx: commands.Context, hex_code: str) -> None: + """Command to create an embed from a HEX input.""" + if "#" not in hex_code: + hex_code = f"#{hex_code}" + hex_tuple = ImageColor.getrgb(hex_code) + await self.send_colour_response(ctx, hex_tuple) + + @colour.command() + async def name(self, ctx: commands.Context, user_colour_name: str) -> None: + """Command to create an embed from a name input.""" + hex_colour = self.match_colour_name(user_colour_name) + hex_tuple = ImageColor.getrgb(hex_colour) + await self.send_colour_response(ctx, hex_tuple) + + @colour.command() + async def random(self, ctx: commands.Context) -> None: + """Command to create an embed from a randomly chosen colour from the reference file.""" + hex_colour = random.choice(list(self.colour_mapping.values())) + hex_tuple = ImageColor.getrgb(f"#{hex_colour}") + await self.send_colour_response(ctx, hex_tuple) + + def get_colour_conversions(self, rgb: tuple[int, int, int]) -> dict[str, str]: + """Create a dictionary mapping of colour types and their values.""" + colour_name = self._rgb_to_name(rgb) + if colour_name is None: + colour_name = "No match found" + return { + "RGB": rgb, + "HSV": self._rgb_to_hsv(rgb), + "HSL": self._rgb_to_hsl(rgb), + "CMYK": self._rgb_to_cmyk(rgb), + "Hex": self._rgb_to_hex(rgb), + "Name": colour_name + } + + @staticmethod + def _rgb_to_hsv(rgb: tuple[int, int, int]) -> tuple[int, int, int]: + """Convert RGB values to HSV values.""" + rgb_list = [val / 255 for val in rgb] + h, s, v = colorsys.rgb_to_hsv(*rgb_list) + hsv = (round(h * 360), round(s * 100), round(v * 100)) + return hsv + + @staticmethod + def _rgb_to_hsl(rgb: tuple[int, int, int]) -> tuple[int, int, int]: + """Convert RGB values to HSL values.""" + rgb_list = [val / 255.0 for val in rgb] + h, l, s = colorsys.rgb_to_hls(*rgb_list) + hsl = (round(h * 360), round(s * 100), round(l * 100)) + return hsl + + @staticmethod + def _rgb_to_cmyk(rgb: tuple[int, int, int, int]) -> tuple[int, int, int, int]: + """Convert RGB values to CMYK values.""" + rgb_list = [val / 255.0 for val in rgb] + if not any(rgb_list): + return 0, 0, 0, 100 + k = 1 - max(rgb_list) + c = round((1 - rgb_list[0] - k) * 100 / (1 - k)) + m = round((1 - rgb_list[1] - k) * 100 / (1 - k)) + y = round((1 - rgb_list[2] - k) * 100 / (1 - k)) + cmyk = (c, m, y, round(k * 100)) + return cmyk + + @staticmethod + def _rgb_to_hex(rgb: tuple[int, int, int]) -> str: + """Convert RGB values to HEX code.""" + hex_ = ''.join([hex(val)[2:].zfill(2) for val in rgb]) + hex_code = f"#{hex_}".upper() + return hex_code + + def _rgb_to_name(self, rgb: tuple[int, int, int]) -> str: + """Convert RGB values to a fuzzy matched name.""" + input_hex_colour = self._rgb_to_hex(rgb) + try: + match, certainty, _ = rapidfuzz.process.extractOne( + query=input_hex_colour, + choices=self.colour_mapping.values(), + score_cutoff=80 + ) + colour_name = [name for name, hex_code in self.colour_mapping.items() if hex_code == match][0] + except TypeError: + colour_name = None + return colour_name + + def match_colour_name(self, input_colour_name: str) -> str: + """Convert a colour name to HEX code.""" + try: + match, certainty, _ = rapidfuzz.process.extractOne( + query=input_colour_name, + choices=self.colour_mapping.keys(), + score_cutoff=80 + ) + hex_match = f"#{self.colour_mapping[match]}" + except (ValueError, TypeError): + raise commands.BadArgument(message=f"No color found for: `{input_colour_name}`") + return hex_match + + +def setup(bot: Bot) -> None: + """Load the Colour cog.""" + bot.add_cog(Colour(bot)) -- cgit v1.2.3 From 2cd5aa83a722f4137e32514dfe9c45688be365ac Mon Sep 17 00:00:00 2001 From: NipaDev <60810623+Nipa-Code@users.noreply.github.com> Date: Fri, 5 Nov 2021 03:18:11 +0200 Subject: Add option to get specific amount of realpython articles Add a feature to choice in between 1-5 (inclusive) articles. If value not specified, the default, 5 will be used. Co-authored-by: ChrisJL --- bot/exts/utilities/realpython.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/bot/exts/utilities/realpython.py b/bot/exts/utilities/realpython.py index ef8b2638..bf8f1341 100644 --- a/bot/exts/utilities/realpython.py +++ b/bot/exts/utilities/realpython.py @@ -1,5 +1,6 @@ import logging from html import unescape +from typing import Optional from urllib.parse import quote_plus from discord import Embed @@ -31,9 +32,18 @@ class RealPython(commands.Cog): @commands.command(aliases=["rp"]) @commands.cooldown(1, 10, commands.cooldowns.BucketType.user) - async def realpython(self, ctx: commands.Context, *, user_search: str) -> None: - """Send 5 articles that match the user's search terms.""" - params = {"q": user_search, "limit": 5, "kind": "article"} + async def realpython(self, ctx: commands.Context, amount: Optional[int] = 5, *, user_search: str) -> None: + """ + Send some articles from RealPython that match the search terms. + + By default the top 5 matches are sent, this can be overwritten to + a number between 1 and 5 by specifying an amount before the search query. + """ + if not 1 <= amount <= 5: + await ctx.send("`amount` must be between 1 and 5 (inclusive).") + return + + params = {"q": user_search, "limit": amount, "kind": "article"} async with self.bot.http_session.get(url=API_ROOT, params=params) as response: if response.status != 200: logger.error( -- cgit v1.2.3 From 726109fc47eec6259e6d8f8291b330e3353809e4 Mon Sep 17 00:00:00 2001 From: NipaDev <60810623+Nipa-Code@users.noreply.github.com> Date: Fri, 5 Nov 2021 12:49:20 +0200 Subject: Limit user reactions on embed pagination * Limit user reactions on embed pagination Limit user reactions to prevent non-author from removing message by adding user restriction to paginator. * Fixed the format of code to single line. Co-authored-by: ChrisJL --- bot/exts/utilities/wikipedia.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bot/exts/utilities/wikipedia.py b/bot/exts/utilities/wikipedia.py index c5283de0..e5e8e289 100644 --- a/bot/exts/utilities/wikipedia.py +++ b/bot/exts/utilities/wikipedia.py @@ -86,9 +86,7 @@ class WikipediaSearch(commands.Cog): ) embed.set_thumbnail(url=WIKI_THUMBNAIL) embed.timestamp = datetime.utcnow() - await LinePaginator.paginate( - contents, ctx, embed - ) + await LinePaginator.paginate(contents, ctx, embed, restrict_to_user=ctx.author) else: await ctx.send( "Sorry, we could not find a wikipedia article using that search term." -- cgit v1.2.3 From a32fae52425fe6955be2be013f6149c90cc86e61 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sat, 6 Nov 2021 11:22:13 -0400 Subject: lowering challenges for compatibility with uppercase languages --- bot/exts/utilities/challenges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/challenges.py b/bot/exts/utilities/challenges.py index e4738455..1f296390 100644 --- a/bot/exts/utilities/challenges.py +++ b/bot/exts/utilities/challenges.py @@ -278,7 +278,7 @@ class Challenges(commands.Cog): if language.lower() not in SUPPORTED_LANGUAGES["stable"] + SUPPORTED_LANGUAGES["beta"]: raise commands.BadArgument("This is not a recognized language on codewars.com!") - get_kata_link = f"https://codewars.com/kata/search/{language}" + get_kata_link = f"https://codewars.com/kata/search/{language.lower()}" params = {} if query is not None: -- cgit v1.2.3 From 7227fe279574ee0626ad6c7ead754c1212669af2 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sat, 6 Nov 2021 23:02:26 -0400 Subject: Change language to language.lower() --- bot/exts/utilities/challenges.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/exts/utilities/challenges.py b/bot/exts/utilities/challenges.py index 1f296390..751ea2a3 100644 --- a/bot/exts/utilities/challenges.py +++ b/bot/exts/utilities/challenges.py @@ -275,10 +275,12 @@ class Challenges(commands.Cog): `.challenge , ` - Pulls a random challenge with the query provided, under that difficulty within the language's scope. """ - if language.lower() not in SUPPORTED_LANGUAGES["stable"] + SUPPORTED_LANGUAGES["beta"]: + + language = language.lower() + if language not in SUPPORTED_LANGUAGES["stable"] + SUPPORTED_LANGUAGES["beta"]: raise commands.BadArgument("This is not a recognized language on codewars.com!") - get_kata_link = f"https://codewars.com/kata/search/{language.lower()}" + get_kata_link = f"https://codewars.com/kata/search/{language}" params = {} if query is not None: -- cgit v1.2.3 From 65cfed425e19fa508c7d4e8736deb55c564cecbb Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Sat, 6 Nov 2021 23:07:31 -0400 Subject: Handle `.wtf` command without query (#939) Co-authored-by: Hedy Li Co-authored-by: Xithrius <15021300+Xithrius@users.noreply.github.com> --- bot/exts/utilities/wtf_python.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/bot/exts/utilities/wtf_python.py b/bot/exts/utilities/wtf_python.py index 66a022d7..980b3dba 100644 --- a/bot/exts/utilities/wtf_python.py +++ b/bot/exts/utilities/wtf_python.py @@ -79,7 +79,7 @@ class WTFPython(commands.Cog): return match if certainty > MINIMUM_CERTAINTY else None @commands.command(aliases=("wtf", "WTF")) - async def wtf_python(self, ctx: commands.Context, *, query: str) -> None: + async def wtf_python(self, ctx: commands.Context, *, query: Optional[str] = None) -> None: """ Search WTF Python repository. @@ -87,6 +87,18 @@ class WTFPython(commands.Cog): Usage: --> .wtf wild imports """ + if query is None: + no_query_embed = Embed( + title="WTF Python?!", + colour=constants.Colours.dark_green, + description="A repository filled with suprising snippets that can make you say WTF?!\n\n" + f"[Go to the Repository]({BASE_URL})" + ) + logo = File(LOGO_PATH, filename="wtf_logo.jpg") + no_query_embed.set_thumbnail(url="attachment://wtf_logo.jpg") + await ctx.send(embed=no_query_embed, file=logo) + return + if len(query) > 50: embed = Embed( title=random.choice(constants.ERROR_REPLIES), -- cgit v1.2.3 From 1503845b4c5349e1a49c9c9ecdc37425b659e73b Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 7 Nov 2021 11:19:05 -0500 Subject: Fix line after function docstring --- bot/exts/utilities/challenges.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/exts/utilities/challenges.py b/bot/exts/utilities/challenges.py index 751ea2a3..ab7ae442 100644 --- a/bot/exts/utilities/challenges.py +++ b/bot/exts/utilities/challenges.py @@ -275,7 +275,6 @@ class Challenges(commands.Cog): `.challenge , ` - Pulls a random challenge with the query provided, under that difficulty within the language's scope. """ - language = language.lower() if language not in SUPPORTED_LANGUAGES["stable"] + SUPPORTED_LANGUAGES["beta"]: raise commands.BadArgument("This is not a recognized language on codewars.com!") -- cgit v1.2.3 From 4fed12e92a6bb7280e6b69af07267dd82b0d6d0d Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Tue, 9 Nov 2021 08:03:20 -0500 Subject: fix: type hinting _rgb_to_cmyk --- bot/exts/utilities/colour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index 5ff1e2b8..c2a5e7e7 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -178,7 +178,7 @@ class Colour(commands.Cog): return hsl @staticmethod - def _rgb_to_cmyk(rgb: tuple[int, int, int, int]) -> tuple[int, int, int, int]: + def _rgb_to_cmyk(rgb: tuple[int, int, int]) -> tuple[int, int, int, int]: """Convert RGB values to CMYK values.""" rgb_list = [val / 255.0 for val in rgb] if not any(rgb_list): -- cgit v1.2.3 From 9990492921dd3cd1321eee90a9016b239b38741c Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Tue, 9 Nov 2021 22:55:54 +0100 Subject: use pseudo-tty for sir-lancebot service This allows the coloredlogs module to automatically detect colour support when running in docker --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index cef49213..34408e82 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,7 @@ services: dockerfile: Dockerfile container_name: sir-lancebot init: true + tty: true depends_on: - redis -- cgit v1.2.3 From 2df4ae0458e7d2765e76cff832a4b21a292ae176 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Tue, 9 Nov 2021 23:18:01 +0100 Subject: remove unnecessary volumes --- Dockerfile | 3 --- 1 file changed, 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0b5876bd..44ef0574 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,6 +25,3 @@ COPY . . # Start the bot CMD ["python", "-m", "bot"] - -# Define docker persistent volumes -VOLUME /bot/bot/log /bot/data -- cgit v1.2.3 From 2bb00fbf680d49c92bb40af71659489801cc0f99 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Tue, 9 Nov 2021 19:44:18 -0500 Subject: fix: handle alpha values in hex code Co-authored-by: Sn4u <35849006+Sn4u@users.noreply.github.com> --- bot/exts/utilities/colour.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index c2a5e7e7..7b128393 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -130,7 +130,13 @@ class Colour(commands.Cog): """Command to create an embed from a HEX input.""" if "#" not in hex_code: hex_code = f"#{hex_code}" + if len(hex_code) not in (4, 5, 7, 9) or any(_ not in string.hexdigits+"#" for _ in hex_code): + raise commands.BadArgument( + message=f"HEX values must be hexadecimal and take the form *#RRGGBB* or *#RGB*. User input was: `{hex_code}`.") + hex_tuple = ImageColor.getrgb(hex_code) + if len(hex_tuple) == 4: + hex_tuple = hex_tuple[:-1] # color must be RGB. If RGBA, we remove the alpha value await self.send_colour_response(ctx, hex_tuple) @colour.command() -- cgit v1.2.3 From 385cb3aacf60bdaea67fe59890e9c5d894f9d65a Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Tue, 9 Nov 2021 19:48:09 -0500 Subject: fix: add import, handle no name match in embed -Added `import string` to use the `string.hexdigits` method to check hex codes. -Handled bug where no name match found would be repeated in the embed in the first line as well as the value for the Name field. --- bot/exts/utilities/colour.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index 7b128393..4528615b 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -2,6 +2,7 @@ import colorsys import json import pathlib import random +import string from io import BytesIO import discord @@ -27,8 +28,6 @@ class Colour(commands.Cog): async def send_colour_response(self, ctx: commands.Context, rgb: tuple[int, int, int]) -> None: """Create and send embed from user given colour information.""" name = self._rgb_to_name(rgb) - if name is None: - name = "No match found" try: colour_or_color = ctx.invoked_parents[0] @@ -132,7 +131,9 @@ class Colour(commands.Cog): hex_code = f"#{hex_code}" if len(hex_code) not in (4, 5, 7, 9) or any(_ not in string.hexdigits+"#" for _ in hex_code): raise commands.BadArgument( - message=f"HEX values must be hexadecimal and take the form *#RRGGBB* or *#RGB*. User input was: `{hex_code}`.") + message="HEX values must be hexadecimal and take the form *#RRGGBB* or *#RGB*. " + f"User input was: `{hex_code}`." + ) hex_tuple = ImageColor.getrgb(hex_code) if len(hex_tuple) == 4: -- cgit v1.2.3 From 57c13ff35801281b5e592c9f6c195093a2fc5c7b Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Tue, 9 Nov 2021 20:20:19 -0500 Subject: bug: handle multi word name entries and full input --- bot/exts/utilities/colour.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index 4528615b..cede3312 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -39,8 +39,10 @@ class Colour(commands.Cog): input_colour = "random" elif colour_mode in ("colour", "color"): input_colour = rgb + elif colour_mode == "name": + input_colour = ctx.kwargs["user_colour_name"] else: - input_colour = ctx.args[2:][0] + input_colour = tuple(ctx.args[2:]) if colour_mode not in ("name", "hex", "random", "color", "colour"): colour_mode = colour_mode.upper() @@ -141,7 +143,7 @@ class Colour(commands.Cog): await self.send_colour_response(ctx, hex_tuple) @colour.command() - async def name(self, ctx: commands.Context, user_colour_name: str) -> None: + async def name(self, ctx: commands.Context, *, user_colour_name: str) -> None: """Command to create an embed from a name input.""" hex_colour = self.match_colour_name(user_colour_name) hex_tuple = ImageColor.getrgb(hex_colour) -- cgit v1.2.3 From 1de5e9b905310eb4745b9b5c83561dea3a289ea9 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Tue, 9 Nov 2021 20:39:39 -0500 Subject: chore: pull hex match out of try/except block --- bot/exts/utilities/colour.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index cede3312..656dddba 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -228,10 +228,9 @@ class Colour(commands.Cog): choices=self.colour_mapping.keys(), score_cutoff=80 ) - hex_match = f"#{self.colour_mapping[match]}" except (ValueError, TypeError): raise commands.BadArgument(message=f"No color found for: `{input_colour_name}`") - return hex_match + return f"#{self.colour_mapping[match]}" def setup(bot: Bot) -> None: -- cgit v1.2.3 From 93e6e96c89e621db6e463f4d5716533fd85cd96f Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Tue, 9 Nov 2021 20:44:28 -0500 Subject: chore: re-arrange command invocation with try/except --- bot/exts/utilities/colour.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index 656dddba..e69930f4 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -75,12 +75,14 @@ class Colour(commands.Cog): @commands.group(aliases=("color",), invoke_without_command=True) async def colour(self, ctx: commands.Context, *, extra: str) -> None: """User initiated command to create an embed that displays colour information.""" - if ctx.invoked_subcommand is None: - try: - extra_colour = ImageColor.getrgb(extra) - await self.send_colour_response(ctx, extra_colour) - except ValueError: - await invoke_help_command(ctx) + if ctx.invoked_subcommand: + return + + try: + extra_colour = ImageColor.getrgb(extra) + await self.send_colour_response(ctx, extra_colour) + except ValueError: + await invoke_help_command(ctx) @colour.command() async def rgb(self, ctx: commands.Context, red: int, green: int, blue: int) -> None: -- cgit v1.2.3 From c70a4e7670805ca774d1c0d634e86dc6759d8669 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Tue, 9 Nov 2021 21:22:49 -0500 Subject: chore: remove 0 from range and change to " --- bot/exts/utilities/colour.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index e69930f4..9cbf3394 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -87,7 +87,7 @@ class Colour(commands.Cog): @colour.command() async def rgb(self, ctx: commands.Context, red: int, green: int, blue: int) -> None: """Command to create an embed from an RGB input.""" - if any(c not in range(0, 256) for c in (red, green, blue)): + if any(c not in range(256) for c in (red, green, blue)): raise commands.BadArgument( message=f"RGB values can only be from 0 to 255. User input was: `{red, green, blue}`." ) @@ -97,7 +97,7 @@ class Colour(commands.Cog): @colour.command() async def hsv(self, ctx: commands.Context, hue: int, saturation: int, value: int) -> None: """Command to create an embed from an HSV input.""" - if (hue not in range(0, 361)) or any(c not in range(0, 101) for c in (saturation, value)): + if (hue not in range(361)) or any(c not in range(101) for c in (saturation, value)): raise commands.BadArgument( message="Hue can only be from 0 to 360. Saturation and Value can only be from 0 to 100. " f"User input was: `{hue, saturation, value}`." @@ -108,7 +108,7 @@ class Colour(commands.Cog): @colour.command() async def hsl(self, ctx: commands.Context, hue: int, saturation: int, lightness: int) -> None: """Command to create an embed from an HSL input.""" - if (hue not in range(0, 361)) or any(c not in range(0, 101) for c in (saturation, lightness)): + if (hue not in range(361)) or any(c not in range(101) for c in (saturation, lightness)): raise commands.BadArgument( message="Hue can only be from 0 to 360. Saturation and Lightness can only be from 0 to 100. " f"User input was: `{hue, saturation, lightness}`." @@ -119,7 +119,7 @@ class Colour(commands.Cog): @colour.command() async def cmyk(self, ctx: commands.Context, cyan: int, magenta: int, yellow: int, key: int) -> None: """Command to create an embed from a CMYK input.""" - if any(c not in range(0, 101) for c in (cyan, magenta, yellow, key)): + if any(c not in range(101) for c in (cyan, magenta, yellow, key)): raise commands.BadArgument( message=f"CMYK values can only be from 0 to 100. User input was: `{cyan, magenta, yellow, key}`." ) @@ -204,7 +204,7 @@ class Colour(commands.Cog): @staticmethod def _rgb_to_hex(rgb: tuple[int, int, int]) -> str: """Convert RGB values to HEX code.""" - hex_ = ''.join([hex(val)[2:].zfill(2) for val in rgb]) + hex_ = "".join([hex(val)[2:].zfill(2) for val in rgb]) hex_code = f"#{hex_}".upper() return hex_code -- cgit v1.2.3 From 524d33a9909697386283d966dcda1dc712a16fb0 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Tue, 9 Nov 2021 21:55:49 -0500 Subject: fix: replace 'random' in embed with color mode --- bot/exts/utilities/colour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index 9cbf3394..7e5065cd 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -36,7 +36,7 @@ class Colour(commands.Cog): colour_mode = ctx.invoked_with if colour_mode == "random": - input_colour = "random" + colour_mode = colour_or_color elif colour_mode in ("colour", "color"): input_colour = rgb elif colour_mode == "name": -- cgit v1.2.3 From 6f9da835444e0d357edc06839d9b98eafe72af30 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Wed, 10 Nov 2021 06:46:55 -0500 Subject: fix: handle user hex in embed --- bot/exts/utilities/colour.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index 7e5065cd..ec7545cd 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -41,6 +41,8 @@ class Colour(commands.Cog): input_colour = rgb elif colour_mode == "name": input_colour = ctx.kwargs["user_colour_name"] + elif colour_mode == "hex": + input_colour = ctx.args[2:][0] else: input_colour = tuple(ctx.args[2:]) -- cgit v1.2.3 From b9b1d8c10e999e16cbd067dc49710634e23e6bd7 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Wed, 10 Nov 2021 06:48:05 -0500 Subject: fix: change kwarg to color_input instead of extra --- bot/exts/utilities/colour.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index ec7545cd..1e64993c 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -75,13 +75,13 @@ class Colour(commands.Cog): await ctx.send(file=thumbnail_file, embed=colour_embed) @commands.group(aliases=("color",), invoke_without_command=True) - async def colour(self, ctx: commands.Context, *, extra: str) -> None: + async def colour(self, ctx: commands.Context, *, color_input: str) -> None: """User initiated command to create an embed that displays colour information.""" if ctx.invoked_subcommand: return try: - extra_colour = ImageColor.getrgb(extra) + extra_colour = ImageColor.getrgb(color_input) await self.send_colour_response(ctx, extra_colour) except ValueError: await invoke_help_command(ctx) -- cgit v1.2.3 From 6b21ffaef0e545d1710f2703c450c5492e100be7 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Wed, 10 Nov 2021 08:37:18 -0500 Subject: test: UI/UX updates, not tested yet --- bot/exts/utilities/colour.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index 1e64993c..259e3394 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -4,6 +4,7 @@ import pathlib import random import string from io import BytesIO +from typing import Optional, Union import discord import rapidfuzz @@ -75,8 +76,11 @@ class Colour(commands.Cog): await ctx.send(file=thumbnail_file, embed=colour_embed) @commands.group(aliases=("color",), invoke_without_command=True) - async def colour(self, ctx: commands.Context, *, color_input: str) -> None: + async def colour(self, ctx: commands.Context, *, color_input: Optional[str] = None) -> None: """User initiated command to create an embed that displays colour information.""" + if color_input is None: + await self.random() + if ctx.invoked_subcommand: return @@ -133,13 +137,17 @@ class Colour(commands.Cog): @colour.command() async def hex(self, ctx: commands.Context, hex_code: str) -> None: """Command to create an embed from a HEX input.""" - if "#" not in hex_code: + if hex_code[0] != "#": hex_code = f"#{hex_code}" - if len(hex_code) not in (4, 5, 7, 9) or any(_ not in string.hexdigits+"#" for _ in hex_code): - raise commands.BadArgument( - message="HEX values must be hexadecimal and take the form *#RRGGBB* or *#RGB*. " - f"User input was: `{hex_code}`." + + if len(hex_code) not in (4, 5, 7, 9) or any(_ not in string.hexdigits for _ in hex_code[1:]): + hex_error_embed = discord.Embed( + title="The input hex code is not valid.", + message=f"Cannot convert `{hex_code}` to a recognizable Hex format. " + "Hex values must be hexadecimal and take the form *#RRGGBB* or *#RGB*.", + colour=discord.Colour.dark_red() ) + await ctx.send(hex_error_embed) hex_tuple = ImageColor.getrgb(hex_code) if len(hex_tuple) == 4: @@ -149,7 +157,7 @@ class Colour(commands.Cog): @colour.command() async def name(self, ctx: commands.Context, *, user_colour_name: str) -> None: """Command to create an embed from a name input.""" - hex_colour = self.match_colour_name(user_colour_name) + hex_colour = await self.match_colour_name(ctx, user_colour_name) hex_tuple = ImageColor.getrgb(hex_colour) await self.send_colour_response(ctx, hex_tuple) @@ -224,7 +232,7 @@ class Colour(commands.Cog): colour_name = None return colour_name - def match_colour_name(self, input_colour_name: str) -> str: + async def match_colour_name(self, ctx: commands.Context, input_colour_name: str) -> Union[str, None]: """Convert a colour name to HEX code.""" try: match, certainty, _ = rapidfuzz.process.extractOne( @@ -233,7 +241,12 @@ class Colour(commands.Cog): score_cutoff=80 ) except (ValueError, TypeError): - raise commands.BadArgument(message=f"No color found for: `{input_colour_name}`") + name_error_embed = discord.Embed( + title="No colour match found.", + description=f"No color found for: `{input_colour_name}`", + colour=discord.Color.dark_red() + ) + await ctx.send(name_error_embed) return f"#{self.colour_mapping[match]}" -- cgit v1.2.3 From f2735273ceb7a2eabf3beee93e1ed21635b4bfd0 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Wed, 10 Nov 2021 08:40:46 -0500 Subject: test: return after default random invocation --- bot/exts/utilities/colour.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index 259e3394..e0607bf5 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -80,6 +80,7 @@ class Colour(commands.Cog): """User initiated command to create an embed that displays colour information.""" if color_input is None: await self.random() + return if ctx.invoked_subcommand: return -- cgit v1.2.3 From 87711b7b90110fc9d62d1ef880f69c1fbe6366b6 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Wed, 10 Nov 2021 14:59:33 -0500 Subject: test: correct embed descriptions --- bot/exts/utilities/colour.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index e0607bf5..08a6973b 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -144,11 +144,11 @@ class Colour(commands.Cog): if len(hex_code) not in (4, 5, 7, 9) or any(_ not in string.hexdigits for _ in hex_code[1:]): hex_error_embed = discord.Embed( title="The input hex code is not valid.", - message=f"Cannot convert `{hex_code}` to a recognizable Hex format. " + description=f"Cannot convert `{hex_code}` to a recognizable Hex format. " "Hex values must be hexadecimal and take the form *#RRGGBB* or *#RGB*.", colour=discord.Colour.dark_red() ) - await ctx.send(hex_error_embed) + await ctx.send(embed=hex_error_embed) hex_tuple = ImageColor.getrgb(hex_code) if len(hex_tuple) == 4: @@ -247,7 +247,7 @@ class Colour(commands.Cog): description=f"No color found for: `{input_colour_name}`", colour=discord.Color.dark_red() ) - await ctx.send(name_error_embed) + await ctx.send(embed=name_error_embed) return f"#{self.colour_mapping[match]}" -- cgit v1.2.3 From aaad72a40b21b9d3b1d43d67a3206923722d8575 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Wed, 10 Nov 2021 19:08:58 -0500 Subject: cleanup: finalize reviews requested changes -Change _ to `digit` -Remove redundant "Command" from docstrings. Changed to "Create embed from ..." -Change hex command custom embed to `BadArgument` for consistency --- bot/exts/utilities/colour.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index 08a6973b..9f7bedb5 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -79,7 +79,7 @@ class Colour(commands.Cog): async def colour(self, ctx: commands.Context, *, color_input: Optional[str] = None) -> None: """User initiated command to create an embed that displays colour information.""" if color_input is None: - await self.random() + await self.random(ctx) return if ctx.invoked_subcommand: @@ -93,7 +93,7 @@ class Colour(commands.Cog): @colour.command() async def rgb(self, ctx: commands.Context, red: int, green: int, blue: int) -> None: - """Command to create an embed from an RGB input.""" + """Create an embed from an RGB input.""" if any(c not in range(256) for c in (red, green, blue)): raise commands.BadArgument( message=f"RGB values can only be from 0 to 255. User input was: `{red, green, blue}`." @@ -103,7 +103,7 @@ class Colour(commands.Cog): @colour.command() async def hsv(self, ctx: commands.Context, hue: int, saturation: int, value: int) -> None: - """Command to create an embed from an HSV input.""" + """Create an embed from an HSV input.""" if (hue not in range(361)) or any(c not in range(101) for c in (saturation, value)): raise commands.BadArgument( message="Hue can only be from 0 to 360. Saturation and Value can only be from 0 to 100. " @@ -114,7 +114,7 @@ class Colour(commands.Cog): @colour.command() async def hsl(self, ctx: commands.Context, hue: int, saturation: int, lightness: int) -> None: - """Command to create an embed from an HSL input.""" + """Create an embed from an HSL input.""" if (hue not in range(361)) or any(c not in range(101) for c in (saturation, lightness)): raise commands.BadArgument( message="Hue can only be from 0 to 360. Saturation and Lightness can only be from 0 to 100. " @@ -125,7 +125,7 @@ class Colour(commands.Cog): @colour.command() async def cmyk(self, ctx: commands.Context, cyan: int, magenta: int, yellow: int, key: int) -> None: - """Command to create an embed from a CMYK input.""" + """Create an embed from a CMYK input.""" if any(c not in range(101) for c in (cyan, magenta, yellow, key)): raise commands.BadArgument( message=f"CMYK values can only be from 0 to 100. User input was: `{cyan, magenta, yellow, key}`." @@ -137,18 +137,15 @@ class Colour(commands.Cog): @colour.command() async def hex(self, ctx: commands.Context, hex_code: str) -> None: - """Command to create an embed from a HEX input.""" + """Create an embed from a HEX input.""" if hex_code[0] != "#": hex_code = f"#{hex_code}" - if len(hex_code) not in (4, 5, 7, 9) or any(_ not in string.hexdigits for _ in hex_code[1:]): - hex_error_embed = discord.Embed( - title="The input hex code is not valid.", - description=f"Cannot convert `{hex_code}` to a recognizable Hex format. " - "Hex values must be hexadecimal and take the form *#RRGGBB* or *#RGB*.", - colour=discord.Colour.dark_red() + if len(hex_code) not in (4, 5, 7, 9) or any(digit not in string.hexdigits for digit in hex_code[1:]): + raise commands.BadArgument( + message=f"Cannot convert `{hex_code}` to a recognizable Hex format. " + "Hex values must be hexadecimal and take the form *#RRGGBB* or *#RGB*." ) - await ctx.send(embed=hex_error_embed) hex_tuple = ImageColor.getrgb(hex_code) if len(hex_tuple) == 4: @@ -157,14 +154,14 @@ class Colour(commands.Cog): @colour.command() async def name(self, ctx: commands.Context, *, user_colour_name: str) -> None: - """Command to create an embed from a name input.""" + """Create an embed from a name input.""" hex_colour = await self.match_colour_name(ctx, user_colour_name) hex_tuple = ImageColor.getrgb(hex_colour) await self.send_colour_response(ctx, hex_tuple) @colour.command() async def random(self, ctx: commands.Context) -> None: - """Command to create an embed from a randomly chosen colour from the reference file.""" + """Create an embed from a randomly chosen colour from the reference file.""" hex_colour = random.choice(list(self.colour_mapping.values())) hex_tuple = ImageColor.getrgb(f"#{hex_colour}") await self.send_colour_response(ctx, hex_tuple) -- cgit v1.2.3 From 231ad0e58ee7b2eb58f48833abe469bf61582bdc Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Wed, 10 Nov 2021 19:20:56 -0500 Subject: cleanup: change main command docstring to be more clear --- bot/exts/utilities/colour.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index 9f7bedb5..b233fc1e 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -77,7 +77,11 @@ class Colour(commands.Cog): @commands.group(aliases=("color",), invoke_without_command=True) async def colour(self, ctx: commands.Context, *, color_input: Optional[str] = None) -> None: - """User initiated command to create an embed that displays colour information.""" + """ + Create an embed that displays colour information. + + If no subcommand is called, a randomly selected colour will be selected and shown. + """ if color_input is None: await self.random(ctx) return -- cgit v1.2.3 From 3332bcf61e4586432225e2626a90a3430055a8a8 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Thu, 11 Nov 2021 16:45:27 -0500 Subject: fix: change color to colour in no match found result Co-authored-by: Hedy Li --- bot/exts/utilities/colour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index b233fc1e..ee53c535 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -245,7 +245,7 @@ class Colour(commands.Cog): except (ValueError, TypeError): name_error_embed = discord.Embed( title="No colour match found.", - description=f"No color found for: `{input_colour_name}`", + description=f"No colour found for: `{input_colour_name}`", colour=discord.Color.dark_red() ) await ctx.send(embed=name_error_embed) -- cgit v1.2.3 From ed1fb463c4e04fa4628e46df33144a4b5c579b82 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Thu, 11 Nov 2021 16:54:10 -0500 Subject: fix: remove async call for match_colour_name --- bot/exts/utilities/colour.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index b233fc1e..ed69beaf 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -159,7 +159,15 @@ class Colour(commands.Cog): @colour.command() async def name(self, ctx: commands.Context, *, user_colour_name: str) -> None: """Create an embed from a name input.""" - hex_colour = await self.match_colour_name(ctx, user_colour_name) + hex_colour = self.match_colour_name(ctx, user_colour_name) + if hex_colour is None: + name_error_embed = discord.Embed( + title="No colour match found.", + description=f"No color found for: `{user_colour_name}`", + colour=discord.Color.dark_red() + ) + await ctx.send(embed=name_error_embed) + return hex_tuple = ImageColor.getrgb(hex_colour) await self.send_colour_response(ctx, hex_tuple) @@ -234,7 +242,7 @@ class Colour(commands.Cog): colour_name = None return colour_name - async def match_colour_name(self, ctx: commands.Context, input_colour_name: str) -> Union[str, None]: + def match_colour_name(self, ctx: commands.Context, input_colour_name: str) -> Union[str, None]: """Convert a colour name to HEX code.""" try: match, certainty, _ = rapidfuzz.process.extractOne( @@ -243,12 +251,7 @@ class Colour(commands.Cog): score_cutoff=80 ) except (ValueError, TypeError): - name_error_embed = discord.Embed( - title="No colour match found.", - description=f"No color found for: `{input_colour_name}`", - colour=discord.Color.dark_red() - ) - await ctx.send(embed=name_error_embed) + return None return f"#{self.colour_mapping[match]}" -- cgit v1.2.3 From a8aebd4fe051052f52b68c822d0762c56eba9591 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Thu, 11 Nov 2021 16:58:09 -0500 Subject: fix: change color to Colour in comment --- bot/exts/utilities/colour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index ed69beaf..a35b393d 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -153,7 +153,7 @@ class Colour(commands.Cog): hex_tuple = ImageColor.getrgb(hex_code) if len(hex_tuple) == 4: - hex_tuple = hex_tuple[:-1] # color must be RGB. If RGBA, we remove the alpha value + hex_tuple = hex_tuple[:-1] # Colour must be RGB. If RGBA, we remove the alpha value await self.send_colour_response(ctx, hex_tuple) @colour.command() -- cgit v1.2.3 From 36a316f8ca27b35325ab31d575cc944a72f22c01 Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Thu, 11 Nov 2021 17:00:05 -0500 Subject: update: remove redundancy in docstring Co-authored-by: Hedy Li --- bot/exts/utilities/colour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index ed69beaf..3b5c8681 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -80,7 +80,7 @@ class Colour(commands.Cog): """ Create an embed that displays colour information. - If no subcommand is called, a randomly selected colour will be selected and shown. + If no subcommand is called, a randomly selected colour will be shown. """ if color_input is None: await self.random(ctx) -- cgit v1.2.3 From 1914905bdc7bd591ff906562ee0496346d37a045 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Thu, 11 Nov 2021 17:02:45 -0500 Subject: fix: remove alpha values in embed for hex --- bot/exts/utilities/colour.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index c865859a..d438fa27 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -43,7 +43,7 @@ class Colour(commands.Cog): elif colour_mode == "name": input_colour = ctx.kwargs["user_colour_name"] elif colour_mode == "hex": - input_colour = ctx.args[2:][0] + input_colour = ctx.args[2:][0][0:-2] else: input_colour = tuple(ctx.args[2:]) @@ -80,7 +80,7 @@ class Colour(commands.Cog): """ Create an embed that displays colour information. - If no subcommand is called, a randomly selected colour will be shown. + If no subcommand is called, a randomly selected colour will be selected and shown. """ if color_input is None: await self.random(ctx) -- cgit v1.2.3 From 6b3ac76a7e5e63503bbba139538e52d23480b3fc Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Thu, 11 Nov 2021 17:07:15 -0500 Subject: update: remove redundancy in dosctring --- bot/exts/utilities/colour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index d438fa27..6772fa1f 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -80,7 +80,7 @@ class Colour(commands.Cog): """ Create an embed that displays colour information. - If no subcommand is called, a randomly selected colour will be selected and shown. + If no subcommand is called, a randomly selected colour will be shown. """ if color_input is None: await self.random(ctx) -- cgit v1.2.3 From 6a9f1886c70658636f36272ea8b8f4c3f5f4a7a9 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Fri, 12 Nov 2021 06:09:32 +0400 Subject: Disable File Logs In Production The most recent changes to our log setup had the loggers writing to a read-only location in prod. This would cause an error during startup. To get around this while keeping the change, I moved it to only be used if debug is True. Signed-off-by: Hassan Abouelela --- bot/log.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/bot/log.py b/bot/log.py index 5e0e909d..97561be4 100644 --- a/bot/log.py +++ b/bot/log.py @@ -18,19 +18,22 @@ def setup() -> None: format_string = "%(asctime)s | %(name)s | %(levelname)s | %(message)s" log_format = logging.Formatter(format_string) + root_logger = logging.getLogger() - # Set up file logging - log_file = Path("logs/sir-lancebot.log") - log_file.parent.mkdir(exist_ok=True) + # Copied from constants file, which we can't import yet since loggers aren't instantiated + debug = os.environ.get("BOT_DEBUG", "true").lower() == "true" - # File handler rotates logs every 5 MB - file_handler = logging.handlers.RotatingFileHandler( - log_file, maxBytes=5 * (2 ** 20), backupCount=10, encoding="utf-8", - ) - file_handler.setFormatter(log_format) + if debug: + # Set up file logging + log_file = Path("logs/sir-lancebot.log") + log_file.parent.mkdir(exist_ok=True) - root_logger = logging.getLogger() - root_logger.addHandler(file_handler) + # File handler rotates logs every 5 MB + file_handler = logging.handlers.RotatingFileHandler( + log_file, maxBytes=5 * (2 ** 20), backupCount=10, encoding="utf-8", + ) + file_handler.setFormatter(log_format) + root_logger.addHandler(file_handler) if "COLOREDLOGS_LEVEL_STYLES" not in os.environ: coloredlogs.DEFAULT_LEVEL_STYLES = { -- cgit v1.2.3 From 40b58132d488bab962d8aeecfc932a1cb5842a85 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Fri, 12 Nov 2021 06:19:59 +0400 Subject: Move Sentry Into Init Moves the sentry setup to be one of the very first things run during startup, so we are able to catch more errors, such as ones that might occur while setting up logs. Signed-off-by: Hassan Abouelela --- bot/__init__.py | 18 ++++++++++++++++++ bot/__main__.py | 20 +------------------- bot/constants.py | 3 --- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/bot/__init__.py b/bot/__init__.py index b19bd76a..ae53a5a5 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -6,14 +6,32 @@ except ModuleNotFoundError: pass import asyncio +import logging import os from functools import partial, partialmethod import arrow +import sentry_sdk from discord.ext import commands +from sentry_sdk.integrations.logging import LoggingIntegration +from sentry_sdk.integrations.redis import RedisIntegration from bot import log, monkey_patches +sentry_logging = LoggingIntegration( + level=logging.DEBUG, + event_level=logging.WARNING +) + +sentry_sdk.init( + dsn=os.environ.get("BOT_SENTRY_DSN"), + integrations=[ + sentry_logging, + RedisIntegration() + ], + release=f"sir-lancebot@{os.environ.get('GIT_SHA', 'foobar')}" +) + log.setup() # Set timestamp of when execution started (approximately) diff --git a/bot/__main__.py b/bot/__main__.py index c6e5fa57..6889fe2b 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -1,28 +1,10 @@ import logging -import sentry_sdk -from sentry_sdk.integrations.logging import LoggingIntegration -from sentry_sdk.integrations.redis import RedisIntegration - from bot.bot import bot -from bot.constants import Client, GIT_SHA, STAFF_ROLES, WHITELISTED_CHANNELS +from bot.constants import Client, STAFF_ROLES, WHITELISTED_CHANNELS from bot.utils.decorators import whitelist_check from bot.utils.extensions import walk_extensions -sentry_logging = LoggingIntegration( - level=logging.DEBUG, - event_level=logging.WARNING -) - -sentry_sdk.init( - dsn=Client.sentry_dsn, - integrations=[ - sentry_logging, - RedisIntegration() - ], - release=f"sir-lancebot@{GIT_SHA}" -) - log = logging.getLogger(__name__) bot.add_check(whitelist_check(channels=WHITELISTED_CHANNELS, roles=STAFF_ROLES)) diff --git a/bot/constants.py b/bot/constants.py index 9d12000e..2b41b8a4 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -134,7 +134,6 @@ class Client(NamedTuple): guild = int(environ.get("BOT_GUILD", 267624335836053506)) prefix = environ.get("PREFIX", ".") token = environ.get("BOT_TOKEN") - sentry_dsn = environ.get("BOT_SENTRY_DSN") debug = environ.get("BOT_DEBUG", "true").lower() == "true" github_bot_repo = "https://github.com/python-discord/sir-lancebot" # Override seasonal locks: 1 (January) to 12 (December) @@ -348,8 +347,6 @@ WHITELISTED_CHANNELS = ( Channels.voice_chat_1, ) -GIT_SHA = environ.get("GIT_SHA", "foobar") - # Bot replies ERROR_REPLIES = [ "Please don't do that.", -- cgit v1.2.3 From 45ac010fd6d2f8f9774cc5f408aa31c40fac42e6 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Fri, 12 Nov 2021 10:05:15 -0500 Subject: fix: check length of hex before strip --- bot/exts/utilities/colour.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index 6772fa1f..66df5e0b 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -43,7 +43,9 @@ class Colour(commands.Cog): elif colour_mode == "name": input_colour = ctx.kwargs["user_colour_name"] elif colour_mode == "hex": - input_colour = ctx.args[2:][0][0:-2] + input_colour = ctx.args[2:][0] + if len(input_colour) >= 7: + input_colour = input_colour[0:-2] else: input_colour = tuple(ctx.args[2:]) -- cgit v1.2.3 From 5b3a23fe7b81d07c842271dc1947aa60133a2f8d Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Fri, 12 Nov 2021 10:08:21 -0500 Subject: fix: replace Union with Optional type hint Co-authored-by: Xithrius <15021300+Xithrius@users.noreply.github.com> --- bot/exts/utilities/colour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index 66df5e0b..5cd01fb5 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -244,7 +244,7 @@ class Colour(commands.Cog): colour_name = None return colour_name - def match_colour_name(self, ctx: commands.Context, input_colour_name: str) -> Union[str, None]: + def match_colour_name(self, ctx: commands.Context, input_colour_name: str) -> Optional[str]: """Convert a colour name to HEX code.""" try: match, certainty, _ = rapidfuzz.process.extractOne( -- cgit v1.2.3 From 8bbef57ccda68c388cf3d4b4fbc90fedf0e8597e Mon Sep 17 00:00:00 2001 From: brad90four <42116429+brad90four@users.noreply.github.com> Date: Fri, 12 Nov 2021 10:08:42 -0500 Subject: fix: bare return instead of explicit None Co-authored-by: Xithrius <15021300+Xithrius@users.noreply.github.com> --- bot/exts/utilities/colour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index 5cd01fb5..c7edec0d 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -253,7 +253,7 @@ class Colour(commands.Cog): score_cutoff=80 ) except (ValueError, TypeError): - return None + return return f"#{self.colour_mapping[match]}" -- cgit v1.2.3 From 6d5505366313b2ddb9d1a8f11b758e043e466ece Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Fri, 12 Nov 2021 10:10:12 -0500 Subject: fix: remove equal sign from hex length check --- bot/exts/utilities/colour.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index c7edec0d..66dbfa30 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -4,7 +4,7 @@ import pathlib import random import string from io import BytesIO -from typing import Optional, Union +from typing import Optional import discord import rapidfuzz @@ -44,7 +44,7 @@ class Colour(commands.Cog): input_colour = ctx.kwargs["user_colour_name"] elif colour_mode == "hex": input_colour = ctx.args[2:][0] - if len(input_colour) >= 7: + if len(input_colour) > 7: input_colour = input_colour[0:-2] else: input_colour = tuple(ctx.args[2:]) -- cgit v1.2.3 From e7923ed258ed7bf51cafbeb030df265a0992b1f1 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Sat, 13 Nov 2021 09:44:03 -0500 Subject: fix: remove unnecessary return in main command --- bot/exts/utilities/colour.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index 66dbfa30..0681e807 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -88,9 +88,6 @@ class Colour(commands.Cog): await self.random(ctx) return - if ctx.invoked_subcommand: - return - try: extra_colour = ImageColor.getrgb(color_input) await self.send_colour_response(ctx, extra_colour) -- cgit v1.2.3 From e87e037e772fa2319c786210f176fd5088f00131 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Sat, 13 Nov 2021 10:09:04 -0500 Subject: fix: update type hint, color to colour, embed wording --- bot/exts/utilities/colour.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index 0681e807..e33f65c6 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -29,7 +29,6 @@ class Colour(commands.Cog): async def send_colour_response(self, ctx: commands.Context, rgb: tuple[int, int, int]) -> None: """Create and send embed from user given colour information.""" name = self._rgb_to_name(rgb) - try: colour_or_color = ctx.invoked_parents[0] except IndexError: @@ -38,8 +37,9 @@ class Colour(commands.Cog): colour_mode = ctx.invoked_with if colour_mode == "random": colour_mode = colour_or_color + input_colour = name elif colour_mode in ("colour", "color"): - input_colour = rgb + input_colour = name elif colour_mode == "name": input_colour = ctx.kwargs["user_colour_name"] elif colour_mode == "hex": @@ -55,8 +55,8 @@ class Colour(commands.Cog): colour_mode = colour_mode.title() colour_embed = discord.Embed( - title=colour_or_color.title(), - description=f"{colour_mode} information for `{name or input_colour}`.", + title=f"{name or input_colour}", + description=f"{colour_or_color.title()} information for {colour_mode} `{input_colour or name}`.", colour=discord.Color.from_rgb(*rgb) ) colour_conversions = self.get_colour_conversions(rgb) @@ -78,18 +78,18 @@ class Colour(commands.Cog): await ctx.send(file=thumbnail_file, embed=colour_embed) @commands.group(aliases=("color",), invoke_without_command=True) - async def colour(self, ctx: commands.Context, *, color_input: Optional[str] = None) -> None: + async def colour(self, ctx: commands.Context, *, colour_input: Optional[str] = None) -> None: """ Create an embed that displays colour information. If no subcommand is called, a randomly selected colour will be shown. """ - if color_input is None: + if colour_input is None: await self.random(ctx) return try: - extra_colour = ImageColor.getrgb(color_input) + extra_colour = ImageColor.getrgb(colour_input) await self.send_colour_response(ctx, extra_colour) except ValueError: await invoke_help_command(ctx) @@ -162,7 +162,7 @@ class Colour(commands.Cog): if hex_colour is None: name_error_embed = discord.Embed( title="No colour match found.", - description=f"No color found for: `{user_colour_name}`", + description=f"No colour found for: `{user_colour_name}`", colour=discord.Color.dark_red() ) await ctx.send(embed=name_error_embed) @@ -172,7 +172,7 @@ class Colour(commands.Cog): @colour.command() async def random(self, ctx: commands.Context) -> None: - """Create an embed from a randomly chosen colour from the reference file.""" + """Create an embed from a randomly chosen colour.""" hex_colour = random.choice(list(self.colour_mapping.values())) hex_tuple = ImageColor.getrgb(f"#{hex_colour}") await self.send_colour_response(ctx, hex_tuple) @@ -227,7 +227,7 @@ class Colour(commands.Cog): hex_code = f"#{hex_}".upper() return hex_code - def _rgb_to_name(self, rgb: tuple[int, int, int]) -> str: + def _rgb_to_name(self, rgb: tuple[int, int, int]) -> Optional[str]: """Convert RGB values to a fuzzy matched name.""" input_hex_colour = self._rgb_to_hex(rgb) try: -- cgit v1.2.3 From e9cda06c2ab147ace8e64a27503cf99e17948418 Mon Sep 17 00:00:00 2001 From: bradtimmis Date: Sat, 13 Nov 2021 10:17:00 -0500 Subject: fix: handle bare command embed title --- bot/exts/utilities/colour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index e33f65c6..7c83fc66 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -39,7 +39,7 @@ class Colour(commands.Cog): colour_mode = colour_or_color input_colour = name elif colour_mode in ("colour", "color"): - input_colour = name + input_colour = ctx.kwargs["colour_input"] elif colour_mode == "name": input_colour = ctx.kwargs["user_colour_name"] elif colour_mode == "hex": -- cgit v1.2.3 From eb60d8b69e3eb5a4d1fdbe85e906350f426479c1 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Sun, 14 Nov 2021 16:48:28 +0100 Subject: Resources: add copyright notice to ryanzec_colours.json --- bot/resources/utilities/ryanzec_colours.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/utilities/ryanzec_colours.json b/bot/resources/utilities/ryanzec_colours.json index ad8f05fd..552d5a3f 100644 --- a/bot/resources/utilities/ryanzec_colours.json +++ b/bot/resources/utilities/ryanzec_colours.json @@ -1,5 +1,5 @@ { - "_": "Source: https://github.com/ryanzec/name-that-color/blob/0bb5ec7f37e4f6e7f2c164f39f7f08cca7c8e621/lib/ntc.js#L116-L1681", + "_": "Source: https://github.com/ryanzec/name-that-color/blob/0bb5ec7f37e4f6e7f2c164f39f7f08cca7c8e621/lib/ntc.js#L116-L1681\n\nCopyright (c) 2014 Ryan Zec\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.", "Abbey": "4C4F56", "Acadia": "1B1404", "Acapulco": "7CB0A1", -- cgit v1.2.3 From c56c5a9ab01a45a50877c228843fa27625f03512 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Thu, 18 Nov 2021 21:02:15 +0100 Subject: Introduce command changes in the AoC Cog - The AoC day and star browser has been separated from the leaderboard command, from now on it's a separate command - The leaderboard command has a new `self_placement_name` option, that shows the personal stats for the specified profile name. --- bot/exts/events/advent_of_code/_cog.py | 72 +++++++++++++++++++----------- bot/exts/events/advent_of_code/_helpers.py | 25 +++++++++-- 2 files changed, 66 insertions(+), 31 deletions(-) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 2c1f4541..7f35c306 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -180,24 +180,18 @@ class AdventOfCode(commands.Cog): @in_month(Month.DECEMBER) @adventofcode_group.command( - name="leaderboard", - aliases=("board", "lb"), - brief="Get a snapshot of the PyDis private AoC leaderboard", + name="dayandstar", + aliases=("daynstar", "daystar"), + brief="Get a view that lets you filter the leaderboard by day and star", ) @whitelist_override(channels=AOC_WHITELIST_RESTRICTED) - async def aoc_leaderboard( + async def aoc_day_and_star_leaderboard( self, ctx: commands.Context, - day_and_star: Optional[bool] = False, - maximum_scorers: Optional[int] = 10 + maximum_scorers_day_and_star: Optional[int] = 10 ) -> None: - """ - Get the current top scorers of the Python Discord Leaderboard. - - Additionally, you can provide an argument `day_and_star` (Boolean) to have the bot send a View - that will let you filter by day and star. - """ - if maximum_scorers > AocConfig.max_day_and_star_results or maximum_scorers <= 0: + """Have the bot send a View that will let you filter the leaderboard by day and star.""" + if maximum_scorers_day_and_star > AocConfig.max_day_and_star_results or maximum_scorers_day_and_star <= 0: raise commands.BadArgument( f"The maximum number of results you can query is {AocConfig.max_day_and_star_results}" ) @@ -207,25 +201,12 @@ class AdventOfCode(commands.Cog): except _helpers.FetchingLeaderboardFailedError: await ctx.send(":x: Unable to fetch leaderboard!") return - if not day_and_star: - - number_of_participants = leaderboard["number_of_participants"] - - top_count = min(AocConfig.leaderboard_displayed_members, number_of_participants) - header = f"Here's our current top {top_count}! {Emojis.christmas_tree * 3}" - - table = f"```\n{leaderboard['top_leaderboard']}\n```" - info_embed = _helpers.get_summary_embed(leaderboard) - - await ctx.send(content=f"{header}\n\n{table}", embed=info_embed) - return - # This is a dictionary that contains solvers in respect of day, and star. # e.g. 1-1 means the solvers of the first star of the first day and their completion time per_day_and_star = json.loads(leaderboard['leaderboard_per_day_and_star']) view = AoCDropdownView( day_and_star_data=per_day_and_star, - maximum_scorers=maximum_scorers, + maximum_scorers=maximum_scorers_day_and_star, original_author=ctx.author ) message = await ctx.send( @@ -235,6 +216,43 @@ class AdventOfCode(commands.Cog): await view.wait() await message.edit(view=None) + @in_month(Month.DECEMBER) + @adventofcode_group.command( + name="leaderboard", + aliases=("board", "lb"), + brief="Get a snapshot of the PyDis private AoC leaderboard", + ) + @whitelist_override(channels=AOC_WHITELIST_RESTRICTED) + async def aoc_leaderboard( + self, + ctx: commands.Context, + self_placement_name: Optional[str] = None, + ) -> None: + """ + Get the current top scorers of the Python Discord Leaderboard. + + Additionaly you can specify a `self_placement_name` + that will append the specified profile's personal stats to the top of the leaderboard + """ + async with ctx.typing(): + try: + leaderboard = await _helpers.fetch_leaderboard(self_placement_name=self_placement_name) + except _helpers.FetchingLeaderboardFailedError: + await ctx.send(":x: Unable to fetch leaderboard!") + return + + number_of_participants = leaderboard["number_of_participants"] + + top_count = min(AocConfig.leaderboard_displayed_members, number_of_participants) + self_placement_header = "(and your personal stats compared to the top 10)" if self_placement_name else "" + header = f"Here's our current top {top_count}{self_placement_header}! {Emojis.christmas_tree * 3}" + + table = f"```\n{leaderboard['top_leaderboard']}\n```" + info_embed = _helpers.get_summary_embed(leaderboard) + + await ctx.send(content=f"{header}\n\n{table}", embed=info_embed) + return + @in_month(Month.DECEMBER) @adventofcode_group.command( name="global", diff --git a/bot/exts/events/advent_of_code/_helpers.py b/bot/exts/events/advent_of_code/_helpers.py index af64bc81..58fc3054 100644 --- a/bot/exts/events/advent_of_code/_helpers.py +++ b/bot/exts/events/advent_of_code/_helpers.py @@ -10,6 +10,7 @@ from typing import Any, Optional import aiohttp import arrow import discord +from discord.ext import commands from bot.bot import Bot from bot.constants import AdventOfCode, Channels, Colours @@ -160,10 +161,23 @@ def _parse_raw_leaderboard_data(raw_leaderboard_data: dict) -> dict: return {"daily_stats": daily_stats, "leaderboard": sorted_leaderboard, 'per_day_and_star': per_day_star_stats} -def _format_leaderboard(leaderboard: dict[str, dict]) -> str: +def _format_leaderboard(leaderboard: dict[str, dict], self_placement_name: str = None) -> str: """Format the leaderboard using the AOC_TABLE_TEMPLATE.""" leaderboard_lines = [HEADER] + self_placement_exists = False for rank, data in enumerate(leaderboard.values(), start=1): + if self_placement_name and data["name"].lower() == self_placement_name.lower(): + leaderboard_lines.insert( + 1, + AOC_TABLE_TEMPLATE.format( + rank=rank, + name=f"(You) {data['name']}", + score=str(data["score"]), + stars=f"({data['star_1']}, {data['star_2']})" + ) + ) + self_placement_exists = True + continue leaderboard_lines.append( AOC_TABLE_TEMPLATE.format( rank=rank, @@ -172,7 +186,10 @@ def _format_leaderboard(leaderboard: dict[str, dict]) -> str: stars=f"({data['star_1']}, {data['star_2']})" ) ) - + if self_placement_name and not self_placement_exists: + raise commands.BadArgument( + "Sorry, your profile does not exist in this leaderboard." + ) return "\n".join(leaderboard_lines) @@ -260,7 +277,7 @@ def _get_top_leaderboard(full_leaderboard: str) -> str: @_caches.leaderboard_cache.atomic_transaction -async def fetch_leaderboard(invalidate_cache: bool = False) -> dict: +async def fetch_leaderboard(invalidate_cache: bool = False, self_placement_name: str = None) -> dict: """ Get the current Python Discord combined leaderboard. @@ -284,7 +301,7 @@ async def fetch_leaderboard(invalidate_cache: bool = False) -> dict: leaderboard = parsed_leaderboard_data["leaderboard"] number_of_participants = len(leaderboard) - formatted_leaderboard = _format_leaderboard(leaderboard) + formatted_leaderboard = _format_leaderboard(leaderboard, self_placement_name) full_leaderboard_url = await _upload_leaderboard(formatted_leaderboard) leaderboard_fetched_at = datetime.datetime.utcnow().isoformat() -- cgit v1.2.3 From ad66899453b027f3a5522cc103f5c128456144c4 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Fri, 19 Nov 2021 18:06:52 +0100 Subject: Update Caching logic in AoC helpers - This commit adds a new set of leaderboard data to the cache so that it calculates the correct information if issued with either a `self_placement_name` or not. - It also introduces code consistency upgrades Co-Authored-By: Johannes Christ --- bot/exts/events/advent_of_code/_cog.py | 7 +++--- bot/exts/events/advent_of_code/_helpers.py | 39 +++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 7f35c306..cd41e9ce 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -231,7 +231,7 @@ class AdventOfCode(commands.Cog): """ Get the current top scorers of the Python Discord Leaderboard. - Additionaly you can specify a `self_placement_name` + Additionally you can specify a `self_placement_name` that will append the specified profile's personal stats to the top of the leaderboard """ async with ctx.typing(): @@ -246,8 +246,9 @@ class AdventOfCode(commands.Cog): top_count = min(AocConfig.leaderboard_displayed_members, number_of_participants) self_placement_header = "(and your personal stats compared to the top 10)" if self_placement_name else "" header = f"Here's our current top {top_count}{self_placement_header}! {Emojis.christmas_tree * 3}" - - table = f"```\n{leaderboard['top_leaderboard']}\n```" + table = "```\n" \ + f"{leaderboard['placement_leaderboard'] if self_placement_name else leaderboard['top_leaderboard']}" \ + "\n```" info_embed = _helpers.get_summary_embed(leaderboard) await ctx.send(content=f"{header}\n\n{table}", embed=info_embed) diff --git a/bot/exts/events/advent_of_code/_helpers.py b/bot/exts/events/advent_of_code/_helpers.py index 58fc3054..35258544 100644 --- a/bot/exts/events/advent_of_code/_helpers.py +++ b/bot/exts/events/advent_of_code/_helpers.py @@ -71,6 +71,33 @@ class FetchingLeaderboardFailedError(Exception): """Raised when one or more leaderboards could not be fetched at all.""" +def _format_leaderboard_line(rank: int, data: dict[str, Any], *, is_author: bool) -> str: + """ + Build a string representing a line of the leaderboard. + + Parameters: + rank: + Rank in the leaderboard of this entry. + + data: + Mapping with entry information. + + Keyword arguments: + is_author: + Whether to address the name displayed in the returned line + personally. + + Returns: + A formatted line for the leaderboard. + """ + return AOC_TABLE_TEMPLATE.format( + rank=rank, + name=data['name'] if not is_author else f"(You) {data['name']}", + score=str(data['score']), + stars=f"({data['star_1']}, {data['star_2']})" + ) + + def leaderboard_sorting_function(entry: tuple[str, dict]) -> tuple[int, int]: """ Provide a sorting value for our leaderboard. @@ -287,7 +314,6 @@ async def fetch_leaderboard(invalidate_cache: bool = False, self_placement_name: miss, this function is locked to one call at a time using a decorator. """ cached_leaderboard = await _caches.leaderboard_cache.to_dict() - # Check if the cached leaderboard contains everything we expect it to. If it # does not, this probably means the cache has not been created yet or has # expired in Redis. This check also accounts for a malformed cache. @@ -301,11 +327,12 @@ async def fetch_leaderboard(invalidate_cache: bool = False, self_placement_name: leaderboard = parsed_leaderboard_data["leaderboard"] number_of_participants = len(leaderboard) - formatted_leaderboard = _format_leaderboard(leaderboard, self_placement_name) + formatted_leaderboard = _format_leaderboard(leaderboard) full_leaderboard_url = await _upload_leaderboard(formatted_leaderboard) leaderboard_fetched_at = datetime.datetime.utcnow().isoformat() cached_leaderboard = { + "placement_leaderboard": json.dumps(raw_leaderboard_data), "full_leaderboard": formatted_leaderboard, "top_leaderboard": _get_top_leaderboard(formatted_leaderboard), "full_leaderboard_url": full_leaderboard_url, @@ -324,7 +351,13 @@ async def fetch_leaderboard(invalidate_cache: bool = False, self_placement_name: _caches.leaderboard_cache.namespace, AdventOfCode.leaderboard_cache_expiry_seconds ) - + if self_placement_name: + formatted_placement_leaderboard = _parse_raw_leaderboard_data( + json.loads(cached_leaderboard["placement_leaderboard"]) + )["leaderboard"] + cached_leaderboard["placement_leaderboard"] = _get_top_leaderboard( + _format_leaderboard(formatted_placement_leaderboard, self_placement_name=self_placement_name) + ) return cached_leaderboard -- cgit v1.2.3 From 5059e61c354054b8cd463866cbd4ab4b1c0a8c09 Mon Sep 17 00:00:00 2001 From: aru Date: Sat, 20 Nov 2021 09:55:50 -0500 Subject: Remove unnecessary edits during pagination pick 3cd4c92b1e24c8cfdae8c5c68c19607c62cc01ed from python-discord/bot remove additional unnecessary edit --- bot/utils/pagination.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/bot/utils/pagination.py b/bot/utils/pagination.py index 013ef9e7..188b279f 100644 --- a/bot/utils/pagination.py +++ b/bot/utils/pagination.py @@ -211,8 +211,6 @@ class LinePaginator(Paginator): log.debug(f"Got first page reaction - changing to page 1/{len(paginator.pages)}") - embed.description = "" - await message.edit(embed=embed) embed.description = paginator.pages[current_page] if footer_text: embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})") @@ -226,8 +224,6 @@ class LinePaginator(Paginator): log.debug(f"Got last page reaction - changing to page {current_page + 1}/{len(paginator.pages)}") - embed.description = "" - await message.edit(embed=embed) embed.description = paginator.pages[current_page] if footer_text: embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})") @@ -245,8 +241,6 @@ class LinePaginator(Paginator): current_page -= 1 log.debug(f"Got previous page reaction - changing to page {current_page + 1}/{len(paginator.pages)}") - embed.description = "" - await message.edit(embed=embed) embed.description = paginator.pages[current_page] if footer_text: @@ -266,8 +260,6 @@ class LinePaginator(Paginator): current_page += 1 log.debug(f"Got next page reaction - changing to page {current_page + 1}/{len(paginator.pages)}") - embed.description = "" - await message.edit(embed=embed) embed.description = paginator.pages[current_page] if footer_text: @@ -428,8 +420,6 @@ class ImagePaginator(Paginator): reaction_type = "next" # Magic happens here, after page and reaction_type is set - embed.description = "" - await message.edit(embed=embed) embed.description = paginator.pages[current_page] image = paginator.images[current_page] or EmptyEmbed -- cgit v1.2.3 From b794b02c4aa81218500151fde53e95d8c10c9817 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sat, 20 Nov 2021 23:10:57 +0300 Subject: Disable File Logging By Default Place logging to file behind an environment variable. Signed-off-by: Hassan Abouelela --- bot/constants.py | 1 + bot/log.py | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 2b41b8a4..33bc8b61 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -135,6 +135,7 @@ class Client(NamedTuple): prefix = environ.get("PREFIX", ".") token = environ.get("BOT_TOKEN") debug = environ.get("BOT_DEBUG", "true").lower() == "true" + file_logs = environ.get("FILE_LOGS", "false").lower() == "true" github_bot_repo = "https://github.com/python-discord/sir-lancebot" # Override seasonal locks: 1 (January) to 12 (December) month_override = int(environ["MONTH_OVERRIDE"]) if "MONTH_OVERRIDE" in environ else None diff --git a/bot/log.py b/bot/log.py index 97561be4..29e696e0 100644 --- a/bot/log.py +++ b/bot/log.py @@ -20,10 +20,7 @@ def setup() -> None: log_format = logging.Formatter(format_string) root_logger = logging.getLogger() - # Copied from constants file, which we can't import yet since loggers aren't instantiated - debug = os.environ.get("BOT_DEBUG", "true").lower() == "true" - - if debug: + if Client.file_logs: # Set up file logging log_file = Path("logs/sir-lancebot.log") log_file.parent.mkdir(exist_ok=True) -- cgit v1.2.3 From 377dcf969c74ef83cf534e7ba058fbb87bd28d88 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sun, 21 Nov 2021 01:55:20 +0300 Subject: Add Better Support For Whitelisting DM Commands Adds a toggle to the whitelist override, to allow explicit enabling of a command in DMs, and sets the default to False. Removes redundant `guild_only` decorators. Signed-off-by: Hassan Abouelela --- bot/exts/fun/anagram.py | 1 - bot/exts/fun/battleship.py | 1 - bot/exts/fun/connect_four.py | 3 --- bot/exts/fun/tic_tac_toe.py | 3 +-- bot/utils/decorators.py | 27 +++++++++++++++++---------- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/bot/exts/fun/anagram.py b/bot/exts/fun/anagram.py index 9aee5f18..79280fa9 100644 --- a/bot/exts/fun/anagram.py +++ b/bot/exts/fun/anagram.py @@ -49,7 +49,6 @@ class Anagram(commands.Cog): self.games: dict[int, AnagramGame] = {} @commands.command(name="anagram", aliases=("anag", "gram", "ag")) - @commands.guild_only() async def anagram_command(self, ctx: commands.Context) -> None: """ Given shuffled letters, rearrange them into anagrams. diff --git a/bot/exts/fun/battleship.py b/bot/exts/fun/battleship.py index f4351954..beff196f 100644 --- a/bot/exts/fun/battleship.py +++ b/bot/exts/fun/battleship.py @@ -369,7 +369,6 @@ class Battleship(commands.Cog): return any(player in (game.p1.user, game.p2.user) for game in self.games) @commands.group(invoke_without_command=True) - @commands.guild_only() async def battleship(self, ctx: commands.Context) -> None: """ Play a game of Battleship with someone else! diff --git a/bot/exts/fun/connect_four.py b/bot/exts/fun/connect_four.py index 647bb2b7..f53695d5 100644 --- a/bot/exts/fun/connect_four.py +++ b/bot/exts/fun/connect_four.py @@ -6,7 +6,6 @@ from typing import Optional, Union import discord import emojis from discord.ext import commands -from discord.ext.commands import guild_only from bot.bot import Bot from bot.constants import Emojis @@ -361,7 +360,6 @@ class ConnectFour(commands.Cog): self.games.remove(game) raise - @guild_only() @commands.group( invoke_without_command=True, aliases=("4inarow", "connect4", "connectfour", "c4"), @@ -426,7 +424,6 @@ class ConnectFour(commands.Cog): await self._play_game(ctx, user, board_size, str(emoji1), str(emoji2)) - @guild_only() @connect_four.command(aliases=("bot", "computer", "cpu")) async def ai( self, diff --git a/bot/exts/fun/tic_tac_toe.py b/bot/exts/fun/tic_tac_toe.py index 946b6f7b..5dd38a81 100644 --- a/bot/exts/fun/tic_tac_toe.py +++ b/bot/exts/fun/tic_tac_toe.py @@ -3,7 +3,7 @@ import random from typing import Callable, Optional, Union import discord -from discord.ext.commands import Cog, Context, check, group, guild_only +from discord.ext.commands import Cog, Context, check, group from bot.bot import Bot from bot.constants import Emojis @@ -253,7 +253,6 @@ class TicTacToe(Cog): def __init__(self): self.games: list[Game] = [] - @guild_only() @is_channel_free() @is_requester_free() @group(name="tictactoe", aliases=("ttt", "tic"), invoke_without_command=True) diff --git a/bot/utils/decorators.py b/bot/utils/decorators.py index 132aaa87..7a3b14ad 100644 --- a/bot/utils/decorators.py +++ b/bot/utils/decorators.py @@ -196,15 +196,14 @@ def whitelist_check(**default_kwargs: Container[int]) -> Callable[[Context], boo If `whitelist_override` is present, it is added to the global whitelist. """ def predicate(ctx: Context) -> bool: - # Skip DM invocations - if not ctx.guild: - log.debug(f"{ctx.author} tried to use the '{ctx.command.name}' command from a DM.") - return True - kwargs = default_kwargs.copy() + allow_dms = False # Update kwargs based on override if hasattr(ctx.command.callback, "override"): + # Handle DM invocations + allow_dms = ctx.command.callback.override_dm + # Remove default kwargs if reset is True if ctx.command.callback.override_reset: kwargs = {} @@ -234,8 +233,12 @@ def whitelist_check(**default_kwargs: Container[int]) -> Callable[[Context], boo f"invoked by {ctx.author}." ) - log.trace(f"Calling whitelist check for {ctx.author} for command {ctx.command.name}.") - result = in_whitelist_check(ctx, fail_silently=True, **kwargs) + if ctx.guild is None: + log.debug(f"{ctx.author} tried using the '{ctx.command.name}' command from a DM.") + result = allow_dms + else: + log.trace(f"Calling whitelist check for {ctx.author} for command {ctx.command.name}.") + result = in_whitelist_check(ctx, fail_silently=True, **kwargs) # Return if check passed if result: @@ -260,8 +263,8 @@ def whitelist_check(**default_kwargs: Container[int]) -> Callable[[Context], boo default_whitelist_channels.discard(Channels.community_bot_commands) channels.difference_update(default_whitelist_channels) - # Add all whitelisted category channels - if categories: + # Add all whitelisted category channels, but skip if we're in DMs + if categories and ctx.guild is not None: for category_id in categories: category = ctx.guild.get_channel(category_id) if category is None: @@ -280,18 +283,22 @@ def whitelist_check(**default_kwargs: Container[int]) -> Callable[[Context], boo return predicate -def whitelist_override(bypass_defaults: bool = False, **kwargs: Container[int]) -> Callable: +def whitelist_override(bypass_defaults: bool = False, allow_dm: bool = False, **kwargs: Container[int]) -> Callable: """ Override global whitelist context, with the kwargs specified. All arguments from `in_whitelist_check` are supported, with the exception of `fail_silently`. Set `bypass_defaults` to True if you want to completely bypass global checks. + Set `allow_dm` to True if you want to allow the command to be invoked from within direct messages. + Note that you have to be careful with any references to the guild. + This decorator has to go before (below) below the `command` decorator. """ def inner(func: Callable) -> Callable: func.override = kwargs func.override_reset = bypass_defaults + func.override_dm = allow_dm return func return inner -- cgit v1.2.3 From 19a21dc6b07a6c7947fbde1de9c6aee21ccc0ef8 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Sun, 21 Nov 2021 11:20:13 +0100 Subject: Add check to ensure the day-and-star data exists --- bot/exts/events/advent_of_code/views/dayandstarview.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/bot/exts/events/advent_of_code/views/dayandstarview.py b/bot/exts/events/advent_of_code/views/dayandstarview.py index 243db32e..a0bfa316 100644 --- a/bot/exts/events/advent_of_code/views/dayandstarview.py +++ b/bot/exts/events/advent_of_code/views/dayandstarview.py @@ -17,14 +17,19 @@ class AoCDropdownView(discord.ui.View): self.original_author = original_author def generate_output(self) -> str: - """Generates a formatted codeblock with AoC statistics based on the currently selected day and star.""" + """ + Generates a formatted codeblock with AoC statistics based on the currently selected day and star. + + Optionally, when the requested day and star data does not exist yet it returns an error message. + """ header = AOC_DAY_AND_STAR_TEMPLATE.format( rank="Rank", name="Name", completion_time="Completion time (UTC)" ) lines = [f"{header}\n{'-' * (len(header) + 2)}"] - - for rank, scorer in enumerate(self.data[f"{self.day}-{self.star}"][:self.maximum_scorers]): + if not (day_and_star_data := self.data.get(f"{self.day}-{self.star}")): + return ":x: The requested data for the specified day and star does not exist yet." + for rank, scorer in enumerate(day_and_star_data[:self.maximum_scorers]): time_data = datetime.fromtimestamp(scorer['completion_time']).strftime("%I:%M:%S %p") lines.append(AOC_DAY_AND_STAR_TEMPLATE.format( datastamp="", -- cgit v1.2.3 From 0a1ded20ee8111efc45805dcf4a668f575f1c798 Mon Sep 17 00:00:00 2001 From: DMFriends Date: Mon, 22 Nov 2021 16:15:23 -0500 Subject: Fix a grammatical mistake in the description of the `.spookify` command --- bot/exts/avatar_modification/avatar_modify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/avatar_modification/avatar_modify.py b/bot/exts/avatar_modification/avatar_modify.py index fbee96dc..3ee70cfd 100644 --- a/bot/exts/avatar_modification/avatar_modify.py +++ b/bot/exts/avatar_modification/avatar_modify.py @@ -286,7 +286,7 @@ class AvatarModify(commands.Cog): @avatar_modify.command( aliases=("savatar", "spookify"), root_aliases=("spookyavatar", "spookify", "savatar"), - brief="Spookify an user's avatar." + brief="Spookify a user's avatar." ) async def spookyavatar(self, ctx: commands.Context) -> None: """This "spookifies" the user's avatar, with a random *spooky* effect.""" -- cgit v1.2.3 From be7da6f34e50b7a99ef6c3a7eb67a48b24b818ed Mon Sep 17 00:00:00 2001 From: Izan Date: Mon, 29 Nov 2021 09:11:17 +0000 Subject: Change `MODERATION_ROLES` and `STAFF_ROLES` constants to be a set --- bot/constants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index b4191c5e..bef7f3d1 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -332,8 +332,8 @@ class Reddit: # Default role combinations -MODERATION_ROLES = Roles.moderation_team, Roles.admins, Roles.owners -STAFF_ROLES = Roles.helpers, Roles.moderation_team, Roles.admins, Roles.owners +MODERATION_ROLES = {Roles.moderation_team, Roles.admins, Roles.owners} +STAFF_ROLES = {Roles.helpers, Roles.moderation_team, Roles.admins, Roles.owners} # Whitelisted channels WHITELISTED_CHANNELS = ( -- cgit v1.2.3 From a958d1c1f6a3308b5ffd03cb7f9ef846b0871265 Mon Sep 17 00:00:00 2001 From: Izan Date: Mon, 29 Nov 2021 09:34:31 +0000 Subject: Revert change to if statement checking if staff in `.aoc join` --- bot/exts/events/advent_of_code/_cog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index c3073fd5..34ade5b1 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -148,7 +148,7 @@ class AdventOfCode(commands.Cog): author = ctx.author log.info(f"{author.name} ({author.id}) has requested a PyDis AoC leaderboard code") - if AocConfig.staff_leaderboard_id and any(r.id in STAFF_ROLES for r in author.roles): + if AocConfig.staff_leaderboard_id and any(r.id == Roles.helpers for r in author.roles): join_code = AocConfig.leaderboards[AocConfig.staff_leaderboard_id].join_code else: try: -- cgit v1.2.3 From 338fc7a0b37483feba56a8810cd2e1a344ab8b22 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 29 Nov 2021 10:48:26 +0000 Subject: Swap conditional in aoc count This is so that there is less code within the if block, making it easier to parse the whole command by eye. --- bot/exts/events/advent_of_code/_cog.py | 41 +++++++++++++++++----------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index cd41e9ce..c91a5019 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -100,32 +100,31 @@ class AdventOfCode(commands.Cog): @whitelist_override(channels=AOC_WHITELIST) async def aoc_countdown(self, ctx: commands.Context) -> None: """Return time left until next day.""" - if not _helpers.is_in_advent(): - datetime_now = arrow.now(_helpers.EST) - - # Calculate the delta to this & next year's December 1st to see which one is closest and not in the past - this_year = arrow.get(datetime(datetime_now.year, 12, 1), _helpers.EST) - next_year = arrow.get(datetime(datetime_now.year + 1, 12, 1), _helpers.EST) - deltas = (dec_first - datetime_now for dec_first in (this_year, next_year)) - delta = min(delta for delta in deltas if delta >= timedelta()) # timedelta() gives 0 duration delta - - # Add a finer timedelta if there's less than a day left - if delta.days == 0: - delta_str = f"approximately {delta.seconds // 3600} hours" - else: - delta_str = f"{delta.days} days" + if _helpers.is_in_advent(): + tomorrow, time_left = _helpers.time_left_to_est_midnight() + hours, minutes = time_left.seconds // 3600, time_left.seconds // 60 % 60 - await ctx.send( - "The Advent of Code event is not currently running. " - f"The next event will start in {delta_str}." - ) + await ctx.send(f"There are {hours} hours and {minutes} minutes left until day {tomorrow.day}.") return - tomorrow, time_left = _helpers.time_left_to_est_midnight() + datetime_now = arrow.now(_helpers.EST) - hours, minutes = time_left.seconds // 3600, time_left.seconds // 60 % 60 + # Calculate the delta to this & next year's December 1st to see which one is closest and not in the past + this_year = arrow.get(datetime(datetime_now.year, 12, 1), _helpers.EST) + next_year = arrow.get(datetime(datetime_now.year + 1, 12, 1), _helpers.EST) + deltas = (dec_first - datetime_now for dec_first in (this_year, next_year)) + delta = min(delta for delta in deltas if delta >= timedelta()) # timedelta() gives 0 duration delta - await ctx.send(f"There are {hours} hours and {minutes} minutes left until day {tomorrow.day}.") + # Add a finer timedelta if there's less than a day left + if delta.days == 0: + delta_str = f"approximately {delta.seconds // 3600} hours" + else: + delta_str = f"{delta.days} days" + + await ctx.send( + "The Advent of Code event is not currently running. " + f"The next event will start in {delta_str}." + ) @adventofcode_group.command(name="about", aliases=("ab", "info"), brief="Learn about Advent of Code") @whitelist_override(channels=AOC_WHITELIST) -- cgit v1.2.3 From 790bcdf2fedc8dd7aac8c5a2610dc8395852bce0 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 29 Nov 2021 10:49:28 +0000 Subject: Use a Discord timestamp to show countdown This gives the user the ability to hover the timestamp with their mouse to get an exact date and time. --- bot/exts/events/advent_of_code/_cog.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index c91a5019..94722005 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -115,15 +115,11 @@ class AdventOfCode(commands.Cog): deltas = (dec_first - datetime_now for dec_first in (this_year, next_year)) delta = min(delta for delta in deltas if delta >= timedelta()) # timedelta() gives 0 duration delta - # Add a finer timedelta if there's less than a day left - if delta.days == 0: - delta_str = f"approximately {delta.seconds // 3600} hours" - else: - delta_str = f"{delta.days} days" + next_aoc_timestamp = int((datetime_now + delta).timestamp()) await ctx.send( "The Advent of Code event is not currently running. " - f"The next event will start in {delta_str}." + f"The next event will start ." ) @adventofcode_group.command(name="about", aliases=("ab", "info"), brief="Learn about Advent of Code") -- cgit v1.2.3 From c81d30357db2c0950d5e781cdd5e9053907b228d Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 29 Nov 2021 15:10:29 +0000 Subject: Use Discord timestamps for aoc next day messages countdowns --- bot/exts/events/advent_of_code/_cog.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 94722005..af1cbcf5 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -101,14 +101,13 @@ class AdventOfCode(commands.Cog): async def aoc_countdown(self, ctx: commands.Context) -> None: """Return time left until next day.""" if _helpers.is_in_advent(): - tomorrow, time_left = _helpers.time_left_to_est_midnight() - hours, minutes = time_left.seconds // 3600, time_left.seconds // 60 % 60 + tomorrow, _ = _helpers.time_left_to_est_midnight() + next_day_timestamp = int(tomorrow.timestamp()) - await ctx.send(f"There are {hours} hours and {minutes} minutes left until day {tomorrow.day}.") + await ctx.send(f"Day {tomorrow.day} starts .") return datetime_now = arrow.now(_helpers.EST) - # Calculate the delta to this & next year's December 1st to see which one is closest and not in the past this_year = arrow.get(datetime(datetime_now.year, 12, 1), _helpers.EST) next_year = arrow.get(datetime(datetime_now.year + 1, 12, 1), _helpers.EST) -- cgit v1.2.3 From 65398ed2b9fb67dbe842996e01142722bd0667f3 Mon Sep 17 00:00:00 2001 From: aru Date: Wed, 1 Dec 2021 17:15:25 -0500 Subject: fix: hanukkah command respects month boundaries rewrote hanukkah to use datetime.strptime left a helper method and some variables in order to allow extending to use a cache in the future, rather than requesting the api every invoke that is out of scope of this commit and pull, since the command is currently broken. I've only kept the same functionality, without trying to rewrite the entire command. --- bot/exts/holidays/hanukkah/hanukkah_embed.py | 84 ++++++++++++---------------- 1 file changed, 36 insertions(+), 48 deletions(-) diff --git a/bot/exts/holidays/hanukkah/hanukkah_embed.py b/bot/exts/holidays/hanukkah/hanukkah_embed.py index ac3eab7b..5767f91e 100644 --- a/bot/exts/holidays/hanukkah/hanukkah_embed.py +++ b/bot/exts/holidays/hanukkah/hanukkah_embed.py @@ -21,45 +21,41 @@ class HanukkahEmbed(commands.Cog): def __init__(self, bot: Bot): self.bot = bot - self.hanukkah_days = [] - self.hanukkah_months = [] - self.hanukkah_years = [] + self.hanukkah_dates: list[datetime.date] = [] - async def get_hanukkah_dates(self) -> list[str]: + def _parse_time_to_datetime(self, date: list[str]) -> datetime.datetime: + """Format the times provided by the api to datetime forms.""" + try: + return datetime.datetime.strptime(date, "%Y-%m-%dT%H:%M:%S%z") + except ValueError: + # there is a possibility of an event not having a time, just a day + # to catch this, we try again without time information + return datetime.datetime.strptime(date, "%Y-%m-%d") + + async def fetch_hanukkah_dates(self) -> list[datetime.date]: """Gets the dates for hanukkah festival.""" - hanukkah_dates = [] + # clear the datetime objects to prevent a memory link + self.hanukkah_dates = [] async with self.bot.http_session.get(HEBCAL_URL) as response: json_data = await response.json() festivals = json_data["items"] for festival in festivals: if festival["title"].startswith("Chanukah"): date = festival["date"] - hanukkah_dates.append(date) - return hanukkah_dates + self.hanukkah_dates.append(self._parse_time_to_datetime(date).date()) + return self.hanukkah_dates @in_month(Month.NOVEMBER, Month.DECEMBER) @commands.command(name="hanukkah", aliases=("chanukah",)) async def hanukkah_festival(self, ctx: commands.Context) -> None: """Tells you about the Hanukkah Festivaltime of festival, festival day, etc).""" - hanukkah_dates = await self.get_hanukkah_dates() - self.hanukkah_dates_split(hanukkah_dates) - hanukkah_start_day = int(self.hanukkah_days[0]) - hanukkah_start_month = int(self.hanukkah_months[0]) - hanukkah_start_year = int(self.hanukkah_years[0]) - hanukkah_end_day = int(self.hanukkah_days[8]) - hanukkah_end_month = int(self.hanukkah_months[8]) - hanukkah_end_year = int(self.hanukkah_years[8]) - - hanukkah_start = datetime.date(hanukkah_start_year, hanukkah_start_month, hanukkah_start_day) - hanukkah_end = datetime.date(hanukkah_end_year, hanukkah_end_month, hanukkah_end_day) + hanukkah_dates = await self.fetch_hanukkah_dates() + start_day = hanukkah_dates[0] + end_day = hanukkah_dates[-1] today = datetime.date.today() - # today = datetime.date(2019, 12, 24) (for testing) - day = str(today.day) - month = str(today.month) - year = str(today.year) embed = Embed(title="Hanukkah", colour=Colours.blue) - if day in self.hanukkah_days and month in self.hanukkah_months and year in self.hanukkah_years: - if int(day) == hanukkah_start_day: + if start_day <= today <= end_day: + if start_day == today: now = datetime.datetime.utcnow() hours = now.hour + 4 # using only hours hanukkah_start_hour = 18 @@ -77,35 +73,27 @@ class HanukkahEmbed(commands.Cog): ) await ctx.send(embed=embed) return - festival_day = self.hanukkah_days.index(day) + festival_day = hanukkah_dates.index(today) number_suffixes = ["st", "nd", "rd", "th"] suffix = number_suffixes[festival_day - 1 if festival_day <= 3 else 3] message = ":menorah:" * festival_day - embed.description = f"It is the {festival_day}{suffix} day of Hanukkah!\n{message}" - await ctx.send(embed=embed) + embed.description = ( + f"It is the {festival_day}{suffix} day of Hanukkah!\n{message}" + ) + elif today < start_day: + format_start = start_day.strftime("%d of %B") + embed.description = ( + "Hanukkah has not started yet. " + f"Hanukkah will start at sundown on {format_start}." + ) else: - if today < hanukkah_start: - festival_starting_month = hanukkah_start.strftime("%B") - embed.description = ( - f"Hanukkah has not started yet. " - f"Hanukkah will start at sundown on {hanukkah_start_day}th " - f"of {festival_starting_month}." - ) - else: - festival_end_month = hanukkah_end.strftime("%B") - embed.description = ( - f"Looks like you missed Hanukkah!" - f"Hanukkah ended on {hanukkah_end_day}th of {festival_end_month}." - ) - - await ctx.send(embed=embed) + format_end = end_day.strftime("%d of %B") + embed.description = ( + "Looks like you missed Hanukkah! " + f"Hanukkah ended on {format_end}." + ) - def hanukkah_dates_split(self, hanukkah_dates: list[str]) -> None: - """We are splitting the dates for hanukkah into days, months and years.""" - for date in hanukkah_dates: - self.hanukkah_days.append(date[8:10]) - self.hanukkah_months.append(date[5:7]) - self.hanukkah_years.append(date[0:4]) + await ctx.send(embed=embed) def setup(bot: Bot) -> None: -- cgit v1.2.3 From eba75f73964a258119e16ba2abfc181055281022 Mon Sep 17 00:00:00 2001 From: Janine vN Date: Wed, 1 Dec 2021 22:41:07 -0500 Subject: Add `.aoc link` command This new command will allow people to associate their Discord ID with their Advent of Code name. This Redis Cache idea was taken from the hacktoberfest stats command, which allows people to associate their github username to then pull the correct stats. This does not check if the name exists on the leaderboard and that is intentional. Due to the cooldown on the leaderboard I don't want to rely on that before someone can link their account. Additionally, someone may change their display name on the Advent of Code side and I don't think validation of it existing on the leaderboard gets us anything. The usefulness of this function will not be apparent in this cog, but it is necessary for something fun I'd like to do. --- bot/exts/events/advent_of_code/_cog.py | 69 ++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index af1cbcf5..1b1cd9f8 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -6,6 +6,7 @@ from typing import Optional import arrow import discord +from async_rediscache import RedisCache from discord.ext import commands from bot.bot import Bot @@ -29,6 +30,8 @@ AOC_WHITELIST = AOC_WHITELIST_RESTRICTED + (Channels.advent_of_code,) class AdventOfCode(commands.Cog): """Advent of Code festivities! Ho Ho Ho!""" + account_links = RedisCache() + def __init__(self, bot: Bot): self.bot = bot @@ -172,6 +175,72 @@ class AdventOfCode(commands.Cog): else: await ctx.message.add_reaction(Emojis.envelope) + @in_month(Month.NOVEMBER, Month.DECEMBER) + @adventofcode_group.command( + name="link", + aliases=("connect",), + brief="Tie your Discord account with your Advent of Code name." + ) + @whitelist_override(channels=AOC_WHITELIST) + async def aoc_link_account(self, ctx: commands.Context, aoc_name: str = None) -> None: + """ + Link your Discord Account to your Advent of Code name. + + Stored in a Redis Cache, Discord ID: Advent of Code Name + """ + cache_items = await self.account_links.items() + + # A short circuit in case the cache is empty + if len(cache_items) == 0 and aoc_name: + log.info(f"{ctx.author} ({ctx.author.id}) is now linked to {aoc_name}.") + await self.account_links.set(ctx.author.id, aoc_name) + await ctx.reply(f"You have linked your Discord ID to {aoc_name}.") + return + elif len(cache_items) == 0: + await ctx.reply( + "You have not linked an Advent of Code account." + "Please re-run the command with one specified." + ) + return + + cache_aoc_name = [value for _, value in cache_items] + + if aoc_name: + # Let's check the current values in the cache to make sure it isn't already tied to a different account + if aoc_name == await self.account_links.get(ctx.author.id): + await ctx.reply(f"{aoc_name} is already tied to your account.") + return + elif aoc_name in cache_aoc_name: + log.info( + f"{ctx.author} ({ctx.author.id}) tried to connect their account to {aoc_name}," + " but it's already connected to another user." + ) + await ctx.reply( + f"{aoc_name} is already tied to another account." + " Please contact an admin if you believe this is an error." + ) + return + + # Update an existing link + if old_aoc_name := await self.account_links.get(ctx.author.id): + log.info(f"{ctx.author} ({ctx.author.id}) has changed their link from {old_aoc_name} to {aoc_name}.") + await self.account_links.set(ctx.author.id, aoc_name) + await ctx.reply(f"Your linked account has been changed to {aoc_name}.") + else: + # Create a new link + log.info(f"{ctx.author} ({ctx.author.id}) is now linked to {aoc_name}.") + await self.account_links.set(ctx.author.id, aoc_name) + await ctx.reply(f"You have linked your Discord ID to {aoc_name}.") + else: + # User has not supplied a name, let's check if they're in the cache or not + if cache_name := await self.account_links.get(ctx.author.id): + await ctx.reply(f"You have already linked an Advent of Code account: {cache_name}.") + else: + await ctx.reply( + "You have not linked an Advent of Code account." + " Please re-run the command with one specified." + ) + @in_month(Month.DECEMBER) @adventofcode_group.command( name="dayandstar", -- cgit v1.2.3 From 1dba7a7a7132839230f776a8ecb73b9def93174f Mon Sep 17 00:00:00 2001 From: Ben Soyka Date: Wed, 1 Dec 2021 20:44:46 -0700 Subject: Make self_placement_name keyword-only in .aoc lb --- bot/exts/events/advent_of_code/_cog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index af1cbcf5..900c3469 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -220,6 +220,7 @@ class AdventOfCode(commands.Cog): async def aoc_leaderboard( self, ctx: commands.Context, + *, self_placement_name: Optional[str] = None, ) -> None: """ -- cgit v1.2.3 From 4959f085537421814159734070b74dadd566501f Mon Sep 17 00:00:00 2001 From: Ben Soyka Date: Wed, 1 Dec 2021 21:16:49 -0700 Subject: Strip quotes from start/end of the username for .aoc lb --- bot/exts/events/advent_of_code/_cog.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 900c3469..c6225356 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -229,6 +229,10 @@ class AdventOfCode(commands.Cog): Additionally you can specify a `self_placement_name` that will append the specified profile's personal stats to the top of the leaderboard """ + # Strip quotes from the self placement name if needed (e.g. "My Name" -> My Name) + if self_placement_name and self_placement_name[0] == self_placement_name[-1] == '"': + self_placement_name = self_placement_name[1:-1] + async with ctx.typing(): try: leaderboard = await _helpers.fetch_leaderboard(self_placement_name=self_placement_name) -- cgit v1.2.3 From aea61a096bfbf60fab9711698c9353635b13ea0c Mon Sep 17 00:00:00 2001 From: Ben Soyka Date: Wed, 1 Dec 2021 21:43:17 -0700 Subject: Shorten parameter name for .aoc lb --- bot/exts/events/advent_of_code/_cog.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index c6225356..0f6739fc 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -217,25 +217,20 @@ class AdventOfCode(commands.Cog): brief="Get a snapshot of the PyDis private AoC leaderboard", ) @whitelist_override(channels=AOC_WHITELIST_RESTRICTED) - async def aoc_leaderboard( - self, - ctx: commands.Context, - *, - self_placement_name: Optional[str] = None, - ) -> None: + async def aoc_leaderboard(self, ctx: commands.Context, *, aoc_name: Optional[str] = None) -> None: """ Get the current top scorers of the Python Discord Leaderboard. - Additionally you can specify a `self_placement_name` - that will append the specified profile's personal stats to the top of the leaderboard + Additionally you can specify an `aoc_name` that will append the + specified profile's personal stats to the top of the leaderboard """ # Strip quotes from the self placement name if needed (e.g. "My Name" -> My Name) - if self_placement_name and self_placement_name[0] == self_placement_name[-1] == '"': - self_placement_name = self_placement_name[1:-1] + if aoc_name and aoc_name.startswith('"') and aoc_name.endswith('"'): + aoc_name = aoc_name[1:-1] async with ctx.typing(): try: - leaderboard = await _helpers.fetch_leaderboard(self_placement_name=self_placement_name) + leaderboard = await _helpers.fetch_leaderboard(self_placement_name=aoc_name) except _helpers.FetchingLeaderboardFailedError: await ctx.send(":x: Unable to fetch leaderboard!") return @@ -243,10 +238,10 @@ class AdventOfCode(commands.Cog): number_of_participants = leaderboard["number_of_participants"] top_count = min(AocConfig.leaderboard_displayed_members, number_of_participants) - self_placement_header = "(and your personal stats compared to the top 10)" if self_placement_name else "" + self_placement_header = "(and your personal stats compared to the top 10)" if aoc_name else "" header = f"Here's our current top {top_count}{self_placement_header}! {Emojis.christmas_tree * 3}" table = "```\n" \ - f"{leaderboard['placement_leaderboard'] if self_placement_name else leaderboard['top_leaderboard']}" \ + f"{leaderboard['placement_leaderboard'] if aoc_name else leaderboard['top_leaderboard']}" \ "\n```" info_embed = _helpers.get_summary_embed(leaderboard) -- cgit v1.2.3 From 4ec414ad37c6c10218b80ebd1b028840c0f0f853 Mon Sep 17 00:00:00 2001 From: ToxicKidz Date: Tue, 29 Jun 2021 11:47:30 -0400 Subject: chore: Merge the .issue command into the github cog --- bot/exts/utilities/githubinfo.py | 273 +++++++++++++++++++++++++++++++++++++- bot/exts/utilities/issues.py | 277 --------------------------------------- 2 files changed, 267 insertions(+), 283 deletions(-) delete mode 100644 bot/exts/utilities/issues.py diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index 539e388b..f0820731 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -1,5 +1,8 @@ import logging import random +import re +import typing as t +from dataclasses import dataclass from datetime import datetime from urllib.parse import quote, quote_plus @@ -7,24 +10,183 @@ import discord from discord.ext import commands from bot.bot import Bot -from bot.constants import Colours, NEGATIVE_REPLIES +from bot.constants import ( + Categories, + Channels, + Colours, + ERROR_REPLIES, + Emojis, + NEGATIVE_REPLIES, + Tokens, + WHITELISTED_CHANNELS +) from bot.exts.core.extensions import invoke_help_command +from bot.utils.decorators import whitelist_override log = logging.getLogger(__name__) GITHUB_API_URL = "https://api.github.com" +BAD_RESPONSE = { + 404: "Issue/pull request not located! Please enter a valid number!", + 403: "Rate limit has been hit! Please try again later!" +} +REQUEST_HEADERS = { + "Accept": "application/vnd.github.v3+json" +} + +REPOSITORY_ENDPOINT = "https://api.github.com/orgs/{org}/repos?per_page=100&type=public" +ISSUE_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/issues/{number}" +PR_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/pulls/{number}" + +if GITHUB_TOKEN := Tokens.github: + REQUEST_HEADERS["Authorization"] = f"token {GITHUB_TOKEN}" + +WHITELISTED_CATEGORIES = ( + Categories.development, Categories.devprojects, Categories.media, Categories.staff +) + +CODE_BLOCK_RE = re.compile( + r"^`([^`\n]+)`" # Inline codeblock + r"|```(.+?)```", # Multiline codeblock + re.DOTALL | re.MULTILINE +) + +# Maximum number of issues in one message +MAXIMUM_ISSUES = 5 + +# Regex used when looking for automatic linking in messages +# regex101 of current regex https://regex101.com/r/V2ji8M/6 +AUTOMATIC_REGEX = re.compile( + r"((?P[a-zA-Z0-9][a-zA-Z0-9\-]{1,39})\/)?(?P[\w\-\.]{1,100})#(?P[0-9]+)" +) + + +@dataclass +class FoundIssue: + """Dataclass representing an issue found by the regex.""" + + organisation: t.Optional[str] + repository: str + number: str + + def __hash__(self) -> int: + return hash((self.organisation, self.repository, self.number)) + + +@dataclass +class FetchError: + """Dataclass representing an error while fetching an issue.""" + + return_code: int + message: str + + +@dataclass +class IssueState: + """Dataclass representing the state of an issue.""" + + repository: str + number: int + url: str + title: str + emoji: str + class GithubInfo(commands.Cog): - """Fetches info from GitHub.""" + """A Cog that fetches info from GitHub.""" def __init__(self, bot: Bot): self.bot = bot + self.repos = [] + + @staticmethod + def remove_codeblocks(message: str) -> str: + """Remove any codeblock in a message.""" + return CODE_BLOCK_RE.sub("", message) + + async def fetch_issues( + self, + number: int, + repository: str, + user: str + ) -> t.Union[IssueState, FetchError]: + """ + Retrieve an issue from a GitHub repository. - async def fetch_data(self, url: str) -> dict: - """Retrieve data as a dictionary.""" - async with self.bot.http_session.get(url) as r: - return await r.json() + Returns IssueState on success, FetchError on failure. + """ + url = ISSUE_ENDPOINT.format(user=user, repository=repository, number=number) + pulls_url = PR_ENDPOINT.format(user=user, repository=repository, number=number) + log.trace(f"Querying GH issues API: {url}") + + async with self.bot.http_session.get(url, headers=REQUEST_HEADERS) as r: + json_data = await r.json() + + if r.status == 403: + if r.headers.get("X-RateLimit-Remaining") == "0": + log.info(f"Ratelimit reached while fetching {url}") + return FetchError(403, "Ratelimit reached, please retry in a few minutes.") + return FetchError(403, "Cannot access issue.") + elif r.status in (404, 410): + return FetchError(r.status, "Issue not found.") + elif r.status != 200: + return FetchError(r.status, "Error while fetching issue.") + + # The initial API request is made to the issues API endpoint, which will return information + # if the issue or PR is present. However, the scope of information returned for PRs differs + # from issues: if the 'issues' key is present in the response then we can pull the data we + # need from the initial API call. + if "issues" in json_data["html_url"]: + if json_data.get("state") == "open": + emoji = Emojis.issue_open + else: + emoji = Emojis.issue_closed + + # If the 'issues' key is not contained in the API response and there is no error code, then + # we know that a PR has been requested and a call to the pulls API endpoint is necessary + # to get the desired information for the PR. + else: + log.trace(f"PR provided, querying GH pulls API for additional information: {pulls_url}") + async with self.bot.http_session.get(pulls_url) as p: + pull_data = await p.json() + if pull_data["draft"]: + emoji = Emojis.pull_request_draft + elif pull_data["state"] == "open": + emoji = Emojis.pull_request_open + # When 'merged_at' is not None, this means that the state of the PR is merged + elif pull_data["merged_at"] is not None: + emoji = Emojis.pull_request_merged + else: + emoji = Emojis.pull_request_closed + + issue_url = json_data.get("html_url") + + return IssueState(repository, number, issue_url, json_data.get("title", ""), emoji) + + @staticmethod + def format_embed( + results: t.List[t.Union[IssueState, FetchError]], + user: str, + repository: t.Optional[str] = None + ) -> discord.Embed: + """Take a list of IssueState or FetchError and format a Discord embed for them.""" + description_list = [] + + for result in results: + if isinstance(result, IssueState): + description_list.append(f"{result.emoji} [{result.title}]({result.url})") + elif isinstance(result, FetchError): + description_list.append(f":x: [{result.return_code}] {result.message}") + + resp = discord.Embed( + colour=Colours.bright_green, + description="\n".join(description_list) + ) + + embed_url = f"https://github.com/{user}/{repository}" if repository else f"https://github.com/{user}" + resp.set_author(name="GitHub", url=embed_url) + return resp @commands.group(name="github", aliases=("gh", "git")) @commands.cooldown(1, 10, commands.BucketType.user) @@ -33,6 +195,105 @@ class GithubInfo(commands.Cog): if ctx.invoked_subcommand is None: await invoke_help_command(ctx) + @whitelist_override(channels=WHITELISTED_CHANNELS, categories=WHITELISTED_CATEGORIES) + @github_group.command(aliases=("pr", "issues", "prs"), root_aliases=("issue", "pr")) + async def issue( + self, + ctx: commands.Context, + numbers: commands.Greedy[int], + repository: str = "sir-lancebot", + user: str = "python-discord" + ) -> None: + """Command to retrieve issue(s) from a GitHub repository.""" + # Remove duplicates + numbers = set(numbers) + + err_message = None + if not numbers: + err_message = "You must have at least one issue/PR!" + + elif len(numbers) > MAXIMUM_ISSUES: + err_message = f"Too many issues/PRs! (maximum of {MAXIMUM_ISSUES})" + + # If there's an error with command invocation then send an error embed + if err_message is not None: + err_embed = discord.Embed( + title=random.choice(ERROR_REPLIES), + color=Colours.soft_red, + description=err_message + ) + await ctx.send(embed=err_embed) + await invoke_help_command(ctx) + return + + results = [await self.fetch_issues(number, repository, user) for number in numbers] + await ctx.send(embed=self.format_embed(results, user, repository)) + + @commands.Cog.listener() + async def on_message(self, message: discord.Message) -> None: + """ + Automatic issue linking. + + Listener to retrieve issue(s) from a GitHub repository using automatic linking if matching /#. + """ + # Ignore bots + if message.author.bot: + return + + issues = [ + FoundIssue(*match.group("org", "repo", "number")) + for match in AUTOMATIC_REGEX.finditer(self.remove_codeblocks(message.content)) + ] + links = [] + + if issues: + # Block this from working in DMs + if not message.guild: + await message.channel.send( + embed=discord.Embed( + title=random.choice(NEGATIVE_REPLIES), + description=( + "You can't retrieve issues from DMs. " + f"Try again in <#{Channels.community_bot_commands}>" + ), + colour=Colours.soft_red + ) + ) + return + + log.trace(f"Found {issues = }") + # Remove duplicates + issues = set(issues) + + if len(issues) > MAXIMUM_ISSUES: + embed = discord.Embed( + title=random.choice(ERROR_REPLIES), + color=Colours.soft_red, + description=f"Too many issues/PRs! (maximum of {MAXIMUM_ISSUES})" + ) + await message.channel.send(embed=embed, delete_after=5) + return + + for repo_issue in issues: + result = await self.fetch_issues( + int(repo_issue.number), + repo_issue.repository, + repo_issue.organisation or "python-discord" + ) + if isinstance(result, IssueState): + links.append(result) + + if not links: + return + + resp = self.format_embed(links, "python-discord") + await message.channel.send(embed=resp) + + async def fetch_data(self, url: str) -> dict: + """Retrieve data as a dictionary.""" + async with self.bot.http_session.get(url) as r: + return await r.json() + @github_group.command(name="user", aliases=("userinfo",)) async def github_user_info(self, ctx: commands.Context, username: str) -> None: """Fetches a user's GitHub information.""" diff --git a/bot/exts/utilities/issues.py b/bot/exts/utilities/issues.py deleted file mode 100644 index b6d5a43e..00000000 --- a/bot/exts/utilities/issues.py +++ /dev/null @@ -1,277 +0,0 @@ -import logging -import random -import re -from dataclasses import dataclass -from typing import Optional, Union - -import discord -from discord.ext import commands - -from bot.bot import Bot -from bot.constants import ( - Categories, Channels, Colours, ERROR_REPLIES, Emojis, NEGATIVE_REPLIES, Tokens, WHITELISTED_CHANNELS -) -from bot.utils.decorators import whitelist_override -from bot.utils.extensions import invoke_help_command - -log = logging.getLogger(__name__) - -BAD_RESPONSE = { - 404: "Issue/pull request not located! Please enter a valid number!", - 403: "Rate limit has been hit! Please try again later!" -} -REQUEST_HEADERS = { - "Accept": "application/vnd.github.v3+json" -} - -REPOSITORY_ENDPOINT = "https://api.github.com/orgs/{org}/repos?per_page=100&type=public" -ISSUE_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/issues/{number}" -PR_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/pulls/{number}" - -if GITHUB_TOKEN := Tokens.github: - REQUEST_HEADERS["Authorization"] = f"token {GITHUB_TOKEN}" - -WHITELISTED_CATEGORIES = ( - Categories.development, Categories.devprojects, Categories.media, Categories.staff -) - -CODE_BLOCK_RE = re.compile( - r"^`([^`\n]+)`" # Inline codeblock - r"|```(.+?)```", # Multiline codeblock - re.DOTALL | re.MULTILINE -) - -# Maximum number of issues in one message -MAXIMUM_ISSUES = 5 - -# Regex used when looking for automatic linking in messages -# regex101 of current regex https://regex101.com/r/V2ji8M/6 -AUTOMATIC_REGEX = re.compile( - r"((?P[a-zA-Z0-9][a-zA-Z0-9\-]{1,39})\/)?(?P[\w\-\.]{1,100})#(?P[0-9]+)" -) - - -@dataclass -class FoundIssue: - """Dataclass representing an issue found by the regex.""" - - organisation: Optional[str] - repository: str - number: str - - def __hash__(self) -> int: - return hash((self.organisation, self.repository, self.number)) - - -@dataclass -class FetchError: - """Dataclass representing an error while fetching an issue.""" - - return_code: int - message: str - - -@dataclass -class IssueState: - """Dataclass representing the state of an issue.""" - - repository: str - number: int - url: str - title: str - emoji: str - - -class Issues(commands.Cog): - """Cog that allows users to retrieve issues from GitHub.""" - - def __init__(self, bot: Bot): - self.bot = bot - self.repos = [] - - @staticmethod - def remove_codeblocks(message: str) -> str: - """Remove any codeblock in a message.""" - return re.sub(CODE_BLOCK_RE, "", message) - - async def fetch_issues( - self, - number: int, - repository: str, - user: str - ) -> Union[IssueState, FetchError]: - """ - Retrieve an issue from a GitHub repository. - - Returns IssueState on success, FetchError on failure. - """ - url = ISSUE_ENDPOINT.format(user=user, repository=repository, number=number) - pulls_url = PR_ENDPOINT.format(user=user, repository=repository, number=number) - log.trace(f"Querying GH issues API: {url}") - - async with self.bot.http_session.get(url, headers=REQUEST_HEADERS) as r: - json_data = await r.json() - - if r.status == 403: - if r.headers.get("X-RateLimit-Remaining") == "0": - log.info(f"Ratelimit reached while fetching {url}") - return FetchError(403, "Ratelimit reached, please retry in a few minutes.") - return FetchError(403, "Cannot access issue.") - elif r.status in (404, 410): - return FetchError(r.status, "Issue not found.") - elif r.status != 200: - return FetchError(r.status, "Error while fetching issue.") - - # The initial API request is made to the issues API endpoint, which will return information - # if the issue or PR is present. However, the scope of information returned for PRs differs - # from issues: if the 'issues' key is present in the response then we can pull the data we - # need from the initial API call. - if "issues" in json_data["html_url"]: - if json_data.get("state") == "open": - emoji = Emojis.issue_open - else: - emoji = Emojis.issue_closed - - # If the 'issues' key is not contained in the API response and there is no error code, then - # we know that a PR has been requested and a call to the pulls API endpoint is necessary - # to get the desired information for the PR. - else: - log.trace(f"PR provided, querying GH pulls API for additional information: {pulls_url}") - async with self.bot.http_session.get(pulls_url) as p: - pull_data = await p.json() - if pull_data["draft"]: - emoji = Emojis.pull_request_draft - elif pull_data["state"] == "open": - emoji = Emojis.pull_request_open - # When 'merged_at' is not None, this means that the state of the PR is merged - elif pull_data["merged_at"] is not None: - emoji = Emojis.pull_request_merged - else: - emoji = Emojis.pull_request_closed - - issue_url = json_data.get("html_url") - - return IssueState(repository, number, issue_url, json_data.get("title", ""), emoji) - - @staticmethod - def format_embed( - results: list[Union[IssueState, FetchError]], - user: str, - repository: Optional[str] = None - ) -> discord.Embed: - """Take a list of IssueState or FetchError and format a Discord embed for them.""" - description_list = [] - - for result in results: - if isinstance(result, IssueState): - description_list.append(f"{result.emoji} [{result.title}]({result.url})") - elif isinstance(result, FetchError): - description_list.append(f":x: [{result.return_code}] {result.message}") - - resp = discord.Embed( - colour=Colours.bright_green, - description="\n".join(description_list) - ) - - embed_url = f"https://github.com/{user}/{repository}" if repository else f"https://github.com/{user}" - resp.set_author(name="GitHub", url=embed_url) - return resp - - @whitelist_override(channels=WHITELISTED_CHANNELS, categories=WHITELISTED_CATEGORIES) - @commands.command(aliases=("issues", "pr", "prs")) - async def issue( - self, - ctx: commands.Context, - numbers: commands.Greedy[int], - repository: str = "sir-lancebot", - user: str = "python-discord" - ) -> None: - """Command to retrieve issue(s) from a GitHub repository.""" - # Remove duplicates - numbers = set(numbers) - - err_message = None - if not numbers: - err_message = "You must have at least one issue/PR!" - - elif len(numbers) > MAXIMUM_ISSUES: - err_message = f"Too many issues/PRs! (maximum of {MAXIMUM_ISSUES})" - - # If there's an error with command invocation then send an error embed - if err_message is not None: - err_embed = discord.Embed( - title=random.choice(ERROR_REPLIES), - color=Colours.soft_red, - description=err_message - ) - await ctx.send(embed=err_embed) - await invoke_help_command(ctx) - return - - results = [await self.fetch_issues(number, repository, user) for number in numbers] - await ctx.send(embed=self.format_embed(results, user, repository)) - - @commands.Cog.listener() - async def on_message(self, message: discord.Message) -> None: - """ - Automatic issue linking. - - Listener to retrieve issue(s) from a GitHub repository using automatic linking if matching /#. - """ - # Ignore bots - if message.author.bot: - return - - issues = [ - FoundIssue(*match.group("org", "repo", "number")) - for match in AUTOMATIC_REGEX.finditer(self.remove_codeblocks(message.content)) - ] - links = [] - - if issues: - # Block this from working in DMs - if not message.guild: - await message.channel.send( - embed=discord.Embed( - title=random.choice(NEGATIVE_REPLIES), - description=( - "You can't retrieve issues from DMs. " - f"Try again in <#{Channels.community_bot_commands}>" - ), - colour=Colours.soft_red - ) - ) - return - - log.trace(f"Found {issues = }") - # Remove duplicates - issues = set(issues) - - if len(issues) > MAXIMUM_ISSUES: - embed = discord.Embed( - title=random.choice(ERROR_REPLIES), - color=Colours.soft_red, - description=f"Too many issues/PRs! (maximum of {MAXIMUM_ISSUES})" - ) - await message.channel.send(embed=embed, delete_after=5) - return - - for repo_issue in issues: - result = await self.fetch_issues( - int(repo_issue.number), - repo_issue.repository, - repo_issue.organisation or "python-discord" - ) - if isinstance(result, IssueState): - links.append(result) - - if not links: - return - - resp = self.format_embed(links, "python-discord") - await message.channel.send(embed=resp) - - -def setup(bot: Bot) -> None: - """Load the Issues cog.""" - bot.add_cog(Issues(bot)) -- cgit v1.2.3 From ba10b9b6525beac6637e5a13ead03fb018751201 Mon Sep 17 00:00:00 2001 From: ToxicKidz Date: Sat, 31 Jul 2021 18:42:36 -0400 Subject: chore: Remove the .issue command --- bot/exts/utilities/githubinfo.py | 37 ------------------------------------- 1 file changed, 37 deletions(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index f0820731..b0b327b6 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -18,11 +18,8 @@ from bot.constants import ( Emojis, NEGATIVE_REPLIES, Tokens, - WHITELISTED_CHANNELS ) from bot.exts.core.extensions import invoke_help_command -from bot.utils.decorators import whitelist_override - log = logging.getLogger(__name__) GITHUB_API_URL = "https://api.github.com" @@ -195,40 +192,6 @@ class GithubInfo(commands.Cog): if ctx.invoked_subcommand is None: await invoke_help_command(ctx) - @whitelist_override(channels=WHITELISTED_CHANNELS, categories=WHITELISTED_CATEGORIES) - @github_group.command(aliases=("pr", "issues", "prs"), root_aliases=("issue", "pr")) - async def issue( - self, - ctx: commands.Context, - numbers: commands.Greedy[int], - repository: str = "sir-lancebot", - user: str = "python-discord" - ) -> None: - """Command to retrieve issue(s) from a GitHub repository.""" - # Remove duplicates - numbers = set(numbers) - - err_message = None - if not numbers: - err_message = "You must have at least one issue/PR!" - - elif len(numbers) > MAXIMUM_ISSUES: - err_message = f"Too many issues/PRs! (maximum of {MAXIMUM_ISSUES})" - - # If there's an error with command invocation then send an error embed - if err_message is not None: - err_embed = discord.Embed( - title=random.choice(ERROR_REPLIES), - color=Colours.soft_red, - description=err_message - ) - await ctx.send(embed=err_embed) - await invoke_help_command(ctx) - return - - results = [await self.fetch_issues(number, repository, user) for number in numbers] - await ctx.send(embed=self.format_embed(results, user, repository)) - @commands.Cog.listener() async def on_message(self, message: discord.Message) -> None: """ -- cgit v1.2.3 From 101001bfabae0cef37cf36ebbf4420f9d80736e4 Mon Sep 17 00:00:00 2001 From: ToxicKidz Date: Sat, 18 Sep 2021 10:28:35 -0400 Subject: chore: Apply suggested changes --- bot/exts/utilities/githubinfo.py | 90 ++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 55 deletions(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index b0b327b6..c9a65668 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -4,15 +4,15 @@ import re import typing as t from dataclasses import dataclass from datetime import datetime -from urllib.parse import quote, quote_plus +from urllib.parse import quote import discord +from aiohttp import ClientResponse from discord.ext import commands from bot.bot import Bot from bot.constants import ( Categories, - Channels, Colours, ERROR_REPLIES, Emojis, @@ -20,14 +20,11 @@ from bot.constants import ( Tokens, ) from bot.exts.core.extensions import invoke_help_command + log = logging.getLogger(__name__) GITHUB_API_URL = "https://api.github.com" -BAD_RESPONSE = { - 404: "Issue/pull request not located! Please enter a valid number!", - 403: "Rate limit has been hit! Please try again later!" -} REQUEST_HEADERS = { "Accept": "application/vnd.github.v3+json" } @@ -102,11 +99,11 @@ class GithubInfo(commands.Cog): """Remove any codeblock in a message.""" return CODE_BLOCK_RE.sub("", message) - async def fetch_issues( - self, - number: int, - repository: str, - user: str + async def fetch_issue( + self, + number: int, + repository: str, + user: str ) -> t.Union[IssueState, FetchError]: """ Retrieve an issue from a GitHub repository. @@ -117,8 +114,7 @@ class GithubInfo(commands.Cog): pulls_url = PR_ENDPOINT.format(user=user, repository=repository, number=number) log.trace(f"Querying GH issues API: {url}") - async with self.bot.http_session.get(url, headers=REQUEST_HEADERS) as r: - json_data = await r.json() + json_data, r = await self.fetch_data(url) if r.status == 403: if r.headers.get("X-RateLimit-Remaining") == "0": @@ -145,17 +141,17 @@ class GithubInfo(commands.Cog): # to get the desired information for the PR. else: log.trace(f"PR provided, querying GH pulls API for additional information: {pulls_url}") - async with self.bot.http_session.get(pulls_url) as p: - pull_data = await p.json() - if pull_data["draft"]: - emoji = Emojis.pull_request_draft - elif pull_data["state"] == "open": - emoji = Emojis.pull_request_open - # When 'merged_at' is not None, this means that the state of the PR is merged - elif pull_data["merged_at"] is not None: - emoji = Emojis.pull_request_merged - else: - emoji = Emojis.pull_request_closed + + pull_data, _ = await self.fetch_data(pulls_url) + if pull_data["draft"]: + emoji = Emojis.pull_request_draft + elif pull_data["state"] == "open": + emoji = Emojis.pull_request_open + # When 'merged_at' is not None, this means that the state of the PR is merged + elif pull_data["merged_at"] is not None: + emoji = Emojis.pull_request_merged + else: + emoji = Emojis.pull_request_closed issue_url = json_data.get("html_url") @@ -163,9 +159,7 @@ class GithubInfo(commands.Cog): @staticmethod def format_embed( - results: t.List[t.Union[IssueState, FetchError]], - user: str, - repository: t.Optional[str] = None + results: t.List[t.Union[IssueState, FetchError]] ) -> discord.Embed: """Take a list of IssueState or FetchError and format a Discord embed for them.""" description_list = [] @@ -181,8 +175,7 @@ class GithubInfo(commands.Cog): description="\n".join(description_list) ) - embed_url = f"https://github.com/{user}/{repository}" if repository else f"https://github.com/{user}" - resp.set_author(name="GitHub", url=embed_url) + resp.set_author(name="GitHub") return resp @commands.group(name="github", aliases=("gh", "git")) @@ -212,16 +205,6 @@ class GithubInfo(commands.Cog): if issues: # Block this from working in DMs if not message.guild: - await message.channel.send( - embed=discord.Embed( - title=random.choice(NEGATIVE_REPLIES), - description=( - "You can't retrieve issues from DMs. " - f"Try again in <#{Channels.community_bot_commands}>" - ), - colour=Colours.soft_red - ) - ) return log.trace(f"Found {issues = }") @@ -238,7 +221,7 @@ class GithubInfo(commands.Cog): return for repo_issue in issues: - result = await self.fetch_issues( + result = await self.fetch_issue( int(repo_issue.number), repo_issue.repository, repo_issue.organisation or "python-discord" @@ -249,19 +232,19 @@ class GithubInfo(commands.Cog): if not links: return - resp = self.format_embed(links, "python-discord") + resp = self.format_embed(links) await message.channel.send(embed=resp) - async def fetch_data(self, url: str) -> dict: - """Retrieve data as a dictionary.""" - async with self.bot.http_session.get(url) as r: - return await r.json() + async def fetch_data(self, url: str) -> tuple[dict[str], ClientResponse]: + """Retrieve data as a dictionary and the response in a tuple.""" + async with self.bot.http_session.get(url, heades=REQUEST_HEADERS) as r: + return await r.json(), r @github_group.command(name="user", aliases=("userinfo",)) async def github_user_info(self, ctx: commands.Context, username: str) -> None: """Fetches a user's GitHub information.""" async with ctx.typing(): - user_data = await self.fetch_data(f"{GITHUB_API_URL}/users/{quote_plus(username)}") + user_data, _ = await self.fetch_data(f"{GITHUB_API_URL}/users/{username}") # User_data will not have a message key if the user exists if "message" in user_data: @@ -274,7 +257,7 @@ class GithubInfo(commands.Cog): await ctx.send(embed=embed) return - org_data = await self.fetch_data(user_data["organizations_url"]) + org_data, _ = await self.fetch_data(user_data["organizations_url"]) orgs = [f"[{org['login']}](https://github.com/{org['login']})" for org in org_data] orgs_to_add = " | ".join(orgs) @@ -290,8 +273,8 @@ class GithubInfo(commands.Cog): embed = discord.Embed( title=f"`{user_data['login']}`'s GitHub profile info", - description=f"```\n{user_data['bio']}\n```\n" if user_data["bio"] else "", - colour=discord.Colour.og_blurple(), + description=f"```{user_data['bio']}```\n" if user_data["bio"] else "", + colour=discord.Colour.blurple(), url=user_data["html_url"], timestamp=datetime.strptime(user_data["created_at"], "%Y-%m-%dT%H:%M:%SZ") ) @@ -315,10 +298,7 @@ class GithubInfo(commands.Cog): ) if user_data["type"] == "User": - embed.add_field( - name="Gists", - value=f"[{gists}](https://gist.github.com/{quote_plus(username, safe='')})" - ) + embed.add_field(name="Gists", value=f"[{gists}](https://gist.github.com/{quote(username, safe='')})") embed.add_field( name=f"Organization{'s' if len(orgs)!=1 else ''}", @@ -347,7 +327,7 @@ class GithubInfo(commands.Cog): return async with ctx.typing(): - repo_data = await self.fetch_data(f"{GITHUB_API_URL}/repos/{quote(repo)}") + repo_data, _ = await self.fetch_data(f"{GITHUB_API_URL}/repos/{quote(repo)}") # There won't be a message key if this repo exists if "message" in repo_data: @@ -363,7 +343,7 @@ class GithubInfo(commands.Cog): embed = discord.Embed( title=repo_data["name"], description=repo_data["description"], - colour=discord.Colour.og_blurple(), + colour=discord.Colour.blurple(), url=repo_data["html_url"] ) -- cgit v1.2.3 From cb4114823b91056d9b552d3e75c3c8ca9e879da7 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Thu, 2 Dec 2021 11:33:57 +0000 Subject: Make dataclasses hashable, and fix kwarg spelling error --- bot/exts/utilities/githubinfo.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index c9a65668..ee05497a 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -11,14 +11,7 @@ from aiohttp import ClientResponse from discord.ext import commands from bot.bot import Bot -from bot.constants import ( - Categories, - Colours, - ERROR_REPLIES, - Emojis, - NEGATIVE_REPLIES, - Tokens, -) +from bot.constants import Categories, Colours, ERROR_REPLIES, Emojis, NEGATIVE_REPLIES, Tokens from bot.exts.core.extensions import invoke_help_command log = logging.getLogger(__name__) @@ -41,7 +34,7 @@ WHITELISTED_CATEGORIES = ( ) CODE_BLOCK_RE = re.compile( - r"^`([^`\n]+)`" # Inline codeblock + r"^`([^`\n]+)`" # Inline codeblock r"|```(.+?)```", # Multiline codeblock re.DOTALL | re.MULTILINE ) @@ -56,7 +49,7 @@ AUTOMATIC_REGEX = re.compile( ) -@dataclass +@dataclass(eq=True, frozen=True) class FoundIssue: """Dataclass representing an issue found by the regex.""" @@ -64,11 +57,8 @@ class FoundIssue: repository: str number: str - def __hash__(self) -> int: - return hash((self.organisation, self.repository, self.number)) - -@dataclass +@dataclass(eq=True, frozen=True) class FetchError: """Dataclass representing an error while fetching an issue.""" @@ -76,7 +66,7 @@ class FetchError: message: str -@dataclass +@dataclass(eq=True, frozen=True) class IssueState: """Dataclass representing the state of an issue.""" @@ -237,7 +227,7 @@ class GithubInfo(commands.Cog): async def fetch_data(self, url: str) -> tuple[dict[str], ClientResponse]: """Retrieve data as a dictionary and the response in a tuple.""" - async with self.bot.http_session.get(url, heades=REQUEST_HEADERS) as r: + async with self.bot.http_session.get(url, headers=REQUEST_HEADERS) as r: return await r.json(), r @github_group.command(name="user", aliases=("userinfo",)) -- cgit v1.2.3 From cdd4067ad7497b48440074494aa4de15a908f7d5 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Thu, 2 Dec 2021 11:42:04 +0000 Subject: use og_blurple in issue embed for consistency --- bot/exts/utilities/githubinfo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index ee05497a..b7dbe64d 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -264,7 +264,7 @@ class GithubInfo(commands.Cog): embed = discord.Embed( title=f"`{user_data['login']}`'s GitHub profile info", description=f"```{user_data['bio']}```\n" if user_data["bio"] else "", - colour=discord.Colour.blurple(), + colour=discord.Colour.og_blurple(), url=user_data["html_url"], timestamp=datetime.strptime(user_data["created_at"], "%Y-%m-%dT%H:%M:%SZ") ) @@ -333,7 +333,7 @@ class GithubInfo(commands.Cog): embed = discord.Embed( title=repo_data["name"], description=repo_data["description"], - colour=discord.Colour.blurple(), + colour=discord.Colour.og_blurple(), url=repo_data["html_url"] ) -- cgit v1.2.3 From 4a06ec80cc9e78294475ab23c41649d47798cca4 Mon Sep 17 00:00:00 2001 From: Ben Soyka Date: Thu, 2 Dec 2021 15:33:29 -0700 Subject: Note that only one layer of quotes is stripped in .aoc lb --- bot/exts/events/advent_of_code/_cog.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 0f6739fc..9ae2d059 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -224,7 +224,9 @@ class AdventOfCode(commands.Cog): Additionally you can specify an `aoc_name` that will append the specified profile's personal stats to the top of the leaderboard """ - # Strip quotes from the self placement name if needed (e.g. "My Name" -> My Name) + # Strip quotes from the AoC username if needed (e.g. "My Name" -> My Name) + # Note: only strips one layer of quotes to allow names with quotes at the start and end + # e.g. ""My Name"" -> "My Name" if aoc_name and aoc_name.startswith('"') and aoc_name.endswith('"'): aoc_name = aoc_name[1:-1] -- cgit v1.2.3 From 914e4abe1f168167d8b1126adeb1bc27bff28e6d Mon Sep 17 00:00:00 2001 From: Ben Soyka Date: Thu, 2 Dec 2021 15:34:45 -0700 Subject: Note why .aoc lb strips quotes from names --- bot/exts/events/advent_of_code/_cog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 9ae2d059..3f9f5787 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -225,6 +225,7 @@ class AdventOfCode(commands.Cog): specified profile's personal stats to the top of the leaderboard """ # Strip quotes from the AoC username if needed (e.g. "My Name" -> My Name) + # This is to keep compatibility with those already used to wrapping the AoC name in quotes # Note: only strips one layer of quotes to allow names with quotes at the start and end # e.g. ""My Name"" -> "My Name" if aoc_name and aoc_name.startswith('"') and aoc_name.endswith('"'): -- cgit v1.2.3 From e584697e0809e60cda899b2148a1efcff993177a Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 3 Dec 2021 18:38:42 +0000 Subject: Move logging and remove unused varibales in GitHubInfo cog --- bot/exts/utilities/githubinfo.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index b7dbe64d..009e0fad 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -11,7 +11,7 @@ from aiohttp import ClientResponse from discord.ext import commands from bot.bot import Bot -from bot.constants import Categories, Colours, ERROR_REPLIES, Emojis, NEGATIVE_REPLIES, Tokens +from bot.constants import Colours, ERROR_REPLIES, Emojis, NEGATIVE_REPLIES, Tokens from bot.exts.core.extensions import invoke_help_command log = logging.getLogger(__name__) @@ -26,12 +26,8 @@ REPOSITORY_ENDPOINT = "https://api.github.com/orgs/{org}/repos?per_page=100&type ISSUE_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/issues/{number}" PR_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/pulls/{number}" -if GITHUB_TOKEN := Tokens.github: - REQUEST_HEADERS["Authorization"] = f"token {GITHUB_TOKEN}" - -WHITELISTED_CATEGORIES = ( - Categories.development, Categories.devprojects, Categories.media, Categories.staff -) +if Tokens.github: + REQUEST_HEADERS["Authorization"] = f"token {Tokens.github}" CODE_BLOCK_RE = re.compile( r"^`([^`\n]+)`" # Inline codeblock @@ -102,7 +98,6 @@ class GithubInfo(commands.Cog): """ url = ISSUE_ENDPOINT.format(user=user, repository=repository, number=number) pulls_url = PR_ENDPOINT.format(user=user, repository=repository, number=number) - log.trace(f"Querying GH issues API: {url}") json_data, r = await self.fetch_data(url) @@ -130,8 +125,6 @@ class GithubInfo(commands.Cog): # we know that a PR has been requested and a call to the pulls API endpoint is necessary # to get the desired information for the PR. else: - log.trace(f"PR provided, querying GH pulls API for additional information: {pulls_url}") - pull_data, _ = await self.fetch_data(pulls_url) if pull_data["draft"]: emoji = Emojis.pull_request_draft @@ -227,6 +220,7 @@ class GithubInfo(commands.Cog): async def fetch_data(self, url: str) -> tuple[dict[str], ClientResponse]: """Retrieve data as a dictionary and the response in a tuple.""" + log.trace(f"Querying GH issues API: {url}") async with self.bot.http_session.get(url, headers=REQUEST_HEADERS) as r: return await r.json(), r -- cgit v1.2.3 From 7cb5c5517043c0006b001c15373b664b86cc6b43 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Fri, 3 Dec 2021 17:28:15 -0500 Subject: feat: implement moving commands add exceptions and handler for commands that move locations --- bot/constants.py | 3 +++ bot/exts/core/error_handler.py | 10 +++++++++- bot/utils/exceptions.py | 7 +++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/bot/constants.py b/bot/constants.py index f4b1cab0..854fbe55 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -23,6 +23,7 @@ __all__ = ( "Reddit", "RedisConfig", "RedirectOutput", + "PYTHON_PREFIX" "MODERATION_ROLES", "STAFF_ROLES", "WHITELISTED_CHANNELS", @@ -34,6 +35,8 @@ __all__ = ( log = logging.getLogger(__name__) +PYTHON_PREFIX = "!" + @dataclasses.dataclass class AdventOfCodeLeaderboard: id: str diff --git a/bot/exts/core/error_handler.py b/bot/exts/core/error_handler.py index fd2123e7..676a1e70 100644 --- a/bot/exts/core/error_handler.py +++ b/bot/exts/core/error_handler.py @@ -12,7 +12,7 @@ from sentry_sdk import push_scope from bot.bot import Bot from bot.constants import Channels, Colours, ERROR_REPLIES, NEGATIVE_REPLIES, RedirectOutput from bot.utils.decorators import InChannelCheckFailure, InMonthCheckFailure -from bot.utils.exceptions import APIError, UserNotPlayingError +from bot.utils.exceptions import APIError, MovedCommandError, UserNotPlayingError log = logging.getLogger(__name__) @@ -130,6 +130,14 @@ class CommandErrorHandler(commands.Cog): ) return + if isinstance(error, MovedCommandError): + description = ( + f"This command, `{ctx.prefix}{ctx.command.qualified_name}` has moved to `{error.new_command_name}`.\n" + f"Please use `{error.new_command_name}` instead." + ) + await ctx.send(embed=self.error_embed(description, NEGATIVE_REPLIES)) + return + with push_scope() as scope: scope.user = { "id": ctx.author.id, diff --git a/bot/utils/exceptions.py b/bot/utils/exceptions.py index bf0e5813..3cd96325 100644 --- a/bot/utils/exceptions.py +++ b/bot/utils/exceptions.py @@ -15,3 +15,10 @@ class APIError(Exception): self.api = api self.status_code = status_code self.error_msg = error_msg + + +class MovedCommandError(Exception): + """Raised when a command has moved locations.""" + + def __init__(self, new_command_name: str): + self.new_command_name = new_command_name -- cgit v1.2.3 From c7f62d3d63b4fe84c8f96bf38b66198838fdb538 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Fri, 3 Dec 2021 17:28:39 -0500 Subject: yank lovefest role management commands --- bot/exts/holidays/valentines/be_my_valentine.py | 31 +++++++++---------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/bot/exts/holidays/valentines/be_my_valentine.py b/bot/exts/holidays/valentines/be_my_valentine.py index 4d454c3a..a9a4731f 100644 --- a/bot/exts/holidays/valentines/be_my_valentine.py +++ b/bot/exts/holidays/valentines/be_my_valentine.py @@ -7,14 +7,17 @@ import discord from discord.ext import commands from bot.bot import Bot -from bot.constants import Channels, Colours, Lovefest, Month +from bot.constants import Channels, Colours, Lovefest, Month, PYTHON_PREFIX from bot.utils.decorators import in_month +from bot.utils.exceptions import MovedCommandError from bot.utils.extensions import invoke_help_command log = logging.getLogger(__name__) HEART_EMOJIS = [":heart:", ":gift_heart:", ":revolving_hearts:", ":sparkling_heart:", ":two_hearts:"] +MOVED_COMMAND = f"{PYTHON_PREFIX}subscribe" + class BeMyValentine(commands.Cog): """A cog that sends Valentines to other users!""" @@ -30,7 +33,7 @@ class BeMyValentine(commands.Cog): return loads(p.read_text("utf8")) @in_month(Month.FEBRUARY) - @commands.group(name="lovefest") + @commands.group(name="lovefest", help=f"This command has been moved to {MOVED_COMMAND}") async def lovefest_role(self, ctx: commands.Context) -> None: """ Subscribe or unsubscribe from the lovefest role. @@ -43,27 +46,15 @@ class BeMyValentine(commands.Cog): if not ctx.invoked_subcommand: await invoke_help_command(ctx) - @lovefest_role.command(name="sub") + @lovefest_role.command(name="sub", help=f"This command has been moved to {MOVED_COMMAND}") async def add_role(self, ctx: commands.Context) -> None: - """Adds the lovefest role.""" - user = ctx.author - role = ctx.guild.get_role(Lovefest.role_id) - if role not in ctx.author.roles: - await user.add_roles(role) - await ctx.send("The Lovefest role has been added !") - else: - await ctx.send("You already have the role !") + """NOTE: This command has been moved to bot.""" + raise MovedCommandError(MOVED_COMMAND) - @lovefest_role.command(name="unsub") + @lovefest_role.command(name="unsub", help=f"This command has been moved to {MOVED_COMMAND}") async def remove_role(self, ctx: commands.Context) -> None: - """Removes the lovefest role.""" - user = ctx.author - role = ctx.guild.get_role(Lovefest.role_id) - if role not in ctx.author.roles: - await ctx.send("You dont have the lovefest role.") - else: - await user.remove_roles(role) - await ctx.send("The lovefest role has been successfully removed!") + """NOTE: This command has been moved to bot.""" + raise MovedCommandError(MOVED_COMMAND) @commands.cooldown(1, 1800, commands.BucketType.user) @commands.group(name="bemyvalentine", invoke_without_command=True) -- cgit v1.2.3 From 2d0760821b629bcb3269534ed46c0a9af4026264 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Fri, 3 Dec 2021 17:19:57 -0500 Subject: chore: remove subcommands entirely --- bot/exts/holidays/valentines/be_my_valentine.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/bot/exts/holidays/valentines/be_my_valentine.py b/bot/exts/holidays/valentines/be_my_valentine.py index a9a4731f..1e13e424 100644 --- a/bot/exts/holidays/valentines/be_my_valentine.py +++ b/bot/exts/holidays/valentines/be_my_valentine.py @@ -10,7 +10,6 @@ from bot.bot import Bot from bot.constants import Channels, Colours, Lovefest, Month, PYTHON_PREFIX from bot.utils.decorators import in_month from bot.utils.exceptions import MovedCommandError -from bot.utils.extensions import invoke_help_command log = logging.getLogger(__name__) @@ -33,7 +32,7 @@ class BeMyValentine(commands.Cog): return loads(p.read_text("utf8")) @in_month(Month.FEBRUARY) - @commands.group(name="lovefest", help=f"This command has been moved to {MOVED_COMMAND}") + @commands.command(name="lovefest", help=f"NOTE: This command has been moved to {MOVED_COMMAND}") async def lovefest_role(self, ctx: commands.Context) -> None: """ Subscribe or unsubscribe from the lovefest role. @@ -43,17 +42,6 @@ class BeMyValentine(commands.Cog): 1) use the command \".lovefest sub\" to get the lovefest role. 2) use the command \".lovefest unsub\" to get rid of the lovefest role. """ - if not ctx.invoked_subcommand: - await invoke_help_command(ctx) - - @lovefest_role.command(name="sub", help=f"This command has been moved to {MOVED_COMMAND}") - async def add_role(self, ctx: commands.Context) -> None: - """NOTE: This command has been moved to bot.""" - raise MovedCommandError(MOVED_COMMAND) - - @lovefest_role.command(name="unsub", help=f"This command has been moved to {MOVED_COMMAND}") - async def remove_role(self, ctx: commands.Context) -> None: - """NOTE: This command has been moved to bot.""" raise MovedCommandError(MOVED_COMMAND) @commands.cooldown(1, 1800, commands.BucketType.user) -- cgit v1.2.3 From cfb2cc69287f0aefab0937d0e8e8f99811d50948 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Fri, 3 Dec 2021 18:26:33 -0500 Subject: chore: update lovefest docstring to reflect deprecation --- bot/exts/holidays/valentines/be_my_valentine.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/bot/exts/holidays/valentines/be_my_valentine.py b/bot/exts/holidays/valentines/be_my_valentine.py index 1e13e424..1572d474 100644 --- a/bot/exts/holidays/valentines/be_my_valentine.py +++ b/bot/exts/holidays/valentines/be_my_valentine.py @@ -35,12 +35,9 @@ class BeMyValentine(commands.Cog): @commands.command(name="lovefest", help=f"NOTE: This command has been moved to {MOVED_COMMAND}") async def lovefest_role(self, ctx: commands.Context) -> None: """ - Subscribe or unsubscribe from the lovefest role. + Deprecated lovefest role command. - The lovefest role makes you eligible to receive anonymous valentines from other users. - - 1) use the command \".lovefest sub\" to get the lovefest role. - 2) use the command \".lovefest unsub\" to get rid of the lovefest role. + This command has been moved to bot, and will be removed in the future. """ raise MovedCommandError(MOVED_COMMAND) -- cgit v1.2.3 From 846c34fd8988eaede08186fa9fb342213dc85585 Mon Sep 17 00:00:00 2001 From: Janine vN Date: Fri, 3 Dec 2021 18:46:39 -0500 Subject: Remove unneeded check and add comments Removes the unneeded check for if the cache is empty. Also adds a seconds comment about the format of the contents of the Redis cache. --- bot/exts/events/advent_of_code/_cog.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 1b1cd9f8..8d87b5bc 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -30,6 +30,7 @@ AOC_WHITELIST = AOC_WHITELIST_RESTRICTED + (Channels.advent_of_code,) class AdventOfCode(commands.Cog): """Advent of Code festivities! Ho Ho Ho!""" + # Redis Cache for linking Discord IDs to Advent of Code usernames account_links = RedisCache() def __init__(self, bot: Bot): @@ -186,23 +187,9 @@ class AdventOfCode(commands.Cog): """ Link your Discord Account to your Advent of Code name. - Stored in a Redis Cache, Discord ID: Advent of Code Name + Stored in a Redis Cache with the format of `Discord ID: Advent of Code Name` """ cache_items = await self.account_links.items() - - # A short circuit in case the cache is empty - if len(cache_items) == 0 and aoc_name: - log.info(f"{ctx.author} ({ctx.author.id}) is now linked to {aoc_name}.") - await self.account_links.set(ctx.author.id, aoc_name) - await ctx.reply(f"You have linked your Discord ID to {aoc_name}.") - return - elif len(cache_items) == 0: - await ctx.reply( - "You have not linked an Advent of Code account." - "Please re-run the command with one specified." - ) - return - cache_aoc_name = [value for _, value in cache_items] if aoc_name: -- cgit v1.2.3 From 1ac965f67fea8e5dd0e0f026aca27e3368f4e44e Mon Sep 17 00:00:00 2001 From: Janine vN Date: Fri, 3 Dec 2021 20:50:17 -0500 Subject: Add unlink AoC command Adds the ability for the user to unlink their advent of code name. It will delete the entry in the cache if it exists. --- bot/exts/events/advent_of_code/_cog.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 8d87b5bc..7f904f6b 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -228,6 +228,27 @@ class AdventOfCode(commands.Cog): " Please re-run the command with one specified." ) + @in_month(Month.NOVEMBER, Month.DECEMBER) + @adventofcode_group.command( + name="unlink", + aliases=("disconnect",), + brief="Tie your Discord account with your Advent of Code name." + ) + @whitelist_override(channels=AOC_WHITELIST) + async def aoc_unlink_account(self, ctx: commands.Context) -> None: + """ + Unlink your Discord ID with your Advent of Code leaderboard name. + + Deletes the entry that was Stored in the Redis cache. + """ + if aoc_cache_name := await self.account_links.get(ctx.author.id): + log.info(f"Unlinking {ctx.author} ({ctx.author.id}) from Advent of Code account {aoc_cache_name}") + await self.account_links.delete(ctx.author.id) + await ctx.reply(f"We have removed the link between your Discord ID and {aoc_cache_name}.") + else: + log.info(f"Attempted to unlink {ctx.author} ({ctx.author.id}), but not link was found.") + await ctx.reply("You don't have an Advent of Code account linked.") + @in_month(Month.DECEMBER) @adventofcode_group.command( name="dayandstar", -- cgit v1.2.3 From 00e3fe5bede6f8310405e143179530df0ad3ca95 Mon Sep 17 00:00:00 2001 From: Janine vN Date: Fri, 3 Dec 2021 20:53:59 -0500 Subject: Adjust wording on log statements to present tense --- bot/exts/events/advent_of_code/_cog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 7f904f6b..b7d26cb9 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -210,12 +210,12 @@ class AdventOfCode(commands.Cog): # Update an existing link if old_aoc_name := await self.account_links.get(ctx.author.id): - log.info(f"{ctx.author} ({ctx.author.id}) has changed their link from {old_aoc_name} to {aoc_name}.") + log.info(f"Changing link for{ctx.author} ({ctx.author.id}) from {old_aoc_name} to {aoc_name}.") await self.account_links.set(ctx.author.id, aoc_name) await ctx.reply(f"Your linked account has been changed to {aoc_name}.") else: # Create a new link - log.info(f"{ctx.author} ({ctx.author.id}) is now linked to {aoc_name}.") + log.info(f"Linking {ctx.author} ({ctx.author.id}) to account {aoc_name}.") await self.account_links.set(ctx.author.id, aoc_name) await ctx.reply(f"You have linked your Discord ID to {aoc_name}.") else: @@ -246,7 +246,7 @@ class AdventOfCode(commands.Cog): await self.account_links.delete(ctx.author.id) await ctx.reply(f"We have removed the link between your Discord ID and {aoc_cache_name}.") else: - log.info(f"Attempted to unlink {ctx.author} ({ctx.author.id}), but not link was found.") + log.info(f"Attempted to unlink {ctx.author} ({ctx.author.id}), but no link was found.") await ctx.reply("You don't have an Advent of Code account linked.") @in_month(Month.DECEMBER) -- cgit v1.2.3 From b3a7c79ffb1f4220bad8ca3814d594a9b1b00826 Mon Sep 17 00:00:00 2001 From: Janine vN Date: Fri, 3 Dec 2021 21:02:28 -0500 Subject: Make aoc_name a keyword arguemnt to accept spaces Makes `aoc_name` in the link command a keyword only argument. This allows users to link accounts with spaces in the name without having to use quotes. --- bot/exts/events/advent_of_code/_cog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index b7d26cb9..55fd0ac6 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -183,7 +183,7 @@ class AdventOfCode(commands.Cog): brief="Tie your Discord account with your Advent of Code name." ) @whitelist_override(channels=AOC_WHITELIST) - async def aoc_link_account(self, ctx: commands.Context, aoc_name: str = None) -> None: + async def aoc_link_account(self, ctx: commands.Context, *, aoc_name: str = None) -> None: """ Link your Discord Account to your Advent of Code name. -- cgit v1.2.3 From b134010e1682a40d2aa9e3ce893c274388896680 Mon Sep 17 00:00:00 2001 From: Janine vN Date: Fri, 3 Dec 2021 21:29:01 -0500 Subject: Adjust `.aoc lb` to use linked account in cache If the user has not supplied a name to use for the leaderboard, then code will check if they have an account linked. If they do, it will use the linked account in the leaderboard to show placement. --- bot/exts/events/advent_of_code/_cog.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 16176c69..49b604ab 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -311,6 +311,10 @@ class AdventOfCode(commands.Cog): if aoc_name and aoc_name.startswith('"') and aoc_name.endswith('"'): aoc_name = aoc_name[1:-1] + # Check if an advent of code account is linked in the Redis Cache if aoc_name is not given + if (aoc_cache_name := await self.account_links.get(ctx.author.id)) and aoc_name is None: + aoc_name = aoc_cache_name + async with ctx.typing(): try: leaderboard = await _helpers.fetch_leaderboard(self_placement_name=aoc_name) @@ -321,7 +325,7 @@ class AdventOfCode(commands.Cog): number_of_participants = leaderboard["number_of_participants"] top_count = min(AocConfig.leaderboard_displayed_members, number_of_participants) - self_placement_header = "(and your personal stats compared to the top 10)" if aoc_name else "" + self_placement_header = " (and your personal stats compared to the top 10)" if aoc_name else "" header = f"Here's our current top {top_count}{self_placement_header}! {Emojis.christmas_tree * 3}" table = "```\n" \ f"{leaderboard['placement_leaderboard'] if aoc_name else leaderboard['top_leaderboard']}" \ -- cgit v1.2.3 From f4324a0f447a7791743ce9a5c4f0957f32738b78 Mon Sep 17 00:00:00 2001 From: Janine vN Date: Sat, 4 Dec 2021 10:55:49 -0500 Subject: Adjust variable name for clarity and add space --- bot/exts/events/advent_of_code/_cog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 49b604ab..52254ea1 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -193,14 +193,14 @@ class AdventOfCode(commands.Cog): Stored in a Redis Cache with the format of `Discord ID: Advent of Code Name` """ cache_items = await self.account_links.items() - cache_aoc_name = [value for _, value in cache_items] + cache_aoc_names = [value for _, value in cache_items] if aoc_name: # Let's check the current values in the cache to make sure it isn't already tied to a different account if aoc_name == await self.account_links.get(ctx.author.id): await ctx.reply(f"{aoc_name} is already tied to your account.") return - elif aoc_name in cache_aoc_name: + elif aoc_name in cache_aoc_names: log.info( f"{ctx.author} ({ctx.author.id}) tried to connect their account to {aoc_name}," " but it's already connected to another user." @@ -213,7 +213,7 @@ class AdventOfCode(commands.Cog): # Update an existing link if old_aoc_name := await self.account_links.get(ctx.author.id): - log.info(f"Changing link for{ctx.author} ({ctx.author.id}) from {old_aoc_name} to {aoc_name}.") + log.info(f"Changing link for {ctx.author} ({ctx.author.id}) from {old_aoc_name} to {aoc_name}.") await self.account_links.set(ctx.author.id, aoc_name) await ctx.reply(f"Your linked account has been changed to {aoc_name}.") else: -- cgit v1.2.3 From 03b5464e6e37f625ba7a590d45120f4b46b1d0aa Mon Sep 17 00:00:00 2001 From: Janine vN Date: Sat, 4 Dec 2021 11:00:48 -0500 Subject: Add more information to `.aoc lb` error embed Advent of Code Leaderboard BadArgument error embed now mentions to join the leaderboard and to wait up to 30 minutes if you've joined recently. --- bot/exts/events/advent_of_code/_helpers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/exts/events/advent_of_code/_helpers.py b/bot/exts/events/advent_of_code/_helpers.py index 35258544..807cc275 100644 --- a/bot/exts/events/advent_of_code/_helpers.py +++ b/bot/exts/events/advent_of_code/_helpers.py @@ -216,6 +216,9 @@ def _format_leaderboard(leaderboard: dict[str, dict], self_placement_name: str = if self_placement_name and not self_placement_exists: raise commands.BadArgument( "Sorry, your profile does not exist in this leaderboard." + "\n\n" + "To join our leaderboard, run the command `.aoc join`." + " If you've joined recently, please wait up to 30 minutes for our leaderboard to refresh." ) return "\n".join(leaderboard_lines) -- cgit v1.2.3 From 163a6e14c22b7992f44dca699de9e71c11362613 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 5 Dec 2021 13:11:03 +0000 Subject: Patch d.py's message converters to infer channelID from the given context Discord.py's message converter is supposed to infer channelID based on ctx.channel if only a messageID is given. A refactor (linked below) a few weeks before d.py's archival broke this, so that if only a messageID is given to the converter, it will only find that message if it's in the bot's cache. --- bot/__init__.py | 5 +++++ bot/monkey_patches.py | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/bot/__init__.py b/bot/__init__.py index ae53a5a5..3136c863 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -43,6 +43,11 @@ if os.name == "nt": monkey_patches.patch_typing() +# This patches any convertors that use PartialMessage, but not the PartialMessageConverter itself +# as library objects are made by this mapping. +# https://github.com/Rapptz/discord.py/blob/1a4e73d59932cdbe7bf2c281f25e32529fc7ae1f/discord/ext/commands/converter.py#L984-L1004 +commands.converter.PartialMessageConverter = monkey_patches.FixedPartialMessageConverter + # Monkey-patch discord.py decorators to use the both the Command and Group subclasses which supports root aliases. # Must be patched before any cogs are added. commands.command = partial(commands.command, cls=monkey_patches.Command) diff --git a/bot/monkey_patches.py b/bot/monkey_patches.py index fa6627d1..19965c19 100644 --- a/bot/monkey_patches.py +++ b/bot/monkey_patches.py @@ -1,10 +1,12 @@ import logging +import re from datetime import datetime, timedelta from discord import Forbidden, http from discord.ext import commands log = logging.getLogger(__name__) +MESSAGE_ID_RE = re.compile(r'(?P[0-9]{15,20})$') class Command(commands.Command): @@ -65,3 +67,25 @@ def patch_typing() -> None: pass http.HTTPClient.send_typing = honeybadger_type + + +class FixedPartialMessageConverter(commands.PartialMessageConverter): + """ + Make the Message converter infer channelID from the given context if only a messageID is given. + + Discord.py's Message converter is supposed to infer channelID based + on ctx.channel if only a messageID is given. A refactor commit, linked below, + a few weeks before d.py's archival broke this defined behaviour of the converter. + Currently, if only a messageID is given to the converter, it will only find that message + if it's in the bot's cache. + + https://github.com/Rapptz/discord.py/commit/1a4e73d59932cdbe7bf2c281f25e32529fc7ae1f + """ + + @staticmethod + def _get_id_matches(ctx: commands.Context, argument: str) -> tuple[int, int, int]: + """Inserts ctx.channel.id before calling super method if argument is just a messageID.""" + match = MESSAGE_ID_RE.match(argument) + if match: + argument = f"{ctx.channel.id}-{match.group('message_id')}" + return commands.PartialMessageConverter._get_id_matches(ctx, argument) -- cgit v1.2.3 From 598cf151cfaaa8e6777cd602714c10666ad45b4a Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 5 Dec 2021 13:12:41 +0000 Subject: Reflect new message converter behaviour in bm help message Since w epatched the message converter to work as intended, the help message given to a user when failing to resolve a message reference to a message object has been updated. --- bot/exts/utilities/bookmark.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/bookmark.py b/bot/exts/utilities/bookmark.py index a11c366b..b50205a0 100644 --- a/bot/exts/utilities/bookmark.py +++ b/bot/exts/utilities/bookmark.py @@ -102,7 +102,7 @@ class Bookmark(commands.Cog): "You must either provide a valid message to bookmark, or reply to one." "\n\nThe lookup strategy for a message is as follows (in order):" "\n1. Lookup by '{channel ID}-{message ID}' (retrieved by shift-clicking on 'Copy ID')" - "\n2. Lookup by message ID (the message **must** have been sent after the bot last started)" + "\n2. Lookup by message ID (the message **must** be in the context channel)" "\n3. Lookup by message URL" ) target_message = ctx.message.reference.resolved -- cgit v1.2.3 From 61e264bb39be551b4c5d083206df0bc2b09d1c10 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Fri, 10 Dec 2021 13:47:17 +0100 Subject: Remove myself from the codeowners --- .github/CODEOWNERS | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cffe15d5..d164ad04 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,10 +4,6 @@ bot/exts/events/hacktoberfest/** @ks129 bot/exts/holidays/halloween/** @ks129 # CI & Docker -.github/workflows/** @Akarys42 @SebastiaanZ @Den4200 -Dockerfile @Akarys42 @Den4200 -docker-compose.yml @Akarys42 @Den4200 - -# Tools -poetry.lock @Akarys42 -pyproject.toml @Akarys42 +.github/workflows/** @SebastiaanZ @Den4200 +Dockerfile @Den4200 +docker-compose.yml @Den4200 -- cgit v1.2.3 From 70b9ec7dbdc346e7ddcac7624968424cf0e66ef6 Mon Sep 17 00:00:00 2001 From: evgriff Date: Fri, 10 Dec 2021 19:09:31 +0000 Subject: Added the user's score to Candy Command according to #947 --- bot/exts/holidays/halloween/candy_collection.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/bot/exts/holidays/halloween/candy_collection.py b/bot/exts/holidays/halloween/candy_collection.py index bb9c93be..6edce1a0 100644 --- a/bot/exts/holidays/halloween/candy_collection.py +++ b/bot/exts/holidays/halloween/candy_collection.py @@ -173,6 +173,7 @@ class CandyCollection(commands.Cog): async def candy(self, ctx: commands.Context) -> None: """Get the candy leaderboard and save to JSON.""" records = await self.candy_records.items() + user = await self.bot.fetch_user(ctx.author.id) def generate_leaderboard() -> str: top_sorted = sorted( @@ -187,12 +188,23 @@ class CandyCollection(commands.Cog): for index, record in enumerate(top_five) ) if top_five else "No Candies" + def get_user_candy_score() -> str: + for user_id, score in records: + if user_id == user.id: + return f'<@{user.id}>: {score}' + return f'<@{user.id}>: 0' + e = discord.Embed(colour=discord.Colour.og_blurple()) e.add_field( name="Top Candy Records", value=generate_leaderboard(), inline=False ) + e.add_field( + name=f'{user.name}' + "'s Candy Score", + value=get_user_candy_score(), + inline=False + ) e.add_field( name="\u200b", value="Candies will randomly appear on messages sent. " -- cgit v1.2.3 From bcd47e1a0642dcc7c538d86d29c8311aed828bf5 Mon Sep 17 00:00:00 2001 From: evgriff Date: Fri, 10 Dec 2021 20:15:45 +0000 Subject: added key check --- bot/exts/utilities/issues.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/issues.py b/bot/exts/utilities/issues.py index b6d5a43e..b3f8340c 100644 --- a/bot/exts/utilities/issues.py +++ b/bot/exts/utilities/issues.py @@ -139,7 +139,7 @@ class Issues(commands.Cog): log.trace(f"PR provided, querying GH pulls API for additional information: {pulls_url}") async with self.bot.http_session.get(pulls_url) as p: pull_data = await p.json() - if pull_data["draft"]: + if "draft" in pull_data and pull_data["draft"]: emoji = Emojis.pull_request_draft elif pull_data["state"] == "open": emoji = Emojis.pull_request_open -- cgit v1.2.3 From f32b52791926249ea9ae5d3f2dbffce26569e827 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Dec 2021 20:58:47 +0000 Subject: chore(deps): bump lxml from 4.6.4 to 4.6.5 Bumps [lxml](https://github.com/lxml/lxml) from 4.6.4 to 4.6.5. - [Release notes](https://github.com/lxml/lxml/releases) - [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt) - [Commits](https://github.com/lxml/lxml/compare/lxml-4.6.4...lxml-4.6.5) --- updated-dependencies: - dependency-name: lxml dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- poetry.lock | 125 +++++++++++++++++++++++++++++---------------------------- pyproject.toml | 2 +- 2 files changed, 64 insertions(+), 63 deletions(-) diff --git a/poetry.lock b/poetry.lock index e02c2baa..5e950343 100644 --- a/poetry.lock +++ b/poetry.lock @@ -199,6 +199,7 @@ voice = ["PyNaCl (>=1.3.0,<1.5)"] [package.source] type = "url" url = "https://github.com/Rapptz/discord.py/archive/45d498c1b76deaf3b394d17ccf56112fa691d160.zip" + [[package]] name = "distlib" version = "0.3.3" @@ -418,7 +419,7 @@ python-versions = ">=3.7" [[package]] name = "lxml" -version = "4.6.4" +version = "4.6.5" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." category = "main" optional = false @@ -851,7 +852,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "c370c87b0425b330845ef1aad9cead2c3c3f9db7be95895895ac96fc3a2d28a6" +content-hash = "b091f392619f058d265309c9474d9d2b86f53f634017210cca11713cdb11ae14" [metadata.files] aiodns = [ @@ -1159,66 +1160,66 @@ kiwisolver = [ {file = "kiwisolver-1.3.2.tar.gz", hash = "sha256:fc4453705b81d03568d5b808ad8f09c77c47534f6ac2e72e733f9ca4714aa75c"}, ] lxml = [ - {file = "lxml-4.6.4-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bbf2dc330bd44bfc0254ab37677ec60f7c7ecea55ad8ba1b8b2ea7bf20c265f5"}, - {file = "lxml-4.6.4-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b667c51682fe9b9788c69465956baa8b6999531876ccedcafc895c74ad716cd8"}, - {file = "lxml-4.6.4-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:72e730d33fe2e302fd07285f14624fca5e5e2fb2bb4fb2c3941e318c41c443d1"}, - {file = "lxml-4.6.4-cp27-cp27m-win32.whl", hash = "sha256:433df8c7dde0f9e41cbf4f36b0829d50a378116ef5e962ba3881f2f5f025c7be"}, - {file = "lxml-4.6.4-cp27-cp27m-win_amd64.whl", hash = "sha256:35752ee40f7bbf6adc9ff4e1f4b84794a3593736dcce80db32e3c2aa85e294ac"}, - {file = "lxml-4.6.4-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ff5bb2a198ea67403bb6818705e9a4f90e0313f2215428ec51001ce56d939fb"}, - {file = "lxml-4.6.4-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9b87727561c1150c0cc91c5d9d389448b37a7d15f0ba939ed3d1acb2f11bf6c5"}, - {file = "lxml-4.6.4-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:45fdb2899c755138722797161547a40b3e2a06feda620cc41195ee7e97806d81"}, - {file = "lxml-4.6.4-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:38b9de0de3aa689fe9fb9877ae1be1e83b8cf9621f7e62049d0436b9ecf4ad64"}, - {file = "lxml-4.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:662523cd2a0246740225c7e32531f2e766544122e58bee70e700a024cfc0cf81"}, - {file = "lxml-4.6.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:4aa349c5567651f34d4eaae7de6ed5b523f6d70a288f9c6fbac22d13a0784e04"}, - {file = "lxml-4.6.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:08eb9200d88b376a8ed5e50f1dc1d1a45b49305169674002a3b5929943390591"}, - {file = "lxml-4.6.4-cp310-cp310-win32.whl", hash = "sha256:bdc224f216ead849e902151112efef6e96c41ee1322e15d4e5f7c8a826929aee"}, - {file = "lxml-4.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:ab6db93a2b6b66cbf62b4e4a7135f476e708e8c5c990d186584142c77d7f975a"}, - {file = "lxml-4.6.4-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:50790313df028aa05cf22be9a8da033b86c42fa32523e4fd944827b482b17bf0"}, - {file = "lxml-4.6.4-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6764998345552b1dfc9326a932d2bad6367c6b37a176bb73ada6b9486bf602f7"}, - {file = "lxml-4.6.4-cp35-cp35m-win32.whl", hash = "sha256:543b239b191bb3b6d9bef5f09f1fb2be5b7eb09ab4d386aa655e4d53fbe9ff47"}, - {file = "lxml-4.6.4-cp35-cp35m-win_amd64.whl", hash = "sha256:a75c1ad05eedb1a3ff2a34a52a4f0836cfaa892e12796ba39a7732c82701eff4"}, - {file = "lxml-4.6.4-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:47e955112ce64241fdb357acf0216081f9f3255b3ac9c502ca4b3323ec1ca558"}, - {file = "lxml-4.6.4-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:20d7c8d90d449c6a353b15ee0459abae8395dbe59ad01e406ccbf30cd81c6f98"}, - {file = "lxml-4.6.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:240db6f3228d26e3c6f4fad914b9ddaaf8707254e8b3efd564dc680c8ec3c264"}, - {file = "lxml-4.6.4-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:351482da8dd028834028537f08724b1de22d40dcf3bb723b469446564f409074"}, - {file = "lxml-4.6.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e678a643177c0e5ec947b645fa7bc84260dfb9b6bf8fb1fdd83008dfc2ca5928"}, - {file = "lxml-4.6.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:15d0381feb56f08f78c5cc4fc385ddfe0bde1456e37f54a9322833371aec4060"}, - {file = "lxml-4.6.4-cp36-cp36m-win32.whl", hash = "sha256:4ba74afe5ee5cb5e28d83b513a6e8f0875fda1dc1a9aea42cc0065f029160d2a"}, - {file = "lxml-4.6.4-cp36-cp36m-win_amd64.whl", hash = "sha256:9c91a73971a922c13070fd8fa5a114c858251791ba2122a941e6aa781c713e44"}, - {file = "lxml-4.6.4-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:6020c70ff695106bf80651953a23e37718ef1fee9abd060dcad8e32ab2dc13f3"}, - {file = "lxml-4.6.4-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f5dd358536b8a964bf6bd48de038754c1609e72e5f17f5d21efe2dda17594dbf"}, - {file = "lxml-4.6.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:7ae7089d81fc502df4b217ad77f03c54039fe90dac0acbe70448d7e53bfbc57e"}, - {file = "lxml-4.6.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:80d10d53d3184837445ff8562021bdd37f57c4cadacbf9d8726cc16220a00d54"}, - {file = "lxml-4.6.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e95da348d57eb448d226a44b868ff2ca5786fbcbe417ac99ff62d0a7d724b9c7"}, - {file = "lxml-4.6.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ffd65cfa33fed01735c82aca640fde4cc63f0414775cba11e06f84fae2085a6e"}, - {file = "lxml-4.6.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:877666418598f6cb289546c77ff87590cfd212f903b522b0afa0b9fb73b3ccfb"}, - {file = "lxml-4.6.4-cp37-cp37m-win32.whl", hash = "sha256:e91d24623e747eeb2d8121f4a94c6a7ad27dc48e747e2dc95bfe88632bd028a2"}, - {file = "lxml-4.6.4-cp37-cp37m-win_amd64.whl", hash = "sha256:4ec9a80dd5704ecfde54319b6964368daf02848c8954d3bacb9b64d1c7659159"}, - {file = "lxml-4.6.4-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:2901625f4a878a055d275beedc20ba9cb359cefc4386a967222fee29eb236038"}, - {file = "lxml-4.6.4-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:b567178a74a2261345890eac66fbf394692a6e002709d329f28a673ca6042473"}, - {file = "lxml-4.6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4717123f7c11c81e0da69989e5a64079c3f402b0efeb4c6241db6c369d657bd8"}, - {file = "lxml-4.6.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:cf201bf5594d1aab139fe53e3fca457e4f8204a5bbd65d48ab3b82a16f517868"}, - {file = "lxml-4.6.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a77a3470ba37e11872c75ca95baf9b3312133a3d5a5dc720803b23098c653976"}, - {file = "lxml-4.6.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:619c6d2b552bba00491e96c0518aad94002651c108a0f7364ff2d7798812c00e"}, - {file = "lxml-4.6.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:601f0ab75538b280aaf1e720eb9d68d4fa104ac274e1e9e6971df488f4dcdb0f"}, - {file = "lxml-4.6.4-cp38-cp38-win32.whl", hash = "sha256:75d3c5bbc0ddbad03bb68b9be638599f67e4b98ed3dcd0fec9f6f39e41ee96cb"}, - {file = "lxml-4.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:4341d135f5660db10184963d9c3418c3e28d7f868aaf8b11a323ebf85813f7f4"}, - {file = "lxml-4.6.4-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:9db24803fa71e3305fe4a7812782b708da21a0b774b130dd1860cf40a6d7a3ee"}, - {file = "lxml-4.6.4-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:afd60230ad9d8bcba005945ec3a343722f09e0b7f8ae804246e5d2cfc6bd71a6"}, - {file = "lxml-4.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:0c15e1cd55055956e77b0732270f1c6005850696bc3ef3e03d01e78af84eaa42"}, - {file = "lxml-4.6.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6d422b3c729737d8a39279a25fa156c983a56458f8b2f97661ee6fb22b80b1d6"}, - {file = "lxml-4.6.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2eb90f6ec3c236ef2f1bb38aee7c0d23e77d423d395af6326e7cca637519a4cb"}, - {file = "lxml-4.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:51a0e5d243687596f46e24e464121d4b232ad772e2d1785b2a2c0eb413c285d4"}, - {file = "lxml-4.6.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d43bd68714049c84e297c005456a15ecdec818f7b5aa5868c8b0a865cfb78a44"}, - {file = "lxml-4.6.4-cp39-cp39-win32.whl", hash = "sha256:ee9e4b07b0eba4b6a521509e9e1877476729c1243246b6959de697ebea739643"}, - {file = "lxml-4.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:48eaac2991b3036175b42ee8d3c23f4cca13f2be8426bf29401a690ab58c88f4"}, - {file = "lxml-4.6.4-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:2b06a91cf7b8acea7793006e4ae50646cef0fe35ce5acd4f5cb1c77eb228e4a1"}, - {file = "lxml-4.6.4-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:523f195948a1ba4f9f5b7294d83c6cd876547dc741820750a7e5e893a24bbe38"}, - {file = "lxml-4.6.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:b0ca0ada9d3bc18bd6f611bd001a28abdd49ab9698bd6d717f7f5394c8e94628"}, - {file = "lxml-4.6.4-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:197b7cb7a753cf553a45115739afd8458464a28913da00f5c525063f94cd3f48"}, - {file = "lxml-4.6.4-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:6298f5b42a26581206ef63fffa97c754245d329414108707c525512a5197f2ba"}, - {file = "lxml-4.6.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0b12c95542f04d10cba46b3ff28ea52ea56995b78cf918f0b11b05e75812bb79"}, - {file = "lxml-4.6.4.tar.gz", hash = "sha256:daf9bd1fee31f1c7a5928b3e1059e09a8d683ea58fb3ffc773b6c88cb8d1399c"}, + {file = "lxml-4.6.5-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:abcf7daa5ebcc89328326254f6dd6d566adb483d4d00178892afd386ab389de2"}, + {file = "lxml-4.6.5-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3884476a90d415be79adfa4e0e393048630d0d5bcd5757c4c07d8b4b00a1096b"}, + {file = "lxml-4.6.5-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:add017c5bd6b9ec3a5f09248396b6ee2ce61c5621f087eb2269c813cd8813808"}, + {file = "lxml-4.6.5-cp27-cp27m-win32.whl", hash = "sha256:a702005e447d712375433ed0499cb6e1503fadd6c96a47f51d707b4d37b76d3c"}, + {file = "lxml-4.6.5-cp27-cp27m-win_amd64.whl", hash = "sha256:da07c7e7fc9a3f40446b78c54dbba8bfd5c9100dfecb21b65bfe3f57844f5e71"}, + {file = "lxml-4.6.5-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a708c291900c40a7ecf23f1d2384ed0bc0604e24094dd13417c7e7f8f7a50d93"}, + {file = "lxml-4.6.5-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f33d8efb42e4fc2b31b3b4527940b25cdebb3026fb56a80c1c1c11a4271d2352"}, + {file = "lxml-4.6.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:f6befb83bca720b71d6bd6326a3b26e9496ae6649e26585de024890fe50f49b8"}, + {file = "lxml-4.6.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:59d77bfa3bea13caee95bc0d3f1c518b15049b97dd61ea8b3d71ce677a67f808"}, + {file = "lxml-4.6.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:68a851176c931e2b3de6214347b767451243eeed3bea34c172127bbb5bf6c210"}, + {file = "lxml-4.6.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a7790a273225b0c46e5f859c1327f0f659896cc72eaa537d23aa3ad9ff2a1cc1"}, + {file = "lxml-4.6.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6548fc551de15f310dd0564751d9dc3d405278d45ea9b2b369ed1eccf142e1f5"}, + {file = "lxml-4.6.5-cp310-cp310-win32.whl", hash = "sha256:dc8a0dbb2a10ae8bb609584f5c504789f0f3d0d81840da4849102ec84289f952"}, + {file = "lxml-4.6.5-cp310-cp310-win_amd64.whl", hash = "sha256:1ccbfe5d17835db906f2bab6f15b34194db1a5b07929cba3cf45a96dbfbfefc0"}, + {file = "lxml-4.6.5-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca9a40497f7e97a2a961c04fa8a6f23d790b0521350a8b455759d786b0bcb203"}, + {file = "lxml-4.6.5-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e5b4b0d9440046ead3bd425eb2b852499241ee0cef1ae151038e4f87ede888c4"}, + {file = "lxml-4.6.5-cp35-cp35m-win32.whl", hash = "sha256:87f8f7df70b90fbe7b49969f07b347e3f978f8bd1046bb8ecae659921869202b"}, + {file = "lxml-4.6.5-cp35-cp35m-win_amd64.whl", hash = "sha256:ce52aad32ec6e46d1a91ff8b8014a91538800dd533914bfc4a82f5018d971408"}, + {file = "lxml-4.6.5-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:8021eeff7fabde21b9858ed058a8250ad230cede91764d598c2466b0ba70db8b"}, + {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:cab343b265e38d4e00649cbbad9278b734c5715f9bcbb72c85a1f99b1a58e19a"}, + {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:3534d7c468c044f6aef3c0aff541db2826986a29ea73f2ca831f5d5284d9b570"}, + {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdb98f4c9e8a1735efddfaa995b0c96559792da15d56b76428bdfc29f77c4cdb"}, + {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:5ea121cb66d7e5cb396b4c3ca90471252b94e01809805cfe3e4e44be2db3a99c"}, + {file = "lxml-4.6.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:121fc6f71c692b49af6c963b84ab7084402624ffbe605287da362f8af0668ea3"}, + {file = "lxml-4.6.5-cp36-cp36m-win32.whl", hash = "sha256:1a2a7659b8eb93c6daee350a0d844994d49245a0f6c05c747f619386fb90ba04"}, + {file = "lxml-4.6.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2f77556266a8fe5428b8759fbfc4bd70be1d1d9c9b25d2a414f6a0c0b0f09120"}, + {file = "lxml-4.6.5-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:558485218ee06458643b929765ac1eb04519ca3d1e2dcc288517de864c747c33"}, + {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ba0006799f21d83c3717fe20e2707a10bbc296475155aadf4f5850f6659b96b9"}, + {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:916d457ad84e05b7db52700bad0a15c56e0c3000dcaf1263b2fb7a56fe148996"}, + {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c580c2a61d8297a6e47f4d01f066517dbb019be98032880d19ece7f337a9401d"}, + {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a21b78af7e2e13bec6bea12fc33bc05730197674f3e5402ce214d07026ccfebd"}, + {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:46515773570a33eae13e451c8fcf440222ef24bd3b26f40774dd0bd8b6db15b2"}, + {file = "lxml-4.6.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:124f09614f999551ac65e5b9875981ce4b66ac4b8e2ba9284572f741935df3d9"}, + {file = "lxml-4.6.5-cp37-cp37m-win32.whl", hash = "sha256:b4015baed99d046c760f09a4c59d234d8f398a454380c3cf0b859aba97136090"}, + {file = "lxml-4.6.5-cp37-cp37m-win_amd64.whl", hash = "sha256:12ae2339d32a2b15010972e1e2467345b7bf962e155671239fba74c229564b7f"}, + {file = "lxml-4.6.5-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:76b6c296e4f7a1a8a128aec42d128646897f9ae9a700ef6839cdc9b3900db9b5"}, + {file = "lxml-4.6.5-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:534032a5ceb34bba1da193b7d386ac575127cc39338379f39a164b10d97ade89"}, + {file = "lxml-4.6.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:60aeb14ff9022d2687ef98ce55f6342944c40d00916452bb90899a191802137a"}, + {file = "lxml-4.6.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9801bcd52ac9c795a7d81ea67471a42cffe532e46cfb750cd5713befc5c019c0"}, + {file = "lxml-4.6.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3b95fb7e6f9c2f53db88f4642231fc2b8907d854e614710996a96f1f32018d5c"}, + {file = "lxml-4.6.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:642eb4cabd997c9b949a994f9643cd8ae00cf4ca8c5cd9c273962296fadf1c44"}, + {file = "lxml-4.6.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:af4139172ff0263d269abdcc641e944c9de4b5d660894a3ec7e9f9db63b56ac9"}, + {file = "lxml-4.6.5-cp38-cp38-win32.whl", hash = "sha256:57cf05466917e08f90e323f025b96f493f92c0344694f5702579ab4b7e2eb10d"}, + {file = "lxml-4.6.5-cp38-cp38-win_amd64.whl", hash = "sha256:4f415624cf8b065796649a5e4621773dc5c9ea574a944c76a7f8a6d3d2906b41"}, + {file = "lxml-4.6.5-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7679bb6e4d9a3978a46ab19a3560e8d2b7265ef3c88152e7fdc130d649789887"}, + {file = "lxml-4.6.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c34234a1bc9e466c104372af74d11a9f98338a3f72fae22b80485171a64e0144"}, + {file = "lxml-4.6.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4b9390bf973e3907d967b75be199cf1978ca8443183cf1e78ad80ad8be9cf242"}, + {file = "lxml-4.6.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fcc849b28f584ed1dbf277291ded5c32bb3476a37032df4a1d523b55faa5f944"}, + {file = "lxml-4.6.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:46f21f2600d001af10e847df9eb3b832e8a439f696c04891bcb8a8cedd859af9"}, + {file = "lxml-4.6.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:99cf827f5a783038eb313beee6533dddb8bdb086d7269c5c144c1c952d142ace"}, + {file = "lxml-4.6.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:925174cafb0f1179a7fd38da90302555d7445e34c9ece68019e53c946be7f542"}, + {file = "lxml-4.6.5-cp39-cp39-win32.whl", hash = "sha256:12d8d6fe3ddef629ac1349fa89a638b296a34b6529573f5055d1cb4e5245f73b"}, + {file = "lxml-4.6.5-cp39-cp39-win_amd64.whl", hash = "sha256:a52e8f317336a44836475e9c802f51c2dc38d612eaa76532cb1d17690338b63b"}, + {file = "lxml-4.6.5-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:11ae552a78612620afd15625be9f1b82e3cc2e634f90d6b11709b10a100cba59"}, + {file = "lxml-4.6.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:473701599665d874919d05bb33b56180447b3a9da8d52d6d9799f381ce23f95c"}, + {file = "lxml-4.6.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:7f00cc64b49d2ef19ddae898a3def9dd8fda9c3d27c8a174c2889ee757918e71"}, + {file = "lxml-4.6.5-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:73e8614258404b2689a26cb5d002512b8bc4dfa18aca86382f68f959aee9b0c8"}, + {file = "lxml-4.6.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ff44de36772b05c2eb74f2b4b6d1ae29b8f41ed5506310ce1258d44826ee38c1"}, + {file = "lxml-4.6.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:5d5254c815c186744c8f922e2ce861a2bdeabc06520b4b30b2f7d9767791ce6e"}, + {file = "lxml-4.6.5.tar.gz", hash = "sha256:6e84edecc3a82f90d44ddee2ee2a2630d4994b8471816e226d2b771cda7ac4ca"}, ] matplotlib = [ {file = "matplotlib-3.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c988bb43414c7c2b0a31bd5187b4d27fd625c080371b463a6d422047df78913"}, diff --git a/pyproject.toml b/pyproject.toml index 71c79b13..d758385e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ emojis = "~=0.6.0" matplotlib = "~=3.4.1" coloredlogs = "~=15.0" colorama = { version = "~=0.4.3", markers = "sys_platform == 'win32'" } -lxml = "~=4.4" +lxml = "~=4.6" [tool.poetry.dev-dependencies] flake8 = "~=3.8" -- cgit v1.2.3 From 770292ea5a064ab57accabda93022fdfa64addd1 Mon Sep 17 00:00:00 2001 From: evgriff <44847470+evgriff@users.noreply.github.com> Date: Wed, 15 Dec 2021 12:43:11 -0500 Subject: Update bot/exts/holidays/halloween/candy_collection.py From @Shivansh-007 Co-authored-by: Shivansh-007 --- bot/exts/holidays/halloween/candy_collection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/holidays/halloween/candy_collection.py b/bot/exts/holidays/halloween/candy_collection.py index 6edce1a0..6e5ae84b 100644 --- a/bot/exts/holidays/halloween/candy_collection.py +++ b/bot/exts/holidays/halloween/candy_collection.py @@ -201,7 +201,7 @@ class CandyCollection(commands.Cog): inline=False ) e.add_field( - name=f'{user.name}' + "'s Candy Score", + name="Your Candy Score", value=get_user_candy_score(), inline=False ) -- cgit v1.2.3 From 43f1a14765133229053c06bd36517f2f5081f20d Mon Sep 17 00:00:00 2001 From: evgriff Date: Wed, 15 Dec 2021 17:52:49 +0000 Subject: Adding suggestions, removing erroneous commit --- bot/exts/holidays/halloween/candy_collection.py | 9 ++++----- bot/exts/utilities/issues.py | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/bot/exts/holidays/halloween/candy_collection.py b/bot/exts/holidays/halloween/candy_collection.py index 6edce1a0..9b16d543 100644 --- a/bot/exts/holidays/halloween/candy_collection.py +++ b/bot/exts/holidays/halloween/candy_collection.py @@ -173,7 +173,6 @@ class CandyCollection(commands.Cog): async def candy(self, ctx: commands.Context) -> None: """Get the candy leaderboard and save to JSON.""" records = await self.candy_records.items() - user = await self.bot.fetch_user(ctx.author.id) def generate_leaderboard() -> str: top_sorted = sorted( @@ -190,9 +189,9 @@ class CandyCollection(commands.Cog): def get_user_candy_score() -> str: for user_id, score in records: - if user_id == user.id: - return f'<@{user.id}>: {score}' - return f'<@{user.id}>: 0' + if user_id == ctx.author.id: + return f'<@{ctx.author.id}>: {score}' + return f'<@{ctx.author.id}>: 0' e = discord.Embed(colour=discord.Colour.og_blurple()) e.add_field( @@ -201,7 +200,7 @@ class CandyCollection(commands.Cog): inline=False ) e.add_field( - name=f'{user.name}' + "'s Candy Score", + name="Your Candy Score", value=get_user_candy_score(), inline=False ) diff --git a/bot/exts/utilities/issues.py b/bot/exts/utilities/issues.py index b3f8340c..b6d5a43e 100644 --- a/bot/exts/utilities/issues.py +++ b/bot/exts/utilities/issues.py @@ -139,7 +139,7 @@ class Issues(commands.Cog): log.trace(f"PR provided, querying GH pulls API for additional information: {pulls_url}") async with self.bot.http_session.get(pulls_url) as p: pull_data = await p.json() - if "draft" in pull_data and pull_data["draft"]: + if pull_data["draft"]: emoji = Emojis.pull_request_draft elif pull_data["state"] == "open": emoji = Emojis.pull_request_open -- cgit v1.2.3 From 55f008ee03c15b24551bdc5509459337df9acd8e Mon Sep 17 00:00:00 2001 From: aru Date: Wed, 15 Dec 2021 18:15:16 -0500 Subject: minor: allow color command in dev-media (#944) Co-authored-by: Xithrius <15021300+Xithrius@users.noreply.github.com> --- bot/exts/utilities/colour.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index 7c83fc66..ee6bad93 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -11,8 +11,10 @@ import rapidfuzz from PIL import Image, ImageColor from discord.ext import commands +from bot import constants from bot.bot import Bot from bot.exts.core.extensions import invoke_help_command +from bot.utils.decorators import whitelist_override THUMBNAIL_SIZE = (80, 80) @@ -78,6 +80,11 @@ class Colour(commands.Cog): await ctx.send(file=thumbnail_file, embed=colour_embed) @commands.group(aliases=("color",), invoke_without_command=True) + @whitelist_override( + channels=constants.WHITELISTED_CHANNELS, + roles=constants.STAFF_ROLES, + categories=[constants.Categories.development, constants.Categories.media] + ) async def colour(self, ctx: commands.Context, *, colour_input: Optional[str] = None) -> None: """ Create an embed that displays colour information. -- cgit v1.2.3 From 85242f87d8c946b645d0ca50ed266a78dd51393d Mon Sep 17 00:00:00 2001 From: evgriff Date: Thu, 16 Dec 2021 20:06:44 +0000 Subject: user.mention --- bot/exts/holidays/halloween/candy_collection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/holidays/halloween/candy_collection.py b/bot/exts/holidays/halloween/candy_collection.py index 9b16d543..61dc8f97 100644 --- a/bot/exts/holidays/halloween/candy_collection.py +++ b/bot/exts/holidays/halloween/candy_collection.py @@ -190,8 +190,8 @@ class CandyCollection(commands.Cog): def get_user_candy_score() -> str: for user_id, score in records: if user_id == ctx.author.id: - return f'<@{ctx.author.id}>: {score}' - return f'<@{ctx.author.id}>: 0' + return f'{ctx.author.mention}: {score}' + return f'{ctx.author.mention}: 0' e = discord.Embed(colour=discord.Colour.og_blurple()) e.add_field( -- cgit v1.2.3 From e48fda5a7ae3509dd0ef7e591fedbf3dab125ba1 Mon Sep 17 00:00:00 2001 From: evgriff Date: Thu, 16 Dec 2021 20:09:12 +0000 Subject: Double quotes --- bot/exts/holidays/halloween/candy_collection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/holidays/halloween/candy_collection.py b/bot/exts/holidays/halloween/candy_collection.py index 61dc8f97..729bbc97 100644 --- a/bot/exts/holidays/halloween/candy_collection.py +++ b/bot/exts/holidays/halloween/candy_collection.py @@ -190,8 +190,8 @@ class CandyCollection(commands.Cog): def get_user_candy_score() -> str: for user_id, score in records: if user_id == ctx.author.id: - return f'{ctx.author.mention}: {score}' - return f'{ctx.author.mention}: 0' + return f"{ctx.author.mention}: {score}" + return f"{ctx.author.mention}: 0" e = discord.Embed(colour=discord.Colour.og_blurple()) e.add_field( -- cgit v1.2.3 From 4d47cae15594babfb6e4c4a77b0fc13176e8f7bc Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 28 Dec 2021 20:10:17 +0000 Subject: Remove latex cog and matplotlib matplotlib, and its sub dependencies, caused a fresh install of an environment to take multiple minutes. As the latex cog is the only one that used it, and that is currently disabled, we have decided to remove it entirely. Git gives us the benefit of being able to see deleted files. So whoever decides to implement latex again can use that for reference. --- bot/exts/utilities/latex.py | 101 -------- poetry.lock | 579 +++++++++++++++++++------------------------- pyproject.toml | 1 - 3 files changed, 250 insertions(+), 431 deletions(-) delete mode 100644 bot/exts/utilities/latex.py diff --git a/bot/exts/utilities/latex.py b/bot/exts/utilities/latex.py deleted file mode 100644 index 36c7e0ab..00000000 --- a/bot/exts/utilities/latex.py +++ /dev/null @@ -1,101 +0,0 @@ -import asyncio -import hashlib -import pathlib -import re -from concurrent.futures import ThreadPoolExecutor -from io import BytesIO - -import discord -import matplotlib.pyplot as plt -from discord.ext import commands - -from bot.bot import Bot - -# configure fonts and colors for matplotlib -plt.rcParams.update( - { - "font.size": 16, - "mathtext.fontset": "cm", # Computer Modern font set - "mathtext.rm": "serif", - "figure.facecolor": "36393F", # matches Discord's dark mode background color - "text.color": "white", - } -) - -FORMATTED_CODE_REGEX = re.compile( - r"(?P(?P```)|``?)" # code delimiter: 1-3 backticks; (?P=block) only matches if it's a block - r"(?(block)(?:(?P[a-z]+)\n)?)" # if we're in a block, match optional language (only letters plus newline) - r"(?:[ \t]*\n)*" # any blank (empty or tabs/spaces only) lines before the code - r"(?P.*?)" # extract all code inside the markup - r"\s*" # any more whitespace before the end of the code markup - r"(?P=delim)", # match the exact same delimiter from the start again - re.DOTALL | re.IGNORECASE, # "." also matches newlines, case insensitive -) - -CACHE_DIRECTORY = pathlib.Path("_latex_cache") -CACHE_DIRECTORY.mkdir(exist_ok=True) - - -class Latex(commands.Cog): - """Renders latex.""" - - @staticmethod - def _render(text: str, filepath: pathlib.Path) -> BytesIO: - """ - Return the rendered image if latex compiles without errors, otherwise raise a BadArgument Exception. - - Saves rendered image to cache. - """ - fig = plt.figure() - rendered_image = BytesIO() - fig.text(0, 1, text, horizontalalignment="left", verticalalignment="top") - - try: - plt.savefig(rendered_image, bbox_inches="tight", dpi=600) - except ValueError as e: - raise commands.BadArgument(str(e)) - - rendered_image.seek(0) - - with open(filepath, "wb") as f: - f.write(rendered_image.getbuffer()) - - return rendered_image - - @staticmethod - def _prepare_input(text: str) -> str: - text = text.replace(r"\\", "$\n$") # matplotlib uses \n for newlines, not \\ - - if match := FORMATTED_CODE_REGEX.match(text): - return match.group("code") - else: - return text - - @commands.command() - @commands.max_concurrency(1, commands.BucketType.guild, wait=True) - async def latex(self, ctx: commands.Context, *, text: str) -> None: - """Renders the text in latex and sends the image.""" - text = self._prepare_input(text) - query_hash = hashlib.md5(text.encode()).hexdigest() - image_path = CACHE_DIRECTORY.joinpath(f"{query_hash}.png") - async with ctx.typing(): - if image_path.exists(): - await ctx.send(file=discord.File(image_path)) - return - - with ThreadPoolExecutor() as pool: - image = await asyncio.get_running_loop().run_in_executor( - pool, self._render, text, image_path - ) - - await ctx.send(file=discord.File(image, "latex.png")) - - -def setup(bot: Bot) -> None: - """Load the Latex Cog.""" - # As we have resource issues on this cog, - # we have it currently disabled while we fix it. - import logging - logging.info("Latex cog is currently disabled. It won't be loaded.") - return - bot.add_cog(Latex()) diff --git a/poetry.lock b/poetry.lock index 5e950343..6a83efed 100644 --- a/poetry.lock +++ b/poetry.lock @@ -76,29 +76,17 @@ python-versions = ">=3.5.3" [[package]] name = "attrs" -version = "21.2.0" +version = "21.3.0" description = "Classes Without Boilerplate" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] - -[[package]] -name = "backports.entry-points-selectable" -version = "1.1.1" -description = "Compatibility shim providing selectable entry points for older implementations" -category = "dev" -optional = false -python-versions = ">=2.7" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] +tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] [[package]] name = "beautifulsoup4" @@ -173,12 +161,18 @@ humanfriendly = ">=9.1" cron = ["capturer (>=2.4)"] [[package]] -name = "cycler" -version = "0.11.0" -description = "Composable style cycles" +name = "deprecated" +version = "1.2.13" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["tox", "bump2version (<1)", "sphinx (<2)", "importlib-metadata (<3)", "importlib-resources (<4)", "configparser (<5)", "sphinxcontrib-websupport (<2)", "zipp (<2)", "PyTest (<5)", "PyTest-Cov (<2.6)", "pytest", "pytest-cov"] [[package]] name = "discord.py" @@ -199,10 +193,9 @@ voice = ["PyNaCl (>=1.3.0,<1.5)"] [package.source] type = "url" url = "https://github.com/Rapptz/discord.py/archive/45d498c1b76deaf3b394d17ccf56112fa691d160.zip" - [[package]] name = "distlib" -version = "0.3.3" +version = "0.3.4" description = "Distribution utilities" category = "dev" optional = false @@ -218,7 +211,7 @@ python-versions = "*" [[package]] name = "fakeredis" -version = "1.6.1" +version = "1.7.0" description = "Fake implementation of redis API for testing purposes." category = "main" optional = false @@ -226,7 +219,7 @@ python-versions = ">=3.5" [package.dependencies] packaging = "*" -redis = "<3.6.0" +redis = "<4.1.0" six = ">=1.12" sortedcontainers = "*" @@ -236,11 +229,11 @@ lua = ["lupa"] [[package]] name = "filelock" -version = "3.3.2" +version = "3.4.2" description = "A platform independent file lock." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] @@ -378,14 +371,14 @@ pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_ve [[package]] name = "identify" -version = "2.3.5" +version = "2.4.1" description = "File identification library for Python" category = "dev" optional = false python-versions = ">=3.6.1" [package.extras] -license = ["editdistance-s"] +license = ["ukkonen"] [[package]] name = "idna" @@ -409,17 +402,9 @@ requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] plugins = ["setuptools"] -[[package]] -name = "kiwisolver" -version = "1.3.2" -description = "A fast implementation of the Cassowary constraint solver" -category = "main" -optional = false -python-versions = ">=3.7" - [[package]] name = "lxml" -version = "4.6.5" +version = "4.7.1" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." category = "main" optional = false @@ -431,22 +416,6 @@ html5 = ["html5lib"] htmlsoup = ["beautifulsoup4"] source = ["Cython (>=0.29.7)"] -[[package]] -name = "matplotlib" -version = "3.4.3" -description = "Python plotting package" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -cycler = ">=0.10" -kiwisolver = ">=1.0.1" -numpy = ">=1.16" -pillow = ">=6.2.0" -pyparsing = ">=2.2.1" -python-dateutil = ">=2.7" - [[package]] name = "mccabe" version = "0.6.1" @@ -479,24 +448,16 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "numpy" -version = "1.21.1" -description = "NumPy is the fundamental package for array computing with Python." -category = "main" -optional = false -python-versions = ">=3.7" - [[package]] name = "packaging" -version = "21.0" +version = "21.3" description = "Core utilities for Python packages" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -pyparsing = ">=2.0.2" +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "pep8-naming" @@ -534,11 +495,11 @@ test = ["docutils", "pytest-cov", "pytest-pycodestyle", "pytest-runner"] [[package]] name = "platformdirs" -version = "2.4.0" +version = "2.4.1" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] @@ -546,7 +507,7 @@ test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock [[package]] name = "pre-commit" -version = "2.15.0" +version = "2.16.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false @@ -633,7 +594,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pyparsing" -version = "3.0.5" +version = "3.0.6" description = "Python parsing module" category = "main" optional = false @@ -663,7 +624,7 @@ six = ">=1.5" [[package]] name = "python-dotenv" -version = "0.19.1" +version = "0.19.2" description = "Read key-value pairs from a .env file and set them as environment variables" category = "dev" optional = false @@ -682,7 +643,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "rapidfuzz" -version = "1.8.2" +version = "1.9.1" description = "rapid fuzzy string matching" category = "main" optional = false @@ -693,14 +654,17 @@ full = ["numpy"] [[package]] name = "redis" -version = "3.5.3" -description = "Python client for Redis key-value store" +version = "4.0.2" +description = "Python client for Redis database and key-value store" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" + +[package.dependencies] +deprecated = "*" [package.extras] -hiredis = ["hiredis (>=0.1.3)"] +hiredis = ["hiredis (>=1.0.0)"] [[package]] name = "sentry-sdk" @@ -740,7 +704,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "snowballstemmer" -version = "2.1.0" +version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." category = "dev" optional = false @@ -756,7 +720,7 @@ python-versions = "*" [[package]] name = "soupsieve" -version = "2.3" +version = "2.3.1" description = "A modern CSS selector implementation for Beautiful Soup." category = "main" optional = false @@ -799,11 +763,11 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "typing-extensions" -version = "3.10.0.2" -description = "Backported and Experimental Type Hints for Python 3.5+" +version = "4.0.1" +description = "Backported and Experimental Type Hints for Python 3.6+" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "urllib3" @@ -820,14 +784,13 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.10.0" +version = "20.11.0" description = "Virtual Python Environment builder" category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] -"backports.entry-points-selectable" = ">=1.0.4" distlib = ">=0.3.1,<1" filelock = ">=3.2,<4" platformdirs = ">=2,<3" @@ -837,6 +800,14 @@ six = ">=1.9.0,<2" docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] +[[package]] +name = "wrapt" +version = "1.13.3" +description = "Module for decorators, wrappers and monkey patching." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + [[package]] name = "yarl" version = "1.7.2" @@ -852,7 +823,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "b091f392619f058d265309c9474d9d2b86f53f634017210cca11713cdb11ae14" +content-hash = "e3682fd5b518ada5066b36015210f00d223c5485c24e4e7c377e371fe6ef0a0d" [metadata.files] aiodns = [ @@ -915,12 +886,8 @@ async-timeout = [ {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, ] attrs = [ - {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, - {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, -] -"backports.entry-points-selectable" = [ - {file = "backports.entry_points_selectable-1.1.1-py2.py3-none-any.whl", hash = "sha256:7fceed9532a7aa2bd888654a7314f864a3c16a4e710b34a58cfc0f08114c663b"}, - {file = "backports.entry_points_selectable-1.1.1.tar.gz", hash = "sha256:914b21a479fde881635f7af5adc7f6e38d6b274be32269070c53b698c60d5386"}, + {file = "attrs-21.3.0-py2.py3-none-any.whl", hash = "sha256:8f7335278dedd26b58c38e006338242cc0977f06d51579b2b8b87b9b33bff66c"}, + {file = "attrs-21.3.0.tar.gz", hash = "sha256:50f3c9b216dc9021042f71b392859a773b904ce1a029077f58f6598272432045"}, ] beautifulsoup4 = [ {file = "beautifulsoup4-4.10.0-py3-none-any.whl", hash = "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf"}, @@ -998,26 +965,26 @@ coloredlogs = [ {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, ] -cycler = [ - {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"}, - {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, +deprecated = [ + {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, + {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, ] "discord.py" = [] distlib = [ - {file = "distlib-0.3.3-py2.py3-none-any.whl", hash = "sha256:c8b54e8454e5bf6237cc84c20e8264c3e991e824ef27e8f1e81049867d861e31"}, - {file = "distlib-0.3.3.zip", hash = "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05"}, + {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, + {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, ] emojis = [ {file = "emojis-0.6.0-py3-none-any.whl", hash = "sha256:7da34c8a78ae262fd68cef9e2c78a3c1feb59784489eeea0f54ba1d4b7111c7c"}, {file = "emojis-0.6.0.tar.gz", hash = "sha256:bf605d1f1a27a81cd37fe82eb65781c904467f569295a541c33710b97e4225ec"}, ] fakeredis = [ - {file = "fakeredis-1.6.1-py3-none-any.whl", hash = "sha256:5eb1516f1fe1813e9da8f6c482178fc067af09f53de587ae03887ef5d9d13024"}, - {file = "fakeredis-1.6.1.tar.gz", hash = "sha256:0d06a9384fb79da9f2164ce96e34eb9d4e2ea46215070805ea6fd3c174590b47"}, + {file = "fakeredis-1.7.0-py3-none-any.whl", hash = "sha256:6f1e04f64557ad3b6835bdc6e5a8d022cbace4bdc24a47ad58f6a72e0fbff760"}, + {file = "fakeredis-1.7.0.tar.gz", hash = "sha256:c9bd12e430336cbd3e189fae0e91eb99997b93e76dbfdd6ed67fa352dc684c71"}, ] filelock = [ - {file = "filelock-3.3.2-py3-none-any.whl", hash = "sha256:bb2a1c717df74c48a2d00ed625e5a66f8572a3a30baacb7657add1d7bac4097b"}, - {file = "filelock-3.3.2.tar.gz", hash = "sha256:7afc856f74fa7006a289fd10fa840e1eebd8bbff6bffb69c26c54a0512ea8cf8"}, + {file = "filelock-3.4.2-py3-none-any.whl", hash = "sha256:cf0fc6a2f8d26bd900f19bf33915ca70ba4dd8c56903eeb14e1e7a2fd7590146"}, + {file = "filelock-3.4.2.tar.gz", hash = "sha256:38b4f4c989f9d06d44524df1b24bd19e167d851f19b50bf3e3559952dddc5b80"}, ] flake8 = [ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, @@ -1102,8 +1069,8 @@ humanfriendly = [ {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, ] identify = [ - {file = "identify-2.3.5-py2.py3-none-any.whl", hash = "sha256:ba945bddb4322394afcf3f703fa68eda08a6acc0f99d9573eb2be940aa7b9bba"}, - {file = "identify-2.3.5.tar.gz", hash = "sha256:6f0368ba0f21c199645a331beb7425d5374376e71bc149e9cb55e45cb45f832d"}, + {file = "identify-2.4.1-py2.py3-none-any.whl", hash = "sha256:0192893ff68b03d37fed553e261d4a22f94ea974093aefb33b29df2ff35fed3c"}, + {file = "identify-2.4.1.tar.gz", hash = "sha256:64d4885e539f505dd8ffb5e93c142a1db45480452b1594cacd3e91dca9a984e9"}, ] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, @@ -1113,136 +1080,67 @@ isort = [ {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] -kiwisolver = [ - {file = "kiwisolver-1.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1d819553730d3c2724582124aee8a03c846ec4362ded1034c16fb3ef309264e6"}, - {file = "kiwisolver-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d93a1095f83e908fc253f2fb569c2711414c0bfd451cab580466465b235b470"}, - {file = "kiwisolver-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4550a359c5157aaf8507e6820d98682872b9100ce7607f8aa070b4b8af6c298"}, - {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2210f28778c7d2ee13f3c2a20a3a22db889e75f4ec13a21072eabb5693801e84"}, - {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:82f49c5a79d3839bc8f38cb5f4bfc87e15f04cbafa5fbd12fb32c941cb529cfb"}, - {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9661a04ca3c950a8ac8c47f53cbc0b530bce1b52f516a1e87b7736fec24bfff0"}, - {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ddb500a2808c100e72c075cbb00bf32e62763c82b6a882d403f01a119e3f402"}, - {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72be6ebb4e92520b9726d7146bc9c9b277513a57a38efcf66db0620aec0097e0"}, - {file = "kiwisolver-1.3.2-cp310-cp310-win32.whl", hash = "sha256:83d2c9db5dfc537d0171e32de160461230eb14663299b7e6d18ca6dca21e4977"}, - {file = "kiwisolver-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:cba430db673c29376135e695c6e2501c44c256a81495da849e85d1793ee975ad"}, - {file = "kiwisolver-1.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4116ba9a58109ed5e4cb315bdcbff9838f3159d099ba5259c7c7fb77f8537492"}, - {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19554bd8d54cf41139f376753af1a644b63c9ca93f8f72009d50a2080f870f77"}, - {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a4cf5bbdc861987a7745aed7a536c6405256853c94abc9f3287c3fa401b174"}, - {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0007840186bacfaa0aba4466d5890334ea5938e0bb7e28078a0eb0e63b5b59d5"}, - {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec2eba188c1906b05b9b49ae55aae4efd8150c61ba450e6721f64620c50b59eb"}, - {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3dbb3cea20b4af4f49f84cffaf45dd5f88e8594d18568e0225e6ad9dec0e7967"}, - {file = "kiwisolver-1.3.2-cp37-cp37m-win32.whl", hash = "sha256:5326ddfacbe51abf9469fe668944bc2e399181a2158cb5d45e1d40856b2a0589"}, - {file = "kiwisolver-1.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c6572c2dab23c86a14e82c245473d45b4c515314f1f859e92608dcafbd2f19b8"}, - {file = "kiwisolver-1.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b5074fb09429f2b7bc82b6fb4be8645dcbac14e592128beeff5461dcde0af09f"}, - {file = "kiwisolver-1.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:22521219ca739654a296eea6d4367703558fba16f98688bd8ce65abff36eaa84"}, - {file = "kiwisolver-1.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c358721aebd40c243894298f685a19eb0491a5c3e0b923b9f887ef1193ddf829"}, - {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ba5a1041480c6e0a8b11a9544d53562abc2d19220bfa14133e0cdd9967e97af"}, - {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44e6adf67577dbdfa2d9f06db9fbc5639afefdb5bf2b4dfec25c3a7fbc619536"}, - {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d45d1c74f88b9f41062716c727f78f2a59a5476ecbe74956fafb423c5c87a76"}, - {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70adc3658138bc77a36ce769f5f183169bc0a2906a4f61f09673f7181255ac9b"}, - {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6a5431940f28b6de123de42f0eb47b84a073ee3c3345dc109ad550a3307dd28"}, - {file = "kiwisolver-1.3.2-cp38-cp38-win32.whl", hash = "sha256:ee040a7de8d295dbd261ef2d6d3192f13e2b08ec4a954de34a6fb8ff6422e24c"}, - {file = "kiwisolver-1.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:8dc3d842fa41a33fe83d9f5c66c0cc1f28756530cd89944b63b072281e852031"}, - {file = "kiwisolver-1.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a498bcd005e8a3fedd0022bb30ee0ad92728154a8798b703f394484452550507"}, - {file = "kiwisolver-1.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:80efd202108c3a4150e042b269f7c78643420cc232a0a771743bb96b742f838f"}, - {file = "kiwisolver-1.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f8eb7b6716f5b50e9c06207a14172cf2de201e41912ebe732846c02c830455b9"}, - {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f441422bb313ab25de7b3dbfd388e790eceb76ce01a18199ec4944b369017009"}, - {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:30fa008c172355c7768159983a7270cb23838c4d7db73d6c0f6b60dde0d432c6"}, - {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f8f6c8f4f1cff93ca5058d6ec5f0efda922ecb3f4c5fb76181f327decff98b8"}, - {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba677bcaff9429fd1bf01648ad0901cea56c0d068df383d5f5856d88221fe75b"}, - {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7843b1624d6ccca403a610d1277f7c28ad184c5aa88a1750c1a999754e65b439"}, - {file = "kiwisolver-1.3.2-cp39-cp39-win32.whl", hash = "sha256:e6f5eb2f53fac7d408a45fbcdeda7224b1cfff64919d0f95473420a931347ae9"}, - {file = "kiwisolver-1.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:eedd3b59190885d1ebdf6c5e0ca56828beb1949b4dfe6e5d0256a461429ac386"}, - {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:dedc71c8eb9c5096037766390172c34fb86ef048b8e8958b4e484b9e505d66bc"}, - {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bf7eb45d14fc036514c09554bf983f2a72323254912ed0c3c8e697b62c4c158f"}, - {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2b65bd35f3e06a47b5c30ea99e0c2b88f72c6476eedaf8cfbc8e66adb5479dcf"}, - {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25405f88a37c5f5bcba01c6e350086d65e7465fd1caaf986333d2a045045a223"}, - {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:bcadb05c3d4794eb9eee1dddf1c24215c92fb7b55a80beae7a60530a91060560"}, - {file = "kiwisolver-1.3.2.tar.gz", hash = "sha256:fc4453705b81d03568d5b808ad8f09c77c47534f6ac2e72e733f9ca4714aa75c"}, -] lxml = [ - {file = "lxml-4.6.5-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:abcf7daa5ebcc89328326254f6dd6d566adb483d4d00178892afd386ab389de2"}, - {file = "lxml-4.6.5-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3884476a90d415be79adfa4e0e393048630d0d5bcd5757c4c07d8b4b00a1096b"}, - {file = "lxml-4.6.5-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:add017c5bd6b9ec3a5f09248396b6ee2ce61c5621f087eb2269c813cd8813808"}, - {file = "lxml-4.6.5-cp27-cp27m-win32.whl", hash = "sha256:a702005e447d712375433ed0499cb6e1503fadd6c96a47f51d707b4d37b76d3c"}, - {file = "lxml-4.6.5-cp27-cp27m-win_amd64.whl", hash = "sha256:da07c7e7fc9a3f40446b78c54dbba8bfd5c9100dfecb21b65bfe3f57844f5e71"}, - {file = "lxml-4.6.5-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a708c291900c40a7ecf23f1d2384ed0bc0604e24094dd13417c7e7f8f7a50d93"}, - {file = "lxml-4.6.5-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f33d8efb42e4fc2b31b3b4527940b25cdebb3026fb56a80c1c1c11a4271d2352"}, - {file = "lxml-4.6.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:f6befb83bca720b71d6bd6326a3b26e9496ae6649e26585de024890fe50f49b8"}, - {file = "lxml-4.6.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:59d77bfa3bea13caee95bc0d3f1c518b15049b97dd61ea8b3d71ce677a67f808"}, - {file = "lxml-4.6.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:68a851176c931e2b3de6214347b767451243eeed3bea34c172127bbb5bf6c210"}, - {file = "lxml-4.6.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a7790a273225b0c46e5f859c1327f0f659896cc72eaa537d23aa3ad9ff2a1cc1"}, - {file = "lxml-4.6.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6548fc551de15f310dd0564751d9dc3d405278d45ea9b2b369ed1eccf142e1f5"}, - {file = "lxml-4.6.5-cp310-cp310-win32.whl", hash = "sha256:dc8a0dbb2a10ae8bb609584f5c504789f0f3d0d81840da4849102ec84289f952"}, - {file = "lxml-4.6.5-cp310-cp310-win_amd64.whl", hash = "sha256:1ccbfe5d17835db906f2bab6f15b34194db1a5b07929cba3cf45a96dbfbfefc0"}, - {file = "lxml-4.6.5-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca9a40497f7e97a2a961c04fa8a6f23d790b0521350a8b455759d786b0bcb203"}, - {file = "lxml-4.6.5-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e5b4b0d9440046ead3bd425eb2b852499241ee0cef1ae151038e4f87ede888c4"}, - {file = "lxml-4.6.5-cp35-cp35m-win32.whl", hash = "sha256:87f8f7df70b90fbe7b49969f07b347e3f978f8bd1046bb8ecae659921869202b"}, - {file = "lxml-4.6.5-cp35-cp35m-win_amd64.whl", hash = "sha256:ce52aad32ec6e46d1a91ff8b8014a91538800dd533914bfc4a82f5018d971408"}, - {file = "lxml-4.6.5-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:8021eeff7fabde21b9858ed058a8250ad230cede91764d598c2466b0ba70db8b"}, - {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:cab343b265e38d4e00649cbbad9278b734c5715f9bcbb72c85a1f99b1a58e19a"}, - {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:3534d7c468c044f6aef3c0aff541db2826986a29ea73f2ca831f5d5284d9b570"}, - {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdb98f4c9e8a1735efddfaa995b0c96559792da15d56b76428bdfc29f77c4cdb"}, - {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:5ea121cb66d7e5cb396b4c3ca90471252b94e01809805cfe3e4e44be2db3a99c"}, - {file = "lxml-4.6.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:121fc6f71c692b49af6c963b84ab7084402624ffbe605287da362f8af0668ea3"}, - {file = "lxml-4.6.5-cp36-cp36m-win32.whl", hash = "sha256:1a2a7659b8eb93c6daee350a0d844994d49245a0f6c05c747f619386fb90ba04"}, - {file = "lxml-4.6.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2f77556266a8fe5428b8759fbfc4bd70be1d1d9c9b25d2a414f6a0c0b0f09120"}, - {file = "lxml-4.6.5-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:558485218ee06458643b929765ac1eb04519ca3d1e2dcc288517de864c747c33"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ba0006799f21d83c3717fe20e2707a10bbc296475155aadf4f5850f6659b96b9"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:916d457ad84e05b7db52700bad0a15c56e0c3000dcaf1263b2fb7a56fe148996"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c580c2a61d8297a6e47f4d01f066517dbb019be98032880d19ece7f337a9401d"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a21b78af7e2e13bec6bea12fc33bc05730197674f3e5402ce214d07026ccfebd"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:46515773570a33eae13e451c8fcf440222ef24bd3b26f40774dd0bd8b6db15b2"}, - {file = "lxml-4.6.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:124f09614f999551ac65e5b9875981ce4b66ac4b8e2ba9284572f741935df3d9"}, - {file = "lxml-4.6.5-cp37-cp37m-win32.whl", hash = "sha256:b4015baed99d046c760f09a4c59d234d8f398a454380c3cf0b859aba97136090"}, - {file = "lxml-4.6.5-cp37-cp37m-win_amd64.whl", hash = "sha256:12ae2339d32a2b15010972e1e2467345b7bf962e155671239fba74c229564b7f"}, - {file = "lxml-4.6.5-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:76b6c296e4f7a1a8a128aec42d128646897f9ae9a700ef6839cdc9b3900db9b5"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:534032a5ceb34bba1da193b7d386ac575127cc39338379f39a164b10d97ade89"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:60aeb14ff9022d2687ef98ce55f6342944c40d00916452bb90899a191802137a"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9801bcd52ac9c795a7d81ea67471a42cffe532e46cfb750cd5713befc5c019c0"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3b95fb7e6f9c2f53db88f4642231fc2b8907d854e614710996a96f1f32018d5c"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:642eb4cabd997c9b949a994f9643cd8ae00cf4ca8c5cd9c273962296fadf1c44"}, - {file = "lxml-4.6.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:af4139172ff0263d269abdcc641e944c9de4b5d660894a3ec7e9f9db63b56ac9"}, - {file = "lxml-4.6.5-cp38-cp38-win32.whl", hash = "sha256:57cf05466917e08f90e323f025b96f493f92c0344694f5702579ab4b7e2eb10d"}, - {file = "lxml-4.6.5-cp38-cp38-win_amd64.whl", hash = "sha256:4f415624cf8b065796649a5e4621773dc5c9ea574a944c76a7f8a6d3d2906b41"}, - {file = "lxml-4.6.5-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7679bb6e4d9a3978a46ab19a3560e8d2b7265ef3c88152e7fdc130d649789887"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c34234a1bc9e466c104372af74d11a9f98338a3f72fae22b80485171a64e0144"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4b9390bf973e3907d967b75be199cf1978ca8443183cf1e78ad80ad8be9cf242"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fcc849b28f584ed1dbf277291ded5c32bb3476a37032df4a1d523b55faa5f944"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:46f21f2600d001af10e847df9eb3b832e8a439f696c04891bcb8a8cedd859af9"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:99cf827f5a783038eb313beee6533dddb8bdb086d7269c5c144c1c952d142ace"}, - {file = "lxml-4.6.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:925174cafb0f1179a7fd38da90302555d7445e34c9ece68019e53c946be7f542"}, - {file = "lxml-4.6.5-cp39-cp39-win32.whl", hash = "sha256:12d8d6fe3ddef629ac1349fa89a638b296a34b6529573f5055d1cb4e5245f73b"}, - {file = "lxml-4.6.5-cp39-cp39-win_amd64.whl", hash = "sha256:a52e8f317336a44836475e9c802f51c2dc38d612eaa76532cb1d17690338b63b"}, - {file = "lxml-4.6.5-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:11ae552a78612620afd15625be9f1b82e3cc2e634f90d6b11709b10a100cba59"}, - {file = "lxml-4.6.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:473701599665d874919d05bb33b56180447b3a9da8d52d6d9799f381ce23f95c"}, - {file = "lxml-4.6.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:7f00cc64b49d2ef19ddae898a3def9dd8fda9c3d27c8a174c2889ee757918e71"}, - {file = "lxml-4.6.5-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:73e8614258404b2689a26cb5d002512b8bc4dfa18aca86382f68f959aee9b0c8"}, - {file = "lxml-4.6.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ff44de36772b05c2eb74f2b4b6d1ae29b8f41ed5506310ce1258d44826ee38c1"}, - {file = "lxml-4.6.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:5d5254c815c186744c8f922e2ce861a2bdeabc06520b4b30b2f7d9767791ce6e"}, - {file = "lxml-4.6.5.tar.gz", hash = "sha256:6e84edecc3a82f90d44ddee2ee2a2630d4994b8471816e226d2b771cda7ac4ca"}, -] -matplotlib = [ - {file = "matplotlib-3.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c988bb43414c7c2b0a31bd5187b4d27fd625c080371b463a6d422047df78913"}, - {file = "matplotlib-3.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f1c5efc278d996af8a251b2ce0b07bbeccb821f25c8c9846bdcb00ffc7f158aa"}, - {file = "matplotlib-3.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:eeb1859efe7754b1460e1d4991bbd4a60a56f366bc422ef3a9c5ae05f0bc70b5"}, - {file = "matplotlib-3.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:844a7b0233e4ff7fba57e90b8799edaa40b9e31e300b8d5efc350937fa8b1bea"}, - {file = "matplotlib-3.4.3-cp37-cp37m-win32.whl", hash = "sha256:85f0c9cf724715e75243a7b3087cf4a3de056b55e05d4d76cc58d610d62894f3"}, - {file = "matplotlib-3.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c70b6311dda3e27672f1bf48851a0de816d1ca6aaf3d49365fbdd8e959b33d2b"}, - {file = "matplotlib-3.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b884715a59fec9ad3b6048ecf3860f3b2ce965e676ef52593d6fa29abcf7d330"}, - {file = "matplotlib-3.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a78a3b51f29448c7f4d4575e561f6b0dbb8d01c13c2046ab6c5220eb25c06506"}, - {file = "matplotlib-3.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6a724e3a48a54b8b6e7c4ae38cd3d07084508fa47c410c8757e9db9791421838"}, - {file = "matplotlib-3.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:48e1e0859b54d5f2e29bb78ca179fd59b971c6ceb29977fb52735bfd280eb0f5"}, - {file = "matplotlib-3.4.3-cp38-cp38-win32.whl", hash = "sha256:01c9de93a2ca0d128c9064f23709362e7fefb34910c7c9e0b8ab0de8258d5eda"}, - {file = "matplotlib-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:ebfb01a65c3f5d53a8c2a8133fec2b5221281c053d944ae81ff5822a68266617"}, - {file = "matplotlib-3.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b8b53f336a4688cfce615887505d7e41fd79b3594bf21dd300531a4f5b4f746a"}, - {file = "matplotlib-3.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:fcd6f1954943c0c192bfbebbac263f839d7055409f1173f80d8b11a224d236da"}, - {file = "matplotlib-3.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6be8df61b1626e1a142c57e065405e869e9429b4a6dab4a324757d0dc4d42235"}, - {file = "matplotlib-3.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:41b6e307458988891fcdea2d8ecf84a8c92d53f84190aa32da65f9505546e684"}, - {file = "matplotlib-3.4.3-cp39-cp39-win32.whl", hash = "sha256:f72657f1596199dc1e4e7a10f52a4784ead8a711f4e5b59bea95bdb97cf0e4fd"}, - {file = "matplotlib-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:f15edcb0629a0801738925fe27070480f446fcaa15de65946ff946ad99a59a40"}, - {file = "matplotlib-3.4.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:556965514b259204637c360d213de28d43a1f4aed1eca15596ce83f768c5a56f"}, - {file = "matplotlib-3.4.3-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:54a026055d5f8614f184e588f6e29064019a0aa8448450214c0b60926d62d919"}, - {file = "matplotlib-3.4.3.tar.gz", hash = "sha256:fc4f526dfdb31c9bd6b8ca06bf9fab663ca12f3ec9cdf4496fb44bc680140318"}, + {file = "lxml-4.7.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:d546431636edb1d6a608b348dd58cc9841b81f4116745857b6cb9f8dadb2725f"}, + {file = "lxml-4.7.1-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6308062534323f0d3edb4e702a0e26a76ca9e0e23ff99be5d82750772df32a9e"}, + {file = "lxml-4.7.1-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f76dbe44e31abf516114f6347a46fa4e7c2e8bceaa4b6f7ee3a0a03c8eba3c17"}, + {file = "lxml-4.7.1-cp27-cp27m-win32.whl", hash = "sha256:d5618d49de6ba63fe4510bdada62d06a8acfca0b4b5c904956c777d28382b419"}, + {file = "lxml-4.7.1-cp27-cp27m-win_amd64.whl", hash = "sha256:9393a05b126a7e187f3e38758255e0edf948a65b22c377414002d488221fdaa2"}, + {file = "lxml-4.7.1-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:50d3dba341f1e583265c1a808e897b4159208d814ab07530202b6036a4d86da5"}, + {file = "lxml-4.7.1-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:44f552e0da3c8ee3c28e2eb82b0b784200631687fc6a71277ea8ab0828780e7d"}, + {file = "lxml-4.7.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:e662c6266e3a275bdcb6bb049edc7cd77d0b0f7e119a53101d367c841afc66dc"}, + {file = "lxml-4.7.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4c093c571bc3da9ebcd484e001ba18b8452903cd428c0bc926d9b0141bcb710e"}, + {file = "lxml-4.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3e26ad9bc48d610bf6cc76c506b9e5ad9360ed7a945d9be3b5b2c8535a0145e3"}, + {file = "lxml-4.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a5f623aeaa24f71fce3177d7fee875371345eb9102b355b882243e33e04b7175"}, + {file = "lxml-4.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7b5e2acefd33c259c4a2e157119c4373c8773cf6793e225006a1649672ab47a6"}, + {file = "lxml-4.7.1-cp310-cp310-win32.whl", hash = "sha256:67fa5f028e8a01e1d7944a9fb616d1d0510d5d38b0c41708310bd1bc45ae89f6"}, + {file = "lxml-4.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:b1d381f58fcc3e63fcc0ea4f0a38335163883267f77e4c6e22d7a30877218a0e"}, + {file = "lxml-4.7.1-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:38d9759733aa04fb1697d717bfabbedb21398046bd07734be7cccc3d19ea8675"}, + {file = "lxml-4.7.1-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:dfd0d464f3d86a1460683cd742306d1138b4e99b79094f4e07e1ca85ee267fe7"}, + {file = "lxml-4.7.1-cp35-cp35m-win32.whl", hash = "sha256:534e946bce61fd162af02bad7bfd2daec1521b71d27238869c23a672146c34a5"}, + {file = "lxml-4.7.1-cp35-cp35m-win_amd64.whl", hash = "sha256:6ec829058785d028f467be70cd195cd0aaf1a763e4d09822584ede8c9eaa4b03"}, + {file = "lxml-4.7.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:ade74f5e3a0fd17df5782896ddca7ddb998845a5f7cd4b0be771e1ffc3b9aa5b"}, + {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:41358bfd24425c1673f184d7c26c6ae91943fe51dfecc3603b5e08187b4bcc55"}, + {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6e56521538f19c4a6690f439fefed551f0b296bd785adc67c1777c348beb943d"}, + {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5b0f782f0e03555c55e37d93d7a57454efe7495dab33ba0ccd2dbe25fc50f05d"}, + {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:490712b91c65988012e866c411a40cc65b595929ececf75eeb4c79fcc3bc80a6"}, + {file = "lxml-4.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:34c22eb8c819d59cec4444d9eebe2e38b95d3dcdafe08965853f8799fd71161d"}, + {file = "lxml-4.7.1-cp36-cp36m-win32.whl", hash = "sha256:2a906c3890da6a63224d551c2967413b8790a6357a80bf6b257c9a7978c2c42d"}, + {file = "lxml-4.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:36b16fecb10246e599f178dd74f313cbdc9f41c56e77d52100d1361eed24f51a"}, + {file = "lxml-4.7.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:a5edc58d631170de90e50adc2cc0248083541affef82f8cd93bea458e4d96db8"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:87c1b0496e8c87ec9db5383e30042357b4839b46c2d556abd49ec770ce2ad868"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:0a5f0e4747f31cff87d1eb32a6000bde1e603107f632ef4666be0dc065889c7a"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:bf6005708fc2e2c89a083f258b97709559a95f9a7a03e59f805dd23c93bc3986"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc15874816b9320581133ddc2096b644582ab870cf6a6ed63684433e7af4b0d3"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b5e96e25e70917b28a5391c2ed3ffc6156513d3db0e1476c5253fcd50f7a944"}, + {file = "lxml-4.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ec9027d0beb785a35aa9951d14e06d48cfbf876d8ff67519403a2522b181943b"}, + {file = "lxml-4.7.1-cp37-cp37m-win32.whl", hash = "sha256:9fbc0dee7ff5f15c4428775e6fa3ed20003140560ffa22b88326669d53b3c0f4"}, + {file = "lxml-4.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1104a8d47967a414a436007c52f533e933e5d52574cab407b1e49a4e9b5ddbd1"}, + {file = "lxml-4.7.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:fc9fb11b65e7bc49f7f75aaba1b700f7181d95d4e151cf2f24d51bfd14410b77"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:317bd63870b4d875af3c1be1b19202de34c32623609ec803b81c99193a788c1e"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:610807cea990fd545b1559466971649e69302c8a9472cefe1d6d48a1dee97440"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:09b738360af8cb2da275998a8bf79517a71225b0de41ab47339c2beebfff025f"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a2ab9d089324d77bb81745b01f4aeffe4094306d939e92ba5e71e9a6b99b71e"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eed394099a7792834f0cb4a8f615319152b9d801444c1c9e1b1a2c36d2239f9e"}, + {file = "lxml-4.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:735e3b4ce9c0616e85f302f109bdc6e425ba1670a73f962c9f6b98a6d51b77c9"}, + {file = "lxml-4.7.1-cp38-cp38-win32.whl", hash = "sha256:772057fba283c095db8c8ecde4634717a35c47061d24f889468dc67190327bcd"}, + {file = "lxml-4.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:13dbb5c7e8f3b6a2cf6e10b0948cacb2f4c9eb05029fe31c60592d08ac63180d"}, + {file = "lxml-4.7.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:718d7208b9c2d86aaf0294d9381a6acb0158b5ff0f3515902751404e318e02c9"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:5bee1b0cbfdb87686a7fb0e46f1d8bd34d52d6932c0723a86de1cc532b1aa489"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:e410cf3a2272d0a85526d700782a2fa92c1e304fdcc519ba74ac80b8297adf36"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:585ea241ee4961dc18a95e2f5581dbc26285fcf330e007459688096f76be8c42"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a555e06566c6dc167fbcd0ad507ff05fd9328502aefc963cb0a0547cfe7f00db"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:adaab25be351fff0d8a691c4f09153647804d09a87a4e4ea2c3f9fe9e8651851"}, + {file = "lxml-4.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:82d16a64236970cb93c8d63ad18c5b9f138a704331e4b916b2737ddfad14e0c4"}, + {file = "lxml-4.7.1-cp39-cp39-win32.whl", hash = "sha256:59e7da839a1238807226f7143c68a479dee09244d1b3cf8c134f2fce777d12d0"}, + {file = "lxml-4.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:a1bbc4efa99ed1310b5009ce7f3a1784698082ed2c1ef3895332f5df9b3b92c2"}, + {file = "lxml-4.7.1-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:0607ff0988ad7e173e5ddf7bf55ee65534bd18a5461183c33e8e41a59e89edf4"}, + {file = "lxml-4.7.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:6c198bfc169419c09b85ab10cb0f572744e686f40d1e7f4ed09061284fc1303f"}, + {file = "lxml-4.7.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a58d78653ae422df6837dd4ca0036610b8cb4962b5cfdbd337b7b24de9e5f98a"}, + {file = "lxml-4.7.1-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:e18281a7d80d76b66a9f9e68a98cf7e1d153182772400d9a9ce855264d7d0ce7"}, + {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8e54945dd2eeb50925500957c7c579df3cd07c29db7810b83cf30495d79af267"}, + {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:447d5009d6b5447b2f237395d0018901dcc673f7d9f82ba26c1b9f9c3b444b60"}, + {file = "lxml-4.7.1.tar.gz", hash = "sha256:a1613838aa6b89af4ba10a0f3a972836128801ed008078f8c1244e65958f1b24"}, ] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, @@ -1330,39 +1228,9 @@ nodeenv = [ {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, ] -numpy = [ - {file = "numpy-1.21.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38e8648f9449a549a7dfe8d8755a5979b45b3538520d1e735637ef28e8c2dc50"}, - {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd7d7409fa643a91d0a05c7554dd68aa9c9bb16e186f6ccfe40d6e003156e33a"}, - {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a75b4498b1e93d8b700282dc8e655b8bd559c0904b3910b144646dbbbc03e062"}, - {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1412aa0aec3e00bc23fbb8664d76552b4efde98fb71f60737c83efbac24112f1"}, - {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e46ceaff65609b5399163de5893d8f2a82d3c77d5e56d976c8b5fb01faa6b671"}, - {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c6a2324085dd52f96498419ba95b5777e40b6bcbc20088fddb9e8cbb58885e8e"}, - {file = "numpy-1.21.1-cp37-cp37m-win32.whl", hash = "sha256:73101b2a1fef16602696d133db402a7e7586654682244344b8329cdcbbb82172"}, - {file = "numpy-1.21.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7a708a79c9a9d26904d1cca8d383bf869edf6f8e7650d85dbc77b041e8c5a0f8"}, - {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95b995d0c413f5d0428b3f880e8fe1660ff9396dcd1f9eedbc311f37b5652e16"}, - {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:635e6bd31c9fb3d475c8f44a089569070d10a9ef18ed13738b03049280281267"}, - {file = "numpy-1.21.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a3d5fb89bfe21be2ef47c0614b9c9c707b7362386c9a3ff1feae63e0267ccb6"}, - {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a326af80e86d0e9ce92bcc1e65c8ff88297de4fa14ee936cb2293d414c9ec63"}, - {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:791492091744b0fe390a6ce85cc1bf5149968ac7d5f0477288f78c89b385d9af"}, - {file = "numpy-1.21.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0318c465786c1f63ac05d7c4dbcecd4d2d7e13f0959b01b534ea1e92202235c5"}, - {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a513bd9c1551894ee3d31369f9b07460ef223694098cf27d399513415855b68"}, - {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:91c6f5fc58df1e0a3cc0c3a717bb3308ff850abdaa6d2d802573ee2b11f674a8"}, - {file = "numpy-1.21.1-cp38-cp38-win32.whl", hash = "sha256:978010b68e17150db8765355d1ccdd450f9fc916824e8c4e35ee620590e234cd"}, - {file = "numpy-1.21.1-cp38-cp38-win_amd64.whl", hash = "sha256:9749a40a5b22333467f02fe11edc98f022133ee1bfa8ab99bda5e5437b831214"}, - {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d7a4aeac3b94af92a9373d6e77b37691b86411f9745190d2c351f410ab3a791f"}, - {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9e7912a56108aba9b31df688a4c4f5cb0d9d3787386b87d504762b6754fbb1b"}, - {file = "numpy-1.21.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:25b40b98ebdd272bc3020935427a4530b7d60dfbe1ab9381a39147834e985eac"}, - {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a92c5aea763d14ba9d6475803fc7904bda7decc2a0a68153f587ad82941fec1"}, - {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05a0f648eb28bae4bcb204e6fd14603de2908de982e761a2fc78efe0f19e96e1"}, - {file = "numpy-1.21.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f01f28075a92eede918b965e86e8f0ba7b7797a95aa8d35e1cc8821f5fc3ad6a"}, - {file = "numpy-1.21.1-cp39-cp39-win32.whl", hash = "sha256:88c0b89ad1cc24a5efbb99ff9ab5db0f9a86e9cc50240177a571fbe9c2860ac2"}, - {file = "numpy-1.21.1-cp39-cp39-win_amd64.whl", hash = "sha256:01721eefe70544d548425a07c80be8377096a54118070b8a62476866d5208e33"}, - {file = "numpy-1.21.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2d4d1de6e6fb3d28781c73fbde702ac97f03d79e4ffd6598b880b2d95d62ead4"}, - {file = "numpy-1.21.1.zip", hash = "sha256:dff4af63638afcc57a3dfb9e4b26d434a7a602d225b42d746ea7fe2edf1342fd"}, -] packaging = [ - {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, - {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] pep8-naming = [ {file = "pep8-naming-0.12.1.tar.gz", hash = "sha256:bb2455947757d162aa4cad55dba4ce029005cd1692f2899a21d51d8630ca7841"}, @@ -1416,12 +1284,12 @@ pip-licenses = [ {file = "pip_licenses-3.5.3-py3-none-any.whl", hash = "sha256:59c148d6a03784bf945d232c0dc0e9de4272a3675acaa0361ad7712398ca86ba"}, ] platformdirs = [ - {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, - {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, + {file = "platformdirs-2.4.1-py3-none-any.whl", hash = "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca"}, + {file = "platformdirs-2.4.1.tar.gz", hash = "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda"}, ] pre-commit = [ - {file = "pre_commit-2.15.0-py2.py3-none-any.whl", hash = "sha256:a4ed01000afcb484d9eb8d504272e642c4c4099bbad3a6b27e519bd6a3e928a6"}, - {file = "pre_commit-2.15.0.tar.gz", hash = "sha256:3c25add78dbdfb6a28a651780d5c311ac40dd17f160eb3954a0c59da40a505a7"}, + {file = "pre_commit-2.16.0-py2.py3-none-any.whl", hash = "sha256:758d1dc9b62c2ed8881585c254976d66eae0889919ab9b859064fc2fe3c7743e"}, + {file = "pre_commit-2.16.0.tar.gz", hash = "sha256:fe9897cac830aa7164dbd02a4e7b90cae49630451ce88464bca73db486ba9f65"}, ] psutil = [ {file = "psutil-5.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64"}, @@ -1506,8 +1374,8 @@ pyflakes = [ {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] pyparsing = [ - {file = "pyparsing-3.0.5-py3-none-any.whl", hash = "sha256:4881e3d2979f27b41a3a2421b10be9cbfa7ce2baa6c7117952222f8bbea6650c"}, - {file = "pyparsing-3.0.5.tar.gz", hash = "sha256:9329d1c1b51f0f76371c4ded42c5ec4cc0be18456b22193e0570c2da98ed288b"}, + {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, + {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, ] pyreadline3 = [ {file = "pyreadline3-3.3-py3-none-any.whl", hash = "sha256:0003fd0079d152ecbd8111202c5a7dfa6a5569ffd65b235e45f3c2ecbee337b4"}, @@ -1518,8 +1386,8 @@ python-dateutil = [ {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] python-dotenv = [ - {file = "python-dotenv-0.19.1.tar.gz", hash = "sha256:14f8185cc8d494662683e6914addcb7e95374771e707601dfc70166946b4c4b8"}, - {file = "python_dotenv-0.19.1-py2.py3-none-any.whl", hash = "sha256:bbd3da593fc49c249397cbfbcc449cf36cb02e75afc8157fcc6a81df6fb7750a"}, + {file = "python-dotenv-0.19.2.tar.gz", hash = "sha256:a5de49a31e953b45ff2d2fd434bbc2670e8db5273606c1e737cc6b93eff3655f"}, + {file = "python_dotenv-0.19.2-py2.py3-none-any.whl", hash = "sha256:32b2bdc1873fd3a3c346da1c6db83d0053c3c62f28f1f38516070c4c8971b1d3"}, ] pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, @@ -1553,61 +1421,62 @@ pyyaml = [ {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, ] rapidfuzz = [ - {file = "rapidfuzz-1.8.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:d218a4aac1488cc7d63f1a597a33863aa304f6ac590d70057e708ec6865a4118"}, - {file = "rapidfuzz-1.8.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:8e47b2f9f49edbfd582f6703cde54d22ffa98d83c8393ccd07073852b33832ea"}, - {file = "rapidfuzz-1.8.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:17e456d2ba6bee54b5c83bb403c33e02c3873958e153c0413021a2a042b0940d"}, - {file = "rapidfuzz-1.8.2-cp27-cp27m-win32.whl", hash = "sha256:ab067c4f04f037686d6cad1a7fce4c3998548f38778f0edb351280b902b8b3e1"}, - {file = "rapidfuzz-1.8.2-cp27-cp27m-win_amd64.whl", hash = "sha256:6c3e298aa955b164c85e7e0e2372805da1d6bae7399dad256211caafdab46e7f"}, - {file = "rapidfuzz-1.8.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f62471a08d2c46c79a8e0129b74c2997e62692c36bef47a7392b542d6dafc6bf"}, - {file = "rapidfuzz-1.8.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:271ce29c63eb87d76bcd384753cdfbfb8f2a0aeb3c7d0891787b1f19b007a0e8"}, - {file = "rapidfuzz-1.8.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:22f49eb345759602cc002200217a62564d947f65a568723f99b80c74027ce77b"}, - {file = "rapidfuzz-1.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4ec6015b74cdbd4ac8bbbf280522e913f11c4656e334559a8454713def4f3b33"}, - {file = "rapidfuzz-1.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d53a474e3b7592a2ff3d4c1e545324314845d3ccbbc80f6dd89d044bceaf8a59"}, - {file = "rapidfuzz-1.8.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:242ebe941991a086b9e455dadcdaf624bb895457e3ce254b0b51a7b9adc68fed"}, - {file = "rapidfuzz-1.8.2-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dab9822666819350aa32008fe35a7c7da422289d6961a01a2e00e99c2581e7fe"}, - {file = "rapidfuzz-1.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8107e175577f200a2426c4896b8e17a97b86f513122cba4155c7462f0fcc18ac"}, - {file = "rapidfuzz-1.8.2-cp310-cp310-win32.whl", hash = "sha256:526d92f1ff354303cba0a4cbf184d11b94395841d00eaecf8963c6dc89deec21"}, - {file = "rapidfuzz-1.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:55168678ae7df1ad8099ec2f0ce54024c924b375b18a6f5d3237c930083fcfca"}, - {file = "rapidfuzz-1.8.2-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:23c0632047bf8fac891ef73dfd397e951514d11fb5f168b630e3569ffcd51d61"}, - {file = "rapidfuzz-1.8.2-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:90d0e10c7075f761b17e590cf9254b32e7084de7e2b4cd11201031c61386714e"}, - {file = "rapidfuzz-1.8.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:c9537e7f2a489654671c4e3bdc3e01d1116052b3889217da7fe2f0b0f0b27e10"}, - {file = "rapidfuzz-1.8.2-cp35-cp35m-win32.whl", hash = "sha256:8ded55e5395af1705bbc89ab94bea3c73218e1f71ae2b72cd905a827ab131fa1"}, - {file = "rapidfuzz-1.8.2-cp35-cp35m-win_amd64.whl", hash = "sha256:2d4240fce22e74c6f3d381201288cea6cc5af8d310ec271b573b26558c2eaec8"}, - {file = "rapidfuzz-1.8.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:90901b7be174b1f14455c5601eff46b03b174cf1d61cec0fd6d37c74dd727c88"}, - {file = "rapidfuzz-1.8.2-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7dbd1524e0f2d4290cd60fe1474068a8d8a27a41e1dfa14b1e4d53735529a11c"}, - {file = "rapidfuzz-1.8.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b8eee29e35ce51adf1cbfcc5b64d1a89d71401a5101b8fe9b87abbe6bf4f893"}, - {file = "rapidfuzz-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:648fd2e561b6beb208e6cf46eb9f20ba952add69c2f90fb4e46898a821bca4c9"}, - {file = "rapidfuzz-1.8.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:40dacd641d93708818ff2c9921eb6e5b9463e9b3b7f410dde3c96e0e0f80c414"}, - {file = "rapidfuzz-1.8.2-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:28528d34fb73f991e2822fdf1aa28eff7832c2189c8d2d0b598f15dd1b1b7d88"}, - {file = "rapidfuzz-1.8.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f46137937dfa2ec04fbb9f0225ff3160b9f1ed993dc5abf126ccdeae38726c5"}, - {file = "rapidfuzz-1.8.2-cp37-cp37m-win32.whl", hash = "sha256:709ea3151a7448bb15adc3212cbdd4766eca557f1ae73cdff8ae656b6899e71a"}, - {file = "rapidfuzz-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c426eb833d16ddeec95c966efa78492803d741c85cf6553febf78979fc267692"}, - {file = "rapidfuzz-1.8.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:91e20676b562c972de8af6a153df6eaaea27da96ade8321e51da7bab32285242"}, - {file = "rapidfuzz-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ea3c3c1a5401affeb7d279c2b7cf418d5e7d7e170eb010fcb1b45bb28de5e4d1"}, - {file = "rapidfuzz-1.8.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ab21f86812a7da111a17c118fc8e827b4c289d02f39536f7a6acc6baf479dcc0"}, - {file = "rapidfuzz-1.8.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0eef79b223e239af5cc9c0b89ae8096a38bc8a9109474ff610151ea0115b931c"}, - {file = "rapidfuzz-1.8.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:20dfda3bd9c14e6d7951360af66792a15e81051fa017db00c71bad4b1e25688d"}, - {file = "rapidfuzz-1.8.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec24328d855b88e8a472379c61af9972f0d7992edfe2ebaca03afed2c5282f94"}, - {file = "rapidfuzz-1.8.2-cp38-cp38-win32.whl", hash = "sha256:69ab203a4b59c7e9ddec96044bd684d6165eab3362f84677822936fea2e8c4f1"}, - {file = "rapidfuzz-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:bd039be9798a3deba25301827b86b3ff084d69e2b4b16ae8da6a34638235680c"}, - {file = "rapidfuzz-1.8.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7219ca078df644aa0694af9174b7abb09a75302021f1e062f39fcf183cb9f087"}, - {file = "rapidfuzz-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d240b7cd8dc7890c103adf4666a43fb3b3d1e5b1231e59aa3986e18dba1d086f"}, - {file = "rapidfuzz-1.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06f639a65f1269a52679942437228c804a354618dda79b68a70612c5c0a36f4e"}, - {file = "rapidfuzz-1.8.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:750ea6977e68a17f16308ac044017f8ce35c8a553698696f5a4fbb77e3110c8c"}, - {file = "rapidfuzz-1.8.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:517f26474012dbce37358a447165ca3b100b9b1b83ee8b3d6877accfdb209d46"}, - {file = "rapidfuzz-1.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b2bf70a24e129af11e217d94cb34f96a1487606bf4a9d8c8f16bc49c9465b56"}, - {file = "rapidfuzz-1.8.2-cp39-cp39-win32.whl", hash = "sha256:02ec1154e62493c0cb66c8b058d7196de48acbee7e884b757cff7bf075252232"}, - {file = "rapidfuzz-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:be564aea09ec8da0478b67f35be73f8c129054d45da7f4b02cd7e035d1aea96d"}, - {file = "rapidfuzz-1.8.2-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:54406d07788ca85aff12134425a2b931e842ee2c3b1ae915523cdd3d104c1136"}, - {file = "rapidfuzz-1.8.2-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:5648fcf1cbeacb05ea4b441ddf99c7f5e15dd53b7d3192e7449581b638e8b3f7"}, - {file = "rapidfuzz-1.8.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7d1d6ab126f960aa0df10454a06d17e303680df6370cc3c44c1f0697c4587c5c"}, - {file = "rapidfuzz-1.8.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1412ec4d43f0d1854ed3dd220dacbb68b977519553e80a2c4a250d3569793f92"}, - {file = "rapidfuzz-1.8.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:9dd34ad050ccb680d9dbf9871a53e1f01b084488d5310349c3e9cedcb4561eb2"}, - {file = "rapidfuzz-1.8.2.tar.gz", hash = "sha256:d6efbb2b6b18b3a67d7bdfbcd9bb72732f55736852bbef823bdf210f9e0c6c90"}, + {file = "rapidfuzz-1.9.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:68227a8b25291d6a2140aef049271ea30a77be5ef672a58e582a55a5cc1fce93"}, + {file = "rapidfuzz-1.9.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c33541995b96ff40025c1456b8c74b7dd2ab9cbf91943fc35a7bb621f48940e2"}, + {file = "rapidfuzz-1.9.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:c2fafbbf97a4632822248f4201601b691e2eac5fdb30e5d7a96d07a6d058a7d4"}, + {file = "rapidfuzz-1.9.1-cp27-cp27m-win32.whl", hash = "sha256:364795f617a99e1dbb55ac3947ab8366588b72531cb2d6152666287d20610706"}, + {file = "rapidfuzz-1.9.1-cp27-cp27m-win_amd64.whl", hash = "sha256:f171d9e66144b0647f9b998ef10bdd919a640e4b1357250c8ef6259deb5ffe0d"}, + {file = "rapidfuzz-1.9.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:c83801a7c5209663aa120b815a4f2c39e95fe8e0b774ec58a1e0affd6a2fcfc6"}, + {file = "rapidfuzz-1.9.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:67e61c2baa6bb1848c4a33752f1781124dcc90bf3f31b18b44db1ae4e4e26634"}, + {file = "rapidfuzz-1.9.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8ab7eb003a18991347174910f11d38ff40399081185d9e3199ec277535f7828b"}, + {file = "rapidfuzz-1.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5ad450badf06ddf98a246140b5059ba895ee8445e8102a5a289908327f551f81"}, + {file = "rapidfuzz-1.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:402b2174bded62a793c5f7d9aec16bc32c661402360a934819ae72b54cfbce1e"}, + {file = "rapidfuzz-1.9.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:92066ccb054efc2e17afb4049c98b550969653cd58f71dd756cfcc8e6864630a"}, + {file = "rapidfuzz-1.9.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8dc0bf1814accee08a9c9bace6672ef06eae6b0446fce88e3e97e23dfaf3ea10"}, + {file = "rapidfuzz-1.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdbd387efb8478605951344f327dd03bf053c138d757369a43404305b99e55db"}, + {file = "rapidfuzz-1.9.1-cp310-cp310-win32.whl", hash = "sha256:b1c54807e556dbcc6caf4ce0f24446c01b195f3cc46e2a6e74b82d3a21eaa45d"}, + {file = "rapidfuzz-1.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:ac3273364cd1619cab3bf0ba731efea5405833f9eba362da7dcd70bd42073d8e"}, + {file = "rapidfuzz-1.9.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:d9faf62606c08a0a6992dd480c72b6a068733ae02688dc35f2e36ba0d44673f4"}, + {file = "rapidfuzz-1.9.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:f6a56a48be047637b1b0b2459a11cf7cd5aa7bbe16a439bd4f73b4af39e620e4"}, + {file = "rapidfuzz-1.9.1-cp35-cp35m-win32.whl", hash = "sha256:aa91609979e9d2700f0ff100df99b36e7d700b70169ee385d43d5de9e471ae97"}, + {file = "rapidfuzz-1.9.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b4cfdd0915ab4cec86c2ff6bab9f01b03454f3de0963c37f9f219df2ddf42b95"}, + {file = "rapidfuzz-1.9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c6bfa4ad0158a093cd304f795ceefdc3861ae6942a61432b2a50858be6de88ca"}, + {file = "rapidfuzz-1.9.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:eb0ea02295d9278bd2dcd2df4760b0f2887b6c3f2f374005ec5af320d8d3a37e"}, + {file = "rapidfuzz-1.9.1-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d5187cd5cd6273e9fee07de493a42a2153134a4914df74cb1abb0744551c548a"}, + {file = "rapidfuzz-1.9.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6e5b8af63f9c05b64454460759ed84a715d581d598ec4484f4ec512f398e8b1"}, + {file = "rapidfuzz-1.9.1-cp36-cp36m-win32.whl", hash = "sha256:36137f88f2b28115af506118e64e11c816611eab2434293af7fdacd1290ffb9d"}, + {file = "rapidfuzz-1.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:fcc420cad46be7c9887110edf04cdee545f26dbf22650a443d89790fc35f7b88"}, + {file = "rapidfuzz-1.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b06de314f426aebff8a44319016bbe2b22f7848c84e44224f80b0690b7b08b18"}, + {file = "rapidfuzz-1.9.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e5de44e719faea79e45322b037f0d4a141d750b80d2204fa68f43a42a24f0fbc"}, + {file = "rapidfuzz-1.9.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f9439df09a782afd01b67005a3b110c70bbf9e1cf06d2ac9b293ce2d02d3c549"}, + {file = "rapidfuzz-1.9.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e903d4702647465721e2d0431c95f04fd56a06577f06f41e2960c83fd63c1bad"}, + {file = "rapidfuzz-1.9.1-cp37-cp37m-win32.whl", hash = "sha256:a5298f4ac1975edcbb15583eab659a44b33aebaf3bccf172e185cfea68771c08"}, + {file = "rapidfuzz-1.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:103193a01921b54fcdad6b01cfda3a68e00aeafca236b7ecd5b1b2c2e7e96337"}, + {file = "rapidfuzz-1.9.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1d98a3187040dca855e02179a35c137f72ef83ce243783d44ea59efa86b94b3a"}, + {file = "rapidfuzz-1.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cb92bf7fc911b787055a88d9295ca3b4fe8576e3b59271f070f1b1b181eb087d"}, + {file = "rapidfuzz-1.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3f014a0f5f8159a94c6ee884fedd1c30e07fb866a5d76ff2c18091bc6363b76f"}, + {file = "rapidfuzz-1.9.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:31474074a99f72289ac325fbd77983e7d355d48860bfe7a4f6f6396fdb24410a"}, + {file = "rapidfuzz-1.9.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec67d79af5a2d7b0cf67b570a5579710e461cadda4120478e813b63491f394dd"}, + {file = "rapidfuzz-1.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ebc0d3d15ed32f98f0052cf6e3e9c9b8010fb93c04fb74d2022e3c51ec540e2"}, + {file = "rapidfuzz-1.9.1-cp38-cp38-win32.whl", hash = "sha256:477ab1a3044bab89db45caabc562b158f68765ecaa638b73ba17e92f09dfa5ff"}, + {file = "rapidfuzz-1.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:8e872763dc0367d7544aa585d2e8b27af233323b8a7cd2f9b78cafa05bae5018"}, + {file = "rapidfuzz-1.9.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8401c41e219ae36ca7a88762776a6270511650d4cc70d024ae61561e96d67e47"}, + {file = "rapidfuzz-1.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ea10bd8e0436801c3264f7084a5ea194f12ba9fe1ba898aa4a2107d276501292"}, + {file = "rapidfuzz-1.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:433737914b46c1ffa0c678eceae1c260dc6b7fb5b6cad4c725d3e3607c764b32"}, + {file = "rapidfuzz-1.9.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8c3b08e90e45acbc469d1f456681643256e952bf84ec7714f58979baba0c8a1c"}, + {file = "rapidfuzz-1.9.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bbcd265b3c86176e5db4cbba7b4364d7333c214ee80e2d259c7085929934ca9d"}, + {file = "rapidfuzz-1.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d69fabcd635783cd842e7d5ee4b77164314c5124b82df5a0c436ab3d698f8a9"}, + {file = "rapidfuzz-1.9.1-cp39-cp39-win32.whl", hash = "sha256:01f16b6f3fa5d1a26c12f5da5de0032f1e12c919d876005b57492a8ec9a5c043"}, + {file = "rapidfuzz-1.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:0bcc5bbfdbe6068cc2cf0029ab6cde08dceac498d232fa3a61dd34fbfa0b3f36"}, + {file = "rapidfuzz-1.9.1-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:de869c8f4e8edb9b2f7b8232a04896645501defcbd9d85bc0202ff3ec6285f6b"}, + {file = "rapidfuzz-1.9.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:db5978e970fb0955974d51021da4b929e2e4890fef17792989ee32658e2b159c"}, + {file = "rapidfuzz-1.9.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:33479f75f36ac3a1d8421365d4fa906e013490790730a89caba31d06e6f71738"}, + {file = "rapidfuzz-1.9.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:af991cb333ec526d894923163050931b3a870b7694bf7687aaa6154d341a98f5"}, + {file = "rapidfuzz-1.9.1.tar.gz", hash = "sha256:bd7a4fe33ba49db3417f0f57a8af02462554f1296dedcf35b026cd3525efef74"}, ] redis = [ - {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"}, - {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, + {file = "redis-4.0.2-py3-none-any.whl", hash = "sha256:c8481cf414474e3497ec7971a1ba9b998c8efad0f0d289a009a5bbef040894f9"}, + {file = "redis-4.0.2.tar.gz", hash = "sha256:ccf692811f2c1fc7a92b466aa2599e4a6d2d73d5f736a2c70be600657c0da34a"}, ] sentry-sdk = [ {file = "sentry-sdk-0.20.3.tar.gz", hash = "sha256:4ae8d1ced6c67f1c8ea51d82a16721c166c489b76876c9f2c202b8a50334b237"}, @@ -1618,16 +1487,16 @@ six = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] snowballstemmer = [ - {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, - {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] sortedcontainers = [ {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] soupsieve = [ - {file = "soupsieve-2.3-py3-none-any.whl", hash = "sha256:617ffc4d0dfd39c66f4d1413a6e165663a34eca86be9b54f97b91756300ff6df"}, - {file = "soupsieve-2.3.tar.gz", hash = "sha256:e4860f889dfa88774c07da0b276b70c073b6470fa1a4a8350800bb7bce3dcc76"}, + {file = "soupsieve-2.3.1-py3-none-any.whl", hash = "sha256:1a3cca2617c6b38c0343ed661b1fa5de5637f257d4fe22bd9f1338010a1efefb"}, + {file = "soupsieve-2.3.1.tar.gz", hash = "sha256:b8d49b1cd4f037c7082a9683dfa1801aa2597fb11c3a1155b7a5b94829b4f1f9"}, ] taskipy = [ {file = "taskipy-1.9.0-py3-none-any.whl", hash = "sha256:02bd2c51c7356ed3f7f8853210ada1cd2ab273e68359ee865021c3057eec6615"}, @@ -1642,17 +1511,69 @@ toml = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] typing-extensions = [ - {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, - {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, - {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, + {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, + {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, ] urllib3 = [ {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, ] virtualenv = [ - {file = "virtualenv-20.10.0-py2.py3-none-any.whl", hash = "sha256:4b02e52a624336eece99c96e3ab7111f469c24ba226a53ec474e8e787b365814"}, - {file = "virtualenv-20.10.0.tar.gz", hash = "sha256:576d05b46eace16a9c348085f7d0dc8ef28713a2cabaa1cf0aea41e8f12c9218"}, + {file = "virtualenv-20.11.0-py2.py3-none-any.whl", hash = "sha256:eb0cb34160f32c6596405308ee6a8a4abbf3247b2b9794ae655a156d43abf48e"}, + {file = "virtualenv-20.11.0.tar.gz", hash = "sha256:2f15b9226cb74b59c21e8236dd791c395bee08cdd33b99cddd18e1f866cdb098"}, +] +wrapt = [ + {file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca"}, + {file = "wrapt-1.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44"}, + {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056"}, + {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785"}, + {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096"}, + {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33"}, + {file = "wrapt-1.13.3-cp310-cp310-win32.whl", hash = "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f"}, + {file = "wrapt-1.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755"}, + {file = "wrapt-1.13.3-cp35-cp35m-win32.whl", hash = "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851"}, + {file = "wrapt-1.13.3-cp35-cp35m-win_amd64.whl", hash = "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13"}, + {file = "wrapt-1.13.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918"}, + {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade"}, + {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc"}, + {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf"}, + {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125"}, + {file = "wrapt-1.13.3-cp36-cp36m-win32.whl", hash = "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36"}, + {file = "wrapt-1.13.3-cp36-cp36m-win_amd64.whl", hash = "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10"}, + {file = "wrapt-1.13.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068"}, + {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709"}, + {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df"}, + {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2"}, + {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b"}, + {file = "wrapt-1.13.3-cp37-cp37m-win32.whl", hash = "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829"}, + {file = "wrapt-1.13.3-cp37-cp37m-win_amd64.whl", hash = "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea"}, + {file = "wrapt-1.13.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9"}, + {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554"}, + {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c"}, + {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b"}, + {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce"}, + {file = "wrapt-1.13.3-cp38-cp38-win32.whl", hash = "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79"}, + {file = "wrapt-1.13.3-cp38-cp38-win_amd64.whl", hash = "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb"}, + {file = "wrapt-1.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb"}, + {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32"}, + {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7"}, + {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e"}, + {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640"}, + {file = "wrapt-1.13.3-cp39-cp39-win32.whl", hash = "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374"}, + {file = "wrapt-1.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb"}, + {file = "wrapt-1.13.3.tar.gz", hash = "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185"}, ] yarl = [ {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"}, diff --git a/pyproject.toml b/pyproject.toml index d758385e..2a216209 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,6 @@ sentry-sdk = "~=0.19" PyYAML = "~=5.4" async-rediscache = {extras = ["fakeredis"], version = "~=0.1.4"} emojis = "~=0.6.0" -matplotlib = "~=3.4.1" coloredlogs = "~=15.0" colorama = { version = "~=0.4.3", markers = "sys_platform == 'win32'" } lxml = "~=4.6" -- cgit v1.2.3 From 06a704aae87ecd0d4a9e65263be25185eee66b69 Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 25 Dec 2021 22:11:49 +0000 Subject: Add member util functions This adds some useful utils, get_or_fetch and handle role change. These utils handle errors themselves, so can simplify implementations within the commands. --- bot/utils/members.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 bot/utils/members.py diff --git a/bot/utils/members.py b/bot/utils/members.py new file mode 100644 index 00000000..9c8c8dd8 --- /dev/null +++ b/bot/utils/members.py @@ -0,0 +1,47 @@ +import logging +import typing as t + +import discord + +log = logging.getLogger(__name__) + + +async def get_or_fetch_member(guild: discord.Guild, member_id: int) -> t.Optional[discord.Member]: + """ + Attempt to get a member from cache; on failure fetch from the API. + + Return `None` to indicate the member could not be found. + """ + if member := guild.get_member(member_id): + log.trace("%s retrieved from cache.", member) + else: + try: + member = await guild.fetch_member(member_id) + except discord.errors.NotFound: + log.trace("Failed to fetch %d from API.", member_id) + return None + log.trace("%s fetched from API.", member) + return member + + +async def handle_role_change( + member: discord.Member, + coro: t.Callable[..., t.Coroutine], + role: discord.Role +) -> None: + """ + Change `member`'s cooldown role via awaiting `coro` and handle errors. + + `coro` is intended to be `discord.Member.add_roles` or `discord.Member.remove_roles`. + """ + try: + await coro(role) + except discord.NotFound: + log.debug(f"Failed to change role for {member} ({member.id}): member not found") + except discord.Forbidden: + log.debug( + f"Forbidden to change role for {member} ({member.id}); " + f"possibly due to role hierarchy" + ) + except discord.HTTPException as e: + log.error(f"Failed to change role for {member} ({member.id}): {e.status} {e.code}") -- cgit v1.2.3 From 21518aed7b3cfb9c2ec4fa1722689a6c4df56372 Mon Sep 17 00:00:00 2001 From: ChrisLovering Date: Sat, 25 Dec 2021 22:23:12 +0000 Subject: Add hourly task to assign AoC completer role This task uses the cached leaderboard to see who has all 50 stars and assigns them a role to highlight them as completers. --- bot/constants.py | 1 + bot/exts/events/advent_of_code/_cog.py | 60 ++++++++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 854fbe55..01f825a0 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -291,6 +291,7 @@ class Roles(NamedTuple): helpers = int(environ.get("ROLE_HELPERS", 267630620367257601)) core_developers = 587606783669829632 everyone = int(environ.get("BOT_GUILD", 267624335836053506)) + aoc_completionist = int(environ.get("AOC_COMPLETIONIST_ROLE_ID", 916691790181056532)) class Tokens(NamedTuple): diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 52254ea1..6974d89c 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -7,12 +7,15 @@ from typing import Optional import arrow import discord from async_rediscache import RedisCache -from discord.ext import commands +from discord.ext import commands, tasks from bot.bot import Bot -from bot.constants import AdventOfCode as AocConfig, Channels, Colours, Emojis, Month, Roles, WHITELISTED_CHANNELS +from bot.constants import ( + AdventOfCode as AocConfig, Channels, Client, Colours, Emojis, Month, Roles, WHITELISTED_CHANNELS +) from bot.exts.events.advent_of_code import _helpers from bot.exts.events.advent_of_code.views.dayandstarview import AoCDropdownView +from bot.utils import members from bot.utils.decorators import InChannelCheckFailure, in_month, whitelist_override, with_role from bot.utils.extensions import invoke_help_command @@ -31,6 +34,7 @@ class AdventOfCode(commands.Cog): """Advent of Code festivities! Ho Ho Ho!""" # Redis Cache for linking Discord IDs to Advent of Code usernames + # RedisCache[member_id: aoc_username_string] account_links = RedisCache() def __init__(self, bot: Bot): @@ -52,6 +56,57 @@ class AdventOfCode(commands.Cog): self.status_task.set_name("AoC Status Countdown") self.status_task.add_done_callback(_helpers.background_task_callback) + self.completionist_task.start() + + @tasks.loop(minutes=10.0) + async def completionist_task(self) -> None: + """ + Give members who have completed all 50 AoC stars the completionist role. + + Runs on a schedule, as defined in the task.loop decorator. + """ + await self.bot.wait_until_guild_available() + guild = self.bot.get_guild(Client.guild) + completionist_role = guild.get_role(Roles.aoc_completionist) + if completionist_role is None: + log.warning("Could not find the AoC completionist role; cancelling completionist task.") + self.completer_task.cancel() + return + + aoc_name_to_member_id = { + aoc_name: member_id + for member_id, aoc_name in await self.account_links.items() + } + + try: + leaderboard = await _helpers.fetch_leaderboard() + except _helpers.FetchingLeaderboardFailedError: + await self.bot.send_log("Unable to fetch AoC leaderboard during role sync.") + return + + placement_leaderboard = json.loads(leaderboard["placement_leaderboard"]) + + for member_aoc_info in placement_leaderboard.values(): + if not member_aoc_info["stars"] == 50: + # Only give the role to people who have completed all 50 stars + continue + + member_id = aoc_name_to_member_id[member_aoc_info["name"]] + if not member_id: + continue + + member = members.get_or_fetch_member(guild, member_id) + if member is None: + continue + + if completionist_role in member.roles: + continue + + if await self.completionist_block_list.contains(member_id): + continue + + await members.handle_role_change(member, member.add_roles, completionist_role) + @commands.group(name="adventofcode", aliases=("aoc",)) @whitelist_override(channels=AOC_WHITELIST) async def adventofcode_group(self, ctx: commands.Context) -> None: @@ -408,6 +463,7 @@ class AdventOfCode(commands.Cog): log.debug("Unloading the cog and canceling the background task.") self.notification_task.cancel() self.status_task.cancel() + self.completionist_task.cancel() def _build_about_embed(self) -> discord.Embed: """Build and return the informational "About AoC" embed from the resources file.""" -- cgit v1.2.3 From e44460d56364ff45ab19352af9719da5def4161f Mon Sep 17 00:00:00 2001 From: ChrisLovering Date: Sat, 25 Dec 2021 22:28:10 +0000 Subject: Ability to block users from AoC completer role --- bot/exts/events/advent_of_code/_cog.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 6974d89c..67d43556 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -37,6 +37,10 @@ class AdventOfCode(commands.Cog): # RedisCache[member_id: aoc_username_string] account_links = RedisCache() + # A dict with keys of member_ids to block from getting the role + # RedisCache[member_id: None] + completionist_block_list = RedisCache() + def __init__(self, bot: Bot): self.bot = bot @@ -91,11 +95,11 @@ class AdventOfCode(commands.Cog): # Only give the role to people who have completed all 50 stars continue - member_id = aoc_name_to_member_id[member_aoc_info["name"]] + member_id = aoc_name_to_member_id.get(member_aoc_info["name"], None) if not member_id: continue - member = members.get_or_fetch_member(guild, member_id) + member = await members.get_or_fetch_member(guild, member_id) if member is None: continue @@ -114,6 +118,19 @@ class AdventOfCode(commands.Cog): if not ctx.invoked_subcommand: await invoke_help_command(ctx) + @with_role(Roles.admins) + @adventofcode_group.command( + name="block", + brief="Block a user from getting the completionist role.", + ) + async def block_from_role(self, ctx: commands.Context, member: discord.Member) -> None: + """Block the given member from receiving the AoC completionist role, removing it from them if needed.""" + completionist_role = ctx.guild.get_role(Roles.aoc_completionist) + if completionist_role in member.roles: + await member.remove_roles(completionist_role) + + await self.completionist_block_list.set(member.id, "sentinel") + @commands.guild_only() @adventofcode_group.command( name="subscribe", -- cgit v1.2.3 From d55cbc8e4ed371f0ca11b97b1dd6e41cf8f6719c Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 28 Dec 2021 22:20:01 +0000 Subject: Condense conditional logic in AoC completionist role task --- bot/exts/events/advent_of_code/_cog.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 67d43556..9d412adf 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -100,16 +100,11 @@ class AdventOfCode(commands.Cog): continue member = await members.get_or_fetch_member(guild, member_id) - if member is None: + if member is None or completionist_role in member.roles: continue - if completionist_role in member.roles: - continue - - if await self.completionist_block_list.contains(member_id): - continue - - await members.handle_role_change(member, member.add_roles, completionist_role) + if not await self.completionist_block_list.contains(member_id): + await members.handle_role_change(member, member.add_roles, completionist_role) @commands.group(name="adventofcode", aliases=("aoc",)) @whitelist_override(channels=AOC_WHITELIST) -- cgit v1.2.3 From dc2e3a72cf7958d9410d3b1f1e9d13d1eadcef89 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 28 Dec 2021 22:41:20 +0000 Subject: Inform invoker after successfully blocking a user from AoC comp. role --- bot/exts/events/advent_of_code/_cog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 9d412adf..30bcaae6 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -125,6 +125,7 @@ class AdventOfCode(commands.Cog): await member.remove_roles(completionist_role) await self.completionist_block_list.set(member.id, "sentinel") + await ctx.send(f":+1: Blocked {member.mention} from getting the AoC completionist role.") @commands.guild_only() @adventofcode_group.command( -- cgit v1.2.3 From 4ecec867d9f1d06f23d7fd7216dd0902de82cfa0 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 29 Dec 2021 13:34:24 +0000 Subject: Add logging to AoC role task to help debugging issues --- bot/exts/events/advent_of_code/_cog.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 30bcaae6..c597fd0e 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -74,7 +74,7 @@ class AdventOfCode(commands.Cog): completionist_role = guild.get_role(Roles.aoc_completionist) if completionist_role is None: log.warning("Could not find the AoC completionist role; cancelling completionist task.") - self.completer_task.cancel() + self.completionist_task.cancel() return aoc_name_to_member_id = { @@ -97,13 +97,20 @@ class AdventOfCode(commands.Cog): member_id = aoc_name_to_member_id.get(member_aoc_info["name"], None) if not member_id: + log.debug(f"Could not find member_id for {member_aoc_info['name']}, not giving role.") continue member = await members.get_or_fetch_member(guild, member_id) - if member is None or completionist_role in member.roles: + if member is None: + log.debug(f"Could not find {member_id}, not giving role.") + continue + + if completionist_role in member.roles: + log.debug(f"{member.name} ({member.mention}) already has the completionist role.") continue if not await self.completionist_block_list.contains(member_id): + log.debug(f"Giving completionist role to {member.name} ({member.mention}).") await members.handle_role_change(member, member.add_roles, completionist_role) @commands.group(name="adventofcode", aliases=("aoc",)) -- cgit v1.2.3 From 0c043c825482578e4a940ce2a8f0d4b5a952fe29 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 29 Dec 2021 13:35:10 +0000 Subject: Raise error when bot can't modify the given role in member helper util --- bot/utils/members.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/utils/members.py b/bot/utils/members.py index 9c8c8dd8..de5850ca 100644 --- a/bot/utils/members.py +++ b/bot/utils/members.py @@ -39,7 +39,7 @@ async def handle_role_change( except discord.NotFound: log.debug(f"Failed to change role for {member} ({member.id}): member not found") except discord.Forbidden: - log.debug( + log.error( f"Forbidden to change role for {member} ({member.id}); " f"possibly due to role hierarchy" ) -- cgit v1.2.3 From 8907a96b6a2a26ab8885cd5dd7bec0874f419e3e Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 1 Jan 2022 14:24:24 +0000 Subject: Allow AoC commands to be run in January --- bot/exts/events/advent_of_code/_cog.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index c597fd0e..493d58b2 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -254,7 +254,7 @@ class AdventOfCode(commands.Cog): else: await ctx.message.add_reaction(Emojis.envelope) - @in_month(Month.NOVEMBER, Month.DECEMBER) + @in_month(Month.NOVEMBER, Month.DECEMBER, Month.JANUARY) @adventofcode_group.command( name="link", aliases=("connect",), @@ -306,7 +306,7 @@ class AdventOfCode(commands.Cog): " Please re-run the command with one specified." ) - @in_month(Month.NOVEMBER, Month.DECEMBER) + @in_month(Month.NOVEMBER, Month.DECEMBER, Month.JANUARY) @adventofcode_group.command( name="unlink", aliases=("disconnect",), @@ -327,7 +327,7 @@ class AdventOfCode(commands.Cog): log.info(f"Attempted to unlink {ctx.author} ({ctx.author.id}), but no link was found.") await ctx.reply("You don't have an Advent of Code account linked.") - @in_month(Month.DECEMBER) + @in_month(Month.DECEMBER, Month.JANUARY) @adventofcode_group.command( name="dayandstar", aliases=("daynstar", "daystar"), @@ -365,7 +365,7 @@ class AdventOfCode(commands.Cog): await view.wait() await message.edit(view=None) - @in_month(Month.DECEMBER) + @in_month(Month.DECEMBER, Month.JANUARY) @adventofcode_group.command( name="leaderboard", aliases=("board", "lb"), @@ -410,7 +410,7 @@ class AdventOfCode(commands.Cog): await ctx.send(content=f"{header}\n\n{table}", embed=info_embed) return - @in_month(Month.DECEMBER) + @in_month(Month.DECEMBER, Month.JANUARY) @adventofcode_group.command( name="global", aliases=("globalboard", "gb"), -- cgit v1.2.3 From d32c15206be6a1a726a57eea614de4856d5473d8 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 1 Jan 2022 14:26:34 +0000 Subject: Allow AoC join to be ran in the month before and after the event --- bot/exts/events/advent_of_code/_cog.py | 10 +++++++--- tox.ini | 6 ++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 493d58b2..a9625153 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -213,9 +213,13 @@ class AdventOfCode(commands.Cog): @whitelist_override(channels=AOC_WHITELIST) async def join_leaderboard(self, ctx: commands.Context) -> None: """DM the user the information for joining the Python Discord leaderboard.""" - current_year = datetime.now().year - if current_year != AocConfig.year: - await ctx.send(f"The Python Discord leaderboard for {current_year} is not yet available!") + current_date = datetime.now() + if ( + current_date.month not in (Month.NOVEMBER, Month.DECEMBER) and current_date.year != AocConfig.year or + current_date.month != Month.JANUARY and current_date.year != AocConfig.year + 1 + ): + # Only allow joining the leaderboard in the run up to AOC and the January following. + await ctx.send(f"The Python Discord leaderboard for {current_date.year} is not yet available!") return author = ctx.author diff --git a/tox.ini b/tox.ini index af87e6fc..f561fcd9 100644 --- a/tox.ini +++ b/tox.ini @@ -11,9 +11,11 @@ ignore= # Docstring Quotes D301,D302, # Docstring Content - D400,D401,D402,D404,D405,D406,D407,D408,D409,D410,D411,D412,D413,D414,D416,D417 + D400,D401,D402,D404,D405,D406,D407,D408,D409,D410,D411,D412,D413,D414,D416,D417, # Type Annotations - ANN002,ANN003,ANN101,ANN102,ANN204,ANN206 + ANN002,ANN003,ANN101,ANN102,ANN204,ANN206, + # Binary operators over multiple lines + W504, exclude= __pycache__,.cache, venv,.venv, -- cgit v1.2.3 From 9d915091c9f23782034fa7a8278cc00f994c593f Mon Sep 17 00:00:00 2001 From: Sn4u <35849006+Sn4u@users.noreply.github.com> Date: Sat, 1 Jan 2022 23:30:06 +0000 Subject: add missing enums --- bot/exts/fun/game.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/bot/exts/fun/game.py b/bot/exts/fun/game.py index f9c150e6..5f56bef7 100644 --- a/bot/exts/fun/game.py +++ b/bot/exts/fun/game.py @@ -118,6 +118,7 @@ class GameStatus(IntEnum): Offline = 5 Cancelled = 6 Rumored = 7 + Delisted = 8 class AgeRatingCategories(IntEnum): @@ -125,6 +126,11 @@ class AgeRatingCategories(IntEnum): ESRB = 1 PEGI = 2 + CERO = 3 + USK = 4 + GRAC = 5 + CLASS_IND = 6 + ACB = 7 class AgeRatings(IntEnum): @@ -142,6 +148,32 @@ class AgeRatings(IntEnum): T = 10 M = 11 AO = 12 + CERO_A = 13 + CERO_B = 14 + CERO_C = 15 + CERO_D = 16 + CERO_Z = 17 + USK_0 = 18 + USK_6 = 19 + USK_12 = 20 + USK_18 = 21 + GRAC_ALL = 22 + GRAC_Twelve = 23 + GRAC_Fifteen = 24 + GRAC_Eighteen = 25 + GRAC_TESTING = 26 + CLASS_IND_L = 27 + CLASS_IND_Ten = 28 + CLASS_IND_Twelve = 29 + CLASS_IND_Fourteen = 30 + CLASS_IND_Sixteen = 31 + CLASS_IND_Eighteen = 32 + ACB_G = 33 + ACB_PG = 34 + ACB_M = 35 + ACB_MA15 = 36 + ACB_R18 = 37 + ACB_RC = 38 class Games(Cog): -- cgit v1.2.3 From 7cc3e1ceeee906c649013bd9d205b50cb5dcad59 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Fri, 3 Dec 2021 14:55:50 -0500 Subject: nit: make last refreshed time above the rest of the embed --- bot/exts/events/advent_of_code/_helpers.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/bot/exts/events/advent_of_code/_helpers.py b/bot/exts/events/advent_of_code/_helpers.py index 807cc275..ac2a0f6b 100644 --- a/bot/exts/events/advent_of_code/_helpers.py +++ b/bot/exts/events/advent_of_code/_helpers.py @@ -368,11 +368,13 @@ def get_summary_embed(leaderboard: dict) -> discord.Embed: """Get an embed with the current summary stats of the leaderboard.""" leaderboard_url = leaderboard["full_leaderboard_url"] refresh_minutes = AdventOfCode.leaderboard_cache_expiry_seconds // 60 + refreshed_unix = int(datetime.datetime.fromisoformat(leaderboard["leaderboard_fetched_at"]).timestamp()) - aoc_embed = discord.Embed( - colour=Colours.soft_green, - timestamp=datetime.datetime.fromisoformat(leaderboard["leaderboard_fetched_at"]), - description=f"*The leaderboard is refreshed every {refresh_minutes} minutes.*" + aoc_embed = discord.Embed(colour=Colours.soft_green) + + aoc_embed.description = ( + f"The leaderboard is refreshed every {refresh_minutes} minutes.\n" + f"Last Updated: " ) aoc_embed.add_field( name="Number of Participants", @@ -386,7 +388,6 @@ def get_summary_embed(leaderboard: dict) -> discord.Embed: inline=True, ) aoc_embed.set_author(name="Advent of Code", url=leaderboard_url) - aoc_embed.set_footer(text="Last Updated") aoc_embed.set_thumbnail(url=AOC_EMBED_THUMBNAIL) return aoc_embed -- cgit v1.2.3 From 4c451b170e0005726dac2489835045c1dc43ce62 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Sun, 2 Jan 2022 01:23:27 -0500 Subject: fix: don't rely on the current timezone --- bot/exts/events/advent_of_code/_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/events/advent_of_code/_helpers.py b/bot/exts/events/advent_of_code/_helpers.py index ac2a0f6b..15b1329d 100644 --- a/bot/exts/events/advent_of_code/_helpers.py +++ b/bot/exts/events/advent_of_code/_helpers.py @@ -332,7 +332,7 @@ async def fetch_leaderboard(invalidate_cache: bool = False, self_placement_name: number_of_participants = len(leaderboard) formatted_leaderboard = _format_leaderboard(leaderboard) full_leaderboard_url = await _upload_leaderboard(formatted_leaderboard) - leaderboard_fetched_at = datetime.datetime.utcnow().isoformat() + leaderboard_fetched_at = datetime.datetime.now(datetime.timezone.utc).isoformat() cached_leaderboard = { "placement_leaderboard": json.dumps(raw_leaderboard_data), -- cgit v1.2.3 From cf7a432a44ef47bca28249cf514df3b431a64bdc Mon Sep 17 00:00:00 2001 From: DMFriends <86751519+DMFriends@users.noreply.github.com> Date: Sun, 2 Jan 2022 07:09:29 -0500 Subject: Add a Madlibs game (#901) Co-authored-by: Bluenix Co-authored-by: Shom770 <82843611+Shom770@users.noreply.github.com> Co-authored-by: aru Co-authored-by: ChrisJL --- bot/exts/fun/madlibs.py | 148 +++++++++++++++++++++++++++++++ bot/resources/fun/madlibs_templates.json | 135 ++++++++++++++++++++++++++++ 2 files changed, 283 insertions(+) create mode 100644 bot/exts/fun/madlibs.py create mode 100644 bot/resources/fun/madlibs_templates.json diff --git a/bot/exts/fun/madlibs.py b/bot/exts/fun/madlibs.py new file mode 100644 index 00000000..21708e53 --- /dev/null +++ b/bot/exts/fun/madlibs.py @@ -0,0 +1,148 @@ +import json +from asyncio import TimeoutError +from pathlib import Path +from random import choice +from typing import TypedDict + +import discord +from discord.ext import commands + +from bot.bot import Bot +from bot.constants import Colours, NEGATIVE_REPLIES + +TIMEOUT = 60.0 + + +class MadlibsTemplate(TypedDict): + """Structure of a template in the madlibs JSON file.""" + + title: str + blanks: list[str] + value: list[str] + + +class Madlibs(commands.Cog): + """Cog for the Madlibs game.""" + + def __init__(self, bot: Bot): + self.bot = bot + self.templates = self._load_templates() + self.edited_content = {} + self.checks = set() + + @staticmethod + def _load_templates() -> list[MadlibsTemplate]: + madlibs_stories = Path("bot/resources/fun/madlibs_templates.json") + + with open(madlibs_stories) as file: + return json.load(file) + + @staticmethod + def madlibs_embed(part_of_speech: str, number_of_inputs: int) -> discord.Embed: + """Method to generate an embed with the game information.""" + madlibs_embed = discord.Embed(title="Madlibs", color=Colours.python_blue) + + madlibs_embed.add_field( + name="Enter a word that fits the given part of speech!", + value=f"Part of speech: {part_of_speech}\n\nMake sure not to spam, or you may get auto-muted!" + ) + + madlibs_embed.set_footer(text=f"Inputs remaining: {number_of_inputs}") + + return madlibs_embed + + @commands.Cog.listener() + async def on_message_edit(self, _: discord.Message, after: discord.Message) -> None: + """A listener that checks for message edits from the user.""" + for check in self.checks: + if check(after): + break + else: + return + + self.edited_content[after.id] = after.content + + @commands.command() + @commands.max_concurrency(1, per=commands.BucketType.user) + async def madlibs(self, ctx: commands.Context) -> None: + """ + Play Madlibs with the bot! + + Madlibs is a game where the player is asked to enter a word that + fits a random part of speech (e.g. noun, adjective, verb, plural noun, etc.) + a random amount of times, depending on the story chosen by the bot at the beginning. + """ + random_template = choice(self.templates) + + def author_check(message: discord.Message) -> bool: + return message.channel.id == ctx.channel.id and message.author.id == ctx.author.id + + self.checks.add(author_check) + + loading_embed = discord.Embed( + title="Madlibs", description="Loading your Madlibs game...", color=Colours.python_blue + ) + original_message = await ctx.send(embed=loading_embed) + + submitted_words = {} + + for i, part_of_speech in enumerate(random_template["blanks"]): + inputs_left = len(random_template["blanks"]) - i + + madlibs_embed = self.madlibs_embed(part_of_speech, inputs_left) + await original_message.edit(embed=madlibs_embed) + + try: + message = await self.bot.wait_for(event="message", check=author_check, timeout=TIMEOUT) + except TimeoutError: + timeout_embed = discord.Embed( + title=choice(NEGATIVE_REPLIES), + description="Uh oh! You took too long to respond!", + color=Colours.soft_red + ) + + await ctx.send(ctx.author.mention, embed=timeout_embed) + + for msg_id in submitted_words: + self.edited_content.pop(msg_id, submitted_words[msg_id]) + + self.checks.remove(author_check) + + return + + submitted_words[message.id] = message.content + + blanks = [self.edited_content.pop(msg_id, submitted_words[msg_id]) for msg_id in submitted_words] + + self.checks.remove(author_check) + + story = [] + for value, blank in zip(random_template["value"], blanks): + story.append(f"{value}__{blank}__") + + # In each story template, there is always one more "value" + # (fragment from the story) than there are blanks (words that the player enters) + # so we need to compensate by appending the last line of the story again. + story.append(random_template["value"][-1]) + + story_embed = discord.Embed( + title=random_template["title"], + description="".join(story), + color=Colours.bright_green + ) + + story_embed.set_footer(text=f"Generated for {ctx.author}", icon_url=ctx.author.display_avatar.url) + + await ctx.send(embed=story_embed) + + @madlibs.error + async def handle_madlibs_error(self, ctx: commands.Context, error: commands.CommandError) -> None: + """Error handler for the Madlibs command.""" + if isinstance(error, commands.MaxConcurrencyReached): + await ctx.send("You are already playing Madlibs!") + error.handled = True + + +def setup(bot: Bot) -> None: + """Load the Madlibs cog.""" + bot.add_cog(Madlibs(bot)) diff --git a/bot/resources/fun/madlibs_templates.json b/bot/resources/fun/madlibs_templates.json new file mode 100644 index 00000000..0023d48f --- /dev/null +++ b/bot/resources/fun/madlibs_templates.json @@ -0,0 +1,135 @@ +[ + { + "title": "How To Cross a Piranha-Infested River", + "blanks": ["foreign country", "adverb", "adjective", "animal", + "verb ending in 'ing'", "verb", "verb ending in 'ing'", + "adverb", "adjective", "a place", "type of liquid", "part of the body", "verb"], + "value": ["If you are traveling in ", " and find yourself having to cross a piranha-filled river, here's how to do it ", + ": \n* Piranhas are more ", " during the day, so cross the river at night.\n* Avoid areas with netted ", + " traps--piranhas may be ", " there looking to ", " them!\n* When "," the river, swim ", + ". You don't want to wake them up and make them ", "!\n* Whatever you do, if you have an open wound, try to find another way to get back to the ", + ". Piranhas are attracted to fresh ", " and will most likely take a bite out of your ", " if you ", " in the water!"] + }, + { + "title": "Three Little Pigs", + "blanks": ["adjective", "verb", "verb", "verb", "plural noun", "verb", "verb", "past tense verb", "plural noun", "adjective", "verb", + "plural noun", "noun", "verb", "past tense verb", "noun", "noun", "noun", "past tense verb", "adjective", "past tense verb", + "past tense verb", "noun", "past tense verb"], + "value": ["Once up a time, there were three ", " pigs. One day, their mother said, \"You are all grown up and must ", " on your own.\" So they left to ", + " their houses. The first little pig wanted only to ", " all day and quickly built his house out of ", ". The second little pig wanted to ", + " and ", " all day so he ", " his house with ", ". The third ", " pig knew the wolf lived nearby and worked hard to ", " his house out of ", + ". One day, the wolf knocked on the first pig's ", ". \"Let me in or I'll ", " your house down!\" The pig didn't, so the wolf ", " down the ", + ". The wolf knocked on the second pig's ", ". \"Let me in or I'll blow your ", " down!\" The pig didn't, so the wolf ", + " down the house. Then the wolf knocked on the third ", " pig's door. \"Let me in or I'll blow your house down!\" The little pig didn't so the wolf ", + " and ", ". He could not blow the house down. All the pigs went to live in the ", " house and they all ", " happily ever after."] + }, + { + "title": "Talk Like a Pirate", + "blanks": ["noun", "adjective", "verb", "adverb", "noun", "adjective", "plural noun", "plural noun", "plural noun", "part of the body", "noun", + "noun", "noun", "noun", "part of the body"], + "value": ["Ye can always pretend to be a bloodthirsty ", ", threatening everyone by waving yer ", " sword in the air, but until ye learn to ", + " like a pirate, ye'll never be ", " accepted as an authentic ", ". So here's what ye do: Cleverly work into yer daily conversations ", + " pirate phrases such as \"Ahoy there, ", "Avast, ye ", ",\" and \"Shiver me ", ".\" Remember to drop all yer gs when ye say such words as sailin', spittin', and fightin'. This will give ye a/an ", + " start to being recognized as a swashbucklin' ", ". Once ye have the lingo down pat, it helps to wear a three-cornered ", " on yer head, stash a/an ", + " in yer pants, and keep a/an ", " perched atop yer ", ". Aye, now ye be a real pirate!"] + }, + { + "title": "How to Date the Coolest Guy/Girl in School", + "blanks": ["plural noun", "adverb", "verb", "article of clothing", "body part", "adjective", "noun", "plural noun", "another body part", "plural noun", + "another body part", "noun", "noun", "verb ending in 'ing'", "adjective", "adjective", "verb"], + "value": ["It's simple. Turn the ", ". Make him/her want ", " to date you. Make sure you're always dressed to ", ". Each and every day, wear a/an ", + " that you know shows off your ", " to ", " advantage and make your ", " look like a million ", ". Even if the two of you make meaningful ", + " contact, don't admit it. No hugs or ", ". Just shake his/her ", " firmly. And remember, when he/she asks you out, even though a chill may run down your ", + " and you can't stop your ", " from ", ", just play it ", ". Take a long pause before answering in a very ", " voice. \"I'll have to ", + " it over.\""] + }, + { + "title": "The Fun Park", + "blanks": ["adjective", "plural noun", "noun", "adverb", "number", "past tense verb", "adjective ending in -est", "past tense verb", "adverb", "adjective"], + "value": ["Today, my fabulous camp group went to a(an) ", " amusement park. It was a fun park with lots of cool ", + " and enjoyable play structures. When we got there, my kind counselor shouted loudly, \"Everybody off the ", + ".\" My counselor handed out yellow tickets, and we scurried in. I was so excited! I couldn't figure out what exciting thing to do first. I saw a scary roller coaster I really liked so, I ", + " ran over to get in the long line that had about ", " people in it. when I finally got on the roller coaster I was ", + ". In fact, I was so nervous my two knees were knocking together. This was the ", " ride I had ever been on! In about two minutes I heard the crank and grinding of the gears. Thats when the ride began! When I got to the bottom, I was a little ", + " but I was proud of myself. The rest of the day went ", ". It was a ", " day at the fun park."] + }, + { + "title": "A Spooky Campfire Story", + "blanks": ["adjective", "adjective", "number", "adjective", "animal", "noun", "animal", "name", "verb", "adjective", "adjective"], + "value": ["Every summer, I get totally amped and ", " to go camping in the deep, ", " forests. It's good to get away from it all - but not too far, like getting lost! Last year, my friend and I went hiking and got lost for ", + " hour(s). We started off on a(n) ", " adventure, but we kept losing the trail. Night began to fall, and when we heard the howls of a ", + ", we began to panic. It was getting darker and our flashlights were running on ", ". I'm sure glad my pet ", ", ", ", was with us. He is one gifted creature, because he was able to guide us back by ", + " the ", " s'mores by the campfire. This year, before setting off on an ", " journey, I'll be sure to have working flashlights - and of course, my gifted pet!"] + }, + { + "title": "Weird News", + "blanks": ["noun", "place", "verb ending in ing", "noun", "name", "verb", "noun", "verb", "noun", "part of body", "type of liquid", "place", " past tense verb ", "foreign country", "verb", "noun", "past tense verb", "adjective", "verb", "noun", "plural noun"], + "value": ["A ", " in a ", " was arrested this morning after he was caught ", " in front of ", ". ", " had a history of ", ", but no one - not even his ", "- ever imagined he'd ", " with a ", " stuck in his ", ". After drinking a ", ", cops followed him to a ", + " where he reportedly ", " in the fry machine. Later, a woman from ", " was charged with a similar crime. But rather than ", " with a ", ", she ", " with a ", " dog. Either way, we imagine that after witnessing him ", " with a ", " there are probably a whole lot of ", + " that are going to need some therapy!"] + }, + { + "title": "All About Vampires", + "blanks": ["adjective", "adjective", "body part", "verb ending in -ing", "verb", "verb", "verb", "noun", "verb", "verb", "body part", "verb (ending with -s)", "verb (ending with -s)", "verb", "noun", "body part", "adjective"], + "value": ["Vampires are ", "! They have ", " ", " for ", " blood. The sun can ", " vampires, so they only ", " at night and ", " during the day. Vampires also don't like ", " so ", " it or ", " it around your ", + " to keep them away. If a vampire ", "s a person and ", "s their blood, they become a vampire, too. The only way to ", " a vampire is with a ", " through the ", ", but ", + " luck getting close enough to one!"] + }, + { + "title": "Our Cafeteria", + "blanks": ["adjective", "verb", "adjective", "noun", "verb", "adjective", "noun", "adjective", "adjective", "noun", "noun"], + "value": ["Our school cafeteria has really ", " food. Just thinking about it makes my stomach ", ". The spaghetti is ", " and tastes like ", ". One day, I swear one of my meatballs started to ", + "! The turkey tacos are totally ", " and look kind of like old ", ". My friend Dana actually likes the meatloaf, even though it's ", " and ", ". I call it \"Mystery Meatloaf\" and think it's really made out of ", + ". My dad said he'd make my lunches, but the first day, he made me a sandwich out of ", " and peanut butter! I think I'd rather take my chances with the cafeteria!"] + }, + { + "title": "Trip to the Park", + "blanks": ["adjective", "adjective", "noun", "adjective", "adjective", "verb", "verb", "verb", "adjective", "verb"], + "value": ["Yesterday, my friend and I went to the park. On our way to the ", " park, we saw big ", " balloons tied to a ", ". Once we got to the ", " park, the sky turned ", + ". It started to ", " and ", ". My friend and I ", " all the way home. Tomorrow we will try to go to the ", " park again and hopefully it doesn't ", "!"] + }, + { + "title": "A Scary Halloween Story", + "blanks": ["adjective", "name", "adjective", "noun", "verb", "animal", "adjective", "name", "adjective", "noun", "noun"], + "value": ["They say my school is haunted; my ", " friend ", " says they saw a ", " ", " floating at the end of the hall near the cafeteria. Some say if you ", + " down that hallway at night, you'll hear a ", " growling deeply. My ", " friend ", " saw a ", " ", " slithering under the tables once. I hope I never see any ", + " crawling; eating lunch there is scary enough!"] + }, + { + "title": "Zombie Picnic", + "blanks": ["verb", "verb", "body part", "body part", "body part", "noun", "adjective", "type of liquid", "body part", "verb", "adjective", "body part", "body part"], + "value": ["If zombies had a picnic, what would they ", " to eat? Everybody knows zombies love to ", " ", ", but did you know they also enjoy ", " and even ", + "? The best ", " for a zombie picnic is when the moon is ", ". At least one zombie will bring ", " to drink, and it's not a picnic without ", + " with extra flesh on top. After eating, zombies will ", " ", " games like kick the ", " and ", " toss. What fun!"] + }, + { + "title": "North Pole", + "blanks": ["plural noun", "adjective", "plural noun", "verb", "verb", "number", "noun", "adjective", "plural noun", "plural noun", "animal", "verb", "verb", "plural noun"], + "value": ["Santa, Mrs. Claus, and the ", " live at the North pole. The weather is always ", " there, but the ", " ", " toys for Santa to ", + " to children on Christmas, so holiday cheer lasts year-round there. There's no land at the North Pole; instead there is a ", "-inch thick sheet of ", + " there, ", " enough to hold Santa's Village! The ", " help load Santa's sleigh with ", ", and Santa's ", " ", " his sleigh on Christmas Eve to ", + " ", " to children around the entire world."] + }, + { + "title": "Snowstorm!", + "blanks": ["plural noun", "adjective", "noun", "noun", "adjective", "adjective", "adjective", "noun", "noun", "adjective"], + "value": ["Weather plays an important part in our ", " everyday. What is weather, you ask? According to ", " scientists, who are known as meteorologists, weather is what the ", + " is like at any given time of the ", ". It doesn't matter if the air is ", " or ", ", it's all weather. When vapors in ", " clouds condense, we have ", + " and snow. A lot of ", " means a ", " snowstorm!"] + }, + { + "title": "Learning About History", + "blanks": ["adjective", "noun", "nouns", "adjective", "nouns", "nouns", "animals", "nouns", "nouns", "number", "number", "nouns", "adjective", "nouns"], + "value": ["History is ", " because we learn about ", " and ", " that happened long ago. I can't believe people used to dress in ", " clothing and kids played with ", + " and ", " instead of video games. Also, before cars were invented, people actually rode ", "! People read ", " instead of computers and tablets, and sent messages via ", + " that took ", " days to arrive. I wonder how kids will view my life in ", " year(s); maybe they will ride flying cars to school and play with ", " and ", " ", "!"] + }, + { + "title": "Star Wars", + "blanks": ["adjective", "noun", "adjective", "noun; place", "adjective", "adjective", "adjective", "adjective", "plural noun", "adjective", "plural noun", "plural noun", "adjective", + "noun", "verb (ending with -s)", "adjective", "verb", "plural noun; type of job", "adjective", "verb", "adjective"], + "value": ["Star Wars is a ", " ", " of ", " versus evil in a ", " far far away. There are ", " battles between ", " ships in ", " space and ", " duels with ", " called ", + " sabers. ", " called \"droids\" are helpers and ", " to the heroes. A ", " power called The ", " ", " people to do ", " things, like ", " ", " use The Force for the ", + " side and the Sith ", " it for the ", " side."] + } +] -- cgit v1.2.3 From c7b93780e509e05121ce1c3deacc38c6f884a6c9 Mon Sep 17 00:00:00 2001 From: aru Date: Sun, 2 Jan 2022 15:16:35 -0500 Subject: Yank aoc subscribe (#972) --- bot/exts/events/advent_of_code/_cog.py | 45 +++++++--------------------------- 1 file changed, 9 insertions(+), 36 deletions(-) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index a9625153..a5410871 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -11,12 +11,13 @@ from discord.ext import commands, tasks from bot.bot import Bot from bot.constants import ( - AdventOfCode as AocConfig, Channels, Client, Colours, Emojis, Month, Roles, WHITELISTED_CHANNELS + AdventOfCode as AocConfig, Channels, Client, Colours, Emojis, Month, PYTHON_PREFIX, Roles, WHITELISTED_CHANNELS ) from bot.exts.events.advent_of_code import _helpers from bot.exts.events.advent_of_code.views.dayandstarview import AoCDropdownView from bot.utils import members from bot.utils.decorators import InChannelCheckFailure, in_month, whitelist_override, with_role +from bot.utils.exceptions import MovedCommandError from bot.utils.extensions import invoke_help_command log = logging.getLogger(__name__) @@ -137,45 +138,17 @@ class AdventOfCode(commands.Cog): @commands.guild_only() @adventofcode_group.command( name="subscribe", - aliases=("sub", "notifications", "notify", "notifs"), - brief="Notifications for new days" + aliases=("sub", "notifications", "notify", "notifs", "unsubscribe", "unsub"), + help=f"NOTE: This command has been moved to {PYTHON_PREFIX}subscribe", ) @whitelist_override(channels=AOC_WHITELIST) async def aoc_subscribe(self, ctx: commands.Context) -> None: - """Assign the role for notifications about new days being ready.""" - current_year = datetime.now().year - if current_year != AocConfig.year: - await ctx.send(f"You can't subscribe to {current_year}'s Advent of Code announcements yet!") - return - - role = ctx.guild.get_role(AocConfig.role_id) - unsubscribe_command = f"{ctx.prefix}{ctx.command.root_parent} unsubscribe" - - if role not in ctx.author.roles: - await ctx.author.add_roles(role) - await ctx.send( - "Okay! You have been __subscribed__ to notifications about new Advent of Code tasks. " - f"You can run `{unsubscribe_command}` to disable them again for you." - ) - else: - await ctx.send( - "Hey, you already are receiving notifications about new Advent of Code tasks. " - f"If you don't want them any more, run `{unsubscribe_command}` instead." - ) - - @in_month(Month.DECEMBER) - @commands.guild_only() - @adventofcode_group.command(name="unsubscribe", aliases=("unsub",), brief="Notifications for new days") - @whitelist_override(channels=AOC_WHITELIST) - async def aoc_unsubscribe(self, ctx: commands.Context) -> None: - """Remove the role for notifications about new days being ready.""" - role = ctx.guild.get_role(AocConfig.role_id) + """ + Deprecated role command. - if role in ctx.author.roles: - await ctx.author.remove_roles(role) - await ctx.send("Okay! You have been __unsubscribed__ from notifications about new Advent of Code tasks.") - else: - await ctx.send("Hey, you don't even get any notifications about new Advent of Code tasks currently anyway.") + This command has been moved to bot, and will be removed in the future. + """ + raise MovedCommandError(f"{PYTHON_PREFIX}subscribe") @adventofcode_group.command(name="countdown", aliases=("count", "c"), brief="Return time left until next day") @whitelist_override(channels=AOC_WHITELIST) -- cgit v1.2.3 From 2c66844a3e9e59b97fba43de0d850d8cf597d9ad Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 2 Jan 2022 22:23:47 +0000 Subject: Update GitHub issue closed emoji GitHub updated their issue closed emoji from red to purple, this updates the emoji to relfect that new colour. --- bot/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/constants.py b/bot/constants.py index 01f825a0..3b426c47 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -197,7 +197,7 @@ class Emojis: # These icons are from Github's repo https://github.com/primer/octicons/ issue_open = "<:IssueOpen:852596024777506817>" - issue_closed = "<:IssueClosed:852596024739758081>" + issue_closed = "<:IssueClosed:927326162861039626>" issue_draft = "<:IssueDraft:852596025147523102>" # Not currently used by Github, but here for future. pull_request_open = "<:PROpen:852596471505223781>" pull_request_closed = "<:PRClosed:852596024732286976>" -- cgit v1.2.3 From 97f83b0c2e5b98cab9b967310396c56071a9f9f2 Mon Sep 17 00:00:00 2001 From: bones <35849006+Sn4u@users.noreply.github.com> Date: Sat, 8 Jan 2022 21:00:15 +0000 Subject: Merge Epoch Command (PR #983) Add timestamp converter command. Co-authored-by: Sn4u Co-authored-by: mathstrains21 <89940630+mathstrains21@users.noreply.github.com> Co-authored-by: mathstrains21 --- bot/exts/utilities/epoch.py | 135 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 bot/exts/utilities/epoch.py diff --git a/bot/exts/utilities/epoch.py b/bot/exts/utilities/epoch.py new file mode 100644 index 00000000..b9feed18 --- /dev/null +++ b/bot/exts/utilities/epoch.py @@ -0,0 +1,135 @@ +from typing import Optional, Union + +import arrow +import discord +from dateutil import parser +from discord.ext import commands + +from bot.bot import Bot +from bot.utils.extensions import invoke_help_command + +# https://discord.com/developers/docs/reference#message-formatting-timestamp-styles +STYLES = { + "Epoch": ("",), + "Short Time": ("t", "h:mm A",), + "Long Time": ("T", "h:mm:ss A"), + "Short Date": ("d", "MM/DD/YYYY"), + "Long Date": ("D", "MMMM D, YYYY"), + "Short Date/Time": ("f", "MMMM D, YYYY h:mm A"), + "Long Date/Time": ("F", "dddd, MMMM D, YYYY h:mm A"), + "Relative Time": ("R",) +} +DROPDOWN_TIMEOUT = 60 + + +class DateString(commands.Converter): + """Convert a relative or absolute date/time string to an arrow.Arrow object.""" + + async def convert(self, ctx: commands.Context, argument: str) -> Union[arrow.Arrow, Optional[tuple]]: + """ + Convert a relative or absolute date/time string to an arrow.Arrow object. + + Try to interpret the date string as a relative time. If conversion fails, try to interpret it as an absolute + time. Tokens that are not recognised are returned along with the part of the string that was successfully + converted to an arrow object. If the date string cannot be parsed, BadArgument is raised. + """ + try: + return arrow.utcnow().dehumanize(argument) + except ValueError: + try: + dt, ignored_tokens = parser.parse(argument, fuzzy_with_tokens=True) + except parser.ParserError: + raise commands.BadArgument(f"`{argument}` Could not be parsed to a relative or absolute date.") + except OverflowError: + raise commands.BadArgument(f"`{argument}` Results in a date outside of the supported range.") + return arrow.get(dt), ignored_tokens + + +class Epoch(commands.Cog): + """Convert an entered time and date to a unix timestamp.""" + + @commands.command(name="epoch") + async def epoch(self, ctx: commands.Context, *, date_time: DateString = None) -> None: + """ + Convert an entered date/time string to the equivalent epoch. + + **Relative time** + Must begin with `in...` or end with `...ago`. + Accepted units: "seconds", "minutes", "hours", "days", "weeks", "months", "years". + eg `.epoch in a month 4 days and 2 hours` + + **Absolute time** + eg `.epoch 2022/6/15 16:43 -04:00` + Absolute times must be entered in descending orders of magnitude. + If AM or PM is left unspecified, the 24-hour clock is assumed. + Timezones are optional, and will default to UTC. The following timezone formats are accepted: + Z (UTC) + Β±HH:MM + Β±HHMM + Β±HH + + Times in the dropdown are shown in UTC + """ + if not date_time: + await invoke_help_command(ctx) + return + + if isinstance(date_time, tuple): + # Remove empty strings. Strip extra whitespace from the remaining items + ignored_tokens = list(map(str.strip, filter(str.strip, date_time[1]))) + date_time = date_time[0] + if ignored_tokens: + await ctx.send(f"Could not parse the following token(s): `{', '.join(ignored_tokens)}`") + await ctx.send(f"Date and time parsed as: `{date_time.format(arrow.FORMAT_RSS)}`") + + epoch = int(date_time.timestamp()) + view = TimestampMenuView(ctx, self._format_dates(date_time), epoch) + original = await ctx.send(f"`{epoch}`", view=view) + await view.wait() # wait until expiration before removing the dropdown + await original.edit(view=None) + + @staticmethod + def _format_dates(date: arrow.Arrow) -> list[str]: + """ + Return a list of date strings formatted according to the discord timestamp styles. + + These are used in the description of each style in the dropdown + """ + date = date.to('utc') + formatted = [str(int(date.timestamp()))] + formatted += [date.format(format[1]) for format in list(STYLES.values())[1:7]] + formatted.append(date.humanize()) + return formatted + + +class TimestampMenuView(discord.ui.View): + """View for the epoch command which contains a single `discord.ui.Select` dropdown component.""" + + def __init__(self, ctx: commands.Context, formatted_times: list[str], epoch: int): + super().__init__(timeout=DROPDOWN_TIMEOUT) + self.ctx = ctx + self.epoch = epoch + self.dropdown: discord.ui.Select = self.children[0] + for label, date_time in zip(STYLES.keys(), formatted_times): + self.dropdown.add_option(label=label, description=date_time) + + @discord.ui.select(placeholder="Select the format of your timestamp") + async def select_format(self, _: discord.ui.Select, interaction: discord.Interaction) -> discord.Message: + """Drop down menu which contains a list of formats which discord timestamps can take.""" + selected = interaction.data["values"][0] + if selected == "Epoch": + return await interaction.response.edit_message(content=f"`{self.epoch}`") + return await interaction.response.edit_message(content=f"``") + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + """Check to ensure that the interacting user is the user who invoked the command.""" + if interaction.user != self.ctx.author: + embed = discord.Embed(description="Sorry, but this dropdown menu can only be used by the original author.") + await interaction.response.send_message(embed=embed, ephemeral=True) + return False + return True + + +def setup(bot: Bot) -> None: + """Load the Epoch cog.""" + bot.add_cog(Epoch()) -- cgit v1.2.3 From 6387fa042c4bbd31ccb04c3820e9264f3281b98f Mon Sep 17 00:00:00 2001 From: Sn4u <35849006+Sn4u@users.noreply.github.com> Date: Sat, 8 Jan 2022 22:11:24 +0000 Subject: handle OverflowError in relative times converter --- bot/exts/utilities/epoch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/epoch.py b/bot/exts/utilities/epoch.py index b9feed18..03758af0 100644 --- a/bot/exts/utilities/epoch.py +++ b/bot/exts/utilities/epoch.py @@ -35,7 +35,7 @@ class DateString(commands.Converter): """ try: return arrow.utcnow().dehumanize(argument) - except ValueError: + except (ValueError, OverflowError): try: dt, ignored_tokens = parser.parse(argument, fuzzy_with_tokens=True) except parser.ParserError: -- cgit v1.2.3 From ce16d0df6c10121f761899daea07d3656c4e7537 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 8 Jan 2022 22:27:48 +0000 Subject: Change single quote to double quote. This trivial change is actually to try and reproduce a bug we had with our GitHub actions. --- bot/monkey_patches.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/monkey_patches.py b/bot/monkey_patches.py index 19965c19..36708415 100644 --- a/bot/monkey_patches.py +++ b/bot/monkey_patches.py @@ -6,7 +6,7 @@ from discord import Forbidden, http from discord.ext import commands log = logging.getLogger(__name__) -MESSAGE_ID_RE = re.compile(r'(?P[0-9]{15,20})$') +MESSAGE_ID_RE = re.compile(r"(?P[0-9]{15,20})$") class Command(commands.Command): -- cgit v1.2.3 From 4f823346d56929964a092bd8119ec54f1458701e Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 8 Jan 2022 22:44:28 +0000 Subject: Remove trailing whitespace Like the previous commit, this trivial change is actually to try and reproduce a bug we had with our GitHub actions. --- bot/monkey_patches.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/monkey_patches.py b/bot/monkey_patches.py index 36708415..925d3206 100644 --- a/bot/monkey_patches.py +++ b/bot/monkey_patches.py @@ -6,7 +6,7 @@ from discord import Forbidden, http from discord.ext import commands log = logging.getLogger(__name__) -MESSAGE_ID_RE = re.compile(r"(?P[0-9]{15,20})$") +MESSAGE_ID_RE = re.compile(r"(?P[0-9]{15,20})$") class Command(commands.Command): -- cgit v1.2.3 From 5276045e3594c8851b83dd8b8fd8e7ba014ac3b2 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 9 Jan 2022 19:06:36 +0000 Subject: Fix aoc join in date logic --- bot/exts/events/advent_of_code/_cog.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index a5410871..01161f26 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -187,9 +187,10 @@ class AdventOfCode(commands.Cog): async def join_leaderboard(self, ctx: commands.Context) -> None: """DM the user the information for joining the Python Discord leaderboard.""" current_date = datetime.now() - if ( - current_date.month not in (Month.NOVEMBER, Month.DECEMBER) and current_date.year != AocConfig.year or - current_date.month != Month.JANUARY and current_date.year != AocConfig.year + 1 + allowed_months = (Month.NOVEMBER.value, Month.DECEMBER.value) + if not ( + current_date.month in allowed_months and current_date.year == AocConfig.year or + current_date.month == Month.JANUARY.value and current_date.year == AocConfig.year + 1 ): # Only allow joining the leaderboard in the run up to AOC and the January following. await ctx.send(f"The Python Discord leaderboard for {current_date.year} is not yet available!") -- cgit v1.2.3 From 69dc2cc350cb306ceafbeca8cf0d6216d8dc39c5 Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Mon, 10 Jan 2022 13:51:08 +0530 Subject: add latex command --- .gitignore | 2 +- bot/exts/fun/latex/__init__.py | 7 ++ bot/exts/fun/latex/_latex_cog.py | 60 +++++++++++ bot/exts/fun/latex/_renderer.py | 45 +++++++++ poetry.lock | 213 ++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 6 files changed, 326 insertions(+), 2 deletions(-) create mode 100644 bot/exts/fun/latex/__init__.py create mode 100644 bot/exts/fun/latex/_latex_cog.py create mode 100644 bot/exts/fun/latex/_renderer.py diff --git a/.gitignore b/.gitignore index ce122d29..c914e27a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # bot (project-specific) log/* data/* -_latex_cache/* +exts/fun/latex/cache/* diff --git a/bot/exts/fun/latex/__init__.py b/bot/exts/fun/latex/__init__.py new file mode 100644 index 00000000..e58e0447 --- /dev/null +++ b/bot/exts/fun/latex/__init__.py @@ -0,0 +1,7 @@ +from bot.bot import Bot +from bot.exts.fun.latex._latex_cog import Latex + + +def setup(bot: Bot) -> None: + """Load the Latex Cog.""" + bot.add_cog(Latex(bot)) diff --git a/bot/exts/fun/latex/_latex_cog.py b/bot/exts/fun/latex/_latex_cog.py new file mode 100644 index 00000000..239f499c --- /dev/null +++ b/bot/exts/fun/latex/_latex_cog.py @@ -0,0 +1,60 @@ +import asyncio +import hashlib +import sys +from pathlib import Path +import re + +import discord +from discord.ext import commands + + +FORMATTED_CODE_REGEX = re.compile( + r"(?P(?P```)|``?)" # code delimiter: 1-3 backticks; (?P=block) only matches if it's a block + r"(?(block)(?:(?P[a-z]+)\n)?)" # if we're in a block, match optional language (only letters plus newline) + r"(?:[ \t]*\n)*" # any blank (empty or tabs/spaces only) lines before the code + r"(?P.*?)" # extract all code inside the markup + r"\s*" # any more whitespace before the end of the code markup + r"(?P=delim)", # match the exact same delimiter from the start again + re.DOTALL | re.IGNORECASE, # "." also matches newlines, case insensitive +) + +THIS_DIR = Path(__file__).parent +CACHE_DIRECTORY = THIS_DIR / "cache" +CACHE_DIRECTORY.mkdir(exist_ok=True) + + +def _prepare_input(text: str) -> str: + text = text.replace(r"\\", "$\n$") # matplotlib uses \n for newlines, not \\ + + if match := FORMATTED_CODE_REGEX.match(text): + return match.group("code") + else: + return text + + +class Latex(commands.Cog): + """Renders latex.""" + @commands.command() + @commands.max_concurrency(1, commands.BucketType.guild, wait=True) + async def latex(self, ctx: commands.Context, *, query: str) -> None: + """Renders the text in latex and sends the image.""" + query = _prepare_input(query) + query_hash = hashlib.md5(query.encode()).hexdigest() + image_path = CACHE_DIRECTORY / f"{query_hash}.png" + async with ctx.typing(): + if not image_path.exists(): + proc = await asyncio.subprocess.create_subprocess_exec( + sys.executable, + "_renderer.py", + query, + image_path.relative_to(THIS_DIR), + cwd=THIS_DIR, + stderr=asyncio.subprocess.PIPE + ) + return_code = await proc.wait() + if return_code != 0: + image_path.unlink() + err = (await proc.stderr.read()).decode() + raise commands.BadArgument(err) + + await ctx.send(file=discord.File(image_path, "latex.png")) diff --git a/bot/exts/fun/latex/_renderer.py b/bot/exts/fun/latex/_renderer.py new file mode 100644 index 00000000..3f6528ad --- /dev/null +++ b/bot/exts/fun/latex/_renderer.py @@ -0,0 +1,45 @@ +import sys + +from pathlib import Path +from typing import BinaryIO + +import matplotlib.pyplot as plt + +# configure fonts and colors for matplotlib +plt.rcParams.update( + { + "font.size": 16, + "mathtext.fontset": "cm", # Computer Modern font set + "mathtext.rm": "serif", + "figure.facecolor": "36393F", # matches Discord's dark mode background color + "text.color": "white", + } +) + + +def render(text: str, file_handle: BinaryIO) -> None: + """ + Saves rendered image in `file_handle`. In case the input is invalid latex, it prints the error to `stderr`. + """ + fig = plt.figure() + fig.text(0, 1, text, horizontalalignment="left", verticalalignment="top") + try: + plt.savefig(file_handle, bbox_inches="tight", dpi=600) + except ValueError as err: + # get rid of traceback, keeping just the latex error + sys.exit(err) + + +def main(): + """ + Renders a latex query and saves the output in a specified file. + Expects two command line arguments: the query and the path to the output file. + """ + query = sys.argv[1] + out_file_path = Path(sys.argv[2]) + with open(out_file_path, "wb") as out_file: + render(query, out_file) + + +if __name__ == "__main__": + main() diff --git a/poetry.lock b/poetry.lock index 6a83efed..c482c61f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -160,6 +160,14 @@ humanfriendly = ">=9.1" [package.extras] cron = ["capturer (>=2.4)"] +[[package]] +name = "cycler" +version = "0.11.0" +description = "Composable style cycles" +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "deprecated" version = "1.2.13" @@ -193,6 +201,7 @@ voice = ["PyNaCl (>=1.3.0,<1.5)"] [package.source] type = "url" url = "https://github.com/Rapptz/discord.py/archive/45d498c1b76deaf3b394d17ccf56112fa691d160.zip" + [[package]] name = "distlib" version = "0.3.4" @@ -350,6 +359,27 @@ python-versions = "*" [package.dependencies] pycodestyle = ">=2.0.0,<3.0.0" +[[package]] +name = "fonttools" +version = "4.28.5" +description = "Tools to manipulate font files" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +all = ["fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "zopfli (>=0.1.4)", "lz4 (>=1.7.4.2)", "matplotlib", "sympy", "skia-pathops (>=0.5.0)", "brotlicffi (>=0.8.0)", "scipy", "brotli (>=1.0.1)", "munkres", "unicodedata2 (>=13.0.0)", "xattr"] +graphite = ["lz4 (>=1.7.4.2)"] +interpolatable = ["scipy", "munkres"] +lxml = ["lxml (>=4.0,<5)"] +pathops = ["skia-pathops (>=0.5.0)"] +plot = ["matplotlib"] +symfont = ["sympy"] +type1 = ["xattr"] +ufo = ["fs (>=2.2.0,<3)"] +unicode = ["unicodedata2 (>=13.0.0)"] +woff = ["zopfli (>=0.1.4)", "brotlicffi (>=0.8.0)", "brotli (>=1.0.1)"] + [[package]] name = "hiredis" version = "2.0.0" @@ -402,6 +432,14 @@ requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] plugins = ["setuptools"] +[[package]] +name = "kiwisolver" +version = "1.3.2" +description = "A fast implementation of the Cassowary constraint solver" +category = "main" +optional = false +python-versions = ">=3.7" + [[package]] name = "lxml" version = "4.7.1" @@ -416,6 +454,25 @@ html5 = ["html5lib"] htmlsoup = ["beautifulsoup4"] source = ["Cython (>=0.29.7)"] +[[package]] +name = "matplotlib" +version = "3.5.1" +description = "Python plotting package" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +cycler = ">=0.10" +fonttools = ">=4.22.0" +kiwisolver = ">=1.0.1" +numpy = ">=1.17" +packaging = ">=20.0" +pillow = ">=6.2.0" +pyparsing = ">=2.2.1" +python-dateutil = ">=2.7" +setuptools_scm = ">=4" + [[package]] name = "mccabe" version = "0.6.1" @@ -448,6 +505,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "numpy" +version = "1.22.0" +description = "NumPy is the fundamental package for array computing with Python." +category = "main" +optional = false +python-versions = ">=3.8" + [[package]] name = "packaging" version = "21.3" @@ -694,6 +759,21 @@ sanic = ["sanic (>=0.8)"] sqlalchemy = ["sqlalchemy (>=1.2)"] tornado = ["tornado (>=5)"] +[[package]] +name = "setuptools-scm" +version = "6.3.2" +description = "the blessed package to manage your versions by scm tags" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +packaging = ">=20.0" +tomli = ">=1.0.0" + +[package.extras] +toml = ["setuptools (>=42)", "tomli (>=1.0.0)"] + [[package]] name = "six" version = "1.16.0" @@ -761,6 +841,14 @@ category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "tomli" +version = "2.0.0" +description = "A lil' TOML parser" +category = "main" +optional = false +python-versions = ">=3.7" + [[package]] name = "typing-extensions" version = "4.0.1" @@ -823,7 +911,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "e3682fd5b518ada5066b36015210f00d223c5485c24e4e7c377e371fe6ef0a0d" +content-hash = "6e2fd70bb8b92763ee3bd93f5eef323a96179e8e04de4d6c2ace5f6fa3a4042d" [metadata.files] aiodns = [ @@ -965,6 +1053,10 @@ coloredlogs = [ {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, ] +cycler = [ + {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"}, + {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, +] deprecated = [ {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, @@ -1021,6 +1113,10 @@ flake8-tidy-imports = [ flake8-todo = [ {file = "flake8-todo-0.7.tar.gz", hash = "sha256:6e4c5491ff838c06fe5a771b0e95ee15fc005ca57196011011280fc834a85915"}, ] +fonttools = [ + {file = "fonttools-4.28.5-py3-none-any.whl", hash = "sha256:edf251d5d2cc0580d5f72de4621c338d8c66c5f61abb50cf486640f73c8194d5"}, + {file = "fonttools-4.28.5.zip", hash = "sha256:545c05d0f7903a863c2020e07b8f0a57517f2c40d940bded77076397872d14ca"}, +] hiredis = [ {file = "hiredis-2.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b4c8b0bc5841e578d5fb32a16e0c305359b987b850a06964bd5a62739d688048"}, {file = "hiredis-2.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0adea425b764a08270820531ec2218d0508f8ae15a448568109ffcae050fee26"}, @@ -1080,6 +1176,52 @@ isort = [ {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] +kiwisolver = [ + {file = "kiwisolver-1.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1d819553730d3c2724582124aee8a03c846ec4362ded1034c16fb3ef309264e6"}, + {file = "kiwisolver-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d93a1095f83e908fc253f2fb569c2711414c0bfd451cab580466465b235b470"}, + {file = "kiwisolver-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4550a359c5157aaf8507e6820d98682872b9100ce7607f8aa070b4b8af6c298"}, + {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2210f28778c7d2ee13f3c2a20a3a22db889e75f4ec13a21072eabb5693801e84"}, + {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:82f49c5a79d3839bc8f38cb5f4bfc87e15f04cbafa5fbd12fb32c941cb529cfb"}, + {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9661a04ca3c950a8ac8c47f53cbc0b530bce1b52f516a1e87b7736fec24bfff0"}, + {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ddb500a2808c100e72c075cbb00bf32e62763c82b6a882d403f01a119e3f402"}, + {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72be6ebb4e92520b9726d7146bc9c9b277513a57a38efcf66db0620aec0097e0"}, + {file = "kiwisolver-1.3.2-cp310-cp310-win32.whl", hash = "sha256:83d2c9db5dfc537d0171e32de160461230eb14663299b7e6d18ca6dca21e4977"}, + {file = "kiwisolver-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:cba430db673c29376135e695c6e2501c44c256a81495da849e85d1793ee975ad"}, + {file = "kiwisolver-1.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4116ba9a58109ed5e4cb315bdcbff9838f3159d099ba5259c7c7fb77f8537492"}, + {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19554bd8d54cf41139f376753af1a644b63c9ca93f8f72009d50a2080f870f77"}, + {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a4cf5bbdc861987a7745aed7a536c6405256853c94abc9f3287c3fa401b174"}, + {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0007840186bacfaa0aba4466d5890334ea5938e0bb7e28078a0eb0e63b5b59d5"}, + {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec2eba188c1906b05b9b49ae55aae4efd8150c61ba450e6721f64620c50b59eb"}, + {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3dbb3cea20b4af4f49f84cffaf45dd5f88e8594d18568e0225e6ad9dec0e7967"}, + {file = "kiwisolver-1.3.2-cp37-cp37m-win32.whl", hash = "sha256:5326ddfacbe51abf9469fe668944bc2e399181a2158cb5d45e1d40856b2a0589"}, + {file = "kiwisolver-1.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c6572c2dab23c86a14e82c245473d45b4c515314f1f859e92608dcafbd2f19b8"}, + {file = "kiwisolver-1.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b5074fb09429f2b7bc82b6fb4be8645dcbac14e592128beeff5461dcde0af09f"}, + {file = "kiwisolver-1.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:22521219ca739654a296eea6d4367703558fba16f98688bd8ce65abff36eaa84"}, + {file = "kiwisolver-1.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c358721aebd40c243894298f685a19eb0491a5c3e0b923b9f887ef1193ddf829"}, + {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ba5a1041480c6e0a8b11a9544d53562abc2d19220bfa14133e0cdd9967e97af"}, + {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44e6adf67577dbdfa2d9f06db9fbc5639afefdb5bf2b4dfec25c3a7fbc619536"}, + {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d45d1c74f88b9f41062716c727f78f2a59a5476ecbe74956fafb423c5c87a76"}, + {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70adc3658138bc77a36ce769f5f183169bc0a2906a4f61f09673f7181255ac9b"}, + {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6a5431940f28b6de123de42f0eb47b84a073ee3c3345dc109ad550a3307dd28"}, + {file = "kiwisolver-1.3.2-cp38-cp38-win32.whl", hash = "sha256:ee040a7de8d295dbd261ef2d6d3192f13e2b08ec4a954de34a6fb8ff6422e24c"}, + {file = "kiwisolver-1.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:8dc3d842fa41a33fe83d9f5c66c0cc1f28756530cd89944b63b072281e852031"}, + {file = "kiwisolver-1.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a498bcd005e8a3fedd0022bb30ee0ad92728154a8798b703f394484452550507"}, + {file = "kiwisolver-1.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:80efd202108c3a4150e042b269f7c78643420cc232a0a771743bb96b742f838f"}, + {file = "kiwisolver-1.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f8eb7b6716f5b50e9c06207a14172cf2de201e41912ebe732846c02c830455b9"}, + {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f441422bb313ab25de7b3dbfd388e790eceb76ce01a18199ec4944b369017009"}, + {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:30fa008c172355c7768159983a7270cb23838c4d7db73d6c0f6b60dde0d432c6"}, + {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f8f6c8f4f1cff93ca5058d6ec5f0efda922ecb3f4c5fb76181f327decff98b8"}, + {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba677bcaff9429fd1bf01648ad0901cea56c0d068df383d5f5856d88221fe75b"}, + {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7843b1624d6ccca403a610d1277f7c28ad184c5aa88a1750c1a999754e65b439"}, + {file = "kiwisolver-1.3.2-cp39-cp39-win32.whl", hash = "sha256:e6f5eb2f53fac7d408a45fbcdeda7224b1cfff64919d0f95473420a931347ae9"}, + {file = "kiwisolver-1.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:eedd3b59190885d1ebdf6c5e0ca56828beb1949b4dfe6e5d0256a461429ac386"}, + {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:dedc71c8eb9c5096037766390172c34fb86ef048b8e8958b4e484b9e505d66bc"}, + {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bf7eb45d14fc036514c09554bf983f2a72323254912ed0c3c8e697b62c4c158f"}, + {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2b65bd35f3e06a47b5c30ea99e0c2b88f72c6476eedaf8cfbc8e66adb5479dcf"}, + {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25405f88a37c5f5bcba01c6e350086d65e7465fd1caaf986333d2a045045a223"}, + {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:bcadb05c3d4794eb9eee1dddf1c24215c92fb7b55a80beae7a60530a91060560"}, + {file = "kiwisolver-1.3.2.tar.gz", hash = "sha256:fc4453705b81d03568d5b808ad8f09c77c47534f6ac2e72e733f9ca4714aa75c"}, +] lxml = [ {file = "lxml-4.7.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:d546431636edb1d6a608b348dd58cc9841b81f4116745857b6cb9f8dadb2725f"}, {file = "lxml-4.7.1-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6308062534323f0d3edb4e702a0e26a76ca9e0e23ff99be5d82750772df32a9e"}, @@ -1142,6 +1284,43 @@ lxml = [ {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:447d5009d6b5447b2f237395d0018901dcc673f7d9f82ba26c1b9f9c3b444b60"}, {file = "lxml-4.7.1.tar.gz", hash = "sha256:a1613838aa6b89af4ba10a0f3a972836128801ed008078f8c1244e65958f1b24"}, ] +matplotlib = [ + {file = "matplotlib-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:456cc8334f6d1124e8ff856b42d2cc1c84335375a16448189999496549f7182b"}, + {file = "matplotlib-3.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8a77906dc2ef9b67407cec0bdbf08e3971141e535db888974a915be5e1e3efc6"}, + {file = "matplotlib-3.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e70ae6475cfd0fad3816dcbf6cac536dc6f100f7474be58d59fa306e6e768a4"}, + {file = "matplotlib-3.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53273c5487d1c19c3bc03b9eb82adaf8456f243b97ed79d09dded747abaf1235"}, + {file = "matplotlib-3.5.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3b6f3fd0d8ca37861c31e9a7cab71a0ef14c639b4c95654ea1dd153158bf0df"}, + {file = "matplotlib-3.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8c87cdaf06fd7b2477f68909838ff4176f105064a72ca9d24d3f2a29f73d393"}, + {file = "matplotlib-3.5.1-cp310-cp310-win32.whl", hash = "sha256:e2f28a07b4f82abb40267864ad7b3a4ed76f1b1663e81c7efc84a9b9248f672f"}, + {file = "matplotlib-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:d70a32ee1f8b55eed3fd4e892f0286df8cccc7e0475c11d33b5d0a148f5c7599"}, + {file = "matplotlib-3.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:68fa30cec89b6139dc559ed6ef226c53fd80396da1919a1b5ef672c911aaa767"}, + {file = "matplotlib-3.5.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e3484d8455af3fdb0424eae1789af61f6a79da0c80079125112fd5c1b604218"}, + {file = "matplotlib-3.5.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e293b16cf303fe82995e41700d172a58a15efc5331125d08246b520843ef21ee"}, + {file = "matplotlib-3.5.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e3520a274a0e054e919f5b3279ee5dbccf5311833819ccf3399dab7c83e90a25"}, + {file = "matplotlib-3.5.1-cp37-cp37m-win32.whl", hash = "sha256:2252bfac85cec7af4a67e494bfccf9080bcba8a0299701eab075f48847cca907"}, + {file = "matplotlib-3.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:abf67e05a1b7f86583f6ebd01f69b693b9c535276f4e943292e444855870a1b8"}, + {file = "matplotlib-3.5.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6c094e4bfecd2fa7f9adffd03d8abceed7157c928c2976899de282f3600f0a3d"}, + {file = "matplotlib-3.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:506b210cc6e66a0d1c2bb765d055f4f6bc2745070fb1129203b67e85bbfa5c18"}, + {file = "matplotlib-3.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b04fc29bcef04d4e2d626af28d9d892be6aba94856cb46ed52bcb219ceac8943"}, + {file = "matplotlib-3.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:577ed20ec9a18d6bdedb4616f5e9e957b4c08563a9f985563a31fd5b10564d2a"}, + {file = "matplotlib-3.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e486f60db0cd1c8d68464d9484fd2a94011c1ac8593d765d0211f9daba2bd535"}, + {file = "matplotlib-3.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b71f3a7ca935fc759f2aed7cec06cfe10bc3100fadb5dbd9c435b04e557971e1"}, + {file = "matplotlib-3.5.1-cp38-cp38-win32.whl", hash = "sha256:d24e5bb8028541ce25e59390122f5e48c8506b7e35587e5135efcb6471b4ac6c"}, + {file = "matplotlib-3.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:778d398c4866d8e36ee3bf833779c940b5f57192fa0a549b3ad67bc4c822771b"}, + {file = "matplotlib-3.5.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bb1c613908f11bac270bc7494d68b1ef6e7c224b7a4204d5dacf3522a41e2bc3"}, + {file = "matplotlib-3.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:edf5e4e1d5fb22c18820e8586fb867455de3b109c309cb4fce3aaed85d9468d1"}, + {file = "matplotlib-3.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:40e0d7df05e8efe60397c69b467fc8f87a2affeb4d562fe92b72ff8937a2b511"}, + {file = "matplotlib-3.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a350ca685d9f594123f652ba796ee37219bf72c8e0fc4b471473d87121d6d34"}, + {file = "matplotlib-3.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3e66497cd990b1a130e21919b004da2f1dc112132c01ac78011a90a0f9229778"}, + {file = "matplotlib-3.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:87900c67c0f1728e6db17c6809ec05c025c6624dcf96a8020326ea15378fe8e7"}, + {file = "matplotlib-3.5.1-cp39-cp39-win32.whl", hash = "sha256:b8a4fb2a0c5afbe9604f8a91d7d0f27b1832c3e0b5e365f95a13015822b4cd65"}, + {file = "matplotlib-3.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:fe8d40c434a8e2c68d64c6d6a04e77f21791a93ff6afe0dce169597c110d3079"}, + {file = "matplotlib-3.5.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:34a1fc29f8f96e78ec57a5eff5e8d8b53d3298c3be6df61e7aa9efba26929522"}, + {file = "matplotlib-3.5.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b19a761b948e939a9e20173aaae76070025f0024fc8f7ba08bef22a5c8573afc"}, + {file = "matplotlib-3.5.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6803299cbf4665eca14428d9e886de62e24f4223ac31ab9c5d6d5339a39782c7"}, + {file = "matplotlib-3.5.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:14334b9902ec776461c4b8c6516e26b450f7ebe0b3ef8703bf5cdfbbaecf774a"}, + {file = "matplotlib-3.5.1.tar.gz", hash = "sha256:b2e9810e09c3a47b73ce9cab5a72243a1258f61e7900969097a817232246ce1c"}, +] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, @@ -1228,6 +1407,30 @@ nodeenv = [ {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, ] +numpy = [ + {file = "numpy-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d22662b4b10112c545c91a0741f2436f8ca979ab3d69d03d19322aa970f9695"}, + {file = "numpy-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:11a1f3816ea82eed4178102c56281782690ab5993251fdfd75039aad4d20385f"}, + {file = "numpy-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5dc65644f75a4c2970f21394ad8bea1a844104f0fe01f278631be1c7eae27226"}, + {file = "numpy-1.22.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c16cec1c8cf2728f1d539bd55aaa9d6bb48a7de2f41eb944697293ef65a559"}, + {file = "numpy-1.22.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97e82c39d9856fe7d4f9b86d8a1e66eff99cf3a8b7ba48202f659703d27c46f"}, + {file = "numpy-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:e41e8951749c4b5c9a2dc5fdbc1a4eec6ab2a140fdae9b460b0f557eed870f4d"}, + {file = "numpy-1.22.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bece0a4a49e60e472a6d1f70ac6cdea00f9ab80ff01132f96bd970cdd8a9e5a9"}, + {file = "numpy-1.22.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:818b9be7900e8dc23e013a92779135623476f44a0de58b40c32a15368c01d471"}, + {file = "numpy-1.22.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:47ee7a839f5885bc0c63a74aabb91f6f40d7d7b639253768c4199b37aede7982"}, + {file = "numpy-1.22.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a024181d7aef0004d76fb3bce2a4c9f2e67a609a9e2a6ff2571d30e9976aa383"}, + {file = "numpy-1.22.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f71d57cc8645f14816ae249407d309be250ad8de93ef61d9709b45a0ddf4050c"}, + {file = "numpy-1.22.0-cp38-cp38-win32.whl", hash = "sha256:283d9de87c0133ef98f93dfc09fad3fb382f2a15580de75c02b5bb36a5a159a5"}, + {file = "numpy-1.22.0-cp38-cp38-win_amd64.whl", hash = "sha256:2762331de395739c91f1abb88041f94a080cb1143aeec791b3b223976228af3f"}, + {file = "numpy-1.22.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:76ba7c40e80f9dc815c5e896330700fd6e20814e69da9c1267d65a4d051080f1"}, + {file = "numpy-1.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0cfe07133fd00b27edee5e6385e333e9eeb010607e8a46e1cd673f05f8596595"}, + {file = "numpy-1.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6ed0d073a9c54ac40c41a9c2d53fcc3d4d4ed607670b9e7b0de1ba13b4cbfe6f"}, + {file = "numpy-1.22.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41388e32e40b41dd56eb37fcaa7488b2b47b0adf77c66154d6b89622c110dfe9"}, + {file = "numpy-1.22.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b55b953a1bdb465f4dc181758570d321db4ac23005f90ffd2b434cc6609a63dd"}, + {file = "numpy-1.22.0-cp39-cp39-win32.whl", hash = "sha256:5a311ee4d983c487a0ab546708edbdd759393a3dc9cd30305170149fedd23c88"}, + {file = "numpy-1.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:a97a954a8c2f046d3817c2bce16e3c7e9a9c2afffaf0400f5c16df5172a67c9c"}, + {file = "numpy-1.22.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb02929b0d6bfab4c48a79bd805bd7419114606947ec8284476167415171f55b"}, + {file = "numpy-1.22.0.zip", hash = "sha256:a955e4128ac36797aaffd49ab44ec74a71c11d6938df83b1285492d277db5397"}, +] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, @@ -1482,6 +1685,10 @@ sentry-sdk = [ {file = "sentry-sdk-0.20.3.tar.gz", hash = "sha256:4ae8d1ced6c67f1c8ea51d82a16721c166c489b76876c9f2c202b8a50334b237"}, {file = "sentry_sdk-0.20.3-py2.py3-none-any.whl", hash = "sha256:e75c8c58932bda8cd293ea8e4b242527129e1caaec91433d21b8b2f20fee030b"}, ] +setuptools-scm = [ + {file = "setuptools_scm-6.3.2-py3-none-any.whl", hash = "sha256:4c64444b1d49c4063ae60bfe1680f611c8b13833d556fd1d6050c0023162a119"}, + {file = "setuptools_scm-6.3.2.tar.gz", hash = "sha256:a49aa8081eeb3514eb9728fa5040f2eaa962d6c6f4ec9c32f6c1fba88f88a0f2"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1510,6 +1717,10 @@ toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] +tomli = [ + {file = "tomli-2.0.0-py3-none-any.whl", hash = "sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224"}, + {file = "tomli-2.0.0.tar.gz", hash = "sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1"}, +] typing-extensions = [ {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, diff --git a/pyproject.toml b/pyproject.toml index 2a216209..1d6ada16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ emojis = "~=0.6.0" coloredlogs = "~=15.0" colorama = { version = "~=0.4.3", markers = "sys_platform == 'win32'" } lxml = "~=4.6" +matplotlib = "^3.5.1" [tool.poetry.dev-dependencies] flake8 = "~=3.8" -- cgit v1.2.3 From 36422df82d9aa010a71a7744a38105fa91aad20d Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Mon, 10 Jan 2022 13:57:28 +0530 Subject: run isort and flake8 --- bot/exts/fun/latex/_latex_cog.py | 4 ++-- bot/exts/fun/latex/_renderer.py | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/bot/exts/fun/latex/_latex_cog.py b/bot/exts/fun/latex/_latex_cog.py index 239f499c..9ab85238 100644 --- a/bot/exts/fun/latex/_latex_cog.py +++ b/bot/exts/fun/latex/_latex_cog.py @@ -1,13 +1,12 @@ import asyncio import hashlib +import re import sys from pathlib import Path -import re import discord from discord.ext import commands - FORMATTED_CODE_REGEX = re.compile( r"(?P(?P```)|``?)" # code delimiter: 1-3 backticks; (?P=block) only matches if it's a block r"(?(block)(?:(?P[a-z]+)\n)?)" # if we're in a block, match optional language (only letters plus newline) @@ -34,6 +33,7 @@ def _prepare_input(text: str) -> str: class Latex(commands.Cog): """Renders latex.""" + @commands.command() @commands.max_concurrency(1, commands.BucketType.guild, wait=True) async def latex(self, ctx: commands.Context, *, query: str) -> None: diff --git a/bot/exts/fun/latex/_renderer.py b/bot/exts/fun/latex/_renderer.py index 3f6528ad..fb72b94c 100644 --- a/bot/exts/fun/latex/_renderer.py +++ b/bot/exts/fun/latex/_renderer.py @@ -1,5 +1,4 @@ import sys - from pathlib import Path from typing import BinaryIO @@ -19,7 +18,9 @@ plt.rcParams.update( def render(text: str, file_handle: BinaryIO) -> None: """ - Saves rendered image in `file_handle`. In case the input is invalid latex, it prints the error to `stderr`. + Saves rendered image in `file_handle`. + + In case the input is invalid latex, it prints the error to `stderr`. """ fig = plt.figure() fig.text(0, 1, text, horizontalalignment="left", verticalalignment="top") @@ -30,9 +31,10 @@ def render(text: str, file_handle: BinaryIO) -> None: sys.exit(err) -def main(): +def main() -> None: """ Renders a latex query and saves the output in a specified file. + Expects two command line arguments: the query and the path to the output file. """ query = sys.argv[1] -- cgit v1.2.3 From 8c849136a1bbcdaf367851fac2bf5d4716717325 Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Mon, 10 Jan 2022 17:21:15 +0530 Subject: format error messages in codeblocks --- bot/exts/fun/latex/_latex_cog.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/bot/exts/fun/latex/_latex_cog.py b/bot/exts/fun/latex/_latex_cog.py index 9ab85238..0cd4c981 100644 --- a/bot/exts/fun/latex/_latex_cog.py +++ b/bot/exts/fun/latex/_latex_cog.py @@ -3,6 +3,7 @@ import hashlib import re import sys from pathlib import Path +from textwrap import dedent import discord from discord.ext import commands @@ -31,6 +32,18 @@ def _prepare_input(text: str) -> str: return text +def _format_err(text: str) -> str: + # prevent escaping the codeblock by inserting a zero-width-joiner + text = text.replace("`", "`\u200d") + return dedent( + f""" + ``` + {text} + ``` + """ + ).strip() + + class Latex(commands.Cog): """Renders latex.""" @@ -55,6 +68,6 @@ class Latex(commands.Cog): if return_code != 0: image_path.unlink() err = (await proc.stderr.read()).decode() - raise commands.BadArgument(err) + raise commands.BadArgument(_format_err(err)) await ctx.send(file=discord.File(image_path, "latex.png")) -- cgit v1.2.3 From 89ebfc684d257286bc7cfcb9023658952314cd3b Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Mon, 10 Jan 2022 17:21:41 +0530 Subject: fix path to cache in gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c914e27a..765f33c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # bot (project-specific) log/* data/* -exts/fun/latex/cache/* +bot/exts/fun/latex/cache/* -- cgit v1.2.3 From 5ef4b73a9d6f3d15d6fb22874e7de3af7d2928e3 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 12 Jan 2022 14:06:50 +0000 Subject: Fix AoC name lookup for anon users When a user doesn't set a name, the AoC API doesn't return a name key at all, so we need to make use of the ID field instead, to build the name based on a similar tempalte that AoC uses for it's leaderboard. Co-authored-by: ToxicKidz <78174417+ToxicKidz@users.noreply.github.com> --- bot/exts/events/advent_of_code/_cog.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 01161f26..3acfef39 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -96,7 +96,9 @@ class AdventOfCode(commands.Cog): # Only give the role to people who have completed all 50 stars continue - member_id = aoc_name_to_member_id.get(member_aoc_info["name"], None) + aoc_name = member_aoc_info["name"] or f"Anonymous #{member_aoc_info['id']}" + + member_id = aoc_name_to_member_id.get(aoc_name) if not member_id: log.debug(f"Could not find member_id for {member_aoc_info['name']}, not giving role.") continue -- cgit v1.2.3 From 3dab549c11e07571e583d8e3d3f6fb3e6fa06031 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Jan 2022 04:01:49 +0000 Subject: chore(deps): bump pillow from 8.4.0 to 9.0.0 Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.4.0 to 9.0.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/8.4.0...9.0.0) --- updated-dependencies: - dependency-name: pillow dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- poetry.lock | 80 ++++++++++++++++++++++++++-------------------------------- pyproject.toml | 2 +- 2 files changed, 37 insertions(+), 45 deletions(-) diff --git a/poetry.lock b/poetry.lock index 6a83efed..68bfc43a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -193,6 +193,7 @@ voice = ["PyNaCl (>=1.3.0,<1.5)"] [package.source] type = "url" url = "https://github.com/Rapptz/discord.py/archive/45d498c1b76deaf3b394d17ccf56112fa691d160.zip" + [[package]] name = "distlib" version = "0.3.4" @@ -473,11 +474,11 @@ flake8-polyfill = ">=1.0.2,<2" [[package]] name = "pillow" -version = "8.4.0" +version = "9.0.0" description = "Python Imaging Library (Fork)" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "pip-licenses" @@ -823,7 +824,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "e3682fd5b518ada5066b36015210f00d223c5485c24e4e7c377e371fe6ef0a0d" +content-hash = "e824a5fa909d43e861478178ad7e77ee04be05a60cd3028bda8bd4754c848616" [metadata.files] aiodns = [ @@ -1237,47 +1238,38 @@ pep8-naming = [ {file = "pep8_naming-0.12.1-py2.py3-none-any.whl", hash = "sha256:4a8daeaeb33cfcde779309fc0c9c0a68a3bbe2ad8a8308b763c5068f86eb9f37"}, ] pillow = [ - {file = "Pillow-8.4.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:81f8d5c81e483a9442d72d182e1fb6dcb9723f289a57e8030811bac9ea3fef8d"}, - {file = "Pillow-8.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3f97cfb1e5a392d75dd8b9fd274d205404729923840ca94ca45a0af57e13dbe6"}, - {file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb9fc393f3c61f9054e1ed26e6fe912c7321af2f41ff49d3f83d05bacf22cc78"}, - {file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d82cdb63100ef5eedb8391732375e6d05993b765f72cb34311fab92103314649"}, - {file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc1afda735a8d109007164714e73771b499768b9bb5afcbbee9d0ff374b43f"}, - {file = "Pillow-8.4.0-cp310-cp310-win32.whl", hash = "sha256:e3dacecfbeec9a33e932f00c6cd7996e62f53ad46fbe677577394aaa90ee419a"}, - {file = "Pillow-8.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:620582db2a85b2df5f8a82ddeb52116560d7e5e6b055095f04ad828d1b0baa39"}, - {file = "Pillow-8.4.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:1bc723b434fbc4ab50bb68e11e93ce5fb69866ad621e3c2c9bdb0cd70e345f55"}, - {file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72cbcfd54df6caf85cc35264c77ede902452d6df41166010262374155947460c"}, - {file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70ad9e5c6cb9b8487280a02c0ad8a51581dcbbe8484ce058477692a27c151c0a"}, - {file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25a49dc2e2f74e65efaa32b153527fc5ac98508d502fa46e74fa4fd678ed6645"}, - {file = "Pillow-8.4.0-cp36-cp36m-win32.whl", hash = "sha256:93ce9e955cc95959df98505e4608ad98281fff037350d8c2671c9aa86bcf10a9"}, - {file = "Pillow-8.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2e4440b8f00f504ee4b53fe30f4e381aae30b0568193be305256b1462216feff"}, - {file = "Pillow-8.4.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8c803ac3c28bbc53763e6825746f05cc407b20e4a69d0122e526a582e3b5e153"}, - {file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8a17b5d948f4ceeceb66384727dde11b240736fddeda54ca740b9b8b1556b29"}, - {file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1394a6ad5abc838c5cd8a92c5a07535648cdf6d09e8e2d6df916dfa9ea86ead8"}, - {file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:792e5c12376594bfcb986ebf3855aa4b7c225754e9a9521298e460e92fb4a488"}, - {file = "Pillow-8.4.0-cp37-cp37m-win32.whl", hash = "sha256:d99ec152570e4196772e7a8e4ba5320d2d27bf22fdf11743dd882936ed64305b"}, - {file = "Pillow-8.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7b7017b61bbcdd7f6363aeceb881e23c46583739cb69a3ab39cb384f6ec82e5b"}, - {file = "Pillow-8.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:d89363f02658e253dbd171f7c3716a5d340a24ee82d38aab9183f7fdf0cdca49"}, - {file = "Pillow-8.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a0956fdc5defc34462bb1c765ee88d933239f9a94bc37d132004775241a7585"}, - {file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b7bb9de00197fb4261825c15551adf7605cf14a80badf1761d61e59da347779"}, - {file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72b9e656e340447f827885b8d7a15fc8c4e68d410dc2297ef6787eec0f0ea409"}, - {file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5a4532a12314149d8b4e4ad8ff09dde7427731fcfa5917ff16d0291f13609df"}, - {file = "Pillow-8.4.0-cp38-cp38-win32.whl", hash = "sha256:82aafa8d5eb68c8463b6e9baeb4f19043bb31fefc03eb7b216b51e6a9981ae09"}, - {file = "Pillow-8.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:066f3999cb3b070a95c3652712cffa1a748cd02d60ad7b4e485c3748a04d9d76"}, - {file = "Pillow-8.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:5503c86916d27c2e101b7f71c2ae2cddba01a2cf55b8395b0255fd33fa4d1f1a"}, - {file = "Pillow-8.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4acc0985ddf39d1bc969a9220b51d94ed51695d455c228d8ac29fcdb25810e6e"}, - {file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b052a619a8bfcf26bd8b3f48f45283f9e977890263e4571f2393ed8898d331b"}, - {file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:493cb4e415f44cd601fcec11c99836f707bb714ab03f5ed46ac25713baf0ff20"}, - {file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8831cb7332eda5dc89b21a7bce7ef6ad305548820595033a4b03cf3091235ed"}, - {file = "Pillow-8.4.0-cp39-cp39-win32.whl", hash = "sha256:5e9ac5f66616b87d4da618a20ab0a38324dbe88d8a39b55be8964eb520021e02"}, - {file = "Pillow-8.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:3eb1ce5f65908556c2d8685a8f0a6e989d887ec4057326f6c22b24e8a172c66b"}, - {file = "Pillow-8.4.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ddc4d832a0f0b4c52fff973a0d44b6c99839a9d016fe4e6a1cb8f3eea96479c2"}, - {file = "Pillow-8.4.0-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3e5ddc44c14042f0844b8cf7d2cd455f6cc80fd7f5eefbe657292cf601d9ad"}, - {file = "Pillow-8.4.0-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70e94281588ef053ae8998039610dbd71bc509e4acbc77ab59d7d2937b10698"}, - {file = "Pillow-8.4.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:3862b7256046fcd950618ed22d1d60b842e3a40a48236a5498746f21189afbbc"}, - {file = "Pillow-8.4.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4901622493f88b1a29bd30ec1a2f683782e57c3c16a2dbc7f2595ba01f639df"}, - {file = "Pillow-8.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84c471a734240653a0ec91dec0996696eea227eafe72a33bd06c92697728046b"}, - {file = "Pillow-8.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:244cf3b97802c34c41905d22810846802a3329ddcb93ccc432870243211c79fc"}, - {file = "Pillow-8.4.0.tar.gz", hash = "sha256:b8e2f83c56e141920c39464b852de3719dfbfb6e3c99a2d8da0edf4fb33176ed"}, + {file = "Pillow-9.0.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:113723312215b25c22df1fdf0e2da7a3b9c357a7d24a93ebbe80bfda4f37a8d4"}, + {file = "Pillow-9.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bb47a548cea95b86494a26c89d153fd31122ed65255db5dcbc421a2d28eb3379"}, + {file = "Pillow-9.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31b265496e603985fad54d52d11970383e317d11e18e856971bdbb86af7242a4"}, + {file = "Pillow-9.0.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d154ed971a4cc04b93a6d5b47f37948d1f621f25de3e8fa0c26b2d44f24e3e8f"}, + {file = "Pillow-9.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fe92813d208ce8aa7d76da878bdc84b90809f79ccbad2a288e9bcbeac1d9bd"}, + {file = "Pillow-9.0.0-cp310-cp310-win32.whl", hash = "sha256:d5dcea1387331c905405b09cdbfb34611050cc52c865d71f2362f354faee1e9f"}, + {file = "Pillow-9.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:52abae4c96b5da630a8b4247de5428f593465291e5b239f3f843a911a3cf0105"}, + {file = "Pillow-9.0.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:72c3110228944019e5f27232296c5923398496b28be42535e3b2dc7297b6e8b6"}, + {file = "Pillow-9.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97b6d21771da41497b81652d44191489296555b761684f82b7b544c49989110f"}, + {file = "Pillow-9.0.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72f649d93d4cc4d8cf79c91ebc25137c358718ad75f99e99e043325ea7d56100"}, + {file = "Pillow-9.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aaf07085c756f6cb1c692ee0d5a86c531703b6e8c9cae581b31b562c16b98ce"}, + {file = "Pillow-9.0.0-cp37-cp37m-win32.whl", hash = "sha256:03b27b197deb4ee400ed57d8d4e572d2d8d80f825b6634daf6e2c18c3c6ccfa6"}, + {file = "Pillow-9.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a09a9d4ec2b7887f7a088bbaacfd5c07160e746e3d47ec5e8050ae3b2a229e9f"}, + {file = "Pillow-9.0.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:490e52e99224858f154975db61c060686df8a6b3f0212a678e5d2e2ce24675c9"}, + {file = "Pillow-9.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:500d397ddf4bbf2ca42e198399ac13e7841956c72645513e8ddf243b31ad2128"}, + {file = "Pillow-9.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ebd8b9137630a7bbbff8c4b31e774ff05bbb90f7911d93ea2c9371e41039b52"}, + {file = "Pillow-9.0.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd0e5062f11cb3e730450a7d9f323f4051b532781026395c4323b8ad055523c4"}, + {file = "Pillow-9.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f3b4522148586d35e78313db4db0df4b759ddd7649ef70002b6c3767d0fdeb7"}, + {file = "Pillow-9.0.0-cp38-cp38-win32.whl", hash = "sha256:0b281fcadbb688607ea6ece7649c5d59d4bbd574e90db6cd030e9e85bde9fecc"}, + {file = "Pillow-9.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5050d681bcf5c9f2570b93bee5d3ec8ae4cf23158812f91ed57f7126df91762"}, + {file = "Pillow-9.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:c2067b3bb0781f14059b112c9da5a91c80a600a97915b4f48b37f197895dd925"}, + {file = "Pillow-9.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2d16b6196fb7a54aff6b5e3ecd00f7c0bab1b56eee39214b2b223a9d938c50af"}, + {file = "Pillow-9.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98cb63ca63cb61f594511c06218ab4394bf80388b3d66cd61d0b1f63ee0ea69f"}, + {file = "Pillow-9.0.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc462d24500ba707e9cbdef436c16e5c8cbf29908278af053008d9f689f56dee"}, + {file = "Pillow-9.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3586e12d874ce2f1bc875a3ffba98732ebb12e18fb6d97be482bd62b56803281"}, + {file = "Pillow-9.0.0-cp39-cp39-win32.whl", hash = "sha256:68e06f8b2248f6dc8b899c3e7ecf02c9f413aab622f4d6190df53a78b93d97a5"}, + {file = "Pillow-9.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:6579f9ba84a3d4f1807c4aab4be06f373017fc65fff43498885ac50a9b47a553"}, + {file = "Pillow-9.0.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:47f5cf60bcb9fbc46011f75c9b45a8b5ad077ca352a78185bd3e7f1d294b98bb"}, + {file = "Pillow-9.0.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fd8053e1f8ff1844419842fd474fc359676b2e2a2b66b11cc59f4fa0a301315"}, + {file = "Pillow-9.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c5439bfb35a89cac50e81c751317faea647b9a3ec11c039900cd6915831064d"}, + {file = "Pillow-9.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95545137fc56ce8c10de646074d242001a112a92de169986abd8c88c27566a05"}, + {file = "Pillow-9.0.0.tar.gz", hash = "sha256:ee6e2963e92762923956fe5d3479b1fdc3b76c83f290aad131a2f98c3df0593e"}, ] pip-licenses = [ {file = "pip-licenses-3.5.3.tar.gz", hash = "sha256:f44860e00957b791c6c6005a3328f2d5eaeee96ddb8e7d87d4b0aa25b02252e4"}, diff --git a/pyproject.toml b/pyproject.toml index 2a216209..7d3f0a5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ aioredis = "~1.3" rapidfuzz = "~=1.4" arrow = "~=1.1.0" beautifulsoup4 = "~=4.9" -pillow = "~=8.1" +pillow = "~=9.0" sentry-sdk = "~=0.19" PyYAML = "~=5.4" async-rediscache = {extras = ["fakeredis"], version = "~=0.1.4"} -- cgit v1.2.3 From dc8c63fa92fbf8b74aafb17b5f58d67e079633b4 Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Mon, 17 Jan 2022 00:10:39 +0100 Subject: Make error messages more consistent in the AoC daystar view (#973) From now on, when the interacting user and the original author of the view is different, the bot will send an ephemeral message regarding the issue. --- bot/exts/events/advent_of_code/views/dayandstarview.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bot/exts/events/advent_of_code/views/dayandstarview.py b/bot/exts/events/advent_of_code/views/dayandstarview.py index a0bfa316..5529c12b 100644 --- a/bot/exts/events/advent_of_code/views/dayandstarview.py +++ b/bot/exts/events/advent_of_code/views/dayandstarview.py @@ -42,7 +42,13 @@ class AoCDropdownView(discord.ui.View): async def interaction_check(self, interaction: discord.Interaction) -> bool: """Global check to ensure that the interacting user is the user who invoked the command originally.""" - return interaction.user == self.original_author + if interaction.user != self.original_author: + await interaction.response.send_message( + ":x: You can't interact with someone else's response. Please run the command yourself!", + ephemeral=True + ) + return False + return True @discord.ui.select( placeholder="Day", -- cgit v1.2.3 From a10c07a85a2387a26103aafcabb7bd3e789624c3 Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Tue, 18 Jan 2022 19:30:16 +0530 Subject: rewrite, use the rtex api instead of mpl --- bot/exts/fun/latex/_latex_cog.py | 86 +++++++++++++++++++++++++++------------- bot/exts/fun/latex/_renderer.py | 47 ---------------------- bot/exts/fun/latex/template.txt | 5 +++ 3 files changed, 63 insertions(+), 75 deletions(-) delete mode 100644 bot/exts/fun/latex/_renderer.py create mode 100644 bot/exts/fun/latex/template.txt diff --git a/bot/exts/fun/latex/_latex_cog.py b/bot/exts/fun/latex/_latex_cog.py index 0cd4c981..72d48b2a 100644 --- a/bot/exts/fun/latex/_latex_cog.py +++ b/bot/exts/fun/latex/_latex_cog.py @@ -1,13 +1,14 @@ -import asyncio import hashlib import re -import sys +import string from pathlib import Path -from textwrap import dedent +from typing import BinaryIO, Optional import discord from discord.ext import commands +from bot.bot import Bot + FORMATTED_CODE_REGEX = re.compile( r"(?P(?P```)|``?)" # code delimiter: 1-3 backticks; (?P=block) only matches if it's a block r"(?(block)(?:(?P[a-z]+)\n)?)" # if we're in a block, match optional language (only letters plus newline) @@ -18,35 +19,65 @@ FORMATTED_CODE_REGEX = re.compile( re.DOTALL | re.IGNORECASE, # "." also matches newlines, case insensitive ) +LATEX_API_URL = "https://rtex.probablyaweb.site/api/v2" +PASTEBIN_URL = "https://paste.pythondiscord.com" + THIS_DIR = Path(__file__).parent CACHE_DIRECTORY = THIS_DIR / "cache" CACHE_DIRECTORY.mkdir(exist_ok=True) +TEMPLATE = string.Template((THIS_DIR / "template.txt").read_text()) def _prepare_input(text: str) -> str: - text = text.replace(r"\\", "$\n$") # matplotlib uses \n for newlines, not \\ - if match := FORMATTED_CODE_REGEX.match(text): return match.group("code") else: return text -def _format_err(text: str) -> str: - # prevent escaping the codeblock by inserting a zero-width-joiner - text = text.replace("`", "`\u200d") - return dedent( - f""" - ``` - {text} - ``` - """ - ).strip() +class InvalidLatexError(Exception): + """Represents an error caused by invalid latex.""" + + def __init__(self, logs: str): + super().__init__(logs) + self.logs = logs class Latex(commands.Cog): """Renders latex.""" + def __init__(self, bot: Bot): + self.bot = bot + + async def _generate_image(self, query: str, out_file: BinaryIO) -> None: + """Make an API request and save the generated image to cache.""" + payload = {"code": query, "format": "png"} + async with self.bot.http_session.post(LATEX_API_URL, data=payload, raise_for_status=True) as response: + response_json = await response.json() + if response_json["status"] != "success": + raise InvalidLatexError(logs=response_json["log"]) + async with self.bot.http_session.get( + f"{LATEX_API_URL}/{response_json['filename']}", + data=payload, + raise_for_status=True + ) as response: + out_file.write(await response.read()) + + async def _upload_to_pastebin(self, text: str) -> Optional[str]: + """Uploads `text` to the paste service, returning the url if successful.""" + try: + async with self.bot.http_session.post( + PASTEBIN_URL + "/documents", + data=text, + raise_for_status=True + ) as response: + response_json = await response.json() + if "key" in response_json: + return f"{PASTEBIN_URL}/{response_json['key']}.txt?noredirect" + except Exception: + # 400 (Bad Request) means there are too many characters + pass + @commands.command() @commands.max_concurrency(1, commands.BucketType.guild, wait=True) async def latex(self, ctx: commands.Context, *, query: str) -> None: @@ -56,18 +87,17 @@ class Latex(commands.Cog): image_path = CACHE_DIRECTORY / f"{query_hash}.png" async with ctx.typing(): if not image_path.exists(): - proc = await asyncio.subprocess.create_subprocess_exec( - sys.executable, - "_renderer.py", - query, - image_path.relative_to(THIS_DIR), - cwd=THIS_DIR, - stderr=asyncio.subprocess.PIPE - ) - return_code = await proc.wait() - if return_code != 0: + try: + with open(image_path, "wb") as out_file: + await self._generate_image(TEMPLATE.substitute(text=query), out_file) + except InvalidLatexError as err: + logs_paste_url = await self._upload_to_pastebin(err.logs) + embed = discord.Embed(title="Failed to render input.") + if logs_paste_url: + embed.description = f"[View Logs]({logs_paste_url})" + else: + embed.description = "Couldn't upload logs." + await ctx.send(embed=embed) image_path.unlink() - err = (await proc.stderr.read()).decode() - raise commands.BadArgument(_format_err(err)) - + return await ctx.send(file=discord.File(image_path, "latex.png")) diff --git a/bot/exts/fun/latex/_renderer.py b/bot/exts/fun/latex/_renderer.py deleted file mode 100644 index fb72b94c..00000000 --- a/bot/exts/fun/latex/_renderer.py +++ /dev/null @@ -1,47 +0,0 @@ -import sys -from pathlib import Path -from typing import BinaryIO - -import matplotlib.pyplot as plt - -# configure fonts and colors for matplotlib -plt.rcParams.update( - { - "font.size": 16, - "mathtext.fontset": "cm", # Computer Modern font set - "mathtext.rm": "serif", - "figure.facecolor": "36393F", # matches Discord's dark mode background color - "text.color": "white", - } -) - - -def render(text: str, file_handle: BinaryIO) -> None: - """ - Saves rendered image in `file_handle`. - - In case the input is invalid latex, it prints the error to `stderr`. - """ - fig = plt.figure() - fig.text(0, 1, text, horizontalalignment="left", verticalalignment="top") - try: - plt.savefig(file_handle, bbox_inches="tight", dpi=600) - except ValueError as err: - # get rid of traceback, keeping just the latex error - sys.exit(err) - - -def main() -> None: - """ - Renders a latex query and saves the output in a specified file. - - Expects two command line arguments: the query and the path to the output file. - """ - query = sys.argv[1] - out_file_path = Path(sys.argv[2]) - with open(out_file_path, "wb") as out_file: - render(query, out_file) - - -if __name__ == "__main__": - main() diff --git a/bot/exts/fun/latex/template.txt b/bot/exts/fun/latex/template.txt new file mode 100644 index 00000000..a20cc279 --- /dev/null +++ b/bot/exts/fun/latex/template.txt @@ -0,0 +1,5 @@ +\documentclass{article} +\begin{document} + \pagenumbering{gobble} + $text +\end{document} -- cgit v1.2.3 From 7e51db4b333cfece3641eb80d64dd51e06070706 Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Wed, 19 Jan 2022 18:36:49 +0530 Subject: remove mpl from deps --- poetry.lock | 210 --------------------------------------------------------- pyproject.toml | 1 - 2 files changed, 211 deletions(-) diff --git a/poetry.lock b/poetry.lock index a63c5fb6..68bfc43a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -160,14 +160,6 @@ humanfriendly = ">=9.1" [package.extras] cron = ["capturer (>=2.4)"] -[[package]] -name = "cycler" -version = "0.11.0" -description = "Composable style cycles" -category = "main" -optional = false -python-versions = ">=3.6" - [[package]] name = "deprecated" version = "1.2.13" @@ -359,27 +351,6 @@ python-versions = "*" [package.dependencies] pycodestyle = ">=2.0.0,<3.0.0" -[[package]] -name = "fonttools" -version = "4.28.5" -description = "Tools to manipulate font files" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -all = ["fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "zopfli (>=0.1.4)", "lz4 (>=1.7.4.2)", "matplotlib", "sympy", "skia-pathops (>=0.5.0)", "brotlicffi (>=0.8.0)", "scipy", "brotli (>=1.0.1)", "munkres", "unicodedata2 (>=13.0.0)", "xattr"] -graphite = ["lz4 (>=1.7.4.2)"] -interpolatable = ["scipy", "munkres"] -lxml = ["lxml (>=4.0,<5)"] -pathops = ["skia-pathops (>=0.5.0)"] -plot = ["matplotlib"] -symfont = ["sympy"] -type1 = ["xattr"] -ufo = ["fs (>=2.2.0,<3)"] -unicode = ["unicodedata2 (>=13.0.0)"] -woff = ["zopfli (>=0.1.4)", "brotlicffi (>=0.8.0)", "brotli (>=1.0.1)"] - [[package]] name = "hiredis" version = "2.0.0" @@ -432,14 +403,6 @@ requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] plugins = ["setuptools"] -[[package]] -name = "kiwisolver" -version = "1.3.2" -description = "A fast implementation of the Cassowary constraint solver" -category = "main" -optional = false -python-versions = ">=3.7" - [[package]] name = "lxml" version = "4.7.1" @@ -454,25 +417,6 @@ html5 = ["html5lib"] htmlsoup = ["beautifulsoup4"] source = ["Cython (>=0.29.7)"] -[[package]] -name = "matplotlib" -version = "3.5.1" -description = "Python plotting package" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -cycler = ">=0.10" -fonttools = ">=4.22.0" -kiwisolver = ">=1.0.1" -numpy = ">=1.17" -packaging = ">=20.0" -pillow = ">=6.2.0" -pyparsing = ">=2.2.1" -python-dateutil = ">=2.7" -setuptools_scm = ">=4" - [[package]] name = "mccabe" version = "0.6.1" @@ -505,14 +449,6 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "numpy" -version = "1.22.0" -description = "NumPy is the fundamental package for array computing with Python." -category = "main" -optional = false -python-versions = ">=3.8" - [[package]] name = "packaging" version = "21.3" @@ -759,21 +695,6 @@ sanic = ["sanic (>=0.8)"] sqlalchemy = ["sqlalchemy (>=1.2)"] tornado = ["tornado (>=5)"] -[[package]] -name = "setuptools-scm" -version = "6.3.2" -description = "the blessed package to manage your versions by scm tags" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -packaging = ">=20.0" -tomli = ">=1.0.0" - -[package.extras] -toml = ["setuptools (>=42)", "tomli (>=1.0.0)"] - [[package]] name = "six" version = "1.16.0" @@ -841,14 +762,6 @@ category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -[[package]] -name = "tomli" -version = "2.0.0" -description = "A lil' TOML parser" -category = "main" -optional = false -python-versions = ">=3.7" - [[package]] name = "typing-extensions" version = "4.0.1" @@ -1053,10 +966,6 @@ coloredlogs = [ {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, ] -cycler = [ - {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"}, - {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, -] deprecated = [ {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, @@ -1113,10 +1022,6 @@ flake8-tidy-imports = [ flake8-todo = [ {file = "flake8-todo-0.7.tar.gz", hash = "sha256:6e4c5491ff838c06fe5a771b0e95ee15fc005ca57196011011280fc834a85915"}, ] -fonttools = [ - {file = "fonttools-4.28.5-py3-none-any.whl", hash = "sha256:edf251d5d2cc0580d5f72de4621c338d8c66c5f61abb50cf486640f73c8194d5"}, - {file = "fonttools-4.28.5.zip", hash = "sha256:545c05d0f7903a863c2020e07b8f0a57517f2c40d940bded77076397872d14ca"}, -] hiredis = [ {file = "hiredis-2.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b4c8b0bc5841e578d5fb32a16e0c305359b987b850a06964bd5a62739d688048"}, {file = "hiredis-2.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0adea425b764a08270820531ec2218d0508f8ae15a448568109ffcae050fee26"}, @@ -1176,52 +1081,6 @@ isort = [ {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] -kiwisolver = [ - {file = "kiwisolver-1.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1d819553730d3c2724582124aee8a03c846ec4362ded1034c16fb3ef309264e6"}, - {file = "kiwisolver-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d93a1095f83e908fc253f2fb569c2711414c0bfd451cab580466465b235b470"}, - {file = "kiwisolver-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4550a359c5157aaf8507e6820d98682872b9100ce7607f8aa070b4b8af6c298"}, - {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2210f28778c7d2ee13f3c2a20a3a22db889e75f4ec13a21072eabb5693801e84"}, - {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:82f49c5a79d3839bc8f38cb5f4bfc87e15f04cbafa5fbd12fb32c941cb529cfb"}, - {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9661a04ca3c950a8ac8c47f53cbc0b530bce1b52f516a1e87b7736fec24bfff0"}, - {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ddb500a2808c100e72c075cbb00bf32e62763c82b6a882d403f01a119e3f402"}, - {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72be6ebb4e92520b9726d7146bc9c9b277513a57a38efcf66db0620aec0097e0"}, - {file = "kiwisolver-1.3.2-cp310-cp310-win32.whl", hash = "sha256:83d2c9db5dfc537d0171e32de160461230eb14663299b7e6d18ca6dca21e4977"}, - {file = "kiwisolver-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:cba430db673c29376135e695c6e2501c44c256a81495da849e85d1793ee975ad"}, - {file = "kiwisolver-1.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4116ba9a58109ed5e4cb315bdcbff9838f3159d099ba5259c7c7fb77f8537492"}, - {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19554bd8d54cf41139f376753af1a644b63c9ca93f8f72009d50a2080f870f77"}, - {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a4cf5bbdc861987a7745aed7a536c6405256853c94abc9f3287c3fa401b174"}, - {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0007840186bacfaa0aba4466d5890334ea5938e0bb7e28078a0eb0e63b5b59d5"}, - {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec2eba188c1906b05b9b49ae55aae4efd8150c61ba450e6721f64620c50b59eb"}, - {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3dbb3cea20b4af4f49f84cffaf45dd5f88e8594d18568e0225e6ad9dec0e7967"}, - {file = "kiwisolver-1.3.2-cp37-cp37m-win32.whl", hash = "sha256:5326ddfacbe51abf9469fe668944bc2e399181a2158cb5d45e1d40856b2a0589"}, - {file = "kiwisolver-1.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c6572c2dab23c86a14e82c245473d45b4c515314f1f859e92608dcafbd2f19b8"}, - {file = "kiwisolver-1.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b5074fb09429f2b7bc82b6fb4be8645dcbac14e592128beeff5461dcde0af09f"}, - {file = "kiwisolver-1.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:22521219ca739654a296eea6d4367703558fba16f98688bd8ce65abff36eaa84"}, - {file = "kiwisolver-1.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c358721aebd40c243894298f685a19eb0491a5c3e0b923b9f887ef1193ddf829"}, - {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ba5a1041480c6e0a8b11a9544d53562abc2d19220bfa14133e0cdd9967e97af"}, - {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44e6adf67577dbdfa2d9f06db9fbc5639afefdb5bf2b4dfec25c3a7fbc619536"}, - {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d45d1c74f88b9f41062716c727f78f2a59a5476ecbe74956fafb423c5c87a76"}, - {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70adc3658138bc77a36ce769f5f183169bc0a2906a4f61f09673f7181255ac9b"}, - {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6a5431940f28b6de123de42f0eb47b84a073ee3c3345dc109ad550a3307dd28"}, - {file = "kiwisolver-1.3.2-cp38-cp38-win32.whl", hash = "sha256:ee040a7de8d295dbd261ef2d6d3192f13e2b08ec4a954de34a6fb8ff6422e24c"}, - {file = "kiwisolver-1.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:8dc3d842fa41a33fe83d9f5c66c0cc1f28756530cd89944b63b072281e852031"}, - {file = "kiwisolver-1.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a498bcd005e8a3fedd0022bb30ee0ad92728154a8798b703f394484452550507"}, - {file = "kiwisolver-1.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:80efd202108c3a4150e042b269f7c78643420cc232a0a771743bb96b742f838f"}, - {file = "kiwisolver-1.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f8eb7b6716f5b50e9c06207a14172cf2de201e41912ebe732846c02c830455b9"}, - {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f441422bb313ab25de7b3dbfd388e790eceb76ce01a18199ec4944b369017009"}, - {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:30fa008c172355c7768159983a7270cb23838c4d7db73d6c0f6b60dde0d432c6"}, - {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f8f6c8f4f1cff93ca5058d6ec5f0efda922ecb3f4c5fb76181f327decff98b8"}, - {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba677bcaff9429fd1bf01648ad0901cea56c0d068df383d5f5856d88221fe75b"}, - {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7843b1624d6ccca403a610d1277f7c28ad184c5aa88a1750c1a999754e65b439"}, - {file = "kiwisolver-1.3.2-cp39-cp39-win32.whl", hash = "sha256:e6f5eb2f53fac7d408a45fbcdeda7224b1cfff64919d0f95473420a931347ae9"}, - {file = "kiwisolver-1.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:eedd3b59190885d1ebdf6c5e0ca56828beb1949b4dfe6e5d0256a461429ac386"}, - {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:dedc71c8eb9c5096037766390172c34fb86ef048b8e8958b4e484b9e505d66bc"}, - {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bf7eb45d14fc036514c09554bf983f2a72323254912ed0c3c8e697b62c4c158f"}, - {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2b65bd35f3e06a47b5c30ea99e0c2b88f72c6476eedaf8cfbc8e66adb5479dcf"}, - {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25405f88a37c5f5bcba01c6e350086d65e7465fd1caaf986333d2a045045a223"}, - {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:bcadb05c3d4794eb9eee1dddf1c24215c92fb7b55a80beae7a60530a91060560"}, - {file = "kiwisolver-1.3.2.tar.gz", hash = "sha256:fc4453705b81d03568d5b808ad8f09c77c47534f6ac2e72e733f9ca4714aa75c"}, -] lxml = [ {file = "lxml-4.7.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:d546431636edb1d6a608b348dd58cc9841b81f4116745857b6cb9f8dadb2725f"}, {file = "lxml-4.7.1-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6308062534323f0d3edb4e702a0e26a76ca9e0e23ff99be5d82750772df32a9e"}, @@ -1284,43 +1143,6 @@ lxml = [ {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:447d5009d6b5447b2f237395d0018901dcc673f7d9f82ba26c1b9f9c3b444b60"}, {file = "lxml-4.7.1.tar.gz", hash = "sha256:a1613838aa6b89af4ba10a0f3a972836128801ed008078f8c1244e65958f1b24"}, ] -matplotlib = [ - {file = "matplotlib-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:456cc8334f6d1124e8ff856b42d2cc1c84335375a16448189999496549f7182b"}, - {file = "matplotlib-3.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8a77906dc2ef9b67407cec0bdbf08e3971141e535db888974a915be5e1e3efc6"}, - {file = "matplotlib-3.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e70ae6475cfd0fad3816dcbf6cac536dc6f100f7474be58d59fa306e6e768a4"}, - {file = "matplotlib-3.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53273c5487d1c19c3bc03b9eb82adaf8456f243b97ed79d09dded747abaf1235"}, - {file = "matplotlib-3.5.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3b6f3fd0d8ca37861c31e9a7cab71a0ef14c639b4c95654ea1dd153158bf0df"}, - {file = "matplotlib-3.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8c87cdaf06fd7b2477f68909838ff4176f105064a72ca9d24d3f2a29f73d393"}, - {file = "matplotlib-3.5.1-cp310-cp310-win32.whl", hash = "sha256:e2f28a07b4f82abb40267864ad7b3a4ed76f1b1663e81c7efc84a9b9248f672f"}, - {file = "matplotlib-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:d70a32ee1f8b55eed3fd4e892f0286df8cccc7e0475c11d33b5d0a148f5c7599"}, - {file = "matplotlib-3.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:68fa30cec89b6139dc559ed6ef226c53fd80396da1919a1b5ef672c911aaa767"}, - {file = "matplotlib-3.5.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e3484d8455af3fdb0424eae1789af61f6a79da0c80079125112fd5c1b604218"}, - {file = "matplotlib-3.5.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e293b16cf303fe82995e41700d172a58a15efc5331125d08246b520843ef21ee"}, - {file = "matplotlib-3.5.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e3520a274a0e054e919f5b3279ee5dbccf5311833819ccf3399dab7c83e90a25"}, - {file = "matplotlib-3.5.1-cp37-cp37m-win32.whl", hash = "sha256:2252bfac85cec7af4a67e494bfccf9080bcba8a0299701eab075f48847cca907"}, - {file = "matplotlib-3.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:abf67e05a1b7f86583f6ebd01f69b693b9c535276f4e943292e444855870a1b8"}, - {file = "matplotlib-3.5.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6c094e4bfecd2fa7f9adffd03d8abceed7157c928c2976899de282f3600f0a3d"}, - {file = "matplotlib-3.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:506b210cc6e66a0d1c2bb765d055f4f6bc2745070fb1129203b67e85bbfa5c18"}, - {file = "matplotlib-3.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b04fc29bcef04d4e2d626af28d9d892be6aba94856cb46ed52bcb219ceac8943"}, - {file = "matplotlib-3.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:577ed20ec9a18d6bdedb4616f5e9e957b4c08563a9f985563a31fd5b10564d2a"}, - {file = "matplotlib-3.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e486f60db0cd1c8d68464d9484fd2a94011c1ac8593d765d0211f9daba2bd535"}, - {file = "matplotlib-3.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b71f3a7ca935fc759f2aed7cec06cfe10bc3100fadb5dbd9c435b04e557971e1"}, - {file = "matplotlib-3.5.1-cp38-cp38-win32.whl", hash = "sha256:d24e5bb8028541ce25e59390122f5e48c8506b7e35587e5135efcb6471b4ac6c"}, - {file = "matplotlib-3.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:778d398c4866d8e36ee3bf833779c940b5f57192fa0a549b3ad67bc4c822771b"}, - {file = "matplotlib-3.5.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bb1c613908f11bac270bc7494d68b1ef6e7c224b7a4204d5dacf3522a41e2bc3"}, - {file = "matplotlib-3.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:edf5e4e1d5fb22c18820e8586fb867455de3b109c309cb4fce3aaed85d9468d1"}, - {file = "matplotlib-3.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:40e0d7df05e8efe60397c69b467fc8f87a2affeb4d562fe92b72ff8937a2b511"}, - {file = "matplotlib-3.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a350ca685d9f594123f652ba796ee37219bf72c8e0fc4b471473d87121d6d34"}, - {file = "matplotlib-3.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3e66497cd990b1a130e21919b004da2f1dc112132c01ac78011a90a0f9229778"}, - {file = "matplotlib-3.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:87900c67c0f1728e6db17c6809ec05c025c6624dcf96a8020326ea15378fe8e7"}, - {file = "matplotlib-3.5.1-cp39-cp39-win32.whl", hash = "sha256:b8a4fb2a0c5afbe9604f8a91d7d0f27b1832c3e0b5e365f95a13015822b4cd65"}, - {file = "matplotlib-3.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:fe8d40c434a8e2c68d64c6d6a04e77f21791a93ff6afe0dce169597c110d3079"}, - {file = "matplotlib-3.5.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:34a1fc29f8f96e78ec57a5eff5e8d8b53d3298c3be6df61e7aa9efba26929522"}, - {file = "matplotlib-3.5.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b19a761b948e939a9e20173aaae76070025f0024fc8f7ba08bef22a5c8573afc"}, - {file = "matplotlib-3.5.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6803299cbf4665eca14428d9e886de62e24f4223ac31ab9c5d6d5339a39782c7"}, - {file = "matplotlib-3.5.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:14334b9902ec776461c4b8c6516e26b450f7ebe0b3ef8703bf5cdfbbaecf774a"}, - {file = "matplotlib-3.5.1.tar.gz", hash = "sha256:b2e9810e09c3a47b73ce9cab5a72243a1258f61e7900969097a817232246ce1c"}, -] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, @@ -1407,30 +1229,6 @@ nodeenv = [ {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, ] -numpy = [ - {file = "numpy-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d22662b4b10112c545c91a0741f2436f8ca979ab3d69d03d19322aa970f9695"}, - {file = "numpy-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:11a1f3816ea82eed4178102c56281782690ab5993251fdfd75039aad4d20385f"}, - {file = "numpy-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5dc65644f75a4c2970f21394ad8bea1a844104f0fe01f278631be1c7eae27226"}, - {file = "numpy-1.22.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c16cec1c8cf2728f1d539bd55aaa9d6bb48a7de2f41eb944697293ef65a559"}, - {file = "numpy-1.22.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97e82c39d9856fe7d4f9b86d8a1e66eff99cf3a8b7ba48202f659703d27c46f"}, - {file = "numpy-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:e41e8951749c4b5c9a2dc5fdbc1a4eec6ab2a140fdae9b460b0f557eed870f4d"}, - {file = "numpy-1.22.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bece0a4a49e60e472a6d1f70ac6cdea00f9ab80ff01132f96bd970cdd8a9e5a9"}, - {file = "numpy-1.22.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:818b9be7900e8dc23e013a92779135623476f44a0de58b40c32a15368c01d471"}, - {file = "numpy-1.22.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:47ee7a839f5885bc0c63a74aabb91f6f40d7d7b639253768c4199b37aede7982"}, - {file = "numpy-1.22.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a024181d7aef0004d76fb3bce2a4c9f2e67a609a9e2a6ff2571d30e9976aa383"}, - {file = "numpy-1.22.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f71d57cc8645f14816ae249407d309be250ad8de93ef61d9709b45a0ddf4050c"}, - {file = "numpy-1.22.0-cp38-cp38-win32.whl", hash = "sha256:283d9de87c0133ef98f93dfc09fad3fb382f2a15580de75c02b5bb36a5a159a5"}, - {file = "numpy-1.22.0-cp38-cp38-win_amd64.whl", hash = "sha256:2762331de395739c91f1abb88041f94a080cb1143aeec791b3b223976228af3f"}, - {file = "numpy-1.22.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:76ba7c40e80f9dc815c5e896330700fd6e20814e69da9c1267d65a4d051080f1"}, - {file = "numpy-1.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0cfe07133fd00b27edee5e6385e333e9eeb010607e8a46e1cd673f05f8596595"}, - {file = "numpy-1.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6ed0d073a9c54ac40c41a9c2d53fcc3d4d4ed607670b9e7b0de1ba13b4cbfe6f"}, - {file = "numpy-1.22.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41388e32e40b41dd56eb37fcaa7488b2b47b0adf77c66154d6b89622c110dfe9"}, - {file = "numpy-1.22.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b55b953a1bdb465f4dc181758570d321db4ac23005f90ffd2b434cc6609a63dd"}, - {file = "numpy-1.22.0-cp39-cp39-win32.whl", hash = "sha256:5a311ee4d983c487a0ab546708edbdd759393a3dc9cd30305170149fedd23c88"}, - {file = "numpy-1.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:a97a954a8c2f046d3817c2bce16e3c7e9a9c2afffaf0400f5c16df5172a67c9c"}, - {file = "numpy-1.22.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb02929b0d6bfab4c48a79bd805bd7419114606947ec8284476167415171f55b"}, - {file = "numpy-1.22.0.zip", hash = "sha256:a955e4128ac36797aaffd49ab44ec74a71c11d6938df83b1285492d277db5397"}, -] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, @@ -1676,10 +1474,6 @@ sentry-sdk = [ {file = "sentry-sdk-0.20.3.tar.gz", hash = "sha256:4ae8d1ced6c67f1c8ea51d82a16721c166c489b76876c9f2c202b8a50334b237"}, {file = "sentry_sdk-0.20.3-py2.py3-none-any.whl", hash = "sha256:e75c8c58932bda8cd293ea8e4b242527129e1caaec91433d21b8b2f20fee030b"}, ] -setuptools-scm = [ - {file = "setuptools_scm-6.3.2-py3-none-any.whl", hash = "sha256:4c64444b1d49c4063ae60bfe1680f611c8b13833d556fd1d6050c0023162a119"}, - {file = "setuptools_scm-6.3.2.tar.gz", hash = "sha256:a49aa8081eeb3514eb9728fa5040f2eaa962d6c6f4ec9c32f6c1fba88f88a0f2"}, -] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1708,10 +1502,6 @@ toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] -tomli = [ - {file = "tomli-2.0.0-py3-none-any.whl", hash = "sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224"}, - {file = "tomli-2.0.0.tar.gz", hash = "sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1"}, -] typing-extensions = [ {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, diff --git a/pyproject.toml b/pyproject.toml index 587444c5..7d3f0a5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,6 @@ emojis = "~=0.6.0" coloredlogs = "~=15.0" colorama = { version = "~=0.4.3", markers = "sys_platform == 'win32'" } lxml = "~=4.6" -matplotlib = "^3.5.1" [tool.poetry.dev-dependencies] flake8 = "~=3.8" -- cgit v1.2.3 From caf36c69a06e2c87d258b61afc17d80d53835c79 Mon Sep 17 00:00:00 2001 From: Xithrius <15021300+Xithrius@users.noreply.github.com> Date: Thu, 20 Jan 2022 17:59:10 -0800 Subject: Added even more conversation starter topics. (#1014) --- bot/resources/utilities/py_topics.yaml | 26 +++++++++++++++++++++++--- bot/resources/utilities/starter.yaml | 3 +-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/bot/resources/utilities/py_topics.yaml b/bot/resources/utilities/py_topics.yaml index 1cd2c325..4f684841 100644 --- a/bot/resources/utilities/py_topics.yaml +++ b/bot/resources/utilities/py_topics.yaml @@ -35,12 +35,22 @@ - Have you ever worked with a microcontroller or anything physical with Python before? - Have you ever tried making your own programming language? - Has a recently discovered Python module changed your general use of Python? + - What is your motivation for programming? + - What's your favorite Python related book? + - What's your favorite use of recursion in Python? + - If you could change one thing in Python, what would it be? + - What third-party library do you wish was in the Python standard library? + - Which package do you use the most and why? + - Which Python feature do you love the most? + - Do you have any plans for future projects? + - What modules/libraries do you want to see more projects using? + - What's the most ambitious thing you've done with Python so far? # algos-and-data-structs 650401909852864553: - -# async +# async-and-concurrency 630504881542791169: - Are there any frameworks you wish were async? - How have coroutines changed the way you write Python? @@ -54,12 +64,13 @@ 342318764227821568: - Where do you get your best data? - What is your preferred database and for what use? + - What is the least safe use of databases you've seen? -# data-science +# data-science-and-ai 366673247892275221: - -# discord.py +# discord-bots 343944376055103488: - What unique features does your bot contain, if any? - What commands/features are you proud of making? @@ -78,6 +89,8 @@ - What's a common part of programming we can make harder? - What are the pros and cons of messing with __magic__()? - What's your favorite Python hack? + - What's the weirdest language feature that Python doesn't have, and how can we change that? + - What is the most esoteric code you've written? # game-development 660625198390837248: @@ -110,6 +123,10 @@ - How often do you use GitHub Actions and workflows to automate your repositories? - What's your favorite app on GitHub? +# type-hinting +891788761371906108: + - + # unit-testing 463035728335732738: - @@ -120,6 +137,7 @@ - What's your most used Bash command? - How often do you update your Unix machine? - How often do you upgrade on production? + - What is your least favorite thing about interoperability amongst *NIX operating systems and/or platforms? # user-interfaces 338993628049571840: @@ -128,6 +146,7 @@ - Do you perfer Command Line Interfaces (CLI) or Graphic User Interfaces (GUI)? - What's your favorite CLI (Command Line Interface) or TUI (Terminal Line Interface)? - What's your best GUI project? + - What the best-looking app you've used? # web-development 366673702533988363: @@ -136,3 +155,4 @@ - What is your favorite API library? - What do you use for your frontend? - What does your stack look like? + - What's the best-looking website you've visited? diff --git a/bot/resources/utilities/starter.yaml b/bot/resources/utilities/starter.yaml index 6b0de0ef..ce759e1a 100644 --- a/bot/resources/utilities/starter.yaml +++ b/bot/resources/utilities/starter.yaml @@ -32,8 +32,6 @@ - How many years have you spent coding? - What book do you highly recommend everyone to read? - What websites do you use daily to keep yourself up to date with the industry? -- What made you want to join this Discord server? -- How are you? - What is the best advice you have ever gotten in regards to programming/software? - What is the most satisfying thing you've done in your life? - Who is your favorite music composer/producer/singer? @@ -49,3 +47,4 @@ - What artistic talents do you have? - What is the tallest building you've entered? - What is the oldest computer you've ever used? +- What animals do you like? -- cgit v1.2.3 From 9d49fa0ae13900be5f14d237f92c43d11d85dfc9 Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Fri, 21 Jan 2022 20:21:33 +0530 Subject: rework directory structure --- .gitignore | 2 +- bot/exts/fun/latex.py | 108 +++++++++++++++++++++++++++++++++++ bot/exts/fun/latex/__init__.py | 7 --- bot/exts/fun/latex/_latex_cog.py | 103 --------------------------------- bot/exts/fun/latex/template.txt | 5 -- bot/resources/fun/latex_template.txt | 5 ++ 6 files changed, 114 insertions(+), 116 deletions(-) create mode 100644 bot/exts/fun/latex.py delete mode 100644 bot/exts/fun/latex/__init__.py delete mode 100644 bot/exts/fun/latex/_latex_cog.py delete mode 100644 bot/exts/fun/latex/template.txt create mode 100644 bot/resources/fun/latex_template.txt diff --git a/.gitignore b/.gitignore index 765f33c6..665df8cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # bot (project-specific) log/* data/* -bot/exts/fun/latex/cache/* +bot/exts/fun/_latex_cache/* diff --git a/bot/exts/fun/latex.py b/bot/exts/fun/latex.py new file mode 100644 index 00000000..ac43e95b --- /dev/null +++ b/bot/exts/fun/latex.py @@ -0,0 +1,108 @@ +import hashlib +import re +import string +from pathlib import Path +from typing import BinaryIO, Optional + +import discord +from discord.ext import commands + +from bot.bot import Bot + +FORMATTED_CODE_REGEX = re.compile( + r"(?P(?P```)|``?)" # code delimiter: 1-3 backticks; (?P=block) only matches if it's a block + r"(?(block)(?:(?P[a-z]+)\n)?)" # if we're in a block, match optional language (only letters plus newline) + r"(?:[ \t]*\n)*" # any blank (empty or tabs/spaces only) lines before the code + r"(?P.*?)" # extract all code inside the markup + r"\s*" # any more whitespace before the end of the code markup + r"(?P=delim)", # match the exact same delimiter from the start again + re.DOTALL | re.IGNORECASE, # "." also matches newlines, case insensitive +) + +LATEX_API_URL = "https://rtex.probablyaweb.site/api/v2" +PASTEBIN_URL = "https://paste.pythondiscord.com" + +THIS_DIR = Path(__file__).parent +CACHE_DIRECTORY = THIS_DIR / "_latex_cache" +CACHE_DIRECTORY.mkdir(exist_ok=True) +TEMPLATE = string.Template(Path("bot/resources/fun/latex_template.txt").read_text()) + + +def _prepare_input(text: str) -> str: + if match := FORMATTED_CODE_REGEX.match(text): + return match.group("code") + else: + return text + + +class InvalidLatexError(Exception): + """Represents an error caused by invalid latex.""" + + def __init__(self, logs: str): + super().__init__(logs) + self.logs = logs + + +class Latex(commands.Cog): + """Renders latex.""" + + def __init__(self, bot: Bot): + self.bot = bot + + async def _generate_image(self, query: str, out_file: BinaryIO) -> None: + """Make an API request and save the generated image to cache.""" + payload = {"code": query, "format": "png"} + async with self.bot.http_session.post(LATEX_API_URL, data=payload, raise_for_status=True) as response: + response_json = await response.json() + if response_json["status"] != "success": + raise InvalidLatexError(logs=response_json["log"]) + async with self.bot.http_session.get( + f"{LATEX_API_URL}/{response_json['filename']}", + data=payload, + raise_for_status=True + ) as response: + out_file.write(await response.read()) + + async def _upload_to_pastebin(self, text: str) -> Optional[str]: + """Uploads `text` to the paste service, returning the url if successful.""" + try: + async with self.bot.http_session.post( + PASTEBIN_URL + "/documents", + data=text, + raise_for_status=True + ) as response: + response_json = await response.json() + if "key" in response_json: + return f"{PASTEBIN_URL}/{response_json['key']}.txt?noredirect" + except Exception: + # 400 (Bad Request) means there are too many characters + pass + + @commands.command() + @commands.max_concurrency(1, commands.BucketType.guild, wait=True) + async def latex(self, ctx: commands.Context, *, query: str) -> None: + """Renders the text in latex and sends the image.""" + query = _prepare_input(query) + query_hash = hashlib.md5(query.encode()).hexdigest() + image_path = CACHE_DIRECTORY / f"{query_hash}.png" + async with ctx.typing(): + if not image_path.exists(): + try: + with open(image_path, "wb") as out_file: + await self._generate_image(TEMPLATE.substitute(text=query), out_file) + except InvalidLatexError as err: + logs_paste_url = await self._upload_to_pastebin(err.logs) + embed = discord.Embed(title="Failed to render input.") + if logs_paste_url: + embed.description = f"[View Logs]({logs_paste_url})" + else: + embed.description = "Couldn't upload logs." + await ctx.send(embed=embed) + image_path.unlink() + return + await ctx.send(file=discord.File(image_path, "latex.png")) + + +def setup(bot: Bot) -> None: + """Load the Latex Cog.""" + bot.add_cog(Latex(bot)) diff --git a/bot/exts/fun/latex/__init__.py b/bot/exts/fun/latex/__init__.py deleted file mode 100644 index e58e0447..00000000 --- a/bot/exts/fun/latex/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from bot.bot import Bot -from bot.exts.fun.latex._latex_cog import Latex - - -def setup(bot: Bot) -> None: - """Load the Latex Cog.""" - bot.add_cog(Latex(bot)) diff --git a/bot/exts/fun/latex/_latex_cog.py b/bot/exts/fun/latex/_latex_cog.py deleted file mode 100644 index 72d48b2a..00000000 --- a/bot/exts/fun/latex/_latex_cog.py +++ /dev/null @@ -1,103 +0,0 @@ -import hashlib -import re -import string -from pathlib import Path -from typing import BinaryIO, Optional - -import discord -from discord.ext import commands - -from bot.bot import Bot - -FORMATTED_CODE_REGEX = re.compile( - r"(?P(?P```)|``?)" # code delimiter: 1-3 backticks; (?P=block) only matches if it's a block - r"(?(block)(?:(?P[a-z]+)\n)?)" # if we're in a block, match optional language (only letters plus newline) - r"(?:[ \t]*\n)*" # any blank (empty or tabs/spaces only) lines before the code - r"(?P.*?)" # extract all code inside the markup - r"\s*" # any more whitespace before the end of the code markup - r"(?P=delim)", # match the exact same delimiter from the start again - re.DOTALL | re.IGNORECASE, # "." also matches newlines, case insensitive -) - -LATEX_API_URL = "https://rtex.probablyaweb.site/api/v2" -PASTEBIN_URL = "https://paste.pythondiscord.com" - -THIS_DIR = Path(__file__).parent -CACHE_DIRECTORY = THIS_DIR / "cache" -CACHE_DIRECTORY.mkdir(exist_ok=True) -TEMPLATE = string.Template((THIS_DIR / "template.txt").read_text()) - - -def _prepare_input(text: str) -> str: - if match := FORMATTED_CODE_REGEX.match(text): - return match.group("code") - else: - return text - - -class InvalidLatexError(Exception): - """Represents an error caused by invalid latex.""" - - def __init__(self, logs: str): - super().__init__(logs) - self.logs = logs - - -class Latex(commands.Cog): - """Renders latex.""" - - def __init__(self, bot: Bot): - self.bot = bot - - async def _generate_image(self, query: str, out_file: BinaryIO) -> None: - """Make an API request and save the generated image to cache.""" - payload = {"code": query, "format": "png"} - async with self.bot.http_session.post(LATEX_API_URL, data=payload, raise_for_status=True) as response: - response_json = await response.json() - if response_json["status"] != "success": - raise InvalidLatexError(logs=response_json["log"]) - async with self.bot.http_session.get( - f"{LATEX_API_URL}/{response_json['filename']}", - data=payload, - raise_for_status=True - ) as response: - out_file.write(await response.read()) - - async def _upload_to_pastebin(self, text: str) -> Optional[str]: - """Uploads `text` to the paste service, returning the url if successful.""" - try: - async with self.bot.http_session.post( - PASTEBIN_URL + "/documents", - data=text, - raise_for_status=True - ) as response: - response_json = await response.json() - if "key" in response_json: - return f"{PASTEBIN_URL}/{response_json['key']}.txt?noredirect" - except Exception: - # 400 (Bad Request) means there are too many characters - pass - - @commands.command() - @commands.max_concurrency(1, commands.BucketType.guild, wait=True) - async def latex(self, ctx: commands.Context, *, query: str) -> None: - """Renders the text in latex and sends the image.""" - query = _prepare_input(query) - query_hash = hashlib.md5(query.encode()).hexdigest() - image_path = CACHE_DIRECTORY / f"{query_hash}.png" - async with ctx.typing(): - if not image_path.exists(): - try: - with open(image_path, "wb") as out_file: - await self._generate_image(TEMPLATE.substitute(text=query), out_file) - except InvalidLatexError as err: - logs_paste_url = await self._upload_to_pastebin(err.logs) - embed = discord.Embed(title="Failed to render input.") - if logs_paste_url: - embed.description = f"[View Logs]({logs_paste_url})" - else: - embed.description = "Couldn't upload logs." - await ctx.send(embed=embed) - image_path.unlink() - return - await ctx.send(file=discord.File(image_path, "latex.png")) diff --git a/bot/exts/fun/latex/template.txt b/bot/exts/fun/latex/template.txt deleted file mode 100644 index a20cc279..00000000 --- a/bot/exts/fun/latex/template.txt +++ /dev/null @@ -1,5 +0,0 @@ -\documentclass{article} -\begin{document} - \pagenumbering{gobble} - $text -\end{document} diff --git a/bot/resources/fun/latex_template.txt b/bot/resources/fun/latex_template.txt new file mode 100644 index 00000000..a20cc279 --- /dev/null +++ b/bot/resources/fun/latex_template.txt @@ -0,0 +1,5 @@ +\documentclass{article} +\begin{document} + \pagenumbering{gobble} + $text +\end{document} -- cgit v1.2.3 From 6e791eb757f1213be737cd49433078ef7b06f484 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 25 Jan 2022 21:47:20 +0000 Subject: setuptools use stdlib distutils over embedded This is caused by an upstream issue with setuptools 60.* (via virtualenv) changeing the default to using the setuptools-embedded distutils rather than the stdlib distutils, which breaks within pip's isolated builds. This is explained quite well here https://github.com/pre-commit/pre-commit/issues/2178#issuecomment-1002163763 --- .github/workflows/lint.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 756b3c16..14cfb702 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -26,6 +26,9 @@ jobs: Mozilla Public License 2.0 (MPL 2.0); Public Domain; Python Software Foundation License + # See https://github.com/pre-commit/pre-commit/issues/2178#issuecomment-1002163763 + # for why we set this. + SETUPTOOLS_USE_DISTUTILS: stdlib # Configure pip to cache dependencies and do a user install PIP_NO_CACHE_DIR: false -- cgit v1.2.3 From 1fd39adbb53b14015033fcaff0cbfb4aa81c65d7 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Tue, 25 Jan 2022 12:13:07 +0300 Subject: Clean Up Constants File This commit moves the logging constants from the Client class to a new Logging class. Flake8 is renabled for constants, and the error it was disabled for was specifically ignored. All new errors were fixed. It also fixes a bug with __all__, which was trying to export a missing symbol due to a missing comma. Signed-off-by: Hassan Abouelela --- bot/constants.py | 13 ++++++++++--- bot/log.py | 8 ++++---- tox.ini | 4 +++- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 3b426c47..7e7ee749 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -12,6 +12,7 @@ __all__ = ( "Channels", "Categories", "Client", + "Logging", "Colours", "Emojis", "Icons", @@ -23,7 +24,7 @@ __all__ = ( "Reddit", "RedisConfig", "RedirectOutput", - "PYTHON_PREFIX" + "PYTHON_PREFIX", "MODERATION_ROLES", "STAFF_ROLES", "WHITELISTED_CHANNELS", @@ -37,6 +38,7 @@ log = logging.getLogger(__name__) PYTHON_PREFIX = "!" + @dataclasses.dataclass class AdventOfCodeLeaderboard: id: str @@ -130,18 +132,24 @@ class Categories(NamedTuple): media = 799054581991997460 staff = 364918151625965579 + codejam_categories_name = "Code Jam" # Name of the codejam team categories + class Client(NamedTuple): name = "Sir Lancebot" guild = int(environ.get("BOT_GUILD", 267624335836053506)) prefix = environ.get("PREFIX", ".") token = environ.get("BOT_TOKEN") debug = environ.get("BOT_DEBUG", "true").lower() == "true" - file_logs = environ.get("FILE_LOGS", "false").lower() == "true" github_bot_repo = "https://github.com/python-discord/sir-lancebot" # Override seasonal locks: 1 (January) to 12 (December) month_override = int(environ["MONTH_OVERRIDE"]) if "MONTH_OVERRIDE" in environ else None + + +class Logging(NamedTuple): + debug = Client.debug + file_logs = environ.get("FILE_LOGS", "false").lower() == "true" trace_loggers = environ.get("BOT_TRACE_LOGGERS") @@ -231,7 +239,6 @@ class Emojis: status_dnd = "<:status_dnd:470326272082313216>" status_offline = "<:status_offline:470326266537705472>" - stackoverflow_tag = "<:stack_tag:870926975307501570>" stackoverflow_views = "<:stack_eye:870926992692879371>" diff --git a/bot/log.py b/bot/log.py index 29e696e0..a87a836a 100644 --- a/bot/log.py +++ b/bot/log.py @@ -6,7 +6,7 @@ from pathlib import Path import coloredlogs -from bot.constants import Client +from bot.constants import Logging def setup() -> None: @@ -20,7 +20,7 @@ def setup() -> None: log_format = logging.Formatter(format_string) root_logger = logging.getLogger() - if Client.file_logs: + if Logging.file_logs: # Set up file logging log_file = Path("logs/sir-lancebot.log") log_file.parent.mkdir(exist_ok=True) @@ -45,7 +45,7 @@ def setup() -> None: coloredlogs.install(level=logging.TRACE, stream=sys.stdout) - root_logger.setLevel(logging.DEBUG if Client.debug else logging.INFO) + root_logger.setLevel(logging.DEBUG if Logging.debug else logging.INFO) # Silence irrelevant loggers logging.getLogger("discord").setLevel(logging.ERROR) logging.getLogger("websockets").setLevel(logging.ERROR) @@ -81,7 +81,7 @@ def _set_trace_loggers() -> None: Otherwise if the env var begins with a "*", the root logger is set to the trace level and other contents are ignored. """ - level_filter = Client.trace_loggers + level_filter = Logging.trace_loggers if level_filter: if level_filter.startswith("*"): logging.getLogger().setLevel(logging.TRACE) diff --git a/tox.ini b/tox.ini index f561fcd9..61ff9616 100644 --- a/tox.ini +++ b/tox.ini @@ -20,5 +20,7 @@ exclude= __pycache__,.cache, venv,.venv, tests, - constants.py +per-file-ignores = + # Don't require docstrings in constants + constants.py:D101 import-order-style=pycharm -- cgit v1.2.3 From 335620341046e3b7be547ac9f18d25d1fb9bec55 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Tue, 25 Jan 2022 12:15:50 +0300 Subject: Reduce AOC Logging Output The AOC cog produces a lot of large logs very frequently which have minimal value, causing the logs to be significantly harder to navigate. Signed-off-by: Hassan Abouelela --- bot/constants.py | 2 +- bot/exts/events/advent_of_code/_helpers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 7e7ee749..d39f7361 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -55,7 +55,7 @@ class AdventOfCodeLeaderboard: def session(self) -> str: """Return either the actual `session` cookie or the fallback cookie.""" if self.use_fallback_session: - log.info(f"Returning fallback cookie for board `{self.id}`.") + log.trace(f"Returning fallback cookie for board `{self.id}`.") return AdventOfCode.fallback_session return self._session diff --git a/bot/exts/events/advent_of_code/_helpers.py b/bot/exts/events/advent_of_code/_helpers.py index 15b1329d..6c004901 100644 --- a/bot/exts/events/advent_of_code/_helpers.py +++ b/bot/exts/events/advent_of_code/_helpers.py @@ -255,7 +255,7 @@ async def _fetch_leaderboard_data() -> dict[str, Any]: # Two attempts, one with the original session cookie and one with the fallback session for attempt in range(1, 3): - log.info(f"Attempting to fetch leaderboard `{leaderboard.id}` ({attempt}/2)") + log.debug(f"Attempting to fetch leaderboard `{leaderboard.id}` ({attempt}/2)") cookies = {"session": leaderboard.session} try: raw_data = await _leaderboard_request(leaderboard_url, leaderboard.id, cookies) -- cgit v1.2.3 From 42163c1b57047b8f80f6dcdd93ff4602cae26979 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Tue, 25 Jan 2022 12:34:46 +0300 Subject: Bump isort Version Signed-off-by: Hassan Abouelela --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a2e9a398..2131db72 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: hooks: - id: python-check-blanket-noqa - repo: https://github.com/pycqa/isort - rev: 5.8.0 + rev: 5.10.1 hooks: - id: isort name: isort (python) -- cgit v1.2.3 From 7ae4792482f2a496a747f3b5892c623f71100f3f Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Wed, 26 Jan 2022 19:44:41 +0530 Subject: paste image on a white background --- bot/exts/fun/latex.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/bot/exts/fun/latex.py b/bot/exts/fun/latex.py index ac43e95b..80145843 100644 --- a/bot/exts/fun/latex.py +++ b/bot/exts/fun/latex.py @@ -1,10 +1,12 @@ import hashlib import re import string +from io import BytesIO from pathlib import Path from typing import BinaryIO, Optional import discord +from PIL import Image from discord.ext import commands from bot.bot import Bot @@ -27,6 +29,9 @@ CACHE_DIRECTORY = THIS_DIR / "_latex_cache" CACHE_DIRECTORY.mkdir(exist_ok=True) TEMPLATE = string.Template(Path("bot/resources/fun/latex_template.txt").read_text()) +BG_COLOR = (54, 57, 63, 255) +PAD = 10 + def _prepare_input(text: str) -> str: if match := FORMATTED_CODE_REGEX.match(text): @@ -35,6 +40,14 @@ def _prepare_input(text: str) -> str: return text +def _process_image(data: bytes, out_file: BinaryIO) -> None: + image = Image.open(BytesIO(data)).convert("RGBA") + width, height = image.size + background = Image.new("RGBA", (width + 2 * PAD, height + 2 * PAD), "WHITE") + background.paste(image, (PAD, PAD), image) + background.save(out_file) + + class InvalidLatexError(Exception): """Represents an error caused by invalid latex.""" @@ -58,10 +71,9 @@ class Latex(commands.Cog): raise InvalidLatexError(logs=response_json["log"]) async with self.bot.http_session.get( f"{LATEX_API_URL}/{response_json['filename']}", - data=payload, raise_for_status=True ) as response: - out_file.write(await response.read()) + _process_image(await response.read(), out_file) async def _upload_to_pastebin(self, text: str) -> Optional[str]: """Uploads `text` to the paste service, returning the url if successful.""" -- cgit v1.2.3 From 7fc90a6cb9e1e867d0fdfe52ae3d0a50c4f07c05 Mon Sep 17 00:00:00 2001 From: Ben Soyka Date: Mon, 31 Jan 2022 17:38:35 -0700 Subject: Fix footer reference to deprecated unsubscribe command --- bot/exts/holidays/valentines/lovecalculator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/holidays/valentines/lovecalculator.py b/bot/exts/holidays/valentines/lovecalculator.py index a53014e5..277c9a39 100644 --- a/bot/exts/holidays/valentines/lovecalculator.py +++ b/bot/exts/holidays/valentines/lovecalculator.py @@ -90,7 +90,7 @@ class LoveCalculator(Cog): name="A letter from Dr. Love:", value=data["text"] ) - embed.set_footer(text=f"You can unsubscribe from lovefest by using {Client.prefix}lovefest unsub") + embed.set_footer(text="You can unsubscribe from lovefest by using !subscribe") await ctx.send(embed=embed) -- cgit v1.2.3 From 851e6adb03e54244ef59c75a41783d9ec548ab69 Mon Sep 17 00:00:00 2001 From: Ben Soyka Date: Mon, 31 Jan 2022 18:55:45 -0700 Subject: Add period to love calculator footer --- bot/exts/holidays/valentines/lovecalculator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/holidays/valentines/lovecalculator.py b/bot/exts/holidays/valentines/lovecalculator.py index 277c9a39..d49d59d4 100644 --- a/bot/exts/holidays/valentines/lovecalculator.py +++ b/bot/exts/holidays/valentines/lovecalculator.py @@ -90,7 +90,7 @@ class LoveCalculator(Cog): name="A letter from Dr. Love:", value=data["text"] ) - embed.set_footer(text="You can unsubscribe from lovefest by using !subscribe") + embed.set_footer(text="You can unsubscribe from lovefest by using !subscribe.") await ctx.send(embed=embed) -- cgit v1.2.3 From 3b0217eb43c8cf01dc0d387ff64639d3f1cfe2fe Mon Sep 17 00:00:00 2001 From: Ben Soyka Date: Mon, 31 Jan 2022 21:02:03 -0700 Subject: Use constant for Python bot prefix --- bot/exts/holidays/valentines/lovecalculator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/holidays/valentines/lovecalculator.py b/bot/exts/holidays/valentines/lovecalculator.py index d49d59d4..c2860fb2 100644 --- a/bot/exts/holidays/valentines/lovecalculator.py +++ b/bot/exts/holidays/valentines/lovecalculator.py @@ -12,7 +12,7 @@ from discord.ext import commands from discord.ext.commands import BadArgument, Cog, clean_content from bot.bot import Bot -from bot.constants import Channels, Client, Lovefest, Month +from bot.constants import Channels, Client, Lovefest, Month, PYTHON_PREFIX from bot.utils.decorators import in_month log = logging.getLogger(__name__) @@ -90,7 +90,7 @@ class LoveCalculator(Cog): name="A letter from Dr. Love:", value=data["text"] ) - embed.set_footer(text="You can unsubscribe from lovefest by using !subscribe.") + embed.set_footer(text=f"You can unsubscribe from lovefest by using {PYTHON_PREFIX}subscribe.") await ctx.send(embed=embed) -- cgit v1.2.3 From a400f81de9f14aba698c47f965824cb7e9b4ed10 Mon Sep 17 00:00:00 2001 From: Ben Soyka Date: Mon, 31 Jan 2022 21:04:46 -0700 Subject: Replace command/channel for lovefest role error in calculator --- bot/exts/holidays/valentines/lovecalculator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/holidays/valentines/lovecalculator.py b/bot/exts/holidays/valentines/lovecalculator.py index c2860fb2..367eacaf 100644 --- a/bot/exts/holidays/valentines/lovecalculator.py +++ b/bot/exts/holidays/valentines/lovecalculator.py @@ -51,7 +51,7 @@ class LoveCalculator(Cog): raise BadArgument( "This command can only be ran against members with the lovefest role! " "This role be can assigned by running " - f"`{Client.prefix}lovefest sub` in <#{Channels.community_bot_commands}>." + f"`{PYTHON_PREFIX}subscribe` in <#{Channels.bot}>." ) if whom is None: -- cgit v1.2.3 From 74885a48306b30945b9899a4d44a64ba84b4c24c Mon Sep 17 00:00:00 2001 From: Ben Soyka Date: Mon, 31 Jan 2022 21:08:05 -0700 Subject: Remove unused import --- bot/exts/holidays/valentines/lovecalculator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/holidays/valentines/lovecalculator.py b/bot/exts/holidays/valentines/lovecalculator.py index 367eacaf..99fba150 100644 --- a/bot/exts/holidays/valentines/lovecalculator.py +++ b/bot/exts/holidays/valentines/lovecalculator.py @@ -12,7 +12,7 @@ from discord.ext import commands from discord.ext.commands import BadArgument, Cog, clean_content from bot.bot import Bot -from bot.constants import Channels, Client, Lovefest, Month, PYTHON_PREFIX +from bot.constants import Channels, Lovefest, Month, PYTHON_PREFIX from bot.utils.decorators import in_month log = logging.getLogger(__name__) -- cgit v1.2.3 From 365fe989ee0b659b211ffc503f7ab3ca9641c7c9 Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Tue, 1 Feb 2022 10:34:31 +0530 Subject: add comments and docstrings, remove an unused variable --- bot/exts/fun/latex.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bot/exts/fun/latex.py b/bot/exts/fun/latex.py index 80145843..1dc9c2e5 100644 --- a/bot/exts/fun/latex.py +++ b/bot/exts/fun/latex.py @@ -29,11 +29,11 @@ CACHE_DIRECTORY = THIS_DIR / "_latex_cache" CACHE_DIRECTORY.mkdir(exist_ok=True) TEMPLATE = string.Template(Path("bot/resources/fun/latex_template.txt").read_text()) -BG_COLOR = (54, 57, 63, 255) PAD = 10 def _prepare_input(text: str) -> str: + """Extract latex from a codeblock, if it is in one.""" if match := FORMATTED_CODE_REGEX.match(text): return match.group("code") else: @@ -41,9 +41,14 @@ def _prepare_input(text: str) -> str: def _process_image(data: bytes, out_file: BinaryIO) -> None: + """Read `data` as an image file, and paste it on a white background.""" image = Image.open(BytesIO(data)).convert("RGBA") width, height = image.size background = Image.new("RGBA", (width + 2 * PAD, height + 2 * PAD), "WHITE") + + # paste the image on the background, using the same image as the mask + # when an RGBA image is passed as the mask, its alpha band is used. + # this has the effect of skipping pasting the pixels where the image is transparent. background.paste(image, (PAD, PAD), image) background.save(out_file) @@ -95,6 +100,8 @@ class Latex(commands.Cog): async def latex(self, ctx: commands.Context, *, query: str) -> None: """Renders the text in latex and sends the image.""" query = _prepare_input(query) + + # the hash of the query is used as the filename in the cache. query_hash = hashlib.md5(query.encode()).hexdigest() image_path = CACHE_DIRECTORY / f"{query_hash}.png" async with ctx.typing(): -- cgit v1.2.3 From 2447c51a4dc74d48872caa58fe8f5cc350cf3754 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 13 Oct 2021 20:22:54 -0400 Subject: initial commit --- bot/exts/utilities/trivianight.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 bot/exts/utilities/trivianight.py diff --git a/bot/exts/utilities/trivianight.py b/bot/exts/utilities/trivianight.py new file mode 100644 index 00000000..29a9e3d1 --- /dev/null +++ b/bot/exts/utilities/trivianight.py @@ -0,0 +1,15 @@ +from discord.ext import commands + +from bot.bot import Bot + + +class TriviaNight(commands.Cog): + """Cog for the Python Trivia Night event.""" + + def __init__(self, bot: Bot): + self.bot = bot + + +def setup(bot: Bot) -> None: + """Load the TriviaNight cog.""" + bot.add_cog(TriviaNight(bot)) -- cgit v1.2.3 From a4b2facdd51ed7cff7e045e62d12c93797df0e11 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 13 Oct 2021 22:02:37 -0400 Subject: organizing score board (scoreboard.py) --- bot/exts/events/trivianight/__init__.py | 0 bot/exts/events/trivianight/scoreboard.py | 59 ++++++++++++++++++++++++++++++ bot/exts/events/trivianight/trivianight.py | 15 ++++++++ bot/exts/utilities/trivianight.py | 15 -------- 4 files changed, 74 insertions(+), 15 deletions(-) create mode 100644 bot/exts/events/trivianight/__init__.py create mode 100644 bot/exts/events/trivianight/scoreboard.py create mode 100644 bot/exts/events/trivianight/trivianight.py delete mode 100644 bot/exts/utilities/trivianight.py diff --git a/bot/exts/events/trivianight/__init__.py b/bot/exts/events/trivianight/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bot/exts/events/trivianight/scoreboard.py b/bot/exts/events/trivianight/scoreboard.py new file mode 100644 index 00000000..4e94e361 --- /dev/null +++ b/bot/exts/events/trivianight/scoreboard.py @@ -0,0 +1,59 @@ +import discord.ui +from discord import ButtonStyle, Embed, Interaction +from discord.ui import Button, View + +from bot.constants import Colours + + +class ScoreboardView(View): + """View for the scoreboard.""" + + def __init__(self): + self.points = {} + self.speed = {} + + def create_speed_embed(self) -> None: + """Helper function that iterates through `self.speed` to generate a leaderboard embed.""" + speed_embed = Embed( + title="Average Time Taken to Answer a Question", + description="See the leaderboard for how fast each user took to answer a question correctly!", + color=Colours.python_blue, + ) + for user, time_taken in list(self.speed.items())[:10]: + speed_embed.add_field( + name=user, value=f"`{(time_taken[1] / time_taken[0]):.3f}s` (on average)", inline=False + ) + + return speed_embed + + @discord.ui.button(label="Scoreboard for Speed", style=ButtonStyle.green) + async def speed_leaderboard(self, button: Button, interaction: Interaction) -> None: + """Send an ephemeral message with the speed leaderboard embed.""" + await interaction.response.send_message(embed=self.create_speed_embed(), ephemeral=True) + + +class Scoreboard: + """Class for the scoreboard for the trivianight event.""" + + def __init__(self, view: View): + self.view = view + + def __setitem__(self, key: str, value: int): + if key.startswith("points: "): + key = key.removeprefix("points: ") + if key not in self.view.points.keys(): + self.view.points[key] = value + else: + self.view.points[key] += self.view.points[key] + elif key.startswith("speed: "): + key = key.removeprefix("speed: ") + if key not in self.view.speed.keys(): + self.view.speed[key] = [1, value] + else: + self.view.speed[key] = [self.view.speed[key][0] + 1, self.view.speed[key][1] + value] + + def __getitem__(self, item: str): + if item.startswith("points: "): + return self.view.points[item.removeprefix("points: ")] + elif item.startswith("speed: "): + return self.view.speed[item.removepreix("speed: ")] diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py new file mode 100644 index 00000000..29a9e3d1 --- /dev/null +++ b/bot/exts/events/trivianight/trivianight.py @@ -0,0 +1,15 @@ +from discord.ext import commands + +from bot.bot import Bot + + +class TriviaNight(commands.Cog): + """Cog for the Python Trivia Night event.""" + + def __init__(self, bot: Bot): + self.bot = bot + + +def setup(bot: Bot) -> None: + """Load the TriviaNight cog.""" + bot.add_cog(TriviaNight(bot)) diff --git a/bot/exts/utilities/trivianight.py b/bot/exts/utilities/trivianight.py deleted file mode 100644 index 29a9e3d1..00000000 --- a/bot/exts/utilities/trivianight.py +++ /dev/null @@ -1,15 +0,0 @@ -from discord.ext import commands - -from bot.bot import Bot - - -class TriviaNight(commands.Cog): - """Cog for the Python Trivia Night event.""" - - def __init__(self, bot: Bot): - self.bot = bot - - -def setup(bot: Bot) -> None: - """Load the TriviaNight cog.""" - bot.add_cog(TriviaNight(bot)) -- cgit v1.2.3 From 9d347be892925f7bc431ae7177e54fee0503996d Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 17 Oct 2021 23:10:15 -0400 Subject: question view shell finished --- bot/exts/events/trivianight/questions.py | 71 +++++++++++++++++++++++++++++++ bot/exts/events/trivianight/scoreboard.py | 34 ++++++++++++--- 2 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 bot/exts/events/trivianight/questions.py diff --git a/bot/exts/events/trivianight/questions.py b/bot/exts/events/trivianight/questions.py new file mode 100644 index 00000000..21277ce9 --- /dev/null +++ b/bot/exts/events/trivianight/questions.py @@ -0,0 +1,71 @@ +from random import choice +from time import perf_counter + +from discord import ButtonStyle, Embed, Interaction +from discord.ui import Button, View + +from bot.constants import Colours, NEGATIVE_REPLIES +from .scoreboard import Scoreboard + + +class QuestionButton(Button): + """Button subclass for the options of the questions.""" + + def __init__(self, label: str): + self._time = perf_counter() + self.users_picked = {} + super().__init__(label=label, style=ButtonStyle.green) + + def answer(self, label: str) -> dict: + """Returns the dictionary of the users who picked the answer only if it was correct.""" + return self.users_picked if label == self.label else {} + + async def callback(self, interaction: Interaction) -> None: + """When a user interacts with the button, this will be called.""" + if interaction.user.id not in self.users_picked.keys(): + self.users_picked[interaction.user.id] = [self.label, 1, perf_counter() - self._time] + elif self.users_picked[interaction.user.id][1] < 3: + self.users_picked[interaction.user.id] = [ + self.label, self.users_picked[interaction.user.id][0] + 1, perf_counter() - self._time + ] + else: + await interaction.response.send_message( + embed=Embed( + title=choice(NEGATIVE_REPLIES), + description="You've already changed your answer more than once!", + color=Colours.soft_red + ), + ephemeral=True + ) + + +class QuestionView(View): + """View for the questions.""" + + def __init__(self, scoreboard: Scoreboard): + self.scoreboard = scoreboard + self.current_question = {} + self._users_picked = {} + + def _create_current_question(self) -> Embed: + """Helper function to create the embed for the current question.""" + question_embed = Embed( + title=f"Question {self.current_question['number']}", + description=self.current_question["description"], + color=Colours.python_yellow + ) + for label, answer in zip(("A", "B", "C", "D"), self.current_question["answers"]): + question_embed.add_field(name=label, value=answer, inline=False) + + self.buttons = [QuestionButton(label) for label in ("A", "B", "C", "D")] + return question_embed + + def end_question(self) -> dict: + """Returns the dictionaries from the corresponding buttons for those who got it correct.""" + labels = ("A", "B", "C", "D") + label = labels[self.current_question["correct"].index(self.current_question["answers"])] + return_dict = {} + for button in self.buttons: + return_dict.update(button.answer(label)) + + return return_dict diff --git a/bot/exts/events/trivianight/scoreboard.py b/bot/exts/events/trivianight/scoreboard.py index 4e94e361..34535690 100644 --- a/bot/exts/events/trivianight/scoreboard.py +++ b/bot/exts/events/trivianight/scoreboard.py @@ -1,18 +1,34 @@ +from typing import Union + import discord.ui from discord import ButtonStyle, Embed, Interaction from discord.ui import Button, View +from bot.bot import Bot from bot.constants import Colours class ScoreboardView(View): """View for the scoreboard.""" - def __init__(self): + def __init__(self, bot: Bot): + self.bot = bot self.points = {} self.speed = {} - def create_speed_embed(self) -> None: + def create_main_leaderboard(self) -> Embed: + """Helper function that iterates through `self.points` to generate the main leaderboard embed.""" + main_embed = Embed( + title="Winners of the Trivia Night", + description="See the leaderboard for who got the most points during the Trivia Night!", + color=Colours.python_blue, + ) + for user, points in list(self.points.items())[:10]: + main_embed.add_field(name=self.bot.get_user(user), value=f"`{points}` pts", inline=False) + + return main_embed + + def _create_speed_embed(self) -> Embed: """Helper function that iterates through `self.speed` to generate a leaderboard embed.""" speed_embed = Embed( title="Average Time Taken to Answer a Question", @@ -21,7 +37,9 @@ class ScoreboardView(View): ) for user, time_taken in list(self.speed.items())[:10]: speed_embed.add_field( - name=user, value=f"`{(time_taken[1] / time_taken[0]):.3f}s` (on average)", inline=False + name=self.bot.get_user(user), + value=f"`{(time_taken[1] / time_taken[0]):.3f}s` (on average)", + inline=False ) return speed_embed @@ -29,14 +47,14 @@ class ScoreboardView(View): @discord.ui.button(label="Scoreboard for Speed", style=ButtonStyle.green) async def speed_leaderboard(self, button: Button, interaction: Interaction) -> None: """Send an ephemeral message with the speed leaderboard embed.""" - await interaction.response.send_message(embed=self.create_speed_embed(), ephemeral=True) + await interaction.response.send_message(embed=self._create_speed_embed(), ephemeral=True) class Scoreboard: """Class for the scoreboard for the trivianight event.""" - def __init__(self, view: View): - self.view = view + def __init__(self): + self.view = ScoreboardView() def __setitem__(self, key: str, value: int): if key.startswith("points: "): @@ -57,3 +75,7 @@ class Scoreboard: return self.view.points[item.removeprefix("points: ")] elif item.startswith("speed: "): return self.view.speed[item.removepreix("speed: ")] + + def display(self) -> Union[Embed, View]: + """Returns the embed of the main leaderboard along with the ScoreboardView.""" + return self.view.create_main_leaderboard(), self.view -- cgit v1.2.3 From e37115a620ff33d4f0aeb9bac3ced3e8578605e0 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 18 Oct 2021 22:53:54 -0400 Subject: finished basic question interface --- bot/exts/events/trivianight/questions.py | 56 +++++++++++++++++++++++++++---- bot/exts/events/trivianight/scoreboard.py | 2 +- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/bot/exts/events/trivianight/questions.py b/bot/exts/events/trivianight/questions.py index 21277ce9..2c1dbc81 100644 --- a/bot/exts/events/trivianight/questions.py +++ b/bot/exts/events/trivianight/questions.py @@ -42,12 +42,10 @@ class QuestionButton(Button): class QuestionView(View): """View for the questions.""" - def __init__(self, scoreboard: Scoreboard): - self.scoreboard = scoreboard + def __init__(self): self.current_question = {} - self._users_picked = {} - def _create_current_question(self) -> Embed: + def create_current_question(self) -> Embed: """Helper function to create the embed for the current question.""" question_embed = Embed( title=f"Question {self.current_question['number']}", @@ -58,14 +56,60 @@ class QuestionView(View): question_embed.add_field(name=label, value=answer, inline=False) self.buttons = [QuestionButton(label) for label in ("A", "B", "C", "D")] + for button in self.buttons: + self.add_item(button) return question_embed - def end_question(self) -> dict: + def end_question(self) -> tuple[dict, Embed]: """Returns the dictionaries from the corresponding buttons for those who got it correct.""" labels = ("A", "B", "C", "D") label = labels[self.current_question["correct"].index(self.current_question["answers"])] return_dict = {} for button in self.buttons: return_dict.update(button.answer(label)) + self.remove_item(button) + + answer_embed = Embed( + title=f"The correct answer for Question {self.current_question['number']} was", + color=Colours.grass_green + ) + answer_embed.add_field( + name=label, + value=self.current_question["correct"].index(self.current_question["answers"]), + inline=False + ) + + return return_dict, answer_embed + + +class Questions: + """An interface to use from the TriviaNight cog for questions.""" + + def __init__(self, scoreboard: Scoreboard): + self.scoreboard = scoreboard + self.questions = [] + self._ptr = -1 + + def set_questions(self, questions: list) -> None: + """Setting `self.questions` dynamically via a function to set it.""" + self.questions = questions + + def next_question(self) -> None: + """Advances to the next question.""" + self._ptr += 1 + if self._ptr < len(self.questions): + self.questions[self._ptr]["visited"] = True + self.view.current_question = self.questions[self._ptr] + + def current_question(self) -> tuple[Embed, QuestionView]: + """Returns an embed entailing the current question as an embed with a view.""" + return self.view.create_current_question(), self.view + + def end_question(self) -> None: + """Terminates answering of the question and displays the correct answer.""" + scores, answer_embed = self.view.end_question() + for user, score in scores.items(): + self.scoreboard[f"points: {user}"] = score[1] + self.scoreboard[f"speed: {user}"] = score[2] - return return_dict + return answer_embed diff --git a/bot/exts/events/trivianight/scoreboard.py b/bot/exts/events/trivianight/scoreboard.py index 34535690..27a45e30 100644 --- a/bot/exts/events/trivianight/scoreboard.py +++ b/bot/exts/events/trivianight/scoreboard.py @@ -74,7 +74,7 @@ class Scoreboard: if item.startswith("points: "): return self.view.points[item.removeprefix("points: ")] elif item.startswith("speed: "): - return self.view.speed[item.removepreix("speed: ")] + return self.view.speed[item.removeprefix("speed: ")] def display(self) -> Union[Embed, View]: """Returns the embed of the main leaderboard along with the ScoreboardView.""" -- cgit v1.2.3 From a7d00f8de9a5cfa2b9c76f1a2b39ac861787e24e Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 19 Oct 2021 21:43:53 -0400 Subject: add trivianight structure in the main cog --- bot/exts/events/trivianight/_questions.py | 116 +++++++++++++++++++++++++++++ bot/exts/events/trivianight/_scoreboard.py | 81 ++++++++++++++++++++ bot/exts/events/trivianight/questions.py | 115 ---------------------------- bot/exts/events/trivianight/scoreboard.py | 81 -------------------- bot/exts/events/trivianight/trivianight.py | 27 +++++++ 5 files changed, 224 insertions(+), 196 deletions(-) create mode 100644 bot/exts/events/trivianight/_questions.py create mode 100644 bot/exts/events/trivianight/_scoreboard.py delete mode 100644 bot/exts/events/trivianight/questions.py delete mode 100644 bot/exts/events/trivianight/scoreboard.py diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py new file mode 100644 index 00000000..ef56ee81 --- /dev/null +++ b/bot/exts/events/trivianight/_questions.py @@ -0,0 +1,116 @@ +from random import choice +from time import perf_counter + +from discord import ButtonStyle, Embed, Interaction +from discord.ui import Button, View + +from bot.constants import Colours, NEGATIVE_REPLIES +from ._scoreboard import Scoreboard + + +class QuestionButton(Button): + """Button subclass for the options of the questions.""" + + def __init__(self, label: str): + self._time = perf_counter() + self.users_picked = {} + super().__init__(label=label, style=ButtonStyle.green) + + def answer(self, label: str) -> dict: + """Returns the dictionary of the users who picked the answer only if it was correct.""" + return self.users_picked if label == self.label else {} + + async def callback(self, interaction: Interaction) -> None: + """When a user interacts with the button, this will be called.""" + if interaction.user.id not in self.users_picked.keys(): + self.users_picked[interaction.user.id] = [self.label, 1, perf_counter() - self._time] + elif self.users_picked[interaction.user.id][1] < 3: + self.users_picked[interaction.user.id] = [ + self.label, self.users_picked[interaction.user.id][0] + 1, perf_counter() - self._time + ] + else: + await interaction.response.send_message( + embed=Embed( + title=choice(NEGATIVE_REPLIES), + description="You've already changed your answer more than once!", + color=Colours.soft_red + ), + ephemeral=True + ) + + +class QuestionView(View): + """View for the questions.""" + + def __init__(self): + self.current_question = {} + + def create_current_question(self) -> Embed: + """Helper function to create the embed for the current question.""" + question_embed = Embed( + title=f"Question {self.current_question['number']}", + description=self.current_question["description"], + color=Colours.python_yellow + ) + for label, answer in zip(("A", "B", "C", "D"), self.current_question["answers"]): + question_embed.add_field(name=label, value=answer, inline=False) + + self.buttons = [QuestionButton(label) for label in ("A", "B", "C", "D")] + for button in self.buttons: + self.add_item(button) + return question_embed + + def end_question(self) -> tuple[dict, Embed]: + """Returns the dictionaries from the corresponding buttons for those who got it correct.""" + labels = ("A", "B", "C", "D") + label = labels[self.current_question["correct"].index(self.current_question["answers"])] + return_dict = {} + for button in self.buttons: + return_dict.update(button.answer(label)) + self.remove_item(button) + + answer_embed = Embed( + title=f"The correct answer for Question {self.current_question['number']} was", + color=Colours.grass_green + ) + answer_embed.add_field( + name=label, + value=self.current_question["correct"].index(self.current_question["answers"]), + inline=False + ) + + return return_dict, answer_embed + + +class Questions: + """An interface to use from the TriviaNight cog for questions.""" + + def __init__(self, scoreboard: Scoreboard): + self.scoreboard = scoreboard + self.view = QuestionView() + self.questions = [] + self._ptr = -1 + + def set_questions(self, questions: list) -> None: + """Setting `self.questions` dynamically via a function to set it.""" + self.questions = questions + + def next_question(self) -> None: + """Advances to the next question.""" + self._ptr += 1 + if self._ptr < len(self.questions): + self.questions[self._ptr]["visited"] = True + self.view.current_question = self.questions[self._ptr] + + def current_question(self) -> tuple[Embed, QuestionView]: + """Returns an embed entailing the current question as an embed with a view.""" + return self.view.create_current_question(), self.view + + def end_question(self) -> None: + """Terminates answering of the question and displays the correct answer.""" + scores, answer_embed = self.view.end_question() + for user, score in scores.items(): + self.scoreboard[f"points: {user}"] = score[1] + self.scoreboard[f"speed: {user}"] = score[2] + + return answer_embed diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py new file mode 100644 index 00000000..96ff5ced --- /dev/null +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -0,0 +1,81 @@ +from typing import Union + +import discord.ui +from discord import ButtonStyle, Embed, Interaction +from discord.ui import Button, View + +from bot.bot import Bot +from bot.constants import Colours + + +class ScoreboardView(View): + """View for the scoreboard.""" + + def __init__(self, bot: Bot): + self.bot = bot + self.points = {} + self.speed = {} + + def create_main_leaderboard(self) -> Embed: + """Helper function that iterates through `self.points` to generate the main leaderboard embed.""" + main_embed = Embed( + title="Winners of the Trivia Night", + description="See the leaderboard for who got the most points during the Trivia Night!", + color=Colours.python_blue, + ) + for user, points in list(self.points.items())[:10]: + main_embed.add_field(name=self.bot.get_user(user), value=f"`{points}` pts", inline=False) + + return main_embed + + def _create_speed_embed(self) -> Embed: + """Helper function that iterates through `self.speed` to generate a leaderboard embed.""" + speed_embed = Embed( + title="Average Time Taken to Answer a Question", + description="See the leaderboard for how fast each user took to answer a question correctly!", + color=Colours.python_blue, + ) + for user, time_taken in list(self.speed.items())[:10]: + speed_embed.add_field( + name=self.bot.get_user(user), + value=f"`{(time_taken[1] / time_taken[0]):.3f}s` (on average)", + inline=False + ) + + return speed_embed + + @discord.ui.button(label="Scoreboard for Speed", style=ButtonStyle.green) + async def speed_leaderboard(self, button: Button, interaction: Interaction) -> None: + """Send an ephemeral message with the speed leaderboard embed.""" + await interaction.response.send_message(embed=self._create_speed_embed(), ephemeral=True) + + +class Scoreboard: + """Class for the scoreboard for the trivianight event.""" + + def __init__(self, bot: Bot): + self.view = ScoreboardView(bot) + + def __setitem__(self, key: str, value: int): + if key.startswith("points: "): + key = key.removeprefix("points: ") + if key not in self.view.points.keys(): + self.view.points[key] = value + else: + self.view.points[key] += self.view.points[key] + elif key.startswith("speed: "): + key = key.removeprefix("speed: ") + if key not in self.view.speed.keys(): + self.view.speed[key] = [1, value] + else: + self.view.speed[key] = [self.view.speed[key][0] + 1, self.view.speed[key][1] + value] + + def __getitem__(self, item: str): + if item.startswith("points: "): + return self.view.points[item.removeprefix("points: ")] + elif item.startswith("speed: "): + return self.view.speed[item.removeprefix("speed: ")] + + def display(self) -> Union[Embed, View]: + """Returns the embed of the main leaderboard along with the ScoreboardView.""" + return self.view.create_main_leaderboard(), self.view diff --git a/bot/exts/events/trivianight/questions.py b/bot/exts/events/trivianight/questions.py deleted file mode 100644 index 2c1dbc81..00000000 --- a/bot/exts/events/trivianight/questions.py +++ /dev/null @@ -1,115 +0,0 @@ -from random import choice -from time import perf_counter - -from discord import ButtonStyle, Embed, Interaction -from discord.ui import Button, View - -from bot.constants import Colours, NEGATIVE_REPLIES -from .scoreboard import Scoreboard - - -class QuestionButton(Button): - """Button subclass for the options of the questions.""" - - def __init__(self, label: str): - self._time = perf_counter() - self.users_picked = {} - super().__init__(label=label, style=ButtonStyle.green) - - def answer(self, label: str) -> dict: - """Returns the dictionary of the users who picked the answer only if it was correct.""" - return self.users_picked if label == self.label else {} - - async def callback(self, interaction: Interaction) -> None: - """When a user interacts with the button, this will be called.""" - if interaction.user.id not in self.users_picked.keys(): - self.users_picked[interaction.user.id] = [self.label, 1, perf_counter() - self._time] - elif self.users_picked[interaction.user.id][1] < 3: - self.users_picked[interaction.user.id] = [ - self.label, self.users_picked[interaction.user.id][0] + 1, perf_counter() - self._time - ] - else: - await interaction.response.send_message( - embed=Embed( - title=choice(NEGATIVE_REPLIES), - description="You've already changed your answer more than once!", - color=Colours.soft_red - ), - ephemeral=True - ) - - -class QuestionView(View): - """View for the questions.""" - - def __init__(self): - self.current_question = {} - - def create_current_question(self) -> Embed: - """Helper function to create the embed for the current question.""" - question_embed = Embed( - title=f"Question {self.current_question['number']}", - description=self.current_question["description"], - color=Colours.python_yellow - ) - for label, answer in zip(("A", "B", "C", "D"), self.current_question["answers"]): - question_embed.add_field(name=label, value=answer, inline=False) - - self.buttons = [QuestionButton(label) for label in ("A", "B", "C", "D")] - for button in self.buttons: - self.add_item(button) - return question_embed - - def end_question(self) -> tuple[dict, Embed]: - """Returns the dictionaries from the corresponding buttons for those who got it correct.""" - labels = ("A", "B", "C", "D") - label = labels[self.current_question["correct"].index(self.current_question["answers"])] - return_dict = {} - for button in self.buttons: - return_dict.update(button.answer(label)) - self.remove_item(button) - - answer_embed = Embed( - title=f"The correct answer for Question {self.current_question['number']} was", - color=Colours.grass_green - ) - answer_embed.add_field( - name=label, - value=self.current_question["correct"].index(self.current_question["answers"]), - inline=False - ) - - return return_dict, answer_embed - - -class Questions: - """An interface to use from the TriviaNight cog for questions.""" - - def __init__(self, scoreboard: Scoreboard): - self.scoreboard = scoreboard - self.questions = [] - self._ptr = -1 - - def set_questions(self, questions: list) -> None: - """Setting `self.questions` dynamically via a function to set it.""" - self.questions = questions - - def next_question(self) -> None: - """Advances to the next question.""" - self._ptr += 1 - if self._ptr < len(self.questions): - self.questions[self._ptr]["visited"] = True - self.view.current_question = self.questions[self._ptr] - - def current_question(self) -> tuple[Embed, QuestionView]: - """Returns an embed entailing the current question as an embed with a view.""" - return self.view.create_current_question(), self.view - - def end_question(self) -> None: - """Terminates answering of the question and displays the correct answer.""" - scores, answer_embed = self.view.end_question() - for user, score in scores.items(): - self.scoreboard[f"points: {user}"] = score[1] - self.scoreboard[f"speed: {user}"] = score[2] - - return answer_embed diff --git a/bot/exts/events/trivianight/scoreboard.py b/bot/exts/events/trivianight/scoreboard.py deleted file mode 100644 index 27a45e30..00000000 --- a/bot/exts/events/trivianight/scoreboard.py +++ /dev/null @@ -1,81 +0,0 @@ -from typing import Union - -import discord.ui -from discord import ButtonStyle, Embed, Interaction -from discord.ui import Button, View - -from bot.bot import Bot -from bot.constants import Colours - - -class ScoreboardView(View): - """View for the scoreboard.""" - - def __init__(self, bot: Bot): - self.bot = bot - self.points = {} - self.speed = {} - - def create_main_leaderboard(self) -> Embed: - """Helper function that iterates through `self.points` to generate the main leaderboard embed.""" - main_embed = Embed( - title="Winners of the Trivia Night", - description="See the leaderboard for who got the most points during the Trivia Night!", - color=Colours.python_blue, - ) - for user, points in list(self.points.items())[:10]: - main_embed.add_field(name=self.bot.get_user(user), value=f"`{points}` pts", inline=False) - - return main_embed - - def _create_speed_embed(self) -> Embed: - """Helper function that iterates through `self.speed` to generate a leaderboard embed.""" - speed_embed = Embed( - title="Average Time Taken to Answer a Question", - description="See the leaderboard for how fast each user took to answer a question correctly!", - color=Colours.python_blue, - ) - for user, time_taken in list(self.speed.items())[:10]: - speed_embed.add_field( - name=self.bot.get_user(user), - value=f"`{(time_taken[1] / time_taken[0]):.3f}s` (on average)", - inline=False - ) - - return speed_embed - - @discord.ui.button(label="Scoreboard for Speed", style=ButtonStyle.green) - async def speed_leaderboard(self, button: Button, interaction: Interaction) -> None: - """Send an ephemeral message with the speed leaderboard embed.""" - await interaction.response.send_message(embed=self._create_speed_embed(), ephemeral=True) - - -class Scoreboard: - """Class for the scoreboard for the trivianight event.""" - - def __init__(self): - self.view = ScoreboardView() - - def __setitem__(self, key: str, value: int): - if key.startswith("points: "): - key = key.removeprefix("points: ") - if key not in self.view.points.keys(): - self.view.points[key] = value - else: - self.view.points[key] += self.view.points[key] - elif key.startswith("speed: "): - key = key.removeprefix("speed: ") - if key not in self.view.speed.keys(): - self.view.speed[key] = [1, value] - else: - self.view.speed[key] = [self.view.speed[key][0] + 1, self.view.speed[key][1] + value] - - def __getitem__(self, item: str): - if item.startswith("points: "): - return self.view.points[item.removeprefix("points: ")] - elif item.startswith("speed: "): - return self.view.speed[item.removeprefix("speed: ")] - - def display(self) -> Union[Embed, View]: - """Returns the embed of the main leaderboard along with the ScoreboardView.""" - return self.view.create_main_leaderboard(), self.view diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 29a9e3d1..66b2ae43 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -1,6 +1,13 @@ +from json import loads +from random import choice + +from discord import Embed from discord.ext import commands from bot.bot import Bot +from bot.constants import Colours, POSITIVE_REPLIES +from ._questions import Questions +from ._scoreboard import Scoreboard class TriviaNight(commands.Cog): @@ -8,6 +15,26 @@ class TriviaNight(commands.Cog): def __init__(self, bot: Bot): self.bot = bot + self.scoreboard = Scoreboard(self.bot) + self.questions = Questions(self.scoreboard) + + @commands.group() + async def trivianight(self, ctx: commands.Context) -> None: + """No-op subcommand group for organizing different commands.""" + return + + @trivianight.command() + async def load(self, ctx: commands.Context) -> None: + """Load the JSON file provided into the questions.""" + json_text = (await ctx.message.attachments[0].read()).decode("utf8") + serialized_json = loads(json_text) + self.questions.set_questions(serialized_json) + success_embed = Embed( + title=choice(POSITIVE_REPLIES), + description="The JSON was loaded successfully!", + color=Colours.soft_green + ) + await ctx.send(embed=success_embed) def setup(bot: Bot) -> None: -- cgit v1.2.3 From 00b2f33e3366f885ad7d24bade546f604e065710 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 20 Oct 2021 20:44:42 -0400 Subject: added functionality trivianight next, trivianight stop, trivianight end added --- bot/exts/events/trivianight/_questions.py | 74 +++++++++++++++++------------- bot/exts/events/trivianight/_scoreboard.py | 30 +++++++----- bot/exts/events/trivianight/trivianight.py | 32 +++++++++++-- 3 files changed, 89 insertions(+), 47 deletions(-) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index ef56ee81..f558c50e 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -1,7 +1,9 @@ -from random import choice +from random import choice, randrange from time import perf_counter +from typing import Union -from discord import ButtonStyle, Embed, Interaction +import discord +from discord import Embed, Interaction from discord.ui import Button, View from bot.constants import Colours, NEGATIVE_REPLIES @@ -11,22 +13,21 @@ from ._scoreboard import Scoreboard class QuestionButton(Button): """Button subclass for the options of the questions.""" - def __init__(self, label: str): - self._time = perf_counter() - self.users_picked = {} - super().__init__(label=label, style=ButtonStyle.green) + def __init__(self, label: str, users_picked: dict): + self.users_picked = users_picked + super().__init__(label=label, style=discord.ButtonStyle.green) - def answer(self, label: str) -> dict: - """Returns the dictionary of the users who picked the answer only if it was correct.""" - return self.users_picked if label == self.label else {} + def set_time(self) -> None: + """Sets an instance attribute to a perf counter simulating the question beginning.""" + self._time = perf_counter() async def callback(self, interaction: Interaction) -> None: """When a user interacts with the button, this will be called.""" if interaction.user.id not in self.users_picked.keys(): self.users_picked[interaction.user.id] = [self.label, 1, perf_counter() - self._time] - elif self.users_picked[interaction.user.id][1] < 3: + elif self.users_picked[interaction.user.id][1] < 2: self.users_picked[interaction.user.id] = [ - self.label, self.users_picked[interaction.user.id][0] + 1, perf_counter() - self._time + self.label, self.users_picked[interaction.user.id][1] + 1, perf_counter() - self._time ] else: await interaction.response.send_message( @@ -43,7 +44,12 @@ class QuestionView(View): """View for the questions.""" def __init__(self): + super().__init__() self.current_question = {} + self.users_picked = {} + self.buttons = [QuestionButton(label, self.users_picked) for label in ("A", "B", "C", "D")] + for button in self.buttons: + self.add_item(button) def create_current_question(self) -> Embed: """Helper function to create the embed for the current question.""" @@ -53,30 +59,27 @@ class QuestionView(View): color=Colours.python_yellow ) for label, answer in zip(("A", "B", "C", "D"), self.current_question["answers"]): - question_embed.add_field(name=label, value=answer, inline=False) + question_embed.add_field(name=f"Choice {label}", value=answer, inline=False) - self.buttons = [QuestionButton(label) for label in ("A", "B", "C", "D")] for button in self.buttons: - self.add_item(button) + button.set_time() + return question_embed def end_question(self) -> tuple[dict, Embed]: """Returns the dictionaries from the corresponding buttons for those who got it correct.""" labels = ("A", "B", "C", "D") - label = labels[self.current_question["correct"].index(self.current_question["answers"])] - return_dict = {} + label = labels[self.current_question["answers"].index(self.current_question["correct"])] + return_dict = {name: info for name, info in self.users_picked.items() if info[0] == label} + self.users_picked = {} + for button in self.buttons: - return_dict.update(button.answer(label)) - self.remove_item(button) + button.users_picked = self.users_picked answer_embed = Embed( title=f"The correct answer for Question {self.current_question['number']} was", - color=Colours.grass_green - ) - answer_embed.add_field( - name=label, - value=self.current_question["correct"].index(self.current_question["answers"]), - inline=False + description=self.current_question["correct"], + color=Colours.soft_green ) return return_dict, answer_embed @@ -87,7 +90,6 @@ class Questions: def __init__(self, scoreboard: Scoreboard): self.scoreboard = scoreboard - self.view = QuestionView() self.questions = [] self._ptr = -1 @@ -95,18 +97,26 @@ class Questions: """Setting `self.questions` dynamically via a function to set it.""" self.questions = questions - def next_question(self) -> None: - """Advances to the next question.""" - self._ptr += 1 - if self._ptr < len(self.questions): - self.questions[self._ptr]["visited"] = True - self.view.current_question = self.questions[self._ptr] + def next_question(self) -> Union[Embed, None]: + """Uses another, new question.""" + if all("visited" in question.keys() for question in self.questions.values()): + return Embed( + title=choice(NEGATIVE_REPLIES), + description="All of the questions in the question bank have been used.", + color=Colours.soft_red + ) + + while "visited" in self.questions[self._ptr].keys(): + self._ptr = randrange(0, len(self.questions)) + + self.questions[self._ptr]["visited"] = True + self.view.current_question = self.questions[self._ptr] def current_question(self) -> tuple[Embed, QuestionView]: """Returns an embed entailing the current question as an embed with a view.""" return self.view.create_current_question(), self.view - def end_question(self) -> None: + def end_question(self) -> Embed: """Terminates answering of the question and displays the correct answer.""" scores, answer_embed = self.view.end_question() for user, score in scores.items(): diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 96ff5ced..cab0288f 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -12,32 +12,41 @@ class ScoreboardView(View): """View for the scoreboard.""" def __init__(self, bot: Bot): + super().__init__() self.bot = bot self.points = {} self.speed = {} - def create_main_leaderboard(self) -> Embed: + async def create_main_leaderboard(self) -> Embed: """Helper function that iterates through `self.points` to generate the main leaderboard embed.""" main_embed = Embed( title="Winners of the Trivia Night", description="See the leaderboard for who got the most points during the Trivia Night!", color=Colours.python_blue, ) - for user, points in list(self.points.items())[:10]: - main_embed.add_field(name=self.bot.get_user(user), value=f"`{points}` pts", inline=False) + + # Limit self.points.items() to 10 items at maximum (top 10 users) in the future + for user, points in list(self.points.items()): + user = await self.bot.fetch_user(int(user)) + main_embed.add_field( + name=f"{user.name}#{user.discriminator}", + value=f"`{points}` pts", + inline=False + ) return main_embed - def _create_speed_embed(self) -> Embed: + async def _create_speed_embed(self) -> Embed: """Helper function that iterates through `self.speed` to generate a leaderboard embed.""" speed_embed = Embed( title="Average Time Taken to Answer a Question", description="See the leaderboard for how fast each user took to answer a question correctly!", color=Colours.python_blue, ) - for user, time_taken in list(self.speed.items())[:10]: + for user, time_taken in list(self.speed.items()): + user = await self.bot.fetch_user(int(user)) speed_embed.add_field( - name=self.bot.get_user(user), + name=f"{user.name}#{user.discriminator}", value=f"`{(time_taken[1] / time_taken[0]):.3f}s` (on average)", inline=False ) @@ -47,15 +56,12 @@ class ScoreboardView(View): @discord.ui.button(label="Scoreboard for Speed", style=ButtonStyle.green) async def speed_leaderboard(self, button: Button, interaction: Interaction) -> None: """Send an ephemeral message with the speed leaderboard embed.""" - await interaction.response.send_message(embed=self._create_speed_embed(), ephemeral=True) + await interaction.response.send_message(embed=await self._create_speed_embed(), ephemeral=True) class Scoreboard: """Class for the scoreboard for the trivianight event.""" - def __init__(self, bot: Bot): - self.view = ScoreboardView(bot) - def __setitem__(self, key: str, value: int): if key.startswith("points: "): key = key.removeprefix("points: ") @@ -76,6 +82,6 @@ class Scoreboard: elif item.startswith("speed: "): return self.view.speed[item.removeprefix("speed: ")] - def display(self) -> Union[Embed, View]: + async def display(self) -> Union[Embed, View]: """Returns the embed of the main leaderboard along with the ScoreboardView.""" - return self.view.create_main_leaderboard(), self.view + return await self.view.create_main_leaderboard(), self.view diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 66b2ae43..609f6651 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -1,3 +1,4 @@ +import logging from json import loads from random import choice @@ -6,8 +7,8 @@ from discord.ext import commands from bot.bot import Bot from bot.constants import Colours, POSITIVE_REPLIES -from ._questions import Questions -from ._scoreboard import Scoreboard +from ._questions import QuestionView, Questions +from ._scoreboard import Scoreboard, ScoreboardView class TriviaNight(commands.Cog): @@ -15,7 +16,7 @@ class TriviaNight(commands.Cog): def __init__(self, bot: Bot): self.bot = bot - self.scoreboard = Scoreboard(self.bot) + self.scoreboard = Scoreboard() self.questions = Questions(self.scoreboard) @commands.group() @@ -28,6 +29,9 @@ class TriviaNight(commands.Cog): """Load the JSON file provided into the questions.""" json_text = (await ctx.message.attachments[0].read()).decode("utf8") serialized_json = loads(json_text) + self.questions.view = QuestionView() + logging.getLogger(__name__).debug(self.questions.view) + self.scoreboard.view = ScoreboardView(self.bot) self.questions.set_questions(serialized_json) success_embed = Embed( title=choice(POSITIVE_REPLIES), @@ -36,6 +40,28 @@ class TriviaNight(commands.Cog): ) await ctx.send(embed=success_embed) + @trivianight.command() + async def next(self, ctx: commands.Context) -> None: + """Gets a random question from the unanswered question list and lets user choose the answer.""" + next_question = self.questions.next_question() + if isinstance(next_question, Embed): + await ctx.send(embed=next_question) + return + + question_embed, question_view = self.questions.current_question() + await ctx.send(embed=question_embed, view=question_view) + + @trivianight.command() + async def stop(self, ctx: commands.Context) -> None: + """End the ongoing question to show the correct question.""" + await ctx.send(embed=self.questions.end_question()) + + @trivianight.command() + async def end(self, ctx: commands.Context) -> None: + """Ends the trivia night event and displays the scoreboard.""" + scoreboard_embed, scoreboard_view = await self.scoreboard.display() + await ctx.send(embed=scoreboard_embed, view=scoreboard_view) + def setup(bot: Bot) -> None: """Load the TriviaNight cog.""" -- cgit v1.2.3 From c00acc579015a5bdb407ab96d05a1368e3894c53 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sat, 23 Oct 2021 15:24:02 -0400 Subject: added question and list command --- bot/exts/events/trivianight/_questions.py | 72 +++++++++++++++++++++++++----- bot/exts/events/trivianight/_scoreboard.py | 17 +++---- bot/exts/events/trivianight/trivianight.py | 31 +++++++++++++ 3 files changed, 101 insertions(+), 19 deletions(-) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index f558c50e..df3f237a 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -1,6 +1,6 @@ from random import choice, randrange from time import perf_counter -from typing import Union +from typing import TypedDict, Union import discord from discord import Embed, Interaction @@ -10,6 +10,15 @@ from bot.constants import Colours, NEGATIVE_REPLIES from ._scoreboard import Scoreboard +class CurrentQuestion(TypedDict): + """Representing the different 'keys' of the question taken from the JSON.""" + + number: str + description: str + answers: list + correct: str + + class QuestionButton(Button): """Button subclass for the options of the questions.""" @@ -25,10 +34,26 @@ class QuestionButton(Button): """When a user interacts with the button, this will be called.""" if interaction.user.id not in self.users_picked.keys(): self.users_picked[interaction.user.id] = [self.label, 1, perf_counter() - self._time] + await interaction.response.send_message( + embed=Embed( + title="Success!", + description=f"You chose answer choice {self.label}.", + color=Colours.soft_green + ), + ephemeral=True + ) elif self.users_picked[interaction.user.id][1] < 2: self.users_picked[interaction.user.id] = [ self.label, self.users_picked[interaction.user.id][1] + 1, perf_counter() - self._time ] + await interaction.response.send_message( + embed=Embed( + title="Success!", + description=f"You changed your answer to answer choice {self.label}.", + color=Colours.soft_green + ), + ephemeral=True + ) else: await interaction.response.send_message( embed=Embed( @@ -45,7 +70,7 @@ class QuestionView(View): def __init__(self): super().__init__() - self.current_question = {} + self.current_question: CurrentQuestion self.users_picked = {} self.buttons = [QuestionButton(label, self.users_picked) for label in ("A", "B", "C", "D")] for button in self.buttons: @@ -91,26 +116,51 @@ class Questions: def __init__(self, scoreboard: Scoreboard): self.scoreboard = scoreboard self.questions = [] - self._ptr = -1 def set_questions(self, questions: list) -> None: """Setting `self.questions` dynamically via a function to set it.""" self.questions = questions - def next_question(self) -> Union[Embed, None]: - """Uses another, new question.""" - if all("visited" in question.keys() for question in self.questions.values()): + def next_question(self, number: int = None) -> Union[Embed, None]: + """ + Chooses a random unvisited question from the question bank. + + If the number parameter is specified, it'll head to that specific question. + """ + if all("visited" in question.keys() for question in self.questions): return Embed( title=choice(NEGATIVE_REPLIES), description="All of the questions in the question bank have been used.", color=Colours.soft_red ) - while "visited" in self.questions[self._ptr].keys(): - self._ptr = randrange(0, len(self.questions)) + if number is None: + question_number = randrange(0, len(self.questions)) + while "visited" in self.questions[question_number].keys(): + question_number = randrange(0, len(self.questions)) + else: + question_number = number + + self.questions[question_number]["visited"] = True + self.view.current_question = self.questions[question_number] + + def list_questions(self) -> str: + """ + Lists all questions from the question bank. + + It will put the following into a message: + - Question number + - Question description + - If the question was already 'visited' (displayed) + """ + spaces = len(sorted(self.questions, key=lambda question: len(question['description']))[-1]["description"]) + 3 + formatted_string = "" + for question in self.questions: + formatted_string += f"`Q{question['number']}: {question['description']!r}" \ + f"{' ' * (spaces - len(question['description']) + 2)}" \ + f"|` {':x:' if not question.get('visited') else ':checkmark:'}\n" - self.questions[self._ptr]["visited"] = True - self.view.current_question = self.questions[self._ptr] + return formatted_string.strip() def current_question(self) -> tuple[Embed, QuestionView]: """Returns an embed entailing the current question as an embed with a view.""" @@ -120,7 +170,7 @@ class Questions: """Terminates answering of the question and displays the correct answer.""" scores, answer_embed = self.view.end_question() for user, score in scores.items(): - self.scoreboard[f"points: {user}"] = score[1] + self.scoreboard[f"points: {user}"] = 1 self.scoreboard[f"speed: {user}"] = score[2] return answer_embed diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index cab0288f..7eb7a6a8 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -25,14 +25,14 @@ class ScoreboardView(View): color=Colours.python_blue, ) - # Limit self.points.items() to 10 items at maximum (top 10 users) in the future - for user, points in list(self.points.items()): + current_placement = 1 + for user, points in self.points.items(): user = await self.bot.fetch_user(int(user)) main_embed.add_field( - name=f"{user.name}#{user.discriminator}", - value=f"`{points}` pts", - inline=False + name=f"{current_placement}. {user.name}#{user.discriminator}", + value=f"`{points}` pts" ) + current_placement += 1 return main_embed @@ -43,13 +43,14 @@ class ScoreboardView(View): description="See the leaderboard for how fast each user took to answer a question correctly!", color=Colours.python_blue, ) - for user, time_taken in list(self.speed.items()): + current_placement = 1 + for user, time_taken in self.speed.items(): user = await self.bot.fetch_user(int(user)) speed_embed.add_field( - name=f"{user.name}#{user.discriminator}", + name=f"{current_placement}. {user.name}#{user.discriminator}", value=f"`{(time_taken[1] / time_taken[0]):.3f}s` (on average)", - inline=False ) + current_placement += 1 return speed_embed diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 609f6651..2ec869ab 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -40,6 +40,20 @@ class TriviaNight(commands.Cog): ) await ctx.send(embed=success_embed) + @trivianight.command() + async def reset(self, ctx: commands.Context) -> None: + """Resets previous questions and scoreboards.""" + self.scoreboard.view = ScoreboardView(self.bot) + for question in self.questions.questions: + del question["visited"] + + success_embed = Embed( + title=choice(POSITIVE_REPLIES), + description="The scoreboards were reset and questions marked unvisited!", + color=Colours.soft_green + ) + await ctx.send(embed=success_embed) + @trivianight.command() async def next(self, ctx: commands.Context) -> None: """Gets a random question from the unanswered question list and lets user choose the answer.""" @@ -51,6 +65,23 @@ class TriviaNight(commands.Cog): question_embed, question_view = self.questions.current_question() await ctx.send(embed=question_embed, view=question_view) + @trivianight.command() + async def question(self, ctx: commands.Context, question_number: int) -> None: + """Gets a question from the question bank depending on the question number provided.""" + question = self.questions.next_question(question_number) + if isinstance(question, Embed): + await ctx.send(embed=question) + return + + question_embed, question_view = self.questions.current_question() + await ctx.send(embed=question_embed, view=question_view) + + @trivianight.command() + async def list(self, ctx: commands.Context) -> None: + """Displays all the questions from the question bank.""" + formatted_string = self.questions.list_questions() + await ctx.send(formatted_string) + @trivianight.command() async def stop(self, ctx: commands.Context) -> None: """End the ongoing question to show the correct question.""" -- cgit v1.2.3 From 2ae3deeccaafc8cabe5263decc8ce3c69db970d1 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sat, 23 Oct 2021 17:17:51 -0400 Subject: added ranks as an ephemeral and redid scoreboard --- bot/exts/events/trivianight/_scoreboard.py | 79 +++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 18 deletions(-) diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 7eb7a6a8..d3e1bfb3 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -1,11 +1,12 @@ +from random import choice from typing import Union import discord.ui -from discord import ButtonStyle, Embed, Interaction +from discord import ButtonStyle, Embed, Interaction, Member from discord.ui import Button, View from bot.bot import Bot -from bot.constants import Colours +from bot.constants import Colours, NEGATIVE_REPLIES class ScoreboardView(View): @@ -25,43 +26,85 @@ class ScoreboardView(View): color=Colours.python_blue, ) + formatted_string = "" + participant_points = list(self.points.items())[:30] if len(self.points.items()) > 30 else self.points.items() current_placement = 1 - for user, points in self.points.items(): + for user, points in participant_points: user = await self.bot.fetch_user(int(user)) - main_embed.add_field( - name=f"{current_placement}. {user.name}#{user.discriminator}", - value=f"`{points}` pts" - ) + formatted_string += f"`{current_placement}`. {user.mention} " + formatted_string += f"({points} pts)\n" + if current_placement % 10 == 0: + formatted_string += "⎯⎯⎯⎯⎯⎯⎯⎯\n" current_placement += 1 return main_embed async def _create_speed_embed(self) -> Embed: """Helper function that iterates through `self.speed` to generate a leaderboard embed.""" - speed_embed = Embed( - title="Average Time Taken to Answer a Question", - description="See the leaderboard for how fast each user took to answer a question correctly!", - color=Colours.python_blue, - ) current_placement = 1 - for user, time_taken in self.speed.items(): + formatted_string = "" + participant_speed = list(self.speed.items())[:30] if len(self.speed.items()) > 30 else self.speed.items() + + for user, time_taken in participant_speed: user = await self.bot.fetch_user(int(user)) - speed_embed.add_field( - name=f"{current_placement}. {user.name}#{user.discriminator}", - value=f"`{(time_taken[1] / time_taken[0]):.3f}s` (on average)", - ) + formatted_string += f"`{current_placement}`. {user.mention} " + formatted_string += f"({time_taken:.1f}s)\n" + if current_placement % 10 == 0: + formatted_string += "⎯⎯⎯⎯⎯⎯⎯⎯\n" current_placement += 1 + speed_embed = Embed( + title="Average time taken to answer a question", + description=formatted_string, + color=Colours.python_blue + ) return speed_embed + def _get_rank(self, member: Member) -> Embed: + """Gets the member's rank for the points leaderboard and speed leaderboard.""" + rank_embed = Embed(title=f"Ranks for {member.display_name}", color=Colours.python_blue) + try: + points_rank = str(list(self.points.keys()).index(str(member.id)) + 1) + speed_rank = str(list(self.speed.keys()).index(str(member.id)) + 1) + except ValueError: + return Embed( + title=choice(NEGATIVE_REPLIES), + description="It looks like you didn't participate in the Trivia Night event!", + color=Colours.soft_red + ) + + suffixes = {"1": "st", "2": "nd", "3": "rd"} + rank_embed.add_field( + name="Total Points", + value=( + f"You got {points_rank}{'th' if not (suffix := suffixes.get(points_rank[-1])) else suffix} place" + f" with {self.points[str(member.id)]} points." + ), + inline=False + ) + rank_embed.add_field( + name="Average Speed", + value=( + f"You got {speed_rank}{'th' if not (suffix := suffixes.get(speed_rank[-1])) else suffix} place" + f" with a time of {(self.speed[str(member.id)][1] / self.speed[str(member.id)][0]):.1f} seconds." + ), + inline=False + ) + return rank_embed + @discord.ui.button(label="Scoreboard for Speed", style=ButtonStyle.green) async def speed_leaderboard(self, button: Button, interaction: Interaction) -> None: """Send an ephemeral message with the speed leaderboard embed.""" await interaction.response.send_message(embed=await self._create_speed_embed(), ephemeral=True) + @discord.ui.button(label="What's my rank?", style=ButtonStyle.blurple) + async def rank_button(self, button: Button, interaction: Interaction) -> None: + """Send an ephemeral message with the user's rank for the overall points/average speed.""" + await interaction.response.send_message(embed=self._get_rank(interaction.user), ephemeral=True) + class Scoreboard: - """Class for the scoreboard for the trivianight event.""" + """Class for the scoreboard for the Trivia Night event.""" def __setitem__(self, key: str, value: int): if key.startswith("points: "): -- cgit v1.2.3 From cfae0a04156fbde062fa8847f85f16de49cd3a83 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sat, 23 Oct 2021 23:45:54 -0400 Subject: using enumerate for brevity --- bot/exts/events/trivianight/_scoreboard.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index d3e1bfb3..c39fc666 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -28,12 +28,11 @@ class ScoreboardView(View): formatted_string = "" participant_points = list(self.points.items())[:30] if len(self.points.items()) > 30 else self.points.items() - current_placement = 1 - for user, points in participant_points: + for current_placement, (user, points) in participant_points: user = await self.bot.fetch_user(int(user)) - formatted_string += f"`{current_placement}`. {user.mention} " + formatted_string += f"`{current_placement + 1}`. {user.mention} " formatted_string += f"({points} pts)\n" - if current_placement % 10 == 0: + if (current_placement + 1) % 10 == 0: formatted_string += "⎯⎯⎯⎯⎯⎯⎯⎯\n" current_placement += 1 @@ -41,15 +40,14 @@ class ScoreboardView(View): async def _create_speed_embed(self) -> Embed: """Helper function that iterates through `self.speed` to generate a leaderboard embed.""" - current_placement = 1 formatted_string = "" participant_speed = list(self.speed.items())[:30] if len(self.speed.items()) > 30 else self.speed.items() - for user, time_taken in participant_speed: + for current_placement, (user, time_taken) in enumerate(participant_speed): user = await self.bot.fetch_user(int(user)) - formatted_string += f"`{current_placement}`. {user.mention} " + formatted_string += f"`{current_placement + 1}`. {user.mention} " formatted_string += f"({time_taken:.1f}s)\n" - if current_placement % 10 == 0: + if (current_placement + 1) % 10 == 0: formatted_string += "⎯⎯⎯⎯⎯⎯⎯⎯\n" current_placement += 1 -- cgit v1.2.3 From 0a4c259d4726a3eb044cb9f57b871f4d46bed328 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 26 Oct 2021 07:35:05 -0400 Subject: Better type hinting Co-authored-by: Bluenix --- bot/exts/events/trivianight/_questions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index df3f237a..73d147f0 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -15,7 +15,7 @@ class CurrentQuestion(TypedDict): number: str description: str - answers: list + answers: list[str] correct: str -- cgit v1.2.3 From 848cf63fa885dc51e08d8975b0e834b272cec52d Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 26 Oct 2021 07:36:46 -0400 Subject: Brevity with "answer choice" Co-authored-by: Bluenix --- bot/exts/events/trivianight/_questions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 73d147f0..0c81d6d3 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -37,7 +37,7 @@ class QuestionButton(Button): await interaction.response.send_message( embed=Embed( title="Success!", - description=f"You chose answer choice {self.label}.", + description=f"You chose answer {self.label}.", color=Colours.soft_green ), ephemeral=True -- cgit v1.2.3 From 2b614d8b1ae89fc7e7e85e318b238b464aeae8b4 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 26 Oct 2021 07:40:07 -0400 Subject: Brevity with iterables Co-authored-by: Bluenix --- bot/exts/events/trivianight/_questions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 0c81d6d3..49fc894e 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -83,7 +83,7 @@ class QuestionView(View): description=self.current_question["description"], color=Colours.python_yellow ) - for label, answer in zip(("A", "B", "C", "D"), self.current_question["answers"]): + for label, answer in zip("ABCD", self.current_question["answers"]): question_embed.add_field(name=f"Choice {label}", value=answer, inline=False) for button in self.buttons: -- cgit v1.2.3 From 314cde47e37688fd60173a6f7586f0ab2ba2002b Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 26 Oct 2021 07:41:09 -0400 Subject: Improving word choice (answer [x] rather than answer choice [x]) Co-authored-by: Bluenix --- bot/exts/events/trivianight/_questions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 49fc894e..58b6abdf 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -84,7 +84,7 @@ class QuestionView(View): color=Colours.python_yellow ) for label, answer in zip("ABCD", self.current_question["answers"]): - question_embed.add_field(name=f"Choice {label}", value=answer, inline=False) + question_embed.add_field(name=f"Answer {label}", value=answer, inline=False) for button in self.buttons: button.set_time() -- cgit v1.2.3 From d0c46d4fdf603c08fe50b836e4f8cef0ba9b9430 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 26 Oct 2021 07:42:10 -0400 Subject: Use partial ellipsis in revealing answer Co-authored-by: Bluenix --- bot/exts/events/trivianight/_questions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 58b6abdf..f94b340f 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -102,7 +102,7 @@ class QuestionView(View): button.users_picked = self.users_picked answer_embed = Embed( - title=f"The correct answer for Question {self.current_question['number']} was", + title=f"The correct answer for Question {self.current_question['number']} was..", description=self.current_question["correct"], color=Colours.soft_green ) -- cgit v1.2.3 From 805cb8025433c87454027aad4e70bbe72b86dbdb Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 27 Oct 2021 19:08:34 -0400 Subject: Bluenix's reviews' changes --- bot/exts/events/trivianight/_questions.py | 32 +++++++++------ bot/exts/events/trivianight/_scoreboard.py | 66 ++++++++++++++---------------- bot/exts/events/trivianight/trivianight.py | 3 +- 3 files changed, 51 insertions(+), 50 deletions(-) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index f94b340f..741e8422 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -7,9 +7,19 @@ from discord import Embed, Interaction from discord.ui import Button, View from bot.constants import Colours, NEGATIVE_REPLIES + from ._scoreboard import Scoreboard +class UserScore: + """Marker class for passing into the scoreboard to add points/record speed.""" + + __slots__ = ("user_id",) + + def __init__(self, user_id: int): + self.user_id = user_id + + class CurrentQuestion(TypedDict): """Representing the different 'keys' of the question taken from the JSON.""" @@ -26,30 +36,26 @@ class QuestionButton(Button): self.users_picked = users_picked super().__init__(label=label, style=discord.ButtonStyle.green) - def set_time(self) -> None: - """Sets an instance attribute to a perf counter simulating the question beginning.""" - self._time = perf_counter() - async def callback(self, interaction: Interaction) -> None: """When a user interacts with the button, this will be called.""" if interaction.user.id not in self.users_picked.keys(): - self.users_picked[interaction.user.id] = [self.label, 1, perf_counter() - self._time] + self.users_picked[interaction.user.id] = [self.label, True, perf_counter() - self._time] await interaction.response.send_message( embed=Embed( - title="Success!", + title="Confirming that..", description=f"You chose answer {self.label}.", color=Colours.soft_green ), ephemeral=True ) - elif self.users_picked[interaction.user.id][1] < 2: + elif self.users_picked[interaction.user.id][1] is True: self.users_picked[interaction.user.id] = [ - self.label, self.users_picked[interaction.user.id][1] + 1, perf_counter() - self._time + self.label, False, perf_counter() - self._time ] await interaction.response.send_message( embed=Embed( - title="Success!", - description=f"You changed your answer to answer choice {self.label}.", + title="Confirming that..", + description=f"You changed your answer to answer {self.label}.", color=Colours.soft_green ), ephemeral=True @@ -86,8 +92,9 @@ class QuestionView(View): for label, answer in zip("ABCD", self.current_question["answers"]): question_embed.add_field(name=f"Answer {label}", value=answer, inline=False) + current_time = perf_counter() for button in self.buttons: - button.set_time() + button._time = current_time return question_embed @@ -170,7 +177,6 @@ class Questions: """Terminates answering of the question and displays the correct answer.""" scores, answer_embed = self.view.end_question() for user, score in scores.items(): - self.scoreboard[f"points: {user}"] = 1 - self.scoreboard[f"speed: {user}"] = score[2] + self.scoreboard[UserScore(user)] = {"points": 1, "speed": score[2]} return answer_embed diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index c39fc666..dbec966d 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -20,36 +20,38 @@ class ScoreboardView(View): async def create_main_leaderboard(self) -> Embed: """Helper function that iterates through `self.points` to generate the main leaderboard embed.""" - main_embed = Embed( - title="Winners of the Trivia Night", - description="See the leaderboard for who got the most points during the Trivia Night!", - color=Colours.python_blue, - ) - formatted_string = "" - participant_points = list(self.points.items())[:30] if len(self.points.items()) > 30 else self.points.items() - for current_placement, (user, points) in participant_points: + for current_placement, (user, points) in enumerate(self.points.items()): + if current_placement + 1 > 30: + break + user = await self.bot.fetch_user(int(user)) formatted_string += f"`{current_placement + 1}`. {user.mention} " formatted_string += f"({points} pts)\n" if (current_placement + 1) % 10 == 0: formatted_string += "⎯⎯⎯⎯⎯⎯⎯⎯\n" - current_placement += 1 + + main_embed = Embed( + title="Winners of the Trivia Night", + description=formatted_string, + color=Colours.python_blue, + ) return main_embed async def _create_speed_embed(self) -> Embed: """Helper function that iterates through `self.speed` to generate a leaderboard embed.""" formatted_string = "" - participant_speed = list(self.speed.items())[:30] if len(self.speed.items()) > 30 else self.speed.items() - for current_placement, (user, time_taken) in enumerate(participant_speed): + for current_placement, (user, time_taken) in enumerate(self.speed.items()): + if current_placement + 1 > 30: + break + user = await self.bot.fetch_user(int(user)) formatted_string += f"`{current_placement + 1}`. {user.mention} " - formatted_string += f"({time_taken:.1f}s)\n" + formatted_string += f"({(time_taken[-1] / time_taken[0]):.1f}s)\n" if (current_placement + 1) % 10 == 0: formatted_string += "⎯⎯⎯⎯⎯⎯⎯⎯\n" - current_placement += 1 speed_embed = Embed( title="Average time taken to answer a question", @@ -61,9 +63,10 @@ class ScoreboardView(View): def _get_rank(self, member: Member) -> Embed: """Gets the member's rank for the points leaderboard and speed leaderboard.""" rank_embed = Embed(title=f"Ranks for {member.display_name}", color=Colours.python_blue) + # These are stored as strings so that the last digit can be determined to choose the suffix try: - points_rank = str(list(self.points.keys()).index(str(member.id)) + 1) - speed_rank = str(list(self.speed.keys()).index(str(member.id)) + 1) + points_rank = str(list(self.points.keys()).index(member.id) + 1) + speed_rank = str(list(self.speed.keys()).index(member.id) + 1) except ValueError: return Embed( title=choice(NEGATIVE_REPLIES), @@ -76,7 +79,7 @@ class ScoreboardView(View): name="Total Points", value=( f"You got {points_rank}{'th' if not (suffix := suffixes.get(points_rank[-1])) else suffix} place" - f" with {self.points[str(member.id)]} points." + f" with {self.points[member.id]} points." ), inline=False ) @@ -84,7 +87,7 @@ class ScoreboardView(View): name="Average Speed", value=( f"You got {speed_rank}{'th' if not (suffix := suffixes.get(speed_rank[-1])) else suffix} place" - f" with a time of {(self.speed[str(member.id)][1] / self.speed[str(member.id)][0]):.1f} seconds." + f" with a time of {(self.speed[member.id][1] / self.speed[member.id][0]):.1f} seconds." ), inline=False ) @@ -105,24 +108,17 @@ class Scoreboard: """Class for the scoreboard for the Trivia Night event.""" def __setitem__(self, key: str, value: int): - if key.startswith("points: "): - key = key.removeprefix("points: ") - if key not in self.view.points.keys(): - self.view.points[key] = value - else: - self.view.points[key] += self.view.points[key] - elif key.startswith("speed: "): - key = key.removeprefix("speed: ") - if key not in self.view.speed.keys(): - self.view.speed[key] = [1, value] - else: - self.view.speed[key] = [self.view.speed[key][0] + 1, self.view.speed[key][1] + value] - - def __getitem__(self, item: str): - if item.startswith("points: "): - return self.view.points[item.removeprefix("points: ")] - elif item.startswith("speed: "): - return self.view.speed[item.removeprefix("speed: ")] + if key.user_id not in self.view.points.keys(): + self.view.points[key.user_id] = value["points"] + else: + self.view.points[key.user_id] += self.view.points[key.user_id] + + if key.user_id not in self.view.speed.keys(): + self.view.speed[key.user_id] = [1, value["speed"]] + else: + self.view.speed[key.user_id] = [ + self.view.speed[key.user_id][0] + 1, self.view.speed[key.user_id][1] + value["speed"] + ] async def display(self) -> Union[Embed, View]: """Returns the embed of the main leaderboard along with the ScoreboardView.""" diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 2ec869ab..bb7c205b 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -1,4 +1,3 @@ -import logging from json import loads from random import choice @@ -7,6 +6,7 @@ from discord.ext import commands from bot.bot import Bot from bot.constants import Colours, POSITIVE_REPLIES + from ._questions import QuestionView, Questions from ._scoreboard import Scoreboard, ScoreboardView @@ -30,7 +30,6 @@ class TriviaNight(commands.Cog): json_text = (await ctx.message.attachments[0].read()).decode("utf8") serialized_json = loads(json_text) self.questions.view = QuestionView() - logging.getLogger(__name__).debug(self.questions.view) self.scoreboard.view = ScoreboardView(self.bot) self.questions.set_questions(serialized_json) success_embed = Embed( -- cgit v1.2.3 From f31eaf8f094f9d4572c9e6312fe3510a97441163 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 2 Nov 2021 18:07:17 -0400 Subject: adding stats to the correct answer embed --- bot/exts/events/trivianight/_questions.py | 38 ++++++++++++++++++++++++++---- bot/exts/events/trivianight/_scoreboard.py | 4 ++-- bot/exts/events/trivianight/trivianight.py | 10 ++++++++ 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 741e8422..f0a20521 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -32,13 +32,22 @@ class CurrentQuestion(TypedDict): class QuestionButton(Button): """Button subclass for the options of the questions.""" - def __init__(self, label: str, users_picked: dict): + def __init__(self, label: str, users_picked: dict, view: View): self.users_picked = users_picked + self._view = view super().__init__(label=label, style=discord.ButtonStyle.green) async def callback(self, interaction: Interaction) -> None: """When a user interacts with the button, this will be called.""" + original_message = interaction.message + original_embed = original_message.embeds[0] + if interaction.user.id not in self.users_picked.keys(): + people_answered = original_embed.footer.text + people_answered = f"{int(people_answered[0]) + 1} " \ + f"{'person has' if int(people_answered[0]) + 1 == 1 else 'people have'} answered" + original_embed.set_footer(text=people_answered) + await original_message.edit(embed=original_embed, view=self._view) self.users_picked[interaction.user.id] = [self.label, True, perf_counter() - self._time] await interaction.response.send_message( embed=Embed( @@ -78,7 +87,7 @@ class QuestionView(View): super().__init__() self.current_question: CurrentQuestion self.users_picked = {} - self.buttons = [QuestionButton(label, self.users_picked) for label in ("A", "B", "C", "D")] + self.buttons = [QuestionButton(label, self.users_picked, self) for label in ("A", "B", "C", "D")] for button in self.buttons: self.add_item(button) @@ -92,6 +101,7 @@ class QuestionView(View): for label, answer in zip("ABCD", self.current_question["answers"]): question_embed.add_field(name=f"Answer {label}", value=answer, inline=False) + question_embed.set_footer(text="0 people have answered") current_time = perf_counter() for button in self.buttons: button._time = current_time @@ -103,10 +113,15 @@ class QuestionView(View): labels = ("A", "B", "C", "D") label = labels[self.current_question["answers"].index(self.current_question["correct"])] return_dict = {name: info for name, info in self.users_picked.items() if info[0] == label} - self.users_picked = {} + all_players = list(self.users_picked.items()) + answers_chosen = { + answer_choice: len( + tuple(filter(lambda x: x[0] == answer_choice, self.users_picked.values())) + ) / len(all_players) + for answer_choice in "ABCD" + } - for button in self.buttons: - button.users_picked = self.users_picked + answers_chosen = dict(sorted(answers_chosen.items(), key=lambda item: item[1], reverse=True)) answer_embed = Embed( title=f"The correct answer for Question {self.current_question['number']} was..", @@ -114,6 +129,19 @@ class QuestionView(View): color=Colours.soft_green ) + for answer, percent in answers_chosen.items(): + # The `ord` function is used here to change the letter, say 'A' to its corresponding position in the answers + answer_embed.add_field( + name=f"{percent * 100:.1f}% of players chose", + value=self.current_question['answers'][ord(answer) - 65], + inline=False + ) + + self.users_picked = {} + + for button in self.buttons: + button.users_picked = self.users_picked + return return_dict, answer_embed diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index dbec966d..1bde59f5 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -26,7 +26,7 @@ class ScoreboardView(View): break user = await self.bot.fetch_user(int(user)) - formatted_string += f"`{current_placement + 1}`. {user.mention} " + formatted_string += f"**{current_placement + 1}.** {user.mention} " formatted_string += f"({points} pts)\n" if (current_placement + 1) % 10 == 0: formatted_string += "⎯⎯⎯⎯⎯⎯⎯⎯\n" @@ -48,7 +48,7 @@ class ScoreboardView(View): break user = await self.bot.fetch_user(int(user)) - formatted_string += f"`{current_placement + 1}`. {user.mention} " + formatted_string += f"**{current_placement + 1}.** {user.mention} " formatted_string += f"({(time_taken[-1] / time_taken[0]):.1f}s)\n" if (current_placement + 1) % 10 == 0: formatted_string += "⎯⎯⎯⎯⎯⎯⎯⎯\n" diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index bb7c205b..62b619e8 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -19,6 +19,14 @@ class TriviaNight(commands.Cog): self.scoreboard = Scoreboard() self.questions = Questions(self.scoreboard) + @staticmethod + def unicodeify(text: str) -> str: + """Takes `text` and adds zero-width spaces to prevent copy and pasting the question.""" + return "".join( + f"{letter}\u200b" if letter not in ('\n', '\t', '`', 'p', 'y') else letter + for idx, letter in enumerate(text) + ) + @commands.group() async def trivianight(self, ctx: commands.Context) -> None: """No-op subcommand group for organizing different commands.""" @@ -29,6 +37,8 @@ class TriviaNight(commands.Cog): """Load the JSON file provided into the questions.""" json_text = (await ctx.message.attachments[0].read()).decode("utf8") serialized_json = loads(json_text) + for idx, question in enumerate(serialized_json): + serialized_json[idx] = {**question, **{"description": self.unicodeify(question["description"])}} self.questions.view = QuestionView() self.scoreboard.view = ScoreboardView(self.bot) self.questions.set_questions(serialized_json) -- cgit v1.2.3 From adfddbd7c282e4361fc5d844068b952fae27eaed Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Thu, 4 Nov 2021 11:15:33 -0400 Subject: fixing list showing zero-width spaces --- bot/exts/events/trivianight/_questions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index f0a20521..6937d8ec 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -191,7 +191,8 @@ class Questions: spaces = len(sorted(self.questions, key=lambda question: len(question['description']))[-1]["description"]) + 3 formatted_string = "" for question in self.questions: - formatted_string += f"`Q{question['number']}: {question['description']!r}" \ + question_description = question['description'].replace("\u200b", "") + formatted_string += f"`Q{question['number']}: {question_description!r}" \ f"{' ' * (spaces - len(question['description']) + 2)}" \ f"|` {':x:' if not question.get('visited') else ':checkmark:'}\n" -- cgit v1.2.3 From 7ce200cbe8875baa7071abad4dcca1c7492bf366 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Thu, 4 Nov 2021 11:26:38 -0400 Subject: cog description for .trivianight, and allowing to pick questions that were already chosen --- bot/exts/events/trivianight/_questions.py | 13 +++++++++---- bot/exts/events/trivianight/trivianight.py | 17 ++++++++++++++--- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 6937d8ec..8b4df74d 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -162,7 +162,7 @@ class Questions: If the number parameter is specified, it'll head to that specific question. """ - if all("visited" in question.keys() for question in self.questions): + if all("visited" in question.keys() for question in self.questions) and number is None: return Embed( title=choice(NEGATIVE_REPLIES), description="All of the questions in the question bank have been used.", @@ -179,7 +179,7 @@ class Questions: self.questions[question_number]["visited"] = True self.view.current_question = self.questions[question_number] - def list_questions(self) -> str: + def list_questions(self) -> Union[Embed, str]: """ Lists all questions from the question bank. @@ -188,11 +188,16 @@ class Questions: - Question description - If the question was already 'visited' (displayed) """ + if not self.questions: + return Embed( + title=choice(NEGATIVE_REPLIES), + description="No questions are currently loaded in!", + color=Colours.soft_red + ) spaces = len(sorted(self.questions, key=lambda question: len(question['description']))[-1]["description"]) + 3 formatted_string = "" for question in self.questions: - question_description = question['description'].replace("\u200b", "") - formatted_string += f"`Q{question['number']}: {question_description!r}" \ + formatted_string += f"`Q{question['number']}: {question['description']}" \ f"{' ' * (spaces - len(question['description']) + 2)}" \ f"|` {':x:' if not question.get('visited') else ':checkmark:'}\n" diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 62b619e8..37a29222 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -30,7 +30,15 @@ class TriviaNight(commands.Cog): @commands.group() async def trivianight(self, ctx: commands.Context) -> None: """No-op subcommand group for organizing different commands.""" - return + cog_description = Embed( + title="What is .trivianight?", + description=( + "This 'cog' is for the Python Discord's TriviaNight (date tentative)! Compete against other" + "players in a trivia about Python!" + ), + color=Colours.soft_green + ) + await ctx.send(embed=cog_description) @trivianight.command() async def load(self, ctx: commands.Context) -> None: @@ -88,8 +96,11 @@ class TriviaNight(commands.Cog): @trivianight.command() async def list(self, ctx: commands.Context) -> None: """Displays all the questions from the question bank.""" - formatted_string = self.questions.list_questions() - await ctx.send(formatted_string) + question_list = self.questions.list_questions() + if isinstance(question_list, Embed): + await ctx.send(embed=question_list) + + await ctx.send(question_list) @trivianight.command() async def stop(self, ctx: commands.Context) -> None: -- cgit v1.2.3 From 1848d0ddd318ff3eee0c9c9efed9fef89f460b21 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Thu, 4 Nov 2021 13:17:50 -0400 Subject: kahoot style scoring, time limits, and bug fixes --- bot/exts/events/trivianight/_questions.py | 59 +++++++++++++++++++----------- bot/exts/events/trivianight/_scoreboard.py | 2 +- bot/exts/events/trivianight/trivianight.py | 40 ++++++++++++++------ 3 files changed, 66 insertions(+), 35 deletions(-) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 8b4df74d..aaedf068 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -1,6 +1,6 @@ from random import choice, randrange from time import perf_counter -from typing import TypedDict, Union +from typing import Optional, TypedDict, Union import discord from discord import Embed, Interaction @@ -27,6 +27,8 @@ class CurrentQuestion(TypedDict): description: str answers: list[str] correct: str + points: Optional[int] + time: Optional[int] class QuestionButton(Button): @@ -106,22 +108,16 @@ class QuestionView(View): for button in self.buttons: button._time = current_time - return question_embed + time_limit = self.current_question.get("time", 10) + + return question_embed, time_limit def end_question(self) -> tuple[dict, Embed]: """Returns the dictionaries from the corresponding buttons for those who got it correct.""" labels = ("A", "B", "C", "D") label = labels[self.current_question["answers"].index(self.current_question["correct"])] - return_dict = {name: info for name, info in self.users_picked.items() if info[0] == label} + return_dict = {name: (*info, info[0] == label) for name, info in self.users_picked.items()} all_players = list(self.users_picked.items()) - answers_chosen = { - answer_choice: len( - tuple(filter(lambda x: x[0] == answer_choice, self.users_picked.values())) - ) / len(all_players) - for answer_choice in "ABCD" - } - - answers_chosen = dict(sorted(answers_chosen.items(), key=lambda item: item[1], reverse=True)) answer_embed = Embed( title=f"The correct answer for Question {self.current_question['number']} was..", @@ -129,20 +125,33 @@ class QuestionView(View): color=Colours.soft_green ) - for answer, percent in answers_chosen.items(): - # The `ord` function is used here to change the letter, say 'A' to its corresponding position in the answers - answer_embed.add_field( - name=f"{percent * 100:.1f}% of players chose", - value=self.current_question['answers'][ord(answer) - 65], - inline=False - ) + if len(all_players) != 0: + answers_chosen = { + answer_choice: len( + tuple(filter(lambda x: x[0] == answer_choice, self.users_picked.values())) + ) / len(all_players) + for answer_choice in "ABCD" + } + + answers_chosen = dict(sorted(answers_chosen.items(), key=lambda item: item[1], reverse=True)) + + for answer, percent in answers_chosen.items(): + # The `ord` function is used here to change the letter to its corresponding position + answer_embed.add_field( + name=f"{percent * 100:.1f}% of players chose", + value=self.current_question['answers'][ord(answer) - 65], + inline=False + ) self.users_picked = {} for button in self.buttons: button.users_picked = self.users_picked - return return_dict, answer_embed + time_limit = self.current_question.get("time", 10) + question_points = self.current_question.get("points", 10) + + return return_dict, answer_embed, time_limit, question_points class Questions: @@ -209,8 +218,14 @@ class Questions: def end_question(self) -> Embed: """Terminates answering of the question and displays the correct answer.""" - scores, answer_embed = self.view.end_question() + scores, answer_embed, time_limit, total_points = self.view.end_question() for user, score in scores.items(): - self.scoreboard[UserScore(user)] = {"points": 1, "speed": score[2]} - + # Overhead with calculating scores leads to inflated times, subtracts 0.5 to give an accurate depiction + time_taken = score[2] - 0.5 + point_calculation = (1 - (time_taken / time_limit) / 2) * total_points + if score[-1] is True: + self.scoreboard[UserScore(user)] = {"points": point_calculation, "speed": time_taken} + elif score[-1] is False and score[2] <= 2: + # Get the negative of the point_calculation to deduct it + self.scoreboard[UserScore(user)] = {"points": -point_calculation} return answer_embed diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 1bde59f5..2adb5e37 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -27,7 +27,7 @@ class ScoreboardView(View): user = await self.bot.fetch_user(int(user)) formatted_string += f"**{current_placement + 1}.** {user.mention} " - formatted_string += f"({points} pts)\n" + formatted_string += f"({points:.1f} pts)\n" if (current_placement + 1) % 10 == 0: formatted_string += "⎯⎯⎯⎯⎯⎯⎯⎯\n" diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 37a29222..9973b6b1 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -1,3 +1,4 @@ +import asyncio from json import loads from random import choice @@ -27,18 +28,19 @@ class TriviaNight(commands.Cog): for idx, letter in enumerate(text) ) - @commands.group() + @commands.group(aliases=["tn"], invoke_without_command=True) async def trivianight(self, ctx: commands.Context) -> None: """No-op subcommand group for organizing different commands.""" - cog_description = Embed( - title="What is .trivianight?", - description=( - "This 'cog' is for the Python Discord's TriviaNight (date tentative)! Compete against other" - "players in a trivia about Python!" - ), - color=Colours.soft_green - ) - await ctx.send(embed=cog_description) + if ctx.invoked_subcommand is None: + cog_description = Embed( + title="What is .trivianight?", + description=( + "This 'cog' is for the Python Discord's TriviaNight (date tentative)! Compete against other" + "players in a trivia about Python!" + ), + color=Colours.soft_green + ) + await ctx.send(embed=cog_description) @trivianight.command() async def load(self, ctx: commands.Context) -> None: @@ -79,9 +81,16 @@ class TriviaNight(commands.Cog): await ctx.send(embed=next_question) return - question_embed, question_view = self.questions.current_question() + (question_embed, time_limit), question_view = self.questions.current_question() await ctx.send(embed=question_embed, view=question_view) + for time_remaining in range(time_limit - 1, -1, -1): + await asyncio.sleep(1) + if time_remaining % 5 == 0: + await ctx.send(f"{time_remaining}s remaining") + + await ctx.send(embed=self.questions.end_question()) + @trivianight.command() async def question(self, ctx: commands.Context, question_number: int) -> None: """Gets a question from the question bank depending on the question number provided.""" @@ -90,9 +99,16 @@ class TriviaNight(commands.Cog): await ctx.send(embed=question) return - question_embed, question_view = self.questions.current_question() + (question_embed, time_limit), question_view = self.questions.current_question() await ctx.send(embed=question_embed, view=question_view) + for time_remaining in range(time_limit - 1, -1, -1): + await asyncio.sleep(1) + if time_remaining % 5 == 0: + await ctx.send(f"{time_remaining}s remaining") + + await ctx.send(embed=self.questions.end_question()) + @trivianight.command() async def list(self, ctx: commands.Context) -> None: """Displays all the questions from the question bank.""" -- cgit v1.2.3 From a08e127f1535f63a24e785bfb1c16c445491303d Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Thu, 4 Nov 2021 16:02:37 -0400 Subject: bug fixes --- bot/exts/events/trivianight/_questions.py | 13 +++++- bot/exts/events/trivianight/_scoreboard.py | 6 +-- bot/exts/events/trivianight/trivianight.py | 75 ++++++++++++++++++++++++------ 3 files changed, 76 insertions(+), 18 deletions(-) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index aaedf068..8f2f5571 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -1,3 +1,4 @@ +import logging from random import choice, randrange from time import perf_counter from typing import Optional, TypedDict, Union @@ -10,6 +11,8 @@ from bot.constants import Colours, NEGATIVE_REPLIES from ._scoreboard import Scoreboard +logger = logging.getLogger(__name__) + class UserScore: """Marker class for passing into the scoreboard to add points/record speed.""" @@ -93,7 +96,9 @@ class QuestionView(View): for button in self.buttons: self.add_item(button) - def create_current_question(self) -> Embed: + self.active_question = False + + def create_current_question(self) -> Union[Embed, None]: """Helper function to create the embed for the current question.""" question_embed = Embed( title=f"Question {self.current_question['number']}", @@ -110,6 +115,8 @@ class QuestionView(View): time_limit = self.current_question.get("time", 10) + self.active_question = True + return question_embed, time_limit def end_question(self) -> tuple[dict, Embed]: @@ -151,6 +158,8 @@ class QuestionView(View): time_limit = self.current_question.get("time", 10) question_points = self.current_question.get("points", 10) + self.active_question = False + return return_dict, answer_embed, time_limit, question_points @@ -183,7 +192,7 @@ class Questions: while "visited" in self.questions[question_number].keys(): question_number = randrange(0, len(self.questions)) else: - question_number = number + question_number = number - 1 self.questions[question_number]["visited"] = True self.view.current_question = self.questions[question_number] diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 2adb5e37..076fd406 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -79,7 +79,7 @@ class ScoreboardView(View): name="Total Points", value=( f"You got {points_rank}{'th' if not (suffix := suffixes.get(points_rank[-1])) else suffix} place" - f" with {self.points[member.id]} points." + f" with {self.points[member.id]:.1f} points." ), inline=False ) @@ -108,12 +108,12 @@ class Scoreboard: """Class for the scoreboard for the Trivia Night event.""" def __setitem__(self, key: str, value: int): - if key.user_id not in self.view.points.keys(): + if value.get("points") and key.user_id not in self.view.points.keys(): self.view.points[key.user_id] = value["points"] else: self.view.points[key.user_id] += self.view.points[key.user_id] - if key.user_id not in self.view.speed.keys(): + if value.get("speed") and key.user_id not in self.view.speed.keys(): self.view.speed[key.user_id] = [1, value["speed"]] else: self.view.speed[key.user_id] = [ diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 9973b6b1..46db8c74 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -1,12 +1,13 @@ import asyncio -from json import loads +from json import JSONDecodeError, loads from random import choice +from typing import Optional from discord import Embed from discord.ext import commands from bot.bot import Bot -from bot.constants import Colours, POSITIVE_REPLIES +from bot.constants import Colours, NEGATIVE_REPLIES, POSITIVE_REPLIES from ._questions import QuestionView, Questions from ._scoreboard import Scoreboard, ScoreboardView @@ -43,10 +44,27 @@ class TriviaNight(commands.Cog): await ctx.send(embed=cog_description) @trivianight.command() - async def load(self, ctx: commands.Context) -> None: + async def load(self, ctx: commands.Context, *, to_load: Optional[str]) -> None: """Load the JSON file provided into the questions.""" - json_text = (await ctx.message.attachments[0].read()).decode("utf8") - serialized_json = loads(json_text) + if ctx.message.attachments: + json_text = (await ctx.message.attachments[0].read()).decode("utf8") + elif to_load.startswith("https://discord.com/channels") or \ + to_load.startswith("https://discordapp.com/channels"): + channel_id, message_id = to_load.split("/")[-2:] + channel = await ctx.guild.fetch_channel(int(channel_id)) + message = await channel.fetch_message(int(message_id)) + if message.attachments: + json_text = (await message.attachments[0].read()).decode("utf8") + else: + json_text = message.content.replace("```", "").replace("json", "") + else: + json_text = message.content.replace("```", "").replace("json", "") + + try: + serialized_json = loads(json_text) + except JSONDecodeError: + raise commands.BadArgument("Invalid JSON") + for idx, question in enumerate(serialized_json): serialized_json[idx] = {**question, **{"description": self.unicodeify(question["description"])}} self.questions.view = QuestionView() @@ -64,7 +82,8 @@ class TriviaNight(commands.Cog): """Resets previous questions and scoreboards.""" self.scoreboard.view = ScoreboardView(self.bot) for question in self.questions.questions: - del question["visited"] + if "visited" in question.keys(): + del question["visited"] success_embed = Embed( title=choice(POSITIVE_REPLIES), @@ -76,20 +95,35 @@ class TriviaNight(commands.Cog): @trivianight.command() async def next(self, ctx: commands.Context) -> None: """Gets a random question from the unanswered question list and lets user choose the answer.""" + if self.questions.view.active_question is True: + error_embed = Embed( + title=choice(NEGATIVE_REPLIES), + description="There is already an ongoing question!", + color=Colours.soft_red + ) + await ctx.send(embed=error_embed) + return + next_question = self.questions.next_question() if isinstance(next_question, Embed): await ctx.send(embed=next_question) return (question_embed, time_limit), question_view = self.questions.current_question() - await ctx.send(embed=question_embed, view=question_view) + message = await ctx.send(embed=question_embed, view=question_view) + + for time_remaining in range(time_limit, -1, -1): + if self.questions.view.active_question is False: + await ctx.send(embed=self.questions.end_question()) + await message.edit(embed=question_embed, view=None) + return - for time_remaining in range(time_limit - 1, -1, -1): await asyncio.sleep(1) - if time_remaining % 5 == 0: + if time_remaining % 5 == 0 and time_remaining not in (time_limit, 0): await ctx.send(f"{time_remaining}s remaining") await ctx.send(embed=self.questions.end_question()) + await message.edit(embed=question_embed, view=None) @trivianight.command() async def question(self, ctx: commands.Context, question_number: int) -> None: @@ -100,14 +134,20 @@ class TriviaNight(commands.Cog): return (question_embed, time_limit), question_view = self.questions.current_question() - await ctx.send(embed=question_embed, view=question_view) + message = await ctx.send(embed=question_embed, view=question_view) + + for time_remaining in range(time_limit, -1, -1): + if self.questions.view.active_question is False: + await ctx.send(embed=self.questions.end_question()) + await message.edit(embed=question_embed, view=None) + return - for time_remaining in range(time_limit - 1, -1, -1): await asyncio.sleep(1) - if time_remaining % 5 == 0: + if time_remaining % 5 == 0 and time_remaining not in (time_limit, 0): await ctx.send(f"{time_remaining}s remaining") await ctx.send(embed=self.questions.end_question()) + await message.edit(embed=question_embed, view=None) @trivianight.command() async def list(self, ctx: commands.Context) -> None: @@ -121,7 +161,16 @@ class TriviaNight(commands.Cog): @trivianight.command() async def stop(self, ctx: commands.Context) -> None: """End the ongoing question to show the correct question.""" - await ctx.send(embed=self.questions.end_question()) + if self.questions.view.active_question is False: + error_embed = Embed( + title=choice(NEGATIVE_REPLIES), + description="There is not an ongoing question to stop!", + color=Colours.soft_red + ) + await ctx.send(embed=error_embed) + return + + self.questions.view.active_question = False @trivianight.command() async def end(self, ctx: commands.Context) -> None: -- cgit v1.2.3 From 00dcdee6d5b4fbbb763af6138974337e04421a5d Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Thu, 4 Nov 2021 16:04:05 -0400 Subject: change checkmark to white_check_mark --- bot/exts/events/trivianight/_questions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 8f2f5571..f433baa8 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -217,7 +217,7 @@ class Questions: for question in self.questions: formatted_string += f"`Q{question['number']}: {question['description']}" \ f"{' ' * (spaces - len(question['description']) + 2)}" \ - f"|` {':x:' if not question.get('visited') else ':checkmark:'}\n" + f"|` {':x:' if not question.get('visited') else ':white_check_mark:'}\n" return formatted_string.strip() -- cgit v1.2.3 From 4b999a506d1736d636d298b3316686014f4630fb Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Thu, 4 Nov 2021 16:08:03 -0400 Subject: better docstring for load --- bot/exts/events/trivianight/trivianight.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 46db8c74..96493f2f 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -45,7 +45,22 @@ class TriviaNight(commands.Cog): @trivianight.command() async def load(self, ctx: commands.Context, *, to_load: Optional[str]) -> None: - """Load the JSON file provided into the questions.""" + """ + Loads a JSON file from the provided attachment or argument. + + The JSON provided is formatted where it is a list of dictionaries, each dictionary containing the keys below: + - number: int (represents the current question #) + - description: str (represents the question itself) + - answers: list (represents the different answers possible, must be a length of 4) + - correct: str (represents the correct answer in terms of what the correct answer is in `answers` + - time: Optional[int] (represents the timer for the question and how long it should run, default is 10) + - points: Optional[int] (represents how many points are awarded for each question, default is 10) + + The load command accepts three different ways of loading in a JSON: + - an attachment of the JSON file + - a message link to the attachment/JSON + - reading the JSON itself via a codeblock or plain text + """ if ctx.message.attachments: json_text = (await ctx.message.attachments[0].read()).decode("utf8") elif to_load.startswith("https://discord.com/channels") or \ -- cgit v1.2.3 From ec00a842b958edf0100e197ae29f52a9a33d6a6b Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Thu, 4 Nov 2021 16:13:20 -0400 Subject: fixing rivianight list formatting --- bot/exts/events/trivianight/_questions.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index f433baa8..53f0a4e0 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -212,11 +212,16 @@ class Questions: description="No questions are currently loaded in!", color=Colours.soft_red ) - spaces = len(sorted(self.questions, key=lambda question: len(question['description']))[-1]["description"]) + 3 + spaces = len( + sorted( + self.questions, key=lambda question: len(question['description'].replace("\u200b", "")) + )[-1]["description"].replace("\u200b", "") + ) + 3 formatted_string = "" for question in self.questions: - formatted_string += f"`Q{question['number']}: {question['description']}" \ - f"{' ' * (spaces - len(question['description']) + 2)}" \ + question_description = question["description"].replace("\u200b", "") + formatted_string += f"`Q{question['number']}: {question_description}" \ + f"{' ' * (spaces - len(question_description) + 2)}" \ f"|` {':x:' if not question.get('visited') else ':white_check_mark:'}\n" return formatted_string.strip() -- cgit v1.2.3 From 73200817a80c19f48ab8c5af74e004d6c81841d5 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sat, 6 Nov 2021 11:20:57 -0400 Subject: more bug fixes/sorting --- bot/exts/events/trivianight/_scoreboard.py | 3 +++ bot/exts/events/trivianight/trivianight.py | 4 +--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 076fd406..e9678bb8 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -21,6 +21,9 @@ class ScoreboardView(View): async def create_main_leaderboard(self) -> Embed: """Helper function that iterates through `self.points` to generate the main leaderboard embed.""" formatted_string = "" + self.points = dict(sorted(self.points.items(), key=lambda item: item[-1], reverse=True)) + self.speed = dict(sorted(self.speed.items(), key=lambda item: item[-1])) + for current_placement, (user, points) in enumerate(self.points.items()): if current_placement + 1 > 30: break diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 96493f2f..75d2c671 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -71,9 +71,7 @@ class TriviaNight(commands.Cog): if message.attachments: json_text = (await message.attachments[0].read()).decode("utf8") else: - json_text = message.content.replace("```", "").replace("json", "") - else: - json_text = message.content.replace("```", "").replace("json", "") + json_text = message.content.replace("```", "").replace("json", "").replace("\n", "") try: serialized_json = loads(json_text) -- cgit v1.2.3 From 1e44a974ed1946c424b61c43d3fbc5ef23e37613 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 16 Nov 2021 17:39:09 -0500 Subject: feat: allow a dynamic number of questions, not just being 4 answers --- bot/exts/events/trivianight/_questions.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 53f0a4e0..f94371da 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -92,20 +92,22 @@ class QuestionView(View): super().__init__() self.current_question: CurrentQuestion self.users_picked = {} - self.buttons = [QuestionButton(label, self.users_picked, self) for label in ("A", "B", "C", "D")] - for button in self.buttons: - self.add_item(button) self.active_question = False def create_current_question(self) -> Union[Embed, None]: """Helper function to create the embed for the current question.""" + self.current_labels = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[:len(self.current_question["answers"])] question_embed = Embed( title=f"Question {self.current_question['number']}", description=self.current_question["description"], color=Colours.python_yellow ) - for label, answer in zip("ABCD", self.current_question["answers"]): + self.buttons = [QuestionButton(label, self.users_picked, self) for label in self.current_labels] + for button in self.buttons: + self.add_item(button) + + for label, answer in zip(self.current_labels, self.current_question["answers"]): question_embed.add_field(name=f"Answer {label}", value=answer, inline=False) question_embed.set_footer(text="0 people have answered") @@ -121,7 +123,7 @@ class QuestionView(View): def end_question(self) -> tuple[dict, Embed]: """Returns the dictionaries from the corresponding buttons for those who got it correct.""" - labels = ("A", "B", "C", "D") + labels = self.current_labels label = labels[self.current_question["answers"].index(self.current_question["correct"])] return_dict = {name: (*info, info[0] == label) for name, info in self.users_picked.items()} all_players = list(self.users_picked.items()) @@ -137,7 +139,7 @@ class QuestionView(View): answer_choice: len( tuple(filter(lambda x: x[0] == answer_choice, self.users_picked.values())) ) / len(all_players) - for answer_choice in "ABCD" + for answer_choice in labels } answers_chosen = dict(sorted(answers_chosen.items(), key=lambda item: item[1], reverse=True)) -- cgit v1.2.3 From 8098e98e6b7fa8e214b78a232c5c37d8e24dfe6b Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 16 Nov 2021 18:26:02 -0500 Subject: map percentage of people who got it correct to color --- bot/exts/events/trivianight/_questions.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index f94371da..9f2b20da 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -128,10 +128,14 @@ class QuestionView(View): return_dict = {name: (*info, info[0] == label) for name, info in self.users_picked.items()} all_players = list(self.users_picked.items()) + # Maps the % of people who got it right to a color, from a range of red to green + percentage_to_color = { + range(0, 26): 0xFC94A1, range(26, 51): 0xFFCCCB, range(51, 76): 0xCDFFCC, range(76, 101): 0xB0F5AB + } + answer_embed = Embed( title=f"The correct answer for Question {self.current_question['number']} was..", - description=self.current_question["correct"], - color=Colours.soft_green + description=self.current_question["correct"] ) if len(all_players) != 0: @@ -144,7 +148,12 @@ class QuestionView(View): answers_chosen = dict(sorted(answers_chosen.items(), key=lambda item: item[1], reverse=True)) - for answer, percent in answers_chosen.items(): + for idx, (answer, percent) in enumerate(answers_chosen.items()): + # Setting the color of answer_embed to the % of people that got it correct via the mapping + if idx == 0: + all_ranges = [range(0, 26), range(26, 51), range(51, 76), range(76, 101)] + answer_embed.color = percentage_to_color[all_ranges[round(percent * 100) // 25 - 1]] + # The `ord` function is used here to change the letter to its corresponding position answer_embed.add_field( name=f"{percent * 100:.1f}% of players chose", -- cgit v1.2.3 From c0917e9de25321f1d533568a0abe2bef8ee8c91b Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 16 Nov 2021 18:39:14 -0500 Subject: fixing rivianight reset bugs buttons would be repeated due to a faulty reset --- bot/exts/events/trivianight/_scoreboard.py | 4 ++-- bot/exts/events/trivianight/trivianight.py | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index e9678bb8..08025214 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -113,12 +113,12 @@ class Scoreboard: def __setitem__(self, key: str, value: int): if value.get("points") and key.user_id not in self.view.points.keys(): self.view.points[key.user_id] = value["points"] - else: + elif value.get("points"): self.view.points[key.user_id] += self.view.points[key.user_id] if value.get("speed") and key.user_id not in self.view.speed.keys(): self.view.speed[key.user_id] = [1, value["speed"]] - else: + elif value.get("speed"): self.view.speed[key.user_id] = [ self.view.speed[key.user_id][0] + 1, self.view.speed[key.user_id][1] + value["speed"] ] diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 75d2c671..224b0620 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -94,13 +94,18 @@ class TriviaNight(commands.Cog): async def reset(self, ctx: commands.Context) -> None: """Resets previous questions and scoreboards.""" self.scoreboard.view = ScoreboardView(self.bot) - for question in self.questions.questions: + all_questions = self.questions.questions + self.questions.view = QuestionView() + + for question in all_questions: if "visited" in question.keys(): del question["visited"] + self.questions.questions = list(all_questions) + success_embed = Embed( title=choice(POSITIVE_REPLIES), - description="The scoreboards were reset and questions marked unvisited!", + description="The scoreboards were reset and questions reset!", color=Colours.soft_green ) await ctx.send(embed=success_embed) -- cgit v1.2.3 From 07ac7f87610d513342e9393c29ea49c67cb76215 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 17 Nov 2021 17:47:09 -0500 Subject: bluenix review changes --- bot/exts/events/trivianight/__init__.py | 7 ++++++ bot/exts/events/trivianight/_questions.py | 38 +++++++++--------------------- bot/exts/events/trivianight/_scoreboard.py | 7 +++--- bot/exts/events/trivianight/trivianight.py | 7 +++--- 4 files changed, 26 insertions(+), 33 deletions(-) diff --git a/bot/exts/events/trivianight/__init__.py b/bot/exts/events/trivianight/__init__.py index e69de29b..87de18e0 100644 --- a/bot/exts/events/trivianight/__init__.py +++ b/bot/exts/events/trivianight/__init__.py @@ -0,0 +1,7 @@ +class UserScore: + """Marker class for passing into the scoreboard to add points/record speed.""" + + __slots__ = ("user_id",) + + def __init__(self, user_id: int): + self.user_id = user_id diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 9f2b20da..2bbff1d7 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -1,4 +1,3 @@ -import logging from random import choice, randrange from time import perf_counter from typing import Optional, TypedDict, Union @@ -9,19 +8,9 @@ from discord.ui import Button, View from bot.constants import Colours, NEGATIVE_REPLIES +from . import UserScore from ._scoreboard import Scoreboard -logger = logging.getLogger(__name__) - - -class UserScore: - """Marker class for passing into the scoreboard to add points/record speed.""" - - __slots__ = ("user_id",) - - def __init__(self, user_id: int): - self.user_id = user_id - class CurrentQuestion(TypedDict): """Representing the different 'keys' of the question taken from the JSON.""" @@ -100,7 +89,7 @@ class QuestionView(View): self.current_labels = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[:len(self.current_question["answers"])] question_embed = Embed( title=f"Question {self.current_question['number']}", - description=self.current_question["description"], + description=self.current_question["obfuscated_description"], color=Colours.python_yellow ) self.buttons = [QuestionButton(label, self.users_picked, self) for label in self.current_labels] @@ -128,11 +117,6 @@ class QuestionView(View): return_dict = {name: (*info, info[0] == label) for name, info in self.users_picked.items()} all_players = list(self.users_picked.items()) - # Maps the % of people who got it right to a color, from a range of red to green - percentage_to_color = { - range(0, 26): 0xFC94A1, range(26, 51): 0xFFCCCB, range(51, 76): 0xCDFFCC, range(76, 101): 0xB0F5AB - } - answer_embed = Embed( title=f"The correct answer for Question {self.current_question['number']} was..", description=self.current_question["correct"] @@ -146,13 +130,12 @@ class QuestionView(View): for answer_choice in labels } - answers_chosen = dict(sorted(answers_chosen.items(), key=lambda item: item[1], reverse=True)) - for idx, (answer, percent) in enumerate(answers_chosen.items()): # Setting the color of answer_embed to the % of people that got it correct via the mapping if idx == 0: - all_ranges = [range(0, 26), range(26, 51), range(51, 76), range(76, 101)] - answer_embed.color = percentage_to_color[all_ranges[round(percent * 100) // 25 - 1]] + # Maps the % of people who got it right to a color, from a range of red to green + percentage_to_color = [0xFC94A1, 0xFFCCCB, 0xCDFFCC, 0xB0F5AB, 0xB0F5AB] + answer_embed.color = percentage_to_color[round(percent * 100) // 25] # The `ord` function is used here to change the letter to its corresponding position answer_embed.add_field( @@ -206,6 +189,8 @@ class Questions: question_number = number - 1 self.questions[question_number]["visited"] = True + + # The `self.view` refers to the QuestionView self.view.current_question = self.questions[question_number] def list_questions(self) -> Union[Embed, str]: @@ -225,12 +210,12 @@ class Questions: ) spaces = len( sorted( - self.questions, key=lambda question: len(question['description'].replace("\u200b", "")) - )[-1]["description"].replace("\u200b", "") + self.questions, key=lambda question: len(question['description']) + )[-1]["description"] ) + 3 formatted_string = "" for question in self.questions: - question_description = question["description"].replace("\u200b", "") + question_description = question["description"] formatted_string += f"`Q{question['number']}: {question_description}" \ f"{' ' * (spaces - len(question_description) + 2)}" \ f"|` {':x:' if not question.get('visited') else ':white_check_mark:'}\n" @@ -245,8 +230,7 @@ class Questions: """Terminates answering of the question and displays the correct answer.""" scores, answer_embed, time_limit, total_points = self.view.end_question() for user, score in scores.items(): - # Overhead with calculating scores leads to inflated times, subtracts 0.5 to give an accurate depiction - time_taken = score[2] - 0.5 + time_taken = score[2] point_calculation = (1 - (time_taken / time_limit) / 2) * total_points if score[-1] is True: self.scoreboard[UserScore(user)] = {"points": point_calculation, "speed": time_taken} diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 08025214..635660a2 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -1,5 +1,4 @@ from random import choice -from typing import Union import discord.ui from discord import ButtonStyle, Embed, Interaction, Member @@ -8,6 +7,8 @@ from discord.ui import Button, View from bot.bot import Bot from bot.constants import Colours, NEGATIVE_REPLIES +from . import UserScore + class ScoreboardView(View): """View for the scoreboard.""" @@ -110,7 +111,7 @@ class ScoreboardView(View): class Scoreboard: """Class for the scoreboard for the Trivia Night event.""" - def __setitem__(self, key: str, value: int): + def __setitem__(self, key: UserScore, value: dict): if value.get("points") and key.user_id not in self.view.points.keys(): self.view.points[key.user_id] = value["points"] elif value.get("points"): @@ -123,6 +124,6 @@ class Scoreboard: self.view.speed[key.user_id][0] + 1, self.view.speed[key.user_id][1] + value["speed"] ] - async def display(self) -> Union[Embed, View]: + async def display(self) -> tuple[Embed, View]: """Returns the embed of the main leaderboard along with the ScoreboardView.""" return await self.view.create_main_leaderboard(), self.view diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 224b0620..615a9dd3 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -51,7 +51,7 @@ class TriviaNight(commands.Cog): The JSON provided is formatted where it is a list of dictionaries, each dictionary containing the keys below: - number: int (represents the current question #) - description: str (represents the question itself) - - answers: list (represents the different answers possible, must be a length of 4) + - answers: list[str] (represents the different answers possible, must be a length of 4) - correct: str (represents the correct answer in terms of what the correct answer is in `answers` - time: Optional[int] (represents the timer for the question and how long it should run, default is 10) - points: Optional[int] (represents how many points are awarded for each question, default is 10) @@ -79,7 +79,8 @@ class TriviaNight(commands.Cog): raise commands.BadArgument("Invalid JSON") for idx, question in enumerate(serialized_json): - serialized_json[idx] = {**question, **{"description": self.unicodeify(question["description"])}} + serialized_json[idx]["obfuscated_description"] = self.unicodeify(question["description"]) + self.questions.view = QuestionView() self.scoreboard.view = ScoreboardView(self.bot) self.questions.set_questions(serialized_json) @@ -101,7 +102,7 @@ class TriviaNight(commands.Cog): if "visited" in question.keys(): del question["visited"] - self.questions.questions = list(all_questions) + self.questions.set_questions(list(all_questions)) success_embed = Embed( title=choice(POSITIVE_REPLIES), -- cgit v1.2.3 From 450da462d2b9ee3c74fb8f79a9f1bc523fecf8bb Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 17 Nov 2021 17:55:21 -0500 Subject: add command role checks to moderation roles + trivia night roles --- bot/exts/events/trivianight/trivianight.py | 33 ++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 615a9dd3..d00a70aa 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -7,11 +7,14 @@ from discord import Embed from discord.ext import commands from bot.bot import Bot -from bot.constants import Colours, NEGATIVE_REPLIES, POSITIVE_REPLIES +from bot.constants import Colours, MODERATION_ROLES, NEGATIVE_REPLIES, POSITIVE_REPLIES from ._questions import QuestionView, Questions from ._scoreboard import Scoreboard, ScoreboardView +# The ID you see below is the Events Lead role ID +TRIVIA_NIGHT_ROLES = MODERATION_ROLES + (778361735739998228,) + class TriviaNight(commands.Cog): """Cog for the Python Trivia Night event.""" @@ -32,18 +35,18 @@ class TriviaNight(commands.Cog): @commands.group(aliases=["tn"], invoke_without_command=True) async def trivianight(self, ctx: commands.Context) -> None: """No-op subcommand group for organizing different commands.""" - if ctx.invoked_subcommand is None: - cog_description = Embed( - title="What is .trivianight?", - description=( - "This 'cog' is for the Python Discord's TriviaNight (date tentative)! Compete against other" - "players in a trivia about Python!" - ), - color=Colours.soft_green - ) - await ctx.send(embed=cog_description) + cog_description = Embed( + title="What is .trivianight?", + description=( + "This 'cog' is for the Python Discord's TriviaNight (date tentative)! Compete against other" + "players in a trivia about Python!" + ), + color=Colours.soft_green + ) + await ctx.send(embed=cog_description) @trivianight.command() + @commands.has_any_role(*TRIVIA_NIGHT_ROLES) async def load(self, ctx: commands.Context, *, to_load: Optional[str]) -> None: """ Loads a JSON file from the provided attachment or argument. @@ -63,6 +66,8 @@ class TriviaNight(commands.Cog): """ if ctx.message.attachments: json_text = (await ctx.message.attachments[0].read()).decode("utf8") + elif not to_load: + raise commands.BadArgument("You didn't attach an attachment nor link a message!") elif to_load.startswith("https://discord.com/channels") or \ to_load.startswith("https://discordapp.com/channels"): channel_id, message_id = to_load.split("/")[-2:] @@ -92,6 +97,7 @@ class TriviaNight(commands.Cog): await ctx.send(embed=success_embed) @trivianight.command() + @commands.has_any_role(*TRIVIA_NIGHT_ROLES) async def reset(self, ctx: commands.Context) -> None: """Resets previous questions and scoreboards.""" self.scoreboard.view = ScoreboardView(self.bot) @@ -112,6 +118,7 @@ class TriviaNight(commands.Cog): await ctx.send(embed=success_embed) @trivianight.command() + @commands.has_any_role(*TRIVIA_NIGHT_ROLES) async def next(self, ctx: commands.Context) -> None: """Gets a random question from the unanswered question list and lets user choose the answer.""" if self.questions.view.active_question is True: @@ -145,6 +152,7 @@ class TriviaNight(commands.Cog): await message.edit(embed=question_embed, view=None) @trivianight.command() + @commands.has_any_role(*TRIVIA_NIGHT_ROLES) async def question(self, ctx: commands.Context, question_number: int) -> None: """Gets a question from the question bank depending on the question number provided.""" question = self.questions.next_question(question_number) @@ -169,6 +177,7 @@ class TriviaNight(commands.Cog): await message.edit(embed=question_embed, view=None) @trivianight.command() + @commands.has_any_role(*TRIVIA_NIGHT_ROLES) async def list(self, ctx: commands.Context) -> None: """Displays all the questions from the question bank.""" question_list = self.questions.list_questions() @@ -178,6 +187,7 @@ class TriviaNight(commands.Cog): await ctx.send(question_list) @trivianight.command() + @commands.has_any_role(*TRIVIA_NIGHT_ROLES) async def stop(self, ctx: commands.Context) -> None: """End the ongoing question to show the correct question.""" if self.questions.view.active_question is False: @@ -192,6 +202,7 @@ class TriviaNight(commands.Cog): self.questions.view.active_question = False @trivianight.command() + @commands.has_any_role(*TRIVIA_NIGHT_ROLES) async def end(self, ctx: commands.Context) -> None: """Ends the trivia night event and displays the scoreboard.""" scoreboard_embed, scoreboard_view = await self.scoreboard.display() -- cgit v1.2.3 From 9b8bd12fa3b56b76fa6f8c759597e0f845d641c3 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 17 Nov 2021 18:00:37 -0500 Subject: setup function --- bot/exts/events/trivianight/trivianight.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index d00a70aa..bee55670 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -24,6 +24,17 @@ class TriviaNight(commands.Cog): self.scoreboard = Scoreboard() self.questions = Questions(self.scoreboard) + def setup_views(self, questions: dict) -> None: + """ + Sets up the views for `self.questions` and `self.scoreboard` respectively. + + Parameters: + - questions: The dictionary to set the questions for self.questions to use. + """ + self.questions.view = QuestionView() + self.scoreboard.view = ScoreboardView(self.bot) + self.questions.set_questions(questions) + @staticmethod def unicodeify(text: str) -> str: """Takes `text` and adds zero-width spaces to prevent copy and pasting the question.""" @@ -86,9 +97,8 @@ class TriviaNight(commands.Cog): for idx, question in enumerate(serialized_json): serialized_json[idx]["obfuscated_description"] = self.unicodeify(question["description"]) - self.questions.view = QuestionView() - self.scoreboard.view = ScoreboardView(self.bot) - self.questions.set_questions(serialized_json) + self.setup_views(serialized_json) + success_embed = Embed( title=choice(POSITIVE_REPLIES), description="The JSON was loaded successfully!", @@ -100,15 +110,13 @@ class TriviaNight(commands.Cog): @commands.has_any_role(*TRIVIA_NIGHT_ROLES) async def reset(self, ctx: commands.Context) -> None: """Resets previous questions and scoreboards.""" - self.scoreboard.view = ScoreboardView(self.bot) all_questions = self.questions.questions - self.questions.view = QuestionView() for question in all_questions: if "visited" in question.keys(): del question["visited"] - self.questions.set_questions(list(all_questions)) + self.setup_views(list(all_questions)) success_embed = Embed( title=choice(POSITIVE_REPLIES), -- cgit v1.2.3 From afe61522fdb8c6592b251476aef0d6823676c176 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 17 Nov 2021 18:03:35 -0500 Subject: locking trivia night roles to admins and event lead only --- bot/exts/events/trivianight/trivianight.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index bee55670..ed2bfdbe 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -7,13 +7,13 @@ from discord import Embed from discord.ext import commands from bot.bot import Bot -from bot.constants import Colours, MODERATION_ROLES, NEGATIVE_REPLIES, POSITIVE_REPLIES +from bot.constants import Colours, NEGATIVE_REPLIES, POSITIVE_REPLIES, Roles from ._questions import QuestionView, Questions from ._scoreboard import Scoreboard, ScoreboardView # The ID you see below is the Events Lead role ID -TRIVIA_NIGHT_ROLES = MODERATION_ROLES + (778361735739998228,) +TRIVIA_NIGHT_ROLES = (Roles.admin, 78361735739998228) class TriviaNight(commands.Cog): -- cgit v1.2.3 From 49c9cc470e0fac075140f8f7938a5fb140b7ff0c Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 28 Nov 2021 22:44:34 -0500 Subject: making the docstrings nicer --- bot/exts/events/trivianight/_questions.py | 48 ++++++++++++++++++++++---- bot/exts/events/trivianight/_scoreboard.py | 41 +++++++++++++++++++--- bot/exts/events/trivianight/trivianight.py | 55 ++++++++++++++++++++++++++---- 3 files changed, 125 insertions(+), 19 deletions(-) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 2bbff1d7..eaabed4f 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -32,7 +32,13 @@ class QuestionButton(Button): super().__init__(label=label, style=discord.ButtonStyle.green) async def callback(self, interaction: Interaction) -> None: - """When a user interacts with the button, this will be called.""" + """ + When a user interacts with the button, this will be called. + + Parameters: + - interaction: an instance of discord.Interaction representing the interaction between the user and the + button. + """ original_message = interaction.message original_embed = original_message.embeds[0] @@ -84,8 +90,13 @@ class QuestionView(View): self.active_question = False - def create_current_question(self) -> Union[Embed, None]: - """Helper function to create the embed for the current question.""" + def create_current_question(self) -> tuple[Embed, int]: + """ + Helper function to create the embed for the current question. + + Returns an embed containing the question along with each answer choice in the form of a view, + along with the integer representing the time limit of the current question. + """ self.current_labels = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[:len(self.current_question["answers"])] question_embed = Embed( title=f"Question {self.current_question['number']}", @@ -110,8 +121,16 @@ class QuestionView(View): return question_embed, time_limit - def end_question(self) -> tuple[dict, Embed]: - """Returns the dictionaries from the corresponding buttons for those who got it correct.""" + def end_question(self) -> tuple[dict, Embed, int, int]: + """ + Ends the question and displays the statistics on who got the question correct, awards points, etc. + + Returns: + - a dictionary containing all the people who answered and whether or not they got it correct + - an embed displaying the correct answers and the % of people that chose each answer. + - an integer showing the time limit of the current question in seconds + - an integer showing the amount of points the question will award* to those that got it correct + """ labels = self.current_labels label = labels[self.current_question["answers"].index(self.current_question["correct"])] return_dict = {name: (*info, info[0] == label) for name, info in self.users_picked.items()} @@ -165,7 +184,13 @@ class Questions: self.questions = [] def set_questions(self, questions: list) -> None: - """Setting `self.questions` dynamically via a function to set it.""" + """ + Setting `self.questions` dynamically via a function to set it. + + Parameters: + - questions: a list representing all the questions, which is essentially the JSON provided + to load the questions + """ self.questions = questions def next_question(self, number: int = None) -> Union[Embed, None]: @@ -173,6 +198,9 @@ class Questions: Chooses a random unvisited question from the question bank. If the number parameter is specified, it'll head to that specific question. + + Parameters: + - number: An optional integer representing the question number (only used for `.trivianight question` calls) """ if all("visited" in question.keys() for question in self.questions) and number is None: return Embed( @@ -227,7 +255,13 @@ class Questions: return self.view.create_current_question(), self.view def end_question(self) -> Embed: - """Terminates answering of the question and displays the correct answer.""" + """ + Terminates answering of the question and displays the correct answer. + + The function returns an embed containing the information about the question such as the following: + - % of people that chose each option + - the correct answer + """ scores, answer_embed, time_limit, total_points = self.view.end_question() for user, score in scores.items(): time_taken = score[2] diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 635660a2..40f93475 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -20,7 +20,13 @@ class ScoreboardView(View): self.speed = {} async def create_main_leaderboard(self) -> Embed: - """Helper function that iterates through `self.points` to generate the main leaderboard embed.""" + """ + Helper function that iterates through `self.points` to generate the main leaderboard embed. + + The main leaderboard would be formatted like the following: + **1**. @mention of the user (# of points) + along with the 29 other users who made it onto the leaderboard. + """ formatted_string = "" self.points = dict(sorted(self.points.items(), key=lambda item: item[-1], reverse=True)) self.speed = dict(sorted(self.speed.items(), key=lambda item: item[-1])) @@ -44,7 +50,13 @@ class ScoreboardView(View): return main_embed async def _create_speed_embed(self) -> Embed: - """Helper function that iterates through `self.speed` to generate a leaderboard embed.""" + """ + Helper function that iterates through `self.speed` to generate a leaderboard embed. + + The speed leaderboard would be formatted like the following: + **1**. @mention of the user ([average speed as a float with the precision of one decimal point]s) + along with the 29 other users who made it onto the leaderboard. + """ formatted_string = "" for current_placement, (user, time_taken) in enumerate(self.speed.items()): @@ -65,7 +77,12 @@ class ScoreboardView(View): return speed_embed def _get_rank(self, member: Member) -> Embed: - """Gets the member's rank for the points leaderboard and speed leaderboard.""" + """ + Gets the member's rank for the points leaderboard and speed leaderboard. + + Parameters: + - member: An instance of discord.Member representing the person who is trying to get their rank. + """ rank_embed = Embed(title=f"Ranks for {member.display_name}", color=Colours.python_blue) # These are stored as strings so that the last digit can be determined to choose the suffix try: @@ -99,12 +116,26 @@ class ScoreboardView(View): @discord.ui.button(label="Scoreboard for Speed", style=ButtonStyle.green) async def speed_leaderboard(self, button: Button, interaction: Interaction) -> None: - """Send an ephemeral message with the speed leaderboard embed.""" + """ + Send an ephemeral message with the speed leaderboard embed. + + Parameters: + - button: The discord.ui.Button instance representing the `Speed Leaderboard` button. + - interaction: The discord.Interaction instance containing information on the interaction between the user + and the button. + """ await interaction.response.send_message(embed=await self._create_speed_embed(), ephemeral=True) @discord.ui.button(label="What's my rank?", style=ButtonStyle.blurple) async def rank_button(self, button: Button, interaction: Interaction) -> None: - """Send an ephemeral message with the user's rank for the overall points/average speed.""" + """ + Send an ephemeral message with the user's rank for the overall points/average speed. + + Parameters: + - button: The discord.ui.Button instance representing the `What's my rank?` button. + - interaction: The discord.Interaction instance containing information on the interaction between the user + and the button. + """ await interaction.response.send_message(embed=self._get_rank(interaction.user), ephemeral=True) diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index ed2bfdbe..a86bd73f 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -37,7 +37,12 @@ class TriviaNight(commands.Cog): @staticmethod def unicodeify(text: str) -> str: - """Takes `text` and adds zero-width spaces to prevent copy and pasting the question.""" + """ + Takes `text` and adds zero-width spaces to prevent copy and pasting the question. + + Parameters: + - text: A string that represents the question description to 'unicodeify' + """ return "".join( f"{letter}\u200b" if letter not in ('\n', '\t', '`', 'p', 'y') else letter for idx, letter in enumerate(text) @@ -45,7 +50,11 @@ class TriviaNight(commands.Cog): @commands.group(aliases=["tn"], invoke_without_command=True) async def trivianight(self, ctx: commands.Context) -> None: - """No-op subcommand group for organizing different commands.""" + """ + The command group for the Python Discord Trivia Night. + + If invoked without a subcommand (i.e. simply .trivianight), it will explain what the Trivia Night event is. + """ cog_description = Embed( title="What is .trivianight?", description=( @@ -88,6 +97,8 @@ class TriviaNight(commands.Cog): json_text = (await message.attachments[0].read()).decode("utf8") else: json_text = message.content.replace("```", "").replace("json", "").replace("\n", "") + else: + json_text = to_load.replace("```", "").replace("json", "").replace("\n", "") try: serialized_json = loads(json_text) @@ -128,7 +139,12 @@ class TriviaNight(commands.Cog): @trivianight.command() @commands.has_any_role(*TRIVIA_NIGHT_ROLES) async def next(self, ctx: commands.Context) -> None: - """Gets a random question from the unanswered question list and lets user choose the answer.""" + """ + Gets a random question from the unanswered question list and lets the user(s) choose the answer. + + This command will continuously count down until the time limit of the question is exhausted. + However, if `.trivianight stop` is invoked, the counting down is interrupted to show the final results. + """ if self.questions.view.active_question is True: error_embed = Embed( title=choice(NEGATIVE_REPLIES), @@ -162,7 +178,15 @@ class TriviaNight(commands.Cog): @trivianight.command() @commands.has_any_role(*TRIVIA_NIGHT_ROLES) async def question(self, ctx: commands.Context, question_number: int) -> None: - """Gets a question from the question bank depending on the question number provided.""" + """ + Gets a question from the question bank depending on the question number provided. + + The logic of this command is similar to `.trivianight next`, with the only difference being that you need to + specify the question number. + + Parameters: + - question_number: An integer represents the question number to go to (i.e. .trivianight question 5). + """ question = self.questions.next_question(question_number) if isinstance(question, Embed): await ctx.send(embed=question) @@ -187,7 +211,12 @@ class TriviaNight(commands.Cog): @trivianight.command() @commands.has_any_role(*TRIVIA_NIGHT_ROLES) async def list(self, ctx: commands.Context) -> None: - """Displays all the questions from the question bank.""" + """ + Displays all the questions from the question bank. + + Questions are displayed in the following format: + Q(number): Question description | :white_check_mark: if the question was used otherwise :x:. + """ question_list = self.questions.list_questions() if isinstance(question_list, Embed): await ctx.send(embed=question_list) @@ -197,7 +226,11 @@ class TriviaNight(commands.Cog): @trivianight.command() @commands.has_any_role(*TRIVIA_NIGHT_ROLES) async def stop(self, ctx: commands.Context) -> None: - """End the ongoing question to show the correct question.""" + """ + End the ongoing question to show the correct question. + + This command should be used if the question should be ended early or if the time limit fails + """ if self.questions.view.active_question is False: error_embed = Embed( title=choice(NEGATIVE_REPLIES), @@ -212,7 +245,15 @@ class TriviaNight(commands.Cog): @trivianight.command() @commands.has_any_role(*TRIVIA_NIGHT_ROLES) async def end(self, ctx: commands.Context) -> None: - """Ends the trivia night event and displays the scoreboard.""" + """ + Ends the trivia night event and displays the scoreboard view. + + The scoreboard view consists of the two scoreboards with the 30 players who got the highest points and the + 30 players who had the fastest average response time to a question where they got the question right. + + The scoreboard view also has a button where the user can see their own rank, points and average speed if they + didn't make it onto the leaderboard. + """ scoreboard_embed, scoreboard_view = await self.scoreboard.display() await ctx.send(embed=scoreboard_embed, view=scoreboard_view) -- cgit v1.2.3 From 52f1b8cbcc41950f9845d4ef1026253691b921c7 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 28 Nov 2021 22:46:29 -0500 Subject: add logic to make sure that the event can't be ended during a question --- bot/exts/events/trivianight/trivianight.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index a86bd73f..1fffe9fe 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -254,6 +254,15 @@ class TriviaNight(commands.Cog): The scoreboard view also has a button where the user can see their own rank, points and average speed if they didn't make it onto the leaderboard. """ + if self.questions.view.active_question is True: + error_embed = Embed( + title=choice(NEGATIVE_REPLIES), + description="You can't end the event while a question is ongoing!", + color=Colours.soft_red + ) + await ctx.send(embed=error_embed) + return + scoreboard_embed, scoreboard_view = await self.scoreboard.display() await ctx.send(embed=scoreboard_embed, view=scoreboard_view) -- cgit v1.2.3 From 5d84b7947dbaee89e91ed4e8743b7bee3ec9b677 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 29 Nov 2021 18:36:59 -0500 Subject: forgot eturn after error message --- bot/exts/events/trivianight/trivianight.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 1fffe9fe..981b6937 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -220,6 +220,7 @@ class TriviaNight(commands.Cog): question_list = self.questions.list_questions() if isinstance(question_list, Embed): await ctx.send(embed=question_list) + return await ctx.send(question_list) -- cgit v1.2.3 From f821d802357193f82723233f5dd1d55d51ec8ea6 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 29 Nov 2021 19:40:28 -0500 Subject: prevent bugs with question regarding multiple choices --- bot/exts/events/trivianight/_questions.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index eaabed4f..0ab657d2 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -103,9 +103,10 @@ class QuestionView(View): description=self.current_question["obfuscated_description"], color=Colours.python_yellow ) - self.buttons = [QuestionButton(label, self.users_picked, self) for label in self.current_labels] - for button in self.buttons: - self.add_item(button) + if "_previously_visited" not in self.current_question.keys(): + self.buttons = [QuestionButton(label, self.users_picked, self) for label in self.current_labels] + for button in self.buttons: + self.add_item(button) for label, answer in zip(self.current_labels, self.current_question["answers"]): question_embed.add_field(name=f"Answer {label}", value=answer, inline=False) @@ -216,6 +217,8 @@ class Questions: else: question_number = number - 1 + if "visited" in self.questions[question_number].keys(): + self.questions[question_number]["_previously_visited"] = True self.questions[question_number]["visited"] = True # The `self.view` refers to the QuestionView -- cgit v1.2.3 From 176590740ed21f8faa26348ab710ee722fca6926 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 29 Nov 2021 19:41:33 -0500 Subject: typo in trivianight cog explanation --- bot/exts/events/trivianight/trivianight.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 981b6937..59b809b1 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -59,7 +59,7 @@ class TriviaNight(commands.Cog): title="What is .trivianight?", description=( "This 'cog' is for the Python Discord's TriviaNight (date tentative)! Compete against other" - "players in a trivia about Python!" + " players in a trivia about Python!" ), color=Colours.soft_green ) -- cgit v1.2.3 From 2a7d6942646f9cd0c71d3f1e5812dd1e49043be8 Mon Sep 17 00:00:00 2001 From: Bluenix Date: Sun, 12 Dec 2021 18:00:15 +0100 Subject: Add Question representation for trivia night data --- bot/exts/events/trivianight/_game.py | 105 +++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 bot/exts/events/trivianight/_game.py diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py new file mode 100644 index 00000000..086d0de6 --- /dev/null +++ b/bot/exts/events/trivianight/_game.py @@ -0,0 +1,105 @@ +import time +from string import ascii_uppercase +from typing import Iterable, Optional, TypedDict + + +DEFAULT_QUESTION_POINTS = 10 +DEFAULT_QUESTION_TIME = 10 + + +class QuestionData(TypedDict): + """Representing the different 'keys' of the question taken from the JSON.""" + + number: str + description: str + answers: list[str] + correct: str + points: Optional[int] + time: Optional[int] + + +UserGuess = tuple[ + str, # The answer that was guessed + bool, # Whether the answer can be changed again + float # The time it took to guess +] + + +class Question: + """Interface for one question in a trivia night game.""" + + def __init__(self, data: QuestionData): + self._data = data + self._guesses: dict[int, UserGuess] = {} + self._started = None + + # These properties are mostly proxies to the underlying data: + + @property + def number(self) -> str: + """The number of the question.""" + return self._data["number"] + + @property + def description(self) -> str: + """The description of the question.""" + return self._data["description"] + + @property + def answers(self) -> list[tuple[str, str]]: + """The possible answers for this answer. + + This is a property that returns a list of letter, answer pairs. + """ + return [(ascii_uppercase[i], q) for (i, q) in enumerate(self._data["answers"])] + + @property + def correct(self) -> str: + """The correct answer for this question.""" + return self._data["correct"] + + @property + def max_points(self) -> int: + """The maximum points that can be awarded for this question.""" + return self._data.get("points") or DEFAULT_QUESTION_POINTS + + @property + def time(self) -> float: + """The time allowed to answer the question.""" + return self._data.get("time") or DEFAULT_QUESTION_TIME + + def start(self) -> float: + """Start the question and return the time it started.""" + self._started = time.perf_counter() + return self._started + + def _update_guess(self, user: int, answer: str) -> UserGuess: + """Update an already existing guess.""" + if self._started is None: + raise RuntimeError("Question is not open for answers.") + + if self._guesses[user][1] is False: + raise RuntimeError(f"User({user}) has already updated their guess once.") + + self._guesses[user] = (answer, False, time.perf_counter() - self._started) + return self._guesses[user] + + def guess(self, user: int, answer: str) -> UserGuess: + """Add a guess made by a user to the current question.""" + if user in self._guesses: + return self._update_guess(user, answer) + + if self._started is None: + raise RuntimeError("Question is not open for answers.") + + self._guesses[user] = (answer, True, time.perf_counter() - self._started) + return self._guesses[user] + + def stop(self) -> dict[int, UserGuess]: + """Stop the question and return the guesses that were made.""" + guesses = self._guesses + + self._started = None + self._guesses = {} + + return guesses -- cgit v1.2.3 From 33269228576ffb7d48187fd9cd0297d8b0b657e3 Mon Sep 17 00:00:00 2001 From: Bluenix Date: Sun, 12 Dec 2021 22:30:44 +0100 Subject: Restructure trivia night game around new Question representation --- bot/exts/events/trivianight/_game.py | 60 ++++++- bot/exts/events/trivianight/_questions.py | 253 ++++++++--------------------- bot/exts/events/trivianight/trivianight.py | 161 +++++++----------- 3 files changed, 179 insertions(+), 295 deletions(-) diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index 086d0de6..aac745a7 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -1,4 +1,5 @@ import time +from random import randrange from string import ascii_uppercase from typing import Iterable, Optional, TypedDict @@ -25,6 +26,14 @@ UserGuess = tuple[ ] +class QuestionClosed(RuntimeError): + """Exception raised when the question is not open for guesses anymore.""" + + +class AlreadyUpdated(RuntimeError): + """Exception raised when the user has already updated their guess once.""" + + class Question: """Interface for one question in a trivia night game.""" @@ -76,10 +85,10 @@ class Question: def _update_guess(self, user: int, answer: str) -> UserGuess: """Update an already existing guess.""" if self._started is None: - raise RuntimeError("Question is not open for answers.") + raise QuestionClosed("Question is not open for answers.") if self._guesses[user][1] is False: - raise RuntimeError(f"User({user}) has already updated their guess once.") + raise AlreadyUpdated(f"User({user}) has already updated their guess once.") self._guesses[user] = (answer, False, time.perf_counter() - self._started) return self._guesses[user] @@ -90,7 +99,7 @@ class Question: return self._update_guess(user, answer) if self._started is None: - raise RuntimeError("Question is not open for answers.") + raise QuestionClosed("Question is not open for answers.") self._guesses[user] = (answer, True, time.perf_counter() - self._started) return self._guesses[user] @@ -103,3 +112,48 @@ class Question: self._guesses = {} return guesses + + +class TriviaNightGame: + """Interface for managing a game of trivia night.""" + + def __init__(self, data: list[QuestionData]) -> None: + self._questions = [Question(q) for q in data] + self.current_question: Optional[Question] = None + + def __iter__(self) -> Iterable[Question]: + return iter(self._questions) + + def next_question(self, number: str = None) -> Question: + """ + Consume one random question from the trivia night game. + + One question is randomly picked from the list of questions which is then removed and returned. + """ + if self.current_question is not None: + raise RuntimeError("Cannot call next_question() when there is a current question.") + + if number is not None: + try: + question = [q for q in self._questions if q.number == number][0] + except IndexError: + raise ValueError(f"Question number {number} does not exist.") + else: + question = self._questions.pop(randrange(len(self._questions))) + + self.current_question = question + self.current_question.start() + return question + + def end_question(self) -> None: + """ + End the current question. + + This method should be called when the question has been answered, it must be called before + attempting to call `next_question()` again. + """ + if self.current_question is None: + raise RuntimeError("Cannot call end_question() when there is no current question.") + + self.current_question.stop() + self.current_question = None diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 0ab657d2..1d7bd4a9 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -1,4 +1,5 @@ from random import choice, randrange +from string import ascii_uppercase from time import perf_counter from typing import Optional, TypedDict, Union @@ -9,28 +10,18 @@ from discord.ui import Button, View from bot.constants import Colours, NEGATIVE_REPLIES from . import UserScore +from ._game import Question, AlreadyUpdated, QuestionClosed from ._scoreboard import Scoreboard -class CurrentQuestion(TypedDict): - """Representing the different 'keys' of the question taken from the JSON.""" +class AnswerButton(Button): + """Button subclass that's used to guess on a particular answer.""" - number: str - description: str - answers: list[str] - correct: str - points: Optional[int] - time: Optional[int] - - -class QuestionButton(Button): - """Button subclass for the options of the questions.""" - - def __init__(self, label: str, users_picked: dict, view: View): - self.users_picked = users_picked - self._view = view + def __init__(self, label: str, question: Question): super().__init__(label=label, style=discord.ButtonStyle.green) + self.question = question + async def callback(self, interaction: Interaction) -> None: """ When a user interacts with the button, this will be called. @@ -39,114 +30,108 @@ class QuestionButton(Button): - interaction: an instance of discord.Interaction representing the interaction between the user and the button. """ - original_message = interaction.message - original_embed = original_message.embeds[0] - - if interaction.user.id not in self.users_picked.keys(): - people_answered = original_embed.footer.text - people_answered = f"{int(people_answered[0]) + 1} " \ - f"{'person has' if int(people_answered[0]) + 1 == 1 else 'people have'} answered" - original_embed.set_footer(text=people_answered) - await original_message.edit(embed=original_embed, view=self._view) - self.users_picked[interaction.user.id] = [self.label, True, perf_counter() - self._time] + try: + guess = self.question.guess(interaction.user.id, self.label) + except AlreadyUpdated: await interaction.response.send_message( embed=Embed( - title="Confirming that..", - description=f"You chose answer {self.label}.", - color=Colours.soft_green + title=choice(NEGATIVE_REPLIES), + description="You've already changed your answer more than once!", + color=Colours.soft_red ), ephemeral=True ) - elif self.users_picked[interaction.user.id][1] is True: - self.users_picked[interaction.user.id] = [ - self.label, False, perf_counter() - self._time - ] + return + except QuestionClosed: + await interaction.response.send_message( + embed=Embed( + title=choice(NEGATIVE_REPLIES), + description="The question is no longer accepting guesses!", + color=Colours.soft_red + ), + ) + return + + if guess[1]: await interaction.response.send_message( embed=Embed( title="Confirming that..", - description=f"You changed your answer to answer {self.label}.", + description=f"You chose answer {self.label}.", color=Colours.soft_green ), ephemeral=True ) else: + # guess[1] is False and they cannot change their answer again. Which + # indicates that they changed it this time around. await interaction.response.send_message( embed=Embed( - title=choice(NEGATIVE_REPLIES), - description="You've already changed your answer more than once!", - color=Colours.soft_red + title="Confirming that..", + description=f"You changed your answer to answer {self.label}.", + color=Colours.soft_green ), ephemeral=True ) class QuestionView(View): - """View for the questions.""" + """View for one trivia night question.""" - def __init__(self): + def __init__(self, question: Question) -> None: super().__init__() - self.current_question: CurrentQuestion - self.users_picked = {} + self.question = question - self.active_question = False + for letter, _ in self.question.answers: + self.add_item(AnswerButton(letter, self.question)) - def create_current_question(self) -> tuple[Embed, int]: + @staticmethod + def unicodeify(text: str) -> str: """ - Helper function to create the embed for the current question. + Takes `text` and adds zero-width spaces to prevent copy and pasting the question. - Returns an embed containing the question along with each answer choice in the form of a view, - along with the integer representing the time limit of the current question. + Parameters: + - text: A string that represents the question description to 'unicodeify' """ - self.current_labels = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[:len(self.current_question["answers"])] + return "".join( + f"{letter}\u200b" if letter not in ('\n', '\t', '`', 'p', 'y') else letter + for idx, letter in enumerate(text) + ) + + def create_embed(self) -> Embed: + """Helper function to create the embed for the current question.""" question_embed = Embed( - title=f"Question {self.current_question['number']}", - description=self.current_question["obfuscated_description"], + title=f"Question {self.question.number}", + description=self.unicodeify(self.question.description), color=Colours.python_yellow ) - if "_previously_visited" not in self.current_question.keys(): - self.buttons = [QuestionButton(label, self.users_picked, self) for label in self.current_labels] - for button in self.buttons: - self.add_item(button) - for label, answer in zip(self.current_labels, self.current_question["answers"]): + for label, answer in self.question.answers: question_embed.add_field(name=f"Answer {label}", value=answer, inline=False) - question_embed.set_footer(text="0 people have answered") - current_time = perf_counter() - for button in self.buttons: - button._time = current_time - - time_limit = self.current_question.get("time", 10) - - self.active_question = True + return question_embed - return question_embed, time_limit - - def end_question(self) -> tuple[dict, Embed, int, int]: + def end_question(self) -> Embed: """ Ends the question and displays the statistics on who got the question correct, awards points, etc. Returns: - - a dictionary containing all the people who answered and whether or not they got it correct - - an embed displaying the correct answers and the % of people that chose each answer. - - an integer showing the time limit of the current question in seconds - - an integer showing the amount of points the question will award* to those that got it correct + An embed displaying the correct answers and the % of people that chose each answer. """ - labels = self.current_labels - label = labels[self.current_question["answers"].index(self.current_question["correct"])] - return_dict = {name: (*info, info[0] == label) for name, info in self.users_picked.items()} - all_players = list(self.users_picked.items()) + guesses = self.question.stop() + + labels = ascii_uppercase[:len(self.question.answers)] + correct = [label for (label, description) in self.question.answers if description == self.question.correct] answer_embed = Embed( - title=f"The correct answer for Question {self.current_question['number']} was..", - description=self.current_question["correct"] + title=f"The correct answer for Question {self.question.number} was..", + description=self.question.correct ) - if len(all_players) != 0: + if len(guesses) != 0: answers_chosen = { answer_choice: len( - tuple(filter(lambda x: x[0] == answer_choice, self.users_picked.values())) - ) / len(all_players) + tuple(filter(lambda x: x[0] == correct, guesses.values())) + ) / len(guesses) for answer_choice in labels } @@ -160,118 +145,8 @@ class QuestionView(View): # The `ord` function is used here to change the letter to its corresponding position answer_embed.add_field( name=f"{percent * 100:.1f}% of players chose", - value=self.current_question['answers'][ord(answer) - 65], + value=self.question.answers[ord(answer) - 65][1], inline=False ) - self.users_picked = {} - - for button in self.buttons: - button.users_picked = self.users_picked - - time_limit = self.current_question.get("time", 10) - question_points = self.current_question.get("points", 10) - - self.active_question = False - - return return_dict, answer_embed, time_limit, question_points - - -class Questions: - """An interface to use from the TriviaNight cog for questions.""" - - def __init__(self, scoreboard: Scoreboard): - self.scoreboard = scoreboard - self.questions = [] - - def set_questions(self, questions: list) -> None: - """ - Setting `self.questions` dynamically via a function to set it. - - Parameters: - - questions: a list representing all the questions, which is essentially the JSON provided - to load the questions - """ - self.questions = questions - - def next_question(self, number: int = None) -> Union[Embed, None]: - """ - Chooses a random unvisited question from the question bank. - - If the number parameter is specified, it'll head to that specific question. - - Parameters: - - number: An optional integer representing the question number (only used for `.trivianight question` calls) - """ - if all("visited" in question.keys() for question in self.questions) and number is None: - return Embed( - title=choice(NEGATIVE_REPLIES), - description="All of the questions in the question bank have been used.", - color=Colours.soft_red - ) - - if number is None: - question_number = randrange(0, len(self.questions)) - while "visited" in self.questions[question_number].keys(): - question_number = randrange(0, len(self.questions)) - else: - question_number = number - 1 - - if "visited" in self.questions[question_number].keys(): - self.questions[question_number]["_previously_visited"] = True - self.questions[question_number]["visited"] = True - - # The `self.view` refers to the QuestionView - self.view.current_question = self.questions[question_number] - - def list_questions(self) -> Union[Embed, str]: - """ - Lists all questions from the question bank. - - It will put the following into a message: - - Question number - - Question description - - If the question was already 'visited' (displayed) - """ - if not self.questions: - return Embed( - title=choice(NEGATIVE_REPLIES), - description="No questions are currently loaded in!", - color=Colours.soft_red - ) - spaces = len( - sorted( - self.questions, key=lambda question: len(question['description']) - )[-1]["description"] - ) + 3 - formatted_string = "" - for question in self.questions: - question_description = question["description"] - formatted_string += f"`Q{question['number']}: {question_description}" \ - f"{' ' * (spaces - len(question_description) + 2)}" \ - f"|` {':x:' if not question.get('visited') else ':white_check_mark:'}\n" - - return formatted_string.strip() - - def current_question(self) -> tuple[Embed, QuestionView]: - """Returns an embed entailing the current question as an embed with a view.""" - return self.view.create_current_question(), self.view - - def end_question(self) -> Embed: - """ - Terminates answering of the question and displays the correct answer. - - The function returns an embed containing the information about the question such as the following: - - % of people that chose each option - - the correct answer - """ - scores, answer_embed, time_limit, total_points = self.view.end_question() - for user, score in scores.items(): - time_taken = score[2] - point_calculation = (1 - (time_taken / time_limit) / 2) * total_points - if score[-1] is True: - self.scoreboard[UserScore(user)] = {"points": point_calculation, "speed": time_taken} - elif score[-1] is False and score[2] <= 2: - # Get the negative of the point_calculation to deduct it - self.scoreboard[UserScore(user)] = {"points": -point_calculation} return answer_embed diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 59b809b1..86da0c3a 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -4,11 +4,13 @@ from random import choice from typing import Optional from discord import Embed +from discord.colour import Color from discord.ext import commands from bot.bot import Bot from bot.constants import Colours, NEGATIVE_REPLIES, POSITIVE_REPLIES, Roles +from ._game import TriviaNightGame from ._questions import QuestionView, Questions from ._scoreboard import Scoreboard, ScoreboardView @@ -16,37 +18,12 @@ from ._scoreboard import Scoreboard, ScoreboardView TRIVIA_NIGHT_ROLES = (Roles.admin, 78361735739998228) -class TriviaNight(commands.Cog): +class TriviaNightCog(commands.Cog): """Cog for the Python Trivia Night event.""" def __init__(self, bot: Bot): self.bot = bot - self.scoreboard = Scoreboard() - self.questions = Questions(self.scoreboard) - - def setup_views(self, questions: dict) -> None: - """ - Sets up the views for `self.questions` and `self.scoreboard` respectively. - - Parameters: - - questions: The dictionary to set the questions for self.questions to use. - """ - self.questions.view = QuestionView() - self.scoreboard.view = ScoreboardView(self.bot) - self.questions.set_questions(questions) - - @staticmethod - def unicodeify(text: str) -> str: - """ - Takes `text` and adds zero-width spaces to prevent copy and pasting the question. - - Parameters: - - text: A string that represents the question description to 'unicodeify' - """ - return "".join( - f"{letter}\u200b" if letter not in ('\n', '\t', '`', 'p', 'y') else letter - for idx, letter in enumerate(text) - ) + self.game: Optional[TriviaNightGame] = None @commands.group(aliases=["tn"], invoke_without_command=True) async def trivianight(self, ctx: commands.Context) -> None: @@ -84,6 +61,14 @@ class TriviaNight(commands.Cog): - a message link to the attachment/JSON - reading the JSON itself via a codeblock or plain text """ + if self.game is not None: + await ctx.send(embed=Embed( + title=choice(NEGATIVE_REPLIES), + description="There is already a trivia night running!", + color=Colours.soft_red + )) + return + if ctx.message.attachments: json_text = (await ctx.message.attachments[0].read()).decode("utf8") elif not to_load: @@ -105,10 +90,7 @@ class TriviaNight(commands.Cog): except JSONDecodeError: raise commands.BadArgument("Invalid JSON") - for idx, question in enumerate(serialized_json): - serialized_json[idx]["obfuscated_description"] = self.unicodeify(question["description"]) - - self.setup_views(serialized_json) + self.game = TriviaNightGame(serialized_json) success_embed = Embed( title=choice(POSITIVE_REPLIES), @@ -117,35 +99,24 @@ class TriviaNight(commands.Cog): ) await ctx.send(embed=success_embed) - @trivianight.command() - @commands.has_any_role(*TRIVIA_NIGHT_ROLES) - async def reset(self, ctx: commands.Context) -> None: - """Resets previous questions and scoreboards.""" - all_questions = self.questions.questions - - for question in all_questions: - if "visited" in question.keys(): - del question["visited"] - - self.setup_views(list(all_questions)) - - success_embed = Embed( - title=choice(POSITIVE_REPLIES), - description="The scoreboards were reset and questions reset!", - color=Colours.soft_green - ) - await ctx.send(embed=success_embed) - - @trivianight.command() + @trivianight.command(aliases=('next',)) @commands.has_any_role(*TRIVIA_NIGHT_ROLES) - async def next(self, ctx: commands.Context) -> None: + async def question(self, ctx: commands.Context, question_number: str = None) -> None: """ Gets a random question from the unanswered question list and lets the user(s) choose the answer. This command will continuously count down until the time limit of the question is exhausted. However, if `.trivianight stop` is invoked, the counting down is interrupted to show the final results. """ - if self.questions.view.active_question is True: + if self.game is None: + await ctx.send(embed=Embed( + title=choice(NEGATIVE_REPLIES), + description="There is no trivia night running!", + color=Colours.soft_red + )) + return + + if self.game.current_question is not None: error_embed = Embed( title=choice(NEGATIVE_REPLIES), description="There is already an ongoing question!", @@ -154,65 +125,40 @@ class TriviaNight(commands.Cog): await ctx.send(embed=error_embed) return - next_question = self.questions.next_question() - if isinstance(next_question, Embed): - await ctx.send(embed=next_question) - return + next_question = self.game.next_question(question_number) - (question_embed, time_limit), question_view = self.questions.current_question() - message = await ctx.send(embed=question_embed, view=question_view) + question_view = QuestionView(next_question) + question_embed = question_view.create_embed() - for time_remaining in range(time_limit, -1, -1): - if self.questions.view.active_question is False: - await ctx.send(embed=self.questions.end_question()) - await message.edit(embed=question_embed, view=None) - return - - await asyncio.sleep(1) - if time_remaining % 5 == 0 and time_remaining not in (time_limit, 0): - await ctx.send(f"{time_remaining}s remaining") - - await ctx.send(embed=self.questions.end_question()) - await message.edit(embed=question_embed, view=None) - - @trivianight.command() - @commands.has_any_role(*TRIVIA_NIGHT_ROLES) - async def question(self, ctx: commands.Context, question_number: int) -> None: - """ - Gets a question from the question bank depending on the question number provided. - - The logic of this command is similar to `.trivianight next`, with the only difference being that you need to - specify the question number. - - Parameters: - - question_number: An integer represents the question number to go to (i.e. .trivianight question 5). - """ - question = self.questions.next_question(question_number) - if isinstance(question, Embed): - await ctx.send(embed=question) - return - - (question_embed, time_limit), question_view = self.questions.current_question() + next_question.start() message = await ctx.send(embed=question_embed, view=question_view) - for time_remaining in range(time_limit, -1, -1): - if self.questions.view.active_question is False: - await ctx.send(embed=self.questions.end_question()) - await message.edit(embed=question_embed, view=None) - return - - await asyncio.sleep(1) - if time_remaining % 5 == 0 and time_remaining not in (time_limit, 0): - await ctx.send(f"{time_remaining}s remaining") + # Exponentially sleep less and less until the time limit is reached + percentage = 1 + while True: + percentage *= 0.5 + duration = next_question.time * percentage + + await asyncio.sleep(duration) + if int(duration) > 1: + # It is quite ugly to display decimals, the delay for requests to reach Discord + # cause sub-second accuracy to be quite pointless. + await ctx.send(f"{int(duration)}s remaining...") + else: + # Since each time we divide the percentage by 2 and sleep one half of the halves (then sleep a + # half, of that half) we must sleep both halves at the end. + await asyncio.sleep(duration) + break - await ctx.send(embed=self.questions.end_question()) + await ctx.send(embed=question_view.end_question()) await message.edit(embed=question_embed, view=None) + question_view.stop() @trivianight.command() @commands.has_any_role(*TRIVIA_NIGHT_ROLES) async def list(self, ctx: commands.Context) -> None: """ - Displays all the questions from the question bank. + Display all the questions left in the question bank. Questions are displayed in the following format: Q(number): Question description | :white_check_mark: if the question was used otherwise :x:. @@ -255,7 +201,15 @@ class TriviaNight(commands.Cog): The scoreboard view also has a button where the user can see their own rank, points and average speed if they didn't make it onto the leaderboard. """ - if self.questions.view.active_question is True: + if self.game is None: + await ctx.send(embed=Embed( + title=choice(NEGATIVE_REPLIES), + description="There is no trivia night running!", + color=Colours.soft_red + )) + return + + if self.game.current_question is not None: error_embed = Embed( title=choice(NEGATIVE_REPLIES), description="You can't end the event while a question is ongoing!", @@ -266,8 +220,9 @@ class TriviaNight(commands.Cog): scoreboard_embed, scoreboard_view = await self.scoreboard.display() await ctx.send(embed=scoreboard_embed, view=scoreboard_view) + self.game = None def setup(bot: Bot) -> None: """Load the TriviaNight cog.""" - bot.add_cog(TriviaNight(bot)) + bot.add_cog(TriviaNightCog(bot)) -- cgit v1.2.3 From 56d0786ead74a32e4fab3b74eef225292407d9cc Mon Sep 17 00:00:00 2001 From: Bluenix Date: Mon, 13 Dec 2021 17:59:48 +0100 Subject: Fix incorrect variable usage in list comprehension This appears to stem from a misunderstanding by me when restructuring the code. --- bot/exts/events/trivianight/_questions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 1d7bd4a9..9a2cb7d2 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -130,7 +130,7 @@ class QuestionView(View): if len(guesses) != 0: answers_chosen = { answer_choice: len( - tuple(filter(lambda x: x[0] == correct, guesses.values())) + tuple(filter(lambda x: x[0] == answer_choice, guesses.values())) ) / len(guesses) for answer_choice in labels } -- cgit v1.2.3 From 7e8b7335ed02897a85eac97e10f10a0c21658b15 Mon Sep 17 00:00:00 2001 From: Bluenix Date: Sun, 19 Dec 2021 22:22:44 +0100 Subject: Fix bugs after testing and add TODO comments --- bot/exts/events/trivianight/_game.py | 1 - bot/exts/events/trivianight/trivianight.py | 38 ++++++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index aac745a7..7f2e48dc 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -142,7 +142,6 @@ class TriviaNightGame: question = self._questions.pop(randrange(len(self._questions))) self.current_question = question - self.current_question.start() return question def end_question(self) -> None: diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 86da0c3a..1465a03d 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -11,7 +11,7 @@ from bot.bot import Bot from bot.constants import Colours, NEGATIVE_REPLIES, POSITIVE_REPLIES, Roles from ._game import TriviaNightGame -from ._questions import QuestionView, Questions +from ._questions import QuestionView from ._scoreboard import Scoreboard, ScoreboardView # The ID you see below is the Events Lead role ID @@ -152,7 +152,8 @@ class TriviaNightCog(commands.Cog): await ctx.send(embed=question_view.end_question()) await message.edit(embed=question_embed, view=None) - question_view.stop() + + self.game.end_question() @trivianight.command() @commands.has_any_role(*TRIVIA_NIGHT_ROLES) @@ -163,6 +164,21 @@ class TriviaNightCog(commands.Cog): Questions are displayed in the following format: Q(number): Question description | :white_check_mark: if the question was used otherwise :x:. """ + if self.game is None: + await ctx.send(embed=Embed( + title=choice(NEGATIVE_REPLIES), + description="There is no trivia night running!", + color=Colours.soft_red + )) + return + + # TODO: Because of how the game currently works, only the questions left will be able to + # be gotten. Iterate through self.game: + # + # for question in self.game: + # # This is an instance of Question from _game.py + # print(question.description) + question_list = self.questions.list_questions() if isinstance(question_list, Embed): await ctx.send(embed=question_list) @@ -178,16 +194,27 @@ class TriviaNightCog(commands.Cog): This command should be used if the question should be ended early or if the time limit fails """ - if self.questions.view.active_question is False: + if self.game is None: + await ctx.send(embed=Embed( + title=choice(NEGATIVE_REPLIES), + description="There is no trivia night running!", + color=Colours.soft_red + )) + return + + if self.game.current_question is None: error_embed = Embed( title=choice(NEGATIVE_REPLIES), - description="There is not an ongoing question to stop!", + description="There is no ongoing question!", color=Colours.soft_red ) await ctx.send(embed=error_embed) return - self.questions.view.active_question = False + # TODO: We need to tell the 'trivianight next' command that the game has ended, if it is still + # running that means it is currently counting down waiting to end the question. Use an asyncio.Event and + # asyncio.wait(self.lock.wait(), timeout=duration) as opposed to asyncio.sleep(duration). + self.game.end_question() @trivianight.command() @commands.has_any_role(*TRIVIA_NIGHT_ROLES) @@ -218,6 +245,7 @@ class TriviaNightCog(commands.Cog): await ctx.send(embed=error_embed) return + # TODO: Refactor the scoreboard after the game simplification. scoreboard_embed, scoreboard_view = await self.scoreboard.display() await ctx.send(embed=scoreboard_embed, view=scoreboard_view) self.game = None -- cgit v1.2.3 From 8c7baf05a82cf53813ed3eabc197abf7a0d98a63 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 19 Dec 2021 20:27:34 -0500 Subject: refactor --- bot/exts/events/trivianight/_game.py | 31 ++++++++++++++++++++++--- bot/exts/events/trivianight/_questions.py | 36 +++++++++++++++++++++++------- bot/exts/events/trivianight/_scoreboard.py | 36 +++++++++++++++++++----------- bot/exts/events/trivianight/trivianight.py | 27 +++++++++------------- 4 files changed, 90 insertions(+), 40 deletions(-) diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index 7f2e48dc..db303c58 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -3,7 +3,6 @@ from random import randrange from string import ascii_uppercase from typing import Iterable, Optional, TypedDict - DEFAULT_QUESTION_POINTS = 10 DEFAULT_QUESTION_TIME = 10 @@ -56,7 +55,8 @@ class Question: @property def answers(self) -> list[tuple[str, str]]: - """The possible answers for this answer. + """ + The possible answers for this answer. This is a property that returns a list of letter, answer pairs. """ @@ -119,7 +119,11 @@ class TriviaNightGame: def __init__(self, data: list[QuestionData]) -> None: self._questions = [Question(q) for q in data] + # A copy of the questions to keep for `.trivianight list` + self._all_questions = list(self._questions) self.current_question: Optional[Question] = None + self._points = {} + self._speed = {} def __iter__(self) -> Iterable[Question]: return iter(self._questions) @@ -135,7 +139,7 @@ class TriviaNightGame: if number is not None: try: - question = [q for q in self._questions if q.number == number][0] + question = [q for q in self._all_questions if q.number == number][0] except IndexError: raise ValueError(f"Question number {number} does not exist.") else: @@ -156,3 +160,24 @@ class TriviaNightGame: self.current_question.stop() self.current_question = None + + def list_questions(self) -> None: + """ + List all the questions. + + This method should be called when `.trivianight list` is called to display the following information: + - Question number + - Question description + - Visited/not visited + """ + formatted_string = "" + + spaces = max(len(q.description) for q in self._all_questions) + + for question in self._all_questions: + visited, not_visited = ":checkmark:", ":x:" + formatted_string += f"`Q{question.number}: {question.description}" \ + f"{' ' * (spaces - len(question.description))}|`" \ + f" {visited if question not in self._all_questions else not_visited}\n" + + return formatted_string diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 9a2cb7d2..7fb6dedf 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -1,7 +1,5 @@ -from random import choice, randrange +from random import choice from string import ascii_uppercase -from time import perf_counter -from typing import Optional, TypedDict, Union import discord from discord import Embed, Interaction @@ -10,7 +8,7 @@ from discord.ui import Button, View from bot.constants import Colours, NEGATIVE_REPLIES from . import UserScore -from ._game import Question, AlreadyUpdated, QuestionClosed +from ._game import AlreadyUpdated, Question, QuestionClosed from ._scoreboard import Scoreboard @@ -110,7 +108,7 @@ class QuestionView(View): return question_embed - def end_question(self) -> Embed: + def end_question(self, scoreboard: Scoreboard) -> Embed: """ Ends the question and displays the statistics on who got the question correct, awards points, etc. @@ -120,7 +118,6 @@ class QuestionView(View): guesses = self.question.stop() labels = ascii_uppercase[:len(self.question.answers)] - correct = [label for (label, description) in self.question.answers if description == self.question.correct] answer_embed = Embed( title=f"The correct answer for Question {self.question.number} was..", @@ -135,9 +132,13 @@ class QuestionView(View): for answer_choice in labels } - for idx, (answer, percent) in enumerate(answers_chosen.items()): + answers_chosen = dict( + sorted(list(answers_chosen.items()), key=lambda item: item[1], reverse=True) + ) + + for answer, percent in answers_chosen.items(): # Setting the color of answer_embed to the % of people that got it correct via the mapping - if idx == 0: + if dict(self.question.answers)[answer[0]] == self.question.correct: # Maps the % of people who got it right to a color, from a range of red to green percentage_to_color = [0xFC94A1, 0xFFCCCB, 0xCDFFCC, 0xB0F5AB, 0xB0F5AB] answer_embed.color = percentage_to_color[round(percent * 100) // 25] @@ -149,4 +150,23 @@ class QuestionView(View): inline=False ) + # Assign points to users + for user_id, answer in guesses.items(): + if dict(self.question.answers)[answer[0]] == self.question.correct: + scoreboard.assign_points( + UserScore(int(user_id)), + points=(1 - (answer[-1] / self.question.time) / 2) * self.question.max_points, + speed=answer[-1] + ) + elif answer[-1] <= 2: + scoreboard.assign_points( + UserScore(int(user_id)), + points=-(1 - (answer[-1] / self.question.time) / 2) * self.question.max_points + ) + else: + scoreboard.assign_points( + UserScore(int(user_id)), + points=0 + ) + return answer_embed diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 40f93475..babd1bd6 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -16,8 +16,6 @@ class ScoreboardView(View): def __init__(self, bot: Bot): super().__init__() self.bot = bot - self.points = {} - self.speed = {} async def create_main_leaderboard(self) -> Embed: """ @@ -142,19 +140,31 @@ class ScoreboardView(View): class Scoreboard: """Class for the scoreboard for the Trivia Night event.""" - def __setitem__(self, key: UserScore, value: dict): - if value.get("points") and key.user_id not in self.view.points.keys(): - self.view.points[key.user_id] = value["points"] - elif value.get("points"): - self.view.points[key.user_id] += self.view.points[key.user_id] - - if value.get("speed") and key.user_id not in self.view.speed.keys(): - self.view.speed[key.user_id] = [1, value["speed"]] - elif value.get("speed"): - self.view.speed[key.user_id] = [ - self.view.speed[key.user_id][0] + 1, self.view.speed[key.user_id][1] + value["speed"] + def __init__(self, bot: Bot): + self.view = ScoreboardView(bot) + self._points = {} + self._speed = {} + + def assign_points(self, user: UserScore, *, points: int = None, speed: float = None) -> None: + """ + Assign points or deduct points to/from a certain user. + + This method should be called once the question has finished and all answers have been registered. + """ + if points is not None and user.user_id not in self._points.keys(): + self._points[user.user_id] = points + elif points is not None: + self._points[user.user_id] += self._points[user.user_id] + + if speed is not None and user.user_id not in self._speed.keys(): + self._speed[user.user_id] = [1, speed] + elif speed is not None: + self._speed[user.user_id] = [ + self._speed[user.user_id][0] + 1, self._speed[user.user_id][1] + speed ] async def display(self) -> tuple[Embed, View]: """Returns the embed of the main leaderboard along with the ScoreboardView.""" + self.view.points = self._points + self.view.speed = self._speed return await self.view.create_main_leaderboard(), self.view diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 1465a03d..f158ec0c 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -4,7 +4,6 @@ from random import choice from typing import Optional from discord import Embed -from discord.colour import Color from discord.ext import commands from bot.bot import Bot @@ -12,7 +11,7 @@ from bot.constants import Colours, NEGATIVE_REPLIES, POSITIVE_REPLIES, Roles from ._game import TriviaNightGame from ._questions import QuestionView -from ._scoreboard import Scoreboard, ScoreboardView +from ._scoreboard import Scoreboard # The ID you see below is the Events Lead role ID TRIVIA_NIGHT_ROLES = (Roles.admin, 78361735739998228) @@ -24,6 +23,8 @@ class TriviaNightCog(commands.Cog): def __init__(self, bot: Bot): self.bot = bot self.game: Optional[TriviaNightGame] = None + self.scoreboard: Optional[Scoreboard] = None + self.question_closed: asyncio.Event = None @commands.group(aliases=["tn"], invoke_without_command=True) async def trivianight(self, ctx: commands.Context) -> None: @@ -91,12 +92,16 @@ class TriviaNightCog(commands.Cog): raise commands.BadArgument("Invalid JSON") self.game = TriviaNightGame(serialized_json) + self.question_closed = asyncio.Event() success_embed = Embed( title=choice(POSITIVE_REPLIES), description="The JSON was loaded successfully!", color=Colours.soft_green ) + + self.scoreboard = Scoreboard(self.bot) + await ctx.send(embed=success_embed) @trivianight.command(aliases=('next',)) @@ -140,6 +145,7 @@ class TriviaNightCog(commands.Cog): duration = next_question.time * percentage await asyncio.sleep(duration) + if int(duration) > 1: # It is quite ugly to display decimals, the delay for requests to reach Discord # cause sub-second accuracy to be quite pointless. @@ -150,7 +156,7 @@ class TriviaNightCog(commands.Cog): await asyncio.sleep(duration) break - await ctx.send(embed=question_view.end_question()) + await ctx.send(embed=question_view.end_question(self.scoreboard)) await message.edit(embed=question_embed, view=None) self.game.end_question() @@ -172,14 +178,7 @@ class TriviaNightCog(commands.Cog): )) return - # TODO: Because of how the game currently works, only the questions left will be able to - # be gotten. Iterate through self.game: - # - # for question in self.game: - # # This is an instance of Question from _game.py - # print(question.description) - - question_list = self.questions.list_questions() + question_list = self.game.list_questions() if isinstance(question_list, Embed): await ctx.send(embed=question_list) return @@ -211,10 +210,7 @@ class TriviaNightCog(commands.Cog): await ctx.send(embed=error_embed) return - # TODO: We need to tell the 'trivianight next' command that the game has ended, if it is still - # running that means it is currently counting down waiting to end the question. Use an asyncio.Event and - # asyncio.wait(self.lock.wait(), timeout=duration) as opposed to asyncio.sleep(duration). - self.game.end_question() + self.ongoing_question = False @trivianight.command() @commands.has_any_role(*TRIVIA_NIGHT_ROLES) @@ -245,7 +241,6 @@ class TriviaNightCog(commands.Cog): await ctx.send(embed=error_embed) return - # TODO: Refactor the scoreboard after the game simplification. scoreboard_embed, scoreboard_view = await self.scoreboard.display() await ctx.send(embed=scoreboard_embed, view=scoreboard_view) self.game = None -- cgit v1.2.3 From a50739aac6144022a481d78c637b5d5fd7769913 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 21 Dec 2021 15:59:08 -0500 Subject: full refactor complete structure overhauled, changed stop logic --- bot/exts/events/trivianight/trivianight.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index f158ec0c..18e0dce1 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -144,7 +144,15 @@ class TriviaNightCog(commands.Cog): percentage *= 0.5 duration = next_question.time * percentage - await asyncio.sleep(duration) + await asyncio.wait([self.question_closed.wait()], timeout=duration) + + if self.question_closed.is_set(): + await ctx.send(embed=question_view.end_question(self.scoreboard)) + await message.edit(embed=question_embed, view=None) + + self.game.end_question() + self.question_closed.clear() + return if int(duration) > 1: # It is quite ugly to display decimals, the delay for requests to reach Discord @@ -153,7 +161,14 @@ class TriviaNightCog(commands.Cog): else: # Since each time we divide the percentage by 2 and sleep one half of the halves (then sleep a # half, of that half) we must sleep both halves at the end. - await asyncio.sleep(duration) + await asyncio.wait([self.question_closed.wait()], timeout=duration) + if self.question_closed.is_set(): + await ctx.send(embed=question_view.end_question(self.scoreboard)) + await message.edit(embed=question_embed, view=None) + + self.game.end_question() + self.question_closed.clear() + return break await ctx.send(embed=question_view.end_question(self.scoreboard)) @@ -210,7 +225,7 @@ class TriviaNightCog(commands.Cog): await ctx.send(embed=error_embed) return - self.ongoing_question = False + self.question_closed.set() @trivianight.command() @commands.has_any_role(*TRIVIA_NIGHT_ROLES) -- cgit v1.2.3 From b6685b2acffc7958c0f960b3ab04ada731500d24 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 28 Dec 2021 00:28:33 -0500 Subject: remove UserScore --- bot/exts/events/trivianight/__init__.py | 7 ------- bot/exts/events/trivianight/_questions.py | 7 +++---- bot/exts/events/trivianight/_scoreboard.py | 18 ++++++++---------- 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/bot/exts/events/trivianight/__init__.py b/bot/exts/events/trivianight/__init__.py index 87de18e0..e69de29b 100644 --- a/bot/exts/events/trivianight/__init__.py +++ b/bot/exts/events/trivianight/__init__.py @@ -1,7 +0,0 @@ -class UserScore: - """Marker class for passing into the scoreboard to add points/record speed.""" - - __slots__ = ("user_id",) - - def __init__(self, user_id: int): - self.user_id = user_id diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 7fb6dedf..0835d762 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -7,7 +7,6 @@ from discord.ui import Button, View from bot.constants import Colours, NEGATIVE_REPLIES -from . import UserScore from ._game import AlreadyUpdated, Question, QuestionClosed from ._scoreboard import Scoreboard @@ -154,18 +153,18 @@ class QuestionView(View): for user_id, answer in guesses.items(): if dict(self.question.answers)[answer[0]] == self.question.correct: scoreboard.assign_points( - UserScore(int(user_id)), + int(user_id), points=(1 - (answer[-1] / self.question.time) / 2) * self.question.max_points, speed=answer[-1] ) elif answer[-1] <= 2: scoreboard.assign_points( - UserScore(int(user_id)), + int(user_id), points=-(1 - (answer[-1] / self.question.time) / 2) * self.question.max_points ) else: scoreboard.assign_points( - UserScore(int(user_id)), + int(user_id), points=0 ) diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index babd1bd6..d9107dca 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -7,8 +7,6 @@ from discord.ui import Button, View from bot.bot import Bot from bot.constants import Colours, NEGATIVE_REPLIES -from . import UserScore - class ScoreboardView(View): """View for the scoreboard.""" @@ -145,22 +143,22 @@ class Scoreboard: self._points = {} self._speed = {} - def assign_points(self, user: UserScore, *, points: int = None, speed: float = None) -> None: + def assign_points(self, user_id: int, *, points: int = None, speed: float = None) -> None: """ Assign points or deduct points to/from a certain user. This method should be called once the question has finished and all answers have been registered. """ - if points is not None and user.user_id not in self._points.keys(): - self._points[user.user_id] = points + if points is not None and user_id not in self._points.keys(): + self._points[user_id] = points elif points is not None: - self._points[user.user_id] += self._points[user.user_id] + self._points[user_id] += self._points[user_id] - if speed is not None and user.user_id not in self._speed.keys(): - self._speed[user.user_id] = [1, speed] + if speed is not None and user_id not in self._speed.keys(): + self._speed[user_id] = [1, speed] elif speed is not None: - self._speed[user.user_id] = [ - self._speed[user.user_id][0] + 1, self._speed[user.user_id][1] + speed + self._speed[user_id] = [ + self._speed[user_id][0] + 1, self._speed[user_id][1] + speed ] async def display(self) -> tuple[Embed, View]: -- cgit v1.2.3 From f48a03318b9a67563807f0a081430026ecfe8419 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 28 Dec 2021 00:30:51 -0500 Subject: fix \ with if --- bot/exts/events/trivianight/trivianight.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 18e0dce1..0971ea2a 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -74,8 +74,10 @@ class TriviaNightCog(commands.Cog): json_text = (await ctx.message.attachments[0].read()).decode("utf8") elif not to_load: raise commands.BadArgument("You didn't attach an attachment nor link a message!") - elif to_load.startswith("https://discord.com/channels") or \ - to_load.startswith("https://discordapp.com/channels"): + elif ( + to_load.startswith("https://discord.com/channels") + or to_load.startswith("https://discordapp.com/channels") + ): channel_id, message_id = to_load.split("/")[-2:] channel = await ctx.guild.fetch_channel(int(channel_id)) message = await channel.fetch_message(int(message_id)) -- cgit v1.2.3 From e9f65c805ca16cdc1bfb2e13f6a6c91d9f511480 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 28 Dec 2021 00:31:47 -0500 Subject: display error when something goes wrong with .trivianight load --- bot/exts/events/trivianight/trivianight.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 0971ea2a..aa6b8967 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -90,8 +90,8 @@ class TriviaNightCog(commands.Cog): try: serialized_json = loads(json_text) - except JSONDecodeError: - raise commands.BadArgument("Invalid JSON") + except JSONDecodeError as error: + raise commands.BadArgument(f"Looks like something went wrong:\n{str(error)}") self.game = TriviaNightGame(serialized_json) self.question_closed = asyncio.Event() -- cgit v1.2.3 From 736cddce6b84cd8df09da1904c20e503dc4a46e1 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 28 Dec 2021 00:37:50 -0500 Subject: improving ordinal number usage in _scoreboard.ScoreboardView._get_rank --- bot/exts/events/trivianight/_scoreboard.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index d9107dca..6f95f22c 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -91,19 +91,31 @@ class ScoreboardView(View): color=Colours.soft_red ) - suffixes = {"1": "st", "2": "nd", "3": "rd"} + suffix = ["th", "st", "nd", "rd", "th"][min(int(points_rank) % 10, 4)] + if (int(points_rank) % 100) in {11, 12, 13}: + suffix = "th" + + points_rank = str(points_rank) + suffix + rank_embed.add_field( name="Total Points", value=( - f"You got {points_rank}{'th' if not (suffix := suffixes.get(points_rank[-1])) else suffix} place" + f"You got {points_rank} place" f" with {self.points[member.id]:.1f} points." ), inline=False ) + + suffix = ["th", "st", "nd", "rd", "th"][min(int(speed_rank) % 10, 4)] + if (int(speed_rank) % 100) in {11, 12, 13}: + suffix = "th" + + speed_rank = str(speed_rank) + suffix + rank_embed.add_field( name="Average Speed", value=( - f"You got {speed_rank}{'th' if not (suffix := suffixes.get(speed_rank[-1])) else suffix} place" + f"You got {speed_rank} place" f" with a time of {(self.speed[member.id][1] / self.speed[member.id][0]):.1f} seconds." ), inline=False -- cgit v1.2.3 From f436e87e2a3d4a2150293090d8236068b21d9257 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 9 Jan 2022 17:21:04 -0500 Subject: fix visited for .tn list not working --- bot/exts/events/trivianight/_game.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index db303c58..0b5fe562 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -178,6 +178,6 @@ class TriviaNightGame: visited, not_visited = ":checkmark:", ":x:" formatted_string += f"`Q{question.number}: {question.description}" \ f"{' ' * (spaces - len(question.description))}|`" \ - f" {visited if question not in self._all_questions else not_visited}\n" + f" {visited if question not in self._questions else not_visited}\n" return formatted_string -- cgit v1.2.3 From d212af6ac1a965d64077559a593d092e03e5ba42 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 9 Jan 2022 17:25:22 -0500 Subject: sending the question is no longer accepting guesses as an ephemeral --- bot/exts/events/trivianight/_questions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 0835d762..d729009d 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -46,6 +46,7 @@ class AnswerButton(Button): description="The question is no longer accepting guesses!", color=Colours.soft_red ), + ephemeral=True ) return -- cgit v1.2.3 From 77a20ef8008d0815c3250433c88b5096ae843fa4 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 9 Jan 2022 17:46:56 -0500 Subject: sort the speed leaderboard properly --- bot/exts/events/trivianight/_scoreboard.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 6f95f22c..bf744389 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -24,8 +24,6 @@ class ScoreboardView(View): along with the 29 other users who made it onto the leaderboard. """ formatted_string = "" - self.points = dict(sorted(self.points.items(), key=lambda item: item[-1], reverse=True)) - self.speed = dict(sorted(self.speed.items(), key=lambda item: item[-1])) for current_placement, (user, points) in enumerate(self.points.items()): if current_placement + 1 > 30: @@ -175,6 +173,7 @@ class Scoreboard: async def display(self) -> tuple[Embed, View]: """Returns the embed of the main leaderboard along with the ScoreboardView.""" - self.view.points = self._points - self.view.speed = self._speed + self.view.points = dict(sorted(self._points.items(), key=lambda item: item[-1], reverse=True)) + self.view.speed = dict(sorted(self._speed.items(), key=lambda item: item[-1][1] / item[-1][0])) + return await self.view.create_main_leaderboard(), self.view -- cgit v1.2.3 From 60a1747edfee370fb9a7af86d4c13118f665c65e Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 9 Jan 2022 17:55:07 -0500 Subject: preventing the number of points from reaching too high --- bot/exts/events/trivianight/_scoreboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index bf744389..769d6a88 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -162,7 +162,7 @@ class Scoreboard: if points is not None and user_id not in self._points.keys(): self._points[user_id] = points elif points is not None: - self._points[user_id] += self._points[user_id] + self._points[user_id] += points if speed is not None and user_id not in self._speed.keys(): self._speed[user_id] = [1, speed] -- cgit v1.2.3 From e7163dd5dd23ecc75d8b628262733510abf9d0d6 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 9 Jan 2022 18:10:59 -0500 Subject: use correct emoji name for .tn list --- bot/exts/events/trivianight/_game.py | 2 +- bot/exts/events/trivianight/_questions.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index 0b5fe562..9d8b98c1 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -175,7 +175,7 @@ class TriviaNightGame: spaces = max(len(q.description) for q in self._all_questions) for question in self._all_questions: - visited, not_visited = ":checkmark:", ":x:" + visited, not_visited = ":white_check_mark:", ":x:" formatted_string += f"`Q{question.number}: {question.description}" \ f"{' ' * (spaces - len(question.description))}|`" \ f" {visited if question not in self._questions else not_visited}\n" diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index d729009d..391e0a9e 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -128,7 +128,7 @@ class QuestionView(View): answers_chosen = { answer_choice: len( tuple(filter(lambda x: x[0] == answer_choice, guesses.values())) - ) / len(guesses) + ) for answer_choice in labels } @@ -136,16 +136,16 @@ class QuestionView(View): sorted(list(answers_chosen.items()), key=lambda item: item[1], reverse=True) ) - for answer, percent in answers_chosen.items(): + for answer, people_answered in answers_chosen.items(): # Setting the color of answer_embed to the % of people that got it correct via the mapping if dict(self.question.answers)[answer[0]] == self.question.correct: # Maps the % of people who got it right to a color, from a range of red to green percentage_to_color = [0xFC94A1, 0xFFCCCB, 0xCDFFCC, 0xB0F5AB, 0xB0F5AB] - answer_embed.color = percentage_to_color[round(percent * 100) // 25] + answer_embed.color = percentage_to_color[round(people_answered / len(guesses) * 100) // 25] # The `ord` function is used here to change the letter to its corresponding position answer_embed.add_field( - name=f"{percent * 100:.1f}% of players chose", + name=f"{people_answered / len(guesses) * 100:.1f}% of players chose", value=self.question.answers[ord(answer) - 65][1], inline=False ) -- cgit v1.2.3 From 60d131d44f9b3ccf06d1a2c0679e5daa3ae8b299 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 9 Jan 2022 18:18:17 -0500 Subject: QoL: adding an emoji and the number of people who answered for when the question is done --- bot/exts/events/trivianight/_questions.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 391e0a9e..013ffb0a 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -137,15 +137,22 @@ class QuestionView(View): ) for answer, people_answered in answers_chosen.items(): + is_correct_answer = dict(self.question.answers)[answer[0]] == self.question.correct + # Setting the color of answer_embed to the % of people that got it correct via the mapping - if dict(self.question.answers)[answer[0]] == self.question.correct: + if is_correct_answer: # Maps the % of people who got it right to a color, from a range of red to green percentage_to_color = [0xFC94A1, 0xFFCCCB, 0xCDFFCC, 0xB0F5AB, 0xB0F5AB] answer_embed.color = percentage_to_color[round(people_answered / len(guesses) * 100) // 25] + field_title = ( + (":white_check_mark: " if is_correct_answer else "") + + f"{people_answered} players (or {people_answered / len(guesses) * 100:.1f}% of players) chose" + ) + # The `ord` function is used here to change the letter to its corresponding position answer_embed.add_field( - name=f"{people_answered / len(guesses) * 100:.1f}% of players chose", + name=field_title, value=self.question.answers[ord(answer) - 65][1], inline=False ) -- cgit v1.2.3 From b68302524628573ee7e20ccd81db6bb60c05061b Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 9 Jan 2022 18:26:08 -0500 Subject: fixing .tn next logic --- bot/exts/events/trivianight/_game.py | 8 +++++++- bot/exts/events/trivianight/trivianight.py | 17 +++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index 9d8b98c1..7f667dcf 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -33,6 +33,10 @@ class AlreadyUpdated(RuntimeError): """Exception raised when the user has already updated their guess once.""" +class AllQuestionsVisited(RuntimeError): + """Exception raised when all of the questions have been visited.""" + + class Question: """Interface for one question in a trivia night game.""" @@ -142,6 +146,8 @@ class TriviaNightGame: question = [q for q in self._all_questions if q.number == number][0] except IndexError: raise ValueError(f"Question number {number} does not exist.") + elif len(self._questions) == 0: + raise AllQuestionsVisited("All of the questions have been visited.") else: question = self._questions.pop(randrange(len(self._questions))) @@ -161,7 +167,7 @@ class TriviaNightGame: self.current_question.stop() self.current_question = None - def list_questions(self) -> None: + def list_questions(self) -> str: """ List all the questions. diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index aa6b8967..af517260 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -9,7 +9,7 @@ from discord.ext import commands from bot.bot import Bot from bot.constants import Colours, NEGATIVE_REPLIES, POSITIVE_REPLIES, Roles -from ._game import TriviaNightGame +from ._game import AllQuestionsVisited, TriviaNightGame from ._questions import QuestionView from ._scoreboard import Scoreboard @@ -132,7 +132,16 @@ class TriviaNightCog(commands.Cog): await ctx.send(embed=error_embed) return - next_question = self.game.next_question(question_number) + try: + next_question = self.game.next_question(question_number) + except AllQuestionsVisited: + error_embed = Embed( + title=choice(NEGATIVE_REPLIES), + description="All of the questions have been used.", + color=Colours.soft_red + ) + await ctx.send(embed=error_embed) + return question_view = QuestionView(next_question) question_embed = question_view.create_embed() @@ -196,10 +205,6 @@ class TriviaNightCog(commands.Cog): return question_list = self.game.list_questions() - if isinstance(question_list, Embed): - await ctx.send(embed=question_list) - return - await ctx.send(question_list) @trivianight.command() -- cgit v1.2.3 From 570b138df7b2a30cff67bf99fa56173c557d45c3 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 9 Jan 2022 18:26:31 -0500 Subject: shorten field titles for after a question has been answered --- bot/exts/events/trivianight/_questions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 013ffb0a..2d337b40 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -147,7 +147,7 @@ class QuestionView(View): field_title = ( (":white_check_mark: " if is_correct_answer else "") - + f"{people_answered} players (or {people_answered / len(guesses) * 100:.1f}% of players) chose" + + f"{people_answered} players ({people_answered / len(guesses) * 100:.1f}%) chose" ) # The `ord` function is used here to change the letter to its corresponding position -- cgit v1.2.3 From 4497ec3c52fe17427567e8e81eeb618a61c7dd24 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 10 Jan 2022 21:55:11 -0500 Subject: 3 second countdown before question starts. --- bot/exts/events/trivianight/trivianight.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index af517260..6676a14e 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -143,6 +143,9 @@ class TriviaNightCog(commands.Cog): await ctx.send(embed=error_embed) return + await ctx.send("Next question in 3 seconds! Get ready...") + await asyncio.sleep(3) + question_view = QuestionView(next_question) question_embed = question_view.create_embed() -- cgit v1.2.3 From 203923de98d5063d5fa8b1951a7fb01103a66957 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 10 Jan 2022 22:03:19 -0500 Subject: change .. to ... --- bot/exts/events/trivianight/_questions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 2d337b40..d6beced9 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -53,7 +53,7 @@ class AnswerButton(Button): if guess[1]: await interaction.response.send_message( embed=Embed( - title="Confirming that..", + title="Confirming that...", description=f"You chose answer {self.label}.", color=Colours.soft_green ), @@ -64,7 +64,7 @@ class AnswerButton(Button): # indicates that they changed it this time around. await interaction.response.send_message( embed=Embed( - title="Confirming that..", + title="Confirming that...", description=f"You changed your answer to answer {self.label}.", color=Colours.soft_green ), @@ -120,7 +120,7 @@ class QuestionView(View): labels = ascii_uppercase[:len(self.question.answers)] answer_embed = Embed( - title=f"The correct answer for Question {self.question.number} was..", + title=f"The correct answer for Question {self.question.number} was...", description=self.question.correct ) -- cgit v1.2.3 From cffb97531a4fe1ccd3df7d7d0bdcc57ed749c16b Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 10 Jan 2022 22:06:03 -0500 Subject: default timer is at 20 seconds --- bot/exts/events/trivianight/_game.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index 7f667dcf..994006bf 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -4,7 +4,7 @@ from string import ascii_uppercase from typing import Iterable, Optional, TypedDict DEFAULT_QUESTION_POINTS = 10 -DEFAULT_QUESTION_TIME = 10 +DEFAULT_QUESTION_TIME = 20 class QuestionData(TypedDict): -- cgit v1.2.3 From de66d41a7ab350b684ed7b5f829136ebdb4d9c37 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 10 Jan 2022 22:09:23 -0500 Subject: Apply suggestions from code review Co-authored-by: Johannes Christ --- bot/exts/events/trivianight/_game.py | 9 ++++----- bot/exts/events/trivianight/_scoreboard.py | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index 994006bf..a47025c2 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -18,11 +18,10 @@ class QuestionData(TypedDict): time: Optional[int] -UserGuess = tuple[ - str, # The answer that was guessed - bool, # Whether the answer can be changed again - float # The time it took to guess -] +class UserGuess(NamedTuple): + answer: str + editable: bool + elapsed: float class QuestionClosed(RuntimeError): diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 769d6a88..583532a3 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -80,8 +80,8 @@ class ScoreboardView(View): rank_embed = Embed(title=f"Ranks for {member.display_name}", color=Colours.python_blue) # These are stored as strings so that the last digit can be determined to choose the suffix try: - points_rank = str(list(self.points.keys()).index(member.id) + 1) - speed_rank = str(list(self.speed.keys()).index(member.id) + 1) + points_rank = str(list(self.points).index(member.id) + 1) + speed_rank = str(list(self.speed).index(member.id) + 1) except ValueError: return Embed( title=choice(NEGATIVE_REPLIES), -- cgit v1.2.3 From 7af2e3d215962d613566416cdc69457b332ecb20 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 10 Jan 2022 22:14:28 -0500 Subject: int to ordinal as a separate method --- bot/exts/events/trivianight/_scoreboard.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 583532a3..56a86029 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -15,6 +15,15 @@ class ScoreboardView(View): super().__init__() self.bot = bot + @staticmethod + def _int_to_ordinal(number: int) -> str: + """Converts an integer into an ordinal number, i.e. 1 to 1st.""" + suffix = ["th", "st", "nd", "rd", "th"][min(number % 10, 4)] + if (number % 100) in {11, 12, 13}: + suffix = "th" + + return str(number) + suffix + async def create_main_leaderboard(self) -> Embed: """ Helper function that iterates through `self.points` to generate the main leaderboard embed. @@ -89,31 +98,19 @@ class ScoreboardView(View): color=Colours.soft_red ) - suffix = ["th", "st", "nd", "rd", "th"][min(int(points_rank) % 10, 4)] - if (int(points_rank) % 100) in {11, 12, 13}: - suffix = "th" - - points_rank = str(points_rank) + suffix - rank_embed.add_field( name="Total Points", value=( - f"You got {points_rank} place" + f"You got {self._int_to_ordinal(int(points_rank))} place" f" with {self.points[member.id]:.1f} points." ), inline=False ) - suffix = ["th", "st", "nd", "rd", "th"][min(int(speed_rank) % 10, 4)] - if (int(speed_rank) % 100) in {11, 12, 13}: - suffix = "th" - - speed_rank = str(speed_rank) + suffix - rank_embed.add_field( name="Average Speed", value=( - f"You got {speed_rank} place" + f"You got {self._int_to_ordinal(int(speed_rank))} place" f" with a time of {(self.speed[member.id][1] / self.speed[member.id][0]):.1f} seconds." ), inline=False -- cgit v1.2.3 From 74dcb9910da45c4e135ece34f6491c625bba6a79 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 11 Jan 2022 20:00:13 -0500 Subject: reverting namedtuple change --- bot/exts/events/trivianight/_game.py | 9 +++++---- bot/exts/events/trivianight/_scoreboard.py | 7 ++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index a47025c2..1526aa14 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -18,10 +18,11 @@ class QuestionData(TypedDict): time: Optional[int] -class UserGuess(NamedTuple): - answer: str - editable: bool - elapsed: float +UserGuess = tuple[ + str, # Represents the answer choice the user chose. + bool, # Represents if the user can edit their answer. + float # Represents the amount of time passed since the question began. +] class QuestionClosed(RuntimeError): diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 56a86029..7ec3c76b 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -17,7 +17,12 @@ class ScoreboardView(View): @staticmethod def _int_to_ordinal(number: int) -> str: - """Converts an integer into an ordinal number, i.e. 1 to 1st.""" + """ + Converts an integer into an ordinal number, i.e. 1 to 1st. + + Parameters: + - number: an integer representing the number to convert to an ordinal number. + """ suffix = ["th", "st", "nd", "rd", "th"][min(number % 10, 4)] if (number % 100) in {11, 12, 13}: suffix = "th" -- cgit v1.2.3 From 4e5f98468d8fc450e01c99450f43f0c77b543747 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 12 Jan 2022 15:40:48 -0500 Subject: reverting back to original commit for a NamedTuple --- bot/exts/events/trivianight/_game.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index 1526aa14..6d177783 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -1,7 +1,7 @@ import time from random import randrange from string import ascii_uppercase -from typing import Iterable, Optional, TypedDict +from typing import Iterable, NamedTuple, Optional, TypedDict DEFAULT_QUESTION_POINTS = 10 DEFAULT_QUESTION_TIME = 20 @@ -18,11 +18,12 @@ class QuestionData(TypedDict): time: Optional[int] -UserGuess = tuple[ - str, # Represents the answer choice the user chose. - bool, # Represents if the user can edit their answer. - float # Represents the amount of time passed since the question began. -] +class UserGuess(NamedTuple): + """Represents the user's guess for a question.""" + + answer: str + editable: bool + elapsed: float class QuestionClosed(RuntimeError): -- cgit v1.2.3 From e015fff0a1147a2a2b00a47ca7a465bc9f0d89e3 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Fri, 28 Jan 2022 22:50:19 -0500 Subject: fix question bug --- bot/exts/events/trivianight/_game.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index 6d177783..4b115086 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -144,7 +144,7 @@ class TriviaNightGame: if number is not None: try: - question = [q for q in self._all_questions if q.number == number][0] + question = [q for q in self._all_questions if q.number == int(number)][0] except IndexError: raise ValueError(f"Question number {number} does not exist.") elif len(self._questions) == 0: -- cgit v1.2.3 From 13aea5abca919a4b6105dcfc31b95d14aded5430 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 30 Jan 2022 20:32:58 -0500 Subject: initialize scoreboard view only during display --- bot/exts/events/trivianight/_scoreboard.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 7ec3c76b..d0d0a49c 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -151,7 +151,7 @@ class Scoreboard: """Class for the scoreboard for the Trivia Night event.""" def __init__(self, bot: Bot): - self.view = ScoreboardView(bot) + self._bot = bot self._points = {} self._speed = {} @@ -175,7 +175,9 @@ class Scoreboard: async def display(self) -> tuple[Embed, View]: """Returns the embed of the main leaderboard along with the ScoreboardView.""" - self.view.points = dict(sorted(self._points.items(), key=lambda item: item[-1], reverse=True)) - self.view.speed = dict(sorted(self._speed.items(), key=lambda item: item[-1][1] / item[-1][0])) + view = ScoreboardView(self._bot) - return await self.view.create_main_leaderboard(), self.view + view.points = dict(sorted(self._points.items(), key=lambda item: item[-1], reverse=True)) + view.speed = dict(sorted(self._speed.items(), key=lambda item: item[-1][1] / item[-1][0])) + + return await view.create_main_leaderboard(), view -- cgit v1.2.3 From 1f5111a75004c677805a16269e0730200cbfea87 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 31 Jan 2022 19:32:54 -0500 Subject: fix scoreboard bugs --- bot/exts/events/trivianight/_scoreboard.py | 7 +++-- bot/exts/events/trivianight/trivianight.py | 42 +++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index d0d0a49c..a5a5fcac 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -173,11 +173,14 @@ class Scoreboard: self._speed[user_id][0] + 1, self._speed[user_id][1] + speed ] - async def display(self) -> tuple[Embed, View]: + async def display(self, speed_leaderboard: bool = False) -> tuple[Embed, View]: """Returns the embed of the main leaderboard along with the ScoreboardView.""" view = ScoreboardView(self._bot) view.points = dict(sorted(self._points.items(), key=lambda item: item[-1], reverse=True)) view.speed = dict(sorted(self._speed.items(), key=lambda item: item[-1][1] / item[-1][0])) - return await view.create_main_leaderboard(), view + return ( + await view.create_main_leaderboard(), + view if not speed_leaderboard else await view._create_speed_embed() + ) diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 6676a14e..bdcf243a 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -241,7 +241,7 @@ class TriviaNightCog(commands.Cog): @commands.has_any_role(*TRIVIA_NIGHT_ROLES) async def end(self, ctx: commands.Context) -> None: """ - Ends the trivia night event and displays the scoreboard view. + Displays the scoreboard view. The scoreboard view consists of the two scoreboards with the 30 players who got the highest points and the 30 players who had the fastest average response time to a question where they got the question right. @@ -268,8 +268,48 @@ class TriviaNightCog(commands.Cog): scoreboard_embed, scoreboard_view = await self.scoreboard.display() await ctx.send(embed=scoreboard_embed, view=scoreboard_view) + + @trivianight.command() + @commands.has_any_role(*TRIVIA_NIGHT_ROLES) + async def scoreboard(self, ctx: commands.Context) -> None: + """ + Displays the scoreboard. + + The scoreboard consists of the two scoreboards with the 30 players who got the highest points and the + 30 players who had the fastest average response time to a question where they got the question right. + """ + if self.game is None: + await ctx.send(embed=Embed( + title=choice(NEGATIVE_REPLIES), + description="There is no trivia night running!", + color=Colours.soft_red + )) + return + + if self.game.current_question is not None: + error_embed = Embed( + title=choice(NEGATIVE_REPLIES), + description="You can't end the event while a question is ongoing!", + color=Colours.soft_red + ) + await ctx.send(embed=error_embed) + return + + scoreboard_embed, speed_scoreboard = await self.scoreboard.display(speed_leaderboard=True) + await ctx.send(embeds=(scoreboard_embed, speed_scoreboard)) + + @trivianight.command() + @commands.has_any_role(*TRIVIA_NIGHT_ROLES) + async def end_game(self, ctx: commands.Context) -> None: + """Ends the ongoing game.""" self.game = None + await ctx.send(embed=Embed( + title=choice(POSITIVE_REPLIES), + description="The game has been stopped.", + color=Colours.soft_green + )) + def setup(bot: Bot) -> None: """Load the TriviaNight cog.""" -- cgit v1.2.3 From 282ac3dc9b3926b16ade2697de03b61634f9dcdd Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 31 Jan 2022 23:26:50 -0500 Subject: pagination --- bot/exts/events/trivianight/trivianight.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index bdcf243a..cf1e45c3 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -8,6 +8,7 @@ from discord.ext import commands from bot.bot import Bot from bot.constants import Colours, NEGATIVE_REPLIES, POSITIVE_REPLIES, Roles +from bot.utils.pagination import LinePaginator from ._game import AllQuestionsVisited, TriviaNightGame from ._questions import QuestionView @@ -207,8 +208,19 @@ class TriviaNightCog(commands.Cog): )) return - question_list = self.game.list_questions() - await ctx.send(question_list) + question_list = self.game.list_questions().split("\n") + + list_embed = Embed(title="All Trivia Night Questions") + + if len(question_list) <= 5: + list_embed.description = "\n".join(question_list) + await ctx.send(embed=list_embed) + else: + await LinePaginator.paginate( + ("\n".join(question_list[idx:idx+5]) for idx in range(0, len(question_list), 5)), + ctx, + list_embed + ) @trivianight.command() @commands.has_any_role(*TRIVIA_NIGHT_ROLES) -- cgit v1.2.3 From 88a65659d5c45ed6be54f35245168f6e30192015 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 7 Feb 2022 18:52:07 -0500 Subject: fix lists command --- bot/exts/events/trivianight/_game.py | 22 +++++++++++++++------- bot/exts/events/trivianight/trivianight.py | 8 ++++---- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index 4b115086..4937e6e3 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -177,14 +177,22 @@ class TriviaNightGame: - Question description - Visited/not visited """ + question_list = [] formatted_string = "" - spaces = max(len(q.description) for q in self._all_questions) + visited = ":white_check_mark:" + not_visited = ":x:" for question in self._all_questions: - visited, not_visited = ":white_check_mark:", ":x:" - formatted_string += f"`Q{question.number}: {question.description}" \ - f"{' ' * (spaces - len(question.description))}|`" \ - f" {visited if question not in self._questions else not_visited}\n" - - return formatted_string + formatted_string += ( + f"**Q{question.number}** {not_visited if question in self._questions else visited}" + f"\n{question.description}\n\n" + ) + if question.number % 5 == 0: + question_list.append(formatted_string.rstrip()) + formatted_string = "" + + if formatted_string: + question_list.append(formatted_string) + + return question_list diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index cf1e45c3..d190fc13 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -208,16 +208,16 @@ class TriviaNightCog(commands.Cog): )) return - question_list = self.game.list_questions().split("\n") + question_list = self.game.list_questions() list_embed = Embed(title="All Trivia Night Questions") - if len(question_list) <= 5: - list_embed.description = "\n".join(question_list) + if len(question_list) == 1: + list_embed.description = question_list[0] await ctx.send(embed=list_embed) else: await LinePaginator.paginate( - ("\n".join(question_list[idx:idx+5]) for idx in range(0, len(question_list), 5)), + question_list, ctx, list_embed ) -- cgit v1.2.3 From 817091476e94df9b633a4d2686cdb712f73f6eb9 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 7 Feb 2022 19:27:17 -0500 Subject: let the paginator do its thing --- bot/exts/events/trivianight/_game.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index 4937e6e3..8b012a17 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -178,21 +178,15 @@ class TriviaNightGame: - Visited/not visited """ question_list = [] - formatted_string = "" visited = ":white_check_mark:" not_visited = ":x:" for question in self._all_questions: - formatted_string += ( + formatted_string = ( f"**Q{question.number}** {not_visited if question in self._questions else visited}" f"\n{question.description}\n\n" ) - if question.number % 5 == 0: - question_list.append(formatted_string.rstrip()) - formatted_string = "" - - if formatted_string: - question_list.append(formatted_string) + question_list.append(formatted_string.rstrip()) return question_list -- cgit v1.2.3 From 6e4d1156faff51d05b53ada0e6c0bb2a56f56533 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 9 Feb 2022 18:14:26 -0500 Subject: add event runner role id --- bot/exts/events/trivianight/trivianight.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index d190fc13..397ff91a 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -14,8 +14,8 @@ from ._game import AllQuestionsVisited, TriviaNightGame from ._questions import QuestionView from ._scoreboard import Scoreboard -# The ID you see below is the Events Lead role ID -TRIVIA_NIGHT_ROLES = (Roles.admin, 78361735739998228) +# The ID you see below are the Events Lead role ID and the Event Runner Role ID +TRIVIA_NIGHT_ROLES = (Roles.admin, 78361735739998228, 940911658799333408) class TriviaNightCog(commands.Cog): -- cgit v1.2.3 From 54a061841f1aafb67ecac2cee4b66dea7f72c776 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 9 Feb 2022 21:57:59 -0500 Subject: missing extra 7 --- bot/exts/events/trivianight/trivianight.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 397ff91a..7d1f4070 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -15,7 +15,7 @@ from ._questions import QuestionView from ._scoreboard import Scoreboard # The ID you see below are the Events Lead role ID and the Event Runner Role ID -TRIVIA_NIGHT_ROLES = (Roles.admin, 78361735739998228, 940911658799333408) +TRIVIA_NIGHT_ROLES = (Roles.admin, 778361735739998228, 940911658799333408) class TriviaNightCog(commands.Cog): -- cgit v1.2.3 From 52a2bf7e373bea1e2b0738eb9d3c5561609c968c Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sat, 12 Feb 2022 11:00:11 -0500 Subject: fix typo --- bot/exts/events/trivianight/trivianight.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 7d1f4070..18d8327a 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -15,7 +15,7 @@ from ._questions import QuestionView from ._scoreboard import Scoreboard # The ID you see below are the Events Lead role ID and the Event Runner Role ID -TRIVIA_NIGHT_ROLES = (Roles.admin, 778361735739998228, 940911658799333408) +TRIVIA_NIGHT_ROLES = (Roles.admins, 778361735739998228, 940911658799333408) class TriviaNightCog(commands.Cog): -- cgit v1.2.3 From c815a19612be9e0a28403786696964e14420204f Mon Sep 17 00:00:00 2001 From: ToxicKidz Date: Mon, 14 Feb 2022 18:26:10 -0500 Subject: fix: Add newlines in codeblock formatting --- bot/exts/utilities/githubinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index 009e0fad..963f54e5 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -257,7 +257,7 @@ class GithubInfo(commands.Cog): embed = discord.Embed( title=f"`{user_data['login']}`'s GitHub profile info", - description=f"```{user_data['bio']}```\n" if user_data["bio"] else "", + description=f"```\n{user_data['bio']}\n```\n" if user_data["bio"] else "", colour=discord.Colour.og_blurple(), url=user_data["html_url"], timestamp=datetime.strptime(user_data["created_at"], "%Y-%m-%dT%H:%M:%SZ") -- cgit v1.2.3 From 833d2e5201028e90dfb9e84da4766fba498dc04a Mon Sep 17 00:00:00 2001 From: ChrisJL Date: Tue, 15 Feb 2022 11:27:11 +0000 Subject: Disable AoC completionist task This disabled the completionist task that checks the leaderboard for people who have 50 stars and gives out the role. Since the event is running, we are not keeping the session cookies up to date, so this is flooding #dev-log with errors. This task should be altered in preparation for next event so that commenting out this line isn't required. Co-authored-by: ToxicKidz <78174417+ToxicKidz@users.noreply.github.com> --- bot/exts/events/advent_of_code/_cog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 3acfef39..518841d4 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -61,7 +61,8 @@ class AdventOfCode(commands.Cog): self.status_task.set_name("AoC Status Countdown") self.status_task.add_done_callback(_helpers.background_task_callback) - self.completionist_task.start() + # Don't start task while event isn't running + # self.completionist_task.start() @tasks.loop(minutes=10.0) async def completionist_task(self) -> None: -- cgit v1.2.3 From f4ffffb052552ea3271b1c7d533d127d583023e4 Mon Sep 17 00:00:00 2001 From: MaskDuck Date: Wed, 16 Feb 2022 01:33:42 +0700 Subject: Fix #1024 (#1030) --- bot/exts/utilities/epoch.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bot/exts/utilities/epoch.py b/bot/exts/utilities/epoch.py index 03758af0..42312dd1 100644 --- a/bot/exts/utilities/epoch.py +++ b/bot/exts/utilities/epoch.py @@ -86,7 +86,10 @@ class Epoch(commands.Cog): view = TimestampMenuView(ctx, self._format_dates(date_time), epoch) original = await ctx.send(f"`{epoch}`", view=view) await view.wait() # wait until expiration before removing the dropdown - await original.edit(view=None) + try: + await original.edit(view=None) + except discord.NotFound: # disregard the error message if the message is deleled + pass @staticmethod def _format_dates(date: arrow.Arrow) -> list[str]: -- cgit v1.2.3 From 12c4b533eef4404fc6488a39918af97e17c1f839 Mon Sep 17 00:00:00 2001 From: DMFriends <86751519+DMFriends@users.noreply.github.com> Date: Wed, 16 Feb 2022 17:14:35 -0500 Subject: Add topics for `#programming-pedagogy` channel --- bot/resources/utilities/py_topics.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bot/resources/utilities/py_topics.yaml b/bot/resources/utilities/py_topics.yaml index 4f684841..92fc0b73 100644 --- a/bot/resources/utilities/py_topics.yaml +++ b/bot/resources/utilities/py_topics.yaml @@ -46,6 +46,15 @@ - What modules/libraries do you want to see more projects using? - What's the most ambitious thing you've done with Python so far? +# programming-pedagogy +934931964509691966: + - What is the best way to teach/learn OOP? + - What benefits are there to teaching programming to students who aren't training to become developers? + - What are some basic concepts that we need to know before teaching programming to others? + - What are the most common difficulties/misconceptions students encounter while learning to program? + - What makes a project a good learning experience for beginners? + - What can make difficult concepts more fun for students to learn? + # algos-and-data-structs 650401909852864553: - -- cgit v1.2.3 From 436c9f740cc5002ff8199b57a6b7bc1a778d6b37 Mon Sep 17 00:00:00 2001 From: TizzySaurus <47674925+TizzySaurus@users.noreply.github.com> Date: Sun, 20 Feb 2022 11:24:51 +0000 Subject: Allow `.src` in dev-contrib and community-meta (#1033) --- bot/constants.py | 9 +++++---- bot/exts/core/error_handler.py | 3 ++- bot/exts/core/source.py | 4 +++- bot/exts/holidays/easter/egg_facts.py | 2 +- bot/exts/holidays/halloween/candy_collection.py | 6 +++--- bot/exts/holidays/halloween/spookynamerate.py | 8 ++++---- bot/exts/holidays/pride/pride_facts.py | 2 +- bot/exts/holidays/pride/pride_leader.py | 2 +- bot/exts/holidays/valentines/be_my_valentine.py | 2 +- bot/exts/holidays/valentines/lovecalculator.py | 4 ++-- bot/utils/checks.py | 2 +- bot/utils/decorators.py | 4 ++-- 12 files changed, 26 insertions(+), 22 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index d39f7361..b4d7bc24 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -109,7 +109,8 @@ class Cats: class Channels(NamedTuple): advent_of_code = int(environ.get("AOC_CHANNEL_ID", 897932085766004786)) advent_of_code_commands = int(environ.get("AOC_COMMANDS_CHANNEL_ID", 897932607545823342)) - bot = 267659945086812160 + bot_commands = 267659945086812160 + community_meta = 267659945086812160 organisation = 551789653284356126 devlog = int(environ.get("CHANNEL_DEVLOG", 622895325144940554)) dev_contrib = 635950537262759947 @@ -118,7 +119,7 @@ class Channels(NamedTuple): off_topic_0 = 291284109232308226 off_topic_1 = 463035241142026251 off_topic_2 = 463035268514185226 - community_bot_commands = int(environ.get("CHANNEL_COMMUNITY_BOT_COMMANDS", 607247579608121354)) + sir_lancebot_playground = int(environ.get("CHANNEL_COMMUNITY_BOT_COMMANDS", 607247579608121354)) voice_chat_0 = 412357430186344448 voice_chat_1 = 799647045886541885 staff_voice = 541638762007101470 @@ -350,8 +351,8 @@ STAFF_ROLES = {Roles.helpers, Roles.moderation_team, Roles.admins, Roles.owners} # Whitelisted channels WHITELISTED_CHANNELS = ( - Channels.bot, - Channels.community_bot_commands, + Channels.bot_commands, + Channels.sir_lancebot_playground, Channels.off_topic_0, Channels.off_topic_1, Channels.off_topic_2, diff --git a/bot/exts/core/error_handler.py b/bot/exts/core/error_handler.py index 676a1e70..983632ba 100644 --- a/bot/exts/core/error_handler.py +++ b/bot/exts/core/error_handler.py @@ -98,7 +98,8 @@ class CommandErrorHandler(commands.Cog): if isinstance(error, commands.NoPrivateMessage): await ctx.send( embed=self.error_embed( - f"This command can only be used in the server. Go to <#{Channels.community_bot_commands}> instead!", + "This command can only be used in the server. " + f"Go to <#{Channels.sir_lancebot_playground}> instead!", NEGATIVE_REPLIES ) ) diff --git a/bot/exts/core/source.py b/bot/exts/core/source.py index 7572ce51..e9568933 100644 --- a/bot/exts/core/source.py +++ b/bot/exts/core/source.py @@ -6,14 +6,16 @@ from discord import Embed from discord.ext import commands from bot.bot import Bot -from bot.constants import Source +from bot.constants import Channels, Source, WHITELISTED_CHANNELS from bot.utils.converters import SourceConverter, SourceType +from bot.utils.decorators import whitelist_override class BotSource(commands.Cog): """Displays information about the bot's source code.""" @commands.command(name="source", aliases=("src",)) + @whitelist_override(channels=WHITELISTED_CHANNELS+[Channels.community_meta, Channels.dev_contrib]) async def source_command(self, ctx: commands.Context, *, source_item: SourceConverter = None) -> None: """Display information and a GitHub link to the source code of a command, tag, or cog.""" if not source_item: diff --git a/bot/exts/holidays/easter/egg_facts.py b/bot/exts/holidays/easter/egg_facts.py index 5f216e0d..152af6a4 100644 --- a/bot/exts/holidays/easter/egg_facts.py +++ b/bot/exts/holidays/easter/egg_facts.py @@ -31,7 +31,7 @@ class EasterFacts(commands.Cog): """A background task that sends an easter egg fact in the event channel everyday.""" await self.bot.wait_until_guild_available() - channel = self.bot.get_channel(Channels.community_bot_commands) + channel = self.bot.get_channel(Channels.sir_lancebot_playground) await channel.send(embed=self.make_embed()) @commands.command(name="eggfact", aliases=("fact",)) diff --git a/bot/exts/holidays/halloween/candy_collection.py b/bot/exts/holidays/halloween/candy_collection.py index 729bbc97..220ba8e5 100644 --- a/bot/exts/holidays/halloween/candy_collection.py +++ b/bot/exts/holidays/halloween/candy_collection.py @@ -55,7 +55,7 @@ class CandyCollection(commands.Cog): if message.author.bot: return # ensure it's hacktober channel - if message.channel.id != Channels.community_bot_commands: + if message.channel.id != Channels.sir_lancebot_playground: return # do random check for skull first as it has the lower chance @@ -77,7 +77,7 @@ class CandyCollection(commands.Cog): return # check to ensure it is in correct channel - if message.channel.id != Channels.community_bot_commands: + if message.channel.id != Channels.sir_lancebot_playground: return # if its not a candy or skull, and it is one of 10 most recent messages, @@ -139,7 +139,7 @@ class CandyCollection(commands.Cog): @property def hacktober_channel(self) -> discord.TextChannel: """Get #hacktoberbot channel from its ID.""" - return self.bot.get_channel(Channels.community_bot_commands) + return self.bot.get_channel(Channels.sir_lancebot_playground) @staticmethod async def send_spook_msg( diff --git a/bot/exts/holidays/halloween/spookynamerate.py b/bot/exts/holidays/halloween/spookynamerate.py index a3aa4f13..02fb71c3 100644 --- a/bot/exts/holidays/halloween/spookynamerate.py +++ b/bot/exts/holidays/halloween/spookynamerate.py @@ -223,7 +223,7 @@ class SpookyNameRate(Cog): if self.first_time: await channel.send( "Okkey... Welcome to the **Spooky Name Rate Game**! It's a relatively simple game.\n" - f"Everyday, a random name will be sent in <#{Channels.community_bot_commands}> " + f"Everyday, a random name will be sent in <#{Channels.sir_lancebot_playground}> " "and you need to try and spookify it!\nRegister your name using " f"`{Client.prefix}spookynamerate add spookified name`" ) @@ -359,10 +359,10 @@ class SpookyNameRate(Cog): """Gets the sir-lancebot-channel after waiting until ready.""" await self.bot.wait_until_ready() channel = self.bot.get_channel( - Channels.community_bot_commands - ) or await self.bot.fetch_channel(Channels.community_bot_commands) + Channels.sir_lancebot_playground + ) or await self.bot.fetch_channel(Channels.sir_lancebot_playground) if not channel: - logger.warning("Bot is unable to get the #seasonalbot-commands channel. Please check the channel ID.") + logger.warning("Bot is unable to get the #sir-lancebot-playground channel. Please check the channel ID.") return channel @staticmethod diff --git a/bot/exts/holidays/pride/pride_facts.py b/bot/exts/holidays/pride/pride_facts.py index e6ef7108..340f0b43 100644 --- a/bot/exts/holidays/pride/pride_facts.py +++ b/bot/exts/holidays/pride/pride_facts.py @@ -30,7 +30,7 @@ class PrideFacts(commands.Cog): """Background task to post the daily pride fact every day.""" await self.bot.wait_until_guild_available() - channel = self.bot.get_channel(Channels.community_bot_commands) + channel = self.bot.get_channel(Channels.sir_lancebot_playground) await self.send_select_fact(channel, datetime.utcnow()) async def send_random_fact(self, ctx: commands.Context) -> None: diff --git a/bot/exts/holidays/pride/pride_leader.py b/bot/exts/holidays/pride/pride_leader.py index 298c9328..adf01134 100644 --- a/bot/exts/holidays/pride/pride_leader.py +++ b/bot/exts/holidays/pride/pride_leader.py @@ -83,7 +83,7 @@ class PrideLeader(commands.Cog): embed.add_field( name="For More Information", value=f"Do `{constants.Client.prefix}wiki {name}`" - f" in <#{constants.Channels.community_bot_commands}>", + f" in <#{constants.Channels.sir_lancebot_playground}>", inline=False ) embed.set_thumbnail(url=pride_leader["url"]) diff --git a/bot/exts/holidays/valentines/be_my_valentine.py b/bot/exts/holidays/valentines/be_my_valentine.py index 1572d474..cbb95157 100644 --- a/bot/exts/holidays/valentines/be_my_valentine.py +++ b/bot/exts/holidays/valentines/be_my_valentine.py @@ -70,7 +70,7 @@ class BeMyValentine(commands.Cog): raise commands.UserInputError("Come on, you can't send a valentine to yourself :expressionless:") emoji_1, emoji_2 = self.random_emoji() - channel = self.bot.get_channel(Channels.community_bot_commands) + channel = self.bot.get_channel(Channels.sir_lancebot_playground) valentine, title = self.valentine_check(valentine_type) embed = discord.Embed( diff --git a/bot/exts/holidays/valentines/lovecalculator.py b/bot/exts/holidays/valentines/lovecalculator.py index 99fba150..10dea9df 100644 --- a/bot/exts/holidays/valentines/lovecalculator.py +++ b/bot/exts/holidays/valentines/lovecalculator.py @@ -32,7 +32,7 @@ class LoveCalculator(Cog): Tells you how much the two love each other. This command requires at least one member as input, if two are given love will be calculated between - those two users, if only one is given, the second member is asusmed to be the invoker. + those two users, if only one is given, the second member is assumed to be the invoker. Members are converted from: - User ID - Mention @@ -51,7 +51,7 @@ class LoveCalculator(Cog): raise BadArgument( "This command can only be ran against members with the lovefest role! " "This role be can assigned by running " - f"`{PYTHON_PREFIX}subscribe` in <#{Channels.bot}>." + f"`{PYTHON_PREFIX}subscribe` in <#{Channels.bot_commands}>." ) if whom is None: diff --git a/bot/utils/checks.py b/bot/utils/checks.py index 8c426ed7..5433f436 100644 --- a/bot/utils/checks.py +++ b/bot/utils/checks.py @@ -33,7 +33,7 @@ def in_whitelist_check( channels: Container[int] = (), categories: Container[int] = (), roles: Container[int] = (), - redirect: Optional[int] = constants.Channels.community_bot_commands, + redirect: Optional[int] = constants.Channels.sir_lancebot_playground, fail_silently: bool = False, ) -> bool: """ diff --git a/bot/utils/decorators.py b/bot/utils/decorators.py index 7a3b14ad..8954e016 100644 --- a/bot/utils/decorators.py +++ b/bot/utils/decorators.py @@ -257,10 +257,10 @@ def whitelist_check(**default_kwargs: Container[int]) -> Callable[[Context], boo channels = set(kwargs.get("channels") or {}) categories = kwargs.get("categories") - # Only output override channels + community_bot_commands + # Only output override channels + sir_lancebot_playground if channels: default_whitelist_channels = set(WHITELISTED_CHANNELS) - default_whitelist_channels.discard(Channels.community_bot_commands) + default_whitelist_channels.discard(Channels.sir_lancebot_playground) channels.difference_update(default_whitelist_channels) # Add all whitelisted category channels, but skip if we're in DMs -- cgit v1.2.3 From 206b23cd02b020288b8897cc8eb30ab3a67a8250 Mon Sep 17 00:00:00 2001 From: Izan Date: Sun, 20 Feb 2022 11:31:59 +0000 Subject: Fix TypeError caused by adding a list to a tuple --- bot/exts/core/source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/core/source.py b/bot/exts/core/source.py index e9568933..2801be0f 100644 --- a/bot/exts/core/source.py +++ b/bot/exts/core/source.py @@ -15,7 +15,7 @@ class BotSource(commands.Cog): """Displays information about the bot's source code.""" @commands.command(name="source", aliases=("src",)) - @whitelist_override(channels=WHITELISTED_CHANNELS+[Channels.community_meta, Channels.dev_contrib]) + @whitelist_override(channels=WHITELISTED_CHANNELS+(Channels.community_meta, Channels.dev_contrib)) async def source_command(self, ctx: commands.Context, *, source_item: SourceConverter = None) -> None: """Display information and a GitHub link to the source code of a command, tag, or cog.""" if not source_item: -- cgit v1.2.3 From 58e33b1b3dbf623ac4e7796a392dd8446b809744 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 20 Feb 2022 12:20:24 +0000 Subject: Don't call bot.run() if IN_CI env var is set --- bot/__main__.py | 3 ++- bot/constants.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/__main__.py b/bot/__main__.py index 6889fe2b..0bf7b398 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -12,4 +12,5 @@ bot.add_check(whitelist_check(channels=WHITELISTED_CHANNELS, roles=STAFF_ROLES)) for ext in walk_extensions(): bot.load_extension(ext) -bot.run(Client.token) +if not Client.in_ci: + bot.run(Client.token) diff --git a/bot/constants.py b/bot/constants.py index b4d7bc24..5d876d97 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -143,6 +143,7 @@ class Client(NamedTuple): prefix = environ.get("PREFIX", ".") token = environ.get("BOT_TOKEN") debug = environ.get("BOT_DEBUG", "true").lower() == "true" + in_ci = environ.get("IN_CI", "false").lower() == "true" github_bot_repo = "https://github.com/python-discord/sir-lancebot" # Override seasonal locks: 1 (January) to 12 (December) month_override = int(environ["MONTH_OVERRIDE"]) if "MONTH_OVERRIDE" in environ else None -- cgit v1.2.3 From 95cf8824509461ec343795e752f3df6309fb8c7c Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 20 Feb 2022 12:21:45 +0000 Subject: Add a step during lint that inits the bot This step doesn't actually call bot.run(), and is designed to catch errors in imports/cog setup functions before they are merged. --- .github/workflows/lint.yaml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 14cfb702..2cbfc2f5 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -12,7 +12,7 @@ concurrency: jobs: lint: - name: Run pre-commit & flake8 + name: Run linting & tests runs-on: ubuntu-latest env: # List of licenses that are compatible with the MIT License and @@ -86,6 +86,14 @@ jobs: pip-licenses --allow-only="$ALLOWED_LICENSE" \ --package $(poetry export -f requirements.txt --without-hashes | sed "s/==.*//g" | tr "\n" " ") + # Attempt to run the bot. Setting `IN_CI` to true, so bot.run() is never called. + # This is to catch import and cog setup errors that may appear in PRs, to avoid crash loops if merged. + - name: Attempt bot setup + run: "python -m bot" + env: + USE_FAKEREDIS: true + IN_CI: true + # This step caches our pre-commit environment. To make sure we # do create a new environment when our pre-commit setup changes, # we create a cache key based on relevant factors. -- cgit v1.2.3 From c97d3e3759ca30aa67a0f73029fe91cab81a2d75 Mon Sep 17 00:00:00 2001 From: Leon SandΓΈy Date: Mon, 21 Feb 2022 15:26:39 +0000 Subject: Update Sir Lancebot's banner image to match new branding --- sir-lancebot-logo.png | Bin 65083 -> 122287 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/sir-lancebot-logo.png b/sir-lancebot-logo.png index fc606bf7..d8e28ad8 100644 Binary files a/sir-lancebot-logo.png and b/sir-lancebot-logo.png differ -- cgit v1.2.3 From f0501714374f50467b63f75be545625ff96b170a Mon Sep 17 00:00:00 2001 From: MaskDuck Date: Fri, 4 Mar 2022 15:58:07 +0700 Subject: truncate output. (#1041) --- bot/exts/core/internal_eval/_internal_eval.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bot/exts/core/internal_eval/_internal_eval.py b/bot/exts/core/internal_eval/_internal_eval.py index 5b5461f0..190a15ec 100644 --- a/bot/exts/core/internal_eval/_internal_eval.py +++ b/bot/exts/core/internal_eval/_internal_eval.py @@ -34,6 +34,8 @@ RAW_CODE_REGEX = re.compile( re.DOTALL # "." also matches newlines ) +MAX_LENGTH = 99980 + class InternalEval(commands.Cog): """Top secret code evaluation for admins and owners.""" @@ -85,9 +87,10 @@ class InternalEval(commands.Cog): async def _upload_output(self, output: str) -> Optional[str]: """Upload `internal eval` output to our pastebin and return the url.""" + data = self.shorten_output(output, max_length=MAX_LENGTH) try: async with self.bot.http_session.post( - "https://paste.pythondiscord.com/documents", data=output, raise_for_status=True + "https://paste.pythondiscord.com/documents", data=data, raise_for_status=True ) as resp: data = await resp.json() -- cgit v1.2.3 From d35e49cdb0d68e808d723c15d0f6cb82589be036 Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Sun, 13 Mar 2022 07:39:24 +0530 Subject: Handle Missing Logs in Latex API Response (#1036) Co-authored-by: Xithrius <15021300+Xithrius@users.noreply.github.com> --- bot/exts/fun/latex.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/bot/exts/fun/latex.py b/bot/exts/fun/latex.py index 1dc9c2e5..d43ec8c4 100644 --- a/bot/exts/fun/latex.py +++ b/bot/exts/fun/latex.py @@ -56,7 +56,7 @@ def _process_image(data: bytes, out_file: BinaryIO) -> None: class InvalidLatexError(Exception): """Represents an error caused by invalid latex.""" - def __init__(self, logs: str): + def __init__(self, logs: Optional[str]): super().__init__(logs) self.logs = logs @@ -73,7 +73,7 @@ class Latex(commands.Cog): async with self.bot.http_session.post(LATEX_API_URL, data=payload, raise_for_status=True) as response: response_json = await response.json() if response_json["status"] != "success": - raise InvalidLatexError(logs=response_json["log"]) + raise InvalidLatexError(logs=response_json.get("log")) async with self.bot.http_session.get( f"{LATEX_API_URL}/{response_json['filename']}", raise_for_status=True @@ -110,12 +110,15 @@ class Latex(commands.Cog): with open(image_path, "wb") as out_file: await self._generate_image(TEMPLATE.substitute(text=query), out_file) except InvalidLatexError as err: - logs_paste_url = await self._upload_to_pastebin(err.logs) embed = discord.Embed(title="Failed to render input.") - if logs_paste_url: - embed.description = f"[View Logs]({logs_paste_url})" + if err.logs is None: + embed.description = "No logs available." else: - embed.description = "Couldn't upload logs." + logs_paste_url = await self._upload_to_pastebin(err.logs) + if logs_paste_url: + embed.description = f"[View Logs]({logs_paste_url})" + else: + embed.description = "Couldn't upload logs." await ctx.send(embed=embed) image_path.unlink() return -- cgit v1.2.3 From a468379dec50a69b1932516e3ed66ce0f157fd2b Mon Sep 17 00:00:00 2001 From: Gustav Odinger <65498475+gustavwilliam@users.noreply.github.com> Date: Mon, 14 Mar 2022 01:56:00 +0100 Subject: Add Twemoji utility command (#988) Co-authored-by: Xithrius <15021300+Xithrius@users.noreply.github.com> --- bot/constants.py | 1 + bot/exts/utilities/twemoji.py | 150 ++++++++++++++++++++++++++++++++++++++++++ poetry.lock | 16 ++++- pyproject.toml | 1 + 4 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 bot/exts/utilities/twemoji.py diff --git a/bot/constants.py b/bot/constants.py index 5d876d97..da81a089 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -157,6 +157,7 @@ class Logging(NamedTuple): class Colours: blue = 0x0279FD + twitter_blue = 0x1DA1F2 bright_green = 0x01D277 dark_green = 0x1F8B4C orange = 0xE67E22 diff --git a/bot/exts/utilities/twemoji.py b/bot/exts/utilities/twemoji.py new file mode 100644 index 00000000..c915f05b --- /dev/null +++ b/bot/exts/utilities/twemoji.py @@ -0,0 +1,150 @@ +import logging +import re +from typing import Literal, Optional + +import discord +from discord.ext import commands +from emoji import UNICODE_EMOJI_ENGLISH, is_emoji + +from bot.bot import Bot +from bot.constants import Colours, Roles +from bot.utils.decorators import whitelist_override +from bot.utils.extensions import invoke_help_command + +log = logging.getLogger(__name__) +BASE_URLS = { + "png": "https://raw.githubusercontent.com/twitter/twemoji/master/assets/72x72/", + "svg": "https://raw.githubusercontent.com/twitter/twemoji/master/assets/svg/", +} +CODEPOINT_REGEX = re.compile(r"[a-f1-9][a-f0-9]{3,5}$") + + +class Twemoji(commands.Cog): + """Utilities for working with Twemojis.""" + + def __init__(self, bot: Bot): + self.bot = bot + + @staticmethod + def get_url(codepoint: str, format: Literal["png", "svg"]) -> str: + """Returns a source file URL for the specified Twemoji, in the corresponding format.""" + return f"{BASE_URLS[format]}{codepoint}.{format}" + + @staticmethod + def alias_to_name(alias: str) -> str: + """ + Transform a unicode alias to an emoji name. + + Example usages: + >>> alias_to_name(":falling_leaf:") + "Falling leaf" + >>> alias_to_name(":family_man_girl_boy:") + "Family man girl boy" + """ + name = alias.strip(":").replace("_", " ") + return name.capitalize() + + @staticmethod + def build_embed(codepoint: str) -> discord.Embed: + """Returns the main embed for the `twemoji` commmand.""" + emoji = "".join(Twemoji.emoji(e) or "" for e in codepoint.split("-")) + + embed = discord.Embed( + title=Twemoji.alias_to_name(UNICODE_EMOJI_ENGLISH[emoji]), + description=f"{codepoint.replace('-', ' ')}\n[Download svg]({Twemoji.get_url(codepoint, 'svg')})", + colour=Colours.twitter_blue, + ) + embed.set_thumbnail(url=Twemoji.get_url(codepoint, "png")) + return embed + + @staticmethod + def emoji(codepoint: Optional[str]) -> Optional[str]: + """ + Returns the emoji corresponding to a given `codepoint`, or `None` if no emoji was found. + + The return value is an emoji character, such as "πŸ‚". The `codepoint` + argument can be of any format, since it will be trimmed automatically. + """ + if code := Twemoji.trim_code(codepoint): + return chr(int(code, 16)) + + @staticmethod + def codepoint(emoji: Optional[str]) -> Optional[str]: + """ + Returns the codepoint, in a trimmed format, of a single emoji. + + `emoji` should be an emoji character, such as "🐍" and "πŸ₯°", and + not a codepoint like "1f1f8". When working with combined emojis, + such as "πŸ‡ΈπŸ‡ͺ" and "πŸ‘¨β€πŸ‘©β€πŸ‘¦", send the component emojis through the method + one at a time. + """ + if emoji is None: + return None + return hex(ord(emoji)).removeprefix("0x") + + @staticmethod + def trim_code(codepoint: Optional[str]) -> Optional[str]: + """ + Returns the meaningful information from the given `codepoint`. + + If no codepoint is found, `None` is returned. + + Example usages: + >>> trim_code("U+1f1f8") + "1f1f8" + >>> trim_code("\u0001f1f8") + "1f1f8" + >>> trim_code("1f466") + "1f466" + """ + if code := CODEPOINT_REGEX.search(codepoint or ""): + return code.group() + + @staticmethod + def codepoint_from_input(raw_emoji: tuple[str, ...]) -> str: + """ + Returns the codepoint corresponding to the passed tuple, separated by "-". + + The return format matches the format used in URLs for Twemoji source files. + + Example usages: + >>> codepoint_from_input(("🐍",)) + "1f40d" + >>> codepoint_from_input(("1f1f8", "1f1ea")) + "1f1f8-1f1ea" + >>> codepoint_from_input(("πŸ‘¨β€πŸ‘§β€πŸ‘¦",)) + "1f468-200d-1f467-200d-1f466" + """ + raw_emoji = [emoji.lower() for emoji in raw_emoji] + if is_emoji(raw_emoji[0]): + emojis = (Twemoji.codepoint(emoji) or "" for emoji in raw_emoji[0]) + return "-".join(emojis) + + emoji = "".join( + Twemoji.emoji(Twemoji.trim_code(code)) or "" for code in raw_emoji + ) + if is_emoji(emoji): + return "-".join(Twemoji.codepoint(e) or "" for e in emoji) + + raise ValueError("No codepoint could be obtained from the given input") + + @commands.command(aliases=("tw",)) + @whitelist_override(roles=(Roles.everyone,)) + async def twemoji(self, ctx: commands.Context, *raw_emoji: str) -> None: + """Sends a preview of a given Twemoji, specified by codepoint or emoji.""" + if len(raw_emoji) == 0: + await invoke_help_command(ctx) + return + try: + codepoint = self.codepoint_from_input(raw_emoji) + except ValueError: + raise commands.BadArgument( + "please include a valid emoji or emoji codepoint." + ) + + await ctx.send(embed=self.build_embed(codepoint)) + + +def setup(bot: Bot) -> None: + """Load the Twemoji cog.""" + bot.add_cog(Twemoji(bot)) diff --git a/poetry.lock b/poetry.lock index 68bfc43a..56eb53d9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -202,6 +202,17 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "emoji" +version = "1.6.3" +description = "Emoji for Python" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +dev = ["pytest", "coverage", "coveralls"] + [[package]] name = "emojis" version = "0.6.0" @@ -824,7 +835,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "e824a5fa909d43e861478178ad7e77ee04be05a60cd3028bda8bd4754c848616" +content-hash = "27075494e06333e5934e751f9847f419efb712b6d4d4e6173785d47319de1f29" [metadata.files] aiodns = [ @@ -975,6 +986,9 @@ distlib = [ {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, ] +emoji = [ + {file = "emoji-1.6.3.tar.gz", hash = "sha256:cc28bdc1010b1c03c241f69c7af1e8715144ef45a273bfadc14dc89319ba26d0"}, +] emojis = [ {file = "emojis-0.6.0-py3-none-any.whl", hash = "sha256:7da34c8a78ae262fd68cef9e2c78a3c1feb59784489eeea0f54ba1d4b7111c7c"}, {file = "emojis-0.6.0.tar.gz", hash = "sha256:bf605d1f1a27a81cd37fe82eb65781c904467f569295a541c33710b97e4225ec"}, diff --git a/pyproject.toml b/pyproject.toml index 7d3f0a5e..a72fa706 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ emojis = "~=0.6.0" coloredlogs = "~=15.0" colorama = { version = "~=0.4.3", markers = "sys_platform == 'win32'" } lxml = "~=4.6" +emoji = "^1.6.1" [tool.poetry.dev-dependencies] flake8 = "~=3.8" -- cgit v1.2.3 From 7fd74af261385bdf7d989f459bec4c9b0cb4392a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Mar 2022 01:00:06 +0000 Subject: Bump pillow from 9.0.0 to 9.0.1 (#1045) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 69 ++++++++++++++++++++++++++++++++----------------------------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/poetry.lock b/poetry.lock index 56eb53d9..5c52a535 100644 --- a/poetry.lock +++ b/poetry.lock @@ -485,7 +485,7 @@ flake8-polyfill = ">=1.0.2,<2" [[package]] name = "pillow" -version = "9.0.0" +version = "9.0.1" description = "Python Imaging Library (Fork)" category = "main" optional = false @@ -1252,38 +1252,41 @@ pep8-naming = [ {file = "pep8_naming-0.12.1-py2.py3-none-any.whl", hash = "sha256:4a8daeaeb33cfcde779309fc0c9c0a68a3bbe2ad8a8308b763c5068f86eb9f37"}, ] pillow = [ - {file = "Pillow-9.0.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:113723312215b25c22df1fdf0e2da7a3b9c357a7d24a93ebbe80bfda4f37a8d4"}, - {file = "Pillow-9.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bb47a548cea95b86494a26c89d153fd31122ed65255db5dcbc421a2d28eb3379"}, - {file = "Pillow-9.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31b265496e603985fad54d52d11970383e317d11e18e856971bdbb86af7242a4"}, - {file = "Pillow-9.0.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d154ed971a4cc04b93a6d5b47f37948d1f621f25de3e8fa0c26b2d44f24e3e8f"}, - {file = "Pillow-9.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fe92813d208ce8aa7d76da878bdc84b90809f79ccbad2a288e9bcbeac1d9bd"}, - {file = "Pillow-9.0.0-cp310-cp310-win32.whl", hash = "sha256:d5dcea1387331c905405b09cdbfb34611050cc52c865d71f2362f354faee1e9f"}, - {file = "Pillow-9.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:52abae4c96b5da630a8b4247de5428f593465291e5b239f3f843a911a3cf0105"}, - {file = "Pillow-9.0.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:72c3110228944019e5f27232296c5923398496b28be42535e3b2dc7297b6e8b6"}, - {file = "Pillow-9.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97b6d21771da41497b81652d44191489296555b761684f82b7b544c49989110f"}, - {file = "Pillow-9.0.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72f649d93d4cc4d8cf79c91ebc25137c358718ad75f99e99e043325ea7d56100"}, - {file = "Pillow-9.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aaf07085c756f6cb1c692ee0d5a86c531703b6e8c9cae581b31b562c16b98ce"}, - {file = "Pillow-9.0.0-cp37-cp37m-win32.whl", hash = "sha256:03b27b197deb4ee400ed57d8d4e572d2d8d80f825b6634daf6e2c18c3c6ccfa6"}, - {file = "Pillow-9.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a09a9d4ec2b7887f7a088bbaacfd5c07160e746e3d47ec5e8050ae3b2a229e9f"}, - {file = "Pillow-9.0.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:490e52e99224858f154975db61c060686df8a6b3f0212a678e5d2e2ce24675c9"}, - {file = "Pillow-9.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:500d397ddf4bbf2ca42e198399ac13e7841956c72645513e8ddf243b31ad2128"}, - {file = "Pillow-9.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ebd8b9137630a7bbbff8c4b31e774ff05bbb90f7911d93ea2c9371e41039b52"}, - {file = "Pillow-9.0.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd0e5062f11cb3e730450a7d9f323f4051b532781026395c4323b8ad055523c4"}, - {file = "Pillow-9.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f3b4522148586d35e78313db4db0df4b759ddd7649ef70002b6c3767d0fdeb7"}, - {file = "Pillow-9.0.0-cp38-cp38-win32.whl", hash = "sha256:0b281fcadbb688607ea6ece7649c5d59d4bbd574e90db6cd030e9e85bde9fecc"}, - {file = "Pillow-9.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5050d681bcf5c9f2570b93bee5d3ec8ae4cf23158812f91ed57f7126df91762"}, - {file = "Pillow-9.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:c2067b3bb0781f14059b112c9da5a91c80a600a97915b4f48b37f197895dd925"}, - {file = "Pillow-9.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2d16b6196fb7a54aff6b5e3ecd00f7c0bab1b56eee39214b2b223a9d938c50af"}, - {file = "Pillow-9.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98cb63ca63cb61f594511c06218ab4394bf80388b3d66cd61d0b1f63ee0ea69f"}, - {file = "Pillow-9.0.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc462d24500ba707e9cbdef436c16e5c8cbf29908278af053008d9f689f56dee"}, - {file = "Pillow-9.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3586e12d874ce2f1bc875a3ffba98732ebb12e18fb6d97be482bd62b56803281"}, - {file = "Pillow-9.0.0-cp39-cp39-win32.whl", hash = "sha256:68e06f8b2248f6dc8b899c3e7ecf02c9f413aab622f4d6190df53a78b93d97a5"}, - {file = "Pillow-9.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:6579f9ba84a3d4f1807c4aab4be06f373017fc65fff43498885ac50a9b47a553"}, - {file = "Pillow-9.0.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:47f5cf60bcb9fbc46011f75c9b45a8b5ad077ca352a78185bd3e7f1d294b98bb"}, - {file = "Pillow-9.0.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fd8053e1f8ff1844419842fd474fc359676b2e2a2b66b11cc59f4fa0a301315"}, - {file = "Pillow-9.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c5439bfb35a89cac50e81c751317faea647b9a3ec11c039900cd6915831064d"}, - {file = "Pillow-9.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95545137fc56ce8c10de646074d242001a112a92de169986abd8c88c27566a05"}, - {file = "Pillow-9.0.0.tar.gz", hash = "sha256:ee6e2963e92762923956fe5d3479b1fdc3b76c83f290aad131a2f98c3df0593e"}, + {file = "Pillow-9.0.1-1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5d24e1d674dd9d72c66ad3ea9131322819ff86250b30dc5821cbafcfa0b96b4"}, + {file = "Pillow-9.0.1-1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2632d0f846b7c7600edf53c48f8f9f1e13e62f66a6dbc15191029d950bfed976"}, + {file = "Pillow-9.0.1-1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9618823bd237c0d2575283f2939655f54d51b4527ec3972907a927acbcc5bfc"}, + {file = "Pillow-9.0.1-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:9bfdb82cdfeccec50aad441afc332faf8606dfa5e8efd18a6692b5d6e79f00fd"}, + {file = "Pillow-9.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5100b45a4638e3c00e4d2320d3193bdabb2d75e79793af7c3eb139e4f569f16f"}, + {file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:528a2a692c65dd5cafc130de286030af251d2ee0483a5bf50c9348aefe834e8a"}, + {file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f29d831e2151e0b7b39981756d201f7108d3d215896212ffe2e992d06bfe049"}, + {file = "Pillow-9.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:855c583f268edde09474b081e3ddcd5cf3b20c12f26e0d434e1386cc5d318e7a"}, + {file = "Pillow-9.0.1-cp310-cp310-win32.whl", hash = "sha256:d9d7942b624b04b895cb95af03a23407f17646815495ce4547f0e60e0b06f58e"}, + {file = "Pillow-9.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:81c4b81611e3a3cb30e59b0cf05b888c675f97e3adb2c8672c3154047980726b"}, + {file = "Pillow-9.0.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:413ce0bbf9fc6278b2d63309dfeefe452835e1c78398efb431bab0672fe9274e"}, + {file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80fe64a6deb6fcfdf7b8386f2cf216d329be6f2781f7d90304351811fb591360"}, + {file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cef9c85ccbe9bee00909758936ea841ef12035296c748aaceee535969e27d31b"}, + {file = "Pillow-9.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d19397351f73a88904ad1aee421e800fe4bbcd1aeee6435fb62d0a05ccd1030"}, + {file = "Pillow-9.0.1-cp37-cp37m-win32.whl", hash = "sha256:d21237d0cd37acded35154e29aec853e945950321dd2ffd1a7d86fe686814669"}, + {file = "Pillow-9.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ede5af4a2702444a832a800b8eb7f0a7a1c0eed55b644642e049c98d589e5092"}, + {file = "Pillow-9.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:b5b3f092fe345c03bca1e0b687dfbb39364b21ebb8ba90e3fa707374b7915204"}, + {file = "Pillow-9.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:335ace1a22325395c4ea88e00ba3dc89ca029bd66bd5a3c382d53e44f0ccd77e"}, + {file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db6d9fac65bd08cea7f3540b899977c6dee9edad959fa4eaf305940d9cbd861c"}, + {file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f154d173286a5d1863637a7dcd8c3437bb557520b01bddb0be0258dcb72696b5"}, + {file = "Pillow-9.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d4b1341ac07ae07eb2cc682f459bec932a380c3b122f5540432d8977e64eae"}, + {file = "Pillow-9.0.1-cp38-cp38-win32.whl", hash = "sha256:effb7749713d5317478bb3acb3f81d9d7c7f86726d41c1facca068a04cf5bb4c"}, + {file = "Pillow-9.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:7f7609a718b177bf171ac93cea9fd2ddc0e03e84d8fa4e887bdfc39671d46b00"}, + {file = "Pillow-9.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:80ca33961ced9c63358056bd08403ff866512038883e74f3a4bf88ad3eb66838"}, + {file = "Pillow-9.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c3c33ac69cf059bbb9d1a71eeaba76781b450bc307e2291f8a4764d779a6b28"}, + {file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12875d118f21cf35604176872447cdb57b07126750a33748bac15e77f90f1f9c"}, + {file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:514ceac913076feefbeaf89771fd6febde78b0c4c1b23aaeab082c41c694e81b"}, + {file = "Pillow-9.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3c5c79ab7dfce6d88f1ba639b77e77a17ea33a01b07b99840d6ed08031cb2a7"}, + {file = "Pillow-9.0.1-cp39-cp39-win32.whl", hash = "sha256:718856856ba31f14f13ba885ff13874be7fefc53984d2832458f12c38205f7f7"}, + {file = "Pillow-9.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:f25ed6e28ddf50de7e7ea99d7a976d6a9c415f03adcaac9c41ff6ff41b6d86ac"}, + {file = "Pillow-9.0.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:011233e0c42a4a7836498e98c1acf5e744c96a67dd5032a6f666cc1fb97eab97"}, + {file = "Pillow-9.0.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253e8a302a96df6927310a9d44e6103055e8fb96a6822f8b7f514bb7ef77de56"}, + {file = "Pillow-9.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6295f6763749b89c994fcb6d8a7f7ce03c3992e695f89f00b741b4580b199b7e"}, + {file = "Pillow-9.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a9f44cd7e162ac6191491d7249cceb02b8116b0f7e847ee33f739d7cb1ea1f70"}, + {file = "Pillow-9.0.1.tar.gz", hash = "sha256:6c8bc8238a7dfdaf7a75f5ec5a663f4173f8c367e5a39f87e720495e1eed75fa"}, ] pip-licenses = [ {file = "pip-licenses-3.5.3.tar.gz", hash = "sha256:f44860e00957b791c6c6005a3328f2d5eaeee96ddb8e7d87d4b0aa25b02252e4"}, -- cgit v1.2.3