Skip to content

Commit f6e457f

Browse files
committed
🐛 Always load settings lazily
1 parent 660072b commit f6e457f

File tree

9 files changed

+65
-58
lines changed

9 files changed

+65
-58
lines changed

src/fastapi_cloud_cli/commands/login.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import typer
77
from pydantic import BaseModel
88

9-
from fastapi_cloud_cli.config import settings
9+
from fastapi_cloud_cli.config import Settings
1010
from fastapi_cloud_cli.utils.api import APIClient
1111
from fastapi_cloud_cli.utils.auth import AuthConfig, write_auth_config
1212
from fastapi_cloud_cli.utils.cli import get_rich_toolkit, handle_http_errors
@@ -29,6 +29,8 @@ class TokenResponse(BaseModel):
2929
def _start_device_authorization(
3030
client: httpx.Client,
3131
) -> AuthorizationData:
32+
settings = Settings.get()
33+
3234
response = client.post(
3335
"/login/device/authorization", data={"client_id": settings.client_id}
3436
)
@@ -39,6 +41,8 @@ def _start_device_authorization(
3941

4042

4143
def _fetch_access_token(client: httpx.Client, device_code: str, interval: int) -> str:
44+
settings = Settings.get()
45+
4246
while True:
4347
response = client.post(
4448
"/login/device/token",

src/fastapi_cloud_cli/config.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@ def from_user_settings(cls, config_path: Path) -> "Settings":
2121

2222
return cls(**user_settings)
2323

24-
25-
settings = Settings.from_user_settings(get_cli_config_path())
24+
@classmethod
25+
def get(cls) -> "Settings":
26+
return cls.from_user_settings(get_cli_config_path())

src/fastapi_cloud_cli/utils/api.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import httpx
22

33
from fastapi_cloud_cli import __version__
4-
from fastapi_cloud_cli.config import settings
4+
from fastapi_cloud_cli.config import Settings
55
from fastapi_cloud_cli.utils.auth import get_auth_token
66

77

88
class APIClient(httpx.Client):
99
def __init__(self) -> None:
10+
settings = Settings.get()
11+
1012
token = get_auth_token()
1113

1214
super().__init__(

tests/test_cli_deploy.py

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from typer.testing import CliRunner
1212

1313
from fastapi_cloud_cli.cli import app
14-
from fastapi_cloud_cli.config import settings
14+
from fastapi_cloud_cli.config import Settings
1515
from tests.conftest import ConfiguredApp
1616
from tests.utils import Keys, changing_dir
1717

@@ -58,7 +58,7 @@ def _get_random_deployment(
5858
}
5959

6060

61-
@pytest.mark.respx(base_url=settings.base_api_url)
61+
@pytest.mark.respx(base_url=Settings.base_api_url)
6262
def test_shows_waitlist_form_when_not_logged_in(
6363
logged_out_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
6464
) -> None:
@@ -90,7 +90,7 @@ def test_shows_waitlist_form_when_not_logged_in(
9090
assert "Let's go! Thanks for your interest in FastAPI Cloud! 🚀" in result.output
9191

9292

93-
@pytest.mark.respx(base_url=settings.base_api_url)
93+
@pytest.mark.respx(base_url=Settings.base_api_url)
9494
def test_shows_waitlist_form_when_not_logged_in_longer_flow(
9595
logged_out_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
9696
) -> None:
@@ -162,7 +162,7 @@ def test_asks_to_setup_the_app(logged_in_cli: None, tmp_path: Path) -> None:
162162
assert "Setup and deploy" in result.output
163163

164164

165-
@pytest.mark.respx(base_url=settings.base_api_url)
165+
@pytest.mark.respx(base_url=Settings.base_api_url)
166166
def test_shows_error_when_trying_to_get_teams(
167167
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
168168
) -> None:
@@ -180,7 +180,7 @@ def test_shows_error_when_trying_to_get_teams(
180180
assert "Error fetching teams. Please try again later" in result.output
181181

182182

183-
@pytest.mark.respx(base_url=settings.base_api_url)
183+
@pytest.mark.respx(base_url=Settings.base_api_url)
184184
def test_handles_invalid_auth(
185185
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
186186
) -> None:
@@ -198,7 +198,7 @@ def test_handles_invalid_auth(
198198
assert "The specified token is not valid" in result.output
199199

200200

201-
@pytest.mark.respx(base_url=settings.base_api_url)
201+
@pytest.mark.respx(base_url=Settings.base_api_url)
202202
def test_shows_teams(
203203
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
204204
) -> None:
@@ -225,7 +225,7 @@ def test_shows_teams(
225225
assert team_2["name"] in result.output
226226

227227

228-
@pytest.mark.respx(base_url=settings.base_api_url)
228+
@pytest.mark.respx(base_url=Settings.base_api_url)
229229
def test_asks_for_app_name_after_team(
230230
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
231231
) -> None:
@@ -248,7 +248,7 @@ def test_asks_for_app_name_after_team(
248248
assert "What's your app name?" in result.output
249249

250250

251-
@pytest.mark.respx(base_url=settings.base_api_url)
251+
@pytest.mark.respx(base_url=Settings.base_api_url)
252252
def test_creates_app_on_backend(
253253
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
254254
) -> None:
@@ -277,7 +277,7 @@ def test_creates_app_on_backend(
277277
assert "App created successfully" in result.output
278278

279279

280-
@pytest.mark.respx(base_url=settings.base_api_url)
280+
@pytest.mark.respx(base_url=Settings.base_api_url)
281281
def test_uses_existing_app(
282282
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
283283
) -> None:
@@ -304,7 +304,7 @@ def test_uses_existing_app(
304304
assert app_data["slug"] in result.output
305305

306306

307-
@pytest.mark.respx(base_url=settings.base_api_url)
307+
@pytest.mark.respx(base_url=Settings.base_api_url)
308308
def test_exits_successfully_when_deployment_is_done(
309309
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
310310
) -> None:
@@ -376,7 +376,7 @@ def test_exits_successfully_when_deployment_is_done(
376376
# TODO: show a message when the deployment is done (based on the status)
377377

378378

379-
@pytest.mark.respx(base_url=settings.base_api_url)
379+
@pytest.mark.respx(base_url=Settings.base_api_url)
380380
def test_exits_successfully_when_deployment_is_done_when_app_is_configured(
381381
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
382382
) -> None:
@@ -438,7 +438,7 @@ def test_exits_successfully_when_deployment_is_done_when_app_is_configured(
438438
assert deployment_data["url"] in result.output
439439

440440

441-
@pytest.mark.respx(base_url=settings.base_api_url)
441+
@pytest.mark.respx(base_url=Settings.base_api_url)
442442
def test_exits_with_error_when_deployment_fails_to_build(
443443
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
444444
) -> None:
@@ -493,7 +493,7 @@ def test_exits_with_error_when_deployment_fails_to_build(
493493
assert deployment_data["dashboard_url"] in result.output
494494

495495

496-
@pytest.mark.respx(base_url=settings.base_api_url)
496+
@pytest.mark.respx(base_url=Settings.base_api_url)
497497
def test_shows_error_when_deployment_build_fails(
498498
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
499499
) -> None:
@@ -547,7 +547,7 @@ def test_shows_error_when_deployment_build_fails(
547547
assert result.exit_code == 1
548548

549549

550-
@pytest.mark.respx(base_url=settings.base_api_url)
550+
@pytest.mark.respx(base_url=Settings.base_api_url)
551551
def test_shows_error_when_app_does_not_exist(
552552
logged_in_cli: None, configured_app: ConfiguredApp, respx_mock: respx.MockRouter
553553
) -> None:
@@ -620,7 +620,7 @@ def _deploy_without_waiting(respx_mock: respx.MockRouter, tmp_path: Path) -> Res
620620
return runner.invoke(app, ["deploy", "--no-wait"])
621621

622622

623-
@pytest.mark.respx(base_url=settings.base_api_url)
623+
@pytest.mark.respx(base_url=Settings.base_api_url)
624624
def test_can_skip_waiting(
625625
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
626626
) -> None:
@@ -631,7 +631,7 @@ def test_can_skip_waiting(
631631
assert "Check the status of your deployment at" in result.output
632632

633633

634-
@pytest.mark.respx(base_url=settings.base_api_url)
634+
@pytest.mark.respx(base_url=Settings.base_api_url)
635635
def test_creates_config_folder_and_creates_git_ignore(
636636
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
637637
) -> None:
@@ -642,7 +642,7 @@ def test_creates_config_folder_and_creates_git_ignore(
642642
assert (tmp_path / ".fastapicloud" / ".gitignore").read_text() == "*"
643643

644644

645-
@pytest.mark.respx(base_url=settings.base_api_url)
645+
@pytest.mark.respx(base_url=Settings.base_api_url)
646646
def test_does_not_duplicate_entry_in_git_ignore(
647647
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
648648
) -> None:
@@ -654,7 +654,7 @@ def test_does_not_duplicate_entry_in_git_ignore(
654654
assert git_ignore_path.read_text() == ".fastapicloud\n"
655655

656656

657-
@pytest.mark.respx(base_url=settings.base_api_url)
657+
@pytest.mark.respx(base_url=Settings.base_api_url)
658658
def test_creates_environment_variables_during_app_setup(
659659
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
660660
) -> None:
@@ -696,7 +696,7 @@ def test_creates_environment_variables_during_app_setup(
696696
assert "Environment variables set up successfully!" in result.output
697697

698698

699-
@pytest.mark.respx(base_url=settings.base_api_url)
699+
@pytest.mark.respx(base_url=Settings.base_api_url)
700700
def test_rejects_invalid_environment_variable_names(
701701
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
702702
) -> None:
@@ -741,7 +741,7 @@ def test_rejects_invalid_environment_variable_names(
741741
assert "Environment variables set up successfully!" in result.output
742742

743743

744-
@pytest.mark.respx(base_url=settings.base_api_url)
744+
@pytest.mark.respx(base_url=Settings.base_api_url)
745745
def test_shows_error_for_invalid_waitlist_form_data(
746746
logged_out_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
747747
) -> None:
@@ -768,7 +768,7 @@ def test_shows_error_for_invalid_waitlist_form_data(
768768
assert "Invalid form data. Please try again." in result.output
769769

770770

771-
@pytest.mark.respx(base_url=settings.base_api_url)
771+
@pytest.mark.respx(base_url=Settings.base_api_url)
772772
def test_shows_no_apps_found_message_when_team_has_no_apps(
773773
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
774774
) -> None:

tests/test_cli_login.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@
88
from typer.testing import CliRunner
99

1010
from fastapi_cloud_cli.cli import app
11-
from fastapi_cloud_cli.config import settings
11+
from fastapi_cloud_cli.config import Settings
1212

1313
runner = CliRunner()
1414

1515
assets_path = Path(__file__).parent / "assets"
1616

1717

18-
@pytest.mark.respx(base_url=settings.base_api_url)
18+
@pytest.mark.respx(base_url=Settings.base_api_url)
1919
def test_shows_a_message_if_something_is_wrong(respx_mock: respx.MockRouter) -> None:
2020
with patch("fastapi_cloud_cli.commands.login.typer.launch") as mock_open:
2121
respx_mock.post(
22-
"/login/device/authorization", data={"client_id": settings.client_id}
22+
"/login/device/authorization", data={"client_id": Settings.client_id}
2323
).mock(return_value=Response(500))
2424

2525
result = runner.invoke(app, ["login"])
@@ -33,11 +33,11 @@ def test_shows_a_message_if_something_is_wrong(respx_mock: respx.MockRouter) ->
3333
assert not mock_open.called
3434

3535

36-
@pytest.mark.respx(base_url=settings.base_api_url)
36+
@pytest.mark.respx(base_url=Settings.base_api_url)
3737
def test_full_login(respx_mock: respx.MockRouter, temp_auth_config: Path) -> None:
3838
with patch("fastapi_cloud_cli.commands.login.typer.launch") as mock_open:
3939
respx_mock.post(
40-
"/login/device/authorization", data={"client_id": settings.client_id}
40+
"/login/device/authorization", data={"client_id": Settings.client_id}
4141
).mock(
4242
return_value=Response(
4343
200,
@@ -53,7 +53,7 @@ def test_full_login(respx_mock: respx.MockRouter, temp_auth_config: Path) -> Non
5353
"/login/device/token",
5454
data={
5555
"device_code": "5678",
56-
"client_id": settings.client_id,
56+
"client_id": Settings.client_id,
5757
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
5858
},
5959
).mock(return_value=Response(200, json={"access_token": "test_token_1234"}))
@@ -73,7 +73,7 @@ def test_full_login(respx_mock: respx.MockRouter, temp_auth_config: Path) -> Non
7373
assert '"access_token":"test_token_1234"' in temp_auth_config.read_text()
7474

7575

76-
@pytest.mark.respx(base_url=settings.base_api_url)
76+
@pytest.mark.respx(base_url=Settings.base_api_url)
7777
def test_fetch_access_token_success_immediately(respx_mock: respx.MockRouter) -> None:
7878
from fastapi_cloud_cli.commands.login import _fetch_access_token
7979
from fastapi_cloud_cli.utils.api import APIClient
@@ -82,7 +82,7 @@ def test_fetch_access_token_success_immediately(respx_mock: respx.MockRouter) ->
8282
"/login/device/token",
8383
data={
8484
"device_code": "test_device_code",
85-
"client_id": settings.client_id,
85+
"client_id": Settings.client_id,
8686
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
8787
},
8888
).mock(return_value=Response(200, json={"access_token": "test_token_success"}))
@@ -93,7 +93,7 @@ def test_fetch_access_token_success_immediately(respx_mock: respx.MockRouter) ->
9393
assert access_token == "test_token_success"
9494

9595

96-
@pytest.mark.respx(base_url=settings.base_api_url)
96+
@pytest.mark.respx(base_url=Settings.base_api_url)
9797
def test_fetch_access_token_authorization_pending_then_success(
9898
respx_mock: respx.MockRouter,
9999
) -> None:
@@ -105,7 +105,7 @@ def test_fetch_access_token_authorization_pending_then_success(
105105
"/login/device/token",
106106
data={
107107
"device_code": "test_device_code",
108-
"client_id": settings.client_id,
108+
"client_id": Settings.client_id,
109109
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
110110
},
111111
).mock(
@@ -123,7 +123,7 @@ def test_fetch_access_token_authorization_pending_then_success(
123123
mock_sleep.assert_called_once_with(3)
124124

125125

126-
@pytest.mark.respx(base_url=settings.base_api_url)
126+
@pytest.mark.respx(base_url=Settings.base_api_url)
127127
def test_fetch_access_token_handles_400_error_not_authorization_pending(
128128
respx_mock: respx.MockRouter,
129129
) -> None:
@@ -134,7 +134,7 @@ def test_fetch_access_token_handles_400_error_not_authorization_pending(
134134
"/login/device/token",
135135
data={
136136
"device_code": "test_device_code",
137-
"client_id": settings.client_id,
137+
"client_id": Settings.client_id,
138138
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
139139
},
140140
).mock(return_value=Response(400, json={"error": "access_denied"}))
@@ -144,7 +144,7 @@ def test_fetch_access_token_handles_400_error_not_authorization_pending(
144144
_fetch_access_token(client, "test_device_code", 5)
145145

146146

147-
@pytest.mark.respx(base_url=settings.base_api_url)
147+
@pytest.mark.respx(base_url=Settings.base_api_url)
148148
def test_fetch_access_token_handles_500_error(respx_mock: respx.MockRouter) -> None:
149149
from fastapi_cloud_cli.commands.login import _fetch_access_token
150150
from fastapi_cloud_cli.utils.api import APIClient
@@ -153,7 +153,7 @@ def test_fetch_access_token_handles_500_error(respx_mock: respx.MockRouter) -> N
153153
"/login/device/token",
154154
data={
155155
"device_code": "test_device_code",
156-
"client_id": settings.client_id,
156+
"client_id": Settings.client_id,
157157
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
158158
},
159159
).mock(return_value=Response(500))

0 commit comments

Comments
 (0)