跳转到主要内容

根据icalendar RFC5545计算事件、待办事项和日志的重复时间。

项目描述

GitHub CI build and test status Python Package Version on Pypi Downloads from Pypi Support on Open Collective issues seek funding

ICal有一些复杂性:事件、待办事项和日志条目可以重复,可以从源中删除,并且稍后可以编辑。此工具负责这些情况。

让我们集中我们的专业知识,构建一个可以解决这个问题的工具!

RFC 2445 is deprecated RFC 5545 is supported RFC 7529 is not implemented RFC 7953 is not implemented
  • 夏令时(完成)

  • 重复事件(完成)

  • 带编辑的重复事件(完成)

  • 省略事件的重复事件(完成)

  • 编辑发生在稍后的事件重复事件(完成)

  • 普通事件(完成)

  • 仅日期的重复(完成)

  • 无限重复(完成)

  • 结束重复(完成)

  • 有起始日期但没有结束日期的事件(完成)

  • 以日期作为起始时间的事件和以日期时间作为起始时间的事件(完成)

  • RRULE(完成)

  • 具有多个RRULE的事件(完成)

  • RDATE(完成)

  • 持续时间 (已完成)

  • EXDATE (已完成)

  • X-WR-TIMEZONE 兼容性 (已完成)

  • 非公历事件重复 (待办)

  • RECURRENCE-ID 与 THISANDFUTURE - 修改所有未来事件 (已完成)

未包含

安装

您可以使用 pip 安装此包。

pip install 'recurring-ical-events==3.*'

在 Debian/Ubuntu 上,您使用包管理器安装 python-recurring-ical-events

sudo apt-get install python-recurring-ical-events

如果您想通过命令行或 shell 使用此功能,可以使用 ics-query

支持

我们接受捐赠以维持我们的工作,一次性或定期。请考虑捐赠资金给开源,因为每个人都能从中受益。

用法

icalendar 模块负责解析和转换日历。使用 recurring_ical_events 模块创建一个时间跨度内所有事件的重复。

要导入此模块,请编写

>>> import recurring_ical_events

您可以使用几种方法来展开重复事件,如 at(a_time)between(a_start, an_end)

示例

>>> import icalendar
>>> import recurring_ical_events
>>> from pathlib import Path

# read the calendar file and parse it
# CALENDARS = Path("to/your/calendar/directory")
>>> calendar_file : Path = CALENDARS / "fablab_cottbus.ics"
>>> ical_string = calendar_file.read_bytes()
>>> print(ical_string[:28])
BEGIN:VCALENDAR
VERSION:2.0
>>> a_calendar = icalendar.Calendar.from_ical(ical_string)

# request the events in a specific interval
# start on the 1st of January 2017 0:00
>>> start_date = (2017, 1, 1)

# the event on the 1st of January 2018 is not included
>>> end_date =   (2018,  1, 1)
>>> events = recurring_ical_events.of(a_calendar).between(start_date, end_date)
>>> for event in events:
...     start = event["DTSTART"].dt
...     summary = event["SUMMARY"]
...     print(f"start {start} summary {summary}")
start 2017-03-11 17:00:00+01:00 summary Vereinssitzung
start 2017-06-10 10:00:00+02:00 summary Repair und Recycling Café
start 2017-06-11 16:30:00+02:00 summary Brandenburger Maker-Treffen
start 2017-07-05 17:45:00+02:00 summary Der Computer-Treff fällt aus
start 2017-07-29 14:00:00+02:00 summary Sommerfest
start 2017-10-19 16:00:00+02:00 summary 3D-Modelle programmieren mit OpenSCAD
start 2017-10-20 16:00:00+02:00 summary Programmier dir deine eigene Crypto-Währung
start 2017-10-21 13:00:00+02:00 summary Programmiere deine eigene Wetterstation
start 2017-10-22 13:00:00+02:00 summary Luftqualität: Ein Workshop zum selber messen (Einsteiger)
start 2017-10-22 13:00:00+02:00 summary Websites selbst programmieren

at(a_date)

