fivetran-connector-sdk 1.6.0__tar.gz → 1.7.0__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 (23) hide show
  1. {fivetran_connector_sdk-1.6.0 → fivetran_connector_sdk-1.7.0}/PKG-INFO +2 -2
  2. {fivetran_connector_sdk-1.6.0 → fivetran_connector_sdk-1.7.0}/pyproject.toml +1 -1
  3. {fivetran_connector_sdk-1.6.0 → fivetran_connector_sdk-1.7.0}/src/fivetran_connector_sdk/__init__.py +44 -52
  4. {fivetran_connector_sdk-1.6.0 → fivetran_connector_sdk-1.7.0}/src/fivetran_connector_sdk/connector_helper.py +134 -42
  5. {fivetran_connector_sdk-1.6.0 → fivetran_connector_sdk-1.7.0}/src/fivetran_connector_sdk/helpers.py +22 -11
  6. {fivetran_connector_sdk-1.6.0 → fivetran_connector_sdk-1.7.0}/src/fivetran_connector_sdk/operations.py +43 -46
  7. {fivetran_connector_sdk-1.6.0 → fivetran_connector_sdk-1.7.0}/src/fivetran_connector_sdk.egg-info/PKG-INFO +2 -2
  8. {fivetran_connector_sdk-1.6.0 → fivetran_connector_sdk-1.7.0}/src/fivetran_connector_sdk.egg-info/requires.txt +1 -1
  9. {fivetran_connector_sdk-1.6.0 → fivetran_connector_sdk-1.7.0}/README.md +0 -0
  10. {fivetran_connector_sdk-1.6.0 → fivetran_connector_sdk-1.7.0}/setup.cfg +0 -0
  11. {fivetran_connector_sdk-1.6.0 → fivetran_connector_sdk-1.7.0}/src/fivetran_connector_sdk/constants.py +0 -0
  12. {fivetran_connector_sdk-1.6.0 → fivetran_connector_sdk-1.7.0}/src/fivetran_connector_sdk/logger.py +0 -0
  13. {fivetran_connector_sdk-1.6.0 → fivetran_connector_sdk-1.7.0}/src/fivetran_connector_sdk/protos/__init__.py +0 -0
  14. {fivetran_connector_sdk-1.6.0 → fivetran_connector_sdk-1.7.0}/src/fivetran_connector_sdk/protos/common_pb2.py +0 -0
  15. {fivetran_connector_sdk-1.6.0 → fivetran_connector_sdk-1.7.0}/src/fivetran_connector_sdk/protos/common_pb2.pyi +0 -0
  16. {fivetran_connector_sdk-1.6.0 → fivetran_connector_sdk-1.7.0}/src/fivetran_connector_sdk/protos/common_pb2_grpc.py +0 -0
  17. {fivetran_connector_sdk-1.6.0 → fivetran_connector_sdk-1.7.0}/src/fivetran_connector_sdk/protos/connector_sdk_pb2.py +0 -0
  18. {fivetran_connector_sdk-1.6.0 → fivetran_connector_sdk-1.7.0}/src/fivetran_connector_sdk/protos/connector_sdk_pb2.pyi +0 -0
  19. {fivetran_connector_sdk-1.6.0 → fivetran_connector_sdk-1.7.0}/src/fivetran_connector_sdk/protos/connector_sdk_pb2_grpc.py +0 -0
  20. {fivetran_connector_sdk-1.6.0 → fivetran_connector_sdk-1.7.0}/src/fivetran_connector_sdk.egg-info/SOURCES.txt +0 -0
  21. {fivetran_connector_sdk-1.6.0 → fivetran_connector_sdk-1.7.0}/src/fivetran_connector_sdk.egg-info/dependency_links.txt +0 -0
  22. {fivetran_connector_sdk-1.6.0 → fivetran_connector_sdk-1.7.0}/src/fivetran_connector_sdk.egg-info/entry_points.txt +0 -0
  23. {fivetran_connector_sdk-1.6.0 → fivetran_connector_sdk-1.7.0}/src/fivetran_connector_sdk.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fivetran_connector_sdk
3
- Version: 1.6.0
3
+ Version: 1.7.0
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
@@ -13,7 +13,7 @@ Requires-Python: >=3.9
13
13
  Description-Content-Type: text/markdown
14
14
  Requires-Dist: grpcio==1.71.0
15
15
  Requires-Dist: grpcio-tools==1.71.0
16
- Requires-Dist: requests==2.32.3
16
+ Requires-Dist: requests==2.32.4
17
17
  Requires-Dist: pipreqs==0.5.0
18
18
  Requires-Dist: unidecode==1.4.0
19
19
 
