Skip to content

Commit 9cdb53f

Browse files
committed
Initialize external connection Streamlit example
1 parent 3ef0072 commit 9cdb53f

File tree

2 files changed

+244
-6
lines changed

2 files changed

+244
-6
lines changed

streamlit/view_groups.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,23 @@
143143
"page": "views/users_obo.py",
144144
"icon": ":material/key:",
145145
},
146-
{
147-
"label": "Retrieve a secret",
148-
"help": "Get a sensitive API key without hard-coding it.",
149-
"page": "views/secrets_retrieve.py",
150-
"icon": ":material/lock:",
151-
},
152146
],
153147
},
148+
{
149+
"title": "External services",
150+
"views": [
151+
{
152+
"label": "External connections",
153+
"help": "Connect to a Unity Catalog-governed HTTP endpoint.",
154+
"page": "views/external_connections.py",
155+
"icon": ":material/link:",
156+
},
157+
{
158+
"label": "Retrieve a secret",
159+
"help": "Get a sensitive API key without hard-coding it.",
160+
"page": "views/secrets_retrieve.py",
161+
"icon": ":material/lock:",
162+
},
163+
],
164+
},
154165
]
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import streamlit as st
2+
from databricks.sdk import WorkspaceClient
3+
from databricks.sdk.service.serving import ExternalFunctionRequestHttpMethod
4+
import json
5+
import re
6+
7+
8+
@st.cache_resource
9+
def get_client_obo() -> WorkspaceClient:
10+
user_token = st.context.headers["X-Forwarded-Access-Token"]
11+
if not user_token:
12+
st.error("User token is required for OBO authentication")
13+
return None
14+
15+
if user_token:
16+
return WorkspaceClient(
17+
token=user_token,
18+
auth_type="pat"
19+
)
20+
21+
22+
def init_github_mcp_connection(w: WorkspaceClient, uc_connection_name: str):
23+
"""Initialize GitHub MCP and get session ID"""
24+
st.session_state.mcp_session_id = None
25+
try:
26+
27+
init_json = {
28+
"jsonrpc": "2.0",
29+
"id": "init-1",
30+
"method": "initialize",
31+
"params": {}
32+
}
33+
34+
response = w.serving_endpoints.http_request(
35+
conn=uc_connection_name,
36+
method=ExternalFunctionRequestHttpMethod.POST,
37+
path="/",
38+
json=init_json,
39+
)
40+
41+
session_id = response.headers.get("mcp-session-id")
42+
if session_id:
43+
st.session_state.mcp_session_id = session_id
44+
return session_id, None
45+
46+
else:
47+
return None, "No session ID returned by server"
48+
49+
except Exception as e:
50+
return None, f"Error initializing MCP: {str(e)}"
51+
52+
53+
def extract_login_url_from_error(error: str):
54+
"""Extract login URL from error message"""
55+
56+
url_pattern = r'https://[^\s]+/explore/connections/[^\s]+'
57+
match = re.search(url_pattern, error)
58+
59+
if match:
60+
return match.group(0)
61+
62+
return None
63+
64+
65+
def is_connection_login_error(error: str):
66+
"""Check if error is a connection login error"""
67+
return "Credential for user identity" in error and "Please login first to the connection" in error
68+
69+
70+
HTTP_METHODS = [
71+
{"label": "GET", "value": "GET"},
72+
{"label": "POST", "value": "POST"}
73+
]
74+
75+
AUTH_TYPES = [
76+
{"label": "Bearer token", "value": "bearer_token"},
77+
{"label": "OAuth User to Machine Per User", "value": "oauth_user_machine_per_user"},
78+
{"label": "OAuth Machine to Machine", "value": "oauth_machine_machine"}
79+
]
80+
81+
82+
st.header(body="External connections", divider=True)
83+
st.subheader("Securely call external API services")
84+
st.write(
85+
"This recipe demonstrates how to use Unity Catalog-managed external HTTP connections for secure and governed access, for example, to GitHub, Jira, and Slack."
86+
)
87+
88+
tab_app, tab_code, tab_config = st.tabs(["Try it", "Code snippet", "Requirements"])
89+
90+
with tab_app:
91+
st.info(
92+
"This sample will only work as intended when deployed to Databricks Apps and not when running locally. Also, you need to configure on-behalf-of-user authentication for this Databricks Apps application.",
93+
icon="ℹ️",
94+
)
95+
96+
connection_name = st.text_input("Connection Name", placeholder="Enter connection name...")
97+
auth_mode = st.radio(
98+
"Authentication Mode:",
99+
["Bearer token", "OAuth User to Machine Per User", "OAuth Machine to Machine"],
100+
help="Bearer token is the user token.",
101+
)
102+
http_method = st.selectbox("HTTP Method", options=["GET", "POST", "PUT", "DELETE", "PATCH"], )
103+
path = st.text_input("Path", placeholder="/api/endpoint")
104+
request_type = st.selectbox("Request Type", options=["Non-MCP", "MCP"])
105+
request_headers = st.text_area("Request headers", value='{"Content-Type": "application/json"}')
106+
request_data = st.text_area("Request data", value='{"key": "value"}')
107+
108+
all_fields_filled = path and connection_name != ""
109+
if not all_fields_filled:
110+
st.info("Please fill in all required fields to run a query.")
111+
112+
113+
if st.button("Send Request"):
114+
if auth_mode == "Bearer token":
115+
w = WorkspaceClient()
116+
elif auth_mode == "OAuth User to Machine Per User":
117+
w = get_client_obo()
118+
elif auth_mode == "OAuth Machine to Machine":
119+
# TODO: Add OAuth Machine-to-Machine logic
120+
w = WorkspaceClient()
121+
122+
if request_headers and request_headers.strip():
123+
try:
124+
request_headers = json.loads(request_headers)
125+
except json.JSONDecodeError:
126+
st.error("❌ Invalid JSON in headers")
127+
128+
if request_data and request_data.strip():
129+
try:
130+
request_data = json.loads(request_data)
131+
except json.JSONDecodeError:
132+
st.error("❌ Invalid JSON data")
133+
134+
http_method = getattr(ExternalFunctionRequestHttpMethod, http_method)
135+
136+
if request_type == "MCP":
137+
if not st.session_state.mcp_session_id:
138+
session_id, error = init_github_mcp_connection(w)
139+
if error:
140+
if is_connection_login_error(error):
141+
login_url = extract_login_url_from_error(error)
142+
if login_url:
143+
st.warning("🔐 Connection Login Required")
144+
st.markdown("You need to authenticate with the external connection first.")
145+
st.markdown(f"[Login to Connection]({login_url})")
146+
else:
147+
st.error("❌ MCP error: {error}")
148+
else:
149+
st.error("❌ MCP initialization error: {error}")
150+
151+
st.session_state.mcp_session_id = session_id
152+
153+
request_headers["Mcp-Session-Id"] = st.session_state.mcp_session_id
154+
155+
response = w.serving_endpoints.http_request(
156+
conn=connection_name,
157+
method=http_method,
158+
path=path,
159+
headers=request_headers,
160+
json=request_data,
161+
)
162+
st.subheader("Response")
163+
st.json(response.json())
164+
165+
166+
167+
with tab_code:
168+
st.code("""
169+
import streamlit as st
170+
from databricks import sql
171+
from databricks.sdk.core import Config
172+
173+
cfg = Config()
174+
175+
def get_user_token():
176+
headers = st.context.headers
177+
user_token = headers["X-Forwarded-Access-Token"]
178+
return user_token
179+
180+
@st.cache_resource
181+
def connect_with_obo(http_path, user_token):
182+
return sql.connect(
183+
server_hostname=cfg.host,
184+
http_path=http_path,
185+
access_token=user_token
186+
)
187+
188+
def execute_query(table_name, conn):
189+
with conn.cursor() as cursor:
190+
query = f"SELECT * FROM {table_name} LIMIT 10"
191+
cursor.execute(query)
192+
return cursor.fetchall_arrow().to_pandas()
193+
194+
user_token = get_user_token()
195+
196+
http_path = "/sql/1.0/warehouses/abcd1234"
197+
table_name = "samples.nyctaxi.trips"
198+
199+
if st.button("Run Query"):
200+
conn = connect_with_obo(http_path, user_token)
201+
202+
df = execute_query(table_name, conn)
203+
st.dataframe(df)
204+
""")
205+
206+
with tab_config:
207+
col1, col2, col3 = st.columns(3)
208+
209+
with col1:
210+
st.markdown("""
211+
**Permissions (user or app service principal)**
212+
* `SELECT` permissions on the tables being queried
213+
* `CAN USE` on the SQL warehouse
214+
""")
215+
with col2:
216+
st.markdown("""
217+
**Databricks resources**
218+
* SQL warehouse
219+
* Unity Catalog table
220+
""")
221+
with col3:
222+
st.markdown("""
223+
**Dependencies**
224+
* [Databricks SDK](https://pypi.org/project/databricks-sdk/) - `databricks-sdk`
225+
* [Databricks SQL Connector](https://pypi.org/project/databricks-sql-connector/) - `databricks-sql-connector`
226+
* [Streamlit](https://pypi.org/project/streamlit/) - `streamlit`
227+
""")

0 commit comments

Comments
 (0)