fivetran-connector-sdk 0.8.21.1__tar.gz → 0.8.26.1__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 (18) hide show
  1. {fivetran_connector_sdk-0.8.21.1 → fivetran_connector_sdk-0.8.26.1}/PKG-INFO +2 -2
  2. {fivetran_connector_sdk-0.8.21.1 → fivetran_connector_sdk-0.8.26.1}/README.md +1 -1
  3. {fivetran_connector_sdk-0.8.21.1 → fivetran_connector_sdk-0.8.26.1}/src/fivetran_connector_sdk/__init__.py +172 -35
  4. {fivetran_connector_sdk-0.8.21.1 → fivetran_connector_sdk-0.8.26.1}/src/fivetran_connector_sdk.egg-info/PKG-INFO +2 -2
  5. {fivetran_connector_sdk-0.8.21.1 → fivetran_connector_sdk-0.8.26.1}/pyproject.toml +0 -0
  6. {fivetran_connector_sdk-0.8.21.1 → fivetran_connector_sdk-0.8.26.1}/setup.cfg +0 -0
  7. {fivetran_connector_sdk-0.8.21.1 → fivetran_connector_sdk-0.8.26.1}/src/fivetran_connector_sdk/protos/__init__.py +0 -0
  8. {fivetran_connector_sdk-0.8.21.1 → fivetran_connector_sdk-0.8.26.1}/src/fivetran_connector_sdk/protos/common_pb2.py +0 -0
  9. {fivetran_connector_sdk-0.8.21.1 → fivetran_connector_sdk-0.8.26.1}/src/fivetran_connector_sdk/protos/common_pb2.pyi +0 -0
  10. {fivetran_connector_sdk-0.8.21.1 → fivetran_connector_sdk-0.8.26.1}/src/fivetran_connector_sdk/protos/common_pb2_grpc.py +0 -0
  11. {fivetran_connector_sdk-0.8.21.1 → fivetran_connector_sdk-0.8.26.1}/src/fivetran_connector_sdk/protos/connector_sdk_pb2.py +0 -0
  12. {fivetran_connector_sdk-0.8.21.1 → fivetran_connector_sdk-0.8.26.1}/src/fivetran_connector_sdk/protos/connector_sdk_pb2.pyi +0 -0
  13. {fivetran_connector_sdk-0.8.21.1 → fivetran_connector_sdk-0.8.26.1}/src/fivetran_connector_sdk/protos/connector_sdk_pb2_grpc.py +0 -0
  14. {fivetran_connector_sdk-0.8.21.1 → fivetran_connector_sdk-0.8.26.1}/src/fivetran_connector_sdk.egg-info/SOURCES.txt +0 -0
  15. {fivetran_connector_sdk-0.8.21.1 → fivetran_connector_sdk-0.8.26.1}/src/fivetran_connector_sdk.egg-info/dependency_links.txt +0 -0
  16. {fivetran_connector_sdk-0.8.21.1 → fivetran_connector_sdk-0.8.26.1}/src/fivetran_connector_sdk.egg-info/entry_points.txt +0 -0
  17. {fivetran_connector_sdk-0.8.21.1 → fivetran_connector_sdk-0.8.26.1}/src/fivetran_connector_sdk.egg-info/requires.txt +0 -0
  18. {fivetran_connector_sdk-0.8.21.1 → fivetran_connector_sdk-0.8.26.1}/src/fivetran_connector_sdk.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fivetran_connector_sdk
3
- Version: 0.8.21.1
3
+ Version: 0.8.26.1
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
@@ -17,7 +17,7 @@ Requires-Dist: get_pypi_latest_version==0.0.12
17
17
  Requires-Dist: pipreqs==0.5.0
18
18
 
19
19
  # **fivetran-connector-sdk**
