fivetran-connector-sdk 2.2.1__py3-none-any.whl → 2.3.0__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.
@@ -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.2.1"
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
- confirm = 'y'
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
- if isinstance(node.func, ast.Name) and node.func.id == "exit":
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 os.path.isdir(path):
703
- if (name not in EXCLUDED_DIRS) and (not name.startswith(".")):
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 evaluate(local_path: str, deploy_key: str):
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
- print_library_log(f"Unable to upload the project, failed with error: {response.reason}", Logging.Level.SEVERE)
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 = 30 # seconds
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
- if _LOG_DATA_TYPE_INFERENCE.get("boolean_" + table, True):
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.2.1
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
@@ -1,10 +1,10 @@
1
- fivetran_connector_sdk/__init__.py,sha256=TcTrOpMTlZbknXNfmkYXghZpKmhB5QORP7xiQEP-EMg,23809
2
- fivetran_connector_sdk/connector_helper.py,sha256=BtiFeLZCDqYBeQlO1IY2R94MbcBgR3-Omg0CHgCeZD0,45186
3
- fivetran_connector_sdk/constants.py,sha256=0btZyHUTmoMdrGuIYoWiS-9kLY8pOy5N1jlzxdobj2Q,2476
1
+ fivetran_connector_sdk/__init__.py,sha256=zp6iAJh22DP3s-1v7EF3K0SAnrWjVx1Zz_-FxcHZKHs,23809
2
+ fivetran_connector_sdk/connector_helper.py,sha256=UMI743HxfVKJMkQhDD2QNFfqOqicqJ6BqyqL6UjcDuo,48462
3
+ fivetran_connector_sdk/constants.py,sha256=4fVdH9BaJAPIHpEe1crYn3Cz1TFJ0GJIyfgHG8tw9UA,2564
4
4
  fivetran_connector_sdk/helpers.py,sha256=7YVB1JQ9T0hg90Z0pjJxFp0pQzeBfefrfvS4SYtrlv4,15254
5
5
  fivetran_connector_sdk/logger.py,sha256=ud8v8-mKx65OAPaZvxBqt2-CU0vjgBeiYwuiqsYh_hA,3063
6
6
  fivetran_connector_sdk/operation_stream.py,sha256=DXLDv961xZ_GVSEPUFLtZy0IEf_ayQSEXFpEJp-CAu4,6194
7
- fivetran_connector_sdk/operations.py,sha256=1NZjgP_DGLh6103aki2OHfeloPhXfLwtRaN7NIMRQbE,12300
7
+ fivetran_connector_sdk/operations.py,sha256=ce43mOBYE2IYxuc8oaG-PXSxjY-iGKMhYWN3bBHUq18,12416
8
8
  fivetran_connector_sdk/protos/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  fivetran_connector_sdk/protos/common_pb2.py,sha256=zkzs6Rd-lvsev6Nsq37xc4HLJZ_uNXPkotCLY7Y7i5U,8770
10
10
  fivetran_connector_sdk/protos/common_pb2.pyi,sha256=FdqlPKRqiXdUDT3e7adP5X42_Qzv_ItydUNJFKnJJIE,11478
@@ -12,8 +12,8 @@ fivetran_connector_sdk/protos/common_pb2_grpc.py,sha256=qni6h6BoA1nwJXr2bNtznfTk
12
12
  fivetran_connector_sdk/protos/connector_sdk_pb2.py,sha256=Inv87MlK5Q56GNvMNFQHyqIePDMKnkW9y_BrT9DgPck,7835
13
13
  fivetran_connector_sdk/protos/connector_sdk_pb2.pyi,sha256=3AC-bK6ZM-Bmr_RETOB3y_0u4ATWlwcbHzqVanDuOB0,8115
14
14
  fivetran_connector_sdk/protos/connector_sdk_pb2_grpc.py,sha256=bGlvc_vGwA9-FTqrj-BYlVcA-7jS8A9MSZ-XpZFytvY,8795
15
- fivetran_connector_sdk-2.2.1.dist-info/METADATA,sha256=2ryTaDn61Yvf1pGev8f2fA-fCGi4hmw_lm4cmdblEpU,3197
16
- fivetran_connector_sdk-2.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
- fivetran_connector_sdk-2.2.1.dist-info/entry_points.txt,sha256=uQn0KPnFlQmXJfxlk0tifdNsSXWfVlnAFzNqjXZM_xM,57
18
- fivetran_connector_sdk-2.2.1.dist-info/top_level.txt,sha256=-_xk2MFY4psIh7jw1lJePMzFb5-vask8_ZtX-UzYWUI,23
19
- fivetran_connector_sdk-2.2.1.dist-info/RECORD,,
15
+ fivetran_connector_sdk-2.3.0.dist-info/METADATA,sha256=UuKyXswGsJPKY7FOLdALqiLKFn6wHLgUm8zyi7FjljA,3197
16
+ fivetran_connector_sdk-2.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
+ fivetran_connector_sdk-2.3.0.dist-info/entry_points.txt,sha256=uQn0KPnFlQmXJfxlk0tifdNsSXWfVlnAFzNqjXZM_xM,57
18
+ fivetran_connector_sdk-2.3.0.dist-info/top_level.txt,sha256=-_xk2MFY4psIh7jw1lJePMzFb5-vask8_ZtX-UzYWUI,23
19
+ fivetran_connector_sdk-2.3.0.dist-info/RECORD,,