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,9 @@
1
+ from .console import AsyncConsole
2
+ from .panel import AsyncPanel, AsyncServer, AsyncWrapper
3
+
4
+ __all__ = [
5
+ 'AsyncPanel',
6
+ 'AsyncServer',
7
+ 'AsyncConsole',
8
+ 'AsyncWrapper',
9
+ ]
@@ -0,0 +1,289 @@
1
+ import asyncio
2
+ import inspect
3
+ import time
4
+
5
+ from websockets.asyncio.client import connect
6
+ from websockets.exceptions import ConnectionClosed, WebSocketException
7
+
8
+ from calagoapi.console.console import (
9
+ ConsoleEvent,
10
+ socketUrl,
11
+ sslContext,
12
+ encode,
13
+ decode,
14
+ logger,
15
+ POWER_ACTIONS,
16
+ TOKEN_EVENTS,
17
+ ERROR_EVENTS,
18
+ )
19
+ from calagoapi.utils.exceptions import CalagoAPIError, ConsoleError, InvalidActionError, WaitTimeoutError
20
+
21
+
22
+ class AsyncConsole:
23
+ '''
24
+ The live console of a server for asyncio code, available as `server.console`
25
+ on servers from AsyncPanel.
26
+
27
+ Same methods as Console, but awaitable. Callbacks may be plain functions
28
+ or coroutines.
29
+
30
+ Example:
31
+ async with server.console as console:
32
+ await console.sendCommand('list')
33
+ async for line in console.lines():
34
+ print(line)
35
+
36
+ server.console.onConsoleLine(print)
37
+ await server.console.listen()
38
+ ...
39
+ await server.console.stop()
40
+
41
+ Args:
42
+ server: The (regular) GenericServer whose console to open.
43
+ '''
44
+ def __init__(self, server):
45
+ self.server = server
46
+ self.socket = None
47
+ self.permissions = []
48
+ self.handlers = {}
49
+ self.running = False
50
+ self.task = None
51
+
52
+ def __repr__(self):
53
+ state = 'connected' if self.isConnected() else 'closed'
54
+ return f'<AsyncConsole {self.server.name!r} {state}>'
55
+
56
+ async def __aenter__(self):
57
+ return await self.connect()
58
+
59
+ async def __aexit__(self, *exc):
60
+ await self.stop()
61
+
62
+ # connection
63
+ async def connect(self, timeout: int = 10) -> 'AsyncConsole':
64
+ '''
65
+ Opens the websocket and authenticates. Does nothing if already connected.
66
+
67
+ Raises:
68
+ ConsoleError: The daemon could not be reached or refused the token.
69
+ '''
70
+ if self.socket:
71
+ return self
72
+ credentials = await asyncio.to_thread(self.server.getWebsocket)
73
+ url = socketUrl(credentials['url'])
74
+ try:
75
+ self.socket = await connect(
76
+ url,
77
+ origin=self.server.panel.url,
78
+ ssl=sslContext(url, self.server.panel.client.session.verify),
79
+ open_timeout=timeout,
80
+ user_agent_header='CalagoAPI',
81
+ )
82
+ except (OSError, WebSocketException, asyncio.TimeoutError) as e:
83
+ raise ConsoleError(f'Could not connect to the console of {self.server.name}: {e}') from e
84
+ self.server.panel.consoles.add(self)
85
+
86
+ await self._send('auth', credentials['token'])
87
+ deadline = time.monotonic() + timeout
88
+ while True:
89
+ event = await self._receive(max(deadline - time.monotonic(), 0.01))
90
+ if event is None:
91
+ await self.close()
92
+ raise ConsoleError(f'The console of {self.server.name} did not confirm authentication')
93
+ if event.getEvent() == 'auth success':
94
+ self.permissions = event.getArgs()
95
+ return self
96
+ if event.getEvent() in ERROR_EVENTS:
97
+ await self.close()
98
+ raise ConsoleError(f'Console authentication failed: {event.getArg()}')
99
+
100
+ async def close(self) -> None:
101
+ '''Closes the websocket. Use stop() to also end a running listen().'''
102
+ socket, self.socket = self.socket, None
103
+ self.server.panel.consoles.discard(self)
104
+ if socket:
105
+ await socket.close()
106
+
107
+ def isConnected(self) -> bool:
108
+ '''Returns True while the websocket is open.'''
109
+ return self.socket is not None
110
+
111
+ def getPermissions(self) -> list:
112
+ '''Returns the console permissions the daemon granted.'''
113
+ return self.permissions
114
+
115
+ # sending
116
+ async def _send(self, event, *args):
117
+ if not self.socket:
118
+ raise ConsoleError('The console is not connected, call connect() first')
119
+ try:
120
+ await self.socket.send(encode(event, *args))
121
+ except ConnectionClosed as e:
122
+ self.socket = None
123
+ raise ConsoleError(f'The console connection was closed: {e}') from e
124
+
125
+ async def sendCommand(self, command: str) -> None:
126
+ '''Sends a console command over the websocket.'''
127
+ await self._send('send command', command)
128
+
129
+ async def setState(self, action: str) -> None:
130
+ '''Sends a power action: start, restart, stop or kill.'''
131
+ if action not in POWER_ACTIONS:
132
+ raise InvalidActionError(f'Invalid power action "{action}", expected one of: {", ".join(POWER_ACTIONS)}')
133
+ await self._send('set state', action)
134
+
135
+ async def requestLogs(self) -> None:
136
+ '''Asks the daemon to resend recent console lines.'''
137
+ await self._send('send logs')
138
+
139
+ async def requestStats(self) -> None:
140
+ '''Asks the daemon for resource usage now.'''
141
+ await self._send('send stats')
142
+
143
+ async def requestStatus(self) -> None:
144
+ '''Asks the daemon for the power state now.'''
145
+ await self._send('send status')
146
+
147
+ # receiving
148
+ async def _receive(self, timeout=None):
149
+ if not self.socket:
150
+ return None
151
+ try:
152
+ raw = await asyncio.wait_for(self.socket.recv(), timeout)
153
+ except asyncio.TimeoutError:
154
+ return None
155
+ except ConnectionClosed:
156
+ await self.close()
157
+ return None
158
+ return decode(raw) or ConsoleEvent('unknown')
159
+
160
+ async def _renewToken(self):
161
+ credentials = await asyncio.to_thread(self.server.getWebsocket)
162
+ await self._send('auth', credentials['token'])
163
+
164
+ async def events(self, timeout: float = None):
165
+ '''
166
+ Yields every event from the daemon until the connection closes.
167
+
168
+ Args:
169
+ timeout: Stop when no event arrives for this many seconds. None waits forever.
170
+ '''
171
+ while self.socket:
172
+ event = await self._receive(timeout)
173
+ if event is None:
174
+ return
175
+ if event.getEvent() in TOKEN_EVENTS:
176
+ await self._renewToken()
177
+ continue
178
+ yield event
179
+
180
+ async def lines(self, history: bool = True, timeout: float = None):
181
+ '''
182
+ Yields console lines as they are printed.
183
+
184
+ Example:
185
+ async for line in console.lines(history=False):
186
+ print(line)
187
+ '''
188
+ await self.connect()
189
+ if history:
190
+ await self.requestLogs()
191
+ async for event in self.events(timeout):
192
+ if event.getEvent() == 'console output':
193
+ yield event.getArg(0, '')
194
+
195
+ async def waitFor(self, text: str, timeout: float = 60) -> str:
196
+ '''
197
+ Waits until a console line contains `text` and returns that line.
198
+
199
+ Raises:
200
+ WaitTimeoutError: No matching line within `timeout` seconds.
201
+ '''
202
+ await self.connect()
203
+ deadline = time.monotonic() + timeout
204
+ while True:
205
+ remaining = deadline - time.monotonic()
206
+ event = await self._receive(remaining) if remaining > 0 else None
207
+ if event is None:
208
+ raise WaitTimeoutError(f'No console line containing {text!r} within {timeout}s')
209
+ if event.getEvent() in TOKEN_EVENTS:
210
+ await self._renewToken()
211
+ elif event.getEvent() == 'console output' and text in event.getArg(0, ''):
212
+ return event.getArg()
213
+
214
+ # callbacks
215
+ def on(self, event: str, callback) -> 'AsyncConsole':
216
+ '''Calls `callback(ConsoleEvent)` for an event while listen() runs. Use '*' for every event.'''
217
+ self.handlers.setdefault(event, []).append(callback)
218
+ return self
219
+
220
+ def onConsoleLine(self, callback) -> 'AsyncConsole':
221
+ '''Calls `callback(line)` for every console line.'''
222
+ return self.on('console output', lambda event: callback(event.getArg(0, '')))
223
+
224
+ def onStatus(self, callback) -> 'AsyncConsole':
225
+ '''Calls `callback(state)` when the power state changes.'''
226
+ return self.on('status', lambda event: callback(event.getArg()))
227
+
228
+ def onStats(self, callback) -> 'AsyncConsole':
229
+ '''Calls `callback(resources)` with a resource usage dict.'''
230
+ return self.on('stats', lambda event: callback(event.getData()))
231
+
232
+ async def _dispatch(self, event):
233
+ for callback in self.handlers.get(event.getEvent(), []) + self.handlers.get('*', []):
234
+ try:
235
+ result = callback(event)
236
+ if inspect.isawaitable(result):
237
+ await result
238
+ except Exception:
239
+ logger.exception('console callback for %r failed', event.getEvent())
240
+
241
+ async def listen(self, background: bool = True, history: bool = False, reconnect: bool = True) -> 'AsyncConsole':
242
+ '''
243
+ Delivers events to the registered callbacks.
244
+
245
+ Args:
246
+ background: Run as an asyncio task and return immediately. False waits until stop().
247
+ history: Replay recent console lines first.
248
+ reconnect: Reconnect automatically (with backoff) if the connection drops.
249
+ '''
250
+ if self.running:
251
+ return self
252
+ await self.connect()
253
+ self.running = True
254
+ if background:
255
+ self.task = asyncio.create_task(self._loop(history, reconnect))
256
+ else:
257
+ await self._loop(history, reconnect)
258
+ return self
259
+
260
+ async def _loop(self, history, reconnect):
261
+ delay = 1
262
+ while self.running:
263
+ try:
264
+ await self.connect()
265
+ delay = 1
266
+ if history:
267
+ await self.requestLogs()
268
+ history = False
269
+ async for event in self.events():
270
+ await self._dispatch(event)
271
+ except CalagoAPIError as e:
272
+ logger.warning('console of %s disconnected: %s', self.server.name, e)
273
+ if not self.running or not reconnect:
274
+ break
275
+ await asyncio.sleep(delay)
276
+ delay = min(delay * 2, 30)
277
+ self.running = False
278
+
279
+ async def stop(self) -> None:
280
+ '''Stops listen() and closes the connection.'''
281
+ self.running = False
282
+ await self.close()
283
+ task, self.task = self.task, None
284
+ if task and task is not asyncio.current_task():
285
+ task.cancel()
286
+ try:
287
+ await task
288
+ except asyncio.CancelledError:
289
+ pass
calagoapi/aio/panel.py ADDED
@@ -0,0 +1,176 @@
1
+ import asyncio
2
+ import functools
3
+ import inspect
4
+
5
+ from calagoapi.panel import Panel
6
+ from calagoapi.console import Console
7
+ from calagoapi.servers.generic import GenericServer
8
+ from calagoapi.utils.model import Page
9
+ from calagoapi.aio.console import AsyncConsole
10
+
11
+
12
+ def wrap(value):
13
+ '''Wraps CalagoAPI objects returned by a call so their methods are awaitable too'''
14
+ if isinstance(value, Page):
15
+ return value.map(wrap)
16
+ if isinstance(value, list):
17
+ return [wrap(v) for v in value]
18
+ if isinstance(value, GenericServer):
19
+ return AsyncServer(value)
20
+ if isinstance(value, Console):
21
+ return AsyncConsole(value.server)
22
+ if type(value).__module__.startswith('calagoapi.') and not isinstance(value, BaseException):
23
+ return AsyncWrapper(value)
24
+ return value
25
+
26
+
27
+ def unwrap(value):
28
+ '''Turns wrapped objects passed as arguments back into the regular objects'''
29
+ if isinstance(value, AsyncWrapper):
30
+ return value.target
31
+ if isinstance(value, list):
32
+ return [unwrap(v) for v in value]
33
+ if isinstance(value, tuple):
34
+ return tuple(unwrap(v) for v in value)
35
+ if isinstance(value, dict):
36
+ return {unwrap(k): unwrap(v) for k, v in value.items()}
37
+ return value
38
+
39
+
40
+ class AsyncWrapper:
41
+ '''
42
+ The async version of any CalagoAPI object.
43
+
44
+ Every method of the wrapped object becomes a coroutine with the same name,
45
+ arguments and docstring. Calls run in a worker thread, so many can run at
46
+ once with asyncio.gather(). Plain attributes (like `server.name`) are read
47
+ directly. Objects returned by a method are wrapped as well.
48
+
49
+ Example:
50
+ backups = await server.backups.getBackups()
51
+ await backups[0].lock()
52
+ print(await backups[0].getName()) # getters are awaitable too
53
+ print(backups[0].data['name']) # plain attributes are not
54
+ '''
55
+ def __init__(self, target):
56
+ object.__setattr__(self, 'target', target)
57
+
58
+ def __getattr__(self, name):
59
+ value = getattr(self.target, name)
60
+ if not inspect.ismethod(value):
61
+ return wrap(value)
62
+
63
+ @functools.wraps(value)
64
+ async def method(*args, **kwargs):
65
+ args = [unwrap(a) for a in args]
66
+ kwargs = {k: unwrap(v) for k, v in kwargs.items()}
67
+ return wrap(await asyncio.to_thread(value, *args, **kwargs))
68
+
69
+ return method
70
+
71
+ def __setattr__(self, name, value):
72
+ setattr(self.target, name, value)
73
+
74
+ def __getitem__(self, key):
75
+ return self.target[key]
76
+
77
+ def __contains__(self, key):
78
+ return key in self.target
79
+
80
+ def __eq__(self, other):
81
+ return self.target == unwrap(other)
82
+
83
+ def __hash__(self):
84
+ return hash(self.target)
85
+
86
+ def __repr__(self):
87
+ return f'<async {self.target!r}>'
88
+
89
+ def __str__(self):
90
+ return str(self.target)
91
+
92
+ def __dir__(self):
93
+ return sorted(set(dir(self.target)) | set(super().__dir__()))
94
+
95
+
96
+ class AsyncServer(AsyncWrapper):
97
+ '''
98
+ The async version of GenericServer / MinecraftServer.
99
+
100
+ Every server method is awaitable, and `server.console` is an AsyncConsole.
101
+
102
+ Example:
103
+ server = await panel.servers.getServer('abc123')
104
+ await server.restart()
105
+ async for line in server.streamConsole():
106
+ print(line)
107
+ '''
108
+ def __init__(self, server):
109
+ super().__init__(server)
110
+ object.__setattr__(self, 'console', AsyncConsole(server))
111
+
112
+ async def streamConsole(self, history: bool = True, timeout: float = None):
113
+ '''
114
+ Yields console lines live, as they are printed.
115
+
116
+ Example:
117
+ async for line in server.streamConsole(history=False):
118
+ print(line)
119
+ '''
120
+ async with AsyncConsole(self.target) as console:
121
+ async for line in console.lines(history, timeout):
122
+ yield line
123
+
124
+ async def onConsoleLine(self, callback, history: bool = False) -> AsyncConsole:
125
+ '''
126
+ Calls `callback(line)` for every console line in a background task.
127
+ The callback may be a plain function or a coroutine.
128
+
129
+ Example:
130
+ await server.onConsoleLine(print)
131
+ '''
132
+ self.console.onConsoleLine(callback)
133
+ return await self.console.listen(history=history)
134
+
135
+
136
+ class AsyncPanel(AsyncWrapper):
137
+ '''
138
+ The async version of Panel, for asyncio applications such as Discord bots.
139
+
140
+ Takes the same arguments as Panel. Every method of every object is
141
+ awaitable and keeps its name, so the documentation of the regular API
142
+ applies unchanged, just add `await`.
143
+
144
+ Requests run in worker threads on top of the regular client, so slow
145
+ calls never block the event loop and independent calls run concurrently.
146
+ The live console uses a native asyncio websocket.
147
+
148
+ Example:
149
+ import asyncio
150
+ from calagoapi import AsyncPanel
151
+
152
+ async def main():
153
+ async with AsyncPanel(url='https://panel.example.com', api_key='...') as panel:
154
+ servers = await panel.servers.getServers()
155
+ statuses = await asyncio.gather(*(server.getStatus() for server in servers))
156
+ for server, status in zip(servers, statuses):
157
+ print(server.name, status)
158
+
159
+ asyncio.run(main())
160
+ '''
161
+ def __init__(self, url: str, api_key: str, name: str = None, timeout: int = 10, verify: bool = True,
162
+ max_retries: int = 3):
163
+ super().__init__(Panel(url, api_key, name, timeout, verify, max_retries))
164
+
165
+ async def __aenter__(self):
166
+ return self
167
+
168
+ async def __aexit__(self, *exc):
169
+ await self.close()
170
+
171
+ async def close(self) -> None:
172
+ '''Closes open consoles and the pooled HTTP connections.'''
173
+ for console in list(self.target.consoles):
174
+ if inspect.iscoroutinefunction(console.close):
175
+ await console.close()
176
+ await asyncio.to_thread(self.target.close)
@@ -0,0 +1,63 @@
1
+ from .client import Client
2
+ from .servers import ServersAPI
3
+ from .files import FilesAPI
4
+ from .backups import BackupsAPI
5
+ from .databases import DatabasesAPI
6
+ from .schedules import SchedulesAPI
7
+ from .network import NetworkAPI
8
+ from .users import UsersAPI
9
+ from .attachments import AttachmentsAPI
10
+ from .versions import VersionsAPI
11
+ from .account import AccountAPI
12
+ from .auth import AuthAPI
13
+ from .public import PublicAPI
14
+ from .admin import AdminAPI, ResourceAPI
15
+
16
+
17
+ class API:
18
+ '''
19
+ Bundle of every internal API module, available as `panel.api`.
20
+
21
+ These modules are the only place CalagoAPI talks HTTP. You normally
22
+ never need them, but they are handy for calls the high-level objects
23
+ do not wrap yet.
24
+
25
+ Example:
26
+ panel.api.servers.resources(server_id)
27
+ panel.api.client.get('/api/client/permissions')
28
+ '''
29
+ def __init__(self, client: Client):
30
+ self.client = client
31
+ self.servers = ServersAPI(client)
32
+ self.files = FilesAPI(client)
33
+ self.backups = BackupsAPI(client)
34
+ self.databases = DatabasesAPI(client)
35
+ self.schedules = SchedulesAPI(client)
36
+ self.network = NetworkAPI(client)
37
+ self.users = UsersAPI(client)
38
+ self.attachments = AttachmentsAPI(client)
39
+ self.versions = VersionsAPI(client)
40
+ self.account = AccountAPI(client)
41
+ self.auth = AuthAPI(client)
42
+ self.public = PublicAPI(client)
43
+ self.admin = AdminAPI(client)
44
+
45
+
46
+ __all__ = [
47
+ 'API',
48
+ 'Client',
49
+ 'ServersAPI',
50
+ 'FilesAPI',
51
+ 'BackupsAPI',
52
+ 'DatabasesAPI',
53
+ 'SchedulesAPI',
54
+ 'NetworkAPI',
55
+ 'UsersAPI',
56
+ 'AttachmentsAPI',
57
+ 'VersionsAPI',
58
+ 'AccountAPI',
59
+ 'AuthAPI',
60
+ 'PublicAPI',
61
+ 'AdminAPI',
62
+ 'ResourceAPI',
63
+ ]