fastapi-cloud-cli 0.5.2__tar.gz → 0.7.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.5.2 → fastapi_cloud_cli-0.7.0}/PKG-INFO +2 -3
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/pyproject.toml +13 -4
- fastapi_cloud_cli-0.7.0/src/fastapi_cloud_cli/__init__.py +1 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/commands/deploy.py +34 -14
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/utils/api.py +1 -2
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/test_archive.py +60 -28
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/test_cli_deploy.py +81 -31
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/test_cli_login.py +3 -1
- fastapi_cloud_cli-0.5.2/requirements-tests.txt +0 -8
- fastapi_cloud_cli-0.5.2/requirements.txt +0 -5
- fastapi_cloud_cli-0.5.2/src/fastapi_cloud_cli/__init__.py +0 -1
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/LICENSE +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/README.md +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/scripts/format.sh +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/scripts/lint.sh +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/scripts/test-cov-html.sh +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/scripts/test.sh +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/__main__.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/cli.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/commands/__init__.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/commands/env.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/commands/login.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/commands/logout.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/commands/unlink.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/commands/whoami.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/config.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/logging.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/py.typed +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/utils/__init__.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/utils/apps.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/utils/auth.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/utils/cli.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/utils/config.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/utils/env.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/utils/pydantic_compat.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/utils/sentry.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/__init__.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/broken_package/mod/__init__.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/broken_package/mod/app.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/broken_package/utils.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/default_files/default_api/api.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/default_files/default_app/api.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/default_files/default_app/app.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/default_files/default_app_dir_api/app/__init__.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/default_files/default_app_dir_api/app/api.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/default_files/default_app_dir_app/app/__init__.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/default_files/default_app_dir_app/app/api.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/default_files/default_app_dir_app/app/app.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/default_files/default_app_dir_main/app/__init__.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/default_files/default_app_dir_main/app/api.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/default_files/default_app_dir_main/app/app.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/default_files/default_app_dir_main/app/main.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/default_files/default_app_dir_non_default/app/__init__.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/default_files/default_app_dir_non_default/app/nondefault.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/default_files/default_main/api.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/default_files/default_main/app.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/default_files/default_main/main.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/default_files/non_default/nonstandard.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/package/__init__.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/package/core/__init__.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/package/core/utils.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/package/mod/__init__.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/package/mod/api.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/package/mod/app.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/package/mod/other.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/single_file_api.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/single_file_app.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/single_file_other.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/conftest.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/test_api_client.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/test_auth.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/test_cli.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/test_cli_logout.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/test_cli_unlink.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/test_cli_whoami.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/test_config.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/test_deploy_utils.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/test_env_delete.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/test_env_list.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/test_env_set.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/test_sentry.py +0 -0
- {fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.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.
|
|
3
|
+
Version: 0.7.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
|
|
@@ -25,7 +25,6 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
25
25
|
Classifier: Programming Language :: Python :: 3.11
|
|
26
26
|
Classifier: Programming Language :: Python :: 3.12
|
|
27
27
|
Classifier: Programming Language :: Python :: 3.13
|
|
28
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
29
28
|
Project-URL: Homepage, https://github.com/fastapilabs/fastapi-cloud-cli
|
|
30
29
|
Project-URL: Documentation, https://fastapi.tiangolo.com/fastapi-cloud-cli/
|
|
31
30
|
Project-URL: Repository, https://github.com/fastapilabs/fastapi-cloud-cli
|
|
@@ -39,7 +38,7 @@ Requires-Dist: httpx>=0.27.0
|
|
|
39
38
|
Requires-Dist: rich-toolkit>=0.14.5
|
|
40
39
|
Requires-Dist: pydantic[email]>=1.6.1
|
|
41
40
|
Requires-Dist: sentry-sdk>=2.20.0
|
|
42
|
-
Requires-Dist: fastar>=0.
|
|
41
|
+
Requires-Dist: fastar>=0.8.0
|
|
43
42
|
Provides-Extra: standard
|
|
44
43
|
Requires-Dist: uvicorn[standard]>=0.15.0; extra == "standard"
|
|
45
44
|
Description-Content-Type: text/markdown
|
|
@@ -29,7 +29,6 @@ classifiers = [
|
|
|
29
29
|
"Programming Language :: Python :: 3.11",
|
|
30
30
|
"Programming Language :: Python :: 3.12",
|
|
31
31
|
"Programming Language :: Python :: 3.13",
|
|
32
|
-
"License :: OSI Approved :: MIT License",
|
|
33
32
|
]
|
|
34
33
|
dependencies = [
|
|
35
34
|
"typer >= 0.12.3",
|
|
@@ -39,9 +38,9 @@ dependencies = [
|
|
|
39
38
|
"rich-toolkit >= 0.14.5",
|
|
40
39
|
"pydantic[email] >= 1.6.1",
|
|
41
40
|
"sentry-sdk >= 2.20.0",
|
|
42
|
-
"fastar >= 0.
|
|
41
|
+
"fastar >= 0.8.0",
|
|
43
42
|
]
|
|
44
|
-
version = "0.
|
|
43
|
+
version = "0.7.0"
|
|
45
44
|
|
|
46
45
|
[project.license]
|
|
47
46
|
text = "MIT"
|
|
@@ -58,6 +57,17 @@ Repository = "https://github.com/fastapilabs/fastapi-cloud-cli"
|
|
|
58
57
|
Issues = "https://github.com/fastapilabs/fastapi-cloud-cli/issues"
|
|
59
58
|
Changelog = "https://github.com/fastapilabs/fastapi-cloud-cli/blob/main/release-notes.md"
|
|
60
59
|
|
|
60
|
+
[dependency-groups]
|
|
61
|
+
dev = [
|
|
62
|
+
"pre-commit>=2.17.0,<5.0.0",
|
|
63
|
+
"pytest>=4.4.0,<9.0.0",
|
|
64
|
+
"coverage[toml]>=6.2,<8.0",
|
|
65
|
+
"mypy==1.14.1",
|
|
66
|
+
"ruff==0.13.0",
|
|
67
|
+
"respx==0.22.0",
|
|
68
|
+
"time-machine==2.15.0",
|
|
69
|
+
]
|
|
70
|
+
|
|
61
71
|
[build-system]
|
|
62
72
|
requires = [
|
|
63
73
|
"pdm-backend",
|
|
@@ -74,7 +84,6 @@ path = "src/fastapi_cloud_cli/__init__.py"
|
|
|
74
84
|
[tool.pdm.build]
|
|
75
85
|
source-includes = [
|
|
76
86
|
"tests/",
|
|
77
|
-
"requirements*.txt",
|
|
78
87
|
"scripts/",
|
|
79
88
|
]
|
|
80
89
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.7.0"
|
{fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/commands/deploy.py
RENAMED
|
@@ -6,6 +6,7 @@ import time
|
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from itertools import cycle
|
|
8
8
|
from pathlib import Path
|
|
9
|
+
from textwrap import dedent
|
|
9
10
|
from typing import Any, Dict, List, Optional, Union
|
|
10
11
|
|
|
11
12
|
import fastar
|
|
@@ -32,6 +33,19 @@ from fastapi_cloud_cli.utils.pydantic_compat import (
|
|
|
32
33
|
logger = logging.getLogger(__name__)
|
|
33
34
|
|
|
34
35
|
|
|
36
|
+
def _cancel_upload(deployment_id: str) -> None:
|
|
37
|
+
logger.debug("Cancelling upload for deployment: %s", deployment_id)
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
with APIClient() as client:
|
|
41
|
+
response = client.post(f"/deployments/{deployment_id}/upload-cancelled")
|
|
42
|
+
response.raise_for_status()
|
|
43
|
+
|
|
44
|
+
logger.debug("Upload cancellation notification sent successfully")
|
|
45
|
+
except Exception as e:
|
|
46
|
+
logger.debug("Failed to notify server about upload cancellation: %s", e)
|
|
47
|
+
|
|
48
|
+
|
|
35
49
|
def _get_app_name(path: Path) -> str:
|
|
36
50
|
# TODO: use pyproject.toml to get the app name
|
|
37
51
|
return path.name
|
|
@@ -68,7 +82,7 @@ def archive(path: Path, tar_path: Path) -> Path:
|
|
|
68
82
|
logger.debug("Archive will be created at: %s", tar_path)
|
|
69
83
|
|
|
70
84
|
file_count = 0
|
|
71
|
-
with fastar.open(tar_path, "w") as tar:
|
|
85
|
+
with fastar.open(tar_path, "w:zst") as tar:
|
|
72
86
|
for filename in files:
|
|
73
87
|
if filename.is_dir():
|
|
74
88
|
continue
|
|
@@ -260,8 +274,7 @@ LONG_WAIT_MESSAGES = [
|
|
|
260
274
|
|
|
261
275
|
|
|
262
276
|
def _configure_app(toolkit: RichToolkit, path_to_deploy: Path) -> AppConfig:
|
|
263
|
-
|
|
264
|
-
raise typer.Exit(0)
|
|
277
|
+
toolkit.print(f"Setting up and deploying [blue]{path_to_deploy}[/blue]", tag="path")
|
|
265
278
|
|
|
266
279
|
toolkit.print_line()
|
|
267
280
|
|
|
@@ -389,12 +402,15 @@ def _wait_for_deployment(
|
|
|
389
402
|
|
|
390
403
|
last_message_changed_at = time.monotonic()
|
|
391
404
|
|
|
392
|
-
except (BuildLogError, TooManyRetriesError) as e:
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
405
|
+
except (BuildLogError, TooManyRetriesError, TimeoutError) as e:
|
|
406
|
+
progress.set_error(
|
|
407
|
+
dedent(f"""
|
|
408
|
+
[error]Build log streaming failed: {e}[/]
|
|
409
|
+
|
|
410
|
+
Unable to stream build logs. Check the dashboard for status: [link={deployment.dashboard_url}]{deployment.dashboard_url}[/link]
|
|
411
|
+
""").strip()
|
|
397
412
|
)
|
|
413
|
+
|
|
398
414
|
raise typer.Exit(1) from e
|
|
399
415
|
|
|
400
416
|
|
|
@@ -599,15 +615,19 @@ def deploy(
|
|
|
599
615
|
logger.debug("Creating deployment for app: %s", app.id)
|
|
600
616
|
deployment = _create_deployment(app.id)
|
|
601
617
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
618
|
+
try:
|
|
619
|
+
progress.log(
|
|
620
|
+
f"Deployment created successfully! Deployment slug: {deployment.slug}"
|
|
621
|
+
)
|
|
605
622
|
|
|
606
|
-
|
|
623
|
+
progress.log("Uploading deployment...")
|
|
607
624
|
|
|
608
|
-
|
|
625
|
+
_upload_deployment(deployment.id, archive_path)
|
|
609
626
|
|
|
610
|
-
|
|
627
|
+
progress.log("Deployment uploaded successfully!")
|
|
628
|
+
except KeyboardInterrupt:
|
|
629
|
+
_cancel_upload(deployment.id)
|
|
630
|
+
raise
|
|
611
631
|
|
|
612
632
|
toolkit.print_line()
|
|
613
633
|
|
|
@@ -114,8 +114,7 @@ def attempts(
|
|
|
114
114
|
for attempt_number in range(total_attempts):
|
|
115
115
|
if time.monotonic() - start > timeout.total_seconds():
|
|
116
116
|
raise TimeoutError(
|
|
117
|
-
"Build log streaming timed out after
|
|
118
|
-
timeout.total_seconds(),
|
|
117
|
+
f"Build log streaming timed out after {timeout.total_seconds():.0f}s"
|
|
119
118
|
)
|
|
120
119
|
|
|
121
120
|
with attempt(attempt_number):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import tarfile
|
|
2
1
|
from pathlib import Path
|
|
3
2
|
|
|
3
|
+
import fastar
|
|
4
4
|
import pytest
|
|
5
5
|
|
|
6
6
|
from fastapi_cloud_cli.commands.deploy import archive
|
|
@@ -13,6 +13,13 @@ def src_path(tmp_path: Path) -> Path:
|
|
|
13
13
|
return path
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def dst_path(tmp_path: Path) -> Path:
|
|
18
|
+
path = tmp_path / "destination"
|
|
19
|
+
path.mkdir()
|
|
20
|
+
return path
|
|
21
|
+
|
|
22
|
+
|
|
16
23
|
@pytest.fixture
|
|
17
24
|
def tar_path(tmp_path: Path) -> Path:
|
|
18
25
|
return tmp_path / "archive.tar"
|
|
@@ -29,7 +36,7 @@ def test_archive_creates_tar_file(src_path: Path, tar_path: Path) -> None:
|
|
|
29
36
|
|
|
30
37
|
|
|
31
38
|
def test_archive_excludes_venv_and_similar_folders(
|
|
32
|
-
src_path: Path, tar_path: Path
|
|
39
|
+
src_path: Path, tar_path: Path, dst_path: Path
|
|
33
40
|
) -> None:
|
|
34
41
|
"""Should exclude .venv directory from archive."""
|
|
35
42
|
# the only files we want to include
|
|
@@ -53,24 +60,38 @@ def test_archive_excludes_venv_and_similar_folders(
|
|
|
53
60
|
|
|
54
61
|
archive(src_path, tar_path)
|
|
55
62
|
|
|
56
|
-
with
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
with fastar.open(tar_path, "r") as tar:
|
|
64
|
+
tar.unpack(dst_path)
|
|
65
|
+
|
|
66
|
+
assert set(dst_path.glob("**/*")) == {
|
|
67
|
+
dst_path / "main.py",
|
|
68
|
+
dst_path / "static",
|
|
69
|
+
dst_path / "static" / "index.html",
|
|
70
|
+
}
|
|
59
71
|
|
|
60
72
|
|
|
61
|
-
def test_archive_preserves_relative_paths(
|
|
73
|
+
def test_archive_preserves_relative_paths(
|
|
74
|
+
src_path: Path, tar_path: Path, dst_path: Path
|
|
75
|
+
) -> None:
|
|
62
76
|
(src_path / "src").mkdir()
|
|
63
77
|
(src_path / "src" / "app").mkdir()
|
|
64
78
|
(src_path / "src" / "app" / "main.py").write_text("print('hello')")
|
|
65
79
|
|
|
66
80
|
archive(src_path, tar_path)
|
|
67
81
|
|
|
68
|
-
with
|
|
69
|
-
|
|
70
|
-
assert names == ["src/app/main.py"]
|
|
82
|
+
with fastar.open(tar_path, "r") as tar:
|
|
83
|
+
tar.unpack(dst_path)
|
|
71
84
|
|
|
85
|
+
assert set(dst_path.glob("**/*")) == {
|
|
86
|
+
dst_path / "src",
|
|
87
|
+
dst_path / "src" / "app",
|
|
88
|
+
dst_path / "src" / "app" / "main.py",
|
|
89
|
+
}
|
|
72
90
|
|
|
73
|
-
|
|
91
|
+
|
|
92
|
+
def test_archive_respects_fastapicloudignore(
|
|
93
|
+
src_path: Path, tar_path: Path, dst_path: Path
|
|
94
|
+
) -> None:
|
|
74
95
|
"""Should exclude files specified in .fastapicloudignore."""
|
|
75
96
|
(src_path / "main.py").write_text("print('hello')")
|
|
76
97
|
(src_path / "config.py").write_text("CONFIG = 'value'")
|
|
@@ -82,16 +103,17 @@ def test_archive_respects_fastapicloudignore(src_path: Path, tar_path: Path) ->
|
|
|
82
103
|
|
|
83
104
|
archive(src_path, tar_path)
|
|
84
105
|
|
|
85
|
-
with
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
106
|
+
with fastar.open(tar_path, "r") as tar:
|
|
107
|
+
tar.unpack(dst_path)
|
|
108
|
+
|
|
109
|
+
assert set(dst_path.glob("**/*")) == {
|
|
110
|
+
dst_path / "main.py",
|
|
111
|
+
dst_path / "config.py",
|
|
112
|
+
}
|
|
91
113
|
|
|
92
114
|
|
|
93
115
|
def test_archive_respects_fastapicloudignore_unignore(
|
|
94
|
-
src_path: Path, tar_path: Path
|
|
116
|
+
src_path: Path, tar_path: Path, dst_path: Path
|
|
95
117
|
) -> None:
|
|
96
118
|
"""Test we can use .fastapicloudignore to unignore files inside .gitignore"""
|
|
97
119
|
(src_path / "main.py").write_text("print('hello')")
|
|
@@ -109,12 +131,20 @@ def test_archive_respects_fastapicloudignore_unignore(
|
|
|
109
131
|
|
|
110
132
|
archive(src_path, tar_path)
|
|
111
133
|
|
|
112
|
-
with
|
|
113
|
-
|
|
114
|
-
assert set(names) == {"main.py", "static/build/style.css"}
|
|
134
|
+
with fastar.open(tar_path, "r") as tar:
|
|
135
|
+
tar.unpack(dst_path)
|
|
115
136
|
|
|
137
|
+
assert set(dst_path.glob("**/*")) == {
|
|
138
|
+
dst_path / "main.py",
|
|
139
|
+
dst_path / "static",
|
|
140
|
+
dst_path / "static" / "build",
|
|
141
|
+
dst_path / "static" / "build" / "style.css",
|
|
142
|
+
}
|
|
116
143
|
|
|
117
|
-
|
|
144
|
+
|
|
145
|
+
def test_archive_includes_hidden_files(
|
|
146
|
+
src_path: Path, tar_path: Path, dst_path: Path
|
|
147
|
+
) -> None:
|
|
118
148
|
"""Should include hidden files in the archive by default."""
|
|
119
149
|
(src_path / "main.py").write_text("print('hello')")
|
|
120
150
|
(src_path / ".env").write_text("SECRET_KEY=xyz")
|
|
@@ -123,10 +153,12 @@ def test_archive_includes_hidden_files(src_path: Path, tar_path: Path) -> None:
|
|
|
123
153
|
|
|
124
154
|
archive(src_path, tar_path)
|
|
125
155
|
|
|
126
|
-
with
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
156
|
+
with fastar.open(tar_path, "r") as tar:
|
|
157
|
+
tar.unpack(dst_path)
|
|
158
|
+
|
|
159
|
+
assert set(dst_path.glob("**/*")) == {
|
|
160
|
+
dst_path / "main.py",
|
|
161
|
+
dst_path / ".env",
|
|
162
|
+
dst_path / ".config",
|
|
163
|
+
dst_path / ".config" / "settings.json",
|
|
164
|
+
}
|
|
@@ -15,6 +15,7 @@ from typer.testing import CliRunner
|
|
|
15
15
|
|
|
16
16
|
from fastapi_cloud_cli.cli import app
|
|
17
17
|
from fastapi_cloud_cli.config import Settings
|
|
18
|
+
from fastapi_cloud_cli.utils.api import BuildLogError, TooManyRetriesError
|
|
18
19
|
from tests.conftest import ConfiguredApp
|
|
19
20
|
from tests.utils import Keys, build_logs_response, changing_dir
|
|
20
21
|
|
|
@@ -211,20 +212,6 @@ def test_shows_waitlist_form_when_not_logged_in_longer_flow(
|
|
|
211
212
|
assert "Let's go! Thanks for your interest in FastAPI Cloud! 🚀" in result.output
|
|
212
213
|
|
|
213
214
|
|
|
214
|
-
def test_asks_to_setup_the_app(logged_in_cli: None, tmp_path: Path) -> None:
|
|
215
|
-
steps = [Keys.RIGHT_ARROW, Keys.ENTER]
|
|
216
|
-
|
|
217
|
-
with changing_dir(tmp_path), patch(
|
|
218
|
-
"rich_toolkit.container.getchar"
|
|
219
|
-
) as mock_getchar:
|
|
220
|
-
mock_getchar.side_effect = steps
|
|
221
|
-
|
|
222
|
-
result = runner.invoke(app, ["deploy"])
|
|
223
|
-
|
|
224
|
-
assert result.exit_code == 0
|
|
225
|
-
assert "Setup and deploy" in result.output
|
|
226
|
-
|
|
227
|
-
|
|
228
215
|
@pytest.mark.respx(base_url=settings.base_api_url)
|
|
229
216
|
def test_shows_error_when_trying_to_get_teams(
|
|
230
217
|
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
|
|
@@ -323,7 +310,7 @@ def test_asks_for_app_name_after_team(
|
|
|
323
310
|
def test_creates_app_on_backend(
|
|
324
311
|
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
|
|
325
312
|
) -> None:
|
|
326
|
-
steps = [Keys.ENTER, Keys.ENTER,
|
|
313
|
+
steps = [Keys.ENTER, Keys.ENTER, *"demo", Keys.ENTER]
|
|
327
314
|
|
|
328
315
|
team = _get_random_team()
|
|
329
316
|
|
|
@@ -354,7 +341,7 @@ def test_creates_app_on_backend(
|
|
|
354
341
|
def test_uses_existing_app(
|
|
355
342
|
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
|
|
356
343
|
) -> None:
|
|
357
|
-
steps = [Keys.ENTER, Keys.
|
|
344
|
+
steps = [Keys.ENTER, Keys.RIGHT_ARROW, Keys.ENTER, *"demo", Keys.ENTER]
|
|
358
345
|
|
|
359
346
|
team = _get_random_team()
|
|
360
347
|
|
|
@@ -384,7 +371,6 @@ def test_exits_successfully_when_deployment_is_done(
|
|
|
384
371
|
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
|
|
385
372
|
) -> None:
|
|
386
373
|
steps = [
|
|
387
|
-
Keys.ENTER,
|
|
388
374
|
Keys.ENTER,
|
|
389
375
|
Keys.ENTER,
|
|
390
376
|
*"demo",
|
|
@@ -634,7 +620,6 @@ def test_shows_error_when_app_does_not_exist(
|
|
|
634
620
|
|
|
635
621
|
def _deploy_without_waiting(respx_mock: respx.MockRouter, tmp_path: Path) -> Result:
|
|
636
622
|
steps = [
|
|
637
|
-
Keys.ENTER,
|
|
638
623
|
Keys.ENTER,
|
|
639
624
|
Keys.ENTER,
|
|
640
625
|
*"demo",
|
|
@@ -759,7 +744,6 @@ def test_shows_no_apps_found_message_when_team_has_no_apps(
|
|
|
759
744
|
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
|
|
760
745
|
) -> None:
|
|
761
746
|
steps = [
|
|
762
|
-
Keys.ENTER, # Setup and deploy
|
|
763
747
|
Keys.ENTER, # Select team
|
|
764
748
|
Keys.RIGHT_ARROW, # Choose existing app (No)
|
|
765
749
|
Keys.ENTER,
|
|
@@ -788,11 +772,14 @@ def test_shows_no_apps_found_message_when_team_has_no_apps(
|
|
|
788
772
|
)
|
|
789
773
|
|
|
790
774
|
|
|
775
|
+
@pytest.mark.parametrize(
|
|
776
|
+
"error",
|
|
777
|
+
[BuildLogError, TooManyRetriesError, TimeoutError],
|
|
778
|
+
)
|
|
791
779
|
@pytest.mark.respx(base_url=settings.base_api_url)
|
|
792
|
-
def
|
|
793
|
-
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
|
|
780
|
+
def test_shows_error_message_on_build_exception(
|
|
781
|
+
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter, error: Exception
|
|
794
782
|
) -> None:
|
|
795
|
-
"""Test that BuildLogError is caught and shows dashboard link (lines 384, 387-392)."""
|
|
796
783
|
app_data = _get_random_app()
|
|
797
784
|
team_data = _get_random_team()
|
|
798
785
|
app_id = app_data["id"]
|
|
@@ -819,11 +806,10 @@ def test_handles_build_log_streaming_error(
|
|
|
819
806
|
return_value=Response(200)
|
|
820
807
|
)
|
|
821
808
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
with changing_dir(tmp_path):
|
|
809
|
+
with changing_dir(tmp_path), patch(
|
|
810
|
+
"fastapi_cloud_cli.utils.api.APIClient.stream_build_logs",
|
|
811
|
+
side_effect=error,
|
|
812
|
+
):
|
|
827
813
|
result = runner.invoke(app, ["deploy"])
|
|
828
814
|
|
|
829
815
|
assert result.exit_code == 1
|
|
@@ -832,10 +818,8 @@ def test_handles_build_log_streaming_error(
|
|
|
832
818
|
|
|
833
819
|
|
|
834
820
|
@pytest.mark.respx(base_url=settings.base_api_url)
|
|
835
|
-
def
|
|
836
|
-
logged_in_cli: None,
|
|
837
|
-
tmp_path: Path,
|
|
838
|
-
respx_mock: respx.MockRouter,
|
|
821
|
+
def test_shows_error_message_on_build_log_http_error(
|
|
822
|
+
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
|
|
839
823
|
) -> None:
|
|
840
824
|
app_data = _get_random_app()
|
|
841
825
|
team_data = _get_random_team()
|
|
@@ -870,6 +854,7 @@ def test_shows_error_message_when_build_log_streaming_fails(
|
|
|
870
854
|
with changing_dir(tmp_path), patch("time.sleep"):
|
|
871
855
|
result = runner.invoke(app, ["deploy"])
|
|
872
856
|
|
|
857
|
+
assert result.exit_code == 1
|
|
873
858
|
assert "Unable to stream build logs" in result.output
|
|
874
859
|
assert deployment_data["dashboard_url"] in result.output
|
|
875
860
|
|
|
@@ -1007,3 +992,68 @@ def test_long_wait_messages(
|
|
|
1007
992
|
result = runner.invoke(app, ["deploy"])
|
|
1008
993
|
|
|
1009
994
|
assert "long wait message" in result.output
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
@pytest.mark.respx(base_url=settings.base_api_url)
|
|
998
|
+
def test_calls_upload_cancelled_when_user_interrupts(
|
|
999
|
+
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
|
|
1000
|
+
) -> None:
|
|
1001
|
+
app_data = _get_random_app()
|
|
1002
|
+
team_data = _get_random_team()
|
|
1003
|
+
app_id = app_data["id"]
|
|
1004
|
+
team_id = team_data["id"]
|
|
1005
|
+
deployment_data = _get_random_deployment(app_id=app_id)
|
|
1006
|
+
|
|
1007
|
+
config_path = tmp_path / ".fastapicloud" / "cloud.json"
|
|
1008
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1009
|
+
config_path.write_text(f'{{"app_id": "{app_id}", "team_id": "{team_id}"}}')
|
|
1010
|
+
|
|
1011
|
+
respx_mock.get(f"/apps/{app_id}").mock(return_value=Response(200, json=app_data))
|
|
1012
|
+
respx_mock.post(f"/apps/{app_id}/deployments/").mock(
|
|
1013
|
+
return_value=Response(201, json=deployment_data)
|
|
1014
|
+
)
|
|
1015
|
+
|
|
1016
|
+
upload_cancelled_route = respx_mock.post(
|
|
1017
|
+
f"/deployments/{deployment_data['id']}/upload-cancelled"
|
|
1018
|
+
).mock(return_value=Response(200))
|
|
1019
|
+
|
|
1020
|
+
with changing_dir(tmp_path), patch(
|
|
1021
|
+
"fastapi_cloud_cli.commands.deploy._upload_deployment",
|
|
1022
|
+
side_effect=KeyboardInterrupt(),
|
|
1023
|
+
):
|
|
1024
|
+
runner.invoke(app, ["deploy"])
|
|
1025
|
+
|
|
1026
|
+
assert upload_cancelled_route.called
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
@pytest.mark.respx(base_url=settings.base_api_url)
|
|
1030
|
+
def test_cancel_upload_swallows_exceptions(
|
|
1031
|
+
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
|
|
1032
|
+
) -> None:
|
|
1033
|
+
app_data = _get_random_app()
|
|
1034
|
+
team_data = _get_random_team()
|
|
1035
|
+
app_id = app_data["id"]
|
|
1036
|
+
team_id = team_data["id"]
|
|
1037
|
+
deployment_data = _get_random_deployment(app_id=app_id)
|
|
1038
|
+
|
|
1039
|
+
config_path = tmp_path / ".fastapicloud" / "cloud.json"
|
|
1040
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1041
|
+
config_path.write_text(f'{{"app_id": "{app_id}", "team_id": "{team_id}"}}')
|
|
1042
|
+
|
|
1043
|
+
respx_mock.get(f"/apps/{app_id}").mock(return_value=Response(200, json=app_data))
|
|
1044
|
+
respx_mock.post(f"/apps/{app_id}/deployments/").mock(
|
|
1045
|
+
return_value=Response(201, json=deployment_data)
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
upload_cancelled_route = respx_mock.post(
|
|
1049
|
+
f"/deployments/{deployment_data['id']}/upload-cancelled"
|
|
1050
|
+
).mock(return_value=Response(500))
|
|
1051
|
+
|
|
1052
|
+
with changing_dir(tmp_path), patch(
|
|
1053
|
+
"fastapi_cloud_cli.commands.deploy._upload_deployment",
|
|
1054
|
+
side_effect=KeyboardInterrupt(),
|
|
1055
|
+
):
|
|
1056
|
+
result = runner.invoke(app, ["deploy"])
|
|
1057
|
+
|
|
1058
|
+
assert upload_cancelled_route.called
|
|
1059
|
+
assert "HTTPStatusError" not in result.output
|
|
@@ -19,7 +19,9 @@ assets_path = Path(__file__).parent / "assets"
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
@pytest.mark.respx(base_url=settings.base_api_url)
|
|
22
|
-
def test_shows_a_message_if_something_is_wrong(
|
|
22
|
+
def test_shows_a_message_if_something_is_wrong(
|
|
23
|
+
logged_out_cli: None, respx_mock: respx.MockRouter
|
|
24
|
+
) -> None:
|
|
23
25
|
with patch("fastapi_cloud_cli.commands.login.typer.launch") as mock_open:
|
|
24
26
|
respx_mock.post(
|
|
25
27
|
"/login/device/authorization", data={"client_id": settings.client_id}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.5.2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/commands/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/commands/logout.py
RENAMED
|
File without changes
|
{fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/commands/unlink.py
RENAMED
|
File without changes
|
{fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/commands/whoami.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/src/fastapi_cloud_cli/utils/pydantic_compat.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/broken_package/mod/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/default_files/default_api/api.py
RENAMED
|
File without changes
|
{fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/default_files/default_app/api.py
RENAMED
|
File without changes
|
{fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/default_files/default_app/app.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/default_files/default_main/api.py
RENAMED
|
File without changes
|
{fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/default_files/default_main/app.py
RENAMED
|
File without changes
|
{fastapi_cloud_cli-0.5.2 → fastapi_cloud_cli-0.7.0}/tests/assets/default_files/default_main/main.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|