Skip to content

Commit ec9ee93

Browse files
author
Alon Yeshurun
committed
Add fab host app version
1 parent 756dd40 commit ec9ee93

File tree

3 files changed

+123
-41
lines changed

3 files changed

+123
-41
lines changed

src/fabric_cli/client/fab_api_client.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -282,22 +282,29 @@ def _handle_successful_response(args: Namespace, response: ApiResponse) -> ApiRe
282282

283283

284284
def _build_user_agent(ctxt_cmd: str) -> str:
285-
"""Build the User-Agent header for API requests, including context command and HostApp if applicable."""
286-
base_user_agent = f"{fab_constant.API_USER_AGENT}/{fab_constant.FAB_VERSION} ({ctxt_cmd}; {platform.system()}; {platform.machine()}; {platform.release()})"
287-
host_app_suffix = _get_host_app_suffix()
288-
return f"{base_user_agent}{host_app_suffix}"
285+
"""Build the User-Agent header for API requests.
286+
287+
Example:
288+
ms-fabric-cli/1.0.0 (create; Windows/10; Python/3.10.2) host-app/ado/2.0.0
289+
"""
290+
user_agent = f"{fab_constant.API_USER_AGENT}/{fab_constant.FAB_VERSION} ({ctxt_cmd}; {platform.system()}/{platform.release()}; Python/{platform.python_version()})"
291+
host_app = _get_host_app()
292+
if host_app:
293+
user_agent += host_app
294+
295+
return user_agent
289296

290297

