hana-cloud-interface 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hana-cloud-interface might be problematic. Click here for more details.
- hana_cloud_interface-0.1.0/PKG-INFO +10 -0
- hana_cloud_interface-0.1.0/README.md +0 -0
- hana_cloud_interface-0.1.0/hana_cloud_interface.egg-info/PKG-INFO +10 -0
- hana_cloud_interface-0.1.0/hana_cloud_interface.egg-info/SOURCES.txt +8 -0
- hana_cloud_interface-0.1.0/hana_cloud_interface.egg-info/dependency_links.txt +1 -0
- hana_cloud_interface-0.1.0/hana_cloud_interface.egg-info/requires.txt +4 -0
- hana_cloud_interface-0.1.0/hana_cloud_interface.egg-info/top_level.txt +1 -0
- hana_cloud_interface-0.1.0/main.py +232 -0
- hana_cloud_interface-0.1.0/pyproject.toml +7 -0
- hana_cloud_interface-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hana-cloud-interface
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Runs a SQL command on SAP HANA Cloud using OAuth single sign on and returns a pandas or polars dataframe
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: keyring
|
|
8
|
+
Requires-Dist: hdbcli
|
|
9
|
+
Requires-Dist: polars
|
|
10
|
+
Requires-Dist: pandas
|
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hana-cloud-interface
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Runs a SQL command on SAP HANA Cloud using OAuth single sign on and returns a pandas or polars dataframe
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: keyring
|
|
8
|
+
Requires-Dist: hdbcli
|
|
9
|
+
Requires-Dist: polars
|
|
10
|
+
Requires-Dist: pandas
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
main
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import keyring
|
|
2
|
+
import json
|
|
3
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
4
|
+
import webbrowser
|
|
5
|
+
from functools import partial
|
|
6
|
+
import threading
|
|
7
|
+
import urllib.parse
|
|
8
|
+
import datetime
|
|
9
|
+
import requests
|
|
10
|
+
import traceback
|
|
11
|
+
from hdbcli import dbapi
|
|
12
|
+
import polars as pl
|
|
13
|
+
import pandas as pd
|
|
14
|
+
import os
|
|
15
|
+
|
|
16
|
+
config_file = r'C:\python\Hana_Cloud_interface\hc_oauth_config.json'
|
|
17
|
+
Browser_override = r'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe'
|
|
18
|
+
data_frame_type_global = 'pandas' # Default data frame type for hana_sql function
|
|
19
|
+
debug = False
|
|
20
|
+
|
|
21
|
+
def test_connection(oauth_config):
|
|
22
|
+
oauth_Token = keyring.get_password('SAP_hana_sso', 'oauth_Token')
|
|
23
|
+
|
|
24
|
+
if not oauth_Token:
|
|
25
|
+
if debug: print('No existing token found')
|
|
26
|
+
return False
|
|
27
|
+
else:
|
|
28
|
+
oauth_Token = json.loads(oauth_Token)
|
|
29
|
+
try: # Try to connect to server and automatically revalidate if It can't connect
|
|
30
|
+
cursor = dbapi.connect(address=oauth_Token['prod_URL'],port='443',authenticationMethods='jwt',password=oauth_Token['access_token'],encrypt=True, sslValidateCertificate=True).cursor()
|
|
31
|
+
#cursor.close()
|
|
32
|
+
|
|
33
|
+
age_Token = datetime.datetime.now() -datetime.datetime.strptime(oauth_Token['time'], "%m/%d/%Y, %H:%M:%S")
|
|
34
|
+
if debug: print('Connection successful token is {} mins old'.format(age_Token.seconds/60))
|
|
35
|
+
|
|
36
|
+
if age_Token.seconds/60 > 30:
|
|
37
|
+
if debug: print('Token is over 30 mins old will refresh')
|
|
38
|
+
refresh_access_token(oauth_Token['refresh_token'],oauth_config)
|
|
39
|
+
except :
|
|
40
|
+
if debug: print("Error during connection attempt:")
|
|
41
|
+
if debug: traceback.print_exc()
|
|
42
|
+
# old token is invalid delete it
|
|
43
|
+
keyring.delete_password('SAP_hana_sso', 'config_data')
|
|
44
|
+
if debug: print('Connection failed')
|
|
45
|
+
return False
|
|
46
|
+
return cursor
|
|
47
|
+
|
|
48
|
+
def refresh_access_token(refresh_token,oauth_config): # Refreshers token
|
|
49
|
+
token_url = oauth_config['TOKEN_URL']
|
|
50
|
+
# Set up the request payload
|
|
51
|
+
payload = {
|
|
52
|
+
'grant_type': 'refresh_token',
|
|
53
|
+
'client_id': oauth_config['CLIENT_ID'],
|
|
54
|
+
'client_secret': oauth_config['CLIENT_SECRET'],
|
|
55
|
+
'refresh_token': refresh_token
|
|
56
|
+
}
|
|
57
|
+
# Make the request to the token endpoint
|
|
58
|
+
response = requests.post(token_url, data=payload)
|
|
59
|
+
|
|
60
|
+
# Check if the request was successful
|
|
61
|
+
if response.status_code == 200:
|
|
62
|
+
# save to json
|
|
63
|
+
token_response = response.json()
|
|
64
|
+
|
|
65
|
+
dictionary = {
|
|
66
|
+
"time": datetime.datetime.now().strftime("%m/%d/%Y, %H:%M:%S"),
|
|
67
|
+
"access_token": token_response['access_token'],
|
|
68
|
+
"refresh_token": token_response['refresh_token'],
|
|
69
|
+
"prod_URL":oauth_config['HC_prod_URL'],
|
|
70
|
+
}
|
|
71
|
+
keyring.set_password('SAP_hana_sso', 'oauth_Token', json.dumps(dictionary))
|
|
72
|
+
|
|
73
|
+
return
|
|
74
|
+
else:
|
|
75
|
+
response.raise_for_status()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# A class that runs the HTTP Server Responses
|
|
79
|
+
class RequestHandler(BaseHTTPRequestHandler):
|
|
80
|
+
|
|
81
|
+
def __init__(self, oauth_input, *args, **kwargs):
|
|
82
|
+
self.oauth_config = oauth_input
|
|
83
|
+
# BaseHTTPRequestHandler calls do_GET **inside** __init__ !!!
|
|
84
|
+
# So we have to call super().__init__ after setting attributes.
|
|
85
|
+
super().__init__(*args, **kwargs)
|
|
86
|
+
|
|
87
|
+
def do_GET(self):
|
|
88
|
+
# Parse the query parameters
|
|
89
|
+
query_components = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
|
|
90
|
+
# Check if the path is the callback path
|
|
91
|
+
if self.path.startswith('/callback'):
|
|
92
|
+
# Extract the authorization code
|
|
93
|
+
authorization_code = query_components.get('code')
|
|
94
|
+
if authorization_code:
|
|
95
|
+
authorization_code = authorization_code[0]
|
|
96
|
+
|
|
97
|
+
# Exchange the authorization code for an access token
|
|
98
|
+
access_token,refresh_token = self.exchange_code_for_token(authorization_code)
|
|
99
|
+
|
|
100
|
+
# Respond to the client
|
|
101
|
+
self.send_response(200)
|
|
102
|
+
self.send_header('Content-type', 'text/html')
|
|
103
|
+
self.end_headers()
|
|
104
|
+
self.wfile.write(b"Authorization code received. You can close this window.")
|
|
105
|
+
|
|
106
|
+
# save to json
|
|
107
|
+
dictionary = {
|
|
108
|
+
"time": datetime.datetime.now().strftime("%m/%d/%Y, %H:%M:%S"),
|
|
109
|
+
"access_token": access_token,
|
|
110
|
+
"refresh_token":refresh_token,
|
|
111
|
+
"prod_URL":self.oauth_config['HC_prod_URL'],
|
|
112
|
+
}
|
|
113
|
+
keyring.set_password('SAP_hana_sso', 'oauth_Token', json.dumps(dictionary))
|
|
114
|
+
|
|
115
|
+
threading.Thread(target=self.shutdown_server).start()
|
|
116
|
+
else:
|
|
117
|
+
self.send_response(400)
|
|
118
|
+
self.send_header('Content-type', 'text/html')
|
|
119
|
+
self.end_headers()
|
|
120
|
+
self.wfile.write(b"Authorization code not found in the query parameters.")
|
|
121
|
+
|
|
122
|
+
def exchange_code_for_token(self, code):
|
|
123
|
+
# Prepare the token request payload
|
|
124
|
+
payload = {
|
|
125
|
+
'grant_type': 'authorization_code',
|
|
126
|
+
'code': code,
|
|
127
|
+
'redirect_uri': self.oauth_config['REDIRECT_URI'],
|
|
128
|
+
'client_id': self.oauth_config['CLIENT_ID'],
|
|
129
|
+
'client_secret': self.oauth_config['CLIENT_SECRET']
|
|
130
|
+
}
|
|
131
|
+
# Make the POST request to exchange the authorization code for an access token
|
|
132
|
+
response = requests.post(self.oauth_config['TOKEN_URL'], data=payload)
|
|
133
|
+
if response.status_code == 200:
|
|
134
|
+
token_data = response.json()
|
|
135
|
+
return token_data['access_token'],token_data['refresh_token']
|
|
136
|
+
else:
|
|
137
|
+
raise Exception(f"Failed to obtain access token: {response.status_code} {response.text}")
|
|
138
|
+
# Function to shut down the server
|
|
139
|
+
def shutdown_server(self):
|
|
140
|
+
print('end')
|
|
141
|
+
self.server.shutdown()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Requests a new token
|
|
145
|
+
def get_token(i=0):
|
|
146
|
+
#print('get token', i)
|
|
147
|
+
oauth_config = config_reader()
|
|
148
|
+
result = test_connection(oauth_config)
|
|
149
|
+
if not result:
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
params = {
|
|
154
|
+
'response_type': 'code',
|
|
155
|
+
'client_id': oauth_config['CLIENT_ID'],
|
|
156
|
+
'redirect_uri': oauth_config['REDIRECT_URI'],
|
|
157
|
+
'scope': oauth_config['SCOPE']
|
|
158
|
+
}
|
|
159
|
+
authorization_url = f"{oauth_config['AUTH_URL']}?{urllib.parse.urlencode(params)}"
|
|
160
|
+
|
|
161
|
+
# Open web browser and go to oauth url
|
|
162
|
+
if Browser_override != '':
|
|
163
|
+
webbrowser.get(Browser_override).open(authorization_url,new=1)
|
|
164
|
+
else:
|
|
165
|
+
webbrowser.open(authorization_url,new=1)
|
|
166
|
+
|
|
167
|
+
server_address = ('', 8080)
|
|
168
|
+
handler = partial(RequestHandler, oauth_config)
|
|
169
|
+
httpd = HTTPServer(server_address, handler)
|
|
170
|
+
httpd.timeout = 10 # timeout
|
|
171
|
+
|
|
172
|
+
httpd.handle_request()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# see if function timed out and failed
|
|
176
|
+
if i > 0:
|
|
177
|
+
raise TimeoutError('Failed to get token after 2 attempts')
|
|
178
|
+
if keyring.get_password('SAP_hana_sso', 'oauth_Token') is None:
|
|
179
|
+
print('Request Timed out will try again')
|
|
180
|
+
|
|
181
|
+
return get_token(i+1)
|
|
182
|
+
else:
|
|
183
|
+
return result
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# returns configuration data for single sign on from either file or keychain
|
|
187
|
+
def config_reader():
|
|
188
|
+
# reads config data from keychain
|
|
189
|
+
config_data = keyring.get_password('SAP_hana_sso', 'config_data')
|
|
190
|
+
if not config_data: # If there is no existing config data saved to the credentials manager read from file and save it
|
|
191
|
+
if not os.path.exists(config_file):
|
|
192
|
+
raise FileNotFoundError(f"Config file not found: {config_file}")
|
|
193
|
+
with open(config_file) as file:
|
|
194
|
+
config_data = json.load(file)
|
|
195
|
+
keyring.set_password('SAP_hana_sso', 'config_data', json.dumps(config_data))
|
|
196
|
+
return config_data
|
|
197
|
+
else:
|
|
198
|
+
return json.loads(config_data)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def hana_sql(sql_command='test',DF_type = data_frame_type_global):
|
|
204
|
+
""" handles single sign on then runs a SQL command
|
|
205
|
+
|
|
206
|
+
Parameters:
|
|
207
|
+
sql_command (str): SQL command to run or 'test' to just validate token
|
|
208
|
+
df_type (str): Type of data frame to return ('pandas' or 'polars') defaults to pandas
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
data frame of type specified in df_type
|
|
212
|
+
|
|
213
|
+
"""
|
|
214
|
+
cursor = get_token()
|
|
215
|
+
|
|
216
|
+
#cursor = dbapi.connect(address=oauth_Token['prod_URL'],port='443',authenticationMethods='jwt',password=oauth_Token['access_token'],encrypt=True, sslValidateCertificate=True).cursor()
|
|
217
|
+
|
|
218
|
+
cursor.execute(sql_command) # Run SQL command
|
|
219
|
+
# Retrieve data and convert it to a pandas data frame
|
|
220
|
+
data = cursor.fetchall()
|
|
221
|
+
data_name = [i[0] for i in cursor.description]
|
|
222
|
+
cursor.close()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if DF_type == 'pandas':
|
|
226
|
+
return pd.DataFrame(data,columns=data_name)
|
|
227
|
+
elif DF_type == 'polars':
|
|
228
|
+
data = [row.column_values for row in data]
|
|
229
|
+
return pl.DataFrame(data,orient='row',schema=data_name)
|
|
230
|
+
else:
|
|
231
|
+
raise ValueError("DF_type must be either 'pandas' or 'polars'")
|
|
232
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hana-cloud-interface"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Runs a SQL command on SAP HANA Cloud using OAuth single sign on and returns a pandas or polars dataframe"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = ['keyring','hdbcli','polars','pandas']
|