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.
@@ -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()