Silo origin adapter #11

Closed
opened 2026-02-05 18:26:58 +00:00 by forbes · 0 comments
Owner

Overview

Adapt the existing Silo workbench code to implement the FileOrigin interface. This creates a bridge between the new origin system and the existing Silo functionality, allowing Silo instances to be used as file origins.

Key Understanding: Silo is a hybrid local-remote system:

  • Documents are always stored locally in ~/projects/cad/{category}/{part_number}_{desc}.FCStd
  • The database tracks metadata, part numbers, and revision history
  • MinIO stores revision snapshots for sync/backup
  • Local files are a working cache, not a full replication of the database

Parent Issue

Epic: #8 Unified File Origin System

Goals

  1. Create SiloOrigin class implementing FileOrigin interface
  2. Wrap existing Silo commands into origin operations
  3. Support multiple Silo instances (each is a separate origin)
  4. Handle connection state and authentication
  5. Maintain backwards compatibility with existing Silo code

Detailed Design

Silo Workflow Model

┌─────────────────────────────────────────────────────────────────────┐
│                         SILO ARCHITECTURE                           │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   ┌─────────────┐         ┌─────────────┐         ┌─────────────┐  │
│   │   Silo DB   │◄───────►│  Silo API   │◄───────►│   MinIO     │  │
│   │  (metadata) │         │  (gateway)  │         │  (files)    │  │
│   └─────────────┘         └──────┬──────┘         └─────────────┘  │
│                                  │                                  │
│                                  ▼                                  │
│   ┌──────────────────────────────────────────────────────────────┐ │
│   │                    LOCAL FILESYSTEM                          │ │
│   │  ~/projects/cad/                                             │ │
│   │  ├── F01_fasteners/                                          │ │
│   │  │   ├── F01-0001_M3_Screw.FCStd    ← Working copy          │ │
│   │  │   └── F01-0002_M4_Bolt.FCStd                              │ │
│   │  └── C01_couplings/                                          │ │
│   │      └── C01-0001_Shaft_Coupler.FCStd                        │ │
│   └──────────────────────────────────────────────────────────────┘ │
│                                                                     │
│   Documents contain: SiloPartNumber, SiloRevision, SiloOriginId    │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

SiloOrigin Class (Python)

# mods/silo/pkg/freecad/silo_origin.py

class SiloOrigin(QtCore.QObject):
    """FileOrigin implementation for a Silo instance."""
    
    connectionStateChanged = QtCore.Signal(int)
    operationStarted = QtCore.Signal(str)
    operationFinished = QtCore.Signal(str, bool)
    
    def __init__(self, config: dict):
        super().__init__()
        self._id = config.get("id", "silo-default")
        self._nickname = config.get("nickname", "Silo")
        self._api_url = config.get("api_url")
        self._sync = SiloSync(self._api_url, ...)
    
    # Workflow characteristics
    def tracks_externally(self) -> bool:
        return True  # Silo tracks documents in database
    
    # Document identity is part number, not file path
    def document_identity(self, doc) -> str:
        obj = get_tracked_object(doc)
        if obj and hasattr(obj, "SiloPartNumber"):
            return obj.SiloPartNumber
        return ""
    
    # === Core Operations ===
    
    def new_document(self):
        """Show part creation form with category selection from schema YAML."""
        # 1. Load part numbering schema from Silo
        # 2. Show category selection dialog
        # 3. User fills in description, selects category
        # 4. Generate part number via API
        # 5. Create new FreeCAD doc with Silo properties
        # 6. Save to canonical path: ~/projects/cad/{cat}/{pn}_{desc}.FCStd
        from silo_commands import _new_item_dialog
        _new_item_dialog(self._sync)
    
    def open_document(self):
        """Show database search pane with local file status."""
        # 1. Show search dialog querying Silo database
        # 2. Results show: part number, description, local status
        #    - "Local" = file exists in ~/projects/cad/
        #    - "Remote" = only in database, needs download
        # 3. On selection:
        #    - If local: FreeCAD.openDocument(local_path)
        #    - If remote: Download from MinIO, then open
        from silo_commands import _open_item_dialog
        _open_item_dialog(self._sync)
    
    def save_document(self, doc):
        """Save locally + sync to MinIO."""
        # 1. Save to local canonical path
        # 2. Upload to MinIO (creates new revision)
        # This is a SYNC operation - local save always succeeds,
        # upload may fail (offline mode graceful degradation)
        from silo_commands import _save_to_silo
        _save_to_silo(doc, self._sync)
    
    def save_document_as(self, doc):
        """Handle Save As based on document state."""
        if not self.owns_document(doc):
            # Local doc being migrated to Silo
            # Show part creation form, register in DB, add Silo properties
            self._migrate_to_silo(doc)
        else:
            # Already Silo doc - this is a COPY operation
            # Create new part number, copy content
            self._copy_silo_document(doc)
    
    def _migrate_to_silo(self, doc):
        """Migrate a local document to Silo tracking."""
        # 1. Show part creation form (category, description)
        # 2. Generate new part number via API
        # 3. Add SiloPartNumber, SiloRevision, SiloOriginId properties
        # 4. Move file to canonical path
        # 5. Upload to MinIO
        pass
    
    def _copy_silo_document(self, doc):
        """Create a copy of a Silo document with new part number."""
        # 1. Show part creation form (pre-fill from original)
        # 2. Generate new part number
        # 3. Save copy with new properties
        # 4. Upload to MinIO
        pass
    
    # === Document Ownership ===
    
    def owns_document(self, doc) -> bool:
        """Check if document is tracked by THIS Silo instance."""
        # Check for SiloOriginId property matching this instance
        if hasattr(doc, "SiloOriginId"):
            return doc.SiloOriginId == self._id
        # Legacy: check for SiloPartNumber without origin ID
        obj = get_tracked_object(doc)
        return obj is not None and hasattr(obj, "SiloPartNumber")
    
    # === Extended Operations ===
    
    def commit_document(self, doc):
        """Create named revision with comment."""
        from silo_commands import _commit_dialog
        _commit_dialog(doc, self._sync)
    
    def pull_document(self, doc):
        """Download specific revision from MinIO."""
        from silo_commands import _pull_dialog
        _pull_dialog(doc, self._sync)
    
    def show_bom(self, doc):
        """Show Bill of Materials dialog."""
        from silo_commands import _show_bom_dialog
        _show_bom_dialog(doc, self._sync)

