From fc617e6829a59fc80adc1196acd35cebf270ddf4 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Mon, 28 Oct 2024 15:28:59 +0000 Subject: [PATCH] Use library for calendar merging It's functionally the same code, but could add some enhancements in future --- calmerge/calendars.py | 17 ++-- poetry.lock | 154 +++++++++++++++++++++++++++++++++--- pyproject.toml | 3 +- tests/test_calendar_view.py | 8 +- tests/test_write.py | 2 +- 5 files changed, 157 insertions(+), 27 deletions(-) diff --git a/calmerge/calendars.py b/calmerge/calendars.py index cb0bba9..67c2a94 100644 --- a/calmerge/calendars.py +++ b/calmerge/calendars.py @@ -6,11 +6,14 @@ import icalendar from aiocache import Cache from aiohttp import ClientSession +from mergecal import merge_calendars from .config import CalendarConfig fetch_cache = Cache(Cache.MEMORY, ttl=3600) +PRODID = "-//Torchbox//Calmerge//EN" + async def fetch_calendar(session: ClientSession, url: str) -> icalendar.Calendar: cache_key = "calendar_" + url @@ -25,17 +28,15 @@ async def fetch_calendar(session: ClientSession, url: str) -> icalendar.Calendar async def fetch_merged_calendar(calendar_config: CalendarConfig) -> icalendar.Calendar: - merged_calendar = icalendar.Calendar() + calendars = [] async with ClientSession() as session: - calendars = [fetch_calendar(session, str(url)) for url in calendar_config.urls] - - for coro in asyncio.as_completed(calendars): - calendar = await coro - for component in calendar.walk("VEVENT"): - merged_calendar.add_component(component) + for coro in asyncio.as_completed( + [fetch_calendar(session, str(url)) for url in calendar_config.urls] + ): + calendars.append(await coro) - return merged_calendar + return merge_calendars(calendars, prodid=PRODID) def shift_event_by_offset(event: icalendar.cal.Component, offset: timedelta) -> None: diff --git a/poetry.lock b/poetry.lock index 17c17db..ad93009 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "aiocache" @@ -185,6 +185,20 @@ tests = ["attrs[tests-no-zope]", "zope-interface"] tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" @@ -348,18 +362,21 @@ files = [ [[package]] name = "icalendar" -version = "5.0.11" +version = "6.0.1" description = "iCalendar parser/generator" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "icalendar-5.0.11-py3-none-any.whl", hash = "sha256:81864971ac43a1b7d0a555dc1b667836ce59fc719a7f845a96f2f03205fb83b9"}, - {file = "icalendar-5.0.11.tar.gz", hash = "sha256:7a298bb864526589d0de81f4b736eeb6ff9e539fefb405f7977aa5c1e201ca00"}, + {file = "icalendar-6.0.1-py3-none-any.whl", hash = "sha256:9bf3d69203bd0366a9a29a8b0e220574580b86d7918afcb628fc6920287922f3"}, + {file = "icalendar-6.0.1.tar.gz", hash = "sha256:1ff44825d7b41c3f77eac9e09cc67a770dd3c2377430c23b0eb7d91603088892"}, ] [package.dependencies] python-dateutil = "*" -pytz = "*" +tzdata = "*" + +[package.extras] +test = ["coverage", "hypothesis", "pytest", "pytz"] [[package]] name = "idna" @@ -400,6 +417,30 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + [[package]] name = "markupsafe" version = "2.1.5" @@ -469,6 +510,33 @@ files = [ {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "mergecal" +version = "0.3.6" +description = "A Python library to merge iCalendar feeds." +optional = false +python-versions = "<4.0,>=3.9" +files = [ + {file = "mergecal-0.3.6-py3-none-any.whl", hash = "sha256:fd0459fe3e5c4a82b9e17bba272798b814cf009a5324bdb9418c1c4902dd3dfb"}, + {file = "mergecal-0.3.6.tar.gz", hash = "sha256:0da61fa0512bcdaef612972bf10411606e784ba6ccf66623cae60559a69873d4"}, +] + +[package.dependencies] +icalendar = ">=6.0.0,<7.0.0" +rich = ">=10" +typer = {version = ">=0.12.0,<0.13.0", extras = ["all"]} + [[package]] name = "multidict" version = "6.0.5" @@ -761,6 +829,20 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + [[package]] name = "pytest" version = "8.1.1" @@ -851,16 +933,23 @@ files = [ six = ">=1.5" [[package]] -name = "pytz" -version = "2024.1" -description = "World timezone definitions, modern and historical" +name = "rich" +version = "13.9.3" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false -python-versions = "*" +python-versions = ">=3.8.0" files = [ - {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, - {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, + {file = "rich-13.9.3-py3-none-any.whl", hash = "sha256:9836f5096eb2172c9e77df411c1b009bace4193d6a481d534fea75ebba758283"}, + {file = "rich-13.9.3.tar.gz", hash = "sha256:bc1e01b899537598cf02579d2b9f4a415104d3fc439313a7a2c165d76557a08e"}, ] +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "ruff" version = "0.3.2" @@ -887,6 +976,17 @@ files = [ {file = "ruff-0.3.2.tar.gz", hash = "sha256:fa78ec9418eb1ca3db392811df3376b46471ae93792a81af2d1cbb0e5dcb5142"}, ] +[[package]] +name = "shellingham" +version = "1.5.4" +description = "Tool to Detect Surrounding Shell" +optional = false +python-versions = ">=3.7" +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + [[package]] name = "six" version = "1.16.0" @@ -898,6 +998,23 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "typer" +version = "0.12.5" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.7" +files = [ + {file = "typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b"}, + {file = "typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722"}, +] + +[package.dependencies] +click = ">=8.0.0" +rich = ">=10.11.0" +shellingham = ">=1.3.0" +typing-extensions = ">=3.7.4.3" + [[package]] name = "typing-extensions" version = "4.10.0" @@ -909,6 +1026,17 @@ files = [ {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, ] +[[package]] +name = "tzdata" +version = "2024.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, +] + [[package]] name = "yarl" version = "1.9.4" @@ -1015,4 +1143,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "28b6b10d94a08f82c17f6f531ef0d1969a5f66b6a67c19cecd82c3f5a52c209c" +content-hash = "99f770d7e06b0fc500232073f1a22749fac5b37c1008410f11370e8aca5a50bd" diff --git a/pyproject.toml b/pyproject.toml index 2717a3b..dc128ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,11 +10,12 @@ readme = "README.md" python = "^3.11" aiohttp = "^3.9.3" pydantic = "^2.6.4" -icalendar = "^5.0.11" +icalendar = "^6" aiocache = "^0.12.2" aiohttp-jinja2 = "^1.6" aiohttp-remotes = "^1.2.0" +mergecal = "^0.3.6" [tool.poetry.group.dev.dependencies] ruff = "^0.3.2" pytest = "^8.1.1" diff --git a/tests/test_calendar_view.py b/tests/test_calendar_view.py index 8994e70..d8aeec0 100644 --- a/tests/test_calendar_view.py +++ b/tests/test_calendar_view.py @@ -10,7 +10,7 @@ async def test_retrieves_calendars(client: TestClient) -> None: assert response.status == 200 calendar = icalendar.Calendar.from_ical(await response.text()) - assert not calendar.is_broken + assert calendar.errors == [] assert calendar["X-WR-CALNAME"] == "Python" assert calendar["X-WR-CALDESC"] == "Python EOL" @@ -37,7 +37,7 @@ async def test_requires_auth(client: TestClient) -> None: assert response.status == 200 calendar = icalendar.Calendar.from_ical(await response.text()) - assert not calendar.is_broken + assert calendar.errors == [] async def test_offset(client: TestClient) -> None: @@ -47,8 +47,8 @@ async def test_offset(client: TestClient) -> None: original_response = await client.get("/python.ics") original_calendar = icalendar.Calendar.from_ical(await original_response.text()) - assert not offset_calendar.is_broken - assert not original_calendar.is_broken + assert offset_calendar.errors == [] + assert original_calendar.errors == [] assert ( len(offset_calendar.walk("VEVENT")) == len(original_calendar.walk("VEVENT")) * 2 diff --git a/tests/test_write.py b/tests/test_write.py index f1fba38..a8c5e29 100644 --- a/tests/test_write.py +++ b/tests/test_write.py @@ -26,4 +26,4 @@ def test_write_config(tmp_path: Path, config: Config, config_path: Path) -> None assert calendar_path.is_file() calendar = icalendar.Calendar.from_ical(calendar_path.read_text()) - assert not calendar.is_broken + assert calendar.errors == []