fastapi-cloud-cli 0.1.4__tar.gz → 0.2.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.1.4 → fastapi_cloud_cli-0.2.0}/PKG-INFO +1 -1
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/pyproject.toml +1 -1
- fastapi_cloud_cli-0.2.0/src/fastapi_cloud_cli/__init__.py +1 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/cli.py +2 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/commands/deploy.py +67 -30
- fastapi_cloud_cli-0.2.0/src/fastapi_cloud_cli/commands/unlink.py +29 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/utils/api.py +1 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/test_cli_deploy.py +100 -17
- fastapi_cloud_cli-0.2.0/tests/test_cli_unlink.py +50 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/test_deploy_utils.py +3 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/test_env_delete.py +3 -1
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/test_env_set.py +3 -1
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/utils.py +1 -0
- fastapi_cloud_cli-0.1.4/src/fastapi_cloud_cli/__init__.py +0 -1
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/LICENSE +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/README.md +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/requirements-tests.txt +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/requirements.txt +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/scripts/format.sh +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/scripts/lint.sh +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/scripts/test-cov-html.sh +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/scripts/test.sh +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/__main__.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/commands/__init__.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/commands/env.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/commands/login.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/commands/logout.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/commands/whoami.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/config.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/logging.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/py.typed +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/utils/__init__.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/utils/apps.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/utils/auth.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/utils/cli.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/utils/config.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/utils/env.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/utils/sentry.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/__init__.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/broken_package/mod/__init__.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/broken_package/mod/app.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/broken_package/utils.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_api/api.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_app/api.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_app/app.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_app_dir_api/app/__init__.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_app_dir_api/app/api.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_app_dir_app/app/__init__.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_app_dir_app/app/api.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_app_dir_app/app/app.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_app_dir_main/app/__init__.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_app_dir_main/app/api.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_app_dir_main/app/app.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_app_dir_main/app/main.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_app_dir_non_default/app/__init__.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_app_dir_non_default/app/nondefault.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_main/api.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_main/app.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_main/main.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/non_default/nonstandard.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/package/__init__.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/package/core/__init__.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/package/core/utils.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/package/mod/__init__.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/package/mod/api.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/package/mod/app.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/package/mod/other.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/single_file_api.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/single_file_app.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/single_file_other.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/conftest.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/test_cli.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/test_cli_login.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/test_cli_logout.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/test_cli_whoami.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/test_config.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/test_env_list.py +0 -0
- {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/test_sentry.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
@@ -4,6 +4,7 @@ from .commands.deploy import deploy
|
|
|
4
4
|
from .commands.env import env_app
|
|
5
5
|
from .commands.login import login
|
|
6
6
|
from .commands.logout import logout
|
|
7
|
+
from .commands.unlink import unlink
|
|
7
8
|
from .commands.whoami import whoami
|
|
8
9
|
from .logging import setup_logging
|
|
9
10
|
from .utils.sentry import init_sentry
|
|
@@ -20,6 +21,7 @@ app.command()(deploy)
|
|
|
20
21
|
app.command()(login)
|
|
21
22
|
app.command()(logout)
|
|
22
23
|
app.command()(whoami)
|
|
24
|
+
app.command()(unlink)
|
|
23
25
|
|
|
24
26
|
app.add_typer(env_app, name="env")
|
|
25
27
|
|
{fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/commands/deploy.py
RENAMED
|
@@ -20,6 +20,7 @@ from rich_toolkit import RichToolkit
|
|
|
20
20
|
from rich_toolkit.menu import Option
|
|
21
21
|
from typing_extensions import Annotated
|
|
22
22
|
|
|
23
|
+
from fastapi_cloud_cli.commands.login import login
|
|
23
24
|
from fastapi_cloud_cli.utils.api import APIClient
|
|
24
25
|
from fastapi_cloud_cli.utils.apps import AppConfig, get_app_config, write_app_config
|
|
25
26
|
from fastapi_cloud_cli.utils.auth import is_logged_in
|
|
@@ -108,8 +109,11 @@ class DeploymentStatus(str, Enum):
|
|
|
108
109
|
ready_for_build = "ready_for_build"
|
|
109
110
|
building = "building"
|
|
110
111
|
extracting = "extracting"
|
|
112
|
+
extracting_failed = "extracting_failed"
|
|
111
113
|
building_image = "building_image"
|
|
114
|
+
building_image_failed = "building_image_failed"
|
|
112
115
|
deploying = "deploying"
|
|
116
|
+
deploying_failed = "deploying_failed"
|
|
113
117
|
success = "success"
|
|
114
118
|
failed = "failed"
|
|
115
119
|
|
|
@@ -120,8 +124,11 @@ class DeploymentStatus(str, Enum):
|
|
|
120
124
|
cls.ready_for_build: "Ready for build",
|
|
121
125
|
cls.building: "Building",
|
|
122
126
|
cls.extracting: "Extracting",
|
|
127
|
+
cls.extracting_failed: "Extracting failed",
|
|
123
128
|
cls.building_image: "Building image",
|
|
129
|
+
cls.building_image_failed: "Build failed",
|
|
124
130
|
cls.deploying: "Deploying",
|
|
131
|
+
cls.deploying_failed: "Deploying failed",
|
|
125
132
|
cls.success: "Success",
|
|
126
133
|
cls.failed: "Failed",
|
|
127
134
|
}[status]
|
|
@@ -345,42 +352,43 @@ def _wait_for_deployment(
|
|
|
345
352
|
with toolkit.progress(
|
|
346
353
|
next(messages), inline_logs=True, lines_to_show=20
|
|
347
354
|
) as progress:
|
|
348
|
-
|
|
349
|
-
|
|
355
|
+
with handle_http_errors(progress=progress):
|
|
356
|
+
for line in _stream_build_logs(deployment.id):
|
|
357
|
+
time_elapsed = time.monotonic() - started_at
|
|
350
358
|
|
|
351
|
-
|
|
359
|
+
data = json.loads(line)
|
|
352
360
|
|
|
353
|
-
|
|
354
|
-
|
|
361
|
+
if "message" in data:
|
|
362
|
+
progress.log(Text.from_ansi(data["message"].rstrip()))
|
|
355
363
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
364
|
+
if data.get("type") == "complete":
|
|
365
|
+
progress.log("")
|
|
366
|
+
progress.log(
|
|
367
|
+
f"🐔 Ready the chicken! Your app is ready at [link={deployment.url}]{deployment.url}[/link]"
|
|
368
|
+
)
|
|
361
369
|
|
|
362
|
-
|
|
370
|
+
progress.log("")
|
|
363
371
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
372
|
+
progress.log(
|
|
373
|
+
f"You can also check the app logs at [link={deployment.dashboard_url}]{deployment.dashboard_url}[/link]"
|
|
374
|
+
)
|
|
367
375
|
|
|
368
|
-
|
|
376
|
+
break
|
|
369
377
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
378
|
+
if data.get("type") == "failed":
|
|
379
|
+
progress.log("")
|
|
380
|
+
progress.log(
|
|
381
|
+
f"😔 Oh no! Something went wrong. Check out the logs at [link={deployment.dashboard_url}]{deployment.dashboard_url}[/link]"
|
|
382
|
+
)
|
|
383
|
+
raise typer.Exit(1)
|
|
376
384
|
|
|
377
|
-
|
|
378
|
-
|
|
385
|
+
if time_elapsed > 30:
|
|
386
|
+
messages = cycle(LONG_WAIT_MESSAGES) # pragma: no cover
|
|
379
387
|
|
|
380
|
-
|
|
381
|
-
|
|
388
|
+
if (time.monotonic() - last_message_changed_at) > 2:
|
|
389
|
+
progress.title = next(messages) # pragma: no cover
|
|
382
390
|
|
|
383
|
-
|
|
391
|
+
last_message_changed_at = time.monotonic() # pragma: no cover
|
|
384
392
|
|
|
385
393
|
|
|
386
394
|
def _setup_environment_variables(toolkit: RichToolkit, app_id: str) -> None:
|
|
@@ -549,10 +557,33 @@ def deploy(
|
|
|
549
557
|
|
|
550
558
|
with get_rich_toolkit() as toolkit:
|
|
551
559
|
if not is_logged_in():
|
|
552
|
-
logger.debug("User not logged in,
|
|
553
|
-
_waitlist_form(toolkit)
|
|
560
|
+
logger.debug("User not logged in, prompting for login or waitlist")
|
|
554
561
|
|
|
555
|
-
|
|
562
|
+
toolkit.print_title("Welcome to FastAPI Cloud!", tag="FastAPI")
|
|
563
|
+
toolkit.print_line()
|
|
564
|
+
|
|
565
|
+
toolkit.print(
|
|
566
|
+
"You need to be logged in to deploy to FastAPI Cloud.",
|
|
567
|
+
tag="info",
|
|
568
|
+
)
|
|
569
|
+
toolkit.print_line()
|
|
570
|
+
|
|
571
|
+
choice = toolkit.ask(
|
|
572
|
+
"What would you like to do?",
|
|
573
|
+
tag="auth",
|
|
574
|
+
options=[
|
|
575
|
+
Option({"name": "Login to my existing account", "value": "login"}),
|
|
576
|
+
Option({"name": "Join the waiting list", "value": "waitlist"}),
|
|
577
|
+
],
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
toolkit.print_line()
|
|
581
|
+
|
|
582
|
+
if choice == "login":
|
|
583
|
+
login()
|
|
584
|
+
else:
|
|
585
|
+
_waitlist_form(toolkit)
|
|
586
|
+
raise typer.Exit(1)
|
|
556
587
|
|
|
557
588
|
toolkit.print_title("Starting deployment", tag="FastAPI")
|
|
558
589
|
toolkit.print_line()
|
|
@@ -585,7 +616,13 @@ def deploy(
|
|
|
585
616
|
"App not found. Make sure you're logged in the correct account."
|
|
586
617
|
)
|
|
587
618
|
|
|
588
|
-
|
|
619
|
+
if not app:
|
|
620
|
+
toolkit.print_line()
|
|
621
|
+
toolkit.print(
|
|
622
|
+
"If you deleted this app, you can run [bold]fastapi unlink[/] to unlink the local configuration.",
|
|
623
|
+
tag="tip",
|
|
624
|
+
)
|
|
625
|
+
raise typer.Exit(1)
|
|
589
626
|
|
|
590
627
|
logger.debug("Creating archive for deployment")
|
|
591
628
|
archive_path = archive(path or Path.cwd()) # noqa: F841
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from fastapi_cloud_cli.utils.cli import get_rich_toolkit
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def unlink() -> Any:
|
|
14
|
+
"""
|
|
15
|
+
Unlink by deleting the `.fastapicloud` directory.
|
|
16
|
+
"""
|
|
17
|
+
with get_rich_toolkit(minimal=True) as toolkit:
|
|
18
|
+
config_dir = Path.cwd() / ".fastapicloud"
|
|
19
|
+
|
|
20
|
+
if not config_dir.exists():
|
|
21
|
+
toolkit.print(
|
|
22
|
+
"No FastAPI Cloud configuration found in the current directory."
|
|
23
|
+
)
|
|
24
|
+
logger.debug(f"Configuration directory not found: {config_dir}")
|
|
25
|
+
raise typer.Exit(1)
|
|
26
|
+
|
|
27
|
+
shutil.rmtree(config_dir)
|
|
28
|
+
toolkit.print("FastAPI Cloud configuration has been unlinked successfully! 🚀")
|
|
29
|
+
logger.debug(f"Deleted configuration directory: {config_dir}")
|
|
@@ -60,10 +60,63 @@ def _get_random_deployment(
|
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
@pytest.mark.respx(base_url=settings.base_api_url)
|
|
63
|
-
def
|
|
63
|
+
def test_chooses_login_option_when_not_logged_in(
|
|
64
64
|
logged_out_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
|
|
65
65
|
) -> None:
|
|
66
|
-
steps = [
|
|
66
|
+
steps = [Keys.ENTER]
|
|
67
|
+
|
|
68
|
+
respx_mock.post(
|
|
69
|
+
"/login/device/authorization", data={"client_id": settings.client_id}
|
|
70
|
+
).mock(
|
|
71
|
+
return_value=Response(
|
|
72
|
+
200,
|
|
73
|
+
json={
|
|
74
|
+
"verification_uri_complete": "http://test.com",
|
|
75
|
+
"verification_uri": "http://test.com",
|
|
76
|
+
"user_code": "1234",
|
|
77
|
+
"device_code": "5678",
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
respx_mock.post(
|
|
82
|
+
"/login/device/token",
|
|
83
|
+
data={
|
|
84
|
+
"device_code": "5678",
|
|
85
|
+
"client_id": settings.client_id,
|
|
86
|
+
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
|
87
|
+
},
|
|
88
|
+
).mock(return_value=Response(200, json={"access_token": "test_token_1234"}))
|
|
89
|
+
|
|
90
|
+
with changing_dir(tmp_path), patch(
|
|
91
|
+
"rich_toolkit.container.getchar"
|
|
92
|
+
) as mock_getchar, patch(
|
|
93
|
+
"fastapi_cloud_cli.commands.login.typer.launch"
|
|
94
|
+
) as mock_launch:
|
|
95
|
+
mock_getchar.side_effect = steps
|
|
96
|
+
|
|
97
|
+
result = runner.invoke(app, ["deploy"])
|
|
98
|
+
|
|
99
|
+
assert "Welcome to FastAPI Cloud!" in result.output
|
|
100
|
+
assert "What would you like to do?" in result.output
|
|
101
|
+
assert "Login to my existing account" in result.output
|
|
102
|
+
assert "Join the waiting list" in result.output
|
|
103
|
+
assert "Now you are logged in!" in result.output
|
|
104
|
+
assert mock_launch.called
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@pytest.mark.respx(base_url=settings.base_api_url)
|
|
108
|
+
def test_chooses_waitlist_option_when_not_logged_in(
|
|
109
|
+
logged_out_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
|
|
110
|
+
) -> None:
|
|
111
|
+
steps = [
|
|
112
|
+
Keys.DOWN_ARROW,
|
|
113
|
+
Keys.ENTER,
|
|
114
|
+
*"some@example.com",
|
|
115
|
+
Keys.ENTER,
|
|
116
|
+
Keys.RIGHT_ARROW,
|
|
117
|
+
Keys.ENTER,
|
|
118
|
+
Keys.ENTER,
|
|
119
|
+
]
|
|
67
120
|
|
|
68
121
|
respx_mock.post(
|
|
69
122
|
"/users/waiting-list",
|
|
@@ -80,13 +133,17 @@ def test_shows_waitlist_form_when_not_logged_in(
|
|
|
80
133
|
).mock(return_value=Response(200))
|
|
81
134
|
|
|
82
135
|
with changing_dir(tmp_path), patch(
|
|
83
|
-
"rich_toolkit.
|
|
136
|
+
"rich_toolkit.container.getchar"
|
|
84
137
|
) as mock_getchar:
|
|
85
138
|
mock_getchar.side_effect = steps
|
|
86
139
|
|
|
87
140
|
result = runner.invoke(app, ["deploy"])
|
|
88
141
|
|
|
89
142
|
assert result.exit_code == 1
|
|
143
|
+
assert "Welcome to FastAPI Cloud!" in result.output
|
|
144
|
+
assert "What would you like to do?" in result.output
|
|
145
|
+
assert "Login to my existing account" in result.output
|
|
146
|
+
assert "Join the waiting list" in result.output
|
|
90
147
|
assert "We're currently in private beta" in result.output
|
|
91
148
|
assert "Let's go! Thanks for your interest in FastAPI Cloud! 🚀" in result.output
|
|
92
149
|
|
|
@@ -96,6 +153,8 @@ def test_shows_waitlist_form_when_not_logged_in_longer_flow(
|
|
|
96
153
|
logged_out_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
|
|
97
154
|
) -> None:
|
|
98
155
|
steps = [
|
|
156
|
+
Keys.DOWN_ARROW, # Select "Join the waiting list"
|
|
157
|
+
Keys.ENTER,
|
|
99
158
|
*"some@example.com",
|
|
100
159
|
Keys.ENTER,
|
|
101
160
|
Keys.ENTER,
|
|
@@ -138,7 +197,7 @@ def test_shows_waitlist_form_when_not_logged_in_longer_flow(
|
|
|
138
197
|
).mock(return_value=Response(200))
|
|
139
198
|
|
|
140
199
|
with changing_dir(tmp_path), patch(
|
|
141
|
-
"rich_toolkit.
|
|
200
|
+
"rich_toolkit.container.getchar"
|
|
142
201
|
) as mock_getchar:
|
|
143
202
|
mock_getchar.side_effect = steps
|
|
144
203
|
|
|
@@ -153,7 +212,7 @@ def test_asks_to_setup_the_app(logged_in_cli: None, tmp_path: Path) -> None:
|
|
|
153
212
|
steps = [Keys.RIGHT_ARROW, Keys.ENTER]
|
|
154
213
|
|
|
155
214
|
with changing_dir(tmp_path), patch(
|
|
156
|
-
"rich_toolkit.
|
|
215
|
+
"rich_toolkit.container.getchar"
|
|
157
216
|
) as mock_getchar:
|
|
158
217
|
mock_getchar.side_effect = steps
|
|
159
218
|
|
|
@@ -171,7 +230,9 @@ def test_shows_error_when_trying_to_get_teams(
|
|
|
171
230
|
|
|
172
231
|
respx_mock.get("/teams/").mock(return_value=Response(500))
|
|
173
232
|
|
|
174
|
-
with changing_dir(tmp_path), patch(
|
|
233
|
+
with changing_dir(tmp_path), patch(
|
|
234
|
+
"rich_toolkit.container.getchar"
|
|
235
|
+
) as mock_getchar:
|
|
175
236
|
mock_getchar.side_effect = steps
|
|
176
237
|
|
|
177
238
|
result = runner.invoke(app, ["deploy"])
|
|
@@ -189,7 +250,9 @@ def test_handles_invalid_auth(
|
|
|
189
250
|
|
|
190
251
|
respx_mock.get("/teams/").mock(return_value=Response(401))
|
|
191
252
|
|
|
192
|
-
with changing_dir(tmp_path), patch(
|
|
253
|
+
with changing_dir(tmp_path), patch(
|
|
254
|
+
"rich_toolkit.container.getchar"
|
|
255
|
+
) as mock_getchar:
|
|
193
256
|
mock_getchar.side_effect = steps
|
|
194
257
|
|
|
195
258
|
result = runner.invoke(app, ["deploy"])
|
|
@@ -215,7 +278,9 @@ def test_shows_teams(
|
|
|
215
278
|
)
|
|
216
279
|
)
|
|
217
280
|
|
|
218
|
-
with changing_dir(tmp_path), patch(
|
|
281
|
+
with changing_dir(tmp_path), patch(
|
|
282
|
+
"rich_toolkit.container.getchar"
|
|
283
|
+
) as mock_getchar:
|
|
219
284
|
mock_getchar.side_effect = steps
|
|
220
285
|
|
|
221
286
|
result = runner.invoke(app, ["deploy"])
|
|
@@ -239,7 +304,9 @@ def test_asks_for_app_name_after_team(
|
|
|
239
304
|
)
|
|
240
305
|
)
|
|
241
306
|
|
|
242
|
-
with changing_dir(tmp_path), patch(
|
|
307
|
+
with changing_dir(tmp_path), patch(
|
|
308
|
+
"rich_toolkit.container.getchar"
|
|
309
|
+
) as mock_getchar:
|
|
243
310
|
mock_getchar.side_effect = steps
|
|
244
311
|
|
|
245
312
|
result = runner.invoke(app, ["deploy"])
|
|
@@ -268,7 +335,9 @@ def test_creates_app_on_backend(
|
|
|
268
335
|
return_value=Response(201, json=_get_random_app(team_id=team["id"]))
|
|
269
336
|
)
|
|
270
337
|
|
|
271
|
-
with changing_dir(tmp_path), patch(
|
|
338
|
+
with changing_dir(tmp_path), patch(
|
|
339
|
+
"rich_toolkit.container.getchar"
|
|
340
|
+
) as mock_getchar:
|
|
272
341
|
mock_getchar.side_effect = steps
|
|
273
342
|
|
|
274
343
|
result = runner.invoke(app, ["deploy"])
|
|
@@ -294,7 +363,9 @@ def test_uses_existing_app(
|
|
|
294
363
|
return_value=Response(200, json={"data": [app_data]})
|
|
295
364
|
)
|
|
296
365
|
|
|
297
|
-
with changing_dir(tmp_path), patch(
|
|
366
|
+
with changing_dir(tmp_path), patch(
|
|
367
|
+
"rich_toolkit.container.getchar"
|
|
368
|
+
) as mock_getchar:
|
|
298
369
|
mock_getchar.side_effect = steps
|
|
299
370
|
|
|
300
371
|
result = runner.invoke(app, ["deploy"])
|
|
@@ -367,7 +438,9 @@ def test_exits_successfully_when_deployment_is_done(
|
|
|
367
438
|
)
|
|
368
439
|
)
|
|
369
440
|
|
|
370
|
-
with changing_dir(tmp_path), patch(
|
|
441
|
+
with changing_dir(tmp_path), patch(
|
|
442
|
+
"rich_toolkit.container.getchar"
|
|
443
|
+
) as mock_getchar:
|
|
371
444
|
mock_getchar.side_effect = steps
|
|
372
445
|
|
|
373
446
|
result = runner.invoke(app, ["deploy"])
|
|
@@ -615,7 +688,9 @@ def _deploy_without_waiting(respx_mock: respx.MockRouter, tmp_path: Path) -> Res
|
|
|
615
688
|
return_value=Response(200)
|
|
616
689
|
)
|
|
617
690
|
|
|
618
|
-
with changing_dir(tmp_path), patch(
|
|
691
|
+
with changing_dir(tmp_path), patch(
|
|
692
|
+
"rich_toolkit.container.getchar"
|
|
693
|
+
) as mock_getchar:
|
|
619
694
|
mock_getchar.side_effect = steps
|
|
620
695
|
|
|
621
696
|
return runner.invoke(app, ["deploy", "--no-wait"])
|
|
@@ -687,7 +762,9 @@ def test_creates_environment_variables_during_app_setup(
|
|
|
687
762
|
f"/apps/{app_data['id']}/environment-variables/", json={"API_KEY": "secret123"}
|
|
688
763
|
).mock(return_value=Response(200))
|
|
689
764
|
|
|
690
|
-
with changing_dir(tmp_path), patch(
|
|
765
|
+
with changing_dir(tmp_path), patch(
|
|
766
|
+
"rich_toolkit.container.getchar"
|
|
767
|
+
) as mock_getchar:
|
|
691
768
|
mock_getchar.side_effect = steps
|
|
692
769
|
|
|
693
770
|
result = runner.invoke(app, ["deploy"])
|
|
@@ -731,7 +808,9 @@ def test_rejects_invalid_environment_variable_names(
|
|
|
731
808
|
f"/apps/{app_data['id']}/environment-variables/", json={"VALID_KEY": "value123"}
|
|
732
809
|
).mock(return_value=Response(200))
|
|
733
810
|
|
|
734
|
-
with changing_dir(tmp_path), patch(
|
|
811
|
+
with changing_dir(tmp_path), patch(
|
|
812
|
+
"rich_toolkit.container.getchar"
|
|
813
|
+
) as mock_getchar:
|
|
735
814
|
mock_getchar.side_effect = steps
|
|
736
815
|
|
|
737
816
|
result = runner.invoke(app, ["deploy"])
|
|
@@ -747,6 +826,8 @@ def test_shows_error_for_invalid_waitlist_form_data(
|
|
|
747
826
|
logged_out_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
|
|
748
827
|
) -> None:
|
|
749
828
|
steps = [
|
|
829
|
+
Keys.DOWN_ARROW, # Select "Join the waiting list"
|
|
830
|
+
Keys.ENTER,
|
|
750
831
|
*"test@example.com",
|
|
751
832
|
Keys.ENTER,
|
|
752
833
|
Keys.ENTER, # Choose to provide more information
|
|
@@ -754,7 +835,7 @@ def test_shows_error_for_invalid_waitlist_form_data(
|
|
|
754
835
|
]
|
|
755
836
|
|
|
756
837
|
with changing_dir(tmp_path), patch(
|
|
757
|
-
"rich_toolkit.
|
|
838
|
+
"rich_toolkit.container.getchar"
|
|
758
839
|
) as mock_getchar, patch("rich_toolkit.form.Form.run") as mock_form_run:
|
|
759
840
|
mock_getchar.side_effect = steps
|
|
760
841
|
# Simulate form returning data with invalid email field to trigger ValidationError
|
|
@@ -789,7 +870,9 @@ def test_shows_no_apps_found_message_when_team_has_no_apps(
|
|
|
789
870
|
return_value=Response(200, json={"data": []})
|
|
790
871
|
)
|
|
791
872
|
|
|
792
|
-
with changing_dir(tmp_path), patch(
|
|
873
|
+
with changing_dir(tmp_path), patch(
|
|
874
|
+
"rich_toolkit.container.getchar"
|
|
875
|
+
) as mock_getchar:
|
|
793
876
|
mock_getchar.side_effect = steps
|
|
794
877
|
|
|
795
878
|
result = runner.invoke(app, ["deploy"])
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
4
|
+
from typer.testing import CliRunner
|
|
5
|
+
|
|
6
|
+
from fastapi_cloud_cli.cli import app
|
|
7
|
+
|
|
8
|
+
runner = CliRunner()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_unlink_removes_fastapicloud_dir(tmp_path: Path) -> None:
|
|
12
|
+
config_dir = tmp_path / ".fastapicloud"
|
|
13
|
+
config_dir.mkdir(parents=True)
|
|
14
|
+
|
|
15
|
+
cloud_json = config_dir / "cloud.json"
|
|
16
|
+
cloud_json.write_text('{"app_id": "123", "team_id": "456"}')
|
|
17
|
+
|
|
18
|
+
readme_file = config_dir / "README.md"
|
|
19
|
+
readme_file.write_text("# FastAPI Cloud Configuration")
|
|
20
|
+
|
|
21
|
+
gitignore_file = config_dir / ".gitignore"
|
|
22
|
+
gitignore_file.write_text("*")
|
|
23
|
+
|
|
24
|
+
with patch("fastapi_cloud_cli.commands.unlink.Path.cwd", return_value=tmp_path):
|
|
25
|
+
result = runner.invoke(app, ["unlink"])
|
|
26
|
+
|
|
27
|
+
assert result.exit_code == 0
|
|
28
|
+
assert (
|
|
29
|
+
"FastAPI Cloud configuration has been unlinked successfully! 🚀"
|
|
30
|
+
in result.output
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
assert not config_dir.exists()
|
|
34
|
+
assert not cloud_json.exists()
|
|
35
|
+
assert not readme_file.exists()
|
|
36
|
+
assert not gitignore_file.exists()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_unlink_when_no_configuration_exists(tmp_path: Path) -> None:
|
|
40
|
+
config_dir = tmp_path / ".fastapicloud"
|
|
41
|
+
assert not config_dir.exists()
|
|
42
|
+
|
|
43
|
+
with patch("fastapi_cloud_cli.commands.unlink.Path.cwd", return_value=tmp_path):
|
|
44
|
+
result = runner.invoke(app, ["unlink"])
|
|
45
|
+
|
|
46
|
+
assert result.exit_code == 1
|
|
47
|
+
assert (
|
|
48
|
+
"No FastAPI Cloud configuration found in the current directory."
|
|
49
|
+
in result.output
|
|
50
|
+
)
|
|
@@ -51,8 +51,11 @@ def test_includes_paths(path: Path) -> None:
|
|
|
51
51
|
(DeploymentStatus.ready_for_build, "Ready for build"),
|
|
52
52
|
(DeploymentStatus.building, "Building"),
|
|
53
53
|
(DeploymentStatus.extracting, "Extracting"),
|
|
54
|
+
(DeploymentStatus.extracting_failed, "Extracting failed"),
|
|
54
55
|
(DeploymentStatus.building_image, "Building image"),
|
|
56
|
+
(DeploymentStatus.building_image_failed, "Build failed"),
|
|
55
57
|
(DeploymentStatus.deploying, "Deploying"),
|
|
58
|
+
(DeploymentStatus.deploying_failed, "Deploying failed"),
|
|
56
59
|
(DeploymentStatus.success, "Success"),
|
|
57
60
|
(DeploymentStatus.failed, "Failed"),
|
|
58
61
|
],
|
|
@@ -122,7 +122,9 @@ def test_shows_selector_for_environment_variables(
|
|
|
122
122
|
return_value=Response(204)
|
|
123
123
|
)
|
|
124
124
|
|
|
125
|
-
with changing_dir(configured_app), patch(
|
|
125
|
+
with changing_dir(configured_app), patch(
|
|
126
|
+
"rich_toolkit.container.getchar", side_effect=steps
|
|
127
|
+
):
|
|
126
128
|
result = runner.invoke(app, ["env", "delete"])
|
|
127
129
|
|
|
128
130
|
assert result.exit_code == 0
|
|
@@ -86,7 +86,9 @@ def test_asks_for_name_and_value(
|
|
|
86
86
|
return_value=Response(200)
|
|
87
87
|
)
|
|
88
88
|
|
|
89
|
-
with changing_dir(configured_app), patch(
|
|
89
|
+
with changing_dir(configured_app), patch(
|
|
90
|
+
"rich_toolkit.container.getchar", side_effect=steps
|
|
91
|
+
):
|
|
90
92
|
result = runner.invoke(app, ["env", "set"])
|
|
91
93
|
|
|
92
94
|
assert result.exit_code == 0
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.4"
|
|
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.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/commands/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/commands/logout.py
RENAMED
|
File without changes
|
{fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.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
|
|
File without changes
|
|
File without changes
|
{fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/broken_package/mod/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_api/api.py
RENAMED
|
File without changes
|
{fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_app/api.py
RENAMED
|
File without changes
|
{fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.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.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_main/api.py
RENAMED
|
File without changes
|
{fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_main/app.py
RENAMED
|
File without changes
|
{fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.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
|