fastapi-cloud-cli 0.7.0__tar.gz → 0.9.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.
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/PKG-INFO +3 -4
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/pyproject.toml +4 -5
- fastapi_cloud_cli-0.9.0/src/fastapi_cloud_cli/__init__.py +1 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/commands/deploy.py +110 -45
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/commands/env.py +13 -10
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/commands/login.py +7 -13
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/commands/whoami.py +8 -2
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/utils/api.py +10 -10
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/utils/apps.py +2 -4
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/utils/auth.py +35 -17
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/utils/cli.py +14 -4
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/utils/sentry.py +4 -2
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/conftest.py +1 -1
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_archive.py +1 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_auth.py +32 -32
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_cli_deploy.py +399 -55
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_cli_whoami.py +7 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_env_delete.py +3 -2
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_env_set.py +3 -2
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/utils.py +4 -3
- fastapi_cloud_cli-0.7.0/src/fastapi_cloud_cli/__init__.py +0 -1
- fastapi_cloud_cli-0.7.0/src/fastapi_cloud_cli/utils/pydantic_compat.py +0 -72
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/LICENSE +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/README.md +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/scripts/format.sh +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/scripts/lint.sh +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/scripts/test-cov-html.sh +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/scripts/test.sh +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/__main__.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/cli.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/commands/__init__.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/commands/logout.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/commands/unlink.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/config.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/logging.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/py.typed +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/utils/__init__.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/utils/config.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/utils/env.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/__init__.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/broken_package/mod/__init__.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/broken_package/mod/app.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/broken_package/utils.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_api/api.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app/api.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app/app.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app_dir_api/app/__init__.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app_dir_api/app/api.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app_dir_app/app/__init__.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app_dir_app/app/api.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app_dir_app/app/app.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app_dir_main/app/__init__.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app_dir_main/app/api.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app_dir_main/app/app.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app_dir_main/app/main.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app_dir_non_default/app/__init__.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app_dir_non_default/app/nondefault.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_main/api.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_main/app.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_main/main.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/non_default/nonstandard.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/package/__init__.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/package/core/__init__.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/package/core/utils.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/package/mod/__init__.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/package/mod/api.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/package/mod/app.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/package/mod/other.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/single_file_api.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/single_file_app.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/single_file_other.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_api_client.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_cli.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_cli_login.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_cli_logout.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_cli_unlink.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_config.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_deploy_utils.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_env_list.py +0 -0
- {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_sentry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fastapi-cloud-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.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
|
|
@@ -19,7 +19,6 @@ Classifier: Framework :: FastAPI
|
|
|
19
19
|
Classifier: Intended Audience :: Developers
|
|
20
20
|
Classifier: License :: OSI Approved :: MIT License
|
|
21
21
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.9
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.10
|
|
25
24
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -30,13 +29,13 @@ Project-URL: Documentation, https://fastapi.tiangolo.com/fastapi-cloud-cli/
|
|
|
30
29
|
Project-URL: Repository, https://github.com/fastapilabs/fastapi-cloud-cli
|
|
31
30
|
Project-URL: Issues, https://github.com/fastapilabs/fastapi-cloud-cli/issues
|
|
32
31
|
Project-URL: Changelog, https://github.com/fastapilabs/fastapi-cloud-cli/blob/main/release-notes.md
|
|
33
|
-
Requires-Python: >=3.
|
|
32
|
+
Requires-Python: >=3.9
|
|
34
33
|
Requires-Dist: typer>=0.12.3
|
|
35
34
|
Requires-Dist: uvicorn[standard]>=0.15.0
|
|
36
35
|
Requires-Dist: rignore>=0.5.1
|
|
37
36
|
Requires-Dist: httpx>=0.27.0
|
|
38
37
|
Requires-Dist: rich-toolkit>=0.14.5
|
|
39
|
-
Requires-Dist: pydantic[email]>=
|
|
38
|
+
Requires-Dist: pydantic[email]>=2.0
|
|
40
39
|
Requires-Dist: sentry-sdk>=2.20.0
|
|
41
40
|
Requires-Dist: fastar>=0.8.0
|
|
42
41
|
Provides-Extra: standard
|
|
@@ -5,7 +5,7 @@ description = "Deploy and manage FastAPI Cloud apps from the command line 🚀"
|
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Patrick Arminio", email = "patrick@fastapilabs.com" },
|
|
7
7
|
]
|
|
8
|
-
requires-python = ">=3.
|
|
8
|
+
requires-python = ">=3.9"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
classifiers = [
|
|
11
11
|
"Intended Audience :: Information Technology",
|
|
@@ -23,7 +23,6 @@ classifiers = [
|
|
|
23
23
|
"Intended Audience :: Developers",
|
|
24
24
|
"License :: OSI Approved :: MIT License",
|
|
25
25
|
"Programming Language :: Python :: 3 :: Only",
|
|
26
|
-
"Programming Language :: Python :: 3.8",
|
|
27
26
|
"Programming Language :: Python :: 3.9",
|
|
28
27
|
"Programming Language :: Python :: 3.10",
|
|
29
28
|
"Programming Language :: Python :: 3.11",
|
|
@@ -36,11 +35,11 @@ dependencies = [
|
|
|
36
35
|
"rignore >= 0.5.1",
|
|
37
36
|
"httpx >= 0.27.0",
|
|
38
37
|
"rich-toolkit >= 0.14.5",
|
|
39
|
-
"pydantic[email] >=
|
|
38
|
+
"pydantic[email] >= 2.0",
|
|
40
39
|
"sentry-sdk >= 2.20.0",
|
|
41
40
|
"fastar >= 0.8.0",
|
|
42
41
|
]
|
|
43
|
-
version = "0.
|
|
42
|
+
version = "0.9.0"
|
|
44
43
|
|
|
45
44
|
[project.license]
|
|
46
45
|
text = "MIT"
|
|
@@ -59,7 +58,7 @@ Changelog = "https://github.com/fastapilabs/fastapi-cloud-cli/blob/main/release-
|
|
|
59
58
|
|
|
60
59
|
[dependency-groups]
|
|
61
60
|
dev = [
|
|
62
|
-
"
|
|
61
|
+
"prek>=0.2.24,<1.0.0",
|
|
63
62
|
"pytest>=4.4.0,<9.0.0",
|
|
64
63
|
"coverage[toml]>=6.2,<8.0",
|
|
65
64
|
"mypy==1.14.1",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.9.0"
|
{fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/commands/deploy.py
RENAMED
|
@@ -7,28 +7,22 @@ from enum import Enum
|
|
|
7
7
|
from itertools import cycle
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from textwrap import dedent
|
|
10
|
-
from typing import
|
|
10
|
+
from typing import Annotated, Any, Optional, Union
|
|
11
11
|
|
|
12
12
|
import fastar
|
|
13
13
|
import rignore
|
|
14
14
|
import typer
|
|
15
15
|
from httpx import Client
|
|
16
|
-
from pydantic import BaseModel, EmailStr, ValidationError
|
|
16
|
+
from pydantic import BaseModel, EmailStr, TypeAdapter, ValidationError
|
|
17
17
|
from rich.text import Text
|
|
18
18
|
from rich_toolkit import RichToolkit
|
|
19
19
|
from rich_toolkit.menu import Option
|
|
20
|
-
from typing_extensions import Annotated
|
|
21
20
|
|
|
22
21
|
from fastapi_cloud_cli.commands.login import login
|
|
23
22
|
from fastapi_cloud_cli.utils.api import APIClient, BuildLogError, TooManyRetriesError
|
|
24
23
|
from fastapi_cloud_cli.utils.apps import AppConfig, get_app_config, write_app_config
|
|
25
|
-
from fastapi_cloud_cli.utils.auth import
|
|
24
|
+
from fastapi_cloud_cli.utils.auth import Identity
|
|
26
25
|
from fastapi_cloud_cli.utils.cli import get_rich_toolkit, handle_http_errors
|
|
27
|
-
from fastapi_cloud_cli.utils.pydantic_compat import (
|
|
28
|
-
TypeAdapter,
|
|
29
|
-
model_dump,
|
|
30
|
-
model_validate,
|
|
31
|
-
)
|
|
32
26
|
|
|
33
27
|
logger = logging.getLogger(__name__)
|
|
34
28
|
|
|
@@ -57,6 +51,7 @@ def _should_exclude_entry(path: Path) -> bool:
|
|
|
57
51
|
"__pycache__",
|
|
58
52
|
".mypy_cache",
|
|
59
53
|
".pytest_cache",
|
|
54
|
+
".git",
|
|
60
55
|
".gitignore",
|
|
61
56
|
".fastapicloudignore",
|
|
62
57
|
]
|
|
@@ -102,14 +97,14 @@ class Team(BaseModel):
|
|
|
102
97
|
name: str
|
|
103
98
|
|
|
104
99
|
|
|
105
|
-
def _get_teams() ->
|
|
100
|
+
def _get_teams() -> list[Team]:
|
|
106
101
|
with APIClient() as client:
|
|
107
102
|
response = client.get("/teams/")
|
|
108
103
|
response.raise_for_status()
|
|
109
104
|
|
|
110
105
|
data = response.json()["data"]
|
|
111
106
|
|
|
112
|
-
return [model_validate(
|
|
107
|
+
return [Team.model_validate(team) for team in data]
|
|
113
108
|
|
|
114
109
|
|
|
115
110
|
class AppResponse(BaseModel):
|
|
@@ -126,7 +121,7 @@ def _create_app(team_id: str, app_name: str) -> AppResponse:
|
|
|
126
121
|
|
|
127
122
|
response.raise_for_status()
|
|
128
123
|
|
|
129
|
-
return model_validate(
|
|
124
|
+
return AppResponse.model_validate(response.json())
|
|
130
125
|
|
|
131
126
|
|
|
132
127
|
class DeploymentStatus(str, Enum):
|
|
@@ -179,12 +174,12 @@ def _create_deployment(app_id: str) -> CreateDeploymentResponse:
|
|
|
179
174
|
response = client.post(f"/apps/{app_id}/deployments/")
|
|
180
175
|
response.raise_for_status()
|
|
181
176
|
|
|
182
|
-
return model_validate(
|
|
177
|
+
return CreateDeploymentResponse.model_validate(response.json())
|
|
183
178
|
|
|
184
179
|
|
|
185
180
|
class RequestUploadResponse(BaseModel):
|
|
186
181
|
url: str
|
|
187
|
-
fields:
|
|
182
|
+
fields: dict[str, str]
|
|
188
183
|
|
|
189
184
|
|
|
190
185
|
def _upload_deployment(deployment_id: str, archive_path: Path) -> None:
|
|
@@ -204,7 +199,7 @@ def _upload_deployment(deployment_id: str, archive_path: Path) -> None:
|
|
|
204
199
|
response = fastapi_client.post(f"/deployments/{deployment_id}/upload")
|
|
205
200
|
response.raise_for_status()
|
|
206
201
|
|
|
207
|
-
upload_data = model_validate(
|
|
202
|
+
upload_data = RequestUploadResponse.model_validate(response.json())
|
|
208
203
|
logger.debug("Received upload URL: %s", upload_data.url)
|
|
209
204
|
|
|
210
205
|
logger.debug("Starting file upload to S3")
|
|
@@ -239,17 +234,17 @@ def _get_app(app_slug: str) -> Optional[AppResponse]:
|
|
|
239
234
|
|
|
240
235
|
data = response.json()
|
|
241
236
|
|
|
242
|
-
return model_validate(
|
|
237
|
+
return AppResponse.model_validate(data)
|
|
243
238
|
|
|
244
239
|
|
|
245
|
-
def _get_apps(team_id: str) ->
|
|
240
|
+
def _get_apps(team_id: str) -> list[AppResponse]:
|
|
246
241
|
with APIClient() as client:
|
|
247
242
|
response = client.get("/apps/", params={"team_id": team_id})
|
|
248
243
|
response.raise_for_status()
|
|
249
244
|
|
|
250
245
|
data = response.json()["data"]
|
|
251
246
|
|
|
252
|
-
return [model_validate(
|
|
247
|
+
return [AppResponse.model_validate(app) for app in data]
|
|
253
248
|
|
|
254
249
|
|
|
255
250
|
WAITING_MESSAGES = [
|
|
@@ -300,6 +295,8 @@ def _configure_app(toolkit: RichToolkit, path_to_deploy: Path) -> AppConfig:
|
|
|
300
295
|
|
|
301
296
|
toolkit.print_line()
|
|
302
297
|
|
|
298
|
+
selected_app: Optional[AppResponse] = None
|
|
299
|
+
|
|
303
300
|
if not create_new_app:
|
|
304
301
|
with toolkit.progress("Fetching apps...") as progress:
|
|
305
302
|
with handle_http_errors(
|
|
@@ -316,18 +313,45 @@ def _configure_app(toolkit: RichToolkit, path_to_deploy: Path) -> AppConfig:
|
|
|
316
313
|
|
|
317
314
|
raise typer.Exit(1)
|
|
318
315
|
|
|
319
|
-
|
|
316
|
+
selected_app = toolkit.ask(
|
|
320
317
|
"Select the app you want to deploy to:",
|
|
321
318
|
options=[Option({"name": app.slug, "value": app}) for app in apps],
|
|
322
319
|
)
|
|
323
|
-
|
|
324
|
-
|
|
320
|
+
|
|
321
|
+
app_name = (
|
|
322
|
+
selected_app.slug
|
|
323
|
+
if selected_app
|
|
324
|
+
else toolkit.input(
|
|
325
325
|
title="What's your app name?",
|
|
326
326
|
default=_get_app_name(path_to_deploy),
|
|
327
327
|
)
|
|
328
|
+
)
|
|
328
329
|
|
|
329
|
-
|
|
330
|
+
toolkit.print_line()
|
|
331
|
+
|
|
332
|
+
toolkit.print("Deployment configuration:", tag="summary")
|
|
333
|
+
toolkit.print_line()
|
|
334
|
+
toolkit.print(f"Team: [bold]{team.name}[/bold]")
|
|
335
|
+
toolkit.print(f"App name: [bold]{app_name}[/bold]")
|
|
336
|
+
toolkit.print_line()
|
|
337
|
+
|
|
338
|
+
choice = toolkit.ask(
|
|
339
|
+
"Does everything look right?",
|
|
340
|
+
tag="confirm",
|
|
341
|
+
options=[
|
|
342
|
+
Option({"name": "Yes, start the deployment!", "value": "deploy"}),
|
|
343
|
+
Option({"name": "No, let me start over", "value": "cancel"}),
|
|
344
|
+
],
|
|
345
|
+
)
|
|
346
|
+
toolkit.print_line()
|
|
330
347
|
|
|
348
|
+
if choice == "cancel":
|
|
349
|
+
toolkit.print("Deployment cancelled.")
|
|
350
|
+
raise typer.Exit(0)
|
|
351
|
+
|
|
352
|
+
if selected_app: # pragma: no cover
|
|
353
|
+
app = selected_app
|
|
354
|
+
else:
|
|
331
355
|
with toolkit.progress(title="Creating app...") as progress:
|
|
332
356
|
with handle_http_errors(progress):
|
|
333
357
|
app = _create_app(team.id, app_name)
|
|
@@ -363,9 +387,12 @@ def _wait_for_deployment(
|
|
|
363
387
|
|
|
364
388
|
last_message_changed_at = time.monotonic()
|
|
365
389
|
|
|
366
|
-
with
|
|
367
|
-
|
|
368
|
-
|
|
390
|
+
with (
|
|
391
|
+
toolkit.progress(
|
|
392
|
+
next(messages), inline_logs=True, lines_to_show=20
|
|
393
|
+
) as progress,
|
|
394
|
+
APIClient() as client,
|
|
395
|
+
):
|
|
369
396
|
try:
|
|
370
397
|
for log in client.stream_build_logs(deployment.id):
|
|
371
398
|
time_elapsed = time.monotonic() - started_at
|
|
@@ -376,13 +403,13 @@ def _wait_for_deployment(
|
|
|
376
403
|
if log.type == "complete":
|
|
377
404
|
progress.log("")
|
|
378
405
|
progress.log(
|
|
379
|
-
f"
|
|
406
|
+
f"You can also check the app logs at [link={deployment.dashboard_url}]{deployment.dashboard_url}[/link]"
|
|
380
407
|
)
|
|
381
408
|
|
|
382
409
|
progress.log("")
|
|
383
410
|
|
|
384
411
|
progress.log(
|
|
385
|
-
f"
|
|
412
|
+
f"🐔 Ready the chicken! Your app is ready at [link={deployment.url}]{deployment.url}[/link]"
|
|
386
413
|
)
|
|
387
414
|
|
|
388
415
|
break
|
|
@@ -432,7 +459,7 @@ def _send_waitlist_form(
|
|
|
432
459
|
with toolkit.progress("Sending your request...") as progress:
|
|
433
460
|
with APIClient() as client:
|
|
434
461
|
with handle_http_errors(progress):
|
|
435
|
-
response = client.post("/users/waiting-list", json=model_dump(
|
|
462
|
+
response = client.post("/users/waiting-list", json=result.model_dump())
|
|
436
463
|
|
|
437
464
|
response.raise_for_status()
|
|
438
465
|
|
|
@@ -457,7 +484,7 @@ def _waitlist_form(toolkit: RichToolkit) -> None:
|
|
|
457
484
|
|
|
458
485
|
toolkit.print_line()
|
|
459
486
|
|
|
460
|
-
result = model_validate(
|
|
487
|
+
result = SignupToWaitingList.model_validate({"email": email})
|
|
461
488
|
|
|
462
489
|
if toolkit.confirm(
|
|
463
490
|
"Do you want to get access faster by giving us more information?",
|
|
@@ -481,8 +508,7 @@ def _waitlist_form(toolkit: RichToolkit) -> None:
|
|
|
481
508
|
result = form.run() # type: ignore
|
|
482
509
|
|
|
483
510
|
try:
|
|
484
|
-
result = model_validate(
|
|
485
|
-
SignupToWaitingList,
|
|
511
|
+
result = SignupToWaitingList.model_validate(
|
|
486
512
|
{
|
|
487
513
|
"email": email,
|
|
488
514
|
**result, # type: ignore
|
|
@@ -531,15 +557,27 @@ def deploy(
|
|
|
531
557
|
skip_wait: Annotated[
|
|
532
558
|
bool, typer.Option("--no-wait", help="Skip waiting for deployment status")
|
|
533
559
|
] = False,
|
|
560
|
+
provided_app_id: Annotated[
|
|
561
|
+
Union[str, None],
|
|
562
|
+
typer.Option(
|
|
563
|
+
"--app-id",
|
|
564
|
+
help="Application ID to deploy to",
|
|
565
|
+
envvar="FASTAPI_CLOUD_APP_ID",
|
|
566
|
+
),
|
|
567
|
+
] = None,
|
|
534
568
|
) -> Any:
|
|
535
569
|
"""
|
|
536
570
|
Deploy a [bold]FastAPI[/bold] app to FastAPI Cloud. 🚀
|
|
537
571
|
"""
|
|
538
572
|
logger.debug("Deploy command started")
|
|
539
|
-
logger.debug(
|
|
573
|
+
logger.debug(
|
|
574
|
+
"Deploy path: %s, skip_wait: %s, app_id: %s", path, skip_wait, provided_app_id
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
identity = Identity()
|
|
540
578
|
|
|
541
579
|
with get_rich_toolkit() as toolkit:
|
|
542
|
-
if not is_logged_in():
|
|
580
|
+
if not identity.is_logged_in():
|
|
543
581
|
logger.debug("User not logged in, prompting for login or waitlist")
|
|
544
582
|
|
|
545
583
|
toolkit.print_title("Welcome to FastAPI Cloud!", tag="FastAPI")
|
|
@@ -576,19 +614,43 @@ def deploy(
|
|
|
576
614
|
|
|
577
615
|
app_config = get_app_config(path_to_deploy)
|
|
578
616
|
|
|
579
|
-
if
|
|
617
|
+
if app_config and provided_app_id and app_config.app_id != provided_app_id:
|
|
618
|
+
toolkit.print(
|
|
619
|
+
f"[error]Error: Provided app ID ({provided_app_id}) does not match the local "
|
|
620
|
+
f"config ({app_config.app_id}).[/]"
|
|
621
|
+
)
|
|
622
|
+
toolkit.print_line()
|
|
623
|
+
toolkit.print(
|
|
624
|
+
"Run [bold]fastapi cloud unlink[/] to remove the local config, "
|
|
625
|
+
"or remove --app-id / unset FASTAPI_CLOUD_APP_ID to use the configured app.",
|
|
626
|
+
tag="tip",
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
raise typer.Exit(1) from None
|
|
630
|
+
|
|
631
|
+
if provided_app_id:
|
|
632
|
+
target_app_id = provided_app_id
|
|
633
|
+
elif app_config:
|
|
634
|
+
target_app_id = app_config.app_id
|
|
635
|
+
else:
|
|
580
636
|
logger.debug("No app config found, configuring new app")
|
|
637
|
+
|
|
581
638
|
app_config = _configure_app(toolkit, path_to_deploy=path_to_deploy)
|
|
582
639
|
toolkit.print_line()
|
|
640
|
+
|
|
641
|
+
target_app_id = app_config.app_id
|
|
642
|
+
|
|
643
|
+
if provided_app_id:
|
|
644
|
+
toolkit.print(f"Deploying to app [blue]{target_app_id}[/blue]...")
|
|
583
645
|
else:
|
|
584
|
-
logger.debug("Existing app config found, proceeding with deployment")
|
|
585
646
|
toolkit.print("Deploying app...")
|
|
586
|
-
|
|
647
|
+
|
|
648
|
+
toolkit.print_line()
|
|
587
649
|
|
|
588
650
|
with toolkit.progress("Checking app...", transient=True) as progress:
|
|
589
651
|
with handle_http_errors(progress):
|
|
590
|
-
logger.debug("Checking app with ID: %s",
|
|
591
|
-
app = _get_app(
|
|
652
|
+
logger.debug("Checking app with ID: %s", target_app_id)
|
|
653
|
+
app = _get_app(target_app_id)
|
|
592
654
|
|
|
593
655
|
if not app:
|
|
594
656
|
logger.debug("App not found in API")
|
|
@@ -598,10 +660,12 @@ def deploy(
|
|
|
598
660
|
|
|
599
661
|
if not app:
|
|
600
662
|
toolkit.print_line()
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
663
|
+
|
|
664
|
+
if not provided_app_id:
|
|
665
|
+
toolkit.print(
|
|
666
|
+
"If you deleted this app, you can run [bold]fastapi cloud unlink[/] to unlink the local configuration.",
|
|
667
|
+
tag="tip",
|
|
668
|
+
)
|
|
605
669
|
raise typer.Exit(1)
|
|
606
670
|
|
|
607
671
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
@@ -609,9 +673,10 @@ def deploy(
|
|
|
609
673
|
archive_path = Path(temp_dir) / "archive.tar"
|
|
610
674
|
archive(path or Path.cwd(), archive_path)
|
|
611
675
|
|
|
612
|
-
with
|
|
613
|
-
title="Creating deployment"
|
|
614
|
-
|
|
676
|
+
with (
|
|
677
|
+
toolkit.progress(title="Creating deployment") as progress,
|
|
678
|
+
handle_http_errors(progress),
|
|
679
|
+
):
|
|
615
680
|
logger.debug("Creating deployment for app: %s", app.id)
|
|
616
681
|
deployment = _create_deployment(app.id)
|
|
617
682
|
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Annotated, Any, Union
|
|
4
4
|
|
|
5
5
|
import typer
|
|
6
6
|
from pydantic import BaseModel
|
|
7
|
-
from typing_extensions import Annotated
|
|
8
7
|
|
|
9
8
|
from fastapi_cloud_cli.utils.api import APIClient
|
|
10
9
|
from fastapi_cloud_cli.utils.apps import get_app_config
|
|
11
|
-
from fastapi_cloud_cli.utils.auth import
|
|
10
|
+
from fastapi_cloud_cli.utils.auth import Identity
|
|
12
11
|
from fastapi_cloud_cli.utils.cli import get_rich_toolkit, handle_http_errors
|
|
13
12
|
from fastapi_cloud_cli.utils.env import validate_environment_variable_name
|
|
14
|
-
from fastapi_cloud_cli.utils.pydantic_compat import model_validate
|
|
15
13
|
|
|
16
14
|
logger = logging.getLogger(__name__)
|
|
17
15
|
|
|
@@ -22,7 +20,7 @@ class EnvironmentVariable(BaseModel):
|
|
|
22
20
|
|
|
23
21
|
|
|
24
22
|
class EnvironmentVariableResponse(BaseModel):
|
|
25
|
-
data:
|
|
23
|
+
data: list[EnvironmentVariable]
|
|
26
24
|
|
|
27
25
|
|
|
28
26
|
def _get_environment_variables(app_id: str) -> EnvironmentVariableResponse:
|
|
@@ -30,7 +28,7 @@ def _get_environment_variables(app_id: str) -> EnvironmentVariableResponse:
|
|
|
30
28
|
response = client.get(f"/apps/{app_id}/environment-variables/")
|
|
31
29
|
response.raise_for_status()
|
|
32
30
|
|
|
33
|
-
return model_validate(
|
|
31
|
+
return EnvironmentVariableResponse.model_validate(response.json())
|
|
34
32
|
|
|
35
33
|
|
|
36
34
|
def _delete_environment_variable(app_id: str, name: str) -> bool:
|
|
@@ -72,8 +70,10 @@ def list(
|
|
|
72
70
|
List the environment variables for the app.
|
|
73
71
|
"""
|
|
74
72
|
|
|
73
|
+
identity = Identity()
|
|
74
|
+
|
|
75
75
|
with get_rich_toolkit(minimal=True) as toolkit:
|
|
76
|
-
if not is_logged_in():
|
|
76
|
+
if not identity.is_logged_in():
|
|
77
77
|
toolkit.print(
|
|
78
78
|
"No credentials found. Use [blue]`fastapi login`[/] to login.",
|
|
79
79
|
tag="auth",
|
|
@@ -125,9 +125,10 @@ def delete(
|
|
|
125
125
|
Delete an environment variable from the app.
|
|
126
126
|
"""
|
|
127
127
|
|
|
128
|
+
identity = Identity()
|
|
129
|
+
|
|
128
130
|
with get_rich_toolkit(minimal=True) as toolkit:
|
|
129
|
-
|
|
130
|
-
if not is_logged_in():
|
|
131
|
+
if not identity.is_logged_in():
|
|
131
132
|
toolkit.print(
|
|
132
133
|
"No credentials found. Use [blue]`fastapi login`[/] to login.",
|
|
133
134
|
tag="auth",
|
|
@@ -210,8 +211,10 @@ def set(
|
|
|
210
211
|
Set an environment variable for the app.
|
|
211
212
|
"""
|
|
212
213
|
|
|
214
|
+
identity = Identity()
|
|
215
|
+
|
|
213
216
|
with get_rich_toolkit(minimal=True) as toolkit:
|
|
214
|
-
if not is_logged_in():
|
|
217
|
+
if not identity.is_logged_in():
|
|
215
218
|
toolkit.print(
|
|
216
219
|
"No credentials found. Use [blue]`fastapi login`[/] to login.",
|
|
217
220
|
tag="auth",
|
|
@@ -8,15 +8,8 @@ from pydantic import BaseModel
|
|
|
8
8
|
|
|
9
9
|
from fastapi_cloud_cli.config import Settings
|
|
10
10
|
from fastapi_cloud_cli.utils.api import APIClient
|
|
11
|
-
from fastapi_cloud_cli.utils.auth import
|
|
12
|
-
AuthConfig,
|
|
13
|
-
get_auth_token,
|
|
14
|
-
is_logged_in,
|
|
15
|
-
is_token_expired,
|
|
16
|
-
write_auth_config,
|
|
17
|
-
)
|
|
11
|
+
from fastapi_cloud_cli.utils.auth import AuthConfig, Identity, write_auth_config
|
|
18
12
|
from fastapi_cloud_cli.utils.cli import get_rich_toolkit, handle_http_errors
|
|
19
|
-
from fastapi_cloud_cli.utils.pydantic_compat import model_validate_json
|
|
20
13
|
|
|
21
14
|
logger = logging.getLogger(__name__)
|
|
22
15
|
|
|
@@ -44,7 +37,7 @@ def _start_device_authorization(
|
|
|
44
37
|
|
|
45
38
|
response.raise_for_status()
|
|
46
39
|
|
|
47
|
-
return model_validate_json(
|
|
40
|
+
return AuthorizationData.model_validate_json(response.text)
|
|
48
41
|
|
|
49
42
|
|
|
50
43
|
def _fetch_access_token(client: httpx.Client, device_code: str, interval: int) -> str:
|
|
@@ -74,7 +67,7 @@ def _fetch_access_token(client: httpx.Client, device_code: str, interval: int) -
|
|
|
74
67
|
|
|
75
68
|
time.sleep(interval)
|
|
76
69
|
|
|
77
|
-
response_data = model_validate_json(
|
|
70
|
+
response_data = TokenResponse.model_validate_json(response.text)
|
|
78
71
|
|
|
79
72
|
return response_data.access_token
|
|
80
73
|
|
|
@@ -83,13 +76,14 @@ def login() -> Any:
|
|
|
83
76
|
"""
|
|
84
77
|
Login to FastAPI Cloud. 🚀
|
|
85
78
|
"""
|
|
86
|
-
|
|
87
|
-
|
|
79
|
+
identity = Identity()
|
|
80
|
+
|
|
81
|
+
if identity.is_expired():
|
|
88
82
|
with get_rich_toolkit(minimal=True) as toolkit:
|
|
89
83
|
toolkit.print("Your session has expired. Logging in again...")
|
|
90
84
|
toolkit.print_line()
|
|
91
85
|
|
|
92
|
-
if is_logged_in():
|
|
86
|
+
if identity.is_logged_in():
|
|
93
87
|
with get_rich_toolkit(minimal=True) as toolkit:
|
|
94
88
|
toolkit.print("You are already logged in.")
|
|
95
89
|
toolkit.print(
|
{fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/commands/whoami.py
RENAMED
|
@@ -5,14 +5,20 @@ from rich import print
|
|
|
5
5
|
from rich_toolkit.progress import Progress
|
|
6
6
|
|
|
7
7
|
from fastapi_cloud_cli.utils.api import APIClient
|
|
8
|
-
from fastapi_cloud_cli.utils.auth import
|
|
8
|
+
from fastapi_cloud_cli.utils.auth import Identity
|
|
9
9
|
from fastapi_cloud_cli.utils.cli import handle_http_errors
|
|
10
10
|
|
|
11
11
|
logger = logging.getLogger(__name__)
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def whoami() -> Any:
|
|
15
|
-
|
|
15
|
+
identity = Identity()
|
|
16
|
+
|
|
17
|
+
if identity.auth_mode == "token":
|
|
18
|
+
print("⚡ [bold]Using API token from environment variable[/bold]")
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
if not identity.is_logged_in():
|
|
16
22
|
print("No credentials found. Use [blue]`fastapi login`[/] to login.")
|
|
17
23
|
return
|
|
18
24
|
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
3
|
import time
|
|
4
|
+
from collections.abc import Generator
|
|
4
5
|
from contextlib import contextmanager
|
|
5
6
|
from datetime import timedelta
|
|
6
7
|
from functools import wraps
|
|
7
8
|
from typing import (
|
|
9
|
+
Annotated,
|
|
8
10
|
Callable,
|
|
9
|
-
Generator,
|
|
10
11
|
Literal,
|
|
11
12
|
Optional,
|
|
12
13
|
TypeVar,
|
|
@@ -14,13 +15,13 @@ from typing import (
|
|
|
14
15
|
)
|
|
15
16
|
|
|
16
17
|
import httpx
|
|
17
|
-
from pydantic import BaseModel, Field, ValidationError
|
|
18
|
-
from typing_extensions import
|
|
18
|
+
from pydantic import BaseModel, Field, TypeAdapter, ValidationError
|
|
19
|
+
from typing_extensions import ParamSpec
|
|
19
20
|
|
|
20
21
|
from fastapi_cloud_cli import __version__
|
|
21
22
|
from fastapi_cloud_cli.config import Settings
|
|
22
|
-
|
|
23
|
-
from
|
|
23
|
+
|
|
24
|
+
from .auth import Identity
|
|
24
25
|
|
|
25
26
|
logger = logging.getLogger(__name__)
|
|
26
27
|
|
|
@@ -48,8 +49,8 @@ class BuildLogLineMessage(BaseModel):
|
|
|
48
49
|
|
|
49
50
|
|
|
50
51
|
BuildLogLine = Union[BuildLogLineMessage, BuildLogLineGeneric]
|
|
51
|
-
BuildLogAdapter
|
|
52
|
-
Annotated[BuildLogLine, Field(discriminator="type")]
|
|
52
|
+
BuildLogAdapter: TypeAdapter[BuildLogLine] = TypeAdapter(
|
|
53
|
+
Annotated[BuildLogLine, Field(discriminator="type")]
|
|
53
54
|
)
|
|
54
55
|
|
|
55
56
|
|
|
@@ -132,14 +133,13 @@ def attempts(
|
|
|
132
133
|
class APIClient(httpx.Client):
|
|
133
134
|
def __init__(self) -> None:
|
|
134
135
|
settings = Settings.get()
|
|
135
|
-
|
|
136
|
-
token = get_auth_token()
|
|
136
|
+
identity = Identity()
|
|
137
137
|
|
|
138
138
|
super().__init__(
|
|
139
139
|
base_url=settings.base_api_url,
|
|
140
140
|
timeout=httpx.Timeout(20),
|
|
141
141
|
headers={
|
|
142
|
-
"Authorization": f"Bearer {token}",
|
|
142
|
+
"Authorization": f"Bearer {identity.token}",
|
|
143
143
|
"User-Agent": f"fastapi-cloud-cli/{__version__}",
|
|
144
144
|
},
|
|
145
145
|
)
|
|
@@ -4,8 +4,6 @@ from typing import Optional
|
|
|
4
4
|
|
|
5
5
|
from pydantic import BaseModel
|
|
6
6
|
|
|
7
|
-
from fastapi_cloud_cli.utils.pydantic_compat import model_dump_json, model_validate_json
|
|
8
|
-
|
|
9
7
|
logger = logging.getLogger("fastapi_cli")
|
|
10
8
|
|
|
11
9
|
|
|
@@ -23,7 +21,7 @@ def get_app_config(path_to_deploy: Path) -> Optional[AppConfig]:
|
|
|
23
21
|
return None
|
|
24
22
|
|
|
25
23
|
logger.debug("App config loaded successfully")
|
|
26
|
-
return model_validate_json(
|
|
24
|
+
return AppConfig.model_validate_json(config_path.read_text(encoding="utf-8"))
|
|
27
25
|
|
|
28
26
|
|
|
29
27
|
README = """
|
|
@@ -52,7 +50,7 @@ def write_app_config(path_to_deploy: Path, app_config: AppConfig) -> None:
|
|
|
52
50
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
53
51
|
|
|
54
52
|
config_path.write_text(
|
|
55
|
-
model_dump_json(
|
|
53
|
+
app_config.model_dump_json(),
|
|
56
54
|
encoding="utf-8",
|
|
57
55
|
)
|
|
58
56
|
readme_path.write_text(README, encoding="utf-8")
|