dc-python-sdk 1.5.42__tar.gz → 1.5.43__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 (37) hide show
  1. {dc_python_sdk-1.5.42/src/dc_python_sdk.egg-info → dc_python_sdk-1.5.43}/PKG-INFO +2 -1
  2. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/pyproject.toml +3 -2
  3. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/setup.cfg +2 -1
  4. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43/src/dc_python_sdk.egg-info}/PKG-INFO +2 -1
  5. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_python_sdk.egg-info/requires.txt +1 -0
  6. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_sdk/src/models/pipeline_details.py +2 -0
  7. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_sdk/src/pipeline.py +33 -6
  8. dc_python_sdk-1.5.43/src/dc_sdk/src/services/api.py +205 -0
  9. dc_python_sdk-1.5.43/src/dc_sdk/src/services/aws.py +283 -0
  10. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_sdk/src/services/environment.py +4 -0
  11. dc_python_sdk-1.5.42/src/dc_sdk/src/services/api.py +0 -123
  12. dc_python_sdk-1.5.42/src/dc_sdk/src/services/aws.py +0 -141
  13. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/LICENSE +0 -0
  14. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/README.md +0 -0
  15. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_python_sdk.egg-info/SOURCES.txt +0 -0
  16. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_python_sdk.egg-info/dependency_links.txt +0 -0
  17. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_python_sdk.egg-info/entry_points.txt +0 -0
  18. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_python_sdk.egg-info/top_level.txt +0 -0
  19. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_sdk/__init__.py +0 -0
  20. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_sdk/app.py +0 -0
  21. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_sdk/cli.py +0 -0
  22. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_sdk/errors.py +0 -0
  23. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_sdk/handler.py +0 -0
  24. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_sdk/src/__init__.py +0 -0
  25. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_sdk/src/ai.py +0 -0
  26. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_sdk/src/ai_http.py +0 -0
  27. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_sdk/src/mapping.py +0 -0
  28. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_sdk/src/models/__init__.py +0 -0
  29. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_sdk/src/models/enums.py +0 -0
  30. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_sdk/src/models/errors.py +0 -0
  31. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_sdk/src/models/log_templates.py +0 -0
  32. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_sdk/src/server.py +0 -0
  33. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_sdk/src/services/__init__.py +0 -0
  34. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_sdk/src/services/loader.py +0 -0
  35. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_sdk/src/services/logger.py +0 -0
  36. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_sdk/src/services/session.py +0 -0
  37. {dc_python_sdk-1.5.42 → dc_python_sdk-1.5.43}/src/dc_sdk/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dc-python-sdk
3
- Version: 1.5.42
3
+ Version: 1.5.43
4
4
  Summary: Data Connector Python SDK
5
5
  Home-page: https://github.com/data-connector/dc-python-sdk
6
6
  Author: DataConnector
@@ -18,6 +18,7 @@ Requires-Dist: awslambdaric
18
18
  Requires-Dist: requests
19
19
  Requires-Dist: boto3>=1.40.0
20
20
  Requires-Dist: openai
21
+ Requires-Dist: pycryptodome
21
22
  Provides-Extra: test
22
23
  Requires-Dist: python-dotenv>=0.20.0; extra == "test"
23
24
  Requires-Dist: faker>=13.12.0; extra == "test"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dc-python-sdk"
7
- version = "1.5.42"
7
+ version = "1.5.43"
8
8
  description = "Data Connector Python SDK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.6"
@@ -22,7 +22,8 @@ dependencies = [
22
22
  "awslambdaric",
23
23
  "requests",
24
24
  "boto3>=1.40.0",
25
- "openai"
25
+ "openai",
26
+ "pycryptodome",
26
27
  ]
27
28
 
28
29
  [project.optional-dependencies]
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = dc-python-sdk
3
- version = 1.5.42
3
+ version = 1.5.43
4
4
  author = DataConnector
5
5
  author_email = josh@dataconnector.com
6
6
  description = A small example package
@@ -23,6 +23,7 @@ install_requires =
23
23
  fastapi
24
24
  uvicorn
25
25
  awslambdaric
26
+ pycryptodome
26
27
  requests
27
28
  boto3>=1.40.0
28
29
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dc-python-sdk
3
- Version: 1.5.42
3
+ Version: 1.5.43
4
4
  Summary: Data Connector Python SDK
5
5
  Home-page: https://github.com/data-connector/dc-python-sdk
6
6
  Author: DataConnector
@@ -18,6 +18,7 @@ Requires-Dist: awslambdaric
18
18
  Requires-Dist: requests
19
19
  Requires-Dist: boto3>=1.40.0
20
20
  Requires-Dist: openai
21
+ Requires-Dist: pycryptodome
21
22
  Provides-Extra: test
22
23
  Requires-Dist: python-dotenv>=0.20.0; extra == "test"
23
24
  Requires-Dist: faker>=13.12.0; extra == "test"
