From 28799ab9f99646137efd760b1ae0ee6e02b1d921 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 23 Jul 2026 16:18:04 +0300 Subject: [PATCH 1/2] Remove duplicate PyBay 2022 --- 2022.csv | 1 - 1 file changed, 1 deletion(-) diff --git a/2022.csv b/2022.csv index ee6dd84..971b5e6 100644 --- a/2022.csv +++ b/2022.csv @@ -11,7 +11,6 @@ PyCon Russia,2022-07-30,2022-07-31,"Moscow, Central Federal District, Russia",RU EuroSciPy,2022-08-29,2022-09-02,"Basel, Basel-Stadt, Switzerland",CHE,University of Basel,2022-05-30,2022-05-30,https://www.euroscipy.org/2022,https://pretalx.com/euroscipy-2022/cfp, PyCon Slovakia,2022-09-09,2022-09-11,"Bratislava, Bratislava, Slovakia",SVK,Faculty of Informatics and Information Technologies STU,,,https://2022.pycon.sk,, PyBay,2022-09-10,2022-09-10,"San Francisco, California, United States of America",USA,Parklab Gardens,,,https://pybay.com,,https://pybay.com/sponsors-prospectus -PyBay,2022-09-10,2022-09-10,"San Francisco, California, USA",USA,Parklab Gardens,,,,, PyCon Portugal,2022-09-24,2022-09-24,"Porto, Oporto, Portugal",PRT,University of Porto,2022-06-30,2022-06-03,https://2022.pycon.pt,https://2022.pycon.pt/talks/cfp,https://2022.pycon.pt/sponsors/sponsorship SciPy Latin America,2022-09-26,2022-09-28,"Salta, Salta, Argentina",ARG,Universidad Nacional de Salta,,,https://pythoncientifico.ar,https://pythoncientifico.ar/pages/llamado-a-propuestas,https://pythoncientifico.ar/sponsors PyCon Ghana,2022-10-13,2022-10-15,"Accra, Greater Accra, Ghana",GHA,,,,https://gh.pycon.org,https://gh.pycon.org/talks/speaking,https://gh.pycon.org/support-us From b83c3ef4005f917ad3685d7b135a1d1275723617 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 23 Jul 2026 16:36:19 +0300 Subject: [PATCH 2/2] Generate an iCalendar ics of all conferences --- .github/workflows/ics.yaml | 49 ++++++++++++++++++++ .gitignore | 1 + README.md | 7 +++ scripts/generate_ics.py | 91 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 148 insertions(+) create mode 100644 .github/workflows/ics.yaml create mode 100644 scripts/generate_ics.py diff --git a/.github/workflows/ics.yaml b/.github/workflows/ics.yaml new file mode 100644 index 0000000..6dd4524 --- /dev/null +++ b/.github/workflows/ics.yaml @@ -0,0 +1,49 @@ +name: Publish ICS + +on: + push: + pull_request: + branches: [main] + workflow_dispatch: + +permissions: {} + +env: + FORCE_COLOR: 1 + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: astral-sh/setup-uv@v9.0.0 + - run: uv run scripts/generate_ics.py site/conferences.ics + - run: | + cat > site/index.html << 'EOF' + + + + Python conferences calendar +

Python conferences calendar

+

Subscribe to conferences.ics + in your calendar app to get all conferences listed in + + python-organizers/conferences.

+ EOF + - uses: actions/upload-pages-artifact@v5 + with: + path: site + + deploy: + if: github.ref == 'refs/heads/main' + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v5 diff --git a/.gitignore b/.gitignore index ef238c1..6b59c96 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ dist/ .~lock.* # data conferences.csv +conferences.ics diff --git a/README.md b/README.md index 9b8a554..11fae43 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,13 @@ Each CSV file is [compatible with Google Calendar](https://support.google.com/ca | Proposal URL | Proposal information | URL | | Sponsorship URL | Sponsorship information | URL | +## Calendar Feed + +Subscribe to +in your calendar app to get all listed conferences as all-day events, with +proposal deadlines and links in the event description. The feed is regenerated +from the CSV files on every push to `main`. + ## Tests & Linting There are tests to ensure that the files are written in the correct format diff --git a/scripts/generate_ics.py b/scripts/generate_ics.py new file mode 100644 index 0000000..e35072d --- /dev/null +++ b/scripts/generate_ics.py @@ -0,0 +1,91 @@ +"""Generate an iCalendar file from the year CSV files. + +Usage: uv run scripts/generate_ics.py [output.ics] +""" + +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "icalendar==7.2.2", +# ] +# /// + +import csv +import datetime as dt +import sys +from pathlib import Path + +from icalendar import Calendar, Event + +CALENDAR_NAME = "Python Conferences" +UID_DOMAIN = "python-organizers.github.io" + + +def make_event(row: dict[str, str], dtstamp: dt.datetime, seen_uids: set[str]) -> Event: + event = Event() + + # Stable UIDs so subscribed calendars update events instead of + # duplicating them + slug = "".join(c if c.isalnum() else "-" for c in row["Subject"].lower()) + uid = f"{row['Start Date']}-{slug}@{UID_DOMAIN}" + while uid in seen_uids: + uid = "x" + uid + seen_uids.add(uid) + event.add("uid", uid) + + event.add("dtstamp", dtstamp) + event.add("dtstart", dt.date.fromisoformat(row["Start Date"])) + # DTEND is exclusive, so all-day events end the day after the last day + event.add("dtend", dt.date.fromisoformat(row["End Date"]) + dt.timedelta(days=1)) + event.add("summary", row["Subject"]) + + if location := ", ".join(filter(None, [row["Venue"], row["Location"]])): + event.add("location", location) + + description = "\n".join( + f"{label}: {value}" + for label, value in ( + ("Tutorial deadline", row["Tutorial Deadline"]), + ("Talk deadline", row["Talk Deadline"]), + ("Website", row["Website URL"]), + ("Proposals", row["Proposal URL"]), + ("Sponsorship", row["Sponsorship URL"]), + ) + if value + ) + if description: + event.add("description", description) + if row["Website URL"]: + event.add("url", row["Website URL"]) + + return event + + +def main() -> None: + output = Path(sys.argv[1] if len(sys.argv) > 1 else "conferences.ics") + repository_folder = Path(__file__).parents[1] + dtstamp = dt.datetime.now(dt.timezone.utc) + + calendar = Calendar() + calendar.add("prodid", "-//python-organizers//conferences//EN") + calendar.add("version", "2.0") + calendar.add("calscale", "GREGORIAN") + calendar.add("method", "PUBLISH") + calendar.add("x-wr-calname", CALENDAR_NAME) + calendar.add("name", CALENDAR_NAME) + + count = 0 + seen_uids = set() + for csv_file in sorted(repository_folder.glob("20*.csv")): + with open(csv_file, newline="") as f: + for row in csv.DictReader(f): + calendar.add_component(make_event(row, dtstamp, seen_uids)) + count += 1 + + output.parent.mkdir(parents=True, exist_ok=True) + output.write_bytes(calendar.to_ical()) + print(f"Wrote {count} events to {output}") + + +if __name__ == "__main__": + main()