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
calagoapi/api/auth.py ADDED
@@ -0,0 +1,52 @@
1
+ # Internal Calagopus AuthAPI (public, unauthenticated endpoints)
2
+ class AuthAPI:
3
+ def __init__(self, client):
4
+ self.client = client
5
+
6
+ def login(self, user, password, captcha=None):
7
+ return self.client.post('/api/auth/login', json={'user': user, 'password': password, 'captcha': captcha})
8
+
9
+ def checkpoint(self, code, confirmation_token, method=None):
10
+ return self.client.post('/api/auth/login/checkpoint', json={
11
+ 'code': code,
12
+ 'confirmation_token': confirmation_token,
13
+ 'method': method,
14
+ })['user']
15
+
16
+ def checkpointEmail(self, confirmation_token):
17
+ return self.client.post('/api/auth/login/checkpoint/email', json={'confirmation_token': confirmation_token})
18
+
19
+ def register(self, username, email, password, name_first=None, name_last=None, language=None, captcha=None):
20
+ return self.client.post('/api/auth/register', json={
21
+ 'username': username,
22
+ 'email': email,
23
+ 'password': password,
24
+ 'name_first': name_first,
25
+ 'name_last': name_last,
26
+ 'language': language,
27
+ 'captcha': captcha,
28
+ })['user']
29
+
30
+ def verifyEmail(self, token):
31
+ return self.client.post('/api/auth/email/verify', json={'token': token})
32
+
33
+ def forgotPassword(self, email, captcha=None):
34
+ return self.client.post('/api/auth/password/forgot', json={'email': email, 'captcha': captcha})
35
+
36
+ def resetPassword(self, token, new_password):
37
+ return self.client.post('/api/auth/password/reset', json={'token': token, 'new_password': new_password})
38
+
39
+ def oauthProviders(self):
40
+ return self.client.get('/api/auth/oauth')['oauth_providers']
41
+
42
+ def oauthRedirectUrl(self, oauth_provider_id):
43
+ return f'{self.client.url}/api/auth/oauth/redirect/{oauth_provider_id}'
44
+
45
+ def securityKeyOptions(self, user=None, discoverable=False):
46
+ if discoverable:
47
+ return self.client.get('/api/auth/login/security-key/discoverable')
48
+ return self.client.get('/api/auth/login/security-key', params={'user': user})
49
+
50
+ def securityKeyLogin(self, uuid, public_key_credential, discoverable=False):
51
+ path = '/api/auth/login/security-key/discoverable' if discoverable else '/api/auth/login/security-key'
52
+ return self.client.post(path, json={'uuid': uuid, 'public_key_credential': public_key_credential})['user']
@@ -0,0 +1,99 @@
1
+ # Internal Calagopus BackupsAPI
2
+ class BackupsAPI:
3
+ def __init__(self, client):
4
+ self.client = client
5
+
6
+ def _path(self, server_id, suffix=''):
7
+ return f'/api/client/servers/{server_id}/backups{suffix}'
8
+
9
+ # backups
10
+ def list(self, server_id, search=None, ungrouped=None, kind=None, database_instance_uuid=None, database_type=None, page=None, per_page=None):
11
+ return self.client.list(self._path(server_id), 'backups', {
12
+ 'search': search,
13
+ 'ungrouped': ungrouped,
14
+ 'kind': kind,
15
+ 'database_instance_uuid': database_instance_uuid,
16
+ 'database_type': database_type,
17
+ }, page, per_page)
18
+
19
+ def system(self, server_id, search=None, page=None, per_page=None):
20
+ return self.client.list(self._path(server_id, '/system'), 'backups', {'search': search}, page, per_page)
21
+
22
+ def get(self, server_id, backup_id):
23
+ return self.client.get(self._path(server_id, f'/{backup_id}'))['backup']
24
+
25
+ def create(self, server_id, name=None, ignored_files=None, backup_group_uuid=None, database_instance_uuid=None):
26
+ return self.client.post(self._path(server_id), json={
27
+ 'name': name,
28
+ 'ignored_files': ignored_files or [],
29
+ 'backup_group_uuid': backup_group_uuid,
30
+ 'database_instance_uuid': database_instance_uuid,
31
+ })['backup']
32
+
33
+ def update(self, server_id, backup_id, name=None, backup_group_uuid=None, locked=None):
34
+ return self.client.patch(self._path(server_id, f'/{backup_id}'), json={
35
+ 'name': name,
36
+ 'backup_group_uuid': backup_group_uuid,
37
+ 'locked': locked,
38
+ })
39
+
40
+ def delete(self, server_id, backup_id):
41
+ return self.client.delete(self._path(server_id, f'/{backup_id}'))
42
+
43
+ def deleteMany(self, server_id, backup_ids):
44
+ return self.client.delete(self._path(server_id), json={'selector': {'type': 'uuids', 'uuids': backup_ids}})
45
+
46
+ def updateMany(self, server_id, backup_ids, backup_group_uuid=None, locked=None):
47
+ return self.client.patch(self._path(server_id), json={
48
+ 'selector': {'type': 'uuids', 'uuids': backup_ids},
49
+ 'backup_group_uuid': backup_group_uuid,
50
+ 'locked': locked,
51
+ })
52
+
53
+ def downloadUrl(self, server_id, backup_id, archive_format=None):
54
+ return self.client.get(
55
+ self._path(server_id, f'/{backup_id}/download'),
56
+ params={'archive_format': archive_format}
57
+ )['url']
58
+
59
+ def query(self, server_id, backup_id):
60
+ return self.client.get(self._path(server_id, f'/{backup_id}/query'))
61
+
62
+ def export(self, server_id, backup_id, path, archive_format=None, foreground=None):
63
+ return self.client.post(self._path(server_id, f'/{backup_id}/export'), json={
64
+ 'path': path,
65
+ 'archive_format': archive_format,
66
+ 'foreground': foreground,
67
+ })
68
+
69
+ def restore(self, server_id, backup_id, truncate_directory=None, restore_startup=None, database_instance_uuid=None):
70
+ return self.client.post(self._path(server_id, f'/{backup_id}/restore'), json={
71
+ 'truncate_directory': truncate_directory,
72
+ 'restore_startup': restore_startup,
73
+ 'database_instance_uuid': database_instance_uuid,
74
+ })
75
+
76
+ def unlock(self, server_id):
77
+ return self.client.post(self._path(server_id, '/unlock'))
78
+
79
+ def usage(self, server_id):
80
+ return self.client.get(self._path(server_id, '/usage'))['usage']
81
+
82
+ # backup groups
83
+ def groups(self, server_id):
84
+ return self.client.get(self._path(server_id, '/groups'))['backup_groups']
85
+
86
+ def groupBackups(self, server_id, group_id, search=None, page=None, per_page=None):
87
+ return self.client.list(self._path(server_id, f'/groups/{group_id}'), 'backups', {'search': search}, page, per_page)
88
+
89
+ def createGroup(self, server_id, name, retention=None):
90
+ return self.client.post(self._path(server_id, '/groups'), json={'name': name, 'retention': retention})['backup_group']
91
+
92
+ def updateGroup(self, server_id, group_id, name=None, retention=None):
93
+ return self.client.patch(self._path(server_id, f'/groups/{group_id}'), json={'name': name, 'retention': retention})
94
+
95
+ def deleteGroup(self, server_id, group_id, lock_backups=None):
96
+ return self.client.delete(self._path(server_id, f'/groups/{group_id}'), json={'lock_backups': lock_backups})
97
+
98
+ def orderGroups(self, server_id, group_order):
99
+ return self.client.put(self._path(server_id, '/groups/order'), json={'backup_group_order': group_order})
@@ -0,0 +1,198 @@
1
+ import time
2
+ from urllib.parse import quote
3
+
4
+ import requests
5
+
6
+ from calagoapi.utils.model import Page
7
+ from calagoapi.utils.exceptions import (
8
+ CalagoAPIError,
9
+ BadRequestError,
10
+ AuthenticationError,
11
+ PermissionDeniedError,
12
+ NotFoundError,
13
+ ConflictError,
14
+ RateLimitError,
15
+ ServerError,
16
+ PanelConnectionError,
17
+ )
18
+
19
+
20
+ RETRY_STATUSES = (502, 503, 504)
21
+
22
+ ERRORS = {
23
+ 400: BadRequestError,
24
+ 401: AuthenticationError,
25
+ 403: PermissionDeniedError,
26
+ 404: NotFoundError,
27
+ 409: ConflictError,
28
+ 429: RateLimitError,
29
+ }
30
+
31
+
32
+ def segment(value):
33
+ '''Encodes a free-text value (file name, identifier, ...) for use as one URL path segment'''
34
+ return quote(str(value), safe='')
35
+
36
+
37
+ def clean(values):
38
+ '''Drops None values so optional parameters are never sent'''
39
+ if values is None:
40
+ return None
41
+ return {k: v for k, v in values.items() if v is not None}
42
+
43
+
44
+ class Client:
45
+ '''
46
+ Internal HTTP client for Calagopus.
47
+
48
+ Every request made by CalagoAPI goes through this class. It handles
49
+ authentication, JSON encoding, error handling, retries and pagination.
50
+ You normally never need it directly, use `Panel` instead.
51
+
52
+ Retries: rate limited requests (429) are retried after the panel's
53
+ Retry-After delay. GET requests are also retried on connection errors
54
+ and on 502, 503 and 504. Other requests are never retried, so a power
55
+ action or backup is never sent twice.
56
+ '''
57
+ def __init__(self, url: str, api_key: str, timeout: int = 10, verify: bool = True, user_agent: str = None,
58
+ max_retries: int = 3, max_retry_wait: int = 60):
59
+ self.url = url.rstrip('/')
60
+ self.api_key = api_key
61
+ self.timeout = timeout
62
+ self.max_retries = max_retries
63
+ self.max_retry_wait = max_retry_wait
64
+
65
+ self.session = requests.Session()
66
+ self.session.verify = verify
67
+ self.session.headers.update(self.headers)
68
+ if user_agent:
69
+ self.session.headers['User-Agent'] = user_agent
70
+
71
+ @property
72
+ def headers(self):
73
+ return {
74
+ 'Authorization': f'Bearer {self.api_key}',
75
+ 'Accept': 'application/json',
76
+ }
77
+
78
+ # http core
79
+ def request(self, method, path, params=None, json=None, data=None, headers=None, raw=False, timeout=None, stream=False):
80
+ # optional body fields left as None are omitted, the panel then keeps its default
81
+ if isinstance(json, dict):
82
+ json = clean(json)
83
+
84
+ attempt = 0
85
+ while True:
86
+ try:
87
+ r = self.session.request(
88
+ method,
89
+ f'{self.url}{path}',
90
+ params=clean(params),
91
+ json=json,
92
+ data=data,
93
+ headers=headers,
94
+ timeout=timeout or self.timeout,
95
+ stream=stream,
96
+ )
97
+ except requests.RequestException as e:
98
+ if method == 'GET' and attempt < self.max_retries:
99
+ attempt += 1
100
+ time.sleep(self.retryDelay(attempt))
101
+ continue
102
+ raise PanelConnectionError(f'Could not reach {self.url}: {e}') from e
103
+
104
+ retryable = r.status_code == 429 or (method == 'GET' and r.status_code in RETRY_STATUSES)
105
+ if retryable and attempt < self.max_retries:
106
+ attempt += 1
107
+ time.sleep(self.retryDelay(attempt, r.headers.get('Retry-After')))
108
+ continue
109
+ break
110
+
111
+ if r.status_code >= 400:
112
+ self.raiseError(r)
113
+ if raw:
114
+ return r
115
+ return self.parse(r)
116
+
117
+ def retryDelay(self, attempt, retry_after=None):
118
+ '''Seconds to wait before a retry: the panel's Retry-After, or 0.5s, 1s, 2s, ...'''
119
+ try:
120
+ delay = float(retry_after)
121
+ except (TypeError, ValueError):
122
+ delay = 0.5 * 2 ** (attempt - 1)
123
+ return min(max(delay, 0), self.max_retry_wait)
124
+
125
+ def close(self):
126
+ '''Closes the pooled HTTP connections.'''
127
+ self.session.close()
128
+
129
+ def get(self, path, params=None, **kwargs):
130
+ return self.request('GET', path, params=params, **kwargs)
131
+ def post(self, path, json=None, params=None, **kwargs):
132
+ return self.request('POST', path, params=params, json=json, **kwargs)
133
+ def put(self, path, json=None, params=None, **kwargs):
134
+ return self.request('PUT', path, params=params, json=json, **kwargs)
135
+ def patch(self, path, json=None, params=None, **kwargs):
136
+ return self.request('PATCH', path, params=params, json=json, **kwargs)
137
+ def delete(self, path, json=None, params=None, **kwargs):
138
+ return self.request('DELETE', path, params=params, json=json, **kwargs)
139
+
140
+ # plain-text bodies (file writes, database imports)
141
+ def postText(self, path, content, params=None, content_type='text/plain'):
142
+ if isinstance(content, str):
143
+ content = content.encode('utf-8')
144
+ return self.request('POST', path, params=params, data=content, headers={'Content-Type': content_type})
145
+
146
+ # responses
147
+ @staticmethod
148
+ def parse(r):
149
+ if not r.content:
150
+ return None
151
+ if 'json' in r.headers.get('Content-Type', ''):
152
+ return r.json()
153
+ return r.text
154
+
155
+ @staticmethod
156
+ def raiseError(r):
157
+ errors = []
158
+ try:
159
+ body = r.json()
160
+ if isinstance(body, dict):
161
+ errors = body.get('errors') or []
162
+ except ValueError:
163
+ pass
164
+
165
+ message = '; '.join(str(e) for e in errors) or r.text.strip() or r.reason
166
+ message = f'{r.status_code} {r.request.method} {r.request.path_url}: {message}'
167
+
168
+ if r.status_code == 429:
169
+ retry_after = r.headers.get('Retry-After')
170
+ raise RateLimitError(message, r.status_code, errors, r, float(retry_after) if retry_after else None)
171
+ if r.status_code >= 500:
172
+ raise ServerError(message, r.status_code, errors, r)
173
+ raise ERRORS.get(r.status_code, CalagoAPIError)(message, r.status_code, errors, r)
174
+
175
+ # pagination
176
+ def list(self, path, key, params=None, page=None, per_page=None) -> Page:
177
+ '''
178
+ Fetches a paginated listing.
179
+
180
+ When `page` is given only that page is returned, otherwise every page is
181
+ fetched and combined into one Page.
182
+ '''
183
+ params = dict(params or {})
184
+
185
+ if page is not None:
186
+ params.update(page=page, per_page=per_page or 50)
187
+ result = self.get(path, params=params)[key]
188
+ return Page(result['data'], result['total'], result['page'], result['per_page'])
189
+
190
+ items = []
191
+ current = 1
192
+ while True:
193
+ params.update(page=current, per_page=per_page or 100)
194
+ result = self.get(path, params=params)[key]
195
+ items.extend(result['data'])
196
+ if not result['data'] or len(items) >= result['total']:
197
+ return Page(items, result['total'], 1, len(items))
198
+ current += 1
@@ -0,0 +1,217 @@
1
+ # Internal Calagopus DatabasesAPI
2
+ class DatabasesAPI:
3
+ def __init__(self, client):
4
+ self.client = client
5
+
6
+ def _path(self, server_id, suffix=''):
7
+ return f'/api/client/servers/{server_id}/databases{suffix}'
8
+
9
+ def _instance(self, server_id, instance_id, suffix=''):
10
+ return self._path(server_id, f'/instances/{instance_id}{suffix}')
11
+
12
+ # hosted databases
13
+ def list(self, server_id, search=None, include_password=None, page=None, per_page=None):
14
+ return self.client.list(self._path(server_id), 'databases', {
15
+ 'search': search,
16
+ 'include_password': include_password,
17
+ }, page, per_page)
18
+
19
+ def get(self, server_id, database_id, include_password=None):
20
+ return self.client.get(
21
+ self._path(server_id, f'/{database_id}'),
22
+ params={'include_password': include_password}
23
+ )['database']
24
+
25
+ def create(self, server_id, database_host_uuid, name):
26
+ return self.client.post(self._path(server_id), json={
27
+ 'database_host_uuid': database_host_uuid,
28
+ 'name': name,
29
+ })['database']
30
+
31
+ def update(self, server_id, database_id, locked=None):
32
+ return self.client.patch(self._path(server_id, f'/{database_id}'), json={'locked': locked})
33
+
34
+ def delete(self, server_id, database_id):
35
+ return self.client.delete(self._path(server_id, f'/{database_id}'))
36
+
37
+ def rotatePassword(self, server_id, database_id):
38
+ return self.client.post(self._path(server_id, f'/{database_id}/rotate-password'))['password']
39
+
40
+ def recreate(self, server_id, database_id):
41
+ return self.client.post(self._path(server_id, f'/{database_id}/recreate'))
42
+
43
+ def size(self, server_id, database_id):
44
+ return self.client.get(self._path(server_id, f'/{database_id}/size'))['size']
45
+
46
+ def hosts(self, server_id):
47
+ return self.client.get(self._path(server_id, '/hosts'))['database_hosts']
48
+
49
+ # database explorer, `base` is the database path from databasePath() / instanceDatabasePath()
50
+ def databasePath(self, server_id, database_id):
51
+ return self._path(server_id, f'/{database_id}')
52
+
53
+ def instanceDatabasePath(self, server_id, instance_id, database_id):
54
+ return self._instance(server_id, instance_id, f'/databases/{database_id}')
55
+
56
+ def schema(self, base):
57
+ return self.client.get(f'{base}/explorer/schema')
58
+
59
+ def tableTypes(self, base):
60
+ return self.client.get(f'{base}/explorer/tables/types')['types']
61
+
62
+ def query(self, base, query, rows=None, read_only=None):
63
+ return self.client.post(f'{base}/explorer/query', json={
64
+ 'query': query,
65
+ 'rows': rows,
66
+ 'read_only': read_only,
67
+ })['results']
68
+
69
+ def rows(self, base, table, schema=None, order_by=None, descending=None, limit=None, offset=None, filters=None):
70
+ return self.client.post(f'{base}/explorer/rows', json={
71
+ 'table': table,
72
+ 'schema': schema,
73
+ 'order_by': order_by,
74
+ 'descending': descending,
75
+ 'limit': limit,
76
+ 'offset': offset,
77
+ 'filters': filters,
78
+ })['result']
79
+
80
+ def insertRows(self, base, table, rows, schema=None):
81
+ return self.client.post(f'{base}/explorer/rows/insert', json={'table': table, 'rows': rows, 'schema': schema})['affected']
82
+
83
+ def updateRows(self, base, table, rows, schema=None):
84
+ return self.client.post(f'{base}/explorer/rows/update', json={'table': table, 'rows': rows, 'schema': schema})['affected']
85
+
86
+ def deleteRows(self, base, table, rows, schema=None):
87
+ return self.client.post(f'{base}/explorer/rows/delete', json={'table': table, 'rows': rows, 'schema': schema})['affected']
88
+
89
+ def createTable(self, base, table, columns, schema=None):
90
+ return self.client.post(f'{base}/explorer/tables', json={'table': table, 'columns': columns, 'schema': schema})
91
+
92
+ def renameTable(self, base, table, name, schema=None):
93
+ return self.client.post(f'{base}/explorer/tables/rename', json={'table': table, 'name': name, 'schema': schema})
94
+
95
+ def deleteTable(self, base, table, schema=None):
96
+ return self.client.post(f'{base}/explorer/tables/delete', json={'table': table, 'schema': schema})
97
+
98
+ def addColumn(self, base, table, column, schema=None):
99
+ return self.client.post(f'{base}/explorer/tables/columns', json={'table': table, 'column': column, 'schema': schema})
100
+
101
+ def renameColumn(self, base, table, column, name, schema=None):
102
+ return self.client.post(f'{base}/explorer/tables/columns/rename', json={
103
+ 'table': table,
104
+ 'column': column,
105
+ 'name': name,
106
+ 'schema': schema,
107
+ })
108
+
109
+ def deleteColumn(self, base, table, column, schema=None):
110
+ return self.client.post(f'{base}/explorer/tables/columns/delete', json={'table': table, 'column': column, 'schema': schema})
111
+
112
+ # database instances
113
+ def instances(self, server_id, search=None, page=None, per_page=None):
114
+ return self.client.list(self._path(server_id, '/instances'), 'instances', {'search': search}, page, per_page)
115
+
116
+ def instance(self, server_id, instance_id):
117
+ return self.client.get(self._instance(server_id, instance_id))
118
+
119
+ def templates(self, server_id):
120
+ return self.client.get(self._path(server_id, '/instances/templates'))['templates']
121
+
122
+ def createInstance(self, server_id, template_uuid, name, image=None):
123
+ return self.client.post(self._path(server_id, '/instances'), json={
124
+ 'template_uuid': template_uuid,
125
+ 'name': name,
126
+ 'image': image,
127
+ })['instance']
128
+
129
+ def updateInstance(self, server_id, instance_id, name=None, locked=None):
130
+ return self.client.patch(self._instance(server_id, instance_id), json={'name': name, 'locked': locked})
131
+
132
+ def deleteInstance(self, server_id, instance_id, force=None):
133
+ return self.client.delete(self._instance(server_id, instance_id), params={'force': force})
134
+
135
+ def instancePower(self, server_id, instance_id, action):
136
+ return self.client.post(self._instance(server_id, instance_id, '/power'), json={'action': action})
137
+
138
+ def instanceResources(self, server_id, instance_id):
139
+ return self.client.get(self._instance(server_id, instance_id, '/resources'))['resources']
140
+
141
+ def instanceLogs(self, server_id, instance_id, lines=None):
142
+ return self.client.get(self._instance(server_id, instance_id, '/logs'), params={'lines': lines})
143
+
144
+ def upgradeInstance(self, server_id, instance_id):
145
+ return self.client.post(self._instance(server_id, instance_id, '/update'))['instance']
146
+
147
+ def exportInstance(self, server_id, instance_id):
148
+ return self.client.get(self._instance(server_id, instance_id, '/export'))
149
+
150
+ def importInstance(self, server_id, instance_id, content, wipe=None):
151
+ return self.client.postText(
152
+ self._instance(server_id, instance_id, '/import'), content,
153
+ params={'wipe': wipe}, content_type='application/octet-stream'
154
+ )
155
+
156
+ def cancelInstanceOperation(self, server_id, instance_id, operation_id):
157
+ return self.client.delete(self._instance(server_id, instance_id, f'/operations/{operation_id}'))
158
+
159
+ # databases inside an instance
160
+ def instanceDatabases(self, server_id, instance_id):
161
+ return self.client.get(self._instance(server_id, instance_id, '/databases'))['databases']
162
+
163
+ def createInstanceDatabase(self, server_id, instance_id, name):
164
+ return self.client.post(self._instance(server_id, instance_id, '/databases'), json={'name': name})['database']
165
+
166
+ def deleteInstanceDatabase(self, server_id, instance_id, database_id):
167
+ return self.client.delete(self.instanceDatabasePath(server_id, instance_id, database_id))
168
+
169
+ def recreateInstanceDatabase(self, server_id, instance_id, database_id):
170
+ return self.client.post(f'{self.instanceDatabasePath(server_id, instance_id, database_id)}/recreate')
171
+
172
+ def instanceDatabaseSize(self, server_id, instance_id, database_id):
173
+ return self.client.get(f'{self.instanceDatabasePath(server_id, instance_id, database_id)}/size')['size']
174
+
175
+ def exportInstanceDatabase(self, server_id, instance_id, database_id):
176
+ return self.client.get(f'{self.instanceDatabasePath(server_id, instance_id, database_id)}/export')
177
+
178
+ def importInstanceDatabase(self, server_id, instance_id, database_id, content, source_db=None, wipe=None):
179
+ return self.client.postText(
180
+ f'{self.instanceDatabasePath(server_id, instance_id, database_id)}/import', content,
181
+ params={'source_db': source_db, 'wipe': wipe}, content_type='application/octet-stream'
182
+ )
183
+
184
+ def importInstanceDatabaseRemote(self, server_id, instance_id, database_id, url, source_db=None, wipe=None):
185
+ return self.client.post(f'{self.instanceDatabasePath(server_id, instance_id, database_id)}/import/remote', json={
186
+ 'url': url,
187
+ 'source_db': source_db,
188
+ 'wipe': wipe,
189
+ })['operation']
190
+
191
+ # instance users
192
+ def instanceUsers(self, server_id, instance_id):
193
+ return self.client.get(self._instance(server_id, instance_id, '/users'))['users']
194
+
195
+ def createInstanceUser(self, server_id, instance_id, username, databases=None):
196
+ return self.client.post(self._instance(server_id, instance_id, '/users'), json={
197
+ 'username': username,
198
+ 'databases': databases,
199
+ })['user']
200
+
201
+ def deleteInstanceUser(self, server_id, instance_id, user_id):
202
+ return self.client.delete(self._instance(server_id, instance_id, f'/users/{user_id}'))
203
+
204
+ def rotateInstanceUserPassword(self, server_id, instance_id, user_id):
205
+ return self.client.post(self._instance(server_id, instance_id, f'/users/{user_id}/rotate-password'))['password']
206
+
207
+ def setInstanceUserDatabases(self, server_id, instance_id, user_id, databases):
208
+ return self.client.put(
209
+ self._instance(server_id, instance_id, f'/users/{user_id}/databases'),
210
+ json={'databases': databases}
211
+ )['databases']
212
+
213
+ def setInstanceUserPermission(self, server_id, instance_id, user_id, database_id, permission):
214
+ return self.client.put(
215
+ self._instance(server_id, instance_id, f'/users/{user_id}/databases/{database_id}'),
216
+ json={'permission': permission}
217
+ )['database']