From 707c98c5ce3296bdaac25570806b4ba29741ff6a Mon Sep 17 00:00:00 2001 From: GotthardG <51994228+GotthardG@users.noreply.github.com> Date: Fri, 9 May 2025 13:51:01 +0200 Subject: [PATCH] Add validations and logging for puck beamtime assignment. Introduced checks to prevent reassigning beamtime if puck samples have recorded events. Updated logging in beamline-related methods to provide more insight. Simplified data structure updates for dewars, pucks, and samples, ensuring consistency with beamtime assignments. --- backend/app/data/data.py | 13 ++- backend/app/routers/dewar.py | 38 ++++--- backend/app/routers/puck.py | 36 ++++--- backend/main.py | 4 +- frontend/src/App.tsx | 27 ++++- frontend/src/components/Calendar.tsx | 112 ++++++++++++------- frontend/src/components/RunDetails.tsx | 143 ++++++++++++++++++++----- frontend/src/pages/PlanningView.tsx | 14 ++- testfunctions.ipynb | 30 +++--- 9 files changed, 303 insertions(+), 114 deletions(-) diff --git a/backend/app/data/data.py b/backend/app/data/data.py index 70b771f..8048362 100644 --- a/backend/app/data/data.py +++ b/backend/app/data/data.py @@ -709,9 +709,18 @@ dewar_to_beamtime = { for dewar in dewars # Or use actual beamtime ids } -# Update dewars and their pucks with consistent beamtime for dewar in dewars: - dewar.beamtime_id = dewar_to_beamtime[dewar.id] + assigned_beamtime_obj = next( + b for b in beamtimes if b.id == dewar_to_beamtime[dewar.id] + ) + dewar.beamtimes = [assigned_beamtime_obj] + +for puck in pucks: + assigned_beamtime_obj = next( + b for b in beamtimes if b.id == dewar_to_beamtime[puck.dewar_id] + ) + puck.beamtimes = [assigned_beamtime_obj] + for puck in pucks: dewar_id = puck.dewar_id # Assuming puck has dewar_id diff --git a/backend/app/routers/dewar.py b/backend/app/routers/dewar.py index d1dfc98..0c7c82b 100644 --- a/backend/app/routers/dewar.py +++ b/backend/app/routers/dewar.py @@ -51,6 +51,8 @@ from app.crud import ( ) from app.routers.auth import get_current_user +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) dewar_router = APIRouter() @@ -599,6 +601,19 @@ async def assign_beamtime_to_dewar( if not dewar: raise HTTPException(status_code=404, detail="Dewar not found") + # Check if any sample (in any puck on this dewar) has sample events + for puck in dewar.pucks: + for sample in puck.samples: + sample_event_exists = ( + db.query(SampleEvent).filter(SampleEvent.sample_id == sample.id).first() + ) + if sample_event_exists: + raise HTTPException( + status_code=400, + detail="Cannot change beamtime:" + "at least one sample has events recorded.", + ) + # Find the Beamtime instance, if not unassigning beamtime = ( db.query(BeamtimeModel).filter(BeamtimeModel.id == beamtime_id).first() @@ -609,9 +624,7 @@ async def assign_beamtime_to_dewar( if beamtime_id == 0: dewar.beamtimes = [] else: - dewar.beamtimes = [ - beamtime - ] # assign one; append if you want to support multiple + dewar.beamtimes = [beamtime] db.commit() db.refresh(dewar) @@ -621,15 +634,11 @@ async def assign_beamtime_to_dewar( else: puck.beamtimes = [beamtime] for sample in puck.samples: - has_sample_event = ( - db.query(SampleEvent).filter(SampleEvent.sample_id == sample.id).count() - > 0 - ) - if not has_sample_event: - if beamtime_id == 0: - sample.beamtimes = [] - else: - sample.beamtimes = [beamtime] + # Can assume all have no events because of previous check + if beamtime_id == 0: + sample.beamtimes = [] + else: + sample.beamtimes = [beamtime] db.commit() return {"status": "success", "dewar_id": dewar.id, "beamtime_id": beamtime_id} @@ -746,6 +755,7 @@ async def get_single_shipment(id: int, db: Session = Depends(get_db)): operation_id="get_dewars_by_beamtime", ) async def get_dewars_by_beamtime(beamtime_id: int, db: Session = Depends(get_db)): + logger.info(f"get_dewars_by_beamtime called with beamtime_id={beamtime_id}") beamtime = ( db.query(BeamtimeModel) .options(joinedload(BeamtimeModel.dewars)) @@ -753,5 +763,9 @@ async def get_dewars_by_beamtime(beamtime_id: int, db: Session = Depends(get_db) .first() ) if not beamtime: + logger.warning(f"Beamtime {beamtime_id} not found") raise HTTPException(status_code=404, detail="Beamtime not found") + logger.info( + f"Returning {len(beamtime.dewars)} dewars: {[d.id for d in beamtime.dewars]}" + ) return beamtime.dewars diff --git a/backend/app/routers/puck.py b/backend/app/routers/puck.py index b53ae98..11c863e 100644 --- a/backend/app/routers/puck.py +++ b/backend/app/routers/puck.py @@ -665,13 +665,25 @@ async def get_pucks_by_slot(slot_identifier: str, db: Session = Depends(get_db)) @router.patch("/puck/{puck_id}/assign-beamtime", operation_id="assignPuckToBeamtime") async def assign_beamtime_to_puck( puck_id: int, - beamtime_id: int, # expects ?beamtime_id=123 in the query + beamtime_id: int, db: Session = Depends(get_db), ): puck = db.query(PuckModel).filter(PuckModel.id == puck_id).first() if not puck: raise HTTPException(status_code=404, detail="Puck not found") + # Check if any sample in this puck has sample events + for sample in puck.samples: + sample_event_exists = ( + db.query(SampleEvent).filter(SampleEvent.sample_id == sample.id).first() + ) + if sample_event_exists: + raise HTTPException( + status_code=400, + detail="Cannot change beamtime:" + "at least one sample has events recorded.", + ) + beamtime = ( db.query(BeamtimeModel).filter(BeamtimeModel.id == beamtime_id).first() if beamtime_id @@ -681,22 +693,15 @@ async def assign_beamtime_to_puck( if beamtime_id == 0: puck.beamtimes = [] else: - puck.beamtimes = [ - beamtime - ] # or use .append(beamtime) if you want to support multiple + puck.beamtimes = [beamtime] db.commit() db.refresh(puck) - # Update samples as well for sample in puck.samples: - has_sample_event = ( - db.query(SampleEvent).filter(SampleEvent.sample_id == sample.id).count() > 0 - ) - if not has_sample_event: - if beamtime_id == 0: - sample.beamtimes = [] - else: - sample.beamtimes = [beamtime] + if beamtime_id == 0: + sample.beamtimes = [] + else: + sample.beamtimes = [beamtime] db.commit() return {"status": "success", "puck_id": puck.id, "beamtime_id": beamtime_id} @@ -707,6 +712,7 @@ async def assign_beamtime_to_puck( operation_id="get_pucks_by_beamtime", ) async def get_pucks_by_beamtime(beamtime_id: int, db: Session = Depends(get_db)): + logger.info(f"get_pucks_by_beamtime called with beamtime_id={beamtime_id}") beamtime = ( db.query(BeamtimeModel) .options(joinedload(BeamtimeModel.pucks)) # eager load pucks @@ -714,5 +720,9 @@ async def get_pucks_by_beamtime(beamtime_id: int, db: Session = Depends(get_db)) .first() ) if not beamtime: + logger.warning(f"Beamtime {beamtime_id} not found") raise HTTPException(status_code=404, detail="Beamtime not found") + logger.info( + f"Returning {len(beamtime.pucks)} pucks: {[p.id for p in beamtime.pucks]}" + ) return beamtime.pucks diff --git a/backend/main.py b/backend/main.py index 328c368..caf1b7d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -168,8 +168,8 @@ async def lifespan(app: FastAPI): load_slots_data(db) else: # dev or test environments print(f"{environment.capitalize()} environment: Regenerating database.") - # 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 # from app.models import ExperimentParameters # adjust the import as needed # inspector = reflection.Inspector.from_engine(engine) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 443bea2..681d102 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -84,8 +84,31 @@ const App: React.FC = () => { } /> } />} /> - } />} /> - } />} /> + + } + /> + } + /> + + } + /> + } + /> { @@ -34,7 +42,7 @@ const darkenColor = (color: string, percent: number): string => { return `#${newColor}`; }; -const Calendar: React.FC = () => { +const Calendar = ({ activePgroup }: CalendarProps) => { const [events, setEvents] = useState([]); const [isLoading, setIsLoading] = useState(false); const [fetchError, setFetchError] = useState(null); @@ -59,6 +67,7 @@ const Calendar: React.FC = () => { setFetchError(null); try { const beamtimes = await BeamtimesService.getMyBeamtimesProtectedBeamtimesMyBeamtimesGet(); + console.log('Loaded beamtimes:', beamtimes); const grouped: { [key: string]: any[] } = {}; beamtimes.forEach((beamtime: any) => { const key = `${beamtime.start_date}|${beamtime.beamline}|${beamtime.pgroups}`; @@ -70,8 +79,9 @@ const Calendar: React.FC = () => { const shifts = group.map((bt: any) => bt.shift).join(" + "); const ids = group.map((bt: any) => bt.id); const first = group[0]; + console.log(`[DEBUG] pgroups: ${first.pgroups}`); // Ensure the value of pgroups here is correct return { - id: `${first.beamline}-${first.start_date}-${first.pgroups}`, // ensure uniqueness + id: `${first.beamline}-${first.start_date}-${first.pgroups}`, title: `${first.beamline}: ${shifts}`, start: first.start_date, end: first.end_date, @@ -82,6 +92,9 @@ const Calendar: React.FC = () => { borderColor: '#000', textColor: '#fff', beamtimes: group, + extendedProps: { + pgroups: first.pgroups, // Check that this is a valid, comma-separated string + }, }; }); setEvents(formattedEvents); @@ -89,6 +102,7 @@ const Calendar: React.FC = () => { // Fetch associations for all const assoc: { [id: string]: { dewars: string[]; pucks: string[] } } = {}; + console.log('Fetched associations after loading events:', assoc); await Promise.all( Object.values(grouped).map(async (group) => { @@ -103,6 +117,8 @@ const Calendar: React.FC = () => { DewarsService.getDewarsByBeamtime(beamtimeId), PucksService.getPucksByBeamtime(beamtimeId), ]); + console.log(`Dewars for beamtime ${beamtimeId}:`, dewars); + console.log(`Pucks for beamtime ${beamtimeId}:`, pucks); dewars.forEach((d: any) => dewarsSet.add(d.id)); pucks.forEach((p: any) => pucksSet.add(p.id)); }) @@ -115,8 +131,10 @@ const Calendar: React.FC = () => { }; }) ); + console.log("Final eventAssociations:", assoc); setEventAssociations(assoc); + } catch (error) { setFetchError('Failed to load beamtime data. Please try again later.'); setEvents([]); @@ -250,39 +268,38 @@ const Calendar: React.FC = () => { ? eventInfo.event.extendedProps.beamtimes.length : 1; const minHeight = beamtimesInGroup * 26; - const beamline = eventInfo.event.extendedProps.beamline || 'Unknown'; - const isSelected = selectedEventId === eventInfo.event.id; - const isSubmitted = eventInfo.event.extendedProps.isSubmitted; - const assoc = eventAssociations[eventInfo.event.id] || { dewars: [], pucks: [] }; - const backgroundColor = isSubmitted - ? darkenColor(beamlineColors[beamline] || beamlineColors.Unknown, -20) - : isSelected - ? '#FFD700' - : (beamlineColors[beamline] || beamlineColors.Unknown); - - return ( -
+ const beamline = eventInfo.event.extendedProps.beamline || 'Unknown'; + const isSelected = selectedEventId === eventInfo.event.id; + const isSubmitted = eventInfo.event.extendedProps.isSubmitted; + const assoc = eventAssociations[eventInfo.event.id] || { dewars: [], pucks: [] }; + const backgroundColor = isSubmitted + ? darkenColor(beamlineColors[beamline] || beamlineColors.Unknown, -20) + : isSelected + ? '#FFD700' + : (beamlineColors[beamline] || beamlineColors.Unknown); + return ( +
{eventInfo.event.title} - + 🧊 { textAlign: 'center' }}>{assoc.pucks.length} + {eventInfo.event.extendedProps?.pgroups && eventInfo.event.extendedProps.pgroups.split(',') + .map((pgroup: string) => ( + + )) + } -
- ); -}; - - - // Used in Dewar/Puck assign status reporting +
+ ); + }; function getAssignedEventForDewar(dewarId: string) { return Object.entries(eventAssociations).find(([eid, assoc]) => assoc.dewars.includes(dewarId) diff --git a/frontend/src/components/RunDetails.tsx b/frontend/src/components/RunDetails.tsx index 7c0073e..688f15b 100644 --- a/frontend/src/components/RunDetails.tsx +++ b/frontend/src/components/RunDetails.tsx @@ -39,13 +39,13 @@ interface ProcessingResults { resolution: number; unit_cell: string; spacegroup: string; - rmerge: number; - rmeas: number; - isig: number; + rmerge: CCPoint[]; + rmeas: CCPoint[]; + isig: CCPoint[]; cc: CCPoint[]; cchalf: CCPoint[]; - completeness: number; - multiplicity: number; + completeness: CCPoint[]; + multiplicity: CCPoint[]; nobs: number; total_refl: number; unique_refl: number; @@ -81,13 +81,13 @@ const RunDetails: React.FC = ({ run, onHeightChange, basePath, resolution: res.result?.resolution ?? 0, unit_cell: res.result?.unit_cell || 'N/A', spacegroup: res.result?.spacegroup || 'N/A', - rmerge: res.result?.rmerge ?? 0, - rmeas: res.result?.rmeas ?? 0, - isig: res.result?.isig ?? 0, + rmerge: res.result?.rmerge || [], + rmeas: res.result?.rmeas || [], + isig: res.result?.isig || [], cc: res.result?.cc || [], cchalf: res.result?.cchalf || [], - completeness: res.result?.completeness ?? 0, - multiplicity: res.result?.multiplicity ?? 0, + completeness: res.result?.completeness || [], + multiplicity: res.result?.multiplicity || [], nobs: res.result?.nobs ?? 0, total_refl: res.result?.total_refl ?? 0, unique_refl: res.result?.unique_refl ?? 0, @@ -111,15 +111,45 @@ const RunDetails: React.FC = ({ run, onHeightChange, basePath, { field: 'resolution', headerName: 'Resolution (Ã…)', flex: 1 }, { field: 'unit_cell', headerName: 'Unit Cell (Ã…)', flex: 1.5 }, { field: 'spacegroup', headerName: 'Spacegroup', flex: 1 }, - { field: 'rmerge', headerName: 'Rmerge', flex: 1 }, - { field: 'rmeas', headerName: 'Rmeas', flex: 1 }, - { field: 'isig', headerName: 'I/sig(I)', flex: 1 }, + { + field: 'rmerge', + headerName: 'Rmerge', + flex: 1, + valueGetter: (params: GridValueGetterParams) => + params.row?.rmerge + ? Array.isArray(params.row.rmerge) + ? params.row.rmerge.map((value: CCPoint) => `${value.value.toFixed(2)}@${value.resolution.toFixed(2)}`).join(', ') + : params.row.rmerge.toFixed(2) + : 'N/A', + }, + { + field: 'rmeas', + headerName: 'Rmeas', + flex: 1, + valueGetter: (params: GridValueGetterParams) => + params.row?.rmeas + ? Array.isArray(params.row.rmeas) + ? params.row.rmeas.map((value: CCPoint) => `${value.value.toFixed(2)}@${value.resolution.toFixed(2)}`).join(', ') + : params.row.rmeas.toFixed(2) + : 'N/A', + }, + { + field: 'isig', + headerName: 'I/sig(I)', + flex: 1, + valueGetter: (params: GridValueGetterParams) => + params.row?.isig + ? Array.isArray(params.row.isig) + ? params.row.isig.map((value: CCPoint) => `${value.value.toFixed(2)}@${value.resolution.toFixed(2)}`).join(', ') + : params.row.isig.toFixed(2) + : 'N/A', + }, { field: 'cc', headerName: 'CC', flex: 1, valueGetter: (params: GridValueGetterParams) => - Array.isArray(params.row?.cc) + params.row?.cc && Array.isArray(params.row.cc) ? params.row.cc.map((point: CCPoint) => `${point.value.toFixed(2)}@${point.resolution.toFixed(2)}`).join(', ') : '', }, @@ -128,18 +158,39 @@ const RunDetails: React.FC = ({ run, onHeightChange, basePath, headerName: 'CC(1/2)', flex: 1, valueGetter: (params: GridValueGetterParams) => - Array.isArray(params.row?.cchalf) + params.row?.cchalf && Array.isArray(params.row.cchalf) ? params.row.cchalf.map((point: CCPoint) => `${point.value.toFixed(2)}@${point.resolution.toFixed(2)}`).join(', ') : '', }, - { field: 'completeness', headerName: 'Completeness (%)', flex: 1 }, - { field: 'multiplicity', headerName: 'Multiplicity', flex: 1 }, + { + field: 'completeness', + headerName: 'Completeness (%)', + flex: 1, + valueGetter: (params: GridValueGetterParams) => + params.row?.completeness + ? Array.isArray(params.row.completeness) + ? params.row.completeness.map((value: CCPoint) => `${value.value.toFixed(2)}@${value.resolution.toFixed(2)}`).join(', ') + : params.row.completeness.toFixed(2) + : 'N/A', + }, + { + field: 'multiplicity', + headerName: 'Multiplicity', + flex: 1, + valueGetter: (params: GridValueGetterParams) => + params.row?.multiplicity + ? Array.isArray(params.row.multiplicity) + ? params.row.multiplicity.map((value: CCPoint) => `${value.value.toFixed(2)}@${value.resolution.toFixed(2)}`).join(', ') + : params.row.multiplicity.toFixed(2) + : 'N/A', + }, { field: 'nobs', headerName: 'N obs.', flex: 1 }, { field: 'total_refl', headerName: 'Total Reflections', flex: 1 }, { field: 'unique_refl', headerName: 'Unique Reflections', flex: 1 }, { field: 'comments', headerName: 'Comments', flex: 2 }, ]; + const updateHeight = () => { if (containerRef.current) { const newHeight = containerRef.current.offsetHeight; @@ -311,31 +362,71 @@ const RunDetails: React.FC = ({ run, onHeightChange, basePath, {processingResult && processingResult.length > 0 && ( -
- CC and CC(1/2) vs Resolution +
+ Processing Metrics vs Resolution point.resolution) // Grab the resolution values - .reverse(), // Reverse the data for resolution + .map((point) => point.resolution) // Use resolution values for the x-axis + .reverse(), // Reverse the resolution values to go from high-res to low-res label: 'Resolution (Ã…)', - reverse: true, // This ensures the visual flip on the chart, low-res to right and high-res to left + reverse: true, // Flip visually so low-res is to the right }, ]} series={[ { data: processingResult[0].cc - .map((point) => point.value) - .reverse(), // Reverse the CC values to match the reversed resolution + .map((point) => point.value) // Map CC values + .reverse(), // Reverse order for visual consistency label: 'CC', }, { data: processingResult[0].cchalf - .map((point) => point.value) - .reverse(), // Reverse the CC(1/2) values to match the reversed resolution + .map((point) => point.value) // Map CC(1/2) values + .reverse(), label: 'CC(1/2)', }, + { + data: Array.isArray(processingResult[0].rmerge) + ? processingResult[0].rmerge + .map((point: CCPoint) => point.value) // Map Rmerge values + .reverse() + : [], // Handle edge case where Rmerge isn't an array + label: 'Rmerge', + }, + { + data: Array.isArray(processingResult[0].rmeas) + ? processingResult[0].rmeas + .map((point: CCPoint) => point.value) // Map Rmeas values + .reverse() + : [], + label: 'Rmeas', + }, + { + data: Array.isArray(processingResult[0].isig) + ? processingResult[0].isig + .map((point: CCPoint) => point.value) // Map I/sig(I) values + .reverse() + : [], + label: 'I/sig(I)', + }, + { + data: Array.isArray(processingResult[0].completeness) + ? processingResult[0].completeness + .map((point: CCPoint) => point.value) // Map Completeness values + .reverse() + : [], + label: 'Completeness (%)', + }, + { + data: Array.isArray(processingResult[0].multiplicity) + ? processingResult[0].multiplicity + .map((point: CCPoint) => point.value) // Map Multiplicity values + .reverse() + : [], + label: 'Multiplicity', + }, ]} height={300} /> diff --git a/frontend/src/pages/PlanningView.tsx b/frontend/src/pages/PlanningView.tsx index f1ea5d8..7565bb7 100644 --- a/frontend/src/pages/PlanningView.tsx +++ b/frontend/src/pages/PlanningView.tsx @@ -1,10 +1,16 @@ -// Planning.tsx import React from 'react'; import CustomCalendar from '../components/Calendar.tsx'; -const PlanningView: React.FC = () => { - return ; - //return
Welcome to the Planning Page
; +interface PlanningViewProps { + onPgroupChange?: (pgroup: string) => void; + activePgroup: string; +} + +const PlanningView: React.FC = ({ onPgroupChange, activePgroup }) => { + return ; }; export default PlanningView; \ No newline at end of file diff --git a/testfunctions.ipynb b/testfunctions.ipynb index 73fe46f..8e96946 100644 --- a/testfunctions.ipynb +++ b/testfunctions.ipynb @@ -446,21 +446,21 @@ { "metadata": { "ExecuteTime": { - "end_time": "2025-05-08T13:31:36.929465Z", - "start_time": "2025-05-08T13:31:36.925054Z" + "end_time": "2025-05-08T15:17:09.428353Z", + "start_time": "2025-05-08T15:17:09.424769Z" } }, "cell_type": "code", - "source": "sample_id = 44", + "source": "sample_id = 106", "id": "54d4d46ca558e7b9", "outputs": [], - "execution_count": 28 + "execution_count": 36 }, { "metadata": { "ExecuteTime": { - "end_time": "2025-05-08T13:31:40.023546Z", - "start_time": "2025-05-08T13:31:39.978510Z" + "end_time": "2025-05-08T15:17:16.752451Z", + "start_time": "2025-05-08T15:17:16.719047Z" } }, "cell_type": "code", @@ -478,7 +478,7 @@ " sample_event_create = SampleEventCreate(\n", " sample_id=sample_id,\n", " #event_type=\"Centering\" # Valid event type\n", - " event_type=\"Collecting\" # Valid event type\n", + " event_type=\"Mounting\" # Valid event type\n", " )\n", "\n", " # Debug the payload before sending\n", @@ -512,7 +512,7 @@ "DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): localhost:8000\n", "/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 'localhost'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings\n", " warnings.warn(\n", - "DEBUG:urllib3.connectionpool:https://localhost:8000 \"POST /samples/samples/44/events HTTP/1.1\" 200 884\n" + "DEBUG:urllib3.connectionpool:https://localhost:8000 \"POST /samples/samples/106/events HTTP/1.1\" 200 718\n" ] }, { @@ -520,25 +520,25 @@ "output_type": "stream", "text": [ "Payload being sent to API:\n", - "{\"event_type\":\"Collecting\"}\n", + "{\"event_type\":\"Mounting\"}\n", "API response:\n", - "('id', 44)\n", - "('sample_name', 'Sample044')\n", - "('position', 7)\n", - "('puck_id', 7)\n", + "('id', 106)\n", + "('sample_name', 'Sample106')\n", + "('position', 13)\n", + "('puck_id', 11)\n", "('crystalname', None)\n", "('proteinname', None)\n", "('positioninpuck', None)\n", "('priority', None)\n", "('comments', None)\n", "('data_collection_parameters', None)\n", - "('events', [SampleEventResponse(event_type='Mounting', id=87, sample_id=44, timestamp=datetime.datetime(2025, 5, 7, 10, 16)), SampleEventResponse(event_type='Unmounting', id=88, sample_id=44, timestamp=datetime.datetime(2025, 5, 7, 10, 16, 50)), SampleEventResponse(event_type='Collecting', id=507, sample_id=44, timestamp=datetime.datetime(2025, 5, 8, 13, 31, 40, 6059))])\n", + "('events', [SampleEventResponse(event_type='Mounting', id=452, sample_id=106, timestamp=datetime.datetime(2025, 5, 8, 15, 17, 16, 743011))])\n", "('mount_count', 0)\n", "('unmount_count', 0)\n" ] } ], - "execution_count": 29 + "execution_count": 37 }, { "metadata": {