pipeline_run.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. """Models for a Slicer Pipeline run (#1425 PR B).
  2. A PipelineRun is one "Run pipeline" click: slice the source file once with the
  3. pipeline's four preset slots, then enqueue a single print on the pipeline's
  4. pinned target printer (PR B = single-target dispatch). PR C extends this with
  5. copies > 1 and class targeting + fanout strategies.
  6. Status on a PipelineRun is mostly COMPUTED from the underlying slice_job
  7. (in-memory) + the linked queue_entry's state at read time — see
  8. ``api/routes/pipeline_runs.py`` ``_compute_run_status`` for the rules. The
  9. ``status`` column is the persisted snapshot used as a fallback / for filtering
  10. in list queries; it's updated on terminal transitions (slice failure, cancel,
  11. or queue-entry completion).
  12. """
  13. from datetime import datetime
  14. from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, func
  15. from sqlalchemy.orm import Mapped, mapped_column, relationship
  16. from backend.app.core.database import Base
  17. class PipelineRun(Base):
  18. """One run-pipeline invocation. PR B always carries exactly one
  19. PipelineJob (copies=1); PR C will allow N."""
  20. __tablename__ = "pipeline_runs"
  21. id: Mapped[int] = mapped_column(primary_key=True)
  22. # Pipeline + source. ``ondelete='SET NULL'`` on both so run history survives
  23. # the user soft-deleting a pipeline or removing the source library file.
  24. pipeline_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("slicer_pipelines.id", ondelete="SET NULL"))
  25. source_library_file_id: Mapped[int | None] = mapped_column(
  26. Integer, ForeignKey("library_files.id", ondelete="SET NULL")
  27. )
  28. # Mutually exclusive with source_library_file_id. When set, the orchestrator
  29. # reads ``archive.source_3mf_path`` (falling back to ``file_path``) for the
  30. # slice input. Lets ArchiveCard's "Run with pipeline" reuse the same /run
  31. # endpoint instead of growing a second route.
  32. source_archive_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("print_archives.id", ondelete="SET NULL"))
  33. # Set when this run was created by ``POST /pipeline-runs/{parent}/retry-failed``.
  34. # Chains the new run back to the run whose failed copies it re-attempts so
  35. # the dashboard can show "Retry of run #N" inline. ``SET NULL`` so cleaning
  36. # up old runs doesn't dangle retries.
  37. parent_run_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("pipeline_runs.id", ondelete="SET NULL"))
  38. copies: Mapped[int] = mapped_column(Integer, default=1)
  39. # Snapshot status — terminal transitions are persisted here, in-flight
  40. # reads compute from slice_job + queue_entry. Values:
  41. # 'queued', 'slicing', 'dispatching', 'in_progress',
  42. # 'completed', 'failed', 'cancelled'
  43. status: Mapped[str] = mapped_column(String(20), default="queued")
  44. # Slice integration. slice_job_id is the in-memory slice_dispatch id (so
  45. # it's a plain int, not an FK). sliced_library_file_id is the produced
  46. # gcode.3mf row.
  47. slice_job_id: Mapped[int | None] = mapped_column(Integer)
  48. sliced_library_file_id: Mapped[int | None] = mapped_column(
  49. Integer, ForeignKey("library_files.id", ondelete="SET NULL")
  50. )
  51. # True when the operator chose to "Run anyway" past eligibility issues
  52. # (filament mismatch, etc.). Surfaced in run history so the audit log
  53. # shows which runs bypassed the pre-flight.
  54. eligibility_overridden: Mapped[bool] = mapped_column(Boolean, default=False, server_default="0")
  55. error_message: Mapped[str | None] = mapped_column(Text)
  56. created_by: Mapped[int | None] = mapped_column(Integer, ForeignKey("users.id", ondelete="SET NULL"))
  57. created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
  58. started_at: Mapped[datetime | None] = mapped_column(DateTime)
  59. completed_at: Mapped[datetime | None] = mapped_column(DateTime)
  60. jobs: Mapped[list["PipelineJob"]] = relationship(
  61. back_populates="run",
  62. cascade="all, delete-orphan",
  63. order_by="PipelineJob.copy_index",
  64. )
  65. class PipelineJob(Base):
  66. """One copy within a PipelineRun. PR B: always exactly one per run.
  67. Each job binds the run to one queue entry (``queue_entry_id``). The
  68. queue entry's status drives this job's status; this row mostly carries
  69. the run-side narrative (dispatch timestamps, error message) so deleting
  70. the queue entry later doesn't lose the audit trail.
  71. """
  72. __tablename__ = "pipeline_jobs"
  73. id: Mapped[int] = mapped_column(primary_key=True)
  74. pipeline_run_id: Mapped[int] = mapped_column(Integer, ForeignKey("pipeline_runs.id", ondelete="CASCADE"))
  75. copy_index: Mapped[int] = mapped_column(Integer, default=0)
  76. assigned_printer_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("printers.id", ondelete="SET NULL"))
  77. queue_entry_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("print_queue.id", ondelete="SET NULL"))
  78. # Values: 'pending', 'awaiting_printer', 'queued', 'printing',
  79. # 'completed', 'failed', 'cancelled'
  80. status: Mapped[str] = mapped_column(String(20), default="pending")
  81. error_message: Mapped[str | None] = mapped_column(Text)
  82. dispatched_at: Mapped[datetime | None] = mapped_column(DateTime)
  83. completed_at: Mapped[datetime | None] = mapped_column(DateTime)
  84. run: Mapped["PipelineRun"] = relationship(back_populates="jobs")