gem-mapping-studio 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- forome/gem/_reference/__init__.py +12 -0
- forome/gem/_reference/dimensions.md +388 -0
- forome/gem/_reference/genetic_evidence.shacl.ttl +387 -0
- forome/gem/_reference/semantic_types.yaml +949 -0
- forome/gem/extraction/__init__.py +0 -0
- forome/gem/extraction/extract_annotations.py +106 -0
- forome/gem/extraction/extract_annotations_pypdf.py +140 -0
- forome/gem/extraction/yaml_to_rdf.py +191 -0
- forome/gem/umls/__init__.py +0 -0
- forome/gem/umls/_paths.py +58 -0
- forome/gem/umls/adjudicate_ui.py +2253 -0
- forome/gem/umls/build_umls_crosswalk.py +384 -0
- forome/gem/umls/classify_credibility_sweep.py +112 -0
- forome/gem/umls/fetch_semantic_network.py +133 -0
- forome/gem/umls/local_umls.py +806 -0
- forome/gem/umls/render_crosswalk_tex.py +312 -0
- forome/gem/umls/semantic_types.py +130 -0
- forome/gem/umls/sweep.py +576 -0
- forome/gem/umls/uts_client.py +415 -0
- forome/gem/validation/__init__.py +0 -0
- forome/gem/validation/compute_coverage.py +149 -0
- forome/gem/validation/validate_annotations.py +123 -0
- gem_mapping_studio-0.2.2.dist-info/METADATA +465 -0
- gem_mapping_studio-0.2.2.dist-info/RECORD +29 -0
- gem_mapping_studio-0.2.2.dist-info/WHEEL +5 -0
- gem_mapping_studio-0.2.2.dist-info/entry_points.txt +11 -0
- gem_mapping_studio-0.2.2.dist-info/licenses/LICENSE-code.txt +202 -0
- gem_mapping_studio-0.2.2.dist-info/licenses/LICENSE.txt +395 -0
- gem_mapping_studio-0.2.2.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Extract PDF highlights and callouts using PyMuPDF.
|
|
3
|
+
|
|
4
|
+
For each highlight, PyMuPDF can give us the covered text directly via
|
|
5
|
+
page.get_textbox(rect) or via the annotation's get_text() method. It also
|
|
6
|
+
gives us better handling of Popup-linked callout notes.
|
|
7
|
+
"""
|
|
8
|
+
import sys
|
|
9
|
+
import json
|
|
10
|
+
import fitz # PyMuPDF
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def rect_from_quadpoints(quads):
|
|
14
|
+
"""Given the 8-float quadpoints, return a fitz.Rect bounding box."""
|
|
15
|
+
if not quads:
|
|
16
|
+
return None
|
|
17
|
+
xs = [quads[i] for i in range(0, len(quads), 2)]
|
|
18
|
+
ys = [quads[i] for i in range(1, len(quads), 2)]
|
|
19
|
+
return fitz.Rect(min(xs), min(ys), max(xs), max(ys))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main(pdf_path, output_json=None):
|
|
23
|
+
doc = fitz.open(pdf_path)
|
|
24
|
+
results = []
|
|
25
|
+
|
|
26
|
+
for page_num, page in enumerate(doc, start=1):
|
|
27
|
+
annot = page.first_annot
|
|
28
|
+
while annot is not None:
|
|
29
|
+
info = annot.info or {}
|
|
30
|
+
atype = annot.type # (type_id, type_name)
|
|
31
|
+
type_name = atype[1] if atype else "?"
|
|
32
|
+
contents = info.get("content", "") or ""
|
|
33
|
+
author = info.get("title", "") or ""
|
|
34
|
+
|
|
35
|
+
entry = {
|
|
36
|
+
"page": page_num,
|
|
37
|
+
"type": type_name,
|
|
38
|
+
"author": author,
|
|
39
|
+
"note": contents.strip() if contents else "",
|
|
40
|
+
"rect": list(annot.rect),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if type_name == "Highlight":
|
|
44
|
+
# Try multiple extraction strategies and pick the best
|
|
45
|
+
texts = []
|
|
46
|
+
|
|
47
|
+
# 1. Quadpoints-based: iterate the vertices in groups of 4
|
|
48
|
+
try:
|
|
49
|
+
verts = annot.vertices
|
|
50
|
+
if verts:
|
|
51
|
+
# vertices come as list of tuples; each highlight quad
|
|
52
|
+
# uses 4 points
|
|
53
|
+
for i in range(0, len(verts), 4):
|
|
54
|
+
quad = verts[i:i+4]
|
|
55
|
+
if len(quad) == 4:
|
|
56
|
+
xs = [p[0] for p in quad]
|
|
57
|
+
ys = [p[1] for p in quad]
|
|
58
|
+
r = fitz.Rect(min(xs), min(ys),
|
|
59
|
+
max(xs), max(ys))
|
|
60
|
+
t = page.get_textbox(r).strip()
|
|
61
|
+
if t:
|
|
62
|
+
texts.append(t)
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
# 2. Fallback: use the annotation rect
|
|
67
|
+
if not texts:
|
|
68
|
+
try:
|
|
69
|
+
t = page.get_textbox(annot.rect).strip()
|
|
70
|
+
if t:
|
|
71
|
+
texts.append(t)
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
highlighted = " ".join(texts).strip()
|
|
76
|
+
# Clean up the weird line breaks and excess whitespace
|
|
77
|
+
highlighted = " ".join(highlighted.split())
|
|
78
|
+
entry["highlighted_text"] = highlighted
|
|
79
|
+
|
|
80
|
+
results.append(entry)
|
|
81
|
+
annot = annot.next
|
|
82
|
+
|
|
83
|
+
# Filter out noise: empty highlights with no note
|
|
84
|
+
cleaned = []
|
|
85
|
+
for r in results:
|
|
86
|
+
if r["type"] == "Highlight":
|
|
87
|
+
if not r.get("highlighted_text") and not r.get("note"):
|
|
88
|
+
continue # skip truly empty
|
|
89
|
+
cleaned.append(r)
|
|
90
|
+
|
|
91
|
+
if output_json:
|
|
92
|
+
with open(output_json, "w") as f:
|
|
93
|
+
json.dump(cleaned, f, indent=2)
|
|
94
|
+
print(f"Wrote {len(cleaned)} annotations to {output_json}")
|
|
95
|
+
else:
|
|
96
|
+
for r in cleaned:
|
|
97
|
+
print(f"\n[Page {r['page']}] {r['type']} (author: {r['author']})")
|
|
98
|
+
if r.get("note"):
|
|
99
|
+
print(f" NOTE: {r['note']}")
|
|
100
|
+
if r.get("highlighted_text"):
|
|
101
|
+
print(f" TEXT: {r['highlighted_text']}")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
main(sys.argv[1],
|
|
106
|
+
output_json=sys.argv[2] if len(sys.argv) > 2 else None)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Extract PDF annotations (highlights and text-note callouts) from a PDF.
|
|
3
|
+
|
|
4
|
+
For each annotation on each page, report:
|
|
5
|
+
- annotation subtype (Highlight, Text, Popup, etc.)
|
|
6
|
+
- page number (1-indexed)
|
|
7
|
+
- the author of the annotation if present
|
|
8
|
+
- the 'Contents' field (what the annotator typed as a note)
|
|
9
|
+
- the quadpoints / rect (location on page)
|
|
10
|
+
- the text actually under the highlight, extracted by coordinates
|
|
11
|
+
|
|
12
|
+
This matters because a 'Highlight' annotation stores WHERE it is, not WHAT it
|
|
13
|
+
covers; the covered text has to be recovered from the page's text content
|
|
14
|
+
using the quadpoints.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
import pypdf
|
|
19
|
+
from pypdf.generic import ArrayObject, NumberObject
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def extract_text_under_quads(page, quadpoints):
|
|
23
|
+
"""Given a page and the Highlight's /QuadPoints, extract the text under
|
|
24
|
+
each quad. QuadPoints come in groups of 8: (x1,y1, x2,y2, x3,y3, x4,y4)
|
|
25
|
+
describing the four corners of each rectangle covered by the highlight.
|
|
26
|
+
|
|
27
|
+
We use pypdf's visitor-based text extraction to collect only text whose
|
|
28
|
+
position falls inside any quad rectangle.
|
|
29
|
+
"""
|
|
30
|
+
if not quadpoints:
|
|
31
|
+
return ""
|
|
32
|
+
|
|
33
|
+
# Group quadpoints into (x_min, y_min, x_max, y_max) rects.
|
|
34
|
+
rects = []
|
|
35
|
+
q = [float(v) for v in quadpoints]
|
|
36
|
+
for i in range(0, len(q), 8):
|
|
37
|
+
xs = q[i:i+8:2]
|
|
38
|
+
ys = q[i+1:i+8:2]
|
|
39
|
+
rects.append((min(xs), min(ys), max(xs), max(ys)))
|
|
40
|
+
|
|
41
|
+
collected = []
|
|
42
|
+
|
|
43
|
+
def visitor(text, cm, tm, fontDict, fontSize):
|
|
44
|
+
# tm is the text matrix; its e,f are the x,y position.
|
|
45
|
+
try:
|
|
46
|
+
x = tm[4]
|
|
47
|
+
y = tm[5]
|
|
48
|
+
except Exception:
|
|
49
|
+
return
|
|
50
|
+
for (xmin, ymin, xmax, ymax) in rects:
|
|
51
|
+
# Allow a small slack in y because tm gives the baseline, not the
|
|
52
|
+
# character box.
|
|
53
|
+
if xmin - 1 <= x <= xmax + 1 and ymin - 4 <= y <= ymax + 4:
|
|
54
|
+
if text and text.strip():
|
|
55
|
+
collected.append(text)
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
page.extract_text(visitor_text=visitor)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
return f"<extraction error: {e}>"
|
|
62
|
+
|
|
63
|
+
return " ".join(collected).strip()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main(pdf_path):
|
|
67
|
+
reader = pypdf.PdfReader(pdf_path)
|
|
68
|
+
print(f"PDF: {pdf_path}")
|
|
69
|
+
print(f"Pages: {len(reader.pages)}")
|
|
70
|
+
print()
|
|
71
|
+
|
|
72
|
+
# First pass: find all Popup annotations, keyed by IndirectRef of the
|
|
73
|
+
# Popup object itself. A Highlight often has a /Popup pointer to a
|
|
74
|
+
# Popup annotation which in turn has /Contents (the actual callout text).
|
|
75
|
+
popups_by_ref = {}
|
|
76
|
+
for page_idx, page in enumerate(reader.pages):
|
|
77
|
+
if "/Annots" not in page:
|
|
78
|
+
continue
|
|
79
|
+
for annot_ref in page["/Annots"]:
|
|
80
|
+
annot = annot_ref.get_object()
|
|
81
|
+
subtype = annot.get("/Subtype")
|
|
82
|
+
if subtype == "/Popup":
|
|
83
|
+
popups_by_ref[annot_ref.idnum] = annot
|
|
84
|
+
|
|
85
|
+
annot_count = 0
|
|
86
|
+
for page_idx, page in enumerate(reader.pages):
|
|
87
|
+
if "/Annots" not in page:
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
for annot_ref in page["/Annots"]:
|
|
91
|
+
annot = annot_ref.get_object()
|
|
92
|
+
subtype = annot.get("/Subtype", "?")
|
|
93
|
+
|
|
94
|
+
# We are most interested in Highlight and Text (callout/sticky).
|
|
95
|
+
# Popups are auxiliary; they'll be read via their parent Highlight.
|
|
96
|
+
if subtype not in ("/Highlight", "/Text", "/Underline",
|
|
97
|
+
"/Squiggly", "/StrikeOut"):
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
annot_count += 1
|
|
101
|
+
contents = annot.get("/Contents", "")
|
|
102
|
+
author = annot.get("/T", "")
|
|
103
|
+
quads = annot.get("/QuadPoints", None)
|
|
104
|
+
rect = annot.get("/Rect", None)
|
|
105
|
+
|
|
106
|
+
# If Highlight has a Popup with Contents, prefer that text
|
|
107
|
+
popup_ref = annot.get("/Popup")
|
|
108
|
+
popup_contents = ""
|
|
109
|
+
if popup_ref is not None:
|
|
110
|
+
try:
|
|
111
|
+
popup = popup_ref.get_object()
|
|
112
|
+
popup_contents = popup.get("/Contents", "") or ""
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
note_text = contents or popup_contents or ""
|
|
117
|
+
|
|
118
|
+
highlighted_text = ""
|
|
119
|
+
if subtype == "/Highlight" and quads is not None:
|
|
120
|
+
highlighted_text = extract_text_under_quads(page, quads)
|
|
121
|
+
|
|
122
|
+
print(f"--- Annotation #{annot_count} (page {page_idx+1}) ---")
|
|
123
|
+
print(f" subtype: {subtype}")
|
|
124
|
+
if author:
|
|
125
|
+
print(f" author: {author}")
|
|
126
|
+
if note_text:
|
|
127
|
+
print(f" note/callout: {note_text!r}")
|
|
128
|
+
if highlighted_text:
|
|
129
|
+
print(f" highlighted text: {highlighted_text!r}")
|
|
130
|
+
elif subtype == "/Text":
|
|
131
|
+
# Sticky note — no highlighted text, but has a location
|
|
132
|
+
if rect:
|
|
133
|
+
print(f" location (rect): {list(rect)}")
|
|
134
|
+
print()
|
|
135
|
+
|
|
136
|
+
print(f"Total annotations processed: {annot_count}")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
main(sys.argv[1])
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Convert a Genetic Evidence Model annotation YAML to RDF (Turtle).
|
|
3
|
+
|
|
4
|
+
The emitted RDF is what the SHACL shapes in ``schema/genetic_evidence.shacl.ttl``
|
|
5
|
+
validate. Every ``GeneticEvidence`` item becomes a ``gem:GeneticEvidence``
|
|
6
|
+
node carrying its dimension values (as ``gem:`` IRIs for categorical
|
|
7
|
+
dimensions, literals for free-text ones) and its assertions, each with a
|
|
8
|
+
``gem:sourceSpan`` blank node.
|
|
9
|
+
|
|
10
|
+
Design notes
|
|
11
|
+
------------
|
|
12
|
+
* Field-name aliases are normalized: ``resolution``/``target_resolution`` and
|
|
13
|
+
source-span ``key_phrase``/``phrase`` (the v0 -> v1 protocol rename).
|
|
14
|
+
* Legacy method synonyms are mapped to canonical values
|
|
15
|
+
(``IN_VIVO_EXPERIMENT`` -> ``IN_VIVO`` etc.) so the shapes need only the
|
|
16
|
+
canonical enumeration.
|
|
17
|
+
* The explicit escape value ``not_applicable_or_omitted`` (used when a
|
|
18
|
+
curator considered a dimension and chose not to fill it) is emitted as
|
|
19
|
+
``gem:NOT_APPLICABLE_OR_OMITTED``; the shapes accept it as "addressed".
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
python3 extraction/yaml_to_rdf.py annotations/jossin2017.yaml [out.ttl]
|
|
23
|
+
python3 extraction/yaml_to_rdf.py annotations/*.yaml --stdout
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
import yaml
|
|
31
|
+
|
|
32
|
+
GEM = "https://w3id.org/genetic-evidence-model/"
|
|
33
|
+
NA = "NOT_APPLICABLE_OR_OMITTED"
|
|
34
|
+
|
|
35
|
+
# Categorical dimensions: YAML key -> gem: property local name. Values become
|
|
36
|
+
# gem:<VALUE> IRIs (drawn from the enumerations declared in the SHACL file).
|
|
37
|
+
CATEGORICAL = {
|
|
38
|
+
"knowledge_domain": "knowledgeDomain",
|
|
39
|
+
"method": "method",
|
|
40
|
+
"target_type": "targetType",
|
|
41
|
+
"resolution": "resolution",
|
|
42
|
+
"target_resolution": "resolution", # v1 alias
|
|
43
|
+
"credibility": "credibility",
|
|
44
|
+
"phenotype_scale": "phenotypeScale",
|
|
45
|
+
"variant_ascertainment": "variantAscertainment",
|
|
46
|
+
"measurement_target": "measurementTarget",
|
|
47
|
+
"gene_relation": "geneRelation",
|
|
48
|
+
"knockout_type": "knockoutType",
|
|
49
|
+
"mode_of_inheritance": "modeOfInheritance",
|
|
50
|
+
"penetrance": "penetrance",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Free-text dimensions emitted as plain literals.
|
|
54
|
+
LITERAL_DIMS = {
|
|
55
|
+
"organism": "organism",
|
|
56
|
+
"target": "target",
|
|
57
|
+
"specificity_of_phenotype": "specificityOfPhenotype",
|
|
58
|
+
"subdomain": "subdomain", # enum values contain spaces ("Candidate Gene Study")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# A value safe to use as a Turtle IRI local name (no spaces/punctuation).
|
|
62
|
+
import re as _re
|
|
63
|
+
_TOKEN = _re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
64
|
+
|
|
65
|
+
# Boolean dimensions.
|
|
66
|
+
BOOLEAN_DIMS = {
|
|
67
|
+
"mendelian_segregation": "mendelianSegregation",
|
|
68
|
+
"exact_variant": "exactVariant",
|
|
69
|
+
"genetic_background_considered": "geneticBackgroundConsidered",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Legacy method synonyms -> canonical value (dimensions.md "Backward compatibility").
|
|
73
|
+
METHOD_SYNONYMS = {
|
|
74
|
+
"IN_VIVO_EXPERIMENT": "IN_VIVO",
|
|
75
|
+
"IN_VITRO_EXPERIMENT": "IN_VITRO",
|
|
76
|
+
"BIOINFORMATICS_PREDICTION": "BIOINFORMATICS_INFERENCE",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def esc(text: str) -> str:
|
|
81
|
+
"""Escape a string for a Turtle long-literal (triple-quoted)."""
|
|
82
|
+
return str(text).replace("\\", "\\\\").replace('"""', '\\"\\"\\"')
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _values(raw):
|
|
86
|
+
"""Normalise a YAML dimension value into a list of scalar strings."""
|
|
87
|
+
if raw is None:
|
|
88
|
+
return []
|
|
89
|
+
if isinstance(raw, list):
|
|
90
|
+
return [v for v in raw if v is not None]
|
|
91
|
+
if isinstance(raw, dict):
|
|
92
|
+
# e.g. hpo_terms: {annotated_as: not_applicable_or_omitted, ...}
|
|
93
|
+
if raw.get("annotated_as"):
|
|
94
|
+
return [raw["annotated_as"]]
|
|
95
|
+
return []
|
|
96
|
+
return [raw]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _iri_value(prop: str, val) -> str:
|
|
100
|
+
"""Map a scalar dimension value to a gem: IRI local name."""
|
|
101
|
+
s = str(val).strip()
|
|
102
|
+
if s.lower() == "not_applicable_or_omitted":
|
|
103
|
+
return NA
|
|
104
|
+
if prop == "method":
|
|
105
|
+
s = METHOD_SYNONYMS.get(s, s)
|
|
106
|
+
return s
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def item_triples(item: dict, idx: int) -> list[str]:
|
|
110
|
+
"""Return a list of Turtle blocks for one GeneticEvidence item."""
|
|
111
|
+
raw_id = str(item.get("id", f"item{idx}"))
|
|
112
|
+
node = f"gem:item_{raw_id.replace('.', '_').replace('-', '_')}"
|
|
113
|
+
item_props = [f'gem:itemId "{esc(raw_id)}"']
|
|
114
|
+
extra_blocks = [] # assertion + source-span nodes, declared separately
|
|
115
|
+
|
|
116
|
+
for key, prop in CATEGORICAL.items():
|
|
117
|
+
for v in _values(item.get(key)):
|
|
118
|
+
tok = _iri_value(prop, v)
|
|
119
|
+
if _TOKEN.match(tok):
|
|
120
|
+
item_props.append(f"gem:{prop} gem:{tok}")
|
|
121
|
+
else: # value is not a clean enum token; keep it as a literal
|
|
122
|
+
item_props.append(f'gem:{prop} """{esc(v)}"""')
|
|
123
|
+
|
|
124
|
+
for key, prop in LITERAL_DIMS.items():
|
|
125
|
+
if item.get(key) not in (None, ""):
|
|
126
|
+
item_props.append(f'gem:{prop} """{esc(item[key])}"""')
|
|
127
|
+
|
|
128
|
+
for key, prop in BOOLEAN_DIMS.items():
|
|
129
|
+
if item.get(key) is not None:
|
|
130
|
+
b = "true" if item[key] in (True, "true", "True") else "false"
|
|
131
|
+
item_props.append(f"gem:{prop} {b}")
|
|
132
|
+
|
|
133
|
+
for a_i, a in enumerate(item.get("assertions", []) or []):
|
|
134
|
+
an = f"{node}_a{a_i}"
|
|
135
|
+
span = a.get("source_span") or {}
|
|
136
|
+
phrase = span.get("phrase", span.get("key_phrase"))
|
|
137
|
+
page = span.get("page")
|
|
138
|
+
a_props = [f'gem:assertionId "{esc(a.get("id", f"{raw_id}.A{a_i}"))}"']
|
|
139
|
+
if a.get("statement"):
|
|
140
|
+
a_props.append(f'gem:statement """{esc(a["statement"])}"""')
|
|
141
|
+
if phrase is not None or page is not None:
|
|
142
|
+
sp_props = []
|
|
143
|
+
if page is not None:
|
|
144
|
+
sp_props.append(f"gem:page {int(page)}")
|
|
145
|
+
if phrase is not None:
|
|
146
|
+
sp_props.append(f'gem:phrase """{esc(phrase)}"""')
|
|
147
|
+
extra_blocks.append(
|
|
148
|
+
f"{an}_span a gem:SourceSpan ;\n "
|
|
149
|
+
+ " ;\n ".join(sp_props) + " .")
|
|
150
|
+
a_props.append(f"gem:sourceSpan {an}_span")
|
|
151
|
+
extra_blocks.append(
|
|
152
|
+
f"{an} a gem:GeneticEvidenceAssertion ;\n "
|
|
153
|
+
+ " ;\n ".join(a_props) + " .")
|
|
154
|
+
item_props.append(f"gem:assertion {an}")
|
|
155
|
+
|
|
156
|
+
item_block = (f"{node} a gem:GeneticEvidence ;\n "
|
|
157
|
+
+ " ;\n ".join(item_props) + " .")
|
|
158
|
+
return [item_block] + extra_blocks
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def convert(path: Path) -> str:
|
|
162
|
+
data = yaml.safe_load(path.read_text())
|
|
163
|
+
triples = [f"@prefix gem: <{GEM}> .",
|
|
164
|
+
f"@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .",
|
|
165
|
+
f"# Generated from {path.name} by extraction/yaml_to_rdf.py", ""]
|
|
166
|
+
for idx, item in enumerate(data.get("evidence", []) or []):
|
|
167
|
+
triples.extend(item_triples(item, idx))
|
|
168
|
+
triples.append("")
|
|
169
|
+
return "\n".join(triples) + "\n"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def main(argv):
|
|
173
|
+
args = [a for a in argv if not a.startswith("--")]
|
|
174
|
+
to_stdout = "--stdout" in argv
|
|
175
|
+
if not args:
|
|
176
|
+
print(__doc__)
|
|
177
|
+
return 2
|
|
178
|
+
for src in args:
|
|
179
|
+
p = Path(src)
|
|
180
|
+
ttl = convert(p)
|
|
181
|
+
if to_stdout or len(args) == 1 and len(argv) == 1:
|
|
182
|
+
sys.stdout.write(ttl)
|
|
183
|
+
else:
|
|
184
|
+
out = p.with_suffix(".ttl")
|
|
185
|
+
out.write_text(ttl)
|
|
186
|
+
print(f"wrote {out}")
|
|
187
|
+
return 0
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
File without changes
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Locate the repository's data and schema directories.
|
|
2
|
+
|
|
3
|
+
The UMLS crosswalk tooling is a repo-run data project: its inputs/outputs live
|
|
4
|
+
in ``data/umls/`` and the schema in ``schema/`` at the repo root, discovered by
|
|
5
|
+
walking up from this module. ``GEM_DATA_DIR`` overrides the data directory
|
|
6
|
+
(used by tests / alternate checkouts).
|
|
7
|
+
|
|
8
|
+
Standalone installs (``pip install forome-gem``, no repo checkout) fall back
|
|
9
|
+
to the synced reference copies packaged in ``forome.gem._reference`` for the
|
|
10
|
+
Semantic Network and the SHACL schema, and to the current working directory
|
|
11
|
+
as the default workspace (the Studio treats an empty directory as a fresh
|
|
12
|
+
workspace).
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
_HERE = Path(__file__).resolve()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _repo_root() -> Path | None:
|
|
23
|
+
"""The repo checkout this module runs from, or None (standalone install)."""
|
|
24
|
+
for cand in _HERE.parents:
|
|
25
|
+
if (cand / "data" / "umls").is_dir() and (cand / "schema").is_dir():
|
|
26
|
+
return cand
|
|
27
|
+
for cand in _HERE.parents:
|
|
28
|
+
if (cand / "pyproject.toml").is_file():
|
|
29
|
+
return cand
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
_REPO = _repo_root()
|
|
34
|
+
_PACKAGED = _HERE.parent.parent / "_reference" # forome/gem/_reference
|
|
35
|
+
|
|
36
|
+
# Kept for repo-mode consumers; in standalone mode it points at the package's
|
|
37
|
+
# parent purely so derived paths stay well-defined (they will not exist).
|
|
38
|
+
REPO_ROOT = _REPO if _REPO is not None else _HERE.parents[6] if len(_HERE.parents) > 6 else _HERE.parent
|
|
39
|
+
|
|
40
|
+
# The workspace: inventory / crosswalk / adjudications live here. ``GEM_DATA_DIR``
|
|
41
|
+
# points the tooling at any mapping directory (which may be empty, e.g. when
|
|
42
|
+
# building axes from scratch). Standalone default: the current directory.
|
|
43
|
+
if "GEM_DATA_DIR" in os.environ:
|
|
44
|
+
DATA_DIR = Path(os.environ["GEM_DATA_DIR"])
|
|
45
|
+
elif _REPO is not None:
|
|
46
|
+
DATA_DIR = _REPO / "data" / "umls"
|
|
47
|
+
else:
|
|
48
|
+
DATA_DIR = Path.cwd()
|
|
49
|
+
|
|
50
|
+
# Canonical reference data (the UMLS Semantic Network) resolves to the repo's
|
|
51
|
+
# copy when in a checkout — independent of the workspace, so type exploration
|
|
52
|
+
# works even when GEM_DATA_DIR is an empty directory — and to the packaged
|
|
53
|
+
# synced copy otherwise.
|
|
54
|
+
REFERENCE_DIR = (_REPO / "data" / "umls") if _REPO is not None else _PACKAGED
|
|
55
|
+
# The SHACL schema and dimensions.md: repo ``schema/`` or the packaged copies
|
|
56
|
+
# (the packaged layout is flat: the same filenames, no ``schema/`` prefix).
|
|
57
|
+
SCHEMA_DIR = (_REPO / "schema") if _REPO is not None else _PACKAGED
|
|
58
|
+
PAPER_SECTIONS = REPO_ROOT / "paper" / "sections"
|