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.

@@ -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,8 @@
1
+ README.md
2
+ main.py
3
+ pyproject.toml
4
+ hana_cloud_interface.egg-info/PKG-INFO
5
+ hana_cloud_interface.egg-info/SOURCES.txt
6
+ hana_cloud_interface.egg-info/dependency_links.txt
7
+ hana_cloud_interface.egg-info/requires.txt
8
+ hana_cloud_interface.egg-info/top_level.txt
@@ -0,0 +1,4 @@
1
+ keyring
2
+ hdbcli
3
+ polars
4
+ pandas
@@ -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']
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+