cit-pydata 0.0.1.dev0__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.
Files changed (29) hide show
  1. cit-pydata-0.0.1.dev0/LICENSE +19 -0
  2. cit-pydata-0.0.1.dev0/PKG-INFO +33 -0
  3. cit-pydata-0.0.1.dev0/README.md +6 -0
  4. cit-pydata-0.0.1.dev0/pyproject.toml +36 -0
  5. cit-pydata-0.0.1.dev0/setup.cfg +4 -0
  6. cit-pydata-0.0.1.dev0/src/cit_pydata/__init__.py +9 -0
  7. cit-pydata-0.0.1.dev0/src/cit_pydata/aws/__init__.py +1 -0
  8. cit-pydata-0.0.1.dev0/src/cit_pydata/aws/api.py +204 -0
  9. cit-pydata-0.0.1.dev0/src/cit_pydata/box/__init__.py +1 -0
  10. cit-pydata-0.0.1.dev0/src/cit_pydata/box/api.py +107 -0
  11. cit-pydata-0.0.1.dev0/src/cit_pydata/joblog/__init__.py +1 -0
  12. cit-pydata-0.0.1.dev0/src/cit_pydata/joblog/api.py +93 -0
  13. cit-pydata-0.0.1.dev0/src/cit_pydata/marketo/__init__.py +1 -0
  14. cit-pydata-0.0.1.dev0/src/cit_pydata/marketo/api.py +587 -0
  15. cit-pydata-0.0.1.dev0/src/cit_pydata/salesforce/__init__.py +1 -0
  16. cit-pydata-0.0.1.dev0/src/cit_pydata/salesforce/api.py +604 -0
  17. cit-pydata-0.0.1.dev0/src/cit_pydata/sfsync/__init__.py +1 -0
  18. cit-pydata-0.0.1.dev0/src/cit_pydata/sfsync/api.py +39 -0
  19. cit-pydata-0.0.1.dev0/src/cit_pydata/sftp/__init__.py +1 -0
  20. cit-pydata-0.0.1.dev0/src/cit_pydata/sftp/api.py +379 -0
  21. cit-pydata-0.0.1.dev0/src/cit_pydata/sql/__init__.py +1 -0
  22. cit-pydata-0.0.1.dev0/src/cit_pydata/sql/api.py +939 -0
  23. cit-pydata-0.0.1.dev0/src/cit_pydata/util/__init__.py +1 -0
  24. cit-pydata-0.0.1.dev0/src/cit_pydata/util/api.py +418 -0
  25. cit-pydata-0.0.1.dev0/src/cit_pydata.egg-info/PKG-INFO +33 -0
  26. cit-pydata-0.0.1.dev0/src/cit_pydata.egg-info/SOURCES.txt +27 -0
  27. cit-pydata-0.0.1.dev0/src/cit_pydata.egg-info/dependency_links.txt +1 -0
  28. cit-pydata-0.0.1.dev0/src/cit_pydata.egg-info/requires.txt +71 -0
  29. cit-pydata-0.0.1.dev0/src/cit_pydata.egg-info/top_level.txt +1 -0
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2024 The Python Packaging Authority
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.1
2
+ Name: cit-pydata
3
+ Version: 0.0.1.dev0
4
+ Summary: Python clients for commonly used api services including Salesforce, SQL, Marketo, Box, SFSync*, and JobLog*
5
+ Author-email: Dionis Wang <dwang@eab.com>
6
+ Project-URL: Homepage, https://github.com/EAB-IT-DEV/cit-pydata
7
+ Project-URL: Issues, https://github.com/EAB-IT-DEV/cit-pydata/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: POSIX :: Linux
11
+ Requires-Python: >=3.7
12
+ Description-Content-Type: text/markdown
13
+ Provides-Extra: aws
14
+ Provides-Extra: box
15
+ Provides-Extra: joblog
16
+ Provides-Extra: salesforce
17
+ Provides-Extra: sftp
18
+ Provides-Extra: sql
19
+ Provides-Extra: sql-pyodbc
20
+ Provides-Extra: sql-pymssql
21
+ Provides-Extra: sql-psycopg2
22
+ Provides-Extra: sql-mssql
23
+ Provides-Extra: sql-postgres
24
+ Provides-Extra: local
25
+ Provides-Extra: all
26
+ License-File: LICENSE
27
+
28
+ # cit-pydata
29
+
30
+ This repo provides Python clients for commonly used api services including Salesforce, SQL, Marketo, Box, SFSync*, and JobLog*.
31
+
32
+ *SFSync = custom Salesforce <> MSQ SQL integration
33
+ *JobLog = custom centralized logging for data integration jobs
@@ -0,0 +1,6 @@
1
+ # cit-pydata
2
+
3
+ This repo provides Python clients for commonly used api services including Salesforce, SQL, Marketo, Box, SFSync*, and JobLog*.
4
+
5
+ *SFSync = custom Salesforce <> MSQ SQL integration
6
+ *JobLog = custom centralized logging for data integration jobs
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "cit-pydata"
7
+ version = "0.0.1.dev0"
8
+ authors = [
9
+ { name="Dionis Wang", email="dwang@eab.com" },
10
+ ]
11
+ description = "Python clients for commonly used api services including Salesforce, SQL, Marketo, Box, SFSync*, and JobLog*"
12
+ readme = "README.md"
13
+ requires-python = ">=3.7"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: POSIX :: Linux",
18
+ ]
19
+ [project.urls]
20
+ Homepage = "https://github.com/EAB-IT-DEV/cit-pydata"
21
+ Issues = "https://github.com/EAB-IT-DEV/cit-pydata/issues"
22
+
23
+ [project.optional-dependencies]
24
+ aws = ["aws-lambda-powertools"]
25
+ box = ["boxsdk"]
26
+ joblog = ["marketorestpython"]
27
+ salesforce = ["simple_salesforce","pyforce"]
28
+ sftp = ["paramiko"]
29
+ sql = ["SQLAlchemy==1.4.25", "numpy", "pandas"]
30
+ sql-pyodbc = ["SQLAlchemy==1.4.25", "pyodbc", "numpy", "pandas"]
31
+ sql-pymssql = ["SQLAlchemy==1.4.25", "pymssql", "numpy", "pandas"]
32
+ sql-psycopg2 = ["SQLAlchemy==1.4.25", "psycopg2-binary==2.9.6", "numpy", "pandas"]
33
+ sql-mssql = ["SQLAlchemy==1.4.25", "pymssql", "numpy", "pandas"]
34
+ sql-postgres = ["SQLAlchemy==1.4.25", "psycopg2-binary==2.9.6", "numpy", "pandas"]
35
+ local = ["python-dotenv", "boto3"]
36
+ all = ["aws-lambda-powertools","pandas","boxsdk","marketorestpython","simple_salesforce","pyforce","paramiko","SQLAlchemy==1.4.25", "pyodbc","pymssql", "numpy", "pandas","psycopg2-binary==2.9.6","python-dotenv", "boto3"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,9 @@
1
+ import aws
2
+ import box
3
+ import joblog
4
+ import marketo
5
+ import salesforce
6
+ import sfsync
7
+ import sftp
8
+ import sql
9
+ import util
@@ -0,0 +1 @@
1
+ from .api import S3Client, SSMClient
@@ -0,0 +1,204 @@
1
+ import sys
2
+
3
+ from cit_pydata.util import api as util_api
4
+ from cit_pydata.util import ON_AWS
5
+
6
+ from typing import Literal
7
+ import boto3
8
+
9
+
10
+ def _get_boto_session(
11
+ service: str = None,
12
+ environment: str = None,
13
+ iam_user: str = None,
14
+ path_to_env_file: str = None,
15
+ region: str = None,
16
+ logger=util_api.get_logger(__name__, "info"),
17
+ ):
18
+ if ON_AWS:
19
+ return boto3.Session()
20
+
21
+ _environment_variable = environment + "_" + iam_user + "_" + "aws_access_key_id"
22
+ aws_access_key_id = util_api.get_environment_variable(
23
+ logger=logger,
24
+ path_to_env_file=path_to_env_file,
25
+ variable_name=_environment_variable,
26
+ )
27
+
28
+ if aws_access_key_id is None:
29
+ logger.error(f"Access Key Id does not exist for environment {environment}")
30
+ return
31
+
32
+ _environment_variable = environment + "_" + iam_user + "_" + "aws_secret_access_key"
33
+ aws_secret_access_key = util_api.get_environment_variable(
34
+ logger=logger,
35
+ path_to_env_file=path_to_env_file,
36
+ variable_name=_environment_variable,
37
+ )
38
+
39
+ if aws_secret_access_key is None:
40
+ logger.error(f"Secret Key not specified for environment {environment}")
41
+ return
42
+
43
+ _aws_region = region
44
+ if _aws_region is None:
45
+ _environment_variable = environment + "_" + iam_user + "_" + "aws_region"
46
+ _aws_region = util_api.get_environment_variable(
47
+ logger=logger,
48
+ path_to_env_file=path_to_env_file,
49
+ variable_name=_environment_variable,
50
+ )
51
+
52
+ if _aws_region is None:
53
+ logger.error(f"AWS Region not specified for environment {environment}")
54
+ return
55
+
56
+ _session = boto3.Session(
57
+ aws_access_key_id=aws_access_key_id,
58
+ aws_secret_access_key=aws_secret_access_key,
59
+ region_name=_aws_region,
60
+ )
61
+
62
+ return _session
63
+
64
+
65
+ class S3Client:
66
+ def __init__(
67
+ self,
68
+ environment: str = None,
69
+ iam_user: str = None,
70
+ path_to_env_file: str = None,
71
+ log_level: Literal["info", "debug"] = "info",
72
+ ):
73
+ self.logger = util_api.get_logger(__name__, log_level)
74
+ environment = environment.lower()
75
+
76
+ self.session = _get_boto_session(
77
+ service="s3",
78
+ environment=environment,
79
+ iam_user=iam_user,
80
+ path_to_env_file=path_to_env_file,
81
+ logger=self.logger,
82
+ )
83
+ self.client = self.session.client("s3")
84
+
85
+ def get_object_metadata(self, **kwargs):
86
+ """
87
+ Returns pandas dataframe with S3 Objects
88
+
89
+ kwargs for method client.list_objects_v2
90
+ https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.Client.list_objects_v2
91
+ """
92
+
93
+ # self.logger.info(f'Getting S3 objects: {**kwargs}')
94
+ object_list = []
95
+ next_page_token = "init"
96
+
97
+ # Object metadata is retrieved in batches of 1000
98
+ while next_page_token:
99
+ response: dict = None
100
+ if next_page_token == "init":
101
+ response = self.client.list_objects_v2(**kwargs)
102
+ else:
103
+ response = self.client.list_objects_v2(
104
+ ContinuationToken=next_page_token, **kwargs
105
+ )
106
+
107
+ next_page_token = response.get("NextContinuationToken", None)
108
+ # self.logger.debug(next_page_token)
109
+ for object_item in response["Contents"]:
110
+ object_list.append(object_item)
111
+ response.pop("Contents")
112
+
113
+ if not pandas in sys.modules:
114
+ import pandas
115
+ df_s3_objects = pandas.DataFrame(object_list)
116
+ # self.logger.debug(response)
117
+ # self.logger.debug(df_s3_objects.info())
118
+
119
+ return response, df_s3_objects
120
+
121
+ def metadata_to_sql(self, sql_client, table_name: str, dataframe):
122
+ if "sqlalchemy" not in sys.modules:
123
+ import sqlalchemy
124
+
125
+ try:
126
+ sql_client.insert_df(
127
+ table_name=table_name,
128
+ df=dataframe,
129
+ index=False,
130
+ dtype={"LastModified": sqlalchemy.DateTime},
131
+ )
132
+ # dataframe.to_sql(name = table_name, con = sql_client.sql_engine, if_exists='replace', index=False, dtype={'LastModified':sqlalchemy.DateTime})
133
+ except Exception as e:
134
+ self.logger.error(f"Failed to send S3 Object metadata to SQL")
135
+ self.logger.exception(str(e))
136
+
137
+
138
+ class SSMClient:
139
+ def __init__(
140
+ self,
141
+ environment: str = None,
142
+ iam_user: str = None,
143
+ path_to_env_file: str = None,
144
+ log_level: Literal["info", "debug"] = "info",
145
+ logger=None,
146
+ ):
147
+ self.logger = util_api.get_logger(__name__, log_level) if not logger else logger
148
+ self.session = _get_boto_session(
149
+ service="ssm",
150
+ environment=environment,
151
+ iam_user=iam_user,
152
+ path_to_env_file=path_to_env_file,
153
+ logger=self.logger,
154
+ )
155
+ self.client = self.session.client("ssm")
156
+
157
+ def get_parameter(
158
+ self, name: str, with_decryption: bool = False, is_verbose: bool = False
159
+ ):
160
+ value = None
161
+ try:
162
+ value = self.client.get_parameter(Name=name, WithDecryption=with_decryption)
163
+ except Exception as ce:
164
+ self.logger.error(ce)
165
+ return None
166
+
167
+ if value is None:
168
+ self.logger.error(f'SSM Parameter not found "{name}"')
169
+ return
170
+
171
+ if is_verbose:
172
+ return value
173
+ else:
174
+ return value["Parameter"]["Value"]
175
+
176
+ # for parameter in response['Parameters']:
177
+ # return parameter['Value']
178
+
179
+ def describe_parameters(
180
+ self, filter_parameters: list[dict], is_verbose: bool = False
181
+ ):
182
+ """
183
+ filter_parameters=[
184
+ {
185
+ 'Key': 'string',
186
+ 'Option': 'string',
187
+ 'Values': [
188
+ 'string',
189
+ ]
190
+ },
191
+ """
192
+ max_results = 50
193
+ describe_param_dict: dict = self.client.describe_parameters(
194
+ ParameterFilters=filter_parameters, MaxResults=max_results
195
+ )
196
+
197
+ param_list = describe_param_dict.get("Parameters", None)
198
+ # return_list = []
199
+ # for parameter in param_list
200
+ # param_keys_to_return_list = ['Name']
201
+ # param_dict = parameter.get()
202
+ # return_list.append()
203
+
204
+ return param_list
@@ -0,0 +1 @@
1
+ from .api import BoxClient
@@ -0,0 +1,107 @@
1
+ import os
2
+ import sys
3
+
4
+ from cit_pydata.util import api as util_api
5
+
6
+
7
+ class BoxClient:
8
+ def __init__(self, box_jwt_auth_file_path, logger=None):
9
+ self.logger = util_api.get_logger(__name__, "INFO") if not logger else logger
10
+
11
+ self.box_jwt_auth_file_path = box_jwt_auth_file_path
12
+
13
+ # client is the boxsdk Client object
14
+ self.client = None
15
+
16
+ def _get_client(self):
17
+ if "boxsdk" not in sys.modules:
18
+ from boxsdk import Client, JWTAuth, OAuth2
19
+ # JWT Auth
20
+ auth = JWTAuth.from_settings_file(self.box_jwt_auth_file_path)
21
+
22
+ try:
23
+ self.client = Client(auth)
24
+ except Client.BoxAPIException as err:
25
+ self.logger.error("Failed to Authenticate to Box API:", err)
26
+
27
+ def get_folder(self, folder_id: str):
28
+ if not self.client:
29
+ self._get_client()
30
+
31
+ return self.client.folder(folder_id=folder_id)
32
+
33
+ def get_file(self):
34
+ pass
35
+
36
+ def get_folder_items(self, folder_id: str, filter_type: str = None):
37
+ if not self.client:
38
+ self._get_client()
39
+
40
+ item_id_dict = {}
41
+ items = self.client.folder(folder_id=folder_id).get_items()
42
+
43
+ assert filter_type in ["file", "folder", None]
44
+ if filter_type == "file":
45
+ for item in items:
46
+ if item.type == filter_type:
47
+ item_id_dict[item.id] = item
48
+ elif filter_type == "folder":
49
+ for item in items:
50
+ if item.type == filter_type:
51
+ item_id_dict[item.id] = item
52
+ else:
53
+ for item in items:
54
+ item_id_dict[item.id] = item
55
+
56
+ for item in items:
57
+ if item.type == filter_type:
58
+ item_id_dict[item.id] = {"name": item.name, "type": item.type}
59
+ else:
60
+ item_id_dict[item.id] = {"name": item.name, "type": item.type}
61
+
62
+ return item_id_dict
63
+
64
+ def get_shared_link(self, folder_id):
65
+ if not self.client:
66
+ self._get_client()
67
+
68
+ folder = self.get_folder(folder_id)
69
+ shared_link = folder.get().shared_link
70
+ return shared_link
71
+
72
+ def copy_file(self, source_file_id, target_folder_id):
73
+ """
74
+ Copies Box fild to another folder
75
+ source_file_id - Box file id of the file to copy
76
+ target_folder_id - Box folder id of where to copy the file too
77
+ """
78
+ if not self.client:
79
+ self._get_client()
80
+
81
+ file_to_copy = self.client.file(source_file_id)
82
+ target_folder = self.client.folder(target_folder_id)
83
+ try:
84
+ file_copy = file_to_copy.copy(target_folder)
85
+ self.logger.info(
86
+ 'File "{0}" has been copied into folder "{1}"'.format(
87
+ file_copy.name, file_copy.parent.name
88
+ )
89
+ )
90
+ except Exception as e:
91
+ self.logger.error(f"Failed to copy Box item {e}")
92
+
93
+ def upload_file(self, file_path, box_folder_id):
94
+ if not self.client:
95
+ self._get_client()
96
+
97
+ if os.path.exists(file_path):
98
+ new_file = self.client.folder(box_folder_id).upload(file_path)
99
+ self.logger.info(
100
+ 'File "{0}" uploaded to Box with file ID {1}'.format(
101
+ new_file.name, new_file.id
102
+ )
103
+ )
104
+ return True
105
+ else:
106
+ self.logger.error(f"File does not exist {file_path}")
107
+ return False
@@ -0,0 +1 @@
1
+ from .api import JobLogClient
@@ -0,0 +1,93 @@
1
+ import sys
2
+
3
+ from cit_pydata.util import api as util_api
4
+
5
+
6
+ class JobLogClient:
7
+ def __init__(self, sql_client, job_name, logger=None):
8
+ self.logger = util_api.get_logger(__name__, "INFO") if not logger else logger
9
+
10
+ self.sql_client = sql_client
11
+
12
+ self.job_name = job_name
13
+ self.log_guid = None
14
+
15
+ def start(self):
16
+ if "uuid" not in sys.modules:
17
+ import uuid
18
+ if not self.log_guid:
19
+ self.log_guid = str(uuid.uuid4())
20
+ param_list = [self.job_name, "initializing", self.log_guid]
21
+ self.sql_client.execute_stored_procedure("usp_Start_JobLog", param_list)
22
+ self.logger.info(
23
+ f"JOBLOG - Started Job {self.job_name} with GUID {self.log_guid}"
24
+ )
25
+ return self.log_guid
26
+ else:
27
+ self.logger.error(f"JOBLOG - Failed to start. Logging already started")
28
+
29
+ def finish(self, is_success: bool = True, message: str = None):
30
+ if is_success:
31
+ result = "Success"
32
+ self.logger.info(
33
+ f'JOBLOG - SUCCESS - "{self.job_name}" with GUID {self.log_guid}'
34
+ )
35
+ else:
36
+ result = "Failure"
37
+ self.logger.info(
38
+ f'JOBLOG - FAILED - "{self.job_name}" with GUID {self.log_guid}'
39
+ )
40
+
41
+ param_list = [self.log_guid, message, result]
42
+
43
+ # Update JobLog with result
44
+ self.sql_client.execute_stored_procedure("usp_Update_JobLog", param_list)
45
+
46
+ def update_status(self, message):
47
+ param_list = [self.log_guid, message, None]
48
+ self.sql_client.execute_stored_procedure("usp_Update_JobLog", param_list)
49
+
50
+ def update_joblog(sql_client, method, guid, status, logger=None):
51
+ # If no sql_client supplied quietly ignore
52
+ if sql_client:
53
+ assert method in ["Success", "Failure", "Status"]
54
+
55
+ if method in ["Success", "Failure"]:
56
+ result = method
57
+ param_list = [guid, status, result]
58
+ elif method == "Status":
59
+ param_list = [guid, status]
60
+
61
+ stored_procedure_name = "usp_Update_JobLog"
62
+ sql_client.execute_stored_procedure(stored_procedure_name, param_list)
63
+
64
+ def insert_message(
65
+ self,
66
+ message_name,
67
+ message_type,
68
+ value_string: str = None,
69
+ value_bit: bool = None,
70
+ value_int: int = None,
71
+ value_date=None,
72
+ ):
73
+ """
74
+ INSERT INTO tbl_JobLogAttribute
75
+ (JobLogGUID,AttributeName,AttributeType,AttributeLogType,ValueType,ValueString,ValueBit,ValueInteger,ValueDate)
76
+
77
+ sample usage:
78
+ job_log.insert_message('Job Parameter', 'Input Parameter', 'getActiveMembershipMainContact')
79
+ """
80
+ param_list = [
81
+ self.log_guid,
82
+ message_name,
83
+ message_type,
84
+ None,
85
+ None,
86
+ value_string,
87
+ value_bit,
88
+ value_int,
89
+ value_date,
90
+ ]
91
+ self.logger.debug(f"JOBLOG - insert attribute param_list: {param_list}")
92
+ # Update JobLog with result
93
+ self.sql_client.execute_stored_procedure("usp_LogJobAttribute", param_list)
@@ -0,0 +1 @@
1
+ from .api import MarketoClient