@@ -4,6 +4,7 @@ awslambdaric
4
4
  requests
5
5
  boto3>=1.40.0
6
6
  openai
7
+ pycryptodome
7
8
 
8
9
  [ai]
9
10
  openai
@@ -33,6 +33,8 @@ class PipelineDetails:
33
33
  self.options = json.loads(row_data['pipeline_object_options_json']) if 'pipeline_object_options_json' in row_data and row_data['pipeline_object_options_json'] else dict()
34
34
  self.max_allowed_retrieval = row_data.get('max_allowed_retrieval')
35
35
  self.primary_key_column_nm = row_data.get('primary_key_column_nm')
36
+ self.destination_credential_information = row_data.get('destination_credential_information')
37
+ self.source_credential_information = row_data.get('source_credential_information')
36
38
 
37
39
  def increment_stage(self):
38
40
  self.stage += 1
@@ -67,27 +67,54 @@ class PipelineConductor:
67
67
  if self.authentication_tries == 3 or "firewall" in str(e): # todo: fix add retry flg in raised errors
68
68
  raise e
69
69
  self.authentication_tries += 1
70
- self._refresh_credentials()
70
+ self._refresh_source_credentials()
71
71
  time.sleep(10)
72
72
 
73
- if authenticated and self.mode == "prod":
73
+ if authenticated:
74
74
  self.api.send_new_credentials(True, self.pipeline_id, self.connector.credentials)
75
75
  self.log(self.log_templates.AUTHENTICATION_FINISH.format(self.pipeline_details.source_credential_nm))
76
76
 
77
- def _refresh_credentials(self):
77
+ def _refresh_source_credentials(self):
78
78
  response = self.api.get_credential_updates(True, self.pipeline_id)
79
79
 
80
- creds = self.aws.decrypt_customer_data_object(response['credential'], response['organization'])
80
+ creds = self.aws.decrypt_customer_data_object(response['credential'], response['organization'],
81
+ encrypted_data_key_txt=self.pipeline_details.source_credential_information.get('encrypted_data_key_txt'),
82
+ encryption_iv_txt=self.pipeline_details.source_credential_information.get('encryption_iv_txt'),
83
+ encryption_auth_tag_txt=self.pipeline_details.source_credential_information.get('encryption_auth_tag_txt')
84
+ )
81
85
 
82
86
  self.connector.credentials = creds
87
+
88
+ def _refresh_destination_credentials(self):
89
+ response = self.api.get_credential_updates(False, self.pipeline_id)
90
+
91
+ creds = self.aws.decrypt_customer_data_object(response['credential'], response['organization'],
92
+ encrypted_data_key_txt=self.pipeline_details.destination_credential_information.get('encrypted_data_key_txt'),
93
+ encryption_iv_txt=self.pipeline_details.destination_credential_information.get('encryption_iv_txt'),
94
+ encryption_auth_tag_txt=self.pipeline_details.destination_credential_information.get('encryption_auth_tag_txt')
95
+ )
96
+
97
+ self.connector.credentials = creds
83
98
 
84
99
  def authenticate_destination(self):
85
100
  if not self.credentials:
86
101
  raise errors.AuthenticationError("Credentials are not set, please update your credentials.")
87
102
 
88
103
  self.log(self.log_templates.AUTHENTICATION_START.format(self.pipeline_details.destination_credential_nm, self.pipeline_details.destination_object_id))
89
- self.connector.authenticate()
90
- self.log(self.log_templates.AUTHENTICATION_FINISH.format(self.pipeline_details.destination_credential_nm))
104
+ authenticated = False
105
+ while self.authentication_tries <= 3 and not authenticated:
106
+ try:
107
+ authenticated = self.connector.authenticate()
108
+ except Exception as e:
109
+ if self.authentication_tries == 3 or "firewall" in str(e): # todo: fix add retry flg in raised errors
110
+ raise e
111
+ self.authentication_tries += 1
112
+ self._refresh_destination_credentials()
113
+ time.sleep(10)
114
+
115
+ if authenticated:
116
+ self.api.send_new_credentials(False, self.pipeline_id, self.connector.credentials)
117
+ self.log(self.log_templates.AUTHENTICATION_FINISH.format(self.pipeline_details.destination_credential_nm))
91
118
 
92
119
  def get_data(self):
93
120
  # Determine batch size
