server4agent 0.7.0__tar.gz → 0.9.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 → server4agent-0.9.0}/CHANGELOG.md +29 -0
- {server4agent-0.7.0 → server4agent-0.9.0}/PKG-INFO +4 -14
- {server4agent-0.7.0 → server4agent-0.9.0}/README.md +3 -13
- {server4agent-0.7.0 → server4agent-0.9.0}/server4agent/__init__.py +1 -8
- {server4agent-0.7.0 → server4agent-0.9.0}/server4agent/_async/client.py +4 -146
- {server4agent-0.7.0 → server4agent-0.9.0}/server4agent/_models.py +5 -69
- {server4agent-0.7.0 → server4agent-0.9.0}/server4agent/_sync/client.py +8 -169
- server4agent-0.9.0/server4agent/_version.py +1 -0
- {server4agent-0.7.0 → server4agent-0.9.0}/tests/test_client.py +18 -93
- server4agent-0.7.0/server4agent/_version.py +0 -1
- {server4agent-0.7.0 → server4agent-0.9.0}/.gitignore +0 -0
- {server4agent-0.7.0 → server4agent-0.9.0}/LICENSE +0 -0
- {server4agent-0.7.0 → server4agent-0.9.0}/SECURITY.md +0 -0
- {server4agent-0.7.0 → server4agent-0.9.0}/examples/async_quickstart.py +0 -0
- {server4agent-0.7.0 → server4agent-0.9.0}/examples/await_input.py +0 -0
- {server4agent-0.7.0 → server4agent-0.9.0}/examples/exec_and_files.py +0 -0
- {server4agent-0.7.0 → server4agent-0.9.0}/examples/preview_env.py +0 -0
- {server4agent-0.7.0 → server4agent-0.9.0}/examples/quickstart.py +0 -0
- {server4agent-0.7.0 → server4agent-0.9.0}/examples/webhook_receiver.py +0 -0
- {server4agent-0.7.0 → server4agent-0.9.0}/pyproject.toml +0 -0
- {server4agent-0.7.0 → server4agent-0.9.0}/server4agent/_async/__init__.py +0 -0
- {server4agent-0.7.0 → server4agent-0.9.0}/server4agent/_async/transport.py +0 -0
- {server4agent-0.7.0 → server4agent-0.9.0}/server4agent/_sync/__init__.py +0 -0
- {server4agent-0.7.0 → server4agent-0.9.0}/server4agent/_sync/transport.py +0 -0
- {server4agent-0.7.0 → server4agent-0.9.0}/server4agent/_transport.py +0 -0
- {server4agent-0.7.0 → server4agent-0.9.0}/server4agent/errors.py +0 -0
- {server4agent-0.7.0 → server4agent-0.9.0}/server4agent/py.typed +0 -0
- {server4agent-0.7.0 → server4agent-0.9.0}/server4agent/webhooks.py +0 -0
- {server4agent-0.7.0 → server4agent-0.9.0}/tests/test_webhooks.py +0 -0
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
Tuning runs are removed. The API no longer has the `/tuning/sessions`
|
|
6
|
+
endpoints, so neither client calls them. This is a breaking change.
|
|
7
|
+
|
|
8
|
+
- `client.tuning` is gone from both clients, with `list()`, `start()`, `get()`
|
|
9
|
+
and `report()`, and so are the `TuningSession` and `AsyncTuningSession`
|
|
10
|
+
handles and the `TuningStart` and `TuningStatus` types.
|
|
11
|
+
- `WEBHOOK_EVENTS` no longer lists `tuning.started` or `tuning.completed`, and
|
|
12
|
+
`webhooks.create()` with either is refused as an unknown event. Nothing
|
|
13
|
+
sends them any more. A webhook created before they were retired may still
|
|
14
|
+
have them stored; `webhooks.list()` leaves them off its `events`.
|
|
15
|
+
|
|
16
|
+
## 0.8.0
|
|
17
|
+
|
|
18
|
+
Projects can put their URL behind a password.
|
|
19
|
+
|
|
20
|
+
- `ProjectAccess` gains `"password"`: a visitor to the project's URL is asked
|
|
21
|
+
for a password the owner set, and gets in without any account. A project
|
|
22
|
+
read from the API can now report it, so code that branches on `access`
|
|
23
|
+
should handle the new value.
|
|
24
|
+
- `projects.create()` and `project.update()` (sync and async, and the flat
|
|
25
|
+
`client.projects.update(project_id, ...)`) take `access_password`. It is
|
|
26
|
+
required when switching to `access="password"` unless a password is already
|
|
27
|
+
set, and refused with any other access. Sent on its own to a password
|
|
28
|
+
project, it changes the password and signs out everyone who used the old
|
|
29
|
+
one. Switching to another access discards it. It is at least 4 characters
|
|
30
|
+
(a 4 digit code works), stored encrypted, and never returned by the API.
|
|
31
|
+
|
|
3
32
|
## 0.7.0
|
|
4
33
|
|
|
5
34
|
The client now covers every operation in the REST API. Everything below is
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: server4agent
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Python client for the Server4Agent REST API — sync + async, typed, with retries.
|
|
5
5
|
Project-URL: Homepage, https://www.server4agent.com
|
|
6
6
|
Project-URL: Documentation, https://www.server4agent.com/docs/sdks
|
|
@@ -99,6 +99,8 @@ server.refresh() # re-fetch in place
|
|
|
99
99
|
|
|
100
100
|
project = client.projects.get("prj_xyz")
|
|
101
101
|
project.update(visibility="public")
|
|
102
|
+
# Ask visitors for a password before the URL opens. No account needed to get in.
|
|
103
|
+
project.update(access="password", access_password="a long shared passphrase")
|
|
102
104
|
```
|
|
103
105
|
|
|
104
106
|
Binary files (e.g. a generated `.xlsx`) round-trip through `write`/`read_binary`
|
|
@@ -227,18 +229,6 @@ if me.usage.blocked:
|
|
|
227
229
|
print("new tasks are refused:", me.usage.reason)
|
|
228
230
|
```
|
|
229
231
|
|
|
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
232
|
## Errors
|
|
243
233
|
|
|
244
234
|
Every failure is a subclass of `Server4AgentError`, so you can catch broadly or
|
|
@@ -296,7 +286,7 @@ Runnable scripts in [`examples/`](examples/):
|
|
|
296
286
|
## API surface
|
|
297
287
|
|
|
298
288
|
`servers`, `projects`, `templates`, `tasks`, `builds`, `files`, `secrets`,
|
|
299
|
-
`
|
|
289
|
+
`keys`, `webhooks`, and `client.me()`. Every REST operation has a
|
|
300
290
|
method on both clients; see the
|
|
301
291
|
[REST API docs](https://www.server4agent.com/docs/api) for the endpoints each
|
|
302
292
|
one wraps.
|
|
@@ -73,6 +73,8 @@ server.refresh() # re-fetch in place
|
|
|
73
73
|
|
|
74
74
|
project = client.projects.get("prj_xyz")
|
|
75
75
|
project.update(visibility="public")
|
|
76
|
+
# Ask visitors for a password before the URL opens. No account needed to get in.
|
|
77
|
+
project.update(access="password", access_password="a long shared passphrase")
|
|
76
78
|
```
|
|
77
79
|
|
|
78
80
|
Binary files (e.g. a generated `.xlsx`) round-trip through `write`/`read_binary`
|
|
@@ -201,18 +203,6 @@ if me.usage.blocked:
|
|
|
201
203
|
print("new tasks are refused:", me.usage.reason)
|
|
202
204
|
```
|
|
203
205
|
|
|
204
|
-
## Tuning runs
|
|
205
|
-
|
|
206
|
-
A tuning run fixes an agent from an evaluation platform's report, then reports
|
|
207
|
-
back what changed. Reading a session is what advances it, so `wait()` polls it
|
|
208
|
-
to `reported` or `failed`:
|
|
209
|
-
|
|
210
|
-
```python
|
|
211
|
-
start = client.tuning.start("prop_123", project_id=project.id)
|
|
212
|
-
session = start.session.wait(on_poll=lambda s: print(s.status))
|
|
213
|
-
print(session.change_summary, session.changed_files)
|
|
214
|
-
```
|
|
215
|
-
|
|
216
206
|
## Errors
|
|
217
207
|
|
|
218
208
|
Every failure is a subclass of `Server4AgentError`, so you can catch broadly or
|
|
@@ -270,7 +260,7 @@ Runnable scripts in [`examples/`](examples/):
|
|
|
270
260
|
## API surface
|
|
271
261
|
|
|
272
262
|
`servers`, `projects`, `templates`, `tasks`, `builds`, `files`, `secrets`,
|
|
273
|
-
`
|
|
263
|
+
`keys`, `webhooks`, and `client.me()`. Every REST operation has a
|
|
274
264
|
method on both clients; see the
|
|
275
265
|
[REST API docs](https://www.server4agent.com/docs/api) for the endpoints each
|
|
276
266
|
one wraps.
|
|
@@ -4,7 +4,6 @@ from ._async.client import (
|
|
|
4
4
|
AsyncProject,
|
|
5
5
|
AsyncServer,
|
|
6
6
|
AsyncTask,
|
|
7
|
-
AsyncTuningSession,
|
|
8
7
|
)
|
|
9
8
|
from ._models import (
|
|
10
9
|
AccountUsage,
|
|
@@ -29,12 +28,10 @@ from ._models import (
|
|
|
29
28
|
ProjectVisibility,
|
|
30
29
|
Secret,
|
|
31
30
|
SecretInput,
|
|
32
|
-
TuningStart,
|
|
33
|
-
TuningStatus,
|
|
34
31
|
WEBHOOK_EVENTS,
|
|
35
32
|
Webhook,
|
|
36
33
|
)
|
|
37
|
-
from ._sync.client import Build, Project, Server, Server4Agent, Task
|
|
34
|
+
from ._sync.client import Build, Project, Server, Server4Agent, Task
|
|
38
35
|
from ._version import __version__
|
|
39
36
|
from .errors import (
|
|
40
37
|
APIConnectionError,
|
|
@@ -61,13 +58,11 @@ __all__ = [
|
|
|
61
58
|
"Project",
|
|
62
59
|
"Task",
|
|
63
60
|
"Build",
|
|
64
|
-
"TuningSession",
|
|
65
61
|
# async handles
|
|
66
62
|
"AsyncServer",
|
|
67
63
|
"AsyncProject",
|
|
68
64
|
"AsyncTask",
|
|
69
65
|
"AsyncBuild",
|
|
70
|
-
"AsyncTuningSession",
|
|
71
66
|
# data types
|
|
72
67
|
"ApiKey",
|
|
73
68
|
"CreatedApiKey",
|
|
@@ -78,7 +73,6 @@ __all__ = [
|
|
|
78
73
|
"AppStart",
|
|
79
74
|
"Me",
|
|
80
75
|
"AccountUsage",
|
|
81
|
-
"TuningStart",
|
|
82
76
|
"ExecResult",
|
|
83
77
|
"BuildStep",
|
|
84
78
|
"ProjectTemplate",
|
|
@@ -93,7 +87,6 @@ __all__ = [
|
|
|
93
87
|
"ProjectVisibility",
|
|
94
88
|
"ProjectAccess",
|
|
95
89
|
"ProjectInitPhase",
|
|
96
|
-
"TuningStatus",
|
|
97
90
|
"WEBHOOK_EVENTS",
|
|
98
91
|
# errors
|
|
99
92
|
"Server4AgentError",
|
|
@@ -27,9 +27,6 @@ from .._models import (
|
|
|
27
27
|
ServerKind,
|
|
28
28
|
ServerTier,
|
|
29
29
|
TASK_TERMINAL_STATES,
|
|
30
|
-
TUNING_TERMINAL_STATES,
|
|
31
|
-
TuningStart,
|
|
32
|
-
TuningStatus,
|
|
33
30
|
Webhook,
|
|
34
31
|
_Record,
|
|
35
32
|
bundle_b64,
|
|
@@ -37,7 +34,6 @@ from .._models import (
|
|
|
37
34
|
parse_project,
|
|
38
35
|
parse_server,
|
|
39
36
|
parse_task,
|
|
40
|
-
parse_tuning_session,
|
|
41
37
|
secret_inputs,
|
|
42
38
|
)
|
|
43
39
|
from .._transport import quote_segment as _seg
|
|
@@ -174,6 +170,7 @@ class AsyncProject(_Record):
|
|
|
174
170
|
description: Optional[str] = None,
|
|
175
171
|
visibility: Optional[str] = None,
|
|
176
172
|
access: Optional[str] = None,
|
|
173
|
+
access_password: Optional[str] = None,
|
|
177
174
|
lifecycle: Optional[str] = None,
|
|
178
175
|
run_plan: Optional[Mapping[str, Any]] = None,
|
|
179
176
|
) -> "AsyncProject":
|
|
@@ -184,6 +181,7 @@ class AsyncProject(_Record):
|
|
|
184
181
|
"description": description,
|
|
185
182
|
"visibility": visibility,
|
|
186
183
|
"access": access,
|
|
184
|
+
"access_password": access_password,
|
|
187
185
|
"lifecycle": lifecycle,
|
|
188
186
|
"run_plan": dict(run_plan) if run_plan is not None else None,
|
|
189
187
|
}.items()
|
|
@@ -315,74 +313,6 @@ class AsyncBuild(_Record):
|
|
|
315
313
|
return self
|
|
316
314
|
|
|
317
315
|
|
|
318
|
-
class AsyncTuningSession(_Record):
|
|
319
|
-
"""A tuning run. Reading it advances it; ``wait()`` polls until it is
|
|
320
|
-
``reported`` or ``failed``."""
|
|
321
|
-
id: str
|
|
322
|
-
server_id: str
|
|
323
|
-
project_id: str
|
|
324
|
-
run_id: Optional[str]
|
|
325
|
-
build_run_id: Optional[str]
|
|
326
|
-
proposal_id: str
|
|
327
|
-
evaluation_api_base: str
|
|
328
|
-
key_secret: str
|
|
329
|
-
brief_path: Optional[str]
|
|
330
|
-
brief_revision: Optional[str]
|
|
331
|
-
status: str
|
|
332
|
-
change_summary: Optional[str]
|
|
333
|
-
changed_files: List[str]
|
|
334
|
-
deployment_url: Optional[str]
|
|
335
|
-
reported_at: Optional[str]
|
|
336
|
-
report_error: Optional[str]
|
|
337
|
-
created_at: str
|
|
338
|
-
updated_at: str
|
|
339
|
-
run: Optional["AsyncTask"]
|
|
340
|
-
|
|
341
|
-
def __init__(self, transport: AsyncTransport, raw: dict, run: Optional[dict] = None) -> None:
|
|
342
|
-
self._t = transport
|
|
343
|
-
self.__dict__.update(parse_tuning_session(raw))
|
|
344
|
-
self.run = AsyncTask(transport, run) if run else None
|
|
345
|
-
|
|
346
|
-
async def refresh(self) -> "AsyncTuningSession":
|
|
347
|
-
r = await self._t.request("GET", f"/tuning/sessions/{_seg(self.id)}")
|
|
348
|
-
self.__dict__.update(parse_tuning_session(r["session"]))
|
|
349
|
-
self.run = AsyncTask(self._t, r["run"]) if r.get("run") else None
|
|
350
|
-
return self
|
|
351
|
-
|
|
352
|
-
async def wait(
|
|
353
|
-
self,
|
|
354
|
-
*,
|
|
355
|
-
poll_interval: float = 2.0,
|
|
356
|
-
timeout: float = 1800.0,
|
|
357
|
-
on_poll: Optional[Callable[["AsyncTuningSession"], Union[None, Awaitable[None]]]] = None,
|
|
358
|
-
) -> "AsyncTuningSession":
|
|
359
|
-
"""Await until the session is reported or failed. ``on_poll`` (sync or
|
|
360
|
-
async) is called with ``self`` after each refresh."""
|
|
361
|
-
deadline = time.monotonic() + timeout
|
|
362
|
-
while self.status not in TUNING_TERMINAL_STATES:
|
|
363
|
-
if time.monotonic() >= deadline:
|
|
364
|
-
raise APITimeoutError(f"Tuning session {self.id} still {self.status!r} after {timeout}s.")
|
|
365
|
-
await asyncio.sleep(poll_interval)
|
|
366
|
-
await self.refresh()
|
|
367
|
-
if on_poll is not None:
|
|
368
|
-
res = on_poll(self)
|
|
369
|
-
if res is not None:
|
|
370
|
-
await res
|
|
371
|
-
return self
|
|
372
|
-
|
|
373
|
-
async def report(
|
|
374
|
-
self,
|
|
375
|
-
*,
|
|
376
|
-
change_summary: Optional[str] = None,
|
|
377
|
-
changed_files: Optional[Sequence[str]] = None,
|
|
378
|
-
deployment_url: Optional[str] = None,
|
|
379
|
-
failed: Optional[bool] = None,
|
|
380
|
-
) -> "AsyncTuningSession":
|
|
381
|
-
r = await _report_tuning(self._t, self.id, change_summary, changed_files, deployment_url, failed)
|
|
382
|
-
self.__dict__.update(parse_tuning_session(r["session"]))
|
|
383
|
-
return self
|
|
384
|
-
|
|
385
|
-
|
|
386
316
|
# =============================================================================
|
|
387
317
|
# Scoped sub-resources
|
|
388
318
|
# =============================================================================
|
|
@@ -403,6 +333,7 @@ class _ScopedProjects:
|
|
|
403
333
|
description: Optional[str] = None,
|
|
404
334
|
visibility: Optional[str] = None,
|
|
405
335
|
access: Optional[str] = None,
|
|
336
|
+
access_password: Optional[str] = None,
|
|
406
337
|
lifecycle: Optional[str] = None,
|
|
407
338
|
template: Optional[str] = None,
|
|
408
339
|
source: Optional[Mapping[str, Any]] = None,
|
|
@@ -416,6 +347,7 @@ class _ScopedProjects:
|
|
|
416
347
|
"description": description,
|
|
417
348
|
"visibility": visibility,
|
|
418
349
|
"access": access,
|
|
350
|
+
"access_password": access_password,
|
|
419
351
|
"lifecycle": lifecycle,
|
|
420
352
|
"template": template,
|
|
421
353
|
"source": dict(source) if source is not None else None,
|
|
@@ -582,27 +514,6 @@ async def _logs(
|
|
|
582
514
|
return LogsSnapshot.parse(r["logs"]) if r.get("logs") is not None else None
|
|
583
515
|
|
|
584
516
|
|
|
585
|
-
async def _report_tuning(
|
|
586
|
-
t: AsyncTransport,
|
|
587
|
-
session_id: str,
|
|
588
|
-
change_summary: Optional[str],
|
|
589
|
-
changed_files: Optional[Sequence[str]],
|
|
590
|
-
deployment_url: Optional[str],
|
|
591
|
-
failed: Optional[bool],
|
|
592
|
-
) -> dict:
|
|
593
|
-
body = {
|
|
594
|
-
k: v
|
|
595
|
-
for k, v in {
|
|
596
|
-
"change_summary": change_summary,
|
|
597
|
-
"changed_files": list(changed_files) if changed_files is not None else None,
|
|
598
|
-
"deployment_url": deployment_url,
|
|
599
|
-
"failed": failed,
|
|
600
|
-
}.items()
|
|
601
|
-
if v is not None
|
|
602
|
-
}
|
|
603
|
-
return await t.request("PATCH", f"/tuning/sessions/{_seg(session_id)}", body=body)
|
|
604
|
-
|
|
605
|
-
|
|
606
517
|
# =============================================================================
|
|
607
518
|
# Top-level resources
|
|
608
519
|
# =============================================================================
|
|
@@ -822,58 +733,6 @@ class SecretsResource:
|
|
|
822
733
|
return await _ScopedSecrets(self._t, f"/projects/{_seg(project_id)}/secrets").set(secrets)
|
|
823
734
|
|
|
824
735
|
|
|
825
|
-
class TuningResource:
|
|
826
|
-
def __init__(self, transport: AsyncTransport) -> None:
|
|
827
|
-
self._t = transport
|
|
828
|
-
|
|
829
|
-
async def list(
|
|
830
|
-
self, *, project_id: Optional[str] = None, status: Optional[TuningStatus] = None
|
|
831
|
-
) -> List[AsyncTuningSession]:
|
|
832
|
-
r = await self._t.request("GET", "/tuning/sessions", query={"project_id": project_id, "status": status})
|
|
833
|
-
return [AsyncTuningSession(self._t, s) for s in r["sessions"]]
|
|
834
|
-
|
|
835
|
-
async def start(
|
|
836
|
-
self,
|
|
837
|
-
proposal_id: str,
|
|
838
|
-
*,
|
|
839
|
-
project_id: Optional[str] = None,
|
|
840
|
-
server_id: Optional[str] = None,
|
|
841
|
-
evaluation_api_base: Optional[str] = None,
|
|
842
|
-
key_secret: Optional[str] = None,
|
|
843
|
-
) -> TuningStart[AsyncTuningSession]:
|
|
844
|
-
body = {
|
|
845
|
-
k: v
|
|
846
|
-
for k, v in {
|
|
847
|
-
"proposal_id": proposal_id,
|
|
848
|
-
"project_id": project_id,
|
|
849
|
-
"server_id": server_id,
|
|
850
|
-
"evaluation_api_base": evaluation_api_base,
|
|
851
|
-
"key_secret": key_secret,
|
|
852
|
-
}.items()
|
|
853
|
-
if v is not None
|
|
854
|
-
}
|
|
855
|
-
r = await self._t.request("POST", "/tuning/sessions", body=body)
|
|
856
|
-
return TuningStart.parse(AsyncTuningSession(self._t, r["session"]), r)
|
|
857
|
-
|
|
858
|
-
async def get(self, tuning_session_id: str) -> AsyncTuningSession:
|
|
859
|
-
r = await self._t.request("GET", f"/tuning/sessions/{_seg(tuning_session_id)}")
|
|
860
|
-
return AsyncTuningSession(self._t, r["session"], r.get("run"))
|
|
861
|
-
|
|
862
|
-
async def report(
|
|
863
|
-
self,
|
|
864
|
-
tuning_session_id: str,
|
|
865
|
-
*,
|
|
866
|
-
change_summary: Optional[str] = None,
|
|
867
|
-
changed_files: Optional[Sequence[str]] = None,
|
|
868
|
-
deployment_url: Optional[str] = None,
|
|
869
|
-
failed: Optional[bool] = None,
|
|
870
|
-
) -> AsyncTuningSession:
|
|
871
|
-
r = await _report_tuning(
|
|
872
|
-
self._t, tuning_session_id, change_summary, changed_files, deployment_url, failed
|
|
873
|
-
)
|
|
874
|
-
return AsyncTuningSession(self._t, r["session"])
|
|
875
|
-
|
|
876
|
-
|
|
877
736
|
class KeysResource:
|
|
878
737
|
def __init__(self, transport: AsyncTransport) -> None:
|
|
879
738
|
self._t = transport
|
|
@@ -961,7 +820,6 @@ class AsyncServer4Agent:
|
|
|
961
820
|
self.builds = BuildsResource(self._t)
|
|
962
821
|
self.files = FilesResource(self._t)
|
|
963
822
|
self.secrets = SecretsResource(self._t)
|
|
964
|
-
self.tuning = TuningResource(self._t)
|
|
965
823
|
self.keys = KeysResource(self._t)
|
|
966
824
|
self.webhooks = WebhooksResource(self._t)
|
|
967
825
|
|
|
@@ -21,9 +21,10 @@ ServerStatus = Literal["running", "sleeping", "building"]
|
|
|
21
21
|
ServerAction = Literal["start", "stop", "restart"]
|
|
22
22
|
ProjectVisibility = Literal["public", "private"]
|
|
23
23
|
#: Who may open a public project's URL. "team" puts it behind a Server4Agent
|
|
24
|
-
#: sign-in and membership of the owning account; "
|
|
25
|
-
#:
|
|
26
|
-
|
|
24
|
+
#: sign-in and membership of the owning account; "password" asks for a password
|
|
25
|
+
#: the owner set (``access_password``) and needs no account; "public" is anyone
|
|
26
|
+
#: with the link, which is the default and how every project behaved before this.
|
|
27
|
+
ProjectAccess = Literal["public", "team", "password"]
|
|
27
28
|
ProjectLifecycle = Literal["persistent", "ephemeral"]
|
|
28
29
|
ProjectRuntimeStatus = Literal["unbuilt", "running", "paused"]
|
|
29
30
|
#: Where a project's bring-up has got to, from seeding the workspace to a
|
|
@@ -37,15 +38,10 @@ BuildStatus = Literal["queued", "running", "completed"]
|
|
|
37
38
|
#: ``"live"`` when the project's URL answered within the deploy's wait,
|
|
38
39
|
#: ``"publishing"`` when it is public but had not answered yet.
|
|
39
40
|
DeployStatus = Literal["live", "publishing"]
|
|
40
|
-
#: ``"building"`` while the agent's code is brought up (a run started with
|
|
41
|
-
#: ``server_id``), ``"fixing"`` while the agent works on the brief, then
|
|
42
|
-
#: ``"reported"`` or ``"failed"``.
|
|
43
|
-
TuningStatus = Literal["building", "fixing", "reported", "failed"]
|
|
44
41
|
|
|
45
42
|
# A task poll stops here: nothing further happens without you (input) or it's over.
|
|
46
43
|
TASK_TERMINAL_STATES = frozenset({"completed", "failed", "cancelled", "awaiting_input"})
|
|
47
44
|
BUILD_TERMINAL_STATES = frozenset({"completed"})
|
|
48
|
-
TUNING_TERMINAL_STATES = frozenset({"reported", "failed"})
|
|
49
45
|
|
|
50
46
|
WEBHOOK_EVENTS = (
|
|
51
47
|
"task.created",
|
|
@@ -59,8 +55,6 @@ WEBHOOK_EVENTS = (
|
|
|
59
55
|
"quota.threshold",
|
|
60
56
|
"quota.cap_reached",
|
|
61
57
|
"quota.egress_cap_reached",
|
|
62
|
-
"tuning.started",
|
|
63
|
-
"tuning.completed",
|
|
64
58
|
)
|
|
65
59
|
|
|
66
60
|
|
|
@@ -477,40 +471,6 @@ class ProjectImport(Generic[P]):
|
|
|
477
471
|
)
|
|
478
472
|
|
|
479
473
|
|
|
480
|
-
@dataclass
|
|
481
|
-
class TuningStart(Generic[P]):
|
|
482
|
-
"""What ``tuning.start()`` answered.
|
|
483
|
-
|
|
484
|
-
Starting twice for one proposal never starts a second agent: the API hands
|
|
485
|
-
back the run already under way, with ``started`` False and a ``message``.
|
|
486
|
-
"""
|
|
487
|
-
session: P
|
|
488
|
-
#: False when a run already existed for this proposal and ``session`` is it.
|
|
489
|
-
started: bool
|
|
490
|
-
message: Optional[str] = None
|
|
491
|
-
#: False when the brief was issued before the platform finished its
|
|
492
|
-
#: analysis, so it carries per-case judgements but no summarised diagnosis.
|
|
493
|
-
analysis_ready: Optional[bool] = None
|
|
494
|
-
#: Set when the run started but the evaluation platform could not be told.
|
|
495
|
-
#: The run is unaffected; the next status report retries.
|
|
496
|
-
warning: Optional[str] = None
|
|
497
|
-
#: Set when this call built the agent's code into a new project
|
|
498
|
-
#: (``server_id``): ``projectId``, ``source`` and ``envKeys``.
|
|
499
|
-
built: Optional[dict] = None
|
|
500
|
-
|
|
501
|
-
@staticmethod
|
|
502
|
-
def parse(session: Any, raw: dict) -> "TuningStart[Any]":
|
|
503
|
-
return TuningStart(
|
|
504
|
-
session=session,
|
|
505
|
-
# The API answers an existing run with a message and a new one without.
|
|
506
|
-
started="message" not in raw,
|
|
507
|
-
message=raw.get("message"),
|
|
508
|
-
analysis_ready=raw.get("analysisReady", raw.get("analysis_ready")),
|
|
509
|
-
warning=raw.get("warning"),
|
|
510
|
-
built=raw.get("built"),
|
|
511
|
-
)
|
|
512
|
-
|
|
513
|
-
|
|
514
474
|
# --- request-body helpers ------------------------------------------------------
|
|
515
475
|
|
|
516
476
|
def secret_inputs(secrets: SecretsArg) -> List[dict]:
|
|
@@ -584,8 +544,7 @@ def parse_project(raw: dict) -> Dict[str, Any]:
|
|
|
584
544
|
|
|
585
545
|
def parse_task(raw: dict) -> Dict[str, Any]:
|
|
586
546
|
return dict(
|
|
587
|
-
|
|
588
|
-
id=raw["id"] if "id" in raw else raw["run_id"],
|
|
547
|
+
id=raw["id"],
|
|
589
548
|
server_id=raw.get("serverId", raw.get("server_id")),
|
|
590
549
|
project_id=raw.get("projectId", raw.get("project_id")),
|
|
591
550
|
goal=raw["goal"],
|
|
@@ -613,26 +572,3 @@ def parse_build(raw: dict) -> Dict[str, Any]:
|
|
|
613
572
|
url=raw.get("url"),
|
|
614
573
|
created_at=raw.get("createdAt", raw.get("created_at")),
|
|
615
574
|
)
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
def parse_tuning_session(raw: dict) -> Dict[str, Any]:
|
|
619
|
-
return dict(
|
|
620
|
-
id=raw["id"],
|
|
621
|
-
server_id=raw.get("serverId", raw.get("server_id")),
|
|
622
|
-
project_id=raw.get("projectId", raw.get("project_id")),
|
|
623
|
-
run_id=raw.get("runId", raw.get("run_id")),
|
|
624
|
-
build_run_id=raw.get("buildRunId", raw.get("build_run_id")),
|
|
625
|
-
proposal_id=raw.get("proposalId", raw.get("proposal_id")),
|
|
626
|
-
evaluation_api_base=raw.get("evaluationApiBase", raw.get("evaluation_api_base")),
|
|
627
|
-
key_secret=raw.get("keySecretKey", raw.get("key_secret")),
|
|
628
|
-
brief_path=raw.get("briefPath", raw.get("brief_path")),
|
|
629
|
-
brief_revision=raw.get("briefRevision", raw.get("brief_revision")),
|
|
630
|
-
status=raw["status"],
|
|
631
|
-
change_summary=raw.get("changeSummary", raw.get("change_summary")),
|
|
632
|
-
changed_files=raw.get("changedFiles", raw.get("changed_files")) or [],
|
|
633
|
-
deployment_url=raw.get("deploymentUrl", raw.get("deployment_url")),
|
|
634
|
-
reported_at=raw.get("reportedAt", raw.get("reported_at")),
|
|
635
|
-
report_error=raw.get("reportError", raw.get("report_error")),
|
|
636
|
-
created_at=raw.get("createdAt", raw.get("created_at")),
|
|
637
|
-
updated_at=raw.get("updatedAt", raw.get("updated_at")),
|
|
638
|
-
)
|
|
@@ -26,9 +26,6 @@ from .._models import (
|
|
|
26
26
|
ServerKind,
|
|
27
27
|
ServerTier,
|
|
28
28
|
TASK_TERMINAL_STATES,
|
|
29
|
-
TUNING_TERMINAL_STATES,
|
|
30
|
-
TuningStart,
|
|
31
|
-
TuningStatus,
|
|
32
29
|
Webhook,
|
|
33
30
|
_Record,
|
|
34
31
|
bundle_b64,
|
|
@@ -36,7 +33,6 @@ from .._models import (
|
|
|
36
33
|
parse_project,
|
|
37
34
|
parse_server,
|
|
38
35
|
parse_task,
|
|
39
|
-
parse_tuning_session,
|
|
40
36
|
secret_inputs,
|
|
41
37
|
)
|
|
42
38
|
from .._transport import quote_segment as _seg
|
|
@@ -193,11 +189,15 @@ class Project(_Record):
|
|
|
193
189
|
description: Optional[str] = None,
|
|
194
190
|
visibility: Optional[str] = None,
|
|
195
191
|
access: Optional[str] = None,
|
|
192
|
+
access_password: Optional[str] = None,
|
|
196
193
|
lifecycle: Optional[str] = None,
|
|
197
194
|
run_plan: Optional[Mapping[str, Any]] = None,
|
|
198
195
|
) -> "Project":
|
|
199
196
|
"""``run_plan`` replaces the stored plan wholesale and is recorded for the
|
|
200
|
-
next bring-up, not acted on: ``init()`` is how you ask for the rebuild.
|
|
197
|
+
next bring-up, not acted on: ``init()`` is how you ask for the rebuild.
|
|
198
|
+
``access_password`` sets or changes the password for ``access="password"``;
|
|
199
|
+
sent alone it changes the password and signs out everyone who used the
|
|
200
|
+
old one. It is never returned."""
|
|
201
201
|
body = {
|
|
202
202
|
k: v
|
|
203
203
|
for k, v in {
|
|
@@ -205,6 +205,7 @@ class Project(_Record):
|
|
|
205
205
|
"description": description,
|
|
206
206
|
"visibility": visibility,
|
|
207
207
|
"access": access,
|
|
208
|
+
"access_password": access_password,
|
|
208
209
|
"lifecycle": lifecycle,
|
|
209
210
|
"run_plan": dict(run_plan) if run_plan is not None else None,
|
|
210
211
|
}.items()
|
|
@@ -366,86 +367,6 @@ class Build(_Record):
|
|
|
366
367
|
return self
|
|
367
368
|
|
|
368
369
|
|
|
369
|
-
class TuningSession(_Record):
|
|
370
|
-
"""A run that fixes an agent from an evaluation platform's report.
|
|
371
|
-
|
|
372
|
-
Reading it is what advances it: each ``refresh()`` checks the agent run
|
|
373
|
-
behind it, and the first one after that run finishes reports the outcome
|
|
374
|
-
back to the platform. ``wait()`` polls until it is reported or failed.
|
|
375
|
-
"""
|
|
376
|
-
id: str
|
|
377
|
-
server_id: str
|
|
378
|
-
project_id: str
|
|
379
|
-
#: The agent run doing the fixing (a task on the server). None while building.
|
|
380
|
-
run_id: Optional[str]
|
|
381
|
-
#: The bring-up run, when this session built the project from the agent's source.
|
|
382
|
-
build_run_id: Optional[str]
|
|
383
|
-
proposal_id: str
|
|
384
|
-
evaluation_api_base: str
|
|
385
|
-
#: Name of the secret holding the evaluation API key, never its value.
|
|
386
|
-
key_secret: str
|
|
387
|
-
brief_path: Optional[str]
|
|
388
|
-
brief_revision: Optional[str]
|
|
389
|
-
status: str
|
|
390
|
-
change_summary: Optional[str]
|
|
391
|
-
changed_files: List[str]
|
|
392
|
-
deployment_url: Optional[str]
|
|
393
|
-
reported_at: Optional[str]
|
|
394
|
-
#: Why the last report to the platform failed. The next read retries.
|
|
395
|
-
report_error: Optional[str]
|
|
396
|
-
created_at: str
|
|
397
|
-
updated_at: str
|
|
398
|
-
#: The agent run behind the session, as of the last ``get()`` or ``refresh()``.
|
|
399
|
-
run: Optional["Task"]
|
|
400
|
-
|
|
401
|
-
def __init__(self, transport: SyncTransport, raw: dict, run: Optional[dict] = None) -> None:
|
|
402
|
-
self._t = transport
|
|
403
|
-
self.__dict__.update(parse_tuning_session(raw))
|
|
404
|
-
self.run = Task(transport, run) if run else None
|
|
405
|
-
|
|
406
|
-
def refresh(self) -> "TuningSession":
|
|
407
|
-
r = self._t.request("GET", f"/tuning/sessions/{_seg(self.id)}")
|
|
408
|
-
self.__dict__.update(parse_tuning_session(r["session"]))
|
|
409
|
-
self.run = Task(self._t, r["run"]) if r.get("run") else None
|
|
410
|
-
return self
|
|
411
|
-
|
|
412
|
-
def wait(
|
|
413
|
-
self,
|
|
414
|
-
*,
|
|
415
|
-
poll_interval: float = 2.0,
|
|
416
|
-
timeout: float = 1800.0,
|
|
417
|
-
on_poll: Optional[Callable[["TuningSession"], None]] = None,
|
|
418
|
-
) -> "TuningSession":
|
|
419
|
-
"""Block until the session is ``reported`` or ``failed``. Raises
|
|
420
|
-
``APITimeoutError`` if ``timeout`` elapses first.
|
|
421
|
-
|
|
422
|
-
``on_poll`` is called with ``self`` after each refresh."""
|
|
423
|
-
deadline = time.monotonic() + timeout
|
|
424
|
-
while self.status not in TUNING_TERMINAL_STATES:
|
|
425
|
-
if time.monotonic() >= deadline:
|
|
426
|
-
raise APITimeoutError(f"Tuning session {self.id} still {self.status!r} after {timeout}s.")
|
|
427
|
-
time.sleep(poll_interval)
|
|
428
|
-
self.refresh()
|
|
429
|
-
if on_poll is not None:
|
|
430
|
-
on_poll(self)
|
|
431
|
-
return self
|
|
432
|
-
|
|
433
|
-
def report(
|
|
434
|
-
self,
|
|
435
|
-
*,
|
|
436
|
-
change_summary: Optional[str] = None,
|
|
437
|
-
changed_files: Optional[Sequence[str]] = None,
|
|
438
|
-
deployment_url: Optional[str] = None,
|
|
439
|
-
failed: Optional[bool] = None,
|
|
440
|
-
) -> "TuningSession":
|
|
441
|
-
"""Send your own account of the fix to the evaluation platform, rather
|
|
442
|
-
than letting it be read off the run's final message. ``failed=True``
|
|
443
|
-
closes the run as failed."""
|
|
444
|
-
r = _report_tuning(self._t, self.id, change_summary, changed_files, deployment_url, failed)
|
|
445
|
-
self.__dict__.update(parse_tuning_session(r["session"]))
|
|
446
|
-
return self
|
|
447
|
-
|
|
448
|
-
|
|
449
370
|
# =============================================================================
|
|
450
371
|
# Scoped sub-resources — reached via a Server handle (server.tasks, ...).
|
|
451
372
|
# =============================================================================
|
|
@@ -466,6 +387,7 @@ class _ScopedProjects:
|
|
|
466
387
|
description: Optional[str] = None,
|
|
467
388
|
visibility: Optional[str] = None,
|
|
468
389
|
access: Optional[str] = None,
|
|
390
|
+
access_password: Optional[str] = None,
|
|
469
391
|
lifecycle: Optional[str] = None,
|
|
470
392
|
template: Optional[str] = None,
|
|
471
393
|
source: Optional[Mapping[str, Any]] = None,
|
|
@@ -483,6 +405,7 @@ class _ScopedProjects:
|
|
|
483
405
|
"description": description,
|
|
484
406
|
"visibility": visibility,
|
|
485
407
|
"access": access,
|
|
408
|
+
"access_password": access_password,
|
|
486
409
|
"lifecycle": lifecycle,
|
|
487
410
|
"template": template,
|
|
488
411
|
"source": dict(source) if source is not None else None,
|
|
@@ -662,27 +585,6 @@ def _logs(
|
|
|
662
585
|
return LogsSnapshot.parse(r["logs"]) if r.get("logs") is not None else None
|
|
663
586
|
|
|
664
587
|
|
|
665
|
-
def _report_tuning(
|
|
666
|
-
t: SyncTransport,
|
|
667
|
-
session_id: str,
|
|
668
|
-
change_summary: Optional[str],
|
|
669
|
-
changed_files: Optional[Sequence[str]],
|
|
670
|
-
deployment_url: Optional[str],
|
|
671
|
-
failed: Optional[bool],
|
|
672
|
-
) -> dict:
|
|
673
|
-
body = {
|
|
674
|
-
k: v
|
|
675
|
-
for k, v in {
|
|
676
|
-
"change_summary": change_summary,
|
|
677
|
-
"changed_files": list(changed_files) if changed_files is not None else None,
|
|
678
|
-
"deployment_url": deployment_url,
|
|
679
|
-
"failed": failed,
|
|
680
|
-
}.items()
|
|
681
|
-
if v is not None
|
|
682
|
-
}
|
|
683
|
-
return t.request("PATCH", f"/tuning/sessions/{_seg(session_id)}", body=body)
|
|
684
|
-
|
|
685
|
-
|
|
686
588
|
# =============================================================================
|
|
687
589
|
# Top-level resources — the flat client.servers.* API.
|
|
688
590
|
# =============================================================================
|
|
@@ -909,68 +811,6 @@ class SecretsResource:
|
|
|
909
811
|
return _ScopedSecrets(self._t, f"/projects/{_seg(project_id)}/secrets").set(secrets)
|
|
910
812
|
|
|
911
813
|
|
|
912
|
-
class TuningResource:
|
|
913
|
-
"""Tuning runs: fix an agent from an evaluation platform's report, then
|
|
914
|
-
report back what changed so the platform can score the fixed agent."""
|
|
915
|
-
|
|
916
|
-
def __init__(self, transport: SyncTransport) -> None:
|
|
917
|
-
self._t = transport
|
|
918
|
-
|
|
919
|
-
def list(
|
|
920
|
-
self, *, project_id: Optional[str] = None, status: Optional[TuningStatus] = None
|
|
921
|
-
) -> List[TuningSession]:
|
|
922
|
-
"""Newest first."""
|
|
923
|
-
r = self._t.request("GET", "/tuning/sessions", query={"project_id": project_id, "status": status})
|
|
924
|
-
return [TuningSession(self._t, s) for s in r["sessions"]]
|
|
925
|
-
|
|
926
|
-
def start(
|
|
927
|
-
self,
|
|
928
|
-
proposal_id: str,
|
|
929
|
-
*,
|
|
930
|
-
project_id: Optional[str] = None,
|
|
931
|
-
server_id: Optional[str] = None,
|
|
932
|
-
evaluation_api_base: Optional[str] = None,
|
|
933
|
-
key_secret: Optional[str] = None,
|
|
934
|
-
) -> TuningStart[TuningSession]:
|
|
935
|
-
"""Start fixing the agent behind an improvement proposal.
|
|
936
|
-
|
|
937
|
-
Pass ``project_id`` when the agent's code is already a project here, or
|
|
938
|
-
``server_id`` to build it into a new project on that server first. The
|
|
939
|
-
evaluation API key is read from the secret named ``key_secret``
|
|
940
|
-
(``AGENTX_API_KEY`` unless you name another); a scoped key may not set
|
|
941
|
-
``key_secret`` or ``evaluation_api_base``."""
|
|
942
|
-
body = {
|
|
943
|
-
k: v
|
|
944
|
-
for k, v in {
|
|
945
|
-
"proposal_id": proposal_id,
|
|
946
|
-
"project_id": project_id,
|
|
947
|
-
"server_id": server_id,
|
|
948
|
-
"evaluation_api_base": evaluation_api_base,
|
|
949
|
-
"key_secret": key_secret,
|
|
950
|
-
}.items()
|
|
951
|
-
if v is not None
|
|
952
|
-
}
|
|
953
|
-
r = self._t.request("POST", "/tuning/sessions", body=body)
|
|
954
|
-
return TuningStart.parse(TuningSession(self._t, r["session"]), r)
|
|
955
|
-
|
|
956
|
-
def get(self, tuning_session_id: str) -> TuningSession:
|
|
957
|
-
"""Read a session, which also advances it (see ``TuningSession``)."""
|
|
958
|
-
r = self._t.request("GET", f"/tuning/sessions/{_seg(tuning_session_id)}")
|
|
959
|
-
return TuningSession(self._t, r["session"], r.get("run"))
|
|
960
|
-
|
|
961
|
-
def report(
|
|
962
|
-
self,
|
|
963
|
-
tuning_session_id: str,
|
|
964
|
-
*,
|
|
965
|
-
change_summary: Optional[str] = None,
|
|
966
|
-
changed_files: Optional[Sequence[str]] = None,
|
|
967
|
-
deployment_url: Optional[str] = None,
|
|
968
|
-
failed: Optional[bool] = None,
|
|
969
|
-
) -> TuningSession:
|
|
970
|
-
r = _report_tuning(self._t, tuning_session_id, change_summary, changed_files, deployment_url, failed)
|
|
971
|
-
return TuningSession(self._t, r["session"])
|
|
972
|
-
|
|
973
|
-
|
|
974
814
|
class KeysResource:
|
|
975
815
|
def __init__(self, transport: SyncTransport) -> None:
|
|
976
816
|
self._t = transport
|
|
@@ -1061,7 +901,6 @@ class Server4Agent:
|
|
|
1061
901
|
self.builds = BuildsResource(self._t)
|
|
1062
902
|
self.files = FilesResource(self._t)
|
|
1063
903
|
self.secrets = SecretsResource(self._t)
|
|
1064
|
-
self.tuning = TuningResource(self._t)
|
|
1065
904
|
self.keys = KeysResource(self._t)
|
|
1066
905
|
self.webhooks = WebhooksResource(self._t)
|
|
1067
906
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.9.0"
|
|
@@ -24,7 +24,6 @@ from server4agent import (
|
|
|
24
24
|
Server,
|
|
25
25
|
Server4Agent,
|
|
26
26
|
Server4AgentError,
|
|
27
|
-
TuningStart,
|
|
28
27
|
WEBHOOK_EVENTS,
|
|
29
28
|
)
|
|
30
29
|
from server4agent._sync.client import (
|
|
@@ -804,97 +803,6 @@ def test_me(api):
|
|
|
804
803
|
assert me.authenticated_key == {"id": "key_1", "label": "ci"}
|
|
805
804
|
|
|
806
805
|
|
|
807
|
-
# --- tuning -------------------------------------------------------------------
|
|
808
|
-
|
|
809
|
-
def _tuning_raw(**over):
|
|
810
|
-
base = {
|
|
811
|
-
"id": "tun_1", "serverId": "srv_1", "projectId": "prj_1", "runId": "tsk_5",
|
|
812
|
-
"buildRunId": None, "proposalId": "prop_1",
|
|
813
|
-
"evaluationApiBase": "https://api.evals.example", "keySecretKey": "EVAL_API_KEY",
|
|
814
|
-
"briefPath": ".tuning/brief.md", "briefRevision": "r1", "status": "fixing",
|
|
815
|
-
"changeSummary": None, "changedFiles": [], "deploymentUrl": None,
|
|
816
|
-
"reportedAt": None, "reportError": None,
|
|
817
|
-
"createdAt": "2026-10-11T00:00:00Z", "updatedAt": "2026-10-11T00:00:00Z",
|
|
818
|
-
}
|
|
819
|
-
base.update(over)
|
|
820
|
-
return base
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
def test_tuning_list_with_filters(api):
|
|
824
|
-
c = api.client(_routes({("GET", "/tuning/sessions"): (200, {"sessions": [_tuning_raw()]})}))
|
|
825
|
-
sessions = api.call(c.tuning.list(project_id="prj_1", status="fixing"))
|
|
826
|
-
assert api.last["query"] == {"project_id": "prj_1", "status": "fixing"}
|
|
827
|
-
s = sessions[0]
|
|
828
|
-
assert (s.id, s.server_id, s.project_id, s.run_id, s.proposal_id) == ("tun_1", "srv_1", "prj_1", "tsk_5", "prop_1")
|
|
829
|
-
assert (s.key_secret, s.evaluation_api_base, s.status) == ("EVAL_API_KEY", "https://api.evals.example", "fixing")
|
|
830
|
-
assert s.run is None
|
|
831
|
-
|
|
832
|
-
api.call(c.tuning.list())
|
|
833
|
-
assert api.last["query"] == {}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
def test_tuning_start_new_and_already_running(api):
|
|
837
|
-
responses = iter([
|
|
838
|
-
(201, {"session": _tuning_raw(), "analysisReady": False, "warning": "platform not told"}),
|
|
839
|
-
(200, {"session": _tuning_raw(), "message": "A tuning run already exists for this proposal."}),
|
|
840
|
-
])
|
|
841
|
-
|
|
842
|
-
def handler(req):
|
|
843
|
-
status, body = next(responses)
|
|
844
|
-
return httpx.Response(status, json=body)
|
|
845
|
-
|
|
846
|
-
c = api.client(handler)
|
|
847
|
-
first = api.call(c.tuning.start("prop_1", project_id="prj_1", key_secret="EVAL_API_KEY"))
|
|
848
|
-
assert api.last["method"] == "POST" and api.last["path"] == "/tuning/sessions"
|
|
849
|
-
assert api.last["body"] == {"proposal_id": "prop_1", "project_id": "prj_1", "key_secret": "EVAL_API_KEY"}
|
|
850
|
-
assert isinstance(first, TuningStart)
|
|
851
|
-
assert first.started is True and first.analysis_ready is False and first.warning == "platform not told"
|
|
852
|
-
assert first.session.id == "tun_1"
|
|
853
|
-
|
|
854
|
-
again = api.call(c.tuning.start("prop_1", server_id="srv_1"))
|
|
855
|
-
assert api.last["body"] == {"proposal_id": "prop_1", "server_id": "srv_1"}
|
|
856
|
-
assert again.started is False and again.message.startswith("A tuning run already exists")
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
def test_tuning_get_carries_the_run_and_wait_polls_to_reported(api):
|
|
860
|
-
run = { # the run arrives in the run shape, keyed run_id
|
|
861
|
-
"run_id": "tsk_5", "server_id": "srv_1", "project_id": "prj_1", "goal": "Fix the agent",
|
|
862
|
-
"message_type": "system", "status": "running", "created_at": "t", "log": "reading brief",
|
|
863
|
-
"result": None, "question": None, "change": None, "usage": None,
|
|
864
|
-
}
|
|
865
|
-
states = iter(["fixing", "reported"])
|
|
866
|
-
|
|
867
|
-
def handler(req):
|
|
868
|
-
return httpx.Response(200, json={"session": _tuning_raw(status=next(states)), "run": run})
|
|
869
|
-
|
|
870
|
-
c = api.client(handler)
|
|
871
|
-
s = api.call(c.tuning.get(HOSTILE))
|
|
872
|
-
assert api.last["path"] == f"/tuning/sessions/{HOSTILE_SEG}"
|
|
873
|
-
assert s.status == "fixing"
|
|
874
|
-
assert (s.run.id, s.run.server_id, s.run.project_id, s.run.log) == ("tsk_5", "srv_1", "prj_1", "reading brief")
|
|
875
|
-
|
|
876
|
-
polled = []
|
|
877
|
-
done = api.call(s.wait(poll_interval=0, on_poll=lambda x: polled.append(x.status)))
|
|
878
|
-
assert done.status == "reported" and polled == ["reported"]
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
def test_tuning_report(api):
|
|
882
|
-
reported = _tuning_raw(status="reported", changeSummary="Grounded answers in the docs", changedFiles=["agent.py"])
|
|
883
|
-
c = api.client(_routes({
|
|
884
|
-
("PATCH", f"/tuning/sessions/{HOSTILE_SEG}"): (200, {"session": reported}),
|
|
885
|
-
("GET", "/tuning/sessions"): (200, {"sessions": [_tuning_raw(id=HOSTILE)]}),
|
|
886
|
-
}))
|
|
887
|
-
s = api.call(c.tuning.report(HOSTILE, change_summary="Grounded answers in the docs", changed_files=("agent.py",)))
|
|
888
|
-
assert api.last["method"] == "PATCH"
|
|
889
|
-
assert api.last["body"] == {"change_summary": "Grounded answers in the docs", "changed_files": ["agent.py"]}
|
|
890
|
-
assert s.status == "reported" and s.changed_files == ["agent.py"]
|
|
891
|
-
|
|
892
|
-
handle = api.call(c.tuning.list())[0]
|
|
893
|
-
api.call(handle.report(failed=True))
|
|
894
|
-
assert api.last["body"] == {"failed": True}
|
|
895
|
-
assert handle.status == "reported"
|
|
896
|
-
|
|
897
|
-
|
|
898
806
|
# --- drift fixed in existing methods ------------------------------------------
|
|
899
807
|
|
|
900
808
|
def test_deployments_parse_with_project_id(api):
|
|
@@ -947,7 +855,8 @@ def test_webhooks_create_without_events_omits_them(api):
|
|
|
947
855
|
})}))
|
|
948
856
|
api.call(c.webhooks.create("https://hooks.northstar.example/s4a"))
|
|
949
857
|
assert api.last["body"] == {"url": "https://hooks.northstar.example/s4a"}
|
|
950
|
-
assert "
|
|
858
|
+
assert "quota.egress_cap_reached" in WEBHOOK_EVENTS
|
|
859
|
+
assert "tuning.started" not in WEBHOOK_EVENTS and "tuning.completed" not in WEBHOOK_EVENTS
|
|
951
860
|
|
|
952
861
|
|
|
953
862
|
def test_exec_in_a_project_folder(api):
|
|
@@ -1028,6 +937,22 @@ def test_project_update_run_plan_and_refresh_keeps_init_run_id(api):
|
|
|
1028
937
|
assert project.init_run_id == "tsk_3" # only the create response carries it
|
|
1029
938
|
|
|
1030
939
|
|
|
940
|
+
def test_project_access_password_goes_out_as_access_password(api):
|
|
941
|
+
c = api.client(_routes({
|
|
942
|
+
("POST", "/servers/srv_1/projects"): (201, _project_raw(access="password")),
|
|
943
|
+
("PATCH", "/projects/prj_1"): (200, _project_raw(access="password")),
|
|
944
|
+
}))
|
|
945
|
+
project = api.call(c.projects.create(
|
|
946
|
+
"srv_1", name="web", access="password", access_password="correct horse battery",
|
|
947
|
+
))
|
|
948
|
+
assert api.last["body"] == {
|
|
949
|
+
"name": "web", "access": "password", "access_password": "correct horse battery",
|
|
950
|
+
}
|
|
951
|
+
assert project.access == "password"
|
|
952
|
+
api.call(project.update(access_password="a new passphrase"))
|
|
953
|
+
assert api.last["method"] == "PATCH" and api.last["body"] == {"access_password": "a new passphrase"}
|
|
954
|
+
|
|
955
|
+
|
|
1031
956
|
def test_template_source_kind_and_failed_build_steps(api):
|
|
1032
957
|
c = api.client(_routes({
|
|
1033
958
|
("GET", "/templates"): (200, {"templates": [{
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.7.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|