File: mihomo/client.py

import typing
from enum import Enum

import aiohttp

from . import tools
from .errors import HttpRequestError, InvalidParams, UserNotFound
from .models import StarrailInfoParsed
from .models.v1 import StarrailInfoParsedV1


class Language(Enum):
    CHT = "cht"
    CHS = "cn"
    DE = "de"
    EN = "en"
    ES = "es"
    FR = "fr"
    ID = "id"
    JP = "jp"
    KR = "kr"
    PT = "pt"
    RU = "ru"
    TH = "th"
    VI = "vi"


class MihomoAPI:
    """
    Represents an client for Mihomo API.

    Args:
        language (Language, optional):
            The language to use for API responses.Defaults to Language.CHT.

    Attributes:
        - BASE_URL (str): The base URL of the API.
        - ASSET_URL (str): The base URL for the asset files.

    """

    BASE_URL: typing.Final[str] = "https://api.mihomo.me/sr_info_parsed"
    ASSET_URL: typing.Final[
        str
    ] = "https://raw.githubusercontent.com/Mar-7th/StarRailRes/master"

    def __init__(self, language: Language = Language.CHT):
        self.lang = language

    async def request(
        self,
        uid: int | str,
        language: Language,
        *,
        params: dict[str, str] = {},
    ) -> typing.Any:
        """
        Makes an HTTP request to the API.

        Args:
            - uid (int | str): The user ID.
            - language (Language): The language to use for the API response.

        Returns:
            typing.Any: The response from the API.

        Raises:
            HttpRequestError: If the HTTP request fails.
            InvalidParams: If the API request contains invalid parameters.
            UserNotFound: If the requested user is not found.

        """
        url = self.BASE_URL + "/" + str(uid)
        params.update({"lang": language.value})

        async with aiohttp.ClientSession() as session:
            async with session.get(url, params=params) as response:
                match response.status:
                    case 200:
                        return await response.json(encoding="utf-8")
                    case 400:
                        try:
                            data = await response.json(encoding="utf-8")
                        except:
                            raise InvalidParams()
                        else:
                            if isinstance(data, dict) and (
                                detail := data.get("detail")
                            ):
                                raise InvalidParams(detail)
                            raise InvalidParams()
                    case 404:
                        raise UserNotFound()
                    case _:
                        raise HttpRequestError(response.status, str(response.reason))

    async def fetch_user(
        self,
        uid: int,
        *,
        replace_icon_name_with_url: bool = False,
    ) -> StarrailInfoParsed:
        """
        Fetches user data from the API.

        Args:
            - uid (`int`): The user ID.
            - replace_icon_name_with_url (`bool`): Whether to replace icon names with asset URLs.

        Returns:
            StarrailInfoParsed: The parsed user data from mihomo API.

        """
        data = await self.request(uid, self.lang)
        if replace_icon_name_with_url is True:
            data = tools.replace_icon_name_with_url(data)
        data = StarrailInfoParsed.parse_obj(data)
        return data

    async def fetch_user_v1(
        self,
        uid: int,
        *,
        replace_icon_name_with_url: bool = False,
    ) -> StarrailInfoParsedV1:
        """
        Fetches user data from the API using version 1 format.

        Args:
            - uid (`int`): The user ID.
            - replace_icon_name_with_url (`bool`): Whether to replace icon names with asset URLs.

        Returns:
            StarrailInfoParsedV1: The parsed user data from the Mihomo API (version 1).

        """
        data = await self.request(uid, self.lang, params={"version": "v1"})
        data = tools.remove_empty_dict(data)
        if replace_icon_name_with_url is True:
            data = tools.replace_icon_name_with_url(data)
        data = StarrailInfoParsedV1.parse_obj(data)
        data = tools.replace_trailblazer_name(data)
        return data

    def get_icon_url(self, icon: str) -> str:
        """
        Gets the asset url for the given icon.

        Args:
            icon (str): The icon name.

        Returns:
            str: The asset url for the icon.

        """
        return self.ASSET_URL + "/" + icon



File: mihomo/tools.py

from typing import Final, TypeVar

from .models import Character, StarrailInfoParsed
from .models.v1 import Character, StarrailInfoParsedV1

RawData = TypeVar("RawData")
ParsedData = TypeVar("ParsedData", StarrailInfoParsed, StarrailInfoParsedV1)

ASSET_URL: Final[str] = "https://raw.githubusercontent.com/Mar-7th/StarRailRes/master"


