cascade-protocol 1.0.0__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.
- cascade_protocol/__init__.py +179 -0
- cascade_protocol/deserializer/__init__.py +10 -0
- cascade_protocol/deserializer/turtle_parser.py +353 -0
- cascade_protocol/models/__init__.py +84 -0
- cascade_protocol/models/allergy.py +67 -0
- cascade_protocol/models/common.py +209 -0
- cascade_protocol/models/condition.py +79 -0
- cascade_protocol/models/coverage.py +148 -0
- cascade_protocol/models/family_history.py +55 -0
- cascade_protocol/models/health_profile.py +87 -0
- cascade_protocol/models/immunization.py +102 -0
- cascade_protocol/models/lab_result.py +108 -0
- cascade_protocol/models/medication.py +182 -0
- cascade_protocol/models/patient_profile.py +179 -0
- cascade_protocol/models/procedure.py +72 -0
- cascade_protocol/models/vital_sign.py +99 -0
- cascade_protocol/models/wellness.py +126 -0
- cascade_protocol/pandas_integration/__init__.py +13 -0
- cascade_protocol/pandas_integration/dataframe.py +142 -0
- cascade_protocol/pod/__init__.py +7 -0
- cascade_protocol/pod/pod.py +324 -0
- cascade_protocol/serializer/__init__.py +37 -0
- cascade_protocol/serializer/turtle_serializer.py +543 -0
- cascade_protocol/validator/__init__.py +11 -0
- cascade_protocol/validator/validator.py +380 -0
- cascade_protocol/vocabularies/__init__.py +24 -0
- cascade_protocol/vocabularies/namespaces.py +493 -0
- cascade_protocol-1.0.0.dist-info/METADATA +382 -0
- cascade_protocol-1.0.0.dist-info/RECORD +31 -0
- cascade_protocol-1.0.0.dist-info/WHEEL +4 -0
- cascade_protocol-1.0.0.dist-info/licenses/LICENSE +200 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cascade-protocol — Python SDK for the Cascade Protocol.
|
|
3
|
+
|
|
4
|
+
A privacy-first, local-first standard for serializing personal health data
|
|
5
|
+
as RDF/Turtle. Zero network calls. All processing is local.
|
|
6
|
+
|
|
7
|
+
Quick start:
|
|
8
|
+
>>> from cascade_protocol import Medication, serialize, validate, Pod
|
|
9
|
+
>>>
|
|
10
|
+
>>> med = Medication(
|
|
11
|
+
... id="urn:uuid:med0-0001-aaaa-bbbb-ccccddddeeee",
|
|
12
|
+
... medication_name="Metoprolol Succinate",
|
|
13
|
+
... is_active=True,
|
|
14
|
+
... dose="25mg",
|
|
15
|
+
... data_provenance="ClinicalGenerated",
|
|
16
|
+
... schema_version="1.3",
|
|
17
|
+
... )
|
|
18
|
+
>>> turtle = serialize(med)
|
|
19
|
+
>>> result = validate(turtle)
|
|
20
|
+
>>> pod = Pod.open("./my-pod")
|
|
21
|
+
>>> meds = pod.query("medications")
|
|
22
|
+
>>> df = meds.to_dataframe()
|
|
23
|
+
|
|
24
|
+
See: https://cascadeprotocol.org/docs
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from cascade_protocol.models import (
|
|
28
|
+
# Base
|
|
29
|
+
CascadeRecord,
|
|
30
|
+
# Record types
|
|
31
|
+
Medication,
|
|
32
|
+
Condition,
|
|
33
|
+
Allergy,
|
|
34
|
+
LabResult,
|
|
35
|
+
VitalSign,
|
|
36
|
+
Immunization,
|
|
37
|
+
Procedure,
|
|
38
|
+
FamilyHistory,
|
|
39
|
+
Coverage,
|
|
40
|
+
PatientProfile,
|
|
41
|
+
EmergencyContact,
|
|
42
|
+
Address,
|
|
43
|
+
PharmacyInfo,
|
|
44
|
+
ActivitySnapshot,
|
|
45
|
+
SleepSnapshot,
|
|
46
|
+
HealthProfile,
|
|
47
|
+
# Type aliases
|
|
48
|
+
ProvenanceType,
|
|
49
|
+
ProvenanceClass,
|
|
50
|
+
ConditionStatus,
|
|
51
|
+
AllergySeverity,
|
|
52
|
+
AllergyCategory,
|
|
53
|
+
LabInterpretation,
|
|
54
|
+
MedicationClinicalIntent,
|
|
55
|
+
CourseOfTherapyType,
|
|
56
|
+
PrescriptionCategory,
|
|
57
|
+
SourceFhirResourceType,
|
|
58
|
+
VitalType,
|
|
59
|
+
VitalInterpretation,
|
|
60
|
+
ImmunizationStatus,
|
|
61
|
+
PlanType,
|
|
62
|
+
CoverageType,
|
|
63
|
+
SubscriberRelationship,
|
|
64
|
+
BiologicalSex,
|
|
65
|
+
AgeGroup,
|
|
66
|
+
BloodType,
|
|
67
|
+
ProcedureStatus,
|
|
68
|
+
)
|
|
69
|
+
from cascade_protocol.serializer.turtle_serializer import (
|
|
70
|
+
serialize,
|
|
71
|
+
serialize_from_dict,
|
|
72
|
+
serialize_medication,
|
|
73
|
+
serialize_condition,
|
|
74
|
+
serialize_allergy,
|
|
75
|
+
serialize_lab_result,
|
|
76
|
+
serialize_vital_sign,
|
|
77
|
+
serialize_immunization,
|
|
78
|
+
serialize_procedure,
|
|
79
|
+
serialize_family_history,
|
|
80
|
+
serialize_coverage,
|
|
81
|
+
serialize_patient_profile,
|
|
82
|
+
serialize_activity_snapshot,
|
|
83
|
+
serialize_sleep_snapshot,
|
|
84
|
+
)
|
|
85
|
+
from cascade_protocol.deserializer.turtle_parser import parse, parse_one
|
|
86
|
+
from cascade_protocol.validator.validator import (
|
|
87
|
+
validate,
|
|
88
|
+
validate_dict,
|
|
89
|
+
ValidationResult,
|
|
90
|
+
ValidationError,
|
|
91
|
+
)
|
|
92
|
+
from cascade_protocol.pod.pod import Pod, RecordSet
|
|
93
|
+
from cascade_protocol.vocabularies.namespaces import (
|
|
94
|
+
NAMESPACES,
|
|
95
|
+
TYPE_MAPPING,
|
|
96
|
+
TYPE_TO_MAPPING_KEY,
|
|
97
|
+
PROPERTY_PREDICATES,
|
|
98
|
+
CURRENT_SCHEMA_VERSION,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
__version__ = "1.0.0"
|
|
102
|
+
__author__ = "Cascade Agentic Labs"
|
|
103
|
+
__license__ = "Apache-2.0"
|
|
104
|
+
|
|
105
|
+
__all__ = [
|
|
106
|
+
# Version
|
|
107
|
+
"__version__",
|
|
108
|
+
# Models
|
|
109
|
+
"CascadeRecord",
|
|
110
|
+
"Medication",
|
|
111
|
+
"Condition",
|
|
112
|
+
"Allergy",
|
|
113
|
+
"LabResult",
|
|
114
|
+
"VitalSign",
|
|
115
|
+
"Immunization",
|
|
116
|
+
"Procedure",
|
|
117
|
+
"FamilyHistory",
|
|
118
|
+
"Coverage",
|
|
119
|
+
"PatientProfile",
|
|
120
|
+
"EmergencyContact",
|
|
121
|
+
"Address",
|
|
122
|
+
"PharmacyInfo",
|
|
123
|
+
"ActivitySnapshot",
|
|
124
|
+
"SleepSnapshot",
|
|
125
|
+
"HealthProfile",
|
|
126
|
+
# Type aliases
|
|
127
|
+
"ProvenanceType",
|
|
128
|
+
"ProvenanceClass",
|
|
129
|
+
"ConditionStatus",
|
|
130
|
+
"AllergySeverity",
|
|
131
|
+
"AllergyCategory",
|
|
132
|
+
"LabInterpretation",
|
|
133
|
+
"MedicationClinicalIntent",
|
|
134
|
+
"CourseOfTherapyType",
|
|
135
|
+
"PrescriptionCategory",
|
|
136
|
+
"SourceFhirResourceType",
|
|
137
|
+
"VitalType",
|
|
138
|
+
"VitalInterpretation",
|
|
139
|
+
"ImmunizationStatus",
|
|
140
|
+
"PlanType",
|
|
141
|
+
"CoverageType",
|
|
142
|
+
"SubscriberRelationship",
|
|
143
|
+
"BiologicalSex",
|
|
144
|
+
"AgeGroup",
|
|
145
|
+
"BloodType",
|
|
146
|
+
"ProcedureStatus",
|
|
147
|
+
# Serialization
|
|
148
|
+
"serialize",
|
|
149
|
+
"serialize_from_dict",
|
|
150
|
+
"serialize_medication",
|
|
151
|
+
"serialize_condition",
|
|
152
|
+
"serialize_allergy",
|
|
153
|
+
"serialize_lab_result",
|
|
154
|
+
"serialize_vital_sign",
|
|
155
|
+
"serialize_immunization",
|
|
156
|
+
"serialize_procedure",
|
|
157
|
+
"serialize_family_history",
|
|
158
|
+
"serialize_coverage",
|
|
159
|
+
"serialize_patient_profile",
|
|
160
|
+
"serialize_activity_snapshot",
|
|
161
|
+
"serialize_sleep_snapshot",
|
|
162
|
+
# Deserialization
|
|
163
|
+
"parse",
|
|
164
|
+
"parse_one",
|
|
165
|
+
# Validation
|
|
166
|
+
"validate",
|
|
167
|
+
"validate_dict",
|
|
168
|
+
"ValidationResult",
|
|
169
|
+
"ValidationError",
|
|
170
|
+
# Pod
|
|
171
|
+
"Pod",
|
|
172
|
+
"RecordSet",
|
|
173
|
+
# Vocabulary
|
|
174
|
+
"NAMESPACES",
|
|
175
|
+
"TYPE_MAPPING",
|
|
176
|
+
"TYPE_TO_MAPPING_KEY",
|
|
177
|
+
"PROPERTY_PREDICATES",
|
|
178
|
+
"CURRENT_SCHEMA_VERSION",
|
|
179
|
+
]
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Turtle parser for deserializing Cascade Protocol records.
|
|
3
|
+
|
|
4
|
+
Uses rdflib for robust Turtle parsing, then maps RDF triples back
|
|
5
|
+
to Python model objects using the PROPERTY_PREDICATES reverse map.
|
|
6
|
+
|
|
7
|
+
Supports:
|
|
8
|
+
- @prefix declarations
|
|
9
|
+
- Subject-predicate-object triples
|
|
10
|
+
- Typed literals (xsd:dateTime, xsd:date, xsd:integer, xsd:double)
|
|
11
|
+
- URI references
|
|
12
|
+
- Boolean literals
|
|
13
|
+
- RDF lists
|
|
14
|
+
- Blank nodes (PatientProfile nested objects)
|
|
15
|
+
- Multi-value predicates (repeated predicate with different objects)
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
>>> from cascade_protocol.deserializer import parse, parse_one
|
|
19
|
+
>>> meds = parse(turtle_string, "MedicationRecord")
|
|
20
|
+
>>> med = parse_one(turtle_string, "MedicationRecord")
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import Any, TYPE_CHECKING
|
|
26
|
+
|
|
27
|
+
from cascade_protocol.models.common import CascadeRecord
|
|
28
|
+
from cascade_protocol.models.medication import Medication
|
|
29
|
+
from cascade_protocol.models.condition import Condition
|
|
30
|
+
from cascade_protocol.models.allergy import Allergy
|
|
31
|
+
from cascade_protocol.models.lab_result import LabResult
|
|
32
|
+
from cascade_protocol.models.vital_sign import VitalSign
|
|
33
|
+
from cascade_protocol.models.immunization import Immunization
|
|
34
|
+
from cascade_protocol.models.procedure import Procedure
|
|
35
|
+
from cascade_protocol.models.family_history import FamilyHistory
|
|
36
|
+
from cascade_protocol.models.coverage import Coverage
|
|
37
|
+
from cascade_protocol.models.patient_profile import PatientProfile, EmergencyContact, Address, PharmacyInfo
|
|
38
|
+
from cascade_protocol.models.wellness import ActivitySnapshot, SleepSnapshot
|
|
39
|
+
from cascade_protocol.vocabularies.namespaces import (
|
|
40
|
+
NAMESPACES,
|
|
41
|
+
TYPE_MAPPING,
|
|
42
|
+
TYPE_TO_MAPPING_KEY,
|
|
43
|
+
build_reverse_predicate_map,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Reverse mappings
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
# VitalSign uses clinical: namespace for snomedCode and interpretation
|
|
51
|
+
_ADDITIONAL_REVERSE = {
|
|
52
|
+
f"{NAMESPACES['clinical']}snomedCode": "snomed_code",
|
|
53
|
+
f"{NAMESPACES['clinical']}interpretation": "interpretation",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_REVERSE_PREDICATE_MAP = build_reverse_predicate_map(_ADDITIONAL_REVERSE)
|
|
57
|
+
|
|
58
|
+
# Reverse type map: full RDF type URI -> (record_type_string, mapping_key)
|
|
59
|
+
def _build_reverse_type_map() -> dict[str, tuple[str, str]]:
|
|
60
|
+
result: dict[str, tuple[str, str]] = {}
|
|
61
|
+
for mapping_key, mapping in TYPE_MAPPING.items():
|
|
62
|
+
rdf_type = mapping["rdf_type"]
|
|
63
|
+
colon_idx = rdf_type.find(":")
|
|
64
|
+
if colon_idx >= 0:
|
|
65
|
+
ns_prefix = rdf_type[:colon_idx]
|
|
66
|
+
local_name = rdf_type[colon_idx + 1:]
|
|
67
|
+
ns_uri = NAMESPACES.get(ns_prefix)
|
|
68
|
+
if ns_uri:
|
|
69
|
+
result[f"{ns_uri}{local_name}"] = (local_name, mapping_key)
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
_REVERSE_TYPE_MAP = _build_reverse_type_map()
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# Field type classification
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
_BOOLEAN_FIELDS = {"is_active", "as_needed"}
|
|
79
|
+
|
|
80
|
+
_INTEGER_FIELDS = {
|
|
81
|
+
"computed_age", "refills_allowed", "supply_duration_days", "onset_age",
|
|
82
|
+
"steps", "active_minutes", "calories", "awakenings",
|
|
83
|
+
"total_sleep_minutes", "deep_sleep_minutes", "rem_sleep_minutes", "light_sleep_minutes",
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_FLOAT_FIELDS = {
|
|
87
|
+
"value", "reference_range_low", "reference_range_high", "distance",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_ARRAY_FIELDS = {
|
|
91
|
+
"drug_codes", "affects_vital_signs", "monitored_vital_signs",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# Record type -> model class mapping
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
_TYPE_CLASS_MAP: dict[str, type] = {
|
|
99
|
+
"MedicationRecord": Medication,
|
|
100
|
+
"ConditionRecord": Condition,
|
|
101
|
+
"AllergyRecord": Allergy,
|
|
102
|
+
"LabResultRecord": LabResult,
|
|
103
|
+
"VitalSign": VitalSign,
|
|
104
|
+
"ImmunizationRecord": Immunization,
|
|
105
|
+
"ProcedureRecord": Procedure,
|
|
106
|
+
"FamilyHistoryRecord": FamilyHistory,
|
|
107
|
+
"CoverageRecord": Coverage,
|
|
108
|
+
"InsurancePlan": Coverage,
|
|
109
|
+
"PatientProfile": PatientProfile,
|
|
110
|
+
"ActivitySnapshot": ActivitySnapshot,
|
|
111
|
+
"SleepSnapshot": SleepSnapshot,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
# Resolve type URI
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
def _resolve_type_uri(type_str: str) -> str | None:
|
|
119
|
+
"""Resolve a record type string (e.g. 'MedicationRecord') to a full RDF type URI."""
|
|
120
|
+
for mapping in TYPE_MAPPING.values():
|
|
121
|
+
rdf_type = mapping["rdf_type"]
|
|
122
|
+
colon_idx = rdf_type.find(":")
|
|
123
|
+
if colon_idx >= 0:
|
|
124
|
+
ns_prefix = rdf_type[:colon_idx]
|
|
125
|
+
local_name = rdf_type[colon_idx + 1:]
|
|
126
|
+
if local_name == type_str:
|
|
127
|
+
ns_uri = NAMESPACES.get(ns_prefix)
|
|
128
|
+
if ns_uri:
|
|
129
|
+
return f"{ns_uri}{local_name}"
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
# rdflib-based parsing
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
def _parse_with_rdflib(turtle: str) -> list[dict[str, Any]]:
|
|
137
|
+
"""
|
|
138
|
+
Parse Turtle content using rdflib and extract all typed subjects.
|
|
139
|
+
|
|
140
|
+
Returns a list of dicts, one per unique subject, with an internal
|
|
141
|
+
``_rdf_type`` key set to the full RDF type URI.
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
import rdflib
|
|
145
|
+
from rdflib import Graph, URIRef, Literal, BNode
|
|
146
|
+
from rdflib.namespace import RDF, XSD
|
|
147
|
+
except ImportError:
|
|
148
|
+
raise ImportError(
|
|
149
|
+
"rdflib is required for Turtle parsing. "
|
|
150
|
+
"Install it with: pip install rdflib"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
g = Graph()
|
|
154
|
+
g.parse(data=turtle, format="turtle")
|
|
155
|
+
|
|
156
|
+
RDF_TYPE = RDF.type
|
|
157
|
+
CASCADE_NS = NAMESPACES["cascade"]
|
|
158
|
+
|
|
159
|
+
# Group triples by subject
|
|
160
|
+
subject_triples: dict[str, list[tuple[str, Any, str]]] = {}
|
|
161
|
+
for s, p, o in g:
|
|
162
|
+
subj_str = str(s)
|
|
163
|
+
if isinstance(s, BNode):
|
|
164
|
+
subj_str = f"_:{s}"
|
|
165
|
+
subject_triples.setdefault(subj_str, [])
|
|
166
|
+
subject_triples[subj_str].append((str(p), o, subj_str))
|
|
167
|
+
|
|
168
|
+
results: list[dict[str, Any]] = []
|
|
169
|
+
|
|
170
|
+
for subj_str, triples in subject_triples.items():
|
|
171
|
+
# Find rdf:type
|
|
172
|
+
rdf_type_uri: str | None = None
|
|
173
|
+
for pred_uri, obj, _ in triples:
|
|
174
|
+
if pred_uri == str(RDF_TYPE):
|
|
175
|
+
rdf_type_uri = str(obj)
|
|
176
|
+
break
|
|
177
|
+
|
|
178
|
+
if rdf_type_uri is None:
|
|
179
|
+
continue # Skip subjects without a type
|
|
180
|
+
|
|
181
|
+
# Check if it's a known Cascade type
|
|
182
|
+
type_info = _REVERSE_TYPE_MAP.get(rdf_type_uri)
|
|
183
|
+
if type_info is None:
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
record_type, _ = type_info
|
|
187
|
+
|
|
188
|
+
record: dict[str, Any] = {
|
|
189
|
+
"id": subj_str,
|
|
190
|
+
"type": record_type,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# Group by predicate (for repeated predicates -> arrays)
|
|
194
|
+
pred_values: dict[str, list[Any]] = {}
|
|
195
|
+
for pred_uri, obj, _ in triples:
|
|
196
|
+
if pred_uri == str(RDF_TYPE):
|
|
197
|
+
continue
|
|
198
|
+
pred_values.setdefault(pred_uri, [])
|
|
199
|
+
pred_values[pred_uri].append(obj)
|
|
200
|
+
|
|
201
|
+
for pred_uri, objects in pred_values.items():
|
|
202
|
+
py_key = _REVERSE_PREDICATE_MAP.get(pred_uri)
|
|
203
|
+
if not py_key:
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
# Array fields
|
|
207
|
+
if py_key in _ARRAY_FIELDS:
|
|
208
|
+
values: list[Any] = []
|
|
209
|
+
for obj in objects:
|
|
210
|
+
if isinstance(obj, (rdflib.URIRef,)):
|
|
211
|
+
values.append(str(obj))
|
|
212
|
+
elif isinstance(obj, Literal):
|
|
213
|
+
values.append(str(obj))
|
|
214
|
+
elif hasattr(obj, "__iter__"):
|
|
215
|
+
# RDF collection
|
|
216
|
+
try:
|
|
217
|
+
for item in obj:
|
|
218
|
+
values.append(str(item))
|
|
219
|
+
except Exception:
|
|
220
|
+
values.append(str(obj))
|
|
221
|
+
else:
|
|
222
|
+
values.append(str(obj))
|
|
223
|
+
record[py_key] = values
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
# Single-value fields: use first object
|
|
227
|
+
obj = objects[0]
|
|
228
|
+
|
|
229
|
+
# dataProvenance: extract local name from cascade namespace
|
|
230
|
+
if py_key == "data_provenance":
|
|
231
|
+
obj_str = str(obj)
|
|
232
|
+
if obj_str.startswith(CASCADE_NS):
|
|
233
|
+
record[py_key] = obj_str[len(CASCADE_NS):]
|
|
234
|
+
else:
|
|
235
|
+
record[py_key] = obj_str
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
# Boolean fields
|
|
239
|
+
if py_key in _BOOLEAN_FIELDS:
|
|
240
|
+
if isinstance(obj, Literal):
|
|
241
|
+
record[py_key] = str(obj).lower() == "true"
|
|
242
|
+
else:
|
|
243
|
+
record[py_key] = str(obj).lower() == "true"
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
# Integer fields
|
|
247
|
+
if py_key in _INTEGER_FIELDS:
|
|
248
|
+
try:
|
|
249
|
+
record[py_key] = int(str(obj))
|
|
250
|
+
except (ValueError, TypeError):
|
|
251
|
+
record[py_key] = str(obj)
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
# Float fields
|
|
255
|
+
if py_key in _FLOAT_FIELDS:
|
|
256
|
+
try:
|
|
257
|
+
record[py_key] = float(str(obj))
|
|
258
|
+
except (ValueError, TypeError):
|
|
259
|
+
record[py_key] = str(obj)
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
# Typed literals
|
|
263
|
+
if isinstance(obj, Literal):
|
|
264
|
+
if obj.datatype == XSD.integer:
|
|
265
|
+
try:
|
|
266
|
+
record[py_key] = int(str(obj))
|
|
267
|
+
except ValueError:
|
|
268
|
+
record[py_key] = str(obj)
|
|
269
|
+
elif obj.datatype in (XSD.double, XSD.decimal, XSD.float):
|
|
270
|
+
try:
|
|
271
|
+
record[py_key] = float(str(obj))
|
|
272
|
+
except ValueError:
|
|
273
|
+
record[py_key] = str(obj)
|
|
274
|
+
elif obj.datatype == XSD.boolean:
|
|
275
|
+
record[py_key] = str(obj).lower() == "true"
|
|
276
|
+
else:
|
|
277
|
+
record[py_key] = str(obj)
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
# URI reference
|
|
281
|
+
if isinstance(obj, rdflib.URIRef):
|
|
282
|
+
record[py_key] = str(obj)
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
# Default
|
|
286
|
+
record[py_key] = str(obj)
|
|
287
|
+
|
|
288
|
+
results.append(record)
|
|
289
|
+
|
|
290
|
+
return results
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _dict_to_record(data: dict[str, Any]) -> CascadeRecord | None:
|
|
294
|
+
"""Convert a parsed dict to the appropriate CascadeRecord subclass."""
|
|
295
|
+
record_type = data.get("type", "")
|
|
296
|
+
cls = _TYPE_CLASS_MAP.get(record_type)
|
|
297
|
+
if cls is None:
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
from dataclasses import fields as dc_fields
|
|
301
|
+
valid_keys = {f.name for f in dc_fields(cls)}
|
|
302
|
+
kwargs = {k: v for k, v in data.items() if k in valid_keys}
|
|
303
|
+
return cls(**kwargs) # type: ignore[call-arg]
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def parse(turtle: str, record_type: str) -> list[CascadeRecord]:
|
|
307
|
+
"""
|
|
308
|
+
Parse Turtle content and return typed records matching the specified type.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
turtle: Turtle document content.
|
|
312
|
+
record_type: Record type string (e.g., ``"MedicationRecord"``, ``"VitalSign"``).
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
List of parsed records of the specified type.
|
|
316
|
+
|
|
317
|
+
Raises:
|
|
318
|
+
ValueError: If the record type is unknown.
|
|
319
|
+
ImportError: If rdflib is not installed.
|
|
320
|
+
|
|
321
|
+
Example:
|
|
322
|
+
>>> meds = parse(turtle_string, "MedicationRecord")
|
|
323
|
+
"""
|
|
324
|
+
type_uri = _resolve_type_uri(record_type)
|
|
325
|
+
if type_uri is None:
|
|
326
|
+
raise ValueError(f"Unknown record type: {record_type!r}")
|
|
327
|
+
|
|
328
|
+
all_records = _parse_with_rdflib(turtle)
|
|
329
|
+
matching = [r for r in all_records if r.get("type") == record_type]
|
|
330
|
+
|
|
331
|
+
result: list[CascadeRecord] = []
|
|
332
|
+
for data in matching:
|
|
333
|
+
rec = _dict_to_record(data)
|
|
334
|
+
if rec is not None:
|
|
335
|
+
result.append(rec)
|
|
336
|
+
return result
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def parse_one(turtle: str, record_type: str) -> CascadeRecord | None:
|
|
340
|
+
"""
|
|
341
|
+
Parse a single record from Turtle content.
|
|
342
|
+
|
|
343
|
+
Returns the first record matching the specified type, or ``None`` if none found.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
turtle: Turtle document content.
|
|
347
|
+
record_type: Record type string.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
The parsed record, or None.
|
|
351
|
+
"""
|
|
352
|
+
results = parse(turtle, record_type)
|
|
353
|
+
return results[0] if results else None
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cascade Protocol data models.
|
|
3
|
+
|
|
4
|
+
All record types available as top-level imports from this package.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from cascade_protocol.models.common import (
|
|
8
|
+
CascadeRecord,
|
|
9
|
+
ProvenanceType,
|
|
10
|
+
ProvenanceClass,
|
|
11
|
+
ConditionStatus,
|
|
12
|
+
AllergySeverity,
|
|
13
|
+
AllergyCategory,
|
|
14
|
+
LabInterpretation,
|
|
15
|
+
MedicationClinicalIntent,
|
|
16
|
+
CourseOfTherapyType,
|
|
17
|
+
PrescriptionCategory,
|
|
18
|
+
SourceFhirResourceType,
|
|
19
|
+
VitalType,
|
|
20
|
+
VitalInterpretation,
|
|
21
|
+
ImmunizationStatus,
|
|
22
|
+
PlanType,
|
|
23
|
+
CoverageType,
|
|
24
|
+
SubscriberRelationship,
|
|
25
|
+
BiologicalSex,
|
|
26
|
+
AgeGroup,
|
|
27
|
+
BloodType,
|
|
28
|
+
ProcedureStatus,
|
|
29
|
+
)
|
|
30
|
+
from cascade_protocol.models.medication import Medication
|
|
31
|
+
from cascade_protocol.models.condition import Condition
|
|
32
|
+
from cascade_protocol.models.allergy import Allergy
|
|
33
|
+
from cascade_protocol.models.lab_result import LabResult
|
|
34
|
+
from cascade_protocol.models.vital_sign import VitalSign
|
|
35
|
+
from cascade_protocol.models.immunization import Immunization
|
|
36
|
+
from cascade_protocol.models.procedure import Procedure
|
|
37
|
+
from cascade_protocol.models.family_history import FamilyHistory
|
|
38
|
+
from cascade_protocol.models.coverage import Coverage
|
|
39
|
+
from cascade_protocol.models.patient_profile import PatientProfile, EmergencyContact, Address, PharmacyInfo
|
|
40
|
+
from cascade_protocol.models.wellness import ActivitySnapshot, SleepSnapshot
|
|
41
|
+
from cascade_protocol.models.health_profile import HealthProfile
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
# Base
|
|
45
|
+
"CascadeRecord",
|
|
46
|
+
# Type aliases
|
|
47
|
+
"ProvenanceType",
|
|
48
|
+
"ProvenanceClass",
|
|
49
|
+
"ConditionStatus",
|
|
50
|
+
"AllergySeverity",
|
|
51
|
+
"AllergyCategory",
|
|
52
|
+
"LabInterpretation",
|
|
53
|
+
"MedicationClinicalIntent",
|
|
54
|
+
"CourseOfTherapyType",
|
|
55
|
+
"PrescriptionCategory",
|
|
56
|
+
"SourceFhirResourceType",
|
|
57
|
+
"VitalType",
|
|
58
|
+
"VitalInterpretation",
|
|
59
|
+
"ImmunizationStatus",
|
|
60
|
+
"PlanType",
|
|
61
|
+
"CoverageType",
|
|
62
|
+
"SubscriberRelationship",
|
|
63
|
+
"BiologicalSex",
|
|
64
|
+
"AgeGroup",
|
|
65
|
+
"BloodType",
|
|
66
|
+
"ProcedureStatus",
|
|
67
|
+
# Record types
|
|
68
|
+
"Medication",
|
|
69
|
+
"Condition",
|
|
70
|
+
"Allergy",
|
|
71
|
+
"LabResult",
|
|
72
|
+
"VitalSign",
|
|
73
|
+
"Immunization",
|
|
74
|
+
"Procedure",
|
|
75
|
+
"FamilyHistory",
|
|
76
|
+
"Coverage",
|
|
77
|
+
"PatientProfile",
|
|
78
|
+
"EmergencyContact",
|
|
79
|
+
"Address",
|
|
80
|
+
"PharmacyInfo",
|
|
81
|
+
"ActivitySnapshot",
|
|
82
|
+
"SleepSnapshot",
|
|
83
|
+
"HealthProfile",
|
|
84
|
+
]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Allergy data model for the Cascade Protocol.
|
|
3
|
+
|
|
4
|
+
Represents an allergy or intolerance record, sourced from EHR imports
|
|
5
|
+
or self-reported by the patient.
|
|
6
|
+
|
|
7
|
+
RDF type: ``health:AllergyRecord``
|
|
8
|
+
Vocabulary: https://ns.cascadeprotocol.org/health/v1#
|
|
9
|
+
|
|
10
|
+
See: https://cascadeprotocol.org/docs/cascade-protocol-schemas
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
|
|
17
|
+
from cascade_protocol.models.common import CascadeRecord
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Allergy(CascadeRecord):
|
|
22
|
+
"""
|
|
23
|
+
An allergy record in the Cascade Protocol.
|
|
24
|
+
|
|
25
|
+
Required fields: ``allergen``, ``data_provenance``, ``schema_version``.
|
|
26
|
+
All date fields use ISO 8601 string format.
|
|
27
|
+
|
|
28
|
+
Serializes as ``health:AllergyRecord`` in Turtle.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
type: str = field(default="AllergyRecord", init=True)
|
|
32
|
+
|
|
33
|
+
allergen: str = ""
|
|
34
|
+
"""
|
|
35
|
+
Name of the allergen substance.
|
|
36
|
+
Maps to ``health:allergen`` in Turtle serialization.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
allergy_category: str | None = None
|
|
40
|
+
"""
|
|
41
|
+
Category of the allergen (e.g., ``"medication"``, ``"food"``, ``"environmental"``).
|
|
42
|
+
Maps to ``health:allergyCategory`` in Turtle serialization.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
reaction: str | None = None
|
|
46
|
+
"""
|
|
47
|
+
Description of the allergic reaction (e.g., ``"Hives (urticaria)"``).
|
|
48
|
+
Maps to ``health:reaction`` in Turtle serialization.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
allergy_severity: str | None = None
|
|
52
|
+
"""
|
|
53
|
+
Severity of the allergic reaction (mild, moderate, severe, life-threatening).
|
|
54
|
+
Maps to ``health:allergySeverity`` in Turtle serialization.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
onset_date: str | None = None
|
|
58
|
+
"""
|
|
59
|
+
Date of allergy onset (ISO 8601).
|
|
60
|
+
Maps to ``health:onsetDate`` in Turtle serialization.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def from_dataframe(cls, df: "pd.DataFrame") -> list["Allergy"]: # type: ignore[name-defined]
|
|
65
|
+
"""Reconstruct a list of Allergy records from a pandas DataFrame."""
|
|
66
|
+
from cascade_protocol.pandas_integration.dataframe import dataframe_to_records
|
|
67
|
+
return dataframe_to_records(df, cls) # type: ignore[return-value]
|