From b3ff16cf75376e929f75e209db22a8a944609c38 Mon Sep 17 00:00:00 2001 From: ens-lgil Date: Mon, 7 Sep 2026 09:43:42 +0100 Subject: [PATCH 1/5] Update genetic scoring files creation from files --- exports/config_template.py | 2 +- exports/metadata_build_export.py | 28 ++++++++----- ...generate_genetic_score_files_from_files.py | 42 ++++++++++++++++--- 3 files changed, 55 insertions(+), 17 deletions(-) diff --git a/exports/config_template.py b/exports/config_template.py index 4a93a8c..a4b21ad 100644 --- a/exports/config_template.py +++ b/exports/config_template.py @@ -36,7 +36,7 @@ # Scoring files exports - from text files scoring_file_from_file_config = { - 'pmid': '', # e.g. 35501419 + 'publication_id': '', # e.g. OPP000001 'input_dir_root': '', 'output_dir_root': '' } \ No newline at end of file diff --git a/exports/metadata_build_export.py b/exports/metadata_build_export.py index f20a43d..4bb50fb 100644 --- a/exports/metadata_build_export.py +++ b/exports/metadata_build_export.py @@ -13,7 +13,9 @@ class MetadataExport: def __init__(self, exports_dir:str, sqlite_dir:str, dataset:Dataset): self.dataset = dataset self.dataset_id = dataset.id - self.sqlite_dir = f'{sqlite_dir}/{self.dataset_id}' + self.sqlite_dir = None + if sqlite_dir: + self.sqlite_dir = f'{sqlite_dir}/{self.dataset_id}' self.data = { 'dataset': self.get_data_attr(self.dataset,'Dataset'), 'publication': self.get_data_attr(self.dataset.publication,'Publication'), @@ -26,15 +28,18 @@ def __init__(self, exports_dir:str, sqlite_dir:str, dataset:Dataset): # Find corresponding SQLite file self.sqlite_file = None - for s_file in os.listdir(self.sqlite_dir): - if s_file.startswith(dataset.id) and s_file.endswith('.db'): - self.sqlite_file = (s_file) - break - if not self.sqlite_file: - print(f"ERROR: Can't find a SQLite file for the dataset {self.dataset_id} in {self.sqlite_dir}") - exit() - - filename = self.sqlite_file.replace('.db','_metadata.xlsx') + if self.sqlite_dir: + for s_file in os.listdir(self.sqlite_dir): + if s_file.startswith(dataset.id) and s_file.endswith('.db'): + self.sqlite_file = (s_file) + break + if not self.sqlite_file: + print(f"ERROR: Can't find a SQLite file for the dataset {self.dataset_id} in {self.sqlite_dir}") + exit() + if self.sqlite_file: + filename = self.sqlite_file.replace('.db','_metadata.xlsx') + else: + filename = f'{self.dataset_id}_metadata.xlsx' self.filepath = f'{exports_dir}/{filename}' @@ -192,7 +197,8 @@ def generate_metadata(self): self.data['publication'] = self.get_data_attr(self.dataset.publication,'Publication') # Fetch phi values from the SQLite export - self.fetch_phi_values_from_sqlite() + if self.sqlite_dir: + self.fetch_phi_values_from_sqlite() # Add file urls to the dataset self.add_dataset_file_urls() diff --git a/exports/scripts/generate_genetic_score_files_from_files.py b/exports/scripts/generate_genetic_score_files_from_files.py index 95ecbe9..0f3cefb 100644 --- a/exports/scripts/generate_genetic_score_files_from_files.py +++ b/exports/scripts/generate_genetic_score_files_from_files.py @@ -20,18 +20,50 @@ def generate_scoring_file_header(score:Score, dataset:Dataset): reported_trait = score.trait_reported else: reported_trait = score.trait_reported_id - return f'''#omicspred_id={score.id} + + trait_type = dataset.platform.platform_master.type + + # Setup 'trait_mapped' + molecular_traits = [] + trait_mapped = set() + match trait_type: + case 'Proteomics': + molecular_traits = score.proteins.all() + case 'Transcriptomics': + molecular_traits = score.genes.all() + case 'Metabolomics': + molecular_traits = score.metabolites.all() + case _: + print("Can't find a valid platform type.") # Default case + exit() + for mt in molecular_traits: + if mt.name and mt.external_id: + trait_mapped.add(f'{mt.name} ({mt.external_id})') + elif mt.name: + trait_mapped.add(mt.name) + elif mt.external_id: + trait_mapped.add(mt.external_id) + + platform_version = f' ({dataset.platform.version})' if dataset.platform.version else '' + return f'''###GENETIC SCORING FILE - see https://www.pgscatalog.org/downloads/#dl_ftp_scoring for additional information +#format_version=2.0 +##GENETIC SCORE (OPGS) INFORMATION +#omicspred_id={score.id} #pgs_name={score.name} -#trait_type=proteomics +#trait_type={trait_type} #measurement_tissue={dataset.tissue.label} ({dataset.tissue.id}) -#measurement_platform=Somalogic ({dataset.platform.version}) +#measurement_platform={dataset.platform.name}{platform_version} +#trait_mapped={'|'.join(sorted(trait_mapped))} #trait_reported={reported_trait} #genome_build={score.variants_genomebuild} #variants_number={score.variants_number} +##SOURCE INFORMATION +#pgp_id={dataset.publication.id} #citation={publication_model.firstauthor} et al. {publication_model.journal} ({publication_model.pub_year}). doi:{publication_model.doi} #license={score.license}''' + def write_scoring_file(header:str, content:str, filepath:str) -> None: ''' Write new scoring file with header ''' with open(filepath, 'w') as output_file: @@ -56,7 +88,7 @@ def get_dataset_label(id:str, name:str) -> str: def run(): - pmid = scoring_file_from_file_config['pmid'] + publication_id = scoring_file_from_file_config['publication_id'] input_dir_root = scoring_file_from_file_config['input_dir_root'] output_dir_root = scoring_file_from_file_config['output_dir_root'] @@ -69,7 +101,7 @@ def run(): print(f'-> input_dir_name > {scores_dir}: {scores_dir_path}') input_dirs[scores_dir] = scores_dir_path - datasets = Dataset.objects.filter(publication__pmid=pmid) + datasets = Dataset.objects.filter(publication__id=publication_id) for dataset in datasets: print(f"# {dataset.name} ({dataset.id}) - {dataset.num}") From 75638bcdc929d482887c0eb229441ea2245f19d1 Mon Sep 17 00:00:00 2001 From: ens-lgil Date: Mon, 7 Sep 2026 09:46:59 +0100 Subject: [PATCH 2/5] Add an Info model and a script to update it --- config/settings.py | 1 + imports/scripts/update_db_info_count.py | 33 +++++++++++++ omicspred/models.py | 11 ++++- rest_api/context_processors.py | 7 +++ rest_api/fixtures/db_test.default.json | 64 +++++++++++++++++++++++++ rest_api/views.py | 15 ++---- 6 files changed, 118 insertions(+), 13 deletions(-) create mode 100644 imports/scripts/update_db_info_count.py create mode 100644 rest_api/context_processors.py diff --git a/config/settings.py b/config/settings.py index e32fd0e..4ecfd18 100644 --- a/config/settings.py +++ b/config/settings.py @@ -142,6 +142,7 @@ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', + 'rest_api.context_processors.op_settings', ], }, }, diff --git a/imports/scripts/update_db_info_count.py b/imports/scripts/update_db_info_count.py new file mode 100644 index 0000000..eab604b --- /dev/null +++ b/imports/scripts/update_db_info_count.py @@ -0,0 +1,33 @@ +from omicspred.models import * + +def run(): + current_info = {} + for info in Info.objects.all(): + info_name = info.name + current_info[info_name] = info + + # Datasets + new_info = { + 'datasets': Dataset.objects.count(), + 'scores': Score.objects.count(), + 'publications': Publication.objects.count(), + 'platforms': PlatformMaster.objects.count(), + 'pathways': Pathway.objects.count(), + 'phenotypes': Phenotype.objects.count(), + 'phewas': ScorePheWAS.objects.count(), + 'tissues': Tissue.objects.filter(type='tissue').count() + } + + for info_name in new_info.keys(): + if info_name in current_info.keys(): + current_info_model = current_info[info_name] + current_info_model.value = new_info[info_name] + current_info_model.save() + print(f"- Update '{info_name}' count ({new_info[info_name]})") + else: + info_model = Info( + name = info_name, + value = new_info[info_name] + ) + info_model.save() + print(f"- Create '{info_name}' count ({new_info[info_name]})") \ No newline at end of file diff --git a/omicspred/models.py b/omicspred/models.py index 9c8e3ed..37d3603 100644 --- a/omicspred/models.py +++ b/omicspred/models.py @@ -607,7 +607,8 @@ class Performance(models.Model): # Evaluation Type EVALUATION_CHOICES = [ ('T', 'Training'), - ('IV', 'Independent Validation'), + ('IDV', 'Independent Validation'), + ('IV', 'Internal Validation'), ('EV', 'External Validation'), ('E', 'Evaluation') ] @@ -810,4 +811,10 @@ class ExternalSource(models.Model): """ Class to hold ExternalSource values """ name = models.CharField('External Source Name', max_length=50) version = models.CharField('External Source Version', max_length=50, null=True) - url = models.CharField('External Source URL', max_length=100) \ No newline at end of file + url = models.CharField('External Source URL', max_length=100) + + +class Info(models.Model): + """ Class to hold overall information counts/values """ + name = models.CharField('Info Name', max_length=50) + value = models.IntegerField(verbose_name='Info Value') \ No newline at end of file diff --git a/rest_api/context_processors.py b/rest_api/context_processors.py new file mode 100644 index 0000000..53da405 --- /dev/null +++ b/rest_api/context_processors.py @@ -0,0 +1,7 @@ +from django.conf import settings + + +def op_settings(request): + return { + 'is_public_site' : settings.PUBLIC_SITE + } \ No newline at end of file diff --git a/rest_api/fixtures/db_test.default.json b/rest_api/fixtures/db_test.default.json index 2bbf967..78251d8 100644 --- a/rest_api/fixtures/db_test.default.json +++ b/rest_api/fixtures/db_test.default.json @@ -1044,5 +1044,69 @@ "variants_number_used": 19, "variants_fraction_found": 1 } + }, + { + "model": "omicspred.Info", + "pk": 1, + "fields": { + "name": "datasets", + "value": 216 + } + }, + { + "model": "omicspred.Info", + "pk": 2, + "fields": { + "name": "scores", + "value": 3339610 + } + }, + { + "model": "omicspred.Info", + "pk": 3, + "fields": { + "name": "publications", + "value": 7 + } + }, + { + "model": "omicspred.Info", + "pk": 4, + "fields": { + "name": "platforms", + "value": 6 + } + }, + { + "model": "omicspred.Info", + "pk": 5, + "fields": { + "name": "pathways", + "value": 2748 + } + }, + { + "model": "omicspred.Info", + "pk": 6, + "fields": { + "name": "phenotypes", + "value": 927 + } + }, + { + "model": "omicspred.Info", + "pk": 7, + "fields": { + "name": "phewas", + "value": 369024 + } + }, + { + "model": "omicspred.Info", + "pk": 8, + "fields": { + "name": "tissues", + "value": 53 + } } ] \ No newline at end of file diff --git a/rest_api/views.py b/rest_api/views.py index ae6d4a6..2d566e6 100644 --- a/rest_api/views.py +++ b/rest_api/views.py @@ -1819,21 +1819,14 @@ class RestInfo(generics.RetrieveAPIView): """ def get(self, request): - + data_count = {} + for info in Info.objects.all().order_by('id'): + data_count[info.name] = info.value data = { 'rest_api': { "version": settings.REST_API_VERSION }, - 'data_count': { - 'datasets': Dataset.objects.count(), - 'scores': Score.objects.count(), - 'publications': Publication.objects.count(), - 'platforms': PlatformMaster.objects.count(), - 'pathways': Pathway.objects.count(), - 'phenotypes': Phenotype.objects.count(), - 'phewas': ScorePheWAS.objects.count(), - 'tissues': Tissue.objects.filter(type='tissue').count() - } + 'data_count': data_count } return Response(data) From 07b654530bb3f55a84c8487f56122223586e410e Mon Sep 17 00:00:00 2001 From: ens-lgil Date: Mon, 7 Sep 2026 09:50:41 +0100 Subject: [PATCH 3/5] Update import scripts --- imports/README.md | 1 + imports/omicspred/models/performance.py | 7 +++++ imports/omicspred/models/publication.py | 28 ++++++++++++------- imports/omicspred/models/sample.py | 2 +- imports/omicspred/spreadsheets/spreadsheet.py | 14 ++++++---- 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/imports/README.md b/imports/README.md index 3e73890..d687838 100644 --- a/imports/README.md +++ b/imports/README.md @@ -17,3 +17,4 @@ python manage.py runscript import_metadata 4. update_ensembl_proteins.py 5. download_reactome_data.py (optional - only for updated dataset from Reactome) 6. get_reactome_mappings.py +7. update_db_info_count.py diff --git a/imports/omicspred/models/performance.py b/imports/omicspred/models/performance.py index ae68c7d..0db88d5 100644 --- a/imports/omicspred/models/performance.py +++ b/imports/omicspred/models/performance.py @@ -82,7 +82,14 @@ def get_cohort_label(self, sample_model:Sample) -> str: def update_eval_type(self): + eval_type_mapping = { + 'Variant associations': 'Training', + 'Score development': 'Training', + 'Validation': 'External Validation' + } eval_type = self.data['eval_type'] + if eval_type in eval_type_mapping.keys(): + eval_type = eval_type_mapping[eval_type] eval_type_choices = Performance.eval_type.field.choices eval_types = {} for choice in eval_type_choices: diff --git a/imports/omicspred/models/publication.py b/imports/omicspred/models/publication.py index 8c5c60a..21ea791 100644 --- a/imports/omicspred/models/publication.py +++ b/imports/omicspred/models/publication.py @@ -6,26 +6,34 @@ class PublicationData(GenericData): - def __init__(self,pmid): + def __init__(self,pmid:int,data:dict): GenericData.__init__(self) - self.pmid = int(pmid) - self.check_model_exist() - if not self.model: - self.fetch_publication_information() + if pmid and str(pmid).isdigit(): + self.pmid = int(pmid) + self.check_model_exist() + if not self.model: + self.fetch_publication_information() + else: + self.pmid = None + self.data = data + self.firstauthor = data['firstauthor'] def check_model_exist(self): ''' Check if a Publication model already exists. ''' - try: - publication = Publication.objects.get(pmid=self.pmid) - self.model = publication - except Publication.DoesNotExist: + if self.pmid: + try: + publication = Publication.objects.get(pmid=self.pmid) + self.model = publication + except Publication.DoesNotExist: + self.model = None + else: self.model = None - def rest_api_call_to_epmc(self,query): + def rest_api_call_to_epmc(self,query:str): ''' REST API call to EuropePMC - query: the search query diff --git a/imports/omicspred/models/sample.py b/imports/omicspred/models/sample.py index 02cfad8..8b7650c 100644 --- a/imports/omicspred/models/sample.py +++ b/imports/omicspred/models/sample.py @@ -69,7 +69,7 @@ def create_model(self): elif field == 'sample_percent_male': # Remove % character val_str = str(val) - if re.search('\%',val_str): + if re.search('\\%',val_str): val_str = re.sub(r'\%', r'', val_str) val_str = re.sub(r' ', r'', val_str) val = float(val_str) diff --git a/imports/omicspred/spreadsheets/spreadsheet.py b/imports/omicspred/spreadsheets/spreadsheet.py index af3cf70..c003612 100644 --- a/imports/omicspred/spreadsheets/spreadsheet.py +++ b/imports/omicspred/spreadsheets/spreadsheet.py @@ -136,17 +136,20 @@ def extract_data(self): model = 'Publication' logger.info(f"Start to parse the {model} spreadsheet") pmid = None + data = {} pub_info = self.dataframe.iloc[0] for col in pub_info.keys(): # m, f = self.get_model_field_from_schema(col,self.spreadsheet_schema) m, f = self.spreadsheet_schema.loc[col][:2] if f == 'pmid': pmid = pub_info[col] - break - if pmid: - pub_data = PublicationData(pmid) + # break + elif f in ['doi','journal','date_publication','firstauthor']: + data[f] = pub_info[col] + pub_data = PublicationData(pmid,data) + if pmid and str(pmid).isdigit(): pub_data.fetch_publication_information() - self.parsed_data[pmid] = pub_data + self.parsed_data[pmid] = pub_data @@ -286,7 +289,8 @@ def extract_data(self): dataset_name_suffix = score_components[1] ################################################# - dataset_tag = f'{platform.name}_{platform_version}_{self.publication.pmid}_{dataset_tag_suffix}' + publication_tag = self.publication.pmid if self.publication.pmid else self.publication.firstauthor + dataset_tag = f'{platform.name}_{platform_version}_{publication_tag}_{dataset_tag_suffix}' if dataset_tag in self.datasets.keys(): dataset = self.datasets[dataset_tag] dataset.add_score() From 6c26f8f06f83b730a72ed97c39f0b41d5303d2bf Mon Sep 17 00:00:00 2001 From: ens-lgil Date: Mon, 7 Sep 2026 09:51:19 +0100 Subject: [PATCH 4/5] Update models with the new Info model --- omicspred/migrations/0001_initial.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/omicspred/migrations/0001_initial.py b/omicspred/migrations/0001_initial.py index 09fda48..7d19e18 100644 --- a/omicspred/migrations/0001_initial.py +++ b/omicspred/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.6 on 2026-07-03 13:41 +# Generated by Django 6.0.8 on 2026-09-07 08:48 import django.contrib.postgres.fields.ranges import django.core.validators @@ -53,6 +53,14 @@ class Migration(migrations.Migration): ('url', models.CharField(max_length=100, verbose_name='External Source URL')), ], ), + migrations.CreateModel( + name='Info', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, verbose_name='Info Name')), + ('value', models.IntegerField(verbose_name='Info Value')), + ], + ), migrations.CreateModel( name='Pathway', fields=[ @@ -205,7 +213,7 @@ class Migration(migrations.Migration): name='Performance', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('eval_type', models.CharField(choices=[('T', 'Training'), ('IV', 'Independent Validation'), ('EV', 'External Validation'), ('E', 'Evaluation')], default='', max_length=25, verbose_name='Evaluation Type')), + ('eval_type', models.CharField(choices=[('T', 'Training'), ('IDV', 'Independent Validation'), ('IV', 'Internal Validation'), ('EV', 'External Validation'), ('E', 'Evaluation')], default='', max_length=25, verbose_name='Evaluation Type')), ('performance_additional', models.TextField(default='', verbose_name='Additional Information')), ('source_gwas_catalog', models.CharField(max_length=20, null=True, verbose_name='GWAS Catalog Study ID (GCST...)')), ('source_doi', models.CharField(max_length=100, null=True, verbose_name='Source DOI')), From f7a2915437b7c80f0e1aa049456ca439447483c2 Mon Sep 17 00:00:00 2001 From: ens-lgil Date: Mon, 7 Sep 2026 09:51:45 +0100 Subject: [PATCH 5/5] Diverse minor updates --- requirements.txt | 2 +- rest_api/static/rest_api/images/favicon.ico | Bin 15086 -> 15406 bytes rest_api/templates/robots.txt | 2 ++ 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c5cf57a..38b0ea0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ #### Django library #### -Django==6.0.7 +Django==6.0.8 #### Python PostgreSQL library #### # psycopg2-binary==2.9.11 psycopg[binary]==3.3.4 diff --git a/rest_api/static/rest_api/images/favicon.ico b/rest_api/static/rest_api/images/favicon.ico index 3927d37b139bf1c3843844e90a73c0c70e17a3c9..3c7bff9942f83882a944e63752f2de54f05cda2d 100644 GIT binary patch literal 15406 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