@@ -15,7 +15,7 @@ classifiers = [
15
15
  dependencies = [
16
16
  "grpcio==1.71.0",
17
17
  "grpcio-tools==1.71.0",
18
- "requests==2.32.3",
18
+ "requests==2.32.4",
19
19
  "pipreqs==0.5.0",
20
20
  "unidecode==1.4.0"
21
21
  ]
@@ -19,27 +19,27 @@ from fivetran_connector_sdk.operations import Operations
19
19
  from fivetran_connector_sdk import constants
20
20
  from fivetran_connector_sdk.constants import (
21
21
  TESTER_VER, VERSION_FILENAME, UTF_8,
22
- VALID_COMMANDS, DEFAULT_PYTHON_VERSION, SUPPORTED_PYTHON_VERSIONS, FIVETRAN_HD_AGENT_ID, TABLES
22
+ VALID_COMMANDS, DEFAULT_PYTHON_VERSION, SUPPORTED_PYTHON_VERSIONS, TABLES
23
23
  )
24
24
  from fivetran_connector_sdk.helpers import (
25
- print_library_log, reset_local_file_directory, find_connector_object, validate_and_load_configuration,
26
- validate_and_load_state, get_input_from_cli, suggest_correct_command,
25
+ print_library_log, reset_local_file_directory, find_connector_object, suggest_correct_command,
27
26
  )
28
27
  from fivetran_connector_sdk.connector_helper import (
29
28
  validate_requirements_file, upload_project,
30
- update_connection, are_setup_tests_failing,
31
- validate_deploy_parameters, get_connection_id,
29
+ update_connection, are_setup_tests_failing, get_connection_id,
32
30
  handle_failing_tests_message_and_exit, delete_file_if_exists,
33
31
  create_connection, get_os_arch_suffix, get_group_info,
34
32
  java_exe_helper, run_tester, process_tables,
35
33
  update_base_url_if_required, exit_check,
36
34
  get_available_port, tester_root_dir_helper,
37
35
  check_dict, check_newer_version, cleanup_uploaded_project,
36
+ get_destination_group, get_connection_name, get_api_key, get_state,
37
+ get_python_version, get_hd_agent_id, get_configuration
38
38
  )
39
39
 
40
40
  # Version format: <major_version>.<minor_version>.<patch_version>
41
41
  # (where Major Version = 1 for GA, Minor Version is incremental MM from Jan 25 onwards, Patch Version is incremental within a month)
42
- __version__ = "1.6.0"
42
+ __version__ = "1.7.0"
43
43
  TESTER_VERSION = TESTER_VER
44
44
  MAX_MESSAGE_LENGTH = 32 * 1024 * 1024 # 32MB
45
45
 
@@ -62,7 +62,8 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
62
62
  update_base_url_if_required()
63
63
 
64
64
  # Call this method to deploy the connector to Fivetran platform
65
- def deploy(self, args: dict, deploy_key: str, group: str, connection: str, hd_agent_id: str, configuration: dict = None):
65
+ def deploy(self, project_path: str, deploy_key: str, group: str, connection: str, hd_agent_id: str,
66
+ configuration: dict = None, config_path = None, python_version: str = None, force: bool = False):
66
67
  """Deploys the connector to the Fivetran platform.
67
68
 
68
69
  Args:
@@ -77,8 +78,6 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
77
78
 
78
79
  print_library_log("We support only `.py` files and a `requirements.txt` file as part of the code upload. *No other code files* are supported or uploaded during the deployment process. Ensure that your code is structured accordingly and all dependencies are listed in `requirements.txt`")
79
80
 
80
- validate_deploy_parameters(connection, deploy_key)
81
-
82
81
  check_dict(configuration, True)
83
82
 
84
83
  secrets_list = []
@@ -92,10 +91,10 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
92
91
  "sync_method": "DIRECT"
93
92
  }
94
93
 
95
- if args.python_version:
96
- connection_config["python_version"] = args.python_version
94
+ if python_version:
95
+ connection_config["python_version"] = python_version
97
96
 
98
- validate_requirements_file(args.project_path, True, __version__, args.force)
97
+ validate_requirements_file(project_path, True, __version__, force)
99
98
 
100
99
  group_id, group_name = get_group_info(group, deploy_key)
101
100
  connection_id, service = get_connection_id(connection, group, group_id, deploy_key) or (None, None)
@@ -106,21 +105,21 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
106
105
  f"The connection '{connection}' already exists and does not use the 'Connector SDK' service. You cannot update this connection.", Logging.Level.SEVERE)
107
106
  sys.exit(1)
108
107
  else:
109
- if args.force:
108
+ if force:
110
109
  confirm = "y"
111
- if args.configuration:
110
+ if configuration:
112
111
  confirm_config = "y"
113
112
  else:
114
113
  confirm = input(
115
- f"The connection '{connection}' already exists in the destination '{group}'. Updating it will overwrite the existing code. Do you want to proceed with the update? (Y/N): ")
116
- if confirm.lower() == "y" and args.configuration:
117
- confirm_config = input(f"Your deploy will overwrite the configuration using the values provided in '{args.configuration}': key-value pairs not present in the new configuration will be removed; existing keys' values set in the cofiguration file or in the dashboard will be overwritten with new (empty or non-empty) values; new key-value pairs will be added. Do you want to proceed with the update? (Y/N): ")
114
+ f"The connection '{connection}' already exists in the destination '{group}'. Updating it will overwrite the existing code. Do you want to proceed with the update? (y/N): ")
115
+ if confirm.lower() == "y" and configuration:
116
+ confirm_config = input(f"Your deploy will overwrite the configuration using the values provided in '{config_path}': key-value pairs not present in the new configuration will be removed; existing keys' values set in the configuration file or in the dashboard will be overwritten with new (empty or non-empty) values; new key-value pairs will be added. Do you want to proceed with the update? (y/N): ")
118
117
  if confirm.lower() == "y" and (not connection_config["secrets_list"] or (confirm_config.lower() == "y")):
119
118
  print_library_log("Updating the connection...\n")
120
119
  upload_project(
121
- args.project_path, deploy_key, group_id, group_name, connection)
120
+ project_path, deploy_key, group_id, group_name, connection)
122
121
  response = update_connection(
123
- args, connection_id, connection, group_name, connection_config, deploy_key, hd_agent_id)
122
+ connection_id, connection, group_name, connection_config, deploy_key, hd_agent_id)
124
123
  print("✓")
125
124
  print_library_log(f"Python version {response.json()['data']['config']['python_version']} to be used at runtime.",
126
125
  Logging.Level.INFO)
@@ -131,7 +130,7 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
131
130
  print_library_log("Update canceled. The process is now terminating.")
132
131
  sys.exit(1)
133
132
  else:
134
- upload_project(args.project_path, deploy_key,
133
+ upload_project(project_path, deploy_key,
135
134
  group_id, group_name, connection)
136
135
  response = create_connection(
137
136
  deploy_key, group_id, connection_config, hd_agent_id)
@@ -377,6 +376,9 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
377
376
  print_library_log(error_message, Logging.Level.SEVERE)
378
377
  raise RuntimeError(error_message) from e
379
378
 
379
+ def print_version():
380
+ print_library_log("fivetran_connector_sdk " + __version__)
381
+ sys.exit(0)
380
382
 
381
383
  def main():
382
384
  """The main entry point for the script.
@@ -389,7 +391,8 @@ def main():
389
391
  parser._option_string_actions["-h"].help = "Show this help message and exit"
390
392
 
391
393
  # Positional
392
- parser.add_argument("command", help="|".join(VALID_COMMANDS))
394
+ parser.add_argument("--version", action="store_true", help="Print the version of the fivetran_connector_sdk and exit")
395
+ parser.add_argument("command", nargs="?", help="|".join(VALID_COMMANDS))
393
396
  parser.add_argument("project_path", nargs='?', default=os.getcwd(), help="Path to connector project directory")
394
397
 
395
398
  # Optional (Not all of these are valid with every mutually exclusive option below)
@@ -401,52 +404,41 @@ def main():
401
404
  parser.add_argument("-f", "--force", action="store_true", help="Force update an existing connection")
402
405
  parser.add_argument("--python-version", "--python", type=str, help=f"Supported Python versions you can use: {SUPPORTED_PYTHON_VERSIONS}. Defaults to {DEFAULT_PYTHON_VERSION}")
403
406
  parser.add_argument("--hybrid-deployment-agent-id", type=str, help="The Hybrid Deployment agent within the Fivetran system. If nothing is passed, the default agent of the destination is used.")
404
-
405
407
  args = parser.parse_args()
406
408
 
409
+ if args.version:
410
+ print_version()
411
+
412
+ if not args.command:
413
+ parser.print_help()
414
+ sys.exit(1)
415
+
407
416
  if args.command.lower() == "version":
408
- print_library_log("fivetran_connector_sdk " + __version__)
409
- return
417
+ print_version()
410
418
  elif args.command.lower() == "reset":
411
419
  reset_local_file_directory(args)
412
- return
420
+ sys.exit(0)
413
421
 
414
422
  connector_object = find_connector_object(args.project_path)
415
423
 
416
424
  if not connector_object:
417
425
  sys.exit(1)
418
426
 
419
- # Process optional args
420
- ft_group = args.destination if args.destination else None
421
- ft_connection = args.connection if args.connection else None
422
- ft_deploy_key = args.api_key if args.api_key else None
423
- hd_agent_id = args.hybrid_deployment_agent_id if args.hybrid_deployment_agent_id else os.getenv(FIVETRAN_HD_AGENT_ID, None)
424
- configuration = args.configuration if args.configuration else None
425
- state = args.state if args.state else os.getenv('FIVETRAN_STATE', None)
426
-
427
- configuration = validate_and_load_configuration(args, configuration)
428
- state = validate_and_load_state(args, state)
429
-
430
- FIVETRAN_BASE_64_ENCODED_API_KEY = os.getenv('FIVETRAN_BASE_64_ENCODED_API_KEY', None)
431
- FIVETRAN_DESTINATION_NAME = os.getenv('FIVETRAN_DESTINATION_NAME', None)
432
- FIVETRAN_CONNECTION_NAME = os.getenv('FIVETRAN_CONNECTION_NAME', None)
433
-
434
427
  if args.command.lower() == "deploy":
435
- if args.state:
436
- print_library_log("'state' parameter is not used for 'deploy' command", Logging.Level.WARNING)
437
-
438
- if not ft_deploy_key:
439
- ft_deploy_key = get_input_from_cli("Please provide the API Key", FIVETRAN_BASE_64_ENCODED_API_KEY)
440
-
441
- if not ft_group:
442
- ft_group = get_input_from_cli("Please provide the destination", FIVETRAN_DESTINATION_NAME)
443
-
444
- if not ft_connection:
445
- ft_connection = get_input_from_cli("Please provide the connection name",FIVETRAN_CONNECTION_NAME)
428
+ ft_group = get_destination_group(args)
429
+ ft_connection = get_connection_name(args)
430
+ ft_deploy_key = get_api_key(args)
431
+ python_version = get_python_version(args)
432
+ hd_agent_id = get_hd_agent_id(args)
433
+ configuration, config_path = get_configuration(args)
434
+ get_state(args)
446
435
 
447
- connector_object.deploy(args, ft_deploy_key, ft_group, ft_connection, hd_agent_id, configuration)
436
+ connector_object.deploy(args.project_path, ft_deploy_key, ft_group, ft_connection, hd_agent_id,
437
+ configuration, config_path, python_version, args.force)
448
438
 
449
439
  elif args.command.lower() == "debug":
440
+ configuration, config_path = get_configuration(args)
441
+ state = get_state(args)
450
442
  connector_object.debug(args.project_path, configuration, state)
451
443
  else:
452
444
  if not suggest_correct_command(args.command):
@@ -18,9 +18,10 @@ from fivetran_connector_sdk.protos import common_pb2
18
18
  from fivetran_connector_sdk import constants
19
19
  from fivetran_connector_sdk.logger import Logging
20
20
  from fivetran_connector_sdk.helpers import (
21
- print_library_log,
22
- get_renamed_table_name,
23
- get_renamed_column_name
21
+ print_library_log, get_renamed_table_name,
22
+ get_renamed_column_name, get_input_from_cli,
23
+ validate_and_load_state,
24
+ validate_and_load_configuration
24
25
  )
25
26
  from fivetran_connector_sdk.constants import (
26
27
  OS_MAP,
@@ -49,6 +50,97 @@ from fivetran_connector_sdk.constants import (
49
50
  TABLES,
50
51
  )
51
52
 
53
+ def get_destination_group(args):
54
+ ft_group = args.destination if args.destination else None
55
+ env_destination_name = os.getenv('FIVETRAN_DESTINATION_NAME', None)
56
+ if not ft_group:
57
+ ft_group = get_input_from_cli("Provide the destination name (as displayed in your dashboard destination list)", env_destination_name)
58
+ return ft_group
59
+
60
+ def get_connection_name(args):
61
+ ft_connection = args.connection if args.connection else None
62
+ env_connection_name = os.getenv('FIVETRAN_CONNECTION_NAME', None)
63
+ if not ft_connection:
64
+ for retrying in range(MAX_RETRIES):
65
+ ft_connection = get_input_from_cli("Provide the connection name", env_connection_name)
66
+ if not is_connection_name_valid(ft_connection):
67
+ if retrying==MAX_RETRIES-1:
68
+ sys.exit(1)
69
+ else:
70
+ print_library_log(f"Connection name: {ft_connection} is invalid!\n The connection name should start with an "
71
+ f"underscore or a lowercase letter (a-z), followed by any combination of underscores, lowercase "
72
+ f"letters, or digits (0-9). Uppercase characters are not allowed.", Logging.Level.SEVERE)
73
+ print_library_log("Please retry...", Logging.Level.INFO)
74
+ else:
75
+ break
76
+ if not is_connection_name_valid(ft_connection):
77
+ print_library_log(f"Connection name: {ft_connection} is invalid!\n The connection name should start with an "
78
+ f"underscore or a lowercase letter (a-z), followed by any combination of underscores, lowercase "
79
+ f"letters, or digits (0-9). Uppercase characters are not allowed.", Logging.Level.SEVERE)
80
+ sys.exit(1)
81
+ return ft_connection
82
+
83
+ def get_api_key(args):
84
+ ft_deploy_key = args.api_key if args.api_key else None
85
+ env_api_key = os.getenv('FIVETRAN_API_KEY', None)
86
+ if not ft_deploy_key:
87
+ ft_deploy_key = get_input_from_cli("Provide your API Key (Base 64 Encoded)", env_api_key, True)
88
+ return ft_deploy_key
89
+
90
+ def get_python_version(args):
91
+ python_version = args.python_version if args.python_version else None
92
+ env_python_version = os.getenv('FIVETRAN_PYTHON_VERSION', None)
93
+ if env_python_version and not python_version and not args.force:
94
+ python_version = get_input_from_cli("Provide your python version", env_python_version)
95
+ return python_version
96
+
97
+ def get_hd_agent_id(args):
98
+ hd_agent_id = args.hybrid_deployment_agent_id if args.hybrid_deployment_agent_id else None
99
+ env_hd_agent_id = os.getenv('FIVETRAN_HD_AGENT_ID', None)
100
+
101
+ if env_hd_agent_id and not hd_agent_id and not args.force:
102
+ hd_agent_id = get_input_from_cli("Provide the Hybrid Deployment Agent ID", env_hd_agent_id)
103
+ return hd_agent_id
104
+
105
+ def get_state(args):
106
+ if args.command.lower() == "deploy" and args.state:
107
+ print_library_log("Unrecognised argument for deploy: --state."
108
+ "'state' is not set using the 'deploy' command. You can manage the state for deployed connections via "
109
+ "Fivetran API, https://fivetran.com/docs/connector-sdk/working-with-connector-sdk#workingwithstatejsonfile",
110
+ Logging.Level.WARNING)
111
+ sys.exit(1)
112
+ state = args.state if args.state else os.getenv('FIVETRAN_STATE', None)
113
+ state = validate_and_load_state(args, state)
114
+ return state
115
+
116
+ def get_configuration(args):
117
+ configuration = args.configuration if args.configuration else None
118
+ env_configuration = os.getenv('FIVETRAN_CONFIGURATION', None)
119
+ if not configuration and not args.force and args.command.lower() == "deploy":
120
+ json_filepath = os.path.join(args.project_path, "configuration.json")
121
+ if os.path.exists(json_filepath):
122
+ print_library_log("configuration.json file detected in the project, "
123
+ "but no configuration input provided via the command line", Logging.Level.WARNING)
124
+ env_configuration = env_configuration if env_configuration else "configuration.json"
125
+ confirm = input(f"Does this debug run/deploy need configuration (y/N):")
126
+ if confirm.lower()=='y':
127
+ for retrying in range(MAX_RETRIES):
128
+ try:
129
+ configuration = get_input_from_cli("Provide the configuration file path", env_configuration)
130
+ config_values = validate_and_load_configuration(args.project_path, configuration)
131
+ return config_values, configuration
132
+ except ValueError as e:
133
+ if retrying==MAX_RETRIES-1:
134
+ print_library_log(f"{e}. Invalid Configuration, Exiting..", Logging.Level.WARNING)
135
+ sys.exit(1)
136
+ else:
137
+ print_library_log(f"{e}. Please retry..", Logging.Level.INFO)
138
+ else:
139
+ print_library_log("No input required for configuration. Continuing without configuration.", Logging.Level.INFO)
140
+ config_values = validate_and_load_configuration(args.project_path, configuration)
141
+ return config_values, configuration
142
+
143
+
52
144
  def check_newer_version(version: str):
53
145
  """Periodically checks for a newer version of the SDK and notifies the user if one is available."""
54
146
  tester_root_dir = tester_root_dir_helper()
@@ -80,7 +172,7 @@ def check_newer_version(version: str):
80
172
  except Exception:
81
173
  retry_after = 2 ** index
82
174
  print_library_log(f"Unable to check if a newer version of `fivetran-connector-sdk` is available. "
83
- f"Retrying again after {retry_after} seconds", Logging.Level.WARNING)
175
+ f"Retrying after {retry_after} seconds", Logging.Level.WARNING)
84
176
  time.sleep(retry_after)
85
177
 
86
178
 
@@ -89,7 +181,7 @@ def tester_root_dir_helper() -> str:
89
181
  return os.path.join(os.path.expanduser("~"), ROOT_LOCATION)
90
182
 
91
183
  def _warn_exit_usage(filename, line_no, func):
92
- 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 " +
184
+ print_library_log(f"Avoid using {func} to exit from the Python code as this can cause the connector to become stuck. Throw an error if required " +
93
185
  f"at: {filename}:{line_no}. See the Technical Reference for details: https://fivetran.com/docs/connector-sdk/technical-reference#handlingexceptions",
94
186
  Logging.Level.WARNING)
95
187
 
@@ -134,13 +226,13 @@ def check_dict(incoming: dict, string_only: bool = False) -> dict:
134
226
 
135
227
  if not isinstance(incoming, dict):
136
228
  raise ValueError(
137
- "Configuration must be provided as a JSON dictionary. Please check your input. Reference: https://fivetran.com/docs/connectors/connector-sdk/detailed-guide#workingwithconfigurationjsonfile")
229
+ "Configuration must be provided as a JSON dictionary. Check your input. Reference: https://fivetran.com/docs/connectors/connector-sdk/detailed-guide#workingwithconfigurationjsonfile")
138
230
 
