dc-python-sdk 1.5.48__tar.gz → 1.5.50__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.
- {dc_python_sdk-1.5.48/src/dc_python_sdk.egg-info → dc_python_sdk-1.5.50}/PKG-INFO +1 -1
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/pyproject.toml +1 -1
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/setup.cfg +1 -1
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50/src/dc_python_sdk.egg-info}/PKG-INFO +1 -1
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_python_sdk.egg-info/SOURCES.txt +3 -0
- dc_python_sdk-1.5.50/src/dc_sdk/__init__.py +3 -0
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_sdk/cli.py +0 -2
- dc_python_sdk-1.5.50/src/dc_sdk/data_stream.py +33 -0
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_sdk/handler.py +34 -3
- dc_python_sdk-1.5.50/src/dc_sdk/src/connection_status.py +34 -0
- dc_python_sdk-1.5.50/src/dc_sdk/src/destination_object_template.py +74 -0
- dc_python_sdk-1.5.50/src/dc_sdk/src/mapping.py +191 -0
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_sdk/src/pipeline.py +140 -5
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_sdk/src/services/aws.py +52 -1
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_sdk/types.py +11 -1
- dc_python_sdk-1.5.48/src/dc_sdk/src/mapping.py +0 -109
- dc_python_sdk-1.5.48/src/dc_sdk/src/services/__init__.py +0 -0
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/LICENSE +0 -0
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/README.md +0 -0
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_python_sdk.egg-info/dependency_links.txt +0 -0
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_python_sdk.egg-info/entry_points.txt +0 -0
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_python_sdk.egg-info/requires.txt +0 -0
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_python_sdk.egg-info/top_level.txt +0 -0
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_sdk/app.py +0 -0
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_sdk/errors.py +0 -0
- {dc_python_sdk-1.5.48/src/dc_sdk → dc_python_sdk-1.5.50/src/dc_sdk/src}/__init__.py +0 -0
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_sdk/src/ai.py +0 -0
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_sdk/src/ai_http.py +0 -0
- {dc_python_sdk-1.5.48/src/dc_sdk/src → dc_python_sdk-1.5.50/src/dc_sdk/src/models}/__init__.py +0 -0
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_sdk/src/models/enums.py +0 -0
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_sdk/src/models/errors.py +0 -0
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_sdk/src/models/log_templates.py +0 -0
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_sdk/src/models/pipeline_details.py +0 -0
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_sdk/src/server.py +0 -0
- {dc_python_sdk-1.5.48/src/dc_sdk/src/models → dc_python_sdk-1.5.50/src/dc_sdk/src/services}/__init__.py +0 -0
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_sdk/src/services/api.py +0 -0
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_sdk/src/services/environment.py +0 -0
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_sdk/src/services/loader.py +0 -0
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_sdk/src/services/logger.py +0 -0
- {dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_sdk/src/services/session.py +0 -0
|
@@ -11,12 +11,15 @@ src/dc_python_sdk.egg-info/top_level.txt
|
|
|
11
11
|
src/dc_sdk/__init__.py
|
|
12
12
|
src/dc_sdk/app.py
|
|
13
13
|
src/dc_sdk/cli.py
|
|
14
|
+
src/dc_sdk/data_stream.py
|
|
14
15
|
src/dc_sdk/errors.py
|
|
15
16
|
src/dc_sdk/handler.py
|
|
16
17
|
src/dc_sdk/types.py
|
|
17
18
|
src/dc_sdk/src/__init__.py
|
|
18
19
|
src/dc_sdk/src/ai.py
|
|
19
20
|
src/dc_sdk/src/ai_http.py
|
|
21
|
+
src/dc_sdk/src/connection_status.py
|
|
22
|
+
src/dc_sdk/src/destination_object_template.py
|
|
20
23
|
src/dc_sdk/src/mapping.py
|
|
21
24
|
src/dc_sdk/src/pipeline.py
|
|
22
25
|
src/dc_sdk/src/server.py
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any, Dict, Iterable, Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class DataStream:
|
|
7
|
+
"""Byte stream returned by connector get_data_stream for transfer/upload."""
|
|
8
|
+
|
|
9
|
+
stream: Iterable[bytes]
|
|
10
|
+
|
|
11
|
+
# What are the bytes?
|
|
12
|
+
media_type: str
|
|
13
|
+
|
|
14
|
+
# Optional processing hints
|
|
15
|
+
container: Optional[str] = None # zip, gzip, tar
|
|
16
|
+
format: Optional[str] = None # csv, ndjson, parquet, xml, xlsx
|
|
17
|
+
|
|
18
|
+
# Optional metadata
|
|
19
|
+
filename: Optional[str] = None
|
|
20
|
+
encoding: Optional[str] = None
|
|
21
|
+
|
|
22
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
23
|
+
|
|
24
|
+
def hints(self) -> Dict[str, Any]:
|
|
25
|
+
"""Serializable extract/normalize hints (excludes the live stream)."""
|
|
26
|
+
return {
|
|
27
|
+
"media_type": self.media_type,
|
|
28
|
+
"container": self.container,
|
|
29
|
+
"format": self.format,
|
|
30
|
+
"filename": self.filename,
|
|
31
|
+
"encoding": self.encoding,
|
|
32
|
+
"metadata": self.metadata,
|
|
33
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from dc_sdk.errors import Error
|
|
2
2
|
from dc_sdk.src.mapping import Mapping
|
|
3
|
+
from dc_sdk.src.connection_status import STATUS_ERROR
|
|
3
4
|
import traceback
|
|
4
5
|
from importlib.metadata import version
|
|
5
6
|
|
|
@@ -9,7 +10,9 @@ def get_action_name(action: int) -> str:
|
|
|
9
10
|
1: "retrieving fields",
|
|
10
11
|
2: "retrieving 5 row preview",
|
|
11
12
|
3: "testing connection",
|
|
12
|
-
4: "starting the connector"
|
|
13
|
+
4: "starting the connector",
|
|
14
|
+
5: "retrieving metadata",
|
|
15
|
+
6: "retrieving objects",
|
|
13
16
|
}[action]
|
|
14
17
|
|
|
15
18
|
def apply_data_filters(results, data_filters):
|
|
@@ -159,6 +162,9 @@ def handler(event, context):
|
|
|
159
162
|
mapping = None
|
|
160
163
|
action = int(event['action']) if 'action' in event else 4
|
|
161
164
|
get_objects = event.get('get_objects', True)
|
|
165
|
+
skip_authenticate = event.get('skip_authenticate', False)
|
|
166
|
+
force_authenticate = event.get('force_authenticate', False)
|
|
167
|
+
include_metadata = event.get('include_metadata', True)
|
|
162
168
|
credentials_dict = event['credentials'] if 'credentials' in event else None
|
|
163
169
|
object_id = event['object_id'] if 'object_id' in event else None
|
|
164
170
|
field_ids = event['mapping'] if 'mapping' in event else None
|
|
@@ -174,9 +180,14 @@ def handler(event, context):
|
|
|
174
180
|
|
|
175
181
|
if action == 0:
|
|
176
182
|
if get_objects:
|
|
177
|
-
results, message = mapping.connect_get_objects(
|
|
183
|
+
results, message = mapping.connect_get_objects(
|
|
184
|
+
skip_authenticate=skip_authenticate,
|
|
185
|
+
force_authenticate=force_authenticate,
|
|
186
|
+
)
|
|
178
187
|
else:
|
|
179
|
-
results, message = mapping.authenticate(
|
|
188
|
+
results, message = mapping.authenticate(
|
|
189
|
+
force_authenticate=force_authenticate or not skip_authenticate,
|
|
190
|
+
)
|
|
180
191
|
elif action == 1:
|
|
181
192
|
results, message = mapping.get_fields(object_id, options)
|
|
182
193
|
elif action == 2:
|
|
@@ -185,6 +196,17 @@ def handler(event, context):
|
|
|
185
196
|
results = apply_data_filters(results, data_filters)
|
|
186
197
|
elif action == 3:
|
|
187
198
|
results, message = mapping.test_connection()
|
|
199
|
+
elif action == 5:
|
|
200
|
+
results, message = mapping.get_metadata_only(
|
|
201
|
+
skip_authenticate=skip_authenticate,
|
|
202
|
+
force_authenticate=force_authenticate,
|
|
203
|
+
)
|
|
204
|
+
elif action == 6:
|
|
205
|
+
results, message = mapping.get_objects_only(
|
|
206
|
+
skip_authenticate=skip_authenticate,
|
|
207
|
+
force_authenticate=force_authenticate,
|
|
208
|
+
include_metadata=include_metadata,
|
|
209
|
+
)
|
|
188
210
|
else:
|
|
189
211
|
raise Error("Invalid action", "InvalidActionError")
|
|
190
212
|
except Error as e:
|
|
@@ -192,19 +214,28 @@ def handler(event, context):
|
|
|
192
214
|
internal_error = e.internal
|
|
193
215
|
error_trace = traceback.format_exc()
|
|
194
216
|
error = error_trace
|
|
217
|
+
if mapping:
|
|
218
|
+
mapping._set_connection_status(STATUS_ERROR)
|
|
195
219
|
|
|
196
220
|
except Exception as e:
|
|
197
221
|
error_trace = traceback.format_exc()
|
|
198
222
|
error = error_trace
|
|
199
223
|
internal_error = True
|
|
224
|
+
if mapping:
|
|
225
|
+
mapping._set_connection_status(STATUS_ERROR)
|
|
200
226
|
|
|
201
227
|
if error:
|
|
202
228
|
print(error)
|
|
203
229
|
|
|
230
|
+
last_status_dsc = None
|
|
231
|
+
if mapping and mapping.connector.credentials:
|
|
232
|
+
last_status_dsc = mapping.connector.credentials.get("last_status_dsc")
|
|
233
|
+
|
|
204
234
|
return {
|
|
205
235
|
'message': message if not internal_error else f"Something went wrong with {action_name}",
|
|
206
236
|
'results': results,
|
|
207
237
|
'credentials': mapping.connector.credentials if mapping else None,
|
|
238
|
+
'last_status_dsc': last_status_dsc,
|
|
208
239
|
'error': error,
|
|
209
240
|
'internal_error': internal_error
|
|
210
241
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
STATUS_CONNECTED = "Connected"
|
|
2
|
+
STATUS_ERROR = "Error"
|
|
3
|
+
STATUS_UNKNOWN = "Unknown"
|
|
4
|
+
STATUS_NOT_SETUP = "NotSetup"
|
|
5
|
+
STATUS_PENDING = "Pending"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def normalize_status(status):
|
|
9
|
+
if not status:
|
|
10
|
+
return STATUS_UNKNOWN
|
|
11
|
+
|
|
12
|
+
normalized = str(status).strip()
|
|
13
|
+
lowered = normalized.lower()
|
|
14
|
+
|
|
15
|
+
if lowered in ("connected", "success"):
|
|
16
|
+
return STATUS_CONNECTED
|
|
17
|
+
if lowered in ("error", "failed", "failed to connect"):
|
|
18
|
+
return STATUS_ERROR
|
|
19
|
+
if lowered in ("notsetup", "not_setup", "not setup"):
|
|
20
|
+
return STATUS_NOT_SETUP
|
|
21
|
+
if lowered == "pending":
|
|
22
|
+
return STATUS_PENDING
|
|
23
|
+
if lowered == "unknown":
|
|
24
|
+
return STATUS_UNKNOWN
|
|
25
|
+
|
|
26
|
+
return normalized
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def should_skip_authenticate(status):
|
|
30
|
+
return normalize_status(status) == STATUS_CONNECTED
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def status_from_authenticated(authenticated):
|
|
34
|
+
return STATUS_CONNECTED if authenticated else STATUS_ERROR
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Resolve dynamic tokens in destination object paths / file names at run time."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
_TOKEN_RE = re.compile(r"\{([A-Za-z0-9_-]+)\}")
|
|
10
|
+
|
|
11
|
+
_DATE_FORMATS = {
|
|
12
|
+
"YYYYMMDD": "%Y%m%d",
|
|
13
|
+
"YYYY-MM-DD": "%Y-%m-%d",
|
|
14
|
+
"YYMMDD": "%y%m%d",
|
|
15
|
+
"HHMMSS": "%H%M%S",
|
|
16
|
+
"YYYYMMDDHHMMSS": "%Y%m%d%H%M%S",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _slug(value: Optional[str]) -> str:
|
|
21
|
+
if value is None:
|
|
22
|
+
return ""
|
|
23
|
+
text = str(value).strip()
|
|
24
|
+
if not text:
|
|
25
|
+
return ""
|
|
26
|
+
text = re.sub(r"[^A-Za-z0-9]+", "_", text)
|
|
27
|
+
return text.strip("_")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def resolve_destination_object_template(
|
|
31
|
+
template: Optional[str],
|
|
32
|
+
*,
|
|
33
|
+
source_object_id: Optional[str] = None,
|
|
34
|
+
source_connector_nm: Optional[str] = None,
|
|
35
|
+
when: Optional[datetime] = None,
|
|
36
|
+
) -> str:
|
|
37
|
+
"""
|
|
38
|
+
Expand `{YYYYMMDD}`, `{object}`, `{connector}`, etc. in a destination path.
|
|
39
|
+
|
|
40
|
+
Unknown tokens are left unchanged so mis-typed braces remain visible in logs.
|
|
41
|
+
"""
|
|
42
|
+
if template is None:
|
|
43
|
+
return ""
|
|
44
|
+
value = str(template)
|
|
45
|
+
if "{" not in value:
|
|
46
|
+
return value
|
|
47
|
+
|
|
48
|
+
now = when or datetime.now(timezone.utc)
|
|
49
|
+
if now.tzinfo is None:
|
|
50
|
+
now = now.replace(tzinfo=timezone.utc)
|
|
51
|
+
|
|
52
|
+
replacements = {
|
|
53
|
+
"object": _slug(source_object_id),
|
|
54
|
+
"connector": _slug(source_connector_nm),
|
|
55
|
+
}
|
|
56
|
+
for token, fmt in _DATE_FORMATS.items():
|
|
57
|
+
replacements[token] = now.strftime(fmt)
|
|
58
|
+
|
|
59
|
+
def _replace(match: re.Match) -> str:
|
|
60
|
+
key = match.group(1)
|
|
61
|
+
if key in replacements:
|
|
62
|
+
return replacements[key]
|
|
63
|
+
# Autocomplete may insert uppercase tokens; users often type lowercase
|
|
64
|
+
# (`{hhmmss}`). Resolve date formats case-insensitively, and
|
|
65
|
+
# `{object}` / `{connector}` case-insensitively.
|
|
66
|
+
upper = key.upper()
|
|
67
|
+
if upper in _DATE_FORMATS:
|
|
68
|
+
return replacements[upper]
|
|
69
|
+
lower = key.lower()
|
|
70
|
+
if lower in ("object", "connector"):
|
|
71
|
+
return replacements[lower]
|
|
72
|
+
return match.group(0)
|
|
73
|
+
|
|
74
|
+
return _TOKEN_RE.sub(_replace, value)
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
from dc_sdk import errors
|
|
2
|
+
import logging
|
|
3
|
+
from dc_sdk.src.services.loader import load_connector
|
|
4
|
+
from dc_sdk.src.connection_status import (
|
|
5
|
+
STATUS_CONNECTED,
|
|
6
|
+
STATUS_ERROR,
|
|
7
|
+
should_skip_authenticate,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Mapping():
|
|
14
|
+
def __init__(self, credentials):
|
|
15
|
+
Connector = load_connector()
|
|
16
|
+
self.connector = Connector(credentials)
|
|
17
|
+
|
|
18
|
+
def _set_connection_status(self, status):
|
|
19
|
+
if self.connector.credentials is None:
|
|
20
|
+
self.connector.credentials = {}
|
|
21
|
+
self.connector.credentials["last_status_dsc"] = status
|
|
22
|
+
|
|
23
|
+
def _initialize_connector_session(self):
|
|
24
|
+
init = getattr(self.connector, "ensure_initialized", None)
|
|
25
|
+
if callable(init):
|
|
26
|
+
init()
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
legacy_init = getattr(self.connector, "_ensure_initialized", None)
|
|
30
|
+
if callable(legacy_init):
|
|
31
|
+
legacy_init()
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
if not self.connector.authenticate():
|
|
35
|
+
raise errors.AuthenticationError(message="Failed to initialize connection session")
|
|
36
|
+
|
|
37
|
+
def _ensure_session(self, force_authenticate=False):
|
|
38
|
+
credentials = self.connector.credentials or {}
|
|
39
|
+
status = credentials.get("last_status_dsc")
|
|
40
|
+
|
|
41
|
+
if not force_authenticate and should_skip_authenticate(status):
|
|
42
|
+
try:
|
|
43
|
+
self._initialize_connector_session()
|
|
44
|
+
return True
|
|
45
|
+
except Exception:
|
|
46
|
+
self._set_connection_status(STATUS_ERROR)
|
|
47
|
+
raise
|
|
48
|
+
|
|
49
|
+
if self.connector.authenticate():
|
|
50
|
+
self._set_connection_status(STATUS_CONNECTED)
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
self._set_connection_status(STATUS_ERROR)
|
|
54
|
+
raise errors.AuthenticationError(message="Failed to authenticate")
|
|
55
|
+
|
|
56
|
+
def _resolve_session(self, skip_authenticate=False, force_authenticate=False):
|
|
57
|
+
if force_authenticate:
|
|
58
|
+
self._ensure_session(force_authenticate=True)
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
credentials = self.connector.credentials or {}
|
|
62
|
+
status = credentials.get("last_status_dsc")
|
|
63
|
+
|
|
64
|
+
if skip_authenticate and should_skip_authenticate(status):
|
|
65
|
+
self._ensure_session(force_authenticate=False)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
if skip_authenticate and not should_skip_authenticate(status):
|
|
69
|
+
self._ensure_session(force_authenticate=True)
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
self._ensure_session(force_authenticate=False)
|
|
73
|
+
|
|
74
|
+
def _safe_get_metadata(self):
|
|
75
|
+
try:
|
|
76
|
+
return self.connector.get_metadata()
|
|
77
|
+
except errors.NotImplementedError:
|
|
78
|
+
return None
|
|
79
|
+
except Exception:
|
|
80
|
+
logger.exception("Error getting metadata")
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
def authenticate(self, force_authenticate=True):
|
|
84
|
+
results = None
|
|
85
|
+
message = None
|
|
86
|
+
|
|
87
|
+
self._ensure_session(force_authenticate=force_authenticate)
|
|
88
|
+
|
|
89
|
+
metadata = self._safe_get_metadata()
|
|
90
|
+
results = {
|
|
91
|
+
"metadata": metadata
|
|
92
|
+
}
|
|
93
|
+
message = "Authenticated successfully"
|
|
94
|
+
|
|
95
|
+
return [results, message]
|
|
96
|
+
|
|
97
|
+
def connect_get_objects(self, skip_authenticate=False, force_authenticate=False):
|
|
98
|
+
results = None
|
|
99
|
+
metadata = None
|
|
100
|
+
objects = None
|
|
101
|
+
message = None
|
|
102
|
+
|
|
103
|
+
self._resolve_session(
|
|
104
|
+
skip_authenticate=skip_authenticate,
|
|
105
|
+
force_authenticate=force_authenticate,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
objects = self.connector.get_objects()
|
|
109
|
+
metadata = self._safe_get_metadata()
|
|
110
|
+
|
|
111
|
+
results = {
|
|
112
|
+
"metadata": metadata,
|
|
113
|
+
"objects": objects,
|
|
114
|
+
}
|
|
115
|
+
message = "Retrieved objects"
|
|
116
|
+
|
|
117
|
+
return [results, message]
|
|
118
|
+
|
|
119
|
+
def get_metadata_only(self, skip_authenticate=False, force_authenticate=False):
|
|
120
|
+
self._resolve_session(
|
|
121
|
+
skip_authenticate=skip_authenticate,
|
|
122
|
+
force_authenticate=force_authenticate,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
metadata = self._safe_get_metadata()
|
|
126
|
+
return [{"metadata": metadata}, "Retrieved metadata"]
|
|
127
|
+
|
|
128
|
+
def get_objects_only(self, skip_authenticate=False, force_authenticate=False, include_metadata=True):
|
|
129
|
+
self._resolve_session(
|
|
130
|
+
skip_authenticate=skip_authenticate,
|
|
131
|
+
force_authenticate=force_authenticate,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
objects = self.connector.get_objects()
|
|
135
|
+
results = {"objects": objects}
|
|
136
|
+
|
|
137
|
+
if include_metadata:
|
|
138
|
+
results["metadata"] = self._safe_get_metadata()
|
|
139
|
+
|
|
140
|
+
return [results, "Retrieved objects"]
|
|
141
|
+
|
|
142
|
+
def test_connection(self):
|
|
143
|
+
results = None
|
|
144
|
+
message = None
|
|
145
|
+
|
|
146
|
+
self._ensure_session(force_authenticate=True)
|
|
147
|
+
message = "Authenticated successfully"
|
|
148
|
+
|
|
149
|
+
return [results, message]
|
|
150
|
+
|
|
151
|
+
def get_fields(self, object_id, options):
|
|
152
|
+
results = None
|
|
153
|
+
message = None
|
|
154
|
+
self._ensure_session(force_authenticate=False)
|
|
155
|
+
results = self.connector.get_fields(object_id, options=options)
|
|
156
|
+
message = "Retrieved fields"
|
|
157
|
+
|
|
158
|
+
return [results, message]
|
|
159
|
+
|
|
160
|
+
def get_five_row_preview(self, object_id, field_ids, options, n_rows=5, filters=None, next_page=None):
|
|
161
|
+
results = None
|
|
162
|
+
message = None
|
|
163
|
+
|
|
164
|
+
self._ensure_session(force_authenticate=False)
|
|
165
|
+
results = self.connector.get_data(
|
|
166
|
+
object_id,
|
|
167
|
+
field_ids=field_ids,
|
|
168
|
+
n_rows=n_rows,
|
|
169
|
+
filters=filters,
|
|
170
|
+
next_page=next_page,
|
|
171
|
+
options=options,
|
|
172
|
+
)
|
|
173
|
+
message = "Retrieved 5 row preview"
|
|
174
|
+
|
|
175
|
+
return [results, message]
|
|
176
|
+
|
|
177
|
+
def get_data(self, object_id, field_ids, n_rows, next_page, options):
|
|
178
|
+
results = None
|
|
179
|
+
message = None
|
|
180
|
+
|
|
181
|
+
self._ensure_session(force_authenticate=False)
|
|
182
|
+
results = self.connector.get_data(
|
|
183
|
+
object_id,
|
|
184
|
+
field_ids=field_ids,
|
|
185
|
+
n_rows=n_rows,
|
|
186
|
+
next_page=next_page,
|
|
187
|
+
options=options,
|
|
188
|
+
)
|
|
189
|
+
message = "Retrieved data"
|
|
190
|
+
|
|
191
|
+
return [results, message]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json, io, math, re, inspect
|
|
2
|
+
import os
|
|
2
3
|
import time
|
|
3
4
|
from .services.environment import PipelineEnvironment
|
|
4
5
|
from .services.api import DataConnectorAPI
|
|
@@ -6,10 +7,26 @@ from .models.log_templates import LogTemplates
|
|
|
6
7
|
from .services.aws import AwsService
|
|
7
8
|
from .models import errors
|
|
8
9
|
from .services.loader import load_connector
|
|
10
|
+
from .destination_object_template import resolve_destination_object_template
|
|
11
|
+
from dc_sdk.data_stream import DataStream
|
|
9
12
|
|
|
10
13
|
TEMP_UPLOADS = 'temporary-files'
|
|
11
14
|
SUB_FOLDER = 'etlJobHistory'
|
|
12
15
|
|
|
16
|
+
_FORMAT_EXTENSIONS = {
|
|
17
|
+
"csv": ".csv",
|
|
18
|
+
"ndjson": ".ndjson",
|
|
19
|
+
"json": ".json",
|
|
20
|
+
"parquet": ".parquet",
|
|
21
|
+
"xml": ".xml",
|
|
22
|
+
"xlsx": ".xlsx",
|
|
23
|
+
}
|
|
24
|
+
_CONTAINER_EXTENSIONS = {
|
|
25
|
+
"zip": ".zip",
|
|
26
|
+
"gzip": ".gz",
|
|
27
|
+
"tar": ".tar",
|
|
28
|
+
}
|
|
29
|
+
|
|
13
30
|
|
|
14
31
|
class PipelineConductor:
|
|
15
32
|
def __init__(self,
|
|
@@ -159,9 +176,49 @@ class PipelineConductor:
|
|
|
159
176
|
|
|
160
177
|
self.log(self.log_templates.GET_DATA_FINISH.format(self.row_count, self.pipeline_details.source_object_id))
|
|
161
178
|
|
|
179
|
+
def get_data_stream(self):
|
|
180
|
+
"""
|
|
181
|
+
POC: pull a DataStream from the connector and upload raw bytes to S3.
|
|
182
|
+
|
|
183
|
+
Not wired into the default SOURCE path yet — call explicitly when testing.
|
|
184
|
+
Retains extract/normalize hints via a sibling .meta.json object.
|
|
185
|
+
"""
|
|
186
|
+
if not hasattr(self.connector, "get_data_stream"):
|
|
187
|
+
raise errors.DataError(
|
|
188
|
+
"Connector does not implement get_data_stream; cannot use stream transfer."
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
self.log(self.log_templates.GET_DATA_START.format(
|
|
192
|
+
self.pipeline_details.source_object_id,
|
|
193
|
+
", ".join(str(fid) for fid in self._get_field_ids())
|
|
194
|
+
))
|
|
195
|
+
|
|
196
|
+
data_stream = self.connector.get_data_stream(
|
|
197
|
+
self.pipeline_details.source_object_id,
|
|
198
|
+
self._get_field_ids(),
|
|
199
|
+
filters=self._get_filters(),
|
|
200
|
+
options=self.pipeline_details.options,
|
|
201
|
+
)
|
|
202
|
+
if not isinstance(data_stream, DataStream):
|
|
203
|
+
raise errors.DataError(
|
|
204
|
+
"get_data_stream must return a dc_sdk.data_stream.DataStream instance."
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
key_name = self._process_data_stream(data_stream)
|
|
208
|
+
|
|
209
|
+
self.log(self.log_templates.GET_DATA_FINISH.format(
|
|
210
|
+
self.row_count, self.pipeline_details.source_object_id
|
|
211
|
+
))
|
|
212
|
+
return key_name
|
|
213
|
+
|
|
162
214
|
def load_data(self, batch_start):
|
|
163
215
|
self._ensure_mapping_has_default_data_types()
|
|
164
|
-
|
|
216
|
+
destination_object_id = resolve_destination_object_template(
|
|
217
|
+
self.pipeline_details.destination_object_id,
|
|
218
|
+
source_object_id=getattr(self.pipeline_details, "source_object_id", None),
|
|
219
|
+
source_connector_nm=getattr(self.pipeline_details, "source_connector_nm", None),
|
|
220
|
+
)
|
|
221
|
+
self.log(self.log_templates.LOAD_DATA_START.format(destination_object_id, len(self._get_field_ids())))
|
|
165
222
|
|
|
166
223
|
keys = []
|
|
167
224
|
if PipelineEnvironment.platform == "aws":
|
|
@@ -171,7 +228,7 @@ class PipelineConductor:
|
|
|
171
228
|
|
|
172
229
|
if not keys:
|
|
173
230
|
# Call load_data with empty data when there are no keys
|
|
174
|
-
loaded = self._call_connector_load_data([],
|
|
231
|
+
loaded = self._call_connector_load_data([], destination_object_id, self._get_mapping(), self.pipeline_details.update_method_cd, 0, 1)
|
|
175
232
|
if not loaded:
|
|
176
233
|
raise errors.LoadDataError("Loading data failed.")
|
|
177
234
|
else:
|
|
@@ -184,8 +241,8 @@ class PipelineConductor:
|
|
|
184
241
|
|
|
185
242
|
|
|
186
243
|
data = json.load(file_object)
|
|
187
|
-
self.log(self.log_templates.LOAD_DATA_LOADED.format(len(data),
|
|
188
|
-
loaded = self._call_connector_load_data(data,
|
|
244
|
+
self.log(self.log_templates.LOAD_DATA_LOADED.format(len(data), destination_object_id))
|
|
245
|
+
loaded = self._call_connector_load_data(data, destination_object_id, self._get_mapping(), self.pipeline_details.update_method_cd, index, len(keys))
|
|
189
246
|
if loaded:
|
|
190
247
|
self.row_count += len(data)
|
|
191
248
|
if self.mode == "prod":
|
|
@@ -196,7 +253,7 @@ class PipelineConductor:
|
|
|
196
253
|
self.aws.delete_object(key)
|
|
197
254
|
else:
|
|
198
255
|
raise errors.LoadDataError("Loading data failed.")
|
|
199
|
-
self.log(self.log_templates.LOAD_DATA_FINISHED.format(self.row_count,
|
|
256
|
+
self.log(self.log_templates.LOAD_DATA_FINISHED.format(self.row_count, destination_object_id))
|
|
200
257
|
self._persist_sync_cursor_after_load()
|
|
201
258
|
|
|
202
259
|
def _create_connector(self, connector_cls, credentials):
|
|
@@ -422,6 +479,84 @@ class PipelineConductor:
|
|
|
422
479
|
|
|
423
480
|
return limit_reached
|
|
424
481
|
|
|
482
|
+
def _process_data_stream(self, data_stream: DataStream):
|
|
483
|
+
"""Upload a DataStream's bytes to object storage and persist normalize hints."""
|
|
484
|
+
self.batch_index += 1
|
|
485
|
+
extension = self._data_stream_extension(data_stream)
|
|
486
|
+
|
|
487
|
+
if self.mode == "prod":
|
|
488
|
+
key_name = f"e{self.pipeline_run_history_id}-b{self.batch_index}{extension}"
|
|
489
|
+
else:
|
|
490
|
+
key_name = f"devTransfers/e{self.prefix}-b{self.batch_index}{extension}"
|
|
491
|
+
|
|
492
|
+
if PipelineEnvironment.platform == "aws":
|
|
493
|
+
key_name = f"transfers/{key_name}"
|
|
494
|
+
self.aws.upload_stream(
|
|
495
|
+
key_name,
|
|
496
|
+
data_stream.stream,
|
|
497
|
+
content_type=data_stream.media_type,
|
|
498
|
+
)
|
|
499
|
+
meta_key = f"{key_name}.meta.json"
|
|
500
|
+
meta_buffer = io.StringIO(json.dumps(data_stream.hints()))
|
|
501
|
+
self.aws.upload_object(meta_key, json_buffer=meta_buffer)
|
|
502
|
+
self.bytes_transferred += self.aws.get_object_size(key_name)
|
|
503
|
+
elif PipelineEnvironment.platform == "azure":
|
|
504
|
+
key_name = f"{PipelineEnvironment.app_env}/{key_name}"
|
|
505
|
+
if not hasattr(self.azure, "upload_stream"):
|
|
506
|
+
raise errors.DataError(
|
|
507
|
+
"Azure upload_stream is not implemented for DataStream POC."
|
|
508
|
+
)
|
|
509
|
+
self.azure.upload_stream(
|
|
510
|
+
key_name,
|
|
511
|
+
data_stream.stream,
|
|
512
|
+
content_type=data_stream.media_type,
|
|
513
|
+
)
|
|
514
|
+
meta_key = f"{key_name}.meta.json"
|
|
515
|
+
meta_buffer = io.StringIO(json.dumps(data_stream.hints()))
|
|
516
|
+
self.azure.upload_object(meta_key, json_buffer=meta_buffer)
|
|
517
|
+
self.bytes_transferred += self.azure.get_object_size(key_name)
|
|
518
|
+
else:
|
|
519
|
+
raise errors.DataError(
|
|
520
|
+
f"Unsupported platform for DataStream upload: {PipelineEnvironment.platform}"
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
self.successful_keys.append(key_name)
|
|
524
|
+
self.internal_log(self.log_templates.INTERNAL_S3_UPLOAD_LOG.format(key_name))
|
|
525
|
+
|
|
526
|
+
row_count = data_stream.metadata.get("row_count")
|
|
527
|
+
if row_count is not None:
|
|
528
|
+
try:
|
|
529
|
+
self.row_count += int(row_count)
|
|
530
|
+
except (TypeError, ValueError):
|
|
531
|
+
pass
|
|
532
|
+
|
|
533
|
+
if self.mode == "prod":
|
|
534
|
+
self.update_history({
|
|
535
|
+
"updateAction": "ROWS_RETRIEVED",
|
|
536
|
+
"RowsRetrievedNBR": self.row_count,
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
return key_name
|
|
540
|
+
|
|
541
|
+
@staticmethod
|
|
542
|
+
def _data_stream_extension(data_stream: DataStream) -> str:
|
|
543
|
+
if data_stream.filename:
|
|
544
|
+
_, ext = os.path.splitext(data_stream.filename)
|
|
545
|
+
if ext:
|
|
546
|
+
return ext.lower()
|
|
547
|
+
|
|
548
|
+
if data_stream.container:
|
|
549
|
+
container_ext = _CONTAINER_EXTENSIONS.get(data_stream.container.lower())
|
|
550
|
+
if container_ext:
|
|
551
|
+
return container_ext
|
|
552
|
+
|
|
553
|
+
if data_stream.format:
|
|
554
|
+
format_ext = _FORMAT_EXTENSIONS.get(data_stream.format.lower())
|
|
555
|
+
if format_ext:
|
|
556
|
+
return format_ext
|
|
557
|
+
|
|
558
|
+
return ".bin"
|
|
559
|
+
|
|
425
560
|
def _get_credentials(self):
|
|
426
561
|
is_source = self.task == "SOURCE"
|
|
427
562
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import boto3
|
|
3
3
|
import json
|
|
4
|
-
from typing import Dict
|
|
4
|
+
from typing import Dict, Iterable, Optional
|
|
5
5
|
from base64 import b64decode
|
|
6
6
|
from Crypto.Cipher import AES
|
|
7
7
|
|
|
@@ -14,6 +14,38 @@ s3_resource = boto3.resource('s3')
|
|
|
14
14
|
ecs_resource = boto3.client('ecs')
|
|
15
15
|
kms_client = boto3.client('kms')
|
|
16
16
|
|
|
17
|
+
|
|
18
|
+
class _ChunkIteratorReader:
|
|
19
|
+
"""File-like wrapper so boto3 can upload an Iterable[bytes] via upload_fileobj."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, chunks: Iterable[bytes]):
|
|
22
|
+
self._iterator = iter(chunks)
|
|
23
|
+
self._buffer = b""
|
|
24
|
+
self._exhausted = False
|
|
25
|
+
|
|
26
|
+
def read(self, size: int = -1) -> bytes:
|
|
27
|
+
if size == 0:
|
|
28
|
+
return b""
|
|
29
|
+
|
|
30
|
+
if size < 0:
|
|
31
|
+
parts = [self._buffer]
|
|
32
|
+
self._buffer = b""
|
|
33
|
+
if not self._exhausted:
|
|
34
|
+
parts.extend(self._iterator)
|
|
35
|
+
self._exhausted = True
|
|
36
|
+
return b"".join(parts)
|
|
37
|
+
|
|
38
|
+
while len(self._buffer) < size and not self._exhausted:
|
|
39
|
+
try:
|
|
40
|
+
self._buffer += next(self._iterator)
|
|
41
|
+
except StopIteration:
|
|
42
|
+
self._exhausted = True
|
|
43
|
+
|
|
44
|
+
out = self._buffer[:size]
|
|
45
|
+
self._buffer = self._buffer[size:]
|
|
46
|
+
return out
|
|
47
|
+
|
|
48
|
+
|
|
17
49
|
class AwsService:
|
|
18
50
|
def __init__(self, s3_bucket) -> None:
|
|
19
51
|
self.s3_bucket = s3_bucket
|
|
@@ -48,6 +80,25 @@ class AwsService:
|
|
|
48
80
|
else:
|
|
49
81
|
raise ValueError("Either json_buffer or file_path must be provided")
|
|
50
82
|
|
|
83
|
+
def upload_stream(
|
|
84
|
+
self,
|
|
85
|
+
key_name: str,
|
|
86
|
+
stream: Iterable[bytes],
|
|
87
|
+
content_type: Optional[str] = None,
|
|
88
|
+
):
|
|
89
|
+
"""Upload an iterable of byte chunks to S3 without requiring a local file."""
|
|
90
|
+
extra_args = {}
|
|
91
|
+
if content_type:
|
|
92
|
+
extra_args["ContentType"] = content_type
|
|
93
|
+
|
|
94
|
+
reader = _ChunkIteratorReader(stream)
|
|
95
|
+
if extra_args:
|
|
96
|
+
s3_client.upload_fileobj(
|
|
97
|
+
reader, self.s3_bucket, key_name, ExtraArgs=extra_args
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
s3_client.upload_fileobj(reader, self.s3_bucket, key_name)
|
|
101
|
+
|
|
51
102
|
def get_object_size(self, key_name):
|
|
52
103
|
response = s3_client.head_object(
|
|
53
104
|
Bucket=self.s3_bucket, Key=key_name)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from typing import Protocol, Any, Dict, List, Optional
|
|
2
2
|
|
|
3
|
+
from dc_sdk.data_stream import DataStream
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
class ConnectorProtocol(Protocol):
|
|
5
7
|
|
|
@@ -25,6 +27,14 @@ class ConnectorProtocol(Protocol):
|
|
|
25
27
|
options: Dict[str, Any] = {}
|
|
26
28
|
) -> Any: ...
|
|
27
29
|
|
|
30
|
+
def get_data_stream(
|
|
31
|
+
self,
|
|
32
|
+
object_id: str,
|
|
33
|
+
field_ids: List[str],
|
|
34
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
35
|
+
options: Dict[str, Any] = {}
|
|
36
|
+
) -> DataStream: ...
|
|
37
|
+
|
|
28
38
|
def load_data(
|
|
29
39
|
self,
|
|
30
40
|
data: List[Dict[str, Any]],
|
|
@@ -35,4 +45,4 @@ class ConnectorProtocol(Protocol):
|
|
|
35
45
|
total_batches: int
|
|
36
46
|
) -> Any: ...
|
|
37
47
|
|
|
38
|
-
def close(self) -> None: ...
|
|
48
|
+
def close(self) -> None: ...
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
from dc_sdk import errors
|
|
2
|
-
import logging
|
|
3
|
-
from dc_sdk.src.services.loader import load_connector
|
|
4
|
-
|
|
5
|
-
logger = logging.getLogger(__name__)
|
|
6
|
-
class Mapping():
|
|
7
|
-
def __init__(self, credentials):
|
|
8
|
-
Connector = load_connector()
|
|
9
|
-
self.connector = Connector(credentials)
|
|
10
|
-
|
|
11
|
-
def authenticate(self):
|
|
12
|
-
results = None
|
|
13
|
-
metadata = None
|
|
14
|
-
message = None
|
|
15
|
-
|
|
16
|
-
if self.connector.authenticate():
|
|
17
|
-
try:
|
|
18
|
-
metadata = self.connector.get_metadata()
|
|
19
|
-
except errors.NotImplementedError:
|
|
20
|
-
pass
|
|
21
|
-
except:
|
|
22
|
-
logger.exception("Error getting metadata")
|
|
23
|
-
|
|
24
|
-
results = {
|
|
25
|
-
'metadata': metadata
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
message = "Authenticated successfully"
|
|
29
|
-
else:
|
|
30
|
-
raise errors.AuthenticationError(message="Failed to authenticate")
|
|
31
|
-
|
|
32
|
-
return [results, message]
|
|
33
|
-
|
|
34
|
-
def connect_get_objects(self):
|
|
35
|
-
results = None
|
|
36
|
-
metadata = None
|
|
37
|
-
objects = None
|
|
38
|
-
message = None
|
|
39
|
-
|
|
40
|
-
if self.connector.authenticate():
|
|
41
|
-
objects = self.connector.get_objects()
|
|
42
|
-
try:
|
|
43
|
-
metadata = self.connector.get_metadata()
|
|
44
|
-
except errors.NotImplementedError:
|
|
45
|
-
pass
|
|
46
|
-
except Exception as e:
|
|
47
|
-
print("Error getting metadata")
|
|
48
|
-
print(str(e))
|
|
49
|
-
|
|
50
|
-
results = {
|
|
51
|
-
'metadata': metadata,
|
|
52
|
-
'objects': objects,
|
|
53
|
-
}
|
|
54
|
-
message = "Authenticated successfully"
|
|
55
|
-
else:
|
|
56
|
-
# If not authenticated, raise the authentication error.
|
|
57
|
-
raise errors.AuthenticationError(message="Failed to authenticate")
|
|
58
|
-
|
|
59
|
-
return [results, message]
|
|
60
|
-
|
|
61
|
-
def test_connection(self):
|
|
62
|
-
results = None
|
|
63
|
-
message = None
|
|
64
|
-
|
|
65
|
-
if self.connector.authenticate():
|
|
66
|
-
message = "Authenticated successfully"
|
|
67
|
-
else:
|
|
68
|
-
# If not authenticated, raise the authentication error.
|
|
69
|
-
raise errors.AuthenticationError(message="Failed to authenticate")
|
|
70
|
-
|
|
71
|
-
return [results, message]
|
|
72
|
-
|
|
73
|
-
def get_fields(self, object_id, options):
|
|
74
|
-
results = None
|
|
75
|
-
message = None
|
|
76
|
-
if self.connector.authenticate():
|
|
77
|
-
results = self.connector.get_fields(object_id, options=options)
|
|
78
|
-
message = "Retrieved fields"
|
|
79
|
-
else:
|
|
80
|
-
# If not authenticated, raise the authentication error.
|
|
81
|
-
raise errors.AuthenticationError("Failed to authenticate")
|
|
82
|
-
|
|
83
|
-
return [results, message]
|
|
84
|
-
|
|
85
|
-
def get_five_row_preview(self, object_id, field_ids, options, n_rows = 5, filters = None, next_page = None):
|
|
86
|
-
results = None
|
|
87
|
-
message = None
|
|
88
|
-
|
|
89
|
-
if self.connector.authenticate():
|
|
90
|
-
results = self.connector.get_data(object_id, field_ids=field_ids, n_rows=n_rows, filters=filters, next_page=next_page, options=options)
|
|
91
|
-
message = "Retrieved 5 row preview"
|
|
92
|
-
else:
|
|
93
|
-
# If not authenticated, raise the authentication error.
|
|
94
|
-
raise errors.AuthenticationError("Failed to authenticate")
|
|
95
|
-
|
|
96
|
-
return [results, message]
|
|
97
|
-
|
|
98
|
-
def get_data(self, object_id, field_ids, n_rows, next_page, options):
|
|
99
|
-
results = None
|
|
100
|
-
message = None
|
|
101
|
-
|
|
102
|
-
if self.connector.authenticate():
|
|
103
|
-
results = self.connector.get_data(object_id, field_ids=field_ids, n_rows=n_rows, next_page=next_page, options=options)
|
|
104
|
-
message = "Retrieved data"
|
|
105
|
-
else:
|
|
106
|
-
# If not authenticated, raise the authentication error.
|
|
107
|
-
raise errors.AuthenticationError("Failed to authenticate")
|
|
108
|
-
|
|
109
|
-
return [results, message]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dc_python_sdk-1.5.48 → dc_python_sdk-1.5.50}/src/dc_python_sdk.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dc_python_sdk-1.5.48/src/dc_sdk/src → dc_python_sdk-1.5.50/src/dc_sdk/src/models}/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|