fivetran-connector-sdk 1.3.0__py3-none-any.whl → 1.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fivetran_connector_sdk/__init__.py +95 -39
- {fivetran_connector_sdk-1.3.0.dist-info → fivetran_connector_sdk-1.3.2.dist-info}/METADATA +1 -1
- {fivetran_connector_sdk-1.3.0.dist-info → fivetran_connector_sdk-1.3.2.dist-info}/RECORD +6 -6
- {fivetran_connector_sdk-1.3.0.dist-info → fivetran_connector_sdk-1.3.2.dist-info}/WHEEL +1 -1
- {fivetran_connector_sdk-1.3.0.dist-info → fivetran_connector_sdk-1.3.2.dist-info}/entry_points.txt +0 -0
- {fivetran_connector_sdk-1.3.0.dist-info → fivetran_connector_sdk-1.3.2.dist-info}/top_level.txt +0 -0
@@ -17,6 +17,7 @@ import time
|
|
17
17
|
import traceback
|
18
18
|
import re
|
19
19
|
import socket
|
20
|
+
import ast
|
20
21
|
|
21
22
|
from concurrent import futures
|
22
23
|
from datetime import datetime
|
@@ -31,13 +32,13 @@ from fivetran_connector_sdk.protos import connector_sdk_pb2_grpc
|
|
31
32
|
|
32
33
|
# Version format: <major_version>.<minor_version>.<patch_version>
|
33
34
|
# (where Major Version = 1 for GA, Minor Version is incremental MM from Jan 25 onwards, Patch Version is incremental within a month)
|
34
|
-
__version__ = "1.3.
|
35
|
+
__version__ = "1.3.2"
|
35
36
|
|
36
37
|
MAC_OS = "mac"
|
37
38
|
WIN_OS = "windows"
|
38
39
|
LINUX_OS = "linux"
|
39
40
|
|
40
|
-
TESTER_VERSION = "0.25.
|
41
|
+
TESTER_VERSION = "0.25.0415.001"
|
41
42
|
TESTER_FILENAME = "run_sdk_tester.jar"
|
42
43
|
VERSION_FILENAME = "version.txt"
|
43
44
|
UPLOAD_FILENAME = "code.zip"
|
@@ -52,6 +53,7 @@ MAX_RETRIES = 3
|
|
52
53
|
LOGGING_PREFIX = "Fivetran-Connector-SDK"
|
53
54
|
LOGGING_DELIMITER = ": "
|
54
55
|
VIRTUAL_ENV_CONFIG = "pyvenv.cfg"
|
56
|
+
ROOT_FILENAME = "connector.py"
|
55
57
|
|
56
58
|
# Compile patterns used in the implementation
|
57
59
|
WORD_DASH_DOT_PATTERN = re.compile(r'^[\w.-]*$')
|
@@ -75,6 +77,8 @@ DEBUGGING = False
|
|
75
77
|
EXECUTED_VIA_CLI = False
|
76
78
|
PRODUCTION_BASE_URL = "https://api.fivetran.com"
|
77
79
|
TABLES = {}
|
80
|
+
RENAMED_TABLE_NAMES = {}
|
81
|
+
RENAMED_COL_NAMES = {}
|
78
82
|
INSTALLATION_SCRIPT_MISSING_MESSAGE = "The 'installation.sh' file is missing in the 'drivers' directory. Please ensure that 'installation.sh' is present to properly configure drivers."
|
79
83
|
INSTALLATION_SCRIPT = "installation.sh"
|
80
84
|
DRIVERS = "drivers"
|
@@ -142,15 +146,23 @@ class Logging:
|
|
142
146
|
Logging.__log(Logging.Level.WARNING, message)
|
143
147
|
|
144
148
|
@staticmethod
|
145
|
-
def severe(message: str):
|
149
|
+
def severe(message: str, exception: Exception = None):
|
146
150
|
"""Logs a severe-level message.
|
147
151
|
|
148
152
|
Args:
|
149
153
|
message (str): The message to log.
|
154
|
+
exception (Exception, optional): Exception to be logged if provided.
|
150
155
|
"""
|
151
156
|
if Logging.LOG_LEVEL <= Logging.Level.SEVERE:
|
152
157
|
Logging.__log(Logging.Level.SEVERE, message)
|
153
158
|
|
159
|
+
if exception:
|
160
|
+
exc_type, exc_value, exc_traceback = type(exception), exception, exception.__traceback__
|
161
|
+
tb_str = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback, limit=1))
|
162
|
+
|
163
|
+
for error in tb_str.split("\n"):
|
164
|
+
Logging.__log(Logging.Level.SEVERE, error)
|
165
|
+
|
154
166
|
|
155
167
|
class Operations:
|
156
168
|
@staticmethod
|
@@ -177,6 +189,8 @@ class Operations:
|
|
177
189
|
field_name = get_renamed_column_name(field)
|
178
190
|
columns[field_name] = common_pb2.Column(
|
179
191
|
name=field_name, type=common_pb2.DataType.UNSPECIFIED, primary_key=False)
|
192
|
+
new_table = common_pb2.Table(name=table, columns=columns.values())
|
193
|
+
TABLES[table] = new_table
|
180
194
|
|
181
195
|
mapped_data = _map_data_to_columns(data, columns)
|
182
196
|
record = connector_sdk_pb2.Record(
|
@@ -326,7 +340,6 @@ def _get_columns(table: str) -> dict:
|
|
326
340
|
columns = {}
|
327
341
|
if table in TABLES:
|
328
342
|
for column in TABLES[table].columns:
|
329
|
-
column.name = get_renamed_column_name(column.name)
|
330
343
|
columns[column.name] = column
|
331
344
|
|
332
345
|
return columns
|
@@ -350,36 +363,10 @@ def _map_data_to_columns(data: dict, columns: dict) -> dict:
|
|
350
363
|
elif (key in columns) and columns[key].type != common_pb2.DataType.UNSPECIFIED:
|
351
364
|
map_defined_data_type(columns, key, mapped_data, v)
|
352
365
|
else:
|
353
|
-
|
354
|
-
|
366
|
+
mapped_data[key] = common_pb2.ValueType(string=str(v))
|
355
367
|
return mapped_data
|
356
368
|
|
357
369
|
|
358
|
-
def map_inferred_data_type(k, mapped_data, v):
|
359
|
-
# We can infer type from the value
|
360
|
-
if isinstance(v, int):
|
361
|
-
if abs(v) > JAVA_LONG_MAX_VALUE:
|
362
|
-
mapped_data[k] = common_pb2.ValueType(float=v)
|
363
|
-
else:
|
364
|
-
mapped_data[k] = common_pb2.ValueType(long=v)
|
365
|
-
elif isinstance(v, float):
|
366
|
-
mapped_data[k] = common_pb2.ValueType(float=v)
|
367
|
-
elif isinstance(v, bool):
|
368
|
-
mapped_data[k] = common_pb2.ValueType(bool=v)
|
369
|
-
elif isinstance(v, bytes):
|
370
|
-
mapped_data[k] = common_pb2.ValueType(binary=v)
|
371
|
-
elif isinstance(v, list):
|
372
|
-
raise ValueError(
|
373
|
-
"Values for the columns cannot be of type 'list'. Please ensure that all values are of a supported type. Reference: https://fivetran.com/docs/connectors/connector-sdk/technical-reference#supporteddatatypes")
|
374
|
-
elif isinstance(v, dict):
|
375
|
-
mapped_data[k] = common_pb2.ValueType(json=json.dumps(v))
|
376
|
-
elif isinstance(v, str):
|
377
|
-
mapped_data[k] = common_pb2.ValueType(string=v)
|
378
|
-
else:
|
379
|
-
# Convert arbitrary objects to string
|
380
|
-
mapped_data[k] = common_pb2.ValueType(string=str(v))
|
381
|
-
|
382
|
-
|
383
370
|
def map_defined_data_type(columns, k, mapped_data, v):
|
384
371
|
if columns[k].type == common_pb2.DataType.BOOLEAN:
|
385
372
|
mapped_data[k] = common_pb2.ValueType(bool=v)
|
@@ -423,6 +410,36 @@ def map_defined_data_type(columns, k, mapped_data, v):
|
|
423
410
|
else:
|
424
411
|
raise ValueError(f"Unsupported data type encountered: {columns[k].type}. Please use valid data types.")
|
425
412
|
|
413
|
+
def _warn_exit_usage(filename, line_no, func):
|
414
|
+
print_library_log(f"Avoid using {func} to exit from the Python code as this can cause the connector to become stuck. Throw a error if required " +
|
415
|
+
f"at: {filename}:{line_no}. See the Technical Reference for details: https://fivetran.com/docs/connector-sdk/technical-reference#handlingexceptions",
|
416
|
+
Logging.Level.WARNING)
|
417
|
+
|
418
|
+
def _exit_check(project_path):
|
419
|
+
"""Checks for the presence of 'exit()' in the calling code.
|
420
|
+
Args:
|
421
|
+
project_path: The absolute project_path to check exit in the connector.py file in the project.
|
422
|
+
"""
|
423
|
+
# We expect the connector.py to catch errors or throw exceptions
|
424
|
+
# This is a warning shown to let the customer know that we expect either the yield call or error thrown
|
425
|
+
# exit() or sys.exit() in between some yields can cause the connector to be stuck without processing further upsert calls
|
426
|
+
|
427
|
+
filepath = os.path.join(project_path, ROOT_FILENAME)
|
428
|
+
with open(filepath, "r") as f:
|
429
|
+
try:
|
430
|
+
tree = ast.parse(f.read())
|
431
|
+
for node in ast.walk(tree):
|
432
|
+
if isinstance(node, ast.Call):
|
433
|
+
if isinstance(node.func, ast.Name) and node.func.id == "exit":
|
434
|
+
_warn_exit_usage(ROOT_FILENAME, node.lineno, "exit()")
|
435
|
+
elif isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name):
|
436
|
+
if node.func.attr == "_exit" and node.func.value.id == "os":
|
437
|
+
_warn_exit_usage(ROOT_FILENAME, node.lineno, "os._exit()")
|
438
|
+
if node.func.attr == "exit" and node.func.value.id == "sys":
|
439
|
+
_warn_exit_usage(ROOT_FILENAME, node.lineno, "sys.exit()")
|
440
|
+
except SyntaxError as e:
|
441
|
+
print_library_log(f"SyntaxError in {ROOT_FILENAME}: {e}", Logging.Level.SEVERE)
|
442
|
+
|
426
443
|
|
427
444
|
def _parse_datetime_str(dt):
|
428
445
|
return datetime.strptime(dt, "%Y-%m-%dT%H:%M:%S.%f%z" if '.' in dt else "%Y-%m-%dT%H:%M:%S%z")
|
@@ -459,7 +476,7 @@ def _check_dict(incoming: dict, string_only: bool = False) -> dict:
|
|
459
476
|
Args:
|
460
477
|
incoming (dict): The dictionary to validate.
|
461
478
|
string_only (bool): Whether to allow only string values.
|
462
|
-
|
479
|
+
|
463
480
|
Returns:
|
464
481
|
dict: The validated dictionary.
|
465
482
|
"""
|
@@ -661,14 +678,20 @@ def get_renamed_table_name(source_table):
|
|
661
678
|
"""
|
662
679
|
Process a source table name to ensure it conforms to naming rules.
|
663
680
|
"""
|
664
|
-
|
681
|
+
if source_table not in RENAMED_TABLE_NAMES:
|
682
|
+
RENAMED_TABLE_NAMES[source_table] = safe_drop_underscores(source_table)
|
683
|
+
|
684
|
+
return RENAMED_TABLE_NAMES[source_table]
|
665
685
|
|
666
686
|
|
667
687
|
def get_renamed_column_name(source_column):
|
668
688
|
"""
|
669
689
|
Process a source column name to ensure it conforms to naming rules.
|
670
690
|
"""
|
671
|
-
|
691
|
+
if source_column not in RENAMED_COL_NAMES:
|
692
|
+
RENAMED_COL_NAMES[source_column] = redshift_safe(source_column)
|
693
|
+
|
694
|
+
return RENAMED_COL_NAMES[source_column]
|
672
695
|
|
673
696
|
class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
|
674
697
|
def __init__(self, update, schema=None):
|
@@ -895,8 +918,7 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
|
|
895
918
|
connection_config = {
|
896
919
|
"schema": connection,
|
897
920
|
"secrets_list": secrets_list,
|
898
|
-
"sync_method": "DIRECT"
|
899
|
-
"custom_payloads": []
|
921
|
+
"sync_method": "DIRECT"
|
900
922
|
}
|
901
923
|
|
902
924
|
# args.python_version is already validated in validate_deploy_parameters - so its safe to add in connection_config
|
@@ -907,6 +929,8 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
|
|
907
929
|
self.validate_requirements_file(args.project_path, True, args.force)
|
908
930
|
|
909
931
|
group_id, group_name = self.__get_group_info(group, deploy_key)
|
932
|
+
if hd_agent_id is None:
|
933
|
+
hd_agent_id = self.__get_hd_agent_id(group_id, deploy_key)
|
910
934
|
connection_id, service = self.__get_connection_id(connection, group, group_id, deploy_key) or (None, None)
|
911
935
|
|
912
936
|
if connection_id:
|
@@ -1248,6 +1272,31 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
|
|
1248
1272
|
return LINUX_OS
|
1249
1273
|
raise ValueError(f"Unrecognized OS: {os_sysname}")
|
1250
1274
|
|
1275
|
+
|
1276
|
+
@staticmethod
|
1277
|
+
def __get_hd_agent_id(destination_id: str, deploy_key: str) -> str:
|
1278
|
+
"""Retrieves the destination information for the specified destination ID and deployment key.
|
1279
|
+
|
1280
|
+
Args:
|
1281
|
+
destination_id (str): The destination ID.
|
1282
|
+
deploy_key (str): The deployment key.
|
1283
|
+
|
1284
|
+
Returns:
|
1285
|
+
str: The hybrid_deployment_agent_id if HD enabled destination else returns None
|
1286
|
+
"""
|
1287
|
+
destinations_url = f"{PRODUCTION_BASE_URL}/v1/destinations/{destination_id}"
|
1288
|
+
|
1289
|
+
headers = {"Authorization": f"Basic {deploy_key}"}
|
1290
|
+
resp = rq.get(destinations_url, headers=headers)
|
1291
|
+
|
1292
|
+
if not resp.ok:
|
1293
|
+
print_library_log(
|
1294
|
+
f"The request failed with status code: {resp.status_code}. Please ensure you're using a valid base64-encoded API key and try again.", Logging.Level.SEVERE)
|
1295
|
+
os._exit(1)
|
1296
|
+
|
1297
|
+
data = resp.json().get("data", {})
|
1298
|
+
return data.get("hybrid_deployment_agent_id")
|
1299
|
+
|
1251
1300
|
@staticmethod
|
1252
1301
|
def __get_group_info(group: str, deploy_key: str) -> tuple[str, str]:
|
1253
1302
|
"""Retrieves the group information for the specified group and deployment key.
|
@@ -1411,6 +1460,7 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
|
|
1411
1460
|
self.validate_requirements_file(project_path, False)
|
1412
1461
|
print_library_log(f"Debugging connector at: {project_path}")
|
1413
1462
|
available_port = get_available_port()
|
1463
|
+
_exit_check(project_path)
|
1414
1464
|
|
1415
1465
|
if available_port is None:
|
1416
1466
|
raise RuntimeError("SEVERE: Unable to allocate a port in the range 50051-50061. "
|
@@ -1423,7 +1473,7 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
|
|
1423
1473
|
|
1424
1474
|
try:
|
1425
1475
|
print_library_log("Running connector tester...")
|
1426
|
-
for log_msg in self.__run_tester(java_exe, tester_root_dir, project_path, available_port):
|
1476
|
+
for log_msg in self.__run_tester(java_exe, tester_root_dir, project_path, available_port, json.dumps(self.state), json.dumps(self.configuration)):
|
1427
1477
|
print(log_msg, end="")
|
1428
1478
|
except:
|
1429
1479
|
print(traceback.format_exc())
|
@@ -1464,7 +1514,7 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
|
|
1464
1514
|
yield line
|
1465
1515
|
|
1466
1516
|
@staticmethod
|
1467
|
-
def __run_tester(java_exe: str, root_dir: str, project_path: str, port: int):
|
1517
|
+
def __run_tester(java_exe: str, root_dir: str, project_path: str, port: int, state_json: str, configuration_json: str):
|
1468
1518
|
"""Runs the connector tester.
|
1469
1519
|
|
1470
1520
|
Args:
|
@@ -1487,7 +1537,9 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
|
|
1487
1537
|
"--connector-sdk=true",
|
1488
1538
|
f"--port={port}",
|
1489
1539
|
f"--working-dir={working_dir}",
|
1490
|
-
"--tester-type=source"
|
1540
|
+
"--tester-type=source",
|
1541
|
+
f"--state={state_json}",
|
1542
|
+
f"--configuration={configuration_json}"]
|
1491
1543
|
|
1492
1544
|
popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
|
1493
1545
|
for line in Connector.process_stream(popen.stderr):
|
@@ -1879,6 +1931,10 @@ def validate_and_load_configuration(args, configuration):
|
|
1879
1931
|
def validate_and_load_state(args, state):
|
1880
1932
|
if state:
|
1881
1933
|
json_filepath = os.path.join(args.project_path, args.state)
|
1934
|
+
else:
|
1935
|
+
json_filepath = os.path.join(args.project_path, "files", "state.json")
|
1936
|
+
|
1937
|
+
if os.path.exists(json_filepath):
|
1882
1938
|
if os.path.isfile(json_filepath):
|
1883
1939
|
with open(json_filepath, 'r') as fi:
|
1884
1940
|
state = json.load(fi)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: fivetran_connector_sdk
|
3
|
-
Version: 1.3.
|
3
|
+
Version: 1.3.2
|
4
4
|
Summary: Build custom connectors on Fivetran platform
|
5
5
|
Author-email: Fivetran <developers@fivetran.com>
|
6
6
|
Project-URL: Homepage, https://fivetran.com/docs/connectors/connector-sdk
|
@@ -1,4 +1,4 @@
|
|
1
|
-
fivetran_connector_sdk/__init__.py,sha256=
|
1
|
+
fivetran_connector_sdk/__init__.py,sha256=NU9PygEtaibP-4ugblU8kieDGEJRBdLQcfSvlo9YYkY,83637
|
2
2
|
fivetran_connector_sdk/protos/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
3
|
fivetran_connector_sdk/protos/common_pb2.py,sha256=kUwVcyZHgLigNR-KnHZn7dHrlxaMnUXqzprsRx6T72M,6831
|
4
4
|
fivetran_connector_sdk/protos/common_pb2.pyi,sha256=S0hdIzoXyyOKD5cjiGeDDLYpQ9J3LjAvu4rCj1JvJWE,9038
|
@@ -6,8 +6,8 @@ fivetran_connector_sdk/protos/common_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXH
|
|
6
6
|
fivetran_connector_sdk/protos/connector_sdk_pb2.py,sha256=9Ke_Ti1s0vAeXapfXT-EryrT2-TSGQb8mhs4gxTpUMk,7732
|
7
7
|
fivetran_connector_sdk/protos/connector_sdk_pb2.pyi,sha256=FWYxRgshEF3QDYAE0TM_mv4N2gGvkxCH_uPpxnMc4oA,8406
|
8
8
|
fivetran_connector_sdk/protos/connector_sdk_pb2_grpc.py,sha256=ZfJLp4DW7uP4pFOZ74s_wQ6tD3eIPi-08UfnLwe4tzo,7163
|
9
|
-
fivetran_connector_sdk-1.3.
|
10
|
-
fivetran_connector_sdk-1.3.
|
11
|
-
fivetran_connector_sdk-1.3.
|
12
|
-
fivetran_connector_sdk-1.3.
|
13
|
-
fivetran_connector_sdk-1.3.
|
9
|
+
fivetran_connector_sdk-1.3.2.dist-info/METADATA,sha256=Z0t971CmSiseBUZcJ8RwFGdizkgGTO0CPKhNjqZGcvY,2967
|
10
|
+
fivetran_connector_sdk-1.3.2.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
11
|
+
fivetran_connector_sdk-1.3.2.dist-info/entry_points.txt,sha256=uQn0KPnFlQmXJfxlk0tifdNsSXWfVlnAFzNqjXZM_xM,57
|
12
|
+
fivetran_connector_sdk-1.3.2.dist-info/top_level.txt,sha256=-_xk2MFY4psIh7jw1lJePMzFb5-vask8_ZtX-UzYWUI,23
|
13
|
+
fivetran_connector_sdk-1.3.2.dist-info/RECORD,,
|
{fivetran_connector_sdk-1.3.0.dist-info → fivetran_connector_sdk-1.3.2.dist-info}/entry_points.txt
RENAMED
File without changes
|
{fivetran_connector_sdk-1.3.0.dist-info → fivetran_connector_sdk-1.3.2.dist-info}/top_level.txt
RENAMED
File without changes
|