pyearthmc 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyearthmc
3
+ Version: 0.2.0
4
+ Summary: A complete and type-safe Python client for EMC (EarthMC Minecraft Server)
5
+ Author: RafaCabra
6
+ License-Expression: MIT
7
+ Requires-Dist: httpx>=0.28.1
8
+ Requires-Dist: pydantic>=2.13.5
9
+ Requires-Dist: rich>=15.0.0
10
+ Requires-Python: >=3.14
11
+ Description-Content-Type: text/markdown
12
+
13
+ # PyEMC
14
+
15
+ A complete Python client for the [EarthMC](https://earthmc.net/)
16
+ Minecraft server API.
17
+
18
+ PyEMC models the raw EarthMC JSON endpoints as validated [Pydantic v2](https://docs.pydantic.dev/)
19
+ objects and wraps them behind an async HTTP client. You get autocompletion,
20
+ type checking, and data validation out of the box.
21
+
22
+ ## Features
23
+
24
+ - **Type-safe:** Every request and response is a Pydantic v2 model. Typos and
25
+ malformed payloads fail fast instead of silently breaking later.
26
+ - **Async:** Built on `httpx.AsyncClient` for concurrent, requests.
27
+ - **Two request modes:** `overview` (a light `GET` returning a name/UUID list)
28
+ and `detailed` (a `POST` returning full entity data).
29
+ - **Computed fields:** Extra data is generated on top of the raw API response
30
+ (e.g. when the response arrived, and the current vote-party vote count).
31
+
32
+ ## Requirements
33
+
34
+ - Python **3.14+**
35
+ - An internet connection to reach `https://api.earthmc.net`
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install pyearthmc
41
+ ```
42
+
43
+ Or with [uv](https://docs.astral.sh/uv/):
44
+
45
+ ```bash
46
+ uv add pyearthmc
47
+ ```
48
+
49
+ ## Quickstart
50
+
51
+ ```python
52
+ import asyncio
53
+
54
+ import httpx
55
+
56
+ from pyearthmc import EmcClient
57
+
58
+
59
+ async def main():
60
+ async with httpx.AsyncClient() as client:
61
+ emc = EmcClient(client)
62
+
63
+ server = await emc.get_server_info()
64
+ print(server.version)
65
+ print(server.voteParty.numVotes)
66
+
67
+
68
+ if __name__ == "__main__":
69
+ asyncio.run(main())
70
+ ```
71
+
72
+ ## Usage
73
+
74
+ ### Overview mode
75
+
76
+ Returns a plain list of `NamedEntity` (`name` and `uuid`).
77
+
78
+ ```python
79
+ towns = await emc.get_towns() # overview (default)
80
+ nations = await emc.get_nations()
81
+ players = await emc.get_players()
82
+ ```
83
+
84
+ ### Detailed mode (full data)
85
+
86
+ Provide a `NamedRequest` and the result is a full response.
87
+
88
+ ```python
89
+ from pyearthmc.models.common import NamedRequest
90
+
91
+ town = await emc.get_towns(
92
+ mode="detailed",
93
+ town_request=NamedRequest(query=["Colombia"]),
94
+ )
95
+ print(town.towns[0].mayor.name)
96
+
97
+ player = await emc.get_players(
98
+ mode="detailed",
99
+ player_request=NamedRequest(query=["RafaCabra"]),
100
+ )
101
+ print(player.players[0].status.isOnline)
102
+ ```
103
+
104
+ > **Note:** In `detailed` mode the query accepts either a name or a UUID, and a
105
+ > request body is required, otherwise a `ValueError` is raised.
106
+
107
+ ## Contributing
108
+
109
+ Contributions are welcome! See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for
110
+ development setup and conventions.
111
+
112
+ ## License
113
+
114
+ [MIT](./LICENSE)
@@ -0,0 +1,102 @@
1
+ # PyEMC
2
+
3
+ A complete Python client for the [EarthMC](https://earthmc.net/)
4
+ Minecraft server API.
5
+
6
+ PyEMC models the raw EarthMC JSON endpoints as validated [Pydantic v2](https://docs.pydantic.dev/)
7
+ objects and wraps them behind an async HTTP client. You get autocompletion,
8
+ type checking, and data validation out of the box.
9
+
10
+ ## Features
11
+
12
+ - **Type-safe:** Every request and response is a Pydantic v2 model. Typos and
13
+ malformed payloads fail fast instead of silently breaking later.
14
+ - **Async:** Built on `httpx.AsyncClient` for concurrent, requests.
15
+ - **Two request modes:** `overview` (a light `GET` returning a name/UUID list)
16
+ and `detailed` (a `POST` returning full entity data).
17
+ - **Computed fields:** Extra data is generated on top of the raw API response
18
+ (e.g. when the response arrived, and the current vote-party vote count).
19
+
20
+ ## Requirements
21
+
22
+ - Python **3.14+**
23
+ - An internet connection to reach `https://api.earthmc.net`
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install pyearthmc
29
+ ```
30
+
31
+ Or with [uv](https://docs.astral.sh/uv/):
32
+
33
+ ```bash
34
+ uv add pyearthmc
35
+ ```
36
+
37
+ ## Quickstart
38
+
39
+ ```python
40
+ import asyncio
41
+
42
+ import httpx
43
+
44
+ from pyearthmc import EmcClient
45
+
46
+
47
+ async def main():
48
+ async with httpx.AsyncClient() as client:
49
+ emc = EmcClient(client)
50
+
51
+ server = await emc.get_server_info()
52
+ print(server.version)
53
+ print(server.voteParty.numVotes)
54
+
55
+
56
+ if __name__ == "__main__":
57
+ asyncio.run(main())
58
+ ```
59
+
60
+ ## Usage
61
+
62
+ ### Overview mode
63
+
64
+ Returns a plain list of `NamedEntity` (`name` and `uuid`).
65
+
66
+ ```python
67
+ towns = await emc.get_towns() # overview (default)
68
+ nations = await emc.get_nations()
69
+ players = await emc.get_players()
70
+ ```
71
+
72
+ ### Detailed mode (full data)
73
+
74
+ Provide a `NamedRequest` and the result is a full response.
75
+
76
+ ```python
77
+ from pyearthmc.models.common import NamedRequest
78
+
79
+ town = await emc.get_towns(
80
+ mode="detailed",
81
+ town_request=NamedRequest(query=["Colombia"]),
82
+ )
83
+ print(town.towns[0].mayor.name)
84
+
85
+ player = await emc.get_players(
86
+ mode="detailed",
87
+ player_request=NamedRequest(query=["RafaCabra"]),
88
+ )
89
+ print(player.players[0].status.isOnline)
90
+ ```
91
+
92
+ > **Note:** In `detailed` mode the query accepts either a name or a UUID, and a
93
+ > request body is required, otherwise a `ValueError` is raised.
94
+
95
+ ## Contributing
96
+
97
+ Contributions are welcome! See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for
98
+ development setup and conventions.
99
+
100
+ ## License
101
+
102
+ [MIT](./LICENSE)
@@ -0,0 +1,55 @@
1
+ [project]
2
+ name = "pyearthmc"
3
+ version = "0.2.0"
4
+ description = "A complete and type-safe Python client for EMC (EarthMC Minecraft Server)"
5
+ license = "MIT"
6
+ readme = "README.md"
7
+ requires-python = ">=3.14"
8
+ dependencies = [
9
+ "httpx>=0.28.1",
10
+ "pydantic>=2.13.5",
11
+ "rich>=15.0.0",
12
+ ]
13
+
14
+ [[project.authors]]
15
+ name = "RafaCabra"
16
+
17
+ [dependency-groups]
18
+ dev = [
19
+ "mypy>=1.16",
20
+ "pyright>=1.1.0",
21
+ "pytest>=8.0",
22
+ "pytest-asyncio>=1.0",
23
+ "pyfiglet>=1.0.4",
24
+ "ruff>=0.11",
25
+ ]
26
+
27
+ [tool.pytest.ini_options]
28
+ testpaths = ["tests"]
29
+ asyncio_mode = "auto"
30
+
31
+ [tool.mypy]
32
+ python_version = "3.14"
33
+ files = ["src"]
34
+ ignore_missing_imports = true
35
+
36
+ [tool.ruff]
37
+ target-version = "py314"
38
+ line-length = 160
39
+ src = [
40
+ "src",
41
+ "tests",
42
+ "examples",
43
+ ]
44
+
45
+ [tool.ruff.lint]
46
+ select = [
47
+ "E",
48
+ "F",
49
+ "I",
50
+ "UP",
51
+ ]
52
+
53
+ [build-system]
54
+ requires = ["uv_build>=0.12.7,<0.13.0"]
55
+ build-backend = "uv_build"
@@ -0,0 +1,48 @@
1
+ [project]
2
+ name = "pyearthmc"
3
+ version = "0.2.0"
4
+ description = "A complete and type-safe Python client for EMC (EarthMC Minecraft Server)"
5
+ license = "MIT"
6
+ readme = "README.md"
7
+ authors = [
8
+ { name = "RafaCabra" },
9
+ ]
10
+ requires-python = ">=3.14"
11
+ dependencies = [
12
+ "httpx>=0.28.1",
13
+ "pydantic>=2.13.5",
14
+ "rich>=15.0.0",
15
+ ]
16
+
17
+
18
+ [dependency-groups]
19
+ dev = [
20
+ "mypy>=1.16",
21
+ "pyright>=1.1.0",
22
+ "pytest>=8.0",
23
+ "pytest-asyncio>=1.0",
24
+ "pyfiglet>=1.0.4",
25
+ "ruff>=0.11",
26
+ ]
27
+
28
+ [tool.pytest.ini_options]
29
+ testpaths = ["tests"]
30
+ asyncio_mode = "auto"
31
+
32
+
33
+ [tool.mypy]
34
+ python_version = "3.14"
35
+ files = ["src"]
36
+ ignore_missing_imports = true
37
+
38
+ [tool.ruff]
39
+ target-version = "py314"
40
+ line-length = 160
41
+ src = ["src", "tests", "examples"]
42
+
43
+ [tool.ruff.lint]
44
+ select = ["E", "F", "I", "UP"]
45
+
46
+ [build-system]
47
+ requires = ["uv_build>=0.12.7,<0.13.0"]
48
+ build-backend = "uv_build"
@@ -0,0 +1,6 @@
1
+
2
+ from .client import EmcClient
3
+
4
+ __version__ = "0.2.0"
5
+
6
+ __all__ = ["EmcClient", "__version__"]
@@ -0,0 +1,288 @@
1
+
2
+ from typing import Literal, overload
3
+
4
+ import httpx
5
+ from rich.progress import track
6
+
7
+ from pyearthmc.models.common import NamedEntity, NamedRequest, RequestMode
8
+ from pyearthmc.models.nations import Nation, NationResponse
9
+ from pyearthmc.models.players import Player, PlayerResponse
10
+ from pyearthmc.models.server import ServerResponse
11
+ from pyearthmc.models.towns import Town, TownResponse
12
+
13
+
14
+ class EmcClient:
15
+ """
16
+ Main client for interacting with the EarthMC API.
17
+ It coordinates requests and responses.
18
+ All the requests begin with `get_` and return a response model.
19
+ Every single request and response is handled via pydantic models
20
+ providing type safety and validation. Feel free to check the docs for each function
21
+ and the models to know exactly what each response contains.
22
+
23
+ Args:
24
+ client (httpx.AsyncClient): The async HTTP client to use for requests.
25
+
26
+ Examples:
27
+ ```python
28
+ with httpx.AsyncClient() as client:
29
+ emc_client = EmcClient(client)
30
+ ```
31
+
32
+ Notes:
33
+ recommended to use `async with EmcClient(httpx.AsyncClient()) as client:`
34
+
35
+ As always, contributions will be appreciated, even if only to improve the documentation.
36
+ """
37
+
38
+
39
+ def __init__(self, client: httpx.AsyncClient | None = None):
40
+ if client is None:
41
+ client = httpx.AsyncClient()
42
+ self.client: httpx.AsyncClient = client
43
+
44
+ async def get_server_info(self) -> ServerResponse:
45
+ """
46
+ Examples:
47
+ ```python
48
+ with httpx.AsyncClient() as client:
49
+ emc_client = EmcClient(client)
50
+ server_info = await emc_client.get_server_info()
51
+ print(server_info)
52
+ ```
53
+
54
+ Get the core server information.
55
+ It includes the server version, moon phase, timestamps, status, stats and voteparty status.
56
+ Check the API documentation for more details.
57
+
58
+ Args:
59
+ None
60
+
61
+ Note:
62
+ Replaces a GET request to https://api.earthmc.net/v4/
63
+
64
+ Returns:
65
+ ServerResponse: The server information including voteparty status, version and others.
66
+ """
67
+
68
+ response = await self.client.get("https://api.earthmc.net/v4/")
69
+
70
+ if response.status_code != 200:
71
+ raise httpx.HTTPStatusError(f"Unexpected status code: {response.status_code, response.text}", request=response.request, response=response)
72
+
73
+
74
+ return ServerResponse.model_validate(response.json())
75
+
76
+ @overload
77
+ async def get_towns(self, mode: Literal["overview"] = "overview", town_request: None = None) -> list[NamedEntity]: ...
78
+
79
+ @overload
80
+ async def get_towns(self, mode: Literal["detailed"], town_request: NamedRequest | None = None) -> TownResponse: ...
81
+
82
+ async def get_towns(self, mode: RequestMode = "overview", town_request: NamedRequest | None = None) -> list[NamedEntity] | TownResponse: # type: ignore[misc]
83
+ """
84
+ Get information about towns in the server.
85
+ Depends on the request mode. If you want to query a specific town,
86
+ use 'detailed' mode and provide the town request parameters.
87
+ You can provide either the town name or UUID.
88
+
89
+ Note:
90
+ The 'overview' mode replaces a GET request to https://api.earthmc.net/v4/towns.
91
+ The 'detailed' mode replaces a POST request to https://api.earthmc.net/v4/towns.
92
+
93
+ Args:
94
+ mode (RequestMode): The request mode, either 'overview' or 'detailed'.
95
+ town_request (NamedRequest | None): The town request parameters, if mode is 'detailed'.
96
+
97
+ Returns:
98
+ list[NamedEntity] | TownResponse:
99
+ It returns a list of NamedEntity when using 'overview' mode,
100
+ and a TownResponse when using 'detailed' mode.
101
+
102
+ Raises:
103
+ ValueError: If mode is 'detailed' and town_request is None.
104
+ httpx.HTTPStatusError: If the request fails.
105
+
106
+ Examples:
107
+ ```python
108
+ with httpx.AsyncClient() as client:
109
+ emc_client = EmcClient(client)
110
+ towns = await emc_client.get_towns()
111
+ print(towns)
112
+ ```
113
+
114
+ ```python
115
+ with httpx.AsyncClient() as client:
116
+ emc_client = EmcClient(client)
117
+ town_request = NamedRequest(query=["Colombia"])
118
+ towns = await emc_client.get_towns(mode="detailed", town_request=town_request)
119
+ print(towns)
120
+ ```
121
+ """
122
+
123
+ if mode == "overview":
124
+ payload = None
125
+ response = await self.client.get("https://api.earthmc.net/v4/towns")
126
+
127
+
128
+ elif mode == "detailed":
129
+ if town_request is None:
130
+ raise ValueError("town must be provided when mode is 'detailed'")
131
+
132
+ payload = town_request.model_dump(exclude_none=True)
133
+ response = await self.client.post("https://api.earthmc.net/v4/towns", json=payload)
134
+
135
+ if response.status_code != 200:
136
+ raise httpx.HTTPStatusError(f"Unexpected status code: {response.status_code, response.text} to request {payload}",
137
+ request=response.request, response=response)
138
+
139
+ if mode == "overview":
140
+ overview_towns: list[NamedEntity] = [NamedEntity.model_validate(town) for town in track(response.json(),
141
+ description="Loading and validating overview towns...")]
142
+ return overview_towns
143
+
144
+ elif mode == "detailed":
145
+ detailed_towns: list[Town] = [Town.model_validate(town) for town in track(response.json(),
146
+ description="Loading and validating detailed towns...")]
147
+ return TownResponse(towns=detailed_towns)
148
+
149
+
150
+ @overload
151
+ async def get_nations(self, mode: Literal["overview"] = "overview", nation_request: None = None) -> list[NamedEntity]: ...
152
+
153
+ @overload
154
+ async def get_nations(self, mode: Literal["detailed"], nation_request: NamedRequest | None = None) -> NationResponse: ...
155
+
156
+ async def get_nations(self, mode: RequestMode = "overview", nation_request: NamedRequest | None = None) -> list[NamedEntity] | NationResponse: # type: ignore[misc]
157
+ """
158
+ Get information about nations in the server.
159
+ Depends on the request mode. If you want to query a specific nation,
160
+ use `detailed` mode and provide the nation request parameters.
161
+ You can provide either the nation name or UUID.
162
+
163
+ Note:
164
+ The 'overview' mode makes a GET request to https://api.earthmc.net/v4/nations,
165
+ while the 'detailed' mode makes a POST request to https://api.earthmc.net/v4/nations
166
+
167
+ Args:
168
+ mode (RequestMode): The request mode, either 'overview' or 'detailed'.
169
+ nation_request (NamedRequest | None): The nation request parameters, required for 'detailed' mode.
170
+
171
+ Returns:
172
+ list[NamedEntity] | NationResponse:
173
+ It returns a list of NamedEntity when using 'overview' mode,
174
+ and a NationResponse when using 'detailed' mode.
175
+
176
+ Raises:
177
+ ValueError: If 'detailed' mode is used without providing nation_request.
178
+ httpx.HTTPStatusError: If the request fails.
179
+
180
+ Examples:
181
+ ```python
182
+ with httpx.AsyncClient() as client:
183
+ emc_client = EmcClient(client)
184
+ nations = await emc_client.get_nations()
185
+ print(nations)
186
+ ```
187
+
188
+ ```python
189
+ with httpx.AsyncClient() as client:
190
+ emc_client = EmcClient(client)
191
+ nation_request = NamedRequest(query=["Amazon"])
192
+ nations = await emc_client.get_nations(mode="detailed", nation_request=nation_request)
193
+ print(nations)
194
+ ```
195
+
196
+ """
197
+ if mode == "overview":
198
+ response = await self.client.get("https://api.earthmc.net/v4/nations")
199
+
200
+
201
+ elif mode == "detailed":
202
+ if nation_request is None:
203
+ raise ValueError("nation_request is required for detailed mode")
204
+
205
+ payload = nation_request.model_dump()
206
+ response = await self.client.post("https://api.earthmc.net/v4/nations", json=payload)
207
+
208
+ if response.status_code != 200:
209
+ raise httpx.HTTPStatusError(f"Unexpected status code: {response.status_code, response.text}", request=response.request, response=response)
210
+
211
+ if mode == "overview":
212
+ overview_nations: list[NamedEntity] = [NamedEntity.model_validate(nation) for nation in track(response.json(),
213
+ description="Fetching overview nations")]
214
+ return overview_nations
215
+
216
+ elif mode == "detailed":
217
+ detailed_nations: list[Nation] = [Nation.model_validate(nation) for nation in track(response.json(),
218
+ description="Fetching detailed nations")]
219
+ return NationResponse(nations=detailed_nations)
220
+
221
+ @overload
222
+ async def get_players(self, mode: Literal["overview"] = "overview", player_request: None = None) -> list[NamedEntity]: ...
223
+
224
+ @overload
225
+ async def get_players(self, mode: Literal["detailed"], player_request: NamedRequest | None = None) -> PlayerResponse: ...
226
+
227
+ async def get_players(self, mode: RequestMode = "overview", player_request: NamedRequest | None = None) -> list[NamedEntity] | PlayerResponse: # type: ignore[misc]
228
+ """
229
+ Get information about players.
230
+ Depends on the `mode` parameter. If you want to query a specific player,
231
+ use `detailed` mode and provide a `player_request`.
232
+ You can provide either the player name or UUID.
233
+
234
+ Note:
235
+ `overview mode`: Replaces a GET request to https://api.earthmc.net/v4/players
236
+ `detailed mode`: Replaces a POST request to https://api.earthmc.net/v4/players
237
+
238
+ Args:
239
+ mode (RequestMode): The mode to use for the request. Defaults to `overview`.
240
+ player_request (NamedRequest | None): The player request to use for `detailed` mode. Defaults to `None`.
241
+
242
+ Returns:
243
+ list[NamedEntity] | PlayerResponse: The players when using `overview` mode.
244
+ PlayerResponse: The players when using `detailed` mode.
245
+
246
+ Raises:
247
+ ValueError: If `player_request` is not provided for `detailed` mode.
248
+ httpx.HTTPStatusError: If the request fails.
249
+
250
+ Examples:
251
+ ```python
252
+ with httpx.AsyncClient() as client:
253
+ emc_client = EmcClient(client)
254
+ players = await emc_client.get_players("overview")
255
+ print(players)
256
+ ```
257
+
258
+ ```python
259
+ with httpx.AsyncClient() as client:
260
+ emc_client = EmcClient(client)
261
+ player_request = NamedRequest(query=["RafaCabra"])
262
+ players = await emc_client.get_players("detailed", player_request=player_request)
263
+ print(players)
264
+ ```
265
+ """
266
+ if mode == "overview":
267
+ response = await self.client.get("https://api.earthmc.net/v4/players")
268
+
269
+
270
+ elif mode == "detailed":
271
+ if player_request is None:
272
+ raise ValueError("player_request is required for detailed mode")
273
+
274
+ payload = player_request.model_dump()
275
+ response = await self.client.post("https://api.earthmc.net/v4/players", json=payload)
276
+
277
+ if response.status_code != 200:
278
+ raise httpx.HTTPStatusError(f"Unexpected status code: {response.status_code, response.text}", request=response.request, response=response)
279
+
280
+ if mode == "overview":
281
+ overview_players: list[NamedEntity] = [NamedEntity.model_validate(player) for player in track(response.json(),
282
+ description="Fetching overview players")]
283
+ return overview_players
284
+
285
+ elif mode == "detailed":
286
+ detailed_players: list[Player] = [Player.model_validate(player) for player in track(response.json(),
287
+ description="Fetching detailed players")]
288
+ return PlayerResponse(players=detailed_players)
@@ -0,0 +1,51 @@
1
+ # Models
2
+
3
+ This directory contains the **Pydantic v2 data models** used by PyEMC.
4
+
5
+ They define the structure of the data exchanged between the client and the
6
+ EarthMC API. Every request and response in the library is handled through these
7
+ models, providing validation, serialization, autocompletion, and type checking.
8
+
9
+ Because everything is a typed model, you get IDE autocompletion and static
10
+ type-checking out of the box, you don't need to re-check the raw API docs for
11
+ every field.
12
+
13
+ ## Files
14
+
15
+ | File | Contents |
16
+ | --- | --- |
17
+ | `common.py` | Shared building blocks used by all endpoints |
18
+ | `server.py` | `ServerResponse` and its nested server/status/stats/vote-party models |
19
+ | `towns.py` | `Town`, `TownResponse` and town-specific models |
20
+ | `nations.py` | `Nation`, `NationResponse` and nation-specific models |
21
+ | `players.py` | `Player`, `PlayerResponse` and player-specific models |
22
+
23
+ ## Shared primitives (`common.py`)
24
+
25
+ - `RequestMode`, `Literal["overview", "detailed"]`, controls the request method
26
+ and the shape of the response.
27
+ - `NamedRequest`, the `detailed` request body: `query: list[str]` of names or
28
+ UUIDs to look up.
29
+ - `NamedEntity`, the minimal `{name, uuid}` pair returned by `overview` mode and
30
+ used to reference related entities (mayor, nation, residents, etc.).
31
+ - `UnnamedEntity`, an entity identified by `uuid` only.
32
+ - `Metadata`, adds a computed `response_arrived` timestamp to every response.
33
+ - `DefaultResponse`, base for all typed detailed responses; carries `metadata`.
34
+
35
+ ## Computed fields
36
+
37
+ Some fields are generated on top of the raw API response via
38
+ `@computed_field`. These add value without altering the underlying data:
39
+
40
+ - `Metadata.response_arrived`, a Unix timestamp recorded when the model was
41
+ constructed, so you know how stale the data is.
42
+ - `VotepartyStatus.numVotes`, the current vote-party vote count, derived from
43
+ `target - numRemaining`.
44
+
45
+ ## Conventions
46
+
47
+ - Field names mirror the raw EarthMC API (camelCase, e.g. `isOnline`,
48
+ `numTownBlocks`), so the JSON maps 1:1 onto the models.
49
+ - Optional fields that the API may omit are typed `Optional` / `None`.
50
+ - When a new endpoint is added, create a matching module here following the
51
+ `server.py` / `towns.py` pattern and export its response via the client.
File without changes
@@ -0,0 +1,54 @@
1
+
2
+ import datetime
3
+ from typing import Literal
4
+
5
+ from pydantic import BaseModel, computed_field
6
+
7
+ RequestMode = Literal['overview', 'detailed']
8
+ type UnnamedCoordinates = tuple[int, int]
9
+
10
+
11
+
12
+ class NamedRequest(BaseModel):
13
+ # I absolutely hate doing this without type safety,
14
+ # but the API expects a list of strings and I don't want to write a custom validator
15
+ # Let me know if you have a better solution.
16
+ query: list[str]
17
+
18
+ class Metadata(BaseModel):
19
+ @computed_field
20
+ def response_arrived(self) -> int:
21
+ return int(datetime.datetime.now(datetime.UTC).timestamp())
22
+
23
+ class SimpleTimestamp(BaseModel):
24
+ registered: int
25
+
26
+ class NamedEntity(BaseModel):
27
+ name: str | None = None
28
+ uuid: str | None = None
29
+
30
+ class SimpleQuery(BaseModel):
31
+ query: NamedEntity
32
+
33
+ class UnnamedEntity(BaseModel):
34
+ uuid: str
35
+
36
+ class SimpleCoordinates(BaseModel):
37
+ x: float
38
+ z: float
39
+
40
+ class WorldCoordinates(SimpleCoordinates):
41
+ world: str
42
+ y: int
43
+ pitch: float
44
+ yaw: float
45
+
46
+
47
+ class NamedResponse(NamedEntity):
48
+ metadata: Metadata = Metadata()
49
+
50
+ class UnnamedResponse(UnnamedEntity):
51
+ metadata: Metadata = Metadata()
52
+
53
+ class DefaultResponse(BaseModel):
54
+ metadata: Metadata = Metadata()
@@ -0,0 +1,52 @@
1
+ from pydantic.main import BaseModel
2
+
3
+ from pyearthmc.models.common import (
4
+ DefaultResponse,
5
+ NamedEntity,
6
+ SimpleTimestamp,
7
+ WorldCoordinates,
8
+ )
9
+
10
+
11
+ class NationStats(BaseModel):
12
+ numTownBlocks: int
13
+ numResidents: int
14
+ numTowns: int
15
+ numAllies: int
16
+ numEnemies: int
17
+ balance: int
18
+
19
+ class NationStatus(BaseModel):
20
+ isPublic: bool
21
+ isOpen: bool
22
+ isNeutral: bool
23
+
24
+ class NationCoordinates(BaseModel):
25
+ spawn: WorldCoordinates
26
+
27
+ class NationRanks(BaseModel):
28
+ Chancellor: list[NamedEntity] | None = None
29
+ Colonist: list[NamedEntity] | None = None
30
+ Diplomat: list[NamedEntity] | None = None
31
+
32
+
33
+ class Nation(NamedEntity):
34
+ board: str
35
+ dynmapColour: str
36
+ dynmapOutline: str
37
+ wiki: str | None = None
38
+ king: NamedEntity
39
+ capital: NamedEntity
40
+ timestamps: SimpleTimestamp
41
+ status: NationStatus
42
+ stats: NationStats
43
+ coordinates: NationCoordinates
44
+ residents: list[NamedEntity]
45
+ towns: list[NamedEntity]
46
+ allies: list[NamedEntity] | None = None
47
+ enemies: list[NamedEntity] | None = None
48
+ sanctions: list[NamedEntity] | None = None
49
+ ranks: NationRanks
50
+
51
+ class NationResponse(DefaultResponse):
52
+ nations: list[Nation]
@@ -0,0 +1,8 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class NearbyQuery(BaseModel):
5
+ target_type: str
6
+ target: str | list[int]
7
+ search_type: str
8
+ radius: int
@@ -0,0 +1,8 @@
1
+ from pydantic import BaseModel
2
+
3
+ from pyearthmc.models.common import NamedEntity
4
+
5
+
6
+ class OnlinePlayersResponse(BaseModel):
7
+ count: int
8
+ players: list[NamedEntity]
@@ -0,0 +1,42 @@
1
+ from pydantic import BaseModel
2
+
3
+ from pyearthmc.models.common import DefaultResponse, NamedEntity
4
+
5
+
6
+ class PlayerTimestamp(BaseModel):
7
+ registered: int
8
+ joinedTownAt: int | None = None
9
+ lastOnline: int
10
+
11
+ class PlayerStats(BaseModel):
12
+ balance: int
13
+ numFriends: int
14
+
15
+ class PlayerStatus(BaseModel):
16
+ isOnline: bool
17
+ isNPC: bool
18
+ isMayor: bool
19
+ isKing: bool
20
+ hasTown: bool
21
+ hasNation: bool
22
+
23
+ class PlayerRanks(BaseModel):
24
+ townRanks: list[str]
25
+ nationRanks: list[str]
26
+
27
+ class Player(NamedEntity):
28
+ title: str | None = None
29
+ surname: str | None = None
30
+ formatted_name: str | None = None
31
+ about: str | None = None
32
+ town: NamedEntity | None = None
33
+ nation: NamedEntity | None = None
34
+ timestamps: PlayerTimestamp
35
+ status: PlayerStatus
36
+ stats: PlayerStats
37
+ ranks: PlayerRanks | None = None
38
+ friends: list[NamedEntity]
39
+ discord: int | None = None
40
+
41
+ class PlayerResponse(DefaultResponse):
42
+ players: list[Player]
@@ -0,0 +1,45 @@
1
+ from pydantic import BaseModel
2
+ from pydantic.fields import computed_field
3
+
4
+ from pyearthmc.models.common import DefaultResponse
5
+
6
+
7
+ class ServerTimestamp(BaseModel):
8
+ newDayTime: int
9
+ serverTimeOfDay: int
10
+
11
+
12
+ class ServerStatus(BaseModel):
13
+ hasStorm: bool
14
+ isThundering: bool
15
+
16
+ class ServerStats(BaseModel):
17
+ time: int
18
+ fullTime: int
19
+ maxPlayers: int
20
+ numOnlinePlayers: int
21
+ numOnlineNomads: int
22
+ numResidents: int
23
+ numNomads: int
24
+ numTowns: int
25
+ numTownBlocks: int
26
+ numNations: int
27
+ numQuarters: int
28
+ numCuboids: int
29
+
30
+ class VotepartyStatus(BaseModel):
31
+ target: int
32
+ numRemaining: int
33
+
34
+ @computed_field
35
+ def numVotes(self) -> int:
36
+ return self.target - self.numRemaining
37
+
38
+
39
+ class ServerResponse(DefaultResponse):
40
+ version: str
41
+ moonPhase: str
42
+ timestamps: ServerTimestamp
43
+ status: ServerStatus
44
+ stats: ServerStats
45
+ voteParty: VotepartyStatus
@@ -0,0 +1,74 @@
1
+
2
+ from pydantic import BaseModel
3
+
4
+ from pyearthmc.models.common import (
5
+ DefaultResponse,
6
+ NamedEntity,
7
+ UnnamedCoordinates,
8
+ UnnamedEntity,
9
+ WorldCoordinates,
10
+ )
11
+
12
+
13
+ class TownTimestamp(BaseModel):
14
+ registered: int
15
+ joinedNationAt: int | None = None
16
+ ruinedAt: int | None = None
17
+
18
+ class TownStats(BaseModel):
19
+ numTownBlocks: int
20
+ maxTownBlocks: int
21
+ numResidents: int
22
+ numTrusted: int
23
+ numOutlaws: int
24
+ balance: int
25
+ forSalePrice: int | None
26
+
27
+ class TownStatus(BaseModel):
28
+ isPublic: bool
29
+ isOpen: bool
30
+ isNeutral: bool
31
+ isCapital: bool
32
+ isOverclaimed: bool | None = None
33
+ isRuined: bool
34
+ isForSale: bool
35
+ hasNation: bool
36
+ hasOverclaimShield: bool | None = None
37
+ canOutsiderSpawn: bool | None = None
38
+
39
+
40
+ class TownRanks(BaseModel):
41
+ Councilor: list[NamedEntity] | None = None
42
+ Builder: list[NamedEntity] | None = None
43
+ Recruiter: list[NamedEntity] | None = None
44
+ Police: list[NamedEntity] | None = None
45
+ Tax_exempt: list[NamedEntity] | None = None
46
+ Treasurer: list[NamedEntity] | None = None
47
+ Realtor: list[NamedEntity] | None = None
48
+ Settler: list[NamedEntity] | None = None
49
+
50
+ class TownCoordinates(BaseModel):
51
+ spawn: WorldCoordinates
52
+ homeBlock: UnnamedCoordinates
53
+ townBlocks: list[UnnamedCoordinates]
54
+
55
+
56
+ class Town(NamedEntity):
57
+ board: str
58
+ founder: str
59
+ wiki: str | None = None
60
+ mayor: NamedEntity
61
+ nation: NamedEntity
62
+ timestamps: TownTimestamp
63
+ status: TownStatus
64
+ stats: TownStats
65
+ coordinates: TownCoordinates
66
+ residents: list[NamedEntity]
67
+ trusted: list[NamedEntity] | None = None
68
+
69
+ outlaws: list[NamedEntity] | None = None
70
+ quarters: list[UnnamedEntity] | None = None
71
+ ranks: TownRanks
72
+
73
+ class TownResponse(DefaultResponse):
74
+ towns: list[Town]