From 4cf95e15266731221d13c54315f165923806cc7f Mon Sep 17 00:00:00 2001 From: Amndeep Singh Mann Date: Thu, 29 Sep 2022 03:08:21 -0400 Subject: [PATCH 1/6] Remove some unused code --- roboragi_old/AniDB.py | 83 -- roboragi_old/Anilist.py | 372 -------- roboragi_old/AnimeBot.py | 367 -------- roboragi_old/AnimePlanet.py | 112 --- roboragi_old/CommentBuilder.py | 1407 ---------------------------- roboragi_old/Config.py.example | 18 - roboragi_old/DatabaseHandler.py | 605 ------------ roboragi_old/Discord.py | 28 - roboragi_old/DiscordoragiSearch.py | 482 ---------- roboragi_old/Hummingbird.py | 69 -- roboragi_old/LNDB.py | 67 -- roboragi_old/MAL.py | 412 -------- roboragi_old/MU.py | 145 --- roboragi_old/NU.py | 71 -- roboragi_old/PreCache.py | 82 -- roboragi_old/Reference.py | 24 - roboragi_old/Wikipedia.py | 62 -- roboragi_old/reference.db | Bin 3072 -> 0 bytes roboragi_old/synonyms.db | Bin 118784 -> 0 bytes 19 files changed, 4406 deletions(-) delete mode 100644 roboragi_old/AniDB.py delete mode 100644 roboragi_old/Anilist.py delete mode 100644 roboragi_old/AnimeBot.py delete mode 100644 roboragi_old/AnimePlanet.py delete mode 100644 roboragi_old/CommentBuilder.py delete mode 100644 roboragi_old/Config.py.example delete mode 100644 roboragi_old/DatabaseHandler.py delete mode 100644 roboragi_old/Discord.py delete mode 100644 roboragi_old/DiscordoragiSearch.py delete mode 100644 roboragi_old/Hummingbird.py delete mode 100644 roboragi_old/LNDB.py delete mode 100644 roboragi_old/MAL.py delete mode 100644 roboragi_old/MU.py delete mode 100644 roboragi_old/NU.py delete mode 100644 roboragi_old/PreCache.py delete mode 100644 roboragi_old/Reference.py delete mode 100644 roboragi_old/Wikipedia.py delete mode 100644 roboragi_old/reference.db delete mode 100644 roboragi_old/synonyms.db diff --git a/roboragi_old/AniDB.py b/roboragi_old/AniDB.py deleted file mode 100644 index f32dfa0..0000000 --- a/roboragi_old/AniDB.py +++ /dev/null @@ -1,83 +0,0 @@ -''' -AniDB.py -Handles all AniDB information -''' - -from pyquery import PyQuery as pq -import aiohttp -import urllib -import difflib -import traceback -import pprint - -session = aiohttp.ClientSession() - -async def getAnimeURL(searchText): - cleanSearchText = urllib.parse.quote(searchText) - try: - async with session.get('http://anisearch.outrance.pl/?task=search&query=' + cleanSearchText, timeout=10) as resp: - html = await resp.read() - anidb = pq(html) - except: - traceback.print_exc() - return None - - animeList = [] - - for anime in anidb('animetitles anime'): - titles = [] - for title in pq(anime).find('title').items(): - titleInfo = {} - titleInfo['title'] = title.text() - titleInfo['lang'] = title.attr['lang'] - titles.append(titleInfo) - - url = 'http://anidb.net/a' + anime.attrib['aid'] - - if titles: - data = { 'titles': titles, - 'url': url - } - - animeList.append(data) - - closest = getClosestAnime(searchText, animeList) - - if closest: - return closest['url'] - else: - return None - -def getAnimeURLById(animeId): - return 'http://anidb.net/a' + str(animeId) - -def getClosestAnime(searchText, animeList): - nameList = [] - - trustedNames = [] #i.e. English/default names - untrustedNames = [] #everything else (French, Italian etc) - - for anime in animeList: - for title in anime['titles']: - if title['lang'].lower() in ['x-jat', 'en']: - trustedNames.append(title['title'].lower()) - else: - untrustedNames.append(title['title'].lower()) - - closestNameFromList = difflib.get_close_matches(searchText.lower(), trustedNames, 1, 0.85) - - if closestNameFromList: - for anime in animeList: - for title in anime['titles']: - if closestNameFromList[0].lower() == title['title'].lower() and title['lang'].lower() in ['x-jat', 'en']: - return anime - else: - closestNameFromList = difflib.get_close_matches(searchText.lower(), untrustedNames, 1, 0.85) - - if closestNameFromList: - for anime in animeList: - for title in anime['titles']: - if closestNameFromList[0].lower() == title['title'].lower() and title['lang'].lower() not in ['x-jat', 'en']: - return anime - - return None diff --git a/roboragi_old/Anilist.py b/roboragi_old/Anilist.py deleted file mode 100644 index 96e1599..0000000 --- a/roboragi_old/Anilist.py +++ /dev/null @@ -1,372 +0,0 @@ - -""" -Anilist.py -Handles all of the connections to Anilist. -""" -import DatabaseHandler -import aiohttp -import urllib -import difflib -import traceback -import pprint -import asyncio -ANICLIENT = '' -ANISECRET = '' - -session = aiohttp.ClientSession() - -try: - import Config - ANICLIENT = Config.aniclient - ANISECRET = Config.anisecret -except ImportError: - pass - -access_token = '' - -escape_table = { - "&": " ", - "\'": "\\'", - '\"': '\\"', - '/': ' ', - '-': ' ' - #'!': '\!' - } - -#Anilist's database doesn't like weird symbols when searching it, so you have to escape or replace a bunch of stuff. -def escape(text): - return "".join(escape_table.get(c,c) for c in text) - -def getSynonyms(request): - synonyms = [] - - synonyms.append(request['title_english']) if request['title_english'] else None - synonyms.append(request['title_romaji']) if request['title_romaji'] else None - synonyms.extend(request['synonyms']) if request['synonyms'] else None - - return synonyms - -#Sets up the connection to Anilist. You need a token to get stuff from them, which expires every hour. -async def setup(): - print('Setting up AniList') - loop = asyncio.get_event_loop() - try: - async with session.post('https://anilist.co/api/auth/access_token', params={'grant_type':'client_credentials', 'client_id':ANICLIENT, 'client_secret':ANISECRET}) as resp: - request = await resp.json() - global access_token - access_token = request['access_token'] - except Exception as e: - print('Error getting Anilist token: '+ e) - -#Returns the closest anime (as a Json-like object) it can find using the given searchtext -async def getAnimeDetails(searchText): - cachedAnime = DatabaseHandler.checkForMalEntry('anilistanime', searchText) - if cachedAnime is not None: - if cachedAnime['update']: - print("found cached anime, needs update in anilist") - return await getAnimeDetailsById(cachedAnime['id']) - else: - print("found cached anime, doesn't need update in anilist") - return cachedAnime['content'] - try: - #htmlSearchText = escape(searchText) - htmlSearchText = urllib.parse.quote(searchText) - async with session.get("https://anilist.co/api/anime/search/" + htmlSearchText, params={'access_token':access_token}, timeout=10) as resp: - if resp.status != 200: - await setup() - request = await session.get("https://anilist.co/api/anime/search/" + htmlSearchText, params={'access_token':access_token}, timeout=10) - - request = await resp.json() - - #Of the given list of shows, we try to find the one we think is closest to our search term - closestAnime = getClosestAnime(searchText, request) - - if closestAnime: - fullDetails = await getFullAnimeDetails(closestAnime['id']) - return fullDetails - else: - return None - - except Exception as e: - traceback.print_exc() - return None - -#Returns the anime details based on an id -async def getAnimeDetailsById(animeID): - try: - return await getFullAnimeDetails(animeID) - except Exception as e: - return None - -#Gets the "full" anime details (which aren't displayed when we search using the basic function). Gives us cool data like time until the next episode is aired. -async def getFullAnimeDetails(animeID): - try: - async with session.get("https://anilist.co/api/anime/" + str(animeID), params={'access_token':access_token}, timeout=10) as resp: - if resp.status != 200: - await setup() - resp = await session.get("https://anilist.co/api/anime/" + str(animeID), params={'access_token':access_token}, timeout=10) - - - if resp.status == 200: - request = await resp.json() - request['genres'] = [genre for genre in request['genres'] if genre] - request['synonyms'] = [synonym for synonym in request['synonyms'] if synonym] - - return request - else: - return None - except Exception as e: - print("Error finding anime:{} in anilist.\nError:{}".format(searchText, e)) - #traceback.print_exc() - return None - -#Given a list, it finds the closest anime series it can. -def getClosestAnime(searchText, animeList): - try: - animeNameList = [] - animeNameListNoSyn = [] - - #For each anime series, add all the titles/synonyms to an array and do a fuzzy string search to find the one closest to our search text. - #We also fill out an array that doesn't contain the synonyms. This is to protect against shows with multiple adaptations and similar synonyms (e.g. Haiyore Nyaruko-San) - for anime in animeList: - if 'title_english' in anime: - animeNameList.append(anime['title_english'].lower()) - animeNameListNoSyn.append(anime['title_english'].lower()) - - if 'title_romaji' in anime: - animeNameList.append(anime['title_romaji'].lower()) - animeNameListNoSyn.append(anime['title_romaji'].lower()) - - if 'synonyms' in anime: - for synonym in anime['synonyms']: - animeNameList.append(synonym.lower()) - - closestNameFromList = difflib.get_close_matches(searchText.lower(), animeNameList, 1, 0.95)[0] - - for anime in animeList: - if (anime['title_english'].lower() == closestNameFromList.lower()) or (anime['title_romaji'].lower() == closestNameFromList.lower()): - return anime - else: - for synonym in anime['synonyms']: - if (synonym.lower() == closestNameFromList.lower()) and (synonym.lower() not in animeNameListNoSyn): - return anime - - return None - except Exception as e: - print("Error finding anime:{} in anilist.\nError:{}".format(searchText, e)) - #traceback.print_exc() - return None - -#Makes a search for a manga series using a specific author -async def getMangaWithAuthor(searchText, authorName): - try: - - async with session.get("https://anilist.co/api/manga/search/" + searchText, params={'access_token':access_token}, timeout=10) as resp: - if resp.status !=200: - await setup() - resp = await session.get("https://anilist.co/api/manga/search/" + searchText, params={'access_token':access_token}, timeout=10) - - - request = await resp.json() - closestManga = getListOfCloseManga(searchText, request) - fullMangaList = [] - - for manga in closestManga: - try: - async with session.get("https://anilist.co/api/manga/" + str(manga['id']) + "/staff", params={'access_token':access_token}, timeout=10) as fullManga: - if fullManga.status !=200: - await setup() - fullManga = await session.get("https://anilist.co/api/manga/" + str(manga['id']) + "/staff", params={'access_token':access_token}, timeout=10) - - fullMangaJson = await fullManga.json() - fullMangaList.append(fullMangaJson) - except: - pass - - potentialHits = [] - for manga in fullMangaList: - for staff in manga['staff']: - isRightName = True - fullStaffName = staff['name_first'] + ' ' + staff['name_last'] - authorNamesSplit = authorName.split(' ') - - for name in authorNamesSplit: - if not (name.lower() in fullStaffName.lower()): - isRightName = False - - if isRightName: - potentialHits.append(manga) - - if potentialHits: - return getClosestManga(searchText, potentialHits) - - return None - - except Exception as e: - traceback.print_exc() - return None - -async def getLightNovelDetails(searchText): - return await getMangaDetails(searchText, True) - -#Returns the closest manga series given a specific search term -async def getMangaDetails(searchText, isLN=False): - cachedAnime = DatabaseHandler.checkForMalEntry('anilistmanga', searchText, isLN) - if cachedAnime is not None: - if cachedAnime['update']: - print("found cached anime, needs update in anilist") - return await getMangaDetailsById(cachedAnime['id']) - else: - print("found cached anime, doesn't need update in anilist") - return cachedAnime['content'] - try: - async with session.get("https://anilist.co/api/manga/search/" + searchText, params={'access_token':access_token}, timeout=10) as resp: - if resp.status != 200: - await setup() - resp = await session.get("https://anilist.co/api/manga/search/" + searchText, params={'access_token':access_token}, timeout=10) - - request = await resp.json() - closestManga = getClosestManga(searchText, request, isLN) - - if (closestManga is not None): - response = await session.get("https://anilist.co/api/manga/" + str(closestManga['id']), params={'access_token':access_token}, timeout=10) - json = await response.json() - - json['genres'] = [genre for genre in json['genres'] if genre] - json['synonyms'] = [synonym for synonym in json['synonyms'] if synonym] - - return json - else: - return None - - except Exception as e: - print("Error finding manga:{} in anilist.\nError:{}".format(searchText, e)) - #traceback.print_exc() - return None - -#Returns the closest manga series given an id -async def getMangaDetailsById(mangaId): - try: - async with session.get("https://anilist.co/api/manga/" + str(mangaId), params={'access_token':access_token}, timeout=10) as resp: - request = await resp.json() - return request - except Exception as e: - - return None - -#Used to determine the closest manga to a given search term in a list -def getListOfCloseManga(searchText, mangaList): - try: - ratio = 0.90 - returnList = [] - - for manga in mangaList: - alreadyExists = False - for thing in returnList: - if int(manga['id']) == int(thing['id']): - alreadyExists = True - break - if (alreadyExists): - continue - - if round(difflib.SequenceMatcher(lambda x: x == "", manga['title_english'].lower(), searchText.lower()).ratio(), 3) >= ratio: - returnList.append(manga) - elif round(difflib.SequenceMatcher(lambda x: x == "", manga['title_romaji'].lower(), searchText.lower()).ratio(), 3) >= ratio: - returnList.append(manga) - elif not (manga['synonyms'] is None): - for synonym in manga['synonyms']: - if round(difflib.SequenceMatcher(lambda x: x == "", synonym.lower(), searchText.lower()).ratio(), 3) >= ratio: - returnList.append(manga) - break - return returnList - except Exception as e: - traceback.print_exc() - return None - -#Used to determine the closest manga to a given search term in a list -def getClosestManga(searchText, mangaList, isLN=False): - try: - mangaNameList = [] - - for manga in mangaList: - if isLN and 'novel' not in manga['type'].lower(): - mangaList.remove(manga) - elif not isLN and 'novel' in manga['type'].lower(): - mangaList.remove(manga) - - for manga in mangaList: - if isLN and 'novel' not in manga['type'].lower(): - mangaList.remove(manga) - elif not isLN and 'novel' in manga['type'].lower(): - mangaList.remove(manga) - - for manga in mangaList: - mangaNameList.append(manga['title_english'].lower()) - mangaNameList.append(manga['title_romaji'].lower()) - - for synonym in manga['synonyms']: - mangaNameList.append(synonym.lower()) - - closestNameFromList = difflib.get_close_matches(searchText.lower(), mangaNameList, 1, 0.90)[0] - - for manga in mangaList: - if not ('one shot' in manga['type'].lower()): - if (manga['title_english'].lower() == closestNameFromList.lower()) or (manga['title_romaji'].lower() == closestNameFromList.lower()): - return manga - - for manga in mangaList: - for synonym in manga['synonyms']: - if synonym.lower() == closestNameFromList.lower(): - return manga - - return None - except Exception as e: - print("Error finding manga:{} in anilist.\nError:{}".format(searchText, e)) - #traceback.print_exc() - return None - -################################THESE ARE FOR POPULATING THE CACHE ##################################### -async def getGenres(medium): - try: - async with session.get("https://anilist.co/api/genre_list/".format(medium), params={'access_token':access_token}, timeout=10)as resp: - return await resp.json() - - except Exception as e: - print(e) - return None - -async def GetTop40ByGenre(medium, genre): - try: - async with session.get("https://anilist.co/api/browse/{}".format(medium), params={'access_token':access_token, 'genres':genre, 'sort':'popularity'}, timeout=10) as resp: - if resp.status != 200: - await setup() - resp = await session.get("https://anilist.co/api/manga/search/" + searchText, params={'access_token':access_token}, timeout=10) - if resp.status != 200: - print("Failed to get api info error code {}".format(resp.status)) - - request = await resp.json() - return request - except Exception as e: - print(e) - return None - -# Returns a json with the 40 anime from the 'page' of populartiy -async def get_page_by_popularity(medium, page): - try: - async with session.get("https://anilist.co/api/browse/{}".format(medium), params={'access_token':access_token, 'sort':'popularity-desc', 'page': page}, timeout=10) as resp: - if resp.status != 200: - await setup() - resp = await session.get("https://anilist.co/api/browse/{}".format(medium), params={'access_token':access_token, 'sort':'popularity', 'page': page}, timeout=10) - if resp.status != 200: - print("Failed to get ani-api info error code {}".format(resp.status)) - - request = await resp.json() - return request - pass - except Exception as e: - print(e) - return None - - -loop = asyncio.get_event_loop() -loop.run_until_complete(setup()) diff --git a/roboragi_old/AnimeBot.py b/roboragi_old/AnimeBot.py deleted file mode 100644 index 5d8d059..0000000 --- a/roboragi_old/AnimeBot.py +++ /dev/null @@ -1,367 +0,0 @@ -''' -AnimeBot.py -Acts as the "main" file and ties all the other functionality together. -''' - -import asyncio -import re -import traceback -import requests -import time - -import discord -import Discord -import DiscordoragiSearch -import CommentBuilder -import DatabaseHandler -import Config -import Reference - -#the servers where expanded requests are disabled -disableexpanded = [''] -async_queue = asyncio.Queue(maxsize = 32) -ownerID = '164546159140929538' - -@Discord.client.event -async def on_ready(): - print('Logged in as') - print(Discord.client.user.name) - print(Discord.client.user.id) - print('------') - -@Discord.client.event -async def on_server_join(server): - DatabaseHandler.addServerToDatabase(server.id) - print("Added server {} to database".format(server.id)) - -async def process_message(message, is_edit=False): - #Anime/Manga requests that are found go into separate arrays - animeArray = [] - mangaArray = [] - lnArray = [] - - #Checks if bot has permissions to embed - if message.channel.type != discord.ChannelType.private: - canEmbed = message.channel.server.default_role.permissions.embed_links - else: - canEmbed = True - if not canEmbed: - botMember = Discord.getMemberFromID(Config.clientid, message.server) - defaultroleperm = botMember.top_role.permissions - canEmbed = defaultroleperm.embed_links - - - isAdmin = message.author.top_role.permissions.administrator - isServerMod = message.author.top_role.permissions.manage_server - isOwner = message.author.id == ownerID - - if message.author.bot: - return - - - #ignores all "code" markup (i.e. anything between backticks) - preCleanMessage = re.sub(r"\`(.*?)\`", "", message.clean_content) - cleanMessage = re.sub(r'<:.+?:([0-9]{15,21})>', "", preCleanMessage) - messageReply = '' - - if re.search('({!help.*?}|{{!help.*?}}||<>)', cleanMessage, re.S) is not None: - try: - localEm = CommentBuilder.buildHelpEmbed() - await Discord.client.send_message(message.channel, embed = localEm) - return - except: - return - - if re.search('({!command.*?}|{{!command.*?}}||<>)', cleanMessage, re.S) is not None: - if 'toggleexpanded' in cleanMessage.lower() and (isAdmin or isServerMod): - try: - allowedStatus = DatabaseHandler.toggleAllowExpanded(message.server.id) - print("Toggled allowExpanded for server {}".format(message.server.id)) - if allowedStatus.lower() == 'true': - await Discord.client.send_message(message.channel, "Expanded requests are now allowed.") - else: - await Discord.client.send_message(message.channel, "Expanded requests are now disallowed.") - return - except Exception as e: - print(e) - return - - if 'addserver' in cleanMessage.lower() and (isOwner == True): - try: - DatabaseHandler.addServerToDatabase(message.server.id) - await Discord.client.send_message(message.channel, "Server has been added.") - return - except Exception as e: - print(e) - return - - else: - print("command failed, user probably has insufficient rights") - return - - - sender = re.search('[@]([A-Za-z0-9 _-]+?)(>|}|$)', cleanMessage, re.S) - mentionArray = message.raw_mentions - if re.search('({!stats.*?}|{{!stats.*?}}||<>)', cleanMessage, re.S) is not None and sender is not None: - for mention in mentionArray: - if not canEmbed: - messageReply = CommentBuilder.buildStatsComment(server=message.server, username=mention) - else: - localEm = CommentBuilder.buildStatsEmbed(server=message.server, username=mention) - await Discord.client.send_message(message.channel, embed=localEm) - return None - if re.search('({!sstats}|{{!sstats}}||<>)', cleanMessage, re.S) is not None: - if not canEmbed: - messageReply = CommentBuilder.buildStatsComment(server = message.server) - else: - localEm = CommentBuilder.buildStatsEmbed(server = message.server) - await Discord.client.send_message(message.channel, embed=localEm) - return None - elif re.search('({!stats.*?}|{{!stats.*?}}||<>)', cleanMessage, re.S) is not None: - if not canEmbed: - messageReply = CommentBuilder.buildStatsComment() - else: - localEm = CommentBuilder.buildStatsEmbed() - await Discord.client.send_message(message.channel, embed=localEm) - return None - else: - - #The basic algorithm here is: - #If it's an expanded request, build a reply using the data in the braces, clear the arrays, add the reply to the relevant array and ignore everything else. - #If it's a normal request, build a reply using the data in the braces, add the reply to the relevant array. - - #Counts the number of expanded results vs total results. If it's not just a single expanded result, they all get turned into normal requests. - numOfRequest = 0 - numOfExpandedRequest = 0 - forceNormal = False - expandedAllowed = DatabaseHandler.checkServerConfig('allowexpanded', message.server.id) - if expandedAllowed == False: - forceNormal = True - for match in re.finditer("\{{2}([^}]*)\}{2}|\<{2}([^>]*)\>{2}", cleanMessage, re.S): - numOfRequest += 1 - numOfExpandedRequest += 1 - print("Request found: {}".format(match.group(0))) - - for match in re.finditer("(?<=(?]*)(?=\>(?!\>))", cleanMessage, re.S): - numOfRequest += 1 - print("Request found: {}".format(match.group(0))) - - if (numOfExpandedRequest >= 1) and (numOfRequest > 1): - forceNormal = True - - #if numOfRequest != 0: - #await Discord.client.send_typing(message.channel) - #Expanded Anime - for match in re.finditer("\{{2}([^}]*)\}{2}", cleanMessage, re.S): - reply = '' - if match.group(1) != '': - if (forceNormal) or (str(message.channel).lower() in disableexpanded): - reply = await DiscordoragiSearch.buildAnimeReply(match.group(1), message, False, canEmbed) - else: - reply = await DiscordoragiSearch.buildAnimeReply(match.group(1), message, True, canEmbed) - - if (reply is not None): - animeArray.append(reply) - else: - print("Empty request, ignoring") - - #Normal Anime - for match in re.finditer("(?<=(?]*)\>{2}(?!(:|\>))", cleanMessage, re.S): - if match.group(1) != '': - reply = '' - - if (forceNormal) or (str(message.channel).lower() in disableexpanded): - reply = await DiscordoragiSearch.buildMangaReply(match.group(1), message, False, canEmbed) - else: - reply = await DiscordoragiSearch.buildMangaReply(match.group(1), message, True, canEmbed) - - if (reply is not None): - mangaArray.append(reply) - else: - print("Empty request, ignoring") - - #AUTHOR SEARCH EXPANDED - for match in re.finditer("\<{2}([^>]*)\>{2}:\(([^)]+)\)", cleanMessage, re.S): - if match.group(1) != '': - reply = '' - - if (forceNormal) or (str(message.server).lower() in disableexpanded): - reply = await DiscordoragiSearch.buildMangaReplyWithAuthor(match.group(1), match.group(2), message, False, canEmbed) - else: - reply = await DiscordoragiSearch.buildMangaReplyWithAuthor(match.group(1), match.group(2), message, True, canEmbed) - - if (reply is not None): - mangaArray.append(reply) - else: - print("Empty request, ignoring") - - #Normal Manga - #NORMAL - for match in re.finditer("(?<=(?]+)\>(?!(:|\>))", cleanMessage, re.S): - if match.group(1) != '': - reply = await DiscordoragiSearch.buildMangaReply(match.group(1), message, False, canEmbed) - - if (reply is not None): - mangaArray.append(reply) - else: - print("Empty request, ignoring") - - #AUTHOR SEARCH - for match in re.finditer("(?<=(?]*)\>:\(([^)]+)\)", cleanMessage, re.S): - reply = await DiscordoragiSearch.buildMangaReplyWithAuthor(match.group(1), match.group(2), message, False, canEmbed) - - if (reply is not None): - mangaArray.append(reply) - - #Expanded LN - for match in re.finditer("\]{2}([^]]*)\[{2}", cleanMessage, re.S): - if match.group(1) != '': - reply = '' - - if (forceNormal) or (str(message.server).lower() in disableexpanded): - reply = await DiscordoragiSearch.buildLightNovelReply(match.group(1), False, message, canEmbed) - else: - reply = await DiscordoragiSearch.buildLightNovelReply(match.group(1), True, message, canEmbed) - - if (reply is not None): - lnArray.append(reply) - else: - print("Empty request, ignoring") - - #Normal LN - for match in re.finditer("(?<=(?', then recombine them - for i, animeReply in enumerate(animeArray): - if not (i is 0): - messageReply += '\n\n' - if not (animeReply['title'] in postedAnimeTitles): - postedAnimeTitles.append(animeReply['title']) - if not canEmbed: - messageReply += animeReply['comment'] - else: - messageReply = 'n/a' - if mangaArray: - messageReply += '\n\n' - #Adding all the manga to the final message - for i, mangaReply in enumerate(mangaArray): - if not (i is 0): - messageReply += '\n\n' - if not (mangaReply['title'] in postedMangaTitles): - postedMangaTitles.append(mangaReply['title']) - if not canEmbed: - messageReply += mangaReply['comment'] - else: - messageReply = 'n/a' - if lnArray: - messageReply += '\n\n' - #Adding all the manga to the final comment - for i, lnReply in enumerate(lnArray): - if not (i is 0): - commentReply += '\n\n' - - if not (lnReply['title'] in postedLNTitles): - postedLNTitles.append(lnReply['title']) - if not canEmbed: - messageReply += lnReply['comment'] - else: - messageReply = 'N/A' - #If there are more than 10 requests, shorten them all - if not (messageReply is '') and (len(animeArray) + len(mangaArray) >= 10): - messageReply = re.sub(r"\^\((.*?)\)", "", messageReply, flags=re.M) - #If there was actually something found, add the signature and post the message to Reddit. Then, add the message to the "already seen" database. - if not (messageReply is ''): - - if is_edit: - if not canEmbed: - await Discord.client.send_message(message.channel, messageReply) - else: - for i, animeReply in enumerate(animeArray): - await Discord.client.send_message(message.channel, embed=animeReply['embed']) - for i, mangaReply in enumerate(mangaArray): - await Discord.client.send_message(message.channel, embed=mangaReply['embed']) - for i, lnReply in enumerate(lnArray): - await Discord.client.send_message(message.channel, embed=lnReply['embed']) - else: - try: - print("Message created.\n") - if not canEmbed: - await Discord.client.send_message(message.channel, messageReply) - else: - for i, animeReply in enumerate(animeArray): - await Discord.client.send_message(message.channel, embed=animeReply['embed']) - for i, mangaReply in enumerate(mangaArray): - await Discord.client.send_message(message.channel, embed=mangaReply['embed']) - for i, lnReply in enumerate(lnArray): - await Discord.client.send_message(message.channel, embed=lnReply['embed']) - except discord.errors.Forbidden: - print('Request from banned channel: ' + str(message.channel) + '\n') - except Exception as e: - print(e) - traceback.print_exc() - except: - traceback.print_exc() - else: - try: - if is_edit: - return None - else: - DatabaseHandler.addMessage(message.id, message.author.id, message.server.id, False) - except: - traceback.print_exc() - -#Overwrite on_message so we can run our stuff -@Discord.client.event -async def on_message(message): - from DiscordoragiSearch import isValidMessage #local import here to fix attribute not found error - print('Message recieved') - #Is the message valid (i.e. it's not made by Discordoragi and I haven't seen it already). If no, try to add it to the "already seen pile" and skip to the next message. If yes, keep going. - if not (isValidMessage(message)): - try: - if not (DatabaseHandler.messageExists(message.id)): - DatabaseHandler.addMessage(message.id, message.author.id, message.server.id, False) - except Exception: - traceback.print_exc() - pass - else: - await process_message(message) - -# ------------------------------------# -#Here's the stuff that actually gets run - -#Initialise Discord. -print('Starting Bot') -Discord.run() diff --git a/roboragi_old/AnimePlanet.py b/roboragi_old/AnimePlanet.py deleted file mode 100644 index 4136664..0000000 --- a/roboragi_old/AnimePlanet.py +++ /dev/null @@ -1,112 +0,0 @@ -from pyquery import PyQuery as pq -import aiohttp -import difflib -import traceback -import pprint -import collections - -BASE_URL = "http://www.anime-planet.com" - -session = aiohttp.ClientSession() - -def sanitiseSearchText(searchText): - return searchText.replace('(TV)', 'TV') - -async def getAnimeURL(searchText): - try: - searchText = sanitiseSearchText(searchText) - - async with session.get(BASE_URL + "/anime/all?name=" + searchText.replace(" ", "%20"), timeout=10) as resp: - html = await resp.text() - ap = pq(html) - animeList = [] - - #If it's taken us to the search page - if ap.find('.cardDeck.pure-g.cd-narrow[data-type="anime"]'): - for entry in ap.find('.card.pure-1-6'): - entryTitle = pq(entry).find('h4').text() - entryURL = pq(entry).find('a').attr('href') - - anime = {} - anime['title'] = entryTitle - anime['url'] = BASE_URL + entryURL - animeList.append(anime) - - closestName = difflib.get_close_matches(searchText.lower(), [x['title'].lower() for x in animeList], 1, 0.85)[0] - closestURL = '' - - for anime in animeList: - if anime['title'].lower() == closestName: - return anime['url'] - - #Else if it's taken us right to the series page, get the url from the meta tag - else: - return ap.find("meta[property='og:url']").attr('content') - return None - - except Exception as e: - return None - -#Probably doesn't need to be split into two functions given how similar they are, but it might be worth keeping separate for the sake of issues between anime/manga down the line -async def getMangaURL(searchText, authorName=None): - try: - if authorName: - async with sessions.get(BASE_URL + "/manga/all?name=" + searchText.replace(" ", "%20") + '&author=' + authorName.replace(" ", "%20"), timeout=10) as resp: - html = await resp.text() - if "No results found" in html: - rearrangedAuthorNames = collections.deque(authorName.split(' ')) - rearrangedAuthorNames.rotate(-1) - rearrangedName = ' '.join(rearrangedAuthorNames) - async with session.get(BASE_URL + "/manga/all?name=" + searchText.replace(" ", "%20") + '&author=' + rearrangedName.replace(" ", "%20"), timeout=10) as resp: - html = await resp.text() - - else: - async with session.get(BASE_URL + "/manga/all?name=" + searchText.replace(" ", "%20"), timeout=10) as resp: - html = await resp.text() - - ap = pq(html) - - mangaList = [] - - #If it's taken us to the search page - if ap.find('.cardDeck.pure-g.cd-narrow[data-type="manga"]'): - for entry in ap.find('.card.pure-1-6'): - entryTitle = pq(entry).find('h4').text() - entryURL = pq(entry).find('a').attr('href') - - manga = {} - manga['title'] = entryTitle - manga['url'] = BASE_URL + entryURL - mangaList.append(manga) - - if authorName: - authorName = authorName.lower() - authorName = authorName.split(' ') - - for manga in mangaList: - manga['title'] = manga['title'].lower() - - for name in authorName: - manga['title'] = manga['title'].replace(name, '') - manga['title'] = manga['title'].replace('(', '').replace(')', '').strip() - - closestName = difflib.get_close_matches(searchText.lower(), [x['title'].lower() for x in mangaList], 1, 0.85)[0] - closestURL = '' - - for manga in mangaList: - if manga['title'].lower() == closestName: - return manga['url'] - - #Else if it's taken us right to the series page, get the url from the meta tag - else: - return ap.find("meta[property='og:url']").attr('content') - return None - - except: - return None - -def getAnimeURLById(animeId): - return 'http://www.anime-planet.com/anime/' + str(animeId) - -def getMangaURLById(mangaId): - return 'http://www.anime-planet.com/manga/' + str(mangaId) \ No newline at end of file diff --git a/roboragi_old/CommentBuilder.py b/roboragi_old/CommentBuilder.py deleted file mode 100644 index dc3a43f..0000000 --- a/roboragi_old/CommentBuilder.py +++ /dev/null @@ -1,1407 +0,0 @@ -''' -CommentBuilder.py -Takes the data given to it by search and formats it into a comment -''' - -import re -from os import linesep -from discord import Embed -import traceback - -import DatabaseHandler -import pprint -import Discord - -#Removes the (Source: MAL) or (Written by X) bits from the decriptions in the databases -def cleanupDescription(desc): - for match in re.finditer("([\[\<\(](.*?)[\]\>\)])", desc, re.S): - if 'ource' in match.group(1).lower(): - desc = desc.replace(match.group(1), '') - if 'MAL' in match.group(1): - desc = desc.replace(match.group(1), '') - - for match in re.finditer("([\<](.*?)[\>])", desc, re.S): - if 'br' in match.group(1).lower(): - desc = desc.replace(match.group(1), '') - - reply = '' - for i, line in enumerate(linesep.join([s for s in desc.splitlines() if s]).splitlines()): - if i is not 0: - reply += '\n' - reply += line + '\n' - return reply - -#Builds an anime comment from MAL/Anilist data -def buildAnimeComment(isExpanded, mal, ani, ap, anidb): - try: - comment = '' - - title = None - jTitle = None - - cType = None - - malURL = None - aniURL = None - apURL = ap - anidbURL = anidb - - youtubeTrailer = None - - status = None - episodes = None - genres = [] - - countdown = None - nextEpisode = None - - desc = None - - if mal: - desc = mal['synopsis'] - - if mal['type']: - cType = mal['type'] - - malURL = 'http://myanimelist.net/anime/' + str(mal['id']) - - if ani is not None: - title = ani['title_romaji'] - aniURL = 'http://anilist.co/anime/' + str(ani['id']) - - try: - cType = ani['type'] - desc = ani['description'] - except: - pass - - status = ani['airing_status'].title() - - try: - if ani['title_japanese'] is not None: - jTitle = ani['title_japanese'] - - if ani['youtube_id'] is not None: - youtubeTrailer = ani['youtube_id'] - - if ani['total_episodes'] is not None: - if ani['total_episodes'] == 0: - episodes = 'Unknown' - else: - episodes = ani['total_episodes'] - - if ani['genres'] is not None: - genres = ani['genres'] - - if ani['airing'] is not None: - countdown = ani['airing']['countdown'] - nextEpisode = ani['airing']['next_episode'] - except: - print('No full details for Anilist') - - stats = DatabaseHandler.getRequestStats(title, 'Anime') - - if ani is not None: - stats = DatabaseHandler.getRequestStats(ani['title_romaji'],'Anime') - - #---------- BUILDING THE COMMENT ----------# - - #----- TITLE -----# - comment += '**' + title.strip() + '** - \n\n' - - #----- LINKS -----# - urlComments = [] - - if malURL is not None: - urlComments.append(malURL) - if apURL is not None: - urlComments.append(apURL) - if ani is not None: - urlComments.append(aniURL) - if anidbURL is not None: - urlComments.append(anidbURL) - - for i, link in enumerate(urlComments): - if i is not 0: - comment += '\n\n' - comment += link - - #----- JAPANESE TITLE -----# - if (isExpanded): - if jTitle is not None: - comment += '\n\n' - - splitJTitle = jTitle.split() - for i, word in enumerate(splitJTitle): - if not (i == 0): - comment += ' ' - comment += word - - #----- INFO LINE -----# - if (isExpanded): - comment += '\n\n(' - - if cType: - comment += '**' + cType + '** | ' - - comment += '**Status:** ' + status - - if cType != 'Movie': - comment += ' | **Episodes:** ' + str(episodes) - - comment += ' | **Genres:** ' - else: - comment += '\n\n(' - - if cType: - comment += cType + ' | ' - - comment += 'Status: ' + status - - if cType != 'Movie': - comment += ' | Episodes: ' + str(episodes) - - comment += ' | Genres: ' - - if not (genres == []): - for i, genre in enumerate(genres): - if i is not 0: - comment += ', ' - comment += genre - else: - comment += 'None' - - if (isExpanded) and (stats is not None): - comment += ' \n**Stats:** ' + str(stats['total']) + ' requests across ' + str(stats['uniqueSubreddits']) + ' server(s)) - ' + str(round(stats['totalAsPercentage'],3)) + '% of all requests' - else: - comment += ')' - - #----- EPISODE COUNTDOWN -----# - if (countdown is not None) and (nextEpisode is not None): - #countdown is given to us in seconds - days, countdown = divmod(countdown, 24*60*60) - hours, countdown = divmod(countdown, 60*60) - minutes, countdown = divmod(countdown, 60) - - comment += '\n\n(Episode ' + str(nextEpisode) + ' airs in ' + str(days) + ' days, ' + str(hours) + ' hours, ' + str(minutes) + ' minutes)' - - #----- DESCRIPTION -----# - if (isExpanded): - comment += '\n\n' + cleanupDescription(desc) - - #----- END -----# - receipt = '(A) Request successful: ' + title + ' - ' - if malURL is not None: - receipt += 'MAL ' - if apURL is not None: - receipt += 'AP ' - if ani is not None: - receipt += 'ANI ' - if anidbURL is not None: - receipt += 'ADB ' - print(receipt.encode('utf8')) - - #We return the title/comment separately so we can track if multiples of the same comment have been requests (e.g. {Nisekoi}{Nisekoi}{Nisekoi}) - dictToReturn = {} - dictToReturn['title'] = title - dictToReturn['comment'] = comment - - return dictToReturn - except Exception as e: - print("Error creating embed: {}\n".format(e)) - #traceback.print_exc() - return None - -#Builds a manga comment from MAL/Anilist/MangaUpdates data -def buildMangaComment(isExpanded, mal, ani, mu, ap): - try: - comment = '' - - title = None - jTitle = None - - cType = None - - malURL = None - aniURL = None - muURL = mu - apURL = ap - - status = None - chapters = None - volumes = None - genres = [] - - desc = None - - if not (mal is None): - title = mal['title'] - malURL = 'http://myanimelist.net/manga/' + str(mal['id']) - desc = mal['synopsis'] - status = mal['status'] - - cType = mal['type'] - - try: - if (int(mal['chapters']) == 0): - chapters = 'Unknown' - else: - chapters = mal['chapters'] - except: - chapters = 'Unknown' - - try: - volumes = mal['volumes'] - except: - volumes = 'Unknown' - - if ani is not None: - if title is None: - title = ani['title_english'] - aniURL = 'http://anilist.co/manga/' + str(ani['id']) - desc = ani['description'] - status = ani['publishing_status'].title() - - cType = ani['type'] - - try: - if ani['title_japanese'] is not None: - jTitle = ani['title_japanese'] - - if ani['total_chapters'] is not None: - if ani['total_chapters'] == 0: - chapters = 'Unknown' - else: - chapters = ani['total_chapters'] - - if ani['total_volumes'] is not None: - volumes = ani['total_volumes'] - else: - volumes = 'Unknown' - - if ani['genres'] is not None: - genres = ani['genres'] - - except Exception as e: - print(e) - - stats = DatabaseHandler.getRequestStats(title,'Manga') - - #---------- BUILDING THE COMMENT ----------# - - #----- TITLE -----# - comment += '**' + title.strip() + '** - \n\n' - - #----- LINKS -----# - urlComments = [] - - if malURL is not None: - urlComments.append(malURL) - if apURL is not None: - urlComments.append(apURL) - if aniURL is not None: - urlComments.append(aniURL) - if muURL is not None: - urlComments.append(muURL) - - for i, link in enumerate(urlComments): - if i is not 0: - comment += '\n\n' - comment += link - - comment += '\n\n' - - #----- JAPANESE TITLE -----# - if (isExpanded): - if jTitle is not None: - comment += '\n\n' - - splitJTitle = jTitle.split() - for i, word in enumerate(splitJTitle): - if not (i == 0): - comment += ' ' - comment += word - - #----- INFO LINE -----# - - if (isExpanded): - comment += '\n\n(' - - if cType: - if cType == 'Novel': - cType = 'Light Novel' - - comment += '**' + cType + '** | ' - - comment += '**Status:** ' + status - - if (cType != 'Light Novel'): - if str(chapters) is not 'Unknown': - comment += ' | **Chapters:** ' + str(chapters) - else: - comment += ' | **Volumes:** ' + str(volumes) - - if genres: - comment += ' | **Genres:** ' - else: - comment += '\n\n(' - - if cType: - if cType == 'Novel': - cType = 'Light Novel' - - comment += cType + ' | ' - - comment += 'Status: ' + status - - if (cType != 'Light Novel'): - if str(chapters) is not 'Unknown': - comment += ' | Chapters: ' + str(chapters) - else: - comment += ' | Volumes: ' + str(volumes) - - if genres: - comment += ' | Genres: ' - - if genres: - for i, genre in enumerate(genres): - if i is not 0: - comment += ', ' - comment += genre - - if (isExpanded) and (stats is not None): - comment += ' \n**Stats:** ' + str(stats['total']) + ' requests across ' + str(stats['uniqueSubreddits']) + ' server(s)) - ' + str(round(stats['totalAsPercentage'],3)) + '% of all requests' - else: - comment += ')' - - #----- DESCRIPTION -----# - if (isExpanded): - comment += '\n\n' + cleanupDescription(desc) - - #----- END -----# - receipt = '(M) Request successful: ' + title + ' - ' - if malURL is not None: - receipt += 'MAL ' - if ap is not None: - receipt += 'AP ' - if ani is not None: - receipt += 'ANI ' - if muURL is not None: - receipt += 'MU ' - print(receipt.encode('utf8')) - - dictToReturn = {} - dictToReturn['title'] = title - dictToReturn['comment'] = comment - - return dictToReturn - except: - traceback.print_exc() - return None - -#Builds a manga comment from MAL/Anilist/MangaUpdates data -def buildLightNovelComment(isExpanded, mal, ani, nu, lndb): - try: - comment = '' - - title = None - jTitle = None - - cType = None - - malURL = None - aniURL = None - nuURL = nu - lndbURL = lndb - - status = None - chapters = None - volumes = None - genres = [] - - desc = None - - if not (mal is None): - title = mal['title'] - malURL = 'http://myanimelist.net/manga/' + str(mal['id']) - desc = mal['synopsis'] - status = mal['status'] - - cType = mal['type'] - - try: - if (int(mal['chapters']) == 0): - chapters = 'Unknown' - else: - chapters = mal['chapters'] - except: - chapters = 'Unknown' - - try: - volumes = mal['volumes'] - except: - volumes = 'Unknown' - - if ani is not None: - if title is None: - title = ani['title_english'] - aniURL = 'http://anilist.co/manga/' + str(ani['id']) - desc = ani['description'] - status = ani['publishing_status'].title() - - cType = ani['type'] - - try: - if ani['title_japanese'] is not None: - jTitle = ani['title_japanese'] - - if ani['total_chapters'] is not None: - if ani['total_chapters'] == 0: - chapters = 'Unknown' - else: - chapters = ani['total_chapters'] - - if ani['total_volumes'] is not None: - volumes = ani['total_volumes'] - else: - volumes = 'Unknown' - - if ani['genres'] is not None: - genres = ani['genres'] - - except Exception as e: - print(e) - - stats = DatabaseHandler.getRequestStats(title,'LN') - - #---------- BUILDING THE COMMENT ----------# - - #----- TITLE -----# - comment += '**' + title.strip() + '** -\n\n' - - #----- LINKS -----# - urlComments = [] - - if malURL is not None: - urlComments.append(malURL) - if aniURL is not None: - urlComments.append(aniURL) - if nuURL is not None: - urlComments.append(nuURL) - if lndbURL is not None: - urlComments.append(lndbURL) - - for i, link in enumerate(urlComments): - if i is not 0: - comment += '\n ' - comment += link - - comment += '\n\n' - - #----- JAPANESE TITLE -----# - if (isExpanded): - if jTitle is not None: - comment += '\n\n' - - splitJTitle = jTitle.split() - for i, word in enumerate(splitJTitle): - if not (i == 0): - comment += ' ' - comment += word - - #----- INFO LINE -----# - - if (isExpanded): - comment += '\n\n(' - - if cType: - if cType == 'Novel': - cType = 'Light Novel' - - comment += '**' + cType + '** | ' - - comment += '**Status:** ' + status - - if (cType != 'Light Novel'): - if str(chapters) is not 'Unknown': - comment += ' | **Chapters:** ' + str(chapters) - else: - comment += ' | **Volumes:** ' + str(volumes) - - if genres: - comment += ' | **Genres:** ' - else: - comment += '\n\n(' - - if cType: - if cType == 'Novel': - cType = 'Light Novel' - - comment += cType + ' | ' - - comment += 'Status: ' + status - - if (cType != 'Light Novel'): - if str(chapters) is not 'Unknown': - comment += ' | Chapters: ' + str(chapters) - else: - comment += ' | Volumes: ' + str(volumes) - - if genres: - comment += ' | Genres: ' - - if genres: - for i, genre in enumerate(genres): - if i is not 0: - comment += ', ' - comment += genre - - if (isExpanded) and (stats is not None): - comment += ' \n**Stats:** ' + str(stats['total']) + ' requests across ' + str(stats['uniqueSubreddits']) + ' subreddit(s)^) ^- ^' + str(round(stats['totalAsPercentage'],3)) + '% ^of ^all ^requests' - else: - comment += ')' - - #----- DESCRIPTION -----# - if (isExpanded): - comment += '\n\n' + cleanupDescription(desc) - - #----- END -----# - receipt = '(LN) Request successful: ' + title + ' - ' - if malURL is not None: - receipt += 'MAL ' - if ani is not None: - receipt += 'ANI ' - if nuURL is not None: - receipt += 'MU ' - if lndbURL is not None: - receipt += 'LNDB ' - print(receipt.encode('utf8')) - - dictToReturn = {} - dictToReturn['title'] = title - dictToReturn['comment'] = comment - - return dictToReturn - except: - traceback.print_exc() - return None - -#Builds a stats comment. If it is basic stats the default server id is the Discordoragi help server -def buildStatsComment(server=None, username=None, serverID="171004769069039616"): - try: - statComment = '' - receipt = '(S) Request successful: Stats' - - if username: - userStats = DatabaseHandler.getUserStats(username) - - if userStats: - statComment += 'Some stats on ' + username + ':\n\n' - statComment += '- **' + str(userStats['totalUserComments']) + '** total comments searched (' + str(round(userStats['totalUserCommentsAsPercentage'], 3)) + '% of all comments)\n' - statComment += '- **' + str(userStats['totalUserRequests']) + '** requests made (' + str(round(userStats['totalUserRequestsAsPercentage'], 3)) + '% of all requests and #' + str(userStats['overallRequestRank']) + ' overall)\n' - statComment += '- **' + str(userStats['uniqueRequests']) + '** unique anime/manga requested\n' - statComment += '- **/r/' + str(userStats['favouriteSubreddit']) + '** is their favourite server with ' + str(userStats['favouriteSubredditCount']) + ' requests (' + str(round(userStats['favouriteSubredditCountAsPercentage'], 3)) + '% of the server\'s requests)\n' - statComment += '\n' - statComment += 'Their most frequently requested anime/manga overall are:\n\n' - - for i, request in enumerate(userStats['topRequests']): - statComment += str(i + 1) + '. **' + str(request[0]) + '** (' + str(request[1]) + ' - ' + str(request[2]) + ' requests) \n' - else: - statComment += '/u/' + str(username) + ' hasn\'t used Roboragi yet.' - - receipt += ' - /u/' + username - elif server: - serverID = server.id - server = str(server) - serverStats = DatabaseHandler.getSubredditStats(server.lower()) - - if serverStats: - statComment += '**' + server +' Stats**\n\n' - - statComment += 'I\'ve searched through ' + str(serverStats['totalComments']) - statComment += ' unique comments on ' + server - statComment += ' and fulfilled a total of ' + str(serverStats['total']) + ' requests, ' - statComment += 'representing ' + str(round(serverStats['totalAsPercentage'], 2)) + '% of all requests. ' - statComment += 'A total of ' + str(serverStats['uniqueNames']) + ' unique anime/manga have been requested here, ' - statComment += 'with a mean value of ' + str(round(serverStats['meanValuePerRequest'], 3)) + ' requests/show' - statComment += ' and a standard deviation of ' + str(round(serverStats['standardDeviation'], 3)) + '.' - - statComment += '\n\n' - - statComment += 'The most frequently requested anime/manga on this server are:\n\n' - - for i, request in enumerate(serverStats['topRequests']): - statComment += str(i + 1) + '. **' + str(request[0]) + '** (' + str(request[1]) + ' - ' + str(request[2]) + ' requests)\n' - - statComment += '\n' - - statComment += 'The most frequent requesters on this server are:\n\n' - for i, requester in enumerate(serverStats['topRequesters']): - statComment += str(i + 1) + '. /u/' + str(requester[0]) + ' (' + str(requester[1]) + ' requests)\n' - - else: - statComment += 'There have been no requests on ' + str(server) + ' yet.' - - receipt += ' - ' + server - else: - basicStats = DatabaseHandler.getBasicStats(serverID) - - #The overall stats section - statComment += '**Overall Stats**\n\n' - - statComment += 'I\'ve searched through ' + str(basicStats['totalComments']) - statComment += ' unique comments and fulfilled a total of ' + str(basicStats['total']) - statComment += ' requests across ' + str(basicStats['uniqueSubreddits']) + ' unique server(s). ' - statComment += 'A total of ' + str(basicStats['uniqueNames']) - statComment += ' unique anime/manga have been requested, with a mean value of ' + str(round(basicStats['meanValuePerRequest'],3)) - statComment += ' requests/show and a standard deviation of ' + str(round(basicStats['standardDeviation'], 3)) + '.' - - statComment += '\n\n' - - statComment += 'The most frequently requested anime/manga overall are:\n\n' - - for i, request in enumerate(basicStats['topRequests']): - statComment += str(i + 1) + '. **' + str(request[0]) + '** (' + str(request[1]) + ' - ' + str(request[2]) + ' requests)\n' - - statComment += '\n' - - statComment += 'The most frequent requesters overall are: \n' - for i, requester in enumerate(basicStats['topRequesters']): - statComment += str(i + 1) + '. ' + str(Discord.getUsernameFromID(requester[0], )) + ' (' + str(requester[1]) + ' requests) \n' - - statComment += '\n' - receipt += ' - Basic' - - print(receipt.encode('utf8')) - return statComment - except: - traceback.print_exc() - return None - -# Builds an embed using the same data -def buildAnimeEmbed(isExpanded, mal, ani, ap, anidb): - try: - comment = '' - descComment = '' - title = None - jTitle = None - - cType = None - - malimage = '' - malURL = None - aniURL = None - apURL = ap - anidbURL = anidb - - youtubeTrailer = None - - status = None - episodes = None - genres = [] - - countdown = None - nextEpisode = None - - desc = None - - if mal: - desc = mal['synopsis'] - - if mal['type']: - cType = mal['type'] - - malURL = 'http://myanimelist.net/anime/' + str(mal['id']) - if mal['title']: - title = mal['title'] - - if mal['english']: - title = mal['english'] - - if mal['image']: - malimage = mal['image'] - - if mal['status']: - status = mal['status'] - if ani is not None: - title = ani['title_romaji'] - aniURL = 'http://anilist.co/anime/' + str(ani['id']) - - try: - cType = ani['type'] - desc = ani['description'] - except: - pass - - if status is None: - try: - status = ani['airing_status'].title() - except Exception as e: - print(e) - try: - if ani['title_japanese'] is not None: - jTitle = ani['title_japanese'] - - if ani['youtube_id'] is not None: - youtubeTrailer = ani['youtube_id'] - - if ani['total_episodes'] is not None: - if ani['total_episodes'] == 0: - episodes = 'Unknown' - else: - episodes = ani['total_episodes'] - - if ani['genres'] is not None: - genres = ani['genres'] - - if ani['airing'] is not None: - countdown = ani['airing']['countdown'] - nextEpisode = ani['airing']['next_episode'] - except: - print('No full details for Anilist') - - stats = DatabaseHandler.getRequestStats(title, 'Anime') - - if ani is not None: - stats = DatabaseHandler.getRequestStats(ani['title_romaji'],'Anime') - - #---------- BUILDING THE COMMENT ----------# - - comment = '' - - #----- LINKS -----# - urlComments = [] - allLinks = '' - - try: - mal_english = mal['english'] - except: - pass - - - if malURL is not None: - urlComments.append("[MAL]({})".format(sanitise_url_for_markdown(malURL))) - if apURL is not None: - urlComments.append("[AP]({})".format(sanitise_url_for_markdown(apURL))) - if ani is not None: - urlComments.append("[AL]({})".format(sanitise_url_for_markdown(aniURL))) - if anidbURL is not None: - urlComments.append("[AniDB]({})".format(sanitise_url_for_markdown(anidbURL))) - - for i, link in enumerate(urlComments): - if i is not 0: - allLinks += ', ' - allLinks += link - #----- JAPANESE TITLE -----# - if (isExpanded): - if jTitle is not None: - comment += '\n\n' - - splitJTitle = jTitle.split() - for i, word in enumerate(splitJTitle): - if not (i == 0): - comment += ' ' - comment += word - - #----- INFO LINE -----# - if (isExpanded): - comment += '\n\n(' - - if cType: - comment += '**' + cType + '** | ' - - comment += '**Status:** ' + status - - if cType != 'Movie': - comment += ' | **Episodes:** ' + str(episodes) - - comment += ' | **Genres:** ' - else: - comment += '\n\n(' - - if cType: - comment += cType + ' | ' - - comment += 'Status: ' + status - - if cType != 'Movie': - comment += ' | Episodes: ' + str(episodes) - - comment += ' | Genres: ' - - if not (genres == []): - for i, genre in enumerate(genres): - if i is not 0: - comment += ', ' - comment += genre - else: - comment += 'None' - - if (isExpanded) and (stats is not None): - comment += ') \n\n**Stats:** ' + str(stats['total']) + ' requests across ' + str(stats['uniqueSubreddits']) + ' server(s)) - ' + str(round(stats['totalAsPercentage'],3)) + '% of all requests' - else: - comment += ')' - - #----- EPISODE COUNTDOWN -----# - if (countdown is not None) and (nextEpisode is not None): - #countdown is given to us in seconds - days, countdown = divmod(countdown, 24*60*60) - hours, countdown = divmod(countdown, 60*60) - minutes, countdown = divmod(countdown, 60) - - comment += '\n\n(Episode ' + str(nextEpisode) + ' airs in ' + str(days) + ' days, ' + str(hours) + ' hours, ' + str(minutes) + ' minutes)' - - #----- DESCRIPTION -----# - if (isExpanded): - descComment += cleanupDescription(desc) - - #----- END -----# - receipt = '(A) Request successful: ' + title + ' - ' - if malURL is not None: - receipt += 'MAL ' - if apURL is not None: - receipt += 'AP ' - if ani is not None: - receipt += 'AL ' - if anidbURL is not None: - receipt += 'ADB ' - print(receipt.encode('utf8')) - try: - embed = buildEmbedObject(title, allLinks, comment, malimage, isExpanded, descComment) - except Exception as e: - print(e) - #We return the title/comment separately so we can track if multiples of the same comment have been requests (e.g. {Nisekoi}{Nisekoi}{Nisekoi}) - dictToReturn = {} - dictToReturn['title'] = title - dictToReturn['embed'] = embed - return dictToReturn - except Exception as e: - print(e) - #traceback.print_exc() - return None - -#sets up the embed for Mangas -def buildMangaEmbed(isExpanded, mal, ani, mu, ap): - try: - comment = '' - descComment = '' - - title = None - jTitle = None - - cType = None - - malimage = '' - malURL = None - aniURL = None - muURL = mu - apURL = ap - - status = None - chapters = None - volumes = None - genres = [] - - desc = None - - if not (mal is None): - title = mal['title'] - malURL = 'http://myanimelist.net/manga/' + str(mal['id']) - desc = mal['synopsis'] - status = mal['status'] - malimage = mal['image'] - - cType = mal['type'] - - try: - if (int(mal['chapters']) == 0): - chapters = 'Unknown' - else: - chapters = mal['chapters'] - except: - chapters = 'Unknown' - - try: - if (int(mal['volumes']) == 0): - volumes = 'Unknown' - else: - volumes = mal['volumes'] - except: - volumes = 'Unknown' - - if ani is not None: - if title is None: - title = ani['title_english'] - aniURL = 'http://anilist.co/manga/' + str(ani['id']) - if ani['description']: - desc = ani['description'] - - try: - status = ani['publishing_status'].title() - except: - pass - - cType = ani['type'] - - try: - if ani['title_japanese'] is not None: - jTitle = ani['title_japanese'] - - if ani['total_chapters'] is not None: - if ani['total_chapters'] == 0: - chapters = 'Unknown' - else: - chapters = ani['total_chapters'] - - if ani['total_volumes'] is not None: - if ani['total_volumes'] == 0: - volumes = 'Unknown' - else: - volumes = ani['total_volumes'] - - if ani['genres'] is not None: - genres = ani['genres'] - - except Exception as e: - print(e) - - stats = DatabaseHandler.getRequestStats(title,'Manga') - - #---------- BUILDING THE COMMENT ----------# - - #----- LINKS -----# - urlComments = [] - allLinks = '' - if malURL is not None: - urlComments.append("[MAL]({})".format(sanitise_url_for_markdown(malURL))) - if aniURL is not None: - urlComments.append("[ANI]({})".format(sanitise_url_for_markdown(aniURL))) - if apURL is not None: - urlComments.append("[AP]({})".format(sanitise_url_for_markdown(apURL))) - if muURL is not None: - urlComments.append("[MU]({})".format(sanitise_url_for_markdown(muURL))) - - for i, link in enumerate(urlComments): - if i is not 0: - allLinks += ', ' - allLinks += link - - #----- JAPANESE TITLE -----# - if (isExpanded): - if jTitle is not None: - comment += '\n\n' - - splitJTitle = jTitle.split() - for i, word in enumerate(splitJTitle): - if not (i == 0): - comment += ' ' - comment += word - - #----- INFO LINE -----# - - if (isExpanded): - comment += '\n\n(' - - if cType: - if cType == 'Novel': - cType = 'Light Novel' - - comment += '**' + cType + '** | ' - - comment += '**Status:** ' + status - - if (cType != 'Light Novel'): - if str(volumes) is not 'Unknown': - comment += ' | **Volumes:** ' + str(volumes) - if str(chapters) is not 'Unknown': - comment += ' | **Chapters:** ' + str(chapters) - else: - if str(volumes) is not 'Unknown': - comment += ' | **Volumes:** ' + str(volumes) - - if genres: - comment += ' | **Genres:** ' - else: - comment += '\n\n(' - - if cType: - if cType == 'Novel': - cType = 'Light Novel' - - comment += cType + ' | ' - - comment += 'Status: ' + status - - if (cType != 'Light Novel'): - if str(volumes) is not 'Unknown': - comment += ' | Volumes: ' + str(volumes) - if str(chapters) is not 'Unknown': - comment += ' | Chapters: ' + str(chapters) - else: - if str(volumes) is not 'Unknown': - comment += ' | Volumes: ' + str(volumes) - - if genres: - comment += ' | Genres: ' - - if genres: - for i, genre in enumerate(genres): - if i is not 0: - comment += ', ' - comment += genre - - if (isExpanded) and (stats is not None): - comment += ') \n\n**Stats:** ' + str(stats['total']) + ' requests across ' + str(stats['uniqueSubreddits']) + ' server(s)) - ' + str(round(stats['totalAsPercentage'],3)) + '% of all requests' - else: - comment += ')' - - #----- DESCRIPTION -----# - if (isExpanded): - descComment += cleanupDescription(desc) - - #----- END -----# - receipt = '(M) Request successful: ' + title + ' - ' - if malURL is not None: - receipt += 'MAL ' - if ap is not None: - receipt += 'AP ' - if ani is not None: - receipt += 'AL ' - if muURL is not None: - receipt += 'MU ' - print(receipt.encode('utf8')) - - #----- Build embed object -----# - try: - embed = buildEmbedObject(title, allLinks, comment, malimage, isExpanded, descComment) - except Exception as e: - print(e) - dictToReturn = {} - dictToReturn['title'] = title - dictToReturn['embed'] = embed - - return dictToReturn - except Exception as e: - print(e) - #traceback.print_exc() - return None - -#sets up the embed for Light Novels -def buildLightNovelEmbed(isExpanded, mal, ani, nu, lndb): - try: - comment = '' - descComment= '' - - title = None - jTitle = None - - cType = None - - malimage = '' - malURL = None - aniURL = None - nuURL = nu - lndbURL = lndb - - status = None - chapters = None - volumes = None - genres = [] - - desc = None - - if not (mal is None): - title = mal['title'] - malURL = 'http://myanimelist.net/manga/' + str(mal['id']) - desc = mal['synopsis'] - status = mal['status'] - malimage = mal['image'] - - cType = mal['type'] - - try: - if (int(mal['chapters']) == 0): - chapters = 'Unknown' - else: - chapters = mal['chapters'] - except: - chapters = 'Unknown' - - try: - if (int(mal['volumes']) == 0): - volumes = 'Unknown' - else: - volumes = mal['volumes'] - except: - volumes = 'Unknown' - - if ani is not None: - if title is None: - title = ani['title_english'] - aniURL = 'http://anilist.co/manga/' + str(ani['id']) - if ani['description']: - desc = ani['description'] - try: - status = ani['publishing_status'].title() - except: - pass - - cType = ani['type'] - - try: - if ani['title_japanese'] is not None: - jTitle = ani['title_japanese'] - - if ani['total_chapters'] is not None: - if ani['total_chapters'] == 0: - chapters = 'Unknown' - else: - chapters = ani['total_chapters'] - else: - volumes = 'Unknown' - - if ani['total_volumes'] is not None: - if ani['total_volumes'] == 0: - volumes = 'Unknown' - else: - volumes = ani['total_volumes'] - else: - volumes = 'Unknown' - - if ani['genres'] is not None: - genres = ani['genres'] - - except Exception as e: - print(e) - - stats = DatabaseHandler.getRequestStats(title,'LN') - - #---------- BUILDING THE COMMENT ----------# - - #----- LINKS -----# - urlComments = [] - allLinks = '' - if malURL is not None: - urlComments.append("[MAL]({})".format(sanitise_url_for_markdown(malURL))) - if aniURL is not None: - urlComments.append("[ANI]({})".format(sanitise_url_for_markdown(aniURL))) - if nuURL is not None: - urlComments.append("[NU]({})".format(sanitise_url_for_markdown(nuURL))) - if lndbURL is not None: - urlComments.append("[LNDB]({})".format(sanitise_url_for_markdown(lndbURL))) - - for i, link in enumerate(urlComments): - if i is not 0: - allLinks += ', ' - allLinks += link - - #----- JAPANESE TITLE -----# - if (isExpanded): - if jTitle is not None: - comment += '\n\n' - - splitJTitle = jTitle.split() - for i, word in enumerate(splitJTitle): - if not (i == 0): - comment += ' ' - comment += word - - #----- INFO LINE -----# - - if (isExpanded): - comment += '\n\n(' - - if cType: - if cType == 'Novel': - cType = 'Light Novel' - - comment += '**' + cType + '** | ' - - comment += '**Status:** ' + status - - if (cType != 'Light Novel'): - if str(volumes) is not 'Unknown': - comment += ' | **Volumes:** ' + str(volumes) - if str(chapters) is not 'Unknown': - comment += ' | **Chapters:** ' + str(chapters) - else: - if str(volumes) is not 'Unknown': - comment += ' | **Volumes:** ' + str(volumes) - - if genres: - comment += ' | **Genres:** ' - else: - comment += '\n\n(' - - if cType: - if cType == 'Novel': - cType = 'Light Novel' - - comment += cType + ' | ' - - comment += 'Status: ' + status - - if (cType != 'Light Novel'): - if str(chapters) is not 'Unknown': - comment += ' | Chapters: ' + str(chapters) - else: - comment += ' | Volumes: ' + str(volumes) - - if genres: - comment += ' | Genres: ' - - if genres: - for i, genre in enumerate(genres): - if i is not 0: - comment += ', ' - comment += genre - - if (isExpanded) and (stats is not None): - comment += ')\n\n**Stats:** ' + str(stats['total']) + ' requests across ' + str(stats['uniqueSubreddits']) + ' server(s)) - ' + str(round(stats['totalAsPercentage'],3)) + '% of all requests' - else: - comment += ')' - - #----- DESCRIPTION -----# - if (isExpanded): - descComment += cleanupDescription(desc) - - #----- END -----# - receipt = '(LN) Request successful: ' + title + ' - ' - if malURL is not None: - receipt += 'MAL ' - if ani is not None: - receipt += 'AL ' - if nuURL is not None: - receipt += 'MU ' - if lndbURL is not None: - receipt += 'LNDB ' - print(receipt.encode('utf8')) - - embed = buildEmbedObject(title, allLinks, comment, malimage, isExpanded, descComment) - - dictToReturn = {} - dictToReturn['title'] = title - dictToReturn['embed'] = embed - - return dictToReturn - except Exception as e: - print(e) - #traceback.print_exc() - return None - -def buildStatsEmbed(server=None, username=None, serverID="171004769069039616"): - try: - userNick = '' - statComment = '' - receipt = '(S) Request successful: Stats' - - if username is not None: - reqMember = server.get_member(username) - if reqMember.nick: - userNick = reqMember.nick - else: - userNick = reqMember.name - userStats = DatabaseHandler.getUserStats(username) - - if userStats: - statComment += 'Some stats on ' + userNick + ':\n\n' - statComment += '- **' + str(userStats['totalUserRequests']) + '** requests made (' + str(round(userStats['totalUserRequestsAsPercentage'], 3)) + '% of all requests and #' + str(userStats['overallRequestRank']) + ' overall)\n' - statComment += '- **' + str(userStats['uniqueRequests']) + '** unique anime/manga requested\n' - statComment += '\n' - statComment += 'Their most frequently requested anime/manga overall are:\n\n' - - for i, request in enumerate(userStats['topRequests']): - statComment += str(i + 1) + '. **' + str(request[0]) + '** (' + str(request[1]) + ' - ' + str(request[2]) + ' requests) \n' - else: - statComment += str(userNick) + ' hasn\'t used Roboragi yet.' - - receipt += ' - ' + userNick - elif server: - serverStats = DatabaseHandler.getSubredditStats(server) - - if serverStats: - statComment += '**' + server.name +' Stats**\n\n' - statComment += 'On ' + server.name - statComment += ' I have fulfilled a total of ' + str(serverStats['total']) + ' requests, ' - statComment += 'representing ' + str(round(serverStats['totalAsPercentage'], 2)) + '% of all requests. ' - statComment += 'A total of ' + str(serverStats['uniqueNames']) + ' unique anime/manga have been requested here, ' - statComment += 'with a mean value of ' + str(round(serverStats['meanValuePerRequest'], 3)) + ' requests/show' - statComment += ' and a standard deviation of ' + str(round(serverStats['standardDeviation'], 3)) + '.' - - statComment += '\n\n' - - statComment += 'The most frequently requested anime/manga on this server are:\n\n' - - for i, request in enumerate(serverStats['topRequests']): - statComment += str(i + 1) + '. **' + str(request[0]) + '** (' + str(request[1]) + ' - ' + str(request[2]) + ' requests)\n' - - else: - statComment += 'There have been no requests on ' + str(server) + ' yet.' - - receipt += ' - ' + server.name - else: - basicStats = DatabaseHandler.getBasicStats(serverID) - - #The overall stats section - statComment += '**Overall Stats**\n\n' - - statComment += 'I\'ve searched through ' + str(basicStats['totalComments']) - statComment += ' unique comments and fulfilled a total of ' + str(basicStats['total']) - statComment += ' requests across ' + str(basicStats['uniqueSubreddits']) + ' unique server(s). ' - statComment += 'A total of ' + str(basicStats['uniqueNames']) - statComment += ' unique anime/manga have been requested, with a mean value of ' + str(round(basicStats['meanValuePerRequest'],3)) - statComment += ' requests/show and a standard deviation of ' + str(round(basicStats['standardDeviation'], 3)) + '.' - - statComment += '\n\n' - - statComment += 'The most frequently requested anime/manga overall are:\n\n' - - for i, request in enumerate(basicStats['topRequests']): - statComment += str(i + 1) + '. **' + str(request[0]) + '** (' + str(request[1]) + ' - ' + str(request[2]) + ' requests)\n' - - - statComment += '\n' - receipt += ' - Basic' - - print(receipt.encode('utf8')) - localEmbed = buildEmbedObject('Stats', '', statComment, '', False, '') - return localEmbed - except: - traceback.print_exc() - return None - -def buildHelpEmbed(): - try: - embedTitle = "Help" - helpComment = "You can call the bot by using specific tags on one of the active servers. Anime can be called using {curly braces}, manga can be called using and light novels can be called using reverse square brace ]light novels\[ (e.g. {Nisekoi} or or ]Utsuro no Hako to Zero no Maria\[). {Single} ]will\[ give you a normal set of information while {{double}} <> ]]will\[\[ give you expanded information. Examples of these requests can be found [here](https://github.com/dashwav/Discordoragi/wiki/Example-Output)" - localEmbed = buildEmbedObject(embedTitle, '', helpComment, '', False, '') - return localEmbed - except: - traceback.print_exc() - return None - -def buildEmbedObject(embedTitle, embedLinks, embedContent, embedThumbnail, isExpanded, descComment): - - localFooterTitle='\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_' - localFooter = "{anime}, , \]LN\[ | [FAQ](https://github.com/dashwav/Discordoragi/wiki) | [/r/](http://www.reddit.com/r/Roboragi/) | [Discord](https://discord.gg/SNv9yKs) | [Source](https://github.com/dashwav/Discordoragi) | [Synonyms](https://www.reddit.com/r/Roboragi/wiki/synonyms)" - try: - embed = Embed(title=embedTitle, description=embedLinks, type='rich') - - embed.set_thumbnail(url = embedThumbnail) - - embed.add_field(name='__Info__', value=embedContent) - - if isExpanded: - if len(descComment.rstrip()) > 1023: - descCommentCut = descComment.rstrip()[:1020] + '...' - embed.add_field(name ='__Description__', value = descCommentCut) - else: - embed.add_field(name = '__Description__', value = descComment) - - embed.add_field(name=localFooterTitle, value=localFooter) - return embed - except Exception as e: - print(e) - -def sanitise_url_for_markdown(url): - return url.replace('(', '\(').replace(')', '\)') \ No newline at end of file diff --git a/roboragi_old/Config.py.example b/roboragi_old/Config.py.example deleted file mode 100644 index 3dc67be..0000000 --- a/roboragi_old/Config.py.example +++ /dev/null @@ -1,18 +0,0 @@ -#Bot Info - Register your bot here: https://discordapp.com/developers/applications/me -clientid='id' -username='username' -token='token' - -#Database Info -dbname='discordoragi' -dbuser='discordoragi' -dbpassword='password' -dbhost='localhost' - -#Mal Info -maluseragent='' # A basic description of your program -malauth='' # Follow the instructions here: http://en.wikipedia.org/wiki/Basic_access_authentication - -#Anilist Info - Create an account on AniList the go to the developer tab on your profile page -aniclient='' -anisecret='' \ No newline at end of file diff --git a/roboragi_old/DatabaseHandler.py b/roboragi_old/DatabaseHandler.py deleted file mode 100644 index 76ff05d..0000000 --- a/roboragi_old/DatabaseHandler.py +++ /dev/null @@ -1,605 +0,0 @@ -''' -DatabaseHandler.py -Handles all connections to the database. The database runs on PostgreSQL and is connected to via psycopg2. -''' - -import psycopg2 -from psycopg2 import sql -from psycopg2.extras import Json, DictCursor - -import datetime -from math import sqrt -import traceback -import discord - -DBNAME = '' -DBUSER = '' -DBPASSWORD = '' -DBHOST = '' - -try: - import Config - DBNAME = Config.dbname - DBUSER = Config.dbuser - DBPASSWORD = Config.dbpassword - DBHOST = Config.dbhost -except ImportError: - pass - -conn = psycopg2.connect("dbname='" + DBNAME + "' user='" + DBUSER + "' host='" + DBHOST + "' password='" + DBPASSWORD + "'") -cur = conn.cursor() - -#Sets up the database and creates the databases if they haven't already been made. -def setup(): - try: - conn = psycopg2.connect("dbname='" + DBNAME + "' user='" + DBUSER + "' host='" + DBHOST + "' password='" + DBPASSWORD + "'") - except: - print("Unable to connect to the database") - - cur = conn.cursor() - - #Create requests table - try: - cur.execute('CREATE TABLE requests ( id SERIAL PRIMARY KEY, name varchar(320), type varchar(16), requester varchar(50), server varchar(50), requesttimestamp timestamp DEFAULT current_timestamp)') - conn.commit() - except Exception as e: - #traceback.print_exc() - cur.execute('ROLLBACK') - conn.commit() - - #Create messages table - try: - cur.execute('CREATE TABLE messages ( messageid varchar(32) PRIMARY KEY, requester varchar(50), server varchar(50), hadRequest boolean)') - conn.commit() - except Exception as e: - #traceback.print_exc() - cur.execute('ROLLBACK') - conn.commit() - - #Create malAnime table - try: - cur.execute('CREATE TABLE malanime ( id varchar(16) PRIMARY KEY, name varchar(320) , synonyms varchar(320)[], accesstimestamp timestamp DEFAULT current_timestamp, dict JSONB)') - conn.commit() - except Exception as e: - #traceback.print_exc() - cur.execute('ROLLBACK') - conn.commit() - - #Create malmanga table - try: - cur.execute('CREATE TABLE malmanga ( id varchar(16) PRIMARY KEY, name varchar(320) ,medium varchar(16), synonyms varchar(320)[], accesstimestamp timestamp DEFAULT current_timestamp, dict JSONB)') - conn.commit() - except Exception as e: - #traceback.print_exc() - cur.execute('ROLLBACK') - conn.commit() - - #Create anilistanime table - try: - cur.execute('CREATE TABLE anilistanime ( id varchar(16) PRIMARY KEY, name varchar(320) , synonyms varchar(320)[], accesstimestamp timestamp DEFAULT current_timestamp, dict JSONB)') - conn.commit() - except Exception as e: - #traceback.print_exc() - cur.execute('ROLLBACK') - conn.commit() - - #Create anilistmanga table - try: - cur.execute('CREATE TABLE anilistmanga ( id varchar(16) PRIMARY KEY, name varchar(320) ,medium varchar(16), synonyms varchar(320)[], accesstimestamp timestamp DEFAULT current_timestamp, dict JSONB)') - conn.commit() - except Exception as e: - #traceback.print_exc() - cur.execute('ROLLBACK') - conn.commit() - - try: - cur.execute('CREATE TABLE serverconfig (serverid varchar(50) PRIMARY KEY, allowexpanded varchar(16), allowstats varchar(16))') - conn.commit() - except Exception as e: - #traceback.print_exc() - cur.execute('ROLLBACK') - conn.commit() - -setup() - -#--------------------------------------# -# Server config - -def addServerToDatabase(serverId): - try: - - cur = conn.cursor(cursor_factory = DictCursor) - cur.execute('SELECT * FROM serverconfig WHERE serverid = (%s)', [str(serverId)]) - row = cur.fetchone() - if row is None: - cur.execute('INSERT INTO serverconfig (serverid, allowexpanded, allowstats) VALUES (%s, %s, %s)', [serverId, 'true', 'true']) - except: - traceback.print_exc() - cur.execute('ROLLBACK') - conn.commit() - -def toggleAllowExpanded(serverId): - try: - cur = conn.cursor(cursor_factory = DictCursor) - cur.execute('SELECT * FROM serverconfig WHERE serverid = (%s)', [str(serverId)]) - row = cur.fetchone() - if row is not None: - if row['allowexpanded'].lower() == 'true': - toggledSetting = 'false' - else: - toggledSetting = 'true' - cur.execute('UPDATE serverconfig SET allowexpanded= %s WHERE serverid = %s', [toggledSetting, serverId]) - return toggledSetting - except: - traceback.print_exc() - cur.execute('ROLLBACK') - conn.commit() - -def checkServerConfig(setting, serverId): - try: - cur = conn.cursor(cursor_factory = DictCursor) - cur.execute('SELECT * FROM serverconfig WHERE serverid = (%s)', [str(serverId)]) - row = cur.fetchone() - if row is not None: - if row[setting] == 'true': - return True - else: - return False - except: - traceback.print_exc() - cur.execute('ROLLBACK') - conn.commit() - -#--------------------------------------# -# Caching -def addMalEntry(table, anime): - try: - cur = conn.cursor(cursor_factory=DictCursor) - animeName = anime['title'] - animeID = anime['id'] - synonyms = anime['synonyms'] - animeSyn = [] - animeSyn.append(animeName.lower()) - if anime['synonyms']: - for synonym in anime['synonyms']: - animeSyn.append(synonym.lower().strip()) - if anime['english']: - animeSyn.append(anime['english'].lower()) - - cur.execute(sql.SQL("SELECT * FROM {} WHERE id = (%s)").format(sql.Identifier(table)), [str(animeID)]) - row = cur.fetchone() - - if row is not None: - timeDiff = datetime.datetime.now() - row['accesstimestamp'] - if timeDiff.days >= 1: - cur.execute(sql.SQL("UPDATE {} SET synonyms = %s, dict = %s, accesstimestamp = current_timestamp WHERE id = %s").format(sql.Identifier(table)), [animeSyn, Json(anime), str(animeID)]) - conn.commit() - print("updated info") - return - else: - return - - if 'novel' in anime['type'].lower() or 'manga' in anime['type'].lower(): - if 'novel' in anime['type'].lower(): - print("adding ln to mal") - novelOrManga = 'light novel' - else: - novelOrManga = 'manga' - - cur.execute(sql.SQL("INSERT into {} (id, name, medium, synonyms, dict) values (%s, %s, %s, %s, %s)").format(sql.Identifier(table)), [ str(animeID), animeName.lower(), novelOrManga, animeSyn, Json(anime)]) - conn.commit() - return - - cur.execute(sql.SQL("INSERT into {} (id, name, synonyms, dict) values (%s, %s, %s, %s)").format(sql.Identifier(table)), [ str(animeID), animeName.lower(), animeSyn, Json(anime)]) - conn.commit() - except Exception as e: - traceback.print_exc() - cur.execute('ROLLBACK') - conn.commit() - -def addAniEntry(table, anime): - try: - cur = conn.cursor(cursor_factory=DictCursor) - if anime['title_english']: - animeName = anime['title_english'] - elif anime['title_romaji']: - animeName = anime['title_romaji'] - animeID = anime['id'] - synonyms = anime['synonyms'] - novelOrManga = 'manga' - animeSyn = [] - animeSyn.append(animeName.lower()) - if anime['synonyms']: - for synonym in anime['synonyms']: - animeSyn.append(synonym.lower().strip()) - if anime['title_english']: - animeSyn.append(anime['title_english'].lower()) - elif anime['title_romaji']: - animeSyn.append(anime['title_romaji'].lower()) - - cur.execute(sql.SQL("SELECT * FROM {} WHERE id = (%s)").format(sql.Identifier(table)), [str(animeID)]) - row = cur.fetchone() - - if row is not None: - timeDiff = datetime.datetime.now() - row['accesstimestamp'] - if timeDiff.days >= 1: - cur.execute(sql.SQL("UPDATE {} SET synonyms = %s, dict = %s, accesstimestamp = current_timestamp WHERE id = %s").format(sql.Identifier(table)), [animeSyn, Json(anime), str(animeID)]) - conn.commit() - print("updated info") - return - else: - return - if anime['series_type'] == 'manga': - if anime['type'] == 'Novel': - print("light novel being added") - novelOrManga = 'light novel' - cur.execute(sql.SQL("INSERT into {} (id, name, medium, synonyms, dict) values (%s, %s, %s, %s, %s)").format(sql.Identifier(table)), [ str(animeID), animeName.lower(), novelOrManga, animeSyn, Json(anime)]) - conn.commit() - return - - cur.execute(sql.SQL("INSERT into {} (id, name, synonyms, dict) values (%s, %s, %s, %s)").format(sql.Identifier(table)), [ str(animeID), animeName.lower(), animeSyn, Json(anime)]) - conn.commit() - except Exception as e: - traceback.print_exc() - cur.execute('ROLLBACK') - conn.commit() - -def checkForMalEntry(table, name, animeId = None, isLN = None): - try: - cur = conn.cursor(cursor_factory=DictCursor) - nameInList = '{'+name.lower().strip()+'}' - if animeId is not None: - cur.execute(sql.SQL("SELECT * FROM {} WHERE id = (%s)").format(sql.Identifier(table)), [str(animeId)]) - else: - if table == 'malmanga' or table == 'anilistmanga': - if isLN: - cur.execute(sql.SQL("SELECT * FROM {} WHERE medium = %s AND synonyms @> %s").format(sql.Identifier(table)), ['light novel', nameInList]) - else: - cur.execute(sql.SQL("SELECT * FROM {} WHERE medium = %s AND synonyms @> %s").format(sql.Identifier(table)), ['manga', nameInList]) - else: - cur.execute(sql.SQL("SELECT * FROM {} WHERE synonyms @> %s").format(sql.Identifier(table)), [nameInList]) - row = cur.fetchone() - cachedReply = {} - - if row is not None: - #print("found cached entry") - timeDiff = datetime.datetime.now() - row['accesstimestamp'] - if timeDiff.days >= 1: - cachedReply['update'] = True - cachedReply['id'] = row['id'] - else: - cachedReply['update'] = False - cachedReply['content'] = row['dict'] - - return cachedReply - #print("didn't find cached entry in mal") - return None - - except Exception as e: - traceback.print_exc() - cur.execute('ROLLBACK') - conn.commit() - -def PopulateCache(table, content): - setup() - novelOrManga = 'manga' - try: - cur = conn.cursor(cursor_factory=DictCursor) - if table == 'malanime' or table == 'malmanga': - animeName = content['title'] - animeID = content['id'] - synonyms = content['synonyms'] - if 'novel' in content['type']: - print("adding ln to mal") - novelOrManga = 'light novel' - animeSyn = [] - animeSyn.append(animeName.lower()) - if content['synonyms']: - for synonym in content['synonyms']: - animeSyn.append(synonym.lower().strip()) - if content['english']: - animeSyn.append(content['english'].lower()) - else: - if content['title_english']: - animeName = content['title_english'] - elif content['title_romaji']: - animeName = content['title_romaji'] - if content['type'] == 'Novel': - print("light novel being added to ani") - novelOrManga = 'light novel' - animeID = content['id'] - synonyms = content['synonyms'] - animeSyn = [] - animeSyn.append(animeName.lower()) - if content['synonyms']: - for synonym in content['synonyms']: - animeSyn.append(synonym.lower().strip()) - - - if content['title_english']: - animeSyn.append(content['title_english'].lower()) - elif content['title_romaji']: - animeSyn.append(content['title_romaji'].lower()) - - cur.execute(sql.SQL("SELECT * FROM {} WHERE id = (%s)").format(sql.Identifier(table)), [str(animeID)]) - row = cur.fetchone() - - if row is not None: - return - else: - expired_date = "1999-01-08 04:05:06" - if table =='malmanga' or table == 'anilistmanga': - cur.execute(sql.SQL("INSERT into {} (id, name, medium, synonyms, accesstimestamp, dict) values (%s, %s, %s, %s, %s, %s)").format(sql.Identifier(table)), [ str(animeID), animeName.lower(), novelOrManga, animeSyn, expired_date, Json(content)]) - conn.commit() - return - cur.execute(sql.SQL("INSERT into {} (id, name, synonyms, accesstimestamp, dict) values (%s, %s, %s, %s, %s)").format(sql.Identifier(table)), [ str(animeID), animeName.lower(), animeSyn, expired_date, Json(content)]) - print("Added {} to the {}:\n".format(animeName, table)) - conn.commit() - except Exception as e: - traceback.print_exc() - cur.execute('ROLLBACK') - conn.commit() - -#--------------------------------------# - -# Adds a message to the "already seen" database. Also handles submissions, which have a similar ID structure. -def addMessage(messageid, requester, serverid, hadRequest): - try: - server = serverid.lower() - - cur.execute('INSERT INTO messages (messageid, requester, server, hadRequest) VALUES (%s, %s, %s, %s)', (messageid, requester, server, hadRequest)) - conn.commit() - except Exception as e: - traceback.print_exc() - cur.execute('ROLLBACK') - conn.commit() - -#Returns true if the message/submission has already been checked. -def messageExists(messageid): - try: - cur.execute('SELECT * FROM messages WHERE messageid = %s', (messageid,)) - if (cur.fetchone()) is None: - conn.commit() - return False - else: - conn.commit() - return True - except Exception as e: - traceback.print_exc() - cur.execute('ROLLBACK') - conn.commit() - return True - -#Adds a request to the request-tracking database. rType is either "Anime" or "Manga". -def addRequest(name, rType, requester, serverid): - try: - server = serverid.lower() - - if ('nihilate' not in server): - cur.execute('INSERT INTO requests (name, type, requester, server) VALUES (%s, %s, %s, %s)', (name, rType, requester, server)) - conn.commit() - except Exception as e: - traceback.print_exc() - cur.execute('ROLLBACK') - conn.commit() - -#Returns an object which contains data about the overall database stats (i.e. ALL servers). -def getBasicStats(serverID, top_media_number=5, top_username_number=5): - try: - basicStatDict = {} - - cur.execute("SELECT COUNT(1) FROM messages") - totalComments = int(cur.fetchone()[0]) - basicStatDict['totalComments'] = totalComments - - cur.execute("SELECT COUNT(1) FROM requests;") - total = int(cur.fetchone()[0]) - basicStatDict['total'] = total - - cur.execute("SELECT COUNT(1) FROM (SELECT DISTINCT name FROM requests) as temp;") - dNames = int(cur.fetchone()[0]) - basicStatDict['uniqueNames'] = dNames - - cur.execute("SELECT COUNT(1) FROM (SELECT DISTINCT server FROM requests) as temp;") - dSubreddits = int(cur.fetchone()[0]) - basicStatDict['uniqueSubreddits'] = dSubreddits - - meanValue = float(total)/dNames - basicStatDict['meanValuePerRequest'] = meanValue - - variance = 0 - cur.execute("SELECT name, count(name) FROM requests GROUP by name") - for entry in cur.fetchall(): - variance += (entry[1] - meanValue) * (entry[1] - meanValue) - - variance = variance / dNames - stdDev = sqrt(variance) - basicStatDict['standardDeviation'] = stdDev - - cur.execute("SELECT name, type, COUNT(name) FROM requests GROUP BY name, type ORDER BY COUNT(name) DESC, name ASC LIMIT %s", (top_media_number,)) - topRequests = cur.fetchall() - basicStatDict['topRequests'] = [] - for request in topRequests: - basicStatDict['topRequests'].append(request) - - cur.execute("SELECT requester, COUNT(requester), server, COUNT(server) FROM requests WHERE server = %s GROUP BY requester, server ORDER BY COUNT(requester) DESC, requester ASC, COUNT(server) DESC, server ASC LIMIT %s", (serverID, top_username_number,)) - topRequesters = cur.fetchall() - basicStatDict['topRequesters'] = [] - for requester in topRequesters: - basicStatDict['topRequesters'].append(requester) - - conn.commit() - return basicStatDict - - except Exception as e: - traceback.print_exc() - cur.execute('ROLLBACK') - conn.commit() - return None - -#Returns an object which contains request-specifc data. Basically just used for the expanded comments. -def getRequestStats(requestName, type): - try: - basicRequestDict = {} - - requestType = type - - cur.execute("SELECT COUNT(*) FROM requests") - total = int(cur.fetchone()[0]) - - cur.execute("SELECT COUNT(*) FROM requests WHERE name = %s AND type = %s", (requestName, requestType)) - requestTotal = int(cur.fetchone()[0]) - basicRequestDict['total'] = requestTotal - - if requestTotal == 0: - return None - - cur.execute("SELECT COUNT(DISTINCT server) FROM requests WHERE name = %s AND type = %s", (requestName, requestType)) - dSubreddits = int(cur.fetchone()[0]) - basicRequestDict['uniqueSubreddits'] = dSubreddits - - totalAsPercentage = (float(requestTotal)/total) * 100 - basicRequestDict['totalAsPercentage'] = totalAsPercentage - - conn.commit() - return basicRequestDict - - except: - cur.execute('ROLLBACK') - conn.commit() - return None - -#Returns an object which contains data about the overall database stats (i.e. ALL servers). -def getUserStats(username, top_media_number=5): - try: - basicUserStatDict = {} - username = str(username).lower() - - """ - cur.execute("SELECT COUNT(1) FROM messages where LOWER(requester) = %s", (username,)) - totalUserComments = int(cur.fetchone()[0]) - basicUserStatDict['totalUserComments'] = totalUserComments - - - cur.execute("SELECT COUNT(1) FROM messages") - totalNumComments = int(cur.fetchone()[0]) - totalCommentsAsPercentage = (float(totalUserComments)/totalNumComments) * 100 - basicUserStatDict['totalUserCommentsAsPercentage'] = totalCommentsAsPercentage - """ - - cur.execute("SELECT COUNT(*) FROM requests where LOWER(requester) = %s", (username,)) - totalUserRequests = int(cur.fetchone()[0]) - basicUserStatDict['totalUserRequests'] = totalUserRequests - - cur.execute("SELECT COUNT(1) FROM requests") - totalNumRequests = int(cur.fetchone()[0]) - totalRequestsAsPercentage = (float(totalUserRequests)/totalNumRequests) * 100 - basicUserStatDict['totalUserRequestsAsPercentage'] = totalRequestsAsPercentage - - cur.execute('''SELECT row FROM - (SELECT requester, count(1), ROW_NUMBER() over (order by count(1) desc) as row - from requests - group by requester) as overallrequestrank - where lower(requester) = %s''', (username,)) - overallRequestRank = int(cur.fetchone()[0]) - basicUserStatDict['overallRequestRank'] = overallRequestRank - - cur.execute("SELECT COUNT(DISTINCT (name, type)) FROM requests WHERE LOWER(requester) = %s", (username,)) - uniqueRequests = int(cur.fetchone()[0]) - basicUserStatDict['uniqueRequests'] = uniqueRequests - - - cur.execute('''select r.server, count(r.server), total.totalcount from requests r - inner join (select server, count(server) as totalcount from requests - group by server) total on total.server = r.server - where LOWER(requester) = %s - group by r.server, total.totalcount - order by count(r.server) desc - limit 1 - ''', (username,)) - favouriteSubredditStats = cur.fetchone() - favouriteSubreddit = str(favouriteSubredditStats[0]) - favouriteSubredditCount = int(favouriteSubredditStats[1]) - favouriteSubredditOverallCount = int(favouriteSubredditStats[2]) - basicUserStatDict['favouriteSubreddit'] = favouriteSubreddit - basicUserStatDict['favouriteSubredditCount'] = favouriteSubredditCount - basicUserStatDict['favouriteSubredditCountAsPercentage'] = (float(favouriteSubredditCount)/favouriteSubredditOverallCount) * 100 - - cur.execute('''SELECT name, type, COUNT(name) FROM requests where LOWER(requester) = %s - GROUP BY name, type ORDER BY COUNT(name) DESC, name ASC LIMIT %s''', (username, top_media_number)) - topRequests = cur.fetchall() - basicUserStatDict['topRequests'] = [] - for request in topRequests: - basicUserStatDict['topRequests'].append(request) - - conn.commit() - return basicUserStatDict - - except Exception as e: - cur.execute('ROLLBACK') - conn.commit() - return None - -#Similar to getBasicStats - returns an object which contains data about a specific server. -def getSubredditStats(server, top_media_number=5, top_username_number=5): - try: - basicSubredditDict = {} - print(server.name+"\n") - print(server.id+"\n") - serverID = server.id - - """ - cur.execute("SELECT COUNT(*) FROM messages WHERE server = %s", (serverID,)) - totalComments = int(cur.fetchone()[0]) - basicSubredditDict['totalComments'] = totalComments - """ - - cur.execute("SELECT COUNT(*) FROM requests;") - total = int(cur.fetchone()[0]) - - cur.execute("SELECT COUNT(*) FROM requests WHERE server = %s", (serverID,)) - sTotal = int(cur.fetchone()[0]) - basicSubredditDict['total'] = sTotal - - if sTotal == 0: - return None - - cur.execute("SELECT COUNT(DISTINCT (name, type)) FROM requests WHERE server = %s", (serverID,)) - dNames = int(cur.fetchone()[0]) - basicSubredditDict['uniqueNames'] = dNames - - totalAsPercentage = (float(sTotal)/total) * 100 - basicSubredditDict['totalAsPercentage'] = totalAsPercentage - - meanValue = float(sTotal)/dNames - basicSubredditDict['meanValuePerRequest'] = meanValue - - variance = 0 - cur.execute("SELECT name, type, count(name) FROM requests WHERE server = %s GROUP by name, type", (serverID,)) - for entry in cur.fetchall(): - variance += (entry[2] - meanValue) * (entry[2] - meanValue) - - variance = variance / dNames - stdDev = sqrt(variance) - basicSubredditDict['standardDeviation'] = stdDev - - cur.execute("SELECT name, type, COUNT(name) FROM requests WHERE server = %s GROUP BY name, type ORDER BY COUNT(name) DESC, name ASC LIMIT %s", (serverID, top_media_number)) - topRequests = cur.fetchall() - basicSubredditDict['topRequests'] = [] - for request in topRequests: - basicSubredditDict['topRequests'].append(request) - - cur.execute("SELECT requester, COUNT(requester) FROM requests WHERE server = %s GROUP BY requester ORDER BY COUNT(requester) DESC, requester ASC LIMIT %s", (serverID, top_username_number)) - topRequesters = cur.fetchall() - basicSubredditDict['topRequesters'] = [] - for requester in topRequesters: - basicSubredditDict['topRequesters'].append(requester) - - conn.commit() - - return basicSubredditDict - except Exception as e: - traceback.print_exc() - cur.execute('ROLLBACK') - conn.commit() - return None diff --git a/roboragi_old/Discord.py b/roboragi_old/Discord.py deleted file mode 100644 index 3d0a1de..0000000 --- a/roboragi_old/Discord.py +++ /dev/null @@ -1,28 +0,0 @@ -''' -Discord.py -Used for communication with Discord -''' - -import discord -import asyncio - -try: - import Config - print('Getting Config Info') - TOKEN = Config.token -except ImportError: - pass - -client = discord.Client() - -def run(): - client.run(TOKEN) - -def getMemberFromID(userID, server): - return discord.utils.get(server.members, id=userID) - -def getServerFromID(serverID): - return discord.utils.get(Discord.client.servers, id=serverID) - -def getServerFromName(serverName): - return discord.utils.get(Discord.client.servers, name=serverName) diff --git a/roboragi_old/DiscordoragiSearch.py b/roboragi_old/DiscordoragiSearch.py deleted file mode 100644 index f871862..0000000 --- a/roboragi_old/DiscordoragiSearch.py +++ /dev/null @@ -1,482 +0,0 @@ -''' -DiscordoragiSearch .py -Returns a built comment created from multiple databases when given a search term. -''' - -import MAL -import AnimePlanet as AniP -import AniDB -import Hummingbird -import Anilist -import MU -import NU -import LNDB - -import CommentBuilder -import DatabaseHandler - -import traceback -import time - -import sqlite3 -import json - -import asyncio -import pprint - -USERNAME = '' - -try: - import Config - USERNAME = Config.username -except ImportError: - pass - -sqlConn = sqlite3.connect('synonyms.db') -sqlCur = sqlConn.cursor() - -try: - sqlCur.execute('SELECT dbLinks FROM synonyms WHERE type = "Manga" and lower(name) = ?', ["despair simulator"]) -except sqlite3.Error: - traceback.print_exc() - -#Checks if the message is valid (i.e. not already seen, not a post by Roboragi and the parent commenter isn't Roboragi) -def isValidMessage(message): - try: - if (DatabaseHandler.messageExists(message.id)): - return False - - try: - if (message.author.name == USERNAME): - DatabaseHandler.addMessage(message.id, message.author.id, message.server.id, False) - return False - except: - pass - - return True - - except: - traceback.print_exc() - return False - -#Builds a manga reply from multiple sources -async def buildMangaReply(searchText, message, isExpanded, canEmbed, blockTracking=False): - try: - ani = None - mal = None - mu = None - ap = None - - try: - sqlCur.execute('SELECT dbLinks FROM synonyms WHERE type = "Manga" and lower(name) = ?', [searchText.lower()]) - except sqlite3.Error as e: - print(e) - - alternateLinks = sqlCur.fetchone() - - if (alternateLinks): - synonym = json.loads(alternateLinks[0]) - - if 'mal' in synonym: - if (synonym['mal']): - mal = await MAL.getMangaDetails(synonym['mal'][0], synonym['mal'][1]) - - if 'ani' in synonym: - if (synonym['ani']): - ani = await Anilist.getMangaDetailsById(synonym['ani']) - - if 'mu' in synonym: - if (synonym['mu']): - mu = MU.getMangaURLById(synonym['mu']) - - if 'ap' in synonym: - if (synonym['ap']): - ap = AniP.getMangaURLById(synonym['ap']) - - else: - #Basic breakdown: - #If Anilist finds something, use it to find the MAL version. - #If hits either MAL or Ani, use it to find the MU version. - #If it hits either, add it to the request-tracking DB. - ani = await Anilist.getMangaDetails(searchText) - - if ani: - try: - mal = await MAL.getMangaDetails(ani['title_romaji']) - except Exception as e: - print(e) - pass - - if not mal: - try: - mal = await MAL.getMangaDetails(ani['title_english']) - except: - pass - - if not mal: - mal = await MAL.getMangaDetails(searchText) - - else: - mal = await MAL.getMangaDetails(searchText) - - if mal: - ani = await Anilist.getMangaDetails(mal['title']) - - #----- Finally... -----# - if ani or mal: - try: - titleToAdd = '' - if mal: - titleToAdd = mal['title'] - else: - try: - titleToAdd = ani['title_english'] - except: - titleToAdd = ani['title_romaji'] - - - if not alternateLinks: - #MU stuff - if mal: - mu = await MU.getMangaURL(mal['title']) - else: - mu = await MU.getMangaURL(ani['title_romaji']) - - #Do the anime-planet stuff - if mal and not ap: - if mal['title'] and not ap: - ap = await AniP.getMangaURL(mal['title']) - if mal['english'] and not ap: - ap = await AniP.getMangaURL(mal['english']) - if mal['synonyms'] and not ap: - for synonym in mal['synonyms']: - if ap: - break - ap = await AniP.getMangaURL(synonym) - - if ani and not ap: - if ani['title_english'] and not ap: - ap = await AniP.getMangaURL(ani['title_english']) - if ani['title_romaji'] and not ap: - ap = await AniP.getMangaURL(ani['title_romaji']) - if ani['synonyms'] and not ap: - for synonym in ani['synonyms']: - if ap: - break - ap = await AniP.getMangaURL(synonym) - if not blockTracking: - DatabaseHandler.addRequest(titleToAdd, 'Manga', message.author.id, message.server.id) - except: - traceback.print_exc() - pass - if mal: - try: - DatabaseHandler.addMalEntry('malmanga', mal) - except: - traceback.print_exc() - pass - if ani: - try: - DatabaseHandler.addAniEntry('anilistmanga', ani) - except: - traceback.print_exc() - pass - if not canEmbed: - return CommentBuilder.buildMangaComment(isExpanded, mal, ani, mu, ap) - else: - return CommentBuilder.buildMangaEmbed(isExpanded, mal, ani, mu, ap) - except Exception as e: - traceback.print_exc() - return None - -#Builds a manga search for a specific series by a specific author -async def buildMangaReplyWithAuthor(searchText, authorName, message, isExpanded, canEmbed, blockTracking=False): - try: - ani = await Anilist.getMangaWithAuthor(searchText, authorName) - mal = None - mu = None - ap = None - - if ani: - try: - mal = await MAL.getMangaCloseToDescription(searchText, ani['description']) - ap = await AniP.getMangaURL(ani['title_english'], authorName) - except Exception as e: - print(e) - else: - ap = await AniP.getMangaURL(searchText, authorName) - - mu = await MU.getMangaWithAuthor(searchText, authorName) - - if ani: - try: - titleToAdd = '' - if mal is not None: - titleToAdd = mal['title'] - else: - titleToAdd = ani['title_english'] - - if not blockTracking: - DatabaseHandler.addRequest(titleToAdd, 'Manga', message.author.id, message.server.id) - except: - traceback.print_exc() - pass - - if not canEmbed: - return CommentBuilder.buildMangaComment(isExpanded, mal, ani, mu, ap) - else: - return CommentBuilder.buildMangaEmbed(isExpanded, mal, ani, mu, ap) - - except Exception as e: - traceback.print_exc() - return None - -#Builds an anime reply from multiple sources -async def buildAnimeReply(searchText, message, isExpanded, canEmbed, blockTracking=False): - try: - mal = {'search_function': MAL.getAnimeDetails, - 'synonym_function': MAL.getSynonyms, - 'checked_synonyms': [], - 'result': None} - ani = {'search_function': Anilist.getAnimeDetails, - 'synonym_function': Anilist.getSynonyms, - 'checked_synonyms': [], - 'result': None} - ap = {'search_function': AniP.getAnimeURL, - 'result': None} - adb = {'search_function': AniDB.getAnimeURL, - 'result': None} - - try: - sqlCur.execute('SELECT dbLinks FROM synonyms WHERE type = "Anime" and lower(name) = ?', [searchText.lower()]) - except sqlite3.Error as e: - print(e) - - alternateLinks = sqlCur.fetchone() - - if (alternateLinks): - synonym = json.loads(alternateLinks[0]) - - if synonym: - malsyn = None - if 'mal' in synonym and synonym['mal']: - malsyn = synonym['mal'] - anisyn = None - if 'ani' in synonym and synonym['ani']: - anisyn = synonym['ani'] - - apsyn = None - if 'ap' in synonym and synonym['ap']: - apsyn = synonym['ap'] - - adbsyn = None - if 'adb' in synonym and synonym['adb']: - adbsyn = synonym['adb'] - - mal['result'] = await MAL.getAnimeDetails(malsyn[0],malsyn[1]) if malsyn else None - ani['result'] = await Anilist.getAnimeDetailsById(anisyn) if anisyn else None - ap['result'] = AniP.getAnimeURLById(apsyn) if apsyn else None - adb['result'] = AniDB.getAnimeURLById(adbsyn) if adbsyn else None - print(ani['result']) - - else: - data_sources = [ani, mal] - aux_sources = [ap, adb] - #aux_sources = [ap] - - synonyms = set([searchText]) - - for x in range(len(data_sources)): - for source in data_sources: - if source['result']: - break - else: - for synonym in synonyms: - if synonym in source['checked_synonyms']: - continue - - source['result'] = await source['search_function'](synonym) - source['checked_synonyms'].append(synonym) - - if source['result']: - break - - if source['result']: - synonyms.update([synonym.lower() for synonym in source['synonym_function'](source['result'])]) - - for source in aux_sources: - for synonym in synonyms: - source['result'] = await source['search_function'](synonym) - - if source['result']: - break - - if ani['result'] or mal['result']: - try: - titleToAdd = '' - if mal['result']: - if 'title' in mal['result']: - titleToAdd = mal['result']['title'] - '''if hb['result']: - if 'title' in hb['result']: - titleToAdd = hb['result']['title']''' - if ani['result']: - if 'title_romaji' in ani['result']: - titleToAdd = ani['result']['title_romaji'] - - if not blockTracking: - DatabaseHandler.addRequest(titleToAdd, 'Anime', message.author.id, message.server.id) - except: - traceback.print_exc() - pass - if mal['result']: - print('trying to add an anime to cache') - try: - DatabaseHandler.addMalEntry('malanime', mal['result']) - except: - traceback.print_exc() - pass - if ani: - try: - DatabaseHandler.addAniEntry('anilistanime', ani['result']) - except: - traceback.print_exc() - pass - if not canEmbed: - return CommentBuilder.buildAnimeComment(isExpanded, mal['result'], ani['result'], ap['result'], adb['result']) - else: - return CommentBuilder.buildAnimeEmbed(isExpanded, mal['result'], ani['result'], ap['result'], adb['result']) - - except Exception as e: - traceback.print_exc() - return None - -#Builds an LN reply from multiple sources -async def buildLightNovelReply(searchText, isExpanded, message, canEmbed, blockTracking=False): - try: - mal = {'search_function': MAL.getLightNovelDetails, - 'synonym_function': MAL.getSynonyms, - 'checked_synonyms': [], - 'result': None} - ani = {'search_function': Anilist.getLightNovelDetails, - 'synonym_function': Anilist.getSynonyms, - 'checked_synonyms': [], - 'result': None} - nu = {'search_function': NU.getLightNovelURL, - 'result': None} - lndb = {'search_function': LNDB.getLightNovelURL, - 'result': None} - - try: - sqlCur.execute('SELECT dbLinks FROM synonyms WHERE type = "LN" and lower(name) = ?', [searchText.lower()]) - except sqlite3.Error as e: - print(e) - - alternateLinks = sqlCur.fetchone() - - if (alternateLinks): - synonym = json.loads(alternateLinks[0]) - - if synonym: - malsyn = None - if 'mal' in synonym and synonym['mal']: - malsyn = synonym['mal'] - - anisyn = None - if 'ani' in synonym and synonym['ani']: - anisyn = synonym['ani'] - - nusyn = None - if 'nu' in synonym and synonym['nu']: - nusyn = synonym['nu'] - - lndbsyn = None - if 'lndb' in synonym and synonym['lndb']: - lndbsyn = synonym['lndb'] - - mal['result'] = await MAL.getLightNovelDetails(malsyn[0],malsyn[1]) if malsyn else None - ani['result'] = await Anilist.getMangaDetailsById(anisyn) if anisyn else None - nu['result'] = NU.getLightNovelById(nusyn) if nusyn else None - lndb['result'] =LNDB.getLightNovelById(lndbsyn) if lndbsyn else None - - else: - data_sources = [ani, mal] - aux_sources = [nu, lndb] - - synonyms = set([searchText]) - - for x in range(len(data_sources)): - for source in data_sources: - if source['result']: - break - else: - for synonym in synonyms: - if synonym in source['checked_synonyms']: - continue - - source['result'] = await source['search_function'](synonym) - source['checked_synonyms'].append(synonym) - - if source['result']: - break - - if source['result']: - synonyms.update([synonym.lower() for synonym in source['synonym_function'](source['result'])]) - - for source in aux_sources: - for synonym in synonyms: - source['result'] =await source['search_function'](synonym) - - if source['result']: - break - - if ani['result'] or mal['result']: - try: - titleToAdd = '' - if mal['result']: - titleToAdd = mal['result']['title'] - if ani['result']: - try: - titleToAdd = ani['result']['title_romaji'] - except: - titleToAdd = ani['result']['title_english'] - - if (str(message.server).lower is not 'nihilate') and (str(message.server).lower is not 'roboragi') and not blockTracking: - DatabaseHandler.addRequest(titleToAdd, 'LN', message.author.id, message.server.id) - except: - traceback.print_exc() - pass - if mal['result']: - try: - DatabaseHandler.addMalEntry('malmanga', mal['result']) - except: - traceback.print_exc() - pass - if ani['result']: - try: - DatabaseHandler.addAniEntry('anilistmanga', ani['result']) - except: - traceback.print_exc() - pass - if not canEmbed: - return CommentBuilder.buildLightNovelComment(isExpanded, mal['result'], ani['result'], nu['result'], lndb['result']) - else: - return CommentBuilder.buildLightNovelEmbed(isExpanded, mal['result'], ani['result'], nu['result'], lndb['result']) - except Exception as e: - traceback.print_exc() - return None - -#Checks if the bot is the parent of this comment. -def isBotAParent(comment, reddit): - try: - parentComment = reddit.get_info(thing_id=comment.parent_id) - - if (parentComment.author.name == USERNAME): - return True - else: - return False - - except: - #traceback.print_exc() - return False - diff --git a/roboragi_old/Hummingbird.py b/roboragi_old/Hummingbird.py deleted file mode 100644 index bc83ea9..0000000 --- a/roboragi_old/Hummingbird.py +++ /dev/null @@ -1,69 +0,0 @@ -''' -Hummingbird.py -Handles all of the connections to Hummingbird. -''' -import aiohttp -import difflib -import requests -import traceback -import pprint - -session = aiohttp.ClientSession() - -def getSynonyms(request): - synonyms = [] - - synonyms.append(request['title']) if request['title'] else None - synonyms.append(request['alternate_title']) if request['alternate_title'] else None - - return synonyms - -#Returns the closest anime (as a Json-like object) it can find using the given searchtext -async def getAnimeDetails(searchText): - try: - request = await session.get('https://hummingbird.me/api/v1/search/anime?query=' + searchText.lower(), timeout=10) - closestAnime = getClosestAnime(searchText, request.json()) - - if not (closestAnime is None): - return closestAnime - else: - return None - - except Exception as e: - print(e) - return None - -#Returns the closest anime by id -async def getAnimeDetailsById(animeId): - try: - response = await session.get('http://hummingbird.me/api/v1/anime/' + str(animeId), timeout=10) - - return response.json() - except Exception as e: - return None - -#Sometimes the "right" anime isn't at the top of the list, so we get the titles -#of everything and do some fuzzy string searching against the search text -def getClosestAnime(searchText, animeList): - try: - animeNameList = [] - - for anime in animeList: - animeNameList.append(anime['title'].lower()) - - if anime['alternate_title'] is not None: - animeNameList.append(anime['alternate_title'].lower()) - - closestNameFromList = difflib.get_close_matches(searchText.lower(), animeNameList, 1, 0.95)[0] - - - for anime in animeList: - if anime['title'].lower() == closestNameFromList.lower(): - return anime - elif anime['alternate_title'] is not None: - if anime['alternate_title'].lower() == closestNameFromList.lower(): - return anime - - return None - except: - return None diff --git a/roboragi_old/LNDB.py b/roboragi_old/LNDB.py deleted file mode 100644 index 25d142a..0000000 --- a/roboragi_old/LNDB.py +++ /dev/null @@ -1,67 +0,0 @@ -''' -LNDB.py -Handles all LNDB information -''' - -from pyquery import PyQuery as pq -import requests -import aiohttp -import difflib -import traceback -import pprint -import collections - -session = aiohttp.ClientSession() - -async def getLightNovelURL(searchText): - try: - searchText = searchText.replace(' ', '+') - async with session.get('http://lndb.info/search?text=' + searchText, timeout=10) as resp: - html = await resp.text() - - lndb = pq(html) - - lnList = [] - - if 'light_novel' in html.url: - #we've immediately hit a result - return html.url - else: - #scan the search page for stuff - - lnList = [] - - for thing in lndb.find('#bodylightnovelscontentid table tr'): - title = pq(thing).find('a').text() - url = pq(thing).find('a').attr('href') - - if title: - data = { 'title': title, - 'url': url } - lnList.append(data) - - closest = findClosestLightNovel(searchText, lnList) - return closest['url'] - - except Exception as e: - return None - -def findClosestLightNovel(searchText, lnList): - try: - nameList = [] - - for ln in lnList: - nameList.append(ln['title'].lower()) - - closestNameFromList = difflib.get_close_matches(searchText.lower(), nameList, 1, 0.80) - - for ln in lnList: - if ln['title'].lower() == closestNameFromList[0].lower(): - return ln - - return None - except: - return None - -def getLightNovelById(lnId): - return 'http://lndb.info/light_novel/' + str(lnId) diff --git a/roboragi_old/MAL.py b/roboragi_old/MAL.py deleted file mode 100644 index 75ab87d..0000000 --- a/roboragi_old/MAL.py +++ /dev/null @@ -1,412 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -MAL.py -Handles all of the connections to MyAnimeList. -""" - -import xml.etree.cElementTree as ET -import DatabaseHandler -import aiohttp -import traceback -import pprint -import difflib -import urllib - -try: - import Config - print('Setting up MAL Connection') - MALUSERAGENT = Config.maluseragent - MALAUTH = Config.malauth -except ImportError: - pass - -try: - mal = aiohttp.ClientSession(headers = {'Authorization': MALAUTH, 'User-Agent': MALUSERAGENT}) -except Exception as e: - print(e) - - -#Sets up the connection to MAL. -def setup(): - mal = aiohttp.ClientSession(headers = {'Authorization': MALAUTH, 'User-Agent': MALUSERAGENT}) - -def getSynonyms(request): - synonyms = [] - - synonyms.append(request['title']) if request['title'] else None - synonyms.append(request['english']) if request['english'] else None - synonyms.extend(request['synonyms']) if request['synonyms'] else None - - return synonyms - -#Returns the closest anime (as a Json-like object) it can find using the given searchtext. MAL returns XML (bleh) so we have to convert it ourselves. -async def getAnimeDetails(searchText, animeId=None): - cachedAnime = DatabaseHandler.checkForMalEntry('malanime', searchText, animeId) - if cachedAnime is not None: - if cachedAnime['update']: - print("found cached anime, needs update in mal") - pass - else: - print("found cached anime, doesn't need update in mal") - return cachedAnime['content'] - - cleanSearchText = urllib.parse.quote(searchText) - try: - try: - async with mal.get('https://myanimelist.net/api/anime/search.xml?q=' + cleanSearchText.rstrip(), timeout=10) as resp: - if resp.status != 200: - print("Searching for {} failed with error code {}".format(searchText.rstrip(), resp.status)) - request = await resp.text() - except Exception as e: - print(e) - setup() - try: - async with mal.get('https://myanimelist.net/api/anime/search.xml?q=' + searchText.rstrip(), timeout=10) as resp: - request = await resp.text() - except aiohttp.exceptions.RequestException as e: # This is the correct syntax - print(e) - - #convertedRequest = convertShittyXML(request) - rawList = ET.fromstring(request) - - - animeList = [] - - for anime in rawList.findall('./entry'): - animeID = anime.find('id').text - title = anime.find('title').text - title_english = anime.find('english').text - - synonyms = None - if anime.find('synonyms').text is not None: - synonyms = anime.find('synonyms').text.split(";") - - episodes = anime.find('episodes').text - animeType = anime.find('type').text - status = anime.find('status').text - start_date = anime.find('start_date').text - end_date = anime.find('end_date').text - synopsis = anime.find('synopsis').text - image = anime.find('image').text - - data = {'id': animeID, - 'title': title, - 'english': title_english, - 'synonyms': synonyms, - 'episodes': episodes, - 'type': animeType, - 'status': status, - 'start_date': start_date, - 'end_date': end_date, - 'synopsis': synopsis, - 'image': image} - - animeList.append(data) - - if animeId: - closestAnime = getThingById(animeId, animeList) - elif cachedAnime and cachedAnime['update']: - closestAnime = getThingById(cachedAnime['id'], animeList) - else: - closestAnime = getClosestAnime(searchText.strip(), animeList) - - return closestAnime - - except Exception as e: - print("Error finding anime:{} on MAL\nError:{}".format(searchText, e)) - #traceback.print_exc() - - return None - -#Given a list, it finds the closest anime series it can. -def getClosestAnime(searchText, animeList): - try: - nameList = [] - for anime in animeList: - nameList.append(anime['title'].lower().strip()) - - if anime['english'] is not None: - nameList.append(anime['english'].lower().strip()) - - if anime['synonyms']: - for synonym in anime['synonyms']: - nameList.append(synonym.lower().strip()) - - closestNameFromList = difflib.get_close_matches(searchText.lower(), nameList, cutoff=0.90)[0] - - for anime in animeList: - if anime['title']: - if anime['title'].lower() == closestNameFromList.lower(): - return anime - elif anime['english']: - if anime['english'].lower() == closestNameFromList.lower(): - return anime - else: - for synonym in anime['synonyms']: - if synonym.lower() == closestNameFromList.lower(): - return anime - - return None - except Exception: - #print("Error finding anime:{} on MAL\nError:{}".format(searchText, e)) - #traceback.print_exc() - return None - -#MAL's XML is a piece of crap. It needs to be escaped twice because they do shit like this: &sup2; -def convertShittyXML(text): - import html.parser - #It pains me to write shitty code, but MAL needs to improve their API and I'm sick of not being able to parse shit - text = text.replace('É', 'É').replace('×', 'x').replace('’', "'").replace('‘', "'").replace('&hellip', '...').replace('&le', '<').replace('<;', '; ').replace('♥', '♥').replace('—', '-') - text = text.replace('é', 'é').replace('–', '-').replace('Á', 'Á').replace('´', 'à').replace('“', '"').replace('”', '"').replace('Ø', 'Ø').replace('½', '½').replace('∞', '∞') - text = text.replace('à', 'à').replace('è', 'è').replace('†', '†').replace('²', '²').replace(''', "'") - - #text = text.replace('&', '&') - - return text - - - text=html.parser.HTMLParser().unescape(text) - return html.parser.HTMLParser().unescape(text) - -#Used to check if two descriptions are relatively close. This is used in place of author searching because MAL don't give authors at any point. -def getClosestFromDescription(mangaList, descriptionToCheck): - try: - descList = [] - for manga in mangaList: - descList.append(manga['synopsis'].lower()) - - closestNameFromList = difflib.get_close_matches(descriptionToCheck.lower(), descList, 1, 0.1)[0] - - for manga in mangaList: - if closestNameFromList == manga['synopsis'].lower(): - return manga - - except: - return None - -#Since MAL doesn't give me an author, I make a search using similar descriptions instead. Super janky. -async def getMangaCloseToDescription(searchText, descriptionToCheck): - cleanSearchText = urllib.parse.quote(searchText) - try: - try: - async with mal.get('https://myanimelist.net/api/manga/search.xml?q=' + cleanSearchText.rstrip(), timeout=10) as resp: - request = await resp.text() - - except: - setup() - async with mal.get('https://myanimelist.net/api/manga/search.xml?q=' + cleanSearchText.rstrip(), timeout=10) as resp: - request = await resp.text() - - - convertedRequest = convertShittyXML(request) - #print(convertedRequest) - rawList = ET.fromstring(convertedRequest) - - mangaList = [] - - for manga in rawList.findall('./entry'): - mangaId = manga.find('id').text - title = manga.find('title').text - title_english = manga.find('english').text - - synonyms = None - if manga.find('synonyms').text is not None: - synonyms = manga.find('synonyms').text.split(";") - - chapters = manga.find('chapters').text - volumes = manga.find('volumes').text - mangaType = manga.find('type').text - status = manga.find('status').text - start_date = manga.find('start_date').text - end_date = manga.find('end_date').text - synopsis = manga.find('synopsis').text - image = manga.find('image').text - - data = {'id': mangaId, - 'title': title, - 'english': title_english, - 'synonyms': synonyms, - 'chapters': chapters, - 'volumes': volumes, - 'type': mangaType, - 'status': status, - 'start_date': start_date, - 'end_date': end_date, - 'synopsis': synopsis, - 'image': image} - - mangaList.append(data) - - closeManga = getListOfCloseManga(searchText, mangaList) - - return getClosestFromDescription(closeManga, descriptionToCheck) - except: - print("Error finding manga:{} on MAL\nError:{}".format(searchText, e)) - #traceback.print_exc() - return None - -async def getLightNovelDetails(searchText, lnId=None): - return await getMangaDetails(searchText, lnId, True) - -#Returns the closest manga series given a specific search term. Again, MAL returns XML, so we conver it ourselves -async def getMangaDetails(searchText, mangaId=None, isLN=False): - cachedManga = DatabaseHandler.checkForMalEntry('malmanga', searchText, mangaId, isLN) - if cachedManga is not None: - if cachedManga['update']: - print("found cached anime, needs update in mal") - pass - else: - print("found cached anime, doesn't need update in mal") - return cachedManga['content'] - cleanSearchText = urllib.parse.quote(searchText) - try: - try: - async with mal.get('https://myanimelist.net/api/manga/search.xml?q=' + cleanSearchText.rstrip(), timeout=10) as resp: - request = await resp.text() - - except Exception as e: - print(e) - setup() - async with mal.get('https://myanimelist.net/api/manga/search.xml?q=' + cleanSearchText.rstrip(), timeout=10) as resp: - request = await resp.text() - - - #convertedRequest = convertShittyXML(request) - rawList = ET.fromstring(request) - #print(convertedRequest) - - mangaList = [] - - for manga in rawList.findall('./entry'): - newMangaId= manga.find('id').text - title = manga.find('title').text - title_english = manga.find('english').text - - synonyms = None - if manga.find('synonyms').text is not None: - synonyms = manga.find('synonyms').text.split(";") - - chapters = manga.find('chapters').text - volumes = manga.find('volumes').text - mangaType = manga.find('type').text - status = manga.find('status').text - start_date = manga.find('start_date').text - end_date = manga.find('end_date').text - synopsis = manga.find('synopsis').text - image = manga.find('image').text - - data = {'id': newMangaId, - 'title': title, - 'english': title_english, - 'synonyms': synonyms, - 'chapters': chapters, - 'volumes': volumes, - 'type': mangaType, - 'status': status, - 'start_date': start_date, - 'end_date': end_date, - 'synopsis': synopsis, - 'image': image } - - #print(data['title']) - #ignore or allow LNs - if 'novel' in mangaType.lower(): - if isLN: - mangaList.append(data) - else: - if not isLN: - mangaList.append(data) - #print(mangaId) - if mangaId: - closestManga = getThingById(mangaId, mangaList) - elif cachedManga and cachedManga['update']: - closestManga = getThingById(cachedManga['id'], mangaList) - else: - closestManga = getClosestManga(searchText.strip(), mangaList) - - if closestManga: - return closestManga - else: - return None - - except Exception as e: - print("Error finding manga:{} on MAL\nError:{}".format(searchText, e)) - #traceback.print_exc() - return None - -#Returns a list of manga with titles very close to the search text. Current unused because MAL's API is shit and doesn't return author names. -def getListOfCloseManga(searchText, mangaList): - try: - ratio = 0.90 - returnList = [] - - for manga in mangaList: - if round(difflib.SequenceMatcher(lambda x: x == "", manga['title'].lower(), searchText.lower()).ratio(), 3) >= ratio: - returnList.append(manga) - elif manga['english'] is not None: - if round(difflib.SequenceMatcher(lambda x: x == "", manga['english'].lower(), searchText.lower()).ratio(), 3) >= ratio: - returnList.append(manga) - elif manga['synonyms'] is not None: - for synonym in manga['synonyms']: - if round(difflib.SequenceMatcher(lambda x: x == "", synonym, searchText.lower()).ratio(), 3) >= ratio: - returnList.append(manga) - break - - return returnList - - except Exception: - traceback.print_exc() - return None - -#Used to determine the closest manga to a given search term in a list -def getClosestManga(searchText, mangaList): - try: - nameList = [] - - for manga in mangaList: - nameList.append(manga['title'].lower().strip()) - - if manga['english'] is not None: - nameList.append(manga['english'].lower().strip()) - - if manga['synonyms'] is not None: - for synonym in manga['synonyms']: - nameList.append(synonym.lower().strip()) - #print(searchText) - closestNameFromList = difflib.get_close_matches(searchText.lower().strip(), nameList,1, 0.90)[0] - #print(closestNameFromList) - for manga in mangaList: - if manga['title'].lower() == closestNameFromList.lower(): - return manga - elif manga['english'] is not None: - if manga['english'].lower() == closestNameFromList.lower(): - return manga - - for manga in mangaList: - if manga['synonyms'] is not None: - for synonym in manga['synonyms']: - if synonym.lower().strip() == closestNameFromList.lower(): - return manga - - return None - except Exception as e: - #print("Error finding manga:{} on MAL\nError:{}".format(searchText, e)) - #traceback.print_exc() - return None - - -#Used to find thing by an id -def getThingById(thingId, thingList): - try: - for thing in thingList: - if int(thing['id']) == int(thingId): - return thing - - return None - except Exception: - traceback.print_exc() - return None - - -setup() diff --git a/roboragi_old/MU.py b/roboragi_old/MU.py deleted file mode 100644 index 26cda6f..0000000 --- a/roboragi_old/MU.py +++ /dev/null @@ -1,145 +0,0 @@ -''' -MU.py -Handles all MangaUpdates information -''' - -from pyquery import PyQuery as pq -import aiohttp -import difflib -import traceback -import pprint -import collections - -req = aiohttp.ClientSession() - -def findClosestManga(searchText, mangaList): - try: - nameList = [] - - for manga in mangaList: - nameList.append(manga['title'].lower()) - - closestNameFromList = difflib.get_close_matches(searchText.lower(), nameList, 1, 0.85) - - for manga in mangaList: - if manga['title'].lower() == closestNameFromList[0].lower(): - return manga - - return None - except: - return None - -async def findAuthorURL(authorName): - try: - payload = {'search': authorName} - async with req.get('https://mangaupdates.com/authors.html', params=payload, timeout=10) as resp: - html = await resp.text() - - mu = pq(html) - authorURL = None - - for thing in pq(mu).find('table tr td .text .pad'): - try: - url = pq(thing).find('a').attr('href') - if 'http://www.mangaupdates.com/authors.html?id=' in url: - authorURL = url - except: - pass - - return authorURL - except: - - traceback.print_exc() - return None - -async def findSeriesURLViaAuthor(seriesName, authorName, authorURL): - try: - async with req.get(authorURL, timeout=10) as resp: - html = await resp.text() - - mu = pq(html) - authorURL = None - - authorName = authorName.lower() - authorName = authorName.split(' ') - - for thing in mu.find('table tr .text'): - try: - title = pq(thing).find('a')[1].text - url = pq(thing).find('a').attr('href') - - if url: - if 'http://www.mangaupdates.com/series.html?id=' in url: - title = title.lower() - - for name in authorName: - title = title.replace(name, '') - - s = difflib.SequenceMatcher(lambda x: x == "", seriesName, title) - if s.ratio() > 0.5: - return url - - except: - pass - - return authorURL - except: - - traceback.print_exc() - return None - -async def getMangaWithAuthor(searchText, authorName): - try: - url = await findAuthorURL(authorName) - - if not url: - rearrangedAuthorNames = collections.deque(authorName.split(' ')) - rearrangedAuthorNames.rotate(-1) - rearrangedName = ' '.join(rearrangedAuthorNames) - url = await findAuthorURL(rearrangedName) - - if url: - return await findSeriesURLViaAuthor(searchText, authorName, url) - else: - return None - - except: - traceback.print_exc() - return None - -async def getMangaURL(searchText): - try: - payload = {'search': searchText} - async with req.get('https://mangaupdates.com/series.html', params=payload, timeout=10) as resp: - html = await resp.text() - - - mu = pq(html) - - mangaList = [] - - for thing in mu.find('.series_rows_table tr'): - title = pq(thing).find('.col1').text() - url = pq(thing).find('.col1 a').attr('href') - genres = pq(thing).find('.col2').text() - year = pq(thing).find('.col3').text() - rating = pq(thing).find('.col4').text() - - if title: - data = { 'title': title, - 'url': url, - 'genres': genres, - 'year': year, - 'rating': rating } - - mangaList.append(data) - - closest = findClosestManga(searchText, mangaList) - return closest['url'] - - except: - - return None - -def getMangaURLById(mangaId): - return 'https://www.mangaupdates.com/series.html?id=' + str(mangaId) diff --git a/roboragi_old/NU.py b/roboragi_old/NU.py deleted file mode 100644 index 06be551..0000000 --- a/roboragi_old/NU.py +++ /dev/null @@ -1,71 +0,0 @@ -''' -NovelUpdates.py -Handles all NovelUpdates information -''' - -from pyquery import PyQuery as pq -import aiohttp -import difflib -import traceback -import pprint -import collections - -req = aiohttp.ClientSession() - -async def getLightNovelURL(searchText): - try: - searchText = searchText.replace(' ', '+') - async with req.get('http://www.novelupdates.com/?s=' + searchText, timeout=10) as resp: - html = await resp.text() - - - nu = pq(html) - - lnList = [] - - for thing in nu.find('.w-blog-entry'): - title = pq(thing).find('.w-blog-entry-title').text() - url = pq(thing).find('.w-blog-entry-link').attr('href') - - if title: - data = { 'title': title, - 'url': url } - lnList.append(data) - - closest = findClosestLightNovel(searchText, lnList) - return closest['url'] - - except: - - return None - -def findClosestLightNovel(searchText, lnList): - try: - nameList = [] - nameListWithoutWN = [] - - for ln in lnList: - nameList.append(ln['title'].lower()) - - if '(wn)' not in ln['title'].lower(): - nameListWithoutWN.append(ln['title'].lower()) - - - closestNameFromListWithoutWN = difflib.get_close_matches(searchText.lower(), nameListWithoutWN, 1, 0.80) - closestNameFromListWithWN = difflib.get_close_matches(searchText.lower(), nameList, 1, 0.80) - - if closestNameFromListWithoutWN: - nameToUse = closestNameFromListWithoutWN[0].lower() - else: - nameToUse = closestNameFromListWithWN[0].lower() - - for ln in lnList: - if ln['title'].lower() == nameToUse: - return ln - - return None - except: - return None - -def getLightNovelById(lnId): - return 'http://www.novelupdates.com/series/' + str(lnId) diff --git a/roboragi_old/PreCache.py b/roboragi_old/PreCache.py deleted file mode 100644 index eb1df84..0000000 --- a/roboragi_old/PreCache.py +++ /dev/null @@ -1,82 +0,0 @@ -import aiohttp -import asyncio -import DatabaseHandler -import Anilist -import MAL -import traceback -import urllib -import math - - -async def setup(): - end_index = input("How many anime titles do you want? ") - #result = await top_n_by_popularity('anime', end_index) - result2 = await top_n_by_popularity('manga', end_index) - result3 = await top40ByGenre('manga') - -async def top40ByGenre(medium): - errorList = [] - genres = await Anilist.getGenres(medium) - for entry in genres: - top40 = await Anilist.GetTop40ByGenre(medium, entry['genre']) - for entry in top40: - print("Working on anilist id: {}".format(entry['id'])) - try: - DatabaseHandler.PopulateCache('anilist{}'.format(medium), entry) - except Exception as e: - print("{} failed with exception {}".format(entry['id'], e)) - try: - animeName = None - if entry['title_romaji']: - animeName = entry['title_romaji'] - else: - animeName = entry['title_english'] - if medium == 'anime': - malanime = await MAL.getAnimeDetails(animeName) - elif medium == 'manga': - malanime = await MAL.getMangaDetails(animeName) - if malanime: - try: - DatabaseHandler.PopulateCache('mal{}'.format(medium), malanime) - except Exception as e: - print("{} failed with exception {}".format(malanime['id'], e)) - except Exception as e: - print("debug 1 error: {}".format(e)) - -async def top_n_by_popularity(medium, n): - count = 1 - final_page = math.ceil(float(n)/float(40)) - while count < final_page: - try: - print("\n\n-------------Starting page {}------------\n\n".format(count)) - page_entries = await Anilist.get_page_by_popularity(medium, count) - for entry in page_entries: - print("Working on anilist id: {}".format(entry['id'])) - try: - DatabaseHandler.PopulateCache('anilist{}'.format(medium), entry) - except Exception as e: - print("{} failed with exception {}\n".format(entry['id'], e)) - try: - animeName = None - if entry['title_romaji']: - animeName = entry['title_romaji'] - else: - animeName = entry['title_english'] - if medium == 'anime': - malanime = await MAL.getAnimeDetails(animeName) - elif medium == 'manga': - malanime = await MAL.getMangaDetails(animeName) - if malanime: - try: - DatabaseHandler.PopulateCache('mal{}'.format(medium), malanime) - except Exception as e: - print("{} failed with exception {}\n".format(malanime['id'], e)) - except Exception as e: - print("debug 1 error: {}\n".format(e)) - count +=1 - except Exception as e: - count +=1 - print(e) - -loop = asyncio.get_event_loop() -loop.run_until_complete(setup()) diff --git a/roboragi_old/Reference.py b/roboragi_old/Reference.py deleted file mode 100644 index 3c6bd98..0000000 --- a/roboragi_old/Reference.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- - -import sqlite3 - -sqlConn = sqlite3.connect('reference.db') -sqlCur = sqlConn.cursor() - -def is_april_fools_2016(username): - try: - sqlCur.execute("SELECT 1 FROM aprilfools2016 WHERE username = ? LIMIT 1", [username]) - result = sqlCur.fetchone() - - if result: - return True - else: - return False - except Exception as e: - return False - -def get_bling(username): - if is_april_fools_2016(username): - return ' ^(| \U0001F4B0)' - else: - return '' diff --git a/roboragi_old/Wikipedia.py b/roboragi_old/Wikipedia.py deleted file mode 100644 index 2b20012..0000000 --- a/roboragi_old/Wikipedia.py +++ /dev/null @@ -1,62 +0,0 @@ -import requests -import difflib -import pprint -from urllib.parse import quote - -BASE_RESULT_URL = 'https://en.wikipedia.org/wiki/' -BASE_API_URL = 'https://en.wikipedia.org/w/api.php?' - -wiki = requests.Session() -wiki.headers.update({'User-Agent': 'Roboragi - An Anime/Manga Reddit Bot - Contact /u/Nihilate on Reddit'}) - -def getAnimeURL(searchText): - return getThingURL(searchText, 'Anime') - -def getMangaURL(searchText): - return getThingURL(searchText, 'Manga') - -def getThingURL(searchText, searchType=None): - try: - request = wiki.get(BASE_API_URL + 'action=query&format=json&list=search&utf8=1&srsearch=' + searchText, timeout=10) - except: - return None - - result = request.json() - - pprint.pprint(result) - - thingTitles = [] - - for thing in result['query']['search']: - #bloody disambiguation - if 'can refer to' in thing['snippet']: - continue - - if searchType: - if searchType.lower() in thing['snippet']: - thingTitles.append(thing['title']) - else: - thingTitles.append(thing['title']) - - print(thingTitles) - - closestThings = difflib.get_close_matches(searchText.lower(), [title.lower() for title in thingTitles], 1, 0.90) - - if closestThings: - for title in thingTitles: - if closestThings[0].lower() in title.lower(): - return BASE_RESULT_URL + quote(title) - else: - if thingTitles: - for thing in result['query']['search']: - if thing['title'].lower() in thingTitles[0].lower(): - if (searchText.lower() in thing['snippet'].lower()): - return BASE_RESULT_URL + quote(thingTitles[0]) - break - - return None - -def getThingURLById(thingId): - return BASE_RESULT_URL + quote(thingId) - -print(getAnimeURL('monogatari series')) diff --git a/roboragi_old/reference.db b/roboragi_old/reference.db deleted file mode 100644 index 3ec969502df25a16890ae538ea01c58233b02225..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3072 zcmeIyO>7%Q6ae6zH))dfPwb>=oz!)0H+2&?bsO7B`U3|X>NZi`#C6h!A|z+x9miAe z&W7D_Qb!=Dgai@?gpiOpRpJ0bf>RG1sEXhO2!zCe145PP1&R;{DB^+yJBgFnQF}u| zNVT-`%|C24Id0pWINzvg{IFrW zlDPM?7yqRI&L}v8KYm!?qx+94@V~6UbEqQ_kE82pK^ApNmMx~2O;fj1BgygB89Z6Y zX6CYyxy(c^8(C<5xDXj~sS7omTAUbkL7mG!H5Zwlajr|b+)s9q$bSud9gJ`G1)p__w03wKCb+(;QRO^`2J?`E$KeBJc~nMo=o}hHgQyp^JIVgrPxm+sXKI4kf|c@=*n|tu7({26>l(F^ zBdL+@Q752yj&f?TQgNAxswo{HpL9j z5w7`1AZ{~JsgUJm%I!tV+;be#XGuwzRy}cnXtbC&1rert41!OS>Y`asbR-~h;>8wf@ElR3oVv7=WMtb)!3|EL@3u+NvXM~ri=LkdzX9g`6nWcBdA$EpX)mcHT zu@hq_-9r$z%S^0sQwVhsg82&JmdU#gLoBZo!FbIm7KrAKL3n})p*t@>+Ia~2r`fDM z$~&EQ)-1EihLicE+l9?St4n6QxS=JQIR) zPd`K_h)uQGDzOZ29}HwTr^}>NwTN&9AT-0-v}q8{vlpTn%aKvDM^B|)dmzMCnXcEV z?(c>8vPDbGtl4F~#xz^q4Z&H$s?0F`Jx;4CL^D_IRiam&jdgZI|0G#kBSQ5k; z6YlfDV4f`Nra4!k*;TXZ?Sg^x&W=dpVvX9O(+m9t+j$G!=YhcjTOm4KA>3)P<#$7T z(zJMe(rH}DGHu)Kg0ORnG~48?q^b~P^Un6Fc26fHoL&o7W>hOKQO;_%y92_r#F!*T zawO?(hk?t^w)v8C3K;k8lBsl^7Fb!Q2I2lTh%eF-ajIaJ1u2=fiXoUYtEPVPlnM}> zVAc|~)?5fe)2zarLqJ{Y{%j#;!V*S!9v^|z_Nw p3BOZ@Z^`eF$(!i6Z)9w;ze{Fp#os1#H1uORZtw>;4yl{|{{bemTXO&a diff --git a/roboragi_old/synonyms.db b/roboragi_old/synonyms.db deleted file mode 100644 index c8bea7fe55b54b561c8c429a5b0c96c1f3f5d97f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118784 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%0AXrN*79gWWTG5=;*I#XCdKEpPdSI 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 Date: Sun, 2 Oct 2022 00:36:55 -0400 Subject: [PATCH 2/6] containerized the database --- .env.example | 14 ++++++++++++++ .gitignore | 9 +++++++++ docker-compose.yml | 31 +++++++++++++++++++++++++++++++ pgdata/.gitkeep | 0 4 files changed, 54 insertions(+) create mode 100644 .env.example create mode 100644 docker-compose.yml create mode 100644 pgdata/.gitkeep diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a826350 --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +# docker-compose configuration for postgres +DB_PORT=5432 +DB_VOLUME="/absolute/path/to/pgdata" + +# postgres startup variables +POSTGRES_USER=discordoragi +POSTGRES_PASSWORD="CHANGE THIS TO A SAFE PASSWORD" +POSTGRES_DB=discordoragi + +# connection info for db +DB_USER="$POSTGRES_USER" +DB_PASSWORD="$POSTGRES_PASSWORD" +DB_DATABASE="$POSTGRES_DB" +DB_HOST=db diff --git a/.gitignore b/.gitignore index 35f0e3b..365ed06 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,12 @@ oauth.ini #Pycharm .idea .vscode + +# Vim +**/*.swp + +# Configuration +.env + +# Database +pgdata/ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4d20e8f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,31 @@ +version: '3.8' +services: + db: + image: postgres:14-alpine + restart: always + volumes: + - discordoragi_pgdata:/var/lib/postgresql/data + expose: + - "${DB_PORT}" + environment: + POSTGRES_USER: "${POSTGRES_USER}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + POSTGRES_DB: "${POSTGRES_DB}" + test: + image: ubuntu:latest + depends_on: + - db + environment: + DB_USER: "${POSTGRES_USER}" + DB_PASSWORD: "${POSTGRES_PASSWORD}" + DB_DATABASE: "${POSTGRES_DB}" + DB_HOST: "${DB_HOST}" + DB_PORT: "${DB_PORT}" + command: ["sleep", "infinity"] +volumes: + discordoragi_pgdata: + driver: local + driver_opts: + type: none + o: bind + device: "${DB_VOLUME}" diff --git a/pgdata/.gitkeep b/pgdata/.gitkeep new file mode 100644 index 0000000..e69de29 From f9fcd6c23001a25efddb665954cc26393bbddb61 Mon Sep 17 00:00:00 2001 From: Amndeep Singh Mann Date: Sun, 2 Oct 2022 02:53:51 -0400 Subject: [PATCH 3/6] remove unnecessary envvars and add one for the database url --- .env.example | 4 +--- docker-compose.yml | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index a826350..fc1239b 100644 --- a/.env.example +++ b/.env.example @@ -8,7 +8,5 @@ POSTGRES_PASSWORD="CHANGE THIS TO A SAFE PASSWORD" POSTGRES_DB=discordoragi # connection info for db -DB_USER="$POSTGRES_USER" -DB_PASSWORD="$POSTGRES_PASSWORD" -DB_DATABASE="$POSTGRES_DB" +DB_URL="postgresql://" DB_HOST=db diff --git a/docker-compose.yml b/docker-compose.yml index 4d20e8f..2e2154f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,11 +16,12 @@ services: depends_on: - db environment: + DB_URL: "${DB_URL}" DB_USER: "${POSTGRES_USER}" DB_PASSWORD: "${POSTGRES_PASSWORD}" - DB_DATABASE: "${POSTGRES_DB}" DB_HOST: "${DB_HOST}" DB_PORT: "${DB_PORT}" + DB_DATABASE: "${POSTGRES_DB}" command: ["sleep", "infinity"] volumes: discordoragi_pgdata: From 764843b6c29091bd9461f63ae9608b3570a3d38f Mon Sep 17 00:00:00 2001 From: Amndeep Singh Mann Date: Sun, 2 Oct 2022 04:52:56 -0400 Subject: [PATCH 4/6] put code in an overarching directory and changed imports to be relative --- cogs/__init__.py | 3 --- discordoragi/__init__.py | 0 {bot => discordoragi/bot}/__init__.py | 3 +-- {bot => discordoragi/bot}/discordoragi.py | 4 ++-- discordoragi/cogs/__init__.py | 3 +++ {cogs => discordoragi/cogs}/search.py | 0 {config => discordoragi/config}/example_config.yml | 0 {helpers => discordoragi/helpers}/__init__.py | 0 {helpers => discordoragi/helpers}/database_helpers.py | 0 {helpers => discordoragi/helpers}/discord_helpers.py | 0 run.py => discordoragi/run.py | 4 ++-- 11 files changed, 8 insertions(+), 9 deletions(-) delete mode 100644 cogs/__init__.py create mode 100644 discordoragi/__init__.py rename {bot => discordoragi/bot}/__init__.py (57%) rename {bot => discordoragi/bot}/discordoragi.py (91%) create mode 100644 discordoragi/cogs/__init__.py rename {cogs => discordoragi/cogs}/search.py (100%) rename {config => discordoragi/config}/example_config.yml (100%) rename {helpers => discordoragi/helpers}/__init__.py (100%) rename {helpers => discordoragi/helpers}/database_helpers.py (100%) rename {helpers => discordoragi/helpers}/discord_helpers.py (100%) rename run.py => discordoragi/run.py (81%) diff --git a/cogs/__init__.py b/cogs/__init__.py deleted file mode 100644 index 550a448..0000000 --- a/cogs/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from cogs.search import Search - -__all__ = ['Search'] diff --git a/discordoragi/__init__.py b/discordoragi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/__init__.py b/discordoragi/bot/__init__.py similarity index 57% rename from bot/__init__.py rename to discordoragi/bot/__init__.py index 6907a7b..482bcea 100644 --- a/bot/__init__.py +++ b/discordoragi/bot/__init__.py @@ -1,5 +1,4 @@ - -from bot.discordoragi import Discordoragi +from .discordoragi import Discordoragi __all__ = ['Discordoragi', 'SessionManager', 'HTTPStatusError'] diff --git a/bot/discordoragi.py b/discordoragi/bot/discordoragi.py similarity index 91% rename from bot/discordoragi.py rename to discordoragi/bot/discordoragi.py index de2b941..6de2fec 100644 --- a/bot/discordoragi.py +++ b/discordoragi/bot/discordoragi.py @@ -5,8 +5,8 @@ import yaml from time import time from aiohttp_wrapper import SessionManager -from helpers.discord_helpers import get_name_with_discriminator -from helpers import PostgresController +from ..helpers.discord_helpers import get_name_with_discriminator +from ..helpers import PostgresController from logging import Formatter, INFO, StreamHandler, getLogger diff --git a/discordoragi/cogs/__init__.py b/discordoragi/cogs/__init__.py new file mode 100644 index 0000000..4821b33 --- /dev/null +++ b/discordoragi/cogs/__init__.py @@ -0,0 +1,3 @@ +from .search import Search + +__all__ = ['Search'] diff --git a/cogs/search.py b/discordoragi/cogs/search.py similarity index 100% rename from cogs/search.py rename to discordoragi/cogs/search.py diff --git a/config/example_config.yml b/discordoragi/config/example_config.yml similarity index 100% rename from config/example_config.yml rename to discordoragi/config/example_config.yml diff --git a/helpers/__init__.py b/discordoragi/helpers/__init__.py similarity index 100% rename from helpers/__init__.py rename to discordoragi/helpers/__init__.py diff --git a/helpers/database_helpers.py b/discordoragi/helpers/database_helpers.py similarity index 100% rename from helpers/database_helpers.py rename to discordoragi/helpers/database_helpers.py diff --git a/helpers/discord_helpers.py b/discordoragi/helpers/discord_helpers.py similarity index 100% rename from helpers/discord_helpers.py rename to discordoragi/helpers/discord_helpers.py diff --git a/run.py b/discordoragi/run.py similarity index 81% rename from run.py rename to discordoragi/run.py index acbc13b..8f69518 100644 --- a/run.py +++ b/discordoragi/run.py @@ -1,8 +1,8 @@ """ Actually runs the code """ -from bot import Discordoragi -from cogs import Search +from .bot import Discordoragi +from .cogs import Search from asyncio import get_event_loop From 6eb26bb04392fb3f79be42d6e2980d31cd0439e5 Mon Sep 17 00:00:00 2001 From: Amndeep Singh Mann Date: Sun, 2 Oct 2022 04:56:07 -0400 Subject: [PATCH 5/6] use poetry instead of .python-version and requirements.txt --- .python-version | 1 - poetry.lock | 544 +++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 25 +++ requirements.txt | 6 - 4 files changed, 569 insertions(+), 7 deletions(-) delete mode 100644 .python-version create mode 100644 poetry.lock create mode 100644 pyproject.toml delete mode 100644 requirements.txt diff --git a/.python-version b/.python-version deleted file mode 100644 index b727628..0000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.6.2 diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..f5796c8 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,544 @@ +[[package]] +name = "aiohttp" +version = "3.7.4.post0" +description = "Async http client/server framework (asyncio)" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +async-timeout = ">=3.0,<4.0" +attrs = ">=17.3.0" +chardet = ">=2.0,<5.0" +multidict = ">=4.5,<7.0" +typing-extensions = ">=3.6.5" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["aiodns", "brotlipy", "cchardet"] + +[[package]] +name = "aiohttp-wrapper" +version = "1.0.0" +description = "Abstraction of HTTP requests using aiohttp" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +aiohttp = ">=2.2.5" +xmltodict = ">=0.11.0" + +[[package]] +name = "async-timeout" +version = "3.0.1" +description = "Timeout context manager for asyncio programs" +category = "main" +optional = false +python-versions = ">=3.5.3" + +[[package]] +name = "asyncpg" +version = "0.26.0" +description = "An asyncio PostgreSQL driver" +category = "main" +optional = false +python-versions = ">=3.6.0" + +[package.extras] +dev = ["Cython (>=0.29.24,<0.30.0)", "Sphinx (>=4.1.2,<4.2.0)", "flake8 (>=3.9.2,<3.10.0)", "pycodestyle (>=2.7.0,<2.8.0)", "pytest (>=6.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "uvloop (>=0.15.3)"] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["flake8 (>=3.9.2,<3.10.0)", "pycodestyle (>=2.7.0,<2.8.0)", "uvloop (>=0.15.3)"] + +[[package]] +name = "attrs" +version = "22.1.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] +docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] +tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] + +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 2 and 3" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "cssselect" +version = "1.1.0" +description = "cssselect parses CSS3 Selectors and translates them to XPath 1.0" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "discord.py" +version = "1.7.3" +description = "A Python wrapper for the Discord API" +category = "main" +optional = false +python-versions = ">=3.5.3" + +[package.dependencies] +aiohttp = ">=3.6.0,<3.8.0" + +[package.extras] +docs = ["sphinx (==3.0.3)", "sphinxcontrib-trio (==1.1.2)", "sphinxcontrib-websupport"] +voice = ["PyNaCl (>=1.3.0,<1.5)"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "lxml" +version = "4.9.1" +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 = "minoshiro" +version = "0.1.9" +description = "An async Python3.6 library to search for anime, manga andlight novel using various web apis." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +aiohttp = ">=2.2.5" +aiohttp-wrapper = ">=1.0.0" +pyquery = ">=1.2.17" +xmltodict = ">=0.11.0" + +[package.extras] +postgres = ["asyncpg (>=0.12.0)"] + +[[package]] +name = "multidict" +version = "6.0.2" +description = "multidict implementation" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "pyquery" +version = "1.4.3" +description = "A jquery-like library for python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +cssselect = ">0.7.9" +lxml = ">=2.1" + +[[package]] +name = "PyYAML" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "typing-extensions" +version = "4.3.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "xmltodict" +version = "0.13.0" +description = "Makes working with XML feel like you are working with JSON" +category = "main" +optional = false +python-versions = ">=3.4" + +[[package]] +name = "yarl" +version = "1.8.1" +description = "Yet another URL library" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + +[metadata] +lock-version = "1.1" +python-versions = "^3.10" +content-hash = "ecb23ade9527f612b99a044e82ddf6269fe4bb6349f60a7c36a0b27865579f93" + +[metadata.files] +aiohttp = [ + {file = "aiohttp-3.7.4.post0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-win32.whl", hash = "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287"}, + {file = "aiohttp-3.7.4.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-win32.whl", hash = "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f"}, + {file = "aiohttp-3.7.4.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-win32.whl", hash = "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16"}, + {file = "aiohttp-3.7.4.post0-cp38-cp38-win_amd64.whl", hash = "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-win32.whl", hash = "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9"}, + {file = "aiohttp-3.7.4.post0-cp39-cp39-win_amd64.whl", hash = "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe"}, + {file = "aiohttp-3.7.4.post0.tar.gz", hash = "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf"}, +] +aiohttp-wrapper = [ + {file = "aiohttp_wrapper-1.0.0-py3-none-any.whl", hash = "sha256:bef4d545e34ed9cbca3ebf52c0cd6d757f6e90bad957d471bef88baf393b26c2"}, + {file = "aiohttp_wrapper-1.0.0.tar.gz", hash = "sha256:f96acd3a06a3ab79759e1718bcec309e3a76976e5d8594bb270e81c4e741d3ad"}, +] +async-timeout = [ + {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, + {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, +] +asyncpg = [ + {file = "asyncpg-0.26.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ed3880b3aec8bda90548218fe0914d251d641f798382eda39a17abfc4910af0"}, + {file = "asyncpg-0.26.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5bd99ee7a00e87df97b804f178f31086e88c8106aca9703b1d7be5078999e68"}, + {file = "asyncpg-0.26.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:868a71704262834065ca7113d80b1f679609e2df77d837747e3d92150dd5a39b"}, + {file = "asyncpg-0.26.0-cp310-cp310-win32.whl", hash = "sha256:838e4acd72da370ad07243898e886e93d3c0c9413f4444d600ba60a5cc206014"}, + {file = "asyncpg-0.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:a254d09a3a989cc1839ba2c34448b879cdd017b528a0cda142c92fbb6c13d957"}, + {file = "asyncpg-0.26.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3ecbe8ed3af4c739addbfbd78f7752866cce2c4e9cc3f953556e4960349ae360"}, + {file = "asyncpg-0.26.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ce7d8c0ab4639bbf872439eba86ef62dd030b245ad0e17c8c675d93d7a6b2d"}, + {file = "asyncpg-0.26.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7129bd809990fd119e8b2b9982e80be7712bb6041cd082be3e415e60e5e2e98f"}, + {file = "asyncpg-0.26.0-cp36-cp36m-win32.whl", hash = "sha256:03f44926fa7ff7ccd59e98f05c7e227e9de15332a7da5bbcef3654bf468ee597"}, + {file = "asyncpg-0.26.0-cp36-cp36m-win_amd64.whl", hash = "sha256:b1f7b173af649b85126429e11a628d01a5b75973d2a55d64dba19ad8f0e9f904"}, + {file = "asyncpg-0.26.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:efe056fd22fc6ed5c1ab353b6510808409566daac4e6f105e2043797f17b8dad"}, + {file = "asyncpg-0.26.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d96cf93e01df9fb03cef5f62346587805e6c0ca6f654c23b8d35315bdc69af59"}, + {file = "asyncpg-0.26.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:235205b60d4d014921f7b1cdca0e19669a9a8978f7606b3eb8237ca95f8e716e"}, + {file = "asyncpg-0.26.0-cp37-cp37m-win32.whl", hash = "sha256:0de408626cfc811ef04f372debfcdd5e4ab5aeb358f2ff14d1bdc246ed6272b5"}, + {file = "asyncpg-0.26.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f92d501bf213b16fabad4fbb0061398d2bceae30ddc228e7314c28dcc6641b79"}, + {file = "asyncpg-0.26.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9acb22a7b6bcca0d80982dce3d67f267d43e960544fb5dd934fd3abe20c48014"}, + {file = "asyncpg-0.26.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e550d8185f2c4725c1e8d3c555fe668b41bd092143012ddcc5343889e1c2a13d"}, + {file = "asyncpg-0.26.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:050e339694f8c5d9aebcf326ca26f6622ef23963a6a3a4f97aeefc743954afd5"}, + {file = "asyncpg-0.26.0-cp38-cp38-win32.whl", hash = "sha256:b0c3f39ebfac06848ba3f1e280cb1fada7cc1229538e3dad3146e8d1f9deb92a"}, + {file = "asyncpg-0.26.0-cp38-cp38-win_amd64.whl", hash = "sha256:49fc7220334cc31d14866a0b77a575d6a5945c0fa3bb67f17304e8b838e2a02b"}, + {file = "asyncpg-0.26.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d156e53b329e187e2dbfca8c28c999210045c45ef22a200b50de9b9e520c2694"}, + {file = "asyncpg-0.26.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b4051012ca75defa9a1dc6b78185ca58cdc3a247187eb76a6bcf55dfaa2fad4"}, + {file = "asyncpg-0.26.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6d60f15a0ac18c54a6ca6507c28599c06e2e87a0901e7b548f15243d71905b18"}, + {file = "asyncpg-0.26.0-cp39-cp39-win32.whl", hash = "sha256:ede1a3a2c377fe12a3930f4b4dd5340e8b32929541d5db027a21816852723438"}, + {file = "asyncpg-0.26.0-cp39-cp39-win_amd64.whl", hash = "sha256:8e1e79f0253cbd51fc43c4d0ce8804e46ee71f6c173fdc75606662ad18756b52"}, + {file = "asyncpg-0.26.0.tar.gz", hash = "sha256:77e684a24fee17ba3e487ca982d0259ed17bae1af68006f4cf284b23ba20ea2c"}, +] +attrs = [ + {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, + {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, +] +chardet = [ + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] +cssselect = [ + {file = "cssselect-1.1.0-py2.py3-none-any.whl", hash = "sha256:f612ee47b749c877ebae5bb77035d8f4202c6ad0f0fc1271b3c18ad6c4468ecf"}, + {file = "cssselect-1.1.0.tar.gz", hash = "sha256:f95f8dedd925fd8f54edb3d2dfb44c190d9d18512377d3c1e2388d16126879bc"}, +] +"discord.py" = [ + {file = "discord.py-1.7.3-py3-none-any.whl", hash = "sha256:c6f64db136de0e18e090f6752ea68bdd4ab0a61b82dfe7acecefa22d6477bb0c"}, + {file = "discord.py-1.7.3.tar.gz", hash = "sha256:462cd0fe307aef8b29cbfa8dd613e548ae4b2cb581d46da9ac0d46fb6ea19408"}, +] +idna = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] +lxml = [ + {file = "lxml-4.9.1-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:98cafc618614d72b02185ac583c6f7796202062c41d2eeecdf07820bad3295ed"}, + {file = "lxml-4.9.1-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c62e8dd9754b7debda0c5ba59d34509c4688f853588d75b53c3791983faa96fc"}, + {file = "lxml-4.9.1-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:21fb3d24ab430fc538a96e9fbb9b150029914805d551deeac7d7822f64631dfc"}, + {file = "lxml-4.9.1-cp27-cp27m-win32.whl", hash = "sha256:86e92728ef3fc842c50a5cb1d5ba2bc66db7da08a7af53fb3da79e202d1b2cd3"}, + {file = "lxml-4.9.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4cfbe42c686f33944e12f45a27d25a492cc0e43e1dc1da5d6a87cbcaf2e95627"}, + {file = "lxml-4.9.1-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dad7b164905d3e534883281c050180afcf1e230c3d4a54e8038aa5cfcf312b84"}, + {file = "lxml-4.9.1-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a614e4afed58c14254e67862456d212c4dcceebab2eaa44d627c2ca04bf86837"}, + {file = "lxml-4.9.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f9ced82717c7ec65a67667bb05865ffe38af0e835cdd78728f1209c8fffe0cad"}, + {file = "lxml-4.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:d9fc0bf3ff86c17348dfc5d322f627d78273eba545db865c3cd14b3f19e57fa5"}, + {file = "lxml-4.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e5f66bdf0976ec667fc4594d2812a00b07ed14d1b44259d19a41ae3fff99f2b8"}, + {file = "lxml-4.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fe17d10b97fdf58155f858606bddb4e037b805a60ae023c009f760d8361a4eb8"}, + {file = "lxml-4.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8caf4d16b31961e964c62194ea3e26a0e9561cdf72eecb1781458b67ec83423d"}, + {file = "lxml-4.9.1-cp310-cp310-win32.whl", hash = "sha256:4780677767dd52b99f0af1f123bc2c22873d30b474aa0e2fc3fe5e02217687c7"}, + {file = "lxml-4.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:b122a188cd292c4d2fcd78d04f863b789ef43aa129b233d7c9004de08693728b"}, + {file = "lxml-4.9.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:be9eb06489bc975c38706902cbc6888f39e946b81383abc2838d186f0e8b6a9d"}, + {file = "lxml-4.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f1be258c4d3dc609e654a1dc59d37b17d7fef05df912c01fc2e15eb43a9735f3"}, + {file = "lxml-4.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:927a9dd016d6033bc12e0bf5dee1dde140235fc8d0d51099353c76081c03dc29"}, + {file = "lxml-4.9.1-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9232b09f5efee6a495a99ae6824881940d6447debe272ea400c02e3b68aad85d"}, + {file = "lxml-4.9.1-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:04da965dfebb5dac2619cb90fcf93efdb35b3c6994fea58a157a834f2f94b318"}, + {file = "lxml-4.9.1-cp35-cp35m-win32.whl", hash = "sha256:4d5bae0a37af799207140652a700f21a85946f107a199bcb06720b13a4f1f0b7"}, + {file = "lxml-4.9.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4878e667ebabe9b65e785ac8da4d48886fe81193a84bbe49f12acff8f7a383a4"}, + {file = "lxml-4.9.1-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:1355755b62c28950f9ce123c7a41460ed9743c699905cbe664a5bcc5c9c7c7fb"}, + {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:bcaa1c495ce623966d9fc8a187da80082334236a2a1c7e141763ffaf7a405067"}, + {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6eafc048ea3f1b3c136c71a86db393be36b5b3d9c87b1c25204e7d397cee9536"}, + {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:13c90064b224e10c14dcdf8086688d3f0e612db53766e7478d7754703295c7c8"}, + {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206a51077773c6c5d2ce1991327cda719063a47adc02bd703c56a662cdb6c58b"}, + {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e8f0c9d65da595cfe91713bc1222af9ecabd37971762cb830dea2fc3b3bb2acf"}, + {file = "lxml-4.9.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8f0a4d179c9a941eb80c3a63cdb495e539e064f8054230844dcf2fcb812b71d3"}, + {file = "lxml-4.9.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:830c88747dce8a3e7525defa68afd742b4580df6aa2fdd6f0855481e3994d391"}, + {file = "lxml-4.9.1-cp36-cp36m-win32.whl", hash = "sha256:1e1cf47774373777936c5aabad489fef7b1c087dcd1f426b621fda9dcc12994e"}, + {file = "lxml-4.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:5974895115737a74a00b321e339b9c3f45c20275d226398ae79ac008d908bff7"}, + {file = "lxml-4.9.1-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:1423631e3d51008871299525b541413c9b6c6423593e89f9c4cfbe8460afc0a2"}, + {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:2aaf6a0a6465d39b5ca69688fce82d20088c1838534982996ec46633dc7ad6cc"}, + {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:9f36de4cd0c262dd9927886cc2305aa3f2210db437aa4fed3fb4940b8bf4592c"}, + {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae06c1e4bc60ee076292e582a7512f304abdf6c70db59b56745cca1684f875a4"}, + {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:57e4d637258703d14171b54203fd6822fda218c6c2658a7d30816b10995f29f3"}, + {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6d279033bf614953c3fc4a0aa9ac33a21e8044ca72d4fa8b9273fe75359d5cca"}, + {file = "lxml-4.9.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a60f90bba4c37962cbf210f0188ecca87daafdf60271f4c6948606e4dabf8785"}, + {file = "lxml-4.9.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6ca2264f341dd81e41f3fffecec6e446aa2121e0b8d026fb5130e02de1402785"}, + {file = "lxml-4.9.1-cp37-cp37m-win32.whl", hash = "sha256:27e590352c76156f50f538dbcebd1925317a0f70540f7dc8c97d2931c595783a"}, + {file = "lxml-4.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:eea5d6443b093e1545ad0210e6cf27f920482bfcf5c77cdc8596aec73523bb7e"}, + {file = "lxml-4.9.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:f05251bbc2145349b8d0b77c0d4e5f3b228418807b1ee27cefb11f69ed3d233b"}, + {file = "lxml-4.9.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:487c8e61d7acc50b8be82bda8c8d21d20e133c3cbf41bd8ad7eb1aaeb3f07c97"}, + {file = "lxml-4.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d1a92d8e90b286d491e5626af53afef2ba04da33e82e30744795c71880eaa21"}, + {file = "lxml-4.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:b570da8cd0012f4af9fa76a5635cd31f707473e65a5a335b186069d5c7121ff2"}, + {file = "lxml-4.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ef87fca280fb15342726bd5f980f6faf8b84a5287fcc2d4962ea8af88b35130"}, + {file = "lxml-4.9.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:93e414e3206779ef41e5ff2448067213febf260ba747fc65389a3ddaa3fb8715"}, + {file = "lxml-4.9.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6653071f4f9bac46fbc30f3c7838b0e9063ee335908c5d61fb7a4a86c8fd2036"}, + {file = "lxml-4.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:32a73c53783becdb7eaf75a2a1525ea8e49379fb7248c3eeefb9412123536387"}, + {file = "lxml-4.9.1-cp38-cp38-win32.whl", hash = "sha256:1a7c59c6ffd6ef5db362b798f350e24ab2cfa5700d53ac6681918f314a4d3b94"}, + {file = "lxml-4.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:1436cf0063bba7888e43f1ba8d58824f085410ea2025befe81150aceb123e345"}, + {file = "lxml-4.9.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:4beea0f31491bc086991b97517b9683e5cfb369205dac0148ef685ac12a20a67"}, + {file = "lxml-4.9.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:41fb58868b816c202e8881fd0f179a4644ce6e7cbbb248ef0283a34b73ec73bb"}, + {file = "lxml-4.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:bd34f6d1810d9354dc7e35158aa6cc33456be7706df4420819af6ed966e85448"}, + {file = "lxml-4.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:edffbe3c510d8f4bf8640e02ca019e48a9b72357318383ca60e3330c23aaffc7"}, + {file = "lxml-4.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d949f53ad4fc7cf02c44d6678e7ff05ec5f5552b235b9e136bd52e9bf730b91"}, + {file = "lxml-4.9.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:079b68f197c796e42aa80b1f739f058dcee796dc725cc9a1be0cdb08fc45b000"}, + {file = "lxml-4.9.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9c3a88d20e4fe4a2a4a84bf439a5ac9c9aba400b85244c63a1ab7088f85d9d25"}, + {file = "lxml-4.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4e285b5f2bf321fc0857b491b5028c5f276ec0c873b985d58d7748ece1d770dd"}, + {file = "lxml-4.9.1-cp39-cp39-win32.whl", hash = "sha256:ef72013e20dd5ba86a8ae1aed7f56f31d3374189aa8b433e7b12ad182c0d2dfb"}, + {file = "lxml-4.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:10d2017f9150248563bb579cd0d07c61c58da85c922b780060dcc9a3aa9f432d"}, + {file = "lxml-4.9.1-pp37-pypy37_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538747a9d7827ce3e16a8fdd201a99e661c7dee3c96c885d8ecba3c35d1032c"}, + {file = "lxml-4.9.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0645e934e940107e2fdbe7c5b6fb8ec6232444260752598bc4d09511bd056c0b"}, + {file = "lxml-4.9.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6daa662aba22ef3258934105be2dd9afa5bb45748f4f702a3b39a5bf53a1f4dc"}, + {file = "lxml-4.9.1-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:603a464c2e67d8a546ddaa206d98e3246e5db05594b97db844c2f0a1af37cf5b"}, + {file = "lxml-4.9.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c4b2e0559b68455c085fb0f6178e9752c4be3bba104d6e881eb5573b399d1eb2"}, + {file = "lxml-4.9.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0f3f0059891d3254c7b5fb935330d6db38d6519ecd238ca4fce93c234b4a0f73"}, + {file = "lxml-4.9.1-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c852b1530083a620cb0de5f3cd6826f19862bafeaf77586f1aef326e49d95f0c"}, + {file = "lxml-4.9.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:287605bede6bd36e930577c5925fcea17cb30453d96a7b4c63c14a257118dbb9"}, + {file = "lxml-4.9.1.tar.gz", hash = "sha256:fe749b052bb7233fe5d072fcb549221a8cb1a16725c47c37e42b0b9cb3ff2c3f"}, +] +minoshiro = [ + {file = "minoshiro-0.1.9.tar.gz", hash = "sha256:41e5a9c58ac0314a30a93d97c5560648eb3c31a0b724cbb41cbaccb13c15f453"}, +] +multidict = [ + {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"}, + {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"}, + {file = "multidict-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389"}, + {file = "multidict-6.0.2-cp310-cp310-win32.whl", hash = "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293"}, + {file = "multidict-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658"}, + {file = "multidict-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15"}, + {file = "multidict-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc"}, + {file = "multidict-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a"}, + {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60"}, + {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86"}, + {file = "multidict-6.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d"}, + {file = "multidict-6.0.2-cp38-cp38-win32.whl", hash = "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57"}, + {file = "multidict-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96"}, + {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c"}, + {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e"}, + {file = "multidict-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937"}, + {file = "multidict-6.0.2-cp39-cp39-win32.whl", hash = "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a"}, + {file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"}, + {file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"}, +] +pyquery = [ + {file = "pyquery-1.4.3-py3-none-any.whl", hash = "sha256:1fc33b7699455ed25c75282bc8f80ace1ac078b0dda5a933dacbd8b1c1f83963"}, + {file = "pyquery-1.4.3.tar.gz", hash = "sha256:a388eefb6bc4a55350de0316fbd97cda999ae669b6743ae5b99102ba54f5aa72"}, +] +PyYAML = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] +typing-extensions = [ + {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, + {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, +] +xmltodict = [ + {file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"}, + {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"}, +] +yarl = [ + {file = "yarl-1.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:abc06b97407868ef38f3d172762f4069323de52f2b70d133d096a48d72215d28"}, + {file = "yarl-1.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:07b21e274de4c637f3e3b7104694e53260b5fc10d51fb3ec5fed1da8e0f754e3"}, + {file = "yarl-1.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9de955d98e02fab288c7718662afb33aab64212ecb368c5dc866d9a57bf48880"}, + {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ec362167e2c9fd178f82f252b6d97669d7245695dc057ee182118042026da40"}, + {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:20df6ff4089bc86e4a66e3b1380460f864df3dd9dccaf88d6b3385d24405893b"}, + {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5999c4662631cb798496535afbd837a102859568adc67d75d2045e31ec3ac497"}, + {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed19b74e81b10b592084a5ad1e70f845f0aacb57577018d31de064e71ffa267a"}, + {file = "yarl-1.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e4808f996ca39a6463f45182e2af2fae55e2560be586d447ce8016f389f626f"}, + {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2d800b9c2eaf0684c08be5f50e52bfa2aa920e7163c2ea43f4f431e829b4f0fd"}, + {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6628d750041550c5d9da50bb40b5cf28a2e63b9388bac10fedd4f19236ef4957"}, + {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f5af52738e225fcc526ae64071b7e5342abe03f42e0e8918227b38c9aa711e28"}, + {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:76577f13333b4fe345c3704811ac7509b31499132ff0181f25ee26619de2c843"}, + {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c03f456522d1ec815893d85fccb5def01ffaa74c1b16ff30f8aaa03eb21e453"}, + {file = "yarl-1.8.1-cp310-cp310-win32.whl", hash = "sha256:ea30a42dc94d42f2ba4d0f7c0ffb4f4f9baa1b23045910c0c32df9c9902cb272"}, + {file = "yarl-1.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:9130ddf1ae9978abe63808b6b60a897e41fccb834408cde79522feb37fb72fb0"}, + {file = "yarl-1.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0ab5a138211c1c366404d912824bdcf5545ccba5b3ff52c42c4af4cbdc2c5035"}, + {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0fb2cb4204ddb456a8e32381f9a90000429489a25f64e817e6ff94879d432fc"}, + {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85cba594433915d5c9a0d14b24cfba0339f57a2fff203a5d4fd070e593307d0b"}, + {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca7e596c55bd675432b11320b4eacc62310c2145d6801a1f8e9ad160685a231"}, + {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0f77539733e0ec2475ddcd4e26777d08996f8cd55d2aef82ec4d3896687abda"}, + {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29e256649f42771829974e742061c3501cc50cf16e63f91ed8d1bf98242e5507"}, + {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7fce6cbc6c170ede0221cc8c91b285f7f3c8b9fe28283b51885ff621bbe0f8ee"}, + {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:59ddd85a1214862ce7c7c66457f05543b6a275b70a65de366030d56159a979f0"}, + {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:12768232751689c1a89b0376a96a32bc7633c08da45ad985d0c49ede691f5c0d"}, + {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:b19255dde4b4f4c32e012038f2c169bb72e7f081552bea4641cab4d88bc409dd"}, + {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6c8148e0b52bf9535c40c48faebb00cb294ee577ca069d21bd5c48d302a83780"}, + {file = "yarl-1.8.1-cp37-cp37m-win32.whl", hash = "sha256:de839c3a1826a909fdbfe05f6fe2167c4ab033f1133757b5936efe2f84904c07"}, + {file = "yarl-1.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:dd032e8422a52e5a4860e062eb84ac94ea08861d334a4bcaf142a63ce8ad4802"}, + {file = "yarl-1.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:19cd801d6f983918a3f3a39f3a45b553c015c5aac92ccd1fac619bd74beece4a"}, + {file = "yarl-1.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6347f1a58e658b97b0a0d1ff7658a03cb79bdbda0331603bed24dd7054a6dea1"}, + {file = "yarl-1.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c0da7e44d0c9108d8b98469338705e07f4bb7dab96dbd8fa4e91b337db42548"}, + {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5587bba41399854703212b87071c6d8638fa6e61656385875f8c6dff92b2e461"}, + {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31a9a04ecccd6b03e2b0e12e82131f1488dea5555a13a4d32f064e22a6003cfe"}, + {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:205904cffd69ae972a1707a1bd3ea7cded594b1d773a0ce66714edf17833cdae"}, + {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea513a25976d21733bff523e0ca836ef1679630ef4ad22d46987d04b372d57fc"}, + {file = "yarl-1.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0b51530877d3ad7a8d47b2fff0c8df3b8f3b8deddf057379ba50b13df2a5eae"}, + {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2b8f245dad9e331540c350285910b20dd913dc86d4ee410c11d48523c4fd546"}, + {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ab2a60d57ca88e1d4ca34a10e9fb4ab2ac5ad315543351de3a612bbb0560bead"}, + {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:449c957ffc6bc2309e1fbe67ab7d2c1efca89d3f4912baeb8ead207bb3cc1cd4"}, + {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a165442348c211b5dea67c0206fc61366212d7082ba8118c8c5c1c853ea4d82e"}, + {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b3ded839a5c5608eec8b6f9ae9a62cb22cd037ea97c627f38ae0841a48f09eae"}, + {file = "yarl-1.8.1-cp38-cp38-win32.whl", hash = "sha256:c1445a0c562ed561d06d8cbc5c8916c6008a31c60bc3655cdd2de1d3bf5174a0"}, + {file = "yarl-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:56c11efb0a89700987d05597b08a1efcd78d74c52febe530126785e1b1a285f4"}, + {file = "yarl-1.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e80ed5a9939ceb6fda42811542f31c8602be336b1fb977bccb012e83da7e4936"}, + {file = "yarl-1.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6afb336e23a793cd3b6476c30f030a0d4c7539cd81649683b5e0c1b0ab0bf350"}, + {file = "yarl-1.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4c322cbaa4ed78a8aac89b2174a6df398faf50e5fc12c4c191c40c59d5e28357"}, + {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fae37373155f5ef9b403ab48af5136ae9851151f7aacd9926251ab26b953118b"}, + {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5395da939ffa959974577eff2cbfc24b004a2fb6c346918f39966a5786874e54"}, + {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:076eede537ab978b605f41db79a56cad2e7efeea2aa6e0fa8f05a26c24a034fb"}, + {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d1a50e461615747dd93c099f297c1994d472b0f4d2db8a64e55b1edf704ec1c"}, + {file = "yarl-1.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7de89c8456525650ffa2bb56a3eee6af891e98f498babd43ae307bd42dca98f6"}, + {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4a88510731cd8d4befaba5fbd734a7dd914de5ab8132a5b3dde0bbd6c9476c64"}, + {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2d93a049d29df172f48bcb09acf9226318e712ce67374f893b460b42cc1380ae"}, + {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:21ac44b763e0eec15746a3d440f5e09ad2ecc8b5f6dcd3ea8cb4773d6d4703e3"}, + {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d0272228fabe78ce00a3365ffffd6f643f57a91043e119c289aaba202f4095b0"}, + {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99449cd5366fe4608e7226c6cae80873296dfa0cde45d9b498fefa1de315a09e"}, + {file = "yarl-1.8.1-cp39-cp39-win32.whl", hash = "sha256:8b0af1cf36b93cee99a31a545fe91d08223e64390c5ecc5e94c39511832a4bb6"}, + {file = "yarl-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:de49d77e968de6626ba7ef4472323f9d2e5a56c1d85b7c0e2a190b2173d3b9be"}, + {file = "yarl-1.8.1.tar.gz", hash = "sha256:af887845b8c2e060eb5605ff72b6f2dd2aab7a761379373fd89d314f4752abbf"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..323b1df --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[tool.poetry] +name = "Discordoragi" +version = "4.0.0a" +description = "Discordoragi is a Discord bot usign the Minoshiro library which creates anime and manga links from MAL, Anilist, MangaUpdates, and Anime-Planet when requested." +license = "GPL-2.0-or-later" +authors = [ + "Alex Portlock 'Nihilate' <>", + "James Wolff 'jwolff52' ", + "dashwav ", + "Amndeep Singh Mann 'Amndeep7' " +] + +[tool.poetry.dependencies] +python = "^3.10" +minoshiro = "^0.1.9" +"discord.py" = "^1.7.3" +pyyaml = "^6.0" +asyncpg = "^0.26.0" + +[tool.poetry.scripts] +discordoragi = "discordoragi.run:run" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 0625f20..0000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -requests==2.7.0 -six==1.9.0 -minoshiro==0.1.9 -psycopg2==2.7.0 -pyquery==1.2.13 -discord.py==1.3.4 From 0a12eaa7dddc9e154d37dbb75a40ffd63ca90e17 Mon Sep 17 00:00:00 2001 From: Amndeep Singh Mann Date: Sun, 2 Oct 2022 04:58:31 -0400 Subject: [PATCH 6/6] created a test image while i try to get the bot working. this will be swapped out for a multistage dockerfile when this project is in a stage that's closer to production --- .dockerignore | 17 +++++++++++++++++ Dockerfile | 11 +++++++++++ docker-compose.yml | 3 ++- 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..35fdcca --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +# git +.git/ +.gitignore + +# vim +**/*.swp + +# database +pgdata/ + +# envvars +.env +.env.example + +# docker +.dockerignore +docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ab53d07 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM ubuntu:latest + +ENV PATH="/root/.local/bin:$PATH" + +WORKDIR /app + +COPY pyproject.toml . + +RUN apt update && apt install -y tree vim postgresql-client build-essential libssl-dev libffi-dev python3 python3-dev python3-pip python3-venv && pip install pipx && pipx install poetry && poetry install + +COPY . . diff --git a/docker-compose.yml b/docker-compose.yml index 2e2154f..4aaccbe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,7 +12,8 @@ services: POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" POSTGRES_DB: "${POSTGRES_DB}" test: - image: ubuntu:latest + build: . + image: discordoragi_test:latest depends_on: - db environment: