calagoapi 1.0.dev1__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.
Files changed (71) hide show
  1. calagoapi/__init__.py +190 -0
  2. calagoapi/account/__init__.py +14 -0
  3. calagoapi/account/account.py +292 -0
  4. calagoapi/account/auth.py +72 -0
  5. calagoapi/account/models.py +221 -0
  6. calagoapi/admin/__init__.py +48 -0
  7. calagoapi/admin/admin.py +341 -0
  8. calagoapi/admin/managers.py +490 -0
  9. calagoapi/admin/resources.py +970 -0
  10. calagoapi/aio/__init__.py +9 -0
  11. calagoapi/aio/console.py +289 -0
  12. calagoapi/aio/panel.py +176 -0
  13. calagoapi/api/__init__.py +63 -0
  14. calagoapi/api/account.py +195 -0
  15. calagoapi/api/admin.py +402 -0
  16. calagoapi/api/attachments.py +27 -0
  17. calagoapi/api/auth.py +52 -0
  18. calagoapi/api/backups.py +99 -0
  19. calagoapi/api/client.py +198 -0
  20. calagoapi/api/databases.py +217 -0
  21. calagoapi/api/files.py +210 -0
  22. calagoapi/api/network.py +84 -0
  23. calagoapi/api/public.py +13 -0
  24. calagoapi/api/schedules.py +79 -0
  25. calagoapi/api/servers.py +114 -0
  26. calagoapi/api/users.py +27 -0
  27. calagoapi/api/versions.py +29 -0
  28. calagoapi/backups/__init__.py +8 -0
  29. calagoapi/backups/backup.py +292 -0
  30. calagoapi/backups/manager.py +177 -0
  31. calagoapi/console/__init__.py +6 -0
  32. calagoapi/console/console.py +392 -0
  33. calagoapi/databases/__init__.py +12 -0
  34. calagoapi/databases/database.py +445 -0
  35. calagoapi/databases/explorer.py +159 -0
  36. calagoapi/databases/manager.py +122 -0
  37. calagoapi/files/__init__.py +9 -0
  38. calagoapi/files/file.py +196 -0
  39. calagoapi/files/manager.py +437 -0
  40. calagoapi/network/__init__.py +8 -0
  41. calagoapi/network/allocation.py +99 -0
  42. calagoapi/network/manager.py +132 -0
  43. calagoapi/nodes/__init__.py +6 -0
  44. calagoapi/nodes/node.py +477 -0
  45. calagoapi/panel.py +162 -0
  46. calagoapi/schedules/__init__.py +14 -0
  47. calagoapi/schedules/builders.py +461 -0
  48. calagoapi/schedules/manager.py +107 -0
  49. calagoapi/schedules/schedule.py +214 -0
  50. calagoapi/servers/__init__.py +13 -0
  51. calagoapi/servers/admin.py +430 -0
  52. calagoapi/servers/generic.py +785 -0
  53. calagoapi/servers/manager.py +167 -0
  54. calagoapi/servers/minecraft.py +207 -0
  55. calagoapi/tunnels/__init__.py +5 -0
  56. calagoapi/tunnels/tunnel.py +112 -0
  57. calagoapi/users/__init__.py +11 -0
  58. calagoapi/users/activity.py +45 -0
  59. calagoapi/users/subuser.py +127 -0
  60. calagoapi/users/user.py +267 -0
  61. calagoapi/utils/__init__.py +43 -0
  62. calagoapi/utils/convert.py +113 -0
  63. calagoapi/utils/exceptions.py +72 -0
  64. calagoapi/utils/model.py +142 -0
  65. calagoapi/utils/resource.py +100 -0
  66. calagoapi/utils/time.py +68 -0
  67. calagoapi-1.0.dev1.dist-info/METADATA +266 -0
  68. calagoapi-1.0.dev1.dist-info/RECORD +71 -0
  69. calagoapi-1.0.dev1.dist-info/WHEEL +5 -0
  70. calagoapi-1.0.dev1.dist-info/licenses/LICENSE +674 -0
  71. calagoapi-1.0.dev1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,267 @@
