vilvik 0.1.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.1.0/PKG-INFO ADDED
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.4
2
+ Name: vilvik
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the Vilvik genetic-algorithm cloud API.
5
+ Author-email: Vilvik <ahmed.f.gad@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://vilvik.com
8
+ Project-URL: Documentation, https://vilvik.com/docs
9
+ Project-URL: Repository, https://github.com/ahmedfgad/vilvik
10
+ Project-URL: Issues, https://github.com/ahmedfgad/vilvik/issues
11
+ Keywords: genetic-algorithm,optimization,pygad,vilvik
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ Requires-Dist: requests>=2.28
26
+ Provides-Extra: async
27
+ Requires-Dist: httpx>=0.24; extra == "async"
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7; extra == "dev"
30
+ Requires-Dist: pytest-cov>=4; extra == "dev"
31
+ Requires-Dist: responses>=0.23; extra == "dev"
32
+
33
+ # vilvik
34
+
35
+ Official Python SDK for [Vilvik](https://vilvik.com) — a cloud platform that
36
+ runs Genetic Algorithm (PyGAD) workloads with a REST API, scoped keys, and
37
+ webhook delivery.
38
+
39
+ ```bash
40
+ pip install vilvik
41
+ ```
42
+
43
+ ## Quick start
44
+
45
+ ```python
46
+ import vilvik
47
+
48
+ client = vilvik.Client(api_key="vlk_live_…")
49
+
50
+ submission = client.submissions.create(
51
+ fitness_func="""
52
+ def fitness_func(ga_instance, solution, idx):
53
+ return -sum(s * s for s in solution)
54
+ """,
55
+ num_genes=5,
56
+ num_generations=100,
57
+ sol_per_pop=50,
58
+ name="quadratic-minimisation",
59
+ )
60
+
61
+ print(submission.id, submission.status) # "abc123…", "queued"
62
+
63
+ result = client.results.wait_for(submission.id, timeout=300)
64
+ print(result.best_fitness, result.best_solution)
65
+ ```
66
+
67
+ ## The `run()` one-liner
68
+
69
+ For scripts and notebook cells, `vilvik.run(...)` packages create-and-wait
70
+ into a context manager that also cancels the run if you exit the block
71
+ early:
72
+
73
+ ```python
74
+ import vilvik
75
+
76
+ fn = """
77
+ def fitness_func(ga_instance, solution, idx):
78
+ return -sum(s * s for s in solution)
79
+ """
80
+
81
+ with vilvik.run(fitness_func=fn, num_genes=5, num_generations=50) as result:
82
+ print(result.best_fitness)
83
+ ```
84
+
85
+ The API key is read from `VILVIK_API_KEY` if you do not pass it
86
+ explicitly.
87
+
88
+ ## Listing and pagination
89
+
90
+ The list endpoints return a `Page` whose items are typed dataclasses; for
91
+ walking everything use `iter_all`:
92
+
93
+ ```python
94
+ page = client.submissions.list(limit=25)
95
+ for sub in page:
96
+ print(sub.id, sub.status, sub.name)
97
+
98
+ # All submissions, transparently following the cursor:
99
+ for sub in client.submissions.iter_all():
100
+ ...
101
+ ```
102
+
103
+ ## Branching a finished run
104
+
105
+ `B7 — branching run-graph` is exposed via `Results.continue_run`. Each
106
+ call creates a child submission whose `parent_submission` is the result
107
+ you forked from. Pass any GA parameter you want to override:
108
+
109
+ ```python
110
+ parent = client.results.get(result_id)
111
+ variant_a = client.results.continue_run(parent.id, mutation_probability=0.10)
112
+ variant_b = client.results.continue_run(parent.id, sol_per_pop=100)
113
+ ```
114
+
115
+ The Vilvik dashboard renders the resulting lineage as an interactive tree
116
+ on the result page.
117
+
118
+ ## Errors
119
+
120
+ Every SDK error inherits from `vilvik.VilvikError`. Subclasses let you
121
+ catch specific failure modes:
122
+
123
+ ```python
124
+ try:
125
+ client.submissions.get("does-not-exist")
126
+ except vilvik.NotFoundError as e:
127
+ print("Not found:", e.request_id)
128
+ except vilvik.RateLimitError as e:
129
+ print("Slow down, retry after", e.retry_after, "seconds")
130
+ except vilvik.AuthenticationError:
131
+ print("Check your API key and scopes")
132
+ except vilvik.VilvikError:
133
+ print("Something else went wrong")
134
+ ```
135
+
136
+ ## Configuration
137
+
138
+ | Argument | Default | Notes |
139
+ | --------------- | ---------------------------------- | -------------------------------------------------- |
140
+ | `api_key` | `os.environ["VILVIK_API_KEY"]` | Required. Bearer token created in the dashboard. |
141
+ | `base_url` | `https://vilvik.com/api/v1` | Override for staging or self-hosted instances. |
142
+ | `timeout` | `60.0` seconds | Per-HTTP-request timeout. |
143
+ | `max_retries` | `2` | Idempotent (GET / HEAD) retries on network errors. |
144
+
145
+ ## Development
146
+
147
+ ```bash
148
+ pip install -e ".[dev]"
149
+ pytest
150
+ ```
151
+
152
+ ## License
153
+
154
+ MIT.
vilvik-0.1.0/README.md ADDED
@@ -0,0 +1,122 @@
1
+ # vilvik
2
+
3
+ Official Python SDK for [Vilvik](https://vilvik.com) — a cloud platform that
4
+ runs Genetic Algorithm (PyGAD) workloads with a REST API, scoped keys, and
5
+ webhook delivery.
6
+
7
+ ```bash
8
+ pip install vilvik
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```python
14
+ import vilvik
15
+
16
+ client = vilvik.Client(api_key="vlk_live_…")
17
+
18
+ submission = client.submissions.create(
19
+ fitness_func="""
20
+ def fitness_func(ga_instance, solution, idx):
21
+ return -sum(s * s for s in solution)
22
+ """,
23
+ num_genes=5,
24
+ num_generations=100,
25
+ sol_per_pop=50,
26
+ name="quadratic-minimisation",
27
+ )
28
+
29
+ print(submission.id, submission.status) # "abc123…", "queued"
30
+
31
+ result = client.results.wait_for(submission.id, timeout=300)
32
+ print(result.best_fitness, result.best_solution)
33
+ ```
34
+
35
+ ## The `run()` one-liner
36
+
37
+ For scripts and notebook cells, `vilvik.run(...)` packages create-and-wait
38
+ into a context manager that also cancels the run if you exit the block
39
+ early:
40
+
41
+ ```python
42
+ import vilvik
43
+
44
+ fn = """
45
+ def fitness_func(ga_instance, solution, idx):
46
+ return -sum(s * s for s in solution)
47
+ """
48
+
49
+ with vilvik.run(fitness_func=fn, num_genes=5, num_generations=50) as result:
50
+ print(result.best_fitness)
51
+ ```
52
+
53
+ The API key is read from `VILVIK_API_KEY` if you do not pass it
54
+ explicitly.
55
+
56
+ ## Listing and pagination
57
+
58
+ The list endpoints return a `Page` whose items are typed dataclasses; for
59
+ walking everything use `iter_all`:
60
+
61
+ ```python
62
+ page = client.submissions.list(limit=25)
63
+ for sub in page:
64
+ print(sub.id, sub.status, sub.name)
65
+
66
+ # All submissions, transparently following the cursor:
67
+ for sub in client.submissions.iter_all():
68
+ ...
69
+ ```
70
+
71
+ ## Branching a finished run
72
+
73
+ `B7 — branching run-graph` is exposed via `Results.continue_run`. Each
74
+ call creates a child submission whose `parent_submission` is the result
75
+ you forked from. Pass any GA parameter you want to override:
76
+
77
+ ```python
78
+ parent = client.results.get(result_id)
79
+ variant_a = client.results.continue_run(parent.id, mutation_probability=0.10)
80
+ variant_b = client.results.continue_run(parent.id, sol_per_pop=100)
81
+ ```
82
+
83
+ The Vilvik dashboard renders the resulting lineage as an interactive tree
84
+ on the result page.
85
+
86
+ ## Errors
87
+
88
+ Every SDK error inherits from `vilvik.VilvikError`. Subclasses let you
89
+ catch specific failure modes:
90
+
91
+ ```python
92
+ try:
93
+ client.submissions.get("does-not-exist")
94
+ except vilvik.NotFoundError as e:
95
+ print("Not found:", e.request_id)
96
+ except vilvik.RateLimitError as e:
97
+ print("Slow down, retry after", e.retry_after, "seconds")
98
+ except vilvik.AuthenticationError:
99
+ print("Check your API key and scopes")
100
+ except vilvik.VilvikError:
101
+ print("Something else went wrong")
102
+ ```
103
+
104
+ ## Configuration
105
+
106
+ | Argument | Default | Notes |
107
+ | --------------- | ---------------------------------- | -------------------------------------------------- |
108
+ | `api_key` | `os.environ["VILVIK_API_KEY"]` | Required. Bearer token created in the dashboard. |
109
+ | `base_url` | `https://vilvik.com/api/v1` | Override for staging or self-hosted instances. |
110
+ | `timeout` | `60.0` seconds | Per-HTTP-request timeout. |
111
+ | `max_retries` | `2` | Idempotent (GET / HEAD) retries on network errors. |
112
+
113
+ ## Development
114
+
115
+ ```bash
116
+ pip install -e ".[dev]"
117
+ pytest
118
+ ```
119
+
120
+ ## License
121
+
122
+ MIT.
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vilvik"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for the Vilvik genetic-algorithm cloud API."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Vilvik", email = "ahmed.f.gad@gmail.com" }]
13
+ keywords = ["genetic-algorithm", "optimization", "pygad", "vilvik"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: Science/Research",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
26
+ ]
27
+ dependencies = [
28
+ "requests>=2.28",
29
+ ]
30
+
31
+ [project.optional-dependencies]
32
+ async = ["httpx>=0.24"]
33
+ dev = [
34
+ "pytest>=7",
35
+ "pytest-cov>=4",
36
+ "responses>=0.23",
37
+ ]
38
+
39
+ [project.urls]
40
+ Homepage = "https://vilvik.com"
41
+ Documentation = "https://vilvik.com/docs"
42
+ Repository = "https://github.com/ahmedfgad/vilvik"
43
+ Issues = "https://github.com/ahmedfgad/vilvik/issues"
44
+
45
+ [tool.setuptools.packages.find]
46
+ include = ["vilvik*"]
47
+ exclude = ["tests*"]
vilvik-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,379 @@
1
+ """Unit tests for `vilvik.Client` and its resource sub-clients."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+
8
+ import pytest
9
+
10
+ import vilvik
11
+ from vilvik.client import Client
12
+
13
+ BASE = "https://example.test/api/v1"
14
+
15
+
16
+ # --------------- Construction / config ---------------
17
+
18
+
19
+ def test_client_requires_api_key(monkeypatch):
20
+ monkeypatch.delenv("VILVIK_API_KEY", raising=False)
21
+ with pytest.raises(ValueError):
22
+ Client()
23
+
24
+
25
+ def test_client_reads_api_key_from_env(monkeypatch):
26
+ monkeypatch.setenv("VILVIK_API_KEY", "vlk_from_env")
27
+ c = Client(base_url=BASE)
28
+ # Smoke check: no exception means the env var was picked up.
29
+ assert c.base_url == BASE
30
+
31
+
32
+ # --------------- Submissions ---------------
33
+
34
+
35
+ def test_submissions_create_round_trip(mock_api, client):
36
+ mock_api.add(
37
+ "POST",
38
+ f"{BASE}/submissions",
39
+ json={
40
+ "id": "sub_abc",
41
+ "status": "queued",
42
+ "status_url": f"{BASE}/submissions/sub_abc",
43
+ "result_url": f"{BASE}/results?submission_id=sub_abc",
44
+ "created_at": "2026-05-18T10:00:00Z",
45
+ "request_id": "req_1",
46
+ },
47
+ status=202,
48
+ )
49
+
50
+ sub = client.submissions.create(
51
+ fitness_func="def fitness_func(g, s, i): return 0",
52
+ num_genes=3,
53
+ num_generations=10,
54
+ sol_per_pop=20,
55
+ )
56
+
57
+ assert sub.id == "sub_abc"
58
+ assert sub.status == "queued"
59
+ assert not sub.is_terminal
60
+ assert sub.created_at is not None
61
+
62
+ call = mock_api.calls[0]
63
+ assert call.request.headers["Authorization"] == f"Bearer {client._transport.api_key}"
64
+ assert call.request.headers["Idempotency-Key"]
65
+ body = json.loads(call.request.body)
66
+ assert body["num_genes"] == 3
67
+ assert body["num_generations"] == 10
68
+ assert body["fitness_func"].startswith("def fitness_func")
69
+
70
+
71
+ def test_submissions_create_forwards_extra_ga_params(mock_api, client):
72
+ mock_api.add(
73
+ "POST",
74
+ f"{BASE}/submissions",
75
+ json={"id": "sub_x", "status": "queued"},
76
+ status=202,
77
+ )
78
+ client.submissions.create(
79
+ fitness_func="x",
80
+ num_genes=1,
81
+ mutation_probability=0.05,
82
+ parent_selection_type="tournament",
83
+ )
84
+ body = json.loads(mock_api.calls[0].request.body)
85
+ assert body["mutation_probability"] == 0.05
86
+ assert body["parent_selection_type"] == "tournament"
87
+
88
+
89
+ def test_submissions_get(mock_api, client):
90
+ mock_api.add(
91
+ "GET",
92
+ f"{BASE}/submissions/sub_abc",
93
+ json={"id": "sub_abc", "status": "succeeded"},
94
+ )
95
+ sub = client.submissions.get("sub_abc")
96
+ assert sub.is_terminal
97
+ assert sub.status == "succeeded"
98
+
99
+
100
+ def test_submissions_list_and_iter_all(mock_api, client):
101
+ mock_api.add(
102
+ "GET",
103
+ f"{BASE}/submissions",
104
+ json={
105
+ "data": [
106
+ {"id": "a", "status": "queued"},
107
+ {"id": "b", "status": "running"},
108
+ ],
109
+ "next_cursor": "cur_2",
110
+ },
111
+ )
112
+ mock_api.add(
113
+ "GET",
114
+ f"{BASE}/submissions",
115
+ json={
116
+ "data": [{"id": "c", "status": "succeeded"}],
117
+ "next_cursor": None,
118
+ },
119
+ )
120
+ ids = [s.id for s in client.submissions.iter_all()]
121
+ assert ids == ["a", "b", "c"]
122
+
123
+
124
+ def test_submissions_reexecute_sends_overrides(mock_api, client):
125
+ mock_api.add(
126
+ "POST",
127
+ f"{BASE}/submissions/sub_abc/reexecute",
128
+ json={"id": "sub_def", "status": "queued"},
129
+ status=202,
130
+ )
131
+ client.submissions.reexecute("sub_abc", mutation_probability=0.2)
132
+ body = json.loads(mock_api.calls[0].request.body)
133
+ assert body == {"mutation_probability": 0.2}
134
+
135
+
136
+ def test_submissions_delete(mock_api, client):
137
+ mock_api.add("DELETE", f"{BASE}/submissions/sub_abc", status=204)
138
+ # Must not raise.
139
+ assert client.submissions.delete("sub_abc") is None
140
+
141
+
142
+ # --------------- Results ---------------
143
+
144
+
145
+ def test_results_get(mock_api, client):
146
+ mock_api.add(
147
+ "GET",
148
+ f"{BASE}/results/res_1",
149
+ json={
150
+ "id": "res_1",
151
+ "submission_id": "sub_abc",
152
+ "best_fitness": -0.0123,
153
+ "best_solution": [1, 2, 3],
154
+ "num_generations_ran": 100,
155
+ },
156
+ )
157
+ r = client.results.get("res_1")
158
+ assert r.best_fitness == pytest.approx(-0.0123)
159
+ assert r.best_solution == [1, 2, 3]
160
+ assert r.num_generations_ran == 100
161
+
162
+
163
+ def test_results_continue_run(mock_api, client):
164
+ mock_api.add(
165
+ "POST",
166
+ f"{BASE}/results/res_1/continue",
167
+ json={"id": "sub_child", "status": "queued"},
168
+ status=202,
169
+ )
170
+ child = client.results.continue_run("res_1", sol_per_pop=200)
171
+ assert child.id == "sub_child"
172
+ body = json.loads(mock_api.calls[0].request.body)
173
+ assert body == {"sol_per_pop": 200}
174
+
175
+
176
+ def test_wait_for_returns_result_when_submission_succeeds(mock_api, client, monkeypatch):
177
+ monkeypatch.setattr("vilvik.client.time.sleep", lambda *_a, **_k: None)
178
+ # First poll: still running. Second poll: succeeded. Then list returns one row.
179
+ mock_api.add(
180
+ "GET",
181
+ f"{BASE}/submissions/sub_abc",
182
+ json={"id": "sub_abc", "status": "running"},
183
+ )
184
+ mock_api.add(
185
+ "GET",
186
+ f"{BASE}/submissions/sub_abc",
187
+ json={"id": "sub_abc", "status": "succeeded"},
188
+ )
189
+ mock_api.add(
190
+ "GET",
191
+ f"{BASE}/results",
192
+ json={
193
+ "data": [
194
+ {"id": "res_1", "submission_id": "sub_abc", "best_fitness": 7.0},
195
+ ],
196
+ "next_cursor": None,
197
+ },
198
+ )
199
+ r = client.results.wait_for("sub_abc", timeout=5, poll_interval=0)
200
+ assert r.best_fitness == 7.0
201
+
202
+
203
+ def test_wait_for_raises_on_failed_submission(mock_api, client, monkeypatch):
204
+ monkeypatch.setattr("vilvik.client.time.sleep", lambda *_a, **_k: None)
205
+ mock_api.add(
206
+ "GET",
207
+ f"{BASE}/submissions/sub_bad",
208
+ json={"id": "sub_bad", "status": "failed"},
209
+ )
210
+ with pytest.raises(vilvik.APIError) as info:
211
+ client.results.wait_for("sub_bad", timeout=5, poll_interval=0)
212
+ assert info.value.code == "submission_failed"
213
+
214
+
215
+ def test_wait_for_times_out(mock_api, client, monkeypatch):
216
+ # Two stubbed responses, both "running" — the deadline will trigger first.
217
+ monkeypatch.setattr("vilvik.client.time.sleep", lambda *_a, **_k: None)
218
+ # responses replays the last match if exhausted; we set monotonic to
219
+ # walk past the deadline on the second call.
220
+ times = iter([0.0, 0.0, 999.0])
221
+ monkeypatch.setattr("vilvik.client.time.monotonic", lambda: next(times))
222
+ mock_api.add(
223
+ "GET",
224
+ f"{BASE}/submissions/sub_x",
225
+ json={"id": "sub_x", "status": "running"},
226
+ )
227
+ with pytest.raises(vilvik.TimeoutError):
228
+ client.results.wait_for("sub_x", timeout=1, poll_interval=0)
229
+
230
+
231
+ # --------------- Code uploads & webhooks ---------------
232
+
233
+
234
+ def test_code_upload_create(mock_api, client):
235
+ mock_api.add(
236
+ "POST",
237
+ f"{BASE}/code-uploads",
238
+ json={"id": "code_1", "field": "fitness_func", "size_bytes": 42},
239
+ status=201,
240
+ )
241
+ blob = client.code_uploads.create(
242
+ field="fitness_func",
243
+ code="def fitness_func(g, s, i): return 0",
244
+ )
245
+ assert blob.id == "code_1"
246
+ assert blob.field_name == "fitness_func"
247
+
248
+
249
+ def test_webhooks_list(mock_api, client):
250
+ mock_api.add(
251
+ "GET",
252
+ f"{BASE}/webhooks",
253
+ json={
254
+ "data": [
255
+ {"id": "wh_1", "url": "https://example.test/hook",
256
+ "event_types": ["submission.completed"]},
257
+ ],
258
+ },
259
+ )
260
+ hooks = client.webhooks.list()
261
+ assert hooks[0].id == "wh_1"
262
+ assert "submission.completed" in hooks[0].event_types
263
+
264
+
265
+ # --------------- Error translation ---------------
266
+
267
+
268
+ def test_authentication_error_translation(mock_api, client):
269
+ mock_api.add(
270
+ "GET",
271
+ f"{BASE}/submissions/sub_abc",
272
+ json={"error": {"code": "invalid_key", "message": "bad key",
273
+ "request_id": "req_z"}},
274
+ status=401,
275
+ )
276
+ with pytest.raises(vilvik.AuthenticationError) as info:
277
+ client.submissions.get("sub_abc")
278
+ assert info.value.code == "invalid_key"
279
+ assert info.value.request_id == "req_z"
280
+ assert info.value.status_code == 401
281
+
282
+
283
+ def test_rate_limit_error_picks_up_retry_after(mock_api, client):
284
+ mock_api.add(
285
+ "GET",
286
+ f"{BASE}/submissions",
287
+ json={"error": {"code": "rate_limited", "message": "slow down"}},
288
+ status=429,
289
+ headers={"Retry-After": "7"},
290
+ )
291
+ with pytest.raises(vilvik.RateLimitError) as info:
292
+ client.submissions.list()
293
+ assert info.value.retry_after == 7
294
+
295
+
296
+ def test_not_found_error(mock_api, client):
297
+ mock_api.add(
298
+ "GET",
299
+ f"{BASE}/submissions/ghost",
300
+ json={"error": {"code": "not_found", "message": "missing"}},
301
+ status=404,
302
+ )
303
+ with pytest.raises(vilvik.NotFoundError):
304
+ client.submissions.get("ghost")
305
+
306
+
307
+ def test_validation_error(mock_api, client):
308
+ mock_api.add(
309
+ "POST",
310
+ f"{BASE}/submissions",
311
+ json={"error": {"code": "validation_failed",
312
+ "message": "num_genes is required"}},
313
+ status=400,
314
+ )
315
+ with pytest.raises(vilvik.ValidationError):
316
+ client.submissions.create(num_generations=10)
317
+
318
+
319
+ def test_generic_api_error_for_5xx(mock_api, client):
320
+ mock_api.add(
321
+ "GET",
322
+ f"{BASE}/submissions/sub_abc",
323
+ json={"error": {"code": "upstream", "message": "boom"}},
324
+ status=502,
325
+ )
326
+ with pytest.raises(vilvik.APIError) as info:
327
+ client.submissions.get("sub_abc")
328
+ assert info.value.status_code == 502
329
+ # Not auth/notfound/validation/ratelimit.
330
+ assert not isinstance(info.value, vilvik.AuthenticationError)
331
+ assert not isinstance(info.value, vilvik.NotFoundError)
332
+ assert not isinstance(info.value, vilvik.ValidationError)
333
+ assert not isinstance(info.value, vilvik.RateLimitError)
334
+
335
+
336
+ # --------------- run() context manager ---------------
337
+
338
+
339
+ def test_run_context_manager_yields_result(mock_api, monkeypatch):
340
+ monkeypatch.setattr("vilvik.client.time.sleep", lambda *_a, **_k: None)
341
+ monkeypatch.setenv("VILVIK_API_KEY", "vlk_test_env")
342
+ mock_api.add(
343
+ "POST",
344
+ f"{BASE}/submissions",
345
+ json={"id": "sub_quick", "status": "queued"},
346
+ status=202,
347
+ )
348
+ mock_api.add(
349
+ "GET",
350
+ f"{BASE}/submissions/sub_quick",
351
+ json={"id": "sub_quick", "status": "succeeded"},
352
+ )
353
+ mock_api.add(
354
+ "GET",
355
+ f"{BASE}/results",
356
+ json={
357
+ "data": [{"id": "res_q", "submission_id": "sub_quick",
358
+ "best_fitness": 1.5}],
359
+ "next_cursor": None,
360
+ },
361
+ )
362
+ # The submission lookup at __exit__ time finds a terminal run, so no
363
+ # DELETE is issued — `assert_all_requests_are_fired=False` keeps the
364
+ # mock from complaining about unused matchers.
365
+ mock_api.add(
366
+ "GET",
367
+ f"{BASE}/submissions/sub_quick",
368
+ json={"id": "sub_quick", "status": "succeeded"},
369
+ )
370
+
371
+ with vilvik.run(
372
+ base_url=BASE,
373
+ fitness_func="x",
374
+ num_genes=2,
375
+ poll_interval=0,
376
+ timeout=5,
377
+ ) as result:
378
+ assert result.id == "res_q"
379
+ assert result.best_fitness == 1.5