def remove_empty_dict(data: RawData) -> RawData:
    """
    Recursively removes empty dictionaries from the given raw data.

    Args:
        - data (`RawData`): The input raw data.

    Returns:
        - `RawData`: The data with empty dictionaries removed.
    """
    if isinstance(data, dict):
        for key in data.keys():
            data[key] = None if (data[key] == {}) else remove_empty_dict(data[key])
    elif isinstance(data, list):
        for i in range(len(data)):
            data[i] = remove_empty_dict(data[i])
    return data


def replace_icon_name_with_url(data: RawData) -> RawData:
    """
    Replaces icon file names with asset URLs in the given raw data.

    Example: Replace "/icon/avatar/1201.png" with
    "https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/icon/avatar/1201.png"

    Args:
        - data (`RawData`): The input raw data.

    Returns:
        - `RawData`: The data with icon file names replaced by asset URLs.
    """
    if isinstance(data, dict):
        for key in data.keys():
            data[key] = replace_icon_name_with_url(data[key])
    elif isinstance(data, list):
        for i in range(len(data)):
            data[i] = replace_icon_name_with_url(data[i])
    elif isinstance(data, str):
        if ".png" in data:
            data = ASSET_URL + "/" + data
    return data


def replace_trailblazer_name(data: StarrailInfoParsedV1) -> StarrailInfoParsedV1:
    """
    Replaces the trailblazer name with the player's name.

    Args:
        - data (`StarrailInfoParsed`): The input StarrailInfoParsed data.

    Returns:
        - `StarrailInfoParsed`: The updated StarrailInfoParsed data.
    """
    for i in range(len(data.characters)):
        if data.characters[i].name == r"{NICKNAME}":
            data.characters[i].name = data.player.name
    return data


def remove_duplicate_character(data: ParsedData) -> ParsedData:
    """
    Removes duplicate characters from the given StarrailInfoParsed data.

    Args:
        - data (`ParsedData`): The input StarrailInfoParsed data.

    Returns:
        - `ParsedData`: The updated StarrailInfoParsed data without duplicate characters.
    """
    new_characters = []
    characters_ids: set[str] = set()
    for character in data.characters:
        if character.id not in characters_ids:
            new_characters.append(character)
            characters_ids.add(character.id)
    data.characters = new_characters
    return data


def merge_character_data(new_data: ParsedData, old_data: ParsedData) -> ParsedData:
    """
    Append the old data characters to the list of new data characters.
    The player's info from the old data will be omitted/discarded.

    Args:
        - new_data (`ParsedData`): The new data to be merged.
        - old_data (`ParsedData`): The old data to merge into.

    Returns:
        - `ParsedData`: The merged new data.
    """
    for character in old_data.characters:
        new_data.characters.append(character)
    new_data = remove_duplicate_character(new_data)
    return new_data



File: mihomo/__init__.py

from . import tools
from .client import *
from .errors import *
from .models import *



File: mihomo/errors.py

class BaseException(Exception):
    """Base exception class."""

    message: str = ""

    def __init__(self, message: str | None = None, *args: object) -> None:
        if message is not None:
            self.message = message
        super().__init__(self.message, *args)


class HttpRequestError(BaseException):
    """Exception raised when an HTTP request fails."""

    status: int = 0
    reason: str = ""

    def __init__(
        self,
        status: int,
        reason: str,
        message: str | None = None,
        *args: object,
    ) -> None:
        if not message:
            message = f"[{status}] {reason}"
        self.status = status
        self.reason = reason
        self.message = message
        super().__init__(message, *args)


class UserNotFound(BaseException):
    """Exception raised when a user is not found."""

    message = "User not found."


class InvalidParams(BaseException):
    """Exception raised when invalid parameters are provided."""

    message: str = "Invalid parameters"



File: mihomo/models/equipment.py

from pydantic import BaseModel, Field

from .combat import Path
from .stat import Attribute, MainAffix, Property, SubAffix


