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.
@@ -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,10 @@
1
+ """
2
+ Cascade Protocol Turtle deserializer.
3
+ """
4
+
5
+ from cascade_protocol.deserializer.turtle_parser import (
6
+ parse,
7
+ parse_one,
8
+ )
9
+
10
+ __all__ = ["parse", "parse_one"]
@@ -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]