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 +154 -0
- vilvik-0.1.0/README.md +122 -0
- vilvik-0.1.0/pyproject.toml +47 -0
- vilvik-0.1.0/setup.cfg +4 -0
- vilvik-0.1.0/tests/test_client.py +379 -0
- vilvik-0.1.0/vilvik/__init__.py +56 -0
- vilvik-0.1.0/vilvik/_http.py +151 -0
- vilvik-0.1.0/vilvik/client.py +294 -0
- vilvik-0.1.0/vilvik/exceptions.py +81 -0
- vilvik-0.1.0/vilvik/models.py +155 -0
- vilvik-0.1.0/vilvik/run.py +66 -0
- vilvik-0.1.0/vilvik.egg-info/PKG-INFO +154 -0
- vilvik-0.1.0/vilvik.egg-info/SOURCES.txt +14 -0
- vilvik-0.1.0/vilvik.egg-info/dependency_links.txt +1 -0
- vilvik-0.1.0/vilvik.egg-info/requires.txt +9 -0
- vilvik-0.1.0/vilvik.egg-info/top_level.txt +1 -0
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,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
|