class LightCone(BaseModel):
    """
    Represents a light cone (weapon).

    Attributes:
        - id (`int`): The ID of the light cone.
        - name (`str`): The name of the light cone.
        - rarity (`int`): The rarity of the light cone.
        - superimpose (`int`): The superimpose rank of the light cone.
        - level (`int`): The current level of the light cone.
        - max_level (`int`): The maximum light cone level according to the current ascension phase.
        - ascension (`int`): The ascension phase of the light cone.
        - icon (`str`): The light cone icon image.
        - preview (`str`): The light cone preview image.
        - portrait (`str`): The light cone portrait image.
        - path (`Path`): The path of the light cone.
        - attributes (list[`Attribute`]): The list of attributes of the light cone.
        - properties (list[`Property`]): The list of properties of the light cone.
    """

    id: int
    """The ID of the light cone"""
    name: str
    """The name of the light cone"""
    rarity: int
    """The rarity of the light cone"""
    superimpose: int = Field(..., alias="rank")
    """The superimpose rank of the light cone"""
    level: int
    """The level of the light cone"""
    ascension: int = Field(..., alias="promotion")
    """The ascension phase of the light cone"""
    icon: str
    """The light cone icon image"""
    preview: str
    """The light cone preview image"""
    portrait: str
    """The light cone portrait image"""
    path: Path
    """The path of the light cone"""
    attributes: list[Attribute]
    """The list of attributes of the light cone"""
    properties: list[Property]
    """The list of properties of the light cone"""

    @property
    def max_level(self) -> int:
        """The maximum light cone level according to the current ascension phase"""
        return 20 + 10 * self.ascension


class Relic(BaseModel):
    """
    Represents a relic.

    Attributes:
        - id (`int`): The ID of the relic.
        - name (`str`): The name of the relic.
        - set_id (`int`): The ID of the relic set.
        - set_name (`str`): The name of the relic set.
        - rarity (`int`): The rarity of the relic.
        - level (`int`): The level of the relic.
        - main_property (`MainAffix`): The main affix of the relic.
        - sub_property (list[`SubAffix`]): The list of sub-affixes of the relic.
        - icon (`str`): The relic icon.
    """

    id: int
    """The ID of the relic"""
    name: str
    """The name of the relic"""
    set_id: int
    """The ID of the relic set"""
    set_name: str
    """The name of the relic set"""
    rarity: int
    """The rarity of the relic"""
    level: int
    """The level of the relic"""
    main_affix: MainAffix
    """The main affix of the relic"""
    sub_affixes: list[SubAffix] = Field([], alias="sub_affix")
    """The list of sub-affixes of the relic"""
    icon: str
    """The relic icon"""


class RelicSet(BaseModel):
    """
    Represents a set of relics.

    Attributes:
        - id (`int`): The ID of the relic set.
        - name (`str`): The name of the relic set.
        - icon (`str`): The icon of the relic set.
        - num (`int`): The number of relics in the set.
        - desc (`str`): The description of the relic set.
        - properties (list[`Property`]): The list of properties of the relic set.
    """

    id: int
    """The ID of the relic set"""
    name: str
    """The name of the relic set"""
    icon: str
    """The icon of the relic set"""
    num: int
    """The number of relics in the set"""
    desc: str
    """The description of the relic set"""
    properties: list[Property]
    """The list of properties of the relic set"""



File: mihomo/models/combat.py

from pydantic import BaseModel


class Element(BaseModel):
    """
    Represents an element.

    Attributes:
        - id (`str`): The ID of the element.
        - name (`str`): The name of the element.
        - color (`str`): The color of the element.
        - icon (`str`): The element icon.
    """

    id: str
    """The ID of the element"""
    name: str
    """The name of the element"""
    color: str
    """The color of the element"""
    icon: str
    """The element icon"""


class Path(BaseModel):
    """
    Paths are congregations of Imaginary energy, with which the ideals harmonize.

    Attributes:
        - id (`str`): The ID of the path.
        - name (`str`): The name of the path.
        - icon (`str`): The path icon.
    """

    id: str
    """The ID of the path"""
    name: str
    """The name of the path"""
    icon: str
    """The path icon"""


class Trace(BaseModel):
    """
    Represents a character's skill trace.

    Attributes:
        - id (`int`): The ID of the trace.
        - name (`str`): The name of the trace.
        - level (`int`): The current level of the trace.
        - max_level (`int`): The maximum level of the trace.
        - element (`Element` | None): The element of the trace, or None if not applicable.
        - type (`str`): The type of the trace.
        - type_text (`str`): The type text of the trace.
        - effect (`str`): The effect of the trace.
        - effect_text (`str`): The effect text of the trace.
        - simple_desc (`str`): The simple description of the trace.
        - desc (`str`): The detailed description of the trace.
        - icon (`str`): The trace icon.
    """

    id: int
    """The ID of the trace"""
    name: str
    """The name of the trace"""
    level: int
    """The current level of the trace"""
    max_level: int
    """The maximum level of the trace"""
    element: Element | None = None
    """The element of the trace"""
    type: str
    """The type of the trace"""
    type_text: str
    """The type text of the trace"""
    effect: str
    """The effect of the trace"""
    effect_text: str
    """The effect text of the trace"""
    simple_desc: str
    """The simple description of the trace"""
    desc: str
    """The detailed description of the trace"""
    icon: str
    """The trace icon"""


