Skip to content

Commit f9e1f21

Browse files
Merge pull request #204 from mcphersonlab/auto/conference-updates-20260612
Automated conference updates
2 parents b26dfe6 + 9bc2a6d commit f9e1f21

6 files changed

Lines changed: 365 additions & 16 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Update conferences
2+
3+
on:
4+
schedule:
5+
- cron: '0 6 1 * *' # monthly on the 1st at 06:00 UTC
6+
workflow_dispatch: {}
7+
8+
jobs:
9+
update:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: write
13+
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
18+
- name: Set up Python
19+
uses: actions/setup-python@v4
20+
with:
21+
python-version: '3.11'
22+
23+
- name: Install dependencies
24+
run: |
25+
python -m pip install --upgrade pip
26+
pip install requests beautifulsoup4 lxml
27+
28+
- name: Fetch conference pages
29+
run: python3 scripts/fetch_conference_dates.py
30+
31+
- name: Prepare proposals
32+
run: python3 scripts/prepare_conference_updates.py
33+
34+
- name: Select update candidates
35+
run: python3 scripts/select_updates.py
36+
37+
- name: Apply updates
38+
run: python3 scripts/apply_updates.py
39+
40+
- name: Create Pull Request
41+
uses: peter-evans/create-pull-request@v5
42+
with:
43+
token: ${{ secrets.GITHUB_TOKEN }}
44+
commit-message: Automated conference updates
45+
branch: auto/conference-updates
46+
title: Automated conference updates
47+
body: |
48+
This PR was created automatically by the monthly conference update workflow. It contains updated start/end dates for conferences when confident matches were found on the official websites. Please review before merging.
49+
labels: automated,conferences

research/_conferences_data.py

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,11 @@
100100
"location": "Las Vegas, NV",
101101
"lat": 36.1699,
102102
"lon": -115.1398,
103-
"start_date": "2027-01-27",
104-
"end_date": "2027-01-29",
103+
"start_date": "2026-01-28",
104+
"end_date": "2026-01-30",
105105
"abstract_open": "",
106106
"abstract_close": "",
107-
"abstract_note": "2027 dates TBD; check IMARI website",
107+
"abstract_note": "Dates may change; check IMARI website for updates",
108108
"type": "Conference",
109109
"url": "https://imari.org",
110110
"projects": [
@@ -180,7 +180,7 @@
180180
"lat": 39.9526,
181181
"lon": -75.1652,
182182
"start_date": "2027-02-20",
183-
"end_date": "2027-02-24",
183+
"end_date": "2027-02-20",
184184
"abstract_open": "",
185185
"abstract_close": "",
186186
"abstract_note": "Abstract dates TBD; see Biophysical Society website for updates",
@@ -199,8 +199,8 @@
199199
"location": "San Diego, CA",
200200
"lat": 32.7157,
201201
"lon": -117.1611,
202-
"start_date": "2027-03-21",
203-
"end_date": "2027-03-24",
202+
"start_date": "2026-03-24",
203+
"end_date": "2026-03-24",
204204
"abstract_open": "",
205205
"abstract_close": "",
206206
"abstract_note": "Abstract dates TBD; check CROI website",
@@ -359,8 +359,8 @@
359359
"location": "Philadelphia, PA",
360360
"lat": 39.9526,
361361
"lon": -75.1652,
362-
"start_date": "2026-05-17",
363-
"end_date": "2026-05-20",
362+
"start_date": "2026-09-22",
363+
"end_date": "2026-09-23",
364364
"abstract_open": "2025-10-31",
365365
"abstract_close": "2026-01-09",
366366
"abstract_note": "",
@@ -379,8 +379,8 @@
379379
"location": "Washington, DC",
380380
"lat": 38.9072,
381381
"lon": -77.0369,
382-
"start_date": "2026-06-11",
383-
"end_date": "2026-06-15",
382+
"start_date": "2027-06-22",
383+
"end_date": "2027-06-25",
384384
"abstract_open": "2025-10-15",
385385
"abstract_close": "2026-01-21",
386386
"abstract_note": "Travel awards: Oct 15 - Dec 2, 2025; Abstracts: Dec 2, 2025 - Jan 21, 2026",
@@ -441,7 +441,7 @@
441441
"lat": 32.9342,
442442
"lon": -97.0781,
443443
"start_date": "2026-07-18",
444-
"end_date": "2026-07-21",
444+
"end_date": "2026-07-18",
445445
"abstract_open": "",
446446
"abstract_close": "",
447447
"abstract_note": "",
@@ -551,7 +551,7 @@
551551
"location": "Houston, TX",
552552
"lat": 29.7174,
553553
"lon": -95.4018,
554-
"start_date": "2026-09-14",
554+
"start_date": "2026-09-15",
555555
"end_date": "2026-09-17",
556556
"abstract_open": "",
557557
"abstract_close": "",
@@ -588,8 +588,8 @@
588588
"location": "Lisbon, Portugal",
589589
"lat": 38.7223,
590590
"lon": -9.1393,
591-
"start_date": "2026-09-22",
592-
"end_date": "2026-09-24",
591+
"start_date": "2026-04-26",
592+
"end_date": "2026-04-26",
593593
"abstract_open": "2026-03-10",
594594
"abstract_close": "2026-03-31",
595595
"abstract_note": "",
@@ -679,7 +679,6 @@ def _format_conference_date(date_str, conference_name, field_name):
679679
f"Invalid {field_name} date {date_str!r} for conference {conference_name!r} in research/_conferences_data.py"
680680
) from exc
681681