您可以得到在 a_date 举行的全部事件。日期可以是年份,例如 2023,年份的月份,例如 2023 年的 1 月(2023, 1),特定月份的某一天,例如(2023, 1, 1),小时,例如(2023, 1, 1, 0),分钟,例如(2023, 1, 1, 0, 0),甚至秒以及一个 datetime.date 对象和一个 datetime.datetime

起始和结束都是包含的。例如:如果一个事件持续多天,它仍然包含在 a_date 上。

>>> import datetime

# save the query object for the calendar
>>> query = recurring_ical_events.of(a_calendar)
>>> len(query.at(2023))                      # a year - 2023 has 12 events happening
12
>>> len(query.at((2023,)))                   # a year
12
>>> len(query.at((2023, 1)))                 # January in 2023 - only one event is in January
1
>>> len(query.at((2023, 1, 1)))              # the 1st of January in 2023
0
>>> len(query.at("20230101"))                # the 1st of January in 2023
0
>>> len(query.at((2023, 1, 1, 0)))           # the first hour of the year 2023
0
>>> len(query.at((2023, 1, 1, 0, 0)))        # the first minute in 2023
0
>>> len(query.at(datetime.date(2023, 1, 1))) # the first day in 2023
0
>>> len(query.at(datetime.date.today()))     # today
0
>>> len(query.at(datetime.datetime.now()))   # this exact second
0

结果 events 是一个 icalendar events 列表,见下文。

between(start, end)

between(start, end) 返回在开始和结束时间之间发生的事件。这两个参数可以是 datetime.datetimedatetime.date,传递给 datetime.datetime 的数字元组或以 %Y%m%dyyyymmdd)和 %Y%m%dT%H%M%SZyyyymmddThhmmssZ)的形式的字符串。此外,end 参数可以是一个 datetime.timedelta,表示结束是相对于 start 的。有关参数示例,请参阅上文的 at(a_date)

>>> query = recurring_ical_events.of(a_calendar)

# What happens in 2016, 2017 and 2018?
>>> events = recurring_ical_events.of(a_calendar).between(2016, 2019)
>>> len(events) # quite a lot is happening!
39

结果 events 是一个 icalendar events 列表,见下文。

after(earliest_end)

您可以使用 after(earliest_end) 获取在某个时间或日期之后发生的事件。在 earliest_end 期间发生的事件也包含在迭代中。

>>> earlierst_end = 2023
>>> for i, event in enumerate(query.after(earlierst_end)):
...     print(f"{event['SUMMARY']} ends {event['DTEND'].dt}") # all dates printed are after January 1st 2023
...     if i > 10: break  # we might get endless events and a lot of them!
Repair Café ends 2023-01-07 17:00:00+01:00
Repair Café ends 2023-02-04 17:00:00+01:00
Repair Café ends 2023-03-04 17:00:00+01:00
Repair Café ends 2023-04-01 17:00:00+02:00
Repair Café ends 2023-05-06 17:00:00+02:00
Repair Café ends 2023-06-03 17:00:00+02:00
Repair Café ends 2023-07-01 17:00:00+02:00
Repair Café ends 2023-08-05 17:00:00+02:00
Repair Café ends 2023-09-02 17:00:00+02:00
Repair Café ends 2023-10-07 17:00:00+02:00
Repair Café ends 2023-11-04 17:00:00+01:00
Repair Café ends 2023-12-02 17:00:00+01:00

all()

如果您想遍历所有组件的出现,可以使用 all()。由于日历可以定义大量的重复条目,此方法生成它们并忘记它们,从而减少内存开销。

此示例显示了日历中发生的第一件事

>>> first_event = next(query.all()) # not all events are generated
>>> print(f"The first event is {first_event['SUMMARY']}")
The first event is Weihnachts Repair-Café

count()

您可以使用 count() 来计算事件和其他组件的出现次数。

>>> number_of_TODOs = recurring_ical_events.of(a_calendar, components=["VTODO"]).count()
>>> print(f"You have {number_of_TODOs} things to do!")
You have 0 things to do!

