fastapi-cloud-cli 0.19.0__tar.gz → 0.21.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 (117) hide show
  1. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/PKG-INFO +2 -5
  2. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/README.md +0 -3
  3. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/pyproject.toml +22 -7
  4. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/__init__.py +1 -0
  5. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/src/fastapi_cloud_cli/cli.py +17 -5
  6. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/_flow.py +159 -0
  7. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/apps/__init__.py +21 -0
  8. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/apps/create.py +192 -0
  9. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/apps/get.py +122 -0
  10. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/apps/link.py +249 -0
  11. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/apps/list.py +268 -0
  12. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/apps/unlink.py +75 -0
  13. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/auth/__init__.py +14 -0
  14. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/auth/wait.py +67 -0
  15. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/ci/__init__.py +15 -0
  16. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/ci/print_workflow.py +45 -0
  17. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/deploy/__init__.py +3 -0
  18. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/deploy/archive.py +115 -0
  19. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/deploy/cloud.py +86 -0
  20. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/deploy/command.py +370 -0
  21. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/deploy/configure.py +161 -0
  22. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/deploy/upload.py +93 -0
  23. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/deploy/wait.py +170 -0
  24. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/deployments.py +497 -0
  25. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/env/__init__.py +17 -0
  26. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/env/_shared.py +53 -0
  27. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/env/delete.py +194 -0
  28. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/env/get.py +139 -0
  29. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/env/list.py +113 -0
  30. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/env/set.py +215 -0
  31. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/login.py +120 -0
  32. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/logout.py +30 -0
  33. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/logs.py +282 -0
  34. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/setup_ci.py +536 -0
  35. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/teams/__init__.py +138 -0
  36. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/teams/get.py +91 -0
  37. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/tokens/__init__.py +15 -0
  38. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/tokens/create.py +182 -0
  39. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/tokens/delete.py +102 -0
  40. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/tokens/list.py +115 -0
  41. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/commands/whoami.py +68 -0
  42. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/src/fastapi_cloud_cli/config.py +1 -0
  43. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/src/fastapi_cloud_cli/utils/api.py +99 -13
  44. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/src/fastapi_cloud_cli/utils/apps.py +36 -0
  45. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/utils/cli.py +458 -0
  46. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/utils/errors.py +30 -0
  47. fastapi_cloud_cli-0.21.0/src/fastapi_cloud_cli/utils/execution.py +22 -0
  48. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/tests/conftest.py +1 -0
  49. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/tests/test_api_client.py +47 -22
  50. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/tests/test_archive.py +1 -1
  51. fastapi_cloud_cli-0.21.0/tests/test_cli.py +500 -0
  52. fastapi_cloud_cli-0.21.0/tests/test_cli_apps.py +848 -0
  53. fastapi_cloud_cli-0.21.0/tests/test_cli_ci.py +297 -0
  54. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/tests/test_cli_deploy.py +343 -190
  55. fastapi_cloud_cli-0.21.0/tests/test_cli_deployments.py +768 -0
  56. fastapi_cloud_cli-0.21.0/tests/test_cli_link.py +446 -0
  57. fastapi_cloud_cli-0.21.0/tests/test_cli_login.py +593 -0
  58. fastapi_cloud_cli-0.21.0/tests/test_cli_logout.py +54 -0
  59. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/tests/test_cli_setup_ci.py +210 -18
  60. fastapi_cloud_cli-0.21.0/tests/test_cli_teams.py +226 -0
  61. fastapi_cloud_cli-0.21.0/tests/test_cli_tokens.py +702 -0
  62. fastapi_cloud_cli-0.21.0/tests/test_cli_unlink.py +122 -0
  63. fastapi_cloud_cli-0.21.0/tests/test_cli_whoami.py +284 -0
  64. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/tests/test_deploy_utils.py +1 -1
  65. fastapi_cloud_cli-0.21.0/tests/test_env_delete.py +309 -0
  66. fastapi_cloud_cli-0.21.0/tests/test_env_get.py +247 -0
  67. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/tests/test_env_list.py +105 -14
  68. fastapi_cloud_cli-0.21.0/tests/test_env_set.py +312 -0
  69. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/tests/test_logs.py +194 -17
  70. fastapi_cloud_cli-0.21.0/tests/test_utils_apps.py +29 -0
  71. fastapi_cloud_cli-0.19.0/src/fastapi_cloud_cli/__init__.py +0 -1
  72. fastapi_cloud_cli-0.19.0/src/fastapi_cloud_cli/commands/deploy.py +0 -905
  73. fastapi_cloud_cli-0.19.0/src/fastapi_cloud_cli/commands/env.py +0 -329
  74. fastapi_cloud_cli-0.19.0/src/fastapi_cloud_cli/commands/link.py +0 -120
  75. fastapi_cloud_cli-0.19.0/src/fastapi_cloud_cli/commands/login.py +0 -134
  76. fastapi_cloud_cli-0.19.0/src/fastapi_cloud_cli/commands/logout.py +0 -12
  77. fastapi_cloud_cli-0.19.0/src/fastapi_cloud_cli/commands/logs.py +0 -189
  78. fastapi_cloud_cli-0.19.0/src/fastapi_cloud_cli/commands/setup_ci.py +0 -365
  79. fastapi_cloud_cli-0.19.0/src/fastapi_cloud_cli/commands/unlink.py +0 -29
  80. fastapi_cloud_cli-0.19.0/src/fastapi_cloud_cli/commands/whoami.py +0 -37
  81. fastapi_cloud_cli-0.19.0/src/fastapi_cloud_cli/utils/cli.py +0 -128
  82. fastapi_cloud_cli-0.19.0/tests/test_cli.py +0 -84
  83. fastapi_cloud_cli-0.19.0/tests/test_cli_link.py +0 -189
  84. fastapi_cloud_cli-0.19.0/tests/test_cli_login.py +0 -238
  85. fastapi_cloud_cli-0.19.0/tests/test_cli_logout.py +0 -31
  86. fastapi_cloud_cli-0.19.0/tests/test_cli_unlink.py +0 -50
  87. fastapi_cloud_cli-0.19.0/tests/test_cli_whoami.py +0 -104
  88. fastapi_cloud_cli-0.19.0/tests/test_env_delete.py +0 -147
  89. fastapi_cloud_cli-0.19.0/tests/test_env_set.py +0 -144
  90. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/LICENSE +0 -0
  91. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/scripts/add_latest_release_date.py +0 -0
  92. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/scripts/format.sh +0 -0
  93. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/scripts/lint.sh +0 -0
  94. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/scripts/prepare_release.py +0 -0
  95. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/scripts/test-cov-html.sh +0 -0
  96. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/scripts/test.sh +0 -0
  97. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/src/fastapi_cloud_cli/__main__.py +0 -0
  98. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/src/fastapi_cloud_cli/commands/__init__.py +0 -0
  99. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/src/fastapi_cloud_cli/logging.py +0 -0
  100. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/src/fastapi_cloud_cli/py.typed +0 -0
  101. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/src/fastapi_cloud_cli/utils/__init__.py +0 -0
  102. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/src/fastapi_cloud_cli/utils/auth.py +0 -0
  103. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/src/fastapi_cloud_cli/utils/config.py +0 -0
  104. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/src/fastapi_cloud_cli/utils/dates.py +0 -0
  105. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/src/fastapi_cloud_cli/utils/env.py +0 -0
  106. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/src/fastapi_cloud_cli/utils/progress_file.py +0 -0
  107. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/src/fastapi_cloud_cli/utils/sentry.py +0 -0
  108. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/src/fastapi_cloud_cli/utils/version_check.py +0 -0
  109. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/tests/__init__.py +0 -0
  110. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/tests/test_auth.py +0 -0
  111. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/tests/test_config.py +0 -0
  112. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/tests/test_dates.py +0 -0
  113. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/tests/test_prepare_release.py +0 -0
  114. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/tests/test_progress_file.py +0 -0
  115. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/tests/test_sentry.py +0 -0
  116. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/tests/test_version_check.py +0 -0
  117. {fastapi_cloud_cli-0.19.0 → fastapi_cloud_cli-0.21.0}/tests/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastapi-cloud-cli
