fivetran-connector-sdk 1.7.5__tar.gz → 1.8.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. {fivetran_connector_sdk-1.7.5 → fivetran_connector_sdk-1.8.1}/PKG-INFO +1 -1
  2. {fivetran_connector_sdk-1.7.5 → fivetran_connector_sdk-1.8.1}/src/fivetran_connector_sdk/__init__.py +11 -2
  3. {fivetran_connector_sdk-1.7.5 → fivetran_connector_sdk-1.8.1}/src/fivetran_connector_sdk/connector_helper.py +39 -42
  4. {fivetran_connector_sdk-1.7.5 → fivetran_connector_sdk-1.8.1}/src/fivetran_connector_sdk/constants.py +7 -1
  5. {fivetran_connector_sdk-1.7.5 → fivetran_connector_sdk-1.8.1}/src/fivetran_connector_sdk/operations.py +143 -41
  6. {fivetran_connector_sdk-1.7.5 → fivetran_connector_sdk-1.8.1}/src/fivetran_connector_sdk/protos/connector_sdk_pb2.py +14 -12
  7. {fivetran_connector_sdk-1.7.5 → fivetran_connector_sdk-1.8.1}/src/fivetran_connector_sdk/protos/connector_sdk_pb2.pyi +10 -2
  8. {fivetran_connector_sdk-1.7.5 → fivetran_connector_sdk-1.8.1}/src/fivetran_connector_sdk.egg-info/PKG-INFO +1 -1
  9. {fivetran_connector_sdk-1.7.5 → fivetran_connector_sdk-1.8.1}/README.md +0 -0
  10. {fivetran_connector_sdk-1.7.5 → fivetran_connector_sdk-1.8.1}/pyproject.toml +0 -0
  11. {fivetran_connector_sdk-1.7.5 → fivetran_connector_sdk-1.8.1}/setup.cfg +0 -0
  12. {fivetran_connector_sdk-1.7.5 → fivetran_connector_sdk-1.8.1}/src/fivetran_connector_sdk/helpers.py +0 -0
  13. {fivetran_connector_sdk-1.7.5 → fivetran_connector_sdk-1.8.1}/src/fivetran_connector_sdk/logger.py +0 -0
  14. {fivetran_connector_sdk-1.7.5 → fivetran_connector_sdk-1.8.1}/src/fivetran_connector_sdk/protos/__init__.py +0 -0
  15. {fivetran_connector_sdk-1.7.5 → fivetran_connector_sdk-1.8.1}/src/fivetran_connector_sdk/protos/common_pb2.py +0 -0
  16. {fivetran_connector_sdk-1.7.5 → fivetran_connector_sdk-1.8.1}/src/fivetran_connector_sdk/protos/common_pb2.pyi +0 -0
  17. {fivetran_connector_sdk-1.7.5 → fivetran_connector_sdk-1.8.1}/src/fivetran_connector_sdk/protos/common_pb2_grpc.py +0 -0
  18. {fivetran_connector_sdk-1.7.5 → fivetran_connector_sdk-1.8.1}/src/fivetran_connector_sdk/protos/connector_sdk_pb2_grpc.py +0 -0
  19. {fivetran_connector_sdk-1.7.5 → fivetran_connector_sdk-1.8.1}/src/fivetran_connector_sdk.egg-info/SOURCES.txt +0 -0
  20. {fivetran_connector_sdk-1.7.5 → fivetran_connector_sdk-1.8.1}/src/fivetran_connector_sdk.egg-info/dependency_links.txt +0 -0
  21. {fivetran_connector_sdk-1.7.5 → fivetran_connector_sdk-1.8.1}/src/fivetran_connector_sdk.egg-info/entry_points.txt +0 -0
  22. {fivetran_connector_sdk-1.7.5 → fivetran_connector_sdk-1.8.1}/src/fivetran_connector_sdk.egg-info/requires.txt +0 -0
  23. {fivetran_connector_sdk-1.7.5 → fivetran_connector_sdk-1.8.1}/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.7.5
3
+ Version: 1.8.1
4
4
  Summary: Build custom connectors on Fivetran platform
5
5
  Author-email: Fivetran <developers@fivetran.com>
6
6
  Project-URL: Homepage, https://fivetran.com/docs/connectors/connector-sdk
@@ -42,7 +42,7 @@ from fivetran_connector_sdk.connector_helper import (
42
42
 
43
43
  # Version format: <major_version>.<minor_version>.<patch_version>
44
44
  # (where Major Version = 1 for GA, Minor Version is incremental MM from Jan 25 onwards, Patch Version is incremental within a month)
45
- __version__ = "1.7.5"
45
+ __version__ = "1.8.1"
46
46
  TESTER_VERSION = TESTER_VER
47
47
  MAX_MESSAGE_LENGTH = 32 * 1024 * 1024 # 32MB
48
48
 
@@ -396,7 +396,16 @@ class Connector(connector_sdk_pb2_grpc.SourceConnectorServicer):
396
396
  thread = threading.Thread(target=run_update)
397
397
  thread.start()
398
398
 
399
- yield from Operations.operation_stream
399
+ # consumer - yield the operations in the operation_stream.
400
+ for response in Operations.operation_stream:
401
+ # checkpoint call always returns list of responses.
402
+ if isinstance(response, list):
403
+ for res in response:
404
+ yield res
405
+ # checkpoint call blocks the queue (see _OperationStream.add method). unblock the queue after yielding all responses.
406
+ Operations.operation_stream.unblock()
407
+ else:
408
+ yield response
400
409
 
401
410
  thread.join()
402
411
 
@@ -57,27 +57,22 @@ def get_destination_group(args):
57
57
  ft_group = get_input_from_cli("Provide the destination name (as displayed in your dashboard destination list)", env_destination_name)
58
58
  return ft_group
59
59
 
60
- def get_connection_name(args):
60
+ def get_connection_name(args, retrying=0):
61
61
  ft_connection = args.connection if args.connection else None
62
62
  env_connection_name = os.getenv('FIVETRAN_CONNECTION_NAME', None)
63
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
64
+ ft_connection = get_input_from_cli("Provide the connection name", env_connection_name)
76
65
  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)
