Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite Query response class to #306's style #536

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
90337a7
Rename `status_response` to `responses`
PerchunPak May 14, 2023
482cd59
Rewrite Query response class to 306's style
PerchunPak May 14, 2023
0a8b293
Merge branch 'master' into rewrite-query-answer-class
PerchunPak Aug 14, 2023
8d74db9
Ignore pyright's warning about wildcard import
PerchunPak Aug 14, 2023
11727d6
Merge remote-tracking branch 'upstream/master' into rewrite-query-ans…
PerchunPak Oct 1, 2023
aa9fec2
Merge remote-tracking branch 'origin/master' into rewrite-query-answe…
PerchunPak Oct 11, 2023
7a883e7
Rename `transform_connection_to_objects` to `_parse_response`
PerchunPak Oct 11, 2023
8e7fed4
Do not use star import
PerchunPak Oct 11, 2023
67a516e
Merge remote-tracking branch 'origin/master' into rewrite-query-answe…
PerchunPak Dec 4, 2023
3a531d0
Merge branch 'master' into rewrite-query-answer-class
PerchunPak Jan 7, 2024
e78a9a8
Return `utils.deprecated` import
PerchunPak Jan 7, 2024
1580c6c
Replace usage of deprecated `QueryResponse.map` to `.map_name` in CLI
PerchunPak Jan 7, 2024
c1de661
Rename `responses` back to `status_response`
PerchunPak Feb 19, 2024
252855a
Merge branch 'master' into rewrite-query-answer-class
PerchunPak Feb 19, 2024
4a3f5d6
Freeze all dataclasses
PerchunPak Feb 19, 2024
8f34ffb
Merge branch 'master' into rewrite-query-answer-class
PerchunPak May 26, 2024
8003e7c
Merge branch 'master' into rewrite-query-answer-class
PerchunPak May 28, 2024
d8b2053
Merge branch 'master' into rewrite-query-answer-class
PerchunPak Jun 3, 2024
3f55640
Merge branch 'master' into rewrite-query-answer-class
PerchunPak Jul 18, 2024
923a083
Replace all usages of `status_response` with `responses`
PerchunPak Jul 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 15 additions & 77 deletions docs/api/basic.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,85 +60,23 @@ For Java Server
:inherited-members:
:exclude-members: build

.. module:: mcstatus.querier

.. class:: QueryResponse
:canonical: mcstatus.querier.QueryResponse

The response object for :meth:`JavaServer.query() <mcstatus.server.JavaServer.query>`.

.. class:: Players
:canonical: mcstatus.querier.QueryResponse.Players

Class for storing information about players on the server.

.. attribute:: online
:type: int
:canonical: mcstatus.querier.QueryResponse.Players.online

The number of online players.

.. attribute:: max
:type: int
:canonical: mcstatus.querier.QueryResponse.Players.max

The maximum allowed number of players (server slots).

.. attribute:: names
:type: list[str]
:canonical: mcstatus.querier.QueryResponse.Players.names

The list of online players.

.. class:: Software
:canonical: mcstatus.querier.QueryResponse.Software

Class for storing information about software on the server.

.. attribute:: version
:type: str
:canonical: mcstatus.querier.QueryResponse.Software.version

The version of the software.

.. attribute:: brand
:type: str
:value: "vanilla"
:canonical: mcstatus.querier.QueryResponse.Software.brand

The brand of the software. Like `Paper <https://papermc.io>`_ or `Spigot <https://www.spigotmc.org>`_.

.. attribute:: plugins
:type: list[str]
:canonical: mcstatus.querier.QueryResponse.Software.plugins

The list of plugins. Can be empty if hidden.

.. attribute:: motd
:type: ~mcstatus.motd.Motd
:canonical: mcstatus.querier.QueryResponse.motd

The MOTD of the server. Also known as description.

.. seealso:: :doc:`/api/motd_parsing`.

.. attribute:: map
:type: str
:canonical: mcstatus.querier.QueryResponse.map

The name of the map.

.. attribute:: players
:type: ~QueryResponse.Players
:canonical: mcstatus.querier.QueryResponse.players