682-
683682
def format_conference_abstract_deadline(conference):
684683
if conference.get("abstract_open") and conference.get("abstract_close"):
685684
open_date = _format_conference_date(conference["abstract_open"], conference["name"], "abstract_open")
@@ -697,6 +696,59 @@ def format_conference_abstract_deadline(conference):
697696
"name": conf["name"],
698697
"abstract_display": format_conference_abstract_deadline(conf),
699698
}
700-
for conf in conferences
699+
for conf in sorted(conferences, key=lambda c: c.get("start_date") or "")
701700
if conf.get("url")
702701
}
702+
703+
704+
def _validate_conferences(conf_list):
705+
"""Run light validation on conference entries and raise helpful errors.
706+
707+
Checks performed:
708+
- `start_date` and `end_date` parse as YYYY-MM-DD when present
709+
- `start_date` is not after `end_date` when both present
710+
- `lat` and `lon` are numeric when present
711+
- `url` is present and non-empty
712+
"""
713+
for conf in conf_list:
714+
name = conf.get("name", "(unknown)")
715+
716+
# Dates
717+
sd = conf.get("start_date")
718+
ed = conf.get("end_date")
719+
try:
720+
if sd:
721+
sd_dt = datetime.strptime(sd, "%Y-%m-%d")
722+
else:
723+
sd_dt = None
724+
if ed:
725+
ed_dt = datetime.strptime(ed, "%Y-%m-%d")
726+
else:
727+
ed_dt = None
728+
except ValueError as exc:
729+
raise ValueError(f"Invalid date for conference {name!r}: {exc}") from exc
730+
731+
if sd_dt and ed_dt and sd_dt > ed_dt:
732+
raise ValueError(
733+
f"Conference {name!r} has start_date {sd!r} after end_date {ed!r}"
734+
)
735+
736+
# Coordinates
737+
lat = conf.get("lat")
738+
lon = conf.get("lon")
739+
try:
740+
if lat is not None:
741+
float(lat)
742+
if lon is not None:
743+
float(lon)
744+
except (TypeError, ValueError) as exc:
745+
raise ValueError(f"Invalid coordinates for conference {name!r}: {exc}") from exc
746+
747+
# URL
748+
url = conf.get("url")
749+
if not url:
750+
raise ValueError(f"Conference {name!r} is missing a URL field")
751+
752+
753+
# Run validation at import time so problems are visible early
754+
_validate_conferences(conferences)

