bsm-api-client 1.2.0__tar.gz → 1.3.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.
Files changed (47) hide show
  1. {bsm_api_client-1.2.0/src/bsm_api_client.egg-info → bsm_api_client-1.3.0}/PKG-INFO +5 -4
  2. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/pyproject.toml +5 -4
  3. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/api_client.py +2 -0
  4. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/cli/__main__.py +4 -2
  5. bsm_api_client-1.3.0/src/bsm_api_client/cli/account.py +74 -0
  6. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/cli/addon.py +10 -2
  7. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/cli/auth.py +17 -2
  8. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/cli/backup.py +37 -8
  9. bsm_api_client-1.3.0/src/bsm_api_client/cli/content.py +20 -0
  10. bsm_api_client-1.3.0/src/bsm_api_client/cli/decorators.py +73 -0
  11. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/cli/main_menus.py +1 -1
  12. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/cli/server.py +44 -27
  13. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/cli/world.py +28 -6
  14. bsm_api_client-1.3.0/src/bsm_api_client/client/_account_methods.py +102 -0
  15. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/client/_content_methods.py +40 -0
  16. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/client/_manager_methods.py +3 -2
  17. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/client/_server_info_methods.py +8 -6
  18. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/models.py +68 -0
  19. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0/src/bsm_api_client.egg-info}/PKG-INFO +5 -4
  20. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client.egg-info/SOURCES.txt +4 -0
  21. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client.egg-info/requires.txt +4 -3
  22. bsm_api_client-1.3.0/test/test_account_methods.py +142 -0
  23. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/test/test_content_methods.py +151 -1
  24. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/test/test_server_info_methods.py +4 -4
  25. bsm_api_client-1.2.0/src/bsm_api_client/cli/decorators.py +0 -39
  26. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/LICENSE +0 -0
  27. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/README.md +0 -0
  28. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/setup.cfg +0 -0
  29. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/__init__.py +0 -0
  30. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/cli/__init__.py +0 -0
  31. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/cli/allowlist.py +0 -0
  32. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/cli/config.py +0 -0
  33. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/cli/permissions.py +0 -0
  34. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/cli/player.py +0 -0
  35. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/cli/plugins.py +0 -0
  36. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/cli/properties.py +0 -0
  37. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/cli/system.py +0 -0
  38. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/client/__init__.py +0 -0
  39. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/client/_plugin_methods.py +0 -0
  40. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/client/_server_action_methods.py +0 -0
  41. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/client_base.py +0 -0
  42. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client/exceptions.py +0 -0
  43. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client.egg-info/dependency_links.txt +0 -0
  44. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client.egg-info/entry_points.txt +0 -0
  45. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/src/bsm_api_client.egg-info/top_level.txt +0 -0
  46. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/test/test_api_client.py +0 -0
  47. {bsm_api_client-1.2.0 → bsm_api_client-1.3.0}/test/test_manager_methods.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bsm-api-client
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: A Python client library for the Bedrock Server Manager API
5
5
  Author-email: DMedina559 <dmedina559-github@outlook.com>
6
6
  Project-URL: Homepage, https://github.com/DMedina559/bsm-api-client
@@ -21,12 +21,13 @@ Requires-Python: >=3.8
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
23
  Requires-Dist: aiohttp<4.0.0,>=3.8.0
24
- Requires-Dist: pydantic<2.12,>=2.11.0
24
+ Requires-Dist: pydantic<2.13,>=2.11.0
25
25
  Provides-Extra: dev
26
26
  Requires-Dist: pytest<8.5,>=8.4.0; extra == "dev"
27
- Requires-Dist: pytest-asyncio<1.2.0,>=1.1.0; extra == "dev"
28
- Requires-Dist: black<25.2,>=25.1.0; extra == "dev"
27
+ Requires-Dist: pytest-asyncio<1.3.0,>=1.1.0; extra == "dev"
28
+ Requires-Dist: black<25.12,>=25.1.0; extra == "dev"
29
29
  Requires-Dist: flake8<7.4,>=7.3.0; extra == "dev"