Open Dialog: Database Search Pane

The Open operation shows a search interface querying the database:

┌─────────────────────────────────────────────────────────────────────┐
│ Open from Silo (Work)                                         [X]  │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Search: [M3 screw________________] [🔍]                           │
│                                                                     │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ Part #     │ Description          │ Category    │ Status    │   │
│  ├────────────┼──────────────────────┼─────────────┼───────────┤   │
│  │ F01-0001   │ M3x10 Socket Screw   │ Fasteners   │ ● Local   │   │
│  │ F01-0002   │ M3x16 Socket Screw   │ Fasteners   │ ● Local   │   │
│  │ F01-0015   │ M3x20 Button Head    │ Fasteners   │ ○ Remote  │   │
│  │ F01-0023   │ M3 Nylock Nut        │ Fasteners   │ ● Local   │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
│  ● Local = file exists locally    ○ Remote = download required     │
│                                                                     │
├─────────────────────────────────────────────────────────────────────┤
│                                              [Cancel]     [Open]   │
└─────────────────────────────────────────────────────────────────────┘

Document Properties (Silo Tracking)

Documents tracked by Silo have these properties on the primary object:

# Set when document is created/migrated to Silo
obj.addProperty("App::PropertyString", "SiloPartNumber", "Silo")
obj.addProperty("App::PropertyInteger", "SiloRevision", "Silo")
obj.addProperty("App::PropertyString", "SiloOriginId", "Silo")  # NEW: multi-instance support
obj.addProperty("App::PropertyString", "SiloItemType", "Silo")  # "part" or "assembly"

Implementation Tasks

  • Create SiloOrigin Python class
  • Implement tracks_externally() returning True
  • Implement document_identity() returning part number
  • Implement owns_document() checking Silo properties
  • Refactor open_document() to show database search pane
  • Implement save_document() as sync operation
  • Implement save_document_as() with migration vs copy logic
  • Add SiloOriginId property for multi-instance support
  • Migrate existing Silo commands to be callable from origin
  • Handle connection state and offline mode

Files to Create/Modify

  • mods/silo/pkg/freecad/silo_origin.py (new)
  • mods/silo/pkg/freecad/silo_commands.py (refactor for reuse)
  • mods/silo/pkg/freecad/InitGui.py (register origins)