>>> number_of_journal_entries = recurring_ical_events.of(a_calendar, components=["VJOURNAL"]).count()
>>> print(f"There are {number_of_journal_entries} journal entries in the calendar.")
There are 0 journal entries in the calendar.

然而,这可能会非常昂贵!

events as list - at()between()

between(start, end)at(a_date) 的结果都是一个包含 icalendar 事件 的列表。默认情况下,所有具有重复属性的事件属性,如 UIDSUMMARY,都将被复制。然而,这些属性可能与原始事件不同。

  • DTSTART 是事件实例的开始时间。(始终存在)

  • DTEND 是事件实例的结束时间。(始终存在)

  • RDATEEXDATERRULE 是创建事件重复的规则。它们不包括在重复事件中,请参阅 问题 23。要更改此,请使用 of(calendar, keep_recurrence_attributes=True)

生成器 - after()all()

当使用 after(earliest_end)all() 时,如果结果组件是有序的,则结果是返回事件的迭代器。

for event in recurring_ical_events.of(an_icalendar_object).after(datetime.datetime.now()):
    print(event["DTSTART"]) # The start is ordered

不同的组件,而不仅仅是事件

默认情况下,recurring_ical_events 只选择事件,正如其名称所暗示的。但是,日历中还有其他不同的 组件。您可以通过将 components 传递给 of 函数来选择您想要返回的组件。

of(a_calendar, components=["VEVENT"])

以下是一个选择支持组件类型的模板代码

>>> query_events = recurring_ical_events.of(a_calendar)
>>> query_journals = recurring_ical_events.of(a_calendar, components=["VJOURNAL"])
>>> query_todos = recurring_ical_events.of(a_calendar, components=["VTODO"])
>>> query_all = recurring_ical_events.of(a_calendar, components=["VTODO", "VEVENT", "VJOURNAL"])

如果这里没有列出组件类型,则可以添加。请在源代码存储库中创建一个问题。

有关进一步自定义的信息,请参阅有关如何扩展默认功能的章节。

速度

如果您多次使用 between()at(),则重新使用来自 of() 的对象会更快。

>>> query = recurring_ical_events.of(a_calendar)
>>> events_of_day_1 = query.at((2019, 2, 1))
>>> events_of_day_2 = query.at((2019, 2, 2))
>>> events_of_day_3 = query.at((2019, 2, 3))

# ... and so on

跳过格式错误的 ical 事件

某些事件可能格式错误,因此无法由 recurring-ical-events 处理。将 skip_bad_series=True 作为 of() 参数传递将完全跳过这些事件。

# Create a calendar that contains broken events.
>>> calendar_file = CALENDARS / "bad_rrule_missing_until_event.ics"
>>> calendar_with_bad_event = icalendar.Calendar.from_ical(calendar_file.read_bytes())

 # By default, broken events result in errors.
>>> recurring_ical_events.of(calendar_with_bad_event, skip_bad_series=False).count()
Traceback (most recent call last):
  ...
recurring_ical_events.BadRuleStringFormat: UNTIL parameter is missing: FREQ=WEEKLY;BYDAY=TH;WKST=SU;UNTL=20191023

# With skip_bad_series=True we skip the series that we cannot handle.
>>> recurring_ical_events.of(calendar_with_bad_event, skip_bad_series=True).count()
0

架构

Architecture Diagram showing the components interacting

每个 icalendar Calendar 都可以包含事件、日记条目、待办事项和其他称为 Components 的内容。这些条目根据它们的 UID 进行分组。这样的 UID 定义了在给定时间发生的 OccurrencesSeries。由于每个 Component 都不同,因此 ComponentAdapter 提供了一个统一的接口来与它们交互。《Calendar》被过滤,对于每个 UID,一个 Series 可以使用一个或多个 ComponentAdapters 来创建在时间间隔内发生的 Occurrences。这些 Occurrences 在内部使用,并转换为 Components 以供进一步使用。

扩展 recurring-ical-events

recurring-ical-events 的所有功能都可以扩展和修改。要了解扩展的位置,请查看 架构

