diff --git a/pyiron_rdm/classic.py b/pyiron_rdm/classic.py index 3624772..22b61db 100644 --- a/pyiron_rdm/classic.py +++ b/pyiron_rdm/classic.py @@ -31,6 +31,16 @@ def classic_general_job(job, export_env_file: bool): job_cdict = process_general_job(job) return job_cdict +def classic_table(table_job, export_env_file: bool): + from ob.concept_dict import process_table_job + + if export_env_file: + from ob.concept_dict import export_env + export_env(table_job.path) + + table_cdict = process_table_job(table_job) + return table_cdict + def classic_lammps(lammps_job, export_env_file): '''export_env_file: Bool''' from ob.concept_dict import process_lammps_job @@ -111,14 +121,6 @@ def validate_upload_options(options, allowed_keys, allowed_defects=None): raise ValueError( f'Invalid defect(s) in "options[\'defects\']": {sorted(invalid_defects)}. \ Allowed defects are: {sorted(allowed_defects)}') - materials = options.get('materials') - if materials: - if not isinstance(materials, list): - options['materials'] = [options['materials']] - pseudopots = options.get('pseudopotentials') - if pseudopots: - if not isinstance(pseudopots, list): - options['pseudopotentials'] = [options['pseudopotentials']] def openbis_login(url, username, instance='bam', s3_config_path = None): #instance = get_datamodel(o) @@ -155,11 +157,15 @@ def upload_classic_pyiron(job, o, space, project, collection=None, export_env_fi else: options = {} - structure = job.structure - if not structure: - print('This job does not contain a structure and will not be uploaded. \ - Please add structure before trying to upload.') - return + job_type = job.to_dict()['TYPE'].lower() + structure = None + + if 'tablejob' not in job_type: + structure = job.structure + if not structure: + print('This job does not contain a structure and will not be uploaded. \ + Please add structure before trying to upload.') + return # Project env file - TODO what is this for?? pr = job.project @@ -169,18 +175,15 @@ def upload_classic_pyiron(job, o, space, project, collection=None, export_env_fi if not collection: collection = pr.name - space = space.upper() - project = project.upper() - collection = collection.upper() # ------------------------------------VALIDATION---------------------------------------------- cdicts_to_validate = [] - struct_dict = classic_structure(pr, structure, structure_name=job.name + '_structure', options=options, - is_init_struct=is_init_struct, init_structure=init_structure) - cdicts_to_validate.append(struct_dict) + if structure: + struct_dict = classic_structure(pr, structure, structure_name=job.name + '_structure', options=options, + is_init_struct=is_init_struct, init_structure=init_structure) + cdicts_to_validate.append(struct_dict) - job_type = job.to_dict()['TYPE'] proceed = True if 'lammps' in job_type: job_cdict = classic_lammps(job, export_env_file=export_env_file) @@ -197,6 +200,10 @@ def upload_classic_pyiron(job, o, space, project, collection=None, export_env_fi cdicts_to_validate.append(equil_struct_dict) cdicts_to_validate += [child_cdict for child_cdict in child_jobs_cdict] + elif 'tablejob' in job_type: + table_cdict = classic_table(job, export_env_file=export_env_file) + cdicts_to_validate.append(table_cdict) + else: print(f'The {job_type} job type is not implemented for OpenBIS upload yet.') proceed = input("Type 'yes' to proceed with an upload to general pyiron job type.") @@ -213,7 +220,7 @@ def upload_classic_pyiron(job, o, space, project, collection=None, export_env_fi elif datamodel == 'bam': upload_final_struct = False - if upload_final_struct and (not 'murn' in job.to_dict()['TYPE']): + if upload_final_struct and all(x not in job_type for x in ('murn', 'tablejob')): if is_init_struct: init_structure = structure final_structure = job.get_structure() @@ -231,42 +238,49 @@ def upload_classic_pyiron(job, o, space, project, collection=None, export_env_fi #--------------------------------------UPLOAD------------------------------------------------- from ob.ob_upload import openbis_upload_validated - # Structure - cdict, props_dict, object_type, ds_types, ob_parents, object_name = validated_to_upload[0] - ob_structure_id = openbis_upload_validated(o, space, project, collection, object_name, - object_type, ob_parents, props_dict, ds_types, cdict) - - if datamodel == 'sfb1394': - job_parents = None # job does not have init structure as parent - str_parent = ob_structure_id # equil structure has init as parent - elif datamodel == 'bam': - job_parents = ob_structure_id # job has init structure as parent - str_parent = None # equil structure does not have init as parent - - # (Main) job - cdict, props_dict, object_type, ds_types, ob_parents, object_name = validated_to_upload[1] - ob_job_id = openbis_upload_validated(o, space, project, collection, object_name, - object_type, ob_parents, props_dict, ds_types, cdict, parent_ids=job_parents) - - if 'murn' in job_type: - ob_children_ids = [] - # Murn equilibrium structure - cdict, props_dict, object_type, ds_types, ob_parents, object_name = validated_to_upload[2] - ob_equil_struct_id = openbis_upload_validated(o, space, project, collection, object_name, - object_type, ob_parents, props_dict, ds_types, cdict, parent_ids=str_parent) - ob_children_ids.append(ob_equil_struct_id) - # Murn children jobs - for validated_child in validated_to_upload[3:]: - cdict, props_dict, object_type, ds_types, ob_parents, object_name = validated_child - ob_child_id = openbis_upload_validated(o, space, project, collection, object_name, - object_type, ob_parents, props_dict, ds_types, cdict, parent_ids=str_parent) - ob_children_ids.append(ob_child_id) - from ob.ob_upload import link_children - link_children(o, ob_job_id, ob_children_ids) - - # Final structure upload (already included as equilibrium for murn) - elif upload_final_struct: - cdict, props_dict, object_type, ds_types, ob_parents, object_name = validated_to_upload[-1] - ob_final_structure_id = openbis_upload_validated(o, space, project, collection, object_name, - object_type, ob_parents, props_dict, ds_types, cdict, parent_ids=[ob_structure_id, ob_job_id]) + # Pyiron table job - no structure + if 'tablejob' in job_type: + cdict, props_dict, object_type, ds_types, ob_parents, object_name = validated_to_upload[0] + ob_job_id = openbis_upload_validated(o, space, project, collection, object_name, + object_type, ob_parents, props_dict, ds_types, cdict) + + else: + # Structure + cdict, props_dict, object_type, ds_types, ob_parents, object_name = validated_to_upload[0] + ob_structure_id = openbis_upload_validated(o, space, project, collection, object_name, + object_type, ob_parents, props_dict, ds_types, cdict) + + if datamodel == 'sfb1394': + job_parents = None # job does not have init structure as parent + str_parent = ob_structure_id # equil structure has init as parent + elif datamodel == 'bam': + job_parents = ob_structure_id # job has init structure as parent + str_parent = None # equil structure does not have init as parent + + # (Main) job + cdict, props_dict, object_type, ds_types, ob_parents, object_name = validated_to_upload[1] + ob_job_id = openbis_upload_validated(o, space, project, collection, object_name, + object_type, ob_parents, props_dict, ds_types, cdict, parent_ids=job_parents) + + if 'murn' in job_type: + ob_children_ids = [] + # Murn equilibrium structure + cdict, props_dict, object_type, ds_types, ob_parents, object_name = validated_to_upload[2] + ob_equil_struct_id = openbis_upload_validated(o, space, project, collection, object_name, + object_type, ob_parents, props_dict, ds_types, cdict, parent_ids=str_parent) + ob_children_ids.append(ob_equil_struct_id) + # Murn children jobs + for validated_child in validated_to_upload[3:]: + cdict, props_dict, object_type, ds_types, ob_parents, object_name = validated_child + ob_child_id = openbis_upload_validated(o, space, project, collection, object_name, + object_type, ob_parents, props_dict, ds_types, cdict, parent_ids=str_parent) + ob_children_ids.append(ob_child_id) + from ob.ob_upload import link_children + link_children(o, ob_job_id, ob_children_ids) + + # Final structure upload (already included as equilibrium for murn) + elif upload_final_struct: + cdict, props_dict, object_type, ds_types, ob_parents, object_name = validated_to_upload[-1] + ob_final_structure_id = openbis_upload_validated(o, space, project, collection, object_name, + object_type, ob_parents, props_dict, ds_types, cdict, parent_ids=[ob_structure_id, ob_job_id]) #--------------------------------------------------------------------------------------------- \ No newline at end of file diff --git a/pyiron_rdm/concept_dict.py b/pyiron_rdm/concept_dict.py index efea272..7eaa499 100644 --- a/pyiron_rdm/concept_dict.py +++ b/pyiron_rdm/concept_dict.py @@ -21,12 +21,24 @@ def process_general_job(job): method_dict = {} add_simulation_software(job, method_dict) + add_general_contexts(method_dict) get_simulation_folder(job, method_dict) file_name = job.path + '_concept_dict.json' with open(file_name, 'w') as f: json.dump(method_dict, f, indent=2) return method_dict +def process_table_job(table_job): + method_dict = {} + add_simulation_software(table_job, method_dict) + add_general_contexts(method_dict) + extract_table_info(table_job, method_dict) + get_simulation_folder(table_job, method_dict) + file_name = table_job.path + '_concept_dict.json' + with open(file_name, 'w') as f: + json.dump(method_dict, f, indent=2) + return method_dict + def process_lammps_job(job): method_dict = {} add_lammps_contexts(method_dict) @@ -81,6 +93,15 @@ def process_vasp_job(job): json.dump(method_dict, f, indent=2) return method_dict +def add_general_contexts(method_dict): + method_dict['@context'] = {} + method_dict['@context']['path'] = 'http://purls.helmholtz-metadaten.de/cmso/hasPath' + method_dict['@context']['inputs'] = 'http://purls.helmholtz-metadaten.de/asmo/hasInputParameter' + method_dict['@context']['label'] = 'http://www.w3.org/2000/01/rdf-schema#label' + method_dict['@context']['value'] = 'http://purls.helmholtz-metadaten.de/asmo/hasValue' + method_dict['@context']['outputs'] = 'http://purls.helmholtz-metadaten.de/cmso/hasCalculatedProperty' + method_dict['@context']['workflow_manager'] = 'http://demo.fiz-karlsruhe.de/matwerk/E457491' + def add_lammps_contexts(method_dict): method_dict['@context'] = {} method_dict['@context']['sample'] = 'http://purls.helmholtz-metadaten.de/cmso/AtomicScaleSample' @@ -1181,6 +1202,47 @@ def extract_vasp_calculated_quantities(job, method_dict): ) method_dict['outputs'] = outputs +def extract_table_info(table_job, method_dict): + import base64 + import json + + table_df = table_job.get_dataframe() + inputs = [] + inputs.append( + { + "label": "columns", + "value": ", ".join(table_df.columns) + } + ) + inputs.append( + { + "label": "number_of_jobs", + "value": len(table_df) + } + ) + + table_df_cl = table_df.head(100).astype(str).replace("nan", "NaN") + spreadsheet = { + "headers": table_df_cl.columns.to_list(), + "data": table_df_cl.to_numpy().tolist(), + "width": [150] * (len(table_df_cl.columns)), + } + preview = ( + "" + + str( + base64.b64encode(bytes(json.dumps(spreadsheet), encoding="utf-8")), + encoding="utf-8", + ) + + "" +) + inputs.append( + { + "label": "table_preview", + "value": preview + } + ) + method_dict['inputs'] = inputs + def export_env(path): """Exports to path+_environment.yml""" import os diff --git a/pyiron_rdm/ob_OT_bam.py b/pyiron_rdm/ob_OT_bam.py index f297a12..ae7652d 100644 --- a/pyiron_rdm/ob_OT_bam.py +++ b/pyiron_rdm/ob_OT_bam.py @@ -3,15 +3,15 @@ def material_par(props_dict, options): object_type = "MATERIAL_V1" if options.get("materials"): - permid_materials = options["materials"] + parent_materials = options["materials"] where_clause = {} requested_attrs = [] else: mat_dict_pct_str = species_by_num_to_pct(props_dict) where_clause = {"chem_species_by_comp_in_pct": mat_dict_pct_str} requested_attrs = ["chem_species_by_comp_in_pct"] - permid_materials = "" - return object_type, permid_materials, where_clause, requested_attrs + parent_materials = "" + return object_type, parent_materials, where_clause, requested_attrs def intpot_par(cdict): object_type = "INTERATOMIC_POTENTIAL" @@ -21,8 +21,8 @@ def intpot_par(cdict): def pseudopot_par(options): object_type = "PSEUDOPOTENTIAL" - permid_pseudopots = options.get("pseudopotentials", "") - return object_type, permid_pseudopots + parent_materials = options.get("pseudopotentials", "") + return object_type, parent_materials def sw_par(cdict): import re @@ -97,9 +97,9 @@ def get_ot_info(cdict): ) def get_inv_parent(parent_name, cdict, props_dict, options): - ob_type, permids, where_clause, requested_attrs, ob_code = "", "", {}, [], "" + ob_type, parents, where_clause, requested_attrs, ob_code = "", "", {}, [], "" if parent_name == "material": - ob_type, permids, where_clause, requested_attrs = material_par(props_dict, options) + ob_type, parents, where_clause, requested_attrs = material_par(props_dict, options) elif parent_name == "compute_resource": ob_type, ob_code = compresource_par(cdict) elif parent_name == "software": @@ -111,7 +111,7 @@ def get_inv_parent(parent_name, cdict, props_dict, options): elif parent_name == "wf_reference": ob_type, ob_code = wfref_par(cdict) - return ob_type, permids, where_clause, requested_attrs, ob_code + return ob_type, parents, where_clause, requested_attrs, ob_code # upload options ______________________________________________ diff --git a/pyiron_rdm/ob_OT_sfb1394.py b/pyiron_rdm/ob_OT_sfb1394.py index 60eb88d..207cd36 100644 --- a/pyiron_rdm/ob_OT_sfb1394.py +++ b/pyiron_rdm/ob_OT_sfb1394.py @@ -4,17 +4,17 @@ def material_par(props_dict: dict, options: dict): if options.get("materials"): object_type = "CRYSTALLINE_MATERIAL" - permid_materials = options["materials"] + parent_materials = options["materials"] where_clause = {} requested_attrs = [] else: mat_dict_pct_str = species_by_num_to_pct(props_dict) object_type = "MATERIAL" - permid_materials = "" + parent_materials = "" where_clause = {"compo_atomic_percent": mat_dict_pct_str} requested_attrs = ["compo_atomic_percent"] # could output a warning here to provide a crystalline_material permId - return object_type, permid_materials, where_clause, requested_attrs + return object_type, parent_materials, where_clause, requested_attrs def interatomicpot_par(cdict): @@ -32,8 +32,8 @@ def workstation_par(cdict): def pseudopot_par(options): object_type = "PSEUDOPOTENTIAL" - permid_pseudopots = options.get("pseudopotentials", "") - return object_type, permid_pseudopots + parent_pseudopots = options.get("pseudopotentials", "") + return object_type, parent_pseudopots def software_par(cdict): @@ -106,9 +106,9 @@ def get_ot_info(cdict): def get_inv_parent(parent_name, cdict, props_dict: dict, options: dict): - ob_type, permids, where_clause, requested_attrs, ob_code = "", "", {}, [], "" + ob_type, parents, where_clause, requested_attrs, ob_code = "", "", {}, [], "" if parent_name == "material": - ob_type, permids, where_clause, requested_attrs = material_par( + ob_type, parents, where_clause, requested_attrs = material_par( props_dict, options ) elif parent_name == "workstation": @@ -118,9 +118,9 @@ def get_inv_parent(parent_name, cdict, props_dict: dict, options: dict): elif parent_name == "interatomic_potential": ob_type, where_clause, requested_attrs = interatomicpot_par(cdict) elif parent_name == "pseudopotential": - ob_type, permids = pseudopot_par(options) + ob_type, parents = pseudopot_par(options) - return ob_type, permids, where_clause, requested_attrs, ob_code + return ob_type, parents, where_clause, requested_attrs, ob_code # upload options ______________________________________________ @@ -191,7 +191,7 @@ def pseudopotential_suggester(o, structure, **kwargs): return -def slow_pseudopotential_matcher( +def slow_pseudopotential_suggester( o, job ): # could also take job['POTCAR'] instead? In case already loaded somewhere? potcar = job["POTCAR"] @@ -229,56 +229,26 @@ def get_subsystems(chemsys: str) -> list: return ["-".join(combination) for combination in all_combinations] -def crystalline_material_suggester(o, structure, tol: float = 0.02, space_group_number: int|None =None, match_subcomposition: bool =False, openbis_kwargs: dict|None = None): - """Suggests a list of crystalline materials from the openBIS inventory for a structure of interest. - - Args: - o (pybis.Openbis): The openBIS session object used to query crystalline materials. - structure (Atoms | str): The structure for which to find materials in the openBIS instance. - tol (float): Tolerance factor for matching chemical composition. - Materials are accepted if the absolute difference in atomic percentage for each element between - the candidate and reference structure is less than `100 * tol`. For example, a `tol` of 0.01 (or 1%) - means atomic percentages must be within +/- 1% of the reference. - space_group (int, optional): The space group number to filter materials by. Defaults to None. - match_subcomposition (bool, optional): Whether to search for materials for each subsystem based on their - composition and tolerance. Defaults to False. - openbis_kwargs (dict, optional): Expert feature. A dictionary of openBIS-specific codes and their values - to apply additional filtering. Defaults to None. - Returns: - pybis.things.Things: An openBIS query result object. The data can be accessed as a pandas DataFrame via - `.df` attribute. - """ - - openbis_kwargs = openbis_kwargs if openbis_kwargs is not None else {} - - if isinstance(structure, str): - try: - from ase import Atoms - structure = Atoms(structure) - except ImportError: - raise ImportError('For the parsing of structure like strings, ase needs to be installed.') - - chem_system = "-".join(sorted(set(structure.get_chemical_symbols()))) +def crystalline_material_suggester(o, structure, tol: float = 0.02, **kwargs): + # tolerance is a decimal number + chem_system = "-".join(sorted(structure.get_species_symbols())) + # space_group = 'SPACE_GROUP_' + str(structure.get_symmetry().spacegroup['Number']) # atomic composition of structure - species_dict = dict() - for i in structure.get_chemical_symbols(): - species_dict[i] = species_dict.get(i, 0) + 1 + species_dict = dict(structure.get_number_species_atoms()) atomic_pct_dict = get_atomic_percent_dict(species_dict) - # matching candidates from openBIS candidates = [] for chemical_system in get_subsystems(chem_system): - where_dict = {"CHEMICAL_SYSTEM": chemical_system,} - prop_list = list(openbis_kwargs.keys()) + ["CHEMICAL_SYSTEM"] - if space_group_number is not None: - where_dict['SPACE_GROUP_SHORT'] = 'SPACE_GROUP_' + str(space_group_number) - prop_list += ['SPACE_GROUP_SHORT'] candidates += o.get_objects( type="CRYSTALLINE_MATERIAL", - where=where_dict, - props=prop_list + where={ + "CHEMICAL_SYSTEM": chemical_system, + # 'SPACE_GROUP_SHORT': space_group, + }, + props=list(kwargs.keys()) + ["CHEMICAL_SYSTEM"], + # props=list(kwargs.keys()) + ['CHEMICAL_SYSTEM', 'SPACE_GROUP_SHORT'] ) # define properties to display @@ -299,12 +269,7 @@ def crystalline_material_suggester(o, structure, tol: float = 0.02, space_group_ from ast import literal_eval candidate_atomic_pct = literal_eval(atomic_pct) - subsystem_pct_dict = atomic_pct_dict.copy() - - if match_subcomposition: - subsystem_pct_dict = get_atomic_percent_dict({k: atomic_pct_dict[k] for k in candidate_atomic_pct}) - if is_within_tolerance(subsystem_pct_dict, candidate_atomic_pct, tol): - filtered.append(candidate.permId) + if is_within_tolerance(atomic_pct_dict, candidate_atomic_pct, tol): + filtered.append(candidate.permId) - ob_objects= o.get_objects(permId=filtered, props=props) - return ob_objects + return o.get_objects(permId=filtered, props=props) diff --git a/pyiron_rdm/ob_cfg_bam.py b/pyiron_rdm/ob_cfg_bam.py index c5fab21..6e1c1dc 100644 --- a/pyiron_rdm/ob_cfg_bam.py +++ b/pyiron_rdm/ob_cfg_bam.py @@ -49,6 +49,18 @@ def map_cdict_to_ob(o, cdict, concept_dict): # job, project, server elif 'job_name' in cdict.keys(): props['$name'] = cdict['job_name'] + if 'job_type' in cdict.keys(): + jobtype_map = { + 'Calphy': 'CALPHY', + 'ElasticMatrixJob': 'ELASTICMATRIXJOB', + 'PacemakerJob': 'PACEMAKERJOB', + 'PhonopyJob': 'PHONOPYJOB', + 'Sphinx': 'SPHINX', + 'TableJob': 'TABLEJOB' + } + jobtype_val = jobtype_map.get(cdict.get('job_type')) + if jobtype_val: + props |= {'pyiron_job_type': jobtype_val} if 'job_status' in cdict.keys(): if cdict['job_status'] == 'finished': # TODO we also did True if status 'not_converged' or 'converged'?? props['sim_job_finished'] = True @@ -246,6 +258,16 @@ def map_cdict_to_ob(o, cdict, concept_dict): 'atomistic_n_kpt_y': kpts_y, 'atomistic_n_kpt_z': kpts_z} + if 'job_type' in cdict.keys() and 'TableJob' in cdict['job_type']: + try: + num_jobs = cdict['number_of_jobs'] + cols = cdict['columns'] + props['description'] = f'Pyiron table job from {num_jobs} jobs \ + with the following columns: {cols}.' + props['description'] + except KeyError: + pass + if 'table_preview' in cdict.keys(): + props['pyiron_table_preview'] = cdict['table_preview'] else: print("Neither structure_name nor job_name in the object conceptual dictionary. \ diff --git a/pyiron_rdm/ob_cfg_sfb1394.py b/pyiron_rdm/ob_cfg_sfb1394.py index 28778df..3d78011 100644 --- a/pyiron_rdm/ob_cfg_sfb1394.py +++ b/pyiron_rdm/ob_cfg_sfb1394.py @@ -49,6 +49,18 @@ def map_cdict_to_ob(o, cdict, concept_dict): # job, project, server elif "job_name" in cdict.keys(): props["$name"] = cdict["job_name"] + if "job_type" in cdict.keys(): + jobtype_map = { + "Calphy": "CALPHY", + "ElasticMatrixJob": "ELASTICMATRIXJOB", + "PacemakerJob": "PACEMAKERJOB", + "PhonopyJob": "PHONOPYJOB", + "Sphinx": "SPHINX", + "TableJob": "TABLEJOB" + } + jobtype_val = jobtype_map.get(cdict.get("job_type")) + if jobtype_val: + props |= {"pyiron_job_type": jobtype_val} if "job_status" in cdict.keys(): if ( cdict["job_status"] == "finished" @@ -334,6 +346,17 @@ def map_cdict_to_ob(o, cdict, concept_dict): "atomistic_n_kpt_z": kpts_z, } + if "job_type" in cdict.keys() and "TableJob" in cdict["job_type"]: + try: + num_jobs = cdict["number_of_jobs"] + cols = cdict["columns"] + props["description_multiline"] = f"Pyiron table job from {num_jobs} jobs \ + with the following columns: {cols}." + props["description_multiline"] + except KeyError: + pass + if "table_preview" in cdict.keys(): + props["pyiron_table_preview"] = cdict["table_preview"] + else: print( "Neither structure_name nor job_name in the object conceptual dictionary. \ diff --git a/pyiron_rdm/ob_upload.py b/pyiron_rdm/ob_upload.py index 68df09c..9759a1e 100644 --- a/pyiron_rdm/ob_upload.py +++ b/pyiron_rdm/ob_upload.py @@ -291,26 +291,26 @@ def validate_inventory_parents(o, inv_parents, cdict, props_dict, options): ob_parents = [] get_inv_parent = importlib.import_module(o.ot).get_inv_parent for inv_parent in inv_parents: - ob_type, permids, where, attrs, code = get_inv_parent( + ob_type, parents, where, attrs, code = get_inv_parent( inv_parent, cdict, props_dict, options ) - if permids is not None: - permids = [o.get_object(p).permId for p in permids] - if permids: # multiple parents allowed when more permIds provided + if parents is not None: + parents = [o.get_object(p).permId for p in parents] + if parents: # multiple parents allowed when more permIds provided parents = o.get_objects( type=ob_type, - permId=permids, + permId=parents, ) if parents: ob_parents += parents else: issues.append( - f'Parent object not found: No objects of the type {ob_type} and permId "{permids}" in inventory.' + f'Parent object not found: No objects of the type {ob_type} and permId "{parents}" in inventory.' ) elif code or where: # single parent allowed otherwise; taking the first parent = o.get_objects( - type=ob_type, permId=permids, code=code, where=where, attrs=attrs + type=ob_type, permId=parents, code=code, where=where, attrs=attrs )[0] if parent: ob_parents.append(parent) @@ -326,7 +326,7 @@ def validate_inventory_parents(o, inv_parents, cdict, props_dict, options): ) else: issues_str = "Parent object not found: Not enough information to search. Known information: " - issues_str += f'type = {ob_type}, permId = "{permids}", code = "{code}", attribute match: {where}' + issues_str += f'type = {ob_type}, permId = "{parents}", code = "{code}", attribute match: {where}' issues.append(issues_str) return issues, ob_parents