fastapi-cloud-cli 0.21.0__tar.gz → 0.22.1__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 (100) hide show
  1. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/PKG-INFO +1 -1
  2. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/pyproject.toml +1 -1
  3. fastapi_cloud_cli-0.22.1/src/fastapi_cloud_cli/__init__.py +1 -0
  4. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/apps/__init__.py +2 -0
  5. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/apps/create.py +2 -2
  6. fastapi_cloud_cli-0.22.1/src/fastapi_cloud_cli/commands/apps/update.py +122 -0
  7. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/logs.py +13 -1
  8. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_cli_apps.py +136 -1
  9. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_logs.py +13 -0
  10. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/__init__.py +0 -1
  11. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/LICENSE +0 -0
  12. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/README.md +0 -0
  13. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/scripts/add_latest_release_date.py +0 -0
  14. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/scripts/format.sh +0 -0
  15. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/scripts/lint.sh +0 -0
  16. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/scripts/prepare_release.py +0 -0
  17. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/scripts/test-cov-html.sh +0 -0
  18. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/scripts/test.sh +0 -0
  19. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/__main__.py +0 -0
  20. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/cli.py +0 -0
  21. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/__init__.py +0 -0
  22. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/_flow.py +0 -0
  23. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/apps/get.py +0 -0
  24. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/apps/link.py +0 -0
  25. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/apps/list.py +0 -0
  26. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/apps/unlink.py +0 -0
  27. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/auth/__init__.py +0 -0
  28. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/auth/wait.py +0 -0
  29. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/ci/__init__.py +0 -0
  30. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/ci/print_workflow.py +0 -0
  31. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/deploy/__init__.py +0 -0
  32. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/deploy/archive.py +0 -0
  33. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/deploy/cloud.py +0 -0
  34. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/deploy/command.py +0 -0
  35. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/deploy/configure.py +0 -0
  36. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/deploy/upload.py +0 -0
  37. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/deploy/wait.py +0 -0
  38. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/deployments.py +0 -0
  39. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/env/__init__.py +0 -0
  40. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/env/_shared.py +0 -0
  41. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/env/delete.py +0 -0
  42. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/env/get.py +0 -0
  43. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/env/list.py +0 -0
  44. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/env/set.py +0 -0
  45. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/login.py +0 -0
  46. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/logout.py +0 -0
  47. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/setup_ci.py +0 -0
  48. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/teams/__init__.py +0 -0
  49. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/teams/get.py +0 -0
  50. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/tokens/__init__.py +0 -0
  51. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/tokens/create.py +0 -0
  52. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/tokens/delete.py +0 -0
  53. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/tokens/list.py +0 -0
  54. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/commands/whoami.py +0 -0
  55. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/config.py +0 -0
  56. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/logging.py +0 -0
  57. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/py.typed +0 -0
  58. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/utils/__init__.py +0 -0
  59. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/utils/api.py +0 -0
  60. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/utils/apps.py +0 -0
  61. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/utils/auth.py +0 -0
  62. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/utils/cli.py +0 -0
  63. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/utils/config.py +0 -0
  64. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/utils/dates.py +0 -0
  65. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/utils/env.py +0 -0
  66. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/utils/errors.py +0 -0
  67. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/utils/execution.py +0 -0
  68. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/utils/progress_file.py +0 -0
  69. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/utils/sentry.py +0 -0
  70. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/src/fastapi_cloud_cli/utils/version_check.py +0 -0
  71. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/__init__.py +0 -0
  72. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/conftest.py +0 -0
  73. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_api_client.py +0 -0
  74. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_archive.py +0 -0
  75. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_auth.py +0 -0
  76. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_cli.py +0 -0
  77. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_cli_ci.py +0 -0
  78. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_cli_deploy.py +0 -0
  79. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_cli_deployments.py +0 -0
  80. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_cli_link.py +0 -0
  81. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_cli_login.py +0 -0
  82. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_cli_logout.py +0 -0
  83. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_cli_setup_ci.py +0 -0
  84. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_cli_teams.py +0 -0
  85. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_cli_tokens.py +0 -0
  86. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_cli_unlink.py +0 -0
  87. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_cli_whoami.py +0 -0
  88. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_config.py +0 -0
  89. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_dates.py +0 -0
  90. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_deploy_utils.py +0 -0
  91. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_env_delete.py +0 -0
  92. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_env_get.py +0 -0
  93. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_env_list.py +0 -0
  94. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_env_set.py +0 -0
  95. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_prepare_release.py +0 -0
  96. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_progress_file.py +0 -0
  97. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_sentry.py +0 -0
  98. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_utils_apps.py +0 -0
  99. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/test_version_check.py +0 -0
  100. {fastapi_cloud_cli-0.21.0 → fastapi_cloud_cli-0.22.1}/tests/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastapi-cloud-cli
