Compare commits

..

15 Commits

Author SHA1 Message Date
af98994a53 Merge pull request 'chore: update silo-client pointer to main merge commit' (#48) from fix/silo-client-pointer into main
Reviewed-on: #48
2026-02-19 01:58:00 +00:00
Zoe Forbes
d266bfb653 chore: update silo-client pointer to main merge commit
Points to silo-client main (285bd1f) which includes the merged
kc-metadata-api methods from PR #19.
2026-02-18 19:56:58 -06:00
a92174e0b9 Merge pull request 'chore: bump silo-client to feat/kc-metadata-api' (#47) from feat/kc-metadata-api into main
Reviewed-on: #47
2026-02-19 01:43:01 +00:00
Zoe Forbes
edbaf65923 chore: bump silo-client to feat/kc-metadata-api
Tracks new .kc metadata and dependency resolution API methods
needed by Create module server integration (silo-mod#43).
2026-02-18 19:35:41 -06:00
80f8ec27a0 Merge pull request 'chore(deps): update silo-client to c5c8288e' (#36) from auto/update-silo-client-c5c8288e into main
Reviewed-on: #36
2026-02-18 22:34:25 +00:00
kindred-bot
6b3e8b7518 chore(deps): update silo-client to c5c8288e
Upstream: c5c8288eeb
2026-02-18 21:06:32 +00:00
b3fe98c696 Merge pull request 'fix: remove MinIO references and degraded mode' (#35) from fix/remove-minio-references into main
Reviewed-on: #35
2026-02-18 20:56:18 +00:00
Zoe Forbes
c537e2f08f fix: remove MinIO references and degraded mode
The silo server now uses filesystem storage instead of MinIO.

- Remove all MinIO references from docstrings, tooltips, and UI text
- Remove obsolete 'degraded' server mode (no separate storage service)
- Update Silo_Info display: 'File in MinIO' → 'File on Server'
- Update SiloOrigin class docstring
2026-02-18 14:55:57 -06:00
29b1f32fd9 Merge pull request 'refactor: migrate to kindred-addon-sdk for overlay, origin, and theme' (#34) from feat/migrate-to-sdk into main
Reviewed-on: #34
2026-02-17 17:05:34 +00:00
dca6380199 refactor: migrate to kindred-addon-sdk for overlay, origin, and theme (#250)
Replace FreeCADGui.registerEditingOverlay() with kindred_sdk.register_overlay().
Replace FreeCADGui.addOrigin()/removeOrigin() with kindred_sdk wrappers.
Replace hardcoded _MOCHA dict with kindred_sdk.get_theme_tokens().
Add sdk dependency to package.xml <kindred> element.
2026-02-17 11:03:21 -06:00
27f0cc0f34 feat: add <kindred> element to package.xml
Declares min_create_version=0.1.0, load_priority=60, pure_python=true,
and documents universal overlay context.
2026-02-17 11:03:20 -06:00
a5eff534b5 Merge pull request 'chore(deps): update silo-client to 5e6f2cb9' (#33) from auto/update-silo-client-5e6f2cb9 into main
Reviewed-on: #33
2026-02-17 14:48:26 +00:00
kindred-bot
1001424b16 chore(deps): update silo-client to 5e6f2cb9
Upstream: 5e6f2cb963
2026-02-17 14:48:03 +00:00
7e3127498a Merge pull request 'feat(schema): make schema name configurable (closes #28)' (#32) from feat/configurable-schema-name into main
Reviewed-on: #32
2026-02-16 19:20:02 +00:00
82d8741059 feat(schema): make schema name configurable, update silo-client submodule
Replace all hardcoded 'kindred-rd' schema references with the new
configurable get_schema_name() setting. Priority: FreeCAD preference
SchemaName > SILO_SCHEMA env var > default 'kindred-rd'.

- Add get_schema_name() to FreeCADSiloSettings and _get_schema_name() helper
- Add Schema Name field to Settings dialog
- Replace raw urllib calls in schema_form.py with SiloClient methods
  (get_property_schema, generate_part_number)
- Inline parse_part_number/sanitize_filename (removed from silo-client)
- Simplify category folder naming to use category code directly
- Update silo-client submodule to origin/main + configurable schema branch

Closes #28
2026-02-16 13:17:50 -06:00
10 changed files with 119 additions and 104 deletions

View File

@@ -106,7 +106,9 @@ def _register_silo_overlay():
return False
try:
FreeCADGui.registerEditingOverlay(
from kindred_sdk import register_overlay
register_overlay(
"silo", # overlay id
["Silo Origin"], # toolbar names to append
_silo_overlay_match, # match function

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -12,4 +12,17 @@
<subdirectory>./</subdirectory>
</workbench>
</content>
<!-- Kindred Create extensions -->
<kindred>
<min_create_version>0.1.0</min_create_version>
<load_priority>60</load_priority>
<pure_python>true</pure_python>
<dependencies>
<dependency>sdk</dependency>
</dependencies>
<contexts>
<context id="*" action="overlay"/>
</contexts>
</kindred>
</package>

View File

@@ -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"],

View File

@@ -14,14 +14,7 @@ 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"
@@ -32,6 +25,27 @@ SILO_PROJECTS_DIR = os.environ.get(
)
# ---------------------------------------------------------------------------
# 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):
"""Format a datetime as a human-friendly relative string."""
now = datetime.now()
@@ -96,6 +110,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 +160,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()
@@ -172,13 +197,13 @@ def _clear_auth():
# Server mode tracking
# ---------------------------------------------------------------------------
_server_mode = "offline" # "normal" | "read-only" | "degraded" | "offline"
_server_mode = "offline" # "normal" | "read-only" | "offline"
def _fetch_server_mode() -> str:
"""Fetch server mode from the /ready endpoint.
Returns one of: "normal", "read-only", "degraded", "offline".
Returns one of: "normal", "read-only", "offline".
"""
api_url = _get_api_url().rstrip("/")
base_url = api_url[:-4] if api_url.endswith("/api") else api_url
@@ -193,8 +218,6 @@ def _fetch_server_mode() -> str:
return "normal"
if status in ("read-only", "read_only", "readonly"):
return "read-only"
if status in ("degraded",):
return "degraded"
# Unknown status but server responded — treat as normal
return "normal"
except Exception:
@@ -234,24 +257,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 +537,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)
@@ -547,7 +568,7 @@ class SiloSync:
def upload_file(
self, part_number: str, file_path: str, comment: str = "Auto-save"
) -> Optional[Dict]:
"""Upload file to MinIO."""
"""Upload file to the server."""
try:
doc = FreeCAD.openDocument(file_path)
if not doc:
@@ -561,7 +582,7 @@ class SiloSync:
return None
def download_file(self, part_number: str) -> Optional[Path]:
"""Download latest file from MinIO."""
"""Download the latest revision file from the server."""
try:
item = self.client.get_item(part_number)
file_path = get_cad_file_path(part_number, item.get("description", ""))
@@ -760,12 +781,12 @@ def _push_dag_after_upload(doc, part_number, revision_number):
class Silo_Save:
"""Save locally and upload to MinIO."""
"""Save locally and upload to the server."""
def GetResources(self):
return {
"MenuText": "Save",
"ToolTip": "Save locally and upload to MinIO (Ctrl+S)",
"ToolTip": "Save locally and upload to server (Ctrl+S)",
"Pixmap": _icon("save"),
}
@@ -820,7 +841,7 @@ class Silo_Save:
FreeCAD.Console.PrintMessage(f"Saved: {file_path}\n")
# Try to upload to MinIO
# Try to upload to server
try:
result = _client._upload_file(
part_number, str(file_path), properties, "Auto-save"
@@ -1099,12 +1120,12 @@ def _pull_dependencies(part_number, progress_callback=None):
class Silo_Pull:
"""Download from MinIO / sync from database."""
"""Download revision file from the server."""
def GetResources(self):
return {
"MenuText": "Pull",
"ToolTip": "Download from MinIO with revision selection",
"ToolTip": "Download file with revision selection",
"Pixmap": _icon("pull"),
}
@@ -1260,12 +1281,12 @@ class Silo_Pull:
class Silo_Push:
"""Upload local files to MinIO."""
"""Upload local files to the server."""
def GetResources(self):
return {
"MenuText": "Push",
"ToolTip": "Upload local files that aren't in MinIO",
"ToolTip": "Upload local files that aren't on the server",
"Pixmap": _icon("push"),
}
@@ -1386,7 +1407,7 @@ class Silo_Info:
msg += f"<p><b>Local Revision:</b> {getattr(obj, 'SiloRevision', '-')}</p>"
has_file, _ = _client.has_file(part_number)
msg += f"<p><b>File in MinIO:</b> {'Yes' if has_file else 'No'}</p>"
msg += f"<p><b>File on Server:</b> {'Yes' if has_file else 'No'}</p>"
# Show current revision status
if revisions:
@@ -1769,6 +1790,31 @@ 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))
@@ -1911,6 +1957,7 @@ class Silo_Settings:
auth_display = "not configured"
status_label = QtGui.QLabel(
f"<b>Active URL:</b> {_get_api_url()}<br>"
f"<b>Schema:</b> {_get_schema_name()}<br>"
f"<b>SSL verification:</b> {'enabled' if _get_ssl_verify() else 'disabled'}<br>"
f"<b>CA certificate:</b> {cert_display}<br>"
f"<b>Authentication:</b> {auth_display}"
@@ -1932,6 +1979,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)
@@ -2356,7 +2404,7 @@ class SiloEventListener(QtCore.QThread):
connection_status = QtCore.Signal(
str, int, str
) # (status, retry_count, error_message)
server_mode_changed = QtCore.Signal(str) # "normal" / "read-only" / "degraded"
server_mode_changed = QtCore.Signal(str) # "normal" / "read-only" / "offline"
# DAG events
dag_updated = QtCore.Signal(str, int, int) # part_number, node_count, edge_count
@@ -2819,11 +2867,6 @@ class SiloAuthDockWidget:
"background: #FFC107; color: #000; padding: 4px; font-size: 11px;",
True,
),
"degraded": (
"MinIO unavailable \u2014 file ops limited",
"background: #FF9800; color: #000; padding: 4px; font-size: 11px;",
True,
),
"offline": (
"Disconnected from silo",
"background: #F44336; color: #fff; padding: 4px; font-size: 11px;",
@@ -2929,12 +2972,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,8 +2986,7 @@ 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)
@@ -3377,8 +3417,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:

View File

@@ -30,7 +30,7 @@ class SiloOrigin:
Key behaviors:
- Documents are always stored locally (hybrid local-remote model)
- Database tracks metadata, part numbers, and revision history
- MinIO stores revision snapshots for sync/backup
- Server stores revision files for sync/backup
- Identity is tracked by UUID (SiloItemId), displayed as part number
"""
@@ -388,9 +388,7 @@ class SiloOrigin:
# Upload to Silo
properties = collect_document_properties(doc)
_client._upload_file(
obj.SiloPartNumber, str(file_path), properties, comment=""
)
_client._upload_file(obj.SiloPartNumber, str(file_path), properties, comment="")
# Clear modified flag (Modified is on Gui.Document, not App.Document)
gui_doc = FreeCADGui.getDocument(doc.Name)
@@ -567,12 +565,9 @@ def register_silo_origin():
This should be called during workbench initialization to make
Silo available as a file origin.
"""
origin = get_silo_origin()
try:
FreeCADGui.addOrigin(origin)
FreeCAD.Console.PrintLog("Registered Silo origin\n")
except Exception as e:
FreeCAD.Console.PrintWarning(f"Could not register Silo origin: {e}\n")
from kindred_sdk import register_origin
register_origin(get_silo_origin())
def unregister_silo_origin():
@@ -582,9 +577,7 @@ def unregister_silo_origin():
"""
global _silo_origin
if _silo_origin:
try:
FreeCADGui.removeOrigin(_silo_origin)
FreeCAD.Console.PrintLog("Unregistered Silo origin\n")
except Exception as e:
FreeCAD.Console.PrintWarning(f"Could not unregister Silo origin: {e}\n")
from kindred_sdk import unregister_origin
unregister_origin(_silo_origin)
_silo_origin = None

View File

@@ -19,23 +19,10 @@ from PySide import QtCore, QtGui, QtWidgets
# ---------------------------------------------------------------------------
# Catppuccin Mocha palette
# ---------------------------------------------------------------------------
_MOCHA = {
"base": "#1e1e2e",
"mantle": "#181825",
"crust": "#11111b",
"surface0": "#313244",
"surface1": "#45475a",
"surface2": "#585b70",
"text": "#cdd6f4",
"subtext0": "#a6adc8",
"subtext1": "#bac2de",
"blue": "#89b4fa",
"green": "#a6e3a1",
"red": "#f38ba8",
"peach": "#fab387",
"lavender": "#b4befe",
"overlay0": "#6c7086",
}
# Catppuccin Mocha palette — sourced from kindred-addon-sdk
from kindred_sdk.theme import get_theme_tokens
_MOCHA = get_theme_tokens()
_PREF_GROUP = "User parameter:BaseApp/Preferences/Mod/KindredSilo"