class TraceTreeNode(BaseModel):
    """
    Represents a node in the trace skill tree of a character.

    Attributes:
    - id (`int`): The ID of the trace.
    - level (`int`): The level of the trace.
    - max_level (`int`): The max level of the trace.
    - icon (`str`): The icon of the trace.
    - anchor (`str`): The position of the trace tree node.
    - parent (`int` | `None`): The preceding node id of trace.
    """

    id: int
    """The ID of the trace"""
    level: int
    """The level of the trace"""
    max_level: int
    """The max level of the trace"""
    icon: str
    """The icon of the trace"""
    anchor: str
    """The position of the trace tree node"""
    parent: int | None = None
    """The preceding node id of trace"""



File: mihomo/models/__init__.py

from .base import *
from .character import *
from .combat import *
from .equipment import *
from .player import *
from .stat import *



File: mihomo/models/player.py

from pydantic import BaseModel, Field, root_validator


class Avatar(BaseModel):
    """Profile picture"""

    id: int
    name: str
    icon: str


class ForgottenHall(BaseModel):
    """The progress of the Forgotten Hall

    Attributes:
        - memory (`int`): The progress of the memory.
        - memory_of_chaos_id (`int`): The ID of the memory of chaos, or None if not applicable.
        - memory_of_chaos (`int`): The progress of the memory of chaos, or None if not applicable.
    """

    memory: int = Field(..., alias="level")
    """The progress of the memory (level)"""
    memory_of_chaos_id: int = Field(..., alias="chaos_id")
    """The ID of the memory of chaos (chaos_id)"""
    memory_of_chaos: int = Field(..., alias="chaos_level")
    """The progress of the memory of chaos (chaos_level)"""


class Player(BaseModel):
    """
    Player basic info

    Attributes:
        - uid (`int`): The player's uid.
        - name (`str`): The player's nickname.
        - level (`int`): The player's Trailblaze level.
        - world_level (`int`): The player's Equilibrium level.
        - friend_count (`int`): The number of friends.
        - avatar (`Avatar`): The player's profile picture.
        - signature (`str`): The player's bio.
        - is_display (`bool`): Is the player's profile display enabled.

        - forgotten_hall (`ForgottenHall` | None): The progress of the Forgotten Hall, or None if not applicable.
        - simulated_universes (`int`): The number of simulated universes passed.
        - light_cones (`int`): The number of light cones owned.
        - characters (`int`): The number of characters owned.
        - achievements (`int`): The number of achievements unlocked.
    """

    uid: int
    """Player's uid"""
    name: str = Field(..., alias="nickname")
    """Player's nickname"""
    level: int
    """Trailblaze level"""
    world_level: int
    """Equilibrium level"""
    friend_count: int
    """Number of friends"""
    avatar: Avatar
    """Profile picture"""
    signature: str
    """Bio"""
    is_display: bool
    """Is the player's profile display enabled."""

    forgotten_hall: ForgottenHall | None = Field(None, alias="memory_data")
    """The progress of the Forgotten Hall (memory_data)"""
    simulated_universes: int = Field(0, alias="universe_level")
    """Number of simulated universes passed (universe_level)"""
    light_cones: int = Field(0, alias="light_cone_count")
    """Number of light cones owned"""
    characters: int = Field(0, alias="avatar_count")
    """Number of characters owned"""
    achievements: int = Field(0, alias="achievement_count")
    """Number of achievements unlocked"""

    @root_validator(pre=True)
    def decompose_space_info(cls, data):
        if isinstance(data, dict):
            space_info = data.get("space_info")
            if isinstance(space_info, dict):
                data.update(space_info)
        return data

    @root_validator(pre=True)
    def transform_for_backward_compatibility(cls, data):
        if isinstance(data, dict):
            if "pass_area_progress" in data and "universe_level" not in data:
                data["universe_level"] = data["pass_area_progress"]
            if "challenge_data" in data and "memory_data" not in data:
                c: dict[str, int] = data["challenge_data"]
                data["memory_data"] = {}
                data["memory_data"]["level"] = c.get("pre_maze_group_index")
                data["memory_data"]["chaos_id"] = c.get("maze_group_id")
                data["memory_data"]["chaos_level"] = c.get("maze_group_index")
        return data



File: mihomo/models/stat.py

from pydantic import BaseModel, Field


