Moving to new organization

This commit is contained in:
Kevin Tindall
2022-02-14 18:30:54 -06:00
parent d790c35d22
commit 2df0accce3
35 changed files with 1 additions and 4530 deletions

15
.flake8
View File

@@ -1,15 +0,0 @@
[flake8]
max-line-length=127
extend-ignore=E203
application-import-names=mcstatus
ban-relative-imports=true
import-order-style=pycharm
exclude=.venv,.git,.cache
ignore=
ANN001, # TEMPORARY: parameter annotation (we still have a lot of these)
ANN201, # TEMPORARY: return type annotation (we still have a lot of these)
ANN002, # *args annotation
ANN003, # **kwargs annotation
ANN101, # self param annotation
ANN102, # cls param annotation
ANN204, # return type annotation for special methods

View File

@@ -1,64 +0,0 @@
name: Tox test
on: [pull_request, push]
env:
# Make sure pip caches dependencies and installs as user
PIP_NO_CACHE_DIR: false
PIP_USER: 1
# Make sure poetry won't use virtual environments
POETRY_VIRTUALENVS_CREATE: false
# Specify paths here, so we know exactly where things are for caching
PYTHONUSERBASE: ${{ github.workspace }}/.cache/py-user-base
POETRY_CACHE_DIR: ${{ github.workspace }}/.cache/py-user-base
TOXDIR: ${{ github.workspace }}/.tox
jobs:
tox-test:
runs-on: ${{ matrix.platform }}
strategy:
matrix:
platform: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.7", "3.8", "3.9", "3.10"]
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Python setup
id: python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
# Cache python dependencies so that unless we change them,
# we won't need to reinstall them with each workflow run.
# The key is a composite of multiple values, which when changed
# the cache won't be restored in order to make updating possible
- name: Python dependency caching
uses: actions/cache@v2
id: python_cache
with:
path: |
${{ env.PYTHONUSERBASE }}
${{ env.TOXDIR }}
key: "python-0-${{ runner.os }}-${{ env.PYTHONUSERBASE }}-\
${{ env.TOXDIR }}-${{ steps.python.outputs.python-version }}-\
${{ hashFiles('./pyproject.toml', './poetry.lock') }}"
# In case the dependencies weren't restored, install them
- name: Install dependencies using poetry
if: steps.python_cache.outputs.cache-hit != 'true'
run: |
pip install poetry
pip install tox
pip install tox-poetry
pip install tox-gh-actions
- name: Test with tox
run: python -m tox
env:
PIP_USER: 0 # We want tox to use it's environments, not user installs

View File

@@ -1,63 +0,0 @@
name: Validation
on: [pull_request, push]
env:
# Make sure pip caches dependencies and installs as user
PIP_NO_CACHE_DIR: false
PIP_USER: 1
# Make sure poetry won't use virtual environments
POETRY_VIRTUALENVS_CREATE: false
# Specify paths here, so we know what to cache
POETRY_CACHE_DIR: ${{ github.workspace }}/.cache/py-user-base
PYTHONUSERBASE: ${{ github.workspace }}/.cache/py-user-base
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Add custom PYTHONUSERBASE to PATH
run: echo '${{ env.PYTHONUSERBASE }}/bin/' >> $GITHUB_PATH
- name: Checkout repository
uses: actions/checkout@v2
- name: Python setup
id: python
uses: actions/setup-python@v2
with:
python-version: '3.9.5'
# Cache python dependencies so that unless we change them,
# we won't need to reinstall them with each workflow run.
# The key is a composite of multiple values, which when changed
# the cache won't be restored in order to make updating possible
- name: Python dependency caching
uses: actions/cache@v2
id: python_cache
with:
path: ${{ env.PYTHONUSERBASE }}
key: "python-0-${{ runner.os }}-${{ env.PYTHONUSERBASE }}-\
${{ steps.python.outputs.python-version }}-\
${{ hashFiles('./pyproject.toml', './poetry.lock') }}"
# In case the dependencies weren't restored, install them
- name: Install dependencies using poetry
if: steps.python_cache.outputs.cache-hit != 'true'
run: |
pip install poetry
poetry install
# Run the actual linting steps here:
- name: Run black formatter check
run: black --check .
- name: Run pyright type checker
run: pyright -v $PYTHONUSERBASE
- name: Run flake8 linter
run: flake8 .

108
.gitignore vendored
View File

