intelion-cloud 0.2.3__tar.gz → 0.4.0__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 (36) hide show
  1. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/PKG-INFO +1 -1
  2. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/intelion_cloud/__init__.py +5 -1
  3. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/intelion_cloud/models/__init__.py +4 -1
  4. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/intelion_cloud/models/components.py +3 -0
  5. intelion_cloud-0.4.0/intelion_cloud/models/flavors.py +98 -0
  6. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/intelion_cloud/models/servers.py +38 -0
  7. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/intelion_cloud/resources/cloud_servers.py +16 -0
  8. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/intelion_cloud.egg-info/PKG-INFO +1 -1
  9. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/intelion_cloud.egg-info/SOURCES.txt +1 -0
  10. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/pyproject.toml +1 -1
  11. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/tests/test_cloud_servers.py +10 -0
  12. intelion_cloud-0.4.0/tests/test_queue_and_rotation_fields.py +157 -0
  13. intelion_cloud-0.2.3/intelion_cloud/models/flavors.py +0 -44
  14. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/LICENSE +0 -0
  15. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/README.md +0 -0
  16. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/intelion_cloud/_client.py +0 -0
  17. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/intelion_cloud/_pagination.py +0 -0
  18. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/intelion_cloud/_transport.py +0 -0
  19. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/intelion_cloud/constants.py +0 -0
  20. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/intelion_cloud/exceptions.py +0 -0
  21. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/intelion_cloud/models/_base.py +0 -0
  22. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/intelion_cloud/models/users.py +0 -0
  23. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/intelion_cloud/resources/__init__.py +0 -0
  24. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/intelion_cloud/resources/_base.py +0 -0
  25. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/intelion_cloud/resources/flavors.py +0 -0
  26. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/intelion_cloud/resources/os_images.py +0 -0
  27. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/intelion_cloud/resources/users.py +0 -0
  28. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/intelion_cloud.egg-info/dependency_links.txt +0 -0
  29. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/intelion_cloud.egg-info/requires.txt +0 -0
  30. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/intelion_cloud.egg-info/top_level.txt +0 -0
  31. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/setup.cfg +0 -0
  32. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/tests/test_client.py +0 -0
  33. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/tests/test_flavors.py +0 -0
  34. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/tests/test_models.py +0 -0
  35. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/tests/test_transport.py +0 -0
  36. {intelion_cloud-0.2.3 → intelion_cloud-0.4.0}/tests/test_users.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: intelion-cloud
3
- Version: 0.2.3
3
+ Version: 0.4.0
4
4
  Summary: Python client for the Intelion Cloud API
5
5
  Author-email: Intelion Cloud <support@intelion.cloud>
6
6
  License-Expression: MIT
