22import asyncio
33import copy
44import logging
5+ import math
56from typing import Dict , List , Tuple
7+
8+ import numpy as np
69from aiohttp import web
710from aiohttp .web_request import Request
811from bson import json_util
912from beacon import conf
13+ from beacon .db import client
14+ from pymongo import ReturnDocument
1015import yaml
16+ import math
1117
1218from beacon .request import ontologies
1319from beacon .request .model import AlphanumericFilter , Granularity , RequestParams
@@ -60,62 +66,166 @@ async def wrapper(request: Request):
6066
6167 return wrapper
6268
69+ # support functions for the budget strategy
70+
71+ # update the budget of a specific individual for a user in the budget collection
72+ def update_individual_budget (user_id , individual_id , amount ):
73+ try :
74+ budget_collection = client .beacon ['budget' ]
75+ #LOG.debug(f"Updating budget for individual_id={individual_id} by amount={amount}")
76+
77+ # Find the document and update it, returning the updated document
78+ updated_document = budget_collection .find_one_and_update (
79+ {"individualId" : individual_id , "userId" : user_id },
80+ {"$inc" : {"budget" : - amount }},
81+ return_document = ReturnDocument .AFTER # Return the updated document
82+ )
83+
84+ return updated_document
85+
86+ except Exception as e :
87+ LOG .error (f"Error updating budget: { str (e )} " )
88+ return None
89+
90+ def pvalue_strategy (access_token , records , qparams ):
91+ helper = []
92+ total_cases = 0
93+ removed_individuals = []
94+ removed = False
95+
96+ for record in records :
97+ individual_ids = set ()
98+ individuals_to_remove = set ()
99+
100+ # step 4: compute the risk for that query: ri = -log(1 - Di)
101+ allele_frequency = record .get ('alleleFrequency' )
102+ N = client .beacon .get_collection ('individuals' ).count_documents ({}) # total number of individuals !! if user requestes dataset, N = individuals in that dataset
103+ Di = (1 - allele_frequency ) ** (2 * N )
104+ ri = - (math .log10 (1 - Di ))
105+ LOG .debug (f"O CUSTO DESTA QUERY É ESTE = { ri } " )
106+
107+ # fetch individualId from the biosample collection
108+ case_level_data = record .get ('caseLevelData' , [])
109+ for case in case_level_data :
110+ individual_id = case .get ('biosampleId' ) # biosampleId = individualId
111+ individual_ids .add (individual_id )
112+
113+ for individualId in individual_ids :
114+
115+ search_criteria = {
116+ "userId" : access_token ,
117+ "individualId" : individualId
118+ }
119+
120+ # Step 2: check if query has been asked before
121+ response_history = client .beacon ['history' ].find_one ({"userId" : access_token , "query" : qparams .summary ()})
122+ if response_history is not None :
123+ LOG .debug (f"Query was previously done by the same user" )
124+ return response_history ["response" ], helper , total_cases , removed , removed_individuals # Return stored answer if query was asked before by the same user
125+
126+ # Step 3: check if there are records with bj > ri
127+ budget_info = client .beacon ['budget' ].find_one (search_criteria )
128+ if not budget_info :
129+ p_value = 0.5 # upper bound on test errors
130+ bj = - (math .log10 (p_value )) # initial budget
131+ budget_info = {
132+ "userId" : access_token ,
133+ "individualId" : individualId ,
134+ "budget" : bj
135+ }
136+ client .beacon ['budget' ].insert_one (budget_info )
137+
138+ # re-fetch the budget_info to ensure we have the latest data
139+ budget_info = client .beacon ['budget' ].find_one (search_criteria )
140+
141+ if budget_info and budget_info ['budget' ] < ri :
142+
143+ individuals_to_remove .add (individualId )
144+ else :
145+ if budget_info ['budget' ] >= ri :
146+ # Step 7: reduce their budgets by ri
147+ update_individual_budget (access_token , individualId , ri )
148+ budget_info = client .beacon ['budget' ].find_one (search_criteria )
149+
150+ if individuals_to_remove :
151+ # filter the individuals from the record
152+ removed = True
153+ removed_individuals = individuals_to_remove
154+ LOG .debug (f"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" )
155+ LOG .debug (f"Removed individuals: { list (individuals_to_remove )} " ) # signal to know which individuals have no more budget
156+ LOG .debug (f"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" )
157+ record ['caseLevelData' ] = [case for case in record ['caseLevelData' ] if case .get ('biosampleId' ) not in individuals_to_remove ]
158+ if record ['caseLevelData' ] != []:
159+ helper .append (record )
160+ else :
161+ helper .append (record )
162+
163+ total_cases += len (record ['caseLevelData' ])
164+
165+ return None , helper , total_cases , removed , removed_individuals
166+
167+
63168
64169# handler with authentication & REMS
65170# mostly from BioData.pt
66171def generic_handler (db_fn , request = None ):
67-
68- async def wrapper (request :Request ):
172+
173+ async def wrapper (request : Request ):
69174 LOG .info ("-- Generic handler --" )
70-
175+
71176 # Get params
72177 entry_id = request .match_info .get ('id' , None )
73178 json_body = await request .json () if request .method == "POST" and request .has_body and request .can_read_body else {}
74- qparams :RequestParams = RequestParams (** json_body ).from_request (request )
75-
179+ qparams : RequestParams = RequestParams (** json_body ).from_request (request )
180+
76181 LOG .debug (f"Query Params = { qparams } " )
77-
182+
78183 LOG .debug (f"Headers = { request .headers } " )
79-
184+
80185 access_token_header = request .headers .get ('Authorization' )
81186 access_token_cookies = request .cookies .get ("Authorization" )
82187 LOG .debug (f"Access token header = { access_token_header } " )
83188 LOG .debug (f"Access token cookies = { access_token_cookies } " )
84-
85- # set access_token as the one we recieve in the header
189+
190+ registered = False
191+ public = False
192+
193+ # set access_token as the one we receive in the header
86194 # if not in header, get the one from cookies
87195 if access_token_header :
88196 access_token = access_token_header
89197 else :
90198 access_token = access_token_cookies
91-
199+
92200 # get specified datasets
93- requested_datasets = qparams .query .request_parameters .get ("datasets" , None )
201+ requested_datasets = qparams .query .request_parameters .get ("datasets" , None )
94202 LOG .debug (f"requested_datasets = { requested_datasets } " )
95-
203+
96204 # Start async task to request datasets from permissions server
97205 task_permissions = asyncio .create_task (get_accessible_datasets (access_token , requested_datasets ))
98206
99207 # if no datasets were specified, use all in DB
100208 if requested_datasets is None :
101-
102209 # get list of all datasets in DB
103210 _ , _ , all_dataset_docs = get_datasets (None , RequestParams ())
104- all_dataset_ids = [ doc ["id" ] for doc in all_dataset_docs ]
211+ all_dataset_ids = [doc ["id" ] for doc in all_dataset_docs ]
105212 target_datasets = all_dataset_ids
106213 # else, query only the ones specified
107214 else :
108215 target_datasets = requested_datasets
109-
216+
110217 # TODO query all datasets in parallel
111218 tasks_dataset_queries = []
112219 # { dataset_id:(count, records) }
113220 datasets_query_results :Dict [str , Tuple [int ,List [dict ]]] = {}
114-
221+
115222 db_fn_submodule = str (db_fn .__module__ ).split ("." )[- 1 ]
116223 LOG .debug (f"db_fn submodule = { db_fn_submodule } " )
117-
118-
224+
225+ # get response of permissions server
226+ accessible_datasets : List [str ] = [] # array of dataset ids
227+ accessible_datasets , public , registered = await task_permissions
228+
119229 # TODO do this asynchronously
120230 for dataset_id in target_datasets :
121231 qparams_dataset = copy .deepcopy (qparams )
@@ -127,46 +237,66 @@ async def wrapper(request:Request):
127237 if db_fn_submodule == "g_variants" :
128238 filter_dataset_id = {
129239 "id" : "_info.datasetId" ,
130- "value" : dataset_id
240+ "value" : dataset_id
131241 }
132242 else :
133243 filter_dataset_id = {
134- "id" : "datasetId" ,
244+ "id" : "datasetId" ,
135245 "value" : dataset_id
136246 }
137-
247+
138248 qparams_dataset .query .filters .append (filter_dataset_id )
139249 LOG .debug (f"Dataset Qparams = { qparams_dataset } " )
140250 entity_schema , count , records = db_fn (entry_id , qparams_dataset )
141- dataset_result = (count , list (records ))
142- datasets_query_results [dataset_id ] = (dataset_result )
143-
144-
251+
252+ ######################## P-VALUE STRATEGY ########################
253+ # apply the p-value strategy if user is authenticated but not registered and only if submodule is genomic variations
254+ count = 1
255+ LOG .debug (f"Is the user public? { public } " )
256+ LOG .debug (f"Is the user registered { registered } " )
257+ if not public and not registered and db_fn_submodule == "g_variants" :
258+ history , records , total_cases , removed , removed_individuals = pvalue_strategy (access_token , records , qparams )
259+ dataset_result = (count , records , total_cases )
260+ datasets_query_results [dataset_id ] = (dataset_result )
261+
262+ if history is not None :
263+ LOG .debug (f"Query was previously made by the same user" )
264+ return await json_stream (request , history )
265+
145266 LOG .debug (f"schema = { entity_schema } " )
146-
147- # get response of permissions server
148- accessible_datasets :List [str ] = [] # array of dataset ids
149- accessible_datasets = await task_permissions
150267
151268 # get the max authorized granularity
152269 requested_granularity = qparams .query .requested_granularity
153270 max_granularity = Granularity (conf .max_beacon_granularity )
154271 response_granularity = Granularity .get_lower (requested_granularity , max_granularity )
155-
272+
156273 # build response
157-
158- response = build_generic_response (
274+ response , store = build_generic_response (
159275 results_by_dataset = datasets_query_results ,
160276 accessible_datasets = accessible_datasets ,
161277 granularity = response_granularity ,
162278 qparams = qparams ,
163- entity_schema = entity_schema
279+ entity_schema = entity_schema ,
280+ registered = registered ,
281+ public = public
164282 )
283+ LOG .debug (f"Will the response be stored? { store } " )
165284
166- return await json_stream (request , response )
167-
285+
286+ document = {
287+ "userId" : access_token ,
288+ "query" : qparams .summary (),
289+ "response" : response
290+ }
291+
292+ if store :
293+ client .beacon ['history' ].insert_one (document )
168294
295+
296+ return await json_stream (request , removed_individuals )
297+
169298 return wrapper
299+
170300
171301
172302
0 commit comments