From fdc0bf8125c22dbdb6fc168e3334acda92c8eb1a Mon Sep 17 00:00:00 2001 From: Tim Lorsbach Date: Tue, 17 Mar 2026 07:54:19 +0100 Subject: [PATCH 1/6] Mimic ReferringScenarios --- enviPath_python/objects.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/enviPath_python/objects.py b/enviPath_python/objects.py index a9fc3bd..0642b0c 100644 --- a/enviPath_python/objects.py +++ b/enviPath_python/objects.py @@ -785,6 +785,10 @@ def create(package: Package, name: str = None, description: str = None, date: st if scenariotype: scenario_payload['type'] = scenariotype.capitalize() if referring_scenario_id: + + if package.requester.new_api is not None and package.requester.new_api: + raise ValueError(f"To add 'ReferringScenarios' call Scenario.add_referring() instead of Scenario.create()!") + scenario_payload['addReferring'] = 'true' scenario_payload['referringScenario'] = referring_scenario_id if collection_URI: @@ -800,6 +804,34 @@ def create(package: Package, name: str = None, description: str = None, date: st else: return Scenario(package.requester, id=res.json()['scenarioLocation']) + def add_referring(self, additional_information: List['AdditionalInformation'], attach_object_id: str | None): + + if len(additional_information) == 0: + raise ValueError("At least one additional information object is required!") + + payload = { + "scenario": self.id + } + + payload["adInfoTypes[]"] = ",".join([ai.name for ai in additional_information]) + + for ai in additional_information: + # Will raise an error if invalid + ai.validate() + payload.update(**ai.params) + + + if attach_object_id: + payload["attach_obj"] = attach_object_id + + package_id = self.id.split("/scenario")[0] + url = "{}/{}".format(package_id, "additional-information") + res = self.requester.post_request( + url, payload=payload, allow_redirects=False + ) + res.raise_for_status() + return Scenario(self.requester, id=self.id) + def update_scenario(self, additional_information: List['AdditionalInformation']): """ Updates an existing scenario From 65ed3f301d492a2f6d5b0a049e81d283a500afd8 Mon Sep 17 00:00:00 2001 From: Tim Lorsbach Date: Wed, 6 May 2026 20:43:30 +0200 Subject: [PATCH 2/6] Add get/set model --- enviPath_python/objects.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/enviPath_python/objects.py b/enviPath_python/objects.py index 0642b0c..326c9d5 100644 --- a/enviPath_python/objects.py +++ b/enviPath_python/objects.py @@ -4530,8 +4530,21 @@ class HalfLifeAdditionalInformation(AdditionalInformation): name = "halflife" mandatories = ['lower', 'upper'] allowed_values = ['', 'reported', 'self-calculated', 'neither'] + allowed_models = ["SFO", "FOMC", "DFOP", "HS", "SFO-SFO", "DFOP-SFO", "FOMC-DFOP", "HS-SFO", "other"] # Setter + def set_model(self, value): + """ + Sets the model of the half-life. + + :param value: The model, one of "SFO", "FOMC", "DFOP", "HS", "SFO-SFO", "DFOP-SFO", "FOMC-DFOP", "HS-SFO", "other". + :type value: str + """ + if value.upper() not in self.allowed_models: + raise ValueError(f"{value.upper()} is not an allowed model values. Allowed values are {self.allowed_models}") + + self.params["model"] = value.upper() + def set_lower(self, value): """ Sets the lower bound of the half-life. @@ -4589,6 +4602,15 @@ def set_fit(self, value): self.params["fit"] = value # Getter + def get_model(self): + """ + Retrieves the model of the half-life. + + :return: The model of the half-life if set; otherwise, None. + :rtype: str + """ + return self.params.get("model", None) + def get_lower(self): """ Retrieves the lower bound of the half-life. @@ -4659,6 +4681,7 @@ def parse(cls, data_string): dt50 = parts[3] res = { 'firstOrder': True if parts[0] == 'SFO' else False, + 'model': parts[0], 'fit': parts[1], 'comment': parts[2], 'lower': float(dt50.split(' - ')[0]), From cc47430ac53c9bdc4423696817633d72fc78cd06 Mon Sep 17 00:00:00 2001 From: Tim Lorsbach Date: Wed, 6 May 2026 20:48:42 +0200 Subject: [PATCH 3/6] harmonize with model_ws --- enviPath_python/objects.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/enviPath_python/objects.py b/enviPath_python/objects.py index 326c9d5..b397f46 100644 --- a/enviPath_python/objects.py +++ b/enviPath_python/objects.py @@ -4701,6 +4701,7 @@ class HalfLifeWaterSedimentAdditionalInformation(AdditionalInformation): name = "halflife_ws" mandatories = ["total_low", "total_high"] allowed_values = ['', 'reported', 'self-calculated', 'neither'] + allowed_models = ["SFO", "FOMC", "DFOP", "HS", "SFO-SFO", "DFOP-SFO", "FOMC-DFOP", "HS-SFO", "other"] # Setter def set_total_low(self, value): @@ -4768,12 +4769,15 @@ def set_fit_ws(self, value): def set_model_ws(self, value): """ - Sets the model used for water and sediment half-life estimation. + Sets the model of the half-life. - :param value: The model used for water and sediment half-life estimation. + :param value: The model, one of "SFO", "FOMC", "DFOP", "HS", "SFO-SFO", "DFOP-SFO", "FOMC-DFOP", "HS-SFO", "other". :type value: str """ - self.params["model_ws"] = value + if value.upper() not in self.allowed_models: + raise ValueError(f"{value.upper()} is not an allowed model values. Allowed values are {self.allowed_models}") + + self.params["model_ws"] = value.upper() def set_comment_ws(self, value): """ From c308e58f467a2658074efe132e002d16b0a0f0d8 Mon Sep 17 00:00:00 2001 From: Tim Lorsbach Date: Wed, 27 May 2026 15:28:27 +0200 Subject: [PATCH 4/6] enable copy --- enviPath_python/objects.py | 204 +++++++++++++++++++++++-------------- 1 file changed, 130 insertions(+), 74 deletions(-) diff --git a/enviPath_python/objects.py b/enviPath_python/objects.py index b397f46..b44842e 100644 --- a/enviPath_python/objects.py +++ b/enviPath_python/objects.py @@ -19,7 +19,7 @@ from abc import ABC, abstractmethod from collections import namedtuple, defaultdict from io import BytesIO -from typing import List, Optional, Union +from typing import List, Optional, Union, Dict, Any from enviPath_python.enums import Endpoint, ClassifierType, FingerprinterType, AssociationType, EvaluationType, \ Permission @@ -665,6 +665,22 @@ def copy(self, target_package: 'Package', debug=False): if debug: print(' done') + if target_package.requester.eP.new_api is not None and target_package.requester.eP.new_api: + for scenario in self.get_scenarios(): + linking_ais = defaultdict(list) + collection = scenario._get('collection') + for ai_type, ais in collection.items(): + for ai in ais: + if ai["related"] is not None: + copy_ai = ai.copy() + copy_ai["related"]["url"] = id_mapping[ai["related"]["url"]] + linking_ais[ai_type].append(copy_ai) + + if len(linking_ais): + scen = Scenario(target_package.requester, id=id_mapping[scenario.get_id()]) + scen.update_scenario(linking_ais) + + @staticmethod def merge_packages(target: 'Package', sources: List['Package'], debug=False) -> None: """ @@ -696,11 +712,11 @@ def __init__(self, requester, *args, **kwargs): super().__init__(requester, *args, **kwargs) self.additional_information_list = [] self.warnings = [] - + def get_scenariotype(self): """ Returns the type of scenario - :return: + :return: """ return self._get("type") @@ -832,31 +848,46 @@ def add_referring(self, additional_information: List['AdditionalInformation'], a res.raise_for_status() return Scenario(self.requester, id=self.id) - def update_scenario(self, additional_information: List['AdditionalInformation']): + def update_scenario(self, additional_information: List['AdditionalInformation'] | Dict[str, List[Dict[str, Any]]]): """ Updates an existing scenario - :param additional_information: Scenario data content provided as a AdditionalInformation object + :param additional_information: Scenario data content provided as a AdditionalInformation object or Dict :return: The updated scenario :rtype: Scenario """ scenario_payload = {} - if additional_information: - self.loaded = False - scenario_payload['adInfoTypes[]'] = ','.join([ai.name for ai in additional_information]) - for ai in additional_information: - # Will raise an error if invalid - ai.validate() - scenario_payload.update(**ai.params) + if isinstance(additional_information, dict): + payload = { + "scenario": self.id, + "ais": json.dumps(additional_information) + } - scenario_payload['updateScenario'] = 'true' - scenario_payload['fullScenario'] = 'false' - scenario_payload['jsonredirect'] = 'false' + package_id = self.id.split("/scenario")[0] + url = "{}/{}".format(package_id, "additional-information") + res = self.requester.post_request( + url, payload=payload, allow_redirects=False + ) - res = self.requester.post_request(self.get_id(), payload=scenario_payload, allow_redirects=False) - res.raise_for_status() - return Scenario(self.requester, id=self.id) + res.raise_for_status() + return Scenario(self.requester, id=self.id) + else: + if additional_information: + self.loaded = False + scenario_payload['adInfoTypes[]'] = ','.join([ai.name for ai in additional_information]) + for ai in additional_information: + # Will raise an error if invalid + ai.validate() + scenario_payload.update(**ai.params) + + scenario_payload['updateScenario'] = 'true' + scenario_payload['fullScenario'] = 'false' + scenario_payload['jsonredirect'] = 'false' + + res = self.requester.post_request(self.get_id(), payload=scenario_payload, allow_redirects=False) + res.raise_for_status() + return Scenario(self.requester, id=self.id) def has_referring_scenario(self) -> bool: """ @@ -923,72 +954,97 @@ def copy(self, package: 'Package', debug=False, id_lookup={}) -> (dict, 'Scenari parent scenario to the referred one :return: a dictionary similar to `id_lookup` and the copied Scenario """ - mapping = dict() - - ais = self.get_additional_information() - ais_to_add = [] - # TODO make it pretty :S + mapping = dict() - if self.has_referring_scenario(): - ref_scenario = self.get_referring_scenario() - ref_ais = ref_scenario.get_additional_information() + # Create plain Scenario + date = self._get('date') + if date is None or date == 'no date': + date = None - # Assemble the ReferringScenarioAdditionalInformation by creating a new one with the adjusted id - ais_to_add.append( - ReferringScenarioAdditionalInformation(referringscenario=id_lookup[ref_scenario.get_id()])) + name = self.get_name() + # replaces " - (000XX)" with an empty string as this will be added by the server... + name = re.sub(r" - \(\d+\)$", '', name) + + # Check if request from target package is new API + if package.requester.eP.new_api is not None and package.requester.eP.new_api: + s = Scenario.create( + package, + name=name, + description=self.get_description(), + date=date, + scenariotype=self._get('type'), + ) + + # ais = {'BioReactor': [{'related': None, 'size': 10.0, 'type': 'afasfa'}]} + ais = self._get('collection') + + direct_ais = defaultdict(list) + for k, vals in ais.items(): + for v in vals: + print(v["related"]) + if v['related'] is None: + direct_ais[k].append(v) + + if len(direct_ais): + s.update_scenario(direct_ais) - for ai in ais: - present_in_ref = False - for ref_ai in ref_ais: - if ai.name == ref_ai.name: - if ai.params == ref_ai.params: - present_in_ref = True - # print("Wont add {} with params {} as its stored in ref".format(ai.name, ai.params)) - break + else: + ais = self.get_additional_information() + ais_to_add = [] - if not present_in_ref: - ais_to_add.append(ai) + # TODO make it pretty :S - else: - ais_to_add = ais + if self.has_referring_scenario(): + ref_scenario = self.get_referring_scenario() + ref_ais = ref_scenario.get_additional_information() - from collections import Counter - cnt = Counter([x.name for x in ais_to_add]) - post_poned_ais = [] - # check if multi ais such as acidity are present - for k, v in cnt.items(): - if v > 1: - for ai in ais_to_add: - post_poned_ais.append(ai) + # Assemble the ReferringScenarioAdditionalInformation by creating a new one with the adjusted id + ais_to_add.append( + ReferringScenarioAdditionalInformation(referringscenario=id_lookup[ref_scenario.get_id()])) - for ai in post_poned_ais: - ais_to_add.remove(ai) + for ai in ais: + present_in_ref = False + for ref_ai in ref_ais: + if ai.name == ref_ai.name: + if ai.params == ref_ai.params: + present_in_ref = True + # print("Wont add {} with params {} as its stored in ref".format(ai.name, ai.params)) + break - # Create plain Scenario - date = self._get('date') - if date is None or date == 'no date': - date = None + if not present_in_ref: + ais_to_add.append(ai) - name = self.get_name() - # replaces " - (000XX)" with an empty string as this will be added by the server... - name = re.sub(" - \(\d+\)$", '', name) - - # Create the copy! - s = Scenario.create(package, - name=name, - description=self.get_description(), - date=date, - scenariotype=self._get('type'), - additional_information=ais_to_add, - referring_scenario_id=None, - collection_URI=None, - ) - - # Add the remaining ones... - for ai in post_poned_ais: - # TODO check if list is the right choice... - s.update_scenario([ai]) + else: + ais_to_add = ais + + from collections import Counter + cnt = Counter([x.name for x in ais_to_add]) + post_poned_ais = [] + # check if multi ais such as acidity are present + for k, v in cnt.items(): + if v > 1: + for ai in ais_to_add: + post_poned_ais.append(ai) + + for ai in post_poned_ais: + ais_to_add.remove(ai) + + # Create the copy! + s = Scenario.create(package, + name=name, + description=self.get_description(), + date=date, + scenariotype=self._get('type'), + additional_information=ais_to_add, + referring_scenario_id=None, + collection_URI=None, + ) + + # Add the remaining ones... + for ai in post_poned_ais: + # TODO check if list is the right choice... + s.update_scenario([ai]) mapping[self.get_id()] = s.get_id() From 39eede38b30d694ddad549473eb557f44d649426 Mon Sep 17 00:00:00 2001 From: Tim Lorsbach Date: Thu, 28 May 2026 00:01:39 +0200 Subject: [PATCH 5/6] ... --- enviPath_python/objects.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/enviPath_python/objects.py b/enviPath_python/objects.py index b44842e..adde1ef 100644 --- a/enviPath_python/objects.py +++ b/enviPath_python/objects.py @@ -781,7 +781,7 @@ def create(package: Package, name: str = None, description: str = None, date: st scenario_payload['studyname'] = name if description: scenario_payload['studydescription'] = description - if date: + if date and date != "No date": if len(date.split('-')) == 3: scenario_payload['dateYear'] = date.split('-')[0] scenario_payload['dateMonth'] = date.split('-')[1] @@ -982,7 +982,6 @@ def copy(self, package: 'Package', debug=False, id_lookup={}) -> (dict, 'Scenari direct_ais = defaultdict(list) for k, vals in ais.items(): for v in vals: - print(v["related"]) if v['related'] is None: direct_ais[k].append(v) From e36a03ca72f848526e68c997a30f911cb4e01029 Mon Sep 17 00:00:00 2001 From: Tim Lorsbach Date: Thu, 28 May 2026 13:55:42 +0200 Subject: [PATCH 6/6] ... --- enviPath_python/objects.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/enviPath_python/objects.py b/enviPath_python/objects.py index adde1ef..057b188 100644 --- a/enviPath_python/objects.py +++ b/enviPath_python/objects.py @@ -7461,3 +7461,26 @@ def parse(cls, data_string): :rtype: SedimentPorosityAdditionalInformation """ return cls._parse_default(data_string, ['sedimentporosity']) + + +class PFASConfidence(AdditionalInformation): + name = "pfasconfidence" + mandatories = ["level"] + + def set_level(self, level): + self.params["level"] = level + + def get_level(self, level): + return self.params.get("level") + + @classmethod + def parse(cls, data_string): + """ + Parses the data_string to create a SedimentPorosityAdditionalInformation instance. + + :param data_string: String containing sediment porosity data. + :type data_string: str + :return: SedimentPorosityAdditionalInformation instance. + :rtype: SedimentPorosityAdditionalInformation + """ + return cls._parse_default(data_string, ['level'])