扩展的第一个地方是组件集合。组件被收集到一个 Series 中。一个系列之所以在一起,是因为所有组件都有一个相同的 UID。在这个例子中,我们收集了一个匹配特定 UID 的 VEVENT。

>>> from recurring_ical_events import SelectComponents, EventAdapter, Series
>>> from icalendar.cal import Component
>>> from typing import Sequence

# create the calendar
>>> calendar_file = CALENDARS / "machbar_16_feb_2019.ics"
>>> machbar_calendar = icalendar.Calendar.from_ical(calendar_file.read_bytes())

# Create a collector of components that searches for an event with a specific UID
>>> class CollectOneUIDEvent(SelectComponents):
...     def __init__(self, uid:str) -> None:
...         self.uid = uid
...     def collect_series_from(self, source: Component, suppress_errors: tuple) -> Sequence[Series]:
...         components : list[Component] = []
...         for component in source.walk("VEVENT"):
...             if component.get("UID") == self.uid:
...                 components.append(EventAdapter(component))
...         return [Series(components)] if components else []

# collect only one UID: 4mm2ak3in2j3pllqdk1ubtbp9p@google.com
>>> one_uid = CollectOneUIDEvent("4mm2ak3in2j3pllqdk1ubtbp9p@google.com")
>>> uid_query = recurring_ical_events.of(machbar_calendar, components=[one_uid])
>>> uid_query.count()  # the event has no recurrence and thus there is only one
1

已经创建了多种扩展功能的方法来覆盖内部实现。这些方法可以被子类化或组合。

下面,您可以选择收集所有组件。可以为 SeriesOccurrence 创建子类。

>>> from recurring_ical_events import AllKnownComponents, Series, Occurrence

# we create a calendar with one event
>>> calendar_file = CALENDARS / "one_event.ics"
>>> one_event = icalendar.Calendar.from_ical(calendar_file.read_bytes())

# You can override the Occurrence and Series classes for all computable components
>>> select_all_known = AllKnownComponents(series=Series, occurrence=Occurrence)
>>> select_all_known.names  # these are the supported types of components
['VEVENT', 'VTODO', 'VJOURNAL']
>>> query_all_known = recurring_ical_events.of(one_event, components=[select_all_known])

# There should be exactly one event.
>>> query_all_known.count()
1

这个例子表明,可以扩展特定类型组件的行为。除了系列之外,您还可以更改提供统一接口的 ComponentAdapter(例如 VEVENT)。

>>> from recurring_ical_events import ComponentsWithName, EventAdapter, JournalAdapter, TodoAdapter

# You can also choose to select only specific subcomponents by their name.
# The default arguments are added to show the extensibility.
>>> select_events =   ComponentsWithName("VEVENT",   adapter=EventAdapter,   series=Series, occurrence=Occurrence)
>>> select_todos =    ComponentsWithName("VTODO",    adapter=TodoAdapter,    series=Series, occurrence=Occurrence)
>>> select_journals = ComponentsWithName("VJOURNAL", adapter=JournalAdapter, series=Series, occurrence=Occurrence)

# There should be one event happening and nothing else
>>> recurring_ical_events.of(one_event, components=[select_events]).count()
1
>>> recurring_ical_events.of(one_event, components=[select_todos]).count()
0
>>> recurring_ical_events.of(one_event, components=[select_journals]).count()
0

因此,如果您想修改查询返回的所有事件,可以通过子类化 Occurrence 类来实现。

# This occurence changes adds a new attribute to the resulting events
>>> class MyOccurrence(Occurrence):
...     """An occurrence that modifies the component."""
...     def as_component(self, keep_recurrence_attributes: bool) -> Component:
...         """Return a shallow copy of the source component and modify some attributes."""
...         component = super().as_component(keep_recurrence_attributes)
...         component["X-MY-ATTRIBUTE"] = "my occurrence"
...         return component
>>> query = recurring_ical_events.of(one_event, components=[ComponentsWithName("VEVENT", occurrence=MyOccurrence)])
>>> event = next(query.all())
>>> event["X-MY-ATTRIBUTE"]
'my occurrence'

