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.
@@ -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)