alab_management.sample_view.analysis_view module#
Analysis results and analysis claims on samples: the one implementation behind the Data API.
This is the in-process half of decision D7/D9 in alab_one’s docs/data-architecture.md: an offline analysis
service consumes artifacts and writes its results back through AlabOS onto the sample, and the HTTP routes in
alab_management/dashboard/routes/data_api.py (/api/data/analysis/...) are only an adapter over this
class, so an in-process AlabOS caller and a remote client go through the same rules.
What “one write path” does and does not mean here, spelled out because the distinction is exactly what a reviewer should check:
This class never builds a Mongo client of its own. Every write to the live database uses the collection handle of the
SampleViewthe caller hands in.The unconditional result write (no
owner) goes throughSampleView.update_sample_metadata.The claim-conditional result write, and every claim operation, issue their own
update_oneon that same collection handle, becauseupdate_sample_metadatais unconditional and cannot express “only if I still hold the claim”. They stamplast_updatedthe same way it does._mirror_to_completedwrites the completed database throughCompletedSampleView, which is a second collection the caller did not hand in, and is the one place this class reaches past the givenSampleView.
Three operations, matching what an analysis worker needs each cycle:
pending_sampleswhich samples have input data for a technique but no result matching my configclaim/releaseso two workers never analyze the same sample (a crashed worker’s claim expires on its own)write_resultstoremetadata.analysis.<kind>additively, optionally only while still holding the claim
Both documents live under metadata, side by side and never nested in each other:
metadata.analysis.<kind> = {...the result...}
metadata.analysis_claims.<kind> = {claimed_by, claimed_at, expires_at, ttl_s}
- class AnalysisView(sample_view=None)[source]#
Bases:
objectRead and write the analysis blocks of samples through an existing
SampleView.- claim(sample_id, kind, owner, ttl_s=7200)[source]#
Take (or renew) this kind’s claim on one sample;
Nonewhen another live worker holds it.Renewing is the same call from the same owner, so a heartbeat is just a repeat: there is no separate renew operation to get wrong, and a claim nobody renews expires at
expires_at. RaisesSampleNotFoundErrorwhen there is no such sample, which is a different thing from losing the race.- Return type:
dict[str,Any] |None
- get_claim(sample_id, kind)[source]#
The claim document currently stored for this kind, if any.
- Return type:
dict[str,Any] |None
- pending_samples(kind, data_source, params_hash, limit=50, owner=None, include_errors=False, include_claimed=False)[source]#
Samples that carry
data_source’s input data but no result whoseparams_hashis the caller’s.params_hashis the caller’s own identifier for its analysis configuration; AlabOS only compares it for equality with what is stored, and never interprets it. A stored block withoutparams_hash(a hand-written result) counts as fresh and is left alone. Errors are skipped unlessinclude_errors, because when to retry an error is the caller’s policy, not AlabOS’s.- Return type:
list[dict[str,Any]]
- release(sample_id, kind, owner)[source]#
Drop this kind’s claim if
ownerstill holds it. False when it was already taken over.- Return type:
bool
- write_result(sample_id, kind, result, owner=None, release_claim=True, mirror_completed=True)[source]#
Store
metadata.analysis.<kind>on one sample.Returns
{"written": ..., "mirrored": ...}.writtenis False only whenownerwas given and that owner no longer holds the claim;mirroredsays whether the same key was also set on this sample’s document in the completed database.SampleNotFoundErroris raised when there is no such sample, on both paths.The write is additive: it replaces that one key and touches no other metadata. Without
ownerit goes throughSampleView.update_sample_metadata, which turns{"analysis.<kind>": ...}into a$setof the field pathmetadata.analysis.<kind>.With
ownerthe update is conditional, and it is worth being precise about what it checks: it matches on the claim’s current holder, not onexpires_at. So a worker that overran its lease still stores its result when nobody took the sample over – deliberate, because a finished analysis should not be discarded just because a lease lapsed – while a worker whose claim was taken over, or whose successor has already released it, matches no claim and getswritten: False. The case this cannot defend against is a writer that passes noownerat all, since such a write opts out of claim arbitration by definition.- Return type:
dict[str,Any]
- exception SampleNotFoundError[source]#
Bases:
ValueErrorNo sample with the given id exists in the live database.
A subclass of
ValueErroron purpose, so in-process code written againstSampleView.update_sample_metadata– which raises a plainValueErrorfor the same situation – keeps working, while the HTTP adapter can still answer 404 instead of lumping it in with a validation error.
- check_kind(kind)[source]#
A
kindbecomes part of a Mongo field path, so only a plain identifier is accepted.- Return type:
str
- check_result_document(result)[source]#
The stored result must be a non-empty JSON object whose keys are safe as Mongo field names.
- Return type:
dict[str,Any]
- data_source_query(data_source)[source]#
The query that says “this sample carries the input data for that source”.
Built here from the source’s declared
field(and optional BSONtype), so a config file cannot inject a filter: the worst a bad entry can do is name a field that matches nothing.- Return type:
dict[str,Any]
- input_data_sources()[source]#
The configured sources, keyed by name, read fresh so a config edit needs no restart.
Returns
{}when the section is absent, which is the default: a deployment that has not declared where its patterns live gets nopendingquery rather than a guess.- Return type:
dict[str,dict[str,Any]]