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,76 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import time
5
+ from typing import Any, Mapping, Optional
6
+
7
+ import httpx
8
+
9
+ from .. import _transport as t
10
+ from ..errors import APIConnectionError, APITimeoutError
11
+
12
+
13
+ class AsyncTransport:
14
+ """Async HTTP with timeouts, method-aware retries, and typed errors."""
15
+
16
+ def __init__(
17
+ self,
18
+ base_url: str,
19
+ api_key: str,
20
+ *,
21
+ timeout: float = t.DEFAULT_TIMEOUT,
22
+ max_retries: int = t.DEFAULT_MAX_RETRIES,
23
+ client: Optional[httpx.AsyncClient] = None,
24
+ ) -> None:
25
+ self.max_retries = max_retries
26
+ self._client = client or httpx.AsyncClient(
27
+ base_url=base_url.rstrip("/"),
28
+ timeout=timeout,
29
+ headers=t.default_headers(api_key),
30
+ )
31
+
32
+ async def request(
33
+ self,
34
+ method: str,
35
+ path: str,
36
+ *,
37
+ query: Optional[Mapping[str, Any]] = None,
38
+ body: Any = None,
39
+ ) -> Any:
40
+ params = t.clean_params(query)
41
+ attempt = 0
42
+ while True:
43
+ t.logger.debug("--> %s %s", method, path)
44
+ started = time.perf_counter()
45
+ try:
46
+ resp = await self._client.request(method, path, params=params, json=body)
47
+ except httpx.TimeoutException:
48
+ if t.can_retry_exception(method, attempt, self.max_retries):
49
+ await asyncio.sleep(t.retry_delay(attempt, None))
50
+ attempt += 1
51
+ continue
52
+ raise APITimeoutError() from None
53
+ except httpx.TransportError as exc:
54
+ if t.can_retry_exception(method, attempt, self.max_retries):
55
+ await asyncio.sleep(t.retry_delay(attempt, None))
56
+ attempt += 1
57
+ continue
58
+ raise APIConnectionError(str(exc)) from None
59
+
60
+ elapsed_ms = (time.perf_counter() - started) * 1000
61
+ t.logger.debug("<-- %s %s %s (%.0fms)", method, path, resp.status_code, elapsed_ms)
62
+
63
+ if t.should_retry_status(method, resp.status_code, attempt, self.max_retries):
64
+ delay = t.retry_delay(attempt, resp.headers.get("retry-after"))
65
+ t.logger.debug("... retrying in %.2fs (attempt %d)", delay, attempt + 1)
66
+ await asyncio.sleep(delay)
67
+ attempt += 1
68
+ continue
69
+
70
+ data = t.decode_json(resp.content)
71
+ if resp.status_code >= 400:
72
+ t.raise_for_status(resp.status_code, data, resp.headers)
73
+ return data
74
+
75
+ async def aclose(self) -> None:
76
+ await self._client.aclose()
@@ -0,0 +1,197 @@
1
+ """Wire types and parsers.
2
+
3
+ Pure-data responses are plain dataclasses. The four resources you can *act on*
4
+ (server, project, task, build) are returned as handles defined in the sync/async
5
+ clients; the field-parsing for those lives here as ``parse_*`` functions so both
6
+ clients stay identical.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Dict, List, Literal, Optional
12
+
13
+ ServerTier = Literal["small", "medium", "large"]
14
+ ServerStatus = Literal["running", "sleeping", "building"]
15
+ ServerAction = Literal["start", "stop", "restart"]
16
+ ProjectVisibility = Literal["public", "private"]
17
+ ProjectLifecycle = Literal["persistent", "ephemeral"]
18
+ ProjectRuntimeStatus = Literal["unbuilt", "running", "paused"]
19
+ TaskStatus = Literal["running", "completed", "failed", "awaiting_input", "cancelled"]
20
+ BuildStatus = Literal["queued", "running", "completed"]
21
+
22
+ # A task poll stops here: nothing further happens without you (input) or it's over.
23
+ TASK_TERMINAL_STATES = frozenset({"completed", "failed", "cancelled", "awaiting_input"})
24
+ BUILD_TERMINAL_STATES = frozenset({"completed"})
25
+
26
+ WEBHOOK_EVENTS = (
27
+ "task.created",
28
+ "task.completed",
29
+ "task.failed",
30
+ "deployment.live",
31
+ "deployment.removed",
32
+ "server.created",
33
+ "server.stopped",
34
+ "server.deleted",
35
+ "quota.threshold",
36
+ "quota.cap_reached",
37
+ )
38
+
39
+
40
+ class _Record:
41
+ """Base for handle types: a readable repr over public (non-underscore) attrs."""
42
+
43
+ def __repr__(self) -> str:
44
+ fields = ", ".join(
45
+ f"{k}={v!r}" for k, v in self.__dict__.items() if not k.startswith("_")
46
+ )
47
+ return f"{type(self).__name__}({fields})"
48
+
49
+
50
+ # --- pure data (no behavior) --------------------------------------------------
51
+
52
+ @dataclass
53
+ class Deployment:
54
+ url: str
55
+ live: bool
56
+
57
+
58
+ @dataclass
59
+ class ExecResult:
60
+ stdout: str
61
+
62
+
63
+ @dataclass
64
+ class BuildStep:
65
+ key: str
66
+ label: str
67
+ status: Literal["pending", "running", "done"]
68
+
69
+
70
+ @dataclass
71
+ class ProjectTemplateDefaults:
72
+ name: str
73
+ description: str
74
+ visibility: ProjectVisibility
75
+ lifecycle: ProjectLifecycle
76
+
77
+
78
+ @dataclass
79
+ class ProjectTemplate:
80
+ id: str
81
+ label: str
82
+ description: str
83
+ defaults: ProjectTemplateDefaults
84
+
85
+ @staticmethod
86
+ def parse(raw: dict) -> "ProjectTemplate":
87
+ return ProjectTemplate(
88
+ id=raw["id"],
89
+ label=raw["label"],
90
+ description=raw["description"],
91
+ defaults=ProjectTemplateDefaults(**raw["defaults"]),
92
+ )
93
+
94
+
95
+ @dataclass
96
+ class ApiKey:
97
+ id: str
98
+ label: str
99
+ masked: str # e.g. "sk_live_abc···wxyz"; never the full secret
100
+ allowed_server_ids: List[str]
101
+ allowed_project_ids: List[str]
102
+ created_at: str
103
+ last_used_at: Optional[str]
104
+
105
+ @staticmethod
106
+ def parse(raw: dict) -> "ApiKey":
107
+ return ApiKey(
108
+ id=raw["id"],
109
+ label=raw["label"],
110
+ masked=raw["masked"],
111
+ allowed_server_ids=raw.get("allowed_server_ids", []),
112
+ allowed_project_ids=raw.get("allowed_project_ids", []),
113
+ created_at=raw["created_at"],
114
+ last_used_at=raw.get("last_used_at"),
115
+ )
116
+
117
+
118
+ @dataclass
119
+ class CreatedApiKey(ApiKey):
120
+ # The plaintext sk_live_ key. Store it now — it can't be retrieved again.
121
+ key: str = ""
122
+
123
+
124
+ @dataclass
125
+ class Webhook:
126
+ id: str
127
+ url: str
128
+ events: List[str]
129
+ created_at: str
130
+
131
+ @staticmethod
132
+ def parse(raw: dict) -> "Webhook":
133
+ return Webhook(id=raw["id"], url=raw["url"], events=raw["events"], created_at=raw["created_at"])
134
+
135
+
136
+ @dataclass
137
+ class CreatedWebhook(Webhook):
138
+ # The signing secret (whsec_...). Store it now — it can't be retrieved again.
139
+ secret: str = ""
140
+
141
+
142
+ # --- field parsers for the handle types --------------------------------------
143
+
144
+ def parse_server(raw: dict) -> Dict[str, Any]:
145
+ return dict(
146
+ id=raw["id"],
147
+ task=raw["task"],
148
+ tier=raw["tier"],
149
+ status=raw["status"],
150
+ region=raw["region"],
151
+ credits_used=raw.get("creditsUsed", raw.get("credits_used", 0)),
152
+ storage_used_gb=raw.get("storageUsedGb", raw.get("storage_used_gb", 0)),
153
+ public_url=raw.get("publicUrl", raw.get("public_url")),
154
+ created_at=raw.get("createdAt", raw.get("created_at")),
155
+ )
156
+
157
+
158
+ def parse_project(raw: dict) -> Dict[str, Any]:
159
+ return dict(
160
+ id=raw["id"],
161
+ server_id=raw["server_id"],
162
+ name=raw["name"],
163
+ slug=raw["slug"],
164
+ description=raw.get("description"),
165
+ visibility=raw["visibility"],
166
+ lifecycle=raw["lifecycle"],
167
+ runtime_status=raw["runtime_status"],
168
+ template=raw.get("template"),
169
+ url=raw.get("url"),
170
+ created_at=raw["created_at"],
171
+ updated_at=raw["updated_at"],
172
+ )
173
+
174
+
175
+ def parse_task(raw: dict) -> Dict[str, Any]:
176
+ return dict(
177
+ id=raw["id"],
178
+ server_id=raw.get("serverId", raw.get("server_id")),
179
+ goal=raw["goal"],
180
+ status=raw["status"],
181
+ created_at=raw.get("createdAt", raw.get("created_at")),
182
+ result=raw.get("result"),
183
+ question=raw.get("question"),
184
+ )
185
+
186
+
187
+ def parse_build(raw: dict) -> Dict[str, Any]:
188
+ return dict(
189
+ id=raw["id"],
190
+ server_id=raw.get("serverId", raw.get("server_id")),
191
+ goal=raw["goal"],
192
+ status=raw["status"],
193
+ steps=[BuildStep(**s) for s in raw.get("steps", [])],
194
+ artifacts=raw.get("artifacts", []),
195
+ url=raw.get("url"),
196
+ created_at=raw.get("createdAt", raw.get("created_at")),
197
+ )
File without changes