3
- Version: 0.19.0
3
+ Version: 0.21.0
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
@@ -33,7 +33,7 @@ Requires-Dist: typer>=0.16.0
33
33
  Requires-Dist: uvicorn[standard]>=0.17.6
34
34
  Requires-Dist: rignore>=0.5.1
35
35
  Requires-Dist: httpx>=0.27.0
36
- Requires-Dist: rich-toolkit>=0.19.7
36
+ Requires-Dist: rich-toolkit>=0.20.1
37
37
  Requires-Dist: pydantic[email]>=2.7.4; python_version < "3.13"
38
38
  Requires-Dist: pydantic[email]>=2.8.0; python_version == "3.13"
39
39
  Requires-Dist: pydantic[email]>=2.12.0; python_version >= "3.14"
@@ -49,9 +49,6 @@ Description-Content-Type: text/markdown
49
49
  <a href="https://github.com/fastapilabs/fastapi-cloud-cli/actions/workflows/test.yml" target="_blank">
50
50
  <img src="https://github.com/fastapilabs/fastapi-cloud-cli/actions/workflows/test.yml/badge.svg" alt="Test">
51
51
  </a>
52
- <a href="https://github.com/fastapilabs/fastapi-cloud-cli/actions/workflows/publish.yml" target="_blank">
53
- <img src="https://github.com/fastapilabs/fastapi-cloud-cli/actions/workflows/publish.yml/badge.svg" alt="Publish">
54
- </a>
55
52
  <a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/fastapilabs/fastapi-cloud-cli" target="_blank">
