Refactor job model and endpoints for improved structure

Updated the job model to include `sample_id` and `run_id` fields, replacing `experiment_parameters_id`. Adjusted relationships and modified routers to reflect these changes. Added an endpoint for updating job status and restructured job streaming logic to include detailed experiment and sample data.
This commit is contained in:
GotthardG 2025-04-29 14:43:39 +02:00
parent 3eb4050d82
commit 9af2e84f9e
7 changed files with 154 additions and 142 deletions

View File

@ -317,8 +317,9 @@ class Jobs(Base):
__tablename__ = "jobs"
id = Column(Integer, primary_key=True, index=True)
experiment_parameters_id = Column(Integer, ForeignKey("experiment_parameters.id"))
sample_id = Column(Integer, ForeignKey("samples.id"), nullable=False)
run_id = Column(Integer, ForeignKey("experiment_parameters.id"), nullable=False)
status = Column(String, nullable=False)
parameters = relationship(ExperimentParameters)
experiment_parameters = relationship(ExperimentParameters)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, onupdate=func.now())

View File

@ -4,7 +4,13 @@ from fastapi import APIRouter, Depends
from fastapi.encoders import jsonable_encoder
from sqlalchemy.orm import Session
from starlette.responses import StreamingResponse
from app.models import JobStatus, Jobs as JobModel
from app.models import (
JobStatus,
Jobs as JobModel,
ExperimentParameters as ExperimentParametersModel,
Sample as SampleModel,
)
from app.schemas import JobsResponse, JobsUpdate
from app.dependencies import get_db
router = APIRouter()
@ -13,9 +19,37 @@ router = APIRouter()
async def job_event_generator(db: Session):
while True:
jobs = db.query(JobModel).filter(JobModel.status == JobStatus.TODO).all()
if jobs:
job_payload = jsonable_encoder(jobs)
yield f"data: {json.dumps(job_payload)}\n\n"
job_items = []
for job in jobs:
sample = db.query(SampleModel).filter_by(id=job.sample_id).first()
experiment = (
db.query(ExperimentParametersModel)
.filter(ExperimentParametersModel.sample_id == sample.id)
.first()
)
job_item = JobsResponse(
job_id=job.id,
sample_id=sample.id,
run_id=getattr(experiment, "run_number", None),
sample_name=sample.sample_name,
status=job.status,
type=getattr(job, "type", "default_type"),
created_at=job.created_at,
updated_at=job.updated_at,
data_collection_parameters=sample.data_collection_parameters,
experiment_parameters=experiment.beamline_parameters
if experiment
else None,
)
job_items.append(job_item)
if job_items:
# Use Pydantic's .json() for each item, if you need a fine structure, or:
serialized = jsonable_encoder(job_items)
yield f"data: {json.dumps(serialized)}\n\n"
await asyncio.sleep(5)
@ -25,3 +59,29 @@ async def job_event_generator(db: Session):
@router.get("/jobs/stream")
async def stream_jobs(db: Session = Depends(get_db)):
return StreamingResponse(job_event_generator(db), media_type="text/event-stream")
@router.post(
"/jobs/update_status", response_model=JobsUpdate, operation_id="update_status"
)
def update_jobs_status(payload: JobsUpdate, db: Session = Depends(get_db)):
# Fetch the job by job_id
job = db.query(JobModel).filter(JobModel.id == payload.job_id).first()
if not job:
# Optionally, use HTTPException for proper status code
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="Job not found")
# Update the status
job.status = payload.status
# Optionally update 'updated_at'
from sqlalchemy import func
job.updated_at = func.now()
db.commit()
db.refresh(job)
# Return the updated job's info as response
return JobsUpdate(job_id=job.id, status=job.status)

View File

