vilvik 0.2.2__tar.gz → 0.4.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.
- {vilvik-0.2.2 → vilvik-0.4.0}/PKG-INFO +1 -1
- {vilvik-0.2.2 → vilvik-0.4.0}/tests/test_client.py +98 -5
- vilvik-0.4.0/vilvik/_version.py +1 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/vilvik/client.py +51 -2
- {vilvik-0.2.2 → vilvik-0.4.0}/vilvik/models.py +19 -8
- {vilvik-0.2.2 → vilvik-0.4.0}/vilvik.egg-info/PKG-INFO +1 -1
- vilvik-0.2.2/vilvik/_version.py +0 -1
- {vilvik-0.2.2 → vilvik-0.4.0}/LICENSE +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/README.md +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/pyproject.toml +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/setup.cfg +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/tests/test_capture.py +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/tests/test_credentials.py +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/tests/test_extract.py +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/tests/test_imports_resource.py +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/tests/test_login.py +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/tests/test_public_api.py +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/tests/test_push.py +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/vilvik/__init__.py +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/vilvik/__main__.py +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/vilvik/_capture.py +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/vilvik/_credentials.py +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/vilvik/_extract.py +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/vilvik/_http.py +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/vilvik/exceptions.py +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/vilvik/login.py +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/vilvik/push.py +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/vilvik/run.py +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/vilvik.egg-info/SOURCES.txt +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/vilvik.egg-info/dependency_links.txt +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/vilvik.egg-info/entry_points.txt +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/vilvik.egg-info/requires.txt +0 -0
- {vilvik-0.2.2 → vilvik-0.4.0}/vilvik.egg-info/top_level.txt +0 -0
|
@@ -86,6 +86,46 @@ def test_submissions_create_forwards_extra_ga_params(mock_api, client):
|
|
|
86
86
|
assert body["parent_selection_type"] == "tournament"
|
|
87
87
|
|
|
88
88
|
|
|
89
|
+
def test_submissions_create_auto_detects_entry_symbol(mock_api, client):
|
|
90
|
+
"""The entry symbol is detected from the source and sent, so callers
|
|
91
|
+
don't have to repeat the function name. The runtime requires it."""
|
|
92
|
+
mock_api.add("POST", f"{BASE}/submissions",
|
|
93
|
+
json={"id": "sub_e", "status": "queued"}, status=202)
|
|
94
|
+
client.submissions.create(
|
|
95
|
+
fitness_func="def my_fitness(g, s, i):\n return sum(s)\n",
|
|
96
|
+
num_genes=3,
|
|
97
|
+
)
|
|
98
|
+
body = json.loads(mock_api.calls[0].request.body)
|
|
99
|
+
assert body["fitness_func_entry"] == "my_fitness"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_submissions_create_honors_explicit_entry_symbol(mock_api, client):
|
|
103
|
+
"""An explicit <role>_entry is never overridden by detection."""
|
|
104
|
+
mock_api.add("POST", f"{BASE}/submissions",
|
|
105
|
+
json={"id": "sub_e2", "status": "queued"}, status=202)
|
|
106
|
+
client.submissions.create(
|
|
107
|
+
fitness_func="def a(g, s, i): return 0\ndef b(g, s, i): return 1\n",
|
|
108
|
+
fitness_func_entry="a",
|
|
109
|
+
num_genes=3,
|
|
110
|
+
)
|
|
111
|
+
body = json.loads(mock_api.calls[0].request.body)
|
|
112
|
+
assert body["fitness_func_entry"] == "a"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_submissions_create_detects_entry_for_callbacks(mock_api, client):
|
|
116
|
+
"""Callback code fields passed as source also get their entry symbol."""
|
|
117
|
+
mock_api.add("POST", f"{BASE}/submissions",
|
|
118
|
+
json={"id": "sub_e3", "status": "queued"}, status=202)
|
|
119
|
+
client.submissions.create(
|
|
120
|
+
fitness_func="def fitness_func(g, s, i): return 0",
|
|
121
|
+
on_generation="def log_gen(ga):\n pass\n",
|
|
122
|
+
num_genes=3,
|
|
123
|
+
)
|
|
124
|
+
body = json.loads(mock_api.calls[0].request.body)
|
|
125
|
+
assert body["fitness_func_entry"] == "fitness_func"
|
|
126
|
+
assert body["on_generation_entry"] == "log_gen"
|
|
127
|
+
|
|
128
|
+
|
|
89
129
|
def test_submissions_get(mock_api, client):
|
|
90
130
|
mock_api.add(
|
|
91
131
|
"GET",
|
|
@@ -173,6 +213,44 @@ def test_results_continue_run(mock_api, client):
|
|
|
173
213
|
assert body == {"sol_per_pop": 200}
|
|
174
214
|
|
|
175
215
|
|
|
216
|
+
def test_results_delete(mock_api, client):
|
|
217
|
+
mock_api.add("DELETE", f"{BASE}/results/res_1", status=204)
|
|
218
|
+
# Must not raise and returns None on 204.
|
|
219
|
+
assert client.results.delete("res_1") is None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_results_update_sends_patch_and_returns_result(mock_api, client):
|
|
223
|
+
mock_api.add(
|
|
224
|
+
"PATCH",
|
|
225
|
+
f"{BASE}/results/res_1",
|
|
226
|
+
json={
|
|
227
|
+
"id": "res_1",
|
|
228
|
+
"submission_id": "sub_abc",
|
|
229
|
+
"name": "renamed result",
|
|
230
|
+
"is_shared": True,
|
|
231
|
+
},
|
|
232
|
+
)
|
|
233
|
+
r = client.results.update("res_1", name="renamed result", is_shared=True)
|
|
234
|
+
assert mock_api.calls[0].request.method == "PATCH"
|
|
235
|
+
body = json.loads(mock_api.calls[0].request.body)
|
|
236
|
+
assert body == {"name": "renamed result", "is_shared": True}
|
|
237
|
+
assert r.name == "renamed result"
|
|
238
|
+
assert r.is_shared is True
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def test_results_update_sends_only_provided_fields(mock_api, client):
|
|
242
|
+
"""Partial update: passing only is_shared=False must send just that key
|
|
243
|
+
(False is a real value, not 'unset')."""
|
|
244
|
+
mock_api.add(
|
|
245
|
+
"PATCH",
|
|
246
|
+
f"{BASE}/results/res_1",
|
|
247
|
+
json={"id": "res_1", "submission_id": "sub_abc", "is_shared": False},
|
|
248
|
+
)
|
|
249
|
+
client.results.update("res_1", is_shared=False)
|
|
250
|
+
body = json.loads(mock_api.calls[0].request.body)
|
|
251
|
+
assert body == {"is_shared": False}
|
|
252
|
+
|
|
253
|
+
|
|
176
254
|
def test_wait_for_returns_result_when_submission_succeeds(mock_api, client, monkeypatch):
|
|
177
255
|
monkeypatch.setattr("vilvik.client.time.sleep", lambda *_a, **_k: None)
|
|
178
256
|
# First poll: still running. Second poll: succeeded. Then list returns one row.
|
|
@@ -235,15 +313,30 @@ def test_code_upload_create(mock_api, client):
|
|
|
235
313
|
mock_api.add(
|
|
236
314
|
"POST",
|
|
237
315
|
f"{BASE}/code-uploads",
|
|
238
|
-
json={"
|
|
316
|
+
json={"code_id": "code_1", "content_size": 42, "is_expired": False},
|
|
239
317
|
status=201,
|
|
240
318
|
)
|
|
241
319
|
blob = client.code_uploads.create(
|
|
242
|
-
|
|
243
|
-
code="def fitness_func(g, s, i): return 0",
|
|
320
|
+
content="def fitness_func(g, s, i): return 0",
|
|
244
321
|
)
|
|
245
|
-
assert blob.
|
|
246
|
-
assert blob.
|
|
322
|
+
assert blob.code_id == "code_1"
|
|
323
|
+
assert blob.content_size == 42
|
|
324
|
+
# The request sends the source under `content`, matching the REST API.
|
|
325
|
+
body = json.loads(mock_api.calls[0].request.body)
|
|
326
|
+
assert body == {"content": "def fitness_func(g, s, i): return 0"}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def test_code_upload_referenced_in_submission(mock_api, client):
|
|
330
|
+
"""An uploaded blob is referenced by `<role>_id`, using its code_id."""
|
|
331
|
+
mock_api.add("POST", f"{BASE}/submissions",
|
|
332
|
+
json={"id": "sub_ref", "status": "queued"}, status=202)
|
|
333
|
+
client.submissions.create(
|
|
334
|
+
fitness_func_id="code_1",
|
|
335
|
+
fitness_func_entry="fitness_func",
|
|
336
|
+
num_genes=3,
|
|
337
|
+
)
|
|
338
|
+
body = json.loads(mock_api.calls[0].request.body)
|
|
339
|
+
assert body["fitness_func_id"] == "code_1"
|
|
247
340
|
|
|
248
341
|
|
|
249
342
|
def test_webhooks_list(mock_api, client):
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
|
@@ -12,6 +12,7 @@ import os
|
|
|
12
12
|
import time
|
|
13
13
|
from typing import Any, Dict, Iterable, Iterator, List, Optional
|
|
14
14
|
|
|
15
|
+
from vilvik._capture import CODE_ROLES, _detect_entry_from_source
|
|
15
16
|
from vilvik._http import DEFAULT_BASE_URL, DEFAULT_TIMEOUT_SECONDS, Transport
|
|
16
17
|
from vilvik.exceptions import TimeoutError as VilvikTimeout
|
|
17
18
|
from vilvik.models import CodeUpload, ImportRecord, Page, Result, Submission, Webhook
|
|
@@ -67,6 +68,24 @@ class Submissions(_Resource):
|
|
|
67
68
|
body["notification_email"] = notification_email
|
|
68
69
|
body.update({k: v for k, v in ga_params.items() if v is not None})
|
|
69
70
|
|
|
71
|
+
# Fill in the entry symbol for each code field, the way the website's
|
|
72
|
+
# new-submission form does. The runtime requires the top-level name to
|
|
73
|
+
# call and no longer guesses, so a code field sent without its
|
|
74
|
+
# `<role>_entry` is rejected by the server. Detect it from the source
|
|
75
|
+
# (the last top-level def/class), but never override an entry the
|
|
76
|
+
# caller passed explicitly. If detection fails we leave it unset and
|
|
77
|
+
# let the server return its clear "entry symbol is required" error.
|
|
78
|
+
for role in CODE_ROLES:
|
|
79
|
+
source = body.get(role)
|
|
80
|
+
if not isinstance(source, str) or not source.strip():
|
|
81
|
+
continue
|
|
82
|
+
entry_key = f"{role}_entry"
|
|
83
|
+
if body.get(entry_key):
|
|
84
|
+
continue
|
|
85
|
+
detected = _detect_entry_from_source(source)
|
|
86
|
+
if detected:
|
|
87
|
+
body[entry_key] = detected
|
|
88
|
+
|
|
70
89
|
payload = self._t.request(
|
|
71
90
|
"POST",
|
|
72
91
|
"/submissions",
|
|
@@ -141,6 +160,30 @@ class Results(_Resource):
|
|
|
141
160
|
items = [Result.from_api(row) for row in payload.get("data", [])]
|
|
142
161
|
return Page(items=items, next_cursor=payload.get("next_cursor"), raw=payload)
|
|
143
162
|
|
|
163
|
+
def update(
|
|
164
|
+
self,
|
|
165
|
+
result_id: str,
|
|
166
|
+
*,
|
|
167
|
+
name: Optional[str] = None,
|
|
168
|
+
is_shared: Optional[bool] = None,
|
|
169
|
+
) -> Result:
|
|
170
|
+
"""PATCH /results/{id} — rename a result or change its shared flag.
|
|
171
|
+
|
|
172
|
+
Only the fields you pass are sent, so `is_shared=False` updates just
|
|
173
|
+
that flag. Returns the updated result.
|
|
174
|
+
"""
|
|
175
|
+
body: Dict[str, Any] = {}
|
|
176
|
+
if name is not None:
|
|
177
|
+
body["name"] = name
|
|
178
|
+
if is_shared is not None:
|
|
179
|
+
body["is_shared"] = is_shared
|
|
180
|
+
payload = self._t.request("PATCH", f"/results/{result_id}", json_body=body)
|
|
181
|
+
return Result.from_api(payload)
|
|
182
|
+
|
|
183
|
+
def delete(self, result_id: str) -> None:
|
|
184
|
+
"""DELETE /results/{id} — remove a result. The submission is untouched."""
|
|
185
|
+
self._t.request("DELETE", f"/results/{result_id}")
|
|
186
|
+
|
|
144
187
|
def continue_run(
|
|
145
188
|
self,
|
|
146
189
|
result_id: str,
|
|
@@ -212,11 +255,17 @@ class Results(_Resource):
|
|
|
212
255
|
class CodeUploads(_Resource):
|
|
213
256
|
"""Endpoints under `/api/v1/code-uploads`."""
|
|
214
257
|
|
|
215
|
-
def create(self, *,
|
|
258
|
+
def create(self, *, content: str) -> CodeUpload:
|
|
259
|
+
"""Upload a code blob and get back a `code_id` to reference.
|
|
260
|
+
|
|
261
|
+
The upload is role-agnostic. Reference it from a submission via the
|
|
262
|
+
matching `<role>_id` parameter, e.g. passing the returned
|
|
263
|
+
`code_id` as `fitness_func_id=...` to `submissions.create`.
|
|
264
|
+
"""
|
|
216
265
|
payload = self._t.request(
|
|
217
266
|
"POST",
|
|
218
267
|
"/code-uploads",
|
|
219
|
-
json_body={"
|
|
268
|
+
json_body={"content": content},
|
|
220
269
|
)
|
|
221
270
|
return CodeUpload.from_api(payload)
|
|
222
271
|
|
|
@@ -70,6 +70,8 @@ class Result:
|
|
|
70
70
|
|
|
71
71
|
id: str
|
|
72
72
|
submission_id: str
|
|
73
|
+
name: Optional[str] = None
|
|
74
|
+
is_shared: Optional[bool] = None
|
|
73
75
|
best_fitness: Optional[float] = None
|
|
74
76
|
best_solution: Optional[List[Any]] = None
|
|
75
77
|
num_generations_ran: Optional[int] = None
|
|
@@ -82,6 +84,8 @@ class Result:
|
|
|
82
84
|
return cls(
|
|
83
85
|
id=str(data.get("id", "")),
|
|
84
86
|
submission_id=str(data.get("submission_id", "")),
|
|
87
|
+
name=data.get("name"),
|
|
88
|
+
is_shared=data.get("is_shared"),
|
|
85
89
|
best_fitness=data.get("best_fitness"),
|
|
86
90
|
best_solution=data.get("best_solution"),
|
|
87
91
|
num_generations_ran=data.get("num_generations_ran"),
|
|
@@ -95,23 +99,30 @@ class Result:
|
|
|
95
99
|
class CodeUpload:
|
|
96
100
|
"""A reusable code blob that submissions can reference by id.
|
|
97
101
|
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
Uploads are role-agnostic: the server stores the source and returns a
|
|
103
|
+
`code_id`. You choose which role it fills when you reference it from a
|
|
104
|
+
submission, e.g. `fitness_func_id=upload.code_id`.
|
|
100
105
|
"""
|
|
101
106
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
107
|
+
code_id: str
|
|
108
|
+
content: Optional[str] = None
|
|
109
|
+
content_size: Optional[int] = None
|
|
110
|
+
content_sha256: Optional[str] = None
|
|
105
111
|
created_at: Optional[datetime] = None
|
|
112
|
+
expires_at: Optional[datetime] = None
|
|
113
|
+
is_expired: Optional[bool] = None
|
|
106
114
|
raw: Dict[str, Any] = field(default_factory=dict)
|
|
107
115
|
|
|
108
116
|
@classmethod
|
|
109
117
|
def from_api(cls, data: Dict[str, Any]) -> "CodeUpload":
|
|
110
118
|
return cls(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
119
|
+
code_id=str(data.get("code_id", "")),
|
|
120
|
+
content=data.get("content"),
|
|
121
|
+
content_size=data.get("content_size"),
|
|
122
|
+
content_sha256=data.get("content_sha256"),
|
|
114
123
|
created_at=_parse_dt(data.get("created_at")),
|
|
124
|
+
expires_at=_parse_dt(data.get("expires_at")),
|
|
125
|
+
is_expired=data.get("is_expired"),
|
|
115
126
|
raw=dict(data),
|
|
116
127
|
)
|
|
117
128
|
|
vilvik-0.2.2/vilvik/_version.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.2"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|