此库允许在组件选择期间扩展功能,以便使用这些类进行计算

  • ComponentsWithName - 用于特定名称的组件

  • AllKnownComponents - 用于所有已知组件

  • SelectComponents - 提供的接口

您可以通过子类化这些进一步自定义行为

  • ComponentAdapter,例如 EventAdapterJournalAdapterTodoAdapter

  • 系列

  • 发生

  • 日历查询

版本修复

如果您在代码中使用此库,您可能想确保可以接收更新,但它们不会破坏您的代码。版本号是这样处理的:a.b.c 示例:0.1.12

  • c 在每次小错误修复时都会更改。

  • b 在添加新功能时更改。

  • a 在接口或可能破坏您代码的主要假设更改时更改。

因此,我建议修复此库的版本,以保持相同的 a,同时 bc 可以更改。

开发

代码风格

在 git 提交之前,请安装 pre-commit。它将确保代码使用 ruff 按预期进行格式化和审核。

pre-commit install

测试

此项目的发展由测试驱动。测试确保了接口的一致性,并且随着时间的推移知识丢失较少。如果您想更改代码,测试有助于确保将来不会出现错误。在这方面,它们是必需的。示例代码和 ics 文件可以转移到测试中,以加快错误修复。

您可以在 测试文件夹 中查看测试。如果您有一个此库无法生成预期输出的日历 ICS 文件,您可以将其添加到 test/calendars 文件夹,并为您期望的内容编写测试。如果您愿意,可以先 打开一个问题,例如讨论更改和如何进行。

要运行测试,我们使用 toxtox 测试我们想要兼容的所有不同的 Python 版本。

pip3 install tox

要运行所有测试

tox

在特定 Python 版本中运行测试

tox -e py39

新版本

要发布新版本,

  1. 编辑 Changelog 部分

  2. 编辑setup.py文件,修改__version__变量

  3. 创建提交并推送

  4. 等待GitHub Actions完成构建

  5. 运行

    python3 setup.py tag_and_deploy
  6. 通知有关其发布的问题

