diff --git a/docs/api/basic.rst b/docs/api/basic.rst index 554e4ff7..7fbe47c0 100644 --- a/docs/api/basic.rst +++ b/docs/api/basic.rst @@ -34,27 +34,27 @@ These are the classes that you get back after making a request. For Java Server *************** -.. module:: mcstatus.status_response +.. module:: mcstatus.responses -.. autoclass:: mcstatus.status_response.JavaStatusResponse() +.. autoclass:: mcstatus.responses.JavaStatusResponse() :members: :undoc-members: :inherited-members: :exclude-members: build -.. autoclass:: mcstatus.status_response.JavaStatusPlayers() +.. autoclass:: mcstatus.responses.JavaStatusPlayers() :members: :undoc-members: :inherited-members: :exclude-members: build -.. autoclass:: mcstatus.status_response.JavaStatusPlayer() +.. autoclass:: mcstatus.responses.JavaStatusPlayer() :members: :undoc-members: :inherited-members: :exclude-members: build -.. autoclass:: mcstatus.status_response.JavaStatusVersion() +.. autoclass:: mcstatus.responses.JavaStatusVersion() :members: :undoc-members: :inherited-members: @@ -144,22 +144,22 @@ For Java Server For Bedrock Servers ******************* -.. module:: mcstatus.status_response +.. module:: mcstatus.responses :noindex: -.. autoclass:: mcstatus.status_response.BedrockStatusResponse() +.. autoclass:: mcstatus.responses.BedrockStatusResponse() :members: :undoc-members: :inherited-members: :exclude-members: build -.. autoclass:: mcstatus.status_response.BedrockStatusPlayers() +.. autoclass:: mcstatus.responses.BedrockStatusPlayers() :members: :undoc-members: :inherited-members: :exclude-members: build -.. autoclass:: mcstatus.status_response.BedrockStatusVersion() +.. autoclass:: mcstatus.responses.BedrockStatusVersion() :members: :undoc-members: :inherited-members: diff --git a/docs/api/internal.rst b/docs/api/internal.rst index cbb29d62..33563390 100644 --- a/docs/api/internal.rst +++ b/docs/api/internal.rst @@ -49,17 +49,17 @@ versions. They are only documented here for linkable reference to them. :undoc-members: :show-inheritance: -.. autoclass:: mcstatus.status_response.BaseStatusResponse +.. autoclass:: mcstatus.responses.BaseStatusResponse :members: :undoc-members: :show-inheritance: -.. autoclass:: mcstatus.status_response.BaseStatusPlayers +.. autoclass:: mcstatus.responses.BaseStatusPlayers :members: :undoc-members: :show-inheritance: -.. autoclass:: mcstatus.status_response.BaseStatusVersion +.. autoclass:: mcstatus.responses.BaseStatusVersion :members: :undoc-members: :show-inheritance: diff --git a/docs/examples/code/ping_as_java_and_bedrock_in_one_time.py b/docs/examples/code/ping_as_java_and_bedrock_in_one_time.py index 1e09d654..16481b14 100644 --- a/docs/examples/code/ping_as_java_and_bedrock_in_one_time.py +++ b/docs/examples/code/ping_as_java_and_bedrock_in_one_time.py @@ -1,7 +1,7 @@ import asyncio from mcstatus import BedrockServer, JavaServer -from mcstatus.status_response import BedrockStatusResponse, JavaStatusResponse +from mcstatus.responses import BedrockStatusResponse, JavaStatusResponse async def status(host: str) -> JavaStatusResponse | BedrockStatusResponse: diff --git a/docs/examples/ping_as_java_and_bedrock_in_one_time.rst b/docs/examples/ping_as_java_and_bedrock_in_one_time.rst index 3c512165..c18fd8ca 100644 --- a/docs/examples/ping_as_java_and_bedrock_in_one_time.rst +++ b/docs/examples/ping_as_java_and_bedrock_in_one_time.rst @@ -5,8 +5,8 @@ You can easily ping a server as a Java server and as a Bedrock server in one tim .. literalinclude:: code/ping_as_java_and_bedrock_in_one_time.py -As you can see in the code, ``status`` function returns :class:`~mcstatus.status_response.JavaStatusResponse` -or :class:`~mcstatus.status_response.BedrockStatusResponse` object. You can use +As you can see in the code, ``status`` function returns :class:`~mcstatus.responses.JavaStatusResponse` +or :class:`~mcstatus.responses.BedrockStatusResponse` object. You can use :func:`isinstance` checks to access attributes that are only in one of the objects. diff --git a/docs/pages/faq.rst b/docs/pages/faq.rst index 213815b1..70980937 100644 --- a/docs/pages/faq.rst +++ b/docs/pages/faq.rst @@ -57,7 +57,7 @@ How to get server image? On Bedrock, only official servers have a server image. There is no way to get or set an icon to a custom server. For Java servers, you can use -:attr:`status.icon ` +:attr:`status.icon ` attribute. It will return `Base64 `_ encoded PNG image. If you wish to save this image into a file, this is how: diff --git a/mcstatus/bedrock_status.py b/mcstatus/bedrock_status.py index f98156d0..1e5ce67d 100644 --- a/mcstatus/bedrock_status.py +++ b/mcstatus/bedrock_status.py @@ -8,7 +8,7 @@ import asyncio_dgram from mcstatus.address import Address -from mcstatus.status_response import BedrockStatusResponse +from mcstatus.responses import BedrockStatusResponse class BedrockServerStatus: diff --git a/mcstatus/forge_data.py b/mcstatus/forge_data.py index 99412d78..680863c6 100644 --- a/mcstatus/forge_data.py +++ b/mcstatus/forge_data.py @@ -56,7 +56,7 @@ class RawForgeData(TypedDict, total=False): RawForgeData = dict -@dataclass +@dataclass(frozen=True) class ForgeDataChannel: name: str """Channel name and ID (for example ``fml:handshake``).""" @@ -95,7 +95,7 @@ def decode(cls, buffer: Connection, mod_id: str | None = None) -> Self: ) -@dataclass +@dataclass(frozen=True) class ForgeDataMod: name: str marker: str @@ -192,7 +192,7 @@ def read_optimized_buffer(self) -> Connection: return buffer -@dataclass +@dataclass(frozen=True) class ForgeData: fml_network_version: int """Forge Mod Loader network version.""" diff --git a/mcstatus/motd/__init__.py b/mcstatus/motd/__init__.py index d9320363..a36064cd 100644 --- a/mcstatus/motd/__init__.py +++ b/mcstatus/motd/__init__.py @@ -11,7 +11,7 @@ if t.TYPE_CHECKING: from typing_extensions import Self - from mcstatus.status_response import RawJavaResponseMotd, RawJavaResponseMotdWhenDict # circular import + from mcstatus.responses import RawJavaResponseMotd, RawJavaResponseMotdWhenDict # circular import else: RawJavaResponseMotdWhenDict = dict diff --git a/mcstatus/pinger.py b/mcstatus/pinger.py index 17945eaa..83464b42 100644 --- a/mcstatus/pinger.py +++ b/mcstatus/pinger.py @@ -6,7 +6,7 @@ from mcstatus.address import Address from mcstatus.protocol.connection import Connection, TCPAsyncSocketConnection, TCPSocketConnection -from mcstatus.status_response import JavaStatusResponse +from mcstatus.responses import JavaStatusResponse class ServerPinger: diff --git a/mcstatus/responses.py b/mcstatus/responses.py new file mode 100644 index 00000000..6786fafe --- /dev/null +++ b/mcstatus/responses.py @@ -0,0 +1,342 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Any, TYPE_CHECKING + +from mcstatus.forge_data import ForgeData, RawForgeData +from mcstatus.motd import Motd + +if TYPE_CHECKING: + from typing_extensions import NotRequired, Self, TypeAlias, TypedDict + + class RawJavaResponsePlayer(TypedDict): + name: str + id: str + + class RawJavaResponsePlayers(TypedDict): + online: int + max: int + sample: NotRequired[list[RawJavaResponsePlayer]] + + class RawJavaResponseVersion(TypedDict): + name: str + protocol: int + + class RawJavaResponseMotdWhenDict(TypedDict, total=False): + text: str # only present if `translate` is set + translate: str # same to the above field + extra: list[RawJavaResponseMotdWhenDict | str] + + color: str + bold: bool + strikethrough: bool + italic: bool + underlined: bool + obfuscated: bool + + RawJavaResponseMotd: TypeAlias = "RawJavaResponseMotdWhenDict | list[RawJavaResponseMotdWhenDict | str] | str" + + class RawJavaResponse(TypedDict): + description: RawJavaResponseMotd + players: RawJavaResponsePlayers + version: RawJavaResponseVersion + favicon: NotRequired[str] + forgeData: NotRequired[RawForgeData] + modinfo: NotRequired[RawForgeData] + enforcesSecureChat: NotRequired[bool] + +else: + RawJavaResponsePlayer = dict + RawJavaResponsePlayers = dict + RawJavaResponseVersion = dict + RawJavaResponseMotdWhenDict = dict + RawJavaResponse = dict + +from mcstatus.utils import deprecated + +__all__ = [ + "BaseStatusPlayers", + "BaseStatusResponse", + "BaseStatusVersion", + "BedrockStatusPlayers", + "BedrockStatusResponse", + "BedrockStatusVersion", + "JavaStatusPlayer", + "JavaStatusPlayers", + "JavaStatusResponse", + "JavaStatusVersion", +] + + +@dataclass(frozen=True) +class BaseStatusResponse(ABC): + """Class for storing shared data from a status response.""" + + players: BaseStatusPlayers + """The players information.""" + version: BaseStatusVersion + """The version information.""" + motd: Motd + """Message Of The Day. Also known as description. + + .. seealso:: :doc:`/api/motd_parsing`. + """ + latency: float + """Latency between a server and the client (you). In milliseconds.""" + + @property + def description(self) -> str: + """Alias to the :meth:`mcstatus.motd.Motd.to_minecraft` method.""" + return self.motd.to_minecraft() + + @classmethod + @abstractmethod + def build(cls, *args, **kwargs) -> Self: + """Build BaseStatusResponse and check is it valid. + + :param args: Arguments in specific realisation. + :param kwargs: Keyword arguments in specific realisation. + :return: :class:`BaseStatusResponse` object. + """ + raise NotImplementedError("You can't use abstract methods.") + + +@dataclass(frozen=True) +class JavaStatusResponse(BaseStatusResponse): + """The response object for :meth:`JavaServer.status() `.""" + + raw: RawJavaResponse + """Raw response from the server. + + This is :class:`~typing.TypedDict` actually, please see sources to find what is here. + """ + players: JavaStatusPlayers + version: JavaStatusVersion + enforces_secure_chat: bool | None + """Whether the server enforces secure chat (every message is signed up with a key). + + .. seealso:: + `Signed Chat explanation `_, + `22w17a changelog, where this was added `_. + + .. versionadded:: 11.1.0 + """ + icon: str | None + """The icon of the server. In `Base64 `_ encoded PNG image format. + + .. seealso:: :ref:`pages/faq:how to get server image?` + """ + forge_data: ForgeData | None + """Forge mod data (mod list, channels, etc). Only present if this is a forge (modded) server.""" + + @classmethod + def build(cls, raw: RawJavaResponse, latency: float = 0) -> Self: + """Build JavaStatusResponse and check is it valid. + + :param raw: Raw response :class:`dict`. + :param latency: Time that server took to response (in milliseconds). + :raise ValueError: If the required keys (``players``, ``version``, ``description``) are not present. + :raise TypeError: + If the required keys (``players`` - :class:`dict`, ``version`` - :class:`dict`, + ``description`` - :class:`str`) are not of the expected type. + :return: :class:`JavaStatusResponse` object. + """ + forge_data: ForgeData | None = None + if "forgeData" in raw or "modinfo" in raw: + raw_forge = raw.get("forgeData") or raw.get("modinfo") + assert raw_forge is not None + forge_data = ForgeData.build(raw_forge) + + return cls( + raw=raw, + players=JavaStatusPlayers.build(raw["players"]), + version=JavaStatusVersion.build(raw["version"]), + motd=Motd.parse(raw["description"], bedrock=False), + enforces_secure_chat=raw.get("enforcesSecureChat"), + icon=raw.get("favicon"), + latency=latency, + forge_data=forge_data, + ) + + +@dataclass(frozen=True) +class BedrockStatusResponse(BaseStatusResponse): + """The response object for :meth:`BedrockServer.status() `.""" + + players: BedrockStatusPlayers + version: BedrockStatusVersion + map_name: str | None + """The name of the map.""" + gamemode: str | None + """The name of the gamemode on the server.""" + + @classmethod + def build(cls, decoded_data: list[Any], latency: float) -> Self: + """Build BaseStatusResponse and check is it valid. + + :param decoded_data: Raw decoded response object. + :param latency: Latency of the request. + :return: :class:`BedrockStatusResponse` object. + """ + + try: + map_name = decoded_data[7] + except IndexError: + map_name = None + try: + gamemode = decoded_data[8] + except IndexError: + gamemode = None + + return cls( + players=BedrockStatusPlayers( + online=int(decoded_data[4]), + max=int(decoded_data[5]), + ), + version=BedrockStatusVersion( + name=decoded_data[3], + protocol=int(decoded_data[2]), + brand=decoded_data[0], + ), + motd=Motd.parse(decoded_data[1], bedrock=True), + latency=latency, + map_name=map_name, + gamemode=gamemode, + ) + + +@dataclass(frozen=True) +class BaseStatusPlayers(ABC): + """Class for storing information about players on the server.""" + + online: int + """Current number of online players.""" + max: int + """The maximum allowed number of players (aka server slots).""" + + +@dataclass(frozen=True) +class JavaStatusPlayers(BaseStatusPlayers): + """Class for storing information about players on the server.""" + + sample: list[JavaStatusPlayer] | None + """List of players, who are online. If server didn't provide this, it will be :obj:`None`. + + Actually, this is what appears when you hover over the slot count on the multiplayer screen. + + .. note:: + It's often empty or even contains some advertisement, because the specific server implementations or plugins can + disable providing this information or even change it to something custom. + + There is nothing that ``mcstatus`` can to do here if the player sample was modified/disabled like this. + """ + + @classmethod + def build(cls, raw: RawJavaResponsePlayers) -> Self: + """Build :class:`JavaStatusPlayers` from raw response :class:`dict`. + + :param raw: Raw response :class:`dict`. + :raise ValueError: If the required keys (``online``, ``max``) are not present. + :raise TypeError: + If the required keys (``online`` - :class:`int`, ``max`` - :class:`int`, + ``sample`` - :class:`list`) are not of the expected type. + :return: :class:`JavaStatusPlayers` object. + """ + sample = None + if "sample" in raw: + sample = [JavaStatusPlayer.build(player) for player in raw["sample"]] + return cls( + online=raw["online"], + max=raw["max"], + sample=sample, + ) + + +@dataclass(frozen=True) +class BedrockStatusPlayers(BaseStatusPlayers): + """Class for storing information about players on the server.""" + + +@dataclass(frozen=True) +class JavaStatusPlayer: + """Class with information about a single player.""" + + name: str + """Name of the player.""" + id: str + """ID of the player (in `UUID `_ format).""" + + @property + def uuid(self) -> str: + """Alias to :attr:`.id` field.""" + return self.id + + @classmethod + def build(cls, raw: RawJavaResponsePlayer) -> Self: + """Build :class:`JavaStatusPlayer` from raw response :class:`dict`. + + :param raw: Raw response :class:`dict`. + :raise ValueError: If the required keys (``name``, ``id``) are not present. + :raise TypeError: If the required keys (``name`` - :class:`str`, ``id`` - :class:`str`) + are not of the expected type. + :return: :class:`JavaStatusPlayer` object. + """ + return cls(name=raw["name"], id=raw["id"]) + + +@dataclass(frozen=True) +class BaseStatusVersion(ABC): + """A class for storing version information.""" + + name: str + """The version name, like ``1.19.3``. + + See `Minecraft wiki `__ + for complete list. + """ + protocol: int + """The protocol version, like ``761``. + + See `Minecraft wiki `__. + """ + + +@dataclass(frozen=True) +class JavaStatusVersion(BaseStatusVersion): + """A class for storing version information.""" + + @classmethod + def build(cls, raw: RawJavaResponseVersion) -> Self: + """Build :class:`JavaStatusVersion` from raw response dict. + + :param raw: Raw response :class:`dict`. + :raise ValueError: If the required keys (``name``, ``protocol``) are not present. + :raise TypeError: If the required keys (``name`` - :class:`str`, ``protocol`` - :class:`int`) + are not of the expected type. + :return: :class:`JavaStatusVersion` object. + """ + return cls(name=raw["name"], protocol=raw["protocol"]) + + +@dataclass(frozen=True) +class BedrockStatusVersion(BaseStatusVersion): + """A class for storing version information.""" + + name: str + """The version name, like ``1.19.60``. + + See `Minecraft wiki `__ + for complete list. + """ + brand: str + """``MCPE`` or ``MCEE`` for Education Edition.""" + + @property + @deprecated(replacement="name", date="2023-12") + def version(self) -> str: + """ + .. deprecated:: 11.0.0 + Will be removed 2023-12, use :attr:`.name` instead. + """ + return self.name diff --git a/mcstatus/server.py b/mcstatus/server.py index e5b5ebbe..7bb57b4c 100644 --- a/mcstatus/server.py +++ b/mcstatus/server.py @@ -13,7 +13,7 @@ UDPSocketConnection, ) from mcstatus.querier import AsyncServerQuerier, QueryResponse, ServerQuerier -from mcstatus.status_response import BedrockStatusResponse, JavaStatusResponse +from mcstatus.responses import BedrockStatusResponse, JavaStatusResponse from mcstatus.utils import retry if TYPE_CHECKING: @@ -120,7 +120,7 @@ def status(self, **kwargs) -> JavaStatusResponse: """Checks the status of a Minecraft Java Edition server via the status protocol. :param kwargs: Passed to a :class:`~mcstatus.pinger.ServerPinger` instance. - :return: Status information in a :class:`~mcstatus.status_response.JavaStatusResponse` instance. + :return: Status information in a :class:`~mcstatus.responses.JavaStatusResponse` instance. """ with TCPSocketConnection(self.address, self.timeout) as connection: @@ -137,7 +137,7 @@ async def async_status(self, **kwargs) -> JavaStatusResponse: """Asynchronously checks the status of a Minecraft Java Edition server via the status protocol. :param kwargs: Passed to a :class:`~mcstatus.pinger.AsyncServerPinger` instance. - :return: Status information in a :class:`~mcstatus.status_response.JavaStatusResponse` instance. + :return: Status information in a :class:`~mcstatus.responses.JavaStatusResponse` instance. """ async with TCPAsyncSocketConnection(self.address, self.timeout) as connection: @@ -193,7 +193,7 @@ def status(self, **kwargs) -> BedrockStatusResponse: """Checks the status of a Minecraft Bedrock Edition server. :param kwargs: Passed to a :class:`~mcstatus.bedrock_status.BedrockServerStatus` instance. - :return: Status information in a :class:`~mcstatus.status_response.BedrockStatusResponse` instance. + :return: Status information in a :class:`~mcstatus.responses.BedrockStatusResponse` instance. """ return BedrockServerStatus(self.address, self.timeout, **kwargs).read_status() @@ -202,6 +202,6 @@ async def async_status(self, **kwargs) -> BedrockStatusResponse: """Asynchronously checks the status of a Minecraft Bedrock Edition server. :param kwargs: Passed to a :class:`~mcstatus.bedrock_status.BedrockServerStatus` instance. - :return: Status information in a :class:`~mcstatus.status_response.BedrockStatusResponse` instance. + :return: Status information in a :class:`~mcstatus.responses.BedrockStatusResponse` instance. """ return await BedrockServerStatus(self.address, self.timeout, **kwargs).read_status_async() diff --git a/mcstatus/status_response.py b/mcstatus/status_response.py index 85d44422..1d02f400 100644 --- a/mcstatus/status_response.py +++ b/mcstatus/status_response.py @@ -1,59 +1,17 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from dataclasses import dataclass -from typing import Any, TYPE_CHECKING - -from mcstatus.forge_data import ForgeData, RawForgeData -from mcstatus.motd import Motd - -if TYPE_CHECKING: - from typing_extensions import NotRequired, Self, TypeAlias, TypedDict - - class RawJavaResponsePlayer(TypedDict): - name: str - id: str - - class RawJavaResponsePlayers(TypedDict): - online: int - max: int - sample: NotRequired[list[RawJavaResponsePlayer]] - - class RawJavaResponseVersion(TypedDict): - name: str - protocol: int - - class RawJavaResponseMotdWhenDict(TypedDict, total=False): - text: str # only present if `translate` is set - translate: str # same to the above field - extra: list[RawJavaResponseMotdWhenDict | str] - - color: str - bold: bool - strikethrough: bool - italic: bool - underlined: bool - obfuscated: bool - - RawJavaResponseMotd: TypeAlias = "RawJavaResponseMotdWhenDict | list[RawJavaResponseMotdWhenDict | str] | str" - - class RawJavaResponse(TypedDict): - description: RawJavaResponseMotd - players: RawJavaResponsePlayers - version: RawJavaResponseVersion - favicon: NotRequired[str] - forgeData: NotRequired[RawForgeData] - modinfo: NotRequired[RawForgeData] - enforcesSecureChat: NotRequired[bool] - -else: - RawJavaResponsePlayer = dict - RawJavaResponsePlayers = dict - RawJavaResponseVersion = dict - RawJavaResponseMotdWhenDict = dict - RawJavaResponse = dict - - +from mcstatus.responses import ( + BaseStatusPlayers, + BaseStatusResponse, + BaseStatusVersion, + BedrockStatusPlayers, + BedrockStatusResponse, + BedrockStatusVersion, + JavaStatusPlayer, + JavaStatusPlayers, + JavaStatusResponse, + JavaStatusVersion, +) + +# __all__ is frozen on the moment of deprecation __all__ = [ "BaseStatusPlayers", "BaseStatusResponse", @@ -68,265 +26,10 @@ class RawJavaResponse(TypedDict): ] -@dataclass(frozen=True) -class BaseStatusResponse(ABC): - """Class for storing shared data from a status response.""" - - players: BaseStatusPlayers - """The players information.""" - version: BaseStatusVersion - """The version information.""" - motd: Motd - """Message Of The Day. Also known as description. - - .. seealso:: :doc:`/api/motd_parsing`. - """ - latency: float - """Latency between a server and the client (you). In milliseconds.""" - - @property - def description(self) -> str: - """Alias to the :meth:`mcstatus.motd.Motd.to_minecraft` method.""" - return self.motd.to_minecraft() - - @classmethod - @abstractmethod - def build(cls, *args, **kwargs) -> Self: - """Build BaseStatusResponse and check is it valid. - - :param args: Arguments in specific realisation. - :param kwargs: Keyword arguments in specific realisation. - :return: :class:`BaseStatusResponse` object. - """ - raise NotImplementedError("You can't use abstract methods.") - - -@dataclass(frozen=True) -class JavaStatusResponse(BaseStatusResponse): - """The response object for :meth:`JavaServer.status() `.""" - - raw: RawJavaResponse - """Raw response from the server. - - This is :class:`~typing.TypedDict` actually, please see sources to find what is here. - """ - players: JavaStatusPlayers - version: JavaStatusVersion - enforces_secure_chat: bool | None - """Whether the server enforces secure chat (every message is signed up with a key). - - .. seealso:: - `Signed Chat explanation `_, - `22w17a changelog, where this was added `_. - - .. versionadded:: 11.1.0 - """ - icon: str | None - """The icon of the server. In `Base64 `_ encoded PNG image format. - - .. seealso:: :ref:`pages/faq:how to get server image?` - """ - forge_data: ForgeData | None - """Forge mod data (mod list, channels, etc). Only present if this is a forge (modded) server.""" - - @classmethod - def build(cls, raw: RawJavaResponse, latency: float = 0) -> Self: - """Build JavaStatusResponse and check is it valid. - - :param raw: Raw response :class:`dict`. - :param latency: Time that server took to response (in milliseconds). - :raise ValueError: If the required keys (``players``, ``version``, ``description``) are not present. - :raise TypeError: - If the required keys (``players`` - :class:`dict`, ``version`` - :class:`dict`, - ``description`` - :class:`str`) are not of the expected type. - :return: :class:`JavaStatusResponse` object. - """ - forge_data: ForgeData | None = None - if "forgeData" in raw or "modinfo" in raw: - raw_forge = raw.get("forgeData") or raw.get("modinfo") - assert raw_forge is not None - forge_data = ForgeData.build(raw_forge) - - return cls( - raw=raw, - players=JavaStatusPlayers.build(raw["players"]), - version=JavaStatusVersion.build(raw["version"]), - motd=Motd.parse(raw["description"], bedrock=False), - enforces_secure_chat=raw.get("enforcesSecureChat"), - icon=raw.get("favicon"), - latency=latency, - forge_data=forge_data, - ) - - -@dataclass(frozen=True) -class BedrockStatusResponse(BaseStatusResponse): - """The response object for :meth:`BedrockServer.status() `.""" - - players: BedrockStatusPlayers - version: BedrockStatusVersion - map_name: str | None - """The name of the map.""" - gamemode: str | None - """The name of the gamemode on the server.""" - - @classmethod - def build(cls, decoded_data: list[Any], latency: float) -> Self: - """Build BaseStatusResponse and check is it valid. - - :param decoded_data: Raw decoded response object. - :param latency: Latency of the request. - :return: :class:`BedrockStatusResponse` object. - """ - - try: - map_name = decoded_data[7] - except IndexError: - map_name = None - try: - gamemode = decoded_data[8] - except IndexError: - gamemode = None - - return cls( - players=BedrockStatusPlayers( - online=int(decoded_data[4]), - max=int(decoded_data[5]), - ), - version=BedrockStatusVersion( - name=decoded_data[3], - protocol=int(decoded_data[2]), - brand=decoded_data[0], - ), - motd=Motd.parse(decoded_data[1], bedrock=True), - latency=latency, - map_name=map_name, - gamemode=gamemode, - ) - - -@dataclass(frozen=True) -class BaseStatusPlayers(ABC): - """Class for storing information about players on the server.""" - - online: int - """Current number of online players.""" - max: int - """The maximum allowed number of players (aka server slots).""" - - -@dataclass(frozen=True) -class JavaStatusPlayers(BaseStatusPlayers): - """Class for storing information about players on the server.""" - - sample: list[JavaStatusPlayer] | None - """List of players, who are online. If server didn't provide this, it will be :obj:`None`. - - Actually, this is what appears when you hover over the slot count on the multiplayer screen. - - .. note:: - It's often empty or even contains some advertisement, because the specific server implementations or plugins can - disable providing this information or even change it to something custom. - - There is nothing that ``mcstatus`` can to do here if the player sample was modified/disabled like this. - """ - - @classmethod - def build(cls, raw: RawJavaResponsePlayers) -> Self: - """Build :class:`JavaStatusPlayers` from raw response :class:`dict`. - - :param raw: Raw response :class:`dict`. - :raise ValueError: If the required keys (``online``, ``max``) are not present. - :raise TypeError: - If the required keys (``online`` - :class:`int`, ``max`` - :class:`int`, - ``sample`` - :class:`list`) are not of the expected type. - :return: :class:`JavaStatusPlayers` object. - """ - sample = None - if "sample" in raw: - sample = [JavaStatusPlayer.build(player) for player in raw["sample"]] - return cls( - online=raw["online"], - max=raw["max"], - sample=sample, - ) - - -@dataclass(frozen=True) -class BedrockStatusPlayers(BaseStatusPlayers): - """Class for storing information about players on the server.""" - - -@dataclass(frozen=True) -class JavaStatusPlayer: - """Class with information about a single player.""" - - name: str - """Name of the player.""" - id: str - """ID of the player (in `UUID `_ format).""" - - @property - def uuid(self) -> str: - """Alias to :attr:`.id` field.""" - return self.id - - @classmethod - def build(cls, raw: RawJavaResponsePlayer) -> Self: - """Build :class:`JavaStatusPlayer` from raw response :class:`dict`. - - :param raw: Raw response :class:`dict`. - :raise ValueError: If the required keys (``name``, ``id``) are not present. - :raise TypeError: If the required keys (``name`` - :class:`str`, ``id`` - :class:`str`) - are not of the expected type. - :return: :class:`JavaStatusPlayer` object. - """ - return cls(name=raw["name"], id=raw["id"]) - - -@dataclass(frozen=True) -class BaseStatusVersion(ABC): - """A class for storing version information.""" - - name: str - """The version name, like ``1.19.3``. - - See `Minecraft wiki `__ - for complete list. - """ - protocol: int - """The protocol version, like ``761``. - - See `Minecraft wiki `__. - """ - - -@dataclass(frozen=True) -class JavaStatusVersion(BaseStatusVersion): - """A class for storing version information.""" - - @classmethod - def build(cls, raw: RawJavaResponseVersion) -> Self: - """Build :class:`JavaStatusVersion` from raw response dict. - - :param raw: Raw response :class:`dict`. - :raise ValueError: If the required keys (``name``, ``protocol``) are not present. - :raise TypeError: If the required keys (``name`` - :class:`str`, ``protocol`` - :class:`int`) - are not of the expected type. - :return: :class:`JavaStatusVersion` object. - """ - return cls(name=raw["name"], protocol=raw["protocol"]) - - -@dataclass(frozen=True) -class BedrockStatusVersion(BaseStatusVersion): - """A class for storing version information.""" - - name: str - """The version name, like ``1.19.60``. +import warnings - See `Minecraft wiki `__ - for complete list. - """ - brand: str - """``MCPE`` or ``MCEE`` for Education Edition.""" +warnings.warn( + "`mcstatus.status_response` is deprecated, and will be removed at 2024-12, use `mcstatus.responses` instead", + DeprecationWarning, + stacklevel=2, +) diff --git a/tests/motd/test_motd.py b/tests/motd/test_motd.py index 151b72d7..3012d603 100644 --- a/tests/motd/test_motd.py +++ b/tests/motd/test_motd.py @@ -4,7 +4,7 @@ from mcstatus.motd import Motd from mcstatus.motd.components import Formatting, MinecraftColor, TranslationTag, WebColor -from mcstatus.status_response import RawJavaResponseMotdWhenDict +from mcstatus.responses import RawJavaResponseMotdWhenDict class TestMotdParse: diff --git a/tests/motd/test_transformers.py b/tests/motd/test_transformers.py index 5d9fce0a..0c73f9e4 100644 --- a/tests/motd/test_transformers.py +++ b/tests/motd/test_transformers.py @@ -9,7 +9,7 @@ from mcstatus.motd.transformers import AnsiTransformer, HtmlTransformer, MinecraftTransformer, PlainTransformer if typing.TYPE_CHECKING: - from mcstatus.status_response import RawJavaResponseMotd + from mcstatus.responses import RawJavaResponseMotd class TestMotdPlain: diff --git a/tests/status_response/__init__.py b/tests/responses/__init__.py similarity index 93% rename from tests/status_response/__init__.py rename to tests/responses/__init__.py index ca9a8d63..59c09b36 100644 --- a/tests/status_response/__init__.py +++ b/tests/responses/__init__.py @@ -3,18 +3,18 @@ import abc from typing import Any, TypeVar, cast -from mcstatus.status_response import BaseStatusResponse +from mcstatus.responses import BaseStatusResponse -__all__ = ["BaseStatusResponseTest"] -_T = TypeVar("_T", bound="type[BaseStatusResponseTest]") +__all__ = ["BaseResponseTest"] +_T = TypeVar("_T", bound="type[BaseResponseTest]") -class BaseStatusResponseTest(abc.ABC): +class BaseResponseTest(abc.ABC): EXPECTED_VALUES: list[tuple[str, Any]] | None = None EXPECTED_TYPES: list[tuple[str, type]] | None = None ATTRIBUTES_IN: list[str] | None = None # if we don't specify item in raw answer, target field will be None - # first element is a list with fields to remove, and attribute that + # a first element is a list with fields to remove, and attribute that # must be None. a dict is a raw answer to pass into `build` method OPTIONAL_FIELDS: tuple[list[tuple[str, str]], dict[str, Any]] | None = None @@ -89,7 +89,7 @@ def _marks_table(self) -> dict[str, tuple[str, tuple[Any, ...]]]: @staticmethod def construct(class_: _T) -> _T: - instance: BaseStatusResponseTest = class_() # type: ignore + instance: BaseResponseTest = class_() # type: ignore instance._validate() for implementation_name, meet_dependencies in instance._dependency_table().items(): if not meet_dependencies: diff --git a/tests/status_response/conftest.py b/tests/responses/conftest.py similarity index 83% rename from tests/status_response/conftest.py rename to tests/responses/conftest.py index 153b2025..b2b29f50 100644 --- a/tests/status_response/conftest.py +++ b/tests/responses/conftest.py @@ -5,11 +5,11 @@ import pytest from _pytest.python import Function, Metafunc -from tests.status_response import BaseStatusResponseTest +from tests.responses import BaseResponseTest def pytest_generate_tests(metafunc: Metafunc) -> None: - if issubclass(typing.cast(type, metafunc.cls), BaseStatusResponseTest): + if issubclass(typing.cast(type, metafunc.cls), BaseResponseTest): instance = typing.cast(type, metafunc.cls)() if metafunc.definition.name not in instance._marks_table().keys(): return @@ -22,7 +22,7 @@ def pytest_generate_tests(metafunc: Metafunc) -> None: def pytest_collection_modifyitems(items: list[Function]) -> None: for item in items: - if isinstance(item.instance, BaseStatusResponseTest): + if isinstance(item.instance, BaseResponseTest): if item.obj.__name__ not in item.instance._marks_table().keys(): continue diff --git a/tests/status_response/test_bedrock.py b/tests/responses/test_bedrock.py similarity index 82% rename from tests/status_response/test_bedrock.py rename to tests/responses/test_bedrock.py index 7b8cd2c4..8281b24b 100644 --- a/tests/status_response/test_bedrock.py +++ b/tests/responses/test_bedrock.py @@ -1,8 +1,8 @@ from pytest import fixture, mark from mcstatus.motd import Motd -from mcstatus.status_response import BedrockStatusPlayers, BedrockStatusResponse, BedrockStatusVersion -from tests.status_response import BaseStatusResponseTest +from mcstatus.responses import BedrockStatusPlayers, BedrockStatusResponse, BedrockStatusVersion +from tests.responses import BaseResponseTest @fixture(scope="module") @@ -27,8 +27,8 @@ def build(): ) -@BaseStatusResponseTest.construct -class TestBedrockStatusResponse(BaseStatusResponseTest): +@BaseResponseTest.construct +class TestBedrockStatusResponse(BaseResponseTest): EXPECTED_VALUES = [ ("motd", Motd.parse("§r§4G§r§6a§r§ey§r§2B§r§1o§r§9w§r§ds§r§4e§r§6r", bedrock=True)), ("latency", 123.0), @@ -66,8 +66,8 @@ def test_optional_parameters_is_none(self, field, pop_index): assert getattr(build, field) is None -@BaseStatusResponseTest.construct -class TestBedrockStatusPlayers(BaseStatusResponseTest): +@BaseResponseTest.construct +class TestBedrockStatusPlayers(BaseResponseTest): EXPECTED_VALUES = [("online", 1), ("max", 69)] @fixture(scope="class") @@ -75,8 +75,8 @@ def build(self, build): return build.players -@BaseStatusResponseTest.construct -class TestBedrockStatusVersion(BaseStatusResponseTest): +@BaseResponseTest.construct +class TestBedrockStatusVersion(BaseResponseTest): EXPECTED_VALUES = [("name", "1.18.100500"), ("protocol", 422), ("brand", "MCPE")] @fixture(scope="class") diff --git a/tests/status_response/test_forge_data.py b/tests/responses/test_forge_data.py similarity index 99% rename from tests/status_response/test_forge_data.py rename to tests/responses/test_forge_data.py index 11b8857b..dd81414c 100644 --- a/tests/status_response/test_forge_data.py +++ b/tests/responses/test_forge_data.py @@ -1,11 +1,11 @@ import pytest from mcstatus.forge_data import ForgeData, ForgeDataChannel, ForgeDataMod, RawForgeData -from tests.status_response import BaseStatusResponseTest +from tests.responses import BaseResponseTest -@BaseStatusResponseTest.construct -class TestForgeDataV1(BaseStatusResponseTest): +@BaseResponseTest.construct +class TestForgeDataV1(BaseResponseTest): RAW = { "type": "FML", "modList": [ @@ -39,8 +39,8 @@ def build(self) -> ForgeData: return ForgeData.build(self.RAW) # type: ignore # dict[str, Unknown] cannot be assigned to TypedDict -@BaseStatusResponseTest.construct -class TestForgeDataV2(BaseStatusResponseTest): +@BaseResponseTest.construct +class TestForgeDataV2(BaseResponseTest): RAW = { "fmlNetworkVersion": 2, "channels": [ @@ -63,8 +63,8 @@ def build(self) -> ForgeData: return ForgeData.build(self.RAW) # type: ignore # dict[str, Unknown] cannot be assigned to TypedDict -@BaseStatusResponseTest.construct -class TestForgeDataV3(BaseStatusResponseTest): +@BaseResponseTest.construct +class TestForgeDataV3(BaseResponseTest): RAW = { "channels": [], "mods": [], @@ -104,8 +104,8 @@ def build(self) -> ForgeData: return ForgeData.build(self.RAW) # type: ignore # dict[str, Unknown] cannot be assigned to TypedDict -@BaseStatusResponseTest.construct -class TestForgeData(BaseStatusResponseTest): +@BaseResponseTest.construct +class TestForgeData(BaseResponseTest): EXPECTED_VALUES = [ ("fml_network_version", 3), ( diff --git a/tests/status_response/test_java.py b/tests/responses/test_java.py similarity index 86% rename from tests/status_response/test_java.py rename to tests/responses/test_java.py index 5156d32d..256cafcd 100644 --- a/tests/status_response/test_java.py +++ b/tests/responses/test_java.py @@ -1,12 +1,12 @@ import pytest from mcstatus.motd import Motd -from mcstatus.status_response import JavaStatusPlayer, JavaStatusPlayers, JavaStatusResponse, JavaStatusVersion -from tests.status_response import BaseStatusResponseTest +from mcstatus.responses import JavaStatusPlayer, JavaStatusPlayers, JavaStatusResponse, JavaStatusVersion +from tests.responses import BaseResponseTest -@BaseStatusResponseTest.construct -class TestJavaStatusResponse(BaseStatusResponseTest): +@BaseResponseTest.construct +class TestJavaStatusResponse(BaseResponseTest): RAW = { "players": {"max": 20, "online": 0}, "version": {"name": "1.8-pre1", "protocol": 44}, @@ -41,8 +41,8 @@ def build(self) -> JavaStatusResponse: return JavaStatusResponse.build(self.RAW) # type: ignore # dict[str, Unknown] cannot be assigned to TypedDict -@BaseStatusResponseTest.construct -class TestJavaStatusPlayers(BaseStatusResponseTest): +@BaseResponseTest.construct +class TestJavaStatusPlayers(BaseResponseTest): EXPECTED_VALUES = [ ("max", 20), ("online", 0), @@ -86,8 +86,8 @@ def test_empty_sample_turns_into_empty_list(self) -> None: assert JavaStatusPlayers.build({"max": 20, "online": 0, "sample": []}).sample == [] -@BaseStatusResponseTest.construct -class TestJavaStatusPlayer(BaseStatusResponseTest): +@BaseResponseTest.construct +class TestJavaStatusPlayer(BaseResponseTest): EXPECTED_VALUES = [("name", "foo"), ("id", "0b3717c4-f45d-47c8-b8e2-3d9ff6f93a89")] @pytest.fixture(scope="class") @@ -101,8 +101,8 @@ def test_id_field_the_same_as_uuid(self) -> None: assert build.uuid is unique -@BaseStatusResponseTest.construct -class TestJavaStatusVersion(BaseStatusResponseTest): +@BaseResponseTest.construct +class TestJavaStatusVersion(BaseResponseTest): EXPECTED_VALUES = [("name", "1.8-pre1"), ("protocol", 44)] @pytest.fixture(scope="class") diff --git a/tests/status_response/test_shared.py b/tests/responses/test_shared.py similarity index 81% rename from tests/status_response/test_shared.py rename to tests/responses/test_shared.py index 5183695a..acd6f884 100644 --- a/tests/status_response/test_shared.py +++ b/tests/responses/test_shared.py @@ -1,6 +1,6 @@ from pytest import raises -from mcstatus.status_response import BaseStatusResponse +from mcstatus.responses import BaseStatusResponse class TestMCStatusResponse: diff --git a/tests/test_bedrock_status.py b/tests/test_bedrock_status.py index be82c393..863e27a1 100644 --- a/tests/test_bedrock_status.py +++ b/tests/test_bedrock_status.py @@ -6,7 +6,7 @@ from mcstatus.address import Address from mcstatus.bedrock_status import BedrockServerStatus -from mcstatus.status_response import BedrockStatusResponse +from mcstatus.responses import BedrockStatusResponse def test_bedrock_response_is_expected_type():