server4agent 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- server4agent/__init__.py +79 -0
- server4agent/_async/__init__.py +0 -0
- server4agent/_async/client.py +559 -0
- server4agent/_async/transport.py +76 -0
- server4agent/_models.py +197 -0
- server4agent/_sync/__init__.py +0 -0
- server4agent/_sync/client.py +567 -0
- server4agent/_sync/transport.py +75 -0
- server4agent/_transport.py +115 -0
- server4agent/_version.py +1 -0
- server4agent/errors.py +103 -0
- server4agent/py.typed +0 -0
- server4agent/webhooks.py +59 -0
- server4agent-0.1.0.dist-info/METADATA +151 -0
- server4agent-0.1.0.dist-info/RECORD +17 -0
- server4agent-0.1.0.dist-info/WHEEL +4 -0
- server4agent-0.1.0.dist-info/licenses/LICENSE +21 -0
server4agent/__init__.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from ._async.client import AsyncServer4Agent
|
|
2
|
+
from ._async.client import (
|
|
3
|
+
AsyncBuild,
|
|
4
|
+
AsyncProject,
|
|
5
|
+
AsyncServer,
|
|
6
|
+
AsyncTask,
|
|
7
|
+
)
|
|
8
|
+
from ._models import (
|
|
9
|
+
ApiKey,
|
|
10
|
+
BuildStep,
|
|
11
|
+
CreatedApiKey,
|
|
12
|
+
CreatedWebhook,
|
|
13
|
+
Deployment,
|
|
14
|
+
ExecResult,
|
|
15
|
+
ProjectTemplate,
|
|
16
|
+
ProjectTemplateDefaults,
|
|
17
|
+
WEBHOOK_EVENTS,
|
|
18
|
+
Webhook,
|
|
19
|
+
)
|
|
20
|
+
from ._sync.client import Build, Project, Server, Server4Agent, Task
|
|
21
|
+
from ._version import __version__
|
|
22
|
+
from .errors import (
|
|
23
|
+
APIConnectionError,
|
|
24
|
+
APIStatusError,
|
|
25
|
+
APITimeoutError,
|
|
26
|
+
AuthenticationError,
|
|
27
|
+
BadRequestError,
|
|
28
|
+
ConflictError,
|
|
29
|
+
InternalServerError,
|
|
30
|
+
NotFoundError,
|
|
31
|
+
PermissionDeniedError,
|
|
32
|
+
RateLimitError,
|
|
33
|
+
Server4AgentError,
|
|
34
|
+
UnprocessableEntityError,
|
|
35
|
+
)
|
|
36
|
+
from .webhooks import verify_webhook_signature
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
# clients
|
|
40
|
+
"Server4Agent",
|
|
41
|
+
"AsyncServer4Agent",
|
|
42
|
+
# sync handles
|
|
43
|
+
"Server",
|
|
44
|
+
"Project",
|
|
45
|
+
"Task",
|
|
46
|
+
"Build",
|
|
47
|
+
# async handles
|
|
48
|
+
"AsyncServer",
|
|
49
|
+
"AsyncProject",
|
|
50
|
+
"AsyncTask",
|
|
51
|
+
"AsyncBuild",
|
|
52
|
+
# data types
|
|
53
|
+
"ApiKey",
|
|
54
|
+
"CreatedApiKey",
|
|
55
|
+
"Webhook",
|
|
56
|
+
"CreatedWebhook",
|
|
57
|
+
"Deployment",
|
|
58
|
+
"ExecResult",
|
|
59
|
+
"BuildStep",
|
|
60
|
+
"ProjectTemplate",
|
|
61
|
+
"ProjectTemplateDefaults",
|
|
62
|
+
"WEBHOOK_EVENTS",
|
|
63
|
+
# errors
|
|
64
|
+
"Server4AgentError",
|
|
65
|
+
"APIConnectionError",
|
|
66
|
+
"APITimeoutError",
|
|
67
|
+
"APIStatusError",
|
|
68
|
+
"BadRequestError",
|
|
69
|
+
"AuthenticationError",
|
|
70
|
+
"PermissionDeniedError",
|
|
71
|
+
"NotFoundError",
|
|
72
|
+
"ConflictError",
|
|
73
|
+
"UnprocessableEntityError",
|
|
74
|
+
"RateLimitError",
|
|
75
|
+
"InternalServerError",
|
|
76
|
+
# webhooks
|
|
77
|
+
"verify_webhook_signature",
|
|
78
|
+
"__version__",
|
|
79
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
from typing import Awaitable, Callable, List, Optional, Sequence, Union
|
|
7
|
+
|
|
8
|
+
from .. import _transport as _t
|
|
9
|
+
from .._models import (
|
|
10
|
+
ApiKey,
|
|
11
|
+
BUILD_TERMINAL_STATES,
|
|
12
|
+
CreatedApiKey,
|
|
13
|
+
CreatedWebhook,
|
|
14
|
+
Deployment,
|
|
15
|
+
ProjectTemplate,
|
|
16
|
+
ServerAction,
|
|
17
|
+
ServerTier,
|
|
18
|
+
TASK_TERMINAL_STATES,
|
|
19
|
+
Webhook,
|
|
20
|
+
_Record,
|
|
21
|
+
parse_build,
|
|
22
|
+
parse_project,
|
|
23
|
+
parse_server,
|
|
24
|
+
parse_task,
|
|
25
|
+
)
|
|
26
|
+
from .._transport import quote_segment as _seg
|
|
27
|
+
from ..errors import APITimeoutError
|
|
28
|
+
from .transport import AsyncTransport
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# =============================================================================
|
|
32
|
+
# Handles
|
|
33
|
+
# =============================================================================
|
|
34
|
+
|
|
35
|
+
class AsyncServer(_Record):
|
|
36
|
+
id: str
|
|
37
|
+
task: str
|
|
38
|
+
tier: ServerTier
|
|
39
|
+
status: str
|
|
40
|
+
region: str
|
|
41
|
+
credits_used: float
|
|
42
|
+
storage_used_gb: float
|
|
43
|
+
public_url: Optional[str]
|
|
44
|
+
created_at: Optional[str]
|
|
45
|
+
|
|
46
|
+
def __init__(self, transport: AsyncTransport, raw: dict) -> None:
|
|
47
|
+
self._t = transport
|
|
48
|
+
self.__dict__.update(parse_server(raw))
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def projects(self) -> "_ScopedProjects":
|
|
52
|
+
return _ScopedProjects(self._t, self.id)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def tasks(self) -> "_ScopedTasks":
|
|
56
|
+
return _ScopedTasks(self._t, self.id)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def builds(self) -> "_ScopedBuilds":
|
|
60
|
+
return _ScopedBuilds(self._t, self.id)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def files(self) -> "_ScopedFiles":
|
|
64
|
+
return _ScopedFiles(self._t, self.id)
|
|
65
|
+
|
|
66
|
+
async def refresh(self) -> "AsyncServer":
|
|
67
|
+
r = await self._t.request("GET", f"/servers/{_seg(self.id)}")
|
|
68
|
+
self.__dict__.update(parse_server(r["server"]))
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
async def update(self, *, task: Optional[str] = None, region: Optional[str] = None) -> "AsyncServer":
|
|
72
|
+
body = {k: v for k, v in {"task": task, "region": region}.items() if v is not None}
|
|
73
|
+
r = await self._t.request("PATCH", f"/servers/{_seg(self.id)}", body=body)
|
|
74
|
+
self.__dict__.update(parse_server(r["server"]))
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
async def delete(self) -> None:
|
|
78
|
+
await self._t.request("DELETE", f"/servers/{_seg(self.id)}")
|
|
79
|
+
|
|
80
|
+
async def control(self, action: ServerAction) -> dict:
|
|
81
|
+
return await self._t.request("POST", f"/servers/{_seg(self.id)}/actions", body={"action": action})
|
|
82
|
+
|
|
83
|
+
async def start(self) -> dict:
|
|
84
|
+
return await self.control("start")
|
|
85
|
+
|
|
86
|
+
async def stop(self) -> dict:
|
|
87
|
+
return await self.control("stop")
|
|
88
|
+
|
|
89
|
+
async def restart(self) -> dict:
|
|
90
|
+
return await self.control("restart")
|
|
91
|
+
|
|
92
|
+
async def exec(self, command: str) -> str:
|
|
93
|
+
r = await self._t.request("POST", f"/servers/{_seg(self.id)}/exec", body={"command": command})
|
|
94
|
+
return r["stdout"]
|
|
95
|
+
|
|
96
|
+
async def deploy(self) -> str:
|
|
97
|
+
r = await self._t.request("POST", f"/servers/{_seg(self.id)}/deployments")
|
|
98
|
+
return r["url"]
|
|
99
|
+
|
|
100
|
+
async def deployments(self) -> List[Deployment]:
|
|
101
|
+
r = await self._t.request("GET", f"/servers/{_seg(self.id)}/deployments")
|
|
102
|
+
return [Deployment(**d) for d in r["deployments"]]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class AsyncProject(_Record):
|
|
106
|
+
id: str
|
|
107
|
+
server_id: str
|
|
108
|
+
name: str
|
|
109
|
+
slug: str
|
|
110
|
+
description: Optional[str]
|
|
111
|
+
visibility: str
|
|
112
|
+
lifecycle: str
|
|
113
|
+
runtime_status: str
|
|
114
|
+
template: Optional[str]
|
|
115
|
+
url: Optional[str]
|
|
116
|
+
created_at: str
|
|
117
|
+
updated_at: str
|
|
118
|
+
|
|
119
|
+
def __init__(self, transport: AsyncTransport, raw: dict) -> None:
|
|
120
|
+
self._t = transport
|
|
121
|
+
self.__dict__.update(parse_project(raw))
|
|
122
|
+
|
|
123
|
+
async def refresh(self) -> "AsyncProject":
|
|
124
|
+
r = await self._t.request("GET", f"/projects/{_seg(self.id)}")
|
|
125
|
+
self.__dict__.update(parse_project(r))
|
|
126
|
+
return self
|
|
127
|
+
|
|
128
|
+
async def update(
|
|
129
|
+
self,
|
|
130
|
+
*,
|
|
131
|
+
name: Optional[str] = None,
|
|
132
|
+
description: Optional[str] = None,
|
|
133
|
+
visibility: Optional[str] = None,
|
|
134
|
+
lifecycle: Optional[str] = None,
|
|
135
|
+
) -> "AsyncProject":
|
|
136
|
+
body = {
|
|
137
|
+
k: v
|
|
138
|
+
for k, v in {
|
|
139
|
+
"name": name,
|
|
140
|
+
"description": description,
|
|
141
|
+
"visibility": visibility,
|
|
142
|
+
"lifecycle": lifecycle,
|
|
143
|
+
}.items()
|
|
144
|
+
if v is not None
|
|
145
|
+
}
|
|
146
|
+
r = await self._t.request("PATCH", f"/projects/{_seg(self.id)}", body=body)
|
|
147
|
+
self.__dict__.update(parse_project(r))
|
|
148
|
+
return self
|
|
149
|
+
|
|
150
|
+
async def delete(self) -> None:
|
|
151
|
+
await self._t.request("DELETE", f"/projects/{_seg(self.id)}")
|
|
152
|
+
|
|
153
|
+
async def cleanup(self) -> None:
|
|
154
|
+
await self._t.request("POST", f"/projects/{_seg(self.id)}/cleanup")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class AsyncTask(_Record):
|
|
158
|
+
id: str
|
|
159
|
+
server_id: str
|
|
160
|
+
goal: str
|
|
161
|
+
status: str
|
|
162
|
+
created_at: str
|
|
163
|
+
result: Optional[str]
|
|
164
|
+
question: Optional[str]
|
|
165
|
+
|
|
166
|
+
def __init__(self, transport: AsyncTransport, raw: dict) -> None:
|
|
167
|
+
self._t = transport
|
|
168
|
+
self.__dict__.update(parse_task(raw))
|
|
169
|
+
|
|
170
|
+
async def refresh(self) -> "AsyncTask":
|
|
171
|
+
r = await self._t.request("GET", f"/servers/{_seg(self.server_id)}/tasks/{_seg(self.id)}")
|
|
172
|
+
self.__dict__.update(parse_task(r["task"]))
|
|
173
|
+
return self
|
|
174
|
+
|
|
175
|
+
async def wait(
|
|
176
|
+
self,
|
|
177
|
+
*,
|
|
178
|
+
poll_interval: float = 2.0,
|
|
179
|
+
timeout: float = 300.0,
|
|
180
|
+
on_poll: Optional[Callable[["AsyncTask"], Union[None, Awaitable[None]]]] = None,
|
|
181
|
+
) -> "AsyncTask":
|
|
182
|
+
"""Await until the task reaches a terminal state. ``on_poll`` (sync or
|
|
183
|
+
async) is called with ``self`` after each refresh."""
|
|
184
|
+
deadline = time.monotonic() + timeout
|
|
185
|
+
while self.status not in TASK_TERMINAL_STATES:
|
|
186
|
+
if time.monotonic() >= deadline:
|
|
187
|
+
raise APITimeoutError(f"Task {self.id} still {self.status!r} after {timeout}s.")
|
|
188
|
+
await asyncio.sleep(poll_interval)
|
|
189
|
+
await self.refresh()
|
|
190
|
+
if on_poll is not None:
|
|
191
|
+
res = on_poll(self)
|
|
192
|
+
if res is not None:
|
|
193
|
+
await res
|
|
194
|
+
return self
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class AsyncBuild(_Record):
|
|
198
|
+
id: str
|
|
199
|
+
server_id: str
|
|
200
|
+
goal: str
|
|
201
|
+
status: str
|
|
202
|
+
steps: list
|
|
203
|
+
artifacts: list
|
|
204
|
+
url: Optional[str]
|
|
205
|
+
created_at: str
|
|
206
|
+
|
|
207
|
+
def __init__(self, transport: AsyncTransport, raw: dict) -> None:
|
|
208
|
+
self._t = transport
|
|
209
|
+
self.__dict__.update(parse_build(raw))
|
|
210
|
+
|
|
211
|
+
async def refresh(self) -> "AsyncBuild":
|
|
212
|
+
r = await self._t.request("GET", f"/servers/{_seg(self.server_id)}/builds/{_seg(self.id)}")
|
|
213
|
+
self.__dict__.update(parse_build(r["build"]))
|
|
214
|
+
return self
|
|
215
|
+
|
|
216
|
+
async def wait(
|
|
217
|
+
self,
|
|
218
|
+
*,
|
|
219
|
+
poll_interval: float = 2.0,
|
|
220
|
+
timeout: float = 600.0,
|
|
221
|
+
on_poll: Optional[Callable[["AsyncBuild"], Union[None, Awaitable[None]]]] = None,
|
|
222
|
+
) -> "AsyncBuild":
|
|
223
|
+
"""Await until the build completes. ``on_poll`` (sync or async) is called
|
|
224
|
+
with ``self`` after each refresh."""
|
|
225
|
+
deadline = time.monotonic() + timeout
|
|
226
|
+
while self.status not in BUILD_TERMINAL_STATES:
|
|
227
|
+
if time.monotonic() >= deadline:
|
|
228
|
+
raise APITimeoutError(f"Build {self.id} still {self.status!r} after {timeout}s.")
|
|
229
|
+
await asyncio.sleep(poll_interval)
|
|
230
|
+
await self.refresh()
|
|
231
|
+
if on_poll is not None:
|
|
232
|
+
res = on_poll(self)
|
|
233
|
+
if res is not None:
|
|
234
|
+
await res
|
|
235
|
+
return self
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# =============================================================================
|
|
239
|
+
# Scoped sub-resources
|
|
240
|
+
# =============================================================================
|
|
241
|
+
|
|
242
|
+
class _ScopedProjects:
|
|
243
|
+
def __init__(self, transport: AsyncTransport, server_id: str) -> None:
|
|
244
|
+
self._t = transport
|
|
245
|
+
self._sid = server_id
|
|
246
|
+
|
|
247
|
+
async def list(self) -> List[AsyncProject]:
|
|
248
|
+
r = await self._t.request("GET", f"/servers/{_seg(self._sid)}/projects")
|
|
249
|
+
return [AsyncProject(self._t, p) for p in r["projects"]]
|
|
250
|
+
|
|
251
|
+
async def create(
|
|
252
|
+
self,
|
|
253
|
+
*,
|
|
254
|
+
name: Optional[str] = None,
|
|
255
|
+
description: Optional[str] = None,
|
|
256
|
+
visibility: Optional[str] = None,
|
|
257
|
+
lifecycle: Optional[str] = None,
|
|
258
|
+
template: Optional[str] = None,
|
|
259
|
+
) -> AsyncProject:
|
|
260
|
+
body = {
|
|
261
|
+
k: v
|
|
262
|
+
for k, v in {
|
|
263
|
+
"name": name,
|
|
264
|
+
"description": description,
|
|
265
|
+
"visibility": visibility,
|
|
266
|
+
"lifecycle": lifecycle,
|
|
267
|
+
"template": template,
|
|
268
|
+
}.items()
|
|
269
|
+
if v is not None
|
|
270
|
+
}
|
|
271
|
+
r = await self._t.request("POST", f"/servers/{_seg(self._sid)}/projects", body=body)
|
|
272
|
+
return AsyncProject(self._t, r)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class _ScopedTasks:
|
|
276
|
+
def __init__(self, transport: AsyncTransport, server_id: str) -> None:
|
|
277
|
+
self._t = transport
|
|
278
|
+
self._sid = server_id
|
|
279
|
+
|
|
280
|
+
async def create(self, goal: str) -> AsyncTask:
|
|
281
|
+
r = await self._t.request("POST", f"/servers/{_seg(self._sid)}/tasks", body={"goal": goal})
|
|
282
|
+
return AsyncTask(self._t, {"id": r["task_id"], "serverId": self._sid, "goal": goal, "status": r["status"], "createdAt": r.get("createdAt", "")})
|
|
283
|
+
|
|
284
|
+
async def list(self) -> List[AsyncTask]:
|
|
285
|
+
r = await self._t.request("GET", f"/servers/{_seg(self._sid)}/tasks")
|
|
286
|
+
return [AsyncTask(self._t, x) for x in r["tasks"]]
|
|
287
|
+
|
|
288
|
+
async def get(self, task_id: str) -> AsyncTask:
|
|
289
|
+
r = await self._t.request("GET", f"/servers/{_seg(self._sid)}/tasks/{_seg(task_id)}")
|
|
290
|
+
return AsyncTask(self._t, r["task"])
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class _ScopedBuilds:
|
|
294
|
+
def __init__(self, transport: AsyncTransport, server_id: str) -> None:
|
|
295
|
+
self._t = transport
|
|
296
|
+
self._sid = server_id
|
|
297
|
+
|
|
298
|
+
async def start(self, goal: str) -> AsyncBuild:
|
|
299
|
+
r = await self._t.request("POST", f"/servers/{_seg(self._sid)}/builds", body={"goal": goal})
|
|
300
|
+
return AsyncBuild(self._t, {"id": r["build_id"], "serverId": self._sid, "goal": goal, "status": r["status"], "steps": r.get("steps", []), "createdAt": r.get("createdAt", "")})
|
|
301
|
+
|
|
302
|
+
async def list(self) -> List[AsyncBuild]:
|
|
303
|
+
r = await self._t.request("GET", f"/servers/{_seg(self._sid)}/builds")
|
|
304
|
+
return [AsyncBuild(self._t, x) for x in r["builds"]]
|
|
305
|
+
|
|
306
|
+
async def get(self, build_id: str) -> AsyncBuild:
|
|
307
|
+
r = await self._t.request("GET", f"/servers/{_seg(self._sid)}/builds/{_seg(build_id)}")
|
|
308
|
+
return AsyncBuild(self._t, r["build"])
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class _ScopedFiles:
|
|
312
|
+
def __init__(self, transport: AsyncTransport, server_id: str) -> None:
|
|
313
|
+
self._t = transport
|
|
314
|
+
self._sid = server_id
|
|
315
|
+
|
|
316
|
+
async def list(self) -> List[str]:
|
|
317
|
+
r = await self._t.request("GET", f"/servers/{_seg(self._sid)}/files")
|
|
318
|
+
return r["files"]
|
|
319
|
+
|
|
320
|
+
async def read(self, path: str) -> str:
|
|
321
|
+
r = await self._t.request("GET", f"/servers/{_seg(self._sid)}/files", query={"path": path})
|
|
322
|
+
return r["content"]
|
|
323
|
+
|
|
324
|
+
async def write(self, path: str, content: str) -> None:
|
|
325
|
+
await self._t.request("PUT", f"/servers/{_seg(self._sid)}/files", body={"path": path, "content": content})
|
|
326
|
+
|
|
327
|
+
async def delete(self, path: str) -> None:
|
|
328
|
+
await self._t.request("DELETE", f"/servers/{_seg(self._sid)}/files", query={"path": path})
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# =============================================================================
|
|
332
|
+
# Top-level resources
|
|
333
|
+
# =============================================================================
|
|
334
|
+
|
|
335
|
+
class ServersResource:
|
|
336
|
+
def __init__(self, transport: AsyncTransport) -> None:
|
|
337
|
+
self._t = transport
|
|
338
|
+
|
|
339
|
+
async def create(
|
|
340
|
+
self, *, task: Optional[str] = None, tier: Optional[ServerTier] = None, region: Optional[str] = None
|
|
341
|
+
) -> AsyncServer:
|
|
342
|
+
body = {k: v for k, v in {"task": task, "tier": tier, "region": region}.items() if v is not None}
|
|
343
|
+
r = await self._t.request("POST", "/servers", body=body)
|
|
344
|
+
return AsyncServer(self._t, {
|
|
345
|
+
"id": r["id"], "task": task or "", "tier": r["tier"], "status": r["status"],
|
|
346
|
+
"region": region or "", "publicUrl": r.get("url"),
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
async def list(self) -> List[AsyncServer]:
|
|
350
|
+
r = await self._t.request("GET", "/servers")
|
|
351
|
+
return [AsyncServer(self._t, s) for s in r["servers"]]
|
|
352
|
+
|
|
353
|
+
async def get(self, server_id: str) -> AsyncServer:
|
|
354
|
+
r = await self._t.request("GET", f"/servers/{_seg(server_id)}")
|
|
355
|
+
return AsyncServer(self._t, r["server"])
|
|
356
|
+
|
|
357
|
+
async def update(self, server_id: str, *, task: Optional[str] = None, region: Optional[str] = None) -> AsyncServer:
|
|
358
|
+
body = {k: v for k, v in {"task": task, "region": region}.items() if v is not None}
|
|
359
|
+
r = await self._t.request("PATCH", f"/servers/{_seg(server_id)}", body=body)
|
|
360
|
+
return AsyncServer(self._t, r["server"])
|
|
361
|
+
|
|
362
|
+
async def delete(self, server_id: str) -> None:
|
|
363
|
+
await self._t.request("DELETE", f"/servers/{_seg(server_id)}")
|
|
364
|
+
|
|
365
|
+
async def control(self, server_id: str, action: ServerAction) -> dict:
|
|
366
|
+
return await self._t.request("POST", f"/servers/{_seg(server_id)}/actions", body={"action": action})
|
|
367
|
+
|
|
368
|
+
async def exec(self, server_id: str, command: str) -> str:
|
|
369
|
+
r = await self._t.request("POST", f"/servers/{_seg(server_id)}/exec", body={"command": command})
|
|
370
|
+
return r["stdout"]
|
|
371
|
+
|
|
372
|
+
async def deploy(self, server_id: str) -> str:
|
|
373
|
+
r = await self._t.request("POST", f"/servers/{_seg(server_id)}/deployments")
|
|
374
|
+
return r["url"]
|
|
375
|
+
|
|
376
|
+
async def list_deployments(self, server_id: str) -> List[Deployment]:
|
|
377
|
+
r = await self._t.request("GET", f"/servers/{_seg(server_id)}/deployments")
|
|
378
|
+
return [Deployment(**d) for d in r["deployments"]]
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class ProjectsResource:
|
|
382
|
+
def __init__(self, transport: AsyncTransport) -> None:
|
|
383
|
+
self._t = transport
|
|
384
|
+
|
|
385
|
+
async def list(self) -> List[AsyncProject]:
|
|
386
|
+
r = await self._t.request("GET", "/projects")
|
|
387
|
+
return [AsyncProject(self._t, p) for p in r["projects"]]
|
|
388
|
+
|
|
389
|
+
async def list_for_server(self, server_id: str) -> List[AsyncProject]:
|
|
390
|
+
return await _ScopedProjects(self._t, server_id).list()
|
|
391
|
+
|
|
392
|
+
async def create(self, server_id: str, **opts) -> AsyncProject:
|
|
393
|
+
return await _ScopedProjects(self._t, server_id).create(**opts)
|
|
394
|
+
|
|
395
|
+
async def get(self, project_id: str) -> AsyncProject:
|
|
396
|
+
r = await self._t.request("GET", f"/projects/{_seg(project_id)}")
|
|
397
|
+
return AsyncProject(self._t, r)
|
|
398
|
+
|
|
399
|
+
async def update(self, project_id: str, **patch) -> AsyncProject:
|
|
400
|
+
body = {k: v for k, v in patch.items() if v is not None}
|
|
401
|
+
r = await self._t.request("PATCH", f"/projects/{_seg(project_id)}", body=body)
|
|
402
|
+
return AsyncProject(self._t, r)
|
|
403
|
+
|
|
404
|
+
async def delete(self, project_id: str) -> None:
|
|
405
|
+
await self._t.request("DELETE", f"/projects/{_seg(project_id)}")
|
|
406
|
+
|
|
407
|
+
async def cleanup(self, project_id: str) -> None:
|
|
408
|
+
await self._t.request("POST", f"/projects/{_seg(project_id)}/cleanup")
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class TemplatesResource:
|
|
412
|
+
def __init__(self, transport: AsyncTransport) -> None:
|
|
413
|
+
self._t = transport
|
|
414
|
+
|
|
415
|
+
async def list(self) -> List[ProjectTemplate]:
|
|
416
|
+
r = await self._t.request("GET", "/templates")
|
|
417
|
+
return [ProjectTemplate.parse(x) for x in r["templates"]]
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
class TasksResource:
|
|
421
|
+
def __init__(self, transport: AsyncTransport) -> None:
|
|
422
|
+
self._t = transport
|
|
423
|
+
|
|
424
|
+
async def create(self, server_id: str, goal: str) -> AsyncTask:
|
|
425
|
+
return await _ScopedTasks(self._t, server_id).create(goal)
|
|
426
|
+
|
|
427
|
+
async def list(self, server_id: str) -> List[AsyncTask]:
|
|
428
|
+
return await _ScopedTasks(self._t, server_id).list()
|
|
429
|
+
|
|
430
|
+
async def get(self, server_id: str, task_id: str) -> AsyncTask:
|
|
431
|
+
return await _ScopedTasks(self._t, server_id).get(task_id)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
class BuildsResource:
|
|
435
|
+
def __init__(self, transport: AsyncTransport) -> None:
|
|
436
|
+
self._t = transport
|
|
437
|
+
|
|
438
|
+
async def start(self, server_id: str, goal: str) -> AsyncBuild:
|
|
439
|
+
return await _ScopedBuilds(self._t, server_id).start(goal)
|
|
440
|
+
|
|
441
|
+
async def list(self, server_id: str) -> List[AsyncBuild]:
|
|
442
|
+
return await _ScopedBuilds(self._t, server_id).list()
|
|
443
|
+
|
|
444
|
+
async def get(self, server_id: str, build_id: str) -> AsyncBuild:
|
|
445
|
+
return await _ScopedBuilds(self._t, server_id).get(build_id)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
class FilesResource:
|
|
449
|
+
def __init__(self, transport: AsyncTransport) -> None:
|
|
450
|
+
self._t = transport
|
|
451
|
+
|
|
452
|
+
async def list(self, server_id: str) -> List[str]:
|
|
453
|
+
return await _ScopedFiles(self._t, server_id).list()
|
|
454
|
+
|
|
455
|
+
async def read(self, server_id: str, path: str) -> str:
|
|
456
|
+
return await _ScopedFiles(self._t, server_id).read(path)
|
|
457
|
+
|
|
458
|
+
async def write(self, server_id: str, path: str, content: str) -> None:
|
|
459
|
+
await _ScopedFiles(self._t, server_id).write(path, content)
|
|
460
|
+
|
|
461
|
+
async def delete(self, server_id: str, path: str) -> None:
|
|
462
|
+
await _ScopedFiles(self._t, server_id).delete(path)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
class KeysResource:
|
|
466
|
+
def __init__(self, transport: AsyncTransport) -> None:
|
|
467
|
+
self._t = transport
|
|
468
|
+
|
|
469
|
+
async def list(self) -> List[ApiKey]:
|
|
470
|
+
r = await self._t.request("GET", "/keys")
|
|
471
|
+
return [ApiKey.parse(k) for k in r["keys"]]
|
|
472
|
+
|
|
473
|
+
async def create(self, label: str) -> CreatedApiKey:
|
|
474
|
+
r = await self._t.request("POST", "/keys", body={"label": label})
|
|
475
|
+
base = ApiKey.parse(r)
|
|
476
|
+
return CreatedApiKey(key=r["key"], **base.__dict__)
|
|
477
|
+
|
|
478
|
+
async def update_scope(
|
|
479
|
+
self,
|
|
480
|
+
key_id: str,
|
|
481
|
+
*,
|
|
482
|
+
allowed_server_ids: Optional[Sequence[str]] = None,
|
|
483
|
+
allowed_project_ids: Optional[Sequence[str]] = None,
|
|
484
|
+
) -> dict:
|
|
485
|
+
body = {
|
|
486
|
+
"allowed_server_ids": list(allowed_server_ids) if allowed_server_ids else [],
|
|
487
|
+
"allowed_project_ids": list(allowed_project_ids) if allowed_project_ids else [],
|
|
488
|
+
}
|
|
489
|
+
return await self._t.request("PATCH", f"/keys/{_seg(key_id)}", body=body)
|
|
490
|
+
|
|
491
|
+
async def revoke(self, key_id: str) -> None:
|
|
492
|
+
await self._t.request("DELETE", f"/keys/{_seg(key_id)}")
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
class WebhooksResource:
|
|
496
|
+
def __init__(self, transport: AsyncTransport) -> None:
|
|
497
|
+
self._t = transport
|
|
498
|
+
|
|
499
|
+
async def list(self) -> List[Webhook]:
|
|
500
|
+
r = await self._t.request("GET", "/webhooks")
|
|
501
|
+
return [Webhook.parse(w) for w in r["webhooks"]]
|
|
502
|
+
|
|
503
|
+
async def create(self, url: str, events: Optional[Sequence[str]] = None) -> CreatedWebhook:
|
|
504
|
+
body = {"url": url, "events": list(events) if events is not None else None}
|
|
505
|
+
r = await self._t.request("POST", "/webhooks", body=body)
|
|
506
|
+
base = Webhook.parse(r)
|
|
507
|
+
return CreatedWebhook(secret=r["secret"], **base.__dict__)
|
|
508
|
+
|
|
509
|
+
async def delete(self, webhook_id: str) -> None:
|
|
510
|
+
await self._t.request("DELETE", f"/webhooks/{_seg(webhook_id)}")
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
class AsyncServer4Agent:
|
|
514
|
+
"""Async client for the Server4Agent REST API.
|
|
515
|
+
|
|
516
|
+
import asyncio
|
|
517
|
+
from server4agent import AsyncServer4Agent
|
|
518
|
+
|
|
519
|
+
async def main():
|
|
520
|
+
async with AsyncServer4Agent() as client:
|
|
521
|
+
server = await client.servers.create(tier="small")
|
|
522
|
+
build = await (await server.builds.start("a FastAPI todo API")).wait()
|
|
523
|
+
print(build.url)
|
|
524
|
+
|
|
525
|
+
asyncio.run(main())
|
|
526
|
+
"""
|
|
527
|
+
|
|
528
|
+
def __init__(
|
|
529
|
+
self,
|
|
530
|
+
api_key: Optional[str] = None,
|
|
531
|
+
*,
|
|
532
|
+
base_url: str = _t.DEFAULT_BASE_URL,
|
|
533
|
+
timeout: float = _t.DEFAULT_TIMEOUT,
|
|
534
|
+
max_retries: int = _t.DEFAULT_MAX_RETRIES,
|
|
535
|
+
) -> None:
|
|
536
|
+
key = api_key or os.environ.get("SERVER4AGENT_API_KEY")
|
|
537
|
+
if not key:
|
|
538
|
+
raise ValueError(
|
|
539
|
+
"Server4Agent: no API key. Pass api_key=... or set SERVER4AGENT_API_KEY."
|
|
540
|
+
)
|
|
541
|
+
self._t = AsyncTransport(base_url, key, timeout=timeout, max_retries=max_retries)
|
|
542
|
+
|
|
543
|
+
self.servers = ServersResource(self._t)
|
|
544
|
+
self.projects = ProjectsResource(self._t)
|
|
545
|
+
self.templates = TemplatesResource(self._t)
|
|
546
|
+
self.tasks = TasksResource(self._t)
|
|
547
|
+
self.builds = BuildsResource(self._t)
|
|
548
|
+
self.files = FilesResource(self._t)
|
|
549
|
+
self.keys = KeysResource(self._t)
|
|
550
|
+
self.webhooks = WebhooksResource(self._t)
|
|
551
|
+
|
|
552
|
+
async def aclose(self) -> None:
|
|
553
|
+
await self._t.aclose()
|
|
554
|
+
|
|
555
|
+
async def __aenter__(self) -> "AsyncServer4Agent":
|
|
556
|
+
return self
|
|
557
|
+
|
|
558
|
+
async def __aexit__(self, *exc) -> None:
|
|
559
|
+
await self.aclose()
|