MDF Enforced Validator and Loader is a lightweight and modular framework designed to ensure data integrity through MDF enforced validation and seamless data ingestion into the graph database. This repository serves as the source code for MEVAL, providing tools, scripts, and workflows to validate and load data to graph database using Graph Model Description Format (MDF) as the source of accessing model features.
MEVAL includes three core classes under the meval package that work together to support model-aware validation and graph loading workflows:
ModelParser wraps bento_mdf.MDFReader and provides easy access to MDF model metadata.(bento_mdf repository: https://github.com/CBIIT/bento-mdf)
It is used to inspect node definitions, key properties, required fields, parent-child relationships, property types, and permissible values. This class is the model introspection layer used by both validation and loading logic.
Loader handles graph database ingestion for TSV data files.
It reads files in chunks, prepares node properties and relationships from each chunk, and performs upsert operations (MERGE semantics) for nodes and edges.
It also includes helper methods for index creation, duplicate cleanup, and graph maintenance tasks such as finding floating/orphan nodes (nodes without a path to a root node, such as study/program node).
Validator enforces MDF-based data quality checks before loading.
It validates TSV file format, validates record-level values against model constraints, checks relationship consistency across files, and supports unique-entry checks.
It also provides utilities such as deterministic UUID generation and adding UUID columns to TSV files.
- Python 3.13
- Create and activate a virtual environment with Python 3.13+.
- Install ctos-meval
pipinstallctos-mevalThe sections below provide separate examples for each core module.
frombento_mdfimportMDFReaderfromneo4jimportGraphDatabasefrommeval.loaderimportLoaderfrommeval.parserimportModelParserfrommeval.validatorimportValidatormodel_file="tests/test_files/ccdi-dcc-model-test.yml"props_file="tests/test_files/ccdi-dcc-model-props-test.yml"# Initialize a ModelParser instancemodel_parser=ModelParser(
model_file=model_file,
props_file=props_file,
handle="test",
)
# Initialize a Validator instancemdf=MDFReader(model_file, props_file, handle="test")
validator=Validator(mdf=mdf)
# Initialize a Loader instancedriver=GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "your_password"))
loader=Loader(driver=driver)# list all node types in the modelnode_list=model_parser.get_node_list()
print(node_list)
# get a full list of methodsdir(model_parser)
# inspect one nodenode_name="participant"# list all properties under node participantmodel_parser.get_node_props_list(node_name)
# list all the required properties under node participantmodel_parser.get_node_props_list_required(node_name)
# get key prop of node participantmodel_parser.get_node_key_prop(node_name)node_name="participant"prop="sex_at_birth"# get property typemodel_parser.get_prop_type(node_name, prop)
# get permissible valuesmodel_parser.get_permissible_values(node_name, prop)
# if property strictmodel_parser.if_prop_strict(node_name, prop)
# if property requiredmodel_parser.if_prop_required(node_name, prop)
# full metadata dict for a propertymodel_parser.get_prop_attr_dict(node_name, prop)# get root node (no outgoing edges)model_parser.get_root_node()
# check if root nodemodel_parser.if_root_node(node_name)
# check if leaf node (no edge that ends with the tested node)model_parser.if_leaf_node(node_name)
# parent/child traversal# get a list of nodes that node "participant" can point tomodel_parser.get_parent_nodes(node_name)
# get a list of nodes that can have edge that ends with "participant"model_parser.get_child_nodes(node_name)
# edge metadatamodel_parser.get_all_edge_triplets()
# get edge multiplicitymodel_parser.get_edge_multiplicity(edge_src=node_name, edge_dst="consent_group")
# get edge name/handlemodel_parser.get_edge_handle(edge_src=node_name, edge_dst="consent_group")participant_file="tests/test_files/participant_test_without_uuid.tsv"# format validation for one fileformat_errors=validator.validate_tsv_format(participant_file)
# format validation for multiple filesformat_errors_by_file=validator.validate_tsv_files_format([
participant_file,
"tests/test_files/survival_test.tsv",
])
# record-level MDF validation (returns only invalid rows)invalid_records=validator.validate_tsv_records(
file_path=participant_file,
id_field="guid",
delimiter=";",
)rel_files= [
"tests/test_files/rel_test_files/test_rel_study.tsv",
"tests/test_files/rel_test_files/test_rel_participant.tsv",
"tests/test_files/rel_test_files/test_rel_consent_group.tsv",
"tests/test_files/rel_test_files/test_rel_generic_file.tsv",
]
# cross-file relationship validationrel_errors=validator.validate_tsv_rels(rel_files, rel_delimiter=";")
# detect duplicate key-property entries within each node typeduplicated_entries=validator.validate_tsv_uniq_entry(rel_files)# add guid + converted relationship guid columns to a TSVValidator.add_uuid_to_tsv_file(
file_path="tests/test_files/participant_test_without_uuid.tsv",
project_name="ccdi_dcc",
mdf=mdf,
output_file_path="/tmp/participant_with_guid.tsv",
uuid_column="guid",
delimiter=";",
)
# recursively find all .tsv files in a foldertsv_paths=Validator.find_tsv_files("tests/test_files", recursive=True)MEVAL currently contains functions to Upsert data into a graph database.
Upsert loading means each record/edge is either inserted if it doesn't already exist or updated if it does. Both data node and edge are entities in a graph database. No deletion of data node or edge is performed during data loading in Upsert mode.
Note: Relationships can only be created if data nodes at two ends have been created. That's why data nodes are loaded first before relationships.
# upsert node properties from a filenode_summary=loader.upsert_file_records(
file_path="tests/test_files/rel_test_files/test_rel_participant.tsv",
model_parser=model_parser,
id_field="guid",
chunk_size=3000,
delimiter=";",
)
print(node_summary)
# upsert relationships from a filerel_summary=loader.upsert_file_relationships(
file_path="tests/test_files/rel_test_files/test_rel_participant.tsv",
processed_rel_dict= {},
model_parser=model_parser,
id_field="guid",
delimiter=";",
)
print(rel_summary)# create label-property index pairs for an entire modelcreated_indexes=loader.create_index(model_parser=model_parser, property_name="guid")
# drop a single label-property pair indexindex_list= [{"label":"study", "property":"guid"}]
loader.drop_index(index_list)
# drop all indexes in a graph databaseloader.drop_all_indexes()
# check graph health and clean floating nodesfloating_ids=loader.find_nodes_without_path_to_root(root_node_label="study")
# close connection to a graph databaseloader.close()