class Attribute(BaseModel):
    """
    Represents an attribute.

    Attributes:
        - field (`str`): The field of the attribute.
        - name (`str`): The name of the attribute.
        - icon (`str`): The attribute icon image.
        - value (`float`): The value of the attribute.
        - displayed_value (`str`): The displayed value of the attribute.
        - is_percent (`bool`): Indicates if the value is in percentage.
    """

    field: str
    """The field of the attribute"""
    name: str
    """The name of the attribute"""
    icon: str
    """The attribute icon image"""
    value: float
    """The value of the attribute"""
    displayed_value: str = Field(..., alias="display")
    """The displayed value of the attribute"""
    is_percent: bool = Field(..., alias="percent")
    """Indicates if the value is in percentage"""


class Property(BaseModel):
    """
    Represents a property.

    Attributes:
        - type (`str`): The type of the property.
        - field (`str`): The field of the property.
        - name (`str`): The name of the property.
        - icon (`str`): The property icon image.
        - value (`float`): The value of the property.
        - displayed_value (`str`): The displayed value of the property.
        - is_percent (`bool`): Indicates if the value is in percentage.
    """

    type: str
    """The type of the property"""
    field: str
    """The field of the property"""
    name: str
    """The name of the property"""
    icon: str
    """The property icon image"""
    value: float
    """The value of the property"""
    displayed_value: str = Field(..., alias="display")
    """The displayed value of the property"""
    is_percent: bool = Field(..., alias="percent")
    """Indicates if the value is in percentage"""


class MainAffix(Property):
    """
    Represents a relic main affix.

    Attributes:
        - type (`str`): The type of the affix.
        - field (`str`): The field of the affix.
        - name (`str`): The name of the affix.
        - icon (`str`): The affix icon image.
        - value (`float`): The value of the affix.
        - displayed_value (`str`): The displayed value of the affix.
        - is_percent (`bool`): Indicates if the value is in percentage.
    """

    ...


class SubAffix(MainAffix):
    """
    Represents a relic sub-affix.

    Attributes:
        - type (`str`): The type of the affix.
        - field (`str`): The field of the affix.
        - name (`str`): The name of the affix.
        - icon (`str`): The affix icon image.
        - value (`float`): The value of the affix.
        - displayed_value (`str`): The displayed value of the affix.
        - is_percent (`bool`): Indicates if the value is in percentage.
        - count (`int`): The upgrade times of the affix.
        - step (`int`): The additional value of the affix.
    """

    count: int
    """The upgrade times of the affix"""
    step: int
    """The additional value of the affix"""



File: mihomo/models/character.py

from typing import Any

from pydantic import BaseModel, Field, root_validator

from .combat import Element, Path, Trace, TraceTreeNode
from .equipment import LightCone, Relic, RelicSet
from .stat import Attribute, Property


class Character(BaseModel):
    """
    Represents a character.

    Attributes:
    - Basic info:
        - id (`str`): The character's ID.
        - name (`str`): The character's name.
        - rarity (`int`): The character's rarity.
        - level (`int`): The character's current level.
        - max_level (`int`): The maximum character level according to the current ascension phase.
        - ascension (`int`): Ascension phase.
        - eidolon (`int`): The character's eidolon rank.
        - eidolon_icons (list[`str`]): The list of character's eiodolon icons.
    - Image
        - icon (`str`): The character avatar image
        - preview (`str`): The character's preview image.
        - portrait (`str`): The character's portrait image.
    - Combat
        - path (`Path`): The character's path.
        - element (`Element`): The character's element.
        - traces (list[`Trace`]): The list of character's skill traces.
        - trace_tree (list[`TraceTreeNode]): The list of the character's skill traces.
    - Equipment
        - light_cone (`LightCone` | `None`): The character's light cone (weapon), or None if not applicable.
        - relics (list[`Relic`] | `None`): The list of character's relics, or None if not applicable.
        - relic_set (list[`RelicSet`] | `None`): The list of character's relic sets, or None if not applicable.
        - stats (list[`Stat`]): The list of character's stats.
    - Stats
        - attributes (list[`Attribute`]): The list of character's attributes.
        - additions (list[`Attribute`]): The list of character's additional attributes.
        - properties (list[`Property`]): The list of character's properties.
    """

    id: str
    """Character's ID"""
    name: str
    """Character's name"""
    rarity: int
    """Character's rarity"""
    level: int
    """Character's level"""
    ascension: int = Field(..., alias="promotion")
    """Ascension phase"""
    eidolon: int = Field(..., alias="rank")
    """Character's eidolon rank"""
    eidolon_icons: list[str] = Field([], alias="rank_icons")
    """The list of character's eiodolon icons"""

    icon: str
    """Character avatar image"""
    preview: str
    """Character preview image"""
    portrait: str
    """Character portrait image"""

    path: Path
    """Character's path"""
    element: Element
    """Character's element"""
    traces: list[Trace] = Field(..., alias="skills")
    """The list of character's skill traces"""
    trace_tree: list[TraceTreeNode] = Field([], alias="skill_trees")
    """The list of the character's skill traces"""

    light_cone: LightCone | None = None
    """Character's light cone (weapon)"""
    relics: list[Relic] = []
    """The list of character's relics"""
    relic_sets: list[RelicSet] = []
    """The list of character's relic sets"""

    attributes: list[Attribute]
    """The list of character's attributes"""
    additions: list[Attribute]
    """The list of character's additional attributes"""
    properties: list[Property]
    """The list of character's properties"""

    @property
    def max_level(self) -> int:
        """The maximum character level according to the current ascension phase"""
        return 20 + 10 * self.ascension