@@ -0,0 +1,205 @@
1
+ import os
2
+ import requests
3
+ import sys
4
+ import json
5
+ import time
6
+ import random
7
+
8
+ from ..models.enums import EnvironmentVariablesEnum
9
+ from ..models.pipeline_details import PipelineDetails
10
+
11
+
12
+ class DataConnectorAPI:
13
+ RETRY_STATUS_CODES = {408, 429, 500, 502, 503, 504}
14
+
15
+ def __init__(self, environment=None):
16
+ self.server_endpoint = os.environ.get(EnvironmentVariablesEnum.SERVER_ENDPOINT.value)
17
+ self.pipeline_id = os.environ.get(EnvironmentVariablesEnum.PIPELINE_ID.value)
18
+ self.server_api_key = os.environ.get(EnvironmentVariablesEnum.SERVER_API_KEY.value)
19
+ self.pipelines_base_url = f"{self.server_endpoint}/api/pipelines"
20
+
21
+ def get(self, endpoint):
22
+ return self._request("GET", endpoint)
23
+
24
+ def post(self, endpoint, body=None):
25
+ return self._request("POST", endpoint, body)
26
+
27
+ def put(self, endpoint, body=None):
28
+ return self._request("PUT", endpoint, body)
29
+
30
+ def log(self, message, stage, task, error=None, internal=False):
31
+ print("Log Output: ", message)
32
+
33
+ pipeline_run_history_id = os.environ.get(
34
+ EnvironmentVariablesEnum.PIPELINE_RUN_HISTORY_ID.value
35
+ )
36
+
37
+ payload = {
38
+ "PipelineID": self.pipeline_id,
39
+ "PipelineRunHistoryID": pipeline_run_history_id,
40
+ "LogStageID": stage,
41
+ "LogTXT": message,
42
+ "ExternalFacingFLG": 0 if internal else 1,
43
+ "ErrorLocationDSC": None,
44
+ "LogTypeCD": 2 if error else 1,
45
+ "LogTypeDSC": "Error" if error else "Info",
46
+ "task": task,
47
+ }
48
+
49
+ try:
50
+ self.post("log", payload)
51
+ except Exception as e:
52
+ print(f"WARNING: Failed to post pipeline log. Continuing. Error: {e}")
53
+
54
+ def logv2(self, logger_payload):
55
+ pipeline_run_history_id = os.environ.get(
56
+ EnvironmentVariablesEnum.PIPELINE_RUN_HISTORY_ID.value
57
+ )
58
+
59
+ payload = {
60
+ "PipelineID": self.pipeline_id,
61
+ "PipelineRunHistoryID": pipeline_run_history_id,
62
+ **logger_payload,
63
+ }
64
+
65
+ try:
66
+ self.post("logv2", payload)
67
+ except Exception as e:
68
+ print(f"WARNING: Failed to post pipeline logv2. Continuing. Error: {e}")
69
+
70
+ def get_pipeline_details(
71
+ self,
72
+ pipeline_id: str,
73
+ task,
74
+ pipeline_run_history_id: str,
75
+ pipeline_mapping_id: str = None,
76
+ ):
77
+ url = f"{pipeline_id}"
78
+
79
+ if pipeline_mapping_id:
80
+ url = f"{url}?PipelineMappingID={pipeline_mapping_id}"
81
+
82
+ response_json = self.get(url)
83
+
84
+ return PipelineDetails(response_json, task, pipeline_id, pipeline_run_history_id)
85
+
86
+ def create_new_history(self, pipeline_id, pipeline_mapping_id: str = None):
87
+ body = {"PipelineMappingID": pipeline_mapping_id} if pipeline_mapping_id else None
88
+ response = self.post(f"{pipeline_id}/history", body)
89
+
90
+ return response["PipelineRunHistoryID"]
91
+
92
+ def send_new_credentials(self, source_flg, pipeline_id, credentials):
93
+ self.post(
94
+ f"credentials/{pipeline_id}",
95
+ {
96
+ "source": source_flg,
97
+ "credentials": json.dumps(credentials),
98
+ },
99
+ )
100
+
101
+ def get_credential_updates(self, source_flg, pipeline_id):
102
+ response = self.get(
103
+ f"credentials/{pipeline_id}?source={'true' if source_flg else 'false'}"
104
+ )
105
+
106
+ return response
107
+
108
+ def create_pipeline_mapping(self, pipeline_id, pipeline_mapping_json):
109
+ body = {
110
+ "pipeline_id": pipeline_id,
111
+ "pipeline_mapping_json": pipeline_mapping_json,
112
+ }
113
+
114
+ self.post(f"{pipeline_id}/mapping", body)
115
+
116
+ def _request(self, method, endpoint, body=None, max_attempts=4):
117
+ endpoint = endpoint.lstrip("/")
118
+ url = f"{self.pipelines_base_url}/{endpoint}"
119
+
120
+ headers = {
121
+ "Accept": "application/json",
122
+ "x-data-connector-access-token": self.server_api_key,
123
+ }
124
+
125
+ last_error = None
126
+
127
+ for attempt in range(1, max_attempts + 1):
128
+ try:
129
+ response = requests.request(
130
+ method=method,
131
+ url=url,
132
+ json=body,
133
+ headers=headers,
134
+ timeout=(5, 30),
135
+ )
136
+
137
+ try:
138
+ response_data = response.json()
139
+ except ValueError:
140
+ response_data = response.text
141
+
142
+ if response.ok:
143
+ return response_data
144
+
145
+ if response.status_code in self.RETRY_STATUS_CODES and attempt < max_attempts:
146
+ sleep_seconds = self._get_retry_delay(attempt)
147
+ print(
148
+ f"WARNING: API {method} {endpoint} failed with "
149
+ f"{response.status_code}. Retrying attempt "
150
+ f"{attempt + 1}/{max_attempts} in {sleep_seconds:.2f}s."
151
+ )
152
+ time.sleep(sleep_seconds)
153
+ continue
154
+
155
+ raise Exception(
156
+ f"API ERROR: {method} {endpoint} failed | "
157
+ f"Status: {response.status_code} | "
158
+ f"Response: {response_data}"
159
+ )
160
+
161
+ except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
162
+ last_error = e
163
+
164
+ if attempt < max_attempts:
165
+ sleep_seconds = self._get_retry_delay(attempt)
166
+ print(
167
+ f"WARNING: API {method} {endpoint} connection/timeout error. "
168
+ f"Retrying attempt {attempt + 1}/{max_attempts} "
169
+ f"in {sleep_seconds:.2f}s. Error: {e}"
170
+ )
171
+ time.sleep(sleep_seconds)
172
+ continue
173
+
174
+ raise Exception(
175
+ f"API ERROR: {method} {endpoint} failed after "
176
+ f"{max_attempts} attempts | Error: {str(e)}"
177
+ )
178
+
179
+ except requests.exceptions.RequestException as e:
180
+ last_error = e
181
+
182
+ if attempt < max_attempts:
183
+ sleep_seconds = self._get_retry_delay(attempt)
184
+ print(
185
+ f"WARNING: API {method} {endpoint} unexpected request error. "
186
+ f"Retrying attempt {attempt + 1}/{max_attempts} "
187
+ f"in {sleep_seconds:.2f}s. Error: {e}"
188
+ )
189
+ time.sleep(sleep_seconds)
190
+ continue
191
+
192
+ raise Exception(
193
+ f"API ERROR: {method} {endpoint} unexpected error after "
194
+ f"{max_attempts} attempts | Error: {str(e)}"
195
+ )
196
+
197
+ raise Exception(
198
+ f"API ERROR: {method} {endpoint} failed after "
199
+ f"{max_attempts} attempts | Last error: {str(last_error)}"
200
+ )
201
+
202
+ def _get_retry_delay(self, attempt):
203
+ base_delay = min(2 ** attempt, 10)
204
+ jitter = random.uniform(0, 0.5)
205
+ return base_delay + jitter
@@ -0,0 +1,283 @@
1
+ import os
2
+ import boto3
3
+ import json
4
+ from typing import Dict
5
+ from base64 import b64decode
6
+ from Crypto.Cipher import AES
7
+
8
+ kms_client = boto3.client('kms')
9
+
10
+ APP_ENV = os.environ.get("APP_ENV")
11
+
12
+ s3_client = boto3.client('s3')
13
+ s3_resource = boto3.resource('s3')
14
+ ecs_resource = boto3.client('ecs')
15
+ kms_client = boto3.client('kms')
16
+
17
+ class AwsService:
18
+ def __init__(self, s3_bucket) -> None:
19
+ self.s3_bucket = s3_bucket
20
+ self._ecs_cluster_name = APP_ENV if APP_ENV != "local" else "development"
21
+
22
+ def get_keys(self, pipeline_run_history_id):
23
+ keys = []
24
+ response = s3_client.list_objects(Bucket=self.s3_bucket, Prefix=f"transfers/e{pipeline_run_history_id}")
25
+ if 'Contents' in response:
26
+ for key in response['Contents']:
27
+ keys.append(key['Key'])
28
+ return keys
29
+
30
+ def get_dev_keys(self, prefix):
31
+ keys = []
32
+ response = s3_client.list_objects(Bucket=self.s3_bucket, Prefix=f"devTransfers/e{prefix}")
33
+ if 'Contents' in response:
34
+ for key in response['Contents']:
35
+ keys.append(key['Key'])
36
+ return keys
37
+
38
+ def download_object(self, key_name):
39
+ file = s3_resource.Object(self.s3_bucket, key_name)
40
+ return file.get()['Body']
41
+
42
+ def upload_object(self, key_name, json_buffer=None, file_path=None):
43
+ if json_buffer is not None:
44
+ s3_resource.Object(
45
+ self.s3_bucket, key_name).put(Body=json_buffer.getvalue())
46
+ elif file_path is not None:
47
+ s3_client.upload_file(file_path, self.s3_bucket, key_name)
48
+ else:
49
+ raise ValueError("Either json_buffer or file_path must be provided")
50
+
51
+ def get_object_size(self, key_name):
52
+ response = s3_client.head_object(
53
+ Bucket=self.s3_bucket, Key=key_name)
54
+ return float(response['ContentLength'])
55
+
56
+ def delete_object(self, key_name):
57
+ s3_client.delete_object(
58
+ Bucket=self.s3_bucket,
59
+ Key=key_name,
60
+ )
61
+
62
+ def start_ecs_task(self, task_name:str, task_version:str, pipeline_id: str, pipeline_run_history_id: str):
63
+ response = ecs_resource.run_task(
64
+ cluster=self._ecs_cluster_name,
65
+ count=1,
66
+ enableECSManagedTags=False,
67
+ enableExecuteCommand=False,
68
+ launchType='FARGATE',
69
+ networkConfiguration={
70
+ 'awsvpcConfiguration': {
71
+ 'subnets': [
72
+ 'subnet-095c3230c890e0841',
73
+ ],
74
+ 'securityGroups': [
75
+ 'sg-08d46d487577cbdf9',
76
+ ],
77
+ 'assignPublicIp': 'ENABLED'
78
+ }
79
+ },
80
+ overrides={
81
+ 'containerOverrides': [
82
+ {
83
+ 'name': task_name,
84
+ 'command': ['app.py'],
85
+ 'environment': [
86
+ {
87
+ 'name': 'PIPELINE_ID',
88
+ 'value': pipeline_id
89
+ },
90
+ {
91
+ 'name': 'PIPELINE_RUN_HISTORY_ID',
92
+ 'value': pipeline_run_history_id
93
+ },
94
+ {
95
+ 'name': 'TASK',
96
+ 'value': 'DESTINATION'
97
+ },
98
+ {
99
+ 'name': 'APP_ENV',
100
+ 'value': os.environ.get("APP_ENV")
101
+ },
102
+ {
103
+ 'name': 'SERVER_ENDPOINT',
104
+ 'value': os.environ.get("SERVER_ENDPOINT")
105
+ },
106
+ {
107
+ 'name': 'SERVER_API_KEY',
108
+ 'value': os.environ.get("SERVER_API_KEY")
109
+ },
110
+ {
111
+ 'name': 'AWS_DEFAULT_REGION',
112
+ 'value': os.environ.get("AWS_DEFAULT_REGION")
113
+ }
114
+ ],
115
+ },
116
+ ],
117
+ },
118
+ platformVersion='LATEST',
119
+ taskDefinition=f"{task_name}:{task_version}"
120
+ )
121
+
122
+ if len(response['tasks']) == 0:
123
+ raise Exception("Failed to start next connector")
124
+
125
+ return response['tasks'][0]['taskArn'].split("/")[-1]
126
+
127
+ def decrypt_customer_data(
128
+ self,
129
+ encrypted_data: str,
130
+ customer_id: str,
131
+ encrypted_data_key_txt: str = None,
132
+ encryption_iv_txt: str = None,
133
+ encryption_auth_tag_txt: str = None
134
+ ) -> str:
135
+ """Decrypt data with customer context using legacy or envelope encryption"""
136
+ encryption_service = EncryptionService()
137
+
138
+ return encryption_service.decrypt_customer_data(
139
+ encrypted_data=encrypted_data,
140
+ customer_id=customer_id,
141
+ encrypted_data_key_txt=encrypted_data_key_txt,
142
+ encryption_iv_txt=encryption_iv_txt,
143
+ encryption_auth_tag_txt=encryption_auth_tag_txt
144
+ )
145
+
146
+
147
+ def decrypt_customer_data_object(
148
+ self,
149
+ encrypted_data: str,
150
+ customer_id: str,
151
+ encrypted_data_key_txt: str = None,
152
+ encryption_iv_txt: str = None,
153
+ encryption_auth_tag_txt: str = None
154
+ ) -> Dict:
155
+ """Decrypt JSON data with customer context using legacy or envelope encryption"""
156
+ encryption_service = EncryptionService()
157
+
158
+ return encryption_service.decrypt_customer_data_object(
159
+ encrypted_data=encrypted_data,
160
+ customer_id=customer_id,
161
+ encrypted_data_key_txt=encrypted_data_key_txt,
162
+ encryption_iv_txt=encryption_iv_txt,
163
+ encryption_auth_tag_txt=encryption_auth_tag_txt
164
+ )
165
+
166
+
167
+
168
+
169
+
170
+ class EncryptionService:
171
+ def decrypt_customer_data(
172
+ self,
173
+ encrypted_data: str,
174
+ customer_id: str,
175
+ encrypted_data_key_txt: str = None,
176
+ encryption_iv_txt: str = None,
177
+ encryption_auth_tag_txt: str = None
178
+ ) -> str:
179
+ """
180
+ Decrypt customer data using either:
181
+ - legacy KMS direct encryption
182
+ - envelope encryption
183
+ """
184
+
185
+ try:
186
+ is_envelope = (
187
+ encrypted_data_key_txt is not None and
188
+ encryption_iv_txt is not None and
189
+ encryption_auth_tag_txt is not None
190
+ )
191
+
192
+ if is_envelope:
193
+ return self._decrypt_customer_data_envelope(
194
+ encrypted_data=encrypted_data,
195
+ customer_id=customer_id,
196
+ encrypted_data_key_txt=encrypted_data_key_txt,
197
+ encryption_iv_txt=encryption_iv_txt,
198
+ encryption_auth_tag_txt=encryption_auth_tag_txt
199
+ )
200
+
201
+ return self._decrypt_customer_data_legacy(
202
+ encrypted_data=encrypted_data,
203
+ customer_id=customer_id
204
+ )
205
+
206
+ except Exception as e:
207
+ raise Exception(f'Decryption failed: {str(e)}')
208
+
209
+ def _decrypt_customer_data_legacy(
210
+ self,
211
+ encrypted_data: str,
212
+ customer_id: str
213
+ ) -> str:
214
+
215
+ response = kms_client.decrypt(
216
+ CiphertextBlob=b64decode(encrypted_data.encode('utf-8')),
217
+ EncryptionContext={
218
+ 'CustomerId': customer_id
219
+ }
220
+ )
221
+
222
+ if 'Plaintext' not in response:
223
+ raise Exception('Legacy decryption failed')
224
+
225
+ return response['Plaintext'].decode('utf-8')
226
+
227
+ def _decrypt_customer_data_envelope(
228
+ self,
229
+ encrypted_data: str,
230
+ customer_id: str,
231
+ encrypted_data_key_txt: str,
232
+ encryption_iv_txt: str,
233
+ encryption_auth_tag_txt: str
234
+ ) -> str:
235
+
236
+ # decrypt data key using KMS
237
+ data_key_response = kms_client.decrypt(
238
+ CiphertextBlob=b64decode(
239
+ encrypted_data_key_txt.encode('utf-8')
240
+ ),
241
+ EncryptionContext={
242
+ 'CustomerId': customer_id
243
+ }
244
+ )
245
+
246
+ if 'Plaintext' not in data_key_response:
247
+ raise Exception('Data key decryption failed')
248
+
249
+ plaintext_data_key = data_key_response['Plaintext']
250
+
251
+ cipher = AES.new(
252
+ plaintext_data_key,
253
+ AES.MODE_GCM,
254
+ nonce=b64decode(encryption_iv_txt.encode('utf-8'))
255
+ )
256
+
257
+ cipher.update(b'')
258
+
259
+ decrypted_data = cipher.decrypt_and_verify(
260
+ b64decode(encrypted_data.encode('utf-8')),
261
+ b64decode(encryption_auth_tag_txt.encode('utf-8'))
262
+ )
263
+
264
+ return decrypted_data.decode('utf-8')
265
+
266
+ def decrypt_customer_data_object(
267
+ self,
268
+ encrypted_data: str,
269
+ customer_id: str,
270
+ encrypted_data_key_txt: str = None,
271
+ encryption_iv_txt: str = None,
272
+ encryption_auth_tag_txt: str = None
273
+ ) -> dict:
274
+
275
+ data = self.decrypt_customer_data(
276
+ encrypted_data=encrypted_data,
277
+ customer_id=customer_id,
278
+ encrypted_data_key_txt=encrypted_data_key_txt,
279
+ encryption_iv_txt=encryption_iv_txt,
280
+ encryption_auth_tag_txt=encryption_auth_tag_txt
281
+ )
282
+
283
+ return json.loads(data)
@@ -78,6 +78,10 @@ class PipelineEnvironment:
78
78
 
