diff --git a/python/.gitignore b/python/.gitignore index 4d81b4c..15bf77f 100644 --- a/python/.gitignore +++ b/python/.gitignore @@ -157,4 +157,4 @@ About # other .idea/ - +.vscode/* diff --git a/python/scripts/make_diff_changelog.py b/python/scripts/make_diff_changelog.py index 20f23c1..0d11c19 100644 --- a/python/scripts/make_diff_changelog.py +++ b/python/scripts/make_diff_changelog.py @@ -6,14 +6,18 @@ import logging from pathlib import Path -from typing import Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple, Type, Union import click from bento_mdf.diff import diff_models from bento_mdf.mdf import MDF from bento_meta.entity import Entity -from bento_meta.objects import Edge, Node, Property, Term -from bento_meta.util.changelog import changeset_id_generator, update_config_changeset_id +from bento_meta.objects import Concept, Edge, Node, Property, Tag, Term, ValueSet +from bento_meta.util.changelog import ( + changeset_id_generator, + escape_quotes_in_attr, + update_config_changeset_id, +) from bento_meta.util.cypher.clauses import ( Delete, DetachDelete, @@ -36,259 +40,419 @@ REMOVE_PROPERTY = "remove property graph property" -def split_diff( - diff: Dict[str, Dict[str, Optional[Union[List, Dict]]]], model_handle: str -) -> List[Tuple[str, Union[Entity, dict]]]: - """splits model diff into segments that represent one change each""" - diff_segments = [] - diff_order = [ - REMOVE_NODE, - ADD_NODE, - REMOVE_PROPERTY, - ADD_PROPERTY, - REMOVE_RELATIONSHIP, - ADD_RELATIONSHIP, - ] - - def add_node(entity, diff_segments): - diff_segments.append((ADD_NODE, entity)) - - def remove_node(entity, diff_segments): - diff_segments.append((REMOVE_NODE, entity)) - - def add_relationship(src, rel, dst, diff_segments): - diff_segments.append((ADD_RELATIONSHIP, {"rel": rel, "src": src, "dst": dst})) - - def remove_relationship(src, rel, dst, diff_segments): - diff_segments.append( - (REMOVE_RELATIONSHIP, {"rel": rel, "src": src, "dst": dst}) +class DiffSplitter: + """splits model diff into cypher statements that represent one change each""" + + def __init__( + self, diff: Dict[str, Dict[str, Optional[Union[List, Dict]]]], model_handle: str + ) -> None: + self.diff = diff + self.diff_summary = self.diff.pop("summary", None) + self.model_handle = model_handle + # entity_types order matters: earlier types may depend on later types + # e.g. removing an edge whose src is a removed node + self.entity_types = ["terms", "props", "edges", "nodes"] + self.entity_classes: Dict[str, Type[Entity]] = { + "nodes": Node, + "edges": Edge, + "props": Property, + "terms": Term, + "value_set": ValueSet, + "concept": Concept, + "tags": Tag, + } + self.diff_statements = [] + self.statement_order = [ + REMOVE_NODE, + ADD_NODE, + REMOVE_PROPERTY, + ADD_PROPERTY, + REMOVE_RELATIONSHIP, + ADD_RELATIONSHIP, + ] + + def get_diff_statements(self) -> List[Tuple[str, Union[Entity, dict]]]: + """Split diff into segments & return sorted in segment_order""" + for entity_type in self.entity_types: + self.split_entity_diff(entity_type=entity_type) + statements_sorted = sorted( + self.diff_statements, key=lambda x: self.statement_order.index(x[0]) ) + return [x[1] for x in statements_sorted] # statement part only - def add_property(entity, prop_handle, prop_value, diff_segments): - diff_segments.append( - ( - ADD_PROPERTY, - { - "entity": entity, - "prop_handle": prop_handle, - "prop_value": prop_value, - }, + def add_node_statement(self, entity: Entity) -> None: + """Add cypher statement that adds an entity""" + escape_quotes_in_attr(entity) + ent_c = N(label=entity.get_label(), props=entity.get_attr_dict()) + stmt = Statement(Merge(ent_c)) + self.diff_statements.append((ADD_NODE, stmt)) + + def remove_node_statement(self, entity: Entity) -> None: + """Add cypher statement that removes an entity""" + escape_quotes_in_attr(entity) + ent_c = N(label=entity.get_label(), props=entity.get_attr_dict()) + if isinstance(entity, Edge): + src_c = N(label="node", props=entity.src.get_attr_dict()) + dst_c = N(label="node", props=entity.dst.get_attr_dict()) + src_trip = T(ent_c, R(Type="has_src"), src_c) + dst_trip = T(ent_c, R(Type="has_dst"), dst_c) + path = G(src_trip, dst_trip) + match_clause = Match(path) + else: + match_clause = Match(ent_c) + stmt = Statement(match_clause, DetachDelete(ent_c.var)) + + self.diff_statements.append((REMOVE_NODE, stmt)) + + def add_relationship_statement(self, src: Entity, rel: str, dst: Entity) -> None: + """Add cypher statement that adds a relationship from src to dst entities""" + for ent in [src, dst]: + escape_quotes_in_attr(ent) + rel_c = R(Type=rel) + src_c = N(label=src.get_label(), props=src.get_attr_dict()) + dst_c = N(label=dst.get_label(), props=dst.get_attr_dict()) + plain_trip = T(_plain_var(src_c), rel_c, _plain_var(dst_c)) + stmt = Statement(Match(src_c, dst_c), Merge(plain_trip)) + + self.diff_statements.append((ADD_RELATIONSHIP, stmt)) + + def remove_relationship_statement(self, src: Entity, rel: str, dst: Entity) -> None: + """Add cypher statement that removes a relationship from src to dst entities""" + for ent in [src, dst]: + escape_quotes_in_attr(ent) + rel_c = R(Type=rel) + src_c = N(label=src.get_label(), props=src.get_attr_dict()) + dst_c = N(label=dst.get_label(), props=dst.get_attr_dict()) + trip = T(src_c, rel_c, dst_c) + stmt = Statement(Match(trip), Delete(_plain_var(rel_c))) + + self.diff_statements.append((REMOVE_RELATIONSHIP, stmt)) + + def add_long_relationship_statement( + self, + parent: Entity, + parent_rel: str, + obj_ent: Entity, + src: Entity, + rel: str, + dst: Entity, + ) -> None: + """ + Add cypher statement that adds a relationship from src to dst entities + + Includes (parent)-[parrel]-(obj) relationship to correctly match ent. + """ + for ent in [parent, obj_ent, src, dst]: + escape_quotes_in_attr(ent) + parent_c = N(label=parent.get_label(), props=parent.get_attr_dict()) + parent_rel_c = R(Type=parent_rel) + rel_c = R(Type=rel) + src_c = N(label=src.get_label(), props=src.get_attr_dict()) + dst_c = N(label=dst.get_label(), props=dst.get_attr_dict()) + + # kludge for concepts - need to clean up rel. direction stuff + if obj_ent.get_label() == src_c.label: + parent_trip = T(parent_c, parent_rel_c, src_c) + else: + parent_trip = T(parent_c, parent_rel_c, dst_c) + + plain_trip = T(_plain_var(src_c), rel_c, _plain_var(dst_c)) + stmt = Statement(Match(parent_trip, dst_c), Merge(plain_trip)) + + self.diff_statements.append((ADD_RELATIONSHIP, stmt)) + + def remove_long_relationship_statement( + self, + parent: Entity, + parent_rel: str, + obj_ent: Entity, + src: Entity, + rel: str, + dst: Entity, + ) -> None: + """ + Add cypher statement that removes a relationship from src to dst entities + + Includes (parent)-[parrel]-(src) relationship to correctly id src ent. + """ + for ent in [parent, obj_ent, src, dst]: + escape_quotes_in_attr(ent) + parent_c = N(label=parent.get_label(), props=parent.get_attr_dict()) + parent_rel_c = R(Type=parent_rel) + rel_c = R(Type=rel) + src_c = N(label=src.get_label(), props=src.get_attr_dict()) + dst_c = N(label=dst.get_label(), props=dst.get_attr_dict()) + + # kludge for concepts - need to clean up rel. direction stuff + if obj_ent.get_label() == src_c.label: + parent_trip = T(parent_c, parent_rel_c, src_c) + else: + parent_trip = T(parent_c, parent_rel_c, dst_c) + + trip = T(src_c, rel_c, dst_c) + stmt = Statement(Match(parent_trip, trip), Delete(_plain_var(rel_c))) + + self.diff_statements.append((REMOVE_RELATIONSHIP, stmt)) + + def add_property_statement( + self, entity: Entity, prop_handle: str, prop_value: Any + ) -> None: + """Add cypher statement that adds a property to an entity""" + escape_quotes_in_attr(entity) + prop_value_unesc = prop_value.replace(r"\'", "'").replace(r"\"", '"') + prop_value_esc = prop_value_unesc.replace("'", r"\'").replace('"', r"\"") + + prop_c = P(handle=prop_handle, value=prop_value_esc) + ent_c_noprop = N(label=entity.get_label(), props=entity.get_attr_dict()) + ent_c_prop = N(label=entity.get_label(), props=entity.get_attr_dict()) + ent_c_prop._add_props(prop_c) + ent_c_prop.var = ent_c_noprop.var + stmt = Statement(Match(ent_c_noprop), Set(ent_c_prop.props[prop_handle])) + + self.diff_statements.append((ADD_PROPERTY, stmt)) + + def remove_property_statement(self, entity: Entity, prop_handle: str) -> None: + """Add cypher statement that removes a property from an entity""" + escape_quotes_in_attr(entity) + ent_c = N(label=entity.get_label(), props=entity.get_attr_dict()) + stmt = Statement(Match(ent_c), Remove(ent_c, prop=prop_handle)) + + self.diff_statements.append((REMOVE_PROPERTY, stmt)) + + def update_simple_attr_segment( + self, entity: Entity, attr: str, old_value: Any, new_value: Any + ) -> None: + """Add segment to update simple attribute.""" + if old_value and not new_value: + self.remove_property_statement( + entity=entity, + prop_handle=attr, + ) + else: + self.add_property_statement( + entity=entity, + prop_handle=attr, + prop_value=new_value, ) + + def update_object_attr_segment( + self, + entity: Entity, + attr: str, + old_values: Dict[str, Entity], + new_values: Dict[str, Entity], + ) -> None: + """ + Add segment to update object attribute. + + These are 'term containers' like concept & value_set. + """ + object_attr_class = self.entity_classes.get(attr) + if not object_attr_class: + raise AttributeError(f"{attr} not in self.entity_classes") + object_attr = object_attr_class() + object_attr = self.get_entity_of_type(entity_type=attr) + parent, parent_rel, obj_ent = self.get_triplet( + entity=entity, attr=attr, value=object_attr ) + if new_values and not old_values: + # add relationship between entity and object attr if DNE + self.add_relationship_statement(src=parent, rel=parent_rel, dst=obj_ent) + for old_value in old_values.values(): + old_entity = self.get_entity_of_type( + entity_type="terms", entity_attr_dict=old_value.get_attr_dict() + ) + src, rel, dst = self.get_triplet( + entity=object_attr, attr="terms", value=old_entity + ) + self.remove_long_relationship_statement( + parent=parent, + parent_rel=parent_rel, + obj_ent=obj_ent, + src=src, + rel=rel, + dst=dst, + ) + for new_value in new_values.values(): + new_entity = self.get_entity_of_type( + entity_type="terms", entity_attr_dict=new_value.get_attr_dict() + ) + src, rel, dst = self.get_triplet( + entity=object_attr, attr="terms", value=new_entity + ) + self.add_long_relationship_statement( + parent=parent, + parent_rel=parent_rel, + obj_ent=obj_ent, + src=src, + rel=rel, + dst=dst, + ) + + def update_collection_attr_segment( + self, + entity: Entity, + attr: str, + old_values: Dict[str, Entity], + new_values: Dict[str, Entity], + ) -> None: + """ + Update collection attr (e.g. list of props, tags, terms of an object attr) + + parent_entity is for Valuesets & Concepts so that it can locate the correct one. + """ + for old_value in old_values.values(): + old_entity = self.get_entity_of_type( + entity_type=attr, entity_attr_dict=old_value.get_attr_dict() + ) + if isinstance(old_entity, Tag): # not handled in add/rem + self.remove_node_statement(entity=old_entity) + src, rel, dst = self.get_triplet(entity=entity, attr=attr, value=old_entity) + self.remove_relationship_statement(src=src, rel=rel, dst=dst) + for new_value in new_values.values(): + new_entity = self.get_entity_of_type( + entity_type=attr, entity_attr_dict=new_value.get_attr_dict() + ) + if isinstance(new_entity, Tag): # not handled in add/rem + self.add_node_statement(entity=new_entity) + src, rel, dst = self.get_triplet(entity=entity, attr=attr, value=new_entity) + self.add_relationship_statement(src=src, rel=rel, dst=dst) + + def get_triplet( + self, entity: Entity, attr: str, value: Entity + ) -> Tuple[Entity, str, Entity]: + """Uses mapspec() to get rel name and direction, returns src, rel, dst""" + rel_str = entity.mapspec()["relationship"][attr]["rel"] + rel = rel_str.replace(":", "").replace("<", "").replace(">", "") + + if ">" not in rel_str: # True unless rel is from left to right + return (value, rel, entity) + return (entity, rel, value) + + def get_class_attrs( + self, entity_type: Type[Entity], include_generic_attrs: bool = True + ) -> Dict[str, List[str]]: + """ + Get class attrs from entity class's attspec_, returns tuple of + + If include_generic_attrs=True, adds Entity.att_spec_ attrs without a '_' prefix. + """ + class_attrs = entity_type.attspec_ + + if include_generic_attrs: + generic_atts = {x: y for x, y in Entity.attspec_.items() if x[0] != "_"} + ent_attrs = {**generic_atts, **class_attrs} + else: + ent_attrs = class_attrs - def remove_property(entity, prop_handle, prop_value, diff_segments): - diff_segments.append( - ( - REMOVE_PROPERTY, + simple_attrs = [x for x, y in ent_attrs.items() if y == "simple"] + obj_attrs = [x for x, y in ent_attrs.items() if y == "object"] + coll_attrs = [x for x, y in ent_attrs.items() if y == "collection"] + + return { + "simple": simple_attrs, + "object": obj_attrs, + "collection": coll_attrs, + } + + def get_entity_of_type( + self, entity_type: str, entity_attr_dict: Optional[Dict[str, Any]] = None + ) -> Entity: + """returns instantiated entity of given type with given attrs""" + object_attr_class = self.entity_classes.get(entity_type) + if not object_attr_class: + raise AttributeError(f"{entity_type} not in self.entity_classes") + if entity_attr_dict: + return object_attr_class(entity_attr_dict) + return object_attr_class() + + def generate_entity_from_key( + self, + entity_type: str, + entity_key: Union[str, Tuple[str, str], Tuple[str, str, str]], + ) -> Entity: + """Generate bento-meta entity from its key""" + if entity_type == "nodes": + return Node({"handle": entity_key, "model": self.model_handle}) + if entity_type == "edges" and len(entity_key) == 3: + return Edge( { - "entity": entity, - "prop_handle": prop_handle, - "prop_value": prop_value, - }, + "handle": entity_key[0], + "model": self.model_handle, + "src": Node({"handle": entity_key[1], "model": self.model_handle}), + "dst": Node({"handle": entity_key[2], "model": self.model_handle}), + } ) - ) + if entity_type == "props" and len(entity_key) == 2: + return Property( + { + "handle": entity_key[1], + "model": self.model_handle, + "_parent_handle": entity_key[0], + } + ) + if entity_type == "terms" and len(entity_key) == 2: + return Term({"value": entity_key[0], "origin_name": entity_key[1]}) + raise ValueError(f"Unknown entity type: {entity_type}") - node_diff = diff.get("nodes") - edge_diff = diff.get("edges") - prop_diff = diff.get("props") - - if node_diff: - if node_diff.get("a"): - for node_hdl in node_diff.get("a"): - remove_node( - Node({"handle": node_hdl, "model": model_handle}), diff_segments - ) - if node_diff.get("b"): - for node_hdl in node_diff.get("b"): - add_node( - Node({"handle": node_hdl, "model": model_handle}), diff_segments - ) - for node_hdl, change in node_diff.items(): - if node_hdl in {"a", "b"}: - continue - node_props = change["props"] - node = Node({"handle": node_hdl, "model": model_handle}) - if node_props.get("a"): - for prop_hdl in node_props.get("a"): - remove_relationship( - node, - "has_property", - Property({"handle": prop_hdl, "model": model_handle}), - diff_segments, + def split_entity_diff(self, entity_type: str) -> None: + """For each entity type in diff (e.g. nodes, edges, props), splits into segments""" + entity_diff = self.diff.get(entity_type) + if not entity_diff: + logger.warning(f"No diff for entity type {entity_type}") + return + # removed entities + removed_entities = entity_diff.get("removed", {}) + if removed_entities: + for entity in removed_entities.values(): + self.remove_node_statement(entity=entity) + # added entities + added_entities = entity_diff.get("added", {}) + if added_entities: + for entity in added_entities.values(): + self.add_node_statement(entity=entity) + if entity_type == "edges": + self.add_relationship_statement( + src=entity, rel="has_src", dst=entity.src ) - if node_props.get("b"): - for prop_hdl in node_props.get("b"): - add_relationship( - node, - "has_property", - Property({"handle": prop_hdl, "model": model_handle}), - diff_segments, + self.add_relationship_statement( + src=entity, rel="has_dst", dst=entity.dst ) - if edge_diff: - if edge_diff.get("a"): - for edge_hdl, src_hdl, dst_hdl in edge_diff.get("a"): - edge = Edge( - { - "handle": edge_hdl, - "src": Node({"handle": src_hdl, "model": model_handle}), - "dst": Node({"handle": dst_hdl, "model": model_handle}), - } - ) - remove_node(edge, diff_segments) - if edge_diff.get("b"): - for edge_hdl, src_hdl, dst_hdl in edge_diff.get("b"): - edge = Edge( - { - "handle": edge_hdl, - "src": Node({"handle": src_hdl, "model": model_handle}), - "dst": Node({"handle": dst_hdl, "model": model_handle}), - } - ) - add_node(edge, diff_segments) - add_relationship(edge, "has_src", edge.src, diff_segments) - add_relationship(edge, "has_dst", edge.dst, diff_segments) - for edge_tup, change_dict in edge_diff.items(): - if edge_tup in {"a", "b"}: - continue - edge = Edge( - { - "handle": edge_tup[0], - "src": Node({"handle": edge_tup[1], "model": model_handle}), - "dst": Node({"handle": edge_tup[2], "model": model_handle}), - } + # changed entities (updated attrs) + class_attrs = self.get_class_attrs(entity_type=self.entity_classes[entity_type]) + for entity_key, change_dict in entity_diff.get("changed", {}).items(): + entity = self.generate_entity_from_key( + entity_type=entity_type, entity_key=entity_key ) - for edge_attr, change in change_dict.items(): - if edge_attr == "props" and change.get("a"): - for prop_hdl in change.get("a"): - remove_relationship( - edge, - "has_property", - Property({"handle": prop_hdl, "model": model_handle}), - diff_segments, - ) - if edge_attr == "props" and change.get("b"): - for prop_hdl in change.get("b"): - add_relationship( - edge, - "has_property", - Property({"handle": prop_hdl, "model": model_handle}), - diff_segments, - ) - else: - if change.get("a"): - remove_property( - edge, - edge_attr, - change.get("a"), - diff_segments, - ) - if change.get("b"): - add_property( - edge, - edge_attr, - change.get("b"), - diff_segments, - ) - if prop_diff: - if prop_diff.get("a"): - for prop_tuple in prop_diff.get("a"): - # parent_hdls = prop_tuple[:-1] - prop_hdl = prop_tuple[-1] - prop = Property({"handle": prop_hdl, "model": model_handle}) - remove_node(prop, diff_segments) - if prop_diff.get("b"): - for prop_tuple in prop_diff.get("b"): - # parent_hdls = prop_tuple[:-1] - prop_hdl = prop_tuple[-1] - prop = Property({"handle": prop_hdl, "model": model_handle}) - add_node(prop, diff_segments) - for prop_tup, change_dict in prop_diff.items(): - if prop_tup in {"a", "b"}: - continue - prop = Property({"handle": prop_tup[1]}) - for prop_attr, change in change_dict.items(): - if prop_attr == "value_set" and change.get("a"): - value_set = change.get("a") - remove_relationship( - prop, - "has_value_set", - value_set, - diff_segments, + for attr, attr_changes in change_dict.items(): + # update simple attribute (e.g. desc/is_required) + if attr in class_attrs["simple"]: + self.update_simple_attr_segment( + entity=entity, + attr=attr, + old_value=attr_changes.get("removed", []), + new_value=attr_changes.get("added", []), ) - if prop_attr == "value_set" and change.get("b"): - value_set = change.get("b") - add_node(value_set, diff_segments) - add_relationship( - prop, - "has_value_set", - value_set, - diff_segments, + # update object attr (i.e. term container, e.g. concept, value_set) + elif attr in class_attrs["object"]: + self.update_object_attr_segment( + entity=entity, + attr=attr, + old_values=attr_changes.get("removed", []), + new_values=attr_changes.get("added", []), + ) + # update collection attr (e.g. list of props, tags) + elif attr in class_attrs["collection"]: + self.update_collection_attr_segment( + entity=entity, + attr=attr, + old_values=attr_changes.get("removed", []), + new_values=attr_changes.get("added", []), ) - for term in value_set.terms: - term_ent = Term({"value": term}) - add_node(term_ent, diff_segments) - add_relationship(value_set, "has_term", term_ent, diff_segments) else: - if change.get("a"): - remove_property( - prop, - prop_attr, - change.get("a"), - diff_segments, - ) - if change.get("b"): - add_property( - prop, - prop_attr, - change.get("b"), - diff_segments, - ) - return sorted(diff_segments, key=lambda x: diff_order.index(x[0])) - - -def convert_diff_segment_to_cypher_statement( - diff_segment: Tuple[str, Union[Entity, dict]] -) -> Statement: - """converts a diff segment to a changeset""" - change, item = diff_segment - if change == ADD_NODE: - ent = N(label=item.get_label(), props=item.get_attr_dict()) - stmt = Statement(Merge(ent)) - elif change == REMOVE_NODE: - ent = N(label=item.get_label(), props=item.get_attr_dict()) - if type(item) == Edge: - src = N(label="node", props=item.src.get_attr_dict()) - dst = N(label="node", props=item.dst.get_attr_dict()) - src_trip = T(ent, R(Type="has_src"), src) - dst_trip = T(ent, R(Type="has_dst"), dst) - path = G(src_trip, dst_trip) - match_clause = Match(path) - else: - match_clause = Match(ent) - stmt = Statement(match_clause, DetachDelete(ent.var)) - elif change == ADD_RELATIONSHIP: - rel = R(Type=item["rel"]) - src = N(label=item["src"].get_label(), props=item["src"].get_attr_dict()) - dst = N(label=item["dst"].get_label(), props=item["dst"].get_attr_dict()) - plain_trip = T(_plain_var(src), rel, _plain_var(dst)) - stmt = Statement(Match(src, dst), Merge(plain_trip)) - elif change == REMOVE_RELATIONSHIP: - rel = R(Type=item["rel"]) - src = N(label=item["src"].get_label(), props=item["src"].get_attr_dict()) - dst = N(label=item["dst"].get_label(), props=item["dst"].get_attr_dict()) - trip = T(src, rel, dst) - stmt = Statement(Match(trip), Delete(_plain_var(rel))) - elif change == ADD_PROPERTY: - prop = P(handle=item["prop_handle"], value=item["prop_value"]) - ent = N(label=item["entity"].get_label(), props=item["entity"].get_attr_dict()) - ent._add_props(prop) - stmt = Statement(Match(ent), Set(ent.props[item["prop_handle"]])) - elif change == REMOVE_PROPERTY: - ent = N(label=item["entity"].get_label(), props=item["entity"].get_attr_dict()) - stmt = Statement(Match(ent), Remove(ent, prop=item["prop_handle"])) - else: - raise RuntimeError(f"invalid diff segment {diff_segment}") - return stmt + raise AttributeError( + f"Attribute '{attr}' not found in {class_attrs} for {entity_type}" + ) def convert_diff_to_changelog( @@ -297,15 +461,15 @@ def convert_diff_to_changelog( """converts diff beween two models to a changelog""" changeset_id = changeset_id_generator(config_file_path=config_file_path) changelog = Changelog() - diff_segments = split_diff(diff, model_handle) + diff_splitter = DiffSplitter(diff, model_handle) + diff_statements = diff_splitter.get_diff_statements() - for segment in diff_segments: - cypher_stmt = convert_diff_segment_to_cypher_statement(segment) + for cypher_statement in diff_statements: changelog.add_changeset( Changeset( id=str(next(changeset_id)), author=author, - change_type=CypherChange(text=str(cypher_stmt)), + change_type=CypherChange(text=str(cypher_statement)), ) ) @@ -377,7 +541,7 @@ def main( """ get liquibase changelog from different versions of mdf files for a model """ - mdf_old = MDF(*old_mdf_files, handle=model_handle, _commit=_commit, raiseError=True) + mdf_old = MDF(*old_mdf_files, handle=model_handle, raiseError=True) mdf_new = MDF(*new_mdf_files, handle=model_handle, _commit=_commit, raiseError=True) if not mdf_old.model or not mdf_new.model: raise RuntimeError("Error getting model from MDF") @@ -392,7 +556,6 @@ def main( author=author, config_file_path=config_file_path, ) - path = Path(output_file_path) changelog.save_to_file(file_path=str(path), encoding="UTF-8") diff --git a/python/scripts/make_mapping_changelog.py b/python/scripts/make_mapping_changelog.py index 0abb70f..8c95a68 100644 --- a/python/scripts/make_mapping_changelog.py +++ b/python/scripts/make_mapping_changelog.py @@ -210,6 +210,7 @@ def convert_mappings_to_changelog( Changeset( id=str(next(changeset_id)), author=author, + run_always=True, change_type=CypherChange(text=str(stmt)), ) ) diff --git a/python/scripts/make_model_changelog.py b/python/scripts/make_model_changelog.py index c51ad04..12927e4 100644 --- a/python/scripts/make_model_changelog.py +++ b/python/scripts/make_model_changelog.py @@ -12,7 +12,11 @@ from bento_meta.mdb.mdb import make_nanoid from bento_meta.model import Model from bento_meta.objects import Concept, Term, ValueSet -from bento_meta.util.changelog import changeset_id_generator, update_config_changeset_id +from bento_meta.util.changelog import ( + changeset_id_generator, + escape_quotes_in_attr, + update_config_changeset_id, +) from bento_meta.util.cypher.clauses import Create, Match, Merge, OnCreateSet, Statement from bento_meta.util.cypher.entities import N, R, T, _plain_var from liquichange.changelog import Changelog, Changeset, CypherChange @@ -20,33 +24,6 @@ logger = logging.getLogger(__name__) -def escape_quotes_in_attr(entity: Entity) -> None: - """ - Escapes quotes in entity attributes. - - Quotes in string attributes may or may not already be escaped, so this function - unescapes all previously escaped ' and " characters and replaces them with - """ - for key, val in vars(entity).items(): - if ( - val - and val is not None - and key != "pvt" - and isinstance( - val, - str, - ) - ): - # First unescape any previously escaped quotes - val = val.replace(r"\'", "'").replace(r"\"", '"') - - # Escape all quotes - val = val.replace("'", r"\'").replace('"', r"\"") - - # Update the modified value back to the attribute - setattr(entity, key, val) - - def cypherize_entity(entity: Entity) -> N: """Represents metamodel Entity object as a property graph Node.""" return N(label=entity.get_label(), props=entity.get_attr_dict()) diff --git a/python/src/bento_meta/objects.py b/python/src/bento_meta/objects.py index 5d02987..7ba495f 100644 --- a/python/src/bento_meta/objects.py +++ b/python/src/bento_meta/objects.py @@ -77,7 +77,7 @@ class Property(Entity): "is_required": "simple", "concept": "object", "value_set": "object", - "parent_handle": "simple", + "_parent_handle": "simple", } mapspec_ = { "label": "property", diff --git a/python/src/bento_meta/util/changelog.py b/python/src/bento_meta/util/changelog.py index ac90695..a224bbe 100644 --- a/python/src/bento_meta/util/changelog.py +++ b/python/src/bento_meta/util/changelog.py @@ -5,7 +5,9 @@ """ import configparser import logging -from typing import Generator, Optional +from typing import Generator + +from bento_meta.entity import Entity logger = logging.getLogger(__name__) @@ -36,3 +38,30 @@ def update_config_changeset_id(config_file_path: str, new_changeset_id: int) -> config.set(section="changelog", option="changeset_id", value=str(new_changeset_id)) with open(file=config_file_path, mode="w", encoding="UTF-8") as config_file: config.write(fp=config_file) + + +def escape_quotes_in_attr(entity: Entity) -> None: + """ + Escapes quotes in entity attributes. + + Quotes in string attributes may or may not already be escaped, so this function + unescapes all previously escaped ' and " characters and replaces them with + """ + for key, val in vars(entity).items(): + if ( + val + and val is not None + and key != "pvt" + and isinstance( + val, + str, + ) + ): + # First unescape any previously escaped quotes + val = val.replace(r"\'", "'").replace(r"\"", '"') + + # Escape all quotes + val = val.replace("'", r"\'").replace('"', r"\"") + + # Update the modified value back to the attribute + setattr(entity, key, val)