diff --git a/caimira/store/configuration.py b/caimira/store/configuration.py index cc791140..e1272c5c 100644 --- a/caimira/store/configuration.py +++ b/caimira/store/configuration.py @@ -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, + "exhalation_rate": { + "associated_distribution": "Numpy Log-normal Distribution (random.lognormal)", + "parameters": { + "mean_gaussian": 0.21380242785625422, + "standard_deviation_gaussian": 0.09435378091059601, + }, }, - } - self.mask_distributions = { - 'Type I': { - 'η_inhale': { - 'associated_distribution': 'Numpy Uniform Distribution (random.uniform)', - 'parameters': { - 'low': 0.25, - 'high': 0.80, - }, + }, + "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, }, - 'FFP2': { - 'η_inhale': { - 'associated_distribution': 'Numpy Uniform Distribution (random.uniform)', - 'parameters': { - 'low': 0.83, - 'high': 0.91, - }, + "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?': 'No', - 'factor_exhale': 1, }, - 'Cloth': { - 'η_inhale': { - 'associated_distribution': 'Numpy Uniform Distribution (random.uniform)', - 'parameters': { - 'low': 0.05, - 'high': 0.40, - }, + }, + "Heavy exercise": { + "inhalation_rate": { + "associated_distribution": "Numpy Log-normal Distribution (random.lognormal)", + "parameters": { + "mean_gaussian": 1.1644665696723049, + "standard_deviation_gaussian": 0.21744554768657565, }, - 'Known filtration efficiency of masks when exhaling?': 'Yes', - 'η_exhale': { - 'associated_distribution': 'Numpy Uniform Distribution (random.uniform)', - 'parameters': { - 'low': 0.20, - 'high': 0.50, - }, + }, + "exhalation_rate": { + "associated_distribution": "Numpy Log-normal Distribution (random.lognormal)", + "parameters": { + "mean_gaussian": 1.1644665696723049, + "standard_deviation_gaussian": 0.21744554768657565, }, - '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., - } - - #################################### - - self.room = { - 'defaults': { - 'inside_temp': 293, - 'humidity_with_heating': 0.3, - 'humidity_without_heating': 0.5, + }, + } + 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, + }, }, - } - self.ventilation = { - 'natural': { - 'discharge_factor': { - 'sliding': 0.6, + "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, }, }, - '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, + "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, + }, }, - } - 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, + "Known filtration efficiency of masks when exhaling?": "Yes", + "η_exhale": { + "associated_distribution": "Numpy Uniform Distribution (random.uniform)", + "parameters": { + "low": 0.20, + "high": 0.50, }, }, - } - 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}, - } + "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, + } - 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) + room = { + "defaults": { + "inside_temp": 293, + "humidity_with_heating": 0.3, + "humidity_without_heating": 0.5, + }, + } + 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, + }, + }, + } + 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}, + } - 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()) diff --git a/caimira/store/data_service.py b/caimira/store/data_service.py index 703884b8..e203be48 100644 --- a/caimira/store/data_service.py +++ b/caimira/store/data_service.py @@ -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}"} - - response = await http_client.fetch(HTTPRequest( - url=self.host + '/login', - method='POST', - headers=headers, - body=json.dumps(json_body), - ), - raise_error=True) - - self._access_token = json.loads(response.body)['access_token'] - return self._access_token - - async def fetch(self): - access_token = await self._login() - - http_client = AsyncHTTPClient() - headers = {'Authorization': f'Bearer {access_token}'} - - response = await http_client.fetch(HTTPRequest( - url=self.host + '/data', - method='GET', - headers=headers, - ), - raise_error=True) - - return json.loads(response.body) + url = f"{self._host}/login" + headers = {"Content-Type": "application/json"} + json_body = dict(email=client_email, password=client_password) + + 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) + + def fetch(self): + access_token = self._login() + + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json", + } + url = f"{self._host}/data" + + 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) + + +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.") diff --git a/caimira/store/global_store.py b/caimira/store/global_store.py deleted file mode 100644 index 164af849..00000000 --- a/caimira/store/global_store.py +++ /dev/null @@ -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 diff --git a/caimira/tests/test_data_service.py b/caimira/tests/test_data_service.py index 29c8b9e6..9e0189e7 100644 --- a/caimira/tests/test_data_service.py +++ b/caimira/tests/test_data_service.py @@ -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)