hana-cloud-interface 0.3.0__py3-none-any.whl
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.
- hana_cloud_interface/__init__.py +1 -0
- hana_cloud_interface/main.py +337 -0
- hana_cloud_interface/py.typed +0 -0
- hana_cloud_interface/test_doc.ipynb +652 -0
- hana_cloud_interface-0.3.0.dist-info/METADATA +97 -0
- hana_cloud_interface-0.3.0.dist-info/RECORD +8 -0
- hana_cloud_interface-0.3.0.dist-info/WHEEL +4 -0
- hana_cloud_interface-0.3.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .main import Hana_cloud_interface
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import threading
|
|
3
|
+
import datetime
|
|
4
|
+
|
|
5
|
+
from selenium import webdriver
|
|
6
|
+
from selenium.webdriver.chrome.options import Options
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import polars as pl
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
from hdbcli import dbapi
|
|
14
|
+
from hana_ml.dataframe import ConnectionContext, create_dataframe_from_pandas
|
|
15
|
+
|
|
16
|
+
from platformdirs import *
|
|
17
|
+
import os
|
|
18
|
+
import json
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Hana_cloud_interface:
|
|
24
|
+
def __init__(self,config=None, df=None):
|
|
25
|
+
"""
|
|
26
|
+
Initialize the Hana_cloud_interface class.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
config (str, optional): Path to the configuration JSON file. If provided and valid, the configuration will be loaded and saved to the user data directory.
|
|
30
|
+
df (str, optional): DataFrame type to use for query results ('pandas' or 'polars'). Can be overridden per query.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
Exception: If the provided config path does not exist.
|
|
34
|
+
"""
|
|
35
|
+
self.DF_type_global_override = df
|
|
36
|
+
|
|
37
|
+
# set up app data location if it doesn't exist
|
|
38
|
+
appname = "HanaCloudinterface"
|
|
39
|
+
appauthor = "powerco"
|
|
40
|
+
self.app_data_location =user_data_dir(appname, appauthor)
|
|
41
|
+
os.makedirs(self.app_data_location, exist_ok=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# read to config file and saves it to user data location
|
|
46
|
+
if config is not None:
|
|
47
|
+
if os.path.exists(config):
|
|
48
|
+
with open(config, 'r') as f:
|
|
49
|
+
dictionary = json.load(f)
|
|
50
|
+
file_path = os.path.join(self.app_data_location, ('token.json'))
|
|
51
|
+
with open(file_path, 'w') as f:
|
|
52
|
+
json.dump(dictionary, f)
|
|
53
|
+
else:
|
|
54
|
+
raise Exception('path to configuration file not valid')
|
|
55
|
+
|
|
56
|
+
def _save_json(self,type,dictionary):
|
|
57
|
+
"""
|
|
58
|
+
Save a dictionary as a JSON file in the application data directory.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
type (str): The type of file to save ('config' or 'token').
|
|
62
|
+
dictionary (dict): The dictionary to save as JSON.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValueError: If the type is not 'config' or 'token'.
|
|
66
|
+
"""
|
|
67
|
+
# Save password dictionary as JSON in the config_location folder
|
|
68
|
+
if type == 'config':
|
|
69
|
+
file_path = os.path.join(self.app_data_location, ('config.json'))
|
|
70
|
+
elif type == 'token':
|
|
71
|
+
file_path = os.path.join(self.app_data_location, ('token.json'))
|
|
72
|
+
else:
|
|
73
|
+
raise ValueError("type must be either 'config' or 'token'")
|
|
74
|
+
with open(file_path, 'w') as f:
|
|
75
|
+
json.dump(dictionary, f)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _read_json(self):
|
|
79
|
+
"""
|
|
80
|
+
Read the configuration and token JSON files from the application data directory.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
list: A list containing two elements:
|
|
84
|
+
- The configuration dictionary if found, otherwise None.
|
|
85
|
+
- The token dictionary if found, otherwise None.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
file_path_1 = os.path.join(self.app_data_location, ('config.json'))
|
|
89
|
+
file_path_2 = os.path.join(self.app_data_location, ('token.json'))
|
|
90
|
+
out = [None,None]
|
|
91
|
+
if os.path.exists(file_path_1):
|
|
92
|
+
with open(file_path_1, 'r') as f:
|
|
93
|
+
out[0] = json.load(f)
|
|
94
|
+
if os.path.exists(file_path_2):
|
|
95
|
+
with open(file_path_2, 'r') as f:
|
|
96
|
+
out[1] = json.load(f)
|
|
97
|
+
return out
|
|
98
|
+
|
|
99
|
+
def _get_token(self,oauth_config):
|
|
100
|
+
"""
|
|
101
|
+
Obtain an OAuth token using the provided OAuth configuration.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
oauth_config (dict): A dictionary containing OAuth configuration parameters, including:
|
|
105
|
+
- CLIENT_ID
|
|
106
|
+
- CLIENT_SECRET
|
|
107
|
+
- REDIRECT_URI
|
|
108
|
+
- AUTH_URL
|
|
109
|
+
- TOKEN_URL
|
|
110
|
+
- SCOPE
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
dict: The token data obtained from the OAuth server, including access token, refresh token, and time obtained.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
Exception: If the token cannot be obtained due to an HTTP error or invalid configuration.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
import urllib.parse
|
|
120
|
+
params = {
|
|
121
|
+
'response_type': 'code',
|
|
122
|
+
'client_id': oauth_config['CLIENT_ID'],
|
|
123
|
+
'redirect_uri': oauth_config['REDIRECT_URI'],
|
|
124
|
+
'scope': oauth_config['SCOPE']
|
|
125
|
+
}
|
|
126
|
+
authorization_url = f"{oauth_config['AUTH_URL']}?{urllib.parse.urlencode(params)}"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# Define a specific directory for your profile data
|
|
130
|
+
# (Ensure this directory exists before running the script)
|
|
131
|
+
profile_dir = os.path.join(self.app_data_location, "selenium_profile")
|
|
132
|
+
print('Profile directory:', profile_dir)
|
|
133
|
+
if not os.path.exists(profile_dir):
|
|
134
|
+
os.makedirs(profile_dir)
|
|
135
|
+
|
|
136
|
+
chrome_options = Options()
|
|
137
|
+
# Use 'user-data-dir' to save/load the entire profile
|
|
138
|
+
chrome_options.add_argument(f"--user-data-dir={profile_dir}")
|
|
139
|
+
# Optional: Specify a specific profile within that directory (e.g., "Default" or "Profile 1")
|
|
140
|
+
chrome_options.add_argument("--profile-directory=Default")
|
|
141
|
+
|
|
142
|
+
driver = webdriver.Chrome(options=chrome_options)
|
|
143
|
+
driver.get(authorization_url)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
while not driver.current_url.startswith("http://localhost:8080/callback"):
|
|
147
|
+
time.sleep(0.1)
|
|
148
|
+
print("Callback URL reached:", driver.current_url)
|
|
149
|
+
query_components = urllib.parse.parse_qs(urllib.parse.urlparse(driver.current_url).query)
|
|
150
|
+
authorization_code = query_components.get('code')
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
threading.Thread(target=driver.quit).start()
|
|
154
|
+
|
|
155
|
+
print('Authorization code:', authorization_code)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
payload = {
|
|
159
|
+
'grant_type': 'authorization_code',
|
|
160
|
+
'code': authorization_code,
|
|
161
|
+
'redirect_uri': oauth_config['REDIRECT_URI'],
|
|
162
|
+
'client_id': oauth_config['CLIENT_ID'],
|
|
163
|
+
'client_secret': oauth_config['CLIENT_SECRET']
|
|
164
|
+
}
|
|
165
|
+
response = requests.post(oauth_config['TOKEN_URL'], data=payload)
|
|
166
|
+
if response.status_code == 200:
|
|
167
|
+
token_data = response.json()
|
|
168
|
+
token_data['time_obtained'] = time.time()
|
|
169
|
+
else:
|
|
170
|
+
raise Exception(f"Failed to obtain access token: {response.status_code} {response.text}")
|
|
171
|
+
|
|
172
|
+
self._save_json('token',token_data)
|
|
173
|
+
return token_data
|
|
174
|
+
|
|
175
|
+
def _refresh_access_token(self,token_data,oauth_config):
|
|
176
|
+
"""
|
|
177
|
+
Refresh the OAuth access token using the refresh token.
|
|
178
|
+
if the refresh fails it will get a new token
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
token_data (dict): The current token data containing at least the 'refresh_token'.
|
|
182
|
+
oauth_config (dict): The OAuth configuration dictionary with required client and endpoint information.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
dict: The refreshed token data, including the new access token and updated 'time_obtained'.
|
|
186
|
+
|
|
187
|
+
"""
|
|
188
|
+
token_url = oauth_config['TOKEN_URL']
|
|
189
|
+
# Set up the request payload
|
|
190
|
+
payload = {
|
|
191
|
+
'grant_type': 'refresh_token',
|
|
192
|
+
'client_id': oauth_config['CLIENT_ID'],
|
|
193
|
+
'client_secret': oauth_config['CLIENT_SECRET'],
|
|
194
|
+
'refresh_token': token_data['refresh_token']
|
|
195
|
+
}
|
|
196
|
+
# Make the request to the token endpoint
|
|
197
|
+
response = requests.post(token_url, data=payload)
|
|
198
|
+
|
|
199
|
+
# Check if the request was successful
|
|
200
|
+
if response.status_code == 200:
|
|
201
|
+
token_data = response.json()
|
|
202
|
+
token_data['time_obtained'] = time.time()
|
|
203
|
+
self._save_json('token',token_data)
|
|
204
|
+
else:
|
|
205
|
+
print('Failed to refresh token, obtaining new token')
|
|
206
|
+
print('response:',response.status_code,response.text)
|
|
207
|
+
token_data = self._get_token(oauth_config)
|
|
208
|
+
|
|
209
|
+
return token_data
|
|
210
|
+
|
|
211
|
+
def _connect_to_hana(self):
|
|
212
|
+
"""
|
|
213
|
+
Establish a connection to the HANA Cloud database using OAuth authentication.
|
|
214
|
+
|
|
215
|
+
This method reads the OAuth configuration and token data from JSON files in the application data directory.
|
|
216
|
+
If the token is missing or expired, it obtains or refreshes the token as needed.
|
|
217
|
+
Returns a database cursor for executing SQL commands.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
hdbcli.dbapi.Cursor: A cursor object connected to the HANA Cloud database.
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
Exception: If the configuration file is missing or invalid.
|
|
224
|
+
"""
|
|
225
|
+
oauth_config,token_data = self._read_json()
|
|
226
|
+
if oauth_config is None:
|
|
227
|
+
raise Exception('No config file found, please run configuration first')
|
|
228
|
+
elif token_data is None:
|
|
229
|
+
token_data = self._get_token(oauth_config)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
expiry_timestamp = token_data['time_obtained'] + token_data['expires_in']
|
|
233
|
+
expiry_datetime = datetime.datetime.fromtimestamp(expiry_timestamp)
|
|
234
|
+
print("Token expires at:", expiry_datetime)
|
|
235
|
+
# Refresh token if it will expire in less than 20 minutes (1200 seconds)
|
|
236
|
+
|
|
237
|
+
if time.time() > expiry_timestamp:
|
|
238
|
+
print('Token has expired, obtaining new token')
|
|
239
|
+
token_data = self._get_token(oauth_config)
|
|
240
|
+
elif expiry_timestamp - time.time() < 1200:
|
|
241
|
+
print('Token will expire in less than 20 minutes, refreshing token')
|
|
242
|
+
token_data = self._refresh_access_token(token_data, oauth_config)
|
|
243
|
+
|
|
244
|
+
return dbapi.connect(address=oauth_config['HC_prod_URL'],port='443',authenticationMethods='jwt',password=token_data['access_token'],encrypt=True, sslValidateCertificate=True).cursor()
|
|
245
|
+
|
|
246
|
+
def hana_sql(self, sql_command='validate',DF_type_local_override = None):
|
|
247
|
+
"""
|
|
248
|
+
Execute a SQL command on the HANA Cloud database and return the result as a DataFrame.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
sql_command (str, optional): The SQL command to execute. Defaults to 'validate', which only checks the connection.
|
|
252
|
+
DF_type_local_override (str, optional): DataFrame type for this query ('pandas' or 'polars'). Overrides the global setting if provided.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
str: If sql_command is 'validate', returns a validation message.
|
|
256
|
+
pandas.DataFrame or polars.DataFrame: The query result as a DataFrame, depending on the specified or default DataFrame type.
|
|
257
|
+
|
|
258
|
+
Raises:
|
|
259
|
+
ValueError: If the DataFrame type is not specified or is invalid.
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
cursor = self._connect_to_hana()
|
|
263
|
+
if sql_command == 'validate':
|
|
264
|
+
return 'Connection successful validated'
|
|
265
|
+
|
|
266
|
+
if DF_type_local_override is not None:
|
|
267
|
+
DF_type = DF_type_local_override
|
|
268
|
+
else:
|
|
269
|
+
DF_type = self.DF_type_global_override
|
|
270
|
+
|
|
271
|
+
cursor.execute(sql_command) # Run SQL command
|
|
272
|
+
# Retrieve data and convert it to a pandas data frame
|
|
273
|
+
data = cursor.fetchall()
|
|
274
|
+
data_name = [i[0] for i in cursor.description]
|
|
275
|
+
cursor.close()
|
|
276
|
+
|
|
277
|
+
if DF_type == 'pandas':
|
|
278
|
+
|
|
279
|
+
return pd.DataFrame(data,columns=data_name)
|
|
280
|
+
elif DF_type == 'polars':
|
|
281
|
+
data = [row.column_values for row in data]
|
|
282
|
+
return pl.DataFrame(data,orient='row',schema=data_name)
|
|
283
|
+
else:
|
|
284
|
+
raise ValueError("DF_type must be either 'pandas' or 'polars' needs to be defined when you initialise the class or pass it to the hana_sql function")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def hana_upload(self,data, data_name, SCHEMA):
|
|
288
|
+
"""
|
|
289
|
+
Upload a DataFrame to the HANA Cloud database.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
data (pandas.DataFrame or polars.DataFrame): The data to upload. If a polars DataFrame is provided, it will be converted to pandas.
|
|
293
|
+
data_name (str): The name of the table to create or replace in HANA Cloud.
|
|
294
|
+
SCHEMA (str): The schema in which to create the table.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
bool: True if the upload is successful.
|
|
298
|
+
|
|
299
|
+
Raises:
|
|
300
|
+
Exception: If the upload fails due to connection or data issues.
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
# If the input data is a polars DataFrame, convert it to pandas
|
|
304
|
+
if isinstance(data, pl.DataFrame):
|
|
305
|
+
data = data.to_pandas()
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# validates that the token still works
|
|
309
|
+
cursor = self._connect_to_hana()
|
|
310
|
+
cursor.close()
|
|
311
|
+
|
|
312
|
+
oauth_config,token_data = self._read_json()
|
|
313
|
+
|
|
314
|
+
conn = ConnectionContext(address=oauth_config['HC_prod_URL'],port='443',authenticationMethods='jwt',password=token_data['access_token'],encrypt=True, sslValidateCertificate=True)
|
|
315
|
+
|
|
316
|
+
create_dataframe_from_pandas(conn, data, data_name,
|
|
317
|
+
schema=SCHEMA,
|
|
318
|
+
force=True, # True: truncate and insert
|
|
319
|
+
replace=True) # True: Null is replaced by 0
|
|
320
|
+
return True
|
|
321
|
+
|
|
322
|
+
def validate():
|
|
323
|
+
"""
|
|
324
|
+
Validate the connection to the HANA Cloud database.
|
|
325
|
+
|
|
326
|
+
This function creates an instance of the Hana_cloud_interface class and calls the hana_sql method
|
|
327
|
+
with the 'validate' command to check the connection.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
str: A message indicating whether the connection was successful or failed.
|
|
331
|
+
"""
|
|
332
|
+
hana_interface = Hana_cloud_interface()
|
|
333
|
+
try:
|
|
334
|
+
result = hana_interface.hana_sql('validate')
|
|
335
|
+
return result
|
|
336
|
+
except Exception as e:
|
|
337
|
+
return f'Connection failed: {str(e)}'
|
|
File without changes
|
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cells": [
|
|
3
|
+
{
|
|
4
|
+
"cell_type": "code",
|
|
5
|
+
"execution_count": null,
|
|
6
|
+
"id": "1b2ba115",
|
|
7
|
+
"metadata": {},
|
|
8
|
+
"outputs": [],
|
|
9
|
+
"source": [
|
|
10
|
+
"import time\n",
|
|
11
|
+
"import threading\n",
|
|
12
|
+
"import datetime\n",
|
|
13
|
+
"\n",
|
|
14
|
+
"from selenium import webdriver\n",
|
|
15
|
+
"from selenium.webdriver.chrome.options import Options\n",
|
|
16
|
+
"\n",
|
|
17
|
+
"import pandas as pd\n",
|
|
18
|
+
"import polars as pl\n",
|
|
19
|
+
"\n",
|
|
20
|
+
"import requests\n",
|
|
21
|
+
"\n",
|
|
22
|
+
"from hdbcli import dbapi\n",
|
|
23
|
+
"from hana_ml.dataframe import ConnectionContext, create_dataframe_from_pandas\n",
|
|
24
|
+
"\n",
|
|
25
|
+
"from platformdirs import *\n",
|
|
26
|
+
"import os\n",
|
|
27
|
+
"import json\n",
|
|
28
|
+
"\n",
|
|
29
|
+
"\n",
|
|
30
|
+
"\n",
|
|
31
|
+
"\n",
|
|
32
|
+
"class Hana_cloud_interface:\n",
|
|
33
|
+
" def __init__(self,config=None, df=None):\n",
|
|
34
|
+
" \"\"\"\n",
|
|
35
|
+
" Initialize the Hana_cloud_interface class.\n",
|
|
36
|
+
"\n",
|
|
37
|
+
" Args:\n",
|
|
38
|
+
" config (str, optional): Path to the configuration JSON file. If provided and valid, the configuration will be loaded and saved to the user data directory.\n",
|
|
39
|
+
" df (str, optional): DataFrame type to use for query results ('pandas' or 'polars'). Can be overridden per query.\n",
|
|
40
|
+
"\n",
|
|
41
|
+
" Raises:\n",
|
|
42
|
+
" Exception: If the provided config path does not exist.\n",
|
|
43
|
+
" \"\"\"\n",
|
|
44
|
+
" self.DF_type_global_override = df\n",
|
|
45
|
+
" \n",
|
|
46
|
+
" # set up app data location if it doesn't exist\n",
|
|
47
|
+
" appname = \"HanaCloudinterface\"\n",
|
|
48
|
+
" appauthor = \"powerco\"\n",
|
|
49
|
+
" self.app_data_location =user_data_dir(appname, appauthor)\n",
|
|
50
|
+
" os.makedirs(self.app_data_location, exist_ok=True)\n",
|
|
51
|
+
" \n",
|
|
52
|
+
" \n",
|
|
53
|
+
" \n",
|
|
54
|
+
" # read to config file and saves it to user data location\n",
|
|
55
|
+
" if config is not None:\n",
|
|
56
|
+
" if os.path.exists(config):\n",
|
|
57
|
+
" with open(config, 'r') as f:\n",
|
|
58
|
+
" dictionary = json.load(f)\n",
|
|
59
|
+
" file_path = os.path.join(self.app_data_location, ('token.json')) \n",
|
|
60
|
+
" with open(file_path, 'w') as f:\n",
|
|
61
|
+
" json.dump(dictionary, f)\n",
|
|
62
|
+
" else:\n",
|
|
63
|
+
" raise Exception('path to configuration file not valid')\n",
|
|
64
|
+
"\n",
|
|
65
|
+
" def _save_json(self,type,dictionary):\n",
|
|
66
|
+
" \"\"\"\n",
|
|
67
|
+
" Save a dictionary as a JSON file in the application data directory.\n",
|
|
68
|
+
"\n",
|
|
69
|
+
" Args:\n",
|
|
70
|
+
" type (str): The type of file to save ('config' or 'token').\n",
|
|
71
|
+
" dictionary (dict): The dictionary to save as JSON.\n",
|
|
72
|
+
"\n",
|
|
73
|
+
" Raises:\n",
|
|
74
|
+
" ValueError: If the type is not 'config' or 'token'.\n",
|
|
75
|
+
" \"\"\"\n",
|
|
76
|
+
" # Save password dictionary as JSON in the config_location folder\n",
|
|
77
|
+
" if type == 'config':\n",
|
|
78
|
+
" file_path = os.path.join(self.app_data_location, ('config.json'))\n",
|
|
79
|
+
" elif type == 'token':\n",
|
|
80
|
+
" file_path = os.path.join(self.app_data_location, ('token.json')) \n",
|
|
81
|
+
" else:\n",
|
|
82
|
+
" raise ValueError(\"type must be either 'config' or 'token'\")\n",
|
|
83
|
+
" with open(file_path, 'w') as f:\n",
|
|
84
|
+
" json.dump(dictionary, f)\n",
|
|
85
|
+
"\n",
|
|
86
|
+
"\n",
|
|
87
|
+
" def _read_json(self):\n",
|
|
88
|
+
" \"\"\"\n",
|
|
89
|
+
" Read the configuration and token JSON files from the application data directory.\n",
|
|
90
|
+
"\n",
|
|
91
|
+
" Returns:\n",
|
|
92
|
+
" list: A list containing two elements:\n",
|
|
93
|
+
" - The configuration dictionary if found, otherwise None.\n",
|
|
94
|
+
" - The token dictionary if found, otherwise None.\n",
|
|
95
|
+
" \"\"\"\n",
|
|
96
|
+
" \n",
|
|
97
|
+
" file_path_1 = os.path.join(self.app_data_location, ('config.json'))\n",
|
|
98
|
+
" file_path_2 = os.path.join(self.app_data_location, ('token.json'))\n",
|
|
99
|
+
" out = [None,None]\n",
|
|
100
|
+
" if os.path.exists(file_path_1):\n",
|
|
101
|
+
" with open(file_path_1, 'r') as f:\n",
|
|
102
|
+
" out[0] = json.load(f)\n",
|
|
103
|
+
" if os.path.exists(file_path_2):\n",
|
|
104
|
+
" with open(file_path_2, 'r') as f:\n",
|
|
105
|
+
" out[1] = json.load(f)\n",
|
|
106
|
+
" return out\n",
|
|
107
|
+
"\n",
|
|
108
|
+
" def _get_token(self,oauth_config): \n",
|
|
109
|
+
" \"\"\"\n",
|
|
110
|
+
" Obtain an OAuth token using the provided OAuth configuration.\n",
|
|
111
|
+
"\n",
|
|
112
|
+
" Args:\n",
|
|
113
|
+
" oauth_config (dict): A dictionary containing OAuth configuration parameters, including:\n",
|
|
114
|
+
" - CLIENT_ID\n",
|
|
115
|
+
" - CLIENT_SECRET\n",
|
|
116
|
+
" - REDIRECT_URI\n",
|
|
117
|
+
" - AUTH_URL\n",
|
|
118
|
+
" - TOKEN_URL\n",
|
|
119
|
+
" - SCOPE\n",
|
|
120
|
+
"\n",
|
|
121
|
+
" Returns:\n",
|
|
122
|
+
" dict: The token data obtained from the OAuth server, including access token, refresh token, and time obtained.\n",
|
|
123
|
+
"\n",
|
|
124
|
+
" Raises:\n",
|
|
125
|
+
" Exception: If the token cannot be obtained due to an HTTP error or invalid configuration.\n",
|
|
126
|
+
" \"\"\"\n",
|
|
127
|
+
" \n",
|
|
128
|
+
" import urllib.parse\n",
|
|
129
|
+
" params = {\n",
|
|
130
|
+
" 'response_type': 'code',\n",
|
|
131
|
+
" 'client_id': oauth_config['CLIENT_ID'],\n",
|
|
132
|
+
" 'redirect_uri': oauth_config['REDIRECT_URI'],\n",
|
|
133
|
+
" 'scope': oauth_config['SCOPE']\n",
|
|
134
|
+
" }\n",
|
|
135
|
+
" authorization_url = f\"{oauth_config['AUTH_URL']}?{urllib.parse.urlencode(params)}\"\n",
|
|
136
|
+
"\n",
|
|
137
|
+
"\n",
|
|
138
|
+
" # Define a specific directory for your profile data\n",
|
|
139
|
+
" # (Ensure this directory exists before running the script)\n",
|
|
140
|
+
" profile_dir = os.path.join(self.app_data_location, \"selenium_profile\")\n",
|
|
141
|
+
" print('Profile directory:', profile_dir)\n",
|
|
142
|
+
" if not os.path.exists(profile_dir):\n",
|
|
143
|
+
" os.makedirs(profile_dir)\n",
|
|
144
|
+
"\n",
|
|
145
|
+
" chrome_options = Options()\n",
|
|
146
|
+
" # Use 'user-data-dir' to save/load the entire profile\n",
|
|
147
|
+
" chrome_options.add_argument(f\"--user-data-dir={profile_dir}\")\n",
|
|
148
|
+
" # Optional: Specify a specific profile within that directory (e.g., \"Default\" or \"Profile 1\")\n",
|
|
149
|
+
" chrome_options.add_argument(\"--profile-directory=Default\") \n",
|
|
150
|
+
"\n",
|
|
151
|
+
" driver = webdriver.Chrome(options=chrome_options)\n",
|
|
152
|
+
" driver.get(authorization_url)\n",
|
|
153
|
+
"\n",
|
|
154
|
+
"\n",
|
|
155
|
+
" while not driver.current_url.startswith(\"http://localhost:8080/callback\"):\n",
|
|
156
|
+
" time.sleep(0.1)\n",
|
|
157
|
+
" print(\"Callback URL reached:\", driver.current_url)\n",
|
|
158
|
+
" query_components = urllib.parse.parse_qs(urllib.parse.urlparse(driver.current_url).query)\n",
|
|
159
|
+
" authorization_code = query_components.get('code')\n",
|
|
160
|
+
"\n",
|
|
161
|
+
"\n",
|
|
162
|
+
" threading.Thread(target=driver.quit).start()\n",
|
|
163
|
+
"\n",
|
|
164
|
+
" print('Authorization code:', authorization_code)\n",
|
|
165
|
+
"\n",
|
|
166
|
+
"\n",
|
|
167
|
+
" payload = {\n",
|
|
168
|
+
" 'grant_type': 'authorization_code',\n",
|
|
169
|
+
" 'code': authorization_code,\n",
|
|
170
|
+
" 'redirect_uri': oauth_config['REDIRECT_URI'],\n",
|
|
171
|
+
" 'client_id': oauth_config['CLIENT_ID'],\n",
|
|
172
|
+
" 'client_secret': oauth_config['CLIENT_SECRET']\n",
|
|
173
|
+
" }\n",
|
|
174
|
+
" response = requests.post(oauth_config['TOKEN_URL'], data=payload)\n",
|
|
175
|
+
" if response.status_code == 200:\n",
|
|
176
|
+
" token_data = response.json()\n",
|
|
177
|
+
" token_data['time_obtained'] = time.time()\n",
|
|
178
|
+
" else:\n",
|
|
179
|
+
" raise Exception(f\"Failed to obtain access token: {response.status_code} {response.text}\") \n",
|
|
180
|
+
" \n",
|
|
181
|
+
" self._save_json('token',token_data)\n",
|
|
182
|
+
" return token_data\n",
|
|
183
|
+
"\n",
|
|
184
|
+
" def _refresh_access_token(self,token_data,oauth_config): \n",
|
|
185
|
+
" \"\"\"\n",
|
|
186
|
+
" Refresh the OAuth access token using the refresh token.\n",
|
|
187
|
+
" if the refresh fails it will get a new token\n",
|
|
188
|
+
"\n",
|
|
189
|
+
" Args:\n",
|
|
190
|
+
" token_data (dict): The current token data containing at least the 'refresh_token'.\n",
|
|
191
|
+
" oauth_config (dict): The OAuth configuration dictionary with required client and endpoint information.\n",
|
|
192
|
+
"\n",
|
|
193
|
+
" Returns:\n",
|
|
194
|
+
" dict: The refreshed token data, including the new access token and updated 'time_obtained'.\n",
|
|
195
|
+
"\n",
|
|
196
|
+
" \"\"\"\n",
|
|
197
|
+
" token_url = oauth_config['TOKEN_URL']\n",
|
|
198
|
+
" # Set up the request payload\n",
|
|
199
|
+
" payload = {\n",
|
|
200
|
+
" 'grant_type': 'refresh_token',\n",
|
|
201
|
+
" 'client_id': oauth_config['CLIENT_ID'],\n",
|
|
202
|
+
" 'client_secret': oauth_config['CLIENT_SECRET'],\n",
|
|
203
|
+
" 'refresh_token': token_data['refresh_token']\n",
|
|
204
|
+
" }\n",
|
|
205
|
+
" # Make the request to the token endpoint\n",
|
|
206
|
+
" response = requests.post(token_url, data=payload)\n",
|
|
207
|
+
"\n",
|
|
208
|
+
" # Check if the request was successful\n",
|
|
209
|
+
" if response.status_code == 200:\n",
|
|
210
|
+
" token_data = response.json()\n",
|
|
211
|
+
" token_data['time_obtained'] = time.time()\n",
|
|
212
|
+
" self._save_json('token',token_data)\n",
|
|
213
|
+
" else:\n",
|
|
214
|
+
" print('Failed to refresh token, obtaining new token')\n",
|
|
215
|
+
" print('response:',response.status_code,response.text)\n",
|
|
216
|
+
" token_data = self._get_token(oauth_config)\n",
|
|
217
|
+
" \n",
|
|
218
|
+
" return token_data\n",
|
|
219
|
+
" \n",
|
|
220
|
+
" def _connect_to_hana(self):\n",
|
|
221
|
+
" \"\"\"\n",
|
|
222
|
+
" Establish a connection to the HANA Cloud database using OAuth authentication.\n",
|
|
223
|
+
"\n",
|
|
224
|
+
" This method reads the OAuth configuration and token data from JSON files in the application data directory.\n",
|
|
225
|
+
" If the token is missing or expired, it obtains or refreshes the token as needed.\n",
|
|
226
|
+
" Returns a database cursor for executing SQL commands.\n",
|
|
227
|
+
"\n",
|
|
228
|
+
" Returns:\n",
|
|
229
|
+
" hdbcli.dbapi.Cursor: A cursor object connected to the HANA Cloud database.\n",
|
|
230
|
+
"\n",
|
|
231
|
+
" Raises:\n",
|
|
232
|
+
" Exception: If the configuration file is missing or invalid.\n",
|
|
233
|
+
" \"\"\"\n",
|
|
234
|
+
" oauth_config,token_data = self._read_json()\n",
|
|
235
|
+
" if oauth_config is None:\n",
|
|
236
|
+
" raise Exception('No config file found, please run configuration first')\n",
|
|
237
|
+
" elif token_data is None:\n",
|
|
238
|
+
" token_data = self._get_token(oauth_config)\n",
|
|
239
|
+
"\n",
|
|
240
|
+
"\n",
|
|
241
|
+
" expiry_timestamp = token_data['time_obtained'] + token_data['expires_in']\n",
|
|
242
|
+
" expiry_datetime = datetime.datetime.fromtimestamp(expiry_timestamp)\n",
|
|
243
|
+
" print(\"Token expires at:\", expiry_datetime)\n",
|
|
244
|
+
" # Refresh token if it will expire in less than 20 minutes (1200 seconds)\n",
|
|
245
|
+
"\n",
|
|
246
|
+
" if time.time() > expiry_timestamp:\n",
|
|
247
|
+
" print('Token has expired, obtaining new token')\n",
|
|
248
|
+
" token_data = self._get_token(oauth_config)\n",
|
|
249
|
+
" elif expiry_timestamp - time.time() < 1200:\n",
|
|
250
|
+
" print('Token will expire in less than 20 minutes, refreshing token')\n",
|
|
251
|
+
" token_data = self._refresh_access_token(token_data, oauth_config)\n",
|
|
252
|
+
" \n",
|
|
253
|
+
" return dbapi.connect(address=oauth_config['HC_prod_URL'],port='443',authenticationMethods='jwt',password=token_data['access_token'],encrypt=True, sslValidateCertificate=True).cursor()\n",
|
|
254
|
+
"\n",
|
|
255
|
+
" def hana_sql(self, sql_command='validate',DF_type_local_override = None):\n",
|
|
256
|
+
" \"\"\"\n",
|
|
257
|
+
" Execute a SQL command on the HANA Cloud database and return the result as a DataFrame.\n",
|
|
258
|
+
"\n",
|
|
259
|
+
" Args:\n",
|
|
260
|
+
" sql_command (str, optional): The SQL command to execute. Defaults to 'validate', which only checks the connection.\n",
|
|
261
|
+
" DF_type_local_override (str, optional): DataFrame type for this query ('pandas' or 'polars'). Overrides the global setting if provided.\n",
|
|
262
|
+
"\n",
|
|
263
|
+
" Returns:\n",
|
|
264
|
+
" str: If sql_command is 'validate', returns a validation message.\n",
|
|
265
|
+
" pandas.DataFrame or polars.DataFrame: The query result as a DataFrame, depending on the specified or default DataFrame type.\n",
|
|
266
|
+
"\n",
|
|
267
|
+
" Raises:\n",
|
|
268
|
+
" ValueError: If the DataFrame type is not specified or is invalid.\n",
|
|
269
|
+
" \"\"\"\n",
|
|
270
|
+
" \n",
|
|
271
|
+
" cursor = self._connect_to_hana()\n",
|
|
272
|
+
" if sql_command == 'validate':\n",
|
|
273
|
+
" return 'Connection successful validated'\n",
|
|
274
|
+
" \n",
|
|
275
|
+
" if DF_type_local_override is not None:\n",
|
|
276
|
+
" DF_type = DF_type_local_override\n",
|
|
277
|
+
" else:\n",
|
|
278
|
+
" DF_type = self.DF_type_global_override\n",
|
|
279
|
+
" \n",
|
|
280
|
+
" cursor.execute(sql_command) # Run SQL command\n",
|
|
281
|
+
" # Retrieve data and convert it to a pandas data frame\n",
|
|
282
|
+
" data = cursor.fetchall() \n",
|
|
283
|
+
" data_name = [i[0] for i in cursor.description] \n",
|
|
284
|
+
" cursor.close() \n",
|
|
285
|
+
" \n",
|
|
286
|
+
" if DF_type == 'pandas':\n",
|
|
287
|
+
"\n",
|
|
288
|
+
" return pd.DataFrame(data,columns=data_name)\n",
|
|
289
|
+
" elif DF_type == 'polars':\n",
|
|
290
|
+
" data = [row.column_values for row in data]\n",
|
|
291
|
+
" return pl.DataFrame(data,orient='row',schema=data_name)\n",
|
|
292
|
+
" else:\n",
|
|
293
|
+
" raise ValueError(\"DF_type must be either 'pandas' or 'polars' needs to be defined when you initialise the class or pass it to the hana_sql function\")\n",
|
|
294
|
+
" \n",
|
|
295
|
+
" \n",
|
|
296
|
+
" def hana_upload(self,data, data_name, SCHEMA):\n",
|
|
297
|
+
" \"\"\"\n",
|
|
298
|
+
" Upload a DataFrame to the HANA Cloud database.\n",
|
|
299
|
+
"\n",
|
|
300
|
+
" Args:\n",
|
|
301
|
+
" data (pandas.DataFrame or polars.DataFrame): The data to upload. If a polars DataFrame is provided, it will be converted to pandas.\n",
|
|
302
|
+
" data_name (str): The name of the table to create or replace in HANA Cloud.\n",
|
|
303
|
+
" SCHEMA (str): The schema in which to create the table.\n",
|
|
304
|
+
"\n",
|
|
305
|
+
" Returns:\n",
|
|
306
|
+
" bool: True if the upload is successful.\n",
|
|
307
|
+
"\n",
|
|
308
|
+
" Raises:\n",
|
|
309
|
+
" Exception: If the upload fails due to connection or data issues.\n",
|
|
310
|
+
" \"\"\"\n",
|
|
311
|
+
" \n",
|
|
312
|
+
" # If the input data is a polars DataFrame, convert it to pandas\n",
|
|
313
|
+
" if isinstance(data, pl.DataFrame):\n",
|
|
314
|
+
" data = data.to_pandas()\n",
|
|
315
|
+
"\n",
|
|
316
|
+
"\n",
|
|
317
|
+
" # validates that the token still works\n",
|
|
318
|
+
" cursor = self._connect_to_hana()\n",
|
|
319
|
+
" cursor.close()\n",
|
|
320
|
+
"\n",
|
|
321
|
+
" oauth_config,token_data = self._read_json()\n",
|
|
322
|
+
" \n",
|
|
323
|
+
" conn = ConnectionContext(address=oauth_config['HC_prod_URL'],port='443',authenticationMethods='jwt',password=token_data['access_token'],encrypt=True, sslValidateCertificate=True) \n",
|
|
324
|
+
"\n",
|
|
325
|
+
" create_dataframe_from_pandas(conn, data, data_name,\n",
|
|
326
|
+
" schema=SCHEMA, \n",
|
|
327
|
+
" force=True, # True: truncate and insert\n",
|
|
328
|
+
" replace=True) # True: Null is replaced by 0\n",
|
|
329
|
+
" return True"
|
|
330
|
+
]
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
"cell_type": "code",
|
|
334
|
+
"execution_count": 3,
|
|
335
|
+
"id": "958cd124",
|
|
336
|
+
"metadata": {},
|
|
337
|
+
"outputs": [
|
|
338
|
+
{
|
|
339
|
+
"name": "stdout",
|
|
340
|
+
"output_type": "stream",
|
|
341
|
+
"text": [
|
|
342
|
+
"Token expires at: 2026-02-03 10:06:44.782597\n"
|
|
343
|
+
]
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
"data": {
|
|
347
|
+
"text/plain": [
|
|
348
|
+
"'Connection successful validated'"
|
|
349
|
+
]
|
|
350
|
+
},
|
|
351
|
+
"execution_count": 3,
|
|
352
|
+
"metadata": {},
|
|
353
|
+
"output_type": "execute_result"
|
|
354
|
+
}
|
|
355
|
+
],
|
|
356
|
+
"source": [
|
|
357
|
+
"hci = Hana_cloud_interface()\n",
|
|
358
|
+
"hci.hana_sql()"
|
|
359
|
+
]
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
"cell_type": "code",
|
|
363
|
+
"execution_count": null,
|
|
364
|
+
"id": "632c77a9",
|
|
365
|
+
"metadata": {},
|
|
366
|
+
"outputs": [
|
|
367
|
+
{
|
|
368
|
+
"name": "stdout",
|
|
369
|
+
"output_type": "stream",
|
|
370
|
+
"text": [
|
|
371
|
+
"Token expires at: 2026-02-02 19:15:41.178823\n"
|
|
372
|
+
]
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
"data": {
|
|
376
|
+
"text/html": [
|
|
377
|
+
"<div><style>\n",
|
|
378
|
+
".dataframe > thead > tr,\n",
|
|
379
|
+
".dataframe > tbody > tr {\n",
|
|
380
|
+
" text-align: right;\n",
|
|
381
|
+
" white-space: pre-wrap;\n",
|
|
382
|
+
"}\n",
|
|
383
|
+
"</style>\n",
|
|
384
|
+
"<small>shape: (12, 2)</small><table border=\"1\" class=\"dataframe\"><thead><tr><th>POWERCO_FINANCIAL_YEAR</th><th>ICP_DAILY_COUNT_AVERAGE_YTD</th></tr><tr><td>str</td><td>i64</td></tr></thead><tbody><tr><td>"2016-2017"</td><td>333925</td></tr><tr><td>"2023-2024"</td><td>359939</td></tr><tr><td>"2014-2015"</td><td>326597</td></tr><tr><td>"2021-2022"</td><td>352685</td></tr><tr><td>"2020-2021"</td><td>347964</td></tr><tr><td>…</td><td>…</td></tr><tr><td>"2025-2026"</td><td>364634</td></tr><tr><td>"2015-2016"</td><td>330630</td></tr><tr><td>"2022-2023"</td><td>356743</td></tr><tr><td>"2017-2018"</td><td>336728</td></tr><tr><td>"2019-2020"</td><td>344040</td></tr></tbody></table></div>"
|
|
385
|
+
],
|
|
386
|
+
"text/plain": [
|
|
387
|
+
"shape: (12, 2)\n",
|
|
388
|
+
"┌────────────────────────┬─────────────────────────────┐\n",
|
|
389
|
+
"│ POWERCO_FINANCIAL_YEAR ┆ ICP_DAILY_COUNT_AVERAGE_YTD │\n",
|
|
390
|
+
"│ --- ┆ --- │\n",
|
|
391
|
+
"│ str ┆ i64 │\n",
|
|
392
|
+
"╞════════════════════════╪═════════════════════════════╡\n",
|
|
393
|
+
"│ 2016-2017 ┆ 333925 │\n",
|
|
394
|
+
"│ 2023-2024 ┆ 359939 │\n",
|
|
395
|
+
"│ 2014-2015 ┆ 326597 │\n",
|
|
396
|
+
"│ 2021-2022 ┆ 352685 │\n",
|
|
397
|
+
"│ 2020-2021 ┆ 347964 │\n",
|
|
398
|
+
"│ … ┆ … │\n",
|
|
399
|
+
"│ 2025-2026 ┆ 364634 │\n",
|
|
400
|
+
"│ 2015-2016 ┆ 330630 │\n",
|
|
401
|
+
"│ 2022-2023 ┆ 356743 │\n",
|
|
402
|
+
"│ 2017-2018 ┆ 336728 │\n",
|
|
403
|
+
"│ 2019-2020 ┆ 344040 │\n",
|
|
404
|
+
"└────────────────────────┴─────────────────────────────┘"
|
|
405
|
+
]
|
|
406
|
+
},
|
|
407
|
+
"execution_count": 14,
|
|
408
|
+
"metadata": {},
|
|
409
|
+
"output_type": "execute_result"
|
|
410
|
+
}
|
|
411
|
+
],
|
|
412
|
+
"source": [
|
|
413
|
+
"hci = Hana_cloud_interface(df='polars')\n",
|
|
414
|
+
"\n",
|
|
415
|
+
"\n",
|
|
416
|
+
"q = '''\n",
|
|
417
|
+
" SELECT DISTINCT \n",
|
|
418
|
+
" \"POWERCO_FINANCIAL_YEAR\",\n",
|
|
419
|
+
" \"ICPDAILYCOUNTAVERAGEYTD\" as \"ICP_DAILY_COUNT_AVERAGE_YTD\"\n",
|
|
420
|
+
" FROM \"ZPUBLIC\".\"ZV_BW4_D_F_MDS_ICP_COUNT\"\n",
|
|
421
|
+
" WHERE (\"LATESTINFYDATEIND\" = '1' ) \n",
|
|
422
|
+
"'''\n",
|
|
423
|
+
"\n",
|
|
424
|
+
"hci.hana_sql(q)"
|
|
425
|
+
]
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
"cell_type": "code",
|
|
429
|
+
"execution_count": 4,
|
|
430
|
+
"id": "092fe8a5",
|
|
431
|
+
"metadata": {},
|
|
432
|
+
"outputs": [
|
|
433
|
+
{
|
|
434
|
+
"name": "stdout",
|
|
435
|
+
"output_type": "stream",
|
|
436
|
+
"text": [
|
|
437
|
+
"Token expires at: 2026-02-02 15:00:38.586668\n"
|
|
438
|
+
]
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
"data": {
|
|
442
|
+
"text/html": [
|
|
443
|
+
"<div>\n",
|
|
444
|
+
"<style scoped>\n",
|
|
445
|
+
" .dataframe tbody tr th:only-of-type {\n",
|
|
446
|
+
" vertical-align: middle;\n",
|
|
447
|
+
" }\n",
|
|
448
|
+
"\n",
|
|
449
|
+
" .dataframe tbody tr th {\n",
|
|
450
|
+
" vertical-align: top;\n",
|
|
451
|
+
" }\n",
|
|
452
|
+
"\n",
|
|
453
|
+
" .dataframe thead th {\n",
|
|
454
|
+
" text-align: right;\n",
|
|
455
|
+
" }\n",
|
|
456
|
+
"</style>\n",
|
|
457
|
+
"<table border=\"1\" class=\"dataframe\">\n",
|
|
458
|
+
" <thead>\n",
|
|
459
|
+
" <tr style=\"text-align: right;\">\n",
|
|
460
|
+
" <th></th>\n",
|
|
461
|
+
" <th>POWERCO_FINANCIAL_YEAR</th>\n",
|
|
462
|
+
" <th>ICP_DAILY_COUNT_AVERAGE_YTD</th>\n",
|
|
463
|
+
" </tr>\n",
|
|
464
|
+
" </thead>\n",
|
|
465
|
+
" <tbody>\n",
|
|
466
|
+
" <tr>\n",
|
|
467
|
+
" <th>0</th>\n",
|
|
468
|
+
" <td>2016-2017</td>\n",
|
|
469
|
+
" <td>333925</td>\n",
|
|
470
|
+
" </tr>\n",
|
|
471
|
+
" <tr>\n",
|
|
472
|
+
" <th>1</th>\n",
|
|
473
|
+
" <td>2023-2024</td>\n",
|
|
474
|
+
" <td>359939</td>\n",
|
|
475
|
+
" </tr>\n",
|
|
476
|
+
" <tr>\n",
|
|
477
|
+
" <th>2</th>\n",
|
|
478
|
+
" <td>2014-2015</td>\n",
|
|
479
|
+
" <td>326597</td>\n",
|
|
480
|
+
" </tr>\n",
|
|
481
|
+
" <tr>\n",
|
|
482
|
+
" <th>3</th>\n",
|
|
483
|
+
" <td>2021-2022</td>\n",
|
|
484
|
+
" <td>352685</td>\n",
|
|
485
|
+
" </tr>\n",
|
|
486
|
+
" <tr>\n",
|
|
487
|
+
" <th>4</th>\n",
|
|
488
|
+
" <td>2020-2021</td>\n",
|
|
489
|
+
" <td>347964</td>\n",
|
|
490
|
+
" </tr>\n",
|
|
491
|
+
" <tr>\n",
|
|
492
|
+
" <th>5</th>\n",
|
|
493
|
+
" <td>2018-2019</td>\n",
|
|
494
|
+
" <td>340379</td>\n",
|
|
495
|
+
" </tr>\n",
|
|
496
|
+
" <tr>\n",
|
|
497
|
+
" <th>6</th>\n",
|
|
498
|
+
" <td>2024-2025</td>\n",
|
|
499
|
+
" <td>362455</td>\n",
|
|
500
|
+
" </tr>\n",
|
|
501
|
+
" <tr>\n",
|
|
502
|
+
" <th>7</th>\n",
|
|
503
|
+
" <td>2025-2026</td>\n",
|
|
504
|
+
" <td>364634</td>\n",
|
|
505
|
+
" </tr>\n",
|
|
506
|
+
" <tr>\n",
|
|
507
|
+
" <th>8</th>\n",
|
|
508
|
+
" <td>2015-2016</td>\n",
|
|
509
|
+
" <td>330630</td>\n",
|
|
510
|
+
" </tr>\n",
|
|
511
|
+
" <tr>\n",
|
|
512
|
+
" <th>9</th>\n",
|
|
513
|
+
" <td>2022-2023</td>\n",
|
|
514
|
+
" <td>356743</td>\n",
|
|
515
|
+
" </tr>\n",
|
|
516
|
+
" <tr>\n",
|
|
517
|
+
" <th>10</th>\n",
|
|
518
|
+
" <td>2017-2018</td>\n",
|
|
519
|
+
" <td>336728</td>\n",
|
|
520
|
+
" </tr>\n",
|
|
521
|
+
" <tr>\n",
|
|
522
|
+
" <th>11</th>\n",
|
|
523
|
+
" <td>2019-2020</td>\n",
|
|
524
|
+
" <td>344040</td>\n",
|
|
525
|
+
" </tr>\n",
|
|
526
|
+
" </tbody>\n",
|
|
527
|
+
"</table>\n",
|
|
528
|
+
"</div>"
|
|
529
|
+
],
|
|
530
|
+
"text/plain": [
|
|
531
|
+
" POWERCO_FINANCIAL_YEAR ICP_DAILY_COUNT_AVERAGE_YTD\n",
|
|
532
|
+
"0 2016-2017 333925\n",
|
|
533
|
+
"1 2023-2024 359939\n",
|
|
534
|
+
"2 2014-2015 326597\n",
|
|
535
|
+
"3 2021-2022 352685\n",
|
|
536
|
+
"4 2020-2021 347964\n",
|
|
537
|
+
"5 2018-2019 340379\n",
|
|
538
|
+
"6 2024-2025 362455\n",
|
|
539
|
+
"7 2025-2026 364634\n",
|
|
540
|
+
"8 2015-2016 330630\n",
|
|
541
|
+
"9 2022-2023 356743\n",
|
|
542
|
+
"10 2017-2018 336728\n",
|
|
543
|
+
"11 2019-2020 344040"
|
|
544
|
+
]
|
|
545
|
+
},
|
|
546
|
+
"execution_count": 4,
|
|
547
|
+
"metadata": {},
|
|
548
|
+
"output_type": "execute_result"
|
|
549
|
+
}
|
|
550
|
+
],
|
|
551
|
+
"source": [
|
|
552
|
+
"hci = Hana_cloud_interface()\n",
|
|
553
|
+
"\n",
|
|
554
|
+
"q = '''\n",
|
|
555
|
+
" SELECT DISTINCT \n",
|
|
556
|
+
" \"POWERCO_FINANCIAL_YEAR\",\n",
|
|
557
|
+
" \"ICPDAILYCOUNTAVERAGEYTD\" as \"ICP_DAILY_COUNT_AVERAGE_YTD\"\n",
|
|
558
|
+
" FROM \"ZPUBLIC\".\"ZV_BW4_D_F_MDS_ICP_COUNT\"\n",
|
|
559
|
+
" WHERE (\"LATESTINFYDATEIND\" = '1' ) \n",
|
|
560
|
+
"'''\n",
|
|
561
|
+
"\n",
|
|
562
|
+
"hci.hana_sql(q,'pandas')"
|
|
563
|
+
]
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
"cell_type": "code",
|
|
567
|
+
"execution_count": 7,
|
|
568
|
+
"id": "c3632643",
|
|
569
|
+
"metadata": {},
|
|
570
|
+
"outputs": [
|
|
571
|
+
{
|
|
572
|
+
"name": "stdout",
|
|
573
|
+
"output_type": "stream",
|
|
574
|
+
"text": [
|
|
575
|
+
"Token expires at: 2026-02-02 15:00:38.586668\n"
|
|
576
|
+
]
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
"data": {
|
|
580
|
+
"text/html": [
|
|
581
|
+
"<div><style>\n",
|
|
582
|
+
".dataframe > thead > tr,\n",
|
|
583
|
+
".dataframe > tbody > tr {\n",
|
|
584
|
+
" text-align: right;\n",
|
|
585
|
+
" white-space: pre-wrap;\n",
|
|
586
|
+
"}\n",
|
|
587
|
+
"</style>\n",
|
|
588
|
+
"<small>shape: (12, 2)</small><table border=\"1\" class=\"dataframe\"><thead><tr><th>POWERCO_FINANCIAL_YEAR</th><th>ICP_DAILY_COUNT_AVERAGE_YTD</th></tr><tr><td>str</td><td>i64</td></tr></thead><tbody><tr><td>"2016-2017"</td><td>333925</td></tr><tr><td>"2023-2024"</td><td>359939</td></tr><tr><td>"2014-2015"</td><td>326597</td></tr><tr><td>"2021-2022"</td><td>352685</td></tr><tr><td>"2020-2021"</td><td>347964</td></tr><tr><td>…</td><td>…</td></tr><tr><td>"2025-2026"</td><td>364634</td></tr><tr><td>"2015-2016"</td><td>330630</td></tr><tr><td>"2022-2023"</td><td>356743</td></tr><tr><td>"2017-2018"</td><td>336728</td></tr><tr><td>"2019-2020"</td><td>344040</td></tr></tbody></table></div>"
|
|
589
|
+
],
|
|
590
|
+
"text/plain": [
|
|
591
|
+
"shape: (12, 2)\n",
|
|
592
|
+
"┌────────────────────────┬─────────────────────────────┐\n",
|
|
593
|
+
"│ POWERCO_FINANCIAL_YEAR ┆ ICP_DAILY_COUNT_AVERAGE_YTD │\n",
|
|
594
|
+
"│ --- ┆ --- │\n",
|
|
595
|
+
"│ str ┆ i64 │\n",
|
|
596
|
+
"╞════════════════════════╪═════════════════════════════╡\n",
|
|
597
|
+
"│ 2016-2017 ┆ 333925 │\n",
|
|
598
|
+
"│ 2023-2024 ┆ 359939 │\n",
|
|
599
|
+
"│ 2014-2015 ┆ 326597 │\n",
|
|
600
|
+
"│ 2021-2022 ┆ 352685 │\n",
|
|
601
|
+
"│ 2020-2021 ┆ 347964 │\n",
|
|
602
|
+
"│ … ┆ … │\n",
|
|
603
|
+
"│ 2025-2026 ┆ 364634 │\n",
|
|
604
|
+
"│ 2015-2016 ┆ 330630 │\n",
|
|
605
|
+
"│ 2022-2023 ┆ 356743 │\n",
|
|
606
|
+
"│ 2017-2018 ┆ 336728 │\n",
|
|
607
|
+
"│ 2019-2020 ┆ 344040 │\n",
|
|
608
|
+
"└────────────────────────┴─────────────────────────────┘"
|
|
609
|
+
]
|
|
610
|
+
},
|
|
611
|
+
"execution_count": 7,
|
|
612
|
+
"metadata": {},
|
|
613
|
+
"output_type": "execute_result"
|
|
614
|
+
}
|
|
615
|
+
],
|
|
616
|
+
"source": [
|
|
617
|
+
"hci = Hana_cloud_interface()\n",
|
|
618
|
+
"\n",
|
|
619
|
+
"q = '''\n",
|
|
620
|
+
" SELECT DISTINCT \n",
|
|
621
|
+
" \"POWERCO_FINANCIAL_YEAR\",\n",
|
|
622
|
+
" \"ICPDAILYCOUNTAVERAGEYTD\" as \"ICP_DAILY_COUNT_AVERAGE_YTD\"\n",
|
|
623
|
+
" FROM \"ZPUBLIC\".\"ZV_BW4_D_F_MDS_ICP_COUNT\"\n",
|
|
624
|
+
" WHERE (\"LATESTINFYDATEIND\" = '1' ) \n",
|
|
625
|
+
"'''\n",
|
|
626
|
+
"\n",
|
|
627
|
+
"hci.hana_sql(q,'polars')"
|
|
628
|
+
]
|
|
629
|
+
}
|
|
630
|
+
],
|
|
631
|
+
"metadata": {
|
|
632
|
+
"kernelspec": {
|
|
633
|
+
"display_name": "hana-cloud-interface (3.13.5)",
|
|
634
|
+
"language": "python",
|
|
635
|
+
"name": "python3"
|
|
636
|
+
},
|
|
637
|
+
"language_info": {
|
|
638
|
+
"codemirror_mode": {
|
|
639
|
+
"name": "ipython",
|
|
640
|
+
"version": 3
|
|
641
|
+
},
|
|
642
|
+
"file_extension": ".py",
|
|
643
|
+
"mimetype": "text/x-python",
|
|
644
|
+
"name": "python",
|
|
645
|
+
"nbconvert_exporter": "python",
|
|
646
|
+
"pygments_lexer": "ipython3",
|
|
647
|
+
"version": "3.13.5"
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
"nbformat": 4,
|
|
651
|
+
"nbformat_minor": 5
|
|
652
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: hana-cloud-interface
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Runs a SQL command on SAP HANA Cloud using OAuth single sign on and returns a pandas or polars dataframe
|
|
5
|
+
Author: charlotte corpe
|
|
6
|
+
Author-email: charlotte corpe <charlotte.corpe@powerco.co.nz>
|
|
7
|
+
Requires-Dist: hdbcli
|
|
8
|
+
Requires-Dist: polars
|
|
9
|
+
Requires-Dist: pandas
|
|
10
|
+
Requires-Dist: hana-ml
|
|
11
|
+
Requires-Dist: platformdirs
|
|
12
|
+
Requires-Dist: selenium
|
|
13
|
+
Requires-Dist: webdriver-manager
|
|
14
|
+
Requires-Dist: ipykernel
|
|
15
|
+
Requires-Dist: requests-oauthlib
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# Usage of the `hana_cloud_interface` package
|
|
20
|
+
|
|
21
|
+
This package provides a simple interface to connect to SAP HANA Cloud databases and execute SQL queries. Below are some examples of how to use the package.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## initialising class
|
|
25
|
+
when initialising the class it has two optional args.
|
|
26
|
+
|
|
27
|
+
- config_file : Path to the configuration file (JSON format) containing OAuth credentials and other settings. the config file only needs to be added on the first time running the class on a computer.
|
|
28
|
+
|
|
29
|
+
- data_frame_type : Default data frame type for SQL query results. Options are 'pandas' or 'polars'. Default is 'pandas'.
|
|
30
|
+
if the data type is not added here it will need to be added for every time you run hana_sql function
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
Hana_cloud_interface(config_file = 'location of configuration file', df = 'pandas')
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## hana_sql() function
|
|
37
|
+
Execute a SQL command on the HANA Cloud database and return the result as a DataFrame.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
sql_command (str, optional): The SQL command to execute. Defaults to 'validate', which only checks the connection.
|
|
41
|
+
DF_type_local_override (str, optional): DataFrame type for this query ('pandas' or 'polars'). Overrides the global setting if provided.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
str: If sql_command is 'validate', returns a validation message.
|
|
45
|
+
pandas.DataFrame or polars.DataFrame: The query result as a DataFrame, depending on the specified or default DataFrame type.
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
ValueError: If the DataFrame type is not specified or is invalid.
|
|
49
|
+
|
|
50
|
+
## hana_upload() function
|
|
51
|
+
Upload a DataFrame to the HANA Cloud database.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
data (pandas.DataFrame or polars.DataFrame): The data to upload. If a polars DataFrame is provided, it will be converted to pandas.
|
|
55
|
+
data_name (str): The name of the table to create or replace in HANA Cloud.
|
|
56
|
+
SCHEMA (str): The schema in which to create the table.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
bool: True if the upload is successful.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
Exception: If the upload fails due to connection or data issues.
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
## configuration file
|
|
66
|
+
the configuration file is a .json file
|
|
67
|
+
```python
|
|
68
|
+
{
|
|
69
|
+
"CLIENT_ID": "",
|
|
70
|
+
"CLIENT_SECRET": "",
|
|
71
|
+
"AUTH_URL": "",
|
|
72
|
+
"TOKEN_URL": "",
|
|
73
|
+
"protected_url": "",
|
|
74
|
+
"REDIRECT_URI": "",
|
|
75
|
+
"SCOPE": "",
|
|
76
|
+
"HC_prod_URL": ""
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## example
|
|
81
|
+
the main function is very simple It takes a SQL command as a string and returns the data
|
|
82
|
+
```python
|
|
83
|
+
import hana_cloud_interface
|
|
84
|
+
|
|
85
|
+
# initialise class
|
|
86
|
+
hci = Hana_cloud_interface(df='polars')
|
|
87
|
+
|
|
88
|
+
sql_command = """
|
|
89
|
+
SELECT top 10
|
|
90
|
+
"data1"
|
|
91
|
+
"data2"
|
|
92
|
+
FROM "table1"
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
data = hci.hana_sql(sql_command)
|
|
96
|
+
|
|
97
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
hana_cloud_interface/__init__.py,sha256=eb53b0e75f591672241b2e92afd37eb22cd7e83681dd006b95a6d834d508eb9a,38
|
|
2
|
+
hana_cloud_interface/main.py,sha256=3585cf1c79abb9c7264402475d3bcad6f3e5696985126751d8c0644895ed96f7,13220
|
|
3
|
+
hana_cloud_interface/py.typed,sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855,0
|
|
4
|
+
hana_cloud_interface/test_doc.ipynb,sha256=6cb75af297c2df27423734d4297a80586f0f940e2cdd30516a3de8c1df080b51,27979
|
|
5
|
+
hana_cloud_interface-0.3.0.dist-info/WHEEL,sha256=b70116f4076fa664af162441d2ba3754dbb4ec63e09d563bdc1e9ab023cce400,78
|
|
6
|
+
hana_cloud_interface-0.3.0.dist-info/entry_points.txt,sha256=917e7d5e3aebcdb046ecc05aa1867cf208548aa5d2277b5714f205596b96b4e9,88
|
|
7
|
+
hana_cloud_interface-0.3.0.dist-info/METADATA,sha256=4c0cfeaf52b1bc3c9e42ae57078bafa33322bd5d598dbae47d89c826dba7ee49,3170
|
|
8
|
+
hana_cloud_interface-0.3.0.dist-info/RECORD,,
|