66
+ print_library_log(
67
+ f"Connection name: {ft_connection} is invalid!\n The connection name should start with an "
68
+ f"underscore or a lowercase letter (a-z), followed by any combination of underscores, lowercase "
69
+ f"letters, or digits (0-9). Uppercase characters are not allowed.", Logging.Level.SEVERE)
70
+ args.connection = None
71
+ if retrying >= MAX_RETRIES or args.force:
72
+ sys.exit(1)
73
+ else:
74
+ print_library_log("Please retry...", Logging.Level.INFO)
75
+ return get_connection_name(args, retrying + 1)
81
76
  return ft_connection
82
77
 
83
78
  def get_api_key(args):
@@ -113,32 +108,36 @@ def get_state(args):
113
108
  state = validate_and_load_state(args, state)
114
109
  return state
115
110
 
116
- def get_configuration(args):
111
+ def get_configuration(args, retrying = 0):
117
112
  configuration = args.configuration if args.configuration else None
118
113
  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)
114
+ try:
115
+ if not configuration and not args.force and args.command.lower() == "deploy":
116
+ confirm = 'y'
117
+ if not retrying:
118
+ json_filepath = os.path.join(args.project_path, "configuration.json")
119
+ if os.path.exists(json_filepath):
120
+ print_library_log("configuration.json file detected in the project, "
121
+ "but no configuration input provided via the command line", Logging.Level.WARNING)
122
+ env_configuration = env_configuration if env_configuration else "configuration.json"
123
+ confirm = input(f"Does this debug run/deploy need configuration (y/N):")
124
+ if confirm.lower()=='y':
125
+ configuration = get_input_from_cli("Provide the configuration file path", env_configuration)
126
+ config_values = validate_and_load_configuration(args.project_path, configuration)
127
+ return config_values, configuration
128
+ else:
129
+ print_library_log("No input required for configuration. Continuing without configuration.", Logging.Level.INFO)
130
+ return {}, None
131
+ config_values = validate_and_load_configuration(args.project_path, configuration)
132
+ return config_values, configuration
133
+ except ValueError as e:
134
+ args.configuration = None
135
+ if retrying >= MAX_RETRIES or args.force:
136
+ print_library_log(f"{e}. Invalid Configuration, Exiting..", Logging.Level.WARNING)
137
+ sys.exit(1)
138
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
139
+ print_library_log(f"{e}. Please retry..", Logging.Level.INFO)
140
+ return get_configuration(args, retrying + 1)
142
141
 
143
142
 
144
143
  def check_newer_version(version: str):
@@ -989,8 +988,6 @@ def process_data_type(column, type):
989
988
  column.type = common_pb2.DataType.FLOAT
990
989
  elif type.upper() == "DOUBLE":
991
990
  column.type = common_pb2.DataType.DOUBLE
992
- elif type.upper() == "NAIVE_TIME":
993
- column.type = common_pb2.DataType.NAIVE_TIME
994
991
  elif type.upper() == "NAIVE_DATE":
995
992
  column.type = common_pb2.DataType.NAIVE_DATE
996
993
  elif type.upper() == "NAIVE_DATETIME":
@@ -1,6 +1,7 @@
1
+ import os
1
2
  import re
2
3
 
3
- TESTER_VER = "2.25.0701.001"
4
+ TESTER_VER = "2.25.0806.001"
4
5
 
5
6
  WIN_OS = "windows"
6
7
  ARM_64 = "arm64"
@@ -34,11 +35,16 @@ OUTPUT_FILES_DIR = "files"
34
35
  REQUIREMENTS_TXT = "requirements.txt"
35
36
  PYPI_PACKAGE_DETAILS_URL = "https://pypi.org/pypi/fivetran_connector_sdk/json"
36
37
  ONE_DAY_IN_SEC = 24 * 60 * 60
38
+ CHECKPOINT_OP_TIMEOUT_IN_SEC = 30 # seconds
37
39
  MAX_RETRIES = 3
38
40
  LOGGING_PREFIX = "Fivetran-Connector-SDK"
39
41
  LOGGING_DELIMITER = ": "
40
42
  VIRTUAL_ENV_CONFIG = "pyvenv.cfg"
41
43
  ROOT_FILENAME = "connector.py"
44
+ MAX_RECORDS_IN_BATCH = 100
45
+ MAX_BATCH_SIZE_IN_BYTES = 100000 # 100 KB
46
+ # The increased queue size shall be rolled out as Feature flag
47
+ QUEUE_SIZE = int(os.environ.get("QUEUE_SIZE", 1))
42
48
 
43
49
  # Compile patterns used in the implementation
44
50
  WORD_DASH_DOT_PATTERN = re.compile(r'^[\w.-]*$')
@@ -1,7 +1,7 @@
1
1
  import json
2
- import os
3
2
  import sys
