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.
- pyearthmc-0.2.0/PKG-INFO +114 -0
- pyearthmc-0.2.0/README.md +102 -0
- pyearthmc-0.2.0/pyproject.toml +55 -0
- pyearthmc-0.2.0/pyproject.toml.orig +48 -0
- pyearthmc-0.2.0/src/pyearthmc/__init__.py +6 -0
- pyearthmc-0.2.0/src/pyearthmc/client.py +288 -0
- pyearthmc-0.2.0/src/pyearthmc/models/README.md +51 -0
- pyearthmc-0.2.0/src/pyearthmc/models/__init__.py +0 -0
- pyearthmc-0.2.0/src/pyearthmc/models/common.py +54 -0
- pyearthmc-0.2.0/src/pyearthmc/models/nations.py +52 -0
- pyearthmc-0.2.0/src/pyearthmc/models/nearby.py +8 -0
- pyearthmc-0.2.0/src/pyearthmc/models/online.py +8 -0
- pyearthmc-0.2.0/src/pyearthmc/models/players.py +42 -0
- pyearthmc-0.2.0/src/pyearthmc/models/server.py +45 -0
- pyearthmc-0.2.0/src/pyearthmc/models/towns.py +74 -0
pyearthmc-0.2.0/PKG-INFO
ADDED
|
@@ -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,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,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]
|