Source code for nexusLIMS.exporters.base

"""Base protocols and data structures for export destinations.

This module defines the core interfaces and data structures for the
NexusLIMS export framework, which allows records to be exported to
multiple repository destinations (CDCS, LabArchives, eLabFTW, etc.)
using a plugin-based architecture.
"""

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from datetime import datetime
from typing import TYPE_CHECKING, Any, Protocol

if TYPE_CHECKING:
    from pathlib import Path

    from nexusLIMS.harvesters.reservation_event import ReservationEvent
    from nexusLIMS.schemas.activity import AcquisitionActivity

_logger = logging.getLogger(__name__)


[docs] @dataclass class ExportResult: """Result of a single export attempt. Parameters ---------- success Whether the export succeeded destination_name Name of the destination plugin (e.g., "cdcs") record_id Destination-specific record identifier (if successful) record_url Direct URL to view the exported record (if successful) error_message Error message if export failed timestamp When the export attempt occurred metadata Additional destination-specific metadata """ success: bool destination_name: str record_id: str | None = None record_url: str | None = None error_message: str | None = None timestamp: datetime = field(default_factory=datetime.now) metadata: dict[str, Any] = field(default_factory=dict) def __repr__(self): """Return string representation of ExportResult.""" status = "SUCCESS" if self.success else "FAILED" return ( f"ExportResult(destination={self.destination_name}, " f"status={status}, " f"record_id={self.record_id})" )
[docs] @dataclass class ExportContext: """Context passed to export destination plugins. Provides all necessary information for a destination to export a record, including file path, session metadata, and results from previously-run higher-priority destinations (for inter-destination dependencies). Parameters ---------- xml_file_path Path to the XML record file to export session_identifier Unique identifier for this session instrument_pid Instrument identifier (e.g., "FEI-Titan-TEM-012345") dt_from Session start datetime dt_to Session end datetime user Username associated with this session (if known) metadata Additional session metadata previous_results Results from higher-priority destinations that have already run. Destinations can access these to create inter-destination dependencies (e.g., LabArchives including a CDCS link). """ xml_file_path: Path session_identifier: str instrument_pid: str dt_from: datetime dt_to: datetime user: str | None = None metadata: dict[str, Any] = field(default_factory=dict) previous_results: dict[str, ExportResult] = field(default_factory=dict) activities: list[AcquisitionActivity] = field(default_factory=list) reservation_event: ReservationEvent | None = None
[docs] def get_result(self, destination_name: str) -> ExportResult | None: """Get result from a specific destination, if it has already run. Parameters ---------- destination_name Name of the destination to query (e.g., "cdcs") Returns ------- ExportResult | None The result if the destination has run, None otherwise """ return self.previous_results.get(destination_name)
[docs] def add_result(self, destination_name: str, result: ExportResult) -> None: """Add or update result from a destination. Parameters ---------- destination_name Name of the destination (e.g., "cdcs", "elabftw") result The export result to store Examples -------- >>> from nexusLIMS.exporters.base import ExportResult >>> result = ExportResult(success=True, message="Uploaded successfully") >>> context.add_result("cdcs", result) """ self.previous_results[destination_name] = result
[docs] def has_successful_export(self, destination_name: str) -> bool: """Check if a destination successfully exported. Parameters ---------- destination_name Name of the destination to check (e.g., "cdcs") Returns ------- bool True if the destination ran and succeeded, False otherwise """ result = self.get_result(destination_name) return result is not None and result.success
[docs] class ExportDestination(Protocol): """Protocol for export destination plugins. Export destinations are discovered automatically by walking the exporters/destinations/ directory. Any class matching this protocol will be registered as an export destination. Attributes ---------- name : str Unique identifier for this destination (e.g., "cdcs") priority : int Selection priority (0-1000, higher runs first). Use priority to manage inter-destination dependencies: higher-priority destinations run first and their results are available to lower-priority destinations. """ name: str priority: int @property def enabled(self) -> bool: """Whether this destination is enabled and configured. Check if all required configuration is present (API keys, URLs, etc.) and the destination should be used for exports. Returns ------- bool True if destination is ready to use, False otherwise """ ...
[docs] def validate_config(self) -> tuple[bool, str | None]: """Validate configuration. Perform startup-time validation of configuration (API keys, connectivity, etc.) and return detailed error information. Returns ------- tuple[bool, str | None] (is_valid, error_message) - is_valid: True if configuration is valid - error_message: None if valid, descriptive error if invalid """ ...
[docs] def export(self, context: ExportContext) -> ExportResult: """Export record to this destination. CRITICAL: This method MUST NOT raise exceptions. All errors must be caught and returned as ExportResult with success=False and error_message set. Parameters ---------- context Export context with file path, session metadata, and results from higher-priority destinations Returns ------- ExportResult Result of the export attempt (success or failure) """ ...