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.
@@ -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>&quot;2016-2017&quot;</td><td>333925</td></tr><tr><td>&quot;2023-2024&quot;</td><td>359939</td></tr><tr><td>&quot;2014-2015&quot;</td><td>326597</td></tr><tr><td>&quot;2021-2022&quot;</td><td>352685</td></tr><tr><td>&quot;2020-2021&quot;</td><td>347964</td></tr><tr><td>&hellip;</td><td>&hellip;</td></tr><tr><td>&quot;2025-2026&quot;</td><td>364634</td></tr><tr><td>&quot;2015-2016&quot;</td><td>330630</td></tr><tr><td>&quot;2022-2023&quot;</td><td>356743</td></tr><tr><td>&quot;2017-2018&quot;</td><td>336728</td></tr><tr><td>&quot;2019-2020&quot;</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>&quot;2016-2017&quot;</td><td>333925</td></tr><tr><td>&quot;2023-2024&quot;</td><td>359939</td></tr><tr><td>&quot;2014-2015&quot;</td><td>326597</td></tr><tr><td>&quot;2021-2022&quot;</td><td>352685</td></tr><tr><td>&quot;2020-2021&quot;</td><td>347964</td></tr><tr><td>&hellip;</td><td>&hellip;</td></tr><tr><td>&quot;2025-2026&quot;</td><td>364634</td></tr><tr><td>&quot;2015-2016&quot;</td><td>330630</td></tr><tr><td>&quot;2022-2023&quot;</td><td>356743</td></tr><tr><td>&quot;2017-2018&quot;</td><td>336728</td></tr><tr><td>&quot;2019-2020&quot;</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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.8.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ Reliability_Application_Manager = hana_cloud_interface.main:validate
3
+