dscin-ppy 0.0.1__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.
- dscin_ppy-0.0.1/PKG-INFO +12 -0
- dscin_ppy-0.0.1/README.md +0 -0
- dscin_ppy-0.0.1/dscin_ppy/__init__.py +12 -0
- dscin_ppy-0.0.1/dscin_ppy/aws_utils.py +86 -0
- dscin_ppy-0.0.1/dscin_ppy/constants.py +10 -0
- dscin_ppy-0.0.1/dscin_ppy/data_extracts.py +234 -0
- dscin_ppy-0.0.1/dscin_ppy/db_utils.py +14 -0
- dscin_ppy-0.0.1/dscin_ppy/errors.py +2 -0
- dscin_ppy-0.0.1/dscin_ppy/etl_job.py +14 -0
- dscin_ppy-0.0.1/dscin_ppy/exposing_reports.py +55 -0
- dscin_ppy-0.0.1/dscin_ppy/functions.py +206 -0
- dscin_ppy-0.0.1/dscin_ppy/job_parameters.py +278 -0
- dscin_ppy-0.0.1/dscin_ppy.egg-info/PKG-INFO +12 -0
- dscin_ppy-0.0.1/dscin_ppy.egg-info/SOURCES.txt +17 -0
- dscin_ppy-0.0.1/dscin_ppy.egg-info/dependency_links.txt +1 -0
- dscin_ppy-0.0.1/dscin_ppy.egg-info/requires.txt +4 -0
- dscin_ppy-0.0.1/dscin_ppy.egg-info/top_level.txt +1 -0
- dscin_ppy-0.0.1/setup.cfg +4 -0
- dscin_ppy-0.0.1/setup.py +16 -0
dscin_ppy-0.0.1/PKG-INFO
ADDED
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from functions import *
|
|
2
|
+
from db_utils import *
|
|
3
|
+
from etl_job import *
|
|
4
|
+
from aws_utils import *
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
bucket = os.getenv("S3_TRANSDICT_BUCKET")
|
|
8
|
+
key = os.getenv("S3_TRANSDICT_KEY")
|
|
9
|
+
|
|
10
|
+
download_object_from_s3(
|
|
11
|
+
bucket, key, "src/translation_dictionaries.py"
|
|
12
|
+
)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import boto3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def build_s3_url(bucket, key):
|
|
5
|
+
return f's3a://{bucket}/{key}'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def read_object_from_s3(bucket, key):
|
|
9
|
+
s3_client = boto3.client("s3")
|
|
10
|
+
try:
|
|
11
|
+
# Get the file inside the S3 Bucket
|
|
12
|
+
s3_response = s3_client.get_object(Bucket=bucket, Key=key)
|
|
13
|
+
return s3_response["Body"].read()
|
|
14
|
+
except s3_client.exceptions.NoSuchBucket as e:
|
|
15
|
+
print("The S3 bucket does not exist.")
|
|
16
|
+
print(e)
|
|
17
|
+
except s3_client.exceptions.NoSuchKey as e:
|
|
18
|
+
print("The S3 objects does not exist in the S3 bucket.")
|
|
19
|
+
print(e)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def open_object_from_s3(bucket, key):
|
|
23
|
+
s3_client = boto3.client("s3")
|
|
24
|
+
try:
|
|
25
|
+
# Get the file inside the S3 Bucket
|
|
26
|
+
s3_response = s3_client.get_object(Bucket=bucket, Key=key)
|
|
27
|
+
return s3_response["Body"]
|
|
28
|
+
except s3_client.exceptions.NoSuchBucket as e:
|
|
29
|
+
print("The S3 bucket does not exist.")
|
|
30
|
+
print(e)
|
|
31
|
+
except s3_client.exceptions.NoSuchKey as e:
|
|
32
|
+
print("The S3 objects does not exist in the S3 bucket.")
|
|
33
|
+
print(e)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def write_dataframe_to_s3(dataframe, s3_url, file_type):
|
|
37
|
+
if file_type == "parquet":
|
|
38
|
+
dataframe.to_parquet(s3_url, engine='pyarrow', index=False)
|
|
39
|
+
elif file_type == "csv":
|
|
40
|
+
dataframe.to_csv(s3_url, index=False)
|
|
41
|
+
elif file_type == "xlsx":
|
|
42
|
+
dataframe.to_excel(s3_url, index=False)
|
|
43
|
+
else:
|
|
44
|
+
raise Exception("Format not supported")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_value_from_parameter_store(key, decrypt=False):
|
|
48
|
+
ssm_client = boto3.client("ssm")
|
|
49
|
+
value = ssm_client.get_parameter(Name=key, WithDecryption=decrypt)
|
|
50
|
+
return value["Parameter"]["Value"]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def save_to_s3(df, s3_bucket, s3_key, file_format):
|
|
54
|
+
s3_url = build_s3_url(s3_bucket, s3_key)
|
|
55
|
+
write_dataframe_to_s3(df, s3_url, file_format)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def copy_s3_object(copy_source, target_bucket, target_key):
|
|
59
|
+
s3_resource = boto3.resource("s3")
|
|
60
|
+
s3_bucket = s3_resource.Bucket(target_bucket)
|
|
61
|
+
s3_bucket.copy(copy_source, target_key)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def list_s3_objects(s3_bucket, s3_key_prefix):
|
|
65
|
+
s3_client = boto3.client("s3")
|
|
66
|
+
objects = s3_client.list_objects_v2(Bucket=s3_bucket, Prefix=s3_key_prefix)
|
|
67
|
+
# Extract the keys of the objects
|
|
68
|
+
objects_list = [
|
|
69
|
+
obj['Key'] for obj in objects.get('Contents', []) if obj
|
|
70
|
+
]
|
|
71
|
+
return objects_list
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def upload_object_to_s3(obj, s3_bucket, s3_key):
|
|
75
|
+
s3_client = boto3.client("s3")
|
|
76
|
+
s3_client.upload_fileobj(obj, s3_bucket, s3_key)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def remove_object_from_s3(bucket, key):
|
|
80
|
+
s3_client = boto3.client('s3')
|
|
81
|
+
s3_client.delete_object(Bucket=bucket, Key=key)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def download_object_from_s3(bucket, key, filename):
|
|
85
|
+
s3_client = boto3.client('s3')
|
|
86
|
+
s3_client.download_file(Bucket=bucket, Key=key, Filename=filename)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# INPUT PARAMS
|
|
2
|
+
ENV_PARAM = "env"
|
|
3
|
+
EXPORT_DATA_PARAM = "export_data"
|
|
4
|
+
LAST_X_DAYS_PARAM = "last_x_days"
|
|
5
|
+
SEASON_YEAR = "season_year"
|
|
6
|
+
HANA_SEASON = "hana_season"
|
|
7
|
+
FILE_NAME_PARAM = "file_name"
|
|
8
|
+
PREV_SEASONS_PARAM = "prev_seasons"
|
|
9
|
+
SEND_EMAIL ="send_email"
|
|
10
|
+
MODEL_NAME_EXT = "model_name_ext"
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# This module contains functions to save and load data
|
|
2
|
+
import os
|
|
3
|
+
import csv
|
|
4
|
+
import time
|
|
5
|
+
import pickle
|
|
6
|
+
import datetime
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
log = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def remove_special_charachers(text, special=" ", replace=None):
|
|
14
|
+
if replace is None:
|
|
15
|
+
text = text.replace(special, "")
|
|
16
|
+
else:
|
|
17
|
+
text = text.replace(special, replace)
|
|
18
|
+
return text
|
|
19
|
+
|
|
20
|
+
def construct_path(*path):
|
|
21
|
+
path = os.path.join(*path)
|
|
22
|
+
path = (
|
|
23
|
+
os.path.join(os.path.dirname(__file__), path)
|
|
24
|
+
if not os.path.isabs(path)
|
|
25
|
+
else path
|
|
26
|
+
)
|
|
27
|
+
return path
|
|
28
|
+
|
|
29
|
+
def write_pickle(obj, *path):
|
|
30
|
+
"""
|
|
31
|
+
Filepath in .pkl
|
|
32
|
+
"""
|
|
33
|
+
path = construct_path(*path)
|
|
34
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
35
|
+
with open(path, "wb") as f:
|
|
36
|
+
pickle.dump(obj, f, pickle.HIGHEST_PROTOCOL)
|
|
37
|
+
|
|
38
|
+
def read_pickle(*path):
|
|
39
|
+
path = construct_path(*path)
|
|
40
|
+
with open(path, "rb") as f:
|
|
41
|
+
return pickle.load(f)
|
|
42
|
+
|
|
43
|
+
def append_csv(dataframe, path):
|
|
44
|
+
os.path.isfile(path)
|
|
45
|
+
path = construct_path(path)
|
|
46
|
+
if not os.path.exists(os.path.dirname(path)):
|
|
47
|
+
os.makedirs(os.path.dirname(path))
|
|
48
|
+
if os.path.isfile(path):
|
|
49
|
+
dataframe.to_csv(path, index=False, mode="a", header=False, sep=";")
|
|
50
|
+
else:
|
|
51
|
+
dataframe.to_csv(path, index=False, mode="a", header=True, sep=";")
|
|
52
|
+
|
|
53
|
+
def write_csv(dataframe: pd.DataFrame, *path, **kwargs):
|
|
54
|
+
"""
|
|
55
|
+
Writes a DataFrame to disk as csv file, maintaining project standards.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
DataFrame (pandas.DataFrame): the DataFrame to save
|
|
59
|
+
path: file location
|
|
60
|
+
"""
|
|
61
|
+
path = construct_path(*path)
|
|
62
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
63
|
+
dataframe.to_csv(path, index=False, **kwargs)
|
|
64
|
+
|
|
65
|
+
def read_from_table(connection, table_name, cols_list=None, sum_list=None,
|
|
66
|
+
filter_condition=None, date_cols=None, extract_csv=False,
|
|
67
|
+
path="table_extraction.csv"):
|
|
68
|
+
"""
|
|
69
|
+
Function that extracts defined table
|
|
70
|
+
If {extract_csv} is True, Extracted file saved as csv to the defined {path}
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
- connection: pyhdb.connect(host, port, user, password)
|
|
74
|
+
- table_name: table name in the database
|
|
75
|
+
- col_list: list of columns to query
|
|
76
|
+
- sum_list: list of columns to sum from the aggregation
|
|
77
|
+
- filter_condition: string containing the filtering instruction
|
|
78
|
+
- date_cols: list of column date in the data
|
|
79
|
+
- extract_csv: Boolean to control whether to save extraction to csv
|
|
80
|
+
- path: path string where to save the extracted data to csv
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
- df (pd.DataFrame) extracted data
|
|
84
|
+
|
|
85
|
+
"""
|
|
86
|
+
try:
|
|
87
|
+
if cols_list is not None:
|
|
88
|
+
if sum_list is not None:
|
|
89
|
+
group_list = set(cols_list) - set(sum_list)
|
|
90
|
+
group_str = '","'.join(group_list)
|
|
91
|
+
group_str = '"' + group_str + '"'
|
|
92
|
+
cols_list = set(cols_list) - set(sum_list)
|
|
93
|
+
cols_string = '","'.join(cols_list)
|
|
94
|
+
cols_string = '"' + cols_string + '"'
|
|
95
|
+
|
|
96
|
+
if sum_list is not None:
|
|
97
|
+
sum_list = ['sum("' + s + '")' for s in sum_list]
|
|
98
|
+
sum_list = ",".join(sum_list)
|
|
99
|
+
cols_string = cols_string + "," + sum_list
|
|
100
|
+
else:
|
|
101
|
+
cols_string = "*"
|
|
102
|
+
|
|
103
|
+
if filter_condition is not None and sum_list is None:
|
|
104
|
+
extract_query = "SELECT {} FROM {} WHERE {}".format(
|
|
105
|
+
cols_string, table_name, filter_condition
|
|
106
|
+
)
|
|
107
|
+
elif filter_condition is not None and sum_list is not None:
|
|
108
|
+
extract_query = "SELECT {} FROM {} WHERE {} GROUP BY {}".format(
|
|
109
|
+
cols_string, table_name, filter_condition, group_str
|
|
110
|
+
)
|
|
111
|
+
elif filter_condition is None and sum_list is not None:
|
|
112
|
+
extract_query = "SELECT {} FROM {} GROUP BY {}".format(
|
|
113
|
+
cols_string, table_name, group_str
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
extract_query = "SELECT {} FROM {}".format(
|
|
117
|
+
cols_string, table_name
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
log.info(extract_query)
|
|
121
|
+
no_of_zerorecord_check:int = 1
|
|
122
|
+
while no_of_zerorecord_check != 0:
|
|
123
|
+
log.info(
|
|
124
|
+
str("no_of_zerorecord_check :" + str(no_of_zerorecord_check))
|
|
125
|
+
)
|
|
126
|
+
log.info(
|
|
127
|
+
str("Extraction start at :" + str(datetime.datetime.now()))
|
|
128
|
+
)
|
|
129
|
+
_data = pd.DataFrame()
|
|
130
|
+
data_list = []
|
|
131
|
+
|
|
132
|
+
if date_cols is None:
|
|
133
|
+
for chunk in pd.read_sql_query(extract_query, connection, chunksize=25000):
|
|
134
|
+
log.info(
|
|
135
|
+
str(
|
|
136
|
+
"Extracting Data from the table " +
|
|
137
|
+
table_name +
|
|
138
|
+
"as chunksize of 25000"
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
if extract_csv:
|
|
142
|
+
append_csv(chunk, path)
|
|
143
|
+
else:
|
|
144
|
+
data_list.append(chunk)
|
|
145
|
+
del chunk
|
|
146
|
+
else:
|
|
147
|
+
for chunk in pd.read_sql_query(extract_query, connection, chunksize=25000, parse_dates=date_cols):
|
|
148
|
+
log.info(
|
|
149
|
+
str(
|
|
150
|
+
"Extracting Data from the table " +
|
|
151
|
+
table_name +
|
|
152
|
+
"as chunksize of 25000"
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
if extract_csv:
|
|
156
|
+
append_csv(chunk, path)
|
|
157
|
+
else:
|
|
158
|
+
data_list.append(chunk)
|
|
159
|
+
del chunk
|
|
160
|
+
|
|
161
|
+
if extract_csv is False:
|
|
162
|
+
_data = pd.concat(data_list, ignore_index=True)
|
|
163
|
+
|
|
164
|
+
numOfRows = len(_data.index)
|
|
165
|
+
|
|
166
|
+
if numOfRows == 0:
|
|
167
|
+
# job sleeps for 1 minutes
|
|
168
|
+
log.info("Connection has been broken, wait for 10 secs")
|
|
169
|
+
time.sleep(10)
|
|
170
|
+
else:
|
|
171
|
+
break
|
|
172
|
+
|
|
173
|
+
no_of_zerorecord_check = no_of_zerorecord_check - 1
|
|
174
|
+
|
|
175
|
+
log.info(
|
|
176
|
+
str("Extraction end at :" + str(datetime.datetime.now()))
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return _data
|
|
180
|
+
|
|
181
|
+
except Exception as e:
|
|
182
|
+
raise e
|
|
183
|
+
|
|
184
|
+
def read_csv(file_path, sep=",", decimal=".", header=0, index_col=""):
|
|
185
|
+
try:
|
|
186
|
+
if os.path.isfile(file_path):
|
|
187
|
+
if index_col != "":
|
|
188
|
+
_data = pd.read_csv(
|
|
189
|
+
file_path,
|
|
190
|
+
sep=sep,
|
|
191
|
+
decimal=decimal,
|
|
192
|
+
header=header,
|
|
193
|
+
quoting=csv.QUOTE_NONE,
|
|
194
|
+
index_col=index_col,
|
|
195
|
+
dtype="str"
|
|
196
|
+
)
|
|
197
|
+
else:
|
|
198
|
+
_data = pd.read_csv(
|
|
199
|
+
file_path,
|
|
200
|
+
sep=sep,
|
|
201
|
+
decimal=decimal,
|
|
202
|
+
header=header,
|
|
203
|
+
quoting=csv.QUOTE_NONE,
|
|
204
|
+
dtype="str"
|
|
205
|
+
)
|
|
206
|
+
return _data
|
|
207
|
+
else:
|
|
208
|
+
raise Exception
|
|
209
|
+
except Exception:
|
|
210
|
+
raise
|
|
211
|
+
|
|
212
|
+
def read_excel(file_path, sheet_name=0, skiprows=None):
|
|
213
|
+
try:
|
|
214
|
+
if os.path.isfile(file_path):
|
|
215
|
+
_data = pd.read_excel(
|
|
216
|
+
file_path, sheet_name=sheet_name, skiprows=skiprows
|
|
217
|
+
)
|
|
218
|
+
return _data
|
|
219
|
+
else:
|
|
220
|
+
raise Exception
|
|
221
|
+
except Exception:
|
|
222
|
+
raise
|
|
223
|
+
|
|
224
|
+
def write_excel(dataframe: pd.DataFrame, *path, **kwargs):
|
|
225
|
+
"""
|
|
226
|
+
Writes a DataFrame to disk as excel file, maintaining project standards.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
DataFrame (pandas.DataFrame): the DataFrame to save
|
|
230
|
+
path: file location
|
|
231
|
+
"""
|
|
232
|
+
path = construct_path(*path)
|
|
233
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
234
|
+
dataframe.to_excel(path, index=False, **kwargs)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from hdbcli import dbapi
|
|
2
|
+
|
|
3
|
+
from config import db_config
|
|
4
|
+
|
|
5
|
+
def get_connection():
|
|
6
|
+
conn = dbapi.connect(
|
|
7
|
+
address=db_config.sap_hana_url,
|
|
8
|
+
port=db_config.sap_hana_port,
|
|
9
|
+
user=db_config.sap_hana_username,
|
|
10
|
+
password=db_config.sap_hana_password,
|
|
11
|
+
)
|
|
12
|
+
_ = conn.cursor()
|
|
13
|
+
|
|
14
|
+
return conn
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import job_parameters
|
|
2
|
+
|
|
3
|
+
class EtlJob:
|
|
4
|
+
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.params = job_parameters.gather_parameters()
|
|
7
|
+
|
|
8
|
+
def get_parameter(self, parameter, required: bool, default=None):
|
|
9
|
+
parameter_input_value = self.params.get(parameter.parameter_name)
|
|
10
|
+
parameter.validate(parameter_input_value, default, required)
|
|
11
|
+
return parameter.get_value_or_else(parameter_input_value, default)
|
|
12
|
+
|
|
13
|
+
def run(self):
|
|
14
|
+
pass
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from config.env_config import env_config
|
|
2
|
+
from aws_utils import copy_s3_object
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
log = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def copy_report_to_email_bucket(env_param, file_name, s3_key, datatype='cid'):
|
|
9
|
+
s3_bucket = env_config[env_param]["feature-store-bucket"]
|
|
10
|
+
s3_exp_bucket = env_config[env_param]["exposed-bucket"]
|
|
11
|
+
s3_email_root_key = env_config[env_param]["exposed-send-to-email-path"]
|
|
12
|
+
if datatype == 'cid':
|
|
13
|
+
report_prefix = env_config[env_param]["exposed-cid-report-prefix"]
|
|
14
|
+
elif datatype == 'cod':
|
|
15
|
+
report_prefix = env_config[env_param]["exposed-cod-report-prefix"]
|
|
16
|
+
elif datatype == 'd2s':
|
|
17
|
+
report_prefix = env_config[env_param]["exposed-demand-to-supply-prefix"]
|
|
18
|
+
else:
|
|
19
|
+
raise ValueError(f"unkwon data type {datatype}")
|
|
20
|
+
|
|
21
|
+
copy_source = {"Bucket": s3_bucket, "Key": s3_key}
|
|
22
|
+
exp_file_name = f"{report_prefix}_{file_name}"
|
|
23
|
+
s3_exp_key = f"{s3_email_root_key}/{exp_file_name}"
|
|
24
|
+
|
|
25
|
+
log.info(f"Started file upload {exp_file_name} to email bucket.")
|
|
26
|
+
copy_s3_object(copy_source, s3_exp_bucket, s3_exp_key)
|
|
27
|
+
log.info(f"Finished file upload {exp_file_name} to email bucket.")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def copy_report_to_tableau_bucket(env_param, file_name, s3_key):
|
|
31
|
+
s3_bucket = env_config[env_param]["feature-store-bucket"]
|
|
32
|
+
s3_exp_bucket = env_config[env_param]["exposed-bucket"]
|
|
33
|
+
s3_tableau_root_key = env_config[env_param]["exposed-send-to-tableau-path"]
|
|
34
|
+
|
|
35
|
+
copy_source = {"Bucket": s3_bucket, "Key": s3_key}
|
|
36
|
+
s3_exp_key = f"{s3_tableau_root_key}/{file_name}"
|
|
37
|
+
|
|
38
|
+
log.info(f"Started file upload {file_name} to tableau bucket.")
|
|
39
|
+
copy_s3_object(copy_source, s3_exp_bucket, s3_exp_key)
|
|
40
|
+
log.info(f"Finished file upload {file_name} to tableau bucket.")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def copy_report_to_email_bucket_marketplace(env_param, file_name, s3_key):
|
|
44
|
+
s3_bucket = env_config[env_param]["feature-store-bucket"]
|
|
45
|
+
s3_exp_bucket = env_config[env_param]["exposed-bucket"]
|
|
46
|
+
s3_email_root_key = env_config[env_param]["exposed-send-to-email-path"]
|
|
47
|
+
report_prefix = env_config[env_param]["exposed-cod-report-prefix_compact"]
|
|
48
|
+
|
|
49
|
+
copy_source = {"Bucket": s3_bucket, "Key": s3_key}
|
|
50
|
+
exp_file_name = f"{report_prefix}_{file_name}"
|
|
51
|
+
s3_exp_key = f"{s3_email_root_key}/{exp_file_name}"
|
|
52
|
+
|
|
53
|
+
log.info(f"Started file upload {exp_file_name} to email bucket.")
|
|
54
|
+
copy_s3_object(copy_source, s3_exp_bucket, s3_exp_key)
|
|
55
|
+
log.info(f"Finished file upload {exp_file_name} to email bucket.")
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from time import time
|
|
6
|
+
from functools import wraps
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def timing(f):
|
|
11
|
+
@wraps(f)
|
|
12
|
+
def wrap(*args, **kw):
|
|
13
|
+
ts = time()
|
|
14
|
+
result = f(*args, **kw)
|
|
15
|
+
te = time()
|
|
16
|
+
print('func:%r args:[%r, %r] took: %2.4f sec' % \
|
|
17
|
+
(f.__name__, args, kw, te-ts))
|
|
18
|
+
return result
|
|
19
|
+
return wrap
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def fillna_using_dtype(df):
|
|
23
|
+
column_types = df.dtypes.copy()
|
|
24
|
+
|
|
25
|
+
for k,v in df.dtypes.items():
|
|
26
|
+
if v == object:
|
|
27
|
+
column_types[k] = "#"
|
|
28
|
+
elif v == float or v == int:
|
|
29
|
+
column_types[k] = 0
|
|
30
|
+
elif v == datetime:
|
|
31
|
+
column_types[k] = pd.NaT
|
|
32
|
+
else:
|
|
33
|
+
column_types[k] = "#"
|
|
34
|
+
|
|
35
|
+
df.fillna(column_types, inplace=True)
|
|
36
|
+
|
|
37
|
+
return df
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def replace_comma_with_semicolon(x):
|
|
41
|
+
if isinstance(x, str):
|
|
42
|
+
return x.replace(',', ';')
|
|
43
|
+
else:
|
|
44
|
+
return x
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def extract_datetime_columns_from_dictionary(translation_dictionary):
|
|
48
|
+
"""
|
|
49
|
+
Function to extract from {translation_dictionary} the columns with
|
|
50
|
+
datetime type and return a list of the corresponding technical names
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
- translation_dictionary: (dict)
|
|
54
|
+
translation dictionary, consisting of key-value pairs
|
|
55
|
+
{technical column name: [human column name (str), column data type]}
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
- date_cols: (list[str])
|
|
59
|
+
list of all technical column names with data type datetime
|
|
60
|
+
|
|
61
|
+
Notes:
|
|
62
|
+
- An error is returned if {translation_dictionary} is empty
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
assert len(translation_dictionary) > 0, "translation_dictionary is empty"
|
|
66
|
+
|
|
67
|
+
date_cols = [
|
|
68
|
+
key for key, val in translation_dictionary.items() if val[1] == datetime
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
return date_cols
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def extract_dtype_columns_from_dictionary(translation_dictionary, data_type):
|
|
75
|
+
"""
|
|
76
|
+
Function to extract from {translation_dictionary} the columns with
|
|
77
|
+
{data_type} type and return a list of the corresponding technical names
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
- translation_dictionary: (dict)
|
|
81
|
+
translation dictionary, consisting of key-value pairs
|
|
82
|
+
{technical column name: [human column name (str), column data type]}
|
|
83
|
+
- data_type: (str)
|
|
84
|
+
data type of the desired columns (for example: str, float, int,...)
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
- data_type_cols: (list[str])
|
|
88
|
+
list of all technical column names with data type {data_type}
|
|
89
|
+
|
|
90
|
+
Notes:
|
|
91
|
+
- An error is returned if {translation_dictionary} is empty
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
assert len(translation_dictionary) > 0, "translation_dictionary is empty"
|
|
95
|
+
|
|
96
|
+
### Create a list of the d_non_technical_column_names that match data_type
|
|
97
|
+
data_type_cols = [
|
|
98
|
+
key for key, val in translation_dictionary.items() if val[1] == data_type
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
return data_type_cols
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def extract_column_translations_from_dictionary(translation_dictionary):
|
|
105
|
+
"""
|
|
106
|
+
Function to extract from {translation_dictionary} the technical names of
|
|
107
|
+
columns and their corresponding human-readable name and return a dictionary
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
- translation_dictionary: (dict)
|
|
111
|
+
translation dictionary, consisting of key-value pairs
|
|
112
|
+
{technical column name: [human column name (str), column data type]}
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
- column_translation_dictionary: (dict{str: str}])
|
|
116
|
+
dictionary mapping columns' technical and human-readable names
|
|
117
|
+
|
|
118
|
+
Notes:
|
|
119
|
+
- An error is returned if {translation_dictionary} is empty
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
assert len(translation_dictionary) > 0, "translation_dictionary is empty"
|
|
123
|
+
|
|
124
|
+
# Create a dictionary of technical_column_name: non_technical_column_name
|
|
125
|
+
column_translation_dictionary = {
|
|
126
|
+
key: val[0] for key, val in translation_dictionary.items()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return column_translation_dictionary
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def convert_column_dtype_to_str(df, translation_dictionary):
|
|
133
|
+
"""
|
|
134
|
+
Function to convert to string the columns of {df} using the
|
|
135
|
+
{translation_dictionary} as a lookup reference for data types
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
- df: (pd.DataFrame)
|
|
139
|
+
Pandas.DataFrame whose columns to convert to str
|
|
140
|
+
- translation_dictionary: (dict)
|
|
141
|
+
reference dictionary for looking up the columns with str data type
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
- df (pd.DataFrame):
|
|
145
|
+
staring Pandas.DataFrame with columns converted to str data type
|
|
146
|
+
|
|
147
|
+
Notes:
|
|
148
|
+
- An error is returned if {translation_dictionary} is empty
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
assert len(translation_dictionary) != 0, "translation_dictionary is empty"
|
|
152
|
+
|
|
153
|
+
### Extract the str columns from translation_dictionary and store in list
|
|
154
|
+
str_cols = extract_dtype_columns_from_dictionary(translation_dictionary, str)
|
|
155
|
+
str_cols = [col for col in str_cols if col in df.columns.tolist()]
|
|
156
|
+
|
|
157
|
+
if len(str_cols) != len(set(str_cols)):
|
|
158
|
+
raise ValueError("Duplicate column names found in str_cols.")
|
|
159
|
+
|
|
160
|
+
### Convert type
|
|
161
|
+
df[str_cols] = df[str_cols].astype(str)
|
|
162
|
+
|
|
163
|
+
return df
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def convert_column_dtype_to_numeric(df, translation_dictionary, data_type):
|
|
167
|
+
"""
|
|
168
|
+
Function to convert to numeric {data_type} the columns of {df} using
|
|
169
|
+
the {translation_dictionary} as a lookup reference for data types
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
- df: (pd.DataFrame)
|
|
173
|
+
Pandas.DataFrame whose columns to convert to {data_type}
|
|
174
|
+
- translation_dictionary: (dict)
|
|
175
|
+
reference dictionary for looking up the columns with {data_type} data type
|
|
176
|
+
- data_type: (str)
|
|
177
|
+
numeric data type object to which convert the columns of {df}
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
- df (pd.DataFrame):
|
|
181
|
+
staring Pandas.DataFrame with columns converted to {data_type} data type
|
|
182
|
+
|
|
183
|
+
Notes:
|
|
184
|
+
- Empty values are replaced with 0's
|
|
185
|
+
- {data_type} needs to be of type float or int
|
|
186
|
+
- An error is returned if {translation_dictionary} is empty
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
assert data_type == float or data_type == int, "data_type must be either int or float!"
|
|
190
|
+
assert len(translation_dictionary) > 0, "translation_dictionary is empty"
|
|
191
|
+
|
|
192
|
+
### Extract the int columns from translation_dictionary and store in list
|
|
193
|
+
num_cols = extract_dtype_columns_from_dictionary(translation_dictionary, data_type)
|
|
194
|
+
num_cols = [col for col in num_cols if col in df.columns.tolist()]
|
|
195
|
+
|
|
196
|
+
### Replace empty values in num_cols with 0
|
|
197
|
+
df[num_cols] = df[num_cols].replace(r"^\s*$", np.nan, regex=True)
|
|
198
|
+
df[num_cols] = df[num_cols].fillna(0)
|
|
199
|
+
|
|
200
|
+
### Convert type
|
|
201
|
+
if data_type == float:
|
|
202
|
+
df[num_cols] = df[num_cols].astype(float)
|
|
203
|
+
elif data_type == int:
|
|
204
|
+
df[num_cols] = df[num_cols].astype(float).astype(int)
|
|
205
|
+
|
|
206
|
+
return df
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
|
|
2
|
+
import argparse
|
|
3
|
+
|
|
4
|
+
import constants
|
|
5
|
+
from errors import ValidationError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Parameter:
|
|
9
|
+
|
|
10
|
+
def get_value_or_else(self, value, default):
|
|
11
|
+
return value or default
|
|
12
|
+
|
|
13
|
+
def validate(self, value, default, required):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
def cast_value(self, value):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EnvParameter(Parameter):
|
|
21
|
+
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
self.parameter_name = constants.ENV_PARAM
|
|
24
|
+
|
|
25
|
+
def validate(self, value, default, required):
|
|
26
|
+
if not value:
|
|
27
|
+
value = default
|
|
28
|
+
if not value and required:
|
|
29
|
+
raise ValidationError(
|
|
30
|
+
f"Parameter is required: {self.parameter_name}"
|
|
31
|
+
)
|
|
32
|
+
if value not in ['dev', 'stage', 'prod']:
|
|
33
|
+
raise ValidationError(
|
|
34
|
+
f"{self.parameter_name} available values: 'dev', 'stage', 'prod'"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def get_value_or_else(self, value, default):
|
|
38
|
+
value = super().get_value_or_else(value, default)
|
|
39
|
+
return self.cast_value(value)
|
|
40
|
+
|
|
41
|
+
def cast_value(self, value):
|
|
42
|
+
return str(value)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ExportDataParameter(Parameter):
|
|
46
|
+
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
self.parameter_name = constants.EXPORT_DATA_PARAM
|
|
49
|
+
|
|
50
|
+
def validate(self, value, default, required):
|
|
51
|
+
if not value:
|
|
52
|
+
value = default
|
|
53
|
+
if not value and required:
|
|
54
|
+
raise ValidationError(
|
|
55
|
+
f"Parameter is required: {self.parameter_name}"
|
|
56
|
+
)
|
|
57
|
+
if value not in ['True', 'False']:
|
|
58
|
+
raise ValidationError(
|
|
59
|
+
f"{self.parameter_name} available values: True/False"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def get_value_or_else(self, value, default):
|
|
63
|
+
value = super().get_value_or_else(value, default)
|
|
64
|
+
return self.cast_value(value)
|
|
65
|
+
|
|
66
|
+
# TODO: cast u boolean?
|
|
67
|
+
def cast_value(self, value):
|
|
68
|
+
return str(value)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class LastXDaysParameter(Parameter):
|
|
72
|
+
def __init__(self) -> None:
|
|
73
|
+
self.parameter_name = constants.LAST_X_DAYS_PARAM
|
|
74
|
+
|
|
75
|
+
def validate(self, value, default, required):
|
|
76
|
+
if not value:
|
|
77
|
+
value = default
|
|
78
|
+
if not value and required:
|
|
79
|
+
raise ValidationError(
|
|
80
|
+
f"Parameter is required: {self.parameter_name}"
|
|
81
|
+
)
|
|
82
|
+
if not value.isnumeric():
|
|
83
|
+
raise ValidationError(
|
|
84
|
+
f"Value of {self.parameter_name} must be a number."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def get_value_or_else(self, value, default):
|
|
88
|
+
value = super().get_value_or_else(value, default)
|
|
89
|
+
return self.cast_value(value)
|
|
90
|
+
|
|
91
|
+
def cast_value(self, value):
|
|
92
|
+
return str(value)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class SeasonYearParameter(Parameter):
|
|
96
|
+
def __init__(self) -> None:
|
|
97
|
+
self.parameter_name = constants.SEASON_YEAR
|
|
98
|
+
|
|
99
|
+
def validate(self, value, default, required):
|
|
100
|
+
if not value:
|
|
101
|
+
value = default
|
|
102
|
+
if not value and required:
|
|
103
|
+
raise ValidationError(
|
|
104
|
+
f"Parameter is required: {self.parameter_name}"
|
|
105
|
+
)
|
|
106
|
+
if not value.isnumeric():
|
|
107
|
+
raise ValidationError(
|
|
108
|
+
f"Value of {self.parameter_name} must be a number"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def get_value_or_else(self, value, default):
|
|
112
|
+
value = super().get_value_or_else(value, default)
|
|
113
|
+
return self.cast_value(value)
|
|
114
|
+
|
|
115
|
+
def cast_value(self, value):
|
|
116
|
+
return int(value)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class HanaSeasonParameter(Parameter):
|
|
120
|
+
def __init__(self) -> None:
|
|
121
|
+
self.parameter_name = constants.HANA_SEASON
|
|
122
|
+
def validate(self, value, default, required):
|
|
123
|
+
if not value:
|
|
124
|
+
value = default
|
|
125
|
+
if not value and required:
|
|
126
|
+
raise ValidationError(
|
|
127
|
+
f"Parameter is required: {self.parameter_name}"
|
|
128
|
+
)
|
|
129
|
+
if value not in ['WI', 'FA', 'SP', 'SU']:
|
|
130
|
+
raise ValidationError(
|
|
131
|
+
f"{self.parameter_name} available values: 'WI','SP','SU','FA'"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def get_value_or_else(self, value, default):
|
|
135
|
+
value = super().get_value_or_else(value, default)
|
|
136
|
+
return self.cast_value(value)
|
|
137
|
+
|
|
138
|
+
def cast_value(self, value):
|
|
139
|
+
return str(value)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class FileNameParameter(Parameter):
|
|
143
|
+
def __init__(self) -> None:
|
|
144
|
+
self.parameter_name = constants.FILE_NAME_PARAM
|
|
145
|
+
|
|
146
|
+
def validate(self, value, default, required):
|
|
147
|
+
if not value:
|
|
148
|
+
value = default
|
|
149
|
+
if not value and required:
|
|
150
|
+
raise ValidationError(
|
|
151
|
+
f"Parameter is required: {self.parameter_name}"
|
|
152
|
+
)
|
|
153
|
+
if not isinstance(value, str):
|
|
154
|
+
raise ValidationError(
|
|
155
|
+
f"{self.parameter_name} should be the name of the file in string format"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def get_value_or_else(self, value, default):
|
|
159
|
+
value = super().get_value_or_else(value, default)
|
|
160
|
+
return self.cast_value(value)
|
|
161
|
+
|
|
162
|
+
def cast_value(self, value):
|
|
163
|
+
return str(value)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class PreviousSeasonParameter(Parameter):
|
|
167
|
+
def __init__(self) -> None:
|
|
168
|
+
self.parameter_name = constants.PREV_SEASONS_PARAM
|
|
169
|
+
|
|
170
|
+
def validate(self, value, default, required):
|
|
171
|
+
if not value:
|
|
172
|
+
value = default
|
|
173
|
+
if not value and required:
|
|
174
|
+
raise ValidationError(
|
|
175
|
+
f"Parameter is required: {self.parameter_name}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def get_value_or_else(self, value, default):
|
|
179
|
+
value = super().get_value_or_else(value, default)
|
|
180
|
+
return self.cast_value(value)
|
|
181
|
+
|
|
182
|
+
def cast_value(self, value):
|
|
183
|
+
return int(value)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class SendEmailParameter(Parameter):
|
|
187
|
+
def __init__(self) -> None:
|
|
188
|
+
self.parameter_name = constants.SEND_EMAIL
|
|
189
|
+
|
|
190
|
+
def validate(self, value, default, required):
|
|
191
|
+
if value is None:
|
|
192
|
+
value = default
|
|
193
|
+
if not value and required:
|
|
194
|
+
raise ValidationError(
|
|
195
|
+
f"Parameter is required: {self.parameter_name}"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def get_value_or_else(self, value, default):
|
|
199
|
+
if value is None:
|
|
200
|
+
value = super().get_value_or_else(value, default)
|
|
201
|
+
return self.cast_value(value)
|
|
202
|
+
|
|
203
|
+
def cast_value(self, value):
|
|
204
|
+
return value
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class ModelNameExt(Parameter):
|
|
208
|
+
def __init__(self) -> None:
|
|
209
|
+
self.parameter_name = constants.MODEL_NAME_EXT
|
|
210
|
+
|
|
211
|
+
def validate(self, value, default, required):
|
|
212
|
+
if not value:
|
|
213
|
+
value = default
|
|
214
|
+
if not value and required:
|
|
215
|
+
raise ValidationError(
|
|
216
|
+
f"Parameter is required: {self.parameter_name}"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def get_value_or_else(self, value, default):
|
|
220
|
+
value = super().get_value_or_else(value, default)
|
|
221
|
+
return self.cast_value(value)
|
|
222
|
+
|
|
223
|
+
def cast_value(self, value):
|
|
224
|
+
return value
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def gather_parameters():
|
|
228
|
+
parser = argparse.ArgumentParser()
|
|
229
|
+
parser.add_argument(
|
|
230
|
+
f"--{constants.ENV_PARAM}",
|
|
231
|
+
help="environment in which job is running",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
parser.add_argument(
|
|
235
|
+
f"--{constants.EXPORT_DATA_PARAM}",
|
|
236
|
+
help="save data parameter",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
parser.add_argument(
|
|
240
|
+
f"--{constants.LAST_X_DAYS_PARAM}",
|
|
241
|
+
help="parameter to determine the number of days considered in the query",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
parser.add_argument(
|
|
245
|
+
f"--{constants.SEASON_YEAR}",
|
|
246
|
+
help="parameter to determine the current season year",
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
parser.add_argument(
|
|
250
|
+
f"--{constants.HANA_SEASON}",
|
|
251
|
+
help="parameter to determine the current Hana season",
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
parser.add_argument(
|
|
255
|
+
f"--{constants.FILE_NAME_PARAM}",
|
|
256
|
+
help="file name parameter",
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
parser.add_argument(
|
|
260
|
+
f"--{constants.PREV_SEASONS_PARAM}",
|
|
261
|
+
help="number of previous seasons to consider",
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
parser.add_argument(
|
|
265
|
+
f"--{constants.SEND_EMAIL}",
|
|
266
|
+
action="store_false",
|
|
267
|
+
help="DO NOT send out email with link to download file",
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
parser.add_argument(
|
|
271
|
+
f"--{constants.MODEL_NAME_EXT}",
|
|
272
|
+
help="suffix to append to model name",
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
args = parser.parse_args()
|
|
276
|
+
params_dictionary = vars(args)
|
|
277
|
+
|
|
278
|
+
return params_dictionary
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.py
|
|
3
|
+
dscin_ppy/__init__.py
|
|
4
|
+
dscin_ppy/aws_utils.py
|
|
5
|
+
dscin_ppy/constants.py
|
|
6
|
+
dscin_ppy/data_extracts.py
|
|
7
|
+
dscin_ppy/db_utils.py
|
|
8
|
+
dscin_ppy/errors.py
|
|
9
|
+
dscin_ppy/etl_job.py
|
|
10
|
+
dscin_ppy/exposing_reports.py
|
|
11
|
+
dscin_ppy/functions.py
|
|
12
|
+
dscin_ppy/job_parameters.py
|
|
13
|
+
dscin_ppy.egg-info/PKG-INFO
|
|
14
|
+
dscin_ppy.egg-info/SOURCES.txt
|
|
15
|
+
dscin_ppy.egg-info/dependency_links.txt
|
|
16
|
+
dscin_ppy.egg-info/requires.txt
|
|
17
|
+
dscin_ppy.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dscin_ppy
|
dscin_ppy-0.0.1/setup.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from setuptools import find_packages, setup
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="dscin_ppy",
|
|
5
|
+
version="0.0.1",
|
|
6
|
+
author="Andrea Chiappo",
|
|
7
|
+
author_email="chiappo.andrea@gmail.com",
|
|
8
|
+
description="Collection of utility functions",
|
|
9
|
+
packages=find_packages(include=["dscin_ppy"]),
|
|
10
|
+
install_requires=[
|
|
11
|
+
"boto3==1.35.15",
|
|
12
|
+
"pandas==2.0.1",
|
|
13
|
+
"numpy==1.23.5",
|
|
14
|
+
"hdbcli==2.17.22"
|
|
15
|
+
]
|
|
16
|
+
)
|