File: mihomo/models/base.py

from pydantic import BaseModel, Field

from .character import Character
from .player import Player


class StarrailInfoParsed(BaseModel):
    """
    Mihomo parsed data

    Attributes:
        - player (`Player`): The player's info.
        - characters (list[`Character`]): The list of characters.
    """

    player: Player
    """Player's basic info"""
    characters: list[Character]
    """The list of characters"""



File: mihomo/models/v1/equipment.py

from pydantic import BaseModel, Field


class LightCone(BaseModel):
    """
    Represents a light cone (weapon).

    Attributes:
        - name (`str`): The name of the light cone.
        - rarity (`int`): The rarity of the light cone.
        - superimpose (`int`): The superimpose rank of the light cone.
        - level (`int`): The level of the light cone.
        - icon (`str`): The light cone icon.
    """

    name: str
    rarity: int
    superimpose: int = Field(..., alias="rank")
    level: int
    icon: str


class RelicProperty(BaseModel):
    """
    Represents a property of a relic.

    Attributes:
        - name (`str`): The name of the relic property.
        - value (`str`): The value of the relic property.
        - icon (`str`): The property icon.
    """

    name: str
    value: str
    icon: str


class Relic(BaseModel):
    """
    Represents a relic.

    Attributes:
        - name (`str`): The name of the relic.
        - rarity (`int`): The rarity of the relic.
        - level (`int`): The level of the relic.
        - main_property (`RelicProperty`): The main property of the relic.
        - sub_property (list[`RelicProperty`]): The list of sub properties of the relic.
        - icon (`str`): The relic icon.
    """

    name: str
    rarity: int
    level: int
    main_property: RelicProperty
    sub_property: list[RelicProperty]
    icon: str


class RelicSet(BaseModel):
    """
    Represents a set of relics.

    Attributes:
        - name (`str`): The name of the relic set.
        - icon (`str`): The relic set icon.
        - desc (`int`): The description of the relic set.
    """

    name: str
    icon: str
    desc: int



File: mihomo/models/v1/__init__.py

from .base import *
from .character import *
from .equipment import *
from .player import *



File: mihomo/models/v1/player.py

from pydantic import BaseModel, Field


class Player(BaseModel):
    """
    Player basic info

    Attributes:
        - uid (`str`): The player's uid.
        - name (`str`): The player's nickname.
        - level (`int`): The player's Trailblaze level.
        - icon (`str`): The player's profile picture.
        - signature (`str`): The player's bio.
    """

    uid: str
    """Player's uid"""
    name: str
    """Player's nickname"""
    level: int
    """Trailblaze level"""
    icon: str
    """Profile picture"""
    signature: str
    """Bio"""


class ForgottenHall(BaseModel):
    """The progress of the Forgotten Hall

    Attributes:
        - memory (`int`): The progress of the memory.
        - memory_of_chaos_id (`int` | `None`): The ID of the memory of chaos, or None if not applicable.
        - memory_of_chaos (`int` | `None`): The progress of the memory of chaos, or None if not applicable.
    """

    memory: int | None = Field(None, alias="PreMazeGroupIndex")
    """The progress of the memory"""
    memory_of_chaos_id: int | None = Field(None, alias="MazeGroupIndex")
    """The ID of the memory of chaos"""
    memory_of_chaos: int | None = Field(None, alias="MazeGroupID")
    """The progress of the memory of chaos"""