1
+ from calagoapi.utils.model import Model, idOf
2
+ from calagoapi.users.activity import Activity
3
+
4
+
5
+ class User(Model):
6
+ '''
7
+ A panel user, as seen by an administrator.
8
+
9
+ Returned by `panel.admin.users`. Requires an admin API key.
10
+
11
+ Example:
12
+ user = panel.admin.users.getUser('...')
13
+ if user.isSuspended():
14
+ user.unsuspend()
15
+ user.setPassword('a-new-password')
16
+ '''
17
+ def _api(self):
18
+ return self.panel.api.admin.users
19
+
20
+ # getters
21
+ def getUsername(self) -> str:
22
+ '''Returns the username.'''
23
+ return self.data.get('username')
24
+
25
+ def getEmail(self) -> str:
26
+ '''Returns the email address.'''
27
+ return self.data.get('email')
28
+
29
+ def getName(self) -> str:
30
+ '''Returns "First Last", or the username if no name is set.'''
31
+ name = ' '.join(filter(None, [self.data.get('name_first'), self.data.get('name_last')]))
32
+ return name or self.getUsername()
33
+
34
+ def getFirstName(self) -> str:
35
+ '''Returns the first name.'''
36
+ return self.data.get('name_first')
37
+
38
+ def getLastName(self) -> str:
39
+ '''Returns the last name.'''
40
+ return self.data.get('name_last')
41
+
42
+ def getExternalId(self) -> str:
43
+ '''Returns the external id used by billing systems, if set.'''
44
+ return self.data.get('external_id')
45
+
46
+ def getRole(self) -> dict:
47
+ '''Returns the assigned role, or None.'''
48
+ return self.data.get('role')
49
+
50
+ def getLanguage(self) -> str:
51
+ '''Returns the preferred language code.'''
52
+ return self.data.get('language')
53
+
54
+ def getAvatar(self) -> str:
55
+ '''Returns the avatar URL, if any.'''
56
+ return self.data.get('avatar')
57
+
58
+ def getTwoFactorMethods(self) -> list:
59
+ '''Returns enabled two-factor methods: totp, security_key, email.'''
60
+ return self.data.get('two_factor_methods', [])
61
+
62
+ # state checks
63
+ def isAdmin(self) -> bool:
64
+ '''Returns True for root administrators.'''
65
+ return bool(self.data.get('admin'))
66
+
67
+ def isSuspended(self) -> bool:
68
+ '''Returns True if the user is suspended.'''
69
+ return bool(self.data.get('suspended'))
70
+
71
+ def isFrozen(self) -> bool:
72
+ '''Returns True if the account is frozen (read-only).'''
73
+ return bool(self.data.get('frozen'))
74
+
75
+ def isEmailVerified(self) -> bool:
76
+ '''Returns True if the email address is verified.'''
77
+ return bool(self.data.get('email_verified'))
78
+
79
+ def hasTwoFactor(self) -> bool:
80
+ '''Returns True if any two-factor method is enabled.'''
81
+ return bool(self.getTwoFactorMethods() or self.data.get('totp_enabled'))
82
+
83
+ def hasPassword(self) -> bool:
84
+ '''Returns True if the user has a password (OAuth-only users do not).'''
85
+ return bool(self.data.get('has_password'))
86
+
87
+ # actions
88
+ def refresh(self) -> 'User':
89
+ '''Re-fetches the user.'''
90
+ self.data = self._api().get(self.getUUID())
91
+ return self
92
+
93
+ def update(self, **fields) -> 'User':
94
+ '''
95
+ Updates the user.
96
+
97
+ Accepted fields: username, email, name_first, name_last, password, admin,
98
+ frozen, suspended, language, role_uuid, external_id.
99
+
100
+ Example:
101
+ user.update(email='new@example.com', language='de')
102
+ '''
103
+ if 'role' in fields:
104
+ fields['role_uuid'] = idOf(fields.pop('role'))
105
+ self._api().update(self.getUUID(), **fields)
106
+ return self._update({k: v for k, v in fields.items() if k != 'password'})
107
+
108
+ def setPassword(self, password: str) -> 'User':
109
+ '''Sets a new password.'''
110
+ return self.update(password=password)
111
+
112
+ def setEmail(self, email: str) -> 'User':
113
+ '''Changes the email address.'''
114
+ return self.update(email=email)
115
+
116
+ def setUsername(self, username: str) -> 'User':
117
+ '''Changes the username.'''
118
+ return self.update(username=username)
119
+
120
+ def setAdmin(self, admin: bool) -> 'User':
121
+ '''Grants or revokes root admin.'''
122
+ return self.update(admin=admin)
123
+
124
+ def setRole(self, role) -> 'User':
125
+ '''Assigns a role (Role object or UUID).'''
126
+ return self.update(role=role)
127
+
128
+ def suspend(self) -> 'User':
129
+ '''Suspends the user.'''
130
+ return self.update(suspended=True)
131
+
132
+ def unsuspend(self) -> 'User':
133
+ '''Lifts a suspension.'''
134
+ return self.update(suspended=False)
135
+
136
+ def freeze(self) -> 'User':
137
+ '''Freezes the account.'''
138
+ return self.update(frozen=True)
139
+
140
+ def unfreeze(self) -> 'User':
141
+ '''Unfreezes the account.'''
142
+ return self.update(frozen=False)
143
+
144
+ def disableTwoFactor(self) -> None:
145
+ '''Removes every two-factor method from the account.'''
146
+ return self.panel.api.admin.userDisableTwoFactor(self.getUUID())
147
+
148
+ def sendPasswordReset(self) -> None:
149
+ '''Emails the user a password reset link.'''
150
+ return self.panel.api.admin.userSendPasswordReset(self.getUUID())
151
+
152
+ def verifyEmail(self) -> None:
153
+ '''Marks the email address as verified.'''
154
+ return self.panel.api.admin.userVerifyEmail(self.getUUID())
155
+
156
+ def getServers(self, owned: bool = None, search: str = None, page: int = None, per_page: int = None):
157
+ '''
158
+ Returns servers the user can access.
159
+
160
+ Args:
161
+ owned: True for owned servers only, False for subuser access only.
162
+ '''
163
+ from calagoapi.servers.admin import AdminServer
164
+ return self.panel.api.admin.userServers(self.getUUID(), owned, search, page, per_page).map(
165
+ lambda data: AdminServer(self.panel, data)
166
+ )
167
+
168
+ def getActivity(self, search: str = None, page: int = None, per_page: int = 50):
169
+ '''Returns the user's activity log (first page by default).'''
170
+ return self.panel.api.admin.userActivity(self.getUUID(), search, page or 1, per_page).map(
171
+ lambda data: Activity(self.panel, data)
172
+ )
173
+
174
+ def getOAuthLinks(self, page: int = None, per_page: int = None):
175
+ '''Returns linked OAuth accounts.'''
176
+ return self.panel.api.admin.userOauthLinks(self.getUUID()).list(page=page, per_page=per_page)
177
+
178
+ def getOAuthLink(self, oauth_link_id: str) -> dict:
179
+ '''Returns one linked OAuth identity.'''
180
+ return self.panel.api.admin.userOauthLinks(self.getUUID()).get(oauth_link_id)
181
+
182
+ def linkOAuth(self, oauth_provider, identifier: str) -> dict:
183
+ '''Links an OAuth identity to the user.'''
184
+ return self.panel.api.admin.userOauthLinks(self.getUUID()).create(
185
+ oauth_provider_uuid=idOf(oauth_provider), identifier=identifier
186
+ )
187
+
188
+ def unlinkOAuth(self, oauth_link_id: str) -> None:
189
+ '''Removes a linked OAuth identity.'''
190
+ return self.panel.api.admin.userOauthLinks(self.getUUID()).delete(oauth_link_id)
191
+
192
+ def delete(self) -> None:
193
+ '''Deletes the user.'''
194
+ return self._api().delete(self.getUUID())
195
+
196
+
197
+ class UserManager:
198
+ '''
199
+ Admin user management, available as `panel.admin.users`.
200
+
201
+ Example:
202
+ user = panel.admin.users.createUser('steve', 'steve@example.com', password='hunter2')
203
+ for user in panel.admin.users.getUsers(search='steve'):
204
+ print(user.getUsername(), user.isAdmin())
205
+ '''
206
+ def __init__(self, panel):
207
+ self.panel = panel
208
+ self.api = panel.api.admin.users
209
+
210
+ def _wrap(self, data):
211
+ return User(self.panel, data)
212
+
213
+ def getUsers(self, search: str = None, page: int = None, per_page: int = None):
214
+ '''Lists users as a Page of User objects.'''
215
+ return self.api.list(search, page, per_page).map(self._wrap)
216
+
217
+ def getUser(self, user_id: str) -> User:
218
+ '''Returns a user by UUID.'''
219
+ return self._wrap(self.api.get(user_id))
220
+
221
+ def getUserByExternalId(self, external_id: str) -> User:
222
+ '''Returns a user by external id (as set by billing integrations).'''
223
+ return self._wrap(self.panel.api.admin.userByExternalId(external_id))
224
+
225
+ def getUserByUsername(self, username: str) -> User:
226
+ '''Returns a user by exact username or email, or None.'''
227
+ for user in self.getUsers(search=username):
228
+ if username in (user.getUsername(), user.getEmail()):
229
+ return user
230
+ return None
231
+
232
+ def createUser(self, username: str, email: str, password: str = None, admin: bool = False, language: str = 'en',
233
+ name_first: str = None, name_last: str = None, role=None, external_id: str = None,
234
+ send_email: bool = None, verify_email: bool = None) -> User:
235
+ '''
236
+ Creates a user.
237
+
238
+ Args:
239
+ username: Username.
240
+ email: Email address.
241
+ password: Password. Leave empty to let the user set one via email.
242
+ admin: Make the user a root administrator.
243
+ language: Language code.
244
+ name_first: First name.
245
+ name_last: Last name.
246
+ role: Role object or UUID.
247
+ external_id: Id from an external system.
248
+ send_email: Send the welcome email.
249
+ verify_email: Mark the email address as already verified.
250
+ '''
251
+ return self._wrap(self.api.create(
252
+ username=username,
253
+ email=email,
254
+ password=password,
255
+ admin=admin,
256
+ language=language,
257
+ name_first=name_first,
258
+ name_last=name_last,
259
+ role_uuid=idOf(role),
260
+ external_id=external_id,
261
+ send_email=send_email,
262
+ verify_email=verify_email,
263
+ ))
264
+
265
+ def deleteUser(self, user) -> None:
266
+ '''Deletes a user.'''
267
+ return self.api.delete(idOf(user))
@@ -0,0 +1,43 @@
1
+ from .model import Model, Page, idOf
2
+ from .convert import convert, formatBytes, formatDuration
3
+ from .time import waitForState, waitForOnline, waitForOffline, secsUntil, minsUntil
4
+ from .exceptions import (
5
+ CalagoAPIError,
6
+ BadRequestError,
7
+ AuthenticationError,
8
+ PermissionDeniedError,
9
+ NotFoundError,
10
+ ConflictError,
11
+ RateLimitError,
12
+ ServerError,
13
+ PanelConnectionError,
14
+ ConsoleError,
15
+ InvalidActionError,
16
+ WaitTimeoutError,
17
+ )
18
+
19
+ __all__ = [
20
+ 'Model',
21
+ 'Page',
22
+ 'idOf',
23
+ 'convert',
24
+ 'formatBytes',
25
+ 'formatDuration',
26
+ 'waitForState',
27
+ 'waitForOnline',
28
+ 'waitForOffline',
29
+ 'secsUntil',
30
+ 'minsUntil',
31
+ 'CalagoAPIError',
32
+ 'BadRequestError',
33
+ 'AuthenticationError',
34
+ 'PermissionDeniedError',
35
+ 'NotFoundError',
36
+ 'ConflictError',
37
+ 'RateLimitError',
38
+ 'ServerError',
39
+ 'PanelConnectionError',
40
+ 'ConsoleError',
41
+ 'InvalidActionError',
42
+ 'WaitTimeoutError',
43
+ ]
@@ -0,0 +1,113 @@
1
+ from types import SimpleNamespace
2
+
3
+
4
+ TIME_UNITS = {
5
+ 'ns': 1e-9,
6
+ 'us': 1e-6,
7
+ 'ms': 1e-3,
8
+ 's': 1,
9
+ 'min': 60,
10
+ 'h': 3600,
11
+ 'day': 86400,
12
+ }
13
+
14
+ BYTE_UNITS = {
15
+ # decimal
16
+ 'b': 1,
17
+ 'kb': 1_000,
18
+ 'mb': 1_000_000,
19
+ 'gb': 1_000_000_000,
20
+ 'tb': 1_000_000_000_000,
21
+
22
+ # binary
23
+ 'kib': 1024,
24
+ 'mib': 1024**2,
25
+ 'gib': 1024**3,
26
+ 'tib': 1024**4,
27
+ }
28
+
29
+
30
+ def _convert(value, src, dst, units, rounding=None):
31
+ if src not in units or dst not in units:
32
+ raise ValueError(f'Unknown unit, expected one of: {", ".join(units)}')
33
+ result = float(value) * units[src] / units[dst]
34
+ return round(result, rounding) if rounding is not None else result
35
+
36
+
37
+ def _time(value, src, dst, rounding=None):
38
+ '''
39
+ Converts a duration between units.
40
+
41
+ Args:
42
+ value: The number to convert.
43
+ src: Source unit: ns, us, ms, s, min, h, day.
44
+ dst: Target unit.
45
+ rounding: Optional number of decimal places.
46
+
47
+ Example:
48
+ convert.time(90_000, 'ms', 'min') # 1.5
49
+ '''
50
+ return _convert(value, src, dst, TIME_UNITS, rounding)
51
+
52
+
53
+ def _byte(value, src, dst, rounding=None):
54
+ '''
55
+ Converts a size between units.
56
+
57
+ Args:
58
+ value: The number to convert.
59
+ src: Source unit: b, kb, mb, gb, tb, kib, mib, gib, tib.
60
+ dst: Target unit.
61
+ rounding: Optional number of decimal places.
62
+
63
+ Example:
64
+ convert.byte(server.resources()['memory_bytes'], 'b', 'gib', 2)
65
+ '''
66
+ return _convert(value, src, dst, BYTE_UNITS, rounding)
67
+
68
+
69
+ def formatBytes(value, rounding: int = 2) -> str:
70
+ '''
71
+ Formats a byte count as a human readable string using binary units.
72
+
73
+ Example:
74
+ formatBytes(1536) # '1.5 KiB'
75
+ '''
76
+ if value is None:
77
+ return None
78
+ value = float(value)
79
+ for unit in ('B', 'KiB', 'MiB', 'GiB', 'TiB'):
80
+ if abs(value) < 1024 or unit == 'TiB':
81
+ return f'{round(value, rounding):g} {unit}'
82
+ value /= 1024
83
+
84
+
85
+ def formatDuration(milliseconds) -> str:
86
+ '''
87
+ Formats a duration in milliseconds the same way HungerLib formats uptime.
88
+
89
+ Example:
90
+ formatDuration(3_900_000) # '1h 5m'
91
+ '''
92
+ if milliseconds is None:
93
+ return None
94
+ seconds = int(milliseconds) // 1000
95
+ days = seconds // 86400
96
+ hours = (seconds % 86400) // 3600
97
+ minutes = (seconds % 3600) // 60
98
+ secs = seconds % 60
99
+ if days > 0:
100
+ return f'{days}d {hours}h'
101
+ if hours > 0:
102
+ return f'{hours}h {minutes}m'
103
+ if minutes > 0:
104
+ return f'{minutes}m {secs}s'
105
+ return f'{secs}s'
106
+
107
+
108
+ convert = SimpleNamespace(
109
+ time=_time,
110
+ byte=_byte,
111
+ formatBytes=formatBytes,
112
+ formatDuration=formatDuration,
113
+ )
@@ -0,0 +1,72 @@
1
+ # CalagoAPI exceptions
2
+ class CalagoAPIError(Exception):
3
+ '''
4
+ Base class for every error raised by CalagoAPI.
5
+
6
+ Catch this to handle any panel error in one place.
7
+
8
+ Attributes:
9
+ message: Human readable error message.
10
+ status_code: HTTP status code returned by the panel, or None if the request never completed.
11
+ errors: List of error strings returned by Calagopus (its `errors` array).
12
+ response: The raw `requests.Response`, or None.
13
+
14
+ Example:
15
+ from calagoapi import Panel, CalagoAPIError
16
+
17
+ try:
18
+ panel.servers.getServer('does-not-exist')
19
+ except CalagoAPIError as e:
20
+ print(e.status_code, e.errors)
21
+ '''
22
+ def __init__(self, message, status_code=None, errors=None, response=None):
23
+ super().__init__(message)
24
+ self.message = message
25
+ self.status_code = status_code
26
+ self.errors = errors or []
27
+ self.response = response
28
+
29
+
30
+ # HTTP errors
31
+ class BadRequestError(CalagoAPIError):
32
+ '''The panel rejected the request (HTTP 400). Usually invalid parameters.'''
33
+
34
+ class AuthenticationError(CalagoAPIError):
35
+ '''The API key is missing, invalid or expired (HTTP 401).'''
36
+
37
+ class PermissionDeniedError(CalagoAPIError):
38
+ '''The API key is valid but lacks permission for this action (HTTP 403).'''
39
+
40
+ class NotFoundError(CalagoAPIError):
41
+ '''The requested resource does not exist (HTTP 404).'''
42
+
43
+ class ConflictError(CalagoAPIError):
44
+ '''The request conflicts with the current state, e.g. a server that is still installing (HTTP 409).'''
45
+
46
+ class RateLimitError(CalagoAPIError):
47
+ '''
48
+ Too many requests (HTTP 429).
49
+
50
+ Attributes:
51
+ retry_after: Seconds to wait before retrying, if the panel sent a Retry-After header.
52
+ '''
53
+ def __init__(self, message, status_code=None, errors=None, response=None, retry_after=None):
54
+ super().__init__(message, status_code, errors, response)
55
+ self.retry_after = retry_after
56
+
57
+ class ServerError(CalagoAPIError):
58
+ '''The panel failed to handle the request (HTTP 5xx).'''
59
+
60
+ class PanelConnectionError(CalagoAPIError):
61
+ '''The panel could not be reached (DNS failure, refused connection, timeout, ...).'''
62
+
63
+ class ConsoleError(CalagoAPIError):
64
+ '''The live console websocket could not connect, authenticate or send a message.'''
65
+
66
+
67
+ # Usage errors
68
+ class InvalidActionError(CalagoAPIError, ValueError):
69
+ '''An invalid value was passed to a method, e.g. an unknown power action.'''
70
+
71
+ class WaitTimeoutError(CalagoAPIError, TimeoutError):
72
+ '''A wait helper gave up before the expected state was reached.'''
@@ -0,0 +1,142 @@
1
+ import math
2
+
3
+
4
+ def idOf(value):
5
+ '''
6
+ Returns the id of an object, or the value itself if it already is an id.
7
+
8
+ Lets every method accept either an object or its UUID.
9
+
10
+ Example:
11
+ idOf(server) # 'abc123...'
12
+ idOf('abc123') # 'abc123'
13
+ '''
14
+ if isinstance(value, Model):
15
+ return getattr(value, 'server_id', None) or value.getUUID()
16
+ return value
17
+
18
+
19
+ class Model:
20
+ '''
21
+ Base class for every object returned by the panel.
22
+
23
+ Wraps the raw JSON returned by Calagopus and exposes it through getters.
24
+ The raw data is always available through `.data`, `get()` or `toDict()`,
25
+ so nothing the panel returns is ever hidden.
26
+
27
+ Args:
28
+ panel: The `Panel` this object belongs to.
29
+ data: Raw data returned by the panel.
30
+
31
+ Example:
32
+ backup = server.getBackup('...')
33
+ backup.getName() # getter
34
+ backup.get('checksum') # raw access
35
+ backup['bytes'] # raw access
36
+ backup.toDict() # copy of all data
37
+ '''
38
+ def __init__(self, panel, data: dict = None):
39
+ self.panel = panel
40
+ self.data = data or {}
41
+
42
+ def __getitem__(self, key):
43
+ return self.data[key]
44
+
45
+ def __contains__(self, key):
46
+ return key in self.data
47
+
48
+ def __eq__(self, other):
49
+ if not isinstance(other, self.__class__):
50
+ return NotImplemented
51
+ return self.getUUID() is not None and self.getUUID() == other.getUUID()
52
+
53
+ def __hash__(self):
54
+ return hash((self.__class__.__name__, self.getUUID()))
55
+
56
+ def __repr__(self):
57
+ label = self.data.get('name') or self.data.get('username') or self.getUUID()
58
+ return f'<{self.__class__.__name__} {label!r}>'
59
+
60
+ def get(self, key: str, default=None):
61
+ '''
62
+ Returns a raw field from the panel data.
63
+
64
+ Args:
65
+ key: Field name as returned by Calagopus.
66
+ default: Value returned when the field is missing.
67
+
68
+ Example:
69
+ server.get('sftp_port')
70
+ '''
71
+ return self.data.get(key, default)
72
+
73
+ def toDict(self) -> dict:
74
+ '''Returns a copy of the raw panel data.'''
75
+ return dict(self.data)
76
+
77
+ def getUUID(self) -> str:
78
+ '''Returns the object's UUID.'''
79
+ return self.data.get('uuid')
80
+
81
+ def getCreated(self) -> str:
82
+ '''Returns when the object was created, as an ISO-8601 string.'''
83
+ return self.data.get('created')
84
+
85
+ def _update(self, data: dict):
86
+ # merge local changes after a successful PATCH
87
+ self.data.update({k: v for k, v in data.items() if v is not None})
88
+ return self
89
+
90
+
91
+ class Page(list):
92
+ '''
93
+ A list of results, plus pagination info from the panel.
94
+
95
+ Behaves exactly like a normal list, so you can loop over it, index it and
96
+ call `len()` on it.
97
+
98
+ Attributes:
99
+ total: Total number of results on the panel (across all pages).
100
+ page: The page number this list represents.
101
+ per_page: Page size used for the request.
102
+
103
+ Example:
104
+ servers = panel.servers.getServers(page=1, per_page=25)
105
+ print(f'Showing {len(servers)} of {servers.getTotal()}')
106
+ if servers.hasNextPage():
107
+ more = panel.servers.getServers(page=2, per_page=25)
108
+ '''
109
+ def __init__(self, items=None, total=None, page=1, per_page=None):
110
+ super().__init__(items or [])
111
+ self.total = len(self) if total is None else total
112
+ self.page = page
113
+ self.per_page = per_page or len(self)
114
+
115
+ def __repr__(self):
116
+ return f'<Page {self.page}/{self.getPageCount()} items={len(self)} total={self.total}>'
117
+
118
+ def getTotal(self) -> int:
119
+ '''Returns the total number of results across every page.'''
120
+ return self.total
121
+
122
+ def getPage(self) -> int:
123
+ '''Returns the current page number.'''
124
+ return self.page
125
+
126
+ def getPerPage(self) -> int:
127
+ '''Returns the page size.'''
128
+ return self.per_page
129
+
130
+ def getPageCount(self) -> int:
131
+ '''Returns how many pages exist in total.'''
132
+ if not self.per_page:
133
+ return 1
134
+ return max(1, math.ceil(self.total / self.per_page))
135
+
136
+ def hasNextPage(self) -> bool:
137
+ '''Returns True if there are more results after this page.'''
138
+ return self.page < self.getPageCount()
139
+
140
+ def map(self, func) -> 'Page':
141
+ '''Returns a new Page with `func` applied to every item, keeping the pagination info.'''
142
+ return Page([func(item) for item in self], self.total, self.page, self.per_page)