Acceptance Criteria

  • SiloOrigin implements full FileOrigin interface
  • Open shows database search pane with local status
  • Save performs local save + MinIO sync
  • Save As migrates local docs or copies Silo docs
  • Document ownership determined by Silo properties
  • Multiple Silo instances can be registered
  • Offline mode gracefully degrades (local save succeeds)
  • Existing Silo functionality unchanged

Dependencies

  • #9 Origin abstraction layer

Blocking

  • #12 Modify Std_* commands
  • #15 Multi-instance Silo configuration
## Overview Adapt the existing Silo workbench code to implement the `FileOrigin` interface. This creates a bridge between the new origin system and the existing Silo functionality, allowing Silo instances to be used as file origins. **Key Understanding**: Silo is a hybrid local-remote system: - Documents are **always stored locally** in `~/projects/cad/{category}/{part_number}_{desc}.FCStd` - The database tracks metadata, part numbers, and revision history - MinIO stores revision snapshots for sync/backup - Local files are a **working cache**, not a full replication of the database ## Parent Issue Epic: #8 Unified File Origin System ## Goals 1. Create `SiloOrigin` class implementing `FileOrigin` interface 2. Wrap existing Silo commands into origin operations 3. Support multiple Silo instances (each is a separate origin) 4. Handle connection state and authentication 5. Maintain backwards compatibility with existing Silo code ## Detailed Design ### Silo Workflow Model ``` ┌─────────────────────────────────────────────────────────────────────┐ │ SILO ARCHITECTURE │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Silo DB │◄───────►│ Silo API │◄───────►│ MinIO │ │ │ │ (metadata) │ │ (gateway) │ │ (files) │ │ │ └─────────────┘ └──────┬──────┘ └─────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ LOCAL FILESYSTEM │ │ │ │ ~/projects/cad/ │ │ │ │ ├── F01_fasteners/ │ │ │ │ │ ├── F01-0001_M3_Screw.FCStd ← Working copy │ │ │ │ │ └── F01-0002_M4_Bolt.FCStd │ │ │ │ └── C01_couplings/ │ │ │ │ └── C01-0001_Shaft_Coupler.FCStd │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │ │ Documents contain: SiloPartNumber, SiloRevision, SiloOriginId │ │ │ └─────────────────────────────────────────────────────────────────────┘ ``` ### SiloOrigin Class (Python) ```python # mods/silo/pkg/freecad/silo_origin.py class SiloOrigin(QtCore.QObject): """FileOrigin implementation for a Silo instance.""" connectionStateChanged = QtCore.Signal(int) operationStarted = QtCore.Signal(str) operationFinished = QtCore.Signal(str, bool) def __init__(self, config: dict): super().__init__() self._id = config.get("id", "silo-default") self._nickname = config.get("nickname", "Silo") self._api_url = config.get("api_url") self._sync = SiloSync(self._api_url, ...) # Workflow characteristics def tracks_externally(self) -> bool: return True # Silo tracks documents in database # Document identity is part number, not file path def document_identity(self, doc) -> str: obj = get_tracked_object(doc) if obj and hasattr(obj, "SiloPartNumber"): return obj.SiloPartNumber return "" # === Core Operations === def new_document(self): """Show part creation form with category selection from schema YAML.""" # 1. Load part numbering schema from Silo # 2. Show category selection dialog # 3. User fills in description, selects category # 4. Generate part number via API # 5. Create new FreeCAD doc with Silo properties # 6. Save to canonical path: ~/projects/cad/{cat}/{pn}_{desc}.FCStd from silo_commands import _new_item_dialog _new_item_dialog(self._sync) def open_document(self): """Show database search pane with local file status.""" # 1. Show search dialog querying Silo database # 2. Results show: part number, description, local status # - "Local" = file exists in ~/projects/cad/ # - "Remote" = only in database, needs download # 3. On selection: # - If local: FreeCAD.openDocument(local_path) # - If remote: Download from MinIO, then open from silo_commands import _open_item_dialog _open_item_dialog(self._sync) def save_document(self, doc): """Save locally + sync to MinIO.""" # 1. Save to local canonical path # 2. Upload to MinIO (creates new revision) # This is a SYNC operation - local save always succeeds, # upload may fail (offline mode graceful degradation) from silo_commands import _save_to_silo _save_to_silo(doc, self._sync) def save_document_as(self, doc): """Handle Save As based on document state.""" if not self.owns_document(doc): # Local doc being migrated to Silo # Show part creation form, register in DB, add Silo properties self._migrate_to_silo(doc) else: # Already Silo doc - this is a COPY operation # Create new part number, copy content self._copy_silo_document(doc) def _migrate_to_silo(self, doc): """Migrate a local document to Silo tracking.""" # 1. Show part creation form (category, description) # 2. Generate new part number via API # 3. Add SiloPartNumber, SiloRevision, SiloOriginId properties # 4. Move file to canonical path # 5. Upload to MinIO pass def _copy_silo_document(self, doc): """Create a copy of a Silo document with new part number.""" # 1. Show part creation form (pre-fill from original) # 2. Generate new part number # 3. Save copy with new properties # 4. Upload to MinIO pass # === Document Ownership === def owns_document(self, doc) -> bool: """Check if document is tracked by THIS Silo instance.""" # Check for SiloOriginId property matching this instance if hasattr(doc, "SiloOriginId"): return doc.SiloOriginId == self._id # Legacy: check for SiloPartNumber without origin ID obj = get_tracked_object(doc) return obj is not None and hasattr(obj, "SiloPartNumber") # === Extended Operations === def commit_document(self, doc): """Create named revision with comment.""" from silo_commands import _commit_dialog _commit_dialog(doc, self._sync) def pull_document(self, doc): """Download specific revision from MinIO.""" from silo_commands import _pull_dialog _pull_dialog(doc, self._sync) def show_bom(self, doc): """Show Bill of Materials dialog.""" from silo_commands import _show_bom_dialog _show_bom_dialog(doc, self._sync) ``` ### Open Dialog: Database Search Pane The Open operation shows a search interface querying the database: ``` ┌─────────────────────────────────────────────────────────────────────┐ │ Open from Silo (Work) [X] │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ Search: [M3 screw________________] [🔍] │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Part # │ Description │ Category │ Status │ │ │ ├────────────┼──────────────────────┼─────────────┼───────────┤ │ │ │ F01-0001 │ M3x10 Socket Screw │ Fasteners │ ● Local │ │ │ │ F01-0002 │ M3x16 Socket Screw │ Fasteners │ ● Local │ │ │ │ F01-0015 │ M3x20 Button Head │ Fasteners │ ○ Remote │ │ │ │ F01-0023 │ M3 Nylock Nut │ Fasteners │ ● Local │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ ● Local = file exists locally ○ Remote = download required │ │ │ ├─────────────────────────────────────────────────────────────────────┤ │ [Cancel] [Open] │ └─────────────────────────────────────────────────────────────────────┘ ``` ### Document Properties (Silo Tracking) Documents tracked by Silo have these properties on the primary object: ```python # Set when document is created/migrated to Silo obj.addProperty("App::PropertyString", "SiloPartNumber", "Silo") obj.addProperty("App::PropertyInteger", "SiloRevision", "Silo") obj.addProperty("App::PropertyString", "SiloOriginId", "Silo") # NEW: multi-instance support obj.addProperty("App::PropertyString", "SiloItemType", "Silo") # "part" or "assembly" ``` ## Implementation Tasks - [ ] Create `SiloOrigin` Python class - [ ] Implement `tracks_externally()` returning `True` - [ ] Implement `document_identity()` returning part number - [ ] Implement `owns_document()` checking Silo properties - [ ] Refactor `open_document()` to show database search pane - [ ] Implement `save_document()` as sync operation - [ ] Implement `save_document_as()` with migration vs copy logic - [ ] Add `SiloOriginId` property for multi-instance support - [ ] Migrate existing Silo commands to be callable from origin - [ ] Handle connection state and offline mode ## Files to Create/Modify - `mods/silo/pkg/freecad/silo_origin.py` (new) - `mods/silo/pkg/freecad/silo_commands.py` (refactor for reuse) - `mods/silo/pkg/freecad/InitGui.py` (register origins) ## Acceptance Criteria - [ ] `SiloOrigin` implements full `FileOrigin` interface - [ ] Open shows database search pane with local status - [ ] Save performs local save + MinIO sync - [ ] Save As migrates local docs or copies Silo docs - [ ] Document ownership determined by Silo properties - [ ] Multiple Silo instances can be registered - [ ] Offline mode gracefully degrades (local save succeeds) - [ ] Existing Silo functionality unchanged ## Dependencies - #9 Origin abstraction layer ## Blocking - #12 Modify Std_* commands - #15 Multi-instance Silo configuration
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: kindred/create#11