Compare commits
1 Commits
c5c8288eeb
...
f602eee7cc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f602eee7cc |
@@ -53,7 +53,9 @@ class SiloSettings:
|
||||
"""Return path to a custom CA certificate file, or ``""``."""
|
||||
raise NotImplementedError
|
||||
|
||||
def save_auth(self, username: str, role: str = "", source: str = "", token: str = ""):
|
||||
def save_auth(
|
||||
self, username: str, role: str = "", source: str = "", token: str = ""
|
||||
):
|
||||
"""Persist authentication info after a successful login."""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -258,7 +260,9 @@ class SiloClient:
|
||||
except urllib.error.URLError as e:
|
||||
raise RuntimeError(f"Connection error: {e.reason}")
|
||||
|
||||
def _upload_ods(self, path: str, ods_bytes: bytes, filename: str = "upload.ods") -> Any:
|
||||
def _upload_ods(
|
||||
self, path: str, ods_bytes: bytes, filename: str = "upload.ods"
|
||||
) -> Any:
|
||||
"""POST an ODS file as multipart/form-data."""
|
||||
boundary = "----SiloCalcUpload" + str(abs(hash(filename)))[-8:]
|
||||
parts: list = []
|
||||
@@ -289,7 +293,9 @@ class SiloClient:
|
||||
except urllib.error.URLError as e:
|
||||
raise RuntimeError(f"Connection error: {e.reason}")
|
||||
|
||||
def _upload_csv(self, path: str, csv_bytes: bytes, filename: str = "import.csv") -> Any:
|
||||
def _upload_csv(
|
||||
self, path: str, csv_bytes: bytes, filename: str = "import.csv"
|
||||
) -> Any:
|
||||
"""POST a CSV file as multipart/form-data."""
|
||||
boundary = "----SiloCsvUpload" + str(abs(hash(filename)))[-8:]
|
||||
parts: list = []
|
||||
@@ -322,7 +328,9 @@ class SiloClient:
|
||||
|
||||
# -- Authentication -----------------------------------------------------
|
||||
|
||||
def login(self, username: str, password: str, token_name: str = "") -> Dict[str, Any]:
|
||||
def login(
|
||||
self, username: str, password: str, token_name: str = ""
|
||||
) -> Dict[str, Any]:
|
||||
"""Session login and create a persistent API token.
|
||||
|
||||
Performs: ``POST /login`` (session) -> ``GET /api/auth/me`` (verify)
|
||||
@@ -342,7 +350,9 @@ class SiloClient:
|
||||
|
||||
# Step 1: POST credentials to /login
|
||||
login_url = f"{self._origin}/login"
|
||||
form_data = urllib.parse.urlencode({"username": username, "password": password}).encode()
|
||||
form_data = urllib.parse.urlencode(
|
||||
{"username": username, "password": password}
|
||||
).encode()
|
||||
req = urllib.request.Request(
|
||||
login_url,
|
||||
data=form_data,
|
||||
@@ -421,7 +431,9 @@ class SiloClient:
|
||||
"""List API tokens for the current user."""
|
||||
return self._request("GET", "/auth/tokens")
|
||||
|
||||
def create_token(self, name: str, expires_in_days: Optional[int] = None) -> Dict[str, Any]:
|
||||
def create_token(
|
||||
self, name: str, expires_in_days: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new API token."""
|
||||
data: Dict[str, Any] = {"name": name}
|
||||
if expires_in_days is not None:
|
||||
@@ -449,7 +461,9 @@ class SiloClient:
|
||||
url = f"{self._origin}/health"
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
try:
|
||||
with urllib.request.urlopen(req, context=self._ssl_context(), timeout=5) as resp:
|
||||
with urllib.request.urlopen(
|
||||
req, context=self._ssl_context(), timeout=5
|
||||
) as resp:
|
||||
return True, f"OK ({resp.status})"
|
||||
except urllib.error.HTTPError as e:
|
||||
return True, f"Server error ({e.code})"
|
||||
@@ -461,7 +475,9 @@ class SiloClient:
|
||||
# -- Items --------------------------------------------------------------
|
||||
|
||||
def get_item(self, part_number: str) -> Dict[str, Any]:
|
||||
return self._request("GET", f"/items/{urllib.parse.quote(part_number, safe='')}")
|
||||
return self._request(
|
||||
"GET", f"/items/{urllib.parse.quote(part_number, safe='')}"
|
||||
)
|
||||
|
||||
def list_items(
|
||||
self,
|
||||
@@ -517,11 +533,15 @@ class SiloClient:
|
||||
def search_items(self, query: str, limit: int = 100, offset: int = 0) -> list:
|
||||
"""Full-text search across items."""
|
||||
q = urllib.parse.quote(query)
|
||||
return self._request("GET", f"/items/search?q={q}&limit={limit}&offset={offset}")
|
||||
return self._request(
|
||||
"GET", f"/items/search?q={q}&limit={limit}&offset={offset}"
|
||||
)
|
||||
|
||||
def get_item_by_uuid(self, uuid: str) -> Dict[str, Any]:
|
||||
"""Get an item by its internal UUID."""
|
||||
return self._request("GET", f"/items/by-uuid/{urllib.parse.quote(uuid, safe='')}")
|
||||
return self._request(
|
||||
"GET", f"/items/by-uuid/{urllib.parse.quote(uuid, safe='')}"
|
||||
)
|
||||
|
||||
def delete_item(self, part_number: str) -> None:
|
||||
"""Delete an item by part number."""
|
||||
@@ -582,7 +602,9 @@ class SiloClient:
|
||||
f"/items/{urllib.parse.quote(part_number, safe='')}/revisions/{revision}",
|
||||
)
|
||||
|
||||
def compare_revisions(self, part_number: str, from_rev: int, to_rev: int) -> Dict[str, Any]:
|
||||
def compare_revisions(
|
||||
self, part_number: str, from_rev: int, to_rev: int
|
||||
) -> Dict[str, Any]:
|
||||
pn = urllib.parse.quote(part_number, safe="")
|
||||
return self._request(
|
||||
"GET",
|
||||
@@ -650,6 +672,39 @@ class SiloClient:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# -- .kc Metadata -------------------------------------------------------
|
||||
|
||||
def get_metadata(self, part_number: str) -> Dict[str, Any]:
|
||||
"""Get indexed .kc metadata for an item."""
|
||||
pn = urllib.parse.quote(part_number, safe="")
|
||||
return self._request("GET", f"/items/{pn}/metadata")
|
||||
|
||||
def update_metadata(
|
||||
self, part_number: str, fields: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Update .kc metadata fields."""
|
||||
pn = urllib.parse.quote(part_number, safe="")
|
||||
return self._request("PUT", f"/items/{pn}/metadata", fields)
|
||||
|
||||
def patch_lifecycle(self, part_number: str, state: str) -> Dict[str, Any]:
|
||||
"""Transition lifecycle state."""
|
||||
pn = urllib.parse.quote(part_number, safe="")
|
||||
return self._request(
|
||||
"PATCH", f"/items/{pn}/metadata/lifecycle", {"state": state}
|
||||
)
|
||||
|
||||
def patch_tags(self, part_number: str, tags: list) -> Dict[str, Any]:
|
||||
"""Add/remove tags on .kc metadata."""
|
||||
pn = urllib.parse.quote(part_number, safe="")
|
||||
return self._request("PATCH", f"/items/{pn}/metadata/tags", {"tags": tags})
|
||||
|
||||
# -- .kc Dependencies ---------------------------------------------------
|
||||
|
||||
def resolve_dependencies(self, part_number: str) -> list:
|
||||
"""Resolve dependency UUIDs to part numbers and file availability."""
|
||||
pn = urllib.parse.quote(part_number, safe="")
|
||||
return self._request("GET", f"/items/{pn}/dependencies/resolve")
|
||||
|
||||
# -- BOM / Relationships ------------------------------------------------
|
||||
|
||||
def get_bom(self, part_number: str) -> list:
|
||||
@@ -734,12 +789,16 @@ class SiloClient:
|
||||
pn = urllib.parse.quote(part_number, safe="")
|
||||
return self._download(f"/items/{pn}/bom/export.csv")
|
||||
|
||||
def import_bom_csv(self, part_number: str, csv_bytes: bytes, filename: str = "bom.csv") -> Any:
|
||||
def import_bom_csv(
|
||||
self, part_number: str, csv_bytes: bytes, filename: str = "bom.csv"
|
||||
) -> Any:
|
||||
"""Import a BOM from a CSV file."""
|
||||
pn = urllib.parse.quote(part_number, safe="")
|
||||
return self._upload_csv(f"/items/{pn}/bom/import", csv_bytes, filename)
|
||||
|
||||
def merge_bom(self, part_number: str, ods_bytes: bytes, filename: str = "bom.ods") -> Any:
|
||||
def merge_bom(
|
||||
self, part_number: str, ods_bytes: bytes, filename: str = "bom.ods"
|
||||
) -> Any:
|
||||
"""Merge a BOM from an ODS spreadsheet."""
|
||||
pn = urllib.parse.quote(part_number, safe="")
|
||||
return self._upload_ods(f"/items/{pn}/bom/merge", ods_bytes, filename)
|
||||
@@ -783,7 +842,9 @@ class SiloClient:
|
||||
data["label"] = label
|
||||
return self._request("POST", f"/schemas/{s}/segments/{seg}/values", data)
|
||||
|
||||
def update_enum_value(self, schema: str, segment: str, code: str, **fields) -> Dict[str, Any]:
|
||||
def update_enum_value(
|
||||
self, schema: str, segment: str, code: str, **fields
|
||||
) -> Dict[str, Any]:
|
||||
"""Update an enum value in a schema segment."""
|
||||
s = urllib.parse.quote(schema, safe="")
|
||||
seg = urllib.parse.quote(segment, safe="")
|
||||
@@ -806,7 +867,9 @@ class SiloClient:
|
||||
"""Get a single project by code."""
|
||||
return self._request("GET", f"/projects/{urllib.parse.quote(code, safe='')}")
|
||||
|
||||
def create_project(self, code: str, name: str, description: str = "") -> Dict[str, Any]:
|
||||
def create_project(
|
||||
self, code: str, name: str, description: str = ""
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new project."""
|
||||
data: Dict[str, Any] = {"code": code, "name": name}
|
||||
if description:
|
||||
@@ -814,15 +877,21 @@ class SiloClient:
|
||||
return self._request("POST", "/projects", data)
|
||||
|
||||
def get_project_items(self, code: str) -> list:
|
||||
return self._request("GET", f"/projects/{urllib.parse.quote(code, safe='')}/items")
|
||||
return self._request(
|
||||
"GET", f"/projects/{urllib.parse.quote(code, safe='')}/items"
|
||||
)
|
||||
|
||||
def get_item_projects(self, part_number: str) -> list:
|
||||
pn = urllib.parse.quote(part_number, safe="")
|
||||
return self._request("GET", f"/items/{pn}/projects")
|
||||
|
||||
def add_item_projects(self, part_number: str, project_codes: List[str]) -> Dict[str, Any]:
|
||||
def add_item_projects(
|
||||
self, part_number: str, project_codes: List[str]
|
||||
) -> Dict[str, Any]:
|
||||
pn = urllib.parse.quote(part_number, safe="")
|
||||
return self._request("POST", f"/items/{pn}/projects", {"projects": project_codes})
|
||||
return self._request(
|
||||
"POST", f"/items/{pn}/projects", {"projects": project_codes}
|
||||
)
|
||||
|
||||
def update_project(self, code: str, **fields) -> Dict[str, Any]:
|
||||
"""Update a project's fields."""
|
||||
@@ -834,7 +903,9 @@ class SiloClient:
|
||||
|
||||
def delete_project(self, code: str) -> None:
|
||||
"""Delete a project."""
|
||||
self._request("DELETE", f"/projects/{urllib.parse.quote(code, safe='')}", raw=True)
|
||||
self._request(
|
||||
"DELETE", f"/projects/{urllib.parse.quote(code, safe='')}", raw=True
|
||||
)
|
||||
|
||||
def remove_item_project(self, part_number: str, project_code: str) -> None:
|
||||
"""Remove a project tag from an item."""
|
||||
@@ -861,7 +932,9 @@ class SiloClient:
|
||||
code = urllib.parse.quote(project_code, safe="")
|
||||
return self._download(f"/projects/{code}/sheet.ods")
|
||||
|
||||
def upload_sheet_diff(self, ods_bytes: bytes, filename: str = "sheet.ods") -> Dict[str, Any]:
|
||||
def upload_sheet_diff(
|
||||
self, ods_bytes: bytes, filename: str = "sheet.ods"
|
||||
) -> Dict[str, Any]:
|
||||
return self._upload_ods("/sheets/diff", ods_bytes, filename)
|
||||
|
||||
# -- Files / Attachments ------------------------------------------------
|
||||
@@ -910,9 +983,14 @@ class SiloClient:
|
||||
"""Set the thumbnail image for an item (binary PUT)."""
|
||||
pn = urllib.parse.quote(part_number, safe="")
|
||||
url = f"{self.base_url}/items/{pn}/thumbnail"
|
||||
headers = {"Content-Type": content_type, "Content-Length": str(len(image_bytes))}
|
||||
headers = {
|
||||
"Content-Type": content_type,
|
||||
"Content-Length": str(len(image_bytes)),
|
||||
}
|
||||
headers.update(self._auth_headers())
|
||||
req = urllib.request.Request(url, data=image_bytes, headers=headers, method="PUT")
|
||||
req = urllib.request.Request(
|
||||
url, data=image_bytes, headers=headers, method="PUT"
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, context=self._ssl_context()) as resp:
|
||||
resp.read()
|
||||
@@ -977,7 +1055,9 @@ class SiloClient:
|
||||
},
|
||||
)
|
||||
|
||||
def get_dag(self, part_number: str, revision_number: Optional[int] = None) -> Dict[str, Any]:
|
||||
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
|
||||
@@ -1052,7 +1132,9 @@ class SiloClient:
|
||||
|
||||
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")
|
||||
return self._request(
|
||||
"POST", f"/jobs/{urllib.parse.quote(job_id, safe='')}/cancel"
|
||||
)
|
||||
|
||||
# -- Job Definitions ----------------------------------------------------
|
||||
|
||||
@@ -1062,7 +1144,9 @@ class SiloClient:
|
||||
|
||||
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='')}")
|
||||
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)."""
|
||||
@@ -1080,7 +1164,9 @@ class SiloClient:
|
||||
|
||||
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)
|
||||
self._request(
|
||||
"DELETE", f"/runners/{urllib.parse.quote(runner_id, safe='')}", raw=True
|
||||
)
|
||||
|
||||
# -- Runner-facing ------------------------------------------------------
|
||||
|
||||
@@ -1102,7 +1188,9 @@ class SiloClient:
|
||||
data["message"] = message
|
||||
return self._request("PUT", f"/runner/jobs/{jid}/progress", data)
|
||||
|
||||
def runner_complete_job(self, job_id: str, result: Optional[Dict] = None) -> Dict[str, Any]:
|
||||
def runner_complete_job(
|
||||
self, job_id: str, result: Optional[Dict] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Mark a job as successfully completed."""
|
||||
jid = urllib.parse.quote(job_id, safe="")
|
||||
data: Dict[str, Any] = {}
|
||||
|
||||
Reference in New Issue
Block a user