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.
Files changed (35) hide show
  1. server4agent-0.7.0/CHANGELOG.md +141 -0
  2. server4agent-0.7.0/PKG-INFO +305 -0
  3. server4agent-0.7.0/README.md +279 -0
  4. server4agent-0.7.0/examples/await_input.py +30 -0
  5. server4agent-0.7.0/examples/exec_and_files.py +26 -0
  6. server4agent-0.7.0/examples/preview_env.py +51 -0
  7. server4agent-0.7.0/examples/webhook_receiver.py +35 -0
  8. {server4agent-0.1.0 → server4agent-0.7.0}/pyproject.toml +5 -1
  9. {server4agent-0.1.0 → server4agent-0.7.0}/server4agent/__init__.py +36 -1
  10. {server4agent-0.1.0 → server4agent-0.7.0}/server4agent/_async/client.py +443 -24
  11. server4agent-0.7.0/server4agent/_models.py +638 -0
  12. server4agent-0.7.0/server4agent/_sync/client.py +1081 -0
  13. {server4agent-0.1.0 → server4agent-0.7.0}/server4agent/_transport.py +1 -1
  14. server4agent-0.7.0/server4agent/_version.py +1 -0
  15. {server4agent-0.1.0 → server4agent-0.7.0}/server4agent/errors.py +12 -4
  16. server4agent-0.7.0/tests/test_client.py +1053 -0
  17. server4agent-0.1.0/CHANGELOG.md +0 -18
  18. server4agent-0.1.0/PKG-INFO +0 -151
  19. server4agent-0.1.0/README.md +0 -129
  20. server4agent-0.1.0/server4agent/_models.py +0 -197
  21. server4agent-0.1.0/server4agent/_sync/client.py +0 -567
  22. server4agent-0.1.0/server4agent/_version.py +0 -1
  23. server4agent-0.1.0/tests/test_client.py +0 -343
  24. {server4agent-0.1.0 → server4agent-0.7.0}/.gitignore +0 -0
  25. {server4agent-0.1.0 → server4agent-0.7.0}/LICENSE +0 -0
  26. {server4agent-0.1.0 → server4agent-0.7.0}/SECURITY.md +0 -0
  27. {server4agent-0.1.0 → server4agent-0.7.0}/examples/async_quickstart.py +0 -0
  28. {server4agent-0.1.0 → server4agent-0.7.0}/examples/quickstart.py +0 -0
  29. {server4agent-0.1.0 → server4agent-0.7.0}/server4agent/_async/__init__.py +0 -0
  30. {server4agent-0.1.0 → server4agent-0.7.0}/server4agent/_async/transport.py +0 -0
  31. {server4agent-0.1.0 → server4agent-0.7.0}/server4agent/_sync/__init__.py +0 -0
  32. {server4agent-0.1.0 → server4agent-0.7.0}/server4agent/_sync/transport.py +0 -0
  33. {server4agent-0.1.0 → server4agent-0.7.0}/server4agent/py.typed +0 -0
  34. {server4agent-0.1.0 → server4agent-0.7.0}/server4agent/webhooks.py +0 -0
  35. {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.