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.
- calagoapi/__init__.py +190 -0
- calagoapi/account/__init__.py +14 -0
- calagoapi/account/account.py +292 -0
- calagoapi/account/auth.py +72 -0
- calagoapi/account/models.py +221 -0
- calagoapi/admin/__init__.py +48 -0
- calagoapi/admin/admin.py +341 -0
- calagoapi/admin/managers.py +490 -0
- calagoapi/admin/resources.py +970 -0
- calagoapi/aio/__init__.py +9 -0
- calagoapi/aio/console.py +289 -0
- calagoapi/aio/panel.py +176 -0
- calagoapi/api/__init__.py +63 -0
- calagoapi/api/account.py +195 -0
- calagoapi/api/admin.py +402 -0
- calagoapi/api/attachments.py +27 -0
- calagoapi/api/auth.py +52 -0
- calagoapi/api/backups.py +99 -0
- calagoapi/api/client.py +198 -0
- calagoapi/api/databases.py +217 -0
- calagoapi/api/files.py +210 -0
- calagoapi/api/network.py +84 -0
- calagoapi/api/public.py +13 -0
- calagoapi/api/schedules.py +79 -0
- calagoapi/api/servers.py +114 -0
- calagoapi/api/users.py +27 -0
- calagoapi/api/versions.py +29 -0
- calagoapi/backups/__init__.py +8 -0
- calagoapi/backups/backup.py +292 -0
- calagoapi/backups/manager.py +177 -0
- calagoapi/console/__init__.py +6 -0
- calagoapi/console/console.py +392 -0
- calagoapi/databases/__init__.py +12 -0
- calagoapi/databases/database.py +445 -0
- calagoapi/databases/explorer.py +159 -0
- calagoapi/databases/manager.py +122 -0
- calagoapi/files/__init__.py +9 -0
- calagoapi/files/file.py +196 -0
- calagoapi/files/manager.py +437 -0
- calagoapi/network/__init__.py +8 -0
- calagoapi/network/allocation.py +99 -0
- calagoapi/network/manager.py +132 -0
- calagoapi/nodes/__init__.py +6 -0
- calagoapi/nodes/node.py +477 -0
- calagoapi/panel.py +162 -0
- calagoapi/schedules/__init__.py +14 -0
- calagoapi/schedules/builders.py +461 -0
- calagoapi/schedules/manager.py +107 -0
- calagoapi/schedules/schedule.py +214 -0
- calagoapi/servers/__init__.py +13 -0
- calagoapi/servers/admin.py +430 -0
- calagoapi/servers/generic.py +785 -0
- calagoapi/servers/manager.py +167 -0
- calagoapi/servers/minecraft.py +207 -0
- calagoapi/tunnels/__init__.py +5 -0
- calagoapi/tunnels/tunnel.py +112 -0
- calagoapi/users/__init__.py +11 -0
- calagoapi/users/activity.py +45 -0
- calagoapi/users/subuser.py +127 -0
- calagoapi/users/user.py +267 -0
- calagoapi/utils/__init__.py +43 -0
- calagoapi/utils/convert.py +113 -0
- calagoapi/utils/exceptions.py +72 -0
- calagoapi/utils/model.py +142 -0
- calagoapi/utils/resource.py +100 -0
- calagoapi/utils/time.py +68 -0
- calagoapi-1.0.dev1.dist-info/METADATA +266 -0
- calagoapi-1.0.dev1.dist-info/RECORD +71 -0
- calagoapi-1.0.dev1.dist-info/WHEEL +5 -0
- calagoapi-1.0.dev1.dist-info/licenses/LICENSE +674 -0
- calagoapi-1.0.dev1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from calagoapi.utils.model import Model, idOf
|
|
4
|
+
from calagoapi.utils.convert import formatBytes
|
|
5
|
+
from calagoapi.databases.explorer import DatabaseExplorer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Database(Model):
|
|
9
|
+
'''
|
|
10
|
+
A database on a database host, attached to a server.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
server: The owning server.
|
|
14
|
+
data: Raw database data from Calagopus.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
db = server.createDatabase('stats')
|
|
18
|
+
print(db.getHost(), db.getPort(), db.getUsername(), db.getPassword())
|
|
19
|
+
print(db.getConnectionString())
|
|
20
|
+
db.explorer.query('SELECT 1')
|
|
21
|
+
'''
|
|
22
|
+
def __init__(self, server, data: dict):
|
|
23
|
+
super().__init__(server.panel, data)
|
|
24
|
+
self.server = server
|
|
25
|
+
self.explorer = DatabaseExplorer(self.panel, self.panel.api.databases.databasePath(server.server_id, self.getUUID()))
|
|
26
|
+
|
|
27
|
+
# getters
|
|
28
|
+
def getName(self) -> str:
|
|
29
|
+
'''Returns the database name.'''
|
|
30
|
+
return self.data.get('name')
|
|
31
|
+
|
|
32
|
+
def getType(self) -> str:
|
|
33
|
+
'''Returns the database type: mysql, postgres or mongodb.'''
|
|
34
|
+
return self.data.get('type')
|
|
35
|
+
|
|
36
|
+
def getHost(self) -> str:
|
|
37
|
+
'''Returns the hostname to connect to.'''
|
|
38
|
+
return self.data.get('host')
|
|
39
|
+
|
|
40
|
+
def getPort(self) -> int:
|
|
41
|
+
'''Returns the port to connect to.'''
|
|
42
|
+
return self.data.get('port')
|
|
43
|
+
|
|
44
|
+
def getAddress(self) -> str:
|
|
45
|
+
'''Returns "host:port".'''
|
|
46
|
+
return f'{self.getHost()}:{self.getPort()}'
|
|
47
|
+
|
|
48
|
+
def getUsername(self) -> str:
|
|
49
|
+
'''Returns the database username.'''
|
|
50
|
+
return self.data.get('username')
|
|
51
|
+
|
|
52
|
+
def getPassword(self) -> str:
|
|
53
|
+
'''
|
|
54
|
+
Returns the password.
|
|
55
|
+
|
|
56
|
+
Only available when the database was fetched with include_password=True,
|
|
57
|
+
right after creation, or after rotatePassword().
|
|
58
|
+
'''
|
|
59
|
+
return self.data.get('password')
|
|
60
|
+
|
|
61
|
+
def getConnectionString(self) -> str:
|
|
62
|
+
'''
|
|
63
|
+
Returns a connection URI like "mysql://user:pass@host:3306/name".
|
|
64
|
+
|
|
65
|
+
The password is omitted if it is not known.
|
|
66
|
+
'''
|
|
67
|
+
scheme = {'postgres': 'postgresql'}.get(self.getType(), self.getType())
|
|
68
|
+
auth = self.getUsername()
|
|
69
|
+
if self.getPassword():
|
|
70
|
+
auth = f'{auth}:{self.getPassword()}'
|
|
71
|
+
return f'{scheme}://{auth}@{self.getAddress()}/{self.getName()}'
|
|
72
|
+
|
|
73
|
+
def getSize(self, formatted: bool = False):
|
|
74
|
+
'''Returns the current database size in bytes (or formatted with formatted=True).'''
|
|
75
|
+
size = self.panel.api.databases.size(self.server.server_id, self.getUUID())
|
|
76
|
+
return formatBytes(size) if formatted else size
|
|
77
|
+
|
|
78
|
+
def isLocked(self) -> bool:
|
|
79
|
+
'''Returns True if the database is locked against deletion.'''
|
|
80
|
+
return bool(self.data.get('is_locked'))
|
|
81
|
+
|
|
82
|
+
# actions
|
|
83
|
+
def refresh(self, include_password: bool = False) -> 'Database':
|
|
84
|
+
'''Re-fetches the database from the panel.'''
|
|
85
|
+
self.data = self.panel.api.databases.get(self.server.server_id, self.getUUID(), include_password or None)
|
|
86
|
+
return self
|
|
87
|
+
|
|
88
|
+
def rotatePassword(self) -> str:
|
|
89
|
+
'''
|
|
90
|
+
Generates a new password.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
The new password.
|
|
94
|
+
'''
|
|
95
|
+
password = self.panel.api.databases.rotatePassword(self.server.server_id, self.getUUID())
|
|
96
|
+
self.data['password'] = password
|
|
97
|
+
return password
|
|
98
|
+
|
|
99
|
+
def lock(self) -> 'Database':
|
|
100
|
+
'''Locks the database against deletion.'''
|
|
101
|
+
self.panel.api.databases.update(self.server.server_id, self.getUUID(), locked=True)
|
|
102
|
+
return self._update({'is_locked': True})
|
|
103
|
+
|
|
104
|
+
def unlock(self) -> 'Database':
|
|
105
|
+
'''Unlocks the database.'''
|
|
106
|
+
self.panel.api.databases.update(self.server.server_id, self.getUUID(), locked=False)
|
|
107
|
+
return self._update({'is_locked': False})
|
|
108
|
+
|
|
109
|
+
def recreate(self) -> None:
|
|
110
|
+
'''Drops and recreates the database, deleting all of its data.'''
|
|
111
|
+
return self.panel.api.databases.recreate(self.server.server_id, self.getUUID())
|
|
112
|
+
|
|
113
|
+
def delete(self) -> None:
|
|
114
|
+
'''Deletes the database.'''
|
|
115
|
+
return self.panel.api.databases.delete(self.server.server_id, self.getUUID())
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class DatabaseInstance(Model):
|
|
119
|
+
'''
|
|
120
|
+
A dedicated database server (container) owned by a server.
|
|
121
|
+
|
|
122
|
+
Database instances run their own engine (PostgreSQL, MariaDB, MongoDB or Redis)
|
|
123
|
+
and can hold several databases and users.
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
instance = server.databases.createInstance(template, 'main-db')
|
|
127
|
+
instance.start()
|
|
128
|
+
db = instance.createDatabase('players')
|
|
129
|
+
user = instance.createUser('app', databases={db: 'read_write'})
|
|
130
|
+
'''
|
|
131
|
+
def __init__(self, server, data: dict):
|
|
132
|
+
super().__init__(server.panel, data.get('instance', data))
|
|
133
|
+
self.server = server
|
|
134
|
+
self.live = data.get('live')
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def instance_id(self):
|
|
138
|
+
return self.getUUID()
|
|
139
|
+
|
|
140
|
+
def _api(self):
|
|
141
|
+
return self.panel.api.databases
|
|
142
|
+
|
|
143
|
+
# getters
|
|
144
|
+
def getName(self) -> str:
|
|
145
|
+
'''Returns the instance name.'''
|
|
146
|
+
return self.data.get('name')
|
|
147
|
+
|
|
148
|
+
def getType(self) -> str:
|
|
149
|
+
'''Returns the engine type, e.g. postgres, mariadb, mongodb, redis.'''
|
|
150
|
+
return self.data.get('type')
|
|
151
|
+
|
|
152
|
+
def getHost(self) -> str:
|
|
153
|
+
'''Returns the hostname to connect to.'''
|
|
154
|
+
return self.data.get('host')
|
|
155
|
+
|
|
156
|
+
def getPort(self) -> int:
|
|
157
|
+
'''Returns the port to connect to.'''
|
|
158
|
+
return self.data.get('port')
|
|
159
|
+
|
|
160
|
+
def getStatus(self) -> str:
|
|
161
|
+
'''Returns the instance status as reported by the panel.'''
|
|
162
|
+
return self.data.get('status')
|
|
163
|
+
|
|
164
|
+
def getMemory(self) -> int:
|
|
165
|
+
'''Returns the memory limit in MiB.'''
|
|
166
|
+
return self.data.get('memory')
|
|
167
|
+
|
|
168
|
+
def getDisk(self) -> int:
|
|
169
|
+
'''Returns the disk limit in MiB.'''
|
|
170
|
+
return self.data.get('disk')
|
|
171
|
+
|
|
172
|
+
def getCPU(self) -> int:
|
|
173
|
+
'''Returns the CPU limit in percent.'''
|
|
174
|
+
return self.data.get('cpu')
|
|
175
|
+
|
|
176
|
+
def getLiveData(self) -> dict:
|
|
177
|
+
'''Returns live state information, if it was included when fetching.'''
|
|
178
|
+
return self.live
|
|
179
|
+
|
|
180
|
+
def isLocked(self) -> bool:
|
|
181
|
+
'''Returns True if the instance is locked against deletion.'''
|
|
182
|
+
return bool(self.data.get('is_locked'))
|
|
183
|
+
|
|
184
|
+
def hasUpdate(self) -> bool:
|
|
185
|
+
'''Returns True if a newer image is available, see upgrade().'''
|
|
186
|
+
return bool(self.data.get('update_available'))
|
|
187
|
+
|
|
188
|
+
# actions
|
|
189
|
+
def refresh(self) -> 'DatabaseInstance':
|
|
190
|
+
'''Re-fetches the instance from the panel.'''
|
|
191
|
+
result = self._api().instance(self.server.server_id, self.instance_id)
|
|
192
|
+
self.data = result['instance']
|
|
193
|
+
self.live = result.get('live')
|
|
194
|
+
return self
|
|
195
|
+
|
|
196
|
+
def powerAction(self, action: str) -> None:
|
|
197
|
+
'''Sends a power action: start, stop, restart or kill.'''
|
|
198
|
+
return self._api().instancePower(self.server.server_id, self.instance_id, action)
|
|
199
|
+
|
|
200
|
+
def start(self) -> None:
|
|
201
|
+
'''Starts the instance.'''
|
|
202
|
+
return self.powerAction('start')
|
|
203
|
+
|
|
204
|
+
def stop(self) -> None:
|
|
205
|
+
'''Stops the instance.'''
|
|
206
|
+
return self.powerAction('stop')
|
|
207
|
+
|
|
208
|
+
def restart(self) -> None:
|
|
209
|
+
'''Restarts the instance.'''
|
|
210
|
+
return self.powerAction('restart')
|
|
211
|
+
|
|
212
|
+
def kill(self) -> None:
|
|
213
|
+
'''Kills the instance.'''
|
|
214
|
+
return self.powerAction('kill')
|
|
215
|
+
|
|
216
|
+
def getResources(self) -> dict:
|
|
217
|
+
'''Returns live resource usage of the instance.'''
|
|
218
|
+
return self._api().instanceResources(self.server.server_id, self.instance_id)
|
|
219
|
+
|
|
220
|
+
def getLogs(self, lines: int = None) -> str:
|
|
221
|
+
'''Returns the instance console log.'''
|
|
222
|
+
return self._api().instanceLogs(self.server.server_id, self.instance_id, lines)
|
|
223
|
+
|
|
224
|
+
def rename(self, name: str) -> 'DatabaseInstance':
|
|
225
|
+
'''Renames the instance.'''
|
|
226
|
+
self._api().updateInstance(self.server.server_id, self.instance_id, name=name)
|
|
227
|
+
return self._update({'name': name})
|
|
228
|
+
|
|
229
|
+
def lock(self) -> 'DatabaseInstance':
|
|
230
|
+
'''Locks the instance against deletion.'''
|
|
231
|
+
self._api().updateInstance(self.server.server_id, self.instance_id, locked=True)
|
|
232
|
+
return self._update({'is_locked': True})
|
|
233
|
+
|
|
234
|
+
def unlock(self) -> 'DatabaseInstance':
|
|
235
|
+
'''Unlocks the instance.'''
|
|
236
|
+
self._api().updateInstance(self.server.server_id, self.instance_id, locked=False)
|
|
237
|
+
return self._update({'is_locked': False})
|
|
238
|
+
|
|
239
|
+
def upgrade(self) -> 'DatabaseInstance':
|
|
240
|
+
'''Updates the instance to the latest image.'''
|
|
241
|
+
self.data = self._api().upgradeInstance(self.server.server_id, self.instance_id)
|
|
242
|
+
return self
|
|
243
|
+
|
|
244
|
+
def export(self, destination: str = None):
|
|
245
|
+
'''
|
|
246
|
+
Exports a dump of the whole instance.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
destination: Optional local file to write the dump to.
|
|
250
|
+
'''
|
|
251
|
+
dump = self._api().exportInstance(self.server.server_id, self.instance_id)
|
|
252
|
+
return _save(dump, destination)
|
|
253
|
+
|
|
254
|
+
def importDump(self, dump, wipe: bool = False) -> None:
|
|
255
|
+
'''
|
|
256
|
+
Imports a dump into the instance.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
dump: Local file path, bytes or str.
|
|
260
|
+
wipe: Delete existing data first.
|
|
261
|
+
'''
|
|
262
|
+
return self._api().importInstance(self.server.server_id, self.instance_id, _load(dump), wipe)
|
|
263
|
+
|
|
264
|
+
def cancelOperation(self, operation_id: str) -> None:
|
|
265
|
+
'''Cancels a running import/export operation.'''
|
|
266
|
+
return self._api().cancelInstanceOperation(self.server.server_id, self.instance_id, operation_id)
|
|
267
|
+
|
|
268
|
+
def delete(self, force: bool = None) -> None:
|
|
269
|
+
'''Deletes the instance and all its databases.'''
|
|
270
|
+
return self._api().deleteInstance(self.server.server_id, self.instance_id, force)
|
|
271
|
+
|
|
272
|
+
# databases
|
|
273
|
+
def getDatabases(self) -> list:
|
|
274
|
+
'''Returns the databases inside this instance.'''
|
|
275
|
+
return [InstanceDatabase(self, data) for data in self._api().instanceDatabases(self.server.server_id, self.instance_id)]
|
|
276
|
+
|
|
277
|
+
def getDatabase(self, name_or_id: str) -> 'InstanceDatabase':
|
|
278
|
+
'''Returns a database by name or UUID, or None.'''
|
|
279
|
+
for database in self.getDatabases():
|
|
280
|
+
if name_or_id in (database.getUUID(), database.getName()):
|
|
281
|
+
return database
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
def createDatabase(self, name: str) -> 'InstanceDatabase':
|
|
285
|
+
'''Creates a database inside the instance.'''
|
|
286
|
+
return InstanceDatabase(self, self._api().createInstanceDatabase(self.server.server_id, self.instance_id, name))
|
|
287
|
+
|
|
288
|
+
# users
|
|
289
|
+
def getUsers(self) -> list:
|
|
290
|
+
'''Returns the database users of this instance.'''
|
|
291
|
+
return [InstanceUser(self, data) for data in self._api().instanceUsers(self.server.server_id, self.instance_id)]
|
|
292
|
+
|
|
293
|
+
def createUser(self, username: str, databases: dict = None) -> 'InstanceUser':
|
|
294
|
+
'''
|
|
295
|
+
Creates a database user.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
username: The username.
|
|
299
|
+
databases: Optional {database: permission} grants, permission is
|
|
300
|
+
'none', 'read_only' or 'read_write'.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
The new user. `user.getPassword()` holds the generated password.
|
|
304
|
+
|
|
305
|
+
Example:
|
|
306
|
+
user = instance.createUser('app', databases={db: 'read_write'})
|
|
307
|
+
'''
|
|
308
|
+
grants = _grants(databases)
|
|
309
|
+
return InstanceUser(self, self._api().createInstanceUser(self.server.server_id, self.instance_id, username, grants))
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class InstanceDatabase(Model):
|
|
313
|
+
'''
|
|
314
|
+
A database inside a DatabaseInstance.
|
|
315
|
+
|
|
316
|
+
Example:
|
|
317
|
+
db = instance.getDatabase('players')
|
|
318
|
+
db.explorer.getTables()
|
|
319
|
+
db.export('players.sql')
|
|
320
|
+
'''
|
|
321
|
+
def __init__(self, instance: DatabaseInstance, data: dict):
|
|
322
|
+
super().__init__(instance.panel, data)
|
|
323
|
+
self.instance = instance
|
|
324
|
+
self.server = instance.server
|
|
325
|
+
self.explorer = DatabaseExplorer(
|
|
326
|
+
self.panel,
|
|
327
|
+
self.panel.api.databases.instanceDatabasePath(self.server.server_id, instance.instance_id, self.getUUID())
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
def _args(self):
|
|
331
|
+
return self.server.server_id, self.instance.instance_id, self.getUUID()
|
|
332
|
+
|
|
333
|
+
def getName(self) -> str:
|
|
334
|
+
'''Returns the database name.'''
|
|
335
|
+
return self.data.get('name')
|
|
336
|
+
|
|
337
|
+
def getSize(self, formatted: bool = False):
|
|
338
|
+
'''Returns the current size in bytes (or formatted with formatted=True).'''
|
|
339
|
+
size = self.panel.api.databases.instanceDatabaseSize(*self._args())
|
|
340
|
+
return formatBytes(size) if formatted else size
|
|
341
|
+
|
|
342
|
+
def export(self, destination: str = None):
|
|
343
|
+
'''Exports a dump of this database, optionally saving it to `destination`.'''
|
|
344
|
+
return _save(self.panel.api.databases.exportInstanceDatabase(*self._args()), destination)
|
|
345
|
+
|
|
346
|
+
def importDump(self, dump, source_db: str = None, wipe: bool = False) -> None:
|
|
347
|
+
'''
|
|
348
|
+
Imports a dump into this database.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
dump: Local file path, bytes or str.
|
|
352
|
+
source_db: Name of the database inside the dump to import, if it contains several.
|
|
353
|
+
wipe: Delete existing data first.
|
|
354
|
+
'''
|
|
355
|
+
return self.panel.api.databases.importInstanceDatabase(*self._args(), _load(dump), source_db, wipe)
|
|
356
|
+
|
|
357
|
+
def importFromUrl(self, url: str, source_db: str = None, wipe: bool = False) -> dict:
|
|
358
|
+
'''Imports a dump from a remote URL. Returns the background operation.'''
|
|
359
|
+
return self.panel.api.databases.importInstanceDatabaseRemote(*self._args(), url, source_db, wipe)
|
|
360
|
+
|
|
361
|
+
def recreate(self) -> None:
|
|
362
|
+
'''Drops and recreates the database, deleting all of its data.'''
|
|
363
|
+
return self.panel.api.databases.recreateInstanceDatabase(*self._args())
|
|
364
|
+
|
|
365
|
+
def delete(self) -> None:
|
|
366
|
+
'''Deletes the database.'''
|
|
367
|
+
return self.panel.api.databases.deleteInstanceDatabase(*self._args())
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class InstanceUser(Model):
|
|
371
|
+
'''
|
|
372
|
+
A database user inside a DatabaseInstance.
|
|
373
|
+
|
|
374
|
+
Example:
|
|
375
|
+
user = instance.createUser('app')
|
|
376
|
+
user.setPermission(db, 'read_only')
|
|
377
|
+
new_password = user.rotatePassword()
|
|
378
|
+
'''
|
|
379
|
+
def __init__(self, instance: DatabaseInstance, data: dict):
|
|
380
|
+
super().__init__(instance.panel, data)
|
|
381
|
+
self.instance = instance
|
|
382
|
+
self.server = instance.server
|
|
383
|
+
|
|
384
|
+
def _args(self):
|
|
385
|
+
return self.server.server_id, self.instance.instance_id, self.getUUID()
|
|
386
|
+
|
|
387
|
+
def getUsername(self) -> str:
|
|
388
|
+
'''Returns the username.'''
|
|
389
|
+
return self.data.get('username')
|
|
390
|
+
|
|
391
|
+
def getPassword(self) -> str:
|
|
392
|
+
'''Returns the password, only known right after creation or rotation.'''
|
|
393
|
+
return self.data.get('password')
|
|
394
|
+
|
|
395
|
+
def getDatabases(self) -> list:
|
|
396
|
+
'''Returns the database grants of this user.'''
|
|
397
|
+
return self.data.get('databases', [])
|
|
398
|
+
|
|
399
|
+
def setDatabases(self, databases: dict) -> list:
|
|
400
|
+
'''
|
|
401
|
+
Replaces every grant of this user.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
databases: {database: permission}, permission is 'none', 'read_only' or 'read_write'.
|
|
405
|
+
'''
|
|
406
|
+
result = self.panel.api.databases.setInstanceUserDatabases(*self._args(), _grants(databases))
|
|
407
|
+
self.data['databases'] = result
|
|
408
|
+
return result
|
|
409
|
+
|
|
410
|
+
def setPermission(self, database, permission: str) -> dict:
|
|
411
|
+
'''Sets the permission on one database: 'none', 'read_only' or 'read_write'.'''
|
|
412
|
+
return self.panel.api.databases.setInstanceUserPermission(*self._args(), idOf(database), permission)
|
|
413
|
+
|
|
414
|
+
def rotatePassword(self) -> str:
|
|
415
|
+
'''Generates and returns a new password.'''
|
|
416
|
+
password = self.panel.api.databases.rotateInstanceUserPassword(*self._args())
|
|
417
|
+
self.data['password'] = password
|
|
418
|
+
return password
|
|
419
|
+
|
|
420
|
+
def delete(self) -> None:
|
|
421
|
+
'''Deletes the user.'''
|
|
422
|
+
return self.panel.api.databases.deleteInstanceUser(*self._args())
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
# helpers
|
|
426
|
+
def _grants(databases):
|
|
427
|
+
if not databases:
|
|
428
|
+
return None
|
|
429
|
+
return [{'database_uuid': idOf(db), 'permission': permission} for db, permission in databases.items()]
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _load(dump):
|
|
433
|
+
# a local file path, otherwise the dump contents themselves
|
|
434
|
+
if isinstance(dump, (str, os.PathLike)) and os.path.isfile(dump):
|
|
435
|
+
with open(dump, 'rb') as f:
|
|
436
|
+
return f.read()
|
|
437
|
+
return dump
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _save(dump, destination):
|
|
441
|
+
if destination is None:
|
|
442
|
+
return dump
|
|
443
|
+
with open(destination, 'w', encoding='utf-8') as f:
|
|
444
|
+
f.write(dump)
|
|
445
|
+
return destination
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
def _values(row: dict) -> list:
|
|
2
|
+
# {'name': 'Steve'} -> [{'column': 'name', 'value': 'Steve'}]
|
|
3
|
+
return [{'column': column, 'value': None if value is None else str(value)} for column, value in row.items()]
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DatabaseExplorer:
|
|
7
|
+
'''
|
|
8
|
+
Browse and edit a database's tables and rows, available as `database.explorer`.
|
|
9
|
+
|
|
10
|
+
Works the same for hosted databases and databases inside a database instance.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
panel: The Panel.
|
|
14
|
+
base: Internal API path of the database.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
explorer = server.databases.getDatabase('...').explorer
|
|
18
|
+
print(explorer.getTables())
|
|
19
|
+
rows = explorer.getRows('players', limit=10)
|
|
20
|
+
explorer.insertRow('players', {'name': 'Steve', 'score': 10})
|
|
21
|
+
explorer.query('SELECT COUNT(*) FROM players')
|
|
22
|
+
'''
|
|
23
|
+
def __init__(self, panel, base: str):
|
|
24
|
+
self.panel = panel
|
|
25
|
+
self.api = panel.api.databases
|
|
26
|
+
self.base = base
|
|
27
|
+
|
|
28
|
+
# schema
|
|
29
|
+
def getSchema(self) -> dict:
|
|
30
|
+
'''Returns every table with its columns. `truncated` is True for very large schemas.'''
|
|
31
|
+
return self.api.schema(self.base)
|
|
32
|
+
|
|
33
|
+
def getTables(self) -> list:
|
|
34
|
+
'''Returns the list of tables.'''
|
|
35
|
+
return self.getSchema().get('tables', [])
|
|
36
|
+
|
|
37
|
+
def getTableTypes(self) -> list:
|
|
38
|
+
'''Returns the column types this database supports, e.g. ['INT', 'VARCHAR', ...].'''
|
|
39
|
+
return self.api.tableTypes(self.base)
|
|
40
|
+
|
|
41
|
+
# querying
|
|
42
|
+
def query(self, sql: str, rows: int = None, read_only: bool = False) -> list:
|
|
43
|
+
'''
|
|
44
|
+
Runs a raw SQL query.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
sql: The query.
|
|
48
|
+
rows: Maximum rows to return.
|
|
49
|
+
read_only: Run inside a read-only transaction.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
List of result sets.
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
explorer.query('SELECT * FROM players WHERE score > 100', read_only=True)
|
|
56
|
+
'''
|
|
57
|
+
return self.api.query(self.base, sql, rows, read_only)
|
|
58
|
+
|
|
59
|
+
def getRows(self, table: str, limit: int = None, offset: int = None, order_by: str = None,
|
|
60
|
+
descending: bool = None, filters: list = None, schema: str = None) -> dict:
|
|
61
|
+
'''
|
|
62
|
+
Browses rows of a table.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
table: Table name.
|
|
66
|
+
limit: Maximum rows.
|
|
67
|
+
offset: Rows to skip.
|
|
68
|
+
order_by: Column to sort by.
|
|
69
|
+
descending: Sort descending.
|
|
70
|
+
filters: List of {'column': ..., 'operator': ..., 'value': ...}. Operators:
|
|
71
|
+
eq, ne, gt, gte, lt, lte, contains, starts_with, ends_with, is_null, not_null.
|
|
72
|
+
schema: Schema name (PostgreSQL).
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
explorer.getRows('players', filters=[{'column': 'name', 'operator': 'eq', 'value': 'Steve'}])
|
|
76
|
+
'''
|
|
77
|
+
return self.api.rows(self.base, table, schema, order_by, descending, limit, offset, filters)
|
|
78
|
+
|
|
79
|
+
# rows
|
|
80
|
+
def insertRow(self, table: str, values: dict, schema: str = None) -> int:
|
|
81
|
+
'''
|
|
82
|
+
Inserts a row.
|
|
83
|
+
|
|
84
|
+
Example:
|
|
85
|
+
explorer.insertRow('players', {'name': 'Steve', 'score': 10})
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Number of affected rows.
|
|
89
|
+
'''
|
|
90
|
+
return self.api.insertRows(self.base, table, [{'values': _values(values)}], schema)
|
|
91
|
+
|
|
92
|
+
def insertRows(self, table: str, rows: list, schema: str = None) -> int:
|
|
93
|
+
'''Inserts several rows, each given as a dict of column -> value.'''
|
|
94
|
+
return self.api.insertRows(self.base, table, [{'values': _values(row)} for row in rows], schema)
|
|
95
|
+
|
|
96
|
+
def updateRow(self, table: str, where: dict, values: dict, schema: str = None) -> int:
|
|
97
|
+
'''
|
|
98
|
+
Updates the row matching `where`.
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
explorer.updateRow('players', where={'id': 1}, values={'score': 20})
|
|
102
|
+
'''
|
|
103
|
+
return self.api.updateRows(self.base, table, [{'keys': _values(where), 'values': _values(values)}], schema)
|
|
104
|
+
|
|
105
|
+
def deleteRow(self, table: str, where: dict, schema: str = None) -> int:
|
|
106
|
+
'''
|
|
107
|
+
Deletes the row matching `where`.
|
|
108
|
+
|
|
109
|
+
Example:
|
|
110
|
+
explorer.deleteRow('players', where={'id': 1})
|
|
111
|
+
'''
|
|
112
|
+
return self.api.deleteRows(self.base, table, [{'keys': _values(where)}], schema)
|
|
113
|
+
|
|
114
|
+
def deleteRows(self, table: str, rows: list, schema: str = None) -> int:
|
|
115
|
+
'''Deletes several rows, each identified by a dict of key columns.'''
|
|
116
|
+
return self.api.deleteRows(self.base, table, [{'keys': _values(row)} for row in rows], schema)
|
|
117
|
+
|
|
118
|
+
# tables
|
|
119
|
+
def createTable(self, table: str, columns: list, schema: str = None) -> None:
|
|
120
|
+
'''
|
|
121
|
+
Creates a table.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
columns: List of {'name', 'type', 'nullable', 'primary_key', 'auto_increment'} dicts.
|
|
125
|
+
|
|
126
|
+
Example:
|
|
127
|
+
explorer.createTable('players', [
|
|
128
|
+
{'name': 'id', 'type': 'INT', 'primary_key': True, 'auto_increment': True},
|
|
129
|
+
{'name': 'name', 'type': 'VARCHAR(32)'},
|
|
130
|
+
])
|
|
131
|
+
'''
|
|
132
|
+
return self.api.createTable(self.base, table, columns, schema)
|
|
133
|
+
|
|
134
|
+
def renameTable(self, table: str, name: str, schema: str = None) -> None:
|
|
135
|
+
'''Renames a table.'''
|
|
136
|
+
return self.api.renameTable(self.base, table, name, schema)
|
|
137
|
+
|
|
138
|
+
def deleteTable(self, table: str, schema: str = None) -> None:
|
|
139
|
+
'''Drops a table.'''
|
|
140
|
+
return self.api.deleteTable(self.base, table, schema)
|
|
141
|
+
|
|
142
|
+
def addColumn(self, table: str, name: str, type: str, nullable: bool = True, primary_key: bool = False,
|
|
143
|
+
auto_increment: bool = False, schema: str = None) -> None:
|
|
144
|
+
'''
|
|
145
|
+
Adds a column to a table.
|
|
146
|
+
|
|
147
|
+
Example:
|
|
148
|
+
explorer.addColumn('players', 'last_seen', 'TIMESTAMP')
|
|
149
|
+
'''
|
|
150
|
+
column = {'name': name, 'type': type, 'nullable': nullable, 'primary_key': primary_key, 'auto_increment': auto_increment}
|
|
151
|
+
return self.api.addColumn(self.base, table, column, schema)
|
|
152
|
+
|
|
153
|
+
def renameColumn(self, table: str, column: str, name: str, schema: str = None) -> None:
|
|
154
|
+
'''Renames a column.'''
|
|
155
|
+
return self.api.renameColumn(self.base, table, column, name, schema)
|
|
156
|
+
|
|
157
|
+
def deleteColumn(self, table: str, column: str, schema: str = None) -> None:
|
|
158
|
+
'''Drops a column.'''
|
|
159
|
+
return self.api.deleteColumn(self.base, table, column, schema)
|