Compare commits

..

39 Commits

Author SHA1 Message Date
rufei a5e622b950 improve delete log line
Periodic refresh of events / Run-Scraper (push) Successful in 1m22s
2025-04-16 00:27:30 -07:00
rufei 9cd7505517 adjust stdout
Periodic refresh of events / Run-Scraper (push) Successful in 1m6s
2025-04-10 23:54:44 -07:00
rufei c0ed56d9e0 new description in ical 2025-04-10 23:51:00 -07:00
rufei 75e0fd8a47 comments
Periodic refresh of events / Run-Scraper (push) Successful in 1m49s
2025-04-10 23:11:02 -07:00
rufei f41c0b5806 add icaluid to description 2025-04-10 23:09:42 -07:00
rufei da8a7afb56 update str
Periodic refresh of events / Run-Scraper (push) Successful in 46s
2025-04-10 15:50:42 -07:00
rufei 647c20955c mkdir to fix tee 2025-04-10 15:48:45 -07:00
rufei 2faabee6c4 fix naming 2025-04-10 15:47:15 -07:00
rufei efbd8f5122 add stdout to release notes 2025-04-10 15:41:11 -07:00
rufei 4f9c4716ef specify active event count
Periodic refresh of events / Run-Scraper (push) Successful in 1m48s
2025-04-10 15:19:03 -07:00
rufei a5ea31fa6f yay 2025-04-10 15:16:28 -07:00
rufei 8a9fa7a954 summary outcome output 2025-04-10 15:14:32 -07:00
rufei 2573093694 use different ical prefix, better print diffs
Periodic refresh of events / Run-Scraper (push) Waiting to run
2025-04-10 14:30:21 -07:00
rufei 0de41e3522 fix indent
Periodic refresh of events / Run-Scraper (push) Successful in 12m6s
2025-04-10 05:20:08 -07:00
rufei c897eb8265 add secret to pipeline 2025-04-10 05:18:29 -07:00
rufei 1821d86a1e put some temp sleeps in there 2025-04-10 05:15:34 -07:00
rufei 1dfaac75d2 there's a lot to unpack here 2025-04-10 05:09:57 -07:00
rufei 1985a196b7 update run instructions
Periodic refresh of events / Run-Scraper (push) Successful in 31s
2025-04-07 19:32:25 -07:00
rufei f9f5e7eb0d docs
Periodic refresh of events / Run-Scraper (push) Successful in 33s
2025-04-07 19:22:36 -07:00
rufei bbf6169456 docs 2025-04-07 19:20:27 -07:00
rufei 5c992429f4 add gcal links 2025-04-07 19:17:57 -07:00
rufei 3fbfe72959 update readme
Periodic refresh of events / Run-Scraper (push) Successful in 33s
2025-04-07 02:47:30 -07:00
rufei 0f207b2ac9 fix hash generation
Periodic refresh of events / Run-Scraper (push) Successful in 34s
2025-04-07 02:18:37 -07:00
rufei 7464659d78 unify casing 2025-04-07 02:17:43 -07:00
rufei fb48441e73 dont publish on output cache hit 2025-04-07 02:16:51 -07:00
rufei ec434a4121 generate tag from time 2025-04-07 02:03:34 -07:00
rufei 55a204c32d use sha
Periodic refresh of events / Run-Scraper (push) Successful in 34s
2025-04-07 01:57:27 -07:00
rufei 75c2f42e27 revert to output 2025-04-07 01:53:50 -07:00
rufei 01954de5d1 it broke? 2025-04-07 01:50:50 -07:00
rufei d2effa04a7 release md5sum 2025-04-07 01:46:59 -07:00
rufei fbe120ea3c use zip file 2025-04-07 01:35:00 -07:00
rufei f1e9a8147a indent 2025-04-07 01:32:41 -07:00
rufei d846816971 just zip the output 2025-04-07 01:32:09 -07:00
rufei d4b09e4c90 log list
Periodic refresh of events / Run-Scraper (push) Successful in 32s
2025-04-07 01:25:33 -07:00
rufei 4cfb3e3be2 update artifact path 2025-04-07 01:23:26 -07:00
rufei a97d453290 fix tz 2025-04-07 01:19:13 -07:00
rufei a2ed827f1e whoops 2025-04-07 01:11:13 -07:00
rufei 51b84cb2af use time as release 2025-04-07 01:10:30 -07:00
rufei 9f77251b5b download artifact before releasing
Periodic refresh of events / Run-Scraper (push) Successful in 35s
2025-04-07 00:54:18 -07:00
7 changed files with 732 additions and 20 deletions
+32 -4
View File
@@ -45,25 +45,53 @@ jobs:
- name: Install dependencies
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
run: poetry install --no-interaction --no-root
- name: Get current time
id: time
run: echo "::set-output name=time::$(TZ='America/Los_Angeles' date +'%Y-%m-%d %I:%M %p')"
- name: Get time tag
id: time_tag
run: echo "::set-output name=tag::$(TZ='America/Los_Angeles' date +'%Y-%m-%d_%I%M_%p')"
#----------------------------------------------
# install and run the project
#----------------------------------------------
- name: Install package
run: poetry install --no-interaction
- name: Generate New Calendar Files
run: poetry run generate-ics
- name: Generate new calendar Files
run: mkdir output && poetry run generate-ics | tee output/run_logs.txt
env:
GOOGLE_SERVICE_ACCOUNT_CREDS: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_CREDS }}
#----------------------------------------------
# load cached venv if cache exists
#----------------------------------------------
- name: Check if calendars have changed
id: cached-calendars
uses: actions/cache@v4
with:
path: output
key: calendar-files-${{ hashFiles('output/**/*.ics') }}
#----------------------------------------------
# publish artifacts and release
#----------------------------------------------
- name: Archive production artifacts
if: steps.cached-calendars.outputs.cache-hit != 'true'
uses: christopherhx/gitea-upload-artifact@v4
with:
name: calendars
path: |
output/
- name: Zip up artifacts
if: steps.cached-calendars.outputs.cache-hit != 'true'
uses: thedoctor0/zip-release@0.7.5
with:
type: 'zip'
filename: 'calendars.zip'
path: output/
- name: Create new release
if: steps.cached-calendars.outputs.cache-hit != 'true'
uses: akkuman/gitea-release-action@v1
with:
name: ${{ steps.time.outputs.time }}
tag_name: ${{ steps.time_tag.outputs.tag }}
body_path: output/run_logs.txt
files: |-
output/**
api_key: '${{secrets.RELEASE_TOKEN}}'
calendars.zip
+31 -3
View File
@@ -1,10 +1,38 @@
# Events Plus
This script turns the Sakura-Con schedule from Eventny into iCalendar files for import into your calendar program of choice. Events are broken out into separate files, by track.
It is vibe-coded. Anyway, here's how to run it:
Right now, this is a script that turns the Sakura-Con schedule from Eventny into iCalendar files (for import into your calendar program of choice). Events are broken out into separate files, by track.
It may eventually grow to support other events and/or sport a separate UI. See [TODO].
Events are scraped via a pipeline that runs twice hourly, and if there are any changes, [a new release containing iCal files is published](https://g.dracoli.ch/rufei/events-plus/releases).
## Google Calendar Links
For your convenience, these have been imported to publicly available google calendars. You can (and should, for the best UX) import them [into your account here](https://calendar.google.com/calendar/u/0/r/settings/addbyurl).
* Sakura-Con 2025
* [Sakura-Con Presents](https://calendar.google.com/calendar/embed?src=f2802717d1727828bf1bfe9f6cc35844a36ffff6daad7c3fa293ab35bf51c495%40group.calendar.google.com&ctz=America%2FLos_Angeles)
* [Fan Panel](https://calendar.google.com/calendar/embed?src=8062c73edfb8db11c385b255531d97ba7a30672a5e6ded4e24cd589b25282d65%40group.calendar.google.com&ctz=America%2FLos_Angeles)
* [Gaming](https://calendar.google.com/calendar/embed?src=46c95e247f9e449f2968b44edfe659560a232efbf57576efc9f15ba8668ee0ff%40group.calendar.google.com&ctz=America%2FLos_Angeles)
* [Main Stage](https://calendar.google.com/calendar/embed?src=ae20459e6d4c41b46496663b04ec87279fb1a0ed858351ea796d82c217e20579%40group.calendar.google.com&ctz=America%2FLos_Angeles)
* [Cultural Panel](https://calendar.google.com/calendar/embed?src=633adb5a3a96b984092c8e641dda4011b80a8a314d03fc0c6e941895caabf2d2%40group.calendar.google.com&ctz=America%2FLos_Angeles)
* [Guest of Honor](https://calendar.google.com/calendar/embed?src=da7b9a6845eb7868457147046aa388101ef2601cffbaf4d29f1a1c8d00a8bea9%40group.calendar.google.com&ctz=America%2FLos_Angeles)
* [Charity Auction](https://calendar.google.com/calendar/embed?src=a51250c24fd2c4fd2d4bbefc94c055eb1f392ccf89d93799d4a92ff2c319f436%40group.calendar.google.com&ctz=America%2FLos_Angeles)
* [Industry Panel](https://calendar.google.com/calendar/embed?src=fe3a9a7e1e839dd41ac934bb0b79287d4ff705f577f726d06b0f4249bae1ac40%40group.calendar.google.com&ctz=America%2FLos_Angeles)
* [Autographs](https://calendar.google.com/calendar/embed?src=1959b9a550cbdf73d7997c070009ea836c1af93d8330353bbfdbbc2f82aff27a%40group.calendar.google.com&ctz=America%2FLos_Angeles)
* [Summit Stage](https://calendar.google.com/calendar/embed?src=106d7620bbe3a8e14ee593758e7db9c71a311615e3a17ada938f50974b0b9ccf%40group.calendar.google.com&ctz=America%2FLos_Angeles)
## How to Run
```sh
poetry install
eval $(poetry env activate)
python toical.py
poetry run generate-ics
```
## TODO
* ~~sync events to gcal by icaluid on every release via gcal api~~
* ~~compare event ids from last sync to current and highlight changes to previously selected events in release~~
* modify runner scripts to stop running on events that have already elapsed
* refactor event scrapers to use a config containing start/stop and other common metadata
* create multi-select webui
* save selected event uids in cookie
* add support for other events like lbx
+18
View File
@@ -0,0 +1,18 @@
{
"start": 1744948800,
"end": 1745207999,
"calendars": {
"Sakura-Con Presents": "f2802717d1727828bf1bfe9f6cc35844a36ffff6daad7c3fa293ab35bf51c495@group.calendar.google.com",
"Fan Panel": "8062c73edfb8db11c385b255531d97ba7a30672a5e6ded4e24cd589b25282d65@group.calendar.google.com",
"Gaming": "46c95e247f9e449f2968b44edfe659560a232efbf57576efc9f15ba8668ee0ff@group.calendar.google.com",
"Main Stage": "ae20459e6d4c41b46496663b04ec87279fb1a0ed858351ea796d82c217e20579@group.calendar.google.com",
"Cultural Panel": "633adb5a3a96b984092c8e641dda4011b80a8a314d03fc0c6e941895caabf2d2@group.calendar.google.com",
"Guest of Honor": "da7b9a6845eb7868457147046aa388101ef2601cffbaf4d29f1a1c8d00a8bea9@group.calendar.google.com",
"Charity Auction": "a51250c24fd2c4fd2d4bbefc94c055eb1f392ccf89d93799d4a92ff2c319f436@group.calendar.google.com",
"Industry Panel": "fe3a9a7e1e839dd41ac934bb0b79287d4ff705f577f726d06b0f4249bae1ac40@group.calendar.google.com",
"Autographs": "1959b9a550cbdf73d7997c070009ea836c1af93d8330353bbfdbbc2f82aff27a@group.calendar.google.com",
"Summit Stage": "106d7620bbe3a8e14ee593758e7db9c71a311615e3a17ada938f50974b0b9ccf@group.calendar.google.com"
},
"tzSuffix": "-07:00",
"uidPrefix": "sakuracon2025-"
}
Generated
+392 -1
View File
@@ -164,6 +164,18 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi
tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""]
[[package]]
name = "cachetools"
version = "5.5.2"
description = "Extensible memoizing collections and decorators"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"},
{file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"},
]
[[package]]
name = "certifi"
version = "2025.1.31"
@@ -396,6 +408,146 @@ files = [
{file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"},
]
[[package]]
name = "google-api-core"
version = "2.24.2"
description = "Google API client core library"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "google_api_core-2.24.2-py3-none-any.whl", hash = "sha256:810a63ac95f3c441b7c0e43d344e372887f62ce9071ba972eacf32672e072de9"},
{file = "google_api_core-2.24.2.tar.gz", hash = "sha256:81718493daf06d96d6bc76a91c23874dbf2fac0adbbf542831b805ee6e974696"},
]
[package.dependencies]
google-auth = ">=2.14.1,<3.0.0"
googleapis-common-protos = ">=1.56.2,<2.0.0"
proto-plus = [
{version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
{version = ">=1.22.3,<2.0.0", markers = "python_version < \"3.13\""},
]
protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0"
requests = ">=2.18.0,<3.0.0"
[package.extras]
async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.dev0)"]
grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\""]
grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"]
grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"]
[[package]]
name = "google-api-python-client"
version = "2.166.0"
description = "Google API Client Library for Python"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "google_api_python_client-2.166.0-py2.py3-none-any.whl", hash = "sha256:dd8cc74d9fc18538ab05cbd2e93cb4f82382f910c5f6945db06c91f1deae6e45"},
{file = "google_api_python_client-2.166.0.tar.gz", hash = "sha256:b8cf843bd9d736c134aef76cf1dc7a47c9283a2ef24267b97207b9dd43b30ef7"},
]
[package.dependencies]
google-api-core = ">=1.31.5,<2.0.dev0 || >2.3.0,<3.0.0"
google-auth = ">=1.32.0,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0"
google-auth-httplib2 = ">=0.2.0,<1.0.0"
httplib2 = ">=0.19.0,<1.0.0"
uritemplate = ">=3.0.1,<5"
[[package]]
name = "google-auth"
version = "2.38.0"
description = "Google Authentication Library"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "google_auth-2.38.0-py2.py3-none-any.whl", hash = "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a"},
{file = "google_auth-2.38.0.tar.gz", hash = "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4"},
]
[package.dependencies]
cachetools = ">=2.0.0,<6.0"
pyasn1-modules = ">=0.2.1"
rsa = ">=3.1.4,<5"
[package.extras]
aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"]
enterprise-cert = ["cryptography", "pyopenssl"]
pyjwt = ["cryptography (>=38.0.3)", "pyjwt (>=2.0)"]
pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"]
reauth = ["pyu2f (>=0.1.5)"]
requests = ["requests (>=2.20.0,<3.0.0.dev0)"]
[[package]]
name = "google-auth-httplib2"
version = "0.2.0"
description = "Google Authentication Library: httplib2 transport"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05"},
{file = "google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d"},
]
[package.dependencies]
google-auth = "*"
httplib2 = ">=0.19.0"
[[package]]
name = "google-auth-oauthlib"
version = "1.2.1"
description = "Google Authentication Library"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "google_auth_oauthlib-1.2.1-py2.py3-none-any.whl", hash = "sha256:2d58a27262d55aa1b87678c3ba7142a080098cbc2024f903c62355deb235d91f"},
{file = "google_auth_oauthlib-1.2.1.tar.gz", hash = "sha256:afd0cad092a2eaa53cd8e8298557d6de1034c6cb4a740500b5357b648af97263"},
]
[package.dependencies]
google-auth = ">=2.15.0"
requests-oauthlib = ">=0.7.0"
[package.extras]
tool = ["click (>=6.0.0)"]
[[package]]
name = "googleapis-common-protos"
version = "1.69.2"
description = "Common protobufs used in Google APIs"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "googleapis_common_protos-1.69.2-py3-none-any.whl", hash = "sha256:0b30452ff9c7a27d80bfc5718954063e8ab53dd3697093d3bc99581f5fd24212"},
{file = "googleapis_common_protos-1.69.2.tar.gz", hash = "sha256:3e1b904a27a33c821b4b749fd31d334c0c9c30e6113023d495e48979a3dc9c5f"},
]
[package.dependencies]
protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0"
[package.extras]
grpc = ["grpcio (>=1.44.0,<2.0.0)"]
[[package]]
name = "httplib2"
version = "0.22.0"
description = "A comprehensive HTTP client library."
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
groups = ["main"]
files = [
{file = "httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc"},
{file = "httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81"},
]
[package.dependencies]
pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0.2,<3.0.3 || >3.0.3,<4", markers = "python_version > \"3.0\""}
[[package]]
name = "icalendar"
version = "6.1.3"
@@ -430,6 +582,24 @@ files = [
[package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "jsondiff"
version = "2.2.1"
description = "Diff JSON and JSON-like structures in Python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "jsondiff-2.2.1-py3-none-any.whl", hash = "sha256:b1f0f7e2421881848b1d556d541ac01a91680cfcc14f51a9b62cdf4da0e56722"},
{file = "jsondiff-2.2.1.tar.gz", hash = "sha256:658d162c8a86ba86de26303cd86a7b37e1b2c1ec98b569a60e2ca6180545f7fe"},
]
[package.dependencies]
pyyaml = "*"
[package.extras]
dev = ["build", "hypothesis", "pytest", "setuptools-scm"]
[[package]]
name = "multidict"
version = "6.3.2"
@@ -535,6 +705,23 @@ files = [
[package.dependencies]
typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""}
[[package]]
name = "oauthlib"
version = "3.2.2"
description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"},
{file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"},
]
[package.extras]
rsa = ["cryptography (>=3.0.0)"]
signals = ["blinker (>=1.4.0)"]
signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
[[package]]
name = "propcache"
version = "0.3.1"
@@ -643,6 +830,85 @@ files = [
{file = "propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf"},
]
[[package]]
name = "proto-plus"
version = "1.26.1"
description = "Beautiful, Pythonic protocol buffers"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66"},
{file = "proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012"},
]
[package.dependencies]
protobuf = ">=3.19.0,<7.0.0"
[package.extras]
testing = ["google-api-core (>=1.31.5)"]
[[package]]
name = "protobuf"
version = "6.30.2"
description = ""
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "protobuf-6.30.2-cp310-abi3-win32.whl", hash = "sha256:b12ef7df7b9329886e66404bef5e9ce6a26b54069d7f7436a0853ccdeb91c103"},
{file = "protobuf-6.30.2-cp310-abi3-win_amd64.whl", hash = "sha256:7653c99774f73fe6b9301b87da52af0e69783a2e371e8b599b3e9cb4da4b12b9"},
{file = "protobuf-6.30.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:0eb523c550a66a09a0c20f86dd554afbf4d32b02af34ae53d93268c1f73bc65b"},
{file = "protobuf-6.30.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:50f32cc9fd9cb09c783ebc275611b4f19dfdfb68d1ee55d2f0c7fa040df96815"},
{file = "protobuf-6.30.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4f6c687ae8efae6cf6093389a596548214467778146b7245e886f35e1485315d"},
{file = "protobuf-6.30.2-cp39-cp39-win32.whl", hash = "sha256:524afedc03b31b15586ca7f64d877a98b184f007180ce25183d1a5cb230ee72b"},
{file = "protobuf-6.30.2-cp39-cp39-win_amd64.whl", hash = "sha256:acec579c39c88bd8fbbacab1b8052c793efe83a0a5bd99db4a31423a25c0a0e2"},
{file = "protobuf-6.30.2-py3-none-any.whl", hash = "sha256:ae86b030e69a98e08c77beab574cbcb9fff6d031d57209f574a5aea1445f4b51"},
{file = "protobuf-6.30.2.tar.gz", hash = "sha256:35c859ae076d8c56054c25b59e5e59638d86545ed6e2b6efac6be0b6ea3ba048"},
]
[[package]]
name = "pyasn1"
version = "0.6.1"
description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"},
{file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"},
]
[[package]]
name = "pyasn1-modules"
version = "0.4.2"
description = "A collection of ASN.1-based protocols modules"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"},
{file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"},
]
[package.dependencies]
pyasn1 = ">=0.6.1,<0.7.0"
[[package]]
name = "pyparsing"
version = "3.2.3"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf"},
{file = "pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be"},
]
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
@@ -670,6 +936,69 @@ files = [
{file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"},
]
[[package]]
name = "pyyaml"
version = "6.0.2"
description = "YAML parser and emitter for Python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"},
{file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"},
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"},
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"},
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"},
{file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"},
{file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"},
{file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"},
{file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"},
{file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"},
{file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"},
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"},
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"},
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"},
{file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"},
{file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"},
{file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"},
{file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"},
{file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"},
{file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"},
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"},
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"},
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"},
{file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"},
{file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"},
{file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"},
{file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"},
{file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"},
{file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"},
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"},
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"},
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"},
{file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"},
{file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"},
{file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"},
{file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"},
{file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"},
{file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"},
{file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"},
{file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"},
{file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"},
{file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"},
{file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"},
{file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"},
{file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"},
{file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"},
{file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"},
{file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"},
{file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"},
{file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"},
{file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"},
{file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"},
{file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"},
]
[[package]]
name = "requests"
version = "2.32.3"
@@ -692,6 +1021,56 @@ urllib3 = ">=1.21.1,<3"
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "requests-oauthlib"
version = "2.0.0"
description = "OAuthlib authentication support for Requests."
optional = false
python-versions = ">=3.4"
groups = ["main"]
files = [
{file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"},
{file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"},
]
[package.dependencies]
oauthlib = ">=3.0.0"
requests = ">=2.0.0"
[package.extras]
rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
[[package]]
name = "rsa"
version = "4.2"
description = "Pure-Python RSA implementation"
optional = false
python-versions = "*"
groups = ["main"]
markers = "python_version >= \"3.13\""
files = [
{file = "rsa-4.2.tar.gz", hash = "sha256:aaefa4b84752e3e99bd8333a2e1e3e7a7da64614042bd66f775573424370108a"},
]
[package.dependencies]
pyasn1 = ">=0.1.3"
[[package]]
name = "rsa"
version = "4.9"
description = "Pure-Python RSA implementation"
optional = false
python-versions = ">=3.6,<4"
groups = ["main"]
markers = "python_version < \"3.13\""
files = [
{file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"},
{file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"},
]
[package.dependencies]
pyasn1 = ">=0.1.3"
[[package]]
name = "setuptools"
version = "75.3.2"
@@ -750,6 +1129,18 @@ files = [
{file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"},
]
[[package]]
name = "uritemplate"
version = "4.1.1"
description = "Implementation of RFC 6570 URI Templates"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"},
{file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"},
]
[[package]]
name = "urllib3"
version = "2.2.3"
@@ -928,4 +1319,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.9"
content-hash = "b5f9acef18a15409718a71521e2a39a7cae20793f55425d28e46f6b37ad3b3cd"
content-hash = "06976e001b4080474473ef0cb78ee3ab086eea319c391f759d74d72206e2bfa3"
+6 -1
View File
@@ -11,11 +11,16 @@ dependencies = [
"icalendar (>=6.1.3,<7.0.0)",
"datetime (>=5.5,<6.0)",
"requests (>=2.32.3,<3.0.0)",
"aiohttp (>=3.11.16,<4.0.0)"
"aiohttp (>=3.11.16,<4.0.0)",
"google-api-python-client (>=2.166.0,<3.0.0)",
"google-auth-httplib2 (>=0.2.0,<0.3.0)",
"google-auth-oauthlib (>=1.2.1,<2.0.0)",
"jsondiff (>=2.2.1,<3.0.0)"
]
[project.scripts]
generate-ics = 'events_plus.console:generate_all'
clear-gcals = 'events_plus.console:clear_all'
[build-system]
+5 -1
View File
@@ -1,7 +1,11 @@
from .scrapers.sakuracon import collect_sakuracon_events
from .scrapers.sakuracon import collect_sakuracon_events, clear_sakuracon_events
import asyncio
def generate_all():
loop = asyncio.new_event_loop()
loop.run_until_complete(collect_sakuracon_events())
def clear_all():
loop = asyncio.new_event_loop()
loop.run_until_complete(clear_sakuracon_events())
+248 -10
View File
@@ -1,20 +1,41 @@
import requests
from icalendar import Calendar, Event
from datetime import datetime
from datetime import datetime, timezone
from collections import defaultdict
import asyncio
import aiohttp
import json
import os
import base64
import jsondiff as jd
import pprint
from google.oauth2 import service_account
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
EVENTNY_ENDPOINT = "https://www.eventeny.com/funcs/event/event-page-elements-2022-03-06.php"
SAK_BIZ_ID = "233997"
SAK_EVENT_ID = "13462"
BATCH_LIMIT = 190
async def collect_sakuracon_events():
async def clear_sakuracon_events(config_path="config/sakuracon.json"):
with open(config_path) as f:
cfg = json.load(f)
print("using config:")
pprint.pp(cfg, indent=2)
clear_gcals(cfg)
async def collect_sakuracon_events(config_path="config/sakuracon.json"):
with open(config_path) as f:
cfg = json.load(f)
#print("using config:")
#pprint.pp(cfg, indent=2)
events, tracks = await get_event_data()
cals = convert_events_to_icals(events, tracks)
cals = convert_events_to_icals(events, tracks, cfg)
write_ics(cals)
update_gcal(events, tracks, cfg)
async def get_event_data():
@@ -48,7 +69,7 @@ async def get_event_data():
all_events.extend(data['list'])
all_tracks.update(data['track'])
all_events = await insert_descriptions(all_events)
all_events = await insert_fields(all_events)
return all_events, all_tracks
@@ -67,7 +88,7 @@ async def get_description(eventid):
results = json.loads(data.decode())
return results["schedule"]["overview"]["description"] or ""
async def insert_descriptions(events):
async def insert_fields(events):
tasks = []
for event in events:
t = asyncio.create_task(
@@ -79,11 +100,27 @@ async def insert_descriptions(events):
for desc, event in zip(descs, events):
event["raw_description"] = desc
uid_str = f"UID: {event['id']}"
tags_str = ""
if 'hashtag_title' in event and event['hashtag_title']:
tags_str = f"Tags: {','.join(event['hashtag_title'])}\n"
event["description"] = f"Track: {event['tag_title']}\n{tags_str}\n{uid_str}\n{desc}"
return events
def convert_events_to_icals(all_events, all_tracks) -> dict[str, Calendar]:
def batch_create_handler(_, resp, exception):
if exception is not None:
print(exception)
return
print(f"Event created: g {resp['id']}")
def batch_exception_handler(_, resp, exception):
if exception is not None:
pprint.pp(resp)
pprint.pp(exception)
def convert_events_to_icals(all_events, all_tracks, cfg) -> dict[str, Calendar]:
# Group events by track_title
uid_prefix = cfg["uidPrefix"]
calendars = defaultdict(Calendar)
for event in all_events:
@@ -100,6 +137,7 @@ def convert_events_to_icals(all_events, all_tracks) -> dict[str, Calendar]:
# Create event
ical_event = Event()
ical_event.add("uid", uid_prefix + event["id"]) # we are assuming the ids generated by eventny are unique
ical_event.add("summary", event['title'])
ical_event.add("dtstart", datetime.fromisoformat(event['start_calendar']))
ical_event.add("dtend", datetime.fromisoformat(event['end_calendar']))
@@ -109,9 +147,8 @@ def convert_events_to_icals(all_events, all_tracks) -> dict[str, Calendar]:
# Add hashtags as categories if available
if 'hashtag_title' in event and event['hashtag_title']:
ical_event.add('categories', event['hashtag_title'])
tags_str = f"Tags: {','.join(event['hashtag_title'])}\n"
ical_event.add("description", f"Track: {track_title}\n{tags_str}\n{event['raw_description']}")
ical_event.add("description", event['description'])
cal.add_component(ical_event)
return calendars
@@ -123,4 +160,205 @@ def write_ics(calendars, output_dir="output/sakuracon"):
os.makedirs(os.path.dirname(filename), exist_ok=True)
with open(filename, 'wb') as f:
f.write(cal.to_ical())
print(f"Wrote: {filename}")
#print(f"Wrote: {filename}")
def update_gcal(scraped_events, tracks, cfg):
SCOPES = ["https://www.googleapis.com/auth/calendar.events"]
creds_b64 = os.getenv("GOOGLE_SERVICE_ACCOUNT_CREDS")
creds_body = json.loads(base64.b64decode(creds_b64))
creds = service_account.Credentials.from_service_account_info(creds_body, scopes=SCOPES)
tz_suffix = cfg["tzSuffix"]
gcal_map = cfg["calendars"]
uid_prefix = cfg["uidPrefix"]
uids = set([uid_prefix + e["id"] for e in scraped_events])
print(f"There seem to be {len(uids)} unique events by ical uids")
try:
service = build("calendar", "v3", credentials=creds)
# list all events in the gcals
gcal_events = []
gcalevent_gcal_map = {}
uncancelled_count = 0
batch = service.new_batch_http_request()
batchlen = 0
for _, gcal in gcal_map.items():
page_token = None
while True:
resp = service.events().list(calendarId=gcal, maxResults=BATCH_LIMIT, pageToken=page_token, showDeleted=True).execute()
for e in resp["items"]:
#print(e.get("iCalUID"))
gcalevent_gcal_map[e["id"]] = gcal
if e["status"] != "cancelled":
uncancelled_count += 1
gcal_events.extend(resp["items"])
page_token = resp.get("nextPageToken")
if not page_token:
break
print(f"Retrieved {uncancelled_count} active events out of {len(gcal_events)} total gcal events")
# associate icaluid
uid_gcalevent_map = {}
for event in gcal_events:
uid = event.get("iCalUID")
if uid and uid.startswith(uid_prefix):
uid_gcalevent_map[uid] = event
# go through scraped events and note changes from gcal (correlated by icaluid), if any
events_by_track = defaultdict(list)
for e in scraped_events:
events_by_track[e["tag_title"]].append(e)
action_taken_map = {
"added": [],
"updated": [],
"deleted": [],
"unchanged": [],
"other": [],
}
for track_name, events in events_by_track.items():
batch = service.new_batch_http_request()
track_gcal_id = gcal_map[track_name]
for e in events:
ical_uid = uid_prefix + e["id"]
try:
start = datetime.fromisoformat(e["start_calendar"])
end = datetime.fromisoformat(e["end_calendar"])
# sometimes events have weird start/end times that fail gcal's validation, but don't let that kill our vibe
if start >= end:
action_taken_map["other"].append(e)
raise Exception((f"Event {e['title']} (uid: {ical_uid}) has an invalid duration, {start} -> {end}"))
e_content = {
"summary": e["title"],
"start": e["start_calendar"] + tz_suffix,
"end": e["end_calendar"] + tz_suffix,
"location": e['location'].replace('&amp;', '&'),
"description": e["description"],
"status": "confirmed"
}
gcal_body = {
"summary": e["title"],
"iCalUID": ical_uid,
"start": {
"dateTime": e["start_calendar"] + tz_suffix,
},
"end": {
"dateTime": e["end_calendar"] + tz_suffix,
},
"location": e['location'].replace('&amp;', '&'),
"description": e["description"],
"status": "confirmed"
}
if ical_uid in uid_gcalevent_map:
g_e = uid_gcalevent_map[ical_uid]
#print(g_e)
g_content = {
"summary": g_e.get("summary"),
"start": g_e["start"]["dateTime"],
"end": g_e["end"]["dateTime"],
"location": g_e.get("location"),
"description": g_e.get("description"),
"status": g_e["status"] # this will likely either be confirmed or cancelled, and because we are using icaluids, we have to fish the cancelled/trashed event out of the bin using this field
}
changed = e_content != g_content
# if changes exist, update existing event
if not changed:
action_taken_map["unchanged"].append(e)
#print(f"Event {e['title']} (uid: {ical_uid}) seems to be the same, leaving as is")
else:
print(f"Updating event {e['title']} (ical: {ical_uid}/g: {g_e['id']})")
pprint.pp(jd.diff(g_content, e_content, syntax="symmetric"), depth=3)
action_taken_map["updated"].append(e)
# print(e_content)
# print(g_content)
g_e.update(gcal_body)
batch.add(service.events().update(calendarId=track_gcal_id, eventId=g_e["id"], body=g_e), callback=batch_exception_handler)
batchlen += 1
else: # if event is new, insert
print(f"Event {e['title']} (uid: {ical_uid}) seems to be new, adding...")
action_taken_map["added"].append(e)
#service.events().insert(calendarId=track_gcal_id, body=gcal_body).execute()
batch.add(service.events().insert(calendarId=track_gcal_id, body=gcal_body), callback=batch_create_handler)
batchlen += 1
if batchlen == BATCH_LIMIT:
batch.execute()
batchlen = 0
batch = service.new_batch_http_request()
except HttpError as error:
print(f"An error occurred while processing the event: {error}")
print(gcal_body)
except Exception as error:
print(f"An error occurred while processing the event: {error}")
batch.execute()
batchlen = 0
# delete all gcal_events which are not in the events array
# we have the list of gcal events
# we have a set of event uids
# if the gcal event does not have a uid or its uid is not in the uids set, then delete
# but we need the cal of the gcal event in order to delete it
#print(gcalevent_gcal_map)
# dont bother batching the deletes b/c cal has to be same in batch, lazy...
for event in gcal_events:
if "iCalUID" not in event or event["iCalUID"] not in uids and event["status"] != "cancelled":
cal = gcalevent_gcal_map[event["id"]]
action_taken_map["unchanged"].append(event)
print(f"gcal event {event['summary']} g {event['id']} (uid: {event.get('iCalUID') or 'unknown'}) not in latest scrape, deleting...")
service.events().delete(calendarId=cal, eventId=event["id"]).execute()
except HttpError as error:
print(f"An error occurred: {error}")
print(gcal_body)
print("-------------------------")
print("Summary:")
summary_map = {}
for k,v in action_taken_map.items():
summary_map[k] = len(v)
pprint.pp(summary_map, indent=2)
def clear_gcals(cfg):
SCOPES = ["https://www.googleapis.com/auth/calendar.events"]
creds_b64 = os.getenv("GOOGLE_SERVICE_ACCOUNT_CREDS")
creds_body = json.loads(base64.b64decode(creds_b64))
creds = service_account.Credentials.from_service_account_info(creds_body, scopes=SCOPES)
gcal_map = cfg["calendars"]
try:
service = build("calendar", "v3", credentials=creds)
# list all events in the gcals
count = 0
for _, gcal in gcal_map.items():
page_token = None
while True:
batch = service.new_batch_http_request()
resp = service.events().list(calendarId=gcal, maxResults=BATCH_LIMIT, pageToken=page_token).execute()
for e in resp["items"]:
print(e.get("iCalUID"))
events = resp["items"]
print(events)
for e in events:
batch.add(service.events().delete(calendarId=gcal, eventId=e["id"]), callback=batch_exception_handler)
count += len(events)
batch.execute()
page_token = resp.get("nextPageToken")
if not page_token:
break
print(f"Removed {count} gcal events")
except HttpError as error:
print(f"An error occurred: {error}")