class PlayerSpaceInfo(BaseModel):
    """Player details

    Attributes:
        - forgotten_hall (`ForgottenHall` | None): The progress of the Forgotten Hall, or None if not applicable.
        - simulated_universes (`int`): The number of simulated universes passed.
        - light_cones (`int`): The number of light cones owned.
        - characters (`int`): The number of characters owned.
        - achievements (`int`): The number of achievements unlocked.
    """

    forgotten_hall: ForgottenHall | None = Field(None, alias="ChallengeData")
    """The progress of the Forgotten Hall"""
    simulated_universes: int = Field(0, alias="PassAreaProgress")
    """Number of simulated universes passed"""
    light_cones: int = Field(0, alias="LightConeCount")
    """Number of light cones owned"""
    characters: int = Field(0, alias="AvatarCount")
    """Number of characters owned"""
    achievements: int = Field(0, alias="AchievementCount")
    """Number of achievements unlocked"""



File: mihomo/models/v1/character.py

from typing import Any

from pydantic import BaseModel, Field, root_validator

from .equipment import LightCone, Relic, RelicSet


class EidolonIcon(BaseModel):
    """
    Represents an Eidolon icon.

    Attributes:
        - icon (`str`): The eidolon icon.
        - unlock (`bool`): Indicates if the eidolon is unlocked.
    """

    icon: str
    """The eidolon icon"""
    unlock: bool
    """Indicates if the eidolon is unlocked"""


class Trace(BaseModel):
    """
    Represents a character's skill trace.

    Attributes:
        - name (`str`): The name of the trace.
        - level (`int`): The level of the trace.
        - type (`str`): The type of the trace.
        - icon (`str`): The trace icon.
    """

    name: str
    """The name of the trace"""
    level: int
    """The level of the trace"""
    type: str
    """The type of the trace"""
    icon: str
    """The trace icon"""


class Stat(BaseModel):
    """
    Represents a character's stat.

    Attributes:
        - name (`str`): The name of the stat.
        - base (`str`): The base value of the stat.
        - addition (`str` | `None`): The additional value of the stat, or None if not applicable.
        - icon (`str`): The stat icon.
    """

    name: str
    """The name of the stat"""
    base: str
    """The base value of the stat"""
    addition: str | None = None
    """The additional value of the stat"""
    icon: str
    """The stat icon"""


class Character(BaseModel):
    """
    Represents a character.

    Attributes:
    - Basic info:
        - id (`str`): The character's ID.
        - name (`str`): The character's name.
        - rarity (`int`): The character's rarity.
        - level (`int`): The character's level.
    - Eidolon
        - eidolon (`int`): The character's eidolon rank.
        - eidolon_icons (list[`EidolonIcon`]): The list of eidolon icons.
    - Image
        - icon (`str`): The character avatar image
        - preview (`str`): The character's preview image.
        - portrait (`str`): The character's portrait image.
    - Combat type
        - path (`str`): The character's path.
        - path_icon (`str`): The character's path icon.
        - element (`str`): The character's element.
        - element_icon (`str`): The character's element icon.
        - color (`str`): The character's element color.
    - Equipment
        - traces (list[`Trace`]): The list of character's skill traces.
        - light_cone (`LightCone` | `None`): The character's light cone (weapon), or None if not applicable.
        - relics (list[`Relic`] | `None`): The list of character's relics, or None if not applicable.
        - relic_set (list[`RelicSet`] | `None`): The list of character's relic sets, or None if not applicable.
        - stats (list[`Stat`]): The list of character's stats.
    """

    id: str
    """Character's ID"""
    name: str
    """Character's name"""
    rarity: int
    """Character's rarity"""
    level: int
    """Character's level"""

    eidolon: int = Field(..., alias="rank")
    """Character's eidolon rank"""
    eidolon_icons: list[EidolonIcon] = Field(..., alias="rank_icons")
    """The list of eidolon icons"""

    preview: str
    """Character preview image"""
    portrait: str
    """Character portrait image"""

    path: str
    """Character's path"""
    path_icon: str
    """Character's path icon"""

    element: str
    """Character's element"""
    element_icon: str
    """Character's element icon"""

    color: str
    """Character's element color"""

    traces: list[Trace] = Field(..., alias="skill")
    """The list of character's skill traces"""
    light_cone: LightCone | None = None
    """Character's light cone (weapon)"""
    relics: list[Relic] | None = Field(None, alias="relic")
    """The list of character's relics"""
    relic_set: list[RelicSet] | None = None
    """The list of character's relic sets"""
    stats: list[Stat] = Field(..., alias="property")
    """The list of character's stats"""

    @root_validator(pre=True)
    def dict_to_list(cls, data: dict[str, Any]):
        # The keys of the original dict is not necessary, so remove them here.
        if isinstance(data, dict) and data.get("relic") is not None:
            if isinstance(data["relic"], dict):
                data["relic"] = list(data["relic"].values())
        return data

    @property
    def icon(self) -> str:
        """Character avatar image"""
        return f"icon/character/{self.id}.png"