20
- The *fivetran-connector-sdk* is a Python module that enables you to write custom data connectors and deploy them as an extension of [Fivetran](https://www.fivetran.com/). Fivetran automatically manages running the connectors on your scheduled frequency and manages the required compute resources.
20
+ The *fivetran-connector-sdk* SDK allows users to execute custom, self-written Python code within [Fivetran's](https://www.fivetran.com/) secure cloud environment. Fivetran automatically manages running the connectors on your scheduled frequency and manages the required compute resources.
21
21
 
22
22
  The Connector SDK service is the best fit for the following use cases:
23
23
  - Fivetran doesn't have a connector for your source and is unlikely to support it soon.
@@ -1,5 +1,5 @@
1
1
  # **fivetran-connector-sdk**
2
- The *fivetran-connector-sdk* is a Python module that enables you to write custom data connectors and deploy them as an extension of [Fivetran](https://www.fivetran.com/). Fivetran automatically manages running the connectors on your scheduled frequency and manages the required compute resources.
2
+ The *fivetran-connector-sdk* SDK allows users to execute custom, self-written Python code within [Fivetran's](https://www.fivetran.com/) secure cloud environment. Fivetran automatically manages running the connectors on your scheduled frequency and manages the required compute resources.
3
3
 
4
4
  The Connector SDK service is the best fit for the following use cases:
5
5
  - Fivetran doesn't have a connector for your source and is unlikely to support it soon.
@@ -23,13 +23,13 @@ from fivetran_connector_sdk.protos import common_pb2
23
23
  from fivetran_connector_sdk.protos import connector_sdk_pb2
24
24
  from fivetran_connector_sdk.protos import connector_sdk_pb2_grpc
25
25
 
26
- __version__ = "0.8.21.1"
26
+ __version__ = "0.8.26.1"
27
27
 
28
28
  MAC_OS = "mac"
29
29
  WIN_OS = "windows"
30
30
  LINUX_OS = "linux"
31
31
 
32
- TESTER_VERSION = "0.24.0807.001"
32
+ TESTER_VERSION = "0.24.0826.001"
33
33
  TESTER_FILENAME = "run_sdk_tester.jar"
34
34
  VERSION_FILENAME = "version.txt"
35
35
  UPLOAD_FILENAME = "code.zip"
@@ -39,8 +39,16 @@ OUTPUT_FILES_DIR = "files"
39
39
  ONE_DAY_IN_SEC = 24 * 60 * 60
40
40
 
41
41
  EXCLUDED_DIRS = ["__pycache__", "lib", "include", OUTPUT_FILES_DIR]
42
- EXCLUDED_PIPREQS_DIRS = ["bin,etc,include,lib,Lib,lib64,Scripts"]
43
-
42
+ EXCLUDED_PIPREQS_DIRS = ["bin,etc,include,lib,Lib,lib64,Scripts,share"]
43
+ VALID_COMMANDS = ["debug", "deploy", "reset", "version"]
44
+ MAX_ALLOWED_EDIT_DISTANCE_FROM_VALID_COMMAND = 3
45
+ COMMANDS_AND_SYNONYMS = {
46
+ "debug": {"test", "verify", "diagnose", "check"},
47
+ "deploy": {"upload", "ship", "launch", "release"},
48
+ "reset": {"reinitialize", "reinitialise", "re-initialize", "re-initialise", "restart", "restore"},
49
+ }
50
+
51
+ CONNECTION_SCHEMA_NAME_PATTERN = r'^[_a-z][_a-z0-9]*$'
44
52
  DEBUGGING = False
45
53
  TABLES = {}
46
54
 
@@ -130,12 +138,6 @@ class Operations:
130
138
  for field in data.keys():
131
139
  columns[field] = common_pb2.Column(
132
140
  name=field, type=common_pb2.DataType.UNSPECIFIED, primary_key=False)
133
- new_table = common_pb2.Table(name=table, columns=columns.values())
134
-
135
- responses.append(connector_sdk_pb2.UpdateResponse(
136
- operation=connector_sdk_pb2.Operation(
137
- schema_change=connector_sdk_pb2.SchemaChange(
138
- without_schema=common_pb2.TableList(tables=[new_table])))))
139
141
 
140
142
  mapped_data = _map_data_to_columns(data, columns)
141
143
  record = connector_sdk_pb2.Record(
@@ -401,6 +403,19 @@ def _check_dict(incoming: dict, string_only: bool = False) -> dict:
401
403
  return incoming
402
404
 
403
405
 
406
+ def is_connection_name_valid(connection: str):
407
+ """Validates if the incoming connection schema name is valid or not.
408
+ Args:
409
+ connection (str): The connection schema name being validated.
410
+
411
+ Returns:
412
+ bool: True if connection name is valid.
413
+ """
414
+
415
+ pattern = re.compile(CONNECTION_SCHEMA_NAME_PATTERN)
416
+ return pattern.match(connection)
417
+
418
+
404
419
  class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
405
420
  def __init__(self, update, schema=None):
406
421
  """Initializes the Connector instance.
@@ -455,8 +470,17 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
455
470
  dict: A dictionary where keys are package names (lowercased) and
456
471
  values are the full dependency strings.
457
472
  """
458
- return {item.split("==")[0].lower(): item.lower() for item in
459
- self.fetch_requirements_from_file(file_path)}
473
+ requirements_dict = {}
474
+ for requirement in self.fetch_requirements_from_file(file_path):
475
+ requirement = requirement.strip()
476
+ if not requirement or requirement.startswith("#"): # Skip empty lines and comments
477
+ continue
478
+ try:
479
+ key, _ = re.split(r"==|>=|<=|>|<", requirement)
480
+ requirements_dict[key.lower()] = requirement.lower()
481
+ except ValueError:
482
+ print(f"Error: Invalid requirement format: '{requirement}'")
483
+ return requirements_dict
460
484
 
461
485
  def validate_requirements_file(self, project_path: str, is_deploy: bool):
462
486
  """Validates the `requirements.txt` file against the project's actual dependencies.
@@ -471,7 +495,8 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
471
495
  is_deploy (bool): If `True`, the method will exit the process on critical errors.
472
496
 
473
497
  """
474
- subprocess.run(["pipreqs", "--savepath", "tmp_requirements.txt", "--ignore"] + EXCLUDED_PIPREQS_DIRS, text=True, check=True)
498
+ subprocess.check_call(["pipreqs", "--savepath", "tmp_requirements.txt", "--ignore"] + EXCLUDED_PIPREQS_DIRS,
499
+ stderr=subprocess.PIPE)
475
500
  tmp_requirements_file_path = os.path.join(project_path, 'tmp_requirements.txt')
476
501
 
477
502
  tmp_requirements = self.fetch_requirements_as_dict(self, tmp_requirements_file_path)
@@ -509,6 +534,10 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
509
534
 
510
535
  unused_deps = list(requirements.keys() - tmp_requirements.keys())
511
536
  if unused_deps:
537
+ if 'fivetran_connector_sdk' in unused_deps:
538
+ print("ERROR: Please remove fivetran_connector_sdk from requirements.txt. "
539
+ "We always use the latest version of fivetran_connector_sdk when executing your code.")
540
+ os._exit(1)
512
541
  print("INFO: The following dependencies are not needed, "
513
542
  "they are not used or already installed. Please remove them from requirements.txt:")
514
543
  print(*unused_deps)
@@ -530,8 +559,22 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
530
559
  connection (str): The connection name.
531
560
  configuration (dict): The configuration dictionary.
532
561
  """
533
- if not deploy_key: print("SEVERE: The Fivetran API key is missing. Please provide a valid Fivetran API key to create the connector."); os._exit(1)
534
- if not connection: print("SEVERE: The connection name is missing. Please provide a valid connection name to create the connector."); os._exit(1)
562
+ if not deploy_key or not connection:
563
+ print("SEVERE: The deploy command needs the following parameters:"
564
+ "\n\tRequired:\n"
565
+ "\t\t--api-key <BASE64-ENCODED-FIVETRAN-API-KEY-FOR-DEPLOYMENT>\n"
566
+ "\t\t--connection <VALID-CONNECTOR-SCHEMA_NAME>\n"
567
+ "\t(Optional):\n"
568
+ "\t\t--destination <DESTINATION_NAME> (Becomes required if there are multiple destinations)\n"
569
+ "\t\t--configuration <CONFIGURATION_FILE> (Completely replaces the existing configuration)")
570
+ os._exit(1)
571
+
572
+ if not is_connection_name_valid(connection):
573
+ print(f"SEVERE: Connection name: {connection} is invalid!\n The connection name should start with an "
574
+ f"underscore or a lowercase letter (a-z), followed by any combination of underscores, lowercase "
575
+ f"letters, or digits (0-9). Uppercase characters are not allowed.")
576
+ os._exit(1)
577
+
535
578
  _check_dict(configuration, True)
536
579
 
537
580
  secrets_list = []
@@ -549,25 +592,54 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
549
592
  self.validate_requirements_file(project_path, True)
550
593
 
551
594
  group_id, group_name = self.__get_group_info(group, deploy_key)
552
- print(f"INFO: Deploying '{project_path}' to connector '{connection}' in destination '{group_name}'.\n")
553
- upload_file_path = self.__create_upload_file(project_path)
554
- upload_result = self.__upload(upload_file_path, deploy_key, group_id, connection)
555
- os.remove(upload_file_path)
556
- if not upload_result:
557
- os._exit(1)
558
- connection_id = self.__get_connection_id(connection, group, group_id, deploy_key)
595
+ connection_id, service = self.__get_connection_id(
596
+ connection, group, group_id, deploy_key)
597
+
559
598
  if connection_id:
560
- print(f"INFO: The connection '{connection}' already exists in destination '{group}', updating the existing connector... ", end="", flush=True)
561
- self.__update_connection(connection_id, connection, group_name, connection_config, deploy_key)
562
- print("")
599
+ if service != 'connector_sdk':
600
+ print(
601
+ f"SEVERE: The connection '{connection}' already exists and does not use the 'Connector SDK' service. You cannot update this connection.")
602
+ os._exit(1)
603
+ confirm = input(
604
+ f"The connection '{connection}' already exists in the destination '{group}'. Updating it will overwrite the existing code and configuration. Do you want to proceed with the update? (Y/N): ")
605
+ if confirm.lower() == "y":
606
+ print("INFO: Updating the connection...\n")
607
+ self.__upload_project(
608
+ project_path, deploy_key, group_id, group_name, connection)
609
+ self.__update_connection(
610
+ connection_id, connection, group_name, connection_config, deploy_key)
611
+ print("✓")
612
+ print(
613
+ f"INFO: Visit the Fivetran dashboard to manage the connection: https://fivetran.com/dashboard/connectors/{connection_id}/status")
614
+ else:
615
+ print("INFO: Update canceled. The process is now terminating.")
616
+ os._exit(1)
563
617
  else:
564
- response = self.__create_connection(deploy_key, group_id, connection_config)
618
+ self.__upload_project(project_path, deploy_key,
619
+ group_id, group_name, connection)
620
+ response = self.__create_connection(
621
+ deploy_key, group_id, connection_config)
565
622
  if response.ok:
566
- print(f"INFO: Connection named '{connection}' has been created successfully.\n")
623
+ print(
624
+ f"INFO: The connection '{connection}' has been created successfully.\n")
625
+ connection_id = response.json()['data']['id']
626
+ print(
627
+ f"INFO: Visit the Fivetran dashboard to start the initial sync: https://fivetran.com/dashboard/connectors/{connection_id}/status")
567
628
  else:
568
- print(f"SEVERE: Unable to create a new Connection, failed with error: {response.json()['message']}")
629
+ print(
630
+ f"SEVERE: Unable to create a new connection, failed with error: {response.json()['message']}")
569
631
  os._exit(1)
570
632
 
633
+ def __upload_project(self, project_path: str, deploy_key: str, group_id: str, group_name: str, connection: str):
634
+ print(
635
+ f"INFO: Deploying '{project_path}' to connection '{connection}' in destination '{group_name}'.\n")
636
+ upload_file_path = self.__create_upload_file(project_path)
637
+ upload_result = self.__upload(
638
+ upload_file_path, deploy_key, group_id, connection)
639
+ os.remove(upload_file_path)
640
+ if not upload_result:
641
+ os._exit(1)
642
+
571
643
  @staticmethod
572
644
  def __force_sync(id: str, deploy_key: str) -> bool:
573
645
  """Forces a sync operation on the connection with the given ID and deployment key.
@@ -606,7 +678,7 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
606
678
  })
607
679
 
608
680
  if not resp.ok:
609
- print(f"SEVERE: Unable to update Connection '{name}' in destination '{group}', failed with error: '{response.json()['message']}'.")
681
+ print(f"SEVERE: Unable to update Connection '{name}' in destination '{group}', failed with error: '{resp.json()['message']}'.")
610
682
  os._exit(1)
611
683
 
612
684
  @staticmethod
@@ -626,13 +698,14 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
626
698
  headers={"Authorization": f"Basic {deploy_key}"},
627
699
  params={"schema": name})
628
700
  if not resp.ok:
629
- print(f"SEVERE: Unable to fetch connection list in destination '{group}'")
701
+ print(
702
+ f"SEVERE: Unable to fetch connection list in destination '{group}'")
630
703
  os._exit(1)
631
704
 
632
705
  if resp.json()['data']['items']:
633
- return resp.json()['data']['items'][0]['id']
706
+ return resp.json()['data']['items'][0]['id'], resp.json()['data']['items'][0]['service']
634
707
 
635
- return None
708
+ return None, None
636
709
 
637
710
  @staticmethod
638
711
  def __create_connection(deploy_key: str, group_id: str, config: dict) -> rq.Response:
@@ -782,7 +855,7 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
782
855
 
783
856
  if not resp.ok:
784
857
  print(
785
- f"SEVERE: Unable to fetch list of destination names, status code = {resp.status_code}")
858
+ f"SEVERE: Unable to retrieve destination details. The request failed with status code: {resp.status_code}. Please ensure you're using a valid base64-encoded API key and try again.")
786
859
  os._exit(1)
787
860
 
788
861
  data = resp.json().get("data", {})
@@ -839,6 +912,9 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
839
912
  self.state = _check_dict(state)
840
913
  Logging.LOG_LEVEL = log_level
841
914
 
915
+ if not DEBUGGING:
916
+ print(f"Running on fivetran_connector_sdk: {__version__}")
917
+
842
918
  server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
843
919
  connector_sdk_pb2_grpc.add_ConnectorServicer_to_server(self, server)
844
920
  server.add_insecure_port("[::]:" + str(port))
@@ -1186,6 +1262,63 @@ def find_connector_object(project_path) -> Connector:
1186
1262
  sys.exit(1)
1187
1263
 
1188
1264
 
1265
+ def suggest_correct_command(input_command: str) -> bool:
1266
+ # for typos
1267
+ # calculate the edit distance of the input command (lowercased) with each of the valid commands
1268
+ edit_distances_of_commands = sorted([(command, edit_distance(command, input_command.lower())) for command in VALID_COMMANDS], key=lambda x: x[1])
1269
+
1270
+ if edit_distances_of_commands[0][1] <= MAX_ALLOWED_EDIT_DISTANCE_FROM_VALID_COMMAND:
1271
+ # if the closest command is within the max allowed edit distance, we suggest that command
1272
+ # threshold is kept to prevent suggesting a valid command for an obvious wrong command like `fivetran iknowthisisntacommandbuttryanyway`
1273
+ print_suggested_command_message(edit_distances_of_commands[0][0], input_command)
1274
+ return True
1275
+
1276
+ # for synonyms
1277
+ for (command, synonyms) in COMMANDS_AND_SYNONYMS.items():
1278
+ # check if the input command (lowercased) is a recognised synonym of any of the valid commands, if yes, suggest that command
1279
+ if input_command.lower() in synonyms:
1280
+ print_suggested_command_message(command, input_command)
1281
+ return True
1282
+
1283
+ return False
1284
+
1285
+
1286
+ def print_suggested_command_message(valid_command: str, input_command: str) -> None:
1287
+ print(f"`fivetran {input_command}` is not a valid command.")
1288
+ print(f"Did you mean `fivetran {valid_command}`?")
1289
+ print("Use `fivetran --help` for more details.")
1290
+
1291
+
1292
+ def edit_distance(first_string: str, second_string: str) -> int:
1293
+ first_string_length: int = len(first_string)
1294
+ second_string_length: int = len(second_string)
1295
+
1296
+ # Initialize the previous row of distances (for the base case of an empty first string)
1297
+ # 'previous_row[j]' holds the edit distance between an empty prefix of 'first_string' and the first 'j' characters of 'second_string'.
1298
+ # The first row is filled with values [0, 1, 2, ..., second_string_length]
1299
+ previous_row: list[int] = list(range(second_string_length + 1))
1300
+
1301
+ # Rest of the rows
1302
+ for first_string_index in range(1, first_string_length + 1):
1303
+ # Start the current row with the distance for an empty second string
1304
+ current_row: list[int] = [first_string_index] # j = 0
1305
+
1306
+ # Iterate over each character in the second string
1307
+ for second_string_index in range(1, second_string_length + 1):
1308
+ if first_string[first_string_index - 1] == second_string[second_string_index - 1]:
1309
+ # If characters match, no additional cost
1310
+ current_row.append(previous_row[second_string_index - 1])
1311
+ else:
1312
+ # Minimum cost of insertion, deletion, or substitution
1313
+ current_row.append(1 + min(current_row[-1], previous_row[second_string_index], previous_row[second_string_index - 1]))
1314
+
1315
+ # Move to the next row
1316
+ previous_row = current_row
1317
+
1318
+ # The last value in the last row is the edit distance
1319
+ return previous_row[second_string_length]
1320
+
1321
+
1189
1322
  def main():
1190
1323
  """The main entry point for the script.
1191
1324
  Parses command line arguments and passes them to connector object methods
@@ -1194,7 +1327,7 @@ def main():
1194
1327
  parser = argparse.ArgumentParser(allow_abbrev=False)
1195
1328
 
1196
1329
  # Positional
1197
- parser.add_argument("command", help="debug|deploy|reset")
1330
+ parser.add_argument("command", help="|".join(VALID_COMMANDS))
1198
1331
  parser.add_argument("project_path", nargs='?', default=os.getcwd(), help="Path to connector project directory")
1199
1332
 
1200
1333
  # Optional (Not all of these are valid with every mutually exclusive option below)
@@ -1261,8 +1394,12 @@ def main():
1261
1394
  print("ERROR: Reset Failed")
1262
1395
  raise e
1263
1396
 
1397
+ elif args.command.lower() == "version":
1398
+ print("fivetran_connector_sdk " + __version__)
1399
+
1264
1400
  else:
1265
- raise NotImplementedError("Invalid command: ", args.command)
1401
+ if not suggest_correct_command(args.command):
1402
+ raise NotImplementedError(f"Invalid command: {args.command}, see `fivetran --help`")
1266
1403
 
1267
1404
 
1268
1405
  if __name__ == "__main__":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fivetran_connector_sdk
3
- Version: 0.8.21.1
3
+ Version: 0.8.26.1
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
@@ -17,7 +17,7 @@ Requires-Dist: get_pypi_latest_version==0.0.12
17
17
  Requires-Dist: pipreqs==0.5.0
18
18
 
19
19
  # **fivetran-connector-sdk**
20
- The *fivetran-connector-sdk* is a Python module that enables you to write custom data connectors and deploy them as an extension of [Fivetran](https://www.fivetran.com/). Fivetran automatically manages running the connectors on your scheduled frequency and manages the required compute resources.
20
+ The *fivetran-connector-sdk* SDK allows users to execute custom, self-written Python code within [Fivetran's](https://www.fivetran.com/) secure cloud environment. Fivetran automatically manages running the connectors on your scheduled frequency and manages the required compute resources.
21
21
 
22
22
  The Connector SDK service is the best fit for the following use cases:
23
23
  - Fivetran doesn't have a connector for your source and is unlikely to support it soon.