139
231
  if string_only:
140
232
  for k, v in incoming.items():
141
233
  if not isinstance(v, str):
142
234
  print_library_log(
143
- "All values in the configuration must be STRING. Please check your configuration and ensure that every value is a STRING.", Logging.Level.SEVERE)
235
+ "All values in the configuration must be STRING. Check your configuration and ensure that every value is a STRING.", Logging.Level.SEVERE)
144
236
  sys.exit(1)
145
237
 
146
238
  return incoming
@@ -160,28 +252,11 @@ def is_connection_name_valid(connection: str):
160
252
 
161
253
 
162
254
  def log_unused_deps_error(package_name: str, version: str):
163
- print_library_log(f"Please remove `{package_name}` from requirements.txt."
255
+ print_library_log(f"Remove `{package_name}` from requirements.txt."
164
256
  f" The latest version of `{package_name}` is always available when executing your code."
165
257
  f" Current version: {version}", Logging.Level.SEVERE)
166
258
 
167
259
 
168
- def validate_deploy_parameters(connection, deploy_key):
169
- if not deploy_key or not connection:
170
- print_library_log("The deploy command needs the following parameters:"
171
- "\n\tRequired:\n"
172
- "\t\t--api-key <BASE64-ENCODED-FIVETRAN-API-KEY-FOR-DEPLOYMENT>\n"
173
- "\t\t--connection <VALID-CONNECTOR-SCHEMA_NAME>\n"
174
- "\t(Optional):\n"
175
- "\t\t--destination <DESTINATION_NAME> (Becomes required if there are multiple destinations)\n"
176
- "\t\t--configuration <CONFIGURATION_FILE> (Completely replaces the existing configuration)", Logging.Level.SEVERE)
177
- sys.exit(1)
178
- elif not is_connection_name_valid(connection):
179
- print_library_log(f"Connection name: {connection} is invalid!\n The connection name should start with an "
180
- f"underscore or a lowercase letter (a-z), followed by any combination of underscores, lowercase "
181
- f"letters, or digits (0-9). Uppercase characters are not allowed.", Logging.Level.SEVERE)
182
- sys.exit(1)
183
-
184
-
185
260
  def is_port_in_use(port: int):
186
261
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
187
262
  return s.connect_ex(('127.0.0.1', port)) == 0
@@ -205,7 +280,7 @@ def update_base_url_if_required():
205
280
  print_library_log(f"Updating PRODUCTION_BASE_URL to: {base_url}")
206
281
 
207
282
  def fetch_requirements_from_file(file_path: str) -> list[str]:
208
- """Reads a requirements file and returns a list of dependencies.
283
+ """Reads the requirements file and returns a list of dependencies.
209
284
 
210
285
  Args:
211
286
  file_path (str): The path to the requirements file.
@@ -223,7 +298,7 @@ def fetch_requirements_as_dict(file_path: str) -> dict:
223
298
  file_path (str): The path to the requirements file.
224
299
 
225
300
  Returns:
226
- dict: A dictionary where keys are package names (lowercased) and
301
+ dict: A dictionary where keys are package names (lowercase) and
227
302
  values are the full dependency strings.
228
303
  """
229
304
  requirements_dict = {}
@@ -346,7 +421,7 @@ def validate_requirements_file(project_path: str, is_deploy: bool, version: str
346
421
  if confirm.lower() == "n":
347
422
  if 'fivetran_connector_sdk' in unused_deps or 'requests' in unused_deps:
348
423
  print_library_log(
349
- f"Please fix your {REQUIREMENTS_TXT} file by removing pre-installed dependencies [fivetran_connector_sdk, requests] to proceed with the deployment.")
424
+ f"Fix your {REQUIREMENTS_TXT} file by removing pre-installed dependencies [fivetran_connector_sdk, requests] to proceed with the deployment.")
350
425
  sys.exit(1)
351
426
  print_library_log(f"Changes identified for unused libraries have been ignored. These changes have NOT been made to {REQUIREMENTS_TXT}.")
352
427
  elif confirm.lower() == "y":
@@ -376,15 +451,15 @@ def handle_unused_deps(unused_deps, version):
376
451
  if 'fivetran_connector_sdk' in unused_deps:
377
452
  log_unused_deps_error("fivetran_connector_sdk", version)
378
453
  if 'requests' in unused_deps:
379
- log_unused_deps_error("requests", "2.32.3")
454
+ log_unused_deps_error("requests", "2.32.4")
380
455
  print_library_log("The following dependencies are not needed, "
381
- f"they are not used or already installed. Please remove them from {REQUIREMENTS_TXT}:", Logging.Level.WARNING)
456
+ f"they are already installed or not in use. Remove them from {REQUIREMENTS_TXT}:", Logging.Level.WARNING)
382
457
  print(*unused_deps)
383
458
 
384
459
  def handle_missing_deps(missing_deps):
385
- print_library_log(f"Please include the following dependency libraries in {REQUIREMENTS_TXT}, to be used by "
460
+ print_library_log(f"Include the following dependency libraries in {REQUIREMENTS_TXT}, to be used by "
386
461
  "Fivetran production. "
387
- "For more information, please visit: "
462
+ "For more information, see our docs: "
388
463
  "https://fivetran.com/docs/connectors/connector-sdk/detailed-guide"
389
464
  "#workingwithrequirementstxtfile", Logging.Level.SEVERE)
390
465
  print(*list(missing_deps.values()))
@@ -429,7 +504,7 @@ def cleanup_uploaded_project(deploy_key: str, group_id: str, connection: str):
429
504
  if not cleanup_result:
430
505
  sys.exit(1)
431
506
 
432
- def update_connection(args: dict, id: str, name: str, group: str, config: dict, deploy_key: str, hd_agent_id: str):
507
+ def update_connection(id: str, name: str, group: str, config: dict, deploy_key: str, hd_agent_id: str):
433
508
  """Updates the connection with the given ID, name, group, configuration, and deployment key.
434
509
 
435
510
  Args:
@@ -441,7 +516,7 @@ def update_connection(args: dict, id: str, name: str, group: str, config: dict,
441
516
  deploy_key (str): The deployment key.
442
517
  hd_agent_id (str): The hybrid deployment agent ID within the Fivetran system.
443
518
  """
444
- if not args.configuration:
519
+ if not config.get("secrets_list"):
445
520
  del config["secrets_list"]
446
521
 
447
522
  json_payload = {
@@ -476,7 +551,7 @@ def handle_failing_tests_message_and_exit(resp, log_message):
476
551
  print_failing_setup_tests(resp)
477
552
  connection_id = resp.json().get('data', {}).get('id')
478
553
  print_library_log(f"Connection ID: {connection_id}")
479
- print_library_log("Please try again with the deploy command after resolving the issue!")
554
+ print_library_log("Try again with the deploy command after resolving the issue!")
480
555
  sys.exit(1)
481
556
 
482
557
  def are_setup_tests_failing(response) -> bool:
@@ -497,7 +572,7 @@ def print_failing_setup_tests(response):
497
572
  test.get("status") == "FAILED" or test.get("status") == "JOB_FAILED"]
498
573
 
499
574
  if failed_tests:
500
- print_library_log("Following setup tests have failed!", Logging.Level.WARNING)
575
+ print_library_log("The following setup tests have failed!", Logging.Level.WARNING)
501
576
  for test in failed_tests:
502
577
  print_library_log(f"Test: {test.get('title')}", Logging.Level.WARNING)
503
578
  print_library_log(f"Status: {test.get('status')}", Logging.Level.WARNING)
@@ -598,7 +673,7 @@ def zip_folder(project_path: str) -> str:
598
673
 
599
674
  if not connector_file_exists:
600
675
  print_library_log(
601
- "The 'connector.py' file is missing. Please ensure that 'connector.py' is present in your project directory, and that the file name is in lowercase letters. All custom connectors require this file because Fivetran calls it to start a sync.",
676
+ "The 'connector.py' file is missing. Ensure that 'connector.py' is present in your project directory, and that the file name is in lowercase. All custom connectors require this file because Fivetran calls it to start a sync.",
602
677
  Logging.Level.SEVERE)
603
678
  sys.exit(1)
604
679
 
@@ -718,7 +793,7 @@ def get_group_info(group: str, deploy_key: str) -> tuple[str, str]:
718
793
 
719
794
  if not resp.ok:
720
795
  print_library_log(
721
- f"The request failed with status code: {resp.status_code}. Please ensure you're using a valid base64-encoded API key and try again.",
796
+ f"The request failed with status code: {resp.status_code}. Ensure you're using a valid base64-encoded API key and try again.",
722
797
  Logging.Level.SEVERE)
723
798
  sys.exit(1)
724
799
 
@@ -753,7 +828,7 @@ def get_group_info(group: str, deploy_key: str) -> tuple[str, str]:
753
828
  groups = data.get("items", [])
754
829
 
755
830
  print_library_log(
756
- f"The specified destination '{group}' was not found in your account.", Logging.Level.SEVERE)
831
+ f"We couldn't find the specified destination '{group}' in your account.", Logging.Level.SEVERE)
757
832
  sys.exit(1)
758
833
 
759
834
  def java_exe_helper(location: str, os_arch_suffix: str) -> str:
@@ -879,13 +954,22 @@ def process_columns(columns, entry):
879
954
 
880
955
  elif isinstance(type, dict):
881
956
  if type['type'].upper() != "DECIMAL":
882
- raise ValueError("Expecting DECIMAL data type")
957
+ error_message = (
958
+ f"Expecting DECIMAL data type for dictionary column entry, but got: {type['type']} in entry: {entry} "
959
+ f"for column: {column_name}. "
960
+ "Dictionary type is only allowed for DECIMAL columns with 'precision' and 'scale' fields, "
961
+ "as in: {'type': 'DECIMAL', 'precision': <int>, 'scale': <int>}. "
962
+ "For all other data types, use a string as the column type."
963
+ )
964
+ raise ValueError(error_message)
883
965
  column.type = common_pb2.DataType.DECIMAL
884
966
  column.decimal.precision = type['precision']
885
967
  column.decimal.scale = type['scale']
886
968
 
887
969
  else:
888
- raise ValueError("Unrecognized column type: ", str(type))
970
+ raise ValueError(
971
+ f"Unrecognized column type for column: {column_name} in entry: {entry}. Got: {str(type)}"
972
+ )
889
973
 
890
974
  if "primary_key" in entry and name in entry["primary_key"]:
891
975
  column.primary_key = True
@@ -902,7 +986,15 @@ def process_data_type(column, type):
902
986
  elif type.upper() == "LONG":
903
987
  column.type = common_pb2.DataType.LONG
904
988
  elif type.upper() == "DECIMAL":
905
- raise ValueError("DECIMAL data type missing precision and scale")
989
+ raise ValueError(
990
+ "DECIMAL data type missing precision and scale. "
991
+ "Use a dictionary for DECIMAL column type like: "
992
+ '''"col_name": { # Decimal data type with precision and scale.\n'''
993
+ ''' "type": "DECIMAL",\n'''
994
+ ''' "precision": 15,\n'''
995
+ ''' "scale": 2\n'''
996
+ '''}'''
997
+ )
906
998
  elif type.upper() == "FLOAT":
907
999
  column.type = common_pb2.DataType.FLOAT
908
1000
  elif type.upper() == "DOUBLE":
@@ -256,12 +256,16 @@ def edit_distance(first_string: str, second_string: str) -> int:
256
256
  return previous_row[second_string_length]
257
257
 
258
258
 
259
- def get_input_from_cli(prompt: str, default_value: str) -> str:
259
+ def get_input_from_cli(prompt: str, default_value: str, hide_value = False) -> str:
260
260
  """
261
261
  Prompts the user for input.
262
262
  """
263
263
  if default_value:
264
- value = input(f"{prompt} [Default : {default_value}]: ").strip() or default_value
264
+ if hide_value:
265
+ default_value_hidden = default_value[0:8] + "********"
266
+ value = input(f"{prompt} [Default : {default_value_hidden}]: ").strip() or default_value
267
+ else:
268
+ value = input(f"{prompt} [Default : {default_value}]: ").strip() or default_value
265
269
  else:
266
270
  value = input(f"{prompt}: ").strip()
267
271
 
@@ -269,29 +273,36 @@ def get_input_from_cli(prompt: str, default_value: str) -> str:
269
273
  raise ValueError("Missing required input: Expected a value but received None")
270
274
  return value
271
275
 
272
- def validate_and_load_configuration(args, configuration):
276
+ def validate_and_load_configuration(project_path, configuration):
273
277
  if configuration:
274
- json_filepath = os.path.join(args.project_path, args.configuration)
278
+ configuration = os.path.expanduser(configuration)
279
+ if os.path.isabs(configuration):
280
+ json_filepath = os.path.abspath(configuration)
281
+ else:
282
+ relative_path = os.path.join(project_path, configuration)
283
+ json_filepath = os.path.abspath(str(relative_path))
275
284
  if os.path.isfile(json_filepath):
276
285
  with open(json_filepath, 'r', encoding=UTF_8) as fi:
277
- configuration = json.load(fi)
286
+ try:
287
+ configuration = json.load(fi)
288
+ except:
289
+ raise ValueError(
290
+ "Configuration must be provided as a JSON file. Please check your input. Reference: "
291
+ "https://fivetran.com/docs/connectors/connector-sdk/detailed-guide#workingwithconfigurationjsonfile")
278
292
  if len(configuration) > MAX_CONFIG_FIELDS:
279
293
  raise ValueError(f"Configuration field count exceeds maximum of {MAX_CONFIG_FIELDS}. Reduce the field count.")
280
294
  else:
281
295
  raise ValueError(
282
- "Configuration must be provided as a JSON file. Please check your input. Reference: "
283
- "https://fivetran.com/docs/connectors/connector-sdk/detailed-guide#workingwithconfigurationjsonfile")
296
+ f"Configuration path is incorrect, cannot find file at the location {json_filepath}")
284
297
  else:
285
- json_filepath = os.path.join(args.project_path, "configuration.json")
286
- if os.path.exists(json_filepath):
287
- print_library_log("Configuration file detected in the project, but no configuration input provided via the command line", Logging.Level.WARNING)
298
+ print_library_log("No configuration file passed.", Logging.Level.INFO)
288
299
  configuration = {}
289
300
  return configuration
290
301
 
291
302
 
292
303
  def validate_and_load_state(args, state):
293
304
  if state:
294
- json_filepath = os.path.join(args.project_path, args.state)
305
+ json_filepath = os.path.abspath(os.path.join(args.project_path, args.state))
295
306
  else:
296
307
  json_filepath = os.path.join(args.project_path, "files", "state.json")
297
308
 
@@ -180,7 +180,7 @@ def _map_data_to_columns(data: dict, columns: dict) -> dict:
180
180
  if v is None:
181
181
  mapped_data[key] = common_pb2.ValueType(null=True)
182
182
  elif (key in columns) and columns[key].type != common_pb2.DataType.UNSPECIFIED:
183
- map_defined_data_type(columns, key, mapped_data, v)
183
+ map_defined_data_type(columns[key].type, key, mapped_data, v)
184
184
  else:
185
185
  map_inferred_data_type(key, mapped_data, v)
186
186
  return mapped_data
@@ -218,53 +218,50 @@ def map_inferred_data_type(k, mapped_data, v):
218
218
  # Convert arbitrary objects to string
219
219
  mapped_data[k] = common_pb2.ValueType(string=str(v))
220
220
 
221
+ _TYPE_HANDLERS = {
222
+ common_pb2.DataType.BOOLEAN: lambda val: common_pb2.ValueType(bool=val),
223
+ common_pb2.DataType.SHORT: lambda val: common_pb2.ValueType(short=val),
224
+ common_pb2.DataType.INT: lambda val: common_pb2.ValueType(int=val),
225
+ common_pb2.DataType.LONG: lambda val: common_pb2.ValueType(long=val),
226
+ common_pb2.DataType.DECIMAL: lambda val: common_pb2.ValueType(decimal=val),
227
+ common_pb2.DataType.FLOAT: lambda val: common_pb2.ValueType(float=val),
228
+ common_pb2.DataType.DOUBLE: lambda val: common_pb2.ValueType(double=val),
229
+ common_pb2.DataType.NAIVE_DATE: lambda val: common_pb2.ValueType(naive_date= _parse_naive_date_str(val)),
230
+ common_pb2.DataType.NAIVE_DATETIME: lambda val: common_pb2.ValueType(naive_datetime= _parse_naive_datetime_str(val)),
231
+ common_pb2.DataType.UTC_DATETIME: lambda val: common_pb2.ValueType(utc_datetime= _parse_utc_datetime_str(val)),
232
+ common_pb2.DataType.BINARY: lambda val: common_pb2.ValueType(binary=val),
233
+ common_pb2.DataType.XML: lambda val: common_pb2.ValueType(xml=val),
234
+ common_pb2.DataType.STRING: lambda val: common_pb2.ValueType(string=val if isinstance(val, str) else str(val)),
235
+ common_pb2.DataType.JSON: lambda val: common_pb2.ValueType(json=json.dumps(val))
236
+ }
221
237
 
222
- def map_defined_data_type(columns, k, mapped_data, v):
223
- if columns[k].type == common_pb2.DataType.BOOLEAN:
224
- mapped_data[k] = common_pb2.ValueType(bool=v)
225
- elif columns[k].type == common_pb2.DataType.SHORT:
226
- mapped_data[k] = common_pb2.ValueType(short=v)
227
- elif columns[k].type == common_pb2.DataType.INT:
228
- mapped_data[k] = common_pb2.ValueType(int=v)
229
- elif columns[k].type == common_pb2.DataType.LONG:
230
- mapped_data[k] = common_pb2.ValueType(long=v)
231
- elif columns[k].type == common_pb2.DataType.DECIMAL:
232
- mapped_data[k] = common_pb2.ValueType(decimal=v)
233
- elif columns[k].type == common_pb2.DataType.FLOAT:
234
- mapped_data[k] = common_pb2.ValueType(float=v)
235
- elif columns[k].type == common_pb2.DataType.DOUBLE:
236
- mapped_data[k] = common_pb2.ValueType(double=v)
237
- elif columns[k].type == common_pb2.DataType.NAIVE_DATE:
238
- timestamp = timestamp_pb2.Timestamp()
239
- dt = datetime.strptime(v, "%Y-%m-%d")
240
- timestamp.FromDatetime(dt)
241
- mapped_data[k] = common_pb2.ValueType(naive_date=timestamp)
242
- elif columns[k].type == common_pb2.DataType.NAIVE_DATETIME:
243
- if '.' not in v: v = v + ".0"
244
- timestamp = timestamp_pb2.Timestamp()
245
- dt = datetime.strptime(v, "%Y-%m-%dT%H:%M:%S.%f")
246
- timestamp.FromDatetime(dt)
247
- mapped_data[k] = common_pb2.ValueType(naive_datetime=timestamp)
248
- elif columns[k].type == common_pb2.DataType.UTC_DATETIME:
249
- timestamp = timestamp_pb2.Timestamp()
250
- dt = v if isinstance(v, datetime) else _parse_datetime_str(v)
251
- timestamp.FromDatetime(dt)
252
- mapped_data[k] = common_pb2.ValueType(utc_datetime=timestamp)
253
- elif columns[k].type == common_pb2.DataType.BINARY:
254
- mapped_data[k] = common_pb2.ValueType(binary=v)
255
- elif columns[k].type == common_pb2.DataType.XML:
256
- mapped_data[k] = common_pb2.ValueType(xml=v)
257
- elif columns[k].type == common_pb2.DataType.STRING:
258
- incoming = v if isinstance(v, str) else str(v)
259
- mapped_data[k] = common_pb2.ValueType(string=incoming)
260
- elif columns[k].type == common_pb2.DataType.JSON:
261
- mapped_data[k] = common_pb2.ValueType(json=json.dumps(v))
238
+ def map_defined_data_type(data_type, k, mapped_data, v):
239
+ handler = _TYPE_HANDLERS.get(data_type)
240
+ if handler:
241
+ mapped_data[k] = handler(v)
262
242
  else:
263
- raise ValueError(f"Unsupported data type encountered: {columns[k].type}. Please use valid data types.")
264
-
265
- def _parse_datetime_str(dt):
266
- return datetime.strptime(dt, "%Y-%m-%dT%H:%M:%S.%f%z" if '.' in dt else "%Y-%m-%dT%H:%M:%S%z")
267
-
243
+ raise ValueError(f"Unsupported data type encountered: {data_type}. Please use valid data types.")
244
+
245
+ def _parse_utc_datetime_str(v):
246
+ timestamp = timestamp_pb2.Timestamp()
247
+ dt = v
248
+ if not isinstance(v, datetime):
249
+ dt = datetime.strptime(dt, "%Y-%m-%dT%H:%M:%S.%f%z" if '.' in dt else "%Y-%m-%dT%H:%M:%S%z")
250
+ timestamp.FromDatetime(dt)
251
+ return timestamp
252
+
253
+ def _parse_naive_datetime_str(v):
254
+ if '.' not in v: v = v + ".0"
255
+ timestamp = timestamp_pb2.Timestamp()
256
+ dt = datetime.strptime(v, "%Y-%m-%dT%H:%M:%S.%f")
257
+ timestamp.FromDatetime(dt)
258
+ return timestamp
259
+
260
+ def _parse_naive_date_str(v):
261
+ timestamp = timestamp_pb2.Timestamp()
262
+ dt = datetime.strptime(v, "%Y-%m-%d")
263
+ timestamp.FromDatetime(dt)
264
+ return timestamp
268
265
 
269
266
  def _yield_check(stack):
270
267
  """Checks for the presence of 'yield' in the calling code.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fivetran_connector_sdk
3
- Version: 1.6.0
3
+ Version: 1.7.0
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
@@ -13,7 +13,7 @@ Requires-Python: >=3.9
13
13
  Description-Content-Type: text/markdown
14
14
  Requires-Dist: grpcio==1.71.0
15
15
  Requires-Dist: grpcio-tools==1.71.0
16
- Requires-Dist: requests==2.32.3
16
+ Requires-Dist: requests==2.32.4
17
17
  Requires-Dist: pipreqs==0.5.0
18
18
  Requires-Dist: unidecode==1.4.0
19
19
 
@@ -1,5 +1,5 @@
1
1
  grpcio==1.71.0
2
2
  grpcio-tools==1.71.0
3
- requests==2.32.3
3
+ requests==2.32.4
4
4
  pipreqs==0.5.0
5
5
  unidecode==1.4.0