Improve conference importer and series attendance

This commit is contained in:
Edward Betts 2026-06-22 13:08:32 +01:00
parent dbce9e5358
commit a87c9f993e
5 changed files with 87 additions and 0 deletions

View file

@ -502,6 +502,42 @@ def parse_yaml_datetime(value: typing.Any) -> datetime | None:
return None return None
def parse_yaml_date_value(value: typing.Any) -> date | datetime | None:
"""Convert YAML date/datetime strings to date-like values."""
if isinstance(value, datetime):
return value
if isinstance(value, date):
return value
if not isinstance(value, str):
return None
try:
if " " in value or "T" in value:
return datetime.fromisoformat(value)
return date.fromisoformat(value)
except ValueError:
return None
def normalize_date_values(conf: dict[str, typing.Any]) -> None:
"""Normalize quoted ISO date/datetime values produced by the LLM."""
dates = conf.get("dates")
if isinstance(dates, dict):
for field in ("start", "end", "earliest", "latest"):
if field in dates:
parsed = parse_yaml_date_value(dates[field])
if parsed is not None:
dates[field] = parsed
for field in ("start", "end"):
if field in conf:
parsed = parse_yaml_date_value(conf[field])
if parsed is not None:
conf[field] = parsed
def same_type_as_start( def same_type_as_start(
start_value: typing.Any, start_value: typing.Any,
new_dt: datetime, new_dt: datetime,
@ -529,6 +565,7 @@ def same_type_as_start(
def normalize_dates_field(conf: dict[str, typing.Any]) -> None: def normalize_dates_field(conf: dict[str, typing.Any]) -> None:
"""Move legacy top-level date fields into the nested dates mapping.""" """Move legacy top-level date fields into the nested dates mapping."""
normalize_date_values(conf)
raw_dates = conf.get("dates") raw_dates = conf.get("dates")
dates = raw_dates if isinstance(raw_dates, dict) else None dates = raw_dates if isinstance(raw_dates, dict) else None
@ -550,6 +587,20 @@ def normalize_dates_field(conf: dict[str, typing.Any]) -> None:
conf.pop("start", None) conf.pop("start", None)
conf.pop("end", None) conf.pop("end", None)
conf.pop("date_status", None) conf.pop("date_status", None)
normalize_date_values(conf)
def validate_generated_conference(conf: dict[str, typing.Any]) -> None:
"""Validate generated conference YAML before inserting it."""
try:
conference_date_fields(conf)
except ValueError as exc:
generated_yaml = yaml.dump(conf, sort_keys=False, allow_unicode=True).strip()
raise ValueError(
"Generated conference YAML is missing valid date information. "
"Expected nested `dates:` with exact/tentative start/end or "
f"approximate earliest/latest.\n\nGenerated YAML:\n{generated_yaml}"
) from exc
def maybe_extract_explicit_end_time(source_text: str) -> int | None: def maybe_extract_explicit_end_time(source_text: str) -> int | None:
@ -661,6 +712,7 @@ def add_new_conference(url: str, yaml_path: str) -> bool:
normalize_dates_field(new_conf) normalize_dates_field(new_conf)
normalise_end_field(new_conf, source_text) normalise_end_field(new_conf, source_text)
normalize_dates_field(new_conf) normalize_dates_field(new_conf)
validate_generated_conference(new_conf)
if detected_coordinates is not None: if detected_coordinates is not None:
new_conf["latitude"] = detected_coordinates[0] new_conf["latitude"] = detected_coordinates[0]

View file

@ -21,6 +21,9 @@
<tr> <tr>
<td> <td>
<a href="{{ url_for('conference_series_page', series_id=item.id) }}">{{ item.name }}</a> <a href="{{ url_for('conference_series_page', series_id=item.id) }}">{{ item.name }}</a>
{% if item.attended %}
<span class="badge text-bg-success ms-1">attended</span>
{% endif %}
{% if item.url %} {% if item.url %}
<a class="text-muted ms-1 text-decoration-none" href="{{ item.url }}"></a> <a class="text-muted ms-1 text-decoration-none" href="{{ item.url }}"></a>
{% endif %} {% endif %}

View file

@ -145,6 +145,35 @@ def test_normalize_dates_field_moves_legacy_dates() -> None:
} }
def test_normalize_dates_field_parses_quoted_dates() -> None:
"""Quoted ISO dates from generated YAML should become date objects."""
conf: dict[str, typing.Any] = {
"name": "Git Merge",
"dates": {
"status": "exact",
"start": "2026-09-16",
"end": "2026-09-17",
},
}
add_new_conference.normalize_dates_field(conf)
assert conf["dates"]["start"] == date(2026, 9, 16)
assert conf["dates"]["end"] == date(2026, 9, 17)
def test_validate_generated_conference_reports_missing_dates() -> None:
"""Missing generated dates should raise a clear importer error."""
conf: dict[str, typing.Any] = {
"name": "Git Merge",
"topic": "Git",
"location": "TBC",
}
with pytest.raises(ValueError, match="missing valid date information"):
add_new_conference.validate_generated_conference(conf)
def test_build_prompt_includes_nested_dates_and_series() -> None: def test_build_prompt_includes_nested_dates_and_series() -> None:
"""The prompt should describe nested dates and known series IDs.""" """The prompt should describe nested dates and known series IDs."""
prompt = add_new_conference.build_prompt( prompt = add_new_conference.build_prompt(

View file

@ -118,5 +118,6 @@ def test_conference_series_pages(tmp_path: typing.Any, monkeypatch: typing.Any)
assert index_response.status_code == 200 assert index_response.status_code == 200
assert b"PyCascades" in index_response.data assert b"PyCascades" in index_response.data
assert b"attended" in index_response.data
assert detail_response.status_code == 200 assert detail_response.status_code == 200
assert b"trip: Seattle Python trip" in detail_response.data assert b"trip: Seattle Python trip" in detail_response.data

View file

@ -413,12 +413,14 @@ def build_conference_series_list() -> list[StrDict]:
next_conf = next( next_conf = next(
(conf for conf in linked if conf["latest_date"] >= date.today()), None (conf for conf in linked if conf["latest_date"] >= date.today()), None
) )
attended = any(conf.get("going") or conf.get("linked_trip") for conf in linked)
item: StrDict = { item: StrDict = {
"id": series_id, "id": series_id,
**series, **series,
"count": len(linked), "count": len(linked),
"latest": latest, "latest": latest,
"next_conf": next_conf, "next_conf": next_conf,
"attended": attended,
} }
series_items.append(item) series_items.append(item)