The players information.
.. autoclass:: mcstatus.responses.QueryResponse()
:members:
:undoc-members:
:inherited-members:
:exclude-members: build

.. attribute:: software
:type: ~QueryResponse.Software
:canonical: mcstatus.querier.QueryResponse.software
.. autoclass:: mcstatus.responses.QueryPlayers()
:members:
:undoc-members:
:inherited-members:
:exclude-members: build

The software information.
.. autoclass:: mcstatus.responses.QuerySoftware()
:members:
:undoc-members:
:inherited-members:
:exclude-members: build


For Bedrock Servers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
server = JavaServer.lookup("play.hypixel.net")
query = server.query()

if query.players.names:
print("Players online:", ", ".join(query.players.names))
if query.players.list:
print("Players online:", ", ".join(query.players.list))
else:
status = server.status()

Expand Down
4 changes: 2 additions & 2 deletions mcstatus/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def json(server: JavaServer) -> None:
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["map"] = query_res.map_name
data["plugins"] = query_res.software.plugins
except Exception: # TODO: Check what this actually excepts
pass
Expand All @@ -66,7 +66,7 @@ def query(server: JavaServer) -> None:
print(f"software: v{response.software.version} {response.software.brand}")
print(f"plugins: {response.software.plugins}")
print(f'motd: "{response.motd}"')
print(f"players: {response.players.online}/{response.players.max} {response.players.names}")
print(f"players: {response.players.online}/{response.players.max} {', '.join(response.players.list)}")


def main() -> None:
Expand Down
126 changes: 35 additions & 91 deletions mcstatus/querier.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,9 @@
import random
import re
import struct
from typing import TYPE_CHECKING

from mcstatus.motd import Motd
from mcstatus.protocol.connection import Connection, UDPAsyncSocketConnection, UDPSocketConnection

if TYPE_CHECKING:
from typing_extensions import Self
from mcstatus.responses import QueryResponse, RawQueryResponse


class ServerQuerier:
Expand Down Expand Up @@ -60,91 +56,13 @@ def read_query(self) -> QueryResponse:
self.connection.write(request)

response = self._read_packet()
return QueryResponse.from_connection(response)

return QueryResponse.build(*self._parse_response(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
def _parse_response(self, response: Connection) -> tuple[RawQueryResponse, list[str]]:
"""Transform the connection object (the result) into dict which is passed to the QueryResponse constructor.

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:
"""Documentation for this class is written by hand, without docstrings.

This is because the class is not supposed to be auto-documented.

Please see https://mcstatus.readthedocs.io/en/latest/api/basic/#mcstatus.querier.QueryResponse
for the actual documentation.
"""

# THIS IS SO UNPYTHONIC
# it's staying just because the tests depend on this structure
class Players:
online: int
max: int
names: list[str]

# TODO: It's a bit weird that we accept str for number parameters, just to convert them in init
def __init__(self, online: str | int, max: str | int, names: list[str]):
self.online = int(online)
self.max = int(max)
self.names = names

class Software:
version: str
brand: str
plugins: list[str]

def __init__(self, version: str, plugins: str):
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: Motd
map: str
players: Players
software: Software

def __init__(self, raw: dict[str, str], players: list[str]):
try:
self.raw = raw
self.motd = Motd.parse(raw["hostname"], bedrock=False)
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:
:return: A tuple with two elements. First is `raw` answer and second is list of players.
"""
response.read(len("splitnum") + 1 + 1 + 1)
data = {}

Expand All @@ -170,11 +88,37 @@ def from_connection(cls, response: Connection) -> Self:

response.read(len("player_") + 1 + 1)

players = []
players_list = []
while True:
player = response.read_ascii()
if len(player) == 0:
break
players.append(player)
players_list.append(player)

return RawQueryResponse(**data), players_list

return cls(data, players)

class AsyncServerQuerier(ServerQuerier):
ItsDrike marked this conversation as resolved.
Show resolved Hide resolved
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.build(*self._parse_response(response))
Loading
Loading