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.
@@ -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.0"
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.0403.001"
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
- map_inferred_data_type(key, mapped_data, v)
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
- return safe_drop_underscores(source_table)
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
- return redshift_safe(source_column)
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.0
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=ubzp6kUWV4j8A3hgC-GRqtcX4WIQV5vMk5gdVkOZw4s,80578
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.0.dist-info/METADATA,sha256=ilFpUA6zFnHz4_tEpKpG7IZxORmHqdcMdW0wb0z2sME,2967
10
- fivetran_connector_sdk-1.3.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
11
- fivetran_connector_sdk-1.3.0.dist-info/entry_points.txt,sha256=uQn0KPnFlQmXJfxlk0tifdNsSXWfVlnAFzNqjXZM_xM,57
12
- fivetran_connector_sdk-1.3.0.dist-info/top_level.txt,sha256=-_xk2MFY4psIh7jw1lJePMzFb5-vask8_ZtX-UzYWUI,23
13
- fivetran_connector_sdk-1.3.0.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (79.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5