Source code for ccat_ops_db.models

import os
import astropy.units as u
from astropy.coordinates import SkyCoord
from datetime import datetime, timezone
from sqlalchemy import (
    Boolean,
    Column,
    DateTime,
    Float,
    ForeignKey,
    Index,
    Integer,
    String,
    Table,
    Text,
    UniqueConstraint,
    Enum as SQLAlchemyEnum,
    BigInteger,
)
from enum import Enum
from .config.config import ccat_ops_db_settings


from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from typing import Optional

from .ccat_ops_db import Base
from .types import JSONB_VARIANT


[docs] class LocationType(Enum): """Location type enum The location type determines the role of the location in the data transfer system. SOURCE: Telescope instrument computers BUFFER: Input/transit buffers LONG_TERM_ARCHIVE: Permanent storage PROCESSING: Temporary processing areas """ SOURCE = "source" # Telescope instrument computers BUFFER = "buffer" # Input/processing buffers LONG_TERM_ARCHIVE = "long_term_archive" # Permanent storage PROCESSING = "processing" # Temporary processing areas
[docs] class StorageType(Enum): """Storage type enum The storage type determines the physical storage medium of the location. DISK: Traditional disk storage S3: Object storage (AWS S3 or compatible) TAPE: Tape-based archival storage """ DISK = "disk" S3 = "s3" TAPE = "tape"
[docs] class RouteType(Enum): """Route type enum The route type determines the route of the data transfer. DIRECT: RELAY: Route through intermediate site CUSTOM: Custom location-to-location override """ DIRECT = "direct" RELAY = "relay" # Route through intermediate site CUSTOM = "custom" # Custom location-to-location override
[docs] class Status(Enum): """Status enum These Status labels are used to make the Status checks uniform accross the entire data transfer system. These are used accros many concepts such as RawDataPackage, DataTransferPackage, DataTransfer, etc. PENDING: Is used to mark a concept as waiting for execution e.g. a RawDataPackage in PENDING will be scheduled to be build. SCHEDULED: Marks a concept as scheduled for execution e.g. a RawDataPackage in IN_PROGRESS: Marks that a celery job is working on this concept. COMPLETED: Succesfully completed the concept. FAILED: Failed to execute the concept. """ PENDING = ccat_ops_db_settings.status.PENDING SCHEDULED = ccat_ops_db_settings.status.SCHEDULED IN_PROGRESS = ccat_ops_db_settings.status.IN_PROGRESS COMPLETED = ccat_ops_db_settings.status.COMPLETED FAILED = ccat_ops_db_settings.status.FAILED
[docs] class PackageState(Enum): """Package state enum The package state determines the state of the package. WAITING: Waiting for transfer TRANSFERRING: Transferring ARCHIVED: Archived FAILED: Failed """ WAITING = "waiting" # Yellow hourglass - only in primary archive TRANSFERRING = "transferring" # Blue rotating circle - part of DataTransferPackage ARCHIVED = "archived" # Green checkmark - archived with all statuses completed FAILED = "failed" # Red cross - any status failed
[docs] class PhysicalCopyStatus(Enum): PRESENT = "present" STAGED = ( "staged" # Package staged and unpacked, physical RawDataPackage file removed ) DELETION_POSSIBLE = "deletion_possible" DELETION_PENDING = "deletion_pending" DELETION_SCHEDULED = "deletion_scheduled" DELETION_IN_PROGRESS = "deletion_in_progress" DELETION_FAILED = "deletion_failed" DELETED = "deleted" # Terminal state for copies whose bytes are gone and unrecoverable (e.g. a dead # host that was decommissioned). Distinct from DELETED, which denotes an orderly, # intentional deletion. Provenance records (package/file) are kept; only the copy # is marked lost. See DataLocation.decommissioned_at. LOST = "lost"
[docs] class TriggerType(Enum): CONTINUOUS = "continuous" CRON = "cron" MANUAL = "manual"
[docs] class RunStatus(Enum): PENDING = "pending" STAGING_DATA = "staging_data" SUBMITTED = "submitted" RUNNING = "running" COLLECTING_RESULTS = "collecting_results" COMPLETED = "completed" FAILED = "failed" CANCELLED = "cancelled"
[docs] class DataProductType(Enum): SCIENCE = "science" QA_METRIC = "qa_metric" PLOT = "plot" LOG = "log" STATISTICS = "statistics" INTERMEDIATE = "intermediate"
[docs] class OperationKind(str, Enum): """Identity axis of a pipeline operation and the polymorphic discriminator for the uniform Operation model (ops-db #94, ADR-0003). The values are BYTE-IDENTICAL to the frozen breadcrumb strings already stored in ``OperationFailureEvent.operation_type`` and used by recovery, the circuit breaker, and routing. Keeping them equal makes this a code refactor rather than a data migration, and makes the operation-kind drift class (recovery's ``archive`` vs the task's ``long_term_archive``) unrepresentable: this enum is the single source of truth for identity. ``str`` subclassing lets a member compare and serialize as its raw string, so it can be stored directly in the discriminator column and matched against the legacy breadcrumb strings without translation. Note the deliberate name/value asymmetry on two members: PACKAGING == ``raw_data_package`` and ARCHIVE == ``long_term_archive`` -- the names follow the stage, the values follow the frozen strings. """ PACKAGING = "raw_data_package" BUNDLING = "data_transfer_package" TRANSFER = "transfer" UNPACK = "unpack" ARCHIVE = "long_term_archive" STAGING = "staging"
[docs] class Site(Base): """Represents a physical or logical site where data can be stored or processed.""" __tablename__ = "site" id = Column(Integer, primary_key=True) name = Column(String(250), nullable=False) # "CCAT", "Cologne", "Cornell" short_name = Column(String(20), nullable=False) # "ccat", "cologne", "us" site_location = Column(String(250), nullable=True) # "Atacama", "Germany", "USA" long_term_archive_transfers = relationship( "LongTermArchiveTransfer", back_populates="site" ) locations = relationship("DataLocation", back_populates="site") __table_args__ = (UniqueConstraint("short_name", name="uix_site_short_name"),)
[docs] class DataLocation(Base): """Base class for all data storage locations with polymorphic storage types.""" __tablename__ = "data_location" id = Column(Integer, primary_key=True) name = Column(String(250), nullable=False) # "ccat_chai_computer", "fyst_buffer_1" location_type = Column(SQLAlchemyEnum(LocationType, values_callable=lambda x: [e.value for e in x]), nullable=False) site_id = Column(Integer, ForeignKey("site.id"), nullable=False) site = relationship("Site", back_populates="locations") # Buffer hierarchy and failover active = Column(Boolean, default=True, nullable=False) priority = Column(Integer, default=0, nullable=False) # Lower = higher priority # Decommissioning: a non-null decommissioned_at retires the location permanently # (e.g. a dead host whose data is unrecoverable). The row is KEPT so historical # packages/copies still resolve to a known location; it is hidden from active # operational views. active=False is a reversible pause; decommissioned_at is the # terminal retirement marker. decommission_reason records why (audited separately # in OperationalConfigLog). decommissioned_at = Column(DateTime(timezone=True), nullable=True) decommission_reason = Column(Text, nullable=True) # Polymorphic setup storage_type = Column( SQLAlchemyEnum(StorageType, values_callable=lambda x: [e.value for e in x]), nullable=False ) # "disk", "s3", "tape" __mapper_args__ = {"polymorphic_identity": None, "polymorphic_on": storage_type} __table_args__ = ( UniqueConstraint("site_id", "name", name="uix_data_location_name"), )
[docs] class DiskDataLocation(DataLocation): """Disk-based storage location.""" __tablename__ = "disk_data_location" id = Column(Integer, ForeignKey("data_location.id"), primary_key=True) path = Column(String(500), nullable=False) host = Column(String(250)) user = Column(String(250)) __mapper_args__ = {"polymorphic_identity": StorageType.DISK}
[docs] class S3DataLocation(DataLocation): """S3-compatible object storage location.""" __tablename__ = "s3_data_location" id = Column(Integer, ForeignKey("data_location.id"), primary_key=True) bucket_name = Column(String(250), nullable=False) region = Column(String(100)) # Self-serve connection config (non-secret), so an S3/LTA store can be # commissioned from the admin UI without a data-transfer config redeploy. # All nullable: data-transfer falls back to its dynaconf settings when unset # (S3_ENDPOINTS / S3_METHOD / COSCINE_*), so existing deployments are # unaffected until a location is populated. Secrets (S3 keys, Coscine token) # stay in the environment, keyed per-location (see get_s3_credentials / # get_coscine_api_token). The single endpoint_url assumes one URL reachable # from every worker that routes to this location. endpoint_url = Column(String(500), nullable=True) # Access method for this location: "s3" (boto3) or "coscine". NULL => use the # deployment-wide S3_METHOD. Lets one LTA be Coscine and another plain S3. access_method = Column(String(20), nullable=True) # boto3 S3 addressing style for this endpoint: # True => path-style (https://endpoint/bucket/key) e.g. RDS, MinIO # False => virtual-host (https://bucket.endpoint/key) e.g. NRW Datastorage # NULL => boto3 default ("auto") / deployment-wide S3_PATH_STYLE # Different S3 backends require different styles, so it belongs on the location. path_style = Column(Boolean, nullable=True) # Coscine target identifiers (not secrets) used when access_method == "coscine". coscine_project = Column(String(250), nullable=True) coscine_resource = Column(String(250), nullable=True) __mapper_args__ = {"polymorphic_identity": StorageType.S3}
[docs] def get_s3_credentials(self, site_name: str) -> tuple[str, str]: """Resolve S3 credentials from the environment for this location. Credentials are looked up (via dynaconf) under the per-location name:: {Site.short_name}_{DataLocation.name}_S3_ACCESS_KEY_ID {Site.short_name}_{DataLocation.name}_S3_SECRET_ACCESS_KEY Because data-transfer's dynaconf prefix is ``CCAT_DATA_TRANSFER``, the actual environment variables carry that prefix. For the Cologne site (``short_name="cologne"``) with a ``long_term_archive`` location:: CCAT_DATA_TRANSFER_COLOGNE_LONG_TERM_ARCHIVE_S3_ACCESS_KEY_ID=... CCAT_DATA_TRANSFER_COLOGNE_LONG_TERM_ARCHIVE_S3_SECRET_ACCESS_KEY=... Do NOT embed the site name in the location name: the convention already prefixes ``site.short_name``, so a location literally named ``cologne_long_term_archive`` yields the doubled, easy-to-miss ``..._COLOGNE_COLOGNE_LONG_TERM_ARCHIVE_...``. Name the location just ``long_term_archive``. If the per-location variable is unset, this falls back to the global ``S3_ACCESS_KEY_ID`` / ``S3_SECRET_ACCESS_KEY`` (default placeholder ``"MUST_SET_VIA_ENV"``). A misspelled/doubled name therefore surfaces at the S3 layer as ``InvalidAccessKeyId`` rather than a config error — the data-transfer worker logs the resolved env-var name and a masked key fingerprint (``utils.get_s3_client``) to make this diagnosable. Parameters ---------- site_name : str The site's ``short_name`` (passed by the pipeline). Returns ------- tuple[str, str] (access_key_id, secret_access_key) """ from ccat_data_transfer.config.config import ccat_data_transfer_settings # Construct environment variable names access_key_var = f"{site_name}_{self.name}_S3_ACCESS_KEY_ID" secret_key_var = f"{site_name}_{self.name}_S3_SECRET_ACCESS_KEY" # Get credentials from DynaConf settings access_key_id = getattr(ccat_data_transfer_settings, access_key_var, None) secret_access_key = getattr(ccat_data_transfer_settings, secret_key_var, None) # Fall back to global settings if location-specific ones aren't set if not access_key_id: access_key_id = ccat_data_transfer_settings.s3_access_key_id if not secret_access_key: secret_access_key = ccat_data_transfer_settings.s3_secret_access_key return access_key_id, secret_access_key
[docs] def get_coscine_api_token(self, site_name: str) -> str: """Get the Coscine API token for this location (secret, env-resolved). Mirrors :meth:`get_s3_credentials`: looks for a per-location token ``{Site.name}_{DataLocation.name}_COSCINE_API_TOKEN`` and falls back to the deployment-wide ``COSCINE_API_TOKEN``. The non-secret project/resource live on the location (``coscine_project`` / ``coscine_resource``). """ from ccat_data_transfer.config.config import ccat_data_transfer_settings token_var = f"{site_name}_{self.name}_COSCINE_API_TOKEN" token = getattr(ccat_data_transfer_settings, token_var, None) if not token: token = ccat_data_transfer_settings.coscine_api_token return token
[docs] class TapeDataLocation(DataLocation): """Tape-based storage location.""" __tablename__ = "tape_data_location" id = Column(Integer, ForeignKey("data_location.id"), primary_key=True) library_name = Column(String(250)) mount_path = Column(String(500)) __mapper_args__ = {"polymorphic_identity": StorageType.TAPE}
raw_data_package_staging_job_association = Table( "raw_data_package_staging_job_association", Base.metadata, Column("raw_data_package_id", Integer, ForeignKey("raw_data_package.id")), Column("staging_job_id", Integer, ForeignKey("staging_job.id")), ) # Copy-anchored lineage for the uniform Operation model (ops-db #94, ADR-0003). # An Operation consumes input PhysicalCopy(s) and produces output PhysicalCopy(s). # Lineage is traced through the artifact/copy graph rather than operation-to- # operation FKs, because the pipeline's fan-in (bundling) and fan-out (unpack) # live between artifacts. The consume/produce split is two tables, not one with a # role column, so each direction is independently queryable from either side. operation_consumes_physical_copy = Table( "operation_consumes_physical_copy", Base.metadata, Column( "operation_id", Integer, ForeignKey("operation.id", ondelete="CASCADE"), primary_key=True, ), Column( "physical_copy_id", Integer, ForeignKey("physical_copy.id", ondelete="CASCADE"), primary_key=True, ), ) operation_produces_physical_copy = Table( "operation_produces_physical_copy", Base.metadata, Column( "operation_id", Integer, ForeignKey("operation.id", ondelete="CASCADE"), primary_key=True, ), Column( "physical_copy_id", Integer, ForeignKey("physical_copy.id", ondelete="CASCADE"), primary_key=True, ), ) user_role_association = Table( "user_role_association", Base.metadata, Column("user_id", Integer, ForeignKey("user.id"), primary_key=True), Column("role_id", Integer, ForeignKey("role.id"), primary_key=True), ) instrument_observing_program_association = Table( "instrument_observing_program", Base.metadata, Column("instrument_id", Integer, ForeignKey("instrument.id"), primary_key=True), Column( "observing_program_id", Integer, ForeignKey("observing_program.id"), primary_key=True, ), ) obs_unit_instrument_module_configuration_association = Table( "obs_unit_instrument_module_configuration_association", Base.metadata, Column("obs_unit_id", Integer, ForeignKey("obs_unit.id")), Column( "instrument_module_configuration_id", Integer, ForeignKey("instrument_module_configuration.id"), ), )
[docs] class ObservingProgram(Base): __tablename__ = "observing_program" id = Column(Integer, primary_key=True) name = Column(String(250), nullable=False) short_name = Column(String(20), nullable=False, doc="short name without spaces to be used in selection") description = Column(Text) lead_id = Column(Integer, ForeignKey("user.id"), nullable=True) lead = relationship("User", back_populates="observing_programs") sub_observing_programs = relationship( "SubObservingProgram", back_populates="observing_program", ) instruments = relationship( "Instrument", secondary=instrument_observing_program_association, back_populates="observing_programs", ) obs_units = relationship("ObsUnit", back_populates="observing_program")
[docs] class SubObservingProgram(Base): __tablename__ = "sub_observing_program" id = Column(Integer, primary_key=True) name = Column(String(250), nullable=False) short_name = Column(String(20), nullable=False, doc="short name without spaces to be used in selection") description = Column(Text) observing_program_id = Column( Integer, ForeignKey("observing_program.id"), nullable=False, ) observing_program = relationship( "ObservingProgram", back_populates="sub_observing_programs", ) obs_units = relationship("ObsUnit", back_populates="sub_observing_program")
# The following tables implement a data transfer system from one side to the other with # handshaking and deletion on the origin uncommented # the Source Table needs a sqlalchemy column that resembles a SkyCoord object read from # the ra_deg and dec_deg columns as well as the coordinate_system column and the epoch # column
[docs] class Source(Base): """A source is a celestial object This class serves as a base class for various types of sources. It is a polymorphic class in SQLAlchemy, not instantiated directly, but used to provide common attributes. Subclasses, implemented as separate database tables, inherit from the source class and can have additional specific attributes. See classes that are based on this class for more information on the implemented types of sources. """ __tablename__ = "source" id = Column(Integer, primary_key=True) name = Column( String(250), nullable=False, unique=True, doc="The name of the source" ) version = Column( Integer, nullable=False, doc="Version number of the current parameters") history = Column(JSONB_VARIANT, nullable=True, doc="Change history") type = Column(String(250), nullable=False, doc="The polymorphic identity") __mapper_args__ = { "polymorphic_on": type, "polymorphic_identity": "source", } # Add relationships obs_units = relationship("ObsUnit", back_populates="source")
[docs] class FixedSource(Source): """A source that has a fixed coordinates This class is a subclass of the Source class and inherits all attributes from the Source class. The class implements the skycoord property that returns a SkyCoord object from the ra_deg and dec_deg columns. """ __tablename__ = "fixed_source" __mapper_args__ = {"polymorphic_identity": "fixed_source"} id = Column(Integer, ForeignKey("source.id"), primary_key=True) ra_deg = Column(Float, nullable=False, doc="The right ascension in degrees (ICRS)") dec_deg = Column(Float, nullable=False, doc="The declination in degrees (ICRS)") slam = Column(String(50), doc="Longitude string of the original input") sbet = Column(String(50), doc="Latitude string of the original input") vlsr = Column(Float, doc="The local standard of rest velocity in km/s") frame = Column(String(50), doc="The frame of the coordinates of the original input") @property def skycoord(self): """Return a SkyCoord object from the ra_deg and dec_deg columns""" if self.ra_deg is not None and self.dec_deg is not None: return SkyCoord( ra=self.ra_deg * u.deg, dec=self.dec_deg * u.deg, frame="icrs", ) return None
[docs] class SolarSystemObject(Source): """A source that is a solar system object This class is a subclass of the Source class and inherits all attributes from the Source class. """ __tablename__ = "solar_system_object" __mapper_args__ = {"polymorphic_identity": "solar_system_object"} id = Column(Integer, ForeignKey("source.id"), primary_key=True) eph_name = Column( String(50), nullable=True, doc="Standard ephemeris name of the source", ) # https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/FORTRAN/req/naif_ids.html#NAIF%20Integer%20ID%20codes naif_id = Column(Integer, nullable=True, doc="The NAIF ID of the source")
[docs] class ConstantElevationSource(Source): """A source that is observed at a constant elevation This class is a subclass of the Source class and inherits all attributes from the Source class. """ __tablename__ = "constant_elevation_source" __mapper_args__ = {"polymorphic_identity": "constant_elevation_source"} id = Column(Integer, ForeignKey("source.id"), primary_key=True) ra_deg_min = Column( Float, nullable=False, doc="The minimum right ascension of the area in degrees" ) ra_deg_max = Column( Float, nullable=False, doc="The maximum right ascension of the area in degrees" ) dec_deg_min = Column( Float, nullable=False, doc="The minimum declination of the area in degrees" ) dec_deg_max = Column( Float, nullable=False, doc="The maximum declination of the area in degrees" ) slam_min = Column( String(50), doc="The minimum longitude string of the original input" ) slam_max = Column( String(50), doc="The maximum longitude string of the original input" ) sbet_min = Column( String(50), doc="The minimum latitude string of the original input" ) sbet_max = Column( String(50), doc="The maximum latitude string of the original input" ) vlsr = Column(Float, doc="The local standard of rest velocity in km/s") frame = Column(String(50), doc="The frame of the coordinates of the original input")
[docs] class Line(Base): """A spectral line that is observed by an instrument""" __tablename__ = "line" id = Column(Integer, primary_key=True) name = Column(String(250), nullable=False, doc="The name of the spectral line") rest_frequency = Column(Float, nullable=False, doc="The rest frequency of the line") side_band = Column(String(250), nullable=False, doc="The side band of the line") available = Column(Boolean, nullable=False, doc="Whether this line is ready to be scheduled") comment = Column(String(250), nullable=True)
# chai_array_configurations = relationship( # "ChaiArrayConfiguration", back_populates="line" # ) # To be able to also include data from other observatories such as GREAT/SOFIA we # include a specific observatory table
[docs] class Observatory(Base): __tablename__ = "observatory" id = Column(Integer, primary_key=True) name = Column(String(250), nullable=False, unique=True) description = Column(String(250), nullable=False) telescopes = relationship("Telescope", back_populates="observatory")
# To prepare for the possibility of multiple telescopes in an observatory we include a # telescope table
[docs] class Telescope(Base): __tablename__ = "telescope" id = Column(Integer, primary_key=True) name = Column(String(250), nullable=False) description = Column(String(250), nullable=False) lon_deg = Column(Float, nullable=False) lat_deg = Column(Float, nullable=False) alt_m = Column(Float, nullable=False) instruments = relationship("Instrument", back_populates="telescope") observatory_id = Column(Integer, ForeignKey("observatory.id"), nullable=False) observatory = relationship("Observatory", back_populates="telescopes")
# An instrument runs on a specific telescope
[docs] class Instrument(Base): __tablename__ = "instrument" id = Column(Integer, primary_key=True) name = Column(String(250), nullable=False) instrument_type = Column(String(250), nullable=False) description = Column(String(250)) telescope_id = Column(Integer, ForeignKey("telescope.id"), nullable=False) telescope = relationship("Telescope", back_populates="instruments") modules = relationship("InstrumentModule", back_populates="instrument") observing_programs = relationship( "ObservingProgram", secondary=instrument_observing_program_association, back_populates="instruments", ) available = Column(Boolean, nullable=False)
[docs] class InstrumentModule(Base): __tablename__ = "instrument_module" id = Column(Integer, primary_key=True) name = Column(String(250), nullable=False) description = Column(String(250)) instrument_id = Column(Integer, ForeignKey("instrument.id"), nullable=False) instrument = relationship("Instrument", back_populates="modules") instrument_module_configurations = relationship( "InstrumentModuleConfiguration", back_populates="instrument_module" ) available = Column(Boolean, nullable=False) raw_data_packages = relationship( "RawDataPackage", back_populates="instrument_module" )
[docs] class InstrumentModuleConfiguration(Base): __tablename__ = "instrument_module_configuration" id = Column(Integer, primary_key=True) type = Column(String) instrument_module_id = Column(Integer, ForeignKey("instrument_module.id")) instrument_module = relationship( "InstrumentModule", back_populates="instrument_module_configurations" ) raw_data_files = relationship( "RawDataFile", back_populates="instrument_module_configuration" ) __mapper_args__ = { "polymorphic_on": type, "polymorphic_identity": "instrument_module_configuration", } # Polymorphic mapping
[docs] class ChaiModuleConfiguration(InstrumentModuleConfiguration): __tablename__ = "chai_module_configuration" __mapper_args__ = {"polymorphic_identity": "chai_module_configuration"} id = Column( Integer, ForeignKey("instrument_module_configuration.id"), primary_key=True ) line_id = Column(Integer, ForeignKey("line.id"), nullable=False) line = relationship("Line") if_ghz = Column( Float, doc="Intermediate frequency (IF) in GHz at the center of the signal sideband", ) config_parameters = Column( JSONB_VARIANT, doc="List of instrument configuration parameters", )
[docs] class PrimeCamModuleConfiguration(InstrumentModuleConfiguration): __tablename__ = "prime_cam_module_configuration" __mapper_args__ = { "polymorphic_identity": "prime_cam_module_configuration" } # Polymorphic mapping id = Column( Integer, ForeignKey("instrument_module_configuration.id"), primary_key=True ) config_parameters = Column( JSONB_VARIANT, doc="List of instrument configuration parameters", )
[docs] class ObservationConfiguration(Base): __tablename__ = "observation_configuration" id = Column(Integer, primary_key=True) type = Column(String) obs_units = relationship("ObsUnit", back_populates="observation_configuration") __mapper_args__ = { "polymorphic_on": type, "polymorphic_identity": "observation_configuration", } azimuth_range = Column( JSONB_VARIANT, nullable=True, doc="Azimuth range lookup table for constant elevation scans", )
[docs] class ChaiObservationConfiguration(ObservationConfiguration): __tablename__ = "chai_observation_configuration" __mapper_args__ = {"polymorphic_identity": "chai_observation_configuration"} id = Column(Integer, ForeignKey("observation_configuration.id"), primary_key=True) chai_tilings = relationship( "ChaiTiling", back_populates="chai_observation_configuration" ) ntilelines = Column( Integer, nullable=True, doc="Number of tile lines to be grouped (for socring in scheduler)", )
[docs] class PrimeCamObservationConfiguration(ObservationConfiguration): __tablename__ = "prime_cam_observation_configuration" __mapper_args__ = { "polymorphic_identity": "prime_cam_observation_configuration" } # Polymorphic mapping id = Column(Integer, ForeignKey("observation_configuration.id"), primary_key=True) version = Column( Integer, nullable=False, doc="Version number of the current parameters") history = Column(JSONB_VARIANT, nullable=True, doc="Change history") mapping_parameters = Column( JSONB_VARIANT, nullable=True, doc="List of mapping parameters", )
[docs] class ObsMode(Base): __tablename__ = "obs_mode" id = Column(Integer, primary_key=True) name = Column(String(250), nullable=False) description = Column(String(250)) obs_units = relationship("ObsUnit", back_populates="obs_mode")
[docs] class ObsUnit(Base): __tablename__ = "obs_unit" id = Column(Integer, primary_key=True) name = Column(String(250), nullable=False) version = Column( Integer, nullable=False, doc="Version number of the current parameters") history = Column(JSONB_VARIANT, nullable=True, doc="Change history") phase = Column( String(20), nullable=False, doc="Common name of campaigns that share the observing requests.", ) group = Column( String(250), nullable=True, doc="List of other ObsUnits that belong to the same group", ) group_type = Column( String(250), nullable=True, doc="Reason of grouping. 'equal' indicates balanced scheduling, 'either' indicates resetting cadence once one of them are observed.", ) equal_tolerance = Column( Float, nullable=True, doc="Tolerance of inbalanced schedule when group_type is equal", ) min_alt = Column( Float, nullable=True, doc="Minimum allowed altitude (elevation) [deg]") max_alt = Column( Float, nullable=True, doc="Maximum allowed altitude (elevation) [deg]") min_rotang = Column( Float, nullable=True, doc="Minimum allowed rotation angle [deg]") max_rotang = Column( Float, nullable=True, doc="Maximum allowed rotation angle [deg]") nominal_alt = Column( String(250), nullable=True, doc="Either fixed altitude in [deg] or 'var', which allows a flexible choice", ) min_lsa = Column(Float, nullable=True, doc="Minimum LSA [deg]") max_lsa = Column(Float, nullable=True, doc="Maximum LSA [deg]") lsa_margin = Column( Float, nullable=True, doc="Allowed deviation from min_lsa and max_lsa [deg]") cadence = Column( Float, nullable=True, doc="Requested cadence for scheduling [day]") requested_time_h = Column( Float, doc="Requested total observing hours. Mandatory for PrimeCam. For CHAI it is calculated by contents in ChaiTiling", nullable=True, ) unit_duration_h = Column(Float, nullable=False, doc="Observing duration [h]") trans_ref = Column(Float, nullable=True, doc="Reference transmission") # Note for priorities: due to and enhaced usage in the scheduler, # this needs to be float, not integer. priorities = Column(Float, nullable=False, doc="Science priority") available = Column( Boolean, nullable=False, doc="Whether this ObsUnit is ready to be scheduled" ) pre_scheduled_basis = Column( Boolean, doc="Whether this Obsunit is executed on pre-scheduled basis, meaning that it has a very low priority outside of the pre-scheduled slots.", ) additional_parameters = Column( JSONB_VARIANT, nullable=True, doc="Any special parameters or constraints to be stored" ) source_id = Column(Integer, ForeignKey("source.id"), nullable=True) source = relationship("Source", back_populates="obs_units") observing_program_id = Column( Integer, ForeignKey("observing_program.id"), nullable=False, ) observing_program = relationship("ObservingProgram", back_populates="obs_units") sub_observing_program_id = Column(Integer, ForeignKey("sub_observing_program.id")) sub_observing_program = relationship( "SubObservingProgram", back_populates="obs_units", ) obs_mode_id = Column(Integer, ForeignKey("obs_mode.id"), nullable=True) obs_mode = relationship("ObsMode", back_populates="obs_units") primary_instrument_module_configuration_id = Column( Integer, ForeignKey("instrument_module_configuration.id"), nullable=False ) primary_instrument_module_configuration = relationship( "InstrumentModuleConfiguration", backref="primary_obs_units", ) # Note: it is a bit redundant but the following list includes # primary_instrument_module_configuration as well # so that it is easier to retrieve all instrument module configuration # that are associated to this obsunit instrument_module_configurations = relationship( "InstrumentModuleConfiguration", secondary=obs_unit_instrument_module_configuration_association, backref="obs_units", ) observation_configuration_id = Column( Integer, ForeignKey("observation_configuration.id"), nullable=True ) observation_configuration = relationship( "ObservationConfiguration", back_populates="obs_units" ) executed_obs_units = relationship("ExecutedObsUnit", back_populates="obs_unit") pre_scheduled_slots = relationship("PreScheduledSlot", back_populates="obs_unit") raw_data_packages = relationship("RawDataPackage", back_populates="obs_unit")
[docs] class PreScheduledSlot(Base): __tablename__ = "pre_scheduled_slot" id = Column(Integer, primary_key=True) start_time = Column(DateTime(timezone=True), nullable=False) end_time = Column(DateTime(timezone=True), nullable=False) obs_unit_id = Column(Integer, ForeignKey("obs_unit.id"), nullable=False) obs_unit = relationship("ObsUnit", back_populates="pre_scheduled_slots")
[docs] class ExecutedObsUnit(Base): __tablename__ = "executed_obs_unit" id = Column(UUID(as_uuid=True), primary_key=True) start_time = Column(DateTime(timezone=True), nullable=False, index=True) end_time = Column(DateTime(timezone=True)) status = Column( String(20), nullable=True, doc="Status of this observation. 'running' indicates currently running, '*success*' or '*completed*' will be counted as a successfully completed observation.", ) mean_pwv = Column(Float, nullable=True, doc="Mean pwv [mm] during observation") mean_elevation = Column(Float, nullable=True, doc="Mean elevation [deg] during observation") achievement_factor = Column( Float, nullable=True, doc="Correction factor for the achieved time compared to the reference transmission", ) quality = Column(String(20), nullable=True, doc="If *discard*, not considered in counting achievement") obs_unit_id = Column(Integer, ForeignKey("obs_unit.id"), nullable=False) obs_unit = relationship("ObsUnit", back_populates="executed_obs_units") obs_info = Column( JSONB_VARIANT, nullable=True, doc="Static ancillary infomation (e.g. ObsUnit version, tiling ID for CHAI)", ) obs_progress = Column(JSONB_VARIANT, nullable=True, doc="Field for progress tracking") raw_data_packages = relationship( "RawDataPackage", back_populates="executed_obs_unit" ) raw_data_files = relationship("RawDataFile", back_populates="executed_obs_unit")
[docs] class ChaiTiling(Base): __tablename__ = "chai_tiling" id = Column(Integer, primary_key=True) version = Column( Integer, nullable=False, doc="Version number of the current parameters") history = Column(JSONB_VARIANT, nullable=True, doc="Change history") priority_in_tiling = Column(Integer, nullable=False) tile_id = Column(String(10), nullable=False, doc="CHAI internal tile ID") tile_offset_x = Column(Float, nullable=False, doc="tile position along x-axis") tile_offset_y = Column(Float, nullable=False, doc="tile position along y-axis") x_or_y = Column(String(2), doc="Scan direction") tile_unit_scaling_x = Column( Float, doc="Scaling factor for the script-defined tiling unit for x-direction", ) tile_unit_scaling_y = Column( Float, doc="Scaling factor for the script-defined tiling unit for y-direction", ) edge = Column( String(10), doc="Edge name if half of the tile should be observed") goal_ncycle = Column(Integer, nullable=False, doc="Goal number of cycle") # tiling is connected to obsunit through observation configuration chai_observation_configuration_id = Column( Integer, ForeignKey("chai_observation_configuration.id") ) chai_observation_configuration = relationship( "ChaiObservationConfiguration", back_populates="chai_tilings" ) chai_inpar_parameter_id = Column(Integer, ForeignKey("chai_inpar_parameter.id")) chai_inpar_parameter = relationship( "ChaiInparParameter", back_populates="chai_tilings" )
[docs] class ChaiInparParameter(Base): __tablename__ = "chai_inpar_parameter" id = Column(Integer, primary_key=True) # nullable=True is default name = Column(String(100), nullable=False) version = Column( Integer, nullable=False, doc="Version number of the current parameters") history = Column(JSONB_VARIANT, doc="Change history") chai_tilings = relationship("ChaiTiling", back_populates="chai_inpar_parameter") lam = Column(Float, nullable=False, doc="Map center offset in longitude [arcsec]") bet = Column(Float, nullable=False, doc="Map center offset in latitude [arcsec]") cormap = Column(String(10), nullable=False, doc="Map cood. system") line_range = Column( String(50), doc='Range of velocities [km/s] affected by lines (e.g. "-70:-30:v,-10:10:v")', ) goal_resolution = Column( Float, doc="Spectral resolution [km/s] with which the scientific analysis will be done", ) refname = Column( String(20), doc="Reference name when using the absolute positions. Set NAN to use relative", ) refoffl = Column( Float, doc="Reference position relative to on in longitude [arcsec]" ) refoffb = Column( Float, doc="Reference position relative to on in latitude [arcesc]" ) corref = Column(String(10), doc="Reference cood. system") mode = Column(String(10), doc="Observing mode; otfl or otfb") otfpattern = Column( String(50), doc="OTF pattern file name specified in kosma_software" ) ton = Column(Float, doc="On integration time [sec]") toff = Column( Float, doc="Forced off time [s] (-1 to use the default calculation = default)", ) repetition = Column(Integer, doc="Repetition number") offononoff = Column( Integer, doc="Sequence of OFF and ON (1 = OFF-ON-ON-OFF (default), 0 = OFF-ON)", ) stepl = Column(Float, doc="Step size in longitude [arcsec]") stepb = Column(Float, doc="Step size in latitude [arcsec]") mapsizel = Column(Float, doc="Size of map in longitude [arcsec]") mapsizeb = Column(Float, doc="Size of map in latitude [arcsec]") nmapl = Column(Integer, doc="number of raster positions along longitude") nmapb = Column(Integer, doc="number of raster positions along latitude") mapangle = Column(Float, doc="Position angle of the map (counter-clock) [degree]") crosssizel = Column(Float, doc="Size of cross in longitude [arcsec]") crosssizeb = Column(Float, doc="Size of cross in latitude [arcsec]") reverseflg = Column( Integer, doc=" OTF scan direction in cross : 0 = (+x,+y), 1 = (-x,-y), 2 = (-x,+x,-y,+y)", ) scan_dir = Column(Integer, doc="OTF scan direction (1 is +x or +y, -1 is -x or -y)") scan_order = Column( Integer, doc="Order of OTF lines (1 is +x or +y, -1 is -x or -y)" ) evendump = Column( Integer, doc="If 1 allow even number of dump positions without hitting the center. Default = 0", ) novertical = Column( Integer, doc="If 1 skip the second (vertical) scan for cross observations. Default = 0", ) pointingflg = Column( Integer, doc="Flag to indicate that it is a pointing session. Default = 0", ) offperload = Column(Integer, doc="Define how many off for one load measurment") onperload = Column( Integer, doc="Define how many on positions for one load measurement" ) repperload = Column( Integer, doc="Define how many repetition for one load measurment" ) lineperoff = Column( Integer, doc="Define how many otf scan lines for one off measurement" ) onperoff = Column( Integer, doc="Define how many on positions for one off measurement" ) offperpattern = Column(Integer, doc="Define how many off measurement per pattern") refpoint = Column(String(20), doc="Reference point (as is used in setpoint)") act_pixflg = Column( Integer, doc="Flag to use the actual pixel position of refpoint. Default = 0", ) nextflg = Column( String(1), doc="If Y, enable telescope moving when writing data. Default = N", )
[docs] class Role(Base): __tablename__ = "role" id = Column(Integer, primary_key=True) name = Column(String(150), unique=True) description = Column(String(150), nullable=True) # GitHub team mappings for automatic role assignment github_team_mappings = Column( JSONB_VARIANT, nullable=True, doc="GitHub teams that map to this role" ) # Permission scopes for this role permissions = Column( JSONB_VARIANT, nullable=True, doc="List of permissions granted to this role" ) users = relationship( "User", secondary=user_role_association, back_populates="roles" )
[docs] class User(Base): __tablename__ = "user" id = Column(Integer, primary_key=True) email = Column(String(150)) username = Column(String(150)) first_name = Column(String(150), nullable=True) last_name = Column(String(150), nullable=True) title = Column(String(150), nullable=True) affiliation = Column(Text, nullable=True) password = Column(String(150)) # GitHub integration fields github_id = Column(String(50), nullable=True, unique=True, doc="GitHub user ID") github_username = Column(String(150), nullable=True, doc="GitHub username") # User preferences stored as JSON preferences = Column(JSONB_VARIANT, nullable=True, doc="User preferences and settings") roles = relationship( "Role", secondary=user_role_association, back_populates="users" ) # Clean up duplicate fields (removing duplicates) last_login_at = Column(DateTime()) current_login_at = Column(DateTime()) last_login_ip = Column(String(100)) current_login_ip = Column(String(100)) login_count = Column(Integer) active = Column(Boolean()) confirmed_at = Column(DateTime()) # Relationships observing_programs = relationship("ObservingProgram", back_populates="lead") api_tokens = relationship( "ApiToken", back_populates="user", cascade="all, delete-orphan" )
[docs] class ApiToken(Base): """API tokens for programmatic access to the API""" __tablename__ = "api_token" id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey("user.id"), nullable=False) user = relationship("User", back_populates="api_tokens") # Token identification name = Column(String(100), nullable=False, doc="Human-readable name for the token") token_hash = Column( String(255), nullable=False, unique=True, doc="Hashed token value" ) token_prefix = Column( String(10), nullable=False, doc="First few characters for identification" ) # Token permissions and scopes scopes = Column(JSONB_VARIANT, nullable=True, doc="List of permission scopes for this token") # Token lifecycle created_at = Column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False, ) expires_at = Column( DateTime(timezone=True), nullable=True, doc="Token expiration time" ) last_used_at = Column( DateTime(timezone=True), nullable=True, doc="Last time token was used" ) active = Column( Boolean, default=True, nullable=False, doc="Whether token is active" ) # Usage tracking usage_count = Column( Integer, default=0, nullable=False, doc="Number of times token was used" ) last_used_ip = Column(String(100), nullable=True, doc="IP address of last usage")
[docs] def is_expired(self): """Check if token is expired""" if self.expires_at is None: return False return datetime.now(timezone.utc) > self.expires_at
[docs] def is_valid(self): """Check if token is valid (active and not expired)""" return self.active and not self.is_expired()
[docs] class DataTransferRoute(Base): """Defines routes for data transfer between sites and locations. The database implements a flexible routing system that supports: 1. Direct routes between specific locations 2. Relay routes through intermediate sites 3. Custom location-to-location overrides """ __tablename__ = "data_transfer_route" id = Column(Integer, primary_key=True) name = Column(String(250), nullable=False) # Site-level routing origin_site_id = Column(Integer, ForeignKey("site.id"), nullable=False) origin_site = relationship("Site", foreign_keys=[origin_site_id]) destination_site_id = Column(Integer, ForeignKey("site.id"), nullable=False) destination_site = relationship("Site", foreign_keys=[destination_site_id]) # Route configuration route_type = Column(SQLAlchemyEnum(RouteType, values_callable=lambda x: [e.value for e in x]), nullable=False) transfer_method = Column(String(250), nullable=False) # "bbcp", "s3", "cp" # Optional location-specific overrides origin_location_id = Column(Integer, ForeignKey("data_location.id"), nullable=True) origin_location = relationship("DataLocation", foreign_keys=[origin_location_id]) destination_location_id = Column( Integer, ForeignKey("data_location.id"), nullable=True ) destination_location = relationship( "DataLocation", foreign_keys=[destination_location_id] ) # Relay configuration relay_site_id = Column(Integer, ForeignKey("site.id"), nullable=True) relay_site = relationship("Site", foreign_keys=[relay_site_id]) __table_args__ = ( UniqueConstraint( "origin_site_id", "destination_site_id", "route_type", name="uix_data_transfer_route", ), )
data_transfer_package_files_association = Table( "data_transfer_package_files", Base.metadata, Column( "data_transfer_package_id", Integer, ForeignKey("data_transfer_package.id"), primary_key=True, ), Column( "file_id", UUID(as_uuid=True), ForeignKey("raw_data_file.id"), primary_key=True ), )
[docs] class RawDataFile(Base): """Represents a raw data file from an instrument.""" __tablename__ = "raw_data_file" id = Column(UUID(as_uuid=True), primary_key=True) name = Column(String(250), nullable=False) relative_path = Column(String(250), nullable=False) created_at = Column( DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc), ) instrument_module_configuration_id = Column( Integer, ForeignKey("instrument_module_configuration.id"), nullable=False ) instrument_module_configuration = relationship( "InstrumentModuleConfiguration", back_populates="raw_data_files" ) file_type = Column(String(250), nullable=False) size = Column(BigInteger, nullable=False) checksum = Column(String(250), nullable=False) description = Column(Text, nullable=True) # Source location tracking source_location_id = Column(Integer, ForeignKey("data_location.id"), nullable=False) source_location = relationship("DataLocation") # Package relationships raw_data_package_id = Column( Integer, ForeignKey("raw_data_package.id", ondelete="CASCADE"), nullable=True ) raw_data_package = relationship("RawDataPackage", back_populates="raw_data_files") data_transfer_package_id = Column( Integer, ForeignKey("data_transfer_package.id"), nullable=True ) data_transfer_package = relationship( "DataTransferPackage", back_populates="raw_data_files" ) executed_obs_unit_id = Column( UUID(as_uuid=True), ForeignKey("executed_obs_unit.id"), nullable=False ) executed_obs_unit = relationship("ExecutedObsUnit", back_populates="raw_data_files") # Physical copies state = Column(SQLAlchemyEnum(PackageState, values_callable=lambda x: [e.value for e in x]), nullable=True) physical_copies = relationship( "RawDataFilePhysicalCopy", back_populates="raw_data_file", cascade="all, delete-orphan", )
[docs] class RawDataPackageMetadata(Base): """Model for storing additional metadata for raw data packages. This table stores metadata that is not part of the core database models but is needed for IVOA-compatible metadata generation. It includes: - Instrument-specific parameters - Additional quality metrics - Extended provenance information - Custom metadata fields """ __tablename__ = "raw_data_package_metadata" id = Column(Integer, primary_key=True) raw_data_package_id = Column( Integer, ForeignKey("raw_data_package.id", ondelete="CASCADE"), nullable=False ) created_at = Column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) updated_at = Column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), ) # Instrument-specific metadata (JSON) instrument_specific = Column( JSONB_VARIANT, nullable=True, comment="Instrument-specific parameters and configuration" ) # Additional quality metrics quality_metrics = Column( JSONB_VARIANT, nullable=True, comment="Additional quality metrics not in core models" ) # Extended provenance provenance = Column(JSONB_VARIANT, nullable=True, comment="Extended provenance information") # Custom metadata fields custom_metadata = Column(JSONB_VARIANT, nullable=True, comment="Custom metadata fields") # Relationships raw_data_package = relationship("RawDataPackage", back_populates="package_metadata") def __repr__(self): return f"<RawDataPackageMetadata(id={self.id}, raw_data_package_id={self.raw_data_package_id})>"
[docs] class RawDataPackage(Base): """A raw data package is a bundle of raw data files that were observed in an observation unit. But they should never be larger than 50GB in size. """ __tablename__ = "raw_data_package" id = Column(Integer, primary_key=True) name = Column(String(250), nullable=False) relative_path = Column( String(250), nullable=False, doc=""" This is the relative path to the raw data package. The absolute path of the location of this file for each archive is stored in the DataLocation table. The path is relative to the raw_data_path of the DataLocation. """, ) size = Column(BigInteger, nullable=False) executed_obs_unit_id = Column( UUID(as_uuid=True), ForeignKey("executed_obs_unit.id", ondelete="CASCADE"), nullable=False, ) executed_obs_unit = relationship( "ExecutedObsUnit", back_populates="raw_data_packages" ) instrument_module_id = Column( Integer, ForeignKey("instrument_module.id"), nullable=False ) instrument_module = relationship( "InstrumentModule", back_populates="raw_data_packages" ) obs_unit_id = Column( Integer, ForeignKey("obs_unit.id", ondelete="CASCADE"), nullable=False ) obs_unit = relationship("ObsUnit", back_populates="raw_data_packages") created_at = Column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False, ) checksum = Column(String(250), nullable=False) raw_data_files = relationship("RawDataFile", back_populates="raw_data_package") data_transfer_package_id = Column( Integer, ForeignKey("data_transfer_package.id", ondelete="CASCADE"), nullable=True, ) data_transfer_package = relationship( "DataTransferPackage", back_populates="raw_data_packages" ) long_term_archive_transfers = relationship( "LongTermArchiveTransfer", back_populates="raw_data_package", cascade="all, delete-orphan", ) analyze_status = Column( SQLAlchemyEnum(Status, values_callable=lambda x: [e.value for e in x]), nullable=False, default=Status.PENDING ) physical_copies = relationship( "RawDataPackagePhysicalCopy", back_populates="raw_data_package", cascade="all, delete-orphan", ) package_metadata = relationship( "RawDataPackageMetadata", back_populates="raw_data_package", uselist=False, ) state = Column( SQLAlchemyEnum(PackageState, values_callable=lambda x: [e.value for e in x]), nullable=True, default=PackageState.WAITING, ) staging_jobs = relationship( "StagingJob", secondary=raw_data_package_staging_job_association, back_populates="raw_data_packages", )
[docs] class DataTransferPackage(Base): __tablename__ = "data_transfer_package" id = Column(Integer, primary_key=True) hash_id = Column(String(250), nullable=False) file_name = Column(String(250), nullable=False) size = Column(BigInteger, nullable=True) checksum = Column(String(250), nullable=True) relative_path = Column(String(250), nullable=False) origin_location_id = Column(Integer, ForeignKey("data_location.id"), nullable=False) origin_location = relationship("DataLocation", foreign_keys=[origin_location_id]) raw_data_files = relationship( "RawDataFile", back_populates="data_transfer_package", ) raw_data_packages = relationship( "RawDataPackage", back_populates="data_transfer_package", ) data_transfers = relationship( "DataTransfer", back_populates="data_transfer_package" ) physical_copies = relationship( "DataTransferPackagePhysicalCopy", back_populates="data_transfer_package", cascade="all, delete-orphan", )
[docs] class DataTransfer(Base): """Tracks data transfers between locations.""" __tablename__ = "data_transfer" id = Column(Integer, primary_key=True) process_id = Column(String(250)) start_time = Column(DateTime) end_time = Column(DateTime) data_transfer_method = Column(String(250), default="bbcp") transfer_program_log = Column(Text) # Location references origin_location_id = Column(Integer, ForeignKey("data_location.id"), nullable=False) origin_location = relationship("DataLocation", foreign_keys=[origin_location_id]) destination_location_id = Column( Integer, ForeignKey("data_location.id"), nullable=False ) destination_location = relationship( "DataLocation", foreign_keys=[destination_location_id] ) # Package reference data_transfer_package_id = Column( Integer, ForeignKey("data_transfer_package.id"), nullable=False ) data_transfer_package = relationship( "DataTransferPackage", back_populates="data_transfers" ) unpack_start_time = Column(DateTime) unpack_end_time = Column(DateTime) unpack_log = Column(Text) logs = relationship("DataTransferLog", back_populates="data_transfer")
[docs] class SystemLog(Base): __tablename__ = "system_log" id = Column(Integer, primary_key=True) type = Column(String(250), nullable=False) __mapper_args__ = { "polymorphic_on": type, "polymorphic_identity": "system_log", }
[docs] class DataTransferLog(SystemLog): """Log entries for data transfers with references to log files. This model implements a lightweight approach where: - Basic status info and log file path are stored in the database - Full command outputs are stored in files - Detailed metrics are stored in InfluxDB """ __tablename__ = "data_transfer_log" __mapper_args__ = { "polymorphic_identity": "data_transfer_log", } id = Column(Integer, ForeignKey("system_log.id"), primary_key=True) data_transfer_id = Column(Integer, ForeignKey("data_transfer.id"), nullable=False) data_transfer = relationship("DataTransfer", back_populates="logs") # Basic info timestamp = Column(DateTime, nullable=False) status = Column( String(50), nullable=False, doc="Status of this transfer attempt (success/failure)", ) # Path to the log file log_path = Column( String(500), nullable=False, doc="Path to file containing full command output" ) @property def content(self) -> Optional[str]: """Read and return the full log content if file exists.""" if self.log_path and os.path.exists(self.log_path): with open(self.log_path, "r") as f: return f.read() return None
# def __repr__(self): # return ( # f"<DataTransferLog(id={self.id}, " # f"data_transfer_id={self.data_transfer_id}, " # f"timestamp={self.timestamp}, " # f"status={self.status})>" # )
[docs] class OperationFailureEvent(SystemLog): """Append-only failure-history record for any pipeline operation (#85). One row per failure occurrence (retryable and permanent), never erased by a reset/retry — the durable trail behind the denormalized per-row ``error_context`` cache. Generic across operation types, so it keys on ``(operation_type, operation_id)`` — the pair the system already correlates on (Celery task state, recovery handlers, circuit breaker, routing) — and carries NO foreign key to the operation row (it can't reference one of several tables). Precedent for the bare integer id: ``PhysicalCopy.deletion_task_id``. """ __tablename__ = "operation_failure_event" __mapper_args__ = { "polymorphic_identity": "operation_failure_event", } id = Column(Integer, ForeignKey("system_log.id"), primary_key=True) operation_type = Column(String(50), nullable=False) operation_id = Column(Integer, nullable=False) error_type = Column(String(250), nullable=True) error_context = Column(JSONB_VARIANT, nullable=True) created_at = Column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False, ) __table_args__ = ( Index( "ix_operation_failure_event_operation", "operation_type", "operation_id", ), )
[docs] class DataArchive(Base): __tablename__ = "data_archive" id = Column(Integer, primary_key=True) name = Column(String(250), nullable=False)
[docs] class PhysicalCopy(Base): """Base class for tracking physical copies of files across different storage locations. This class implements a polymorphic pattern to track physical copies of different types of files (RawDataFile, RawDataPackage, DataTransferPackage) across different storage locations and types. Each physical copy represents an actual file on disk or other storage medium. """ __tablename__ = "physical_copy" id = Column(Integer, primary_key=True) type = Column(String(50), nullable=False) # Location reference data_location_id = Column(Integer, ForeignKey("data_location.id"), nullable=False) data_location = relationship("DataLocation") created_at = Column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False, ) verified_at = Column(DateTime(timezone=True), nullable=True) status = Column( SQLAlchemyEnum(PhysicalCopyStatus, values_callable=lambda x: [e.value for e in x]), nullable=False, default=PhysicalCopyStatus.PRESENT, ) checksum = Column(String(250), nullable=True) deletion_task_id = Column(Integer, nullable=True) deleted_at = Column(DateTime(timezone=True), nullable=True) # Copy-anchored lineage back-references (ops-db #94): the operations that # consumed this copy as input and that produced it as output. consumed_by_operations = relationship( "Operation", secondary=operation_consumes_physical_copy, back_populates="consumed_copies", ) produced_by_operations = relationship( "Operation", secondary=operation_produces_physical_copy, back_populates="produced_copies", ) __mapper_args__ = { "polymorphic_on": type, "polymorphic_identity": "physical_copy", }
[docs] class RawDataFilePhysicalCopy(PhysicalCopy): """Tracks physical copies of individual raw data files.""" __tablename__ = "raw_data_file_physical_copy" __mapper_args__ = {"polymorphic_identity": "raw_data_file_physical_copy"} id = Column(Integer, ForeignKey("physical_copy.id"), primary_key=True) raw_data_file_id = Column( UUID(as_uuid=True), ForeignKey("raw_data_file.id"), nullable=False ) raw_data_file = relationship("RawDataFile", back_populates="physical_copies") @property def full_path(self): """Get the full path/S3 key for this physical copy.""" if isinstance(self.data_location, DiskDataLocation): return os.path.join( self.data_location.path, self.raw_data_file.relative_path ) elif isinstance(self.data_location, S3DataLocation): # For S3, return just the relative path - the actual S3 key construction # should be handled by the data-transfer package using the shared utility functions return self.raw_data_file.relative_path elif isinstance(self.data_location, TapeDataLocation): return os.path.join( self.data_location.mount_path, self.raw_data_file.relative_path ) return self.raw_data_file.relative_path
[docs] class RawDataPackagePhysicalCopy(PhysicalCopy): """Tracks physical copies of raw data packages.""" __tablename__ = "raw_data_package_physical_copy" __mapper_args__ = {"polymorphic_identity": "raw_data_package_physical_copy"} id = Column(Integer, ForeignKey("physical_copy.id"), primary_key=True) raw_data_package_id = Column( Integer, ForeignKey("raw_data_package.id", ondelete="CASCADE"), nullable=False ) raw_data_package = relationship("RawDataPackage", back_populates="physical_copies") @property def full_path(self): """Get the full path/S3 key for this physical copy.""" if isinstance(self.data_location, DiskDataLocation): return os.path.join( self.data_location.path, self.raw_data_package.relative_path ) elif isinstance(self.data_location, S3DataLocation): # For S3, return just the relative path - the actual S3 key construction # should be handled by the data-transfer package using the shared utility functions return self.raw_data_package.relative_path elif isinstance(self.data_location, TapeDataLocation): return os.path.join( self.data_location.mount_path, self.raw_data_package.relative_path ) return self.raw_data_package.relative_path
[docs] class DataTransferPackagePhysicalCopy(PhysicalCopy): """Tracks physical copies of data transfer packages.""" __tablename__ = "data_transfer_package_physical_copy" __mapper_args__ = {"polymorphic_identity": "data_transfer_package_physical_copy"} id = Column(Integer, ForeignKey("physical_copy.id"), primary_key=True) data_transfer_package_id = Column( Integer, ForeignKey("data_transfer_package.id", ondelete="CASCADE"), nullable=False, ) data_transfer_package = relationship( "DataTransferPackage", back_populates="physical_copies" ) @property def full_path(self): """Get the full path/S3 key for this physical copy.""" if isinstance(self.data_location, DiskDataLocation): return os.path.join( self.data_location.path, self.data_transfer_package.relative_path ) elif isinstance(self.data_location, S3DataLocation): # For S3, return just the relative path - the actual S3 key construction # should be handled by the data-transfer package using the shared utility functions return self.data_transfer_package.relative_path elif isinstance(self.data_location, TapeDataLocation): return os.path.join( self.data_location.mount_path, self.data_transfer_package.relative_path ) return self.data_transfer_package.relative_path
[docs] class LongTermArchiveTransfer(Base): __tablename__ = "long_term_archive_transfer" id = Column(Integer, primary_key=True) site_id = Column(Integer, ForeignKey("site.id")) site = relationship("Site", back_populates="long_term_archive_transfers") origin_data_location_id = Column(Integer, ForeignKey("data_location.id")) origin_data_location = relationship( "DataLocation", foreign_keys=[origin_data_location_id] ) destination_data_location_id = Column(Integer, ForeignKey("data_location.id")) destination_data_location = relationship( "DataLocation", foreign_keys=[destination_data_location_id] ) raw_data_package_id = Column(Integer, ForeignKey("raw_data_package.id")) raw_data_package = relationship( "RawDataPackage", back_populates="long_term_archive_transfers" ) # Store logs directly in the LongTermArchiveTransfer model as a text field logs = relationship( "LongTermArchiveTransferLog", back_populates="long_term_archive_transfer" ) start_time = Column(DateTime, nullable=True) end_time = Column(DateTime, nullable=True) # Add fields for tracking attempts last_attempt_time = Column(DateTime, nullable=True) # Add a field to store error messages error_message = Column(Text, nullable=True) __table_args__ = ( UniqueConstraint( "raw_data_package_id", "origin_data_location_id", "destination_data_location_id", name="uix_long_term_archive_transfer_location", ), )
[docs] class LongTermArchiveTransferLog(SystemLog): __tablename__ = "long_term_archive_transfer_log" id = Column(Integer, ForeignKey("system_log.id"), primary_key=True) long_term_archive_transfer_id = Column( Integer, ForeignKey("long_term_archive_transfer.id"), nullable=False ) long_term_archive_transfer = relationship( "LongTermArchiveTransfer", back_populates="logs" ) log = Column(Text, nullable=False) timestamp = Column(DateTime, nullable=False) __mapper_args__ = { "polymorphic_identity": "long_term_archive_transfer_log", }
[docs] class StagingJob(Base): __tablename__ = "staging_job" id = Column(Integer, primary_key=True) status = Column(SQLAlchemyEnum(Status, values_callable=lambda x: [e.value for e in x]), nullable=False, default=Status.PENDING) failure_error_message = Column(Text, nullable=True) start_time = Column(DateTime, nullable=True) active = Column(Boolean, nullable=False, default=True) end_time = Column(DateTime, nullable=True) retry_count = Column(Integer, nullable=False, default=0) raw_data_packages = relationship( "RawDataPackage", secondary=raw_data_package_staging_job_association, back_populates="staging_jobs", ) logs = relationship("StagingJobLog", back_populates="staging_job") origin_data_location_id = Column( Integer, ForeignKey("data_location.id"), nullable=False ) origin_data_location = relationship( "DataLocation", foreign_keys=[origin_data_location_id] ) destination_data_location_id = Column( Integer, ForeignKey("data_location.id"), nullable=False ) destination_data_location = relationship( "DataLocation", foreign_keys=[destination_data_location_id] )
[docs] class StagingJobLog(SystemLog): __tablename__ = "staging_job_log" id = Column(Integer, ForeignKey("system_log.id"), primary_key=True) staging_job_id = Column(Integer, ForeignKey("staging_job.id"), nullable=False) staging_job = relationship("StagingJob", back_populates="logs") log = Column(Text, nullable=False) timestamp = Column(DateTime, nullable=False) __mapper_args__ = { "polymorphic_identity": "staging_job_log", }
# --------------------------------------------------------------------------- # Uniform Operation model (ops-db #94, ADR-0003) # # Every pipeline stage becomes a uniform, independently addressable operation: # one polymorphic Operation base (joined-table inheritance, same pattern as # SystemLog / PhysicalCopy) plus six stage subclasses. This is ADDITIVE -- the # legacy status/counter columns on RawDataPackage, DataTransferPackage, # DataTransfer, and LongTermArchiveTransfer are retained so the current chain # keeps working until a later cleanup migrates onto these tables. # ---------------------------------------------------------------------------
[docs] class OperationGroup(Base): """Polymorphic parent that bundles N co-created operations (ADR-0003). Its status is DERIVED from its child operations and is never set independently -- modelled as a read-only Python property, not a mapped column, so the derivation rule is the single source of truth and the group cannot drift from its children. Built minimal/claim-free here; the retention claim lives on the StagingOperationGroup specialization. """ __tablename__ = "operation_group" id = Column(Integer, primary_key=True) type = Column(String(50), nullable=False) created_at = Column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False, ) operations = relationship("Operation", back_populates="operation_group") __mapper_args__ = { "polymorphic_on": type, "polymorphic_identity": "operation_group", } @property def status(self) -> "Status": """Derive the group status from the child operations. Rule (in precedence order): - any child FAILED -> FAILED - all children COMPLETED -> COMPLETED - any child past PENDING -> IN_PROGRESS (work is in flight) - otherwise (empty, or all PENDING/SCHEDULED) -> PENDING An empty group has nothing to derive from and reads as PENDING (not-yet-started), which is also the safe default before children are attached. SCHEDULED is folded into the pre-start bucket: a group with only SCHEDULED children has dispatched nothing yet, so it is not in flight; the moment any child reaches IN_PROGRESS (or beyond, while others lag) the group reads IN_PROGRESS. """ children = self.operations if not children: return Status.PENDING statuses = [op.status for op in children] if any(s == Status.FAILED for s in statuses): return Status.FAILED if all(s == Status.COMPLETED for s in statuses): return Status.COMPLETED if all(s in (Status.PENDING, Status.SCHEDULED) for s in statuses): return Status.PENDING return Status.IN_PROGRESS
[docs] class StagingOperationGroup(OperationGroup): """Staging specialization of OperationGroup carrying the retention claim. This is the uniform-model successor to the legacy ``StagingJob`` (which is retained, untouched, during the additive transition). ``active`` is the retention claim that holds the staged packages until released -- the deletion manager treats an active staging group as a hold on its copies. The job's status is still derived from its child staging operations on the OperationGroup base; only the claim is added here. """ __tablename__ = "staging_operation_group" id = Column(Integer, ForeignKey("operation_group.id"), primary_key=True) active = Column(Boolean, nullable=False, default=True) __mapper_args__ = { "polymorphic_identity": "staging_operation_group", }
[docs] class Operation(Base): """Polymorphic base for every pipeline operation (joined-table inheritance). Uniform fields shared by all stages live here; stage-specific fields live on the subclasses. ``operation_kind`` is the polymorphic discriminator and holds an ``OperationKind`` value (== the frozen breadcrumb string). """ __tablename__ = "operation" id = Column(Integer, primary_key=True) # Discriminator / identity axis -- the OperationKind value (frozen string). operation_kind = Column( SQLAlchemyEnum(OperationKind, values_callable=lambda x: [e.value for e in x]), nullable=False, ) # Uniform operation fields (one canonical state machine + one retry counter). status = Column( SQLAlchemyEnum(Status, values_callable=lambda x: [e.value for e in x]), nullable=False, default=Status.PENDING, ) retry_count = Column(Integer, nullable=False, default=0) failure_error_message = Column(Text, nullable=True) # Structured latest-failure breadcrumb; JSONB_VARIANT so SELECT DISTINCT over # an operation row carries an equality operator on Postgres (ADR-0002). error_context = Column(JSONB_VARIANT, nullable=True) start_time = Column(DateTime(timezone=True), nullable=True) end_time = Column(DateTime(timezone=True), nullable=True) # Optional membership in a batched group (status derived there). operation_group_id = Column( Integer, ForeignKey("operation_group.id"), nullable=True ) operation_group = relationship("OperationGroup", back_populates="operations") # Copy-anchored consume/produce lineage (ADR-0003). Identical contract for # every kind: an operation consumes input copies and produces output copies. consumed_copies = relationship( "PhysicalCopy", secondary=operation_consumes_physical_copy, back_populates="consumed_by_operations", ) produced_copies = relationship( "PhysicalCopy", secondary=operation_produces_physical_copy, back_populates="produced_by_operations", ) __mapper_args__ = { "polymorphic_on": operation_kind, # No base polymorphic_identity: Operation is abstract in practice -- every # row is one of the six concrete kinds (mirrors DataLocation's None base). "polymorphic_identity": None, }
[docs] class PackagingOperation(Operation): """Packaging stage: assembles raw data files into a RawDataPackage.""" __tablename__ = "packaging_operation" id = Column(Integer, ForeignKey("operation.id"), primary_key=True) raw_data_package_id = Column( Integer, ForeignKey("raw_data_package.id", ondelete="CASCADE"), nullable=True ) __mapper_args__ = {"polymorphic_identity": OperationKind.PACKAGING}
[docs] class BundlingOperation(Operation): """Bundling stage: assembles packages into a DataTransferPackage.""" __tablename__ = "bundling_operation" id = Column(Integer, ForeignKey("operation.id"), primary_key=True) data_transfer_package_id = Column( Integer, ForeignKey("data_transfer_package.id", ondelete="CASCADE"), nullable=True, ) __mapper_args__ = {"polymorphic_identity": OperationKind.BUNDLING}
[docs] class TransferOperation(Operation): """Transfer stage: moves a DataTransferPackage between locations. Sibling of UnpackOperation under the same DataTransferPackage (ADR-0003) -- the transfer/unpack unicorn record splits into two independent rows. """ __tablename__ = "transfer_operation" id = Column(Integer, ForeignKey("operation.id"), primary_key=True) data_transfer_package_id = Column( Integer, ForeignKey("data_transfer_package.id", ondelete="CASCADE"), nullable=True, ) origin_location_id = Column(Integer, ForeignKey("data_location.id"), nullable=True) destination_location_id = Column( Integer, ForeignKey("data_location.id"), nullable=True ) transfer_method = Column(String(250), nullable=True, default="bbcp") __mapper_args__ = {"polymorphic_identity": OperationKind.TRANSFER}
[docs] class UnpackOperation(Operation): """Unpack stage: verifies and unpacks a transferred DataTransferPackage. Sibling of TransferOperation under the same DataTransferPackage (ADR-0003). Anchored per (data_transfer_package_id, destination_location_id): a package transferred to several destinations is unpacked/verified independently at each, so there is one UnpackOperation per destination, mirroring the sibling TransferOperation's destination_location_id. This makes the transfer+unpack pair 1:1 per destination. destination_location_id is nullable for the additive transition (no backfill). """ __tablename__ = "unpack_operation" id = Column(Integer, ForeignKey("operation.id"), primary_key=True) data_transfer_package_id = Column( Integer, ForeignKey("data_transfer_package.id", ondelete="CASCADE"), nullable=True, ) destination_location_id = Column( Integer, ForeignKey("data_location.id"), nullable=True ) __mapper_args__ = {"polymorphic_identity": OperationKind.UNPACK}
[docs] class ArchiveOperation(Operation): """Archive stage: copies a RawDataPackage to long-term storage.""" __tablename__ = "archive_operation" id = Column(Integer, ForeignKey("operation.id"), primary_key=True) raw_data_package_id = Column( Integer, ForeignKey("raw_data_package.id"), nullable=True ) origin_location_id = Column(Integer, ForeignKey("data_location.id"), nullable=True) destination_location_id = Column( Integer, ForeignKey("data_location.id"), nullable=True ) __mapper_args__ = {"polymorphic_identity": OperationKind.ARCHIVE}
[docs] class StagingOperation(Operation): """Staging stage: downloads + unpacks a package into a processing area. Typically grouped under a StagingOperationGroup (the retention-claim parent). """ __tablename__ = "staging_operation" id = Column(Integer, ForeignKey("operation.id"), primary_key=True) raw_data_package_id = Column( Integer, ForeignKey("raw_data_package.id"), nullable=True ) origin_location_id = Column(Integer, ForeignKey("data_location.id"), nullable=True) destination_location_id = Column( Integer, ForeignKey("data_location.id"), nullable=True ) __mapper_args__ = {"polymorphic_identity": OperationKind.STAGING}
# --------------------------------------------------------------------------- # Workflow Manager Models # --------------------------------------------------------------------------- # Association tables for workflow manager executed_step_input_packages = Table( "executed_step_input_packages", Base.metadata, Column("executed_reduction_step_id", Integer, ForeignKey("executed_reduction_step.id")), Column("raw_data_package_id", Integer, ForeignKey("raw_data_package.id")), Column("role", String, default="science"), ) executed_step_input_products = Table( "executed_step_input_products", Base.metadata, Column("executed_reduction_step_id", Integer, ForeignKey("executed_reduction_step.id")), Column("data_product_id", Integer, ForeignKey("data_product.id")), Column("role", String, default="science"), ) data_product_lineage = Table( "data_product_lineage", Base.metadata, Column("data_product_id", Integer, ForeignKey("data_product.id")), Column("raw_data_package_id", Integer, ForeignKey("raw_data_package.id")), ) reduction_step_dependency = Table( "reduction_step_dependency", Base.metadata, Column( "upstream_step_id", Integer, ForeignKey("reduction_step.id"), primary_key=True, ), Column( "downstream_step_id", Integer, ForeignKey("reduction_step.id"), primary_key=True, ), )
[docs] class ReductionSoftware(Base): __tablename__ = "reduction_software" id = Column(Integer, primary_key=True) name = Column(String, unique=True, nullable=False) description = Column(Text) ghcr_image_url = Column(String) github_repo_url = Column(String) created_at = Column( DateTime, default=lambda: datetime.now(timezone.utc), nullable=False ) active = Column(Boolean, default=True) versions = relationship( "ReductionSoftwareVersion", back_populates="reduction_software" ) reduction_steps = relationship( "ReductionStep", back_populates="reduction_software" )
[docs] class ReductionSoftwareVersion(Base): __tablename__ = "reduction_software_version" id = Column(Integer, primary_key=True) reduction_software_id = Column( Integer, ForeignKey("reduction_software.id"), nullable=False ) version_tag = Column(String, nullable=False) image_digest = Column(String) build_date = Column(DateTime) changelog = Column(Text) created_at = Column( DateTime, default=lambda: datetime.now(timezone.utc), nullable=False ) is_latest = Column(Boolean, default=False) reduction_software = relationship( "ReductionSoftware", back_populates="versions" ) executed_steps = relationship( "ExecutedReductionStep", back_populates="reduction_software_version" ) __table_args__ = ( UniqueConstraint("reduction_software_id", "version_tag"), )
[docs] class ReductionStepConfig(Base): __tablename__ = "reduction_step_config" id = Column(Integer, primary_key=True) name = Column(String, nullable=False) version = Column(String) description = Column(Text) config_parameters = Column(JSONB_VARIANT) environment_variables = Column(JSONB_VARIANT) command_template = Column(String) resource_requirements = Column(JSONB_VARIANT) created_at = Column( DateTime, default=lambda: datetime.now(timezone.utc), nullable=False ) reduction_steps = relationship( "ReductionStep", back_populates="reduction_step_config" )
[docs] class DataGrouping(Base): __tablename__ = "data_grouping" id = Column(Integer, primary_key=True) name = Column(String, nullable=False) description = Column(Text) instrument_module_id = Column( Integer, ForeignKey("instrument_module.id"), nullable=True ) filter_rules = Column(JSONB_VARIANT) created_at = Column( DateTime, default=lambda: datetime.now(timezone.utc), nullable=False ) active = Column(Boolean, default=True) instrument_module = relationship("InstrumentModule") pipelines = relationship("Pipeline", back_populates="data_grouping")
[docs] class Pipeline(Base): __tablename__ = "pipeline" id = Column(Integer, primary_key=True) name = Column(String, nullable=False) description = Column(Text) data_grouping_id = Column( Integer, ForeignKey("data_grouping.id"), nullable=False ) processing_location_id = Column( Integer, ForeignKey("data_location.id"), nullable=False ) trigger_type = Column( SQLAlchemyEnum(TriggerType), default=TriggerType.MANUAL ) trigger_config = Column(JSONB_VARIANT) enabled = Column(Boolean, default=True) priority = Column(Integer, default=0) created_by_user_id = Column(Integer, nullable=True) created_at = Column( DateTime, default=lambda: datetime.now(timezone.utc), nullable=False ) updated_at = Column(DateTime, onupdate=lambda: datetime.now(timezone.utc)) data_grouping = relationship("DataGrouping", back_populates="pipelines") processing_location = relationship("DataLocation") reduction_steps = relationship( "ReductionStep", back_populates="pipeline", order_by="ReductionStep.step_order", )
[docs] class ReductionStep(Base): __tablename__ = "reduction_step" id = Column(Integer, primary_key=True) name = Column(String, nullable=False) description = Column(Text) pipeline_id = Column( Integer, ForeignKey("pipeline.id"), nullable=False ) step_order = Column(Integer, nullable=False) reduction_software_id = Column( Integer, ForeignKey("reduction_software.id"), nullable=False ) pinned_version_id = Column( Integer, ForeignKey("reduction_software_version.id"), nullable=True ) reduction_step_config_id = Column( Integer, ForeignKey("reduction_step_config.id"), nullable=False ) group_by = Column(JSONB_VARIANT) cooldown_seconds = Column(Integer, default=0) schedule = Column(String, nullable=True) max_concurrent_runs = Column(Integer, default=1) version_policy = Column(String, default="compatible") max_retries = Column(Integer, default=3) created_at = Column( DateTime, default=lambda: datetime.now(timezone.utc), nullable=False ) pipeline = relationship("Pipeline", back_populates="reduction_steps") reduction_software = relationship( "ReductionSoftware", back_populates="reduction_steps" ) pinned_version = relationship("ReductionSoftwareVersion") reduction_step_config = relationship( "ReductionStepConfig", back_populates="reduction_steps" ) executed_steps = relationship( "ExecutedReductionStep", back_populates="reduction_step" ) upstream_dependencies = relationship( "ReductionStep", secondary=reduction_step_dependency, primaryjoin=id == reduction_step_dependency.c.downstream_step_id, secondaryjoin=id == reduction_step_dependency.c.upstream_step_id, backref="downstream_dependencies", )
[docs] class ExecutedReductionStep(Base): __tablename__ = "executed_reduction_step" id = Column(Integer, primary_key=True) reduction_step_id = Column( Integer, ForeignKey("reduction_step.id"), nullable=False ) reduction_software_version_id = Column( Integer, ForeignKey("reduction_software_version.id"), nullable=False, ) status = Column( SQLAlchemyEnum(RunStatus), default=RunStatus.PENDING, nullable=False, ) sub_group_key = Column(String) sub_group_metadata = Column(JSONB_VARIANT) execution_command = Column(Text) hpc_job_id = Column(String) hpc_queue = Column(String) staging_job_id = Column( Integer, ForeignKey("staging_job.id"), nullable=True ) start_time = Column(DateTime) end_time = Column(DateTime) processing_time_s = Column(Float, nullable=True) peak_memory_gb = Column(Float, nullable=True) cpu_hours = Column(Float, nullable=True) logs = Column(Text) failure_error_message = Column(Text) retry_count = Column(Integer, default=0) created_at = Column( DateTime, default=lambda: datetime.now(timezone.utc), nullable=False ) trigger_reason = Column(String) __table_args__ = ( Index("ix_executed_reduction_step_status", "status"), Index("ix_executed_reduction_step_step_id", "reduction_step_id"), Index("ix_executed_reduction_step_sub_group", "sub_group_key"), ) reduction_step = relationship( "ReductionStep", back_populates="executed_steps" ) reduction_software_version = relationship( "ReductionSoftwareVersion", back_populates="executed_steps" ) staging_job = relationship("StagingJob") input_packages = relationship( "RawDataPackage", secondary=executed_step_input_packages ) input_products = relationship( "DataProduct", secondary=executed_step_input_products, back_populates="consumed_by_steps", ) data_products = relationship( "DataProduct", back_populates="executed_reduction_step" ) step_logs = relationship( "ExecutedReductionStepLog", back_populates="executed_reduction_step" )
[docs] class DataProduct(Base): __tablename__ = "data_product" id = Column(Integer, primary_key=True) name = Column(String, nullable=False) relative_path = Column(String) size = Column(BigInteger) checksum = Column(String) file_type = Column(String) product_type = Column( SQLAlchemyEnum(DataProductType), default=DataProductType.SCIENCE ) executed_reduction_step_id = Column( Integer, ForeignKey("executed_reduction_step.id"), nullable=False ) data_grouping_id = Column( Integer, ForeignKey("data_grouping.id"), nullable=True ) quality_metrics = Column(JSONB_VARIANT) metadata_ = Column("metadata", JSONB_VARIANT) created_at = Column( DateTime, default=lambda: datetime.now(timezone.utc), nullable=False ) executed_reduction_step = relationship( "ExecutedReductionStep", back_populates="data_products" ) data_grouping = relationship("DataGrouping") raw_data_lineage = relationship( "RawDataPackage", secondary=data_product_lineage ) physical_copies = relationship( "DataProductPhysicalCopy", back_populates="data_product", cascade="all, delete-orphan", ) consumed_by_steps = relationship( "ExecutedReductionStep", secondary=executed_step_input_products, back_populates="input_products", )
[docs] class DataProductPhysicalCopy(PhysicalCopy): __tablename__ = "data_product_physical_copy" id = Column(Integer, ForeignKey("physical_copy.id"), primary_key=True) data_product_id = Column( Integer, ForeignKey("data_product.id"), nullable=False ) data_product = relationship( "DataProduct", back_populates="physical_copies" ) __mapper_args__ = { "polymorphic_identity": "data_product_physical_copy", } @property def full_path(self): if isinstance(self.data_location, DiskDataLocation): return f"{self.data_location.path}/{self.data_product.relative_path}" elif isinstance(self.data_location, S3DataLocation): return f"s3://{self.data_location.bucket_name}/{self.data_product.relative_path}" return None
[docs] class ExecutedReductionStepLog(SystemLog): __tablename__ = "executed_reduction_step_log" id = Column(Integer, ForeignKey("system_log.id"), primary_key=True) executed_reduction_step_id = Column( Integer, ForeignKey("executed_reduction_step.id"), nullable=False ) executed_reduction_step = relationship( "ExecutedReductionStep", back_populates="step_logs" ) log = Column(Text, nullable=False) timestamp = Column(DateTime, nullable=False) __mapper_args__ = { "polymorphic_identity": "executed_reduction_step_log", }
# --------------------------------------------------------------------------- # Operational Config Models # ---------------------------------------------------------------------------
[docs] class SystemSettings(Base): """Key-value store for operational configuration settings. Used by data-transfer SettingsManager to read runtime-tunable parameters (poll intervals, buffer thresholds, transfer limits, etc.). """ __tablename__ = "system_settings" id = Column(Integer, primary_key=True) key = Column(String(250), unique=True, index=True, nullable=False) value = Column(JSONB_VARIANT, nullable=False) description = Column(Text, nullable=True) updated_by = Column(String(150), nullable=True) updated_at = Column( DateTime, default=lambda: datetime.now(timezone.utc), nullable=False ) created_at = Column( DateTime, default=lambda: datetime.now(timezone.utc), nullable=False )
[docs] class OperationalConfigLog(SystemLog): """Audit log for operational configuration changes.""" __tablename__ = "operational_config_log" id = Column(Integer, ForeignKey("system_log.id"), primary_key=True) setting_key = Column(String(250), nullable=False, index=True) old_value = Column(JSONB_VARIANT, nullable=True) new_value = Column(JSONB_VARIANT, nullable=True) action = Column(String(50), nullable=False) changed_by = Column(String(150), nullable=True) changed_at = Column( DateTime, default=lambda: datetime.now(timezone.utc), nullable=False ) __mapper_args__ = { "polymorphic_identity": "operational_config", }