Skip to content

Commit e0b3b67

Browse files
Annotation Manifest For Each Analysis Omic Unit (#213)
* first pass to restructure the analyses' annotation manifest to be per omic unit annotated. Changes are stable and will continue to see if things continue to work when revising the testing * changelog update * first pass for large scale migration script * Simplified logging messages for annotations;
1 parent b394959 commit e0b3b67

16 files changed

Lines changed: 2969 additions & 2052 deletions

β€ŽCHANGELOG.mdβ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
### Features
1111

1212
- The annotations page can select genes & variant of an analysis to view using the page URL's query parameters
13+
- Annotation manifests for analyses are now organized by omic unit, allowing more granular support for versioning of
14+
annotations.
1315

1416
### Bugs
1517

β€Žbackend/src/core/annotation.pyβ€Ž

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ def to_name_string(annotation_unit: AnnotationUnit):
2323
"""
2424
Returns the annotation unit's genomic_unit and corresponding dataset.
2525
"""
26-
return f"{annotation_unit.get_genomic_unit().ljust(30)}{annotation_unit.get_dataset_name().ljust(60)}"
26+
if not annotation_unit.version_calculated():
27+
return f"{annotation_unit.get_genomic_unit().ljust(30)}{annotation_unit.get_dataset_name().ljust(105)}"
28+
29+
name_and_version = f"{annotation_unit.get_genomic_unit().ljust(30)}{annotation_unit.get_dataset_name().ljust(45)}"
30+
31+
return f"{name_and_version}{annotation_unit.get_dataset_source().ljust(30)}{str(annotation_unit.version).ljust(30)}"
2732

2833

2934
def annotation_log_label():
@@ -34,21 +39,24 @@ def annotation_log_label():
3439
return 'Annotation'.ljust(15)
3540

3641

37-
def format_annotation_logging(annotation_unit: AnnotationUnit, dataset=""):
42+
def format_annotation_logging(annotation_unit: AnnotationUnit, note=""):
3843
"""
3944
Provides a formatted string for logging that is consistent with
4045
annotation unit's genomic_unit and corresponding dataset to the console
4146
The string is padded to make the logs uniform and easier to read.
4247
"""
43-
if dataset != "":
44-
annotation_unit_string = f"{annotation_unit.get_genomic_unit().ljust(30)}{dataset.ljust(60)}"
45-
else:
46-
annotation_unit_string = to_name_string(annotation_unit)
48+
annotation_unit_log_string = ""
4749

4850
if "" != annotation_unit.analysis_name:
49-
annotation_unit_string += f"'{annotation_unit.analysis_name}'".ljust(20)
51+
annotation_unit_log_string += f"'{annotation_unit.analysis_name}'".ljust(20)
52+
53+
if annotation_unit is not None:
54+
annotation_unit_log_string += f"{to_name_string(annotation_unit)}"
55+
56+
if note != "":
57+
annotation_unit_log_string += f"{note.ljust(60)}"
5058

51-
return f"{annotation_log_label()}{annotation_unit_string}"
59+
return f"{annotation_log_label()}{annotation_unit_log_string}"
5260

5361

5462
class AnnotationQueue:
@@ -104,7 +112,7 @@ def process_tasks(
104112
analysis_collection: AnalysisCollection
105113
):
106114
"""Processes items that have been added to the queue"""
107-
logger.info("%s Processing annotation tasks queue ...", annotation_log_label())
115+
logger.info("%s Processing annotation tasks queue...", annotation_log_label())
108116

109117
processor = AnnotationProcess(annotation_queue, genomic_unit_collection, analysis_collection)
110118

@@ -167,12 +175,31 @@ def process_annotation_unit(self, annotation_unit: AnnotationUnit):
167175
annotation task can be created. If the annotation unit is ready to annotate, it will be submitted to run
168176
on the task execeutor thread pool.
169177
"""
170-
if not annotation_unit.version_exists():
178+
179+
possible_manifest_entry = self.retrieve_manifest_entry_if_exist(annotation_unit)
180+
if possible_manifest_entry is not None and not annotation_unit.version_calculated():
181+
manifest_annotation_unit = self._create_temporary_annotation_unit(
182+
annotation_unit.genomic_unit, possible_manifest_entry, annotation_unit.analysis_name,
183+
annotation_unit.is_transcript_dataset()
184+
)
185+
if not self.genomic_unit_collection.annotation_exist(manifest_annotation_unit):
186+
logger.error(
187+
'%s Manifest Annotation Does Not Exist...', format_annotation_logging(manifest_annotation_unit)
188+
)
189+
logger.error(
190+
'%s Remove Manifest Entry Manually...', format_annotation_logging(manifest_annotation_unit)
191+
)
192+
else:
193+
logger.info('%s Manifest Annotation Exists...', format_annotation_logging(manifest_annotation_unit))
194+
return
195+
196+
if not annotation_unit.version_calculated():
171197
self.handle_annotation_unit_version_calcuation(annotation_unit)
172198
return
173199

174200
if self.genomic_unit_collection.annotation_exist(annotation_unit):
175201
logger.info('%s Annotation Exists...', format_annotation_logging(annotation_unit))
202+
self.analysis_collection.add_dataset_to_manifest(annotation_unit.analysis_name, annotation_unit)
176203
return
177204

178205
if annotation_unit.has_dependencies():
@@ -201,6 +228,7 @@ def on_task_complete(self, future):
201228
"""
202229
task = self.annotation_task_futures[future]
203230
annotation_unit = task.annotation_unit
231+
logger.info('%s Task Executed...', format_annotation_logging(annotation_unit))
204232

205233
try:
206234
task_process_result = future.result()
@@ -216,14 +244,11 @@ def on_task_complete(self, future):
216244
self.queue.put(annotation_unit)
217245
else:
218246
for annotation in task.extract(task_process_result):
219-
logger.info(
220-
'%s Saving %s...',
221-
format_annotation_logging(annotation_unit,
222-
annotation_unit.get_dataset_name()), annotation['value']
223-
)
247+
logger.info('%s Saving %s...', format_annotation_logging(annotation_unit), annotation['value'])
224248

225249
self.genomic_unit_collection.annotate_genomic_unit(annotation_unit.genomic_unit, annotation)
226250
self.analysis_collection.add_dataset_to_manifest(annotation_unit.analysis_name, annotation_unit)
251+
logger.info('%s Complete...', format_annotation_logging(annotation_unit))
227252

228253
except FileNotFoundError as error:
229254
logger.error('%s Exception [%s] Not Found [%s]', format_annotation_logging(annotation_unit), error, task)
@@ -275,6 +300,12 @@ def set_version_in_cache(self, version_cache_id: str, version):
275300
"""Sets the version to be cached in the version cache"""
276301
self.version_cache[version_cache_id] = version
277302

303+
def retrieve_manifest_entry_if_exist(self, annotation_unit: AnnotationUnit):
304+
"""Queries the collection for an annotation unit's manifest entry. If none exists None is returned."""
305+
return self.analysis_collection.get_manifest_dataset_config(
306+
annotation_unit.analysis_name, annotation_unit.get_genomic_unit(), annotation_unit.get_dataset_name()
307+
)
308+
278309
def handle_annotation_unit_version_calcuation(self, annotation_unit):
279310
"""
280311
Processes the annotation unit to derive the annotation unit's calculated version according to the configuration.
@@ -284,25 +315,24 @@ def handle_annotation_unit_version_calcuation(self, annotation_unit):
284315
version_cache_id = version_task.get_version_cache_id()
285316

286317
if not self.is_version_cache_setup(version_cache_id):
287-
logger.info('%s Creating Task To Version...', format_annotation_logging(annotation_unit))
318+
logger.info('%s Creating Calculate Version Task...', format_annotation_logging(annotation_unit))
288319
self.setup_version_cache(version_cache_id)
289320
self.queue_task_in_tasks_worker(version_task)
290321
return
291322

292323
if self.is_version_cached(version_cache_id):
293324
cached_version = self.version_cache[version_cache_id]
294325
annotation_unit.set_latest_version(cached_version)
295-
logger.info(
296-
'%s Version Gathered from Cache %s...', format_annotation_logging(annotation_unit), cached_version
297-
)
326+
logger.info('%s Version From Cache %s...', format_annotation_logging(annotation_unit), cached_version)
298327
self.queue.put(annotation_unit)
299328

300329
def handle_annotation_unit_dependencies(self, annotation_unit: AnnotationUnit):
301330
"""Retrieves the an annotation unit's dependencies if they exist."""
331+
302332
missing_dependencies = annotation_unit.get_missing_dependencies()
303333
for missing_dataset_name in missing_dependencies:
304334
analysis_manifest_dataset = self.analysis_collection.get_manifest_dataset_config(
305-
annotation_unit.analysis_name, missing_dataset_name
335+
annotation_unit.analysis_name, annotation_unit.get_genomic_unit(), missing_dataset_name
306336
)
307337

308338
if analysis_manifest_dataset is None:
@@ -321,7 +351,7 @@ def handle_annotation_unit_dependencies(self, annotation_unit: AnnotationUnit):
321351

322352
if annotation_unit.if_transcript_needs_provisioning():
323353
transcript_id_manifest_dataset = self.analysis_collection.get_manifest_dataset_config(
324-
annotation_unit.analysis_name, "transcript_id"
354+
annotation_unit.analysis_name, annotation_unit.get_genomic_unit(), "transcript_id"
325355
)
326356

327357
if transcript_id_manifest_dataset is not None:
@@ -337,9 +367,14 @@ def handle_annotation_unit_dependencies(self, annotation_unit: AnnotationUnit):
337367
if transcript_id_dataset_saved:
338368
annotation_unit.set_transcript_provisioned(True)
339369

340-
def _create_temporary_annotation_unit(self, genomic_unit, manifest_dataset, analysis_name):
370+
def _create_temporary_annotation_unit(
371+
self, genomic_unit, manifest_dataset, analysis_name: str, transcript_dataset: bool = False
372+
):
341373
"""Private helper method to create a temporary annotation unit for finding within repository"""
342374
temporary = AnnotationUnit(genomic_unit, manifest_dataset, analysis_name)
343375
temporary.set_latest_version(manifest_dataset['version'])
344376

377+
if transcript_dataset:
378+
temporary.dataset['transcript'] = True
379+
345380
return temporary

β€Žbackend/src/core/annotation_unit.pyβ€Ž

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,14 @@ def should_continue_annotation(self):
124124
delay_count = self.dataset['delay_count'] + 1 if 'delay_count' in self.dataset else 0
125125
self.dataset['delay_count'] = delay_count
126126

127-
is_delay_count_exceeding = self.dataset['delay_count'] > 10
127+
is_delay_count_exceeding = self.dataset['delay_count'] > 20
128128

129129
return not is_delay_count_exceeding
130130

131131
def set_latest_version(self, version_details):
132132
"""Sets the Annotation Unit with the version"""
133133
self.version = version_details
134134

135-
def version_exists(self):
135+
def version_calculated(self):
136136
"""Checks if the Annotation Unit is versioned or not"""
137137
return self.version != ""

β€Žbackend/src/models/analysis.pyβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ class Analysis(BaseAnalysis):
9090
sections: List[Section] = []
9191
discussions: List = []
9292
attachments: List = []
93-
manifest: List = []
93+
manifest: object = {}
9494

9595
def units_to_annotate(self):
9696
"""Returns the types of genomic units within the analysis"""

β€Žbackend/src/repository/analysis_collection.pyβ€Ž

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -134,31 +134,51 @@ def get_genomic_units(self, analysis_name: str):
134134

135135
def add_dataset_to_manifest(self, analysis_name: str, annotation_unit: AnnotationUnit):
136136
"""Adds this dataset and its version to this Analysis."""
137-
138137
dataset = {
139138
annotation_unit.get_dataset_name(): {
140139
'data_source': annotation_unit.get_dataset_source(), 'version': annotation_unit.version
141140
}
142141
}
143142

144-
updated_document = self.collection.find_one_and_update({"name": analysis_name},
145-
{"$addToSet": {"manifest": dataset}},
143+
self.collection.find_one_and_update({
144+
"name": analysis_name, "manifest.unit": {"$ne": annotation_unit.get_genomic_unit()}
145+
}, {'$addToSet': {'manifest': {'unit': annotation_unit.get_genomic_unit(), 'manifest': []}}})
146+
147+
updated_document = self.collection.find_one_and_update({
148+
"name": analysis_name, "manifest.unit": annotation_unit.get_genomic_unit()
149+
}, {"$addToSet": {"manifest.$[entry].manifest": dataset}},
150+
array_filters=[{
151+
'entry.unit': annotation_unit.get_genomic_unit()
152+
}],
146153
return_document=ReturnDocument.AFTER)
147154

148155
return updated_document['manifest']
149156

150-
def get_manifest_dataset_config(self, analysis_name: str, dataset_name: str):
157+
def get_manifest_dataset_config(self, analysis_name: str, omic_unit: str, dataset_name: str):
151158
""" Returns an individual dataset manifest """
152-
dataset_attribute = f"manifest.{dataset_name}"
153159

154-
projection = {"manifest.$": 1}
155-
query = {"name": analysis_name, dataset_attribute: {'$exists': True}}
160+
dataset_field = f"manifest.{dataset_name}"
161+
162+
query = {
163+
"name": analysis_name, "manifest": {"$elemMatch": {"unit": omic_unit, dataset_field: {'$exists': True}}}
164+
}
165+
166+
projection = {"manifest.manifest": 1, "manifest.unit.$": 1}
167+
156168
analysis = self.collection.find_one(query, projection)
157169

158170
if not analysis:
159171
return None
160172

161-
manifest_entry = next((dataset for dataset in analysis['manifest'] if dataset_name in dataset), None)
173+
unit_manifest = next((
174+
unit_manifest for unit_manifest in analysis['manifest']
175+
if "unit" in unit_manifest and unit_manifest['unit'] == omic_unit
176+
), None)
177+
178+
if not unit_manifest:
179+
return None
180+
181+
manifest_entry = next((dataset for dataset in unit_manifest['manifest'] if dataset_name in dataset), None)
162182

163183
return {
164184
"data_set": dataset_name, "data_source": manifest_entry[dataset_name]['data_source'],

β€Žbackend/src/routers/analysis_annotation_router.pyβ€Ž

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def get_annotations_by_gene(analysis_name, gene, repositories=Depends(database))
2323
raise HTTPException(status_code=404, detail=f"Gene'{gene}' annotations not found.")
2424

2525
manifest = AnalysisDatasetManfiest(dataset_manifest)
26-
annotations = manifest.retrieve_annotations(genomic_unit_json['annotations'])
26+
annotations = manifest.retrieve_annotations(gene, genomic_unit_json['annotations'])
2727

2828
return annotations
2929

@@ -45,11 +45,11 @@ def get_annotations_by_hgvs_variant(analysis_name: str, variant: str, repositori
4545
raise HTTPException(status_code=404, detail=f"Variant'{variant}' annotations not found.")
4646

4747
manifest = AnalysisDatasetManfiest(dataset_manifest)
48-
annotations = manifest.retrieve_annotations(genomic_unit_json['annotations'])
48+
annotations = manifest.retrieve_annotations(variant, genomic_unit_json['annotations'])
4949

5050
transcript_annotation_list = []
5151
for transcript_annotation in genomic_unit_json['transcripts']:
52-
transcript_annotations = manifest.retrieve_annotations(transcript_annotation['annotations'])
52+
transcript_annotations = manifest.retrieve_annotations(variant, transcript_annotation['annotations'])
5353
transcript_annotation_list.append(transcript_annotations)
5454

5555
return {**annotations, "transcripts": transcript_annotation_list}
@@ -75,7 +75,7 @@ def __init__(self, analysis_dataset_manifest):
7575
"""
7676
self.manifest = analysis_dataset_manifest
7777

78-
def retrieve_annotations(self, unit_annotations):
78+
def retrieve_annotations(self, omic_unit: str, unit_annotations):
7979
"""
8080
Extracts annotations from the provided list of unit annotations and returns a dictionary
8181
of datasets and their corresponding values.
@@ -94,20 +94,27 @@ def retrieve_annotations(self, unit_annotations):
9494
for annotation_json in unit_annotations:
9595
for dataset in annotation_json:
9696
if len(annotation_json[dataset]) > 0:
97-
analysis_dataset = self.get_value_for_dataset(dataset, annotation_json[dataset])
97+
analysis_dataset = self.get_value_for_dataset(dataset, omic_unit, annotation_json[dataset])
9898
annotations[dataset] = analysis_dataset[
9999
'value'] if analysis_dataset is not None else annotation_json[dataset][0]['value']
100100
return annotations
101101

102-
def get_value_for_dataset(self, dataset_name: str, annotation_json_list: list):
102+
def get_value_for_dataset(self, dataset_name: str, omic_unit: str, annotation_json_list: list):
103103
"""
104104
Retrieves the annotation according to the analysis' dataset manifest entry matching the dataset's name,
105105
'data_source', and 'version'. None is returned when there isn't an entry in the manifest.
106106
"""
107-
dataset_config = next((configuration for configuration in self.manifest if dataset_name in configuration), None)
107+
108+
omic_manifest = next((manifest for manifest in self.manifest if manifest['unit'] == omic_unit), None)
109+
if omic_manifest is None:
110+
return None
111+
112+
dataset_config = next(
113+
(configuration for configuration in omic_manifest['manifest'] if dataset_name in configuration), None
114+
)
108115

109116
if dataset_config is None:
110-
return dataset_config
117+
return None
111118

112119
configuration = dataset_config[dataset_name]
113120

0 commit comments

Comments
Β (0)