[
  {
    "path": ".coveragerc",
    "content": "[report]\nexclude_lines =\n    pragma: no cover\n    ((t|typing)\\.)?TYPE_CHECKING\n    ^\\s\\.\\.\\.\\s$\n    def __repr__\n    class .*\\bProtocol\\):\n    @(abc\\.)?abstractmethod\n    raise NotImplementedError\n    if __name__ == \"__main__\":\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "# Devops & CI workflows\n.github/dependabot.yml      @ItsDrike\n.github/workflows/**        @ItsDrike\n.pre-commit-config.yaml     @ItsDrike\n"
  },
  {
    "path": ".github/renovate.json5",
    "content": "{\n  \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n  \"extends\": [\n    \":automergeMinor\",\n    \":automergePr\",\n    \":combinePatchMinorReleases\",\n    \":configMigration\",\n    \":dependencyDashboard\",\n    \":ignoreModulesAndTests\",\n    \":prHourlyLimitNone\",\n    \":semanticCommitsDisabled\",\n    \"group:allNonMajor\",\n    \"mergeConfidence:all-badges\",\n    \"replacements:all\",\n    \"schedule:daily\",\n    \"workarounds:all\",\n  ],\n\n  \"labels\": [\"a: dependencies\"],\n  \"packageRules\": [\n    {\n      \"groupName\": \"GitHub Actions\",\n      \"matchManagers\": [\"github-actions\"],\n      \"addLabels\": [\"a: devops\"],\n    },\n    {\n      \"groupName\": \"Python Dependencies\",\n      \"matchCategories\": [\"python\"],\n    },\n  ],\n  \"lockFileMaintenance\": {\n    \"enabled\": true,\n  },\n}\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: CI\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n  workflow_dispatch:\n\n# Cancel already running workflows if new ones are scheduled\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  validation:\n    uses: ./.github/workflows/validation.yml\n\n  unit-tests:\n    uses: ./.github/workflows/unit-tests.yml\n\n  # Produce a pull request payload artifact with various data about the\n  # pull-request event (such as the PR number, title, author, ...).\n  # This data is then be picked up by status-embed.yml action.\n  pr_artifact:\n    name: Produce Pull Request payload artifact\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Prepare Pull Request Payload artifact\n        id: prepare-artifact\n        if: always() && github.event_name == 'pull_request'\n        continue-on-error: true\n        run: cat $GITHUB_EVENT_PATH | jq '.pull_request' > pull_request_payload.json\n\n      - name: Upload a Build Artifact\n        if: always() && steps.prepare-artifact.outcome == 'success'\n        continue-on-error: true\n        uses: actions/upload-artifact@v7\n        with:\n          name: pull-request-payload\n          path: pull_request_payload.json\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "---\nname: Publish to PyPI\n\non:\n  push:\n    tags:\n      # This pattern is not a typical regular expression, see:\n      # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet\n      - \"v*\"\n    branches:\n      # Also run on every commit to master. This allows us to test the build & release pipeline and eventually leads to a\n      # Test PyPI release. Unlike with a tag push, this will not release a full PyPI release, nor create a GitHub release.\n      - master\n\npermissions:\n  contents: read\n\nenv:\n  PYTHON_VERSION: \"3.14\"\n\njobs:\n  build:\n    name: \"Build the project\"\n    runs-on: ubuntu-latest\n\n    outputs:\n      version: ${{ steps.check-version.outputs.version }}\n      tagged_release: ${{ steps.check-version.outputs.tagged_release }}\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n        with:\n          # Do a full clone for uv-dynamic-versioning to pick up the git version\n          fetch-depth: 0\n\n      - name: Setup uv\n        uses: astral-sh/setup-uv@v7\n        with:\n          version: \"latest\"\n          python-version: ${{ env.PYTHON_VERSION }}\n          activate-environment: true\n          enable-cache: true\n          cache-suffix: \"build\"\n\n      - name: Install dependencies\n        run: |\n          uv sync --no-default-groups --group release\n\n      - name: Check version status\n        id: check-version\n        run: |\n          version=\"$(hatchling version)\"\n\n          echo \"Project version: $version\"\n          echo \"version=$version\" >> \"$GITHUB_OUTPUT\"\n\n          # Determine whether we're doing a tagged release e.g. this workflow\n          # was triggered by a git tag ref that matches the project's current\n          # version, so a full PyPI release should be made, alongside all of\n          # the other release steps. If this isn't the case, only a Test PyPI\n          # release will be performed.\n          if [[ \"${GITHUB_REF}\" == \"refs/tags/v${version}\" ]]; then\n            echo \"This is a new tagged release\"\n            echo \"tagged_release=true\" >> \"$GITHUB_OUTPUT\"\n          else\n            echo \"This is an untagged dev release\"\n            echo \"tagged_release=false\" >> \"$GITHUB_OUTPUT\"\n          fi\n\n      - name: Build project for distribution\n        run: uv build --all-packages\n\n      - name: Upload build files\n        uses: actions/upload-artifact@v7\n        with:\n          name: \"dist\"\n          path: \"dist/\"\n          if-no-files-found: error\n          retention-days: 5\n\n  publish-test-pypi:\n    name: \"Publish to Test PyPI\"\n    # No if condition here, publish both tagged and untagged releases to Test PyPI.\n    needs: build\n    runs-on: ubuntu-latest\n    environment: test-pypi # no approval\n    permissions:\n      # Used to authenticate to Test PyPI via OIDC.\n      id-token: write\n\n    steps:\n      - name: Download the distribution files from build artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: \"dist\"\n          path: \"dist/\"\n\n      - name: Upload to Test PyPI\n        uses: pypa/gh-action-pypi-publish@release/v1\n        with:\n          # the \"legacy\" in the URL doesn't mean it's deprecated\n          repository-url: https://test.pypi.org/legacy/\n          # Enable verbose mode for easy debugging\n          verbose: true\n\n  publish-pypi:\n    name: \"Publish to PyPI\"\n    if: needs.build.outputs.tagged_release == 'true' # only publish to PyPI on tagged releases\n    needs: build\n    runs-on: ubuntu-latest\n    environment: release # requires approval\n    permissions:\n      # Used to authenticate to PyPI via OIDC.\n      id-token: write\n\n    steps:\n      - name: Download the distribution files from build artifact\n        uses: actions/download-artifact@v8\n        with:\n          name: \"dist\"\n          path: \"dist/\"\n\n      # This uses PyPI's trusted publishing, so no token is required\n      - name: Release to PyPI\n        uses: pypa/gh-action-pypi-publish@release/v1\n"
  },
  {
    "path": ".github/workflows/status_embed.yml",
    "content": "name: Status Embed\n\non:\n  workflow_run:\n    workflows:\n      - CI\n    types:\n      - completed\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  status_embed:\n    name: Send Status Embed to Discord\n    runs-on: ubuntu-latest\n    steps:\n      # A workflow_run event does not contain all the information\n      # we need for a PR embed. That's why we upload an artifact\n      # with that information in the Lint workflow.\n      - name: Get Pull Request Information\n        id: pr_info\n        if: github.event.workflow_run.event == 'pull_request'\n        run: |\n          curl -s -H \"Authorization: token $GITHUB_TOKEN\" ${{ github.event.workflow_run.artifacts_url }} > artifacts.json\n          DOWNLOAD_URL=$(cat artifacts.json | jq -r '.artifacts[] | select(.name == \"pull-request-payload\") | .archive_download_url')\n          [ -z \"$DOWNLOAD_URL\" ] && exit 1\n          curl -sSL -H \"Authorization: token $GITHUB_TOKEN\" -o pull_request_payload.zip $DOWNLOAD_URL || exit 2\n          unzip -p pull_request_payload.zip > pull_request_payload.json\n          [ -s pull_request_payload.json ] || exit 3\n          echo \"pr_author_login=$(jq -r '.user.login // empty' pull_request_payload.json)\" >> $GITHUB_OUTPUT\n          echo \"pr_number=$(jq -r '.number // empty' pull_request_payload.json)\" >> $GITHUB_OUTPUT\n          echo \"pr_title=$(jq -r '.title // empty' pull_request_payload.json)\" >> $GITHUB_OUTPUT\n          echo \"pr_source=$(jq -r '.head.label // empty' pull_request_payload.json)\" >> $GITHUB_OUTPUT\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      # Send an informational status embed to Discord instead of the\n      # standard embeds that Discord sends. This embed will contain\n      # more information and we can fine tune when we actually want\n      # to send an embed.\n      - name: GitHub Actions Status Embed for Discord\n        uses: SebastiaanZ/github-status-embed-for-discord@v0.3.0\n        with:\n          # Our GitHub Actions webhook\n          webhook_id: \"942940470059892796\"\n          webhook_token: ${{ secrets.webhook_token }}\n\n          # We need to provide the information of the workflow that\n          # triggered this workflow instead of this workflow.\n          workflow_name: ${{ github.event.workflow_run.name }}\n          run_id: ${{ github.event.workflow_run.id }}\n          run_number: ${{ github.event.workflow_run.run_number }}\n          status: ${{ github.event.workflow_run.conclusion }}\n          sha: ${{ github.event.workflow_run.head_sha }}\n\n          # Now we can use the information extracted in the previous step:\n          pr_author_login: ${{ steps.pr_info.outputs.pr_author_login }}\n          pr_number: ${{ steps.pr_info.outputs.pr_number }}\n          pr_title: ${{ steps.pr_info.outputs.pr_title }}\n          pr_source: ${{ steps.pr_info.outputs.pr_source }}\n"
  },
  {
    "path": ".github/workflows/unit-tests.yml",
    "content": "name: Unit-Tests\n\non: workflow_call\n\njobs:\n  unit-tests:\n    runs-on: ${{ matrix.platform }}\n\n    strategy:\n      fail-fast: false # Allows for matrix sub-jobs to fail without cancelling the rest\n      matrix:\n        platform: [ubuntu-latest, macos-latest, windows-latest]\n        python-version: [\"3.10\", \"3.14\"]\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Setup uv\n        uses: astral-sh/setup-uv@v7\n        with:\n          version: \"latest\"\n          python-version: ${{ matrix.python-version }}\n          enable-cache: true\n          cache-suffix: \"test-ci\"\n          activate-environment: true\n\n      - name: Install dependencies\n        run: |\n          uv sync --no-default-groups --group test\n\n      - name: Run pytest\n        shell: bash\n        run: pytest -vv\n\n  # This job is used purely to provide a workflow status, which we can mark as a\n  # required action in branch protection rules. This is a better option than marking\n  # the tox-test jobs manually, since their names change as the supported python\n  # versions change. This job provides an easy single action that can be marked required.\n  tests-done:\n    needs: [unit-tests]\n    if: always() && !cancelled()\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Set status based on required jobs\n        env:\n          RESULTS: ${{ join(needs.*.result, ' ') }}\n        run: |\n          for result in $RESULTS; do\n            if [ \"$result\" != \"success\" ]; then\n              exit 1\n            fi\n          done\n"
  },
  {
    "path": ".github/workflows/validation.yml",
    "content": "name: Validation\n\non: workflow_call\n\nenv:\n  PYTHON_VERSION: \"3.14\"\n  PRE_COMMIT_HOME: \"/home/runner/.cache/pre-commit\"\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Setup uv\n        uses: astral-sh/setup-uv@v7\n        with:\n          version: \"latest\"\n          python-version: ${{ env.PYTHON_VERSION }}\n          enable-cache: true\n          cache-suffix: \"validation-ci\"\n          activate-environment: true\n\n      - name: Install dependencies\n        run: |\n          # We need the test & docs groups to allow pyright to type-check the code in tests/ & docs/\n          uv sync --no-default-groups --group lint --group test --group docs\n\n      - name: Get precommit version\n        id: precommit_version\n        run: |\n          PACKAGE_VERSION=$(pip show pre-commit | grep -i \"version:\" | awk '{print $2}')\n          echo \"version=$PACKAGE_VERSION\" >> $GITHUB_ENV\n\n      - name: Pre-commit Environment Caching\n        uses: actions/cache@v5\n        with:\n          path: ${{ env.PRE_COMMIT_HOME }}\n          key:\n            \"precommit-${{ runner.os }}-${{ env.PYTHON_VERSION }}-${{ steps.precommit_version.outputs.version }}-\\\n            ${{ hashFiles('./.pre-commit-config.yaml') }}\"\n          # Restore keys allows us to perform a cache restore even if the full cache key wasn't matched.\n          # That way we still end up saving new cache, but we can still make use of the cache from previous\n          # version.\n          restore-keys: \"precommit-${{ runner.os }}-${{ env.PYTHON_VERSION }}-\"\n\n      - name: Run pre-commit hooks\n        run: SKIP=black,isort,ruff,pyright,uv-lockfile pre-commit run --all-files\n\n      - name: Run ruff linter\n        run: ruff check --output-format=github --show-fixes --exit-non-zero-on-fix .\n\n      - name: Run ruff formatter\n        run: ruff format --diff .\n\n      - name: Run pyright type checker\n        run: pyright .\n\n      - name: Check UV Lockfile\n        run: uv lock --check\n"
  },
  {
    "path": ".gitignore",
    "content": ".python-version\n\n# Created by http://www.gitignore.io\n\n### Python ###\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nvenv/\nbuild/\ndevelop-eggs/\ndist/\neggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Pyenv local version specification\n.python-version\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.coverage*\n!.coveragerc\n.cache\nnosetests.xml\ncoverage.xml\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n\n### PyCharm ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm\n\n## Directory-based project format\n.idea/\n/*.iml\n# if you remove the above rule, at least ignore user-specific stuff:\n# .idea/workspace.xml\n# .idea/tasks.xml\n# .idea/dictionaries\n# and these sensitive or high-churn files:\n# .idea/dataSources.ids\n# .idea/dataSources.xml\n# .idea/sqlDataSources.xml\n# .idea/dynamic.xml\n# and, if using gradle::\n# .idea/gradle.xml\n# .idea/libraries\n\n## File-based project format\n*.ipr\n*.iws\n\n## Additional for IntelliJ\nout/\n\n# generated by mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# generated by JIRA plugin\natlassian-ide-plugin.xml\n\n# generated by Crashlytics plugin (for Android Studio and Intellij)\ncom_crashlytics_export_strings.xml\n\n\n### SublimeText ###\n# workspace files are user-specific\n*.sublime-workspace\n\n# project files should be checked into the repository, unless a significant\n# proportion of contributors will probably not be using SublimeText\n# *.sublime-project\n\n#sftp configuration file\nsftp-config.json\n\n### Visual Studio Code ###\n.vscode\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "ci:\n  autofix_commit_msg: \"[pre-commit.ci] auto fixes from pre-commit.com hooks\"\n  autofix_prs: true\n  autoupdate_commit_msg: \"[pre-commit.ci] pre-commit autoupdate\"\n  autoupdate_schedule: weekly\n  submodules: false\n\nrepos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v4.5.0\n    hooks:\n      - id: check-merge-conflict\n      - id: check-toml # For pyproject.toml\n      - id: check-yaml # For workflows\n      - id: end-of-file-fixer\n      - id: trailing-whitespace\n        args: [--markdown-linebreak-ext=md]\n      - id: end-of-file-fixer\n      - id: mixed-line-ending\n      - id: sort-simple-yaml\n\n  - repo: local\n    hooks:\n      - id: ruff\n        name: Ruff Linter\n        description: Run ruff checks on the code\n        entry: ruff check --force-exclude\n        language: system\n        types: [python]\n        require_serial: true\n        args: [--fix, --exit-non-zero-on-fix]\n\n  - repo: local\n    hooks:\n      - id: ruff-ruff\n        name: Ruff Formatter\n        description: Ruf ruff auto-formatter\n        entry: ruff format\n        language: system\n        types: [python]\n        require_serial: true\n\n  - repo: local\n    hooks:\n      - id: pyright\n        name: Pyright\n        description: Run pyright type checker\n        entry: pyright\n        language: system\n        types: [python]\n        pass_filenames: false # pyright runs for the entire project, it can't run for single files\n\n  - repo: local\n    hooks:\n      - id: uv-lockfile\n        name: UV Lockfile\n        description: Check if the UV lockfile is up to date with pyproject.toml\n        entry: uv lock --check\n        language: system\n        files: '^pyproject\\.toml$|^uv\\.lock$'\n        pass_filenames: false\n"
  },
  {
    "path": ".readthedocs.yaml",
    "content": "version: 2\n\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.13\"\n\n  jobs:\n    post_create_environment:\n      - python -m pip install uv\n    post_install:\n      - UV_PROJECT_ENVIRONMENT=$READTHEDOCS_VIRTUALENV_PATH uv sync --all-extras --group docs --link-mode=copy\n\nsphinx:\n  builder: dirhtml\n  configuration: \"docs/conf.py\"\n  fail_on_warning: true\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "## Contributing\n\nSee [the documentation page](https://mcstatus.readthedocs.io/en/stable/pages/contributing/).\nThe documentation itself is built from the docs/ directory.\n"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# <img src=\"https://i.imgur.com/nPCcxts.png\" height=\"25\" style=\"height: 25px\"> MCStatus\n\n[![discord chat](https://img.shields.io/discord/936788458939224094.svg?logo=Discord)](https://discord.gg/C2wX7zduxC)\n![supported python versions](https://img.shields.io/pypi/pyversions/mcstatus.svg)\n[![current PyPI version](https://img.shields.io/pypi/v/mcstatus.svg)](https://pypi.org/project/mcstatus/)\n[![Docs](https://img.shields.io/readthedocs/mcstatus?label=Docs)](https://mcstatus.readthedocs.io/)\n[![CI status](https://github.com/py-mine/mcstatus/actions/workflows/main.yml/badge.svg)](https://github.com/py-mine/mcstatus/actions/workflows/main.yml)\n\nMcstatus provides an API and command line script to fetch publicly available data from Minecraft servers. Specifically, mcstatus retrieves data by using these protocols: [Server List Ping](https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping) and [Query](https://minecraft.wiki/w/Query). Because of mcstatus, you do not need to fully understand those protocols and can instead skip straight to retrieving minecraft server data quickly in your own programs.\n\n## Installation\n\nMcstatus is available on [PyPI](https://pypi.org/project/mcstatus/), and can be installed trivially with:\n\n```bash\npython3 -m pip install mcstatus\n```\n\n## Usage\n\n### Python API\n\n#### Java Edition (1.7+)\n\n```python\nfrom mcstatus import JavaServer\n\n# You can pass the same address you'd enter into the address field in minecraft into the 'lookup' function\n# If you know the host and port, you may skip this and use JavaServer(\"example.org\", 1234)\nserver = JavaServer.lookup(\"example.org:1234\")\n\n# 'status' is supported by all Minecraft servers that are version 1.7 or higher.\n# Don't expect the player list to always be complete, because many servers run\n# plugins that hide this information or limit the number of players returned or even\n# alter this list to contain fake players for purposes of having a custom message here.\nstatus = server.status()\nprint(f\"The server has {status.players.online} player(s) online and replied in {status.latency} ms\")\n\n# 'ping' is supported by all Minecraft servers that are version 1.7 or higher.\n# It is included in a 'status' call, but is also exposed separate if you do not require the additional info.\nlatency = server.ping()\nprint(f\"The server replied in {latency} ms\")\n\n# 'query' has to be enabled in a server's server.properties file!\n# It may give more information than a ping, such as a full player list or mod information.\nquery = server.query()\nprint(f\"The server has the following players online: {', '.join(query.players.names)}\")\n```\n\n#### Java Edition (Beta 1.8-1.6)\n\n```python\nfrom mcstatus import LegacyServer\n\n# You can pass the same address you'd enter into the address field in minecraft into the 'lookup' function\n# If you know the host and port, you may skip this and use LegacyServer(\"example.org\", 1234)\nserver = LegacyServer.lookup(\"example.org:1234\")\n\n# 'status' is supported by all Minecraft servers.\nstatus = server.status()\nprint(f\"The server has {status.players.online} player(s) online and replied in {status.latency} ms\")\n```\n\n#### Bedrock Edition\n\n```python\nfrom mcstatus import BedrockServer\n\n# You can pass the same address you'd enter into the address field in minecraft into the 'lookup' function\n# If you know the host and port, you may skip this and use BedrockServer(\"example.org\", 19132)\nserver = BedrockServer.lookup(\"example.org:19132\")\n\n# 'status' is the only feature that is supported by Bedrock at this time.\n# In this case status includes players.online, latency, motd, map, gamemode, and players.max. (ex: status.gamemode)\nstatus = server.status()\nprint(f\"The server has {status.players.online} players online and replied in {status.latency} ms\")\n```\n\nSee the [documentation](https://mcstatus.readthedocs.io) to find what you can do with our library!\n\n### Command Line Interface\n\nThe mcstatus library includes a simple CLI. Once installed, it can be used through:\n\n```bash\npython3 -m mcstatus --help\n```\n\n## License\n\nMcstatus is licensed under the Apache 2.0 license. See LICENSE for full text.\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the environment for the first two.\nSPHINXOPTS    ?=\nSPHINXBUILD   ?= sphinx-build\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n"
  },
  {
    "path": "docs/api/basic.rst",
    "content": "Basic Usage\n===========\n\nWe are small package, so our API is not so big. There are only few classes, which are suggested for a basic usage.\n\n\nRequest Classes\n---------------\n\nThese are classes, that you use to send a request to server.\n\n\n.. autoclass:: mcstatus.server.MCServer\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. autoclass:: mcstatus.server.BaseJavaServer\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. autoclass:: mcstatus.server.JavaServer\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. autoclass:: mcstatus.server.LegacyServer\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. autoclass:: mcstatus.server.BedrockServer\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n\nResponse Objects\n----------------\n\nThese are the classes that you get back after making a request.\n\nFor Java Server (1.7+)\n**********************\n\n.. module:: mcstatus.responses.java\n\n.. autoclass:: mcstatus.responses.java.JavaStatusResponse()\n    :members:\n    :undoc-members:\n    :inherited-members:\n    :exclude-members: build\n\n.. autoclass:: mcstatus.responses.java.JavaStatusPlayers()\n    :members:\n    :undoc-members:\n    :inherited-members:\n    :exclude-members: build\n\n.. autoclass:: mcstatus.responses.java.JavaStatusPlayer()\n    :members:\n    :undoc-members:\n    :inherited-members:\n    :exclude-members: build\n\n.. autoclass:: mcstatus.responses.java.JavaStatusVersion()\n    :members:\n    :undoc-members:\n    :inherited-members:\n    :exclude-members: build\n\n.. module:: mcstatus.responses.query\n    :no-index:\n\n.. autoclass:: mcstatus.responses.query.QueryResponse()\n    :members:\n    :undoc-members:\n    :inherited-members:\n    :exclude-members: build\n\n.. autoclass:: mcstatus.responses.query.QueryPlayers()\n    :members:\n    :undoc-members:\n    :inherited-members:\n    :exclude-members: build\n\n.. autoclass:: mcstatus.responses.query.QuerySoftware()\n    :members:\n    :undoc-members:\n    :inherited-members:\n    :exclude-members: build\n\nForge Data\n**********\n\nForge mod metadata is available on :attr:`status.forge_data <mcstatus.responses.java.JavaStatusResponse.forge_data>`.\n\n.. module:: mcstatus.responses.forge\n    :no-index:\n\n.. autoclass:: mcstatus.responses.forge.ForgeData()\n    :members:\n    :undoc-members:\n    :inherited-members:\n    :exclude-members: build\n\n.. autoclass:: mcstatus.responses.forge.ForgeDataChannel()\n    :members:\n    :undoc-members:\n    :inherited-members:\n    :exclude-members: build, decode\n\n.. autoclass:: mcstatus.responses.forge.ForgeDataMod()\n    :members:\n    :undoc-members:\n    :inherited-members:\n    :exclude-members: build, decode\n\nFor Java Server (Beta 1.8-1.6)\n******************************\n\n.. versionadded:: 12.1.0\n\n.. versionadded:: 13.0.0\n   Support for Beta 1.8+ (before was 1.4+)\n\n.. module:: mcstatus.responses.legacy\n    :no-index:\n\n.. autoclass:: mcstatus.responses.legacy.LegacyStatusResponse()\n    :members:\n    :undoc-members:\n    :inherited-members:\n    :exclude-members: build\n\n.. autoclass:: mcstatus.responses.legacy.LegacyStatusPlayers()\n    :members:\n    :undoc-members:\n    :inherited-members:\n    :exclude-members: build\n\n.. autoclass:: mcstatus.responses.legacy.LegacyStatusVersion()\n    :members:\n    :undoc-members:\n    :inherited-members:\n    :exclude-members: build\n\n\nFor Bedrock Servers\n*******************\n\n.. module:: mcstatus.responses.bedrock\n    :no-index:\n\n.. autoclass:: mcstatus.responses.bedrock.BedrockStatusResponse()\n    :members:\n    :undoc-members:\n    :inherited-members:\n    :exclude-members: build\n\n.. autoclass:: mcstatus.responses.bedrock.BedrockStatusPlayers()\n    :members:\n    :undoc-members:\n    :inherited-members:\n    :exclude-members: build\n\n.. autoclass:: mcstatus.responses.bedrock.BedrockStatusVersion()\n    :members:\n    :undoc-members:\n    :inherited-members:\n    :exclude-members: build\n\n\nConclusion\n----------\n\nThat is all! See also our :doc:`examples </examples/examples>`!\n"
  },
  {
    "path": "docs/api/internal.rst",
    "content": "Internal Data\n=============\n\nThis page contains some internal objects, classes, functions, etc. These **are not a part of the Public API** and\nyou **should not use them**, as we do not guarantee their backwards compatibility between different library\nversions. They are only documented here for linkable reference to them.\n\n\n.. autoclass:: mcstatus._protocol.java_client.JavaClient\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. autoclass:: mcstatus._protocol.java_client.AsyncJavaClient\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. autoclass:: mcstatus._protocol.legacy_client.LegacyClient\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. autoclass:: mcstatus._protocol.legacy_client.AsyncLegacyClient\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. autoclass:: mcstatus._protocol.bedrock_client.BedrockClient\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: mcstatus._net.address\n    :members:\n    :exclude-members: Address\n    :undoc-members:\n    :show-inheritance:\n\n    .. autoclass:: Address\n        :members:\n        :undoc-members:\n        :show-inheritance:\n\n        .. attribute:: host\n            :type: str\n            :canonical: mcstatus._net.address.Address.host\n\n            The hostname or IP address of the server.\n\n        .. attribute:: port\n            :type: int\n            :canonical: mcstatus._net.address.Address.port\n\n            The port of the server.\n\n.. automodule:: mcstatus._net.dns\n   :members:\n   :undoc-members:\n   :show-inheritance:\n\n.. autoclass:: mcstatus.responses.base.BaseStatusResponse\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. autoclass:: mcstatus.responses.base.BaseStatusPlayers\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. autoclass:: mcstatus.responses.base.BaseStatusVersion\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "docs/api/motd_parsing.rst",
    "content": "MOTD Parsing\n============\n\nWe provide a really powerful system to parse servers MOTDs.\n\n\nThe main class\n--------------\n\nFirstly there is the main class, which you get directly from :meth:`status <mcstatus.server.MCStatus.status>` methods.\n\n.. autoclass:: mcstatus.motd.Motd\n    :members:\n    :undoc-members:\n\n\nComponents\n----------\n\nThose are used in :attr:`~mcstatus.motd.Motd.parsed` field.\n\n.. automodule:: mcstatus.motd.components\n    :members:\n    :undoc-members:\n\n    .. py:type:: ParsedMotdComponent\n      :canonical: Formatting | MinecraftColor | WebColor | TranslationTag | str\n"
  },
  {
    "path": "docs/conf.py",
    "content": "\"\"\"Configuration file for the Sphinx documentation builder.\n\nThis file does only contain a selection of the most common options. For a\nfull list see the documentation:\nhttp://www.sphinx-doc.org/en/master/config\n\"\"\"\n\n# -- Path setup --------------------------------------------------------------\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n\nfrom __future__ import annotations\n\nimport os\nimport sys\nfrom importlib.metadata import version as importlib_version\nfrom typing import TYPE_CHECKING\n\nfrom packaging.version import Version, parse as parse_version\n\nif TYPE_CHECKING:\n    from typing_extensions import override\nelse:\n    override = lambda f: f  # noqa: E731\n\nsys.path.insert(0, os.path.abspath(\"..\"))  # noqa: PTH100\n\n\n# -- Project information -----------------------------------------------------\n\n\ndef _get_version() -> Version:\n    return parse_version(importlib_version(\"mcstatus\"))\n\n\nproject = \"mcstatus\"\ncopyright = \"mcstatus, py-mine\"\nauthor = \"Dinnerbone\"\n\nparsed_version = _get_version()\n\n# The short X.Y version\nversion = parsed_version.base_version\n# The full version, including alpha/beta/rc tags\nrelease = str(parsed_version)\n\n\n# -- General configuration ---------------------------------------------------\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named \"sphinx.ext.*\") or your custom\n# ones.\nextensions = [\n    \"sphinx.ext.autodoc\",\n    \"sphinx.ext.doctest\",\n    \"sphinx.ext.todo\",\n    \"sphinx.ext.coverage\",\n    \"sphinx.ext.viewcode\",\n    \"sphinx.ext.autosummary\",\n    \"sphinx.ext.autosectionlabel\",\n    # Used to reference for third party projects:\n    \"sphinx.ext.intersphinx\",\n    # Used to include .md files:\n    \"m2r2\",\n]\n\nautoclass_content = \"both\"\nautodoc_member_order = \"bysource\"\n\nautodoc_default_flags = {\n    \"members\": \"\",\n    \"undoc-members\": \"code,error_template\",\n    \"exclude-members\": \"__dict__,__weakref__\",\n}\n\n# Automatically generate section labels:\nautosectionlabel_prefix_document = True\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n\nsource_suffix = [\".rst\", \".md\"]\n\n# The master toctree document.\nmaster_doc = \"index\"\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = \"en\"\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This pattern also affects html_static_path and html_extra_path.\nexclude_patterns = [\"_build\", \"Thumbs.db\", \".DS_Store\"]\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = \"sphinx\"\n\nadd_module_names = False\n\n# -- Options for HTML output -------------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\nhtml_theme = \"furo\"\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\nhtml_theme_options = {\n    \"navigation_with_keys\": True,\n}\n\n# -- Extension configuration -------------------------------------------------\n\n# Third-party projects documentation references:\nintersphinx_mapping = {\n    \"python\": (\"https://docs.python.org/3\", None),\n    \"dns\": (\"https://dnspython.readthedocs.io/en/stable/\", None),\n}\n\n\n# -- Options for todo extension ----------------------------------------------\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = True\n\n\n# Mocks\ndef mock_autodoc() -> None:\n    \"\"\"Mock autodoc to not add ``Bases: object`` to the classes, that do not have super classes.\n\n    See also https://stackoverflow.com/a/75041544/20952782.\n    \"\"\"\n    from sphinx.ext import autodoc  # noqa: PLC0415\n\n    class MockedClassDocumenter(autodoc.ClassDocumenter):\n        @override\n        def add_line(self, line: str, source: str, *lineno: int) -> None:\n            if line == \"   Bases: :py:class:`object`\":\n                return\n            super().add_line(line, source, *lineno)\n\n    autodoc.ClassDocumenter = MockedClassDocumenter\n\n\ndef delete_doc_for_address_base() -> None:\n    \"\"\"``__new__`` is appended to the docstring of ``AddressBase``.\n\n    And we do not want autogenerated nonsense docstring there.\n    \"\"\"  # noqa: D401 # imperative mood\n    from mcstatus._net.address import _AddressBase  # noqa: PLC0415\n\n    del _AddressBase.__new__.__doc__\n\n\nmock_autodoc()\ndelete_doc_for_address_base()\n"
  },
  {
    "path": "docs/examples/code/ping_as_java_and_bedrock_in_one_time.py",
    "content": "from __future__ import annotations\n\nimport asyncio\n\nfrom mcstatus import BedrockServer, JavaServer\nfrom mcstatus.responses.bedrock import BedrockStatusResponse\nfrom mcstatus.responses.java import JavaStatusResponse\n\n\nasync def status(host: str) -> JavaStatusResponse | BedrockStatusResponse:\n    \"\"\"Get status from server, which can be Java or Bedrock.\n\n    The function will ping server as Java and as Bedrock in one time, and return the first response.\n    \"\"\"\n    success_task = await handle_exceptions(\n        *(\n            await asyncio.wait(\n                {\n                    asyncio.create_task(handle_java(host), name=\"Get status as Java\"),\n                    asyncio.create_task(handle_bedrock(host), name=\"Get status as Bedrock\"),\n                },\n                return_when=asyncio.FIRST_COMPLETED,\n            )\n        )\n    )\n\n    if success_task is None:\n        raise ValueError(\"No tasks were successful. Is server offline?\")\n\n    return success_task.result()\n\n\nasync def handle_exceptions(done: set[asyncio.Task], pending: set[asyncio.Task]) -> asyncio.Task | None:\n    \"\"\"Handle exceptions from tasks.\n\n    Also, cancel all pending tasks, if found the correct one.\n    \"\"\"\n    if len(done) == 0:\n        raise ValueError(\"No tasks was given to `done` set.\")\n\n    for i, task in enumerate(done):\n        if task.exception() is not None:\n            if len(pending) == 0:\n                continue\n\n            if i == len(done) - 1:  # firstly check all items from `done` set, and then handle pending set\n                return await handle_exceptions(*(await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)))\n        else:\n            for pending_task in pending:\n                pending_task.cancel()\n            return task\n    return None\n\n\nasync def handle_java(host: str) -> JavaStatusResponse:\n    \"\"\"Wrap mcstatus, to compress lookup and status into one function.\"\"\"\n    return await (await JavaServer.async_lookup(host)).async_status()\n\n\nasync def handle_bedrock(host: str) -> BedrockStatusResponse:\n    \"\"\"Wrap mcstatus, to compress lookup and status into one function.\"\"\"\n    # note: `BedrockServer` doesn't have `async_lookup` method, see it's docstring\n    return await BedrockServer.lookup(host).async_status()\n"
  },
  {
    "path": "docs/examples/code/ping_many_servers_at_once.py",
    "content": "import asyncio\n\nfrom mcstatus import JavaServer\n\n\nasync def ping_server(ip: str) -> None:\n    try:\n        status = await (await JavaServer.async_lookup(ip)).async_status()\n    except Exception:\n        return\n\n    print(f\"{ip} - {status.latency}ms\")  # handle somehow responses here\n\n\nasync def ping_ips(ips: list[str]) -> None:\n    to_process: list[str] = []\n\n    for ip in ips:\n        if len(to_process) <= 10:  # 10 means here how many servers will be pinged at once\n            to_process.append(ip)\n            continue\n\n        await asyncio.wait({asyncio.create_task(ping_server(ip_to_ping)) for ip_to_ping in to_process})\n        to_process = []\n\n\ndef main() -> None:\n    ips = [\"hypixel.net\", \"play.hivemc.com\", \"play.cubecraft.net\", ...]  # insert here your ips!\n    asyncio.run(ping_ips(ips))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "docs/examples/code/player_list_from_query_with_fallback_on_status.py",
    "content": "from mcstatus import JavaServer\n\nserver = JavaServer.lookup(\"play.hypixel.net\")\nquery = server.query()\n\nif query.players.list:\n    print(\"Players online:\", \", \".join(query.players.list))\nelse:\n    status = server.status()\n\n    if not status.players.sample:\n        print(\"Cant find players list, no one online or the server disabled this.\")\n    else:\n        print(\"Players online:\", \", \".join([player.name for player in status.players.sample]))\n"
  },
  {
    "path": "docs/examples/examples.rst",
    "content": "Examples\n========\n\nWe have these examples at the moment:\n\n\n.. toctree::\n\t:maxdepth: 1\n\t:caption: Examples\n\n\tping_as_java_and_bedrock_in_one_time.rst\n\tping_many_servers_at_once.rst\n\tplayer_list_from_query_with_fallback_on_status.rst\n\n\nFeel free to propose us more examples, we will be happy to add them to the list!\n"
  },
  {
    "path": "docs/examples/ping_as_java_and_bedrock_in_one_time.rst",
    "content": "Ping as Java and as Bedrock in one time\n=======================================\n\nYou can easily ping a server as a Java server and as a Bedrock server in one time.\n\n.. literalinclude:: code/ping_as_java_and_bedrock_in_one_time.py\n\nAs you can see in the code, ``status`` function returns\n:class:`~mcstatus.responses.java.JavaStatusResponse` or\n:class:`~mcstatus.responses.bedrock.BedrockStatusResponse` object. You can use\n:func:`isinstance` checks to access attributes that are only in one of the\nobjects.\n\n.. code-block:: python\n\n    response = await status(\"hypixel.net\")\n\n    if isinstance(response, BedrockStatusResponse):\n        map_name = response.map_name\n    else:\n        map_name = None\n    # or\n    map_name = response.map_name if isinstance(response, BedrockStatusResponse) else None\n\n    print(f\"Server map name is: {map_name}\")\n"
  },
  {
    "path": "docs/examples/ping_many_servers_at_once.rst",
    "content": "Ping many servers at once\n=========================\n\nYou can ping many servers at once with mcstatus async methods, just look at\n\n.. literalinclude:: code/ping_many_servers_at_once.py\n"
  },
  {
    "path": "docs/examples/player_list_from_query_with_fallback_on_status.rst",
    "content": "Get player list from query, while falling back on status\n========================================================\n\n.. literalinclude:: code/player_list_from_query_with_fallback_on_status.py\n"
  },
  {
    "path": "docs/index.rst",
    "content": ".. mdinclude:: ../README.md\n\nContent\n-------\n\n.. toctree::\n\t:maxdepth: 1\n\t:caption: Pages\n\n\tpages/faq.rst\n\tpages/contributing.rst\n\tpages/versioning.rst\n\texamples/examples.rst\n\n.. toctree::\n\t:maxdepth: 1\n\t:caption: API Documentation\n\n\tapi/basic.rst\n\tapi/motd_parsing.rst\n\tapi/internal.rst\n\n\nIndices and tables\n------------------\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "docs/pages/contributing.rst",
    "content": "Contributing\n============\n\nSetup\n-----\n\n.. code-block:: sh\n\n   pipx install uv\n   uv sync\n\n   # The following command will depend on your operating system and shell.\n   # For MacOS/Linux:\n   . .venv/bin/activate\n   # For Windows CMD:\n   .venv\\Scripts\\activate.bat\n   # For Windows PowerShell:\n   .venv\\Scripts\\Activate.ps1\n\n   pre-commit install\n\nIn addition to this, you may also want to install ruff and pyright (pylance) plugins for your IDE.\n\nExpectations\n------------\n\nWhen making changes to code that results in new behavior, it is expected that\nautomated tests are submitted as well to prevent the behavior from breaking in\nthe future. It matters not whether those changes are for a bugfix or a new\nfeature; all behavior changes require tests.\n\nFeel free to hop on the `Discord server <https://discord.gg/C2wX7zduxC>`_ to\nchat with other maintainers if you are unsure about something. We would\ndefinitely rather have a conversation first before consuming unnecessary time\nfor something that someone else could already be working on or any other reason\nthat might make the work unnecessary. It is a community effort to maintain and\ngrow mcstatus. Much discussion happens on the `Discord server\n<https://discord.gg/C2wX7zduxC>`_ to collaborate ideas together.\n\nOnce you have all the checks passing and any new behavior changes are tested,\nfeel free to open a pull request. Pull requests are how GitHub allows forks to\nsubmit branches for consideration to be merged into the original repo.\n\nCommon development tasks\n------------------------\n\n.. code-block:: sh\n\n   # Activating the virtual environment, allowing you to work with the project's dependencies\n   # installed there by uv. This command is OS and shell dependent.\n   # For MacOS/Linux:\n   . .venv/bin/activate\n   # For Windows CMD:\n   .venv\\Scripts\\activate.bat\n   # For Windows PowerShell:\n   .venv\\Scripts\\Activate.ps1\n\n   poe docs  # Renders documentation from docs/ folder\n   poe format  # Executes automatic formatter for style consistency\n   poe lint  # Executes linting tools that help increase code quality\n   poe test  # Executes unit tests\n\nListing available tasks\n-----------------------\n\n.. code-block:: sh\n\n   poe\n\nBeing fancy with tasks\n----------------------\n\nYou may pass extra arguments to the underlying tasks. Here's an example that\ntells the underlying ``pytest`` to execute only the query tests with maximum\nverbosity.\n\n.. code-block:: sh\n\n   poe test -vvv -k TestQuery\n"
  },
  {
    "path": "docs/pages/faq.rst",
    "content": "Frequently Asked Questions\n==========================\n\n\nWhy doesn't :class:`~mcstatus.server.BedrockServer` have an async :meth:`~mcstatus.server.MCServer.lookup` method?\n------------------------------------------------------------------------------------------------------------------\n\nWith Java servers, to find the server, we sometimes end up performing an SRV\nDNS lookup. This means making a request to your DNS server and waiting for an\nanswer, making that lookup a blocking operation (during which other things can\nbe done).\n\n.. note::\n    An SRV record allows the server to have an address like: ``hypixel.net``,\n    that points to a some specified IP/Host and port, depending on this record.\n\n    That way, even if the server is hosted on a non-standard port (other than\n    25565, say 8855), you won't need to use ``myserver.com:8855``, since the\n    port number will simply be stored in the SRV record, so people can still\n    connect simply with ``myserver.com``.\n\n    On top of that, it also allows to specify a different IP/Host, which means\n    you don't need to use the same server to run both the website, and the\n    minecraft server. Instead, the SRV record can simply point to a different\n    IP/Host address (like ``mc.hypixel.net``, or ``209.222.114.112``).\n\nHowever with Bedrock servers, no such lookups are required (Bedrock doesn't\nsupport SRV records), and so there is no blocking I/O operation being made,\nthat would justify having an async version.\n\nIn fact, all that the bedrock lookup does is parsing the ``host:port`` address,\nand obtaining the ``host`` and ``port`` parts out of it (with some error\nhandling, and support for default ports).\n\n\nIncorrect encoding\n------------------\n\nIn Query protocol, Minecraft uses ISO 8859-1 for encoding all text (like MOTD,\nserver name, etc.). This can cause problems with non-latin characters. To fix\nsuch error, you can re-encode text into UTF-8.\n\n.. code-block:: python\n\n    >>> query = JavaServer.lookup(\"my-server-ip.com\").query()\n    >>> query.motd.to_minecraft()\n    'Ð\\x9fÑ\\x80Ð¸Ð²Ñ\\x96Ñ\\x82!'\n    >>> query.motd.to_minecraft().encode(\"iso-8859-1\").decode(\"utf-8\")\n    'Привіт!'\n\n:attr:`query.motd <mcstatus.responses.query.QueryResponse.motd>` here can be\nanything, that contains incorrect encoding.\n\n\nHow to get server image?\n------------------------\n\nOn Bedrock, only official servers have a server image. There is no way to get\nor set an icon to a custom server. For Java servers, you can use\n:attr:`status.icon <mcstatus.responses.java.JavaStatusResponse.icon>`\nattribute. It will return `Base64 <https://en.wikipedia.org/wiki/Base64>`_\nencoded PNG image. If you wish to save this image into a file, this is how:\n\n.. code-block:: python\n\n    import base64\n    from mcstatus import JavaServer\n\n    server = JavaServer.lookup(\"hypixel.net\")\n    status = server.status()\n\n    decoded_icon = base64.b64decode(status.icon.removeprefix(\"data:image/png;base64,\"))\n    with open(\"server-icon.png\", \"wb\") as f:\n        f.write(decoded_icon)\n\n.. note::\n    Most modern browsers support simply pasting the raw Base64 image into the\n    URL bar, which will open it as an image preview, allowing you to take a\n    quick look at it without having to use file saving from Python.\n\n    See `How to display Base64 image <https://stackoverflow.com/questions/8499633>`_\n    and `Base64 Images: Support table <https://caniuse.com/atob-btoa>`_.\n"
  },
  {
    "path": "docs/pages/versioning.rst",
    "content": "Versioning Practices & Guarantees\n=================================\n\nThis page explains what you can expect when upgrading mcstatus, and what\nchanges may occur in major, minor, and patch releases.\n\nmcstatus follows the `Semantic Versioning <https://semver.org>`_ model in terms\nof **versioning guarantees and expectations**, using the familiar\n``MAJOR.MINOR.PATCH`` structure.\n\nInternally and for distribution, mcstatus version numbers follow `PEP 440\n<https://peps.python.org/pep-0440/>`_. This primarily affects the exact format\nof pre-releases, post-releases, and development releases, but does not change\nthe meaning of major, minor, or patch version increments.\n\n- **MAJOR**: incompatible (breaking) changes to the public API\n- **MINOR**: backwards-compatible features and improvements\n- **PATCH**: backwards-compatible bug fixes\n\nWhat is \"public API\"?\n---------------------\n\nFor mcstatus, the **public API** is defined by what is **documented in the\npublic API pages**.\n\n- Anything documented under :doc:`/api/basic` and :doc:`/api/motd_parsing` is\n  public API.\n- Anything documented under :doc:`/api/internal` is **not** public API.\n- Anything not documented at all is also **not** public API.\n- Any module, package, attribute, or symbol whose name starts with an\n  underscore (``_``) is considered internal and may change at any time.\n\nRelease types and guarantees\n----------------------------\n\n.. warning::\n\n   **Bug fixes are generally not backported.**\n\n   mcstatus primarily supports the **latest released version** of the library.\n   Bugs are fixed only in that version, and fixes are not backported to older\n   releases.\n\n   This includes most bug fixes and the vast majority of security-related\n   fixes. Backporting fixes significantly increases maintenance overhead and\n   often requires maintaining multiple diverging code paths, which is not\n   sustainable for this project.\n\n   In exceptional cases, a truly critical issue may be addressed via a hotfix\n   release. Such cases are rare and handled on a best-effort basis. If you rely\n   on older versions of mcstatus, you may encounter bugs that will not be fixed\n   unless you upgrade.\n\nPatch releases (x.y.PATCH)\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nPatch releases contain:\n\n- bug fixes\n- documentation changes of any kind (e.g. new docs pages, or also changes to\n  docstrings of some public API objects in the codebase)\n- improvements that do not change the public API contract\n- breaking changes in private API\n- dependency updates (including major dependency updates), as long as the\n  public contract of this library remains compatible\n- changes in library's public API typing behavior (see\n  :ref:`typing-stability-guarantees`)\n\nPatch releases do **not** contain breaking changes to the public API. They also\ngenerally don't introduce any new behavior, other than for purposes of\nresolving existing bugs, or internal updates.\n\nNote that in some cases, if you are relying on behavior which we consider to be\na bug, it is possible that we might end up changing this behavior in a `PATCH`\nrelease, in an effort to fix the unintentional, wrong behavior, breaking your\ndependency. Bug-fixes pretty much always happen without any deprecations.\n\n.. admonition:: Example\n\n  To understand what constitutes a bug-fix with such breakage potential, as an\n  example, if mcstatus incorrectly parses a MOTD format that some server sends,\n  fixing that parsing is a bug fix, even if it changes the output format for\n  that specific broken MOTD.\n\nAnother important note to mention is that mcstatus will not make any efforts to\ndelay its runtime dependency version updates to align them with minor or major\nreleases. Transitive breakages caused by dependency updates are considered\nacceptable as long as mcstatus's documented public API remains compatible.\n\nMinor releases (x.MINOR.z)\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nGenerally, minor releases exist to introduce new features to the library in a\nnon-breaking way. They may include:\n\n- new public features (new classes, functions, parameters, constants) without\n  affecting existing behavior\n- new optional arguments in public functions/class constructors with sensible\n  defaults that don't change existing usage\n- **new deprecations** (introduced as warnings containing new replacements /\n  deprecation reasons)\n- backwards-compatible behavior improvements\n- dropping support for a Python version (e.g. dropping Python 3.9 because it is\n  past its `end-of-life <https://devguide.python.org/versions/>`_)\n- any additional changes that patch releases can contain\n\n\nMinor releases do **not** intentionally introduce breaking changes to the\ndocumented public API.\n\nDeprecations in minor releases\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nNew deprecations may be introduced in minor releases for behavior that is\nscheduled for removal in the next major version. These deprecations are emitted\nas warnings and do not immediately break existing code. For more information\nabout our deprecation handling, see the :ref:`deprecations-and-removals`\nsection.\n\nMajor releases (MAJOR.y.z)\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nMajor releases may include breaking changes to the documented public API. These\nwill be called out in the changelog and typically include one or more of:\n\n- removing a documented public class, function, or attribute\n- renaming a public API without a deprecated alias\n- changing the default value or meaning of a function parameter in a way that\n  changes original usage\n- removing a deprecated alias or deprecated import path\n- any additional changes that minor and patch releases can contain\n\nWe generally try to avoid immediate breaking changes that didn't go through at\nleast a brief deprecation period of at least 1 release cycle (e.g. deprecation\nwill first be introduced in v12.4.0, before the old behavior is removed in\nv13).\n\nIf a major refactor of the library was performed, or just generally one that is\nexpected to make a lot of our users to face deprecations, we try to introduce\nthese deprecations in a major release, instead of a minor one (with them\nstaying in the project until the next major release after this one). Though\nthis is not a guarantee.\n\nWe can still choose to not go through deprecations at all and introduce\nentirely new breaking changes in a new major release. We will however try to\navoid doing so unless we have a very strong reason to do so.\n\nmcstatus and Minecraft versions\n-------------------------------\n\nmcstatus is somewhat coupled to Minecraft server protocols and behaviors. As\nMinecraft evolves, mcstatus may need to make changes that are \"breaking\" at the\nlibrary level, even if they are driven by protocol or ecosystem changes.\n\nmcstatus aims to:\n\n- remain compatible with widely used Minecraft versions and server\n  implementations\n- release updates in a timely manner when protocol behavior or common server\n  responses change\n- provide support for legacy Minecraft versions (within reason), if the latest\n  protocol for obtaining status is no longer compatible with the previous one\n\nFortunately, breaking changes in the protocol when it comes to obtaining server\nstatus are very uncommon. But it is possible that Minecraft introduces a change\nthat our library cannot process at the time of introduction, this might or\nmight not cause hard failures on mcstatus part, even if older Minecraft clients\ncan process these information, mcstatus might not be able to, until we release\na new version to support it.\n\nBecause mcstatus is maintained by volunteers, timing may vary, but we try to\nkeep mcstatus working with the latest Minecraft releases and fix critical bugs\nquickly.\n\n.. _typing-stability-guarantees:\n\nTyping stability guarantees\n---------------------------\n\nmcstatus is a strongly typed library which actively supports and encourages the\nuse of type-checkers.\n\nHowever, typing definitions occasionally need to change alongside internal\nrefactors so that mcstatus itself remains internally type-correct, and Python's\ntyping system unfortunately does not really provide a practical way to\ndeprecate types gracefully.\n\nFor this reason, **typing breakages may occur even in patch releases**.\n\nWe actively try to avoid typing breakages or postpone them to minor or even\nmajor releases when possible, but if doing so would significantly slow down our\nability to deliver a necessary bug-fix or feature, we do not consider\nmaintaining the stability of the public typing interface significant enough to\nprevent us from shipping such a change.\n\n.. admonition:: Example\n\n   To understand what we meant by breaking changes in the public typing\n   interface, it can include things like:\n\n   - Adding a ``@final`` decorator to one of our classes\n   - Making a class generic\n   - Introducing an additional type parameter to already generic class\n   - Removing a generic type parameter / making a class no longer generic\n   - Adding a convenience type-alias variable\n   - Adding a new type into a union of types in an exposed convenience\n     type-alias\n\n   Any of these changes can occur even in a patch release.\n\n.. _deprecations-and-removals:\n\nDeprecations and removals\n-------------------------\n\nWhen we deprecate something, we aim to emit a ``DeprecationWarning`` with a\nmessage containing:\n\n- what was deprecated\n- what to use instead (if a replacement is available)\n- a target removal version\n\nWhen we deprecate something, we generally aim to remove it in a **major\nrelease**, after it has been deprecated for at least one release cycle. (E.g. a\ndeprecation introduced in ``v12.2.0`` will most likely have its removal\nscheduled in ``v13.0.0`` ). For some more significant changes, we can sometimes\nkeep a deprecation around for longer though (e.g. a deprecation introduced in\n``v12.2.0`` with removal scheduled for ``v14.0.0``).\n\nWe will **always** explicitly include the removal version, until which the\ndeprecated behavior will still be guaranteed to remain functional.\n\nPost-removal deprecation handling\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nOnce a deprecated feature has passed its stated removal version, its use will\nresult in a breaking change. This is guaranteed to happen in the stated removal\nversion.\n\n.. warning::\n\n   **Relying on deprecated behavior after its removal is unsupported.**\n\n   Where feasible, mcstatus will explicitly raise the corresponding\n   ``DeprecationWarning`` as a hard exception, rather than allowing the removal\n   to manifest as a less clear runtime failures (such as ``AttributeError``).\n   This is a deliberate best-effort attempt to provide clearer diagnostics and\n   improve the upgrade experience.\n\n   This behavior is **not part of the versioning guarantees**. Any post-removal\n   deprecation handling is considered **temporary by design** and may be\n   intentionally removed after some time, including in patch releases, once the\n   breakage has been in effect for a reasonable period (typically 1-5 months).\n\n   Users must not rely on the presence, wording, or longevity of post-removal\n   deprecation handling. After removal, failures may surface as generic runtime\n   errors without any direct reference to the original deprecation.\n\nFor this reason, you should always pay attention to deprecation warnings and\nresolve them ahead of time, ideally after any minor updates, but at the very\nleast before upgrading to a new major version, to avoid unclear hard breakages.\n"
  },
  {
    "path": "docs/pyproject.toml",
    "content": "[project]\nname = \"docs\"\nversion = \"0.0.0\"\nlicense = \"Apache-2.0\"\nrequires-python = \">=3.12\"\ndependencies = [\n  \"sphinx~=9.1.0\",\n  \"sphinx-autodoc-typehints~=3.10.0\",\n  \"furo>=2025.7.19\",\n  \"m2r2~=0.3.4\",\n  \"packaging~=26.0\",\n  \"uv-dynamic-versioning~=0.14.0\",\n]\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.build.targets.wheel]\nbypass-selection = true\n"
  },
  {
    "path": "mcstatus/__init__.py",
    "content": "from mcstatus.server import BedrockServer, JavaServer, LegacyServer, MCServer\n\n__all__ = [\n    \"BedrockServer\",\n    \"JavaServer\",\n    \"LegacyServer\",\n    \"MCServer\",\n]\n"
  },
  {
    "path": "mcstatus/__main__.py",
    "content": "# ruff: noqa: T201 # usage of `print`\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport socket\nimport sys\nfrom typing import Any, TYPE_CHECKING, TypeAlias\n\nimport dns.resolver\n\nfrom mcstatus import BedrockServer, JavaServer, LegacyServer\nfrom mcstatus.responses import JavaStatusResponse\n\nif TYPE_CHECKING:\n    from mcstatus.motd import Motd\n\nSupportedServers: TypeAlias = \"JavaServer | LegacyServer | BedrockServer\"\n\nPING_PACKET_FAIL_WARNING = (\n    \"warning: contacting {address} failed with a 'ping' packet but succeeded with a 'status' packet,\\n\"\n    \"         this is likely a bug in the server-side implementation.\\n\"\n    '         (note: ping packet failed due to \"{ping_exc}\")\\n'\n    \"         for more details, see: https://mcstatus.readthedocs.io/en/stable/pages/faq/\\n\"\n)\n\nQUERY_FAIL_WARNING = (\n    \"The server did not respond to the query protocol.\"\n    \"\\nPlease ensure that the server has enable-query turned on,\"\n    \" and that the necessary port (same as server-port unless query-port is set) is open in any firewall(s).\"\n    \"\\nSee https://minecraft.wiki/w/Query for further information.\"\n)\n\n\ndef _motd(motd: Motd) -> str:\n    \"\"\"Format MOTD for human-readable output, with leading line break if multiline.\"\"\"\n    s = motd.to_ansi()\n    return f\"\\n{s}\" if \"\\n\" in s else f\" {s}\"\n\n\ndef _kind(serv: SupportedServers) -> str:\n    if isinstance(serv, JavaServer):\n        return \"Java\"\n    if isinstance(serv, LegacyServer):\n        return \"Java (pre-1.7)\"\n    if isinstance(serv, BedrockServer):\n        return \"Bedrock\"\n    raise ValueError(f\"unsupported server for kind: {serv}\")\n\n\ndef _ping_with_fallback(server: SupportedServers) -> float:\n    # only Java has ping method\n    if not isinstance(server, JavaServer):\n        return server.status().latency\n\n    # try faster ping packet first, falling back to status with a warning.\n    ping_exc = None\n    try:\n        return server.ping(tries=1)\n    except Exception as e:  # noqa: BLE001 # blindly catching Exception\n        ping_exc = e\n\n    latency = server.status().latency\n\n    address = f\"{server.address.host}:{server.address.port}\"\n    print(\n        PING_PACKET_FAIL_WARNING.format(address=address, ping_exc=ping_exc),\n        file=sys.stderr,\n    )\n\n    return latency\n\n\ndef ping_cmd(server: SupportedServers) -> int:\n    print(_ping_with_fallback(server))\n    return 0\n\n\ndef status_cmd(server: SupportedServers) -> int:\n    response = server.status()\n\n    java_res = response if isinstance(response, JavaStatusResponse) else None\n\n    if java_res and java_res.players.sample:\n        player_sample = \"\\n  \" + \"\\n  \".join(f\"{player.name} ({player.id})\" for player in java_res.players.sample)\n    else:\n        player_sample = \"\"\n\n    print(f\"version: {_kind(server)} {response.version.name} (protocol {response.version.protocol})\")\n    print(f\"motd:{_motd(response.motd)}\")\n    print(f\"players: {response.players.online}/{response.players.max}{player_sample}\")\n    print(f\"ping: {response.latency:.2f} ms\")\n    return 0\n\n\ndef json_cmd(server: SupportedServers) -> int:\n    data: dict[str, Any] = {\"online\": False, \"kind\": _kind(server)}\n\n    status_res = query_res = exn = None\n    try:\n        status_res = server.status(tries=1)\n    except Exception as e:  # noqa: BLE001 # blindly catching Exception\n        exn = exn or e\n\n    try:\n        if isinstance(server, JavaServer):\n            query_res = server.query(tries=1)\n    except Exception as e:  # noqa: BLE001 # blindly catching Exception\n        exn = exn or e\n\n    # construct 'data' dict outside try/except to ensure data processing errors\n    # are noticed.\n    data[\"online\"] = bool(status_res or query_res)\n    if not data[\"online\"]:\n        assert exn, \"server offline but no exception?\"\n        data[\"error\"] = str(exn)\n\n    if status_res is not None:\n        data[\"status\"] = status_res.as_dict()\n    if query_res is not None:\n        data[\"query\"] = query_res.as_dict()\n\n    json.dump(data, sys.stdout)\n    return 0\n\n\ndef query_cmd(server: SupportedServers) -> int:\n    if not isinstance(server, JavaServer):\n        print(\"The 'query' protocol is only supported by Java servers.\", file=sys.stderr)\n        return 1\n\n    try:\n        response = server.query()\n    except TimeoutError:\n        print(QUERY_FAIL_WARNING, file=sys.stderr)\n        return 1\n\n    print(f\"host: {response.raw['hostip']}:{response.raw['hostport']}\")\n    print(f\"software: {_kind(server)} {response.software.version} {response.software.brand}\")\n    print(f\"motd:{_motd(response.motd)}\")\n    print(f\"plugins: {response.software.plugins}\")\n    print(f\"players: {response.players.online}/{response.players.max} {response.players.list}\")\n    return 0\n\n\ndef main(argv: list[str] = sys.argv[1:]) -> int:\n    parser = argparse.ArgumentParser(\n        \"mcstatus\",\n        description=\"\"\"\n        mcstatus provides an easy way to query Minecraft servers for any information\n        they can expose. It provides three modes of access: query, status, ping and json.\n        \"\"\",\n    )\n\n    parser.add_argument(\"address\", help=\"The address of the server.\")\n    group = parser.add_mutually_exclusive_group()\n    group.add_argument(\"--bedrock\", help=\"Specifies that 'address' is a Bedrock server (default: Java).\", action=\"store_true\")\n    group.add_argument(\n        \"--legacy\", help=\"Specifies that 'address' is a pre-1.7 Java server (default: 1.7+).\", action=\"store_true\"\n    )\n\n    subparsers = parser.add_subparsers(title=\"commands\", description=\"Command to run, defaults to 'status'.\")\n    parser.set_defaults(func=status_cmd)\n\n    subparsers.add_parser(\"ping\", help=\"Ping server for latency.\").set_defaults(func=ping_cmd)\n    subparsers.add_parser(\"status\", help=\"Prints server status.\").set_defaults(func=status_cmd)\n    subparsers.add_parser(\n        \"query\", help=\"Prints detailed server information. Must be enabled in servers' server.properties file.\"\n    ).set_defaults(func=query_cmd)\n    subparsers.add_parser(\n        \"json\",\n        help=\"Prints server status and query in json.\",\n    ).set_defaults(func=json_cmd)\n\n    args = parser.parse_args(argv)\n    if args.bedrock:\n        lookup = BedrockServer.lookup\n    elif args.legacy:\n        lookup = LegacyServer.lookup\n    else:\n        lookup = JavaServer.lookup\n\n    try:\n        server = lookup(args.address)\n        return args.func(server)\n    except (socket.gaierror, dns.resolver.NoNameservers, ConnectionError, TimeoutError) as e:\n        # catch and hide traceback for expected user-facing errors\n        print(f\"Error: {e!r}\", file=sys.stderr)\n        return 1\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n"
  },
  {
    "path": "mcstatus/_compat/README.md",
    "content": "# Compatibility Shims\n\nThis directory holds compatibility shims for deprecated public modules.\n\nThese modules are not part of the main source tree. They are mapped into their\ndeprecated import paths during packaging via Hatchling `force-include` entries\nin `pyproject.toml` for the sdist (wheels are built from the sdist).\n\nExample:\n\n```toml\n[tool.hatch.build.targets.sdist.force-include]\n\"mcstatus/_compat/status_response.py\" = \"mcstatus/status_response.py\"\n```\n\nThis means that the build system will include these for us in the actual built\npackages, without the files having to clutter the actual source tree of the\nproject, making development cleaner and less confusing. As an additional\nbenefit, it prevents us from accidentally importing these deprecated utils from\nwithin mcstatus, as they will simply not be present.\n\n> [!WARNING]\n> This approach does mean that people using mcstatus directly through a git\n> submodule or otherwise attempt to run the mcstatus code from it's direct\n> source, rather than going through the proper python module installation will\n> NOT be able to utilize these deprecations.\n>\n> This isn't really a supported method of utilizing mcstatus though, and people\n> that do so should expect to face issues.\n"
  },
  {
    "path": "mcstatus/_compat/__init__.py",
    "content": ""
  },
  {
    "path": "mcstatus/_compat/forge_data.py",
    "content": "from mcstatus._utils import deprecation_warn\nfrom mcstatus.responses.forge import ForgeData, ForgeDataChannel, ForgeDataMod\n\n__all__ = [\n    \"ForgeData\",\n    \"ForgeDataChannel\",\n    \"ForgeDataMod\",\n]\n\ndeprecation_warn(\n    obj_name=\"mcstatus.forge_data\",\n    removal_version=\"14.0.0\",\n    replacement=\"mcstatus.responses.forge\",\n)\n"
  },
  {
    "path": "mcstatus/_compat/motd_transformers.py",
    "content": "from mcstatus._utils import deprecation_warn\nfrom mcstatus.motd._transformers import (\n    AnsiTransformer,\n    HtmlTransformer,\n    MinecraftTransformer,\n    PlainTransformer,\n    _BaseTransformer as BaseTransformer,\n    _NothingTransformer as NothingTransformer,\n)\n\n__all__ = [\n    \"AnsiTransformer\",\n    \"BaseTransformer\",\n    \"HtmlTransformer\",\n    \"MinecraftTransformer\",\n    \"NothingTransformer\",\n    \"PlainTransformer\",\n]\n\ndeprecation_warn(\n    obj_name=\"mcstatus.motd.transformers\",\n    removal_version=\"13.0.0\",\n    extra_msg=\"MOTD Transformers are no longer a part of mcstatus public API\",\n)\n"
  },
  {
    "path": "mcstatus/_compat/status_response.py",
    "content": "from mcstatus._utils import deprecation_warn\nfrom mcstatus.responses import (\n    BaseStatusPlayers,\n    BaseStatusResponse,\n    BaseStatusVersion,\n    BedrockStatusPlayers,\n    BedrockStatusResponse,\n    BedrockStatusVersion,\n    JavaStatusPlayer,\n    JavaStatusPlayers,\n    JavaStatusResponse,\n    JavaStatusVersion,\n)\n\n__all__ = [\n    \"BaseStatusPlayers\",\n    \"BaseStatusResponse\",\n    \"BaseStatusVersion\",\n    \"BedrockStatusPlayers\",\n    \"BedrockStatusResponse\",\n    \"BedrockStatusVersion\",\n    \"JavaStatusPlayer\",\n    \"JavaStatusPlayers\",\n    \"JavaStatusResponse\",\n    \"JavaStatusVersion\",\n]\n\ndeprecation_warn(\n    obj_name=\"mcstatus.status_response\",\n    removal_version=\"13.0.0\",\n    replacement=\"mcstatus.responses\",\n)\n"
  },
  {
    "path": "mcstatus/_net/__init__.py",
    "content": ""
  },
  {
    "path": "mcstatus/_net/address.py",
    "content": "from __future__ import annotations\n\nimport ipaddress\nimport sys\nimport warnings\nfrom typing import NamedTuple, TYPE_CHECKING\nfrom urllib.parse import urlparse\n\nimport dns.resolver\n\nfrom mcstatus._net import dns as mc_dns\n\nif TYPE_CHECKING:\n    from pathlib import Path\n\n    from typing_extensions import Self\n\n\n__all__ = [\n    \"Address\",\n    \"async_minecraft_srv_address_lookup\",\n    \"minecraft_srv_address_lookup\",\n]\n\n\ndef _valid_urlparse(address: str) -> tuple[str, int | None]:\n    \"\"\"Parse a string address like 127.0.0.1:25565 into host and port parts.\n\n    If the address doesn't have a specified port, None will be returned instead.\n\n    :raises ValueError:\n        Unable to resolve hostname of given address\n    \"\"\"\n    tmp = urlparse(\"//\" + address)\n    if not tmp.hostname:\n        raise ValueError(f\"Invalid address {address!r}, can't parse.\")\n\n    return tmp.hostname, tmp.port\n\n\nclass _AddressBase(NamedTuple):\n    \"\"\"Intermediate NamedTuple class representing an address.\n\n    We can't extend this class directly, since NamedTuples are slotted and\n    read-only, however child classes can extend __new__, allowing us do some\n    needed processing on child classes derived from this base class.\n    \"\"\"\n\n    host: str\n    port: int\n\n\nclass Address(_AddressBase):\n    \"\"\"Extension of a :class:`~typing.NamedTuple` of :attr:`.host` and :attr:`.port`, for storing addresses.\n\n    This class inherits from :class:`tuple`, and is fully compatible with all functions\n    which require pure ``(host, port)`` address tuples, but on top of that, it includes\n    some neat functionalities, such as validity ensuring, alternative constructors\n    for easy quick creation and methods handling IP resolving.\n\n    .. note::\n        The class is not a part of a Public API, but attributes :attr:`host` and :attr:`port` are a part of Public API.\n    \"\"\"\n\n    def __init__(self, host: str, port: int) -> None:  # noqa: ARG002 # unused arguments\n        # We don't pass the host & port args to super's __init__, because NamedTuples handle\n        # everything from __new__ and the passed self already has all of the parameters set.\n        super().__init__()\n\n        self._cached_ip: ipaddress.IPv4Address | ipaddress.IPv6Address | None = None\n\n        # Make sure the address is valid\n        self._ensure_validity(self.host, self.port)\n\n    @staticmethod\n    def _ensure_validity(host: object, port: object) -> None:\n        if not isinstance(host, str):\n            raise TypeError(f\"Host must be a string address, got {type(host)} ({host!r})\")\n        if not isinstance(port, int):\n            raise TypeError(f\"Port must be an integer port number, got {type(port)} ({port!r})\")\n        if port > 65535 or port < 0:\n            raise ValueError(f\"Port must be within the allowed range (0-2^16), got {port!r}\")\n\n    @classmethod\n    def from_tuple(cls, tup: tuple[str, int]) -> Self:\n        \"\"\"Construct the class from a regular tuple of ``(host, port)``, commonly used for addresses.\"\"\"\n        return cls(host=tup[0], port=tup[1])\n\n    @classmethod\n    def from_path(cls, path: Path, *, default_port: int | None = None) -> Self:\n        \"\"\"Construct the class from a :class:`~pathlib.Path` object.\n\n        If path has a port specified, use it, if not fall back to ``default_port`` kwarg.\n        In case ``default_port`` isn't provided and port wasn't specified, raise :exc:`ValueError`.\n        \"\"\"\n        address = str(path)\n        return cls.parse_address(address, default_port=default_port)\n\n    @classmethod\n    def parse_address(cls, address: str, *, default_port: int | None = None) -> Self:\n        \"\"\"Parse a string address like ``127.0.0.1:25565`` into :attr:`.host` and :attr:`.port` parts.\n\n        If the address has a port specified, use it, if not, fall back to ``default_port`` kwarg.\n\n        :raises ValueError:\n            Either the address isn't valid and can't be parsed,\n            or it lacks a port and ``default_port`` wasn't specified.\n        \"\"\"\n        hostname, port = _valid_urlparse(address)\n        if port is None:\n            if default_port is not None:\n                port = default_port\n            else:\n                raise ValueError(\n                    f\"Given address {address!r} doesn't contain port and default_port wasn't specified, can't parse.\"\n                )\n        return cls(host=hostname, port=port)\n\n    def resolve_ip(self, lifetime: float | None = None) -> ipaddress.IPv4Address | ipaddress.IPv6Address:\n        \"\"\"Resolve a hostname's A record into an IP address.\n\n        If the host is already an IP, this resolving is skipped\n        and host is returned directly.\n\n        :param lifetime:\n            How many seconds a query should run before timing out.\n            Default value for this is inherited from :func:`dns.resolver.resolve`.\n        :raises dns.exception.DNSException:\n            One of the exceptions possibly raised by :func:`dns.resolver.resolve`.\n            Most notably this will be :exc:`dns.exception.Timeout` and :exc:`dns.resolver.NXDOMAIN`\n        \"\"\"\n        if self._cached_ip is not None:\n            return self._cached_ip\n\n        host = self.host\n        if self.host == \"localhost\" and sys.platform == \"darwin\":\n            host = \"127.0.0.1\"\n            warnings.warn(\n                \"On macOS because of some mysterious reasons we can't resolve localhost into IP. \"\n                \"Please, replace 'localhost' with '127.0.0.1' (or '::1' for IPv6) in your code to remove this warning.\",\n                category=RuntimeWarning,\n                stacklevel=2,\n            )\n\n        try:\n            ip = ipaddress.ip_address(host)\n        except ValueError:\n            # ValueError is raised if the given address wasn't valid\n            # this means it's a hostname and we should try to resolve\n            # the A record\n            ip_addr = mc_dns.resolve_a_record(self.host, lifetime=lifetime)\n            ip = ipaddress.ip_address(ip_addr)\n\n        self._cached_ip = ip\n        return self._cached_ip\n\n    async def async_resolve_ip(self, lifetime: float | None = None) -> ipaddress.IPv4Address | ipaddress.IPv6Address:\n        \"\"\"Resolve a hostname's A record into an IP address.\n\n        See the docstring for :meth:`.resolve_ip` for further info. This function is purely\n        an async alternative to it.\n        \"\"\"\n        if self._cached_ip is not None:\n            return self._cached_ip\n\n        host = self.host\n        if self.host == \"localhost\" and sys.platform == \"darwin\":\n            host = \"127.0.0.1\"\n            warnings.warn(\n                \"On macOS because of some mysterious reasons we can't resolve localhost into IP. \"\n                \"Please, replace 'localhost' with '127.0.0.1' (or '::1' for IPv6) in your code to remove this warning.\",\n                category=RuntimeWarning,\n                stacklevel=2,\n            )\n\n        try:\n            ip = ipaddress.ip_address(host)\n        except ValueError:\n            # ValueError is raised if the given address wasn't valid\n            # this means it's a hostname and we should try to resolve\n            # the A record\n            ip_addr = await mc_dns.async_resolve_a_record(self.host, lifetime=lifetime)\n            ip = ipaddress.ip_address(ip_addr)\n\n        self._cached_ip = ip\n        return self._cached_ip\n\n\ndef minecraft_srv_address_lookup(\n    address: str,\n    *,\n    default_port: int | None = None,\n    lifetime: float | None = None,\n) -> Address:\n    \"\"\"Lookup the SRV record for a Minecraft server.\n\n    Firstly it parses the address, if it doesn't include port, tries SRV record, and if it's not there,\n    falls back on ``default_port``.\n\n    This function essentially mimics the address field of a Minecraft Java server. It expects an address like\n    ``192.168.0.100:25565``, if this address does contain a port, it will simply use it. If it doesn't, it will try\n    to perform an SRV lookup, which if found, will contain the info on which port to use. If there's no SRV record,\n    this will fall back to the given ``default_port``.\n\n    :param address:\n        The same address which would be used in minecraft's server address field.\n        Can look like: ``127.0.0.1``, or ``192.168.0.100:12345``, or ``mc.hypixel.net``, or ``example.com:12345``.\n    :param lifetime:\n        How many seconds a query should run before timing out.\n        Default value for this is inherited from :func:`dns.resolver.resolve`.\n    :raises ValueError:\n        Either the address isn't valid and can't be parsed,\n        or it lacks a port, SRV record isn't present, and ``default_port`` wasn't specified.\n    \"\"\"\n    host, port = _valid_urlparse(address)\n\n    # If we found a port in the address, there's nothing more we need\n    if port is not None:\n        return Address(host, port)\n\n    # Otherwise, try to check for an SRV record, pointing us to the\n    # port which we should use. If there's no such record, fall back\n    # to the default_port (if it's defined).\n    try:\n        host, port = mc_dns.resolve_mc_srv(host, lifetime=lifetime)\n    except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer) as e:\n        if default_port is None:\n            raise ValueError(\n                f\"Given address {address!r} doesn't contain port, doesn't have an SRV record pointing to a port,\"\n                \" and default_port wasn't specified, can't parse.\"\n            ) from e\n        port = default_port\n\n    return Address(host, port)\n\n\nasync def async_minecraft_srv_address_lookup(\n    address: str,\n    *,\n    default_port: int | None = None,\n    lifetime: float | None = None,\n) -> Address:\n    \"\"\"Just an async alternative to :func:`.minecraft_srv_address_lookup`, check it for more details.\"\"\"\n    host, port = _valid_urlparse(address)\n\n    # If we found a port in the address, there's nothing more we need\n    if port is not None:\n        return Address(host, port)\n\n    # Otherwise, try to check for an SRV record, pointing us to the\n    # port which we should use. If there's no such record, fall back\n    # to the default_port (if it's defined).\n    try:\n        host, port = await mc_dns.async_resolve_mc_srv(host, lifetime=lifetime)\n    except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer) as e:\n        if default_port is None:\n            raise ValueError(\n                f\"Given address {address!r} doesn't contain port, doesn't have an SRV record pointing to a port,\"\n                \" and default_port wasn't specified, can't parse.\"\n            ) from e\n        port = default_port\n\n    return Address(host, port)\n"
  },
  {
    "path": "mcstatus/_net/dns.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING, cast\n\nimport dns.asyncresolver\nimport dns.resolver\nfrom dns.rdatatype import RdataType\n\nif TYPE_CHECKING:\n    from dns.rdtypes.IN.A import A as ARecordAnswer\n    from dns.rdtypes.IN.SRV import SRV as SRVRecordAnswer  # noqa: N811 # constant imported as non constant (it's class)\n\n__all__ = [\n    \"async_resolve_a_record\",\n    \"async_resolve_mc_srv\",\n    \"async_resolve_srv_record\",\n    \"resolve_a_record\",\n    \"resolve_mc_srv\",\n    \"resolve_srv_record\",\n]\n\n\ndef resolve_a_record(hostname: str, lifetime: float | None = None) -> str:\n    \"\"\"Perform a DNS resolution for an A record to given hostname.\n\n    :param hostname: The address to resolve for.\n    :return: The resolved IP address from the A record\n    :raises dns.exception.DNSException:\n        One of the exceptions possibly raised by :func:`dns.resolver.resolve`.\n        Most notably this will be :exc:`dns.exception.Timeout`, :exc:`dns.resolver.NXDOMAIN`\n        and :exc:`dns.resolver.NoAnswer`\n    \"\"\"\n    answers = dns.resolver.resolve(hostname, RdataType.A, lifetime=lifetime, search=True)\n    # There should only be one answer here, though in case the server\n    # does actually point to multiple IPs, we just pick the first one\n    answer = cast(\"ARecordAnswer\", answers[0])\n    ip = str(answer).rstrip(\".\")\n    return ip\n\n\nasync def async_resolve_a_record(hostname: str, lifetime: float | None = None) -> str:\n    \"\"\"Asynchronous alternative to :func:`.resolve_a_record`.\n\n    For more details, check it.\n    \"\"\"\n    answers = await dns.asyncresolver.resolve(hostname, RdataType.A, lifetime=lifetime, search=True)\n    # There should only be one answer here, though in case the server\n    # does actually point to multiple IPs, we just pick the first one\n    answer = cast(\"ARecordAnswer\", answers[0])\n    ip = str(answer).rstrip(\".\")\n    return ip\n\n\ndef resolve_srv_record(query_name: str, lifetime: float | None = None) -> tuple[str, int]:\n    \"\"\"Perform a DNS resolution for SRV record pointing to the Java Server.\n\n    :param query_name: The address to resolve for.\n    :return: A tuple of host string and port number\n    :raises dns.exception.DNSException:\n        One of the exceptions possibly raised by :func:`dns.resolver.resolve`.\n        Most notably this will be :exc:`dns.exception.Timeout`, :exc:`dns.resolver.NXDOMAIN`\n        and :exc:`dns.resolver.NoAnswer`\n    \"\"\"\n    answers = dns.resolver.resolve(query_name, RdataType.SRV, lifetime=lifetime, search=True)\n    # There should only be one answer here, though in case the server\n    # does actually point to multiple IPs, we just pick the first one\n    answer = cast(\"SRVRecordAnswer\", answers[0])\n    host = str(answer.target).rstrip(\".\")\n    port = int(answer.port)\n    return host, port\n\n\nasync def async_resolve_srv_record(query_name: str, lifetime: float | None = None) -> tuple[str, int]:\n    \"\"\"Asynchronous alternative to :func:`.resolve_srv_record`.\n\n    For more details, check it.\n    \"\"\"\n    answers = await dns.asyncresolver.resolve(query_name, RdataType.SRV, lifetime=lifetime, search=True)\n    # There should only be one answer here, though in case the server\n    # does actually point to multiple IPs, we just pick the first one\n    answer = cast(\"SRVRecordAnswer\", answers[0])\n    host = str(answer.target).rstrip(\".\")\n    port = int(answer.port)\n    return host, port\n\n\ndef resolve_mc_srv(hostname: str, lifetime: float | None = None) -> tuple[str, int]:\n    \"\"\"Resolve SRV record for a minecraft server on given hostname.\n\n    :param str hostname: The address, without port, on which an SRV record is present.\n    :return: Obtained target and port from the SRV record, on which the server should live on.\n    :raises dns.exception.DNSException:\n        One of the exceptions possibly raised by :func:`dns.resolver.resolve`.\n        Most notably this will be :exc:`dns.exception.Timeout`, :exc:`dns.resolver.NXDOMAIN`\n        and :exc:`dns.resolver.NoAnswer`.\n    \"\"\"\n    return resolve_srv_record(\"_minecraft._tcp.\" + hostname, lifetime=lifetime)\n\n\nasync def async_resolve_mc_srv(hostname: str, lifetime: float | None = None) -> tuple[str, int]:\n    \"\"\"Asynchronous alternative to :func:`.resolve_mc_srv`.\n\n    For more details, check it.\n    \"\"\"\n    return await async_resolve_srv_record(\"_minecraft._tcp.\" + hostname, lifetime=lifetime)\n"
  },
  {
    "path": "mcstatus/_protocol/__init__.py",
    "content": ""
  },
  {
    "path": "mcstatus/_protocol/bedrock_client.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport socket\nimport struct\nfrom time import perf_counter\nfrom typing import TYPE_CHECKING\n\nimport asyncio_dgram\n\nfrom mcstatus.responses import BedrockStatusResponse\n\nif TYPE_CHECKING:\n    from mcstatus._net.address import Address\n\n__all__ = [\"BedrockClient\"]\n\n\nclass BedrockClient:\n    request_status_data = bytes.fromhex(\n        # see https://minecraft.wiki/w/RakNet#Unconnected_Ping\n        \"01\" + \"0000000000000000\" + \"00ffff00fefefefefdfdfdfd12345678\" + \"0000000000000000\"\n    )\n\n    def __init__(self, address: Address, timeout: float = 3) -> None:\n        self.address = address\n        self.timeout = timeout\n\n    @staticmethod\n    def parse_response(data: bytes, latency: float) -> BedrockStatusResponse:\n        data = data[1:]\n        name_length = struct.unpack(\">H\", data[32:34])[0]\n        decoded_data = data[34 : 34 + name_length].decode().split(\";\")\n\n        return BedrockStatusResponse.build(decoded_data, latency)\n\n    def read_status(self) -> BedrockStatusResponse:\n        start = perf_counter()\n        data = self._read_status()\n        end = perf_counter()\n        return self.parse_response(data, (end - start) * 1000)\n\n    def _read_status(self) -> bytes:\n        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\n        s.settimeout(self.timeout)\n\n        s.sendto(self.request_status_data, self.address)\n        data, _ = s.recvfrom(2048)\n\n        return data\n\n    async def read_status_async(self) -> BedrockStatusResponse:\n        start = perf_counter()\n        data = await self._read_status_async()\n        end = perf_counter()\n\n        return self.parse_response(data, (end - start) * 1000)\n\n    async def _read_status_async(self) -> bytes:\n        stream = None\n        try:\n            conn = asyncio_dgram.connect(self.address)\n            stream = await asyncio.wait_for(conn, timeout=self.timeout)\n\n            await asyncio.wait_for(stream.send(self.request_status_data), timeout=self.timeout)\n            data, _ = await asyncio.wait_for(stream.recv(), timeout=self.timeout)\n        finally:\n            if stream is not None:\n                stream.close()\n\n        return data\n"
  },
  {
    "path": "mcstatus/_protocol/connection.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport errno\nimport socket\nimport struct\nfrom abc import ABC, abstractmethod\nfrom ctypes import c_int32 as signed_int32, c_int64 as signed_int64, c_uint32 as unsigned_int32, c_uint64 as unsigned_int64\nfrom ipaddress import ip_address\nfrom typing import TYPE_CHECKING, TypeAlias, cast\n\nimport asyncio_dgram\n\nif TYPE_CHECKING:\n    from collections.abc import Iterable\n\n    from typing_extensions import Self, SupportsIndex\n\n    from mcstatus._net.address import Address\n\n__all__ = [\n    \"BaseAsyncConnection\",\n    \"BaseAsyncReadSyncWriteConnection\",\n    \"BaseConnection\",\n    \"BaseReadAsync\",\n    \"BaseReadSync\",\n    \"BaseSyncConnection\",\n    \"BaseWriteAsync\",\n    \"BaseWriteSync\",\n    \"Connection\",\n    \"SocketConnection\",\n    \"TCPAsyncSocketConnection\",\n    \"TCPSocketConnection\",\n    \"UDPAsyncSocketConnection\",\n    \"UDPSocketConnection\",\n]\n\nBytesConvertable: TypeAlias = \"SupportsIndex | Iterable[SupportsIndex]\"\n\n\ndef _ip_type(address: int | str) -> int | None:\n    \"\"\"Determine the IP version (IPv4 or IPv6).\n\n    :param address:\n        A string or integer, the IP address. Either IPv4 or IPv6 addresses may be supplied.\n        Integers less than 2**32 will be considered to be IPv4 by default.\n    :return: ``4`` or ``6`` if the IP is IPv4 or IPv6, respectively. :obj:`None` if the IP is invalid.\n    \"\"\"\n    try:\n        return ip_address(address).version\n    except ValueError:\n        return None\n\n\nclass BaseWriteSync(ABC):\n    \"\"\"Base synchronous write class.\"\"\"\n\n    __slots__ = ()\n\n    @abstractmethod\n    def write(self, data: Connection | str | bytearray | bytes) -> None:\n        \"\"\"Write data to ``self``.\"\"\"\n\n    def __repr__(self) -> str:\n        return f\"<{self.__class__.__name__} Object>\"\n\n    @staticmethod\n    def _pack(format_: str, data: int) -> bytes:\n        \"\"\"Pack data in with format in big-endian mode.\"\"\"\n        return struct.pack(\">\" + format_, data)\n\n    def write_varint(self, value: int) -> None:\n        \"\"\"Write varint with value ``value`` to ``self``.\n\n        :param value: Maximum is ``2 ** 31 - 1``, minimum is ``-(2 ** 31)``.\n        :raises ValueError: If value is out of range.\n        \"\"\"\n        remaining = unsigned_int32(value).value\n        for _ in range(5):\n            if not remaining & -0x80:  # remaining & ~0x7F == 0:\n                self.write(struct.pack(\"!B\", remaining))\n                if value > 2**31 - 1 or value < -(2**31):\n                    break\n                return\n            self.write(struct.pack(\"!B\", remaining & 0x7F | 0x80))\n            remaining >>= 7\n        raise ValueError(f'The value \"{value}\" is too big to send in a varint')\n\n    def write_varlong(self, value: int) -> None:\n        \"\"\"Write varlong with value ``value`` to ``self``.\n\n        :param value: Maximum is ``2 ** 63 - 1``, minimum is ``-(2 ** 63)``.\n        :raises ValueError: If value is out of range.\n        \"\"\"\n        remaining = unsigned_int64(value).value\n        for _ in range(10):\n            if not remaining & -0x80:  # remaining & ~0x7F == 0:\n                self.write(struct.pack(\"!B\", remaining))\n                if value > 2**63 - 1 or value < -(2**31):\n                    break\n                return\n            self.write(struct.pack(\"!B\", remaining & 0x7F | 0x80))\n            remaining >>= 7\n        raise ValueError(f'The value \"{value}\" is too big to send in a varlong')\n\n    def write_utf(self, value: str) -> None:\n        \"\"\"Write varint of length of ``value`` up to 32767 bytes, then write ``value`` encoded with ``UTF-8``.\"\"\"\n        self.write_varint(len(value))\n        self.write(bytearray(value, \"utf8\"))\n\n    def write_ascii(self, value: str) -> None:\n        \"\"\"Write value encoded with ``ISO-8859-1``, then write an additional ``0x00`` at the end.\"\"\"\n        self.write(bytearray(value, \"ISO-8859-1\"))\n        self.write(bytearray.fromhex(\"00\"))\n\n    def write_short(self, value: int) -> None:\n        \"\"\"Write 2 bytes for value ``-32768 - 32767``.\"\"\"\n        self.write(self._pack(\"h\", value))\n\n    def write_ushort(self, value: int) -> None:\n        \"\"\"Write 2 bytes for value ``0 - 65535 (2 ** 16 - 1)``.\"\"\"\n        self.write(self._pack(\"H\", value))\n\n    def write_int(self, value: int) -> None:\n        \"\"\"Write 4 bytes for value ``-2147483648 - 2147483647``.\"\"\"\n        self.write(self._pack(\"i\", value))\n\n    def write_uint(self, value: int) -> None:\n        \"\"\"Write 4 bytes for value ``0 - 4294967295 (2 ** 32 - 1)``.\"\"\"\n        self.write(self._pack(\"I\", value))\n\n    def write_long(self, value: int) -> None:\n        \"\"\"Write 8 bytes for value ``-9223372036854775808 - 9223372036854775807``.\"\"\"\n        self.write(self._pack(\"q\", value))\n\n    def write_ulong(self, value: int) -> None:\n        \"\"\"Write 8 bytes for value ``0 - 18446744073709551613 (2 ** 64 - 1)``.\"\"\"\n        self.write(self._pack(\"Q\", value))\n\n    def write_bool(self, value: bool) -> None:  # noqa: FBT001 # Boolean positional argument\n        \"\"\"Write 1 byte for boolean `True` or `False`.\"\"\"\n        self.write(self._pack(\"?\", value))\n\n    def write_buffer(self, buffer: Connection) -> None:\n        \"\"\"Flush buffer, then write a varint of the length of the buffer's data, then write buffer data.\"\"\"\n        data = buffer.flush()\n        self.write_varint(len(data))\n        self.write(data)\n\n\nclass BaseWriteAsync(ABC):\n    \"\"\"Base synchronous write class.\"\"\"\n\n    __slots__ = ()\n\n    @abstractmethod\n    async def write(self, data: Connection | str | bytearray | bytes) -> None:\n        \"\"\"Write data to ``self``.\"\"\"\n\n    def __repr__(self) -> str:\n        return f\"<{self.__class__.__name__} Object>\"\n\n    @staticmethod\n    def _pack(format_: str, data: int) -> bytes:\n        \"\"\"Pack data in with format in big-endian mode.\"\"\"\n        return struct.pack(\">\" + format_, data)\n\n    async def write_varint(self, value: int) -> None:\n        \"\"\"Write varint with value ``value`` to ``self``.\n\n        :param value: Maximum is ``2 ** 31 - 1``, minimum is ``-(2 ** 31)``.\n        :raises ValueError: If value is out of range.\n        \"\"\"\n        remaining = unsigned_int32(value).value\n        for _ in range(5):\n            if not remaining & -0x80:  # remaining & ~0x7F == 0:\n                await self.write(struct.pack(\"!B\", remaining))\n                if value > 2**31 - 1 or value < -(2**31):\n                    break\n                return\n            await self.write(struct.pack(\"!B\", remaining & 0x7F | 0x80))\n            remaining >>= 7\n        raise ValueError(f'The value \"{value}\" is too big to send in a varint')\n\n    async def write_varlong(self, value: int) -> None:\n        \"\"\"Write varlong with value ``value`` to ``self``.\n\n        :param value: Maximum is ``2 ** 63 - 1``, minimum is ``-(2 ** 63)``.\n        :raises ValueError: If value is out of range.\n        \"\"\"\n        remaining = unsigned_int64(value).value\n        for _ in range(10):\n            if not remaining & -0x80:  # remaining & ~0x7F == 0:\n                await self.write(struct.pack(\"!B\", remaining))\n                if value > 2**63 - 1 or value < -(2**31):\n                    break\n                return\n            await self.write(struct.pack(\"!B\", remaining & 0x7F | 0x80))\n            remaining >>= 7\n        raise ValueError(f'The value \"{value}\" is too big to send in a varlong')\n\n    async def write_utf(self, value: str) -> None:\n        \"\"\"Write varint of length of ``value`` up to 32767 bytes, then write ``value`` encoded with ``UTF-8``.\"\"\"\n        await self.write_varint(len(value))\n        await self.write(bytearray(value, \"utf8\"))\n\n    async def write_ascii(self, value: str) -> None:\n        \"\"\"Write value encoded with ``ISO-8859-1``, then write an additional ``0x00`` at the end.\"\"\"\n        await self.write(bytearray(value, \"ISO-8859-1\"))\n        await self.write(bytearray.fromhex(\"00\"))\n\n    async def write_short(self, value: int) -> None:\n        \"\"\"Write 2 bytes for value ``-32768 - 32767``.\"\"\"\n        await self.write(self._pack(\"h\", value))\n\n    async def write_ushort(self, value: int) -> None:\n        \"\"\"Write 2 bytes for value ``0 - 65535 (2 ** 16 - 1)``.\"\"\"\n        await self.write(self._pack(\"H\", value))\n\n    async def write_int(self, value: int) -> None:\n        \"\"\"Write 4 bytes for value ``-2147483648 - 2147483647``.\"\"\"\n        await self.write(self._pack(\"i\", value))\n\n    async def write_uint(self, value: int) -> None:\n        \"\"\"Write 4 bytes for value ``0 - 4294967295 (2 ** 32 - 1)``.\"\"\"\n        await self.write(self._pack(\"I\", value))\n\n    async def write_long(self, value: int) -> None:\n        \"\"\"Write 8 bytes for value ``-9223372036854775808 - 9223372036854775807``.\"\"\"\n        await self.write(self._pack(\"q\", value))\n\n    async def write_ulong(self, value: int) -> None:\n        \"\"\"Write 8 bytes for value ``0 - 18446744073709551613 (2 ** 64 - 1)``.\"\"\"\n        await self.write(self._pack(\"Q\", value))\n\n    async def write_bool(self, value: bool) -> None:  # noqa: FBT001 # Boolean positional argument\n        \"\"\"Write 1 byte for boolean `True` or `False`.\"\"\"\n        await self.write(self._pack(\"?\", value))\n\n    async def write_buffer(self, buffer: Connection) -> None:\n        \"\"\"Flush buffer, then write a varint of the length of the buffer's data, then write buffer data.\"\"\"\n        data = buffer.flush()\n        await self.write_varint(len(data))\n        await self.write(data)\n\n\nclass BaseReadSync(ABC):\n    \"\"\"Base synchronous read class.\"\"\"\n\n    __slots__ = ()\n\n    @abstractmethod\n    def read(self, length: int, /) -> bytearray:\n        \"\"\"Read length bytes from ``self``, and return a byte array.\"\"\"\n\n    def __repr__(self) -> str:\n        return f\"<{self.__class__.__name__} Object>\"\n\n    @staticmethod\n    def _unpack(format_: str, data: bytes) -> int:\n        \"\"\"Unpack data as bytes with format in big-endian.\"\"\"\n        return struct.unpack(\">\" + format_, bytes(data))[0]\n\n    def read_varint(self) -> int:\n        \"\"\"Read varint from ``self`` and return it.\n\n        :param value: Maximum is ``2 ** 31 - 1``, minimum is ``-(2 ** 31)``.\n        :raises IOError: If varint received is out of range.\n        \"\"\"\n        result = 0\n        for i in range(5):\n            part = self.read(1)[0]\n            result |= (part & 0x7F) << (7 * i)\n            if not part & 0x80:\n                return signed_int32(result).value\n        raise OSError(\"Received varint is too big!\")\n\n    def read_varlong(self) -> int:\n        \"\"\"Read varlong from ``self`` and return it.\n\n        :param value: Maximum is ``2 ** 63 - 1``, minimum is ``-(2 ** 63)``.\n        :raises IOError: If varint received is out of range.\n        \"\"\"\n        result = 0\n        for i in range(10):\n            part = self.read(1)[0]\n            result |= (part & 0x7F) << (7 * i)\n            if not part & 0x80:\n                return signed_int64(result).value\n        raise OSError(\"Received varlong is too big!\")\n\n    def read_utf(self) -> str:\n        \"\"\"Read up to 32767 bytes by reading a varint, then decode bytes as ``UTF-8``.\"\"\"\n        length = self.read_varint()\n        return self.read(length).decode(\"utf8\")\n\n    def read_ascii(self) -> str:\n        \"\"\"Read ``self`` until last value is not zero, then return that decoded with ``ISO-8859-1``.\"\"\"\n        result = bytearray()\n        while len(result) == 0 or result[-1] != 0:\n            result.extend(self.read(1))\n        return result[:-1].decode(\"ISO-8859-1\")\n\n    def read_short(self) -> int:\n        \"\"\"Return ``-32768 - 32767``. Read 2 bytes.\"\"\"\n        return self._unpack(\"h\", self.read(2))\n\n    def read_ushort(self) -> int:\n        \"\"\"Return ``0 - 65535 (2 ** 16 - 1)``. Read 2 bytes.\"\"\"\n        return self._unpack(\"H\", self.read(2))\n\n    def read_int(self) -> int:\n        \"\"\"Return ``-2147483648 - 2147483647``. Read 4 bytes.\"\"\"\n        return self._unpack(\"i\", self.read(4))\n\n    def read_uint(self) -> int:\n        \"\"\"Return ``0 - 4294967295 (2 ** 32 - 1)``. 4 bytes read.\"\"\"\n        return self._unpack(\"I\", self.read(4))\n\n    def read_long(self) -> int:\n        \"\"\"Return ``-9223372036854775808 - 9223372036854775807``. Read 8 bytes.\"\"\"\n        return self._unpack(\"q\", self.read(8))\n\n    def read_ulong(self) -> int:\n        \"\"\"Return ``0 - 18446744073709551613 (2 ** 64 - 1)``. Read 8 bytes.\"\"\"\n        return self._unpack(\"Q\", self.read(8))\n\n    def read_bool(self) -> bool:\n        \"\"\"Return `True` or `False`. Read 1 byte.\"\"\"\n        return cast(\"bool\", self._unpack(\"?\", self.read(1)))\n\n    def read_buffer(self) -> Connection:\n        \"\"\"Read a varint for length, then return a new connection from length read bytes.\"\"\"\n        length = self.read_varint()\n        result = Connection()\n        result.receive(self.read(length))\n        return result\n\n\nclass BaseReadAsync(ABC):\n    \"\"\"Asynchronous Read connection base class.\"\"\"\n\n    __slots__ = ()\n\n    @abstractmethod\n    async def read(self, length: int, /) -> bytearray:\n        \"\"\"Read length bytes from ``self``, return a byte array.\"\"\"\n\n    def __repr__(self) -> str:\n        return f\"<{self.__class__.__name__} Object>\"\n\n    @staticmethod\n    def _unpack(format_: str, data: bytes) -> int:\n        \"\"\"Unpack data as bytes with format in big-endian.\"\"\"\n        return struct.unpack(\">\" + format_, bytes(data))[0]\n\n    async def read_varint(self) -> int:\n        \"\"\"Read varint from ``self`` and return it.\n\n        :param value: Maximum is ``2 ** 31 - 1``, minimum is ``-(2 ** 31)``.\n        :raises IOError: If varint received is out of range.\n        \"\"\"\n        result = 0\n        for i in range(5):\n            part = (await self.read(1))[0]\n            result |= (part & 0x7F) << 7 * i\n            if not part & 0x80:\n                return signed_int32(result).value\n        raise OSError(\"Received a varint that was too big!\")\n\n    async def read_varlong(self) -> int:\n        \"\"\"Read varlong from ``self`` and return it.\n\n        :param value: Maximum is ``2 ** 63 - 1``, minimum is ``-(2 ** 63)``.\n        :raises IOError: If varint received is out of range.\n        \"\"\"\n        result = 0\n        for i in range(10):\n            part = (await self.read(1))[0]\n            result |= (part & 0x7F) << (7 * i)\n            if not part & 0x80:\n                return signed_int64(result).value\n        raise OSError(\"Received varlong is too big!\")\n\n    async def read_utf(self) -> str:\n        \"\"\"Read up to 32767 bytes by reading a varint, then decode bytes as ``UTF-8``.\"\"\"\n        length = await self.read_varint()\n        return (await self.read(length)).decode(\"utf8\")\n\n    async def read_ascii(self) -> str:\n        \"\"\"Read ``self`` until last value is not zero, then return that decoded with ``ISO-8859-1``.\"\"\"\n        result = bytearray()\n        while len(result) == 0 or result[-1] != 0:\n            result.extend(await self.read(1))\n        return result[:-1].decode(\"ISO-8859-1\")\n\n    async def read_short(self) -> int:\n        \"\"\"Return ``-32768 - 32767``. Read 2 bytes.\"\"\"\n        return self._unpack(\"h\", await self.read(2))\n\n    async def read_ushort(self) -> int:\n        \"\"\"Return ``0 - 65535 (2 ** 16 - 1)``. Read 2 bytes.\"\"\"\n        return self._unpack(\"H\", await self.read(2))\n\n    async def read_int(self) -> int:\n        \"\"\"Return ``-2147483648 - 2147483647``. Read 4 bytes.\"\"\"\n        return self._unpack(\"i\", await self.read(4))\n\n    async def read_uint(self) -> int:\n        \"\"\"Return ``0 - 4294967295 (2 ** 32 - 1)``. 4 bytes read.\"\"\"\n        return self._unpack(\"I\", await self.read(4))\n\n    async def read_long(self) -> int:\n        \"\"\"Return ``-9223372036854775808 - 9223372036854775807``. Read 8 bytes.\"\"\"\n        return self._unpack(\"q\", await self.read(8))\n\n    async def read_ulong(self) -> int:\n        \"\"\"Return ``0 - 18446744073709551613 (2 ** 64 - 1)``. Read 8 bytes.\"\"\"\n        return self._unpack(\"Q\", await self.read(8))\n\n    async def read_bool(self) -> bool:\n        \"\"\"Return `True` or `False`. Read 1 byte.\"\"\"\n        return cast(\"bool\", self._unpack(\"?\", await self.read(1)))\n\n    async def read_buffer(self) -> Connection:\n        \"\"\"Read a varint for length, then return a new connection from length read bytes.\"\"\"\n        length = await self.read_varint()\n        result = Connection()\n        result.receive(await self.read(length))\n        return result\n\n\nclass BaseConnection:\n    \"\"\"Base Connection class. Implements flush, receive, and remaining.\"\"\"\n\n    __slots__ = ()\n\n    def __repr__(self) -> str:\n        return f\"<{self.__class__.__name__} Object>\"\n\n    def flush(self) -> bytearray:\n        \"\"\"Raise :exc:`TypeError`, unsupported.\"\"\"\n        raise TypeError(f\"{self.__class__.__name__} does not support flush()\")\n\n    def receive(self, _data: BytesConvertable | bytearray) -> None:\n        \"\"\"Raise :exc:`TypeError`, unsupported.\"\"\"\n        raise TypeError(f\"{self.__class__.__name__} does not support receive()\")\n\n    def remaining(self) -> int:\n        \"\"\"Raise :exc:`TypeError`, unsupported.\"\"\"\n        raise TypeError(f\"{self.__class__.__name__} does not support remaining()\")\n\n\nclass BaseSyncConnection(BaseConnection, BaseReadSync, BaseWriteSync):\n    \"\"\"Base synchronous read and write class.\"\"\"\n\n    __slots__ = ()\n\n\nclass BaseAsyncReadSyncWriteConnection(BaseConnection, BaseReadAsync, BaseWriteSync):\n    \"\"\"Base asynchronous read and synchronous write class.\"\"\"\n\n    __slots__ = ()\n\n\nclass BaseAsyncConnection(BaseConnection, BaseReadAsync, BaseWriteAsync):\n    \"\"\"Base asynchronous read and write class.\"\"\"\n\n    __slots__ = ()\n\n\nclass Connection(BaseSyncConnection):\n    \"\"\"Base connection class.\"\"\"\n\n    __slots__ = (\"received\", \"sent\")\n\n    def __init__(self) -> None:\n        self.sent = bytearray()\n        self.received = bytearray()\n\n    def read(self, length: int, /) -> bytearray:\n        \"\"\"Return :attr:`.received` up to length bytes, then cut received up to that point.\"\"\"\n        if len(self.received) < length:\n            raise OSError(f\"Not enough data to read! {len(self.received)} < {length}\")\n\n        result = self.received[:length]\n        self.received = self.received[length:]\n        return result\n\n    def write(self, data: Connection | str | bytearray | bytes) -> None:\n        \"\"\"Extend :attr:`.sent` from ``data``.\"\"\"\n        if isinstance(data, Connection):\n            data = data.flush()\n        if isinstance(data, str):\n            data = bytearray(data, \"utf-8\")\n        self.sent.extend(data)\n\n    def receive(self, data: BytesConvertable | bytearray) -> None:\n        \"\"\"Extend :attr:`.received` with ``data``.\"\"\"\n        if not isinstance(data, bytearray):\n            data = bytearray(data)\n        self.received.extend(data)\n\n    def remaining(self) -> int:\n        \"\"\"Return length of :attr:`.received`.\"\"\"\n        return len(self.received)\n\n    def flush(self) -> bytearray:\n        \"\"\"Return :attr:`.sent`, also clears :attr:`.sent`.\"\"\"\n        result, self.sent = self.sent, bytearray()\n        return result\n\n    def copy(self) -> Connection:\n        \"\"\"Return a copy of ``self``.\"\"\"\n        new = self.__class__()\n        new.receive(self.received)\n        new.write(self.sent)\n        return new\n\n\nclass SocketConnection(BaseSyncConnection):\n    \"\"\"Socket connection.\"\"\"\n\n    __slots__ = (\"socket\",)\n\n    def __init__(self) -> None:\n        # These will only be None until connect is called, ignore the None type assignment\n        self.socket: socket.socket = None  # pyright: ignore[reportAttributeAccessIssue]\n\n    def close(self) -> None:\n        \"\"\"Close :attr:`.socket`.\"\"\"\n        if self.socket is not None:  # If initialized\n            try:\n                self.socket.shutdown(socket.SHUT_RDWR)\n            except OSError as exception:  # Socket wasn't connected (nothing to shut down)\n                if exception.errno != errno.ENOTCONN:\n                    raise\n\n            self.socket.close()\n\n    def __enter__(self) -> Self:\n        return self\n\n    def __exit__(self, *_: object) -> None:\n        self.close()\n\n\nclass TCPSocketConnection(SocketConnection):\n    \"\"\"TCP Connection to address. Timeout defaults to 3 seconds.\"\"\"\n\n    __slots__ = ()\n\n    def __init__(self, addr: tuple[str | None, int], timeout: float = 3) -> None:\n        super().__init__()\n        self.socket = socket.create_connection(addr, timeout=timeout)\n        self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)\n\n    def read(self, length: int, /) -> bytearray:\n        \"\"\"Return length bytes read from :attr:`.socket`. Raises :exc:`IOError` when server doesn't respond.\"\"\"\n        result = bytearray()\n        while len(result) < length:\n            new = self.socket.recv(length - len(result))\n            if len(new) == 0:\n                raise OSError(\"Server did not respond with any information!\")\n            result.extend(new)\n        return result\n\n    def write(self, data: Connection | str | bytes | bytearray) -> None:\n        \"\"\"Send data on :attr:`.socket`.\"\"\"\n        if isinstance(data, Connection):\n            data = bytearray(data.flush())\n        elif isinstance(data, str):\n            data = bytearray(data, \"utf-8\")\n        self.socket.send(data)\n\n\nclass UDPSocketConnection(SocketConnection):\n    \"\"\"UDP Connection class.\"\"\"\n\n    __slots__ = (\"addr\",)\n\n    def __init__(self, addr: Address, timeout: float = 3) -> None:\n        super().__init__()\n        self.addr = addr\n        self.socket = socket.socket(\n            socket.AF_INET if _ip_type(addr[0]) == 4 else socket.AF_INET6,\n            socket.SOCK_DGRAM,\n        )\n        self.socket.settimeout(timeout)\n\n    def remaining(self) -> int:\n        \"\"\"Always return ``65535`` (``2 ** 16 - 1``).\"\"\"  # noqa: D401 # imperative mood\n        return 65535\n\n    def read(self, _length: int, /) -> bytearray:\n        \"\"\"Return up to :meth:`.remaining` bytes. Length does nothing here.\"\"\"\n        result = bytearray()\n        while len(result) == 0:\n            result.extend(self.socket.recvfrom(self.remaining())[0])\n        return result\n\n    def write(self, data: Connection | str | bytes | bytearray) -> None:\n        \"\"\"Use :attr:`.socket` to send data to :attr:`.addr`.\"\"\"\n        if isinstance(data, Connection):\n            data = bytearray(data.flush())\n        elif isinstance(data, str):\n            data = bytearray(data, \"utf-8\")\n        self.socket.sendto(data, self.addr)\n\n\nclass TCPAsyncSocketConnection(BaseAsyncReadSyncWriteConnection):\n    \"\"\"Asynchronous TCP Connection class.\"\"\"\n\n    __slots__ = (\"_addr\", \"reader\", \"timeout\", \"writer\")\n\n    def __init__(self, addr: Address, timeout: float = 3) -> None:\n        # These will only be None until connect is called, ignore the None type assignment\n        self.reader: asyncio.StreamReader = None  # pyright: ignore[reportAttributeAccessIssue]\n        self.writer: asyncio.StreamWriter = None  # pyright: ignore[reportAttributeAccessIssue]\n        self.timeout: float = timeout\n        self._addr = addr\n\n    async def connect(self) -> None:\n        \"\"\"Use :mod:`asyncio` to open a connection to address. Timeout is in seconds.\"\"\"\n        conn = asyncio.open_connection(*self._addr)\n        self.reader, self.writer = await asyncio.wait_for(conn, timeout=self.timeout)\n        if self.writer is not None:  # it might be None in unittest\n            sock: socket.socket = self.writer.transport.get_extra_info(\"socket\")\n            sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)\n\n    async def read(self, length: int, /) -> bytearray:\n        \"\"\"Read up to ``length`` bytes from :attr:`.reader`.\"\"\"\n        result = bytearray()\n        while len(result) < length:\n            new = await asyncio.wait_for(self.reader.read(length - len(result)), timeout=self.timeout)\n            if len(new) == 0:\n                raise OSError(\"Socket did not respond with any information!\")\n            result.extend(new)\n        return result\n\n    def write(self, data: Connection | str | bytes | bytearray) -> None:\n        \"\"\"Write data to :attr:`.writer`.\"\"\"\n        if isinstance(data, Connection):\n            data = bytearray(data.flush())\n        elif isinstance(data, str):\n            data = bytearray(data, \"utf-8\")\n        self.writer.write(data)\n\n    def close(self) -> None:\n        \"\"\"Close :attr:`.writer`.\"\"\"\n        if self.writer is not None:  # If initialized\n            self.writer.close()\n\n    async def __aenter__(self) -> Self:\n        await self.connect()\n        return self\n\n    async def __aexit__(self, *_: object) -> None:\n        self.close()\n\n\nclass UDPAsyncSocketConnection(BaseAsyncConnection):\n    \"\"\"Asynchronous UDP Connection class.\"\"\"\n\n    __slots__ = (\"_addr\", \"stream\", \"timeout\")\n\n    def __init__(self, addr: Address, timeout: float = 3) -> None:\n        # This will only be None until connect is called, ignore the None type assignment\n        self.stream: asyncio_dgram.aio.DatagramClient = None  # pyright: ignore[reportAttributeAccessIssue]\n        self.timeout: float = timeout\n        self._addr = addr\n\n    async def connect(self) -> None:\n        \"\"\"Connect to address. Timeout is in seconds.\"\"\"\n        conn = asyncio_dgram.connect(self._addr)\n        self.stream = await asyncio.wait_for(conn, timeout=self.timeout)\n\n    def remaining(self) -> int:\n        \"\"\"Always return ``65535`` (``2 ** 16 - 1``).\"\"\"  # noqa: D401 # imperative mood\n        return 65535\n\n    async def read(self, _length: int, /) -> bytearray:\n        \"\"\"Read from :attr:`.stream`. Length does nothing here.\"\"\"\n        data, _remote_addr = await asyncio.wait_for(self.stream.recv(), timeout=self.timeout)\n        return bytearray(data)\n\n    async def write(self, data: Connection | str | bytes | bytearray) -> None:\n        \"\"\"Send data with :attr:`.stream`.\"\"\"\n        if isinstance(data, Connection):\n            data = bytearray(data.flush())\n        elif isinstance(data, str):\n            data = bytearray(data, \"utf-8\")\n        await self.stream.send(data)\n\n    def close(self) -> None:\n        \"\"\"Close :attr:`.stream`.\"\"\"\n        if self.stream is not None:  # If initialized\n            self.stream.close()\n\n    async def __aenter__(self) -> Self:\n        await self.connect()\n        return self\n\n    async def __aexit__(self, *_: object) -> None:\n        self.close()\n"
  },
  {
    "path": "mcstatus/_protocol/java_client.py",
    "content": "from __future__ import annotations\n\nimport json\nimport random\nfrom abc import ABC, abstractmethod\nfrom dataclasses import dataclass\nfrom time import perf_counter\nfrom typing import TYPE_CHECKING, final\n\nfrom mcstatus._protocol.connection import Connection, TCPAsyncSocketConnection, TCPSocketConnection\nfrom mcstatus.responses import JavaStatusResponse\n\nif TYPE_CHECKING:\n    from collections.abc import Awaitable\n\n    from mcstatus._net.address import Address\n    from mcstatus.responses._raw import RawJavaResponse\n\n__all__ = [\"AsyncJavaClient\", \"JavaClient\"]\n\n\n@dataclass\nclass _BaseJavaClient(ABC):\n    connection: TCPSocketConnection | TCPAsyncSocketConnection\n    address: Address\n    version: int\n    \"\"\"Version of the client.\"\"\"\n    ping_token: int = None  # pyright: ignore[reportAssignmentType]\n    \"\"\"Token that is used for the request, default is random number.\"\"\"\n\n    def __post_init__(self) -> None:\n        if self.ping_token is None:\n            self.ping_token = random.randint(0, (1 << 63) - 1)\n\n    def handshake(self) -> None:\n        \"\"\"Write the initial handshake packet to the connection.\"\"\"\n        packet = Connection()\n        packet.write_varint(0)\n        packet.write_varint(self.version)\n        packet.write_utf(self.address.host)\n        packet.write_ushort(self.address.port)\n        packet.write_varint(1)  # Intention to query status\n\n        self.connection.write_buffer(packet)\n\n    @abstractmethod\n    def read_status(self) -> JavaStatusResponse | Awaitable[JavaStatusResponse]:\n        \"\"\"Make a status request and parse the response.\"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def test_ping(self) -> float | Awaitable[float]:\n        \"\"\"Send a ping token and measure the latency.\"\"\"\n        raise NotImplementedError\n\n    def _handle_status_response(self, response: Connection, start: float, end: float) -> JavaStatusResponse:\n        \"\"\"Given a response buffer (already read from connection), parse and build the JavaStatusResponse.\"\"\"\n        if response.read_varint() != 0:\n            raise OSError(\"Received invalid status response packet.\")\n        try:\n            raw: RawJavaResponse = json.loads(response.read_utf())\n        except ValueError as e:\n            raise OSError(\"Received invalid JSON\") from e\n\n        try:\n            latency_ms = (end - start) * 1000\n            return JavaStatusResponse.build(raw, latency=latency_ms)\n        except KeyError as e:\n            raise OSError(\"Received invalid status response\") from e\n\n    def _handle_ping_response(self, response: Connection, start: float, end: float) -> float:\n        \"\"\"Given a ping response buffer, validate token and compute latency.\"\"\"\n        if response.read_varint() != 1:\n            raise OSError(\"Received invalid ping response packet.\")\n        received_token = response.read_long()\n        if received_token != self.ping_token:\n            raise OSError(f\"Received mangled ping response (expected token {self.ping_token}, got {received_token})\")\n        return (end - start) * 1000\n\n\n@final\n@dataclass\nclass JavaClient(_BaseJavaClient):\n    connection: TCPSocketConnection  # pyright: ignore[reportIncompatibleVariableOverride]\n\n    def read_status(self) -> JavaStatusResponse:\n        \"\"\"Send the status request and read the response.\"\"\"\n        request = Connection()\n        request.write_varint(0)  # Request status\n        self.connection.write_buffer(request)\n\n        start = perf_counter()\n        response = self.connection.read_buffer()\n        end = perf_counter()\n        return self._handle_status_response(response, start, end)\n\n    def test_ping(self) -> float:\n        \"\"\"Send a ping token and measure the latency.\"\"\"\n        request = Connection()\n        request.write_varint(1)  # Test ping\n        request.write_long(self.ping_token)\n        start = perf_counter()\n        self.connection.write_buffer(request)\n\n        response = self.connection.read_buffer()\n        end = perf_counter()\n        return self._handle_ping_response(response, start, end)\n\n\n@final\n@dataclass\nclass AsyncJavaClient(_BaseJavaClient):\n    connection: TCPAsyncSocketConnection  # pyright: ignore[reportIncompatibleVariableOverride]\n\n    async def read_status(self) -> JavaStatusResponse:\n        \"\"\"Send the status request and read the response.\"\"\"\n        request = Connection()\n        request.write_varint(0)  # Request status\n        self.connection.write_buffer(request)\n\n        start = perf_counter()\n        response = await self.connection.read_buffer()\n        end = perf_counter()\n        return self._handle_status_response(response, start, end)\n\n    async def test_ping(self) -> float:\n        \"\"\"Send a ping token and measure the latency.\"\"\"\n        request = Connection()\n        request.write_varint(1)  # Test ping\n        request.write_long(self.ping_token)\n        start = perf_counter()\n        self.connection.write_buffer(request)\n\n        response = await self.connection.read_buffer()\n        end = perf_counter()\n        return self._handle_ping_response(response, start, end)\n"
  },
  {
    "path": "mcstatus/_protocol/legacy_client.py",
    "content": "from time import perf_counter\n\nfrom mcstatus._protocol.connection import BaseAsyncReadSyncWriteConnection, BaseSyncConnection\nfrom mcstatus.responses import LegacyStatusResponse\n\n__all__ = [\"AsyncLegacyClient\", \"LegacyClient\"]\n\n\nclass _BaseLegacyClient:\n    request_status_data = bytes.fromhex(\n        # see https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping#Client_to_server\n        \"fe01fa\"\n    )\n\n    @staticmethod\n    def parse_response(data: bytes, latency: float) -> LegacyStatusResponse:\n        decoded_data: list[str] = data.decode(\"UTF-16BE\").split(\"\\0\")\n        if decoded_data[0] != \"§1\":\n            # kick packets before 1.4 (12w42a) did not start with §1 and did\n            # not included information about server and protocol version\n            decoded_data = [\"§1\", \"-1\", \"<1.4\", *decoded_data[0].split(\"§\")]\n            if len(decoded_data) != 6:\n                raise OSError(\"Received invalid kick packet reason\")\n        return LegacyStatusResponse.build(decoded_data[1:], latency)\n\n\nclass LegacyClient(_BaseLegacyClient):\n    def __init__(self, connection: BaseSyncConnection) -> None:\n        self.connection = connection\n\n    def read_status(self) -> LegacyStatusResponse:\n        \"\"\"Send the status request and read the response.\"\"\"\n        start = perf_counter()\n        self.connection.write(self.request_status_data)\n        id = self.connection.read(1)\n        if id != b\"\\xff\":\n            raise OSError(\"Received invalid packet ID\")\n        length = self.connection.read_ushort()\n        data = self.connection.read(length * 2)\n        end = perf_counter()\n        return self.parse_response(data, (end - start) * 1000)\n\n\nclass AsyncLegacyClient(_BaseLegacyClient):\n    def __init__(self, connection: BaseAsyncReadSyncWriteConnection) -> None:\n        self.connection = connection\n\n    async def read_status(self) -> LegacyStatusResponse:\n        \"\"\"Send the status request and read the response.\"\"\"\n        start = perf_counter()\n        self.connection.write(self.request_status_data)\n        id = await self.connection.read(1)\n        if id != b\"\\xff\":\n            raise OSError(\"Received invalid packet ID\")\n        length = await self.connection.read_ushort()\n        data = await self.connection.read(length * 2)\n        end = perf_counter()\n        return self.parse_response(data, (end - start) * 1000)\n"
  },
  {
    "path": "mcstatus/_protocol/query_client.py",
    "content": "from __future__ import annotations\n\nimport random\nimport re\nimport struct\nfrom abc import abstractmethod\nfrom dataclasses import dataclass, field\nfrom typing import ClassVar, TYPE_CHECKING, final\n\nfrom mcstatus._protocol.connection import Connection, UDPAsyncSocketConnection, UDPSocketConnection\nfrom mcstatus.responses import QueryResponse\nfrom mcstatus.responses._raw import RawQueryResponse\n\n__all__ = [\"AsyncQueryClient\", \"QueryClient\"]\n\nif TYPE_CHECKING:\n    from collections.abc import Awaitable\n\n\n@dataclass\nclass _BaseQueryClient:\n    MAGIC_PREFIX: ClassVar = bytearray.fromhex(\"FEFD\")\n    PADDING: ClassVar = bytearray.fromhex(\"00000000\")\n    PACKET_TYPE_CHALLENGE: ClassVar = 9\n    PACKET_TYPE_QUERY: ClassVar = 0\n\n    connection: UDPSocketConnection | UDPAsyncSocketConnection\n    challenge: int = field(init=False, default=0)\n\n    @staticmethod\n    def _generate_session_id() -> int:\n        # minecraft only supports lower 4 bits\n        return random.randint(0, 2**31) & 0x0F0F0F0F\n\n    def _create_packet(self) -> Connection:\n        packet = Connection()\n        packet.write(self.MAGIC_PREFIX)\n        packet.write(struct.pack(\"!B\", self.PACKET_TYPE_QUERY))\n        packet.write_uint(self._generate_session_id())\n        packet.write_int(self.challenge)\n        packet.write(self.PADDING)\n        return packet\n\n    def _create_handshake_packet(self) -> Connection:\n        packet = Connection()\n        packet.write(self.MAGIC_PREFIX)\n        packet.write(struct.pack(\"!B\", self.PACKET_TYPE_CHALLENGE))\n        packet.write_uint(self._generate_session_id())\n        return packet\n\n    @abstractmethod\n    def _read_packet(self) -> Connection | Awaitable[Connection]:\n        raise NotImplementedError\n\n    @abstractmethod\n    def handshake(self) -> None | Awaitable[None]:\n        raise NotImplementedError\n\n    @abstractmethod\n    def read_query(self) -> QueryResponse | Awaitable[QueryResponse]:\n        raise NotImplementedError\n\n    def _parse_response(self, response: Connection) -> tuple[RawQueryResponse, list[str]]:\n        \"\"\"Transform the connection object (the result) into dict which is passed to the QueryResponse constructor.\n\n        :return: A tuple with two elements. First is `raw` answer and second is list of players.\n        \"\"\"\n        response.read(len(\"splitnum\") + 3)\n        data = {}\n\n        while True:\n            key = response.read_ascii()\n            if key == \"hostname\":  # hostname is actually motd in the query protocol\n                match = re.search(\n                    b\"(.*?)\\x00(hostip|hostport|game_id|gametype|map|maxplayers|numplayers|plugins|version)\",\n                    response.received,\n                    flags=re.DOTALL,\n                )\n                motd = match.group(1) if match else \"\"\n                # Since the query protocol does not properly support unicode, the motd is still not resolved\n                # correctly; however, this will avoid other parameter parsing errors.\n                data[key] = response.read(len(motd)).decode(\"ISO-8859-1\")\n                response.read(1)  # ignore null byte\n            elif len(key) == 0:\n                response.read(1)\n                break\n            else:\n                value = response.read_ascii()\n                data[key] = value\n\n        response.read(len(\"player_\") + 2)\n\n        players_list = []\n        while True:\n            player = response.read_ascii()\n            if len(player) == 0:\n                break\n            players_list.append(player)\n\n        return RawQueryResponse(**data), players_list\n\n\n@final\n@dataclass\nclass QueryClient(_BaseQueryClient):\n    connection: UDPSocketConnection  # pyright: ignore[reportIncompatibleVariableOverride]\n\n    def _read_packet(self) -> Connection:\n        packet = Connection()\n        packet.receive(self.connection.read(self.connection.remaining()))\n        packet.read(1 + 4)\n        return packet\n\n    def handshake(self) -> None:\n        self.connection.write(self._create_handshake_packet())\n\n        packet = self._read_packet()\n        self.challenge = int(packet.read_ascii())\n\n    def read_query(self) -> QueryResponse:\n        request = self._create_packet()\n        self.connection.write(request)\n\n        response = self._read_packet()\n        return QueryResponse.build(*self._parse_response(response))\n\n\n@final\n@dataclass\nclass AsyncQueryClient(_BaseQueryClient):\n    connection: UDPAsyncSocketConnection  # pyright: ignore[reportIncompatibleVariableOverride]\n\n    async def _read_packet(self) -> Connection:\n        packet = Connection()\n        packet.receive(await self.connection.read(self.connection.remaining()))\n        packet.read(1 + 4)\n        return packet\n\n    async def handshake(self) -> None:\n        await self.connection.write(self._create_handshake_packet())\n\n        packet = await self._read_packet()\n        self.challenge = int(packet.read_ascii())\n\n    async def read_query(self) -> QueryResponse:\n        request = self._create_packet()\n        await self.connection.write(request)\n\n        response = await self._read_packet()\n        return QueryResponse.build(*self._parse_response(response))\n"
  },
  {
    "path": "mcstatus/_utils/__init__.py",
    "content": "from mcstatus._utils.deprecation import deprecated, deprecation_warn\nfrom mcstatus._utils.general import or_none\nfrom mcstatus._utils.retry import retry\n\n__all__ = [\"deprecated\", \"deprecation_warn\", \"or_none\", \"retry\"]\n"
  },
  {
    "path": "mcstatus/_utils/deprecation.py",
    "content": "from __future__ import annotations\n\nimport functools\nimport importlib.metadata\nimport re\nimport warnings\nfrom functools import wraps\nfrom typing import ParamSpec, Protocol, TYPE_CHECKING, TypeVar\n\nif TYPE_CHECKING:\n    from collections.abc import Callable\n\n\n__all__ = [\"deprecated\", \"deprecation_warn\"]\n\nLIB_NAME = \"mcstatus\"\n\n# This comes from the python packaging docs (PEP 440 compliant versioning):\n# https://packaging.python.org/en/latest/specifications/version-specifiers/#appendix-parsing-version-strings-with-regular-expressions\nVERSION_PATTERN_FULL = re.compile(\n    r\"\"\"^\\s*\n    v?\n    (?:\n        (?:(?P<epoch>[0-9]+)!)?                           # epoch\n        (?P<release>[0-9]+(?:\\.[0-9]+)*)                  # release segment\n        (?P<pre>                                          # pre-release\n            [-_\\.]?\n            (?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))\n            [-_\\.]?\n            (?P<pre_n>[0-9]+)?\n        )?\n        (?P<post>                                         # post release\n            (?:-(?P<post_n1>[0-9]+))\n            |\n            (?:\n                [-_\\.]?\n                (?P<post_l>post|rev|r)\n                [-_\\.]?\n                (?P<post_n2>[0-9]+)?\n            )\n        )?\n        (?P<dev>                                          # dev release\n            [-_\\.]?\n            (?P<dev_l>dev)\n            [-_\\.]?\n            (?P<dev_n>[0-9]+)?\n        )?\n    )\n    (?:\\+(?P<local>[a-z0-9]+(?:[-_\\.][a-z0-9]+)*))?       # local version\n    \\s*$\"\"\",\n    re.VERBOSE | re.IGNORECASE,\n)\n# Intentionally restricted to X.Y.Z, unlike PEP 440 release segments.\n# Used only for user-supplied removal_version values parsing.\nREMOVAL_VERSION_RE = re.compile(r\"(\\d+)\\.(\\d+)\\.(\\d+)\")\nDEPRECATED_DIRECTIVE_RE = re.compile(r\"^\\s*\\.\\.\\s+deprecated::\\s*\", flags=re.MULTILINE)\n\nP = ParamSpec(\"P\")\nR = TypeVar(\"R\")\n\n\n@functools.lru_cache(maxsize=1)\ndef _get_project_version() -> tuple[int, int, int]:\n    \"\"\"Return the installed project version normalized to a 3-part release tuple.\n\n    The project version is obtained from :mod:`importlib.metadata` and parsed using the official PEP 440\n    version parsing regular expression.\n\n    All non-release components of the version (pre-releases, post-releases, development releases, and local\n    version identifiers) are intentionally ignored. The release version segment of the version is then\n    normalized to 3 components, padding with zeros if the actual version has less components, or truncating\n    if it has more. Any performed normalizing will emit a :exc:`RuntimeWarning`.\n\n    If the project version cannot be determined or parsed, ``(0, 0, 0)`` is returned and a runtime warning\n    is emitted.\n    \"\"\"\n    try:\n        _project_version = importlib.metadata.version(LIB_NAME)\n    except importlib.metadata.PackageNotFoundError:\n        # v0.0.0 will never mark things as already deprecated (removal_version will always be newer)\n        warnings.warn(f\"Failed to get {LIB_NAME} project version, assuming v0.0.0\", category=RuntimeWarning, stacklevel=1)\n        return (0, 0, 0)\n\n    m = VERSION_PATTERN_FULL.fullmatch(_project_version)\n    if m is None:\n        # This should never happen\n        warnings.warn(\n            f\"Failed to parse {LIB_NAME} project version ({_project_version}), assuming v0.0.0\",\n            category=RuntimeWarning,\n            stacklevel=1,\n        )\n        return (0, 0, 0)\n\n    if m[\"epoch\"] is not None:\n        # we're not using epoch, and we don't expect to start doing so. If we do, the rest of this\n        # implementation would likely need to be changed anyways. Generally, this should never happen.\n        warnings.warn(f\"Failed to parse {LIB_NAME} project version, assuming v0.0.0\", category=RuntimeWarning, stacklevel=1)\n        return (0, 0, 0)\n\n    release = m[\"release\"]\n    nums = [int(p) for p in release.split(\".\")]\n\n    if len(nums) < 3:\n        warnings.warn(\n            f\"{LIB_NAME} version '{release}' has less than 3 release components; remaining components will become zeroes\",\n            category=RuntimeWarning,\n            stacklevel=2,\n        )\n        nums.extend([0] * (3 - len(nums)))\n    elif len(nums) > 3:\n        warnings.warn(\n            f\"{LIB_NAME} version '{release}' has more than 3 release components; extra components are ignored\",\n            category=RuntimeWarning,\n            stacklevel=2,\n        )\n        nums = nums[:3]\n\n    return nums[0], nums[1], nums[2]\n\n\ndef deprecation_warn(\n    *,\n    obj_name: str,\n    removal_version: str | tuple[int, int, int],\n    replacement: str | None = None,\n    extra_msg: str | None = None,\n    stack_level: int = 2,\n) -> None:\n    \"\"\"Produce an appropriate deprecation warning given the parameters.\n\n    If the currently installed project version is already past the specified deprecation version,\n    a :exc:`DeprecationWarning` will be raised as a full exception. Otherwise it will just get\n    emitted as a warning.\n\n    The deprecation message used will be constructed dynamically based on the input parameters.\n\n    :param obj_name: Name of the object that got deprecated (such as ``my_function``).\n    :param removal_version: Version at which this object should be considered as deprecated and should no longer be used.\n    :param replacement: A new alternative to this (now deprecated) object.\n    :param extra_msg: Additional message included in the deprecation warning/exception at the end.\n    :param stack_level: Stack level at which the warning is emitted.\n\n    .. note:\n        If the project version contains any additional qualifiers (e.g. pre-release, post-release, dev/local versions),\n        they will be ignored and the project version will be treated a simple stable (major, minor, micro) version.\n    \"\"\"\n    if isinstance(removal_version, str):\n        if m := REMOVAL_VERSION_RE.fullmatch(removal_version):\n            removal_version = (int(m[1]), int(m[2]), int(m[3]))\n        else:\n            raise ValueError(f\"removal_version must follow regex pattern of: {REMOVAL_VERSION_RE.pattern}\")\n\n    project_version = _get_project_version()\n    already_deprecated = project_version >= removal_version\n\n    msg = f\"{obj_name}\"\n    removal_version_str = \".\".join(str(num) for num in removal_version)\n    if already_deprecated:\n        msg += f\" is passed its removal version ({removal_version_str})\"\n    else:\n        msg += f\" is deprecated and scheduled for removal in {removal_version_str}\"\n\n    if replacement is not None:\n        msg += f\", use {replacement} instead\"\n\n    msg += \".\"\n    if extra_msg is not None:\n        msg += f\" ({extra_msg})\"\n\n    if already_deprecated:\n        raise DeprecationWarning(msg)\n\n    warnings.warn(msg, category=DeprecationWarning, stacklevel=stack_level)\n\n\nclass DecoratorFunction(Protocol):\n    def __call__(self, /, func: Callable[P, R]) -> Callable[P, R]: ...\n\n\ndef deprecated(\n    *,\n    removal_version: str | tuple[int, int, int],\n    display_name: str | None = None,\n    replacement: str | None = None,\n    extra_msg: str | None = None,\n    no_docstring_check: bool = False,\n) -> DecoratorFunction:\n    \"\"\"Mark an object as deprecated.\n\n    Decorator version of :func:`deprecation_warn` function.\n\n    If the currently installed project version is already past the specified deprecation version,\n    a :exc:`DeprecationWarning` will be raised as a full exception. Otherwise it will just get\n    emitted as a warning.\n\n    The deprecation message used will be constructed based on the input parameters.\n\n    :param display_name:\n            Name of the object that got deprecated (such as `my_function`).\n\n            By default, the object name is obtained automatically from ``__qualname__`` (falling back\n            to ``__name__``) of the decorated object. Setting this explicitly will override this obtained\n            name and the `display_name` will be used instead.\n    :param removal_version: Version at which this object should be considered as deprecated and should no longer be used.\n    :param replacement: A new alternative to this (now deprecated) object.\n    :param extra_msg: Additional message included in the deprecation warning/exception at the end.\n    :param no_docstring_check:\n        Disable a runtime check for the docstring of the decorated object containing ``.. deprecated::``.\n\n    .. note:\n        If the project version contains any additional qualifiers (e.g. pre-release, post-release, dev/local versions),\n        they will be ignored and the project version will be treated a simple stable (major, minor, micro) version.\n    \"\"\"\n\n    def inner(func: Callable[P, R]) -> Callable[P, R]:\n        obj_name = getattr(func, \"__qualname__\", func.__name__) if display_name is None else display_name\n\n        if not no_docstring_check:\n            obj_doc = func.__doc__ or \"\"\n            if DEPRECATED_DIRECTIVE_RE.search(obj_doc) is None:\n                raise ValueError(\"Deprecated object does not contain '.. deprecated::' sphinx directive in its docstring\")\n\n        @wraps(func)\n        def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:\n            deprecation_warn(\n                obj_name=obj_name,\n                removal_version=removal_version,\n                replacement=replacement,\n                extra_msg=extra_msg,\n                stack_level=3,\n            )\n            return func(*args, **kwargs)\n\n        return wrapper\n\n    return inner\n"
  },
  {
    "path": "mcstatus/_utils/general.py",
    "content": "from typing import TypeVar\n\n__all__ = [\"or_none\"]\n\n\nT = TypeVar(\"T\")\n\n\ndef or_none(*args: T) -> T | None:\n    \"\"\"Return the first non-None argument.\n\n    This function is similar to the standard inline ``or`` operator, while\n    treating falsey values (such as ``0``, ``''``, or ``False``) as valid\n    results rather than skipping them. It only skips ``None`` values.\n\n    This is useful when selecting between optional values that may be empty\n    but still meaningful.\n\n    Example:\n        .. code-block:: py\n            >>> or_none(\"\", 0, \"fallback\")\n            ''\n            >>> or_none(None, None, \"value\")\n            'value'\n            >>> or_none(None, None)\n            None\n\n        This is often useful when working with dict.get, e.g.:\n\n        .. code-block:: py\n            >>> mydict = {\"a\": \"\"}\n            >>> mydict.get(\"a\") or mydict.get(\"b\")\n            None  # expected ''!\n            >>> or_none(mydict.get(\"a\"), mydict.get(\"b\"))\n            ''\n    \"\"\"\n    for arg in args:\n        if arg is not None:\n            return arg\n    return None\n"
  },
  {
    "path": "mcstatus/_utils/retry.py",
    "content": "from __future__ import annotations\n\nimport inspect\nfrom functools import wraps\nfrom typing import ParamSpec, TYPE_CHECKING, TypeVar, cast\n\nif TYPE_CHECKING:\n    from collections.abc import Callable\n\n__all__ = [\"retry\"]\n\nT = TypeVar(\"T\")\nR = TypeVar(\"R\")\nP = ParamSpec(\"P\")\nP2 = ParamSpec(\"P2\")\n\n\ndef retry(tries: int, exceptions: tuple[type[BaseException]] = (Exception,)) -> Callable[[Callable[P, R]], Callable[P, R]]:\n    \"\"\"Decorator that re-runs given function ``tries`` times if error occurs.\n\n    The amount of tries will either be the value given to the decorator,\n    or if tries is present in keyword arguments on function call, this\n    specified value will take precedence.\n\n    If the function fails even after all the retries, raise the last\n    exception that the function raised.\n\n    .. note::\n        Even if the previous failures caused a different exception, this will only raise the last one.\n    \"\"\"  # noqa: D401 # imperative mood\n\n    def decorate(func: Callable[P, R]) -> Callable[P, R]:\n        @wraps(func)\n        async def async_wrapper(\n            *args: P.args,\n            tries: int = tries,  # pyright: ignore[reportGeneralTypeIssues] # No support for adding kw-only args\n            **kwargs: P.kwargs,\n        ) -> R:\n            last_exc: BaseException\n            for _ in range(tries):\n                try:\n                    return await func(*args, **kwargs)  # pyright: ignore[reportGeneralTypeIssues] # We know func is awaitable here\n                except exceptions as exc:  # noqa: PERF203 # try-except within a loop\n                    last_exc = exc\n            # This won't actually be unbound\n            raise last_exc  # pyright: ignore[reportGeneralTypeIssues,reportPossiblyUnboundVariable]\n\n        @wraps(func)\n        def sync_wrapper(\n            *args: P.args,\n            tries: int = tries,  # pyright: ignore[reportGeneralTypeIssues] # No support for adding kw-only args\n            **kwargs: P.kwargs,\n        ) -> R:\n            last_exc: BaseException\n            for _ in range(tries):\n                try:\n                    return func(*args, **kwargs)\n                except exceptions as exc:  # noqa: PERF203 # try-except within a loop\n                    last_exc = exc\n            # This won't actually be unbound\n            raise last_exc  # pyright: ignore[reportGeneralTypeIssues,reportPossiblyUnboundVariable]\n\n        # We cast here since pythons typing doesn't support adding keyword-only arguments to signature\n        # (Support for this was a rejected idea https://peps.python.org/pep-0612/#concatenating-keyword-parameters)\n        if inspect.iscoroutinefunction(func):\n            return cast(\"Callable[P, R]\", async_wrapper)\n        return cast(\"Callable[P, R]\", sync_wrapper)\n\n    return decorate\n"
  },
  {
    "path": "mcstatus/motd/__init__.py",
    "content": "from __future__ import annotations\n\nimport re\nimport typing as t\nfrom dataclasses import dataclass\n\nfrom mcstatus.motd._simplifies import get_unused_elements, squash_nearby_strings\nfrom mcstatus.motd._transformers import AnsiTransformer, HtmlTransformer, MinecraftTransformer, PlainTransformer\nfrom mcstatus.motd.components import Formatting, MinecraftColor, ParsedMotdComponent, TranslationTag, WebColor\n\nif t.TYPE_CHECKING:\n    from typing_extensions import Self\n\n    from mcstatus.responses._raw import RawJavaResponseMotd, RawJavaResponseMotdWhenDict\n\n__all__ = [\"Motd\"]\n\n_MOTD_COLORS_RE = re.compile(r\"([\\xA7|&][0-9A-FK-OR])\", re.IGNORECASE)\n\n\n@dataclass(frozen=True)\nclass Motd:\n    \"\"\"Represents parsed MOTD.\"\"\"\n\n    parsed: list[ParsedMotdComponent]\n    \"\"\"Parsed MOTD, which then will be transformed.\n\n    Bases on this attribute, you can easily write your own MOTD-to-something parser.\n    \"\"\"\n    raw: RawJavaResponseMotd\n    \"\"\"MOTD in raw format, returning back the received server response unmodified.\"\"\"\n    bedrock: bool = False\n    \"\"\"Is the server Bedrock Edition?\"\"\"\n\n    @classmethod\n    def parse(\n        cls,\n        raw: RawJavaResponseMotd,  # pyright: ignore[reportRedeclaration] # later, we overwrite the type\n        *,\n        bedrock: bool = False,\n    ) -> Self:\n        \"\"\"Parse a raw MOTD to less raw MOTD (:attr:`.parsed` attribute).\n\n        :param raw: Raw MOTD, directly from server.\n        :param bedrock: Is server Bedrock Edition? Nothing changes here, just sets attribute.\n        :returns: New :class:`.Motd` instance.\n        \"\"\"\n        original_raw = raw.copy() if hasattr(raw, \"copy\") else raw  # pyright: ignore[reportAttributeAccessIssue] # Cannot access \"copy\" for type \"str\"\n        if isinstance(raw, list):\n            raw: RawJavaResponseMotdWhenDict = {\"extra\": raw}\n\n        if isinstance(raw, str):\n            parsed = cls._parse_as_str(raw, bedrock=bedrock)\n        elif isinstance(raw, dict):\n            parsed = cls._parse_as_dict(raw, bedrock=bedrock)\n        else:\n            raise TypeError(f\"Expected list, string or dict data, got {raw.__class__!r} ({raw!r}), report this!\")\n\n        return cls(parsed, original_raw, bedrock)\n\n    @staticmethod\n    def _parse_as_str(raw: str, *, bedrock: bool = False) -> list[ParsedMotdComponent]:\n        \"\"\"Parse a MOTD when it's string.\n\n        .. note:: This method returns a lot of empty strings, use :meth:`Motd.simplify` to remove them.\n\n        :param raw: Raw MOTD, directly from server.\n        :param bedrock: Is server Bedrock Edition?\n            Ignores :attr:`MinecraftColor.MINECOIN_GOLD` if it's :obj:`False`.\n        :returns: :obj:`ParsedMotdComponent` list, which need to be passed to ``__init__``.\n        \"\"\"\n        parsed_motd: list[ParsedMotdComponent] = []\n\n        split_raw = _MOTD_COLORS_RE.split(raw)\n        for element in split_raw:\n            clean_element = element.lstrip(\"&§\").lower()\n            standardized_element = element.replace(\"&\", \"§\").lower()\n\n            if standardized_element == \"§g\" and not bedrock:\n                parsed_motd.append(element)  # minecoin_gold on java server, treat as string\n                continue\n\n            if standardized_element.startswith(\"§\"):\n                try:\n                    parsed_motd.append(MinecraftColor(clean_element))\n                except ValueError:\n                    try:\n                        parsed_motd.append(Formatting(clean_element))\n                    except ValueError:\n                        # just a text\n                        parsed_motd.append(element)\n            else:\n                parsed_motd.append(element)\n\n        return parsed_motd\n\n    @classmethod\n    def _parse_as_dict(\n        cls,\n        item: RawJavaResponseMotdWhenDict,\n        *,\n        bedrock: bool = False,\n        auto_add: list[ParsedMotdComponent] | None = None,\n    ) -> list[ParsedMotdComponent]:\n        \"\"\"Parse a MOTD when it's dict.\n\n        :param item: :class:`dict` directly from the server.\n        :param bedrock: Is the server Bedrock Edition?\n            Nothing does here, just going to :meth:`._parse_as_str` while parsing ``text`` field.\n        :param auto_add: Values to add on this item.\n            Most time, this is :class:`Formatting` from top level.\n        :returns: :obj:`ParsedMotdComponent` list, which need to be passed to ``__init__``.\n        \"\"\"\n        parsed_motd: list[ParsedMotdComponent] = auto_add if auto_add is not None else []\n\n        if (color := item.get(\"color\")) is not None:\n            parsed_motd.append(cls._parse_color(color))\n\n        for style_key, style_val in Formatting.__members__.items():\n            lowered_style_key = style_key.lower()\n            if item.get(lowered_style_key) is False:\n                try:\n                    parsed_motd.remove(style_val)\n                except ValueError:\n                    # some servers set the formatting keys to false here, even without it ever being set to true before\n                    continue\n            elif item.get(lowered_style_key) is not None:\n                parsed_motd.append(style_val)\n\n        if (text := item.get(\"text\")) is not None:\n            parsed_motd.extend(cls._parse_as_str(text, bedrock=bedrock))\n        if (translate := item.get(\"translate\")) is not None:\n            parsed_motd.append(TranslationTag(translate))\n        parsed_motd.append(Formatting.RESET)\n\n        if \"extra\" in item:\n            auto_add = list(filter(lambda e: type(e) is Formatting and e != Formatting.RESET, parsed_motd))\n\n            for element in item[\"extra\"]:\n                parsed_motd.extend(\n                    cls._parse_as_dict(element, auto_add=auto_add.copy())\n                    if isinstance(element, dict)\n                    else auto_add + cls._parse_as_str(element, bedrock=bedrock)\n                )\n\n        return parsed_motd\n\n    @staticmethod\n    def _parse_color(color: str) -> ParsedMotdComponent:\n        \"\"\"Parse a color string.\"\"\"\n        try:\n            return MinecraftColor[color.upper()]\n        except KeyError:\n            if color == \"reset\":\n                # Minecraft servers actually can't return {\"reset\": True}, instead, they treat\n                # reset as a color and set {\"color\": \"reset\"}. However logically, reset is\n                # a formatting, and it resets both color and other formatting, so we use\n                # `Formatting.RESET` here.\n                #\n                # see `color` field in\n                # https://minecraft.wiki/w/Java_Edition_protocol/Chat?oldid=2763811#Shared_between_all_components\n                return Formatting.RESET\n\n            # Last attempt: try parsing as HTML (hex rgb) color. Some servers use these to\n            # achieve gradients.\n            try:\n                return WebColor.from_hex(color)\n            except ValueError as e:\n                raise ValueError(f\"Unable to parse color: {color!r}, report this!\") from e\n\n    def simplify(self) -> Self:\n        \"\"\"Create new MOTD without unused elements.\n\n        After parsing, the MOTD may contain some unused elements, like empty strings, or formatting/colors\n        that don't apply to anything. This method is responsible for creating a new motd with all such elements\n        removed, providing a much cleaner representation.\n\n        :returns: New simplified MOTD, with any unused elements removed.\n        \"\"\"\n        parsed = self.parsed.copy()\n        old_parsed: list[ParsedMotdComponent] | None = None\n\n        while parsed != old_parsed:\n            old_parsed = parsed.copy()\n            unused_elements = get_unused_elements(parsed)\n            parsed = [el for index, el in enumerate(parsed) if index not in unused_elements]\n\n        parsed = squash_nearby_strings(parsed)\n        return self.__class__(parsed, self.raw, bedrock=self.bedrock)\n\n    def to_plain(self) -> str:\n        \"\"\"Get plain text from a MOTD, without any colors/formatting.\n\n        Example:\n            ``&0Hello &oWorld`` turns into ``Hello World``.\n        \"\"\"\n        return PlainTransformer().transform(self.parsed)\n\n    def to_minecraft(self) -> str:\n        \"\"\"Transform MOTD to the Minecraft representation.\n\n        .. note:: This will always use ``§``, even if in original MOTD used ``&``.\n\n        Example:\n            .. code-block:: python\n\n                >>> Motd.parse(\"&0Hello &oWorld\")\n                \"§0Hello §oWorld\"\n        \"\"\"\n        return MinecraftTransformer().transform(self.parsed)\n\n    def to_html(self) -> str:\n        \"\"\"Transform MOTD to the HTML format.\n\n        The result is always wrapped in a ``<p>`` tag, if you need to remove it,\n        just do ``result.removeprefix(\"<p>\").removesuffix(\"</p>\")``.\n\n        .. note::\n            You should implement the \"obfuscated\" CSS class yourself using this snippet:\n\n            .. code-block:: javascript\n\n                const obfuscatedCharacters =\n                  \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`~!@#$%^&*()-_=+[]\\\\\"';:<>,./?\";\n                const obfuscatedElems = document.querySelectorAll(\".obfuscated\");\n\n                if (obfuscatedElems !== undefined) {\n                  const render = () => {\n                    obfuscatedElems.forEach((elem) => {\n                      let value = \"\";\n\n                      for (let i = 0, l = elem.innerText.length; i < l; i++) {\n                        value += obfuscatedCharacters.charAt(\n                          Math.floor(Math.random() * obfuscatedCharacters.length),\n                        );\n                      }\n\n                      elem.innerText = value;\n                    });\n                    setTimeout(render, 50);\n                  };\n                  render();\n                }\n\n            Also do note that this formatting does not make sense with\n            non-monospace fonts.\n\n        Example:\n            ``&6Hello&o from &rAnother &kWorld`` turns into\n\n            .. code-block:: html\n\n                <!-- there are no new lines in the actual output, those are added for readability -->\n                <p>\n                 <span style='color:rgb(255, 170, 0);text-shadow:0 0 1px rgb(42, 42, 0)'>\n                  Hello<i> from </span></i>\n                  Another <span class=obfuscated>World</span>\n                </p>\n        \"\"\"  # noqa: D301 # Use `r\"\"\"` if any backslashes in a docstring\n        return HtmlTransformer(bedrock=self.bedrock).transform(self.parsed)\n\n    def to_ansi(self) -> str:\n        \"\"\"Transform MOTD to the ANSI 24-bit format.\n\n        ANSI is mostly used for printing colored text in the terminal.\n\n        \"Obfuscated\" formatting (``&k``) is shown as a blinking one.\n\n        .. seealso:: https://en.wikipedia.org/wiki/ANSI_escape_code.\n        \"\"\"\n        return AnsiTransformer(bedrock=self.bedrock).transform(self.parsed)\n"
  },
  {
    "path": "mcstatus/motd/_simplifies.py",
    "content": "from __future__ import annotations\n\nimport typing as t\n\nfrom mcstatus.motd.components import Formatting, MinecraftColor, ParsedMotdComponent, WebColor\n\nif t.TYPE_CHECKING:\n    from collections.abc import Sequence\n\n__all__ = [\n    \"get_double_colors\",\n    \"get_double_items\",\n    \"get_empty_text\",\n    \"get_end_non_text\",\n    \"get_formatting_before_color\",\n    \"get_meaningless_resets_and_colors\",\n    \"get_unused_elements\",\n    \"squash_nearby_strings\",\n]\n\n_PARSED_MOTD_COMPONENTS_TYPEVAR = t.TypeVar(\"_PARSED_MOTD_COMPONENTS_TYPEVAR\", bound=\"list[ParsedMotdComponent]\")\n\n\ndef get_unused_elements(parsed: Sequence[ParsedMotdComponent]) -> set[int]:\n    \"\"\"Get indices of all items which are unused and can be safely removed from the MOTD.\n\n    This is a wrapper method around several unused item collection methods.\n    \"\"\"\n    to_remove: set[int] = set()\n\n    for simplifier in [\n        get_double_items,\n        get_double_colors,\n        get_formatting_before_color,\n        get_meaningless_resets_and_colors,\n        get_empty_text,\n        get_end_non_text,\n    ]:\n        to_remove.update(simplifier(parsed))\n\n    return to_remove\n\n\ndef squash_nearby_strings(parsed: _PARSED_MOTD_COMPONENTS_TYPEVAR) -> _PARSED_MOTD_COMPONENTS_TYPEVAR:\n    \"\"\"Squash duplicate strings together.\n\n    Note that this function doesn't create a copy of passed array, it modifies it.\n    This is what those typevars are for in the function signature.\n    \"\"\"\n    # in order to not break indexes, we need to fill values and then remove them after the loop\n    fillers: set[int] = set()\n    for index, item in enumerate(parsed):\n        if not isinstance(item, str):\n            continue\n\n        try:\n            next_item = parsed[index + 1]\n        except IndexError:  # Last item (without any next item)\n            break\n\n        if isinstance(next_item, str):\n            parsed[index + 1] = item + next_item\n            fillers.add(index)\n\n    for already_removed, index_to_remove in enumerate(fillers):\n        parsed.pop(index_to_remove - already_removed)\n\n    return parsed\n\n\ndef get_double_items(parsed: Sequence[ParsedMotdComponent]) -> set[int]:\n    \"\"\"Get indices of all doubled items that can be removed.\n\n    Removes any items that are followed by an item of the same kind (compared using ``__eq__``).\n    \"\"\"\n    to_remove: set[int] = set()\n\n    for index, item in enumerate(parsed):\n        try:\n            next_item = parsed[index + 1]\n        except IndexError:  # Last item (without any next item)\n            break\n\n        if isinstance(item, (Formatting, MinecraftColor, WebColor)) and item == next_item:\n            to_remove.add(index)\n\n    return to_remove\n\n\ndef get_double_colors(parsed: Sequence[ParsedMotdComponent]) -> set[int]:\n    \"\"\"Get indices of all doubled color items.\n\n    As colors (obviously) override each other, we only ever care about the last one, ignore\n    the previous ones. (for example: specifying red color, then orange, then yellow, then some text\n    will just result in yellow text)\n    \"\"\"\n    to_remove: set[int] = set()\n\n    prev_color: int | None = None\n    for index, item in enumerate(parsed):\n        if isinstance(item, (MinecraftColor, WebColor)):\n            # If we found a color after another, remove the previous color\n            if prev_color is not None:\n                to_remove.add(prev_color)\n            prev_color = index\n\n        # If we find a string, that's what our color we found previously applies to,\n        # set prev_color to None, marking this color as used\n        if isinstance(item, str):\n            prev_color = None\n\n    return to_remove\n\n\ndef get_formatting_before_color(parsed: Sequence[ParsedMotdComponent]) -> set[int]:\n    \"\"\"Obtain indices of all unused formatting items before colors.\n\n    Colors override any formatting before them, meaning we only ever care about the color, and can\n    ignore all formatting before it. (For example: specifying bold formatting, then italic, then yellow,\n    will just result in yellow text.)\n    \"\"\"\n    to_remove: set[int] = set()\n\n    collected_formattings = []\n    for index, item in enumerate(parsed):\n        # Collect the indices of formatting items\n        if isinstance(item, Formatting):\n            collected_formattings.append(index)\n\n        # Only run checks if we have some collected formatting items\n        if len(collected_formattings) == 0:\n            continue\n\n        # If there's a string after some formattings, the formattings apply to it.\n        # This means they're not unused, remove them.\n        if isinstance(item, str) and not item.isspace():\n            collected_formattings = []\n            continue\n\n        # If there's a color after some formattings, these formattings will be overridden\n        # as colors reset everything. This makes these formattings pointless, mark them\n        # for removal.\n        if isinstance(item, (MinecraftColor, WebColor)):\n            to_remove.update(collected_formattings)\n            collected_formattings = []\n    return to_remove\n\n\ndef get_empty_text(parsed: Sequence[ParsedMotdComponent]) -> set[int]:\n    \"\"\"Get indices of all empty text items.\n\n    Empty strings in motd serve no purpose and can be marked for removal.\n    \"\"\"\n    to_remove: set[int] = set()\n\n    for index, item in enumerate(parsed):\n        if isinstance(item, str) and len(item) == 0:\n            to_remove.add(index)\n\n    return to_remove\n\n\ndef get_end_non_text(parsed: Sequence[ParsedMotdComponent]) -> set[int]:\n    \"\"\"Get indices of all trailing items, found after the last text component.\n\n    Any color/formatting items only make sense when they apply to some text.\n    If there are some at the end, after the last text, they're pointless and\n    can be removed.\n    \"\"\"\n    to_remove: set[int] = set()\n\n    for rev_index, item in enumerate(reversed(parsed)):\n        # The moment we find our last string, stop the loop\n        if isinstance(item, str):\n            break\n\n        # Remove any color/formatting that doesn't apply to text\n        if isinstance(item, (MinecraftColor, WebColor, Formatting)):\n            index = len(parsed) - 1 - rev_index\n            to_remove.add(index)\n\n    return to_remove\n\n\ndef get_meaningless_resets_and_colors(parsed: Sequence[ParsedMotdComponent]) -> set[int]:\n    to_remove: set[int] = set()\n\n    active_color: MinecraftColor | WebColor | None = None\n    active_formatting: Formatting | None = None\n    for index, item in enumerate(parsed):\n        if isinstance(item, (MinecraftColor, WebColor)):\n            if active_color == item:\n                to_remove.add(index)\n            active_color = item\n            continue\n        if isinstance(item, Formatting):\n            if item == Formatting.RESET:\n                if active_color is None and active_formatting is None:\n                    to_remove.add(index)\n                    continue\n                active_color, active_formatting = None, None\n                continue\n            if active_formatting == item:\n                to_remove.add(index)\n            active_formatting = item\n\n    return to_remove\n"
  },
  {
    "path": "mcstatus/motd/_transformers.py",
    "content": "from __future__ import annotations\n\nimport abc\nimport typing as t\nfrom collections.abc import Callable, Sequence\n\nfrom mcstatus.motd.components import Formatting, MinecraftColor, ParsedMotdComponent, TranslationTag, WebColor\n\n__all__ = [\n    \"AnsiTransformer\",\n    \"HtmlTransformer\",\n    \"MinecraftTransformer\",\n    \"PlainTransformer\",\n]\n\nif t.TYPE_CHECKING:\n    from collections.abc import Callable, Sequence\n\n_HOOK_RETURN_TYPE = t.TypeVar(\"_HOOK_RETURN_TYPE\")\n_END_RESULT_TYPE = t.TypeVar(\"_END_RESULT_TYPE\")\n\n# MinecraftColor: (foreground, background) # noqa: ERA001 # commented-out code\n_SHARED_MINECRAFT_COLOR_TO_RGB = {\n    MinecraftColor.BLACK: ((0, 0, 0), (0, 0, 0)),\n    MinecraftColor.DARK_BLUE: ((0, 0, 170), (0, 0, 42)),\n    MinecraftColor.DARK_GREEN: ((0, 170, 0), (0, 42, 0)),\n    MinecraftColor.DARK_AQUA: ((0, 170, 170), (0, 42, 42)),\n    MinecraftColor.DARK_RED: ((170, 0, 0), (42, 0, 0)),\n    MinecraftColor.DARK_PURPLE: ((170, 0, 170), (42, 0, 42)),\n    MinecraftColor.GOLD: ((255, 170, 0), (64, 42, 0)),\n    MinecraftColor.GRAY: ((170, 170, 170), (42, 42, 42)),\n    MinecraftColor.DARK_GRAY: ((85, 85, 85), (21, 21, 21)),\n    MinecraftColor.BLUE: ((85, 85, 255), (21, 21, 63)),\n    MinecraftColor.GREEN: ((85, 255, 85), (21, 63, 21)),\n    MinecraftColor.AQUA: ((85, 255, 255), (21, 63, 63)),\n    MinecraftColor.RED: ((255, 85, 85), (63, 21, 21)),\n    MinecraftColor.LIGHT_PURPLE: ((255, 85, 255), (63, 21, 63)),\n    MinecraftColor.YELLOW: ((255, 255, 85), (63, 63, 21)),\n    MinecraftColor.WHITE: ((255, 255, 255), (63, 63, 63)),\n}\n\n_MINECRAFT_COLOR_TO_RGB_JAVA = _SHARED_MINECRAFT_COLOR_TO_RGB.copy()\n_MINECRAFT_COLOR_TO_RGB_JAVA[MinecraftColor.GRAY] = ((170, 170, 170), (42, 42, 42))\n\n_MINECRAFT_COLOR_TO_RGB_BEDROCK = _SHARED_MINECRAFT_COLOR_TO_RGB.copy()\n_MINECRAFT_COLOR_TO_RGB_BEDROCK.update(\n    {\n        MinecraftColor.GRAY: ((198, 198, 198), (49, 49, 49)),\n        MinecraftColor.MINECOIN_GOLD: ((221, 214, 5), (55, 53, 1)),\n        MinecraftColor.MATERIAL_QUARTZ: ((227, 212, 209), (56, 53, 52)),\n        MinecraftColor.MATERIAL_IRON: ((206, 202, 202), (51, 50, 50)),\n        MinecraftColor.MATERIAL_NETHERITE: ((68, 58, 59), (17, 14, 14)),\n        MinecraftColor.MATERIAL_REDSTONE: ((151, 22, 7), (37, 5, 1)),\n        MinecraftColor.MATERIAL_COPPER: ((180, 104, 77), (45, 26, 19)),\n        MinecraftColor.MATERIAL_GOLD: ((222, 177, 45), (55, 44, 11)),\n        MinecraftColor.MATERIAL_EMERALD: ((17, 159, 54), (4, 40, 13)),\n        MinecraftColor.MATERIAL_DIAMOND: ((44, 186, 168), (11, 46, 42)),\n        MinecraftColor.MATERIAL_LAPIS: ((33, 73, 123), (8, 18, 30)),\n        MinecraftColor.MATERIAL_AMETHYST: ((154, 92, 198), (38, 23, 49)),\n        MinecraftColor.MATERIAL_RESIN: ((235, 114, 20), (59, 29, 5)),\n    }\n)\n\n\nclass _BaseTransformer(abc.ABC, t.Generic[_HOOK_RETURN_TYPE, _END_RESULT_TYPE]):\n    \"\"\"Base MOTD transformer class.\n\n    Transformers are responsible for providing a way to generate an alternative\n    representation of MOTD, for example, as HTML.\n\n    The methods ``_handle_*`` handle each\n    :type:`~mcstatus.motd.components.ParsedMotdComponent` individually.\n    \"\"\"\n\n    def transform(self, motd_components: Sequence[ParsedMotdComponent]) -> _END_RESULT_TYPE:\n        return self._format_output([handled for component in motd_components for handled in self._handle_component(component)])\n\n    @abc.abstractmethod\n    def _format_output(self, results: list[_HOOK_RETURN_TYPE]) -> _END_RESULT_TYPE: ...\n\n    def _handle_component(\n        self, component: ParsedMotdComponent\n    ) -> tuple[_HOOK_RETURN_TYPE, _HOOK_RETURN_TYPE] | tuple[_HOOK_RETURN_TYPE]:\n        handler: Callable[[ParsedMotdComponent], _HOOK_RETURN_TYPE] = {\n            MinecraftColor: self._handle_minecraft_color,\n            WebColor: self._handle_web_color,\n            Formatting: self._handle_formatting,\n            TranslationTag: self._handle_translation_tag,\n            str: self._handle_str,\n        }[type(component)]\n\n        additional = None\n        if isinstance(component, MinecraftColor):\n            additional = self._handle_formatting(Formatting.RESET)\n\n        return (additional, handler(component)) if additional is not None else (handler(component),)\n\n    @abc.abstractmethod\n    def _handle_str(self, element: str, /) -> _HOOK_RETURN_TYPE: ...\n\n    @abc.abstractmethod\n    def _handle_translation_tag(self, _: TranslationTag, /) -> _HOOK_RETURN_TYPE: ...\n\n    @abc.abstractmethod\n    def _handle_web_color(self, element: WebColor, /) -> _HOOK_RETURN_TYPE: ...\n\n    @abc.abstractmethod\n    def _handle_formatting(self, element: Formatting, /) -> _HOOK_RETURN_TYPE: ...\n\n    @abc.abstractmethod\n    def _handle_minecraft_color(self, element: MinecraftColor, /) -> _HOOK_RETURN_TYPE: ...\n\n\nclass _NothingTransformer(_BaseTransformer[str, str]):\n    \"\"\"Transformer that transforms all elements into empty strings.\n\n    This transformer acts as a base for other transformers with string result type.\n    \"\"\"\n\n    def _format_output(self, results: list[str]) -> str:\n        return \"\".join(results)\n\n    def _handle_str(self, _element: str, /) -> str:\n        return \"\"\n\n    def _handle_minecraft_color(self, _element: MinecraftColor, /) -> str:\n        return \"\"\n\n    def _handle_web_color(self, _element: WebColor, /) -> str:\n        return \"\"\n\n    def _handle_formatting(self, _element: Formatting, /) -> str:\n        return \"\"\n\n    def _handle_translation_tag(self, _element: TranslationTag, /) -> str:\n        return \"\"\n\n\nclass PlainTransformer(_NothingTransformer):\n    def _handle_str(self, element: str, /) -> str:\n        return element\n\n\nclass MinecraftTransformer(PlainTransformer):\n    def _handle_component(self, component: ParsedMotdComponent) -> tuple[str, str] | tuple[str]:\n        result = super()._handle_component(component)\n        if len(result) == 2:\n            return (result[1],)\n        return result\n\n    def _handle_minecraft_color(self, element: MinecraftColor, /) -> str:\n        return \"§\" + element.value\n\n    def _handle_formatting(self, element: Formatting, /) -> str:\n        return \"§\" + element.value\n\n\nclass HtmlTransformer(PlainTransformer):\n    _FORMATTING_TO_HTML_TAGS: t.ClassVar = {\n        Formatting.BOLD: \"b\",\n        Formatting.STRIKETHROUGH: \"s\",\n        Formatting.ITALIC: \"i\",\n        Formatting.UNDERLINED: \"u\",\n    }\n\n    # TODO: When dropping v13 support, make sure to drop the default value for the bedrock arg\n    def __init__(self, *, bedrock: bool = False) -> None:\n        self.bedrock = bedrock\n        self.on_reset: list[str] = []\n\n    def transform(self, motd_components: Sequence[ParsedMotdComponent]) -> str:\n        self.on_reset = []\n        return super().transform(motd_components)\n\n    def _format_output(self, results: list[str]) -> str:\n        return \"<p>\" + super()._format_output(results) + \"\".join(self.on_reset) + \"</p>\"\n\n    def _handle_str(self, element: str, /) -> str:\n        return element.replace(\"\\n\", \"<br>\")\n\n    def _handle_minecraft_color(self, element: MinecraftColor, /) -> str:\n        color_map = _MINECRAFT_COLOR_TO_RGB_BEDROCK if self.bedrock else _MINECRAFT_COLOR_TO_RGB_JAVA\n        fg_color, bg_color = color_map[element]\n\n        self.on_reset.append(\"</span>\")\n        return f\"<span style='color:rgb{fg_color};text-shadow:0 0 1px rgb{bg_color}'>\"\n\n    def _handle_web_color(self, element: WebColor, /) -> str:\n        self.on_reset.append(\"</span>\")\n        return f\"<span style='color:rgb{element.rgb}'>\"\n\n    def _handle_formatting(self, element: Formatting, /) -> str:\n        if element is Formatting.RESET:\n            to_return = \"\".join(self.on_reset)\n            self.on_reset = []\n            return to_return\n\n        if element is Formatting.OBFUSCATED:\n            self.on_reset.append(\"</span>\")\n            return \"<span class=obfuscated>\"\n\n        tag_name = self._FORMATTING_TO_HTML_TAGS[element]\n        self.on_reset.append(f\"</{tag_name}>\")\n        return f\"<{tag_name}>\"\n\n\nclass AnsiTransformer(PlainTransformer):\n    _FORMATTING_TO_ANSI_TAGS: t.ClassVar = {\n        Formatting.BOLD: \"1\",\n        Formatting.STRIKETHROUGH: \"9\",\n        Formatting.ITALIC: \"3\",\n        Formatting.UNDERLINED: \"4\",\n        Formatting.OBFUSCATED: \"5\",\n    }\n    _MINECRAFT_COLOR_TO_RGB_JAVA: t.ClassVar = {\n        key: foreground for key, (foreground, _background) in _MINECRAFT_COLOR_TO_RGB_JAVA.items()\n    }\n    _MINECRAFT_COLOR_TO_RGB_BEDROCK: t.ClassVar = {\n        key: foreground for key, (foreground, _background) in _MINECRAFT_COLOR_TO_RGB_BEDROCK.items()\n    }\n\n    # TODO: When dropping v13 support, make sure to drop the default value for the bedrock arg\n    def __init__(self, *, bedrock: bool = True) -> None:\n        self.bedrock = bedrock\n\n    def ansi_color(self, color: tuple[int, int, int] | MinecraftColor) -> str:\n        \"\"\"Transform RGB color to ANSI color code.\"\"\"\n        if isinstance(color, MinecraftColor):\n            color_to_rgb = self._MINECRAFT_COLOR_TO_RGB_BEDROCK if self.bedrock else self._MINECRAFT_COLOR_TO_RGB_JAVA\n            color = color_to_rgb[color]\n\n        return \"\\033[38;2;{};{};{}m\".format(*color)\n\n    def _format_output(self, results: list[str]) -> str:\n        return \"\\033[0m\" + super()._format_output(results) + \"\\033[0m\"\n\n    def _handle_minecraft_color(self, element: MinecraftColor, /) -> str:\n        return self.ansi_color(element)\n\n    def _handle_web_color(self, element: WebColor, /) -> str:\n        return self.ansi_color(element.rgb)\n\n    def _handle_formatting(self, element: Formatting, /) -> str:\n        if element is Formatting.RESET:\n            return \"\\033[0m\"\n        return \"\\033[\" + self._FORMATTING_TO_ANSI_TAGS[element] + \"m\"\n"
  },
  {
    "path": "mcstatus/motd/components.py",
    "content": "from __future__ import annotations\n\nimport typing as t\nfrom dataclasses import dataclass\nfrom enum import Enum\n\nif t.TYPE_CHECKING:\n    from typing_extensions import Self\n\n__all__ = [\n    \"Formatting\",\n    \"MinecraftColor\",\n    \"ParsedMotdComponent\",\n    \"TranslationTag\",\n    \"WebColor\",\n]\n\n\n# NOTE: keep in sync with the definition in docs (`docs/api/motd_parsing.rst`)\n# the autodocs plugin does not support type aliases yet, so those have to be\n# defined manually in docs\nParsedMotdComponent: t.TypeAlias = \"Formatting | MinecraftColor | WebColor | TranslationTag | str\"\n\n\nclass Formatting(Enum):\n    \"\"\"Enum for Formatting codes.\n\n    See `Minecraft wiki <https://minecraft.wiki/w/Formatting_codes#Formatting_codes>`__\n    for more info.\n\n    .. note::\n        :attr:`.STRIKETHROUGH` and :attr:`.UNDERLINED` don't work on Bedrock, which our parser\n        doesn't keep it in mind. See `MCPE-41729 <https://bugs.mojang.com/browse/MCPE-41729>`_.\n    \"\"\"\n\n    BOLD = \"l\"\n    ITALIC = \"o\"\n    UNDERLINED = \"n\"\n    STRIKETHROUGH = \"m\"\n    OBFUSCATED = \"k\"\n    RESET = \"r\"\n\n\nclass MinecraftColor(Enum):\n    \"\"\"Enum for Color codes.\n\n    See `Minecraft wiki <https://minecraft.wiki/w/Formatting_codes#Color_codes>`_\n    for more info.\n    \"\"\"\n\n    BLACK = \"0\"\n    DARK_BLUE = \"1\"\n    DARK_GREEN = \"2\"\n    DARK_AQUA = \"3\"\n    DARK_RED = \"4\"\n    DARK_PURPLE = \"5\"\n    GOLD = \"6\"\n    GRAY = \"7\"\n    DARK_GRAY = \"8\"\n    BLUE = \"9\"\n    GREEN = \"a\"\n    AQUA = \"b\"\n    RED = \"c\"\n    LIGHT_PURPLE = \"d\"\n    YELLOW = \"e\"\n    WHITE = \"f\"\n\n    # Only for bedrock\n    MINECOIN_GOLD = \"g\"\n    MATERIAL_QUARTZ = \"h\"\n    MATERIAL_IRON = \"i\"\n    MATERIAL_NETHERITE = \"j\"\n    MATERIAL_REDSTONE = \"m\"\n    MATERIAL_COPPER = \"n\"\n    MATERIAL_GOLD = \"p\"\n    MATERIAL_EMERALD = \"q\"\n    MATERIAL_DIAMOND = \"s\"\n    MATERIAL_LAPIS = \"t\"\n    MATERIAL_AMETHYST = \"u\"\n    MATERIAL_RESIN = \"v\"\n\n\n@dataclass(frozen=True)\nclass WebColor:\n    \"\"\"Raw HTML color from MOTD.\n\n    Can be found in MOTD when someone uses gradient.\n\n    .. note:: Actually supported in Minecraft 1.16+ only.\n    \"\"\"\n\n    hex: str\n    rgb: tuple[int, int, int]\n\n    @classmethod\n    def from_hex(cls, hex: str) -> Self:  # noqa: A002 # shadowing a hex builtin\n        \"\"\"Construct web color using hex color string.\n\n        :raises ValueError: Invalid hex color string.\n        :returns: New :class:`WebColor` instance.\n        \"\"\"\n        hex = hex.lstrip(\"#\")  # noqa: A001 # shadowing a hex builtin\n\n        if len(hex) not in (3, 6):\n            raise ValueError(f\"Got too long/short hex color: {'#' + hex!r}\")\n        if len(hex) == 3:\n            hex = \"{0}{0}{1}{1}{2}{2}\".format(*hex)  # noqa: A001 # shadowing a hex builtin\n\n        try:\n            rgb = t.cast(\"tuple[int, int, int]\", tuple(int(hex[i : i + 2], 16) for i in (0, 2, 4)))\n        except ValueError as e:\n            raise ValueError(f\"Failed to parse given hex color: {'#' + hex!r}\") from e\n\n        return cls.from_rgb(rgb)\n\n    @classmethod\n    def from_rgb(cls, rgb: tuple[int, int, int]) -> Self:\n        \"\"\"Construct web color using rgb color tuple.\n\n        :raises ValueError: When RGB color is out of its 8-bit range.\n        :returns: New :class:`WebColor` instance.\n        \"\"\"\n        cls._check_rgb(rgb)\n\n        hex = \"#{:02x}{:02x}{:02x}\".format(*rgb)  # noqa: A001 # shadowing a hex builtin\n        return cls(hex, rgb)\n\n    @staticmethod\n    def _check_rgb(rgb: tuple[int, int, int]) -> None:\n        index_to_color_name = {0: \"red\", 1: \"green\", 2: \"blue\"}\n\n        for index, value in enumerate(rgb):\n            if not 255 >= value >= 0:\n                color_name = index_to_color_name[index]\n                raise ValueError(f\"RGB color byte out of its 8-bit range (0-255) for {color_name} ({value=})\")\n\n\n@dataclass(frozen=True)\nclass TranslationTag:\n    \"\"\"Represents a ``translate`` field in server's answer.\n\n    This just exists, but is completely ignored by our transformers.\n    You can find translation tags in :attr:`Motd.parsed <mcstatus.motd.Motd.parsed>` attribute.\n\n    .. seealso:: `Minecraft's wiki. <https://minecraft.wiki/w/Raw_JSON_text_format#Translated_Text>`__\n    \"\"\"\n\n    id: str\n"
  },
  {
    "path": "mcstatus/py.typed",
    "content": ""
  },
  {
    "path": "mcstatus/responses/__init__.py",
    "content": "from mcstatus.responses.base import BaseStatusPlayers, BaseStatusResponse, BaseStatusVersion\nfrom mcstatus.responses.bedrock import BedrockStatusPlayers, BedrockStatusResponse, BedrockStatusVersion\nfrom mcstatus.responses.forge import ForgeData, ForgeDataChannel, ForgeDataMod\nfrom mcstatus.responses.java import JavaStatusPlayer, JavaStatusPlayers, JavaStatusResponse, JavaStatusVersion\nfrom mcstatus.responses.legacy import LegacyStatusPlayers, LegacyStatusResponse, LegacyStatusVersion\nfrom mcstatus.responses.query import QueryPlayers, QueryResponse, QuerySoftware\n\n__all__ = [\n    \"BaseStatusPlayers\",\n    \"BaseStatusResponse\",\n    \"BaseStatusVersion\",\n    \"BedrockStatusPlayers\",\n    \"BedrockStatusResponse\",\n    \"BedrockStatusVersion\",\n    \"ForgeData\",\n    \"ForgeDataChannel\",\n    \"ForgeDataMod\",\n    \"JavaStatusPlayer\",\n    \"JavaStatusPlayers\",\n    \"JavaStatusResponse\",\n    \"JavaStatusVersion\",\n    \"LegacyStatusPlayers\",\n    \"LegacyStatusResponse\",\n    \"LegacyStatusVersion\",\n    \"QueryPlayers\",\n    \"QueryResponse\",\n    \"QuerySoftware\",\n]\n"
  },
  {
    "path": "mcstatus/responses/_raw.py",
    "content": "from __future__ import annotations\n\nfrom typing import Literal, TYPE_CHECKING, TypeAlias, TypedDict\n\nif TYPE_CHECKING:\n    from typing_extensions import NotRequired\n\n__all__ = [\n    \"RawForgeData\",\n    \"RawForgeDataChannel\",\n    \"RawForgeDataMod\",\n    \"RawJavaResponse\",\n    \"RawJavaResponseMotd\",\n    \"RawJavaResponseMotdWhenDict\",\n    \"RawJavaResponsePlayer\",\n    \"RawJavaResponsePlayers\",\n    \"RawJavaResponseVersion\",\n    \"RawQueryResponse\",\n]\n\nRawJavaResponseMotd: TypeAlias = \"RawJavaResponseMotdWhenDict | list[RawJavaResponseMotdWhenDict | str] | str\"\n\n\nclass RawForgeDataChannel(TypedDict):\n    res: str\n    \"\"\"Channel name and ID (for example ``fml:handshake``).\"\"\"\n    version: str\n    \"\"\"Channel version (for example ``1.2.3.4``).\"\"\"\n    required: bool\n    \"\"\"Is this channel required for client to join?\"\"\"\n\n\nclass RawForgeDataMod(TypedDict, total=False):\n    modid: str\n    modId: str\n    modmarker: str\n    \"\"\"Mod version.\"\"\"\n    version: str\n\n\nclass RawForgeData(TypedDict, total=False):\n    fmlNetworkVersion: int\n    channels: list[RawForgeDataChannel]\n    mods: list[RawForgeDataMod]\n    modList: list[RawForgeDataMod]\n    d: str\n    truncated: bool\n\n\nclass RawJavaResponsePlayer(TypedDict):\n    name: str\n    id: str\n\n\nclass RawJavaResponsePlayers(TypedDict):\n    online: int\n    max: int\n    sample: NotRequired[list[RawJavaResponsePlayer] | None]\n\n\nclass RawJavaResponseVersion(TypedDict):\n    name: str\n    protocol: int\n\n\nclass RawJavaResponseMotdWhenDict(TypedDict, total=False):\n    text: str  # only present if `translate` is set\n    translate: str  # same to the above field\n    extra: list[RawJavaResponseMotdWhenDict | str]\n\n    color: str\n    bold: bool\n    strikethrough: bool\n    italic: bool\n    underlined: bool\n    obfuscated: bool\n\n\nclass RawJavaResponse(TypedDict):\n    description: RawJavaResponseMotd\n    players: RawJavaResponsePlayers\n    version: RawJavaResponseVersion\n    favicon: NotRequired[str]\n    forgeData: NotRequired[RawForgeData | None]\n    modinfo: NotRequired[RawForgeData | None]\n    enforcesSecureChat: NotRequired[bool]\n\n\nclass RawQueryResponse(TypedDict):\n    hostname: str\n    gametype: Literal[\"SMP\"]\n    game_id: Literal[\"MINECRAFT\"]\n    version: str\n    plugins: str\n    map: str\n    numplayers: str  # can be transformed into `int`\n    maxplayers: str  # can be transformed into `int`\n    hostport: str  # can be transformed into `int`\n    hostip: str\n"
  },
  {
    "path": "mcstatus/responses/base.py",
    "content": "from __future__ import annotations\n\nfrom abc import ABC, abstractmethod\nfrom dataclasses import asdict, dataclass\nfrom typing import Any, TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from typing_extensions import Self\n\n    from mcstatus.motd import Motd\n\n__all__ = [\n    \"BaseStatusPlayers\",\n    \"BaseStatusResponse\",\n    \"BaseStatusVersion\",\n]\n\n\n@dataclass(frozen=True)\nclass BaseStatusResponse(ABC):\n    \"\"\"Class for storing shared data from a status response.\"\"\"\n\n    players: BaseStatusPlayers\n    \"\"\"The players information.\"\"\"\n    version: BaseStatusVersion\n    \"\"\"The version information.\"\"\"\n    motd: Motd\n    \"\"\"Message Of The Day. Also known as description.\n\n    .. seealso:: :doc:`/api/motd_parsing`.\n    \"\"\"\n    latency: float\n    \"\"\"Latency between a server and the client (you). In milliseconds.\"\"\"\n\n    @property\n    def description(self) -> str:\n        \"\"\"Alias to the :meth:`mcstatus.motd.Motd.to_minecraft` method.\"\"\"\n        return self.motd.to_minecraft()\n\n    @classmethod\n    @abstractmethod\n    def build(cls, *args: Any, **kwargs: Any) -> Self:\n        \"\"\"Build BaseStatusResponse and check is it valid.\n\n        :param args: Arguments in specific realisation.\n        :param kwargs: Keyword arguments in specific realisation.\n        :return: :class:`BaseStatusResponse` object.\n        \"\"\"\n        raise NotImplementedError(\"You can't use abstract methods.\")\n\n    def as_dict(self) -> dict[str, Any]:\n        \"\"\"Return the dataclass as JSON-serializable :class:`dict`.\n\n        Do note that this method doesn't return :class:`string <str>` but\n        :class:`dict`, so you can do some processing on returned value.\n\n        Difference from\n        :attr:`~mcstatus.responses.JavaStatusResponse.raw` is in that,\n        :attr:`~mcstatus.responses.JavaStatusResponse.raw` returns raw response\n        in the same format as we got it. This method returns the response\n        in a more user-friendly JSON serializable format (for example,\n        :attr:`~mcstatus.responses.BaseStatusResponse.motd` is returned as a\n        :func:`Minecraft string <mcstatus.motd.Motd.to_minecraft>` and not\n        :class:`dict`).\n        \"\"\"\n        as_dict = asdict(self)\n        as_dict[\"motd\"] = self.motd.simplify().to_minecraft()\n        return as_dict\n\n\n@dataclass(frozen=True)\nclass BaseStatusPlayers(ABC):\n    \"\"\"Class for storing information about players on the server.\"\"\"\n\n    online: int\n    \"\"\"Current number of online players.\"\"\"\n    max: int\n    \"\"\"The maximum allowed number of players (aka server slots).\"\"\"\n\n\n@dataclass(frozen=True)\nclass BaseStatusVersion(ABC):\n    \"\"\"A class for storing version information.\"\"\"\n\n    name: str\n    \"\"\"The version name, like ``1.19.3``.\n\n    See `Minecraft wiki <https://minecraft.wiki/w/Java_Edition_version_history#Full_release>`__\n    for complete list.\n    \"\"\"\n    protocol: int\n    \"\"\"The protocol version, like ``761``.\n\n    See `Minecraft wiki <https://minecraft.wiki/w/Protocol_version#Java_Edition_2>`__.\n    \"\"\"\n"
  },
  {
    "path": "mcstatus/responses/bedrock.py",
    "content": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import Any, TYPE_CHECKING\n\nfrom mcstatus._utils import deprecated\nfrom mcstatus.motd import Motd\nfrom mcstatus.responses.base import BaseStatusPlayers, BaseStatusResponse, BaseStatusVersion\n\nif TYPE_CHECKING:\n    from typing_extensions import Self\n\n__all__ = [\n    \"BedrockStatusPlayers\",\n    \"BedrockStatusResponse\",\n    \"BedrockStatusVersion\",\n]\n\n\n@dataclass(frozen=True)\nclass BedrockStatusResponse(BaseStatusResponse):\n    \"\"\"The response object for :meth:`BedrockServer.status() <mcstatus.server.BedrockServer.status>`.\"\"\"\n\n    players: BedrockStatusPlayers\n    version: BedrockStatusVersion\n    map_name: str | None\n    \"\"\"The name of the map.\"\"\"\n    gamemode: str | None\n    \"\"\"The name of the gamemode on the server.\"\"\"\n\n    @classmethod\n    def build(cls, decoded_data: list[Any], latency: float) -> Self:\n        \"\"\"Build BaseStatusResponse and check is it valid.\n\n        :param decoded_data: Raw decoded response object.\n        :param latency: Latency of the request.\n        :return: :class:`BedrockStatusResponse` object.\n        \"\"\"\n        try:\n            map_name = decoded_data[7]\n        except IndexError:\n            map_name = None\n        try:\n            gamemode = decoded_data[8]\n        except IndexError:\n            gamemode = None\n\n        return cls(\n            players=BedrockStatusPlayers(\n                online=int(decoded_data[4]),\n                max=int(decoded_data[5]),\n            ),\n            version=BedrockStatusVersion(\n                name=decoded_data[3],\n                protocol=int(decoded_data[2]),\n                brand=decoded_data[0],\n            ),\n            motd=Motd.parse(decoded_data[1], bedrock=True),\n            latency=latency,\n            map_name=map_name,\n            gamemode=gamemode,\n        )\n\n\n@dataclass(frozen=True)\nclass BedrockStatusPlayers(BaseStatusPlayers):\n    \"\"\"Class for storing information about players on the server.\"\"\"\n\n\n@dataclass(frozen=True)\nclass BedrockStatusVersion(BaseStatusVersion):\n    \"\"\"A class for storing version information.\"\"\"\n\n    name: str\n    \"\"\"The version name, like ``1.19.60``.\n\n    See `Minecraft wiki <https://minecraft.wiki/w/Bedrock_Edition_version_history#Bedrock_Edition>`__\n    for complete list.\n    \"\"\"\n    brand: str\n    \"\"\"``MCPE`` or ``MCEE`` for Education Edition.\"\"\"\n\n    @property\n    @deprecated(replacement=\"name\", removal_version=\"13.0.0\")\n    def version(self) -> str:\n        \"\"\"\n        .. deprecated:: 12.0.0\n            Will be removed in 13.0.0, use :attr:`.name` instead.\n        \"\"\"  # noqa: D205, D212 # no summary line\n        return self.name\n"
  },
  {
    "path": "mcstatus/responses/forge.py",
    "content": "\"\"\"Decoder for data from Forge, that is included into a response object.\n\nAfter 1.18.1, Forge started to compress its mod data into a\nUTF-16 string that represents binary data containing data like\nthe forge mod loader network version, a big list of channels\nthat all the forge mods use, and a list of mods the server has.\n\nBefore 1.18.1, the mod data was in `forgeData` attribute inside\na response object. We support this implementation too.\n\nFor more information see this file from forge itself:\nhttps://github.com/MinecraftForge/MinecraftForge/blob/54b08d2711a15418130694342a3fe9a5dfe005d2/src/main/java/net/minecraftforge/network/ServerStatusPing.java#L27-L73\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom io import StringIO\nfrom typing import Final, TYPE_CHECKING\n\nfrom mcstatus._protocol.connection import BaseConnection, BaseReadSync, Connection\nfrom mcstatus._utils import or_none\n\nif TYPE_CHECKING:\n    from typing_extensions import Self\n\n    from mcstatus.responses._raw import RawForgeData, RawForgeDataChannel, RawForgeDataMod\n\n__all__ = [\n    \"ForgeData\",\n    \"ForgeDataChannel\",\n    \"ForgeDataMod\",\n]\n\n_VERSION_FLAG_IGNORE_SERVER_ONLY: Final = 0b1\n_IGNORE_SERVER_ONLY: Final = \"<not required for client>\"\n\n\n@dataclass(frozen=True)\nclass ForgeDataChannel:\n    \"\"\"A single Forge data channel.\"\"\"\n\n    name: str\n    \"\"\"Channel name and ID (for example ``fml:handshake``).\"\"\"\n    version: str\n    \"\"\"Channel version (for example ``1.2.3.4``).\"\"\"\n    required: bool\n    \"\"\"Is this channel required for client to join?\"\"\"\n\n    @classmethod\n    def build(cls, raw: RawForgeDataChannel) -> Self:\n        \"\"\"Build an object about Forge channel from raw response.\n\n        :param raw: ``channel`` element in raw forge response :class:`dict`.\n        :return: :class:`ForgeDataChannel` object.\n        \"\"\"\n        return cls(name=raw[\"res\"], version=raw[\"version\"], required=raw[\"required\"])\n\n    @classmethod\n    def decode(cls, buffer: Connection, mod_id: str | None = None) -> Self:\n        \"\"\"Decode an object about Forge channel from decoded optimized buffer.\n\n        :param buffer: :class:`Connection` object from UTF-16 encoded binary data.\n        :param mod_id: Optional mod id prefix :class:`str`.\n        :return: :class:`ForgeDataChannel` object.\n        \"\"\"\n        channel_identifier = buffer.read_utf()\n        if mod_id is not None:\n            channel_identifier = f\"{mod_id}:{channel_identifier}\"\n        version = buffer.read_utf()\n        client_required = buffer.read_bool()\n\n        return cls(\n            name=channel_identifier,\n            version=version,\n            required=client_required,\n        )\n\n\n@dataclass(frozen=True)\nclass ForgeDataMod:\n    \"\"\"A single Forge mod.\"\"\"\n\n    name: str\n    \"\"\"A mod name.\"\"\"\n    marker: str\n    \"\"\"A mod marker. Usually a version.\"\"\"\n\n    @classmethod\n    def build(cls, raw: RawForgeDataMod) -> Self:\n        \"\"\"Build an object about Forge mod from raw response.\n\n        :param raw: ``mod`` element in raw forge response :class:`dict`.\n        :return: :class:`ForgeDataMod` object.\n        \"\"\"\n        # In FML v1, modmarker was version instead.\n        mod_version = or_none(raw.get(\"modmarker\"), raw.get(\"version\"))\n        if mod_version is None:\n            raise KeyError(f\"Mod version in Forge mod data must be provided. Mod info: {raw}\")\n\n        # In FML v2, modid was modId instead. At least one of the two should exist.\n        mod_id = or_none(raw.get(\"modid\"), raw.get(\"modId\"))\n        if mod_id is None:\n            raise KeyError(f\"Mod ID in Forge mod data must be provided. Mod info: {raw}.\")\n\n        return cls(name=mod_id, marker=mod_version)\n\n    @classmethod\n    def decode(cls, buffer: Connection) -> tuple[Self, list[ForgeDataChannel]]:\n        \"\"\"Decode data about a Forge mod from decoded optimized buffer.\n\n        :param buffer: :class:`Connection` object from UTF-16 encoded binary data.\n        :return: :class:`tuple` object of :class:`ForgeDataMod` object and :class:`list` of :class:`ForgeDataChannel` objects.\n        \"\"\"\n        channel_version_flags = buffer.read_varint()\n\n        channel_count = channel_version_flags >> 1\n        is_server = channel_version_flags & _VERSION_FLAG_IGNORE_SERVER_ONLY != 0\n        mod_id = buffer.read_utf()\n\n        mod_version = _IGNORE_SERVER_ONLY\n        if not is_server:\n            mod_version = buffer.read_utf()\n\n        channels = [ForgeDataChannel.decode(buffer, mod_id) for _ in range(channel_count)]\n\n        return cls(name=mod_id, marker=mod_version), channels\n\n\nclass _StringBuffer(BaseReadSync, BaseConnection):\n    \"\"\"String Buffer for reading utf-16 encoded binary data.\"\"\"\n\n    __slots__ = (\"received\", \"stringio\")\n\n    def __init__(self, stringio: StringIO) -> None:\n        self.stringio = stringio\n        self.received = bytearray()\n\n    def read(self, length: int) -> bytearray:\n        \"\"\"Read length bytes from ``self``, and return a byte array.\"\"\"\n        data = bytearray()\n        while self.received and len(data) < length:\n            data.append(self.received.pop(0))\n        while len(data) < length:\n            result = self.stringio.read(1)\n            if not result:\n                raise OSError(f\"Not enough data to read! {len(data)} < {length}\")\n            data.extend(result.encode(\"utf-16be\"))\n        while len(data) > length:\n            self.received.append(data.pop())\n        return data\n\n    def remaining(self) -> int:\n        \"\"\"Return number of reads remaining.\"\"\"\n        return len(self.stringio.getvalue()) - self.stringio.tell() + len(self.received)\n\n    def read_optimized_size(self) -> int:\n        \"\"\"Read encoded data length.\"\"\"\n        return self.read_short() | (self.read_short() << 15)\n\n    def read_optimized_buffer(self) -> Connection:\n        \"\"\"Read encoded buffer.\"\"\"\n        size = self.read_optimized_size()\n\n        buffer = Connection()\n        value, bits = 0, 0\n        while buffer.remaining() < size:\n            if bits < 8 and self.remaining():\n                # Ignoring sign bit\n                value |= (self.read_short() & 0x7FFF) << bits\n                bits += 15\n            buffer.receive((value & 0xFF).to_bytes(1, \"big\"))\n            value >>= 8\n            bits -= 8\n\n        return buffer\n\n\n@dataclass(frozen=True)\nclass ForgeData:\n    \"\"\"Class for storing information about Forge mods.\"\"\"\n\n    fml_network_version: int\n    \"\"\"Forge Mod Loader network version.\"\"\"\n    channels: list[ForgeDataChannel]\n    \"\"\"List of channels, both for mods and non-mods.\"\"\"\n    mods: list[ForgeDataMod]\n    \"\"\"List of mods.\"\"\"\n    truncated: bool\n    \"\"\"Is the mods list and or channel list incomplete?\"\"\"\n\n    @staticmethod\n    def _decode_optimized(string: str) -> Connection:\n        \"\"\"Decode buffer from UTF-16 optimized binary data ``string``.\"\"\"\n        with StringIO(string) as text:\n            str_buffer = _StringBuffer(text)\n            return str_buffer.read_optimized_buffer()\n\n    @classmethod\n    def build(cls, raw: RawForgeData) -> Self:\n        \"\"\"Build an object about Forge mods from raw response.\n\n        :param raw: ``forgeData`` attribute in raw response :class:`dict`.\n        :return: :class:`ForgeData` object.\n        \"\"\"\n        fml_network_version = raw.get(\"fmlNetworkVersion\", 1)\n\n        # see https://github.com/MinecraftForge/MinecraftForge/blob/7d0330eb08299935714e34ac651a293e2609aa86/src/main/java/net/minecraftforge/network/ServerStatusPing.java#L27-L73  # noqa: E501  # line too long\n        if \"d\" not in raw:\n            mod_list = raw.get(\"mods\") or raw.get(\"modList\")\n            if mod_list is None:\n                raise KeyError(\"Neither `mods` or `modList` keys exist.\")\n\n            return cls(\n                fml_network_version=fml_network_version,\n                channels=[ForgeDataChannel.build(channel) for channel in raw.get(\"channels\", ())],\n                mods=[ForgeDataMod.build(mod) for mod in mod_list],\n                truncated=False,\n            )\n\n        buffer = cls._decode_optimized(raw[\"d\"])\n\n        channels: list[ForgeDataChannel] = []\n        mods: list[ForgeDataMod] = []\n\n        truncated = buffer.read_bool()\n        mod_count = buffer.read_ushort()\n        try:\n            for _ in range(mod_count):\n                mod, mod_channels = ForgeDataMod.decode(buffer)\n\n                channels.extend(mod_channels)\n                mods.append(mod)\n\n            non_mod_channel_count = buffer.read_varint()\n            channels.extend(ForgeDataChannel.decode(buffer) for _ in range(non_mod_channel_count))\n        except OSError:\n            if not truncated:\n                raise  # If answer wasn't truncated, we lost some data on the way\n\n        return cls(\n            fml_network_version=fml_network_version,\n            channels=channels,\n            mods=mods,\n            truncated=truncated,\n        )\n"
  },
  {
    "path": "mcstatus/responses/java.py",
    "content": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import TYPE_CHECKING\n\nfrom mcstatus.motd import Motd\nfrom mcstatus.responses.base import BaseStatusPlayers, BaseStatusResponse, BaseStatusVersion\nfrom mcstatus.responses.forge import ForgeData\n\nif TYPE_CHECKING:\n    from typing_extensions import Self\n\n    from mcstatus.responses._raw import RawJavaResponse, RawJavaResponsePlayer, RawJavaResponsePlayers, RawJavaResponseVersion\n\n__all__ = [\n    \"JavaStatusPlayer\",\n    \"JavaStatusPlayers\",\n    \"JavaStatusResponse\",\n    \"JavaStatusVersion\",\n]\n\n\n@dataclass(frozen=True)\nclass JavaStatusResponse(BaseStatusResponse):\n    \"\"\"The response object for :meth:`JavaServer.status() <mcstatus.server.JavaServer.status>`.\"\"\"\n\n    raw: RawJavaResponse\n    \"\"\"Raw response from the server.\n\n    This is :class:`~typing.TypedDict` actually, please see sources to find what is here.\n    \"\"\"\n    players: JavaStatusPlayers\n    version: JavaStatusVersion\n    enforces_secure_chat: bool | None\n    \"\"\"Whether the server enforces secure chat (every message is signed up with a key).\n\n    .. seealso::\n        `Signed Chat explanation <https://gist.github.com/kennytv/ed783dd244ca0321bbd882c347892874>`_,\n        `22w17a changelog, where this was added <https://www.minecraft.net/nl-nl/article/minecraft-snapshot-22w17a>`_.\n\n    .. versionadded:: 11.1.0\n    \"\"\"\n    icon: str | None\n    \"\"\"The icon of the server. In `Base64 <https://en.wikipedia.org/wiki/Base64>`_ encoded PNG image format.\n\n    .. seealso:: :ref:`pages/faq:how to get server image?`\n    \"\"\"\n    forge_data: ForgeData | None\n    \"\"\"Forge mod data (mod list, channels, etc). Only present if this is a forge (modded) server.\"\"\"\n\n    @classmethod\n    def build(cls, raw: RawJavaResponse, latency: float = 0) -> Self:\n        \"\"\"Build JavaStatusResponse and check is it valid.\n\n        :param raw: Raw response :class:`dict`.\n        :param latency: Time that server took to response (in milliseconds).\n        :raise ValueError: If the required keys (``players``, ``version``, ``description``) are not present.\n        :raise TypeError:\n            If the required keys (``players`` - :class:`dict`, ``version`` - :class:`dict`,\n            ``description`` - :class:`str`) are not of the expected type.\n        :return: :class:`JavaStatusResponse` object.\n        \"\"\"\n        forge_data: ForgeData | None = None\n        if (raw_forge := raw.get(\"forgeData\") or raw.get(\"modinfo\")) and raw_forge is not None:\n            forge_data = ForgeData.build(raw_forge)\n\n        return cls(\n            raw=raw,\n            players=JavaStatusPlayers.build(raw[\"players\"]),\n            version=JavaStatusVersion.build(raw[\"version\"]),\n            motd=Motd.parse(raw.get(\"description\", \"\"), bedrock=False),\n            enforces_secure_chat=raw.get(\"enforcesSecureChat\"),\n            icon=raw.get(\"favicon\"),\n            latency=latency,\n            forge_data=forge_data,\n        )\n\n\n@dataclass(frozen=True)\nclass JavaStatusPlayers(BaseStatusPlayers):\n    \"\"\"Class for storing information about players on the server.\"\"\"\n\n    sample: list[JavaStatusPlayer] | None\n    \"\"\"List of players, who are online. If server didn't provide this, it will be :obj:`None`.\n\n    Actually, this is what appears when you hover over the slot count on the multiplayer screen.\n\n    .. note::\n        It's often empty or even contains some advertisement, because the specific server implementations or plugins can\n        disable providing this information or even change it to something custom.\n\n        There is nothing that ``mcstatus`` can to do here if the player sample was modified/disabled like this.\n    \"\"\"\n\n    @classmethod\n    def build(cls, raw: RawJavaResponsePlayers) -> Self:\n        \"\"\"Build :class:`JavaStatusPlayers` from raw response :class:`dict`.\n\n        :param raw: Raw response :class:`dict`.\n        :raise ValueError: If the required keys (``online``, ``max``) are not present.\n        :raise TypeError:\n            If the required keys (``online`` - :class:`int`, ``max`` - :class:`int`,\n            ``sample`` - :class:`list`) are not of the expected type.\n        :return: :class:`JavaStatusPlayers` object.\n        \"\"\"\n        sample: list[JavaStatusPlayer] | None = None\n        if (sample_raw := raw.get(\"sample\")) is not None:\n            sample = [JavaStatusPlayer.build(player) for player in sample_raw]\n        return cls(\n            online=raw[\"online\"],\n            max=raw[\"max\"],\n            sample=sample,\n        )\n\n\n@dataclass(frozen=True)\nclass JavaStatusPlayer:\n    \"\"\"Class with information about a single player.\"\"\"\n\n    name: str\n    \"\"\"Name of the player.\"\"\"\n    id: str\n    \"\"\"ID of the player (in `UUID <https://en.wikipedia.org/wiki/Universally_unique_identifier>`_ format).\"\"\"\n\n    @property\n    def uuid(self) -> str:\n        \"\"\"Alias to :attr:`.id` field.\"\"\"\n        return self.id\n\n    @classmethod\n    def build(cls, raw: RawJavaResponsePlayer) -> Self:\n        \"\"\"Build :class:`JavaStatusPlayer` from raw response :class:`dict`.\n\n        :param raw: Raw response :class:`dict`.\n        :raise ValueError: If the required keys (``name``, ``id``) are not present.\n        :raise TypeError: If the required keys (``name`` - :class:`str`, ``id`` - :class:`str`)\n            are not of the expected type.\n        :return: :class:`JavaStatusPlayer` object.\n        \"\"\"\n        return cls(name=raw[\"name\"], id=raw[\"id\"])\n\n\n@dataclass(frozen=True)\nclass JavaStatusVersion(BaseStatusVersion):\n    \"\"\"A class for storing version information.\"\"\"\n\n    @classmethod\n    def build(cls, raw: RawJavaResponseVersion) -> Self:\n        \"\"\"Build :class:`JavaStatusVersion` from raw response dict.\n\n        :param raw: Raw response :class:`dict`.\n        :raise ValueError: If the required keys (``name``, ``protocol``) are not present.\n        :raise TypeError: If the required keys (``name`` - :class:`str`, ``protocol`` - :class:`int`)\n            are not of the expected type.\n        :return: :class:`JavaStatusVersion` object.\n        \"\"\"\n        return cls(name=raw[\"name\"], protocol=raw[\"protocol\"])\n"
  },
  {
    "path": "mcstatus/responses/legacy.py",
    "content": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import TYPE_CHECKING\n\nfrom mcstatus.motd import Motd\nfrom mcstatus.responses.base import BaseStatusPlayers, BaseStatusResponse, BaseStatusVersion\n\nif TYPE_CHECKING:\n    from typing_extensions import Self\n\n__all__ = [\n    \"LegacyStatusPlayers\",\n    \"LegacyStatusResponse\",\n    \"LegacyStatusVersion\",\n]\n\n\n@dataclass(frozen=True)\nclass LegacyStatusResponse(BaseStatusResponse):\n    \"\"\"The response object for :meth:`LegacyServerStatus.status() <mcstatus.server.LegacyServer.status>`.\"\"\"\n\n    players: LegacyStatusPlayers\n    version: LegacyStatusVersion\n    \"\"\"The version information, only populates for servers >=12w42b (1.4 onwards).\"\"\"\n\n    @classmethod\n    def build(cls, decoded_data: list[str], latency: float) -> Self:\n        \"\"\"Build BaseStatusResponse and check is it valid.\n\n        :param decoded_data: Raw decoded response object.\n        :param latency: Latency of the request.\n        :return: :class:`LegacyStatusResponse` object.\n        \"\"\"\n        return cls(\n            players=LegacyStatusPlayers(\n                online=int(decoded_data[3]),\n                max=int(decoded_data[4]),\n            ),\n            version=LegacyStatusVersion(\n                name=decoded_data[1],\n                protocol=int(decoded_data[0]),\n            ),\n            motd=Motd.parse(decoded_data[2]),\n            latency=latency,\n        )\n\n\n@dataclass(frozen=True)\nclass LegacyStatusPlayers(BaseStatusPlayers):\n    \"\"\"Class for storing information about players on the server.\"\"\"\n\n\n@dataclass(frozen=True)\nclass LegacyStatusVersion(BaseStatusVersion):\n    \"\"\"A class for storing version information.\"\"\"\n\n    name: str\n    \"\"\"The version name, like ``1.19.3``.\n\n    See `Minecraft wiki <https://minecraft.wiki/w/Java_Edition_version_history>`__\n    for complete list.\n\n    Will be ``<1.4`` for older releases, as those did not send version\n    information.\n    \"\"\"\n    protocol: int\n    \"\"\"The protocol version, like ``761``.\n\n    See `Minecraft wiki <https://minecraft.wiki/w/Protocol_version#Java_Edition_2>`__.\n\n    ``-1`` means 1.3 and lower, before 1.4 servers did not send information\n    about its version.\n    \"\"\"\n"
  },
  {
    "path": "mcstatus/responses/query.py",
    "content": "from __future__ import annotations\n\nfrom dataclasses import asdict, dataclass\nfrom typing import Any, TYPE_CHECKING\n\nfrom mcstatus._utils import deprecated\nfrom mcstatus.motd import Motd\n\nif TYPE_CHECKING:\n    from typing_extensions import Self\n\n    from mcstatus.responses._raw import RawQueryResponse\n\n__all__ = [\n    \"QueryPlayers\",\n    \"QueryResponse\",\n    \"QuerySoftware\",\n]\n\n\n@dataclass(frozen=True)\nclass QueryResponse:\n    \"\"\"The response object for :meth:`JavaServer.query() <mcstatus.server.JavaServer.query>`.\"\"\"\n\n    raw: RawQueryResponse\n    \"\"\"Raw response from the server.\n\n    This is :class:`~typing.TypedDict` actually, please see sources to find what is here.\n    \"\"\"\n    motd: Motd\n    \"\"\"The MOTD of the server. Also known as description.\n\n    .. seealso:: :doc:`/api/motd_parsing`.\n    \"\"\"\n    map_name: str\n    \"\"\"The name of the map. Default is ``world``.\"\"\"\n    players: QueryPlayers\n    \"\"\"The players information.\"\"\"\n    software: QuerySoftware\n    \"\"\"The software information.\"\"\"\n    ip: str\n    \"\"\"The IP address the server is listening/was contacted on.\"\"\"\n    port: int\n    \"\"\"The port the server is listening/was contacted on.\"\"\"\n    game_type: str = \"SMP\"\n    \"\"\"The game type of the server. Hardcoded to ``SMP`` (survival multiplayer).\"\"\"\n    game_id: str = \"MINECRAFT\"\n    \"\"\"The game ID of the server. Hardcoded to ``MINECRAFT``.\"\"\"\n\n    @classmethod\n    def build(cls, raw: RawQueryResponse, players_list: list[str]) -> Self:\n        return cls(\n            raw=raw,\n            motd=Motd.parse(raw[\"hostname\"], bedrock=False),\n            map_name=raw[\"map\"],\n            players=QueryPlayers.build(raw, players_list),\n            software=QuerySoftware.build(raw[\"version\"], raw[\"plugins\"]),\n            ip=raw[\"hostip\"],\n            port=int(raw[\"hostport\"]),\n            game_type=raw[\"gametype\"],\n            game_id=raw[\"game_id\"],\n        )\n\n    def as_dict(self) -> dict[str, Any]:\n        \"\"\"Return the dataclass as JSON-serializable :class:`dict`.\n\n        Do note that this method doesn't return :class:`string <str>` but\n        :class:`dict`, so you can do some processing on returned value.\n\n        Difference from\n        :attr:`~mcstatus.responses.JavaStatusResponse.raw` is in that,\n        :attr:`~mcstatus.responses.JavaStatusResponse.raw` returns raw response\n        in the same format as we got it. This method returns the response\n        in a more user-friendly JSON serializable format (for example,\n        :attr:`~mcstatus.responses.BaseStatusResponse.motd` is returned as a\n        :func:`Minecraft string <mcstatus.motd.Motd.to_minecraft>` and not\n        :class:`dict`).\n        \"\"\"\n        as_dict = asdict(self)\n        as_dict[\"motd\"] = self.motd.simplify().to_minecraft()\n        as_dict[\"players\"] = asdict(self.players)\n        as_dict[\"software\"] = asdict(self.software)\n        return as_dict\n\n    @property\n    @deprecated(replacement=\"map_name\", removal_version=\"13.0.0\")\n    def map(self) -> str | None:\n        \"\"\"\n        .. deprecated:: 12.0.0\n            Will be removed in 13.0.0, use :attr:`.map_name` instead.\n        \"\"\"  # noqa: D205, D212 # no summary line\n        return self.map_name\n\n\n@dataclass(frozen=True)\nclass QueryPlayers:\n    \"\"\"Class for storing information about players on the server.\"\"\"\n\n    online: int\n    \"\"\"The number of online players.\"\"\"\n    max: int\n    \"\"\"The maximum allowed number of players (server slots).\"\"\"\n    list: list[str]\n    \"\"\"The list of online players.\"\"\"\n\n    @classmethod\n    def build(cls, raw: RawQueryResponse, players_list: list[str]) -> Self:\n        return cls(\n            online=int(raw[\"numplayers\"]),\n            max=int(raw[\"maxplayers\"]),\n            list=players_list,\n        )\n\n    @property\n    @deprecated(replacement=\"'list' attribute\", removal_version=\"13.0.0\")\n    def names(self) -> list[str]:\n        \"\"\"\n        .. deprecated:: 12.0.0\n            Will be removed in 13.0.0, use :attr:`.list` instead.\n        \"\"\"  # noqa: D205, D212 # no summary line\n        return self.list\n\n\n@dataclass(frozen=True)\nclass QuerySoftware:\n    \"\"\"Class for storing information about software on the server.\"\"\"\n\n    version: str\n    \"\"\"The version of the software.\"\"\"\n    brand: str\n    \"\"\"The brand of the software. Like `Paper <https://papermc.io>`_ or `Spigot <https://www.spigotmc.org>`_.\"\"\"\n    plugins: list[str]\n    \"\"\"The list of plugins. Can be an empty list if hidden.\"\"\"\n\n    @classmethod\n    def build(cls, version: str, plugins: str) -> Self:\n        brand, parsed_plugins = cls._parse_plugins(plugins)\n        return cls(\n            version=version,\n            brand=brand,\n            plugins=parsed_plugins,\n        )\n\n    @staticmethod\n    def _parse_plugins(plugins: str) -> tuple[str, list[str]]:\n        \"\"\"Parse plugins string to list.\n\n        Returns:\n            :class:`tuple` with two elements. First is brand of server (:attr:`.brand`)\n            and second is a list of :attr:`plugins`.\n        \"\"\"\n        brand = \"vanilla\"\n        parsed_plugins = []\n\n        if plugins:\n            parts = plugins.split(\":\", 1)\n            brand = parts[0].strip()\n\n            if len(parts) == 2:\n                parsed_plugins = [s.strip() for s in parts[1].split(\";\")]\n\n        return brand, parsed_plugins\n"
  },
  {
    "path": "mcstatus/server.py",
    "content": "from __future__ import annotations\n\nfrom abc import ABC\nfrom typing import TYPE_CHECKING\n\nfrom mcstatus._net.address import Address, async_minecraft_srv_address_lookup, minecraft_srv_address_lookup\nfrom mcstatus._protocol.bedrock_client import BedrockClient\nfrom mcstatus._protocol.connection import (\n    TCPAsyncSocketConnection,\n    TCPSocketConnection,\n    UDPAsyncSocketConnection,\n    UDPSocketConnection,\n)\nfrom mcstatus._protocol.java_client import AsyncJavaClient, JavaClient\nfrom mcstatus._protocol.legacy_client import AsyncLegacyClient, LegacyClient\nfrom mcstatus._protocol.query_client import AsyncQueryClient, QueryClient\nfrom mcstatus._utils import retry\n\nif TYPE_CHECKING:\n    from typing_extensions import Self\n\n    from mcstatus.responses import BedrockStatusResponse, JavaStatusResponse, LegacyStatusResponse, QueryResponse\n\n\n__all__ = [\"BedrockServer\", \"JavaServer\", \"LegacyServer\", \"MCServer\"]\n\n\nclass MCServer(ABC):\n    \"\"\"Base abstract class for a general minecraft server.\n\n    This class only contains the basic logic shared across both java and bedrock versions,\n    it doesn't include any version specific settings and it can't be used to make any requests.\n    \"\"\"\n\n    DEFAULT_PORT: int\n\n    def __init__(self, host: str, port: int | None = None, timeout: float = 3) -> None:\n        \"\"\"\n        :param host: The host/ip of the minecraft server.\n        :param port: The port that the server is on.\n        :param timeout: The timeout in seconds before failing to connect.\n        \"\"\"  # noqa: D205, D212 # no summary line\n        if port is None:\n            port = self.DEFAULT_PORT\n        self.address = Address(host, port)\n        self.timeout = timeout\n\n    @classmethod\n    def lookup(cls, address: str, timeout: float = 3) -> Self:\n        \"\"\"Mimics minecraft's server address field.\n\n        :param address: The address of the Minecraft server, like ``example.com:19132``\n        :param timeout: The timeout in seconds before failing to connect.\n        \"\"\"\n        addr = Address.parse_address(address, default_port=cls.DEFAULT_PORT)\n        return cls(addr.host, addr.port, timeout=timeout)\n\n\nclass BaseJavaServer(MCServer):\n    \"\"\"Base class for a Minecraft Java Edition server.\n\n    .. versionadded:: 12.1.0\n    \"\"\"\n\n    DEFAULT_PORT = 25565\n\n    @classmethod\n    def lookup(cls, address: str, timeout: float = 3) -> Self:\n        \"\"\"Mimics minecraft's server address field.\n\n        With Java servers, on top of just parsing the address, we also check the\n        DNS records for an SRV record that points to the server, which is the same\n        behavior as with minecraft's server address field for Java. This DNS record\n        resolution is happening synchronously (see :meth:`.async_lookup`).\n\n        :param address: The address of the Minecraft server, like ``example.com:25565``.\n        :param timeout: The timeout in seconds before failing to connect.\n        \"\"\"\n        addr = minecraft_srv_address_lookup(address, default_port=cls.DEFAULT_PORT, lifetime=timeout)\n        return cls(addr.host, addr.port, timeout=timeout)\n\n    @classmethod\n    async def async_lookup(cls, address: str, timeout: float = 3) -> Self:\n        \"\"\"Asynchronous alternative to :meth:`.lookup`.\n\n        For more details, check the :meth:`JavaServer.lookup() <.lookup>` docstring.\n        \"\"\"\n        addr = await async_minecraft_srv_address_lookup(address, default_port=cls.DEFAULT_PORT, lifetime=timeout)\n        return cls(addr.host, addr.port, timeout=timeout)\n\n\nclass JavaServer(BaseJavaServer):\n    \"\"\"Base class for a 1.7+ Minecraft Java Edition server.\"\"\"\n\n    def __init__(self, host: str, port: int | None = None, timeout: float = 3, query_port: int | None = None) -> None:\n        \"\"\"\n        :param host: The host/ip of the minecraft server.\n        :param port: The port that the server is on.\n        :param timeout: The timeout in seconds before failing to connect.\n        :param query_port: Typically the same as ``port`` but can be different.\n        \"\"\"  # noqa: D205, D212 # no summary line\n        super().__init__(host, port, timeout)\n        if query_port is None:\n            query_port = port or self.DEFAULT_PORT\n        self.query_port = query_port\n        _ = Address(host, self.query_port)  # Ensure query_port is valid\n\n    def ping(self, *, tries: int = 3, version: int = 47, ping_token: int | None = None) -> float:\n        \"\"\"Check the latency between a Minecraft Java Edition server and the client (you).\n\n        Note that most non-vanilla implementations fail to respond to a ping\n        packet unless a status packet is sent first. Expect ``OSError: Server\n        did not respond with any information!`` in those cases. The workaround\n        is to use the latency provided with :meth:`.status` as ping time.\n\n        :param tries: The number of times to retry if an error is encountered.\n        :param version: Version of the client, see https://minecraft.wiki/w/Protocol_version#List_of_protocol_versions.\n        :param ping_token: Token of the packet, default is a random number.\n        :return: The latency between the Minecraft Server and you.\n        \"\"\"\n        with TCPSocketConnection(self.address, self.timeout) as connection:\n            return self._retry_ping(connection, tries=tries, version=version, ping_token=ping_token)\n\n    @retry(tries=3)\n    def _retry_ping(\n        self,\n        connection: TCPSocketConnection,\n        *,\n        tries: int = 3,  # noqa: ARG002 # unused argument\n        version: int,\n        ping_token: int | None,\n    ) -> float:\n        java_client = JavaClient(\n            connection,\n            address=self.address,\n            version=version,\n            ping_token=ping_token,  # pyright: ignore[reportArgumentType] # None is not assignable to int\n        )\n        java_client.handshake()\n        return java_client.test_ping()\n\n    async def async_ping(self, *, tries: int = 3, version: int = 47, ping_token: int | None = None) -> float:\n        \"\"\"Asynchronously check the latency between a Minecraft Java Edition server and the client (you).\n\n        Note that most non-vanilla implementations fail to respond to a ping\n        packet unless a status packet is sent first. Expect ``OSError: Server\n        did not respond with any information!`` in those cases. The workaround\n        is to use the latency provided with :meth:`.async_status` as ping time.\n\n        :param tries: The number of times to retry if an error is encountered.\n        :param version: Version of the client, see https://minecraft.wiki/w/Protocol_version#List_of_protocol_versions.\n        :param ping_token: Token of the packet, default is a random number.\n        :return: The latency between the Minecraft Server and you.\n        \"\"\"\n        async with TCPAsyncSocketConnection(self.address, self.timeout) as connection:\n            return await self._retry_async_ping(connection, tries=tries, version=version, ping_token=ping_token)\n\n    @retry(tries=3)\n    async def _retry_async_ping(\n        self,\n        connection: TCPAsyncSocketConnection,\n        *,\n        tries: int = 3,  # noqa: ARG002 # unused argument\n        version: int,\n        ping_token: int | None,\n    ) -> float:\n        java_client = AsyncJavaClient(\n            connection,\n            address=self.address,\n            version=version,\n            ping_token=ping_token,  # pyright: ignore[reportArgumentType] # None is not assignable to int\n        )\n        java_client.handshake()\n        ping = await java_client.test_ping()\n        return ping\n\n    def status(self, *, tries: int = 3, version: int = 47, ping_token: int | None = None) -> JavaStatusResponse:\n        \"\"\"Check the status of a Minecraft Java Edition server via the status protocol.\n\n        :param tries: The number of times to retry if an error is encountered.\n        :param version: Version of the client, see https://minecraft.wiki/w/Protocol_version#List_of_protocol_versions.\n        :param ping_token: Token of the packet, default is a random number.\n        :return: Status information in a :class:`~mcstatus.responses.JavaStatusResponse` instance.\n        \"\"\"\n        with TCPSocketConnection(self.address, self.timeout) as connection:\n            return self._retry_status(connection, tries=tries, version=version, ping_token=ping_token)\n\n    @retry(tries=3)\n    def _retry_status(\n        self,\n        connection: TCPSocketConnection,\n        *,\n        tries: int = 3,  # noqa: ARG002 # unused argument\n        version: int,\n        ping_token: int | None,\n    ) -> JavaStatusResponse:\n        java_client = JavaClient(\n            connection,\n            address=self.address,\n            version=version,\n            ping_token=ping_token,  # pyright: ignore[reportArgumentType] # None is not assignable to int\n        )\n        java_client.handshake()\n        result = java_client.read_status()\n        return result\n\n    async def async_status(self, *, tries: int = 3, version: int = 47, ping_token: int | None = None) -> JavaStatusResponse:\n        \"\"\"Asynchronously check the status of a Minecraft Java Edition server via the status protocol.\n\n        :param tries: The number of times to retry if an error is encountered.\n        :param version: Version of the client, see https://minecraft.wiki/w/Protocol_version#List_of_protocol_versions.\n        :param ping_token: Token of the packet, default is a random number.\n        :return: Status information in a :class:`~mcstatus.responses.JavaStatusResponse` instance.\n        \"\"\"\n        async with TCPAsyncSocketConnection(self.address, self.timeout) as connection:\n            return await self._retry_async_status(connection, tries=tries, version=version, ping_token=ping_token)\n\n    @retry(tries=3)\n    async def _retry_async_status(\n        self,\n        connection: TCPAsyncSocketConnection,\n        *,\n        tries: int = 3,  # noqa: ARG002 # unused argument\n        version: int,\n        ping_token: int | None,\n    ) -> JavaStatusResponse:\n        java_client = AsyncJavaClient(\n            connection,\n            address=self.address,\n            version=version,\n            ping_token=ping_token,  # pyright: ignore[reportArgumentType] # None is not assignable to int\n        )\n        java_client.handshake()\n        result = await java_client.read_status()\n        return result\n\n    def query(self, *, tries: int = 3) -> QueryResponse:\n        \"\"\"Check the status of a Minecraft Java Edition server via the query protocol.\n\n        :param tries: The number of times to retry if an error is encountered.\n        :return: Query information in a :class:`~mcstatus.responses.QueryResponse` instance.\n        \"\"\"\n        ip = str(self.address.resolve_ip())\n        return self._retry_query(Address(ip, self.query_port), tries=tries)\n\n    @retry(tries=3)\n    def _retry_query(self, addr: Address, tries: int = 3) -> QueryResponse:  # noqa: ARG002 # unused argument\n        with UDPSocketConnection(addr, self.timeout) as connection:\n            query_client = QueryClient(connection)\n            query_client.handshake()\n            return query_client.read_query()\n\n    async def async_query(self, *, tries: int = 3) -> QueryResponse:\n        \"\"\"Asynchronously check the status of a Minecraft Java Edition server via the query protocol.\n\n        :param tries: The number of times to retry if an error is encountered.\n        :return: Query information in a :class:`~mcstatus.responses.QueryResponse` instance.\n        \"\"\"\n        ip = str(await self.address.async_resolve_ip())\n        return await self._retry_async_query(Address(ip, self.query_port), tries=tries)\n\n    @retry(tries=3)\n    async def _retry_async_query(self, address: Address, tries: int = 3) -> QueryResponse:  # noqa: ARG002 # unused argument\n        async with UDPAsyncSocketConnection(address, self.timeout) as connection:\n            query_client = AsyncQueryClient(connection)\n            await query_client.handshake()\n            return await query_client.read_query()\n\n\nclass LegacyServer(BaseJavaServer):\n    \"\"\"Base class for a pre-1.7 Minecraft Java Edition server.\n\n    .. versionadded:: 12.1.0\n    \"\"\"\n\n    @retry(tries=3)\n    def status(self, *, tries: int = 3) -> LegacyStatusResponse:  # noqa: ARG002 # unused argument\n        \"\"\"Check the status of a pre-1.7 Minecraft Java Edition server.\n\n        :param tries: The number of times to retry if an error is encountered.\n        :return: Status information in a :class:`~mcstatus.responses.LegacyStatusResponse` instance.\n        \"\"\"\n        with TCPSocketConnection(self.address, self.timeout) as connection:\n            return LegacyClient(connection).read_status()\n\n    @retry(tries=3)\n    async def async_status(self, *, tries: int = 3) -> LegacyStatusResponse:  # noqa: ARG002 # unused argument\n        \"\"\"Asynchronously check the status of a pre-1.7 Minecraft Java Edition server.\n\n        :param tries: The number of times to retry if an error is encountered.\n        :return: Status information in a :class:`~mcstatus.responses.LegacyStatusResponse` instance.\n        \"\"\"\n        async with TCPAsyncSocketConnection(self.address, self.timeout) as connection:\n            return await AsyncLegacyClient(connection).read_status()\n\n\nclass BedrockServer(MCServer):\n    \"\"\"Base class for a Minecraft Bedrock Edition server.\"\"\"\n\n    DEFAULT_PORT = 19132\n\n    @retry(tries=3)\n    def status(self, *, tries: int = 3) -> BedrockStatusResponse:  # noqa: ARG002 # unused argument\n        \"\"\"Check the status of a Minecraft Bedrock Edition server.\n\n        :param tries: The number of times to retry if an error is encountered.\n        :return: Status information in a :class:`~mcstatus.responses.BedrockStatusResponse` instance.\n        \"\"\"\n        return BedrockClient(self.address, self.timeout).read_status()\n\n    @retry(tries=3)\n    async def async_status(self, *, tries: int = 3) -> BedrockStatusResponse:  # noqa: ARG002 # unused argument\n        \"\"\"Asynchronously check the status of a Minecraft Bedrock Edition server.\n\n        :param tries: The number of times to retry if an error is encountered.\n        :return: Status information in a :class:`~mcstatus.responses.BedrockStatusResponse` instance.\n        \"\"\"\n        return await BedrockClient(self.address, self.timeout).read_status_async()\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"mcstatus\"\ndynamic = [\"version\"]\nlicense = \"Apache-2.0\"\ndescription = \"A library to query Minecraft Servers for their status and capabilities.\"\nreadme = \"README.md\"\nauthors = [\n  { name = \"Nathan Adams\", email = \"dinnerbone@dinnerbone.com\" },\n  { name = \"ItsDrike\", email = \"itsdrike@protonmail.com\" },\n  { name = \"PerchunPak\", email = \"perchunpak@gmail.com\" },\n]\nmaintainers = [\n  { name = \"Kevin Tindall\", email = \"kevinkjt2000@gmail.com\" },\n  { name = \"ItsDrike\", email = \"itsdrike@protonmail.com\" },\n  { name = \"PerchunPak\", email = \"perchunpak@gmail.com\" },\n]\nclassifiers = [\n  \"Development Status :: 5 - Production/Stable\",\n  \"Intended Audience :: Developers\",\n  \"License :: OSI Approved :: Apache Software License\",\n  \"Natural Language :: English\",\n  \"Operating System :: OS Independent\",\n  \"Programming Language :: Python\",\n  \"Programming Language :: Python :: 3\",\n  \"Programming Language :: Python :: 3.10\",\n  \"Programming Language :: Python :: 3.11\",\n  \"Programming Language :: Python :: 3.12\",\n  \"Programming Language :: Python :: 3.13\",\n  \"Programming Language :: Python :: 3.14\",\n  \"Topic :: Games/Entertainment\",\n  \"Topic :: Software Development :: Libraries :: Python Modules\",\n  \"Topic :: System :: Monitoring\",\n  \"Typing :: Typed\",\n]\nkeywords = [\"minecraft\", \"protocol\"]\nrequires-python = \">=3.10\"\ndependencies = [\"asyncio-dgram>=2.1.2\", \"dnspython>=2.4.2\"]\n\n[project.urls]\nDocumentation = \"https://mcstatus.readthedocs.io\"\n\"Source code\" = \"https://github.com/py-mine/mcstatus\"\n\n[tool.uv]\ndefault-groups = [\"dev\", \"lint\", \"test\", \"docs\"]\n\n[dependency-groups]\ndev = [\"poethepoet~=0.44.0\"]\nlint = [\n  \"pre-commit~=4.5.1\",\n  \"ruff~=0.15.0\",\n  \"pyright==1.1.408\",\n  \"typing-extensions~=4.15.0\",\n]\ntest = [\n  \"pytest~=9.0.2\",\n  \"pytest-asyncio~=1.3.0\",\n  \"pytest-cov~=7.1.0\",\n  \"pytest-rerunfailures~=16.1\",\n  \"coverage~=7.13.1\",\n  \"typing-extensions~=4.15.0\",\n  # the actual versions are in the `release` group\n  \"uv-dynamic-versioning\",\n  \"hatchling\",\n]\ndocs = [\"docs; python_version >= '3.12'\"]\nrelease = [\"hatchling~=1.29.0\", \"uv-dynamic-versioning~=0.14.0\"]\n\n[tool.poe.tasks]\n_lint_ruff = \"ruff check .\"\n_lint_pyright = \"pyright .\"\n\n[tool.poe.tasks.build]\ncmd = \"uv build\"\nhelp = \"Builds the whl and tar.gz distributions\"\n\n[tool.poe.tasks.docs]\ncmd = \"make -C docs/ html\"\nhelp = \"Generates documentation locally in html format\"\n\n[tool.poe.tasks.format]\ncmd = \"ruff format\"\nhelp = \"Runs automatic formatting tools\"\n\n[tool.poe.tasks.lint]\nsequence = [\"_lint_pyright\", \"_lint_ruff\"]\nhelp = \"Runs linter tools\"\n\n[tool.poe.tasks.pre-commit]\ncmd = \"pre-commit run --all-files\"\nhelp = \"Executes commit hook checks on all files\"\n\n[tool.poe.tasks.release]\ncmd = \"uv publish\"\ndeps = [\"pre-commit\", \"test\", \"build\"]\nhelp = \"Requires all the checks to pass before building and publishing\"\n\n[tool.poe.tasks.test]\ncmd = \"pytest\"\nhelp = \"Runs the unit tests\"\n\n[tool.pytest.ini_options]\nminversion = \"6.0\"\ntmp_path_retention_policy = \"failed\"\naddopts = \"--strict-markers --doctest-modules --cov=mcstatus --cov-append --cov-branch --cov-report=term-missing -vvv --asyncio-mode=strict\"\ntestpaths = [\"tests\"]\n\n# Remove deprecation warning\nasyncio_default_fixture_loop_scope = \"function\"\n\n[tool.pyright]\npythonPlatform = \"All\"\npythonVersion = \"3.10\"\ntypeCheckingMode = \"standard\"\n\ndisableBytesTypePromotions = false\nenableTypeIgnoreComments = false\nreportUnnecessaryTypeIgnoreComment = true\n\n[tool.ruff]\ntarget-version = \"py310\"\nline-length = 127\n\n[tool.ruff.lint]\nselect = [\"ALL\"]\n\nignore = [\n  \"EM\",      # Very weird rules for using exceptions\n  \"FIX\",     # Line contains TODO, consider resolving the issue\n  \"COM812\",  # Missing trailing comma (in multiline lists/tuples/...)\n  \"D203\",    # Blank line required before class docstring\n  \"D213\",    # Multi-line docstring summary should start at the second line\n  \"PLR2004\", # Magic value used in comparison\n  \"RET504\",  # Unnecessary assignment to a variable before `return` statement\n  \"S101\",    # Use of `assert` detected\n  \"S311\",    # Standard pseudo-random generators are not suitable for cryptographic purposes\n  \"TD002\",   # Missing author in TODO\n  \"TD003\",   # Missing issue link for this TODO\n  \"TRY003\",  # Avoid specifying long messages outside the exception class\n  \"ISC002\",  # Multi line implicit string concatenation\n\n  # Ruff often identifies private modules as public, because we don't prefix them with `_`\n  \"D100\", # Missing docstring in public module\n  \"D101\", # Missing docstring in public class\n  \"D102\", # Missing docstring in public method\n  \"D103\", # Missing docstring in public function\n  \"D104\", # Missing docstring in public package\n  \"D105\", # Missing docstring in magic method\n  \"D106\", # Missing docstring in public nested class\n  \"D107\", # Missing docstring in __init__\n  \"D413\", # Missing blank line after last section\n]\n\n[tool.ruff.lint.per-file-ignores]\n\"tests/**\" = [\n  \"ANN\",    # flake8-annotations\n  \"ARG001\", # Unused function argument\n  \"ARG002\", # Unused method argument\n  \"FBT001\", # Boolean-typed positional argument in function definition\n  \"SLF001\", # Private member accessed\n  \"T201\",   # Usage of `print`\n]\n\"docs/**\" = [\n  \"INP001\", # Implicit namespace package\n]\n\"docs/examples/code/**\" = [\n  \"FA\",     # flake8-future-annotations\n  \"BLE001\", # Do not catch blind exception: `Exception`\n  \"T201\",   # Usage of `print`\n  \"TC001\",  # Move application import into a type-checking block\n]\n\n[tool.ruff.lint.flake8-tidy-imports]\nban-relative-imports = \"all\"\nbanned-module-level-imports = [\"typing_extensions\"]\n\n[tool.ruff.lint.flake8-tidy-imports.banned-api]\n\"mcstatus._compat\".msg = \"Deprecated compatibility shims should not be imported\"\n\n[tool.ruff.lint.isort]\norder-by-type = false\ncase-sensitive = true\ncombine-as-imports = true\n\n# Redundant rules with ruff-format\nforce-single-line = false       # forces all imports to appear on their own line\nforce-wrap-aliases = false      # Split imports with multiple members and at least one alias\nlines-after-imports = -1        # The number of blank lines to place after imports\nlines-between-types = 0         # Number of lines to place between \"direct\" and import from imports\nsplit-on-trailing-comma = false # if last member of multiline import has a comma, don't fold it to single line\n\n[tool.ruff.lint.flake8-annotations]\nallow-star-arg-any = true\n\n[tool.ruff.lint.pylint]\nmax-args = 10\n\n[tool.ruff.lint.flake8-builtins]\nignorelist = [\"id\", \"copyright\"]\n\n[tool.ruff.format]\nline-ending = \"lf\"\n\n[project.scripts]\nmcstatus = \"mcstatus.__main__:main\"\n\n[build-system]\nrequires = [\"hatchling\", \"uv-dynamic-versioning\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.uv.sources]\ndocs = { path = \"docs\" }\n\n[tool.hatch.version]\nsource = \"uv-dynamic-versioning\"\n\n[tool.uv-dynamic-versioning]\nmetadata = false\nfallback-version = \"0.0.0\"\n\n[tool.hatch.build.targets.sdist.force-include]\n\"mcstatus/_compat/status_response.py\" = \"mcstatus/status_response.py\"\n\"mcstatus/_compat/forge_data.py\" = \"mcstatus/forge_data.py\"\n\"mcstatus/_compat/motd_transformers.py\" = \"mcstatus/motd/transformers.py\"\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/helpers.py",
    "content": "import collections.abc as c\nimport importlib.metadata\nfrom contextlib import contextmanager\nfrom functools import wraps\nfrom unittest.mock import patch\n\nfrom mcstatus._utils.deprecation import LIB_NAME, _get_project_version\n\n\n@contextmanager\ndef patch_project_version(version: str | None) -> c.Iterator[None]:\n    \"\"\"Patch the project version reported by ``importlib.metadata.version``.\n\n    This is used to simulate different project versions for testing purposes.\n    If ``version`` is ``None``, a :exc:`PackageNotFoundError` will be raised\n    when trying to get the project version.\n    \"\"\"\n    orig_version_func = importlib.metadata.version\n\n    @wraps(orig_version_func)\n    def patched_version_func(distribution_name: str) -> str:\n        if distribution_name == LIB_NAME:\n            if version is None:\n                raise importlib.metadata.PackageNotFoundError\n            return version\n        return orig_version_func(distribution_name)\n\n    _get_project_version.cache_clear()\n    with patch.object(importlib.metadata, \"version\", new=patched_version_func):\n        try:\n            yield\n        finally:\n            _get_project_version.cache_clear()\n"
  },
  {
    "path": "tests/motd/__init__.py",
    "content": ""
  },
  {
    "path": "tests/motd/conftest.py",
    "content": "import pytest\n\n\n@pytest.fixture(scope=\"session\")\ndef source_java() -> dict:\n    \"\"\"Return ultimate dict with almost all possible aspects, which we should support.\n\n    If feature can handle all from this dict, it's fully tested.\n    Parser should have more tests, on additional features.\n    \"\"\"\n    return {\n        \"extra\": [\n            {\"text\": \"1\"},\n            {\"color\": \"#b3eeff\", \"text\": \"2\"},\n            {\"obfuscated\": True, \"color\": \"black\", \"text\": \"3\"},\n            {\"bold\": True, \"strikethrough\": True, \"color\": \"dark_blue\", \"text\": \"4\"},\n            {\"italic\": True, \"color\": \"dark_green\", \"text\": \"5\"},\n            {\"underlined\": True, \"color\": \"dark_aqua\", \"text\": \"6\"},\n            {\"color\": \"dark_aqua\", \"text\": \"7\"},\n            {\"color\": \"dark_red\", \"text\": \"8\"},\n            {\"color\": \"dark_purple\", \"text\": \"9\"},\n            {\"color\": \"gold\", \"text\": \"10\"},\n            {\"color\": \"gray\", \"text\": \"11\"},\n            {\"color\": \"dark_gray\", \"text\": \"12\"},\n            {\"color\": \"blue\", \"text\": \"13\"},\n            {\"color\": \"green\", \"text\": \"14\"},\n            {\"color\": \"aqua\", \"text\": \"15\"},\n            {\"color\": \"red\", \"text\": \"16\"},\n            {\"color\": \"light_purple\", \"text\": \"17\"},\n            {\"color\": \"yellow\", \"text\": \"18\"},\n            {\"color\": \"white\", \"text\": \"19\"},\n            {\"color\": \"reset\", \"text\": \"20\"},\n            {\"translate\": \"some.random.string\"},\n        ],\n        \"text\": \"top\",\n    }\n\n\n@pytest.fixture(scope=\"session\")\ndef source_bedrock() -> dict:\n    \"\"\"Return ultimate dict with almost all possible aspects, which we should support.\n\n    If feature can handle all from this dict, it's fully tested.\n    Parser should have more tests, on additional features.\n    \"\"\"\n    return {\n        \"extra\": [\n            {\"text\": \"1\"},\n            {\"color\": \"#b3eeff\", \"text\": \"2\"},\n            {\"obfuscated\": True, \"color\": \"black\", \"text\": \"3\"},\n            {\"bold\": True, \"strikethrough\": True, \"color\": \"dark_blue\", \"text\": \"4\"},\n            {\"italic\": True, \"color\": \"dark_green\", \"text\": \"5\"},\n            {\"underlined\": True, \"color\": \"dark_aqua\", \"text\": \"6\"},\n            {\"color\": \"dark_aqua\", \"text\": \"7\"},\n            {\"color\": \"dark_red\", \"text\": \"8\"},\n            {\"color\": \"dark_purple\", \"text\": \"9\"},\n            {\"color\": \"gold\", \"text\": \"10\"},\n            {\"color\": \"gray\", \"text\": \"11\"},\n            {\"color\": \"dark_gray\", \"text\": \"12\"},\n            {\"color\": \"blue\", \"text\": \"13\"},\n            {\"color\": \"green\", \"text\": \"14\"},\n            {\"color\": \"aqua\", \"text\": \"15\"},\n            {\"color\": \"red\", \"text\": \"16\"},\n            {\"color\": \"light_purple\", \"text\": \"17\"},\n            {\"color\": \"yellow\", \"text\": \"18\"},\n            {\"color\": \"white\", \"text\": \"19\"},\n            {\"color\": \"minecoin_gold\", \"text\": \"20\"},\n            {\"color\": \"material_quartz\", \"text\": \"21\"},\n            {\"color\": \"material_iron\", \"text\": \"22\"},\n            {\"color\": \"material_netherite\", \"text\": \"23\"},\n            {\"color\": \"material_redstone\", \"text\": \"24\"},\n            {\"color\": \"material_copper\", \"text\": \"25\"},\n            {\"color\": \"material_gold\", \"text\": \"26\"},\n            {\"color\": \"material_emerald\", \"text\": \"27\"},\n            {\"color\": \"material_diamond\", \"text\": \"28\"},\n            {\"color\": \"material_lapis\", \"text\": \"29\"},\n            {\"color\": \"material_amethyst\", \"text\": \"30\"},\n            {\"color\": \"material_resin\", \"text\": \"31\"},\n            {\"color\": \"reset\", \"text\": \"32\"},\n            {\"translate\": \"some.random.string\"},\n        ],\n        \"text\": \"top\",\n    }\n"
  },
  {
    "path": "tests/motd/test_components.py",
    "content": "from __future__ import annotations\n\nimport pytest\n\nfrom mcstatus.motd.components import WebColor\n\n\nclass TestWebColor:\n    @pytest.mark.parametrize(\n        (\"hex_\", \"rgb\"),\n        [\n            (\"#bfff00\", (191, 255, 0)),\n            (\"#00ff80\", (0, 255, 128)),\n            (\"#4000ff\", (64, 0, 255)),\n        ],\n    )\n    def test_hex_to_rgb_correct(self, hex_, rgb):\n        assert WebColor.from_hex(hex=hex_).rgb == rgb\n\n    @pytest.mark.parametrize(\n        (\"hex_\", \"rgb\"),\n        [\n            (\"#bfff00\", (191, 255, 0)),\n            (\"#00ff80\", (0, 255, 128)),\n            (\"#4000ff\", (64, 0, 255)),\n        ],\n    )\n    def test_rgb_to_hex_correct(self, hex_, rgb):\n        assert WebColor.from_rgb(rgb=rgb).hex == hex_\n\n    def test_hex_in_output_has_number_sign(self):\n        assert WebColor.from_hex(hex=\"#bfff00\").hex == \"#bfff00\"\n        assert WebColor.from_hex(hex=\"4000ff\").hex == \"#4000ff\"\n\n    def test_fail_on_incorrect_hex(self):\n        with pytest.raises(ValueError, match=r\"^Failed to parse given hex color: '#!!!!!!'$\"):\n            WebColor.from_hex(hex=\"#!!!!!!\")\n\n    @pytest.mark.parametrize(\"length\", [0, 1, 2, 4, 5, 7, 8, 9, 10])\n    def test_fail_on_too_long_or_too_short_hex(self, length: int):\n        color = \"a\" * length\n        with pytest.raises(ValueError, match=f\"^Got too long/short hex color: '#{color}'$\"):\n            WebColor.from_hex(hex=\"a\" * length)\n\n    def test_fail_on_incorrect_rgb(self):\n        with pytest.raises(ValueError, match=r\"^RGB color byte out of its 8-bit range \\(0-255\\) for red \\(value=-23\\)$\"):\n            WebColor.from_rgb(rgb=(-23, 699, 1000))\n\n    def test_3_symbols_hex(self):\n        assert WebColor.from_hex(\"a1b\").hex == \"#aa11bb\"\n"
  },
  {
    "path": "tests/motd/test_motd.py",
    "content": "from __future__ import annotations\n\nimport pytest\n\nfrom mcstatus.motd import Motd\nfrom mcstatus.motd.components import Formatting, MinecraftColor, TranslationTag, WebColor\nfrom mcstatus.responses._raw import RawJavaResponseMotdWhenDict\n\n\nclass TestMotdParse:\n    def test_correct_result(self, source_bedrock):\n        assert Motd.parse(source_bedrock) == Motd(\n            [\n                \"top\", Formatting.RESET,\n                \"1\", Formatting.RESET,\n                WebColor.from_hex(hex=\"#b3eeff\"), \"2\", Formatting.RESET,\n                MinecraftColor.BLACK, Formatting.OBFUSCATED, \"3\", Formatting.RESET,\n                MinecraftColor.DARK_BLUE, Formatting.BOLD, Formatting.STRIKETHROUGH, \"4\", Formatting.RESET,\n                MinecraftColor.DARK_GREEN, Formatting.ITALIC, \"5\", Formatting.RESET,\n                MinecraftColor.DARK_AQUA, Formatting.UNDERLINED, \"6\", Formatting.RESET,\n                MinecraftColor.DARK_AQUA, \"7\", Formatting.RESET,\n                MinecraftColor.DARK_RED, \"8\", Formatting.RESET,\n                MinecraftColor.DARK_PURPLE, \"9\", Formatting.RESET,\n                MinecraftColor.GOLD, \"10\", Formatting.RESET,\n                MinecraftColor.GRAY, \"11\", Formatting.RESET,\n                MinecraftColor.DARK_GRAY, \"12\", Formatting.RESET,\n                MinecraftColor.BLUE, \"13\", Formatting.RESET,\n                MinecraftColor.GREEN, \"14\", Formatting.RESET,\n                MinecraftColor.AQUA, \"15\", Formatting.RESET,\n                MinecraftColor.RED, \"16\", Formatting.RESET,\n                MinecraftColor.LIGHT_PURPLE, \"17\", Formatting.RESET,\n                MinecraftColor.YELLOW, \"18\", Formatting.RESET,\n                MinecraftColor.WHITE, \"19\", Formatting.RESET,\n                MinecraftColor.MINECOIN_GOLD, \"20\", Formatting.RESET,\n                MinecraftColor.MATERIAL_QUARTZ, \"21\", Formatting.RESET,\n                MinecraftColor.MATERIAL_IRON, \"22\", Formatting.RESET,\n                MinecraftColor.MATERIAL_NETHERITE, \"23\", Formatting.RESET,\n                MinecraftColor.MATERIAL_REDSTONE, \"24\", Formatting.RESET,\n                MinecraftColor.MATERIAL_COPPER, \"25\", Formatting.RESET,\n                MinecraftColor.MATERIAL_GOLD, \"26\", Formatting.RESET,\n                MinecraftColor.MATERIAL_EMERALD, \"27\", Formatting.RESET,\n                MinecraftColor.MATERIAL_DIAMOND, \"28\", Formatting.RESET,\n                MinecraftColor.MATERIAL_LAPIS, \"29\", Formatting.RESET,\n                MinecraftColor.MATERIAL_AMETHYST, \"30\", Formatting.RESET,\n                MinecraftColor.MATERIAL_RESIN, \"31\", Formatting.RESET,\n                Formatting.RESET, \"32\", Formatting.RESET,\n                TranslationTag(\"some.random.string\"), Formatting.RESET,\n            ],\n            raw=source_bedrock,\n        )  # fmt: skip\n\n    @pytest.mark.parametrize(\"bedrock\", [True, False])\n    def test_bedrock_parameter_nothing_changes(self, bedrock: bool):\n        assert Motd.parse([{\"color\": \"minecoin_gold\", \"text\": \" \"}], bedrock=bedrock).parsed == [\n            Formatting.RESET,\n            MinecraftColor.MINECOIN_GOLD,\n            \" \",\n            Formatting.RESET,\n        ]\n\n    @pytest.mark.parametrize((\"bedrock\", \"expected\"), [(True, MinecraftColor.MINECOIN_GOLD), (False, \"&g\")])\n    def test_parse_as_str_ignore_minecoin_gold_on_java(self, bedrock: bool, expected):\n        assert Motd.parse(\"&g\", bedrock=bedrock).parsed == [expected]\n\n    def test_parse_incorrect_color_passes(self):\n        \"\"\"See `https://github.com/py-mine/mcstatus/pull/335#discussion_r985084188`_.\"\"\"\n        assert Motd.parse(\"&z\").parsed == [\"&z\"]\n\n    def test_parse_uppercase_passes(self):\n        assert Motd.parse(\"&A\").parsed == [\"\", MinecraftColor.GREEN, \"\"]\n\n    @pytest.mark.parametrize(\n        (\"input_\", \"expected\"), [(\"\", [\"\"]), ([], [Formatting.RESET]), ({\"extra\": [], \"text\": \"\"}, [\"\", Formatting.RESET])]\n    )\n    def test_empty_input_also_empty_raw(self, input_, expected):\n        assert Motd.parse(input_).parsed == expected\n\n    def test_top_level_formatting_applies_to_all_in_extra(self) -> None:\n        \"\"\"As described `here <https://minecraft.wiki/w/Java_Edition_protocol/Chat?direction=prev&oldid=2763844#Inheritance>`_.\"\"\"\n        assert Motd.parse({\"text\": \"top\", \"bold\": True, \"extra\": [{\"color\": \"red\", \"text\": \"not top\"}]}).parsed == [\n            Formatting.BOLD,\n            \"top\",\n            Formatting.RESET,\n            Formatting.BOLD,\n            MinecraftColor.RED,\n            \"not top\",\n            Formatting.RESET,\n        ]\n\n    def test_top_level_formatting_can_be_overwrote(self) -> None:\n        \"\"\"As described `here <https://minecraft.wiki/w/Java_Edition_protocol/Chat?direction=prev&oldid=2763844#Inheritance>`_.\"\"\"\n        assert Motd.parse(\n            {\"text\": \"bold\", \"bold\": True, \"extra\": [{\"color\": \"red\", \"bold\": False, \"text\": \"not bold\"}]}\n        ).parsed == [\n            Formatting.BOLD,\n            \"bold\",\n            Formatting.RESET,\n            MinecraftColor.RED,\n            \"not bold\",\n            Formatting.RESET,\n        ]\n\n    def test_top_level_formatting_applies_to_string_inside_extra(self) -> None:\n        \"\"\"Although, it is probably a bug in some modded cores, Minecraft supports it, and we should as well.\n\n        See `#711 <https://github.com/py-mine/mcstatus/issues/711>`_.\n        \"\"\"\n        assert Motd.parse({\"text\": \"top\", \"bold\": True, \"extra\": [\"not top\"]}).parsed == [\n            Formatting.BOLD,\n            \"top\",\n            Formatting.RESET,\n            Formatting.BOLD,\n            \"not top\",\n        ]\n\n    def test_formatting_key_set_to_false_here_without_it_being_set_to_true_before(self) -> None:\n        \"\"\"Some servers set the formatting keys to false here, even without it ever being set to true before.\n\n        See `https://github.com/py-mine/mcstatus/pull/335#discussion_r985086953`_.\n        \"\"\"\n        assert Motd.parse({\"color\": \"red\", \"bold\": False, \"text\": \"not bold\"}).parsed == [\n            MinecraftColor.RED,\n            \"not bold\",\n            Formatting.RESET,\n        ]\n\n    def test_translate_string(self):\n        assert Motd.parse(RawJavaResponseMotdWhenDict(translate=\"the key\")).parsed == [\n            TranslationTag(\"the key\"),\n            Formatting.RESET,\n        ]\n\n    def test_short_text_is_not_considered_as_color(self):\n        \"\"\"See `https://github.com/py-mine/mcstatus/pull/335#discussion_r984535349`_.\"\"\"\n        assert Motd.parse(\"a\").parsed == [\"a\"]\n\n    def test_text_field_contains_formatting(self):\n        \"\"\"See `https://github.com/py-mine/mcstatus/pull/335#issuecomment-1264191303`_.\"\"\"\n        assert Motd.parse({\"text\": \"&aHello!\"}).parsed == [\"\", MinecraftColor.GREEN, \"Hello!\", Formatting.RESET]\n\n    def test_invalid_raw_input(self):\n        obj = object()\n        with pytest.raises(\n            TypeError,\n            match=f\"^Expected list, string or dict data, got <class 'object'> \\\\({obj!r}\\\\), report this!$\",\n        ):\n            Motd.parse(obj)  # pyright: ignore[reportArgumentType]\n\n    def test_invalid_color(self):\n        with pytest.raises(ValueError, match=r\"^Unable to parse color: 'a', report this!$\"):\n            Motd._parse_color(\"a\")\n\n    def test_multiple_times_nested_extras(self):\n        \"\"\"See `https://discord.com/channels/936788458939224094/938591600160956446/1062860329597534258`_.\"\"\"\n        motd = Motd.parse(\n            {\n                \"extra\": [\n                    {\n                        \"extra\": [\n                            {\"extra\": [{\"text\": \"1\"}]},\n                            {\"extra\": [{\"text\": \"2\"}]},\n                            {\"extra\": [{\"text\": \"3\"}]},\n                        ]\n                    },\n                    {\n                        \"extra\": [\n                            {\"extra\": [{\"text\": \"4\"}]},\n                            {\"extra\": [{\"text\": \"5\"}]},\n                            {\"extra\": [{\"text\": \"6\"}]},\n                        ]\n                    },\n                    {\n                        \"extra\": [\n                            {\"extra\": [{\"text\": \"7\"}]},\n                            {\"extra\": [{\"text\": \"8\"}]},\n                            {\"extra\": [{\"text\": \"9\"}]},\n                        ]\n                    },\n                ]\n            }\n        )\n        assert motd.parsed == [\n            Formatting.RESET, Formatting.RESET, Formatting.RESET,\n            \"1\",\n            Formatting.RESET, Formatting.RESET,\n            \"2\",\n            Formatting.RESET, Formatting.RESET,\n            \"3\",\n            Formatting.RESET, Formatting.RESET, Formatting.RESET,\n            \"4\",\n            Formatting.RESET, Formatting.RESET,\n            \"5\",\n            Formatting.RESET, Formatting.RESET,\n            \"6\",\n            Formatting.RESET, Formatting.RESET, Formatting.RESET,\n            \"7\",\n            Formatting.RESET, Formatting.RESET,\n            \"8\",\n            Formatting.RESET, Formatting.RESET,\n            \"9\",\n            Formatting.RESET,\n        ]  # fmt: skip\n\n    def test_raw_attribute(self, source_bedrock):\n        motd = Motd.parse(source_bedrock)\n        assert motd.raw == source_bedrock\n"
  },
  {
    "path": "tests/motd/test_simplifies.py",
    "content": "from __future__ import annotations\n\nfrom contextlib import ExitStack\nfrom unittest import mock\n\nimport pytest\n\nfrom mcstatus.motd import Motd\nfrom mcstatus.motd._simplifies import (\n    get_double_colors,\n    get_double_items,\n    get_empty_text,\n    get_end_non_text,\n    get_formatting_before_color,\n    get_meaningless_resets_and_colors,\n    get_unused_elements,\n)\nfrom mcstatus.motd.components import Formatting, MinecraftColor, TranslationTag, WebColor\n\n\nclass TestMotdSimplifies:\n    def test_get_unused_elements_call_every_simplifier(self):\n        with ExitStack() as stack:\n            mocked = [\n                stack.enter_context(mock.patch(\"mcstatus.motd._simplifies.\" + simplifier))\n                for simplifier in [\n                    get_double_items.__name__,\n                    get_double_colors.__name__,\n                    get_formatting_before_color.__name__,\n                    get_empty_text.__name__,\n                    get_end_non_text.__name__,\n                ]\n            ]\n\n            get_unused_elements([])\n\n            for simplifier in mocked:\n                simplifier.assert_called()\n\n    def test_simplify_returns_new_instance(self):\n        parsed = [\"\", Formatting.RESET]\n        obj = Motd(parsed.copy(), raw=\"\")\n        assert obj.simplify().parsed == []\n        assert obj.parsed == parsed\n\n    def test_simplifies_work(self):\n        get_unused_elements([\"a\", \"b\", \"c\"])\n\n    def test_simplify_runs_few_times(self):\n        \"\"\"See `https://github.com/py-mine/mcstatus/pull/335#discussion_r1051658497`_.\"\"\"\n        obj = Motd([Formatting.BOLD, \"\", Formatting.RESET, \"\", MinecraftColor.RED, \"\"], raw=\"\")\n        assert obj.simplify() == Motd([], raw=\"\")\n\n    @pytest.mark.parametrize(\"first\", [MinecraftColor.RED, WebColor.from_hex(hex=\"#ff0000\")])\n    @pytest.mark.parametrize(\"second\", [MinecraftColor.BLUE, WebColor.from_hex(hex=\"#dd0220\")])\n    def test_get_double_colors(self, first, second):\n        assert get_double_colors([first, second]) == {0}\n\n    @pytest.mark.parametrize(\"first\", [MinecraftColor.RED, WebColor.from_hex(hex=\"#ff0000\")])\n    @pytest.mark.parametrize(\"second\", [MinecraftColor.BLUE, WebColor.from_hex(hex=\"#dd0220\")])\n    @pytest.mark.parametrize(\"third\", [MinecraftColor.BLUE, WebColor.from_hex(hex=\"dd0220\")])\n    def test_get_double_colors_with_three_items(self, first, second, third):\n        assert get_double_colors([first, second, third]) == {0, 1}\n\n    @pytest.mark.parametrize(\"first\", [MinecraftColor.RED, WebColor.from_hex(hex=\"#ff0000\")])\n    @pytest.mark.parametrize(\"second\", [MinecraftColor.BLUE, WebColor.from_hex(hex=\"#dd0220\")])\n    def test_get_double_colors_with_no_double_colors(self, first, second):\n        assert get_double_colors([first, \"\", second]) == set()\n\n    @pytest.mark.parametrize(\"item\", [Formatting.BOLD, MinecraftColor.RED, WebColor.from_hex(hex=\"#ff0000\")])\n    def test_get_double_items(self, item):\n        assert get_double_items([item, item]) == {0}\n\n    @pytest.mark.parametrize(\"item\", [Formatting.BOLD, MinecraftColor.RED, WebColor.from_hex(hex=\"#ff0000\")])\n    def test_get_double_items_with_three_items(self, item):\n        assert get_double_items([item, item, item]) == {0, 1}\n\n    @pytest.mark.parametrize(\"item\", [Formatting.BOLD, MinecraftColor.RED, WebColor.from_hex(hex=\"#ff0000\")])\n    def test_get_double_items_with_no_double_items(self, item):\n        assert get_double_items([item, \"\", item]) == set()\n\n    @pytest.mark.parametrize(\"last_item\", [MinecraftColor.RED, WebColor.from_hex(hex=\"#ff0000\")])\n    def test_get_formatting_before_color(self, last_item):\n        assert get_formatting_before_color([Formatting.BOLD, last_item]) == {0}\n\n    @pytest.mark.parametrize(\"first_item\", [Formatting.RESET, MinecraftColor.RED, WebColor.from_hex(hex=\"#ff0000\"), \"abc\"])\n    def test_get_formatting_before_color_without_formatting_before_color(self, first_item):\n        assert get_formatting_before_color([first_item, \"abc\", MinecraftColor.WHITE]) == set()\n\n    def test_skip_get_formatting_before_color(self):\n        assert get_formatting_before_color([\"abc\", Formatting.BOLD, \"def\", Formatting.RESET, \"ghi\"]) == set()\n\n    @pytest.mark.parametrize(\"last_item\", [MinecraftColor.RED, WebColor.from_hex(hex=\"#ff0000\")])\n    def test_get_formatting_before_color_if_space_between(self, last_item):\n        assert get_formatting_before_color([Formatting.BOLD, \" \", last_item]) == {0}\n\n    def test_get_empty_text_removes_empty_string(self):\n        assert get_empty_text([Formatting.BOLD, \"\", Formatting.RESET, \"\", MinecraftColor.RED, \"\"]) == {1, 3, 5}\n\n    def test_two_formattings_before_minecraft_color(self):\n        \"\"\"See `https://github.com/py-mine/mcstatus/pull/335#discussion_r1048476090`_.\"\"\"\n        assert get_formatting_before_color([Formatting.BOLD, Formatting.ITALIC, MinecraftColor.RED]) == {0, 1}\n\n    def test_two_formattings_one_by_one(self):\n        obj = Motd([Formatting.BOLD, Formatting.ITALIC], raw=\"\")\n        assert obj.simplify().parsed == []\n\n    @pytest.mark.parametrize(\"item\", [Formatting.RESET, MinecraftColor.RED, WebColor.from_hex(hex=\"#ff1234\")])\n    def test_dont_remove_empty_text(self, item):\n        assert get_empty_text([item]) == set()\n\n    @pytest.mark.parametrize(\"last_item\", [Formatting.RESET, MinecraftColor.RED, WebColor.from_hex(hex=\"#ff0000\")])\n    def test_non_text_in_the_end(self, last_item):\n        assert get_end_non_text([\"abc\", Formatting.BOLD, \"def\", Formatting.RESET, \"ghi\", last_item]) == {5}\n\n    def test_translation_tag_in_the_end(self):\n        assert get_end_non_text([\"abc\", Formatting.BOLD, \"def\", Formatting.RESET, \"ghi\", TranslationTag(\"key\")]) == set()\n\n    @pytest.mark.parametrize(\"item\", [Formatting.BOLD, MinecraftColor.RED, WebColor.from_hex(hex=\"#ff0000\")])\n    def test_meaningless_resets_and_colors_active(self, item):\n        assert get_meaningless_resets_and_colors([item, \"foo\", item, \"bar\"]) == {2}\n\n    def test_meaningless_resets_and_colors_reset_nothing(self):\n        assert get_meaningless_resets_and_colors([\"foo\", Formatting.RESET, \"bar\"]) == {1}\n\n    @pytest.mark.parametrize(\"item\", [Formatting.BOLD, MinecraftColor.RED, WebColor.from_hex(hex=\"#ff0000\")])\n    def test_meaningless_resets_and_colors_resets(self, item):\n        assert get_meaningless_resets_and_colors([item, \"foo\", Formatting.RESET, item, \"bar\"]) == set()\n\n    def test_no_conflict_on_poping_items(self):\n        \"\"\"See `https://github.com/py-mine/mcstatus/pull/335#discussion_r1045303652`_.\"\"\"\n        obj = Motd([\"0\", \"1\"], raw=\"\")\n        call_count = 0\n\n        def remove_first_element(*_, **__):\n            nonlocal call_count\n            call_count += 1\n            if call_count in (1, 2):\n                return {0}\n            return set()\n\n        with ExitStack() as stack:\n            for simplifier in [\n                get_double_items.__name__,\n                get_double_colors.__name__,\n                get_formatting_before_color.__name__,\n                get_empty_text.__name__,\n                get_end_non_text.__name__,\n            ]:\n                stack.enter_context(mock.patch(\"mcstatus.motd._simplifies.\" + simplifier, remove_first_element))\n            assert obj.simplify().parsed == [\"1\"]\n\n    def test_simplify_function_provides_the_same_raw(self):\n        obj = object()\n        assert Motd([], raw=obj).simplify().raw is obj  # pyright: ignore[reportArgumentType]\n\n    def test_simplify_do_not_remove_string_contains_only_spaces(self):\n        \"\"\"Those can be used as delimiters.\"\"\"\n        assert Motd([\" \" * 20], raw=\"\").simplify().parsed == [\" \" * 20]\n\n    def test_simplify_meaningless_resets_and_colors(self):\n        assert Motd.parse(\"&a1&a2&a3\").simplify().parsed == [MinecraftColor.GREEN, \"123\"]\n\n    def test_remove_formatting_reset_if_there_was_no_color_or_formatting(self):\n        motd = Motd.parse({\"text\": \"123\", \"extra\": [{\"text\": \"123\"}]})\n        assert motd.parsed == [\"123\", Formatting.RESET, \"123\", Formatting.RESET]\n        assert motd.simplify().parsed == [\"123123\"]\n\n    def test_squash_nearby_strings(self):\n        assert Motd([\"123\", \"123\", \"123\"], raw=\"\").simplify().parsed == [\"123123123\"]\n"
  },
  {
    "path": "tests/motd/test_transformers.py",
    "content": "# ruff: noqa: FBT003 # boolean positional value in `result` fixture\nfrom __future__ import annotations\n\nimport typing\n\nimport pytest\n\nfrom mcstatus.motd import Motd\nfrom mcstatus.motd._transformers import _NothingTransformer\n\nif typing.TYPE_CHECKING:\n    from collections.abc import Callable\n\n    from mcstatus.responses._raw import RawJavaResponseMotd\n\n\ndef test_nothing_transformer():\n    assert _NothingTransformer().transform(Motd.parse(\"&1a&bfoo&r\").parsed) == \"\"\n\n\nclass TestMotdPlain:\n    @pytest.fixture(scope=\"class\")\n    def result(self) -> Callable[[str | RawJavaResponseMotd], str]:\n        return lambda text: Motd.parse(text).to_plain()\n\n    def test_plain_text(self, result):\n        assert result(\"plain\") == \"plain\"\n\n    def test_removes_colors(self, result):\n        assert result(\"&1&ltext\") == \"text\"\n\n    def test_skip_web_colors(self, result):\n        assert result({\"extra\": [{\"color\": \"#4000ff\", \"text\": \"colored text\"}], \"text\": \"\"}) == \"colored text\"\n\n    def test_skip_minecraft_colors(self, result):\n        assert result({\"extra\": [{\"color\": \"red\", \"text\": \"colored text\"}], \"text\": \"\"}) == \"colored text\"\n\n\nclass TestMotdMinecraft:\n    @pytest.fixture(scope=\"class\")\n    def result(self) -> Callable[[str | RawJavaResponseMotd], str]:\n        return lambda text: Motd.parse(text).to_minecraft()\n\n    @pytest.mark.parametrize(\"motd\", [\"&1&2&3\", \"§123§5bc\", \"§1§2§3\"])\n    def test_return_the_same(self, motd: str, result):\n        assert result(motd) == motd.replace(\"&\", \"§\")\n\n    def test_skip_web_colors(self, result):\n        assert result({\"extra\": [{\"color\": \"#4000ff\", \"text\": \"colored text\"}], \"text\": \"\"}) == \"§rcolored text§r\"\n\n\nclass TestMotdHTML:\n    @pytest.fixture(scope=\"class\")\n    def result(self) -> Callable[[str, bool], str]:\n        return lambda text, bedrock: Motd.parse(text, bedrock=bedrock).to_html()\n\n    def test_correct_output_java(self, result: Callable[[str | dict, bool], str], source_java):\n        assert result(source_java, False) == (\n            \"<p>top\"\n            \"1<span style='color:rgb(179, 238, 255)'>2</span>\"\n            \"<span style='color:rgb(0, 0, 0);text-shadow:0 0 1px rgb(0, 0, 0)'><span class=obfuscated>3</span>\"\n            \"</span>\"\n            \"<span style='color:rgb(0, 0, 170);text-shadow:0 0 1px rgb(0, 0, 42)'><b><s>4</span></b></s>\"\n            \"<span style='color:rgb(0, 170, 0);text-shadow:0 0 1px rgb(0, 42, 0)'><i>5</span></i>\"\n            \"<span style='color:rgb(0, 170, 170);text-shadow:0 0 1px rgb(0, 42, 42)'><u>6</span></u>\"\n            \"<span style='color:rgb(0, 170, 170);text-shadow:0 0 1px rgb(0, 42, 42)'>7</span>\"\n            \"<span style='color:rgb(170, 0, 0);text-shadow:0 0 1px rgb(42, 0, 0)'>8</span>\"\n            \"<span style='color:rgb(170, 0, 170);text-shadow:0 0 1px rgb(42, 0, 42)'>9</span>\"\n            \"<span style='color:rgb(255, 170, 0);text-shadow:0 0 1px rgb(64, 42, 0)'>10</span>\"\n            \"<span style='color:rgb(170, 170, 170);text-shadow:0 0 1px rgb(42, 42, 42)'>11</span>\"\n            \"<span style='color:rgb(85, 85, 85);text-shadow:0 0 1px rgb(21, 21, 21)'>12</span>\"\n            \"<span style='color:rgb(85, 85, 255);text-shadow:0 0 1px rgb(21, 21, 63)'>13</span>\"\n            \"<span style='color:rgb(85, 255, 85);text-shadow:0 0 1px rgb(21, 63, 21)'>14</span>\"\n            \"<span style='color:rgb(85, 255, 255);text-shadow:0 0 1px rgb(21, 63, 63)'>15</span>\"\n            \"<span style='color:rgb(255, 85, 85);text-shadow:0 0 1px rgb(63, 21, 21)'>16</span>\"\n            \"<span style='color:rgb(255, 85, 255);text-shadow:0 0 1px rgb(63, 21, 63)'>17</span>\"\n            \"<span style='color:rgb(255, 255, 85);text-shadow:0 0 1px rgb(63, 63, 21)'>18</span>\"\n            \"<span style='color:rgb(255, 255, 255);text-shadow:0 0 1px rgb(63, 63, 63)'>19</span>\"\n            \"20</p>\"\n        )\n\n    def test_correct_output_bedrock(self, result: Callable[[str | dict, bool], str], source_bedrock):\n        assert result(source_bedrock, True) == (\n            \"<p>top\"\n            \"1<span style='color:rgb(179, 238, 255)'>2</span>\"\n            \"<span style='color:rgb(0, 0, 0);text-shadow:0 0 1px rgb(0, 0, 0)'><span class=obfuscated>3</span>\"\n            \"</span>\"\n            \"<span style='color:rgb(0, 0, 170);text-shadow:0 0 1px rgb(0, 0, 42)'><b><s>4</span></b></s>\"\n            \"<span style='color:rgb(0, 170, 0);text-shadow:0 0 1px rgb(0, 42, 0)'><i>5</span></i>\"\n            \"<span style='color:rgb(0, 170, 170);text-shadow:0 0 1px rgb(0, 42, 42)'><u>6</span></u>\"\n            \"<span style='color:rgb(0, 170, 170);text-shadow:0 0 1px rgb(0, 42, 42)'>7</span>\"\n            \"<span style='color:rgb(170, 0, 0);text-shadow:0 0 1px rgb(42, 0, 0)'>8</span>\"\n            \"<span style='color:rgb(170, 0, 170);text-shadow:0 0 1px rgb(42, 0, 42)'>9</span>\"\n            \"<span style='color:rgb(255, 170, 0);text-shadow:0 0 1px rgb(64, 42, 0)'>10</span>\"\n            \"<span style='color:rgb(198, 198, 198);text-shadow:0 0 1px rgb(49, 49, 49)'>11</span>\"\n            \"<span style='color:rgb(85, 85, 85);text-shadow:0 0 1px rgb(21, 21, 21)'>12</span>\"\n            \"<span style='color:rgb(85, 85, 255);text-shadow:0 0 1px rgb(21, 21, 63)'>13</span>\"\n            \"<span style='color:rgb(85, 255, 85);text-shadow:0 0 1px rgb(21, 63, 21)'>14</span>\"\n            \"<span style='color:rgb(85, 255, 255);text-shadow:0 0 1px rgb(21, 63, 63)'>15</span>\"\n            \"<span style='color:rgb(255, 85, 85);text-shadow:0 0 1px rgb(63, 21, 21)'>16</span>\"\n            \"<span style='color:rgb(255, 85, 255);text-shadow:0 0 1px rgb(63, 21, 63)'>17</span>\"\n            \"<span style='color:rgb(255, 255, 85);text-shadow:0 0 1px rgb(63, 63, 21)'>18</span>\"\n            \"<span style='color:rgb(255, 255, 255);text-shadow:0 0 1px rgb(63, 63, 63)'>19</span>\"\n            \"<span style='color:rgb(221, 214, 5);text-shadow:0 0 1px rgb(55, 53, 1)'>20</span>\"\n            \"<span style='color:rgb(227, 212, 209);text-shadow:0 0 1px rgb(56, 53, 52)'>21</span>\"\n            \"<span style='color:rgb(206, 202, 202);text-shadow:0 0 1px rgb(51, 50, 50)'>22</span>\"\n            \"<span style='color:rgb(68, 58, 59);text-shadow:0 0 1px rgb(17, 14, 14)'>23</span>\"\n            \"<span style='color:rgb(151, 22, 7);text-shadow:0 0 1px rgb(37, 5, 1)'>24</span>\"\n            \"<span style='color:rgb(180, 104, 77);text-shadow:0 0 1px rgb(45, 26, 19)'>25</span>\"\n            \"<span style='color:rgb(222, 177, 45);text-shadow:0 0 1px rgb(55, 44, 11)'>26</span>\"\n            \"<span style='color:rgb(17, 159, 54);text-shadow:0 0 1px rgb(4, 40, 13)'>27</span>\"\n            \"<span style='color:rgb(44, 186, 168);text-shadow:0 0 1px rgb(11, 46, 42)'>28</span>\"\n            \"<span style='color:rgb(33, 73, 123);text-shadow:0 0 1px rgb(8, 18, 30)'>29</span>\"\n            \"<span style='color:rgb(154, 92, 198);text-shadow:0 0 1px rgb(38, 23, 49)'>30</span>\"\n            \"<span style='color:rgb(235, 114, 20);text-shadow:0 0 1px rgb(59, 29, 5)'>31</span>\"\n            \"32</p>\"\n        )\n\n    def test_new_line_is_br_tag(self):\n        motd = Motd.parse(\"Some cool\\ntext\")\n        assert motd.to_html() == \"<p>Some cool<br>text</p>\"\n\n\nclass TestMotdAnsi:\n    @pytest.fixture(scope=\"class\")\n    def result(self) -> Callable[[str, bool], str]:\n        return lambda text, bedrock: Motd.parse(text, bedrock=bedrock).to_ansi()\n\n    def test_correct_output_java(self, result: Callable[[str | dict, bool], str], source_java):\n        assert result(source_java, False) == (\n            \"\\033[0mtop\\033[0m\"\n            \"1\\033[0m\"\n            \"\\033[38;2;179;238;255m2\\033[0m\\033[0m\"\n            \"\\033[38;2;0;0;0m\\033[5m3\\033[0m\\033[0m\"\n            \"\\033[38;2;0;0;170m\\033[1m\\033[9m4\\033[0m\\033[0m\"\n            \"\\033[38;2;0;170;0m\\033[3m5\\033[0m\\033[0m\"\n            \"\\033[38;2;0;170;170m\\033[4m6\\033[0m\\033[0m\"\n            \"\\033[38;2;0;170;170m7\\033[0m\\033[0m\"\n            \"\\033[38;2;170;0;0m8\\033[0m\\033[0m\"\n            \"\\033[38;2;170;0;170m9\\033[0m\\033[0m\"\n            \"\\033[38;2;255;170;0m10\\033[0m\\033[0m\"\n            \"\\033[38;2;170;170;170m11\\033[0m\\033[0m\"\n            \"\\033[38;2;85;85;85m12\\033[0m\\033[0m\"\n            \"\\033[38;2;85;85;255m13\\033[0m\\033[0m\"\n            \"\\033[38;2;85;255;85m14\\033[0m\\033[0m\"\n            \"\\033[38;2;85;255;255m15\\033[0m\\033[0m\"\n            \"\\033[38;2;255;85;85m16\\033[0m\\033[0m\"\n            \"\\033[38;2;255;85;255m17\\033[0m\\033[0m\"\n            \"\\033[38;2;255;255;85m18\\033[0m\\033[0m\"\n            \"\\033[38;2;255;255;255m19\\033[0m\\033[0m\"\n            \"20\\033[0m\"\n            \"\\033[0m\\033[0m\"\n        )\n\n    def test_correct_output_bedrock(self, result: Callable[[str | dict, bool], str], source_bedrock):\n        assert result(source_bedrock, True) == (\n            \"\\033[0mtop\\033[0m\"\n            \"1\\033[0m\"\n            \"\\033[38;2;179;238;255m2\\033[0m\\033[0m\"\n            \"\\033[38;2;0;0;0m\\033[5m3\\033[0m\\033[0m\"\n            \"\\033[38;2;0;0;170m\\033[1m\\033[9m4\\033[0m\\033[0m\"\n            \"\\033[38;2;0;170;0m\\033[3m5\\033[0m\\033[0m\"\n            \"\\033[38;2;0;170;170m\\033[4m6\\033[0m\\033[0m\"\n            \"\\033[38;2;0;170;170m7\\033[0m\\033[0m\"\n            \"\\033[38;2;170;0;0m8\\033[0m\\033[0m\"\n            \"\\033[38;2;170;0;170m9\\033[0m\\033[0m\"\n            \"\\033[38;2;255;170;0m10\\033[0m\\033[0m\"\n            \"\\033[38;2;198;198;198m11\\033[0m\\033[0m\"\n            \"\\033[38;2;85;85;85m12\\033[0m\\033[0m\"\n            \"\\033[38;2;85;85;255m13\\033[0m\\033[0m\"\n            \"\\033[38;2;85;255;85m14\\033[0m\\033[0m\"\n            \"\\033[38;2;85;255;255m15\\033[0m\\033[0m\"\n            \"\\033[38;2;255;85;85m16\\033[0m\\033[0m\"\n            \"\\033[38;2;255;85;255m17\\033[0m\\033[0m\"\n            \"\\033[38;2;255;255;85m18\\033[0m\\033[0m\"\n            \"\\033[38;2;255;255;255m19\\033[0m\\033[0m\"\n            \"\\033[38;2;221;214;5m20\\033[0m\\033[0m\"\n            \"\\033[38;2;227;212;209m21\\033[0m\\033[0m\"\n            \"\\033[38;2;206;202;202m22\\033[0m\\033[0m\"\n            \"\\033[38;2;68;58;59m23\\033[0m\\033[0m\"\n            \"\\033[38;2;151;22;7m24\\033[0m\\033[0m\"\n            \"\\033[38;2;180;104;77m25\\033[0m\\033[0m\"\n            \"\\033[38;2;222;177;45m26\\033[0m\\033[0m\"\n            \"\\033[38;2;17;159;54m27\\033[0m\\033[0m\"\n            \"\\033[38;2;44;186;168m28\\033[0m\\033[0m\"\n            \"\\033[38;2;33;73;123m29\\033[0m\\033[0m\"\n            \"\\033[38;2;154;92;198m30\\033[0m\\033[0m\"\n            \"\\033[38;2;235;114;20m31\\033[0m\\033[0m\"\n            \"32\\033[0m\"\n            \"\\033[0m\\033[0m\"\n        )\n"
  },
  {
    "path": "tests/net/__init__.py",
    "content": ""
  },
  {
    "path": "tests/net/test_address.py",
    "content": "from __future__ import annotations\n\nimport ipaddress\nimport sys\nfrom pathlib import Path\nfrom typing import cast\nfrom unittest.mock import MagicMock, Mock, patch\n\nimport dns.resolver\nimport pytest\nfrom dns.rdatatype import RdataType\n\nfrom mcstatus._net.address import Address, async_minecraft_srv_address_lookup, minecraft_srv_address_lookup\n\n\nclass TestSRVLookup:\n    @pytest.mark.parametrize(\"exception\", [dns.resolver.NXDOMAIN, dns.resolver.NoAnswer])\n    def test_address_no_srv(self, exception):\n        with patch(\"dns.resolver.resolve\") as resolve:\n            resolve.side_effect = [exception]\n            address = minecraft_srv_address_lookup(\"example.org\", default_port=25565, lifetime=3)\n            resolve.assert_called_once_with(\"_minecraft._tcp.example.org\", RdataType.SRV, lifetime=3, search=True)\n\n        assert address.host == \"example.org\"\n        assert address.port == 25565\n\n    @pytest.mark.parametrize(\"exception\", [dns.resolver.NXDOMAIN, dns.resolver.NoAnswer])\n    def test_address_no_srv_no_default_port(self, exception):\n        with patch(\"dns.resolver.resolve\") as resolve:\n            resolve.side_effect = [exception]\n            with pytest.raises(ValueError, match=r\"^Given address 'example.org' doesn't contain port\"):\n                minecraft_srv_address_lookup(\"example.org\", lifetime=3)\n            resolve.assert_called_once_with(\"_minecraft._tcp.example.org\", RdataType.SRV, lifetime=3, search=True)\n\n    def test_address_with_srv(self):\n        with patch(\"dns.resolver.resolve\") as resolve:\n            answer = Mock()\n            answer.target = \"different.example.org.\"\n            answer.port = 12345\n            resolve.return_value = [answer]\n\n            address = minecraft_srv_address_lookup(\"example.org\", lifetime=3)\n            resolve.assert_called_once_with(\"_minecraft._tcp.example.org\", RdataType.SRV, lifetime=3, search=True)\n        assert address.host == \"different.example.org\"\n        assert address.port == 12345\n\n    @pytest.mark.asyncio\n    @pytest.mark.parametrize(\"exception\", [dns.resolver.NXDOMAIN, dns.resolver.NoAnswer])\n    async def test_async_address_no_srv(self, exception):\n        with patch(\"dns.asyncresolver.resolve\") as resolve:\n            resolve.side_effect = [exception]\n            address = await async_minecraft_srv_address_lookup(\"example.org\", default_port=25565, lifetime=3)\n            resolve.assert_called_once_with(\"_minecraft._tcp.example.org\", RdataType.SRV, lifetime=3, search=True)\n\n        assert address.host == \"example.org\"\n        assert address.port == 25565\n\n    @pytest.mark.asyncio\n    @pytest.mark.parametrize(\"exception\", [dns.resolver.NXDOMAIN, dns.resolver.NoAnswer])\n    async def test_async_address_no_srv_no_default_port(self, exception):\n        with patch(\"dns.asyncresolver.resolve\") as resolve:\n            resolve.side_effect = [exception]\n            with pytest.raises(ValueError, match=r\"^Given address 'example.org' doesn't contain port\"):\n                await async_minecraft_srv_address_lookup(\"example.org\", lifetime=3)\n            resolve.assert_called_once_with(\"_minecraft._tcp.example.org\", RdataType.SRV, lifetime=3, search=True)\n\n    @pytest.mark.asyncio\n    async def test_async_address_with_srv(self):\n        with patch(\"dns.asyncresolver.resolve\") as resolve:\n            answer = Mock()\n            answer.target = \"different.example.org.\"\n            answer.port = 12345\n            resolve.return_value = [answer]\n\n            address = await async_minecraft_srv_address_lookup(\"example.org\", lifetime=3)\n            resolve.assert_called_once_with(\"_minecraft._tcp.example.org\", RdataType.SRV, lifetime=3, search=True)\n        assert address.host == \"different.example.org\"\n        assert address.port == 12345\n\n\nclass TestAddressValidity:\n    @pytest.mark.parametrize(\n        (\"address\", \"port\"),\n        [\n            (\"example.org\", 25565),\n            (\"192.168.0.100\", 54321),\n            (\"2345:0425:2CA1:0000:0000:0567:5673:23b5\", 100),\n            (\"2345:0425:2CA1::0567:5673:23b5\", 12345),\n        ],\n    )\n    def test_address_validation_valid(self, address, port):\n        Address._ensure_validity(address, port)\n\n    @pytest.mark.parametrize(\n        (\"address\", \"port\"),\n        [\n            (\"example.org\", 100_000),\n            (\"example.org\", -1),\n        ],\n    )\n    def test_address_validation_range(self, address, port):\n        with pytest.raises(ValueError, match=f\"^Port must be within the allowed range \\\\(0-2\\\\^16\\\\), got {port}$\"):\n            Address._ensure_validity(address, port)\n\n    def test_address_validation_port_invalid_type(self):\n        with pytest.raises(TypeError, match=r\"^Port must be an integer port number, got <class 'str'> \\('25565'\\)$\"):\n            Address._ensure_validity(\"example.org\", \"25565\")\n\n    @pytest.mark.parametrize(\n        (\"address\", \"port\"),\n        [(25565, \"example.org\"), (0, 0)],\n    )\n    def test_address_validation_host_invalid_type(self, address, port):\n        with pytest.raises(TypeError, match=f\"^Host must be a string address, got {type(address)!r} \\\\({address!r}\\\\)$\"):\n            Address._ensure_validity(address, port)\n\n    def test_address_host_invalid_format(self):\n        with pytest.raises(ValueError, match=r\"^Invalid address 'hello@#', can't parse\\.$\"):\n            Address.parse_address(\"hello@#\")\n\n\nclass TestAddressConstructing:\n    def test_init_constructor(self):\n        addr = Address(\"example.org\", 25565)\n        assert addr.host == \"example.org\"\n        assert addr.port == 25565\n\n    def test_tuple_behavior(self):\n        addr = Address(\"example.org\", 25565)\n        assert isinstance(addr, tuple)\n        assert len(addr) == 2\n        assert addr[0] == \"example.org\"\n        assert addr[1] == 25565\n\n    def test_from_tuple_constructor(self):\n        addr = Address.from_tuple((\"example.org\", 12345))\n        assert addr.host == \"example.org\"\n        assert addr.port == 12345\n\n    def test_from_path_constructor(self):\n        addr = Address.from_path(Path(\"example.org:25565\"))\n        assert addr.host == \"example.org\"\n        assert addr.port == 25565\n\n    def test_address_with_port_no_default(self):\n        addr = Address.parse_address(\"example.org:25565\")\n        assert addr.host == \"example.org\"\n        assert addr.port == 25565\n\n    def test_address_with_port_default(self):\n        addr = Address.parse_address(\"example.org:25565\", default_port=12345)\n        assert addr.host == \"example.org\"\n        assert addr.port == 25565\n\n    def test_address_without_port_default(self):\n        addr = Address.parse_address(\"example.org\", default_port=12345)\n        assert addr.host == \"example.org\"\n        assert addr.port == 12345\n\n    def test_address_without_port(self):\n        with pytest.raises(\n            ValueError,\n            match=r\"^Given address 'example.org' doesn't contain port and default_port wasn't specified, can't parse.$\",\n        ):\n            Address.parse_address(\"example.org\")\n\n    def test_address_with_invalid_port(self):\n        with pytest.raises(ValueError, match=r\"^Port could not be cast to integer value as 'port'$\"):\n            Address.parse_address(\"example.org:port\")\n\n    def test_address_with_multiple_ports(self):\n        with pytest.raises(ValueError, match=r\"^Port could not be cast to integer value as '12345:25565'$\"):\n            Address.parse_address(\"example.org:12345:25565\")\n\n\nclass TestAddressIPResolving:\n    def setup_method(self):\n        self.host_addr = Address(\"example.org\", 25565)\n        self.ipv4_addr = Address(\"1.1.1.1\", 25565)\n        self.ipv6_addr = Address(\"::1\", 25565)\n\n    def test_ip_resolver_with_hostname(self):\n        with patch(\"dns.resolver.resolve\") as resolve:\n            answer = MagicMock()\n            cast(\"MagicMock\", answer.__str__).return_value = \"48.225.1.104.\"\n            resolve.return_value = [answer]\n\n            resolved_ip = self.host_addr.resolve_ip(lifetime=3)\n\n            resolve.assert_called_once_with(self.host_addr.host, RdataType.A, lifetime=3, search=True)\n            assert isinstance(resolved_ip, ipaddress.IPv4Address)\n            assert str(resolved_ip) == \"48.225.1.104\"\n\n    @pytest.mark.asyncio\n    async def test_async_ip_resolver_with_hostname(self):\n        with patch(\"dns.asyncresolver.resolve\") as resolve:\n            answer = MagicMock()\n            cast(\"MagicMock\", answer.__str__).return_value = \"48.225.1.104.\"\n            resolve.return_value = [answer]\n\n            resolved_ip = await self.host_addr.async_resolve_ip(lifetime=3)\n\n            resolve.assert_called_once_with(self.host_addr.host, RdataType.A, lifetime=3, search=True)\n            assert isinstance(resolved_ip, ipaddress.IPv4Address)\n            assert str(resolved_ip) == \"48.225.1.104\"\n\n    @pytest.mark.parametrize(\"ip_version\", [\"ipv4_addr\", \"ipv6_addr\"])\n    def test_ip_resolver_cache(self, ip_version: str):\n        with patch(\"dns.resolver.resolve\"), patch(\"ipaddress.ip_address\") as resolve:\n            assert getattr(self, ip_version).resolve_ip(lifetime=3) is getattr(self, ip_version).resolve_ip(lifetime=3)\n            resolve.assert_called_once()  # Make sure we didn't needlessly try to resolve\n\n    @pytest.mark.asyncio\n    @pytest.mark.parametrize(\"ip_version\", [\"ipv4_addr\", \"ipv6_addr\"])\n    async def test_async_ip_resolver_cache(self, ip_version: str):\n        with patch(\"dns.resolver.resolve\"), patch(\"ipaddress.ip_address\") as resolve:\n            assert await getattr(self, ip_version).async_resolve_ip(lifetime=3) is await getattr(\n                self, ip_version\n            ).async_resolve_ip(lifetime=3)\n            resolve.assert_called_once()  # Make sure we didn't needlessly try to resolve\n\n    def test_ip_resolver_with_ipv4(self):\n        with patch(\"dns.resolver.resolve\") as resolve:\n            resolved_ip = self.ipv4_addr.resolve_ip(lifetime=3)\n\n            resolve.assert_not_called()  # Make sure we didn't needlessly try to resolve\n            assert isinstance(resolved_ip, ipaddress.IPv4Address)\n            assert str(resolved_ip) == self.ipv4_addr.host\n\n    @pytest.mark.asyncio\n    async def test_async_ip_resolver_with_ipv4(self):\n        with patch(\"dns.asyncresolver.resolve\") as resolve:\n            resolved_ip = await self.ipv4_addr.async_resolve_ip(lifetime=3)\n\n            resolve.assert_not_called()  # Make sure we didn't needlessly try to resolve\n            assert isinstance(resolved_ip, ipaddress.IPv4Address)\n            assert str(resolved_ip) == self.ipv4_addr.host\n\n    def test_ip_resolver_with_ipv6(self):\n        with patch(\"dns.resolver.resolve\") as resolve:\n            resolved_ip = self.ipv6_addr.resolve_ip(lifetime=3)\n\n            resolve.assert_not_called()  # Make sure we didn't needlessly try to resolve\n            assert isinstance(resolved_ip, ipaddress.IPv6Address)\n            assert str(resolved_ip) == self.ipv6_addr.host\n\n    @pytest.mark.asyncio\n    async def test_async_ip_resolver_with_ipv6(self):\n        with patch(\"dns.asyncresolver.resolve\") as resolve:\n            resolved_ip = await self.ipv6_addr.async_resolve_ip(lifetime=3)\n\n            resolve.assert_not_called()  # Make sure we didn't needlessly try to resolve\n            assert isinstance(resolved_ip, ipaddress.IPv6Address)\n            assert str(resolved_ip) == self.ipv6_addr.host\n\n    def test_resolve_localhost(self):\n        addr = Address(\"localhost\", 25565)\n\n        context_manager = pytest.warns(RuntimeWarning) if sys.platform == \"darwin\" else MagicMock()\n        with context_manager:\n            assert addr.resolve_ip() == ipaddress.ip_address(\"127.0.0.1\")\n\n    @pytest.mark.asyncio\n    async def test_async_resolve_localhost(self):\n        addr = Address(\"localhost\", 25565)\n\n        context_manager = pytest.warns(RuntimeWarning) if sys.platform == \"darwin\" else MagicMock()\n        with context_manager:\n            assert await addr.async_resolve_ip() == ipaddress.ip_address(\"127.0.0.1\")\n"
  },
  {
    "path": "tests/protocol/__init__.py",
    "content": ""
  },
  {
    "path": "tests/protocol/test_async_support.py",
    "content": "from inspect import iscoroutinefunction\n\nfrom mcstatus._protocol.connection import TCPAsyncSocketConnection, UDPAsyncSocketConnection\n\n\ndef test_is_completely_asynchronous():\n    conn = TCPAsyncSocketConnection\n    assertions = 0\n    for attribute in dir(conn):\n        if attribute.startswith(\"read_\"):\n            assert iscoroutinefunction(getattr(conn, attribute))\n            assertions += 1\n    assert assertions > 0, \"None of the read_* attributes were async\"\n\n\ndef test_query_is_completely_asynchronous():\n    conn = UDPAsyncSocketConnection\n    assertions = 0\n    for attribute in dir(conn):\n        if attribute.startswith(\"read_\"):\n            assert iscoroutinefunction(getattr(conn, attribute))\n            assertions += 1\n    assert assertions > 0, \"None of the read_* attributes were async\"\n"
  },
  {
    "path": "tests/protocol/test_bedrock_client.py",
    "content": "import sys\nimport time\nfrom unittest import mock\n\nimport pytest\n\nfrom mcstatus._net.address import Address\nfrom mcstatus._protocol.bedrock_client import BedrockClient\nfrom mcstatus.responses import BedrockStatusResponse\n\n\ndef test_bedrock_response_is_expected_type():\n    data = (\n        b\"\\x1c\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x004GT\\x00\\xb8\\x83D\\xde\\x00\\xff\\xff\\x00\\xfe\\xfe\\xfe\\xfe\\xfd\\xfd\\xfd\\xfd\"\n        b\"\\x124Vx\\x00wMCPE;\\xc2\\xa7r\\xc2\\xa74G\\xc2\\xa7r\\xc2\\xa76a\\xc2\\xa7r\\xc2\\xa7ey\\xc2\\xa7r\\xc2\\xa72B\\xc2\\xa7r\\xc2\"\n        b\"\\xa71o\\xc2\\xa7r\\xc2\\xa79w\\xc2\\xa7r\\xc2\\xa7ds\\xc2\\xa7r\\xc2\\xa74e\\xc2\\xa7r\\xc2\\xa76r;422;;1;69;376707197539105\"\n        b\"3022;;Default;1;19132;-1;\"\n    )\n    parsed = BedrockClient.parse_response(data, 1)\n    assert isinstance(parsed, BedrockStatusResponse)\n\n\n@pytest.mark.flaky(reruns=5, condition=sys.platform.startswith(\"win32\"))\ndef test_latency_is_real_number():\n    \"\"\"``time.perf_counter`` returns fractional seconds, we must convert it to milliseconds.\"\"\"\n\n    def mocked_read_status():\n        time.sleep(0.001)\n        return mock.DEFAULT\n\n    bedrock_client = BedrockClient(Address(\"localhost\", 25565))\n    with (\n        mock.patch.object(bedrock_client, \"_read_status\") as mocked_read,\n        mock.patch.object(bedrock_client, \"parse_response\") as mocked_parse_response,\n    ):\n        mocked_read.side_effect = mocked_read_status\n\n        bedrock_client.read_status()\n\n        # we slept 1ms, so this should be always ~1.\n        assert mocked_parse_response.call_args[0][1] >= 1\n\n\n@pytest.mark.asyncio\n@pytest.mark.flaky(reruns=5, condition=sys.platform.startswith(\"win32\"))\nasync def test_async_latency_is_real_number():\n    \"\"\"``time.perf_counter`` returns fractional seconds, we must convert it to milliseconds.\"\"\"\n\n    def mocked_read_status():\n        time.sleep(0.001)\n        return mock.DEFAULT\n\n    bedrock_client = BedrockClient(Address(\"localhost\", 25565))\n    with (\n        mock.patch.object(bedrock_client, \"_read_status_async\") as mocked_read,\n        mock.patch.object(bedrock_client, \"parse_response\") as mocked_parse_response,\n    ):\n        mocked_read.side_effect = mocked_read_status\n\n        await bedrock_client.read_status_async()\n\n        # we slept 1ms, so this should be always ~1.\n        assert mocked_parse_response.call_args[0][1] >= 1\n"
  },
  {
    "path": "tests/protocol/test_connection.py",
    "content": "from unittest.mock import Mock, patch\n\nimport pytest\n\nfrom mcstatus._net.address import Address\nfrom mcstatus._protocol.connection import Connection, TCPSocketConnection, UDPSocketConnection\n\n\nclass TestConnection:\n    connection: Connection\n\n    def setup_method(self):\n        self.connection = Connection()\n\n    def test_flush(self):\n        self.connection.sent = bytearray.fromhex(\"7FAABB\")\n\n        assert self.connection.flush() == bytearray.fromhex(\"7FAABB\")\n        assert self.connection.sent == bytearray()\n\n    def test_receive(self):\n        self.connection.receive(bytearray.fromhex(\"7F\"))\n        self.connection.receive(bytearray.fromhex(\"AABB\"))\n\n        assert self.connection.received == bytearray.fromhex(\"7FAABB\")\n\n    def test_remaining(self):\n        self.connection.receive(bytearray.fromhex(\"7F\"))\n        self.connection.receive(bytearray.fromhex(\"AABB\"))\n\n        assert self.connection.remaining() == 3\n\n    def test_send(self):\n        self.connection.write(bytearray.fromhex(\"7F\"))\n        self.connection.write(bytearray.fromhex(\"AABB\"))\n\n        assert self.connection.flush() == bytearray.fromhex(\"7FAABB\")\n\n    def test_read(self):\n        self.connection.receive(bytearray.fromhex(\"7FAABB\"))\n\n        assert self.connection.read(2) == bytearray.fromhex(\"7FAA\")\n        assert self.connection.read(1) == bytearray.fromhex(\"BB\")\n\n    def _assert_varint_read_write(self, hexstr, value) -> None:\n        self.connection.receive(bytearray.fromhex(hexstr))\n        assert self.connection.read_varint() == value\n\n        self.connection.write_varint(value)\n        assert self.connection.flush() == bytearray.fromhex(hexstr)\n\n    def test_varint_cases(self):\n        self._assert_varint_read_write(\"00\", 0)\n        self._assert_varint_read_write(\"01\", 1)\n        self._assert_varint_read_write(\"0F\", 15)\n        self._assert_varint_read_write(\"FFFFFFFF07\", 2147483647)\n\n        self._assert_varint_read_write(\"FFFFFFFF0F\", -1)\n        self._assert_varint_read_write(\"8080808008\", -2147483648)\n\n    def test_read_invalid_varint(self):\n        self.connection.receive(bytearray.fromhex(\"FFFFFFFF80\"))\n\n        with pytest.raises(IOError, match=r\"^Received varint is too big!$\"):\n            self.connection.read_varint()\n\n    def test_write_invalid_varint(self):\n        with pytest.raises(ValueError, match=r'^The value \"2147483648\" is too big to send in a varint$'):\n            self.connection.write_varint(2147483648)\n        with pytest.raises(ValueError, match=r'^The value \"-2147483649\" is too big to send in a varint$'):\n            self.connection.write_varint(-2147483649)\n\n    def test_read_utf(self):\n        self.connection.receive(bytearray.fromhex(\"0D48656C6C6F2C20776F726C6421\"))\n\n        assert self.connection.read_utf() == \"Hello, world!\"\n\n    def test_write_utf(self):\n        self.connection.write_utf(\"Hello, world!\")\n\n        assert self.connection.flush() == bytearray.fromhex(\"0D48656C6C6F2C20776F726C6421\")\n\n    def test_read_empty_utf(self):\n        self.connection.write_utf(\"\")\n\n        assert self.connection.flush() == bytearray.fromhex(\"00\")\n\n    def test_read_ascii(self):\n        self.connection.receive(bytearray.fromhex(\"48656C6C6F2C20776F726C642100\"))\n\n        assert self.connection.read_ascii() == \"Hello, world!\"\n\n    def test_write_ascii(self):\n        self.connection.write_ascii(\"Hello, world!\")\n\n        assert self.connection.flush() == bytearray.fromhex(\"48656C6C6F2C20776F726C642100\")\n\n    def test_read_empty_ascii(self):\n        self.connection.write_ascii(\"\")\n\n        assert self.connection.flush() == bytearray.fromhex(\"00\")\n\n    def test_read_short_negative(self):\n        self.connection.receive(bytearray.fromhex(\"8000\"))\n\n        assert self.connection.read_short() == -32768\n\n    def test_write_short_negative(self):\n        self.connection.write_short(-32768)\n\n        assert self.connection.flush() == bytearray.fromhex(\"8000\")\n\n    def test_read_short_positive(self):\n        self.connection.receive(bytearray.fromhex(\"7FFF\"))\n\n        assert self.connection.read_short() == 32767\n\n    def test_write_short_positive(self):\n        self.connection.write_short(32767)\n\n        assert self.connection.flush() == bytearray.fromhex(\"7FFF\")\n\n    def test_read_ushort_positive(self):\n        self.connection.receive(bytearray.fromhex(\"8000\"))\n\n        assert self.connection.read_ushort() == 32768\n\n    def test_write_ushort_positive(self):\n        self.connection.write_ushort(32768)\n\n        assert self.connection.flush() == bytearray.fromhex(\"8000\")\n\n    def test_read_int_negative(self):\n        self.connection.receive(bytearray.fromhex(\"80000000\"))\n\n        assert self.connection.read_int() == -2147483648\n\n    def test_write_int_negative(self):\n        self.connection.write_int(-2147483648)\n\n        assert self.connection.flush() == bytearray.fromhex(\"80000000\")\n\n    def test_read_int_positive(self):\n        self.connection.receive(bytearray.fromhex(\"7FFFFFFF\"))\n\n        assert self.connection.read_int() == 2147483647\n\n    def test_write_int_positive(self):\n        self.connection.write_int(2147483647)\n\n        assert self.connection.flush() == bytearray.fromhex(\"7FFFFFFF\")\n\n    def test_read_uint_positive(self):\n        self.connection.receive(bytearray.fromhex(\"80000000\"))\n\n        assert self.connection.read_uint() == 2147483648\n\n    def test_write_uint_positive(self):\n        self.connection.write_uint(2147483648)\n\n        assert self.connection.flush() == bytearray.fromhex(\"80000000\")\n\n    def test_read_long_negative(self):\n        self.connection.receive(bytearray.fromhex(\"8000000000000000\"))\n\n        assert self.connection.read_long() == -9223372036854775808\n\n    def test_write_long_negative(self):\n        self.connection.write_long(-9223372036854775808)\n\n        assert self.connection.flush() == bytearray.fromhex(\"8000000000000000\")\n\n    def test_read_long_positive(self):\n        self.connection.receive(bytearray.fromhex(\"7FFFFFFFFFFFFFFF\"))\n\n        assert self.connection.read_long() == 9223372036854775807\n\n    def test_write_long_positive(self):\n        self.connection.write_long(9223372036854775807)\n\n        assert self.connection.flush() == bytearray.fromhex(\"7FFFFFFFFFFFFFFF\")\n\n    def test_read_ulong_positive(self):\n        self.connection.receive(bytearray.fromhex(\"8000000000000000\"))\n\n        assert self.connection.read_ulong() == 9223372036854775808\n\n    def test_write_ulong_positive(self):\n        self.connection.write_ulong(9223372036854775808)\n\n        assert self.connection.flush() == bytearray.fromhex(\"8000000000000000\")\n\n    @pytest.mark.parametrize((\"as_bytes\", \"as_bool\"), [(\"01\", True), (\"00\", False)])\n    def test_read_bool(self, as_bytes: str, as_bool: bool) -> None:\n        self.connection.receive(bytearray.fromhex(as_bytes))\n\n        assert self.connection.read_bool() is as_bool\n\n    @pytest.mark.parametrize((\"as_bytes\", \"as_bool\"), [(\"01\", True), (\"00\", False)])\n    def test_write_bool(self, as_bytes: str, as_bool: bool) -> None:\n        self.connection.write_bool(as_bool)\n\n        assert self.connection.flush() == bytearray.fromhex(as_bytes)\n\n    def test_read_buffer(self):\n        self.connection.receive(bytearray.fromhex(\"027FAA\"))\n        buffer = self.connection.read_buffer()\n\n        assert buffer.received == bytearray.fromhex(\"7FAA\")\n        assert self.connection.flush() == bytearray()\n\n    def test_write_buffer(self):\n        buffer = Connection()\n        buffer.write(bytearray.fromhex(\"7FAA\"))\n        self.connection.write_buffer(buffer)\n\n        assert self.connection.flush() == bytearray.fromhex(\"027FAA\")\n\n    def test_read_empty(self):\n        self.connection.received = bytearray()\n\n        with pytest.raises(IOError, match=r\"^Not enough data to read! 0 < 1$\"):\n            self.connection.read(1)\n\n    def test_read_not_enough(self):\n        self.connection.received = bytearray(b\"a\")\n\n        with pytest.raises(IOError, match=r\"^Not enough data to read! 1 < 2$\"):\n            self.connection.read(2)\n\n\nclass TestTCPSocketConnection:\n    @pytest.fixture(scope=\"class\")\n    def connection(self):\n        test_addr = Address(\"localhost\", 1234)\n\n        socket = Mock()\n        socket.recv = Mock()\n        socket.send = Mock()\n        with patch(\"socket.create_connection\") as create_connection:\n            create_connection.return_value = socket\n            with TCPSocketConnection(test_addr) as connection:\n                yield connection\n\n    def test_flush(self, connection):\n        with pytest.raises(TypeError, match=r\"^TCPSocketConnection does not support flush\\(\\)$\"):\n            connection.flush()\n\n    def test_receive(self, connection):\n        with pytest.raises(TypeError, match=r\"^TCPSocketConnection does not support receive\\(\\)$\"):\n            connection.receive(\"\")\n\n    def test_remaining(self, connection):\n        with pytest.raises(TypeError, match=r\"^TCPSocketConnection does not support remaining\\(\\)$\"):\n            connection.remaining()\n\n    def test_read(self, connection):\n        connection.socket.recv.return_value = bytearray.fromhex(\"7FAA\")\n\n        assert connection.read(2) == bytearray.fromhex(\"7FAA\")\n\n    def test_read_empty(self, connection):\n        connection.socket.recv.return_value = bytearray()\n\n        with pytest.raises(IOError, match=r\"^Server did not respond with any information!$\"):\n            connection.read(1)\n\n    def test_read_not_enough(self, connection):\n        connection.socket.recv.side_effect = [bytearray(b\"a\"), bytearray()]\n\n        with pytest.raises(IOError, match=r\"^Server did not respond with any information!$\"):\n            connection.read(2)\n\n    def test_write(self, connection):\n        connection.write(bytearray.fromhex(\"7FAA\"))\n\n        connection.socket.send.assert_called_once_with(bytearray.fromhex(\"7FAA\"))\n\n\nclass TestUDPSocketConnection:\n    @pytest.fixture(scope=\"class\")\n    def connection(self):\n        test_addr = Address(\"localhost\", 1234)\n\n        socket = Mock()\n        socket.recvfrom = Mock()\n        socket.sendto = Mock()\n        with patch(\"socket.socket\") as create_socket:\n            create_socket.return_value = socket\n            with UDPSocketConnection(test_addr) as connection:\n                yield connection\n\n    def test_flush(self, connection):\n        with pytest.raises(TypeError, match=r\"^UDPSocketConnection does not support flush\\(\\)$\"):\n            connection.flush()\n\n    def test_receive(self, connection):\n        with pytest.raises(TypeError, match=r\"^UDPSocketConnection does not support receive\\(\\)$\"):\n            connection.receive(\"\")\n\n    def test_remaining(self, connection):\n        assert connection.remaining() == 65535\n\n    def test_read(self, connection):\n        connection.socket.recvfrom.return_value = [bytearray.fromhex(\"7FAA\")]\n\n        assert connection.read(2) == bytearray.fromhex(\"7FAA\")\n\n    def test_read_empty(self, connection):\n        connection.socket.recvfrom.return_value = []\n\n        with pytest.raises(IndexError, match=r\"^list index out of range$\"):\n            connection.read(1)\n\n    def test_write(self, connection):\n        connection.write(bytearray.fromhex(\"7FAA\"))\n\n        connection.socket.sendto.assert_called_once_with(\n            bytearray.fromhex(\"7FAA\"),\n            Address(\"localhost\", 1234),\n        )\n"
  },
  {
    "path": "tests/protocol/test_java_client.py",
    "content": "import sys\nimport time\nfrom unittest import mock\n\nimport pytest\n\nfrom mcstatus._net.address import Address\nfrom mcstatus._protocol.connection import Connection\nfrom mcstatus._protocol.java_client import JavaClient\n\n\nclass TestJavaClient:\n    def setup_method(self):\n        self.java_client = JavaClient(\n            Connection(),  # pyright: ignore[reportArgumentType]\n            address=Address(\"localhost\", 25565),\n            version=44,\n        )\n\n    def test_handshake(self):\n        self.java_client.handshake()\n\n        assert self.java_client.connection.flush() == bytearray.fromhex(\"0F002C096C6F63616C686F737463DD01\")\n\n    def test_read_status(self):\n        self.java_client.connection.receive(\n            bytearray.fromhex(\n                \"7200707B226465736372697074696F6E223A2241204D696E65637261667420536572766572222C22706C6179657273223A7B2\"\n                \"26D6178223A32302C226F6E6C696E65223A307D2C2276657273696F6E223A7B226E616D65223A22312E382D70726531222C22\"\n                \"70726F746F636F6C223A34347D7D\"\n            )\n        )\n        status = self.java_client.read_status()\n\n        assert status.raw == {\n            \"description\": \"A Minecraft Server\",\n            \"players\": {\"max\": 20, \"online\": 0},\n            \"version\": {\"name\": \"1.8-pre1\", \"protocol\": 44},\n        }\n        assert self.java_client.connection.flush() == bytearray.fromhex(\"0100\")\n\n    def test_read_status_invalid_json(self):\n        self.java_client.connection.receive(bytearray.fromhex(\"0300017B\"))\n        with pytest.raises(IOError, match=r\"^Received invalid JSON$\"):\n            self.java_client.read_status()\n\n    def test_read_status_invalid_reply(self):\n        self.java_client.connection.receive(\n            # no motd, see also #922\n            bytearray.fromhex(\n                \"4F004D7B22706C6179657273223A7B226D6178223A32302C226F6E6C696E65223A307D2C2276657273696F6E223A7B226E616\"\n                \"D65223A22312E382D70726531222C2270726F746F636F6C223A34347D7D\"\n            )\n        )\n\n        self.java_client.read_status()\n\n    def test_read_status_invalid_status(self):\n        self.java_client.connection.receive(bytearray.fromhex(\"0105\"))\n\n        with pytest.raises(IOError, match=r\"^Received invalid status response packet.$\"):\n            self.java_client.read_status()\n\n    def test_test_ping(self):\n        self.java_client.connection.receive(bytearray.fromhex(\"09010000000000DD7D1C\"))\n        self.java_client.ping_token = 14515484\n\n        assert self.java_client.test_ping() >= 0\n        assert self.java_client.connection.flush() == bytearray.fromhex(\"09010000000000DD7D1C\")\n\n    def test_test_ping_invalid(self):\n        self.java_client.connection.receive(bytearray.fromhex(\"011F\"))\n        self.java_client.ping_token = 14515484\n\n        with pytest.raises(IOError, match=r\"^Received invalid ping response packet.$\"):\n            self.java_client.test_ping()\n\n    def test_test_ping_wrong_token(self):\n        self.java_client.connection.receive(bytearray.fromhex(\"09010000000000DD7D1C\"))\n        self.java_client.ping_token = 12345\n\n        with pytest.raises(IOError, match=r\"^Received mangled ping response \\(expected token 12345, got 14515484\\)$\"):\n            self.java_client.test_ping()\n\n    @pytest.mark.flaky(reruns=5, condition=sys.platform.startswith(\"win32\"))\n    def test_latency_is_real_number(self):\n        \"\"\"``time.perf_counter`` returns fractional seconds, we must convert it to milliseconds.\"\"\"\n\n        def mocked_read_buffer():\n            time.sleep(0.001)\n            return mock.DEFAULT\n\n        with mock.patch.object(Connection, \"read_buffer\") as mocked:\n            mocked.side_effect = mocked_read_buffer\n            mocked.return_value.read_varint.return_value = 0\n            mocked.return_value.read_utf.return_value = \"\"\"\n            {\n                \"description\": \"A Minecraft Server\",\n                \"players\": {\"max\": 20, \"online\": 0},\n                \"version\": {\"name\": \"1.8-pre1\", \"protocol\": 44}\n            }\n            \"\"\"\n            java_client = JavaClient(\n                Connection(),  # pyright: ignore[reportArgumentType]\n                address=Address(\"localhost\", 25565),\n                version=44,\n            )\n\n            java_client.connection.receive(\n                bytearray.fromhex(\n                    \"7200707B226465736372697074696F6E223A2241204D696E65637261667420536572766572222C22706C6179657273223A\"\n                    \"7B226D6178223A32302C226F6E6C696E65223A307D2C2276657273696F6E223A7B226E616D65223A22312E382D70726531\"\n                    \"222C2270726F746F636F6C223A34347D7D\"\n                )\n            )\n            # we slept 1ms, so this should be always ~1.\n            assert java_client.read_status().latency >= 1\n\n    @pytest.mark.flaky(reruns=5, condition=sys.platform.startswith(\"win32\"))\n    def test_test_ping_is_in_milliseconds(self):\n        \"\"\"``time.perf_counter`` returns fractional seconds, we must convert it to milliseconds.\"\"\"\n\n        def mocked_read_buffer():\n            time.sleep(0.001)\n            return mock.DEFAULT\n\n        with mock.patch.object(Connection, \"read_buffer\") as mocked:\n            mocked.side_effect = mocked_read_buffer\n            mocked.return_value.read_varint.return_value = 1\n            mocked.return_value.read_long.return_value = 123456789\n            java_client = JavaClient(\n                Connection(),  # pyright: ignore[reportArgumentType]\n                address=Address(\"localhost\", 25565),\n                version=44,\n                ping_token=123456789,\n            )\n\n            java_client.connection.receive(\n                bytearray.fromhex(\n                    \"7200707B226465736372697074696F6E223A2241204D696E65637261667420536572766572222C22706C6179657273223A\"\n                    \"7B226D6178223A32302C226F6E6C696E65223A307D2C2276657273696F6E223A7B226E616D65223A22312E382D70726531\"\n                    \"222C2270726F746F636F6C223A34347D7D\"\n                )\n            )\n            # we slept 1ms, so this should be always ~1.\n            assert java_client.test_ping() >= 1\n"
  },
  {
    "path": "tests/protocol/test_java_client_async.py",
    "content": "import asyncio\nimport sys\nimport time\nfrom unittest import mock\n\nimport pytest\n\nfrom mcstatus._net.address import Address\nfrom mcstatus._protocol.connection import Connection\nfrom mcstatus._protocol.java_client import AsyncJavaClient\n\n\ndef async_decorator(f):\n    def wrapper(*args, **kwargs):\n        return asyncio.run(f(*args, **kwargs))\n\n    return wrapper\n\n\nclass FakeAsyncConnection(Connection):\n    async def read_buffer(self):  # pyright: ignore[reportIncompatibleMethodOverride]\n        return super().read_buffer()\n\n\nclass TestAsyncJavaClient:\n    def setup_method(self):\n        self.java_client = AsyncJavaClient(\n            FakeAsyncConnection(),  # pyright: ignore[reportArgumentType]\n            address=Address(\"localhost\", 25565),\n            version=44,\n        )\n\n    def test_handshake(self):\n        self.java_client.handshake()\n\n        assert self.java_client.connection.flush() == bytearray.fromhex(\"0F002C096C6F63616C686F737463DD01\")\n\n    def test_read_status(self):\n        self.java_client.connection.receive(\n            bytearray.fromhex(\n                \"7200707B226465736372697074696F6E223A2241204D696E65637261667420536572766572222C22706C6179657273223A7B2\"\n                \"26D6178223A32302C226F6E6C696E65223A307D2C2276657273696F6E223A7B226E616D65223A22312E382D70726531222C22\"\n                \"70726F746F636F6C223A34347D7D\"\n            )\n        )\n        status = async_decorator(self.java_client.read_status)()\n\n        assert status.raw == {\n            \"description\": \"A Minecraft Server\",\n            \"players\": {\"max\": 20, \"online\": 0},\n            \"version\": {\"name\": \"1.8-pre1\", \"protocol\": 44},\n        }\n        assert self.java_client.connection.flush() == bytearray.fromhex(\"0100\")\n\n    def test_read_status_invalid_json(self):\n        self.java_client.connection.receive(bytearray.fromhex(\"0300017B\"))\n        with pytest.raises(IOError, match=r\"^Received invalid JSON$\"):\n            async_decorator(self.java_client.read_status)()\n\n    def test_read_status_invalid_reply(self):\n        self.java_client.connection.receive(\n            bytearray.fromhex(\n                \"4F004D7B22706C6179657273223A7B226D6178223A32302C226F6E6C696E65223A307D2C2276657273696F6E223A7B226E616\"\n                \"D65223A22312E382D70726531222C2270726F746F636F6C223A34347D7D\"\n            )\n        )\n\n        async_decorator(self.java_client.read_status)()\n\n    def test_read_status_invalid_status(self):\n        self.java_client.connection.receive(bytearray.fromhex(\"0105\"))\n\n        with pytest.raises(IOError, match=r\"^Received invalid status response packet.$\"):\n            async_decorator(self.java_client.read_status)()\n\n    def test_test_ping(self):\n        self.java_client.connection.receive(bytearray.fromhex(\"09010000000000DD7D1C\"))\n        self.java_client.ping_token = 14515484\n\n        assert async_decorator(self.java_client.test_ping)() >= 0\n        assert self.java_client.connection.flush() == bytearray.fromhex(\"09010000000000DD7D1C\")\n\n    def test_test_ping_invalid(self):\n        self.java_client.connection.receive(bytearray.fromhex(\"011F\"))\n        self.java_client.ping_token = 14515484\n\n        with pytest.raises(IOError, match=r\"^Received invalid ping response packet.$\"):\n            async_decorator(self.java_client.test_ping)()\n\n    def test_test_ping_wrong_token(self):\n        self.java_client.connection.receive(bytearray.fromhex(\"09010000000000DD7D1C\"))\n        self.java_client.ping_token = 12345\n\n        with pytest.raises(IOError, match=r\"^Received mangled ping response \\(expected token 12345, got 14515484\\)$\"):\n            async_decorator(self.java_client.test_ping)()\n\n    @pytest.mark.asyncio\n    @pytest.mark.flaky(reruns=5, condition=sys.platform.startswith(\"win32\"))\n    async def test_latency_is_real_number(self):\n        \"\"\"``time.perf_counter`` returns fractional seconds, we must convert it to milliseconds.\"\"\"\n\n        def mocked_read_buffer():\n            time.sleep(0.001)\n            return mock.DEFAULT\n\n        with mock.patch.object(FakeAsyncConnection, \"read_buffer\") as mocked:\n            mocked.side_effect = mocked_read_buffer\n            # overwrite `async` here\n            mocked.return_value.read_varint = lambda: 0\n            mocked.return_value.read_utf = lambda: (\n                \"\"\"\n                {\n                    \"description\": \"A Minecraft Server\",\n                    \"players\": {\"max\": 20, \"online\": 0},\n                    \"version\": {\"name\": \"1.8-pre1\", \"protocol\": 44}\n                }\n                \"\"\"\n            )\n            java_client = AsyncJavaClient(\n                FakeAsyncConnection(),  # pyright: ignore[reportArgumentType]\n                address=Address(\"localhost\", 25565),\n                version=44,\n            )\n\n            java_client.connection.receive(\n                bytearray.fromhex(\n                    \"7200707B226465736372697074696F6E223A2241204D696E65637261667420536572766572222C22706C6179657273223A\"\n                    \"7B226D6178223A32302C226F6E6C696E65223A307D2C2276657273696F6E223A7B226E616D65223A22312E382D70726531\"\n                    \"222C2270726F746F636F6C223A34347D7D\"\n                )\n            )\n            # we slept 1ms, so this should be always ~1.\n            assert (await java_client.read_status()).latency >= 1\n\n    @pytest.mark.asyncio\n    @pytest.mark.flaky(reruns=5, condition=sys.platform.startswith(\"win32\"))\n    async def test_test_ping_is_in_milliseconds(self):\n        \"\"\"``time.perf_counter`` returns fractional seconds, we must convert it to milliseconds.\"\"\"\n\n        def mocked_read_buffer():\n            time.sleep(0.001)\n            return mock.DEFAULT\n\n        with mock.patch.object(FakeAsyncConnection, \"read_buffer\") as mocked:\n            mocked.side_effect = mocked_read_buffer\n            mocked.return_value.read_varint = lambda: 1  # overwrite `async` here\n            mocked.return_value.read_long = lambda: 123456789  # overwrite `async` here\n            java_client = AsyncJavaClient(\n                FakeAsyncConnection(),  # pyright: ignore[reportArgumentType]\n                address=Address(\"localhost\", 25565),\n                version=44,\n                ping_token=123456789,\n            )\n            # we slept 1ms, so this should be always ~1.\n            assert await java_client.test_ping() >= 1\n"
  },
  {
    "path": "tests/protocol/test_legacy_client.py",
    "content": "import pytest\n\nfrom mcstatus._protocol.connection import Connection\nfrom mcstatus._protocol.legacy_client import LegacyClient\nfrom mcstatus.motd import Motd\nfrom mcstatus.responses.legacy import LegacyStatusPlayers, LegacyStatusResponse, LegacyStatusVersion\n\n\ndef test_invalid_kick_reason():\n    with pytest.raises(IOError, match=r\"^Received invalid kick packet reason$\"):\n        LegacyClient.parse_response(\"Invalid Reason\".encode(\"UTF-16BE\"), 123.0)\n\n\n@pytest.mark.parametrize(\n    (\"response\", \"expected\"),\n    [\n        (\n            \"A Minecraft Server§0§20\".encode(\"UTF-16BE\"),\n            LegacyStatusResponse(\n                players=LegacyStatusPlayers(online=0, max=20),\n                version=LegacyStatusVersion(name=\"<1.4\", protocol=-1),\n                motd=Motd.parse(\"A Minecraft Server\"),\n                latency=123.0,\n            ),\n        ),\n        (\n            \"§1\\x0051\\x001.4.7\\x00A Minecraft Server\\x000\\x0020\".encode(\"UTF-16BE\"),\n            LegacyStatusResponse(\n                players=LegacyStatusPlayers(online=0, max=20),\n                version=LegacyStatusVersion(name=\"1.4.7\", protocol=51),\n                motd=Motd.parse(\"A Minecraft Server\"),\n                latency=123.0,\n            ),\n        ),\n    ],\n    ids=[\"b1.8\", \"1.4.7\"],\n)\ndef test_parse_response(response: bytes, expected: LegacyStatusResponse):\n    assert LegacyClient.parse_response(response, 123.0) == expected\n\n\ndef test_invalid_packet_id():\n    socket = Connection()\n    socket.receive(bytearray.fromhex(\"00\"))\n    server = LegacyClient(socket)\n    with pytest.raises(IOError, match=r\"^Received invalid packet ID$\"):\n        server.read_status()\n"
  },
  {
    "path": "tests/protocol/test_query_client.py",
    "content": "from unittest.mock import Mock\n\nfrom mcstatus._protocol.connection import Connection\nfrom mcstatus._protocol.query_client import QueryClient\nfrom mcstatus.motd import Motd\n\n\nclass TestQueryClient:\n    def setup_method(self):\n        self.query_client = QueryClient(Connection())  # pyright: ignore[reportArgumentType]\n\n    def test_handshake(self):\n        self.query_client.connection.receive(bytearray.fromhex(\"090000000035373033353037373800\"))\n        self.query_client.handshake()\n\n        conn_bytes = self.query_client.connection.flush()\n        assert conn_bytes[:3] == bytearray.fromhex(\"FEFD09\")\n        assert self.query_client.challenge == 570350778\n\n    def test_query(self):\n        self.query_client.connection.receive(\n            bytearray.fromhex(\n                \"00000000000000000000000000000000686f73746e616d650041204d696e656372616674205365727665720067616d6574797\"\n                \"06500534d500067616d655f6964004d494e4543524146540076657273696f6e00312e3800706c7567696e7300006d61700077\"\n                \"6f726c64006e756d706c61796572730033006d6178706c617965727300323000686f7374706f727400323535363500686f737\"\n                \"46970003139322e3136382e35362e31000001706c617965725f000044696e6e6572626f6e6500446a696e6e69626f6e650053\"\n                \"746576650000\"\n            )\n        )\n        response = self.query_client.read_query()\n        conn_bytes = self.query_client.connection.flush()\n        assert conn_bytes[:3] == bytearray.fromhex(\"FEFD00\")\n        assert conn_bytes[7:] == bytearray.fromhex(\"0000000000000000\")\n        assert response.raw == {\n            \"hostname\": \"A Minecraft Server\",\n            \"gametype\": \"SMP\",\n            \"game_id\": \"MINECRAFT\",\n            \"version\": \"1.8\",\n            \"plugins\": \"\",\n            \"map\": \"world\",\n            \"numplayers\": \"3\",\n            \"maxplayers\": \"20\",\n            \"hostport\": \"25565\",\n            \"hostip\": \"192.168.56.1\",\n        }\n        assert response.players.list == [\"Dinnerbone\", \"Djinnibone\", \"Steve\"]\n\n    def test_query_handles_unorderd_map_response(self):\n        self.query_client.connection.receive(\n            bytearray(\n                b\"\\x00\\x00\\x00\\x00\\x00GeyserMC\\x00\\x80\\x00hostname\\x00Geyser\\x00hostip\\x001.1.1.1\\x00plugins\\x00\\x00numplayers\"\n                b\"\\x001\\x00gametype\\x00SMP\\x00maxplayers\\x00100\\x00hostport\\x0019132\\x00version\\x00Geyser\"\n                b\" (git-master-0fd903e) 1.18.10\\x00map\\x00Geyser\\x00game_id\\x00MINECRAFT\\x00\\x00\\x01player_\\x00\\x00\\x00\"\n            )\n        )\n        response = self.query_client.read_query()\n        self.query_client.connection.flush()\n\n        assert response.raw[\"game_id\"] == \"MINECRAFT\"\n        assert response.motd == Motd.parse(\"Geyser\")\n        assert response.software.version == \"Geyser (git-master-0fd903e) 1.18.10\"\n\n    def test_query_handles_unicode_motd_with_nulls(self):\n        self.query_client.connection.receive(\n            bytearray(\n                b\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00hostname\\x00\\x00*K\\xd5\\x00gametype\\x00SMP\"\n                b\"\\x00game_id\\x00MINECRAFT\\x00version\\x001.16.5\\x00plugins\\x00Paper on 1.16.5-R0.1-SNAPSHOT\\x00map\\x00world\"\n                b\"\\x00numplayers\\x000\\x00maxplayers\\x0020\\x00hostport\\x0025565\\x00hostip\\x00127.0.1.1\\x00\\x00\\x01player_\\x00\"\n                b\"\\x00\\x00\"\n            )\n        )\n        response = self.query_client.read_query()\n        self.query_client.connection.flush()\n\n        assert response.raw[\"game_id\"] == \"MINECRAFT\"\n        assert response.motd == Motd.parse(\"\\x00*KÕ\")\n\n    def test_query_handles_unicode_motd_with_2a00_at_the_start(self):\n        self.query_client.connection.receive(\n            bytearray.fromhex(\n                \"00000000000000000000000000000000686f73746e616d6500006f746865720067616d657479706500534d500067616d655f6964004d\"\n                \"494e4543524146540076657273696f6e00312e31382e3100706c7567696e7300006d617000776f726c64006e756d706c617965727300\"\n                \"30006d6178706c617965727300323000686f7374706f727400323535363500686f73746970003137322e31372e302e32000001706c61\"\n                \"7965725f000000\"\n            )\n        )\n        response = self.query_client.read_query()\n        self.query_client.connection.flush()\n\n        assert response.raw[\"game_id\"] == \"MINECRAFT\"\n        assert response.motd == Motd.parse(\"\\x00other\")  # \"\\u2a00other\" is actually what is expected,\n        # but the query protocol for vanilla has a bug when it comes to unicode handling.\n        # The status protocol correctly shows \"⨀other\".\n\n    def test_session_id(self):\n        def session_id():\n            return 0x01010101\n\n        self.query_client.connection.receive(bytearray.fromhex(\"090000000035373033353037373800\"))\n        self.query_client._generate_session_id = Mock()\n        self.query_client._generate_session_id = session_id\n        self.query_client.handshake()\n\n        conn_bytes = self.query_client.connection.flush()\n        assert conn_bytes[:3] == bytearray.fromhex(\"FEFD09\")\n        assert conn_bytes[3:] == session_id().to_bytes(4, byteorder=\"big\")\n        assert self.query_client.challenge == 570350778\n"
  },
  {
    "path": "tests/protocol/test_query_client_async.py",
    "content": "from mcstatus._protocol.connection import Connection\nfrom mcstatus._protocol.query_client import AsyncQueryClient\nfrom tests.protocol.test_java_client_async import async_decorator\n\n\nclass FakeUDPAsyncConnection(Connection):\n    async def read(self, length):  # pyright: ignore[reportIncompatibleMethodOverride]\n        return super().read(length)\n\n    async def write(self, data):  # pyright: ignore[reportIncompatibleMethodOverride]\n        return super().write(data)\n\n\nclass TestAsyncQueryClient:\n    def setup_method(self):\n        self.query_client = AsyncQueryClient(FakeUDPAsyncConnection())  # pyright: ignore[reportArgumentType]\n\n    def test_handshake(self):\n        self.query_client.connection.receive(bytearray.fromhex(\"090000000035373033353037373800\"))\n        async_decorator(self.query_client.handshake)()\n        conn_bytes = self.query_client.connection.flush()\n        assert conn_bytes[:3] == bytearray.fromhex(\"FEFD09\")\n        assert self.query_client.challenge == 570350778\n\n    def test_query(self):\n        self.query_client.connection.receive(\n            bytearray.fromhex(\n                \"00000000000000000000000000000000686f73746e616d650041204d696e656372616674205365727665720067616d6574797\"\n                \"06500534d500067616d655f6964004d494e4543524146540076657273696f6e00312e3800706c7567696e7300006d61700077\"\n                \"6f726c64006e756d706c61796572730033006d6178706c617965727300323000686f7374706f727400323535363500686f737\"\n                \"46970003139322e3136382e35362e31000001706c617965725f000044696e6e6572626f6e6500446a696e6e69626f6e650053\"\n                \"746576650000\"\n            )\n        )\n        response = async_decorator(self.query_client.read_query)()\n        conn_bytes = self.query_client.connection.flush()\n        assert conn_bytes[:3] == bytearray.fromhex(\"FEFD00\")\n        assert conn_bytes[7:] == bytearray.fromhex(\"0000000000000000\")\n        assert response.raw == {\n            \"hostname\": \"A Minecraft Server\",\n            \"gametype\": \"SMP\",\n            \"game_id\": \"MINECRAFT\",\n            \"version\": \"1.8\",\n            \"plugins\": \"\",\n            \"map\": \"world\",\n            \"numplayers\": \"3\",\n            \"maxplayers\": \"20\",\n            \"hostport\": \"25565\",\n            \"hostip\": \"192.168.56.1\",\n        }\n        assert response.players.list == [\"Dinnerbone\", \"Djinnibone\", \"Steve\"]\n"
  },
  {
    "path": "tests/protocol/test_timeout.py",
    "content": "import asyncio\nimport typing\nfrom asyncio.exceptions import TimeoutError as AsyncioTimeoutError\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom mcstatus._net.address import Address\nfrom mcstatus._protocol.connection import TCPAsyncSocketConnection\n\n\nclass FakeAsyncStream(asyncio.StreamReader):\n    async def read(self, *args, **kwargs) -> typing.NoReturn:\n        await asyncio.sleep(2)\n        raise NotImplementedError(\"tests are designed to timeout before reaching this line\")\n\n\nasync def fake_asyncio_asyncio_open_connection(hostname: str, port: int):\n    return FakeAsyncStream(), None\n\n\nclass TestAsyncSocketConnection:\n    @pytest.mark.asyncio\n    async def test_tcp_socket_read(self):\n        with patch(\"asyncio.open_connection\", fake_asyncio_asyncio_open_connection):\n            async with TCPAsyncSocketConnection(Address(\"dummy_address\", 1234), timeout=0.01) as tcp_async_socket:\n                with pytest.raises(AsyncioTimeoutError):\n                    await tcp_async_socket.read(10)\n"
  },
  {
    "path": "tests/responses/__init__.py",
    "content": "from __future__ import annotations\n\nimport abc\nfrom typing import Any, ClassVar, TYPE_CHECKING, TypeVar, cast\n\nimport pytest\n\nif TYPE_CHECKING:\n    from mcstatus.responses import BaseStatusResponse\n\n__all__ = [\"BaseResponseTest\"]\n_T = TypeVar(\"_T\", bound=\"type[BaseResponseTest]\")\n\n\nclass BaseResponseTest(abc.ABC):\n    EXPECTED_VALUES: ClassVar[list[tuple[str, Any]] | None] = None\n    EXPECTED_TYPES: ClassVar[list[tuple[str, type]] | None] = None\n    ATTRIBUTES_IN: ClassVar[list[str] | None] = None\n    # if we don't specify item in raw answer, target field will be None\n    # a first element is a list with fields to remove, and attribute that\n    # must be None. a dict is a raw answer to pass into `build` method\n    OPTIONAL_FIELDS: ClassVar[tuple[list[tuple[str, str]], dict[str, Any]] | None] = None\n\n    def _validate(self) -> None:\n        \"\"\"Perform checks to validate the class.\"\"\"\n        if self.EXPECTED_TYPES is not None and self.EXPECTED_VALUES is not None:\n            expected_values_keys = list(dict(self.EXPECTED_VALUES).keys())\n\n            for key in dict(self.EXPECTED_TYPES):\n                if key in expected_values_keys:\n                    raise ValueError(\"You can't test the type of attribute, if already testing its value.\")\n\n        if self.ATTRIBUTES_IN is not None and (self.EXPECTED_VALUES is not None or self.EXPECTED_TYPES is not None):\n            if self.EXPECTED_VALUES and self.EXPECTED_TYPES:\n                to_dict = self.EXPECTED_VALUES.copy()\n                to_dict.extend(self.EXPECTED_TYPES)\n                already_checked_attributes = dict(to_dict).keys()\n            else:\n                to_dict = cast(\"list[tuple[str, type]]\", self.EXPECTED_VALUES or self.EXPECTED_TYPES)\n                already_checked_attributes = dict(to_dict).keys()\n\n            for attribute_name in self.ATTRIBUTES_IN:\n                if attribute_name in already_checked_attributes:\n                    raise ValueError(\"You can't test the type availability, if already testing its value/type.\")\n\n    @abc.abstractmethod\n    @pytest.fixture(scope=\"class\")\n    def build(self) -> BaseStatusResponse: ...\n\n    # implementations for tests\n\n    def test_values_of_attributes(self, build: BaseStatusResponse, field: str, value: Any) -> None:\n        assert getattr(build, field) == value\n\n    def test_types_of_attributes(self, build: BaseStatusResponse, field: str, type_: type) -> None:\n        assert isinstance(getattr(build, field), type_)\n\n    def test_attribute_in(self, build: BaseStatusResponse, field: str) -> None:\n        assert hasattr(build, field)\n\n    def test_optional_field_turns_into_none(self, build: BaseStatusResponse, to_remove: str, attribute_name: str) -> None:\n        raw = cast(\"tuple\", self.OPTIONAL_FIELDS)[1]\n        del raw[to_remove]\n        assert getattr(type(build).build(raw), attribute_name) is None\n\n    def _dependency_table(self) -> dict[str, bool]:\n        # a key in the dict must be a name of a test implementation.\n        # and a value of the dict is a bool. if it's false - we\n        # \"delete\" a test from the class.\n        return {\n            \"test_values_of_attributes\": self.EXPECTED_VALUES is not None,\n            \"test_types_of_attributes\": self.EXPECTED_TYPES is not None,\n            \"test_attribute_in\": self.ATTRIBUTES_IN is not None,\n            \"test_optional_field_turns_into_none\": self.OPTIONAL_FIELDS is not None,\n        }\n\n    def _marks_table(self) -> dict[str, tuple[str, tuple[Any, ...]]]:\n        # hooks in conftest.py parses this table\n\n        # a key in the dict must be a name of a test implementation.\n        # and a value of the dict is a tuple, where first element is\n        # a name of mark to apply to the test, and second element is\n        # positional arguments, which passed to the mark\n        return {\n            \"test_values_of_attributes\": (\"parametrize\", (\"field,value\", self.EXPECTED_VALUES)),\n            \"test_types_of_attributes\": (\"parametrize\", (\"field,type_\", self.EXPECTED_TYPES)),\n            \"test_attribute_in\": (\"parametrize\", (\"field\", self.ATTRIBUTES_IN)),\n            \"test_optional_field_turns_into_none\": (\n                \"parametrize\",\n                (\"to_remove,attribute_name\", self.OPTIONAL_FIELDS[0] if self.OPTIONAL_FIELDS is not None else ()),\n            ),\n        }\n\n    @staticmethod\n    def construct(class_: _T) -> _T:\n        instance: BaseResponseTest = class_()  # pyright: ignore[reportAssignmentType]\n        instance._validate()\n        for implementation_name, meet_dependencies in instance._dependency_table().items():\n            if not meet_dependencies:\n                # delattr works only with initialized classes,\n                # hopefully overwriting with None doesn't have this limitation\n                setattr(class_, implementation_name, None)\n\n        return class_\n"
  },
  {
    "path": "tests/responses/conftest.py",
    "content": "from __future__ import annotations\n\nimport typing\n\nimport pytest\n\nfrom tests.responses import BaseResponseTest\n\nif typing.TYPE_CHECKING:\n    from _pytest.python import Function, Metafunc\n\n\ndef pytest_generate_tests(metafunc: Metafunc) -> None:\n    if metafunc.cls is not None and issubclass(typing.cast(\"type\", metafunc.cls), BaseResponseTest):\n        instance = typing.cast(\"type\", metafunc.cls)()\n        if metafunc.definition.name not in instance._marks_table():\n            return\n\n        marker_name, args = instance._marks_table()[metafunc.definition.name]\n        if marker_name != \"parametrize\":\n            return  # other markers will be handled in `pytest_collection_modifyitems`\n        metafunc.parametrize(*args)\n\n\ndef pytest_collection_modifyitems(items: list[Function]) -> None:\n    for item in items:\n        if isinstance(item.instance, BaseResponseTest):\n            if item.obj.__name__ not in item.instance._marks_table():\n                continue\n\n            marker_name, args = item.instance._marks_table()[item.obj.__name__]\n            if marker_name == \"parametrize\":\n                continue\n            item.add_marker(getattr(pytest.mark, marker_name)(*args))\n"
  },
  {
    "path": "tests/responses/test_base.py",
    "content": "import pytest\n\nfrom mcstatus.responses import BaseStatusResponse\n\n\nclass TestMCStatusResponse:\n    def test_raises_not_implemented_error_on_build(self):\n        with pytest.raises(NotImplementedError):\n            BaseStatusResponse.build({\"foo\": \"bar\"})  # pyright: ignore[reportAbstractUsage]\n"
  },
  {
    "path": "tests/responses/test_bedrock.py",
    "content": "import typing as t\n\nimport pytest\n\nfrom mcstatus.motd import Motd\nfrom mcstatus.responses import BedrockStatusPlayers, BedrockStatusResponse, BedrockStatusVersion\nfrom tests.helpers import patch_project_version\nfrom tests.responses import BaseResponseTest\n\n\n@pytest.fixture(scope=\"module\")\ndef build():\n    return BedrockStatusResponse.build(\n        [\n            \"MCPE\",\n            \"§r§4G§r§6a§r§ey§r§2B§r§1o§r§9w§r§ds§r§4e§r§6r\",\n            \"422\",\n            \"1.18.100500\",\n            \"1\",\n            \"69\",\n            \"3767071975391053022\",\n            \"map name here\",\n            \"Default\",\n            \"1\",\n            \"19132\",\n            \"-1\",\n            \"3\",\n        ],\n        123.0,\n    )\n\n\n@BaseResponseTest.construct\nclass TestBedrockStatusResponse(BaseResponseTest):\n    EXPECTED_VALUES: t.ClassVar = [\n        (\"motd\", Motd.parse(\"§r§4G§r§6a§r§ey§r§2B§r§1o§r§9w§r§ds§r§4e§r§6r\", bedrock=True)),\n        (\"latency\", 123.0),\n        (\"map_name\", \"map name here\"),\n        (\"gamemode\", \"Default\"),\n    ]\n    EXPECTED_TYPES: t.ClassVar = [\n        (\"players\", BedrockStatusPlayers),\n        (\"version\", BedrockStatusVersion),\n    ]\n\n    @pytest.fixture(scope=\"class\")\n    def build(self, build):\n        return build\n\n    @pytest.mark.parametrize((\"field\", \"pop_index\"), [(\"map_name\", 7), (\"gamemode\", 7), (\"gamemode\", 8)])\n    def test_optional_parameters_is_none(self, field, pop_index):\n        parameters = [\n            \"MCPE\",\n            \"§r§4G§r§6a§r§ey§r§2B§r§1o§r§9w§r§ds§r§4e§r§6r\",\n            \"422\",\n            \"1.18.100500\",\n            \"1\",\n            \"69\",\n            \"3767071975391053022\",\n            \"map name here\",\n            \"Default\",\n        ]\n        parameters.pop(pop_index)\n        # remove all variables after `pop_index`\n        if len(parameters) - 1 == pop_index:\n            parameters.pop(pop_index)\n\n        build = BedrockStatusResponse.build(parameters, 123.0)\n        assert getattr(build, field) is None\n\n    def test_as_dict(self, build: BedrockStatusResponse):\n        assert build.as_dict() == {\n            \"gamemode\": \"Default\",\n            \"latency\": 123.0,\n            \"map_name\": \"map name here\",\n            \"motd\": \"§4G§6a§ey§2B§1o§9w§ds§4e§6r\",\n            \"players\": {\"max\": 69, \"online\": 1},\n            \"version\": {\"brand\": \"MCPE\", \"name\": \"1.18.100500\", \"protocol\": 422},\n        }\n\n    def test_description_alias(self, build: BedrockStatusResponse):\n        assert build.description == \"§r§4G§r§6a§r§ey§r§2B§r§1o§r§9w§r§ds§r§4e§r§6r\"\n\n\n@BaseResponseTest.construct\nclass TestBedrockStatusPlayers(BaseResponseTest):\n    EXPECTED_VALUES: t.ClassVar = [(\"online\", 1), (\"max\", 69)]\n\n    @pytest.fixture(scope=\"class\")\n    def build(self, build):\n        return build.players\n\n\n@BaseResponseTest.construct\nclass TestBedrockStatusVersion(BaseResponseTest):\n    EXPECTED_VALUES: t.ClassVar = [(\"name\", \"1.18.100500\"), (\"protocol\", 422), (\"brand\", \"MCPE\")]\n\n    @pytest.fixture(scope=\"class\")\n    def build(self, build):\n        return build.version\n\n    def test_deprecated_version_alias(self, build: BedrockStatusVersion):\n        with (\n            patch_project_version(\"0.0.0\"),\n            pytest.deprecated_call(\n                match=(\n                    r\"^BedrockStatusVersion\\.version is deprecated and scheduled for removal in 13\\.0\\.0, \"\n                    r\"use name instead\\.$\"\n                ),\n            ),\n        ):\n            assert build.version == build.name\n"
  },
  {
    "path": "tests/responses/test_forge_data.py",
    "content": "import typing as t\n\nimport pytest\n\nfrom mcstatus.responses import JavaStatusResponse\nfrom mcstatus.responses._raw import RawForgeData, RawJavaResponse\nfrom mcstatus.responses.forge import ForgeData, ForgeDataChannel, ForgeDataMod\nfrom tests.responses import BaseResponseTest\n\nJAVA_RAW_RESPONSE: RawJavaResponse = {\n    \"players\": {\"max\": 20, \"online\": 0},\n    \"version\": {\"name\": \"1.8-pre1\", \"protocol\": 44},\n    \"description\": \"A Minecraft Server\",\n    \"enforcesSecureChat\": True,\n    \"favicon\": \"data:image/png;base64,foo\",\n}\n\n\n@BaseResponseTest.construct\nclass TestForgeDataV1(BaseResponseTest):\n    RAW: t.ClassVar = {\n        \"type\": \"FML\",\n        \"modList\": [\n            {\"modid\": \"minecraft\", \"version\": \"1.12.2\"},\n            {\"modid\": \"mcp\", \"version\": \"9.42\"},\n            {\"modid\": \"FML\", \"version\": \"8.0.99.99\"},\n            {\"modid\": \"forge\", \"version\": \"14.23.5.2859\"},\n        ],\n        \"channels\": [\n            {\"res\": \"fml:handshake\", \"version\": \"1.2.3.4\", \"required\": True},\n        ],\n    }\n\n    EXPECTED_VALUES: t.ClassVar = [\n        (\"fml_network_version\", 1),\n        (\"channels\", [ForgeDataChannel(name=\"fml:handshake\", version=\"1.2.3.4\", required=True)]),\n        (\n            \"mods\",\n            [\n                ForgeDataMod(name=\"minecraft\", marker=\"1.12.2\"),\n                ForgeDataMod(name=\"mcp\", marker=\"9.42\"),\n                ForgeDataMod(name=\"FML\", marker=\"8.0.99.99\"),\n                ForgeDataMod(name=\"forge\", marker=\"14.23.5.2859\"),\n            ],\n        ),\n        (\"truncated\", False),\n    ]\n\n    @pytest.fixture(scope=\"class\")\n    def build(self) -> ForgeData:\n        return ForgeData.build(self.RAW)  # pyright: ignore[reportArgumentType] # dict[str, Unknown] cannot be assigned to TypedDict\n\n\n@BaseResponseTest.construct\nclass TestForgeDataV2(BaseResponseTest):\n    RAW: t.ClassVar = {\n        \"fmlNetworkVersion\": 2,\n        \"channels\": [\n            {\"res\": \"fml:handshake\", \"version\": \"1.2.3.4\", \"required\": True},\n        ],\n        \"mods\": [\n            {\"modId\": \"forge\", \"modmarker\": \"ANY\"},\n            {\"modId\": \"fusion\", \"modmarker\": \"\"},\n        ],\n    }\n\n    EXPECTED_VALUES: t.ClassVar = [\n        (\"fml_network_version\", 2),\n        (\"channels\", [ForgeDataChannel(name=\"fml:handshake\", version=\"1.2.3.4\", required=True)]),\n        (\"mods\", [ForgeDataMod(name=\"forge\", marker=\"ANY\"), ForgeDataMod(name=\"fusion\", marker=\"\")]),\n        (\"truncated\", False),\n    ]\n\n    @pytest.fixture(scope=\"class\")\n    def build(self) -> ForgeData:\n        return ForgeData.build(self.RAW)  # pyright: ignore[reportArgumentType] # dict[str, Unknown] cannot be assigned to TypedDict\n\n\n@BaseResponseTest.construct\nclass TestForgeDataV3(BaseResponseTest):\n    RAW: t.ClassVar = {\n        \"channels\": [],\n        \"mods\": [],\n        \"truncated\": False,\n        \"fmlNetworkVersion\": 3,\n        \"d\": bytes.fromhex(\n            \"5e0000e0a084e390a4e78d8be39996e2b98ce1a698ccbae2b8b1e681a4e492b8e2a191e29ba7e6b2aee5a\"\n            \"999e3a8b9e789a5e0b088e384b5e0a69ae28280e6b2aee5a999e3a8b9e789a5e0b088e384b5e0a69ae581\"\n            \"80e6b380e5b29be38ab3e48483e38a9ce580b1e2ad8be79ca6e6b9abe1b29be392bae69daee68886e482b\"\n            \"8e2a081dcb0e2b68ee5b49ae1a281e384ae02\"\n        ).decode(\"utf8\"),\n    }\n\n    EXPECTED_VALUES: t.ClassVar = [\n        (\"fml_network_version\", 3),\n        (\n            \"channels\",\n            [\n                ForgeDataChannel(name=\"minecraft:unregister\", version=\"FML3\", required=True),\n                ForgeDataChannel(name=\"minecraft:register\", version=\"FML3\", required=True),\n                ForgeDataChannel(name=\"forge:tier_sorting\", version=\"1.0\", required=False),\n                ForgeDataChannel(name=\"forge:split\", version=\"1.1\", required=True),\n            ],\n        ),\n        (\n            \"mods\",\n            [\n                ForgeDataMod(name=\"minecraft\", marker=\"1.20.1\"),\n                ForgeDataMod(name=\"forge\", marker=\"ANY\"),\n            ],\n        ),\n        (\"truncated\", False),\n    ]\n\n    @pytest.fixture(scope=\"class\")\n    def build(self) -> ForgeData:\n        return ForgeData.build(self.RAW)  # pyright: ignore[reportArgumentType] # dict[str, Unknown] cannot be assigned to TypedDict\n\n\nclass TestForgeDataMod:\n    def test_build_with_empty_input(self):\n        with pytest.raises(KeyError, match=r\"^'Mod version in Forge mod data must be provided\\. Mod info: {}'$\"):\n            ForgeDataMod.build({})\n\n    def test_build_without_mod_id(self):\n        with pytest.raises(\n            KeyError, match=r\"^\\\"Mod ID in Forge mod data must be provided\\. Mod info: {'modmarker': 'foo'}\\.\\\"$\"\n        ):\n            ForgeDataMod.build({\"modmarker\": \"foo\"})\n\n\n@BaseResponseTest.construct\nclass TestForgeData(BaseResponseTest):\n    EXPECTED_VALUES: t.ClassVar = [\n        (\"fml_network_version\", 3),\n        (\n            \"channels\",\n            [\n                ForgeDataChannel(\n                    name=\"cyclopscore:channel_main\",\n                    version=\"1.0.0\",\n                    required=True,\n                ),\n                ForgeDataChannel(\n                    name=\"supermartijn642configlib:sync_configs\",\n                    version=\"1\",\n                    required=False,\n                ),\n                ForgeDataChannel(\n                    name=\"alexsmobs:main_channel\",\n                    version=\"1\",\n                    required=False,\n                ),\n                ForgeDataChannel(\n                    name=\"sophisticatedcore:channel\",\n                    version=\"1\",\n                    required=False,\n                ),\n                ForgeDataChannel(\n                    name=\"rftoolsbase:rftoolsbase\",\n                    version=\"1.0\",\n                    required=True,\n                ),\n                ForgeDataChannel(\n                    name=\"irongenerators:irongenerators\",\n                    version=\"1\",\n                    required=False,\n                ),\n                ForgeDataChannel(\n                    name=\"xaeroworldmap:main\",\n                    version=\"1.0\",\n                    required=True,\n                ),\n                ForgeDataChannel(\n                    name=\"cookingforblockheads:network\",\n                    version=\"1.0\",\n                    required=False,\n                ),\n                ForgeDataChannel(name=\"xnet:xnet\", version=\"1.0\", required=True),\n                ForgeDataChannel(\n                    name=\"placebo:placebo\",\n                    version=\"1.0.0\",\n                    required=True,\n                ),\n                ForgeDataChannel(\n                    name=\"citadel:main_channel\",\n                    version=\"1\",\n                    required=False,\n                ),\n                ForgeDataChannel(\n                    name=\"sophisticatedbackpacks:channel\",\n                    version=\"1\",\n                    required=False,\n                ),\n                ForgeDataChannel(\n                    name=\"buildinggadgets:main\",\n                    version=\"4\",\n                    required=False,\n                ),\n                ForgeDataChannel(\n                    name=\"mekanismgenerators:mekanismgenerators\",\n                    version=\"10.2.5\",\n                    required=False,\n                ),\n                ForgeDataChannel(\n                    name=\"waila:networking\",\n                    version=\"1.0.0\",\n                    required=True,\n                ),\n                ForgeDataChannel(\n                    name=\"shetiphiancore:main_channel\",\n                    version=\"1.0.0\",\n                    required=False,\n                ),\n                ForgeDataChannel(\n                    name=\"dummmmmmy:dummychannel\",\n                    version=\"1\",\n                    required=False,\n                ),\n                ForgeDataChannel(\n                    name=\"supplementaries:network\",\n                    version=\"1\",\n                    required=False,\n                ),\n                ForgeDataChannel(\n                    name=\"refinedstorage:main_channel\",\n                    version=\"1\",\n                    required=False,\n                ),\n                ForgeDataChannel(name=\"corpse:default\", version=\"1.0.0\", required=True),\n                ForgeDataChannel(\n                    name=\"ping:ping_channel\",\n                    version=\"PING1\",\n                    required=True,\n                ),\n                ForgeDataChannel(\n                    name=\"ironfurnaces:ironfurnaces_network\",\n                    version=\"1.0\",\n                    required=True,\n                ),\n                ForgeDataChannel(name=\"botania:main\", version=\"0\", required=False),\n                ForgeDataChannel(name=\"curios:main\", version=\"1\", required=False),  # codespell:ignore curios\n                ForgeDataChannel(name=\"patchouli:main\", version=\"1\", required=False),\n                ForgeDataChannel(name=\"camera:default\", version=\"1.0.0\", required=True),\n                ForgeDataChannel(\n                    name=\"libnonymous:channel\",\n                    version=\"1.0\",\n                    required=True,\n                ),\n                ForgeDataChannel(\n                    name=\"elevatorid:main_channel\",\n                    version=\"1\",\n                    required=False,\n                ),\n                ForgeDataChannel(name=\"worldedit:cui\", version=\"1\", required=True),\n                ForgeDataChannel(name=\"worldedit:internal\", version=\"1\", required=True),\n                ForgeDataChannel(name=\"cfm:network\", version=\"1\", required=False),\n                ForgeDataChannel(\n                    name=\"architectury:network\",\n                    version=\"1\",\n                    required=True,\n                ),\n                ForgeDataChannel(name=\"trashcans:main\", version=\"1\", required=False),\n                ForgeDataChannel(name=\"jei:channel\", version=\"1.0.0\", required=True),\n                ForgeDataChannel(name=\"ae2:main\", version=\"1\", required=True),\n                ForgeDataChannel(\n                    name=\"mekanism:mekanism\",\n                    version=\"10.2.5\",\n                    required=False,\n                ),\n                ForgeDataChannel(name=\"bdlib:multiblock\", version=\"2\", required=False),\n                ForgeDataChannel(name=\"bdlib:misc\", version=\"1\", required=False),\n                ForgeDataChannel(name=\"create:main\", version=\"1\", required=False),\n                ForgeDataChannel(\n                    name=\"waystones:network\",\n                    version=\"1.0\",\n                    required=False,\n                ),\n                ForgeDataChannel(name=\"comforts:main\", version=\"1\", required=False),\n                ForgeDataChannel(\n                    name=\"naturescompass:naturescompass\",\n                    version=\"1.0\",\n                    required=True,\n                ),\n                ForgeDataChannel(\n                    name=\"storagenetwork:main_channel\",\n                    version=\"1\",\n                    required=False,\n                ),\n                ForgeDataChannel(name=\"cofh_core:general\", version=\"1\", required=True),\n                ForgeDataChannel(\n                    name=\"mcjtylib:mcjtylib\",\n                    version=\"1.0\",\n                    required=True,\n                ),\n                ForgeDataChannel(\n                    name=\"mininggadgets:main_network_channel\",\n                    version=\"2\",\n                    required=False,\n                ),\n                ForgeDataChannel(\n                    name=\"crafttweaker:main\",\n                    version=\"1.0.0\",\n                    required=False,\n                ),\n                ForgeDataChannel(name=\"akashictome:main\", version=\"1\", required=False),\n                ForgeDataChannel(\n                    name=\"forge:tier_sorting\",\n                    version=\"1.0\",\n                    required=False,\n                ),\n                ForgeDataChannel(name=\"forge:split\", version=\"1.1\", required=True),\n                ForgeDataChannel(\n                    name=\"colossalchests:channel_main\",\n                    version=\"1.0.0\",\n                    required=True,\n                ),\n                ForgeDataChannel(name=\"selene:network\", version=\"1\", required=False),\n                ForgeDataChannel(\n                    name=\"craftingtweaks:network\",\n                    version=\"1.0\",\n                    required=False,\n                ),\n                ForgeDataChannel(\n                    name=\"minecraft:unregister\",\n                    version=\"FML3\",\n                    required=True,\n                ),\n                ForgeDataChannel(\n                    name=\"minecraft:register\",\n                    version=\"FML3\",\n                    required=True,\n                ),\n                ForgeDataChannel(name=\"titanium:network\", version=\"1.0\", required=True),\n                ForgeDataChannel(\n                    name=\"easy_villagers:default\",\n                    version=\"1.0.0\",\n                    required=True,\n                ),\n                ForgeDataChannel(name=\"pipez:default\", version=\"1.0.0\", required=True),\n                ForgeDataChannel(name=\"mantle:network\", version=\"1\", required=False),\n                ForgeDataChannel(name=\"quark:main\", version=\"1\", required=False),\n                ForgeDataChannel(\n                    name=\"xaerominimap:main\",\n                    version=\"1.0\",\n                    required=True,\n                ),\n                ForgeDataChannel(\n                    name=\"fastbench:channel\",\n                    version=\"4.6.0\",\n                    required=True,\n                ),\n                ForgeDataChannel(name=\"polymorph:main\", version=\"1\", required=False),\n                ForgeDataChannel(\n                    name=\"storagedrawers:main_channel\",\n                    version=\"1\",\n                    required=False,\n                ),\n                ForgeDataChannel(\n                    name=\"enercell:network\",\n                    version=\"0.0.0\",\n                    required=False,\n                ),\n                ForgeDataChannel(name=\"appleskin:sync\", version=\"1\", required=True),\n                ForgeDataChannel(\n                    name=\"modularrouters:main_channel\",\n                    version=\"2\",\n                    required=False,\n                ),\n                ForgeDataChannel(\n                    name=\"the_vault:network\",\n                    version=\"0.26.0\",\n                    required=False,\n                ),\n                ForgeDataChannel(\n                    name=\"modernui:fluxnetworks\",\n                    version=\"707\",\n                    required=False,\n                ),\n            ],\n        ),\n        (\n            \"mods\",\n            [\n                ForgeDataMod(name=\"rsrequestify\", marker=\"2.2.0\"),\n                ForgeDataMod(name=\"cyclopscore\", marker=\"1.15.1\"),\n                ForgeDataMod(name=\"auudio\", marker=\"1.0.3\"),\n                ForgeDataMod(name=\"auxiliaryblocks\", marker=\"1.18.2-0.0.14\"),\n                ForgeDataMod(name=\"supermartijn642configlib\", marker=\"1.1.6\"),\n                ForgeDataMod(name=\"alexsmobs\", marker=\"1.18.6\"),\n                ForgeDataMod(name=\"architects_palette\", marker=\"1.1.2\"),\n                ForgeDataMod(name=\"cagerium\", marker=\"1.18.2-1.1.0\"),\n                ForgeDataMod(name=\"mcwwindows\", marker=\"2.0.3\"),\n                ForgeDataMod(\n                    name=\"sophisticatedcore\",\n                    marker=\"1.18.2-0.5.32.179\",\n                ),\n                ForgeDataMod(name=\"thermal\", marker=\"1.6.3.28\"),\n                ForgeDataMod(name=\"rftoolsbase\", marker=\"1.18-3.0.9\"),\n                ForgeDataMod(name=\"initialinventory\", marker=\"6.0.8\"),\n                ForgeDataMod(name=\"irongenerators\", marker=\"2.0.1\"),\n                ForgeDataMod(name=\"xaeroworldmap\", marker=\"1.25.1\"),\n                ForgeDataMod(name=\"cookingforblockheads\", marker=\"12.0.2\"),\n                ForgeDataMod(\n                    name=\"controlling\",\n                    marker=\"<not required for client>\",\n                ),\n                ForgeDataMod(name=\"xnet\", marker=\"1.18-4.0.5\"),\n                ForgeDataMod(name=\"placebo\", marker=\"6.4.1\"),\n                ForgeDataMod(name=\"citadel\", marker=\"1.11.3\"),\n                ForgeDataMod(name=\"powah\", marker=\"3.0.1-beta\"),\n                ForgeDataMod(name=\"bookshelf\", marker=\"13.2.50\"),\n                ForgeDataMod(name=\"lootbeams\", marker=\"1.18.1\"),\n                ForgeDataMod(\n                    name=\"sophisticatedbackpacks\",\n                    marker=\"1.18.2-3.18.35.752\",\n                ),\n                ForgeDataMod(name=\"twigs\", marker=\"1.1.4-patch1+1.18.2\"),\n                ForgeDataMod(\n                    name=\"buildinggadgets\",\n                    marker=\"3.13.0-build.5+mc1.18.2}\",\n                ),\n                ForgeDataMod(name=\"darkutils\", marker=\"10.0.5\"),\n                ForgeDataMod(name=\"mcwdoors\", marker=\"1.0.6\"),\n                ForgeDataMod(name=\"waddles\", marker=\"1.18.2-0.8.19\"),\n                ForgeDataMod(name=\"mekanismgenerators\", marker=\"10.2.5\"),\n                ForgeDataMod(name=\"balm\", marker=\"3.2.0+0\"),\n                ForgeDataMod(name=\"waila\", marker=\"<not required for client>\"),\n                ForgeDataMod(name=\"jeresources\", marker=\"0.14.1.171\"),\n                ForgeDataMod(\n                    name=\"cloth_config\",\n                    marker=\"<not required for client>\",\n                ),\n                ForgeDataMod(name=\"shetiphiancore\", marker=\"3.10.10\"),\n                ForgeDataMod(name=\"dummmmmmy\", marker=\"1.18-1.5.2\"),\n                ForgeDataMod(name=\"supplementaries\", marker=\"1.18.2-1.5.13\"),\n                ForgeDataMod(name=\"refinedstorage\", marker=\"1.10.2\"),\n                ForgeDataMod(name=\"konkrete\", marker=\"1.3.3\"),\n                ForgeDataMod(name=\"easy_piglins\", marker=\"1.18.2-1.0.0\"),\n                ForgeDataMod(name=\"corpse\", marker=\"1.18.2-1.0.2\"),\n                ForgeDataMod(name=\"packmenu\", marker=\"<not required for client>\"),\n                ForgeDataMod(name=\"mcwbridges\", marker=\"2.0.3\"),\n                ForgeDataMod(name=\"torchmaster\", marker=\"18.1.0\"),\n                ForgeDataMod(name=\"compressium\", marker=\"1.4.2-build.9+mc1.18.2\"),\n                ForgeDataMod(name=\"ping\", marker=\"1.18-1.8.0\"),\n                ForgeDataMod(name=\"ironfurnaces\", marker=\"3.3.1\"),\n                ForgeDataMod(name=\"mcwtrpdoors\", marker=\"1.0.6\"),\n                ForgeDataMod(name=\"mcwfences\", marker=\"1.0.5\"),\n                ForgeDataMod(name=\"supermartijn642corelib\", marker=\"1.0.19\"),\n                ForgeDataMod(name=\"simplylight\", marker=\"1.18.2-1.4.2-build.31\"),\n                ForgeDataMod(name=\"botania\", marker=\"1.18.2-434\"),\n                ForgeDataMod(name=\"highlighter\", marker=\"ANY\"),\n                ForgeDataMod(name=\"spark\", marker=\"<not required for client>\"),\n                ForgeDataMod(name=\"curios\", marker=\"1.18.2-5.0.7.1\"),  # codespell:ignore curios\n                ForgeDataMod(name=\"patchouli\", marker=\"1.18.2-71.1\"),\n                ForgeDataMod(name=\"camera\", marker=\"1.18.2-1.0.4\"),\n                ForgeDataMod(name=\"blockcarpentry\", marker=\"1.18-0.3.0\"),\n                ForgeDataMod(name=\"thermal_foundation\", marker=\"1.6.3.28\"),\n                ForgeDataMod(name=\"thermal_expansion\", marker=\"1.6.3.13\"),\n                ForgeDataMod(name=\"libnonymous\", marker=\"2.1.0\"),\n                ForgeDataMod(name=\"elevatorid\", marker=\"1.18.2-1.8.4\"),\n                ForgeDataMod(name=\"runelic\", marker=\"11.0.1\"),\n                ForgeDataMod(\n                    name=\"worldedit\",\n                    marker=\"<not required for client>\",\n                ),\n                ForgeDataMod(name=\"cfm\", marker=\"7.0.0-pre29\"),\n                ForgeDataMod(name=\"architectury\", marker=\"4.9.84\"),\n                ForgeDataMod(name=\"weirdinggadget\", marker=\"2.2.11\"),\n                ForgeDataMod(name=\"mcwfurnitures\", marker=\"3.0.0\"),\n                ForgeDataMod(name=\"trashcans\", marker=\"1.0.15\"),\n                ForgeDataMod(name=\"mcwlights\", marker=\"1.0.3\"),\n                ForgeDataMod(name=\"cucumber\", marker=\"5.1.2\"),\n                ForgeDataMod(name=\"snad\", marker=\"1.18.2-1.22.04.15a\"),\n                ForgeDataMod(name=\"jei\", marker=\"9.7.0.209\"),\n                ForgeDataMod(name=\"ae2\", marker=\"11.1.4\"),\n                ForgeDataMod(name=\"mekanism\", marker=\"10.2.5\"),\n                ForgeDataMod(name=\"bdlib\", marker=\"1.19.3.7\"),\n                ForgeDataMod(name=\"create\", marker=\"0.5.0.d\"),\n                ForgeDataMod(name=\"waystones\", marker=\"10.1.0\"),\n                ForgeDataMod(name=\"clumps\", marker=\"8.0.0+10\"),\n                ForgeDataMod(name=\"shutupexperimentalsettings\", marker=\"1.0.5\"),\n                ForgeDataMod(name=\"comforts\", marker=\"1.18.2-5.0.0.4\"),\n                ForgeDataMod(name=\"naturescompass\", marker=\"1.18.2-1.9.7-forge\"),\n                ForgeDataMod(name=\"storagenetwork\", marker=\"1.18.2-1.6.1\"),\n                ForgeDataMod(name=\"framedcompactdrawers\", marker=\"1.18-4.1.0\"),\n                ForgeDataMod(name=\"decorative_blocks\", marker=\"2.1.0\"),\n                ForgeDataMod(name=\"botanypots\", marker=\"8.0.12\"),\n                ForgeDataMod(name=\"ftbbackups2\", marker=\"1.0.17\"),\n                ForgeDataMod(name=\"cofh_core\", marker=\"1.6.4.21\"),\n                ForgeDataMod(name=\"mcjtylib\", marker=\"1.18-6.0.15\"),\n                ForgeDataMod(name=\"ispawner\", marker=\"1.0\"),\n                ForgeDataMod(name=\"everycomp\", marker=\"1.18.2-1.5.7\"),\n                ForgeDataMod(name=\"jeitweaker\", marker=\"3.0.0.8\"),\n                ForgeDataMod(name=\"terralith\", marker=\"0.0NONE\"),\n                ForgeDataMod(name=\"mininggadgets\", marker=\"1.11.0\"),\n                ForgeDataMod(name=\"crafttweaker\", marker=\"9.1.197\"),\n                ForgeDataMod(name=\"akashictome\", marker=\"1.5-20\"),\n                ForgeDataMod(name=\"forge\", marker=\"ANY\"),\n                ForgeDataMod(name=\"colossalchests\", marker=\"1.8.3\"),\n                ForgeDataMod(name=\"selene\", marker=\"1.18.2-1.17.9\"),\n                ForgeDataMod(name=\"drippyloadingscreen\", marker=\"1.6.4\"),\n                ForgeDataMod(\n                    name=\"craftingtweaks\",\n                    marker=\"<not required for client>\",\n                ),\n                ForgeDataMod(name=\"minecraft\", marker=\"1.18.2\"),\n                ForgeDataMod(name=\"terrablender\", marker=\"1.18.2-1.1.0.102\"),\n                ForgeDataMod(\n                    name=\"sophisticatedbackpacksvh\",\n                    marker=\"1.18.2-1.0.4.12\",\n                ),\n                ForgeDataMod(name=\"mousetweaks\", marker=\"ANY\"),\n                ForgeDataMod(name=\"titanium\", marker=\"3.5.6\"),\n                ForgeDataMod(name=\"jade\", marker=\"<not required for client>\"),\n                ForgeDataMod(name=\"createtweaker\", marker=\"2.0.0.17\"),\n                ForgeDataMod(name=\"easy_villagers\", marker=\"1.18.2-1.0.10\"),\n                ForgeDataMod(name=\"pipez\", marker=\"1.18.2-1.1.5\"),\n                ForgeDataMod(name=\"iceberg\", marker=\"ANY\"),\n                ForgeDataMod(name=\"flywheel\", marker=\"<not required for client>\"),\n                ForgeDataMod(name=\"mantle\", marker=\"1.9.27\"),\n                ForgeDataMod(name=\"ecologics\", marker=\"1.7.3\"),\n                ForgeDataMod(name=\"quark\", marker=\"3.2-358\"),\n                ForgeDataMod(name=\"xaerominimap\", marker=\"22.11.1\"),\n                ForgeDataMod(name=\"pigpen\", marker=\"8.0.1\"),\n                ForgeDataMod(name=\"fastbench\", marker=\"6.0.2\"),\n                ForgeDataMod(name=\"polymorph\", marker=\"1.18.2-0.44\"),\n                ForgeDataMod(name=\"autoreglib\", marker=\"1.7-53\"),\n                ForgeDataMod(name=\"storagedrawers\", marker=\"10.2.1\"),\n                ForgeDataMod(name=\"fluxnetworks\", marker=\"7.0.7.8\"),\n                ForgeDataMod(name=\"neoncraft2\", marker=\"2.2\"),\n                ForgeDataMod(name=\"enercell\", marker=\"0.0NONE\"),\n                ForgeDataMod(name=\"appleskin\", marker=\"2.4.0+mc1.18\"),\n                ForgeDataMod(\n                    name=\"ferritecore\",\n                    marker=\"<not required for client>\",\n                ),\n                ForgeDataMod(name=\"modularrouters\", marker=\"9.1.1-93\"),\n                ForgeDataMod(name=\"refinedstorageaddons\", marker=\"0.8.2\"),\n                ForgeDataMod(name=\"openloader\", marker=\"12.0.1\"),\n                ForgeDataMod(name=\"the_vault\", marker=\"1.18.2-2.0.10.869\"),\n            ],\n        ),\n        (\"truncated\", False),\n    ]\n\n    @pytest.fixture(scope=\"class\")\n    def build(self) -> ForgeData:\n        value = ForgeData.build(\n            RawForgeData(\n                {\n                    \"channels\": [],\n                    \"d\": (\n                        bytes.fromhex(\n                            \"e0ba8b0000c484e4a0b0e18e9be19997e2baaee1b399e392bae7a5a6e6908ae4a2b8c5b1e380a3e2b1a1e1a39ee39eb6e\"\n                            \"78db0e5bb86e19789e0a0b3e18ba3e49aa6e0b18be38686e685a8e5b39ce38695e6abbbe19896e2b78de0b181e1a097e3\"\n                            \"80ae02d098e2aeabe19987e2b7ade0b181e1a097e38caee1b880e59684e4af83e19b86e4b0ade1b99ce398b1e68dafe69\"\n                            \"b96e490b5e0a5b1e68e83e29985e0b08be1a097e384aed1a8e4b1a0ceabe29997e2b6aee1b298e392bae6b9aae6a1ace0\"\n                            \"b388e78dbbe199a6e0b3ade1a99bcab1e2b8b1e5b1a2e38398e4ae98e39ba7e6aface1af98e38cb7e69da9cba6c384e4a\"\n                            \"090e49890e0b2ade5b39ee39eb6e78da2e6888ce492b8e78781e48da2e2b6a1e1a998e2beb7e6a1a3e5b382e196b9e0ad\"\n                            \"a3cc90e48080e1a184e386b9e6a5a8e4aba8e5868de7ae9be19c85e2b68ce1b499e38abae38485e6899ce4a2b8e48081e\"\n                            \"198b0e2b3ace5b299e3aab4e0b1ade5b1a2e68384e185b1e18b93e29786e0ae8c18e6b48ae6bb86e2979de28db3e79bb6\"\n                            \"e2b9aee0b281e1a097e38caee28884e3b78ce48e83e39a96e2ba8ee5a39ae3a8b0e691a5e5bb86e19789e0a28be18ba3e\"\n                            \"49c86e4b28be1a096e394aee6999ce3a388e3a689e78e93e0b1a0e1a19ae39cb7e6b1a5e6888200e280b8e59a87e2b98c\"\n                            \"e1a19bd0b6e2b8b1e5b1ace3a38ce48691e380a3e4b981e5b499e39eb7e78dace48b84e1978de0a193e18ba3e29c86e0b\"\n                            \"38be1a097e3a4aee69096e58699e7adbbe39b86e2b18ee5b398c6b2e2b8b1c9a0e48080e78d88e49a96e2b4aee5ac98e3\"\n                            \"9cb4e695b6e6a39ce4a6bde2af8be68da0e49885e0b88bdc81e789a9e5b39ee1969de2adb3e19ca6e6ba8ce5b29bcab9e\"\n                            \"2b8b2e5b1a0e3a384e18d88e69bb7e2b3ade5ae99e3a4b2e791a1e6939ed78dc688e580a0e2bc81e1a598e39eb9e6bdb7\"\n                            \"e5a3a4e39691cc8be181a7e49786e0b58ce1a297e6b484e58b82e0b6b9e78688e18c8240e5a385e39eb7e6a5abe4bb9ce\"\n                            \"3b699e18e93e79b86e6b1ade5a89ae382b2e78da4e6888ce3a388e78681e78ca2e2b780e5b499e39ebbe6adb2e68886e4\"\n                            \"82b8e0a081e382b0e4b7ace1b49be39eb9e6b1ace5b392e0a69de480a0e59ba7e4ba8ce0b182e1a297e2b4b8e5b1a8e3a\"\n                            \"380e286a9e69e80e0b2ade4839de19c98c4b0e0b884e38780e1ac8be29996e2b7ace0b681e1a897e384aee6808ed6b1e2\"\n                            \"ac9be798a6e282ade0ae8ce19c98c4b0e0b884e2968ce0aea3e59986e4b68ce0b181e1a297e2b8b1e1a1a6d6b4e78d8be\"\n                            \"397b6e2b48ce1ae98e38ab7c5ac62e19080e7ae80e19db6e4b48ce0b382e1a097e384aee4919ae58695cc8be28290e6b7\"\n                            \"ace5ab9be390b9e6b1a5e0bb8ce4b384e185b1e58ba3d886c980e39eb6e791afe4ab84e39685e38e9be68c90d8a5e4ae8\"\n                            \"ec498e78c96e6839ee296a1e28e9be39a97e0b0ace1a59de384b2e68da1e68396e0b685e1ad9be184a7e29786e0b88ce1\"\n                            \"a497e38cade6899ce3a3a0e2a699e78ba3e49aa6e4878ce390b1e6b9a1e4ab9cd6b1c688e58080e6ba80e5a99de3a6b3e\"\n                            \"38493e6899ce582b8c5a9e49897e0b1aee4b19ae1a295e384aee5b1b0e0a388e181b8e19d96e0b68de1a999e38eb7e685\"\n                            \"a7e4bb88e58695e48e9be68cb1e698a5e0ae8ce19a98e795a2e5a392e3a691e5a6a9e39b92e498ace0b18be19c9ce7b4b\"\n                            \"2e5a888e29685e0adb3cd80e28080e5a482e3a4b0e795abe58ba8e4b6b1e0a0b3e68c83e49885e0b58bd080e68dade4a3\"\n                            \"aee3b6bde1ae93e18197d786e0ae8c1be79c87e4a382e38691e1acabe18397e29786e0b88ce1a497e380ade7819ce492b\"\n                            \"8e18789e584a0e6b2ade1a19ae392b7e6b5b3e4ab8ee196b9e0ae93e79d86e6b98de4869ce1a098e388aee6a99ce39188\"\n                            \"e5acabe69896e6b4ade5ad9ce38ab3e695aee48ba4e3b791e1ae93e181a7e49886e0b28be1aa9700e49088e38685e3ada\"\n                            \"be68cb0e49985e4b08be1a095d483e48baee386a5e58c8be59ba0e6ba8ce1af9de396b9e6b9a9e0ab8ee3a384e78681e1\"\n                            \"8c82e68080e5aa82e3a4b2e78da5e6ab9ee0b789e1acabc2a7e29786e0b48ce1a297e384aee689aee38084e68c98e49bb\"\n                            \"6e6b48ee5a397e39cb7e6a5a6d38ee4b0b8e2ad83e19d86e0b88de5a99ae39cb0e6bda3e4aba4e4b09de0a5b1e68c83d8\"\n                            \"a5e48c8ce382b6e6b9a9e49abed6a1e78db3e49996e282ade0ae8ce19c9830e18884e59690e6adabe59b96e2b6ade1b99\"\n                            \"be1a285e384aee5a9b0e3a384e786a9e48ca2e2b281e5ad9de3b2b6e6a1a3e5b382e196b9e0ada3cc90e68180e5b383e3\"\n                            \"a0bae6b1b0e5ab8ae3a695e0aea3e19ca6e6b2ade48d9ce19c98e3a0b1e6919ce492b4e2a5b1e18ba3e699a6e5ae81e3a\"\n                            \"8b2e6bdb7e59ba4e49085e18081e283a0e4b2aee1a999e38ab7e78da4e5bba8d789e2acbbe181a6e29786e0b08ce1a497\"\n                            \"e6b48ce58b82e7b6b9e48c9ae69896e2b78de5ac99e1a28000e59890e3a6bde18d9be49997e2b2aee0b181e1a697e38ca\"\n                            \"ee1a080d694e4ae9bd7b7e6b4aee5ac99e39cb4e0b1b3e5b1a2e68384e185b1e18b93d786e0ae8cc498e68c86e6939ee4\"\n                            \"b781e68cabe68c90d8a5e0ae8ee19a99e2b8b1e5b1a0e1b388e2aca0e199a6e0baace5b49be1a282e380aee6819cd0844\"\n                            \"0e39897e2b5ace1a59be3aab7e0a880e49b9ae0a79de4ae93e79986e6b2acc59ce19c99e2b8b066e580ace18dbbd8b7e2\"\n                            \"b6ade1b398e38abad9b2e781a2e492b8c5b1e38083e6b1a1e1ad9be3a4b8e78da5e58ba6e39795e0a2b3e48ba3e49786e\"\n                            \"0ad8ce3aab1e6b1a9e5b388e2b3a4e1ada9e68c96d8a5e0ae8ec499e78084e5b392e2a69de78688cc92e296a7e0ae8ce1\"\n                            \"9c9ce0b0b0e58ba0e1b6b9e1abbbe19a86e4b78ce1a59bcab6e4a590e0ba9cd385e68090e29a90e4b7aee5a69be3a4bae\"\n                            \"685aee4ab86e1978de78698e68cb2d8a5e1a985e39eb9e699aee693aad6b9e2ac9be79cb6e2b78be5b499e39ebbe6adb2\"\n                            \"e68886e482b809e582b0e6b1ade1b49de3a0b9e6bda4e6939ee1978de78688e68c82db85e48980e386b6e699b7e5b38ae\"\n                            \"1968de2ae9be68c90e49885e0b58be0ac80e795b3e4aba0e39789e18c8be19d87e4b58de0b69be1a49ae6bda3e4aba4e2\"\n                            \"96b1e38c93e68c90e49885e4b18b1ce78c8be5ab92e38781e68f8be79a96e0b48ce4959de19c98e3a0b1e6919ce492b4e\"\n                            \"285b1e28ba3e496a6e5b598e398b4e2b9a4e689a6e1b088e7ac90e19d86e2b78ce1a19ae1a285e384aee5b1b0e39388e1\"\n                            \"a6a1e48d83e2b6a0e1a998c2b730e19880e296a0e48cbbe19b86e0b3ade5b49ae3a4b2e48483e38a9ce19085ce98e2989\"\n                            \"7e4b5aee48680e3aab1e6a5b2e69b9ee490b9e0a5b1e68e83e29985e0b58be1a097e39caee6899ce39090e4ac8be19ba6\"\n                            \"d8a0e48280e3a084e791a1e58386e596bde4ada3e182b6e29786e0b88ce1a497e39cade5b1a2e18384e0ada8e69a96e28\"\n                            \"0ad0ccc81e685a3e4ab9ad789e0a1a3e18ba3e49c86e4b28be1a296e380aee6a19ce1809ce38cabe59896e0b68ee4859d\"\n                            \"e19c98e2b8b0c9a0e3a080e68c90e39bb6e6b5ace1a198e3a0b9e6b9a5e693a8e2a7a5e78688cc92d6a7e4ae8ce19c993\"\n                            \"0e6a0a4e196a1e6ae93e49896e4afade5af99e39cbae685a4e58ba8e3a6bde0a183e68ba3e69786e0ae8ce1b099e18480\"\n                            \"e583a8e4a695e0adabe79b86e0b2abe5b09ee39cb0e6a5b3e5b39ee490a1e385b1e38ba3e29786e0b38cd681e6a5ace5b\"\n                            \"384e3a6bde6af8be59bb6e2b9aee0b281e1a297e380aee4988ed6a1e78db3e49996e281ade0ae8cc298e0a882e5a38ae5\"\n                            \"a695e28c8be29bb7e0b4aee48c99e19c98e3a0b1e6919ce492b4e485b1e48ba3e28686e5a19be39cb4e68d9fe48b90e3a\"\n                            \"6b9e68cabe1809606c780e3aab9e695aee58b98e1a68de0a688cba3e29786e4858ce3ae84e789afe4a398e18695e28d8b\"\n                            \"e380b7e2baace4819ac298e6a488e6a39ce4a695e0adb3e19b86e298a0e48280e38681e6b5a6e6b896e482b8c5b1cb93e\"\n                            \"2b98ee4b299ce9ce695aee6bba8e4a6bde0ad9bcc9040e1a183e386b9e6a5a8e4aba8e5868de18eabe69e97e49a80e0b9\"\n                            \"8be1b097dcb4e4ab9ce5b791e18dbbe19ab7e298a000e3ae87e6a5a5e4a3a4e3a6a5e3acbbe49896e2b3ace1b499e1a48\"\n                            \"3e388aee6899cc384e6a1a8e798b6e2b38ee1b29de392b7e795b4e4aba4e1978de78698e68c82e49885c980e3a4bae78d\"\n                            \"a1e49b90e3a685e38e9be68c90e49885e4b18bc89ae685ade5b392e4908501e58290e6b1ade5ac9de38eb4e791a8e0aba\"\n                            \"6e3a384e78681ccb2e68480e5b598e3aab1e689ade6938ae59095e0a5b1e28ba306e1b381e382b7e189a4e5b1a2e68384\"\n                            \"e185b1e18b93e49786e0b28ce1a097e2b8b4e6a9a2e0a684e58098e19996e284ade4ae8ee19c9be2b8b0e681a4e1b3a4e\"\n                            \"48c98e69896e2b78de5ac99e1a282e380aee6819ce0a084e0a098e29996e28386e0b18ce1a297e390aee5a888e29685e0\"\n                            \"adb3e18c9040e5ad82e396b2e6b9a1e69b92e1a6b5c688e28ba3e29786e4888de38ab6e685abe58b9ce3978de0a0b3e68\"\n                            \"c83e49985e0b58bc880e68885e5a388e0a6a5e0a183e18ba3e49ca6e0b38be1ae97e6b48ae5a3aae29791e68c93e39bb6\"\n                            \"e2b5ace0b280c880e6a5ade49ba6e49085e18081e381a0e2b98ce1a199e38abae38087e6a99ce482b8e285b1e58186e2b\"\n                            \"0ade5ae9ae1a280c880e6b892e69685e28e9be69bb7e6b2ade4869ce1a098e384aee6819ce3a09ce28cabe79db7e6b98d\"\n                            \"e4839ae19c9830e0b080e3868ce6aeabe39c86c48ee0ae8ee19c98e2acb0e681a2e6a080e48e98e49d96e0baaee1a59ce\"\n                            \"3a0bce789a5e5ab92e3a695e0aea3e39b86e0b2aee5b49de39cb4e78da7e6888ae482b8e2a5b123e6b1a1e1ad9be39eb3\"\n                            \"e791b2e1b3a6e3a384e48689e28ba3e296a6e0ae8de19c98e2b8b0e0a1a8d6b4e78d8be18096e48086ce80e382b7e795b\"\n                            \"4e4aba4e0b78de6adbbe19c86e6b9ace4929ce19c98e3a0b1e6919ce492b4e4a5b1e78ba3e496a6e1af99e38eb9e0b9a5\"\n                            \"e48b9ce59791e2ae93e39cb6e2b7ace5b09be3a6b0cdb3e5b1a2d380e78090e49cb0e4b7aee5a19ce38ab3e695aee6bba\"\n                            \"8e4a6bde68d9be68c90d8a5e0ae8ee19a99e2b8b1e5b1ace38384e0ada8e69a96e6afade5a898e39cb0e695aecb98c384\"\n                            \"e28080e299a1e2b0aee1a59be386b2e6b5afe48ba0e5868de18ca3e79897e4b2aee1b39ce1a285e384aee5a9b0e3a390e\"\n                            \"78689cc82c8a0e5a599e39eb1e685b2e58ba8e19799e18bbbe79b86e6b1ade5b39ae1a482e384aee6819ce2a080e7ac90\"\n                            \"e19d86e2b78ce5b09ee3a8b7d9b3e5b1b0e3a380e18689e38083e0b381e1a29de382b1e6ada3e683aae4a78de0a0b1cba\"\n                            \"3e29786e0b78cd281e6bda3e5838ce0b5bde18dbbd997e498a1e0b68be1a897e388aee0b9a2e1969ce2adb3e19ca6e2b6\"\n                            \"8ce4b180c480e6b488e59386e69791e4ada3e398a6e498a1e0b18be19a9ce2b8b6e5b1a0e59384e6a181e298b6e2ba8de\"\n                            \"5ac9ee384b4e38483e6819c04e4a180dcb6e6b0aee5ae9de3a4b2e38483e6819ce29080e38ca8e29997e6bcaee5af98e3\"\n                            \"a0b6e3848ce6899ce3a3a0e6a691e68c92e49aa5e0b78bd480e695aae6a392e1979de5ac8be29996e683aee0ae8ce19c9\"\n                            \"8e2b8b070e580a4e18cabe19ca7e2b68ce1b49aceb4e2b8b0e1b1a0e3a4bde188aae58390e4b4ade1a99be38eb7e685a7\"\n                            \"e4bb88e58695e38e9be68c90e298a5e0ae8ce0a898e685ade5b392e3a5bde28cabe79db7e6b98de59f9ae390b1e6b9a1e\"\n                            \"4ab9cd6b1c690e480a0e4b1a1e1a19ce3a8b3e79db4e48b8ae196ade3ae93e68e90e498a5e4b18be1ae9ce6b484e58b82\"\n                            \"e196b9e78688e68c82d885e48280e38285e685abe583a6e0b6a5e7aea3e59b96e2838ce4ae8ce19a9ae380b2e5a888e29\"\n                            \"685e0adb3cc90e28280e5a681e3a4b7e695a7c886e694b9e281a2e59a97e6b98ce5b397e3a4b7e6a5b4e4bb9ce4908dc5\"\n                            \"b1e58083e0b9a0e5ac9ce3a8b4e38483e6899ce0a084e1a1b0e49bb6e6b7ade5b39ce398b0e6a1a3e69b8ae4b791e0a0a\"\n                            \"bcba3e69787e48c8ce390b1e6b9a1e4ab9ce7b6b1e0adaae69a96e282ade0ae8ce19c98c4b0e0b084e1978ce2ada3e59b\"\n                            \"a6e286ace4ae8ce1b098e388aee6899ae492b8e786b9e78e92e2b780e5b499e39ebbe6adb2e6888200e28298e19ca6e0b\"\n                            \"88de1b99ce39eb6e691a1e5b392e4b69de18c9be59997e2b78ce0b181e1ac97e390aee1b086e4a68ce38c8be19d86e6b7\"\n                            \"8de5b499e38abbe6ada1e0bba6e196b8e3aea3e29bb7e6b5aee0b180e1a097d080e5a892e3a6a5e1acabe19ca6e0b38ce\"\n                            \"4869de19c98e3a0b1e6919ce590a8e18db3e79997e6b4ace5b49ce3a4b2e49884e1a29ad38de18180e79997e6b4ace5b4\"\n                            \"9ce3a4b2e49884e1a29ad38de68080e59d80e4b98ce1a19ce398b1e6b9a5e4ab88e48789e78688cc92e49787e4ad8ce19\"\n                            \"c98e2b8b1e5b1a0e48384c691e38680e0b7aee5a89ce3a6b4e6a5b4e48b86e19791e18ca3e39896e0b5ace5a19ce396b1\"\n                            \"e799b3e1bb90e3a384e48689e28ba3e296a6e0ae8ce19c98e2b8b4e691a2e2b080e7ada8e39d96e0b2aee5b79de382b2e\"\n                            \"78dabc886e694b9e48092e19d80e2ba8de5ae98e3aab4d5ade5b1a6e3a394e3a6b1e59ba0e6ba8ce1af9de396b9e38483\"\n                            \"e6819cd084e580a0e49896e0b2ace48d80e3a4b1e685a5e4aba8e5b791e0acabe59ab6e0b98ce0b282e1a097e380aee68\"\n                            \"99ce0a39ce2a1b0e39896e6bcaee5b697e398b4e685ace4ab8ee4b789e0a1abe18ba3e49c86e4b28be1a296e380aee689\"\n                            \"9ce1b380e2aca0e199a6e0baace5b49be1a282e380aee6819ce0a08428da97e4b2aee48c9ee19c98e3a0b1e6919ce492b\"\n                            \"4e0a5b1e58ba3c3a6e1a599e382b3e6b1b5e0aba8e3a384e78681e18c82e68080e5a981e38ab1e695a2e4bba4d08de4a9\"\n                            \"b215e0b381e5b99be390bbe695a5d398e39098e78c8be49d86e4b2ade0b181e1b297e388aee0b9aee196b8e3aea3e29bb\"\n                            \"7e2b5aee0b18000e69489e5bb86e3b6b1e4acbbe398b6e282aee4ae8ce19c9bc8b3e6888ad795e5ae93e381b6e49786e4\"\n                            \"ad8ce1aa99d0b8e48b9ae3a6a5e0a08be28083c680e5a19ee3a4b2e6b5afe5b392e396a5cc8be281b7e49986e4b18be19\"\n                            \"c98d0b1e48b9ae3a6a5e0a09bcba326c680e392b8e781a7e5b38ae68095c5b1e18ba3e28186e5a682e3a6b0e689b4e5b3\"\n                            \"8ae2868de380abcba3e49786e4878ce390b1e6b9a1e4ab9ce196b1e786a0e68da2e29885e48280e3a084e6b1afe5abb2e\"\n                            \"4a6bde48e83e182b6e29786e0b88ce1a497e380ade6a19ce18390e0ada8e69a96e280ad0cd480e795a1e5bba8e19789e6\"\n                            \"8cbbe29a96e2838ce4ae8ce19a9be38cb5e1b084e5878ce18dbbe79897e0b2ace5b299e3aeb0e789a5e0b3a6e48384e18\"\n                            \"5b1e18ba3e28686e5a19be39cb4e68d9fe48b90e3a6b9e68cabe1809606cc80e398b3e7a1b5e4ab9ce5b791e18dbbe39a\"\n                            \"b7e683aee0ae8de19c98e2b8b770e3a0a8e7acabe39ba6e2b98ce1a698e1a4bae38883e6919ce28088e78ca8e29996e2b\"\n                            \"1aee1ac99ceb6e2b8b0e1b1a0e3a4bde3a8aae59ba0e6ba8ce1af9de396b9e38085e6819ce482b8e18081e18290e0b88c\"\n                            \"e5ac9ce3a6b2e6a5abe1a39ce3a388e786a1e38c82e6b6a5e0b198e1a297d0b8e78ba6e0b6b9e0a08be18093e485a0e1a\"\n                            \"599e3a4b9e791a9e49b8ae4a6bde18cabe583a0e0b7ade1b599e382b6e789b2e6ab9ee19791e1ae93e18287e29787e4ae\"\n                            \"8ce19a98e38cb9e5a898e29685e7adb3d8b5e4b0ade5ae9be398b2e3888100e4a190e38cabe69a96e0b2ade1b399e39eb\"\n                            \"ae685b2e4ab8ee18685e7aca3e39ba6c2aee0ae8ce19c9c32e5b894e19781e68db3e19bb6e2b28ce1b299e1a283e2b8b2\"\n                            \"e5b1a0e0a384e28188e59a87e4aface5a19de398bae185b4e5b1a2e68384e185b1e28b93d786e4ae8ce1a098e3a0aee78\"\n                            \"9ace3a09ce28cabe79db7e6b98dc69ae19c98e398b2e6819cd080e6a2a8e49bb6e4b2ace5ae9ce392bae698bae6ab98e3\"\n                            \"a7a1e28cabe79db7e6b98de5b39ae1ae81e39cb000\"\n                        )\n                    ).decode(\"utf-8\"),\n                    \"fmlNetworkVersion\": 3,\n                    \"mods\": [],\n                    \"truncated\": True,\n                }\n            )\n        )\n        assert value is not None\n        return value\n\n    def test_build_with_empty_input(self):\n        with pytest.raises(KeyError, match=r\"^'Neither `mods` or `modList` keys exist\\.'$\"):\n            ForgeData.build({})\n\n\n@pytest.mark.parametrize(\"key\", [\"forgeData\", \"modinfo\"])\ndef test_java_status_response_forge_data_is_none(key):\n    # should not raise\n    JavaStatusResponse.build(\n        JAVA_RAW_RESPONSE | {key: None},  # pyright: ignore[reportArgumentType] # dict[str, Unknown] cannot be assigned to TypedDict\n    )\n\n\n@pytest.mark.parametrize(\n    (\"key\", \"raw\"),\n    [\n        (\"forgeData\", TestForgeDataV2.RAW),\n        (\"modinfo\", TestForgeDataV1.RAW),\n    ],\n)\ndef test_java_status_response_forge_data(key: str, raw: bytes) -> None:\n    assert JavaStatusResponse.build(\n        JAVA_RAW_RESPONSE | {key: raw},  # pyright: ignore[reportArgumentType] # dict[str, Unknown] cannot be assigned to TypedDict\n    ).forge_data == ForgeData.build(raw)  # pyright: ignore[reportArgumentType] # dict[str, Unknown] cannot be assigned to TypedDict\n"
  },
  {
    "path": "tests/responses/test_java.py",
    "content": "import typing as t\n\nimport pytest\n\nfrom mcstatus.motd import Motd\nfrom mcstatus.responses import JavaStatusPlayer, JavaStatusPlayers, JavaStatusResponse, JavaStatusVersion\nfrom tests.responses import BaseResponseTest\n\n\n@BaseResponseTest.construct\nclass TestJavaStatusResponse(BaseResponseTest):\n    RAW: t.ClassVar = {\n        \"players\": {\"max\": 20, \"online\": 0},\n        \"version\": {\"name\": \"1.8-pre1\", \"protocol\": 44},\n        \"description\": \"A Minecraft Server\",\n        \"enforcesSecureChat\": True,\n        \"favicon\": \"data:image/png;base64,foo\",\n    }\n\n    EXPECTED_VALUES: t.ClassVar = [\n        (\"players\", JavaStatusPlayers(0, 20, None)),\n        (\"version\", JavaStatusVersion(\"1.8-pre1\", 44)),\n        (\"motd\", Motd.parse(\"A Minecraft Server\", bedrock=False)),\n        (\"latency\", 0),\n        (\"enforces_secure_chat\", True),\n        (\"icon\", \"data:image/png;base64,foo\"),\n        (\"raw\", RAW),\n        (\"forge_data\", None),\n    ]\n    OPTIONAL_FIELDS: t.ClassVar = (\n        [(\"favicon\", \"icon\"), (\"enforcesSecureChat\", \"enforces_secure_chat\")],\n        {\n            \"players\": {\"max\": 20, \"online\": 0},\n            \"version\": {\"name\": \"1.8-pre1\", \"protocol\": 44},\n            \"description\": \"A Minecraft Server\",\n            \"enforcesSecureChat\": True,\n            \"favicon\": \"data:image/png;base64,foo\",\n        },\n    )\n\n    @pytest.fixture(scope=\"class\")\n    def build(self) -> JavaStatusResponse:\n        return JavaStatusResponse.build(self.RAW)  # pyright: ignore[reportArgumentType] # dict[str, Unknown] cannot be assigned to TypedDict\n\n    def test_as_dict(self, build: JavaStatusResponse):\n        assert build.as_dict() == {\n            \"enforces_secure_chat\": True,\n            \"forge_data\": None,\n            \"icon\": \"data:image/png;base64,foo\",\n            \"latency\": 0,\n            \"motd\": \"A Minecraft Server\",\n            \"players\": {\"max\": 20, \"online\": 0, \"sample\": None},\n            \"raw\": {\n                \"description\": \"A Minecraft Server\",\n                \"enforcesSecureChat\": True,\n                \"favicon\": \"data:image/png;base64,foo\",\n                \"players\": {\"max\": 20, \"online\": 0},\n                \"version\": {\"name\": \"1.8-pre1\", \"protocol\": 44},\n            },\n            \"version\": {\"name\": \"1.8-pre1\", \"protocol\": 44},\n        }\n\n    def test_description_alias(self, build: JavaStatusResponse):\n        assert build.description == \"A Minecraft Server\"\n\n\n@BaseResponseTest.construct\nclass TestJavaStatusPlayers(BaseResponseTest):\n    EXPECTED_VALUES: t.ClassVar = [\n        (\"max\", 20),\n        (\"online\", 0),\n        (\n            \"sample\",\n            [\n                JavaStatusPlayer(\"foo\", \"0b3717c4-f45d-47c8-b8e2-3d9ff6f93a89\"),\n                JavaStatusPlayer(\"bar\", \"61699b2e-d327-4a01-9f1e-0ea8c3f06bc6\"),\n                JavaStatusPlayer(\"baz\", \"40e8d003-8872-412d-b09a-4431a5afcbd4\"),\n            ],\n        ),\n    ]\n    OPTIONAL_FIELDS: t.ClassVar = (\n        [(\"sample\", \"sample\")],\n        {\n            \"max\": 20,\n            \"online\": 0,\n            \"sample\": [\n                {\"name\": \"foo\", \"id\": \"0b3717c4-f45d-47c8-b8e2-3d9ff6f93a89\"},\n                {\"name\": \"bar\", \"id\": \"61699b2e-d327-4a01-9f1e-0ea8c3f06bc6\"},\n                {\"name\": \"baz\", \"id\": \"40e8d003-8872-412d-b09a-4431a5afcbd4\"},\n            ],\n        },\n    )\n\n    @pytest.fixture(scope=\"class\")\n    def build(self) -> JavaStatusPlayers:\n        return JavaStatusPlayers.build(\n            {\n                \"max\": 20,\n                \"online\": 0,\n                \"sample\": [\n                    {\"name\": \"foo\", \"id\": \"0b3717c4-f45d-47c8-b8e2-3d9ff6f93a89\"},\n                    {\"name\": \"bar\", \"id\": \"61699b2e-d327-4a01-9f1e-0ea8c3f06bc6\"},\n                    {\"name\": \"baz\", \"id\": \"40e8d003-8872-412d-b09a-4431a5afcbd4\"},\n                ],\n            }\n        )\n\n    def test_empty_sample_turns_into_empty_list(self) -> None:\n        assert JavaStatusPlayers.build({\"max\": 20, \"online\": 0, \"sample\": []}).sample == []\n\n    def test_java_status_players_sample_is_none(self) -> None:\n        # should not raise\n        assert (\n            JavaStatusPlayers.build(\n                {\n                    \"online\": 1,\n                    \"max\": 123,\n                    \"sample\": None,\n                }\n            ).sample\n            is None\n        )\n\n\n@BaseResponseTest.construct\nclass TestJavaStatusPlayer(BaseResponseTest):\n    EXPECTED_VALUES: t.ClassVar = [(\"name\", \"foo\"), (\"id\", \"0b3717c4-f45d-47c8-b8e2-3d9ff6f93a89\")]\n\n    @pytest.fixture(scope=\"class\")\n    def build(self) -> JavaStatusPlayer:\n        return JavaStatusPlayer.build({\"name\": \"foo\", \"id\": \"0b3717c4-f45d-47c8-b8e2-3d9ff6f93a89\"})\n\n    def test_id_field_the_same_as_uuid(self) -> None:\n        unique = object()\n        build = JavaStatusPlayer.build({\"name\": \"foo\", \"id\": unique})  # pyright: ignore[reportArgumentType]\n        assert build.id is build.uuid\n        assert build.uuid is unique\n\n\n@BaseResponseTest.construct\nclass TestJavaStatusVersion(BaseResponseTest):\n    EXPECTED_VALUES: t.ClassVar = [(\"name\", \"1.8-pre1\"), (\"protocol\", 44)]\n\n    @pytest.fixture(scope=\"class\")\n    def build(self) -> JavaStatusVersion:\n        return JavaStatusVersion.build({\"name\": \"1.8-pre1\", \"protocol\": 44})\n"
  },
  {
    "path": "tests/responses/test_legacy.py",
    "content": "import typing as t\n\nimport pytest\n\nfrom mcstatus.motd import Motd\nfrom mcstatus.responses import LegacyStatusPlayers, LegacyStatusResponse, LegacyStatusVersion\nfrom tests.responses import BaseResponseTest\n\n\n@pytest.fixture(scope=\"module\")\ndef build():\n    return LegacyStatusResponse.build(\n        [\n            \"47\",\n            \"1.4.2\",\n            \"A Minecraft Server\",\n            \"0\",\n            \"20\",\n        ],\n        123.0,\n    )\n\n\n@BaseResponseTest.construct\nclass TestLegacyStatusResponse(BaseResponseTest):\n    EXPECTED_VALUES: t.ClassVar = [\n        (\"motd\", Motd.parse(\"A Minecraft Server\")),\n        (\"latency\", 123.0),\n    ]\n    EXPECTED_TYPES: t.ClassVar = [\n        (\"players\", LegacyStatusPlayers),\n        (\"version\", LegacyStatusVersion),\n    ]\n\n    @pytest.fixture(scope=\"class\")\n    def build(self, build):\n        return build\n\n    def test_as_dict(self, build: LegacyStatusResponse):\n        assert build.as_dict() == {\n            \"latency\": 123.0,\n            \"motd\": \"A Minecraft Server\",\n            \"players\": {\"max\": 20, \"online\": 0},\n            \"version\": {\"name\": \"1.4.2\", \"protocol\": 47},\n        }\n\n\n@BaseResponseTest.construct\nclass TestLegacyStatusPlayers(BaseResponseTest):\n    EXPECTED_VALUES: t.ClassVar = [(\"online\", 0), (\"max\", 20)]\n\n    @pytest.fixture(scope=\"class\")\n    def build(self, build):\n        return build.players\n\n\n@BaseResponseTest.construct\nclass TestLegacyStatusVersion(BaseResponseTest):\n    EXPECTED_VALUES: t.ClassVar = [(\"name\", \"1.4.2\"), (\"protocol\", 47)]\n\n    @pytest.fixture(scope=\"class\")\n    def build(self, build):\n        return build.version\n"
  },
  {
    "path": "tests/responses/test_query.py",
    "content": "import typing as t\n\nimport pytest\n\nfrom mcstatus.motd import Motd\nfrom mcstatus.responses import QueryPlayers, QueryResponse, QuerySoftware\nfrom mcstatus.responses._raw import RawQueryResponse\nfrom tests.helpers import patch_project_version\nfrom tests.responses import BaseResponseTest\n\n\n@BaseResponseTest.construct\nclass TestQueryResponse(BaseResponseTest):\n    RAW: t.ClassVar[RawQueryResponse] = RawQueryResponse(\n        hostname=\"A Minecraft Server\",\n        gametype=\"GAME TYPE\",  # pyright: ignore[reportArgumentType] # different from the hardcoded value\n        game_id=\"GAME ID\",  # pyright: ignore[reportArgumentType] # different from the hardcoded value\n        version=\"1.8\",\n        plugins=\"\",\n        map=\"world\",\n        numplayers=\"3\",\n        maxplayers=\"20\",\n        hostport=\"9999\",\n        hostip=\"192.168.56.1\",\n    )\n    RAW_PLAYERS: t.ClassVar = [\"Dinnerbone\", \"Djinnibone\", \"Steve\"]\n\n    EXPECTED_VALUES: t.ClassVar = [\n        (\"raw\", RAW),\n        (\"motd\", Motd.parse(\"A Minecraft Server\")),\n        (\"map_name\", \"world\"),\n        (\"players\", QueryPlayers(online=3, max=20, list=[\"Dinnerbone\", \"Djinnibone\", \"Steve\"])),\n        (\"software\", QuerySoftware(version=\"1.8\", brand=\"vanilla\", plugins=[])),\n        (\"ip\", \"192.168.56.1\"),\n        (\"port\", 9999),\n        (\"game_type\", \"GAME TYPE\"),\n        (\"game_id\", \"GAME ID\"),\n    ]\n\n    @pytest.fixture(scope=\"class\")\n    def build(self):\n        return QueryResponse.build(raw=self.RAW, players_list=self.RAW_PLAYERS)\n\n    def test_as_dict(self, build: QueryResponse):\n        assert build.as_dict() == {\n            \"game_id\": \"GAME ID\",\n            \"game_type\": \"GAME TYPE\",\n            \"ip\": \"192.168.56.1\",\n            \"map_name\": \"world\",\n            \"motd\": \"A Minecraft Server\",\n            \"players\": {\n                \"list\": [\n                    \"Dinnerbone\",\n                    \"Djinnibone\",\n                    \"Steve\",\n                ],\n                \"max\": 20,\n                \"online\": 3,\n            },\n            \"port\": 9999,\n            \"raw\": {\n                \"game_id\": \"GAME ID\",\n                \"gametype\": \"GAME TYPE\",\n                \"hostip\": \"192.168.56.1\",\n                \"hostname\": \"A Minecraft Server\",\n                \"hostport\": \"9999\",\n                \"map\": \"world\",\n                \"maxplayers\": \"20\",\n                \"numplayers\": \"3\",\n                \"plugins\": \"\",\n                \"version\": \"1.8\",\n            },\n            \"software\": {\n                \"brand\": \"vanilla\",\n                \"plugins\": [],\n                \"version\": \"1.8\",\n            },\n        }\n\n    def test_deprecated_map_alias(self, build: QueryResponse):\n        with (\n            patch_project_version(\"0.0.0\"),\n            pytest.deprecated_call(\n                match=r\"^QueryResponse\\.map is deprecated and scheduled for removal in 13\\.0\\.0, use map_name instead\\.$\",\n            ),\n        ):\n            assert build.map == build.map_name\n\n\n@BaseResponseTest.construct\nclass TestQueryPlayers(BaseResponseTest):\n    EXPECTED_VALUES: t.ClassVar = [\n        (\"online\", 3),\n        (\"max\", 20),\n        (\"list\", [\"Dinnerbone\", \"Djinnibone\", \"Steve\"]),\n    ]\n\n    @pytest.fixture(scope=\"class\")\n    def build(self):\n        return QueryPlayers.build(\n            raw={\n                \"hostname\": \"A Minecraft Server\",\n                \"gametype\": \"SMP\",\n                \"game_id\": \"MINECRAFT\",\n                \"version\": \"1.8\",\n                \"plugins\": \"\",\n                \"map\": \"world\",\n                \"numplayers\": \"3\",\n                \"maxplayers\": \"20\",\n                \"hostport\": \"25565\",\n                \"hostip\": \"192.168.56.1\",\n            },\n            players_list=[\"Dinnerbone\", \"Djinnibone\", \"Steve\"],\n        )\n\n    def test_deprecated_names_alias(self, build: QueryPlayers):\n        with (\n            patch_project_version(\"0.0.0\"),\n            pytest.deprecated_call(\n                match=(\n                    r\"^QueryPlayers\\.names is deprecated and scheduled for removal in 13\\.0\\.0, \"\n                    r\"use 'list' attribute instead\\.$\"\n                ),\n            ),\n        ):\n            assert build.names == build.list\n\n\nclass TestQuerySoftware:\n    def test_vanilla(self):\n        software = QuerySoftware.build(\"1.8\", \"\")\n        assert software.brand == \"vanilla\"\n        assert software.version == \"1.8\"\n        assert software.plugins == []\n\n    def test_modded(self):\n        software = QuerySoftware.build(\"1.8\", \"A modded server: Foo 1.0; Bar 2.0; Baz 3.0\")\n        assert software.brand == \"A modded server\"\n        assert software.plugins == [\"Foo 1.0\", \"Bar 2.0\", \"Baz 3.0\"]\n\n    def test_modded_no_plugins(self):\n        software = QuerySoftware.build(\"1.8\", \"A modded server\")\n        assert software.brand == \"A modded server\"\n        assert software.plugins == []\n"
  },
  {
    "path": "tests/test_cli.py",
    "content": "import contextlib\nimport io\nimport json\nimport os\nimport socket\nfrom unittest import mock\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom mcstatus import BedrockServer, JavaServer, LegacyServer\nfrom mcstatus.__main__ import PING_PACKET_FAIL_WARNING, QUERY_FAIL_WARNING, main as main_under_test\nfrom mcstatus.responses import BedrockStatusResponse, JavaStatusResponse, LegacyStatusResponse, QueryResponse\nfrom mcstatus.responses._raw import RawJavaResponse\n\nJAVA_RAW_RESPONSE: RawJavaResponse = {\n    \"players\": {\"max\": 20, \"online\": 0},\n    \"version\": {\"name\": \"1.8-pre1\", \"protocol\": 44},\n    \"description\": \"A Minecraft Server\",\n    \"enforcesSecureChat\": True,\n    \"favicon\": \"data:image/png;base64,foo\",\n}\n\nQUERY_RAW_RESPONSE = [\n    {\n        \"hostname\": \"A Minecraft Server\",\n        \"gametype\": \"GAME TYPE\",\n        \"game_id\": \"GAME ID\",\n        \"version\": \"1.8\",\n        \"plugins\": \"\",\n        \"map\": \"world\",\n        \"numplayers\": \"3\",\n        \"maxplayers\": \"20\",\n        \"hostport\": \"9999\",\n        \"hostip\": \"192.168.56.1\",\n    },\n    [\"Dinnerbone\", \"Djinnibone\", \"Steve\"],\n]\n\nLEGACY_RAW_RESPONSE = [\n    \"47\",\n    \"1.4.2\",\n    \"A Minecraft Server\",\n    \"0\",\n    \"20\",\n]\n\nBEDROCK_RAW_RESPONSE = [\n    \"MCPE\",\n    \"§r§4G§r§6a§r§ey§r§2B§r§1o§r§9w§r§ds§r§4e§r§6r\",\n    \"422\",\n    \"1.18.100500\",\n    \"1\",\n    \"69\",\n    \"3767071975391053022\",\n    \"map name here\",\n    \"Default\",\n    \"1\",\n    \"19132\",\n    \"-1\",\n    \"3\",\n]\n\n# NOTE: if updating this, be sure to change other occurrences of this help text!\n# to update, use: `COLUMNS=100000 poetry run mcstatus --help`\nEXPECTED_HELP_OUTPUT = \"\"\"\nusage: mcstatus [-h] [--bedrock | --legacy] address {ping,status,query,json} ...\n\nmcstatus provides an easy way to query Minecraft servers for any information they can expose. It provides three modes of access: query, status, ping and json.\n\npositional arguments:\n  address               The address of the server.\n\noptions:\n  -h, --help            show this help message and exit\n  --bedrock             Specifies that 'address' is a Bedrock server (default: Java).\n  --legacy              Specifies that 'address' is a pre-1.7 Java server (default: 1.7+).\n\ncommands:\n  Command to run, defaults to 'status'.\n\n  {ping,status,query,json}\n    ping                Ping server for latency.\n    status              Prints server status.\n    query               Prints detailed server information. Must be enabled in servers' server.properties file.\n    json                Prints server status and query in json.\n\"\"\"  # noqa: E501 (line length)\n\n\n@contextlib.contextmanager\ndef patch_stdout_stderr():\n    outpatch = patch(\"sys.stdout\", new=io.StringIO())\n    errpatch = patch(\"sys.stderr\", new=io.StringIO())\n    with outpatch as out, errpatch as err:\n        yield out, err\n\n\n@pytest.fixture\ndef mock_network_requests():\n    with (\n        patch(\"mcstatus.server.JavaServer.lookup\", return_value=JavaServer(\"example.com\", port=25565)),\n        patch(\"mcstatus.server.JavaServer.ping\", return_value=0),\n        patch(\"mcstatus.server.JavaServer.status\", return_value=JavaStatusResponse.build(JAVA_RAW_RESPONSE)),\n        patch(\"mcstatus.server.JavaServer.query\", return_value=QueryResponse.build(*QUERY_RAW_RESPONSE)),\n        patch(\"mcstatus.server.LegacyServer.lookup\", return_value=LegacyServer(\"example.com\", port=25565)),\n        patch(\n            \"mcstatus.server.LegacyServer.status\", return_value=LegacyStatusResponse.build(LEGACY_RAW_RESPONSE, latency=123)\n        ),\n        patch(\"mcstatus.server.BedrockServer.lookup\", return_value=BedrockServer(\"example.com\", port=25565)),\n        patch(\n            \"mcstatus.server.BedrockServer.status\",\n            return_value=(BedrockStatusResponse.build(BEDROCK_RAW_RESPONSE, latency=123)),\n        ),\n    ):\n        yield\n\n\ndef normalise_help_output(s: str) -> str:\n    \"\"\"Normalises the output of `mcstatus --help`.\n\n    A work around some discrepancies between Python versions while still\n    retaining meaningful information for comparison.\n    \"\"\"\n    elided = \"[...]:\"\n\n    s = s.strip()\n\n    # drop lines which end in \":\". these argparse section headings vary between python versions.\n    # it is just a small style change, so it doesn't matter so much to do `sys.version_info` check\n    return \"\\n\".join(ln if not ln.endswith(\":\") else elided for ln in s.splitlines())\n\n\n# NOTE: for premature exits in argparse, we must catch SystemExit.\n# for ordinary exits in the CLI code, we can simply inspect the return value.\n\n\ndef test_no_args():\n    with patch_stdout_stderr() as (out, err), pytest.raises(SystemExit, match=r\"^2$\") as exn:\n        main_under_test([])\n\n    assert out.getvalue() == \"\"\n    assert \"usage: \" in err.getvalue()\n    assert exn.value.code != 0\n\n\ndef test_help():\n    with patch_stdout_stderr() as (out, err), pytest.raises(SystemExit, match=r\"^0$\") as exn:\n        main_under_test([\"--help\"])\n\n    assert \"usage: \" in out.getvalue()\n    assert err.getvalue() == \"\"\n    assert exn.value.code == 0\n\n\n@mock.patch.dict(os.environ, {\"COLUMNS\": \"100000\"})  # prevent line-wrapping in --help output\ndef test_help_matches_recorded_output():\n    with patch_stdout_stderr() as (out, err), pytest.raises(SystemExit, match=r\"^0$\"):\n        main_under_test([\"--help\"])\n\n    assert normalise_help_output(out.getvalue()) == normalise_help_output(EXPECTED_HELP_OUTPUT)\n    assert err.getvalue() == \"\"\n\n\ndef test_one_argument_is_status(mock_network_requests):\n    with patch_stdout_stderr() as (out, err):\n        assert main_under_test([\"example.com\"]) == 0\n\n    assert out.getvalue() == (\n        \"version: Java 1.8-pre1 (protocol 44)\\n\"\n        \"motd: \\x1b[0mA Minecraft Server\\x1b[0m\\n\"\n        \"players: 0/20\\n\"\n        \"ping: 0.00 ms\\n\"\n    )  # fmt: skip\n    assert err.getvalue() == \"\"\n\n\ndef test_status(mock_network_requests):\n    with patch_stdout_stderr() as (out, err):\n        assert main_under_test([\"example.com\", \"status\"]) == 0\n\n    assert out.getvalue() == (\n        \"version: Java 1.8-pre1 (protocol 44)\\n\"\n        \"motd: \\x1b[0mA Minecraft Server\\x1b[0m\\n\"\n        \"players: 0/20\\n\"\n        \"ping: 0.00 ms\\n\"\n    )  # fmt: skip\n    assert err.getvalue() == \"\"\n\n\ndef test_status_with_sample(mock_network_requests):\n    raw_response = JAVA_RAW_RESPONSE.copy()\n    raw_response[\"players\"] = JAVA_RAW_RESPONSE[\"players\"].copy()\n    raw_response[\"players\"][\"sample\"] = [\n        {\"name\": \"foo\", \"id\": \"497dcba3-ecbf-4587-a2dd-5eb0665e6880\"},\n        {\"name\": \"bar\", \"id\": \"50e14f43-dd4e-412f-864d-78943ea28d91\"},\n        {\"name\": \"baz\", \"id\": \"7edb3b2e-869c-485b-af70-76a934e0fcfd\"},\n    ]\n\n    with (\n        patch(\"mcstatus.server.JavaServer.status\", return_value=JavaStatusResponse.build(raw_response)),\n        patch_stdout_stderr() as (out, err),\n    ):\n        assert main_under_test([\"example.com\", \"status\"]) == 0\n\n    assert out.getvalue() == (\n        \"version: Java 1.8-pre1 (protocol 44)\\n\"\n        \"motd: \\x1b[0mA Minecraft Server\\x1b[0m\\n\"\n        \"players: 0/20\\n\"\n        \"  foo (497dcba3-ecbf-4587-a2dd-5eb0665e6880)\\n\"\n        \"  bar (50e14f43-dd4e-412f-864d-78943ea28d91)\\n\"\n        \"  baz (7edb3b2e-869c-485b-af70-76a934e0fcfd)\\n\"\n        \"ping: 0.00 ms\\n\"\n    )\n    assert err.getvalue() == \"\"\n\n\ndef test_status_sample_empty_list(mock_network_requests):\n    raw_response = JAVA_RAW_RESPONSE.copy()\n    raw_response[\"players\"] = JAVA_RAW_RESPONSE[\"players\"].copy()\n    raw_response[\"players\"][\"sample\"] = []\n\n    with (\n        patch(\"mcstatus.server.JavaServer.status\", return_value=JavaStatusResponse.build(raw_response)),\n        patch_stdout_stderr() as (out, err),\n    ):\n        assert main_under_test([\"example.com\", \"status\"]) == 0\n\n    assert out.getvalue() == (\n        \"version: Java 1.8-pre1 (protocol 44)\\n\"\n        \"motd: \\x1b[0mA Minecraft Server\\x1b[0m\\n\"\n        \"players: 0/20\\n\"\n        \"ping: 0.00 ms\\n\"\n    )  # fmt: skip\n    assert err.getvalue() == \"\"\n\n\ndef test_status_bedrock(mock_network_requests):\n    with patch_stdout_stderr() as (out, err):\n        assert main_under_test([\"example.com\", \"--bedrock\", \"status\"]) == 0\n\n    assert out.getvalue() == (\n        \"version: Bedrock 1.18.100500 (protocol 422)\\n\"\n        \"motd: \\x1b[0m\\x1b[0m\\x1b[0m\\x1b[38;2;170;0;0mG\\x1b[0m\\x1b[0m\\x1b[38;2;255;170;0ma\\x1b[0m\\x1b[0m\\x1b[38;2;255;255;85m\"\n        \"y\\x1b[0m\\x1b[0m\\x1b[38;2;0;170;0mB\\x1b[0m\\x1b[0m\\x1b[38;2;0;0;170mo\\x1b[0m\\x1b[0m\\x1b[38;2;85;85;255mw\\x1b[0m\\x1b[0m\"\n        \"\\x1b[38;2;255;85;255ms\\x1b[0m\\x1b[0m\\x1b[38;2;170;0;0me\\x1b[0m\\x1b[0m\\x1b[38;2;255;170;0mr\\x1b[0m\\n\"\n        \"players: 1/69\\n\"\n        \"ping: 123.00 ms\\n\"\n    )\n    assert err.getvalue() == \"\"\n\n\ndef test_status_legacy(mock_network_requests):\n    with patch_stdout_stderr() as (out, err):\n        assert main_under_test([\"example.com\", \"--legacy\", \"status\"]) == 0\n\n    assert out.getvalue() == (\n        \"version: Java (pre-1.7) 1.4.2 (protocol 47)\\nmotd: \\x1b[0mA Minecraft Server\\x1b[0m\\nplayers: 0/20\\nping: 123.00 ms\\n\"\n    )\n    assert err.getvalue() == \"\"\n\n\ndef test_status_offline(mock_network_requests):\n    with patch_stdout_stderr() as (out, err), patch(\"mcstatus.server.JavaServer.status\", side_effect=TimeoutError):\n        assert main_under_test([\"example.com\", \"status\"]) == 1\n\n    assert out.getvalue() == \"\"\n    assert err.getvalue() == \"Error: TimeoutError()\\n\"\n\n\ndef test_query(mock_network_requests):\n    with patch_stdout_stderr() as (out, err):\n        assert main_under_test([\"example.com\", \"query\"]) == 0\n\n    assert out.getvalue() == (\n        \"host: 192.168.56.1:9999\\n\"\n        \"software: Java 1.8 vanilla\\n\"\n        \"motd: \\x1b[0mA Minecraft Server\\x1b[0m\\n\"\n        \"plugins: []\\n\"\n        \"players: 3/20 ['Dinnerbone', 'Djinnibone', 'Steve']\\n\"\n    )\n    assert err.getvalue() == \"\"\n\n\ndef test_query_offline(mock_network_requests):\n    with patch_stdout_stderr() as (out, err), patch(\"mcstatus.server.JavaServer.query\", side_effect=socket.timeout):\n        assert main_under_test([\"example.com\", \"query\"]) != 0\n\n    assert out.getvalue() == \"\"\n    assert err.getvalue() == QUERY_FAIL_WARNING + \"\\n\"\n\n\ndef test_query_on_bedrock(mock_network_requests):\n    with patch_stdout_stderr() as (out, err):\n        assert main_under_test([\"example.com\", \"--bedrock\", \"query\"]) != 0\n\n    assert out.getvalue() == \"\"\n    assert err.getvalue() == \"The 'query' protocol is only supported by Java servers.\\n\"\n\n\ndef test_json(mock_network_requests):\n    with patch_stdout_stderr() as (out, err):\n        assert main_under_test([\"example.com\", \"json\"]) == 0\n\n    data = json.loads(out.getvalue())\n    assert data == {\n        \"online\": True,\n        \"kind\": \"Java\",\n        \"status\": {\n            \"players\": {\"online\": 0, \"max\": 20, \"sample\": None},\n            \"version\": {\"name\": \"1.8-pre1\", \"protocol\": 44},\n            \"motd\": \"A Minecraft Server\",\n            \"latency\": 0,\n            \"raw\": {\n                \"players\": {\"max\": 20, \"online\": 0},\n                \"version\": {\"name\": \"1.8-pre1\", \"protocol\": 44},\n                \"description\": \"A Minecraft Server\",\n                \"enforcesSecureChat\": True,\n                \"favicon\": \"data:image/png;base64,foo\",\n            },\n            \"enforces_secure_chat\": True,\n            \"icon\": \"data:image/png;base64,foo\",\n            \"forge_data\": None,\n        },\n        \"query\": {\n            \"ip\": \"192.168.56.1\",\n            \"port\": 9999,\n            \"map_name\": \"world\",\n            \"motd\": \"A Minecraft Server\",\n            \"game_id\": \"GAME ID\",\n            \"game_type\": \"GAME TYPE\",\n            \"players\": {\n                \"list\": [\n                    \"Dinnerbone\",\n                    \"Djinnibone\",\n                    \"Steve\",\n                ],\n                \"max\": 20,\n                \"online\": 3,\n            },\n            \"software\": {\n                \"brand\": \"vanilla\",\n                \"plugins\": [],\n                \"version\": \"1.8\",\n            },\n            \"raw\": {\n                \"hostname\": \"A Minecraft Server\",\n                \"gametype\": \"GAME TYPE\",\n                \"game_id\": \"GAME ID\",\n                \"version\": \"1.8\",\n                \"plugins\": \"\",\n                \"map\": \"world\",\n                \"numplayers\": \"3\",\n                \"maxplayers\": \"20\",\n                \"hostport\": \"9999\",\n                \"hostip\": \"192.168.56.1\",\n            },\n        },\n    }\n    assert err.getvalue() == \"\"\n\n\ndef test_ping(mock_network_requests):\n    with patch_stdout_stderr() as (out, err):\n        assert main_under_test([\"example.com\", \"ping\"]) == 0\n\n    assert float(out.getvalue()) == 0\n    assert err.getvalue() == \"\"\n\n\ndef test_ping_bedrock(mock_network_requests):\n    with patch_stdout_stderr() as (out, err):\n        assert main_under_test([\"example.com\", \"--bedrock\", \"ping\"]) == 0\n\n    assert float(out.getvalue()) == 123\n    assert err.getvalue() == \"\"\n\n\ndef test_ping_legacy(mock_network_requests):\n    with patch_stdout_stderr() as (out, err):\n        assert main_under_test([\"example.com\", \"--legacy\", \"ping\"]) == 0\n\n    assert float(out.getvalue()) == 123\n    assert err.getvalue() == \"\"\n\n\ndef test_ping_server_doesnt_support(mock_network_requests):\n    with patch_stdout_stderr() as (out, err), patch(\"mcstatus.server.JavaServer.ping\", side_effect=TimeoutError(\"timeout\")):\n        assert main_under_test([\"example.com\", \"ping\"]) == 0\n\n    assert float(out.getvalue()) == 0\n    assert err.getvalue() == PING_PACKET_FAIL_WARNING.format(address=\"example.com:25565\", ping_exc=\"timeout\") + \"\\n\"\n"
  },
  {
    "path": "tests/test_compat.py",
    "content": "\"\"\"Tests for compatibility shims and build-time packaging behavior.\"\"\"\n\nimport importlib\nimport os\nimport shutil\nimport sys\nimport tarfile\nimport zipfile\nfrom collections.abc import Iterator\nfrom contextlib import contextmanager\nfrom pathlib import Path\nfrom typing import Literal\n\nimport pytest\nfrom hatchling.build import build_sdist, build_wheel\n\nfrom tests.helpers import patch_project_version\n\n\n@contextmanager\ndef _chdir(path: Path) -> Iterator[None]:\n    \"\"\"Temporarily change the working directory (Python 3.10 compatibility equivalent of ``contextlib.chdir``).\"\"\"\n    original = Path.cwd()\n    os.chdir(path)\n    try:\n        yield\n    finally:\n        os.chdir(original)\n\n\ndef _extractall_compat(tar: tarfile.TarFile, destination: Path) -> None:\n    \"\"\"Extract a tar archive in a way that works across Python versions and platforms.\n\n    This is a helper utility that avoids a deprecation warning from tarfile stdlib.\n\n    Python 3.14 deprecates ``TarFile.extractall`` without a filter, but Python 3.10 on\n    Windows does not accept the ``filter`` keyword. Use the secure filter when available,\n    and fall back to the legacy call only when the runtime rejects the keyword argument.\n    \"\"\"\n    try:\n        tar.extractall(destination, filter=\"data\")\n    except TypeError as exc:\n        if \"unexpected keyword argument 'filter'\" not in str(exc):\n            raise\n        # This call is unsafe for malicious archives, due to path escapes (like files\n        # with ../foo getting placed outside of our destination) but we know what we\n        # built and this is only used within unit-tests, so it's not really important\n        # to be strict about handling this.\n        tar.extractall(destination)  # noqa: S202\n\n\n@pytest.mark.parametrize(\"raises\", [False, True])\n@pytest.mark.parametrize(\n    (\"module\", \"msg_pattern\"),\n    [\n        (\"mcstatus._compat.forge_data\", r\"use mcstatus\\.responses\\.forge instead\"),\n        (\"mcstatus._compat.motd_transformers\", r\"MOTD Transformers are no longer a part of mcstatus public API\"),\n        (\"mcstatus._compat.status_response\", r\"use mcstatus\\.responses instead\"),\n    ],\n)\ndef test_deprecated_import_path(raises: bool, module: str, msg_pattern: str):\n    \"\"\"Test that the compatibility shims emit deprecation warnings at import time.\n\n    Note that this does NOT test the actual inclusion of the compatibility modules into\n    the source tree at build time. This test intentionally only uses the _compat imports,\n    as the shim files are only included on build time, which means testing those directly\n    would fail.\n    \"\"\"\n    # importlib.import_module caches module, if it didn't raise\n    sys.modules.pop(module, None)\n\n    context_manager = (\n        pytest.raises(DeprecationWarning, match=msg_pattern) if raises else pytest.deprecated_call(match=msg_pattern)\n    )\n    with patch_project_version(\"100.0.0\" if raises else \"0.0.0\"), context_manager:\n        importlib.import_module(module)\n\n\n@pytest.fixture(scope=\"session\")\ndef sdist_path(tmp_path_factory: pytest.TempPathFactory) -> Path:\n    \"\"\"Build an sdist once and return the path of the temporary directory where it exists.\"\"\"\n    source_root = Path(__file__).resolve().parent.parent\n\n    tmp_dir = tmp_path_factory.mktemp(\"build\")\n\n    tmp_path = Path(tmp_dir)\n    build_root = tmp_path / \"mcstatus\"\n    shutil.copytree(\n        source_root,\n        build_root,\n        ignore=shutil.ignore_patterns(\n            \".git\",\n            \".venv\",\n            \"__pycache__\",\n            \".pytest_cache\",\n            \"dist\",\n            \"build\",\n            \"_build\",\n            \".ruff_cache\",\n        ),\n    )\n    dist_dir = tmp_path / \"dist\"\n    dist_dir.mkdir()\n\n    # Build from a clean temp copy so we validate the sdist contents.\n    with _chdir(build_root):\n        sdist_name = build_sdist(str(dist_dir))\n\n    sdist_path = dist_dir / sdist_name\n    return sdist_path\n\n\n@pytest.fixture(scope=\"session\")\ndef sdist_member_names(sdist_path: Path) -> set[str]:\n    \"\"\"Build an sdist once and return all archive member names.\"\"\"\n    with tarfile.open(sdist_path, \"r:gz\") as tar:\n        tar_names = set(tar.getnames())\n        return tar_names\n\n\n@pytest.fixture(scope=\"session\")\ndef wheel_member_names(sdist_path: Path, tmp_path_factory: pytest.TempPathFactory) -> set[str]:\n    \"\"\"Build a wheel once and return all archive member names.\"\"\"\n    tmp_path = tmp_path_factory.mktemp(\"wheel-build\")\n\n    # Extract the sdist files first\n    sdist_extract_root = tmp_path / \"sdist\"\n    sdist_extract_root.mkdir()\n    with tarfile.open(sdist_path, \"r:gz\") as tar:\n        _extractall_compat(tar, sdist_extract_root)\n\n    # Get the first (and only) subdir inside of the sdist extract directory.\n    # This will contain the sdist files from this specific build (e.g. mcstatus-0.0.0).\n    sdist_root = next(path for path in sdist_extract_root.iterdir() if path.is_dir())\n\n    wheel_build_dir = tmp_path / \"dist\"\n    wheel_build_dir.mkdir()\n\n    # Build the wheel from the sdist content to ensure compat shims persist.\n    with _chdir(sdist_root):\n        wheel_name = build_wheel(str(wheel_build_dir))\n\n    wheel_path = wheel_build_dir / wheel_name\n    with zipfile.ZipFile(wheel_path) as wheel:\n        wheel_names = set(wheel.namelist())\n        return wheel_names\n\n\n@pytest.mark.parametrize(\"member_names_from\", [\"sdist\", \"wheel\"])\n@pytest.mark.parametrize(\n    \"expected_path\",\n    [\n        \"mcstatus/status_response.py\",\n        \"mcstatus/forge_data.py\",\n        \"mcstatus/motd/transformers.py\",\n    ],\n)\ndef test_includes_compat_shims(\n    sdist_member_names: set[str],\n    wheel_member_names: set[str],\n    member_names_from: Literal[\"sdist\", \"wheel\"],\n    expected_path: str,\n) -> None:\n    \"\"\"Assert the built wheel and sdist both bundle compatibility shims into their legacy paths.\"\"\"\n    member_names = sdist_member_names if member_names_from == \"sdist\" else wheel_member_names\n    assert any(name.endswith(expected_path) for name in member_names)\n"
  },
  {
    "path": "tests/test_server.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nfrom typing import SupportsIndex, TYPE_CHECKING, TypeAlias\nfrom unittest.mock import call, patch\n\nimport pytest\nimport pytest_asyncio\n\nfrom mcstatus._net.address import Address\nfrom mcstatus._protocol.connection import BaseAsyncReadSyncWriteConnection, Connection\nfrom mcstatus.server import BedrockServer, JavaServer, LegacyServer\n\nif TYPE_CHECKING:\n    from collections.abc import Iterable\n\nBytesConvertable: TypeAlias = \"SupportsIndex | Iterable[SupportsIndex]\"\n\n\nclass AsyncConnection(BaseAsyncReadSyncWriteConnection):\n    def __init__(self) -> None:\n        self.sent = bytearray()\n        self.received = bytearray()\n\n    async def read(self, length: int) -> bytearray:\n        \"\"\"Return :attr:`.received` up to length bytes, then cut received up to that point.\"\"\"\n        if len(self.received) < length:\n            raise OSError(f\"Not enough data to read! {len(self.received)} < {length}\")\n\n        result = self.received[:length]\n        self.received = self.received[length:]\n        return result\n\n    def write(self, data: Connection | str | bytearray | bytes) -> None:\n        \"\"\"Extend :attr:`.sent` from ``data``.\"\"\"\n        if isinstance(data, Connection):\n            data = data.flush()\n        if isinstance(data, str):\n            data = bytearray(data, \"utf-8\")\n        self.sent.extend(data)\n\n    def receive(self, data: BytesConvertable | bytearray) -> None:\n        \"\"\"Extend :attr:`.received` with ``data``.\"\"\"\n        if not isinstance(data, bytearray):\n            data = bytearray(data)\n        self.received.extend(data)\n\n    def remaining(self) -> int:\n        \"\"\"Return length of :attr:`.received`.\"\"\"\n        return len(self.received)\n\n    def flush(self) -> bytearray:\n        \"\"\"Return :attr:`.sent`, also clears :attr:`.sent`.\"\"\"\n        result, self.sent = self.sent, bytearray()\n        return result\n\n\nclass MockProtocolFactory(asyncio.Protocol):\n    transport: asyncio.Transport\n\n    def __init__(self, data_expected_to_receive, data_to_respond_with):\n        self.data_expected_to_receive = data_expected_to_receive\n        self.data_to_respond_with = data_to_respond_with\n\n    def connection_made(self, transport: asyncio.Transport):  # pyright: ignore[reportIncompatibleMethodOverride]\n        print(\"connection_made\")\n        self.transport = transport\n\n    def connection_lost(self, exc):\n        print(\"connection_lost\")\n        self.transport.close()\n\n    def data_received(self, data):\n        assert self.data_expected_to_receive in data\n        self.transport.write(self.data_to_respond_with)\n\n    def eof_received(self):\n        print(\"eof_received\")\n\n    def pause_writing(self):\n        print(\"pause_writing\")\n\n    def resume_writing(self):\n        print(\"resume_writing\")\n\n\n@pytest_asyncio.fixture()\nasync def create_mock_packet_server():\n    event_loop = asyncio.get_running_loop()\n    servers = []\n\n    async def create_server(port, data_expected_to_receive, data_to_respond_with):\n        server = await event_loop.create_server(\n            lambda: MockProtocolFactory(data_expected_to_receive, data_to_respond_with),\n            host=\"localhost\",\n            port=port,\n        )\n        servers.append(server)\n        return server\n\n    yield create_server\n\n    for server in servers:\n        server.close()\n        await server.wait_closed()\n\n\nclass TestBedrockServer:\n    def setup_method(self):\n        self.server = BedrockServer(\"localhost\")\n\n    def test_default_port(self):\n        assert self.server.address.port == 19132\n\n    def test_lookup_constructor(self):\n        s = BedrockServer.lookup(\"example.org\")\n        assert s.address.host == \"example.org\"\n        assert s.address.port == 19132\n\n\nclass TestAsyncJavaServer:\n    @pytest.mark.asyncio\n    async def test_async_ping(self, unused_tcp_port, create_mock_packet_server):\n        await create_mock_packet_server(\n            port=unused_tcp_port,\n            data_expected_to_receive=bytearray.fromhex(\"09010000000001C54246\"),\n            data_to_respond_with=bytearray.fromhex(\"0F002F096C6F63616C686F737463DD0109010000000001C54246\"),\n        )\n        minecraft_server = JavaServer(\"localhost\", port=unused_tcp_port)\n\n        latency = await minecraft_server.async_ping(ping_token=29704774, version=47)\n        assert latency >= 0\n\n    @pytest.mark.asyncio\n    async def test_async_lookup_constructor(self):\n        s = await JavaServer.async_lookup(\"example.org:3333\")\n        assert s.address.host == \"example.org\"\n        assert s.address.port == 3333\n\n\ndef test_java_server_with_query_port():\n    with patch(\"mcstatus.server.JavaServer._retry_query\") as patched_query_func:\n        server = JavaServer(\"localhost\", query_port=12345)\n        server.query()\n        assert server.query_port == 12345\n        assert patched_query_func.call_args == call(Address(\"127.0.0.1\", port=12345), tries=3)\n\n\n@pytest.mark.asyncio\nasync def test_java_server_with_query_port_async():\n    with patch(\"mcstatus.server.JavaServer._retry_async_query\") as patched_query_func:\n        server = JavaServer(\"localhost\", query_port=12345)\n        await server.async_query()\n        assert server.query_port == 12345\n        assert patched_query_func.call_args == call(Address(\"127.0.0.1\", port=12345), tries=3)\n\n\nclass TestJavaServer:\n    def setup_method(self):\n        self.socket = Connection()\n        self.server = JavaServer(\"localhost\")\n\n    def test_default_port(self):\n        assert self.server.address.port == 25565\n\n    def test_ping(self):\n        self.socket.receive(bytearray.fromhex(\"09010000000001C54246\"))\n\n        with patch(\"mcstatus.server.TCPSocketConnection\") as connection:\n            connection.return_value.__enter__.return_value = self.socket\n            latency = self.server.ping(ping_token=29704774, version=47)\n\n        assert self.socket.flush() == bytearray.fromhex(\"0F002F096C6F63616C686F737463DD0109010000000001C54246\")\n        assert self.socket.remaining() == 0, \"Data is pending to be read, but should be empty\"\n        assert latency >= 0\n\n    def test_ping_retry(self):\n        # Use a blank mock for the connection, we don't want to actually create any connections\n        with patch(\"mcstatus.server.TCPSocketConnection\"), patch(\"mcstatus.server.JavaClient\") as java_client:\n            java_client.side_effect = [RuntimeError, RuntimeError, RuntimeError]\n            with pytest.raises(RuntimeError, match=r\"^$\"):\n                self.server.ping()\n            assert java_client.call_count == 3\n\n    def test_status(self):\n        self.socket.receive(\n            bytearray.fromhex(\n                \"6D006B7B226465736372697074696F6E223A2241204D696E65637261667420536572766572222C22706C6179657273223A7B2\"\n                \"26D6178223A32302C226F6E6C696E65223A307D2C2276657273696F6E223A7B226E616D65223A22312E38222C2270726F746F\"\n                \"636F6C223A34377D7D\"\n            )\n        )\n\n        with patch(\"mcstatus.server.TCPSocketConnection\") as connection:\n            connection.return_value.__enter__.return_value = self.socket\n            info = self.server.status(version=47)\n\n        assert self.socket.flush() == bytearray.fromhex(\"0F002F096C6F63616C686F737463DD010100\")\n        assert self.socket.remaining() == 0, \"Data is pending to be read, but should be empty\"\n        assert info.raw == {\n            \"description\": \"A Minecraft Server\",\n            \"players\": {\"max\": 20, \"online\": 0},\n            \"version\": {\"name\": \"1.8\", \"protocol\": 47},\n        }\n        assert info.latency >= 0\n\n    def test_status_retry(self):\n        # Use a blank mock for the connection, we don't want to actually create any connections\n        with patch(\"mcstatus.server.TCPSocketConnection\"), patch(\"mcstatus.server.JavaClient\") as java_client:\n            java_client.side_effect = [RuntimeError, RuntimeError, RuntimeError]\n            with pytest.raises(RuntimeError, match=r\"^$\"):\n                self.server.status()\n            assert java_client.call_count == 3\n\n    def test_query(self):\n        self.socket.receive(bytearray.fromhex(\"090000000035373033353037373800\"))\n        self.socket.receive(\n            bytearray.fromhex(\n                \"00000000000000000000000000000000686f73746e616d650041204d696e656372616674205365727665720067616d6574797\"\n                \"06500534d500067616d655f6964004d494e4543524146540076657273696f6e00312e3800706c7567696e7300006d61700077\"\n                \"6f726c64006e756d706c61796572730033006d6178706c617965727300323000686f7374706f727400323535363500686f737\"\n                \"46970003139322e3136382e35362e31000001706c617965725f000044696e6e6572626f6e6500446a696e6e69626f6e650053\"\n                \"746576650000\"\n            )\n        )\n\n        with patch(\"mcstatus._protocol.connection.Connection.remaining\") as mock_remaining:\n            mock_remaining.side_effect = [15, 208]\n\n            with (\n                patch(\"mcstatus.server.UDPSocketConnection\") as connection,\n                patch.object(self.server.address, \"resolve_ip\") as resolve_ip,\n            ):\n                connection.return_value.__enter__.return_value = self.socket\n                resolve_ip.return_value = \"127.0.0.1\"\n                info = self.server.query()\n\n            conn_bytes = self.socket.flush()\n            assert conn_bytes[:3] == bytearray.fromhex(\"FEFD09\")\n            assert info.raw == {\n                \"hostname\": \"A Minecraft Server\",\n                \"gametype\": \"SMP\",\n                \"game_id\": \"MINECRAFT\",\n                \"version\": \"1.8\",\n                \"plugins\": \"\",\n                \"map\": \"world\",\n                \"numplayers\": \"3\",\n                \"maxplayers\": \"20\",\n                \"hostport\": \"25565\",\n                \"hostip\": \"192.168.56.1\",\n            }\n\n    def test_query_retry(self):\n        # Use a blank mock for the connection, we don't want to actually create any connections\n        with patch(\"mcstatus.server.UDPSocketConnection\"), patch(\"mcstatus.server.QueryClient\") as query_client:\n            query_client.side_effect = [RuntimeError, RuntimeError, RuntimeError]\n            with pytest.raises(RuntimeError, match=r\"^$\"), patch.object(self.server.address, \"resolve_ip\") as resolve_ip:  # noqa: PT012\n                resolve_ip.return_value = \"127.0.0.1\"\n                self.server.query()\n            assert query_client.call_count == 3\n\n    def test_lookup_constructor(self):\n        s = JavaServer.lookup(\"example.org:4444\")\n        assert s.address.host == \"example.org\"\n        assert s.address.port == 4444\n\n\nclass TestLegacyServer:\n    def setup_method(self):\n        self.socket = Connection()\n        self.server = LegacyServer(\"localhost\")\n\n    def test_default_port(self):\n        assert self.server.address.port == 25565\n\n    def test_lookup_constructor(self):\n        s = LegacyServer.lookup(\"example.org:4444\")\n        assert s.address.host == \"example.org\"\n        assert s.address.port == 4444\n\n    def test_status(self):\n        self.socket.receive(\n            bytearray.fromhex(\n                \"ff002300a70031000000340037000000\"\n                \"31002e0034002e003200000041002000\"\n                \"4d0069006e0065006300720061006600\"\n                \"74002000530065007200760065007200\"\n                \"000030000000320030\"\n            )\n        )\n\n        with patch(\"mcstatus.server.TCPSocketConnection\") as connection:\n            connection.return_value.__enter__.return_value = self.socket\n            info = self.server.status()\n\n        assert self.socket.flush() == bytearray.fromhex(\"fe01fa\")\n        assert self.socket.remaining() == 0, \"Data is pending to be read, but should be empty\"\n        assert info.as_dict() == {\n            \"latency\": info.latency,\n            \"motd\": \"A Minecraft Server\",\n            \"players\": {\"max\": 20, \"online\": 0},\n            \"version\": {\"name\": \"1.4.2\", \"protocol\": 47},\n        }\n        assert info.latency >= 0\n\n\nclass TestAsyncLegacyServer:\n    def setup_method(self):\n        self.socket = AsyncConnection()\n        self.server = LegacyServer(\"localhost\")\n\n    @pytest.mark.asyncio\n    async def test_async_lookup_constructor(self):\n        s = await LegacyServer.async_lookup(\"example.org:3333\")\n        assert s.address.host == \"example.org\"\n        assert s.address.port == 3333\n\n    @pytest.mark.asyncio\n    async def test_async_status(self):\n        self.socket.receive(\n            bytearray.fromhex(\n                \"ff002300a70031000000340037000000\"\n                \"31002e0034002e003200000041002000\"\n                \"4d0069006e0065006300720061006600\"\n                \"74002000530065007200760065007200\"\n                \"000030000000320030\"\n            )\n        )\n\n        with patch(\"mcstatus.server.TCPAsyncSocketConnection\") as connection:\n            connection.return_value.__aenter__.return_value = self.socket\n            info = await self.server.async_status()\n\n        assert self.socket.flush() == bytearray.fromhex(\"fe01fa\")\n        assert self.socket.remaining() == 0, \"Data is pending to be read, but should be empty\"\n        assert info.as_dict() == {\n            \"latency\": info.latency,\n            \"motd\": \"A Minecraft Server\",\n            \"players\": {\"max\": 20, \"online\": 0},\n            \"version\": {\"name\": \"1.4.2\", \"protocol\": 47},\n        }\n        assert info.latency >= 0\n"
  },
  {
    "path": "tests/utils/__init__.py",
    "content": ""
  },
  {
    "path": "tests/utils/test_deprecation.py",
    "content": "from __future__ import annotations\n\nimport re\nimport warnings\n\nimport pytest\n\nfrom mcstatus._utils.deprecation import _get_project_version, deprecated, deprecation_warn\nfrom tests.helpers import patch_project_version\n\nLIB_NAME = \"mcstatus\"\n\n\ndef test_invalid_lib_version():\n    with (\n        patch_project_version(\"foo bar\"),\n        pytest.warns(match=f\"^Failed to parse {LIB_NAME} project version \\\\(foo bar\\\\), assuming v0\\\\.0\\\\.0$\"),\n    ):\n        _get_project_version()\n\n\ndef test_epoch_in_lib_version():\n    with (\n        patch_project_version(\"2!1.2.3\"),\n        pytest.warns(\n            match=f\"^Failed to parse {LIB_NAME} project version, assuming v0\\\\.0\\\\.0$\",\n        ),\n    ):\n        _get_project_version()\n\n\n@pytest.mark.parametrize(\"removal_version\", [\"0.9.0\", (0, 9, 0)])\ndef test_deprecation_warn_produces_error(removal_version: str | tuple[int, int, int]):\n    \"\"\"Test deprecation_warn with older removal_version than current version produces exception.\"\"\"\n    with (\n        patch_project_version(\"1.0.0\"),\n        pytest.raises(\n            DeprecationWarning,\n            match=r\"^test is passed its removal version \\(0\\.9\\.0\\)\\.$\",\n        ),\n    ):\n        deprecation_warn(obj_name=\"test\", removal_version=removal_version)\n\n\n@pytest.mark.parametrize(\"removal_version\", [\"1.0.1\", (1, 0, 1)])\ndef test_deprecation_warn_produces_warning(removal_version: str | tuple[int, int, int]):\n    \"\"\"Test deprecation_warn with newer removal_version than current version produces warning.\"\"\"\n    with (\n        patch_project_version(\"1.0.0\"),\n        pytest.deprecated_call(\n            match=r\"^test is deprecated and scheduled for removal in 1\\.0\\.1\\.$\",\n        ),\n    ):\n        deprecation_warn(obj_name=\"test\", removal_version=removal_version)\n\n\ndef test_deprecation_invalid_removal_version():\n    \"\"\"Test deprecation_warn with invalid removal_version.\"\"\"\n    pattern = re.escape(r\"(\\d+)\\.(\\d+)\\.(\\d+)\")\n    with (\n        patch_project_version(\"1.0.0\"),\n        pytest.raises(\n            ValueError,\n            match=f\"^removal_version must follow regex pattern of: {pattern}$\",\n        ),\n    ):\n        deprecation_warn(obj_name=\"test\", removal_version=\"foo!\")\n\n\ndef test_deprecation_warn_unknown_version():\n    \"\"\"Test deprecation_warn with unknown project version.\n\n    This could occur if the project wasn't installed as a package. (e.g. when running directly from\n    source, like via a git submodule.)\n    \"\"\"\n    with (\n        patch_project_version(None),\n        pytest.warns(match=f\"Failed to get {LIB_NAME} project version\", expected_warning=RuntimeWarning),\n        pytest.deprecated_call(match=r\"^test is deprecated and scheduled for removal in 1\\.0\\.0\\.$\"),\n    ):\n        deprecation_warn(obj_name=\"test\", removal_version=\"1.0.0\")\n\n\ndef test_deprecation_decorator_warn():\n    \"\"\"Check deprecated decorator triggers a deprecation warning.\"\"\"\n    with patch_project_version(\"1.0.0\"):\n\n        @deprecated(display_name=\"func\", removal_version=\"1.0.1\")\n        def func(x: object) -> object:\n            \"\"\"Return input value.\n\n            .. deprecated:: 0.0.1\n            \"\"\"\n            return x\n\n        with pytest.deprecated_call(match=r\"^func is deprecated and scheduled for removal in 1\\.0\\.1\\.$\"):\n            assert func(5) == 5\n\n\ndef test_deprecation_decorator_inferred_name():\n    \"\"\"Check deprecated decorator properly infers qualified name of decorated object shown in warning.\"\"\"\n    with patch_project_version(\"1.0.0\"):\n\n        @deprecated(removal_version=\"1.0.1\")\n        def func(x: object) -> object:\n            \"\"\"Return input value.\n\n            .. deprecated:: 0.0.1\n            \"\"\"\n            return x\n\n        qualname = r\"test_deprecation_decorator_inferred_name\\.<locals>\\.func\"\n        with pytest.deprecated_call(match=rf\"^{qualname} is deprecated and scheduled for removal in 1\\.0\\.1\\.$\"):\n            assert func(5) == 5\n\n\ndef test_deprecation_decorator_missing_docstring_directive():\n    \"\"\"Check deprecated decorator validates a docstring contains a deprecation directive.\"\"\"\n    with (\n        patch_project_version(\"1.0.0\"),\n        pytest.raises(\n            ValueError,\n            match=r\"^Deprecated object does not contain '\\.\\. deprecated::' sphinx directive in its docstring$\",\n        ),\n    ):\n\n        @deprecated(display_name=\"func\", removal_version=\"1.0.1\")\n        def func(x: object) -> object:\n            return x\n\n\ndef test_deprecation_decorator_no_docstring_check_opt_out():\n    \"\"\"Check deprecated decorator can skip docstring validation when requested.\"\"\"\n    with patch_project_version(\"1.0.0\"):\n\n        @deprecated(display_name=\"func\", removal_version=\"1.0.1\", no_docstring_check=True)\n        def func(x: object) -> object:\n            return x\n\n        with pytest.deprecated_call(match=r\"^func is deprecated and scheduled for removal in 1\\.0\\.1\\.$\"):\n            assert func(5) == 5\n\n\n@pytest.mark.parametrize(\n    (\"version\", \"expected\"),\n    [\n        (\"1.2.3\", (1, 2, 3)),\n        (\"0.0.1\", (0, 0, 1)),\n        (\"1.0.0\", (1, 0, 0)),\n        (\"10.20.30\", (10, 20, 30)),\n        (\"1.2.3rc1\", (1, 2, 3)),\n        (\"1.2.3-rc1\", (1, 2, 3)),\n        (\"1.2.3.post1\", (1, 2, 3)),\n        (\"1.2.3-1\", (1, 2, 3)),\n        (\"1.2.3.dev4\", (1, 2, 3)),\n        (\"1.2.3+local\", (1, 2, 3)),\n        (\"1.2.3rc1.post2.dev3+loc.1\", (1, 2, 3)),\n    ],\n)\ndef test_project_version_non_normalized_parsing(version: str, expected: tuple[int, int, int]):\n    \"\"\"Ensure PEP440 release versions get parsed out properly, with non-release components are ignored.\"\"\"\n    with patch_project_version(version), warnings.catch_warnings():\n        warnings.simplefilter(\"error\")  # raise warnings as errors (test there are no warnings)\n\n        assert _get_project_version() == expected\n\n\n@pytest.mark.parametrize(\n    (\"version\", \"expected\", \"warning\"),\n    [\n        (\n            \"1.2\",\n            (1, 2, 0),\n            f\"{LIB_NAME} version '1.2' has less than 3 release components; remaining components will become zeroes\",\n        ),\n        (\n            \"1.2.3.4\",\n            (1, 2, 3),\n            f\"{LIB_NAME} version '1.2.3.4' has more than 3 release components; extra components are ignored\",\n        ),\n    ],\n    ids=[\"1.2\", \"1.2.3.4\"],\n)\ndef test_project_version_normalizes_release_components(\n    version: str,\n    expected: tuple[int, int, int],\n    warning: str,\n):\n    \"\"\"Ensure release segments normalize to a 3-component version and warn.\"\"\"\n    with patch_project_version(version), pytest.warns(RuntimeWarning, match=rf\"^{re.escape(warning)}$\"):\n        assert _get_project_version() == expected\n"
  },
  {
    "path": "tests/utils/test_general.py",
    "content": "import pytest\n\nfrom mcstatus._utils.general import or_none\n\n\n@pytest.mark.parametrize(\n    (\"a\", \"b\", \"result\"),\n    [\n        (None, None, None),\n        (None, \"\", \"\"),\n        (\"\", None, \"\"),\n        (\"a\", \"b\", \"a\"),\n    ],\n)\ndef test_or_none(a, b, result):\n    assert or_none(a, b) == result\n\n\ndef test_or_none_many_arguments():\n    assert or_none(*([None] * 100 + [\"value\"])) == \"value\"\n"
  },
  {
    "path": "tests/utils/test_retry.py",
    "content": "import pytest\n\nfrom mcstatus._utils.retry import retry\nfrom tests.protocol.test_java_client_async import async_decorator\n\n\ndef test_sync_success():\n    x = -1\n\n    @retry(tries=2)\n    def func():\n        nonlocal x\n        x += 1\n        return 5 / x\n\n    y = func()\n    assert x == 1\n    assert y == 5\n\n\ndef test_sync_fail():\n    x = -1\n\n    @retry(tries=2)\n    def func():\n        nonlocal x\n        x += 1\n        if x == 0:\n            raise OSError(\"First error\")\n        if x == 1:\n            raise RuntimeError(\"Second error\")\n\n    # We should get the last exception on failure (not OSError)\n    with pytest.raises(RuntimeError, match=r\"^Second error$\"):\n        func()\n\n\ndef test_async_success():\n    x = -1\n\n    @retry(tries=2)\n    async def func():\n        nonlocal x\n        x += 1\n        return 5 / x\n\n    y = async_decorator(func)()\n    assert x == 1\n    assert y == 5\n\n\ndef test_async_fail():\n    x = -1\n\n    @retry(tries=2)\n    async def func():\n        nonlocal x\n        x += 1\n        if x == 0:\n            raise OSError(\"First error\")\n        if x == 1:\n            raise RuntimeError(\"Second error\")\n\n    # We should get the last exception on failure (not OSError)\n    with pytest.raises(RuntimeError, match=r\"^Second error$\"):\n        async_decorator(func)()\n"
  }
]