Skip to content

Commit 144e895

Browse files
committed
adding docstrings, tests
1 parent 54a47c9 commit 144e895

File tree

2 files changed

+154
-0
lines changed

2 files changed

+154
-0
lines changed

src/posit/connect/external/databricks.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,19 @@ def _get_auth_type(local_auth_type: str) -> str:
8282
return POSIT_OAUTH_INTEGRATION_AUTH_TYPE
8383

8484
class PositLocalContentCredentialsProvider:
85+
"""`CredentialsProvider` implementation which provides a fallback for local development using a client credentials flow.
86+
87+
There is an open issue against the Databricks CLI which prevents it from returning service principal access tokens.
88+
https://github.com/databricks/cli/issues/1939
89+
90+
Until the CLI issue is resolved, this CredentialsProvider implements the approach described in the Databricks documentation
91+
for manually generating a workspace-level access token using OAuth M2M authentication. Once it has acquired an access token,
92+
it returns it as a Bearer authorization header like other `CredentialsProvider` implementations.
93+
94+
See Also
95+
--------
96+
* https://docs.databricks.com/en/dev-tools/auth/oauth-m2m.html#manually-generate-a-workspace-level-access-token
97+
"""
8598

8699
def __init__(self, token_endpoint_url: str, client_id: str, client_secret: str):
87100
self._token_endpoint_url = token_endpoint_url
@@ -142,6 +155,73 @@ def __call__(self) -> Dict[str, str]:
142155
return _new_bearer_authorization_header(credentials)
143156

144157
class PositLocalContentCredentialsStrategy(CredentialsStrategy):
158+
"""`CredentialsStrategy` implementation which supports local development using OAuth M2M authentication against databricks.
159+
160+
There is an open issue against the Databricks CLI which prevents it from returning service principal access tokens.
161+
https://github.com/databricks/cli/issues/1939
162+
163+
Until the CLI issue is resolved, this CredentialsStrategy provides a drop-in replacement as a local_strategy that can be used
164+
to develop applications which target Service Account OAuth integrations on Connect.
165+
166+
Examples
167+
--------
168+
In the example below, the PositContentCredentialsStrategy can be initialized anywhere that
169+
the Python process can read environment variables.
170+
171+
CLIENT_ID and CLIENT_SECRET credentials associated with the Databricks Service Principal.
172+
173+
```python
174+
import os
175+
176+
from posit.connect.external.databricks import PositContentCredentialsStrategy, PositLocalContentCredentialsStrategy
177+
178+
import pandas as pd
179+
from databricks import sql
180+
from databricks.sdk.core import ApiClient, Config
181+
from databricks.sdk.service.iam import CurrentUserAPI
182+
183+
DATABRICKS_HOST = "<REDACTED>"
184+
DATABRICKS_HOST_URL = f"https://{DATABRICKS_HOST}"
185+
SQL_HTTP_PATH = "<REDACTED>"
186+
TOKEN_ENDPOINT_URL = f"https://{DATABRICKS_HOST}/oidc/v1/token"
187+
188+
CLIENT_ID = "<REDACTED>"
189+
CLIENT_SECRET = "<REDACTED>"
190+
191+
# Rather than relying on the Databricks CLI as a local strategy, we use
192+
# PositLocalContentCredentialsStragtegy as a drop-in replacement.
193+
# Can be replaced with the Databricks CLI implementation when
194+
# https://github.com/databricks/cli/issues/1939 is resolved.
195+
local_strategy = PositLocalContentCredentialsStrategy(
196+
token_endpoint_url=TOKEN_ENDPOINT_URL,
197+
client_id=CLIENT_ID,
198+
client_secret=CLIENT_SECRET,
199+
)
200+
201+
posit_strategy = PositContentCredentialsStrategy(local_strategy=local_strategy)
202+
203+
cfg = Config(host=DATABRICKS_HOST_URL, credentials_strategy=posit_strategy)
204+
205+
databricks_user_info = CurrentUserAPI(ApiClient(cfg)).me()
206+
print(f"Hello, {databricks_user_info.display_name}!")
207+
208+
query = "SELECT * FROM samples.nyctaxi.trips LIMIT 10;"
209+
with sql.connect(
210+
server_hostname=DATABRICKS_HOST,
211+
http_path=SQL_HTTP_PATH,
212+
credentials_provider=posit_strategy.sql_credentials_provider(cfg),
213+
) as connection:
214+
with connection.cursor() as cursor:
215+
cursor.execute(query)
216+
rows = cursor.fetchall()
217+
print(pd.DataFrame([row.asDict() for row in rows]))
218+
```
219+
220+
See Also
221+
--------
222+
* https://docs.databricks.com/en/dev-tools/auth/oauth-m2m.html#manually-generate-a-workspace-level-access-token
223+
"""
224+
145225
def __init__(self, token_endpoint_url: str, client_id: str, client_secret: str):
146226
self._token_endpoint_url = token_endpoint_url
147227
self._client_id = client_id

