Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,13 @@ jobs:
- name: Install SemaPact Databricks extra from project metadata
run: uv pip install --python .venv/bin/python -e ".[databricks]" pytest

- name: Verify Databricks SDK and existing Unity import boundaries
- name: Verify Databricks SDK, client factory, and existing Unity import boundaries
run: >-
.venv/bin/python -c
"from databricks.sdk import WorkspaceClient; from databricks.sdk.service.catalog import TableInfo; from semapact.importers.unity_importer import import_unity_contract; print(WorkspaceClient.__name__, TableInfo.__name__, import_unity_contract.__name__)"
"import inspect; from databricks.sdk import WorkspaceClient; from databricks.sdk.service.catalog import TableInfo; from semapact.importers.unity_importer import import_unity_contract; from semapact.platforms.databricks import create_databricks_workspace_client; params = inspect.signature(WorkspaceClient).parameters; assert {'host', 'token', 'profile'} <= set(params); print(WorkspaceClient.__name__, TableInfo.__name__, import_unity_contract.__name__, create_databricks_workspace_client.__name__)"

- name: Run Databricks observation tests with official SDK installed
run: .venv/bin/python -m pytest tests/test_observation_databricks.py
- name: Run Databricks platform-boundary tests with official SDK installed
run: .venv/bin/python -m pytest tests/test_databricks_client.py tests/test_observation_databricks.py

coverage:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions semapact/platforms/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""External platform access boundaries for SemaPact."""
5 changes: 5 additions & 0 deletions semapact/platforms/databricks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Databricks platform-access helpers."""

from semapact.platforms.databricks.client import create_databricks_workspace_client

__all__ = ["create_databricks_workspace_client"]
68 changes: 68 additions & 0 deletions semapact/platforms/databricks/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Databricks authenticated-client construction boundary.

This module owns construction of an initialized Databricks ``WorkspaceClient``.
Downstream platform capabilities such as observation consume the resulting
client and remain independent from the authentication mechanism used to create
it.

SemaPact forwards only the connection/authentication hints supplied by the
caller. The Databricks SDK remains responsible for selecting and validating the
authentication mechanism, including its default/unified authentication chain.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from databricks.sdk import WorkspaceClient


def create_databricks_workspace_client(
*,
workspace_url: str | None = None,
token: str | None = None,
profile: str | None = None,
) -> WorkspaceClient:
"""Create a Databricks SDK client from the auth hints the caller has.

SemaPact does not choose an authentication provider. Non-empty values are
forwarded to ``WorkspaceClient`` and omitted values are left for the SDK to
resolve from its standard configuration/authentication chain. Calling this
function with no arguments is therefore equivalent to ``WorkspaceClient()``.

The function performs no credential logging or serialization.
"""
kwargs: dict[str, str] = {}

host = _clean_optional(workspace_url)
if host:
kwargs["host"] = host.rstrip("/")

if token and token.strip():
kwargs["token"] = token

selected_profile = _clean_optional(profile)
if selected_profile:
kwargs["profile"] = selected_profile

workspace_client_cls = _load_workspace_client_class()
return workspace_client_cls(**kwargs)


def _clean_optional(value: str | None) -> str | None:
if value is None:
return None
cleaned = value.strip()
return cleaned or None


def _load_workspace_client_class() -> Any:
"""Load the optional Databricks SDK only when client construction is used."""
try:
from databricks.sdk import WorkspaceClient
except ImportError as exc:
raise RuntimeError(
'Databricks support requires the optional extra: pip install "semapact[databricks]"'
) from exc
return WorkspaceClient
75 changes: 75 additions & 0 deletions tests/test_databricks_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from __future__ import annotations

import pytest

from semapact.platforms.databricks import client as databricks_client
from semapact.platforms.databricks.client import create_databricks_workspace_client


class _FakeWorkspaceClient:
def __init__(self, **kwargs: str) -> None:
self.kwargs = kwargs


def _use_fake_workspace_client(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
databricks_client,
"_load_workspace_client_class",
lambda: _FakeWorkspaceClient,
)


def test_create_databricks_workspace_client_forwards_available_hints(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_use_fake_workspace_client(monkeypatch)

client = create_databricks_workspace_client(
workspace_url=" https://adb.example/ ",
token="secret-token",
profile=" prod ",
)

assert isinstance(client, _FakeWorkspaceClient)
assert client.kwargs == {
"host": "https://adb.example",
"token": "secret-token",
"profile": "prod",
}


@pytest.mark.parametrize(
("kwargs", "expected"),
[
({}, {}),
({"workspace_url": "https://adb.example/"}, {"host": "https://adb.example"}),
({"token": "secret-token"}, {"token": "secret-token"}),
({"profile": "prod"}, {"profile": "prod"}),
],
)
def test_create_databricks_workspace_client_leaves_missing_auth_to_sdk(
monkeypatch: pytest.MonkeyPatch,
kwargs: dict[str, str],
expected: dict[str, str],
) -> None:
_use_fake_workspace_client(monkeypatch)

client = create_databricks_workspace_client(**kwargs)

assert isinstance(client, _FakeWorkspaceClient)
assert client.kwargs == expected


def test_create_databricks_workspace_client_omits_blank_hints(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_use_fake_workspace_client(monkeypatch)

client = create_databricks_workspace_client(
workspace_url=" ",
token=" ",
profile=" ",
)

assert isinstance(client, _FakeWorkspaceClient)
assert client.kwargs == {}
Loading