File: mihomo/models/v1/base.py

from pydantic import BaseModel, Field

from .character import Character
from .player import Player, PlayerSpaceInfo


class StarrailInfoParsedV1(BaseModel):
    """
    Mihomo parsed data V1

    Attributes:
        - player (`Player`): The player's basic info.
        - player_details (`PlayerSpaceInfo`): The player's details.
        - characters (list[`Character`]): The list of characters.
    """

    player: Player
    """Player's basic info"""
    player_details: PlayerSpaceInfo = Field(..., alias="PlayerSpaceInfo")
    """Player's details"""
    characters: list[Character]
    """The list of characters"""



# mihomo
A simple python pydantic model (type hint and autocompletion support) for Honkai: Star Rail parsed data from the Mihomo API.

API url: https://api.mihomo.me/sr_info_parsed/{UID}?lang={LANG}

## Installation
```
pip install -U git+https://github.com/KT-Yeh/mihomo.git
```

## Usage

### Basic
There are two parsed data formats:
- V1:
  - URL: https://api.mihomo.me/sr_info_parsed/800333171?lang=en&version=v1
  - Fetching: use `client.fetch_user_v1(800333171)`
  - Data model: `mihomo.models.v1.StarrailInfoParsedV1`
  - All models defined in `mihomo/models/v1` directory.
- V2: 
  - URL: https://api.mihomo.me/sr_info_parsed/800333171?lang=en
  - Fetching: use `client.fetch_user(800333171)`
  - Data model: `mihomo.models.StarrailInfoParsed`
  - All models defined in `mihomo/models` directory.

If you don't want to use `client.get_icon_url` to get the image url everytime, you can use `client.fetch_user(800333171, replace_icon_name_with_url=True)` to get the parsed data with asset urls.

### Example
```py
import asyncio

from mihomo import Language, MihomoAPI
from mihomo.models import StarrailInfoParsed
from mihomo.models.v1 import StarrailInfoParsedV1

client = MihomoAPI(language=Language.EN)


async def v1():
    data: StarrailInfoParsedV1 = await client.fetch_user_v1(800333171)

    print(f"Name: {data.player.name}")
    print(f"Level: {data.player.level}")
    print(f"Signature: {data.player.signature}")
    print(f"Achievements: {data.player_details.achievements}")
    print(f"Characters count: {data.player_details.characters}")
    print(f"Profile picture url: {client.get_icon_url(data.player.icon)}")
    for character in data.characters:
        print("-----------")
        print(f"Name: {character.name}")
        print(f"Rarity: {character.rarity}")
        print(f"Level: {character.level}")
        print(f"Avatar url: {client.get_icon_url(character.icon)}")
        print(f"Preview url: {client.get_icon_url(character.preview)}")
        print(f"Portrait url: {client.get_icon_url(character.portrait)}")


async def v2():
    data: StarrailInfoParsed = await client.fetch_user(800333171, replace_icon_name_with_url=True)

    print(f"Name: {data.player.name}")
    print(f"Level: {data.player.level}")
    print(f"Signature: {data.player.signature}")
    print(f"Profile picture url: {data.player.avatar.icon}")
    for character in data.characters:
        print("-----------")
        print(f"Name: {character.name}")
        print(f"Rarity: {character.rarity}")
        print(f"Portrait url: {character.portrait}")

asyncio.run(v1())
asyncio.run(v2())
```

### Tools
`from mihomo import tools`
#### Remove Duplicate Character
```py
    data = await client.fetch_user(800333171)
    data = tools.remove_duplicate_character(data)
```

#### Merge Character Data
```py
    old_data = await client.fetch_user(800333171)

    # Change characters in game and wait for the API to refresh
    # ...

    new_data = await client.fetch_user(800333171)
    data = tools.merge_character_data(new_data, old_data)
```

### Data Persistence
Take pickle and json as an example
```py
import pickle
import zlib
from mihomo import MihomoAPI, Language, StarrailInfoParsed

client = MihomoAPI(language=Language.EN)
data = await client.fetch_user(800333171)

# Save
pickle_data = zlib.compress(pickle.dumps(data))
print(len(pickle_data))
json_data = data.json(by_alias=True, ensure_ascii=False)
print(len(json_data))

# Load
data_from_pickle = pickle.loads(zlib.decompress(pickle_data))
data_from_json = StarrailInfoParsed.parse_raw(json_data)
print(type(data_from_pickle))
print(type(data_from_json))
```
