PK!aiohypixel/__init__.py#!/usr/bin/env python3 # -*- coding: utf-8 -*- # MIT License # Copyright (c) 2018-2019 Tmpod # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ This is an asynchronous Hypixel API wrapper written in Python Compatible with Python3.6 and up NOTE: It's highly recommended that you setup logging for this module! A lot of info that you may not want to miss is reported through there. Doing it for aiohttp might also be good (INFO level is generaly enough) """ from . import ( shared, session, player, guild, keys, boosters, friends, watchdogstats, stats ) from .session import * from .shared import * from .player import * from .stats import * from .guild import * from .boosters import * from .keys import * from .friends import * from .watchdogstats import * __author__ = "Tmpod" __url__ = f"https://gitlab.com/{__author__}/aiohypixel/" __version__ = "0.0.1a" __licence__ = "MIT" __copyright__ = f"Copyright (c) 2018-2019 {__author__}" PK!pƷ aiohypixel/__main__.py#!/usr/bin/env python3 # -*- coding: utf-8 -*- # MIT License # Copyright (c) 2018-2019 Tmpod # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ This is just a little *crude* interface for the wrapper. """ from .session import HypixelSession import logging, asyncio, sys from shutil import get_terminal_size WIN_SIZE = get_terminal_size() def fmtp(s: str) -> str: """Just prints the string with a little delimiter around it""" ndash = ("-" * (len(s) - 2)) if len(s) <= WIN_SIZE.columns else "-" * (WIN_SIZE.columns - 2) print(f"\n<{ndash}>\n{s}\n<{ndash}>\n") try: api_key = sys.argv[1] except IndexError: fmtp("You must provide an API key!") else: try: get_type = sys.argv[2] except IndexError: fmtp("You must provide a get method!") else: logging.basicConfig(level="DEBUG") try: query = sys.argv[3] except IndexError: query = None loop = asyncio.get_event_loop() async def main(get_type: str, query: str = None) -> None: session = await HypixelSession(api_key) try: if query is None: fmtp(str(await (getattr(session, f"get_{get_type}"))())) return fmtp(str(await (getattr(session, f"get_{get_type}"))(query))) except AttributeError: fmtp("Invalid!") loop.run_until_complete(main(get_type, query)) PK!K⑭KKaiohypixel/asyncinit.py#!/usr/bin/env python3 # -*- coding: utf-8 -*- # MIT License # # Copyright (c) 2018-2019 Neko404NotFound # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in a$ # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ Mixin that makes the constructor for the class so it must be awaited. Adds an ``__ainit__`` constructor to enable awaiting certain actions during object construction. Useful for stuff like initializing asyncpg connection pools in classes deriving from ``discord.Client`` or ``discord.ext.commands.Bot``. Author: Espy/Neko404NotFound This is a very neat submodule contained in libneko (https://gitlab.com/koyagami/libneko) I've asked it's creator and I have permission to port this file so that the whole libneko module doesn't become a dependency. """ __all__ = ("T", "TT", "AsyncInit") import asyncio import typing T = typing.TypeVar("T") TT = typing.Type[T] class AsyncInit: """ Base class for a class that needs an asynchronous constructor. This modifies some internal signatures to force ``__new__`` to return an awaitable with the resultant object inside as the result. If an ``__ainit__`` dunder is provided, then this will be called and awaited internally after ``__init__`` is invoked. Example usage:: import asyncio from aiohttp import ClientSession from asyncpg import create_pool from asyncpg.pool import Pool from discord.ext import commands class Bot(AsyncInit, commands.Bot): def __init__(self, **kwargs): super().__init__(**kwargs) # Should declare these in constructor here, but it is # not necesarry. self.http_session: ClientSession = None self.postgres_pool: Pool = None async def __ainit__(self, **kwargs): self.http_session = ClientSession() self.postgres_pool = await create_pool(...) ... loop = asyncio.get_event_loop() async def init_bot(): # Obviously we must call this in a coroutine. return await Bot(command_prefix='?!') # Alternatively, from outside a non-running event loop. bot = asyncio.get_event_loop().run_until_complete(Bot(command_prefix='?!')) """ def __init__(self, *args, **kwargs) -> None: """ Default ``__init__`` override to fall back to if the user fails to implement a synchronous constructor. This prevents errors implicitly calling the constructor or missing arguments in the ``__ainit__``. """ try: super().__init__(*args, **kwargs) except TypeError: super().__init__() async def __ainit__(self, *args, **kwargs) -> None: """ Default ``__ainit__`` to fall back to if one is not defined elsewhere. """ ... @staticmethod def __new__(cls, *args, **kwargs) -> typing.Awaitable[T]: """Initialise a new instance of this object.""" # Return a coroutine to be awaited. return cls.__anew__(cls, *args, **kwargs) @staticmethod async def __anew__(cls, *args, **kwargs) -> T: """|coro| Calls both the __init__ and __ainit__ methods. """ obj = super().__new__(cls) cls.__init__(obj, *args, **kwargs) if hasattr(cls, "__ainit__"): cls.__ainit__ = asyncio.coroutine(cls.__ainit__) await cls.__ainit__(obj, *args, **kwargs) return obj if typing.TYPE_CHECKING: # Mutes errors caused by the fact we are doing voodoo with __new__ def __await__(self, *args, **kwargs) -> T: return self PK!:=aiohypixel/boosters.py#!/usr/bin/env python3 # -*- coding: utf-8 -*- # MIT License # Copyright (c) 2018-2019 Tmpod # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ Stuff for the boosters endpoint """ __all__ = ("process_raw_booster", "Booster") from dataclasses import dataclass from datetime import datetime from typing import Union, Tuple from .shared import APIResponse, GAME_TYPES_TABLE def process_raw_booster(json_data: APIResponse): activated_at = datetime.utcfromtimestamp(json_data["dateActivated"] / 1000) return Booster( purchaser_uuid=json_data["purchaserUuid"], amount=json_data["amount"], original_length=json_data["originalLength"], current_length=json_data["length"], game_type=json_data["gameType"], game=GAME_TYPES_TABLE.get(json_data["gameType"]), activated_at=activated_at, stacked=json_data.get("stacked"), ) @dataclass(frozen=True) class Booster: purchaser_uuid: str amount: int original_length: int current_length: int game_type: int game: str activated_at: datetime stacked: Union[ bool, Tuple[str], None ] # not sure what this is... I'll get back here soon PK!__aiohypixel/friends.py#!/usr/bin/env python3 # -*- coding: utf-8 -*- # MIT License # Copyright (c) 2018-2019 Tmpod # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ Stuff for the friends endpoint """ __all__ = ("process_raw_friend", "Friend") from dataclasses import dataclass from datetime import datetime from .shared import APIResponse def process_raw_friend(json_data: APIResponse): started_at = datetime.utcfromtimestamp(json_data["started"] / 1000) return Friend(sender_uuid=json_data["uuidSender"], receiver_uuid=json_data["uuidReceiver"], started_at=started_at) @dataclass(frozen=True) class Friend: sender_uuid: str receiver_uuid: str started_at: datetime PK!_?LZZaiohypixel/guild.py#!/usr/bin/env python3 # -*- coding: utf-8 -*- # MIT License # Copyright (c) 2018-2019 Tmpod # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ Custom classes that are related to guild lookups on the Hypixel API """ __all__ = ("process_raw_guild", "Guild") from dataclasses import dataclass from datetime import datetime from typing import Union, Dict, Tuple, List from .shared import APIResponse LVL_EXP_NEEDED = [ 100000, 150000, 250000, 500000, 750000, 1000000, 1250000, 1500000, 2000000, 2500000, 2500000, 2500000, 2500000, 2500000, 3000000, ] def get_guild_level(exp: int) -> int: level = 0 i = 0 while True: need = LVL_EXP_NEEDED[i] exp -= need if exp < 0: return level level += 1 if i < len(LVL_EXP_NEEDED) - 1: i += 1 @dataclass(frozen=True) class GuildRank: name: str default: bool tag: Union[str, None] created_at: datetime priority: 1 @dataclass(frozen=True) class GuildMember: uuid: str rank: str joined_at: datetime quest_participation: int @dataclass(frozen=True) class Guild: """Describes a Hypixel guild""" raw_data: APIResponse id: str name: str coins: int total_coins: int created_at: datetime joinable: bool tag: Dict[str, str] exp: int level: int preferred_games: List[str] ranks: Tuple[GuildRank] members: Tuple[GuildMember] banner: dict # The API is kinda messy on this one achievements: Dict[str, int] exp_by_game: Dict[str, int] def process_raw_rank(json_data: dict) -> GuildRank: created_at = datetime.utcfromtimestamp(json_data["created"] / 1000) return GuildRank( name=json_data["name"], default=json_data["default"], tag=json_data["tag"], created_at=created_at, priority=json_data["priority"], ) def process_raw_member(json_data: dict) -> GuildMember: joined_at = datetime.utcfromtimestamp(json_data["joined"] / 1000) return GuildMember( uuid=json_data["uuid"], rank=json_data["rank"], joined_at=joined_at, quest_participation=json_data.get("questParticipation", 0), ) def process_raw_guild(json_data: APIResponse) -> Guild: """This will process the JSON data and return a Guild object""" # Converting UNIX timestamp (ms) to a datetime obj created_at = datetime.utcfromtimestamp(json_data["created"] / 1000) lvl = get_guild_level(json_data["exp"]) tag = { "text": json_data["tag"], "colour": json_data["tagColor"], "color": json_data["tagColor"], # for 'muricans } ranks = (process_raw_rank(r) for r in json_data["ranks"]) members = (process_raw_member(m) for m in json_data["members"]) return Guild( raw_data=json_data, id=json_data["_id"], name=json_data["name"], coins=json_data["coins"], total_coins=json_data["coinsEver"], created_at=created_at, tag=tag, exp=json_data["exp"], level=lvl, joinable=json_data.get("joinable", False), preferred_games=json_data.get("preferredGames"), ranks=tuple(ranks), members=tuple(members), banner=json_data.get("banner"), achievements=json_data["achievements"], exp_by_game=json_data["guildExpByGameType"] ) PK!aiohypixel/keys.py#!/usr/bin/env python3 # -*- coding: utf-8 -*- # MIT License # Copyright (c) 2018-2019 Tmpod # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ Stuff for the keys endpoint """ __all__ = ("process_raw_key", "Key") from dataclasses import dataclass from datetime import datetime from .shared import APIResponse def process_raw_key(json_data: APIResponse): return Key( owner_uuid=json_data["ownerUuid"], value=json_data["key"], total_queries=json_data["totalQueries"], queries_in_past_min=json_data.get("queriesInPastMin"), ) @dataclass(frozen=True) class Key: owner_uuid: str value: str # The actual key total_queries: int queries_in_past_min: int PK!)aiohypixel/player.py#!/usr/bin/env python3 # -*- coding: utf-8 -*- # MIT License # Copyright (c) 2018-2019 Tmpod # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ Custom classes that are related to player lookups on the Hypixel API """ from dataclasses import dataclass from datetime import datetime from math import sqrt, floor from typing import Union, List, Dict, Tuple from .shared import APIResponse, ImmutableProxy from .stats import * #: Locations where the player rank might be stored POSSIBLE_RANK_LOC = ("packageRank", "newPackageRank", "monthlyPackageRank", "rank") #: Level calculation stuff EXP_FIELD = 0 LVL_FIELD = 0 BASE = 10000 GROWTH = 2500 HALF_GROWTH = 0.5 * GROWTH REVERSE_PQ_PREFIX = -(BASE - 0.5 * GROWTH) / GROWTH REVERSE_CONST = REVERSE_PQ_PREFIX * REVERSE_PQ_PREFIX GROWTH_DIVIDES_2 = 2 / GROWTH BEDWARS_EXP_PER_PRESTIGE = 489000 BEDWARS_LEVELS_PER_PRESTIGE = 100 def get_level(exp): return floor(1 + REVERSE_PQ_PREFIX + sqrt(REVERSE_CONST + GROWTH_DIVIDES_2 * exp)) def get_exact_level(exp): return get_level(exp) + get_percentage_to_next_lvl(exp) def get_exp_from_lvl_to_next(level): return GROWTH * (level - 1) + BASE def get_total_exp_to_lvl(level): lv = floor(level) x0 = get_total_exp_to_full_lvl(lv) if level == lv: return x0 else: return (get_total_exp_to_full_lvl(lv + 1) - x0) * (level % 1) + x0 def get_total_exp_to_full_lvl(level): return (HALF_GROWTH * (level - 2) + BASE) * (level - 1) def get_percentage_to_next_lvl(exp): lv = get_level(exp) x0 = get_total_exp_to_lvl(lv) return (exp - x0) / (get_total_exp_to_lvl(lv + 1) - x0) def get_exp(EXP_FIELD, LVL_FIELD): exp = int(EXP_FIELD) exp += get_total_exp_to_full_lvl(LVL_FIELD + 1) return exp @dataclass(frozen=True) class Player: """ This will describe a player """ raw_data: APIResponse hypixel_id: int uuid: int username: str aliases: List[str] # in chronological order one_time_achievements: List[str] # also in chronological order achievments: ImmutableProxy mc_version: str rank: str was_staff: bool rank_colour: str rank_color: str # for 'muricans outfit: ImmutableProxy voting: ImmutableProxy parkours: ImmutableProxy #: If it is `None`, it means the full player info wasn't requested stats: Tuple[ImmutableProxy] = None def process_raw_player(json_data: APIResponse, partial: bool = False) -> Player: """ This handles the raw json returned by the API and creates a Player object Pass the `partial` param as True to get a PartialPlayer object instead. """ # Basic player info processed_data = { "hypixel_id": json_data["_id"], "uuid": json_data["uuid"], "username": json_data["displayname"], "aliases": [*json_data["knownAliases"].values()], "raw_data": json_data } # Last used MC version processed_data.update({"mc_version": json_data.get("mcVersionRp")}) # Rank for l in POSSIBLE_RANK_LOC: if l in json_data: if l == "rank" and json_data[l] == "NORMAL": was_staff = True else: was_staff = False if json_data[l].upper() == "NONE": continue rank = ( json_data[l] .title() .replace("_", " ") .replace("Mvp", "MVP") .replace("Vip", "VIP") .replace("Superstar", "MVP++") .replace("Youtuber", "YouTube") .replace(" Plus", "+") ) processed_data.update({"rank": rank, "was_staff": was_staff}) # Calculating player's Network EXP processed_data.update({"level": get_level(json_data["networkExp"])}) # Voting stats # Just processing the time snowflakes # Doing a cheeky workaround :D json_data["voting"] = ImmutableProxy( { k: datetime.utcfromtimestamp(v / 1000) if k.startswith("last") else v for k, v in json_data.get("voting", {}) } ) # That can be translated to: # for k, v in json_data.get("voting", {}): # if k.startswith("last"): # json_data.get("voting", {})[k] = datetime.utcfromtimestamp(v / 1000) # Parkours # More cheeky trickery p_actions = { "timeStart": lambda t: datetime.utcfromtimestamp(t / 1000), "timeTook": lambda t: t / 1000, } json_data["parkours"] = ImmutableProxy( [ImmutableProxy({k: p_actions.get(k, lambda x: x)(v) for k, v in l}) for a in l] for l in json_data.get("parkourCompletions", {}) ) # That can be translated to: # p_tmp = {} # for lobby in json_data.get("parkourCompletions", {}): # new_attemps = [] # for attempt in lobby: # new_attempt = {} # for key, value in attempt: # new_attempt[key] = p_actions.get(key, lambda x: x)(value) # new_attempts.append(new_attempt) # p_tmp[lobby] = new_attemps # json_data["parkours"] = ImmutableProxy(p_tmp) # Current outfit processed_data.update( { "outfit": { k.lower(): v.title().replace("_", " ") for k, v in json_data.get("outfit", {}) } or None } ) if partial: return Player(**processed_data) processed_data.update( {"stats": process_raw_player_stats(json_data.get("stats", {}))} ) return Player(**processed_data) ### Stats ### # This is extracted from Plancke's php stuff BW_XP_PER_PRESTIGE = 489000 BW_LVLS_PER_PRESTIGE = 100 def get_bw_lvl(exp: int) -> int: prestige = exp // BW_XP_PER_PRESTIGE exp = exp % BW_XP_PER_PRESTIGE if prestige > 5: over = prestige % 5 exp += over * BW_XP_PER_PRESTIGE prestige -= over if exp < 500: return 0 + (prestige * BW_LVLS_PER_PRESTIGE) if exp < 1500: return 1 + (prestige * BW_LVLS_PER_PRESTIGE) if exp < 3500: return 2 + (prestige * BW_LVLS_PER_PRESTIGE) if exp < 5500: return 3 + (prestige * BW_LVLS_PER_PRESTIGE) if exp < 9000: return 4 + (prestige * BW_LVLS_PER_PRESTIGE) exp -= 9000 return (exp / 5000 + 4) + (prestige * BW_LVLS_PER_PRESTIGE) def process_raw_player_stats( json_data: APIResponse, game: Union[str, None] = None ) -> Tuple[ImmutableProxy]: """This will process the JSON data into a tuple of game stats""" # Processing bedwars exp json_data.get("Bedwars", {})["level"] = get_bw_lvl( json_data.get("Bedwars", {})["Experience"] ) ... # Process more stuff in the future ig return tuple(ImmutableProxy(d) for _, d in json_data.items()) PK!e,UU5U5aiohypixel/session.py#!/usr/bin/env python3 # -*- coding: utf-8 -*- # MIT License # Copyright (c) 2018-2019 Tmpod # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ This defines a connection to the Hypixel API. All requests will pass through here and this will handle everything for you """ __all__ = ("HypixelSession",) import aiohttp import asyncio import logging from typing import Set, Union from random import choice import re from .asyncinit import AsyncInit from .shared import * from .player import * from .guild import * from .boosters import * from .keys import * from .friends import * from .watchdogstats import * #: This is the base URL for all the requests regarding the Hypixel API BASE_API_URL = "https://api.hypixel.net/" #: Max API keys set acceptable lenght if no 'force validation' option is passed KEY_SET_LEN_NO_FORCE = 10 #: This is the message sent when you try to do a request with an invalid key INVALID_KEY_MESSAGE = "Invalid API key!" #: Mojang name lookup endpoint MOJANG_API_URL = "https://api.mojang.com/users/profiles/minecraft/" #: This is th length of a UUID. 32 hex digits + 4 dashes UUID_LENGTH = (32, 36) #: This is the boundaries of char lenghts for Minecraft usernames MC_NAME_LENGTH = (3, 16) #: Regex pattern to filter UUID strings UUID_RE = re.compile( r"^[0-9a-f]{8}-?[0-9a-f]{4}-?[1-5][0-9a-f]{3}-?[89AB][0-9a-f]{3}-?[0-9a-f]{12}$", re.I ) MONGO_ID_RE = re.compile(r"^[a-f\d]{24}$", re.I) class HypixelSession(AsyncInit): """ Represents a connection to the Hypixel API. """ def __init__( self, api_keys: Union[str, Set[str]], *, base_url: str = BASE_API_URL, force_validation: bool = False, ignore_validation_errors: bool = False, ) -> None: """ Does some basic validation checks and setting some attributes. A validation request will be done in :attr:`__ainit__` """ self._logger = logging.getLogger(__name__) self._logger.debug( "Starting a new session! Doing some base checks to the API key(s)" ) if not isinstance(api_keys, (str, set)): self._logger.error("The keys provided are not valid! Raising ValueError") raise ValueError( "You can only pass a string (single key) or list or set with strings (multiple keys)." ) elif isinstance(api_keys, str): self.api_keys = (api_keys.strip(),) else: self.api_keys = tuple(k.strip() for k in api_keys) self._logger.debug("Finished aggregating the key(s)!\nNow setting up some more stuff...") self._loop = asyncio.get_event_loop() self.aiohttp = aiohttp.ClientSession(loop=self._loop) self._base_url: str = base_url self._force_key_vald: bool = force_validation self._ignore_vald_err: bool = ignore_validation_errors self._logger.debug("Base init all done! Loop and aiohttp session acquired!") async def __ainit__( self, api_keys: Union[str, Set[str]], *, base_url: str = BASE_API_URL, force_validation: bool = False, ignore_validation_errors: bool = False, ) -> None: """ This is where the keys are validated through abstract requests to the API, effectively testing them. This uses the ``key`` endpoint which is also used in :attr:`get_key` """ self._logger.debug( "Now proceeding with a request validation of the provided keys!" ) if len(self.api_keys) > KEY_SET_LEN_NO_FORCE and not self._force_key_vald: self._logger.info( "The collection of keys is too long and you haven't specified " "'force_validation', so it will be skipped!" ) else: invalid_keys = await self._check_keys(self.api_keys) self.api_keys = tuple(set(self.api_keys) - invalid_keys) self._logger.debug("Async init all done! The session is now fully ready!") async def _check_keys(self, keys: Union[str, Set[str]]) -> Union[None, Set[str]]: """This will do the actual checking of the keys via some requests""" rejected_keys = set() for k in keys: async with self.aiohttp.get(f"{BASE_API_URL}key/?key={k}", ssl=False) as resp: result = await resp.json() # Probably the second check isn't even really necessary if ( not result.get("success") and result.get("cause") == INVALID_KEY_MESSAGE ): if self._ignore_vald_err: self._logger.info( '"%s" isn\'t valid! It was rejected by the API! Removing it...', k, ) rejected_keys.add(k) else: self._logger.error( '"%s" isn\'t valid! It was rejected by the API! Raising ValueError!', k, ) raise InvalidAPIKey(f"{k} isn't a valid API key!") self._logger.debug( "Finished checking the API keys. Encoutered %s error%s!", len(rejected_keys) or "no", "s" if rejected_keys else "", ) return rejected_keys async def edit_keys( self, new_keys: Union[str, Set[str]], force_validation: bool = False ) -> None: """ Allows you to edit your registered keys for this session. Will verify the keys again, just like in :attr:`__ainit__` """ if len(self.api_keys) > KEY_SET_LEN_NO_FORCE and not force_validation: self._logger.info( "The collection of keys is too long and you haven't specified " "'force_validation', so it will be skipped!" ) else: invalid_keys = await self._check_keys(self.api_keys) self.api_keys = tuple(set(self.api_keys) - invalid_keys) self._logger.info( "Keys were changed! Check the logs to see if there was any error " "(applicable if you set it to ignore the errors" ) async def name_to_uuid(self, query: str) -> str: """ Gets the UUID for the player with the given name. Uses the Mojang API for that. """ self._logger.debug("Fetching UUID from name: %s", query) async with self.aiohttp.get(f"{MOJANG_API_URL}{query}") as resp: resp.raise_for_status() return (await resp.json())["id"] async def uuid_to_name(self, query: str) -> str: """ Gets the name for the player with the given UUID. Uses the Mojang API for that. """ self._logger.debug("Fetching name from UUID: %s", query) async with self.aiohttp.get(f"{MOJANG_API_URL}{query}") as resp: resp.raise_for_status() return (await resp.json())["name"] async def _fetch_json(self, endpoint: str, params: dict = None): """ Fetches the JSON from the passed endpoint with the given params. Note: ``key`` will always be a param. """ self._logger.debug("Starting to fetch JSON from the **%s** endpoint with these params: %s", endpoint, params) self._logger.debug("Chosing key...") if params is None: params = {"key": choice(self.api_keys)} elif "key" not in params: params["key"] = choice(self.api_keys) self._logger.debug("Chose: `%s`\nNow doing the actual fetching...", params["key"]) async with self.aiohttp.get(f"{BASE_API_URL}{endpoint}", params=params, ssl=False) as resp: resp.raise_for_status() result = await resp.json() try: if not result["success"]: self._logger.error("Request failed! Cause: %s", result["cause"]) raise UnsuccesfulRequest(result["cause"]) self._logger.debug("Request was successful! Returning result...") return result except KeyError: raise HypixelAPIError( "Something went wrong with the Hypixel API! Try again later." ) @staticmethod def is_uuid(query: str) -> bool: return bool(UUID_RE.findall(query)) async def get_player(self, query: str) -> Player: """ Gets a full player object (general info + game stats) with the given query. The query can either be a UUID or a Minecraft IGN. """ self._logger.debug("Checking if query is a UUID...") if self.is_uuid(query): query_type = "uuid" else: query_type = "name" result = await self._fetch_json("player", params={query_type: query}) return process_raw_player(result["player"]) async def get_player_stats(self, query: str) -> Tuple[ImmutableProxy]: """ Gets a player's stats with the given query. The query can either be a UUID or a Minecraft IGN. """ self._logger.debug("Checking if query is a UUID...") if self.is_uuid(query): query_type = "uuid" else: query_type = "name" result = await self._fetch_json("player", params={query_type: query}) return process_raw_player_stats(result["player"]) async def get_player_info(self, query: str) -> Player: """ Gets a player object with just the network info that does not relate to any stats. The query can either be a UUID or a Minecraft IGN. """ self._logger.debug("Checking if query is a UUID...") if self.is_uuid(query): query_type = "uuid" else: query_type = "name" result = await self._fetch_json("player", params={query_type: query}) return process_raw_player(result["player"], partial=True) async def get_guild(self, query: str) -> Guild: """ Gets a guild object from the given query. This query can be either an ID or a name """ self._logger.debug("Checking if query is a UUID...") if MONGO_ID_RE.findall(query): query_type = "id" else: query_type = "name" result = await self._fetch_json("guild", params={query_type: query}) return process_raw_guild(result["guild"]) async def get_guild_by_player(self, query: str) -> Union[None, Guild]: """ Gets a guild object to which the player with the given name/UUID belongs to. Can be ``None``, of course. """ self._logger.debug("Checking if query is a UUID...") if self.is_uuid(query): uuid = query else: self._logger.debug("Need to get the UUID for this name!") uuid = self.name_to_uuid(query) g_id = (await self._fetch_json("findGuild", params={"byUuid": uuid})).get("guild") if g_id is None: return result = await self._fetch_json("guild", params={"id": g_id}) return process_raw_guild(result["guild"]) async def get_key(self, query: str) -> Key: """Gets a Key object from the passed query.""" if not self.is_uuid(query): raise InvalidAPIKey( f"{query} isn't a valid UUID and so it isn't a valid key!" ) result = await self._fetch_json("key", params={"key": query}) return process_raw_key(result["record"]) async def get_friends(self, query: str) -> Tuple[Friend]: """Gets a tuple of Friend objects that the player with the given name/UUID has.""" if not self.is_uuid(query): raise ValueError(f"{query} isn't a valid UUID!") result = await self._fetch_json( "friends", params={"uuid": query} ) friends = (process_raw_friend(f) for f in result.get("records")) return tuple(friends) async def get_boosters(self) -> Tuple[Booster]: """Gets a tuple of Booster objects denoting the currently active boosters on the Hypixel Network""" result = await self._fetch_json("boosters") boosters = tuple(process_raw_booster(b) for b in result.get("boosters", ())) return {"boosters": boosters, "state": result.get("boosterState")} async def get_watchdogstats(self): """Gets the current WatchDog stats""" result = await self._fetch_json("watchdogstats") return process_raw_wds(result) PK!*_@aiohypixel/shared.py#!/usr/bin/env python3 # -*- coding: utf-8 -*- # MIT License # Copyright (c) 2018-2019 Tmpod # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ Custom exceptions related to the Hypixel API and other misc stuff """ __all__ = ( "APIResponse", "PlayerNotFound", "GuildIDNotValid", "HypixelAPIError", "InvalidAPIKey", "GAME_TYPES_TABLE", ) import inspect from typing import NewType, Dict, Union, Mapping, Iterator GAME_TYPES_TABLE = { 2: {"type_name": "QUAKECRAFT", "db_name": "Quake", "pretty_name": "Quake"}, 3: {"type_name": "WALLS", "db_name": "Walls", "pretty_name": "Walls"}, 4: {"type_name": "PAINTBALL", "db_name": "Paintball", "pretty_name": "Paintball"}, 5: { "type_name": "SURVIVAL_GAMES", "db_name": "HungerGames", "pretty_name": "Blitz Survival Games", }, 6: {"type_name": "TNTGAMES", "db_name": "TNTGames", "pretty_name": "TNT Games"}, 7: {"type_name": "VAMPIREZ", "db_name": "VampireZ", "pretty_name": "VampireZ"}, 13: {"type_name": "WALLS3", "db_name": "Walls3", "pretty_name": "Mega Walls"}, 14: {"type_name": "ARCADE", "db_name": "Arcade", "pretty_name": "Arcade"}, 17: {"type_name": "ARENA", "db_name": "Arena", "pretty_name": "Arena"}, 20: {"type_name": "UHC", "db_name": "UHC", "pretty_name": "UHC Champions"}, 21: {"type_name": "MCGO", "db_name": "MCGO", "pretty_name": "Cops and Crims"}, 23: { "type_name": "BATTLEGROUND", "db_name": "Battleground", "pretty_name": "Warlords", }, 24: { "type_name": "SUPER_SMASH", "db_name": "SuperSmash", "pretty_name": "Smash Heroes", }, 25: { "type_name": "GINGERBREAD", "db_name": "GingerBread", "pretty_name": "Turbo Kart Racers", }, 26: {"type_name": "HOUSING", "db_name": "Housing", "pretty_name": "Housing"}, 51: {"type_name": "SKYWARS", "db_name": "SkyWars", "pretty_name": "SkyWars"}, 52: { "type_name": "TRUE_COMBAT", "db_name": "TrueCombat", "pretty_name": "Crazy Walls", }, 54: {"type_name": "SPEED_UHC", "db_name": "SpeedUHC", "pretty_name": "Speed UHC"}, 55: {"type_name": "SKYCLASH", "db_name": "SkyClash", "pretty_name": "SkyClash"}, 56: {"type_name": "LEGACY", "db_name": "Legacy", "pretty_name": "Classic Games"}, 57: {"type_name": "PROTOTYPE", "db_name": "Prototype", "pretty_name": "Prototype"}, 58: {"type_name": "BEDWARS", "db_name": "Bedwars", "pretty_name": "Bed Wars"}, 59: { "type_name": "MURDER_MYSTERY", "db_name": "MurderMystery", "pretty_name": "Murder Mystery", }, 60: { "type_name": "BUILD_BATTLE", "db_name": "BuildBattle", "pretty_name": "Build Battle", }, 61: {"type_name": "DUELS", "db_name": "Duels", "pretty_name": "Duels"}, } APIResponse = NewType("APIResponse", Dict[str, Dict[Union[str, int], Union[str, int, bool, dict]]]) class PlayerNotFound(Exception): """ Raised if a player/UUID is not found. This exception can usually be ignored. You can catch this exception with ``except aiohypixel.PlayerNotFoundException:`` """ pass class GuildIDNotValid(Exception): """ Raised if a Guild is not found using a GuildID. This exception can usually be ignored. You can catch this exception with ``except aiohypixel.GuildIDNotValid:`` """ pass class HypixelAPIError(Exception): """ Raised if something's gone very wrong and the program can't continue. """ pass class UnsuccesfulRequest(Exception): """ Raised when the "success" key from a request is False """ pass class InvalidAPIKey(Exception): """ Raised if the given API Key is invalid """ pass def find(func, iterable): for i in iterable: if func(i): return i return None def get(iterable, **attrs): def predicate(elem): for attr, val in attrs.items(): nested = attr.split('__') obj = elem for attribute in nested: obj = getattr(obj, attribute) if obj != val: return False return True return find(predicate, iterable) class ImmutableProxy(Mapping): """ An immutable proxy class. Similar to the Proxy class, but works differently. This is designed to be used for configuration files to convert them to dot-notation friendly immutable notation. It is recursive, and will convert list objects to tuples, dicts to instances of this class, and protects from recursive lookup issues. Source: https://gitlab.com/koyagami/libneko/blob/master/libneko/aggregates.py#L333-396 Author: Espy/Ko Yagami (https://gitlab.com/koyagami) """ def __init__(self, kwargs, *, recurse=True): """ If recurse is True, it converts all inner dicts to this type too. """ if not isinstance(kwargs, dict): raise TypeError(f"Expected dictionary, got {type(kwargs).__name__}") if recurse: self.__dict = self._recurse_replace(kwargs) else: self.__dict = kwargs def __getattr__(self, item): try: return self.__dict[item] except KeyError: raise AttributeError(item) from None def __getitem__(self, item): return self.__dict[item] def __len__(self) -> int: return len(self.__dict) def __iter__(self) -> Iterator: return iter(self.__dict) def __str__(self): return str(self.__dict) def __repr__(self): return repr(self.__dict) def __hash__(self): return object.__hash__(self) @classmethod def _recurse_replace(cls, obj, already_passed=None): if already_passed is None: already_passed = [] new_dict = {} for k, v in obj.items(): if isinstance(v, dict): if v in already_passed: new_dict[k] = v else: already_passed.append(v) new_dict[k] = cls._recurse_replace(v, already_passed) elif isinstance(v, list): new_dict[k] = tuple(v) else: new_dict[k] = v return cls(new_dict, recurse=False) PK!}aiohypixel/stats.py#!/usr/bin/env python3 # -*- coding: utf-8 -*- # MIT License # Copyright (c) 2018-2019 Tmpod # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ Stats dataclasses and other related functions """ __all__ = ("BaseGameStats", "BedWarsStats") from dataclasses import dataclass BW_XP_PER_PRESTIGE = 489000 BW_LVLS_PER_PRESTIGE = 100 def get_bw_lvl(exp: int) -> int: prestige = exp // BW_XP_PER_PRESTIGE exp = exp % BW_XP_PER_PRESTIGE if prestige > 5: over = prestige % 5 exp += over * BW_XP_PER_PRESTIGE prestige -= over if exp < 500: return 0 + (prestige * BW_LVLS_PER_PRESTIGE) if exp < 1500: return 1 + (prestige * BW_LVLS_PER_PRESTIGE) if exp < 3500: return 2 + (prestige * BW_LVLS_PER_PRESTIGE) if exp < 5500: return 3 + (prestige * BW_LVLS_PER_PRESTIGE) if exp < 9000: return 4 + (prestige * BW_LVLS_PER_PRESTIGE) exp -= 9000 return (exp / 5000 + 4) + (prestige * BW_LVLS_PER_PRESTIGE) @dataclass class BaseGameStats: """ This is the base for every game stats """ raw_data: dict @dataclass class BedWarsStats(BaseGameStats): """BedWars stats for a player""" xp: int PK!C2aiohypixel/watchdogstats.py#!/usr/bin/env python3 # -*- coding: utf-8 -*- # MIT License # Copyright (c) 2018-2019 Tmpod # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ Stuff for the Watchdog Stats endpoint """ __all__ = ("process_raw_wds", "WatchdogStats") from dataclasses import dataclass from .shared import APIResponse def process_raw_wds(json_data: APIResponse): return WatchdogStats( total=json_data["watchdog_total"], rolling_daily=json_data["watchdog_rollingDaily"], last_minute=json_data["watchdog_lastMinute"], staff_total=json_data["staff_total"], staff_rolling_daily=json_data["staff_rollingDaily"] ) @dataclass(frozen=True) class WatchdogStats: total: int rolling_daily: int last_minute: int staff_total: int staff_rolling_daily: int PK!J(++"aiohypixel-0.1.0.dist-info/LICENSEMIT License Copyright (c) 2018-2019 Tmpod Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HڽTU aiohypixel-0.1.0.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!H +r#aiohypixel-0.1.0.dist-info/METADATAN0EYZ)V RFiE ]!T? Mxvٞs)`O4Z @iw:j'ql<PK!HM^!aiohypixel-0.1.0.dist-info/RECORD}GH_ ǛA@ 'X#2Z}W*6p{_ ?R4t`'&P0ߺ0ڙ! }E$ˢmlp1@o]Y-/ds)Wx,O4v7P(CBV'Fn3o&2\> = X;sSVm4~t**3USNǡBdgCc2KSby]Ґ3F VF[68nM`_cm1(@]ynۗ :.S 71;nj@TkP=!dzeE}$q薧<-353QbW7xlyc4%^׍xy߲MP/5ֶ<,/<5u__SS1ꊤ턇2}{ ~ӁTeMF ooήFPK!aiohypixel/__init__.pyPK!pƷ aiohypixel/__main__.pyPK!K⑭KKaiohypixel/asyncinit.pyPK!:=e$aiohypixel/boosters.pyPK!__/-aiohypixel/friends.pyPK!_?LZZ3aiohypixel/guild.pyPK!}Eaiohypixel/keys.pyPK!)cLaiohypixel/player.pyPK!e,UU5U5Ekaiohypixel/session.pyPK!*_@͠aiohypixel/shared.pyPK!}aiohypixel/stats.pyPK!C2aiohypixel/watchdogstats.pyPK!J(++"aiohypixel-0.1.0.dist-info/LICENSEPK!HڽTU 1aiohypixel-0.1.0.dist-info/WHEELPK!H +r#aiohypixel-0.1.0.dist-info/METADATAPK!HM^!aiohypixel-0.1.0.dist-info/RECORDPKd3