sdv-installer 0.0.2.dev0__tar.gz → 0.0.3.dev0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {sdv_installer-0.0.2.dev0/sdv_installer.egg-info → sdv_installer-0.0.3.dev0}/PKG-INFO +3 -4
  2. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0}/pyproject.toml +11 -5
  3. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0}/sdv_installer/__init__.py +1 -1
  4. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0}/sdv_installer/cli/__main__.py +1 -1
  5. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0}/sdv_installer/constants.py +17 -3
  6. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0}/sdv_installer/installation/installer.py +21 -3
  7. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0}/sdv_installer/utils/__init__.py +2 -2
  8. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0}/sdv_installer/utils/package_utils.py +15 -7
  9. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0}/sdv_installer/utils/system_requirements.py +89 -13
  10. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0/sdv_installer.egg-info}/PKG-INFO +3 -4
  11. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0}/sdv_installer.egg-info/requires.txt +1 -1
  12. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0}/LICENSE +0 -0
  13. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0}/README.md +0 -0
  14. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0}/sdv_installer/authentication/__init__.py +0 -0
  15. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0}/sdv_installer/authentication/authentication.py +0 -0
  16. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0}/sdv_installer/cli/__init__.py +0 -0
  17. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0}/sdv_installer/config.py +0 -0
  18. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0}/sdv_installer/installation/__init__.py +0 -0
  19. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0}/sdv_installer/utils/console_utils.py +0 -0
  20. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0}/sdv_installer/utils/data_storage.py +0 -0
  21. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0}/sdv_installer/utils/request_error_handling.py +0 -0
  22. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0}/sdv_installer.egg-info/SOURCES.txt +0 -0
  23. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0}/sdv_installer.egg-info/dependency_links.txt +0 -0
  24. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0}/sdv_installer.egg-info/entry_points.txt +0 -0
  25. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0}/sdv_installer.egg-info/top_level.txt +0 -0
  26. {sdv_installer-0.0.2.dev0 → sdv_installer-0.0.3.dev0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdv-installer
3
- Version: 0.0.2.dev0
3
+ Version: 0.0.3.dev0
4
4
  Summary: Package to install SDV Enterprise packages.
5
5
  Author-email: "DataCebo, Inc." <info@datacebo.com>
6
6
  License: MIT
@@ -10,19 +10,18 @@ Classifier: Intended Audience :: Developers
10
10
  Classifier: License :: Free for non-commercial use
11
11
  Classifier: Natural Language :: English
12
12
  Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.8
14
13
  Classifier: Programming Language :: Python :: 3.9
15
14
  Classifier: Programming Language :: Python :: 3.10
16
15
  Classifier: Programming Language :: Python :: 3.11
17
16
  Classifier: Programming Language :: Python :: 3.12
18
17
  Classifier: Programming Language :: Python :: 3.13
19
18
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
- Requires-Python: <3.14,>=3.8
19
+ Requires-Python: <3.14,>=3.9
21
20
  Description-Content-Type: text/markdown
22
21
  License-File: LICENSE
23
22
  Requires-Dist: requests
24
23
  Requires-Dist: platformdirs
25
- Requires-Dist: pip
24
+ Requires-Dist: pip>=22.3
26
25
  Requires-Dist: packaging
27
26
  Provides-Extra: test
28
27
  Requires-Dist: pytest; extra == "test"
@@ -12,7 +12,7 @@ name = "sdv-installer"
12
12
  authors = [{name = "DataCebo, Inc.", email = 'info@datacebo.com'}]
13
13
  description = "Package to install SDV Enterprise packages."
14
14
  readme = "README.md"
15
- requires-python = ">=3.8, <3.14"
15
+ requires-python = ">=3.9, <3.14"
16
16
  keywords = [
17
17
  "sdv",
18
18
  "synthetic-data",
@@ -28,7 +28,6 @@ classifiers = [
28
28
  "License :: Free for non-commercial use",
29
29
  "Natural Language :: English",
30
30
  "Programming Language :: Python :: 3",
31
- "Programming Language :: Python :: 3.8",
32
31
  "Programming Language :: Python :: 3.9",
33
32
  "Programming Language :: Python :: 3.10",
34
33
  "Programming Language :: Python :: 3.11",
@@ -39,7 +38,7 @@ classifiers = [
39
38
  dependencies = [
40
39
  'requests',
41
40
  'platformdirs',
42
- 'pip',
41
+ 'pip>=22.3',
43
42
  'packaging',
44
43
  ]
45
44
  dynamic = ["version"]
@@ -132,7 +131,6 @@ ignore = [
132
131
  # pydocstyle
133
132
  "D107", # Missing docstring in __init__
134
133
  "D417", # Missing argument descriptions in the docstring, this is a bug from pydocstyle: https://github.com/PyCQA/pydocstyle/issues/449
135
- "PD901",
136
134
  "PD101",
137
135
  ]
138
136
 
@@ -166,7 +164,7 @@ max-doc-length = 100
166
164
  max-line-length = 100
167
165
 
168
166
  [tool.bumpversion]
169
- current_version = "0.0.2.dev0"
167
+ current_version = "0.0.3.dev0"
170
168
  parse = '(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\.(?P<release>[a-z]+)(?P<candidate>\d+))?'
171
169
  serialize = [
172
170
  '{major}.{minor}.{patch}.{release}{candidate}',
@@ -202,3 +200,11 @@ replace = "__version__ = '{new_version}'"
202
200
  filename = "sdv_installer/cli/__main__.py"
203
201
  search = "SDV_INSTALLER_VERSION = '{current_version}'"
204
202
  replace = "SDV_INSTALLER_VERSION = '{new_version}'"
203
+
204
+ [tool.bandit]
205
+ exclude_dirs = [
206
+ "tests",
207
+ "scripts",
208
+ "build",
209
+ ".venv",
210
+ ]
@@ -4,4 +4,4 @@ from sdv_installer import config, constants
4
4
 
5
5
 
6
6
  __all__ = ('config', 'constants')
7
- __version__ = '0.0.2.dev0'
7
+ __version__ = '0.0.3.dev0'
@@ -13,7 +13,7 @@ from sdv_installer.installation import (
13
13
  from sdv_installer.utils.system_requirements import print_check_results
14
14
 
15
15
  # Adding a new version here to avoid circular imports
16
- SDV_INSTALLER_VERSION = '0.0.2.dev0'
16
+ SDV_INSTALLER_VERSION = '0.0.3.dev0'
17
17
 
18
18
 
19
19
  def install_action(args):
@@ -12,7 +12,9 @@ from packaging.version import Version
12
12
 
13
13
  from sdv_installer.config import PYPI_URL
14
14
 
15
- MIN_PYTHON_VERSION = Version('3.8')
15
+ # System Requirement Constants
16
+
17
+ MIN_PYTHON_VERSION = Version('3.9')
16
18
  MAX_PYTHON_VERSION = Version('3.13')
17
19
  REQUIRED_BITNESS = '64-bit'
18
20
 
@@ -33,10 +35,22 @@ SUPPORTED_PLATFORM_TAGS = [
33
35
  'musllinux_1_1_x86_64',
34
36
  'win_amd64',
35
37
  ]
38
+ SUPPORTED_OSES = ['macOS', 'Linux', 'Windows']
39
+ SUPPORTED_ARCHITECTURES = ['x86_64', 'arm64', 'amd64', 'win-amd64']
40
+ MIN_PIP_VERSION = Version('22.3')
41
+ MIN_OS_VERSIONS = {
42
+ 'macOS': Version('11'),
43
+ 'windows': Version('10.0.10240'),
44
+ 'linux': Version('5.10'),
45
+ }
46
+ SUPPORTED_LINUX_DISTROS = {
47
+ 'debian': Version('11'),
48
+ 'ubuntu': Version('22.04'),
49
+ }
36
50
 
51
+ # Packaging
37
52
  EXPECTED_PACKAGES = set([
38
53
  'sdv-enterprise',
39
- 'sdv-enterprise-full',
40
54
  'bundle-cag',
41
55
  'bundle-ai-connectors',
42
56
  'bundle-xsynthesizers',
@@ -54,7 +68,7 @@ CONNECTORS_PACKAGES = [
54
68
  'bundle-ai-connectors',
55
69
  ]
56
70
 
57
-
71
+ # Message Constants
58
72
  ACTION_SUCCESS_MESSAGE = {
59
73
  'install': 'Installed!',
60
74
  'uninstall -y': 'Uninstalled!',
@@ -37,7 +37,7 @@ from sdv_installer.utils import (
37
37
  print_warning_base_connector_package_installed,
38
38
  read_stored_packages,
39
39
  remove_package_name,
40
- split_base_and_bundles,
40
+ split_base_bundles_and_others,
41
41
  store_package_name,
42
42
  )
43
43
 
@@ -172,7 +172,7 @@ def _print_and_perform_action(
172
172
  use_product_type (bool):
173
173
  Whether or not to use the product type to aid in messages that get printed.
174
174
  """
175
- base_packages, bundles = split_base_and_bundles(packages, use_product_type)
175
+ base_packages, bundles, others = split_base_bundles_and_others(packages, use_product_type)
176
176
  action_cap = action.capitalize()
177
177
  package_status = {}
178
178
 
@@ -205,6 +205,20 @@ def _print_and_perform_action(
205
205
  )
206
206
  package_status.update(bundle_package_statuses)
207
207
 
208
+ if others:
209
+ print_message(f'\n{action_cap}ing Packages:')
210
+ other_package_statuses = _perform_pip_action(
211
+ action=action,
212
+ packages=others,
213
+ index_url=index_url,
214
+ debug=debug,
215
+ version=version,
216
+ extra_options=pip_options,
217
+ show_version=False,
218
+ upgrade=upgrade,
219
+ )
220
+ package_status.update(other_package_statuses)
221
+
208
222
  if set(CONNECTORS_PACKAGES).intersection(packages):
209
223
  print_warning_base_connector_package_installed()
210
224
 
@@ -480,11 +494,15 @@ def list_packages(username: str, license_key: str):
480
494
  package_to_product_type = _get_accessible_packages(username, license_key)
481
495
 
482
496
  # Filter bundles
483
- base_packages, bundles = split_base_and_bundles(package_to_product_type)
497
+ base_packages, bundles, others = split_base_bundles_and_others(package_to_product_type)
484
498
  bases_message = '\n'.join(base_packages)
485
499
  print_message(f'\nSDV Enterprise:\n{bases_message}')
486
500
  if bundles:
487
501
  bundles_message = '\n'.join(bundles)
488
502
  print_message(f'\nSDV Bundles:\n{bundles_message}')
489
503
 
504
+ if others:
505
+ others_message = '\n'.join(others)
506
+ print_message(f'\nAdditional Packages:\n{others_message}')
507
+
490
508
  return package_to_product_type
@@ -25,7 +25,7 @@ from sdv_installer.utils.package_utils import (
25
25
  get_package_name,
26
26
  list_current_installed_packages,
27
27
  list_current_installed_packages_with_their_version,
28
- split_base_and_bundles,
28
+ split_base_bundles_and_others,
29
29
  determine_additional_sdv_enterprise_deps,
30
30
  )
31
31
  from sdv_installer.utils.request_error_handling import handle_http_error_response
@@ -51,6 +51,6 @@ __all__ = (
51
51
  'print_warning_base_connector_package_installed',
52
52
  'read_stored_packages',
53
53
  'remove_package_name',
54
- 'split_base_and_bundles',
54
+ 'split_base_bundles_and_others',
55
55
  'store_package_name',
56
56
  )
@@ -15,6 +15,7 @@ from sdv_installer.config import TIMEOUT
15
15
  from sdv_installer.constants import EXPECTED_PACKAGES, ProductType
16
16
 
17
17
  BASE_PREFIX = 'sdv-enterprise'
18
+ BUNDLE_PREFIX = 'bundle-'
18
19
 
19
20
 
20
21
  def list_current_installed_packages():
@@ -125,36 +126,43 @@ def get_latest_package_version(package_name, index_url=None):
125
126
  return text_versions[0]
126
127
 
127
128
 
128
- def split_base_and_bundles(
129
+ def split_base_bundles_and_others(
129
130
  packages: Dict[str, str], use_product_type: bool = True
130
131
  ) -> Tuple[str, list]:
131
132
  """Split base sdv-enterprise packages from bundles.
132
133
 
133
134
  Args:
134
- packages (dict): List of package dicts. Each dict has the keys 'package_name' and
135
- 'package_type'.
136
- use_product_type (bool): Whether or not to use the product type attribute to figure out
137
- if a package is a base package or bundle. If false, fall back to using the package name.
135
+ packages (dict):
136
+ List of package dicts. Each dict has the keys 'package_name' and 'package_type'.
137
+ use_product_type (bool):
138
+ Whether or not to use the product type attribute to figure out
139
+ if a package is a base package or bundle. If false, fall back to using the package name.
138
140
 
139
141
  Returns:
140
142
  tuple (list[str], list[str]). A list of base packages and a list of bundle packages.
141
143
  """
142
144
  base_packages = []
143
145
  bundles = []
146
+ others = []
144
147
  if use_product_type:
145
148
  for package, product_type in packages.items():
146
149
  if product_type == ProductType.BASE.value:
147
150
  base_packages.append(package)
148
151
  elif product_type == ProductType.BUNDLE.value:
149
152
  bundles.append(package)
153
+ else:
154
+ others.append(package)
155
+
150
156
  else:
151
157
  for package in packages:
152
158
  if package.startswith(BASE_PREFIX):
153
159
  base_packages.append(package)
154
- else:
160
+ elif package.startswith(BUNDLE_PREFIX):
155
161
  bundles.append(package)
162
+ else:
163
+ others.append(package)
156
164
 
157
- return sorted(base_packages), sorted(bundles)
165
+ return sorted(base_packages), sorted(bundles), sorted(others)
158
166
 
159
167
 
160
168
  def is_version_bigger(installed_version, requested_version):
@@ -12,21 +12,59 @@ from packaging.version import Version
12
12
 
13
13
  from sdv_installer.constants import (
14
14
  MAX_PYTHON_VERSION,
15
+ MIN_OS_VERSIONS,
16
+ MIN_PIP_VERSION,
15
17
  MIN_PYTHON_VERSION,
16
18
  REQUIRED_BITNESS,
19
+ SUPPORTED_ARCHITECTURES,
20
+ SUPPORTED_LINUX_DISTROS,
21
+ SUPPORTED_OSES,
17
22
  )
18
23
  from sdv_installer.utils.console_utils import print_message
19
24
 
25
+ LINUX_OS_RELEASE_FILE = '/etc/os-release'
26
+
27
+
28
+ def _normalize_kernel_version(kernel_version):
29
+ parts = kernel_version.split('.')
30
+ normalized_version = []
31
+ for part in parts:
32
+ num = ''
33
+ for ch in part:
34
+ if ch.isdigit():
35
+ num += ch
36
+ else:
37
+ break
38
+
39
+ if not num:
40
+ break
41
+
42
+ normalized_version.append(num)
43
+
44
+ return '.'.join(normalized_version)
45
+
46
+
47
+ def _get_linux_distro_version():
48
+ with open(LINUX_OS_RELEASE_FILE) as f:
49
+ data = dict(line.strip().split('=', 1) for line in f if '=' in line)
50
+
51
+ linux_distro_version = data.get('VERSION_ID', '').strip('"')
52
+ return Version(linux_distro_version)
53
+
20
54
 
21
55
  def get_os_info():
22
56
  """Detects the operating system type and version.
23
57
 
24
58
  Returns:
25
59
  Tuple[str, str, Optional[str], bool]: A tuple containing:
26
- - os_system (str): 'macOS', 'Linux', or other OS name.
27
- - os_version (str): The major OS version (e.g., '14').
28
- - linux_distro (Optional[str]): Pretty name of the Linux distribution, if applicable.
29
- - is_linux (bool): Whether the OS is Linux.
60
+ - os_system (str):
61
+ 'macOS', 'Linux', or other OS name.
62
+ - os_version (str):
63
+ The complete OS version (e.g., '15.6.1').
64
+ - linux_distro (Optional[str]):
65
+ Pretty name of the Linux distribution, if applicable.
66
+ - is_linux (bool):
67
+ Whether the OS is Linux.
30
68
  """
31
69
  system = platform.system()
32
70
  version = platform.version()
@@ -38,12 +76,14 @@ def get_os_info():
38
76
  system = 'macOS'
39
77
 
40
78
  elif is_linux:
41
- version = platform.release()
42
- output = subprocess.run('cat /etc/*-release', shell=True, capture_output=True, text=True)
43
- match = re.search(r"PRETTY_NAME='(.*)'", output.stdout.strip())
79
+ version = _normalize_kernel_version(platform.release())
80
+ output = subprocess.run(
81
+ f'cat {LINUX_OS_RELEASE_FILE}', shell=True, capture_output=True, text=True
82
+ )
83
+ match = re.search(r'PRETTY_NAME="(.*)"', output.stdout.strip())
44
84
  linux_distro = match.group(1) if match else None
45
85
 
46
- return system, version.split('.')[0], linux_distro, is_linux
86
+ return system, version, linux_distro, is_linux
47
87
 
48
88
 
49
89
  def get_architecture():
@@ -120,6 +160,11 @@ def get_user_supported_tags():
120
160
  return {tag.platform for tag in sys_tags()}
121
161
 
122
162
 
163
+ def get_pip_version_code():
164
+ """Retrive the `pip` version of the current environment."""
165
+ return Version(pip.__version__)
166
+
167
+
123
168
  def validate_system_requirements(info):
124
169
  """Validates the current system against the minimum technical requirements.
125
170
 
@@ -130,13 +175,44 @@ def validate_system_requirements(info):
130
175
  List[str]: A list of keys that failed validation (e.g., ['python_version']).
131
176
  """
132
177
  errors = []
178
+
179
+ # Validate Python version
133
180
  python_version = get_python_version_code()
134
181
  if python_version < MIN_PYTHON_VERSION or python_version > MAX_PYTHON_VERSION:
135
182
  errors.append('python_version')
136
183
 
184
+ # Validate system bitness
137
185
  if info['system_bit'] != REQUIRED_BITNESS:
138
186
  errors.append('system_bit')
139
187
 
188
+ # Validate OS
189
+ if info['os_system'] not in SUPPORTED_OSES:
190
+ errors.append('os_system')
191
+
192
+ # Validate os minimal version
193
+ if Version(info['os_version']) < MIN_OS_VERSIONS.get(info['os_system'], Version('0')):
194
+ errors.append('os_version')
195
+
196
+ # Validate architecture
197
+ if info['architecture'] not in SUPPORTED_ARCHITECTURES:
198
+ errors.append('architecture')
199
+
200
+ # Validate pip minimal version
201
+ if Version(info['pip_version']) < MIN_PIP_VERSION:
202
+ errors.append('pip_version')
203
+
204
+ if info.get('linux_distro'):
205
+ linux_distro = info.get('linux_distro', '').lower()
206
+ min_version = None
207
+ linux_version = _get_linux_distro_version()
208
+ if 'ubuntu' in linux_distro:
209
+ min_version = SUPPORTED_LINUX_DISTROS.get('ubuntu')
210
+ elif 'debian' in linux_distro:
211
+ min_version = SUPPORTED_LINUX_DISTROS.get('debian')
212
+
213
+ if min_version and linux_version < min_version:
214
+ errors.append('linux_distro')
215
+
140
216
  return errors
141
217
 
142
218
 
@@ -149,15 +225,15 @@ def print_check_results():
149
225
  def mark(key):
150
226
  return ' (!)' if key in errors else ''
151
227
 
152
- print_message(f'Operating system: {info["os_system"]}')
153
- print_message(f'OS Version: {info["os_version"]}')
228
+ print_message(f'Operating system: {info["os_system"]}{mark("os_system")}')
229
+ print_message(f'OS Version: {info["os_version"]}{mark("os_version")}')
154
230
  if info['is_linux'] and info['linux_distro']:
155
- print_message(f'Linux Distribution: {info["linux_distro"]}')
231
+ print_message(f'Linux Distribution: {info["linux_distro"]}{mark("linux_distro")}')
156
232
 
157
- print_message(f'Architecture: {info["architecture"]}')
233
+ print_message(f'Architecture: {info["architecture"]}{mark("architecture")}')
158
234
  print_message(f'Python Version: {info["python_version"]}{mark("python_version")}')
159
235
  print_message(f'System bit: {info["system_bit"]}{mark("system_bit")}')
160
- print_message(f'Pip version: {info["pip_version"]}')
236
+ print_message(f'Pip version: {info["pip_version"]}{mark("pip_version")}')
161
237
 
162
238
  print_message(f'\nResult: {"Passed" if not errors else "Failed"}')
163
239
  if not errors:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdv-installer
3
- Version: 0.0.2.dev0
3
+ Version: 0.0.3.dev0
4
4
  Summary: Package to install SDV Enterprise packages.
5
5
  Author-email: "DataCebo, Inc." <info@datacebo.com>
6
6
  License: MIT
@@ -10,19 +10,18 @@ Classifier: Intended Audience :: Developers
10
10
  Classifier: License :: Free for non-commercial use
11
11
  Classifier: Natural Language :: English
12
12
  Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.8
14
13
  Classifier: Programming Language :: Python :: 3.9
15
14
  Classifier: Programming Language :: Python :: 3.10
16
15
  Classifier: Programming Language :: Python :: 3.11
17
16
  Classifier: Programming Language :: Python :: 3.12
18
17
  Classifier: Programming Language :: Python :: 3.13
19
18
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
- Requires-Python: <3.14,>=3.8
19
+ Requires-Python: <3.14,>=3.9
21
20
  Description-Content-Type: text/markdown
22
21
  License-File: LICENSE
23
22
  Requires-Dist: requests
24
23
  Requires-Dist: platformdirs
25
- Requires-Dist: pip
24
+ Requires-Dist: pip>=22.3
26
25
  Requires-Dist: packaging
27
26
  Provides-Extra: test
28
27
  Requires-Dist: pytest; extra == "test"
@@ -1,6 +1,6 @@
1
1
  requests
2
2
  platformdirs
3
- pip
3
+ pip>=22.3
4
4
  packaging
5
5
 
6
6
  [dev]