@@ -27,11 +27,13 @@ from .models import (
27
27
  CPU,
28
28
  GPU,
29
29
  OSImage,
30
+ PasswordRotation,
30
31
  RAM,
31
32
  SSD,
32
33
  CloudServer,
33
34
  DebtInfo,
34
35
  Flavor,
36
+ FlavorSubstitution,
35
37
  PhysicalServer,
36
38
  Promocode,
37
39
  ServerStatus,
@@ -41,7 +43,7 @@ from .models import (
41
43
  WhiteIP,
42
44
  )
43
45
 
44
- __version__ = "0.2.3"
46
+ __version__ = "0.4.0"
45
47
 
46
48
  __all__ = [
47
49
  # Clients
@@ -51,12 +53,14 @@ __all__ = [
51
53
  "CloudServer",
52
54
  "ServerStatus",
53
55
  "Flavor",
56
+ "FlavorSubstitution",
54
57
  "User",
55
58
  "GPU",
56
59
  "CPU",
57
60
  "RAM",
58
61
  "SSD",
59
62
  "OSImage",
63
+ "PasswordRotation",
60
64
  "UsageAct",
61
65
  "DebtInfo",
62
66
  "Promocode",
@@ -1,11 +1,12 @@
1
1
  """Data models for the Intelion Cloud API."""
2
2
 
3
3
  from .components import CPU, GPU, OSImage, RAM, SSD
4
- from .flavors import Flavor
4
+ from .flavors import Flavor, FlavorSubstitution
5
5
  from .servers import (
6
6
  CloudServer,
7
7
  DebtInfo,
8
8
  PhysicalServer,
9
+ PasswordRotation,
9
10
  Promocode,
10
11
  ServerStatus,
11
12
  SoftwareAddonInstance,
@@ -21,9 +22,11 @@ __all__ = [
21
22
  "SSD",
22
23
  "OSImage",
23
24
  "Flavor",
25
+ "FlavorSubstitution",
24
26
  "CloudServer",
25
27
  "DebtInfo",
26
28
  "PhysicalServer",
29
+ "PasswordRotation",
27
30
  "Promocode",
28
31
  "ServerStatus",
29
32
  "SoftwareAddonInstance",
@@ -204,6 +204,8 @@ class OSImage:
204
204
  ssh_enabled: bool = False
205
205
  rdp_enabled: bool = False
206
206
  compatible_flavor_ids: Optional[List[int]] = None
207
+ cuda_version: Optional[str] = None
208
+ """CUDA version bundled in the image (e.g. ``"13.3"``); ``None`` if not applicable."""
207
209
 
208
210
  @classmethod
209
211
  def from_dict(cls, data: Dict[str, Any]) -> OSImage:
@@ -215,4 +217,5 @@ class OSImage:
215
217
  ssh_enabled=data.get("ssh_enabled", False),
216
218
  rdp_enabled=data.get("rdp_enabled", False),
217
219
  compatible_flavor_ids=data.get("compatible_flavor_ids"),
220
+ cuda_version=_get(data, "cuda_version"),
218
221
  )
@@ -0,0 +1,98 @@
1
+ """Flavor model — server hardware configuration (FlavorConfig)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Dict, Optional
7
+
8
+ from ._base import _get, _parse_nested
9
+ from .components import CPU, GPU, RAM
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class FlavorSubstitution:
14
+ """A replacement flavor offered when the requested GPU is queue-disabled.
15
+
16
+ The API returns this instead of letting you queue for a card that is
17
+ fully leased out long-term — such a queue would never come up. Serialized
18
+ by ``servers.services.flavors._serialize_substitution`` on the server side.
19
+ """
20
+
21
+ flavor_id: int
22
+ flavor_name: str
23
+ gpu_id: int
24
+ gpu_slug: str
25
+ gpu_name: str
26
+ gpu_ram: int
27
+ gpu_count: int
28
+ cpu_id: int
29
+ cpu_count: int
30
+ ram_id: int
31
+ ram_count: int
32
+ monthly_price_rub_cents: int
33
+ hourly_price_rub_cents: int
34
+ exact_match: bool = True
35
+ """``False`` — no exact cpu/ram equivalent exists on the replacement card,
36
+ the closest one was picked. Surface this to the user rather than hiding it."""
37
+
38
+ @classmethod
39
+ def from_dict(cls, data: Dict[str, Any]) -> FlavorSubstitution:
40
+ return cls(
41
+ flavor_id=data["flavor_id"],
42
+ flavor_name=data.get("flavor_name", ""),
43
+ gpu_id=data.get("gpu_id", 0),
44
+ gpu_slug=data.get("gpu_slug", ""),
45
+ gpu_name=data.get("gpu_name", ""),
46
+ gpu_ram=data.get("gpu_ram", 0),
47
+ gpu_count=data.get("gpu_count", 0),
48
+ cpu_id=data.get("cpu_id", 0),
49
+ cpu_count=data.get("cpu_count", 0),
50
+ ram_id=data.get("ram_id", 0),
51
+ ram_count=data.get("ram_count", 0),
52
+ monthly_price_rub_cents=data.get("monthly_price_rub_cents", 0),
53
+ hourly_price_rub_cents=data.get("hourly_price_rub_cents", 0),
54
+ exact_match=data.get("exact_match", True),
55
+ )
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class Flavor:
60
+ """A flavor describing a server hardware configuration template (backed by FlavorConfig)."""
61
+
62
+ id: int
63
+ name: str
64
+ cpu_count: int
65
+ ram_count: int
66
+ gpu_count: Optional[int]
67
+ flavor_monthly_price_rub_cents: int
68
+ flavor_hourly_price_rub_cents: int
69
+ max_available: int
70
+ openstack_id: Optional[str] = None
71
+ cpu: Optional[CPU] = None
72
+ ram: Optional[RAM] = None
73
+ gpu: Optional[GPU] = None
74
+ queue_disabled: bool = False
75
+ """The GPU is leased out long-term: queueing for it is refused (HTTP 409)."""
76
+ suggested_alternative: Optional[FlavorSubstitution] = None
77
+ """Set only when ``queue_disabled`` and ``max_available == 0`` — otherwise ``None``."""
78
+
79
+ @classmethod
80
+ def from_dict(cls, data: Dict[str, Any]) -> Flavor:
81
+ return cls(
82
+ id=data["id"],
83
+ name=data.get("name", ""),
84
+ cpu_count=data.get("cpu_count", 0),
85
+ ram_count=data.get("ram_count", 0),
86
+ gpu_count=_get(data, "gpu_count"),
87
+ flavor_monthly_price_rub_cents=data.get("flavor_monthly_price_rub_cents", 0),
88
+ flavor_hourly_price_rub_cents=data.get("flavor_hourly_price_rub_cents", 0),
89
+ max_available=data.get("max_available", 0),
90
+ openstack_id=_get(data, "openstack_id"),
91
+ cpu=_parse_nested(data, "cpu", CPU),
92
+ ram=_parse_nested(data, "ram", RAM),
93
+ gpu=_parse_nested(data, "gpu", GPU),
94
+ queue_disabled=bool(data.get("queue_disabled", False)),
95
+ suggested_alternative=_parse_nested(
96
+ data, "suggested_alternative", FlavorSubstitution
97
+ ),
98
+ )
@@ -7,6 +7,30 @@ from typing import Any, Dict, List, Optional
7
7
 
8
8
  from ._base import _get, _parse_nested, _parse_nested_list
9
9
  from .components import CPU, GPU, OSImage, RAM, SSD
10
+ from .flavors import FlavorSubstitution
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class PasswordRotation:
15
+ """Root password rotation notice for a server.
16
+
17
+ Present only as an accomplished fact: a pending rotation is deliberately
18
+ not exposed by the API. ``acknowledged`` is kept server-side so the notice
19
+ is not shown again from a different client.
20
+ """
21
+
22
+ status: str
23
+ """``"rotated"`` or ``"failed"``."""
24
+ rotated_at: Optional[str] = None
25
+ acknowledged: bool = False
26
+
27
+ @classmethod
28
+ def from_dict(cls, data: Dict[str, Any]) -> PasswordRotation:
29
+ return cls(
30
+ status=data.get("status", ""),
31
+ rotated_at=_get(data, "rotated_at"),
32
+ acknowledged=data.get("acknowledged", False),
33
+ )
10
34
 
11
35
 
12
36
  @dataclass(frozen=True)
@@ -188,6 +212,14 @@ class CloudServer:
188
212
  total_network_disk_size: int = 0
189
213
  total_local_disk_size: int = 0
190
214
  max_available: int = 0
215
+ queue_disabled: bool = False
216
+ """This server's GPU is leased out long-term — queueing for it is refused."""
217
+ suggested_alternative: Optional[FlavorSubstitution] = None
218
+ """Replacement card offered when ``queue_disabled`` and capacity is zero."""
219
+ password_rotation: Optional[PasswordRotation] = None
220
+ """Set when the root password was rotated (or the rotation failed)."""
221
+ maintenance_reason: Optional[str] = None
222
+ """Customer-facing maintenance reason; ``None`` means the default wording."""
191
223
 
192
224
  @classmethod
193
225
  def from_dict(cls, data: Dict[str, Any]) -> CloudServer:
@@ -231,6 +263,12 @@ class CloudServer:
231
263
  total_network_disk_size=data.get("total_network_disk_size", 0),
232
264
  total_local_disk_size=data.get("total_local_disk_size", 0),
233
265
  max_available=data.get("max_available", 0),
266
+ queue_disabled=bool(data.get("queue_disabled", False)),
267
+ suggested_alternative=_parse_nested(
268
+ data, "suggested_alternative", FlavorSubstitution
269
+ ),
270
+ password_rotation=_parse_nested(data, "password_rotation", PasswordRotation),
271
+ maintenance_reason=_get(data, "maintenance_reason"),
234
272
  )
235
273
 
236
274
 
@@ -137,6 +137,17 @@ class CloudServers(SyncResource):
137
137
  data = self._post(f"{_PATH}{server_id}/actions/", json={"status": -3})
138
138
  return CloudServer.from_dict(data)
139
139
 
140
+ def reinstall_os(self, server_id: int) -> CloudServer:
141
+ """Reinstall the OS on a server, wiping the boot disk.
142
+
143
+ All data on the boot disk is destroyed. The current monthly billing
144
+ cycle is preserved (the open UsageAct is reused). A new IP and root
145
+ password are generated and become available in the user panel a few
146
+ minutes after the call returns.
147
+ """
148
+ data = self._post(f"{_PATH}{server_id}/reinstall-os/")
149
+ return CloudServer.from_dict(data)
150
+
140
151
  # --- Info endpoints ---
141
152
 
142
153
  def get_status(self, server_id: int) -> ServerStatus:
@@ -264,6 +275,11 @@ class AsyncCloudServers(AsyncResource):
264
275
  data = await self._post(f"{_PATH}{server_id}/actions/", json={"status": -3})
265
276
  return CloudServer.from_dict(data)
266
277
 
278
+ async def reinstall_os(self, server_id: int) -> CloudServer:
279
+ """Reinstall the OS on a server, wiping the boot disk."""
280
+ data = await self._post(f"{_PATH}{server_id}/reinstall-os/")
281
+ return CloudServer.from_dict(data)
282
+
267
283
  async def get_status(self, server_id: int) -> ServerStatus:
268
284
  data = await self._get(f"{_PATH}{server_id}/status/")
269
285
  return ServerStatus.from_dict(data)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: intelion-cloud
3
- Version: 0.2.3
3
+ Version: 0.4.0
4
4
  Summary: Python client for the Intelion Cloud API
5
5
  Author-email: Intelion Cloud <support@intelion.cloud>
6
6
  License-Expression: MIT
@@ -28,5 +28,6 @@ tests/test_client.py
28
28
  tests/test_cloud_servers.py
29
29
  tests/test_flavors.py
30
30
  tests/test_models.py
31
+ tests/test_queue_and_rotation_fields.py
31
32
  tests/test_transport.py
32
33
  tests/test_users.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "intelion-cloud"
7
- version = "0.2.3"
7
+ version = "0.4.0"
8
8
  description = "Python client for the Intelion Cloud API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -190,6 +190,16 @@ class TestCloudServersActions:
190
190
  assert server.status == -3
191
191
  client.close()
192
192
 
193
+ @respx.mock(base_url=API_URL)
194
+ def test_reinstall_os(self, respx_mock):
195
+ respx_mock.post("cloud-servers/42/reinstall-os/").respond(
196
+ 200, json=SAMPLE_CLOUD_SERVER
197
+ )
198
+ client = IntelionCloud(token="tok", base_url=BASE_URL)
199
+ server = client.cloud_servers.reinstall_os(42)
200
+ assert server.id == SAMPLE_CLOUD_SERVER["id"]
201
+ client.close()
202
+
193
203
  @respx.mock(base_url=API_URL)
194
204
  def test_start_with_auto_renewal(self, respx_mock):
195
205
  respx_mock.post("cloud-servers/42/actions/").respond(
@@ -0,0 +1,157 @@
1
+ """Fields the API v2 serializers return but the SDK used to drop silently.
2
+
3
+ Server-side sources:
4
+ * ``queue_disabled`` / ``suggested_alternative`` — ``FlavorConfigSerializer``
5
+ and ``UserConfigurationSerializer`` (PR #526);
6
+ * ``password_rotation`` — ``UserConfigurationSerializer`` (incident 2026-07-29);
7
+ * ``maintenance_reason`` — ``UserConfiguration`` model field;
8
+ * ``cuda_version`` — ``OperationalSystemImageSerializer``.
9
+
10
+ Note the shapes are asserted against the real serializers, not against the
11
+ published OpenAPI schema: drf-spectacular types every ``SerializerMethodField``
12
+ as ``string``, so the schema claims ``queue_disabled`` is a string and
13
+ ``suggested_alternative``/``password_rotation`` are strings too, while they are
14
+ in fact a bool and two nested objects.
15
+ """
16
+
17
+ from intelion_cloud.models import CloudServer, Flavor, FlavorSubstitution, OSImage
18
+
19
+ SUBSTITUTION = {
20
+ "flavor_id": 42,
21
+ "flavor_name": "RTX 4090 x1",
22
+ "gpu_id": 7,
23
+ "gpu_slug": "nvidia-rtx-4090",
24
+ "gpu_name": "NVIDIA RTX 4090",
25
+ "gpu_ram": 24,
26
+ "gpu_count": 1,
27
+ "cpu_id": 3,
28
+ "cpu_count": 8,
29
+ "ram_id": 5,
30
+ "ram_count": 32,
31
+ "monthly_price_rub_cents": 4500000,
32
+ "hourly_price_rub_cents": 6800,
33
+ "exact_match": False,
34
+ }
35
+
36
+
37
+ class TestFlavorSubstitution:
38
+ def test_from_dict(self):
39
+ sub = FlavorSubstitution.from_dict(SUBSTITUTION)
40
+ assert sub.flavor_id == 42
41
+ assert sub.gpu_slug == "nvidia-rtx-4090"
42
+ assert sub.monthly_price_rub_cents == 4500000
43
+ assert sub.exact_match is False
44
+
45
+ def test_exact_match_defaults_to_true(self):
46
+ payload = {k: v for k, v in SUBSTITUTION.items() if k != "exact_match"}
47
+ assert FlavorSubstitution.from_dict(payload).exact_match is True
48
+
49
+
50
+ class TestFlavorQueueFields:
51
+ def test_queue_disabled_with_alternative(self):
52
+ flavor = Flavor.from_dict({
53
+ "id": 1,
54
+ "name": "H100 x8",
55
+ "cpu_count": 64,
56
+ "ram_count": 512,
57
+ "gpu_count": 8,
58
+ "flavor_monthly_price_rub_cents": 100,
59
+ "flavor_hourly_price_rub_cents": 1,
60
+ "max_available": 0,
61
+ "queue_disabled": True,
62
+ "suggested_alternative": SUBSTITUTION,
63
+ })
64
+ assert flavor.queue_disabled is True
65
+ assert flavor.suggested_alternative is not None
66
+ assert flavor.suggested_alternative.flavor_name == "RTX 4090 x1"
67
+
68
+ def test_absent_fields_do_not_break_old_payloads(self):
69
+ """Старый сервер (или урезанный ответ) не должен ронять парсинг."""
70
+ flavor = Flavor.from_dict({
71
+ "id": 1,
72
+ "name": "A100 x1",
73
+ "cpu_count": 8,
74
+ "ram_count": 64,
75
+ "gpu_count": 1,
76
+ "flavor_monthly_price_rub_cents": 100,
77
+ "flavor_hourly_price_rub_cents": 1,
78
+ "max_available": 3,
79
+ })
80
+ assert flavor.queue_disabled is False
81
+ assert flavor.suggested_alternative is None
82
+
83
+ def test_null_alternative_stays_none(self):
84
+ flavor = Flavor.from_dict({
85
+ "id": 1,
86
+ "name": "A100 x1",
87
+ "cpu_count": 8,
88
+ "ram_count": 64,
89
+ "gpu_count": 1,
90
+ "flavor_monthly_price_rub_cents": 100,
91
+ "flavor_hourly_price_rub_cents": 1,
92
+ "max_available": 3,
93
+ "queue_disabled": True,
94
+ "suggested_alternative": None,
95
+ })
96
+ assert flavor.queue_disabled is True
97
+ assert flavor.suggested_alternative is None
98
+
99
+
100
+ def _server(**extra):
101
+ base = {
102
+ "id": 8564,
103
+ "name": "srv",
104
+ "status": 1,
105
+ "price_plan": 1,
106
+ "is_auto_renewal": True,
107
+ "monthly_price_rub_cents": 100,
108
+ "hourly_price_rub_cents": 1,
109
+ "server_full_rent_price_rub_cents": 100,
110
+ }
111
+ base.update(extra)
112
+ return CloudServer.from_dict(base)
113
+
114
+
115
+ class TestCloudServerNewFields:
116
+ def test_password_rotation_rotated(self):
117
+ srv = _server(password_rotation={
118
+ "status": "rotated",
119
+ "rotated_at": "2026-07-29T12:00:00+00:00",
120
+ "acknowledged": False,
121
+ })
122
+ assert srv.password_rotation is not None
123
+ assert srv.password_rotation.status == "rotated"
124
+ assert srv.password_rotation.rotated_at == "2026-07-29T12:00:00+00:00"
125
+ assert srv.password_rotation.acknowledged is False
126
+
127
+ def test_password_rotation_failed_without_timestamp(self):
128
+ srv = _server(password_rotation={"status": "failed", "rotated_at": None})
129
+ assert srv.password_rotation.status == "failed"
130
+ assert srv.password_rotation.rotated_at is None
131
+
132
+ def test_password_rotation_null_is_none(self):
133
+ assert _server(password_rotation=None).password_rotation is None
134
+
135
+ def test_maintenance_reason(self):
136
+ assert _server(maintenance_reason="Замена БП").maintenance_reason == "Замена БП"
137
+ assert _server(maintenance_reason=None).maintenance_reason is None
138
+ assert _server().maintenance_reason is None
139
+
140
+ def test_queue_fields(self):
141
+ srv = _server(queue_disabled=True, suggested_alternative=SUBSTITUTION)
142
+ assert srv.queue_disabled is True
143
+ assert srv.suggested_alternative.gpu_count == 1
144
+ assert _server().queue_disabled is False
145
+ assert _server().suggested_alternative is None
146
+
147
+
148
+ class TestOSImageCudaVersion:
149
+ def test_cuda_version(self):
150
+ img = OSImage.from_dict({"id": 1, "name": "Ubuntu 24.04", "cuda_version": "13.3"})
151
+ assert img.cuda_version == "13.3"
152
+
153
+ def test_cuda_version_absent_or_null(self):
154
+ assert OSImage.from_dict({"id": 1, "name": "Windows"}).cuda_version is None
155
+ assert OSImage.from_dict(
156
+ {"id": 1, "name": "Windows", "cuda_version": None}
157
+ ).cuda_version is None
@@ -1,44 +0,0 @@
1
- """Flavor model — server hardware configuration (FlavorConfig)."""
2
-
3
- from __future__ import annotations
4
-
5
- from dataclasses import dataclass
6
- from typing import Any, Dict, Optional
7
-
8
- from ._base import _get, _parse_nested
9
- from .components import CPU, GPU, RAM
10
-
11
-
12
- @dataclass(frozen=True)
13
- class Flavor:
14
- """A flavor describing a server hardware configuration template (backed by FlavorConfig)."""
15
-
16
- id: int
17
- name: str
18
- cpu_count: int
19
- ram_count: int
20
- gpu_count: Optional[int]
21
- flavor_monthly_price_rub_cents: int
22
- flavor_hourly_price_rub_cents: int
23
- max_available: int
24
- openstack_id: Optional[str] = None
25
- cpu: Optional[CPU] = None
26
- ram: Optional[RAM] = None
27
- gpu: Optional[GPU] = None
28
-
29
- @classmethod
30
- def from_dict(cls, data: Dict[str, Any]) -> Flavor:
31
- return cls(
32
- id=data["id"],
33
- name=data.get("name", ""),
34
- cpu_count=data.get("cpu_count", 0),
35
- ram_count=data.get("ram_count", 0),
36
- gpu_count=_get(data, "gpu_count"),
37
- flavor_monthly_price_rub_cents=data.get("flavor_monthly_price_rub_cents", 0),
38
- flavor_hourly_price_rub_cents=data.get("flavor_hourly_price_rub_cents", 0),
39
- max_available=data.get("max_available", 0),
40
- openstack_id=_get(data, "openstack_id"),
41
- cpu=_parse_nested(data, "cpu", CPU),
42
- ram=_parse_nested(data, "ram", RAM),
43
- gpu=_parse_nested(data, "gpu", GPU),
44
- )
File without changes
File without changes
File without changes