30
+ Requires-Dist: bedrock-server-manager<3.7.0,>=3.6.0b1; extra == "dev"
30
31
  Provides-Extra: cli
31
32
  Requires-Dist: click<8.3,>=8.2.0; extra == "cli"
32
33
  Requires-Dist: questionary<2.2,>=2.1.0; extra == "cli"
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "bsm-api-client"
8
- version = "1.2.0"
8
+ version = "1.3.0"
9
9
  authors = [
10
10
  { name="DMedina559", email="dmedina559-github@outlook.com" },
11
11
  ]
@@ -28,15 +28,16 @@ classifiers = [
28
28
  ]
29
29
  dependencies = [
30
30
  "aiohttp >= 3.8.0,<4.0.0",
31
- "pydantic >= 2.11.0,<2.12",
31
+ "pydantic >= 2.11.0,< 2.13",
32
32
  ]
33
33
 
34
34
  [project.optional-dependencies]
35
35
  dev = [
36
36
  "pytest >=8.4.0,<8.5",
37
- "pytest-asyncio >= 1.1.0,<1.2.0",
38
- "black >=25.1.0,<25.2",
37
+ "pytest-asyncio >= 1.1.0,< 1.3.0",
38
+ "black >=25.1.0,<25.12",
39
39
  "flake8 >=7.3.0,<7.4",
40
+ "bedrock-server-manager >=3.6.0b1,<3.7.0",
40
41
  ]