tests/posit/connect/external/test_databricks.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import base64
2+
13
from typing import Dict
24
from unittest.mock import patch
35

@@ -13,6 +15,8 @@
1315
PositContentCredentialsStrategy,
1416
PositCredentialsProvider,
1517
PositCredentialsStrategy,
18+
PositLocalContentCredentialsProvider,
19+
PositLocalContentCredentialsStrategy,
1620
_get_auth_type,
1721
_new_bearer_authorization_header,
1822
)
@@ -92,6 +96,36 @@ def test_get_auth_type_local(self):
9296
def test_get_auth_type_connect(self):
9397
assert _get_auth_type("local-auth") == POSIT_OAUTH_INTEGRATION_AUTH_TYPE
9498

99+
@responses.activate
100+
def test_local_content_credentials_provider(self):
101+
102+
token_url = "https://my-token/url"
103+
client_id = "client_id"
104+
client_secret = "client_secret_123"
105+
basic_auth = f"{client_id}:{client_secret}"
106+
b64_basic_auth = base64.b64encode(basic_auth.encode('utf-8')).decode('utf-8')
107+
108+
responses.post(
109+
token_url,
110+
match=[
111+
responses.matchers.urlencoded_params_matcher(
112+
{
113+
"grant_type": "client_credentials",
114+
"scope": "all-apis",
115+
},
116+
),
117+
responses.matchers.header_matcher({"Authorization": f"Basic {b64_basic_auth}"})
118+
],
119+
json={
120+
"access_token": "oauth2-m2m-access-token",
121+
"token_type": "Bearer",
122+
"expires_in": 3600,
123+
},
124+
)
125+
126+
cp = PositLocalContentCredentialsProvider(token_url, client_id, client_secret)
127+
assert cp() == {"Authorization": "Bearer oauth2-m2m-access-token"}
128+
95129
@patch.dict("os.environ", {"CONNECT_CONTENT_SESSION_TOKEN": "cit"})
96130
@responses.activate
97131
def test_posit_content_credentials_provider(self):
@@ -111,6 +145,46 @@ def test_posit_credentials_provider(self):
111145
cp = PositCredentialsProvider(client=client, user_session_token="cit")
112146
assert cp() == {"Authorization": "Bearer dynamic-viewer-access-token"}
113147

148+
149+
@responses.activate
150+
def test_local_content_credentials_strategy(self):
151+
152+
token_url = "https://my-token/url"
153+
client_id = "client_id"
154+
client_secret = "client_secret_123"
155+
basic_auth = f"{client_id}:{client_secret}"
156+
b64_basic_auth = base64.b64encode(basic_auth.encode('utf-8')).decode('utf-8')
157+
158+
159+
responses.post(
160+
token_url,
161+
match=[
162+
responses.matchers.urlencoded_params_matcher(
163+
{
164+
"grant_type": "client_credentials",
165+
"scope": "all-apis",
166+
},
167+
),
168+
responses.matchers.header_matcher({"Authorization": f"Basic {b64_basic_auth}"})
169+
],
170+
json={
171+
"access_token": "oauth2-m2m-access-token",
172+
"token_type": "Bearer",
173+
"expires_in": 3600,
174+
},
175+
)
176+
177+
cs = PositLocalContentCredentialsStrategy(
178+
token_url,
179+
client_id,
180+
client_secret,
181+
)
182+
cp = cs()
183+
assert cs.auth_type() == "posit-local-client-credentials"
184+
assert cp() == {"Authorization": "Bearer oauth2-m2m-access-token"}
185+
186+
187+
114188
@patch.dict("os.environ", {"CONNECT_CONTENT_SESSION_TOKEN": "cit"})
115189
@responses.activate
116190
@patch.dict("os.environ", {"RSTUDIO_PRODUCT": "CONNECT"})

0 commit comments

Comments
 (0)