291-
def _get_host_app_suffix() -> str:
292-
"""Get the HostApp suffix for the User-Agent header based on environment variable.
298+
def _get_host_app() -> str:
299+
"""Get the HostApp suffix for the User-Agent header based on environment variables.
293300
294301
Returns an empty string if the environment variable is not set or has an invalid value.
295302
"""
296303
_host_app_in_env = os.environ.get(fab_constant.FAB_HOST_APP_ENV_VAR)
297304
if not _host_app_in_env:
298305
return ""
299306

300-
host_app = next(
307+
host_app_name = next(
301308
(
302309
allowed_app
303310
for allowed_app in fab_constant.ALLOWED_FAB_HOST_APP_VALUES
@@ -306,10 +313,20 @@ def _get_host_app_suffix() -> str:
306313
None,
307314
)
308315

309-
if not host_app:
316+
if not host_app_name:
310317
return ""
311318

312-
return f"; HostApp/{host_app}"
319+
host_app = f" host-app/{host_app_name.lower()}"
320+
321+
# Check for optional version
322+
host_app_version = os.environ.get(fab_constant.FAB_HOST_APP_VERSION_ENV_VAR)
323+
324+
# validate host_app_version format is a valid version (e.g., 1.0.0)
325+
if host_app_version and re.match(
326+
r"^\d+(\.\d+){0,2}(-[a-zA-Z0-9\.-]+)?$", host_app_version
327+
):
328+
host_app += f"/{host_app_version}"
329+
return host_app
313330

314331

315332
def _print_response_details(response: ApiResponse) -> None:

src/fabric_cli/core/fab_constant.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@
6464
IDENTITY_TYPE: ["user", "service_principal", "managed_identity"],
6565
}
6666

67-
FAB_HOST_APP_ENV_VAR = "FABRIC_CLI_HOST_APP"
67+
FAB_HOST_APP_ENV_VAR = "FAB_HOST_APP"
68+
FAB_HOST_APP_VERSION_ENV_VAR = "FAB_HOST_APP_VERSION"
6869

6970
# Other constants
7071
FAB_CAPACITY_NAME_NONE = "none"

tests/test_core/test_fab_api_client.py

Lines changed: 95 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import pytest
99

1010
from fabric_cli.client.fab_api_client import (
11-
_get_host_app_suffix,
11+
_get_host_app,
1212
_transform_workspace_url_for_private_link_if_needed,
1313
do_request,
1414
)
@@ -311,32 +311,88 @@ def __init__(self):
311311

312312

313313
@pytest.mark.parametrize(
314-
"host_app_env, expected_suffix",
314+
"host_app_env, host_app_version_env, expected_suffix",
315315
[
316-
("VSCode-Extension", "; HostApp/VSCode-Extension"), # Valid, correct case
317-
("vscode-extension", "; HostApp/VSCode-Extension"), # Valid, lower case
318-
("VSCodE-ExTenSion", "; HostApp/VSCode-Extension"), # Valid, mixed case
319316
(
320-
"Azure-DevOps-Pipeline",
321-
"; HostApp/Azure-DevOps-Pipeline",
322-
), # Valid, correct case
317+
"Fabric-AzureDevops-Extension",
318+
None,
319+
" host-app/fabric-azuredevops-extension",
320+
),
323321
(
324-
"azure-devops-pipeline",
325-
"; HostApp/Azure-DevOps-Pipeline",
326-
), # Valid, lower case
327-
("Invalid-App", ""), # Invalid
328-
("", ""), # Empty
329-
(None, ""), # Not set
322+
"Fabric-AzureDevops-Extension",
323+
"1.2.0",
324+
" host-app/fabric-azuredevops-extension/1.2.0",
325+
),
326+
(
327+
"fabric-azuredevops-extension",
328+
"1.2.0",
329+
" host-app/fabric-azuredevops-extension/1.2.0",
330+
),
331+
("Invalid-App", "1.0.0", ""),
332+
("", None, ""),
333+
(None, None, ""),
334+
(
335+
"Fabric-AzureDevops-Extension",
336+
"1.2.0.4", # Invalid format
337+
" host-app/fabric-azuredevops-extension",
338+
),
339+
(
340+
"Fabric-AzureDevops-Extension",
341+
"1.2.a", # Invalid format
342+
" host-app/fabric-azuredevops-extension",
343+
),
344+
(
345+
"Fabric-AzureDevops-Extension",
346+
"a.b.c", # Invalid format
347+
" host-app/fabric-azuredevops-extension",
348+
),
349+
(
350+
"Fabric-AzureDevops-Extension",
351+
"1", # valid format
352+
" host-app/fabric-azuredevops-extension/1",
353+
),
354+
(
355+
"Fabric-AzureDevops-Extension",
356+
"1.2", # valid format
357+
" host-app/fabric-azuredevops-extension/1.2",
358+
),
359+
(
360+
"Fabric-AzureDevops-Extension",
361+
"1.0.0", # valid format
362+
" host-app/fabric-azuredevops-extension/1.0.0",
363+
),
364+
(
365+
"Fabric-AzureDevops-Extension",
366+
"1.0.0-rc.1", # valid format
367+
" host-app/fabric-azuredevops-extension/1.0.0-rc.1",
368+
),
369+
(
370+
"Fabric-AzureDevops-Extension",
371+
"1.0.0-alpha", # valid format
372+
" host-app/fabric-azuredevops-extension/1.0.0-alpha",
373+
),
374+
(
375+
"Fabric-AzureDevops-Extension",
376+
"1.0.0-beta", # valid format
377+
" host-app/fabric-azuredevops-extension/1.0.0-beta",
378+
),
330379
],
331380
)
332-
def test_get_host_app_suffix(host_app_env, expected_suffix, monkeypatch):
333-
"""Test the _get_host_app_suffix helper function."""
381+
def test_get_host_app(host_app_env, host_app_version_env, expected_suffix, monkeypatch):
382+
"""Test the _get_host_app helper function."""
334383
if host_app_env is not None:
335384
monkeypatch.setenv(fab_constant.FAB_HOST_APP_ENV_VAR, host_app_env)
336385
else:
337386
monkeypatch.delenv(fab_constant.FAB_HOST_APP_ENV_VAR, raising=False)
338387

339-
result = _get_host_app_suffix()
388+
if host_app_version_env is not None:
389+
monkeypatch.setenv(
390+
fab_constant.FAB_HOST_APP_VERSION_ENV_VAR, host_app_version_env
391+
)
392+
else:
393+
monkeypatch.delenv(fab_constant.FAB_HOST_APP_VERSION_ENV_VAR, raising=False)
394+
395+
result = _get_host_app()
340396

341397
assert result == expected_suffix
342398

@@ -346,37 +402,38 @@ def setup_default_private_links(mock_fab_set_state_config):
346402
mock_fab_set_state_config(fab_constant.FAB_WS_PRIVATE_LINKS_ENABLED, "true")
347403

348404

405+
@patch("platform.python_version", return_value="3.11.5")
349406
@patch("platform.release", return_value="5.4.0")
350-
@patch("platform.machine", return_value="x86_64")
351407
@patch("platform.system", return_value="Linux")
352408
@patch("requests.Session.request")
353409
@patch("fabric_cli.core.fab_auth.FabAuth")
354410
@patch("fabric_cli.core.fab_context.Context")
355411
@pytest.mark.parametrize(
356-
"host_app_env, expected_suffix",
412+
"host_app_env, host_app_version_env, expected_suffix",
357413
[
358-
(None, ""), # No env var set
359-
("VSCode-Extension", "; HostApp/VSCode-Extension"), # Valid and allowed
414+
(None, None, ""),
360415
(
361-
"Azure-DevOps-Pipeline",
362-
"; HostApp/Azure-DevOps-Pipeline",
363-
), # Valid and allowed
416+
"Fabric-AzureDevops-Extension",
417+
None,
418+
" host-app/fabric-azuredevops-extension",
419+
),
364420
(
365-
"vscode-extension",
366-
"; HostApp/VSCode-Extension",
367-
), # Valid and allowed (case-insensitive)
368-
("Invalid-App", ""), # Invalid and not in allowlist
369-
("", ""), # Empty value
421+
"Fabric-AzureDevops-Extension",
422+
"1.2.0",
423+
" host-app/fabric-azuredevops-extension/1.2.0",
424+
),
425+
("Invalid-App", "1.0.0", ""),
370426
],
371427
)
372428
def test_do_request_user_agent_header(
373429
mock_context,
374430
mock_auth,
375431
mock_request,
376432
mock_system,
377-
mock_machine,
378433
mock_release,
434+
mock_python_version,
379435
host_app_env,
436+
host_app_version_env,
380437
expected_suffix,
381438
monkeypatch,
382439
):
@@ -386,6 +443,13 @@ def test_do_request_user_agent_header(
386443
else:
387444
monkeypatch.delenv(fab_constant.FAB_HOST_APP_ENV_VAR, raising=False)
388445

446+
if host_app_version_env is not None:
447+
monkeypatch.setenv(
448+
fab_constant.FAB_HOST_APP_VERSION_ENV_VAR, host_app_version_env
449+
)
450+
else:
451+
monkeypatch.delenv(fab_constant.FAB_HOST_APP_VERSION_ENV_VAR, raising=False)
452+
389453
# Configure mocks
390454
mock_auth.return_value.get_access_token.return_value = "dummy-token"
391455
mock_context.return_value.command = "test-command"
@@ -418,7 +482,7 @@ class DummyResponse:
418482

419483
base_user_agent = (
420484
f"{fab_constant.API_USER_AGENT}/{fab_constant.FAB_VERSION} "
421-
f"(test-command; Linux; x86_64; 5.4.0)"
485+
f"(test-command; Linux/5.4.0; Python/3.11.5)"
422486
)
423487
expected_user_agent = base_user_agent + expected_suffix
424488

0 commit comments

Comments
 (0)