fivetran-connector-sdk 1.4.4__py3-none-any.whl → 1.4.6__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 +62 -1621
- fivetran_connector_sdk/connector_helper.py +928 -0
- fivetran_connector_sdk/constants.py +70 -0
- fivetran_connector_sdk/helpers.py +318 -0
- fivetran_connector_sdk/logger.py +91 -0
- fivetran_connector_sdk/operations.py +278 -0
- {fivetran_connector_sdk-1.4.4.dist-info → fivetran_connector_sdk-1.4.6.dist-info}/METADATA +6 -5
- fivetran_connector_sdk-1.4.6.dist-info/RECORD +18 -0
- {fivetran_connector_sdk-1.4.4.dist-info → fivetran_connector_sdk-1.4.6.dist-info}/WHEEL +1 -1
- fivetran_connector_sdk-1.4.4.dist-info/RECORD +0 -13
- {fivetran_connector_sdk-1.4.4.dist-info → fivetran_connector_sdk-1.4.6.dist-info}/entry_points.txt +0 -0
- {fivetran_connector_sdk-1.4.4.dist-info → fivetran_connector_sdk-1.4.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,928 @@
|
|
1
|
+
import os
|
2
|
+
import re
|
3
|
+
import ast
|
4
|
+
import json
|
5
|
+
import time
|
6
|
+
import socket
|
7
|
+
import platform
|
8
|
+
import subprocess
|
9
|
+
import requests as rq
|
10
|
+
|
11
|
+
from http import HTTPStatus
|
12
|
+
from typing import Optional, Tuple
|
13
|
+
from zipfile import ZipFile, ZIP_DEFLATED
|
14
|
+
|
15
|
+
from fivetran_connector_sdk.protos import common_pb2
|
16
|
+
|
17
|
+
from fivetran_connector_sdk import constants
|
18
|
+
from fivetran_connector_sdk.logger import Logging
|
19
|
+
from fivetran_connector_sdk.helpers import (
|
20
|
+
print_library_log,
|
21
|
+
get_renamed_table_name,
|
22
|
+
get_renamed_column_name
|
23
|
+
)
|
24
|
+
from fivetran_connector_sdk.constants import (
|
25
|
+
OS_MAP,
|
26
|
+
ARCH_MAP,
|
27
|
+
WIN_OS,
|
28
|
+
X64,
|
29
|
+
TESTER_FILENAME,
|
30
|
+
UPLOAD_FILENAME,
|
31
|
+
LAST_VERSION_CHECK_FILE,
|
32
|
+
ROOT_LOCATION,
|
33
|
+
CONFIG_FILE,
|
34
|
+
OUTPUT_FILES_DIR,
|
35
|
+
REQUIREMENTS_TXT,
|
36
|
+
PYPI_PACKAGE_DETAILS_URL,
|
37
|
+
ONE_DAY_IN_SEC,
|
38
|
+
MAX_RETRIES,
|
39
|
+
VIRTUAL_ENV_CONFIG,
|
40
|
+
ROOT_FILENAME,
|
41
|
+
EXCLUDED_DIRS,
|
42
|
+
EXCLUDED_PIPREQS_DIRS,
|
43
|
+
INSTALLATION_SCRIPT,
|
44
|
+
INSTALLATION_SCRIPT_MISSING_MESSAGE,
|
45
|
+
DRIVERS,
|
46
|
+
UTF_8,
|
47
|
+
CONNECTION_SCHEMA_NAME_PATTERN,
|
48
|
+
TABLES,
|
49
|
+
)
|
50
|
+
|
51
|
+
def check_newer_version(version: str):
|
52
|
+
"""Periodically checks for a newer version of the SDK and notifies the user if one is available."""
|
53
|
+
tester_root_dir = tester_root_dir_helper()
|
54
|
+
last_check_file_path = os.path.join(tester_root_dir, LAST_VERSION_CHECK_FILE)
|
55
|
+
if not os.path.isdir(tester_root_dir):
|
56
|
+
os.makedirs(tester_root_dir, exist_ok=True)
|
57
|
+
|
58
|
+
if os.path.isfile(last_check_file_path):
|
59
|
+
# Is it time to check again?
|
60
|
+
with open(last_check_file_path, 'r', encoding=UTF_8) as f_in:
|
61
|
+
timestamp = int(f_in.read())
|
62
|
+
if (int(time.time()) - timestamp) < ONE_DAY_IN_SEC:
|
63
|
+
return
|
64
|
+
|
65
|
+
for index in range(MAX_RETRIES):
|
66
|
+
try:
|
67
|
+
# check version and save current time
|
68
|
+
response = rq.get(PYPI_PACKAGE_DETAILS_URL)
|
69
|
+
response.raise_for_status()
|
70
|
+
data = json.loads(response.text)
|
71
|
+
latest_version = data["info"]["version"]
|
72
|
+
if version < latest_version:
|
73
|
+
print_library_log(f"[notice] A new release of 'fivetran-connector-sdk' is available: {latest_version}")
|
74
|
+
print_library_log("[notice] To update, run: pip install --upgrade fivetran-connector-sdk")
|
75
|
+
|
76
|
+
with open(last_check_file_path, 'w', encoding=UTF_8) as f_out:
|
77
|
+
f_out.write(f"{int(time.time())}")
|
78
|
+
break
|
79
|
+
except Exception:
|
80
|
+
retry_after = 2 ** index
|
81
|
+
print_library_log(f"Unable to check if a newer version of `fivetran-connector-sdk` is available. "
|
82
|
+
f"Retrying again after {retry_after} seconds", Logging.Level.WARNING)
|
83
|
+
time.sleep(retry_after)
|
84
|
+
|
85
|
+
|
86
|
+
def tester_root_dir_helper() -> str:
|
87
|
+
"""Returns the root directory for the tester."""
|
88
|
+
return os.path.join(os.path.expanduser("~"), ROOT_LOCATION)
|
89
|
+
|
90
|
+
def _warn_exit_usage(filename, line_no, func):
|
91
|
+
print_library_log(f"Avoid using {func} to exit from the Python code as this can cause the connector to become stuck. Throw a error if required " +
|
92
|
+
f"at: {filename}:{line_no}. See the Technical Reference for details: https://fivetran.com/docs/connector-sdk/technical-reference#handlingexceptions",
|
93
|
+
Logging.Level.WARNING)
|
94
|
+
|
95
|
+
def exit_check(project_path):
|
96
|
+
"""Checks for the presence of 'exit()' in the calling code.
|
97
|
+
Args:
|
98
|
+
project_path: The absolute project_path to check exit in the connector.py file in the project.
|
99
|
+
"""
|
100
|
+
# We expect the connector.py to catch errors or throw exceptions
|
101
|
+
# This is a warning shown to let the customer know that we expect either the yield call or error thrown
|
102
|
+
# exit() or sys.exit() in between some yields can cause the connector to be stuck without processing further upsert calls
|
103
|
+
|
104
|
+
filepath = os.path.join(project_path, ROOT_FILENAME)
|
105
|
+
with open(filepath, "r", encoding=UTF_8) as f:
|
106
|
+
try:
|
107
|
+
tree = ast.parse(f.read())
|
108
|
+
for node in ast.walk(tree):
|
109
|
+
if isinstance(node, ast.Call):
|
110
|
+
if isinstance(node.func, ast.Name) and node.func.id == "exit":
|
111
|
+
_warn_exit_usage(ROOT_FILENAME, node.lineno, "exit()")
|
112
|
+
elif isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name):
|
113
|
+
if node.func.attr == "_exit" and node.func.value.id == "os":
|
114
|
+
_warn_exit_usage(ROOT_FILENAME, node.lineno, "os._exit()")
|
115
|
+
if node.func.attr == "exit" and node.func.value.id == "sys":
|
116
|
+
_warn_exit_usage(ROOT_FILENAME, node.lineno, "sys.exit()")
|
117
|
+
except SyntaxError as e:
|
118
|
+
print_library_log(f"SyntaxError in {ROOT_FILENAME}: {e}", Logging.Level.SEVERE)
|
119
|
+
|
120
|
+
|
121
|
+
def check_dict(incoming: dict, string_only: bool = False) -> dict:
|
122
|
+
"""Validates the incoming dictionary.
|
123
|
+
Args:
|
124
|
+
incoming (dict): The dictionary to validate.
|
125
|
+
string_only (bool): Whether to allow only string values.
|
126
|
+
|
127
|
+
Returns:
|
128
|
+
dict: The validated dictionary.
|
129
|
+
"""
|
130
|
+
|
131
|
+
if not incoming:
|
132
|
+
return {}
|
133
|
+
|
134
|
+
if not isinstance(incoming, dict):
|
135
|
+
raise ValueError(
|
136
|
+
"Configuration must be provided as a JSON dictionary. Please check your input. Reference: https://fivetran.com/docs/connectors/connector-sdk/detailed-guide#workingwithconfigurationjsonfile")
|
137
|
+
|
138
|
+
if string_only:
|
139
|
+
for k, v in incoming.items():
|
140
|
+
if not isinstance(v, str):
|
141
|
+
print_library_log(
|
142
|
+
"All values in the configuration must be STRING. Please check your configuration and ensure that every value is a STRING.", Logging.Level.SEVERE)
|
143
|
+
os._exit(1)
|
144
|
+
|
145
|
+
return incoming
|
146
|
+
|
147
|
+
|
148
|
+
def is_connection_name_valid(connection: str):
|
149
|
+
"""Validates if the incoming connection schema name is valid or not.
|
150
|
+
Args:
|
151
|
+
connection (str): The connection schema name being validated.
|
152
|
+
|
153
|
+
Returns:
|
154
|
+
bool: True if connection name is valid.
|
155
|
+
"""
|
156
|
+
|
157
|
+
pattern = re.compile(CONNECTION_SCHEMA_NAME_PATTERN)
|
158
|
+
return pattern.match(connection)
|
159
|
+
|
160
|
+
|
161
|
+
def log_unused_deps_error(package_name: str, version: str):
|
162
|
+
print_library_log(f"Please remove `{package_name}` from requirements.txt."
|
163
|
+
f" The latest version of `{package_name}` is always available when executing your code."
|
164
|
+
f" Current version: {version}", Logging.Level.SEVERE)
|
165
|
+
|
166
|
+
|
167
|
+
def validate_deploy_parameters(connection, deploy_key):
|
168
|
+
if not deploy_key or not connection:
|
169
|
+
print_library_log("The deploy command needs the following parameters:"
|
170
|
+
"\n\tRequired:\n"
|
171
|
+
"\t\t--api-key <BASE64-ENCODED-FIVETRAN-API-KEY-FOR-DEPLOYMENT>\n"
|
172
|
+
"\t\t--connection <VALID-CONNECTOR-SCHEMA_NAME>\n"
|
173
|
+
"\t(Optional):\n"
|
174
|
+
"\t\t--destination <DESTINATION_NAME> (Becomes required if there are multiple destinations)\n"
|
175
|
+
"\t\t--configuration <CONFIGURATION_FILE> (Completely replaces the existing configuration)", Logging.Level.SEVERE)
|
176
|
+
os._exit(1)
|
177
|
+
elif not is_connection_name_valid(connection):
|
178
|
+
print_library_log(f"Connection name: {connection} is invalid!\n The connection name should start with an "
|
179
|
+
f"underscore or a lowercase letter (a-z), followed by any combination of underscores, lowercase "
|
180
|
+
f"letters, or digits (0-9). Uppercase characters are not allowed.", Logging.Level.SEVERE)
|
181
|
+
os._exit(1)
|
182
|
+
|
183
|
+
|
184
|
+
def is_port_in_use(port: int):
|
185
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
186
|
+
return s.connect_ex(('127.0.0.1', port)) == 0
|
187
|
+
|
188
|
+
|
189
|
+
def get_available_port():
|
190
|
+
for port in range(50051, 50061):
|
191
|
+
if not is_port_in_use(port):
|
192
|
+
return port
|
193
|
+
return None
|
194
|
+
|
195
|
+
|
196
|
+
def update_base_url_if_required():
|
197
|
+
config_file_path = os.path.join(tester_root_dir_helper(), CONFIG_FILE)
|
198
|
+
if os.path.isfile(config_file_path):
|
199
|
+
with open(config_file_path, 'r', encoding=UTF_8) as f:
|
200
|
+
data = json.load(f)
|
201
|
+
base_url = data.get('production_base_url')
|
202
|
+
if base_url is not None:
|
203
|
+
constants.PRODUCTION_BASE_URL = base_url
|
204
|
+
print_library_log(f"Updating PRODUCTION_BASE_URL to: {base_url}")
|
205
|
+
|
206
|
+
def fetch_requirements_from_file(file_path: str) -> list[str]:
|
207
|
+
"""Reads a requirements file and returns a list of dependencies.
|
208
|
+
|
209
|
+
Args:
|
210
|
+
file_path (str): The path to the requirements file.
|
211
|
+
|
212
|
+
Returns:
|
213
|
+
list[str]: A list of dependencies as strings.
|
214
|
+
"""
|
215
|
+
with open(file_path, 'r', encoding=UTF_8) as f:
|
216
|
+
return f.read().splitlines()
|
217
|
+
|
218
|
+
def fetch_requirements_as_dict(file_path: str) -> dict:
|
219
|
+
"""Converts a list of dependencies from the requirements file into a dictionary.
|
220
|
+
|
221
|
+
Args:
|
222
|
+
file_path (str): The path to the requirements file.
|
223
|
+
|
224
|
+
Returns:
|
225
|
+
dict: A dictionary where keys are package names (lowercased) and
|
226
|
+
values are the full dependency strings.
|
227
|
+
"""
|
228
|
+
requirements_dict = {}
|
229
|
+
if not os.path.exists(file_path):
|
230
|
+
return requirements_dict
|
231
|
+
for requirement in fetch_requirements_from_file(file_path):
|
232
|
+
requirement = requirement.strip()
|
233
|
+
if not requirement or requirement.startswith("#"): # Skip empty lines and comments
|
234
|
+
continue
|
235
|
+
try:
|
236
|
+
key = re.split(r"==|>=|<=|>|<", requirement)[0]
|
237
|
+
requirements_dict[key.lower().replace('-', '_')] = requirement.lower()
|
238
|
+
except ValueError:
|
239
|
+
print_library_log(f"Invalid requirement format: '{requirement}'", Logging.Level.SEVERE)
|
240
|
+
return requirements_dict
|
241
|
+
|
242
|
+
def validate_requirements_file(project_path: str, is_deploy: bool, version: str , force: bool = False,):
|
243
|
+
"""Validates the `requirements.txt` file against the project's actual dependencies.
|
244
|
+
|
245
|
+
This method generates a temporary requirements file using `pipreqs`, compares
|
246
|
+
it with the existing `requirements.txt`, and checks for version mismatches,
|
247
|
+
missing dependencies, and unused dependencies. It will issue warnings, errors,
|
248
|
+
or even terminate the process depending on whether it's being run for deployment.
|
249
|
+
|
250
|
+
Args:
|
251
|
+
project_path (str): The path to the project directory containing the `requirements.txt`.
|
252
|
+
is_deploy (bool): If `True`, the method will exit the process on critical errors.
|
253
|
+
version (str): The current version of the connector.
|
254
|
+
force (bool): Force update an existing connection.
|
255
|
+
|
256
|
+
"""
|
257
|
+
# Detect and exclude virtual environment directories
|
258
|
+
venv_dirs = [name for name in os.listdir(project_path)
|
259
|
+
if os.path.isdir(os.path.join(project_path, name)) and
|
260
|
+
VIRTUAL_ENV_CONFIG in os.listdir(os.path.join(project_path, name))]
|
261
|
+
|
262
|
+
ignored_dirs = EXCLUDED_PIPREQS_DIRS + venv_dirs if venv_dirs else EXCLUDED_PIPREQS_DIRS
|
263
|
+
|
264
|
+
# tmp_requirements is only generated when pipreqs command is successful
|
265
|
+
requirements_file_path = os.path.join(project_path, REQUIREMENTS_TXT)
|
266
|
+
tmp_requirements_file_path = os.path.join(project_path, 'tmp_requirements.txt')
|
267
|
+
# copying packages of requirements file to tmp file to handle pipreqs fail use-case
|
268
|
+
copy_requirements_file_to_tmp_requirements_file(requirements_file_path, tmp_requirements_file_path)
|
269
|
+
# Run the pipreqs command and capture stderr
|
270
|
+
attempt = 0
|
271
|
+
while attempt < MAX_RETRIES:
|
272
|
+
attempt += 1
|
273
|
+
result = subprocess.run(
|
274
|
+
["pipreqs", project_path, "--savepath", tmp_requirements_file_path, "--ignore", ",".join(ignored_dirs)],
|
275
|
+
stderr=subprocess.PIPE,
|
276
|
+
text=True # Ensures output is in string format
|
277
|
+
)
|
278
|
+
|
279
|
+
if result.returncode == 0:
|
280
|
+
break
|
281
|
+
|
282
|
+
print_library_log(f"Attempt {attempt}: pipreqs check failed.", Logging.Level.WARNING)
|
283
|
+
|
284
|
+
if attempt < MAX_RETRIES:
|
285
|
+
retry_after = 3 ** attempt
|
286
|
+
print_library_log(f"Retrying in {retry_after} seconds...", Logging.Level.SEVERE)
|
287
|
+
time.sleep(retry_after)
|
288
|
+
else:
|
289
|
+
print_library_log(f"pipreqs failed after {MAX_RETRIES} attempts with:", Logging.Level.SEVERE)
|
290
|
+
print_library_log(result.stderr, Logging.Level.SEVERE)
|
291
|
+
print_library_log(f"Skipping validation of requirements.txt due to error connecting to PyPI (Python Package Index) APIs. Continuing with {'deploy' if is_deploy else 'debug'}...", Logging.Level.SEVERE)
|
292
|
+
|
293
|
+
tmp_requirements = fetch_requirements_as_dict(tmp_requirements_file_path)
|
294
|
+
remove_unwanted_packages(tmp_requirements)
|
295
|
+
delete_file_if_exists(tmp_requirements_file_path)
|
296
|
+
|
297
|
+
# remove corrupt requirements listed by pipreqs
|
298
|
+
corrupt_requirements = [key for key in tmp_requirements if key.startswith("~")]
|
299
|
+
for requirement in corrupt_requirements:
|
300
|
+
del tmp_requirements[requirement]
|
301
|
+
|
302
|
+
update_version_requirements = False
|
303
|
+
update_missing_requirements = False
|
304
|
+
update_unused_requirements = False
|
305
|
+
if len(tmp_requirements) > 0:
|
306
|
+
requirements = load_or_add_requirements_file(requirements_file_path)
|
307
|
+
|
308
|
+
version_mismatch_deps = {key: tmp_requirements[key] for key in
|
309
|
+
(requirements.keys() & tmp_requirements.keys())
|
310
|
+
if requirements[key] != tmp_requirements[key]}
|
311
|
+
if version_mismatch_deps:
|
312
|
+
print_library_log("We recommend using the current stable version for the following libraries:", Logging.Level.WARNING)
|
313
|
+
print(version_mismatch_deps)
|
314
|
+
if is_deploy and not force:
|
315
|
+
confirm = input(
|
316
|
+
f"Would you like us to update {REQUIREMENTS_TXT} to the current stable versions of the dependent libraries? (Y/N):")
|
317
|
+
if confirm.lower() == "y":
|
318
|
+
update_version_requirements = True
|
319
|
+
for requirement in version_mismatch_deps:
|
320
|
+
requirements[requirement] = tmp_requirements[requirement]
|
321
|
+
print_library_log(
|
322
|
+
f"Successfully updated {REQUIREMENTS_TXT} to the current stable versions of the dependent libraries.")
|
323
|
+
elif confirm.lower() == "n":
|
324
|
+
print_library_log(f"Changes identified for libraries with version conflicts have been ignored. These changes have NOT been made to {REQUIREMENTS_TXT}.")
|
325
|
+
|
326
|
+
missing_deps = {key: tmp_requirements[key] for key in (tmp_requirements.keys() - requirements.keys())}
|
327
|
+
if missing_deps:
|
328
|
+
handle_missing_deps(missing_deps)
|
329
|
+
if is_deploy and not force:
|
330
|
+
confirm = input(
|
331
|
+
f"Would you like us to update {REQUIREMENTS_TXT} to add missing dependent libraries? (Y/N):")
|
332
|
+
if confirm.lower() == "n":
|
333
|
+
print_library_log(f"Changes identified as missing dependencies for libraries have been ignored. These changes have NOT been made to {REQUIREMENTS_TXT}.")
|
334
|
+
elif confirm.lower() == "y":
|
335
|
+
update_missing_requirements = True
|
336
|
+
for requirement in missing_deps:
|
337
|
+
requirements[requirement] = tmp_requirements[requirement]
|
338
|
+
print_library_log(f"Successfully added missing dependencies to {REQUIREMENTS_TXT}.")
|
339
|
+
|
340
|
+
unused_deps = list(requirements.keys() - tmp_requirements.keys())
|
341
|
+
if unused_deps:
|
342
|
+
handle_unused_deps(unused_deps, version)
|
343
|
+
if is_deploy and not force:
|
344
|
+
confirm = input(f"Would you like us to update {REQUIREMENTS_TXT} to remove the unused libraries? (Y/N):")
|
345
|
+
if confirm.lower() == "n":
|
346
|
+
if 'fivetran_connector_sdk' in unused_deps or 'requests' in unused_deps:
|
347
|
+
print_library_log(
|
348
|
+
f"Please fix your {REQUIREMENTS_TXT} file by removing pre-installed dependencies [fivetran_connector_sdk, requests] to proceed with the deployment.")
|
349
|
+
os._exit(1)
|
350
|
+
print_library_log(f"Changes identified for unused libraries have been ignored. These changes have NOT been made to {REQUIREMENTS_TXT}.")
|
351
|
+
elif confirm.lower() == "y":
|
352
|
+
update_unused_requirements = True
|
353
|
+
for requirement in unused_deps:
|
354
|
+
del requirements[requirement]
|
355
|
+
print_library_log(f"Successfully removed unused libraries from {REQUIREMENTS_TXT}.")
|
356
|
+
|
357
|
+
|
358
|
+
if update_version_requirements or update_missing_requirements or update_unused_requirements:
|
359
|
+
with open(requirements_file_path, "w", encoding=UTF_8) as file:
|
360
|
+
file.write("\n".join(requirements.values()))
|
361
|
+
print_library_log(f"`{REQUIREMENTS_TXT}` has been updated successfully.")
|
362
|
+
|
363
|
+
else:
|
364
|
+
if os.path.exists(requirements_file_path):
|
365
|
+
print_library_log(f"{REQUIREMENTS_TXT} is not required as no additional "
|
366
|
+
"Python libraries are required or all required libraries for "
|
367
|
+
"your code are pre-installed.", Logging.Level.WARNING)
|
368
|
+
with open(requirements_file_path, 'w') as file:
|
369
|
+
file.write("")
|
370
|
+
|
371
|
+
|
372
|
+
if is_deploy: print_library_log(f"Validation of {REQUIREMENTS_TXT} completed.")
|
373
|
+
|
374
|
+
def handle_unused_deps(unused_deps, version):
|
375
|
+
if 'fivetran_connector_sdk' in unused_deps:
|
376
|
+
log_unused_deps_error("fivetran_connector_sdk", version)
|
377
|
+
if 'requests' in unused_deps:
|
378
|
+
log_unused_deps_error("requests", "2.32.3")
|
379
|
+
print_library_log("The following dependencies are not needed, "
|
380
|
+
f"they are not used or already installed. Please remove them from {REQUIREMENTS_TXT}:", Logging.Level.WARNING)
|
381
|
+
print(*unused_deps)
|
382
|
+
|
383
|
+
def handle_missing_deps(missing_deps):
|
384
|
+
print_library_log(f"Please include the following dependency libraries in {REQUIREMENTS_TXT}, to be used by "
|
385
|
+
"Fivetran production. "
|
386
|
+
"For more information, please visit: "
|
387
|
+
"https://fivetran.com/docs/connectors/connector-sdk/detailed-guide"
|
388
|
+
"#workingwithrequirementstxtfile", Logging.Level.SEVERE)
|
389
|
+
print(*list(missing_deps.values()))
|
390
|
+
|
391
|
+
def load_or_add_requirements_file(requirements_file_path):
|
392
|
+
if os.path.exists(requirements_file_path):
|
393
|
+
requirements = fetch_requirements_as_dict(requirements_file_path)
|
394
|
+
else:
|
395
|
+
with open(requirements_file_path, 'w', encoding=UTF_8):
|
396
|
+
pass
|
397
|
+
requirements = {}
|
398
|
+
print_library_log("Adding `requirements.txt` file to your project folder.", Logging.Level.WARNING)
|
399
|
+
return requirements
|
400
|
+
|
401
|
+
def copy_requirements_file_to_tmp_requirements_file(requirements_file_path: str, tmp_requirements_file_path):
|
402
|
+
if os.path.exists(requirements_file_path):
|
403
|
+
requirements_file_content = fetch_requirements_from_file(requirements_file_path)
|
404
|
+
with open(tmp_requirements_file_path, 'w') as file:
|
405
|
+
file.write("\n".join(requirements_file_content))
|
406
|
+
|
407
|
+
def remove_unwanted_packages(requirements: dict):
|
408
|
+
# remove the `fivetran_connector_sdk` and `requests` packages from requirements as we already pre-installed them.
|
409
|
+
if requirements.get("fivetran_connector_sdk") is not None:
|
410
|
+
requirements.pop("fivetran_connector_sdk")
|
411
|
+
if requirements.get('requests') is not None:
|
412
|
+
requirements.pop("requests")
|
413
|
+
|
414
|
+
|
415
|
+
def upload_project(project_path: str, deploy_key: str, group_id: str, group_name: str, connection: str):
|
416
|
+
print_library_log(
|
417
|
+
f"Deploying '{project_path}' to connection '{connection}' in destination '{group_name}'.\n")
|
418
|
+
upload_file_path = create_upload_file(project_path)
|
419
|
+
upload_result = upload(
|
420
|
+
upload_file_path, deploy_key, group_id, connection)
|
421
|
+
delete_file_if_exists(upload_file_path)
|
422
|
+
if not upload_result:
|
423
|
+
os._exit(1)
|
424
|
+
|
425
|
+
|
426
|
+
def cleanup_uploaded_project(deploy_key: str, group_id: str, connection: str):
|
427
|
+
cleanup_result = cleanup_uploaded_code(deploy_key, group_id, connection)
|
428
|
+
if not cleanup_result:
|
429
|
+
os._exit(1)
|
430
|
+
|
431
|
+
def update_connection(args: dict, id: str, name: str, group: str, config: dict, deploy_key: str, hd_agent_id: str):
|
432
|
+
"""Updates the connection with the given ID, name, group, configuration, and deployment key.
|
433
|
+
|
434
|
+
Args:
|
435
|
+
args (dict): The command arguments.
|
436
|
+
id (str): The connection ID.
|
437
|
+
name (str): The connection name.
|
438
|
+
group (str): The group name.
|
439
|
+
config (dict): The configuration dictionary.
|
440
|
+
deploy_key (str): The deployment key.
|
441
|
+
hd_agent_id (str): The hybrid deployment agent ID within the Fivetran system.
|
442
|
+
"""
|
443
|
+
if not args.configuration:
|
444
|
+
del config["secrets_list"]
|
445
|
+
|
446
|
+
json_payload = {
|
447
|
+
"config": config,
|
448
|
+
"run_setup_tests": True
|
449
|
+
}
|
450
|
+
|
451
|
+
# hybrid_deployment_agent_id is optional when redeploying your connection.
|
452
|
+
# Customer can use it to change existing hybrid_deployment_agent_id.
|
453
|
+
if hd_agent_id:
|
454
|
+
json_payload["hybrid_deployment_agent_id"] = hd_agent_id
|
455
|
+
|
456
|
+
response = rq.patch(f"{constants.PRODUCTION_BASE_URL}/v1/connectors/{id}",
|
457
|
+
headers={"Authorization": f"Basic {deploy_key}"},
|
458
|
+
json=json_payload)
|
459
|
+
|
460
|
+
if response.ok and response.status_code == HTTPStatus.OK:
|
461
|
+
if are_setup_tests_failing(response):
|
462
|
+
handle_failing_tests_message_and_exit(response,"The connection was updated, but setup tests failed!")
|
463
|
+
else:
|
464
|
+
print_library_log(f"Connection '{name}' in group '{group}' updated successfully.", Logging.Level.INFO)
|
465
|
+
|
466
|
+
else:
|
467
|
+
print_library_log(
|
468
|
+
f"Unable to update Connection '{name}' in destination '{group}', failed with error: '{response.json()['message']}'.",
|
469
|
+
Logging.Level.SEVERE)
|
470
|
+
os._exit(1)
|
471
|
+
return response
|
472
|
+
|
473
|
+
def handle_failing_tests_message_and_exit(resp, log_message):
|
474
|
+
print_library_log(log_message, Logging.Level.SEVERE)
|
475
|
+
print_failing_setup_tests(resp)
|
476
|
+
connection_id = resp.json().get('data', {}).get('id')
|
477
|
+
print_library_log(f"Connection ID: {connection_id}")
|
478
|
+
print_library_log("Please try again with the deploy command after resolving the issue!")
|
479
|
+
os._exit(1)
|
480
|
+
|
481
|
+
def are_setup_tests_failing(response) -> bool:
|
482
|
+
"""Checks for failed setup tests in the response and returns True if any test has failed, otherwise False."""
|
483
|
+
response_json = response.json()
|
484
|
+
setup_tests = response_json.get("data", {}).get("setup_tests", [])
|
485
|
+
|
486
|
+
# Return True if any test has "FAILED" status, otherwise False
|
487
|
+
return any(test.get("status") == "FAILED" or test.get("status") == "JOB_FAILED" for test in setup_tests)
|
488
|
+
|
489
|
+
def print_failing_setup_tests(response):
|
490
|
+
"""Checks for failed setup tests in the response and print errors."""
|
491
|
+
response_json = response.json()
|
492
|
+
setup_tests = response_json.get("data", {}).get("setup_tests", [])
|
493
|
+
|
494
|
+
# Collect failed setup tests
|
495
|
+
failed_tests = [test for test in setup_tests if
|
496
|
+
test.get("status") == "FAILED" or test.get("status") == "JOB_FAILED"]
|
497
|
+
|
498
|
+
if failed_tests:
|
499
|
+
print_library_log("Following setup tests have failed!", Logging.Level.WARNING)
|
500
|
+
for test in failed_tests:
|
501
|
+
print_library_log(f"Test: {test.get('title')}", Logging.Level.WARNING)
|
502
|
+
print_library_log(f"Status: {test.get('status')}", Logging.Level.WARNING)
|
503
|
+
print_library_log(f"Message: {test.get('message')}", Logging.Level.WARNING)
|
504
|
+
|
505
|
+
def get_connection_id(name: str, group: str, group_id: str, deploy_key: str) -> Optional[Tuple[str, str]]:
|
506
|
+
"""Retrieves the connection ID for the specified connection schema name, group, and deployment key.
|
507
|
+
|
508
|
+
Args:
|
509
|
+
name (str): The connection name.
|
510
|
+
group (str): The group name.
|
511
|
+
group_id (str): The group ID.
|
512
|
+
deploy_key (str): The deployment key.
|
513
|
+
|
514
|
+
Returns:
|
515
|
+
str: The connection ID, or None
|
516
|
+
"""
|
517
|
+
resp = rq.get(f"{constants.PRODUCTION_BASE_URL}/v1/groups/{group_id}/connectors",
|
518
|
+
headers={"Authorization": f"Basic {deploy_key}"},
|
519
|
+
params={"schema": name})
|
520
|
+
if not resp.ok:
|
521
|
+
print_library_log(
|
522
|
+
f"Unable to fetch connection list in destination '{group}'", Logging.Level.SEVERE)
|
523
|
+
os._exit(1)
|
524
|
+
|
525
|
+
if resp.json()['data']['items']:
|
526
|
+
return resp.json()['data']['items'][0]['id'], resp.json()['data']['items'][0]['service']
|
527
|
+
|
528
|
+
return None
|
529
|
+
|
530
|
+
def create_connection(deploy_key: str, group_id: str, config: dict, hd_agent_id: str) -> rq.Response:
|
531
|
+
"""Creates a new connection with the given deployment key, group ID, and configuration.
|
532
|
+
|
533
|
+
Args:
|
534
|
+
deploy_key (str): The deployment key.
|
535
|
+
group_id (str): The group ID.
|
536
|
+
config (dict): The configuration dictionary.
|
537
|
+
hd_agent_id (str): The hybrid deployment agent ID within the Fivetran system.
|
538
|
+
|
539
|
+
Returns:
|
540
|
+
rq.Response: The response object.
|
541
|
+
"""
|
542
|
+
response = rq.post(f"{constants.PRODUCTION_BASE_URL}/v1/connectors",
|
543
|
+
headers={"Authorization": f"Basic {deploy_key}"},
|
544
|
+
json={
|
545
|
+
"group_id": group_id,
|
546
|
+
"service": "connector_sdk",
|
547
|
+
"config": config,
|
548
|
+
"paused": True,
|
549
|
+
"run_setup_tests": True,
|
550
|
+
"sync_frequency": "360",
|
551
|
+
"hybrid_deployment_agent_id": hd_agent_id
|
552
|
+
})
|
553
|
+
return response
|
554
|
+
|
555
|
+
|
556
|
+
def create_upload_file(project_path: str) -> str:
|
557
|
+
"""Creates an upload file for the given project path.
|
558
|
+
|
559
|
+
Args:
|
560
|
+
project_path (str): The path to the project.
|
561
|
+
|
562
|
+
Returns:
|
563
|
+
str: The path to the upload file.
|
564
|
+
"""
|
565
|
+
print_library_log("Packaging your project for upload...")
|
566
|
+
zip_file_path = zip_folder(project_path)
|
567
|
+
print("✓")
|
568
|
+
return zip_file_path
|
569
|
+
|
570
|
+
|
571
|
+
def zip_folder(project_path: str) -> str:
|
572
|
+
"""Zips the folder at the given project path.
|
573
|
+
|
574
|
+
Args:
|
575
|
+
project_path (str): The path to the project.
|
576
|
+
|
577
|
+
Returns:
|
578
|
+
str: The path to the zip file.
|
579
|
+
"""
|
580
|
+
upload_filepath = os.path.join(project_path, UPLOAD_FILENAME)
|
581
|
+
connector_file_exists = False
|
582
|
+
custom_drivers_exists = False
|
583
|
+
custom_driver_installation_script_exists = False
|
584
|
+
|
585
|
+
with ZipFile(upload_filepath, 'w', ZIP_DEFLATED) as zipf:
|
586
|
+
for root, files in dir_walker(project_path):
|
587
|
+
if os.path.basename(root) == DRIVERS:
|
588
|
+
custom_drivers_exists = True
|
589
|
+
if INSTALLATION_SCRIPT in files:
|
590
|
+
custom_driver_installation_script_exists = True
|
591
|
+
for file in files:
|
592
|
+
if file == ROOT_FILENAME:
|
593
|
+
connector_file_exists = True
|
594
|
+
file_path = os.path.join(root, file)
|
595
|
+
arcname = os.path.relpath(file_path, project_path)
|
596
|
+
zipf.write(file_path, arcname)
|
597
|
+
|
598
|
+
if not connector_file_exists:
|
599
|
+
print_library_log(
|
600
|
+
"The 'connector.py' file is missing. Please ensure that 'connector.py' is present in your project directory, and that the file name is in lowercase letters. All custom connectors require this file because Fivetran calls it to start a sync.",
|
601
|
+
Logging.Level.SEVERE)
|
602
|
+
os._exit(1)
|
603
|
+
|
604
|
+
if custom_drivers_exists and not custom_driver_installation_script_exists:
|
605
|
+
print_library_log(INSTALLATION_SCRIPT_MISSING_MESSAGE, Logging.Level.SEVERE)
|
606
|
+
os._exit(1)
|
607
|
+
|
608
|
+
return upload_filepath
|
609
|
+
|
610
|
+
|
611
|
+
def dir_walker(top):
|
612
|
+
"""Walks the directory tree starting at the given top directory.
|
613
|
+
|
614
|
+
Args:
|
615
|
+
top (str): The top directory to start the walk.
|
616
|
+
|
617
|
+
Yields:
|
618
|
+
tuple: A tuple containing the current directory path and a list of files.
|
619
|
+
"""
|
620
|
+
dirs, files = [], []
|
621
|
+
for name in os.listdir(top):
|
622
|
+
path = os.path.join(top, name)
|
623
|
+
if os.path.isdir(path):
|
624
|
+
if (name not in EXCLUDED_DIRS) and (not name.startswith(".")):
|
625
|
+
if VIRTUAL_ENV_CONFIG not in os.listdir(path): # Check for virtual env indicator
|
626
|
+
dirs.append(name)
|
627
|
+
else:
|
628
|
+
# Include all files if in `drivers` folder
|
629
|
+
if os.path.basename(top) == DRIVERS:
|
630
|
+
files.append(name)
|
631
|
+
if name.endswith(".py") or name == "requirements.txt":
|
632
|
+
files.append(name)
|
633
|
+
|
634
|
+
yield top, files
|
635
|
+
for name in dirs:
|
636
|
+
new_path = os.path.join(top, name)
|
637
|
+
for x in dir_walker(new_path):
|
638
|
+
yield x
|
639
|
+
|
640
|
+
def upload(local_path: str, deploy_key: str, group_id: str, connection: str) -> bool:
|
641
|
+
"""Uploads the local code file for the specified group and connection.
|
642
|
+
|
643
|
+
Args:
|
644
|
+
local_path (str): The local file path.
|
645
|
+
deploy_key (str): The deployment key.
|
646
|
+
group_id (str): The group ID.
|
647
|
+
connection (str): The connection name.
|
648
|
+
|
649
|
+
Returns:
|
650
|
+
bool: True if the upload was successful, False otherwise.
|
651
|
+
"""
|
652
|
+
print_library_log("Uploading your project...")
|
653
|
+
response = rq.post(f"{constants.PRODUCTION_BASE_URL}/v1/deploy/{group_id}/{connection}",
|
654
|
+
files={'file': open(local_path, 'rb')},
|
655
|
+
headers={"Authorization": f"Basic {deploy_key}"})
|
656
|
+
if response.ok:
|
657
|
+
print("✓")
|
658
|
+
return True
|
659
|
+
|
660
|
+
print_library_log(f"Unable to upload the project, failed with error: {response.reason}", Logging.Level.SEVERE)
|
661
|
+
return False
|
662
|
+
|
663
|
+
def cleanup_uploaded_code(deploy_key: str, group_id: str, connection: str) -> bool:
|
664
|
+
"""Cleans up the uploaded code file for the specified group and connection, if creation fails.
|
665
|
+
|
666
|
+
Args:
|
667
|
+
deploy_key (str): The deployment key.
|
668
|
+
group_id (str): The group ID.
|
669
|
+
connection (str): The connection name.
|
670
|
+
|
671
|
+
Returns:
|
672
|
+
bool: True if the cleanup was successful, False otherwise.
|
673
|
+
"""
|
674
|
+
print_library_log("INFO: Cleaning up your uploaded project ")
|
675
|
+
response = rq.post(f"{constants.PRODUCTION_BASE_URL}/v1/cleanup_code/{group_id}/{connection}",
|
676
|
+
headers={"Authorization": f"Basic {deploy_key}"})
|
677
|
+
if response.ok:
|
678
|
+
print("✓")
|
679
|
+
return True
|
680
|
+
|
681
|
+
print_library_log(f"SEVERE: Unable to cleanup the project, failed with error: {response.reason}",
|
682
|
+
Logging.Level.SEVERE)
|
683
|
+
return False
|
684
|
+
|
685
|
+
def get_os_arch_suffix() -> str:
|
686
|
+
"""
|
687
|
+
Returns the operating system and architecture suffix for the current operating system.
|
688
|
+
"""
|
689
|
+
system = platform.system().lower()
|
690
|
+
machine = platform.machine().lower()
|
691
|
+
|
692
|
+
if system not in OS_MAP:
|
693
|
+
raise RuntimeError(f"Unsupported OS: {system}")
|
694
|
+
|
695
|
+
plat = OS_MAP[system]
|
696
|
+
|
697
|
+
if machine not in ARCH_MAP or (plat == WIN_OS and ARCH_MAP[machine] != X64):
|
698
|
+
raise RuntimeError(f"Unsupported architecture '{machine}' for {plat}")
|
699
|
+
|
700
|
+
return f"{plat}-{ARCH_MAP[machine]}"
|
701
|
+
|
702
|
+
def get_group_info(group: str, deploy_key: str) -> tuple[str, str]:
|
703
|
+
"""Retrieves the group information for the specified group and deployment key.
|
704
|
+
|
705
|
+
Args:
|
706
|
+
group (str): The group name.
|
707
|
+
deploy_key (str): The deployment key.
|
708
|
+
|
709
|
+
Returns:
|
710
|
+
tuple[str, str]: A tuple containing the group ID and group name.
|
711
|
+
"""
|
712
|
+
groups_url = f"{constants.PRODUCTION_BASE_URL}/v1/groups"
|
713
|
+
|
714
|
+
params = {"limit": 500}
|
715
|
+
headers = {"Authorization": f"Basic {deploy_key}"}
|
716
|
+
resp = rq.get(groups_url, headers=headers, params=params)
|
717
|
+
|
718
|
+
if not resp.ok:
|
719
|
+
print_library_log(
|
720
|
+
f"The request failed with status code: {resp.status_code}. Please ensure you're using a valid base64-encoded API key and try again.",
|
721
|
+
Logging.Level.SEVERE)
|
722
|
+
os._exit(1)
|
723
|
+
|
724
|
+
data = resp.json().get("data", {})
|
725
|
+
groups = data.get("items")
|
726
|
+
|
727
|
+
if not groups:
|
728
|
+
print_library_log("No destinations defined in the account", Logging.Level.SEVERE)
|
729
|
+
os._exit(1)
|
730
|
+
|
731
|
+
if not group:
|
732
|
+
if len(groups) == 1:
|
733
|
+
return groups[0]['id'], groups[0]['name']
|
734
|
+
else:
|
735
|
+
print_library_log(
|
736
|
+
"Destination name is required when there are multiple destinations in the account",
|
737
|
+
Logging.Level.SEVERE)
|
738
|
+
os._exit(1)
|
739
|
+
else:
|
740
|
+
while True:
|
741
|
+
for grp in groups:
|
742
|
+
if grp['name'] == group:
|
743
|
+
return grp['id'], grp['name']
|
744
|
+
|
745
|
+
next_cursor = data.get("next_cursor")
|
746
|
+
if not next_cursor:
|
747
|
+
break
|
748
|
+
|
749
|
+
params = {"cursor": next_cursor, "limit": 500}
|
750
|
+
resp = rq.get(groups_url, headers=headers, params=params)
|
751
|
+
data = resp.json().get("data", {})
|
752
|
+
groups = data.get("items", [])
|
753
|
+
|
754
|
+
print_library_log(
|
755
|
+
f"The specified destination '{group}' was not found in your account.", Logging.Level.SEVERE)
|
756
|
+
os._exit(1)
|
757
|
+
|
758
|
+
def java_exe_helper(location: str, os_arch_suffix: str) -> str:
|
759
|
+
"""Returns the path to the Java executable.
|
760
|
+
|
761
|
+
Args:
|
762
|
+
location (str): The location of the Java executable.
|
763
|
+
os_arch_suffix (str): The name of the operating system and architecture
|
764
|
+
|
765
|
+
Returns:
|
766
|
+
str: The path to the Java executable.
|
767
|
+
"""
|
768
|
+
java_exe_base = os.path.join(location, "bin", "java")
|
769
|
+
return f"{java_exe_base}.exe" if os_arch_suffix == f"{WIN_OS}-{X64}" else java_exe_base
|
770
|
+
|
771
|
+
def process_stream(stream):
|
772
|
+
"""Processes a stream of text lines, replacing occurrences of a specified pattern.
|
773
|
+
|
774
|
+
This method reads each line from the provided stream, searches for occurrences of
|
775
|
+
a predefined pattern, and replaces them with a specified replacement string.
|
776
|
+
|
777
|
+
Args:
|
778
|
+
stream (iterable): An iterable stream of text lines, typically from a file or another input source.
|
779
|
+
|
780
|
+
Yields:
|
781
|
+
str: Each line from the stream after replacing the matched pattern with the replacement string.
|
782
|
+
"""
|
783
|
+
pattern = r'com\.fivetran\.partner_sdk.*\.tools\.testers\.\S+'
|
784
|
+
|
785
|
+
for line in iter(stream.readline, ""):
|
786
|
+
if not re.search(pattern, line):
|
787
|
+
yield line
|
788
|
+
|
789
|
+
def run_tester(java_exe_str: str, root_dir: str, project_path: str, port: int, state_json: str,
|
790
|
+
configuration_json: str):
|
791
|
+
"""Runs the connector tester.
|
792
|
+
|
793
|
+
Args:
|
794
|
+
java_exe_str (str): The path to the Java executable.
|
795
|
+
root_dir (str): The root directory.
|
796
|
+
project_path (str): The path to the project.
|
797
|
+
port (int): The port number to use for the tester.
|
798
|
+
state_json (str): The state JSON string to pass to the tester.
|
799
|
+
configuration_json (str): The configuration JSON string to pass to the tester.
|
800
|
+
|
801
|
+
Yields:
|
802
|
+
str: The log messages from the tester.
|
803
|
+
"""
|
804
|
+
working_dir = os.path.join(project_path, OUTPUT_FILES_DIR)
|
805
|
+
try:
|
806
|
+
os.mkdir(working_dir)
|
807
|
+
except FileExistsError:
|
808
|
+
pass
|
809
|
+
|
810
|
+
cmd = [java_exe_str,
|
811
|
+
"-jar",
|
812
|
+
os.path.join(root_dir, TESTER_FILENAME),
|
813
|
+
"--connector-sdk=true",
|
814
|
+
f"--port={port}",
|
815
|
+
f"--working-dir={working_dir}",
|
816
|
+
"--tester-type=source",
|
817
|
+
f"--state={state_json}",
|
818
|
+
f"--configuration={configuration_json}"]
|
819
|
+
|
820
|
+
popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
|
821
|
+
for line in process_stream(popen.stderr):
|
822
|
+
yield _maybe_colorize_jar_output(line)
|
823
|
+
|
824
|
+
for line in process_stream(popen.stdout):
|
825
|
+
yield _maybe_colorize_jar_output(line)
|
826
|
+
popen.stdout.close()
|
827
|
+
return_code = popen.wait()
|
828
|
+
if return_code:
|
829
|
+
raise subprocess.CalledProcessError(return_code, cmd)
|
830
|
+
|
831
|
+
def _maybe_colorize_jar_output(line: str) -> str:
|
832
|
+
if not constants.DEBUGGING:
|
833
|
+
return line
|
834
|
+
|
835
|
+
if "SEVERE" in line or "ERROR" in line or "Exception" in line or "FAILED" in line:
|
836
|
+
return f"\033[91m{line}\033[0m" # Red
|
837
|
+
elif "WARN" in line or "WARNING" in line:
|
838
|
+
return f"\033[93m{line}\033[0m" # Yellow
|
839
|
+
return line
|
840
|
+
|
841
|
+
def process_tables(response, table_list):
|
842
|
+
for entry in response:
|
843
|
+
if 'table' not in entry:
|
844
|
+
raise ValueError("Entry missing table name: " + entry)
|
845
|
+
|
846
|
+
table_name = get_renamed_table_name(entry['table'])
|
847
|
+
|
848
|
+
if table_name in table_list:
|
849
|
+
raise ValueError("Table already defined: " + table_name)
|
850
|
+
|
851
|
+
table = common_pb2.Table(name=table_name)
|
852
|
+
columns = {}
|
853
|
+
|
854
|
+
if "primary_key" in entry:
|
855
|
+
process_primary_keys(columns, entry)
|
856
|
+
|
857
|
+
if "columns" in entry:
|
858
|
+
process_columns(columns, entry)
|
859
|
+
|
860
|
+
table.columns.extend(columns.values())
|
861
|
+
TABLES[table_name] = table
|
862
|
+
table_list[table_name] = table
|
863
|
+
|
864
|
+
def process_primary_keys(columns, entry):
|
865
|
+
for pkey_name in entry["primary_key"]:
|
866
|
+
column_name = get_renamed_column_name(pkey_name)
|
867
|
+
column = columns[column_name] if column_name in columns else common_pb2.Column(name=column_name)
|
868
|
+
column.primary_key = True
|
869
|
+
columns[column_name] = column
|
870
|
+
|
871
|
+
def process_columns(columns, entry):
|
872
|
+
for name, type in entry["columns"].items():
|
873
|
+
column_name = get_renamed_column_name(name)
|
874
|
+
column = columns[column_name] if column_name in columns else common_pb2.Column(name=column_name)
|
875
|
+
|
876
|
+
if isinstance(type, str):
|
877
|
+
process_data_type(column, type)
|
878
|
+
|
879
|
+
elif isinstance(type, dict):
|
880
|
+
if type['type'].upper() != "DECIMAL":
|
881
|
+
raise ValueError("Expecting DECIMAL data type")
|
882
|
+
column.type = common_pb2.DataType.DECIMAL
|
883
|
+
column.decimal.precision = type['precision']
|
884
|
+
column.decimal.scale = type['scale']
|
885
|
+
|
886
|
+
else:
|
887
|
+
raise ValueError("Unrecognized column type: ", str(type))
|
888
|
+
|
889
|
+
if "primary_key" in entry and name in entry["primary_key"]:
|
890
|
+
column.primary_key = True
|
891
|
+
|
892
|
+
columns[column_name] = column
|
893
|
+
|
894
|
+
def process_data_type(column, type):
|
895
|
+
if type.upper() == "BOOLEAN":
|
896
|
+
column.type = common_pb2.DataType.BOOLEAN
|
897
|
+
elif type.upper() == "SHORT":
|
898
|
+
column.type = common_pb2.DataType.SHORT
|
899
|
+
elif type.upper() == "INT":
|
900
|
+
column.type = common_pb2.DataType.INT
|
901
|
+
elif type.upper() == "LONG":
|
902
|
+
column.type = common_pb2.DataType.LONG
|
903
|
+
elif type.upper() == "DECIMAL":
|
904
|
+
raise ValueError("DECIMAL data type missing precision and scale")
|
905
|
+
elif type.upper() == "FLOAT":
|
906
|
+
column.type = common_pb2.DataType.FLOAT
|
907
|
+
elif type.upper() == "DOUBLE":
|
908
|
+
column.type = common_pb2.DataType.DOUBLE
|
909
|
+
elif type.upper() == "NAIVE_DATE":
|
910
|
+
column.type = common_pb2.DataType.NAIVE_DATE
|
911
|
+
elif type.upper() == "NAIVE_DATETIME":
|
912
|
+
column.type = common_pb2.DataType.NAIVE_DATETIME
|
913
|
+
elif type.upper() == "UTC_DATETIME":
|
914
|
+
column.type = common_pb2.DataType.UTC_DATETIME
|
915
|
+
elif type.upper() == "BINARY":
|
916
|
+
column.type = common_pb2.DataType.BINARY
|
917
|
+
elif type.upper() == "XML":
|
918
|
+
column.type = common_pb2.DataType.XML
|
919
|
+
elif type.upper() == "STRING":
|
920
|
+
column.type = common_pb2.DataType.STRING
|
921
|
+
elif type.upper() == "JSON":
|
922
|
+
column.type = common_pb2.DataType.JSON
|
923
|
+
else:
|
924
|
+
raise ValueError("Unrecognized column type encountered:: ", str(type))
|
925
|
+
|
926
|
+
def delete_file_if_exists(file_path):
|
927
|
+
if os.path.exists(file_path):
|
928
|
+
os.remove(file_path)
|