|
| 1 | +""" |
| 2 | +export_json.py — Export registry snapshot to data/registry.json |
| 3 | +--------------------------------------------------------------- |
| 4 | +Reads the LadybugDB graph and writes a single JSON file that the |
| 5 | +GitHub Pages frontend consumes. Runs as the last step in the CI |
| 6 | +workflow after every schema ingestion. |
| 7 | +
|
| 8 | +Output shape: |
| 9 | +{ |
| 10 | + "generated_at": "2025-...", |
| 11 | + "sources": [ |
| 12 | + { "label": "schema.org", "version": "1.0.0", "class_count": 38 } |
| 13 | + ], |
| 14 | + "classes": [ |
| 15 | + { |
| 16 | + "uid": "...", |
| 17 | + "iri": "https://schema.org/Person", |
| 18 | + "uri": "https://registry.sensein.io/obj/Person/v/1.0.0", |
| 19 | + "name": "Person", |
| 20 | + "definition": "A person...", |
| 21 | + "version": "1.0.0", |
| 22 | + "abstract": false, |
| 23 | + "source": "schema.org", |
| 24 | + "properties": [ |
| 25 | + { |
| 26 | + "uid": "...", |
| 27 | + "iri": "https://schema.org/name", |
| 28 | + "name": "name", |
| 29 | + "definition": "...", |
| 30 | + "datatype": "xsd:string", |
| 31 | + "range_uri": "", |
| 32 | + "multivalued": false, |
| 33 | + "required": false, |
| 34 | + "source": "schema.org" |
| 35 | + } |
| 36 | + ], |
| 37 | + "subclass_of": ["https://schema.org/Thing"], |
| 38 | + "alignments": [ |
| 39 | + { |
| 40 | + "target_uid": "...", |
| 41 | + "target_name": "Investigator", |
| 42 | + "target_iri": "https://schema.org/Person", |
| 43 | + "target_source": "bbqs", |
| 44 | + "distance": 0.0, |
| 45 | + "method": "iri" |
| 46 | + } |
| 47 | + ] |
| 48 | + } |
| 49 | + ] |
| 50 | +} |
| 51 | +
|
| 52 | +Usage: |
| 53 | + python export_json.py |
| 54 | + python export_json.py --db ./registry.lbug --out data/registry.json |
| 55 | +""" |
| 56 | + |
| 57 | +from __future__ import annotations |
| 58 | +import datetime, json, os |
| 59 | +from pathlib import Path |
| 60 | + |
| 61 | +import click |
| 62 | +import ladybug as lb |
| 63 | + |
| 64 | +DB_PATH = "./registry.lbug" |
| 65 | +OUT_PATH = "data/registry.json" |
| 66 | + |
| 67 | + |
| 68 | +def export(conn: lb.Connection) -> dict: |
| 69 | + # ---- sources ----------------------------------------------------------- |
| 70 | + src_result = conn.execute( |
| 71 | + "MATCH (s:SchemaSource) RETURN s.uid, s.label, s.version" |
| 72 | + ) |
| 73 | + source_rows = src_result.get_all() |
| 74 | + |
| 75 | + source_class_counts: dict[str, int] = {} |
| 76 | + for row in source_rows: |
| 77 | + _, label, _ = row |
| 78 | + r = conn.execute( |
| 79 | + "MATCH (n:SchemaClass {source_label: $src}) RETURN count(n)", |
| 80 | + {"src": label} |
| 81 | + ) |
| 82 | + source_class_counts[label] = r.get_next()[0] |
| 83 | + |
| 84 | + sources = [ |
| 85 | + { |
| 86 | + "label": row[1], |
| 87 | + "version": row[2] or "1.0.0", |
| 88 | + "class_count": source_class_counts.get(row[1], 0), |
| 89 | + } |
| 90 | + for row in source_rows |
| 91 | + ] |
| 92 | + |
| 93 | + # ---- classes ----------------------------------------------------------- |
| 94 | + cls_result = conn.execute(""" |
| 95 | + MATCH (n:SchemaClass) |
| 96 | + RETURN n.uid, n.iri, n.uri, n.name, n.definition, |
| 97 | + n.version, n.abstract, n.source_label |
| 98 | + ORDER BY n.source_label, n.name |
| 99 | + """) |
| 100 | + |
| 101 | + classes = [] |
| 102 | + for row in cls_result.get_all(): |
| 103 | + uid, iri, uri, name, definition, version, abstract, source = row |
| 104 | + |
| 105 | + # properties |
| 106 | + prop_result = conn.execute(""" |
| 107 | + MATCH (c:SchemaClass {uid: $uid})-[:HAS_PROPERTY]->(p:SchemaProperty) |
| 108 | + RETURN p.uid, p.iri, p.name, p.definition, |
| 109 | + p.datatype, p.range_uri, p.multivalued, |
| 110 | + p.required, p.source_label |
| 111 | + ORDER BY p.name |
| 112 | + """, {"uid": uid}) |
| 113 | + |
| 114 | + properties = [ |
| 115 | + { |
| 116 | + "uid": r[0], |
| 117 | + "iri": r[1] or "", |
| 118 | + "name": r[2] or "", |
| 119 | + "definition": r[3] or "", |
| 120 | + "datatype": r[4] or "", |
| 121 | + "range_uri": r[5] or "", |
| 122 | + "multivalued": bool(r[6]), |
| 123 | + "required": bool(r[7]), |
| 124 | + "source": r[8] or "", |
| 125 | + } |
| 126 | + for r in prop_result.get_all() |
| 127 | + ] |
| 128 | + |
| 129 | + # subclass_of |
| 130 | + sub_result = conn.execute(""" |
| 131 | + MATCH (c:SchemaClass {uid: $uid})-[:SUBCLASS_OF]->(p:SchemaClass) |
| 132 | + RETURN p.iri |
| 133 | + """, {"uid": uid}) |
| 134 | + subclass_of = [r[0] for r in sub_result.get_all() if r[0]] |
| 135 | + |
| 136 | + # alignments |
| 137 | + align_result = conn.execute(""" |
| 138 | + MATCH (c:SchemaClass {uid: $uid})-[a:ALIGNED_TO]->(t:SchemaClass) |
| 139 | + RETURN t.uid, t.name, t.iri, t.source_label, a.distance, a.method |
| 140 | + ORDER BY a.distance |
| 141 | + """, {"uid": uid}) |
| 142 | + |
| 143 | + alignments = [ |
| 144 | + { |
| 145 | + "target_uid": r[0], |
| 146 | + "target_name": r[1] or "", |
| 147 | + "target_iri": r[2] or "", |
| 148 | + "target_source": r[3] or "", |
| 149 | + "distance": float(r[4]) if r[4] is not None else 1.0, |
| 150 | + "method": r[5] or "", |
| 151 | + } |
| 152 | + for r in align_result.get_all() |
| 153 | + ] |
| 154 | + |
| 155 | + classes.append({ |
| 156 | + "uid": uid, |
| 157 | + "iri": iri or "", |
| 158 | + "uri": uri or "", |
| 159 | + "name": name or "", |
| 160 | + "definition": definition or "", |
| 161 | + "version": version or "1.0.0", |
| 162 | + "abstract": bool(abstract), |
| 163 | + "source": source or "", |
| 164 | + "properties": properties, |
| 165 | + "subclass_of": subclass_of, |
| 166 | + "alignments": alignments, |
| 167 | + }) |
| 168 | + |
| 169 | + return { |
| 170 | + "generated_at": datetime.datetime.now(datetime.UTC).isoformat(), |
| 171 | + "sources": sources, |
| 172 | + "classes": classes, |
| 173 | + } |
| 174 | + |
| 175 | + |
| 176 | +@click.command() |
| 177 | +@click.option("--db", default=DB_PATH, show_default=True, |
| 178 | + help="Path to LadybugDB file.") |
| 179 | +@click.option("--out", default=OUT_PATH, show_default=True, |
| 180 | + help="Output JSON path.") |
| 181 | +def cli(db: str, out: str) -> None: |
| 182 | + """Export registry snapshot to JSON for the GitHub Pages frontend.""" |
| 183 | + conn = lb.Connection(lb.Database(db)) |
| 184 | + data = export(conn) |
| 185 | + |
| 186 | + out_path = Path(out) |
| 187 | + out_path.parent.mkdir(parents=True, exist_ok=True) |
| 188 | + out_path.write_text(json.dumps(data, indent=2)) |
| 189 | + |
| 190 | + nc = len(data["classes"]) |
| 191 | + ns = len(data["sources"]) |
| 192 | + click.echo(f"Exported {nc} classes from {ns} sources → {out_path}") |
| 193 | + |
| 194 | + |
| 195 | +if __name__ == "__main__": |
| 196 | + cli() |
0 commit comments