sdv-installer 0.0.0.dev11__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.
- sdv_installer/__init__.py +7 -0
- sdv_installer/authentication/__init__.py +8 -0
- sdv_installer/authentication/authentication.py +74 -0
- sdv_installer/cli/__init__.py +1 -0
- sdv_installer/cli/__main__.py +165 -0
- sdv_installer/config.py +19 -0
- sdv_installer/constants.py +130 -0
- sdv_installer/installation/__init__.py +17 -0
- sdv_installer/installation/installer.py +490 -0
- sdv_installer/utils/__init__.py +52 -0
- sdv_installer/utils/console_utils.py +277 -0
- sdv_installer/utils/data_storage.py +86 -0
- sdv_installer/utils/package_utils.py +268 -0
- sdv_installer/utils/request_error_handling.py +53 -0
- sdv_installer/utils/system_requirements.py +173 -0
- sdv_installer-0.0.0.dev11.dist-info/METADATA +120 -0
- sdv_installer-0.0.0.dev11.dist-info/RECORD +21 -0
- sdv_installer-0.0.0.dev11.dist-info/WHEEL +5 -0
- sdv_installer-0.0.0.dev11.dist-info/entry_points.txt +2 -0
- sdv_installer-0.0.0.dev11.dist-info/licenses/LICENSE +21 -0
- sdv_installer-0.0.0.dev11.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Utility functions for securely handling password input with character masking."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
from urllib.parse import urlparse, urlunparse
|
|
6
|
+
|
|
7
|
+
from sdv_installer.config import API_VALIDATE
|
|
8
|
+
from sdv_installer.constants import (
|
|
9
|
+
ACTION_ERROR_MESSAGE,
|
|
10
|
+
ACTION_SUCCESS_ALREADY_INSTALLED,
|
|
11
|
+
ACTION_SUCCESS_ALREADY_UPDATED,
|
|
12
|
+
ACTION_SUCCESS_MESSAGE,
|
|
13
|
+
ADDITIONAL_DEPS_MESSAGE_PREFIX,
|
|
14
|
+
BACKSPACE_KEYS,
|
|
15
|
+
ENTER_KEY,
|
|
16
|
+
NEWLINE_KEY,
|
|
17
|
+
SUMMARY_PARTIAL_SUCCESS_MESSAGE,
|
|
18
|
+
SUMMARY_SUCCESS_MESSAGE,
|
|
19
|
+
SUMMARY_WARNING_MESSAGE,
|
|
20
|
+
ExitCode,
|
|
21
|
+
)
|
|
22
|
+
from sdv_installer.utils.package_utils import is_version_bigger, is_version_equal
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def print_with_flush(text):
|
|
26
|
+
"""Helper function to print text and flush the output immediately."""
|
|
27
|
+
sys.stdout.write(text)
|
|
28
|
+
sys.stdout.flush()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def handle_keypress(character, password):
|
|
32
|
+
"""Handles user keypresses: backspace and normal characteracter input."""
|
|
33
|
+
if character in BACKSPACE_KEYS:
|
|
34
|
+
if password:
|
|
35
|
+
password = password[:-1]
|
|
36
|
+
print_with_flush('\b \b')
|
|
37
|
+
|
|
38
|
+
else:
|
|
39
|
+
password += character
|
|
40
|
+
print_with_flush('*')
|
|
41
|
+
|
|
42
|
+
return password
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_password(prompt, get_char_func):
|
|
46
|
+
"""Generic password input handler."""
|
|
47
|
+
print_with_flush(prompt)
|
|
48
|
+
password = ''
|
|
49
|
+
while True:
|
|
50
|
+
char = get_char_func()
|
|
51
|
+
char = char.decode('utf-8') if isinstance(char, bytes) else char
|
|
52
|
+
if char in [ENTER_KEY, NEWLINE_KEY]:
|
|
53
|
+
print_with_flush('\n')
|
|
54
|
+
break
|
|
55
|
+
password = handle_keypress(char, password)
|
|
56
|
+
|
|
57
|
+
return password
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_char_win():
|
|
61
|
+
"""Windows-specific character reading function."""
|
|
62
|
+
import msvcrt
|
|
63
|
+
|
|
64
|
+
return msvcrt.getch()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_char_unix():
|
|
68
|
+
"""Unix-specific character reading function."""
|
|
69
|
+
import termios
|
|
70
|
+
import tty
|
|
71
|
+
|
|
72
|
+
fd = sys.stdin.fileno()
|
|
73
|
+
old_settings = termios.tcgetattr(fd)
|
|
74
|
+
try:
|
|
75
|
+
tty.setraw(fd)
|
|
76
|
+
return sys.stdin.read(1)
|
|
77
|
+
finally:
|
|
78
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_password_input(prompt='License Key: '):
|
|
82
|
+
"""Password getter with stars."""
|
|
83
|
+
if sys.platform == 'win32':
|
|
84
|
+
return get_password(prompt, get_char_win)
|
|
85
|
+
|
|
86
|
+
return get_password(prompt, get_char_unix)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def print_message(message):
|
|
90
|
+
"""Function to be used to print messages across the library."""
|
|
91
|
+
print(message) # noqa
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def handle_process_exit_code(
|
|
95
|
+
process,
|
|
96
|
+
package,
|
|
97
|
+
printable_package,
|
|
98
|
+
version=None,
|
|
99
|
+
action=None,
|
|
100
|
+
installed_packages=None,
|
|
101
|
+
upgrade=False,
|
|
102
|
+
):
|
|
103
|
+
"""Handle and display messages based on a subprocess exit code.
|
|
104
|
+
|
|
105
|
+
Determines the appropriate success or error message to display
|
|
106
|
+
based on the exit code of a subprocess and the package installation context.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
process (subprocess.Process):
|
|
110
|
+
A completed subprocess with a `returncode` attribute.
|
|
111
|
+
package (str):
|
|
112
|
+
The internal package name.
|
|
113
|
+
printable_package (str):
|
|
114
|
+
The user-friendly package name for display.
|
|
115
|
+
version (str, optional):
|
|
116
|
+
The target version of the package. Defaults to None.
|
|
117
|
+
action (str, optional):
|
|
118
|
+
The action performed (e.g., 'install', 'upgrade'). Defaults to None.
|
|
119
|
+
installed_packages (dict, optional):
|
|
120
|
+
A mapping of package names to installed versions. Defaults to None.
|
|
121
|
+
upgrade (bool, optional):
|
|
122
|
+
Indicates whether this is an upgrade operation. Defaults to False.
|
|
123
|
+
"""
|
|
124
|
+
if process.returncode == ExitCode.SUCCESS:
|
|
125
|
+
msg = ACTION_SUCCESS_MESSAGE.get(action)
|
|
126
|
+
if action == 'install':
|
|
127
|
+
installed_version = installed_packages.get(package) if installed_packages else None
|
|
128
|
+
if installed_version:
|
|
129
|
+
is_installed_bigger = is_version_bigger(installed_version, version)
|
|
130
|
+
is_installed_equal = is_version_equal(installed_version, version)
|
|
131
|
+
if upgrade and is_installed_bigger or is_installed_equal:
|
|
132
|
+
msg = ACTION_SUCCESS_ALREADY_UPDATED.get(action)
|
|
133
|
+
elif installed_version and is_installed_equal:
|
|
134
|
+
msg = ACTION_SUCCESS_ALREADY_INSTALLED.get(action)
|
|
135
|
+
|
|
136
|
+
elif process.returncode >= ExitCode.ERROR:
|
|
137
|
+
msg = ACTION_ERROR_MESSAGE.get(action)
|
|
138
|
+
|
|
139
|
+
sys.stdout.write(f'\r{printable_package} - {msg}\n')
|
|
140
|
+
sys.stdout.flush()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def display_progress_animation(
|
|
144
|
+
process,
|
|
145
|
+
package,
|
|
146
|
+
version=None,
|
|
147
|
+
action=None,
|
|
148
|
+
installed_packages=None,
|
|
149
|
+
upgrade=False,
|
|
150
|
+
show_version=False,
|
|
151
|
+
):
|
|
152
|
+
"""Display an animated progress indicator while a subprocess is running.
|
|
153
|
+
|
|
154
|
+
This function continuously displays an animated progress message
|
|
155
|
+
(e.g., "package .", "package ..", "package ...") until the given
|
|
156
|
+
subprocess finishes. Once completed, it prints a final message
|
|
157
|
+
based on the process's exit code.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
process (subprocess.Popen):
|
|
161
|
+
The subprocess to monitor.
|
|
162
|
+
package (str):
|
|
163
|
+
The name of the package being processed.
|
|
164
|
+
version (str, optional):
|
|
165
|
+
An optional version string to display with the package name.
|
|
166
|
+
action (str, optional):
|
|
167
|
+
The action being performed (e.g., "install", "download"). Used to determine
|
|
168
|
+
the appropriate success or error message.
|
|
169
|
+
"""
|
|
170
|
+
printable_package = f'{package} (version {version})' if show_version and version else package
|
|
171
|
+
|
|
172
|
+
frames = [f'{printable_package} . ', f'{printable_package} .. ', f'{printable_package} ...']
|
|
173
|
+
while True:
|
|
174
|
+
for frame in frames:
|
|
175
|
+
sys.stdout.write(f'\r{frame}')
|
|
176
|
+
sys.stdout.flush()
|
|
177
|
+
time.sleep(0.3)
|
|
178
|
+
if process.poll() is not None:
|
|
179
|
+
handle_process_exit_code(
|
|
180
|
+
process,
|
|
181
|
+
package,
|
|
182
|
+
printable_package,
|
|
183
|
+
version,
|
|
184
|
+
action,
|
|
185
|
+
installed_packages,
|
|
186
|
+
upgrade,
|
|
187
|
+
)
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def print_invalid_credentials():
|
|
192
|
+
"""Print an error message for invalid or expired SDV Enterprise credentials."""
|
|
193
|
+
print_message(
|
|
194
|
+
'Error installing SDV Enterprise. This may be due to an invalid '
|
|
195
|
+
'username/license key combo, or because your license key has expired. '
|
|
196
|
+
"Please double-check that you're providing the right credentials. If "
|
|
197
|
+
"you're continuing to experience issues, please reach out to the "
|
|
198
|
+
'DataCebo team. '
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def print_failed_to_connect():
|
|
203
|
+
"""Print an error message indicating failure to connect to the authentication server."""
|
|
204
|
+
print_message(
|
|
205
|
+
'Failed to connect to the authentication server.\n'
|
|
206
|
+
'Please check your internet connection or firewall settings and that the API server '
|
|
207
|
+
f'{API_VALIDATE} is reachable.'
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def print_warning_base_connector_package_installed():
|
|
212
|
+
"""Print a 'warning' message indicating that the base connector package will be installed."""
|
|
213
|
+
print_message(
|
|
214
|
+
"\nWarning: In order to use AI Connectors, you'll need to install "
|
|
215
|
+
'database-specific packages. Please use the `--options` '
|
|
216
|
+
'flag to specify your database variants.'
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def print_additional_dependencies_installed(additional_dependencies):
|
|
221
|
+
"""Print a message indicating that additional enterprise-related packages were installed.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
action (list[str]):
|
|
225
|
+
The list of additional packages that were installed.
|
|
226
|
+
"""
|
|
227
|
+
if len(additional_dependencies) == 0:
|
|
228
|
+
return
|
|
229
|
+
message = f'{ADDITIONAL_DEPS_MESSAGE_PREFIX} ('
|
|
230
|
+
message += ', '.join(additional_dependencies) + ').'
|
|
231
|
+
print_message('\n' + message)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def print_package_summary(action, package_status, sdv_included):
|
|
235
|
+
"""Print a summary message indicating that whether or not the installation was successful.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
action (str):
|
|
239
|
+
The action that was performed (download or install).
|
|
240
|
+
package_status (dict):
|
|
241
|
+
A dictionary with the packages and whether or not the action was successfully performed.
|
|
242
|
+
sdv_included (bool):
|
|
243
|
+
Whether sdv-enterprise was included in the successful result.
|
|
244
|
+
"""
|
|
245
|
+
all_success = all(package_status.values())
|
|
246
|
+
|
|
247
|
+
if all_success and sdv_included:
|
|
248
|
+
print_message('\n' + SUMMARY_SUCCESS_MESSAGE[action])
|
|
249
|
+
|
|
250
|
+
elif all_success and not sdv_included:
|
|
251
|
+
print_message('\n' + SUMMARY_PARTIAL_SUCCESS_MESSAGE[action])
|
|
252
|
+
else:
|
|
253
|
+
print_message('\n' + SUMMARY_WARNING_MESSAGE[action])
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def mask_license_key(url):
|
|
257
|
+
"""Mask the license key in a URL with stars.
|
|
258
|
+
|
|
259
|
+
This function takes a URL that contains embedded basic authentication
|
|
260
|
+
credentials (username and password/license key) and replaces the
|
|
261
|
+
license key with a masked value ("****") for safe logging or display.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
url (str):
|
|
265
|
+
The URL containing basic auth credentials.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
str:
|
|
269
|
+
The URL with the license key masked. If no username is present,
|
|
270
|
+
the original URL is returned.
|
|
271
|
+
"""
|
|
272
|
+
parsed = urlparse(url)
|
|
273
|
+
if parsed.username:
|
|
274
|
+
netloc = f'{parsed.username}:****@{parsed.hostname}'
|
|
275
|
+
return urlunparse(parsed._replace(netloc=netloc))
|
|
276
|
+
|
|
277
|
+
return url
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Utilities to handle appdirs and store application data."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from platformdirs import user_data_dir
|
|
8
|
+
|
|
9
|
+
APP_NAME = 'SDV-Installer'
|
|
10
|
+
APP_AUTHOR = 'DataCebo'
|
|
11
|
+
LOGGER = logging.getLogger(__name__)
|
|
12
|
+
CONFIG_PATH = Path(__file__).resolve().parent
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def store_package_name(package):
|
|
16
|
+
"""Store the installed package name in a text file in a platform-specific directory.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
packages (str):
|
|
20
|
+
A string representing the package name.
|
|
21
|
+
"""
|
|
22
|
+
storage_dir = Path(user_data_dir(APP_NAME, APP_AUTHOR))
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
storage_dir.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
except PermissionError:
|
|
27
|
+
# No access to user data dir, fall back if possible
|
|
28
|
+
if os.access(CONFIG_PATH, os.W_OK):
|
|
29
|
+
storage_dir = CONFIG_PATH
|
|
30
|
+
else:
|
|
31
|
+
# Nowhere to write
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
except OSError:
|
|
35
|
+
# Filesystem might be read-only or inaccessible
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
package_file = storage_dir / 'installed_packages.txt'
|
|
39
|
+
try:
|
|
40
|
+
with package_file.open('a') as installed_packages:
|
|
41
|
+
installed_packages.write(package + '\n')
|
|
42
|
+
|
|
43
|
+
except (PermissionError, OSError) as error:
|
|
44
|
+
LOGGER.error(f"Failed to write installed package '{package}' to '{package_file}': {error}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def read_stored_packages():
|
|
48
|
+
"""Read the list of installed packages from the platform-specific data file.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
list[str]:
|
|
52
|
+
A list of installed package names.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
FileNotFoundError:
|
|
56
|
+
If the file does not exist.
|
|
57
|
+
"""
|
|
58
|
+
storage_dir = Path(user_data_dir(APP_NAME, APP_AUTHOR))
|
|
59
|
+
package_file = storage_dir / 'installed_packages.txt'
|
|
60
|
+
|
|
61
|
+
if not package_file.exists():
|
|
62
|
+
raise FileNotFoundError('No stored package file found.')
|
|
63
|
+
|
|
64
|
+
with package_file.open('r') as installed_packages:
|
|
65
|
+
return set([line.strip() for line in installed_packages if line.strip()])
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def remove_package_name(package):
|
|
69
|
+
"""Remove a single package name from the installed packages file.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
package (str):
|
|
73
|
+
A string representing the package name to remove.
|
|
74
|
+
"""
|
|
75
|
+
storage_dir = Path(user_data_dir(APP_NAME, APP_AUTHOR))
|
|
76
|
+
package_file = storage_dir / 'installed_packages.txt'
|
|
77
|
+
|
|
78
|
+
if not package_file.exists():
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
# Read all packages and filter out the one to remove
|
|
82
|
+
with package_file.open('r') as package_reader:
|
|
83
|
+
packages = [line.strip() for line in package_reader if line.strip() != package]
|
|
84
|
+
|
|
85
|
+
with package_file.open('w') as package_writer:
|
|
86
|
+
package_writer.write('\n'.join(packages) if packages else '')
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Utility functions for package and virtual env."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import pathlib
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from importlib.metadata import distributions
|
|
8
|
+
from typing import Dict, Tuple
|
|
9
|
+
|
|
10
|
+
from packaging.requirements import Requirement
|
|
11
|
+
from packaging.utils import parse_sdist_filename, parse_wheel_filename
|
|
12
|
+
from packaging.version import InvalidVersion, Version
|
|
13
|
+
|
|
14
|
+
from sdv_installer.constants import EXPECTED_PACKAGES, ProductType
|
|
15
|
+
|
|
16
|
+
BASE_PREFIX = 'sdv-enterprise'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def list_current_installed_packages():
|
|
20
|
+
"""Returns a list of currently installed package names.
|
|
21
|
+
|
|
22
|
+
Uses `importlib.metadata.distributions()` to retrieve metadata for all installed
|
|
23
|
+
distributions and extracts their package names.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
List[str]:
|
|
27
|
+
A list of package names currently installed in the environment.
|
|
28
|
+
"""
|
|
29
|
+
return [dist.metadata['Name'] for dist in distributions()]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def list_current_installed_packages_with_their_version():
|
|
33
|
+
"""Returns a list of currently installed package names.
|
|
34
|
+
|
|
35
|
+
Uses `importlib.metadata.distributions()` to retrieve metadata for all installed
|
|
36
|
+
distributions and extracts their package names.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
List[str]:
|
|
40
|
+
A list of package names currently installed in the environment.
|
|
41
|
+
"""
|
|
42
|
+
return {dist.metadata['Name']: dist.metadata['Version'] for dist in distributions()}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def clean_path(path: str):
|
|
46
|
+
"""Cleans a file path by stripping leading/trailing whitespace and converting to lowercase.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
path (str):
|
|
50
|
+
The input file path as a string.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
str:
|
|
54
|
+
The cleaned file path.
|
|
55
|
+
"""
|
|
56
|
+
return path.strip().lower()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_package_name(file_path: str):
|
|
60
|
+
"""Extracts the package name from a given file path.
|
|
61
|
+
|
|
62
|
+
Supports `.whl`, `.tar.gz`, and other archive formats. For wheel and source distribution
|
|
63
|
+
files, the name is parsed using appropriate helper functions. For other file types,
|
|
64
|
+
the base file name is returned.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
file_path (str):
|
|
68
|
+
Path to the package file.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
str or None:
|
|
72
|
+
The extracted package name, or None if the path is invalid.
|
|
73
|
+
"""
|
|
74
|
+
package_name = None
|
|
75
|
+
if not file_path or not file_path.strip():
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
file_path = clean_path(file_path)
|
|
79
|
+
if file_path.endswith('.whl') or file_path.endswith('.tar.gz'):
|
|
80
|
+
filename = os.path.basename(file_path)
|
|
81
|
+
if file_path.endswith('.whl'):
|
|
82
|
+
package_name, _, _, _ = parse_wheel_filename(filename)
|
|
83
|
+
else:
|
|
84
|
+
package_name, _ = parse_sdist_filename(filename)
|
|
85
|
+
else:
|
|
86
|
+
package_name = pathlib.PurePath(file_path).name
|
|
87
|
+
|
|
88
|
+
return package_name
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_latest_package_version(package_name, index_url=None):
|
|
92
|
+
"""Returns a sorted list of available versions for a given package.
|
|
93
|
+
|
|
94
|
+
This function invokes `pip index versions` via subprocess to retrieve the
|
|
95
|
+
list of available versions for a specified package from the given Python
|
|
96
|
+
package index (PyPI or a custom index).
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
package_name (str):
|
|
100
|
+
The name of the package to query.
|
|
101
|
+
index_url (str, optional):
|
|
102
|
+
The base URL of the package index to use. If not provided, the default PyPI
|
|
103
|
+
index is used.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
str:
|
|
107
|
+
Returns a string representing the highest version available.
|
|
108
|
+
Returns None if the pip command fails.
|
|
109
|
+
"""
|
|
110
|
+
cmd = [sys.executable, '-m', 'pip', 'index', 'versions', package_name]
|
|
111
|
+
if index_url:
|
|
112
|
+
cmd += ['--index-url', index_url]
|
|
113
|
+
|
|
114
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
115
|
+
if result.returncode != 0:
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
for line in result.stdout.splitlines():
|
|
119
|
+
if line.startswith('Available versions: '):
|
|
120
|
+
text_versions = line.replace('Available versions: ', '')
|
|
121
|
+
text_versions = text_versions.split(',')
|
|
122
|
+
break
|
|
123
|
+
|
|
124
|
+
return text_versions[0]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def split_base_and_bundles(
|
|
128
|
+
packages: Dict[str, str], use_product_type: bool = True
|
|
129
|
+
) -> Tuple[str, list]:
|
|
130
|
+
"""Split base sdv-enterprise packages from bundles.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
packages (dict): List of package dicts. Each dict has the keys 'package_name' and
|
|
134
|
+
'package_type'.
|
|
135
|
+
use_product_type (bool): Whether or not to use the product type attribute to figure out
|
|
136
|
+
if a package is a base package or bundle. If false, fall back to using the package name.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
tuple (list[str], list[str]). A list of base packages and a list of bundle packages.
|
|
140
|
+
"""
|
|
141
|
+
base_packages = []
|
|
142
|
+
bundles = []
|
|
143
|
+
if use_product_type:
|
|
144
|
+
for package, product_type in packages.items():
|
|
145
|
+
if product_type == ProductType.BASE.value:
|
|
146
|
+
base_packages.append(package)
|
|
147
|
+
elif product_type == ProductType.BUNDLE.value:
|
|
148
|
+
bundles.append(package)
|
|
149
|
+
else:
|
|
150
|
+
for package in packages:
|
|
151
|
+
if package.startswith(BASE_PREFIX):
|
|
152
|
+
base_packages.append(package)
|
|
153
|
+
else:
|
|
154
|
+
bundles.append(package)
|
|
155
|
+
|
|
156
|
+
return sorted(base_packages), sorted(bundles)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def is_version_bigger(installed_version, requested_version):
|
|
160
|
+
"""Check if the installed version is greater than the requested version."""
|
|
161
|
+
if isinstance(installed_version, str) and isinstance(requested_version, str):
|
|
162
|
+
try:
|
|
163
|
+
return Version(installed_version) > Version(requested_version)
|
|
164
|
+
except InvalidVersion:
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def is_version_equal(installed_version, requested_version):
|
|
171
|
+
"""Check if the installed version is equal to the requested version."""
|
|
172
|
+
if isinstance(installed_version, str) and isinstance(requested_version, str):
|
|
173
|
+
try:
|
|
174
|
+
return Version(installed_version) == Version(requested_version)
|
|
175
|
+
except InvalidVersion:
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def convert_to_set(obj):
|
|
182
|
+
"""Convert a list, tuple, str, int, float, or dictionary to a set."""
|
|
183
|
+
if isinstance(obj, (list, tuple)):
|
|
184
|
+
obj = set(obj)
|
|
185
|
+
elif isinstance(obj, (str, int, float)):
|
|
186
|
+
obj = {obj}
|
|
187
|
+
elif isinstance(obj, dict):
|
|
188
|
+
obj = set(obj.keys())
|
|
189
|
+
return obj
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def get_requirement_name(requirement_name):
|
|
193
|
+
"""Get the name of a requirement, useful to remove extra_requires, specifiers, markers."""
|
|
194
|
+
if isinstance(requirement_name, str):
|
|
195
|
+
return Requirement(requirement_name).name
|
|
196
|
+
return requirement_name
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def is_none_or_empty_iterate(obj):
|
|
200
|
+
"""Check if object is None or an empty list/dict/iterate."""
|
|
201
|
+
return obj is None or (hasattr(obj, '__len__') and len(obj) == 0)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def determine_additional_sdv_enterprise_deps(
|
|
205
|
+
packages_before_install,
|
|
206
|
+
packages_after_install,
|
|
207
|
+
user_requested_packages,
|
|
208
|
+
all_sdv_enterprise_related_pkgs=EXPECTED_PACKAGES,
|
|
209
|
+
):
|
|
210
|
+
"""Determine the additional sdv-enterprise related packages installed, given before & after.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
packages_before_install (set, list[str]):
|
|
214
|
+
All packages before installation with sdv-installer.
|
|
215
|
+
|
|
216
|
+
packages_after_install (set, list[str]):
|
|
217
|
+
All packages after installation with sdv-installer.
|
|
218
|
+
|
|
219
|
+
user_requested_packages (set, list[str], dict[str, str], str, None)
|
|
220
|
+
The user requested packages to install. If it is an itert
|
|
221
|
+
|
|
222
|
+
all_sdv_enterprise_related_pkgs (set, list[str])
|
|
223
|
+
The package names of all base packages and bundle packages
|
|
224
|
+
that could be installed by the user.
|
|
225
|
+
Defaults to EXPECTED_PACKAGES
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
(set): The additional sdv-enterprise related packages that were installed.
|
|
229
|
+
"""
|
|
230
|
+
if is_none_or_empty_iterate(user_requested_packages):
|
|
231
|
+
# User did not say a specific package to install
|
|
232
|
+
# Return empty list to specify no additional deps were installed (everything installed)
|
|
233
|
+
return {}
|
|
234
|
+
|
|
235
|
+
packages_before_install = convert_to_set(packages_before_install)
|
|
236
|
+
packages_after_install = convert_to_set(packages_after_install)
|
|
237
|
+
|
|
238
|
+
user_requested_packages = convert_to_set(user_requested_packages)
|
|
239
|
+
user_requested_packages = {get_requirement_name(pkg) for pkg in user_requested_packages}
|
|
240
|
+
|
|
241
|
+
all_sdv_enterprise_related_pkgs = convert_to_set(all_sdv_enterprise_related_pkgs)
|
|
242
|
+
|
|
243
|
+
all_installed_pkgs = packages_after_install.difference(packages_before_install)
|
|
244
|
+
installed_pkgs_related_to_sdv_enterprise = all_installed_pkgs.intersection(
|
|
245
|
+
all_sdv_enterprise_related_pkgs
|
|
246
|
+
)
|
|
247
|
+
additional_dependencies_installed = installed_pkgs_related_to_sdv_enterprise.difference(
|
|
248
|
+
user_requested_packages
|
|
249
|
+
)
|
|
250
|
+
return additional_dependencies_installed
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def check_is_sdv_enterprise_included(packages):
|
|
254
|
+
"""Check if any of the given package names includes 'sdv-enterprise'.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
packages (list of str):
|
|
258
|
+
A list of package names.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
bool:
|
|
262
|
+
True if 'sdv-enterprise' is found in any package name, False otherwise.
|
|
263
|
+
"""
|
|
264
|
+
for package in packages:
|
|
265
|
+
if package.startswith(BASE_PREFIX):
|
|
266
|
+
return True
|
|
267
|
+
|
|
268
|
+
return False
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Utility functions to handle requests HTTP errors."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class NotFoundError(ValueError):
|
|
5
|
+
"""Error to be raised when an end point is not found (404)."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def handle_http_error_response(response):
|
|
9
|
+
"""Handle HTTP error responses and raises appropriate exceptions with error messages.
|
|
10
|
+
|
|
11
|
+
The function attempts to parse the error details from the response as JSON. If the content
|
|
12
|
+
cannot be parsed as JSON, the raw response text will be used.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
response (requests.Response):
|
|
16
|
+
The HTTP response object that contains the status code and error details.
|
|
17
|
+
|
|
18
|
+
Raises:
|
|
19
|
+
ValueError:
|
|
20
|
+
If the status code is 400 (Bad Request) or 422 (Validation Error).
|
|
21
|
+
PermissionError:
|
|
22
|
+
If the status code is 401 (Unauthorized) or 403 (Forbidden).
|
|
23
|
+
NotFoundError:
|
|
24
|
+
If the status code is 404 (Not Found).
|
|
25
|
+
RuntimeError:
|
|
26
|
+
If the status code is 405 (Method Not Allowed) or 5xx (Server Error).
|
|
27
|
+
Exception:
|
|
28
|
+
For any unexpected status code.
|
|
29
|
+
"""
|
|
30
|
+
status = response.status_code
|
|
31
|
+
try:
|
|
32
|
+
error_detail = response.json()
|
|
33
|
+
except ValueError:
|
|
34
|
+
error_detail = response.text
|
|
35
|
+
|
|
36
|
+
error_map = {
|
|
37
|
+
400: (ValueError, f'Bad Request: {error_detail}'),
|
|
38
|
+
401: (PermissionError, f'Invalid Credentials or Expired License Key: {error_detail}.'),
|
|
39
|
+
403: (PermissionError, 'Forbidden: You don’t have permission to access this resource.'),
|
|
40
|
+
404: (NotFoundError, 'Not Found: The requested endpoint does not exist.'),
|
|
41
|
+
405: (RuntimeError, 'Method Not Allowed: Check the HTTP method.'),
|
|
42
|
+
422: (ValueError, f'Validation Error: {error_detail}'),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if status >= 500:
|
|
46
|
+
exception, message = RuntimeError, f'Server Error ({status}): {error_detail}'
|
|
47
|
+
|
|
48
|
+
else:
|
|
49
|
+
exception, message = error_map.get(
|
|
50
|
+
status, (Exception, f'Unexpected Error ({status}): {error_detail}')
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
raise exception(message)
|