Skip to content

Commit b1ffb41

Browse files
fix: handle missing events and imprecise date parsing in meetup_query
- Catch TypeError alongside KeyError in format_response so null intermediate API fields (memberEvents, events) return empty DataFrame instead of crashing - Use .get() for safer navigation of groupByUrlname and events fields - Fix year-boundary bug in sort_json and prepare_events where year-less human-readable dates (e.g. "Thu 1/2 6:00 pm") got assigned the wrong year near Dec/Jan boundary - Add 7 unit tests covering empty event lists, null API fields, and year-boundary date parsing for both ISO and human-readable formats Closes: TASK-017 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1067c4a commit b1ffb41

File tree

2 files changed

+134
-7
lines changed

2 files changed

+134
-7
lines changed

app/meetup_query.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -285,16 +285,18 @@ def format_response(response, location: str = "Oklahoma City", exclusions: str =
285285
data = response_json['data']['self']['memberEvents']['edges']
286286
if data and len(data) > 0 and data[0]['node']['group']['city'] != location:
287287
print(f"{Fore.YELLOW}{warning:<10}{Fore.RESET}Skipping event outside of {location}")
288-
except KeyError:
288+
except (KeyError, TypeError):
289289
try:
290-
if response_json['data'].get('groupByUrlname') is None:
290+
group = response_json['data'].get('groupByUrlname')
291+
if group is None:
291292
data = ""
292293
print(f"{Fore.YELLOW}{warning:<10}{Fore.RESET}Skipping group due to empty response")
293294
else:
294-
data = response_json['data']['groupByUrlname']['events']['edges']
295-
if response_json['data']['groupByUrlname']['city'] != location:
295+
events = group.get('events')
296+
data = events['edges'] if events else ""
297+
if data and group.get('city') != location:
296298
print(f"{Fore.RED}{error:<10}{Fore.RESET}No data for {location} found")
297-
except KeyError as e:
299+
except (KeyError, TypeError) as e:
298300
print(f"{Fore.RED}{error:<10}{Fore.RESET}KeyError accessing GraphQL data: {e}")
299301
print(f"{Fore.RED}{error:<10}{Fore.RESET}Response structure: {json.dumps(response_json, indent=2)[:500]}")
300302
data = ""
@@ -390,7 +392,10 @@ def sort_json(filename) -> None:
390392
try:
391393
parsed = arrow.get(value, 'ddd M/D h:mm a')
392394
if parsed.year == 1:
393-
parsed = parsed.replace(year=arrow.now(tz).year)
395+
now = arrow.now(tz)
396+
parsed = parsed.replace(year=now.year)
397+
if parsed < now.shift(months=-6):
398+
parsed = parsed.replace(year=now.year + 1)
394399
dates[key] = parsed.format('YYYY-MM-DDTHH:mm:ss')
395400
except ParserError:
396401
try:
@@ -443,7 +448,10 @@ def prepare_events(df) -> list[dict]:
443448
try:
444449
parsed = arrow.get(value, 'ddd M/D h:mm a')
445450
if parsed.year == 1:
446-
parsed = parsed.replace(year=arrow.now(tz).year)
451+
now = arrow.now(tz)
452+
parsed = parsed.replace(year=now.year)
453+
if parsed < now.shift(months=-6):
454+
parsed = parsed.replace(year=now.year + 1)
447455
dates[key] = parsed.format('YYYY-MM-DDTHH:mm:ss')
448456
except ParserError:
449457
try:

tests/test_unit.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,6 +1031,125 @@ def test_prepare_events_empty():
10311031
assert prepare_events(df) == []
10321032

10331033

1034+
@pytest.mark.unit
1035+
def test_format_response_empty_edges():
1036+
"""format_response returns empty DataFrame when edges list is empty."""
1037+
response = json.dumps({"data": {"self": {"memberEvents": {"edges": []}}}})
1038+
with patch("arrow.now", return_value=arrow.get("2024-09-18").to("America/Chicago")):
1039+
df = format_response(response)
1040+
assert df.empty
1041+
1042+
1043+
@pytest.mark.unit
1044+
def test_format_response_null_member_events():
1045+
"""format_response returns empty DataFrame when memberEvents is null."""
1046+
response = json.dumps({"data": {"self": {"memberEvents": None}}})
1047+
with patch("arrow.now", return_value=arrow.get("2024-09-18").to("America/Chicago")):
1048+
df = format_response(response)
1049+
assert df.empty
1050+
1051+
1052+
@pytest.mark.unit
1053+
def test_format_response_null_group_events():
1054+
"""format_response returns empty DataFrame when groupByUrlname events is null."""
1055+
response = json.dumps({"data": {"groupByUrlname": {"events": None, "city": "Oklahoma City"}}})
1056+
with patch("arrow.now", return_value=arrow.get("2024-09-18").to("America/Chicago")):
1057+
df = format_response(response)
1058+
assert df.empty
1059+
1060+
1061+
@pytest.mark.unit
1062+
def test_sort_json_year_boundary(tmp_path):
1063+
"""sort_json preserves year from ISO 8601 dates near year boundaries."""
1064+
test_json = tmp_path / "test.json"
1065+
data = [
1066+
{"date": "2025-01-02T18:00:00", "eventUrl": "url1"},
1067+
{"date": "2024-12-30T10:00:00", "eventUrl": "url2"},
1068+
]
1069+
with open(test_json, "w") as f:
1070+
json.dump(data, f)
1071+
1072+
with (
1073+
patch("meetup_query.json_fn", str(test_json)),
1074+
patch("arrow.now", return_value=arrow.get("2024-12-29")),
1075+
):
1076+
sort_json(test_json)
1077+
1078+
with open(test_json) as f:
1079+
sorted_data = json.load(f)
1080+
1081+
assert len(sorted_data) == 2
1082+
assert sorted_data[0]["eventUrl"] == "url2"
1083+
assert sorted_data[1]["eventUrl"] == "url1"
1084+
assert "12/30" in sorted_data[0]["date"]
1085+
assert "1/2" in sorted_data[1]["date"]
1086+
1087+
1088+
@pytest.mark.unit
1089+
def test_sort_json_human_readable_year_boundary(tmp_path):
1090+
"""sort_json with human-readable dates near year boundary should not guess wrong year."""
1091+
test_json = tmp_path / "test.json"
1092+
data = [
1093+
{"date": "Thu 1/2 6:00 pm", "eventUrl": "url1"},
1094+
{"date": "Mon 12/30 10:00 am", "eventUrl": "url2"},
1095+
]
1096+
with open(test_json, "w") as f:
1097+
json.dump(data, f)
1098+
1099+
with (
1100+
patch("meetup_query.json_fn", str(test_json)),
1101+
patch("arrow.now", return_value=arrow.get("2024-12-29")),
1102+
):
1103+
sort_json(test_json)
1104+
1105+
with open(test_json) as f:
1106+
sorted_data = json.load(f)
1107+
1108+
assert len(sorted_data) == 2
1109+
assert sorted_data[0]["eventUrl"] == "url2"
1110+
assert sorted_data[1]["eventUrl"] == "url1"
1111+
1112+
1113+
@pytest.mark.unit
1114+
def test_prepare_events_human_readable_year_boundary():
1115+
"""prepare_events with human-readable dates near year boundary should not guess wrong year."""
1116+
df = pd.DataFrame({
1117+
"name": ["Group A", "Group B"],
1118+
"date": ["Thu 1/2 6:00 pm", "Mon 12/30 10:00 am"],
1119+
"title": ["Jan Event", "Dec Event"],
1120+
"description": ["desc", "desc"],
1121+
"city": ["OKC", "OKC"],
1122+
"eventUrl": ["url1", "url2"],
1123+
})
1124+
with patch("arrow.now", return_value=arrow.get("2024-12-29")):
1125+
result = prepare_events(df)
1126+
1127+
assert len(result) == 2
1128+
assert result[0]["eventUrl"] == "url2"
1129+
assert result[1]["eventUrl"] == "url1"
1130+
1131+
1132+
@pytest.mark.unit
1133+
def test_prepare_events_year_boundary():
1134+
"""prepare_events preserves year from ISO 8601 dates near year boundaries."""
1135+
df = pd.DataFrame({
1136+
"name": ["Group A", "Group B"],
1137+
"date": ["2025-01-02T18:00:00", "2024-12-30T10:00:00"],
1138+
"title": ["Jan Event", "Dec Event"],
1139+
"description": ["desc", "desc"],
1140+
"city": ["OKC", "OKC"],
1141+
"eventUrl": ["url1", "url2"],
1142+
})
1143+
with patch("arrow.now", return_value=arrow.get("2024-12-29")):
1144+
result = prepare_events(df)
1145+
1146+
assert len(result) == 2
1147+
assert result[0]["eventUrl"] == "url2"
1148+
assert result[1]["eventUrl"] == "url1"
1149+
assert "12/30" in result[0]["date"]
1150+
assert "1/2" in result[1]["date"]
1151+
1152+
10341153
@pytest.mark.unit
10351154
@patch("meetup_query.gen_token")
10361155
@patch("meetup_query.send_request")

0 commit comments

Comments
 (0)