scripts/apply_updates.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env python3
2+
"""Apply selected conference date updates to research/_conferences_data.py
3+
4+
Expects /tmp/candidates.json to contain a mapping of conference name -> {current, proposed}.
5+
The script updates `start_date` and `end_date` fields in-place and prints a JSON summary.
6+
"""
7+
import json
8+
import re
9+
from pathlib import Path
10+
import sys
11+
12+
CANDIDATES = Path('/tmp/candidates.json')
13+
TARGET = Path(__file__).resolve().parents[1] / 'research' / '_conferences_data.py'
14+
15+
if not CANDIDATES.exists():
16+
print(json.dumps({'error': 'candidates file not found'}))
17+
sys.exit(0)
18+
19+
data = json.load(open(CANDIDATES))
20+
if not data:
21+
print(json.dumps({'updated': []}))
22+
sys.exit(0)
23+
24+
text = TARGET.read_text(encoding='utf8')
25+
orig_text = text
26+
updates = []
27+
28+
for name, info in data.items():
29+
proposed = info['proposed']
30+
sd_new, ed_new = proposed
31+
# find the name occurrence
32+
name_token = f'"name": "{name.replace("\"", "\\\"")}"'
33+
pos = text.find(name_token)
34+
if pos == -1:
35+
# try a more relaxed search (escaped characters)
36+
pos = text.find(f'"name": "{name.split("(")[0].strip()}')
37+
if pos == -1:
38+
continue
39+
40+
# find opening brace of this dict
41+
start_brace = text.rfind('{', 0, pos)
42+
# find the end of the dict (first occurrence of '},' after pos)
43+
end_brace = text.find('},', pos)
44+
if end_brace == -1:
45+
end_brace = text.find('\n}\n', pos)
46+
if end_brace == -1:
47+
continue
48+
49+
block = text[start_brace:end_brace+1]
50+
51+
# replace or insert start_date
52+
sd_match = re.search(r'("start_date"\s*:\s*")([0-9\-]*)("\s*,?)', block)
53+
if sd_match:
54+
block = block[:sd_match.start(2)] + sd_new + block[sd_match.end(2):]
55+
else:
56+
# insert after the "name" line
57+
name_line_end = block.find('\n', block.find('"name"'))
58+
insert = '\n "start_date": "' + sd_new + '",'
59+
block = block[:name_line_end+1] + insert + block[name_line_end+1:]
60+
61+
# replace or insert end_date
62+
ed_match = re.search(r'("end_date"\s*:\s*")([0-9\-]*)("\s*,?)', block)
63+
if ed_match:
64+
block = block[:ed_match.start(2)] + ed_new + block[ed_match.end(2):]
65+
else:
66+
# try to insert after start_date line if present
67+
sd_line = block.find('"start_date"')
68+
if sd_line != -1:
69+
sd_line_end = block.find('\n', sd_line)
70+
insert2 = '\n "end_date": "' + ed_new + '",'
71+
block = block[:sd_line_end+1] + insert2 + block[sd_line_end+1:]
72+
else:
73+
# fallback: insert after name line
74+
name_line_end = block.find('\n', block.find('"name"'))
75+
insert2 = '\n "end_date": "' + ed_new + '",'
76+
block = block[:name_line_end+1] + insert2 + block[name_line_end+1:]
77+
78+
# apply the block replacement
79+
text = text[:start_brace] + block + text[end_brace+1:]
80+
updates.append({'name': name, 'proposed': proposed})
81+
82+
if text != orig_text:
83+
TARGET.write_text(text, encoding='utf8')
84+
85+
print(json.dumps({'updated': updates}, indent=2))

scripts/fetch_conference_dates.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#!/usr/bin/env python3
2+
"""Fetch each conference URL and try to extract date and abstract deadline hints.
3+
Produces a JSON-like report printed to stdout.
4+
"""
5+
import re
6+
import json
7+
import sys
8+
from urllib.parse import urlparse
9+
10+
# Import conferences from repo
11+
import importlib.util
12+
from pathlib import Path
13+
14+
# Load conferences from the repository file directly
15+
conf_path = Path(__file__).resolve().parents[1] / "research" / "_conferences_data.py"
16+
spec = importlib.util.spec_from_file_location("_conferences_data", str(conf_path))
17+
mod = importlib.util.module_from_spec(spec)
18+
spec.loader.exec_module(mod)
19+
conferences = getattr(mod, "conferences")
20+
21+
import requests
22+
from bs4 import BeautifulSoup
23+
24+
YEARS = ["2024","2025","2026","2027","2028"]
25+
MONTH_RE = r"(?:January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|Jun|Jul|Aug|Sep|Oct|Nov|Dec)"
26+
DATE_PATTERNS = [
27+
re.compile(rf"({MONTH_RE}\s+\d{{1,2}}(?:[–\-]\d{{1,2}})?\,?\s*(?:{"|".join(YEARS)}))", re.I),
28+
re.compile(rf"(\d{{4}}[\-–]\d{{4}})", re.I),
29+
re.compile(rf"(20\d{{2}})"),
30+
]
31+
ABSTRACT_PAT = re.compile(r"abstract|abstracts|call for abstracts|submission deadline|abstract deadline|call for papers", re.I)
32+
33+
session_results = []
34+
35+
for conf in conferences:
36+
url = conf.get("url")
37+
name = conf.get("name")
38+
entry = {"name": name, "url": url, "found_dates": [], "found_abstracts": []}
39+
if not url:
40+
session_results.append(entry)
41+
continue
42+
try:
43+
r = requests.get(url, timeout=15, headers={"User-Agent": "mcphersonlab-bot/1.0"})
44+
text = r.text
45+
except Exception as e:
46+
entry["error"] = str(e)
47+
session_results.append(entry)
48+
continue
49+
soup = BeautifulSoup(text, "lxml")
50+
visible = soup.get_text(separator="\n")
51+
# Search for date patterns
52+
for pat in DATE_PATTERNS:
53+
for m in pat.finditer(visible):
54+
span = visible[max(0, m.start()-80):m.end()+80].strip()
55+
entry["found_dates"].append(span)
56+
if len(entry["found_dates"])>8:
57+
break
58+
if len(entry["found_dates"])>0:
59+
break
60+
# Search for abstract-related snippets
61+
for m in ABSTRACT_PAT.finditer(visible):
62+
span = visible[max(0, m.start()-120):m.end()+120].strip()
63+
entry["found_abstracts"].append(span)
64+
if len(entry["found_abstracts"])>6:
65+
break
66+
session_results.append(entry)
67+
68+
print(json.dumps(session_results, indent=2))

0 commit comments

Comments
 (0)