Skip to content

Commit c6f4c70

Browse files
committed
ENG-9522: Fix CLI: Lists projects as "Unknown"
1 parent 3c5b9ce commit c6f4c70

4 files changed

Lines changed: 421 additions & 56 deletions

File tree

packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,6 +1078,20 @@ def select_project(project: str, token: str | None = None) -> str:
10781078
return f"{project} is now selected."
10791079

10801080

1081+
def normalize_project_id(value: Any) -> str | None:
1082+
"""Normalize a project ID value, treating empty/whitespace strings and non-strings as None.
1083+
1084+
Args:
1085+
value: The raw project ID value from config, CLI args, or hosting.json.
1086+
1087+
Returns:
1088+
The stripped project ID, or None if the value is missing or blank.
1089+
"""
1090+
if isinstance(value, str) and value.strip():
1091+
return value.strip()
1092+
return None
1093+
1094+
10811095
def get_selected_project() -> str | None:
10821096
"""Retrieve the currently selected project ID.
10831097
@@ -1088,10 +1102,10 @@ def get_selected_project() -> str | None:
10881102
try:
10891103
with constants.Hosting.HOSTING_JSON.open() as config_file:
10901104
hosting_config = json.load(config_file)
1091-
return hosting_config.get("project")
1105+
return normalize_project_id(hosting_config.get("project"))
10921106
except Exception as ex:
10931107
console.debug(
1094-
f"Unable to fetch token from {constants.Hosting.HOSTING_JSON} due to: {ex}"
1108+
f"Unable to read selected project from {constants.Hosting.HOSTING_JSON} due to: {ex}"
10951109
)
10961110
return None
10971111

packages/reflex-hosting-cli/src/reflex_cli/v2/cli.py

Lines changed: 41 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -179,19 +179,24 @@ def deploy(
179179
if not description:
180180
description = config.get("description", None)
181181

182-
# resolve the project id from the project name.
182+
project_id = hosting.normalize_project_id(project_id)
183+
183184
if project_name and not project_id:
184185
result = hosting.search_project(
185186
project_name, client=authenticated_client, interactive=interactive
186187
)
187-
project_id = result.get("id") if result else None
188+
project_id = hosting.normalize_project_id(result.get("id")) if result else None
189+
190+
selected_project_id = hosting.get_selected_project()
188191

192+
validated_project: dict[str, Any] | None = None
189193
try:
190-
# check if provided project exists.
194+
if not project_id:
195+
project_id = selected_project_id
191196
if project_id:
192-
hosting.get_project(project_id, client=authenticated_client)
193-
else:
194-
project_id = hosting.get_selected_project()
197+
validated_project = hosting.get_project(
198+
project_id, client=authenticated_client
199+
)
195200
except httpx.HTTPStatusError as ex:
196201
try:
197202
console.error(ex.response.json().get("detail"))
@@ -210,7 +215,7 @@ def deploy(
210215
if app_name and not app_id:
211216
search_project_id = project_id
212217
if not interactive and not project and not search_project_id:
213-
search_project_id = hosting.get_selected_project()
218+
search_project_id = selected_project_id
214219
elif interactive and not project:
215220
search_project_id = None
216221

@@ -230,16 +235,13 @@ def deploy(
230235
raise click.exceptions.Exit(1) from ex
231236

232237
if app and interactive and not project and not app_id:
233-
default_project_id = hosting.get_selected_project()
238+
default_project_id = selected_project_id
234239
app_project_id = app.get("project_id")
235240

236241
if app_project_id and (
237242
not default_project_id or app_project_id != default_project_id
238243
):
239-
app_project = hosting.get_project(
240-
app_project_id, client=authenticated_client
241-
)
242-
app_project_name = app_project.get("name", "Unknown")
244+
app_project_name = (app.get("project") or {}).get("name") or app_project_id
243245
if (
244246
console.ask(
245247
f"Deploy to app '{app['name']}' in project '{app_project_name}'?",
@@ -262,55 +264,40 @@ def deploy(
262264
)
263265
== "y"
264266
):
265-
# Check if we need confirmation for deploying to non-default project
266267
if not project:
267-
default_project_id = hosting.get_selected_project()
268-
if not default_project_id:
269-
try:
270-
if project_id:
271-
target_project = hosting.get_project(
272-
project_id, client=authenticated_client
273-
)
274-
project_name = target_project.get("name", "Unknown")
275-
else:
276-
token = hosting.get_existing_access_token()
277-
default_project_id = hosting.get_default_project(
278-
authenticated_client
268+
needs_confirmation = not selected_project_id or (
269+
project_id and project_id != selected_project_id
270+
)
271+
if needs_confirmation:
272+
if project_id:
273+
project_display_name = (
274+
(
275+
validated_project.get("name")
276+
if validated_project
277+
else None
279278
)
280-
if default_project_id:
281-
default_project = hosting.get_project(
282-
default_project_id, client=authenticated_client
283-
)
284-
project_name = default_project.get(
285-
"name", "Default Project"
286-
)
287-
else:
288-
project_name = "Default Project"
289-
except Exception:
290-
project_name = "Unknown"
291-
292-
if (
293-
console.ask(
294-
f"Create and deploy app '{app_name}' in project '{project_name}'?",
295-
choices=["y", "n"],
296-
default="y",
279+
or project_name
280+
or project_id
297281
)
298-
!= "y"
299-
):
300-
console.info("Deployment cancelled.")
301-
raise click.exceptions.Exit(0)
302-
elif project_id and project_id != default_project_id:
303-
try:
304-
target_project = hosting.get_project(
305-
project_id, client=authenticated_client
282+
else:
283+
project_display_name = "your default project"
284+
fallback_project_id = hosting.get_default_project(
285+
authenticated_client
306286
)
307-
project_name = target_project.get("name", "Unknown")
308-
except Exception:
309-
project_name = "Unknown"
287+
if fallback_project_id:
288+
try:
289+
fallback_project = hosting.get_project(
290+
fallback_project_id, client=authenticated_client
291+
)
292+
project_display_name = (
293+
fallback_project.get("name") or project_display_name
294+
)
295+
except Exception:
296+
pass
310297

311298
if (
312299
console.ask(
313-
f"Create and deploy app '{app_name}' in project '{project_name}'?",
300+
f"Create and deploy app '{app_name}' in project '{project_display_name}'?",
314301
choices=["y", "n"],
315302
default="y",
316303
)

tests/units/reflex_cli/utils/test_hosting.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
delete_token_from_config,
1414
get_authenticated_client,
1515
get_existing_access_token,
16+
get_selected_project,
17+
normalize_project_id,
1618
save_token_to_config,
1719
)
1820

@@ -177,3 +179,39 @@ def test_scale_params_as_json_is_pure_when_type_is_unspecified():
177179

178180
assert scale_params.type is None
179181
assert first == second == {"type": ScaleType.REGION.value, "regions": {}}
182+
183+
184+
@pytest.mark.parametrize(
185+
"config_content, expected",
186+
[
187+
('{"project": "abc-uuid"}', "abc-uuid"),
188+
('{"project": ""}', None),
189+
('{"project": " "}', None),
190+
('{"project": null}', None),
191+
('{"project": 123}', None),
192+
('{"project": []}', None),
193+
("{}", None),
194+
],
195+
)
196+
def test_get_selected_project_normalizes_empty_to_none(
197+
mocker: MockerFixture, config_content: str, expected: str | None
198+
):
199+
mocker.patch("pathlib.Path.open", mock_open(read_data=config_content))
200+
assert get_selected_project() == expected
201+
202+
203+
@pytest.mark.parametrize(
204+
"value, expected",
205+
[
206+
("abc-uuid", "abc-uuid"),
207+
(" abc-uuid ", "abc-uuid"),
208+
("", None),
209+
(" ", None),
210+
(None, None),
211+
(123, None),
212+
([], None),
213+
({}, None),
214+
],
215+
)
216+
def test_normalize_project_id(value: object, expected: str | None):
217+
assert normalize_project_id(value) == expected

0 commit comments

Comments
 (0)