Skip to content

Commit 9745240

Browse files
Pigbibicodex
andcommitted
feat: add official event entity schema fields
Co-Authored-By: Codex <noreply@openai.com>
1 parent 928d4f5 commit 9745240

2 files changed

Lines changed: 119 additions & 1 deletion

File tree

src/political_event_tracking_research/official_event_import.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@
3434
}
3535
)
3636

37+
ALLOWED_ENTITY_MATCH_TYPES = frozenset(
38+
{"issuer", "direct_beneficiary", "industry_context", "unverified"}
39+
)
40+
3741

3842
@dataclass(frozen=True)
3943
class OfficialRecord:
@@ -45,6 +49,9 @@ class OfficialRecord:
4549
direction: str
4650
source_url: str
4751
summary: str
52+
entity_match_type: str = "unverified"
53+
match_evidence: str = ""
54+
relationship_type: str = ""
4855

4956

5057
def slug(value: str) -> str:
@@ -76,6 +83,9 @@ def load_official_records(path: str | Path) -> list[OfficialRecord]:
7683
direction=row.get("direction", ""),
7784
source_url=row["source_url"],
7885
summary=row.get("summary", ""),
86+
entity_match_type=row.get("entity_match_type") or "unverified",
87+
match_evidence=row.get("match_evidence", ""),
88+
relationship_type=row.get("relationship_type", ""),
7989
)
8090
)
8191
return records
@@ -89,6 +99,10 @@ def validate_record(record: OfficialRecord) -> None:
8999
raise ValueError(f"{record.record_id}: symbol is required")
90100
if record.event_type not in ALLOWED_EVENT_TYPES:
91101
raise ValueError(f"{record.record_id}: unsupported event_type {record.event_type!r}")
102+
if record.entity_match_type not in ALLOWED_ENTITY_MATCH_TYPES:
103+
raise ValueError(
104+
f"{record.record_id}: unsupported entity_match_type {record.entity_match_type!r}"
105+
)
92106
if record.source_type in GOVERNMENT_SOURCE_TYPES:
93107
if not is_government_url(record.source_url):
94108
raise ValueError(f"{record.record_id}: government source URLs must be https .gov URLs")
@@ -125,6 +139,9 @@ def normalize_records(records: list[OfficialRecord]) -> list[dict[str, str]]:
125139
"confidence": confidence_for_source(record),
126140
"source_url": record.source_url,
127141
"notes": record.summary,
142+
"entity_match_type": record.entity_match_type,
143+
"match_evidence": record.match_evidence,
144+
"relationship_type": record.relationship_type,
128145
}
129146
)
130147
rows.sort(key=lambda row: (row["event_date"], row["symbol"], row["event_id"]))
@@ -136,7 +153,19 @@ def import_official_events(input_path: str | Path, output_path: str | Path) -> l
136153
rows = normalize_records(records)
137154
write_csv_rows(
138155
output_path,
139-
["event_id", "event_date", "symbol", "event_type", "direction", "confidence", "source_url", "notes"],
156+
[
157+
"event_id",
158+
"event_date",
159+
"symbol",
160+
"event_type",
161+
"direction",
162+
"confidence",
163+
"source_url",
164+
"notes",
165+
"entity_match_type",
166+
"match_evidence",
167+
"relationship_type",
168+
],
140169
rows,
141170
)
142171
return rows

tests/test_official_event_import.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import csv
34
from pathlib import Path
45

56
import pytest
@@ -30,6 +31,94 @@ def test_import_official_events_normalizes_to_event_schema(tmp_path: Path) -> No
3031
assert rows[-1]["confidence"] == "low"
3132

3233

34+
def test_legacy_input_defaults_entity_schema_fields_and_serializes_stably(tmp_path: Path) -> None:
35+
output = tmp_path / "events.csv"
36+
37+
rows = import_official_events(ROOT / "examples/official_records.example.csv", output)
38+
39+
assert all(row["entity_match_type"] == "unverified" for row in rows)
40+
assert all(row["match_evidence"] == "" for row in rows)
41+
assert all(row["relationship_type"] == "" for row in rows)
42+
with output.open(newline="", encoding="utf-8") as handle:
43+
assert next(csv.reader(handle)) == [
44+
"event_id",
45+
"event_date",
46+
"symbol",
47+
"event_type",
48+
"direction",
49+
"confidence",
50+
"source_url",
51+
"notes",
52+
"entity_match_type",
53+
"match_evidence",
54+
"relationship_type",
55+
]
56+
57+
58+
def test_entity_schema_fields_are_imported_and_normalized() -> None:
59+
record = OfficialRecord(
60+
record_id="entity-record",
61+
record_date="2026-01-10",
62+
symbol="EVT",
63+
source_type="government_filing",
64+
event_type="disclosure_buy",
65+
direction="bullish",
66+
source_url="https://www.sec.gov/example/entity-record",
67+
summary="Entity evidence.",
68+
entity_match_type="direct_beneficiary",
69+
match_evidence="Named beneficiary in filing.",
70+
relationship_type="supplier",
71+
)
72+
73+
assert normalize_records([record])[0] == {
74+
"event_id": "official-government-filing-entity-record",
75+
"event_date": "2026-01-10",
76+
"symbol": "EVT",
77+
"event_type": "disclosure_buy",
78+
"direction": "bullish",
79+
"confidence": "high",
80+
"source_url": "https://www.sec.gov/example/entity-record",
81+
"notes": "Entity evidence.",
82+
"entity_match_type": "direct_beneficiary",
83+
"match_evidence": "Named beneficiary in filing.",
84+
"relationship_type": "supplier",
85+
}
86+
87+
88+
@pytest.mark.parametrize("entity_match_type", ["issuer", "direct_beneficiary", "industry_context", "unverified"])
89+
def test_entity_match_type_accepts_only_contract_values(entity_match_type: str) -> None:
90+
record = OfficialRecord(
91+
record_id="entity-record",
92+
record_date="2026-01-10",
93+
symbol="EVT",
94+
source_type="government_filing",
95+
event_type="disclosure_buy",
96+
direction="bullish",
97+
source_url="https://www.sec.gov/example/entity-record",
98+
summary="Entity evidence.",
99+
entity_match_type=entity_match_type,
100+
)
101+
102+
normalize_records([record])
103+
104+
105+
def test_entity_match_type_rejects_unknown_values() -> None:
106+
record = OfficialRecord(
107+
record_id="entity-record",
108+
record_date="2026-01-10",
109+
symbol="EVT",
110+
source_type="government_filing",
111+
event_type="disclosure_buy",
112+
direction="bullish",
113+
source_url="https://www.sec.gov/example/entity-record",
114+
summary="Entity evidence.",
115+
entity_match_type="inferred",
116+
)
117+
118+
with pytest.raises(ValueError, match="unsupported entity_match_type"):
119+
normalize_records([record])
120+
121+
33122
def test_government_records_reject_non_gov_urls() -> None:
34123
record = OfficialRecord(
35124
record_id="bad-media-record",

0 commit comments

Comments
 (0)