Full Code of MetaCubeX/mihomo for AI

main 008b91bfe8c0 cached
30 files
51.4 KB
13.6k tokens
56 symbols
1 requests
Download .txt
Repository: MetaCubeX/mihomo
Branch: main
Commit: 008b91bfe8c0
Files: 30
Total size: 51.4 KB

Directory structure:
gitextract_4b35i8pu/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   ├── bug_report_zh.yml
│   │   ├── config.yml
│   │   ├── feature_request.yml
│   │   └── feature_request_zh.yml
│   └── workflows/
│       ├── Delete.yml
│       └── build.yml
├── .gitignore
├── LICENSE
├── README.md
├── examples/
│   ├── basic.py
│   ├── data_persistence.py
│   └── merge_data.py
├── mihomo/
│   ├── __init__.py
│   ├── client.py
│   ├── errors.py
│   ├── models/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── character.py
│   │   ├── combat.py
│   │   ├── equipment.py
│   │   ├── player.py
│   │   ├── stat.py
│   │   └── v1/
│   │       ├── __init__.py
│   │       ├── base.py
│   │       ├── character.py
│   │       ├── equipment.py
│   │       └── player.py
│   └── tools.py
└── pyproject.toml

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Bug Report
description: "Report  Mihomo bug"
title: "[Bug] "
labels: ["bug"]
body:
  - type: checkboxes
    id: ensure
    attributes:
      label: Verify steps
      description: Before submitting, please check all the options below to confirm that you have read and understood the following requirements; otherwise, this issue will be closed.
      options:
        - label: I have read the [documentation](https://wiki.metacubex.one/) and understand the meaning of all the configuration items I have written, rather than just piling up seemingly useful options or default values.
          required: false
        - label: I have carefully reviewed the [documentation](https://wiki.metacubex.one/) and have not resolved the issue.
          required: false
        - label: I have searched the [Issue Tracker](……/) for the issue I want to raise and did not find it.
          required: false
        - label: I am a non-Chinese user.
          required: false
        - label: I have tested with the latest Alpha branch version, and the issue still persists.
          required: true
        - label: I have provided the server and client configuration files and processes that can reproduce the issue locally, rather than a sanitized complex client configuration file.
          required: true
        - label: I provided the simplest configuration that can be used to reproduce the errors in my report, rather than relying on remote servers or piling on a lot of unnecessary configurations for reproduction.
          required: true
        - label: I have provided complete logs, rather than just the parts I think are useful out of confidence in my own intelligence.
          required: true
        - label: I have directly reproduced the error using the Mihomo command-line program, rather than using other tools or scripts.
          required: true

  - type: dropdown
    attributes:
      label: Operating System
      description: "Please provide the type of operating system."
      multiple: true
      options:
        - MacOS
        - Windows
        - Linux
        - OpenBSD/FreeBSD
        - Android
  - type: input
    attributes:
      label: System Version
      description: "Please provide the version of the operating system where the issue occurred."
    validations:
      required: true
  - type: textarea
    attributes:
      label: Mihomo Version
      description: "Provide the output of the `mihomo -v` command."
    validations:
      required: true
  - type: textarea
    attributes:
      render: yaml
      label: Configuration File
      description: |-
        Please attach the Mihomo configuration file below.
        Make sure there is no sensitive information in the configuration file (such as server addresses, passwords, ports, etc.)
        Also, ensure that the configuration file can reproduce the error using the Mihomo command-line program locally (if it's a proxy protocol issue, make sure the local server can be used for reproduction).
    validations:
      required: true
  - type: textarea
    attributes:
      label: Description
      description: "Please provide a detailed description of the error."
    validations:
      required: true
  - type: textarea
    attributes:
      label: Reproduction Steps
      description: "Please provide the steps to reproduce the error."
    validations:
      required: true
  - type: textarea
    attributes:
      label: Logs
      description: "Attach the running logs of Mihomo Core below, with `log-level` set to `DEBUG`."
      render: shell

================================================
FILE: .github/ISSUE_TEMPLATE/bug_report_zh.yml
================================================
name: 错误反馈
description: "提交 Mihomo 漏洞"
title: "[Bug] "
labels: ["bug"]
body:
  - type: checkboxes
    id: ensure
    attributes:
      label: 验证步骤
      description: 在提交之前,请勾选以下选项以证明您已经阅读并理解了以下要求,否则该 issue 将被关闭。
      options:
        - label: 我已经阅读了 [文档](https://wiki.metacubex.one/),了解所有我编写的配置文件项的含义,而不是大量堆砌看似有用的选项或默认值。
          required: false
        - label: 我仔细看过 [文档](https://wiki.metacubex.one/) 并未解决问题
          required: false
        - label: 我已在 [Issue Tracker](……/) 中寻找过我要提出的问题,并且没有找到
          required: false
        - label: 我是中文用户,而非其他语言用户
          required: false
        - label: 我已经使用最新的 Alpha 分支版本测试过,问题依旧存在
          required: true
        - label: 我提供了可以在本地重现该问题的服务器、客户端配置文件与流程,而不是一个脱敏的复杂客户端配置文件。
          required: true
        - label: 我提供了可用于重现我报告的错误的最简配置,而不是依赖远程服务器或者堆砌大量对于复现无用的配置等。
          required: true
        - label: 我提供了完整的日志,而不是出于对自身智力的自信而仅提供了部分认为有用的部分。
          required: true
        - label: 我直接使用 Mihomo 命令行程序重现了错误,而不是使用其他工具或脚本。
          required: true

  - type: dropdown
    attributes:
      label: 操作系统
      description: 请提供操作系统类型
      multiple: true
      options:
        - MacOS
        - Windows
        - Linux
        - OpenBSD/FreeBSD
        - Android
  - type: input
    attributes:
      label: 系统版本
      description: 请提供出现问题的操作系统版本
    validations:
      required: true
  - type: textarea
    attributes:
      label: Mihomo 版本
      description: 提供 `mihomo -v` 命令的输出
    validations:
      required: true
  - type: textarea
    attributes:
      render: yaml
      label: 配置文件
      description: |-
        在下方附上 Mihomo 配置文件
        请确保配置文件中没有敏感信息(比如:服务器地址,密码,端口等)
        以及确保可以在本地(如果是代理协议问题,请确保本地服务器可用于复现)使用 Mihomo 原始命令行程序重现错误的配置文件
    validations:
      required: true
  - type: textarea
    attributes:
      label: 描述
      description: 请提供错误的详细描述。
    validations:
      required: true
  - type: textarea
    attributes:
      label: 重现方式
      description: 请提供重现错误的步骤
    validations:
      required: true
  - type: textarea
    attributes:
      label: 日志
      description: 在下方附上 Mihomo Core 的运行日志,`log-level` 使用 `DEBUG`
      render: shell

================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
  - name: mihomo Community Support
    url: https://github.com/MetaCubeX/mihomo/discussions
    about: Please ask and answer questions about mihomo here.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: Feature Request
description: Suggest improvements for this project
title: "[Feature] "
labels: ["enhancement"]
body:
  - type: checkboxes
    id: ensure
    attributes:
      label: Verification Steps
      description: Before submitting, please check the following options to confirm that you have read and understood the requirements below; otherwise, this issue will be closed.
      options:
        - label: I have read the [documentation](https://wiki.metacubex.one/) and confirmed that this feature is not implemented
          required: false
        - label: I have searched for the feature request I want to propose in the [Issue Tracker](……/) and did not find it
          required: false
        - label: I am a non-Chinese user.
          required: false
  - type: textarea
    attributes:
      label: Description
      description: Please provide a detailed description of the feature, rather than vague statements.
    validations:
      required: true

================================================
FILE: .github/ISSUE_TEMPLATE/feature_request_zh.yml
================================================
name: 功能请求
description: 为该项目提出建议
title: "[Feature] "
labels: ["enhancement"]
body:
  - type: checkboxes
    id: ensure
    attributes:
      label: 验证步骤
      description: 在提交之前,请勾选以下选项以证明您已经阅读并理解了以下要求,否则该 issue 将被关闭。
      options:
        - label: 我已经阅读了 [文档](https://wiki.metacubex.one/),确认了该功能没有实现
          required: false
        - label: 我已在 [Issue Tracker](……/) 中寻找过我要提出的功能请求,并且没有找到
          required: false
        - label: 我是中文用户,而非其他语言用户
          required: false
  - type: textarea
    attributes:
      label: 描述
      description: 请提供对于该功能的详细描述,而不是莫名其妙的话术。
    validations:
      required: true

================================================
FILE: .github/workflows/Delete.yml
================================================
name: Delete old workflow
on:
  workflow_dispatch:
  schedule:
    - cron: '0 0 * * 0'
# Run monthly, at 00:00 on the 1st day of month.

jobs:
  del_runs:
    runs-on: ubuntu-latest
    steps:
      - name: Delete workflow runs
        uses: GitRML/delete-workflow-runs@main
        with:
          token: ${{ secrets.AUTH_PAT }}
          repository: ${{ github.repository }}
          retain_days: 30

================================================
FILE: .github/workflows/build.yml
================================================
name: Build
on:
  workflow_dispatch:
    inputs:
      version:
        description: "Tag version to release"
        required: true

permissions:
  contents: write
  packages: write

jobs:
  release_archive:
    runs-on: ubuntu-latest
    if: github.repository == 'KT-Yeh/mihomo'

    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          submodules: true

      - name: Archive Release
        uses: thedoctor0/zip-release@0.7.1
        with:
          type: zip
          filename: 'mihomo_${{ github.ref_name }}.zip'
          exclusions: '*.git*'

      - name: Release
        uses: softprops/action-gh-release@v1
        with:
          generate_release_notes: true
          files: 'mihomo_${{ github.ref_name }}.zip'

================================================
FILE: .gitignore
================================================
__pycache__
venv
build
mihomo.egg-info
*.ipynb

================================================
FILE: LICENSE
================================================
Copyright 2023 KT

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.

================================================
FILE: README.md
================================================
# 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))
```


================================================
FILE: examples/basic.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())


================================================
FILE: examples/data_persistence.py
================================================
import asyncio
import pickle
import zlib

from mihomo import Language, MihomoAPI, StarrailInfoParsed


async def main():
    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))


asyncio.run(main())


================================================
FILE: examples/merge_data.py
================================================
import asyncio

from mihomo import Language, MihomoAPI, tools


async def main():
    client = MihomoAPI(language=Language.EN)
    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)

    print(data)


asyncio.run(main())


================================================
FILE: mihomo/__init__.py
================================================
from . import tools
from .client import *
from .errors import *
from .models import *


================================================
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/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/__init__.py
================================================
from .base import *
from .character import *
from .combat import *
from .equipment import *
from .player import *
from .stat import *


================================================
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/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/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/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/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/v1/__init__.py
================================================
from .base import *
from .character import *
from .equipment import *
from .player import *


================================================
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"""


================================================
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/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/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/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: pyproject.toml
================================================
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "mihomo"
version = "1.1.7"
authors = [
  { name="KT", email="xns77477@gmail.com" },
]
description = "A simple Python Pydantic model for Honkai: Star Rail parsed data from the Mihomo API."
readme = "README.md"
requires-python = ">=3.10"
classifiers = [
    "Programming Language :: Python :: 3",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
]
dependencies = [
  "aiohttp==3.*",
  "pydantic==1.*",
]
Download .txt
gitextract_4b35i8pu/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   ├── bug_report_zh.yml
│   │   ├── config.yml
│   │   ├── feature_request.yml
│   │   └── feature_request_zh.yml
│   └── workflows/
│       ├── Delete.yml
│       └── build.yml
├── .gitignore
├── LICENSE
├── README.md
├── examples/
│   ├── basic.py
│   ├── data_persistence.py
│   └── merge_data.py
├── mihomo/
│   ├── __init__.py
│   ├── client.py
│   ├── errors.py
│   ├── models/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── character.py
│   │   ├── combat.py
│   │   ├── equipment.py
│   │   ├── player.py
│   │   ├── stat.py
│   │   └── v1/
│   │       ├── __init__.py
│   │       ├── base.py
│   │       ├── character.py
│   │       ├── equipment.py
│   │       └── player.py
│   └── tools.py
└── pyproject.toml
Download .txt
SYMBOL INDEX (56 symbols across 16 files)

FILE: examples/basic.py
  function v1 (line 10) | async def v1():
  function v2 (line 29) | async def v2():

FILE: examples/data_persistence.py
  function main (line 8) | async def main():

FILE: examples/merge_data.py
  function main (line 6) | async def main():

FILE: mihomo/client.py
  class Language (line 12) | class Language(Enum):
  class MihomoAPI (line 28) | class MihomoAPI:
    method __init__ (line 47) | def __init__(self, language: Language = Language.CHT):
    method request (line 50) | async def request(
    method fetch_user (line 97) | async def fetch_user(
    method fetch_user_v1 (line 120) | async def fetch_user_v1(
    method get_icon_url (line 145) | def get_icon_url(self, icon: str) -> str:

FILE: mihomo/errors.py
  class BaseException (line 1) | class BaseException(Exception):
    method __init__ (line 6) | def __init__(self, message: str | None = None, *args: object) -> None:
  class HttpRequestError (line 12) | class HttpRequestError(BaseException):
    method __init__ (line 18) | def __init__(
  class UserNotFound (line 33) | class UserNotFound(BaseException):
  class InvalidParams (line 39) | class InvalidParams(BaseException):

FILE: mihomo/models/base.py
  class StarrailInfoParsed (line 7) | class StarrailInfoParsed(BaseModel):

FILE: mihomo/models/character.py
  class Character (line 10) | class Character(BaseModel):
    method max_level (line 90) | def max_level(self) -> int:

FILE: mihomo/models/combat.py
  class Element (line 4) | class Element(BaseModel):
  class Path (line 25) | class Path(BaseModel):
  class Trace (line 43) | class Trace(BaseModel):
  class TraceTreeNode (line 88) | class TraceTreeNode(BaseModel):

FILE: mihomo/models/equipment.py
  class LightCone (line 7) | class LightCone(BaseModel):
    method max_level (line 53) | def max_level(self) -> int:
  class Relic (line 58) | class Relic(BaseModel):
  class RelicSet (line 94) | class RelicSet(BaseModel):

FILE: mihomo/models/player.py
  class Avatar (line 4) | class Avatar(BaseModel):
  class ForgottenHall (line 12) | class ForgottenHall(BaseModel):
  class Player (line 29) | class Player(BaseModel):
    method decompose_space_info (line 79) | def decompose_space_info(cls, data):
    method transform_for_backward_compatibility (line 87) | def transform_for_backward_compatibility(cls, data):

FILE: mihomo/models/stat.py
  class Attribute (line 4) | class Attribute(BaseModel):
  class Property (line 31) | class Property(BaseModel):
  class MainAffix (line 61) | class MainAffix(Property):
  class SubAffix (line 78) | class SubAffix(MainAffix):

FILE: mihomo/models/v1/base.py
  class StarrailInfoParsedV1 (line 7) | class StarrailInfoParsedV1(BaseModel):

FILE: mihomo/models/v1/character.py
  class EidolonIcon (line 8) | class EidolonIcon(BaseModel):
  class Trace (line 23) | class Trace(BaseModel):
  class Stat (line 44) | class Stat(BaseModel):
  class Character (line 65) | class Character(BaseModel):
    method dict_to_list (line 140) | def dict_to_list(cls, data: dict[str, Any]):
    method icon (line 148) | def icon(self) -> str:

FILE: mihomo/models/v1/equipment.py
  class LightCone (line 4) | class LightCone(BaseModel):
  class RelicProperty (line 23) | class RelicProperty(BaseModel):
  class Relic (line 38) | class Relic(BaseModel):
  class RelicSet (line 59) | class RelicSet(BaseModel):

FILE: mihomo/models/v1/player.py
  class Player (line 4) | class Player(BaseModel):
  class ForgottenHall (line 28) | class ForgottenHall(BaseModel):
  class PlayerSpaceInfo (line 45) | class PlayerSpaceInfo(BaseModel):

FILE: mihomo/tools.py
  function remove_empty_dict (line 12) | def remove_empty_dict(data: RawData) -> RawData:
  function replace_icon_name_with_url (line 31) | def replace_icon_name_with_url(data: RawData) -> RawData:
  function replace_trailblazer_name (line 56) | def replace_trailblazer_name(data: StarrailInfoParsedV1) -> StarrailInfo...
  function remove_duplicate_character (line 72) | def remove_duplicate_character(data: ParsedData) -> ParsedData:
  function merge_character_data (line 92) | def merge_character_data(new_data: ParsedData, old_data: ParsedData) -> ...
Condensed preview — 30 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (58K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "chars": 3554,
    "preview": "name: Bug Report\ndescription: \"Report  Mihomo bug\"\ntitle: \"[Bug] \"\nlabels: [\"bug\"]\nbody:\n  - type: checkboxes\n    id: en"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report_zh.yml",
    "chars": 2114,
    "preview": "name: 错误反馈\ndescription: \"提交 Mihomo 漏洞\"\ntitle: \"[Bug] \"\nlabels: [\"bug\"]\nbody:\n  - type: checkboxes\n    id: ensure\n    att"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 197,
    "preview": "blank_issues_enabled: false\ncontact_links:\n  - name: mihomo Community Support\n    url: https://github.com/MetaCubeX/miho"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "chars": 974,
    "preview": "name: Feature Request\ndescription: Suggest improvements for this project\ntitle: \"[Feature] \"\nlabels: [\"enhancement\"]\nbod"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request_zh.yml",
    "chars": 609,
    "preview": "name: 功能请求\ndescription: 为该项目提出建议\ntitle: \"[Feature] \"\nlabels: [\"enhancement\"]\nbody:\n  - type: checkboxes\n    id: ensure\n "
  },
  {
    "path": ".github/workflows/Delete.yml",
    "chars": 418,
    "preview": "name: Delete old workflow\r\non:\r\n  workflow_dispatch:\r\n  schedule:\r\n    - cron: '0 0 * * 0'\r\n# Run monthly, at 00:00 on t"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 759,
    "preview": "name: Build\non:\n  workflow_dispatch:\n    inputs:\n      version:\n        description: \"Tag version to release\"\n        re"
  },
  {
    "path": ".gitignore",
    "chars": 46,
    "preview": "__pycache__\nvenv\nbuild\nmihomo.egg-info\n*.ipynb"
  },
  {
    "path": "LICENSE",
    "chars": 1041,
    "preview": "Copyright 2023 KT\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and ass"
  },
  {
    "path": "README.md",
    "chars": 3626,
    "preview": "# mihomo\nA simple python pydantic model (type hint and autocompletion support) for Honkai: Star Rail parsed data from th"
  },
  {
    "path": "examples/basic.py",
    "chars": 1576,
    "preview": "import asyncio\n\nfrom mihomo import Language, MihomoAPI\nfrom mihomo.models import StarrailInfoParsed\nfrom mihomo.models.v"
  },
  {
    "path": "examples/data_persistence.py",
    "chars": 618,
    "preview": "import asyncio\nimport pickle\nimport zlib\n\nfrom mihomo import Language, MihomoAPI, StarrailInfoParsed\n\n\nasync def main():"
  },
  {
    "path": "examples/merge_data.py",
    "chars": 400,
    "preview": "import asyncio\n\nfrom mihomo import Language, MihomoAPI, tools\n\n\nasync def main():\n    client = MihomoAPI(language=Langua"
  },
  {
    "path": "mihomo/__init__.py",
    "chars": 86,
    "preview": "from . import tools\nfrom .client import *\nfrom .errors import *\nfrom .models import *\n"
  },
  {
    "path": "mihomo/client.py",
    "chars": 4540,
    "preview": "import typing\nfrom enum import Enum\n\nimport aiohttp\n\nfrom . import tools\nfrom .errors import HttpRequestError, InvalidPa"
  },
  {
    "path": "mihomo/errors.py",
    "chars": 1029,
    "preview": "class BaseException(Exception):\n    \"\"\"Base exception class.\"\"\"\n\n    message: str = \"\"\n\n    def __init__(self, message: "
  },
  {
    "path": "mihomo/models/__init__.py",
    "chars": 134,
    "preview": "from .base import *\nfrom .character import *\nfrom .combat import *\nfrom .equipment import *\nfrom .player import *\nfrom ."
  },
  {
    "path": "mihomo/models/base.py",
    "chars": 423,
    "preview": "from pydantic import BaseModel, Field\n\nfrom .character import Character\nfrom .player import Player\n\n\nclass StarrailInfoP"
  },
  {
    "path": "mihomo/models/character.py",
    "chars": 3464,
    "preview": "from typing import Any\n\nfrom pydantic import BaseModel, Field, root_validator\n\nfrom .combat import Element, Path, Trace,"
  },
  {
    "path": "mihomo/models/combat.py",
    "chars": 3081,
    "preview": "from pydantic import BaseModel\n\n\nclass Element(BaseModel):\n    \"\"\"\n    Represents an element.\n\n    Attributes:\n        -"
  },
  {
    "path": "mihomo/models/equipment.py",
    "chars": 3874,
    "preview": "from pydantic import BaseModel, Field\n\nfrom .combat import Path\nfrom .stat import Attribute, MainAffix, Property, SubAff"
  },
  {
    "path": "mihomo/models/player.py",
    "chars": 3632,
    "preview": "from pydantic import BaseModel, Field, root_validator\n\n\nclass Avatar(BaseModel):\n    \"\"\"Profile picture\"\"\"\n\n    id: int\n"
  },
  {
    "path": "mihomo/models/stat.py",
    "chars": 3029,
    "preview": "from pydantic import BaseModel, Field\n\n\nclass Attribute(BaseModel):\n    \"\"\"\n    Represents an attribute.\n\n    Attributes"
  },
  {
    "path": "mihomo/models/v1/__init__.py",
    "chars": 96,
    "preview": "from .base import *\r\nfrom .character import *\r\nfrom .equipment import *\r\nfrom .player import *\r\n"
  },
  {
    "path": "mihomo/models/v1/base.py",
    "chars": 642,
    "preview": "from pydantic import BaseModel, Field\r\n\r\nfrom .character import Character\r\nfrom .player import Player, PlayerSpaceInfo\r\n"
  },
  {
    "path": "mihomo/models/v1/character.py",
    "chars": 4632,
    "preview": "from typing import Any\r\n\r\nfrom pydantic import BaseModel, Field, root_validator\r\n\r\nfrom .equipment import LightCone, Rel"
  },
  {
    "path": "mihomo/models/v1/equipment.py",
    "chars": 1757,
    "preview": "from pydantic import BaseModel, Field\r\n\r\n\r\nclass LightCone(BaseModel):\r\n    \"\"\"\r\n    Represents a light cone (weapon).\r\n"
  },
  {
    "path": "mihomo/models/v1/player.py",
    "chars": 2314,
    "preview": "from pydantic import BaseModel, Field\r\n\r\n\r\nclass Player(BaseModel):\r\n    \"\"\"\r\n    Player basic info\r\n\r\n    Attributes:\r\n"
  },
  {
    "path": "mihomo/tools.py",
    "chars": 3397,
    "preview": "from typing import Final, TypeVar\n\nfrom .models import Character, StarrailInfoParsed\nfrom .models.v1 import Character, S"
  },
  {
    "path": "pyproject.toml",
    "chars": 545,
    "preview": "[build-system]\nrequires = [\"setuptools>=61.0\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"mihomo\"\nversio"
  }
]

About this extraction

This page contains the full source code of the MetaCubeX/mihomo GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 30 files (51.4 KB), approximately 13.6k tokens, and a symbol index with 56 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!