41
42
  cli = [
42
43
  "click >=8.2.0,<8.3",
@@ -12,6 +12,7 @@ from .client._server_info_methods import ServerInfoMethodsMixin
12
12
  from .client._server_action_methods import ServerActionMethodsMixin
13
13
  from .client._content_methods import ContentMethodsMixin
14
14
  from .client._plugin_methods import PluginMethodsMixin
15
+ from .client._account_methods import AccountMethodsMixin
15
16
 
16
17
  _LOGGER = logging.getLogger(__name__.split(".")[0] + ".client")
17
18
 
@@ -23,6 +24,7 @@ class BedrockServerManagerApi(
23
24
  ServerActionMethodsMixin,
24
25
  ContentMethodsMixin,
25
26
  PluginMethodsMixin,
27
+ AccountMethodsMixin,
26
28
  ):
27
29
  """API Client for the Bedrock Server Manager.
28
30
 
@@ -15,7 +15,6 @@ from .auth import auth
15
15
  from .server import server
16
16
  from .addon import addon
17
17
  from .backup import backup
18
- from .cleanup import cleanup
19
18
  from .player import player
20
19
  from .plugins import plugin
21
20
  from .allowlist import allowlist
@@ -23,6 +22,8 @@ from .permissions import permissions
23
22
  from .properties import properties
24
23
  from .system import system
25
24
  from .world import world
25
+ from .account import account
26
+ from .content import content
26
27
  from .main_menus import main_menu
27
28
  from .decorators import AsyncGroup
28
29
 
@@ -60,7 +61,6 @@ cli.add_command(auth)
60
61
  cli.add_command(server)
61
62
  cli.add_command(addon)
62
63
  cli.add_command(backup)
63
- cli.add_command(cleanup)
64
64
  cli.add_command(player)
65
65
  cli.add_command(plugin)
66
66
  cli.add_command(allowlist)
@@ -68,6 +68,8 @@ cli.add_command(permissions)
68
68
  cli.add_command(properties)
69
69
  cli.add_command(system)
70
70
  cli.add_command(world)
71
+ cli.add_command(account)
72
+ cli.add_command(content)
71
73
 
72
74
  if __name__ == "__main__":
73
75
  cli()
@@ -0,0 +1,74 @@
1
+ # src/bsm_api_client/cli/account.py
2
+ """CLI commands for account management."""
3
+ import click
4
+ from .decorators import pass_async_context
5
+
6
+
7
+ @click.group()
8
+ def account():
9
+ """Commands for managing your account."""
10
+ pass
11
+
12
+
13
+ @account.command()
14
+ @pass_async_context
15
+ async def details(ctx):
16
+ """Get your account details."""
17
+ client = ctx.obj["client"]
18
+ details = await client.async_get_account_details()
19
+ click.echo(details.model_dump_json(indent=2))
20
+
21
+
22
+ @account.command()
23
+ @click.option("--theme", prompt="Theme name", help="The name of the theme to set.")
24
+ @pass_async_context
25
+ async def update_theme(ctx, theme):
26
+ """Update your theme."""
27
+ from bsm_api_client.models import ThemeUpdate
28
+
29
+ client = ctx.obj["client"]
30
+ payload = ThemeUpdate(theme=theme)
31
+ response = await client.async_update_theme(payload)
32
+ click.echo(response.model_dump_json(indent=2))
33
+
34
+
35
+ @account.command()
36
+ @click.option("--full-name", prompt="Full Name", help="Your full name.")
37
+ @click.option("--email", prompt="Email", help="Your email address.")
38
+ @pass_async_context
39
+ async def update_profile(ctx, full_name, email):
40
+ """Update your profile."""
41
+ from bsm_api_client.models import ProfileUpdate
42
+
43
+ client = ctx.obj["client"]
44
+ payload = ProfileUpdate(full_name=full_name, email=email)
45
+ response = await client.async_update_profile(payload)
46
+ click.echo(response.model_dump_json(indent=2))
47
+
48
+
49
+ @account.command()
50
+ @click.option(
51
+ "--current-password",
52
+ prompt=True,
53
+ hide_input=True,
54
+ confirmation_prompt=False,
55
+ help="Your current password.",
56
+ )
57
+ @click.option(
58
+ "--new-password",
59
+ prompt=True,
60
+ hide_input=True,
61
+ confirmation_prompt=True,
62
+ help="Your new password.",
63
+ )
64
+ @pass_async_context
65
+ async def change_password(ctx, current_password, new_password):
66
+ """Change your password."""
67
+ from bsm_api_client.models import ChangePasswordRequest
68
+
69
+ client = ctx.obj["client"]
70
+ payload = ChangePasswordRequest(
71
+ current_password=current_password, new_password=new_password
72
+ )
73
+ response = await client.async_change_password(payload)
74
+ click.echo(response.model_dump_json(indent=2))
@@ -1,6 +1,7 @@
1
1
  import click
2
2
  import os
3
3
  import questionary
4
+ from .decorators import pass_async_context, monitor_task
4
5
  from bsm_api_client.models import FileNamePayload
5
6
 
6
7
 
@@ -21,7 +22,7 @@ def addon():
21
22
  type=click.Path(exists=True, dir_okay=False, resolve_path=True),
22
23
  help="Path to the addon file (.mcpack, .mcaddon); skips interactive menu.",
23
24
  )
24
- @click.pass_context
25
+ @pass_async_context
25
26
  async def install_addon(ctx, server_name: str, addon_file_path: str):
26
27
  """Installs a behavior or resource pack addon to a specified server."""
27
28
  client = ctx.obj.get("client")
@@ -62,7 +63,14 @@ async def install_addon(ctx, server_name: str, addon_file_path: str):
62
63
 
63
64
  payload = FileNamePayload(filename=addon_filename)
64
65
  response = await client.async_install_server_addon(server_name, payload)
65
- if response.status == "success":
66
+ if response.task_id:
67
+ await monitor_task(
68
+ client,
69
+ response.task_id,
70
+ f"Addon '{addon_filename}' installed successfully",
71
+ "Failed to install addon",
72
+ )
73
+ elif response.status == "success":
66
74
  click.secho(f"Addon '{addon_filename}' installed successfully.", fg="green")
67
75
  else:
68
76
  click.secho(f"Failed to install addon: {response.message}", fg="red")
@@ -3,6 +3,13 @@ from .config import Config
3
3
  from bsm_api_client import BedrockServerManagerApi, AuthError
4
4
 
5
5
 
6
+ def _validate_and_get_url(url: str) -> str:
7
+ """Validates and returns a url with a scheme."""
8
+ if not url.startswith("http://") and not url.startswith("https://"):
9
+ return f"http://{url}"
10
+ return url
11
+
12
+
6
13
  @click.group()
7
14
  def auth():
8
15
  """Manages authentication."""
@@ -29,7 +36,8 @@ async def login(ctx, base_url, username, password, verify_ssl, token):
29
36
  config = ctx.obj["config"]
30
37
 
31
38
  if base_url:
32
- config.set("base_url", base_url)
39
+ validated_url = _validate_and_get_url(base_url)
40
+ config.set("base_url", validated_url)
33
41
 
34
42
  config.set("verify_ssl", verify_ssl)
35
43
 
@@ -42,6 +50,9 @@ async def login(ctx, base_url, username, password, verify_ssl, token):
42
50
  await interactive_login(ctx)
43
51
  return
44
52
 
53
+ if username and not password:
54
+ password = click.prompt("Password", hide_input=True)
55
+
45
56
  client = BedrockServerManagerApi(
46
57
  base_url=config.base_url,
47
58
  username=username,
@@ -64,8 +75,12 @@ async def interactive_login(ctx):
64
75
  username = click.prompt("Username")
65
76
  password = click.prompt("Password", hide_input=True)
66
77
 
78
+ validated_url = _validate_and_get_url(config.base_url)
79
+ if validated_url != config.base_url:
80
+ config.set("base_url", validated_url)
81
+
67
82
  client = BedrockServerManagerApi(
68
- base_url=config.base_url,
83
+ base_url=validated_url,
69
84
  username=username,
70
85
  password=password,
71
86
  verify_ssl=config.verify_ssl,
@@ -1,6 +1,7 @@
1
1
  import click
2
2
  import os
3
3
  import questionary
4
+ from .decorators import pass_async_context, monitor_task
4
5
  from bsm_api_client.models import BackupActionPayload, RestoreActionPayload
5
6
 
6
7
 
@@ -27,7 +28,7 @@ def backup():
27
28
  "file_to_backup",
28
29
  help="Specific file to back up (required if --type=config).",
29
30
  )
30
- @click.pass_context
31
+ @pass_async_context
31
32
  async def create_backup(ctx, server_name: str, backup_type: str, file_to_backup: str):
32
33
  """Creates a backup of specified server data."""
33
34
  client = ctx.obj.get("client")
@@ -51,16 +52,30 @@ async def create_backup(ctx, server_name: str, backup_type: str, file_to_backup:
51
52
  )
52
53
  response = await client.async_trigger_server_backup(server_name, payload)
53
54
 
54
- if response.status == "success":
55
- click.secho("Backup completed successfully.", fg="green")
55
+ if response.task_id:
56
+ await monitor_task(
57
+ client,
58
+ response.task_id,
59
+ "Backup completed successfully",
60
+ "Failed to create backup",
61
+ )
56
62
  click.echo("Pruning old backups...")
57
63
  prune_response = await client.async_prune_server_backups(server_name)
58
- if prune_response.status == "success":
64
+ if prune_response.task_id:
65
+ await monitor_task(
66
+ client,
67
+ prune_response.task_id,
68
+ "Pruning complete",
69
+ "Failed to prune backups",
70
+ )
71
+ elif prune_response.status == "success":
59
72
  click.secho("Pruning complete.", fg="green")
60
73
  else:
61
74
  click.secho(
62
75
  f"Failed to prune backups: {prune_response.message}", fg="red"
63
76
  )
77
+ elif response.status == "success":
78
+ click.secho("Backup completed successfully.", fg="green")
64
79
  else:
65
80
  click.secho(f"Failed to create backup: {response.message}", fg="red")
66
81
 
@@ -79,7 +94,7 @@ async def create_backup(ctx, server_name: str, backup_type: str, file_to_backup:
79
94
  type=click.Path(exists=True, dir_okay=False, resolve_path=True),
80
95
  help="Path to the backup file to restore; skips interactive menu.",
81
96
  )
82
- @click.pass_context
97
+ @pass_async_context
83
98
  async def restore_backup(ctx, server_name: str, backup_file_path: str):
84
99
  """Restores server data from a specified backup file."""
85
100
  client = ctx.obj.get("client")
@@ -116,7 +131,14 @@ async def restore_backup(ctx, server_name: str, backup_file_path: str):
116
131
  )
117
132
  response = await client.async_restore_server_backup(server_name, payload)
118
133
 
119
- if response.status == "success":
134
+ if response.task_id:
135
+ await monitor_task(
136
+ client,
137
+ response.task_id,
138
+ "Restore completed successfully",
139
+ "Failed to restore backup",
140
+ )
141
+ elif response.status == "success":
120
142
  click.secho("Restore completed successfully.", fg="green")
121
143
  else:
122
144
  click.secho(f"Failed to restore backup: {response.message}", fg="red")
@@ -133,7 +155,7 @@ async def restore_backup(ctx, server_name: str, backup_file_path: str):
133
155
  required=True,
134
156
  help="Name of the server whose backups to prune.",
135
157
  )
136
- @click.pass_context
158
+ @pass_async_context
137
159
  async def prune_backups(ctx, server_name: str):
138
160
  """Deletes old backups for a server."""
139
161
  client = ctx.obj.get("client")
@@ -144,7 +166,14 @@ async def prune_backups(ctx, server_name: str):
144
166
  try:
145
167
  click.echo(f"Pruning old backups for server '{server_name}'...")
146
168
  response = await client.async_prune_server_backups(server_name)
147
- if response.status == "success":
169
+ if response.task_id:
170
+ await monitor_task(
171
+ client,
172
+ response.task_id,
173
+ "Pruning complete",
174
+ "Failed to prune backups",
175
+ )
176
+ elif response.status == "success":
148
177
  click.secho("Pruning complete.", fg="green")
149
178
  else:
150
179
  click.secho(f"Failed to prune backups: {response.message}", fg="red")
@@ -0,0 +1,20 @@
1
+ # src/bsm_api_client/cli/content.py
2
+ """CLI commands for content management."""
3
+ import click
4
+ from .decorators import pass_async_context
5
+
6
+
7
+ @click.group()
8
+ def content():
9
+ """Commands for managing content."""
10
+ pass
11
+
12
+
13
+ @content.command()
14
+ @click.argument("file_path", type=click.Path(exists=True, dir_okay=False))
15
+ @pass_async_context
16
+ async def upload(ctx, file_path):
17
+ """Upload a content file."""
18
+ client = ctx.obj["client"]
19
+ response = await client.async_upload_content(file_path)
20
+ click.echo(response)
@@ -0,0 +1,73 @@
1
+ import asyncio
2
+ import functools
3
+ import time
4
+ import click
5
+
6
+
7
+ class AsyncGroup(click.Group):
8
+ def __init__(self, *args, **kwargs):
9
+ super().__init__(*args, **kwargs)
10
+ self.async_context_settings = {}
11
+
12
+ def context(self, f):
13
+ self.async_context_settings["context"] = f
14
+ return f
15
+
16
+ def invoke(self, ctx):
17
+ ctx.obj = ctx.obj or {}
18
+ if self.async_context_settings.get("context"):
19
+
20
+ async def runner():
21
+ async with self.async_context_settings["context"](ctx):
22
+ result = super(AsyncGroup, self).invoke(ctx)
23
+ if asyncio.iscoroutine(result):
24
+ await result
25
+
26
+ return asyncio.run(runner())
27
+
28
+ result = super().invoke(ctx)
29
+ if asyncio.iscoroutine(result):
30
+ return asyncio.run(result)
31
+ return result
32
+
33
+
34
+ def pass_async_context(f):
35
+ @functools.wraps(f)
36
+ def wrapper(*args, **kwargs):
37
+ ctx = click.get_current_context()
38
+ return f(ctx, *args, **kwargs)
39
+
40
+ return wrapper
41
+
42
+
43
+ async def monitor_task(
44
+ client, task_id: str, success_message: str, failure_message: str
45
+ ):
46
+ """Polls the status of a background task until it completes."""
47
+ click.echo("Task started in the background. Polling for completion...")
48
+ while True:
49
+ try:
50
+ status_response = await client.async_get_task_status(task_id)
51
+ status = status_response.get("status")
52
+ message = status_response.get("message", "No message provided.")
53
+
54
+ if status == "success":
55
+ click.secho(f"{success_message}: {message}", fg="green")
56
+ break
57
+ elif status == "error":
58
+ click.secho(
59
+ f"{failure_message}: {message}",
60
+ fg="red",
61
+ )
62
+ break
63
+ elif status == "pending":
64
+ # Still waiting, continue loop
65
+ pass
66
+ else:
67
+ # Handle unexpected status
68
+ click.secho(f"Unknown task status received: {status}", fg="yellow")
69
+
70
+ time.sleep(2)
71
+ except Exception as e:
72
+ click.secho(f"An error occurred while monitoring task: {e}", fg="red")
73
+ break
@@ -82,7 +82,7 @@ async def main_menu(ctx: click.Context):
82
82
  await ctx.invoke(list_servers)
83
83
 
84
84
  # --- Dynamically build menu choices ---
85
- response = await client.async_get_servers_details()
85
+ response = await client.async_get_servers()
86
86
  server_names = (
87
87
  [s["name"] for s in response.servers] if response.servers else []
88
88
  )
@@ -2,7 +2,7 @@ import os
2
2
  import time
3
3
  import click
4
4
  import questionary
5
- from .decorators import pass_async_context
5
+ from .decorators import pass_async_context, monitor_task
6
6
  from bsm_api_client.models import InstallServerPayload, CommandPayload
7
7
 
8
8
 
@@ -61,7 +61,7 @@ async def list_servers(ctx, loop, server_name):
61
61
  return
62
62
 
63
63
  async def _display_status():
64
- response = await client.async_get_servers_details()
64
+ response = await client.async_get_servers()
65
65
  all_servers = response.servers
66
66
 
67
67
  if server_name:
@@ -120,7 +120,7 @@ async def start_server(ctx, server_name: str):
120
120
  @click.option(
121
121
  "-s", "--server", "server_name", required=True, help="Name of the server to stop."
122
122
  )
123
- @click.pass_context
123
+ @pass_async_context
124
124
  async def stop_server(ctx, server_name: str):
125
125
  """Sends a graceful stop command to a running Bedrock server."""
126
126
  client = ctx.obj.get("client")
@@ -131,7 +131,14 @@ async def stop_server(ctx, server_name: str):
131
131
  click.echo(f"Attempting to stop server '{server_name}'...")
132
132
  try:
133
133
  response = await client.async_stop_server(server_name)
134
- if response.status == "success":
134
+ if response.task_id:
135
+ await monitor_task(
136
+ client,
137
+ response.task_id,
138
+ "Server stopped successfully",
139
+ "Failed to stop server",
140
+ )
141
+ elif response.status == "success":
135
142
  click.secho(f"Stop signal sent to server '{server_name}'.", fg="green")
136
143
  else:
137
144
  click.secho(f"Failed to stop server: {response.message}", fg="red")
@@ -147,7 +154,7 @@ async def stop_server(ctx, server_name: str):
147
154
  required=True,
148
155
  help="Name of the server to restart.",
149
156
  )
150
- @click.pass_context
157
+ @pass_async_context
151
158
  async def restart_server(ctx, server_name: str):
152
159
  """Gracefully restarts a specific Bedrock server."""
153
160
  client = ctx.obj.get("client")
@@ -158,7 +165,14 @@ async def restart_server(ctx, server_name: str):
158
165
  click.echo(f"Attempting to restart server '{server_name}'...")
159
166
  try:
160
167
  response = await client.async_restart_server(server_name)
161
- if response.status == "success":
168
+ if response.task_id:
169
+ await monitor_task(
170
+ client,
171
+ response.task_id,
172
+ "Server restarted successfully",
173
+ "Failed to restart server",
174
+ )
175
+ elif response.status == "success":
162
176
  click.secho(f"Restart signal sent to server '{server_name}'.", fg="green")
163
177
  else:
164
178
  click.secho(f"Failed to restart server: {response.message}", fg="red")
@@ -233,23 +247,12 @@ async def install(ctx):
233
247
  install_result = await client.async_install_new_server(payload)
234
248
 
235
249
  if install_result.task_id:
236
- time.sleep(5) # Give the server time to start installation
237
- click.echo("Server installation started. Polling for completion...")
238
- while True:
239
- status_response = await client.async_get_task_status(
240
- install_result.task_id
241
- )
242
- if status_response["status"] == "success":
243
- click.secho(
244
- "Server installation completed successfully.", fg="green"
245
- )
246
- break
247
- elif status_response["status"] == "error":
248
- click.secho(
249
- f"Installation failed: {status_response['message']}", fg="red"
250
- )
251
- return
252
- time.sleep(2)
250
+ await monitor_task(
251
+ client,
252
+ install_result.task_id,
253
+ "Server installation completed successfully",
254
+ "Installation failed",
255
+ )
253
256
  elif install_result.status == "success":
254
257
  click.secho("Server files installed successfully.", fg="green")
255
258
  else:
@@ -283,7 +286,7 @@ async def install(ctx):
283
286
  @click.option(
284
287
  "-s", "--server", "server_name", required=True, help="Name of the server to update."
285
288
  )
286
- @click.pass_context
289
+ @pass_async_context
287
290
  async def update(ctx, server_name: str):
288
291
  """Checks for and applies updates to an existing Bedrock server."""
289
292
  client = ctx.obj.get("client")
@@ -294,7 +297,14 @@ async def update(ctx, server_name: str):
294
297
  click.echo(f"Checking for updates for server '{server_name}'...")
295
298
  try:
296
299
  response = await client.async_update_server(server_name)
297
- if response.status == "success":
300
+ if response.task_id:
301
+ await monitor_task(
302
+ client,
303
+ response.task_id,
304
+ "Server update completed successfully",
305
+ "Failed to update server",
306
+ )
307
+ elif response.status == "success":
298
308
  click.secho("Update check complete.", fg="green")
299
309
  else:
300
310
  click.secho(f"Failed to update server: {response.message}", fg="red")
@@ -307,7 +317,7 @@ async def update(ctx, server_name: str):
307
317
  "-s", "--server", "server_name", required=True, help="Name of the server to delete."
308
318
  )
309
319
  @click.option("-y", "--yes", is_flag=True, help="Bypass the confirmation prompt.")
310
- @click.pass_context
320
+ @pass_async_context
311
321
  async def delete_server(ctx, server_name: str, yes: bool):
312
322
  """Deletes all data for a server, including world, configs, and backups."""
313
323
  client = ctx.obj.get("client")
@@ -329,7 +339,14 @@ async def delete_server(ctx, server_name: str, yes: bool):
329
339
  click.echo(f"Proceeding with deletion of server '{server_name}'...")
330
340
  try:
331
341
  response = await client.async_delete_server(server_name)
332
- if response.status == "success":
342
+ if response.task_id:
343
+ await monitor_task(
344
+ client,
345
+ response.task_id,
346
+ "Server deleted successfully",
347
+ "Failed to delete server",
348
+ )
349
+ elif response.status == "success":
333
350
  click.secho(
334
351
  f"Server '{server_name}' and all its data have been deleted.",
335
352
  fg="green",