fastapi-cloud-cli 0.11.0__tar.gz → 0.13.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.11.0 → fastapi_cloud_cli-0.13.0}/PKG-INFO +8 -7
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/pyproject.toml +11 -9
- fastapi_cloud_cli-0.13.0/src/fastapi_cloud_cli/__init__.py +1 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/commands/deploy.py +103 -27
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/commands/env.py +8 -8
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/commands/link.py +3 -2
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/commands/login.py +1 -5
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/commands/logs.py +15 -15
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/commands/whoami.py +1 -1
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/logging.py +1 -4
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/utils/api.py +10 -10
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/utils/apps.py +1 -2
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/utils/auth.py +4 -4
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/utils/cli.py +46 -23
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/utils/config.py +6 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/conftest.py +31 -11
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/test_api_client.py +2 -22
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/test_auth.py +5 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/test_cli_deploy.py +389 -50
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/test_cli_link.py +6 -8
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/test_cli_login.py +19 -54
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/test_cli_whoami.py +16 -6
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/test_config.py +20 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/test_deploy_utils.py +63 -1
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/test_env_delete.py +5 -7
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/test_env_list.py +4 -6
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/test_env_set.py +5 -7
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/test_logs.py +27 -14
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/utils.py +1 -0
- fastapi_cloud_cli-0.11.0/src/fastapi_cloud_cli/__init__.py +0 -1
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/LICENSE +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/README.md +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/scripts/format.sh +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/scripts/lint.sh +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/scripts/test-cov-html.sh +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/scripts/test.sh +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/__main__.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/cli.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/commands/__init__.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/commands/logout.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/commands/unlink.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/config.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/py.typed +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/utils/__init__.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/utils/env.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/utils/sentry.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/__init__.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/broken_package/mod/__init__.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/broken_package/mod/app.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/broken_package/utils.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/default_files/default_api/api.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/default_files/default_app/api.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/default_files/default_app/app.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/default_files/default_app_dir_api/app/__init__.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/default_files/default_app_dir_api/app/api.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/default_files/default_app_dir_app/app/__init__.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/default_files/default_app_dir_app/app/api.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/default_files/default_app_dir_app/app/app.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/default_files/default_app_dir_main/app/__init__.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/default_files/default_app_dir_main/app/api.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/default_files/default_app_dir_main/app/app.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/default_files/default_app_dir_main/app/main.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/default_files/default_app_dir_non_default/app/__init__.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/default_files/default_app_dir_non_default/app/nondefault.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/default_files/default_main/api.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/default_files/default_main/app.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/default_files/default_main/main.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/default_files/non_default/nonstandard.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/package/__init__.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/package/core/__init__.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/package/core/utils.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/package/mod/__init__.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/package/mod/api.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/package/mod/app.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/package/mod/other.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/single_file_api.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/single_file_app.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/assets/single_file_other.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/test_archive.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/test_cli.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/test_cli_logout.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/tests/test_cli_unlink.py +0 -0
- {fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.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.13.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.9
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.10
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.11
|
|
25
24
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -29,13 +28,15 @@ Project-URL: Documentation, https://fastapi.tiangolo.com/fastapi-cloud-cli/
|
|
|
29
28
|
Project-URL: Repository, https://github.com/fastapilabs/fastapi-cloud-cli
|
|
30
29
|
Project-URL: Issues, https://github.com/fastapilabs/fastapi-cloud-cli/issues
|
|
31
30
|
Project-URL: Changelog, https://github.com/fastapilabs/fastapi-cloud-cli/blob/main/release-notes.md
|
|
32
|
-
Requires-Python: >=3.
|
|
33
|
-
Requires-Dist: typer>=0.
|
|
34
|
-
Requires-Dist: uvicorn[standard]>=0.
|
|
31
|
+
Requires-Python: >=3.10
|
|
32
|
+
Requires-Dist: typer>=0.16.0
|
|
33
|
+
Requires-Dist: uvicorn[standard]>=0.17.6
|
|
35
34
|
Requires-Dist: rignore>=0.5.1
|
|
36
35
|
Requires-Dist: httpx>=0.27.0
|
|
37
|
-
Requires-Dist: rich-toolkit>=0.
|
|
38
|
-
Requires-Dist: pydantic[email]>=2.
|
|
36
|
+
Requires-Dist: rich-toolkit>=0.19.4
|
|
37
|
+
Requires-Dist: pydantic[email]>=2.7.4; python_version < "3.13"
|
|
38
|
+
Requires-Dist: pydantic[email]>=2.8.0; python_version == "3.13"
|
|
39
|
+
Requires-Dist: pydantic[email]>=2.12.0; python_version >= "3.14"
|
|
39
40
|
Requires-Dist: sentry-sdk>=2.20.0
|
|
40
41
|
Requires-Dist: fastar>=0.8.0
|
|
41
42
|
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.10"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
classifiers = [
|
|
11
11
|
"Intended Audience :: Information Technology",
|
|
@@ -23,23 +23,24 @@ 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.9",
|
|
27
26
|
"Programming Language :: Python :: 3.10",
|
|
28
27
|
"Programming Language :: Python :: 3.11",
|
|
29
28
|
"Programming Language :: Python :: 3.12",
|
|
30
29
|
"Programming Language :: Python :: 3.13",
|
|
31
30
|
]
|
|
32
31
|
dependencies = [
|
|
33
|
-
"typer >= 0.
|
|
34
|
-
"uvicorn[standard] >= 0.
|
|
32
|
+
"typer >= 0.16.0",
|
|
33
|
+
"uvicorn[standard] >= 0.17.6",
|
|
35
34
|
"rignore >= 0.5.1",
|
|
36
35
|
"httpx >= 0.27.0",
|
|
37
|
-
"rich-toolkit >= 0.
|
|
38
|
-
"pydantic[email] >= 2.
|
|
36
|
+
"rich-toolkit >= 0.19.4",
|
|
37
|
+
"pydantic[email] >= 2.7.4; python_version < '3.13'",
|
|
38
|
+
"pydantic[email] >= 2.8.0; python_version == '3.13'",
|
|
39
|
+
"pydantic[email] >= 2.12.0; python_version >= '3.14'",
|
|
39
40
|
"sentry-sdk >= 2.20.0",
|
|
40
41
|
"fastar >= 0.8.0",
|
|
41
42
|
]
|
|
42
|
-
version = "0.
|
|
43
|
+
version = "0.13.0"
|
|
43
44
|
|
|
44
45
|
[project.license]
|
|
45
46
|
text = "MIT"
|
|
@@ -59,8 +60,8 @@ Changelog = "https://github.com/fastapilabs/fastapi-cloud-cli/blob/main/release-
|
|
|
59
60
|
[dependency-groups]
|
|
60
61
|
dev = [
|
|
61
62
|
"prek>=0.2.24,<1.0.0",
|
|
62
|
-
"pytest>=
|
|
63
|
-
"coverage[toml]>=
|
|
63
|
+
"pytest>=7.0.0,<9.0.0",
|
|
64
|
+
"coverage[toml]>=7.2,<8.0",
|
|
64
65
|
"mypy==1.14.1",
|
|
65
66
|
"ruff==0.13.0",
|
|
66
67
|
"respx==0.22.0",
|
|
@@ -101,6 +102,7 @@ source = [
|
|
|
101
102
|
"src",
|
|
102
103
|
"tests",
|
|
103
104
|
]
|
|
105
|
+
relative_files = true
|
|
104
106
|
context = "${CONTEXT}"
|
|
105
107
|
dynamic_context = "test_function"
|
|
106
108
|
omit = [
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.13.0"
|
{fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/commands/deploy.py
RENAMED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import contextlib
|
|
2
2
|
import logging
|
|
3
|
+
import re
|
|
3
4
|
import subprocess
|
|
4
5
|
import tempfile
|
|
5
6
|
import time
|
|
6
7
|
from enum import Enum
|
|
7
8
|
from itertools import cycle
|
|
8
|
-
from pathlib import Path
|
|
9
|
+
from pathlib import Path, PurePosixPath
|
|
9
10
|
from textwrap import dedent
|
|
10
|
-
from typing import Annotated, Any
|
|
11
|
+
from typing import Annotated, Any
|
|
11
12
|
|
|
12
13
|
import fastar
|
|
13
14
|
import rignore
|
|
14
15
|
import typer
|
|
15
16
|
from httpx import Client
|
|
16
|
-
from pydantic import BaseModel, EmailStr, TypeAdapter, ValidationError
|
|
17
|
+
from pydantic import AfterValidator, BaseModel, EmailStr, TypeAdapter, ValidationError
|
|
17
18
|
from rich.text import Text
|
|
18
19
|
from rich_toolkit import RichToolkit
|
|
19
20
|
from rich_toolkit.menu import Option
|
|
@@ -27,6 +28,39 @@ from fastapi_cloud_cli.utils.cli import get_rich_toolkit, handle_http_errors
|
|
|
27
28
|
logger = logging.getLogger(__name__)
|
|
28
29
|
|
|
29
30
|
|
|
31
|
+
def validate_app_directory(v: str | None) -> str | None:
|
|
32
|
+
if v is None:
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
v = v.strip()
|
|
36
|
+
|
|
37
|
+
if not v:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
if v.startswith("~"):
|
|
41
|
+
raise ValueError("cannot start with '~'")
|
|
42
|
+
|
|
43
|
+
path = PurePosixPath(v)
|
|
44
|
+
|
|
45
|
+
if path.is_absolute():
|
|
46
|
+
raise ValueError("must be a relative path, not absolute")
|
|
47
|
+
|
|
48
|
+
if ".." in path.parts:
|
|
49
|
+
raise ValueError("cannot contain '..' path segments")
|
|
50
|
+
|
|
51
|
+
normalized = path.as_posix()
|
|
52
|
+
|
|
53
|
+
if not re.fullmatch(r"[A-Za-z0-9._/ -]+", normalized):
|
|
54
|
+
raise ValueError(
|
|
55
|
+
"contains invalid characters (allowed: letters, numbers, space, / . _ -)"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return normalized
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
AppDirectory = Annotated[str | None, AfterValidator(validate_app_directory)]
|
|
62
|
+
|
|
63
|
+
|
|
30
64
|
def _cancel_upload(deployment_id: str) -> None:
|
|
31
65
|
logger.debug("Cancelling upload for deployment: %s", deployment_id)
|
|
32
66
|
|
|
@@ -113,13 +147,26 @@ def _get_teams() -> list[Team]:
|
|
|
113
147
|
class AppResponse(BaseModel):
|
|
114
148
|
id: str
|
|
115
149
|
slug: str
|
|
150
|
+
directory: str | None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _update_app(app_id: str, directory: str | None) -> AppResponse:
|
|
154
|
+
with APIClient() as client:
|
|
155
|
+
response = client.patch(
|
|
156
|
+
f"/apps/{app_id}",
|
|
157
|
+
json={"directory": directory},
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
response.raise_for_status()
|
|
161
|
+
|
|
162
|
+
return AppResponse.model_validate(response.json())
|
|
116
163
|
|
|
117
164
|
|
|
118
|
-
def _create_app(team_id: str, app_name: str) -> AppResponse:
|
|
165
|
+
def _create_app(team_id: str, app_name: str, directory: str | None) -> AppResponse:
|
|
119
166
|
with APIClient() as client:
|
|
120
167
|
response = client.post(
|
|
121
168
|
"/apps/",
|
|
122
|
-
json={"name": app_name, "team_id": team_id},
|
|
169
|
+
json={"name": app_name, "team_id": team_id, "directory": directory},
|
|
123
170
|
)
|
|
124
171
|
|
|
125
172
|
response.raise_for_status()
|
|
@@ -226,7 +273,7 @@ def _upload_deployment(deployment_id: str, archive_path: Path) -> None:
|
|
|
226
273
|
logger.debug("Upload notification sent successfully")
|
|
227
274
|
|
|
228
275
|
|
|
229
|
-
def _get_app(app_slug: str) ->
|
|
276
|
+
def _get_app(app_slug: str) -> AppResponse | None:
|
|
230
277
|
with APIClient() as client:
|
|
231
278
|
response = client.get(f"/apps/{app_slug}")
|
|
232
279
|
|
|
@@ -278,7 +325,7 @@ def _configure_app(toolkit: RichToolkit, path_to_deploy: Path) -> AppConfig:
|
|
|
278
325
|
|
|
279
326
|
with toolkit.progress("Fetching teams...") as progress:
|
|
280
327
|
with handle_http_errors(
|
|
281
|
-
progress,
|
|
328
|
+
progress, default_message="Error fetching teams. Please try again later."
|
|
282
329
|
):
|
|
283
330
|
teams = _get_teams()
|
|
284
331
|
|
|
@@ -298,12 +345,12 @@ def _configure_app(toolkit: RichToolkit, path_to_deploy: Path) -> AppConfig:
|
|
|
298
345
|
|
|
299
346
|
toolkit.print_line()
|
|
300
347
|
|
|
301
|
-
selected_app:
|
|
348
|
+
selected_app: AppResponse | None = None
|
|
302
349
|
|
|
303
350
|
if not create_new_app:
|
|
304
351
|
with toolkit.progress("Fetching apps...") as progress:
|
|
305
352
|
with handle_http_errors(
|
|
306
|
-
progress,
|
|
353
|
+
progress, default_message="Error fetching apps. Please try again later."
|
|
307
354
|
):
|
|
308
355
|
apps = _get_apps(team.id)
|
|
309
356
|
|
|
@@ -332,10 +379,26 @@ def _configure_app(toolkit: RichToolkit, path_to_deploy: Path) -> AppConfig:
|
|
|
332
379
|
|
|
333
380
|
toolkit.print_line()
|
|
334
381
|
|
|
382
|
+
initial_directory = selected_app.directory if selected_app else ""
|
|
383
|
+
|
|
384
|
+
directory_input = toolkit.input(
|
|
385
|
+
title="Path to the directory containing your app (e.g. src, backend):",
|
|
386
|
+
tag="dir",
|
|
387
|
+
value=initial_directory or "",
|
|
388
|
+
placeholder="[italic]Leave empty if it's the current directory[/italic]",
|
|
389
|
+
validator=TypeAdapter(AppDirectory),
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
directory: str | None = directory_input if directory_input else None
|
|
393
|
+
|
|
394
|
+
toolkit.print_line()
|
|
395
|
+
|
|
335
396
|
toolkit.print("Deployment configuration:", tag="summary")
|
|
336
397
|
toolkit.print_line()
|
|
337
398
|
toolkit.print(f"Team: [bold]{team.name}[/bold]")
|
|
338
399
|
toolkit.print(f"App name: [bold]{app_name}[/bold]")
|
|
400
|
+
toolkit.print(f"Directory: [bold]{directory or '.'}[/bold]")
|
|
401
|
+
|
|
339
402
|
toolkit.print_line()
|
|
340
403
|
|
|
341
404
|
choice = toolkit.ask(
|
|
@@ -352,12 +415,21 @@ def _configure_app(toolkit: RichToolkit, path_to_deploy: Path) -> AppConfig:
|
|
|
352
415
|
toolkit.print("Deployment cancelled.")
|
|
353
416
|
raise typer.Exit(0)
|
|
354
417
|
|
|
355
|
-
if selected_app:
|
|
356
|
-
|
|
418
|
+
if selected_app:
|
|
419
|
+
if directory != selected_app.directory:
|
|
420
|
+
with (
|
|
421
|
+
toolkit.progress(title="Updating app directory...") as progress,
|
|
422
|
+
handle_http_errors(progress),
|
|
423
|
+
):
|
|
424
|
+
app = _update_app(selected_app.id, directory=directory)
|
|
425
|
+
|
|
426
|
+
progress.log(f"App directory updated to '{directory or '.'}'")
|
|
427
|
+
else:
|
|
428
|
+
app = selected_app
|
|
357
429
|
else:
|
|
358
430
|
with toolkit.progress(title="Creating app...") as progress:
|
|
359
431
|
with handle_http_errors(progress):
|
|
360
|
-
app = _create_app(team.id, app_name)
|
|
432
|
+
app = _create_app(team.id, app_name, directory=directory)
|
|
361
433
|
|
|
362
434
|
progress.log(f"App created successfully! App slug: {app.slug}")
|
|
363
435
|
|
|
@@ -446,13 +518,13 @@ def _wait_for_deployment(
|
|
|
446
518
|
|
|
447
519
|
class SignupToWaitingList(BaseModel):
|
|
448
520
|
email: EmailStr
|
|
449
|
-
name:
|
|
450
|
-
organization:
|
|
451
|
-
role:
|
|
452
|
-
team_size:
|
|
453
|
-
location:
|
|
454
|
-
use_case:
|
|
455
|
-
secret_code:
|
|
521
|
+
name: str | None = None
|
|
522
|
+
organization: str | None = None
|
|
523
|
+
role: str | None = None
|
|
524
|
+
team_size: str | None = None
|
|
525
|
+
location: str | None = None
|
|
526
|
+
use_case: str | None = None
|
|
527
|
+
secret_code: str | None = None
|
|
456
528
|
|
|
457
529
|
|
|
458
530
|
def _send_waitlist_form(
|
|
@@ -552,7 +624,7 @@ def _waitlist_form(toolkit: RichToolkit) -> None:
|
|
|
552
624
|
|
|
553
625
|
def deploy(
|
|
554
626
|
path: Annotated[
|
|
555
|
-
|
|
627
|
+
Path | None,
|
|
556
628
|
typer.Argument(
|
|
557
629
|
help="A path to the folder containing the app you want to deploy"
|
|
558
630
|
),
|
|
@@ -561,7 +633,7 @@ def deploy(
|
|
|
561
633
|
bool, typer.Option("--no-wait", help="Skip waiting for deployment status")
|
|
562
634
|
] = False,
|
|
563
635
|
provided_app_id: Annotated[
|
|
564
|
-
|
|
636
|
+
str | None,
|
|
565
637
|
typer.Option(
|
|
566
638
|
"--app-id",
|
|
567
639
|
help="Application ID to deploy to",
|
|
@@ -586,10 +658,16 @@ def deploy(
|
|
|
586
658
|
toolkit.print_title("Welcome to FastAPI Cloud!", tag="FastAPI")
|
|
587
659
|
toolkit.print_line()
|
|
588
660
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
661
|
+
if identity.token and identity.is_expired():
|
|
662
|
+
toolkit.print(
|
|
663
|
+
"Your session has expired. Please log in again.",
|
|
664
|
+
tag="info",
|
|
665
|
+
)
|
|
666
|
+
else:
|
|
667
|
+
toolkit.print(
|
|
668
|
+
"You need to be logged in to deploy to FastAPI Cloud.",
|
|
669
|
+
tag="info",
|
|
670
|
+
)
|
|
593
671
|
toolkit.print_line()
|
|
594
672
|
|
|
595
673
|
choice = toolkit.ask(
|
|
@@ -601,8 +679,6 @@ def deploy(
|
|
|
601
679
|
],
|
|
602
680
|
)
|
|
603
681
|
|
|
604
|
-
toolkit.print_line()
|
|
605
|
-
|
|
606
682
|
if choice == "login":
|
|
607
683
|
login()
|
|
608
684
|
else:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import Annotated, Any
|
|
3
|
+
from typing import Annotated, Any
|
|
4
4
|
|
|
5
5
|
import typer
|
|
6
6
|
from pydantic import BaseModel
|
|
@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
|
|
|
16
16
|
|
|
17
17
|
class EnvironmentVariable(BaseModel):
|
|
18
18
|
name: str
|
|
19
|
-
value:
|
|
19
|
+
value: str | None = None
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class EnvironmentVariableResponse(BaseModel):
|
|
@@ -60,7 +60,7 @@ env_app = typer.Typer()
|
|
|
60
60
|
@env_app.command()
|
|
61
61
|
def list(
|
|
62
62
|
path: Annotated[
|
|
63
|
-
|
|
63
|
+
Path | None,
|
|
64
64
|
typer.Argument(
|
|
65
65
|
help="A path to the folder containing the app you want to deploy"
|
|
66
66
|
),
|
|
@@ -110,12 +110,12 @@ def list(
|
|
|
110
110
|
|
|
111
111
|
@env_app.command()
|
|
112
112
|
def delete(
|
|
113
|
-
name:
|
|
113
|
+
name: str | None = typer.Argument(
|
|
114
114
|
None,
|
|
115
115
|
help="The name of the environment variable to delete",
|
|
116
116
|
),
|
|
117
117
|
path: Annotated[
|
|
118
|
-
|
|
118
|
+
Path | None,
|
|
119
119
|
typer.Argument(
|
|
120
120
|
help="A path to the folder containing the app you want to deploy"
|
|
121
121
|
),
|
|
@@ -192,16 +192,16 @@ def delete(
|
|
|
192
192
|
|
|
193
193
|
@env_app.command()
|
|
194
194
|
def set(
|
|
195
|
-
name:
|
|
195
|
+
name: str | None = typer.Argument(
|
|
196
196
|
None,
|
|
197
197
|
help="The name of the environment variable to set",
|
|
198
198
|
),
|
|
199
|
-
value:
|
|
199
|
+
value: str | None = typer.Argument(
|
|
200
200
|
None,
|
|
201
201
|
help="The value of the environment variable to set",
|
|
202
202
|
),
|
|
203
203
|
path: Annotated[
|
|
204
|
-
|
|
204
|
+
Path | None,
|
|
205
205
|
typer.Argument(
|
|
206
206
|
help="A path to the folder containing the app you want to deploy"
|
|
207
207
|
),
|
{fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/commands/link.py
RENAMED
|
@@ -49,7 +49,8 @@ def link() -> Any:
|
|
|
49
49
|
|
|
50
50
|
with toolkit.progress("Fetching teams...") as progress:
|
|
51
51
|
with handle_http_errors(
|
|
52
|
-
progress,
|
|
52
|
+
progress,
|
|
53
|
+
default_message="Error fetching teams. Please try again later.",
|
|
53
54
|
):
|
|
54
55
|
with APIClient() as client:
|
|
55
56
|
response = client.get("/teams/")
|
|
@@ -77,7 +78,7 @@ def link() -> Any:
|
|
|
77
78
|
|
|
78
79
|
with toolkit.progress("Fetching apps...") as progress:
|
|
79
80
|
with handle_http_errors(
|
|
80
|
-
progress,
|
|
81
|
+
progress, default_message="Error fetching apps. Please try again later."
|
|
81
82
|
):
|
|
82
83
|
with APIClient() as client:
|
|
83
84
|
response = client.get("/apps/", params={"team_id": team["id"]})
|
{fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/commands/login.py
RENAMED
|
@@ -78,17 +78,13 @@ def login() -> Any:
|
|
|
78
78
|
"""
|
|
79
79
|
identity = Identity()
|
|
80
80
|
|
|
81
|
-
if identity.is_expired():
|
|
82
|
-
with get_rich_toolkit(minimal=True) as toolkit:
|
|
83
|
-
toolkit.print("Your session has expired. Logging in again...")
|
|
84
|
-
toolkit.print_line()
|
|
85
|
-
|
|
86
81
|
if identity.is_logged_in():
|
|
87
82
|
with get_rich_toolkit(minimal=True) as toolkit:
|
|
88
83
|
toolkit.print("You are already logged in.")
|
|
89
84
|
toolkit.print(
|
|
90
85
|
"Run [bold]fastapi cloud logout[/bold] first if you want to switch accounts."
|
|
91
86
|
)
|
|
87
|
+
|
|
92
88
|
return
|
|
93
89
|
|
|
94
90
|
with get_rich_toolkit() as toolkit, APIClient() as client:
|
{fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/commands/logs.py
RENAMED
|
@@ -2,9 +2,10 @@ import logging
|
|
|
2
2
|
import re
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Annotated
|
|
5
|
+
from typing import Annotated
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
|
+
from httpx import HTTPError
|
|
8
9
|
from rich.markup import escape
|
|
9
10
|
from rich_toolkit import RichToolkit
|
|
10
11
|
|
|
@@ -16,7 +17,7 @@ from fastapi_cloud_cli.utils.api import (
|
|
|
16
17
|
)
|
|
17
18
|
from fastapi_cloud_cli.utils.apps import AppConfig, get_app_config
|
|
18
19
|
from fastapi_cloud_cli.utils.auth import Identity
|
|
19
|
-
from fastapi_cloud_cli.utils.cli import get_rich_toolkit
|
|
20
|
+
from fastapi_cloud_cli.utils.cli import get_rich_toolkit, handle_http_error
|
|
20
21
|
|
|
21
22
|
logger = logging.getLogger(__name__)
|
|
22
23
|
|
|
@@ -85,21 +86,20 @@ def _process_log_stream(
|
|
|
85
86
|
return
|
|
86
87
|
except KeyboardInterrupt: # pragma: no cover
|
|
87
88
|
toolkit.print_line()
|
|
89
|
+
|
|
88
90
|
return
|
|
89
91
|
except StreamLogError as e:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
toolkit.print(
|
|
97
|
-
"App not found. Make sure to use the correct account.",
|
|
98
|
-
)
|
|
92
|
+
if e.status_code == 404:
|
|
93
|
+
message = "App not found. Make sure to use the correct account."
|
|
94
|
+
|
|
95
|
+
elif isinstance(e.__cause__, HTTPError):
|
|
96
|
+
message = handle_http_error(e.__cause__)
|
|
97
|
+
|
|
99
98
|
else:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
message = f"[red]Error:[/] {escape(str(e))}"
|
|
100
|
+
|
|
101
|
+
toolkit.print(message)
|
|
102
|
+
|
|
103
103
|
raise typer.Exit(1) from None
|
|
104
104
|
except (TooManyRetriesError, TimeoutError):
|
|
105
105
|
toolkit.print(
|
|
@@ -110,7 +110,7 @@ def _process_log_stream(
|
|
|
110
110
|
|
|
111
111
|
def logs(
|
|
112
112
|
path: Annotated[
|
|
113
|
-
|
|
113
|
+
Path | None,
|
|
114
114
|
typer.Argument(
|
|
115
115
|
help="Path to the folder containing the app (defaults to current directory)"
|
|
116
116
|
),
|
{fastapi_cloud_cli-0.11.0 → fastapi_cloud_cli-0.13.0}/src/fastapi_cloud_cli/commands/whoami.py
RENAMED
|
@@ -24,7 +24,7 @@ def whoami() -> Any:
|
|
|
24
24
|
|
|
25
25
|
with APIClient() as client:
|
|
26
26
|
with Progress(title="⚡ Fetching profile", transient=True) as progress:
|
|
27
|
-
with handle_http_errors(progress,
|
|
27
|
+
with handle_http_errors(progress, default_message=""):
|
|
28
28
|
response = client.get("/users/me")
|
|
29
29
|
response.raise_for_status()
|
|
30
30
|
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
|
-
from typing import Union
|
|
4
3
|
|
|
5
4
|
from rich.console import Console
|
|
6
5
|
from rich.logging import RichHandler
|
|
7
6
|
|
|
8
7
|
|
|
9
|
-
def setup_logging(
|
|
10
|
-
terminal_width: Union[int, None] = None, level: Union[int, None] = None
|
|
11
|
-
) -> None:
|
|
8
|
+
def setup_logging(terminal_width: int | None = None, level: int | None = None) -> None:
|
|
12
9
|
if level is None:
|
|
13
10
|
level = (
|
|
14
11
|
logging.DEBUG if os.getenv("FASTAPI_CLOUD_DEBUG") == "1" else logging.INFO
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
3
|
import time
|
|
4
|
-
from collections.abc import Generator
|
|
4
|
+
from collections.abc import Callable, Generator
|
|
5
5
|
from contextlib import contextmanager
|
|
6
6
|
from datetime import timedelta
|
|
7
7
|
from functools import wraps
|
|
8
8
|
from typing import (
|
|
9
9
|
Annotated,
|
|
10
|
-
Callable,
|
|
11
10
|
Literal,
|
|
12
|
-
Optional,
|
|
13
11
|
TypeVar,
|
|
14
|
-
Union,
|
|
15
12
|
)
|
|
16
13
|
|
|
17
14
|
import httpx
|
|
@@ -32,7 +29,9 @@ STREAM_LOGS_TIMEOUT = timedelta(minutes=5)
|
|
|
32
29
|
class StreamLogError(Exception):
|
|
33
30
|
"""Raised when there's an error streaming logs (build or app logs)."""
|
|
34
31
|
|
|
35
|
-
|
|
32
|
+
def __init__(self, message: str, *, status_code: int | None = None) -> None:
|
|
33
|
+
super().__init__(message)
|
|
34
|
+
self.status_code = status_code
|
|
36
35
|
|
|
37
36
|
|
|
38
37
|
class TooManyRetriesError(Exception):
|
|
@@ -47,16 +46,16 @@ class AppLogEntry(BaseModel):
|
|
|
47
46
|
|
|
48
47
|
class BuildLogLineGeneric(BaseModel):
|
|
49
48
|
type: Literal["complete", "failed", "timeout", "heartbeat"]
|
|
50
|
-
id:
|
|
49
|
+
id: str | None = None
|
|
51
50
|
|
|
52
51
|
|
|
53
52
|
class BuildLogLineMessage(BaseModel):
|
|
54
53
|
type: Literal["message"] = "message"
|
|
55
54
|
message: str
|
|
56
|
-
id:
|
|
55
|
+
id: str | None = None
|
|
57
56
|
|
|
58
57
|
|
|
59
|
-
BuildLogLine =
|
|
58
|
+
BuildLogLine = BuildLogLineMessage | BuildLogLineGeneric
|
|
60
59
|
BuildLogAdapter: TypeAdapter[BuildLogLine] = TypeAdapter(
|
|
61
60
|
Annotated[BuildLogLine, Field(discriminator="type")]
|
|
62
61
|
)
|
|
@@ -100,7 +99,8 @@ def attempt(attempt_number: int) -> Generator[None, None, None]:
|
|
|
100
99
|
except Exception:
|
|
101
100
|
error_detail = "(response body unavailable)"
|
|
102
101
|
raise StreamLogError(
|
|
103
|
-
f"HTTP {error.response.status_code}: {error_detail}"
|
|
102
|
+
f"HTTP {error.response.status_code}: {error_detail}",
|
|
103
|
+
status_code=error.response.status_code,
|
|
104
104
|
) from error
|
|
105
105
|
|
|
106
106
|
|
|
@@ -194,7 +194,7 @@ class APIClient(httpx.Client):
|
|
|
194
194
|
|
|
195
195
|
time.sleep(0.5)
|
|
196
196
|
|
|
197
|
-
def _parse_log_line(self, line: str) ->
|
|
197
|
+
def _parse_log_line(self, line: str) -> BuildLogLine | None:
|
|
198
198
|
try:
|
|
199
199
|
return BuildLogAdapter.validate_json(line)
|
|
200
200
|
except (ValidationError, json.JSONDecodeError) as e:
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import Optional
|
|
4
3
|
|
|
5
4
|
from pydantic import BaseModel
|
|
6
5
|
|
|
@@ -12,7 +11,7 @@ class AppConfig(BaseModel):
|
|
|
12
11
|
team_id: str
|
|
13
12
|
|
|
14
13
|
|
|
15
|
-
def get_app_config(path_to_deploy: Path) ->
|
|
14
|
+
def get_app_config(path_to_deploy: Path) -> AppConfig | None:
|
|
16
15
|
config_path = path_to_deploy / ".fastapicloud/cloud.json"
|
|
17
16
|
logger.debug("Looking for app config at: %s", config_path)
|
|
18
17
|
|
|
@@ -4,7 +4,7 @@ import json
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import time
|
|
7
|
-
from typing import Literal
|
|
7
|
+
from typing import Literal
|
|
8
8
|
|
|
9
9
|
from pydantic import BaseModel
|
|
10
10
|
|
|
@@ -36,7 +36,7 @@ def delete_auth_config() -> None:
|
|
|
36
36
|
logger.debug("Auth config file doesn't exist, nothing to delete")
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def read_auth_config() ->
|
|
39
|
+
def read_auth_config() -> AuthConfig | None:
|
|
40
40
|
auth_path = get_auth_path()
|
|
41
41
|
logger.debug("Reading auth config from: %s", auth_path)
|
|
42
42
|
|
|
@@ -48,7 +48,7 @@ def read_auth_config() -> Optional[AuthConfig]:
|
|
|
48
48
|
return AuthConfig.model_validate_json(auth_path.read_text(encoding="utf-8"))
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
def _get_auth_token() ->
|
|
51
|
+
def _get_auth_token() -> str | None:
|
|
52
52
|
logger.debug("Getting auth token")
|
|
53
53
|
auth_data = read_auth_config()
|
|
54
54
|
|
|
@@ -120,7 +120,7 @@ class Identity:
|
|
|
120
120
|
self.token = env_token
|
|
121
121
|
self.auth_mode = "token"
|
|
122
122
|
|
|
123
|
-
def _get_token_from_env(self) ->
|
|
123
|
+
def _get_token_from_env(self) -> str | None:
|
|
124
124
|
return os.environ.get("FASTAPI_CLOUD_TOKEN")
|
|
125
125
|
|
|
126
126
|
def is_expired(self) -> bool:
|