refactor data service

This commit is contained in:
Nicola Tarocco 2023-11-09 15:39:58 +01:00
parent e0714d6aad
commit 035190564c
No known key found for this signature in database
GPG key ID: A08DEF00BA54E806
4 changed files with 530 additions and 426 deletions

View file

@ -1,319 +1,451 @@
import asyncio
import os
from caimira.store.global_store import GlobalStore
class Configuration:
'''
Configuration to handle data. Contains the default values used in the model.
Might suffer update from the Data Service.
'''
"""Configuration singleton to cache data values."""
def __init__(self):
self.data_fetched = False
self.BLOmodel = {
'cn': {'B': 0.06,
'L': 0.2,
'O': 0.0010008,
},
'mu': {
'B': 0.989541,
'L': 1.38629,
'O': 4.97673,
},
'sigma': {
'B': 0.262364,
'L': 0.506818,
'O': 0.585005,
},
}
self.activity_distributions = {
'Seated': {
'inhalation_rate': {
'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)',
'parameters': {'mean_gaussian': -0.6872121723362303, 'standard_deviation_gaussian': 0.10498338229297108},
},
'exhalation_rate': {
'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)',
'parameters': {'mean_gaussian': -0.6872121723362303, 'standard_deviation_gaussian': 0.10498338229297108},
BLOmodel = {
"cn": {
"B": 0.06,
"L": 0.2,
"O": 0.0010008,
},
"mu": {
"B": 0.989541,
"L": 1.38629,
"O": 4.97673,
},
"sigma": {
"B": 0.262364,
"L": 0.506818,
"O": 0.585005,
},
}
activity_distributions = {
"Seated": {
"inhalation_rate": {
"associated_distribution": "Numpy Log-normal Distribution (random.lognormal)",
"parameters": {
"mean_gaussian": -0.6872121723362303,
"standard_deviation_gaussian": 0.10498338229297108,
},
},
'Standing': {
'inhalation_rate': {
'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)',
'parameters': {'mean_gaussian': -0.5742377578494785, 'standard_deviation_gaussian': 0.09373162411398223},
},
'exhalation_rate': {
'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)',
'parameters': {'mean_gaussian': -0.5742377578494785, 'standard_deviation_gaussian': 0.09373162411398223},
"exhalation_rate": {
"associated_distribution": "Numpy Log-normal Distribution (random.lognormal)",
"parameters": {
"mean_gaussian": -0.6872121723362303,
"standard_deviation_gaussian": 0.10498338229297108,
},
},
'Light activity': {
'inhalation_rate': {
'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)',
'parameters': {'mean_gaussian': 0.21380242785625422, 'standard_deviation_gaussian': 0.09435378091059601},
},
'exhalation_rate': {
'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)',
'parameters': {'mean_gaussian': 0.21380242785625422, 'standard_deviation_gaussian': 0.09435378091059601},
},
"Standing": {
"inhalation_rate": {
"associated_distribution": "Numpy Log-normal Distribution (random.lognormal)",
"parameters": {
"mean_gaussian": -0.5742377578494785,
"standard_deviation_gaussian": 0.09373162411398223,
},
},
'Moderate activity': {
'inhalation_rate': {
'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)',
'parameters': {'mean_gaussian': 0.551771330362601, 'standard_deviation_gaussian': 0.1894616357138137},
},
'exhalation_rate': {
'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)',
'parameters': {'mean_gaussian': 0.551771330362601, 'standard_deviation_gaussian': 0.1894616357138137},
"exhalation_rate": {
"associated_distribution": "Numpy Log-normal Distribution (random.lognormal)",
"parameters": {
"mean_gaussian": -0.5742377578494785,
"standard_deviation_gaussian": 0.09373162411398223,
},
},
'Heavy exercise': {
'inhalation_rate': {
'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)',
'parameters': {'mean_gaussian': 1.1644665696723049, 'standard_deviation_gaussian': 0.21744554768657565},
},
'exhalation_rate': {
'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)',
'parameters': {'mean_gaussian': 1.1644665696723049, 'standard_deviation_gaussian': 0.21744554768657565},
},
"Light activity": {
"inhalation_rate": {
"associated_distribution": "Numpy Log-normal Distribution (random.lognormal)",
"parameters": {
"mean_gaussian": 0.21380242785625422,
"standard_deviation_gaussian": 0.09435378091059601,
},
},
}
self.symptomatic_vl_frequencies = {
'log_variable': [2.46032, 2.67431, 2.85434, 3.06155, 3.25856, 3.47256, 3.66957, 3.85979, 4.09927, 4.27081,
4.47631, 4.66653, 4.87204, 5.10302, 5.27456, 5.46478, 5.6533, 5.88428, 6.07281, 6.30549,
6.48552, 6.64856, 6.85407, 7.10373, 7.30075, 7.47229, 7.66081, 7.85782, 8.05653, 8.27053,
8.48453, 8.65607, 8.90573, 9.06878, 9.27429, 9.473, 9.66152, 9.87552],
'frequencies': [0.001206885, 0.007851618, 0.008078144, 0.01502491, 0.013258014, 0.018528495, 0.020053765,
0.021896167, 0.022047184, 0.018604005, 0.01547796, 0.018075445, 0.021503523, 0.022349217,
0.025097721, 0.032875078, 0.030594727, 0.032573045, 0.034717482, 0.034792991,
0.033267721, 0.042887485, 0.036846816, 0.03876473, 0.045016819, 0.040063473, 0.04883754,
0.043944602, 0.048142864, 0.041588741, 0.048762031, 0.027921732, 0.033871788,
0.022122693, 0.016927718, 0.008833228, 0.00478598, 0.002807662],
'kernel_bandwidth': 0.1,
}
self.covid_overal_vl_data = {
'shape_factor': 3.47,
'scale_factor': 7.01,
'start': 0.01,
'stop': 0.99,
'num': 30.0,
'min_bound': 2,
'max_bound': 10,
'interpolation_fp_left': 0,
'interpolation_fp_right': 0,
'max_function': 0.2,
}
self.viable_to_RNA_ratio_distribution = {
'low': 0.01,
'high': 0.6,
}
self.infectious_dose_distribution = {
'low': 10,
'high': 100,
}
self.virus_distributions = {
'SARS_CoV_2': {
'viral_load_in_sputum': 'Ref: Viral load - covid_overal_vl_data',
'infectious_dose': 'Ref: Infectious dose - infectious_dose_distribution',
'viable_to_RNA_ratio': 'Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution',
'transmissibility_factor': 1,
'infectiousness_days': 14,
},
'SARS_CoV_2_ALPHA': {
'viral_load_in_sputum': 'Ref: Viral load - covid_overal_vl_data',
'infectious_dose': 'Ref: Infectious dose - infectious_dose_distribution',
'viable_to_RNA_ratio': 'Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution',
'transmissibility_factor': 0.78,
'infectiousness_days': 14,
},
'SARS_CoV_2_BETA': {
'viral_load_in_sputum': 'Ref: Viral load - covid_overal_vl_data',
'infectious_dose': 'Ref: Infectious dose - infectious_dose_distribution',
'viable_to_RNA_ratio': 'Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution',
'transmissibility_factor': 0.8,
'infectiousness_days': 14
},
'SARS_CoV_2_GAMMA': {
'viral_load_in_sputum': 'Ref: Viral load - covid_overal_vl_data',
'infectious_dose': 'Ref: Infectious dose - infectious_dose_distribution',
'viable_to_RNA_ratio': 'Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution',
'transmissibility_factor': 0.72,
'infectiousness_days': 14
},
'SARS_CoV_2_DELTA': {
'viral_load_in_sputum': 'Ref: Viral load - covid_overal_vl_data',
'infectious_dose': 'Ref: Infectious dose - infectious_dose_distribution',
'viable_to_RNA_ratio': 'Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution',
'transmissibility_factor': 0.51,
'infectiousness_days': 14
},
'SARS_CoV_2_OMICRON': {
'viral_load_in_sputum': 'Ref: Viral load - covid_overal_vl_data',
'infectious_dose': 'Ref: Infectious dose - infectious_dose_distribution',
'viable_to_RNA_ratio': 'Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution',
'transmissibility_factor': 0.2,
'infectiousness_days': 14
},
'SARS_CoV_2_Other': {
'viral_load_in_sputum': 'Ref: Viral load - covid_overal_vl_data',
'infectious_dose': 'Ref: Infectious dose - infectious_dose_distribution',
'viable_to_RNA_ratio': 'Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution',
'transmissibility_factor': 0.1,
'infectiousness_days': 14,
},
}
self.mask_distributions = {
'Type I': {
'η_inhale': {
'associated_distribution': 'Numpy Uniform Distribution (random.uniform)',
'parameters': {
'low': 0.25,
'high': 0.80,
},
"exhalation_rate": {
"associated_distribution": "Numpy Log-normal Distribution (random.lognormal)",
"parameters": {
"mean_gaussian": 0.21380242785625422,
"standard_deviation_gaussian": 0.09435378091059601,
},
'Known filtration efficiency of masks when exhaling?': 'No',
'factor_exhale': 1,
},
'FFP2': {
'η_inhale': {
'associated_distribution': 'Numpy Uniform Distribution (random.uniform)',
'parameters': {
'low': 0.83,
'high': 0.91,
},
},
"Moderate activity": {
"inhalation_rate": {
"associated_distribution": "Numpy Log-normal Distribution (random.lognormal)",
"parameters": {
"mean_gaussian": 0.551771330362601,
"standard_deviation_gaussian": 0.1894616357138137,
},
'Known filtration efficiency of masks when exhaling?': 'No',
'factor_exhale': 1,
},
'Cloth': {
'η_inhale': {
'associated_distribution': 'Numpy Uniform Distribution (random.uniform)',
'parameters': {
'low': 0.05,
'high': 0.40,
},
"exhalation_rate": {
"associated_distribution": "Numpy Log-normal Distribution (random.lognormal)",
"parameters": {
"mean_gaussian": 0.551771330362601,
"standard_deviation_gaussian": 0.1894616357138137,
},
'Known filtration efficiency of masks when exhaling?': 'Yes',
'η_exhale': {
'associated_distribution': 'Numpy Uniform Distribution (random.uniform)',
'parameters': {
'low': 0.20,
'high': 0.50,
},
},
'factor_exhale': 1,
},
}
self.expiration_BLO_factors = {
'Breathing': {'B': 1., 'L': 0., 'O': 0., },
'Speaking': {'B': 1., 'L': 1., 'O': 1., },
'Singing': {'B': 1., 'L': 5., 'O': 5., },
'Shouting': {'B': 1., 'L': 5., 'O': 5., },
}
self.long_range_expiration_distributions = {
'minimum_diameter': 0.1,
'maximum_diameter': 30,
}
self.short_range_expiration_distributions = {
'minimum_diameter': 0.1,
'maximum_diameter': 100,
}
self.short_range_distances = {
'minimum_distance': 0.5,
'maximum_distance': 2.,
}
},
"Heavy exercise": {
"inhalation_rate": {
"associated_distribution": "Numpy Log-normal Distribution (random.lognormal)",
"parameters": {
"mean_gaussian": 1.1644665696723049,
"standard_deviation_gaussian": 0.21744554768657565,
},
},
"exhalation_rate": {
"associated_distribution": "Numpy Log-normal Distribution (random.lognormal)",
"parameters": {
"mean_gaussian": 1.1644665696723049,
"standard_deviation_gaussian": 0.21744554768657565,
},
},
},
}
symptomatic_vl_frequencies = {
"log_variable": [
2.46032,
2.67431,
2.85434,
3.06155,
3.25856,
3.47256,
3.66957,
3.85979,
4.09927,
4.27081,
4.47631,
4.66653,
4.87204,
5.10302,
5.27456,
5.46478,
5.6533,
5.88428,
6.07281,
6.30549,
6.48552,
6.64856,
6.85407,
7.10373,
7.30075,
7.47229,
7.66081,
7.85782,
8.05653,
8.27053,
8.48453,
8.65607,
8.90573,
9.06878,
9.27429,
9.473,
9.66152,
9.87552,
],
"frequencies": [
0.001206885,
0.007851618,
0.008078144,
0.01502491,
0.013258014,
0.018528495,
0.020053765,
0.021896167,
0.022047184,
0.018604005,
0.01547796,
0.018075445,
0.021503523,
0.022349217,
0.025097721,
0.032875078,
0.030594727,
0.032573045,
0.034717482,
0.034792991,
0.033267721,
0.042887485,
0.036846816,
0.03876473,
0.045016819,
0.040063473,
0.04883754,
0.043944602,
0.048142864,
0.041588741,
0.048762031,
0.027921732,
0.033871788,
0.022122693,
0.016927718,
0.008833228,
0.00478598,
0.002807662,
],
"kernel_bandwidth": 0.1,
}
covid_overal_vl_data = {
"shape_factor": 3.47,
"scale_factor": 7.01,
"start": 0.01,
"stop": 0.99,
"num": 30.0,
"min_bound": 2,
"max_bound": 10,
"interpolation_fp_left": 0,
"interpolation_fp_right": 0,
"max_function": 0.2,
}
viable_to_RNA_ratio_distribution = {
"low": 0.01,
"high": 0.6,
}
infectious_dose_distribution = {
"low": 10,
"high": 100,
}
virus_distributions = {
"SARS_CoV_2": {
"viral_load_in_sputum": "Ref: Viral load - covid_overal_vl_data",
"infectious_dose": "Ref: Infectious dose - infectious_dose_distribution",
"viable_to_RNA_ratio": "Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution",
"transmissibility_factor": 1,
"infectiousness_days": 14,
},
"SARS_CoV_2_ALPHA": {
"viral_load_in_sputum": "Ref: Viral load - covid_overal_vl_data",
"infectious_dose": "Ref: Infectious dose - infectious_dose_distribution",
"viable_to_RNA_ratio": "Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution",
"transmissibility_factor": 0.78,
"infectiousness_days": 14,
},
"SARS_CoV_2_BETA": {
"viral_load_in_sputum": "Ref: Viral load - covid_overal_vl_data",
"infectious_dose": "Ref: Infectious dose - infectious_dose_distribution",
"viable_to_RNA_ratio": "Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution",
"transmissibility_factor": 0.8,
"infectiousness_days": 14,
},
"SARS_CoV_2_GAMMA": {
"viral_load_in_sputum": "Ref: Viral load - covid_overal_vl_data",
"infectious_dose": "Ref: Infectious dose - infectious_dose_distribution",
"viable_to_RNA_ratio": "Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution",
"transmissibility_factor": 0.72,
"infectiousness_days": 14,
},
"SARS_CoV_2_DELTA": {
"viral_load_in_sputum": "Ref: Viral load - covid_overal_vl_data",
"infectious_dose": "Ref: Infectious dose - infectious_dose_distribution",
"viable_to_RNA_ratio": "Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution",
"transmissibility_factor": 0.51,
"infectiousness_days": 14,
},
"SARS_CoV_2_OMICRON": {
"viral_load_in_sputum": "Ref: Viral load - covid_overal_vl_data",
"infectious_dose": "Ref: Infectious dose - infectious_dose_distribution",
"viable_to_RNA_ratio": "Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution",
"transmissibility_factor": 0.2,
"infectiousness_days": 14,
},
"SARS_CoV_2_Other": {
"viral_load_in_sputum": "Ref: Viral load - covid_overal_vl_data",
"infectious_dose": "Ref: Infectious dose - infectious_dose_distribution",
"viable_to_RNA_ratio": "Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution",
"transmissibility_factor": 0.1,
"infectiousness_days": 14,
},
}
mask_distributions = {
"Type I": {
"η_inhale": {
"associated_distribution": "Numpy Uniform Distribution (random.uniform)",
"parameters": {
"low": 0.25,
"high": 0.80,
},
},
"Known filtration efficiency of masks when exhaling?": "No",
"factor_exhale": 1,
},
"FFP2": {
"η_inhale": {
"associated_distribution": "Numpy Uniform Distribution (random.uniform)",
"parameters": {
"low": 0.83,
"high": 0.91,
},
},
"Known filtration efficiency of masks when exhaling?": "No",
"factor_exhale": 1,
},
"Cloth": {
"η_inhale": {
"associated_distribution": "Numpy Uniform Distribution (random.uniform)",
"parameters": {
"low": 0.05,
"high": 0.40,
},
},
"Known filtration efficiency of masks when exhaling?": "Yes",
"η_exhale": {
"associated_distribution": "Numpy Uniform Distribution (random.uniform)",
"parameters": {
"low": 0.20,
"high": 0.50,
},
},
"factor_exhale": 1,
},
}
expiration_BLO_factors = {
"Breathing": {
"B": 1.0,
"L": 0.0,
"O": 0.0,
},
"Speaking": {
"B": 1.0,
"L": 1.0,
"O": 1.0,
},
"Singing": {
"B": 1.0,
"L": 5.0,
"O": 5.0,
},
"Shouting": {
"B": 1.0,
"L": 5.0,
"O": 5.0,
},
}
long_range_expiration_distributions = {
"minimum_diameter": 0.1,
"maximum_diameter": 30,
}
short_range_expiration_distributions = {
"minimum_diameter": 0.1,
"maximum_diameter": 100,
}
short_range_distances = {
"minimum_distance": 0.5,
"maximum_distance": 2.0,
}
####################################
####################################
self.room = {
'defaults': {
'inside_temp': 293,
'humidity_with_heating': 0.3,
'humidity_without_heating': 0.5,
room = {
"defaults": {
"inside_temp": 293,
"humidity_with_heating": 0.3,
"humidity_without_heating": 0.5,
},
}
ventilation = {
"natural": {
"discharge_factor": {
"sliding": 0.6,
},
}
self.ventilation = {
'natural': {
'discharge_factor': {
'sliding': 0.6,
},
},
"infiltration_ventilation": 0.25,
}
particle = {
"evaporation_factor": 0.3,
}
population_with_virus = {
"fraction_of_infectious_virus": 1,
}
concentration_model = {
"min_background_concentration": 0.0,
"CO2_concentration_model": {
"CO2_atmosphere_concentration": 440.44,
"CO2_fraction_exhaled": 0.042,
},
}
short_range_model = {
"dilution_factor": {
"mouth_diameter": 0.02,
"exhalation_coefficient": 2,
"tstar": 2,
"penetration_coefficients": {
"𝛽r1": 0.18,
"𝛽r2": 0.2,
"𝛽x1": 2.4,
},
'infiltration_ventilation': 0.25,
}
self.particle = {
'evaporation_factor': 0.3,
}
self.population_with_virus = {
'fraction_of_infectious_virus': 1,
}
self.concentration_model = {
'min_background_concentration': 0.,
'CO2_concentration_model': {
'CO2_atmosphere_concentration': 440.44,
'CO2_fraction_exhaled': 0.042,
},
}
self.short_range_model = {
'dilution_factor': {
'mouth_diameter': 0.02,
'exhalation_coefficient': 2,
'tstar': 2,
'penetration_coefficients': {
'𝛽r1': 0.18, '𝛽r2': .2, '𝛽x1': 2.4,
},
},
}
self.exposure_model = {
'repeats': 1,
}
self.conditional_prob_inf_given_viral_load = {
'lower_percentile': 0.05,
'upper_percentile': 0.95,
'min_vl': 2,
'max_vl': 10,
}
self.monte_carlo_sample_size = 250000
self.population_scenario_activity = {
'office': {'activity': 'Seated', 'expiration': {'Speaking': 1, 'Breathing': 2}},
'smallmeeting': {'activity': 'Seated', 'expiration': {'Speaking': 1, 'Breathing': None}},
'largemeeting': {'activity': 'Standing', 'expiration': {'Speaking': 1, 'Breathing': 2}},
'callcenter': {'activity': 'Seated', 'expiration': {'Speaking': 1}},
'controlroom-day': {'activity': 'Seated', 'expiration': {'Speaking': 1, 'Breathing': 1}},
'controlroom-night': {'activity': 'Seated', 'expiration': {'Speaking': 1, 'Breathing': 9}},
'library': {'activity': 'Seated', 'expiration': {'Breathing': 1}},
'lab': {'activity': 'Light activity', 'expiration': {'Speaking': 1, 'Breathing': 1}},
'workshop': {'activity': 'Moderate activity', 'expiration': {'Speaking': 1, 'Breathing': 1}},
'training': {'activity': 'Standing', 'expiration': {'Speaking': 1}},
'training_attendee': {'activity': 'Seated', 'expiration': {'Breathing': 1}},
'gym': {'activity': 'Heavy exercise', 'expiration': {'Breathing': 1}},
'household-day': {'activity': 'Light activity', 'expiration': {'Breathing': 5, 'Speaking': 5}},
'household-night': {'activity': 'Seated', 'expiration': {'Breathing': 7, 'Speaking': 3}},
'primary-school': {'activity': 'Light activity', 'expiration': {'Breathing': 5, 'Speaking': 5}},
'secondary-school': {'activity': 'Light activity', 'expiration': {'Breathing': 7, 'Speaking': 3}},
'university': {'activity': 'Seated', 'expiration': {'Breathing': 9, 'Speaking': 1}},
'restaurant': {'activity': 'Seated', 'expiration': {'Breathing': 1, 'Speaking': 9}},
'precise': {'activity': None, 'expiration': None},
}
},
}
exposure_model = {
"repeats": 1,
}
conditional_prob_inf_given_viral_load = {
"lower_percentile": 0.05,
"upper_percentile": 0.95,
"min_vl": 2,
"max_vl": 10,
}
monte_carlo_sample_size = 250000
population_scenario_activity = {
"office": {"activity": "Seated", "expiration": {"Speaking": 1, "Breathing": 2}},
"smallmeeting": {
"activity": "Seated",
"expiration": {"Speaking": 1, "Breathing": None},
},
"largemeeting": {
"activity": "Standing",
"expiration": {"Speaking": 1, "Breathing": 2},
},
"callcenter": {"activity": "Seated", "expiration": {"Speaking": 1}},
"controlroom-day": {
"activity": "Seated",
"expiration": {"Speaking": 1, "Breathing": 1},
},
"controlroom-night": {
"activity": "Seated",
"expiration": {"Speaking": 1, "Breathing": 9},
},
"library": {"activity": "Seated", "expiration": {"Breathing": 1}},
"lab": {
"activity": "Light activity",
"expiration": {"Speaking": 1, "Breathing": 1},
},
"workshop": {
"activity": "Moderate activity",
"expiration": {"Speaking": 1, "Breathing": 1},
},
"training": {"activity": "Standing", "expiration": {"Speaking": 1}},
"training_attendee": {"activity": "Seated", "expiration": {"Breathing": 1}},
"gym": {"activity": "Heavy exercise", "expiration": {"Breathing": 1}},
"household-day": {
"activity": "Light activity",
"expiration": {"Breathing": 5, "Speaking": 5},
},
"household-night": {
"activity": "Seated",
"expiration": {"Breathing": 7, "Speaking": 3},
},
"primary-school": {
"activity": "Light activity",
"expiration": {"Breathing": 5, "Speaking": 5},
},
"secondary-school": {
"activity": "Light activity",
"expiration": {"Breathing": 7, "Speaking": 3},
},
"university": {
"activity": "Seated",
"expiration": {"Breathing": 9, "Speaking": 1},
},
"restaurant": {
"activity": "Seated",
"expiration": {"Breathing": 1, "Speaking": 9},
},
"precise": {"activity": None, "expiration": None},
}
async def populate_data(self):
"""
Fetches data from the API and populates the configuration object.
"""
if not self.data_fetched and os.environ.get('DATA_SERVICE_ENABLED', 'False').lower() == 'true':
# Fetch and populate data from API
await GlobalStore.populate_from_api()
data = GlobalStore.get_data()['data']
# Dynamically set attributes based on the data fetched from the API
for attr_name, value in data.items():
setattr(self, attr_name, value)
self.data_fetched = True
return
def update(self, data):
"""Update local cache with data provided as argument."""
for attr_name, value in data.items():
setattr(self, attr_name, value)
# module-level variable as a form of singleton
config = Configuration()
asyncio.run(config.populate_data())

View file

@ -1,74 +1,95 @@
import dataclasses
import json
import logging
import os
import typing
from tornado.httpclient import AsyncHTTPClient, HTTPRequest
import requests
LOG = logging.getLogger(__name__)
from .configuration import config
logger = logging.getLogger(__name__)
@dataclasses.dataclass
class DataService():
'''
Responsible for establishing a connection to a
database through a REST API by handling authentication
and fetching data. It utilizes the Tornado web framework
for asynchronous HTTP requests.
'''
# Credentials used for authentication
credentials: dict
# Host URL for the CAiMIRA Data Service API
host: str = 'https://caimira-data-api.app.cern.ch'
class DataService:
"""Responsible for fetching data from the data service endpoint."""
# Cached access token
_access_token: typing.Optional[str] = None
def __init__(
self,
credentials: typing.Dict[str, str],
host: str = "https://caimira-data-api.app.cern.ch",
):
self._credentials = credentials
self._host = host
def _is_valid(self, access_token):
# decode access_token
# check validity
return False
async def _login(self):
def _login(self):
if self._is_valid(self._access_token):
return self._access_token
# invalid access_token, fetch it again
client_email = self.credentials["data_service_client_email"]
client_password = self.credentials['data_service_client_password']
client_email = self._credentials["email"]
client_password = self._credentials["password"]
if (client_email == None or client_password == None):
if client_email == None or client_password == None:
# If the credentials are not defined, an exception is raised.
raise Exception("DataService credentials not set")
http_client = AsyncHTTPClient()
headers = {'Content-type': 'application/json'}
json_body = {"email": f"{client_email}",
"password": f"{client_password}"}
url = f"{self._host}/login"
headers = {"Content-Type": "application/json"}
json_body = dict(email=client_email, password=client_password)
response = await http_client.fetch(HTTPRequest(
url=self.host + '/login',
method='POST',
headers=headers,
body=json.dumps(json_body),
),
raise_error=True)
try:
response = requests.post(url, json=json_body, headers=headers)
response.raise_for_status()
if response.status_code == 200:
self._access_token = response.json()["access_token"]
return self._access_token
else:
logger.error(
f"Unexpected error on login. Response status code: {response.status_code}, body: f{response.text}"
)
except requests.exceptions.RequestException as e:
logger.exception(e)
self._access_token = json.loads(response.body)['access_token']
return self._access_token
def fetch(self):
access_token = self._login()
async def fetch(self):
access_token = await self._login()
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}
url = f"{self._host}/data"
http_client = AsyncHTTPClient()
headers = {'Authorization': f'Bearer {access_token}'}
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
if response.status_code == 200:
return response.json()
else:
logger.error(
f"Unexpected error when fetching data. Response status code: {response.status_code}, body: f{response.text}"
)
except requests.exceptions.RequestException as e:
logger.exception(e)
response = await http_client.fetch(HTTPRequest(
url=self.host + '/data',
method='GET',
headers=headers,
),
raise_error=True)
return json.loads(response.body)
def update_configuration():
data_service_enabled = os.environ.get("DATA_SERVICE_ENABLED", "False")
is_enabled = data_service_enabled.lower() == "true"
if is_enabled:
credentials = {
"email": os.environ.get("DATA_SERVICE_CLIENT_EMAIL", None),
"password": os.environ.get("DATA_SERVICE_CLIENT_PASSWORD", None),
}
data_service = DataService(credentials)
data = data_service.fetch()
if data:
config.update(data["data"])
else:
logger.error("Could not fetch fresh data from the data service.")

View file

@ -1,43 +0,0 @@
import os
import logging
from caimira.store.data_service import DataService
LOG = logging.getLogger(__name__)
class GlobalStore:
'''
Singleton pattern - ensure that there's only one instance of
GlobalStore throughout the application
'''
_instance = None
def __new__(self):
if self._instance is None:
self._instance = super().__new__(self)
self._instance = {}
return self._instance
@classmethod
async def populate_from_api(self):
data_service_credentials = {
'data_service_client_email': os.environ.get('DATA_SERVICE_CLIENT_EMAIL', None),
'data_service_client_password': os.environ.get('DATA_SERVICE_CLIENT_PASSWORD', None),
}
data_service = None
data_service_enabled = os.environ.get(
'DATA_SERVICE_ENABLED', 'False').lower() == 'true'
if data_service_enabled:
try:
data_service = DataService(data_service_credentials)
self._instance = await data_service.fetch()
except Exception as err:
error_message = f"Something went wrong with the data service: {str(err)}"
LOG.error(error_message, exc_info=True)
@classmethod
def get_data(self):
return self._instance

View file

@ -1,87 +1,81 @@
from dataclasses import dataclass
import unittest
from unittest.mock import patch, MagicMock
from tornado.httpclient import HTTPError
from unittest.mock import Mock, patch
from caimira.store.data_service import DataService
@dataclass
class MockResponse:
body: str
class DataServiceTests(unittest.TestCase):
def setUp(self):
# Set up any necessary test data or configurations
self.credentials = {
"data_service_client_email": "test@example.com",
"data_service_client_password": "password123"
}
self.credentials = {"email": "test@example.com", "password": "password123"}
self.data_service = DataService(self.credentials)
@patch('caimira.apps.calculator.data_service.AsyncHTTPClient')
async def test_login_successful(self, mock_http_client):
@patch("requests.post")
def test_login_successful(self, mock_post):
# Mock successful login response
mock_response = MockResponse('{"access_token": "dummy_token"}')
mock_fetch = MagicMock(return_value=mock_response)
mock_http_client.return_value.fetch = mock_fetch
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"access_token": "dummy_token"}
mock_post.return_value = mock_response
# Call the login method
access_token = await self.data_service._login()
access_token = self.data_service._login()
# Assert that the access token is returned correctly
self.assertEqual(access_token, "dummy_token")
# Verify that the fetch method was called with the expected arguments
mock_fetch.assert_called_once_with(
url='https://caimira-data-api.app.cern.ch/login',
method='POST',
headers={'Content-type': 'application/json'},
body='{"email": "test@example.com", "password": "password123"}'
mock_post.assert_called_once_with(
"https://caimira-data-api.app.cern.ch/login",
json=dict(email="test@example.com", password="password123"),
headers={"Content-Type": "application/json"},
)
@patch('caimira.apps.calculator.data_service.AsyncHTTPClient')
async def test_login_error(self, mock_http_client):
@patch("requests.post")
def test_login_error(self, mock_post):
# Mock login error response
mock_fetch = MagicMock(side_effect=HTTPError(500))
mock_http_client.return_value.fetch = mock_fetch
mock_post.return_value = Mock()
mock_post.return_value.status_code = 500
# Call the login method
access_token = await self.data_service.login()
access_token = self.data_service._login()
# Assert that the login method returns None in case of an error
self.assertIsNone(access_token)
@patch('caimira.apps.calculator.data_service.AsyncHTTPClient')
async def test_fetch_successful(self, mock_http_client):
@patch("requests.get")
@patch.object(DataService, "_login")
def test_fetch_successful(self, mock_login, mock_get):
# Mock successful fetch response
mock_response = MockResponse('{"data": "dummy_data"}')
mock_fetch = MagicMock(return_value=mock_response)
mock_http_client.return_value.fetch = mock_fetch
mock_get.return_value = Mock()
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"data": "dummy_data"}
# Call the fetch method with a mock access token
self.data_service._access_token = "dummy_token"
data = await self.data_service.fetch()
mock_login.return_value = "dummy_token"
data = self.data_service.fetch()
# Assert that the data is returned correctly
self.assertEqual(data, {"data": "dummy_data"})
# Verify that the fetch method was called with the expected arguments
mock_fetch.assert_called_once_with(
url='https://caimira-data-api.app.cern.ch/data',
method='GET',
headers={'Authorization': 'Bearer dummy_token'}
mock_get.assert_called_once_with(
"https://caimira-data-api.app.cern.ch/data",
headers={
"Authorization": "Bearer dummy_token",
"Content-Type": "application/json",
},
)
@patch('caimira.apps.calculator.data_service.AsyncHTTPClient')
async def test_fetch_error(self, mock_http_client):
@patch("requests.get")
@patch.object(DataService, "_login")
def test_fetch_error(self, mock_login, mock_get):
# Mock fetch error response
mock_fetch = MagicMock(side_effect=HTTPError(404))
mock_http_client.return_value.fetch = mock_fetch
mock_get.return_value = Mock()
mock_get.return_value.status_code = 500
# Call the fetch method with a mock access token
self.data_service._access_token = "dummy_token"
data = await self.data_service.fetch()
mock_login.return_value = "dummy_token"
data = self.data_service.fetch()
# Assert that the fetch method returns None in case of an error
self.assertIsNone(data)