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.
Files changed (33) hide show
  1. {vilvik-0.2.2 → vilvik-0.4.0}/PKG-INFO +1 -1
  2. {vilvik-0.2.2 → vilvik-0.4.0}/tests/test_client.py +98 -5
  3. vilvik-0.4.0/vilvik/_version.py +1 -0
  4. {vilvik-0.2.2 → vilvik-0.4.0}/vilvik/client.py +51 -2
  5. {vilvik-0.2.2 → vilvik-0.4.0}/vilvik/models.py +19 -8
  6. {vilvik-0.2.2 → vilvik-0.4.0}/vilvik.egg-info/PKG-INFO +1 -1
  7. vilvik-0.2.2/vilvik/_version.py +0 -1
  8. {vilvik-0.2.2 → vilvik-0.4.0}/LICENSE +0 -0
  9. {vilvik-0.2.2 → vilvik-0.4.0}/README.md +0 -0
  10. {vilvik-0.2.2 → vilvik-0.4.0}/pyproject.toml +0 -0
  11. {vilvik-0.2.2 → vilvik-0.4.0}/setup.cfg +0 -0
  12. {vilvik-0.2.2 → vilvik-0.4.0}/tests/test_capture.py +0 -0
  13. {vilvik-0.2.2 → vilvik-0.4.0}/tests/test_credentials.py +0 -0
  14. {vilvik-0.2.2 → vilvik-0.4.0}/tests/test_extract.py +0 -0
  15. {vilvik-0.2.2 → vilvik-0.4.0}/tests/test_imports_resource.py +0 -0
  16. {vilvik-0.2.2 → vilvik-0.4.0}/tests/test_login.py +0 -0
  17. {vilvik-0.2.2 → vilvik-0.4.0}/tests/test_public_api.py +0 -0
  18. {vilvik-0.2.2 → vilvik-0.4.0}/tests/test_push.py +0 -0
  19. {vilvik-0.2.2 → vilvik-0.4.0}/vilvik/__init__.py +0 -0
  20. {vilvik-0.2.2 → vilvik-0.4.0}/vilvik/__main__.py +0 -0
  21. {vilvik-0.2.2 → vilvik-0.4.0}/vilvik/_capture.py +0 -0
  22. {vilvik-0.2.2 → vilvik-0.4.0}/vilvik/_credentials.py +0 -0
  23. {vilvik-0.2.2 → vilvik-0.4.0}/vilvik/_extract.py +0 -0
  24. {vilvik-0.2.2 → vilvik-0.4.0}/vilvik/_http.py +0 -0
  25. {vilvik-0.2.2 → vilvik-0.4.0}/vilvik/exceptions.py +0 -0
  26. {vilvik-0.2.2 → vilvik-0.4.0}/vilvik/login.py +0 -0
  27. {vilvik-0.2.2 → vilvik-0.4.0}/vilvik/push.py +0 -0
  28. {vilvik-0.2.2 → vilvik-0.4.0}/vilvik/run.py +0 -0
  29. {vilvik-0.2.2 → vilvik-0.4.0}/vilvik.egg-info/SOURCES.txt +0 -0
  30. {vilvik-0.2.2 → vilvik-0.4.0}/vilvik.egg-info/dependency_links.txt +0 -0
  31. {vilvik-0.2.2 → vilvik-0.4.0}/vilvik.egg-info/entry_points.txt +0 -0
  32. {vilvik-0.2.2 → vilvik-0.4.0}/vilvik.egg-info/requires.txt +0 -0
  33. {vilvik-0.2.2 → vilvik-0.4.0}/vilvik.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vilvik
3
- Version: 0.2.2
3
+ Version: 0.4.0
4
4
  Summary: Official Python SDK for the Vilvik optimization cloud API.
5
5
  Author-email: Vilvik <ahmed.f.gad@gmail.com>
6
6
  License: MIT
@@ -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={"id": "code_1", "field": "fitness_func", "size_bytes": 42},
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
- field="fitness_func",
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.id == "code_1"
246
- assert blob.field_name == "fitness_func"
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, *, field: str, code: str) -> CodeUpload:
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={"field": field, "code": code},
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
- The model attribute is named `field_name` rather than `field` to
99
- avoid shadowing `dataclasses.field` inside the class body.
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
- id: str
103
- field_name: str = ""
104
- size_bytes: Optional[int] = None
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
- id=str(data.get("id", "")),
112
- field_name=str(data.get("field", "") or ""),
113
- size_bytes=data.get("size_bytes"),
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vilvik
3
- Version: 0.2.2
3
+ Version: 0.4.0
4
4
  Summary: Official Python SDK for the Vilvik optimization cloud API.
5
5
  Author-email: Vilvik <ahmed.f.gad@gmail.com>
6
6
  License: MIT
@@ -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