3
- Version: 0.21.0
3
+ Version: 0.22.1
4
4
  Summary: Deploy and manage FastAPI Cloud apps from the command line 🚀
5
5
  Author-Email: Patrick Arminio <patrick@fastapilabs.com>
6
6
  License: MIT
@@ -41,7 +41,7 @@ dependencies = [
41
41
  "fastar >= 0.10.0",
42
42
  "detect-installer>=0.1.0",
43
43
  ]
44
- version = "0.21.0"
44
+ version = "0.22.1"
45
45
 
46
46
  [project.license]
47
47
  text = "MIT"
@@ -0,0 +1 @@
1
+ __version__ = "0.22.1"
@@ -5,6 +5,7 @@ from fastapi_cloud_cli.commands.apps.get import get_app
5
5
  from fastapi_cloud_cli.commands.apps.link import link_app
6
6
  from fastapi_cloud_cli.commands.apps.list import list_apps
7
7
  from fastapi_cloud_cli.commands.apps.unlink import unlink_app
8
+ from fastapi_cloud_cli.commands.apps.update import update_app
8
9
  from fastapi_cloud_cli.commands.logs import logs
9
10
 
10
11
  apps_app = typer.Typer(
@@ -17,5 +18,6 @@ apps_app.command("link")(link_app)
17
18
  apps_app.command("list")(list_apps)
18
19
  apps_app.command("logs")(logs)
19
20
  apps_app.command("unlink")(unlink_app)
21
+ apps_app.command("update")(update_app)
20
22
 
21
23
  __all__ = ["apps_app"]
@@ -77,7 +77,7 @@ def create_app(
77
77
  "--directory",
78
78
  help=(
79
79
  "Relative app directory containing the pyproject.toml "
80
- "(for example: src or backend)."
80
+ "(for example: backend or webserver)."
81
81
  ),
82
82
  ),
83
83
  ] = None,
@@ -156,7 +156,7 @@ def create_app(
156
156
  "invalid_input",
157
157
  f"Invalid app directory: {e}",
158
158
  hint=(
159
- "Pass a relative app directory such as `src` or `backend`; "
159
+ "Pass a relative app directory such as `backend` or `webserver`; "
160
160
  "use --path with --link to choose a local filesystem path."
161
161
  ),
162
162
  )
@@ -0,0 +1,122 @@
1
+ import logging
2
+ from typing import Annotated, Any
3
+
4
+ import typer
5
+ from pydantic import BaseModel
6
+ from rich_toolkit import RichToolkit
7
+
8
+ from fastapi_cloud_cli.commands.deploy.archive import validate_app_directory
9
+ from fastapi_cloud_cli.utils.api import APIClient
10
+ from fastapi_cloud_cli.utils.apps import resolve_app_id_or_fail
11
+ from fastapi_cloud_cli.utils.auth import Identity
12
+ from fastapi_cloud_cli.utils.cli import get_rich_toolkit
13
+ from fastapi_cloud_cli.utils.execution import JsonOutputOption
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class UpdatedApp(BaseModel):
19
+ id: str
20
+ team_id: str
21
+ slug: str
22
+ name: str
23
+ directory: str | None
24
+
25
+
26
+ class AppsUpdateOutput(BaseModel):
27
+ app: UpdatedApp
28
+
29
+
30
+ def _update_app(client: APIClient, *, app_id: str, directory: str | None) -> UpdatedApp:
31
+ response = client.patch(
32
+ f"/apps/{app_id}",
33
+ json={"directory": directory},
34
+ )
35
+ response.raise_for_status()
36
+
37
+ return UpdatedApp.model_validate(response.json())
38
+
39
+
40
+ def _render_apps_update_output(data: AppsUpdateOutput, toolkit: RichToolkit) -> None:
41
+ toolkit.print(f"Updated app [bold]{data.app.name}[/bold]", bullet=False)
42
+ toolkit.print(
43
+ f"Directory: [bold]{data.app.directory if data.app.directory is not None else '.'}[/bold]",
44
+ bullet=False,
45
+ )
46
+
47
+
48
+ def update_app(
49
+ app_id: Annotated[
50
+ str | None,
51
+ typer.Argument(
52
+ help="ID of the app to update (defaults to the app linked to the current directory).",
53
+ ),
54
+ ] = None,
55
+ directory: Annotated[
56
+ str | None,
57
+ typer.Option(
58
+ "--directory",
59
+ help=(
60
+ "Relative app directory containing the pyproject.toml "
61
+ "(for example: src or backend)."
62
+ ),
63
+ ),
64
+ ] = None,
65
+ json_output: JsonOutputOption = False,
66
+ ) -> Any:
67
+ """
68
+ Update FastAPI Cloud app metadata.
69
+ """
70
+ identity = Identity()
71
+
72
+ with get_rich_toolkit(json_output=json_output) as toolkit:
73
+ if not identity.is_logged_in():
74
+ toolkit.fail(
75
+ "not_logged_in",
76
+ "No credentials found.",
77
+ hint="Run `fastapi cloud login` or set FASTAPI_CLOUD_TOKEN.",
78
+ )
79
+
80
+ if directory is None:
81
+ toolkit.fail(
82
+ "missing_required_input",
83
+ "No updates provided.",
84
+ hint="Pass --directory to update the app directory.",
85
+ )
86
+
87
+ target_app_id = resolve_app_id_or_fail(
88
+ toolkit,
89
+ app_id=app_id,
90
+ hint="Pass an app ID or run `fastapi cloud apps create --link` first.",
91
+ )
92
+
93
+ try:
94
+ directory = validate_app_directory(directory)
95
+ except ValueError as e:
96
+ toolkit.fail(
97
+ "invalid_input",
98
+ f"Invalid app directory: {e}",
99
+ hint="Pass a relative app directory such as `src` or `backend`.",
100
+ )
101
+
102
+ with APIClient() as client:
103
+ with toolkit.progress(
104
+ title="Updating app",
105
+ transient=True,
106
+ ) as progress:
107
+ with client.handle_http_errors(
108
+ progress,
109
+ default_message="Error updating app. Please try again later.",
110
+ not_found_message="App not found.",
111
+ toolkit=toolkit,
112
+ ):
113
+ app = _update_app(
114
+ client,
115
+ app_id=target_app_id,
116
+ directory=directory,
117
+ )
118
+
119
+ toolkit.success(
120
+ AppsUpdateOutput(app=app),
121
+ render_output=_render_apps_update_output,
122
+ )
@@ -40,6 +40,8 @@ LOG_LEVEL_COLORS = {
40
40
  }
