bsm-api-client 1.0.0__py3-none-any.whl

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,40 @@
1
+ # src/bsm_api_client/__init__.py
2
+ """Python client library for the Bedrock Server Manager API."""
3
+ import logging
4
+ from importlib import metadata
5
+
6
+ from .exceptions import (
7
+ APIError,
8
+ AuthError,
9
+ NotFoundError,
10
+ ServerNotFoundError,
11
+ ServerNotRunningError,
12
+ CannotConnectError,
13
+ InvalidInputError,
14
+ OperationFailedError,
15
+ APIServerSideError,
16
+ )
17
+ from .api_client import BedrockServerManagerApi
18
+
19
+ __all__ = [
20
+ "BedrockServerManagerApi",
21
+ "APIError",
22
+ "AuthError",
23
+ "ServerNotFoundError",
24
+ "ServerNotRunningError",
25
+ "CannotConnectError",
26
+ "InvalidInputError",
27
+ "OperationFailedError",
28
+ "APIServerSideError",
29
+ "__version__",
30
+ ]
31
+
32
+ try:
33
+ __version__ = metadata.version(__name__)
34
+ except metadata.PackageNotFoundError:
35
+ __version__ = "0.0.0"
36
+
37
+ # Add a NullHandler to the root logger of the library.
38
+ # This prevents log messages from being output by default if the
39
+ # consuming application/script doesn't configure logging.
40
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
@@ -0,0 +1,33 @@
1
+ # src/bsm_api_client/client.py
2
+ """Main API client class for Bedrock Server Manager.
3
+ Combines the base client logic with specific endpoint method mixins.
4
+ """
5
+ import logging
6
+ from .client_base import ClientBase
7
+ from .client._manager_methods import ManagerMethodsMixin
8
+ from .client._server_info_methods import ServerInfoMethodsMixin
9
+ from .client._server_action_methods import ServerActionMethodsMixin
10
+ from .client._content_methods import ContentMethodsMixin
11
+ from .client._scheduler_methods import SchedulerMethodsMixin
12
+
13
+ _LOGGER = logging.getLogger(__name__.split(".")[0] + ".client")
14
+
15
+
16
+ class BedrockServerManagerApi(
17
+ ClientBase,
18
+ ManagerMethodsMixin,
19
+ ServerInfoMethodsMixin,
20
+ ServerActionMethodsMixin,
21
+ ContentMethodsMixin,
22
+ SchedulerMethodsMixin,
23
+ ):
24
+ """
25
+ API Client for the Bedrock Server Manager.
26
+
27
+ This class combines the base connection/authentication logic with
28
+ methods for interacting with various API endpoints, organized via mixins.
29
+ """
30
+
31
+ # __init__ is inherited from ClientBase.
32
+ # All async API methods are inherited from mixins.
33
+ pass
@@ -0,0 +1 @@
1
+ # src/bsm_api_client/client/__init__.py
@@ -0,0 +1,321 @@
1
+ # src/bsm_api_client/client/_content_methods.py
2
+ """Mixin class containing content management methods (backups, worlds, addons)."""
3
+ import logging
4
+ from typing import Any, Dict, Optional, List, TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from ..client_base import ClientBase
8
+
9
+ _LOGGER = logging.getLogger(__name__.split(".")[0] + ".client.content")
10
+
11
+ # Define allowed types for validation to avoid magic strings
12
+ ALLOWED_BACKUP_LIST_TYPES = ["world", "properties", "allowlist", "permissions"]
13
+ ALLOWED_BACKUP_ACTION_TYPES = ["world", "config", "all"]
14
+ ALLOWED_RESTORE_TYPES = ["world", "config"]
15
+
16
+
17
+ class ContentMethodsMixin:
18
+ """Mixin for content management endpoints (backups, worlds, addons)."""
19
+
20
+ _request: callable
21
+ if TYPE_CHECKING:
22
+
23
+ async def _request(
24
+ self: "ClientBase",
25
+ method: str,
26
+ path: str,
27
+ json_data: Optional[Dict[str, Any]] = None,
28
+ params: Optional[Dict[str, Any]] = None,
29
+ authenticated: bool = True,
30
+ is_retry: bool = False,
31
+ ) -> Any: ...
32
+
33
+ async def async_list_server_backups(
34
+ self, server_name: str, backup_type: str
35
+ ) -> Dict[str, Any]:
36
+ """
37
+ Lists backup filenames for a specific server and backup type.
38
+
39
+ Corresponds to `GET /api/server/{server_name}/backups/list/{backup_type}`.
40
+ Requires authentication.
41
+
42
+ Args:
43
+ server_name: The name of the server.
44
+ backup_type: The type of backups to list (e.g., "world", "properties", "allowlist", "permissions", "all").
45
+ """
46
+ bt_lower = backup_type.lower()
47
+ if bt_lower not in ALLOWED_BACKUP_LIST_TYPES:
48
+ _LOGGER.error(
49
+ "Invalid backup_type '%s' for listing backups. Allowed: %s",
50
+ backup_type,
51
+ ALLOWED_BACKUP_LIST_TYPES,
52
+ )
53
+ raise ValueError(
54
+ f"Invalid backup_type '{backup_type}' provided. Allowed types are: {', '.join(ALLOWED_BACKUP_LIST_TYPES)}"
55
+ )
56
+ _LOGGER.debug(
57
+ "Fetching '%s' backups list for server '%s'", bt_lower, server_name
58
+ )
59
+
60
+ return await self._request(
61
+ "GET",
62
+ f"/server/{server_name}/backups/list/{bt_lower}",
63
+ authenticated=True,
64
+ )
65
+
66
+ async def async_get_content_worlds(self) -> Dict[str, Any]:
67
+ """
68
+ Lists available world template files (.mcworld) from the manager's content directory.
69
+
70
+ Corresponds to `GET /api/content/worlds`.
71
+ Requires authentication.
72
+ """
73
+ _LOGGER.debug("Fetching available world files from /content/worlds")
74
+ return await self._request("GET", "/content/worlds", authenticated=True)
75
+
76
+ async def async_get_content_addons(self) -> Dict[str, Any]:
77
+ """
78
+ Lists available addon files (.mcpack, .mcaddon) from the manager's content directory.
79
+
80
+ Corresponds to `GET /api/content/addons`.
81
+ Requires authentication.
82
+ """
83
+ _LOGGER.debug("Fetching available addon files from /content/addons")
84
+ return await self._request("GET", "/content/addons", authenticated=True)
85
+
86
+ async def async_trigger_server_backup(
87
+ self,
88
+ server_name: str,
89
+ backup_type: str = "all",
90
+ file_to_backup: Optional[str] = None,
91
+ ) -> Dict[str, Any]:
92
+ """
93
+ Triggers a backup operation for a specific server.
94
+
95
+ Corresponds to `POST /api/server/{server_name}/backup/action`.
96
+ Requires authentication.
97
+
98
+ Args:
99
+ server_name: The name of the server to back up.
100
+ backup_type: Type of backup ("world", "config", "all"). Defaults to "all".
101
+ file_to_backup: Required if backup_type is "config". Specifies the config file.
102
+ """
103
+ bt_lower = backup_type.lower()
104
+ if bt_lower not in ALLOWED_BACKUP_ACTION_TYPES:
105
+ _LOGGER.error(
106
+ "Invalid backup_type '%s' for triggering backup. Allowed: %s",
107
+ backup_type,
108
+ ALLOWED_BACKUP_ACTION_TYPES,
109
+ )
110
+ raise ValueError(
111
+ f"Invalid backup_type '{backup_type}' provided. Allowed types are: {', '.join(ALLOWED_BACKUP_ACTION_TYPES)}"
112
+ )
113
+
114
+ _LOGGER.info(
115
+ "Triggering backup for server '%s', type: %s, file: %s",
116
+ server_name,
117
+ bt_lower,
118
+ file_to_backup or "N/A",
119
+ )
120
+ payload: Dict[str, str] = {"backup_type": bt_lower}
121
+ if bt_lower == "config":
122
+ if not file_to_backup:
123
+ raise ValueError(
124
+ "file_to_backup is required when backup_type is 'config'"
125
+ )
126
+ payload["file_to_backup"] = file_to_backup
127
+ elif file_to_backup:
128
+ _LOGGER.warning(
129
+ "file_to_backup ('%s') provided but will be ignored for backup_type '%s'",
130
+ file_to_backup,
131
+ bt_lower,
132
+ )
133
+
134
+ return await self._request(
135
+ "POST",
136
+ f"/server/{server_name}/backup/action",
137
+ json_data=payload,
138
+ authenticated=True,
139
+ )
140
+
141
+ async def async_export_server_world(self, server_name: str) -> Dict[str, Any]:
142
+ """
143
+ Exports the current world of a server to a .mcworld file in the content directory.
144
+
145
+ Corresponds to `POST /api/server/{server_name}/world/export`.
146
+ Requires authentication.
147
+
148
+ Args:
149
+ server_name: The name of the server whose world to export.
150
+ """
151
+ _LOGGER.info("Triggering world export for server '%s'", server_name)
152
+ return await self._request(
153
+ "POST",
154
+ f"/server/{server_name}/world/export",
155
+ json_data=None,
156
+ authenticated=True,
157
+ )
158
+
159
+ async def async_reset_server_world(self, server_name: str) -> Dict[str, Any]:
160
+ """
161
+ Resets the current world of a server.
162
+
163
+ Corresponds to `DELETE /api/server/{server_name}/world/reset`.
164
+ Requires authentication.
165
+
166
+ Args:
167
+ server_name: The name of the server whose world to export.
168
+ """
169
+ _LOGGER.warning("Triggering world reset for server '%s'", server_name)
170
+ return await self._request(
171
+ "DELETE",
172
+ f"/server/{server_name}/world/reset",
173
+ json_data=None,
174
+ authenticated=True,
175
+ )
176
+
177
+ async def async_prune_server_backups(
178
+ self, server_name: str, keep: Optional[int] = None
179
+ ) -> Dict[str, Any]:
180
+ """
181
+ Prunes older backups for a specific server.
182
+
183
+ Corresponds to `POST /api/server/{server_name}/backups/prune`.
184
+ Requires authentication.
185
+
186
+ Args:
187
+ server_name: The name of the server whose backups to prune.
188
+ keep: The number of recent backups of each type to retain.
189
+ If None, uses the manager's default setting.
190
+ """
191
+ _LOGGER.info(
192
+ "Triggering backup pruning for server '%s', keep: %s",
193
+ server_name,
194
+ keep if keep is not None else "manager default",
195
+ )
196
+ payload: Optional[Dict[str, Any]] = None
197
+ if keep is not None:
198
+ if not isinstance(keep, int) or keep < 0:
199
+ raise ValueError("keep must be a non-negative integer if provided.")
200
+ payload = {"keep": keep}
201
+
202
+ return await self._request(
203
+ "POST",
204
+ f"/server/{server_name}/backups/prune",
205
+ json_data=payload,
206
+ authenticated=True,
207
+ )
208
+
209
+ async def async_restore_server_backup(
210
+ self, server_name: str, restore_type: str, backup_file: str
211
+ ) -> Dict[str, Any]:
212
+ """
213
+ Restores a server's world or a specific configuration file from a backup.
214
+
215
+ Corresponds to `POST /api/server/{server_name}/restore/action`.
216
+ Requires authentication.
217
+
218
+ Args:
219
+ server_name: The name of the server.
220
+ restore_type: Type of restore ("world" or "config").
221
+ backup_file: The filename of the backup to restore (relative to server's backup dir).
222
+ """
223
+ rt_lower = restore_type.lower()
224
+ if rt_lower not in ALLOWED_RESTORE_TYPES:
225
+ _LOGGER.error(
226
+ "Invalid restore_type '%s'. Allowed: %s",
227
+ restore_type,
228
+ ALLOWED_RESTORE_TYPES,
229
+ )
230
+ raise ValueError(
231
+ f"Invalid restore_type '{restore_type}' provided. Allowed types are: {', '.join(ALLOWED_RESTORE_TYPES)}"
232
+ )
233
+
234
+ _LOGGER.info(
235
+ "Requesting restore for server '%s', type: %s, file: '%s'",
236
+ server_name,
237
+ rt_lower,
238
+ backup_file,
239
+ )
240
+ payload = {"restore_type": rt_lower, "backup_file": backup_file}
241
+
242
+ return await self._request(
243
+ "POST",
244
+ f"/server/{server_name}/restore/action",
245
+ json_data=payload,
246
+ authenticated=True,
247
+ )
248
+
249
+ async def async_restore_server_latest_all(self, server_name: str) -> Dict[str, Any]:
250
+ """
251
+ Restores the server's world AND standard configuration files from their latest backups.
252
+
253
+ Corresponds to `POST /api/server/{server_name}/restore/all`.
254
+ Requires authentication.
255
+
256
+ Args:
257
+ server_name: The name of the server to restore.
258
+ """
259
+ _LOGGER.info(
260
+ "Requesting restore of latest 'all' backup for server '%s'", server_name
261
+ )
262
+ return await self._request(
263
+ "POST",
264
+ f"/server/{server_name}/restore/all",
265
+ json_data=None,
266
+ authenticated=True,
267
+ )
268
+
269
+ async def async_install_server_world(
270
+ self, server_name: str, filename: str
271
+ ) -> Dict[str, Any]:
272
+ """
273
+ Installs a world from a .mcworld file (from content directory) to a server.
274
+
275
+ Corresponds to `POST /api/server/{server_name}/world/install`.
276
+ Requires authentication.
277
+
278
+ Args:
279
+ server_name: The name of the server.
280
+ filename: The name of the .mcworld file (relative to content/worlds dir).
281
+ """
282
+ _LOGGER.info(
283
+ "Requesting world install for server '%s' from file '%s'",
284
+ server_name,
285
+ filename,
286
+ )
287
+ payload = {"filename": filename}
288
+
289
+ return await self._request(
290
+ "POST",
291
+ f"/server/{server_name}/world/install",
292
+ json_data=payload,
293
+ authenticated=True,
294
+ )
295
+
296
+ async def async_install_server_addon(
297
+ self, server_name: str, filename: str
298
+ ) -> Dict[str, Any]:
299
+ """
300
+ Installs an addon (.mcaddon or .mcpack file from content directory) to a server.
301
+
302
+ Corresponds to `POST /api/server/{server_name}/addon/install`.
303
+ Requires authentication.
304
+
305
+ Args:
306
+ server_name: The name of the server.
307
+ filename: The name of the addon file (relative to content/addons dir).
308
+ """
309
+ _LOGGER.info(
310
+ "Requesting addon install for server '%s' from file '%s'",
311
+ server_name,
312
+ filename,
313
+ )
314
+ payload = {"filename": filename}
315
+
316
+ return await self._request(
317
+ "POST",
318
+ f"/server/{server_name}/addon/install",
319
+ json_data=payload,
320
+ authenticated=True,
321
+ )
@@ -0,0 +1,146 @@
1
+ # src/bsm_api_client/client/_manager_methods.py
2
+ """Mixin class containing manager-level API methods."""
3
+ import logging
4
+ from typing import Any, Dict, Optional, List, TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from ..client_base import ClientBase
8
+
9
+ _LOGGER = logging.getLogger(__name__.split(".")[0] + ".client.manager")
10
+
11
+
12
+ class ManagerMethodsMixin:
13
+ """Mixin for manager-level endpoints."""
14
+
15
+ _request: callable
16
+ if TYPE_CHECKING:
17
+
18
+ async def _request(
19
+ self: "ClientBase",
20
+ method: str,
21
+ path: str,
22
+ json_data: Optional[Dict[str, Any]] = None,
23
+ params: Optional[Dict[str, Any]] = None,
24
+ authenticated: bool = True,
25
+ is_retry: bool = False,
26
+ ) -> Any: ...
27
+
28
+ async def async_get_info(self) -> Dict[str, Any]:
29
+ """
30
+ Gets system and application information from the manager.
31
+
32
+ Corresponds to `GET /api/info`.
33
+ Requires no authentication.
34
+ """
35
+ _LOGGER.debug("Fetching manager system and application information from /info")
36
+ return await self._request(method="GET", path="/info", authenticated=False)
37
+
38
+ async def async_scan_players(self) -> Dict[str, Any]:
39
+ """
40
+ Triggers scanning of player logs across all servers.
41
+
42
+ Corresponds to `POST /api/players/scan`.
43
+ Requires authentication.
44
+ """
45
+ _LOGGER.info("Triggering player log scan")
46
+ return await self._request(
47
+ method="POST", path="/players/scan", authenticated=True
48
+ )
49
+
50
+ async def async_get_players(self) -> Dict[str, Any]:
51
+ """
52
+ Gets the global list of known players (name and XUID).
53
+
54
+ Corresponds to `GET /api/players/get`.
55
+ Requires authentication.
56
+ """
57
+ _LOGGER.debug("Fetching global player list from /players/get")
58
+ return await self._request(
59
+ method="GET", path="/players/get", authenticated=True
60
+ )
61
+
62
+ async def async_add_players(self, players_data: List[str]) -> Dict[str, Any]:
63
+ """
64
+ Adds or updates players in the global list.
65
+ Each string in `players_data` should be in "PlayerName:PlayerXUID" format.
66
+
67
+ Corresponds to `POST /api/players/add`.
68
+ Requires authentication.
69
+
70
+ Args:
71
+ players_data: A list of player strings to add or update.
72
+ Example: ["Steve:2535460987654321", "Alex:2535461234567890"]
73
+ """
74
+ _LOGGER.info("Adding/updating global players: %s", players_data)
75
+ payload = {"players": players_data}
76
+ return await self._request(
77
+ method="POST",
78
+ path="/players/add",
79
+ json_data=payload,
80
+ authenticated=True,
81
+ )
82
+
83
+ async def async_prune_downloads(
84
+ self, directory: str, keep: Optional[int] = None
85
+ ) -> Dict[str, Any]:
86
+ """
87
+ Triggers pruning of downloaded server archives in a specified directory.
88
+
89
+ Corresponds to `POST /api/downloads/prune`.
90
+ Requires authentication.
91
+
92
+ Args:
93
+ directory: The absolute path to the directory to prune.
94
+ keep: The number of newest files to retain. If None, uses server default.
95
+ """
96
+ _LOGGER.info(
97
+ "Triggering download cache prune for directory '%s', keep: %s",
98
+ directory,
99
+ keep if keep is not None else "server default",
100
+ )
101
+ payload: Dict[str, Any] = {"directory": directory}
102
+ if keep is not None:
103
+ payload["keep"] = keep
104
+
105
+ return await self._request(
106
+ method="POST",
107
+ path="/downloads/prune",
108
+ json_data=payload,
109
+ authenticated=True,
110
+ )
111
+
112
+ async def async_install_new_server(
113
+ self, server_name: str, server_version: str, overwrite: bool = False
114
+ ) -> Dict[str, Any]:
115
+ """
116
+ Requests installation of a new Bedrock server instance.
117
+ The response may indicate success or that confirmation is needed if overwrite is false
118
+ and the server already exists.
119
+
120
+ Corresponds to `POST /api/server/install`.
121
+ Requires authentication.
122
+
123
+ Args:
124
+ server_name: The desired unique name for the new server.
125
+ server_version: The version to install (e.g., "LATEST", "PREVIEW", "1.20.81.01").
126
+ overwrite: If True, will delete existing server data if a server with the
127
+ same name already exists. Defaults to False.
128
+ """
129
+ _LOGGER.info(
130
+ "Requesting installation for server '%s', version: '%s', overwrite: %s",
131
+ server_name,
132
+ server_version,
133
+ overwrite,
134
+ )
135
+ payload = {
136
+ "server_name": server_name,
137
+ "server_version": server_version,
138
+ "overwrite": overwrite,
139
+ }
140
+
141
+ return await self._request(
142
+ method="POST",
143
+ path="/server/install",
144
+ json_data=payload,
145
+ authenticated=True,
146
+ )