server4agent 0.1.0__tar.gz → 0.7.0__tar.gz
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-0.7.0/CHANGELOG.md +141 -0
- server4agent-0.7.0/PKG-INFO +305 -0
- server4agent-0.7.0/README.md +279 -0
- server4agent-0.7.0/examples/await_input.py +30 -0
- server4agent-0.7.0/examples/exec_and_files.py +26 -0
- server4agent-0.7.0/examples/preview_env.py +51 -0
- server4agent-0.7.0/examples/webhook_receiver.py +35 -0
- {server4agent-0.1.0 → server4agent-0.7.0}/pyproject.toml +5 -1
- {server4agent-0.1.0 → server4agent-0.7.0}/server4agent/__init__.py +36 -1
- {server4agent-0.1.0 → server4agent-0.7.0}/server4agent/_async/client.py +443 -24
- server4agent-0.7.0/server4agent/_models.py +638 -0
- server4agent-0.7.0/server4agent/_sync/client.py +1081 -0
- {server4agent-0.1.0 → server4agent-0.7.0}/server4agent/_transport.py +1 -1
- server4agent-0.7.0/server4agent/_version.py +1 -0
- {server4agent-0.1.0 → server4agent-0.7.0}/server4agent/errors.py +12 -4
- server4agent-0.7.0/tests/test_client.py +1053 -0
- server4agent-0.1.0/CHANGELOG.md +0 -18
- server4agent-0.1.0/PKG-INFO +0 -151
- server4agent-0.1.0/README.md +0 -129
- server4agent-0.1.0/server4agent/_models.py +0 -197
- server4agent-0.1.0/server4agent/_sync/client.py +0 -567
- server4agent-0.1.0/server4agent/_version.py +0 -1
- server4agent-0.1.0/tests/test_client.py +0 -343
- {server4agent-0.1.0 → server4agent-0.7.0}/.gitignore +0 -0
- {server4agent-0.1.0 → server4agent-0.7.0}/LICENSE +0 -0
- {server4agent-0.1.0 → server4agent-0.7.0}/SECURITY.md +0 -0
- {server4agent-0.1.0 → server4agent-0.7.0}/examples/async_quickstart.py +0 -0
- {server4agent-0.1.0 → server4agent-0.7.0}/examples/quickstart.py +0 -0
- {server4agent-0.1.0 → server4agent-0.7.0}/server4agent/_async/__init__.py +0 -0
- {server4agent-0.1.0 → server4agent-0.7.0}/server4agent/_async/transport.py +0 -0
- {server4agent-0.1.0 → server4agent-0.7.0}/server4agent/_sync/__init__.py +0 -0
- {server4agent-0.1.0 → server4agent-0.7.0}/server4agent/_sync/transport.py +0 -0
- {server4agent-0.1.0 → server4agent-0.7.0}/server4agent/py.typed +0 -0
- {server4agent-0.1.0 → server4agent-0.7.0}/server4agent/webhooks.py +0 -0
- {server4agent-0.1.0 → server4agent-0.7.0}/tests/test_webhooks.py +0 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
The client now covers every operation in the REST API. Everything below is
|
|
6
|
+
additive: code written against 0.6.0 keeps working unchanged.
|
|
7
|
+
|
|
8
|
+
New:
|
|
9
|
+
|
|
10
|
+
- Secrets, the environment variables injected into agent runs and bring-up.
|
|
11
|
+
`client.secrets.list()`, `set(key, value, description=None)` and
|
|
12
|
+
`delete(secret_id)` manage the account's secrets; `server.secrets` and
|
|
13
|
+
`project.secrets` (or `client.secrets.list_for_server()` /
|
|
14
|
+
`set_for_server()` / `list_for_project()` / `set_for_project()`) list and
|
|
15
|
+
bind them where they apply. Values are write-only: a `Secret` carries the
|
|
16
|
+
key, a note, a masked hint and its bindings, never the value.
|
|
17
|
+
- `server.logs(project_id=None, lines=None)`, `project.logs()` and
|
|
18
|
+
`client.servers.logs(server_id, ...)` return the log-file tails a server's
|
|
19
|
+
background processes write, or `None` before any were reported. When the
|
|
20
|
+
server cannot be read live the API answers with its last saved copy, so
|
|
21
|
+
check `LogsSnapshot.updated_at` before reading a tail as current.
|
|
22
|
+
- `project.start(command, name=None)` starts the project's app as a
|
|
23
|
+
background process on its routed port and keeps it running across restarts.
|
|
24
|
+
It returns an `AppStart` saying whether the app answered. When the app exits
|
|
25
|
+
instead, the `ConflictError` it raises carries the app's last output in
|
|
26
|
+
`err.body["log_tail"]`.
|
|
27
|
+
- `project.init(proceed=None)` starts bring-up for a project seeded from code,
|
|
28
|
+
or resumes one paused waiting for secrets.
|
|
29
|
+
- `project.export(ttl_seconds=None)` packs a project into one portable file
|
|
30
|
+
and returns a link to it, and `server.projects.import_bundle(bundle,
|
|
31
|
+
name=None)` recreates a project from that file, here or on another
|
|
32
|
+
deployment. `bundle` is the file's bytes or a base64 string. The result
|
|
33
|
+
lists the environment variables still to set in `missing_env`, because a
|
|
34
|
+
bundle carries their names and never their values.
|
|
35
|
+
- `client.tuning` starts, lists, reads and reports tuning runs, which fix an
|
|
36
|
+
agent from an evaluation platform's report. A `TuningSession` has `wait()`,
|
|
37
|
+
like a task or a build: reading a session is what advances it, so polling
|
|
38
|
+
is how it reaches `reported` or `failed`.
|
|
39
|
+
- `client.me()` returns the account the key acts on, its spend and budget this
|
|
40
|
+
period (`usage.blocked` says whether new tasks are refused), and how many
|
|
41
|
+
servers, projects and keys it has.
|
|
42
|
+
- `APIStatusError.body` is the whole JSON error response, for details beyond
|
|
43
|
+
the message, such as why a file write was refused (`reason`).
|
|
44
|
+
|
|
45
|
+
Catching up with the API:
|
|
46
|
+
|
|
47
|
+
- `server.deploy()` takes `project_id`, and `project.deploy()` deploys that
|
|
48
|
+
project. A server with several projects used to refuse every deploy from
|
|
49
|
+
this client with a 400, because there was no way to say which one. Deploy
|
|
50
|
+
now only puts a project online once something in it is listening, and
|
|
51
|
+
raises `ConflictError` when nothing is. It still returns the URL as a `str`,
|
|
52
|
+
which now also carries `.status` (`"live"`, or `"publishing"` when the URL
|
|
53
|
+
had not answered yet) and `.project_id`.
|
|
54
|
+
- `server.deployments()` raised `TypeError` on every call once the API began
|
|
55
|
+
returning each deployment's `project_id`. `Deployment.project_id` reads it.
|
|
56
|
+
- `webhooks.create(url)` without `events` sent `"events": null`, which the API
|
|
57
|
+
refuses. It now leaves the field out, which subscribes to every event.
|
|
58
|
+
`WEBHOOK_EVENTS` adds `quota.egress_cap_reached`, `tuning.started` and
|
|
59
|
+
`tuning.completed`.
|
|
60
|
+
- `server.exec()` takes `project_id`, to run inside that project's folder.
|
|
61
|
+
- `tasks.create()` takes `project_id`, and `follow_up_of` to continue an
|
|
62
|
+
earlier task's conversation. A task now reads `project_id`, `log` (the live
|
|
63
|
+
progress note), `change` (what the run committed) and `usage` (tokens, turns
|
|
64
|
+
and cost); one created as a follow-up also reads `follow_up_of` and
|
|
65
|
+
`resumed`.
|
|
66
|
+
- `servers.create()` and `projects.create()` take `secrets`, bound server-wide
|
|
67
|
+
or to the project. `projects.create()` also takes `source` (a template, a git
|
|
68
|
+
repository or an uploaded archive) and `auto_init`.
|
|
69
|
+
- Projects read `source`, `init_phase` and `run_plan`, plus `init_run_id` on
|
|
70
|
+
the handle that started a bring-up. `project.update()` takes `run_plan`.
|
|
71
|
+
- `files.get_download_url()` also returns the file's `path`.
|
|
72
|
+
- Templates read `source_kind`, and a build step's status can be `"failed"`.
|
|
73
|
+
- `ProjectAccess` and `ProjectVisibility` are exported from the package, as
|
|
74
|
+
0.4.0 said they were.
|
|
75
|
+
- `examples/preview_env.py` builds the branch in the project's own folder,
|
|
76
|
+
starts the app with `project.start()` and puts it online with
|
|
77
|
+
`project.deploy()`. It used to build at the server root, so the project URL
|
|
78
|
+
it printed never had anything behind it.
|
|
79
|
+
|
|
80
|
+
## 0.6.0
|
|
81
|
+
|
|
82
|
+
- `server.kind` on every server handle: `"standard"` for a headless box,
|
|
83
|
+
`"browser"` for one with a screen and a browser on it. The field was already
|
|
84
|
+
set at creation but no read path returned it, so a browser server was
|
|
85
|
+
indistinguishable from a headless one once you had it. A server from an API
|
|
86
|
+
that predates the field reads as `"standard"`.
|
|
87
|
+
- `kind` is accepted by `servers.create()` on both the sync and async clients,
|
|
88
|
+
which is what provisions a browser box. It needs a plan that allows the
|
|
89
|
+
medium tier, and costs more to run.
|
|
90
|
+
|
|
91
|
+
## 0.5.0
|
|
92
|
+
|
|
93
|
+
- Requires Python 3.11 or newer. 3.9 is past end of life and 3.10 reaches it in
|
|
94
|
+
October 2026, so the floor now sits on the oldest release that still gets
|
|
95
|
+
upstream fixes. The client itself is unchanged: on 3.11 and newer, 0.5.0
|
|
96
|
+
behaves exactly as 0.4.0 did.
|
|
97
|
+
|
|
98
|
+
## 0.4.0
|
|
99
|
+
|
|
100
|
+
- `project.access` and the `access` keyword on `projects.create()` and
|
|
101
|
+
`project.update()`. `"public"` is anyone with the link, which is what every
|
|
102
|
+
project did before and stays the default. `"team"` puts the project's URL
|
|
103
|
+
behind a Server4Agent sign-in, so only people in the account that owns it can
|
|
104
|
+
open it. A project from an API that predates the field reads as `"public"`.
|
|
105
|
+
- `ProjectAccess` is exported alongside `ProjectVisibility`. The two are
|
|
106
|
+
separate on purpose: `visibility` decides whether the URL serves at all,
|
|
107
|
+
`access` decides who gets through once it does.
|
|
108
|
+
|
|
109
|
+
## 0.3.0
|
|
110
|
+
|
|
111
|
+
- `task.message_type`: `"user"` for a task you created, `"system"` for one the
|
|
112
|
+
platform started for you (a project imported from git brings itself up this
|
|
113
|
+
way). A system task's `goal` is a short summary of the run, not the
|
|
114
|
+
instructions its agent was given. Tasks from an API that predates the field
|
|
115
|
+
read as `"user"`.
|
|
116
|
+
|
|
117
|
+
## 0.2.0
|
|
118
|
+
|
|
119
|
+
- `files.get_download_url(server_id, path, ttl_seconds=None)` — mint a
|
|
120
|
+
signed, expiring link anyone can use to download one workspace file, no
|
|
121
|
+
API key required to fetch it.
|
|
122
|
+
- `files.read_binary(server_id, path)` and `files.write(server_id, path,
|
|
123
|
+
content)` — read/write binary files (e.g. a generated `.xlsx`) without
|
|
124
|
+
corrupting bytes; `write` now also accepts `bytes` alongside `str`.
|
|
125
|
+
|
|
126
|
+
## 0.1.0
|
|
127
|
+
|
|
128
|
+
Initial release.
|
|
129
|
+
|
|
130
|
+
- Sync `Server4Agent` and async `AsyncServer4Agent` clients.
|
|
131
|
+
- Resource-oriented handles: `create()`/`get()` return `Server`/`Project`/`Task`/`Build`
|
|
132
|
+
objects with scoped sub-resources (`server.tasks`, `server.files`, ...) and actions
|
|
133
|
+
(`server.exec()`, `server.deploy()`, `project.update()`).
|
|
134
|
+
- `task.wait()` / `build.wait()` poll until a terminal state, with an `on_poll` callback.
|
|
135
|
+
- Typed error hierarchy under `Server4AgentError` (`NotFoundError`, `RateLimitError`,
|
|
136
|
+
`APIStatusError`, ...) exposing `status`, `code`, and `request_id`.
|
|
137
|
+
- Automatic retries with exponential backoff + jitter; retries are method-aware so a
|
|
138
|
+
`POST` is never retried in a way that could double-create. Configurable `timeout`
|
|
139
|
+
and `max_retries`.
|
|
140
|
+
- Webhook signature verification (`verify_webhook_signature`).
|
|
141
|
+
- Ships `py.typed`. Opt-in request logging via `SERVER4AGENT_LOG=debug`.
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: server4agent
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Python client for the Server4Agent REST API — sync + async, typed, with retries.
|
|
5
|
+
Project-URL: Homepage, https://www.server4agent.com
|
|
6
|
+
Project-URL: Documentation, https://www.server4agent.com/docs/sdks
|
|
7
|
+
Project-URL: Repository, https://github.com/Server4Agent/server4agent-python
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agents,mcp,sdk,server4agent
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: httpx<1,>=0.24
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# server4agent
|
|
28
|
+
|
|
29
|
+
Python client for the [Server4Agent](https://www.server4agent.com) REST API —
|
|
30
|
+
sync **and** async, fully typed, with automatic retries.
|
|
31
|
+
|
|
32
|
+
This SDK is for the code *around* your agent — your backend, a script, a
|
|
33
|
+
notebook, a webhook receiver. If your agent itself does tool-calling, point it
|
|
34
|
+
at the [MCP server](https://www.server4agent.com/docs/mcp) directly; it doesn't
|
|
35
|
+
need this package.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install server4agent
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Requires Python 3.11+. Depends only on [`httpx`](https://www.python-httpx.org/).
|
|
44
|
+
|
|
45
|
+
## Quickstart
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from server4agent import Server4Agent
|
|
49
|
+
|
|
50
|
+
client = Server4Agent() # reads SERVER4AGENT_API_KEY from the environment
|
|
51
|
+
|
|
52
|
+
# create() returns a handle you can act on directly
|
|
53
|
+
server = client.servers.create(tier="small")
|
|
54
|
+
|
|
55
|
+
# kick off a build and block until it's live
|
|
56
|
+
build = server.builds.start("a FastAPI todo API with a web UI").wait()
|
|
57
|
+
print(build.url)
|
|
58
|
+
|
|
59
|
+
# run a command right on the server
|
|
60
|
+
print(server.exec("ls -la"))
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`api_key` can also be passed explicitly: `Server4Agent(api_key="sk_live_...")`.
|
|
64
|
+
Use it as a context manager to close the connection pool:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
with Server4Agent() as client:
|
|
68
|
+
...
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Async
|
|
72
|
+
|
|
73
|
+
The async client mirrors the sync one method-for-method:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import asyncio
|
|
77
|
+
from server4agent import AsyncServer4Agent
|
|
78
|
+
|
|
79
|
+
async def main():
|
|
80
|
+
async with AsyncServer4Agent() as client:
|
|
81
|
+
server = await client.servers.create(tier="small")
|
|
82
|
+
task = await server.tasks.create("Scrape today's HN front page to JSON.")
|
|
83
|
+
await task.wait()
|
|
84
|
+
print(task.status, task.result)
|
|
85
|
+
|
|
86
|
+
asyncio.run(main())
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Handles
|
|
90
|
+
|
|
91
|
+
`create()` and `get()` return rich handles — data records you can also act on:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
server = client.servers.get("srv_abc")
|
|
95
|
+
server.tasks.create("...") # sub-resources are scoped to the server
|
|
96
|
+
server.files.write("app.py", "...")
|
|
97
|
+
server.deploy()
|
|
98
|
+
server.refresh() # re-fetch in place
|
|
99
|
+
|
|
100
|
+
project = client.projects.get("prj_xyz")
|
|
101
|
+
project.update(visibility="public")
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Binary files (e.g. a generated `.xlsx`) round-trip through `write`/`read_binary`
|
|
105
|
+
without corruption, and `get_download_url` mints a shareable, expiring link:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
server.files.write("report.xlsx", xlsx_bytes) # bytes -> base64 on the wire
|
|
109
|
+
data = server.files.read_binary("report.xlsx") # decoded back to bytes
|
|
110
|
+
|
|
111
|
+
link = server.files.get_download_url("report.xlsx", ttl_seconds=3600)
|
|
112
|
+
print(link["url"], link["expires_at"])
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`server.kind` says what sort of box you are holding: `"standard"` is headless,
|
|
116
|
+
`"browser"` has a screen with a browser on it, for sites an agent has to drive
|
|
117
|
+
rather than fetch. It is orthogonal to `tier`, and a nickname proves nothing, so
|
|
118
|
+
read the field rather than the name:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
server = client.servers.create(tier="medium", kind="browser")
|
|
122
|
+
with_screens = [s for s in client.servers.list() if s.kind == "browser"]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`task.wait()` / `build.wait()` poll until a terminal state; both accept
|
|
126
|
+
`poll_interval` and `timeout` (seconds).
|
|
127
|
+
|
|
128
|
+
Not every task in a server's list is one you sent. `task.message_type` is
|
|
129
|
+
`"system"` for runs the platform starts for you, such as the bring-up of a
|
|
130
|
+
project imported from git, and `"user"` for your own. A system task's `goal` is
|
|
131
|
+
a summary of what the run does, not the instructions its agent was given:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
mine = [t for t in server.tasks.list() if t.message_type == "user"]
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`project_id` runs a task in one project's folder, and `follow_up_of` continues
|
|
138
|
+
an earlier task's conversation, e.g. to answer one that is `awaiting_input`:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
task = server.tasks.create("Add a dark mode toggle", project_id=project.id)
|
|
142
|
+
reply = server.tasks.create("Use the system setting by default", follow_up_of=task.id)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Starting and deploying an app
|
|
146
|
+
|
|
147
|
+
`project.start()` runs the app as a background process with `PORT` set to the
|
|
148
|
+
project's routed port, keeps it running across restarts, and says whether it
|
|
149
|
+
answered. `deploy()` then makes the project public once something is listening:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from server4agent import ConflictError
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
started = project.start("node server.js")
|
|
156
|
+
print(started.status, started.listening) # "running" True
|
|
157
|
+
except ConflictError as e:
|
|
158
|
+
print(e.body.get("log_tail")) # the app's last output
|
|
159
|
+
|
|
160
|
+
url = project.deploy() # or server.deploy(project_id=...)
|
|
161
|
+
print(url, url.status) # "live", or "publishing" if not answering yet
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`server.logs(project_id=None, lines=None)` (or `project.logs()`) returns the
|
|
165
|
+
log-file tails of the server's background processes, or `None` before any
|
|
166
|
+
were reported. When the server cannot be read live the API answers with its
|
|
167
|
+
last saved copy, so check `updated_at` before trusting a tail as current:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
snap = project.logs(lines=50)
|
|
171
|
+
if snap:
|
|
172
|
+
print(snap.updated_at)
|
|
173
|
+
for f in snap.files:
|
|
174
|
+
print(f.path, f.tail)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Secrets
|
|
178
|
+
|
|
179
|
+
Secrets are environment variables injected into agent runs and bring-up.
|
|
180
|
+
Values are write-only: reads return the key, a note, a masked hint and where it
|
|
181
|
+
is bound, never the value.
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from server4agent import SecretInput
|
|
185
|
+
|
|
186
|
+
client.secrets.set("STRIPE_KEY", "sk_test_...", description="test mode")
|
|
187
|
+
server.secrets.set([{"key": "DATABASE_URL", "value": "postgres://..."}]) # every project on the server
|
|
188
|
+
project.secrets.set([SecretInput("SENTRY_DSN", "https://...")]) # this project only
|
|
189
|
+
print([s.key for s in project.secrets.list()])
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`servers.create()` and `projects.create()` also take `secrets=[...]`, so a
|
|
193
|
+
project seeded from git can come up without stopping to ask for them:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
project = server.projects.create(
|
|
197
|
+
source={"kind": "git", "git_url": "https://github.com/northstar/app"},
|
|
198
|
+
secrets=[{"key": "DATABASE_URL", "value": "postgres://..."}],
|
|
199
|
+
)
|
|
200
|
+
print(project.init_phase, project.init_run_id) # bring-up starts on its own
|
|
201
|
+
project.init(proceed=True) # resume one waiting on secrets
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Moving a project
|
|
205
|
+
|
|
206
|
+
`export()` packs a project into one file behind a time-limited link, and
|
|
207
|
+
`import_bundle()` recreates it on any server, here or on another deployment.
|
|
208
|
+
A bundle carries the names of the environment variables it needs, never their
|
|
209
|
+
values:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
import httpx
|
|
213
|
+
|
|
214
|
+
bundle = project.export(ttl_seconds=3600)
|
|
215
|
+
data = httpx.get(bundle.url).content
|
|
216
|
+
|
|
217
|
+
result = other_server.projects.import_bundle(data, name="landing-copy")
|
|
218
|
+
print(result.project.id, result.missing_env) # set these before the app can run
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Account and budget
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
me = client.me()
|
|
225
|
+
print(me.account["plan"], me.usage.spend_usd, me.usage.hard_limit_usd)
|
|
226
|
+
if me.usage.blocked:
|
|
227
|
+
print("new tasks are refused:", me.usage.reason)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Tuning runs
|
|
231
|
+
|
|
232
|
+
A tuning run fixes an agent from an evaluation platform's report, then reports
|
|
233
|
+
back what changed. Reading a session is what advances it, so `wait()` polls it
|
|
234
|
+
to `reported` or `failed`:
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
start = client.tuning.start("prop_123", project_id=project.id)
|
|
238
|
+
session = start.session.wait(on_poll=lambda s: print(s.status))
|
|
239
|
+
print(session.change_summary, session.changed_files)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Errors
|
|
243
|
+
|
|
244
|
+
Every failure is a subclass of `Server4AgentError`, so you can catch broadly or
|
|
245
|
+
narrowly:
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
from server4agent import NotFoundError, RateLimitError, APIStatusError
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
client.servers.get("srv_missing")
|
|
252
|
+
except NotFoundError:
|
|
253
|
+
... # 404
|
|
254
|
+
except RateLimitError as e:
|
|
255
|
+
... # 429 (already retried; quote e.request_id to support)
|
|
256
|
+
except APIStatusError as e:
|
|
257
|
+
print(e.status, e.code, e.message, e.request_id)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Retryable failures (429, 5xx, network blips) are retried automatically with
|
|
261
|
+
exponential backoff. Tune it per client:
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
Server4Agent(timeout=30.0, max_retries=4)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Verifying webhooks
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
import os
|
|
271
|
+
from server4agent import verify_webhook_signature
|
|
272
|
+
|
|
273
|
+
# request.data must be the raw body — parse it as JSON only after verifying.
|
|
274
|
+
ok = verify_webhook_signature(
|
|
275
|
+
secret=os.environ["SERVER4AGENT_WEBHOOK_SECRET"],
|
|
276
|
+
body=request.data.decode("utf-8"),
|
|
277
|
+
header=request.headers.get("Server4Agent-Signature"),
|
|
278
|
+
)
|
|
279
|
+
if not ok:
|
|
280
|
+
return "invalid signature", 401
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Examples
|
|
284
|
+
|
|
285
|
+
Runnable scripts in [`examples/`](examples/):
|
|
286
|
+
|
|
287
|
+
| File | What it shows |
|
|
288
|
+
| --- | --- |
|
|
289
|
+
| [`quickstart.py`](examples/quickstart.py) | Provision a server, build an app, print the live URL. |
|
|
290
|
+
| [`async_quickstart.py`](examples/async_quickstart.py) | The same flow on the async client. |
|
|
291
|
+
| [`exec_and_files.py`](examples/exec_and_files.py) | Write a file, install a dependency, and run it on the durable workspace. |
|
|
292
|
+
| [`await_input.py`](examples/await_input.py) | Handle a task that pauses to ask a question (`awaiting_input`). |
|
|
293
|
+
| [`preview_env.py`](examples/preview_env.py) | Ephemeral per-PR preview URL for CI. |
|
|
294
|
+
| [`webhook_receiver.py`](examples/webhook_receiver.py) | Verify webhook signatures in a Flask handler. |
|
|
295
|
+
|
|
296
|
+
## API surface
|
|
297
|
+
|
|
298
|
+
`servers`, `projects`, `templates`, `tasks`, `builds`, `files`, `secrets`,
|
|
299
|
+
`tuning`, `keys`, `webhooks`, and `client.me()`. Every REST operation has a
|
|
300
|
+
method on both clients; see the
|
|
301
|
+
[REST API docs](https://www.server4agent.com/docs/api) for the endpoints each
|
|
302
|
+
one wraps.
|
|
303
|
+
|
|
304
|
+
Server-side only: this SDK holds your `sk_live_` key. Never embed it in a
|
|
305
|
+
notebook or script you share.
|