import requests
from .constants import CollectionKey
from .logger import setup_logging, get_logger
_DEBUG_ = True
_LOGGER_NAME_ = "hypixelez"
_CATA_CUMULATIVE_XP_ = [
50,
125,
235,
395,
625,
955,
1425,
2095,
3045,
4385,
6275,
8940,
12700,
17960,
25340,
35640,
50040,
70040,
97640,
135640,
188140,
259640,
356640,
488640,
668640,
911640,
1239640,
1684640,
2284640,
3084640,
4149640,
5559640,
7459640,
9959640,
13259640,
17559640,
23159640,
30359640,
39559640,
51559640,
66559640,
85559640,
109559640,
139559640,
177559640,
225559640,
285559640,
360559640,
453559640,
569809640,
]
_SKILL_CUMULATIVE_LEVELS_ = [
0,
50,
175,
375,
675,
1175,
1925,
2925,
4425,
6425,
9925,
14925,
22425,
32425,
47425,
67425,
97425,
147425,
222425,
322425,
522425,
822425,
1222425,
1722425,
2322425,
3022425,
3822425,
4722425,
5722425,
6822425,
8022425,
9322425,
10722425,
12222425,
13822425,
15522425,
17322425,
19222425,
21222425,
23322425,
25522425,
27822425,
30222425,
32722425,
35322425,
38072425,
40972425,
44072425,
47472425,
51172425,
55172425,
59472425,
64072425,
68972425,
74172425,
79672425,
85472425,
91572425,
97972425,
104672425,
111672425,
]
_SKILL_LEVEL_UP_LEVELS_ = {
0,
50,
125,
200,
300,
500,
750,
1000,
1500,
2000,
3500,
5000,
7500,
10000,
15000,
20000,
30000,
50000,
75000,
100000,
200000,
300000,
400000,
500000,
600000,
700000,
800000,
900000,
1000000,
1100000,
1200000,
1300000,
1400000,
1500000,
1600000,
1700000,
1800000,
1900000,
2000000,
2100000,
2200000,
2300000,
2400000,
2500000,
2600000,
2750000,
2900000,
3100000,
3400000,
3700000,
4000000,
4300000,
4600000,
4900000,
5200000,
5500000,
5800000,
6100000,
6400000,
6700000,
7000000,
}
def _calculate_level(xp: int, cumulative_levels: list) -> int:
"""Calculate the level for a given XP using a cumulative XP table.
Args:
xp: Total accumulated XP.
cumulative_levels: A list where each item is the cumulative XP required
to reach the corresponding level index.
Returns:
The computed level as an integer index (0-based relative to the table).
"""
for i, required_xp in enumerate(cumulative_levels):
if required_xp > xp:
return i
return len(cumulative_levels)
def _calculate_current_xp(xp: int, cumulative_levels: list) -> int:
"""Calculate current XP progress within the current level.
This returns how much XP the player has earned toward the next level,
not the total XP.
Args:
xp: Total accumulated XP.
cumulative_levels: A list of cumulative XP thresholds.
Returns:
XP accumulated within the current level.
"""
level = _calculate_level(xp, cumulative_levels)
if level == 0:
return xp
return xp - cumulative_levels[level - 1]
[docs]
class HypixelClient:
"""HTTP client for the Hypixel SkyBlock API.
This client also supports resolving Minecraft usernames to UUIDs via Mojang API.
"""
def __init__(
self,
api_key: str,
debug=_DEBUG_,
base_url="https://api.hypixel.net/v2/skyblock/profile",
):
"""Create a Hypixel API client.
Args:
api_key: Hypixel API key (get one at https://developer.hypixel.net/).
debug: If True, enables debug logging; otherwise uses info-level logging.
base_url: Hypixel endpoint used by :meth:`fetch_profile_info`.
Notes:
- Initializes an internal `requests.Session`.
- Maintains an in-memory UUID cache for `get_uuid_by_name`.
"""
setup_logging(debug)
self.logger = get_logger(_LOGGER_NAME_)
self._uuid_cache = {}
self.api_key = api_key
self.session = requests.Session()
self.base_url = base_url
[docs]
def get_uuid_by_name(self, name: str) -> str | None:
"""Resolve a Minecraft username to a UUID using Mojang API.
The result is cached in-memory for the lifetime of the client.
Args:
name: Minecraft username.
Returns:
The UUID string if found; otherwise None.
Returns None also in case of network errors.
Notes:
This method currently swallows `requests` exceptions and returns None
instead of raising.
"""
if name in self._uuid_cache:
self.logger.debug(f"UUID cache HIT for: {name}")
return self._uuid_cache[name]
try:
response = requests.get(
f"https://api.mojang.com/users/profiles/minecraft/{name}", timeout=10
)
response.raise_for_status()
data = response.json()
if "id" not in data:
self.logger.warning(f"UUID not found for player: {name}")
return None
self._uuid_cache[name] = data["id"]
self.logger.debug(f"Cached UUID for: {name}")
return self._uuid_cache[name]
except requests.exceptions.RequestException as e:
self.logger.error(f"Failed to fetch UUID for {name}: {e}")
return None
[docs]
def get_profile_names_ids_by_id(self, uuid: str) -> dict:
"""Get available SkyBlock profiles for a player UUID.
Args:
uuid: Minecraft UUID.
Returns:
A mapping ``{profile_name: profile_id}``, where profile_name is
the Hypixel "cute_name" (e.g. "Peach"), and profile_id is the profile id.
Raises:
requests.RequestException: If the underlying HTTP request fails.
Notes:
This method currently assumes the response contains a ``"profiles"`` key.
"""
headers = {
"API-Key": self.api_key,
}
params = {"uuid": uuid}
response = self.session.get(
"https://api.hypixel.net/v2/skyblock/profiles",
headers=headers,
params=params,
).json()
x = response["profiles"]
names = {}
for i in x:
names[i["cute_name"]] = i["profile_id"]
return names
[docs]
def fetch_profile_info(self, uuid: str, profile: str):
"""Fetch full SkyBlock profile data and wrap it in :class:`SkyblockProfileData`.
Args:
uuid: Minecraft UUID.
profile: SkyBlock profile id.
Returns:
A :class:`SkyblockProfileData` instance with the raw API response and UUID.
Raises:
requests.RequestException: For network issues or non-2xx HTTP status.
Exception: If Hypixel returns ``success=false`` (API-level error).
Notes:
On API-level errors the code currently raises a generic `Exception`.
Consider introducing a custom exception type for better UX and docs.
"""
headers = {
"API-Key": self.api_key,
}
params = {"uuid": uuid, "profile": profile}
try:
response = self.session.get(self.base_url, headers=headers, params=params)
response.raise_for_status()
data = response.json()
if not data["success"]:
raise Exception(f"API Error: {data.get('cause', 'Unknown error')}")
return SkyblockProfileData(data, uuid)
except requests.exceptions.RequestException as e:
raise e
[docs]
class SkyblockProfileData:
"""Wrapper around Hypixel SkyBlock profile JSON with convenience getters.
Most getter methods are "safe": if the requested data is missing, they return
a default value (usually 0 or an empty list) and log a warning.
"""
def __init__(self, raw_data, uuid):
"""Create a profile data wrapper.
Args:
raw_data: Full JSON response from Hypixel profile endpoint.
uuid: Minecraft UUID of the requested player (used to select member data).
"""
self._data = raw_data
self._uuid = uuid
self._logger = get_logger(_LOGGER_NAME_)
[docs]
def get_collection(self, collection_name: CollectionKey | str) -> int:
"""Get the amount collected for a specific collection.
Args:
collection_name: Collection key (e.g. "LOG", "WHEAT").
Full list: :class:`~hypixelez.constants.CollectionKey`.
Returns:
Collection amount if present, otherwise 0.
"""
try:
return self._data["profile"]["members"][self._uuid]["collection"][
collection_name
]
except (KeyError, ValueError):
self._logger.warning(f"Collection '{collection_name}' not found")
return 0
[docs]
def get_slayer_stats(self, slayer_name) -> list:
"""Get slayer boss kill statistics by tiers.
Args:
slayer_name: Slayer id (e.g. "zombie", "spider", "wolf", ...).
Returns:
A list of tier kill counts if present, otherwise an empty list.
Notes:
The current implementation iterates over keys inside the slayer dict and
appends values for keys that contain ``"boss_kills_tier"``.
If you need stable ordering (tier1..tierN), consider sorting keys.
"""
try:
stats = list()
for i in self._data["profile"]["members"][self._uuid]["slayer"][
"slayer_bosses"
][slayer_name]:
if "boss_kills_tier" in i:
stats.append(
self._data["profile"]["members"][self._uuid]["slayer"][
"slayer_bosses"
][slayer_name][i]
)
return stats
except (KeyError, ValueError):
self._logger.warning(f"Slayer '{slayer_name}' not found")
return []
[docs]
def get_skill_level(self, skill_name) -> int:
"""Get the current level for a given SkyBlock skill.
Args:
skill_name: Skill XP key (e.g. "SKILL_CARPENTRY").
Returns:
Skill level if present, otherwise 0.
"""
try:
xp = int(
self._data["profile"]["members"][self._uuid]["player_data"][
"experience"
][skill_name]
)
return _calculate_level(xp, _SKILL_CUMULATIVE_LEVELS_) - 1
except (KeyError, ValueError):
self._logger.warning(f"Skill '{skill_name}' not found")
return 0
[docs]
def get_skill_current_level_xp(self, skill_name) -> int:
"""Get XP progress within the current level for a given skill.
Args:
skill_name: Skill XP key (e.g. "SKILL_CARPENTRY").
Returns:
Current level XP progress if present, otherwise 0.
"""
try:
xp = int(
self._data["profile"]["members"][self._uuid]["player_data"][
"experience"
][skill_name]
)
return _calculate_current_xp(xp, _SKILL_CUMULATIVE_LEVELS_)
except (KeyError, ValueError):
self._logger.warning(f"Skill '{skill_name}' not found")
return 0
[docs]
def get_cata_xp(self) -> int:
"""Get Catacombs XP progress within the current Catacombs level.
Returns:
Current Catacombs level XP progress if present, otherwise 0.
"""
try:
xp = int(
self._data["profile"]["members"][self._uuid]["dungeons"][
"dungeon_types"
]["catacombs"]["experience"]
)
return _calculate_current_xp(xp, _CATA_CUMULATIVE_XP_)
except (KeyError, ValueError):
self._logger.warning(f"Catacomb not found")
return 0
[docs]
def get_cata_level(self) -> int:
"""Get the Catacombs level.
Returns:
Catacombs level if present, otherwise 0.
"""
try:
xp = int(
self._data["profile"]["members"][self._uuid]["dungeons"][
"dungeon_types"
]["catacombs"]["experience"]
)
return _calculate_level(xp, _CATA_CUMULATIVE_XP_)
except (KeyError, ValueError):
self._logger.warning(f"Catacomb not found")
return 0
[docs]
def get_cata_class_xp(self, class_name) -> int:
"""Get dungeon class XP progress within the current class level.
Args:
class_name: Dungeon class id (e.g. "mage", "berserk", "archer", "tank", "healer").
Returns:
Current class level XP progress if present, otherwise 0.
"""
try:
xp = int(
self._data["profile"]["members"][self._uuid]["dungeons"][
"player_classes"
][class_name]["experience"]
)
return _calculate_current_xp(xp, _CATA_CUMULATIVE_XP_)
except (KeyError, ValueError):
self._logger.warning(f"Class '{class_name}' not found")
return 0
[docs]
def get_cata_class_level(self, class_name) -> int:
"""Get the dungeon class level.
Args:
class_name: Dungeon class id (e.g. "mage", "berserk", "archer", "tank", "healer").
Returns:
Class level if present, otherwise 0.
"""
try:
xp = int(
self._data["profile"]["members"][self._uuid]["dungeons"][
"player_classes"
][class_name]["experience"]
)
return _calculate_level(xp, _CATA_CUMULATIVE_XP_)
except (KeyError, ValueError):
self._logger.warning(f"Class '{class_name}' not found")
return 0
[docs]
def get_slayer_xp(self, slayer_name) -> int:
"""Get total slayer XP for a specific slayer.
Args:
slayer_name: Slayer id (e.g. "zombie", "spider", "wolf", ...).
Returns:
Total slayer XP if present, otherwise 0.
"""
try:
slayer_data = self._data["profile"]["members"][self._uuid]["slayer"][
"slayer_bosses"
][slayer_name]
return slayer_data.get("xp", 0) # Явно обращаемся к полю xp
except KeyError:
return 0
[docs]
def get_slayer_level(self, slayer_name) -> int:
"""Get the slayer level derived from claimed levels.
Args:
slayer_name: Slayer id (e.g. "zombie", "spider", "wolf", ...).
Returns:
Highest claimed slayer level if present, otherwise 0.
"""
try:
claimed_levels = self._data["profile"]["members"][self._uuid]["slayer"][
"slayer_bosses"
][slayer_name].get("claimed_levels", {})
max_level = 0
for level_key in claimed_levels:
if level_key.startswith("level_"):
try:
level = int(level_key.split("_")[1])
max_level = max(max_level, level)
except (IndexError, ValueError):
continue
return max_level
except (KeyError, ValueError):
self._logger.warning(f"Slayer '{slayer_name}' not found")
return 0
[docs]
def get_slayer_stats_by_tier(self, slayer_name, tier) -> int:
"""Get slayer boss kill count for a specific tier.
Args:
slayer_name: Slayer id (e.g. "zombie", "spider", "wolf", ...).
tier: Tier number (1-indexed). For example, tier=1 means "tier I".
Returns:
Kill count for the requested tier if present, otherwise 0.
Notes:
If `tier` is out of range, the implementation will fall back to 0.
"""
try:
return self.get_slayer_stats(slayer_name)[tier - 1]
except (KeyError, ValueError):
self._logger.warning(f"Slayer '{slayer_name}' with tier '{tier}' not found")
return 0
[docs]
def get_global_level(self) -> int:
"""Get the global SkyBlock level.
Returns:
Global level if present, otherwise 0.
Notes:
The current implementation derives level as ``experience // 100``.
"""
try:
xp = self._data["profile"]["members"][self._uuid]["leveling"]["experience"]
return xp // 100
except (KeyError, ValueError):
self._logger.warning(f"Global level not found")
return 0
[docs]
def get_global_xp(self) -> int:
"""Get XP progress within the current global SkyBlock level.
Returns:
Current global level XP progress if present, otherwise 0.
Notes:
The current implementation derives progress as ``experience % 100``.
"""
try:
xp = self._data["profile"]["members"][self._uuid]["leveling"]["experience"]
return xp % 100
except (KeyError, ValueError):
self._logger.warning(f"Global xp not found")
return 0