4
3
  import queue
4
+ import threading
5
5
 
6
6
  from datetime import datetime
7
7
  from google.protobuf import timestamp_pb2
@@ -9,6 +9,10 @@ from google.protobuf import timestamp_pb2
9
9
  from fivetran_connector_sdk.constants import (
10
10
  JAVA_LONG_MAX_VALUE,
11
11
  TABLES,
12
+ MAX_RECORDS_IN_BATCH,
13
+ MAX_BATCH_SIZE_IN_BYTES,
14
+ QUEUE_SIZE,
15
+ CHECKPOINT_OP_TIMEOUT_IN_SEC
12
16
  )
13
17
  from fivetran_connector_sdk.helpers import (
14
18
  get_renamed_table_name,
@@ -38,8 +42,15 @@ class _OperationStream:
38
42
  """
39
43
  Initializes the operation stream with a queue and a sentinel object.
40
44
  """
41
- self._queue = queue.Queue(maxsize=1)
45
+ self._queue = queue.Queue(maxsize=QUEUE_SIZE)
42
46
  self._sentinel = object()
47
+ self._is_done = False
48
+ self._buffer = []
49
+ self._buffer_record_count = 0
50
+ self._buffer_size_bytes = 0
51
+ self._checkpoint_lock = threading.Lock()
52
+ self._checkpoint_flush_signal = threading.Event()
53
+ self._checkpoint_flush_signal.set()
43
54
 
44
55
  def __iter__(self):
45
56
  """
@@ -47,14 +58,38 @@ class _OperationStream:
47
58
  """
48
59
  return self
49
60
 
50
- def add(self, data):
61
+ def add(self, operation):
51
62
  """
52
- Adds an item to the stream.
63
+ Adds an operation to the stream. Guarantees that operations within a single thread are processed in the order.
64
+
65
+ In multithreaded environment if a thread initiates a checkpoint, it's producer is blocked until the
66
+ checkpoint flush is complete. This block is localized, other threads
67
+ remain unblocked and can continue to perform other operations
68
+ (such as upserts, updates, deletes), but they are prevented from initiating a new checkpoint
69
+ until the existing one is finished.
53
70
 
54
71
  Args:
55
- data (object): The data item to add to the stream.
72
+ operation (object): The data item to add to the stream.
73
+ """
74
+ if isinstance(operation, connector_sdk_pb2.Checkpoint):
75
+ # lock to ensure checkpoint operations are processed one at a time
76
+ with self._checkpoint_lock:
77
+ # clear the signal to indicate checkpoint operation is being processed.
78
+ self._checkpoint_flush_signal.clear()
79
+ self._queue.put(operation)
80
+ # wait until the consumer flushes the buffer and sets the flag.
81
+ if not self._checkpoint_flush_signal.wait(CHECKPOINT_OP_TIMEOUT_IN_SEC):
82
+ raise TimeoutError(
83
+ "Checkpoint flush timed out. Consumer may have failed to process checkpoint."
84
+ )
85
+ else:
86
+ self._queue.put(operation)
87
+
88
+ def unblock(self):
89
+ """
90
+ Unblocks the queue, called by consumer after the checkpoint flush is completed.
56
91
  """
57
- self._queue.put(data)
92
+ self._checkpoint_flush_signal.set()
58
93
 
59
94
  def mark_done(self):
60
95
  """
@@ -72,16 +107,78 @@ class _OperationStream:
72
107
  Raises:
73
108
  StopIteration: If the sentinel object is encountered.
74
109
  """
75
- item = self._queue.get()
76
- if item is self._sentinel:
110
+ # If stream is completed and buffer is empty, raise StopIteration. Else flush the buffer.
111
+ if self._is_done and not self._buffer:
77
112
  raise StopIteration
78
- return item
79
113
 
80
- _LOG_DATA_TYPE_INFERENCE = {
81
- "boolean": True,
82
- "binary": True,
83
- "json": True
84
- }
114
+ if self._is_done:
115
+ return self._flush_buffer()
116
+
117
+ return self._build_next_batch()
118
+
119
+ def _build_next_batch(self):
120
+ """
121
+ Core logic to build the batch. The loop continues until the buffer is full,
122
+ but can be interrupted by a checkpoint or a sentinel from the producer.
123
+
124
+ Returns:
125
+ connector_sdk_pb2.UpdateResponse or list[connector_sdk_pb2.UpdateResponse]: Either a single response
126
+ containing records or checkpoint, or a list of responses when flushing data with a checkpoint.
127
+
128
+ """
129
+ while self._buffer_record_count < MAX_RECORDS_IN_BATCH and self._buffer_size_bytes < MAX_BATCH_SIZE_IN_BYTES:
130
+ operation = self._queue.get()
131
+
132
+ # Case 1: If operation is sentinel, mark the stream as done, flush the buffer.
133
+ if operation is self._sentinel:
134
+ self._is_done = True
135
+ if self._buffer:
136
+ return self._flush_buffer()
137
+ else:
138
+ raise StopIteration
139
+
140
+ # Case 2: if operation is a Checkpoint, flush the buffer and send the checkpoint.
141
+ elif isinstance(operation, connector_sdk_pb2.Checkpoint):
142
+ return self._flush_buffer_on_checkpoint(operation)
143
+
144
+ # it is record, buffer it to flush in batches
145
+ self._buffer_record_count += 1
146
+ self._buffer_size_bytes += len(operation.SerializeToString())
147
+ self._buffer.append(operation)
148
+
149
+ # Case 3: If buffer size limit is reached, flush the buffer and return the response.
150
+ return self._flush_buffer()
151
+
152
+ def _flush_buffer_on_checkpoint(self, checkpoint: connector_sdk_pb2.Checkpoint):
153
+ """
154
+ Creates the responses containing the checkpoint and buffered records.
155
+
156
+ Args:
157
+ checkpoint (object): Checkpoint operation to be added to the response.
158
+ """
159
+ responses = []
160
+
161
+ if self._buffer:
162
+ responses.append(self._flush_buffer())
163
+
164
+ responses.append(connector_sdk_pb2.UpdateResponse(checkpoint=checkpoint))
165
+ return responses
166
+
167
+ def _flush_buffer(self):
168
+ """
169
+ Flushes the current buffer and returns a response containing the buffered records.
170
+
171
+ Returns:
172
+ connector_sdk_pb2.UpdateResponse: A response containing the buffered records.
173
+ """
174
+ batch_to_flush = self._buffer
175
+ self._buffer = []
176
+ self._buffer_record_count = 0
177
+ self._buffer_size_bytes = 0
178
+ return connector_sdk_pb2.UpdateResponse(records=connector_sdk_pb2.Records(records=batch_to_flush))
179
+
180
+
181
+ _LOG_DATA_TYPE_INFERENCE = {}
85
182
 
86
183
  class Operations:
87
184
  operation_stream = _OperationStream()
@@ -107,17 +204,15 @@ class Operations:
107
204
  new_table = common_pb2.Table(name=table, columns=columns.values())
108
205
  TABLES[table] = new_table
109
206
 
110
- mapped_data = _map_data_to_columns(data, columns)
207
+ mapped_data = _map_data_to_columns(data, columns, table)
111
208
  record = connector_sdk_pb2.Record(
112
209
  schema_name=None,
113
210
  table_name=table,
114
211
  type=common_pb2.RecordType.UPSERT,
115
212
  data=mapped_data
116
213
  )
117
- update_response = connector_sdk_pb2.UpdateResponse(record=record)
118
-
119
- Operations.operation_stream.add(update_response)
120
214
 
215
+ Operations.operation_stream.add(record)
121
216
 
122
217
  @staticmethod
123
218
  def update(table: str, modified: dict):
@@ -132,7 +227,7 @@ class Operations:
132
227
  """
133
228
  table = get_renamed_table_name(table)
134
229
  columns = _get_columns(table)
135
- mapped_data = _map_data_to_columns(modified, columns)
230
+ mapped_data = _map_data_to_columns(modified, columns, table)
136
231
  record = connector_sdk_pb2.Record(
137
232
  schema_name=None,
138
233
  table_name=table,
@@ -140,9 +235,7 @@ class Operations:
140
235
  data=mapped_data
141
236
  )
142
237
 
143
- update_response = connector_sdk_pb2.UpdateResponse(record=record)
144
-
145
- Operations.operation_stream.add(update_response)
238
+ Operations.operation_stream.add(record)
146
239
 
147
240
  @staticmethod
148
241
  def delete(table: str, keys: dict):
@@ -157,7 +250,7 @@ class Operations:
157
250
  """
158
251
  table = get_renamed_table_name(table)
159
252
  columns = _get_columns(table)
160
- mapped_data = _map_data_to_columns(keys, columns)
253
+ mapped_data = _map_data_to_columns(keys, columns, table)
161
254
  record = connector_sdk_pb2.Record(
162
255
  schema_name=None,
163
256
  table_name=table,
@@ -165,9 +258,7 @@ class Operations:
165
258
  data=mapped_data
166
259
  )
167
260
 
168
- update_response = connector_sdk_pb2.UpdateResponse(record=record)
169
-
170
- Operations.operation_stream.add(update_response)
261
+ Operations.operation_stream.add(record)
171
262
 
172
263
  @staticmethod
173
264
  def checkpoint(state: dict):
@@ -190,9 +281,9 @@ class Operations:
190
281
  Returns:
191
282
  connector_sdk_pb2.UpdateResponse: The checkpoint response.
192
283
  """
193
- update_response = connector_sdk_pb2.UpdateResponse(checkpoint=connector_sdk_pb2.Checkpoint(state_json=json.dumps(state)))
284
+ checkpoint = connector_sdk_pb2.Checkpoint(state_json=json.dumps(state))
194
285
 
195
- Operations.operation_stream.add(update_response)
286
+ Operations.operation_stream.add(checkpoint)
196
287
 
197
288
  def _get_columns(table: str) -> dict:
198
289
  """Retrieves the columns for the specified table.
@@ -210,8 +301,24 @@ def _get_columns(table: str) -> dict:
210
301
 
211
302
  return columns
212
303
 
304
+ def _get_table_pk(table: str) -> bool:
305
+ """Retrieves the columns for the specified table.
213
306
 
214
- def _map_data_to_columns(data: dict, columns: dict) -> dict:
307
+ Args:
308
+ table (str): The name of the table.
309
+
310
+ Returns:
311
+ dict: The columns for the table.
312
+ """
313
+ columns = {}
314
+ if table in TABLES:
315
+ for column in TABLES[table].columns:
316
+ if column.primary_key:
317
+ return True
318
+ return False
319
+
320
+
321
+ def _map_data_to_columns(data: dict, columns: dict, table: str = "") -> dict:
215
322
  """Maps data to the specified columns.
216
323
 
217
324
  Args:
@@ -229,35 +336,31 @@ def _map_data_to_columns(data: dict, columns: dict) -> dict:
229
336
  elif (key in columns) and columns[key].type != common_pb2.DataType.UNSPECIFIED:
230
337
  map_defined_data_type(columns[key].type, key, mapped_data, v)
231
338
  else:
232
- map_inferred_data_type(key, mapped_data, v)
339
+ map_inferred_data_type(key, mapped_data, v, table)
233
340
  return mapped_data
234
341
 
235
- def map_inferred_data_type(k, mapped_data, v):
342
+ def map_inferred_data_type(k, mapped_data, v, table=""):
236
343
  # We can infer type from the value
237
344
  if isinstance(v, int):
238
345
  if abs(v) > JAVA_LONG_MAX_VALUE:
239
346
  mapped_data[k] = common_pb2.ValueType(float=v)
240
347
  else:
241
348
  mapped_data[k] = common_pb2.ValueType(long=v)
242
- if _LOG_DATA_TYPE_INFERENCE["boolean"] and isinstance(v, bool):
243
- print_library_log("Fivetran: Boolean Datatype has been inferred", Logging.Level.INFO, True)
244
- _LOG_DATA_TYPE_INFERENCE["boolean"] = False
349
+ if _LOG_DATA_TYPE_INFERENCE.get("boolean_" + table, True) and isinstance(v, bool):
350
+ print_library_log("Fivetran: Boolean Datatype has been inferred for " + table, Logging.Level.INFO, True)
351
+ if not _get_table_pk(table):
352
+ print_library_log("Fivetran: Boolean Datatype inference issue for " + table, Logging.Level.INFO, True)
353
+ _LOG_DATA_TYPE_INFERENCE["boolean_" + table] = False
245
354
  elif isinstance(v, float):
246
355
  mapped_data[k] = common_pb2.ValueType(float=v)
247
356
  elif isinstance(v, bool):
248
357
  mapped_data[k] = common_pb2.ValueType(bool=v)
249
358
  elif isinstance(v, bytes):
250
- if _LOG_DATA_TYPE_INFERENCE["binary"]:
251
- print_library_log("Fivetran: Binary Datatype has been inferred", Logging.Level.INFO, True)
252
- _LOG_DATA_TYPE_INFERENCE["binary"] = False
253
359
  mapped_data[k] = common_pb2.ValueType(binary=v)
254
360
  elif isinstance(v, list):
255
361
  raise ValueError(
256
362
  "Values for the columns cannot be of type 'list'. Please ensure that all values are of a supported type. Reference: https://fivetran.com/docs/connectors/connector-sdk/technical-reference#supporteddatatypes")
257
363
  elif isinstance(v, dict):
258
- if _LOG_DATA_TYPE_INFERENCE["json"]:
259
- print_library_log("Fivetran: JSON Datatype has been inferred", Logging.Level.INFO, True)
260
- _LOG_DATA_TYPE_INFERENCE["json"] = False
261
364
  mapped_data[k] = common_pb2.ValueType(json=json.dumps(v))
262
365
  elif isinstance(v, str):
263
366
  mapped_data[k] = common_pb2.ValueType(string=v)
@@ -334,4 +437,3 @@ def _yield_check(stack):
334
437
  # This should never happen
335
438
  raise RuntimeError(
336
439
  f"The '{called_method}' function is missing in the connector calling code '{calling_code}'. Please ensure that the '{called_method}' function is properly defined in your code to proceed. Reference: https://fivetran.com/docs/connectors/connector-sdk/technical-reference#technicaldetailsmethods")
337
-
@@ -26,7 +26,7 @@ _sym_db = _symbol_database.Default()
26
26
  from fivetran_connector_sdk import common_pb2 as common__pb2
27
27
 
28
28
 
29
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13\x63onnector_sdk.proto\x12\x0f\x66ivetran_sdk.v2\x1a\x0c\x63ommon.proto\"\x8f\x01\n\rSchemaRequest\x12H\n\rconfiguration\x18\x01 \x03(\x0b\x32\x31.fivetran_sdk.v2.SchemaRequest.ConfigurationEntry\x1a\x34\n\x12\x43onfigurationEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xf1\x01\n\x0eSchemaResponse\x12\'\n\x1dschema_response_not_supported\x18\x01 \x01(\x08H\x00\x12\x32\n\x0bwith_schema\x18\x02 \x01(\x0b\x32\x1b.fivetran_sdk.v2.SchemaListH\x00\x12\x34\n\x0ewithout_schema\x18\x03 \x01(\x0b\x32\x1a.fivetran_sdk.v2.TableListH\x00\x12$\n\x17selection_not_supported\x18\x04 \x01(\x08H\x01\x88\x01\x01\x42\n\n\x08responseB\x1a\n\x18_selection_not_supported\"\xf9\x01\n\rUpdateRequest\x12H\n\rconfiguration\x18\x01 \x03(\x0b\x32\x31.fivetran_sdk.v2.UpdateRequest.ConfigurationEntry\x12\x32\n\tselection\x18\x02 \x01(\x0b\x32\x1a.fivetran_sdk.v2.SelectionH\x00\x88\x01\x01\x12\x17\n\nstate_json\x18\x03 \x01(\tH\x01\x88\x01\x01\x1a\x34\n\x12\x43onfigurationEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x0c\n\n_selectionB\r\n\x0b_state_json\"\x91\x01\n\tSelection\x12=\n\x0ewithout_schema\x18\x01 \x01(\x0b\x32#.fivetran_sdk.v2.TablesWithNoSchemaH\x00\x12\x38\n\x0bwith_schema\x18\x02 \x01(\x0b\x32!.fivetran_sdk.v2.TablesWithSchemaH\x00\x42\x0b\n\tselection\"a\n\x12TablesWithNoSchema\x12/\n\x06tables\x18\x01 \x03(\x0b\x32\x1f.fivetran_sdk.v2.TableSelection\x12\x1a\n\x12include_new_tables\x18\x02 \x01(\x08\"b\n\x10TablesWithSchema\x12\x31\n\x07schemas\x18\x01 \x03(\x0b\x32 .fivetran_sdk.v2.SchemaSelection\x12\x1b\n\x13include_new_schemas\x18\x02 \x01(\x08\"\x85\x01\n\x0fSchemaSelection\x12\x10\n\x08included\x18\x01 \x01(\x08\x12\x13\n\x0bschema_name\x18\x02 \x01(\t\x12/\n\x06tables\x18\x03 \x03(\x0b\x32\x1f.fivetran_sdk.v2.TableSelection\x12\x1a\n\x12include_new_tables\x18\x04 \x01(\x08\"\xc2\x01\n\x0eTableSelection\x12\x10\n\x08included\x18\x01 \x01(\x08\x12\x12\n\ntable_name\x18\x02 \x01(\t\x12=\n\x07\x63olumns\x18\x03 \x03(\x0b\x32,.fivetran_sdk.v2.TableSelection.ColumnsEntry\x12\x1b\n\x13include_new_columns\x18\x04 \x01(\x08\x1a.\n\x0c\x43olumnsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x08:\x02\x38\x01\"\x87\x02\n\x0eUpdateResponse\x12)\n\x06record\x18\x01 \x01(\x0b\x32\x17.fivetran_sdk.v2.RecordH\x00\x12\x36\n\rschema_change\x18\x02 \x01(\x0b\x32\x1d.fivetran_sdk.v2.SchemaChangeH\x00\x12\x31\n\ncheckpoint\x18\x03 \x01(\x0b\x32\x1b.fivetran_sdk.v2.CheckpointH\x00\x12+\n\x07warning\x18\x04 \x01(\x0b\x32\x18.fivetran_sdk.v2.WarningH\x00\x12%\n\x04task\x18\x05 \x01(\x0b\x32\x15.fivetran_sdk.v2.TaskH\x00\x42\x0b\n\toperation\"\x82\x01\n\x0cSchemaChange\x12\x32\n\x0bwith_schema\x18\x01 \x01(\x0b\x32\x1b.fivetran_sdk.v2.SchemaListH\x00\x12\x34\n\x0ewithout_schema\x18\x02 \x01(\x0b\x32\x1a.fivetran_sdk.v2.TableListH\x00\x42\x08\n\x06\x63hange\"\xeb\x01\n\x06Record\x12\x18\n\x0bschema_name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x12\n\ntable_name\x18\x02 \x01(\t\x12)\n\x04type\x18\x03 \x01(\x0e\x32\x1b.fivetran_sdk.v2.RecordType\x12/\n\x04\x64\x61ta\x18\x04 \x03(\x0b\x32!.fivetran_sdk.v2.Record.DataEntry\x1aG\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.fivetran_sdk.v2.ValueType:\x02\x38\x01\x42\x0e\n\x0c_schema_name\" \n\nCheckpoint\x12\x12\n\nstate_json\x18\x01 \x01(\t2\xe2\x02\n\x0fSourceConnector\x12l\n\x11\x43onfigurationForm\x12).fivetran_sdk.v2.ConfigurationFormRequest\x1a*.fivetran_sdk.v2.ConfigurationFormResponse\"\x00\x12\x45\n\x04Test\x12\x1c.fivetran_sdk.v2.TestRequest\x1a\x1d.fivetran_sdk.v2.TestResponse\"\x00\x12K\n\x06Schema\x12\x1e.fivetran_sdk.v2.SchemaRequest\x1a\x1f.fivetran_sdk.v2.SchemaResponse\"\x00\x12M\n\x06Update\x12\x1e.fivetran_sdk.v2.UpdateRequest\x1a\x1f.fivetran_sdk.v2.UpdateResponse\"\x00\x30\x01\x42\"H\x01P\x01Z\x1c\x66ivetran.com/fivetran_sdk_v2b\x06proto3')
29
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13\x63onnector_sdk.proto\x12\x0f\x66ivetran_sdk.v2\x1a\x0c\x63ommon.proto\"\x8f\x01\n\rSchemaRequest\x12H\n\rconfiguration\x18\x01 \x03(\x0b\x32\x31.fivetran_sdk.v2.SchemaRequest.ConfigurationEntry\x1a\x34\n\x12\x43onfigurationEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xf1\x01\n\x0eSchemaResponse\x12\'\n\x1dschema_response_not_supported\x18\x01 \x01(\x08H\x00\x12\x32\n\x0bwith_schema\x18\x02 \x01(\x0b\x32\x1b.fivetran_sdk.v2.SchemaListH\x00\x12\x34\n\x0ewithout_schema\x18\x03 \x01(\x0b\x32\x1a.fivetran_sdk.v2.TableListH\x00\x12$\n\x17selection_not_supported\x18\x04 \x01(\x08H\x01\x88\x01\x01\x42\n\n\x08responseB\x1a\n\x18_selection_not_supported\"\xf9\x01\n\rUpdateRequest\x12H\n\rconfiguration\x18\x01 \x03(\x0b\x32\x31.fivetran_sdk.v2.UpdateRequest.ConfigurationEntry\x12\x32\n\tselection\x18\x02 \x01(\x0b\x32\x1a.fivetran_sdk.v2.SelectionH\x00\x88\x01\x01\x12\x17\n\nstate_json\x18\x03 \x01(\tH\x01\x88\x01\x01\x1a\x34\n\x12\x43onfigurationEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x0c\n\n_selectionB\r\n\x0b_state_json\"\x91\x01\n\tSelection\x12=\n\x0ewithout_schema\x18\x01 \x01(\x0b\x32#.fivetran_sdk.v2.TablesWithNoSchemaH\x00\x12\x38\n\x0bwith_schema\x18\x02 \x01(\x0b\x32!.fivetran_sdk.v2.TablesWithSchemaH\x00\x42\x0b\n\tselection\"a\n\x12TablesWithNoSchema\x12/\n\x06tables\x18\x01 \x03(\x0b\x32\x1f.fivetran_sdk.v2.TableSelection\x12\x1a\n\x12include_new_tables\x18\x02 \x01(\x08\"b\n\x10TablesWithSchema\x12\x31\n\x07schemas\x18\x01 \x03(\x0b\x32 .fivetran_sdk.v2.SchemaSelection\x12\x1b\n\x13include_new_schemas\x18\x02 \x01(\x08\"\x85\x01\n\x0fSchemaSelection\x12\x10\n\x08included\x18\x01 \x01(\x08\x12\x13\n\x0bschema_name\x18\x02 \x01(\t\x12/\n\x06tables\x18\x03 \x03(\x0b\x32\x1f.fivetran_sdk.v2.TableSelection\x12\x1a\n\x12include_new_tables\x18\x04 \x01(\x08\"\xc2\x01\n\x0eTableSelection\x12\x10\n\x08included\x18\x01 \x01(\x08\x12\x12\n\ntable_name\x18\x02 \x01(\t\x12=\n\x07\x63olumns\x18\x03 \x03(\x0b\x32,.fivetran_sdk.v2.TableSelection.ColumnsEntry\x12\x1b\n\x13include_new_columns\x18\x04 \x01(\x08\x1a.\n\x0c\x43olumnsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x08:\x02\x38\x01\"\xb4\x02\n\x0eUpdateResponse\x12)\n\x06record\x18\x01 \x01(\x0b\x32\x17.fivetran_sdk.v2.RecordH\x00\x12\x36\n\rschema_change\x18\x02 \x01(\x0b\x32\x1d.fivetran_sdk.v2.SchemaChangeH\x00\x12\x31\n\ncheckpoint\x18\x03 \x01(\x0b\x32\x1b.fivetran_sdk.v2.CheckpointH\x00\x12+\n\x07warning\x18\x04 \x01(\x0b\x32\x18.fivetran_sdk.v2.WarningH\x00\x12%\n\x04task\x18\x05 \x01(\x0b\x32\x15.fivetran_sdk.v2.TaskH\x00\x12+\n\x07records\x18\x06 \x01(\x0b\x32\x18.fivetran_sdk.v2.RecordsH\x00\x42\x0b\n\toperation\"\x82\x01\n\x0cSchemaChange\x12\x32\n\x0bwith_schema\x18\x01 \x01(\x0b\x32\x1b.fivetran_sdk.v2.SchemaListH\x00\x12\x34\n\x0ewithout_schema\x18\x02 \x01(\x0b\x32\x1a.fivetran_sdk.v2.TableListH\x00\x42\x08\n\x06\x63hange\"3\n\x07Records\x12(\n\x07records\x18\x01 \x03(\x0b\x32\x17.fivetran_sdk.v2.Record\"\xeb\x01\n\x06Record\x12\x18\n\x0bschema_name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x12\n\ntable_name\x18\x02 \x01(\t\x12)\n\x04type\x18\x03 \x01(\x0e\x32\x1b.fivetran_sdk.v2.RecordType\x12/\n\x04\x64\x61ta\x18\x04 \x03(\x0b\x32!.fivetran_sdk.v2.Record.DataEntry\x1aG\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.fivetran_sdk.v2.ValueType:\x02\x38\x01\x42\x0e\n\x0c_schema_name\" \n\nCheckpoint\x12\x12\n\nstate_json\x18\x01 \x01(\t2\xe2\x02\n\x0fSourceConnector\x12l\n\x11\x43onfigurationForm\x12).fivetran_sdk.v2.ConfigurationFormRequest\x1a*.fivetran_sdk.v2.ConfigurationFormResponse\"\x00\x12\x45\n\x04Test\x12\x1c.fivetran_sdk.v2.TestRequest\x1a\x1d.fivetran_sdk.v2.TestResponse\"\x00\x12K\n\x06Schema\x12\x1e.fivetran_sdk.v2.SchemaRequest\x1a\x1f.fivetran_sdk.v2.SchemaResponse\"\x00\x12M\n\x06Update\x12\x1e.fivetran_sdk.v2.UpdateRequest\x1a\x1f.fivetran_sdk.v2.UpdateResponse\"\x00\x30\x01\x42\"H\x01P\x01Z\x1c\x66ivetran.com/fivetran_sdk_v2b\x06proto3')
30
30
 
31
31
  _globals = globals()
32
32
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -65,15 +65,17 @@ if not _descriptor._USE_C_DESCRIPTORS:
65
65
  _globals['_TABLESELECTION_COLUMNSENTRY']._serialized_start=1328
66
66
  _globals['_TABLESELECTION_COLUMNSENTRY']._serialized_end=1374
67
67
  _globals['_UPDATERESPONSE']._serialized_start=1377
68
- _globals['_UPDATERESPONSE']._serialized_end=1640
69
- _globals['_SCHEMACHANGE']._serialized_start=1643
70
- _globals['_SCHEMACHANGE']._serialized_end=1773
71
- _globals['_RECORD']._serialized_start=1776
72
- _globals['_RECORD']._serialized_end=2011
73
- _globals['_RECORD_DATAENTRY']._serialized_start=1924
74
- _globals['_RECORD_DATAENTRY']._serialized_end=1995
75
- _globals['_CHECKPOINT']._serialized_start=2013
76
- _globals['_CHECKPOINT']._serialized_end=2045
77
- _globals['_SOURCECONNECTOR']._serialized_start=2048
78
- _globals['_SOURCECONNECTOR']._serialized_end=2402
68
+ _globals['_UPDATERESPONSE']._serialized_end=1685
69
+ _globals['_SCHEMACHANGE']._serialized_start=1688
70
+ _globals['_SCHEMACHANGE']._serialized_end=1818
71
+ _globals['_RECORDS']._serialized_start=1820
72
+ _globals['_RECORDS']._serialized_end=1871
73
+ _globals['_RECORD']._serialized_start=1874
74
+ _globals['_RECORD']._serialized_end=2109
75
+ _globals['_RECORD_DATAENTRY']._serialized_start=2022
76
+ _globals['_RECORD_DATAENTRY']._serialized_end=2093
77
+ _globals['_CHECKPOINT']._serialized_start=2111
78
+ _globals['_CHECKPOINT']._serialized_end=2143
79
+ _globals['_SOURCECONNECTOR']._serialized_start=2146
80
+ _globals['_SOURCECONNECTOR']._serialized_end=2500
79
81
  # @@protoc_insertion_point(module_scope)
@@ -104,18 +104,20 @@ class TableSelection(_message.Message):
104
104
  def __init__(self, included: bool = ..., table_name: _Optional[str] = ..., columns: _Optional[_Mapping[str, bool]] = ..., include_new_columns: bool = ...) -> None: ...
105
105
 
106
106
  class UpdateResponse(_message.Message):
107
- __slots__ = ("record", "schema_change", "checkpoint", "warning", "task")
107
+ __slots__ = ("record", "schema_change", "checkpoint", "warning", "task", "records")
108
108
  RECORD_FIELD_NUMBER: _ClassVar[int]
109
109
  SCHEMA_CHANGE_FIELD_NUMBER: _ClassVar[int]
110
110
  CHECKPOINT_FIELD_NUMBER: _ClassVar[int]
111
111
  WARNING_FIELD_NUMBER: _ClassVar[int]
112
112
  TASK_FIELD_NUMBER: _ClassVar[int]
113
+ RECORDS_FIELD_NUMBER: _ClassVar[int]
113
114
  record: Record
114
115
  schema_change: SchemaChange
115
116
  checkpoint: Checkpoint
116
117
  warning: _common_pb2.Warning
117
118
  task: _common_pb2.Task
118
- def __init__(self, record: _Optional[_Union[Record, _Mapping]] = ..., schema_change: _Optional[_Union[SchemaChange, _Mapping]] = ..., checkpoint: _Optional[_Union[Checkpoint, _Mapping]] = ..., warning: _Optional[_Union[_common_pb2.Warning, _Mapping]] = ..., task: _Optional[_Union[_common_pb2.Task, _Mapping]] = ...) -> None: ...
119
+ records: Records
120
+ def __init__(self, record: _Optional[_Union[Record, _Mapping]] = ..., schema_change: _Optional[_Union[SchemaChange, _Mapping]] = ..., checkpoint: _Optional[_Union[Checkpoint, _Mapping]] = ..., warning: _Optional[_Union[_common_pb2.Warning, _Mapping]] = ..., task: _Optional[_Union[_common_pb2.Task, _Mapping]] = ..., records: _Optional[_Union[Records, _Mapping]] = ...) -> None: ...
119
121
 
120
122
  class SchemaChange(_message.Message):
121
123
  __slots__ = ("with_schema", "without_schema")
@@ -125,6 +127,12 @@ class SchemaChange(_message.Message):
125
127
  without_schema: _common_pb2.TableList
126
128
  def __init__(self, with_schema: _Optional[_Union[_common_pb2.SchemaList, _Mapping]] = ..., without_schema: _Optional[_Union[_common_pb2.TableList, _Mapping]] = ...) -> None: ...
127
129
 
130
+ class Records(_message.Message):
131
+ __slots__ = ("records",)
132
+ RECORDS_FIELD_NUMBER: _ClassVar[int]
133
+ records: _containers.RepeatedCompositeFieldContainer[Record]
134
+ def __init__(self, records: _Optional[_Iterable[_Union[Record, _Mapping]]] = ...) -> None: ...
135
+
128
136
  class Record(_message.Message):
129
137
  __slots__ = ("schema_name", "table_name", "type", "data")
130
138
  class DataEntry(_message.Message):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fivetran_connector_sdk
3
- Version: 1.7.5
3
+ Version: 1.8.1
4
4
  Summary: Build custom connectors on Fivetran platform
5
5
  Author-email: Fivetran <developers@fivetran.com>
6
6
  Project-URL: Homepage, https://fivetran.com/docs/connectors/connector-sdk