变更日志

  • v3.3.2

    • 更新x-wr-timezone

  • v3.3.1

    • 支持RDATE使用PERIOD值类型,其中结束是一个持续时间,参见PR 180

    • 支持修改未来的所有事件(RECURRENCE-ID与RANGE=THISANDFUTURE),参见问题75

  • v3.3.0

    • 使测试与icalendar版本5兼容

    • 重构README,以便用doctest进行测试

    • 从结果中移除DURATION,参见问题139

    • 记录扩展功能的新方法,参见问题133PR 175

  • v3.2.0

    • 允许将datetime.timedelta作为between(absolute_time, datetime.timedelta())的第二个参数

  • v3.1.1

    • 修复:删除具有相同序列号的修改重复,参见问题164

    • 修复:EXDATE现在排除了具有更高SEQUENCE的事件的修改实例,参见问题

  • v3.1.0

    • 添加count() > int来统计日历中的所有发生次数

    • 添加all() > Generator[icalendar.Component]来遍历整个日历

  • v3.0.0

    • 更改架构并添加图表

    • 添加类型提示,参见问题91

    • UnfoldableCalendar重命名为CalendarQuery

    • of(skip_bad_events=None)重命名为of(skip_bad_series=False)

    • of(components=[...])现在也接受ComponentAdapters

    • 修复编辑序列问题,参见问题151

  • v2.2.3

    • 修复:现在考虑RDATE和EXDATE来编辑整个事件,参见问题148

  • v2.2.2

    • 测试对icalendar==6.*的支持

    • 从测试和兼容性列表中删除Python 3.7

    • 从要求中删除pytz

  • v2.2.1

    • 添加对事件中多个RRULE的支持

  • v2.2.0

    • 添加after()方法来遍历即将发生的事件

  • v2.1.3

    • 测试并支持Python 3.12

    • 更改SPDX许可头

    • 修复COUNT为负数的RRULE,参见问题128

  • v2.1.2

  • v2.1.1

    • 声明并测试对Python 3.11的支持

    • 支持通过设置RRULE UNTIL < DTSTART来删除事件,参见问题117

  • v2.1.0

    • 添加了对RDATE中PERIOD值的支持。参见问题113

    • 修复了icalendar>=5.0.9以支持具有时区的RDATE类型PERIOD

    • 修复了pytz>=2023.3以确保兼容性

  • v2.0.2

    • 修复使用pytz时,由于UNTIL而遗漏最后事件的RRULE问题,事件开始于冬令时,结束于夏令时。请参阅问题107

  • v2.0.1

  • v2.0.0b

    • 默认只返回VEVENT。添加of(..., components=...)参数来选择应返回哪些类型的组件。请参阅问题101

    • 移除beta指示符。此库运行良好:特性请求较多,但错误报告不多。

  • v1.1.0b

  • v1.0.3b

    • 从README中移除语法异常。

    • 由于GitLab决定停止支持,切换到GitHub actions。

  • v1.0.2b

    • 添加对包含没有显式时区的事件的X-WR-TIMEZONE日历的支持,请参阅问题86

  • v1.0.1b

    • 添加对zoneinfo.ZoneInfo时区的支持,请参阅问题57

    • 从Travis CI迁移到Gitlab CI。

    • 在Gitlab上添加代码覆盖率。

  • v1.0.0b

    • 移除对Python 2的支持,请参阅问题64

    • 移除对Python 3.5和3.6的支持。

    • 注意:这些已弃用的Python版本可能仍然可以工作。我们只是不保证它们能工作。

    • 支持X-WR-TIMEZONE,请参阅问题71

  • v0.2.4b

    • 正确返回持续时间为0秒的事件。

    • between()at()接受相同类型的参数。这些参数有文档说明。

  • v0.2.3b

    • between()at()现在允许当日历事件没有时区时使用时区参数,报告在问题61问题52中。

  • v0.2.2b

    • 检查at()是否返回事件开始于下一天,请参阅问题44

  • v0.2.1b

    • 检查如果事件被修改以离开请求的时间段,则重复事件是否被删除,请参阅问题62

  • v0.2.0b

    • 添加在事件副本上保留重复属性(RRULE,RDATE,EXDATE)的能力,而不是移除它们。请参阅拉取请求54

  • v0.1.21b

    • 修复跨越夏令时边界重复的问题。请参阅问题48

  • v0.1.20b

    • 修复处理具有比其基事件序列号低的修改后重复事件的问题拉取请求45

  • v0.1.19b

    • 使用@mrx23dot的脚本进行基准测试,并通过4倍因子加速重复计算,请参阅问题42

  • v0.1.18b

    • 处理问题28,使EXDATEs符合预期。

    • 处理问题27,使解析某些rrule UNTIL值时不会崩溃。

  • v0.1.17b

    • 处理问题28,其中传递的参数导致错误。

  • 版本 0.1.16b

    • 空 RRULE 的事件将像没有 RRULE 的事件一样处理。

    • 删除固定依赖版本,见 问题 14

  • 版本 0.1.15b

    • 重复事件也包括子组件。 问题 6

  • 版本 0.1.14b

    • 修复兼容性问题 问题 20:现在支持不同时区的 EXDATE。

  • 版本 0.1.13b

    • 从重复事件中删除属性 RDATE、EXDATE、RRULE 问题 23

    • 使用 vDDDTypes 而不是显式的日期/日期时间类型 Pull Request 19

    • 开始变更日志

使用的库

媒体

Nicco Kunzmann 在 FOSSASIA 2022 峰会上讨论了这个库

Talk about this library at the FOSSASIA 2022 Summit

研究

项目详情


发布历史 发布通知 | RSS 源

下载文件

下载适用于您平台的文件。如果您不确定选择哪个,请了解有关安装包的更多信息。

源代码分发

recurring_ical_events-3.3.2.tar.gz (49.8 kB 查看哈希值)

上传时间 源代码

构建分发

recurring_ical_events-3.3.2-py3-none-any.whl (28.6 kB 查看哈希值)

上传时间 Python 3

支持者

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF 赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误记录 StatusPage StatusPage 状态页面