41
41
 
42
42
  SINCE_PATTERN = re.compile(r"^\d+[smhd]$")
43
+ MIN_LOG_TAIL = 1
44
+ MAX_LOG_TAIL = 1000
43
45
 
44
46
 
45
47
  class AppLogsOutput(BaseModel):
@@ -57,6 +59,15 @@ def _validate_since(value: str) -> str:
57
59
  return value
58
60
 
59
61
 
62
+ def _validate_tail(value: int) -> int:
63
+ if not MIN_LOG_TAIL <= value <= MAX_LOG_TAIL:
64
+ raise typer.BadParameter(
65
+ f"Invalid value. Use a number between {MIN_LOG_TAIL} and {MAX_LOG_TAIL}."
66
+ )
67
+
68
+ return value
69
+
70
+
60
71
  def _get_log_bullet(log: AppLogEntry) -> str:
61
72
  """Colored indicator rendered in the emoji bullet column.
62
73
 
@@ -214,8 +225,9 @@ def logs(
214
225
  100,
215
226
  "--tail",
216
227
  "-t",
217
- help="Number of log lines to show before streaming.",
228
+ help=f"Number of log lines to show before streaming (max {MAX_LOG_TAIL}).",
218
229
  show_default=True,
230
+ callback=_validate_tail,
219
231
  ),
220
232
  since: str = typer.Option(
221
233
  "5m",
@@ -344,7 +344,7 @@ def test_creates_app_json_rejects_invalid_directory(logged_in_cli: None) -> None
344
344
  "code": "invalid_input",
345
345
  "message": ("Invalid app directory: must be a relative path, not absolute"),
346
346
  "hint": (
347
- "Pass a relative app directory such as `src` or `backend`; "
347
+ "Pass a relative app directory such as `backend` or `webserver`; "
348
348
  "use --path with --link to choose a local filesystem path."
349
349
  ),
350
350
  }
@@ -352,6 +352,141 @@ def test_creates_app_json_rejects_invalid_directory(logged_in_cli: None) -> None
352
352
  assert result.stderr == ""
353
353
 
354
354
 
355
+ def test_updates_app_json_returns_not_logged_in_when_logged_out(
356
+ logged_out_cli: None,
357
+ ) -> None:
358
+ result = runner.invoke(
359
+ app,
360
+ [
361
+ "apps",
362
+ "update",
363
+ "00000000-0000-4000-8000-000000000002",
364
+ "--directory",
365
+ "backend",
366
+ "--json",
367
+ ],
368
+ )
369
+
370
+ assert result.exit_code == 1
371
+ assert json.loads(result.stdout) == {
372
+ "error": {
373
+ "code": "not_logged_in",
374
+ "message": "No credentials found.",
375
+ "hint": "Run `fastapi cloud login` or set FASTAPI_CLOUD_TOKEN.",
376
+ }
377
+ }
378
+ assert result.stderr == ""
379
+
380
+
381
+ @pytest.mark.respx
382
+ def test_updates_app_directory_as_json(
383
+ logged_in_cli: None,
384
+ respx_mock: respx.MockRouter,
385
+ ) -> None:
386
+ app_id = "00000000-0000-4000-8000-000000000002"
387
+ app_data = {
388
+ "id": app_id,
389
+ "team_id": "00000000-0000-4000-8000-000000000001",
390
+ "slug": "api",
391
+ "name": "API",
392
+ "directory": "backend",
393
+ }
394
+ respx_mock.patch(
395
+ f"/apps/{app_id}",
396
+ json={"directory": "backend"},
397
+ ).mock(return_value=Response(200, json=app_data))
398
+
399
+ result = runner.invoke(
400
+ app,
401
+ [
402
+ "apps",
403
+ "update",
404
+ app_id,
405
+ "--directory",
406
+ "backend",
407
+ "--json",
408
+ ],
409
+ )
410
+
411
+ assert result.exit_code == 0
412
+ assert json.loads(result.stdout) == {"data": {"app": app_data}}
413
+ assert result.stderr == ""
414
+
415
+
416
+ @pytest.mark.respx
417
+ def test_updates_linked_app_directory_in_human_output(
418
+ logged_in_cli: None,
419
+ respx_mock: respx.MockRouter,
420
+ configured_app: ConfiguredApp,
421
+ ) -> None:
422
+ app_data = {
423
+ "id": configured_app.app_id,
424
+ "team_id": configured_app.team_id,
425
+ "slug": "api",
426
+ "name": "API",
427
+ "directory": "src",
428
+ }
429
+ respx_mock.patch(
430
+ f"/apps/{configured_app.app_id}",
431
+ json={"directory": "src"},
432
+ ).mock(return_value=Response(200, json=app_data))
433
+
434
+ with changing_dir(configured_app.path):
435
+ result = runner.invoke(app, ["apps", "update", "--directory", "src"])
436
+
437
+ assert result.exit_code == 0
438
+ assert "Updated app API" in result.output
439
+ assert "Directory: src" in result.output
440
+
441
+
442
+ def test_updates_app_json_returns_missing_required_input_without_update_flags(
443
+ logged_in_cli: None,
444
+ ) -> None:
445
+ result = runner.invoke(
446
+ app,
447
+ [
448
+ "apps",
449
+ "update",
450
+ "00000000-0000-4000-8000-000000000002",
451
+ "--json",
452
+ ],
453
+ )
454
+
455
+ assert result.exit_code == 1
456
+ assert json.loads(result.stdout) == {
457
+ "error": {
458
+ "code": "missing_required_input",
459
+ "message": "No updates provided.",
460
+ "hint": "Pass --directory to update the app directory.",
461
+ }
462
+ }
463
+ assert result.stderr == ""
464
+
465
+
466
+ def test_updates_app_json_rejects_invalid_directory(logged_in_cli: None) -> None:
467
+ result = runner.invoke(
468
+ app,
469
+ [
470
+ "apps",
471
+ "update",
472
+ "00000000-0000-4000-8000-000000000002",
473
+ "--directory",
474
+ "/tmp/api",
475
+ "--json",
476
+ ],
477
+ )
478
+
479
+ assert result.exit_code == 1
480
+ assert json.loads(result.stdout) == {
481
+ "error": {
482
+ "code": "invalid_input",
483
+ "message": ("Invalid app directory: must be a relative path, not absolute"),
484
+ "hint": "Pass a relative app directory such as `src` or `backend`.",
485
+ }
486
+ }
487
+ assert result.stderr == ""
488
+
489
+
355
490
  @pytest.mark.respx
356
491
  def test_links_existing_app_to_path_as_json(
357
492
  logged_in_cli: None,
@@ -540,6 +540,19 @@ def test_rejects_invalid_since_format(
540
540
  assert "Invalid format" in result.output
541
541
 
542
542
 
543
+ @pytest.mark.parametrize("invalid_tail", [0, 1001])
544
+ def test_rejects_invalid_tail(
545
+ logged_in_cli: None,
546
+ configured_app: ConfiguredApp,
547
+ invalid_tail: int,
548
+ ) -> None:
549
+ with changing_dir(configured_app.path):
550
+ result = runner.invoke(app, ["logs", "--tail", str(invalid_tail)])
551
+
552
+ assert result.exit_code == 2
553
+ assert "between 1 and 1000" in result.output
554
+
555
+
543
556
  @pytest.mark.respx
544
557
  @pytest.mark.parametrize(
545
558
  "valid_since",
@@ -1 +0,0 @@
1
- __version__ = "0.21.0"