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.
Files changed (78) hide show
  1. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/PKG-INFO +1 -1
  2. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/pyproject.toml +1 -1
  3. fastapi_cloud_cli-0.2.0/src/fastapi_cloud_cli/__init__.py +1 -0
  4. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/cli.py +2 -0
  5. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/commands/deploy.py +67 -30
  6. fastapi_cloud_cli-0.2.0/src/fastapi_cloud_cli/commands/unlink.py +29 -0
  7. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/utils/api.py +1 -0
  8. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/test_cli_deploy.py +100 -17
  9. fastapi_cloud_cli-0.2.0/tests/test_cli_unlink.py +50 -0
  10. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/test_deploy_utils.py +3 -0
  11. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/test_env_delete.py +3 -1
  12. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/test_env_set.py +3 -1
  13. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/utils.py +1 -0
  14. fastapi_cloud_cli-0.1.4/src/fastapi_cloud_cli/__init__.py +0 -1
  15. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/LICENSE +0 -0
  16. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/README.md +0 -0
  17. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/requirements-tests.txt +0 -0
  18. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/requirements.txt +0 -0
  19. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/scripts/format.sh +0 -0
  20. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/scripts/lint.sh +0 -0
  21. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/scripts/test-cov-html.sh +0 -0
  22. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/scripts/test.sh +0 -0
  23. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/__main__.py +0 -0
  24. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/commands/__init__.py +0 -0
  25. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/commands/env.py +0 -0
  26. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/commands/login.py +0 -0
  27. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/commands/logout.py +0 -0
  28. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/commands/whoami.py +0 -0
  29. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/config.py +0 -0
  30. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/logging.py +0 -0
  31. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/py.typed +0 -0
  32. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/utils/__init__.py +0 -0
  33. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/utils/apps.py +0 -0
  34. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/utils/auth.py +0 -0
  35. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/utils/cli.py +0 -0
  36. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/utils/config.py +0 -0
  37. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/utils/env.py +0 -0
  38. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/src/fastapi_cloud_cli/utils/sentry.py +0 -0
  39. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/__init__.py +0 -0
  40. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/broken_package/mod/__init__.py +0 -0
  41. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/broken_package/mod/app.py +0 -0
  42. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/broken_package/utils.py +0 -0
  43. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_api/api.py +0 -0
  44. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_app/api.py +0 -0
  45. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_app/app.py +0 -0
  46. {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
  47. {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
  48. {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
  49. {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
  50. {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
  51. {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
  52. {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
  53. {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
  54. {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
  55. {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
  56. {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
  57. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_main/api.py +0 -0
  58. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_main/app.py +0 -0
  59. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/default_main/main.py +0 -0
  60. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/default_files/non_default/nonstandard.py +0 -0
  61. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/package/__init__.py +0 -0
  62. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/package/core/__init__.py +0 -0
  63. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/package/core/utils.py +0 -0
  64. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/package/mod/__init__.py +0 -0
  65. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/package/mod/api.py +0 -0
  66. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/package/mod/app.py +0 -0
  67. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/package/mod/other.py +0 -0
  68. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/single_file_api.py +0 -0
  69. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/single_file_app.py +0 -0
  70. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/assets/single_file_other.py +0 -0
  71. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/conftest.py +0 -0
  72. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/test_cli.py +0 -0
  73. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/test_cli_login.py +0 -0
  74. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/test_cli_logout.py +0 -0
  75. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/test_cli_whoami.py +0 -0
  76. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/test_config.py +0 -0
  77. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.0}/tests/test_env_list.py +0 -0
  78. {fastapi_cloud_cli-0.1.4 → fastapi_cloud_cli-0.2.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.1.4
3
+ Version: 0.2.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
@@ -40,7 +40,7 @@ dependencies = [
40
40
  "pydantic[email] >= 1.6.1",
41
41
  "sentry-sdk >= 2.20.0",
42
42
  ]
43
- version = "0.1.4"
43
+ version = "0.2.0"
44
44
 
45
45
  [project.license]
46
46
  text = "MIT"
@@ -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
 
@@ -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
- for line in _stream_build_logs(deployment.id):
349
- time_elapsed = time.monotonic() - started_at
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
- data = json.loads(line)
359
+ data = json.loads(line)
352
360
 
353
- if "message" in data:
354
- progress.log(Text.from_ansi(data["message"].rstrip()))
361
+ if "message" in data:
362
+ progress.log(Text.from_ansi(data["message"].rstrip()))
355
363
 
356
- if data.get("type") == "complete":
357
- progress.log("")
358
- progress.log(
359
- f"🐔 Ready the chicken! Your app is ready at [link={deployment.url}]{deployment.url}[/link]"
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
- progress.log("")
370
+ progress.log("")
363
371
 
364
- progress.log(
365
- f"You can also check the app logs at [link={deployment.dashboard_url}]{deployment.dashboard_url}[/link]"
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
- break
376
+ break
369
377
 
370
- if data.get("type") == "failed":
371
- progress.log("")
372
- progress.log(
373
- f"😔 Oh no! Something went wrong. Check out the logs at [link={deployment.dashboard_url}]{deployment.dashboard_url}[/link]"
374
- )
375
- raise typer.Exit(1)
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
- if time_elapsed > 30:
378
- messages = cycle(LONG_WAIT_MESSAGES) # pragma: no cover
385
+ if time_elapsed > 30:
386
+ messages = cycle(LONG_WAIT_MESSAGES) # pragma: no cover
379
387
 
380
- if (time.monotonic() - last_message_changed_at) > 2:
381
- progress.title = next(messages) # pragma: no cover
388
+ if (time.monotonic() - last_message_changed_at) > 2:
389
+ progress.title = next(messages) # pragma: no cover
382
390
 
383
- last_message_changed_at = time.monotonic() # pragma: no cover
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, showing waitlist form")
553
- _waitlist_form(toolkit)
560
+ logger.debug("User not logged in, prompting for login or waitlist")
554
561
 
555
- raise typer.Exit(1)
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
- raise typer.Exit(1)
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}")
@@ -13,6 +13,7 @@ class APIClient(httpx.Client):
13
13
 
14
14
  super().__init__(
15
15
  base_url=settings.base_api_url,
16
+ timeout=httpx.Timeout(20),
16
17
  headers={
17
18
  "Authorization": f"Bearer {token}",
18
19
  "User-Agent": f"fastapi-cloud-cli/{__version__}",
@@ -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 test_shows_waitlist_form_when_not_logged_in(
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 = [*"some@example.com", Keys.ENTER, Keys.RIGHT_ARROW, Keys.ENTER, Keys.ENTER]
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.menu.click.getchar"
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.menu.click.getchar"
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.menu.click.getchar"
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("click.getchar") as mock_getchar:
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("click.getchar") as mock_getchar:
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("click.getchar") as mock_getchar:
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("click.getchar") as mock_getchar:
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("click.getchar") as mock_getchar:
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("click.getchar") as mock_getchar:
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("click.getchar") as mock_getchar:
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("click.getchar") as mock_getchar:
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("click.getchar") as mock_getchar:
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("click.getchar") as mock_getchar:
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.menu.click.getchar"
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("click.getchar") as mock_getchar:
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("click.getchar", side_effect=steps):
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("click.getchar", side_effect=steps):
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
@@ -16,6 +16,7 @@ def changing_dir(directory: Union[str, Path]) -> Generator[None, None, None]:
16
16
 
17
17
  class Keys:
18
18
  RIGHT_ARROW = "\x1b[C"
19
+ DOWN_ARROW = "\x1b[B"
19
20
  ENTER = "\r"
20
21
  CTRL_C = "\x03"
21
22
  TAB = "\t"
@@ -1 +0,0 @@
1
- __version__ = "0.1.4"