fastapi-cloud-cli 0.10.1__tar.gz → 0.11.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 (83) hide show
  1. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/PKG-INFO +1 -1
  2. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/pyproject.toml +1 -1
  3. fastapi_cloud_cli-0.11.0/src/fastapi_cloud_cli/__init__.py +1 -0
  4. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/src/fastapi_cloud_cli/cli.py +2 -0
  5. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/src/fastapi_cloud_cli/commands/deploy.py +3 -0
  6. fastapi_cloud_cli-0.11.0/src/fastapi_cloud_cli/commands/link.py +117 -0
  7. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/test_archive.py +3 -3
  8. fastapi_cloud_cli-0.11.0/tests/test_cli_link.py +191 -0
  9. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/test_deploy_utils.py +7 -0
  10. fastapi_cloud_cli-0.10.1/src/fastapi_cloud_cli/__init__.py +0 -1
  11. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/LICENSE +0 -0
  12. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/README.md +0 -0
  13. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/scripts/format.sh +0 -0
  14. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/scripts/lint.sh +0 -0
  15. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/scripts/test-cov-html.sh +0 -0
  16. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/scripts/test.sh +0 -0
  17. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/src/fastapi_cloud_cli/__main__.py +0 -0
  18. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/src/fastapi_cloud_cli/commands/__init__.py +0 -0
  19. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/src/fastapi_cloud_cli/commands/env.py +0 -0
  20. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/src/fastapi_cloud_cli/commands/login.py +0 -0
  21. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/src/fastapi_cloud_cli/commands/logout.py +0 -0
  22. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/src/fastapi_cloud_cli/commands/logs.py +0 -0
  23. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/src/fastapi_cloud_cli/commands/unlink.py +0 -0
  24. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/src/fastapi_cloud_cli/commands/whoami.py +0 -0
  25. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/src/fastapi_cloud_cli/config.py +0 -0
  26. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/src/fastapi_cloud_cli/logging.py +0 -0
  27. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/src/fastapi_cloud_cli/py.typed +0 -0
  28. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/src/fastapi_cloud_cli/utils/__init__.py +0 -0
  29. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/src/fastapi_cloud_cli/utils/api.py +0 -0
  30. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/src/fastapi_cloud_cli/utils/apps.py +0 -0
  31. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/src/fastapi_cloud_cli/utils/auth.py +0 -0
  32. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/src/fastapi_cloud_cli/utils/cli.py +0 -0
  33. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/src/fastapi_cloud_cli/utils/config.py +0 -0
  34. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/src/fastapi_cloud_cli/utils/env.py +0 -0
  35. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/src/fastapi_cloud_cli/utils/sentry.py +0 -0
  36. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/__init__.py +0 -0
  37. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/broken_package/mod/__init__.py +0 -0
  38. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/broken_package/mod/app.py +0 -0
  39. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/broken_package/utils.py +0 -0
  40. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/default_files/default_api/api.py +0 -0
  41. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/default_files/default_app/api.py +0 -0
  42. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/default_files/default_app/app.py +0 -0
  43. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/default_files/default_app_dir_api/app/__init__.py +0 -0
  44. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/default_files/default_app_dir_api/app/api.py +0 -0
  45. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/default_files/default_app_dir_app/app/__init__.py +0 -0
  46. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/default_files/default_app_dir_app/app/api.py +0 -0
  47. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/default_files/default_app_dir_app/app/app.py +0 -0
  48. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/default_files/default_app_dir_main/app/__init__.py +0 -0
  49. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/default_files/default_app_dir_main/app/api.py +0 -0
  50. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/default_files/default_app_dir_main/app/app.py +0 -0
  51. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/default_files/default_app_dir_main/app/main.py +0 -0
  52. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/default_files/default_app_dir_non_default/app/__init__.py +0 -0
  53. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/default_files/default_app_dir_non_default/app/nondefault.py +0 -0
  54. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/default_files/default_main/api.py +0 -0
  55. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/default_files/default_main/app.py +0 -0
  56. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/default_files/default_main/main.py +0 -0
  57. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/default_files/non_default/nonstandard.py +0 -0
  58. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/package/__init__.py +0 -0
  59. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/package/core/__init__.py +0 -0
  60. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/package/core/utils.py +0 -0
  61. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/package/mod/__init__.py +0 -0
  62. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/package/mod/api.py +0 -0
  63. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/package/mod/app.py +0 -0
  64. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/package/mod/other.py +0 -0
  65. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/single_file_api.py +0 -0
  66. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/single_file_app.py +0 -0
  67. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/assets/single_file_other.py +0 -0
  68. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/conftest.py +0 -0
  69. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/test_api_client.py +0 -0
  70. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/test_auth.py +0 -0
  71. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/test_cli.py +0 -0
  72. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/test_cli_deploy.py +0 -0
  73. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/test_cli_login.py +0 -0
  74. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/test_cli_logout.py +0 -0
  75. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/test_cli_unlink.py +0 -0
  76. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/test_cli_whoami.py +0 -0
  77. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/test_config.py +0 -0
  78. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/test_env_delete.py +0 -0
  79. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/test_env_list.py +0 -0
  80. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/test_env_set.py +0 -0
  81. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/test_logs.py +0 -0
  82. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.0}/tests/test_sentry.py +0 -0
  83. {fastapi_cloud_cli-0.10.1 → fastapi_cloud_cli-0.11.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.10.1
3
+ Version: 0.11.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
@@ -39,7 +39,7 @@ dependencies = [
39
39
  "sentry-sdk >= 2.20.0",
40
40
  "fastar >= 0.8.0",
41
41
  ]
42
- version = "0.10.1"
42
+ version = "0.11.0"
43
43
 
44
44
  [project.license]
45
45
  text = "MIT"
@@ -0,0 +1 @@
1
+ __version__ = "0.11.0"
@@ -2,6 +2,7 @@ import typer
2
2
 
3
3
  from .commands.deploy import deploy
4
4
  from .commands.env import env_app
5
+ from .commands.link import link
5
6
  from .commands.login import login
6
7
  from .commands.logout import logout
7
8
  from .commands.logs import logs
@@ -25,6 +26,7 @@ cloud_app = typer.Typer(
25
26
 
26
27
  # fastapi cloud [command]
27
28
  cloud_app.command()(deploy)
29
+ cloud_app.command()(link)
28
30
  cloud_app.command()(login)
29
31
  cloud_app.command()(logs)
30
32
  cloud_app.command()(logout)
@@ -62,6 +62,9 @@ def _should_exclude_entry(path: Path) -> bool:
62
62
  if path.suffix == ".pyc":
63
63
  return True
64
64
 
65
+ if path.name == ".env" or path.name.startswith(".env."):
66
+ return True
67
+
65
68
  return False
66
69
 
67
70
 
@@ -0,0 +1,117 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+ import typer
6
+ from rich_toolkit.menu import Option
7
+
8
+ from fastapi_cloud_cli.utils.api import APIClient
9
+ from fastapi_cloud_cli.utils.apps import AppConfig, get_app_config, write_app_config
10
+ from fastapi_cloud_cli.utils.auth import Identity
11
+ from fastapi_cloud_cli.utils.cli import get_rich_toolkit, handle_http_errors
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def link() -> Any:
17
+ """
18
+ Link a local directory to an existing FastAPI Cloud app.
19
+ """
20
+ identity = Identity()
21
+
22
+ with get_rich_toolkit() as toolkit:
23
+ if not identity.is_logged_in():
24
+ toolkit.print(
25
+ "[error]You need to be logged in to link an app.[/]",
26
+ )
27
+ toolkit.print_line()
28
+ toolkit.print(
29
+ "Run [bold]fastapi cloud login[/] to authenticate.",
30
+ tag="tip",
31
+ )
32
+ raise typer.Exit(1)
33
+
34
+ path_to_link = Path.cwd()
35
+
36
+ if get_app_config(path_to_link):
37
+ toolkit.print(
38
+ "[error]This directory is already linked to an app.[/]",
39
+ )
40
+ toolkit.print_line()
41
+ toolkit.print(
42
+ "Run [bold]fastapi cloud unlink[/] first to remove the existing configuration.",
43
+ tag="tip",
44
+ )
45
+ raise typer.Exit(1)
46
+
47
+ toolkit.print_title("Link to FastAPI Cloud", tag="FastAPI")
48
+ toolkit.print_line()
49
+
50
+ with toolkit.progress("Fetching teams...") as progress:
51
+ with handle_http_errors(
52
+ progress, message="Error fetching teams. Please try again later."
53
+ ):
54
+ with APIClient() as client:
55
+ response = client.get("/teams/")
56
+ response.raise_for_status()
57
+ teams_data = response.json()["data"]
58
+
59
+ if not teams_data:
60
+ toolkit.print(
61
+ "[error]No teams found. Please create a team first.[/]",
62
+ )
63
+ raise typer.Exit(1)
64
+
65
+ toolkit.print_line()
66
+
67
+ team = toolkit.ask(
68
+ "Select the team:",
69
+ tag="team",
70
+ options=[
71
+ Option({"name": t["name"], "value": {"id": t["id"], "name": t["name"]}})
72
+ for t in teams_data
73
+ ],
74
+ )
75
+
76
+ toolkit.print_line()
77
+
78
+ with toolkit.progress("Fetching apps...") as progress:
79
+ with handle_http_errors(
80
+ progress, message="Error fetching apps. Please try again later."
81
+ ):
82
+ with APIClient() as client:
83
+ response = client.get("/apps/", params={"team_id": team["id"]})
84
+ response.raise_for_status()
85
+ apps_data = response.json()["data"]
86
+
87
+ if not apps_data:
88
+ toolkit.print(
89
+ "[error]No apps found in this team.[/]",
90
+ )
91
+ toolkit.print_line()
92
+ toolkit.print(
93
+ "Run [bold]fastapi cloud deploy[/] to create and deploy a new app.",
94
+ tag="tip",
95
+ )
96
+ raise typer.Exit(1)
97
+
98
+ toolkit.print_line()
99
+
100
+ app = toolkit.ask(
101
+ "Select the app to link:",
102
+ tag="app",
103
+ options=[
104
+ Option({"name": a["slug"], "value": {"id": a["id"], "slug": a["slug"]}})
105
+ for a in apps_data
106
+ ],
107
+ )
108
+
109
+ toolkit.print_line()
110
+
111
+ app_config = AppConfig(app_id=app["id"], team_id=team["id"])
112
+ write_app_config(path_to_link, app_config)
113
+
114
+ toolkit.print(
115
+ f"Successfully linked to app [bold]{app['slug']}[/bold]! 🔗",
116
+ )
117
+ logger.debug(f"Linked to app: {app['id']} in team: {team['id']}")
@@ -143,12 +143,13 @@ def test_archive_respects_fastapicloudignore_unignore(
143
143
  }
144
144
 
145
145
 
146
- def test_archive_includes_hidden_files(
146
+ def test_archive_includes_hidden_files_but_excludes_env(
147
147
  src_path: Path, tar_path: Path, dst_path: Path
148
148
  ) -> None:
149
- """Should include hidden files in the archive by default."""
149
+ """Should include hidden files but exclude .env files."""
150
150
  (src_path / "main.py").write_text("print('hello')")
151
151
  (src_path / ".env").write_text("SECRET_KEY=xyz")
152
+ (src_path / ".env.local").write_text("LOCAL_KEY=abc")
152
153
  (src_path / ".config").mkdir()
153
154
  (src_path / ".config" / "settings.json").write_text('{"setting": "value"}')
154
155
 
@@ -159,7 +160,6 @@ def test_archive_includes_hidden_files(
159
160
 
160
161
  assert set(dst_path.glob("**/*")) == {
161
162
  dst_path / "main.py",
162
- dst_path / ".env",
163
163
  dst_path / ".config",
164
164
  dst_path / ".config" / "settings.json",
165
165
  }
@@ -0,0 +1,191 @@
1
+ from pathlib import Path
2
+ from unittest.mock import patch
3
+
4
+ import pytest
5
+ import respx
6
+ from httpx import Response
7
+ from typer.testing import CliRunner
8
+
9
+ from fastapi_cloud_cli.cli import cloud_app as app
10
+ from fastapi_cloud_cli.config import Settings
11
+ from fastapi_cloud_cli.utils.apps import AppConfig
12
+ from tests.conftest import ConfiguredApp
13
+ from tests.utils import Keys, changing_dir
14
+
15
+ runner = CliRunner()
16
+ settings = Settings.get()
17
+
18
+
19
+ def test_shows_a_message_if_not_logged_in(logged_out_cli: None) -> None:
20
+ result = runner.invoke(app, ["link"])
21
+
22
+ assert result.exit_code == 1
23
+ assert "You need to be logged in to link an app." in result.output
24
+
25
+
26
+ def test_shows_a_message_if_already_linked(
27
+ logged_in_cli: None, configured_app: ConfiguredApp
28
+ ) -> None:
29
+ with changing_dir(configured_app.path):
30
+ result = runner.invoke(app, ["link"])
31
+
32
+ assert result.exit_code == 1
33
+ assert "This directory is already linked to an app." in result.output
34
+
35
+
36
+ @pytest.mark.respx(base_url=settings.base_api_url)
37
+ def test_shows_a_message_if_no_teams(
38
+ logged_in_cli: None, respx_mock: respx.MockRouter, tmp_path: Path
39
+ ) -> None:
40
+ respx_mock.get("/teams/").mock(return_value=Response(200, json={"data": []}))
41
+
42
+ with changing_dir(tmp_path):
43
+ result = runner.invoke(app, ["link"])
44
+
45
+ assert result.exit_code == 1
46
+ assert "No teams found" in result.output
47
+
48
+
49
+ @pytest.mark.respx(base_url=settings.base_api_url)
50
+ def test_shows_a_message_if_no_apps(
51
+ logged_in_cli: None, respx_mock: respx.MockRouter, tmp_path: Path
52
+ ) -> None:
53
+ steps = [Keys.ENTER]
54
+
55
+ respx_mock.get("/teams/").mock(
56
+ return_value=Response(
57
+ 200, json={"data": [{"id": "team-1", "name": "My Team", "slug": "my-team"}]}
58
+ )
59
+ )
60
+ respx_mock.get("/apps/", params={"team_id": "team-1"}).mock(
61
+ return_value=Response(200, json={"data": []})
62
+ )
63
+
64
+ with (
65
+ changing_dir(tmp_path),
66
+ patch("rich_toolkit.container.getchar") as mock_getchar,
67
+ ):
68
+ mock_getchar.side_effect = steps
69
+ result = runner.invoke(app, ["link"])
70
+
71
+ assert result.exit_code == 1
72
+ assert "No apps found in this team." in result.output
73
+
74
+
75
+ @pytest.mark.respx(base_url=settings.base_api_url)
76
+ def test_links_successfully(
77
+ logged_in_cli: None, respx_mock: respx.MockRouter, tmp_path: Path
78
+ ) -> None:
79
+ steps = [Keys.ENTER, Keys.ENTER]
80
+
81
+ respx_mock.get("/teams/").mock(
82
+ return_value=Response(
83
+ 200, json={"data": [{"id": "team-1", "name": "My Team", "slug": "my-team"}]}
84
+ )
85
+ )
86
+ respx_mock.get("/apps/", params={"team_id": "team-1"}).mock(
87
+ return_value=Response(200, json={"data": [{"id": "app-1", "slug": "my-app"}]})
88
+ )
89
+
90
+ with (
91
+ changing_dir(tmp_path),
92
+ patch("rich_toolkit.container.getchar") as mock_getchar,
93
+ ):
94
+ mock_getchar.side_effect = steps
95
+ result = runner.invoke(app, ["link"])
96
+
97
+ assert result.exit_code == 0
98
+ assert "Successfully linked to app" in result.output
99
+ assert "my-app" in result.output
100
+
101
+ config_path = tmp_path / ".fastapicloud" / "cloud.json"
102
+ assert config_path.exists()
103
+ config = AppConfig.model_validate_json(config_path.read_text())
104
+ assert config.app_id == "app-1"
105
+ assert config.team_id == "team-1"
106
+
107
+
108
+ @pytest.mark.respx(base_url=settings.base_api_url)
109
+ def test_shows_error_on_teams_api_failure(
110
+ logged_in_cli: None, respx_mock: respx.MockRouter, tmp_path: Path
111
+ ) -> None:
112
+ respx_mock.get("/teams/").mock(return_value=Response(500))
113
+
114
+ with changing_dir(tmp_path):
115
+ result = runner.invoke(app, ["link"])
116
+
117
+ assert result.exit_code == 1
118
+ assert "Error fetching teams" in result.output
119
+
120
+
121
+ @pytest.mark.respx(base_url=settings.base_api_url)
122
+ def test_shows_error_on_apps_api_failure(
123
+ logged_in_cli: None, respx_mock: respx.MockRouter, tmp_path: Path
124
+ ) -> None:
125
+ steps = [Keys.ENTER]
126
+
127
+ respx_mock.get("/teams/").mock(
128
+ return_value=Response(
129
+ 200, json={"data": [{"id": "team-1", "name": "My Team", "slug": "my-team"}]}
130
+ )
131
+ )
132
+ respx_mock.get("/apps/", params={"team_id": "team-1"}).mock(
133
+ return_value=Response(500)
134
+ )
135
+
136
+ with (
137
+ changing_dir(tmp_path),
138
+ patch("rich_toolkit.container.getchar") as mock_getchar,
139
+ ):
140
+ mock_getchar.side_effect = steps
141
+ result = runner.invoke(app, ["link"])
142
+
143
+ assert result.exit_code == 1
144
+ assert "Error fetching apps" in result.output
145
+
146
+
147
+ @pytest.mark.respx(base_url=settings.base_api_url)
148
+ def test_links_with_multiple_teams_and_apps(
149
+ logged_in_cli: None, respx_mock: respx.MockRouter, tmp_path: Path
150
+ ) -> None:
151
+ steps = [Keys.DOWN_ARROW, Keys.ENTER, Keys.DOWN_ARROW, Keys.ENTER]
152
+
153
+ respx_mock.get("/teams/").mock(
154
+ return_value=Response(
155
+ 200,
156
+ json={
157
+ "data": [
158
+ {"id": "team-1", "name": "Team One", "slug": "team-one"},
159
+ {"id": "team-2", "name": "Team Two", "slug": "team-two"},
160
+ ]
161
+ },
162
+ )
163
+ )
164
+ respx_mock.get("/apps/", params={"team_id": "team-2"}).mock(
165
+ return_value=Response(
166
+ 200,
167
+ json={
168
+ "data": [
169
+ {"id": "app-1", "slug": "first-app"},
170
+ {"id": "app-2", "slug": "second-app"},
171
+ ]
172
+ },
173
+ )
174
+ )
175
+
176
+ with (
177
+ changing_dir(tmp_path),
178
+ patch("rich_toolkit.container.getchar") as mock_getchar,
179
+ ):
180
+ mock_getchar.side_effect = steps
181
+ result = runner.invoke(app, ["link"])
182
+
183
+ assert result.exit_code == 0
184
+ assert "Successfully linked to app" in result.output
185
+ assert "second-app" in result.output
186
+
187
+ config_path = tmp_path / ".fastapicloud" / "cloud.json"
188
+ assert config_path.exists()
189
+ config = AppConfig.model_validate_json(config_path.read_text())
190
+ assert config.app_id == "app-2"
191
+ assert config.team_id == "team-2"
@@ -19,6 +19,11 @@ from fastapi_cloud_cli.commands.deploy import DeploymentStatus, _should_exclude_
19
19
  Path(".venv"),
20
20
  Path("__pycache__"),
21
21
  Path("module.pyc"),
22
+ Path("/project/.env"),
23
+ Path("/project/.env.local"),
24
+ Path("/project/.env.production"),
25
+ Path(".env"),
26
+ Path(".env.development"),
22
27
  ],
23
28
  )
24
29
  def test_excludes_paths(path: Path) -> None:
@@ -37,6 +42,8 @@ def test_excludes_paths(path: Path) -> None:
37
42
  Path("/project/src/module.pyx"), # similar to .pyc but different
38
43
  Path("/project/config.json"),
39
44
  Path("/project/README.md"),
45
+ Path("/project/.envrc"), # not a .env file
46
+ Path("/project/env.py"), # not a .env file
40
47
  ],
41
48
  )
42
49
  def test_includes_paths(path: Path) -> None:
@@ -1 +0,0 @@
1
- __version__ = "0.10.1"