56
53
  <img src="https://coverage-badge.samuelcolvin.workers.dev/fastapilabs/fastapi-cloud-cli.svg" alt="Coverage">
57
54
  <a href="https://pypi.org/project/fastapi-cloud-cli" target="_blank">
@@ -3,9 +3,6 @@
3
3
  <a href="https://github.com/fastapilabs/fastapi-cloud-cli/actions/workflows/test.yml" target="_blank">
4
4
  <img src="https://github.com/fastapilabs/fastapi-cloud-cli/actions/workflows/test.yml/badge.svg" alt="Test">
5
5
  </a>
6
- <a href="https://github.com/fastapilabs/fastapi-cloud-cli/actions/workflows/publish.yml" target="_blank">
7
- <img src="https://github.com/fastapilabs/fastapi-cloud-cli/actions/workflows/publish.yml/badge.svg" alt="Publish">
8
- </a>
9
6
  <a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/fastapilabs/fastapi-cloud-cli" target="_blank">
10
7
  <img src="https://coverage-badge.samuelcolvin.workers.dev/fastapilabs/fastapi-cloud-cli.svg" alt="Coverage">
11
8
  <a href="https://pypi.org/project/fastapi-cloud-cli" target="_blank">
@@ -33,7 +33,7 @@ dependencies = [
33
33
  "uvicorn[standard] >= 0.17.6",
34
34
  "rignore >= 0.5.1",
35
35
  "httpx >= 0.27.0",
36
- "rich-toolkit >= 0.19.7",
36
+ "rich-toolkit>=0.20.1",
37
37
  "pydantic[email] >= 2.7.4; python_version < '3.13'",
38
38
  "pydantic[email] >= 2.8.0; python_version == '3.13'",
39
39
  "pydantic[email] >= 2.12.0; python_version >= '3.14'",
@@ -41,7 +41,7 @@ dependencies = [
41
41
  "fastar >= 0.10.0",
42
42
  "detect-installer>=0.1.0",
43
43
  ]
44
- version = "0.19.0"
44
+ version = "0.21.0"
45
45
 
46
46
  [project.license]
47
47
  text = "MIT"
@@ -61,12 +61,12 @@ Changelog = "https://github.com/fastapilabs/fastapi-cloud-cli/blob/main/release-
61
61
  [dependency-groups]
62
62
  dev = [
63
63
  "prek>=0.2.24,<1.0.0",
64
- "pytest>=7.0.0,<9.0.0",
64
+ "pytest>=7.0.0,<10.0.0",
65
65
  "coverage[toml]>=7.2,<8.0",
66
- "mypy==1.14.1",
67
- "ruff==0.13.0",
68
- "respx==0.22.0",
69
- "time-machine==2.15.0",
66
+ "mypy==2.1.0",
67
+ "ruff>=0.15.16",
68
+ "respx==0.23.1",
69
+ "time-machine>=2.19.0",
70
70
  "ty>=0.0.25",
71
71
  "zizmor>=1.24.1",
72
72
  ]
@@ -167,3 +167,18 @@ known-third-party = [
167
167
 
168
168
  [tool.ruff.lint.pyupgrade]
169
169
  keep-runtime-typing = true
170
+
171
+ [tool.typos.files]
172
+ extend-exclude = [
173
+ "uv.lock",
174
+ "coverage/",
175
+ "htmlcov/",
176
+ ]
177
+
178
+ [tool.typos.default]
179
+ extend-ignore-re = [
180
+ "@[a-zA-Z0-9](?:-?[a-zA-Z0-9])*",
181
+ ]
182
+
183
+ [tool.typos.default.extend-identifiers]
184
+ alls = "alls"
@@ -0,0 +1 @@
1
+ __version__ = "0.21.0"
@@ -4,14 +4,20 @@ import typer
4
4
  from rich import print
5
5
 
6
6
  from . import __version__
7
+ from .commands.apps import apps_app
8
+ from .commands.apps.link import link_app
9
+ from .commands.apps.unlink import unlink_app
10
+ from .commands.auth import auth_app
11
+ from .commands.ci import ci_app
7
12
  from .commands.deploy import deploy
13
+ from .commands.deployments import deployments_app
8
14
  from .commands.env import env_app
9
- from .commands.link import link
10
15
  from .commands.login import login
11
16
  from .commands.logout import logout
12
17
  from .commands.logs import logs
13
18
  from .commands.setup_ci import setup_ci
14
- from .commands.unlink import unlink
19
+ from .commands.teams import teams_app
20
+ from .commands.tokens import tokens_app
15
21
  from .commands.whoami import whoami
16
22
  from .logging import setup_logging
17
23
  from .utils.sentry import init_sentry
@@ -29,7 +35,7 @@ def version_callback(value: bool) -> None:
29
35
 
30
36
  cloud_app = typer.Typer(
31
37
  rich_markup_mode="rich",
32
- help="Manage [bold]FastAPI[/bold] Cloud deployments. 🚀",
38
+ help="Manage [bold]FastAPI[/bold] Cloud deployments.",
33
39
  no_args_is_help=True,
34
40
  )
35
41
 
@@ -54,15 +60,21 @@ def cloud_main(
54
60
 
55
61
  # fastapi cloud [command]
56
62
  cloud_app.command()(deploy)
57
- cloud_app.command()(link)
63
+ cloud_app.command("link")(link_app)
58
64
  cloud_app.command()(login)
59
65
  cloud_app.command()(logs)
60
66
  cloud_app.command()(logout)
61
67
  cloud_app.command()(whoami)
62
- cloud_app.command()(unlink)
68
+ cloud_app.command("unlink")(unlink_app)
63
69
  cloud_app.command()(setup_ci)
64
70
 
65
71
  cloud_app.add_typer(env_app, name="env")
72
+ cloud_app.add_typer(auth_app, name="auth")
73
+ cloud_app.add_typer(apps_app, name="apps")
74
+ cloud_app.add_typer(ci_app, name="ci")
75
+ cloud_app.add_typer(deployments_app, name="deployments")
76
+ cloud_app.add_typer(teams_app, name="teams")
77
+ cloud_app.add_typer(tokens_app, name="tokens")
66
78
 
67
79
  # fastapi [command]
68
80
  app.command()(deploy)
@@ -0,0 +1,159 @@
1
+ import logging
2
+ import time
3
+
4
+ import httpx
5
+ from pydantic import BaseModel
6
+ from rich_toolkit import RichToolkit
7
+ from rich_toolkit.progress import Progress
8
+
9
+ from fastapi_cloud_cli.config import Settings
10
+ from fastapi_cloud_cli.utils.api import APIClient
11
+ from fastapi_cloud_cli.utils.auth import AuthConfig, AuthMode, write_auth_config
12
+ from fastapi_cloud_cli.utils.cli import FastAPIRichToolkit
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ DEFAULT_LOGIN_TIMEOUT_SECONDS = 300
17
+
18
+
19
+ class AuthorizationData(BaseModel):
20
+ user_code: str
21
+ device_code: str
22
+ verification_uri: str
23
+ verification_uri_complete: str
24
+ interval: int = 5
25
+
26
+
27
+ class TokenResponse(BaseModel):
28
+ access_token: str
29
+
30
+
31
+ class LoginOutput(BaseModel):
32
+ authenticated: bool
33
+ auth_mode: AuthMode
34
+
35
+
36
+ class DeviceAuthorizationOutput(BaseModel):
37
+ verification_uri: str
38
+ verification_uri_complete: str
39
+ user_code: str
40
+ device_code: str
41
+ interval: int
42
+
43
+
44
+ class LoginTimeoutError(Exception):
45
+ pass
46
+
47
+
48
+ def render_login_output(data: LoginOutput, toolkit: RichToolkit) -> None:
49
+ toolkit.print("Now you are logged in! 🚀")
50
+
51
+
52
+ def device_authorization_output(
53
+ authorization_data: AuthorizationData,
54
+ ) -> DeviceAuthorizationOutput:
55
+ return DeviceAuthorizationOutput(
56
+ verification_uri=authorization_data.verification_uri,
57
+ verification_uri_complete=authorization_data.verification_uri_complete,
58
+ user_code=authorization_data.user_code,
59
+ device_code=authorization_data.device_code,
60
+ interval=authorization_data.interval,
61
+ )
62
+
63
+
64
+ def start_device_authorization(
65
+ client: httpx.Client,
66
+ ) -> AuthorizationData:
67
+ settings = Settings.get()
68
+
69
+ response = client.post(
70
+ "/login/device/authorization", data={"client_id": settings.client_id}
71
+ )
72
+ logger.debug(f"Device authorization response status code: {response.status_code}")
73
+
74
+ response.raise_for_status()
75
+
76
+ return AuthorizationData.model_validate_json(response.text)
77
+
78
+
79
+ def fetch_access_token(
80
+ client: httpx.Client,
81
+ device_code: str,
82
+ interval: int,
83
+ timeout: int = DEFAULT_LOGIN_TIMEOUT_SECONDS,
84
+ ) -> str:
85
+ settings = Settings.get()
86
+ start = time.monotonic()
87
+
88
+ logger.debug("Starting to poll for access token")
89
+ while True:
90
+ response = client.post(
91
+ "/login/device/token",
92
+ data={
93
+ "device_code": device_code,
94
+ "client_id": settings.client_id,
95
+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
96
+ },
97
+ )
98
+ logger.debug(f"Token response status code: {response.status_code}")
99
+
100
+ if response.status_code not in (200, 400):
101
+ response.raise_for_status()
102
+
103
+ if response.status_code == 400:
104
+ data = response.json()
105
+ error = data.get("error")
106
+ logger.debug(f"Token response error: {error}")
107
+
108
+ if error != "authorization_pending":
109
+ response.raise_for_status()
110
+
111
+ if response.status_code == 200:
112
+ break
113
+
114
+ remaining = timeout - (time.monotonic() - start)
115
+ if remaining <= 0:
116
+ raise LoginTimeoutError
117
+
118
+ sleep_for = min(interval, remaining)
119
+
120
+ logger.debug(f"Sleeping for {sleep_for} seconds before retrying...")
121
+ time.sleep(sleep_for)
122
+
123
+ response_data = TokenResponse.model_validate_json(response.text)
124
+ logger.debug("Access token received successfully.")
125
+
126
+ return response_data.access_token
127
+
128
+
129
+ def complete_device_login(
130
+ *,
131
+ client: APIClient,
132
+ progress: Progress,
133
+ toolkit: FastAPIRichToolkit,
134
+ device_code: str,
135
+ interval: int,
136
+ timeout: int,
137
+ cancel_hint: str,
138
+ ) -> LoginOutput:
139
+ try:
140
+ with client.handle_http_errors(progress, toolkit=toolkit):
141
+ access_token = fetch_access_token(client, device_code, interval, timeout)
142
+ except LoginTimeoutError:
143
+ message = "Login timed out before authorization completed."
144
+ toolkit.fail(
145
+ "timeout",
146
+ message,
147
+ hint="Try again with a longer --timeout value.",
148
+ )
149
+ except KeyboardInterrupt:
150
+ message = "Login cancelled before authorization completed."
151
+ toolkit.fail(
152
+ "cancelled",
153
+ message,
154
+ hint=cancel_hint,
155
+ )
156
+
157
+ write_auth_config(AuthConfig(access_token=access_token))
158
+
159
+ return LoginOutput(authenticated=True, auth_mode="user")
@@ -0,0 +1,21 @@
1
+ import typer
2
+
3
+ from fastapi_cloud_cli.commands.apps.create import create_app
4
+ from fastapi_cloud_cli.commands.apps.get import get_app
5
+ from fastapi_cloud_cli.commands.apps.link import link_app
6
+ from fastapi_cloud_cli.commands.apps.list import list_apps
7
+ from fastapi_cloud_cli.commands.apps.unlink import unlink_app
8
+ from fastapi_cloud_cli.commands.logs import logs
9
+
10
+ apps_app = typer.Typer(
11
+ no_args_is_help=True,
12
+ help="Manage your FastAPI Cloud apps.",
13
+ )
14
+ apps_app.command("create")(create_app)
15
+ apps_app.command("get")(get_app)
16
+ apps_app.command("link")(link_app)
17
+ apps_app.command("list")(list_apps)
18
+ apps_app.command("logs")(logs)
19
+ apps_app.command("unlink")(unlink_app)
20
+
21
+ __all__ = ["apps_app"]
@@ -0,0 +1,192 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import Annotated, Any
4
+
5
+ import typer
6
+ from pydantic import BaseModel, Field
7
+ from rich_toolkit import RichToolkit
8
+
9
+ from fastapi_cloud_cli.commands.apps.list import _prompt_for_team
10
+ from fastapi_cloud_cli.commands.deploy.archive import (
11
+ _get_app_name,
12
+ validate_app_directory,
13
+ )
14
+ from fastapi_cloud_cli.utils.api import APIClient
15
+ from fastapi_cloud_cli.utils.apps import AppConfig, write_app_config
16
+ from fastapi_cloud_cli.utils.auth import Identity
17
+ from fastapi_cloud_cli.utils.cli import get_rich_toolkit
18
+ from fastapi_cloud_cli.utils.execution import JsonOutputOption
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class CreatedApp(BaseModel):
24
+ id: str
25
+ team_id: str
26
+ slug: str
27
+ name: str
28
+ directory: str | None
29
+
30
+
31
+ class AppsCreateOutput(BaseModel):
32
+ app: CreatedApp
33
+ linked: bool
34
+ path_to_link: Annotated[Path | None, Field(exclude=True)] = None
35
+
36
+
37
+ def _create_app(
38
+ client: APIClient, *, team_id: str, name: str, directory: str | None
39
+ ) -> CreatedApp:
40
+ response = client.post(
41
+ "/apps/",
42
+ json={"team_id": team_id, "name": name, "directory": directory},
43
+ )
44
+ response.raise_for_status()
45
+
46
+ return CreatedApp.model_validate(response.json())
47
+
48
+
49
+ def _render_apps_create_output(data: AppsCreateOutput, toolkit: RichToolkit) -> None:
50
+ toolkit.print(f"Created app [bold]{data.app.name}[/bold]", bullet=False)
51
+
52
+ if data.linked and data.path_to_link is not None:
53
+ toolkit.print(
54
+ f"Linked [bold]{data.path_to_link}[/bold] to [bold]{data.app.name}[/bold]",
55
+ bullet=False,
56
+ )
57
+
58
+
59
+ def create_app(
60
+ team_id: Annotated[
61
+ str | None,
62
+ typer.Option(
63
+ "--team-id",
64
+ help="ID of the team where the app should be created.",
65
+ ),
66
+ ] = None,
67
+ name: Annotated[
68
+ str | None,
69
+ typer.Option(
70
+ "--name",
71
+ help="Name of the app to create.",
72
+ ),
73
+ ] = None,
74
+ directory: Annotated[
75
+ str | None,
76
+ typer.Option(
77
+ "--directory",
78
+ help=(
79
+ "Relative app directory containing the pyproject.toml "
80
+ "(for example: src or backend)."
81
+ ),
82
+ ),
83
+ ] = None,
84
+ link: Annotated[
85
+ bool | None,
86
+ typer.Option(
87
+ "--link/--no-link",
88
+ help="Link the local directory to the created app.",
89
+ ),
90
+ ] = None,
91
+ path: Annotated[
92
+ Path | None,
93
+ typer.Option(
94
+ "--path",
95
+ help="Directory to link when --link is enabled.",
96
+ ),
97
+ ] = None,
98
+ json_output: JsonOutputOption = False,
99
+ ) -> Any:
100
+ """
101
+ Create a FastAPI Cloud app.
102
+ """
103
+ identity = Identity()
104
+ path_to_link = path or Path.cwd()
105
+
106
+ # JSON output is non-interactive, so it defaults to create-only unless --link is explicit.
107
+ link_app = link if link is not None else not json_output
108
+
109
+ with get_rich_toolkit(json_output=json_output) as toolkit:
110
+ if not identity.is_logged_in():
111
+ toolkit.fail(
112
+ "not_logged_in",
113
+ "No credentials found.",
114
+ hint="Run `fastapi cloud login` or set FASTAPI_CLOUD_TOKEN.",
115
+ )
116
+
117
+ if not link_app and path is not None:
118
+ toolkit.fail(
119
+ "invalid_input",
120
+ "Path can only be used when linking.",
121
+ hint="Pass --link or omit --path.",
122
+ )
123
+
124
+ with APIClient() as client:
125
+ if team_id is None:
126
+ if json_output:
127
+ toolkit.fail(
128
+ "missing_required_input",
129
+ "Team ID is required.",
130
+ hint="Pass --team-id to choose a team.",
131
+ )
132
+
133
+ team = _prompt_for_team(toolkit, client)
134
+ team_id = team.id
135
+ toolkit.print_line()
136
+
137
+ if name is None:
138
+ if json_output:
139
+ toolkit.fail(
140
+ "missing_required_input",
141
+ "App name is required.",
142
+ hint="Pass --name to choose an app name.",
143
+ )
144
+
145
+ name = toolkit.input(
146
+ title="What's your app name?",
147
+ default=_get_app_name(path_to_link),
148
+ bullet=False,
149
+ )
150
+ toolkit.print_line()
151
+
152
+ try:
153
+ directory = validate_app_directory(directory)
154
+ except ValueError as e:
155
+ toolkit.fail(
156
+ "invalid_input",
157
+ f"Invalid app directory: {e}",
158
+ hint=(
159
+ "Pass a relative app directory such as `src` or `backend`; "
160
+ "use --path with --link to choose a local filesystem path."
161
+ ),
162
+ )
163
+
164
+ with toolkit.progress(
165
+ title="Creating app",
166
+ transient=True,
167
+ ) as progress:
168
+ with client.handle_http_errors(
169
+ progress,
170
+ default_message="Error creating app. Please try again later.",
171
+ toolkit=toolkit,
172
+ ):
173
+ app = _create_app(
174
+ client,
175
+ team_id=team_id,
176
+ name=name,
177
+ directory=directory,
178
+ )
179
+
180
+ if link_app:
181
+ write_app_config(
182
+ path_to_link,
183
+ AppConfig(app_id=app.id, team_id=app.team_id),
184
+ )
185
+
186
+ result = AppsCreateOutput(
187
+ app=app,
188
+ linked=link_app,
189
+ path_to_link=path_to_link if link_app else None,
190
+ )
191
+
192
+ toolkit.success(result, render_output=_render_apps_create_output)
@@ -0,0 +1,122 @@
1
+ import logging
2
+ from typing import Annotated, Any
3
+
4
+ import typer
5
+ from pydantic import BaseModel, Field
6
+ from rich.text import Text
7
+ from rich_toolkit import RichToolkit
8
+
9
+ from fastapi_cloud_cli.commands.apps.list import (
10
+ App,
11
+ _get_app,
12
+ _get_app_dashboard_url,
13
+ _get_team,
14
+ )
15
+ from fastapi_cloud_cli.config import Settings
16
+ from fastapi_cloud_cli.utils.api import APIClient
17
+ from fastapi_cloud_cli.utils.apps import resolve_app_id_or_fail
18
+ from fastapi_cloud_cli.utils.auth import Identity
19
+ from fastapi_cloud_cli.utils.cli import get_details_table, get_rich_toolkit
20
+ from fastapi_cloud_cli.utils.execution import JsonOutputOption
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class AppGetOutput(BaseModel):
26
+ app: App
27
+ dashboard_url: Annotated[str | None, Field(exclude=True)] = None
28
+
29
+
30
+ def _render_app_get_output(data: AppGetOutput, toolkit: RichToolkit) -> None:
31
+ app = data.app
32
+
33
+ toolkit.print(f"[bold]{app.name}[/bold]", emoji="📦")
34
+ toolkit.print_line()
35
+ toolkit.print(
36
+ get_details_table(
37
+ [
38
+ ("id", app.id),
39
+ ("slug", app.slug),
40
+ (
41
+ "directory",
42
+ app.directory
43
+ if app.directory is not None
44
+ else Text("-", style="dim"),
45
+ ),
46
+ ("url", app.url if app.url is not None else Text("-", style="dim")),
47
+ (
48
+ "dashboard",
49
+ Text(data.dashboard_url, style=f"link {data.dashboard_url}")
50
+ if data.dashboard_url is not None
51
+ else Text("-", style="dim"),
52
+ ),
53
+ ("team id", app.team_id),
54
+ ]
55
+ )
56
+ )
57
+
58
+
59
+ def get_app(
60
+ app_id: Annotated[
61
+ str | None,
62
+ typer.Argument(
63
+ help="ID of the app to return (defaults to the app linked to the current directory).",
64
+ ),
65
+ ] = None,
66
+ json_output: JsonOutputOption = False,
67
+ ) -> Any:
68
+ """
69
+ Get a FastAPI Cloud app by ID.
70
+ """
71
+ identity = Identity()
72
+
73
+ with get_rich_toolkit(json_output=json_output) as toolkit:
74
+ if not identity.is_logged_in():
75
+ toolkit.fail(
76
+ "not_logged_in",
77
+ "No credentials found.",
78
+ hint="Run `fastapi cloud login` or set FASTAPI_CLOUD_TOKEN.",
79
+ )
80
+
81
+ target_app_id = resolve_app_id_or_fail(
82
+ toolkit,
83
+ app_id=app_id,
84
+ hint="Pass an app ID or run `fastapi cloud apps create --link` first.",
85
+ )
86
+
87
+ with APIClient() as client:
88
+ with toolkit.progress(
89
+ title="Fetching app",
90
+ transient=True,
91
+ ) as progress:
92
+ with client.handle_http_errors(
93
+ progress,
94
+ default_message="Error fetching app. Please try again later.",
95
+ not_found_message="App not found.",
96
+ toolkit=toolkit,
97
+ ):
98
+ app = _get_app(client, target_app_id)
99
+
100
+ dashboard_url = None
101
+ if not json_output:
102
+ with toolkit.progress(
103
+ title="Fetching team",
104
+ transient=True,
105
+ ) as progress:
106
+ with client.handle_http_errors(
107
+ progress,
108
+ default_message="Error fetching team. Please try again later.",
109
+ not_found_message="Team not found.",
110
+ toolkit=toolkit,
111
+ ):
112
+ team = _get_team(client, app.team_id)
113
+
114
+ dashboard_url = _get_app_dashboard_url(
115
+ app,
116
+ team_slug=team.slug,
117
+ settings=Settings.get(),
118
+ )
119
+
120
+ result = AppGetOutput(app=app, dashboard_url=dashboard_url)
121
+
122
+ toolkit.success(result, render_output=_render_app_get_output)