fivetran-connector-sdk 1.4.5__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 +61 -1635
- 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.5.dist-info → fivetran_connector_sdk-1.4.6.dist-info}/METADATA +1 -1
- fivetran_connector_sdk-1.4.6.dist-info/RECORD +18 -0
- {fivetran_connector_sdk-1.4.5.dist-info → fivetran_connector_sdk-1.4.6.dist-info}/WHEEL +1 -1
- fivetran_connector_sdk-1.4.5.dist-info/RECORD +0 -13
- {fivetran_connector_sdk-1.4.5.dist-info → fivetran_connector_sdk-1.4.6.dist-info}/entry_points.txt +0 -0
- {fivetran_connector_sdk-1.4.5.dist-info → fivetran_connector_sdk-1.4.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,70 @@
|
|
1
|
+
import re
|
2
|
+
|
3
|
+
TESTER_VER = "0.25.0521.001"
|
4
|
+
|
5
|
+
WIN_OS = "windows"
|
6
|
+
ARM_64 = "arm64"
|
7
|
+
X64 = "x64"
|
8
|
+
|
9
|
+
OS_MAP = {
|
10
|
+
"darwin": "mac",
|
11
|
+
"linux": "linux",
|
12
|
+
WIN_OS: WIN_OS
|
13
|
+
}
|
14
|
+
|
15
|
+
ARCH_MAP = {
|
16
|
+
"x86_64": X64,
|
17
|
+
"amd64": X64,
|
18
|
+
ARM_64: ARM_64,
|
19
|
+
"aarch64": ARM_64
|
20
|
+
}
|
21
|
+
|
22
|
+
# Global constants - use constants.<Global_variable> to access them as they can be overridden in the
|
23
|
+
DEBUGGING = False
|
24
|
+
EXECUTED_VIA_CLI = False
|
25
|
+
TABLES = {}
|
26
|
+
|
27
|
+
TESTER_FILENAME = "run_sdk_tester.jar"
|
28
|
+
VERSION_FILENAME = "version.txt"
|
29
|
+
UPLOAD_FILENAME = "code.zip"
|
30
|
+
LAST_VERSION_CHECK_FILE = "_last_version_check"
|
31
|
+
ROOT_LOCATION = ".ft_sdk_connector_tester"
|
32
|
+
CONFIG_FILE = "_config.json"
|
33
|
+
OUTPUT_FILES_DIR = "files"
|
34
|
+
REQUIREMENTS_TXT = "requirements.txt"
|
35
|
+
PYPI_PACKAGE_DETAILS_URL = "https://pypi.org/pypi/fivetran_connector_sdk/json"
|
36
|
+
ONE_DAY_IN_SEC = 24 * 60 * 60
|
37
|
+
MAX_RETRIES = 3
|
38
|
+
LOGGING_PREFIX = "Fivetran-Connector-SDK"
|
39
|
+
LOGGING_DELIMITER = ": "
|
40
|
+
VIRTUAL_ENV_CONFIG = "pyvenv.cfg"
|
41
|
+
ROOT_FILENAME = "connector.py"
|
42
|
+
|
43
|
+
# Compile patterns used in the implementation
|
44
|
+
WORD_DASH_DOT_PATTERN = re.compile(r'^[\w.-]*$')
|
45
|
+
NON_WORD_PATTERN = re.compile(r'\W')
|
46
|
+
WORD_OR_DOLLAR_PATTERN = re.compile(r'[\w$]')
|
47
|
+
DROP_LEADING_UNDERSCORE = re.compile(r'_+([a-zA-Z]\w*)')
|
48
|
+
WORD_PATTERN = re.compile(r'\w')
|
49
|
+
|
50
|
+
EXCLUDED_DIRS = ["__pycache__", "lib", "include", OUTPUT_FILES_DIR]
|
51
|
+
EXCLUDED_PIPREQS_DIRS = ["bin,etc,include,lib,Lib,lib64,Scripts,share"]
|
52
|
+
VALID_COMMANDS = ["debug", "deploy", "reset", "version"]
|
53
|
+
MAX_ALLOWED_EDIT_DISTANCE_FROM_VALID_COMMAND = 3
|
54
|
+
COMMANDS_AND_SYNONYMS = {
|
55
|
+
"debug": {"test", "verify", "diagnose", "check"},
|
56
|
+
"deploy": {"upload", "ship", "launch", "release"},
|
57
|
+
"reset": {"reinitialize", "reinitialise", "re-initialize", "re-initialise", "restart", "restore"},
|
58
|
+
}
|
59
|
+
|
60
|
+
CONNECTION_SCHEMA_NAME_PATTERN = r'^[_a-z][_a-z0-9]*$'
|
61
|
+
PRODUCTION_BASE_URL = "https://api.fivetran.com"
|
62
|
+
INSTALLATION_SCRIPT_MISSING_MESSAGE = "The 'installation.sh' file is missing in the 'drivers' directory. Please ensure that 'installation.sh' is present to properly configure drivers."
|
63
|
+
INSTALLATION_SCRIPT = "installation.sh"
|
64
|
+
DRIVERS = "drivers"
|
65
|
+
JAVA_LONG_MAX_VALUE = 9223372036854775807
|
66
|
+
MAX_CONFIG_FIELDS = 100
|
67
|
+
SUPPORTED_PYTHON_VERSIONS = ["3.12", "3.11", "3.10", "3.9"]
|
68
|
+
DEFAULT_PYTHON_VERSION = "3.12"
|
69
|
+
FIVETRAN_HD_AGENT_ID = "FIVETRAN_HD_AGENT_ID"
|
70
|
+
UTF_8 = "utf-8"
|
@@ -0,0 +1,318 @@
|
|
1
|
+
import re
|
2
|
+
import os
|
3
|
+
import shutil
|
4
|
+
import sys
|
5
|
+
import json
|
6
|
+
import unicodedata
|
7
|
+
import importlib.util
|
8
|
+
from datetime import datetime
|
9
|
+
from unidecode import unidecode
|
10
|
+
|
11
|
+
from fivetran_connector_sdk.logger import Logging
|
12
|
+
from fivetran_connector_sdk import constants
|
13
|
+
from fivetran_connector_sdk.constants import (
|
14
|
+
LOGGING_PREFIX,
|
15
|
+
LOGGING_DELIMITER,
|
16
|
+
WORD_DASH_DOT_PATTERN,
|
17
|
+
NON_WORD_PATTERN,
|
18
|
+
WORD_OR_DOLLAR_PATTERN,
|
19
|
+
DROP_LEADING_UNDERSCORE,
|
20
|
+
WORD_PATTERN,
|
21
|
+
ROOT_FILENAME,
|
22
|
+
MAX_CONFIG_FIELDS,
|
23
|
+
MAX_ALLOWED_EDIT_DISTANCE_FROM_VALID_COMMAND,
|
24
|
+
COMMANDS_AND_SYNONYMS,
|
25
|
+
VALID_COMMANDS,
|
26
|
+
OUTPUT_FILES_DIR, UTF_8
|
27
|
+
)
|
28
|
+
|
29
|
+
RENAMED_TABLE_NAMES = {}
|
30
|
+
RENAMED_COL_NAMES = {}
|
31
|
+
|
32
|
+
def print_library_log(message: str, level: Logging.Level = Logging.Level.INFO):
|
33
|
+
"""Logs a library message with the specified logging level.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
level (Logging.Level): The logging level.
|
37
|
+
message (str): The message to log.
|
38
|
+
"""
|
39
|
+
if constants.DEBUGGING or constants.EXECUTED_VIA_CLI:
|
40
|
+
current_time = datetime.now().strftime("%b %d, %Y %I:%M:%S %p")
|
41
|
+
print(f"{Logging.get_color(level)}{current_time} {level.name} {LOGGING_PREFIX}: {message} {Logging.reset_color()}")
|
42
|
+
else:
|
43
|
+
escaped_message = json.dumps(LOGGING_PREFIX + LOGGING_DELIMITER + message)
|
44
|
+
log_message = f'{{"level":"{level.name}", "message": {escaped_message}, "message_origin": "library"}}'
|
45
|
+
print(log_message)
|
46
|
+
|
47
|
+
def is_special(c):
|
48
|
+
"""Check if the character is a special character."""
|
49
|
+
return not WORD_OR_DOLLAR_PATTERN.fullmatch(c)
|
50
|
+
|
51
|
+
|
52
|
+
def starts_word(previous, current):
|
53
|
+
"""
|
54
|
+
Check if the current character starts a new word based on the previous character.
|
55
|
+
"""
|
56
|
+
return (previous and previous.islower() and current.isupper()) or (
|
57
|
+
previous and previous.isdigit() != current.isdigit()
|
58
|
+
)
|
59
|
+
|
60
|
+
|
61
|
+
def underscore_invalid_leading_character(name, valid_leading_regex):
|
62
|
+
"""
|
63
|
+
Ensure the name starts with a valid leading character.
|
64
|
+
"""
|
65
|
+
if name and not valid_leading_regex.match(name[0]):
|
66
|
+
name = f'_{name}'
|
67
|
+
return name
|
68
|
+
|
69
|
+
|
70
|
+
def single_underscore_case(name):
|
71
|
+
"""
|
72
|
+
Convert the input name to single underscore case, replacing special characters and spaces.
|
73
|
+
"""
|
74
|
+
acc = []
|
75
|
+
previous = None
|
76
|
+
|
77
|
+
for char_index, c in enumerate(name):
|
78
|
+
if char_index == 0 and c == '$':
|
79
|
+
acc.append('_')
|
80
|
+
elif is_special(c):
|
81
|
+
acc.append('_')
|
82
|
+
elif c == ' ':
|
83
|
+
acc.append('_')
|
84
|
+
elif starts_word(previous, c):
|
85
|
+
acc.append('_')
|
86
|
+
acc.append(c.lower())
|
87
|
+
else:
|
88
|
+
acc.append(c.lower())
|
89
|
+
|
90
|
+
previous = c
|
91
|
+
|
92
|
+
name = ''.join(acc)
|
93
|
+
return re.sub(r'_+', '_', name)
|
94
|
+
|
95
|
+
|
96
|
+
def contains_only_word_dash_dot(name):
|
97
|
+
"""
|
98
|
+
Check if the name contains only word characters, dashes, and dots.
|
99
|
+
"""
|
100
|
+
return bool(WORD_DASH_DOT_PATTERN.fullmatch(name))
|
101
|
+
|
102
|
+
|
103
|
+
def transliterate(name):
|
104
|
+
"""
|
105
|
+
Transliterate the input name if it contains non-word, dash, or dot characters.
|
106
|
+
"""
|
107
|
+
if contains_only_word_dash_dot(name):
|
108
|
+
return name
|
109
|
+
# Step 1: Normalize the name to NFD form (decomposed form)
|
110
|
+
normalized_name = unicodedata.normalize('NFD', name)
|
111
|
+
# Step 2: Remove combining characters (diacritics, accents, etc.)
|
112
|
+
normalized_name = ''.join(char for char in normalized_name if not unicodedata.combining(char))
|
113
|
+
# Step 3: Normalize back to NFC form (composed form)
|
114
|
+
normalized_name = unicodedata.normalize('NFC', normalized_name)
|
115
|
+
# Step 4: Convert the string to ASCII using `unidecode` (removes any remaining non-ASCII characters)
|
116
|
+
normalized_name = unidecode(normalized_name)
|
117
|
+
# Step 5: Return the normalized name
|
118
|
+
return normalized_name
|
119
|
+
|
120
|
+
|
121
|
+
def redshift_safe(name):
|
122
|
+
"""
|
123
|
+
Make the name safe for use in Redshift.
|
124
|
+
"""
|
125
|
+
name = transliterate(name)
|
126
|
+
name = NON_WORD_PATTERN.sub('_', name)
|
127
|
+
name = single_underscore_case(name)
|
128
|
+
name = underscore_invalid_leading_character(name, WORD_PATTERN)
|
129
|
+
return name
|
130
|
+
|
131
|
+
|
132
|
+
def safe_drop_underscores(name):
|
133
|
+
"""
|
134
|
+
Drop leading underscores if the name starts with valid characters after sanitization.
|
135
|
+
"""
|
136
|
+
safe_name = redshift_safe(name)
|
137
|
+
match = DROP_LEADING_UNDERSCORE.match(safe_name)
|
138
|
+
if match:
|
139
|
+
return match.group(1)
|
140
|
+
return safe_name
|
141
|
+
|
142
|
+
|
143
|
+
def get_renamed_table_name(source_table):
|
144
|
+
"""
|
145
|
+
Process a source table name to ensure it conforms to naming rules.
|
146
|
+
"""
|
147
|
+
if source_table not in RENAMED_TABLE_NAMES:
|
148
|
+
RENAMED_TABLE_NAMES[source_table] = safe_drop_underscores(source_table)
|
149
|
+
|
150
|
+
return RENAMED_TABLE_NAMES[source_table]
|
151
|
+
|
152
|
+
|
153
|
+
def get_renamed_column_name(source_column):
|
154
|
+
"""
|
155
|
+
Process a source column name to ensure it conforms to naming rules.
|
156
|
+
"""
|
157
|
+
if source_column not in RENAMED_COL_NAMES:
|
158
|
+
RENAMED_COL_NAMES[source_column] = redshift_safe(source_column)
|
159
|
+
|
160
|
+
return RENAMED_COL_NAMES[source_column]
|
161
|
+
|
162
|
+
|
163
|
+
# Functions used by main method only
|
164
|
+
|
165
|
+
def find_connector_object(project_path):
|
166
|
+
"""Finds the connector object in the given project path.
|
167
|
+
Args:
|
168
|
+
project_path (str): The path to the project.
|
169
|
+
"""
|
170
|
+
|
171
|
+
sys.path.append(project_path) # Allows python interpreter to search for modules in this path
|
172
|
+
module_name = "connector_connector_code"
|
173
|
+
connector_py = os.path.join(project_path, ROOT_FILENAME)
|
174
|
+
try:
|
175
|
+
spec = importlib.util.spec_from_file_location(module_name, connector_py)
|
176
|
+
module = importlib.util.module_from_spec(spec)
|
177
|
+
sys.modules[module_name] = module
|
178
|
+
spec.loader.exec_module(module)
|
179
|
+
for obj in dir(module):
|
180
|
+
if not obj.startswith('__'): # Exclude built-in attributes
|
181
|
+
obj_attr = getattr(module, obj)
|
182
|
+
if '<fivetran_connector_sdk.Connector object at' in str(obj_attr):
|
183
|
+
return obj_attr
|
184
|
+
except FileNotFoundError:
|
185
|
+
print_library_log(
|
186
|
+
"The connector object is missing in the current directory. Please ensure that you are running the command from correct directory or that you have defined a connector object using the correct syntax in your `connector.py` file. Reference: https://fivetran.com/docs/connectors/connector-sdk/technical-reference#technicaldetailsrequiredobjectconnector",
|
187
|
+
Logging.Level.SEVERE)
|
188
|
+
return None
|
189
|
+
|
190
|
+
print_library_log(
|
191
|
+
"The connector object is missing. Please ensure that you have defined a connector object using the correct syntax in your `connector.py` file. Reference: https://fivetran.com/docs/connectors/connector-sdk/technical-reference#technicaldetailsrequiredobjectconnector",
|
192
|
+
Logging.Level.SEVERE)
|
193
|
+
return None
|
194
|
+
|
195
|
+
|
196
|
+
def suggest_correct_command(input_command: str) -> bool:
|
197
|
+
# for typos
|
198
|
+
# calculate the edit distance of the input command (lowercased) with each of the valid commands
|
199
|
+
edit_distances_of_commands = sorted(
|
200
|
+
[(command, edit_distance(command, input_command.lower())) for command in VALID_COMMANDS], key=lambda x: x[1])
|
201
|
+
|
202
|
+
if edit_distances_of_commands[0][1] <= MAX_ALLOWED_EDIT_DISTANCE_FROM_VALID_COMMAND:
|
203
|
+
# if the closest command is within the max allowed edit distance, we suggest that command
|
204
|
+
# threshold is kept to prevent suggesting a valid command for an obvious wrong command like `fivetran iknowthisisntacommandbuttryanyway`
|
205
|
+
print_suggested_command_message(edit_distances_of_commands[0][0], input_command)
|
206
|
+
return True
|
207
|
+
|
208
|
+
# for synonyms
|
209
|
+
for (command, synonyms) in COMMANDS_AND_SYNONYMS.items():
|
210
|
+
# check if the input command (lowercased) is a recognised synonym of the valid commands, if yes, suggest that command
|
211
|
+
if input_command.lower() in synonyms:
|
212
|
+
print_suggested_command_message(command, input_command)
|
213
|
+
return True
|
214
|
+
|
215
|
+
return False
|
216
|
+
|
217
|
+
|
218
|
+
def print_suggested_command_message(valid_command: str, input_command: str) -> None:
|
219
|
+
print_library_log(f"`fivetran {input_command}` is not a valid command.", Logging.Level.SEVERE)
|
220
|
+
print_library_log(f"Did you mean `fivetran {valid_command}`?", Logging.Level.SEVERE)
|
221
|
+
print_library_log("Use `fivetran --help` for more details.", Logging.Level.SEVERE)
|
222
|
+
|
223
|
+
|
224
|
+
def edit_distance(first_string: str, second_string: str) -> int:
|
225
|
+
first_string_length: int = len(first_string)
|
226
|
+
second_string_length: int = len(second_string)
|
227
|
+
|
228
|
+
# Initialize the previous row of distances (for the base case of an empty first string) 'previous_row[j]' holds
|
229
|
+
# the edit distance between an empty prefix of 'first_string' and the first 'j' characters of 'second_string'.
|
230
|
+
# The first row is filled with values [0, 1, 2, ..., second_string_length]
|
231
|
+
previous_row: list[int] = list(range(second_string_length + 1))
|
232
|
+
|
233
|
+
# Rest of the rows
|
234
|
+
for first_string_index in range(1, first_string_length + 1):
|
235
|
+
# Start the current row with the distance for an empty second string
|
236
|
+
current_row: list[int] = [first_string_index]
|
237
|
+
|
238
|
+
# Iterate over each character in the second string
|
239
|
+
for second_string_index in range(1, second_string_length + 1):
|
240
|
+
if first_string[first_string_index - 1] == second_string[second_string_index - 1]:
|
241
|
+
# If characters match, no additional cost
|
242
|
+
current_row.append(previous_row[second_string_index - 1])
|
243
|
+
else:
|
244
|
+
# Minimum cost of insertion, deletion, or substitution
|
245
|
+
current_row.append(
|
246
|
+
1 + min(current_row[-1], previous_row[second_string_index], previous_row[second_string_index - 1]))
|
247
|
+
|
248
|
+
# Move to the next row
|
249
|
+
previous_row = current_row
|
250
|
+
|
251
|
+
# The last value in the last row is the edit distance
|
252
|
+
return previous_row[second_string_length]
|
253
|
+
|
254
|
+
|
255
|
+
def get_input_from_cli(prompt: str, default_value: str) -> str:
|
256
|
+
"""
|
257
|
+
Prompts the user for input.
|
258
|
+
"""
|
259
|
+
if default_value:
|
260
|
+
value = input(f"{prompt} [Default : {default_value}]: ").strip() or default_value
|
261
|
+
else:
|
262
|
+
value = input(f"{prompt}: ").strip()
|
263
|
+
|
264
|
+
if not value:
|
265
|
+
raise ValueError("Missing required input: Expected a value but received None")
|
266
|
+
return value
|
267
|
+
|
268
|
+
def validate_and_load_configuration(args, configuration):
|
269
|
+
if configuration:
|
270
|
+
json_filepath = os.path.join(args.project_path, args.configuration)
|
271
|
+
if os.path.isfile(json_filepath):
|
272
|
+
with open(json_filepath, 'r', encoding=UTF_8) as fi:
|
273
|
+
configuration = json.load(fi)
|
274
|
+
if len(configuration) > MAX_CONFIG_FIELDS:
|
275
|
+
raise ValueError(f"Configuration field count exceeds maximum of {MAX_CONFIG_FIELDS}. Reduce the field count.")
|
276
|
+
else:
|
277
|
+
raise ValueError(
|
278
|
+
"Configuration must be provided as a JSON file. Please check your input. Reference: "
|
279
|
+
"https://fivetran.com/docs/connectors/connector-sdk/detailed-guide#workingwithconfigurationjsonfile")
|
280
|
+
else:
|
281
|
+
json_filepath = os.path.join(args.project_path, "configuration.json")
|
282
|
+
if os.path.exists(json_filepath):
|
283
|
+
print_library_log("Configuration file detected in the project, but no configuration input provided via the command line", Logging.Level.WARNING)
|
284
|
+
configuration = {}
|
285
|
+
return configuration
|
286
|
+
|
287
|
+
|
288
|
+
def validate_and_load_state(args, state):
|
289
|
+
if state:
|
290
|
+
json_filepath = os.path.join(args.project_path, args.state)
|
291
|
+
else:
|
292
|
+
json_filepath = os.path.join(args.project_path, "files", "state.json")
|
293
|
+
|
294
|
+
if os.path.exists(json_filepath):
|
295
|
+
if os.path.isfile(json_filepath):
|
296
|
+
with open(json_filepath, 'r', encoding=UTF_8) as fi:
|
297
|
+
state = json.load(fi)
|
298
|
+
elif state.lstrip().startswith("{"):
|
299
|
+
state = json.loads(state)
|
300
|
+
else:
|
301
|
+
state = {}
|
302
|
+
return state
|
303
|
+
|
304
|
+
|
305
|
+
def reset_local_file_directory(args):
|
306
|
+
files_path = os.path.join(args.project_path, OUTPUT_FILES_DIR)
|
307
|
+
confirm = input(
|
308
|
+
"This will delete your current state and `warehouse.db` files. Do you want to continue? (Y/N): ")
|
309
|
+
if confirm.lower() != "y":
|
310
|
+
print_library_log("Reset canceled")
|
311
|
+
else:
|
312
|
+
try:
|
313
|
+
if os.path.exists(files_path) and os.path.isdir(files_path):
|
314
|
+
shutil.rmtree(files_path)
|
315
|
+
print_library_log("Reset Successful")
|
316
|
+
except Exception as e:
|
317
|
+
print_library_log("Reset Failed", Logging.Level.SEVERE)
|
318
|
+
raise e
|
@@ -0,0 +1,91 @@
|
|
1
|
+
import json
|
2
|
+
import traceback
|
3
|
+
from enum import IntEnum
|
4
|
+
from datetime import datetime
|
5
|
+
|
6
|
+
from fivetran_connector_sdk import constants
|
7
|
+
|
8
|
+
class Logging:
|
9
|
+
class Level(IntEnum):
|
10
|
+
FINE = 1
|
11
|
+
INFO = 2
|
12
|
+
WARNING = 3
|
13
|
+
SEVERE = 4
|
14
|
+
|
15
|
+
LOG_LEVEL = None
|
16
|
+
|
17
|
+
@staticmethod
|
18
|
+
def __log(level: Level, message: str):
|
19
|
+
"""Logs a message with the specified logging level.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
level (Logging.Level): The logging level.
|
23
|
+
message (str): The message to log.
|
24
|
+
"""
|
25
|
+
if constants.DEBUGGING:
|
26
|
+
current_time = datetime.now().strftime("%b %d, %Y %I:%M:%S %p")
|
27
|
+
print(f"{Logging.get_color(level)}{current_time} {level.name}: {message} {Logging.reset_color()}")
|
28
|
+
else:
|
29
|
+
escaped_message = json.dumps(message)
|
30
|
+
log_message = f'{{"level":"{level.name}", "message": {escaped_message}, "message_origin": "connector_sdk"}}'
|
31
|
+
print(log_message)
|
32
|
+
|
33
|
+
@staticmethod
|
34
|
+
def get_color(level):
|
35
|
+
if level == Logging.Level.WARNING:
|
36
|
+
return "\033[93m" # Yellow
|
37
|
+
elif level == Logging.Level.SEVERE:
|
38
|
+
return "\033[91m" # Red
|
39
|
+
return ""
|
40
|
+
|
41
|
+
@staticmethod
|
42
|
+
def reset_color():
|
43
|
+
return "\033[0m"
|
44
|
+
|
45
|
+
@staticmethod
|
46
|
+
def fine(message: str):
|
47
|
+
"""Logs a fine-level message.
|
48
|
+
|
49
|
+
Args:
|
50
|
+
message (str): The message to log.
|
51
|
+
"""
|
52
|
+
if constants.DEBUGGING and Logging.LOG_LEVEL == Logging.Level.FINE:
|
53
|
+
Logging.__log(Logging.Level.FINE, message)
|
54
|
+
|
55
|
+
@staticmethod
|
56
|
+
def info(message: str):
|
57
|
+
"""Logs an info-level message.
|
58
|
+
|
59
|
+
Args:
|
60
|
+
message (str): The message to log.
|
61
|
+
"""
|
62
|
+
if Logging.LOG_LEVEL <= Logging.Level.INFO:
|
63
|
+
Logging.__log(Logging.Level.INFO, message)
|
64
|
+
|
65
|
+
@staticmethod
|
66
|
+
def warning(message: str):
|
67
|
+
"""Logs a warning-level message.
|
68
|
+
|
69
|
+
Args:
|
70
|
+
message (str): The message to log.
|
71
|
+
"""
|
72
|
+
if Logging.LOG_LEVEL <= Logging.Level.WARNING:
|
73
|
+
Logging.__log(Logging.Level.WARNING, message)
|
74
|
+
|
75
|
+
@staticmethod
|
76
|
+
def severe(message: str, exception: Exception = None):
|
77
|
+
"""Logs a severe-level message.
|
78
|
+
|
79
|
+
Args:
|
80
|
+
message (str): The message to log.
|
81
|
+
exception (Exception, optional): Exception to be logged if provided.
|
82
|
+
"""
|
83
|
+
if Logging.LOG_LEVEL <= Logging.Level.SEVERE:
|
84
|
+
Logging.__log(Logging.Level.SEVERE, message)
|
85
|
+
|
86
|
+
if exception:
|
87
|
+
exc_type, exc_value, exc_traceback = type(exception), exception, exception.__traceback__
|
88
|
+
tb_str = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback, limit=1))
|
89
|
+
|
90
|
+
for error in tb_str.split("\n"):
|
91
|
+
Logging.__log(Logging.Level.SEVERE, error)
|