fivetran-connector-sdk 2.2.1__tar.gz → 2.3.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.
- {fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/PKG-INFO +1 -1
- {fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/src/fivetran_connector_sdk/__init__.py +1 -1
- {fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/src/fivetran_connector_sdk/connector_helper.py +123 -29
- {fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/src/fivetran_connector_sdk/constants.py +3 -1
- {fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/src/fivetran_connector_sdk/operations.py +10 -6
- {fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/src/fivetran_connector_sdk.egg-info/PKG-INFO +1 -1
- {fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/README.md +0 -0
- {fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/pyproject.toml +0 -0
- {fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/setup.cfg +0 -0
- {fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/src/fivetran_connector_sdk/helpers.py +0 -0
- {fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/src/fivetran_connector_sdk/logger.py +0 -0
- {fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/src/fivetran_connector_sdk/operation_stream.py +0 -0
- {fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/src/fivetran_connector_sdk/protos/__init__.py +0 -0
- {fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/src/fivetran_connector_sdk/protos/common_pb2.py +0 -0
- {fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/src/fivetran_connector_sdk/protos/common_pb2.pyi +0 -0
- {fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/src/fivetran_connector_sdk/protos/common_pb2_grpc.py +0 -0
- {fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/src/fivetran_connector_sdk/protos/connector_sdk_pb2.py +0 -0
- {fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/src/fivetran_connector_sdk/protos/connector_sdk_pb2.pyi +0 -0
- {fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/src/fivetran_connector_sdk/protos/connector_sdk_pb2_grpc.py +0 -0
- {fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/src/fivetran_connector_sdk.egg-info/SOURCES.txt +0 -0
- {fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/src/fivetran_connector_sdk.egg-info/dependency_links.txt +0 -0
- {fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/src/fivetran_connector_sdk.egg-info/entry_points.txt +0 -0
- {fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/src/fivetran_connector_sdk.egg-info/requires.txt +0 -0
- {fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.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: 2.
|
3
|
+
Version: 2.3.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
|
{fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/src/fivetran_connector_sdk/__init__.py
RENAMED
@@ -44,7 +44,7 @@ from fivetran_connector_sdk.connector_helper import (
|
|
44
44
|
|
45
45
|
# Version format: <major_version>.<minor_version>.<patch_version>
|
46
46
|
# (where Major Version = 2, Minor Version is incremental MM from Aug 25 onwards, Patch Version is incremental within a month)
|
47
|
-
__version__ = "2.
|
47
|
+
__version__ = "2.3.0"
|
48
48
|
TESTER_VERSION = TESTER_VER
|
49
49
|
MAX_MESSAGE_LENGTH = 32 * 1024 * 1024 # 32MB
|
50
50
|
|
@@ -36,6 +36,7 @@ from fivetran_connector_sdk.constants import (
|
|
36
36
|
CONFIG_FILE,
|
37
37
|
OUTPUT_FILES_DIR,
|
38
38
|
REQUIREMENTS_TXT,
|
39
|
+
CONFIGURATION_JSON,
|
39
40
|
PYPI_PACKAGE_DETAILS_URL,
|
40
41
|
ONE_DAY_IN_SEC,
|
41
42
|
MAX_RETRIES,
|
@@ -49,6 +50,7 @@ from fivetran_connector_sdk.constants import (
|
|
49
50
|
UTF_8,
|
50
51
|
CONNECTION_SCHEMA_NAME_PATTERN,
|
51
52
|
TABLES,
|
53
|
+
EVALUATION_MARKDOWN,
|
52
54
|
)
|
53
55
|
|
54
56
|
def get_destination_group(args):
|
@@ -114,21 +116,7 @@ def get_configuration(args, retrying = 0):
|
|
114
116
|
env_configuration = os.getenv('FIVETRAN_CONFIGURATION', None)
|
115
117
|
try:
|
116
118
|
if not configuration and not args.force and args.command.lower() == "deploy":
|
117
|
-
|
118
|
-
if not retrying:
|
119
|
-
json_filepath = os.path.join(args.project_path, "configuration.json")
|
120
|
-
if os.path.exists(json_filepath):
|
121
|
-
print_library_log("configuration.json file detected in the project, "
|
122
|
-
"but no configuration input provided via the command line", Logging.Level.WARNING)
|
123
|
-
env_configuration = env_configuration if env_configuration else "configuration.json"
|
124
|
-
confirm = input(f"Does this debug run/deploy need configuration (y/N):")
|
125
|
-
if confirm.lower()=='y':
|
126
|
-
configuration = get_input_from_cli("Provide the configuration file path", env_configuration)
|
127
|
-
config_values = validate_and_load_configuration(args.project_path, configuration)
|
128
|
-
return config_values, configuration
|
129
|
-
else:
|
130
|
-
print_library_log("No input required for configuration. Continuing without configuration.", Logging.Level.INFO)
|
131
|
-
return {}, None
|
119
|
+
return _deploy_config_flow(args, env_configuration, retrying)
|
132
120
|
config_values = validate_and_load_configuration(args.project_path, configuration)
|
133
121
|
return config_values, configuration
|
134
122
|
except ValueError as e:
|
@@ -180,11 +168,43 @@ def tester_root_dir_helper() -> str:
|
|
180
168
|
"""Returns the root directory for the tester."""
|
181
169
|
return os.path.join(os.path.expanduser("~"), ROOT_LOCATION)
|
182
170
|
|
171
|
+
|
172
|
+
def _deploy_config_flow(args, env_configuration, retrying):
|
173
|
+
"""Handles the configuration flow for the deploy command."""
|
174
|
+
confirm = 'y'
|
175
|
+
if not retrying:
|
176
|
+
json_filepath = os.path.join(args.project_path, CONFIGURATION_JSON)
|
177
|
+
if os.path.exists(json_filepath):
|
178
|
+
print_library_log("configuration.json file detected in the project, "
|
179
|
+
"but no configuration input provided via the command line", Logging.Level.WARNING)
|
180
|
+
env_configuration = env_configuration if env_configuration else CONFIGURATION_JSON
|
181
|
+
confirm = input("Does this debug run/deploy need configuration (y/N):")
|
182
|
+
if confirm.lower() == 'y':
|
183
|
+
configuration = get_input_from_cli("Provide the configuration file path", env_configuration)
|
184
|
+
config_values = validate_and_load_configuration(args.project_path, configuration)
|
185
|
+
return config_values, configuration
|
186
|
+
else:
|
187
|
+
print_library_log("No input required for configuration. Continuing without configuration.", Logging.Level.INFO)
|
188
|
+
return {}, None
|
189
|
+
|
190
|
+
|
183
191
|
def _warn_exit_usage(filename, line_no, func):
|
184
192
|
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 " +
|
185
193
|
f"at: {filename}:{line_no}. See the Technical Reference for details: https://fivetran.com/docs/connector-sdk/technical-reference#handlingexceptions",
|
186
194
|
Logging.Level.WARNING)
|
187
195
|
|
196
|
+
|
197
|
+
def _check_and_warn_attribute_exit(node):
|
198
|
+
"""Checks for the presence of 'exit()' in the AST node and warns if found."""
|
199
|
+
if isinstance(node.func, ast.Name) and node.func.id == "exit":
|
200
|
+
_warn_exit_usage(ROOT_FILENAME, node.lineno, "exit()")
|
201
|
+
elif isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name):
|
202
|
+
if node.func.attr == "_exit" and node.func.value.id == "os":
|
203
|
+
_warn_exit_usage(ROOT_FILENAME, node.lineno, "os._exit()")
|
204
|
+
if node.func.attr == "exit" and node.func.value.id == "sys":
|
205
|
+
_warn_exit_usage(ROOT_FILENAME, node.lineno, "sys.exit()")
|
206
|
+
|
207
|
+
|
188
208
|
def exit_check(project_path):
|
189
209
|
"""Checks for the presence of 'exit()' in the calling code.
|
190
210
|
Args:
|
@@ -200,13 +220,7 @@ def exit_check(project_path):
|
|
200
220
|
tree = ast.parse(f.read())
|
201
221
|
for node in ast.walk(tree):
|
202
222
|
if isinstance(node, ast.Call):
|
203
|
-
|
204
|
-
_warn_exit_usage(ROOT_FILENAME, node.lineno, "exit()")
|
205
|
-
elif isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name):
|
206
|
-
if node.func.attr == "_exit" and node.func.value.id == "os":
|
207
|
-
_warn_exit_usage(ROOT_FILENAME, node.lineno, "os._exit()")
|
208
|
-
if node.func.attr == "exit" and node.func.value.id == "sys":
|
209
|
-
_warn_exit_usage(ROOT_FILENAME, node.lineno, "sys.exit()")
|
223
|
+
_check_and_warn_attribute_exit(node)
|
210
224
|
except SyntaxError as e:
|
211
225
|
print_library_log(f"SyntaxError in {ROOT_FILENAME}: {e}", Logging.Level.SEVERE)
|
212
226
|
|
@@ -459,6 +473,7 @@ def load_or_add_requirements_file(requirements_file_path):
|
|
459
473
|
requirements = fetch_requirements_as_dict(requirements_file_path)
|
460
474
|
else:
|
461
475
|
with open(requirements_file_path, 'w', encoding=UTF_8):
|
476
|
+
# Intentional empty block: Creating an empty requirements.txt file
|
462
477
|
pass
|
463
478
|
requirements = {}
|
464
479
|
print_library_log("`requirements.txt` file not found in your project folder.", Logging.Level.WARNING)
|
@@ -481,7 +496,7 @@ def evaluate_project(project_path: str, deploy_key: str):
|
|
481
496
|
print_library_log(f"Evaluating '{project_path}'...")
|
482
497
|
upload_file_path = create_upload_file(project_path)
|
483
498
|
try:
|
484
|
-
evaluation_report = evaluate(upload_file_path, deploy_key)
|
499
|
+
evaluation_report = evaluate(upload_file_path, project_path, deploy_key)
|
485
500
|
if not evaluation_report:
|
486
501
|
print_library_log(
|
487
502
|
"Project evaluation failed. No evaluation report was generated. Please check your project for errors and try again.",
|
@@ -686,6 +701,15 @@ def zip_folder(project_path: str) -> str:
|
|
686
701
|
|
687
702
|
return upload_filepath
|
688
703
|
|
704
|
+
def _should_descend_into_dir(name, path):
|
705
|
+
"""Determines whether to traverse into a subdirectory."""
|
706
|
+
if not os.path.isdir(path):
|
707
|
+
return False
|
708
|
+
if name in EXCLUDED_DIRS or name.startswith("."):
|
709
|
+
return False
|
710
|
+
if VIRTUAL_ENV_CONFIG in os.listdir(path): # Check for virtual env indicator
|
711
|
+
return False
|
712
|
+
return True
|
689
713
|
|
690
714
|
def dir_walker(top):
|
691
715
|
"""Walks the directory tree starting at the given top directory.
|
@@ -699,10 +723,8 @@ def dir_walker(top):
|
|
699
723
|
dirs, files = [], []
|
700
724
|
for name in os.listdir(top):
|
701
725
|
path = os.path.join(top, name)
|
702
|
-
if
|
703
|
-
|
704
|
-
if VIRTUAL_ENV_CONFIG not in os.listdir(path): # Check for virtual env indicator
|
705
|
-
dirs.append(name)
|
726
|
+
if _should_descend_into_dir(name, path):
|
727
|
+
dirs.append(name)
|
706
728
|
else:
|
707
729
|
# Include all files if in `drivers` folder
|
708
730
|
if os.path.basename(top) == DRIVERS:
|
@@ -717,7 +739,76 @@ def dir_walker(top):
|
|
717
739
|
yield x
|
718
740
|
|
719
741
|
|
720
|
-
def
|
742
|
+
def render_section(lines, title, items):
|
743
|
+
"""
|
744
|
+
Renders a section of the markdown report.
|
745
|
+
Args:
|
746
|
+
lines (list): The list of lines to append to.
|
747
|
+
title (str): The title of the section.
|
748
|
+
items (list): The list of items in the section.
|
749
|
+
"""
|
750
|
+
lines.extend([f"## {title}", ""])
|
751
|
+
if not items:
|
752
|
+
lines.extend(["_No recommendations._", ""])
|
753
|
+
return
|
754
|
+
for index, item in enumerate(items, 1):
|
755
|
+
issue = item.get("issue", "(missing issue)")
|
756
|
+
lines.append(f"### {index}. {issue}")
|
757
|
+
if recommendation := item.get("recommendation"):
|
758
|
+
lines.append(f"- **Recommendation:** {recommendation}")
|
759
|
+
if current_code := item.get("current_code"):
|
760
|
+
fenced_code = f"```python\n{current_code.rstrip()}\n```"
|
761
|
+
lines += ["\n**Current Code:**", fenced_code]
|
762
|
+
if fix := item.get("code_fix"):
|
763
|
+
fenced_code = f"```python\n{fix.rstrip()}\n```"
|
764
|
+
lines += ["\n**Code Fix:**", fenced_code]
|
765
|
+
lines.append("")
|
766
|
+
|
767
|
+
|
768
|
+
def render_markdown(report):
|
769
|
+
"""
|
770
|
+
Renders the evaluation report as markdown.
|
771
|
+
Args:
|
772
|
+
report (dict): The evaluation report.
|
773
|
+
"""
|
774
|
+
lines = []
|
775
|
+
lines += ["# Evaluation Report", ""]
|
776
|
+
overall_score = report.get("score")
|
777
|
+
if overall_score:
|
778
|
+
lines += [f"**Overall Score:** {overall_score}", ""]
|
779
|
+
|
780
|
+
subscore = report.get("subscores") or {}
|
781
|
+
if subscore:
|
782
|
+
lines += ["## Subscores", "", "| Metric | Score |", "|---|---:|"]
|
783
|
+
lines += [f"| {key} | {value} |" for key, value in subscore.items()]
|
784
|
+
lines.append("")
|
785
|
+
|
786
|
+
render_section(lines, "Required", report.get("required"))
|
787
|
+
render_section(lines, "Good to Have", report.get("good_to_have"))
|
788
|
+
|
789
|
+
return "\n".join(lines)
|
790
|
+
|
791
|
+
|
792
|
+
def save_evaluation_report(evaluation_report, project_path: str):
|
793
|
+
"""Saves the evaluation report to a file in the project path.
|
794
|
+
Args:
|
795
|
+
evaluation_report: The evaluation data.
|
796
|
+
project_path: The path to the project.
|
797
|
+
"""
|
798
|
+
# Save evaluation report
|
799
|
+
working_dir = os.path.join(project_path, OUTPUT_FILES_DIR)
|
800
|
+
os.makedirs(working_dir, exist_ok=True)
|
801
|
+
report_path = os.path.join(working_dir, EVALUATION_MARKDOWN)
|
802
|
+
markdown_content = render_markdown(evaluation_report)
|
803
|
+
try:
|
804
|
+
with open(report_path, 'w', encoding=UTF_8) as report_file:
|
805
|
+
report_file.write(markdown_content)
|
806
|
+
print_library_log(f"Evaluation report saved to {report_path}")
|
807
|
+
except Exception as e:
|
808
|
+
print_library_log(f"Failed to save evaluation report: {e}", Logging.Level.SEVERE)
|
809
|
+
|
810
|
+
|
811
|
+
def evaluate(local_path: str, project_path:str, deploy_key: str):
|
721
812
|
"""Uploads the local code file for evaluation and returns the evaluation report.
|
722
813
|
|
723
814
|
The server responds with a file containing JSON. This function streams the
|
@@ -725,6 +816,7 @@ def evaluate(local_path: str, deploy_key: str):
|
|
725
816
|
|
726
817
|
Args:
|
727
818
|
local_path (str): The local file path.
|
819
|
+
project_path (str): The path to the project.
|
728
820
|
deploy_key (str): The deployment key.
|
729
821
|
|
730
822
|
Returns:
|
@@ -751,6 +843,7 @@ def evaluate(local_path: str, deploy_key: str):
|
|
751
843
|
|
752
844
|
print_library_log("✓ Evaluation Report:")
|
753
845
|
print(json.dumps(evaluation_data, indent=4))
|
846
|
+
save_evaluation_report(evaluation_data, project_path)
|
754
847
|
|
755
848
|
return evaluation_data
|
756
849
|
|
@@ -785,7 +878,8 @@ def upload(local_path: str, deploy_key: str, group_id: str, connection: str) ->
|
|
785
878
|
print("✓")
|
786
879
|
return True
|
787
880
|
|
788
|
-
|
881
|
+
error_message = response.reason + ": " + json.loads(response.text).get("message", "")
|
882
|
+
print_library_log(f"Unable to upload the project, failed with error. {error_message}", Logging.Level.SEVERE)
|
789
883
|
return False
|
790
884
|
|
791
885
|
def cleanup_uploaded_code(deploy_key: str, group_id: str, connection: str) -> bool:
|
@@ -33,9 +33,11 @@ ROOT_LOCATION = ".ft_sdk_connector_tester"
|
|
33
33
|
CONFIG_FILE = "_config.json"
|
34
34
|
OUTPUT_FILES_DIR = "files"
|
35
35
|
REQUIREMENTS_TXT = "requirements.txt"
|
36
|
+
EVALUATION_MARKDOWN = "evaluation_report.md"
|
37
|
+
CONFIGURATION_JSON = "configuration.json"
|
36
38
|
PYPI_PACKAGE_DETAILS_URL = "https://pypi.org/pypi/fivetran_connector_sdk/json"
|
37
39
|
ONE_DAY_IN_SEC = 24 * 60 * 60
|
38
|
-
CHECKPOINT_OP_TIMEOUT_IN_SEC =
|
40
|
+
CHECKPOINT_OP_TIMEOUT_IN_SEC = 120 # seconds
|
39
41
|
MAX_RETRIES = 3
|
40
42
|
LOGGING_PREFIX = "Fivetran-Connector-SDK"
|
41
43
|
LOGGING_DELIMITER = ": "
|
@@ -150,7 +150,6 @@ def _get_table_pk(table: str) -> bool:
|
|
150
150
|
Returns:
|
151
151
|
dict: The columns for the table.
|
152
152
|
"""
|
153
|
-
columns = {}
|
154
153
|
if table in TABLES:
|
155
154
|
for column in TABLES[table].columns:
|
156
155
|
if column.primary_key:
|
@@ -179,15 +178,20 @@ def _map_data_to_columns(data: dict, columns: dict, table: str = "") -> dict:
|
|
179
178
|
map_inferred_data_type(key, mapped_data, v, table)
|
180
179
|
return mapped_data
|
181
180
|
|
181
|
+
def _log_boolean_inference_once(table):
|
182
|
+
"""Log boolean inference once per table and mark it as logged."""
|
183
|
+
key = f"boolean_{table}"
|
184
|
+
if _LOG_DATA_TYPE_INFERENCE.get(key, True):
|
185
|
+
print_library_log("Fivetran: Boolean Datatype has been inferred for " + table, Logging.Level.INFO, True)
|
186
|
+
if not _get_table_pk(table):
|
187
|
+
print_library_log("Fivetran: Boolean Datatype inference issue for " + table, Logging.Level.INFO, True)
|
188
|
+
_LOG_DATA_TYPE_INFERENCE[key] = False
|
189
|
+
|
182
190
|
def map_inferred_data_type(k, mapped_data, v, table=""):
|
183
191
|
# We can infer type from the value
|
184
192
|
if isinstance(v, bool):
|
185
193
|
mapped_data[k] = common_pb2.ValueType(bool=v)
|
186
|
-
|
187
|
-
print_library_log("Fivetran: Boolean Datatype has been inferred for " + table, Logging.Level.INFO, True)
|
188
|
-
if not _get_table_pk(table):
|
189
|
-
print_library_log("Fivetran: Boolean Datatype inference issue for " + table, Logging.Level.INFO, True)
|
190
|
-
_LOG_DATA_TYPE_INFERENCE["boolean_" + table] = False
|
194
|
+
_log_boolean_inference_once(table)
|
191
195
|
elif isinstance(v, int):
|
192
196
|
if abs(v) > JAVA_LONG_MAX_VALUE:
|
193
197
|
mapped_data[k] = common_pb2.ValueType(float=v)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: fivetran_connector_sdk
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.3.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
|
File without changes
|
File without changes
|
File without changes
|
{fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/src/fivetran_connector_sdk/helpers.py
RENAMED
File without changes
|
{fivetran_connector_sdk-2.2.1 → fivetran_connector_sdk-2.3.0}/src/fivetran_connector_sdk/logger.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|