@@ -1,108 +0,0 @@
# Created by http://www.gitignore.io
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
venv/
build/
develop-eggs/
dist/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage*
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
### PyCharm ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
## Directory-based project format
.idea/
/*.iml
# if you remove the above rule, at least ignore user-specific stuff:
# .idea/workspace.xml
# .idea/tasks.xml
# .idea/dictionaries
# and these sensitive or high-churn files:
# .idea/dataSources.ids
# .idea/dataSources.xml
# .idea/sqlDataSources.xml
# .idea/dynamic.xml
# and, if using gradle::
# .idea/gradle.xml
# .idea/libraries
## File-based project format
*.ipr
*.iws
## Additional for IntelliJ
out/
# generated by mpeltonen/sbt-idea plugin
.idea_modules/
# generated by JIRA plugin
atlassian-ide-plugin.xml
# generated by Crashlytics plugin (for Android Studio and Intellij)
com_crashlytics_export_strings.xml
### SublimeText ###
# workspace files are user-specific
*.sublime-workspace
# project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using SublimeText
# *.sublime-project
#sftp configuration file
sftp-config.json
### Visual Studio Code ###
.vscode

View File

@@ -1,4 +0,0 @@
3.9.6
3.8.10
3.7.9
3.6.8

View File

@@ -1,7 +0,0 @@
Setup:
```
pipx install poetry
pipx inject poetry poetry-dynamic-versioning
pipx install tox
pipx inject tox tox-poetry
```

202
LICENSE
View File

@@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

104
README.md
View File

@@ -1,103 +1 @@
![travis build status](https://img.shields.io/travis/Dinnerbone/mcstatus/master.svg)
[![current PyPI version](https://img.shields.io/pypi/v/mcstatus.svg)](https://pypi.org/project/mcstatus/)
![supported python versions](https://img.shields.io/pypi/pyversions/mcstatus.svg)
[![discord chat](https://img.shields.io/discord/936788458939224094.svg?logo=Discord)](https://discord.gg/C2wX7zduxC)
mcstatus
========
`mcstatus` provides an easy way to query Minecraft servers for any information they can expose.
It provides three modes of access (`query`, `status` and `ping`), the differences of which are listed below in usage.
Usage
-----
Java Edition
```python
from mcstatus import MinecraftServer
# If you know the host and port, you may skip this and use MinecraftServer("example.org", 1234)
server = MinecraftServer.lookup("example.org:1234")
# 'status' is supported by all Minecraft servers that are version 1.7 or higher.
status = server.status()
print(f"The server has {status.players.online} players and replied in {status.latency} ms")
# 'ping' is supported by all Minecraft servers that are version 1.7 or higher.
# It is included in a 'status' call, but is exposed separate if you do not require the additional info.
latency = server.ping()
print(f"The server replied in {latency} ms")
# 'query' has to be enabled in a servers' server.properties file.
# It may give more information than a ping, such as a full player list or mod information.
query = server.query()
print(f"The server has the following players online: {', '.join(query.players.names)}")
```
Bedrock Edition
```python
from mcstatus import MinecraftBedrockServer
# If you know the host and port, you may skip this and use MinecraftBedrockServer("example.org", 19132)
server = MinecraftBedrockServer.lookup("example.org:19132")
# 'status' is the only feature that is supported by Bedrock at this time.
# In this case status includes players_online, latency, motd, map, gamemode, and players_max. (ex: status.gamemode)
status = server.status()
print(f"The server has {status.players_online} players online and replied in {status.latency} ms")
```
Command Line Interface
```
$ mcstatus
Usage: mcstatus [OPTIONS] ADDRESS COMMAND [ARGS]...
mcstatus provides an easy way to query Minecraft servers for any
information they can expose. It provides three modes of access: query,
status, and ping.
Examples:
$ mcstatus example.org ping
21.120ms
$ mcstatus example.org:1234 ping
159.903ms
$ mcstatus example.org status
version: v1.8.8 (protocol 47)
description: "A Minecraft Server"
players: 1/20 ['Dinnerbone (61699b2e-d327-4a01-9f1e-0ea8c3f06bc6)']
$ mcstatus example.org query
host: 93.148.216.34:25565
software: v1.8.8 vanilla
plugins: []
motd: "A Minecraft Server"
players: 1/20 ['Dinnerbone (61699b2e-d327-4a01-9f1e-0ea8c3f06bc6)']
Options:
-h, --help Show this message and exit.
Commands:
json combination of several other commands with json formatting
ping prints server latency
query detailed server information
status basic server information
```
Installation
------------
mcstatus is available on pypi, and can be installed trivially with:
```bash
python3 -m pip install mcstatus
```
Alternatively, just clone this repo!
License
-------
mcstatus is licensed under Apache 2.0.
Moving to https://github.com/py-mine/mcstatus

View File

@@ -1 +0,0 @@
from mcstatus.server import MinecraftBedrockServer, MinecraftServer # noqa: F401

View File

@@ -1,99 +0,0 @@
from __future__ import annotations
import asyncio
import socket
import struct
from time import perf_counter
import asyncio_dgram
class BedrockServerStatus:
request_status_data = b"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x00\xfe\xfe\xfe\xfe\xfd\xfd\xfd\xfd\x124Vx"
def __init__(self, host: str, port: int = 19132, timeout: float = 3):
self.host = host
self.port = port
self.timeout = timeout
@staticmethod
def parse_response(data: bytes, latency: float) -> "BedrockStatusResponse":
data = data[1:]
name_length = struct.unpack(">H", data[32:34])[0]
decoded_data = data[34 : 34 + name_length].decode().split(";")
try:
map_ = decoded_data[7]
except IndexError:
map_ = None
try:
gamemode = decoded_data[8]
except IndexError:
gamemode = None
return BedrockStatusResponse(
protocol=decoded_data[2],
brand=decoded_data[0],
version=decoded_data[3],
latency=latency,
players_online=decoded_data[4],
players_max=decoded_data[5],
motd=decoded_data[1],
map_=map_,
gamemode=gamemode,
)
def read_status(self) -> BedrockStatusResponse:
start = perf_counter()
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(self.timeout)
s.sendto(self.request_status_data, (self.host, self.port))
data, _ = s.recvfrom(2048)
return self.parse_response(data, (perf_counter() - start))
async def read_status_async(self) -> BedrockStatusResponse:
start = perf_counter()
stream = None
try:
conn = asyncio_dgram.connect((self.host, self.port))
stream = await asyncio.wait_for(conn, timeout=self.timeout)
await asyncio.wait_for(stream.send(self.request_status_data), timeout=self.timeout)
data, _ = await asyncio.wait_for(stream.recv(), timeout=self.timeout)
finally:
if stream is not None:
stream.close()
return self.parse_response(data, (perf_counter() - start))
class BedrockStatusResponse:
class Version:
def __init__(self, protocol, brand, version):
self.protocol = protocol
self.brand = brand
self.version = version
def __init__(
self,
protocol,
brand,
version,
latency,
players_online,
players_max,
motd,
map_,
gamemode,
):
self.version = self.Version(protocol, brand, version)
self.latency = latency
self.players_online = players_online
self.players_max = players_max
self.motd = motd
self.map = map_
self.gamemode = gamemode

View File

@@ -1,267 +0,0 @@
from __future__ import annotations
import datetime
import json
import random
from typing import List, Optional, Union
from mcstatus.protocol.connection import Connection, TCPAsyncSocketConnection, TCPSocketConnection
STYLE_MAP = {
"bold": "l",
"italic": "o",
"underlined": "n",
"obfuscated": "k",
"color": {
"dark_red": "4",
"red": "c",
"gold": "6",
"yellow": "e",
"dark_green": "2",
"green": "a",
"aqua": "b",
"dark_aqua": "3",
"dark_blue": "1",
"blue": "9",
"light_purple": "d",
"dark_purple": "5",
"white": "f",
"gray": "7",
"dark_gray": "8",
"black": "0",
},
}
class ServerPinger:
def __init__(
self,
connection: TCPSocketConnection,
host: str = "",
port: int = 0,
version: int = 47,
ping_token=None,
):
if ping_token is None:
ping_token = random.randint(0, (1 << 63) - 1)
self.version = version
self.connection = connection
self.host = host
self.port = port
self.ping_token = ping_token
def handshake(self) -> None:
packet = Connection()
packet.write_varint(0)
packet.write_varint(self.version)
packet.write_utf(self.host)
packet.write_ushort(self.port)
packet.write_varint(1) # Intention to query status
self.connection.write_buffer(packet)
def read_status(self) -> "PingResponse":
request = Connection()
request.write_varint(0) # Request status
self.connection.write_buffer(request)
response = self.connection.read_buffer()
if response.read_varint() != 0:
raise IOError("Received invalid status response packet.")
try:
raw = json.loads(response.read_utf())
except ValueError:
raise IOError("Received invalid JSON")
try:
return PingResponse(raw)
except ValueError as e:
raise IOError(f"Received invalid status response: {e}")
def test_ping(self) -> float:
request = Connection()
request.write_varint(1) # Test ping
request.write_long(self.ping_token)
sent = datetime.datetime.now()
self.connection.write_buffer(request)
response = self.connection.read_buffer()
received = datetime.datetime.now()
if response.read_varint() != 1:
raise IOError("Received invalid ping response packet.")
received_token = response.read_long()
if received_token != self.ping_token:
raise IOError(
f"Received mangled ping response packet (expected token {self.ping_token}, received {received_token})"
)
delta = received - sent
return delta.total_seconds() * 1000
class AsyncServerPinger(ServerPinger):
def __init__(
self, connection: TCPAsyncSocketConnection, host: str = "", port: int = 0, version: int = 47, ping_token=None
):
# We do this to inform python about self.connection type (it's async)
super().__init__(connection, host=host, port=port, version=version, ping_token=ping_token) # type: ignore[arg-type]
self.connection: TCPAsyncSocketConnection
async def read_status(self) -> "PingResponse":
request = Connection()
request.write_varint(0) # Request status
self.connection.write_buffer(request)
response = await self.connection.read_buffer()
if response.read_varint() != 0:
raise IOError("Received invalid status response packet.")
try:
raw = json.loads(response.read_utf())
except ValueError:
raise IOError("Received invalid JSON")
try:
return PingResponse(raw)
except ValueError as e:
raise IOError(f"Received invalid status response: {e}")
async def test_ping(self) -> float:
request = Connection()
request.write_varint(1) # Test ping
request.write_long(self.ping_token)
sent = datetime.datetime.now()
self.connection.write_buffer(request)
response = await self.connection.read_buffer()
received = datetime.datetime.now()
if response.read_varint() != 1:
raise IOError("Received invalid ping response packet.")
received_token = response.read_long()
if received_token != self.ping_token:
raise IOError(
f"Received mangled ping response packet (expected token {self.ping_token}, received {received_token})"
)
delta = received - sent
return delta.total_seconds() * 1000
class PingResponse:
# THIS IS SO UNPYTHONIC
# it's staying just because the tests depend on this structure
class Players:
class Player:
name: str
id: str
def __init__(self, raw):
if not isinstance(raw, dict):
raise ValueError(f"Invalid player object (expected dict, found {type(raw)}")
if "name" not in raw:
raise ValueError("Invalid player object (no 'name' value)")
if not isinstance(raw["name"], str):
raise ValueError(f"Invalid player object (expected 'name' to be str, was {type(raw['name'])}")
self.name = raw["name"]
if "id" not in raw:
raise ValueError("Invalid player object (no 'id' value)")
if not isinstance(raw["id"], str):
raise ValueError(f"Invalid player object (expected 'id' to be str, was {type(raw['id'])}")
self.id = raw["id"]
online: int
max: int
sample: Optional[List["PingResponse.Players.Player"]]
def __init__(self, raw):
if not isinstance(raw, dict):
raise ValueError(f"Invalid players object (expected dict, found {type(raw)}")
if "online" not in raw:
raise ValueError("Invalid players object (no 'online' value)")
if not isinstance(raw["online"], int):
raise ValueError(f"Invalid players object (expected 'online' to be int, was {type(raw['online'])})")
self.online = raw["online"]
if "max" not in raw:
raise ValueError("Invalid players object (no 'max' value)")
if not isinstance(raw["max"], int):
raise ValueError(f"Invalid players object (expected 'max' to be int, was {type(raw['max'])}")
self.max = raw["max"]
if "sample" in raw:
if not isinstance(raw["sample"], list):
raise ValueError(f"Invalid players object (expected 'sample' to be list, was {type(raw['max'])})")
self.sample = [PingResponse.Players.Player(p) for p in raw["sample"]]
else:
self.sample = None
class Version:
name: str
protocol: int
def __init__(self, raw):
if not isinstance(raw, dict):
raise ValueError(f"Invalid version object (expected dict, found {type(raw)})")
if "name" not in raw:
raise ValueError("Invalid version object (no 'name' value)")
if not isinstance(raw["name"], str):
raise ValueError(f"Invalid version object (expected 'name' to be str, was {type(raw['name'])})")
self.name = raw["name"]
if "protocol" not in raw:
raise ValueError("Invalid version object (no 'protocol' value)")
if not isinstance(raw["protocol"], int):
raise ValueError(f"Invalid version object (expected 'protocol' to be int, was {type(raw['protocol'])})")
self.protocol = raw["protocol"]
players: Players
version: Version
description: str
favicon: Optional[str]
latency: float = 0
def __init__(self, raw):
self.raw = raw
if "players" not in raw:
raise ValueError("Invalid status object (no 'players' value)")
self.players = PingResponse.Players(raw["players"])
if "version" not in raw:
raise ValueError("Invalid status object (no 'version' value)")
self.version = PingResponse.Version(raw["version"])
if "description" not in raw:
raise ValueError("Invalid status object (no 'description' value)")
self.description = self._parse_description(raw["description"])
self.favicon = raw.get("favicon")
@staticmethod
def _parse_description(raw_description: Union[dict, list, str]) -> str:
if isinstance(raw_description, str):
return raw_description
if isinstance(raw_description, dict):
entries = raw_description.get("extra", [])
end = raw_description["text"]
else:
entries = raw_description
end = ""
description = ""
for entry in entries:
for style_key, style_val in STYLE_MAP.items():
if entry.get(style_key):
try:
if isinstance(style_val, dict):
style_val = style_val[entry[style_key]]
description += f"§{style_val}"
except KeyError:
pass # ignoring these key errors strips out html color codes
description += entry.get("text", "")
return description + end

View File

@@ -1,337 +0,0 @@
from __future__ import annotations
import asyncio
import socket
import struct
from abc import ABC, abstractmethod
from ctypes import c_int32 as signed_int32
from ctypes import c_uint32 as unsigned_int32
from ipaddress import ip_address
from typing import Iterable, Optional, SupportsBytes, TYPE_CHECKING, Tuple, Union
import asyncio_dgram
if TYPE_CHECKING:
from typing_extensions import SupportsIndex # Python 3.7 doesn't support this yet.
BytesConvertable = Union[SupportsIndex, Iterable[SupportsIndex]]
def ip_type(address: Union[int, str]) -> Optional[int]:
try:
return ip_address(address).version
except ValueError:
return None
class Connection:
def __init__(self):
self.sent = bytearray()
self.received = bytearray()
def read(self, length: int) -> bytearray:
result = self.received[:length]
self.received = self.received[length:]
return result
def write(self, data: Union["Connection", str, bytearray, bytes]) -> None:
if isinstance(data, Connection):
data = data.flush()
if isinstance(data, str):
data = bytearray(data, "utf-8")
self.sent.extend(data)
def receive(self, data: Union[BytesConvertable, bytearray]) -> None:
if not isinstance(data, bytearray):
data = bytearray(data)
self.received.extend(data)
def remaining(self) -> int:
return len(self.received)
def flush(self) -> bytearray:
result = self.sent
self.sent = bytearray()
return result
def _unpack(self, format: str, data: Union[BytesConvertable, SupportsBytes]) -> int:
return struct.unpack(">" + format, bytes(data))[0]
def _pack(self, format: str, data: int) -> bytes:
return struct.pack(">" + format, data)
def read_varint(self) -> int:
result = 0
for i in range(5):
part = self.read(1)[0]
result |= (part & 0x7F) << 7 * i
if not part & 0x80:
return signed_int32(result).value
raise IOError("Server sent a varint that was too big!")
def write_varint(self, value: int) -> None:
if value < -(2**31) or 2**31 - 1 < value:
raise ValueError("Minecraft varints must be in the range of [-2**31, 2**31 - 1].")
remaining = unsigned_int32(value).value
for _ in range(5):
if remaining & ~0x7F == 0:
self.write(struct.pack("!B", remaining))
return
self.write(struct.pack("!B", remaining & 0x7F | 0x80))
remaining >>= 7
def read_utf(self) -> str:
length = self.read_varint()
return self.read(length).decode("utf8")
def write_utf(self, value: str) -> None:
self.write_varint(len(value))
self.write(bytearray(value, "utf8"))
def read_ascii(self) -> str:
result = bytearray()
while len(result) == 0 or result[-1] != 0:
result.extend(self.read(1))
return result[:-1].decode("ISO-8859-1")
def write_ascii(self, value: str) -> None:
self.write(bytearray(value, "ISO-8859-1"))
self.write(bytearray.fromhex("00"))
def read_short(self) -> int:
return self._unpack("h", self.read(2))
def write_short(self, value: int) -> None:
self.write(self._pack("h", value))
def read_ushort(self) -> int:
return self._unpack("H", self.read(2))
def write_ushort(self, value: int) -> None:
self.write(self._pack("H", value))
def read_int(self) -> int:
return self._unpack("i", self.read(4))
def write_int(self, value: int) -> None:
self.write(self._pack("i", value))
def read_uint(self) -> int:
return self._unpack("I", self.read(4))
def write_uint(self, value: int) -> None:
self.write(self._pack("I", value))
def read_long(self) -> int:
return self._unpack("q", self.read(8))
def write_long(self, value: int) -> None:
self.write(self._pack("q", value))
def read_ulong(self) -> int:
return self._unpack("Q", self.read(8))
def write_ulong(self, value: int) -> None:
self.write(self._pack("Q", value))
def read_buffer(self) -> "Connection":
length = self.read_varint()
result = Connection()
result.receive(self.read(length))
return result
def write_buffer(self, buffer: "Connection") -> None:
data = buffer.flush()
self.write_varint(len(data))
self.write(data)
class AsyncReadConnection(Connection, ABC):
@abstractmethod
async def read(self, length: int) -> bytearray:
...
async def read_varint(self) -> int:
result = 0
for i in range(5):
part = (await self.read(1))[0]
result |= (part & 0x7F) << (7 * i)
if not part & 0x80:
return signed_int32(result).value
raise IOError("Server sent a varint that was too big!")
async def read_utf(self) -> str:
length = await self.read_varint()
return (await self.read(length)).decode("utf8")
async def read_ascii(self) -> str:
result = bytearray()
while len(result) == 0 or result[-1] != 0:
result.extend(await self.read(1))
return result[:-1].decode("ISO-8859-1")
async def read_short(self) -> int:
return self._unpack("h", await self.read(2))
async def read_ushort(self) -> int:
return self._unpack("H", await self.read(2))
async def read_int(self) -> int:
return self._unpack("i", await self.read(4))
async def read_uint(self) -> int:
return self._unpack("I", await self.read(4))
async def read_long(self) -> int:
return self._unpack("q", await self.read(8))
async def read_ulong(self) -> int:
return self._unpack("Q", await self.read(8))
async def read_buffer(self) -> Connection:
length = await self.read_varint()
result = Connection()
result.receive(await self.read(length))
return result
class TCPSocketConnection(Connection):
def __init__(self, addr: Tuple[str, int], timeout: float = 3):
Connection.__init__(self)
self.socket = socket.create_connection(addr, timeout=timeout)
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
def flush(self) -> bytearray:
raise NotImplementedError("TCPSocketConnection does not support flush()")
def receive(self, data: Union[BytesConvertable, SupportsBytes]) -> None:
raise NotImplementedError("TCPSocketConnection does not support receive()")
def remaining(self) -> int:
raise NotImplementedError("TCPSocketConnection does not support remaining()")
def read(self, length: int) -> bytearray:
result = bytearray()
while len(result) < length:
new = self.socket.recv(length - len(result))
if len(new) == 0:
raise IOError("Server did not respond with any information!")
result.extend(new)
return result
def write(self, data: Union[bytes, bytearray]) -> None:
self.socket.send(data)
def __del__(self):
try:
self.socket.close()
except Exception: # TODO: Check what this actually excepts
pass
class UDPSocketConnection(Connection):
def __init__(self, addr: Tuple[str, int], timeout: float = 3):
Connection.__init__(self)
self.addr = addr
self.socket = socket.socket(
socket.AF_INET if ip_type(addr[0]) == 4 else socket.AF_INET6,
socket.SOCK_DGRAM,
)
self.socket.settimeout(timeout)
def flush(self) -> bytearray:
raise NotImplementedError("UDPSocketConnection does not support flush()")
def receive(self, data: Union[BytesConvertable, SupportsBytes]) -> None:
raise NotImplementedError("UDPSocketConnection does not support receive()")
def remaining(self) -> int:
return 65535
def read(self, length: int) -> bytearray:
result = bytearray()
while len(result) == 0:
result.extend(self.socket.recvfrom(self.remaining())[0])
return result
def write(self, data: Union[Connection, bytes, bytearray]) -> None:
if isinstance(data, Connection):
data = bytearray(data.flush())
self.socket.sendto(data, self.addr)
def __del__(self):
try:
self.socket.close()
except Exception: # TODO: Check what this actually excepts
pass
class TCPAsyncSocketConnection(AsyncReadConnection):
# These will only be None until connect is called, ignore the None type assignment
reader: asyncio.StreamReader = None # type: ignore[assignment]
writer: asyncio.StreamWriter = None # type: ignore[assignment]
timeout: float = None # type: ignore[assignment]
def __init__(self):
super().__init__()
async def connect(self, addr: Tuple[str, int], timeout: float = 3):
self.timeout = timeout
conn = asyncio.open_connection(addr[0], addr[1])
self.reader, self.writer = await asyncio.wait_for(conn, timeout=self.timeout)
async def read(self, length: int) -> bytearray:
result = bytearray()
while len(result) < length:
new = await asyncio.wait_for(self.reader.read(length - len(result)), timeout=self.timeout)
if len(new) == 0:
raise IOError("Server did not respond with any information!")
result.extend(new)
return result
def write(self, data: Union[bytes, bytearray]) -> None:
self.writer.write(data)
def __del__(self):
try:
self.writer.close()
except Exception: # TODO: Check what this actually expects
pass
class UDPAsyncSocketConnection(AsyncReadConnection):
# These will only be None until connect is called, ignore the None type assignment
stream: asyncio_dgram.aio.DatagramClient = None # type: ignore[assignment]
timeout: float = None # type: ignore[assignment]
def __init__(self):
super().__init__()
async def connect(self, addr: Tuple[str, int], timeout: float = 3):
self.timeout = timeout
conn = asyncio_dgram.connect((addr[0], addr[1]))
self.stream = await asyncio.wait_for(conn, timeout=self.timeout)
def flush(self) -> bytearray:
raise NotImplementedError("UDPSocketConnection does not support flush()")
def receive(self, data: Union[SupportsBytes, BytesConvertable]) -> None:
raise NotImplementedError("UDPSocketConnection does not support receive()")
def remaining(self) -> int:
return 65535
async def read(self, length: int) -> bytes:
data, remote_addr = await asyncio.wait_for(self.stream.recv(), timeout=self.timeout)
return data
async def write(self, data: Union[Connection, bytes, bytearray]) -> None:
if isinstance(data, Connection):
data = bytearray(data.flush())
await self.stream.send(data)
def __del__(self):
try:
self.stream.close()
except Exception: # TODO: Check what this actually excepts
pass

View File

@@ -1,166 +0,0 @@
from __future__ import annotations
import random
import re
import struct
from typing import List, TYPE_CHECKING
from mcstatus.protocol.connection import Connection, UDPAsyncSocketConnection, UDPSocketConnection
if TYPE_CHECKING:
from typing_extensions import Self
class ServerQuerier:
MAGIC_PREFIX = bytearray.fromhex("FEFD")
PADDING = bytearray.fromhex("00000000")
PACKET_TYPE_CHALLENGE = 9
PACKET_TYPE_QUERY = 0
def __init__(self, connection: UDPSocketConnection):
self.connection = connection
self.challenge = 0
@staticmethod
def _generate_session_id() -> int:
# minecraft only supports lower 4 bits
return random.randint(0, 2**31) & 0x0F0F0F0F
def _create_packet(self) -> Connection:
packet = Connection()
packet.write(self.MAGIC_PREFIX)
packet.write(struct.pack("!B", self.PACKET_TYPE_QUERY))
packet.write_uint(self._generate_session_id())
packet.write_int(self.challenge)
packet.write(self.PADDING)
return packet
def _create_handshake_packet(self) -> Connection:
packet = Connection()
packet.write(self.MAGIC_PREFIX)
packet.write(struct.pack("!B", self.PACKET_TYPE_CHALLENGE))
packet.write_uint(self._generate_session_id())
return packet
def _read_packet(self) -> Connection:
packet = Connection()
packet.receive(self.connection.read(self.connection.remaining()))
packet.read(1 + 4)
return packet
def handshake(self) -> None:
self.connection.write(self._create_handshake_packet())
packet = self._read_packet()
self.challenge = int(packet.read_ascii())
def read_query(self) -> QueryResponse:
request = self._create_packet()
self.connection.write(request)
response = self._read_packet()
return QueryResponse.from_connection(response)
class AsyncServerQuerier(ServerQuerier):
def __init__(self, connection: UDPAsyncSocketConnection):
# We do this to inform python about self.connection type (it's async)
super().__init__(connection) # type: ignore[arg-type]
self.connection: UDPAsyncSocketConnection
async def _read_packet(self) -> Connection:
packet = Connection()
packet.receive(await self.connection.read(self.connection.remaining()))
packet.read(1 + 4)
return packet
async def handshake(self) -> None:
await self.connection.write(self._create_handshake_packet())
packet = await self._read_packet()
self.challenge = int(packet.read_ascii())
async def read_query(self) -> QueryResponse:
request = self._create_packet()
await self.connection.write(request)
response = await self._read_packet()
return QueryResponse.from_connection(response)
class QueryResponse:
# THIS IS SO UNPYTHONIC
# it's staying just because the tests depend on this structure
class Players:
online: int
max: int
names: List[str]
def __init__(self, online, max, names):
self.online = int(online)
self.max = int(max)
self.names = names
class Software:
version: str
brand: str
plugins: List[str]
def __init__(self, version, plugins):
self.version = version
self.brand = "vanilla"
self.plugins = []
if plugins:
parts = plugins.split(":", 1)
self.brand = parts[0].strip()
if len(parts) == 2:
self.plugins = [s.strip() for s in parts[1].split(";")]
motd: str
map: str
players: Players
software: Software
def __init__(self, raw, players):
try:
self.raw = raw
self.motd = raw["hostname"]
self.map = raw["map"]
self.players = QueryResponse.Players(raw["numplayers"], raw["maxplayers"], players)
self.software = QueryResponse.Software(raw["version"], raw["plugins"])
except KeyError:
raise ValueError("The provided data is not valid")
@classmethod
def from_connection(cls, response: Connection) -> Self:
response.read(len("splitnum") + 1 + 1 + 1)
data = {}
players = []
while True:
key = response.read_ascii()
if key == "hostname": # hostname is actually motd in the query protocol
match = re.search(b"(.*)\x00gametype", response.received, flags=re.DOTALL)
motd = match.group(1) if match else ""
# Since the query protocol does not properly support unicode, the motd is still not resolved
# correctly; however, this will avoid other parameter parsing errors.
data[key] = response.read(len(motd)).decode("ISO-8859-1")
response.read(1) # ignore null byte
elif len(key) == 0:
response.read(1)
break
else:
value = response.read_ascii()
data[key] = value
response.read(len("player_") + 1 + 1)
while True:
motd = response.read_ascii()
if len(motd) == 0:
break
players.append(motd)
return cls(data, players)

View File

@@ -1,132 +0,0 @@
from __future__ import annotations
import socket
from json import dumps as json_dumps
import click
from mcstatus import MinecraftServer
server: MinecraftServer = None # type: ignore[assignment] # This will be set with cli function
@click.group(context_settings=dict(help_option_names=["-h", "--help"]))
@click.argument("address")
def cli(address):
"""
mcstatus provides an easy way to query Minecraft servers for
any information they can expose. It provides three modes of
access: query, status, and ping.
Examples:
\b
$ mcstatus example.org ping
21.120ms
\b
$ mcstatus example.org:1234 ping
159.903ms
\b
$ mcstatus example.org status
version: v1.8.8 (protocol 47)
description: "A Minecraft Server"
players: 1/20 ['Dinnerbone (61699b2e-d327-4a01-9f1e-0ea8c3f06bc6)']
\b
$ mcstatus example.org query
host: 93.148.216.34:25565
software: v1.8.8 vanilla
plugins: []
motd: "A Minecraft Server"
players: 1/20 ['Dinnerbone (61699b2e-d327-4a01-9f1e-0ea8c3f06bc6)']
"""
global server
server = MinecraftServer.lookup(address)
@cli.command(short_help="prints server latency")
def ping():
"""
Ping server for latency.
"""
click.echo(f"{server.ping()}ms")
@cli.command(short_help="basic server information")
def status():
"""
Prints server status. Supported by all Minecraft
servers that are version 1.7 or higher.
"""
response = server.status()
if response.players.sample is not None:
player_sample = str([f"{player.name} ({player.id})" for player in response.players.sample])
else:
player_sample = "No players online"
click.echo(f"version: v{response.version.name} (protocol {response.version.protocol})")
click.echo(f'description: "{response.description}"')
click.echo(f"players: {response.players.online}/{response.players.max} {player_sample}")
@cli.command(short_help="all available server information in json")
def json():
"""
Prints server status and query in json. Supported by all Minecraft
servers that are version 1.7 or higher.
"""
data = {}
data["online"] = False
# Build data with responses and quit on exception
try:
ping_res = server.ping()
data["online"] = True
data["ping"] = ping_res
status_res = server.status(tries=1)
data["version"] = status_res.version.name
data["protocol"] = status_res.version.protocol
data["motd"] = status_res.description
data["player_count"] = status_res.players.online
data["player_max"] = status_res.players.max
data["players"] = []
if status_res.players.sample is not None:
data["players"] = [{"name": player.name, "id": player.id} for player in status_res.players.sample]
query_res = server.query(tries=1) # type: ignore[call-arg] # tries is supported with retry decorator
data["host_ip"] = query_res.raw["hostip"]
data["host_port"] = query_res.raw["hostport"]
data["map"] = query_res.map
data["plugins"] = query_res.software.plugins
except Exception: # TODO: Check what this actually excepts
pass
click.echo(json_dumps(data))
@cli.command(short_help="detailed server information")
def query():
"""
Prints detailed server information. Must be enabled in
servers' server.properties file.
"""
try:
response = server.query()
except socket.timeout:
print(
"The server did not respond to the query protocol."
"\nPlease ensure that the server has enable-query turned on,"
" and that the necessary port (same as server-port unless query-port is set) is open in any firewall(s)."
"\nSee https://wiki.vg/Query for further information."
)
raise click.Abort()
click.echo(f"host: {response.raw['hostip']}:{response.raw['hostport']}")
click.echo(f"software: v{response.software.version} {response.software.brand}")
click.echo(f"plugins: {response.software.plugins}")
click.echo(f'motd: "{response.motd}"')
click.echo(f"players: {response.players.online}/{response.players.max} {response.players.names}")
if __name__ == "__main__":
cli() # type: ignore[call-arg]

View File

@@ -1,256 +0,0 @@
from __future__ import annotations
from typing import Optional, TYPE_CHECKING, Tuple
from urllib.parse import urlparse
import dns.resolver
from dns.exception import DNSException
from mcstatus.bedrock_status import BedrockServerStatus, BedrockStatusResponse
from mcstatus.pinger import AsyncServerPinger, PingResponse, ServerPinger
from mcstatus.protocol.connection import (
TCPAsyncSocketConnection,
TCPSocketConnection,
UDPAsyncSocketConnection,
UDPSocketConnection,
)
from mcstatus.querier import AsyncServerQuerier, QueryResponse, ServerQuerier
from mcstatus.utils import retry
if TYPE_CHECKING:
from typing_extensions import Self
__all__ = ["MinecraftServer", "MinecraftBedrockServer"]
def parse_address(address: str) -> Tuple[str, Optional[int]]:
tmp = urlparse("//" + address)
if not tmp.hostname:
raise ValueError(f"Invalid address '{address}'")
return (tmp.hostname, tmp.port)
def ensure_valid(host: object, port: object):
if not isinstance(host, str):
raise TypeError(f"Host must be a string address, got {type(host)} ({host!r})")
if not isinstance(port, int):
raise TypeError(f"Port must be an integer port number, got {type(port)} ({port})")
if port > 65535 or port < 0:
raise ValueError(f"Port must be within the allowed range (0-2^16), got {port}")
class MinecraftServer:
"""Base class for a Minecraft Java Edition server.
:param str host: The host/address/ip of the Minecraft server.
:param int port: The port that the server is on.
:param float timeout: The timeout in seconds before failing to connect.
:attr host:
:attr port:
"""
def __init__(self, host: str, port: int = 25565, timeout: float = 3):
ensure_valid(host, port)
self.host = host
self.port = port
self.timeout = timeout
@classmethod
def lookup(cls, address: str, timeout: float = 3) -> Self:
"""Parses the given address and checks DNS records for an SRV record that points to the Minecraft server.
:param str address: The address of the Minecraft server, like `example.com:25565`.
:param float timeout: The timeout in seconds before failing to connect.
:return: A `MinecraftServer` instance.
:rtype: MinecraftServer
"""
host, port = parse_address(address)
if port is None:
port = 25565
try:
answers = dns.resolver.resolve("_minecraft._tcp." + host, "SRV")
if len(answers):
answer = answers[0]
host = str(answer.target).rstrip(".")
port = int(answer.port)
except Exception:
pass
return cls(host, port, timeout)
def ping(self, **kwargs) -> float:
"""Checks the latency between a Minecraft Java Edition server and the client (you).
:param type **kwargs: Passed to a `ServerPinger` instance.
:return: The latency between the Minecraft Server and you.
:rtype: float
"""
connection = TCPSocketConnection((self.host, self.port), self.timeout)
return self._retry_ping(connection, **kwargs)
@retry(tries=3)
def _retry_ping(self, connection: TCPSocketConnection, **kwargs) -> float:
pinger = ServerPinger(connection, host=self.host, port=self.port, **kwargs)
pinger.handshake()
return pinger.test_ping()
async def async_ping(self, **kwargs) -> float:
"""Asynchronously checks the latency between a Minecraft Java Edition server and the client (you).
:param type **kwargs: Passed to a `AsyncServerPinger` instance.
:return: The latency between the Minecraft Server and you.
:rtype: float
"""
connection = TCPAsyncSocketConnection()
await connection.connect((self.host, self.port), self.timeout)
return await self._retry_async_ping(connection, **kwargs)
@retry(tries=3)
async def _retry_async_ping(self, connection: TCPAsyncSocketConnection, **kwargs) -> float:
pinger = AsyncServerPinger(connection, host=self.host, port=self.port, **kwargs)
pinger.handshake()
ping = await pinger.test_ping()
return ping
def status(self, **kwargs) -> PingResponse:
"""Checks the status of a Minecraft Java Edition server via the ping protocol.
:param type **kwargs: Passed to a `ServerPinger` instance.
:return: Status information in a `PingResponse` instance.
:rtype: PingResponse
"""
connection = TCPSocketConnection((self.host, self.port), self.timeout)
return self._retry_status(connection, **kwargs)
@retry(tries=3)
def _retry_status(self, connection: TCPSocketConnection, **kwargs) -> PingResponse:
pinger = ServerPinger(connection, host=self.host, port=self.port, **kwargs)
pinger.handshake()
result = pinger.read_status()
result.latency = pinger.test_ping()
return result
async def async_status(self, **kwargs) -> PingResponse:
"""Asynchronously checks the status of a Minecraft Java Edition server via the ping protocol.
:param type **kwargs: Passed to a `AsyncServerPinger` instance.
:return: Status information in a `PingResponse` instance.
:rtype: PingResponse
"""
connection = TCPAsyncSocketConnection()
await connection.connect((self.host, self.port), self.timeout)
return await self._retry_async_status(connection, **kwargs)
@retry(tries=3)
async def _retry_async_status(self, connection: TCPAsyncSocketConnection, **kwargs) -> PingResponse:
pinger = AsyncServerPinger(connection, host=self.host, port=self.port, **kwargs)
pinger.handshake()
result = await pinger.read_status()
result.latency = await pinger.test_ping()
return result
def query(self) -> QueryResponse:
"""Checks the status of a Minecraft Java Edition server via the query protocol.
:return: Query status information in a `QueryResponse` instance.
:rtype: QueryResponse
"""
host = self.host
try:
answers = dns.resolver.resolve(host, "A")
if len(answers):
answer = answers[0]
host = str(answer).rstrip(".")
except DNSException:
pass
return self._retry_query(host)
@retry(tries=3)
def _retry_query(self, host: str) -> QueryResponse:
connection = UDPSocketConnection((host, self.port), self.timeout)
querier = ServerQuerier(connection)
querier.handshake()
return querier.read_query()
async def async_query(self) -> QueryResponse:
"""Asynchronously checks the status of a Minecraft Java Edition server via the query protocol.
:return: Query status information in a `QueryResponse` instance.
:rtype: QueryResponse
"""
host = self.host
try:
answers = dns.resolver.resolve(host, "A")
if len(answers):
answer = answers[0]
host = str(answer).rstrip(".")
except DNSException:
pass
return await self._retry_async_query(host)
@retry(tries=3)
async def _retry_async_query(self, host) -> QueryResponse:
connection = UDPAsyncSocketConnection()
await connection.connect((host, self.port), self.timeout)
querier = AsyncServerQuerier(connection)
await querier.handshake()
return await querier.read_query()
class MinecraftBedrockServer:
"""Base class for a Minecraft Bedrock Edition server.
:param str host: The host/address/ip of the Minecraft server.
:param int port: The port that the server is on.
:param float timeout: The timeout in seconds before failing to connect.
:attr host:
:attr port:
"""
def __init__(self, host: str, port: int = 19132, timeout: float = 3):
ensure_valid(host, port)
self.host = host
self.port = port
self.timeout = timeout
@classmethod
def lookup(cls, address: str) -> Self:
"""Parses a given address and returns a MinecraftBedrockServer instance.
:param str address: The address of the Minecraft server, like `example.com:19132`
:return: A `MinecraftBedrockServer` instance.
:rtype: MinecraftBedrockServer
"""
host, port = parse_address(address)
# If the address didn't contain port, fall back to constructor's default
if port is None:
return cls(host)
return cls(host, port)
@retry(tries=3)
def status(self, **kwargs) -> BedrockStatusResponse:
"""Checks the status of a Minecraft Bedrock Edition server.
:param type **kwargs: Passed to a `BedrockServerStatus` instance.
:return: Status information in a `BedrockStatusResponse` instance.
:rtype: BedrockStatusResponse
"""
return BedrockServerStatus(self.host, self.port, self.timeout, **kwargs).read_status()
@retry(tries=3)
async def async_status(self, **kwargs) -> BedrockStatusResponse:
"""Asynchronously checks the status of a Minecraft Bedrock Edition server.
:param type **kwargs: Passed to a `BedrockServerStatus` instance.
:return: Status information in a `BedrockStatusResponse` instance.
:rtype: BedrockStatusResponse
"""
return await BedrockServerStatus(self.host, self.port, self.timeout, **kwargs).read_status_async()

View File

@@ -1,280 +0,0 @@
from unittest.mock import Mock, patch
import pytest
from mcstatus.protocol.connection import (
Connection,
TCPSocketConnection,
UDPSocketConnection,
)
class TestConnection:
connection: Connection
def setup_method(self):
self.connection = Connection()
def test_flush(self):
self.connection.sent = bytearray.fromhex("7FAABB")
assert self.connection.flush() == bytearray.fromhex("7FAABB")
assert self.connection.sent == bytearray()
def test_receive(self):
self.connection.receive(bytearray.fromhex("7F"))
self.connection.receive(bytearray.fromhex("AABB"))
assert self.connection.received == bytearray.fromhex("7FAABB")
def test_remaining(self):
self.connection.receive(bytearray.fromhex("7F"))
self.connection.receive(bytearray.fromhex("AABB"))
assert self.connection.remaining() == 3
def test_send(self):
self.connection.write(bytearray.fromhex("7F"))
self.connection.write(bytearray.fromhex("AABB"))
assert self.connection.flush() == bytearray.fromhex("7FAABB")
def test_read(self):
self.connection.receive(bytearray.fromhex("7FAABB"))
assert self.connection.read(2) == bytearray.fromhex("7FAA")
assert self.connection.read(1) == bytearray.fromhex("BB")
def _assert_varint_read_write(self, hexstr, value) -> None:
self.connection.receive(bytearray.fromhex(hexstr))
assert self.connection.read_varint() == value
self.connection.write_varint(value)
assert self.connection.flush() == bytearray.fromhex(hexstr)
def test_varint_cases(self):
self._assert_varint_read_write("00", 0)
self._assert_varint_read_write("01", 1)
self._assert_varint_read_write("0F", 15)
self._assert_varint_read_write("FFFFFFFF07", 2147483647)
self._assert_varint_read_write("FFFFFFFF0F", -1)
self._assert_varint_read_write("8080808008", -2147483648)
def test_read_invalid_varint(self):
self.connection.receive(bytearray.fromhex("FFFFFFFF80"))
with pytest.raises(IOError):
self.connection.read_varint()
def test_write_invalid_varint(self):
with pytest.raises(ValueError):
self.connection.write_varint(2147483648)
with pytest.raises(ValueError):
self.connection.write_varint(-2147483649)
def test_read_utf(self):
self.connection.receive(bytearray.fromhex("0D48656C6C6F2C20776F726C6421"))
assert self.connection.read_utf() == "Hello, world!"
def test_write_utf(self):
self.connection.write_utf("Hello, world!")
assert self.connection.flush() == bytearray.fromhex("0D48656C6C6F2C20776F726C6421")
def test_read_empty_utf(self):
self.connection.write_utf("")
assert self.connection.flush() == bytearray.fromhex("00")
def test_read_ascii(self):
self.connection.receive(bytearray.fromhex("48656C6C6F2C20776F726C642100"))
assert self.connection.read_ascii() == "Hello, world!"
def test_write_ascii(self):
self.connection.write_ascii("Hello, world!")
assert self.connection.flush() == bytearray.fromhex("48656C6C6F2C20776F726C642100")
def test_read_empty_ascii(self):
self.connection.write_ascii("")
assert self.connection.flush() == bytearray.fromhex("00")
def test_read_short_negative(self):
self.connection.receive(bytearray.fromhex("8000"))
assert self.connection.read_short() == -32768
def test_write_short_negative(self):
self.connection.write_short(-32768)
assert self.connection.flush() == bytearray.fromhex("8000")
def test_read_short_positive(self):
self.connection.receive(bytearray.fromhex("7FFF"))
assert self.connection.read_short() == 32767
def test_write_short_positive(self):
self.connection.write_short(32767)
assert self.connection.flush() == bytearray.fromhex("7FFF")
def test_read_ushort_positive(self):
self.connection.receive(bytearray.fromhex("8000"))
assert self.connection.read_ushort() == 32768
def test_write_ushort_positive(self):
self.connection.write_ushort(32768)
assert self.connection.flush() == bytearray.fromhex("8000")
def test_read_int_negative(self):
self.connection.receive(bytearray.fromhex("80000000"))
assert self.connection.read_int() == -2147483648
def test_write_int_negative(self):
self.connection.write_int(-2147483648)
assert self.connection.flush() == bytearray.fromhex("80000000")
def test_read_int_positive(self):
self.connection.receive(bytearray.fromhex("7FFFFFFF"))
assert self.connection.read_int() == 2147483647
def test_write_int_positive(self):
self.connection.write_int(2147483647)
assert self.connection.flush() == bytearray.fromhex("7FFFFFFF")
def test_read_uint_positive(self):
self.connection.receive(bytearray.fromhex("80000000"))
assert self.connection.read_uint() == 2147483648
def test_write_uint_positive(self):
self.connection.write_uint(2147483648)
assert self.connection.flush() == bytearray.fromhex("80000000")
def test_read_long_negative(self):
self.connection.receive(bytearray.fromhex("8000000000000000"))
assert self.connection.read_long() == -9223372036854775808
def test_write_long_negative(self):
self.connection.write_long(-9223372036854775808)
assert self.connection.flush() == bytearray.fromhex("8000000000000000")
def test_read_long_positive(self):
self.connection.receive(bytearray.fromhex("7FFFFFFFFFFFFFFF"))
assert self.connection.read_long() == 9223372036854775807
def test_write_long_positive(self):
self.connection.write_long(9223372036854775807)
assert self.connection.flush() == bytearray.fromhex("7FFFFFFFFFFFFFFF")
def test_read_ulong_positive(self):
self.connection.receive(bytearray.fromhex("8000000000000000"))
assert self.connection.read_ulong() == 9223372036854775808
def test_write_ulong_positive(self):
self.connection.write_ulong(9223372036854775808)
assert self.connection.flush() == bytearray.fromhex("8000000000000000")
def test_read_buffer(self):
self.connection.receive(bytearray.fromhex("027FAA"))
buffer = self.connection.read_buffer()
assert buffer.received == bytearray.fromhex("7FAA")
assert self.connection.flush() == bytearray()
def test_write_buffer(self):
buffer = Connection()
buffer.write(bytearray.fromhex("7FAA"))
self.connection.write_buffer(buffer)
assert self.connection.flush() == bytearray.fromhex("027FAA")
class TCPSocketConnectionTest:
def setup_method(self):
socket = Mock()
socket.recv = Mock()
socket.send = Mock()
with patch("socket.create_connection") as create_connection:
create_connection.return_value = socket
self.connection = TCPSocketConnection(("localhost", 1234))
def test_flush(self):
with pytest.raises(TypeError):
self.connection.flush()
def test_receive(self):
with pytest.raises(TypeError):
self.connection.receive("") # type: ignore # This is desired to produce TypeError
def test_remaining(self):
with pytest.raises(TypeError):
self.connection.remaining()
def test_read(self):
self.connection.socket.recv.return_value = bytearray.fromhex("7FAA")
assert self.connection.read(2) == bytearray.fromhex("7FAA")
def test_read_empty(self):
self.connection.socket.recv.return_value = bytearray.fromhex("")
with pytest.raises(IOError):
self.connection.read(2)
def test_write(self):
self.connection.write(bytearray.fromhex("7FAA"))
self.connection.socket.send.assert_called_once_with(bytearray.fromhex("7FAA")) # type: ignore[attr-defined]
class UDPSocketConnectionTest:
def setup_method(self):
socket = Mock()
socket.recvfrom = Mock()
socket.sendto = Mock()
with patch("socket.socket") as create_socket:
create_socket.return_value = socket
self.connection = UDPSocketConnection(("localhost", 1234))
def test_flush(self):
with pytest.raises(TypeError):
self.connection.flush()
def test_receive(self):
with pytest.raises(TypeError):
self.connection.receive("") # type: ignore # This is desired to produce TypeError
def test_remaining(self):
assert self.connection.remaining() == 65535
def test_read(self):
self.connection.socket.recvfrom.return_value = [bytearray.fromhex("7FAA")]
assert self.connection.read(2) == bytearray.fromhex("7FAA")
def test_write(self):
self.connection.write(bytearray.fromhex("7FAA"))
self.connection.socket.sendto.assert_called_once_with( # type: ignore[attr-defined]
bytearray.fromhex("7FAA"),
("localhost", 1234),
)

View File

@@ -1,91 +0,0 @@
import asyncio
import pytest
from mcstatus.pinger import AsyncServerPinger
from mcstatus.protocol.connection import Connection
def async_decorator(f):
def wrapper(*args, **kwargs):
loop = asyncio.get_event_loop()
return loop.run_until_complete(f(*args, **kwargs))
return wrapper
class FakeAsyncConnection(Connection):
async def read_buffer(self):
return super().read_buffer()
class TestAsyncServerPinger:
def setup_method(self):
self.pinger = AsyncServerPinger(
FakeAsyncConnection(), host="localhost", port=25565, version=44 # type: ignore[arg-type]
)
def test_handshake(self):
self.pinger.handshake()
assert self.pinger.connection.flush() == bytearray.fromhex("0F002C096C6F63616C686F737463DD01")
def test_read_status(self):
self.pinger.connection.receive(
bytearray.fromhex(
"7200707B226465736372697074696F6E223A2241204D696E65637261667420536572766572222C22706C6179657273223A7B2"
"26D6178223A32302C226F6E6C696E65223A307D2C2276657273696F6E223A7B226E616D65223A22312E382D70726531222C22"
"70726F746F636F6C223A34347D7D"
)
)
status = async_decorator(self.pinger.read_status)()
assert status.raw == {
"description": "A Minecraft Server",
"players": {"max": 20, "online": 0},
"version": {"name": "1.8-pre1", "protocol": 44},
}
assert self.pinger.connection.flush() == bytearray.fromhex("0100")
def test_read_status_invalid_json(self):
self.pinger.connection.receive(bytearray.fromhex("0300017B"))
with pytest.raises(IOError):
async_decorator(self.pinger.test_ping)()
def test_read_status_invalid_reply(self):
self.pinger.connection.receive(
bytearray.fromhex(
"4F004D7B22706C6179657273223A7B226D6178223A32302C226F6E6C696E65223A307D2C2276657273696F6E223A7B226E616"
"D65223A22312E382D70726531222C2270726F746F636F6C223A34347D7D"
)
)
with pytest.raises(IOError):
async_decorator(self.pinger.test_ping)()
def test_read_status_invalid_status(self):
self.pinger.connection.receive(bytearray.fromhex("0105"))
with pytest.raises(IOError):
async_decorator(self.pinger.test_ping)()
def test_test_ping(self):
self.pinger.connection.receive(bytearray.fromhex("09010000000000DD7D1C"))
self.pinger.ping_token = 14515484
assert async_decorator(self.pinger.test_ping)() >= 0
assert self.pinger.connection.flush() == bytearray.fromhex("09010000000000DD7D1C")
def test_test_ping_invalid(self):
self.pinger.connection.receive(bytearray.fromhex("011F"))
self.pinger.ping_token = 14515484
with pytest.raises(IOError):
async_decorator(self.pinger.test_ping)()
def test_test_ping_wrong_token(self):
self.pinger.connection.receive(bytearray.fromhex("09010000000000DD7D1C"))
self.pinger.ping_token = 12345
with pytest.raises(IOError):
async_decorator(self.pinger.test_ping)()

View File

@@ -1,51 +0,0 @@
from mcstatus.protocol.connection import Connection
from mcstatus.querier import AsyncServerQuerier
from mcstatus.tests.test_async_pinger import async_decorator
class FakeUDPAsyncConnection(Connection):
async def read(self, length):
return super().read(length)
async def write(self, data):
return super().write(data)
class TestMinecraftAsyncQuerier:
def setup_method(self):
self.querier = AsyncServerQuerier(FakeUDPAsyncConnection()) # type: ignore[arg-type]
def test_handshake(self):
self.querier.connection.receive(bytearray.fromhex("090000000035373033353037373800"))
async_decorator(self.querier.handshake)()
conn_bytes = self.querier.connection.flush()
assert conn_bytes[:3] == bytearray.fromhex("FEFD09")
assert self.querier.challenge == 570350778
def test_query(self):
self.querier.connection.receive(
bytearray.fromhex(
"00000000000000000000000000000000686f73746e616d650041204d696e656372616674205365727665720067616d6574797"
"06500534d500067616d655f6964004d494e4543524146540076657273696f6e00312e3800706c7567696e7300006d61700077"
"6f726c64006e756d706c61796572730033006d6178706c617965727300323000686f7374706f727400323535363500686f737"
"46970003139322e3136382e35362e31000001706c617965725f000044696e6e6572626f6e6500446a696e6e69626f6e650053"
"746576650000"
)
)
response = async_decorator(self.querier.read_query)()
conn_bytes = self.querier.connection.flush()
assert conn_bytes[:3] == bytearray.fromhex("FEFD00")
assert conn_bytes[7:] == bytearray.fromhex("0000000000000000")
assert response.raw == {
"hostname": "A Minecraft Server",
"gametype": "SMP",
"game_id": "MINECRAFT",
"version": "1.8",
"plugins": "",
"map": "world",
"numplayers": "3",
"maxplayers": "20",
"hostport": "25565",
"hostip": "192.168.56.1",
}
assert response.players.names == ["Dinnerbone", "Djinnibone", "Steve"]

View File

@@ -1,26 +0,0 @@
from inspect import iscoroutinefunction
from mcstatus.protocol.connection import (
TCPAsyncSocketConnection,
UDPAsyncSocketConnection,
)
def test_is_completely_asynchronous():
conn = TCPAsyncSocketConnection()
assertions = 0
for attribute in dir(conn):
if attribute.startswith("read_"):
assert iscoroutinefunction(getattr(conn, attribute))
assertions += 1
assert assertions > 0, "None of the read_* attributes were async"
def test_query_is_completely_asynchronous():
conn = UDPAsyncSocketConnection()
assertions = 0
for attribute in dir(conn):
if attribute.startswith("read_"):
assert iscoroutinefunction(getattr(conn, attribute))
assertions += 1
assert assertions > 0, "None of the read_* attributes were async"

View File

@@ -1,21 +0,0 @@
from mcstatus.bedrock_status import BedrockServerStatus, BedrockStatusResponse
def test_bedrock_response_contains_expected_fields():
data = (
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"
b"\x124Vx\x00wMCPE;\xc2\xa7r\xc2\xa74G\xc2\xa7r\xc2\xa76a\xc2\xa7r\xc2\xa7ey\xc2\xa7r\xc2\xa72B\xc2\xa7r\xc2"
b"\xa71o\xc2\xa7r\xc2\xa79w\xc2\xa7r\xc2\xa7ds\xc2\xa7r\xc2\xa74e\xc2\xa7r\xc2\xa76r;422;;1;69;376707197539105"
b"3022;;Default;1;19132;-1;"
)
parsed = BedrockServerStatus.parse_response(data, 1)
assert isinstance(parsed, BedrockStatusResponse)
assert "gamemode" in parsed.__dict__
assert "latency" in parsed.__dict__
assert "map" in parsed.__dict__
assert "motd" in parsed.__dict__
assert "players_max" in parsed.__dict__
assert "players_online" in parsed.__dict__
assert "version" in parsed.__dict__
assert "brand" in parsed.version.__dict__
assert "protocol" in parsed.version.__dict__

View File

@@ -1,386 +0,0 @@
import pytest
from mcstatus.pinger import PingResponse, ServerPinger
from mcstatus.protocol.connection import Connection
class TestServerPinger:
def setup_method(self):
self.pinger = ServerPinger(Connection(), host="localhost", port=25565, version=44) # type: ignore[arg-type]
def test_handshake(self):
self.pinger.handshake()
assert self.pinger.connection.flush() == bytearray.fromhex("0F002C096C6F63616C686F737463DD01")
def test_read_status(self):
self.pinger.connection.receive(
bytearray.fromhex(
"7200707B226465736372697074696F6E223A2241204D696E65637261667420536572766572222C22706C6179657273223A7B2"
"26D6178223A32302C226F6E6C696E65223A307D2C2276657273696F6E223A7B226E616D65223A22312E382D70726531222C22"
"70726F746F636F6C223A34347D7D"
)
)
status = self.pinger.read_status()
assert status.raw == {
"description": "A Minecraft Server",
"players": {"max": 20, "online": 0},
"version": {"name": "1.8-pre1", "protocol": 44},
}
assert self.pinger.connection.flush() == bytearray.fromhex("0100")
def test_read_status_invalid_json(self):
self.pinger.connection.receive(bytearray.fromhex("0300017B"))
with pytest.raises(IOError):
self.pinger.read_status()
def test_read_status_invalid_reply(self):
self.pinger.connection.receive(
bytearray.fromhex(
"4F004D7B22706C6179657273223A7B226D6178223A32302C226F6E6C696E65223A307D2C2276657273696F6E223A7B226E616"
"D65223A22312E382D70726531222C2270726F746F636F6C223A34347D7D"
)
)
with pytest.raises(IOError):
self.pinger.read_status()
def test_read_status_invalid_status(self):
self.pinger.connection.receive(bytearray.fromhex("0105"))
with pytest.raises(IOError):
self.pinger.read_status()
def test_test_ping(self):
self.pinger.connection.receive(bytearray.fromhex("09010000000000DD7D1C"))
self.pinger.ping_token = 14515484
assert self.pinger.test_ping() >= 0
assert self.pinger.connection.flush() == bytearray.fromhex("09010000000000DD7D1C")
def test_test_ping_invalid(self):
self.pinger.connection.receive(bytearray.fromhex("011F"))
self.pinger.ping_token = 14515484
with pytest.raises(IOError):
self.pinger.test_ping()
def test_test_ping_wrong_token(self):
self.pinger.connection.receive(bytearray.fromhex("09010000000000DD7D1C"))
self.pinger.ping_token = 12345
with pytest.raises(IOError):
self.pinger.test_ping()
class TestPingResponse:
def test_raw(self):
response = PingResponse(
{
"description": "A Minecraft Server",
"players": {"max": 20, "online": 0},
"version": {"name": "1.8-pre1", "protocol": 44},
}
)
assert response.raw == {
"description": "A Minecraft Server",
"players": {"max": 20, "online": 0},
"version": {"name": "1.8-pre1", "protocol": 44},
}
def test_description(self):
response = PingResponse(
{
"description": "A Minecraft Server",
"players": {"max": 20, "online": 0},
"version": {"name": "1.8-pre1", "protocol": 44},
}
)
assert response.description == "A Minecraft Server"
def test_description_missing(self):
with pytest.raises(ValueError):
PingResponse(
{
"players": {"max": 20, "online": 0},
"version": {"name": "1.8-pre1", "protocol": 44},
}
)
def test_parse_description_strips_html_color_codes(self):
out = PingResponse._parse_description(
{
"extra": [
{"text": " "},
{"strikethrough": True, "color": "#b3eeff", "text": "="},
{"strikethrough": True, "color": "#b9ecff", "text": "="},
{"strikethrough": True, "color": "#c0eaff", "text": "="},
{"strikethrough": True, "color": "#c7e8ff", "text": "="},
{"strikethrough": True, "color": "#cee6ff", "text": "="},
{"strikethrough": True, "color": "#d5e4ff", "text": "="},
{"strikethrough": True, "color": "#dce2ff", "text": "="},
{"strikethrough": True, "color": "#e3e0ff", "text": "="},
{"strikethrough": True, "color": "#eadeff", "text": "="},
{"strikethrough": True, "color": "#f1dcff", "text": "="},
{"strikethrough": True, "color": "#f8daff", "text": "="},
{"strikethrough": True, "color": "#ffd9ff", "text": "="},
{"strikethrough": True, "color": "#f4dcff", "text": "="},
{"strikethrough": True, "color": "#f9daff", "text": "="},
{"strikethrough": True, "color": "#ffd9ff", "text": "="},
{"color": "white", "text": " "},
{"bold": True, "color": "#66ff99", "text": "C"},
{"bold": True, "color": "#75f5a2", "text": "r"},
{"bold": True, "color": "#84ebab", "text": "e"},
{"bold": True, "color": "#93e2b4", "text": "a"},
{"bold": True, "color": "#a3d8bd", "text": "t"},
{"bold": True, "color": "#b2cfc6", "text": "i"},
{"bold": True, "color": "#c1c5cf", "text": "v"},
{"bold": True, "color": "#d1bbd8", "text": "e"},
{"bold": True, "color": "#e0b2e1", "text": "F"},
{"bold": True, "color": "#efa8ea", "text": "u"},
{"bold": True, "color": "#ff9ff4", "text": "n "},
{"strikethrough": True, "color": "#b3eeff", "text": "="},
{"strikethrough": True, "color": "#b9ecff", "text": "="},
{"strikethrough": True, "color": "#c0eaff", "text": "="},
{"strikethrough": True, "color": "#c7e8ff", "text": "="},
{"strikethrough": True, "color": "#cee6ff", "text": "="},
{"strikethrough": True, "color": "#d5e4ff", "text": "="},
{"strikethrough": True, "color": "#dce2ff", "text": "="},
{"strikethrough": True, "color": "#e3e0ff", "text": "="},
{"strikethrough": True, "color": "#eadeff", "text": "="},
{"strikethrough": True, "color": "#f1dcff", "text": "="},
{"strikethrough": True, "color": "#f8daff", "text": "="},
{"strikethrough": True, "color": "#ffd9ff", "text": "="},
{"strikethrough": True, "color": "#f4dcff", "text": "="},
{"strikethrough": True, "color": "#f9daff", "text": "="},
{"strikethrough": True, "color": "#ffd9ff", "text": "="},
{"color": "white", "text": " \n "},
{"bold": True, "color": "#E5E5E5", "text": "The server has been updated to "},
{"bold": True, "color": "#97ABFF", "text": "1.17.1"},
],
"text": "",
}
)
assert out == (
" ===============§f §lC§lr§le§la§lt§li§lv§le§lF§lu§ln ===============§f \n"
" §lThe server has been updated to §l1.17.1"
)
def test_parse_description(self):
out = PingResponse._parse_description("test §2description")
assert out == "test §2description"
out = PingResponse._parse_description({"text": "§8§lfoo"})
assert out == "§8§lfoo"
out = PingResponse._parse_description(
{
"extra": [{"bold": True, "italic": True, "color": "gray", "text": "foo"}, {"color": "gold", "text": "bar"}],
"text": ".",
}
)
# We don't care in which order the style prefixes are, allow any
assert out in {
"§l§o§7foo§6bar.",
"§l§7§ofoo§6bar.",
"§o§l§7foo§6bar.",
"§o§7§lfoo§6bar.",
"§7§o§lfoo§6bar.",
"§7§l§ofoo§6bar.",
}
out = PingResponse._parse_description(
[{"bold": True, "italic": True, "color": "gray", "text": "foo"}, {"color": "gold", "text": "bar"}]
)
assert out in {
"§l§o§7foo§6bar",
"§l§7§ofoo§6bar",
"§o§l§7foo§6bar",
"§o§7§lfoo§6bar",
"§7§o§lfoo§6bar",
"§7§l§ofoo§6bar",
}
def test_version(self):
response = PingResponse(
{
"description": "A Minecraft Server",
"players": {"max": 20, "online": 0},
"version": {"name": "1.8-pre1", "protocol": 44},
}
)
assert response.version is not None
assert response.version.name == "1.8-pre1"
assert response.version.protocol == 44
def test_version_missing(self):
with pytest.raises(ValueError):
PingResponse(
{
"description": "A Minecraft Server",
"players": {"max": 20, "online": 0},
}
)
def test_version_invalid(self):
with pytest.raises(ValueError):
PingResponse(
{
"description": "A Minecraft Server",
"players": {"max": 20, "online": 0},
"version": "foo",
}
)
def test_players(self):
response = PingResponse(
{
"description": "A Minecraft Server",
"players": {"max": 20, "online": 5},
"version": {"name": "1.8-pre1", "protocol": 44},
}
)
assert response.players is not None
assert response.players.max == 20
assert response.players.online == 5
def test_players_missing(self):
with pytest.raises(ValueError):
PingResponse(
{
"description": "A Minecraft Server",
"version": {"name": "1.8-pre1", "protocol": 44},
}
)
def test_favicon(self):
response = PingResponse(
{
"description": "A Minecraft Server",
"players": {"max": 20, "online": 0},
"version": {"name": "1.8-pre1", "protocol": 44},
"favicon": "data:image/png;base64,foo",
}
)
assert response.favicon == "data:image/png;base64,foo"
def test_favicon_missing(self):
response = PingResponse(
{
"description": "A Minecraft Server",
"players": {"max": 20, "online": 0},
"version": {"name": "1.8-pre1", "protocol": 44},
}
)
assert response.favicon is None
class TestPingResponsePlayers:
def test_invalid(self):
with pytest.raises(ValueError):
PingResponse.Players("foo")
def test_max_missing(self):
with pytest.raises(ValueError):
PingResponse.Players({"online": 5})
def test_max_invalid(self):
with pytest.raises(ValueError):
PingResponse.Players({"max": "foo", "online": 5})
def test_online_missing(self):
with pytest.raises(ValueError):
PingResponse.Players({"max": 20})
def test_online_invalid(self):
with pytest.raises(ValueError):
PingResponse.Players({"max": 20, "online": "foo"})
def test_valid(self):
players = PingResponse.Players({"max": 20, "online": 5})
assert players.max == 20
assert players.online == 5
def test_sample(self):
players = PingResponse.Players(
{
"max": 20,
"online": 1,
"sample": [{"name": "Dinnerbone", "id": "61699b2e-d327-4a01-9f1e-0ea8c3f06bc6"}],
}
)
assert players.sample is not None
assert players.sample[0].name == "Dinnerbone"
def test_sample_invalid(self):
with pytest.raises(ValueError):
PingResponse.Players({"max": 20, "online": 1, "sample": "foo"})
def test_sample_missing(self):
players = PingResponse.Players({"max": 20, "online": 1})
assert players.sample is None
class TestPingResponsePlayersPlayer:
def test_invalid(self):
with pytest.raises(ValueError):
PingResponse.Players.Player("foo")
def test_name_missing(self):
with pytest.raises(ValueError):
PingResponse.Players.Player({"id": "61699b2e-d327-4a01-9f1e-0ea8c3f06bc6"})
def test_name_invalid(self):
with pytest.raises(ValueError):
PingResponse.Players.Player({"name": {}, "id": "61699b2e-d327-4a01-9f1e-0ea8c3f06bc6"})
def test_id_missing(self):
with pytest.raises(ValueError):
PingResponse.Players.Player({"name": "Dinnerbone"})
def test_id_invalid(self):
with pytest.raises(ValueError):
PingResponse.Players.Player({"name": "Dinnerbone", "id": {}})
def test_valid(self):
player = PingResponse.Players.Player({"name": "Dinnerbone", "id": "61699b2e-d327-4a01-9f1e-0ea8c3f06bc6"})
assert player.name == "Dinnerbone"
assert player.id == "61699b2e-d327-4a01-9f1e-0ea8c3f06bc6"
class TestPingResponseVersion:
def test_invalid(self):
with pytest.raises(ValueError):
PingResponse.Version("foo")
def test_protocol_missing(self):
with pytest.raises(ValueError):
PingResponse.Version({"name": "foo"})
def test_protocol_invalid(self):
with pytest.raises(ValueError):
PingResponse.Version({"name": "foo", "protocol": "bar"})
def test_name_missing(self):
with pytest.raises(ValueError):
PingResponse.Version({"protocol": 5})
def test_name_invalid(self):
with pytest.raises(ValueError):
PingResponse.Version({"name": {}, "protocol": 5})
def test_valid(self):
players = PingResponse.Version({"name": "foo", "protocol": 5})
assert players.name == "foo"
assert players.protocol == 5

View File

@@ -1,125 +0,0 @@
from mcstatus.protocol.connection import Connection
from mcstatus.querier import QueryResponse, ServerQuerier
class TestMinecraftQuerier:
def setup_method(self):
self.querier = ServerQuerier(Connection()) # type: ignore[arg-type]
def test_handshake(self):
self.querier.connection.receive(bytearray.fromhex("090000000035373033353037373800"))
self.querier.handshake()
conn_bytes = self.querier.connection.flush()
assert conn_bytes[:3] == bytearray.fromhex("FEFD09")
assert self.querier.challenge == 570350778
def test_query(self):
self.querier.connection.receive(
bytearray.fromhex(
"00000000000000000000000000000000686f73746e616d650041204d696e656372616674205365727665720067616d6574797"
"06500534d500067616d655f6964004d494e4543524146540076657273696f6e00312e3800706c7567696e7300006d61700077"
"6f726c64006e756d706c61796572730033006d6178706c617965727300323000686f7374706f727400323535363500686f737"
"46970003139322e3136382e35362e31000001706c617965725f000044696e6e6572626f6e6500446a696e6e69626f6e650053"
"746576650000"
)
)
response = self.querier.read_query()
conn_bytes = self.querier.connection.flush()
assert conn_bytes[:3] == bytearray.fromhex("FEFD00")
assert conn_bytes[7:] == bytearray.fromhex("0000000000000000")
assert response.raw == {
"hostname": "A Minecraft Server",
"gametype": "SMP",
"game_id": "MINECRAFT",
"version": "1.8",
"plugins": "",
"map": "world",
"numplayers": "3",
"maxplayers": "20",
"hostport": "25565",
"hostip": "192.168.56.1",
}
assert response.players.names == ["Dinnerbone", "Djinnibone", "Steve"]
def test_query_handles_unicode_motd_with_nulls(self):
self.querier.connection.receive(
bytearray(
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00hostname\x00\x00*K\xd5\x00gametype\x00SMP"
b"\x00game_id\x00MINECRAFT\x00version\x001.16.5\x00plugins\x00Paper on 1.16.5-R0.1-SNAPSHOT\x00map\x00world"
b"\x00numplayers\x000\x00maxplayers\x0020\x00hostport\x0025565\x00hostip\x00127.0.1.1\x00\x00\x01player_\x00"
b"\x00\x00"
)
)
response = self.querier.read_query()
self.querier.connection.flush()
assert response.raw["game_id"] == "MINECRAFT"
assert response.motd == "\x00*KÕ"
def test_query_handles_unicode_motd_with_2a00_at_the_start(self):
self.querier.connection.receive(
bytearray.fromhex(
"00000000000000000000000000000000686f73746e616d6500006f746865720067616d657479706500534d500067616d655f6964004d"
"494e4543524146540076657273696f6e00312e31382e3100706c7567696e7300006d617000776f726c64006e756d706c617965727300"
"30006d6178706c617965727300323000686f7374706f727400323535363500686f73746970003137322e31372e302e32000001706c61"
"7965725f000000"
)
)
response = self.querier.read_query()
self.querier.connection.flush()
assert response.raw["game_id"] == "MINECRAFT"
assert response.motd == "\x00other" # "\u2a00other" is actually what is expected,
# but the query protocol for vanilla has a bug when it comes to unicode handling.
# The status protocol correctly shows "⨀other".
class TestQueryResponse:
def setup_method(self):
self.raw = {
"hostname": "A Minecraft Server",
"gametype": "SMP",
"game_id": "MINECRAFT",
"version": "1.8",
"plugins": "",
"map": "world",
"numplayers": "3",
"maxplayers": "20",
"hostport": "25565",
"hostip": "192.168.56.1",
}
self.players = ["Dinnerbone", "Djinnibone", "Steve"]
def test_valid(self):
response = QueryResponse(self.raw, self.players)
assert response.motd == "A Minecraft Server"
assert response.map == "world"
assert response.players.online == 3
assert response.players.max == 20
assert response.players.names == ["Dinnerbone", "Djinnibone", "Steve"]
assert response.software.brand == "vanilla"
assert response.software.version == "1.8"
assert response.software.plugins == []
def test_valid2(self):
players = QueryResponse.Players(5, 20, ["Dinnerbone", "Djinnibone", "Steve"])
assert players.online == 5
assert players.max == 20
assert players.names == ["Dinnerbone", "Djinnibone", "Steve"]
def test_vanilla(self):
software = QueryResponse.Software("1.8", "")
assert software.brand == "vanilla"
assert software.version == "1.8"
assert software.plugins == []
def test_modded(self):
software = QueryResponse.Software("1.8", "A modded server: Foo 1.0; Bar 2.0; Baz 3.0")
assert software.brand == "A modded server"
assert software.plugins == ["Foo 1.0", "Bar 2.0", "Baz 3.0"]
def test_modded_no_plugins(self):
software = QueryResponse.Software("1.8", "A modded server")
assert software.brand == "A modded server"
assert software.plugins == []

View File

@@ -1,66 +0,0 @@
import pytest
from mcstatus.tests.test_async_pinger import async_decorator
from mcstatus.utils import retry
def test_sync_success():
x = -1
@retry(tries=2)
def func():
nonlocal x
x += 1
return 5 / x
y = func()
assert x == 1
assert y == 5
def test_sync_fail():
x = -1
@retry(tries=2)
def func():
nonlocal x
x += 1
if x == 0:
raise OSError("First error")
elif x == 1:
raise RuntimeError("Second error")
# We should get the last exception on failure (not OSError)
with pytest.raises(RuntimeError):
func()
def test_async_success():
x = -1
@retry(tries=2)
async def func():
nonlocal x
x += 1
return 5 / x
y = async_decorator(func)()
assert x == 1
assert y == 5
def test_async_fail():
x = -1
@retry(tries=2)
async def func():
nonlocal x
x += 1
if x == 0:
raise OSError("First error")
elif x == 1:
raise RuntimeError("Second error")
# We should get the last exception on failure (not OSError)
with pytest.raises(RuntimeError):
async_decorator(func)()

View File

@@ -1,215 +0,0 @@
import asyncio
import sys
from unittest.mock import Mock, patch
import pytest
from mcstatus.protocol.connection import Connection
from mcstatus.server import MinecraftServer
class MockProtocolFactory(asyncio.Protocol):
transport: asyncio.Transport
def __init__(self, data_expected_to_receive, data_to_respond_with):
self.data_expected_to_receive = data_expected_to_receive
self.data_to_respond_with = data_to_respond_with
def connection_made(self, transport):
print("connection_made")
self.transport = transport
def connection_lost(self, exc):
print("connection_lost")
self.transport.close()
def data_received(self, data):
assert self.data_expected_to_receive in data
self.transport.write(self.data_to_respond_with)
def eof_received(self):
print("eof_received")
def pause_writing(self):
print("pause_writing")
def resume_writing(self):
print("resume_writing")
@pytest.fixture()
async def create_mock_packet_server(event_loop):
servers = []
async def create_server(port, data_expected_to_receive, data_to_respond_with):
server = await event_loop.create_server(
lambda: MockProtocolFactory(data_expected_to_receive, data_to_respond_with),
host="localhost",
port=port,
)
servers.append(server)
return server
yield create_server
for server in servers:
server.close()
await server.wait_closed()
class TestAsyncMinecraftServer:
@pytest.mark.skipif(
sys.platform.startswith("win"),
reason="async bug on Windows https://github.com/Dinnerbone/mcstatus/issues/140",
)
@pytest.mark.asyncio
async def test_async_ping(self, unused_tcp_port, create_mock_packet_server):
await create_mock_packet_server(
port=unused_tcp_port,
data_expected_to_receive=bytearray.fromhex("09010000000001C54246"),
data_to_respond_with=bytearray.fromhex("0F002F096C6F63616C686F737463DD0109010000000001C54246"),
)
minecraft_server = MinecraftServer("localhost", port=unused_tcp_port)
latency = await minecraft_server.async_ping(ping_token=29704774, version=47)
assert latency >= 0
class TestMinecraftServer:
def setup_method(self):
self.socket = Connection()
self.server = MinecraftServer("localhost", port=25565)
def test_ping(self):
self.socket.receive(bytearray.fromhex("09010000000001C54246"))
with patch("mcstatus.server.TCPSocketConnection") as connection:
connection.return_value = self.socket
latency = self.server.ping(ping_token=29704774, version=47)
assert self.socket.flush() == bytearray.fromhex("0F002F096C6F63616C686F737463DD0109010000000001C54246")
assert self.socket.remaining() == 0, "Data is pending to be read, but should be empty"
assert latency >= 0
def test_ping_retry(self):
with patch("mcstatus.server.TCPSocketConnection") as connection:
connection.return_value = None
with patch("mcstatus.server.ServerPinger") as pinger:
pinger.side_effect = [Exception, Exception, Exception]
with pytest.raises(Exception):
self.server.ping()
assert pinger.call_count == 3
def test_status(self):
self.socket.receive(
bytearray.fromhex(
"6D006B7B226465736372697074696F6E223A2241204D696E65637261667420536572766572222C22706C6179657273223A7B2"
"26D6178223A32302C226F6E6C696E65223A307D2C2276657273696F6E223A7B226E616D65223A22312E38222C2270726F746F"
"636F6C223A34377D7D09010000000001C54246"
)
)
with patch("mcstatus.server.TCPSocketConnection") as connection:
connection.return_value = self.socket
info = self.server.status(ping_token=29704774, version=47)
assert self.socket.flush() == bytearray.fromhex("0F002F096C6F63616C686F737463DD01010009010000000001C54246")
assert self.socket.remaining() == 0, "Data is pending to be read, but should be empty"
assert info.raw == {
"description": "A Minecraft Server",
"players": {"max": 20, "online": 0},
"version": {"name": "1.8", "protocol": 47},
}
assert info.latency >= 0
def test_status_retry(self):
with patch("mcstatus.server.TCPSocketConnection") as connection:
connection.return_value = None
with patch("mcstatus.server.ServerPinger") as pinger:
pinger.side_effect = [Exception, Exception, Exception]
with pytest.raises(Exception):
self.server.status()
assert pinger.call_count == 3
def test_query(self):
self.socket.receive(bytearray.fromhex("090000000035373033353037373800"))
self.socket.receive(
bytearray.fromhex(
"00000000000000000000000000000000686f73746e616d650041204d696e656372616674205365727665720067616d6574797"
"06500534d500067616d655f6964004d494e4543524146540076657273696f6e00312e3800706c7567696e7300006d61700077"
"6f726c64006e756d706c61796572730033006d6178706c617965727300323000686f7374706f727400323535363500686f737"
"46970003139322e3136382e35362e31000001706c617965725f000044696e6e6572626f6e6500446a696e6e69626f6e650053"
"746576650000"
)
)
self.socket.remaining = Mock()
self.socket.remaining.side_effect = [15, 208]
with patch("mcstatus.server.UDPSocketConnection") as connection:
connection.return_value = self.socket
info = self.server.query()
conn_bytes = self.socket.flush()
assert conn_bytes[:3] == bytearray.fromhex("FEFD09")
assert info.raw == {
"hostname": "A Minecraft Server",
"gametype": "SMP",
"game_id": "MINECRAFT",
"version": "1.8",
"plugins": "",
"map": "world",
"numplayers": "3",
"maxplayers": "20",
"hostport": "25565",
"hostip": "192.168.56.1",
}
def test_query_retry(self):
with patch("mcstatus.server.UDPSocketConnection") as connection:
connection.return_value = None
with patch("mcstatus.server.ServerQuerier") as querier:
querier.side_effect = [Exception, Exception, Exception]
with pytest.raises(Exception):
self.server.query()
assert querier.call_count == 3
def test_by_address_no_srv(self):
with patch("dns.resolver.resolve") as resolve:
resolve.return_value = []
self.server = MinecraftServer.lookup("example.org")
resolve.assert_called_once_with("_minecraft._tcp.example.org", "SRV")
assert self.server.host == "example.org"
assert self.server.port == 25565
def test_by_address_invalid_srv(self):
with patch("dns.resolver.resolve") as resolve:
resolve.side_effect = [Exception]
self.server = MinecraftServer.lookup("example.org")
resolve.assert_called_once_with("_minecraft._tcp.example.org", "SRV")
assert self.server.host == "example.org"
assert self.server.port == 25565
def test_by_address_with_srv(self):
with patch("dns.resolver.resolve") as resolve:
answer = Mock()
answer.target = "different.example.org."
answer.port = 12345
resolve.return_value = [answer]
self.server = MinecraftServer.lookup("example.org")
resolve.assert_called_once_with("_minecraft._tcp.example.org", "SRV")
assert self.server.host == "different.example.org"
assert self.server.port == 12345
def test_by_address_with_port(self):
self.server = MinecraftServer.lookup("example.org:12345")
assert self.server.host == "example.org"
assert self.server.port == 12345
def test_by_address_with_multiple_ports(self):
with pytest.raises(ValueError):
MinecraftServer.lookup("example.org:12345:6789")
def test_by_address_with_invalid_port(self):
with pytest.raises(ValueError):
MinecraftServer.lookup("example.org:port")

View File

@@ -1,23 +0,0 @@
from unittest.mock import Mock
from mcstatus.protocol.connection import Connection
from mcstatus.querier import ServerQuerier
class TestHandshake:
def setup_method(self):
self.querier = ServerQuerier(Connection()) # type: ignore[arg-type]
def test_session_id(self):
def session_id():
return 0x01010101
self.querier.connection.receive(bytearray.fromhex("090000000035373033353037373800"))
self.querier._generate_session_id = Mock()
self.querier._generate_session_id = session_id
self.querier.handshake()
conn_bytes = self.querier.connection.flush()
assert conn_bytes[:3] == bytearray.fromhex("FEFD09")
assert conn_bytes[3:] == session_id().to_bytes(4, byteorder="big")
assert self.querier.challenge == 570350778

View File

@@ -1,34 +0,0 @@
import asyncio
from unittest.mock import patch
import pytest
from mcstatus.protocol.connection import TCPAsyncSocketConnection
class FakeAsyncStream(asyncio.StreamReader):
async def read(self, n: int) -> bytes:
await asyncio.sleep(delay=2)
return bytes([0] * n)
async def fake_asyncio_asyncio_open_connection(hostname: str, port: int):
return FakeAsyncStream(), None
class TestAsyncSocketConnection:
def setup_method(self):
self.tcp_async_socket = TCPAsyncSocketConnection()
def test_tcp_socket_read(self):
try:
from asyncio.exceptions import TimeoutError
except ImportError:
from asyncio import TimeoutError
loop = asyncio.get_event_loop()
with patch("asyncio.open_connection", fake_asyncio_asyncio_open_connection):
loop.run_until_complete(self.tcp_async_socket.connect(("dummy_address", 1234), timeout=0.01))
with pytest.raises(TimeoutError):
loop.run_until_complete(self.tcp_async_socket.read(10))

View File

@@ -1,48 +0,0 @@
from __future__ import annotations
import asyncio
from functools import wraps
from typing import Callable, Tuple, Type
def retry(tries: int, exceptions: Tuple[Type[BaseException]] = (Exception,)) -> Callable:
"""
Decorator that re-runs given function tries times if error occurs.
The amount of tries will either be the value given to the decorator,
or if tries is present in keyword arguments on function call, this
specified value will take precedense.
If the function fails even after all of the retries, raise the last
exception that the function raised (even if the previous failures caused
a different exception, this will only raise the last one!).
"""
def decorate(func: Callable) -> Callable:
@wraps(func)
async def async_wrapper(*args, tries: int = tries, **kwargs):
last_exc: BaseException
for _ in range(tries):
try:
return await func(*args, **kwargs)
except exceptions as exc:
last_exc = exc
else:
raise last_exc # type: ignore # (This won't actually be unbound)
@wraps(func)
def sync_wrapper(*args, tries: int = tries, **kwargs):
last_exc: BaseException
for _ in range(tries):
try:
return func(*args, **kwargs)
except exceptions as exc:
last_exc = exc
else:
raise last_exc # type: ignore # (This won't actually be unbound)
if asyncio.iscoroutinefunction(func):
return async_wrapper
return sync_wrapper
return decorate

1201
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,73 +0,0 @@
[tool.poetry]
name = "mcstatus"
version = "0.0.0" # version is handled by git tags and poetry-dynamic-versioning
description = "A library to query Minecraft Servers for their status and capabilities."
authors = ["Nathan Adams <dinnerbone@dinnerbone.com>"]
license = "Apache-2.0"
readme = "README.md"
repository = "https://github.com/Dinnerbone/mcstatus"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Topic :: Games/Entertainment",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: System :: Monitoring",
]
packages = [
{ include = "mcstatus" },
{ include = "protocol", from = "mcstatus" },
{ include = "scripts", from = "mcstatus" },
]
[tool.poetry.dependencies]
python = ">=3.7,<4"
asyncio-dgram = "1.2.0"
click = ">=7.1.2,<9"
dnspython = "2.1.0"
[tool.poetry.dev-dependencies]
coverage = "^6.1.1"
pytest = "^6.2.5"
pytest-asyncio = "^0.16.0"
pytest-cov = "^3.0.0"
twine = "^3.5.0"
black = "^22.1.0"
tox = "^3.24.5"
tox-poetry = "^0.4.1"
pyright = "^0.0.13"
typing-extensions = "^4.0.1"
flake8 = "^4.0.1"
flake8-bugbear = "^22.1.11"
flake8-tidy-imports = "^4.6.0"
flake8-import-order = "^0.18.1"
pep8-naming = "^0.12.1"
flake8-annotations = "^2.7.0"
[tool.poetry.scripts]
mcstatus = 'mcstatus.scripts.mcstatus:cli'
[tool.pytest.ini_options]
minversion = "6.0"
addopts = "--strict-markers --doctest-modules --cov=mcstatus --cov-append --cov-branch --cov-report=term-missing -vvv --no-cov-on-fail"
testpaths = ["mcstatus/tests"]
[tool.poetry-dynamic-versioning]
bump = true
enable = true
style = "pep440"
[tool.black]
line-length = 127
[build-system]
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"]
build-backend = "poetry.core.masonry.api"

View File

@@ -1,11 +0,0 @@
{
"exclude": [
// Don't type-check files in the virtual environment, it contains
// external library code which we don't need to check
".venv",
// GitHub validation workflow creates .cache folder to store the
// python environment into, we don't want to check the files in it
// for the same reason as with venv
".cache"
]
}

View File

@@ -1,14 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
poetry install
poetry run tox --recreate
rm -rf dist/
poetry build
echo "Set these environment variables if you wish to not be pestered:"
echo "export POETRY_PYPI_TOKEN_PYPI=my-token"
echo "export POETRY_HTTP_BASIC_PYPI_USERNAME=username"
echo "export POETRY_HTTP_BASIC_PYPI_PASSWORD=password"
poetry publish --no-interaction

41
tox.ini
View File

@@ -1,41 +0,0 @@
[tox]
isolated_build = True
envlist =
format-check,lint,py{37,38,39,310},coverage
[gh-actions]
python =
3.7: py37
3.8: py38
3.9: py39
3.10: py310, coverage
[testenv]
setenv =
COVERAGE_FILE=.coverage.{envname}
commands =
pytest {posargs}
[testenv:format]
commands =
black ./mcstatus
[testenv:format-check]
commands =
black --check ./mcstatus
[testenv:lint]
passenv = HOME
commands =
pyright ./mcstatus
flake8 ./mcstatus
[testenv:coverage]
depends =
py{37,38,39,310}
setenv =
COVERAGE_FILE=.coverage
commands =
coverage combine
coverage report --show-missing --fail-under=80