79
79
  @staticmethod
80
80
  def get_task_id():
81
+ explicit = os.getenv("DC_TASK_ID")
82
+ if explicit:
83
+ return explicit
84
+
81
85
  metadata_uri = os.getenv("ECS_CONTAINER_METADATA_URI_V4")
82
86
 
83
87
  # Not running in ECS
@@ -1,123 +0,0 @@
1
- import os, requests, sys, json
2
- from ..models.enums import EnvironmentVariablesEnum
3
- from ..models.pipeline_details import PipelineDetails
4
-
5
- class DataConnectorAPI:
6
- def __init__(self, environment = None):
7
- self.server_endpoint = os.environ.get(EnvironmentVariablesEnum.SERVER_ENDPOINT.value)
8
- self.pipeline_id = os.environ.get(EnvironmentVariablesEnum.PIPELINE_ID.value)
9
- self.server_api_key = os.environ.get(EnvironmentVariablesEnum.SERVER_API_KEY.value)
10
- self.pipelines_base_url = f"{self.server_endpoint}/api/pipelines"
11
-
12
- def get(self, endpoint):
13
- return self._request("GET", endpoint)
14
-
15
- def post(self, endpoint, body=None):
16
- return self._request("POST", endpoint, body)
17
-
18
- def put(self, endpoint, body=None):
19
- return self._request("PUT", endpoint, body)
20
-
21
- def log(self, message, stage, task, error=None, internal=False):
22
- print("Log Output: ", message)
23
- PIPELINE_RUN_HISTORY_ID = os.environ.get(EnvironmentVariablesEnum.PIPELINE_RUN_HISTORY_ID.value)
24
- payload = {
25
- 'PipelineID': self.pipeline_id,
26
- 'PipelineRunHistoryID': PIPELINE_RUN_HISTORY_ID,
27
- 'LogStageID': stage,
28
- 'LogTXT': message,
29
- 'ExternalFacingFLG': 0 if internal else 1,
30
- 'ErrorLocationDSC': None, # TODO: UPDATE THIS
31
- 'LogTypeCD': 2 if error else 1,
32
- 'LogTypeDSC': 'Error' if error else 'Info',
33
- 'task': task,
34
- }
35
-
36
- self.post('log', payload)
37
-
38
- def logv2(self, logger_payload):
39
- PIPELINE_RUN_HISTORY_ID = os.environ.get(EnvironmentVariablesEnum.PIPELINE_RUN_HISTORY_ID.value)
40
- payload = {
41
- 'PipelineID': self.pipeline_id,
42
- 'PipelineRunHistoryID': PIPELINE_RUN_HISTORY_ID,
43
- **logger_payload
44
- }
45
-
46
- self.post('logv2', payload)
47
-
48
- def get_pipeline_details(self, pipeline_id: str, task, pipeline_run_history_id: str, pipeline_mapping_id: str = None):
49
- url = f"{pipeline_id}"
50
- if pipeline_mapping_id:
51
- url = f"{url}?PipelineMappingID={pipeline_mapping_id}"
52
- json = self.get(url)
53
-
54
- return PipelineDetails(json, task, pipeline_id, pipeline_run_history_id)
55
-
56
- def create_new_history(self, pipeline_id, pipeline_mapping_id: str = None):
57
- if pipeline_mapping_id:
58
- body = {"PipelineMappingID": pipeline_mapping_id}
59
- else:
60
- body = None
61
- response = self.post(f"{pipeline_id}/history", body)
62
-
63
- return response['PipelineRunHistoryID']
64
-
65
- def send_new_credentials(self, source_flg, pipeline_id, credentials):
66
- self.post(f"credentials/{pipeline_id}", {"source": source_flg, "credentials": json.dumps(credentials)})
67
-
68
- def get_credential_updates(self, source_flg, pipeline_id):
69
- response = self.get(f"credentials/{pipeline_id}?source={'true' if source_flg else 'false'}")
70
-
71
- return response
72
-
73
- def create_pipeline_mapping(self, pipeline_id, pipeline_mapping_json):
74
- body = {
75
- "pipeline_id": pipeline_id,
76
- "pipeline_mapping_json": pipeline_mapping_json
77
- }
78
- self.post(f"/{pipeline_id}/mapping", body)
79
-
80
- def _request(self, method, endpoint, body=None):
81
- url = f"{self.pipelines_base_url}/{endpoint}"
82
-
83
- try:
84
- response = requests.request(
85
- method=method,
86
- url=url,
87
- data=body,
88
- headers={
89
- 'Accept': 'application/json',
90
- 'x-data-connector-access-token': self.server_api_key
91
- },
92
- timeout=10 # 🔥 IMPORTANT
93
- )
94
-
95
- # Try to parse JSON safely
96
- try:
97
- response_data = response.json()
98
- except ValueError:
99
- response_data = response.text
100
-
101
- if response.ok:
102
- return response_data
103
- else:
104
- raise Exception(
105
- f"API ERROR: {method} {endpoint} failed | "
106
- f"Status: {response.status_code} | "
107
- f"Response: {response_data}"
108
- )
109
-
110
- except requests.exceptions.Timeout:
111
- raise Exception(
112
- f"API ERROR: {method} {endpoint} timed out (10s)"
113
- )
114
-
115
- except requests.exceptions.ConnectionError:
116
- raise Exception(
117
- f"API ERROR: {method} {endpoint} failed — no response from server"
118
- )
119
-
120
- except requests.exceptions.RequestException as e:
121
- raise Exception(
122
- f"API ERROR: {method} {endpoint} unexpected error | {str(e)}"
123
- )
@@ -1,141 +0,0 @@
1
- import os
2
- import boto3
3
- import json
4
- from typing import Dict
5
- from base64 import b64decode
6
-
7
- APP_ENV = os.environ.get("APP_ENV")
8
-
9
- s3_client = boto3.client('s3')
10
- s3_resource = boto3.resource('s3')
11
- ecs_resource = boto3.client('ecs')
12
- kms_client = boto3.client('kms')
13
-
14
- class AwsService:
15
- def __init__(self, s3_bucket) -> None:
16
- self.s3_bucket = s3_bucket
17
- self._ecs_cluster_name = APP_ENV if APP_ENV != "local" else "development"
18
-
19
- def get_keys(self, pipeline_run_history_id):
20
- keys = []
21
- response = s3_client.list_objects(Bucket=self.s3_bucket, Prefix=f"transfers/e{pipeline_run_history_id}")
22
- if 'Contents' in response:
23
- for key in response['Contents']:
24
- keys.append(key['Key'])
25
- return keys
26
-
27
- def get_dev_keys(self, prefix):
28
- keys = []
29
- response = s3_client.list_objects(Bucket=self.s3_bucket, Prefix=f"devTransfers/e{prefix}")
30
- if 'Contents' in response:
31
- for key in response['Contents']:
32
- keys.append(key['Key'])
33
- return keys
34
-
35
- def download_object(self, key_name):
36
- file = s3_resource.Object(self.s3_bucket, key_name)
37
- return file.get()['Body']
38
-
39
- def upload_object(self, key_name, json_buffer=None, file_path=None):
40
- if json_buffer is not None:
41
- s3_resource.Object(
42
- self.s3_bucket, key_name).put(Body=json_buffer.getvalue())
43
- elif file_path is not None:
44
- s3_client.upload_file(file_path, self.s3_bucket, key_name)
45
- else:
46
- raise ValueError("Either json_buffer or file_path must be provided")
47
-
48
- def get_object_size(self, key_name):
49
- response = s3_client.head_object(
50
- Bucket=self.s3_bucket, Key=key_name)
51
- return float(response['ContentLength'])
52
-
53
- def delete_object(self, key_name):
54
- s3_client.delete_object(
55
- Bucket=self.s3_bucket,
56
- Key=key_name,
57
- )
58
-
59
- def start_ecs_task(self, task_name:str, task_version:str, pipeline_id: str, pipeline_run_history_id: str):
60
- response = ecs_resource.run_task(
61
- cluster=self._ecs_cluster_name,
62
- count=1,
63
- enableECSManagedTags=False,
64
- enableExecuteCommand=False,
65
- launchType='FARGATE',
66
- networkConfiguration={
67
- 'awsvpcConfiguration': {
68
- 'subnets': [
69
- 'subnet-095c3230c890e0841',
70
- ],
71
- 'securityGroups': [
72
- 'sg-08d46d487577cbdf9',
73
- ],
74
- 'assignPublicIp': 'ENABLED'
75
- }
76
- },
77
- overrides={
78
- 'containerOverrides': [
79
- {
80
- 'name': task_name,
81
- 'command': ['app.py'],
82
- 'environment': [
83
- {
84
- 'name': 'PIPELINE_ID',
85
- 'value': pipeline_id
86
- },
87
- {
88
- 'name': 'PIPELINE_RUN_HISTORY_ID',
89
- 'value': pipeline_run_history_id
90
- },
91
- {
92
- 'name': 'TASK',
93
- 'value': 'DESTINATION'
94
- },
95
- {
96
- 'name': 'APP_ENV',
97
- 'value': os.environ.get("APP_ENV")
98
- },
99
- {
100
- 'name': 'SERVER_ENDPOINT',
101
- 'value': os.environ.get("SERVER_ENDPOINT")
102
- },
103
- {
104
- 'name': 'SERVER_API_KEY',
105
- 'value': os.environ.get("SERVER_API_KEY")
106
- },
107
- {
108
- 'name': 'AWS_DEFAULT_REGION',
109
- 'value': os.environ.get("AWS_DEFAULT_REGION")
110
- }
111
- ],
112
- },
113
- ],
114
- },
115
- platformVersion='LATEST',
116
- taskDefinition=f"{task_name}:{task_version}"
117
- )
118
-
119
- if len(response['tasks']) == 0:
120
- raise Exception("Failed to start next connector")
121
-
122
- return response['tasks'][0]['taskArn'].split("/")[-1]
123
-
124
- def decrypt_customer_data(self, encrypted_data: str, customer_id: str) -> str:
125
- """Decrypt data with customer context"""
126
- try:
127
- response = kms_client.decrypt(
128
- CiphertextBlob=b64decode(encrypted_data.encode('utf-8')),
129
- EncryptionContext={'CustomerId': customer_id}
130
- )
131
- if 'Plaintext' not in response:
132
- raise Exception('Decryption failed')
133
-
134
- return response['Plaintext'].decode('utf-8')
135
- except Exception as e:
136
- raise Exception(f'Decryption failed: {str(e)}')
137
-
138
- def decrypt_customer_data_object(self, encrypted_data: str, customer_id: str) -> Dict:
139
- """Decrypt JSON data with customer context"""
140
- data = self.decrypt_customer_data(encrypted_data, customer_id)
141
- return json.loads(data)
File without changes
File without changes