From fb658c5a249275700eab5a5e863e46038f613950 Mon Sep 17 00:00:00 2001 From: Zoe Forbes Date: Sat, 14 Feb 2026 15:06:22 -0600 Subject: [PATCH 1/2] feat: add push_dag and get_dag methods to SiloClient - push_dag(part_number, revision_number, nodes, edges): PUT /api/items/{pn}/dag - get_dag(part_number, revision_number=None): GET /api/items/{pn}/dag Closes kindred/create#215 --- silo_client/__init__.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/silo_client/__init__.py b/silo_client/__init__.py index 2a930af..9a3c71e 100644 --- a/silo_client/__init__.py +++ b/silo_client/__init__.py @@ -902,3 +902,42 @@ class SiloClient: self, ods_bytes: bytes, filename: str = "sheet.ods" ) -> Dict[str, Any]: return self._upload_ods("/sheets/diff", ods_bytes, filename) + + # -- DAG (feature tree) ------------------------------------------------- + + def push_dag( + self, + part_number: str, + revision_number: int, + nodes: List[Dict[str, Any]], + edges: List[Dict[str, Any]], + ) -> Dict[str, Any]: + """Push a feature DAG to the server. + + Calls ``PUT /api/items/{partNumber}/dag`` with the payload + described in ``MULTI_USER_CLIENT.md`` Section 2. + """ + pn = urllib.parse.quote(part_number, safe="") + return self._request( + "PUT", + f"/items/{pn}/dag", + { + "revision_number": revision_number, + "nodes": nodes, + "edges": edges, + }, + ) + + def get_dag( + self, part_number: str, revision_number: Optional[int] = None + ) -> Dict[str, Any]: + """Fetch the stored DAG for an item. + + Calls ``GET /api/items/{partNumber}/dag``, optionally filtered + by *revision_number*. + """ + pn = urllib.parse.quote(part_number, safe="") + path = f"/items/{pn}/dag" + if revision_number is not None: + path += f"?revision={revision_number}" + return self._request("GET", path) -- 2.49.1 From 9b71cf037555b111bb1345c9d93743693d40e68d Mon Sep 17 00:00:00 2001 From: Zoe Forbes Date: Sun, 15 Feb 2026 05:07:15 -0600 Subject: [PATCH 2/2] feat: add job, job-definition, and runner API methods --- silo_client/__init__.py | 82 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/silo_client/__init__.py b/silo_client/__init__.py index 9a3c71e..1828153 100644 --- a/silo_client/__init__.py +++ b/silo_client/__init__.py @@ -941,3 +941,85 @@ class SiloClient: if revision_number is not None: path += f"?revision={revision_number}" return self._request("GET", path) + + # -- Jobs --------------------------------------------------------------- + + def list_jobs( + self, + status: str = "", + item_id: str = "", + definition: str = "", + limit: int = 100, + ) -> list: + """List jobs, optionally filtered by status, item, or definition.""" + params = [f"limit={limit}"] + if status: + params.append(f"status={urllib.parse.quote(status)}") + if item_id: + params.append(f"item_id={urllib.parse.quote(item_id)}") + if definition: + params.append(f"definition={urllib.parse.quote(definition)}") + return self._request("GET", "/jobs?" + "&".join(params)) + + def get_job(self, job_id: str) -> Dict[str, Any]: + """Get details for a specific job.""" + return self._request("GET", f"/jobs/{urllib.parse.quote(job_id, safe='')}") + + def get_job_logs(self, job_id: str) -> list: + """Get log entries for a job.""" + return self._request("GET", f"/jobs/{urllib.parse.quote(job_id, safe='')}/logs") + + def trigger_job( + self, + definition_name: str, + part_number: str = "", + item_id: str = "", + ) -> Dict[str, Any]: + """Manually trigger a job. + + Either *part_number* or *item_id* should identify the target item. + """ + data: Dict[str, Any] = {"definition_name": definition_name} + if part_number: + data["part_number"] = part_number + if item_id: + data["item_id"] = item_id + return self._request("POST", "/jobs", data) + + def cancel_job(self, job_id: str) -> Dict[str, Any]: + """Cancel a pending or running job.""" + return self._request( + "POST", f"/jobs/{urllib.parse.quote(job_id, safe='')}/cancel" + ) + + # -- Job Definitions ---------------------------------------------------- + + def list_job_definitions(self) -> list: + """List all loaded job definitions.""" + return self._request("GET", "/job-definitions") + + def get_job_definition(self, name: str) -> Dict[str, Any]: + """Get a specific job definition by name.""" + return self._request( + "GET", f"/job-definitions/{urllib.parse.quote(name, safe='')}" + ) + + def reload_job_definitions(self) -> Dict[str, Any]: + """Re-read YAML job definitions from disk (admin only).""" + return self._request("POST", "/job-definitions/reload") + + # -- Runners (admin) ---------------------------------------------------- + + def list_runners(self) -> list: + """List all registered runners.""" + return self._request("GET", "/runners") + + def register_runner(self, name: str, tags: List[str]) -> Dict[str, Any]: + """Register a new runner. Returns the token (shown once).""" + return self._request("POST", "/runners", {"name": name, "tags": tags}) + + def delete_runner(self, runner_id: str) -> None: + """Delete a registered runner.""" + self._request( + "DELETE", f"/runners/{urllib.parse.quote(runner_id, safe='')}", raw=True + ) -- 2.49.1