根据icalendar RFC5545计算事件、待办事项和日志的重复时间。
项目描述
ICal有一些复杂性:事件、待办事项和日志条目可以重复,可以从源中删除,并且稍后可以编辑。此工具负责这些情况。
让我们集中我们的专业知识,构建一个可以解决这个问题的工具!
夏令时(完成)
重复事件(完成)
带编辑的重复事件(完成)
省略事件的重复事件(完成)
编辑发生在稍后的事件重复事件(完成)
普通事件(完成)
仅日期的重复(完成)
无限重复(完成)
结束重复(完成)
有起始日期但没有结束日期的事件(完成)
以日期作为起始时间的事件和以日期时间作为起始时间的事件(完成)
RRULE(完成)
具有多个RRULE的事件(完成)
RDATE(完成)
持续时间 (已完成)
EXDATE (已完成)
X-WR-TIMEZONE 兼容性 (已完成)
非公历事件重复 (待办)
RECURRENCE-ID 与 THISANDFUTURE - 修改所有未来事件 (已完成)
未包含
EXRULE (已弃用),见 8.3.2. 属性注册
安装
您可以使用 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.datetime,datetime.date,传递给 datetime.datetime 的数字元组或以 %Y%m%d(yyyymmdd)和 %Y%m%dT%H%M%SZ(yyyymmddThhmmssZ)的形式的字符串。此外,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 事件 的列表。默认情况下,所有具有重复属性的事件属性,如 UID 和 SUMMARY,都将被复制。然而,这些属性可能与原始事件不同。
DTSTART 是事件实例的开始时间。(始终存在)
DTEND 是事件实例的结束时间。(始终存在)
RDATE、EXDATE、RRULE 是创建事件重复的规则。它们不包括在重复事件中,请参阅 问题 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
架构
每个 icalendar Calendar 都可以包含事件、日记条目、待办事项和其他称为 Components 的内容。这些条目根据它们的 UID 进行分组。这样的 UID 定义了在给定时间发生的 Occurrences 的 Series。由于每个 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
已经创建了多种扩展功能的方法来覆盖内部实现。这些方法可以被子类化或组合。
下面,您可以选择收集所有组件。可以为 Series 和 Occurrence 创建子类。
>>> 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,例如 EventAdapter、JournalAdapter 或 TodoAdapter。
系列
发生
日历查询
版本修复
如果您在代码中使用此库,您可能想确保可以接收更新,但它们不会破坏您的代码。版本号是这样处理的:a.b.c 示例:0.1.12
c 在每次小错误修复时都会更改。
b 在添加新功能时更改。
a 在接口或可能破坏您代码的主要假设更改时更改。
因此,我建议修复此库的版本,以保持相同的 a,同时 b 和 c 可以更改。
开发
代码风格
在 git 提交之前,请安装 pre-commit。它将确保代码使用 ruff 按预期进行格式化和审核。
pre-commit install
测试
此项目的发展由测试驱动。测试确保了接口的一致性,并且随着时间的推移知识丢失较少。如果您想更改代码,测试有助于确保将来不会出现错误。在这方面,它们是必需的。示例代码和 ics 文件可以转移到测试中,以加快错误修复。
您可以在 测试文件夹 中查看测试。如果您有一个此库无法生成预期输出的日历 ICS 文件,您可以将其添加到 test/calendars 文件夹,并为您期望的内容编写测试。如果您愿意,可以先 打开一个问题,例如讨论更改和如何进行。
要运行测试,我们使用 tox。 tox 测试我们想要兼容的所有不同的 Python 版本。
pip3 install tox
要运行所有测试
tox
在特定 Python 版本中运行测试
tox -e py39
新版本
要发布新版本,
编辑 Changelog 部分
编辑setup.py文件,修改__version__变量
创建提交并推送
等待GitHub Actions完成构建
运行
python3 setup.py tag_and_deploy
通知有关其发布的问题
变更日志
v3.3.2
更新x-wr-timezone
v3.3.1
v3.3.0
v3.2.0
允许将datetime.timedelta作为between(absolute_time, datetime.timedelta())的第二个参数
v3.1.1
v3.1.0
添加count() > int来统计日历中的所有发生次数
添加all() > Generator[icalendar.Component]来遍历整个日历
v3.0.0
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
修复了重复RRULE导致的崩溃问题。请参阅拉取请求104
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
v0.2.4b
正确返回持续时间为0秒的事件。
between()
和at()
接受相同类型的参数。这些参数有文档说明。
v0.2.3b
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
v0.1.18b
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
开始变更日志
使用的库
python-dateutil - 使用 rrule 计算事件的重复
icalendar - 解析 ICS 文件的库
pytz - 用于时区
x-wr-timezone 用于处理非标准的 X-WR-TIMEZONE 属性。
媒体
Nicco Kunzmann 在 FOSSASIA 2022 峰会上讨论了这个库
研究
项目详情
下载文件
下载适用于您平台的文件。如果您不确定选择哪个,请了解有关安装包的更多信息。
源代码分发
构建分发
recurring_ical_events-3.3.2.tar.gz的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | d3d6252af41a6f2dc39de45b8481030b6fca5d371da775b0b22a6aef12b0075c |
|
MD5 | a77f4c23ff2d2621113251a70428a741 |
|
BLAKE2b-256 | 4a15fe2a2d440180c82cbb5eb16de0fa0477bc72b49a0a41e1fe3eac28e9fc56 |
recurring_ical_events-3.3.2-py3-none-any.whl的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | c1f5a97d354571f431ae5dfd8cf15ec2bb13564c9214b3db3bf377793b8a1831 |
|
MD5 | 266fd5a01bc52ed49eb88dd3216c8393 |
|
BLAKE2b-256 | 64601cc5532fd79d4c83d20083af9e8510d2a94c51e4197022c17a4d2113937c |