diff --git a/freecad/schema_form.py b/freecad/schema_form.py index a6cba20..ae2c1c6 100644 --- a/freecad/schema_form.py +++ b/freecad/schema_form.py @@ -10,9 +10,6 @@ backward-compatible :class:`SchemaFormDialog` modal. """ import json -import urllib.error -import urllib.parse -import urllib.request import FreeCAD from PySide import QtCore, QtGui, QtWidgets @@ -267,17 +264,8 @@ class SchemaFormWidget(QtWidgets.QWidget): def _fetch_properties(self, category: str) -> dict: """Fetch merged property definitions for a category.""" - from silo_commands import _get_api_url, _get_auth_headers, _get_ssl_context - - api_url = _get_api_url().rstrip("/") - url = f"{api_url}/schemas/kindred-rd/properties?category={urllib.parse.quote(category)}" - req = urllib.request.Request(url, method="GET") - req.add_header("Accept", "application/json") - for k, v in _get_auth_headers().items(): - req.add_header(k, v) try: - resp = urllib.request.urlopen(req, context=_get_ssl_context(), timeout=5) - data = json.loads(resp.read().decode("utf-8")) + data = self._client.get_property_schema(category=category) return data.get("properties", data) except Exception as e: FreeCAD.Console.PrintWarning( @@ -287,19 +275,10 @@ class SchemaFormWidget(QtWidgets.QWidget): def _generate_pn_preview(self, category: str) -> str: """Call the server to preview the next part number.""" - from silo_commands import _get_api_url, _get_auth_headers, _get_ssl_context + from silo_commands import _get_schema_name - api_url = _get_api_url().rstrip("/") - url = f"{api_url}/generate-part-number" - payload = json.dumps({"schema": "kindred-rd", "category": category}).encode() - req = urllib.request.Request(url, data=payload, method="POST") - req.add_header("Content-Type", "application/json") - req.add_header("Accept", "application/json") - for k, v in _get_auth_headers().items(): - req.add_header(k, v) try: - resp = urllib.request.urlopen(req, context=_get_ssl_context(), timeout=5) - data = json.loads(resp.read().decode("utf-8")) + data = self._client.generate_part_number(_get_schema_name(), category) return data.get("part_number", "") except Exception: return "" @@ -574,8 +553,10 @@ class SchemaFormWidget(QtWidgets.QWidget): return try: + from silo_commands import _get_schema_name + result = self._client.create_item( - "kindred-rd", + _get_schema_name(), data["category"], data["description"], projects=data["projects"], diff --git a/freecad/silo_commands.py b/freecad/silo_commands.py index 4bf44b8..5f3e06c 100644 --- a/freecad/silo_commands.py +++ b/freecad/silo_commands.py @@ -14,22 +14,34 @@ from typing import Any, Dict, List, Optional, Tuple import FreeCAD import FreeCADGui from PySide import QtCore -from silo_client import ( - CATEGORY_NAMES, - SiloClient, - SiloSettings, - get_category_folder_name, - parse_part_number, - sanitize_filename, -) +from silo_client import SiloClient, SiloSettings # Preference group for Kindred Silo settings _PREF_GROUP = "User parameter:BaseApp/Preferences/Mod/KindredSilo" # Configuration - preferences take priority over env vars -SILO_PROJECTS_DIR = os.environ.get( - "SILO_PROJECTS_DIR", os.path.expanduser("~/projects") -) +SILO_PROJECTS_DIR = os.environ.get("SILO_PROJECTS_DIR", os.path.expanduser("~/projects")) + + +# --------------------------------------------------------------------------- +# Local utility helpers (previously in silo_client, now server-driven) +# --------------------------------------------------------------------------- + + +def _parse_part_number(part_number: str) -> Tuple[str, str]: + """Parse part number into ``(category, sequence)``. E.g. ``"F01-0001"`` -> ``("F01", "0001")``.""" + parts = part_number.split("-") + if len(parts) >= 2: + return parts[0], parts[1] + return part_number, "" + + +def _sanitize_filename(name: str) -> str: + """Sanitize a string for use in filenames.""" + sanitized = re.sub(r'[<>:"/\\|?*]', "_", name) + sanitized = re.sub(r"[\s_]+", "_", sanitized) + sanitized = sanitized.strip("_ ") + return sanitized[:50] def _relative_time(dt): @@ -86,9 +98,7 @@ class FreeCADSiloSettings(SiloSettings): param = FreeCAD.ParamGet(_PREF_GROUP) return param.GetString("SslCertPath", "") - def save_auth( - self, username: str, role: str = "", source: str = "", token: str = "" - ): + def save_auth(self, username: str, role: str = "", source: str = "", token: str = ""): param = FreeCAD.ParamGet(_PREF_GROUP) param.SetString("AuthUsername", username) param.SetString("AuthRole", role) @@ -96,6 +106,13 @@ class FreeCADSiloSettings(SiloSettings): if token: param.SetString("ApiToken", token) + def get_schema_name(self) -> str: + param = FreeCAD.ParamGet(_PREF_GROUP) + name = param.GetString("SchemaName", "") + if not name: + name = os.environ.get("SILO_SCHEMA", "kindred-rd") + return name + def clear_auth(self): param = FreeCAD.ParamGet(_PREF_GROUP) param.SetString("ApiToken", "") @@ -139,6 +156,10 @@ def _get_api_url() -> str: return _fc_settings.get_api_url() +def _get_schema_name() -> str: + return _fc_settings.get_schema_name() + + def _get_ssl_verify() -> bool: return _fc_settings.get_ssl_verify() @@ -146,9 +167,7 @@ def _get_ssl_verify() -> bool: def _get_ssl_context(): from silo_client._ssl import build_ssl_context - return build_ssl_context( - _fc_settings.get_ssl_verify(), _fc_settings.get_ssl_cert_path() - ) + return build_ssl_context(_fc_settings.get_ssl_verify(), _fc_settings.get_ssl_cert_path()) def _get_auth_headers() -> Dict[str, str]: @@ -205,9 +224,7 @@ def _fetch_server_mode() -> str: # Icon helper # --------------------------------------------------------------------------- -_ICON_DIR = os.path.join( - os.path.dirname(os.path.abspath(__file__)), "resources", "icons" -) +_ICON_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources", "icons") def _icon(name): @@ -234,24 +251,22 @@ def get_projects_dir() -> Path: def get_cad_file_path(part_number: str, description: str = "") -> Path: """Generate canonical file path for a CAD file. - Path format: ~/projects/cad/{category_code}_{category_name}/{part_number}_{description}.kc + Path format: ~/projects/cad/{category_code}/{part_number}_{description}.kc """ - category, _ = parse_part_number(part_number) - folder_name = get_category_folder_name(category) + category, _ = _parse_part_number(part_number) if description: - filename = f"{part_number}_{sanitize_filename(description)}.kc" + filename = f"{part_number}_{_sanitize_filename(description)}.kc" else: filename = f"{part_number}.kc" - return get_projects_dir() / "cad" / folder_name / filename + return get_projects_dir() / "cad" / category / filename def find_file_by_part_number(part_number: str) -> Optional[Path]: """Find existing CAD file for a part number. Prefers .kc over .FCStd.""" - category, _ = parse_part_number(part_number) - folder_name = get_category_folder_name(category) - cad_dir = get_projects_dir() / "cad" / folder_name + category, _ = _parse_part_number(part_number) + cad_dir = get_projects_dir() / "cad" / category for search_dir in _search_dirs(cad_dir): for ext in ("*.kc", "*.FCStd"): @@ -516,7 +531,7 @@ class SiloSync: ) # Add a Body for parts (not assemblies) - body_label = sanitize_filename(description) if description else "Body" + body_label = _sanitize_filename(description) if description else "Body" body = doc.addObject("PartDesign::Body", "_" + body_label) body.Label = body_label part_obj.addObject(body) @@ -608,9 +623,7 @@ def handle_kindred_url(url: str): parts = [parsed.netloc] + [p for p in parsed.path.split("/") if p] if len(parts) >= 2 and parts[0] == "item": part_number = parts[1] - FreeCAD.Console.PrintMessage( - f"Silo: Opening item {part_number} from kindred:// URL\n" - ) + FreeCAD.Console.PrintMessage(f"Silo: Opening item {part_number} from kindred:// URL\n") _sync.open_item(part_number) @@ -717,9 +730,7 @@ class Silo_New: }, ) obj.Label = part_number - _sync.save_to_canonical_path( - FreeCAD.ActiveDocument, force_rename=True - ) + _sync.save_to_canonical_path(FreeCAD.ActiveDocument, force_rename=True) else: _sync.create_document_for_item(result, save=True) @@ -752,9 +763,7 @@ def _push_dag_after_upload(doc, part_number, revision_number): result = _client.push_dag(part_number, revision_number, nodes, edges) node_count = result.get("node_count", len(nodes)) edge_count = result.get("edge_count", len(edges)) - FreeCAD.Console.PrintMessage( - f"DAG synced: {node_count} nodes, {edge_count} edges\n" - ) + FreeCAD.Console.PrintMessage(f"DAG synced: {node_count} nodes, {edge_count} edges\n") except Exception as e: FreeCAD.Console.PrintWarning(f"DAG sync failed: {e}\n") @@ -822,9 +831,7 @@ class Silo_Save: # Try to upload to MinIO try: - result = _client._upload_file( - part_number, str(file_path), properties, "Auto-save" - ) + result = _client._upload_file(part_number, str(file_path), properties, "Auto-save") new_rev = result["revision_number"] FreeCAD.Console.PrintMessage(f"Uploaded as revision {new_rev}\n") @@ -859,9 +866,7 @@ class Silo_Commit: obj = get_tracked_object(doc) if not obj: - FreeCAD.Console.PrintError( - "No tracked object. Use 'New' to register first.\n" - ) + FreeCAD.Console.PrintError("No tracked object. Use 'New' to register first.\n") return part_number = obj.SiloPartNumber @@ -878,9 +883,7 @@ class Silo_Commit: if not file_path: return - result = _client._upload_file( - part_number, str(file_path), properties, comment - ) + result = _client._upload_file(part_number, str(file_path), properties, comment) new_rev = result["revision_number"] FreeCAD.Console.PrintMessage(f"Committed revision {new_rev}: {comment}\n") @@ -931,9 +934,7 @@ def _check_pull_conflicts(part_number, local_path, doc=None): server_updated = item.get("updated_at", "") if server_updated: # Parse ISO format timestamp - server_dt = datetime.datetime.fromisoformat( - server_updated.replace("Z", "+00:00") - ) + server_dt = datetime.datetime.fromisoformat(server_updated.replace("Z", "+00:00")) if server_dt > local_mtime: conflicts.append("Server version is newer than local file.") except Exception: @@ -963,9 +964,7 @@ class SiloPullDialog: # Revision table self._table = QtGui.QTableWidget() self._table.setColumnCount(5) - self._table.setHorizontalHeaderLabels( - ["Rev", "Date", "Comment", "Status", "File"] - ) + self._table.setHorizontalHeaderLabels(["Rev", "Date", "Comment", "Status", "File"]) self._table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) self._table.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) self._table.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) @@ -1058,9 +1057,7 @@ def _pull_dependencies(part_number, progress_callback=None): # Skip if already exists locally existing = find_file_by_part_number(child_pn) if existing and existing.exists(): - FreeCAD.Console.PrintMessage( - f" {child_pn}: already exists at {existing}\n" - ) + FreeCAD.Console.PrintMessage(f" {child_pn}: already exists at {existing}\n") # Still recurse — this child may itself be an assembly with missing deps _pull_dependencies(child_pn, progress_callback) continue @@ -1140,18 +1137,14 @@ class Silo_Pull: if not has_any_file: if existing_local: - FreeCAD.Console.PrintMessage( - f"Opening existing local file: {existing_local}\n" - ) + FreeCAD.Console.PrintMessage(f"Opening existing local file: {existing_local}\n") FreeCAD.openDocument(str(existing_local)) else: try: item = _client.get_item(part_number) new_doc = _sync.create_document_for_item(item, save=True) if new_doc: - FreeCAD.Console.PrintMessage( - f"Created local file for {part_number}\n" - ) + FreeCAD.Console.PrintMessage(f"Created local file for {part_number}\n") else: QtGui.QMessageBox.warning( None, @@ -1238,9 +1231,7 @@ class Silo_Pull: progress.setValue(100) progress.close() if dep_pulled: - FreeCAD.Console.PrintMessage( - f"Pulled {len(dep_pulled)} dependency file(s)\n" - ) + FreeCAD.Console.PrintMessage(f"Pulled {len(dep_pulled)} dependency file(s)\n") # Close existing document if open, then reopen if doc and doc.FileName == str(dest_path): @@ -1295,9 +1286,7 @@ class Silo_Push: server_dt = datetime.fromisoformat( server_time_str.replace("Z", "+00:00") ) - local_dt = datetime.fromtimestamp( - local_mtime, tz=timezone.utc - ) + local_dt = datetime.fromtimestamp(local_mtime, tz=timezone.utc) if local_dt > server_dt: unuploaded.append(lf) else: @@ -1310,9 +1299,7 @@ class Silo_Push: pass # Not in DB, skip if not unuploaded: - QtGui.QMessageBox.information( - None, "Push", "All local files are already uploaded." - ) + QtGui.QMessageBox.information(None, "Push", "All local files are already uploaded.") return msg = f"Found {len(unuploaded)} files to upload:\n\n" @@ -1330,9 +1317,7 @@ class Silo_Push: uploaded = 0 for item in unuploaded: - result = _sync.upload_file( - item["part_number"], item["path"], "Synced from local" - ) + result = _sync.upload_file(item["part_number"], item["path"], "Synced from local") if result: uploaded += 1 @@ -1381,7 +1366,9 @@ class Silo_Info: msg = f"

{part_number}

" msg += f"

Type: {item.get('item_type', '-')}

" msg += f"

Description: {item.get('description', '-')}

" - msg += f"

Projects: {', '.join(project_codes) if project_codes else 'None'}

" + msg += ( + f"

Projects: {', '.join(project_codes) if project_codes else 'None'}

" + ) msg += f"

Current Revision: {item.get('current_revision', 1)}

" msg += f"

Local Revision: {getattr(obj, 'SiloRevision', '-')}

" @@ -1447,9 +1434,7 @@ class Silo_TagProjects: try: # Get current projects for item current_projects = _client.get_item_projects(part_number) - current_codes = { - p.get("code", "") for p in current_projects if p.get("code") - } + current_codes = {p.get("code", "") for p in current_projects if p.get("code")} # Get all available projects all_projects = _client.get_projects() @@ -1560,9 +1545,7 @@ class Silo_Rollback: dialog.setMinimumHeight(300) layout = QtGui.QVBoxLayout(dialog) - label = QtGui.QLabel( - f"Select a revision to rollback to (current: Rev {current_rev}):" - ) + label = QtGui.QLabel(f"Select a revision to rollback to (current: Rev {current_rev}):") layout.addWidget(label) # Revision table @@ -1577,12 +1560,8 @@ class Silo_Rollback: for i, rev in enumerate(prev_revisions): table.setItem(i, 0, QtGui.QTableWidgetItem(str(rev["revision_number"]))) table.setItem(i, 1, QtGui.QTableWidgetItem(rev.get("status", "draft"))) - table.setItem( - i, 2, QtGui.QTableWidgetItem(rev.get("created_at", "")[:10]) - ) - table.setItem( - i, 3, QtGui.QTableWidgetItem(rev.get("comment", "") or "") - ) + table.setItem(i, 2, QtGui.QTableWidgetItem(rev.get("created_at", "")[:10])) + table.setItem(i, 3, QtGui.QTableWidgetItem(rev.get("comment", "") or "")) table.resizeColumnsToContents() layout.addWidget(table) @@ -1608,9 +1587,7 @@ class Silo_Rollback: def on_rollback(): selected = table.selectedItems() if not selected: - QtGui.QMessageBox.warning( - dialog, "Rollback", "Please select a revision" - ) + QtGui.QMessageBox.warning(dialog, "Rollback", "Please select a revision") return selected_rev[0] = int(table.item(selected[0].row(), 0).text()) dialog.accept() @@ -1708,9 +1685,7 @@ class Silo_SetStatus: # Update status _client.update_revision(part_number, rev_num, status=status) - FreeCAD.Console.PrintMessage( - f"Updated Rev {rev_num} status to '{status}'\n" - ) + FreeCAD.Console.PrintMessage(f"Updated Rev {rev_num} status to '{status}'\n") QtGui.QMessageBox.information( None, "Status Updated", f"Revision {rev_num} status set to '{status}'" ) @@ -1769,14 +1744,37 @@ class Silo_Settings: layout.addSpacing(10) + # Schema name + schema_label = QtGui.QLabel("Schema Name:") + layout.addWidget(schema_label) + + schema_input = QtGui.QLineEdit() + schema_input.setPlaceholderText("kindred-rd") + current_schema = param.GetString("SchemaName", "") + if current_schema: + schema_input.setText(current_schema) + else: + env_schema = os.environ.get("SILO_SCHEMA", "") + if env_schema: + schema_input.setText(env_schema) + layout.addWidget(schema_input) + + schema_hint = QtGui.QLabel( + "The part-numbering schema to use. Leave empty for " + "SILO_SCHEMA env var or default (kindred-rd)." + ) + schema_hint.setWordWrap(True) + schema_hint.setStyleSheet("color: #888; font-size: 11px;") + layout.addWidget(schema_hint) + + layout.addSpacing(10) + # SSL ssl_checkbox = QtGui.QCheckBox("Verify SSL certificates") ssl_checkbox.setChecked(param.GetBool("SslVerify", True)) layout.addWidget(ssl_checkbox) - ssl_hint = QtGui.QLabel( - "Disable only for internal servers with self-signed certificates." - ) + ssl_hint = QtGui.QLabel("Disable only for internal servers with self-signed certificates.") ssl_hint.setWordWrap(True) ssl_hint.setStyleSheet("color: #888; font-size: 11px;") layout.addWidget(ssl_hint) @@ -1911,6 +1909,7 @@ class Silo_Settings: auth_display = "not configured" status_label = QtGui.QLabel( f"Active URL: {_get_api_url()}
" + f"Schema: {_get_schema_name()}
" f"SSL verification: {'enabled' if _get_ssl_verify() else 'disabled'}
" f"CA certificate: {cert_display}
" f"Authentication: {auth_display}" @@ -1932,6 +1931,7 @@ class Silo_Settings: def on_save(): url = url_input.text().strip() param.SetString("ApiUrl", url) + param.SetString("SchemaName", schema_input.text().strip()) param.SetBool("SslVerify", ssl_checkbox.isChecked()) cert_path = cert_input.text().strip() param.SetString("SslCertPath", cert_path) @@ -2053,9 +2053,7 @@ class Silo_BOM: wu_table = QtGui.QTableWidget() wu_table.setColumnCount(5) - wu_table.setHorizontalHeaderLabels( - ["Parent Part Number", "Type", "Qty", "Unit", "Ref Des"] - ) + wu_table.setHorizontalHeaderLabels(["Parent Part Number", "Type", "Qty", "Unit", "Ref Des"]) wu_table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) wu_table.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) wu_table.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) @@ -2084,16 +2082,12 @@ class Silo_BOM: bom_table.setItem( row, 1, QtGui.QTableWidgetItem(entry.get("child_description", "")) ) - bom_table.setItem( - row, 2, QtGui.QTableWidgetItem(entry.get("rel_type", "")) - ) + bom_table.setItem(row, 2, QtGui.QTableWidgetItem(entry.get("rel_type", ""))) qty = entry.get("quantity") bom_table.setItem( row, 3, QtGui.QTableWidgetItem(str(qty) if qty is not None else "") ) - bom_table.setItem( - row, 4, QtGui.QTableWidgetItem(entry.get("unit") or "") - ) + bom_table.setItem(row, 4, QtGui.QTableWidgetItem(entry.get("unit") or "")) ref_des = entry.get("reference_designators") or [] bom_table.setItem(row, 5, QtGui.QTableWidgetItem(", ".join(ref_des))) bom_table.setItem( @@ -2115,16 +2109,12 @@ class Silo_BOM: wu_table.setItem( row, 0, QtGui.QTableWidgetItem(entry.get("parent_part_number", "")) ) - wu_table.setItem( - row, 1, QtGui.QTableWidgetItem(entry.get("rel_type", "")) - ) + wu_table.setItem(row, 1, QtGui.QTableWidgetItem(entry.get("rel_type", ""))) qty = entry.get("quantity") wu_table.setItem( row, 2, QtGui.QTableWidgetItem(str(qty) if qty is not None else "") ) - wu_table.setItem( - row, 3, QtGui.QTableWidgetItem(entry.get("unit") or "") - ) + wu_table.setItem(row, 3, QtGui.QTableWidgetItem(entry.get("unit") or "")) ref_des = entry.get("reference_designators") or [] wu_table.setItem(row, 4, QtGui.QTableWidgetItem(", ".join(ref_des))) wu_table.resizeColumnsToContents() @@ -2177,9 +2167,7 @@ class Silo_BOM: try: qty = float(qty_text) except ValueError: - QtGui.QMessageBox.warning( - dialog, "BOM", "Quantity must be a number." - ) + QtGui.QMessageBox.warning(dialog, "BOM", "Quantity must be a number.") return unit = unit_input.text().strip() or None @@ -2258,9 +2246,7 @@ class Silo_BOM: try: new_qty = float(qty_text) except ValueError: - QtGui.QMessageBox.warning( - dialog, "BOM", "Quantity must be a number." - ) + QtGui.QMessageBox.warning(dialog, "BOM", "Quantity must be a number.") return new_unit = unit_input.text().strip() or None @@ -2284,9 +2270,7 @@ class Silo_BOM: ) load_bom() except Exception as exc: - QtGui.QMessageBox.warning( - dialog, "BOM", f"Failed to update entry:\n{exc}" - ) + QtGui.QMessageBox.warning(dialog, "BOM", f"Failed to update entry:\n{exc}") def on_remove(): selected = bom_table.selectedItems() @@ -2312,9 +2296,7 @@ class Silo_BOM: _client.delete_bom_entry(part_number, child_pn) load_bom() except Exception as exc: - QtGui.QMessageBox.warning( - dialog, "BOM", f"Failed to remove entry:\n{exc}" - ) + QtGui.QMessageBox.warning(dialog, "BOM", f"Failed to remove entry:\n{exc}") add_btn.clicked.connect(on_add) edit_btn.clicked.connect(on_edit) @@ -2353,9 +2335,7 @@ class SiloEventListener(QtCore.QThread): item_updated = QtCore.Signal(str) # part_number revision_created = QtCore.Signal(str, int) # part_number, revision - connection_status = QtCore.Signal( - str, int, str - ) # (status, retry_count, error_message) + connection_status = QtCore.Signal(str, int, str) # (status, retry_count, error_message) server_mode_changed = QtCore.Signal(str) # "normal" / "read-only" / "degraded" # DAG events @@ -2441,9 +2421,7 @@ class SiloEventListener(QtCore.QThread): req = urllib.request.Request(url, headers=headers, method="GET") try: - self._response = urllib.request.urlopen( - req, context=_get_ssl_context(), timeout=90 - ) + self._response = urllib.request.urlopen(req, context=_get_ssl_context(), timeout=90) except urllib.error.HTTPError as e: if e.code in (404, 501): raise _SSEUnsupported() @@ -2669,9 +2647,7 @@ class SiloAuthDockWidget: layout.addLayout(btn_row) # Keep the auth panel compact so the Activity panel below gets more space - self.widget.setSizePolicy( - QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Maximum - ) + self.widget.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Maximum) # -- Status refresh ----------------------------------------------------- @@ -2785,9 +2761,7 @@ class SiloAuthDockWidget: FreeCAD.Console.PrintMessage("Silo: SSE connected\n") self._seed_activity_feed() elif status == "disconnected": - self._sse_label.setText( - f"Reconnecting ({retry}/{SiloEventListener._MAX_RETRIES})..." - ) + self._sse_label.setText(f"Reconnecting ({retry}/{SiloEventListener._MAX_RETRIES})...") self._sse_label.setStyleSheet("font-size: 11px; color: #FF9800;") self._sse_label.setToolTip(error or "Connection lost") FreeCAD.Console.PrintWarning( @@ -2797,9 +2771,7 @@ class SiloAuthDockWidget: self._sse_label.setText("Disconnected") self._sse_label.setStyleSheet("font-size: 11px; color: #F44336;") self._sse_label.setToolTip(error or "Max retries reached") - FreeCAD.Console.PrintError( - f"Silo: SSE gave up after {retry} retries: {error}\n" - ) + FreeCAD.Console.PrintError(f"Silo: SSE gave up after {retry} retries: {error}\n") elif status == "unsupported": self._sse_label.setText("Not available") self._sse_label.setStyleSheet("font-size: 11px; color: #888;") @@ -2843,17 +2815,11 @@ class SiloAuthDockWidget: self._append_activity_event(f"{part_number} updated", part_number) def _on_remote_revision(self, part_number, revision): - FreeCAD.Console.PrintMessage( - f"Silo: New revision {revision} for {part_number}\n" - ) + FreeCAD.Console.PrintMessage(f"Silo: New revision {revision} for {part_number}\n") mw = FreeCADGui.getMainWindow() if mw is not None: - mw.statusBar().showMessage( - f"Silo: {part_number} rev {revision} available", 5000 - ) - self._append_activity_event( - f"{part_number} Rev {revision} created", part_number - ) + mw.statusBar().showMessage(f"Silo: {part_number} rev {revision} available", 5000) + self._append_activity_event(f"{part_number} Rev {revision} created", part_number) def _append_activity_event(self, text, pn=""): """Prepend an event to the activity feed and rebuild the display.""" @@ -2879,9 +2845,9 @@ class SiloAuthDockWidget: ts = datetime.now() if updated: try: - ts = datetime.fromisoformat( - updated.replace("Z", "+00:00") - ).replace(tzinfo=None) + ts = datetime.fromisoformat(updated.replace("Z", "+00:00")).replace( + tzinfo=None + ) except (ValueError, AttributeError): pass self._activity_events.insert(0, (ts, text, pn)) @@ -2929,12 +2895,10 @@ class SiloAuthDockWidget: def _on_dag_updated(self, part_number, node_count, edge_count): FreeCAD.Console.PrintMessage( - f"Silo: DAG updated for {part_number}" - f" ({node_count} nodes, {edge_count} edges)\n" + f"Silo: DAG updated for {part_number} ({node_count} nodes, {edge_count} edges)\n" ) self._append_activity_event( - f"\u25b6 {part_number} \u2013 DAG synced" - f" ({node_count} nodes, {edge_count} edges)", + f"\u25b6 {part_number} \u2013 DAG synced ({node_count} nodes, {edge_count} edges)", part_number, ) @@ -2945,15 +2909,12 @@ class SiloAuthDockWidget: else: status = f"\u2717 FAIL ({failed_count} failed)" FreeCAD.Console.PrintWarning( - f"Silo: Validation failed for {part_number}" - f" ({failed_count} features failed)\n" + f"Silo: Validation failed for {part_number} ({failed_count} features failed)\n" ) self._append_activity_event(f"{status} \u2013 {part_number}", part_number) def _on_job_created(self, job_id, definition_name, part_number): - FreeCAD.Console.PrintMessage( - f"Silo: Job {definition_name} created for {part_number}\n" - ) + FreeCAD.Console.PrintMessage(f"Silo: Job {definition_name} created for {part_number}\n") self._append_activity_event( f"\u23f3 {part_number} \u2013 {definition_name} queued", part_number, @@ -2968,14 +2929,10 @@ class SiloAuthDockWidget: self._append_activity_event(f"\u2717 Job {job_id[:8]} failed: {error}") def _on_job_claimed(self, job_id, runner_id): - FreeCAD.Console.PrintMessage( - f"Silo: Job {job_id[:8]} claimed by runner {runner_id}\n" - ) + FreeCAD.Console.PrintMessage(f"Silo: Job {job_id[:8]} claimed by runner {runner_id}\n") def _on_job_progress(self, job_id, progress, message): - FreeCAD.Console.PrintMessage( - f"Silo: Job {job_id[:8]} progress {progress}%: {message}\n" - ) + FreeCAD.Console.PrintMessage(f"Silo: Job {job_id[:8]} progress {progress}%: {message}\n") def _on_job_cancelled(self, job_id): FreeCAD.Console.PrintMessage(f"Silo: Job {job_id[:8]} cancelled\n") @@ -3306,15 +3263,9 @@ class JobMonitorDialog: status = job.get("status", "") icon = _STATUS_ICONS.get(status, "?") self._table.setItem(row, 0, QtGui.QTableWidgetItem(f"{icon} {status}")) - self._table.setItem( - row, 1, QtGui.QTableWidgetItem(job.get("definition_name", "")) - ) - self._table.setItem( - row, 2, QtGui.QTableWidgetItem(job.get("part_number", "")) - ) - self._table.setItem( - row, 3, QtGui.QTableWidgetItem(job.get("runner_name", "")) - ) + self._table.setItem(row, 1, QtGui.QTableWidgetItem(job.get("definition_name", ""))) + self._table.setItem(row, 2, QtGui.QTableWidgetItem(job.get("part_number", ""))) + self._table.setItem(row, 3, QtGui.QTableWidgetItem(job.get("runner_name", ""))) progress = job.get("progress", 0) progress_msg = job.get("progress_message", "") @@ -3377,8 +3328,7 @@ class JobMonitorDialog: reply = QtGui.QMessageBox.question( self.dialog, "Cancel Job", - f"Cancel job {job.get('definition_name', '')} for " - f"{job.get('part_number', '')}?", + f"Cancel job {job.get('definition_name', '')} for {job.get('part_number', '')}?", QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, ) if reply != QtGui.QMessageBox.Yes: @@ -3387,9 +3337,7 @@ class JobMonitorDialog: _client.cancel_job(job_id) FreeCAD.Console.PrintMessage(f"Silo: Job {job_id} cancelled\n") except Exception as e: - QtGui.QMessageBox.warning( - self.dialog, "Cancel Failed", f"Failed to cancel job:\n{e}" - ) + QtGui.QMessageBox.warning(self.dialog, "Cancel Failed", f"Failed to cancel job:\n{e}") self._refresh() def _trigger_job(self): @@ -3398,9 +3346,7 @@ class JobMonitorDialog: try: definitions = _client.list_job_definitions() except Exception as e: - QtGui.QMessageBox.warning( - self.dialog, "Error", f"Failed to load job definitions:\n{e}" - ) + QtGui.QMessageBox.warning(self.dialog, "Error", f"Failed to load job definitions:\n{e}") return if not definitions: @@ -3428,13 +3374,9 @@ class JobMonitorDialog: try: result = _client.trigger_job(name, part_number=pn) - FreeCAD.Console.PrintMessage( - f"Silo: Job triggered: {result.get('id', '')}\n" - ) + FreeCAD.Console.PrintMessage(f"Silo: Job triggered: {result.get('id', '')}\n") except Exception as e: - QtGui.QMessageBox.warning( - self.dialog, "Trigger Failed", f"Failed to trigger job:\n{e}" - ) + QtGui.QMessageBox.warning(self.dialog, "Trigger Failed", f"Failed to trigger job:\n{e}") self._refresh() def on_job_event(self): @@ -3496,9 +3438,7 @@ class RunnerAdminDialog: # Runner table self._table = QtGui.QTableWidget() self._table.setColumnCount(5) - self._table.setHorizontalHeaderLabels( - ["Name", "Tags", "Status", "Last Heartbeat", "Jobs"] - ) + self._table.setHorizontalHeaderLabels(["Name", "Tags", "Status", "Last Heartbeat", "Jobs"]) self._table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) self._table.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) self._table.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) @@ -3554,9 +3494,7 @@ class RunnerAdminDialog: def _register_runner(self): from PySide import QtGui - name, ok = QtGui.QInputDialog.getText( - self.dialog, "Register Runner", "Runner name:" - ) + name, ok = QtGui.QInputDialog.getText(self.dialog, "Register Runner", "Runner name:") if not ok or not name: return @@ -3882,9 +3820,7 @@ class Silo_StartPanel: dock = QtGui.QDockWidget("Silo", mw) dock.setObjectName("SiloStartPanel") dock.setWidget(content.widget) - dock.setAllowedAreas( - QtCore.Qt.LeftDockWidgetArea | QtCore.Qt.RightDockWidgetArea - ) + dock.setAllowedAreas(QtCore.Qt.LeftDockWidgetArea | QtCore.Qt.RightDockWidgetArea) mw.addDockWidget(QtCore.Qt.LeftDockWidgetArea, dock) def IsActive(self): @@ -3918,9 +3854,7 @@ class _DiagWorker(QtCore.QThread): self.result.emit("DNS", False, "no hostname in URL") return try: - addrs = socket.getaddrinfo( - hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM - ) + addrs = socket.getaddrinfo(hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM) first_ip = addrs[0][4][0] if addrs else "?" self.result.emit("DNS", True, f"{hostname} -> {first_ip}") except socket.gaierror as e: diff --git a/silo-client b/silo-client index 9b71cf0..8c4fb4c 160000 --- a/silo-client +++ b/silo-client @@ -1 +1 @@ -Subproject commit 9b71cf037555b111bb1345c9d93743693d40e68d +Subproject commit 8c4fb4c4330dd6b63243a791288ba8f2e1d7a05e