@ -351,8 +351,9 @@ def create_experiment_parameters_for_sample(
db.commit()
new_job = JobModel(
experiment_parameters_id=new_exp.id, # <-- Correct reference here
parameters=new_exp, # assuming params has a to_dict() method
sample_id=sample_id,
run_id=new_exp.id,
experiment_parameters=new_exp, # not sure yet
status=JobStatus.TODO,
)
db.add(new_job)

View File

@ -944,16 +944,32 @@ class ResultResponse(BaseModel):
class JobsCreate(BaseModel):
id: str
name: str
id: int
sample_id: int
run_id: int
sample_name: str
status: str
type: str
start_time: datetime
end_time: datetime
description: Optional[str]
parameters: str
datacollectionparameters: DataCollectionParameters
beamlineparameters: BeamlineParameters
created_at: datetime
updated_at: datetime
experiment_parameters: BeamlineParameters
class Config:
from_attributes = True
class JobsResponse(BaseModel):
job_id: int
sample_id: int
run_id: int
sample_name: str
status: str
type: str
created_at: datetime
updated_at: Optional[datetime]
data_collection_parameters: Optional[DataCollectionParameters] = None
experiment_parameters: BeamlineParameters
class JobsUpdate(BaseModel):
job_id: int
status: str

View File

@ -128,8 +128,8 @@ async def lifespan(app: FastAPI):
db = SessionLocal()
try:
if environment == "prod":
Base.metadata.drop_all(bind=engine)
Base.metadata.create_all(bind=engine)
# Base.metadata.drop_all(bind=engine)
# Base.metadata.create_all(bind=engine)
from sqlalchemy.engine import reflection
inspector = reflection.Inspector.from_engine(engine)

View File

@ -9,7 +9,7 @@
"is_executing": true
},
"ExecuteTime": {
"start_time": "2025-04-29T07:44:02.103530Z"
"start_time": "2025-04-29T12:21:15.813326Z"
}
},
"source": [
@ -18,13 +18,24 @@
"import json\n",
"\n",
"SSE_URL = \"https://127.0.0.1:8000/processing/jobs/stream\"\n",
"UPDATE_URL = \"https://127.0.0.1:8000/processing/jobs/update_status\"\n",
"\n",
"def submit_job_update(job_id, status):\n",
" payload = {\n",
" \"job_id\": job_id,\n",
" \"status\": status\n",
" }\n",
" try:\n",
" response = requests.post(UPDATE_URL, json=payload, verify=False)\n",
" if response.status_code == 200:\n",
" print(f\"✅ Job {job_id} status updated to '{status}'. Response: {response.json()}\")\n",
" else:\n",
" print(f\"❌ Failed to update job {job_id}. Status: {response.status_code}. Response: {response.text}\")\n",
" except Exception as e:\n",
" print(f\"Failed to submit update for Job {job_id}: {e}\")\n",
"\n",
"#SSE_URL = \"https://mx-aare-test.psi.ch:1492/processing/jobs/stream\"\n",
"\n",
"\n",
"def listen_and_process_jobs(url):\n",
" print(\"Starting processing pipeline...\")\n",
"def listen_and_update_jobs(url):\n",
" print(\"Starting job status updater...\")\n",
" with requests.get(url, stream=True, verify=False) as response:\n",
" if response.status_code != 200:\n",
" print(f\"Failed to connect with status code: {response.status_code}\")\n",
@ -37,69 +48,25 @@
" jobs = json.loads(event.data)\n",
" print(f\"Jobs received: {jobs}\")\n",
"\n",
" for job in jobs:\n",
" job_id = job.get(\"id\")\n",
" parameters = job.get(\"parameters\")\n",
" if parameters is None:\n",
" print(\n",
" f\"⚠️ Job {job_id if job_id is not None else '[unknown id]'} has no 'parameters'; skipping.\")\n",
" continue\n",
"\n",
" print(f\"Processing job ID: {job_id} with parameters: {parameters}\")\n",
"\n",
" # Your custom job-processing logic goes here\n",
" process_job_logic(job_id, parameters)\n",
"\n",
" # After job processing completes, send results & update job status\n",
" submit_job_result(job_id, processing_result={'success': True})\n",
" #for job in jobs:\n",
" # job_id = job.get(\"job_id\")\n",
" # print(f\"Job ID: {job_id}, Current status: {job.get('status')}\")\n",
" # # Immediately update status to \"submitted\"\n",
" # submit_job_update(job_id, \"submitted\")\n",
" except json.JSONDecodeError as e:\n",
" print(f\"Error decoding event data: {e}\")\n",
" except Exception as e:\n",
" print(f\"Unexpected error while processing event: {e}\")\n",
"\n",
"\n",
"def process_job_logic(job_id, parameters):\n",
" print(f\"📦 {job_id} - Processing detailed logic here: {parameters}\")\n",
" import time\n",
" time.sleep(5)\n",
" print(f\"✅ Job {job_id} processing complete\")\n",
"\n",
"\n",
"def submit_job_result(job_id, processing_result):\n",
" import backend.aareDBclient as aareDBclient\n",
" from aareDBclient.models import ResultCreate\n",
"\n",
" configuration = aareDBclient.Configuration(\n",
" host=\"https://127.0.0.1:8000\",\n",
" verify_ssl=False\n",
" )\n",
"\n",
" with aareDBclient.ApiClient(configuration) as api_client:\n",
" api_instance = aareDBclient.ProcessingApi(api_client)\n",
"\n",
" result = ResultCreate(\n",
" job_id=job_id,\n",
" result=processing_result\n",
" )\n",
" try:\n",
" res = api_instance.submit_processing_result(result_create=result)\n",
" print(f\"Job {job_id} result submitted successfully! Backend Response: {res}\")\n",
" except aareDBclient.rest.ApiException as e:\n",
" print(f\"Failed to submit result for Job {job_id}: {e}\")\n",
"\n",
"\n",
"if __name__ == \"__main__\":\n",
" listen_and_process_jobs(SSE_URL)"
" listen_and_update_jobs(SSE_URL)\n"
],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Starting processing pipeline...\n",
"Jobs received: [{'status': 'todo', 'updated_at': None, 'created_at': '2025-04-29T07:40:46.419291', 'id': 1, 'experiment_parameters_id': 1}, {'status': 'todo', 'updated_at': None, 'created_at': '2025-04-29T07:41:44.451862', 'id': 2, 'experiment_parameters_id': 2}]\n",
"⚠️ Job 1 has no 'parameters'; skipping.\n",
"⚠️ Job 2 has no 'parameters'; skipping.\n"
"Starting job status updater...\n"
]
},
{
@ -109,39 +76,6 @@
"/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/urllib3/connectionpool.py:1103: InsecureRequestWarning: Unverified HTTPS request is being made to host '127.0.0.1'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings\n",
" warnings.warn(\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Jobs received: [{'status': 'todo', 'updated_at': None, 'created_at': '2025-04-29T07:40:46.419291', 'id': 1, 'experiment_parameters_id': 1}, {'status': 'todo', 'updated_at': None, 'created_at': '2025-04-29T07:41:44.451862', 'id': 2, 'experiment_parameters_id': 2}]\n",
"⚠️ Job 1 has no 'parameters'; skipping.\n",
"⚠️ Job 2 has no 'parameters'; skipping.\n",
"Jobs received: [{'status': 'todo', 'updated_at': None, 'created_at': '2025-04-29T07:40:46.419291', 'id': 1, 'experiment_parameters_id': 1}, {'status': 'todo', 'updated_at': None, 'created_at': '2025-04-29T07:41:44.451862', 'id': 2, 'experiment_parameters_id': 2}]\n",
"⚠️ Job 1 has no 'parameters'; skipping.\n",
"⚠️ Job 2 has no 'parameters'; skipping.\n",
"Jobs received: [{'status': 'todo', 'updated_at': None, 'created_at': '2025-04-29T07:40:46.419291', 'id': 1, 'experiment_parameters_id': 1}, {'status': 'todo', 'updated_at': None, 'created_at': '2025-04-29T07:41:44.451862', 'id': 2, 'experiment_parameters_id': 2}]\n",
"⚠️ Job 1 has no 'parameters'; skipping.\n",
"⚠️ Job 2 has no 'parameters'; skipping.\n",
"Jobs received: [{'status': 'todo', 'updated_at': None, 'created_at': '2025-04-29T07:40:46.419291', 'id': 1, 'experiment_parameters_id': 1}, {'status': 'todo', 'updated_at': None, 'created_at': '2025-04-29T07:41:44.451862', 'id': 2, 'experiment_parameters_id': 2}]\n",
"⚠️ Job 1 has no 'parameters'; skipping.\n",
"⚠️ Job 2 has no 'parameters'; skipping.\n",
"Jobs received: [{'status': 'todo', 'updated_at': None, 'created_at': '2025-04-29T07:40:46.419291', 'id': 1, 'experiment_parameters_id': 1}, {'status': 'todo', 'updated_at': None, 'created_at': '2025-04-29T07:41:44.451862', 'id': 2, 'experiment_parameters_id': 2}]\n",
"⚠️ Job 1 has no 'parameters'; skipping.\n",
"⚠️ Job 2 has no 'parameters'; skipping.\n",
"Jobs received: [{'status': 'todo', 'updated_at': None, 'created_at': '2025-04-29T07:40:46.419291', 'id': 1, 'experiment_parameters_id': 1}, {'status': 'todo', 'updated_at': None, 'created_at': '2025-04-29T07:41:44.451862', 'id': 2, 'experiment_parameters_id': 2}]\n",
"⚠️ Job 1 has no 'parameters'; skipping.\n",
"⚠️ Job 2 has no 'parameters'; skipping.\n",
"Jobs received: [{'status': 'todo', 'updated_at': None, 'created_at': '2025-04-29T07:40:46.419291', 'id': 1, 'experiment_parameters_id': 1}, {'status': 'todo', 'updated_at': None, 'created_at': '2025-04-29T07:41:44.451862', 'id': 2, 'experiment_parameters_id': 2}]\n",
"⚠️ Job 1 has no 'parameters'; skipping.\n",
"⚠️ Job 2 has no 'parameters'; skipping.\n",
"Jobs received: [{'status': 'todo', 'updated_at': None, 'created_at': '2025-04-29T07:40:46.419291', 'id': 1, 'experiment_parameters_id': 1}, {'status': 'todo', 'updated_at': None, 'created_at': '2025-04-29T07:41:44.451862', 'id': 2, 'experiment_parameters_id': 2}]\n",
"⚠️ Job 1 has no 'parameters'; skipping.\n",
"⚠️ Job 2 has no 'parameters'; skipping.\n",
"Jobs received: [{'status': 'todo', 'updated_at': None, 'created_at': '2025-04-29T07:40:46.419291', 'id': 1, 'experiment_parameters_id': 1}, {'status': 'todo', 'updated_at': None, 'created_at': '2025-04-29T07:41:44.451862', 'id': 2, 'experiment_parameters_id': 2}]\n",
"⚠️ Job 1 has no 'parameters'; skipping.\n",
"⚠️ Job 2 has no 'parameters'; skipping.\n"
]
}
],
"execution_count": null

File diff suppressed because one or more lines are too long