fivetran-connector-sdk 1.6.0__py3-none-any.whl → 1.7.1__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 +57 -54
- fivetran_connector_sdk/connector_helper.py +142 -47
- fivetran_connector_sdk/constants.py +2 -2
- fivetran_connector_sdk/helpers.py +67 -15
- fivetran_connector_sdk/operations.py +50 -60
- fivetran_connector_sdk/protos/common_pb2.py +64 -44
- fivetran_connector_sdk/protos/common_pb2.pyi +67 -17
- fivetran_connector_sdk/protos/common_pb2_grpc.py +20 -0
- fivetran_connector_sdk/protos/connector_sdk_pb2.py +53 -49
- fivetran_connector_sdk/protos/connector_sdk_pb2.pyi +8 -30
- fivetran_connector_sdk/protos/connector_sdk_pb2_grpc.py +90 -29
- {fivetran_connector_sdk-1.6.0.dist-info → fivetran_connector_sdk-1.7.1.dist-info}/METADATA +3 -2
- fivetran_connector_sdk-1.7.1.dist-info/RECORD +18 -0
- fivetran_connector_sdk-1.6.0.dist-info/RECORD +0 -18
- {fivetran_connector_sdk-1.6.0.dist-info → fivetran_connector_sdk-1.7.1.dist-info}/WHEEL +0 -0
- {fivetran_connector_sdk-1.6.0.dist-info → fivetran_connector_sdk-1.7.1.dist-info}/entry_points.txt +0 -0
- {fivetran_connector_sdk-1.6.0.dist-info → fivetran_connector_sdk-1.7.1.dist-info}/top_level.txt +0 -0
@@ -19,33 +19,33 @@ 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,
|
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,
|
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.
|
42
|
+
__version__ = "1.7.1"
|
43
43
|
TESTER_VERSION = TESTER_VER
|
44
44
|
MAX_MESSAGE_LENGTH = 32 * 1024 * 1024 # 32MB
|
45
45
|
|
46
46
|
__all__ = [cls.__name__ for cls in [Logging, Operations]]
|
47
47
|
|
48
|
-
class Connector(connector_sdk_pb2_grpc.
|
48
|
+
class Connector(connector_sdk_pb2_grpc.SourceConnectorServicer):
|
49
49
|
def __init__(self, update, schema=None):
|
50
50
|
"""Initializes the Connector instance.
|
51
51
|
Args:
|
@@ -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,
|
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:
|
@@ -73,12 +74,21 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
|
|
73
74
|
hd_agent_id (str): The hybrid deployment agent ID within the Fivetran system.
|
74
75
|
configuration (dict): The configuration dictionary.
|
75
76
|
"""
|
77
|
+
deploy_cmd = f"Deploying with parameters: Fivetran deploy --destination {group} --connection {connection} --api-key {deploy_key[0:8]}******** "
|
78
|
+
if config_path:
|
79
|
+
deploy_cmd += f"--configuration {config_path} "
|
80
|
+
if python_version:
|
81
|
+
deploy_cmd += f"--python-version {python_version} "
|
82
|
+
if hd_agent_id:
|
83
|
+
deploy_cmd += f"--hd-agent-id {hd_agent_id} "
|
84
|
+
if force:
|
85
|
+
deploy_cmd += "--force"
|
86
|
+
print_library_log(deploy_cmd)
|
87
|
+
|
76
88
|
constants.EXECUTED_VIA_CLI = True
|
77
89
|
|
78
90
|
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
91
|
|
80
|
-
validate_deploy_parameters(connection, deploy_key)
|
81
|
-
|
82
92
|
check_dict(configuration, True)
|
83
93
|
|
84
94
|
secrets_list = []
|
@@ -92,10 +102,10 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
|
|
92
102
|
"sync_method": "DIRECT"
|
93
103
|
}
|
94
104
|
|
95
|
-
if
|
96
|
-
connection_config["python_version"] =
|
105
|
+
if python_version:
|
106
|
+
connection_config["python_version"] = python_version
|
97
107
|
|
98
|
-
validate_requirements_file(
|
108
|
+
validate_requirements_file(project_path, True, __version__, force)
|
99
109
|
|
100
110
|
group_id, group_name = get_group_info(group, deploy_key)
|
101
111
|
connection_id, service = get_connection_id(connection, group, group_id, deploy_key) or (None, None)
|
@@ -106,21 +116,21 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
|
|
106
116
|
f"The connection '{connection}' already exists and does not use the 'Connector SDK' service. You cannot update this connection.", Logging.Level.SEVERE)
|
107
117
|
sys.exit(1)
|
108
118
|
else:
|
109
|
-
if
|
119
|
+
if force:
|
110
120
|
confirm = "y"
|
111
|
-
if
|
121
|
+
if configuration:
|
112
122
|
confirm_config = "y"
|
113
123
|
else:
|
114
124
|
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? (
|
116
|
-
if confirm.lower() == "y" and
|
117
|
-
confirm_config = input(f"Your deploy will overwrite the configuration using the values provided in '{
|
125
|
+
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): ")
|
126
|
+
if confirm.lower() == "y" and configuration:
|
127
|
+
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
128
|
if confirm.lower() == "y" and (not connection_config["secrets_list"] or (confirm_config.lower() == "y")):
|
119
129
|
print_library_log("Updating the connection...\n")
|
120
130
|
upload_project(
|
121
|
-
|
131
|
+
project_path, deploy_key, group_id, group_name, connection)
|
122
132
|
response = update_connection(
|
123
|
-
|
133
|
+
connection_id, connection, group_name, connection_config, deploy_key, hd_agent_id)
|
124
134
|
print("✓")
|
125
135
|
print_library_log(f"Python version {response.json()['data']['config']['python_version']} to be used at runtime.",
|
126
136
|
Logging.Level.INFO)
|
@@ -131,7 +141,7 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
|
|
131
141
|
print_library_log("Update canceled. The process is now terminating.")
|
132
142
|
sys.exit(1)
|
133
143
|
else:
|
134
|
-
upload_project(
|
144
|
+
upload_project(project_path, deploy_key,
|
135
145
|
group_id, group_name, connection)
|
136
146
|
response = create_connection(
|
137
147
|
deploy_key, group_id, connection_config, hd_agent_id)
|
@@ -185,7 +195,7 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
|
|
185
195
|
('grpc.max_receive_message_length', MAX_MESSAGE_LENGTH),
|
186
196
|
]
|
187
197
|
)
|
188
|
-
connector_sdk_pb2_grpc.
|
198
|
+
connector_sdk_pb2_grpc.add_SourceConnectorServicer_to_server(self, server)
|
189
199
|
server.add_insecure_port("[::]:" + str(port))
|
190
200
|
server.start()
|
191
201
|
if constants.DEBUGGING:
|
@@ -377,6 +387,9 @@ class Connector(connector_sdk_pb2_grpc.ConnectorServicer):
|
|
377
387
|
print_library_log(error_message, Logging.Level.SEVERE)
|
378
388
|
raise RuntimeError(error_message) from e
|
379
389
|
|
390
|
+
def print_version():
|
391
|
+
print_library_log("fivetran_connector_sdk " + __version__)
|
392
|
+
sys.exit(0)
|
380
393
|
|
381
394
|
def main():
|
382
395
|
"""The main entry point for the script.
|
@@ -389,7 +402,8 @@ def main():
|
|
389
402
|
parser._option_string_actions["-h"].help = "Show this help message and exit"
|
390
403
|
|
391
404
|
# Positional
|
392
|
-
parser.add_argument("
|
405
|
+
parser.add_argument("--version", action="store_true", help="Print the version of the fivetran_connector_sdk and exit")
|
406
|
+
parser.add_argument("command", nargs="?", help="|".join(VALID_COMMANDS))
|
393
407
|
parser.add_argument("project_path", nargs='?', default=os.getcwd(), help="Path to connector project directory")
|
394
408
|
|
395
409
|
# Optional (Not all of these are valid with every mutually exclusive option below)
|
@@ -401,52 +415,41 @@ def main():
|
|
401
415
|
parser.add_argument("-f", "--force", action="store_true", help="Force update an existing connection")
|
402
416
|
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
417
|
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
418
|
args = parser.parse_args()
|
406
419
|
|
420
|
+
if args.version:
|
421
|
+
print_version()
|
422
|
+
|
423
|
+
if not args.command:
|
424
|
+
parser.print_help()
|
425
|
+
sys.exit(1)
|
426
|
+
|
407
427
|
if args.command.lower() == "version":
|
408
|
-
|
409
|
-
return
|
428
|
+
print_version()
|
410
429
|
elif args.command.lower() == "reset":
|
411
430
|
reset_local_file_directory(args)
|
412
|
-
|
431
|
+
sys.exit(0)
|
413
432
|
|
414
433
|
connector_object = find_connector_object(args.project_path)
|
415
434
|
|
416
435
|
if not connector_object:
|
417
436
|
sys.exit(1)
|
418
437
|
|
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
438
|
if args.command.lower() == "deploy":
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
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)
|
439
|
+
ft_group = get_destination_group(args)
|
440
|
+
ft_connection = get_connection_name(args)
|
441
|
+
ft_deploy_key = get_api_key(args)
|
442
|
+
python_version = get_python_version(args)
|
443
|
+
hd_agent_id = get_hd_agent_id(args)
|
444
|
+
configuration, config_path = get_configuration(args)
|
445
|
+
get_state(args)
|
446
446
|
|
447
|
-
connector_object.deploy(args, ft_deploy_key, ft_group, ft_connection, hd_agent_id,
|
447
|
+
connector_object.deploy(args.project_path, ft_deploy_key, ft_group, ft_connection, hd_agent_id,
|
448
|
+
configuration, config_path, python_version, args.force)
|
448
449
|
|
449
450
|
elif args.command.lower() == "debug":
|
451
|
+
configuration, config_path = get_configuration(args)
|
452
|
+
state = get_state(args)
|
450
453
|
connector_object.debug(args.project_path, configuration, state)
|
451
454
|
else:
|
452
455
|
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
|
-
|
23
|
-
|
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
|
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
|
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.
|
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.
|
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"
|
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
|
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 (
|
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 = {}
|
@@ -314,7 +389,7 @@ def validate_requirements_file(project_path: str, is_deploy: bool, version: str
|
|
314
389
|
print(version_mismatch_deps)
|
315
390
|
if is_deploy and not force:
|
316
391
|
confirm = input(
|
317
|
-
f"Would you like us to update {REQUIREMENTS_TXT} to the current stable versions of the dependent libraries? (
|
392
|
+
f"Would you like us to update {REQUIREMENTS_TXT} to the current stable versions of the dependent libraries? (y/N):")
|
318
393
|
if confirm.lower() == "y":
|
319
394
|
update_version_requirements = True
|
320
395
|
for requirement in version_mismatch_deps:
|
@@ -329,7 +404,7 @@ def validate_requirements_file(project_path: str, is_deploy: bool, version: str
|
|
329
404
|
handle_missing_deps(missing_deps)
|
330
405
|
if is_deploy and not force:
|
331
406
|
confirm = input(
|
332
|
-
f"Would you like us to update {REQUIREMENTS_TXT} to add missing dependent libraries? (
|
407
|
+
f"Would you like us to update {REQUIREMENTS_TXT} to add missing dependent libraries? (y/N):")
|
333
408
|
if confirm.lower() == "n":
|
334
409
|
print_library_log(f"Changes identified as missing dependencies for libraries have been ignored. These changes have NOT been made to {REQUIREMENTS_TXT}.")
|
335
410
|
elif confirm.lower() == "y":
|
@@ -342,11 +417,11 @@ def validate_requirements_file(project_path: str, is_deploy: bool, version: str
|
|
342
417
|
if unused_deps:
|
343
418
|
handle_unused_deps(unused_deps, version)
|
344
419
|
if is_deploy and not force:
|
345
|
-
confirm = input(f"Would you like us to update {REQUIREMENTS_TXT} to remove the unused libraries? (
|
420
|
+
confirm = input(f"Would you like us to update {REQUIREMENTS_TXT} to remove the unused libraries? (y/N):")
|
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"
|
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.
|
454
|
+
log_unused_deps_error("requests", "2.32.4")
|
380
455
|
print_library_log("The following dependencies are not needed, "
|
381
|
-
f"they are
|
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"
|
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,
|
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(
|
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
|
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("
|
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("
|
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.
|
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}.
|
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"
|
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,17 +954,27 @@ def process_columns(columns, entry):
|
|
879
954
|
|
880
955
|
elif isinstance(type, dict):
|
881
956
|
if type['type'].upper() != "DECIMAL":
|
882
|
-
|
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
|
-
column.decimal.precision = type['precision']
|
885
|
-
column.decimal.scale = type['scale']
|
966
|
+
column.params.decimal.precision = type['precision']
|
967
|
+
column.params.decimal.scale = type['scale']
|
886
968
|
|
887
969
|
else:
|
888
|
-
raise ValueError(
|
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
|
892
976
|
|
977
|
+
|
893
978
|
columns[column_name] = column
|
894
979
|
|
895
980
|
def process_data_type(column, type):
|
@@ -902,11 +987,21 @@ def process_data_type(column, type):
|
|
902
987
|
elif type.upper() == "LONG":
|
903
988
|
column.type = common_pb2.DataType.LONG
|
904
989
|
elif type.upper() == "DECIMAL":
|
905
|
-
raise ValueError(
|
990
|
+
raise ValueError(
|
991
|
+
"DECIMAL data type missing precision and scale. "
|
992
|
+
"Use a dictionary for DECIMAL column type like: "
|
993
|
+
'''"col_name": { # Decimal data type with precision and scale.\n'''
|
994
|
+
''' "type": "DECIMAL",\n'''
|
995
|
+
''' "precision": 15,\n'''
|
996
|
+
''' "scale": 2\n'''
|
997
|
+
'''}'''
|
998
|
+
)
|
906
999
|
elif type.upper() == "FLOAT":
|
907
1000
|
column.type = common_pb2.DataType.FLOAT
|
908
1001
|
elif type.upper() == "DOUBLE":
|
909
1002
|
column.type = common_pb2.DataType.DOUBLE
|
1003
|
+
elif type.upper() == "NAIVE_TIME":
|
1004
|
+
column.type = common_pb2.DataType.NAIVE_TIME
|
910
1005
|
elif type.upper() == "NAIVE_DATE":
|
911
1006
|
column.type = common_pb2.DataType.NAIVE_DATE
|
912
1007
|
elif type.upper() == "NAIVE_DATETIME":
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import re
|
2
2
|
|
3
|
-
TESTER_VER = "
|
3
|
+
TESTER_VER = "2.25.0701.001"
|
4
4
|
|
5
5
|
WIN_OS = "windows"
|
6
6
|
ARM_64 = "arm64"
|
@@ -24,7 +24,7 @@ DEBUGGING = False
|
|
24
24
|
EXECUTED_VIA_CLI = False
|
25
25
|
TABLES = {}
|
26
26
|
|
27
|
-
TESTER_FILENAME = "
|
27
|
+
TESTER_FILENAME = "sdk_connector_tester.jar"
|
28
28
|
VERSION_FILENAME = "version.txt"
|
29
29
|
UPLOAD_FILENAME = "code.zip"
|
30
30
|
LAST_VERSION_CHECK_FILE = "_last_version_check"
|