Skip to content

Commit 21f11df

Browse files
authored
ENG-9522: Fix CLI: Lists projects as "Unknown" (#6530)
* ENG-9522: Fix CLI: Lists projects as "Unknown" * update
1 parent 3c5b9ce commit 21f11df

4 files changed

Lines changed: 421 additions & 58 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 & 56 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"))
@@ -209,9 +214,7 @@ def deploy(
209214
try:
210215
if app_name and not app_id:
211216
search_project_id = project_id
212-
if not interactive and not project and not search_project_id:
213-
search_project_id = hosting.get_selected_project()
214-
elif interactive and not project:
217+
if interactive and not project:
215218
search_project_id = None
216219

217220
app = hosting.search_app(
@@ -230,16 +233,13 @@ def deploy(
230233
raise click.exceptions.Exit(1) from ex
231234

232235
if app and interactive and not project and not app_id:
233-
default_project_id = hosting.get_selected_project()
236+
default_project_id = selected_project_id
234237
app_project_id = app.get("project_id")
235238

236239
if app_project_id and (
237240
not default_project_id or app_project_id != default_project_id
238241
):
239-
app_project = hosting.get_project(
240-
app_project_id, client=authenticated_client
241-
)
242-
app_project_name = app_project.get("name", "Unknown")
242+
app_project_name = (app.get("project") or {}).get("name") or app_project_id
243243
if (
244244
console.ask(
245245
f"Deploy to app '{app['name']}' in project '{app_project_name}'?",
@@ -262,55 +262,40 @@ def deploy(
262262
)
263263
== "y"
264264
):
265-
# Check if we need confirmation for deploying to non-default project
266265
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
266+
needs_confirmation = not selected_project_id or (
267+
project_id and project_id != selected_project_id
268+
)
269+
if needs_confirmation:
270+
if project_id:
271+
project_display_name = (
272+
(
273+
validated_project.get("name")
274+
if validated_project
275+
else None
279276
)
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",
277+
or project_name
278+
or project_id
297279
)
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
280+
else:
281+
project_display_name = "your default project"
282+
fallback_project_id = hosting.get_default_project(
283+
authenticated_client
306284
)
307-
project_name = target_project.get("name", "Unknown")
308-
except Exception:
309-
project_name = "Unknown"
285+
if fallback_project_id:
286+
try:
287+
fallback_project = hosting.get_project(
288+
fallback_project_id, client=authenticated_client
289+
)
290+
project_display_name = (
291+
fallback_project.get("name") or project_display_name
292+
)
293+
except Exception:
294+
pass
310295

311296
if (
312297
console.ask(
313-
f"Create and deploy app '{app_name}' in project '{project_name}'?",
298+
f"Create and deploy app '{app_name}' in project '{project_display_name}'?",
314299
choices=["y", "n"],
315300
default="y",
316301
)

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)