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
@@ -7,6 +7,10 @@ import unicodedata
|
|
7
7
|
import importlib.util
|
8
8
|
from datetime import datetime
|
9
9
|
from unidecode import unidecode
|
10
|
+
from prompt_toolkit import prompt
|
11
|
+
from prompt_toolkit.document import Document
|
12
|
+
from prompt_toolkit.history import FileHistory
|
13
|
+
from prompt_toolkit.completion import Completer, Completion, PathCompleter, merge_completers
|
10
14
|
|
11
15
|
from fivetran_connector_sdk.logger import Logging
|
12
16
|
from fivetran_connector_sdk import constants
|
@@ -255,43 +259,91 @@ def edit_distance(first_string: str, second_string: str) -> int:
|
|
255
259
|
# The last value in the last row is the edit distance
|
256
260
|
return previous_row[second_string_length]
|
257
261
|
|
258
|
-
|
259
|
-
def
|
262
|
+
class EnvironmentVariableCompleter(Completer):
|
263
|
+
def get_completions(self, document, complete_event):
|
264
|
+
text = document.text_before_cursor
|
265
|
+
if text.startswith('$'):
|
266
|
+
# Get the variable name part (text after the '$')
|
267
|
+
var_name = text[1:]
|
268
|
+
|
269
|
+
# Suggest all environment variables that start with the typed name
|
270
|
+
for env_var in os.environ:
|
271
|
+
if env_var.startswith(var_name):
|
272
|
+
yield Completion(
|
273
|
+
f'${env_var}',
|
274
|
+
start_position=-len(text)
|
275
|
+
)
|
276
|
+
|
277
|
+
class EnvVarPathCompleter(Completer):
|
278
|
+
def get_completions(self, document, complete_event):
|
279
|
+
text_before_cursor = document.text_before_cursor
|
280
|
+
expanded_text = os.path.expandvars(text_before_cursor)
|
281
|
+
|
282
|
+
# Create a new document for the PathCompleter to use
|
283
|
+
expanded_document = Document(
|
284
|
+
text=expanded_text, cursor_position=len(expanded_text)
|
285
|
+
)
|
286
|
+
|
287
|
+
# Use a standard PathCompleter on our new, temporary document
|
288
|
+
path_completer = PathCompleter(expanduser=True)
|
289
|
+
yield from path_completer.get_completions(expanded_document, complete_event)
|
290
|
+
|
291
|
+
|
292
|
+
def get_input_from_cli(prompt_txt: str, default_value: str, hide_value = False) -> str:
|
260
293
|
"""
|
261
294
|
Prompts the user for input.
|
262
295
|
"""
|
296
|
+
final_completer = merge_completers([EnvironmentVariableCompleter(), EnvVarPathCompleter()])
|
297
|
+
history = FileHistory(os.path.join(os.path.expanduser('~'), '.fivetran_history'))
|
263
298
|
if default_value:
|
264
|
-
|
299
|
+
if hide_value:
|
300
|
+
default_value_hidden = default_value[0:8] + "********"
|
301
|
+
value = prompt(f"{prompt_txt} [Default : {default_value_hidden}]: ",
|
302
|
+
completer=final_completer, history=history, complete_while_typing=False, complete_style='readline_like'
|
303
|
+
).strip() or default_value
|
304
|
+
else:
|
305
|
+
value = prompt(f"{prompt_txt} [Default : {default_value}]: ",
|
306
|
+
completer=final_completer, history=history, complete_while_typing=False, complete_style='readline_like'
|
307
|
+
).strip() or default_value
|
265
308
|
else:
|
266
|
-
value =
|
309
|
+
value = prompt(f"{prompt_txt}: ",
|
310
|
+
completer=final_completer, history=history, complete_while_typing=False, complete_style='readline_like'
|
311
|
+
).strip()
|
267
312
|
|
268
313
|
if not value:
|
269
314
|
raise ValueError("Missing required input: Expected a value but received None")
|
270
|
-
return value
|
315
|
+
return os.path.expandvars(value)
|
271
316
|
|
272
|
-
def validate_and_load_configuration(
|
317
|
+
def validate_and_load_configuration(project_path, configuration):
|
273
318
|
if configuration:
|
274
|
-
|
319
|
+
configuration = os.path.expanduser(configuration)
|
320
|
+
if os.path.isabs(configuration):
|
321
|
+
json_filepath = os.path.abspath(configuration)
|
322
|
+
else:
|
323
|
+
relative_path = os.path.join(project_path, configuration)
|
324
|
+
json_filepath = os.path.abspath(str(relative_path))
|
275
325
|
if os.path.isfile(json_filepath):
|
276
326
|
with open(json_filepath, 'r', encoding=UTF_8) as fi:
|
277
|
-
|
327
|
+
try:
|
328
|
+
configuration = json.load(fi)
|
329
|
+
except:
|
330
|
+
raise ValueError(
|
331
|
+
"Configuration must be provided as a JSON file. Please check your input. Reference: "
|
332
|
+
"https://fivetran.com/docs/connectors/connector-sdk/detailed-guide#workingwithconfigurationjsonfile")
|
278
333
|
if len(configuration) > MAX_CONFIG_FIELDS:
|
279
334
|
raise ValueError(f"Configuration field count exceeds maximum of {MAX_CONFIG_FIELDS}. Reduce the field count.")
|
280
335
|
else:
|
281
336
|
raise ValueError(
|
282
|
-
"Configuration
|
283
|
-
"https://fivetran.com/docs/connectors/connector-sdk/detailed-guide#workingwithconfigurationjsonfile")
|
337
|
+
f"Configuration path is incorrect, cannot find file at the location {json_filepath}")
|
284
338
|
else:
|
285
|
-
|
286
|
-
if os.path.exists(json_filepath):
|
287
|
-
print_library_log("Configuration file detected in the project, but no configuration input provided via the command line", Logging.Level.WARNING)
|
339
|
+
print_library_log("No configuration file passed.", Logging.Level.INFO)
|
288
340
|
configuration = {}
|
289
341
|
return configuration
|
290
342
|
|
291
343
|
|
292
344
|
def validate_and_load_state(args, state):
|
293
345
|
if state:
|
294
|
-
json_filepath =
|
346
|
+
json_filepath = os.path.abspath(os.path.join(args.project_path, args.state))
|
295
347
|
else:
|
296
348
|
json_filepath = os.path.join(args.project_path, "files", "state.json")
|
297
349
|
|
@@ -312,7 +364,7 @@ def reset_local_file_directory(args):
|
|
312
364
|
confirm = "y"
|
313
365
|
else:
|
314
366
|
confirm = input(
|
315
|
-
"This will delete your current state and `warehouse.db` files. Do you want to continue? (Y/
|
367
|
+
"This will delete your current state and `warehouse.db` files. Do you want to continue? (Y/n): ")
|
316
368
|
if confirm.lower() != "y":
|
317
369
|
print_library_log("Reset canceled")
|
318
370
|
else:
|
@@ -55,13 +55,11 @@ class Operations:
|
|
55
55
|
record = connector_sdk_pb2.Record(
|
56
56
|
schema_name=None,
|
57
57
|
table_name=table,
|
58
|
-
type=common_pb2.
|
58
|
+
type=common_pb2.RecordType.UPSERT,
|
59
59
|
data=mapped_data
|
60
60
|
)
|
61
61
|
|
62
|
-
responses.append(
|
63
|
-
connector_sdk_pb2.UpdateResponse(
|
64
|
-
operation=connector_sdk_pb2.Operation(record=record)))
|
62
|
+
responses.append(connector_sdk_pb2.UpdateResponse(record=record))
|
65
63
|
|
66
64
|
return responses
|
67
65
|
|
@@ -85,12 +83,11 @@ class Operations:
|
|
85
83
|
record = connector_sdk_pb2.Record(
|
86
84
|
schema_name=None,
|
87
85
|
table_name=table,
|
88
|
-
type=common_pb2.
|
86
|
+
type=common_pb2.RecordType.UPDATE,
|
89
87
|
data=mapped_data
|
90
88
|
)
|
91
89
|
|
92
|
-
return connector_sdk_pb2.UpdateResponse(
|
93
|
-
operation=connector_sdk_pb2.Operation(record=record))
|
90
|
+
return connector_sdk_pb2.UpdateResponse(record=record)
|
94
91
|
|
95
92
|
@staticmethod
|
96
93
|
def delete(table: str, keys: dict) -> connector_sdk_pb2.UpdateResponse:
|
@@ -112,12 +109,11 @@ class Operations:
|
|
112
109
|
record = connector_sdk_pb2.Record(
|
113
110
|
schema_name=None,
|
114
111
|
table_name=table,
|
115
|
-
type=common_pb2.
|
112
|
+
type=common_pb2.RecordType.DELETE,
|
116
113
|
data=mapped_data
|
117
114
|
)
|
118
115
|
|
119
|
-
return connector_sdk_pb2.UpdateResponse(
|
120
|
-
operation=connector_sdk_pb2.Operation(record=record))
|
116
|
+
return connector_sdk_pb2.UpdateResponse(record=record)
|
121
117
|
|
122
118
|
@staticmethod
|
123
119
|
def checkpoint(state: dict) -> connector_sdk_pb2.UpdateResponse:
|
@@ -142,10 +138,7 @@ class Operations:
|
|
142
138
|
"""
|
143
139
|
if constants.DEBUGGING:
|
144
140
|
_yield_check(inspect.stack())
|
145
|
-
|
146
|
-
return connector_sdk_pb2.UpdateResponse(
|
147
|
-
operation=connector_sdk_pb2.Operation(checkpoint=connector_sdk_pb2.Checkpoint(
|
148
|
-
state_json=json.dumps(state))))
|
141
|
+
return connector_sdk_pb2.UpdateResponse(checkpoint=connector_sdk_pb2.Checkpoint(state_json=json.dumps(state)))
|
149
142
|
|
150
143
|
def _get_columns(table: str) -> dict:
|
151
144
|
"""Retrieves the columns for the specified table.
|
@@ -180,7 +173,7 @@ def _map_data_to_columns(data: dict, columns: dict) -> dict:
|
|
180
173
|
if v is None:
|
181
174
|
mapped_data[key] = common_pb2.ValueType(null=True)
|
182
175
|
elif (key in columns) and columns[key].type != common_pb2.DataType.UNSPECIFIED:
|
183
|
-
map_defined_data_type(columns, key, mapped_data, v)
|
176
|
+
map_defined_data_type(columns[key].type, key, mapped_data, v)
|
184
177
|
else:
|
185
178
|
map_inferred_data_type(key, mapped_data, v)
|
186
179
|
return mapped_data
|
@@ -218,53 +211,50 @@ def map_inferred_data_type(k, mapped_data, v):
|
|
218
211
|
# Convert arbitrary objects to string
|
219
212
|
mapped_data[k] = common_pb2.ValueType(string=str(v))
|
220
213
|
|
214
|
+
_TYPE_HANDLERS = {
|
215
|
+
common_pb2.DataType.BOOLEAN: lambda val: common_pb2.ValueType(bool=val),
|
216
|
+
common_pb2.DataType.SHORT: lambda val: common_pb2.ValueType(short=val),
|
217
|
+
common_pb2.DataType.INT: lambda val: common_pb2.ValueType(int=val),
|
218
|
+
common_pb2.DataType.LONG: lambda val: common_pb2.ValueType(long=val),
|
219
|
+
common_pb2.DataType.DECIMAL: lambda val: common_pb2.ValueType(decimal=val),
|
220
|
+
common_pb2.DataType.FLOAT: lambda val: common_pb2.ValueType(float=val),
|
221
|
+
common_pb2.DataType.DOUBLE: lambda val: common_pb2.ValueType(double=val),
|
222
|
+
common_pb2.DataType.NAIVE_DATE: lambda val: common_pb2.ValueType(naive_date= _parse_naive_date_str(val)),
|
223
|
+
common_pb2.DataType.NAIVE_DATETIME: lambda val: common_pb2.ValueType(naive_datetime= _parse_naive_datetime_str(val)),
|
224
|
+
common_pb2.DataType.UTC_DATETIME: lambda val: common_pb2.ValueType(utc_datetime= _parse_utc_datetime_str(val)),
|
225
|
+
common_pb2.DataType.BINARY: lambda val: common_pb2.ValueType(binary=val),
|
226
|
+
common_pb2.DataType.XML: lambda val: common_pb2.ValueType(xml=val),
|
227
|
+
common_pb2.DataType.STRING: lambda val: common_pb2.ValueType(string=val if isinstance(val, str) else str(val)),
|
228
|
+
common_pb2.DataType.JSON: lambda val: common_pb2.ValueType(json=json.dumps(val))
|
229
|
+
}
|
221
230
|
|
222
|
-
def map_defined_data_type(
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
mapped_data[k] = common_pb2.ValueType(short=v)
|
227
|
-
elif columns[k].type == common_pb2.DataType.INT:
|
228
|
-
mapped_data[k] = common_pb2.ValueType(int=v)
|
229
|
-
elif columns[k].type == common_pb2.DataType.LONG:
|
230
|
-
mapped_data[k] = common_pb2.ValueType(long=v)
|
231
|
-
elif columns[k].type == common_pb2.DataType.DECIMAL:
|
232
|
-
mapped_data[k] = common_pb2.ValueType(decimal=v)
|
233
|
-
elif columns[k].type == common_pb2.DataType.FLOAT:
|
234
|
-
mapped_data[k] = common_pb2.ValueType(float=v)
|
235
|
-
elif columns[k].type == common_pb2.DataType.DOUBLE:
|
236
|
-
mapped_data[k] = common_pb2.ValueType(double=v)
|
237
|
-
elif columns[k].type == common_pb2.DataType.NAIVE_DATE:
|
238
|
-
timestamp = timestamp_pb2.Timestamp()
|
239
|
-
dt = datetime.strptime(v, "%Y-%m-%d")
|
240
|
-
timestamp.FromDatetime(dt)
|
241
|
-
mapped_data[k] = common_pb2.ValueType(naive_date=timestamp)
|
242
|
-
elif columns[k].type == common_pb2.DataType.NAIVE_DATETIME:
|
243
|
-
if '.' not in v: v = v + ".0"
|
244
|
-
timestamp = timestamp_pb2.Timestamp()
|
245
|
-
dt = datetime.strptime(v, "%Y-%m-%dT%H:%M:%S.%f")
|
246
|
-
timestamp.FromDatetime(dt)
|
247
|
-
mapped_data[k] = common_pb2.ValueType(naive_datetime=timestamp)
|
248
|
-
elif columns[k].type == common_pb2.DataType.UTC_DATETIME:
|
249
|
-
timestamp = timestamp_pb2.Timestamp()
|
250
|
-
dt = v if isinstance(v, datetime) else _parse_datetime_str(v)
|
251
|
-
timestamp.FromDatetime(dt)
|
252
|
-
mapped_data[k] = common_pb2.ValueType(utc_datetime=timestamp)
|
253
|
-
elif columns[k].type == common_pb2.DataType.BINARY:
|
254
|
-
mapped_data[k] = common_pb2.ValueType(binary=v)
|
255
|
-
elif columns[k].type == common_pb2.DataType.XML:
|
256
|
-
mapped_data[k] = common_pb2.ValueType(xml=v)
|
257
|
-
elif columns[k].type == common_pb2.DataType.STRING:
|
258
|
-
incoming = v if isinstance(v, str) else str(v)
|
259
|
-
mapped_data[k] = common_pb2.ValueType(string=incoming)
|
260
|
-
elif columns[k].type == common_pb2.DataType.JSON:
|
261
|
-
mapped_data[k] = common_pb2.ValueType(json=json.dumps(v))
|
231
|
+
def map_defined_data_type(data_type, k, mapped_data, v):
|
232
|
+
handler = _TYPE_HANDLERS.get(data_type)
|
233
|
+
if handler:
|
234
|
+
mapped_data[k] = handler(v)
|
262
235
|
else:
|
263
|
-
raise ValueError(f"Unsupported data type encountered: {
|
264
|
-
|
265
|
-
def
|
266
|
-
|
267
|
-
|
236
|
+
raise ValueError(f"Unsupported data type encountered: {data_type}. Please use valid data types.")
|
237
|
+
|
238
|
+
def _parse_utc_datetime_str(v):
|
239
|
+
timestamp = timestamp_pb2.Timestamp()
|
240
|
+
dt = v
|
241
|
+
if not isinstance(v, datetime):
|
242
|
+
dt = datetime.strptime(dt, "%Y-%m-%dT%H:%M:%S.%f%z" if '.' in dt else "%Y-%m-%dT%H:%M:%S%z")
|
243
|
+
timestamp.FromDatetime(dt)
|
244
|
+
return timestamp
|
245
|
+
|
246
|
+
def _parse_naive_datetime_str(v):
|
247
|
+
if '.' not in v: v = v + ".0"
|
248
|
+
timestamp = timestamp_pb2.Timestamp()
|
249
|
+
dt = datetime.strptime(v, "%Y-%m-%dT%H:%M:%S.%f")
|
250
|
+
timestamp.FromDatetime(dt)
|
251
|
+
return timestamp
|
252
|
+
|
253
|
+
def _parse_naive_date_str(v):
|
254
|
+
timestamp = timestamp_pb2.Timestamp()
|
255
|
+
dt = datetime.strptime(v, "%Y-%m-%d")
|
256
|
+
timestamp.FromDatetime(dt)
|
257
|
+
return timestamp
|
268
258
|
|
269
259
|
def _yield_check(stack):
|
270
260
|
"""Checks for the presence of 'yield' in the calling code.
|
@@ -1,12 +1,22 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
3
|
+
# NO CHECKED-IN PROTOBUF GENCODE
|
3
4
|
# source: common.proto
|
4
|
-
# Protobuf Python Version:
|
5
|
+
# Protobuf Python Version: 5.29.0
|
5
6
|
"""Generated protocol buffer code."""
|
6
7
|
from google.protobuf import descriptor as _descriptor
|
7
8
|
from google.protobuf import descriptor_pool as _descriptor_pool
|
9
|
+
from google.protobuf import runtime_version as _runtime_version
|
8
10
|
from google.protobuf import symbol_database as _symbol_database
|
9
11
|
from google.protobuf.internal import builder as _builder
|
12
|
+
_runtime_version.ValidateProtobufRuntimeVersion(
|
13
|
+
_runtime_version.Domain.PUBLIC,
|
14
|
+
5,
|
15
|
+
29,
|
16
|
+
0,
|
17
|
+
'',
|
18
|
+
'common.proto'
|
19
|
+
)
|
10
20
|
# @@protoc_insertion_point(imports)
|
11
21
|
|
12
22
|
_sym_db = _symbol_database.Default()
|
@@ -15,52 +25,62 @@ _sym_db = _symbol_database.Default()
|
|
15
25
|
from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
|
16
26
|
|
17
27
|
|
18
|
-
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x63ommon.proto\x12\
|
28
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x63ommon.proto\x12\x0f\x66ivetran_sdk.v2\x1a\x1fgoogle/protobuf/timestamp.proto\"\x1a\n\x18\x43onfigurationFormRequest\"\xc1\x01\n\x19\x43onfigurationFormResponse\x12\"\n\x1aschema_selection_supported\x18\x01 \x01(\x08\x12!\n\x19table_selection_supported\x18\x02 \x01(\x08\x12*\n\x06\x66ields\x18\x03 \x03(\x0b\x32\x1a.fivetran_sdk.v2.FormField\x12\x31\n\x05tests\x18\x04 \x03(\x0b\x32\".fivetran_sdk.v2.ConfigurationTest\"\xba\x03\n\tFormField\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05label\x18\x02 \x01(\t\x12\x15\n\x08required\x18\x03 \x01(\x08H\x01\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x02\x88\x01\x01\x12\x30\n\ntext_field\x18\x05 \x01(\x0e\x32\x1a.fivetran_sdk.v2.TextFieldH\x00\x12\x38\n\x0e\x64ropdown_field\x18\x06 \x01(\x0b\x32\x1e.fivetran_sdk.v2.DropdownFieldH\x00\x12\x34\n\x0ctoggle_field\x18\x07 \x01(\x0b\x32\x1c.fivetran_sdk.v2.ToggleFieldH\x00\x12@\n\x12\x63onditional_fields\x18\n \x01(\x0b\x32\".fivetran_sdk.v2.ConditionalFieldsH\x00\x12\x1a\n\rdefault_value\x18\x08 \x01(\tH\x03\x88\x01\x01\x12\x18\n\x0bplaceholder\x18\t \x01(\tH\x04\x88\x01\x01\x42\x06\n\x04typeB\x0b\n\t_requiredB\x0e\n\x0c_descriptionB\x10\n\x0e_default_valueB\x0e\n\x0c_placeholder\"x\n\x11\x43onditionalFields\x12\x37\n\tcondition\x18\x01 \x01(\x0b\x32$.fivetran_sdk.v2.VisibilityCondition\x12*\n\x06\x66ields\x18\x02 \x03(\x0b\x32\x1a.fivetran_sdk.v2.FormField\"\x83\x01\n\x13VisibilityCondition\x12\x17\n\x0f\x63ondition_field\x18\x01 \x01(\t\x12\x14\n\nbool_value\x18\x02 \x01(\x08H\x00\x12\x16\n\x0cstring_value\x18\x03 \x01(\tH\x00\x12\x15\n\x0b\x65mpty_value\x18\x04 \x01(\x08H\x00\x42\x0e\n\x0cvisible_when\"\'\n\rDropdownField\x12\x16\n\x0e\x64ropdown_field\x18\x01 \x03(\t\"\r\n\x0bToggleField\"0\n\x11\x43onfigurationTest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05label\x18\x02 \x01(\t\"\x99\x01\n\x0bTestRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x46\n\rconfiguration\x18\x02 \x03(\x0b\x32/.fivetran_sdk.v2.TestRequest.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\"@\n\x0cTestResponse\x12\x11\n\x07success\x18\x01 \x01(\x08H\x00\x12\x11\n\x07\x66\x61ilure\x18\x02 \x01(\tH\x00\x42\n\n\x08response\"6\n\nSchemaList\x12(\n\x07schemas\x18\x01 \x03(\x0b\x32\x17.fivetran_sdk.v2.Schema\"3\n\tTableList\x12&\n\x06tables\x18\x01 \x03(\x0b\x32\x16.fivetran_sdk.v2.Table\">\n\x06Schema\x12\x0c\n\x04name\x18\x01 \x01(\t\x12&\n\x06tables\x18\x02 \x03(\x0b\x32\x16.fivetran_sdk.v2.Table\"k\n\x0e\x44\x61taTypeParams\x12\x31\n\x07\x64\x65\x63imal\x18\x01 \x01(\x0b\x32\x1e.fivetran_sdk.v2.DecimalParamsH\x00\x12\x1c\n\x12string_byte_length\x18\x02 \x01(\x05H\x00\x42\x08\n\x06params\"1\n\rDecimalParams\x12\x11\n\tprecision\x18\x01 \x01(\r\x12\r\n\x05scale\x18\x02 \x01(\r\"\xab\x03\n\tValueType\x12\x0e\n\x04null\x18\x01 \x01(\x08H\x00\x12\x0e\n\x04\x62ool\x18\x02 \x01(\x08H\x00\x12\x0f\n\x05short\x18\x03 \x01(\x05H\x00\x12\r\n\x03int\x18\x04 \x01(\x05H\x00\x12\x0e\n\x04long\x18\x05 \x01(\x03H\x00\x12\x0f\n\x05\x66loat\x18\x06 \x01(\x02H\x00\x12\x10\n\x06\x64ouble\x18\x07 \x01(\x01H\x00\x12\x30\n\nnaive_date\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x00\x12\x34\n\x0enaive_datetime\x18\t \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x00\x12\x32\n\x0cutc_datetime\x18\n \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x00\x12\x11\n\x07\x64\x65\x63imal\x18\x0b \x01(\tH\x00\x12\x10\n\x06\x62inary\x18\x0c \x01(\x0cH\x00\x12\x10\n\x06string\x18\r \x01(\tH\x00\x12\x0e\n\x04json\x18\x0e \x01(\tH\x00\x12\r\n\x03xml\x18\x0f \x01(\tH\x00\x12\x30\n\nnaive_time\x18\x10 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x00\x42\x07\n\x05inner\"?\n\x05Table\x12\x0c\n\x04name\x18\x01 \x01(\t\x12(\n\x07\x63olumns\x18\x02 \x03(\x0b\x32\x17.fivetran_sdk.v2.Column\"\x95\x01\n\x06\x43olumn\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\'\n\x04type\x18\x02 \x01(\x0e\x32\x19.fivetran_sdk.v2.DataType\x12\x13\n\x0bprimary_key\x18\x03 \x01(\x08\x12\x34\n\x06params\x18\x04 \x01(\x0b\x32\x1f.fivetran_sdk.v2.DataTypeParamsH\x00\x88\x01\x01\x42\t\n\x07_params\"\x1a\n\x07Warning\x12\x0f\n\x07message\x18\x01 \x01(\t\"\x17\n\x04Task\x12\x0f\n\x07message\x18\x01 \x01(\t*4\n\tTextField\x12\r\n\tPlainText\x10\x00\x12\x0c\n\x08Password\x10\x01\x12\n\n\x06Hidden\x10\x02*\xdb\x01\n\x08\x44\x61taType\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x0b\n\x07\x42OOLEAN\x10\x01\x12\t\n\x05SHORT\x10\x02\x12\x07\n\x03INT\x10\x03\x12\x08\n\x04LONG\x10\x04\x12\x0b\n\x07\x44\x45\x43IMAL\x10\x05\x12\t\n\x05\x46LOAT\x10\x06\x12\n\n\x06\x44OUBLE\x10\x07\x12\x0e\n\nNAIVE_DATE\x10\x08\x12\x12\n\x0eNAIVE_DATETIME\x10\t\x12\x10\n\x0cUTC_DATETIME\x10\n\x12\n\n\x06\x42INARY\x10\x0b\x12\x07\n\x03XML\x10\x0c\x12\n\n\x06STRING\x10\r\x12\x08\n\x04JSON\x10\x0e\x12\x0e\n\nNAIVE_TIME\x10\x0f*>\n\nRecordType\x12\n\n\x06UPSERT\x10\x00\x12\n\n\x06UPDATE\x10\x01\x12\n\n\x06\x44\x45LETE\x10\x02\x12\x0c\n\x08TRUNCATE\x10\x03\x42\"H\x01P\x01Z\x1c\x66ivetran.com/fivetran_sdk_v2b\x06proto3')
|
19
29
|
|
20
30
|
_globals = globals()
|
21
31
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
22
32
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'common_pb2', _globals)
|
23
|
-
if _descriptor._USE_C_DESCRIPTORS
|
24
|
-
_globals['DESCRIPTOR'].
|
25
|
-
_globals['DESCRIPTOR']._serialized_options = b'H\001P\001Z\
|
26
|
-
_globals['_TESTREQUEST_CONFIGURATIONENTRY'].
|
33
|
+
if not _descriptor._USE_C_DESCRIPTORS:
|
34
|
+
_globals['DESCRIPTOR']._loaded_options = None
|
35
|
+
_globals['DESCRIPTOR']._serialized_options = b'H\001P\001Z\034fivetran.com/fivetran_sdk_v2'
|
36
|
+
_globals['_TESTREQUEST_CONFIGURATIONENTRY']._loaded_options = None
|
27
37
|
_globals['_TESTREQUEST_CONFIGURATIONENTRY']._serialized_options = b'8\001'
|
28
|
-
_globals['_TEXTFIELD']._serialized_start=
|
29
|
-
_globals['_TEXTFIELD']._serialized_end=
|
30
|
-
_globals['_DATATYPE']._serialized_start=
|
31
|
-
_globals['_DATATYPE']._serialized_end=
|
32
|
-
_globals['
|
33
|
-
_globals['
|
34
|
-
_globals['_CONFIGURATIONFORMREQUEST']._serialized_start=
|
35
|
-
_globals['_CONFIGURATIONFORMREQUEST']._serialized_end=
|
36
|
-
_globals['_CONFIGURATIONFORMRESPONSE']._serialized_start=
|
37
|
-
_globals['_CONFIGURATIONFORMRESPONSE']._serialized_end=
|
38
|
-
_globals['_FORMFIELD']._serialized_start=
|
39
|
-
_globals['_FORMFIELD']._serialized_end=
|
40
|
-
_globals['
|
41
|
-
_globals['
|
42
|
-
_globals['
|
43
|
-
_globals['
|
44
|
-
_globals['
|
45
|
-
_globals['
|
46
|
-
_globals['
|
47
|
-
_globals['
|
48
|
-
_globals['
|
49
|
-
_globals['
|
50
|
-
_globals['
|
51
|
-
_globals['
|
52
|
-
_globals['
|
53
|
-
_globals['
|
54
|
-
_globals['
|
55
|
-
_globals['
|
56
|
-
_globals['
|
57
|
-
_globals['
|
58
|
-
_globals['
|
59
|
-
_globals['
|
60
|
-
_globals['
|
61
|
-
_globals['
|
62
|
-
_globals['
|
63
|
-
_globals['
|
64
|
-
_globals['
|
65
|
-
_globals['
|
38
|
+
_globals['_TEXTFIELD']._serialized_start=2352
|
39
|
+
_globals['_TEXTFIELD']._serialized_end=2404
|
40
|
+
_globals['_DATATYPE']._serialized_start=2407
|
41
|
+
_globals['_DATATYPE']._serialized_end=2626
|
42
|
+
_globals['_RECORDTYPE']._serialized_start=2628
|
43
|
+
_globals['_RECORDTYPE']._serialized_end=2690
|
44
|
+
_globals['_CONFIGURATIONFORMREQUEST']._serialized_start=66
|
45
|
+
_globals['_CONFIGURATIONFORMREQUEST']._serialized_end=92
|
46
|
+
_globals['_CONFIGURATIONFORMRESPONSE']._serialized_start=95
|
47
|
+
_globals['_CONFIGURATIONFORMRESPONSE']._serialized_end=288
|
48
|
+
_globals['_FORMFIELD']._serialized_start=291
|
49
|
+
_globals['_FORMFIELD']._serialized_end=733
|
50
|
+
_globals['_CONDITIONALFIELDS']._serialized_start=735
|
51
|
+
_globals['_CONDITIONALFIELDS']._serialized_end=855
|
52
|
+
_globals['_VISIBILITYCONDITION']._serialized_start=858
|
53
|
+
_globals['_VISIBILITYCONDITION']._serialized_end=989
|
54
|
+
_globals['_DROPDOWNFIELD']._serialized_start=991
|
55
|
+
_globals['_DROPDOWNFIELD']._serialized_end=1030
|
56
|
+
_globals['_TOGGLEFIELD']._serialized_start=1032
|
57
|
+
_globals['_TOGGLEFIELD']._serialized_end=1045
|
58
|
+
_globals['_CONFIGURATIONTEST']._serialized_start=1047
|
59
|
+
_globals['_CONFIGURATIONTEST']._serialized_end=1095
|
60
|
+
_globals['_TESTREQUEST']._serialized_start=1098
|
61
|
+
_globals['_TESTREQUEST']._serialized_end=1251
|
62
|
+
_globals['_TESTREQUEST_CONFIGURATIONENTRY']._serialized_start=1199
|
63
|
+
_globals['_TESTREQUEST_CONFIGURATIONENTRY']._serialized_end=1251
|
64
|
+
_globals['_TESTRESPONSE']._serialized_start=1253
|
65
|
+
_globals['_TESTRESPONSE']._serialized_end=1317
|
66
|
+
_globals['_SCHEMALIST']._serialized_start=1319
|
67
|
+
_globals['_SCHEMALIST']._serialized_end=1373
|
68
|
+
_globals['_TABLELIST']._serialized_start=1375
|
69
|
+
_globals['_TABLELIST']._serialized_end=1426
|
70
|
+
_globals['_SCHEMA']._serialized_start=1428
|
71
|
+
_globals['_SCHEMA']._serialized_end=1490
|
72
|
+
_globals['_DATATYPEPARAMS']._serialized_start=1492
|
73
|
+
_globals['_DATATYPEPARAMS']._serialized_end=1599
|
74
|
+
_globals['_DECIMALPARAMS']._serialized_start=1601
|
75
|
+
_globals['_DECIMALPARAMS']._serialized_end=1650
|
76
|
+
_globals['_VALUETYPE']._serialized_start=1653
|
77
|
+
_globals['_VALUETYPE']._serialized_end=2080
|
78
|
+
_globals['_TABLE']._serialized_start=2082
|
79
|
+
_globals['_TABLE']._serialized_end=2145
|
80
|
+
_globals['_COLUMN']._serialized_start=2148
|
81
|
+
_globals['_COLUMN']._serialized_end=2297
|
82
|
+
_globals['_WARNING']._serialized_start=2299
|
83
|
+
_globals['_WARNING']._serialized_end=2325
|
84
|
+
_globals['_TASK']._serialized_start=2327
|
85
|
+
_globals['_TASK']._serialized_end=2350
|
66
86
|
# @@protoc_insertion_point(module_scope)
|
@@ -30,13 +30,14 @@ class DataType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
|
30
30
|
XML: _ClassVar[DataType]
|
31
31
|
STRING: _ClassVar[DataType]
|
32
32
|
JSON: _ClassVar[DataType]
|
33
|
+
NAIVE_TIME: _ClassVar[DataType]
|
33
34
|
|
34
|
-
class
|
35
|
+
class RecordType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
35
36
|
__slots__ = ()
|
36
|
-
UPSERT: _ClassVar[
|
37
|
-
UPDATE: _ClassVar[
|
38
|
-
DELETE: _ClassVar[
|
39
|
-
TRUNCATE: _ClassVar[
|
37
|
+
UPSERT: _ClassVar[RecordType]
|
38
|
+
UPDATE: _ClassVar[RecordType]
|
39
|
+
DELETE: _ClassVar[RecordType]
|
40
|
+
TRUNCATE: _ClassVar[RecordType]
|
40
41
|
PlainText: TextField
|
41
42
|
Password: TextField
|
42
43
|
Hidden: TextField
|
@@ -55,10 +56,11 @@ BINARY: DataType
|
|
55
56
|
XML: DataType
|
56
57
|
STRING: DataType
|
57
58
|
JSON: DataType
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
59
|
+
NAIVE_TIME: DataType
|
60
|
+
UPSERT: RecordType
|
61
|
+
UPDATE: RecordType
|
62
|
+
DELETE: RecordType
|
63
|
+
TRUNCATE: RecordType
|
62
64
|
|
63
65
|
class ConfigurationFormRequest(_message.Message):
|
64
66
|
__slots__ = ()
|
@@ -77,7 +79,7 @@ class ConfigurationFormResponse(_message.Message):
|
|
77
79
|
def __init__(self, schema_selection_supported: bool = ..., table_selection_supported: bool = ..., fields: _Optional[_Iterable[_Union[FormField, _Mapping]]] = ..., tests: _Optional[_Iterable[_Union[ConfigurationTest, _Mapping]]] = ...) -> None: ...
|
78
80
|
|
79
81
|
class FormField(_message.Message):
|
80
|
-
__slots__ = ("name", "label", "required", "description", "text_field", "dropdown_field", "toggle_field")
|
82
|
+
__slots__ = ("name", "label", "required", "description", "text_field", "dropdown_field", "toggle_field", "conditional_fields", "default_value", "placeholder")
|
81
83
|
NAME_FIELD_NUMBER: _ClassVar[int]
|
82
84
|
LABEL_FIELD_NUMBER: _ClassVar[int]
|
83
85
|
REQUIRED_FIELD_NUMBER: _ClassVar[int]
|
@@ -85,6 +87,9 @@ class FormField(_message.Message):
|
|
85
87
|
TEXT_FIELD_FIELD_NUMBER: _ClassVar[int]
|
86
88
|
DROPDOWN_FIELD_FIELD_NUMBER: _ClassVar[int]
|
87
89
|
TOGGLE_FIELD_FIELD_NUMBER: _ClassVar[int]
|
90
|
+
CONDITIONAL_FIELDS_FIELD_NUMBER: _ClassVar[int]
|
91
|
+
DEFAULT_VALUE_FIELD_NUMBER: _ClassVar[int]
|
92
|
+
PLACEHOLDER_FIELD_NUMBER: _ClassVar[int]
|
88
93
|
name: str
|
89
94
|
label: str
|
90
95
|
required: bool
|
@@ -92,7 +97,30 @@ class FormField(_message.Message):
|
|
92
97
|
text_field: TextField
|
93
98
|
dropdown_field: DropdownField
|
94
99
|
toggle_field: ToggleField
|
95
|
-
|
100
|
+
conditional_fields: ConditionalFields
|
101
|
+
default_value: str
|
102
|
+
placeholder: str
|
103
|
+
def __init__(self, name: _Optional[str] = ..., label: _Optional[str] = ..., required: bool = ..., description: _Optional[str] = ..., text_field: _Optional[_Union[TextField, str]] = ..., dropdown_field: _Optional[_Union[DropdownField, _Mapping]] = ..., toggle_field: _Optional[_Union[ToggleField, _Mapping]] = ..., conditional_fields: _Optional[_Union[ConditionalFields, _Mapping]] = ..., default_value: _Optional[str] = ..., placeholder: _Optional[str] = ...) -> None: ...
|
104
|
+
|
105
|
+
class ConditionalFields(_message.Message):
|
106
|
+
__slots__ = ("condition", "fields")
|
107
|
+
CONDITION_FIELD_NUMBER: _ClassVar[int]
|
108
|
+
FIELDS_FIELD_NUMBER: _ClassVar[int]
|
109
|
+
condition: VisibilityCondition
|
110
|
+
fields: _containers.RepeatedCompositeFieldContainer[FormField]
|
111
|
+
def __init__(self, condition: _Optional[_Union[VisibilityCondition, _Mapping]] = ..., fields: _Optional[_Iterable[_Union[FormField, _Mapping]]] = ...) -> None: ...
|
112
|
+
|
113
|
+
class VisibilityCondition(_message.Message):
|
114
|
+
__slots__ = ("condition_field", "bool_value", "string_value", "empty_value")
|
115
|
+
CONDITION_FIELD_FIELD_NUMBER: _ClassVar[int]
|
116
|
+
BOOL_VALUE_FIELD_NUMBER: _ClassVar[int]
|
117
|
+
STRING_VALUE_FIELD_NUMBER: _ClassVar[int]
|
118
|
+
EMPTY_VALUE_FIELD_NUMBER: _ClassVar[int]
|
119
|
+
condition_field: str
|
120
|
+
bool_value: bool
|
121
|
+
string_value: str
|
122
|
+
empty_value: bool
|
123
|
+
def __init__(self, condition_field: _Optional[str] = ..., bool_value: bool = ..., string_value: _Optional[str] = ..., empty_value: bool = ...) -> None: ...
|
96
124
|
|
97
125
|
class DropdownField(_message.Message):
|
98
126
|
__slots__ = ("dropdown_field",)
|
@@ -155,6 +183,14 @@ class Schema(_message.Message):
|
|
155
183
|
tables: _containers.RepeatedCompositeFieldContainer[Table]
|
156
184
|
def __init__(self, name: _Optional[str] = ..., tables: _Optional[_Iterable[_Union[Table, _Mapping]]] = ...) -> None: ...
|
157
185
|
|
186
|
+
class DataTypeParams(_message.Message):
|
187
|
+
__slots__ = ("decimal", "string_byte_length")
|
188
|
+
DECIMAL_FIELD_NUMBER: _ClassVar[int]
|
189
|
+
STRING_BYTE_LENGTH_FIELD_NUMBER: _ClassVar[int]
|
190
|
+
decimal: DecimalParams
|
191
|
+
string_byte_length: int
|
192
|
+
def __init__(self, decimal: _Optional[_Union[DecimalParams, _Mapping]] = ..., string_byte_length: _Optional[int] = ...) -> None: ...
|
193
|
+
|
158
194
|
class DecimalParams(_message.Message):
|
159
195
|
__slots__ = ("precision", "scale")
|
160
196
|
PRECISION_FIELD_NUMBER: _ClassVar[int]
|
@@ -164,7 +200,7 @@ class DecimalParams(_message.Message):
|
|
164
200
|
def __init__(self, precision: _Optional[int] = ..., scale: _Optional[int] = ...) -> None: ...
|
165
201
|
|
166
202
|
class ValueType(_message.Message):
|
167
|
-
__slots__ = ("null", "bool", "short", "int", "long", "float", "double", "naive_date", "naive_datetime", "utc_datetime", "decimal", "binary", "string", "json", "xml")
|
203
|
+
__slots__ = ("null", "bool", "short", "int", "long", "float", "double", "naive_date", "naive_datetime", "utc_datetime", "decimal", "binary", "string", "json", "xml", "naive_time")
|
168
204
|
NULL_FIELD_NUMBER: _ClassVar[int]
|
169
205
|
BOOL_FIELD_NUMBER: _ClassVar[int]
|
170
206
|
SHORT_FIELD_NUMBER: _ClassVar[int]
|
@@ -180,6 +216,7 @@ class ValueType(_message.Message):
|
|
180
216
|
STRING_FIELD_NUMBER: _ClassVar[int]
|
181
217
|
JSON_FIELD_NUMBER: _ClassVar[int]
|
182
218
|
XML_FIELD_NUMBER: _ClassVar[int]
|
219
|
+
NAIVE_TIME_FIELD_NUMBER: _ClassVar[int]
|
183
220
|
null: bool
|
184
221
|
bool: bool
|
185
222
|
short: int
|
@@ -195,7 +232,8 @@ class ValueType(_message.Message):
|
|
195
232
|
string: str
|
196
233
|
json: str
|
197
234
|
xml: str
|
198
|
-
|
235
|
+
naive_time: _timestamp_pb2.Timestamp
|
236
|
+
def __init__(self, null: bool = ..., bool: bool = ..., short: _Optional[int] = ..., int: _Optional[int] = ..., long: _Optional[int] = ..., float: _Optional[float] = ..., double: _Optional[float] = ..., naive_date: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., naive_datetime: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., utc_datetime: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., decimal: _Optional[str] = ..., binary: _Optional[bytes] = ..., string: _Optional[str] = ..., json: _Optional[str] = ..., xml: _Optional[str] = ..., naive_time: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ...
|
199
237
|
|
200
238
|
class Table(_message.Message):
|
201
239
|
__slots__ = ("name", "columns")
|
@@ -206,13 +244,25 @@ class Table(_message.Message):
|
|
206
244
|
def __init__(self, name: _Optional[str] = ..., columns: _Optional[_Iterable[_Union[Column, _Mapping]]] = ...) -> None: ...
|
207
245
|
|
208
246
|
class Column(_message.Message):
|
209
|
-
__slots__ = ("name", "type", "primary_key", "
|
247
|
+
__slots__ = ("name", "type", "primary_key", "params")
|
210
248
|
NAME_FIELD_NUMBER: _ClassVar[int]
|
211
249
|
TYPE_FIELD_NUMBER: _ClassVar[int]
|
212
250
|
PRIMARY_KEY_FIELD_NUMBER: _ClassVar[int]
|
213
|
-
|
251
|
+
PARAMS_FIELD_NUMBER: _ClassVar[int]
|
214
252
|
name: str
|
215
253
|
type: DataType
|
216
254
|
primary_key: bool
|
217
|
-
|
218
|
-
def __init__(self, name: _Optional[str] = ..., type: _Optional[_Union[DataType, str]] = ..., primary_key: bool = ...,
|
255
|
+
params: DataTypeParams
|
256
|
+
def __init__(self, name: _Optional[str] = ..., type: _Optional[_Union[DataType, str]] = ..., primary_key: bool = ..., params: _Optional[_Union[DataTypeParams, _Mapping]] = ...) -> None: ...
|
257
|
+
|
258
|
+
class Warning(_message.Message):
|
259
|
+
__slots__ = ("message",)
|
260
|
+
MESSAGE_FIELD_NUMBER: _ClassVar[int]
|
261
|
+
message: str
|
262
|
+
def __init__(self, message: _Optional[str] = ...) -> None: ...
|
263
|
+
|
264
|
+
class Task(_message.Message):
|
265
|
+
__slots__ = ("message",)
|
266
|
+
MESSAGE_FIELD_NUMBER: _ClassVar[int]
|
267
|
+
message: str
|
268
|
+
def __init__(self, message: _Optional[str] = ...) -> None: ...
|
@@ -1,4 +1,24 @@
|
|
1
1
|
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
2
2
|
"""Client and server classes corresponding to protobuf-defined services."""
|
3
3
|
import grpc
|
4
|
+
import warnings
|
4
5
|
|
6
|
+
|
7
|
+
GRPC_GENERATED_VERSION = '1.71.0'
|
8
|
+
GRPC_VERSION = grpc.__version__
|
9
|
+
_version_not_supported = False
|
10
|
+
|
11
|
+
try:
|
12
|
+
from grpc._utilities import first_version_is_lower
|
13
|
+
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
|
14
|
+
except ImportError:
|
15
|
+
_version_not_supported = True
|
16
|
+
|
17
|
+
if _version_not_supported:
|
18
|
+
raise RuntimeError(
|
19
|
+
f'The grpc package installed is at version {GRPC_VERSION},'
|
20
|
+
+ f' but the generated code in common_pb2_grpc.py depends on'
|
21
|
+
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
|
22
|
+
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
|
23
|
+
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
|
24
|
+
)
|