diff --git a/CHANGELOG.md b/CHANGELOG.md index 527a722732fcec5e05e2610f1ed77727a87425b6..81b19e4c41504ef350fb638f10b9195564c17109 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `etag` property for the `caosdb.Query` class. The etag allows to debug the caching and to decide whether the server has changed between queries. * function `_read_config_files` to read `pycaosdb.ini` files from different paths. +* function `retrieve_substructure` that recursively adds connected entities. ### Changed ### diff --git a/src/caosdb/common/datatype.py b/src/caosdb/common/datatype.py index 5434f5b6556a13f65754eacc66cb32231366e5b3..3822c7497f9d2489344319edaf3c3aa64e8b2da6 100644 --- a/src/caosdb/common/datatype.py +++ b/src/caosdb/common/datatype.py @@ -62,10 +62,21 @@ def is_list_datatype(datatype): def is_reference(datatype): - """ returns whether the value is a reference + """Returns whether the value is a reference FILE and REFERENCE properties are examples, but also datatypes that are - RecordTypes + RecordTypes. + + Parameters + ---------- + datatype : str + The datatype to check. + + Returns + ------- + bool + True if the datatype is a not base datatype or a list of a base datatype. + Otherwise False is returned. """ if datatype is None: @@ -79,6 +90,39 @@ def is_reference(datatype): else: return True +def get_referenced_recordtype(datatype): + """Return the record type of the referenced datatype. + + Raises + ------ + ValueError + In cases where datatype is not a reference, the list does not have + a referenced record type or the datatype is a FILE. + + Parameters + ---------- + datatype : str + The datatype to check. + + Returns + ------- + str + String containing the name of the referenced datatype. + """ + + if not is_reference(datatype): + raise ValueError("datatype must be a reference") + + if is_list_datatype(datatype): + datatype = get_list_datatype(datatype) + if datatype is None: + raise ValueError("list does not have a list datatype") + + if datatype == FILE: + raise ValueError("FILE references are not considered references with a record type") + + return datatype + def get_id_of_datatype(datatype): """ returns the id of a Record Type diff --git a/src/caosdb/utils/plantuml.py b/src/caosdb/utils/plantuml.py index 75b96ee3aa28ba916adc69e418de398abfe23356..be34b2604f3682bb71b48bbd73e00fe854b3af51 100644 --- a/src/caosdb/utils/plantuml.py +++ b/src/caosdb/utils/plantuml.py @@ -36,11 +36,24 @@ plantuml FILENAME.pu -> FILENAME.png import os import caosdb as db +from caosdb.common.datatype import is_reference, get_referenced_recordtype REFERENCE = "REFERENCE" def get_description(description_str): + """Extract and format a description string from a record type or property. + + Parameters + ---------- + description_str : str + The description string that is going to be formatted. + + Returns + ------- + str + The reformatted description ending in a line break. + """ words = description_str.split() lines = [] lines.append("") @@ -211,15 +224,76 @@ package \"The property P references an instance of D\" <<Rectangle>> { return result +def retrieve_substructure(start_record_types, depth, result_id_set=None, result_container=None, cleanup=True): + """Recursively retrieves CaosDB record types and properties, starting + from given initial types up to a specific depth. + + Parameters + ---------- + start_record_types : Iterable[db.Entity] + Iterable with the entities to be displayed. Starting from these + entities more entities will be retrieved. + depth : int + The maximum depth up to which to retriev sub entities. + result_id_set : set[int] + Used by recursion. Filled with already visited ids. + result_container : db.Container + Used by recursion. Filled with already visited entities. + cleanup : bool + Used by recursion. If True return the resulting result_container. + Don't return anything otherwise. + + Returns + ------- + db.Container + A container containing all the retrieved entites or None if cleanup is False. + """ + # Initialize the id set and result container for level zero recursion depth: + if result_id_set is None: + result_id_set = set() + if result_container is None: + result_container = db.Container() + + for entity in start_record_types: + entity.retrieve() + if entity.id not in result_id_set: + result_container.append(entity) + result_id_set.add(entity.id) + for prop in entity.properties: + if is_reference(prop.datatype) and prop.datatype != db.FILE and depth > 0: + rt = db.RecordType(name=get_referenced_recordtype(prop.datatype)).retrieve() + retrieve_substructure([rt], depth-1, result_id_set, result_container, False) + + if prop.id not in result_id_set: + result_container.append(prop) + result_id_set.add(prop.id) + + for parent in entity.parents: + rt = db.RecordType(id=parent.id).retrieve() + if parent.id not in result_id_set: + result_container.append(rt) + result_id_set.add(parent.id) + if depth > 0: + retrieve_substructure([rt], depth-1, result_id_set, result_container, False) + + if cleanup: + return result_container + return None + + def to_graphics(recordtypes, filename): - """ calls recordtypes_to_plantuml_string(), saves result to file and + """Calls recordtypes_to_plantuml_string(), saves result to file and creates an svg image - plantuml needs to be installed - @params: - recordtypes: itrable with the record types to be displayed - filname: filename of the image (e.g. data_structure; data_structure.pu and - data_structure.svg will be created. + plantuml needs to be installed. + + Parameters + ---------- + recordtypes : Iterable[db.Entity] + Iterable with the entities to be displayed. + filename : str + filename of the image without the extension(e.g. data_structure; + data_structure.pu and data_structure.svg will be created.) """ pu = recordtypes_to_plantuml_string(recordtypes)