intelion-cloud 0.2.0__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.
- intelion_cloud/__init__.py +82 -0
- intelion_cloud/_client.py +108 -0
- intelion_cloud/_pagination.py +66 -0
- intelion_cloud/_transport.py +342 -0
- intelion_cloud/constants.py +74 -0
- intelion_cloud/exceptions.py +92 -0
- intelion_cloud/models/__init__.py +33 -0
- intelion_cloud/models/_base.py +39 -0
- intelion_cloud/models/components.py +218 -0
- intelion_cloud/models/flavors.py +44 -0
- intelion_cloud/models/servers.py +247 -0
- intelion_cloud/models/users.py +73 -0
- intelion_cloud/resources/__init__.py +17 -0
- intelion_cloud/resources/_base.py +152 -0
- intelion_cloud/resources/cloud_servers.py +293 -0
- intelion_cloud/resources/flavors.py +42 -0
- intelion_cloud/resources/os_images.py +66 -0
- intelion_cloud/resources/users.py +100 -0
- intelion_cloud-0.2.0.dist-info/METADATA +134 -0
- intelion_cloud-0.2.0.dist-info/RECORD +23 -0
- intelion_cloud-0.2.0.dist-info/WHEEL +5 -0
- intelion_cloud-0.2.0.dist-info/licenses/LICENSE +21 -0
- intelion_cloud-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""User model."""
|
|
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
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class User:
|
|
13
|
+
"""Intelion Cloud user account."""
|
|
14
|
+
|
|
15
|
+
id: int
|
|
16
|
+
username: str
|
|
17
|
+
email: str
|
|
18
|
+
first_name: str
|
|
19
|
+
last_name: str
|
|
20
|
+
current_balance_rub_cents: int
|
|
21
|
+
held_up_money_rub_cents: int
|
|
22
|
+
is_phone_verified: bool
|
|
23
|
+
is_staff: bool
|
|
24
|
+
legal: Optional[str] = None
|
|
25
|
+
company_name: str = ""
|
|
26
|
+
card_number: Optional[str] = None
|
|
27
|
+
invoice_requested_rub_cents: int = 0
|
|
28
|
+
balance_top_up_recommend_rub_cents: int = 0
|
|
29
|
+
available_top_up_rub_cents: int = 0
|
|
30
|
+
identity_token: Optional[str] = None
|
|
31
|
+
default_payment_type: Optional[str] = None
|
|
32
|
+
tg_id: bool = False
|
|
33
|
+
should_show_referral_banner: bool = False
|
|
34
|
+
white_label_partner: Optional[str] = None
|
|
35
|
+
phone_number: Optional[str] = None
|
|
36
|
+
is_email_verified: bool = False
|
|
37
|
+
account_manager: Optional[str] = None
|
|
38
|
+
is_test: bool = False
|
|
39
|
+
is_edo_connected: bool = False
|
|
40
|
+
invalid_name: bool = False
|
|
41
|
+
last_viewed_event: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_dict(cls, data: Dict[str, Any]) -> User:
|
|
45
|
+
return cls(
|
|
46
|
+
id=data["id"],
|
|
47
|
+
username=data.get("username", ""),
|
|
48
|
+
email=data.get("email", ""),
|
|
49
|
+
first_name=data.get("first_name", ""),
|
|
50
|
+
last_name=data.get("last_name", ""),
|
|
51
|
+
current_balance_rub_cents=data.get("current_balance_rub_cents", 0),
|
|
52
|
+
held_up_money_rub_cents=data.get("held_up_money_rub_cents", 0),
|
|
53
|
+
is_phone_verified=data.get("is_phone_verified", False),
|
|
54
|
+
is_staff=data.get("is_staff", False),
|
|
55
|
+
legal=_get(data, "legal"),
|
|
56
|
+
company_name=data.get("company_name", ""),
|
|
57
|
+
card_number=_get(data, "card_number"),
|
|
58
|
+
invoice_requested_rub_cents=data.get("invoice_requested_rub_cents", 0),
|
|
59
|
+
balance_top_up_recommend_rub_cents=data.get("balance_top_up_recommend_rub_cents", 0),
|
|
60
|
+
available_top_up_rub_cents=data.get("available_top_up_rub_cents", 0),
|
|
61
|
+
identity_token=_get(data, "identity_token"),
|
|
62
|
+
default_payment_type=_get(data, "default_payment_type"),
|
|
63
|
+
tg_id=data.get("tg_id", False),
|
|
64
|
+
should_show_referral_banner=data.get("should_show_referral_banner", False),
|
|
65
|
+
white_label_partner=_get(data, "white_label_partner"),
|
|
66
|
+
phone_number=_get(data, "phone_number"),
|
|
67
|
+
is_email_verified=data.get("is_email_verified", False),
|
|
68
|
+
account_manager=_get(data, "account_manager"),
|
|
69
|
+
is_test=data.get("is_test", False),
|
|
70
|
+
is_edo_connected=data.get("is_edo_connected", False),
|
|
71
|
+
invalid_name=data.get("invalid_name", False),
|
|
72
|
+
last_viewed_event=_get(data, "last_viewed_event"),
|
|
73
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""API resource modules."""
|
|
2
|
+
|
|
3
|
+
from .cloud_servers import AsyncCloudServers, CloudServers
|
|
4
|
+
from .flavors import AsyncFlavors, Flavors
|
|
5
|
+
from .os_images import AsyncOSImages, OSImages
|
|
6
|
+
from .users import AsyncUsers, Users
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"CloudServers",
|
|
10
|
+
"AsyncCloudServers",
|
|
11
|
+
"Flavors",
|
|
12
|
+
"AsyncFlavors",
|
|
13
|
+
"OSImages",
|
|
14
|
+
"AsyncOSImages",
|
|
15
|
+
"Users",
|
|
16
|
+
"AsyncUsers",
|
|
17
|
+
]
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Base resource classes providing HTTP methods to resource subclasses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, Dict, List, Optional, TypeVar
|
|
6
|
+
|
|
7
|
+
from .._pagination import PaginatedResponse, extract_next_page, parse_paginated
|
|
8
|
+
from .._transport import AsyncTransport, SyncTransport
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SyncResource:
|
|
14
|
+
"""Base class for synchronous API resources."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, transport: SyncTransport) -> None:
|
|
17
|
+
self._transport = transport
|
|
18
|
+
|
|
19
|
+
def _get(self, path: str, *, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
20
|
+
response = self._transport.request("GET", path, params=params)
|
|
21
|
+
return response.json()
|
|
22
|
+
|
|
23
|
+
def _post(
|
|
24
|
+
self,
|
|
25
|
+
path: str,
|
|
26
|
+
*,
|
|
27
|
+
json: Optional[Dict[str, Any]] = None,
|
|
28
|
+
params: Optional[Dict[str, Any]] = None,
|
|
29
|
+
) -> Any:
|
|
30
|
+
response = self._transport.request("POST", path, json=json, params=params)
|
|
31
|
+
if response.status_code == 204:
|
|
32
|
+
return None
|
|
33
|
+
return response.json()
|
|
34
|
+
|
|
35
|
+
def _patch(self, path: str, *, json: Dict[str, Any]) -> Any:
|
|
36
|
+
response = self._transport.request("PATCH", path, json=json)
|
|
37
|
+
return response.json()
|
|
38
|
+
|
|
39
|
+
def _list_all(
|
|
40
|
+
self,
|
|
41
|
+
path: str,
|
|
42
|
+
model_cls: Callable[[Dict[str, Any]], T],
|
|
43
|
+
*,
|
|
44
|
+
params: Optional[Dict[str, Any]] = None,
|
|
45
|
+
) -> List[T]:
|
|
46
|
+
"""Fetch all pages and return a flat list of model instances."""
|
|
47
|
+
all_items: List[T] = []
|
|
48
|
+
current_path = path
|
|
49
|
+
current_params = params
|
|
50
|
+
|
|
51
|
+
while True:
|
|
52
|
+
data = self._get(current_path, params=current_params)
|
|
53
|
+
page = parse_paginated(data)
|
|
54
|
+
all_items.extend(model_cls(item) for item in page["results"])
|
|
55
|
+
|
|
56
|
+
next_info = extract_next_page(page["next"])
|
|
57
|
+
if next_info is None:
|
|
58
|
+
break
|
|
59
|
+
current_path, current_params = next_info
|
|
60
|
+
|
|
61
|
+
return all_items
|
|
62
|
+
|
|
63
|
+
def _list_page(
|
|
64
|
+
self,
|
|
65
|
+
path: str,
|
|
66
|
+
model_cls: Callable[[Dict[str, Any]], T],
|
|
67
|
+
*,
|
|
68
|
+
params: Optional[Dict[str, Any]] = None,
|
|
69
|
+
page: int = 1,
|
|
70
|
+
) -> PaginatedResponse[T]:
|
|
71
|
+
"""Fetch a single page of results."""
|
|
72
|
+
params = dict(params) if params else {}
|
|
73
|
+
params["page"] = page
|
|
74
|
+
data = self._get(path, params=params)
|
|
75
|
+
paginated = parse_paginated(data)
|
|
76
|
+
return PaginatedResponse(
|
|
77
|
+
count=paginated["count"],
|
|
78
|
+
results=[model_cls(item) for item in paginated["results"]],
|
|
79
|
+
next_url=paginated["next"],
|
|
80
|
+
previous_url=paginated["previous"],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class AsyncResource:
|
|
85
|
+
"""Base class for asynchronous API resources."""
|
|
86
|
+
|
|
87
|
+
def __init__(self, transport: AsyncTransport) -> None:
|
|
88
|
+
self._transport = transport
|
|
89
|
+
|
|
90
|
+
async def _get(self, path: str, *, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
91
|
+
response = await self._transport.request("GET", path, params=params)
|
|
92
|
+
return response.json()
|
|
93
|
+
|
|
94
|
+
async def _post(
|
|
95
|
+
self,
|
|
96
|
+
path: str,
|
|
97
|
+
*,
|
|
98
|
+
json: Optional[Dict[str, Any]] = None,
|
|
99
|
+
params: Optional[Dict[str, Any]] = None,
|
|
100
|
+
) -> Any:
|
|
101
|
+
response = await self._transport.request("POST", path, json=json, params=params)
|
|
102
|
+
if response.status_code == 204:
|
|
103
|
+
return None
|
|
104
|
+
return response.json()
|
|
105
|
+
|
|
106
|
+
async def _patch(self, path: str, *, json: Dict[str, Any]) -> Any:
|
|
107
|
+
response = await self._transport.request("PATCH", path, json=json)
|
|
108
|
+
return response.json()
|
|
109
|
+
|
|
110
|
+
async def _list_all(
|
|
111
|
+
self,
|
|
112
|
+
path: str,
|
|
113
|
+
model_cls: Callable[[Dict[str, Any]], T],
|
|
114
|
+
*,
|
|
115
|
+
params: Optional[Dict[str, Any]] = None,
|
|
116
|
+
) -> List[T]:
|
|
117
|
+
"""Fetch all pages and return a flat list of model instances."""
|
|
118
|
+
all_items: List[T] = []
|
|
119
|
+
current_path = path
|
|
120
|
+
current_params = params
|
|
121
|
+
|
|
122
|
+
while True:
|
|
123
|
+
data = await self._get(current_path, params=current_params)
|
|
124
|
+
page = parse_paginated(data)
|
|
125
|
+
all_items.extend(model_cls(item) for item in page["results"])
|
|
126
|
+
|
|
127
|
+
next_info = extract_next_page(page["next"])
|
|
128
|
+
if next_info is None:
|
|
129
|
+
break
|
|
130
|
+
current_path, current_params = next_info
|
|
131
|
+
|
|
132
|
+
return all_items
|
|
133
|
+
|
|
134
|
+
async def _list_page(
|
|
135
|
+
self,
|
|
136
|
+
path: str,
|
|
137
|
+
model_cls: Callable[[Dict[str, Any]], T],
|
|
138
|
+
*,
|
|
139
|
+
params: Optional[Dict[str, Any]] = None,
|
|
140
|
+
page: int = 1,
|
|
141
|
+
) -> PaginatedResponse[T]:
|
|
142
|
+
"""Fetch a single page of results."""
|
|
143
|
+
params = dict(params) if params else {}
|
|
144
|
+
params["page"] = page
|
|
145
|
+
data = await self._get(path, params=params)
|
|
146
|
+
paginated = parse_paginated(data)
|
|
147
|
+
return PaginatedResponse(
|
|
148
|
+
count=paginated["count"],
|
|
149
|
+
results=[model_cls(item) for item in paginated["results"]],
|
|
150
|
+
next_url=paginated["next"],
|
|
151
|
+
previous_url=paginated["previous"],
|
|
152
|
+
)
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Cloud server resource — CRUD, lifecycle actions, clone, migrate."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional, Union
|
|
6
|
+
|
|
7
|
+
from .._pagination import PaginatedResponse
|
|
8
|
+
from ..models.servers import CloudServer, ServerStatus
|
|
9
|
+
from ._base import AsyncResource, SyncResource
|
|
10
|
+
|
|
11
|
+
_PATH = "cloud-servers/"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _build_action_payload(
|
|
15
|
+
status: Union[int, str],
|
|
16
|
+
*,
|
|
17
|
+
is_auto_renewal: Optional[bool] = None,
|
|
18
|
+
run_on_affordable_time: bool = False,
|
|
19
|
+
) -> Dict[str, Any]:
|
|
20
|
+
payload: Dict[str, Any] = {"status": status}
|
|
21
|
+
if is_auto_renewal is not None:
|
|
22
|
+
payload["isAutoRenewal"] = is_auto_renewal
|
|
23
|
+
if run_on_affordable_time:
|
|
24
|
+
payload["run_on_affordable_time"] = True
|
|
25
|
+
return payload
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CloudServers(SyncResource):
|
|
29
|
+
"""Synchronous cloud server operations."""
|
|
30
|
+
|
|
31
|
+
def list(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
page: Optional[int] = None,
|
|
35
|
+
) -> Union[List[CloudServer], PaginatedResponse[CloudServer]]:
|
|
36
|
+
"""List cloud servers.
|
|
37
|
+
|
|
38
|
+
Without ``page``, returns all servers (auto-paginated).
|
|
39
|
+
With ``page=N``, returns a single :class:`PaginatedResponse`.
|
|
40
|
+
"""
|
|
41
|
+
if page is not None:
|
|
42
|
+
return self._list_page(_PATH, CloudServer.from_dict, page=page)
|
|
43
|
+
return self._list_all(_PATH, CloudServer.from_dict)
|
|
44
|
+
|
|
45
|
+
def get(self, server_id: int) -> CloudServer:
|
|
46
|
+
"""Get a single cloud server by ID."""
|
|
47
|
+
data = self._get(f"{_PATH}{server_id}/")
|
|
48
|
+
return CloudServer.from_dict(data)
|
|
49
|
+
|
|
50
|
+
def create(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
name: str,
|
|
54
|
+
flavor_id: int,
|
|
55
|
+
ssd_count: int,
|
|
56
|
+
os_id: int,
|
|
57
|
+
price_plan: int = 0,
|
|
58
|
+
promocode_id: Optional[int] = None,
|
|
59
|
+
is_in_queue: bool = False,
|
|
60
|
+
addon_ids: Optional[List[int]] = None,
|
|
61
|
+
) -> CloudServer:
|
|
62
|
+
"""Create a new cloud server configuration.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
name: Server display name.
|
|
66
|
+
flavor_id: FlavorConfig primary key. CPU/RAM/GPU are fixed by the flavor.
|
|
67
|
+
ssd_count: Network disk size in GB (min 30). Maps to ``network_disk_count`` server-side.
|
|
68
|
+
os_id: Operating system image ID (must be compatible with the flavor).
|
|
69
|
+
price_plan: Billing plan (use :class:`~intelion_cloud.constants.PricePlan`).
|
|
70
|
+
promocode_id: Promotional code ID to apply.
|
|
71
|
+
is_in_queue: If True, queue the order when hardware is unavailable.
|
|
72
|
+
addon_ids: Software addon IDs to install at first boot.
|
|
73
|
+
"""
|
|
74
|
+
payload: Dict[str, Any] = {
|
|
75
|
+
"name": name,
|
|
76
|
+
"flavor_id": flavor_id,
|
|
77
|
+
"ssd_count": ssd_count,
|
|
78
|
+
"os_id": os_id,
|
|
79
|
+
"price_plan": price_plan,
|
|
80
|
+
}
|
|
81
|
+
if promocode_id is not None:
|
|
82
|
+
payload["promocode_id"] = promocode_id
|
|
83
|
+
if is_in_queue:
|
|
84
|
+
payload["is_in_queue"] = True
|
|
85
|
+
if addon_ids:
|
|
86
|
+
payload["addon_ids"] = list(addon_ids)
|
|
87
|
+
|
|
88
|
+
data = self._post(_PATH, json=payload)
|
|
89
|
+
return CloudServer.from_dict(data)
|
|
90
|
+
|
|
91
|
+
def update(
|
|
92
|
+
self,
|
|
93
|
+
server_id: int,
|
|
94
|
+
*,
|
|
95
|
+
name: Optional[str] = None,
|
|
96
|
+
is_auto_renewal: Optional[bool] = None,
|
|
97
|
+
) -> CloudServer:
|
|
98
|
+
"""Update server properties (name, auto-renewal)."""
|
|
99
|
+
payload: Dict[str, Any] = {}
|
|
100
|
+
if name is not None:
|
|
101
|
+
payload["name"] = name
|
|
102
|
+
if is_auto_renewal is not None:
|
|
103
|
+
payload["is_auto_renewal"] = is_auto_renewal
|
|
104
|
+
data = self._patch(f"{_PATH}{server_id}/", json=payload)
|
|
105
|
+
return CloudServer.from_dict(data)
|
|
106
|
+
|
|
107
|
+
# --- Lifecycle actions ---
|
|
108
|
+
|
|
109
|
+
def start(
|
|
110
|
+
self,
|
|
111
|
+
server_id: int,
|
|
112
|
+
*,
|
|
113
|
+
is_auto_renewal: Optional[bool] = None,
|
|
114
|
+
run_on_affordable_time: bool = False,
|
|
115
|
+
) -> CloudServer:
|
|
116
|
+
"""Start a stopped server."""
|
|
117
|
+
payload = _build_action_payload(
|
|
118
|
+
2, # ServerStatus.ACTIVE
|
|
119
|
+
is_auto_renewal=is_auto_renewal,
|
|
120
|
+
run_on_affordable_time=run_on_affordable_time,
|
|
121
|
+
)
|
|
122
|
+
data = self._post(f"{_PATH}{server_id}/actions/", json=payload)
|
|
123
|
+
return CloudServer.from_dict(data)
|
|
124
|
+
|
|
125
|
+
def stop(self, server_id: int) -> CloudServer:
|
|
126
|
+
"""Stop (shelve) a running server."""
|
|
127
|
+
data = self._post(f"{_PATH}{server_id}/actions/", json={"status": -1})
|
|
128
|
+
return CloudServer.from_dict(data)
|
|
129
|
+
|
|
130
|
+
def reboot(self, server_id: int) -> CloudServer:
|
|
131
|
+
"""Reboot a running server."""
|
|
132
|
+
data = self._post(f"{_PATH}{server_id}/actions/", json={"status": "REBOOT"})
|
|
133
|
+
return CloudServer.from_dict(data)
|
|
134
|
+
|
|
135
|
+
def delete(self, server_id: int) -> CloudServer:
|
|
136
|
+
"""Delete a server (irreversible)."""
|
|
137
|
+
data = self._post(f"{_PATH}{server_id}/actions/", json={"status": -3})
|
|
138
|
+
return CloudServer.from_dict(data)
|
|
139
|
+
|
|
140
|
+
# --- Info endpoints ---
|
|
141
|
+
|
|
142
|
+
def get_status(self, server_id: int) -> ServerStatus:
|
|
143
|
+
"""Check if a server can be started and get affordable runtime."""
|
|
144
|
+
data = self._get(f"{_PATH}{server_id}/status/")
|
|
145
|
+
return ServerStatus.from_dict(data)
|
|
146
|
+
|
|
147
|
+
def get_password(self, server_id: int) -> str:
|
|
148
|
+
"""Get the server password (available after first boot)."""
|
|
149
|
+
data = self._get(f"{_PATH}{server_id}/password/")
|
|
150
|
+
if isinstance(data, dict):
|
|
151
|
+
return data.get("password", "")
|
|
152
|
+
return str(data)
|
|
153
|
+
|
|
154
|
+
# --- Advanced operations ---
|
|
155
|
+
|
|
156
|
+
def clone(self, server_id: int) -> CloudServer:
|
|
157
|
+
"""Clone a server (creates an identical copy)."""
|
|
158
|
+
data = self._post(f"{_PATH}{server_id}/clone/")
|
|
159
|
+
return CloudServer.from_dict(data)
|
|
160
|
+
|
|
161
|
+
def migrate(
|
|
162
|
+
self,
|
|
163
|
+
server_id: int,
|
|
164
|
+
*,
|
|
165
|
+
flavor_id: int,
|
|
166
|
+
price_plan: int,
|
|
167
|
+
) -> CloudServer:
|
|
168
|
+
"""Migrate a server to a different FlavorConfig and/or price plan.
|
|
169
|
+
|
|
170
|
+
The server must be stopped before migration. CPU/RAM/GPU are fixed by the flavor.
|
|
171
|
+
"""
|
|
172
|
+
payload: Dict[str, Any] = {
|
|
173
|
+
"flavor_id": flavor_id,
|
|
174
|
+
"price_plan": price_plan,
|
|
175
|
+
}
|
|
176
|
+
data = self._post(f"{_PATH}{server_id}/migrate/", json=payload)
|
|
177
|
+
return CloudServer.from_dict(data)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class AsyncCloudServers(AsyncResource):
|
|
181
|
+
"""Asynchronous cloud server operations."""
|
|
182
|
+
|
|
183
|
+
async def list(
|
|
184
|
+
self,
|
|
185
|
+
*,
|
|
186
|
+
page: Optional[int] = None,
|
|
187
|
+
) -> Union[List[CloudServer], PaginatedResponse[CloudServer]]:
|
|
188
|
+
if page is not None:
|
|
189
|
+
return await self._list_page(_PATH, CloudServer.from_dict, page=page)
|
|
190
|
+
return await self._list_all(_PATH, CloudServer.from_dict)
|
|
191
|
+
|
|
192
|
+
async def get(self, server_id: int) -> CloudServer:
|
|
193
|
+
data = await self._get(f"{_PATH}{server_id}/")
|
|
194
|
+
return CloudServer.from_dict(data)
|
|
195
|
+
|
|
196
|
+
async def create(
|
|
197
|
+
self,
|
|
198
|
+
*,
|
|
199
|
+
name: str,
|
|
200
|
+
flavor_id: int,
|
|
201
|
+
ssd_count: int,
|
|
202
|
+
os_id: int,
|
|
203
|
+
price_plan: int = 0,
|
|
204
|
+
promocode_id: Optional[int] = None,
|
|
205
|
+
is_in_queue: bool = False,
|
|
206
|
+
addon_ids: Optional[List[int]] = None,
|
|
207
|
+
) -> CloudServer:
|
|
208
|
+
payload: Dict[str, Any] = {
|
|
209
|
+
"name": name,
|
|
210
|
+
"flavor_id": flavor_id,
|
|
211
|
+
"ssd_count": ssd_count,
|
|
212
|
+
"os_id": os_id,
|
|
213
|
+
"price_plan": price_plan,
|
|
214
|
+
}
|
|
215
|
+
if promocode_id is not None:
|
|
216
|
+
payload["promocode_id"] = promocode_id
|
|
217
|
+
if is_in_queue:
|
|
218
|
+
payload["is_in_queue"] = True
|
|
219
|
+
if addon_ids:
|
|
220
|
+
payload["addon_ids"] = list(addon_ids)
|
|
221
|
+
|
|
222
|
+
data = await self._post(_PATH, json=payload)
|
|
223
|
+
return CloudServer.from_dict(data)
|
|
224
|
+
|
|
225
|
+
async def update(
|
|
226
|
+
self,
|
|
227
|
+
server_id: int,
|
|
228
|
+
*,
|
|
229
|
+
name: Optional[str] = None,
|
|
230
|
+
is_auto_renewal: Optional[bool] = None,
|
|
231
|
+
) -> CloudServer:
|
|
232
|
+
payload: Dict[str, Any] = {}
|
|
233
|
+
if name is not None:
|
|
234
|
+
payload["name"] = name
|
|
235
|
+
if is_auto_renewal is not None:
|
|
236
|
+
payload["is_auto_renewal"] = is_auto_renewal
|
|
237
|
+
data = await self._patch(f"{_PATH}{server_id}/", json=payload)
|
|
238
|
+
return CloudServer.from_dict(data)
|
|
239
|
+
|
|
240
|
+
async def start(
|
|
241
|
+
self,
|
|
242
|
+
server_id: int,
|
|
243
|
+
*,
|
|
244
|
+
is_auto_renewal: Optional[bool] = None,
|
|
245
|
+
run_on_affordable_time: bool = False,
|
|
246
|
+
) -> CloudServer:
|
|
247
|
+
payload = _build_action_payload(
|
|
248
|
+
2,
|
|
249
|
+
is_auto_renewal=is_auto_renewal,
|
|
250
|
+
run_on_affordable_time=run_on_affordable_time,
|
|
251
|
+
)
|
|
252
|
+
data = await self._post(f"{_PATH}{server_id}/actions/", json=payload)
|
|
253
|
+
return CloudServer.from_dict(data)
|
|
254
|
+
|
|
255
|
+
async def stop(self, server_id: int) -> CloudServer:
|
|
256
|
+
data = await self._post(f"{_PATH}{server_id}/actions/", json={"status": -1})
|
|
257
|
+
return CloudServer.from_dict(data)
|
|
258
|
+
|
|
259
|
+
async def reboot(self, server_id: int) -> CloudServer:
|
|
260
|
+
data = await self._post(f"{_PATH}{server_id}/actions/", json={"status": "REBOOT"})
|
|
261
|
+
return CloudServer.from_dict(data)
|
|
262
|
+
|
|
263
|
+
async def delete(self, server_id: int) -> CloudServer:
|
|
264
|
+
data = await self._post(f"{_PATH}{server_id}/actions/", json={"status": -3})
|
|
265
|
+
return CloudServer.from_dict(data)
|
|
266
|
+
|
|
267
|
+
async def get_status(self, server_id: int) -> ServerStatus:
|
|
268
|
+
data = await self._get(f"{_PATH}{server_id}/status/")
|
|
269
|
+
return ServerStatus.from_dict(data)
|
|
270
|
+
|
|
271
|
+
async def get_password(self, server_id: int) -> str:
|
|
272
|
+
data = await self._get(f"{_PATH}{server_id}/password/")
|
|
273
|
+
if isinstance(data, dict):
|
|
274
|
+
return data.get("password", "")
|
|
275
|
+
return str(data)
|
|
276
|
+
|
|
277
|
+
async def clone(self, server_id: int) -> CloudServer:
|
|
278
|
+
data = await self._post(f"{_PATH}{server_id}/clone/")
|
|
279
|
+
return CloudServer.from_dict(data)
|
|
280
|
+
|
|
281
|
+
async def migrate(
|
|
282
|
+
self,
|
|
283
|
+
server_id: int,
|
|
284
|
+
*,
|
|
285
|
+
flavor_id: int,
|
|
286
|
+
price_plan: int,
|
|
287
|
+
) -> CloudServer:
|
|
288
|
+
payload: Dict[str, Any] = {
|
|
289
|
+
"flavor_id": flavor_id,
|
|
290
|
+
"price_plan": price_plan,
|
|
291
|
+
}
|
|
292
|
+
data = await self._post(f"{_PATH}{server_id}/migrate/", json=payload)
|
|
293
|
+
return CloudServer.from_dict(data)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Flavor resource — list available server configurations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import List, Optional, Union
|
|
6
|
+
|
|
7
|
+
from .._pagination import PaginatedResponse
|
|
8
|
+
from ..models.flavors import Flavor
|
|
9
|
+
from ._base import AsyncResource, SyncResource
|
|
10
|
+
|
|
11
|
+
_PATH = "flavors/"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Flavors(SyncResource):
|
|
15
|
+
"""Synchronous flavor operations."""
|
|
16
|
+
|
|
17
|
+
def list(
|
|
18
|
+
self,
|
|
19
|
+
*,
|
|
20
|
+
page: Optional[int] = None,
|
|
21
|
+
) -> Union[List[Flavor], PaginatedResponse[Flavor]]:
|
|
22
|
+
"""List available flavors.
|
|
23
|
+
|
|
24
|
+
Without ``page``, returns all flavors (auto-paginated).
|
|
25
|
+
With ``page=N``, returns a single :class:`PaginatedResponse`.
|
|
26
|
+
"""
|
|
27
|
+
if page is not None:
|
|
28
|
+
return self._list_page(_PATH, Flavor.from_dict, page=page)
|
|
29
|
+
return self._list_all(_PATH, Flavor.from_dict)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AsyncFlavors(AsyncResource):
|
|
33
|
+
"""Asynchronous flavor operations."""
|
|
34
|
+
|
|
35
|
+
async def list(
|
|
36
|
+
self,
|
|
37
|
+
*,
|
|
38
|
+
page: Optional[int] = None,
|
|
39
|
+
) -> Union[List[Flavor], PaginatedResponse[Flavor]]:
|
|
40
|
+
if page is not None:
|
|
41
|
+
return await self._list_page(_PATH, Flavor.from_dict, page=page)
|
|
42
|
+
return await self._list_all(_PATH, Flavor.from_dict)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""OS image resource — list available operating system images."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional, Union
|
|
6
|
+
|
|
7
|
+
from .._pagination import PaginatedResponse
|
|
8
|
+
from ..models.components import OSImage
|
|
9
|
+
from ._base import AsyncResource, SyncResource
|
|
10
|
+
|
|
11
|
+
_PATH = "os-images/"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OSImages(SyncResource):
|
|
15
|
+
"""Synchronous OS image operations."""
|
|
16
|
+
|
|
17
|
+
def list(
|
|
18
|
+
self,
|
|
19
|
+
*,
|
|
20
|
+
flavor_id: Optional[int] = None,
|
|
21
|
+
gpu_id: Optional[int] = None,
|
|
22
|
+
page: Optional[int] = None,
|
|
23
|
+
) -> Union[List[OSImage], PaginatedResponse[OSImage]]:
|
|
24
|
+
"""List available OS images.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
flavor_id: Filter images compatible with a specific FlavorConfig
|
|
28
|
+
(preferred — the canonical compatibility dimension).
|
|
29
|
+
gpu_id: Filter images compatible with any flavor that uses this GPU.
|
|
30
|
+
page: If given, return a single page instead of all results.
|
|
31
|
+
|
|
32
|
+
Without any filter, only universal images (compatible with every flavor)
|
|
33
|
+
are returned.
|
|
34
|
+
"""
|
|
35
|
+
params: Dict[str, Any] = {}
|
|
36
|
+
if flavor_id is not None:
|
|
37
|
+
params["flavor_id"] = flavor_id
|
|
38
|
+
elif gpu_id is not None:
|
|
39
|
+
params["gpu_id"] = gpu_id
|
|
40
|
+
|
|
41
|
+
if page is not None:
|
|
42
|
+
return self._list_page(_PATH, OSImage.from_dict, params=params or None, page=page)
|
|
43
|
+
return self._list_all(_PATH, OSImage.from_dict, params=params or None)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AsyncOSImages(AsyncResource):
|
|
47
|
+
"""Asynchronous OS image operations."""
|
|
48
|
+
|
|
49
|
+
async def list(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
flavor_id: Optional[int] = None,
|
|
53
|
+
gpu_id: Optional[int] = None,
|
|
54
|
+
page: Optional[int] = None,
|
|
55
|
+
) -> Union[List[OSImage], PaginatedResponse[OSImage]]:
|
|
56
|
+
params: Dict[str, Any] = {}
|
|
57
|
+
if flavor_id is not None:
|
|
58
|
+
params["flavor_id"] = flavor_id
|
|
59
|
+
elif gpu_id is not None:
|
|
60
|
+
params["gpu_id"] = gpu_id
|
|
61
|
+
|
|
62
|
+
if page is not None:
|
|
63
|
+
return await self._list_page(
|
|
64
|
+
_PATH, OSImage.from_dict, params=params or None, page=page
|
|
65
|
+
)
|
|
66
|
+
return await self._list_all(_PATH, OSImage.from_dict, params=params or None)
|