From 9dbf52319060d34c4f2a3fa69bf5f6523012c93fcaeb6ba943338e2307c2f9d1 Mon Sep 17 00:00:00 2001 From: PioApocalypse Date: Fri, 6 Feb 2026 16:56:50 +0100 Subject: [PATCH] huge improvements, read below * function get_entry_from_elabid moved to the top to be used by classes * class Layers renamed Layer * class Layer expanded and completed with all NeXus-needed metadata * classes improved with "quality" error management * class Material added, for now it fetches just compound_elabid and compund chemical formula (as attribute and method respectively) * class Material has get_compound() method which needs some commenting * DEBUG MODE improved to cover all cases: layer, sample, substr. and pld target; creates a new shortcut for prompting (read line 147) * removed herobrine (fuck i'm old aren't i) --- src/chained_requests.py | 184 ++++++++++++++++++++++++++++++---------- 1 file changed, 140 insertions(+), 44 deletions(-) diff --git a/src/chained_requests.py b/src/chained_requests.py index 706b1d0..afede67 100644 --- a/src/chained_requests.py +++ b/src/chained_requests.py @@ -2,45 +2,6 @@ import os, json, requests from getpass import getpass from classes import Header -class Layers: - ''' - Layers(layer_data) - where layer_data is a Python dictionary. - - eLabFTW experiments contain most of the data required by the NeXus file - although every layer is on a different eLab entry; unfortunately, some data like the target's chemical formula must be retrieved through additional HTTP requests. Attributes 'target_elabid', 'rheed_system_elabid' and 'laser_system_elabid' contain elabid's for these resources, which are all items. - ''' - def __init__(self, layer_data): - try: - self.extra = layer_data["metadata_decoded"]["extra_fields"] - self.target_elabid = self.extra["Target"]["value"] - self.start_time = layer_data.get("created_at") - self.operator = layer_data.get("fullname") - self.description = layer_data.get("body") - self.deposition_time = self.extra["Duration"]["value"] - self.repetition_rate = self.extra["Repetition rate"]["value"] - self.number_of_pulses = float(self.deposition_time) * float(self.repetition_rate) - # TO-DO: remove trailing space on eLabFTW's template for deposition layers - self.temperature = self.extra["Heater temperature "]["value"] - self.heating_method = self.extra["Heating Method"]["value"] - except KeyError as k: - raise KeyError(f"The provided dictionary lacks a \"{k}\" key.") - -class Entrypoint: - ''' - Entrypoint(sample_data) - where sample_data is a Python dictionary. - - The entrypoint is the starting point of the process of resolving the data chain. The entrypoint must be a dictionary containing the data of a sample, created directly from the JSON of the item endpoint on eLabFTW - which can be done through the function get_entry_from_elabid. - ''' - def __init__(self, sample_data): - try: - self.extra = sample_data["metadata_decoded"]["extra_fields"] - self.linked_items = sample_data["items_links"] - self.batch_elabid = self.extra["Substrate batch"]["value"] - self.linked_experiments = sample_data["related_experiments_links"] - self.linked_experiments_elabid = [ i["entityid"] for i in self.linked_experiments ] - except KeyError as k: - raise KeyError(f"The provided dictionary lacks a \"{k}\" key.") - - def get_entry_from_elabid(elabid, entryType="items"): ''' Function which returns entrypoint data (as dictionary) from its elabid. @@ -57,13 +18,140 @@ def get_entry_from_elabid(elabid, entryType="items"): else: raise ConnectionError(f"HTTP request failed with status code: {response.status_code}.") + + +class Layer: + ''' + Layer(layer_data) - where layer_data is a Python dictionary. + + eLabFTW experiments contain most of the data required by the NeXus file - although every layer is on a different eLab entry; + unfortunately, some data like the target's chemical formula must be retrieved through additional HTTP requests. + Attributes 'target_elabid', 'rheed_system_elabid' and 'laser_system_elabid' contain elabid's for these resources, which are all items. + ''' + def __init__(self, layer_data): + try: + self.extra = layer_data["metadata_decoded"]["extra_fields"] + self.target_elabid = self.extra["Target"]["value"] # elabid + self.rheed_system_elabid = self.extra["RHEED System"]["value"] # elabid + self.laser_system_elabid = self.extra["Laser System"]["value"] # elabid + self.start_time = layer_data.get("created_at") + self.operator = layer_data.get("fullname") + self.description = layer_data.get("body") + self.deposition_time = self.extra["Duration"]["value"] + self.repetition_rate = self.extra["Repetition rate"]["value"] + try: + self.number_of_pulses = float(self.deposition_time) * float(self.repetition_rate) + except ValueError: + # Since number_of_pulses is required, if it can't be calculated raise error: + raise ValueError(""" + Warning: either Duration or Repetition Rate are empty or invalid. + If you think this is an error, please edit your eLabFTW entry and retry. + Setting Number of Pulses to NoneType. + """) + # TO-DO: remove trailing space on eLabFTW's template for deposition layers + self.temperature = self.extra["Heater temperature "]["value"] # TYPO: trailing space, must fix on elabftw + self.process_pressure = self.extra["Process pressure "]["value"] # TYPO: trailing space, must fix on elabftw + # + self.heating_method = self.extra["Heating Method"]["value"] + self.layer_thickness = self.extra["Thickness"]["value"] + self.buffer_gas = self.extra["Buffer gas"]["value"] + self.heater_target_distance = self.extra["Heater-target distance"]["value"] + self.laser_fluence = self.extra["Laser Intensity"]["value"] # here fluence = intensity + self.laser_spot_area = self.extra["Spot Area"]["value"] + try: + self.laser_energy = float(self.laser_fluence) * float(self.laser_spot_area) + except ValueError: + # Since laser_energy is NOT required, if it can't be calculated warn user but allow the software to continue execution: + print(""" + Warning: either Laser Intensity or Spot Area are empty or invalid. + If you think this is an error, please edit your eLabFTW entry and retry. + Setting Laser Energy to NoneType. + """) + # Placeholder + self.laser_energy = None + # Laser rasternig section + self.laser_rastering_geometry = self.extra["Laser Rastering Geometry"]["value"] + self.laser_rastering_positions = self.extra["Laser Rastering Position"]["value"] + self.laser_rastering_velocities = self.extra["Laser Rastering Speed"]["value"] + # Pre annealing section + self.pre_annealing_ambient_gas = self.extra["Buffer gas Pre"]["value"] + self.pre_annealing_pressure = self.extra["Process pressure Pre"]["value"] + self.pre_annealing_temperature = self.extra["Heater temperature Pre"]["value"] + self.pre_annealing_duration = self.extra["Duration Pre"]["value"] + # Post annealing section + self.post_annealing_ambient_gas = self.extra["Buffer gas PA"]["value"] + self.post_annealing_pressure = self.extra["Process pressure PA"]["value"] + self.post_annealing_temperature = self.extra["Heater temperature PA"]["value"] + self.post_annealing_duration = self.extra["Duration PA"]["value"] + # Rejected but suggested by the NeXus standard: + #self.laser_rastering_coefficients = None + except KeyError as k: + # Some keys are not required and can be called through the .get() method - which is permissive and allows null values; + # Other keys are required so if they can't be called (invalid or null) raise error and stop execution of the program: + raise KeyError(f"The provided dictionary lacks a \"{k}\" key. Check the deposition layer entry on eLabFTW and make sure you used the correct Experiment template.") + +class Entrypoint: + ''' + Entrypoint(sample_data) - where sample_data is a Python dictionary. + + The entrypoint is the starting point of the process of resolving the data chain. + The entrypoint must be a dictionary containing the data of a sample, created directly from the JSON of the item endpoint on eLabFTW - which can be done through the function get_entry_from_elabid. + ''' + def __init__(self, sample_data): + try: + self.extra = sample_data["metadata_decoded"]["extra_fields"] + self.linked_items = sample_data["items_links"] + self.batch_elabid = self.extra["Substrate batch"]["value"] + self.linked_experiments = sample_data["related_experiments_links"] + self.linked_experiments_elabid = [ i["entityid"] for i in self.linked_experiments ] + except KeyError as k: + # Some keys are not required and can be called through the .get() method - which is permissive and allows null values; + # Other keys are required so if they can't be called (invalid or null) raise error and stop execution of the program: + raise KeyError(f"The provided dictionary lacks a \"{k}\" key. Check the sample entry on eLabFTW and make sure you used the correct Resource template.") + # Non-required attributes: + self.name = sample_data.get("title") or None # error prevention is more important than preventing empty fields here + +class Material: + ''' + Material(material_data) - where material_data is a Python dictionary. + + Both a PLD Target and a Substrate are materials made of a certain compound, of which we want to know: + * Name and formula; + * Shape and dimensions; + * Misc. + ''' + def __init__(self, material_data): + try: + self.extra = material_data["metadata_decoded"]["extra_fields"] + self.compound_elabid = self.extra["Compound"]["value"] + except KeyError as k: + # Some keys are not required and can be called through the .get() method - which is permissive and allows null values; + # Other keys are required so if they can't be called (invalid or null) raise error and stop execution of the program: + raise KeyError(f"The provided dictionary lacks a \"{k}\" key. Check the target/substrate entry on eLabFTW and make sure you used the correct Resource template.") + def get_compound(self): + compound_data = get_entry_from_elabid(self.compound_elabid, entryType="items") + formula = compound_data["metadata_decoded"]["extra_fields"].get("Chemical formula") + formula_value = formula.get("value") + return formula_value + + + if __name__=="__main__": - print("===== DEBUG MODE! =====") + print(f"=======================\n===== DEBUG MODE! =====\n=======================\n") ELABFTW_API_URL = "https://elabftw.fisica.unina.it/api/v2" apikey = getpass("Paste API key here: ") + # TEST. In production the entryType will probably just be "items" since the entrypoint is an item (sample). entryType = None while entryType not in ["items", "experiments"]: eT = input("Enter a valid entry type [items, experiments]: ") + # This allows for a shortcut: instead of prompting the type before and the elabid after I can just prompt both at the same time - e.g. e51 is exp. 51, i1108 is item 1108... + if eT[0] in ["e", "i"] and eT[-1].isnumeric(): + try: + elabid = int(eT[1:]) + eT = eT[0] + except Exception: + print("Usage: i|item|items|i[ELABID] for items, e|experiment|experiments|e[ELABID] for experiments.") + pass match eT: case "items" | "i" | "item": entryType = "items" @@ -71,16 +159,24 @@ if __name__=="__main__": entryType = "experiments" case _: pass - elabid = input("Input elabid here [default = 1108]: ") or 1108 + # This will probably be reworked in production + try: + elabid = elabid + except NameError: + elabid = input("Input elabid here [default = 1108]: ") or 1108 data = get_entry_from_elabid(elabid, entryType) if entryType == "experiments": - layer = Layers(data) + layer = Layer(data) result = layer.__dict__ result.pop("extra") print(result) elif entryType == "items": - sample = Entrypoint(data) - result = sample.__dict__ + if data.get("category_title") == "Sample": + item = Entrypoint(data) + elif data.get("category_title") in ["PLD Target", "Substrate"]: + item = Material(data) + print(item.get_compound()) + result = item.__dict__ result.pop("extra") print(result) # print(json.dumps(chain.sample_data))